| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- // tools/convolution_calculator.dart
- import 'package:flutter/material.dart';
- import '../symja.dart';
- import 'tool_kit.dart';
- /// 卷积(分布列相加):把 X、Y 两个分布列逐对组合,得到 X + Y 的分布列。
- ///
- /// 行数可变,输入框在 State 里按行维护(每次 setState 生成一份新的 fields 给外壳)。
- /// 概率全部交给 Symja:两两相乘、合并同取值时求和都是 Symja 算的精确分数,
- /// Dart 只在最后把"取值相同"的项归到一组(分组是记账,不是算数)。
- class ConvolutionCalculatorPage extends StatefulWidget {
- const ConvolutionCalculatorPage({super.key});
- @override
- State<ConvolutionCalculatorPage> createState() =>
- _ConvolutionCalculatorPageState();
- }
- /// 一行输入:一个取值 + 一个概率。
- class _Row {
- _Row(String name, int index, String v, String p)
- : value = ToolField('$name 取值 $index', hint: '如 0'),
- // 取值和概率并排一行 —— 它们本来就是一行的两个格子
- prob = ToolField(
- '$name 概率 $index',
- hint: '如 1/2',
- rowWithPrevious: true,
- ) {
- value.controller.text = v;
- prob.controller.text = p;
- }
- final ToolField value;
- final ToolField prob;
- }
- class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
- // 只允许在末尾增删,行号(标签里的 1、2…)才不会错位。
- final List<_Row> _x = [];
- final List<_Row> _y = [];
- @override
- void initState() {
- super.initState();
- // 默认 X = (0, 1/2), (1, 1/2);Y = (0, 1/3), (1, 1/3), (2, 1/3)。
- _x.add(_Row('X', 1, '0', '1/2'));
- _x.add(_Row('X', 2, '1', '1/2'));
- _y.add(_Row('Y', 1, '0', '1/3'));
- _y.add(_Row('Y', 2, '1', '1/3'));
- _y.add(_Row('Y', 3, '2', '1/3'));
- }
- void _addRow(String name, List<_Row> rows) {
- setState(() => rows.add(_Row(name, rows.length + 1, '', '')));
- }
- void _removeRow(List<_Row> rows) {
- if (rows.length <= 1) return;
- 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],
- ];
- String _pairs(List<_Row> rows) =>
- '{${rows.map((r) => '{${r.value.text}, ${r.prob.text}}').join(', ')}}';
- String _probs(List<_Row> rows) =>
- '{${rows.map((r) => r.prob.text).join(', ')}}';
- void _validate(String name, List<_Row> rows) {
- for (var i = 0; i < rows.length; i++) {
- if (rows[i].value.text.isEmpty || rows[i].prob.text.isEmpty) {
- throw Exception('$name 的第 ${i + 1} 行还有空栏,请把取值和概率都填上');
- }
- }
- }
- @override
- Widget build(BuildContext context) => ToolScaffold(
- title: '卷积分布列',
- fields: _fields,
- // 增删按钮贴着**自己那一组**:X 的按钮在 X 那组上面,Y 的在 Y 那组上面。
- // Y 第一个输入框的下标 = X 的行数 × 2(每行两个框)。
- beforeField: <int, Widget>{
- 0: _rowButtons('X', _x),
- _x.length * 2: _rowButtons('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(', ')}}]');
- final totalN = await session.compute(
- approxExpr(
- 'Rationalize[Total[{${ps.join(', ')}}]]',
- session.approximateDigits,
- ),
- );
- groupSums.add(total.text);
- // 精确分数 + 小数:概率这种"看一眼量级"的值,两者都有用
- outputs.add(
- ToolText('X + Y = $key', withApprox(total.text, totalN.text)),
- );
- }
- final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
- final grandN = await session.compute(
- 'N[Rationalize[Total[{${groupSums.join(', ')}}]], '
- '${session.approximateDigits}]',
- );
- outputs.add(ToolText('概率总和', withApprox(grand.text, grandN.text)));
- return outputs;
- },
- );
- /// 一行「删除一行 / 添加一行」—— 输入框自己的标签已经写明了是 X 还是 Y,
- /// 不用再来一行标题重复。
- Widget _rowButtons(String name, List<_Row> rows) => Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- 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('添加一行'),
- ),
- ],
- ),
- );
- }
|