Date: 2026-10-17
Branch: feat/dyno-importer
Base: master
This refactoring successfully transitions dolo.py away from dolang toward dyno.dynspec for model import, equation parsing, recipe conformity checking, and callable compilation.
-
Short-term goal achieved:
ModelV2runs all core algorithms (perturb,simulate,time_iteration,improved_time_iteration,deterministic_solve,egm,value_iteration) without loss of functionality. -
Legacy syntax retired cleanly: Explicit timing (
x[t±d]) replaces historical conveniences (paren-timingx(1)and untimed variables defaulting to date$0$ ). - Dual execution paths: Pure NumPy calls bypass JAX device transfers and dispatch overheads, mitigating latency penalties for CPU-bound inner loops.
-
Parallel Model Architecture (
ModelV2&yaml_import_v2/dyno_import):- Preserves legacy
Modelandyaml_importintact as a safe fallback and parity benchmark. - Decouples legacy code from modern dynspec-based workflows.
- Preserves legacy
-
Build-Time Expression Emission (
functions_v2.py: _emit):- Eliminates AST-walking at JIT trace time.
- Compiles Lark trees once at model build time into nested expression closures, producing clean arithmetic graphs with hand-written JAX tracing quality.
-
Dual Execution Closures (
NP_FUNCTIONSvs.JNP_FUNCTIONS):- Emits expressions into both NumPy and JAX backends.
- Point calls and row-vectorized NumPy evaluations bypass device marshaling (~470 µs → ~10–20 µs), preserving performance for SciPy-style algorithm loops.
-
Rigorous Parity & Regression Contract (
tests/test_model_v2.py):- 57 passing tests verifying mathematical parity against legacy models, row-vectorized shape contracts, derivative parity, and full algorithmic convergence.
-
Recipe-Driven Conformity & Complementarity (
dyno.dynspec.recipe):- Native parsing of
⟂(and legacy|) complementarities via dynspec's AST. - Recipe-driven positional checking and automatic inference of controls and state boundaries.
- Native parsing of
-
Hardcoded Arity Binding (
functions_v2.py: _with_arity):- Only arities 3, 4, 5, 7, and 9 are currently implemented. If an equation group or recipe layout requires a different arity (e.g. 6, 8, or
$\ge 10$ ), it raisesNotImplementedError. -
Recommendation: Generate callable signatures dynamically via
inspect.Signatureor dynamic wrapper compilation to support arbitrary arities.
- Only arities 3, 4, 5, 7, and 9 are currently implemented. If an equation group or recipe layout requires a different arity (e.g. 6, 8, or
-
Variadic Math Operations in
_emit(functions_v2.py:414):-
maxandminmap tonp.maximum/jnp.maximum. -
np.maximum(x1, x2, out=...)only accepts two positional operands. Expressions with$>2$ arguments (e.g.max(a, b, c)) fail because the 3rd argument is treated as theoutparameter. -
Recommendation: Wrap variadic
min/maxin_emitwithfunctools.reduce(fn, evaluated_args).
-
-
Asymmetric Vectorized
diff=TrueDispatch (functions_v2.py:717-749):- NumPy row-vectorized evaluations use finite differences (
_np_vec_diff), which supportsdeterministic_solve. - JAX row-vectorized inputs raise
NotImplementedError("diff=True is not implemented for vectorized arguments."). -
Recommendation: Either document this limitation clearly or implement vectorized Jacobians for JAX via
jax.vmap(jax.jacrev(...)).
- NumPy row-vectorized evaluations use finite differences (
-
Empty Sequence Risk in
_normalize_batched(functions_v2.py:658):-
N = max(a.shape[0] for a in args if a.ndim == 2)raisesValueErrorif no argument hasndim == 2. -
Recommendation: Add
default=1(consistent with_np_batched).
-
-
Repeated Lark AST Parsing in Calibration Solver (
model_v2.py:226):-
_solve_symbolic_dictre-parses pending equations withparser.parse()on every triangular resolution iteration, and parses them again inside_eval_scalar. - Recommendation: Parse pending expressions into Lark trees once upfront.
-
-
Cache Invalidation on
set_calibration(model_v2.py:652-658):-
set_calibrationinvalidates__calibration__and__exogenous__, but not__domain__or__options__if they were already resolved with old parameter values. - Recommendation: Ensure all derived state depending on calibration is cleared on update.
-
-
Duck-Typing Parity:
ModelV2preserves all public attributes and properties expected bydolo.algos:symbols,variables,equations,definitions,calibration(CalibrationDict),exogenous,domain,endo_grid,discretize,residuals,x_bounds, andinfos. -
LoosyDictAliasing: Safely mapsequilibrium/optimality$\to$ arbitrageand fallback bounds without hijacking explicitcontrols_lb/controls_ubgroups. -
Value Iteration Fixes: Reading bounds via
model.x_boundsand clamping out-of-domain state extrapolation makesvalue_iterationcanonical and robust across both importers. -
Display Canonicity: Terminal
__str__matches legacy colored residual tables;_repr_html_renders clean KaTeX formulas directly via dynspec.
.dynoGrammar Alignment: Block tags ([definitions],[transition],[arbitrage], etc.) and header metadata (@name:,@options:,@domain:) are directly supported.- Explicit Timing: Transitioning model files and notebooks to explicit
x[t±d]notation removes a major syntax rift betweendoloanddyno.
-
Short Term:
- Generalize
_with_arityinfunctions_v2.py. - Fix variadic
min/maxhandling in_emit. - Add
default=1guard to_normalize_batched. - Pre-parse expressions in
_solve_symbolic_dict.
- Generalize
-
Medium Term (Eliminating Temporary Shims):
equation_preprocess.py: Delete once dynspec grammar natively accepts==as an alias for=.langspec.py: Reimplementeval_datafor tagged YAML objects (!Normal,!Cartesian), allowingdolangto be completely removed from dependencies.
-
Long Term (Performance & Backend Choice):
- Single-core JAX execution is fast, but Numba gufuncs with
target="parallel"scale better on large multi-core CPU batches. - Because
_emitgenerates backend-independent expression closures, consider exposing a backend switch (e.g.backend="jax"|"numba") so users can choose between JAX's differentiability and Numba's multi-threaded CPU throughput.
- Single-core JAX execution is fast, but Numba gufuncs with