Skip to content

Feat/data mapping routes#254

Open
Siel wants to merge 11 commits intofeat/add-missing-macrosfrom
feat/data-mapping-routes
Open

Feat/data mapping routes#254
Siel wants to merge 11 commits intofeat/add-missing-macrosfrom
feat/data-mapping-routes

Conversation

@Siel
Copy link
Copy Markdown
Member

@Siel Siel commented May 1, 2026

No description provided.

…rings instead of Integer. This gives flexibility to modelers to define routes in terms of indices or named elements. All three surfaces for Model creation are also updated (macros, SDL, and ::new() constructors)
Copilot AI review requested due to automatic review settings May 1, 2026 12:40
@Siel Siel changed the base branch from main to feat/add-missing-macros May 1, 2026 12:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the DSL + Rust macro modeling surfaces to support richer route/output metadata (including route kind + shared-channel semantics), introduces string-based channel identifiers for events, and aligns authoring syntax changes (name = ..., structure = ...) across tests, examples, and runtime backends.

Changes:

  • Add/propagate validated model metadata (parameters/covariates/states/routes/outputs; route kind + channel sharing) through ODE/Analytical/SDE equations and runtime backends.
  • Switch event channel identifiers (inputs/outputs) from usize to a ChannelId wrapper, enabling both named and numeric labels.
  • Update DSL authoring syntax (modelname, kernelstructure) and extend parsing/lowering to preserve route kind and allow numeric output labels.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/support/runtime_corpus.rs Updates DSL fixtures to name/structure and adds full-feature ODE/Analytical runtime corpus cases.
tests/support/bimodal_ke.rs Updates authoring DSL fixture to name = ....
tests/runtime_backend_matrix.rs Adds backend-matrix tests for new full-feature ODE/Analytical corpus cases.
tests/full_feature_macro_parity.rs Adds parity tests comparing full-feature macro models vs handwritten models (metadata + predictions).
tests/browser-e2e/site/app.mjs Updates embedded DSL sources to name = ....
tests/analytical_macro_lowering.rs Adds tests validating analytical! macro lowering vs handwritten behavior across features.
src/test_fixtures.rs Updates fixture syntax to structure = ....
src/simulator/equation/sde/mod.rs Adds optional validated metadata + route mapping behavior to SDE and refactors event handling for ChannelId.
src/simulator/equation/ode/mod.rs Adds optional validated metadata to ODE and resolves inputs/outputs via ChannelId-aware event processing.
src/simulator/equation/ode/closure.rs Updates infusion schedule handling to resolve numeric input indices from ChannelId.
src/simulator/equation/mod.rs Renames metametadata, introduces metadata-aware label resolution and occasion event resolution.
src/simulator/equation/meta.rs Removes the old Meta container in favor of the new metadata system.
src/simulator/equation/analytical/mod.rs Adds optional validated metadata to Analytical and updates infusion/output handling for ChannelId.
src/lib.rs Re-exports new metadata APIs and macros (analytical, ode, sde) via crate root + prelude.
src/error/mod.rs Adds explicit UnknownInputLabel / UnknownOutputLabel errors for label resolution failures.
src/dsl/wasm_direct_emitter.rs Updates route input load shape to new ExecutionLoad::RouteInput { .. } form.
src/dsl/wasm_compile.rs Updates embedded DSL sources to name = ... in wasm compilation tests.
src/dsl/wasm.rs Extends native route info with declaration_index and optional route kind.
src/dsl/rust_backend.rs Updates route input load shape to new ExecutionLoad::RouteInput { .. } form.
src/dsl/native.rs Adds route kind semantics + label resolution for native runtime models; improves event resolution and derived refresh.
src/dsl/model_info.rs Tracks route declaration order/indices + kind; updates injection detection for shared channels and adds tests.
src/dsl/jit.rs Updates route input load lowering to new ExecutionLoad::RouteInput { .. } form and adds shared-channel test.
src/dsl/compiled_backend_abi.rs Updates ABI tests for extended route info fields.
src/data/structs.rs Changes output equation collection to ChannelId; updates expansion logic and lag/fa application guards for numeric parsing.
src/data/row.rs Switches DataRow input/outeq to ChannelId and updates builder APIs to accept string-like labels.
src/data/parser/pmetrics.rs Parses INPUT/OUTEQ as strings into ChannelId (preserving named labels) and adds regression tests.
src/data/event.rs Introduces ChannelId and migrates bolus/infusion/observation labels from usize to ChannelId.
src/data/builder.rs Updates SubjectBuilder APIs to accept string-like labels for input/output channels.
pharmsol-macros/Cargo.toml Enables syn’s visit feature for macro implementation needs.
pharmsol-dsl/tests/dsl_authoring_edge_cases.rs Updates fixtures to name = ... and adds mixed numeric + named output label test.
pharmsol-dsl/src/test_fixtures.rs Updates fixtures to structure = ... and name = ... for recommended authoring style.
pharmsol-dsl/src/semantic.rs Renames analytical kernelstructure and preserves route kind in typed routes.
pharmsol-dsl/src/parser.rs Adds output-statement parsing context to allow numeric output assignment targets; renames kernelstructure.
pharmsol-dsl/src/ir.rs Adds route kind to typed IR and renames analytical field to structure.
pharmsol-dsl/src/execution.rs Adds route kind + declaration_index; changes route buffer sizing and route input load representation.
pharmsol-dsl/src/authoring.rs Renames modelname, kernelstructure; preserves route order + kind; supports numeric output labels.
pharmsol-dsl/src/ast.rs Adds RouteKind to AST and prints analytical blocks with structure = ....
examples/two_compartment.rs Migrates example to declaration-first ode! and metadata-backed route/output lookups.
examples/sde_readme.rs Adds SDE README example using sde! and metadata-backed route/output lookups.
examples/one_compartment.rs Migrates example to analytical! + ode! with metadata-backed route/output indices.
examples/ode_readme.rs Migrates README ODE example to ode! and metadata-backed route/output indices.
examples/macro_vs_handwritten_two_cpt.rs Adds example comparing macro vs handwritten ODE with shared input channel across bolus+infusion.
examples/macro_vs_handwritten_one_cpt.rs Adds example comparing macro vs handwritten ODE on one-compartment IV.
examples/dsl_runtime_jit.rs Updates DSL source to name = ....
examples/covariates.rs Migrates covariates example to declaration-first ode! and metadata-backed route/output indices.
examples/compare_solvers.rs Migrates solver comparison to declaration-first ode! and shared-channel route semantics.
examples/analytical_vs_ode.rs Migrates analytical-vs-ODE comparison to macro surfaces + metadata-backed subject building.
examples/analytical_readme.rs Adds analytical README example using analytical! and metadata-backed route/output indices.
README.md Rewrites quickstart/docs to prefer macro surfaces and documents DSL runtime targets and migrations.
.gitignore Adds docs/ to ignored paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +146 to +192
fn resolve_input_label(
&self,
label: &ChannelId,
expected_kind: RouteKind,
) -> Result<usize, PharmsolError> {
if let Some(metadata) = self.metadata() {
let route =
metadata
.route(label.as_str())
.ok_or_else(|| PharmsolError::UnknownInputLabel {
label: label.to_string(),
})?;

if route.kind() != expected_kind {
return Err(PharmsolError::OtherError(format!(
"input label `{}` is declared as {:?} but used as {:?}",
label,
route.kind(),
expected_kind
)));
}

return Ok(route.channel_index());
}

label
.index()
.ok_or_else(|| PharmsolError::UnknownInputLabel {
label: label.to_string(),
})
}

fn resolve_output_label(&self, label: &ChannelId) -> Result<usize, PharmsolError> {
if let Some(metadata) = self.metadata() {
return metadata.output_index(label.as_str()).ok_or_else(|| {
PharmsolError::UnknownOutputLabel {
label: label.to_string(),
}
});
}

label
.index()
.ok_or_else(|| PharmsolError::UnknownOutputLabel {
label: label.to_string(),
})
}
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🐰 Bencher Report

Branchfeat/data-mapping-routes
Testbedmhovd-pgx

🚨 14 Alerts

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
Conditional dose modificationLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
2.84 µs
(+134.45%)Baseline: 1.21 µs
1.27 µs
(223.51%)

Create large dataset (100 subjects)Latency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
94.97 µs
(+73.25%)Baseline: 54.82 µs
57.19 µs
(166.07%)

Data expand complex (1h intervals)Latency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
83.60 µs
(+198.70%)Baseline: 27.99 µs
30.23 µs
(276.56%)

Data expand simple (1h intervals)Latency
nanoseconds (ns)
📈 plot
🚷 threshold
🚨 alert (🔔)
1,072.20 ns
(+118.55%)Baseline: 490.61 ns
522.06 ns
(205.38%)

Data expand with additional timeLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
121.79 µs
(+213.43%)Baseline: 38.86 µs
42.06 µs
(289.60%)

Filter exclude subjectsLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
39.76 µs
(+29.18%)Baseline: 30.78 µs
31.49 µs
(126.26%)

Filter include subjectsLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
9.66 µs
(+21.52%)Baseline: 7.95 µs
8.39 µs
(115.25%)

Modify all bolus dosesLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
2.85 µs
(+142.30%)Baseline: 1.17 µs
1.23 µs
(232.14%)

Modify all infusion dosesLatency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
2.90 µs
(+139.99%)Baseline: 1.21 µs
1.26 µs
(229.88%)

SubjectBuilder multi-occasionLatency
nanoseconds (ns)
📈 plot
🚷 threshold
🚨 alert (🔔)
532.53 ns
(+101.11%)Baseline: 264.79 ns
275.06 ns
(193.60%)

SubjectBuilder simpleLatency
nanoseconds (ns)
📈 plot
🚷 threshold
🚨 alert (🔔)
212.71 ns
(+103.76%)Baseline: 104.39 ns
109.22 ns
(194.76%)

SubjectBuilder with covariatesLatency
nanoseconds (ns)
📈 plot
🚷 threshold
🚨 alert (🔔)
530.38 ns
(+91.00%)Baseline: 277.68 ns
294.64 ns
(180.01%)

nca_population/100Latency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
130.01 µs
(+5.97%)Baseline: 122.69 µs
128.11 µs
(101.48%)

readme 20Latency
microseconds (µs)
📈 plot
🚷 threshold
🚨 alert (🔔)
354.38 µs
(+7.24%)Baseline: 330.47 µs
352.32 µs
(100.58%)

Click to view all benchmark results
BenchmarkLatencyBenchmark Result
nanoseconds (ns)
(Result Δ%)
Upper Boundary
nanoseconds (ns)
(Limit %)
Conditional dose modification📈 view plot
🚷 view threshold
🚨 view alert (🔔)
2,835.20 ns
(+134.45%)Baseline: 1,209.29 ns
1,268.50 ns
(223.51%)

Create large dataset (100 subjects)📈 view plot
🚷 view threshold
🚨 view alert (🔔)
94,974.00 ns
(+73.25%)Baseline: 54,818.29 ns
57,190.83 ns
(166.07%)

Data expand complex (1h intervals)📈 view plot
🚷 view threshold
🚨 view alert (🔔)
83,597.00 ns
(+198.70%)Baseline: 27,987.03 ns
30,227.27 ns
(276.56%)

Data expand simple (1h intervals)📈 view plot
🚷 view threshold
🚨 view alert (🔔)
1,072.20 ns
(+118.55%)Baseline: 490.61 ns
522.06 ns
(205.38%)

Data expand with additional time📈 view plot
🚷 view threshold
🚨 view alert (🔔)
121,790.00 ns
(+213.43%)Baseline: 38,856.58 ns
42,055.22 ns
(289.60%)

Filter exclude subjects📈 view plot
🚷 view threshold
🚨 view alert (🔔)
39,763.00 ns
(+29.18%)Baseline: 30,780.47 ns
31,492.51 ns
(126.26%)

Filter include subjects📈 view plot
🚷 view threshold
🚨 view alert (🔔)
9,664.20 ns
(+21.52%)Baseline: 7,952.64 ns
8,385.26 ns
(115.25%)

Modify all bolus doses📈 view plot
🚷 view threshold
🚨 view alert (🔔)
2,845.90 ns
(+142.30%)Baseline: 1,174.55 ns
1,225.93 ns
(232.14%)

Modify all infusion doses📈 view plot
🚷 view threshold
🚨 view alert (🔔)
2,895.50 ns
(+139.99%)Baseline: 1,206.53 ns
1,259.57 ns
(229.88%)

SubjectBuilder multi-occasion📈 view plot
🚷 view threshold
🚨 view alert (🔔)
532.53 ns
(+101.11%)Baseline: 264.79 ns
275.06 ns
(193.60%)

SubjectBuilder simple📈 view plot
🚷 view threshold
🚨 view alert (🔔)
212.71 ns
(+103.76%)Baseline: 104.39 ns
109.22 ns
(194.76%)

SubjectBuilder with covariates📈 view plot
🚷 view threshold
🚨 view alert (🔔)
530.38 ns
(+91.00%)Baseline: 277.68 ns
294.64 ns
(180.01%)

nca_auc_cmax_metrics📈 view plot
🚷 view threshold
582.27 ns
(-1.01%)Baseline: 588.21 ns
616.68 ns
(94.42%)
nca_population/10📈 view plot
🚷 view threshold
48,870.00 ns
(+3.85%)Baseline: 47,056.63 ns
50,098.45 ns
(97.55%)
nca_population/100📈 view plot
🚷 view threshold
🚨 view alert (🔔)
130,010.00 ns
(+5.97%)Baseline: 122,686.32 ns
128,111.79 ns
(101.48%)

nca_population/500📈 view plot
🚷 view threshold
366,800.00 ns
(-8.46%)Baseline: 400,712.11 ns
436,950.46 ns
(83.95%)
nca_single_subject📈 view plot
🚷 view threshold
1,005.90 ns
(-0.56%)Baseline: 1,011.54 ns
1,048.96 ns
(95.90%)
one_compartment📈 view plot
🚷 view threshold
28,872.00 ns
(+21.56%)Baseline: 23,751.50 ns
30,634.38 ns
(94.25%)
one_compartment_covariates📈 view plot
🚷 view threshold
44,722.00 ns
(+40.20%)Baseline: 31,898.39 ns
47,365.06 ns
(94.42%)
readme 20📈 view plot
🚷 view threshold
🚨 view alert (🔔)
354,380.00 ns
(+7.24%)Baseline: 330,467.37 ns
352,321.45 ns
(100.58%)

two_compartment📈 view plot
🚷 view threshold
40,016.00 ns
(+47.17%)Baseline: 27,190.58 ns
42,822.20 ns
(93.45%)
🐰 View full continuous benchmarking report in Bencher

Copy link
Copy Markdown
Collaborator

@mhovd mhovd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments

Comment on lines +29 to +31
// Handwritten closures stay on dense internal channels.
// Public labels like `iv` and `cp` live in attached metadata, not in
// the low-level `rateiv[]` / `y[]` buffers.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this means

Comment on lines +32 to +35
// Handwritten closures stay on dense internal channels.
// Public route labels like `load` and `iv` are metadata names; the
// low-level `bolus[]`, `rateiv[]`, and `y[]` buffers remain indexed by
// dense internal slots.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, I don't really understand what this means

Comment thread src/data/event.rs Outdated
}

#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ChannelId(String);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add some documentation here? Since it appears to be very central to the definition of events

Comment on lines +32 to +35
// Handwritten closures stay on dense internal slots.
// Public route labels like `load` and `iv` are metadata names; the
// low-level `bolus[]`, `rateiv[]`, and `y[]` buffers remain indexed by
// dense internal slots.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too verbose for an example

Comment on lines +29 to +31
// Handwritten closures stay on dense internal slots.
// Public labels like `iv` and `cp` live in attached metadata, not in
// the low-level `rateiv[]` / `y[]` buffers.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too verbose / unclear

Comment thread src/data/event.rs
/// * `amount` - Amount of drug administered
/// * `input` - The compartment number receiving the dose
pub fn new(time: f64, amount: f64, input: usize, occasion: usize) -> Self {
/// * `input` - The route label receiving the dose
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// * `input` - The route label receiving the dose
/// * `input` - The input label receiving the dose

Comment thread src/data/event.rs
/// * `time` - Start time of the infusion
/// * `amount` - Total amount of drug to be administered
/// * `input` - The compartment number receiving the dose
/// * `input` - The route label receiving the dose
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// * `input` - The route label receiving the dose
/// * `input` - The input label receiving the dose

Comment thread src/data/event.rs
Comment on lines +97 to +172
macro_rules! impl_label_type {
($name:ident) => {
#[derive(
Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct $name(String);

impl $name {
pub fn new(label: impl ToString) -> Self {
Self(label.to_string())
}

pub fn as_str(&self) -> &str {
&self.0
}

pub fn index(&self) -> Option<usize> {
self.0.parse::<usize>().ok()
}
}

impl From<String> for $name {
fn from(value: String) -> Self {
Self(value)
}
}

impl From<&str> for $name {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}

impl From<usize> for $name {
fn from(value: usize) -> Self {
Self(value.to_string())
}
}

impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

impl PartialEq<usize> for $name {
fn eq(&self, other: &usize) -> bool {
self.index() == Some(*other)
}
}

impl PartialEq<$name> for usize {
fn eq(&self, other: &$name) -> bool {
other == self
}
}

impl PartialEq<usize> for &$name {
fn eq(&self, other: &usize) -> bool {
(**self).eq(other)
}
}

impl PartialEq<&$name> for usize {
fn eq(&self, other: &&$name) -> bool {
other.eq(self)
}
}
};
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this macro is shorter than duplicating the code, but it makes reasoning about the structure and features of the labels difficult. Could we refactor it somehow?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we purely want to avoid duplicating code, I would prefer a trait-based approach.

Comment thread src/data/row.rs
/// Kept as 1-indexed; user must size state arrays accordingly.
pub fn input(mut self, input: usize) -> Self {
self.row.input = Some(input);
/// Preserved as the public route label until model resolution.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Preserved as the public route label until model resolution.
/// Preserved as the public input label until model resolution.

Comment thread src/dsl/runtime.rs
Comment on lines +387 to +400
name = multi_digit_output_runtime
kind = ode

params = ke, v
states = central
outputs = 2, 10, 11

infusion(iv) -> central

dx(central) = -ke * central

out(10) = central / v ~ continuous()
out(2) = central / v ~ continuous()
out(11) = central / v ~ continuous()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little weird, in contrast to named outputs..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants