1use anyhow::{anyhow, Result};
2use pharmsol::{
3 AssayErrorModel, AssayErrorModels, Data, Equation, Event, ResidualErrorModel,
4 ResidualErrorModels,
5};
6use std::collections::{BTreeSet, HashSet};
7
8use crate::estimation::nonparametric::Theta;
9use crate::model::parameter_space::{BoundedParameter, ParameterSpace, UnboundedParameter};
10use crate::model::{EquationMetadataSource, Model, ModelBuilder};
11
12pub trait Framework {
13 type ErrorModels;
14 type Prior;
20}
21
22#[derive(Debug, Clone, Copy)]
23pub struct Parametric;
24
25impl Framework for Parametric {
26 type ErrorModels = ResidualErrorModels;
27 type Prior = ParameterSpace<UnboundedParameter>;
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct NonParametric;
32
33impl Framework for NonParametric {
34 type ErrorModels = AssayErrorModels;
35 type Prior = Theta;
36}
37
38#[derive(Debug, Clone)]
39pub struct EstimationProblem<E: Equation, F: Framework> {
40 pub(crate) model: Model<E>,
41 pub(crate) data: Data,
42 pub(crate) error_models: F::ErrorModels,
43 pub(crate) prior: F::Prior,
49}
50
51impl<E: Equation + EquationMetadataSource> EstimationProblem<E, NonParametric> {
52 pub fn nonparametric(
59 equation: E,
60 data: Data,
61 prior: Theta,
62 error_models: AssayErrorModels,
63 ) -> Result<Self> {
64 let model_builder = Model::builder(equation);
65
66 validate_nonparametric_parameters(&model_builder, prior.parameters())?;
67
68 let model = model_builder.build()?;
69
70 validate_nonparametric_error_models(&model, &data, &error_models)?;
71
72 Ok(EstimationProblem {
73 model,
74 data,
75 error_models,
76 prior,
77 })
78 }
79}
80
81impl<E: Equation> EstimationProblem<E, Parametric> {
82 pub fn parametric(equation: E, data: Data) -> ParametricBuilder<E> {
84 ParametricBuilder {
85 model: Model::builder(equation),
86 data,
87 parameters: ParameterSpace::<UnboundedParameter>::new(),
88 error_models: Vec::new(),
89 }
90 }
91
92 pub fn parameters(&self) -> &ParameterSpace<UnboundedParameter> {
94 &self.prior
95 }
96}
97
98impl<E: Equation> EstimationProblem<E, NonParametric> {
99 pub fn parameters(&self) -> &ParameterSpace<BoundedParameter> {
101 self.prior.parameters()
102 }
103}
104
105pub struct ParametricBuilder<E: Equation> {
106 model: ModelBuilder<E>,
107 data: Data,
108 parameters: ParameterSpace<UnboundedParameter>,
109 error_models: Vec<(String, ResidualErrorModel)>,
110}
111
112impl<E: Equation> ParametricBuilder<E> {
113 pub fn parameter(mut self, parameter: impl Into<UnboundedParameter>) -> Self {
114 self.parameters.push(parameter.into());
115 self
116 }
117
118 pub fn parameters<P, I>(mut self, parameters: I) -> Self
119 where
120 P: Into<UnboundedParameter>,
121 I: IntoIterator<Item = P>,
122 {
123 for param in parameters {
124 self.parameters.push(param.into());
125 }
126 self
127 }
128
129 pub fn error_model(mut self, name: impl Into<String>, model: ResidualErrorModel) -> Self {
130 self.error_models.push((name.into(), model));
131 self
132 }
133}
134
135impl<E: Equation + EquationMetadataSource> ParametricBuilder<E> {
136 pub fn build(self) -> Result<EstimationProblem<E, Parametric>> {
137 validate_parametric_parameters(&self.model, &self.parameters)?;
138 validate_parametric_error_models(&self.model, &self.error_models)?;
139
140 let mut all_errors = ResidualErrorModels::new();
141 for (name, error_model) in self.error_models {
142 let outeq = self
143 .model
144 .output_index(&name)
145 .ok_or_else(|| anyhow!("unknown equation output label: {name}"))?;
146
147 all_errors = all_errors.add(outeq, error_model);
148 }
149
150 Ok(EstimationProblem {
151 model: self.model.build()?,
152 data: self.data,
153 error_models: all_errors,
154 prior: self.parameters,
155 })
156 }
157}
158
159fn validate_nonparametric_parameters<E: Equation + EquationMetadataSource>(
160 model: &ModelBuilder<E>,
161 parameters: &ParameterSpace<BoundedParameter>,
162) -> Result<()> {
163 if parameters.is_empty() {
164 anyhow::bail!("at least one parameter is required for non-parametric models");
165 }
166
167 for parameter in parameters.iter() {
168 if !parameter.lower.is_finite() || !parameter.upper.is_finite() {
169 anyhow::bail!(
170 "invalid bounds for parameter '{}': bounds must be finite numbers",
171 parameter.name
172 );
173 }
174
175 if parameter.lower >= parameter.upper {
176 anyhow::bail!(
177 "invalid bounds for parameter '{}': lower bound ({}) must be strictly less than upper bound ({})",
178 parameter.name,
179 parameter.lower,
180 parameter.upper
181 );
182 }
183 }
184
185 let names: Vec<String> = parameters
186 .iter()
187 .map(|parameter| parameter.name.clone())
188 .collect();
189 validate_parameter_declarations(model, &names)
190}
191
192fn validate_parametric_parameters<E: Equation + EquationMetadataSource>(
193 model: &ModelBuilder<E>,
194 parameters: &ParameterSpace<UnboundedParameter>,
195) -> Result<()> {
196 if parameters.is_empty() {
197 anyhow::bail!("at least one parameter is required for parametric models");
198 }
199
200 let names: Vec<String> = parameters
201 .iter()
202 .map(|parameter| parameter.name.clone())
203 .collect();
204 validate_parameter_declarations(model, &names)
205}
206
207fn validate_parameter_declarations<E: Equation + EquationMetadataSource>(
208 model: &ModelBuilder<E>,
209 provided_names: &[String],
210) -> Result<()> {
211 let mut seen: HashSet<&str> = HashSet::new();
212 let mut duplicates: Vec<String> = Vec::new();
213 for name in provided_names {
214 if !seen.insert(name.as_str()) {
215 duplicates.push(name.clone());
216 }
217 }
218
219 if !duplicates.is_empty() {
220 duplicates.sort();
221 duplicates.dedup();
222 anyhow::bail!(
223 "duplicate parameter declarations found: {}",
224 duplicates.join(", ")
225 );
226 }
227
228 let declared = model.parameter_names();
229
230 let unknown: Vec<String> = provided_names
231 .iter()
232 .filter(|name| model.parameter_index(name).is_none())
233 .cloned()
234 .collect();
235 if !unknown.is_empty() {
236 anyhow::bail!(
237 "unknown parameter name(s): {}. Valid parameters are: {}",
238 unknown.join(", "),
239 declared.join(", ")
240 );
241 }
242
243 let provided: HashSet<&str> = provided_names.iter().map(|name| name.as_str()).collect();
244 let missing: Vec<String> = declared
245 .iter()
246 .filter(|name| !provided.contains(name.as_str()))
247 .cloned()
248 .collect();
249
250 if !missing.is_empty() {
251 anyhow::bail!("missing parameter declaration(s): {}", missing.join(", "));
252 }
253
254 Ok(())
255}
256
257fn validate_nonparametric_error_models<E: Equation + EquationMetadataSource>(
258 model: &Model<E>,
259 data: &Data,
260 error_models: &AssayErrorModels,
261) -> Result<()> {
262 let bound = model
265 .equation
266 .bind_error_models(error_models)
267 .map_err(|e| anyhow!("invalid assay error model output(s): {e}"))?;
268
269 let mut observed_outputs: BTreeSet<usize> = BTreeSet::new();
273 let mut unresolved_labels: BTreeSet<String> = BTreeSet::new();
274 for subject in data.subjects() {
275 for occasion in subject.occasions() {
276 for event in occasion.events() {
277 if let Event::Observation(obs) = event {
278 let label = obs.outeq().to_string();
279 match resolve_output_index(model, &label) {
280 Some(outeq) => {
281 observed_outputs.insert(outeq);
282 }
283 None => {
284 unresolved_labels.insert(label);
285 }
286 }
287 }
288 }
289 }
290 }
291
292 if !unresolved_labels.is_empty() {
293 let labels: Vec<String> = unresolved_labels.into_iter().collect();
294 anyhow::bail!(
295 "the data references output label(s) that are not defined by the model: {}",
296 labels.join(", ")
297 );
298 }
299
300 if observed_outputs.is_empty() {
301 anyhow::bail!("the data contains no observations to fit");
302 }
303
304 for &outeq in &observed_outputs {
306 let has_model = matches!(
307 bound.error_model(outeq),
308 Ok(error_model) if *error_model != AssayErrorModel::None
309 );
310
311 if !has_model {
312 let label = model
313 .output_name(outeq)
314 .map(|name| name.to_string())
315 .unwrap_or_else(|| outeq.to_string());
316 anyhow::bail!(
317 "no assay error model defined for output '{}' (index {}), which is observed in the data",
318 label,
319 outeq
320 );
321 }
322 }
323
324 Ok(())
325}
326
327fn resolve_output_index<E: Equation + EquationMetadataSource>(
330 model: &Model<E>,
331 label: &str,
332) -> Option<usize> {
333 model.output_index(label).or_else(|| {
334 if !label.is_empty() && label.bytes().all(|b| b.is_ascii_digit()) {
335 model.output_index(&format!("outeq_{label}"))
336 } else {
337 None
338 }
339 })
340}
341
342fn validate_parametric_error_models<E: Equation + EquationMetadataSource>(
343 model: &ModelBuilder<E>,
344 error_models: &[(String, ResidualErrorModel)],
345) -> Result<()> {
346 if error_models.is_empty() {
347 anyhow::bail!("at least one residual error model is required");
348 }
349
350 validate_error_model_labels(model, error_models.iter().map(|(name, _)| name.as_str()))
351}
352
353fn validate_error_model_labels<'a, E, I>(model: &ModelBuilder<E>, labels: I) -> Result<()>
354where
355 E: Equation + EquationMetadataSource,
356 I: IntoIterator<Item = &'a str>,
357{
358 let valid_outputs = model.output_names();
359 let mut seen_output_indexes: HashSet<usize> = HashSet::new();
360
361 for name in labels {
362 let outeq = model.output_index(name).ok_or_else(|| {
363 anyhow!(
364 "unknown equation output label: {}. Valid outputs are: {}",
365 name,
366 valid_outputs.join(", ")
367 )
368 })?;
369
370 if !seen_output_indexes.insert(outeq) {
371 anyhow::bail!(
372 "duplicate error model declaration for output '{}' (index {})",
373 name,
374 outeq
375 );
376 }
377 }
378
379 Ok(())
380}