Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/rust-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- name: Run unit tests
run: cargo test --workspace --manifest-path=rust/Cargo.toml -- --skip test_e2e

- name: Publish bambam-core
run: cargo publish -p bambam-core --manifest-path=rust/Cargo.toml --token "${{ secrets.CRATES_IO_TOKEN }}"

- name: Publish bambam-osm
run: cargo publish -p bambam-osm --manifest-path=rust/Cargo.toml --token "${{ secrets.CRATES_IO_TOKEN }}"

Expand Down
1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"bambam-omf",
"bambam-osm",
"bambam-py",
"bambam-core"
]

[workspace.dependencies]
Expand Down
36 changes: 36 additions & 0 deletions rust/bambam-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "bambam-core"
version = "0.2.3"
edition = "2021"
license = "BSD-3-Clause"
exclude = ["test", "**/.DS_Store", "target/"]
readme = "README.md"
repository = "https://github.com/NREL/bambam"
documentation = "https://docs.rs/bambam"
description = "The Behavior and Advanced Mobility Big Access Model"
keywords = [
"nrel",
"access-model",
"accessibility",
"multimodal",
"transit",
]
categories = ["command-line-utilities", "science", "science::geo"]

[dependencies]
itertools = { workspace = true }
routee-compass = { workspace = true }
routee-compass-core = { workspace = true }
routee-compass-powertrain = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uom = { workspace = true }
wkt = { workspace = true }
geo = { workspace = true }
geo-types = { workspace = true }
hex = { workspace = true }
geo-traits = { workspace = true }
geojson = { workspace = true }
wkb = { workspace = true }
rstar = { workspace = true }
chrono = { workspace = true }
Empty file added rust/bambam-core/README.md
Empty file.
2 changes: 2 additions & 0 deletions rust/bambam-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod model;
pub mod util;
326 changes: 326 additions & 0 deletions rust/bambam-core/src/model/bambam_field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
//! Fields and types assigned to the JSON output during bambam runs.
//!
//! # Examples
//!
//! ### Aggregate Data Rows
//!
//! ```json
//! {
//! "opportunity_format": "aggregate",
//! "opportunity_totals": {},
//! "activity_types": [],
//! "info": {
//! "opportunity_runtime": "hh:mm:ss",
//! "mep_runtime": "hh:mm:ss",
//! "tree_size": 0,
//! }
//! "bin": {
//! 10: {
//! "isochrone": {},
//! "opportunities" {},
//! "mep": {},
//! "info": {
//! "time_bin": { .. },
//! "bin_runtime":
//! },
//! }
//! }
//! }
//! ```
//!
//! ### Disaggregate Data Rows
//! ```json
//! {
//! "opportunity_format": "disaggregate",
//! "opportunity_totals": {},
//! "activity_types": [],
//! "opportunities": {
//! "{EdgeListId}-{EdgeId}": {
//! "counts": {},
//! "state": []
//! }
//! }
//! }
//! ```
//!
use crate::model::TimeBin;
use itertools::Itertools;
use routee_compass::plugin::output::OutputPluginError;
use serde::de::DeserializeOwned;
use serde_json::{json, Value};

pub const TIME_BINS: &str = "bin";
pub const TIME_BIN: &str = "time_bin";
pub const INFO: &str = "info";
pub const MODE: &str = "mode";
pub const ISOCHRONE: &str = "isochrone";
pub const ISOCHRONE_FORMAT: &str = "isochrone_format";
pub const TREE_SIZE: &str = "tree_size";
pub const ACTIVITY_TYPES: &str = "activity_types";
pub const OPPORTUNITIES: &str = "opportunities";
pub const OPPORTUNITY_COUNTS: &str = "opportunity_counts";
pub const OPPORTUNITY_ORIENTATION: &str = "opportunity_orientation";
pub const OPPORTUNITY_FORMAT: &str = "opportunity_format";
pub const OPPORTUNITY_TOTALS: &str = "opportunity_totals";
pub const VEHICLE_STATE: &str = "vehicle_state";
pub const OPP_FMT_AGGREGATE: &str = "aggregate";
pub const OPP_FMT_DISAGGREGATE: &str = "disaggregate";
pub const OPPORTUNITY_PLUGIN_RUNTIME: &str = "opportunity_runtime";
pub const OPPORTUNITY_BIN_RUNTIME: &str = "bin_runtime";

pub mod get {
use itertools::Itertools;
use routee_compass::plugin::output::OutputPluginError;
use routee_compass_core::model::{
network::{EdgeId, EdgeListId, VertexId},
state::StateVariable,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::HashMap;

use crate::model::{
bambam_field::as_usize,
output_plugin::{
isochrone::IsochroneOutputFormat,
opportunity::{OpportunityFormat, OpportunityOrientation},
},
};

pub fn mode(value: &Value) -> Result<String, OutputPluginError> {
let path = ["request", super::MODE];
super::get_nested(value, &path).map_err(|e| {
let dot_path = path.join(".");
OutputPluginError::OutputPluginFailed(format!(
"failure retrieving 'mode' value from '{dot_path}': {e}"
))
})
}
pub fn activity_types(value: &Value) -> Result<Vec<String>, OutputPluginError> {
get_from_value(super::ACTIVITY_TYPES, value)
}
pub fn isochrone_format(value: &Value) -> Result<IsochroneOutputFormat, OutputPluginError> {
get_from_value(super::ISOCHRONE_FORMAT, value)
}
pub fn opportunity_format(value: &Value) -> Result<OpportunityFormat, OutputPluginError> {
get_from_value(super::OPPORTUNITY_FORMAT, value)
}
pub fn opportunity_orientation(
value: &Value,
) -> Result<OpportunityOrientation, OutputPluginError> {
get_from_value(super::OPPORTUNITY_ORIENTATION, value)
}
pub fn disaggregate_vertex_id(value: &str) -> Result<VertexId, OutputPluginError> {
let id: usize = super::as_usize(value)?;
Ok(VertexId(id))
}
pub fn disaggregate_edge_id(value: &str) -> Result<(EdgeListId, EdgeId), OutputPluginError> {
match value.split("-").collect_vec()[..] {
[] => Err(OutputPluginError::OutputPluginFailed("disaggregate edge identifier is empty".to_string())),
[edge_list_str, edge_str] => {
let edge_list_id = EdgeListId(as_usize(edge_list_str)?);
let edge_id = EdgeId(as_usize(edge_str)?);
Ok((edge_list_id, edge_id))
},
_ => Err(OutputPluginError::OutputPluginFailed(format!("disaggregate edge identifier is malformed, expected '<EdgeListId>-<EdgeId>', found '{value}'")))
}
}
pub fn totals(value: &Value) -> Result<HashMap<String, f64>, OutputPluginError> {
get_from_value(super::OPPORTUNITY_TOTALS, value)
}
pub fn counts(value: &Value) -> Result<HashMap<String, f64>, OutputPluginError> {
get_from_value(super::OPPORTUNITY_COUNTS, value)
}
pub fn state(value: &Value) -> Result<Vec<StateVariable>, OutputPluginError> {
get_from_value(super::VEHICLE_STATE, value)
}

/// helper for deserializing fields from a JSON value in a deserializable type
fn get_from_value<T>(field: &str, value: &Value) -> Result<T, OutputPluginError>
where
T: DeserializeOwned,
{
let value = value.get(field).ok_or_else(|| {
OutputPluginError::InternalError(format!("cannot find '{field}' in output row"))
})?;
serde_json::from_value(value.clone()).map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"found '{field}' in output row but cannot deserialize due to: {e}"
))
})
}
}

mod set {}

/// gets a deserialized value from a json object at some path. not compatible with json arrays.
pub fn get_nested<T: DeserializeOwned>(json: &Value, path: &[&str]) -> Result<T, String> {
let mut cursor = json;
for k in path {
match cursor.get(k) {
Some(child) => {
cursor = child;
}
None => return Err(nested_error("get", path.to_vec(), k, cursor)),
}
}
let result = serde_json::from_value(cursor.clone())
.map_err(|e| format!("unable to deserialize value '{cursor}': {e}"))?;
Ok(result)
}

/// inserts a json value into a json object at some path, adding any missing parent objects
/// along the way. not compatible with json arrays.
pub fn insert_nested_with_parents(
json: &mut Value,
path: &[&str],
key: &str,
value: Value,
overwrite: bool,
) -> Result<(), String> {
let parents = path.to_vec();
for i in 0..parents.len() {
let key = parents[i];
insert_nested(json, &parents[0..i], key, json![{}], false)?;
}
insert_nested(json, path, key, value, overwrite)
}

/// inserts a json value into a json object at some path. not compatible with json arrays.
pub fn insert_nested(
json: &mut Value,
path: &[&str],
key: &str,
value: Value,
overwrite: bool,
) -> Result<(), String> {
let mut cursor = json;
for k in path {
if cursor.get(k).is_none() {
return Err(nested_error("insert", path.to_vec(), k, cursor));
};
match cursor.get_mut(k) {
Some(child) => {
cursor = child;
}
None => unreachable!("invariant: already None-checked above"),
}
}
let exists = cursor.get(key).is_some();
if exists && !overwrite {
Ok(())
} else {
cursor[key] = value;
Ok(())
}
}

/// assures that the structure exists for a time bin.
///
///
/// with time bin [0, 10]:
///
/// {
/// "bin": {
/// "10": {
/// "info": { "time_bin": { .. } },
/// }
/// }
/// }
pub fn scaffold_time_bin(json: &mut Value, time_bin: &TimeBin) -> Result<(), String> {
if json.get(TIME_BINS).is_none() {
json[TIME_BINS] = json![{}];
}
let time_bin_key = time_bin.key();
insert_nested(json, &[TIME_BINS], &time_bin_key, json![{}], false)?;
insert_nested(
json,
&[TIME_BINS, &time_bin_key],
INFO,
json![{ TIME_BIN: json![time_bin] }],
false,
)?;
Ok(())
}

type TimeBinsIter<'a> = Box<dyn Iterator<Item = Result<(TimeBin, &'a Value), String>> + 'a>;
type TimeBinsIterMut<'a> = Box<dyn Iterator<Item = Result<(TimeBin, &'a mut Value), String>> + 'a>;

pub fn get_time_bins(output: &serde_json::Value) -> Result<Vec<TimeBin>, String> {
let bins_value = output
.get(TIME_BINS)
.ok_or_else(|| field_error(vec![TIME_BINS]))?;
let bins = bins_value
.as_object()
.ok_or_else(|| type_error(vec![TIME_BINS], String::from("JSON object")))?
.values()
.map(|v| get_nested(v, &[INFO, TIME_BIN]))
.collect::<Result<Vec<_>, _>>()?;
Ok(bins)
}

pub fn time_bins_iter(output: &serde_json::Value) -> Result<TimeBinsIter<'_>, String> {
let bins_value = output
.get(TIME_BINS)
.ok_or_else(|| field_error(vec![TIME_BINS]))?;
let bins = bins_value
.as_object()
.ok_or_else(|| type_error(vec![TIME_BINS], String::from("JSON object")))?
.values()
.map(|v| {
let time_bin = get_nested(v, &[INFO, TIME_BIN]);
time_bin.map(|t| (t, v))
});
Ok(Box::new(bins))
}

pub fn time_bins_iter_mut(output: &mut serde_json::Value) -> Result<TimeBinsIterMut<'_>, String> {
let bins_value = output
.get_mut(TIME_BINS)
.ok_or_else(|| field_error(vec![TIME_BINS]))?;
let bins = bins_value
.as_object_mut()
.ok_or_else(|| type_error(vec![TIME_BINS], String::from("JSON object")))?
.values_mut()
.map(move |v| {
let time_bin = get_nested(v, &[INFO, TIME_BIN]);
time_bin.map(|t| (t, v))
});
Ok(Box::new(bins))
}

fn field_error(fields: Vec<&str>) -> String {
let path = fields.join(".");
format!("expected path {path} missing from output object")
}

fn nested_error(action: &str, fields: Vec<&str>, failed_key: &str, object: &Value) -> String {
let path = fields.join(".");
let keylist = object
.as_object()
.map(|o| o.keys().collect_vec())
.unwrap_or_default();
let keys = if keylist.len() > 5 {
let inner = keylist.iter().take(5).join(", ");
format!("[{inner}, ...]")
} else {
let inner = keylist.iter().join(", ");
format!("[{inner}]")
};
format!(
"during {action}, expected path '{path}' missing key '{failed_key}' from JSON object available sibling keys: {keys}"
)
}

fn type_error(fields: Vec<&str>, expected_type: String) -> String {
let path = fields.join(".");
format!("expected value at path {path} to be {expected_type}")
}

fn as_usize(value: &str) -> Result<usize, OutputPluginError> {
value.parse().map_err(|e| {
OutputPluginError::OutputPluginFailed(format!(
"unable to read oppportunity key '{value}' as a numeric value: {e}"
))
})
}
Loading
Loading