AutoPumping 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ from abc import ABC, abstractmethod
2
+ from .safety import SafetyMachine
3
+ from .graph import PumpingGraph
4
+ from .config import Config
5
+ from .executor import PlanExecutor
6
+ from .ui import UI
7
+
8
+
9
+ class AutoPumping(ABC):
10
+ CONFIG_CLASS = Config
11
+ DIAGRAM_PATH = None
12
+
13
+ def __init__(self, config, ui=True):
14
+ self._config = config
15
+ self._safety_machine = SafetyMachine(config, self)
16
+ self._pumping_graph = PumpingGraph(config)
17
+ self._plan_executor = PlanExecutor(self)
18
+ self._ui = None
19
+ if ui:
20
+ self._ui = UI(config, self, diagram=self.DIAGRAM_PATH)
21
+
22
+ def run(self):
23
+ if self._ui is None:
24
+ raise RuntimeError("UI is not enabled for this instance.")
25
+ self._ui.run()
26
+
27
+ def auto(self, **goal_state_data):
28
+ self._plan_executor.execute_plan(
29
+ list(self._pumping_graph.make_plan(self._get_state(), **goal_state_data))
30
+ )
31
+
32
+ def cancel(self):
33
+ self._plan_executor.cancel()
34
+
35
+ @property
36
+ def plan(self):
37
+ return self._plan_executor.plan
38
+
39
+ def _get_state(self):
40
+ return {
41
+ "valves": {
42
+ valve: self.get_valve_state(valve) for valve in self._config.valves
43
+ },
44
+ "pumps": {pump: self.get_pump_state(pump) for pump in self._config.pumps},
45
+ "stage": self.get_stage_position(),
46
+ "gauges": {gauge: self._pumping_graph._get_gauge_range(gauge, self.get_gauge_pressure(gauge)) for gauge in self._config.gauges},
47
+ }
48
+
49
+ @classmethod
50
+ def from_config_file(cls, config_file):
51
+ config = cls.CONFIG_CLASS.load_file(config_file)
52
+ return cls(config)
53
+
54
+ @abstractmethod
55
+ def get_valve_state(self, valve):
56
+ pass
57
+
58
+ def _actuate_valve(self, valve, state):
59
+ if self._safety_machine.can_actuate_valve(valve, state):
60
+ self.actuate_valve(valve, state)
61
+ self._safety_machine.record_valve_change(valve)
62
+ return True
63
+ return False
64
+
65
+ @abstractmethod
66
+ def actuate_valve(self, valve, state):
67
+ pass
68
+
69
+ @abstractmethod
70
+ def get_pump_state(self, pump):
71
+ pass
72
+
73
+ def _set_pump(self, pump, state):
74
+ if self._safety_machine.can_set_pump(pump, state):
75
+ self.set_pump(pump, state)
76
+ self._safety_machine.record_pump_change(pump)
77
+ return True
78
+ return False
79
+
80
+ @abstractmethod
81
+ def set_pump(self, pump, state):
82
+ pass
83
+
84
+ @abstractmethod
85
+ def get_pump_current(self, pump):
86
+ pass
87
+
88
+ @abstractmethod
89
+ def get_pump_voltage(self, pump):
90
+ pass
91
+
92
+ @abstractmethod
93
+ def get_pump_flow(self, pump):
94
+ pass
95
+
96
+ @abstractmethod
97
+ def get_pump_speed(self, pump):
98
+ pass
99
+
100
+ @abstractmethod
101
+ def get_gauge_pressure(self, gauge):
102
+ pass
103
+
104
+ @abstractmethod
105
+ def get_stage_position(self):
106
+ pass
107
+
108
+ def _move_stage(self, position):
109
+ if self._safety_machine.can_move_stage(position):
110
+ self.move_stage(position)
111
+ return True
112
+ return False
113
+
114
+ @abstractmethod
115
+ def move_stage(self, position):
116
+ pass
@@ -0,0 +1,35 @@
1
+ import argparse
2
+ from .dummy import DummyPumping
3
+
4
+
5
+ def build_parser(system_class):
6
+ parser = argparse.ArgumentParser()
7
+ parser.add_argument(
8
+ "-c",
9
+ "--config",
10
+ type=str,
11
+ required=True,
12
+ help="Path to the configuration file.",
13
+ )
14
+ subparsers = parser.add_subparsers(dest="command", help="Sub-command help")
15
+ network_parser = subparsers.add_parser("network", help="Draw the network graph.")
16
+ network_parser.add_argument(
17
+ "--output", "-o", type=str, help="Output file for the network graph."
18
+ )
19
+ run_parser = subparsers.add_parser("run", help="Run the pumping system.")
20
+
21
+ return parser
22
+
23
+
24
+ def main(system_class):
25
+ parser = build_parser(system_class)
26
+ args = parser.parse_args()
27
+ system = system_class.from_config_file(args.config)
28
+ if args.command == "network":
29
+ system._pumping_graph.draw(args.output)
30
+ elif args.command == "run":
31
+ system.run()
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main(DummyPumping)
auto_pumping/config.py ADDED
@@ -0,0 +1,351 @@
1
+ from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
2
+ from enum import Enum
3
+ import yaml
4
+
5
+
6
+ class ValveState(str, Enum):
7
+ OPEN = "open"
8
+ CLOSED = "closed"
9
+
10
+
11
+ class PumpState(str, Enum):
12
+ ON = "on"
13
+ OFF = "off"
14
+
15
+
16
+ class StrictModel(BaseModel):
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+
20
+ class NamedState(StrictModel):
21
+ valves: dict[str, ValveState] = Field({}, description="A mapping of valve names to their states in this named state (not all valves need to be specified)")
22
+ stage: str | None = Field(None, description="Stage position in this named state")
23
+ pumps: dict[str, PumpState] = Field({}, description="A mapping of pump names to their states in this named state (not all pumps need to be specified)")
24
+
25
+ def _check_valves(self, valves: list[str]):
26
+ for valve in self.valves.keys():
27
+ if valve not in valves:
28
+ raise ValueError(f"Valve {valve} not in valves list")
29
+
30
+ def _check_pumps(self, pumps: list[str]):
31
+ for pump in self.pumps.keys():
32
+ if pump not in pumps:
33
+ raise ValueError(f"Pump {pump} not in pumps list")
34
+
35
+ def _check_stage(self, stages: list[str]):
36
+ if self.stage is not None and self.stage not in stages:
37
+ raise ValueError(f"Stage position {self.stage} not in stage position list")
38
+
39
+
40
+ class Inequality(StrictModel):
41
+ LE: float | None = Field(None, description="Upper bound of the inequality (less than or equal to)")
42
+ GE: float | None = Field(None, description="Lower bound of the inequality (greater than or equal to)")
43
+
44
+ @model_validator(mode="after")
45
+ def check_inequality(self):
46
+ if self.LE is not None and self.GE is not None and self.LE < self.GE:
47
+ raise ValueError(
48
+ f"LE {self.LE} must be greater than or equal to GE {self.GE}"
49
+ )
50
+ if self.LE is None and self.GE is None:
51
+ raise ValueError("At least one of LE or GE must be defined")
52
+ return self
53
+
54
+ def evaluate(self, value: float) -> bool:
55
+ if self.LE is not None and value > self.LE:
56
+ return False
57
+ if self.GE is not None and value < self.GE:
58
+ return False
59
+ return True
60
+
61
+ def to_str(self, variable):
62
+ if self.LE is not None and self.GE is not None:
63
+ return f"{self.GE} <= {variable} <= {self.LE}"
64
+ elif self.LE is not None:
65
+ return f"{variable} <= {self.LE}"
66
+ elif self.GE is not None:
67
+ return f"{variable} >= {self.GE}"
68
+ raise ValueError("Inequality must have at least one of LE or GE defined")
69
+
70
+
71
+ class PumpCondition(StrictModel):
72
+ state: PumpState | None = Field(None, description="The required state of the pump for the transition to be valid")
73
+ time: float | None = Field(None, description="Time in seconds that the pump must be in the required state before the transition is valid")
74
+ current: Inequality | None = Field(None, description="Require the pump current draw to be within the range specified by the inequality for the transition to be valid")
75
+ voltage: Inequality | None = Field(None, description="Require the pump voltage to be within the range specified by the inequality for the transition to be valid")
76
+ flow: Inequality | None = Field(None, description="Require the pump flow rate to be within the range specified by the inequality for the transition to be valid")
77
+ speed: Inequality | None = Field(None, description="Require the pump speed to be within the range specified by the inequality for the transition to be valid")
78
+
79
+ @model_validator(mode="before")
80
+ def str2model(cls, v):
81
+ if isinstance(v, str):
82
+ return {"state": v}
83
+ return v
84
+
85
+
86
+ class SafetyConditions(StrictModel):
87
+ pump: dict[str, PumpCondition] | None = Field(None, description="A mapping of pump names to conditions under which the transition should be automatically executed to prevent damage to the system")
88
+
89
+ def _check_pumps(self, pumps: list[str]):
90
+ if self.pump is not None:
91
+ for pump in self.pump.keys():
92
+ if pump not in pumps:
93
+ raise ValueError(f"Pump {pump} not in pumps list")
94
+
95
+
96
+ class PressureChange(StrictModel):
97
+ time: float = Field(..., description="Time in minutes that it is expected it will take to reach the specified pressure")
98
+ gauge: str = Field(..., description="Name of the gauge for the pressure change")
99
+ pressure: float = Field(..., description="Expected gauge pressure after the specified time has elapsed")
100
+
101
+ @model_validator(mode="before")
102
+ def dict2fields(cls, raw_data):
103
+ transformed_data = {}
104
+ for key, val in raw_data.items():
105
+ if key not in cls.model_fields:
106
+ if isinstance(key, str):
107
+ transformed_data["gauge"] = key
108
+ transformed_data["pressure"] = val
109
+ continue
110
+ transformed_data[key] = val
111
+ return transformed_data
112
+
113
+ def _check_gauges(self, gauges: list[str]):
114
+ if self.gauge not in gauges:
115
+ raise ValueError(f"Gauge {self.gauge} not in gauges list")
116
+
117
+
118
+ class ValveCondition(StrictModel):
119
+ state: ValveState = Field(..., description="Required state of the valve")
120
+ time: float | None = Field(None, description="Time in seconds that the valve must be in the required state before the transition is valid")
121
+
122
+ @model_validator(mode="before")
123
+ def str2model(cls, v):
124
+ if isinstance(v, str):
125
+ return {"state": v}
126
+ return v
127
+
128
+
129
+ class TransitionConditions(StrictModel):
130
+ gauge: dict[str, Inequality] = Field({}, description="A mapping of gauge names to pressure inequalities that must be satisfied for the transition to be valid")
131
+ pump: dict[str, PumpCondition] = Field({}, description="A mapping of pump names to conditions that must be satisfied for the transition to be valid")
132
+ valve: dict[str, ValveCondition] = Field({}, description="A mapping of valve names to conditions that must be satisfied for the transition to be valid")
133
+ stage: str | None = Field(None, description="Current stage position required for the transition to be valid")
134
+
135
+ def _check_valves(self, valves: list[str]):
136
+ if self.valve is not None:
137
+ for valve in self.valve.keys():
138
+ if valve not in valves:
139
+ raise ValueError(f"Valve {valve} not in valves list")
140
+
141
+ def _check_gauges(self, gauges: list[str]):
142
+ if self.gauge is not None:
143
+ for gauge in self.gauge.keys():
144
+ if gauge not in gauges:
145
+ raise ValueError(f"Gauge {gauge} not in gauges list")
146
+
147
+ def _check_pumps(self, pumps: list[str]):
148
+ if self.pump is not None:
149
+ for pump in self.pump.keys():
150
+ if pump not in pumps:
151
+ raise ValueError(f"Pump {pump} not in pumps list")
152
+
153
+ def _check_stage(self, stages: list[str]):
154
+ if self.stage is not None and self.stage not in stages:
155
+ raise ValueError(f"Stage position {self.stage} not in stage position list")
156
+
157
+
158
+ class Transition(StrictModel):
159
+ stage: bool = Field(False, description="Set to true if this transition is a stage movement")
160
+ valve: str | None = Field(None, description="Name of the valve to be actuated in this transition")
161
+ pump: str | None = Field(None, description="Name of the pump to be actuated in this transition")
162
+ from_state: ValveState | PumpState | str = Field(
163
+ ..., validation_alias="from", serialization_alias="from", description="State of the valve, pump, or stage before the transition"
164
+ )
165
+ to_state: ValveState | PumpState | str = Field(
166
+ ..., validation_alias="to", serialization_alias="to", description="State of the valve, pump, or stage after the transition"
167
+ )
168
+ condition: TransitionConditions | None = Field(None, description="Conditions that must be met for this transition to be valid")
169
+ wait: dict[str, Inequality] = Field({}, description="A mapping of gauge names to pressure inequalities that should be satisfied before this transition is executed when automated pumping is enabled")
170
+ safety: SafetyConditions | None = Field(None, description="Safety conditions under which this transition should be automatically executed to prevent damage to the system")
171
+ pressure: list[PressureChange] = Field([], description="A list of pressure changes that are expected if this transition is executed")
172
+
173
+ @model_validator(mode="before")
174
+ def pressure2list(cls, v):
175
+ if "pressure" in v and isinstance(v["pressure"], dict):
176
+ v["pressure"] = [v["pressure"]]
177
+ return v
178
+
179
+ @model_validator(mode="after")
180
+ def from_to_state2enum(self):
181
+ if self.valve is not None:
182
+ self.from_state = ValveState(self.from_state)
183
+ self.to_state = ValveState(self.to_state)
184
+ if self.pump is not None:
185
+ self.from_state = PumpState(self.from_state)
186
+ self.to_state = PumpState(self.to_state)
187
+ return self
188
+
189
+ def _check_valves(self, valves: list[str]):
190
+ if self.valve is not None and self.valve not in valves:
191
+ raise ValueError(f"Valve {self.valve} not in valves list")
192
+ if self.condition is not None:
193
+ self.condition._check_valves(valves)
194
+
195
+ def _check_gauges(self, gauges: list[str]):
196
+ if self.condition is not None:
197
+ self.condition._check_gauges(gauges)
198
+ if self.pressure is not None:
199
+ for pressure_change in self.pressure:
200
+ pressure_change._check_gauges(gauges)
201
+ if self.wait is not None:
202
+ for gauge in self.wait.keys():
203
+ if gauge not in gauges:
204
+ raise ValueError(f"Gauge {gauge} not in gauges list")
205
+
206
+ def _check_pumps(self, pumps: list[str]):
207
+ if self.pump is not None and self.pump not in pumps:
208
+ raise ValueError(f"Pump {self.pump} not in pumps list")
209
+ if self.safety is not None:
210
+ self.safety._check_pumps(pumps)
211
+ if self.condition is not None:
212
+ self.condition._check_pumps(pumps)
213
+
214
+ def _check_stage(self, stages: list[str]):
215
+ if self.condition is not None:
216
+ self.condition._check_stage(stages)
217
+ if self.stage:
218
+ if self.from_state not in stages:
219
+ raise ValueError(
220
+ f"Stage position {self.from_state} not in stage position list"
221
+ )
222
+ if self.to_state not in stages:
223
+ raise ValueError(
224
+ f"Stage position {self.to_state} not in stage position list"
225
+ )
226
+
227
+
228
+ class Confirm(StrictModel):
229
+ default: bool = Field(False, description="Default confirmation requirement for transitions")
230
+ pump: dict[str, PumpState | bool] | None = Field(None, description="Require confirmation before turning pumps on or off, or True to require confirmation for all transitions of a specific pump")
231
+ stage: list[str] | None = Field(None, description="Require confirmation before moving the stage to any of these positions")
232
+ valve: dict[str, ValveState | bool] | None = Field(None, description="Require confirmation before opening or closing valves, or True to require confirmation for all transitions of a specific valve")
233
+
234
+ def _check_valves(self, valves: list[str]):
235
+ if self.valve is not None:
236
+ for valve in self.valve.keys():
237
+ if valve not in valves:
238
+ raise ValueError(f"Valve {valve} not in valves list")
239
+
240
+ def _check_pumps(self, pumps: list[str]):
241
+ if self.pump is not None:
242
+ for pump in self.pump.keys():
243
+ if pump not in pumps:
244
+ raise ValueError(f"Pump {pump} not in pumps list")
245
+
246
+ def _check_stage(self, stages: list[str]):
247
+ if self.stage is not None:
248
+ for position in self.stage:
249
+ if position not in stages:
250
+ raise ValueError(
251
+ f"Stage position {position} not in stage position list"
252
+ )
253
+
254
+
255
+ class InitialState(StrictModel):
256
+ valves: dict[str, ValveState] | None = Field(None, description="Initial state of each valve")
257
+ pumps: dict[str, PumpState] | None = Field(None, description="Initial state of each pump")
258
+ stage: str | None = Field(None, description="Initial stage location")
259
+ gauges: dict[str, float] | None = Field(None, description="Initial pressure of each gauge")
260
+
261
+ def _check_valves(self, valves: list[str]):
262
+ for valve in valves:
263
+ if self.valves is None or valve not in self.valves.keys():
264
+ raise ValueError(f"Valve {valve} not in initial state")
265
+
266
+ def _check_gauges(self, gauges: list[str]):
267
+ for gauge in gauges:
268
+ if self.gauges is None or gauge not in self.gauges.keys():
269
+ raise ValueError(f"Gauge {gauge} not in initial state")
270
+
271
+ def _check_pumps(self, pumps: list[str]):
272
+ for pump in pumps:
273
+ if self.pumps is None or pump not in self.pumps.keys():
274
+ raise ValueError(f"Pump {pump} not in initial state")
275
+
276
+ def _check_stage(self, stages: list[str]):
277
+ if len(stages) > 0:
278
+ if self.stage is None:
279
+ raise ValueError(f"Stage position not in initial state")
280
+ if self.stage not in stages:
281
+ raise ValueError(
282
+ f"Initial stage position {self.stage} not in stage position list"
283
+ )
284
+
285
+
286
+ class Config(StrictModel):
287
+ valves: list[str] = Field(..., description="List of all valve names")
288
+ gauges: list[str] = Field(..., description="List of all gauge names")
289
+ pumps: list[str] = Field(..., description="List of all pump names")
290
+ stage: list[str] = Field(..., description="List of all stage positions")
291
+ named_states: dict[str, NamedState] = Field(..., description="A mapping of names of named states to their corresponding state definitions")
292
+ transitions: list[Transition] = Field(..., description="List of all possible transitions")
293
+ confirm: Confirm = Field(..., description="Confirmation requirements for transitions when automated pumping is enabled")
294
+ initial_states: list[InitialState] = Field(..., description="List of initial states to use when building the state graph")
295
+
296
+ @field_validator("initial_states")
297
+ def check_initial_states(cls, v):
298
+ if len(v) == 0:
299
+ raise ValueError("At least one initial state must be defined")
300
+ return v
301
+
302
+ @model_validator(mode="after")
303
+ def check_valves(self):
304
+ for transition in self.transitions:
305
+ transition._check_valves(self.valves)
306
+ self.confirm._check_valves(self.valves)
307
+ for state in self.named_states.values():
308
+ state._check_valves(self.valves)
309
+ for state in self.initial_states:
310
+ state._check_valves(self.valves)
311
+ return self
312
+
313
+ @model_validator(mode="after")
314
+ def check_gauges(self):
315
+ for transition in self.transitions:
316
+ transition._check_gauges(self.gauges)
317
+ for state in self.initial_states:
318
+ state._check_gauges(self.gauges)
319
+ return self
320
+
321
+ @model_validator(mode="after")
322
+ def check_pumps(self):
323
+ for transition in self.transitions:
324
+ transition._check_pumps(self.pumps)
325
+ self.confirm._check_pumps(self.pumps)
326
+ for state in self.named_states.values():
327
+ state._check_pumps(self.pumps)
328
+ for state in self.initial_states:
329
+ state._check_pumps(self.pumps)
330
+ return self
331
+
332
+ @model_validator(mode="after")
333
+ def check_stage(self):
334
+ for transition in self.transitions:
335
+ transition._check_stage(self.stage)
336
+ self.confirm._check_stage(self.stage)
337
+ for state in self.named_states.values():
338
+ state._check_stage(self.stage)
339
+ for state in self.initial_states:
340
+ state._check_stage(self.stage)
341
+ return self
342
+
343
+ @classmethod
344
+ def load_file(cls, path):
345
+ with open(path) as f:
346
+ return cls.load_yaml(f.read())
347
+
348
+ @classmethod
349
+ def load_yaml(cls, yaml_str):
350
+ data = yaml.safe_load(yaml_str)
351
+ return cls(**data)
auto_pumping/dummy.py ADDED
@@ -0,0 +1,49 @@
1
+ from . import AutoPumping
2
+ import random
3
+
4
+
5
+ class DummyPumping(AutoPumping):
6
+ def __init__(self, config):
7
+ self._valve_states = {valve: None for valve in config.valves}
8
+ self._pump_states = {pump: None for pump in config.pumps}
9
+ self._stage_position = None
10
+ super().__init__(config)
11
+
12
+ def actuate_valve(self, valve, state):
13
+ self._valve_states[valve] = state
14
+
15
+ def get_valve_state(self, valve):
16
+ return self._valve_states[valve]
17
+
18
+ def set_pump(self, pump, state):
19
+ self._pump_states[pump] = state
20
+
21
+ def get_pump_state(self, pump):
22
+ return self._pump_states[pump]
23
+
24
+ def get_pump_current(self, pump):
25
+ if pump == "turbo":
26
+ return random.uniform(0.0, 2.0)
27
+ return None
28
+
29
+ def get_pump_voltage(self, pump):
30
+ if pump == "turbo":
31
+ return random.uniform(23.0, 25.0)
32
+ return None
33
+
34
+ def get_pump_flow(self, pump):
35
+ return None
36
+
37
+ def get_pump_speed(self, pump):
38
+ if pump == "turbo":
39
+ return random.uniform(998.0, 1002.0)
40
+ return None
41
+
42
+ def get_stage_position(self):
43
+ return self._stage_position
44
+
45
+ def move_stage(self, position):
46
+ self._stage_position = position
47
+
48
+ def get_gauge_pressure(self, gauge):
49
+ return random.uniform(1e-6, 1e-5)