diff --git a/README.md b/README.md index 8efe5aea..ab930edd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![Model Framework Schematic](https://raw.githubusercontent.com/NREL/altrios/main/.github/images/ALTRIOS_schematic_Alfred_Hicks.png) -The Advanced Locomotive Technology and Rail Infrastructure Optimization System ([ALTRIOS](https://www.nrel.gov/transportation/altrios.html)) is a unique, fully integrated, open-source software tool to evaluate strategies for deploying advanced locomotive technologies and associated infrastructure for cost-effective decarbonization. ALTRIOS simulates freight-demand driven train scheduling, mainline meet-pass planning, locomotive dynamics, train dynamics, energy conversion efficiencies, and energy storage dynamics of line-haul train operations. Because new locomotives represent a significant long-term capital investment and new technologies must be thoroughly demonstrated before deployment, this tool provides guidance on the risk/reward tradeoffs of different technology rollout strategies. An open, integrated simulation tool is invaluable for identifying future research needs and making decisions on technology development, routes, and train selection. ALTRIOS was developed as part of a collaborative effort by a team comprising The National Renewable Energy Laboratory (NREL), University of Illinois Urbana-Champaign (UIUC), Southwest Research Institute (SwRI), and BNSF Railway. +The Advanced Locomotive Technology and Rail Infrastructure Optimization System ([ALTRIOS](https://www.nrel.gov/transportation/altrios.html)) is a unique, fully integrated, open-source software tool to evaluate strategies for deploying advanced locomotive technologies and associated infrastructure for cost-effective decarbonization. ALTRIOS simulates freight-demand driven train scheduling, mainline meet-pass planning, locomotive dynamics, train dynamics, energy conversion efficiencies, and energy storage dynamics of line-haul train operations. Because new locomotives represent a significant long-term capital investment and new technologies must be thoroughly demonstrated before deployment, this tool provides guidance on the risk/reward tradeoffs of different technology rollout strategies. An open, integrated simulation tool is invaluable for identifying future research needs and making decisions on technology development, routes, and train selection. ALTRIOS was developed as part of a collaborative effort by a team comprising The National Renewable Energy Laboratory (NREL), University of Texas (UT), Southwest Research Institute (SwRI), and BNSF Railway. Much of the core code in ALTRIOS is written in the [Rust Programming Language](https://www.rust-lang.org/) to ensure excellent computational performance and robustness, but we've built ALTRIOS with the intent of users interacting with the code through our feature-rich [Python](https://www.python.org/) interface. @@ -22,14 +22,19 @@ If you are an ALTRIOS developer, see [Developer Documentation](https://nrel.gith - Option 2 -- Anaconda: we recommend https://docs.conda.io/en/latest/miniconda.html. 1. Setup a python environment. ALTRIOS can work with Python 3.9, or 3.10, but we recommend 3.10 for better performance and user experience. Create a python environment for ALTRIOS with either of two methods: - Option 1 -- [Python Venv](https://docs.python.org/3/library/venv.html) - 1. Navigate to your project folder in which you'd like to store model data and run ALTRIOS. - 1. Assuming you have Python 3.10 installed, run `python3.10 -m venv altrios-venv` in your terminal enviroment (we recommend PowerShell in Windows, which comes pre-installed). This tells Python 3.10 to use the `venv` module to create a virtual environment (which will be ignored by git if named `altrios-venv`) in the `ALTRIOS/altrios-venv/`. + 1. Navigate to the ALTRIOS folder you just cloned or any folder you'd like for using ALTRIOS. Remember the folder you use! + 1. Assuming you have Python 3.10 installed, run + - `(path to your python3.10 e.g. ~/AppData/Local/Programs/Python/Python310/python.exe) -m venv altrios-venv` in Windows + - `python3.10 -m venv altrios-venv` in Mac/Unix/Linux + in your terminal enviroment (we recommend PowerShell in Windows, which comes pre-installed). + + This tells Python 3.10 to use the `venv` module to create a virtual environment (which will be ignored by git if named `altrios-venv`) in the `ALTRIOS/altrios-venv/`. 1. Activate the environment you just created to install packages or anytime you're running ALTRIOS: - Mac and Linux: `source altrios-venv/bin/activate` - - Windows: `altrios-venv/Scripts/activate.bat` in a windows command prompt or power shell or `source ./altrios-venv/scripts/activate` in git bash terminal + - Windows: `altrios-venv/Scripts/activate.bat` in a windows command prompt or power shell or `source altrios-venv/Scripts/activate` in git bash terminal - When the environment is activated, your terminal session will have a decorator that looks like `(altrios-venv)`. - Option 2 -- Anaconda: - 1. Open an Anaconda prompt (in Windows, we recommend Anaconda Powershell Prompt) and run the command `conda create -n altrios python=3.10` to create an Anaconda environment named `altrios`. + 1. Open an Anaconda prompt (in Windows, we recommend _Anaconda_ Powershell Prompt) and run the command `conda create -n altrios python=3.10` to create an Anaconda environment named `altrios`. 1. Activate the environment to install packages or anytime you're running ALTRIOS: run `conda activate altrios`. ### ALTRIOS Setup diff --git a/build_and_test.sh b/build_and_test.sh index 47bf6904..65aad72a 100644 --- a/build_and_test.sh +++ b/build_and_test.sh @@ -1,10 +1,8 @@ # assumes a python environment has been created and activated echo "Testing rust" && \ (cd rust/ && cargo test --workspace) && \ -# pip install -qe ".[dev]" && \ -# assumes `pip install -qe ".[dev]"` has been run already echo "Building python API" && \ -maturin develop --release && \ +pip install -qe ".[dev]" && \ echo "Running python tests" && \ pytest -v python/altrios/tests && \ echo "Verifying that demos run" && \ diff --git a/pyproject.toml b/pyproject.toml index 63d4cd31..4041d144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ ] dependencies = [ "pandas>=2", - "numpy", + "numpy==1.24", "pymoo==0.6", "openpyxl", "xlrd", diff --git a/rust/altrios-core/src/consist/consist_model.rs b/rust/altrios-core/src/consist/consist_model.rs index 9b999f99..915cbd9a 100644 --- a/rust/altrios-core/src/consist/consist_model.rs +++ b/rust/altrios-core/src/consist/consist_model.rs @@ -182,7 +182,7 @@ impl Consist { |f_sum, (i, loco)| -> anyhow::Result { Ok(loco .force_max()? - .ok_or_else(|| anyhow!("Locomotive {i} does not have `force_max` set"))? + .with_context(|| anyhow!("Locomotive {i} does not have `force_max` set"))? + f_sum) }, ) @@ -331,7 +331,11 @@ impl Consist { // maybe put logic for toggling `engine_on` here for (i, (loco, pwr_out)) in self.loco_vec.iter_mut().zip(pwr_out_vec.iter()).enumerate() { - log::info!("Solving locomotive #{}", i); + log::info!( + "Solving locomotive #{}\n`pwr_out: `{} MW", + i, + pwr_out.get::().format_eng(None) + ); loco.solve_energy_consumption(*pwr_out, dt, engine_on) .map_err(|err| { err.context(format!( @@ -500,7 +504,7 @@ impl Mass for Consist { |m_acc, (i, loco)| -> anyhow::Result { let loco_mass = loco .mass()? - .ok_or_else(|| anyhow!("Locomotive {i} does not have `mass` set"))?; + .with_context(|| anyhow!("Locomotive {i} does not have `mass` set"))?; let new_mass: si::Mass = loco_mass + m_acc; Ok(new_mass) }, diff --git a/rust/altrios-core/src/consist/locomotive/battery_electric_loco.rs b/rust/altrios-core/src/consist/locomotive/battery_electric_loco.rs index f363c330..e522f6ec 100644 --- a/rust/altrios-core/src/consist/locomotive/battery_electric_loco.rs +++ b/rust/altrios-core/src/consist/locomotive/battery_electric_loco.rs @@ -80,7 +80,7 @@ impl LocoTrait for BatteryElectricLoco { ) -> anyhow::Result<()> { // TODO: proposed interface location to feed in the catenary self.res.set_cur_pwr_out_max( - pwr_aux.ok_or(anyhow!(format_dbg!("`pwr_aux` not provided")))?, + pwr_aux.with_context(|| anyhow!(format_dbg!("`pwr_aux` not provided")))?, None, None, )?; diff --git a/rust/altrios-core/src/consist/locomotive/hybrid_loco.rs b/rust/altrios-core/src/consist/locomotive/hybrid_loco.rs index cd8ff3a1..538cb697 100644 --- a/rust/altrios-core/src/consist/locomotive/hybrid_loco.rs +++ b/rust/altrios-core/src/consist/locomotive/hybrid_loco.rs @@ -97,7 +97,7 @@ impl LocoTrait for Box { dt: si::Time, ) -> anyhow::Result<()> { self.res.set_cur_pwr_out_max( - pwr_aux.ok_or(anyhow!(format_dbg!("`pwr_aux` not provided")))?, + pwr_aux.with_context(|| anyhow!(format_dbg!("`pwr_aux` not provided")))?, None, None, )?; diff --git a/rust/altrios-core/src/consist/locomotive/locomotive_model.rs b/rust/altrios-core/src/consist/locomotive/locomotive_model.rs index 32167d13..69311774 100644 --- a/rust/altrios-core/src/consist/locomotive/locomotive_model.rs +++ b/rust/altrios-core/src/consist/locomotive/locomotive_model.rs @@ -171,13 +171,13 @@ impl LocoParams { fn from_hash(mut params: HashMap<&str, f64>) -> anyhow::Result { let pwr_aux_offset_watts = params .remove("pwr_aux_offset_watts") - .ok_or_else(|| anyhow!("Must provide 'pwr_aux_offset_watts'."))?; + .with_context(|| anyhow!("Must provide 'pwr_aux_offset_watts'."))?; let pwr_aux_traction_coeff_ratio = params .remove("pwr_aux_traction_coeff_ratio") - .ok_or_else(|| anyhow!("Must provide 'pwr_aux_traction_coeff_ratio'."))?; + .with_context(|| anyhow!("Must provide 'pwr_aux_traction_coeff_ratio'."))?; let force_max_newtons = params .remove("force_max_newtons") - .ok_or_else(|| anyhow!("Must provide 'force_max_newtons'."))?; + .with_context(|| anyhow!("Must provide 'force_max_newtons'."))?; let mass_kg = params.remove("mass_kg"); ensure!( params.is_empty(), @@ -655,7 +655,8 @@ impl Locomotive { } pub fn force_max(&self) -> anyhow::Result> { - self.check_force_max()?; + self.check_force_max() + .with_context(|| anyhow!(format_dbg!()))?; Ok(self.force_max) } diff --git a/rust/altrios-core/src/meet_pass/est_times/mod.rs b/rust/altrios-core/src/meet_pass/est_times/mod.rs index 1ec4e406..aa653fd8 100644 --- a/rust/altrios-core/src/meet_pass/est_times/mod.rs +++ b/rust/altrios-core/src/meet_pass/est_times/mod.rs @@ -491,6 +491,7 @@ pub fn make_est_times>( let time_depart = speed_limit_train_sim.state.time; // Push initial fake nodes + log::debug!("{}", format_dbg!("Push initial fake nodes.")); est_times.push(EstTime { idx_next: 1, ..Default::default() @@ -502,6 +503,7 @@ pub fn make_est_times>( }); // Add origin estimated times + log::debug!("{}", format_dbg!("Add origin estimated times.")); for orig in origs { ensure!( orig.offset == si::Length::ZERO, @@ -520,6 +522,7 @@ pub fn make_est_times>( ..Default::default() }; + log::debug!("{}", format_dbg!()); insert_est_time( &mut est_times, &mut est_alt, @@ -535,6 +538,7 @@ pub fn make_est_times>( ..Default::default() }, ); + log::debug!("{}", format_dbg!()); insert_est_time( &mut est_times, &mut est_alt, @@ -565,6 +569,7 @@ pub fn make_est_times>( } // Fix distances for different origins + log::debug!("{}", format_dbg!("Fix distances for different origins")); { let mut est_idx_fix = 1; while est_idx_fix != EST_IDX_NA { @@ -580,6 +585,7 @@ pub fn make_est_times>( let mut est_idxs_end = Vec::::with_capacity(8); // Iterate and process all saved sims + log::debug!("{}", format_dbg!("Iterate and process all saved sims")); while let Some(mut sim) = saved_sims.pop() { let mut has_split = false; ensure!( diff --git a/rust/altrios-core/src/track/link/elev.rs b/rust/altrios-core/src/track/link/elev.rs index bcf6308c..68d95332 100644 --- a/rust/altrios-core/src/track/link/elev.rs +++ b/rust/altrios-core/src/track/link/elev.rs @@ -64,7 +64,15 @@ impl ObjState for [Elev] { errors.push(anyhow!("There must be at least two elevations!")); } if !self.windows(2).all(|w| w[0].offset < w[1].offset) { - errors.push(anyhow!("Offsets must be sorted and unique!")); + let err_pairs: Vec> = self + .windows(2) + .filter(|w| w[0].offset >= w[1].offset) + .map(|w| vec![w[0].offset, w[1].offset]) + .collect(); + errors.push(anyhow!( + "Offsets must be sorted and unique! Invalid offsets: {:?}", + err_pairs + )); } errors.make_err() } diff --git a/rust/altrios-core/src/track/link/heading.rs b/rust/altrios-core/src/track/link/heading.rs index 554bd448..90029b4e 100644 --- a/rust/altrios-core/src/track/link/heading.rs +++ b/rust/altrios-core/src/track/link/heading.rs @@ -73,7 +73,15 @@ impl ObjState for [Heading] { errors.push(anyhow!("There must be at least two headings!")); } if !self.windows(2).all(|w| w[0].offset < w[1].offset) { - errors.push(anyhow!("Offsets must be sorted and unique!")); + let err_pairs: Vec> = self + .windows(2) + .filter(|w| w[0].offset >= w[1].offset) + .map(|w| vec![w[0].offset, w[1].offset]) + .collect(); + errors.push(anyhow!( + "Offsets must be sorted and unique! Invalid offsets: {:?}", + err_pairs + )); } errors.make_err() } diff --git a/rust/altrios-core/src/track/link/link_impl.rs b/rust/altrios-core/src/track/link/link_impl.rs index c95ab7a4..a6e94d48 100644 --- a/rust/altrios-core/src/track/link/link_impl.rs +++ b/rust/altrios-core/src/track/link/link_impl.rs @@ -69,10 +69,12 @@ impl Link { self.speed_set = Some( self.speed_sets .get(&train_type) - .ok_or(anyhow!( - "No value found for train_type: {:?} in `speed_sets`.", - train_type - ))? + .with_context(|| { + anyhow!( + "No value found for train_type: {:?} in `speed_sets`.", + train_type + ) + })? .clone(), ); self.speed_sets = HashMap::new(); @@ -323,7 +325,11 @@ impl SerdeAPI for Network { })?; let mut network = match Self::from_reader(file, extension) { Ok(network) => network, - Err(err) => NetworkOld::from_file(filepath).with_context(|| err)?.into(), + Err(err) => NetworkOld::from_file(filepath) + .map_err(|old_err| { + anyhow!("\nattempting to load as `Network`:\n{}\nattempting to load as `NetworkOld`:\n{}", err, old_err) + })? + .into(), }; network.init()?; diff --git a/rust/altrios-core/src/track/path_track/link_point.rs b/rust/altrios-core/src/track/path_track/link_point.rs index e3b6797f..e3052a02 100644 --- a/rust/altrios-core/src/track/path_track/link_point.rs +++ b/rust/altrios-core/src/track/path_track/link_point.rs @@ -97,7 +97,15 @@ impl ObjState for [LinkPoint] { } if !self.windows(2).all(|w| w[0].offset < w[1].offset) { - errors.push(anyhow!("Link point offsets must be sorted and unique!")); + let err_pairs: Vec> = self + .windows(2) + .filter(|w| w[0].offset >= w[1].offset) + .map(|w| vec![w[0].offset, w[1].offset]) + .collect(); + errors.push(anyhow!( + "Link point offsets must be sorted and unique! Invalid offsets: {:?}", + err_pairs + )); } errors.make_err() diff --git a/rust/altrios-core/src/track/path_track/path_res_coeff.rs b/rust/altrios-core/src/track/path_track/path_res_coeff.rs index 1a24a2be..f544d431 100644 --- a/rust/altrios-core/src/track/path_track/path_res_coeff.rs +++ b/rust/altrios-core/src/track/path_track/path_res_coeff.rs @@ -72,7 +72,15 @@ impl ObjState for [PathResCoeff] { errors.push(anyhow!("There must be at least two path res coeffs!")); } if !self.windows(2).all(|w| w[0].offset < w[1].offset) { - errors.push(anyhow!("Offsets must be sorted and unique!")); + let err_pairs: Vec> = self + .windows(2) + .filter(|w| w[0].offset >= w[1].offset) + .map(|w| vec![w[0].offset, w[1].offset]) + .collect(); + errors.push(anyhow!( + "Offsets must be sorted and unique! Invalid offsets: {:?}", + err_pairs + )); } if !self .windows(2) diff --git a/rust/altrios-core/src/track/path_track/path_tpc.rs b/rust/altrios-core/src/track/path_track/path_tpc.rs index 2a600b9c..9662c43d 100644 --- a/rust/altrios-core/src/track/path_track/path_tpc.rs +++ b/rust/altrios-core/src/track/path_track/path_tpc.rs @@ -383,7 +383,7 @@ fn extract_speed_set<'a>( speed_sets .iter() .find(|&sps| sps.0 == &train_params.train_type) - .ok_or_else(|| { + .with_context(|| { anyhow!( "`speed_set` is `None` and `train_params.train_type` {:?} not found in `speed_sets.keys()` {:?}", train_params.train_type, diff --git a/rust/altrios-core/src/train/speed_limit_train_sim.rs b/rust/altrios-core/src/train/speed_limit_train_sim.rs index e389203b..3465c333 100644 --- a/rust/altrios-core/src/train/speed_limit_train_sim.rs +++ b/rust/altrios-core/src/train/speed_limit_train_sim.rs @@ -293,6 +293,11 @@ impl SpeedLimitTrainSim { self.train_res .update_res(&mut self.state, &self.path_tpc, &Dir::Fwd)?; self.solve_required_pwr()?; + log::debug!( + "{}\ntime step: {}", + format_dbg!(), + self.state.time.get::().format_eng(Some(9)) + ); self.loco_con.solve_energy_consumption( self.state.pwr_whl_out, self.state.dt, @@ -318,6 +323,14 @@ impl SpeedLimitTrainSim { || (self.state.offset < self.path_tpc.offset_end() && self.state.speed != si::Velocity::ZERO) { + log::debug!( + "{}", + format_dbg!( + self.state.offset < self.path_tpc.offset_end() - 1000.0 * uc::FT + || (self.state.offset < self.path_tpc.offset_end() + && self.state.speed != si::Velocity::ZERO) + ) + ); self.step()?; } Ok(()) diff --git a/rust/altrios-core/src/train/train_config.rs b/rust/altrios-core/src/train/train_config.rs index 647fb94d..04bcd190 100644 --- a/rust/altrios-core/src/train/train_config.rs +++ b/rust/altrios-core/src/train/train_config.rs @@ -426,7 +426,7 @@ impl TrainSimBuilder { // `self.origin_id` verified to be `Some` earlier location_map .get(self.origin_id.as_ref().unwrap()) - .ok_or_else(|| { + .with_context(|| { anyhow!(format!( "{}\n`origin_id`: \"{}\" not found in `location_map` keys: {:?}", format_dbg!(), @@ -437,7 +437,7 @@ impl TrainSimBuilder { // `self.destination_id` verified to be `Some` earlier location_map .get(self.destination_id.as_ref().unwrap()) - .ok_or_else(|| { + .with_context(|| { anyhow!(format!( "{}\n`destination_id`: \"{}\" not found in `location_map` keys: {:?}", format_dbg!(),