Skip to main content

pflow_core/
builder.rs

1//! Fluent builder API for constructing Petri nets.
2
3use std::collections::HashMap;
4
5use crate::net::PetriNet;
6
7/// Fluent builder for Petri net construction.
8pub struct Builder {
9    net: PetriNet,
10    next_x: f64,
11    place_y: f64,
12    trans_y: f64,
13}
14
15impl Builder {
16    /// Creates a new Builder.
17    pub fn new() -> Self {
18        Self {
19            net: PetriNet::new(),
20            next_x: 100.0,
21            place_y: 100.0,
22            trans_y: 200.0,
23        }
24    }
25
26    /// Adds a place with the given label and initial token count.
27    pub fn place(mut self, label: &str, initial: f64) -> Self {
28        self.net.add_place(
29            label,
30            vec![initial],
31            vec![],
32            self.next_x,
33            self.place_y,
34            None,
35        );
36        self.next_x += 100.0;
37        self
38    }
39
40    /// Adds a place with initial tokens and capacity limit.
41    pub fn place_with_capacity(mut self, label: &str, initial: f64, capacity: f64) -> Self {
42        self.net.add_place(
43            label,
44            vec![initial],
45            vec![capacity],
46            self.next_x,
47            self.place_y,
48            None,
49        );
50        self.next_x += 100.0;
51        self
52    }
53
54    /// Adds a transition with the given label.
55    pub fn transition(mut self, label: &str) -> Self {
56        self.net
57            .add_transition(label, "default", self.next_x, self.trans_y, None);
58        self.next_x += 100.0;
59        self
60    }
61
62    /// Adds a transition with a specific role.
63    pub fn transition_with_role(mut self, label: &str, role: &str) -> Self {
64        self.net
65            .add_transition(label, role, self.next_x, self.trans_y, None);
66        self.next_x += 100.0;
67        self
68    }
69
70    /// Adds an arc from source to target with the given weight.
71    pub fn arc(mut self, source: &str, target: &str, weight: f64) -> Self {
72        self.net
73            .add_arc(source, target, vec![weight], false);
74        self
75    }
76
77    /// Adds an inhibitor arc from source to target.
78    pub fn inhibitor_arc(mut self, source: &str, target: &str, weight: f64) -> Self {
79        self.net
80            .add_arc(source, target, vec![weight], true);
81        self
82    }
83
84    /// Adds bidirectional arcs for a flow pattern: place -> transition -> place.
85    pub fn flow(mut self, from_place: &str, transition: &str, to_place: &str, weight: f64) -> Self {
86        self.net
87            .add_arc(from_place, transition, vec![weight], false);
88        self.net
89            .add_arc(transition, to_place, vec![weight], false);
90        self
91    }
92
93    /// Creates a sequential chain of places connected by transitions.
94    ///
95    /// Elements must be odd length: place, trans, place, trans, place...
96    /// The first place gets `initial_tokens`, all others get 0.
97    pub fn chain(mut self, initial_tokens: f64, elements: &[&str]) -> Self {
98        if elements.len() < 3 || elements.len() % 2 == 0 {
99            return self;
100        }
101
102        self = self.place(elements[0], initial_tokens);
103
104        let mut i = 1;
105        while i < elements.len() {
106            let trans = elements[i];
107            let next_place = elements[i + 1];
108
109            self = self.transition(trans);
110            self = self.place(next_place, 0.0);
111            self.net
112                .add_arc(elements[i - 1], trans, vec![1.0], false);
113            self.net
114                .add_arc(trans, next_place, vec![1.0], false);
115            i += 2;
116        }
117
118        self
119    }
120
121    /// Creates a standard SIR epidemic model.
122    pub fn sir(self, susceptible: f64, infected: f64, recovered: f64) -> Self {
123        self.place("S", susceptible)
124            .place("I", infected)
125            .place("R", recovered)
126            .transition("infect")
127            .transition("recover")
128            .arc("S", "infect", 1.0)
129            .arc("I", "infect", 1.0)
130            .arc("infect", "I", 2.0)
131            .arc("I", "recover", 1.0)
132            .arc("recover", "R", 1.0)
133    }
134
135    /// Returns the completed Petri net.
136    pub fn done(self) -> PetriNet {
137        self.net
138    }
139
140    /// Returns the net and a rates map initialized to the given default rate.
141    pub fn with_rates(self, default_rate: f64) -> (PetriNet, HashMap<String, f64>) {
142        let mut rates = HashMap::new();
143        for label in self.net.transitions.keys() {
144            rates.insert(label.clone(), default_rate);
145        }
146        (self.net, rates)
147    }
148
149    /// Returns the net and allows setting custom rates.
150    pub fn with_custom_rates(
151        self,
152        rates: HashMap<String, f64>,
153    ) -> (PetriNet, HashMap<String, f64>) {
154        (self.net, rates)
155    }
156}
157
158impl Default for Builder {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_basic_builder() {
170        let net = Builder::new()
171            .place("A", 10.0)
172            .place("B", 0.0)
173            .transition("t1")
174            .arc("A", "t1", 1.0)
175            .arc("t1", "B", 1.0)
176            .done();
177
178        assert_eq!(net.places.len(), 2);
179        assert_eq!(net.transitions.len(), 1);
180        assert_eq!(net.arcs.len(), 2);
181        assert_eq!(net.places["A"].token_count(), 10.0);
182    }
183
184    #[test]
185    fn test_sir_builder() {
186        let (net, rates) = PetriNet::build().sir(999.0, 1.0, 0.0).with_rates(1.0);
187
188        assert_eq!(net.places.len(), 3);
189        assert_eq!(net.transitions.len(), 2);
190        assert_eq!(net.arcs.len(), 5);
191        assert_eq!(net.places["S"].token_count(), 999.0);
192        assert_eq!(net.places["I"].token_count(), 1.0);
193        assert_eq!(net.places["R"].token_count(), 0.0);
194        assert_eq!(rates["infect"], 1.0);
195        assert_eq!(rates["recover"], 1.0);
196    }
197
198    #[test]
199    fn test_chain_builder() {
200        let net = Builder::new()
201            .chain(1.0, &["Received", "start", "Processing", "finish", "Complete"])
202            .done();
203
204        assert_eq!(net.places.len(), 3);
205        assert_eq!(net.transitions.len(), 2);
206        assert_eq!(net.places["Received"].token_count(), 1.0);
207        assert_eq!(net.places["Processing"].token_count(), 0.0);
208        assert_eq!(net.places["Complete"].token_count(), 0.0);
209    }
210
211    #[test]
212    fn test_with_rates() {
213        let (net, rates) = Builder::new()
214            .place("A", 10.0)
215            .transition("t1")
216            .transition("t2")
217            .arc("A", "t1", 1.0)
218            .with_rates(0.5);
219
220        assert_eq!(rates.len(), 2);
221        assert_eq!(rates["t1"], 0.5);
222        assert_eq!(rates["t2"], 0.5);
223        assert_eq!(net.places.len(), 1);
224    }
225
226    #[test]
227    fn test_flow_builder() {
228        let net = Builder::new()
229            .place("input", 5.0)
230            .place("output", 0.0)
231            .transition("process")
232            .flow("input", "process", "output", 1.0)
233            .done();
234
235        assert_eq!(net.arcs.len(), 2);
236    }
237}