mycorrhizal 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.
- mycorrhizal/__init__.py +3 -0
- mycorrhizal/common/__init__.py +68 -0
- mycorrhizal/common/interface_builder.py +203 -0
- mycorrhizal/common/interfaces.py +412 -0
- mycorrhizal/common/timebase.py +99 -0
- mycorrhizal/common/wrappers.py +532 -0
- mycorrhizal/enoki/__init__.py +0 -0
- mycorrhizal/enoki/core.py +1545 -0
- mycorrhizal/enoki/testing_utils.py +529 -0
- mycorrhizal/enoki/util.py +220 -0
- mycorrhizal/hypha/__init__.py +0 -0
- mycorrhizal/hypha/core/__init__.py +107 -0
- mycorrhizal/hypha/core/builder.py +404 -0
- mycorrhizal/hypha/core/runtime.py +890 -0
- mycorrhizal/hypha/core/specs.py +234 -0
- mycorrhizal/hypha/util.py +38 -0
- mycorrhizal/rhizomorph/README.md +220 -0
- mycorrhizal/rhizomorph/__init__.py +0 -0
- mycorrhizal/rhizomorph/core.py +1729 -0
- mycorrhizal/rhizomorph/util.py +45 -0
- mycorrhizal/spores/__init__.py +124 -0
- mycorrhizal/spores/cache.py +208 -0
- mycorrhizal/spores/core.py +419 -0
- mycorrhizal/spores/dsl/__init__.py +48 -0
- mycorrhizal/spores/dsl/enoki.py +514 -0
- mycorrhizal/spores/dsl/hypha.py +399 -0
- mycorrhizal/spores/dsl/rhizomorph.py +351 -0
- mycorrhizal/spores/encoder/__init__.py +11 -0
- mycorrhizal/spores/encoder/base.py +42 -0
- mycorrhizal/spores/encoder/json.py +159 -0
- mycorrhizal/spores/extraction.py +484 -0
- mycorrhizal/spores/models.py +288 -0
- mycorrhizal/spores/transport/__init__.py +10 -0
- mycorrhizal/spores/transport/base.py +46 -0
- mycorrhizal-0.1.0.dist-info/METADATA +198 -0
- mycorrhizal-0.1.0.dist-info/RECORD +37 -0
- mycorrhizal-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hypha DSL - Runtime Layer
|
|
4
|
+
|
|
5
|
+
Runtime objects that execute the Petri net specification.
|
|
6
|
+
Manages token flow, transition firing, and asyncio task coordination.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from asyncio import Event, Task
|
|
11
|
+
from typing import Any, List, Dict, Optional, Set, Tuple, Callable, Deque, Union
|
|
12
|
+
from collections import deque
|
|
13
|
+
from itertools import product
|
|
14
|
+
import inspect
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from .specs import NetSpec, PlaceSpec, TransitionSpec, ArcSpec, PlaceType, GuardSpec
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ======================================================================================
|
|
23
|
+
# Interface Integration Helper
|
|
24
|
+
# ======================================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _create_interface_view_if_needed(bb: Any, handler: Callable) -> Any:
|
|
28
|
+
"""
|
|
29
|
+
Create a constrained view if the handler has an interface type hint on its
|
|
30
|
+
blackboard parameter (second parameter, typically named 'bb').
|
|
31
|
+
|
|
32
|
+
This enables type-safe, constrained access to blackboard state based on
|
|
33
|
+
interface definitions created with @blackboard_interface.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
bb: The blackboard instance
|
|
37
|
+
handler: The transition or IO handler function to check for interface type hints
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Either the original blackboard or a constrained view based on interface metadata
|
|
41
|
+
"""
|
|
42
|
+
from typing import get_type_hints
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
sig = inspect.signature(handler)
|
|
46
|
+
params = list(sig.parameters.values())
|
|
47
|
+
|
|
48
|
+
# Check second parameter (usually 'bb' at index 1)
|
|
49
|
+
# Transition handlers have signature: handler(consumed, bb, timebase, state?)
|
|
50
|
+
# IO input handlers have signature: handler(bb, timebase)
|
|
51
|
+
if len(params) >= 2:
|
|
52
|
+
# For transitions, bb is at index 1
|
|
53
|
+
# For IO input with 2 params, bb is at index 0
|
|
54
|
+
bb_param_index = 1 if len(params) >= 3 else 0
|
|
55
|
+
|
|
56
|
+
if params[bb_param_index].name == 'bb':
|
|
57
|
+
bb_type = get_type_hints(handler).get('bb')
|
|
58
|
+
|
|
59
|
+
# If type hint exists and has interface metadata
|
|
60
|
+
if bb_type and hasattr(bb_type, '_readonly_fields'):
|
|
61
|
+
from mycorrhizal.common.wrappers import create_view_from_protocol
|
|
62
|
+
return create_view_from_protocol(bb, bb_type)
|
|
63
|
+
except Exception:
|
|
64
|
+
# If anything goes wrong with type inspection, fall back to original bb
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return bb
|
|
68
|
+
try:
|
|
69
|
+
# Library should not configure root logging; be quiet by default
|
|
70
|
+
logger.addHandler(logging.NullHandler())
|
|
71
|
+
except Exception:
|
|
72
|
+
# NullHandler may not be available in very old Pythons; ignore if so
|
|
73
|
+
pass
|
|
74
|
+
# Library does not configure handlers by default. Callers may configure logging.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PlaceRuntime:
|
|
78
|
+
"""Runtime execution of a place"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, spec: PlaceSpec, bb: Any, timebase: Any, fqn: str):
|
|
81
|
+
self.spec = spec
|
|
82
|
+
self.fqn = fqn
|
|
83
|
+
self.bb = bb
|
|
84
|
+
self.timebase = timebase
|
|
85
|
+
self.state = spec.state_factory() if spec.state_factory else None
|
|
86
|
+
|
|
87
|
+
# tokens can be a bag (list) or queue (deque)
|
|
88
|
+
self.tokens: Union[List[Any], Deque[Any]]
|
|
89
|
+
if spec.place_type == PlaceType.BAG:
|
|
90
|
+
self.tokens = []
|
|
91
|
+
else: # QUEUE
|
|
92
|
+
self.tokens = deque()
|
|
93
|
+
|
|
94
|
+
self.token_added_event = Event()
|
|
95
|
+
self.token_removed_event = Event()
|
|
96
|
+
|
|
97
|
+
self.io_task: Optional[Task] = None
|
|
98
|
+
|
|
99
|
+
def add_token(self, token: Any):
|
|
100
|
+
"""Add a token to this place"""
|
|
101
|
+
if self.spec.place_type == PlaceType.BAG:
|
|
102
|
+
self.tokens.append(token)
|
|
103
|
+
else: # QUEUE
|
|
104
|
+
self.tokens.append(token)
|
|
105
|
+
|
|
106
|
+
self.token_added_event.set()
|
|
107
|
+
|
|
108
|
+
def remove_tokens(self, tokens_to_remove: List[Any]):
|
|
109
|
+
"""Remove specific tokens from this place"""
|
|
110
|
+
if self.spec.place_type == PlaceType.BAG:
|
|
111
|
+
for token in tokens_to_remove:
|
|
112
|
+
self.tokens.remove(token)
|
|
113
|
+
else: # QUEUE
|
|
114
|
+
temp = deque()
|
|
115
|
+
for token in self.tokens:
|
|
116
|
+
if token not in tokens_to_remove:
|
|
117
|
+
temp.append(token)
|
|
118
|
+
self.tokens = temp
|
|
119
|
+
|
|
120
|
+
self.token_removed_event.set()
|
|
121
|
+
self.token_removed_event.clear()
|
|
122
|
+
|
|
123
|
+
def peek_tokens(self, count: int) -> List[Any]:
|
|
124
|
+
"""Peek at tokens without removing them"""
|
|
125
|
+
if len(self.tokens) < count:
|
|
126
|
+
return []
|
|
127
|
+
# Normalize to a list slice for both BAG and QUEUE
|
|
128
|
+
return list(self.tokens)[:count]
|
|
129
|
+
|
|
130
|
+
async def start_io_input(self):
|
|
131
|
+
"""Start IOInputPlace generator task"""
|
|
132
|
+
if not self.spec.is_io_input or not self.spec.handler:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Create interface view if handler has interface type hint
|
|
136
|
+
bb_to_pass = _create_interface_view_if_needed(self.bb, self.spec.handler)
|
|
137
|
+
|
|
138
|
+
async def io_input_loop():
|
|
139
|
+
try:
|
|
140
|
+
sig = inspect.signature(self.spec.handler)
|
|
141
|
+
if len(sig.parameters) == 2:
|
|
142
|
+
gen = self.spec.handler(bb_to_pass, self.timebase)
|
|
143
|
+
else:
|
|
144
|
+
gen = self.spec.handler()
|
|
145
|
+
|
|
146
|
+
async for token in gen:
|
|
147
|
+
self.add_token(token)
|
|
148
|
+
except asyncio.CancelledError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
self.io_task = asyncio.create_task(io_input_loop())
|
|
152
|
+
|
|
153
|
+
async def handle_io_output(self, token: Any):
|
|
154
|
+
"""Handle IOOutputPlace token"""
|
|
155
|
+
if not self.spec.is_io_output or not self.spec.handler:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Create interface view if handler has interface type hint
|
|
159
|
+
bb_to_pass = _create_interface_view_if_needed(self.bb, self.spec.handler)
|
|
160
|
+
|
|
161
|
+
sig = inspect.signature(self.spec.handler)
|
|
162
|
+
if len(sig.parameters) == 3:
|
|
163
|
+
await self.spec.handler(token, bb_to_pass, self.timebase)
|
|
164
|
+
else:
|
|
165
|
+
await self.spec.handler(token)
|
|
166
|
+
|
|
167
|
+
async def cancel(self):
|
|
168
|
+
"""Cancel any running IO tasks"""
|
|
169
|
+
if self.io_task:
|
|
170
|
+
self.io_task.cancel()
|
|
171
|
+
try:
|
|
172
|
+
await self.io_task
|
|
173
|
+
except asyncio.CancelledError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TransitionRuntime:
|
|
178
|
+
"""Runtime execution of a transition"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, spec: TransitionSpec, net: 'NetRuntime', fqn: str):
|
|
181
|
+
self.spec = spec
|
|
182
|
+
self.fqn = fqn
|
|
183
|
+
self.net = net
|
|
184
|
+
self.state = spec.state_factory() if spec.state_factory else None
|
|
185
|
+
# input_arcs now use canonical parts tuples as keys
|
|
186
|
+
self.input_arcs: List[Tuple[Tuple[str, ...], ArcSpec]] = []
|
|
187
|
+
self.output_arcs: List[ArcSpec] = []
|
|
188
|
+
|
|
189
|
+
self.task: Optional[Task] = None
|
|
190
|
+
self._stop_event = Event()
|
|
191
|
+
|
|
192
|
+
def add_input_arc(self, place_parts: Tuple[str, ...], arc: ArcSpec):
|
|
193
|
+
"""Register an input arc"""
|
|
194
|
+
self.input_arcs.append((place_parts, arc))
|
|
195
|
+
|
|
196
|
+
def add_output_arc(self, arc: ArcSpec):
|
|
197
|
+
"""Register an output arc"""
|
|
198
|
+
self.output_arcs.append(arc)
|
|
199
|
+
|
|
200
|
+
def _generate_token_combinations(self) -> List[Tuple[Tuple[Any, ...], ...]]:
|
|
201
|
+
"""
|
|
202
|
+
Generate all valid token combinations for input arcs.
|
|
203
|
+
Returns list of tuples, where each tuple contains sub-tuples for each arc.
|
|
204
|
+
"""
|
|
205
|
+
arc_tokens = []
|
|
206
|
+
|
|
207
|
+
for place_parts, arc in self.input_arcs:
|
|
208
|
+
place = self.net.places[place_parts]
|
|
209
|
+
tokens = place.peek_tokens(arc.weight)
|
|
210
|
+
|
|
211
|
+
if len(tokens) < arc.weight:
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
if arc.weight == 1:
|
|
215
|
+
arc_tokens.append([(t,) for t in tokens])
|
|
216
|
+
else:
|
|
217
|
+
from itertools import combinations
|
|
218
|
+
arc_tokens.append(list(combinations(tokens, arc.weight)))
|
|
219
|
+
|
|
220
|
+
if not arc_tokens:
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
all_combinations = list(product(*arc_tokens))
|
|
224
|
+
return all_combinations
|
|
225
|
+
|
|
226
|
+
async def _check_guard(self, combinations: List[Tuple[Tuple[Any, ...], ...]]) -> Optional[Tuple[Tuple[Any, ...], ...]]:
|
|
227
|
+
"""Check guard and return combination to consume, or None"""
|
|
228
|
+
if not self.spec.guard:
|
|
229
|
+
return combinations[0] if combinations else None
|
|
230
|
+
|
|
231
|
+
guard_func = self.spec.guard.func
|
|
232
|
+
guard_result = guard_func(combinations, self.net.bb, self.net.timebase)
|
|
233
|
+
|
|
234
|
+
if inspect.isgenerator(guard_result):
|
|
235
|
+
for result in guard_result:
|
|
236
|
+
if result is not None:
|
|
237
|
+
return result
|
|
238
|
+
elif inspect.isasyncgen(guard_result):
|
|
239
|
+
async for result in guard_result:
|
|
240
|
+
if result is not None:
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
async def _fire_transition(self, consumed_combination: Tuple[Tuple[Any, ...], ...]):
|
|
246
|
+
"""Execute the transition with consumed tokens"""
|
|
247
|
+
consumed_flat = []
|
|
248
|
+
tokens_to_remove = {}
|
|
249
|
+
|
|
250
|
+
for i, (place_parts, arc) in enumerate(self.input_arcs):
|
|
251
|
+
arc_tokens = consumed_combination[i]
|
|
252
|
+
consumed_flat.extend(arc_tokens)
|
|
253
|
+
|
|
254
|
+
if place_parts not in tokens_to_remove:
|
|
255
|
+
tokens_to_remove[place_parts] = []
|
|
256
|
+
tokens_to_remove[place_parts].extend(arc_tokens)
|
|
257
|
+
|
|
258
|
+
for place_parts, tokens in tokens_to_remove.items():
|
|
259
|
+
self.net.places[place_parts].remove_tokens(tokens)
|
|
260
|
+
|
|
261
|
+
# Create interface view if handler has interface type hint
|
|
262
|
+
bb_to_pass = _create_interface_view_if_needed(self.net.bb, self.spec.handler)
|
|
263
|
+
|
|
264
|
+
sig = inspect.signature(self.spec.handler)
|
|
265
|
+
param_count = len(sig.parameters)
|
|
266
|
+
|
|
267
|
+
if self.state is not None:
|
|
268
|
+
logger.debug("[fire] %s consumed=%s", self.fqn, consumed_flat)
|
|
269
|
+
results = self.spec.handler(consumed_flat, bb_to_pass, self.net.timebase, self.state)
|
|
270
|
+
else:
|
|
271
|
+
logger.debug("[fire] %s consumed=%s", self.fqn, consumed_flat)
|
|
272
|
+
results = self.spec.handler(consumed_flat, bb_to_pass, self.net.timebase)
|
|
273
|
+
logger.debug("[fire] results_type=%s isasyncgen=%s iscoroutine=%s",
|
|
274
|
+
type(results), inspect.isasyncgen(results), inspect.iscoroutine(results))
|
|
275
|
+
|
|
276
|
+
if inspect.isasyncgen(results):
|
|
277
|
+
async for yielded in results:
|
|
278
|
+
await self._process_yield(yielded)
|
|
279
|
+
elif inspect.iscoroutine(results):
|
|
280
|
+
result = await results
|
|
281
|
+
if result is not None:
|
|
282
|
+
await self._process_yield(result)
|
|
283
|
+
|
|
284
|
+
def _resolve_place_ref(self, place_ref) -> Optional[Tuple[str, ...]]:
|
|
285
|
+
"""Resolve a PlaceRef to runtime place parts.
|
|
286
|
+
|
|
287
|
+
Attempts multiple resolution strategies in order:
|
|
288
|
+
1. Exact match using PlaceRef.get_parts()
|
|
289
|
+
2. Parent-relative mapping (for subnet instances)
|
|
290
|
+
3. Suffix fallback (match by local name)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Tuple of place parts if found, None otherwise
|
|
294
|
+
"""
|
|
295
|
+
# Try to get parts from the PlaceRef API
|
|
296
|
+
try:
|
|
297
|
+
parts = tuple(place_ref.get_parts())
|
|
298
|
+
except Exception:
|
|
299
|
+
parts = None
|
|
300
|
+
|
|
301
|
+
logger.debug("[resolve] place_ref=%r parts=%s", place_ref, parts)
|
|
302
|
+
|
|
303
|
+
if parts:
|
|
304
|
+
key = tuple(parts)
|
|
305
|
+
if key in self.net.places:
|
|
306
|
+
logger.debug("[resolve] exact match parts=%s", parts)
|
|
307
|
+
return key
|
|
308
|
+
|
|
309
|
+
# Map relative to this transition's parent parts
|
|
310
|
+
trans_parts = tuple(self.fqn.split('.'))
|
|
311
|
+
if len(trans_parts) > 1:
|
|
312
|
+
parent_prefix = trans_parts[:-1]
|
|
313
|
+
candidate = tuple(list(parent_prefix) + [place_ref.local_name])
|
|
314
|
+
if candidate in self.net.places:
|
|
315
|
+
logger.debug("[resolve] mapped %s -> %s using parent_prefix", parts, candidate)
|
|
316
|
+
return candidate
|
|
317
|
+
|
|
318
|
+
# Fallback: find any place whose last segment matches local_name
|
|
319
|
+
for p in self.net.places.keys():
|
|
320
|
+
if isinstance(p, tuple):
|
|
321
|
+
segs = list(p)
|
|
322
|
+
else:
|
|
323
|
+
segs = p.split('.')
|
|
324
|
+
if segs and segs[-1] == place_ref.local_name:
|
|
325
|
+
logger.debug("[resolve] suffix match %s -> %s", place_ref.local_name, p)
|
|
326
|
+
return tuple(segs)
|
|
327
|
+
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _normalize_to_parts(self, key) -> Tuple[str, ...]:
|
|
331
|
+
"""Normalize a place key to tuple of parts for runtime lookup.
|
|
332
|
+
|
|
333
|
+
Handles various input formats:
|
|
334
|
+
- tuple: returned as-is
|
|
335
|
+
- list: converted to tuple
|
|
336
|
+
- str: split on '.' and converted to tuple
|
|
337
|
+
- other: stringified and split on '.'
|
|
338
|
+
"""
|
|
339
|
+
if isinstance(key, tuple):
|
|
340
|
+
return key
|
|
341
|
+
if isinstance(key, list):
|
|
342
|
+
return tuple(key)
|
|
343
|
+
if isinstance(key, str):
|
|
344
|
+
return tuple(key.split('.'))
|
|
345
|
+
# unknown type; try to stringify
|
|
346
|
+
return tuple(str(key).split('.'))
|
|
347
|
+
|
|
348
|
+
async def _add_token_to_place(self, place_key: Tuple[str, ...], token: Any):
|
|
349
|
+
"""Add a token to the specified place, handling IO output places.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
place_key: Tuple of parts identifying the place
|
|
353
|
+
token: Token to add
|
|
354
|
+
"""
|
|
355
|
+
if place_key in self.net.places:
|
|
356
|
+
place = self.net.places[place_key]
|
|
357
|
+
if place.spec.is_io_output:
|
|
358
|
+
logger.debug("[process_yield] calling io_output on %s token=%r", place_key, token)
|
|
359
|
+
await place.handle_io_output(token)
|
|
360
|
+
else:
|
|
361
|
+
logger.debug("[process_yield] adding token to %s token=%r", place_key, token)
|
|
362
|
+
place.add_token(token)
|
|
363
|
+
|
|
364
|
+
async def _process_yield(self, yielded):
|
|
365
|
+
"""Process yielded output from transition"""
|
|
366
|
+
if isinstance(yielded, dict):
|
|
367
|
+
await self._process_dict_yield(yielded)
|
|
368
|
+
else:
|
|
369
|
+
await self._process_single_yield(yielded)
|
|
370
|
+
|
|
371
|
+
async def _process_dict_yield(self, yielded: dict):
|
|
372
|
+
"""Process dictionary with place keys and token values.
|
|
373
|
+
|
|
374
|
+
Supports:
|
|
375
|
+
- Explicit place -> token mappings
|
|
376
|
+
- Wildcard ('*') token that goes to all output places
|
|
377
|
+
"""
|
|
378
|
+
wildcard_token = yielded.get('*')
|
|
379
|
+
explicit_targets = set()
|
|
380
|
+
|
|
381
|
+
for key, token in yielded.items():
|
|
382
|
+
if key == '*':
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
place_parts = self._resolve_place_ref(key)
|
|
386
|
+
if place_parts is None:
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
key_normalized = self._normalize_to_parts(place_parts)
|
|
390
|
+
explicit_targets.add(key_normalized)
|
|
391
|
+
await self._add_token_to_place(key_normalized, token)
|
|
392
|
+
|
|
393
|
+
if wildcard_token is not None:
|
|
394
|
+
await self._expand_wildcard_to_outputs(wildcard_token, explicit_targets)
|
|
395
|
+
|
|
396
|
+
async def _expand_wildcard_to_outputs(self, wildcard_token: Any, explicit_targets: set):
|
|
397
|
+
"""Expand a wildcard token to all output places not explicitly targeted.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
wildcard_token: Token to distribute to all output places
|
|
401
|
+
explicit_targets: Set of place keys already explicitly targeted
|
|
402
|
+
"""
|
|
403
|
+
for arc in self.output_arcs:
|
|
404
|
+
# normalize target to parts
|
|
405
|
+
try:
|
|
406
|
+
target_parts = tuple(arc.target.get_parts())
|
|
407
|
+
target_key = target_parts
|
|
408
|
+
except Exception:
|
|
409
|
+
# fallback if target is a string
|
|
410
|
+
target_key = self._normalize_to_parts(arc.target)
|
|
411
|
+
|
|
412
|
+
if target_key not in explicit_targets:
|
|
413
|
+
await self._add_token_to_place(target_key, wildcard_token)
|
|
414
|
+
|
|
415
|
+
async def _process_single_yield(self, yielded):
|
|
416
|
+
"""Process a single (place_ref, token) tuple yield."""
|
|
417
|
+
place_ref, token = yielded
|
|
418
|
+
place_parts = self._resolve_place_ref(place_ref)
|
|
419
|
+
|
|
420
|
+
if place_parts is None:
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
key = self._normalize_to_parts(place_parts)
|
|
424
|
+
await self._add_token_to_place(key, token)
|
|
425
|
+
|
|
426
|
+
async def run(self):
|
|
427
|
+
"""Main transition execution loop"""
|
|
428
|
+
try:
|
|
429
|
+
while not self._stop_event.is_set():
|
|
430
|
+
wait_tasks = []
|
|
431
|
+
for place_fqn, arc in self.input_arcs:
|
|
432
|
+
place = self.net.places[place_fqn]
|
|
433
|
+
wait_tasks.append(asyncio.create_task(place.token_added_event.wait()))
|
|
434
|
+
|
|
435
|
+
if not wait_tasks:
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
stop_task = asyncio.create_task(self._stop_event.wait())
|
|
439
|
+
wait_tasks.append(stop_task)
|
|
440
|
+
|
|
441
|
+
done, pending = await asyncio.wait(
|
|
442
|
+
wait_tasks,
|
|
443
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
for task in pending:
|
|
447
|
+
task.cancel()
|
|
448
|
+
|
|
449
|
+
if self._stop_event.is_set():
|
|
450
|
+
break
|
|
451
|
+
# Debug: indicate transition woke
|
|
452
|
+
logger.debug("[trans] %s woke; checking inputs", self.fqn)
|
|
453
|
+
|
|
454
|
+
for place_fqn, arc in self.input_arcs:
|
|
455
|
+
place = self.net.places[place_fqn]
|
|
456
|
+
place.token_added_event.clear()
|
|
457
|
+
|
|
458
|
+
combinations = self._generate_token_combinations()
|
|
459
|
+
logger.debug("[trans] %s combinations=%d", self.fqn, len(combinations))
|
|
460
|
+
|
|
461
|
+
if combinations:
|
|
462
|
+
to_consume = await self._check_guard(combinations)
|
|
463
|
+
if to_consume:
|
|
464
|
+
await self._fire_transition(to_consume)
|
|
465
|
+
|
|
466
|
+
except asyncio.CancelledError:
|
|
467
|
+
pass
|
|
468
|
+
except Exception:
|
|
469
|
+
logger.exception("[%s] Transition error", self.fqn)
|
|
470
|
+
|
|
471
|
+
async def cancel(self):
|
|
472
|
+
"""Cancel this transition's task"""
|
|
473
|
+
self._stop_event.set()
|
|
474
|
+
if self.task:
|
|
475
|
+
self.task.cancel()
|
|
476
|
+
try:
|
|
477
|
+
await self.task
|
|
478
|
+
except asyncio.CancelledError:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class NetRuntime:
|
|
483
|
+
"""Runtime execution of a complete Petri net"""
|
|
484
|
+
|
|
485
|
+
def __init__(self, spec: NetSpec, bb: Any, timebase: Any):
|
|
486
|
+
self.spec = spec
|
|
487
|
+
self.bb = bb
|
|
488
|
+
self.timebase = timebase
|
|
489
|
+
# Use tuple-of-parts as canonical keys internally
|
|
490
|
+
# Keep as Any to simplify gradual migration (place/transition runtime objects)
|
|
491
|
+
self.places: Dict[Tuple[str, ...], Any] = {}
|
|
492
|
+
self.transitions: Dict[Tuple[str, ...], Any] = {}
|
|
493
|
+
|
|
494
|
+
self._flatten_spec(spec)
|
|
495
|
+
self._build_runtime()
|
|
496
|
+
|
|
497
|
+
def _flatten_spec(self, spec: NetSpec):
|
|
498
|
+
"""Flatten nested subnet specs into flat dictionaries by computing FQNs"""
|
|
499
|
+
# Register all places with their computed FQNs
|
|
500
|
+
for place_name, place_spec in spec.places.items():
|
|
501
|
+
place_parts = tuple(spec.get_parts(place_name))
|
|
502
|
+
self.places[place_parts] = None
|
|
503
|
+
|
|
504
|
+
# Register all transitions with their computed FQNs
|
|
505
|
+
for trans_name, trans_spec in spec.transitions.items():
|
|
506
|
+
trans_parts = tuple(spec.get_parts(trans_name))
|
|
507
|
+
self.transitions[trans_parts] = None
|
|
508
|
+
|
|
509
|
+
# Recursively flatten subnets
|
|
510
|
+
for subnet_name, subnet_spec in spec.subnets.items():
|
|
511
|
+
self._flatten_spec(subnet_spec)
|
|
512
|
+
|
|
513
|
+
def _build_runtime(self):
|
|
514
|
+
"""Build runtime objects from flattened spec"""
|
|
515
|
+
# Build all place runtimes
|
|
516
|
+
for place_parts in list(self.places.keys()):
|
|
517
|
+
place_spec = self._find_place_spec_by_parts(place_parts)
|
|
518
|
+
self.places[place_parts] = PlaceRuntime(place_spec, self.bb, self.timebase, '.'.join(place_parts))
|
|
519
|
+
|
|
520
|
+
# Build all transition runtimes
|
|
521
|
+
for trans_parts in list(self.transitions.keys()):
|
|
522
|
+
trans_spec = self._find_transition_spec_by_parts(trans_parts)
|
|
523
|
+
self.transitions[trans_parts] = TransitionRuntime(trans_spec, self, '.'.join(trans_parts))
|
|
524
|
+
|
|
525
|
+
# Connect arcs by resolving references
|
|
526
|
+
for arc in self._collect_all_arcs():
|
|
527
|
+
# Use canonical tuple parts computed by ArcSpec
|
|
528
|
+
source_parts = tuple(arc.source_parts)
|
|
529
|
+
target_parts = tuple(arc.target_parts)
|
|
530
|
+
|
|
531
|
+
if target_parts in self.transitions:
|
|
532
|
+
trans = self.transitions[target_parts]
|
|
533
|
+
trans.add_input_arc(source_parts, arc)
|
|
534
|
+
|
|
535
|
+
if source_parts in self.transitions:
|
|
536
|
+
trans = self.transitions[source_parts]
|
|
537
|
+
trans.add_output_arc(arc)
|
|
538
|
+
|
|
539
|
+
# DEBUG: show place keys and transition connectivity
|
|
540
|
+
logger.debug("[runtime] places=%s", list(self.places.keys()))
|
|
541
|
+
# DEBUG: show transition connectivity
|
|
542
|
+
for tfqn, trans in self.transitions.items():
|
|
543
|
+
try:
|
|
544
|
+
in_count = len(trans.input_arcs)
|
|
545
|
+
out_count = len(trans.output_arcs)
|
|
546
|
+
except Exception:
|
|
547
|
+
in_count = out_count = 0
|
|
548
|
+
logger.debug("[runtime] %s inputs=%d outputs=%d", tfqn, in_count, out_count)
|
|
549
|
+
|
|
550
|
+
def _find_place_spec(self, fqn: str) -> PlaceSpec:
|
|
551
|
+
"""Find place spec by FQN using hierarchy"""
|
|
552
|
+
parts = fqn.split('.')
|
|
553
|
+
return self._find_in_spec(self.spec, parts, is_place=True)
|
|
554
|
+
|
|
555
|
+
def _find_transition_spec(self, fqn: str) -> TransitionSpec:
|
|
556
|
+
"""Find transition spec by FQN using hierarchy"""
|
|
557
|
+
parts = fqn.split('.')
|
|
558
|
+
return self._find_in_spec(self.spec, parts, is_place=False)
|
|
559
|
+
|
|
560
|
+
def _find_in_spec(self, spec: NetSpec, parts: List[str], is_place: bool):
|
|
561
|
+
"""Recursively find place or transition in spec hierarchy"""
|
|
562
|
+
# Remove the root name if it matches
|
|
563
|
+
if parts[0] == spec.name:
|
|
564
|
+
parts = parts[1:]
|
|
565
|
+
|
|
566
|
+
if len(parts) == 1:
|
|
567
|
+
# Leaf - look in places or transitions
|
|
568
|
+
name = parts[0]
|
|
569
|
+
if is_place:
|
|
570
|
+
if name in spec.places:
|
|
571
|
+
return spec.places[name]
|
|
572
|
+
else:
|
|
573
|
+
if name in spec.transitions:
|
|
574
|
+
return spec.transitions[name]
|
|
575
|
+
raise KeyError(f"{'Place' if is_place else 'Transition'} {'.'.join([spec.name] + parts)} not found")
|
|
576
|
+
|
|
577
|
+
# Navigate into subnet
|
|
578
|
+
subnet_name = parts[0]
|
|
579
|
+
if subnet_name in spec.subnets:
|
|
580
|
+
return self._find_in_spec(spec.subnets[subnet_name], parts[1:], is_place)
|
|
581
|
+
|
|
582
|
+
raise KeyError(f"Subnet {subnet_name} not found in {spec.name}")
|
|
583
|
+
|
|
584
|
+
def _to_parts(self, key: Any) -> Tuple[str, ...]:
|
|
585
|
+
"""Normalize a dotted string, parts list/tuple, or ref into tuple parts."""
|
|
586
|
+
if isinstance(key, tuple):
|
|
587
|
+
return key
|
|
588
|
+
if isinstance(key, list):
|
|
589
|
+
return tuple(key)
|
|
590
|
+
if hasattr(key, 'get_parts'):
|
|
591
|
+
try:
|
|
592
|
+
return tuple(key.get_parts())
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
if isinstance(key, str):
|
|
596
|
+
return tuple(key.split('.'))
|
|
597
|
+
# fallback
|
|
598
|
+
return tuple(str(key).split('.'))
|
|
599
|
+
|
|
600
|
+
def _collect_all_arcs(self) -> List[ArcSpec]:
|
|
601
|
+
"""Collect all arcs from spec and subnets recursively"""
|
|
602
|
+
arcs = []
|
|
603
|
+
self._collect_arcs_from_spec(self.spec, arcs)
|
|
604
|
+
return arcs
|
|
605
|
+
|
|
606
|
+
def _collect_arcs_from_spec(self, spec: NetSpec, arcs: List[ArcSpec]):
|
|
607
|
+
"""Recursively collect arcs from spec"""
|
|
608
|
+
arcs.extend(spec.arcs)
|
|
609
|
+
for subnet_spec in spec.subnets.values():
|
|
610
|
+
self._collect_arcs_from_spec(subnet_spec, arcs)
|
|
611
|
+
|
|
612
|
+
def _find_place_spec_by_parts(self, parts: Tuple[str, ...]) -> PlaceSpec:
|
|
613
|
+
"""Find a PlaceSpec using a tuple of path parts."""
|
|
614
|
+
return self._find_in_spec(self.spec, list(parts), is_place=True)
|
|
615
|
+
|
|
616
|
+
def _find_transition_spec_by_parts(self, parts: Tuple[str, ...]) -> TransitionSpec:
|
|
617
|
+
"""Find a TransitionSpec using a tuple of path parts."""
|
|
618
|
+
return self._find_in_spec(self.spec, list(parts), is_place=False)
|
|
619
|
+
|
|
620
|
+
async def start(self):
|
|
621
|
+
"""Start the Petri net execution"""
|
|
622
|
+
for place in self.places.values():
|
|
623
|
+
if place.spec.is_io_input:
|
|
624
|
+
await place.start_io_input()
|
|
625
|
+
|
|
626
|
+
for trans in self.transitions.values():
|
|
627
|
+
trans.task = asyncio.create_task(trans.run())
|
|
628
|
+
|
|
629
|
+
async def stop(self, timeout: float = 5.0):
|
|
630
|
+
"""Stop the Petri net execution"""
|
|
631
|
+
for trans in self.transitions.values():
|
|
632
|
+
await trans.cancel()
|
|
633
|
+
|
|
634
|
+
for place in self.places.values():
|
|
635
|
+
await place.cancel()
|
|
636
|
+
|
|
637
|
+
def add_place(self, fqn: str, place_type: PlaceType = PlaceType.BAG,
|
|
638
|
+
state_factory: Optional[Callable] = None) -> PlaceRuntime:
|
|
639
|
+
"""Dynamically add a place to the running net"""
|
|
640
|
+
parts = self._to_parts(fqn)
|
|
641
|
+
if parts in self.places:
|
|
642
|
+
raise ValueError(f"Place {fqn} already exists")
|
|
643
|
+
|
|
644
|
+
place_spec = PlaceSpec(fqn, place_type, state_factory=state_factory)
|
|
645
|
+
place_runtime = PlaceRuntime(place_spec, self.bb, self.timebase, fqn)
|
|
646
|
+
self.places[parts] = place_runtime
|
|
647
|
+
|
|
648
|
+
self._add_to_spec(place_spec)
|
|
649
|
+
|
|
650
|
+
return place_runtime
|
|
651
|
+
|
|
652
|
+
def add_transition(self, fqn: str, handler: Callable,
|
|
653
|
+
guard: Optional[GuardSpec] = None,
|
|
654
|
+
state_factory: Optional[Callable] = None) -> TransitionRuntime:
|
|
655
|
+
"""Dynamically add a transition to the running net"""
|
|
656
|
+
parts = self._to_parts(fqn)
|
|
657
|
+
if parts in self.transitions:
|
|
658
|
+
raise ValueError(f"Transition {fqn} already exists")
|
|
659
|
+
|
|
660
|
+
trans_spec = TransitionSpec(fqn, handler, guard, state_factory)
|
|
661
|
+
trans_runtime = TransitionRuntime(trans_spec, self, fqn)
|
|
662
|
+
self.transitions[parts] = trans_runtime
|
|
663
|
+
|
|
664
|
+
trans_runtime.task = asyncio.create_task(trans_runtime.run())
|
|
665
|
+
|
|
666
|
+
self._add_to_spec(trans_spec)
|
|
667
|
+
|
|
668
|
+
return trans_runtime
|
|
669
|
+
|
|
670
|
+
def add_arc(self, source_fqn: str, target_fqn: str, weight: int = 1,
|
|
671
|
+
name: Optional[str] = None):
|
|
672
|
+
"""Dynamically add an arc to the running net"""
|
|
673
|
+
if source_fqn not in self.places and source_fqn not in self.transitions:
|
|
674
|
+
raise ValueError(f"Source {source_fqn} does not exist")
|
|
675
|
+
if target_fqn not in self.places and target_fqn not in self.transitions:
|
|
676
|
+
raise ValueError(f"Target {target_fqn} does not exist")
|
|
677
|
+
|
|
678
|
+
arc_name = name or f"{source_fqn}->{target_fqn}"
|
|
679
|
+
arc_spec = ArcSpec(source_fqn, target_fqn, weight, arc_name)
|
|
680
|
+
|
|
681
|
+
if target_fqn in self.transitions:
|
|
682
|
+
trans = self.transitions[target_fqn]
|
|
683
|
+
trans.add_input_arc(source_fqn, arc_spec)
|
|
684
|
+
|
|
685
|
+
if source_fqn in self.transitions:
|
|
686
|
+
trans = self.transitions[source_fqn]
|
|
687
|
+
trans.add_output_arc(arc_spec)
|
|
688
|
+
|
|
689
|
+
self._add_arc_to_spec(arc_spec)
|
|
690
|
+
|
|
691
|
+
async def remove_arc(self, source_fqn: str, target_fqn: str):
|
|
692
|
+
"""Dynamically remove an arc from the running net"""
|
|
693
|
+
arc_to_remove = None
|
|
694
|
+
|
|
695
|
+
for arc in self._collect_all_arcs():
|
|
696
|
+
if arc.source == source_fqn and arc.target == target_fqn:
|
|
697
|
+
arc_to_remove = arc
|
|
698
|
+
break
|
|
699
|
+
|
|
700
|
+
if not arc_to_remove:
|
|
701
|
+
raise ValueError(f"Arc from {source_fqn} to {target_fqn} does not exist")
|
|
702
|
+
|
|
703
|
+
if target_fqn in self.transitions:
|
|
704
|
+
trans = self.transitions[target_fqn]
|
|
705
|
+
|
|
706
|
+
await trans.cancel()
|
|
707
|
+
|
|
708
|
+
trans.input_arcs = [(p, a) for p, a in trans.input_arcs
|
|
709
|
+
if not (a.source == source_fqn and a.target == target_fqn)]
|
|
710
|
+
|
|
711
|
+
if trans.input_arcs:
|
|
712
|
+
trans._stop_event.clear()
|
|
713
|
+
trans.task = asyncio.create_task(trans.run())
|
|
714
|
+
|
|
715
|
+
if source_fqn in self.transitions:
|
|
716
|
+
trans = self.transitions[source_fqn]
|
|
717
|
+
trans.output_arcs = [a for a in trans.output_arcs
|
|
718
|
+
if not (a.source == source_fqn and a.target == target_fqn)]
|
|
719
|
+
|
|
720
|
+
self._remove_arc_from_spec(arc_to_remove)
|
|
721
|
+
|
|
722
|
+
async def remove(self, fqn: str):
|
|
723
|
+
"""Remove a place or transition by fully qualified name"""
|
|
724
|
+
if fqn in self.places:
|
|
725
|
+
place = self.places[fqn]
|
|
726
|
+
await place.cancel()
|
|
727
|
+
|
|
728
|
+
arcs_to_remove = [arc for arc in self._collect_all_arcs()
|
|
729
|
+
if arc.source == fqn or arc.target == fqn]
|
|
730
|
+
|
|
731
|
+
affected_transitions = set()
|
|
732
|
+
|
|
733
|
+
for arc in arcs_to_remove:
|
|
734
|
+
if arc.target in self.transitions:
|
|
735
|
+
trans = self.transitions[arc.target]
|
|
736
|
+
affected_transitions.add(trans)
|
|
737
|
+
trans.input_arcs = [(p, a) for p, a in trans.input_arcs if a != arc]
|
|
738
|
+
|
|
739
|
+
if arc.source in self.transitions:
|
|
740
|
+
trans = self.transitions[arc.source]
|
|
741
|
+
trans.output_arcs = [a for a in trans.output_arcs if a != arc]
|
|
742
|
+
|
|
743
|
+
self._remove_arc_from_spec(arc)
|
|
744
|
+
|
|
745
|
+
for trans in affected_transitions:
|
|
746
|
+
await trans.cancel()
|
|
747
|
+
if trans.input_arcs:
|
|
748
|
+
trans._stop_event.clear()
|
|
749
|
+
trans.task = asyncio.create_task(trans.run())
|
|
750
|
+
|
|
751
|
+
del self.places[fqn]
|
|
752
|
+
self._remove_from_spec(fqn, is_place=True)
|
|
753
|
+
|
|
754
|
+
elif fqn in self.transitions:
|
|
755
|
+
trans = self.transitions[fqn]
|
|
756
|
+
await trans.cancel()
|
|
757
|
+
|
|
758
|
+
arcs_to_remove = [arc for arc in self._collect_all_arcs()
|
|
759
|
+
if arc.source == fqn or arc.target == fqn]
|
|
760
|
+
|
|
761
|
+
for arc in arcs_to_remove:
|
|
762
|
+
self._remove_arc_from_spec(arc)
|
|
763
|
+
|
|
764
|
+
del self.transitions[fqn]
|
|
765
|
+
self._remove_from_spec(fqn, is_place=False)
|
|
766
|
+
|
|
767
|
+
else:
|
|
768
|
+
raise ValueError(f"{fqn} not found in places or transitions")
|
|
769
|
+
|
|
770
|
+
def _add_to_spec(self, spec):
|
|
771
|
+
"""Add place or transition spec to appropriate location"""
|
|
772
|
+
if isinstance(spec, PlaceSpec):
|
|
773
|
+
self._add_to_nested_spec(spec.name, spec, is_place=True)
|
|
774
|
+
elif isinstance(spec, TransitionSpec):
|
|
775
|
+
self._add_to_nested_spec(spec.name, spec, is_place=False)
|
|
776
|
+
|
|
777
|
+
def _add_to_nested_spec(self, fqn: str, spec, is_place: bool):
|
|
778
|
+
"""Add spec to correct nested location based on FQN"""
|
|
779
|
+
parts = fqn.split('.')
|
|
780
|
+
current_spec = self.spec
|
|
781
|
+
|
|
782
|
+
for i, part in enumerate(parts[1:-1], start=1):
|
|
783
|
+
subnet_fqn = '.'.join(parts[:i+1])
|
|
784
|
+
if subnet_fqn not in current_spec.subnets:
|
|
785
|
+
return
|
|
786
|
+
current_spec = current_spec.subnets[subnet_fqn]
|
|
787
|
+
|
|
788
|
+
if is_place:
|
|
789
|
+
current_spec.places[fqn] = spec
|
|
790
|
+
else:
|
|
791
|
+
current_spec.transitions[fqn] = spec
|
|
792
|
+
|
|
793
|
+
def _add_arc_to_spec(self, arc_spec: ArcSpec):
|
|
794
|
+
"""Add arc to appropriate spec level"""
|
|
795
|
+
# arc_spec.source may be a ref or string; use precomputed parts
|
|
796
|
+
parts = list(arc_spec.source_parts)
|
|
797
|
+
current_spec = self.spec
|
|
798
|
+
|
|
799
|
+
for i, part in enumerate(parts[1:-1], start=1):
|
|
800
|
+
subnet_fqn = '.'.join(parts[:i+1])
|
|
801
|
+
if subnet_fqn in current_spec.subnets:
|
|
802
|
+
current_spec = current_spec.subnets[subnet_fqn]
|
|
803
|
+
else:
|
|
804
|
+
break
|
|
805
|
+
|
|
806
|
+
current_spec.arcs.append(arc_spec)
|
|
807
|
+
|
|
808
|
+
def _remove_from_spec(self, fqn: str, is_place: bool):
|
|
809
|
+
"""Remove place or transition from spec"""
|
|
810
|
+
parts = fqn.split('.')
|
|
811
|
+
current_spec = self.spec
|
|
812
|
+
|
|
813
|
+
for i, part in enumerate(parts[1:-1], start=1):
|
|
814
|
+
subnet_fqn = '.'.join(parts[:i+1])
|
|
815
|
+
if subnet_fqn not in current_spec.subnets:
|
|
816
|
+
return
|
|
817
|
+
current_spec = current_spec.subnets[subnet_fqn]
|
|
818
|
+
|
|
819
|
+
if is_place and fqn in current_spec.places:
|
|
820
|
+
del current_spec.places[fqn]
|
|
821
|
+
elif not is_place and fqn in current_spec.transitions:
|
|
822
|
+
del current_spec.transitions[fqn]
|
|
823
|
+
|
|
824
|
+
def _remove_arc_from_spec(self, arc: ArcSpec):
|
|
825
|
+
"""Remove arc from spec"""
|
|
826
|
+
def remove_from_spec_recursive(spec: NetSpec):
|
|
827
|
+
spec.arcs = [a for a in spec.arcs
|
|
828
|
+
if not (a.source == arc.source and a.target == arc.target)]
|
|
829
|
+
for subnet_spec in spec.subnets.values():
|
|
830
|
+
remove_from_spec_recursive(subnet_spec)
|
|
831
|
+
|
|
832
|
+
remove_from_spec_recursive(self.spec)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
class Runner:
|
|
836
|
+
"""High-level runner for executing a Petri net"""
|
|
837
|
+
|
|
838
|
+
def __init__(self, net_func: Any, blackboard: Any):
|
|
839
|
+
if not hasattr(net_func, '_spec'):
|
|
840
|
+
raise ValueError(f"{net_func} is not a valid net")
|
|
841
|
+
|
|
842
|
+
self.spec = net_func._spec
|
|
843
|
+
self.blackboard = blackboard
|
|
844
|
+
self.timebase = None
|
|
845
|
+
self.runtime: Optional[NetRuntime] = None
|
|
846
|
+
|
|
847
|
+
async def start(self, timebase: Any):
|
|
848
|
+
"""Start the net with given timebase"""
|
|
849
|
+
self.timebase = timebase
|
|
850
|
+
self.runtime = NetRuntime(self.spec, self.blackboard, self.timebase)
|
|
851
|
+
await self.runtime.start()
|
|
852
|
+
|
|
853
|
+
async def stop(self, timeout: float = 5.0):
|
|
854
|
+
"""Stop the net"""
|
|
855
|
+
if self.runtime:
|
|
856
|
+
await self.runtime.stop(timeout)
|
|
857
|
+
|
|
858
|
+
def add_place(self, fqn: str, place_type: PlaceType = PlaceType.BAG,
|
|
859
|
+
state_factory: Optional[Callable] = None):
|
|
860
|
+
"""Add a place to the running net"""
|
|
861
|
+
if not self.runtime:
|
|
862
|
+
raise RuntimeError("Net is not running. Call start() first.")
|
|
863
|
+
return self.runtime.add_place(fqn, place_type, state_factory)
|
|
864
|
+
|
|
865
|
+
def add_transition(self, fqn: str, handler: Callable,
|
|
866
|
+
guard: Optional[GuardSpec] = None,
|
|
867
|
+
state_factory: Optional[Callable] = None):
|
|
868
|
+
"""Add a transition to the running net"""
|
|
869
|
+
if not self.runtime:
|
|
870
|
+
raise RuntimeError("Net is not running. Call start() first.")
|
|
871
|
+
return self.runtime.add_transition(fqn, handler, guard, state_factory)
|
|
872
|
+
|
|
873
|
+
def add_arc(self, source_fqn: str, target_fqn: str, weight: int = 1,
|
|
874
|
+
name: Optional[str] = None):
|
|
875
|
+
"""Add an arc to the running net"""
|
|
876
|
+
if not self.runtime:
|
|
877
|
+
raise RuntimeError("Net is not running. Call start() first.")
|
|
878
|
+
self.runtime.add_arc(source_fqn, target_fqn, weight, name)
|
|
879
|
+
|
|
880
|
+
async def remove_arc(self, source_fqn: str, target_fqn: str):
|
|
881
|
+
"""Remove an arc from the running net"""
|
|
882
|
+
if not self.runtime:
|
|
883
|
+
raise RuntimeError("Net is not running. Call start() first.")
|
|
884
|
+
await self.runtime.remove_arc(source_fqn, target_fqn)
|
|
885
|
+
|
|
886
|
+
async def remove(self, fqn: str):
|
|
887
|
+
"""Remove a place or transition"""
|
|
888
|
+
if not self.runtime:
|
|
889
|
+
raise RuntimeError("Net is not running. Call start() first.")
|
|
890
|
+
await self.runtime.remove(fqn)
|