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#[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#[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
185pub(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}