SBML → C++ CVODE ODE code generator for QSP models.
Given a SimBiology-exported SBML Level 2 v4 file, emits the C++ sources consumed by a CVODEBase-backed QSP simulator:
QSP_enum.h,ODE_system.h,ODE_system.cppQSPParam.h,QSPParam.cppqsp_params_xml_snippet.xml(merged into consumerparam_all.xmlviaqsp-refresh-param-xml)
pip install ~/Projects/qsp-codegenqsp-codegen --sbml path/to/PDAC_model.sbml --out-dir path/to/ode/
qsp-refresh-param-xml \
--snippet path/to/ode/qsp_params_xml_snippet.xml \
--xml path/to/param_all.xml \
--xml path/to/param_all_test.xmlRun whenever the QSP model structure changes (species/parameters/reactions). Not needed for parameter-value tweaks.
- Parses SBML Level 2 v4 (SimBiology export dialect).
- Derives analytical Jacobian via sympy + CSE when sympy is available; falls back to numerical Jacobian otherwise.
- Consumer-side invariants (sync checks, ABM-specific param codegen) are not in scope — they live in the consumer repo.
The wheel also ships the model-agnostic pieces of the CVODE-backed
standalone simulator under qsp_codegen/cpp/:
include/qsp_sim_core/CVODEBase.h,ParamBase.h,MolecularModelCVode.hinclude/qsp_sim_core/model_hooks.h— declares theevolve_to_diagnosisoverride point used by the driver to set up a model-specific initial state before the scenario sim.src/CVODEBase.cpp,ParamBase.cpp,default_hooks.cppsrc/qsp_sim_main.cpp— theqsp_simdriver (CVODE stepping, YAML scenario + drug-metadata parsing, segmented dose scheduling, CSV and binary trajectory output, QSTH evolve-cache I/O).CMakeLists.txt,cmake/qsp_sim_coreConfig.cmake,cmake/QspSimCoreDeps.cmake.
Consumer CMakeLists.txt pulls this in via Python:
execute_process(COMMAND python -m qsp_codegen.cmake --prefix
OUTPUT_VARIABLE QSP_SIM_CORE_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE)
list(APPEND CMAKE_PREFIX_PATH "${QSP_SIM_CORE_PREFIX}")
find_package(qsp_sim_core CONFIG REQUIRED)
add_executable(qsp_sim
${QSP_SIM_CORE_DRIVER_SOURCE}
qsp/ode/ODE_system.cpp # emitted by qsp-codegen
qsp/ode/QSPParam.cpp # emitted by qsp-codegen
sim/evolve_to_diagnosis.cpp # consumer override (optional)
sim/set_healthy_populations.cpp # consumer-specific init
)
target_include_directories(qsp_sim PRIVATE qsp/ode sim)
target_link_libraries(qsp_sim PRIVATE qsp_sim_core::qsp_sim_core)qsp_sim integrates in SI seconds by default (multiplies external
--t-end-days and dose times by 86400 internally) on the assumption
that the SBML's <listOfUnitDefinitions> declares rates in 1/day,
1/hr, etc. and the codegen has unit-converted them to 1/s. This
matches QSP models authored with proper unit annotations.
Models exported by SimBiology's vanilla sbmlexport without unit
definitions come through with rate constants left in their authoring
unit (typically 1/day). For these, pass --time-unit days at the
command line so the runtime integrates in model-native days; the same
binary handles both conventions:
# Unit-annotated SBML (default; production QSP models):
qsp_sim --param param_all.xml --csv-out out.csv --t-end-days 365
# SimBiology-vanilla export with unitless 1/day rates:
qsp_sim --param param_all.xml --csv-out out.csv --t-end-days 365 \
--time-unit daysA consumer build can flip the default by compiling qsp_sim with
-DMODEL_UNITS (sets the default time factor to 1.0); the runtime
flag still wins on a per-invocation basis.
Declared in qsp_sim_core/model_hooks.h:
namespace CancerVCT {
struct EvolveOpts { std::string yaml_path; double time_factor; bool verbose; /* ... */ };
struct EvolveResult { bool success; double t_diagnosis_days; double diameter_cm; std::string reject_reason; };
EvolveResult evolve_to_diagnosis(ODE_system& ode, const EvolveOpts& opts);
}The driver calls this when --evolve-to-diagnosis <yaml> is passed on
the command line. A default no-op implementation ships inside the
qsp_sim_core static library and returns {success=true, t_diag=0},
so models without a pre-scenario evolve phase don't need to do
anything. Models that do (e.g. evolve from healthy tissue until a
tumor diameter is reached) define their own evolve_to_diagnosis
in an object file compiled directly into the executable — the strong
definition wins over the archive member at link time.
The hook is a free function rather than a virtual on ODE_system so
that qsp-codegen's emitted ABI stays untouched as the hook interface
evolves.