mdbench 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. mdbench-0.1.0.dist-info/METADATA +237 -0
  2. mdbench-0.1.0.dist-info/RECORD +57 -0
  3. mdbench-0.1.0.dist-info/WHEEL +5 -0
  4. mdbench-0.1.0.dist-info/entry_points.txt +2 -0
  5. mdbench-0.1.0.dist-info/licenses/LICENSE +0 -0
  6. mdbench-0.1.0.dist-info/top_level.txt +1 -0
  7. src/cli/run.py +69 -0
  8. src/core/__init__.py +8 -0
  9. src/core/problem.py +81 -0
  10. src/core/solution.py +25 -0
  11. src/evaluate_result.py +690 -0
  12. src/features/__init__.py +21 -0
  13. src/features/answer.py +45 -0
  14. src/features/evaluation/__init__.py +9 -0
  15. src/features/evaluation/evaluation_package.py +103 -0
  16. src/features/evaluation/mechanism_trace.py +162 -0
  17. src/features/io/__init__.py +10 -0
  18. src/features/io/load_problem.py +160 -0
  19. src/features/io/load_submission.py +154 -0
  20. src/features/io/solve_mechanism_equations.py +281 -0
  21. src/features/sampling/__init__.py +5 -0
  22. src/features/sampling/range_inferrer.py +45 -0
  23. src/features/units/__init__.py +5 -0
  24. src/features/units/unit_inference.py +130 -0
  25. src/features/validation/__init__.py +14 -0
  26. src/features/validation/mechanism_derivation.py +86 -0
  27. src/features/validation/mechanism_fundamentality.py +208 -0
  28. src/features/visualization/__init__.py +5 -0
  29. src/features/visualization/mechanism_graph.py +258 -0
  30. src/metrics/__init__.py +20 -0
  31. src/metrics/formula_similarity.py +46 -0
  32. src/metrics/hybrid_formula_similarity.py +232 -0
  33. src/metrics/mechanism_fundamentality.py +116 -0
  34. src/metrics/mechanism_similarity.py +225 -0
  35. src/metrics/mechanism_simplicity.py +34 -0
  36. src/prepare_problem.py +280 -0
  37. src/synthetic_data.py +247 -0
  38. src/utils/__init__.py +8 -0
  39. src/utils/console.py +15 -0
  40. src/utils/lazy_loader.py +37 -0
  41. src/utils/llm/__init__.py +22 -0
  42. src/utils/llm/core.py +69 -0
  43. src/utils/llm/deepseek_api.py +111 -0
  44. src/utils/llm/gemini_api.py +99 -0
  45. src/utils/llm/llm_api.py +159 -0
  46. src/utils/llm/manual_api.py +56 -0
  47. src/utils/llm/openai_api.py +303 -0
  48. src/utils/llm/openrouter_api.py +141 -0
  49. src/utils/llm/siliconflow_api.py +175 -0
  50. src/utils/llm/tool_call_mixin.py +80 -0
  51. src/utils/log_exception.py +12 -0
  52. src/utils/logger.py +330 -0
  53. src/utils/path_utils.py +28 -0
  54. src/utils/tag2ansi.py +165 -0
  55. src/utils/unit_parser.py +48 -0
  56. src/validate_problem.py +344 -0
  57. src/visualize_mechanism.py +48 -0
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdbench
3
+ Version: 0.1.0
4
+ Summary: A benchmark for discovering scientific mechanisms
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: numpy>=1.24
9
+ Requires-Dist: PyYAML>=6.0
10
+ Requires-Dist: nd2py>=3.2.3
11
+ Requires-Dist: openai>=1.0
12
+ Requires-Dist: google-genai>=1.0
13
+ Requires-Dist: requests>=2.28
14
+ Requires-Dist: python-dotenv>=1.0
15
+ Requires-Dist: sympy>=1.13
16
+ Requires-Dist: scipy>=1.14
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == "dev"
19
+ Requires-Dist: sphinx>=7.0; extra == "dev"
20
+ Requires-Dist: sphinx-book-theme>=1.1; extra == "dev"
21
+ Requires-Dist: myst-parser>=3.0; extra == "dev"
22
+ Requires-Dist: sphinx-autodoc-typehints>=2.0; extra == "dev"
23
+ Requires-Dist: build>=1.2; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # MDBench
27
+
28
+ [简体中文](README.zh-CN.md)
29
+
30
+ MDBench evaluates whether an AI system can recover scientific laws and the
31
+ mechanisms that produce them from equations or observations.
32
+
33
+ ## What mechanism discovery means
34
+
35
+ MDBench treats a phenomenological equation as the observable consequence of
36
+ several simple, mutually consistent relationships. The phenomenological law
37
+ describes *what* variables do; a mechanism explains *why* through physical
38
+ relationships, assumptions, and intermediate variables.
39
+
40
+ For example, Kepler's third law for a circular orbit follows from gravitation,
41
+ Newton's second law, and uniform circular motion. See
42
+ [`problems/demo_problem.yaml`](problems/demo_problem.yaml).
43
+
44
+ Each mechanism relationship uses `variable = formula`, where the formula must
45
+ be parseable by [nd2py](https://pypi.org/project/nd2py/). Explicit relationships
46
+ form a DAG:
47
+
48
+ ```text
49
+ a = f1(x)
50
+ b = f2(x, a)
51
+ y = f3(x, a, b)
52
+ ```
53
+
54
+ Implicit systems are also supported. Relationships are collected until the
55
+ unknown variables form a closed system, then solved symbolically or with a
56
+ numerical root finder:
57
+
58
+ ```text
59
+ a = f1(x, a, b)
60
+ b = f2(x, a, b)
61
+ y = f3(x, a, b)
62
+ ```
63
+
64
+ All variables are declared under `variable_description` as `target`, `inputs`,
65
+ `intermediates`, or `auxiliary_inputs`. The latter are external variables used
66
+ only by the mechanism and eliminated from the final law. The original
67
+ relationships remain in `Problem.mechanism`; executable solution steps are
68
+ stored in `Problem.solution`.
69
+
70
+ ## Tasks and evaluation
71
+
72
+ MDBench provides three tasks:
73
+
74
+ 1. **Symbolic regression:** `(X, y) → phenomenological equation`.
75
+ 2. **Mechanism explanation:** phenomenological equation → mechanism equations.
76
+ 3. **Mechanism discovery:** `(X, y) → mechanism equations`.
77
+
78
+ Mechanism evaluation reports independent metrics and deliberately has no
79
+ overall score:
80
+
81
+ - **Prediction accuracy:** for symbolic regression and mechanism discovery,
82
+ Pearson correlation, R², MAE, RMSE, sMAPE, and tolerance accuracy on public
83
+ training data (feedback) or train/ID/OOD data (final).
84
+ - **Derived-equation equivalence:** final-only SymPy, numeric, and LLM
85
+ cross-check against the private phenomenological equation.
86
+ - **Mechanism fundamentality:** LLM assessment dominated by the least
87
+ fundamental submitted relationship; no reference answer is required.
88
+ - **Ground-truth structure recovery:** soft formula-AST and dependency-graph
89
+ matching against the reference mechanism. Variable names and numeric literal
90
+ values are ignored.
91
+ - **Mechanism description complexity:** reference-free mean, maximum, and total
92
+ nd2py AST nodes; lower values describe simpler submitted relationships.
93
+
94
+ Install MDBench with Python 3.12 or newer:
95
+
96
+ ```bash
97
+ pip install -e ".[dev]"
98
+ mdbench --help
99
+ ```
100
+
101
+ The Sphinx documentation lives in [`docs/`](docs/). Build it with:
102
+
103
+ ```bash
104
+ cd docs
105
+ make html
106
+ ```
107
+
108
+ ## Commands
109
+
110
+ All lifecycle commands accept one or more YAML files or directories through
111
+ `--problems`; the default is `./problems`.
112
+
113
+ ### Validate problems
114
+
115
+ Checks schemas, variable usage, units, sampling specifications, explicit and
116
+ implicit equation solving, and derivation of the target law:
117
+
118
+ ```bash
119
+ mdbench validate
120
+ mdbench validate --problems problems/demo_problem.yaml
121
+ ```
122
+
123
+ An optional LLM check evaluates whether every relationship is sufficiently
124
+ fundamental. API or response failures are reported directly and do not fall
125
+ back to heuristics.
126
+
127
+ ```bash
128
+ mdbench validate --check-fundamentality \
129
+ --llm-provider deepseek --llm-model deepseek-v4-flash
130
+ ```
131
+
132
+ ### Generate synthetic data
133
+
134
+ Creates reproducible train, ID-test, and OOD-test splits:
135
+
136
+ ```bash
137
+ mdbench synthetic --problems problems/ --output-dir data/synthetic_data/
138
+ ```
139
+
140
+ Each NPZ stores the three arrays, their row order in `variables`, and a JSON
141
+ `generation_config` containing the seed and sample counts. Auxiliary inputs are
142
+ generated here and may be hidden later during task preparation.
143
+
144
+ ### Prepare tasks
145
+
146
+ Synthetic data must already exist. Answers are private by default:
147
+
148
+ ```bash
149
+ mdbench prepare \
150
+ --problems problems/ \
151
+ --synthetic-data-dir data/synthetic_data/ \
152
+ --task mechanism_discovery \
153
+ --format directory
154
+ ```
155
+
156
+ Use `--save-answer` to include answers and test splits, `--reveal-auxiliary` to
157
+ expose auxiliary inputs in mechanism tasks, and `--force` to approve planned
158
+ overwrites. Existing directories are never cleared; redundant files are
159
+ reported. `--format directory` writes flat files, while `--format file` packs
160
+ the same logical artifacts into one NPZ.
161
+
162
+ ### Evaluate submissions
163
+
164
+ A submission may be an inline formula, semicolon-separated mechanism equations,
165
+ or a plain-text file with one equation per non-empty line. JSON and YAML
166
+ submissions are intentionally unsupported.
167
+
168
+ ```bash
169
+ mdbench evaluate \
170
+ --evaluation-mode feedback \
171
+ --problem data/problem/PREPARED_TASK \
172
+ --submission submission.txt \
173
+ --verbose
174
+ ```
175
+
176
+ Feedback mode uses only the public task and training data. Benchmark operators
177
+ run final evaluation with `--evaluation-mode final --answer answer.json`, which
178
+ also enables hidden ID/OOD tests and reference-mechanism recovery. For Agent
179
+ runs, copy only the prepared public task into an isolated temporary working
180
+ directory and require the Agent to remain there. Without source problem YAML or
181
+ private answer artifacts, the other lifecycle commands and final evaluation
182
+ cannot access the material they require. `--verbose` prints concise equation
183
+ chains for explicit or implicit solution steps.
184
+
185
+ Fundamentality scoring automatically uses the configured external model and
186
+ prints its provider and model:
187
+
188
+ ```bash
189
+ mdbench evaluate \
190
+ --evaluation-mode feedback \
191
+ --problem data/problem/PREPARED_TASK \
192
+ --submission submission.txt \
193
+ --llm-provider deepseek \
194
+ --llm-model deepseek-v4-flash
195
+ ```
196
+
197
+ Standalone entry points with equivalent behavior are available in `scripts/`:
198
+
199
+ ```text
200
+ validate_problem_main.py validate problem definitions
201
+ synthetic_data_main.py generate synthetic datasets
202
+ prepare_problem_main.py prepare public/private task artifacts
203
+ evaluate_result_main.py evaluate a submission
204
+ ```
205
+
206
+ `scripts/visualize_mechanism_main.py` renders a solved mechanism as DOT, SVG,
207
+ PNG, or PDF. Non-DOT formats require Graphviz.
208
+
209
+ ## Directory conventions
210
+
211
+ ```text
212
+ problems/ source problem YAML files
213
+ data/
214
+ synthetic_data/ generated train/ID/OOD datasets
215
+ problem/ prepared benchmark tasks
216
+ src/
217
+ core/ dependency-light data models
218
+ features/ project-specific I/O, solving, validation, sampling
219
+ metrics/ formula and mechanism metrics
220
+ utils/ reusable utilities and LLM clients
221
+ scripts/ standalone command entry points
222
+ tests/ unit tests and validation fixtures
223
+ ```
224
+
225
+ A prepared directory contains:
226
+
227
+ ```text
228
+ problem.json public task description
229
+ data_train.npy public training data
230
+ answer.json optional private answer
231
+ data_id_test.npy optional private ID test data
232
+ data_ood_test.npy optional private OOD test data
233
+ ```
234
+
235
+ Without `--save-answer`, only `problem.json` and `data_train.npy` are written.
236
+ Public interchange types remain simple: units are `Dict[str, int | float]`,
237
+ formulas are nd2py-compatible strings, and arrays use NumPy formats.
@@ -0,0 +1,57 @@
1
+ mdbench-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ src/evaluate_result.py,sha256=aWul9urABSXmiajwZp6AFQOh57jEu_98dTIAutYugHI,28411
3
+ src/prepare_problem.py,sha256=vUfKsBtH9bQPjGTzX0JmfLSCXWDy7I6OjTaRvXwfbRA,11224
4
+ src/synthetic_data.py,sha256=kGqyswRojqK3d67gA0nVek-y-2_DHU6OKP56NrRh3_E,9661
5
+ src/validate_problem.py,sha256=FLxHY9PRCCqjpFoCsN_ewKyTqjrj5IrzJoD0wHT-f8U,14072
6
+ src/visualize_mechanism.py,sha256=TqsEXf0d382NFakIKmZilbo8JeoYiEMpRJ7w3VE1Y3I,1998
7
+ src/cli/run.py,sha256=A_SMsBRnTKzURKkrz7Ixtsrpnkm06jcN0QMOO9Oi4RI,2596
8
+ src/core/__init__.py,sha256=Me6enC0oja7gfcJ5ucJk6ZrBjHyb24DVEUda6VnazZw,338
9
+ src/core/problem.py,sha256=IG2GGZR-PMJoB5ppUkducdtsU7X7ExOdMSBJU8UZsOs,2443
10
+ src/core/solution.py,sha256=6k2vAuf54JWGcAKOmDWvtE9ALjemuhReGDn_xV2xLlw,743
11
+ src/features/__init__.py,sha256=k10Bq7c3CPBGR1rAfNnGhGzM1itOu5QrY7njPQ-0CXI,619
12
+ src/features/answer.py,sha256=DTmtvHXdcyivG-x_-GxXS4OL4m2myEaRACjihIhLSck,1891
13
+ src/features/evaluation/__init__.py,sha256=mnxEa35jljr-F-IXdUJUmXYOU0PtW5NBXJzz4SpsTrM,378
14
+ src/features/evaluation/evaluation_package.py,sha256=B6Q25B3Sf8rdBTUeIWr2Ue_B8vK6xoBlwyxexGNDx48,4190
15
+ src/features/evaluation/mechanism_trace.py,sha256=fCUmU_-6t5wX7v61nI1LPbVV2_sxKsIeBxs0GObRgMA,6361
16
+ src/features/io/__init__.py,sha256=U1glfV1AqnlXRMmcffqayuf-nwljU3LA2Q777MyaPZw,377
17
+ src/features/io/load_problem.py,sha256=Tkzxp2tNPgV0ZuRJiXD1c4lv4uWQ4vRxR35YsPFaayU,6516
18
+ src/features/io/load_submission.py,sha256=SMg18vH95Z5EDAO3yorrPGLB9ego3YrVVNRlHZVTdOs,6576
19
+ src/features/io/solve_mechanism_equations.py,sha256=8KWQ7Jr5I7A1XBndDgJO3bsWaRpKI9-95Rg2TxKI9MQ,11482
20
+ src/features/sampling/__init__.py,sha256=NmGkqRIGhy75yNnaQ7XFOeSwDbYgKlgbV_aulGHjJbs,114
21
+ src/features/sampling/range_inferrer.py,sha256=XMfOaVnCwJrsKTC5w29LQPd6HckDxhZ7-eyL75q3jZg,2156
22
+ src/features/units/__init__.py,sha256=rmrfBBY-3r78uNWk3dnTWjQJZFIZo1t6uY498uOnR34,160
23
+ src/features/units/unit_inference.py,sha256=8QfcxUVDv9T0JmbtGxczteAcXpiNaF3oUWHBAuF_o08,5376
24
+ src/features/validation/__init__.py,sha256=2Zm0jthm9go76W7mCmtHfv-6ZDUM-h_w7C3MqqvbpzI,412
25
+ src/features/validation/mechanism_derivation.py,sha256=RGoG8p7UPlEeNCfM5DPw2Gt7KGOZiSt8KPILhxWUKek,3258
26
+ src/features/validation/mechanism_fundamentality.py,sha256=lAJXjURaPLVZkLosS35GkBLoxvZaT6LYqq6nxxO-lYg,8512
27
+ src/features/visualization/__init__.py,sha256=RM-_qtlcERQdcEVIKTq-tHFocZhPxbojP5BxytW1Otc,140
28
+ src/features/visualization/mechanism_graph.py,sha256=9T_zCh-AEqEfEdrmolVWZEAsH-n-puCOZw37S1ZnECg,9100
29
+ src/metrics/__init__.py,sha256=vzbkDBNNr0OGTiDsZni0CqkvPNBrOcHSmT3LtrLjgUw,678
30
+ src/metrics/formula_similarity.py,sha256=hZEGfIRY-PwrJzZiOcEp81yjgsl0hT_eCQISbH67Blk,1859
31
+ src/metrics/hybrid_formula_similarity.py,sha256=qG7WqDv-RTBxoYIZbpheJaRDKY7ZAyuPiQiljEomCqQ,8776
32
+ src/metrics/mechanism_fundamentality.py,sha256=qW4tpWf1aPk_610W4OwP-yY2MxMw32zf1d6R42Leqo8,4243
33
+ src/metrics/mechanism_similarity.py,sha256=KX_xhhUvCwHbtnRdMaESeN4NkzaNBgH5GtS0X0_Hn5U,9030
34
+ src/metrics/mechanism_simplicity.py,sha256=GzHbOCbg80Fc7DEOhC7Uda5SRMAJY81scZJ7Snll7Ow,1273
35
+ src/utils/__init__.py,sha256=ILszvDXBot7dBzGfPFPln9YFmCIzUXGyL3EYjLxQ3yo,387
36
+ src/utils/console.py,sha256=JqMIcENpaNS2S8j8XIo5D8ZZI82qRL7MAw4y_uT9rrI,574
37
+ src/utils/lazy_loader.py,sha256=WhWJUchf3LRHG-LTjdSPPKrG_P1LJWBSUE70arRugDI,1670
38
+ src/utils/log_exception.py,sha256=5o_HOCt7otDk9KMwg4JxxkBWYpzddpnlZV4XC4JWITg,406
39
+ src/utils/logger.py,sha256=ZzmbZ4YZCEhIJWKD1ppe571AkbRWvduS-qOJz-ThNWw,8347
40
+ src/utils/path_utils.py,sha256=qXa-l4U_5Rzb_NK5nxBWk7UvLo1SM_kEfe0nNsi5nc8,1043
41
+ src/utils/tag2ansi.py,sha256=HuUkO8qAspRRBUsyutlZxpdHwoWMmUVjvLPVa8_wdog,4693
42
+ src/utils/unit_parser.py,sha256=s-uRfMr7bdyX8qdsN94QahSpIchHAMvMHypNlcJaGos,1942
43
+ src/utils/llm/__init__.py,sha256=OA3Kp9fGbO9FFsVks7FXUiOrm9GA-yO11cu3BVZ7i0Q,914
44
+ src/utils/llm/core.py,sha256=u-5bdVQ17Bm2zcbUPos-SuznGd1anF1PnjiKitYZ96o,2032
45
+ src/utils/llm/deepseek_api.py,sha256=tutynP_-l9o17gQXoa2sDWz_dhVEz6SU9h3-imnzZRE,4459
46
+ src/utils/llm/gemini_api.py,sha256=gW8qq1ZgmsE9SVKfm0FJRr3pqrMGbl7PkVY5DdVA6tQ,3904
47
+ src/utils/llm/llm_api.py,sha256=Y4B9fh_XARWuMEMDeQroplZ_1QGe035gmro2Zhs8G7k,6932
48
+ src/utils/llm/manual_api.py,sha256=Xlrk4wrgeUCkpWX69Zr8mLiiCgB7fYf9EFeuVxJUE9g,2636
49
+ src/utils/llm/openai_api.py,sha256=nPt_VAgjYgg1PHFniJseZG3sRS69D2-W3FI7qibbmv4,14472
50
+ src/utils/llm/openrouter_api.py,sha256=yUIPoxJqrurTwVC_fR29t3b5hP6mq8v61XOJ0PvW3O0,5745
51
+ src/utils/llm/siliconflow_api.py,sha256=gumGbL-VOKuWkBs-pLJxB2kzOd2pXskK7L23XDjElYY,7529
52
+ src/utils/llm/tool_call_mixin.py,sha256=IF0vCSPDscnr_CRP3kshLAJXghf4fBf8wsqk6BJL3iE,3573
53
+ mdbench-0.1.0.dist-info/METADATA,sha256=sgpANKfB8ZYFFZrmhSbEQHDp1QpLbf0J07HLVc6Eg3s,8125
54
+ mdbench-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
55
+ mdbench-0.1.0.dist-info/entry_points.txt,sha256=2rnn_P0AwJrl1aN4Tl9kN1n42ajNVJU0u8kzp1lphd4,44
56
+ mdbench-0.1.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
57
+ mdbench-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mdbench = src.cli.run:cli
File without changes
@@ -0,0 +1 @@
1
+ src
src/cli/run.py ADDED
@@ -0,0 +1,69 @@
1
+ """Command-line interface for all benchmark lifecycle operations."""
2
+ from __future__ import annotations
3
+ import argparse
4
+
5
+
6
+ def get_parser() -> argparse.ArgumentParser:
7
+ preliminary_parser = argparse.ArgumentParser(add_help=False)
8
+ preliminary_parser.add_argument(
9
+ "command",
10
+ nargs="?",
11
+ choices=("validate", "synthetic", "prepare", "evaluate"),
12
+ )
13
+ tmp_args, _ = preliminary_parser.parse_known_args()
14
+
15
+ parser = argparse.ArgumentParser(
16
+ prog="mdbench",
17
+ description="MDBench toolkit",
18
+ epilog="Run 'mdbench <command> --help' for command-specific options.",
19
+ )
20
+ commands = parser.add_subparsers(dest="command", required=True)
21
+ if tmp_args.command == "validate":
22
+ from ..validate_problem import get_parser as update_parser
23
+ update_parser(commands.add_parser("validate"))
24
+ elif tmp_args.command == "synthetic":
25
+ from ..synthetic_data import get_parser as update_parser
26
+ update_parser(commands.add_parser("synthetic"))
27
+ elif tmp_args.command == "prepare":
28
+ from ..prepare_problem import get_parser as update_parser
29
+ update_parser(commands.add_parser("prepare"))
30
+ elif tmp_args.command == "evaluate":
31
+ from ..evaluate_result import get_parser as update_parser
32
+ update_parser(commands.add_parser("evaluate"))
33
+ else:
34
+ commands.add_parser("validate", help="Validate benchmark problem files")
35
+ commands.add_parser("synthetic", help="Generate synthetic benchmark data")
36
+ commands.add_parser("prepare", help="Prepare benchmark task packages")
37
+ commands.add_parser("evaluate", help="Evaluate benchmark submissions")
38
+ return parser
39
+
40
+
41
+ def main(args) -> int:
42
+ if args.command == "validate":
43
+ from ..validate_problem import main as command_main
44
+ return command_main(args)
45
+ elif args.command == "synthetic":
46
+ from ..synthetic_data import main as command_main
47
+ return command_main(args)
48
+ elif args.command == "prepare":
49
+ from ..prepare_problem import main as command_main
50
+ return command_main(args)
51
+ elif args.command == "evaluate":
52
+ from ..evaluate_result import main as command_main
53
+ return command_main(args)
54
+ else:
55
+ raise ValueError(f"Unknown command: {args.command}")
56
+
57
+
58
+ def cli() -> int:
59
+ """Parse command-line arguments and dispatch the selected command."""
60
+ from ..utils.logger import config_logger
61
+
62
+ config_logger()
63
+ parser = get_parser()
64
+ args = parser.parse_args()
65
+ return main(args)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(cli())
src/core/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # Copyright (c) 2026-present, Yumeow. Licensed under the MIT License.
2
+ from .problem import SI, UNIT, VariableSpec, ConstantSpec, MechanismItem, Problem
3
+ from .solution import SolutionFunction, SolutionItem
4
+
5
+ __all__ = [
6
+ "SI", "UNIT", "VariableSpec", "ConstantSpec", "MechanismItem", "Problem",
7
+ "SolutionFunction", "SolutionItem",
8
+ ]
src/core/problem.py ADDED
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2026-present, Yumeow. Licensed under the MIT License.
2
+ from typing import List, Dict, Literal, get_args
3
+ from dataclasses import dataclass, field
4
+ from .solution import SolutionItem
5
+
6
+ SI = Literal['kg', 'm', 's', 'A', 'K', 'mol', 'cd']
7
+
8
+ class UNIT:
9
+ def __init__(self, unit_dict: Dict[SI, int | float]):
10
+ self.unit_dict = unit_dict
11
+
12
+ def __repr__(self):
13
+ return f"UNIT({self.unit_dict})"
14
+
15
+ def __str__(self):
16
+ if not self.unit_dict:
17
+ return "1 (dimensionless)"
18
+ return " ".join(
19
+ name if exponent == 1 else f"{name}^{exponent:g}"
20
+ for name, exponent in sorted(self.unit_dict.items(), key=lambda x: get_args(SI).index(x[0]))
21
+ )
22
+
23
+ def __eq__(self, other):
24
+ if not isinstance(other, UNIT):
25
+ return NotImplemented
26
+ return self.unit_dict == other.unit_dict
27
+
28
+ def to_dict(self) -> Dict[str, int | float]:
29
+ """Return the dependency-free interchange representation."""
30
+ return dict(self.unit_dict)
31
+
32
+ @dataclass
33
+ class VariableSpec:
34
+ name: str
35
+ description: str
36
+ unit: UNIT | None
37
+ sampling: Dict[str, float | str] | None = field(default=None, kw_only=True)
38
+
39
+
40
+ @dataclass
41
+ class ConstantSpec(VariableSpec):
42
+ name: str
43
+ description: str
44
+ unit: UNIT
45
+ value: int | float
46
+
47
+
48
+ @dataclass
49
+ class MechanismItem:
50
+ variable: str
51
+ formula: str # An nd2py-compatible right-hand-side expression.
52
+ formula_description: str
53
+
54
+ @property
55
+ def equation(self) -> str:
56
+ """Return the complete mechanism equation."""
57
+ return f"{self.variable} = {self.formula}"
58
+
59
+
60
+ @dataclass
61
+ class Problem:
62
+ problem_name: str
63
+ problem_description: str
64
+ phenomenological_formula: str # nd2py-compatible right-hand side.
65
+ target_variable: VariableSpec
66
+ input_variables: List[VariableSpec]
67
+ intermediate_variables: List[VariableSpec]
68
+ mechanism: List[MechanismItem]
69
+ auxiliary_input_variables: List[VariableSpec] = field(default_factory=list)
70
+ constants: List[ConstantSpec] = field(default_factory=list)
71
+ solution: List[SolutionItem] = field(default_factory=list)
72
+
73
+ @property
74
+ def all_variables(self) -> List[VariableSpec]:
75
+ """All non-constant variables in their schema order."""
76
+ return [
77
+ self.target_variable,
78
+ *self.input_variables,
79
+ *self.intermediate_variables,
80
+ *self.auxiliary_input_variables,
81
+ ]
src/core/solution.py ADDED
@@ -0,0 +1,25 @@
1
+ """Executable solution steps derived from a problem's mechanism equations."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from dataclasses import dataclass
5
+ from typing import Callable
6
+
7
+ SolutionFunction = Callable[
8
+ [dict[str, np.ndarray]],
9
+ list[np.ndarray]
10
+ ]
11
+
12
+
13
+ @dataclass
14
+ class SolutionItem:
15
+ """One ordered mechanism-solving step.
16
+
17
+ A single-variable step is an ordinary causal evaluation. Multiple
18
+ ``variables`` denote values that must be obtained simultaneously from one
19
+ coupled equation system. ``formulas`` is aligned with ``variables`` when a
20
+ closed form exists, and is empty when numerical solving is required.
21
+ """
22
+
23
+ variables: list[str]
24
+ formulas: list[str]
25
+ function: SolutionFunction