use powerio_core::{HistoryEntry, HistoryId, HistoryKind, PioModule};
use crate::BalancedNetwork;
use crate::codes;
use crate::value::PioValue;
type Error = powerio_core::Error;
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq)]
pub struct TimePointEntry {
pub position: usize,
pub label: String,
pub duration: Option<std::time::Duration>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ScenarioEntry {
pub id: String,
pub probability: Option<f64>,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum StateInventory {
TimePoints(Vec<TimePointEntry>),
Scenarios(Vec<ScenarioEntry>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum StateSelector<'a> {
TimePosition(usize),
Scenario(&'a str),
}
impl std::fmt::Display for StateSelector<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TimePosition(position) => write!(formatter, "time position {position}"),
Self::Scenario(id) => write!(formatter, "scenario `{id}`"),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum SelectedState<'value> {
BalancedNetwork(&'value BalancedNetwork),
BalancedOperatingPoint(&'value powerio_prob::OperatingPoint<BalancedNetwork>),
MulticonductorOperatingPoint(
&'value powerio_prob::OperatingPoint<powerio_dist::MulticonductorNetwork>,
),
}
pub fn state_inventory(value: &PioValue) -> Result<StateInventory> {
match value {
PioValue::BalancedNetworkTimeSeries(series) => Ok(StateInventory::TimePoints(
time_entries(series.time_points()),
)),
PioValue::BalancedOperatingPointTimeSeries(series) => Ok(StateInventory::TimePoints(
time_entries(series.time_points()),
)),
PioValue::MulticonductorOperatingPointTimeSeries(series) => Ok(StateInventory::TimePoints(
time_entries(series.time_points()),
)),
PioValue::BalancedNetworkScenarioSet(set) => Ok(StateInventory::Scenarios(
set.iter()
.map(|scenario| ScenarioEntry {
id: scenario.id().as_str().to_owned(),
probability: scenario.probability(),
})
.collect(),
)),
other => Err(not_a_collection(other)),
}
}
pub fn select_state<'value>(
value: &'value PioValue,
selector: StateSelector<'_>,
) -> Result<SelectedState<'value>> {
match (value, selector) {
(PioValue::BalancedNetworkTimeSeries(series), StateSelector::TimePosition(position)) => {
let item = series
.value(position)
.ok_or_else(|| out_of_range(position, series.len()))?;
Ok(SelectedState::BalancedNetwork(item))
}
(
PioValue::BalancedOperatingPointTimeSeries(series),
StateSelector::TimePosition(position),
) => {
let item = series
.value(position)
.ok_or_else(|| out_of_range(position, series.len()))?;
Ok(SelectedState::BalancedOperatingPoint(item))
}
(
PioValue::MulticonductorOperatingPointTimeSeries(series),
StateSelector::TimePosition(position),
) => {
let item = series
.value(position)
.ok_or_else(|| out_of_range(position, series.len()))?;
Ok(SelectedState::MulticonductorOperatingPoint(item))
}
(PioValue::BalancedNetworkScenarioSet(set), StateSelector::Scenario(id)) => {
let scenario = set.get(id).ok_or_else(|| unknown_scenario(id, set))?;
Ok(SelectedState::BalancedNetwork(scenario.value()))
}
(
PioValue::BalancedNetworkTimeSeries(_)
| PioValue::BalancedOperatingPointTimeSeries(_)
| PioValue::MulticonductorOperatingPointTimeSeries(_),
StateSelector::Scenario(_),
)
| (PioValue::BalancedNetworkScenarioSet(_), StateSelector::TimePosition(_)) => {
Err(Error::new(
&codes::REQUEST_STATE_WRONG_SELECTOR,
format!(
"a {} value keys by {}; the request named {selector}",
value.kind().as_str(),
match value {
PioValue::BalancedNetworkScenarioSet(_) => "scenario ID",
_ => "time position",
}
),
))
}
(other, _) => Err(not_a_collection(other)),
}
}
pub fn export_state(value: &PioValue, selector: StateSelector<'_>) -> Result<PioModule<PioValue>> {
let network = match select_state(value, selector)? {
SelectedState::BalancedNetwork(network) => network.clone(),
SelectedState::BalancedOperatingPoint(point) => point.materialize_network()?,
SelectedState::MulticonductorOperatingPoint(_) => {
return Err(Error::new(
&codes::REQUEST_STATE_UNBOUND_EXPORT,
"a multiconductor operating point selects and reads in place; its static \
materialization is not bound yet"
.to_string(),
));
}
};
let mut module = PioModule::new(PioValue::BalancedNetwork(network));
let entry = HistoryId::new("export-selected-state")
.and_then(|id| HistoryEntry::new(id, HistoryKind::Transform, "export_selected_state"))
.and_then(|entry| {
entry.with_assumption(format!(
"static export of {selector} from a {} value",
value.kind().as_str()
))
})?;
module.add_history_entry(entry)?;
Ok(module)
}
fn time_entries(points: &[powerio_core::TimePoint]) -> Vec<TimePointEntry> {
points
.iter()
.enumerate()
.map(|(position, point)| TimePointEntry {
position,
label: point.label().to_owned(),
duration: point.duration(),
})
.collect()
}
fn not_a_collection(value: &PioValue) -> Error {
Error::new(
&codes::REQUEST_STATE_NOT_A_COLLECTION,
format!(
"a {} value carries no time or scenario collection to select from; \
a static value is used directly, never selected implicitly",
value.kind().as_str()
),
)
}
fn out_of_range(position: usize, len: usize) -> Error {
Error::new(
&codes::REQUEST_STATE_OUT_OF_RANGE,
format!("time position {position} is outside the {len} point axis"),
)
}
fn unknown_scenario(id: &str, set: &powerio_core::ScenarioSet<BalancedNetwork>) -> Error {
let mut known: Vec<&str> = set.iter().map(|scenario| scenario.id().as_str()).collect();
known.sort_unstable();
Error::new(
&codes::REQUEST_STATE_UNKNOWN_SCENARIO,
format!(
"scenario `{id}` is not in the set; the set declares {}",
known.join(", ")
),
)
}