diff --git a/.github/workflows/deploy-book.yaml b/.github/workflows/deploy-book.yaml new file mode 100644 index 00000000..9ff9ea1c --- /dev/null +++ b/.github/workflows/deploy-book.yaml @@ -0,0 +1,65 @@ +name: Deploy mdBook site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["fastsim-2"] + paths: + - "docs/**" + - ".github/workflows/deploy-book.yaml" + pull_request: + branches: ["fastsim-2"] + paths: + - "docs/**" + - ".github/workflows/deploy-book.yaml" + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: [ self-hosted ] + env: + MDBOOK_VERSION: 0.4.21 + steps: + - uses: actions/checkout@v3 + - name: Install mdBook + run: | + curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh + rustup update + cargo install --version ${MDBOOK_VERSION} mdbook + - name: Setup Pages + id: pages + uses: actions/configure-pages@v3 + - name: Build with mdBook + working-directory: ${{runner.workspace}}/mbap-computing/docs/ + run: mdbook build + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: ./docs/book + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: [ self-hosted ] + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/README.md b/README.md index f962f5f8..98bbcf1e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![FASTSim Logo](https://www.nrel.gov/transportation/assets/images/icon-fastsim.jpg) +[![Tests](https://github.com/NREL/fastsim/actions/workflows/tests.yaml/badge.svg)](https://github.com/NREL/fastsim/actions/workflows/tests.yaml) [![wheels](https://github.com/NREL/fastsim/actions/workflows/wheels.yaml/badge.svg)](https://github.com/NREL/fastsim/actions/workflows/wheels.yaml?event=release) ![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10-blue) [![Documentation](https://img.shields.io/badge/documentation-custom-blue.svg)](https://nrel.github.io/fastsim/) [![GitHub](https://img.shields.io/badge/GitHub-fastsim-blue.svg)](https://github.com/NREL/fastsim) + # Description This is the python/rust flavor of [NREL's FASTSimTM](https://www.nrel.gov/transportation/fastsim.html), which is based on the original Excel implementation. Effort will be made to keep the core methodology between this software and the Excel flavor in line with one another. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..4e42a1bc --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book/ \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 00000000..bae0594b --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,10 @@ +[book] +authors = ["Chad Baker"] +language = "en" +multilingual = false +src = "src" +title = "FASTSim Documentation" + +[output.html.fold] +enable = true +level = 0 \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 00000000..d19171dc --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,4 @@ +# Summary + +- [Introduction](./intro.md) +- [How to Update This Book](./how-to-update.md) diff --git a/docs/src/how-to-update.md b/docs/src/how-to-update.md new file mode 100644 index 00000000..65cf96d0 --- /dev/null +++ b/docs/src/how-to-update.md @@ -0,0 +1,15 @@ +# How to Update This Markdown Book + +[mdBook Documentation](https://rust-lang.github.io/mdBook/) + +## Setup + +1. If not already done, [install mdbook](https://rust-lang.github.io/mdBook/guide/installation.html) + +## Publishing + +1. Update `book.toml` or files in `docs/src/` +1. Make sure the docs look good locally: `mdbook build docs/ --open` +1. Commit files and push to `main` branch + +After that, a GitHub action will build the book and publish it [here](https://pages.github.nrel.gov/MBAP/mbap-computing/) diff --git a/docs/src/intro.md b/docs/src/intro.md new file mode 100644 index 00000000..1652ea2f --- /dev/null +++ b/docs/src/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This is the overall FASTSim documentation. We're working toward making this a fully integrated document that includes both the Python API and Rust core documentation for the `fastsim-2` branch and eventually also for the `fastsim-3` branch. diff --git a/rust/README.md b/rust/README.md index 00742ff8..e85caf4c 100644 --- a/rust/README.md +++ b/rust/README.md @@ -1,4 +1,6 @@ # Crate Architecture +[![Tests](https://github.com/NREL/fastsim/actions/workflows/tests.yaml/badge.svg)](https://github.com/NREL/fastsim/actions/workflows/tests.yaml) [![wheels](https://github.com/NREL/fastsim/actions/workflows/wheels.yaml/badge.svg)](https://github.com/NREL/fastsim/actions/workflows/wheels.yaml?event=release) ![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10-blue) [![Documentation](https://img.shields.io/badge/documentation-custom-blue.svg)](https://nrel.github.io/fastsim/) [![GitHub](https://img.shields.io/badge/GitHub-fastsim-blue.svg)](https://github.com/NREL/fastsim) + FASTSim Rust crates are organized as a cargo [workspace](Cargo.toml) as follows: 1. `fastsim-core`: a pure rust lib crate with optional `pyo3` feature. This crate is intended to be used by other crates and is not in itself an app. 1. `fastsim-cli`: a wrapper around `fastsim-core` to enable a standalone CLI app for running fastsim and getting results out. diff --git a/rust/fastsim-cli/Cargo.toml b/rust/fastsim-cli/Cargo.toml index d734c2b0..3ad583e3 100644 --- a/rust/fastsim-cli/Cargo.toml +++ b/rust/fastsim-cli/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/NREL/fastsim" [dependencies] fastsim-core = { path = "../fastsim-core", version = "~0" } +anyhow = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } project-root = "0.2.2" diff --git a/rust/fastsim-cli/src/bin/fastsim-cli.rs b/rust/fastsim-cli/src/bin/fastsim-cli.rs index 4dc31237..334ff477 100644 --- a/rust/fastsim-cli/src/bin/fastsim-cli.rs +++ b/rust/fastsim-cli/src/bin/fastsim-cli.rs @@ -120,9 +120,9 @@ pub fn calculate_mpgge_for_h2_diesel_ice( fs_kwh_out_ach: &Vec, fc_pwr_out_perc: &Vec, h2share: &Vec, -) -> H2AndDieselResults { - assert!(fc_kw_out_ach.len() == fs_kwh_out_ach.len()); - assert!(fc_pwr_out_perc.len() == h2share.len()); +) -> anyhow::Result { + anyhow::ensure!(fc_kw_out_ach.len() == fs_kwh_out_ach.len()); + anyhow::ensure!(fc_pwr_out_perc.len() == h2share.len()); let kwh_per_gallon_diesel = 37.95; let gge_per_kwh = 1.0 / kwh_per_gge; let mut total_diesel_kwh = 0.0; @@ -148,7 +148,7 @@ pub fn calculate_mpgge_for_h2_diesel_ice( total_diesel_gals += diesel_gals; total_diesel_gge += diesel_gge; } - H2AndDieselResults { + Ok(H2AndDieselResults { h2_kwh: total_h2_kwh, h2_gge: total_h2_gge, h2_mpgge: if total_h2_gge > 0.0 { @@ -164,25 +164,25 @@ pub fn calculate_mpgge_for_h2_diesel_ice( } else { 0.0 }, - } + }) } -pub fn integrate_power_to_kwh(dts_s: &Vec, ps_kw: &Vec) -> Vec { - assert!(dts_s.len() == ps_kw.len()); +pub fn integrate_power_to_kwh(dts_s: &Vec, ps_kw: &Vec) -> anyhow::Result> { + anyhow::ensure!(dts_s.len() == ps_kw.len()); let mut energy_kwh = Vec::::with_capacity(dts_s.len()); for idx in 0..dts_s.len() { let dt_s = dts_s[idx]; let p_kw = ps_kw[idx]; energy_kwh.push(p_kw * dt_s / 3600.0); } - energy_kwh + Ok(energy_kwh) } -pub fn main() { +pub fn main() -> anyhow::Result<()> { let fastsim_api = FastSimApi::parse(); if let Some(_cyc_json_str) = fastsim_api.cyc { - panic!("Need to implement: let cyc = RustCycle::from_json(cyc_json_str)"); + anyhow::bail!("Need to implement: let cyc = RustCycle::from_json(cyc_json_str)"); } let (is_adopt_hd, adopt_hd_string, adopt_hd_has_cycle) = if let Some(adopt_hd_string) = &fastsim_api.adopt_hd { @@ -215,15 +215,15 @@ pub fn main() { ); println!("Drag Coefficient: {}", drag_coeff); println!("Wheel RR Coefficient: {}", wheel_rr_coeff); - return; + return Ok(()); } else { - panic!("Need to provide coastdown test coefficients for drag and wheel rr coefficient calculation"); + anyhow::bail!("Need to provide coastdown test coefficients for drag and wheel rr coefficient calculation"); } } else { RustCycle::from_file(&cyc_file_path) } } else if is_adopt_hd && adopt_hd_has_cycle { - RustCycle::from_file(&adopt_hd_string) + RustCycle::from_file(adopt_hd_string) } else { //TODO? use pathbuff to string, for robustness Ok(RustCycle::new( @@ -231,10 +231,9 @@ pub fn main() { vec![0.0], vec![0.0], vec![0.0], - String::from("") + String::from(""), )) - } - .unwrap(); + }?; // TODO: put in logic here for loading vehicle for adopt-hd // with same file format as regular adopt and same outputs retured @@ -243,44 +242,35 @@ pub fn main() { let mut hd_h2_diesel_ice_h2share: Option> = None; let veh = if let Some(veh_string) = fastsim_api.veh { if is_adopt || is_adopt_hd { - let (veh_string, pwr_out_perc, h2share) = json_rewrite(veh_string); + let (veh_string, pwr_out_perc, h2share) = json_rewrite(veh_string)?; hd_h2_diesel_ice_h2share = h2share; fc_pwr_out_perc = pwr_out_perc; - RustVehicle::from_json_str(&veh_string) + let mut veh = RustVehicle::from_json(&veh_string)?; + veh.set_derived()?; + Ok(veh) } else { - RustVehicle::from_json_str(&veh_string) + let mut veh = RustVehicle::from_json(&veh_string)?; + veh.set_derived()?; + Ok(veh) } } else if let Some(veh_file_path) = fastsim_api.veh_file { if is_adopt || is_adopt_hd { - let vehstring = fs::read_to_string(veh_file_path).unwrap(); - let (vehstring, pwr_out_perc, h2share) = json_rewrite(vehstring); + let veh_string = fs::read_to_string(veh_file_path)?; + let (veh_string, pwr_out_perc, h2share) = json_rewrite(veh_string)?; hd_h2_diesel_ice_h2share = h2share; fc_pwr_out_perc = pwr_out_perc; - RustVehicle::from_json_str(&vehstring) + let mut veh = RustVehicle::from_json(&veh_string)?; + veh.set_derived()?; + Ok(veh) } else { RustVehicle::from_file(&veh_file_path) } } else { Ok(RustVehicle::mock_vehicle()) - } - .unwrap(); - - #[cfg(not(windows))] - macro_rules! path_separator { - () => { - "/" - }; - } - - #[cfg(windows)] - macro_rules! path_separator { - () => { - r#"\"# - }; - } + }?; if is_adopt { - let sdl = get_label_fe(&veh, Some(false), Some(false)).unwrap(); + let sdl = get_label_fe(&veh, Some(false), Some(false))?; let res = AdoptResults { adjCombMpgge: sdl.0.adj_comb_mpgge, rangeMiles: sdl.0.net_range_miles, @@ -290,36 +280,17 @@ pub fn main() { traceMissInMph: sdl.0.trace_miss_speed_mph, h2AndDiesel: None, }; - println!("{}", res.to_json()); + println!("{}", res.to_json()?); } else if is_adopt_hd { - let hd_cyc_filestring = include_str!(concat!( - "..", - path_separator!(), - "..", - path_separator!(), - "..", - path_separator!(), - "..", - path_separator!(), - "python", - path_separator!(), - "fastsim", - path_separator!(), - "resources", - path_separator!(), - "cycles", - path_separator!(), - "HHDDTCruiseSmooth.csv" - )); let cyc = if adopt_hd_has_cycle { cyc } else { - RustCycle::from_csv_string(hd_cyc_filestring, "HHDDTCruiseSmooth".to_string()).unwrap() + RustCycle::from_resource("cycles/HHDDTCruiseSmooth.csv")? }; let mut sim_drive = RustSimDrive::new(cyc, veh.clone()); - sim_drive.sim_drive(None, None).unwrap(); + sim_drive.sim_drive(None, None)?; let mut sim_drive_accel = RustSimDrive::new(make_accel_trace(), veh.clone()); - let net_accel = get_net_accel(&mut sim_drive_accel, &veh.scenario_name).unwrap(); + let net_accel = get_net_accel(&mut sim_drive_accel, &veh.scenario_name)?; let mut mpgge = sim_drive.mpgge; let h2_diesel_results = if let Some(hd_h2_diesel_ice_h2share) = hd_h2_diesel_ice_h2share { @@ -333,7 +304,7 @@ pub fn main() { &sim_drive.fs_kwh_out_ach.to_vec(), &fc_pwr_out_perc, &hd_h2_diesel_ice_h2share, - ); + )?; mpgge = dist_mi / (r.diesel_gge + r.h2_gge); Some(r) } else { @@ -358,16 +329,17 @@ pub fn main() { traceMissInMph: sim_drive.trace_miss_speed_mps * MPH_PER_MPS, h2AndDiesel: h2_diesel_results, }; - println!("{}", res.to_json()); + println!("{}", res.to_json()?); } else { let mut sim_drive = RustSimDrive::new(cyc, veh); // // this does nothing if it has already been called for the constructed `sim_drive` - sim_drive.sim_drive(None, None).unwrap(); + sim_drive.sim_drive(None, None)?; println!("{}", sim_drive.mpgge); } // else { // println!("Invalid option `{}` for `--res-fmt`", res_fmt); // } + Ok(()) } fn translate_veh_pt_type(x: i64) -> String { @@ -422,13 +394,14 @@ struct ParsedValue(Value); impl SerdeAPI for ParsedValue {} /// Rewrites the ADOPT JSON string to be in compliance with what FASTSim expects for JSON input. -fn json_rewrite(x: String) -> (String, Option>, Option>) { +#[allow(clippy::type_complexity)] +fn json_rewrite(x: String) -> anyhow::Result<(String, Option>, Option>)> { let adoptstring = x; let mut fc_pwr_out_perc: Option> = None; let mut hd_h2_diesel_ice_h2share: Option> = None; - let mut parsed_data: Value = serde_json::from_str(&adoptstring).unwrap(); + let mut parsed_data: Value = serde_json::from_str(&adoptstring)?; let veh_pt_type_raw = &parsed_data["vehPtType"]; if veh_pt_type_raw.is_i64() { @@ -520,7 +493,7 @@ fn json_rewrite(x: String) -> (String, Option>, Option>) { parsed_data["stop_start"] = json!(false); - let adoptstring = ParsedValue(parsed_data).to_json(); + let adoptstring = ParsedValue(parsed_data).to_json()?; - (adoptstring, fc_pwr_out_perc, hd_h2_diesel_ice_h2share) + Ok((adoptstring, fc_pwr_out_perc, hd_h2_diesel_ice_h2share)) } diff --git a/rust/fastsim-cli/tests/integration-tests.rs b/rust/fastsim-cli/tests/integration-tests.rs index 48d794c0..30675062 100644 --- a/rust/fastsim-cli/tests/integration-tests.rs +++ b/rust/fastsim-cli/tests/integration-tests.rs @@ -5,8 +5,8 @@ use assert_cmd::prelude::{CommandCargoExt, OutputAssertExt}; use predicates::prelude::predicate; #[test] -fn test_that_cli_app_produces_result() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("fastsim-cli")?; +fn test_that_cli_app_produces_result() { + let mut cmd = Command::cargo_bin("fastsim-cli").unwrap(); let mut cyc_file = project_root::get_project_root().unwrap(); cyc_file.push(Path::new("../python/fastsim/resources/cycles/udds.csv")); cyc_file = cyc_file.canonicalize().unwrap(); @@ -28,17 +28,16 @@ fn test_that_cli_app_produces_result() -> Result<(), Box> cmd.assert() .success() .stdout(predicate::str::contains("33.8")); - Ok(()) } #[test] -fn test_that_adopt_hd_option_works_as_expected() -> Result<(), Box> { +fn test_that_adopt_hd_option_works_as_expected() { let expected_results = vec![ ("adoptstring.json", "0.245"), // 0.245 kWh/mile ("adoptstring2.json", "7.906"), // 7.906 mpgge ("adoptstring3.json", "6.882"), // 6.882 mpgge ]; - let mut cmd = Command::cargo_bin("fastsim-cli")?; + let mut cmd = Command::cargo_bin("fastsim-cli").unwrap(); for (veh_file, expected_result) in expected_results.iter() { let mut adopt_veh_file = project_root::get_project_root().unwrap(); let mut adopt_str_path = String::from("../rust/fastsim-cli/tests/assets/"); @@ -59,5 +58,4 @@ fn test_that_adopt_hd_option_works_as_expected() -> Result<(), Box TokenStream { let mut ast = syn::parse_macro_input!(item as syn::ItemStruct); // println!("{}", ast.ident.to_string()); let ident = &ast.ident; - let is_state_or_history: bool = + let _is_state_or_history: bool = ident.to_string().contains("State") || ident.to_string().contains("HistoryVec"); let mut impl_block = TokenStream2::default(); @@ -104,22 +104,6 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { ); } } - - if !is_state_or_history { - py_impl_block.extend::(quote! { - #[pyo3(name = "to_file")] - pub fn to_file_py(&self, filename: &str) -> PyResult<()> { - Ok(self.to_file(filename)?) - } - - #[classmethod] - #[pyo3(name = "from_file")] - pub fn from_file_py(_cls: &PyType, json_str:String) -> PyResult { - // unwrap is ok here because it makes sense to stop execution if a file is not loadable - Ok(Self::from_file(&json_str)?) - } - }); - } } else if let syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed, .. }) = &mut ast.fields { // tuple struct if ast.ident.to_string().contains("Vec") || ast.ident.to_string().contains("Array") { @@ -168,9 +152,9 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn __str__(&self) -> String { format!("{:?}", self.0) } - pub fn __getitem__(&self, idx: i32) -> PyResult<#contained_dtype> { + pub fn __getitem__(&self, idx: i32) -> anyhow::Result<#contained_dtype> { if idx >= self.0.len() as i32 { - Err(PyIndexError::new_err("Index is out of bounds")) + bail!(PyIndexError::new_err("Index is out of bounds")) } else if idx >= 0 { Ok(self.0[idx as usize].clone()) } else { @@ -178,17 +162,17 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { } } pub fn __setitem__(&mut self, _idx: usize, _new_value: #contained_dtype - ) -> PyResult<()> { - Err(PyNotImplementedError::new_err( + ) -> anyhow::Result<()> { + bail!(PyNotImplementedError::new_err( "Setting value at index is not implemented. Run `tolist` method, modify value at index, and then set entire vector.", )) } - pub fn tolist(&self) -> PyResult> { + pub fn tolist(&self) -> anyhow::Result> { Ok(#tolist_body) } - pub fn __list__(&self) -> PyResult> { + pub fn __list__(&self) -> anyhow::Result> { Ok(#tolist_body) } pub fn __len__(&self) -> usize { @@ -211,10 +195,10 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { }; // py_impl_block.extend::(quote! { - // #[classmethod] + // #[staticmethod] // #[pyo3(name = "default")] - // pub fn default_py(_cls: &PyType) -> PyResult { - // Ok(Self::default()) + // pub fn default_py() -> Self { + // Self::default() // } // }); @@ -223,43 +207,71 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn __copy__(&self) -> Self {self.clone()} pub fn __deepcopy__(&self, _memo: &PyDict) -> Self {self.clone()} - /// json serialization method. + #[staticmethod] + #[pyo3(name = "from_resource")] + pub fn from_resource_py(filepath: &PyAny) -> anyhow::Result { + Self::from_resource(PathBuf::extract(filepath)?) + } + + #[pyo3(name = "to_file")] + pub fn to_file_py(&self, filepath: &PyAny) -> anyhow::Result<()> { + self.to_file(PathBuf::extract(filepath)?) + } + + #[staticmethod] + #[pyo3(name = "from_file")] + pub fn from_file_py(filepath: &PyAny) -> anyhow::Result { + Self::from_file(PathBuf::extract(filepath)?) + } + + #[pyo3(name = "to_str")] + pub fn to_str_py(&self, format: &str) -> anyhow::Result { + self.to_str(format) + } + + #[staticmethod] + #[pyo3(name = "from_str")] + pub fn from_str_py(contents: &str, format: &str) -> anyhow::Result { + Self::from_str(contents, format) + } + + /// JSON serialization method. #[pyo3(name = "to_json")] - pub fn to_json_py(&self) -> PyResult { - Ok(self.to_json()) + pub fn to_json_py(&self) -> anyhow::Result { + self.to_json() } - #[classmethod] - /// json deserialization method. + #[staticmethod] + /// JSON deserialization method. #[pyo3(name = "from_json")] - pub fn from_json_py(_cls: &PyType, json_str: &str) -> PyResult { - Ok(Self::from_json(json_str)?) + pub fn from_json_py(json_str: &str) -> anyhow::Result { + Self::from_json(json_str) } - /// yaml serialization method. + /// YAML serialization method. #[pyo3(name = "to_yaml")] - pub fn to_yaml_py(&self) -> PyResult { - Ok(self.to_yaml()) + pub fn to_yaml_py(&self) -> anyhow::Result { + self.to_yaml() } - #[classmethod] - /// yaml deserialization method. + #[staticmethod] + /// YAML deserialization method. #[pyo3(name = "from_yaml")] - pub fn from_yaml_py(_cls: &PyType, yaml_str: &str) -> PyResult { - Ok(Self::from_yaml(yaml_str)?) + pub fn from_yaml_py(yaml_str: &str) -> anyhow::Result { + Self::from_yaml(yaml_str) } /// bincode serialization method. #[pyo3(name = "to_bincode")] - pub fn to_bincode_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyBytes> { - Ok(PyBytes::new(py, &self.to_bincode())) + pub fn to_bincode_py<'py>(&self, py: Python<'py>) -> anyhow::Result<&'py PyBytes> { + Ok(PyBytes::new(py, &self.to_bincode()?)) } - #[classmethod] + #[staticmethod] /// bincode deserialization method. #[pyo3(name = "from_bincode")] - pub fn from_bincode_py(_cls: &PyType, encoded: &PyBytes) -> PyResult { - Ok(Self::from_bincode(encoded.as_bytes())?) + pub fn from_bincode_py(encoded: &PyBytes) -> anyhow::Result { + Self::from_bincode(encoded.as_bytes()) } }); @@ -330,13 +342,12 @@ pub fn impl_getters_and_setters( "orphaned" => { impl_block.extend::(quote! { #[getter] - pub fn get_orphaned(&self) -> PyResult { - Ok(self.orphaned) + pub fn get_orphaned(&self) -> bool { + self.orphaned } /// Reset the orphaned flag to false. - pub fn reset_orphaned(&mut self) -> PyResult<()> { + pub fn reset_orphaned(&mut self) { self.orphaned = false; - Ok(()) } }) } diff --git a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/pyo3_api_utils.rs b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/pyo3_api_utils.rs index e412b3cd..219770bc 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/pyo3_api_utils.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/pyo3_api_utils.rs @@ -4,8 +4,8 @@ macro_rules! impl_vec_get_set { let get_name: TokenStream2 = format!("get_{}", $fident).parse().unwrap(); $impl_block.extend::(quote! { #[getter] - pub fn #get_name(&self) -> PyResult<$wrapper_type> { - Ok($wrapper_type::new(self.#$fident.clone())) + pub fn #get_name(&self) -> $wrapper_type { + $wrapper_type::new(self.#$fident.clone()) } }); } @@ -16,21 +16,20 @@ macro_rules! impl_vec_get_set { if $has_orphaned { $impl_block.extend(quote! { #[setter] - pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> PyResult<()> { + pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> anyhow::Result<()> { if !self.orphaned { self.#$fident = new_value; Ok(()) } else { - Err(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) } } }) } else { $impl_block.extend(quote! { #[setter] - pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> PyResult<()> { + pub fn #set_name(&mut self, new_value: Vec<$contained_type>) { self.#$fident = new_value; - Ok(()) } }) } @@ -39,21 +38,20 @@ macro_rules! impl_vec_get_set { if $has_orphaned { $impl_block.extend(quote! { #[setter] - pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> PyResult<()> { + pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> anyhow::Result<()> { if !self.orphaned { self.#$fident = Array1::from_vec(new_value); Ok(()) } else { - Err(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) } } }) } else { $impl_block.extend(quote! { #[setter] - pub fn #set_name(&mut self, new_value: Vec<$contained_type>) -> PyResult<()> { + pub fn #set_name(&mut self, new_value: Vec<$contained_type>) { self.#$fident = Array1::from_vec(new_value); - Ok(()) } }) } @@ -80,16 +78,16 @@ macro_rules! impl_get_body { let get_block = if $opts.field_has_orphaned { quote! { #[getter] - pub fn #get_name(&mut self) -> PyResult<#$type> { + pub fn #get_name(&mut self) -> #$type { self.#$field.orphaned = true; - Ok(self.#$field.clone()) + self.#$field.clone() } } } else { quote! { #[getter] - pub fn #get_name(&self) -> PyResult<#$type> { - Ok(self.#$field.clone()) + pub fn #get_name(&self) -> #$type { + self.#$field.clone() } } }; @@ -120,7 +118,7 @@ macro_rules! impl_set_body { self.#$field.orphaned = false; Ok(()) } else { - Err(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) } } } else if $has_orphaned { @@ -129,7 +127,7 @@ macro_rules! impl_set_body { self.#$field = new_value; Ok(()) } else { - Err(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(crate::utils::NESTED_STRUCT_ERR)) } } } else { @@ -141,7 +139,7 @@ macro_rules! impl_set_body { $impl_block.extend::(quote! { #[setter] - pub fn #set_name(&mut self, new_value: #$type) -> PyResult<()> { + pub fn #set_name(&mut self, new_value: #$type) -> anyhow::Result<()> { #orphaned_set_block } }); diff --git a/python/fastsim/resources/cycles/HHDDTCruiseSmooth.csv b/rust/fastsim-core/resources/cycles/HHDDTCruiseSmooth.csv similarity index 100% rename from python/fastsim/resources/cycles/HHDDTCruiseSmooth.csv rename to rust/fastsim-core/resources/cycles/HHDDTCruiseSmooth.csv diff --git a/rust/fastsim-core/resources/hwfet.csv b/rust/fastsim-core/resources/cycles/hwfet.csv similarity index 100% rename from rust/fastsim-core/resources/hwfet.csv rename to rust/fastsim-core/resources/cycles/hwfet.csv diff --git a/rust/fastsim-core/resources/udds.csv b/rust/fastsim-core/resources/cycles/udds.csv similarity index 100% rename from rust/fastsim-core/resources/udds.csv rename to rust/fastsim-core/resources/cycles/udds.csv diff --git a/rust/fastsim-core/src/cycle.rs b/rust/fastsim-core/src/cycle.rs index 56087f9a..61c5d4e1 100644 --- a/rust/fastsim-core/src/cycle.rs +++ b/rust/fastsim-core/src/cycle.rs @@ -4,8 +4,6 @@ extern crate ndarray; #[cfg(feature = "pyo3")] use std::collections::HashMap; -use std::fs::File; -use std::path::PathBuf; // local use crate::imports::*; @@ -34,9 +32,9 @@ pub fn calc_constant_jerk_trajectory( dr: f64, vr: f64, dt: f64, -) -> (f64, f64) { - assert!(n > 1); - assert!(dr > d0); +) -> anyhow::Result<(f64, f64)> { + ensure!(n > 1); + ensure!(dr > d0); let n = n as f64; let ddr = dr - d0; let dvr = vr - v0; @@ -48,7 +46,7 @@ pub fn calc_constant_jerk_trajectory( - n * v0 - ((1.0 / 6.0) * n * (n - 1.0) * (n - 2.0) * dt + 0.25 * n * (n - 1.0) * dt * dt) * k) / (0.5 * n * n * dt); - (k, a0) + Ok((k, a0)) } #[cfg_attr(feature = "pyo3", pyfunction)] @@ -197,7 +195,7 @@ pub fn time_spent_moving(cyc: &RustCycle, stopped_speed_m_per_s: Option) -> /// to subsequent stop plus any idle (stopped time). /// Arguments: /// ---------- -/// cycle: drive cycle converted to dictionary by cycle.get_cyc_dict() +/// cycle: drive cycle /// stop_speed_m__s: speed at which vehicle is considered stopped for trip /// separation /// keep_name: (optional) bool, if True and cycle contains "name", adds @@ -351,7 +349,7 @@ pub fn extend_cycle( #[cfg(feature = "pyo3")] #[allow(unused)] -pub fn register(_py: Python<'_>, m: &PyModule) -> Result<(), anyhow::Error> { +pub fn register(_py: Python<'_>, m: &PyModule) -> anyhow::Result<()> { m.add_function(wrap_pyfunction!(calc_constant_jerk_trajectory, m)?)?; m.add_function(wrap_pyfunction!(accel_for_constant_jerk, m)?)?; m.add_function(wrap_pyfunction!(speed_for_constant_jerk, m)?)?; @@ -484,29 +482,28 @@ impl RustCycleCache { } #[allow(clippy::type_complexity)] - pub fn __getnewargs__(&self) -> PyResult<(Vec, Vec, Vec, Vec, &str)> { - Ok((self.time_s.to_vec(), self.mps.to_vec(), self.grade.to_vec(), self.road_type.to_vec(), &self.name)) + pub fn __getnewargs__(&self) -> (Vec, Vec, Vec, Vec, &str) { + (self.time_s.to_vec(), self.mps.to_vec(), self.grade.to_vec(), self.road_type.to_vec(), &self.name) } - #[classmethod] - #[pyo3(name = "from_csv_file")] - pub fn from_csv_file_py(_cls: &PyType, pathstr: String) -> anyhow::Result { - Self::from_csv_file(&pathstr) + #[staticmethod] + #[pyo3(name = "from_csv")] + pub fn from_csv_py(filepath: &PyAny) -> anyhow::Result { + Self::from_csv_file(PathBuf::extract(filepath)?) } - pub fn to_rust(&self) -> anyhow::Result { - Ok(self.clone()) + pub fn to_rust(&self) -> Self { + self.clone() } /// Return a HashMap representing the cycle - pub fn get_cyc_dict(&self) -> anyhow::Result>> { - let dict: HashMap> = HashMap::from([ + pub fn get_cyc_dict(&self) -> HashMap> { + HashMap::from([ ("time_s".to_string(), self.time_s.to_vec()), ("mps".to_string(), self.mps.to_vec()), ("grade".to_string(), self.grade.to_vec()), ("road_type".to_string(), self.road_type.to_vec()), - ]); - Ok(dict) + ]) } #[pyo3(name = "modify_by_const_jerk_trajectory")] @@ -516,8 +513,8 @@ impl RustCycleCache { n: usize, jerk_m_per_s3: f64, accel0_m_per_s2: f64, - ) -> PyResult { - Ok(self.modify_by_const_jerk_trajectory(idx, n, jerk_m_per_s3, accel0_m_per_s2)) + ) -> f64 { + self.modify_by_const_jerk_trajectory(idx, n, jerk_m_per_s3, accel0_m_per_s2) } #[pyo3(name = "modify_with_braking_trajectory")] @@ -526,13 +523,13 @@ impl RustCycleCache { brake_accel_m_per_s2: f64, idx: usize, dts_m: Option - ) -> PyResult<(f64, usize)> { - Ok(self.modify_with_braking_trajectory(brake_accel_m_per_s2, idx, dts_m)) + ) -> anyhow::Result<(f64, usize)> { + self.modify_with_braking_trajectory(brake_accel_m_per_s2, idx, dts_m) } #[pyo3(name = "calc_distance_to_next_stop_from")] - pub fn calc_distance_to_next_stop_from_py(&self, distance_m: f64) -> PyResult { - Ok(self.calc_distance_to_next_stop_from(distance_m, None)) + pub fn calc_distance_to_next_stop_from_py(&self, distance_m: f64) -> f64 { + self.calc_distance_to_next_stop_from(distance_m, None) } #[pyo3(name = "average_grade_over_range")] @@ -540,51 +537,50 @@ impl RustCycleCache { &self, distance_start_m: f64, delta_distance_m: f64, - ) -> PyResult { - Ok(self.average_grade_over_range(distance_start_m, delta_distance_m, None)) + ) -> f64 { + self.average_grade_over_range(distance_start_m, delta_distance_m, None) } #[pyo3(name = "build_cache")] - pub fn build_cache_py(&self) -> PyResult { - Ok(self.build_cache()) + pub fn build_cache_py(&self) -> RustCycleCache { + self.build_cache() } #[pyo3(name = "dt_s_at_i")] - pub fn dt_s_at_i_py(&self, i: usize) -> PyResult { + pub fn dt_s_at_i_py(&self, i: usize) -> f64 { if i == 0 { - Ok(0.0) + 0.0 } else { - Ok(self.dt_s_at_i(i)) + self.dt_s_at_i(i) } } #[getter] - pub fn get_mph(&self) -> PyResult> { - Ok((&self.mps * crate::params::MPH_PER_MPS).to_vec()) + pub fn get_mph(&self) -> Vec { + (&self.mps * crate::params::MPH_PER_MPS).to_vec() } #[setter] - pub fn set_mph(&mut self, new_value: Vec) -> PyResult<()> { + pub fn set_mph(&mut self, new_value: Vec) { self.mps = Array::from_vec(new_value) / MPH_PER_MPS; - Ok(()) } #[getter] /// array of time steps - pub fn get_dt_s(&self) -> PyResult> { - Ok(self.dt_s().to_vec()) + pub fn get_dt_s(&self) -> Vec { + self.dt_s().to_vec() } #[getter] /// cycle length - pub fn get_len(&self) -> PyResult { - Ok(self.len()) + pub fn get_len(&self) -> usize { + self.len() } #[getter] /// distance for each time step based on final speed - pub fn get_dist_m(&self) -> PyResult> { - Ok(self.dist_m().to_vec()) + pub fn get_dist_m(&self) -> Vec { + self.dist_m().to_vec() } #[getter] - pub fn get_delta_elev_m(&self) -> PyResult> { - Ok(self.delta_elev_m().to_vec()) + pub fn get_delta_elev_m(&self) -> Vec { + self.delta_elev_m().to_vec() } )] /// Struct for containing: @@ -614,17 +610,76 @@ pub struct RustCycle { pub orphaned: bool, } +const ACCEPTED_FILE_FORMATS: [&str; 3] = ["yaml", "json", "csv"]; + impl SerdeAPI for RustCycle { - fn from_file(filename: &str) -> Result { - // check if the extension is csv, and if it is, then call Self::from_csv_file - let pathbuf = PathBuf::from(filename); - let file = File::open(filename)?; - let extension = pathbuf.extension().unwrap().to_str().unwrap(); - match extension { - "yaml" => Ok(serde_yaml::from_reader(file)?), - "json" => Ok(serde_json::from_reader(file)?), - "csv" => Ok(Self::from_csv_file(filename)?), - _ => Err(anyhow!("Unsupported file extension {}", extension)), + fn to_file>(&self, filepath: P) -> anyhow::Result<()> { + let filepath = filepath.as_ref(); + let extension = filepath + .extension() + .and_then(OsStr::to_str) + .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?; + match extension.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => serde_yaml::to_writer(&File::create(filepath)?, self)?, + "json" => serde_json::to_writer(&File::create(filepath)?, self)?, + "bin" => bincode::serialize_into(&File::create(filepath)?, self)?, + "csv" => self.write_csv(&mut csv::Writer::from_path(filepath)?)?, + _ => bail!( + "Unsupported file format {extension:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + } + Ok(()) + } + + fn from_reader(rdr: R, format: &str) -> anyhow::Result { + Ok( + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => serde_yaml::from_reader(rdr)?, + "json" => serde_json::from_reader(rdr)?, + "bin" => bincode::deserialize_from(rdr)?, + "csv" => { + // Create empty cycle to be populated + let mut cyc = Self::default(); + let mut rdr = csv::Reader::from_reader(rdr); + for result in rdr.deserialize() { + cyc.push(result?); + } + cyc + } + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + }, + ) + } + + fn to_str(&self, format: &str) -> anyhow::Result { + Ok( + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => self.to_yaml()?, + "json" => self.to_json()?, + "csv" => { + let mut wtr = csv::Writer::from_writer(Vec::with_capacity(self.len())); + self.write_csv(&mut wtr)?; + String::from_utf8(wtr.into_inner()?)? + } + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + }, + ) + } + + /// Note that using this method to instantiate a RustCycle from CSV, rather + /// than the `from_csv_str` method, sets the cycle name to an empty string + fn from_str(contents: &str, format: &str) -> anyhow::Result { + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => Self::from_yaml(contents), + "json" => Self::from_json(contents), + "csv" => Self::from_csv_str(contents, ""), + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), } } } @@ -652,6 +707,50 @@ impl RustCycle { } } + /// Load cycle from CSV file, parsing name from filepath + pub fn from_csv_file>(filepath: P) -> anyhow::Result { + let filepath = filepath.as_ref(); + let name = String::from( + filepath + .file_stem() + .and_then(OsStr::to_str) + .with_context(|| { + format!("Could not parse cycle name from filepath: {filepath:?}") + })?, + ); + let file = File::open(filepath).with_context(|| { + if !filepath.exists() { + format!("File not found: {filepath:?}") + } else { + format!("Could not open file: {filepath:?}") + } + })?; + let mut cyc = Self::from_reader(file, "csv")?; + cyc.name = name; + Ok(cyc) + } + + /// Load cycle from CSV string + pub fn from_csv_str(csv_str: &str, name: &str) -> anyhow::Result { + let mut cyc = Self::from_reader(csv_str.as_bytes(), "csv")?; + cyc.name = name.to_string(); + Ok(cyc) + } + + /// Write cycle data to a CSV writer + fn write_csv(&self, wtr: &mut csv::Writer) -> anyhow::Result<()> { + for i in 0..self.len() { + wtr.serialize(RustCycleElement { + time_s: self.time_s[i], + mps: self.mps[i], + grade: Some(self.grade[i]), + road_type: Some(self.road_type[i]), + })?; + } + wtr.flush()?; + Ok(()) + } + pub fn build_cache(&self) -> RustCycleCache { RustCycleCache::new(self) } @@ -859,10 +958,10 @@ impl RustCycle { brake_accel_m_per_s2: f64, i: usize, dts_m: Option, - ) -> (f64, usize) { - assert!(brake_accel_m_per_s2 < 0.0); + ) -> anyhow::Result<(f64, usize)> { + ensure!(brake_accel_m_per_s2 < 0.0); if i >= self.time_s.len() { - return (*self.mps.last().unwrap(), 0); + return Ok((*self.mps.last().unwrap(), 0)); } let v0 = self.mps[i - 1]; let dt = self.dt_s_at_i(i); @@ -878,7 +977,7 @@ impl RustCycle { None => -0.5 * v0 * v0 / brake_accel_m_per_s2, }; if dts_m <= 0.0 { - return (v0, 0); + return Ok((v0, 0)); } // time-to-stop (s) let tts_s = -v0 / brake_accel_m_per_s2; @@ -886,11 +985,11 @@ impl RustCycle { let n: usize = (tts_s / dt).round() as usize; let n: usize = if n < 2 { 2 } else { n }; // need at least 2 steps let (jerk_m_per_s3, accel_m_per_s2) = - calc_constant_jerk_trajectory(n, 0.0, v0, dts_m, 0.0, dt); - ( + calc_constant_jerk_trajectory(n, 0.0, v0, dts_m, 0.0, dt)?; + Ok(( self.modify_by_const_jerk_trajectory(i, n, jerk_m_per_s3, accel_m_per_s2), n, - ) + )) } /// rust-internal time steps @@ -913,51 +1012,10 @@ impl RustCycle { self.mps[i] * MPH_PER_MPS } - /// Load cycle from csv file - pub fn from_csv_file(pathstr: &str) -> Result { - let pathbuf = PathBuf::from(&pathstr); - - // create empty cycle to be populated - let mut cyc = Self::default(); - - // unwrap is ok because if statement checks existence - let file = File::open(&pathbuf).unwrap(); - let name = String::from(pathbuf.file_stem().unwrap().to_str().unwrap()); - cyc.name = name; - let mut rdr = csv::ReaderBuilder::new() - .has_headers(true) - .from_reader(file); - for result in rdr.deserialize() { - // TODO: make this more elegant than unwrap - let cyc_elem: RustCycleElement = result?; - cyc.push(cyc_elem); - } - - Ok(cyc) - } - /// elevation change w.r.t. to initial pub fn delta_elev_m(&self) -> Array1 { ndarrcumsum(&(self.dist_m() * self.grade.clone())) } - - // load a cycle from a string representation of a csv file - pub fn from_csv_string(data: &str, name: String) -> Result { - let mut cyc = Self { - name, - ..Self::default() - }; - - let mut rdr = csv::ReaderBuilder::new() - .has_headers(true) - .from_reader(data.as_bytes()); - for result in rdr.deserialize() { - let cyc_elem: RustCycleElement = result?; - cyc.push(cyc_elem); - } - - Ok(cyc) - } } pub struct PassingInfo { @@ -1087,12 +1145,11 @@ mod tests { #[test] fn test_loading_a_cycle_from_the_filesystem() { - let mut cyc_file_path = resources_path(); - cyc_file_path.push("cycles/udds.csv"); + let cyc_file_path = resources_path().join("cycles/udds.csv"); let expected_udds_length: usize = 1370; - let cyc = RustCycle::from_csv_file(cyc_file_path.as_os_str().to_str().unwrap()).unwrap(); - assert_eq!(cyc.name, String::from("udds")); + let cyc = RustCycle::from_csv_file(cyc_file_path).unwrap(); let num_entries = cyc.time_s.len(); + assert_eq!(cyc.name, String::from("udds")); assert!(num_entries > 0); assert_eq!(num_entries, cyc.time_s.len()); assert_eq!(num_entries, cyc.mps.len()); @@ -1100,4 +1157,13 @@ mod tests { assert_eq!(num_entries, cyc.road_type.len()); assert_eq!(num_entries, expected_udds_length); } + + #[test] + fn test_str_serde() { + let format = "csv"; + let cyc = RustCycle::test_cyc(); + println!("{cyc:?}"); + let csv_str = cyc.to_str(format).unwrap(); + RustCycle::from_str(&csv_str, format).unwrap(); + } } diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index a28083f9..071a4766 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -1,12 +1,14 @@ pub(crate) use anyhow::{anyhow, bail, ensure, Context}; -pub(crate) use bincode::{deserialize, serialize}; +pub(crate) use bincode; pub(crate) use log; pub(crate) use ndarray::{array, concatenate, s, Array, Array1, Axis}; pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use std::cmp; pub(crate) use std::ffi::OsStr; pub(crate) use std::fs::File; -pub(crate) use std::path::{Path, PathBuf}; +pub(crate) use std::path::Path; +#[allow(unused_imports)] +pub(crate) use std::path::PathBuf; pub(crate) use crate::traits::*; pub(crate) use crate::utils::*; diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 3457ed1f..e6e6bd8d 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -40,6 +40,7 @@ pub mod params; pub mod pyo3imports; pub mod simdrive; pub use simdrive::simdrive_impl; +pub mod resources; pub mod simdrivelabel; pub mod thermal; pub mod traits; diff --git a/rust/fastsim-core/src/macros.rs b/rust/fastsim-core/src/macros.rs index d853718e..ff4f41a1 100644 --- a/rust/fastsim-core/src/macros.rs +++ b/rust/fastsim-core/src/macros.rs @@ -37,11 +37,16 @@ macro_rules! print_to_py { #[macro_export] macro_rules! check_orphaned_and_set { ($struct_self: ident, $field: ident, $value: expr) => { + // TODO: This seems like it could be used instead, but raises an error + // ensure!(!$struct_self.orphaned, utils::NESTED_STRUCT_ERR); + // $struct_self.$field = $value; + // Ok(()) + if !$struct_self.orphaned { $struct_self.$field = $value; - Ok(()) + anyhow::Ok(()) } else { - Err(anyhow!(utils::NESTED_STRUCT_ERR)) + bail!(utils::NESTED_STRUCT_ERR) } }; } diff --git a/rust/fastsim-core/src/pyo3imports.rs b/rust/fastsim-core/src/pyo3imports.rs index ee55a242..06a52ed9 100644 --- a/rust/fastsim-core/src/pyo3imports.rs +++ b/rust/fastsim-core/src/pyo3imports.rs @@ -1,4 +1,4 @@ #![cfg(feature = "pyo3")] pub use pyo3::exceptions::*; pub use pyo3::prelude::*; -pub use pyo3::types::{PyBytes, PyDict, PyType}; +pub use pyo3::types::{PyAny, PyBytes, PyDict, PyType}; diff --git a/rust/fastsim-core/src/resources.rs b/rust/fastsim-core/src/resources.rs new file mode 100644 index 00000000..0cf74313 --- /dev/null +++ b/rust/fastsim-core/src/resources.rs @@ -0,0 +1,2 @@ +use include_dir::{include_dir, Dir}; +pub const RESOURCES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources"); diff --git a/rust/fastsim-core/src/simdrive.rs b/rust/fastsim-core/src/simdrive.rs index 7cefb196..88953ac3 100644 --- a/rust/fastsim-core/src/simdrive.rs +++ b/rust/fastsim-core/src/simdrive.rs @@ -152,7 +152,7 @@ impl Default for RustSimDriveParams { #[pyo3(name = "gap_to_lead_vehicle_m")] /// Provides the gap-with lead vehicle from start to finish - pub fn gap_to_lead_vehicle_m_py(&self) -> PyResult> { + pub fn gap_to_lead_vehicle_m_py(&self) -> anyhow::Result> { Ok(self.gap_to_lead_vehicle_m().to_vec()) } @@ -167,9 +167,9 @@ impl Default for RustSimDriveParams { &mut self, init_soc: Option, aux_in_kw_override: Option>, - ) -> PyResult<()> { + ) -> anyhow::Result<()> { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); - Ok(self.sim_drive(init_soc, aux_in_kw_override)?) + self.sim_drive(init_soc, aux_in_kw_override) } /// Receives second-by-second cycle information, vehicle properties, @@ -187,9 +187,9 @@ impl Default for RustSimDriveParams { &mut self, init_soc: f64, aux_in_kw_override: Option>, - ) -> PyResult<()> { + ) -> anyhow::Result<()> { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); - Ok(self.walk(init_soc, aux_in_kw_override)?) + self.walk(init_soc, aux_in_kw_override) } /// Sets the intelligent driver model parameters for an eco-cruise driving trajectory. @@ -206,13 +206,13 @@ impl Default for RustSimDriveParams { extend_fraction: Option, blend_factor: Option, min_target_speed_m_per_s: Option, - ) -> PyResult<()> { + ) -> anyhow::Result<()> { let by_microtrip: bool = by_microtrip.unwrap_or(false); let extend_fraction: f64 = extend_fraction.unwrap_or(0.1); let blend_factor: f64 = blend_factor.unwrap_or(0.0); let min_target_speed_m_per_s = min_target_speed_m_per_s.unwrap_or(8.0); - Ok(self.activate_eco_cruise_rust( - by_microtrip, extend_fraction, blend_factor, min_target_speed_m_per_s)?) + self.activate_eco_cruise_rust( + by_microtrip, extend_fraction, blend_factor, min_target_speed_m_per_s) } #[pyo3(name = "init_for_step")] @@ -227,20 +227,20 @@ impl Default for RustSimDriveParams { &mut self, init_soc:f64, aux_in_kw_override: Option> - ) -> PyResult<()> { + ) -> anyhow::Result<()> { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); - Ok(self.init_for_step(init_soc, aux_in_kw_override)?) + self.init_for_step(init_soc, aux_in_kw_override) } /// Step through 1 time step. - pub fn sim_drive_step(&mut self) -> PyResult<()> { - Ok(self.step()?) + pub fn sim_drive_step(&mut self) -> anyhow::Result<()> { + self.step() } #[pyo3(name = "solve_step")] /// Perform all the calculations to solve 1 time step. - pub fn solve_step_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.solve_step(i)?) + pub fn solve_step_py(&mut self, i: usize) -> anyhow::Result<()> { + self.solve_step(i) } #[pyo3(name = "set_misc_calcs")] @@ -248,8 +248,8 @@ impl Default for RustSimDriveParams { /// Arguments: /// ---------- /// i: index of time step - pub fn set_misc_calcs_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_misc_calcs(i)?) + pub fn set_misc_calcs_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_misc_calcs(i) } #[pyo3(name = "set_comp_lims")] @@ -257,8 +257,8 @@ impl Default for RustSimDriveParams { // Arguments // ------------ // i: index of time step - pub fn set_comp_lims_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_comp_lims(i)?) + pub fn set_comp_lims_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_comp_lims(i) } #[pyo3(name = "set_power_calcs")] @@ -267,8 +267,8 @@ impl Default for RustSimDriveParams { /// Arguments /// ------------ /// i: index of time step - pub fn set_power_calcs_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_power_calcs(i)?) + pub fn set_power_calcs_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_power_calcs(i) } #[pyo3(name = "set_ach_speed")] @@ -276,8 +276,8 @@ impl Default for RustSimDriveParams { // Arguments // ------------ // i: index of time step - pub fn set_ach_speed_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_ach_speed(i)?) + pub fn set_ach_speed_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_ach_speed(i) } #[pyo3(name = "set_hybrid_cont_calcs")] @@ -285,8 +285,8 @@ impl Default for RustSimDriveParams { /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_calcs_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_hybrid_cont_calcs(i)?) + pub fn set_hybrid_cont_calcs_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_hybrid_cont_calcs(i) } #[pyo3(name = "set_fc_forced_state")] @@ -295,8 +295,8 @@ impl Default for RustSimDriveParams { /// ------------ /// i: index of time step /// `_py` extension is needed to avoid name collision with getter/setter methods - pub fn set_fc_forced_state_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_fc_forced_state_rust(i)?) + pub fn set_fc_forced_state_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_fc_forced_state_rust(i) } #[pyo3(name = "set_hybrid_cont_decisions")] @@ -304,8 +304,8 @@ impl Default for RustSimDriveParams { /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_decisions_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_hybrid_cont_decisions(i)?) + pub fn set_hybrid_cont_decisions_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_hybrid_cont_decisions(i) } #[pyo3(name = "set_fc_power")] @@ -313,8 +313,8 @@ impl Default for RustSimDriveParams { /// Arguments /// ------------ /// i: index of time step - pub fn set_fc_power_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_fc_power(i)?) + pub fn set_fc_power_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_fc_power(i) } #[pyo3(name = "set_time_dilation")] @@ -322,15 +322,15 @@ impl Default for RustSimDriveParams { /// Arguments /// ------------ /// i: index of time step - pub fn set_time_dilation_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_time_dilation(i)?) + pub fn set_time_dilation_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_time_dilation(i) } #[pyo3(name = "set_post_scalars")] /// Sets scalar variables that can be calculated after a cycle is run. /// This includes mpgge, various energy metrics, and others - pub fn set_post_scalars_py(&mut self) -> PyResult<()> { - Ok(self.set_post_scalars()?) + pub fn set_post_scalars_py(&mut self) -> anyhow::Result<()> { + self.set_post_scalars() } #[pyo3(name = "len")] @@ -346,16 +346,12 @@ impl Default for RustSimDriveParams { } #[getter] - pub fn get_fs_cumu_mj_out_ach(&self) -> PyResult { - Ok( - Pyo3ArrayF64::new(ndarrcumsum(&(self.fs_kw_out_ach.clone() * self.cyc.dt_s() * 1e-3))) - ) + pub fn get_fs_cumu_mj_out_ach(&self) -> Pyo3ArrayF64 { + Pyo3ArrayF64::new(ndarrcumsum(&(self.fs_kw_out_ach.clone() * self.cyc.dt_s() * 1e-3))) } #[getter] - pub fn get_fc_cumu_mj_out_ach(&self) -> PyResult { - Ok( - Pyo3ArrayF64::new(ndarrcumsum(&(self.fc_kw_out_ach.clone() * self.cyc.dt_s() * 1e-3))) - ) + pub fn get_fc_cumu_mj_out_ach(&self) -> Pyo3ArrayF64 { + Pyo3ArrayF64::new(ndarrcumsum(&(self.fc_kw_out_ach.clone() * self.cyc.dt_s() * 1e-3))) } )] pub struct RustSimDrive { diff --git a/rust/fastsim-core/src/simdrive/cyc_mods.rs b/rust/fastsim-core/src/simdrive/cyc_mods.rs index de9d50fa..59a3544e 100644 --- a/rust/fastsim-core/src/simdrive/cyc_mods.rs +++ b/rust/fastsim-core/src/simdrive/cyc_mods.rs @@ -38,7 +38,7 @@ impl RustSimDrive { extend_fraction: f64, // 0.1 blend_factor: f64, // 0.0 min_target_speed_m_per_s: f64, // 8.0 - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { self.sim_params.idm_allow = true; if !by_microtrip { self.sim_params.idm_v_desired_m_per_s = @@ -52,18 +52,16 @@ impl RustSimDrive { 0.0 }; } else { - if !(0.0..=1.0).contains(&blend_factor) { - return Err(anyhow!( - "blend_factor must be between 0 and 1 but got {}", - blend_factor - )); - } - if min_target_speed_m_per_s < 0.0 { - return Err(anyhow!( - "min_target_speed_m_per_s must be >= 0 but got {}", - min_target_speed_m_per_s - )); - } + ensure!( + (0.0..=1.0).contains(&blend_factor), + "blend_factor must be between 0 and 1 but got {}", + blend_factor + ); + ensure!( + min_target_speed_m_per_s >= 0.0, + "min_target_speed_m_per_s must be >= 0.0 but got {}", + min_target_speed_m_per_s + ); self.sim_params.idm_v_desired_in_m_per_s_by_distance_m = Some(create_dist_and_target_speeds_by_microtrip( &self.cyc0, @@ -72,12 +70,11 @@ impl RustSimDrive { )); } // Extend the duration of the base cycle - if extend_fraction < 0.0 { - return Err(anyhow!( - "extend_fraction must be >= 0.0 but got {}", - extend_fraction - )); - } + ensure!( + extend_fraction >= 0.0, + "extend_fraction must be >= 0.0 but got {}", + extend_fraction + ); if extend_fraction > 0.0 { self.cyc0 = extend_cycle(&self.cyc0, None, Some(extend_fraction)); self.cyc = self.cyc0.clone(); @@ -231,7 +228,7 @@ impl RustSimDrive { ), } } - pub fn set_time_dilation(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_time_dilation(&mut self, i: usize) -> anyhow::Result<()> { // if prescribed speed is zero, trace is met to avoid div-by-zero errors and other possible wackiness let mut trace_met = (self.cyc.dist_m().slice(s![0..(i + 1)]).sum() - self.dist_m.slice(s![0..(i + 1)]).sum()) @@ -340,7 +337,7 @@ impl RustSimDrive { } } - fn apply_coast_trajectory(&mut self, coast_traj: CoastTrajectory) { + fn apply_coast_trajectory(&mut self, coast_traj: CoastTrajectory) -> anyhow::Result<()> { if coast_traj.found_trajectory { let num_speeds = match coast_traj.speeds_m_per_s { Some(speeds_m_per_s) => { @@ -359,12 +356,13 @@ impl RustSimDrive { self.sim_params.coast_brake_accel_m_per_s2, coast_traj.start_idx + num_speeds, coast_traj.distance_to_brake_m, - ); + )?; for di in 0..(self.cyc0.mps.len() - coast_traj.start_idx) { let idx = coast_traj.start_idx + di; self.impose_coast[idx] = di < num_speeds + n; } } + Ok(()) } /// Generate a coast trajectory without actually modifying the cycle. @@ -619,7 +617,7 @@ impl RustSimDrive { i: usize, min_accel_m_per_s2: f64, max_accel_m_per_s2: f64, - ) -> (bool, usize, f64, f64) { + ) -> anyhow::Result<(bool, usize, f64, f64)> { let tol = 1e-6; // v0 is where n=0, i.e., idx-1 let v0 = self.cyc.mps[i - 1]; @@ -638,7 +636,7 @@ impl RustSimDrive { ); if v0 < (brake_start_speed_m_per_s + tol) { // don't process braking - return not_found; + return Ok(not_found); } let (min_accel_m_per_s2, max_accel_m_per_s2) = if min_accel_m_per_s2 > max_accel_m_per_s2 { (max_accel_m_per_s2, min_accel_m_per_s2) @@ -654,7 +652,7 @@ impl RustSimDrive { .calc_distance_to_next_stop_from(d0, Some(&self.cyc0_cache)); if dts0 < 0.0 { // no stop to coast towards or we're there... - return not_found; + return Ok(not_found); } let dt = self.cyc.dt_s_at_i(i); // distance to brake from the brake start speed (m/s) @@ -663,7 +661,7 @@ impl RustSimDrive { // distance to brake initiation from start of time-step (m) let dtbi0 = dts0 - dtb; if dtbi0 < 0.0 { - return not_found; + return Ok(not_found); } // Now, check rendezvous trajectories let mut step_idx = i; @@ -699,7 +697,7 @@ impl RustSimDrive { dtbi0, brake_start_speed_m_per_s, dt, - ); + )?; if r_bi_accel_m_per_s2 < max_accel_m_per_s2 && r_bi_accel_m_per_s2 > min_accel_m_per_s2 && r_bi_jerk_m_per_s3 >= 0.0 @@ -731,14 +729,14 @@ impl RustSimDrive { step_idx += 1; } if r_best_found { - return ( + return Ok(( r_best_found, r_best_n, r_best_jerk_m_per_s3, r_best_accel_m_per_s2, - ); + )); } - not_found + Ok(not_found) } /// Coast Delay allows us to represent coasting to a stop when the lead @@ -815,11 +813,11 @@ impl RustSimDrive { /// - i: int, index for consideration /// - passing_tol_m: None | float, tolerance for how far we have to go past the lead vehicle to be considered "passing" /// RETURN: Bool, True if cyc was modified - fn prevent_collisions(&mut self, i: usize, passing_tol_m: Option) -> bool { + fn prevent_collisions(&mut self, i: usize, passing_tol_m: Option) -> anyhow::Result { let passing_tol_m = passing_tol_m.unwrap_or(1.0); let collision: PassingInfo = detect_passing(&self.cyc, &self.cyc0, i, Some(passing_tol_m)); if !collision.has_collision { - return false; + return Ok(false); } let mut best: RendezvousTrajectory = RendezvousTrajectory { found_trajectory: false, @@ -871,7 +869,7 @@ impl RustSimDrive { collision.distance_m, collision.speed_m_per_s, dt, - ); + )?; let mut accels_m_per_s2: Vec = vec![]; let mut trace_accels_m_per_s2: Vec = vec![]; for ni in 0..n { @@ -931,7 +929,7 @@ impl RustSimDrive { passing_tol_m + 5.0 }; if new_passing_tol_m > 60.0 { - return false; + return Ok(false); } return self.prevent_collisions(i, Some(new_passing_tol_m)); } @@ -956,13 +954,13 @@ impl RustSimDrive { self.impose_coast[idx] = false; self.coast_delay_index[idx] = 0; } - true + Ok(true) } /// Placeholder for method to impose coasting. /// Might be good to include logic for deciding when to coast. /// Solve for the next-step speed that will yield a zero roadload - pub fn set_coast_speed(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_coast_speed(&mut self, i: usize) -> anyhow::Result<()> { let tol = 1e-6; let v0 = self.mps_ach[i - 1]; if v0 > tol && !self.impose_coast[i] && self.should_impose_coast(i) { @@ -974,10 +972,10 @@ impl RustSimDrive { self.impose_coast[idx] = false; } } else { - self.apply_coast_trajectory(ct); + self.apply_coast_trajectory(ct)?; } if !self.sim_params.coast_allow_passing { - self.prevent_collisions(i, None); + self.prevent_collisions(i, None)?; } } } @@ -1038,7 +1036,7 @@ impl RustSimDrive { self.sim_params.coast_brake_accel_m_per_s2, i, None, - ); + )?; for idx in i..self.cyc.time_s.len() { self.impose_coast[idx] = idx < (i + num_steps); } @@ -1049,7 +1047,7 @@ impl RustSimDrive { i, self.sim_params.coast_brake_accel_m_per_s2, min(accel_proposed, 0.0), - ); + )?; if traj_found { // adjust cyc to perform the trajectory let final_speed_m_per_s = self.cyc.modify_by_const_jerk_trajectory( @@ -1070,7 +1068,7 @@ impl RustSimDrive { self.sim_params.coast_brake_accel_m_per_s2, i_for_brake, None, - ); + )?; for idx in i_for_brake..self.cyc0.mps.len() { self.impose_coast[idx] = idx < i_for_brake + num_steps; } @@ -1089,7 +1087,7 @@ impl RustSimDrive { } if adjusted_current_speed { if !self.sim_params.coast_allow_passing { - self.prevent_collisions(i, None); + self.prevent_collisions(i, None)?; } self.solve_step(i)?; self.newton_iters[i] = 0; // reset newton iters diff --git a/rust/fastsim-core/src/simdrive/simdrive_impl.rs b/rust/fastsim-core/src/simdrive/simdrive_impl.rs index 69d48ec8..ff916267 100644 --- a/rust/fastsim-core/src/simdrive/simdrive_impl.rs +++ b/rust/fastsim-core/src/simdrive/simdrive_impl.rs @@ -391,7 +391,7 @@ impl RustSimDrive { &mut self, init_soc: Option, aux_in_kw_override: Option>, - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { self.hev_sim_count = 0; let init_soc = match init_soc { @@ -451,15 +451,14 @@ impl RustSimDrive { self.walk(init_soc, aux_in_kw_override)?; - self.set_post_scalars()?; - Ok(()) + self.set_post_scalars() } pub fn sim_drive_accel( &mut self, init_soc: Option, aux_in_kw_override: Option>, - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { // Initialize and run sim_drive_walk as appropriate for vehicle attribute vehPtType. let init_soc_auto: f64 = match self.veh.veh_pt_type.as_str() { // If no EV / Hybrid components, no SOC considerations. @@ -488,7 +487,7 @@ impl RustSimDrive { &mut self, init_soc: f64, aux_in_kw_override: Option>, - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { self.init_for_step(init_soc, aux_in_kw_override)?; while self.i < self.cyc.time_s.len() { self.step()?; @@ -515,7 +514,7 @@ impl RustSimDrive { &mut self, init_soc: f64, aux_in_kw_override: Option>, - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { ensure!( self.veh.veh_pt_type == CONV || (self.veh.min_soc..=self.veh.max_soc).contains(&init_soc), @@ -553,7 +552,7 @@ impl RustSimDrive { } /// Step through 1 time step. - pub fn step(&mut self) -> Result<(), anyhow::Error> { + pub fn step(&mut self) -> anyhow::Result<()> { if self.sim_params.idm_allow { self.idm_target_speed_m_per_s[self.i] = match &self.sim_params.idm_v_desired_in_m_per_s_by_distance_m { @@ -594,7 +593,7 @@ impl RustSimDrive { } /// Perform all the calculations to solve 1 time step. - pub fn solve_step(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn solve_step(&mut self, i: usize) -> anyhow::Result<()> { self.set_misc_calcs(i)?; self.set_comp_lims(i)?; self.set_power_calcs(i)?; @@ -610,7 +609,7 @@ impl RustSimDrive { /// Arguments: /// ---------- /// i: index of time step - pub fn set_misc_calcs(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_misc_calcs(&mut self, i: usize) -> anyhow::Result<()> { // if cycle iteration is used, auxInKw must be re-zeroed to trigger the below if statement // TODO: this is probably computationally expensive and was probably a workaround for numba // figure out a way to not need this @@ -638,7 +637,7 @@ impl RustSimDrive { /// ------------ /// i: index of time step /// initSoc: initial SOC for electrified vehicles - pub fn set_comp_lims(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_comp_lims(&mut self, i: usize) -> anyhow::Result<()> { // max fuel storage power output self.cur_max_fs_kw_out[i] = min( self.veh.fs_max_kw, @@ -834,7 +833,7 @@ impl RustSimDrive { /// Arguments /// ------------ /// i: index of time step - pub fn set_power_calcs(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_power_calcs(&mut self, i: usize) -> anyhow::Result<()> { let mps_ach = if self.newton_iters[i] > 0u32 { self.mps_ach[i] } else { @@ -931,7 +930,7 @@ impl RustSimDrive { // Arguments // ------------ // i: index of time step - pub fn set_ach_speed(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_ach_speed(&mut self, i: usize) -> anyhow::Result<()> { // Cycle is met if self.cyc_met[i] { self.mps_ach[i] = self.cyc.mps[i]; @@ -1028,12 +1027,12 @@ impl RustSimDrive { let speed_guess = speed_guesses .iter() .last() - .ok_or(anyhow!("{}", format_dbg!()))? + .ok_or_else(|| anyhow!("{}", format_dbg!()))? * (1.0 - g) - g * new_speed_guesses .iter() .last() - .ok_or(anyhow!("{}", format_dbg!()))? + .ok_or_else(|| anyhow!("{}", format_dbg!()))? / d_pwr_err_per_d_speed_guesses[speed_guesses.len() - 1]; let pwr_err = pwr_err_fn(speed_guess); let pwr_err_per_speed_guess = pwr_err_per_speed_guess_fn(speed_guess); @@ -1045,7 +1044,7 @@ impl RustSimDrive { converged = ((speed_guesses .iter() .last() - .ok_or(anyhow!("{}", format_dbg!()))? + .ok_or_else(|| anyhow!("{}", format_dbg!()))? - speed_guesses[speed_guesses.len() - 2]) / speed_guesses[speed_guesses.len() - 2]) .abs() @@ -1081,7 +1080,7 @@ impl RustSimDrive { /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_calcs(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_hybrid_cont_calcs(&mut self, i: usize) -> anyhow::Result<()> { if self.veh.no_elec_sys { self.regen_buff_soc[i] = 0.0; } else if self.veh.charging_on { @@ -1317,7 +1316,7 @@ impl RustSimDrive { /// Arguments /// ------------ /// i: index of time step - pub fn set_fc_forced_state_rust(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_fc_forced_state_rust(&mut self, i: usize) -> anyhow::Result<()> { // force fuel converter on if it was on in the previous time step, but only if fc // has not been on longer than minFcTimeOn self.fc_forced_on[i] = self.prev_fc_time_on[i] > 0.0 @@ -1362,7 +1361,7 @@ impl RustSimDrive { /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_decisions(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_hybrid_cont_decisions(&mut self, i: usize) -> anyhow::Result<()> { if (-self.mc_elec_in_kw_for_max_fc_eff[i] - self.cur_max_roadway_chg_kw[i]) > 0.0 { self.ess_desired_kw_4fc_eff[i] = (-self.mc_elec_in_kw_for_max_fc_eff[i] - self.cur_max_roadway_chg_kw[i]) @@ -1683,7 +1682,7 @@ impl RustSimDrive { /// Arguments /// ------------ /// i: index of time step - pub fn set_fc_power(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_fc_power(&mut self, i: usize) -> anyhow::Result<()> { if self.veh.fc_max_kw == 0.0 { self.fc_kw_out_ach[i] = 0.0; } else if self.veh.fc_eff_type == H2FC { @@ -1747,7 +1746,7 @@ impl RustSimDrive { /// Sets scalar variables that can be calculated after a cycle is run. /// This includes mpgge, various energy metrics, and others - pub fn set_post_scalars(&mut self) -> Result<(), anyhow::Error> { + pub fn set_post_scalars(&mut self) -> anyhow::Result<()> { if self.fs_kwh_out_ach.sum() == 0.0 { self.mpgge = 0.0; } else { diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index 2932c690..fe1b94c3 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -141,10 +141,7 @@ pub fn make_accel_trace_py() -> RustCycle { make_accel_trace() } -pub fn get_net_accel( - sd_accel: &mut RustSimDrive, - scenario_name: &String, -) -> Result { +pub fn get_net_accel(sd_accel: &mut RustSimDrive, scenario_name: &String) -> anyhow::Result { log::debug!("running `sim_drive_accel`"); sd_accel.sim_drive_accel(None, None)?; if sd_accel.mph_ach.iter().any(|&x| x >= 60.) { @@ -163,7 +160,7 @@ pub fn get_net_accel( #[cfg(feature = "pyo3")] #[pyfunction(name = "get_net_accel")] /// pyo3 version of [get_net_accel] -pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> PyResult { +pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> anyhow::Result { let result = get_net_accel(sd_accel, &scenario_name.to_string())?; Ok(result) } @@ -200,43 +197,8 @@ pub fn get_label_fe( // load the cycles and intstantiate simdrive objects cyc.insert("accel", make_accel_trace()); - #[cfg(not(windows))] - macro_rules! path_separator { - () => { - "/" - }; - } - - #[cfg(windows)] - macro_rules! path_separator { - () => { - r#"\"# - }; - } - - let udds_filestring = include_str!(concat!( - "..", - path_separator!(), - "resources", - path_separator!(), - "udds.csv" - )); - let hwy_filestring = include_str!(concat!( - "..", - path_separator!(), - "resources", - path_separator!(), - "hwfet.csv" - )); - - cyc.insert( - "udds", - RustCycle::from_csv_string(udds_filestring, "udds".to_string())?, - ); - cyc.insert( - "hwy", - RustCycle::from_csv_string(hwy_filestring, "hwfet".to_string())?, - ); + cyc.insert("udds", RustCycle::from_resource("cycles/udds.csv")?); + cyc.insert("hwy", RustCycle::from_resource("cycles/hwfet.csv")?); // run simdrive for non-phev powertrains sd.insert("udds", RustSimDrive::new(cyc["udds"].clone(), veh.clone())); @@ -426,7 +388,7 @@ pub fn get_label_fe_py( veh: &vehicle::RustVehicle, full_detail: Option, verbose: Option, -) -> PyResult<(LabelFe, Option>)> { +) -> anyhow::Result<(LabelFe, Option>)> { let result: (LabelFe, Option>) = get_label_fe(veh, full_detail, verbose)?; Ok(result) @@ -439,7 +401,7 @@ pub fn get_label_fe_phev( adj_params: &AdjCoef, sim_params: &RustSimDriveParams, props: &RustPhysicalProperties, -) -> Result { +) -> anyhow::Result { // PHEV-specific function for label fe. // // Arguments: @@ -722,7 +684,7 @@ pub fn get_label_fe_phev( match *key { "udds" => phev_calcs.udds = phev_calc.clone(), "hwy" => phev_calcs.hwy = phev_calc.clone(), - &_ => return Err(anyhow!("No field for cycle {}", key)), + &_ => bail!("No field for cycle {}", key), }; } @@ -740,7 +702,7 @@ pub fn get_label_fe_phev_py( long_params: RustLongParams, sim_params: &RustSimDriveParams, props: RustPhysicalProperties, -) -> Result { +) -> anyhow::Result { let mut sd_mut = HashMap::new(); for (key, value) in sd { sd_mut.insert(key, value); @@ -806,8 +768,8 @@ mod simdrivelabel_tests { assert!( label_fe.approx_eq(&label_fe_truth, 1e-10), "label_fe:\n{}\n\nlabel_fe_truth:\n{}", - label_fe.to_json(), - label_fe_truth.to_json() + label_fe.to_json().unwrap(), + label_fe_truth.to_json().unwrap(), ); } #[test] diff --git a/rust/fastsim-core/src/thermal.rs b/rust/fastsim-core/src/thermal.rs index ba7fa12a..c3398cbc 100644 --- a/rust/fastsim-core/src/thermal.rs +++ b/rust/fastsim-core/src/thermal.rs @@ -26,7 +26,7 @@ use crate::vehicle_thermal::*; #[pyo3(name = "gap_to_lead_vehicle_m")] /// Provides the gap-with lead vehicle from start to finish - pub fn gap_to_lead_vehicle_m_py(&self) -> PyResult> { + pub fn gap_to_lead_vehicle_m_py(&self) -> anyhow::Result> { Ok(self.gap_to_lead_vehicle_m().to_vec()) } #[pyo3(name = "sim_drive")] @@ -40,9 +40,9 @@ use crate::vehicle_thermal::*; &mut self, init_soc: Option, aux_in_kw_override: Option>, - ) -> PyResult<()> { + ) -> anyhow::Result<()> { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); - Ok(self.sim_drive(init_soc, aux_in_kw_override)?) + self.sim_drive(init_soc, aux_in_kw_override) } /// Receives second-by-second cycle information, vehicle properties, @@ -60,10 +60,9 @@ use crate::vehicle_thermal::*; &mut self, init_soc: f64, aux_in_kw_override: Option>, - ) -> PyResult<()> { + ) { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); self.walk(init_soc, aux_in_kw_override); - Ok(()) } #[pyo3(name = "init_for_step")] @@ -78,21 +77,19 @@ use crate::vehicle_thermal::*; &mut self, init_soc:f64, aux_in_kw_override: Option> - ) -> PyResult<()> { + ) { let aux_in_kw_override = aux_in_kw_override.map(Array1::from); self.init_for_step(init_soc, aux_in_kw_override); - Ok(()) } /// Step through 1 time step. - pub fn sim_drive_step(&mut self) -> PyResult<()> { - Ok(self.step()?) + pub fn sim_drive_step(&mut self) -> anyhow::Result<()> { + self.step() } #[pyo3(name = "solve_step")] /// Perform all the calculations to solve 1 time step. - pub fn solve_step_py(&mut self, i: usize) -> PyResult<()> { - self.solve_step(i); - Ok(()) + pub fn solve_step_py(&mut self, i: usize) -> anyhow::Result<()> { + self.solve_step(i) } #[pyo3(name = "set_misc_calcs")] @@ -100,9 +97,8 @@ use crate::vehicle_thermal::*; /// Arguments: /// ---------- /// i: index of time step - pub fn set_misc_calcs_py(&mut self, i: usize) -> PyResult<()> { + pub fn set_misc_calcs_py(&mut self, i: usize) { self.set_misc_calcs(i); - Ok(()) } #[pyo3(name = "set_comp_lims")] @@ -110,8 +106,8 @@ use crate::vehicle_thermal::*; // Arguments // ------------ // i: index of time step - pub fn set_comp_lims_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_comp_lims(i)?) + pub fn set_comp_lims_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_comp_lims(i) } #[pyo3(name = "set_power_calcs")] @@ -120,8 +116,8 @@ use crate::vehicle_thermal::*; /// Arguments /// ------------ /// i: index of time step - pub fn set_power_calcs_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_power_calcs(i)?) + pub fn set_power_calcs_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_power_calcs(i) } #[pyo3(name = "set_ach_speed")] @@ -129,8 +125,8 @@ use crate::vehicle_thermal::*; // Arguments // ------------ // i: index of time step - pub fn set_ach_speed_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_ach_speed(i)?) + pub fn set_ach_speed_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_ach_speed(i) } #[pyo3(name = "set_hybrid_cont_calcs")] @@ -138,8 +134,8 @@ use crate::vehicle_thermal::*; /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_calcs_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_hybrid_cont_calcs(i)?) + pub fn set_hybrid_cont_calcs_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_hybrid_cont_calcs(i) } #[pyo3(name = "set_fc_forced_state")] @@ -148,8 +144,8 @@ use crate::vehicle_thermal::*; /// ------------ /// i: index of time step /// `_py` extension is needed to avoid name collision with getter/setter methods - pub fn set_fc_forced_state_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_fc_forced_state_rust(i)?) + pub fn set_fc_forced_state_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_fc_forced_state_rust(i) } #[pyo3(name = "set_hybrid_cont_decisions")] @@ -157,8 +153,8 @@ use crate::vehicle_thermal::*; /// Arguments /// ------------ /// i: index of time step - pub fn set_hybrid_cont_decisions_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_hybrid_cont_decisions(i)?) + pub fn set_hybrid_cont_decisions_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_hybrid_cont_decisions(i) } #[pyo3(name = "set_fc_power")] @@ -166,8 +162,8 @@ use crate::vehicle_thermal::*; /// Arguments /// ------------ /// i: index of time step - pub fn set_fc_power_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_fc_power(i)?) + pub fn set_fc_power_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_fc_power(i) } #[pyo3(name = "set_time_dilation")] @@ -175,15 +171,15 @@ use crate::vehicle_thermal::*; /// Arguments /// ------------ /// i: index of time step - pub fn set_time_dilation_py(&mut self, i: usize) -> PyResult<()> { - Ok(self.set_time_dilation(i)?) + pub fn set_time_dilation_py(&mut self, i: usize) -> anyhow::Result<()> { + self.set_time_dilation(i) } #[pyo3(name = "set_post_scalars")] /// Sets scalar variables that can be calculated after a cycle is run. /// This includes mpgge, various energy metrics, and others - pub fn set_post_scalars_py(&mut self) -> PyResult<()> { - Ok(self.set_post_scalars()?) + pub fn set_post_scalars_py(&mut self) -> anyhow::Result<()> { + self.set_post_scalars() } )] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] @@ -259,7 +255,7 @@ impl SimDriveHot { &mut self, init_soc: Option, aux_in_kw_override: Option>, - ) -> Result<(), anyhow::Error> { + ) -> anyhow::Result<()> { self.sd.hev_sim_count = 0; let init_soc = match init_soc { @@ -339,7 +335,7 @@ impl SimDriveHot { self.sd.set_speed_for_target_gap(i); } - pub fn step(&mut self) -> Result<(), anyhow::Error> { + pub fn step(&mut self) -> anyhow::Result<()> { self.set_thermal_calcs(self.sd.i); self.set_misc_calcs(self.sd.i); self.set_comp_lims(self.sd.i)?; @@ -361,8 +357,8 @@ impl SimDriveHot { Ok(()) } - pub fn solve_step(&mut self, i: usize) { - self.sd.solve_step(i).unwrap(); + pub fn solve_step(&mut self, i: usize) -> anyhow::Result<()> { + self.sd.solve_step(i) } pub fn set_thermal_calcs(&mut self, i: usize) { @@ -844,31 +840,31 @@ impl SimDriveHot { self.sd.mps_ach[i - 1] + (self.sd.veh.max_trac_mps2 * self.sd.cyc.dt_s_at_i(i)); } - pub fn set_comp_lims(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_comp_lims(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_comp_lims(i) } - pub fn set_power_calcs(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_power_calcs(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_power_calcs(i) } - pub fn set_ach_speed(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_ach_speed(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_ach_speed(i) } - pub fn set_hybrid_cont_calcs(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_hybrid_cont_calcs(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_hybrid_cont_calcs(i) } - pub fn set_fc_forced_state_rust(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_fc_forced_state_rust(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_fc_forced_state_rust(i) } - pub fn set_hybrid_cont_decisions(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_hybrid_cont_decisions(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_hybrid_cont_decisions(i) } - pub fn set_fc_power(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_fc_power(&mut self, i: usize) -> anyhow::Result<()> { if self.sd.veh.fc_max_kw == 0.0 { self.sd.fc_kw_out_ach[i] = 0.0; } else if self.sd.veh.fc_eff_type == vehicle::H2FC { @@ -984,11 +980,11 @@ impl SimDriveHot { Ok(()) } - pub fn set_time_dilation(&mut self, i: usize) -> Result<(), anyhow::Error> { + pub fn set_time_dilation(&mut self, i: usize) -> anyhow::Result<()> { self.sd.set_time_dilation(i) } - pub fn set_post_scalars(&mut self) -> Result<(), anyhow::Error> { + pub fn set_post_scalars(&mut self) -> anyhow::Result<()> { self.sd.set_post_scalars() } } diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index 94c8a783..4d69395e 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -1,40 +1,31 @@ use crate::imports::*; use std::collections::HashMap; +pub(crate) const ACCEPTED_FILE_FORMATS: [&str; 2] = ["yaml", "json"]; + pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { - /// runs any initialization steps that might be needed + /// Runs any initialization steps that might be needed fn init(&mut self) -> anyhow::Result<()> { Ok(()) } #[allow(clippy::wrong_self_convention)] /// Save current data structure to file. Method adaptively calls serialization methods - /// dependent on the suffix of the file given as str. - /// - /// # Argument: - /// - /// * `filename`: a `str` storing the targeted file name. Currently `.json` and `.yaml` suffixes are - /// supported - /// - /// # Returns: - /// - /// A Rust Result - fn to_file(&self, filename: &str) -> anyhow::Result<()> { - let file = PathBuf::from(filename); - match file + /// dependent on the suffix of the filepath. + fn to_file>(&self, filepath: P) -> anyhow::Result<()> { + let filepath = filepath.as_ref(); + let extension = filepath .extension() - .ok_or_else(|| anyhow!("Unable to parse file extension: {:?}", file))? - .to_str() - .ok_or_else(|| { - anyhow!( - "Unable to convert file extension from `&OsStr` to `&str`: {:?}", - file - ) - })? { - "json" => serde_json::to_writer(&File::create(file)?, self)?, - "yaml" => serde_yaml::to_writer(&File::create(file)?, self)?, - _ => serde_json::to_writer(&File::create(file)?, self)?, - }; + .and_then(OsStr::to_str) + .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?; + match extension.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => serde_yaml::to_writer(&File::create(filepath)?, self)?, + "json" => serde_json::to_writer(&File::create(filepath)?, self)?, + "bin" => bincode::serialize_into(&File::create(filepath)?, self)?, + _ => bail!( + "Unsupported file format {extension:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + } Ok(()) } @@ -44,62 +35,114 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// /// # Argument: /// - /// * `filename`: a `str` storing the targeted file name. Currently `.json` and `.yaml` suffixes are + /// * `filepath`: a `str` storing the targeted file name. Currently `.json` and `.yaml` suffixes are /// supported /// /// # Returns: /// /// A Rust Result wrapping data structure if method is called successfully; otherwise a dynamic /// Error. - fn from_file(filename: &str) -> Result - where - Self: std::marker::Sized, - for<'de> Self: Deserialize<'de>, - { - let extension = Path::new(filename) + fn from_file>(filepath: P) -> anyhow::Result { + let filepath = filepath.as_ref(); + let extension = filepath .extension() .and_then(OsStr::to_str) - .unwrap_or(""); - - let file = File::open(filename)?; + .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?; + let file = File::open(filepath).with_context(|| { + if !filepath.exists() { + format!("File not found: {filepath:?}") + } else { + format!("Could not open file: {filepath:?}") + } + })?; // deserialized file - let mut file_de: Self = match extension { - "yaml" => serde_yaml::from_reader(file)?, - "json" => serde_json::from_reader(file)?, - _ => bail!("Unsupported file extension {}", extension), + let mut deserialized = Self::from_reader(file, extension)?; + deserialized.init()?; + Ok(deserialized) + } + + fn from_resource>(filepath: P) -> anyhow::Result { + let filepath = filepath.as_ref(); + let extension = filepath + .extension() + .and_then(OsStr::to_str) + .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?; + let file = crate::resources::RESOURCES_DIR + .get_file(filepath) + .with_context(|| format!("File not found in resources: {filepath:?}"))?; + let mut deserialized = match extension.trim_start_matches('.').to_lowercase().as_str() { + "bin" => Self::from_bincode(include_dir::File::contents(file))?, + _ => Self::from_str( + include_dir::File::contents_utf8(file) + .with_context(|| format!("File could not be parsed to UTF-8: {filepath:?}"))?, + extension, + )?, }; - file_de.init()?; - Ok(file_de) + deserialized.init()?; + Ok(deserialized) + } + + fn from_reader(rdr: R, format: &str) -> anyhow::Result { + Ok( + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => serde_yaml::from_reader(rdr)?, + "json" => serde_json::from_reader(rdr)?, + "bin" => bincode::deserialize_from(rdr)?, + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + }, + ) + } + + fn to_str(&self, format: &str) -> anyhow::Result { + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => self.to_yaml(), + "json" => self.to_json(), + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + } + } + + fn from_str(contents: &str, format: &str) -> anyhow::Result { + match format.trim_start_matches('.').to_lowercase().as_str() { + "yaml" | "yml" => Self::from_yaml(contents), + "json" => Self::from_json(contents), + _ => bail!( + "Unsupported file format {format:?}, must be one of {ACCEPTED_FILE_FORMATS:?}" + ), + } } - /// json serialization method. - fn to_json(&self) -> String { - serde_json::to_string(&self).unwrap() + /// JSON serialization method + fn to_json(&self) -> anyhow::Result { + Ok(serde_json::to_string(&self)?) } - /// json deserialization method. - fn from_json(json_str: &str) -> Result { + /// JSON deserialization method + fn from_json(json_str: &str) -> anyhow::Result { Ok(serde_json::from_str(json_str)?) } - /// yaml serialization method. - fn to_yaml(&self) -> String { - serde_yaml::to_string(&self).unwrap() + /// YAML serialization method + fn to_yaml(&self) -> anyhow::Result { + Ok(serde_yaml::to_string(&self)?) } - /// yaml deserialization method. - fn from_yaml(yaml_str: &str) -> Result { + /// YAML deserialization method + fn from_yaml(yaml_str: &str) -> anyhow::Result { Ok(serde_yaml::from_str(yaml_str)?) } - /// bincode serialization method. - fn to_bincode(&self) -> Vec { - serialize(&self).unwrap() + /// bincode serialization method + fn to_bincode(&self) -> anyhow::Result> { + Ok(bincode::serialize(&self)?) } - /// bincode deserialization method. - fn from_bincode(encoded: &[u8]) -> Result { - Ok(deserialize(encoded)?) + /// bincode deserialization method + fn from_bincode(encoded: &[u8]) -> anyhow::Result { + Ok(bincode::deserialize(encoded)?) } } diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index 03de4c9a..fab9b912 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -390,7 +390,7 @@ mod tests { use super::*; #[test] - fn test_interp2d() -> anyhow::Result<()> { + fn test_interp2d() { // specified (x, y) point at which to interpolate value let point = [0.5, 0.5]; // grid coordinates: (x0, x1), (y0, y1) @@ -406,12 +406,11 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 0.5); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 0.5); } #[test] - fn test_interp2d_offset() -> anyhow::Result<()> { + fn test_interp2d_offset() { // specified (x, y) point at which to interpolate value let point = [0.25, 0.75]; // grid coordinates: (x0, x1), (y0, y1) @@ -427,12 +426,11 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 0.375); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 0.375); } #[test] - fn test_interp2d_exact_value_lower() -> anyhow::Result<()> { + fn test_interp2d_exact_value_lower() { // specified (x, y) point at which to interpolate value let point = [0.0, 0.0]; // grid coordinates: (x0, x1), (y0, y1) @@ -448,12 +446,11 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 1.0); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 1.0); } #[test] - fn test_interp2d_below_value_lower() -> anyhow::Result<()> { + fn test_interp2d_below_value_lower() { // specified (x, y) point at which to interpolate value let point = [-1.0, -1.0]; // grid coordinates: (x0, x1), (y0, y1) @@ -469,12 +466,11 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 1.0); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 1.0); } #[test] - fn test_interp2d_above_value_upper() -> anyhow::Result<()> { + fn test_interp2d_above_value_upper() { // specified (x, y) point at which to interpolate value let point = [2.0, 2.0]; // grid coordinates: (x0, x1), (y0, y1) @@ -490,12 +486,11 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 1.0); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 1.0); } #[test] - fn test_interp2d_exact_value_upper() -> anyhow::Result<()> { + fn test_interp2d_exact_value_upper() { // specified (x, y) point at which to interpolate value let point = [1.0, 1.0]; // grid coordinates: (x0, x1), (y0, y1) @@ -511,8 +506,7 @@ mod tests { 1.0, // upper right (x1, y1) ], ]; - anyhow::ensure!(interp2d(&point, &grid, &values)? == 1.0); - Ok(()) + assert_eq!(interp2d(&point, &grid, &values).unwrap(), 1.0); } #[test] diff --git a/rust/fastsim-core/src/vehicle.rs b/rust/fastsim-core/src/vehicle.rs index 717ff4bb..60cd27b5 100644 --- a/rust/fastsim-core/src/vehicle.rs +++ b/rust/fastsim-core/src/vehicle.rs @@ -74,13 +74,13 @@ lazy_static! { /// An identify function to allow RustVehicle to be used as a python vehicle and respond to this method /// Returns a clone of the current object - pub fn to_rust(&self) -> PyResult { - Ok(self.clone()) + pub fn to_rust(&self) -> Self { + self.clone() } - #[classmethod] + #[staticmethod] #[pyo3(name = "mock_vehicle")] - fn mock_vehicle_py(_cls: &PyType) -> Self { + fn mock_vehicle_py() -> Self { Self::mock_vehicle() } )] @@ -673,12 +673,9 @@ impl RustVehicle { /// - `fs_mass_kg` /// - `veh_kg` /// - `max_trac_mps2` - pub fn set_derived(&mut self) -> Result<(), anyhow::Error> { + pub fn set_derived(&mut self) -> anyhow::Result<()> { // Vehicle input validation - match self.validate() { - Ok(_) => (), - Err(e) => bail!(e), - }; + self.validate()?; if self.scenario_name != "Template Vehicle for setting up data types" { if self.veh_pt_type == BEV { @@ -833,16 +830,17 @@ impl RustVehicle { } // check that efficiencies are not violating the first law of thermo - assert!( + // TODO: this could perhaps be done in the input validators + ensure!( arrmin(&self.fc_eff_array) >= 0.0, - "min MC eff < 0 is not allowed" + "minimum FC efficiency < 0 is not allowed" ); - assert!(self.fc_peak_eff() < 1.0, "fcPeakEff >= 1 is not allowed."); - assert!( + ensure!(self.fc_peak_eff() < 1.0, "fc_peak_eff >= 1 is not allowed"); + ensure!( arrmin(&self.mc_full_eff_array) >= 0.0, - "min MC eff < 0 is not allowed" + "minimum MC efficiency < 0 is not allowed" ); - assert!(self.mc_peak_eff() < 1.0, "mcPeakEff >= 1 is not allowed."); + ensure!(self.mc_peak_eff() < 1.0, "mc_peak_eff >= 1 is not allowed"); self.set_veh_mass(); @@ -972,18 +970,11 @@ impl RustVehicle { v.set_derived().unwrap(); v } - - pub fn from_json_str(filename: &str) -> Result { - let mut veh_res: Result = Ok(serde_json::from_str(filename)?); - veh_res.as_mut().unwrap().set_derived()?; - veh_res - } } impl SerdeAPI for RustVehicle { fn init(&mut self) -> anyhow::Result<()> { - self.set_derived()?; - Ok(()) + self.set_derived() } } diff --git a/rust/fastsim-core/src/vehicle_thermal.rs b/rust/fastsim-core/src/vehicle_thermal.rs index 314ef6f8..bc40adac 100644 --- a/rust/fastsim-core/src/vehicle_thermal.rs +++ b/rust/fastsim-core/src/vehicle_thermal.rs @@ -90,10 +90,10 @@ impl Default for FcTempEffModelExponential { /// Struct containing parameters and one time-varying variable for HVAC model #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, HistoryVec)] #[add_pyo3_api( - #[classmethod] + #[staticmethod] #[pyo3(name = "default")] - pub fn default_py(_cls: &PyType) -> PyResult { - Ok(Self::default()) + pub fn default_py() -> Self { + Self::default() } )] pub struct HVACModel { @@ -193,29 +193,29 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { #[allow(non_snake_case)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[add_pyo3_api( - #[classmethod] + #[staticmethod] #[pyo3(name = "default")] - pub fn default_py(_cls: &PyType) -> Self { + pub fn default_py() -> Self { Default::default() } pub fn set_cabin_hvac_model_internal( &mut self, hvac_model: HVACModel - ) -> PyResult<()>{ - Ok(check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::Internal(hvac_model))?) + ) -> anyhow::Result<()>{ + check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::Internal(hvac_model)) } - pub fn get_cabin_model_internal(&self, ) -> PyResult { + pub fn get_cabin_model_internal(&self, ) -> anyhow::Result { if let CabinHvacModelTypes::Internal(hvac_model) = &self.cabin_hvac_model { Ok(hvac_model.clone()) } else { - Err(PyAttributeError::new_err("HvacModelTypes::External variant currently used.")) + bail!(PyAttributeError::new_err("HvacModelTypes::External variant currently used.")) } } - pub fn set_cabin_hvac_model_external(&mut self, ) -> PyResult<()> { - Ok(check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::External)?) + pub fn set_cabin_hvac_model_external(&mut self) -> anyhow::Result<()> { + check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::External) } pub fn set_fc_model_internal_exponential( @@ -232,7 +232,7 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { _ => bail!("Invalid option for fc_temp_eff_component.") }; - Ok(check_orphaned_and_set!( + check_orphaned_and_set!( self, fc_model, FcModelTypes::Internal( @@ -240,11 +240,11 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { FcTempEffModelExponential{ offset, lag, minimum }), fc_temp_eff_comp ) - )?) + ) } #[setter] - pub fn set_fc_exp_offset(&mut self, new_offset: f64) -> PyResult<()> { + pub fn set_fc_exp_offset(&mut self, new_offset: f64) -> anyhow::Result<()> { if !self.orphaned { self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model { // If model is internal @@ -267,12 +267,12 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { }; Ok(()) } else { - Err(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) } } #[setter] - pub fn set_fc_exp_lag(&mut self, new_lag: f64) -> PyResult<()>{ + pub fn set_fc_exp_lag(&mut self, new_lag: f64) -> anyhow::Result<()>{ if !self.orphaned { self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model { // If model is internal @@ -295,12 +295,12 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { }; Ok(()) } else { - Err(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) } } #[setter] - pub fn set_fc_exp_minimum(&mut self, new_minimum: f64) -> PyResult<()> { + pub fn set_fc_exp_minimum(&mut self, new_minimum: f64) -> anyhow::Result<()> { if !self.orphaned { self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model { // If model is internal @@ -323,34 +323,34 @@ pub fn get_sphere_conv_params(re: f64) -> (f64, f64) { }; Ok(()) } else { - Err(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) + bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR)) } } #[getter] - pub fn get_fc_exp_offset(&mut self) -> PyResult { + pub fn get_fc_exp_offset(&mut self) -> anyhow::Result { if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ offset, ..}), ..) = &self.fc_model { Ok(*offset) } else { - Err(PyAttributeError::new_err("fc_model is not Exponential")) + bail!(PyAttributeError::new_err("fc_model is not Exponential")) } } #[getter] - pub fn get_fc_exp_lag(&mut self) -> PyResult { + pub fn get_fc_exp_lag(&mut self) -> anyhow::Result { if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ lag, ..}), ..) = &self.fc_model { Ok(*lag) } else { - Err(PyAttributeError::new_err("fc_model is not Exponential")) + bail!(PyAttributeError::new_err("fc_model is not Exponential")) } } #[getter] - pub fn get_fc_exp_minimum(&mut self) -> PyResult { + pub fn get_fc_exp_minimum(&mut self) -> anyhow::Result { if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ minimum, ..}), ..) = &self.fc_model { Ok(*minimum) } else { - Err(PyAttributeError::new_err("fc_model is not Exponential")) + bail!(PyAttributeError::new_err("fc_model is not Exponential")) } }