Skip to main content

muse2/input/process/
investment_constraints.rs

1//! Code for reading process investment constraints from a CSV file.
2use super::super::input_err_msg;
3use crate::id::GetIDValue;
4use crate::input::parse_year_str;
5use crate::input::{read_csv_optional, try_insert};
6use crate::process::{
7    ProcessID, ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessMap,
8};
9use crate::region::parse_region_str;
10use crate::units::{Capacity, CapacityPerYear, Dimensionless, UnitType, Year};
11use anyhow::{Context, Result, ensure};
12use itertools::iproduct;
13use log::warn;
14use serde::Deserialize;
15use std::collections::HashMap;
16use std::path::Path;
17use std::sync::Arc;
18
19const PROCESS_INVESTMENT_CONSTRAINTS_FILE_NAME: &str = "process_investment_constraints.csv";
20
21/// Represents a row of the process investment constraints CSV file
22#[derive(PartialEq, Debug, Deserialize)]
23struct ProcessInvestmentConstraintRaw {
24    process_id: String,
25    regions: String,
26    commission_years: String,
27    addition_limit: Option<CapacityPerYear>,
28    capacity_growth_limit: Option<Dimensionless>,
29    growth_seed: Option<Dimensionless>,
30    total_capacity_limit: Option<Capacity>,
31}
32
33impl ProcessInvestmentConstraintRaw {
34    /// Validate the constraint record for logical consistency and required fields
35    fn validate(&self) -> Result<()> {
36        self.validate_constraints_combination()?;
37
38        // Validate addition_limit
39        if let Some(limit) = self.addition_limit {
40            Self::validate_constraint_value(limit, "addition")?;
41        }
42
43        // Validate capacity_growth_limit
44        if let Some(limit) = self.capacity_growth_limit {
45            Self::validate_constraint_value(limit, "capacity growth")?;
46
47            // Warn user they may have specified limit as percentage, rather than fraction
48            if limit > Dimensionless(1.0) {
49                warn!(
50                    "Interpreting capacity growth constraint '{limit}' as {}%",
51                    limit * Dimensionless(100.0)
52                );
53            }
54        }
55
56        // Validate growth seed: must be in range [1, inf)
57        if let Some(growth_seed) = self.growth_seed {
58            ensure!(
59                growth_seed.is_finite() && growth_seed >= Dimensionless(1.0),
60                "Invalid value for growth seed: '{growth_seed}'; must be greater than or equal to \
61                 1, and finite.",
62            );
63        }
64
65        // Validate total_capacity_limit
66        if let Some(limit) = self.total_capacity_limit {
67            Self::validate_constraint_value(limit, "total capacity")?;
68        }
69
70        Ok(())
71    }
72
73    /// Check whether the limit is finite, otherwise return an error
74    fn validate_constraint_value<T: UnitType>(constraint: T, constraint_name: &str) -> Result<()> {
75        ensure!(
76            constraint.is_finite() && constraint.value() >= 0.0,
77            "Invalid value for {} constraint: '{}'; must be non-negative and finite.",
78            constraint_name,
79            constraint.value(),
80        );
81
82        Ok(())
83    }
84
85    /// Check that the combination of constraints is valid
86    fn validate_constraints_combination(&self) -> Result<()> {
87        // Allow any of addition_limit, capacity_growth_limit and total_capacity_limit to be empty,
88        // but not all three.
89        ensure!(
90            !(self.addition_limit.is_none()
91                && self.capacity_growth_limit.is_none()
92                && self.total_capacity_limit.is_none()),
93            "Invalid investment constraints for '{}': cannot have all process investment \
94             constraints undefined; rather omit the record for that process/region/year.",
95            self.process_id
96        );
97
98        Ok(())
99    }
100}
101
102/// Read the process investment constraints CSV file.
103///
104/// This file contains information about investment constraints that limit how processes can be
105/// deployed (growth rates, absolute additions, capacity limits).
106///
107/// # Arguments
108///
109/// * `model_dir` - Folder containing model configuration files
110/// * `processes` - Map of processes to validate against
111/// * `milestone_years` - Milestone years of simulation to validate against
112///
113/// # Returns
114///
115/// A `HashMap<ProcessID, ProcessInvestmentConstraintsMap>` mapping process IDs to their
116/// investment-constraints maps, or an error.
117pub fn read_process_investment_constraints(
118    model_dir: &Path,
119    processes: &ProcessMap,
120    milestone_years: &[u32],
121) -> Result<HashMap<ProcessID, ProcessInvestmentConstraintsMap>> {
122    let file_path = model_dir.join(PROCESS_INVESTMENT_CONSTRAINTS_FILE_NAME);
123    let constraints_csv = read_csv_optional(&file_path)?;
124    read_process_investment_constraints_from_iter(constraints_csv, processes, milestone_years)
125        .with_context(|| input_err_msg(&file_path))
126}
127
128/// Process raw investment-constraint records into a constraints map.
129///
130/// # Arguments
131///
132/// * `iter` - Iterator over `ProcessInvestmentConstraintRaw` records
133/// * `processes` - Map of known processes to validate against
134/// * `milestone_years` - Milestone years used by the model
135///
136/// # Returns
137///
138/// A `HashMap<ProcessID, ProcessInvestmentConstraintsMap>` mapping process IDs to their
139/// investment-constraints maps, or an error.
140fn read_process_investment_constraints_from_iter<I>(
141    iter: I,
142    processes: &ProcessMap,
143    milestone_years: &[u32],
144) -> Result<HashMap<ProcessID, ProcessInvestmentConstraintsMap>>
145where
146    I: Iterator<Item = ProcessInvestmentConstraintRaw>,
147{
148    let mut map: HashMap<ProcessID, ProcessInvestmentConstraintsMap> = HashMap::new();
149
150    for record in iter {
151        // Validate the raw record
152        record.validate()?;
153
154        // Verify the process exists
155        let (process_id, process) = processes.get_id_value(&record.process_id)?;
156
157        // Parse and validate regions
158        let process_regions = &process.regions;
159        let record_regions =
160            parse_region_str(&record.regions, process_regions).with_context(|| {
161                format!(
162                    "Invalid region for process {process_id}. Valid regions are {process_regions:?}"
163                )
164            })?;
165
166        // Parse associated commission years
167        let milestone_years_in_process_range: Vec<u32> = milestone_years
168            .iter()
169            .copied()
170            .filter(|year| process.years.contains(year))
171            .collect();
172        let constraint_years = parse_year_str(&record.commission_years, &milestone_years_in_process_range)
173            .with_context(|| {
174                format!(
175                    "Invalid year for constraint on process {process_id}. Valid years are {milestone_years_in_process_range:?}",
176                )
177            })?;
178
179        // Create constraints for each region and year combination
180        // For a given milestone year, the addition limit should be multiplied
181        // by the number of years since the previous milestone year. Any
182        // addition limits specified for the first milestone year are ignored.
183        let process_map = map.entry(process_id.clone()).or_default();
184        for (region, &year) in iproduct!(&record_regions, &constraint_years) {
185            // Calculate years since previous milestone year
186            // We can ignore constraints in the first milestone year as no investments are performed then
187            let idx = milestone_years.iter().position(|y| *y == year).expect(
188                "Year should be in milestone_years since it was validated by parse_year_str",
189            );
190            if idx == 0 {
191                continue;
192            }
193            let prev_year = milestone_years[idx - 1];
194            let years_since_prev = year - prev_year;
195
196            // Multiply the addition limit (if provided) by the number of years since previous
197            // milestone.
198            let addition_limit: Option<Capacity> = record
199                .addition_limit
200                .map(|limit| limit * Year(years_since_prev as f64));
201
202            let constraint = Arc::new(ProcessInvestmentConstraint {
203                addition_limit,
204                total_capacity_limit: record.total_capacity_limit,
205            });
206
207            try_insert(process_map, &(region.clone(), year), constraint.clone())?;
208        }
209    }
210    Ok(map)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::fixture::{assert_error, processes};
217    use crate::region::RegionID;
218    use crate::units::Capacity;
219    use rstest::rstest;
220
221    fn validate_raw_constraint(
222        addition_limit: Option<CapacityPerYear>,
223        capacity_growth_limit: Option<Dimensionless>,
224        growth_seed: Option<Dimensionless>,
225        total_capacity_limit: Option<Capacity>,
226    ) -> Result<()> {
227        let constraint = ProcessInvestmentConstraintRaw {
228            process_id: "test_process".into(),
229            regions: "ALL".into(),
230            commission_years: "2030".into(),
231            addition_limit,
232            capacity_growth_limit,
233            growth_seed,
234            total_capacity_limit,
235        };
236        constraint.validate()
237    }
238
239    #[rstest]
240    fn read_constraints_only_uses_milestone_years_within_process_range(processes: ProcessMap) {
241        // Process years are 2010..=2020 from the fixture (excludes 2008)
242        let milestone_years = vec![2008, 2012, 2016];
243
244        let constraints = vec![ProcessInvestmentConstraintRaw {
245            process_id: "process1".into(),
246            regions: "GBR".into(),
247            commission_years: "ALL".into(), // Should apply to milestone years [2012, 2016]
248            addition_limit: Some(CapacityPerYear(100.0)),
249            capacity_growth_limit: Some(Dimensionless(0.5)),
250            growth_seed: Some(Dimensionless(1.0)),
251            total_capacity_limit: Some(Capacity(100.0)),
252        }];
253
254        let result = read_process_investment_constraints_from_iter(
255            constraints.into_iter(),
256            &processes,
257            &milestone_years,
258        )
259        .unwrap();
260
261        let process_id: ProcessID = "process1".into();
262        let process_constraints = result
263            .get(&process_id)
264            .expect("Process constraints should exist");
265
266        let gbr_region: RegionID = "GBR".into();
267
268        // Should have constraints for milestone years within process year
269        // range
270        assert_eq!(process_constraints.len(), 2);
271        assert!(process_constraints.contains_key(&(gbr_region.clone(), 2012)));
272        assert!(process_constraints.contains_key(&(gbr_region.clone(), 2016)));
273
274        // All other years should not have constraints
275        let process = processes.get(&process_id).unwrap();
276        for year in process.years.clone() {
277            if ![2012, 2016].contains(&year) {
278                assert!(
279                    !process_constraints.contains_key(&(gbr_region.clone(), year)),
280                    "Should not contain constraint for year {year}"
281                );
282            }
283        }
284    }
285
286    #[rstest]
287    fn read_process_investment_constraints_from_iter_works(processes: ProcessMap) {
288        // Create milestone years matching the process years
289        let milestone_years: Vec<u32> = vec![2010, 2015, 2020];
290
291        // Create constraint records for the test process
292        let constraints = vec![
293            ProcessInvestmentConstraintRaw {
294                process_id: "process1".into(),
295                regions: "GBR".into(),
296                commission_years: "2010".into(),
297                addition_limit: Some(CapacityPerYear(100.0)),
298                capacity_growth_limit: Some(Dimensionless(0.5)),
299                growth_seed: Some(Dimensionless(1.0)),
300                total_capacity_limit: Some(Capacity(100.0)),
301            },
302            ProcessInvestmentConstraintRaw {
303                process_id: "process1".into(),
304                regions: "ALL".into(),
305                commission_years: "2015".into(),
306                addition_limit: Some(CapacityPerYear(200.0)),
307                capacity_growth_limit: Some(Dimensionless(0.5)),
308                growth_seed: Some(Dimensionless(1.0)),
309                total_capacity_limit: Some(Capacity(100.0)),
310            },
311            ProcessInvestmentConstraintRaw {
312                process_id: "process1".into(),
313                regions: "USA".into(),
314                commission_years: "2020".into(),
315                addition_limit: Some(CapacityPerYear(50.0)),
316                capacity_growth_limit: Some(Dimensionless(0.5)),
317                growth_seed: Some(Dimensionless(1.0)),
318                total_capacity_limit: Some(Capacity(100.0)),
319            },
320        ];
321
322        // Read constraints into the map
323        let result = read_process_investment_constraints_from_iter(
324            constraints.into_iter(),
325            &processes,
326            &milestone_years,
327        )
328        .unwrap();
329
330        // Verify the constraints were correctly stored
331        let process_id: ProcessID = "process1".into();
332        let process_constraints = result
333            .get(&process_id)
334            .expect("Process constraints should exist");
335
336        let gbr_region: RegionID = "GBR".into();
337        let usa_region: RegionID = "USA".into();
338
339        // GBR 2010 constraint is for the first milestone year and should be ignored
340        assert!(
341            !process_constraints.contains_key(&(gbr_region.clone(), 2010)),
342            "GBR 2010 constraint should not exist"
343        );
344
345        // Check GBR 2015 constraint (from ALL regions), scaled by years since previous milestone (5 years)
346        let gbr_2015 = process_constraints
347            .get(&(gbr_region, 2015))
348            .expect("GBR 2015 constraint should exist");
349        assert_eq!(gbr_2015.addition_limit, Some(Capacity(200.0 * 5.0)));
350
351        // Check USA 2015 constraint (from ALL regions), scaled by 5 years
352        let usa_2015 = process_constraints
353            .get(&(usa_region.clone(), 2015))
354            .expect("USA 2015 constraint should exist");
355        assert_eq!(usa_2015.addition_limit, Some(Capacity(200.0 * 5.0)));
356
357        // Check USA 2020 constraint, scaled by years since previous milestone (5 years)
358        let usa_2020 = process_constraints
359            .get(&(usa_region, 2020))
360            .expect("USA 2020 constraint should exist");
361        assert_eq!(usa_2020.addition_limit, Some(Capacity(50.0 * 5.0)));
362
363        // Verify total number of constraints (GBR 2015, USA 2015, USA 2020 = 3)
364        assert_eq!(process_constraints.len(), 3);
365    }
366
367    #[rstest]
368    fn read_constraints_all_regions_all_years(processes: ProcessMap) {
369        // Create milestone years matching the process years
370        let milestone_years: Vec<u32> = vec![2010, 2015, 2020];
371
372        // Create a constraint that applies to all regions and all years
373        let constraints = vec![ProcessInvestmentConstraintRaw {
374            process_id: "process1".into(),
375            regions: "ALL".into(),
376            commission_years: "ALL".into(),
377            addition_limit: Some(CapacityPerYear(75.0)),
378            capacity_growth_limit: Some(Dimensionless(0.5)),
379            growth_seed: Some(Dimensionless(1.0)),
380            total_capacity_limit: Some(Capacity(100.0)),
381        }];
382
383        // Read constraints into the map
384        let result = read_process_investment_constraints_from_iter(
385            constraints.into_iter(),
386            &processes,
387            &milestone_years,
388        )
389        .unwrap();
390
391        // Verify the constraints were correctly stored
392        let process_id: ProcessID = "process1".into();
393        let process_constraints = result
394            .get(&process_id)
395            .expect("Process constraints should exist");
396
397        let gbr_region: RegionID = "GBR".into();
398        let usa_region: RegionID = "USA".into();
399
400        // Verify constraint exists for all region-year combinations except the first milestone year
401        for &year in &milestone_years[1..] {
402            let gbr_constraint = process_constraints
403                .get(&(gbr_region.clone(), year))
404                .unwrap_or_else(|| panic!("GBR {year} constraint should exist"));
405            // scaled by years since previous milestone (5 years)
406            assert_eq!(gbr_constraint.addition_limit, Some(Capacity(75.0 * 5.0)));
407
408            let usa_constraint = process_constraints
409                .get(&(usa_region.clone(), year))
410                .unwrap_or_else(|| panic!("USA {year} constraint should exist"));
411            assert_eq!(usa_constraint.addition_limit, Some(Capacity(75.0 * 5.0)));
412        }
413
414        // Verify total number of constraints (2 regions × 2 years = 4)
415        assert_eq!(process_constraints.len(), 4);
416    }
417
418    #[rstest]
419    fn read_constraints_year_outside_milestone_years(processes: ProcessMap) {
420        // Create constraint with year outside milestone years
421        // Process years are 2010..=2020 from the fixture
422        let milestone_years = vec![2010, 2015, 2020];
423
424        let constraints = vec![ProcessInvestmentConstraintRaw {
425            process_id: "process1".into(),
426            regions: "GBR".into(),
427            commission_years: "2025".into(), // Outside milestone years (2010-2020)
428            addition_limit: Some(CapacityPerYear(100.0)),
429            capacity_growth_limit: Some(Dimensionless(0.5)),
430            growth_seed: Some(Dimensionless(1.0)),
431            total_capacity_limit: Some(Capacity(100.0)),
432        }];
433
434        // Should fail with milestone year validation error
435        let result = read_process_investment_constraints_from_iter(
436            constraints.into_iter(),
437            &processes,
438            &milestone_years,
439        );
440        assert_error!(
441            result,
442            "Invalid year for constraint on process process1. Valid years are [2010, 2015, 2020]"
443        );
444    }
445
446    #[rstest]
447    #[case(Some(CapacityPerYear(10.0)), None, None)] // only addition limits
448    #[case(Some(CapacityPerYear(0.0)), None, None)] // zero addition limit
449    #[case(None, Some(Dimensionless(0.5)), None)] // only growth limit
450    #[case(None, Some(Dimensionless(0.0)), None)] // zero growth limit
451    #[case(None, Some(Dimensionless(1.0)), None)] // growth limit exactly 1
452    #[case(None, Some(Dimensionless(5.0)), None)] // growth limit > 1 (TODO: test that this raises a warning)
453    #[case(None, None, Some(Capacity(100.0)))] // only total limit
454    #[case(None, None, Some(Capacity(0.0)))] // zero total limit
455    #[case(
456        Some(CapacityPerYear(1.0)),
457        Some(Dimensionless(0.1)),
458        Some(Capacity(100.0))
459    )] // all limits, all positive
460    #[case(
461        Some(CapacityPerYear(0.0)),
462        Some(Dimensionless(0.0)),
463        Some(Capacity(0.0))
464    )] // all limits, all zero
465    fn validate_constraints_valid(
466        #[case] addition_limit: Option<CapacityPerYear>,
467        #[case] capacity_growth_limit: Option<Dimensionless>,
468        #[case] total_capacity_limit: Option<Capacity>,
469    ) {
470        let valid = validate_raw_constraint(
471            addition_limit,
472            capacity_growth_limit,
473            None,
474            total_capacity_limit,
475        );
476        valid.unwrap();
477    }
478
479    #[rstest]
480    #[case(
481        Some(CapacityPerYear(-10.0)),
482        None,
483        None,
484        None,
485        "Invalid value for addition constraint: '-10'; must be non-negative and finite."
486    )]
487    #[case(
488        Some(CapacityPerYear(10.0)),
489        Some(Dimensionless(-0.5)),
490        None,
491        None,
492        "Invalid value for capacity growth constraint: '-0.5'; must be non-negative and finite."
493    )]
494    #[case(
495        Some(CapacityPerYear(10.0)),
496        None,
497        None,
498        Some(Capacity(-100.0)),
499        "Invalid value for total capacity constraint: '-100'; must be non-negative and finite."
500    )]
501    fn validate_constraints_rejects_negative(
502        #[case] addition_limit: Option<CapacityPerYear>,
503        #[case] capacity_growth_limit: Option<Dimensionless>,
504        #[case] growth_seed: Option<Dimensionless>,
505        #[case] total_capacity_limit: Option<Capacity>,
506        #[case] error_msg: &str,
507    ) {
508        // Not valid: capacity constraints with negative value
509        let invalid = validate_raw_constraint(
510            addition_limit,
511            capacity_growth_limit,
512            growth_seed,
513            total_capacity_limit,
514        );
515        assert_error!(invalid, error_msg);
516    }
517
518    #[rstest]
519    #[case(
520        Some(CapacityPerYear(f64::INFINITY)),
521        None,
522        None,
523        None,
524        "Invalid value for addition constraint: 'inf'; must be non-negative and finite."
525    )]
526    #[case(
527        Some(CapacityPerYear(10.0)),
528        Some(Dimensionless(f64::INFINITY)),
529        None,
530        None,
531        "Invalid value for capacity growth constraint: 'inf'; must be non-negative and finite."
532    )]
533    #[case(
534        Some(CapacityPerYear(10.0)),
535        None,
536        None,
537        Some(Capacity(f64::INFINITY)),
538        "Invalid value for total capacity constraint: 'inf'; must be non-negative and finite."
539    )]
540    fn validate_constraints_rejects_infinite(
541        #[case] addition_limit: Option<CapacityPerYear>,
542        #[case] capacity_growth_limit: Option<Dimensionless>,
543        #[case] growth_seed: Option<Dimensionless>,
544        #[case] total_capacity_limit: Option<Capacity>,
545        #[case] error_msg: &str,
546    ) {
547        // Not valid: capacity constraints with infinite value
548        let invalid = validate_raw_constraint(
549            addition_limit,
550            capacity_growth_limit,
551            growth_seed,
552            total_capacity_limit,
553        );
554        assert_error!(invalid, error_msg);
555    }
556
557    #[rstest]
558    #[case(
559        Some(CapacityPerYear(f64::NAN)),
560        None,
561        None,
562        None,
563        "Invalid value for addition constraint: 'NaN'; must be non-negative and finite."
564    )]
565    #[case(
566        Some(CapacityPerYear(10.0)),
567        Some(Dimensionless(f64::NAN)),
568        None,
569        None,
570        "Invalid value for capacity growth constraint: 'NaN'; must be non-negative and finite."
571    )]
572    #[case(
573        Some(CapacityPerYear(10.0)),
574        None,
575        None,
576        Some(Capacity(f64::NAN)),
577        "Invalid value for total capacity constraint: 'NaN'; must be non-negative and finite."
578    )]
579    fn validate_constraints_rejects_nan(
580        #[case] addition_limit: Option<CapacityPerYear>,
581        #[case] capacity_growth_limit: Option<Dimensionless>,
582        #[case] growth_seed: Option<Dimensionless>,
583        #[case] total_capacity_limit: Option<Capacity>,
584        #[case] error_msg: &str,
585    ) {
586        // Not valid: capacity constraints with NaN value
587        let invalid = validate_raw_constraint(
588            addition_limit,
589            capacity_growth_limit,
590            growth_seed,
591            total_capacity_limit,
592        );
593        assert_error!(invalid, error_msg);
594    }
595
596    #[rstest]
597    #[case(Some(CapacityPerYear(10.0)), None, None, None)]
598    #[case(Some(CapacityPerYear(10.0)), None, Some(Dimensionless(1.0)), None)]
599    #[case(Some(CapacityPerYear(10.0)), None, Some(Dimensionless(42.0)), None)]
600    fn validate_growth_seed_valid(
601        #[case] addition_limit: Option<CapacityPerYear>,
602        #[case] capacity_growth_limit: Option<Dimensionless>,
603        #[case] growth_seed: Option<Dimensionless>,
604        #[case] total_capacity_limit: Option<Capacity>,
605    ) {
606        // Valid: growth seed with finite values >= 1, or None
607        let valid = validate_raw_constraint(
608            addition_limit,
609            capacity_growth_limit,
610            growth_seed,
611            total_capacity_limit,
612        );
613        valid.unwrap();
614    }
615
616    #[rstest]
617    #[case(
618        Some(CapacityPerYear(10.0)),
619        None,
620        Some(Dimensionless(0.9)),
621        None,
622        "Invalid value for growth seed: '0.9'; must be greater than or equal to 1, and finite."
623    )]
624    #[case(
625        Some(CapacityPerYear(10.0)),
626        None,
627        Some(Dimensionless(-42.0)),
628        None,
629        "Invalid value for growth seed: '-42'; must be greater than or equal to 1, and finite.",
630    )]
631    #[case(
632        Some(CapacityPerYear(10.0)),
633        None,
634        Some(Dimensionless(f64::NAN)),
635        None,
636        "Invalid value for growth seed: 'NaN'; must be greater than or equal to 1, and finite."
637    )]
638    #[case(
639        Some(CapacityPerYear(10.0)),
640        None,
641        Some(Dimensionless(f64::INFINITY)),
642        None,
643        "Invalid value for growth seed: 'inf'; must be greater than or equal to 1, and finite."
644    )]
645    fn validate_growth_seed_invalid(
646        #[case] addition_limit: Option<CapacityPerYear>,
647        #[case] capacity_growth_limit: Option<Dimensionless>,
648        #[case] growth_seed: Option<Dimensionless>,
649        #[case] total_capacity_limit: Option<Capacity>,
650        #[case] error_msg: &str,
651    ) {
652        // Invalid: growth seed with values < 1, NaN or inf
653        let invalid = validate_raw_constraint(
654            addition_limit,
655            capacity_growth_limit,
656            growth_seed,
657            total_capacity_limit,
658        );
659        assert_error!(invalid, error_msg);
660    }
661
662    #[test]
663    fn validate_constraints_combination_rejects_all_undefined() {
664        // Check an error is raised if all of addition_limit, capacity_growth_limit and
665        // total_capacity_limit are undefined.
666        let invalid = validate_raw_constraint(None, None, Some(Dimensionless(1.25)), None);
667        assert_error!(
668            invalid,
669            "Invalid investment constraints for 'test_process': cannot have all process \
670             investment constraints undefined; rather omit the record for that process/\
671             region/year."
672        );
673    }
674}