Skip to content

Commit 78b6eca

Browse files
fix(harness): I() wrap t_observed/observed_dv too; symmetric scalar coercion
The previous sims_at_observed I() wrap (commit 41a2f95) closed only one third of the bug class. The same auto_unbox-without-I() collapse hits ``t_observed`` and ``observed_dv`` on n_obs == 1 subjects: ``jsonlite::toJSON(..., auto_unbox = TRUE)`` of a length-1 numeric vector yields a JSON scalar (``57.5``) rather than a length-1 array (``[57.5]``), and ``PredictedSimulationsSubject.t_observed: list[float]`` rejects the float with ``Input should be a valid list``. Surfaces on every n_obs == 1 subject in pheno_sd; aborts the entire fixture before any per-fold scorecard is produced. Two-layer fix (symmetric with the existing sims_at_observed wire) ----------------------------------------------------------------- - **R-side primary** (r/harness.R): wrap ``t_observed = I(t_obs)`` and ``observed_dv = I(dv_obs)`` in ``.simulate_posterior_predictive`` so jsonlite preserves array shape under auto_unbox = TRUE. - **Pydantic-side defence in depth** (backends/r_schemas.py): a new ``field_validator("t_observed", "observed_dv", mode="before")`` coerces a bare scalar to ``[float(v)]`` so a future R-side regression fails the harness pin (see below) instead of the Phase-1 weekly run. Tests ----- 2 new pins in ``TestPredictedSimulations1DCoercion``: - ``test_harness_uses_I_wrap_for_sparse_subjects`` extended to also pin ``t_observed = I(t_obs)`` and ``observed_dv = I(dv_obs)`` substrings in r/harness.R. - ``test_scalar_t_observed_is_coerced_to_list`` exercises the full n_obs == 1 auto_unbox-without-I() shape (bare floats for both obs vectors + flat list for sims) and asserts all three coerce. Bundled changes (unrelated, were already in the working tree) ------------------------------------------------------------- - README, CHANGELOG, docs (ARCHITECTURE, FORMULAR, suite_a/c READMEs, src/r README): documentation drift cleanup that landed alongside. - src/apmode/cli.py, bundle/models.py, dsl/stan_emitter.py: ruff / formatting touch-ups. - tests/unit/test_deep_inspect.py: matching test updates. Lint + tests ------------ ``ruff check`` + ``ruff format`` clean across src/ + tests/. ``mypy --strict`` clean over 136 source files. Full non-live sweep: 2551 passed, 4 skipped, 17 deselected, 15 warnings in 135 s.
1 parent 41a2f95 commit 78b6eca

14 files changed

Lines changed: 404 additions & 109 deletions

File tree

CHANGELOG.md

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed — Documentation accuracy and drift cleanup
11+
12+
- Synced README / CLAUDE auto-managed counters to the current test collection
13+
and clarified the CLI-command count versus registered command groups.
14+
- Reconciled HTTP API security documentation with the implemented static
15+
API-key and dataset-root requirements for non-loopback binds.
16+
- Updated Architecture, Formular, Suite A/C, and R adapter docs for the
17+
v0.6.1-rc1 surface: 10 Formular transforms, shipped API/RO-Crate commands,
18+
v0.7 absorption preview forms, BLQ M3/M4 Stan support, and the current
19+
benchmark fixture rosters.
20+
- Removed stale/broken ADR references and repo-resident process attribution
21+
from documentation and changelog prose.
22+
23+
### Fixed — CLI trace/graph hardening: typed traces, valid exporters, DAG invariants
24+
25+
The `apmode trace` and `apmode graph` deep-inspection commands now handle
26+
malformed or adversarial bundle artifacts without corrupting machine-readable
27+
output or crashing read-only inspection:
28+
29+
- `apmode trace --json` keeps stdout as pure JSON even when
30+
`agentic_iterations.jsonl` contains corrupt lines. Invalid iteration rows are
31+
validated against `AgenticIterationEntry` and skipped with human-mode
32+
diagnostics rather than causing renderer crashes.
33+
- `apmode trace --cost` now tolerates malformed token/cost/time fields in
34+
`iter_*_meta.json`, warning in human mode and using zero for bad numeric
35+
cells instead of raising `ValueError`.
36+
- `apmode graph --format dot` and `--format mermaid` now emit raw text on
37+
stdout. Rich no longer interprets DOT attributes or Mermaid labels as markup
38+
(for example `node [shape=box, fontsize=10]` is preserved).
39+
- Mermaid export uses stable generated node IDs (`n0`, `n1`, ...) so candidate
40+
IDs such as `a-b` and `a_b` cannot collide after sanitization; original
41+
candidate IDs remain visible in labels.
42+
- `SearchGraph` validation now rejects duplicate `candidate_id` values and
43+
edges that reference missing nodes, aligning the bundle model with the CLI's
44+
advertised DAG semantics.
45+
- `apmode graph --backend` help now includes `bayesian_stan`, matching the
46+
accepted backend enum.
47+
48+
Tests: `tests/unit/test_deep_inspect.py` adds regression pins for corrupt trace
49+
rows under `--json`, invalid typed iteration rows, malformed cost metadata, DOT
50+
stdout, Mermaid ID collisions, duplicate graph nodes, and dangling graph edges.
51+
1052
### Fixed — Sparse-data harness: 1D `sims_at_observed` no longer crashes the runner
1153

1254
When a subject has a single observation (the `pheno_sd` neonatal
@@ -152,9 +194,9 @@ SumIG closed-form input rate integrates to ≈1 over (0, ∞).
152194

153195
### Fixed — Multi-CLI code review hardening pass: cancellation, durability, digest scoping, harness diagnostics
154196

155-
A multi-model code review (droid, crush=glm-5, gemini-3-pro, opencode=minimax-m2.7) of the
156-
`api/`, `bundle/`, `backends/`, and `bayes/` surfaces produced 9 actionable findings, all
157-
addressed in this set of patches. No public API change; existing bundles remain loadable.
197+
A focused code review of the `api/`, `bundle/`, `backends/`, and `bayes/`
198+
surfaces produced 9 actionable findings, all addressed in this set of patches.
199+
No public API change; existing bundles remain loadable.
158200

159201
**API run-state cancellation contract — slot leak on double-cancel closed.**
160202
`apmode.api.runs.execute_run` now wraps the `on_complete` callback and the `FAILED`
@@ -562,11 +604,10 @@ and SAEM streaming progress.
562604
`--allow-public` test was updated to satisfy the concurrent
563605
`APMODE_API_KEY` + `--dataset-root` security gates.
564606

565-
### Fixed — Multi-model audit pass: cancellation, auth, digest integrity
607+
### Fixed — Audit pass: cancellation, auth, digest integrity
566608

567-
A targeted hardening sweep informed by an external multi-model review
568-
(self + clink: gemini, droid, crush, opencode). All 2035 non-live unit
569-
tests pass; `mypy --strict` and `ruff check` are clean.
609+
A targeted hardening sweep of cancellation, auth, and digest-integrity surfaces.
610+
All 2035 non-live unit tests pass; `mypy --strict` and `ruff check` are clean.
570611

571612
- **Bayesian Gate 1 no longer clobbers the classical Gate 1 audit
572613
artifact.** `BundleEmitter.write_gate_decision` now accepts

README.md

Lines changed: 27 additions & 21 deletions
Large diffs are not rendered by default.

benchmarks/suite_a/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Requires: R 4.4+, rxode2, jsonlite, lotri
1818
| a2_2cmt_iv_parallel_mm.csv | 2-cmt IV, parallel linear+MM elim | Compartment count + nonlinear CL |
1919
| a3_transit_1cmt_linear.csv | Transit (n=3), 1-cmt, linear elim | Transit chain detection |
2020
| a4_1cmt_oral_mm.csv | 1-cmt oral, MM elimination | Nonlinear clearance detection |
21+
| a5_tmdd_qss.csv | TMDD quasi-steady-state (SC mAb) | TMDD vs. 2-cmt confusion |
22+
| a6_1cmt_covariates.csv | 1-cmt oral + allometric WT + renal covariate | Covariate structure recovery |
23+
| a7_2cmt_node_absorption.csv | 2-cmt + NODE saturable absorption | NODE shape recovery + surrogate fidelity |
24+
| a8_1cmt_tvcl_covariate.csv | 1-cmt oral + time-varying CL + CRCL covariate | Time-varying clearance + covariate recovery |
2125

2226
## Reference Parameters
2327

benchmarks/suite_c/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
> APMODE plan Tasks 38–43, blueprint in `docs/plans/2026-04-24-apmode-v0.6-completion.md`.
44
55
Phase-1 of Suite C compares APMODE's PK fits against published, peer-reviewed
6-
reference parameterizations on real clinical datasets. Each fixture pairs a
6+
reference parameterizations and selected ground-truth reference datasets. Each fixture pairs a
77
NONMEM-style CSV (resolved via `dataset_id` against
88
`benchmarks/datasets/registry.yaml`) with a `DSLSpec` JSON and a
9-
`LiteratureFixture` YAML capturing the published parameter values plus the
9+
`LiteratureFixture` YAML capturing the reference parameter values plus the
1010
parameterization mapping needed to translate published symbol names
1111
(e.g. `TVCL`) into APMODE's DSL-canonical names (e.g. `CL`).
1212

@@ -16,8 +16,10 @@ parameterization mapping needed to translate published symbol names
1616
|------------|-------|--------------|---------------|
1717
| `theophylline_boeckmann_1992` | oral | 1-cmt + FO ka | [10.1002/psp4.12471](https://doi.org/10.1002/psp4.12471) (Schoemaker et al. 2019, nlmixr SAEM/FOCEI grid which contains the Theoph fit) |
1818
| `warfarin_funaki_2018` | oral | 1-cmt + lagged-FO ka | [10.1002/psp4.12445](https://doi.org/10.1002/psp4.12445) (Fidler et al. 2019, nlmixr documentation including Holford warfarin) |
19-
| `mavoglurant_wendling_2015` | oral | 2-cmt + FO ka | [10.1007/s11095-014-1574-1](https://doi.org/10.1007/s11095-014-1574-1) (Wendling et al. 2015, mavoglurant population PK; APMODE uses the simpler 2-cmt approximation since the published sum-of-inverse-Gaussian input function is outside DSL scope) |
20-
| `gentamicin_germovsek_2017` | iv_bolus | 1-cmt | [10.1128/AAC.00481-17](https://doi.org/10.1128/AAC.00481-17) (Germovsek et al. 2017, gentamicin IOV neonates) |
19+
| `mavoglurant_wendling_2015` | oral | 2-cmt + FO ka | [10.1007/s11095-014-1574-1](https://doi.org/10.1007/s11095-014-1574-1) (Wendling et al. 2015, mavoglurant population PK; the Phase-1 fixture uses the simpler 2-cmt approximation, while SumIG absorption remains a v0.7 preview path) |
20+
| `phenobarbital_grasela_1985` | iv_bolus | 1-cmt | [10.1159/000457062](https://doi.org/10.1159/000457062) (Grasela & Donn 1985, phenobarbital neonatal PK) |
21+
| `oral_1cpt_acop_2016` | oral | 1-cmt + FO ka | [10.32614/CRAN.package.nlmixr2data](https://doi.org/10.32614/CRAN.package.nlmixr2data) (ACOP 2016 / `nlmixr2data::Oral_1CPT` ground-truth recovery fixture) |
22+
| `gentamicin_germovsek_2017` | iv_bolus | 1-cmt | [10.1128/AAC.02659-16](https://doi.org/10.1128/AAC.02659-16) (Germovsek et al. 2017, gentamicin IOV neonates) |
2123
| `schoemaker_nlmixr2_tutorial` | iv_bolus | 1-cmt | [10.1002/psp4.12471](https://doi.org/10.1002/psp4.12471) (Schoemaker et al. 2019, `pkBolus1cmt` reference values) |
2224

2325
## Why the names changed from the original plan draft

docs/ARCHITECTURE.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# APMODE Technical Architecture
22

33
**Version:** 0.3
4-
**Date:** 2026-04-17
5-
**Status:** Current (tracks APMODE 0.5.0-rc2; Phase 3 in progress)
4+
**Date:** 2026-04-25
5+
**Status:** Current (tracks APMODE 0.6.1-rc1; Phase 3 in progress)
66
**Derived from:** PRD v0.3 (§3–§8)
77
**Supersedes:** v0.2 (2026-04-13). Change summary: Phase 2/3 framing removed where
88
shipped (Bayesian backend, NODE, agentic LLM, Gate 2.5, FREM, Gate 3 ranking all
@@ -19,8 +19,9 @@ Flowcept) moved to §10 Non-goals.
1919
+ subprocess) is the main loop. The `BackendRunner` protocol is designed so Flyte
2020
or Temporal could be swapped in for GPU scheduling without rewriting adapters, but
2121
no such migration is planned (see §10).
22-
2. **Process isolation is non-negotiable.** Every backend (R/nlmixr2, Python/JAX,
23-
LLM SDK, R/Rstan) runs in its own subprocess. A segfault in R must not crash the
22+
2. **Process isolation is non-negotiable.** Every backend boundary (R/nlmixr2,
23+
Python/JAX, LLM SDK calls, cmdstanpy/CmdStan) is isolated from the orchestrator
24+
where practical. A segfault in R must not crash the
2425
orchestrator.
2526
3. **The DSL is the moat.** All model specifications flow through the typed PK DSL
2627
(Formular). No backend emits or consumes model code outside of a deterministic
@@ -79,7 +80,7 @@ See `docs/FORMULAR.md` for the language reference.
7980
| AST canonicalization | `src/apmode/dsl/normalize.py` |
8081
| Semantic validator | `src/apmode/dsl/validator.py::validate_dsl(spec, lane=...)` — lane-aware, non-fail-fast |
8182
| Priors + admissibility | `src/apmode/dsl/priors.py` (families, target taxonomy, `_VALID_FAMILIES` matrix) |
82-
| Transforms | `src/apmode/dsl/transforms.py` (6) + `prior_transforms.py::SetPrior` (1) — 7 total |
83+
| Transforms | `src/apmode/dsl/transforms.py` (9) + `prior_transforms.py::SetPrior` (1) — 10 total |
8384
| Emitters | `nlmixr2_emitter.py`, `stan_emitter.py`, `frem_emitter.py` |
8485
| Testing | Hypothesis property tests + syrupy golden masters for emitter output |
8586

@@ -93,7 +94,7 @@ See `docs/FORMULAR.md` for the language reference.
9394
| R invocation | `Rscript src/apmode/r/harness.R` via subprocess; file-based I/O (§4.2) |
9495
| Stan invocation | `cmdstanpy.CmdStanModel` via `src/apmode/bayes/harness.py` wrapped by `bayesian_runner.py` |
9596
| Retry/timeout | Bespoke logic in each runner; timeout from policy file, killed attempts write to new `attempt_id/` subdir |
96-
| CLI | Typer (`src/apmode/cli.py`) — 15 commands |
97+
| CLI | Typer (`src/apmode/cli.py`) — 16 direct commands plus registered `bundle` / `completion` groups |
9798

9899
**Note on deployment posture.** The codebase runs natively on the user's machine via
99100
`uv`; there is no Docker-Compose stack, no R container, no K8s. Users who want
@@ -212,8 +213,8 @@ here.
212213

213214
GitHub Actions: `uv sync --all-extras` → pytest → mypy strict → ruff check + format.
214215
Pre-commit runs ruff + mypy + the policy validator. Matrix is Python 3.12 / 3.13 /
215-
3.14. The benchmark suites run as separate workflows (Suite A nightly, B weekly, C
216-
quarterly — cadence tracked in `.github/workflows/`).
216+
3.14. Suite C has a dedicated workflow in `.github/workflows/suite_c_phase1.yml`;
217+
other benchmark cadences are operator-driven unless a workflow exists in `.github/workflows/`.
217218

218219
---
219220

@@ -222,7 +223,7 @@ quarterly — cadence tracked in `.github/workflows/`).
222223
```
223224
┌────────────────────────────────────────────┐
224225
│ apmode CLI (Typer) │
225-
15 commands: run | validate | inspect
226+
16 direct commands + registered groups
226227
│ datasets | explore | diff | log | trace │
227228
│ lineage | graph | report | doctor | ls │
228229
│ policies | bundle │
@@ -293,11 +294,11 @@ All paths rooted at `src/apmode/`.
293294
| `dsl/ast_models.py` | `DSLSpec` + all module Pydantic nodes |
294295
| `dsl/normalize.py` | AST canonicalization |
295296
| `dsl/validator.py` | Lane-aware `validate_dsl` |
296-
| `dsl/transforms.py` | 6 structural transforms + `FormularTransform` union |
297-
| `dsl/prior_transforms.py` | `SetPrior` transform (7th) |
297+
| `dsl/transforms.py` | 9 structural transforms + `FormularTransform` union |
298+
| `dsl/prior_transforms.py` | `SetPrior` transform (10th) |
298299
| `dsl/priors.py` | Prior families + `_VALID_FAMILIES` admissibility matrix |
299300
| `dsl/nlmixr2_emitter.py` | AST → nlmixr2 R code |
300-
| `dsl/stan_emitter.py` | AST → Stan program (BLQ M3/M4, IOV, NODE`NotImplementedError`) |
301+
| `dsl/stan_emitter.py` | AST → Stan program (IOV, NODE, maturation covariates, and v0.7 absorption preview forms`NotImplementedError`) |
301302
| `dsl/frem_emitter.py` | AST → FREM-augmented nlmixr2 |
302303
| `dsl/_emitter_utils.py` | Shared emitter helpers |
303304

@@ -369,7 +370,7 @@ All paths rooted at `src/apmode/`.
369370

370371
| File | Role |
371372
|------|------|
372-
| `cli.py` | Typer app — 15 commands |
373+
| `cli.py` | Typer app — 16 direct commands plus `bundle` / `completion` groups |
373374
| `paths.py` | `APMODE_POLICIES_DIR` env override + pyproject-walk fallback |
374375
| `routing.py` | Lane Router — evidence-manifest-driven dispatch |
375376
| `logging.py` | `structlog` configuration |
@@ -613,9 +614,9 @@ Phases 1 and 2 are complete. Phase 3 is in progress per CLAUDE.md. The per-month
613614
task list from v0.2 has been removed from this doc; it is preserved in the git
614615
history at `docs/ARCHITECTURE.md@v0.2` and summarized in `CHANGELOG.md`.
615616

616-
**What is active today (0.5.0-rc2):**
617+
**What is active today (0.6.1-rc1):**
617618

618-
- DSL grammar + compiler + validator + 7 typed transforms.
619+
- DSL grammar + compiler + validator + 10 typed transforms.
619620
- Classical NLME backend (nlmixr2, SAEM/FOCEi) with warm-start children.
620621
- Bayesian backend (Stan / Torsten via `cmdstanpy`) with R̂ / ESS / E-BFMI /
621622
Pareto-k Gate 1 integration.
@@ -625,13 +626,15 @@ history at `docs/ARCHITECTURE.md@v0.2` and summarized in `CHANGELOG.md`.
625626
- FREM + MI-PMM + MI-missRanger missing-data pipelines.
626627
- Gate 1 (PIT / NPDE-lite) + Gate 2 (lane-specific) + Gate 2.5 (ICH M15) +
627628
Gate 3 (Borda / weighted ranking with uniform-drop rule).
628-
- Reproducibility bundle with `_COMPLETE` sentinel.
629-
- 14-command CLI + HTML / Markdown regulatory report.
630-
- Benchmark Suite A (full, 7 scenarios) + B + C scaffolding.
629+
- Reproducibility bundle with `_COMPLETE` sentinel and RO-Crate projection.
630+
- Typer CLI (`run`, bundle inspection/reporting, HTTP `serve`, RO-Crate/SBOM subcommands) + HTML / Markdown regulatory report.
631+
- Benchmark Suite A (8 scenarios), Suite B perturbation anchors, and Suite C literature fixtures.
632+
- FastAPI HTTP surface (`apmode serve`) with loopback default, static API-key floor for non-loopback binds, SQLite run store, and cancellation lifecycle.
631633

632-
**What remains for Phase 3 completion:** LORO-CV full integration into the
633-
Optimization lane, NODE posterior-predictive simulation (currently inert stub),
634-
Stan-side BLQ M3/M4 + IOV + maturation-covariate lowering, REST API.
634+
**What remains for Phase 3 completion:** NODE posterior-predictive simulation
635+
(currently inert stub), Stan-side IOV + maturation-covariate lowering, full
636+
Stan/Torsten support for the v0.7 absorption preview forms, and broader
637+
production hardening around public API deployments.
635638

636639
---
637640

@@ -671,9 +674,7 @@ initial-estimate strategy: NCA-seeded + warm-start per §2.5). Items still open:
671674
and a pharmacometric-review workflow. PRD §10 Q1.
672675
2. **Regulatory engagement timing** — when to seek informal FDA / EMA feedback on
673676
the credibility framework (PRD §10 Q4).
674-
3. **LORO-CV operationalization** — the Gate 2 policy fields are in place but the
675-
Optimization-lane pipeline wiring is in progress (Phase 3 completion).
676-
4. **NODE posterior-predictive simulation**`NodeBackendRunner.sample_posterior_predictive`
677+
3. **NODE posterior-predictive simulation**`NodeBackendRunner.sample_posterior_predictive`
677678
is a discoverable inert stub; Phase 3 completion item. Concrete integration
678679
point: `backends/node_trainer.py`.
679680

@@ -693,7 +694,6 @@ this section is the canonical record of why they were deferred.
693694
`structlog` JSON output.
694695
- **Rust parser migration.** Lark in Python is adequate; the Phase-2 LALRPOP+PyO3
695696
branch was considered and rejected.
696-
- **REST API.** Planned for Phase 3 completion — out of scope for 0.5-rc.
697697
- **Web UI.** A minimal browser UI was prototyped and removed; the CLI + HTML
698698
report is the current UX.
699699

docs/FORMULAR.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Formular — The APMODE PK Specification Language
22

3-
**Version:** 0.5 (tracks APMODE 0.5.0-rc1)
4-
**Status:** Current (Phase 3 in progress)
3+
**Version:** 0.6.1 (tracks APMODE 0.6.1-rc1)
4+
**Status:** Current
55
**Canonical source:** `src/apmode/dsl/` — grammar (`pk_grammar.lark`), AST (`ast_models.py`),
66
validator (`validator.py`), transforms (`transforms.py` + `prior_transforms.py`), priors
77
(`priors.py`), emitters (`nlmixr2_emitter.py`, `stan_emitter.py`, `frem_emitter.py`).
@@ -266,12 +266,14 @@ Enforced at `SetPrior` validation time (`apmode.dsl.priors::_VALID_FAMILIES`). I
266266
### Provenance and FDA alignment
267267

268268
Every `PriorSpec` carries a `source ∈ {uninformative, weakly_informative,
269-
historical_data, expert_elicitation, meta_analysis}`. Sources other than
270-
`uninformative` / `weakly_informative` require a non-empty `justification` string
271-
(validator-enforced); `historical_data` additionally requires `historical_refs` listing
272-
source datasets. This aligns with FDA draft guidance FDA-2025-D-3217 (January 2026) on
273-
Bayesian methodology — the resulting `prior_manifest.json` in the bundle is the
274-
artifact Gate 2 consumes for prior-justification review.
269+
historical_data, expert_elicitation, meta_analysis, fixed_external}`. The
270+
informative sources (`historical_data`, `expert_elicitation`, `meta_analysis`) require
271+
a non-empty `justification` string (validator-enforced); `historical_data` additionally
272+
requires `historical_refs` listing source datasets. `fixed_external` marks externally
273+
fixed parameter values used by the SumIG disposition-fixed gate. This aligns with FDA
274+
draft guidance FDA-2025-D-3217 (January 2026) on Bayesian methodology — the resulting
275+
`prior_manifest.json` in the bundle is the artifact Gate 2 consumes for
276+
prior-justification review.
275277

276278
---
277279

@@ -515,9 +517,10 @@ def rhs(t, y, theta):
515517

516518
The emitters are in `nlmixr2_emitter.py`, `stan_emitter.py`, and the NODE harness in
517519
`backends/node_*.py`. Any spec that passes `validate_dsl` for the target lane will
518-
lower cleanly in nlmixr2; Stan additionally requires that BLQ M3/M4 and IOV are
519-
absent (Phase 3 work) and that NODE modules are not present (Stan has no neural-ODE
520-
support).
520+
lower cleanly in nlmixr2. Stan supports the core mechanistic forms and BLQ M3/M4
521+
censored likelihoods; it still rejects IOV, maturation covariate links, NODE modules,
522+
and the v0.7 absorption preview forms (`Erlang`, `ParallelFirstOrder`, `SumIG`) pending
523+
follow-on lowering work.
521524

522525
---
523526

src/apmode/backends/r_schemas.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ class PredictedSimulationsSubject(BaseModel):
101101
# reshapes into a numpy (n_sims, n_obs) array before validation.
102102
sims_at_observed: list[list[float]] = Field(min_length=1)
103103

104+
@field_validator("t_observed", "observed_dv", mode="before")
105+
@classmethod
106+
def _coerce_scalar_to_list(cls, v: object) -> object:
107+
"""Tolerate a bare scalar for the n_obs == 1 case.
108+
109+
The R harness wraps single-observation vectors with ``I(...)``
110+
so ``jsonlite::toJSON(..., auto_unbox = TRUE)`` keeps length-1
111+
numeric arrays as JSON arrays. Without the wrap (older bundles
112+
or a future regression), a subject with n_obs == 1 emits
113+
``t_observed: 1.5`` instead of ``t_observed: [1.5]`` and the
114+
``list[float]`` field rejects the float with
115+
``Input should be a valid list``. Coerce a bare scalar to a
116+
length-1 list defensively; well-formed inputs (list-of-floats)
117+
are unaffected.
118+
"""
119+
if isinstance(v, (int, float)) and not isinstance(v, bool):
120+
return [float(v)]
121+
return v
122+
104123
@field_validator("sims_at_observed", mode="before")
105124
@classmethod
106125
def _coerce_1d_to_2d(cls, v: object) -> object:

0 commit comments

Comments
 (0)