use crate::constants::{H_TOL, PSIperFT};
use crate::model::link::LinkStatus;
use crate::model::network::Network;
use crate::model::node::NodeType;
use crate::solver::state::SolverState;
use serde::{Deserialize, Serialize};
use strum;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Rule {
pub id: Box<str>,
pub conditions: Vec<RuleCondition>,
pub actions: Vec<RuleAction>,
pub priority: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum NodeAttribute {
Demand,
Head,
Pressure,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum TankAttribute {
Level,
FillTime,
DrainTime,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum LinkAttribute {
Flow,
Status,
Setting,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum SystemAttribute {
Demand,
Time,
ClockTime,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum RuleConditionTarget {
Node {
id: Box<str>,
attribute: NodeAttribute,
},
Tank {
id: Box<str>,
attribute: TankAttribute,
},
Link {
id: Box<str>,
attribute: LinkAttribute,
},
System {
attribute: SystemAttribute,
},
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub enum ConditionValue {
Number(f64),
Status(LinkStatus),
Time(usize),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum RuleConditionOperator {
Or,
And,
}
#[derive(
strum::EnumString, strum::Display, Debug, Deserialize, Serialize, Clone, PartialEq, Eq,
)]
pub enum ComparisonOperator {
#[strum(serialize = "IS", serialize = "=")]
Eq,
#[strum(serialize = "NOT", serialize = "<>")]
Ne,
#[strum(serialize = "ABOVE", serialize = ">")]
Gt,
#[strum(serialize = "BELOW", serialize = "<")]
Lt,
#[strum(serialize = ">=")]
Ge,
#[strum(serialize = "<=")]
Le,
}
impl ComparisonOperator {
fn compare_f64(&self, lhs: f64, rhs: f64) -> bool {
let diff = lhs - rhs;
match self {
ComparisonOperator::Eq => diff.abs() <= H_TOL,
ComparisonOperator::Ne => diff.abs() > H_TOL,
ComparisonOperator::Gt => diff > H_TOL,
ComparisonOperator::Lt => diff < -H_TOL,
ComparisonOperator::Ge => diff >= -H_TOL,
ComparisonOperator::Le => diff <= H_TOL,
}
}
fn compare_status(&self, lhs: LinkStatus, rhs: LinkStatus) -> bool {
match self {
ComparisonOperator::Eq => lhs == rhs,
ComparisonOperator::Ne => lhs != rhs,
_ => false,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RuleCondition {
pub operator: RuleConditionOperator,
pub target: RuleConditionTarget,
pub comparison: ComparisonOperator,
pub value: ConditionValue,
}
impl RuleCondition {
pub fn is_active(
&self,
state: &SolverState,
network: &Network,
time: usize,
clocktime: usize,
) -> bool {
match &self.target {
RuleConditionTarget::System { attribute } => {
let value = match attribute {
SystemAttribute::Demand => state.demands.iter().sum::<f64>(),
SystemAttribute::Time => time as f64,
SystemAttribute::ClockTime => clocktime as f64,
};
let target = match self.value {
ConditionValue::Number(v) => v,
ConditionValue::Time(t) => t as f64,
ConditionValue::Status(_) => return false,
};
self.comparison.compare_f64(value, target)
}
RuleConditionTarget::Node { id, attribute } => {
let Some(&idx) = network.node_map.get(id) else {
return false;
};
let node = &network.nodes[idx];
let value = match attribute {
NodeAttribute::Demand => state.demands[idx],
NodeAttribute::Head => state.heads[idx],
NodeAttribute::Pressure => (state.heads[idx] - node.elevation) * PSIperFT,
};
let target = match self.value {
ConditionValue::Number(v) => v,
_ => return false,
};
self.comparison.compare_f64(value, target)
}
RuleConditionTarget::Tank { id, attribute } => {
let Some(&idx) = network.node_map.get(id) else {
return false;
};
let node = &network.nodes[idx];
let NodeType::Tank(tank) = &node.node_type else {
return false;
};
let level = state.heads[idx] - tank.elevation;
let demand = state.demands[idx];
let value = match attribute {
TankAttribute::Level => level,
TankAttribute::FillTime => {
tank.time_to_reach_level(level, tank.max_level, demand) as f64 / 3600.0
}
TankAttribute::DrainTime => {
tank.time_to_reach_level(level, tank.min_level, demand) as f64 / 3600.0
}
};
let target = match self.value {
ConditionValue::Number(v) => v,
_ => return false,
};
self.comparison.compare_f64(value, target)
}
RuleConditionTarget::Link { id, attribute } => {
let Some(&idx) = network.link_map.get(id) else {
return false;
};
match attribute {
LinkAttribute::Flow => {
let target = match self.value {
ConditionValue::Number(v) => v,
_ => return false,
};
self.comparison.compare_f64(state.flows[idx].abs(), target)
}
LinkAttribute::Setting => {
let target = match self.value {
ConditionValue::Number(v) => v,
_ => return false,
};
self.comparison.compare_f64(state.settings[idx], target)
}
LinkAttribute::Status => {
let target = match self.value {
ConditionValue::Status(s) => s,
_ => return false,
};
self.comparison.compare_status(state.statuses[idx], target)
}
}
}
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RuleAction {
pub link_id: Box<str>,
pub setting: Option<f64>,
pub status: Option<LinkStatus>,
pub default_active: bool,
}