Skip to main content

pmcore/model/
parameter_space.rs

1use serde::{Deserialize, Serialize};
2
3/// Ordered collection of parameters.
4///
5/// Use `ParameterSpace<BoundedParameter>` for non-parametric problems and
6/// `ParameterSpace<UnboundedParameter>` for parametric problems.
7#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
8pub struct ParameterSpace<T> {
9    pub items: Vec<T>,
10}
11
12impl<T> ParameterSpace<T> {
13    pub fn new() -> Self {
14        Self { items: Vec::new() }
15    }
16
17    pub fn push(&mut self, item: impl Into<T>) {
18        self.items.push(item.into());
19    }
20
21    pub fn len(&self) -> usize {
22        self.items.len()
23    }
24
25    pub fn is_empty(&self) -> bool {
26        self.items.is_empty()
27    }
28
29    pub fn iter(&self) -> std::slice::Iter<'_, T> {
30        self.items.iter()
31    }
32}
33
34impl<T: ParameterMeta> ParameterSpace<T> {
35    pub fn names(&self) -> Vec<String> {
36        self.items.iter().map(|p| p.name().to_string()).collect()
37    }
38}
39
40/// Helpers for bounded parameter spaces.
41impl ParameterSpace<BoundedParameter> {
42    /// Creates an empty bounded parameter space (for non-parametric problems).
43    ///
44    /// Prefer this over `ParameterSpace::<BoundedParameter>::new()` to avoid the
45    /// turbofish:
46    ///
47    /// ```ignore
48    /// let space = ParameterSpace::bounded()
49    ///     .add("ke", 0.1, 1.0)
50    ///     .add("v", 1.0, 20.0);
51    /// ```
52    pub fn bounded() -> Self {
53        Self::new()
54    }
55
56    /// Adds a bounded parameter with the given `name`, `lower`, and `upper` bounds.
57    #[allow(clippy::should_implement_trait)]
58    pub fn add(mut self, name: impl Into<String>, lower: f64, upper: f64) -> Self {
59        self.items.push(BoundedParameter::new(name, lower, upper));
60        self
61    }
62
63    /// Returns `(lower, upper)` for each parameter.
64    pub fn finite_ranges(&self) -> Vec<(f64, f64)> {
65        self.items.iter().map(|p| (p.lower, p.upper)).collect()
66    }
67}
68
69/// Helpers for unbounded parameter spaces.
70impl ParameterSpace<UnboundedParameter> {
71    /// Creates an empty unbounded parameter space (for parametric problems).
72    ///
73    /// Prefer this over `ParameterSpace::<UnboundedParameter>::new()` to avoid
74    /// the turbofish.
75    pub fn unbounded() -> Self {
76        Self::new()
77    }
78
79    /// Adds an unbounded parameter to the space.
80    #[allow(clippy::should_implement_trait)]
81    pub fn add(mut self, parameter: impl Into<UnboundedParameter>) -> Self {
82        self.items.push(parameter.into());
83        self
84    }
85}
86
87impl<T> FromIterator<T> for ParameterSpace<T> {
88    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
89        Self {
90            items: iter.into_iter().collect(),
91        }
92    }
93}
94
95impl<T> IntoIterator for ParameterSpace<T> {
96    type Item = T;
97    type IntoIter = std::vec::IntoIter<T>;
98
99    fn into_iter(self) -> Self::IntoIter {
100        self.items.into_iter()
101    }
102}
103
104impl<'a, T> IntoIterator for &'a ParameterSpace<T> {
105    type Item = &'a T;
106    type IntoIter = std::slice::Iter<'a, T>;
107
108    fn into_iter(self) -> Self::IntoIter {
109        self.items.iter()
110    }
111}
112
113/// Common metadata exposed by parameter types.
114pub trait ParameterMeta {
115    fn name(&self) -> &str;
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct BoundedParameter {
120    pub name: String,
121    pub lower: f64,
122    pub upper: f64,
123}
124
125impl BoundedParameter {
126    pub fn new(name: impl Into<String>, lower: f64, upper: f64) -> Self {
127        Self {
128            name: name.into(),
129            lower,
130            upper,
131        }
132    }
133}
134
135impl ParameterMeta for BoundedParameter {
136    fn name(&self) -> &str {
137        &self.name
138    }
139}
140
141/// Converts a bounded parameter into a parametric parameter with logit scaling.
142impl From<BoundedParameter> for UnboundedParameter {
143    fn from(p: BoundedParameter) -> Self {
144        UnboundedParameter {
145            name: p.name,
146            scale: ParameterScale::Logit {
147                lower: p.lower,
148                upper: p.upper,
149            },
150            initial: None,
151            estimate: true,
152        }
153    }
154}
155
156/// Parametric parameter with an optional scale transform.
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct UnboundedParameter {
159    pub name: String,
160    pub scale: ParameterScale,
161    pub initial: Option<f64>,
162    pub estimate: bool,
163}
164
165impl UnboundedParameter {
166    /// Creates a parameter with an explicit scale.
167    pub fn new(name: impl Into<String>, scale: ParameterScale) -> Self {
168        Self {
169            name: name.into(),
170            scale,
171            initial: None,
172            estimate: true,
173        }
174    }
175
176    /// Creates a parameter on identity scale.
177    pub fn real(name: impl Into<String>) -> Self {
178        Self::new(name, ParameterScale::Identity)
179    }
180
181    /// Sets an initial value.
182    pub fn with_initial(mut self, value: f64) -> Self {
183        self.initial = Some(value);
184        self
185    }
186}
187
188impl ParameterMeta for UnboundedParameter {
189    fn name(&self) -> &str {
190        &self.name
191    }
192}
193
194/// Scale transform for parametric parameters.
195#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
196pub enum ParameterScale {
197    /// Identity transform.
198    #[default]
199    Identity,
200    /// Log transform.
201    Log,
202    /// Logistic transform on `(lower, upper)`.
203    Logit { lower: f64, upper: f64 },
204    /// Probit transform on `(lower, upper)`.
205    Probit { lower: f64, upper: f64 },
206}
207
208/// Entry point for building parameter declarations.
209///
210/// ```ignore
211/// use pmcore::prelude::*;
212///
213/// // Non-parametric: only bounded parameters are accepted.
214/// builder.parameter(Parameter::bounded("ke", 0.001, 3.0));
215///
216/// // Parametric: pick the scale explicitly.
217/// builder.parameter(Parameter::log("ke"));
218/// builder.parameter(Parameter::logit("frac", 0.0, 1.0));
219/// builder.parameter(Parameter::bounded("v", 25.0, 250.0)); // mapped to Logit
220/// ```
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub struct Parameter;
223
224impl Parameter {
225    /// Creates a bounded parameter.
226    pub fn bounded(name: impl Into<String>, lower: f64, upper: f64) -> BoundedParameter {
227        BoundedParameter::new(name, lower, upper)
228    }
229
230    /// Creates a parametric parameter on identity scale.
231    pub fn real(name: impl Into<String>) -> UnboundedParameter {
232        UnboundedParameter::real(name)
233    }
234
235    /// Creates a parametric parameter with an explicit scale.
236    pub fn scaled(name: impl Into<String>, scale: ParameterScale) -> UnboundedParameter {
237        UnboundedParameter::new(name, scale)
238    }
239
240    /// Creates a parametric parameter on log scale.
241    pub fn log(name: impl Into<String>) -> UnboundedParameter {
242        UnboundedParameter::new(name, ParameterScale::Log)
243    }
244
245    /// Creates a parametric parameter on logit scale.
246    pub fn logit(name: impl Into<String>, lower: f64, upper: f64) -> UnboundedParameter {
247        UnboundedParameter::new(name, ParameterScale::Logit { lower, upper })
248    }
249
250    /// Creates a parametric parameter on probit scale.
251    pub fn probit(name: impl Into<String>, lower: f64, upper: f64) -> UnboundedParameter {
252        UnboundedParameter::new(name, ParameterScale::Probit { lower, upper })
253    }
254}