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.
Files changed (72) hide show
  1. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/AGENTS.md +3 -2
  2. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/PKG-INFO +7 -3
  3. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/README.md +6 -2
  4. apc_model_parser-0.2.2/docs/chatlogs/2026-06-03_julia-rhs-emit-and-library-sync.md +80 -0
  5. apc_model_parser-0.2.2/docs/decisions/0006-julia-numerical-rhs-view.md +52 -0
  6. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/index.md +1 -0
  7. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/model-library-and-versioning.md +3 -2
  8. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/model-parser.md +15 -9
  9. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/storing-mtk-models.md +8 -6
  10. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/README.md +7 -2
  11. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/run.sh +6 -1
  12. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/mkdocs.yml +1 -0
  13. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/pyproject.toml +1 -1
  14. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/__init__.py +2 -2
  15. apc_model_parser-0.2.2/src/model_parser/backends/__init__.py +7 -0
  16. apc_model_parser-0.2.2/src/model_parser/backends/julia_expr.py +84 -0
  17. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/backends/julia_mtk.py +4 -76
  18. apc_model_parser-0.2.2/src/model_parser/backends/julia_rhs.py +129 -0
  19. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/cli.py +20 -2
  20. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_cli.py +6 -0
  21. apc_model_parser-0.2.2/tests/test_julia_rhs.py +58 -0
  22. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/uv.lock +422 -422
  23. apc_model_parser-0.2.1/src/model_parser/backends/__init__.py +0 -5
  24. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/ci.yml +0 -0
  25. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/docs.yml +0 -0
  26. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.github/workflows/release.yml +0 -0
  27. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/.gitignore +0 -0
  28. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/api.md +0 -0
  29. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-01_bootstrap-model-parser.md +0 -0
  30. {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
  31. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-01_docs-pipx-pypi-install.md +0 -0
  32. {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
  33. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/chatlogs/2026-06-03_docs-todo-backlog-mkdocs.md +0 -0
  34. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0001-python-cli-with-julia-backend.md +0 -0
  35. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0002-codegen-over-serialized-system.md +0 -0
  36. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0003-explicit-expression-ir.md +0 -0
  37. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0004-target-mtk-v11-idioms.md +0 -0
  38. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/decisions/0005-cli-verbs-parse-emit.md +0 -0
  39. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/deployment/ci-cd.md +0 -0
  40. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/ir-specification.md +0 -0
  41. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/design/language-strategy.md +0 -0
  42. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/index.md +0 -0
  43. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/conformance-fixtures-parity.md +0 -0
  44. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/diff-bump-hardening.md +0 -0
  45. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/emit-cpp-realtime-backend.md +0 -0
  46. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/emit-ini-round-trip.md +0 -0
  47. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/index.md +0 -0
  48. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/ini-dimensions-inference.md +0 -0
  49. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/ir-schema-migrations.md +0 -0
  50. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/docs/todo/parameter-set-contract-cli.md +0 -0
  51. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/models/model_monod_simple.ini +0 -0
  52. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/examples/models/model_thermal_tank.ini +0 -0
  53. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/Project.toml +0 -0
  54. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/README.md +0 -0
  55. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/julia/ModelParserJL/src/ModelParserJL.jl +0 -0
  56. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/schemas/canonical-ir.schema.json +0 -0
  57. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/__init__.py +0 -0
  58. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/expr_parser.py +0 -0
  59. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/frontends/exprtk_ini.py +0 -0
  60. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/io.py +0 -0
  61. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/__init__.py +0 -0
  62. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/expr.py +0 -0
  63. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/ir/model.py +0 -0
  64. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/schema.py +0 -0
  65. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/semantic_diff.py +0 -0
  66. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/validation/__init__.py +0 -0
  67. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/src/model_parser/validation/validators.py +0 -0
  68. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/conftest.py +0 -0
  69. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_expr_parser.py +0 -0
  70. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_exprtk_ini.py +0 -0
  71. {apc_model_parser-0.2.1 → apc_model_parser-0.2.2}/tests/test_julia_mtk.py +0 -0
  72. {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 --> ModelingToolkit .jl
26
- emit cpp --> (planned) realtime C++
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.1
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 --> ModelingToolkit .jl
30
- emit cpp --> (planned) realtime C++
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 --> ModelingToolkit .jl
17
- emit cpp --> (planned) realtime C++
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
 
@@ -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
- library.lock.json # index: names, paths, content_hash, optional view hashes
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 --> ModelingToolkit .jl
17
- emit cpp --> (planned) realtime C++
18
- emit ini --> (planned) round-trip
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. The first backend generates a
40
- ModelingToolkit (Julia) model script; a Julia companion package
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 <model.ir.json> [-o out.jl]
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. Today: `julia`. Designed
97
- to grow (`emit cpp`, `emit ini`) without touching existing targets.
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
- Two durable artifacts, both derived from the IR:
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` script** — produced by `model-parser emit julia`. A
37
- plain-text, re-runnable artifact that constructs the `System` with current
38
- MTK idioms. Regenerated whenever the IR changes; never hand-edited.
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──► generated .jl ──include──► System
44
- (.ini) │ (.ir.json) │ (re-runnable) (in memory)
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "apc-model-parser"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Convert process-model definitions to and from a canonical intermediate representation."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.1"
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.ir import Call, Expr, IRModel, Num, Sym
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 = _julia_name(ir)
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} = {_render_number(param.default)}")
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.