convolution_calculator.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // tools/convolution_calculator.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:rational/rational.dart';
  4. class ConvolutionCalculatorPage extends StatefulWidget {
  5. @override
  6. _ConvolutionCalculatorPageState createState() => _ConvolutionCalculatorPageState();
  7. }
  8. class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
  9. final List<TextEditingController> _xValueControllers = [];
  10. final List<TextEditingController> _xProbControllers = [];
  11. final List<TextEditingController> _yValueControllers = [];
  12. final List<TextEditingController> _yProbControllers = [];
  13. List<Map<String, Rational>> _convolutionResult = [];
  14. bool _showResult = false;
  15. @override
  16. void initState() {
  17. super.initState();
  18. // 初始化默认数据
  19. _initializeDefaultData();
  20. }
  21. void _initializeDefaultData() {
  22. // 添加X的默认数据
  23. _addXRow(value: '0', prob: '1/2');
  24. _addXRow(value: '1', prob: '1/2');
  25. // 添加Y的默认数据
  26. _addYRow(value: '0', prob: '1/3');
  27. _addYRow(value: '1', prob: '1/3');
  28. _addYRow(value: '2', prob: '1/3');
  29. }
  30. void _addXRow({String value = '', String prob = ''}) {
  31. setState(() {
  32. _xValueControllers.add(TextEditingController(text: value));
  33. _xProbControllers.add(TextEditingController(text: prob));
  34. });
  35. }
  36. void _removeXRow(int index) {
  37. setState(() {
  38. _xValueControllers.removeAt(index);
  39. _xProbControllers.removeAt(index);
  40. });
  41. }
  42. void _addYRow({String value = '', String prob = ''}) {
  43. setState(() {
  44. _yValueControllers.add(TextEditingController(text: value));
  45. _yProbControllers.add(TextEditingController(text: prob));
  46. });
  47. }
  48. void _removeYRow(int index) {
  49. setState(() {
  50. _yValueControllers.removeAt(index);
  51. _yProbControllers.removeAt(index);
  52. });
  53. }
  54. Rational _parseRational(String input) {
  55. try {
  56. if (input.contains('/')) {
  57. List<String> parts = input.split('/');
  58. if (parts.length == 2) {
  59. return Rational.parse(parts[0].trim()) / Rational.parse(parts[1].trim());
  60. }
  61. }
  62. return Rational.parse(input.trim());
  63. } catch (e) {
  64. throw FormatException('无效的概率格式: $input');
  65. }
  66. }
  67. void _calculateConvolution() {
  68. try {
  69. // 解析X分布
  70. Map<Rational, Rational> xDistribution = {};
  71. Rational xSum = Rational.zero;
  72. for (int i = 0; i < _xValueControllers.length; i++) {
  73. Rational value = _parseRational(_xValueControllers[i].text);
  74. Rational prob = _parseRational(_xProbControllers[i].text);
  75. xDistribution[value] = prob;
  76. xSum += prob;
  77. }
  78. // 解析Y分布
  79. Map<Rational, Rational> yDistribution = {};
  80. Rational ySum = Rational.zero;
  81. for (int i = 0; i < _yValueControllers.length; i++) {
  82. Rational value = _parseRational(_yValueControllers[i].text);
  83. Rational prob = _parseRational(_yProbControllers[i].text);
  84. yDistribution[value] = prob;
  85. ySum += prob;
  86. }
  87. // 验证概率和是否为1
  88. if (xSum != Rational.one) {
  89. throw FormatException('X的概率和必须为1,当前为: $xSum');
  90. }
  91. if (ySum != Rational.one) {
  92. throw FormatException('Y的概率和必须为1,当前为: $ySum');
  93. }
  94. // 计算卷积
  95. Map<Rational, Rational> convolution = {};
  96. for (var xEntry in xDistribution.entries) {
  97. for (var yEntry in yDistribution.entries) {
  98. Rational sumValue = xEntry.key + yEntry.key;
  99. Rational prob = xEntry.value * yEntry.value;
  100. convolution[sumValue] = (convolution[sumValue] ?? Rational.zero) + prob;
  101. }
  102. }
  103. // 转换为列表并排序
  104. _convolutionResult = convolution.entries
  105. .map((entry) => {'value': entry.key, 'probability': entry.value})
  106. .toList();
  107. _convolutionResult.sort((a, b) => a['value']!.compareTo(b['value']!));
  108. setState(() {
  109. _showResult = true;
  110. });
  111. } catch (e) {
  112. _showErrorDialog(e.toString());
  113. }
  114. }
  115. void _showErrorDialog(String message) {
  116. showDialog(
  117. context: context,
  118. builder: (context) => AlertDialog(
  119. title: Text('计算错误'),
  120. content: Text(message),
  121. actions: [
  122. TextButton(
  123. onPressed: () => Navigator.of(context).pop(),
  124. child: Text('确定'),
  125. ),
  126. ],
  127. ),
  128. );
  129. }
  130. void _clearAll() {
  131. setState(() {
  132. _xValueControllers.clear();
  133. _xProbControllers.clear();
  134. _yValueControllers.clear();
  135. _yProbControllers.clear();
  136. _convolutionResult.clear();
  137. _showResult = false;
  138. _initializeDefaultData();
  139. });
  140. }
  141. @override
  142. void dispose() {
  143. for (var controller in _xValueControllers) {
  144. controller.dispose();
  145. }
  146. for (var controller in _xProbControllers) {
  147. controller.dispose();
  148. }
  149. for (var controller in _yValueControllers) {
  150. controller.dispose();
  151. }
  152. for (var controller in _yProbControllers) {
  153. controller.dispose();
  154. }
  155. super.dispose();
  156. }
  157. @override
  158. Widget build(BuildContext context) {
  159. return Scaffold(
  160. appBar: AppBar(
  161. title: Text('卷积分布列计算'),
  162. backgroundColor: Colors.white,
  163. elevation: 1,
  164. actions: [
  165. IconButton(
  166. icon: Icon(Icons.clear_all),
  167. onPressed: _clearAll,
  168. tooltip: '清空所有',
  169. ),
  170. ],
  171. ),
  172. body: SingleChildScrollView(
  173. padding: EdgeInsets.all(16),
  174. child: Column(
  175. crossAxisAlignment: CrossAxisAlignment.start,
  176. children: [
  177. _buildDistributionInput('随机变量 X 的分布列', _xValueControllers, _xProbControllers, _addXRow, _removeXRow),
  178. SizedBox(height: 24),
  179. _buildDistributionInput('随机变量 Y 的分布列', _yValueControllers, _yProbControllers, _addYRow, _removeYRow),
  180. SizedBox(height: 24),
  181. Center(
  182. child: ElevatedButton.icon(
  183. onPressed: _calculateConvolution,
  184. icon: Icon(Icons.calculate),
  185. label: Text('计算卷积分布列'),
  186. style: ElevatedButton.styleFrom(
  187. padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  188. ),
  189. ),
  190. ),
  191. SizedBox(height: 24),
  192. if (_showResult) _buildResultTable(),
  193. ],
  194. ),
  195. ),
  196. );
  197. }
  198. Widget _buildDistributionInput(
  199. String title,
  200. List<TextEditingController> valueControllers,
  201. List<TextEditingController> probControllers,
  202. VoidCallback onAdd,
  203. Function(int) onRemove,
  204. ) {
  205. return Card(
  206. elevation: 2,
  207. child: Padding(
  208. padding: EdgeInsets.all(16),
  209. child: Column(
  210. crossAxisAlignment: CrossAxisAlignment.start,
  211. children: [
  212. Text(
  213. title,
  214. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  215. ),
  216. SizedBox(height: 16),
  217. _buildTableHeader(),
  218. SizedBox(height: 8),
  219. ...List.generate(valueControllers.length, (index) {
  220. return _buildInputRow(
  221. valueControllers[index],
  222. probControllers[index],
  223. index,
  224. onRemove,
  225. valueControllers.length,
  226. );
  227. }),
  228. SizedBox(height: 16),
  229. Center(
  230. child: OutlinedButton.icon(
  231. onPressed: onAdd,
  232. icon: Icon(Icons.add, size: 18),
  233. label: Text('添加一行'),
  234. ),
  235. ),
  236. ],
  237. ),
  238. ),
  239. );
  240. }
  241. Widget _buildTableHeader() {
  242. return Row(
  243. children: [
  244. Expanded(
  245. flex: 2,
  246. child: Text('取值', style: TextStyle(fontWeight: FontWeight.bold)),
  247. ),
  248. SizedBox(width: 16),
  249. Expanded(
  250. flex: 3,
  251. child: Text('概率 (支持分数如 1/2)', style: TextStyle(fontWeight: FontWeight.bold)),
  252. ),
  253. SizedBox(width: 16),
  254. Expanded(
  255. flex: 1,
  256. child: Text('操作', style: TextStyle(fontWeight: FontWeight.bold)),
  257. ),
  258. ],
  259. );
  260. }
  261. Widget _buildInputRow(
  262. TextEditingController valueController,
  263. TextEditingController probController,
  264. int index,
  265. Function(int) onRemove,
  266. int totalRows,
  267. ) {
  268. return Padding(
  269. padding: EdgeInsets.symmetric(vertical: 8),
  270. child: Row(
  271. children: [
  272. Expanded(
  273. flex: 2,
  274. child: TextField(
  275. controller: valueController,
  276. decoration: InputDecoration(
  277. hintText: '如: 0, 1, 2...',
  278. border: OutlineInputBorder(),
  279. contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  280. ),
  281. keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
  282. ),
  283. ),
  284. SizedBox(width: 16),
  285. Expanded(
  286. flex: 3,
  287. child: TextField(
  288. controller: probController,
  289. decoration: InputDecoration(
  290. hintText: '如: 1/2, 0.25, 0.3',
  291. border: OutlineInputBorder(),
  292. contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  293. ),
  294. ),
  295. ),
  296. SizedBox(width: 16),
  297. Expanded(
  298. flex: 1,
  299. child: IconButton(
  300. icon: Icon(Icons.remove_circle, color: Colors.red),
  301. onPressed: totalRows > 1 ? () => onRemove(index) : null,
  302. tooltip: '删除此行',
  303. ),
  304. ),
  305. ],
  306. ),
  307. );
  308. }
  309. Widget _buildResultTable() {
  310. return Card(
  311. elevation: 2,
  312. color: Colors.green[50],
  313. child: Padding(
  314. padding: EdgeInsets.all(16),
  315. child: Column(
  316. crossAxisAlignment: CrossAxisAlignment.start,
  317. children: [
  318. Text(
  319. '卷积分布列 X + Y',
  320. style: TextStyle(
  321. fontSize: 18,
  322. fontWeight: FontWeight.bold,
  323. color: Colors.green[800],
  324. ),
  325. ),
  326. SizedBox(height: 16),
  327. _buildResultTableHeader(),
  328. ..._convolutionResult.map(_buildResultRow).toList(),
  329. SizedBox(height: 16),
  330. _buildProbabilitySum(),
  331. ],
  332. ),
  333. ),
  334. );
  335. }
  336. Widget _buildResultTableHeader() {
  337. return Container(
  338. padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
  339. decoration: BoxDecoration(
  340. color: Colors.green[100],
  341. borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
  342. ),
  343. child: Row(
  344. children: [
  345. Expanded(
  346. child: Text('X + Y 取值', style: TextStyle(fontWeight: FontWeight.bold)),
  347. ),
  348. Expanded(
  349. child: Text('概率 (分数)', style: TextStyle(fontWeight: FontWeight.bold)),
  350. ),
  351. Expanded(
  352. child: Text('概率 (小数)', style: TextStyle(fontWeight: FontWeight.bold)),
  353. ),
  354. ],
  355. ),
  356. );
  357. }
  358. Widget _buildResultRow(Map<String, Rational> entry) {
  359. Rational value = entry['value']!;
  360. Rational probability = entry['probability']!;
  361. double decimalProb = probability.toDouble();
  362. return Container(
  363. padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
  364. decoration: BoxDecoration(
  365. border: Border(bottom: BorderSide(color: Colors.green[100]!)),
  366. ),
  367. child: Row(
  368. children: [
  369. Expanded(
  370. child: Text(
  371. value.toString(),
  372. style: TextStyle(fontSize: 16),
  373. ),
  374. ),
  375. Expanded(
  376. child: Text(
  377. _toFractionString(probability),
  378. style: TextStyle(fontSize: 16, fontFamily: 'Monospace'),
  379. ),
  380. ),
  381. Expanded(
  382. child: Text(
  383. decimalProb.toStringAsFixed(6),
  384. style: TextStyle(fontSize: 16, fontFamily: 'Monospace'),
  385. ),
  386. ),
  387. ],
  388. ),
  389. );
  390. }
  391. String _toFractionString(Rational rational) {
  392. if (rational.denominator == BigInt.one) {
  393. return rational.numerator.toString();
  394. }
  395. return '${rational.numerator}/${rational.denominator}';
  396. }
  397. Widget _buildProbabilitySum() {
  398. Rational total = _convolutionResult.fold(
  399. Rational.zero,
  400. (sum, entry) => sum + entry['probability']!,
  401. );
  402. return Container(
  403. padding: EdgeInsets.all(12),
  404. decoration: BoxDecoration(
  405. color: Colors.green[100],
  406. borderRadius: BorderRadius.circular(8),
  407. ),
  408. child: Row(
  409. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  410. children: [
  411. Text('概率总和:', style: TextStyle(fontWeight: FontWeight.bold)),
  412. Text(
  413. '${_toFractionString(total)} = ${total.toDouble().toStringAsFixed(6)}',
  414. style: TextStyle(fontWeight: FontWeight.bold, fontFamily: 'Monospace'),
  415. ),
  416. ],
  417. ),
  418. );
  419. }
  420. }