convolution_calculator.dart 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // tools/convolution_calculator.dart
  2. import 'package:flutter/material.dart';
  3. import '../symja.dart';
  4. import 'tool_kit.dart';
  5. /// 卷积(分布列相加):把 X、Y 两个分布列逐对组合,得到 X + Y 的分布列。
  6. ///
  7. /// 行数可变,输入框在 State 里按行维护(每次 setState 生成一份新的 fields 给外壳)。
  8. /// 概率全部交给 Symja:两两相乘、合并同取值时求和都是 Symja 算的精确分数,
  9. /// Dart 只在最后把"取值相同"的项归到一组(分组是记账,不是算数)。
  10. class ConvolutionCalculatorPage extends StatefulWidget {
  11. const ConvolutionCalculatorPage({super.key});
  12. @override
  13. State<ConvolutionCalculatorPage> createState() =>
  14. _ConvolutionCalculatorPageState();
  15. }
  16. /// 一行输入:一个取值 + 一个概率。
  17. class _Row {
  18. _Row(String name, int index, String v, String p)
  19. : value = ToolField('$name 取值 $index', hint: '如 0'),
  20. // 取值和概率并排一行 —— 它们本来就是一行的两个格子
  21. prob = ToolField(
  22. '$name 概率 $index',
  23. hint: '如 1/2',
  24. rowWithPrevious: true,
  25. ) {
  26. value.controller.text = v;
  27. prob.controller.text = p;
  28. }
  29. final ToolField value;
  30. final ToolField prob;
  31. }
  32. class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
  33. // 只允许在末尾增删,行号(标签里的 1、2…)才不会错位。
  34. final List<_Row> _x = [];
  35. final List<_Row> _y = [];
  36. @override
  37. void initState() {
  38. super.initState();
  39. // 默认 X = (0, 1/2), (1, 1/2);Y = (0, 1/3), (1, 1/3), (2, 1/3)。
  40. _x.add(_Row('X', 1, '0', '1/2'));
  41. _x.add(_Row('X', 2, '1', '1/2'));
  42. _y.add(_Row('Y', 1, '0', '1/3'));
  43. _y.add(_Row('Y', 2, '1', '1/3'));
  44. _y.add(_Row('Y', 3, '2', '1/3'));
  45. }
  46. void _addRow(String name, List<_Row> rows) {
  47. setState(() => rows.add(_Row(name, rows.length + 1, '', '')));
  48. }
  49. void _removeRow(List<_Row> rows) {
  50. if (rows.length <= 1) return;
  51. setState(() => rows.removeLast());
  52. }
  53. /// 校验某一列概率之和为 1。判等走 Symja(见调用处的注释)。
  54. Future<void> _requireSumOne(
  55. SymjaSession session,
  56. String name,
  57. String probs,
  58. ) async {
  59. final ok = await session.compute('Total[$probs] == 1');
  60. if (ok.text == 'True') return;
  61. final sum = await session.compute('Total[$probs]');
  62. throw Exception('$name 的概率之和必须为 1,当前为 ${sum.text}');
  63. }
  64. List<ToolField> get _fields => [
  65. for (final r in _x) ...[r.value, r.prob],
  66. for (final r in _y) ...[r.value, r.prob],
  67. ];
  68. String _pairs(List<_Row> rows) =>
  69. '{${rows.map((r) => '{${r.value.text}, ${r.prob.text}}').join(', ')}}';
  70. String _probs(List<_Row> rows) =>
  71. '{${rows.map((r) => r.prob.text).join(', ')}}';
  72. void _validate(String name, List<_Row> rows) {
  73. for (var i = 0; i < rows.length; i++) {
  74. if (rows[i].value.text.isEmpty || rows[i].prob.text.isEmpty) {
  75. throw Exception('$name 的第 ${i + 1} 行还有空栏,请把取值和概率都填上');
  76. }
  77. }
  78. }
  79. @override
  80. Widget build(BuildContext context) => ToolScaffold(
  81. title: '卷积分布列',
  82. fields: _fields,
  83. intro: Column(
  84. crossAxisAlignment: CrossAxisAlignment.start,
  85. children: [_rowButtons('X', _x), _rowButtons('Y', _y)],
  86. ),
  87. compute: (session) async {
  88. _validate('X', _x);
  89. _validate('Y', _y);
  90. final xPairs = _pairs(_x);
  91. final yPairs = _pairs(_y);
  92. // 概率之和必须为 1:求和和**判等**都交给 Symja —— 概率填小数时
  93. // `Total` 回的是 `0.9999999999999999` / `1.0` 这种,拿 Dart 字符串
  94. // 去比 `1` 会把合法输入判成错的。
  95. await _requireSumOne(session, 'X', _probs(_x));
  96. await _requireSumOne(session, 'Y', _probs(_y));
  97. // 卷积:逐对组合,先不合并(Symja 给的是一对对 {取值, 概率})。
  98. final conv = await session.compute(
  99. 'With[{x=$xPairs, y=$yPairs}, '
  100. 'Flatten[Table[{x[[i,1]]+y[[j,1]], x[[i,2]]*y[[j,2]]}, '
  101. '{i,1,Length[x]}, {j,1,Length[y]}], 1]]',
  102. );
  103. final pairs = parseSymjaList(conv.text);
  104. // 按取值原文分组(纯记账)。每组的概率仍交给 Symja 相加,保证是精确分数。
  105. final order = <String>[];
  106. final groups = <String, List<String>>{};
  107. for (final item in pairs) {
  108. if (item is! List || item.length < 2) continue;
  109. final v = '${item[0]}';
  110. final p = '${item[1]}';
  111. groups
  112. .putIfAbsent(v, () {
  113. order.add(v);
  114. return <String>[];
  115. })
  116. .add(p);
  117. }
  118. // 排序也交给 Symja:Sort 按数值大小排,比 Dart 的字符串排序可靠
  119. // (字符串排序会把 10 排在 2 前面)。取值不多,单独问一次开销很小。
  120. final sorted = order.isEmpty
  121. ? const <Object>[]
  122. : parseSymjaList(
  123. (await session.compute('Sort[{${order.join(', ')}}]')).text,
  124. );
  125. final outputs = <ToolOutput>[];
  126. final groupSums = <String>[];
  127. for (final v in sorted) {
  128. final key = '$v';
  129. final ps = groups[key];
  130. if (ps == null) continue;
  131. final total = await session.compute('Total[{${ps.join(', ')}}]');
  132. final totalN = await session.compute(
  133. approxExpr(
  134. 'Rationalize[Total[{${ps.join(', ')}}]]',
  135. session.approximateDigits,
  136. ),
  137. );
  138. groupSums.add(total.text);
  139. // 精确分数 + 小数:概率这种"看一眼量级"的值,两者都有用
  140. outputs.add(
  141. ToolText('X + Y = $key', withApprox(total.text, totalN.text)),
  142. );
  143. }
  144. final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
  145. final grandN = await session.compute(
  146. 'N[Rationalize[Total[{${groupSums.join(', ')}}]], '
  147. '${session.approximateDigits}]',
  148. );
  149. outputs.add(ToolText('概率总和', withApprox(grand.text, grandN.text)));
  150. return outputs;
  151. },
  152. );
  153. /// 一行「删除一行 / 添加一行」—— 输入框自己的标签已经写明了是 X 还是 Y,
  154. /// 不用再来一行标题重复。
  155. Widget _rowButtons(String name, List<_Row> rows) => Padding(
  156. padding: const EdgeInsets.only(top: 8),
  157. child: Row(
  158. mainAxisAlignment: MainAxisAlignment.end,
  159. children: [
  160. TextButton.icon(
  161. onPressed: () => _removeRow(rows),
  162. icon: const Icon(Icons.remove, size: 18),
  163. label: const Text('删除一行'),
  164. ),
  165. TextButton.icon(
  166. onPressed: () => _addRow(name, rows),
  167. icon: const Icon(Icons.add, size: 18),
  168. label: const Text('添加一行'),
  169. ),
  170. ],
  171. ),
  172. );
  173. }