use std::collections::HashMap;
use crate::network::{BalancedNetwork, BusId};
use super::lexer::{LexedLine, LineKind, lex};
use super::resolve::UnresolvedReason;
use super::sub::Subsystem;
use super::{ContingencyAction, ContingencySet, parse_action};
const KV_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DispatchLevel {
Default,
Up,
Down,
FirstLevel,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DispatchEntry {
Subsystem { name: String, share: Option<f64> },
Bus { bus: BusId, share: Option<f64> },
ParticipatingMachines,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct DispatchBlock {
pub entries: Vec<DispatchEntry>,
pub unread: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct DefaultDispatch {
pub level: DispatchLevel,
pub block: DispatchBlock,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct DispatchedAction {
pub action: ContingencyAction,
pub block: DispatchBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaledQuantity {
Generation,
Load,
Import,
Export,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum SubsystemDispatchRule {
Participate { include_offline: bool },
Scale {
quantity: ScaledQuantity,
pmax_greater_mw: Option<f64>,
include_offline: bool,
include_nonconforming: bool,
},
Baseload { code: i64 },
TurbineType { code: i64 },
Except { words: Vec<String> },
}
impl ContingencySet {
#[must_use]
pub fn calc_default_dispatch(&self) -> Vec<DefaultDispatch> {
self.retained
.iter()
.filter(|statement| !statement.after_end)
.filter_map(|statement| {
let lines = lex(&statement.text);
let (head, body) = split_block(&lines)?;
let upper: Vec<String> = head.keywords();
if upper.len() > 3
|| upper[0] != "DEFAULT"
|| upper.get(1).map(String::as_str) != Some("DISPATCH")
{
return None;
}
let level = match upper.get(2).map(String::as_str) {
None => DispatchLevel::Default,
Some("UP") => DispatchLevel::Up,
Some("DOWN") => DispatchLevel::Down,
Some("FIRSTLEVEL") => DispatchLevel::FirstLevel,
Some(_) => return None,
};
Some(DefaultDispatch {
level,
block: read_block(body),
line: statement.line,
})
})
.collect()
}
}
impl ContingencyAction {
#[must_use]
pub fn calc_dispatched_action(&self) -> Option<DispatchedAction> {
let Self::Unrecognized { text } = self else {
return None;
};
let lines = lex(text);
let (head, body) = split_block(&lines)?;
let upper = head.keywords();
if upper.last().map(String::as_str) != Some("DISPATCH") {
return None;
}
let words = head.words();
let n = upper.len() - 1;
let action = parse_action(&upper[..n], &words[..n])?;
Some(DispatchedAction {
action,
block: read_block(body),
})
}
}
impl Subsystem {
#[must_use]
pub fn calc_dispatch_rules(&self) -> Vec<SubsystemDispatchRule> {
self.retained
.iter()
.chain(self.groups.iter().flat_map(|group| group.retained.iter()))
.filter_map(|statement| {
let lines = lex(&statement.text);
let line = lines.iter().find(|l| l.kind == LineKind::Statement)?;
parse_subsystem_rule(&line.keywords(), &line.words())
})
.collect()
}
}
fn split_block<'a, 'b>(
lines: &'b [LexedLine<'a>],
) -> Option<(&'b LexedLine<'a>, &'b [LexedLine<'a>])> {
let statements: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.kind == LineKind::Statement)
.map(|(index, _)| index)
.collect();
let (&first, rest) = statements.split_first()?;
let last = rest.last().copied().filter(|&index| {
let tokens = &lines[index].tokens;
tokens.len() == 1 && tokens[0].text.eq_ignore_ascii_case("END")
});
let end = last.unwrap_or(lines.len());
Some((&lines[first], &lines[first + 1..end]))
}
fn read_block(lines: &[LexedLine<'_>]) -> DispatchBlock {
let mut block = DispatchBlock::default();
for line in lines.iter().filter(|l| l.kind == LineKind::Statement) {
let upper = line.keywords();
let words = line.words();
let share = |at: usize| -> Option<Option<f64>> {
match words.get(at) {
None => Some(None),
Some(word) => word.parse::<f64>().ok().filter(|v| v.is_finite()).map(Some),
}
};
let entry = match upper[0].as_str() {
"SUBSYSTEM" | "SYSTEM" if words.len() <= 3 => {
words
.get(1)
.zip(share(2))
.map(|(name, share)| DispatchEntry::Subsystem {
name: name.trim().to_owned(),
share,
})
}
"BUS" if words.len() <= 3 => words
.get(1)
.and_then(|bus| bus.parse::<usize>().ok())
.zip(share(2))
.map(|(bus, share)| DispatchEntry::Bus {
bus: BusId(bus),
share,
}),
"PARTICIPATING" if upper.len() == 2 && upper[1] == "MACHINES" => {
Some(DispatchEntry::ParticipatingMachines)
}
_ => None,
};
match entry {
Some(entry) => block.entries.push(entry),
None => block.unread.push(line.trimmed().to_owned()),
}
}
block
}
fn parse_subsystem_rule(upper: &[String], words: &[&str]) -> Option<SubsystemDispatchRule> {
let include = |at: usize, what: &str| {
upper.get(at).is_some_and(|w| w == "INCLUDE")
&& upper.get(at + 1).is_some_and(|w| w == what)
};
match upper.first()?.as_str() {
"PARTICIPATE" => match upper.len() {
1 => Some(SubsystemDispatchRule::Participate {
include_offline: false,
}),
3 if include(1, "OFFLINE") => Some(SubsystemDispatchRule::Participate {
include_offline: true,
}),
_ => None,
},
"BASELOAD" | "TURBINETYPE" if upper.len() == 2 => {
let code = upper[1].parse::<i64>().ok()?;
Some(if upper[0] == "BASELOAD" {
SubsystemDispatchRule::Baseload { code }
} else {
SubsystemDispatchRule::TurbineType { code }
})
}
"EXCEPT" => Some(SubsystemDispatchRule::Except {
words: words[1..].iter().map(|w| (*w).to_owned()).collect(),
}),
"SCALE" => parse_scale(upper),
_ => None,
}
}
fn parse_scale(upper: &[String]) -> Option<SubsystemDispatchRule> {
if upper.get(1)? != "ALL" {
return None;
}
let (quantity, mut at) = match upper.get(2)?.as_str() {
"GENERATION" => (ScaledQuantity::Generation, 3),
"LOAD" | "LOAD_ID" => (ScaledQuantity::Load, 3),
"FOR" => match upper.get(3)?.as_str() {
"IMPORT" => (ScaledQuantity::Import, 4),
"EXPORT" => (ScaledQuantity::Export, 4),
_ => return None,
},
_ => return None,
};
let mut pmax_greater_mw = None;
let mut include_offline = false;
let mut include_nonconforming = false;
while at < upper.len() {
match upper[at].as_str() {
"WITH"
if upper.get(at + 1).is_some_and(|w| w == "PMAX")
&& upper.get(at + 2).is_some_and(|w| w == "GREATER")
&& upper.get(at + 4).is_some_and(|w| w == "MW") =>
{
pmax_greater_mw = Some(
upper
.get(at + 3)?
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())?,
);
at += 5;
}
"INCLUDE" if upper.get(at + 1).is_some_and(|w| w == "OFFLINE") => {
include_offline = true;
at += 2;
}
"INCLUDE" if upper.get(at + 1).is_some_and(|w| w == "NONCONFORMING") => {
include_nonconforming = true;
at += 2;
}
_ => return None,
}
}
Some(SubsystemDispatchRule::Scale {
quantity,
pmax_greater_mw,
include_offline,
include_nonconforming,
})
}
fn normalize_name(name: &str) -> String {
name.split_whitespace()
.map(str::to_ascii_uppercase)
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Debug, Clone, Default)]
pub(super) struct NameIndex {
buses: HashMap<String, Vec<(String, f64, BusId)>>,
branches: HashMap<String, Vec<(String, usize)>>,
}
impl NameIndex {
pub(super) fn new(net: &BalancedNetwork) -> Self {
let mut index = Self::default();
for bus in net.buses() {
if let Some(name) = bus.name.as_deref().map(str::trim).filter(|n| !n.is_empty()) {
index.buses.entry(normalize_name(name)).or_default().push((
name.to_owned(),
bus.base_kv,
bus.id,
));
}
}
for (row, branch) in net.branches().iter().enumerate() {
if let Some(name) = branch
.name
.as_deref()
.map(str::trim)
.filter(|n| !n.is_empty())
{
index
.branches
.entry(normalize_name(name))
.or_default()
.push((name.to_owned(), row));
}
}
index
}
fn bus(&self, token: &str) -> Result<BusId, UnresolvedReason> {
let token = token.trim();
let (name, kv) = match token.rsplit_once(char::is_whitespace) {
Some((name, kv)) => match kv.parse::<f64>() {
Ok(kv) if kv.is_finite() => (name.trim(), Some(kv)),
_ => (token, None),
},
None => (token, None),
};
let candidates: Vec<&(String, f64, BusId)> = self
.buses
.get(&normalize_name(name))
.into_iter()
.flatten()
.filter(|(_, base, _)| kv.is_none_or(|kv| (base - kv).abs() <= KV_TOLERANCE))
.collect();
let exact: Vec<BusId> = candidates
.iter()
.filter(|(stated, ..)| stated == name)
.map(|(.., bus)| *bus)
.collect();
let all: Vec<BusId> = candidates.iter().map(|(.., bus)| *bus).collect();
pick(&exact, &all).map_err(|matches| match matches {
0 => UnresolvedReason::NoSuchBusName,
matches => UnresolvedReason::AmbiguousBusName { matches },
})
}
fn branch(&self, name: &str) -> Result<usize, UnresolvedReason> {
let name = name.trim();
let candidates = self
.branches
.get(&normalize_name(name))
.map_or(&[][..], Vec::as_slice);
let exact: Vec<usize> = candidates
.iter()
.filter(|(stated, _)| stated == name)
.map(|(_, row)| *row)
.collect();
let all: Vec<usize> = candidates.iter().map(|(_, row)| *row).collect();
pick(&exact, &all).map_err(|matches| match matches {
0 => UnresolvedReason::NoSuchBranchName,
matches => UnresolvedReason::AmbiguousBranchName { matches },
})
}
}
fn pick<T: Copy>(exact: &[T], all: &[T]) -> Result<T, usize> {
match (exact, all) {
([one], _) | ([], [one]) => Ok(*one),
([], many) | (many, _) => Err(many.len()),
}
}
pub(super) enum TextAction {
Action(ContingencyAction),
Branch(usize),
}
pub(super) fn read_text_action(
names: &NameIndex,
text: &str,
) -> Option<Result<TextAction, UnresolvedReason>> {
let lines = lex(text);
let (head, _) = split_block(&lines)?;
let mut upper = head.keywords();
let mut words: Vec<String> = head.words().iter().map(|w| (*w).to_owned()).collect();
if upper.last().is_some_and(|w| w == "DISPATCH") && upper.len() > 1 {
upper.pop();
words.pop();
}
let quoted: Vec<usize> = head
.tokens
.iter()
.enumerate()
.filter(|(_, token)| token.quoted)
.map(|(at, _)| at)
.collect();
if matches!(upper[0].as_str(), "OPEN" | "TRIP" | "DISCONNECT")
&& quoted.len() == 1
&& quoted[0] == upper.len() - 1
&& (upper.len() == 2
|| (upper.len() == 3 && matches!(upper[1].as_str(), "BRANCH" | "LINE")))
{
return Some(names.branch(&words[quoted[0]]).map(TextAction::Branch));
}
let mut named = false;
let mut unresolved = None;
let views: Vec<&str> = words.iter().map(String::as_str).collect();
let action = super::parse_action_with_buses(&upper, &views, &mut |at| {
let token = head.tokens.get(at)?;
if let Some(bus) = super::parse_bus(token.text) {
return Some(bus);
}
if !token.quoted {
return None;
}
named = true;
Some(names.bus(token.text).unwrap_or_else(|reason| {
unresolved.get_or_insert(reason);
BusId(0)
}))
})?;
if let Some(reason) = unresolved {
return Some(Err(reason));
}
let dispatched = head.keywords().last().is_some_and(|w| w == "DISPATCH");
(named || dispatched).then_some(Ok(TextAction::Action(action)))
}