Skip to main content

ommx/
modeling_label.rs

1use crate::constraint_type::IDType;
2use crate::logical_memory::LogicalMemoryProfile;
3use fnv::FnvHashMap;
4use std::collections::BTreeSet;
5use std::sync::OnceLock;
6
7fn empty_parameters() -> &'static FnvHashMap<String, String> {
8    static EMPTY: OnceLock<FnvHashMap<String, String>> = OnceLock::new();
9    EMPTY.get_or_init(FnvHashMap::default)
10}
11
12/// Structured label that preserves the element's original modeling context.
13///
14/// This represents the mathematical-model notation users authored before the
15/// model was lowered into OMMX. For example, a variable written as `x[i, j]`
16/// can be represented as `name = "x"` with `subscripts = [i, j]`; a family of
17/// constraints such as `flow_limit[place]` can use `name = "flow limit"` with
18/// `parameters = {"place": place}`.
19#[derive(Debug, Clone, PartialEq, Default, LogicalMemoryProfile)]
20pub struct ModelingLabel {
21    pub name: Option<String>,
22    pub subscripts: Vec<i64>,
23    pub parameters: FnvHashMap<String, String>,
24    pub description: Option<String>,
25}
26
27/// ID-keyed Struct-of-Arrays storage for [`ModelingLabel`].
28#[derive(Debug, Clone, PartialEq, LogicalMemoryProfile)]
29pub struct ModelingLabelStore<ID: IDType> {
30    name: FnvHashMap<ID, String>,
31    subscripts: FnvHashMap<ID, Vec<i64>>,
32    parameters: FnvHashMap<ID, FnvHashMap<String, String>>,
33    description: FnvHashMap<ID, String>,
34}
35
36impl<ID: IDType> Default for ModelingLabelStore<ID> {
37    fn default() -> Self {
38        Self {
39            name: FnvHashMap::default(),
40            subscripts: FnvHashMap::default(),
41            parameters: FnvHashMap::default(),
42            description: FnvHashMap::default(),
43        }
44    }
45}
46
47impl<ID: IDType> ModelingLabelStore<ID> {
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    pub fn name(&self, id: ID) -> Option<&str> {
53        self.name.get(&id).map(String::as_str)
54    }
55
56    pub fn subscripts(&self, id: ID) -> &[i64] {
57        self.subscripts.get(&id).map_or(&[], Vec::as_slice)
58    }
59
60    pub fn parameters(&self, id: ID) -> &FnvHashMap<String, String> {
61        self.parameters
62            .get(&id)
63            .unwrap_or_else(|| empty_parameters())
64    }
65
66    pub fn description(&self, id: ID) -> Option<&str> {
67        self.description.get(&id).map(String::as_str)
68    }
69
70    pub fn collect_for(&self, id: ID) -> ModelingLabel {
71        ModelingLabel {
72            name: self.name.get(&id).cloned(),
73            subscripts: self.subscripts.get(&id).cloned().unwrap_or_default(),
74            parameters: self.parameters.get(&id).cloned().unwrap_or_default(),
75            description: self.description.get(&id).cloned(),
76        }
77    }
78
79    pub fn set_name(&mut self, id: ID, name: impl Into<String>) {
80        self.name.insert(id, name.into());
81    }
82
83    pub fn clear_name(&mut self, id: ID) {
84        self.name.remove(&id);
85    }
86
87    pub fn set_subscripts(&mut self, id: ID, s: impl Into<Vec<i64>>) {
88        let s = s.into();
89        if s.is_empty() {
90            self.subscripts.remove(&id);
91        } else {
92            self.subscripts.insert(id, s);
93        }
94    }
95
96    pub fn push_subscript(&mut self, id: ID, value: i64) {
97        self.subscripts.entry(id).or_default().push(value);
98    }
99
100    pub fn extend_subscripts(&mut self, id: ID, iter: impl IntoIterator<Item = i64>) {
101        let entry = self.subscripts.entry(id).or_default();
102        entry.extend(iter);
103        if entry.is_empty() {
104            self.subscripts.remove(&id);
105        }
106    }
107
108    pub fn set_parameter(&mut self, id: ID, key: impl Into<String>, value: impl Into<String>) {
109        self.parameters
110            .entry(id)
111            .or_default()
112            .insert(key.into(), value.into());
113    }
114
115    pub fn set_parameters(&mut self, id: ID, params: FnvHashMap<String, String>) {
116        if params.is_empty() {
117            self.parameters.remove(&id);
118        } else {
119            self.parameters.insert(id, params);
120        }
121    }
122
123    pub fn set_description(&mut self, id: ID, desc: impl Into<String>) {
124        self.description.insert(id, desc.into());
125    }
126
127    pub fn clear_description(&mut self, id: ID) {
128        self.description.remove(&id);
129    }
130
131    pub fn insert(&mut self, id: ID, label: ModelingLabel) {
132        let ModelingLabel {
133            name,
134            subscripts,
135            parameters,
136            description,
137        } = label;
138        match name {
139            Some(n) => self.name.insert(id, n),
140            None => self.name.remove(&id),
141        };
142        if subscripts.is_empty() {
143            self.subscripts.remove(&id);
144        } else {
145            self.subscripts.insert(id, subscripts);
146        }
147        if parameters.is_empty() {
148            self.parameters.remove(&id);
149        } else {
150            self.parameters.insert(id, parameters);
151        }
152        match description {
153            Some(d) => self.description.insert(id, d),
154            None => self.description.remove(&id),
155        };
156    }
157
158    pub fn remove(&mut self, id: ID) -> ModelingLabel {
159        ModelingLabel {
160            name: self.name.remove(&id),
161            subscripts: self.subscripts.remove(&id).unwrap_or_default(),
162            parameters: self.parameters.remove(&id).unwrap_or_default(),
163            description: self.description.remove(&id),
164        }
165    }
166
167    pub fn contains(&self, id: ID) -> bool {
168        self.name.contains_key(&id)
169            || self.subscripts.contains_key(&id)
170            || self.parameters.contains_key(&id)
171            || self.description.contains_key(&id)
172    }
173
174    pub fn ids(&self) -> BTreeSet<ID> {
175        self.name
176            .keys()
177            .chain(self.subscripts.keys())
178            .chain(self.parameters.keys())
179            .chain(self.description.keys())
180            .copied()
181            .collect()
182    }
183}
184
185/// Validate that every label ID is owned by the enclosing top-level object.
186pub(crate) fn validate_modeling_label_ids<ID: IDType>(
187    store: &ModelingLabelStore<ID>,
188    owned_ids: &BTreeSet<ID>,
189    owner_name: &str,
190) -> crate::Result<()> {
191    if let Some(id) = store.ids().into_iter().find(|id| !owned_ids.contains(id)) {
192        crate::bail!(
193            { ?id },
194            "Modeling label references unknown {owner_name} ID {id:?}",
195        );
196    }
197    Ok(())
198}
199
200impl From<ModelingLabel> for crate::v2::ModelingLabel {
201    fn from(label: ModelingLabel) -> Self {
202        Self {
203            name: label.name,
204            subscripts: label.subscripts,
205            parameters: label.parameters.into_iter().collect(),
206            description: label.description,
207        }
208    }
209}
210
211impl From<crate::v2::ModelingLabel> for ModelingLabel {
212    fn from(label: crate::v2::ModelingLabel) -> Self {
213        Self {
214            name: label.name,
215            subscripts: label.subscripts,
216            parameters: label.parameters.into_iter().collect(),
217            description: label.description,
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::VariableID;
226
227    #[test]
228    fn empty_store_returns_neutral_values() {
229        let store = ModelingLabelStore::<VariableID>::new();
230        let id = VariableID::from(1);
231        assert_eq!(store.name(id), None);
232        assert!(store.subscripts(id).is_empty());
233        assert!(store.parameters(id).is_empty());
234        assert_eq!(store.description(id), None);
235        assert_eq!(store.collect_for(id), ModelingLabel::default());
236        assert!(!store.contains(id));
237        assert!(store.ids().is_empty());
238    }
239
240    #[test]
241    fn insert_then_collect_round_trip() {
242        let mut store = ModelingLabelStore::<VariableID>::new();
243        let id = VariableID::from(42);
244        let mut params = FnvHashMap::default();
245        params.insert("k".into(), "v".into());
246        let label = ModelingLabel {
247            name: Some("x".to_string()),
248            subscripts: vec![0, 1],
249            parameters: params,
250            description: Some("d".to_string()),
251        };
252        store.insert(id, label.clone());
253        assert_eq!(store.collect_for(id), label);
254        assert!(store.ids().contains(&id));
255    }
256
257    #[test]
258    fn empty_label_does_not_create_entries() {
259        let mut store = ModelingLabelStore::<VariableID>::new();
260        let id = VariableID::from(0);
261        store.insert(id, ModelingLabel::default());
262        assert!(!store.contains(id));
263        assert!(store.ids().is_empty());
264    }
265
266    #[test]
267    fn setters_write_through() {
268        let mut store = ModelingLabelStore::<VariableID>::new();
269        let id = VariableID::from(11);
270        store.set_name(id, "demand");
271        store.set_description(id, "desc");
272        store.set_parameter(id, "k1", "v1");
273        store.set_parameter(id, "k2", "v2");
274        store.push_subscript(id, 1);
275        store.push_subscript(id, 2);
276
277        assert_eq!(store.name(id), Some("demand"));
278        assert_eq!(store.description(id), Some("desc"));
279        assert_eq!(store.subscripts(id), &[1, 2]);
280        assert_eq!(store.parameters(id).len(), 2);
281        assert!(store.ids().contains(&id));
282    }
283
284    #[test]
285    fn remove_returns_owned_label_and_clears() {
286        let mut store = ModelingLabelStore::<VariableID>::new();
287        let id = VariableID::from(3);
288        store.set_name(id, "n");
289        store.set_subscripts(id, vec![9]);
290
291        let removed = store.remove(id);
292
293        assert_eq!(removed.name.as_deref(), Some("n"));
294        assert_eq!(removed.subscripts, vec![9]);
295        assert!(!store.contains(id));
296    }
297}