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"))
}
}