1use crate::asset::check_capacity_valid_for_asset;
7use crate::input::{
8 deserialise_proportion_nonzero, input_err_msg, is_sorted_and_unique, read_toml,
9};
10use crate::units::{Capacity, Dimensionless, Flow, MoneyPerFlow};
11use anyhow::{Context, Result, ensure};
12use log::warn;
13use serde::Deserialize;
14use std::path::Path;
15use std::sync::OnceLock;
16
17const MODEL_PARAMETERS_FILE_NAME: &str = "model.toml";
18
19pub const ALLOW_BROKEN_OPTION_NAME: &str = "please_give_me_broken_results";
24
25static BROKEN_OPTIONS_ALLOWED: OnceLock<bool> = OnceLock::new();
30
31pub fn broken_model_options_allowed() -> bool {
38 *BROKEN_OPTIONS_ALLOWED
39 .get()
40 .expect("Broken options flag not set")
41}
42
43fn set_broken_model_options_flag(allowed: bool) {
48 let result = BROKEN_OPTIONS_ALLOWED.set(allowed);
49 if result.is_err() {
50 if cfg!(test) {
51 assert_eq!(allowed, broken_model_options_allowed());
53 } else {
54 panic!("Attempted to set BROKEN_OPTIONS_ALLOWED twice");
55 }
56 }
57}
58
59macro_rules! define_unit_param_default {
60 ($name:ident, $type: ty, $value: expr) => {
61 fn $name() -> $type {
62 <$type>::new($value)
63 }
64 };
65}
66
67macro_rules! define_param_default {
68 ($name:ident, $type: ty, $value: expr) => {
69 fn $name() -> $type {
70 $value
71 }
72 };
73}
74
75define_unit_param_default!(default_candidate_asset_capacity, Capacity, 0.0001);
76define_unit_param_default!(default_capacity_limit_factor, Dimensionless, 0.1);
77define_unit_param_default!(default_value_of_lost_load, MoneyPerFlow, 1e9);
78define_unit_param_default!(default_price_tolerance, Dimensionless, 1e-6);
79define_unit_param_default!(default_remaining_demand_absolute_tolerance, Flow, 1e-12);
80define_param_default!(default_max_ironing_out_iterations, u32, 10);
81define_param_default!(default_capacity_margin, f64, 0.2);
82define_param_default!(default_mothball_years, u32, 0);
83
84#[derive(Debug, Deserialize, PartialEq)]
89pub struct ModelParameters {
90 pub milestone_years: Vec<u32>,
92 #[serde(default, rename = "please_give_me_broken_results")] pub allow_broken_options: bool,
95 #[serde(default = "default_candidate_asset_capacity")]
99 pub candidate_asset_capacity: Capacity,
100 #[serde(default = "default_capacity_limit_factor")]
104 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
105 pub capacity_limit_factor: Dimensionless,
106 #[serde(default = "default_value_of_lost_load")]
110 pub value_of_lost_load: MoneyPerFlow,
111 #[serde(default = "default_max_ironing_out_iterations")]
113 pub max_ironing_out_iterations: u32,
114 #[serde(default = "default_price_tolerance")]
116 pub price_tolerance: Dimensionless,
117 #[serde(default = "default_capacity_margin")]
123 pub capacity_margin: f64,
124 #[serde(default = "default_mothball_years")]
126 pub mothball_years: u32,
127 #[serde(default = "default_remaining_demand_absolute_tolerance")]
129 pub remaining_demand_absolute_tolerance: Flow,
130}
131
132fn check_milestone_years(years: &[u32]) -> Result<()> {
134 ensure!(!years.is_empty(), "`milestone_years` is empty");
135
136 ensure!(
137 is_sorted_and_unique(years),
138 "`milestone_years` must be composed of unique values in order"
139 );
140
141 Ok(())
142}
143
144fn check_value_of_lost_load(value: MoneyPerFlow) -> Result<()> {
146 ensure!(
147 value.is_finite() && value > MoneyPerFlow(0.0),
148 "value_of_lost_load must be a finite number greater than zero"
149 );
150
151 Ok(())
152}
153
154fn check_max_ironing_out_iterations(value: u32) -> Result<()> {
156 ensure!(value > 0, "max_ironing_out_iterations cannot be zero");
157
158 Ok(())
159}
160
161fn check_price_tolerance(value: Dimensionless) -> Result<()> {
163 ensure!(
164 value.is_finite() && value >= Dimensionless(0.0),
165 "price_tolerance must be a finite number greater than or equal to zero"
166 );
167
168 Ok(())
169}
170
171fn check_remaining_demand_absolute_tolerance(
172 allow_broken_options: bool,
173 value: Flow,
174) -> Result<()> {
175 ensure!(
176 value.is_finite() && value >= Flow(0.0),
177 "remaining_demand_absolute_tolerance must be a finite number greater than or equal to zero"
178 );
179
180 let default_value = default_remaining_demand_absolute_tolerance();
181 if !allow_broken_options {
182 ensure!(
183 value == default_value,
184 "Setting a remaining_demand_absolute_tolerance different from the default value of {:e} \
185 is potentially dangerous, set please_give_me_broken_results to true \
186 if you want to allow this.",
187 default_value.0
188 );
189 }
190
191 Ok(())
192}
193
194fn check_capacity_margin(value: f64) -> Result<()> {
196 ensure!(
197 value.is_finite() && value >= 0.0,
198 "capacity_margin must be a finite number greater than or equal to zero"
199 );
200
201 Ok(())
202}
203
204impl ModelParameters {
205 pub fn from_path<P: AsRef<Path>>(model_dir: P) -> Result<ModelParameters> {
215 let file_path = model_dir.as_ref().join(MODEL_PARAMETERS_FILE_NAME);
216 let model_params: ModelParameters = read_toml(&file_path)?;
217
218 set_broken_model_options_flag(model_params.allow_broken_options);
219
220 model_params
221 .validate()
222 .with_context(|| input_err_msg(file_path))?;
223
224 Ok(model_params)
225 }
226
227 fn validate(&self) -> Result<()> {
229 if self.allow_broken_options {
230 warn!(
231 "!!! You've enabled the {ALLOW_BROKEN_OPTION_NAME} option. !!!\n\
232 I see you like to live dangerously 😈. This option should ONLY be used by \
233 developers as it can cause peculiar behaviour that breaks things. NEVER enable it \
234 for results you actually care about or want to publish. You have been warned!"
235 );
236 }
237
238 check_milestone_years(&self.milestone_years)?;
240
241 check_capacity_valid_for_asset(self.candidate_asset_capacity)
245 .context("Invalid value for candidate_asset_capacity")?;
246
247 check_value_of_lost_load(self.value_of_lost_load)?;
249
250 check_max_ironing_out_iterations(self.max_ironing_out_iterations)?;
252
253 check_price_tolerance(self.price_tolerance)?;
255
256 check_capacity_margin(self.capacity_margin)?;
258
259 check_remaining_demand_absolute_tolerance(
261 self.allow_broken_options,
262 self.remaining_demand_absolute_tolerance,
263 )?;
264
265 Ok(())
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use rstest::rstest;
273 use std::fmt::Display;
274 use std::fs::File;
275 use std::io::Write;
276 use tempfile::tempdir;
277
278 fn assert_validation_result<T, U: Display>(
280 result: Result<T>,
281 expected_valid: bool,
282 value: U,
283 expected_error_fragment: &str,
284 ) {
285 if expected_valid {
286 assert!(
287 result.is_ok(),
288 "Expected value {} to be valid, but got error: {:?}",
289 value,
290 result.err()
291 );
292 } else {
293 assert!(
294 result.is_err(),
295 "Expected value {value} to be invalid, but it was accepted",
296 );
297 let error_message = result.err().unwrap().to_string();
298 assert!(
299 error_message.contains(expected_error_fragment),
300 "Error message should mention the validation constraint, got: {error_message}",
301 );
302 }
303 }
304
305 #[test]
306 fn check_milestone_years_works() {
307 check_milestone_years(&[1]).unwrap();
309 check_milestone_years(&[1, 2]).unwrap();
310
311 assert!(check_milestone_years(&[]).is_err());
313 assert!(check_milestone_years(&[1, 1]).is_err());
314 assert!(check_milestone_years(&[2, 1]).is_err());
315 }
316
317 #[test]
318 fn model_params_from_path() {
319 let dir = tempdir().unwrap();
320 {
321 let mut file = File::create(dir.path().join(MODEL_PARAMETERS_FILE_NAME)).unwrap();
322 writeln!(file, "milestone_years = [2020, 2100]").unwrap();
323 }
324
325 let model_params = ModelParameters::from_path(dir.path()).unwrap();
326 assert_eq!(model_params.milestone_years, [2020, 2100]);
327 }
328
329 #[rstest]
330 #[case(1.0, true)] #[case(1e-10, true)] #[case(1e9, true)] #[case(f64::MAX, true)] #[case(0.0, false)] #[case(-1.0, false)] #[case(-1e-10, false)] #[case(f64::INFINITY, false)] #[case(f64::NEG_INFINITY, false)] #[case(f64::NAN, false)] fn check_value_of_lost_load_works(#[case] value: f64, #[case] expected_valid: bool) {
341 let money_per_flow = MoneyPerFlow::new(value);
342 let result = check_value_of_lost_load(money_per_flow);
343
344 assert_validation_result(
345 result,
346 expected_valid,
347 value,
348 "value_of_lost_load must be a finite number greater than zero",
349 );
350 }
351
352 #[rstest]
353 #[case(1, true)] #[case(10, true)] #[case(100, true)] #[case(u32::MAX, true)] #[case(0, false)] fn check_max_ironing_out_iterations_works(#[case] value: u32, #[case] expected_valid: bool) {
359 let result = check_max_ironing_out_iterations(value);
360
361 assert_validation_result(
362 result,
363 expected_valid,
364 value,
365 "max_ironing_out_iterations cannot be zero",
366 );
367 }
368
369 #[rstest]
370 #[case(0.0, true)] #[case(1e-10, true)] #[case(1e-6, true)] #[case(1.0, true)] #[case(f64::MAX, true)] #[case(-1e-10, false)] #[case(-1.0, false)] #[case(f64::INFINITY, false)] #[case(f64::NEG_INFINITY, false)] #[case(f64::NAN, false)] fn check_price_tolerance_works(#[case] value: f64, #[case] expected_valid: bool) {
381 let dimensionless = Dimensionless::new(value);
382 let result = check_price_tolerance(dimensionless);
383
384 assert_validation_result(
385 result,
386 expected_valid,
387 value,
388 "price_tolerance must be a finite number greater than or equal to zero",
389 );
390 }
391
392 #[rstest]
393 #[case(true, 0.0, true)] #[case(true, 1e-10, true)] #[case(true, 1e-15, true)] #[case(false, 1e-12, true)] #[case(true, 1.0, true)] #[case(true, f64::MAX, true)] #[case(true, -1e-10, false)] #[case(true, f64::INFINITY, false)] #[case(true, f64::NEG_INFINITY, false)] #[case(true, f64::NAN, false)] #[case(false, -1e-10, false)] #[case(false, f64::INFINITY, false)] #[case(false, f64::NEG_INFINITY, false)] #[case(false, f64::NAN, false)] fn check_remaining_demand_absolute_tolerance_works(
408 #[case] allow_broken_options: bool,
409 #[case] value: f64,
410 #[case] expected_valid: bool,
411 ) {
412 let flow = Flow::new(value);
413 let result = check_remaining_demand_absolute_tolerance(allow_broken_options, flow);
414
415 assert_validation_result(
416 result,
417 expected_valid,
418 value,
419 "remaining_demand_absolute_tolerance must be a finite number greater than or equal to zero",
420 );
421 }
422
423 #[rstest]
424 #[case(0.0)] #[case(1e-10)] #[case(1.0)] #[case(f64::MAX)] fn check_remaining_demand_absolute_tolerance_requires_broken_options_if_non_default(
429 #[case] value: f64,
430 ) {
431 let flow = Flow::new(value);
432 let result = check_remaining_demand_absolute_tolerance(false, flow);
433 assert_validation_result(
434 result,
435 false,
436 value,
437 "Setting a remaining_demand_absolute_tolerance different from the default value \
438 of 1e-12 is potentially dangerous, set \
439 please_give_me_broken_results to true if you want to allow this.",
440 );
441 }
442
443 #[rstest]
444 #[case(0.0, true)] #[case(0.2, true)] #[case(10.0, true)] #[case(-1e-6, false)] #[case(f64::INFINITY, false)] #[case(f64::NEG_INFINITY, false)] #[case(f64::NAN, false)] fn check_capacity_margin_works(#[case] value: f64, #[case] expected_valid: bool) {
452 let result = check_capacity_margin(value);
453
454 assert_validation_result(
455 result,
456 expected_valid,
457 value,
458 "capacity_margin must be a finite number greater than or equal to zero",
459 );
460 }
461}