Add a unit test in `tests/` that pins the SBML prefix-unit → SI factor mapping computed by `SbmlModel._parse_units()` (src/qsp_codegen/codegen.py).
Why
Currently the parser pulls SI factors from `` elements with logic like:
```python
component = (mult * (10.0 ** scale) * base) ** exp
factor *= component
```
For a species in `MWDERIVEDUNIT_nanomolarity_milliliter` and a parameter in `MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity`, the parser computes very different SI factors (~1e-12 vs ~1e-9) — both correct for the respective declared units but visually similar enough that a regression that swaps prefixes silently could go unnoticed.
Proposed test
Build a tiny synthetic SBML with prefixed-molarity unit definitions matching SimBiology's export pattern (`pico`, `nano`, `micro`, `milli`):
```python
SBML_FIXTURE = '''<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core\" level="3" version="1">
<unitDefinition id="MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity" name="picomolarity">
<unit kind="metre" exponent="-3"/>
<unit kind="mole" exponent="1"/>
<unit kind="dimensionless" multiplier="1e-09"/>
'''
EXPECTED = {
"MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity": 1e-9, # pM → mol/m³
"MWBUILTINPREFIX_nano_MWBUILTINUNIT_molarity": 1e-6,
"MWBUILTINPREFIX_micro_MWBUILTINUNIT_molarity": 1e-3,
"MWBUILTINPREFIX_milli_MWBUILTINUNIT_molarity": 1.0,
}
def test_prefixed_molarity_factors():
sbml = SbmlModel.from_string(SBML_FIXTURE)
for uid, expected in EXPECTED.items():
assert math.isclose(sbml.unit_defs[uid], expected, rel_tol=1e-12), uid
```
Also assert the corresponding `MWDERIVEDUNIT_*_milliliter` (substanceUnit-style) factors come out as `×` to mol amount, e.g., `nanomolarity_milliliter` → 1e-12 mol.
Why this isn't covered today
Existing tests (per a quick grep) exercise generation/compile/simulate end-to-end on the live PDAC SBML, which can pass even when individual unit factors drift, because the rate equations harmonize them downstream and small numerical differences fall under rtol. A targeted parser-level pin would catch silent factor swaps without depending on simulation behavior.
Origin
Surfaced 2026-05-05 during a pdac-build SBI investigation of a candidate codegen unit bug. Diagnosis was retracted (the parser is correct), but the experience showed how easy it is to misread unit factors during analysis. A pinned parser test would have ruled out the codegen-bug hypothesis in seconds.
Add a unit test in `tests/` that pins the SBML prefix-unit → SI factor mapping computed by `SbmlModel._parse_units()` (
src/qsp_codegen/codegen.py).Why
Currently the parser pulls SI factors from `` elements with logic like:
```python
component = (mult * (10.0 ** scale) * base) ** exp
factor *= component
```
For a species in `MWDERIVEDUNIT_nanomolarity_milliliter` and a parameter in `MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity`, the parser computes very different SI factors (~1e-12 vs ~1e-9) — both correct for the respective declared units but visually similar enough that a regression that swaps prefixes silently could go unnoticed.
Proposed test
Build a tiny synthetic SBML with prefixed-molarity unit definitions matching SimBiology's export pattern (`pico`, `nano`, `micro`, `milli`):
```python
SBML_FIXTURE = '''<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core\" level="3" version="1">
<unitDefinition id="MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity" name="picomolarity">
<unit kind="metre" exponent="-3"/>
<unit kind="mole" exponent="1"/>
<unit kind="dimensionless" multiplier="1e-09"/>
'''
EXPECTED = {
"MWBUILTINPREFIX_pico_MWBUILTINUNIT_molarity": 1e-9, # pM → mol/m³
"MWBUILTINPREFIX_nano_MWBUILTINUNIT_molarity": 1e-6,
"MWBUILTINPREFIX_micro_MWBUILTINUNIT_molarity": 1e-3,
"MWBUILTINPREFIX_milli_MWBUILTINUNIT_molarity": 1.0,
}
def test_prefixed_molarity_factors():
sbml = SbmlModel.from_string(SBML_FIXTURE)
for uid, expected in EXPECTED.items():
assert math.isclose(sbml.unit_defs[uid], expected, rel_tol=1e-12), uid
```
Also assert the corresponding `MWDERIVEDUNIT_*_milliliter` (substanceUnit-style) factors come out as `×` to mol amount, e.g., `nanomolarity_milliliter` → 1e-12 mol.
Why this isn't covered today
Existing tests (per a quick grep) exercise generation/compile/simulate end-to-end on the live PDAC SBML, which can pass even when individual unit factors drift, because the rate equations harmonize them downstream and small numerical differences fall under rtol. A targeted parser-level pin would catch silent factor swaps without depending on simulation behavior.
Origin
Surfaced 2026-05-05 during a pdac-build SBI investigation of a candidate codegen unit bug. Diagnosis was retracted (the parser is correct), but the experience showed how easy it is to misread unit factors during analysis. A pinned parser test would have ruled out the codegen-bug hypothesis in seconds.