use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use super::{EdgeId, MetricKey, NodeId, ScenarioId};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Source,
Pool,
Drain,
SortingGate,
TriggerGate,
MixedGate,
Converter,
Trader,
Register,
Delay,
Queue,
Process,
Sink,
Gate,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TriggerMode {
#[serde(alias = "passive")]
Passive,
#[serde(alias = "interactive")]
Interactive,
#[serde(alias = "automatic")]
#[default]
Automatic,
#[serde(alias = "enabling")]
Enabling,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ActionMode {
#[serde(alias = "push-any")]
#[default]
PushAny,
#[serde(alias = "push-all")]
PushAll,
#[serde(alias = "pull-any")]
PullAny,
#[serde(alias = "pull-all")]
PullAll,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct NodeModeConfig {
#[serde(default)]
pub trigger_mode: TriggerMode,
#[serde(default)]
pub action_mode: ActionMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PoolNodeConfig {
pub capacity: Option<u64>,
#[serde(default)]
pub allow_negative_start: bool,
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DrainNodeConfig {
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SortingGateNodeConfig {
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TriggerGateNodeConfig {
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MixedGateNodeConfig {
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ConverterNodeConfig {
#[serde(default)]
pub ignore_disabled_inputs: bool,
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TraderNodeConfig {
#[serde(default)]
pub ignore_disabled_inputs: bool,
#[serde(default)]
pub mode: NodeModeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RegisterNodeConfig {
#[serde(default)]
pub interactive: bool,
pub min_value: Option<i64>,
pub max_value: Option<i64>,
}
fn default_delay_steps() -> u64 {
1
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelayNodeConfig {
#[serde(default = "default_delay_steps")]
pub delay_steps: u64,
#[serde(default)]
pub mode: NodeModeConfig,
}
impl Default for DelayNodeConfig {
fn default() -> Self {
Self { delay_steps: default_delay_steps(), mode: NodeModeConfig::default() }
}
}
fn default_queue_release_per_step() -> u64 {
1
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueueNodeConfig {
pub capacity: Option<u64>,
#[serde(default = "default_queue_release_per_step")]
pub release_per_step: u64,
#[serde(default)]
pub mode: NodeModeConfig,
}
impl Default for QueueNodeConfig {
fn default() -> Self {
Self {
capacity: None,
release_per_step: default_queue_release_per_step(),
mode: NodeModeConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "family", content = "config", rename_all = "snake_case")]
pub enum NodeConfig {
#[default]
None,
Pool(PoolNodeConfig),
Drain(DrainNodeConfig),
SortingGate(SortingGateNodeConfig),
TriggerGate(TriggerGateNodeConfig),
MixedGate(MixedGateNodeConfig),
Converter(ConverterNodeConfig),
Trader(TraderNodeConfig),
Register(RegisterNodeConfig),
Delay(DelayNodeConfig),
Queue(QueueNodeConfig),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransferSpec {
Fixed { amount: f64 },
Fraction { numerator: u64, denominator: u64 },
Remaining,
MetricScaled { metric: MetricKey, factor: f64 },
Expression { formula: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EndConditionSpec {
MaxSteps { steps: u64 },
MetricAtLeast { metric: MetricKey, value_scaled: i64 },
MetricAtMost { metric: MetricKey, value_scaled: i64 },
NodeAtLeast { node_id: NodeId, value_scaled: i64 },
NodeAtMost { node_id: NodeId, value_scaled: i64 },
Any(Vec<EndConditionSpec>),
All(Vec<EndConditionSpec>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VariableUpdateTiming {
#[default]
EveryStep,
RunStart,
Never,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VariableSourceSpec {
Constant { value: f64 },
RandomInterval { min: i64, max: i64 },
RandomList { values: Vec<f64> },
RandomMatrix { values: Vec<Vec<f64>> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct VariableRuntimeConfig {
#[serde(default)]
pub update_timing: VariableUpdateTiming,
#[serde(default)]
pub sources: BTreeMap<String, VariableSourceSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionKind {
#[default]
Resource,
State,
}
fn default_resource_token_size() -> u64 {
1
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceConnectionConfig {
#[serde(default = "default_resource_token_size")]
pub token_size: u64,
}
impl Default for ResourceConnectionConfig {
fn default() -> Self {
Self { token_size: default_resource_token_size() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum StateConnectionRole {
Activator,
Trigger,
#[default]
Modifier,
Filter,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum StateConnectionTarget {
#[default]
Node,
ResourceConnection,
StateConnection,
Formula,
}
fn default_state_connection_formula() -> String {
"+1".to_string()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateConnectionConfig {
#[serde(default)]
pub role: StateConnectionRole,
#[serde(default = "default_state_connection_formula")]
pub formula: String,
#[serde(default)]
pub target: StateConnectionTarget,
#[serde(default, alias = "target_edge")]
pub target_connection: Option<EdgeId>,
#[serde(default, alias = "filter")]
pub resource_filter: Option<String>,
}
impl Default for StateConnectionConfig {
fn default() -> Self {
Self {
role: StateConnectionRole::default(),
formula: default_state_connection_formula(),
target: StateConnectionTarget::default(),
target_connection: None,
resource_filter: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct EdgeConnectionConfig {
#[serde(default, alias = "connection_kind")]
pub kind: ConnectionKind,
#[serde(default)]
pub resource: ResourceConnectionConfig,
#[serde(default)]
pub state: StateConnectionConfig,
}
impl EdgeConnectionConfig {
pub fn is_default_resource(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodeSpec {
pub id: NodeId,
pub kind: NodeKind,
#[serde(default)]
pub config: NodeConfig,
pub label: Option<String>,
pub initial_value: f64,
pub tags: BTreeSet<String>,
pub metadata: BTreeMap<String, String>,
}
impl NodeSpec {
pub fn new(id: NodeId, kind: NodeKind) -> Self {
Self {
id,
kind,
config: NodeConfig::default(),
label: None,
initial_value: 0.0,
tags: BTreeSet::new(),
metadata: BTreeMap::new(),
}
}
pub fn with_initial_value(mut self, initial_value: f64) -> Self {
self.initial_value = initial_value;
self
}
pub fn with_config(mut self, config: NodeConfig) -> Self {
self.config = config;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EdgeSpec {
pub id: EdgeId,
pub from: NodeId,
pub to: NodeId,
pub transfer: TransferSpec,
#[serde(default, skip_serializing_if = "EdgeConnectionConfig::is_default_resource")]
pub connection: EdgeConnectionConfig,
pub enabled: bool,
pub metadata: BTreeMap<String, String>,
}
impl EdgeSpec {
pub fn new(id: EdgeId, from: NodeId, to: NodeId, transfer: TransferSpec) -> Self {
Self {
id,
from,
to,
transfer,
connection: EdgeConnectionConfig::default(),
enabled: true,
metadata: BTreeMap::new(),
}
}
pub fn with_connection(mut self, connection: EdgeConnectionConfig) -> Self {
self.connection = connection;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScenarioSpec {
pub id: ScenarioId,
pub title: Option<String>,
pub description: Option<String>,
pub tags: BTreeSet<String>,
pub nodes: BTreeMap<NodeId, NodeSpec>,
pub edges: BTreeMap<EdgeId, EdgeSpec>,
#[serde(default)]
pub variables: VariableRuntimeConfig,
pub end_conditions: Vec<EndConditionSpec>,
pub tracked_metrics: BTreeSet<MetricKey>,
pub metadata: BTreeMap<String, String>,
}
impl ScenarioSpec {
pub fn new(id: ScenarioId) -> Self {
Self {
id,
title: None,
description: None,
tags: BTreeSet::new(),
nodes: BTreeMap::new(),
edges: BTreeMap::new(),
variables: VariableRuntimeConfig::default(),
end_conditions: vec![EndConditionSpec::MaxSteps { steps: 1 }],
tracked_metrics: BTreeSet::new(),
metadata: BTreeMap::new(),
}
}
pub fn source_sink(transfer: TransferSpec) -> Self {
let source = NodeId::fixture("source");
let sink = NodeId::fixture("sink");
let mut scenario = Self::new(ScenarioId::fixture("scenario-source-sink"))
.with_node(NodeSpec::new(source.clone(), NodeKind::Source).with_initial_value(1.0))
.with_node(NodeSpec::new(sink.clone(), NodeKind::Sink))
.with_edge(EdgeSpec::new(EdgeId::fixture("edge-source-sink"), source, sink, transfer));
scenario.tracked_metrics.insert(MetricKey::fixture("sink"));
scenario
}
pub fn linear_pipeline(node_count: usize) -> Self {
let node_count = node_count.max(2);
let mut scenario = Self::new(ScenarioId::fixture("scenario-linear-pipeline"));
let mut node_ids = Vec::with_capacity(node_count);
for index in 0..node_count {
let (id, kind) = match index {
0 => ("source".to_string(), NodeKind::Source),
last if last == node_count - 1 => ("sink".to_string(), NodeKind::Sink),
_ => (format!("stage-{index}"), NodeKind::Pool),
};
let node_id = NodeId::fixture(id);
let node = if index == 0 {
NodeSpec::new(node_id.clone(), kind).with_initial_value(1.0)
} else {
NodeSpec::new(node_id.clone(), kind)
};
node_ids.push(node_id);
scenario = scenario.with_node(node);
}
for edge_index in 0..(node_ids.len() - 1) {
let from = node_ids[edge_index].clone();
let to = node_ids[edge_index + 1].clone();
let edge = EdgeSpec::new(
EdgeId::fixture(format!("edge-{edge_index}")),
from,
to,
TransferSpec::Fixed { amount: 1.0 },
);
scenario = scenario.with_edge(edge);
}
scenario.tracked_metrics.insert(MetricKey::fixture("sink"));
scenario
}
pub fn with_node(mut self, node: NodeSpec) -> Self {
self.nodes.insert(node.id.clone(), node);
self
}
pub fn with_edge(mut self, edge: EdgeSpec) -> Self {
self.edges.insert(edge.id.clone(), edge);
self
}
pub fn with_end_condition(mut self, condition: EndConditionSpec) -> Self {
self.end_conditions = vec![condition];
self
}
pub fn with_end_conditions<I>(mut self, conditions: I) -> Self
where
I: IntoIterator<Item = EndConditionSpec>,
{
self.end_conditions = conditions.into_iter().collect();
self
}
pub fn push_end_condition(mut self, condition: EndConditionSpec) -> Self {
self.end_conditions.push(condition);
self
}
}