|
|
@@ -1,6 +1,7 @@
|
|
|
// tools/convolution_calculator.dart
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
+import '../symja.dart';
|
|
|
import 'tool_kit.dart';
|
|
|
|
|
|
/// 卷积(分布列相加):把 X、Y 两个分布列逐对组合,得到 X + Y 的分布列。
|
|
|
@@ -19,8 +20,13 @@ class ConvolutionCalculatorPage extends StatefulWidget {
|
|
|
/// 一行输入:一个取值 + 一个概率。
|
|
|
class _Row {
|
|
|
_Row(String name, int index, String v, String p)
|
|
|
- : value = ToolField('$name 取值 $index', hint: '如 0'),
|
|
|
- prob = ToolField('$name 概率 $index', hint: '如 1/2') {
|
|
|
+ : value = ToolField('$name 取值 $index', hint: '如 0'),
|
|
|
+ // 取值和概率并排一行 —— 它们本来就是一行的两个格子
|
|
|
+ prob = ToolField(
|
|
|
+ '$name 概率 $index',
|
|
|
+ hint: '如 1/2',
|
|
|
+ rowWithPrevious: true,
|
|
|
+ ) {
|
|
|
value.controller.text = v;
|
|
|
prob.controller.text = p;
|
|
|
}
|
|
|
@@ -29,8 +35,7 @@ class _Row {
|
|
|
final ToolField prob;
|
|
|
}
|
|
|
|
|
|
-class _ConvolutionCalculatorPageState
|
|
|
- extends State<ConvolutionCalculatorPage> {
|
|
|
+class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
|
|
|
// 只允许在末尾增删,行号(标签里的 1、2…)才不会错位。
|
|
|
final List<_Row> _x = [];
|
|
|
final List<_Row> _y = [];
|
|
|
@@ -55,10 +60,22 @@ class _ConvolutionCalculatorPageState
|
|
|
setState(() => rows.removeLast());
|
|
|
}
|
|
|
|
|
|
+ /// 校验某一列概率之和为 1。判等走 Symja(见调用处的注释)。
|
|
|
+ Future<void> _requireSumOne(
|
|
|
+ SymjaSession session,
|
|
|
+ String name,
|
|
|
+ String probs,
|
|
|
+ ) async {
|
|
|
+ final ok = await session.compute('Total[$probs] == 1');
|
|
|
+ if (ok.text == 'True') return;
|
|
|
+ final sum = await session.compute('Total[$probs]');
|
|
|
+ throw Exception('$name 的概率之和必须为 1,当前为 ${sum.text}');
|
|
|
+ }
|
|
|
+
|
|
|
List<ToolField> get _fields => [
|
|
|
- for (final r in _x) ...[r.value, r.prob],
|
|
|
- for (final r in _y) ...[r.value, r.prob],
|
|
|
- ];
|
|
|
+ for (final r in _x) ...[r.value, r.prob],
|
|
|
+ for (final r in _y) ...[r.value, r.prob],
|
|
|
+ ];
|
|
|
|
|
|
String _pairs(List<_Row> rows) =>
|
|
|
'{${rows.map((r) => '{${r.value.text}, ${r.prob.text}}').join(', ')}}';
|
|
|
@@ -76,96 +93,97 @@ class _ConvolutionCalculatorPageState
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) => ToolScaffold(
|
|
|
- title: '卷积分布列',
|
|
|
- fields: _fields,
|
|
|
- intro: Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
- children: [
|
|
|
- _sectionTitle('X 的分布列', 'X', _x),
|
|
|
- _sectionTitle('Y 的分布列', 'Y', _y),
|
|
|
- ],
|
|
|
- ),
|
|
|
- compute: (session) async {
|
|
|
- _validate('X', _x);
|
|
|
- _validate('Y', _y);
|
|
|
-
|
|
|
- final xPairs = _pairs(_x);
|
|
|
- final yPairs = _pairs(_y);
|
|
|
-
|
|
|
- // 概率之和必须为 1:交给 Symja 求 Total。
|
|
|
- final xSum = await session.compute('Total[${_probs(_x)}]');
|
|
|
- if (xSum.text != '1') {
|
|
|
- throw Exception('X 的概率之和必须为 1,当前为 ${xSum.text}');
|
|
|
- }
|
|
|
- final ySum = await session.compute('Total[${_probs(_y)}]');
|
|
|
- if (ySum.text != '1') {
|
|
|
- throw Exception('Y 的概率之和必须为 1,当前为 ${ySum.text}');
|
|
|
- }
|
|
|
-
|
|
|
- // 卷积:逐对组合,先不合并(Symja 给的是一对对 {取值, 概率})。
|
|
|
- final conv = await session.compute(
|
|
|
- 'With[{x=$xPairs, y=$yPairs}, '
|
|
|
- 'Flatten[Table[{x[[i,1]]+y[[j,1]], x[[i,2]]*y[[j,2]]}, '
|
|
|
- '{i,1,Length[x]}, {j,1,Length[y]}], 1]]',
|
|
|
- );
|
|
|
- final pairs = parseSymjaList(conv.text);
|
|
|
-
|
|
|
- // 按取值原文分组(纯记账)。每组的概率仍交给 Symja 相加,保证是精确分数。
|
|
|
- final order = <String>[];
|
|
|
- final groups = <String, List<String>>{};
|
|
|
- for (final item in pairs) {
|
|
|
- if (item is! List || item.length < 2) continue;
|
|
|
- final v = '${item[0]}';
|
|
|
- final p = '${item[1]}';
|
|
|
- groups.putIfAbsent(v, () {
|
|
|
+ title: '卷积分布列',
|
|
|
+ fields: _fields,
|
|
|
+ intro: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ _sectionTitle('X 的分布列', 'X', _x),
|
|
|
+ _sectionTitle('Y 的分布列', 'Y', _y),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ compute: (session) async {
|
|
|
+ _validate('X', _x);
|
|
|
+ _validate('Y', _y);
|
|
|
+
|
|
|
+ final xPairs = _pairs(_x);
|
|
|
+ final yPairs = _pairs(_y);
|
|
|
+
|
|
|
+ // 概率之和必须为 1:求和和**判等**都交给 Symja —— 概率填小数时
|
|
|
+ // `Total` 回的是 `0.9999999999999999` / `1.0` 这种,拿 Dart 字符串
|
|
|
+ // 去比 `1` 会把合法输入判成错的。
|
|
|
+ await _requireSumOne(session, 'X', _probs(_x));
|
|
|
+ await _requireSumOne(session, 'Y', _probs(_y));
|
|
|
+
|
|
|
+ // 卷积:逐对组合,先不合并(Symja 给的是一对对 {取值, 概率})。
|
|
|
+ final conv = await session.compute(
|
|
|
+ 'With[{x=$xPairs, y=$yPairs}, '
|
|
|
+ 'Flatten[Table[{x[[i,1]]+y[[j,1]], x[[i,2]]*y[[j,2]]}, '
|
|
|
+ '{i,1,Length[x]}, {j,1,Length[y]}], 1]]',
|
|
|
+ );
|
|
|
+ final pairs = parseSymjaList(conv.text);
|
|
|
+
|
|
|
+ // 按取值原文分组(纯记账)。每组的概率仍交给 Symja 相加,保证是精确分数。
|
|
|
+ final order = <String>[];
|
|
|
+ final groups = <String, List<String>>{};
|
|
|
+ for (final item in pairs) {
|
|
|
+ if (item is! List || item.length < 2) continue;
|
|
|
+ final v = '${item[0]}';
|
|
|
+ final p = '${item[1]}';
|
|
|
+ groups
|
|
|
+ .putIfAbsent(v, () {
|
|
|
order.add(v);
|
|
|
return <String>[];
|
|
|
- }).add(p);
|
|
|
- }
|
|
|
-
|
|
|
- // 排序也交给 Symja:Sort 按数值大小排,比 Dart 的字符串排序可靠
|
|
|
- // (字符串排序会把 10 排在 2 前面)。取值不多,单独问一次开销很小。
|
|
|
- final sorted = order.isEmpty
|
|
|
- ? const <Object>[]
|
|
|
- : parseSymjaList(
|
|
|
- (await session.compute('Sort[{${order.join(', ')}}]')).text);
|
|
|
-
|
|
|
- final outputs = <ToolOutput>[];
|
|
|
- final groupSums = <String>[];
|
|
|
- for (final v in sorted) {
|
|
|
- final key = '$v';
|
|
|
- final ps = groups[key];
|
|
|
- if (ps == null) continue;
|
|
|
- final total = await session.compute('Total[{${ps.join(', ')}}]');
|
|
|
- groupSums.add(total.text);
|
|
|
- outputs.add(ToolText('X + Y = $key', total.text));
|
|
|
- }
|
|
|
-
|
|
|
- final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
|
|
|
- outputs.add(ToolText('概率总和', grand.text));
|
|
|
- return outputs;
|
|
|
- },
|
|
|
- );
|
|
|
+ })
|
|
|
+ .add(p);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序也交给 Symja:Sort 按数值大小排,比 Dart 的字符串排序可靠
|
|
|
+ // (字符串排序会把 10 排在 2 前面)。取值不多,单独问一次开销很小。
|
|
|
+ final sorted = order.isEmpty
|
|
|
+ ? const <Object>[]
|
|
|
+ : parseSymjaList(
|
|
|
+ (await session.compute('Sort[{${order.join(', ')}}]')).text,
|
|
|
+ );
|
|
|
+
|
|
|
+ final outputs = <ToolOutput>[];
|
|
|
+ final groupSums = <String>[];
|
|
|
+ for (final v in sorted) {
|
|
|
+ final key = '$v';
|
|
|
+ final ps = groups[key];
|
|
|
+ if (ps == null) continue;
|
|
|
+ final total = await session.compute('Total[{${ps.join(', ')}}]');
|
|
|
+ groupSums.add(total.text);
|
|
|
+ outputs.add(ToolText('X + Y = $key', total.text));
|
|
|
+ }
|
|
|
+
|
|
|
+ final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
|
|
|
+ outputs.add(ToolText('概率总和', grand.text));
|
|
|
+ return outputs;
|
|
|
+ },
|
|
|
+ );
|
|
|
|
|
|
Widget _sectionTitle(String title, String name, List<_Row> rows) => Padding(
|
|
|
- padding: const EdgeInsets.only(top: 8),
|
|
|
- child: Row(
|
|
|
- children: [
|
|
|
- Expanded(
|
|
|
- child: Text(title,
|
|
|
- style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
|
- ),
|
|
|
- TextButton.icon(
|
|
|
- onPressed: () => _removeRow(rows),
|
|
|
- icon: const Icon(Icons.remove, size: 18),
|
|
|
- label: const Text('删除一行'),
|
|
|
- ),
|
|
|
- TextButton.icon(
|
|
|
- onPressed: () => _addRow(name, rows),
|
|
|
- icon: const Icon(Icons.add, size: 18),
|
|
|
- label: const Text('添加一行'),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ padding: const EdgeInsets.only(top: 8),
|
|
|
+ child: Row(
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ title,
|
|
|
+ style: const TextStyle(fontWeight: FontWeight.bold),
|
|
|
+ ),
|
|
|
),
|
|
|
- );
|
|
|
+ TextButton.icon(
|
|
|
+ onPressed: () => _removeRow(rows),
|
|
|
+ icon: const Icon(Icons.remove, size: 18),
|
|
|
+ label: const Text('删除一行'),
|
|
|
+ ),
|
|
|
+ TextButton.icon(
|
|
|
+ onPressed: () => _addRow(name, rows),
|
|
|
+ icon: const Icon(Icons.add, size: 18),
|
|
|
+ label: const Text('添加一行'),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
}
|