apc-model-parser 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/AGENTS.md +3 -2
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/PKG-INFO +7 -3
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/README.md +6 -2
- apc_model_parser-0.2.2/docs/chatlogs/2026-06-03_julia-rhs-emit-and-library-sync.md +80 -0
- apc_model_parser-0.2.2/docs/decisions/0006-julia-numerical-rhs-view.md +52 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/index.md +1 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/model-library-and-versioning.md +3 -2
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/model-parser.md +15 -9
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/storing-mtk-models.md +8 -6
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/README.md +7 -2
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/run.sh +6 -1
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/mkdocs.yml +1 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/pyproject.toml +1 -1
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/__init__.py +2 -2
- apc_model_parser-0.2.2/src/model_parser/backends/__init__.py +7 -0
- apc_model_parser-0.2.2/src/model_parser/backends/julia_expr.py +84 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/backends/julia_mtk.py +4 -76
- apc_model_parser-0.2.2/src/model_parser/backends/julia_rhs.py +129 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/cli.py +20 -2
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_cli.py +6 -0
- apc_model_parser-0.2.2/tests/test_julia_rhs.py +58 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/uv.lock +422 -422
- apc_model_parser-0.2.1/src/model_parser/backends/__init__.py +0 -5
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/ci.yml +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/docs.yml +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/release.yml +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.gitignore +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/api.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-01_bootstrap-model-parser.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-01_ci-mkdocs-github-pages-pypi.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-01_docs-pipx-pypi-install.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-02_model-library-docs-diff-bump.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-03_docs-todo-backlog-mkdocs.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0001-python-cli-with-julia-backend.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0002-codegen-over-serialized-system.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0003-explicit-expression-ir.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0004-target-mtk-v11-idioms.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0005-cli-verbs-parse-emit.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/deployment/ci-cd.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/ir-specification.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/language-strategy.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/index.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/conformance-fixtures-parity.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/diff-bump-hardening.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/emit-cpp-realtime-backend.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/emit-ini-round-trip.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/index.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/ini-dimensions-inference.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/ir-schema-migrations.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/parameter-set-contract-cli.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/models/model_monod_simple.ini +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/models/model_thermal_tank.ini +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/Project.toml +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/README.md +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/src/ModelParserJL.jl +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/schemas/canonical-ir.schema.json +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/__init__.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/expr_parser.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/exprtk_ini.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/io.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/__init__.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/expr.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/model.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/schema.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/semantic_diff.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/validation/__init__.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/validation/validators.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/conftest.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_expr_parser.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_exprtk_ini.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_julia_mtk.py +0 -0
- {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_semantic_diff.py +0 -0
|
@@ -22,8 +22,9 @@ ecosystem and must remain usable **standalone**.
|
|
|
22
22
|
```text
|
|
23
23
|
authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
|
|
24
24
|
|
|
|
25
|
-
emit julia
|
|
26
|
-
emit
|
|
25
|
+
emit julia --> ModelingToolkit .jl
|
|
26
|
+
emit julia-rhs --> numerical f!/outputs! .jl
|
|
27
|
+
emit cpp --> (planned) realtime C++
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
## Language
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apc-model-parser
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Convert process-model definitions to and from a canonical intermediate representation.
|
|
5
5
|
Project-URL: Repository, https://github.com/Advanced-Process-Control/model-parser
|
|
6
6
|
Author: Advanced Process Control
|
|
@@ -26,8 +26,9 @@ model script.
|
|
|
26
26
|
```text
|
|
27
27
|
authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
|
|
28
28
|
|
|
|
29
|
-
emit julia
|
|
30
|
-
emit
|
|
29
|
+
emit julia --> ModelingToolkit .jl
|
|
30
|
+
emit julia-rhs --> numerical f!/outputs! .jl
|
|
31
|
+
emit cpp --> (planned) realtime C++
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
The IR is the single semantic contract. Adding a backend means writing one
|
|
@@ -80,6 +81,9 @@ uv run model-parser parse examples/models/model_monod_simple.ini -o monod.ir.jso
|
|
|
80
81
|
# 2. canonical IR -> ModelingToolkit (v11) Julia model
|
|
81
82
|
uv run model-parser emit julia monod.ir.json -o monod.jl
|
|
82
83
|
|
|
84
|
+
# 2a. canonical IR -> plain numerical ODE RHS (SciML-style f!)
|
|
85
|
+
uv run model-parser emit julia-rhs monod.ir.json -o monod_rhs.jl
|
|
86
|
+
|
|
83
87
|
# Supporting commands
|
|
84
88
|
uv run model-parser validate monod.ir.json --profile julia-analysis
|
|
85
89
|
uv run model-parser inspect monod.ir.json
|
|
@@ -13,8 +13,9 @@ model script.
|
|
|
13
13
|
```text
|
|
14
14
|
authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
|
|
15
15
|
|
|
|
16
|
-
emit julia
|
|
17
|
-
emit
|
|
16
|
+
emit julia --> ModelingToolkit .jl
|
|
17
|
+
emit julia-rhs --> numerical f!/outputs! .jl
|
|
18
|
+
emit cpp --> (planned) realtime C++
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
The IR is the single semantic contract. Adding a backend means writing one
|
|
@@ -67,6 +68,9 @@ uv run model-parser parse examples/models/model_monod_simple.ini -o monod.ir.jso
|
|
|
67
68
|
# 2. canonical IR -> ModelingToolkit (v11) Julia model
|
|
68
69
|
uv run model-parser emit julia monod.ir.json -o monod.jl
|
|
69
70
|
|
|
71
|
+
# 2a. canonical IR -> plain numerical ODE RHS (SciML-style f!)
|
|
72
|
+
uv run model-parser emit julia-rhs monod.ir.json -o monod_rhs.jl
|
|
73
|
+
|
|
70
74
|
# Supporting commands
|
|
71
75
|
uv run model-parser validate monod.ir.json --profile julia-analysis
|
|
72
76
|
uv run model-parser inspect monod.ir.json
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Julia numerical RHS emit target and model-library sync
|
|
3
|
+
topic: codegen
|
|
4
|
+
date_added: 2026-06-03
|
|
5
|
+
tags: [chatlogs]
|
|
6
|
+
links:
|
|
7
|
+
- AGENTS.md
|
|
8
|
+
- docs/design/model-parser.md
|
|
9
|
+
- docs/decisions/0006-julia-numerical-rhs-view.md
|
|
10
|
+
- src/model_parser/backends/julia_expr.py
|
|
11
|
+
- src/model_parser/backends/julia_rhs.py
|
|
12
|
+
- src/model_parser/cli.py
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Commit helper
|
|
16
|
+
|
|
17
|
+
- **SemVer / version bump:** **PATCH** (`0.2.1` → `0.2.2`) — new CLI subcommand `emit julia-rhs` and new public Python API `emit_julia_rhs`; backward-compatible for existing `emit julia` users.
|
|
18
|
+
- **Tags / GitHub Release:** **None** — stack on default branch; tag when cutting a PyPI release (existing `release.yml` on tag).
|
|
19
|
+
- **Suggested commit message (model-parser repo):** `feat(emit): add julia-rhs numerical ODE view`
|
|
20
|
+
- **Copy-paste git commands (model-parser):**
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd /path/to/model-parser
|
|
24
|
+
git add AGENTS.md README.md docs/design/model-library-and-versioning.md \
|
|
25
|
+
docs/design/model-parser.md docs/design/storing-mtk-models.md \
|
|
26
|
+
docs/decisions/0006-julia-numerical-rhs-view.md docs/decisions/index.md \
|
|
27
|
+
docs/chatlogs/2026-06-03_julia-rhs-emit-and-library-sync.md \
|
|
28
|
+
examples/README.md examples/run.sh mkdocs.yml pyproject.toml \
|
|
29
|
+
src/model_parser/__init__.py src/model_parser/backends/__init__.py \
|
|
30
|
+
src/model_parser/backends/julia_expr.py src/model_parser/backends/julia_mtk.py \
|
|
31
|
+
src/model_parser/backends/julia_rhs.py src/model_parser/cli.py \
|
|
32
|
+
tests/test_cli.py tests/test_julia_rhs.py
|
|
33
|
+
git commit -m "$(cat <<'EOF'
|
|
34
|
+
feat(emit): add julia-rhs numerical ODE view
|
|
35
|
+
|
|
36
|
+
Share IR→Julia expression lowering in julia_expr; emit f!/outputs! codegen;
|
|
37
|
+
document packing and ADR 0006; extend examples and model-library design notes.
|
|
38
|
+
EOF
|
|
39
|
+
)"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Suggested commit message (model-library repo, separate clone):** `chore(views): add model_rhs.jl and lock digests from emit julia-rhs`
|
|
43
|
+
- **Copy-paste git commands (model-library):**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd /path/to/model-library
|
|
47
|
+
git add README.md library.lock.json scripts/sync.sh scripts/update_lock.py \
|
|
48
|
+
models/*/model.ir.json models/*/views/model.jl models/*/views/model_rhs.jl
|
|
49
|
+
git commit -m "$(cat <<'EOF'
|
|
50
|
+
chore(views): sync numerical RHS views and lockfile
|
|
51
|
+
|
|
52
|
+
Regenerate IR/MTK views; add model_rhs.jl and julia_rhs_* lock fields per sync.
|
|
53
|
+
EOF
|
|
54
|
+
)"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Git order when tagging:** commit / push branch (with version bump if releasing) → annotated tag on that commit → push tag.
|
|
58
|
+
|
|
59
|
+
## How to try
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd /path/to/model-parser
|
|
63
|
+
uv sync --all-groups
|
|
64
|
+
uv run model-parser parse examples/models/model_monod_simple.ini -o /tmp/m.ir.json
|
|
65
|
+
uv run model-parser emit julia-rhs /tmp/m.ir.json
|
|
66
|
+
uv run pytest tests/test_julia_rhs.py tests/test_cli.py
|
|
67
|
+
uv run mkdocs build --strict
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**model-library:** `MODEL_PARSER_VENV=/path/to/model-parser/.venv ./scripts/sync.sh` then inspect `models/*/views/model_rhs.jl` and `library.lock.json`.
|
|
71
|
+
|
|
72
|
+
## Session narrative
|
|
73
|
+
|
|
74
|
+
**Goal:** Implement a second Julia backend that emits an explicit numerical RHS (`f!` and optional `outputs!`) from the canonical IR, and wire the sibling `model-library` sync + lockfile to generate and fingerprint `views/model_rhs.jl`.
|
|
75
|
+
|
|
76
|
+
**Shipped (model-parser):** New module `julia_expr.py` (shared `expr_to_julia`, `julia_model_slug`, `julia_number_literal`, `JuliaCodegenError`); `julia_mtk.py` refactored to use it; `julia_rhs.py` with `emit_julia_rhs`; CLI `emit julia-rhs`; tests; PATCH version bump; ADR 0006; design/README/AGENTS/examples updates; MkDocs nav entry.
|
|
77
|
+
|
|
78
|
+
**Shipped (model-library):** `sync.sh` runs `emit julia-rhs` to `views/model_rhs.jl`; `update_lock.py` records `julia_rhs_relpath` and `julia_rhs_content_hash`; README updated; all models re-synced (committed artifacts include new `.jl` files and refreshed lock).
|
|
79
|
+
|
|
80
|
+
**Follow-ups:** Optional `ApcModelAnalysis.jl` helper to load `model_rhs.jl`; conformance tests comparing MTK trajectory vs plain `ODEProblem` built from `f!` for small fixtures.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# ADR 0006: Julia numerical RHS view (`emit julia-rhs`)
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Some analysis workflows (explicit vector ODEs, custom sensitivity tooling, or
|
|
10
|
+
bridges that do not use ModelingToolkit) need a **plain in-place RHS**
|
|
11
|
+
`f!(du, u, p, t)` with stable symbol-to-index packing, while the ecosystem
|
|
12
|
+
standard remains an MTK-generated `System` from the same IR.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Add a second Julia **codegen** target, `emit julia-rhs`, that lowers the same
|
|
17
|
+
canonical IR to:
|
|
18
|
+
|
|
19
|
+
- `f_<model_slug>!(du, u, p, t)` when the scaffold has no inputs, or
|
|
20
|
+
`f_<model_slug>!(du, u, p, t, inp)` when it has inputs (`inp` order = IR inputs
|
|
21
|
+
list order);
|
|
22
|
+
- `outputs_<model_slug>!(y, u, p, t)` (same optional `inp`) when the IR
|
|
23
|
+
carries output equations.
|
|
24
|
+
|
|
25
|
+
Packing rules: `u` and `du` follow `ir.states` order; `p` follows `ir.parameters`
|
|
26
|
+
order (defaults in the IR are **not** applied inside `f!` — callers supply
|
|
27
|
+
numeric `p`). Locals are emitted as sequential assignments in IR order, aligned
|
|
28
|
+
with the MTK lowering order.
|
|
29
|
+
|
|
30
|
+
Expression rendering is shared with the MTK backend via `julia_expr.py` so
|
|
31
|
+
semantics are not duplicated.
|
|
32
|
+
|
|
33
|
+
## Consequences
|
|
34
|
+
|
|
35
|
+
- **Positive:** One IR, two diffable Julia views; numerical and symbolic stacks
|
|
36
|
+
stay aligned.
|
|
37
|
+
- **Negative:** Callers must respect documented packing; no automatic wiring to
|
|
38
|
+
MTK `ODEProblem` from the RHS view alone.
|
|
39
|
+
|
|
40
|
+
## Alternatives considered
|
|
41
|
+
|
|
42
|
+
1. **Generate RHS only in downstream repos** — rejected: duplicates expression
|
|
43
|
+
semantics and drifts from the IR contract.
|
|
44
|
+
2. **Single `emit julia` with a flag** — rejected: CLI stays clearer with
|
|
45
|
+
separate targets (`emit cpp`, etc.) per ADR 0005.
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
|
|
49
|
+
- ADR 0002 (codegen over serialized `System`)
|
|
50
|
+
- ADR 0003 (explicit expression IR)
|
|
51
|
+
- ADR 0005 (CLI `emit <target>` shape)
|
|
52
|
+
- `docs/design/model-parser.md` §5 (CLI)
|
|
@@ -17,6 +17,7 @@ code and review naturally in pull requests.
|
|
|
17
17
|
| [0003](0003-explicit-expression-ir.md) | Explicit expression IR, not string rewrites | accepted | 2026-06-01 |
|
|
18
18
|
| [0004](0004-target-mtk-v11-idioms.md) | Generated Julia targets ModelingToolkit v11 idioms | accepted | 2026-06-01 |
|
|
19
19
|
| [0005](0005-cli-verbs-parse-emit.md) | CLI shape: `parse` and `emit <target>` as core verbs | accepted | 2026-06-01 |
|
|
20
|
+
| [0006](0006-julia-numerical-rhs-view.md) | Second Julia view: plain `f!` / `outputs!` from IR (`emit julia-rhs`) | accepted | 2026-06-03 |
|
|
20
21
|
|
|
21
22
|
## Format
|
|
22
23
|
|
{apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/model-library-and-versioning.md
RENAMED
|
@@ -73,7 +73,8 @@ models/<name>/
|
|
|
73
73
|
model.ir.json # canonical IR (generated by model-parser parse)
|
|
74
74
|
views/
|
|
75
75
|
model.jl # generated by model-parser emit julia
|
|
76
|
-
|
|
76
|
+
model_rhs.jl # generated by model-parser emit julia-rhs
|
|
77
|
+
library.lock.json # index: names, paths, content_hash, view SHA-256 digests
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
A thin driver (`scripts/sync.sh`, `just`, or Makefile) invokes the installed `model-parser`. CI runs the same pipeline with `--strict` checks (e.g. `git diff --exit-code` after sync).
|
|
@@ -86,7 +87,7 @@ flowchart TD
|
|
|
86
87
|
B --> C{content_hash changed vs baseline?}
|
|
87
88
|
C -- no --> Z[No semantic change; optional doc-only commit]
|
|
88
89
|
C -- yes --> D[model-parser diff / bump vs previous release IR]
|
|
89
|
-
D --> E[Regenerate views emit julia …]
|
|
90
|
+
D --> E[Regenerate views: emit julia, emit julia-rhs …]
|
|
90
91
|
E --> F[Update library.lock.json and changelog]
|
|
91
92
|
F --> G[Commit; tag if releasing]
|
|
92
93
|
G --> H[CI: re-sync or re-emit; fail on drift]
|
|
@@ -13,9 +13,10 @@ backend-independent IR, and lowers that IR into target views.
|
|
|
13
13
|
```text
|
|
14
14
|
authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
|
|
15
15
|
|
|
|
16
|
-
emit julia
|
|
17
|
-
emit
|
|
18
|
-
emit
|
|
16
|
+
emit julia --> ModelingToolkit .jl
|
|
17
|
+
emit julia-rhs --> numerical f!/outputs! .jl
|
|
18
|
+
emit cpp --> (planned) realtime C++
|
|
19
|
+
emit ini --> (planned) round-trip
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
The IR is the **single semantic contract**. Adding a backend is one `lower` +
|
|
@@ -36,8 +37,9 @@ In scope:
|
|
|
36
37
|
versioned JSON Schema.
|
|
37
38
|
- **Validation** — semantic checks (undeclared symbols, duplicate names,
|
|
38
39
|
missing equations, local ordering) and backend-profile checks.
|
|
39
|
-
- **Backends** — lower the IR into a target view.
|
|
40
|
-
ModelingToolkit (Julia) model script
|
|
40
|
+
- **Backends** — lower the IR into a target view. Backends today: a
|
|
41
|
+
ModelingToolkit (Julia) model script (`emit julia`) and a plain numerical
|
|
42
|
+
ODE RHS plus optional output map (`emit julia-rhs`). A Julia companion package
|
|
41
43
|
(`ModelParserJL`) loads the IR in memory.
|
|
42
44
|
|
|
43
45
|
Out of scope (sibling tools that *consume* the IR):
|
|
@@ -58,7 +60,7 @@ scaffold contract, not an everything-bucket (see the org risk note on
|
|
|
58
60
|
| **Authoring** | What the engineer writes (ExprTk INI today). | yes (`.ini`) |
|
|
59
61
|
| **AST** | Syntax-oriented tree from the parser. Internal. | debug only |
|
|
60
62
|
| **Canonical IR** | Normalized, backend-independent scaffold semantics. | yes (`.ir.json`) |
|
|
61
|
-
| **Backend view** | A lowered target (MTK `.jl`, future C++). | yes (generated) |
|
|
63
|
+
| **Backend view** | A lowered target (MTK `.jl`, numerical RHS `.jl`, future C++). | yes (generated) |
|
|
62
64
|
|
|
63
65
|
The AST is in-memory; the IR is the durable interchange contract. The full IR
|
|
64
66
|
shape and the expression sub-language are specified in
|
|
@@ -82,7 +84,8 @@ contracts ("initial values live in the scenario, not the scaffold").
|
|
|
82
84
|
|
|
83
85
|
```text
|
|
84
86
|
model-parser parse <authoring-file> [--from exprtk-ini] [-o out.ir.json]
|
|
85
|
-
model-parser emit julia
|
|
87
|
+
model-parser emit julia <model.ir.json> [-o out.jl]
|
|
88
|
+
model-parser emit julia-rhs <model.ir.json> [-o out.jl]
|
|
86
89
|
model-parser validate <model.ir.json | authoring-file> [--profile <name>]
|
|
87
90
|
model-parser inspect <model.ir.json | authoring-file>
|
|
88
91
|
model-parser diff <old.ir.json> <new.ir.json> [--json]
|
|
@@ -93,8 +96,10 @@ model-parser schema [-o schema.json]
|
|
|
93
96
|
|
|
94
97
|
- `parse` is the **authoring → IR** transformation. Default and only frontend
|
|
95
98
|
today is `exprtk-ini`.
|
|
96
|
-
- `emit <target>` is the **IR → view** transformation.
|
|
97
|
-
|
|
99
|
+
- `emit <target>` is the **IR → view** transformation. Julia targets: `julia`
|
|
100
|
+
(ModelingToolkit v11 scaffold) and `julia-rhs` (plain `f!` / optional
|
|
101
|
+
`outputs!`). Designed to grow (`emit cpp`, `emit ini`) without touching
|
|
102
|
+
existing targets.
|
|
98
103
|
- `validate` accepts either an IR `.json` or an authoring file (parsed on the
|
|
99
104
|
fly), and an optional `--profile`.
|
|
100
105
|
- `inspect` prints a human summary; `ast` exports a debug tree; `schema` exports
|
|
@@ -113,6 +118,7 @@ Diagnostics use the `OK` / `WARN` / `ERROR` vocabulary.
|
|
|
113
118
|
uv run model-parser parse examples/models/model_monod_simple.ini -o monod.ir.json
|
|
114
119
|
uv run model-parser validate monod.ir.json --profile julia-analysis
|
|
115
120
|
uv run model-parser emit julia monod.ir.json -o monod.jl
|
|
121
|
+
uv run model-parser emit julia-rhs monod.ir.json -o monod_rhs.jl
|
|
116
122
|
```
|
|
117
123
|
|
|
118
124
|
## 6. Language split
|
|
@@ -29,19 +29,21 @@ long-term storage to one symbolic library's internal API is exactly the
|
|
|
29
29
|
|
|
30
30
|
## 2. The chosen approach
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
A durable **canonical IR JSON** plus **one or more generated Julia views** from
|
|
33
|
+
the same IR:
|
|
33
34
|
|
|
34
35
|
1. **Canonical IR JSON** — the source of truth. Language-neutral, diffable,
|
|
35
36
|
schema-validated, content-hashed. This is what we persist and version.
|
|
36
|
-
2. **Generated `.jl`
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
2. **Generated `.jl` scripts** — produced by `model-parser emit julia` (MTK
|
|
38
|
+
`System` builder) and, when needed, `model-parser emit julia-rhs` (plain
|
|
39
|
+
`f!` / `outputs!` for vector ODE workflows). Plain-text, re-runnable artifacts
|
|
40
|
+
regenerated whenever the IR changes; never hand-edited.
|
|
39
41
|
|
|
40
42
|
```text
|
|
41
43
|
store / version
|
|
42
44
|
┌──────────────┐
|
|
43
|
-
authoring ──► │ IR JSON │ ──emit julia──►
|
|
44
|
-
(.ini) │ (.ir.json) │
|
|
45
|
+
authoring ──► │ IR JSON │ ──emit julia──► MTK builder .jl ──► System
|
|
46
|
+
(.ini) │ (.ir.json) │ ──emit julia-rhs──► f!/outputs! .jl
|
|
45
47
|
└──────────────┘
|
|
46
48
|
│
|
|
47
49
|
└──────── ModelParserJL.build_system(ir) ──► System
|
|
@@ -9,7 +9,7 @@ commands assume you have run `uv sync` in the repository root.
|
|
|
9
9
|
|---|---|
|
|
10
10
|
| [`models/model_monod_simple.ini`](models/model_monod_simple.ini) | A 2-state Monod CSTR, no inputs. |
|
|
11
11
|
| [`models/model_thermal_tank.ini`](models/model_thermal_tank.ini) | A 2-state heated tank with 3 inputs. |
|
|
12
|
-
| [`run.sh`](run.sh) | Runs the full `parse → validate → emit` pipeline for both models. |
|
|
12
|
+
| [`run.sh`](run.sh) | Runs the full `parse → validate → emit julia → emit julia-rhs` pipeline for both models. |
|
|
13
13
|
|
|
14
14
|
Generated artifacts are written to `outputs/` (git-ignored).
|
|
15
15
|
|
|
@@ -22,6 +22,9 @@ uv run model-parser parse examples/models/model_monod_simple.ini -o examples/out
|
|
|
22
22
|
# canonical IR -> ModelingToolkit (v11) Julia model
|
|
23
23
|
uv run model-parser emit julia examples/outputs/monod.ir.json -o examples/outputs/monod.jl
|
|
24
24
|
|
|
25
|
+
# canonical IR -> plain numerical RHS (f! / outputs!)
|
|
26
|
+
uv run model-parser emit julia-rhs examples/outputs/monod.ir.json -o examples/outputs/monod_rhs.jl
|
|
27
|
+
|
|
25
28
|
# validate against a backend profile
|
|
26
29
|
uv run model-parser validate examples/outputs/monod.ir.json --profile julia-analysis
|
|
27
30
|
uv run model-parser validate examples/models/model_thermal_tank.ini --profile realtime-cpp
|
|
@@ -37,6 +40,8 @@ Or run everything at once:
|
|
|
37
40
|
|
|
38
41
|
- `parse` warns that `[x0]` / `[u0]` initial values are **dropped** from the
|
|
39
42
|
scaffold: initial values belong to a *scenario*, not the model IR.
|
|
40
|
-
- The generated `.jl` is a standalone artifact; load it with `include("monod.jl")`
|
|
43
|
+
- The generated MTK `.jl` is a standalone artifact; load it with `include("monod.jl")`
|
|
41
44
|
and call `build_monod_simple()`. Provide initial conditions and parameter
|
|
42
45
|
overrides at `ODEProblem` construction time.
|
|
46
|
+
- The `emit julia-rhs` view defines `f_<model>!(du, u, p, t)` (and optional
|
|
47
|
+
`outputs_<model>!`); see header comments in the file for `u` / `p` / `inp` packing.
|
|
@@ -11,6 +11,7 @@ for model in monod_simple thermal_tank; do
|
|
|
11
11
|
ini="$here/models/model_${model}.ini"
|
|
12
12
|
ir="$out/${model}.ir.json"
|
|
13
13
|
jl="$out/${model}.jl"
|
|
14
|
+
rhs="$out/${model}_rhs.jl"
|
|
14
15
|
|
|
15
16
|
echo "== $model: parse =="
|
|
16
17
|
uv run model-parser parse "$ini" -o "$ir"
|
|
@@ -21,7 +22,11 @@ for model in monod_simple thermal_tank; do
|
|
|
21
22
|
echo "== $model: emit julia =="
|
|
22
23
|
uv run model-parser emit julia "$ir" -o "$jl"
|
|
23
24
|
echo "wrote $jl"
|
|
25
|
+
|
|
26
|
+
echo "== $model: emit julia-rhs =="
|
|
27
|
+
uv run model-parser emit julia-rhs "$ir" -o "$rhs"
|
|
28
|
+
echo "wrote $rhs"
|
|
24
29
|
echo
|
|
25
30
|
done
|
|
26
31
|
|
|
27
|
-
echo "Done. Generated IR + Julia in $out"
|
|
32
|
+
echo "Done. Generated IR + Julia views in $out"
|
|
@@ -61,6 +61,7 @@ nav:
|
|
|
61
61
|
- "0003: Explicit expression IR": decisions/0003-explicit-expression-ir.md
|
|
62
62
|
- "0004: Target MTK v11 idioms": decisions/0004-target-mtk-v11-idioms.md
|
|
63
63
|
- "0005: CLI verbs parse / emit": decisions/0005-cli-verbs-parse-emit.md
|
|
64
|
+
- "0006: Julia numerical RHS view": decisions/0006-julia-numerical-rhs-view.md
|
|
64
65
|
- API reference: api.md
|
|
65
66
|
- Deployment: deployment/ci-cd.md
|
|
66
67
|
|
|
@@ -5,7 +5,7 @@ models and the transformations around it:
|
|
|
5
5
|
|
|
6
6
|
- **frontends** parse an authoring format (e.g. ExprTk INI) into the IR;
|
|
7
7
|
- **backends** lower the IR into a target view (e.g. a ModelingToolkit Julia
|
|
8
|
-
script);
|
|
8
|
+
script or a plain numerical ``f!`` RHS);
|
|
9
9
|
- **validation** checks an IR against the schema and backend profiles.
|
|
10
10
|
|
|
11
11
|
See ``docs/design/model-parser.md`` for the authoritative product specification.
|
|
@@ -15,4 +15,4 @@ from model_parser.ir import IR_VERSION, IRModel
|
|
|
15
15
|
|
|
16
16
|
__all__ = ["IRModel", "IR_VERSION", "__version__"]
|
|
17
17
|
|
|
18
|
-
__version__ = "0.2.
|
|
18
|
+
__version__ = "0.2.2"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Backends: lower the canonical IR into target views."""
|
|
2
|
+
|
|
3
|
+
from model_parser.backends.julia_expr import JuliaCodegenError, expr_to_julia
|
|
4
|
+
from model_parser.backends.julia_mtk import emit_julia
|
|
5
|
+
from model_parser.backends.julia_rhs import emit_julia_rhs
|
|
6
|
+
|
|
7
|
+
__all__ = ["emit_julia", "emit_julia_rhs", "expr_to_julia", "JuliaCodegenError"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shared Julia expression lowering from the IR expression tree.
|
|
2
|
+
|
|
3
|
+
Used by multiple Julia codegen backends so expression semantics stay in one
|
|
4
|
+
place (see ADR 0003).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from model_parser.ir import Call, Expr, IRModel, Num, Sym
|
|
10
|
+
|
|
11
|
+
# IR op name -> Julia infix operator.
|
|
12
|
+
_INFIX: dict[str, str] = {
|
|
13
|
+
"+": "+",
|
|
14
|
+
"-": "-",
|
|
15
|
+
"*": "*",
|
|
16
|
+
"/": "/",
|
|
17
|
+
"^": "^",
|
|
18
|
+
"==": "==",
|
|
19
|
+
"!=": "!=",
|
|
20
|
+
"<": "<",
|
|
21
|
+
">": ">",
|
|
22
|
+
"<=": "<=",
|
|
23
|
+
">=": ">=",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# IR op name -> Julia function name.
|
|
27
|
+
_FUNCS: dict[str, str] = {
|
|
28
|
+
"max": "max",
|
|
29
|
+
"min": "min",
|
|
30
|
+
"sqrt": "sqrt",
|
|
31
|
+
"exp": "exp",
|
|
32
|
+
"log": "log",
|
|
33
|
+
"abs": "abs",
|
|
34
|
+
"ifelse": "ifelse",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class JuliaCodegenError(ValueError):
|
|
39
|
+
"""Raised when an IR construct cannot be lowered to Julia."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def julia_model_slug(ir: IRModel) -> str:
|
|
43
|
+
"""Return a safe Julia identifier fragment derived from ``ir.model.name``."""
|
|
44
|
+
name = ir.model.name or "model"
|
|
45
|
+
cleaned = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in name)
|
|
46
|
+
if not cleaned or not (cleaned[0].isalpha() or cleaned[0] == "_"):
|
|
47
|
+
cleaned = f"m_{cleaned}"
|
|
48
|
+
return cleaned
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def julia_number_literal(value: float) -> str:
|
|
52
|
+
"""Format a floating IR literal as Julia source text."""
|
|
53
|
+
if value.is_integer():
|
|
54
|
+
return f"{value:.1f}"
|
|
55
|
+
return repr(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def expr_to_julia(expr: Expr) -> str:
|
|
59
|
+
"""Render an IR expression as a parenthesized Julia expression string."""
|
|
60
|
+
if isinstance(expr, Num):
|
|
61
|
+
return julia_number_literal(expr.value)
|
|
62
|
+
if isinstance(expr, Sym):
|
|
63
|
+
return expr.name
|
|
64
|
+
if isinstance(expr, Call):
|
|
65
|
+
return _render_call(expr)
|
|
66
|
+
raise JuliaCodegenError(f"unknown expression node: {expr!r}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _render_call(call: Call) -> str:
|
|
70
|
+
op = call.op
|
|
71
|
+
args = call.args
|
|
72
|
+
if op == "neg":
|
|
73
|
+
if len(args) != 1:
|
|
74
|
+
raise JuliaCodegenError("'neg' expects exactly one argument")
|
|
75
|
+
return f"(-{expr_to_julia(args[0])})"
|
|
76
|
+
if op in _INFIX:
|
|
77
|
+
if len(args) != 2:
|
|
78
|
+
raise JuliaCodegenError(f"operator {op!r} expects two arguments")
|
|
79
|
+
left, right = (expr_to_julia(a) for a in args)
|
|
80
|
+
return f"({left} {_INFIX[op]} {right})"
|
|
81
|
+
if op in _FUNCS:
|
|
82
|
+
rendered = ", ".join(expr_to_julia(a) for a in args)
|
|
83
|
+
return f"{_FUNCS[op]}({rendered})"
|
|
84
|
+
raise JuliaCodegenError(f"unsupported operator/function {op!r}")
|
|
@@ -17,87 +17,15 @@ The emitted code targets **ModelingToolkit v11** idioms (see ADR 0004):
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from model_parser.
|
|
20
|
+
from model_parser.backends.julia_expr import expr_to_julia, julia_model_slug, julia_number_literal
|
|
21
|
+
from model_parser.ir import IRModel
|
|
21
22
|
|
|
22
23
|
GENERATOR = "model-parser julia-mtk backend"
|
|
23
24
|
|
|
24
|
-
# IR op name -> Julia infix operator.
|
|
25
|
-
_INFIX: dict[str, str] = {
|
|
26
|
-
"+": "+",
|
|
27
|
-
"-": "-",
|
|
28
|
-
"*": "*",
|
|
29
|
-
"/": "/",
|
|
30
|
-
"^": "^",
|
|
31
|
-
"==": "==",
|
|
32
|
-
"!=": "!=",
|
|
33
|
-
"<": "<",
|
|
34
|
-
">": ">",
|
|
35
|
-
"<=": "<=",
|
|
36
|
-
">=": ">=",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# IR op name -> Julia function name.
|
|
40
|
-
_FUNCS: dict[str, str] = {
|
|
41
|
-
"max": "max",
|
|
42
|
-
"min": "min",
|
|
43
|
-
"sqrt": "sqrt",
|
|
44
|
-
"exp": "exp",
|
|
45
|
-
"log": "log",
|
|
46
|
-
"abs": "abs",
|
|
47
|
-
"ifelse": "ifelse",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class JuliaCodegenError(ValueError):
|
|
52
|
-
"""Raised when an IR construct cannot be lowered to Julia."""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def expr_to_julia(expr: Expr) -> str:
|
|
56
|
-
"""Render an IR expression as a parenthesized Julia expression string."""
|
|
57
|
-
if isinstance(expr, Num):
|
|
58
|
-
return _render_number(expr.value)
|
|
59
|
-
if isinstance(expr, Sym):
|
|
60
|
-
return expr.name
|
|
61
|
-
if isinstance(expr, Call):
|
|
62
|
-
return _render_call(expr)
|
|
63
|
-
raise JuliaCodegenError(f"unknown expression node: {expr!r}")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _render_number(value: float) -> str:
|
|
67
|
-
if value.is_integer():
|
|
68
|
-
return f"{value:.1f}"
|
|
69
|
-
return repr(value)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _render_call(call: Call) -> str:
|
|
73
|
-
op = call.op
|
|
74
|
-
args = call.args
|
|
75
|
-
if op == "neg":
|
|
76
|
-
if len(args) != 1:
|
|
77
|
-
raise JuliaCodegenError("'neg' expects exactly one argument")
|
|
78
|
-
return f"(-{expr_to_julia(args[0])})"
|
|
79
|
-
if op in _INFIX:
|
|
80
|
-
if len(args) != 2:
|
|
81
|
-
raise JuliaCodegenError(f"operator {op!r} expects two arguments")
|
|
82
|
-
left, right = (expr_to_julia(a) for a in args)
|
|
83
|
-
return f"({left} {_INFIX[op]} {right})"
|
|
84
|
-
if op in _FUNCS:
|
|
85
|
-
rendered = ", ".join(expr_to_julia(a) for a in args)
|
|
86
|
-
return f"{_FUNCS[op]}({rendered})"
|
|
87
|
-
raise JuliaCodegenError(f"unsupported operator/function {op!r}")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _julia_name(ir: IRModel) -> str:
|
|
91
|
-
name = ir.model.name or "model"
|
|
92
|
-
cleaned = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in name)
|
|
93
|
-
if not cleaned or not (cleaned[0].isalpha() or cleaned[0] == "_"):
|
|
94
|
-
cleaned = f"m_{cleaned}"
|
|
95
|
-
return cleaned
|
|
96
|
-
|
|
97
25
|
|
|
98
26
|
def emit_julia(ir: IRModel) -> str:
|
|
99
27
|
"""Generate a ModelingToolkit v11 Julia script that builds ``ir`` as a System."""
|
|
100
|
-
name =
|
|
28
|
+
name = julia_model_slug(ir)
|
|
101
29
|
iv = ir.independent_variable
|
|
102
30
|
lines: list[str] = []
|
|
103
31
|
|
|
@@ -122,7 +50,7 @@ def emit_julia(ir: IRModel) -> str:
|
|
|
122
50
|
if param.default is None:
|
|
123
51
|
decls.append(param.name)
|
|
124
52
|
else:
|
|
125
|
-
decls.append(f"{param.name} = {
|
|
53
|
+
decls.append(f"{param.name} = {julia_number_literal(param.default)}")
|
|
126
54
|
lines.append(f" @parameters {' '.join(decls)}")
|
|
127
55
|
|
|
128
56
|
# Time-dependent variables: states, inputs, outputs, locals.
|