diff --git a/tests/dsl_constant_derived.rs b/tests/dsl_constant_derived.rs index 295ea377..9ac6989d 100644 --- a/tests/dsl_constant_derived.rs +++ b/tests/dsl_constant_derived.rs @@ -1,5 +1,6 @@ //! A derived assignment whose value folds to a compile-time constant must still -//! be materialised, because other statements read it by slot. +//! be materialised, because other statements read it by slot, and it must be +//! read back as a real whether the reader is an output or a derivative. #![cfg(feature = "dsl")] @@ -22,6 +23,31 @@ fn prediction( Ok(predictions.predictions()[0].prediction()) } +// The subject must carry an observation at the model start (`t = 0`) for the +// integration to be anchored there, matching the single-point helper above. +fn predictions_at( + model_source: &str, + params: &[(&str, f64)], + times: &[f64], +) -> Result, Box> { + let model = compile_module_source_to_runtime(model_source, Some("const_derived"), |_, _| {})?; + let support_point = Parameters::with_model(&model, params.iter().copied())?; + let mut builder = Subject::builder("const_derived"); + for time in times { + builder = builder.missing_observation(*time, "cp"); + } + let subject = builder.build(); + let predictions = model + .estimate_predictions(&subject, &support_point)? + .into_subject() + .ok_or("expected subject predictions")?; + Ok(predictions + .predictions() + .iter() + .map(|prediction| prediction.prediction()) + .collect()) +} + const CONSTANT_DERIVED: &str = r#" name = const_derived kind = ode @@ -126,3 +152,83 @@ fn constant_branch_conditional_is_read_correctly() -> Result<(), Box 0.0) max_r else min_r + +init(central) = 2.0 +init(effector) = 0.0 + +dx(central) = 0.0 +dx(effector) = r * central - effector + +out(cp) = effector +"#; + +#[test] +fn whole_number_derived_scales_a_derivative_correctly() -> Result<(), Box> { + let values = predictions_at( + WHOLE_NUMBER_DERIVED_RATE, + &[("p1", 4.25), ("p2", 1.0)], + &[0.0, 1.0, 2.0], + )?; + for (value, time) in values.iter().zip([0.0f64, 1.0, 2.0]) { + let expected = 2.0 * (-time).exp(); + assert!( + (value - expected).abs() < 1e-4, + "got {value}, expected {expected}" + ); + } + Ok(()) +} + +#[test] +fn whole_number_derived_in_a_conditional_scales_a_derivative_correctly( +) -> Result<(), Box> { + let values = predictions_at( + WHOLE_NUMBER_DERIVED_IN_CONDITIONAL, + &[("p1", 4.25), ("p2", 1.0)], + &[0.0, 1.0, 2.0], + )?; + // `central` is held at 2.0 and the conditional selects `max_r` (1.0), so + // `effector(t) = 2 * (1 - exp(-t))`. A bit-pattern read would put `max_r` + // at 4.607e18 and drive `effector` there instead. + for (value, time) in values.iter().zip([0.0f64, 1.0, 2.0]) { + let expected = 2.0 * (1.0 - (-time).exp()); + assert!( + (value - expected).abs() < 1e-4, + "got {value}, expected {expected}" + ); + } + Ok(()) +}