convolution_calculator.dart 6.0 KB

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