Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/grade checks #80

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion build_and_test.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# assumes a python environment has been created and activated
echo "Testing rust" && \
cd rust/ && cargo test --workspace && cd .. && \
cd rust/ && \
cargo test --workspace && \
cd - && \
echo "Building python API" && \
pip install -qe ".[dev]" && \
echo "Running python tests" && \
Expand Down
16 changes: 10 additions & 6 deletions python/altrios/altrios_pyo3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ from dataclasses import dataclass

class SerdeAPI(object):
@classmethod
def from_bincode(cls) -> Self: ...
def from_bincode(cls, bincode_str: str) -> Self: ...
@classmethod
def from_json(cls) -> Self: ...
def from_json(cls, json_str: str) -> Self: ...
@classmethod
def from_yaml(cls) -> Self: ...
def from_yaml(cls, yaml_str: str) -> Self: ...
@classmethod
def from_file(cls, filename: Path) -> Self: ...
@classmethod
def from_file(cls) -> Self: ...
def to_file(self): ...
def from_str(cls, contents: str, format: str) -> Self: ...
def to_str(self, format:str) -> str: ...
def to_file(self, filename: Path): ...
def to_bincode(self) -> bytes: ...
def to_json(self) -> str: ...
def to_yaml(self) -> str: ...
Expand Down Expand Up @@ -1060,7 +1063,8 @@ class Network(SerdeAPI):
def __len__(self) -> Any: ...
def __setitem__(self, index, object) -> Any: ...
def set_speed_set_for_train_type(self, train_type: TrainType): ...

@classmethod
def from_file_unchecked(cls, filename: Path) -> Self: ...

@dataclass
class LinkPath(SerdeAPI):
Expand Down
2 changes: 1 addition & 1 deletion python/altrios/train_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import altrios as alt
from altrios import defaults, utilities

pl.enable_string_cache()
pl.enable_string_cache(True)

class TrainPlannerConfig:
def __init__(self,
Expand Down
2 changes: 1 addition & 1 deletion rust/altrios-core/src/combo_error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt::{Debug, Display};
use std::ops::{Deref, DerefMut};

///Define a better trait bound for all error types!
/// Define a better trait bound for all error types!
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ComboError<E: Display> {
layer: usize,
Expand Down
11 changes: 7 additions & 4 deletions rust/altrios-core/src/consist/locomotive/locomotive_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ impl Locomotive {
}
}

pub fn set_fuel_converter(&mut self, fc: FuelConverter) -> Result<()> {
pub fn set_fuel_converter(&mut self, fc: FuelConverter) -> anyhow::Result<()> {
match &mut self.loco_type {
PowertrainType::ConventionalLoco(loco) => {
loco.fc = fc;
Expand Down Expand Up @@ -900,7 +900,7 @@ impl Locomotive {
}
}

pub fn set_generator(&mut self, gen: Generator) -> Result<()> {
pub fn set_generator(&mut self, gen: Generator) -> anyhow::Result<()> {
match &mut self.loco_type {
PowertrainType::ConventionalLoco(loco) => {
loco.gen = gen;
Expand Down Expand Up @@ -933,7 +933,10 @@ impl Locomotive {
}
}

pub fn set_reversible_energy_storage(&mut self, res: ReversibleEnergyStorage) -> Result<()> {
pub fn set_reversible_energy_storage(
&mut self,
res: ReversibleEnergyStorage,
) -> anyhow::Result<()> {
match &mut self.loco_type {
PowertrainType::ConventionalLoco(_) => {
bail!("Conventional has no ReversibleEnergyStorage.")
Expand Down Expand Up @@ -968,7 +971,7 @@ impl Locomotive {
}
}

pub fn set_electric_drivetrain(&mut self, edrv: ElectricDrivetrain) -> Result<()> {
pub fn set_electric_drivetrain(&mut self, edrv: ElectricDrivetrain) -> anyhow::Result<()> {
match &mut self.loco_type {
PowertrainType::ConventionalLoco(loco) => {
loco.edrv = edrv;
Expand Down
2 changes: 0 additions & 2 deletions rust/altrios-core/src/consist/locomotive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,3 @@ use crate::consist::locomotive::powertrain::electric_drivetrain::ElectricDrivetr
use crate::consist::locomotive::powertrain::fuel_converter::FuelConverter;
use crate::consist::locomotive::powertrain::generator::Generator;
use crate::consist::locomotive::powertrain::reversible_energy_storage::ReversibleEnergyStorage;

use anyhow::Result;
2 changes: 1 addition & 1 deletion rust/altrios-core/src/meet_pass/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ mod test_dispatch {
fn test_simple_dispatch() {
let network_file_path = project_root::get_project_root()
.unwrap()
.join("../python/altrios/resources/networks/Taconite.yaml");
.join("../python/altrios/resources/networks/Taconite-NoBalloon.yaml");
let network = Network::from_file(network_file_path).unwrap();

let train_sims = vec![
Expand Down
4 changes: 2 additions & 2 deletions rust/altrios-core/src/meet_pass/train_disp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ mod test_train_disp {
fn test_make_train_fwd() {
let network_file_path = project_root::get_project_root()
.unwrap()
.join("../python/altrios/resources/networks/Taconite.yaml");
.join("../python/altrios/resources/networks/Taconite-NoBalloon.yaml");
let network = Network::from_file(network_file_path).unwrap();

let speed_limit_train_sim = crate::train::speed_limit_train_sim_fwd();
Expand All @@ -245,7 +245,7 @@ mod test_train_disp {
// TODO: Make this test depend on a better file
let network_file_path = project_root::get_project_root()
.unwrap()
.join("../python/altrios/resources/networks/Taconite.yaml");
.join("../python/altrios/resources/networks/Taconite-NoBalloon.yaml");
let network = Network::from_file(network_file_path).unwrap();

let speed_limit_train_sim = crate::train::speed_limit_train_sim_rev();
Expand Down
88 changes: 83 additions & 5 deletions rust/altrios-core/src/track/link/link_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ pub struct Link {
/// occupied at a given time. For further explanation, see the [graphical
/// example](https://nrel.github.io/altrios/api-doc/rail-network.html?highlight=network#link-lockout).
pub link_idxs_lockout: Vec<LinkIdx>,
/// Maximum absolute grade that will not trigger an error
#[serde(default = "max_allowed_grade_default")]
pub max_allowed_grade: si::Ratio,
}

fn max_allowed_grade_default() -> si::Ratio {
uc::R * 0.06
}

impl Link {
Expand Down Expand Up @@ -111,6 +118,7 @@ impl From<LinkOld> for Link {
idx_flip: l.idx_flip,
osm_id: l.osm_id,
link_idxs_lockout: l.link_idxs_lockout,
max_allowed_grade: max_allowed_grade_default(),
}
}
}
Expand Down Expand Up @@ -223,30 +231,57 @@ impl ObjState for Link {
));
}

// verify that first offset is zero
// verify that first elevation offset is zero
if self.elevs.first().unwrap().offset != si::Length::ZERO {
errors.push(anyhow!(
"First elevation offset = {:?} is invalid, must equal zero!",
self.elevs.first().unwrap().offset
));
}
// verify that last offset is equal to length
// verify that last elevation offset is equal to length
if self.elevs.last().unwrap().offset != self.length {
errors.push(anyhow!(
"Last elevation offset = {:?} is invalid, must equal length = {:?}!",
self.elevs.last().unwrap().offset,
self.length
));
}

// verify that grade does not exceed `self.max_allowed_grade`
let grades = self
.elevs
.windows(2)
.map(|elevs| {
let (curr, prev) = (elevs[1], elevs[0]);
(curr.elev - prev.elev) / (curr.offset - prev.offset)
})
.collect::<Vec<si::Ratio>>();
if grades
.iter()
.any(|&g| g.abs() >= uc::R * self.max_allowed_grade)
{
grades.iter().zip(&self.elevs).for_each(|(g, e)| {
if g.abs() >= uc::R * 0.05 {
errors.push(anyhow!(
"{}\nGrade {}% at offset {} m exceeds absolute error threshold of {}%",
format_dbg!(),
g.get::<si::ratio>() * 100.,
e.offset.get::<si::meter>(),
self.max_allowed_grade.get::<si::ratio>() * 100.
))
}
});
}

if !self.headings.is_empty() {
// verify that first offset is zero
// verify that first heading offset is zero
if self.headings.first().unwrap().offset != si::Length::ZERO {
errors.push(anyhow!(
"First heading offset = {:?} is invalid, must equal zero!",
self.headings.first().unwrap().offset
));
}
// verify that last offset is equal to length
// verify that last heading offset is equal to length
if self.headings.last().unwrap().offset != self.length {
errors.push(anyhow!(
"Last heading offset = {:?} is invalid, must equal length = {:?}!",
Expand Down Expand Up @@ -282,6 +317,12 @@ impl ObjState for Link {
fn set_speed_set_for_train_type_py(&mut self, train_type: TrainType) -> PyResult<()> {
Ok(self.set_speed_set_for_train_type(train_type)?)
}

#[staticmethod]
#[pyo3(name = "from_file_unchecked")]
pub fn from_file_unchecked_py(filepath: &PyAny) -> anyhow::Result<Self> {
Self::from_file_unchecked(PathBuf::extract(filepath)?)
}
)]
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
/// Struct that contains a `Vec<Link>` for the purpose of providing `SerdeAPI` for `Vec<Link>` in
Expand All @@ -298,6 +339,33 @@ impl Network {
}
Ok(())
}

/// Loads `Network` from file and does not perform any validation checks
fn from_file_unchecked<P: AsRef<Path>>(filepath: P) -> anyhow::Result<Self> {
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 = File::open(filepath).with_context(|| {
if !filepath.exists() {
format!("File not found: {filepath:?}")
} else {
format!("Could not open file: {filepath:?}")
}
})?;
let network = match Self::from_reader(file, extension) {
Ok(network) => network,
Err(err) => NetworkOld::from_file(filepath)
.map_err(|old_err| {
anyhow!("\nattempting to load as `Network`:\n{}\nattempting to load as `NetworkOld`:\n{}", err, old_err)
})?
.into(),
};
// network.init()?; -- this has been commented to illustrate the difference w.r.t. `from_file`

Ok(network)
}
}

impl ObjState for Network {
Expand Down Expand Up @@ -388,6 +456,16 @@ impl ObjState for [Link] {
early_err!(errors, "Links");

for (idx, link) in self.iter().enumerate().skip(1) {
match link.validate() {
ValidationResults::Ok(_) => {}
ValidationResults::Err(e) => errors.push(
Err(anyhow!(e))
.with_context(|| anyhow!("{}\nlink: {}", format_dbg!(), link.idx_curr))
// `unwrap` should always be safe here
.unwrap(),
),
}

// Validate flip and curr
if link.idx_curr.idx() != idx {
errors.push(anyhow!(
Expand Down Expand Up @@ -625,7 +703,7 @@ mod tests {
fn test_set_speed_set_from_train_type() {
let network_file_path = project_root::get_project_root()
.unwrap()
.join("../python/altrios/resources/networks/Taconite.yaml");
.join("../python/altrios/resources/networks/Taconite-NoBalloon.yaml");
let network_speed_sets = Network::from_file(network_file_path).unwrap();
let mut network_speed_set = network_speed_sets.clone();
network_speed_set
Expand Down
Loading