mod premises;
mod rules;
mod simple;
pub(crate) use rules::{apply_link_actions, eval_rules};
pub(crate) use simple::{apply_simple_controls, pswitch, resolve_control_action};
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ActionValue, DemandCategory, Junction, Link, LinkBase, LinkKind, LinkState, LinkStatus,
LogicOp, MixModel, Network, Node, NodeBase, NodeKind, NodeState, Pipe, Premise,
PremiseAttribute, PremiseObject, PremiseOperator, Rule, RuleAction, SimpleControl,
SimulationOptions, Tank, TriggerType,
};
fn minimal_network() -> Network {
let opts = SimulationOptions::default();
let node = Node {
base: NodeBase {
id: "N1".into(),
index: 1,
elevation: 0.0,
initial_quality: 0.0,
},
kind: NodeKind::Junction(Junction {
demands: vec![DemandCategory {
base_demand: 0.0,
pattern: None,
name: None,
}],
emitter_coeff: 0.0,
emitter_exp: 0.5,
}),
source: None,
};
let link = Link {
base: LinkBase {
id: "P1".into(),
index: 1,
from_node: 1,
to_node: 1,
initial_status: LinkStatus::Open,
initial_setting: Some(1.0),
},
kind: LinkKind::Pipe(Pipe {
length: 1000.0,
diameter: 1.0,
roughness: 100.0,
minor_loss: 0.0,
check_valve: false,
bulk_coeff: None,
wall_coeff: None,
leak_coeff_1: 0.0,
leak_coeff_2: 0.0,
}),
};
Network {
title: vec![],
options: opts,
patterns: vec![],
curves: vec![],
nodes: vec![node],
links: vec![link],
controls: vec![],
rules: vec![],
pattern_index: std::collections::HashMap::new(),
report: crate::ReportOptions::default(),
coordinates: std::collections::HashMap::new(),
vertices: std::collections::HashMap::new(),
node_tags: std::collections::HashMap::new(),
link_tags: std::collections::HashMap::new(),
}
}
fn open_state() -> LinkState {
LinkState {
flow: 1.0,
status: LinkStatus::Open,
setting: 1.0,
quality: 0.0,
reaction_rate: 0.0,
}
}
fn zero_node_state() -> NodeState {
NodeState::default()
}
#[test]
fn timer_fires_at_exact_time() {
let mut net = minimal_network();
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::Timer,
trigger_time: Some(3600.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: true,
});
let node_states = vec![zero_node_state()];
let mut link_states = vec![open_state()];
let changed = apply_simple_controls(&net, &node_states, &mut link_states, 3600.0);
assert!(changed);
assert_eq!(link_states[0].status, LinkStatus::Closed);
}
#[test]
fn timer_no_fire_at_wrong_time() {
let mut net = minimal_network();
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::Timer,
trigger_time: Some(3600.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: true,
});
let node_states = vec![zero_node_state()];
let mut link_states = vec![open_state()];
let changed = apply_simple_controls(&net, &node_states, &mut link_states, 7200.0);
assert!(!changed);
assert_eq!(link_states[0].status, LinkStatus::Open);
}
#[test]
fn disabled_control_never_fires() {
let mut net = minimal_network();
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::Timer,
trigger_time: Some(0.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: false,
});
let node_states = vec![zero_node_state()];
let mut link_states = vec![open_state()];
let changed = apply_simple_controls(&net, &node_states, &mut link_states, 0.0);
assert!(!changed);
}
#[test]
fn timeofday_fires_on_modulo_match() {
let mut net = minimal_network();
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::TimeOfDay,
trigger_time: Some(28800.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: true,
});
let node_states = vec![zero_node_state()];
let mut link_states = vec![open_state()];
let changed =
apply_simple_controls(&net, &node_states, &mut link_states, 86400.0 + 28800.0);
assert!(changed);
}
#[test]
fn last_control_on_same_link_wins() {
let mut net = minimal_network();
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::Timer,
trigger_time: Some(0.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: true,
});
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::Timer,
trigger_time: Some(0.0),
trigger_node: None,
trigger_grade: None,
action_status: Some(LinkStatus::Open),
action_setting: None,
enabled: true,
});
let node_states = vec![zero_node_state()];
let mut link_states = vec![LinkState {
flow: 0.0,
status: LinkStatus::Closed,
setting: 1.0,
quality: 0.0,
reaction_rate: 0.0,
}];
apply_simple_controls(&net, &node_states, &mut link_states, 0.0);
assert_eq!(link_states[0].status, LinkStatus::Open);
}
#[test]
fn hilevel_fires_when_volume_at_threshold() {
let mut net = minimal_network();
net.nodes.push(Node {
base: NodeBase {
id: "T1".into(),
index: 2,
elevation: 10.0, initial_quality: 0.0,
},
kind: NodeKind::Tank(Tank {
min_level: 0.0,
max_level: 20.0,
initial_level: 5.0,
diameter: 10.0,
min_volume: 0.0,
volume_curve: None,
mix_model: MixModel::Cstr,
mix_fraction: 1.0,
bulk_coeff: 0.0,
overflow: false,
head_pattern: None,
}),
source: None,
});
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::HiLevel,
trigger_time: None,
trigger_node: Some(2),
trigger_grade: Some(25.0),
action_status: Some(LinkStatus::Closed),
action_setting: None,
enabled: true,
});
let level = 16.0_f64;
let head = 10.0 + level; let area = std::f64::consts::PI * 5.0 * 5.0;
let volume = area * level;
let node_states = vec![
zero_node_state(),
NodeState {
head,
level,
volume,
net_flow: 0.0,
..NodeState::default()
},
];
let mut link_states = vec![open_state()];
let changed = apply_simple_controls(&net, &node_states, &mut link_states, 0.0);
assert!(changed, "HiLevel should fire when tank is above threshold");
assert_eq!(link_states[0].status, LinkStatus::Closed);
}
#[test]
fn lowlevel_fires_when_volume_below_threshold() {
let mut net = minimal_network();
net.nodes.push(Node {
base: NodeBase {
id: "T1".into(),
index: 2,
elevation: 10.0,
initial_quality: 0.0,
},
kind: NodeKind::Tank(Tank {
min_level: 0.0,
max_level: 20.0,
initial_level: 5.0,
diameter: 10.0,
min_volume: 0.0,
volume_curve: None,
mix_model: MixModel::Cstr,
mix_fraction: 1.0,
bulk_coeff: 0.0,
overflow: false,
head_pattern: None,
}),
source: None,
});
net.controls.push(SimpleControl {
link: 1,
trigger_type: TriggerType::LowLevel,
trigger_time: None,
trigger_node: Some(2),
trigger_grade: Some(15.0),
action_status: Some(LinkStatus::Open),
action_setting: None,
enabled: true,
});
let level = 4.0_f64;
let head = 10.0 + level; let area = std::f64::consts::PI * 5.0 * 5.0;
let volume = area * level;
let mut link_states = vec![LinkState {
flow: 0.0,
status: LinkStatus::Closed,
setting: 1.0,
quality: 0.0,
reaction_rate: 0.0,
}];
let node_states = vec![
zero_node_state(),
NodeState {
head,
level,
volume,
net_flow: 0.0,
..NodeState::default()
},
];
let changed = apply_simple_controls(&net, &node_states, &mut link_states, 0.0);
assert!(changed, "LowLevel should fire when tank is below threshold");
assert_eq!(link_states[0].status, LinkStatus::Open);
}
fn make_rule(
priority: f64,
premises: Vec<Premise>,
then_actions: Vec<RuleAction>,
else_actions: Vec<RuleAction>,
) -> crate::Rule {
Rule {
priority,
premises,
then_actions,
else_actions,
}
}
fn head_premise(node: usize, op: PremiseOperator, val: f64, conn: Option<LogicOp>) -> Premise {
Premise {
object: PremiseObject::Node(node),
attribute: PremiseAttribute::Head,
operator: op,
value: val,
connective: conn,
}
}
fn close_action(link: usize) -> RuleAction {
RuleAction {
link,
value: ActionValue::Status(LinkStatus::Closed),
}
}
fn open_action(link: usize) -> RuleAction {
RuleAction {
link,
value: ActionValue::Status(LinkStatus::Open),
}
}
#[test]
fn rule_and_premises_all_true_fires() {
let mut net = minimal_network();
net.nodes.push(Node {
base: NodeBase {
id: "N2".into(),
index: 2,
elevation: 0.0,
initial_quality: 0.0,
},
kind: NodeKind::Junction(Junction {
demands: vec![],
emitter_coeff: 0.0,
emitter_exp: 0.5,
}),
source: None,
});
net.links[0].base.to_node = 2;
net.rules.push(make_rule(
1.0,
vec![
head_premise(1, PremiseOperator::Gt, 50.0, Some(LogicOp::And)),
head_premise(2, PremiseOperator::Gt, 30.0, None),
],
vec![close_action(1)],
vec![],
));
let node_states = vec![
NodeState {
head: 60.0,
..NodeState::default()
},
NodeState {
head: 40.0,
..NodeState::default()
},
];
let link_states = vec![open_state()];
let result = eval_rules(&net, &node_states, &link_states, 0.0);
assert!(result.is_some());
let (actions, _) = result.unwrap();
assert_eq!(actions.len(), 1);
assert!(matches!(
actions[0].1,
ActionValue::Status(LinkStatus::Closed)
));
}
#[test]
fn rule_and_premises_one_false_no_fire() {
let mut net = minimal_network();
net.nodes.push(Node {
base: NodeBase {
id: "N2".into(),
index: 2,
elevation: 0.0,
initial_quality: 0.0,
},
kind: NodeKind::Junction(Junction {
demands: vec![],
emitter_coeff: 0.0,
emitter_exp: 0.5,
}),
source: None,
});
net.rules.push(make_rule(
1.0,
vec![
head_premise(1, PremiseOperator::Gt, 50.0, Some(LogicOp::And)),
head_premise(2, PremiseOperator::Gt, 30.0, None),
],
vec![close_action(1)],
vec![],
));
let node_states = vec![
NodeState {
head: 60.0,
..NodeState::default()
},
NodeState {
head: 20.0,
..NodeState::default()
}, ];
let link_states = vec![open_state()];
let result = eval_rules(&net, &node_states, &link_states, 0.0);
assert!(result.is_none());
}
#[test]
fn rule_or_premises_second_clause_true_fires() {
let mut net = minimal_network();
net.nodes.push(Node {
base: NodeBase {
id: "N2".into(),
index: 2,
elevation: 0.0,
initial_quality: 0.0,
},
kind: NodeKind::Junction(Junction {
demands: vec![],
emitter_coeff: 0.0,
emitter_exp: 0.5,
}),
source: None,
});
net.rules.push(make_rule(
1.0,
vec![
head_premise(1, PremiseOperator::Gt, 100.0, Some(LogicOp::Or)),
head_premise(2, PremiseOperator::Gt, 30.0, None),
],
vec![close_action(1)],
vec![],
));
let node_states = vec![
NodeState {
head: 40.0,
..NodeState::default()
},
NodeState {
head: 40.0,
..NodeState::default()
},
];
let link_states = vec![open_state()];
let result = eval_rules(&net, &node_states, &link_states, 0.0);
assert!(result.is_some());
}
#[test]
fn rule_conflict_higher_priority_wins() {
let mut net = minimal_network();
net.rules.push(make_rule(
2.0,
vec![head_premise(1, PremiseOperator::Ge, 0.0, None)],
vec![open_action(1)],
vec![],
));
net.rules.push(make_rule(
1.0,
vec![head_premise(1, PremiseOperator::Ge, 0.0, None)],
vec![close_action(1)],
vec![],
));
let node_states = vec![NodeState {
head: 10.0,
..NodeState::default()
}];
let link_states = vec![open_state()]; let result = eval_rules(&net, &node_states, &link_states, 0.0);
assert!(result.is_some());
let (actions, _) = result.unwrap();
let status_action = actions.iter().find(|(l, _)| *l == 0).unwrap();
assert!(
matches!(status_action.1, ActionValue::Status(LinkStatus::Open)),
"higher priority (2.0) rule that opens should win over priority 1.0 close rule"
);
}
#[test]
fn rule_else_actions_applied_when_premise_false() {
let mut net = minimal_network();
net.rules.push(make_rule(
1.0,
vec![head_premise(1, PremiseOperator::Gt, 100.0, None)],
vec![close_action(1)],
vec![open_action(1)],
));
let node_states = vec![NodeState {
head: 50.0,
..NodeState::default()
}];
let link_states = vec![LinkState {
flow: 0.0,
status: LinkStatus::Closed,
setting: 1.0,
quality: 0.0,
reaction_rate: 0.0,
}];
let result = eval_rules(&net, &node_states, &link_states, 0.0);
assert!(
result.is_some(),
"ELSE actions should be returned even when no THEN fires"
);
let (actions, fired) = result.unwrap();
assert!(!fired, "THEN did not fire");
assert_eq!(actions.len(), 1);
assert!(matches!(
actions[0].1,
ActionValue::Status(LinkStatus::Open)
));
}
#[test]
fn apply_link_actions_updates_state() {
let net = minimal_network();
let mut link_states = vec![open_state()];
let actions = vec![(0usize, ActionValue::Status(LinkStatus::Closed))];
let changed = apply_link_actions(&mut link_states, &actions, &net);
assert!(changed);
assert_eq!(link_states[0].status, LinkStatus::Closed);
}
#[test]
fn apply_link_actions_no_change_when_same() {
let net = minimal_network();
let mut link_states = vec![open_state()];
let actions = vec![(0usize, ActionValue::Status(LinkStatus::Open))];
let changed = apply_link_actions(&mut link_states, &actions, &net);
assert!(!changed);
}
#[test]
fn time_premise_matches_simulation_time() {
let mut net = minimal_network();
net.rules.push(make_rule(
1.0,
vec![Premise {
object: PremiseObject::Clock,
attribute: PremiseAttribute::Time,
operator: PremiseOperator::Ge,
value: 3600.0,
connective: None,
}],
vec![close_action(1)],
vec![],
));
let node_states = vec![NodeState::default()];
let link_states = vec![open_state()];
assert!(eval_rules(&net, &node_states, &link_states, 3600.0).is_some());
assert!(eval_rules(&net, &node_states, &link_states, 3599.0).is_none());
}
}