apc-model-parser 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.
- apc_model_parser-0.1.0.dist-info/METADATA +111 -0
- apc_model_parser-0.1.0.dist-info/RECORD +18 -0
- apc_model_parser-0.1.0.dist-info/WHEEL +4 -0
- apc_model_parser-0.1.0.dist-info/entry_points.txt +2 -0
- model_parser/__init__.py +18 -0
- model_parser/backends/__init__.py +5 -0
- model_parser/backends/julia_mtk.py +159 -0
- model_parser/cli.py +195 -0
- model_parser/frontends/__init__.py +9 -0
- model_parser/frontends/expr_parser.py +183 -0
- model_parser/frontends/exprtk_ini.py +204 -0
- model_parser/io.py +48 -0
- model_parser/ir/__init__.py +55 -0
- model_parser/ir/expr.py +87 -0
- model_parser/ir/model.py +133 -0
- model_parser/schema.py +29 -0
- model_parser/validation/__init__.py +9 -0
- model_parser/validation/validators.py +195 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apc-model-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert process-model definitions to and from a canonical intermediate representation.
|
|
5
|
+
Project-URL: Repository, https://github.com/Advanced-Process-Control/model-parser
|
|
6
|
+
Author: Advanced Process Control
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: codegen,exprtk,intermediate-representation,modelingtoolkit,process-model
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: pydantic>=2.6
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# model-parser
|
|
15
|
+
|
|
16
|
+
**Convert process-model definitions to and from a canonical intermediate
|
|
17
|
+
representation (IR).**
|
|
18
|
+
|
|
19
|
+
`model-parser` is the Advanced Process Control toolbox component that owns the
|
|
20
|
+
*model scaffold* contract and the transformations around it. It parses an
|
|
21
|
+
authoring format (today: the ExprTk-style INI used by the MPC / simulation
|
|
22
|
+
toolchain) into a normalized, backend-independent **canonical IR**, and lowers
|
|
23
|
+
that IR into target views — starting with a generated **ModelingToolkit (Julia)**
|
|
24
|
+
model script.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
|
|
28
|
+
|
|
|
29
|
+
emit julia --> ModelingToolkit .jl
|
|
30
|
+
emit cpp --> (planned) realtime C++
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The IR is the single semantic contract. Adding a backend means writing one
|
|
34
|
+
`lower` + one `export`, not an N×N mesh of view-to-view translators. See
|
|
35
|
+
[`docs/design/model-parser.md`](docs/design/model-parser.md) for the
|
|
36
|
+
authoritative product specification and
|
|
37
|
+
[`docs/decisions/`](docs/decisions/) for the design decision records.
|
|
38
|
+
|
|
39
|
+
## Why two languages?
|
|
40
|
+
|
|
41
|
+
| Concern | Home | Why |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| CLI, AST, IR, JSON Schema, validators, **codegen** | **Python** (this package) | Parsing & orchestration strength; no symbolic runtime needed to emit code. |
|
|
44
|
+
| IR → MTK `System`, simulation, analysis, conformance | **Julia** ([`julia/ModelParserJL`](julia/ModelParserJL/)) | Natural fit for ModelingToolkit; reference for parity tests. |
|
|
45
|
+
|
|
46
|
+
The Python CLI builds and validates the IR and **generates** Julia code; the
|
|
47
|
+
Julia package can additionally load an IR directly into an MTK `System` for
|
|
48
|
+
in-memory, dynamic workflows. Both consume the *same* IR. See
|
|
49
|
+
[ADR 0001](docs/decisions/0001-python-cli-with-julia-backend.md).
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
This project uses **[uv](https://docs.astral.sh/uv/)** for everything.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv sync --all-groups # include dev tools (ruff, pytest, mkdocs)
|
|
57
|
+
uv run model-parser --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 1. ExprTk INI -> canonical IR JSON
|
|
64
|
+
uv run model-parser parse examples/models/model_monod_simple.ini -o monod.ir.json
|
|
65
|
+
|
|
66
|
+
# 2. canonical IR -> ModelingToolkit (v11) Julia model
|
|
67
|
+
uv run model-parser emit julia monod.ir.json -o monod.jl
|
|
68
|
+
|
|
69
|
+
# Supporting commands
|
|
70
|
+
uv run model-parser validate monod.ir.json --profile julia-analysis
|
|
71
|
+
uv run model-parser inspect monod.ir.json
|
|
72
|
+
uv run model-parser ast examples/models/model_monod_simple.ini # debug tree
|
|
73
|
+
uv run model-parser schema -o schemas/canonical-ir.schema.json # export schema
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`parse` accepts `--from exprtk-ini` (default). `validate` accepts either an IR
|
|
77
|
+
`.json` file or an INI file (parsed on the fly). Exit codes: `0` success,
|
|
78
|
+
`1` validation errors, `2` usage / load failure.
|
|
79
|
+
|
|
80
|
+
## How "stored MTK models" work
|
|
81
|
+
|
|
82
|
+
The persisted, version-controlled form of a model is the **IR JSON** plus the
|
|
83
|
+
**generated `.jl` script** — *not* a serialized `System` object. ModelingToolkit
|
|
84
|
+
v11 changed `System` internals significantly (precompilation, removal of
|
|
85
|
+
`defaults`, deprecation of `@mtkmodel`), so serializing the live object is
|
|
86
|
+
brittle across versions. Regenerating from the IR is the durable path. See
|
|
87
|
+
[ADR 0002](docs/decisions/0002-codegen-over-serialized-system.md) and
|
|
88
|
+
[`docs/design/storing-mtk-models.md`](docs/design/storing-mtk-models.md).
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv sync --all-groups
|
|
94
|
+
uv run ruff check .
|
|
95
|
+
uv run ruff format --check .
|
|
96
|
+
uv run pytest
|
|
97
|
+
uv run mkdocs build --strict # same as CI
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Documentation site (after [Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#publishing-with-a-custom-github-actions-workflow) is set to GitHub Actions):
|
|
101
|
+
[https://advanced-process-control.github.io/model-parser/](https://advanced-process-control.github.io/model-parser/)
|
|
102
|
+
|
|
103
|
+
## Scope
|
|
104
|
+
|
|
105
|
+
**In scope:** authoring-format parsing, the canonical IR data model + JSON
|
|
106
|
+
Schema, semantic & profile validation, IR↔backend lowering/codegen.
|
|
107
|
+
|
|
108
|
+
**Out of scope:** parameter identification, scenario execution, simulation
|
|
109
|
+
result storage, controller synthesis, deployment. Those are sibling tools that
|
|
110
|
+
*consume* the IR. The parser stays small (see
|
|
111
|
+
[`AGENTS.md`](AGENTS.md) scope guardrails).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
model_parser/__init__.py,sha256=dACj1q8TSXCZLi8otXh2vmfUnCjfkrOdRYp9WKohWZU,641
|
|
2
|
+
model_parser/cli.py,sha256=aIuJC6bNVei7Gy2Kp7vu8KnqDTAuEXN9s1wK6XEWSUE,6654
|
|
3
|
+
model_parser/io.py,sha256=GkG2U4GILQVTq8fdb1c1tA9UOEsA69e9wdABQplDFnk,1662
|
|
4
|
+
model_parser/schema.py,sha256=xvyBmFjGGKrVLNzWM2ssC1CVBPtJtgsiXHNGie0oNZM,971
|
|
5
|
+
model_parser/backends/__init__.py,sha256=Y3YmRruw9whtvvtp2zmY-Ne4APNwFxNxhFSaxNxJhQA,172
|
|
6
|
+
model_parser/backends/julia_mtk.py,sha256=1P79lKVEgntYrQmR2VwAl1RaJKSS66cscAmDD_W-QzE,5363
|
|
7
|
+
model_parser/frontends/__init__.py,sha256=to_N4GEqlGX7-ipHuIOztOS-T8xCy8UjygMfKvOMztw,235
|
|
8
|
+
model_parser/frontends/expr_parser.py,sha256=Um49TQltfDhk0qx0xotF9nVbPS0QSquz2PjgqpfSYkw,5875
|
|
9
|
+
model_parser/frontends/exprtk_ini.py,sha256=JpgP_sJqYmk-huSXaReM4ZywSk6MTESAoLfAwSGYWTM,7138
|
|
10
|
+
model_parser/ir/__init__.py,sha256=CIzn0srFqsefwZwDkwu__lw-JpVSLM2j3rvoI73Q8ak,813
|
|
11
|
+
model_parser/ir/expr.py,sha256=890lOT1SqFPmNxzqT4c4ZiXUKz_DQKL3cW5rJDgNSlU,2837
|
|
12
|
+
model_parser/ir/model.py,sha256=dPYqQOWhQnAWmGTQJ0d0gz8_FZ2O324UD2GvK08-H6Q,3887
|
|
13
|
+
model_parser/validation/__init__.py,sha256=oe4BBC2AgumuIoA03tIVNtGoxlTgqfbckSjZVPU5-j4,235
|
|
14
|
+
model_parser/validation/validators.py,sha256=VwTUXyQhz6uYXoYkwWxGR4lUwO0SWJgmCj5rnQQsurQ,6207
|
|
15
|
+
apc_model_parser-0.1.0.dist-info/METADATA,sha256=xXObGpQrAwaDEjqRqZJJ0zk2GJUK6etYwdGl_fsJ_WE,4760
|
|
16
|
+
apc_model_parser-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
apc_model_parser-0.1.0.dist-info/entry_points.txt,sha256=7Lxr9OwAdGWx007g6CSk6-J_fx6l-V7BF2Umj8tj_Yc,54
|
|
18
|
+
apc_model_parser-0.1.0.dist-info/RECORD,,
|
model_parser/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""model-parser: convert process-model definitions to and from a canonical IR.
|
|
2
|
+
|
|
3
|
+
The package owns the canonical intermediate representation (IR) for process
|
|
4
|
+
models and the transformations around it:
|
|
5
|
+
|
|
6
|
+
- **frontends** parse an authoring format (e.g. ExprTk INI) into the IR;
|
|
7
|
+
- **backends** lower the IR into a target view (e.g. a ModelingToolkit Julia
|
|
8
|
+
script);
|
|
9
|
+
- **validation** checks an IR against the schema and backend profiles.
|
|
10
|
+
|
|
11
|
+
See ``docs/design/model-parser.md`` for the authoritative product specification.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from model_parser.ir import IR_VERSION, IRModel
|
|
15
|
+
|
|
16
|
+
__all__ = ["IRModel", "IR_VERSION", "__version__"]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Julia / ModelingToolkit backend: lower the IR into a generated ``.jl`` script.
|
|
2
|
+
|
|
3
|
+
This is the ``lower`` transformation from IR to the Julia analysis view. It
|
|
4
|
+
*generates source code* rather than serializing a compiled ``System`` object —
|
|
5
|
+
see ADR 0002. The generated script is a re-runnable, diffable artifact whose
|
|
6
|
+
source of truth remains the IR.
|
|
7
|
+
|
|
8
|
+
The emitted code targets **ModelingToolkit v11** idioms (see ADR 0004):
|
|
9
|
+
|
|
10
|
+
- explicit ``System(eqs, t)`` construction (no deprecated ``@mtkmodel`` DSL);
|
|
11
|
+
- ``mtkcompile`` instead of ``structural_simplify``;
|
|
12
|
+
- ``t``/``D`` imported as ``t_nounits`` / ``D_nounits``;
|
|
13
|
+
- parameter defaults attached via ``@parameters name = value`` (translated by
|
|
14
|
+
MTK v11 into ``initial_conditions`` for constant values);
|
|
15
|
+
- inputs declared and passed to ``mtkcompile(sys; inputs = [...])``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from model_parser.ir import Call, Expr, IRModel, Num, Sym
|
|
21
|
+
|
|
22
|
+
GENERATOR = "model-parser julia-mtk backend"
|
|
23
|
+
|
|
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
|
+
|
|
98
|
+
def emit_julia(ir: IRModel) -> str:
|
|
99
|
+
"""Generate a ModelingToolkit v11 Julia script that builds ``ir`` as a System."""
|
|
100
|
+
name = _julia_name(ir)
|
|
101
|
+
iv = ir.independent_variable
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
|
|
104
|
+
lines.append(f"# Generated by {GENERATOR}. Do not edit by hand.")
|
|
105
|
+
lines.append("# Source of truth: the canonical IR. Regenerate after IR changes.")
|
|
106
|
+
if ir.provenance is not None:
|
|
107
|
+
lines.append(f"# IR tool: {ir.provenance.tool}")
|
|
108
|
+
if ir.provenance.content_hash:
|
|
109
|
+
lines.append(f"# IR content hash: {ir.provenance.content_hash}")
|
|
110
|
+
lines.append(f"# IR version: {ir.ir_version}")
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append("using ModelingToolkit")
|
|
113
|
+
lines.append(f"using ModelingToolkit: t_nounits as {iv}, D_nounits as D")
|
|
114
|
+
lines.append("")
|
|
115
|
+
lines.append(f'"""Build the `{ir.model.name}` model as a compiled MTK `System`."""')
|
|
116
|
+
lines.append(f"function build_{name}()")
|
|
117
|
+
|
|
118
|
+
# Parameters with defaults.
|
|
119
|
+
if ir.parameters:
|
|
120
|
+
decls = []
|
|
121
|
+
for param in ir.parameters:
|
|
122
|
+
if param.default is None:
|
|
123
|
+
decls.append(param.name)
|
|
124
|
+
else:
|
|
125
|
+
decls.append(f"{param.name} = {_render_number(param.default)}")
|
|
126
|
+
lines.append(f" @parameters {' '.join(decls)}")
|
|
127
|
+
|
|
128
|
+
# Time-dependent variables: states, inputs, outputs, locals.
|
|
129
|
+
tvars = (
|
|
130
|
+
[v.name for v in ir.states]
|
|
131
|
+
+ [v.name for v in ir.inputs]
|
|
132
|
+
+ [v.name for v in ir.outputs]
|
|
133
|
+
+ [local.name for local in ir.locals]
|
|
134
|
+
)
|
|
135
|
+
if tvars:
|
|
136
|
+
decls = " ".join(f"{n}({iv})" for n in tvars)
|
|
137
|
+
lines.append(f" @variables {decls}")
|
|
138
|
+
|
|
139
|
+
lines.append("")
|
|
140
|
+
lines.append(" eqs = [")
|
|
141
|
+
for local in ir.locals:
|
|
142
|
+
lines.append(f" {local.name} ~ {expr_to_julia(local.expr)},")
|
|
143
|
+
for diff in ir.equations.differential:
|
|
144
|
+
lines.append(f" D({diff.state}) ~ {expr_to_julia(diff.rhs)},")
|
|
145
|
+
for out in ir.equations.outputs:
|
|
146
|
+
lines.append(f" {out.output} ~ {expr_to_julia(out.rhs)},")
|
|
147
|
+
lines.append(" ]")
|
|
148
|
+
lines.append("")
|
|
149
|
+
lines.append(f" @named {name} = System(eqs, {iv})")
|
|
150
|
+
|
|
151
|
+
if ir.inputs:
|
|
152
|
+
input_list = ", ".join(v.name for v in ir.inputs)
|
|
153
|
+
lines.append(f" return mtkcompile({name}; inputs = [{input_list}])")
|
|
154
|
+
else:
|
|
155
|
+
lines.append(f" return mtkcompile({name})")
|
|
156
|
+
|
|
157
|
+
lines.append("end")
|
|
158
|
+
lines.append("")
|
|
159
|
+
return "\n".join(lines)
|
model_parser/cli.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Command-line interface for model-parser.
|
|
2
|
+
|
|
3
|
+
The CLI exposes the IR transformations as composable subcommands. The two core
|
|
4
|
+
verbs follow the ecosystem's transformation vocabulary:
|
|
5
|
+
|
|
6
|
+
model-parser parse <model.ini> # authoring format -> canonical IR JSON
|
|
7
|
+
model-parser emit julia <model.ir.json> # canonical IR -> MTK Julia script
|
|
8
|
+
|
|
9
|
+
Plus supporting commands: ``validate``, ``inspect``, ``ast``, and ``schema``.
|
|
10
|
+
|
|
11
|
+
Exit codes:
|
|
12
|
+
0 success, no ERROR-level diagnostics
|
|
13
|
+
1 at least one ERROR diagnostic during execution
|
|
14
|
+
2 invalid usage / load failure before meaningful work
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
from model_parser import __version__
|
|
25
|
+
from model_parser.backends import emit_julia
|
|
26
|
+
from model_parser.frontends import parse_ini_file
|
|
27
|
+
from model_parser.io import dumps_ir, load_ir, save_ir, with_content_hash
|
|
28
|
+
from model_parser.schema import dumps_schema
|
|
29
|
+
from model_parser.validation import validate_ir
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
add_completion=False,
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
help="Convert process-model definitions to and from a canonical IR.",
|
|
35
|
+
)
|
|
36
|
+
emit_app = typer.Typer(no_args_is_help=True, help="Lower the canonical IR into a target view.")
|
|
37
|
+
app.add_typer(emit_app, name="emit")
|
|
38
|
+
|
|
39
|
+
_err = typer.style("error:", fg=typer.colors.RED, bold=True)
|
|
40
|
+
_warn = typer.style("warning:", fg=typer.colors.YELLOW, bold=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _echo_err(message: str) -> None:
|
|
44
|
+
typer.echo(f"{_err} {message}", err=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _echo_warn(message: str) -> None:
|
|
48
|
+
typer.echo(f"{_warn} {message}", err=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.callback(invoke_without_command=True)
|
|
52
|
+
def _main(
|
|
53
|
+
version: bool = typer.Option(
|
|
54
|
+
False, "--version", help="Show the version and exit.", is_eager=True
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
if version:
|
|
58
|
+
typer.echo(f"model-parser {__version__}")
|
|
59
|
+
raise typer.Exit()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def parse(
|
|
64
|
+
source: Path = typer.Argument(..., exists=True, readable=True, help="Authoring file."),
|
|
65
|
+
output: Path | None = typer.Option(
|
|
66
|
+
None, "-o", "--output", help="IR JSON output path (default: stdout)."
|
|
67
|
+
),
|
|
68
|
+
from_format: str = typer.Option(
|
|
69
|
+
"exprtk-ini", "--from", help="Authoring format of the source file."
|
|
70
|
+
),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Parse an authoring file (default: ExprTk INI) into canonical IR JSON."""
|
|
73
|
+
if from_format != "exprtk-ini":
|
|
74
|
+
_echo_err(f"unsupported source format {from_format!r} (only 'exprtk-ini' for now)")
|
|
75
|
+
raise typer.Exit(code=2)
|
|
76
|
+
|
|
77
|
+
result = parse_ini_file(str(source))
|
|
78
|
+
ir = with_content_hash(result.ir)
|
|
79
|
+
for warning in result.warnings:
|
|
80
|
+
_echo_warn(warning)
|
|
81
|
+
|
|
82
|
+
if output is None:
|
|
83
|
+
typer.echo(dumps_ir(ir), nl=False)
|
|
84
|
+
else:
|
|
85
|
+
save_ir(ir, output)
|
|
86
|
+
typer.echo(f"wrote IR to {output}", err=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@emit_app.command("julia")
|
|
90
|
+
def emit_julia_cmd(
|
|
91
|
+
ir_file: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON file."),
|
|
92
|
+
output: Path | None = typer.Option(
|
|
93
|
+
None, "-o", "--output", help="Julia output path (default: stdout)."
|
|
94
|
+
),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Generate a ModelingToolkit (v11) Julia script from an IR file."""
|
|
97
|
+
ir = load_ir(ir_file)
|
|
98
|
+
code = emit_julia(ir)
|
|
99
|
+
if output is None:
|
|
100
|
+
typer.echo(code, nl=False)
|
|
101
|
+
else:
|
|
102
|
+
Path(output).write_text(code, encoding="utf-8")
|
|
103
|
+
typer.echo(f"wrote Julia model to {output}", err=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command()
|
|
107
|
+
def validate(
|
|
108
|
+
target: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON or INI file."),
|
|
109
|
+
profile: str | None = typer.Option(
|
|
110
|
+
None, "--profile", help="Backend profile to check (e.g. julia-analysis, realtime-cpp)."
|
|
111
|
+
),
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Validate an IR (or an INI parsed on the fly) and report diagnostics."""
|
|
114
|
+
ir = _load_any(target)
|
|
115
|
+
report = validate_ir(ir, profile=profile)
|
|
116
|
+
for diag in report.diagnostics:
|
|
117
|
+
prefix = _err if diag.level == "ERROR" else _warn
|
|
118
|
+
typer.echo(f"{prefix} [{diag.code}] {diag.message}", err=True)
|
|
119
|
+
if report.ok:
|
|
120
|
+
typer.echo(f"OK: {ir.model.name} valid" + (f" for profile {profile!r}" if profile else ""))
|
|
121
|
+
else:
|
|
122
|
+
typer.echo(f"FAILED: {len(report.errors)} error(s)", err=True)
|
|
123
|
+
raise typer.Exit(code=1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command()
|
|
127
|
+
def inspect(
|
|
128
|
+
target: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON or INI file."),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Print a human-readable summary of a model."""
|
|
131
|
+
ir = _load_any(target)
|
|
132
|
+
typer.echo(f"model: {ir.model.name}")
|
|
133
|
+
if ir.model.description:
|
|
134
|
+
typer.echo(f"description: {ir.model.description}")
|
|
135
|
+
typer.echo(f"ir version: {ir.ir_version}")
|
|
136
|
+
typer.echo(f"states: {len(ir.states)} ({', '.join(v.name for v in ir.states)})")
|
|
137
|
+
typer.echo(f"inputs: {len(ir.inputs)} ({', '.join(v.name for v in ir.inputs)})")
|
|
138
|
+
typer.echo(f"outputs: {len(ir.outputs)} ({', '.join(v.name for v in ir.outputs)})")
|
|
139
|
+
typer.echo(f"parameters: {len(ir.parameters)}")
|
|
140
|
+
typer.echo(f"locals: {len(ir.locals)}")
|
|
141
|
+
typer.echo(f"profiles: {', '.join(ir.profiles)}")
|
|
142
|
+
if ir.provenance and ir.provenance.content_hash:
|
|
143
|
+
typer.echo(f"hash: {ir.provenance.content_hash}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command()
|
|
147
|
+
def ast(
|
|
148
|
+
source: Path = typer.Argument(..., exists=True, readable=True, help="ExprTk INI file."),
|
|
149
|
+
output: Path | None = typer.Option(
|
|
150
|
+
None, "-o", "--output", help="AST/IR debug JSON output path (default: stdout)."
|
|
151
|
+
),
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Export the parsed IR as a debug JSON tree (parser inspection)."""
|
|
154
|
+
result = parse_ini_file(str(source))
|
|
155
|
+
text = dumps_ir(result.ir)
|
|
156
|
+
if output is None:
|
|
157
|
+
typer.echo(text, nl=False)
|
|
158
|
+
else:
|
|
159
|
+
Path(output).write_text(text, encoding="utf-8")
|
|
160
|
+
typer.echo(f"wrote debug tree to {output}", err=True)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command()
|
|
164
|
+
def schema(
|
|
165
|
+
output: Path | None = typer.Option(
|
|
166
|
+
None, "-o", "--output", help="JSON Schema output path (default: stdout)."
|
|
167
|
+
),
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Print or write the canonical IR JSON Schema."""
|
|
170
|
+
text = dumps_schema()
|
|
171
|
+
if output is None:
|
|
172
|
+
typer.echo(text, nl=False)
|
|
173
|
+
else:
|
|
174
|
+
Path(output).write_text(text, encoding="utf-8")
|
|
175
|
+
typer.echo(f"wrote schema to {output}", err=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _load_any(path: Path):
|
|
179
|
+
"""Load an IR from a ``.json`` file, or parse an INI file on the fly."""
|
|
180
|
+
if path.suffix.lower() == ".json":
|
|
181
|
+
return load_ir(path)
|
|
182
|
+
return parse_ini_file(str(path)).ir
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run() -> None:
|
|
186
|
+
"""Entry point wrapper that maps uncaught errors to exit code 2."""
|
|
187
|
+
try:
|
|
188
|
+
app()
|
|
189
|
+
except (ValueError, FileNotFoundError) as exc: # parse/codegen errors
|
|
190
|
+
_echo_err(str(exc))
|
|
191
|
+
sys.exit(2)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
run()
|