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,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hypha DSL - Specification Layer
|
|
4
|
+
|
|
5
|
+
Core data structures representing the declarative specification of a Petri net.
|
|
6
|
+
These are built by the decorator API and used to instantiate runtime objects.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Optional, Callable, Dict, List, Any, Tuple
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlaceType(Enum):
|
|
15
|
+
"""Type of place storage mechanism"""
|
|
16
|
+
BAG = auto() # Unordered collection
|
|
17
|
+
QUEUE = auto() # FIFO ordered collection
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PlaceSpec:
|
|
22
|
+
"""Specification for a place in the Petri net"""
|
|
23
|
+
name: str # Local name only, e.g. "waiting"
|
|
24
|
+
place_type: PlaceType
|
|
25
|
+
handler: Optional[Callable] = None # For IOPlaces
|
|
26
|
+
state_factory: Optional[Callable] = None
|
|
27
|
+
is_io_input: bool = False
|
|
28
|
+
is_io_output: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class GuardSpec:
|
|
33
|
+
"""Specification for a guard function"""
|
|
34
|
+
func: Callable # Signature: (tokens, bb, timebase) -> Iterator[tuple]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TransitionSpec:
|
|
39
|
+
"""Specification for a transition in the Petri net"""
|
|
40
|
+
name: str # Local name only, e.g. "process_task"
|
|
41
|
+
handler: Callable # Signature: async (consumed, bb, timebase, [state]) -> yields
|
|
42
|
+
guard: Optional[GuardSpec] = None
|
|
43
|
+
state_factory: Optional[Callable] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ArcSpec:
|
|
48
|
+
"""Specification for an arc connecting places and transitions"""
|
|
49
|
+
source: 'PlaceRef | TransitionRef' # Reference objects, not strings
|
|
50
|
+
target: 'PlaceRef | TransitionRef' # Reference objects, not strings
|
|
51
|
+
weight: int = 1
|
|
52
|
+
name: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
# Computed canonical parts for source/target (tuple of name components)
|
|
55
|
+
source_parts: Tuple[str, ...] = field(init=False)
|
|
56
|
+
target_parts: Tuple[str, ...] = field(init=False)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
# Helper to extract parts from a ref/string/tuple
|
|
60
|
+
def _parts_of(obj) -> Tuple[str, ...]:
|
|
61
|
+
try:
|
|
62
|
+
parts = obj.get_parts()
|
|
63
|
+
return tuple(parts)
|
|
64
|
+
except Exception:
|
|
65
|
+
# obj might be a dotted string or a tuple/list
|
|
66
|
+
if isinstance(obj, str):
|
|
67
|
+
return tuple(obj.split('.'))
|
|
68
|
+
if isinstance(obj, (list, tuple)):
|
|
69
|
+
return tuple(obj)
|
|
70
|
+
# Fallback: stringify
|
|
71
|
+
return tuple(str(obj).split('.'))
|
|
72
|
+
|
|
73
|
+
self.source_parts = _parts_of(self.source)
|
|
74
|
+
self.target_parts = _parts_of(self.target)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class NetSpec:
|
|
79
|
+
"""Complete specification of a Petri net"""
|
|
80
|
+
name: str # Local name only, e.g. "SimpleProcessor"
|
|
81
|
+
parent: Optional['NetSpec'] = None # Parent spec for hierarchy
|
|
82
|
+
places: Dict[str, PlaceSpec] = field(default_factory=dict) # Local names as keys
|
|
83
|
+
transitions: Dict[str, TransitionSpec] = field(default_factory=dict) # Local names as keys
|
|
84
|
+
arcs: List[ArcSpec] = field(default_factory=list)
|
|
85
|
+
subnets: Dict[str, 'NetSpec'] = field(default_factory=dict) # Local names as keys
|
|
86
|
+
|
|
87
|
+
def get_fqn(self, local_name: Optional[str] = None) -> str:
|
|
88
|
+
"""Compute fully qualified name by walking parent chain"""
|
|
89
|
+
return '.'.join(self.get_parts(local_name))
|
|
90
|
+
|
|
91
|
+
def get_parts(self, local_name: Optional[str] = None) -> List[str]:
|
|
92
|
+
"""Return the fully qualified name as a list of path components.
|
|
93
|
+
|
|
94
|
+
If local_name is provided, returns the parts for that member
|
|
95
|
+
under this spec (e.g. ['Parent', 'Spec', 'local']). Otherwise
|
|
96
|
+
returns the parts for this spec itself.
|
|
97
|
+
"""
|
|
98
|
+
parts: List[str] = []
|
|
99
|
+
if self.parent:
|
|
100
|
+
parts.extend(self.parent.get_parts())
|
|
101
|
+
|
|
102
|
+
parts.append(self.name)
|
|
103
|
+
|
|
104
|
+
if local_name is not None:
|
|
105
|
+
parts.append(local_name)
|
|
106
|
+
|
|
107
|
+
return parts
|
|
108
|
+
|
|
109
|
+
def to_mermaid(self) -> str:
|
|
110
|
+
"""Generate Mermaid diagram of the net"""
|
|
111
|
+
lines = ["graph TD"]
|
|
112
|
+
|
|
113
|
+
def add_subnet(spec: NetSpec, indent: str = " "):
|
|
114
|
+
spec_fqn = spec.get_fqn()
|
|
115
|
+
|
|
116
|
+
if spec.subnets:
|
|
117
|
+
for subnet_name, subnet_spec in spec.subnets.items():
|
|
118
|
+
subnet_fqn = subnet_spec.get_fqn()
|
|
119
|
+
lines.append(f"{indent}subgraph {subnet_fqn}")
|
|
120
|
+
add_subnet(subnet_spec, indent + " ")
|
|
121
|
+
lines.append(f"{indent}end")
|
|
122
|
+
|
|
123
|
+
for place_name, place_spec in spec.places.items():
|
|
124
|
+
place_fqn = spec.get_fqn(place_name)
|
|
125
|
+
shape = "(("
|
|
126
|
+
close = "))"
|
|
127
|
+
prefix = "[INPUT]</br>" if place_spec.is_io_input else "[OUTPUT]</br>" if place_spec.is_io_output else ""
|
|
128
|
+
lines.append(f"{indent}{place_fqn}{shape}\"{prefix}{place_fqn}\"{close}")
|
|
129
|
+
|
|
130
|
+
for trans_name in spec.transitions.keys():
|
|
131
|
+
trans_fqn = spec.get_fqn(trans_name)
|
|
132
|
+
lines.append(f"{indent}{trans_fqn}[{trans_fqn}]")
|
|
133
|
+
|
|
134
|
+
for arc in spec.arcs:
|
|
135
|
+
source_fqn = arc.source.get_fqn()
|
|
136
|
+
target_fqn = arc.target.get_fqn()
|
|
137
|
+
weight_label = f"|weight={arc.weight}|" if arc.weight > 1 else ""
|
|
138
|
+
lines.append(f"{indent}{source_fqn} -->{weight_label} {target_fqn}")
|
|
139
|
+
|
|
140
|
+
add_subnet(self)
|
|
141
|
+
|
|
142
|
+
return "\n".join(lines)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PlaceRef:
|
|
146
|
+
"""Reference to a place for use in arc definitions"""
|
|
147
|
+
def __init__(self, local_name: str, parent_spec: NetSpec):
|
|
148
|
+
self.local_name = local_name
|
|
149
|
+
self.parent_spec = parent_spec
|
|
150
|
+
|
|
151
|
+
def get_fqn(self) -> str:
|
|
152
|
+
"""Compute FQN dynamically"""
|
|
153
|
+
return '.'.join(self.get_parts())
|
|
154
|
+
|
|
155
|
+
# For backward compatibility
|
|
156
|
+
@property
|
|
157
|
+
def fqn(self) -> str:
|
|
158
|
+
return self.get_fqn()
|
|
159
|
+
|
|
160
|
+
def get_parts(self) -> List[str]:
|
|
161
|
+
"""Return the FQN as list of parts for this place ref."""
|
|
162
|
+
return self.parent_spec.get_parts(self.local_name)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def parts(self) -> List[str]:
|
|
166
|
+
return self.get_parts()
|
|
167
|
+
|
|
168
|
+
def __repr__(self):
|
|
169
|
+
return f"PlaceRef({self.get_fqn()})"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TransitionRef:
|
|
173
|
+
"""Reference to a transition for use in arc definitions"""
|
|
174
|
+
def __init__(self, local_name: str, parent_spec: NetSpec):
|
|
175
|
+
self.local_name = local_name
|
|
176
|
+
self.parent_spec = parent_spec
|
|
177
|
+
|
|
178
|
+
def get_fqn(self) -> str:
|
|
179
|
+
"""Compute FQN dynamically"""
|
|
180
|
+
return '.'.join(self.get_parts())
|
|
181
|
+
# For backward compatibility
|
|
182
|
+
@property
|
|
183
|
+
def fqn(self) -> str:
|
|
184
|
+
return self.get_fqn()
|
|
185
|
+
|
|
186
|
+
def __repr__(self):
|
|
187
|
+
return f"TransitionRef({self.get_fqn()})"
|
|
188
|
+
|
|
189
|
+
def get_parts(self) -> List[str]:
|
|
190
|
+
return self.parent_spec.get_parts(self.local_name)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def parts(self) -> List[str]:
|
|
194
|
+
return self.get_parts()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class SubnetRef:
|
|
198
|
+
"""Reference to a subnet instance for accessing its places/transitions"""
|
|
199
|
+
def __init__(self, spec: NetSpec):
|
|
200
|
+
self.spec = spec
|
|
201
|
+
|
|
202
|
+
def get_fqn(self) -> str:
|
|
203
|
+
"""Compute FQN dynamically"""
|
|
204
|
+
return '.'.join(self.get_parts())
|
|
205
|
+
|
|
206
|
+
# For backward compatibility
|
|
207
|
+
@property
|
|
208
|
+
def fqn(self) -> str:
|
|
209
|
+
return self.get_fqn()
|
|
210
|
+
|
|
211
|
+
def __getattr__(self, name: str):
|
|
212
|
+
# Check places
|
|
213
|
+
if name in self.spec.places:
|
|
214
|
+
return PlaceRef(name, self.spec)
|
|
215
|
+
|
|
216
|
+
# Check transitions
|
|
217
|
+
if name in self.spec.transitions:
|
|
218
|
+
return TransitionRef(name, self.spec)
|
|
219
|
+
|
|
220
|
+
# Check subnets
|
|
221
|
+
if name in self.spec.subnets:
|
|
222
|
+
return SubnetRef(self.spec.subnets[name])
|
|
223
|
+
|
|
224
|
+
raise AttributeError(f"No place, transition, or subnet named {name} in {self.get_fqn()}")
|
|
225
|
+
|
|
226
|
+
def __repr__(self):
|
|
227
|
+
return f"SubnetRef({self.get_fqn()})"
|
|
228
|
+
|
|
229
|
+
def get_parts(self) -> List[str]:
|
|
230
|
+
return self.spec.get_parts()
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def parts(self) -> List[str]:
|
|
234
|
+
return self.get_parts()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hypha Petri Net Utilities
|
|
4
|
+
|
|
5
|
+
Utility functions for Petri net operations like Mermaid diagram generation.
|
|
6
|
+
"""
|
|
7
|
+
from .core.specs import NetSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_mermaid(spec: NetSpec) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Generate Mermaid diagram from a NetSpec.
|
|
13
|
+
|
|
14
|
+
This is a utility wrapper around NetSpec.to_mermaid() for convenience.
|
|
15
|
+
Use this when you have a spec object and want to generate a diagram.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
spec: The NetSpec to generate a diagram for
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Mermaid diagram as a string
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```python
|
|
25
|
+
from mycorrhizal.hypha.core import pn
|
|
26
|
+
from mycorrhizal.hypha.util import to_mermaid
|
|
27
|
+
|
|
28
|
+
@pn.net
|
|
29
|
+
def MyNet(builder):
|
|
30
|
+
# ... define net ...
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# Generate diagram
|
|
34
|
+
mermaid_diagram = to_mermaid(MyNet._spec)
|
|
35
|
+
print(mermaid_diagram)
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
return spec.to_mermaid()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
|
|
2
|
+
# Rhizomorph
|
|
3
|
+
|
|
4
|
+
Asyncio-friendly **Behavior Trees** for Python, designed for clarity, composability, and ergonomics in asynchronous systems.
|
|
5
|
+
|
|
6
|
+
- **Fluent decorator chains**: `bt.failer().gate(N.condition).timeout(0.5)(N.action)`
|
|
7
|
+
- **Owner-aware composites**: factories expand with the correct class owner, so large trees can be split across modules
|
|
8
|
+
- **Cross-tree composition**: embed full subtrees with `bt.subtree()` or borrow composites with `bt.bind()`
|
|
9
|
+
- **Mermaid diagrams**: export static structure diagrams for documentation/debugging
|
|
10
|
+
- **Timebases**: simulate or control time with pluggable clocks (wall/UTC/monotonic/cycle/dictated)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Example
|
|
15
|
+
```python
|
|
16
|
+
from rhizomorph.core import bt, Runner, Status
|
|
17
|
+
|
|
18
|
+
class BB:
|
|
19
|
+
def __init__(self, waypoints):
|
|
20
|
+
self.waypoints = waypoints
|
|
21
|
+
|
|
22
|
+
@bt.tree
|
|
23
|
+
def Patrol():
|
|
24
|
+
@bt.action
|
|
25
|
+
async def go_to_next(bb):
|
|
26
|
+
if bb.waypoints <= 0:
|
|
27
|
+
return Status.SUCCESS
|
|
28
|
+
bb.waypoints -= 1
|
|
29
|
+
return Status.RUNNING if bb.waypoints > 0 else Status.SUCCESS
|
|
30
|
+
|
|
31
|
+
@bt.condition
|
|
32
|
+
def has_waypoints(bb):
|
|
33
|
+
return getattr(bb, "waypoints", 0) > 0
|
|
34
|
+
|
|
35
|
+
@bt.root
|
|
36
|
+
@bt.sequence(memory=True)
|
|
37
|
+
def root(N):
|
|
38
|
+
yield N.has_waypoints
|
|
39
|
+
yield bt.timeout(1.0)(N.go_to_next)
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
bb = BB(waypoints=3)
|
|
43
|
+
runner = Runner(Patrol, bb=bb)
|
|
44
|
+
while True:
|
|
45
|
+
status = await runner.tick()
|
|
46
|
+
print("Status:", status.name, "remaining:", bb.waypoints)
|
|
47
|
+
if status in (Status.SUCCESS, Status.FAILURE):
|
|
48
|
+
break
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Fluent Decorator Chains
|
|
54
|
+
|
|
55
|
+
Decorator wrappers are available via **fluent chains** that read left→right and apply to a child when called at the end:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
yield bt.failer().gate(N.battery_ok).timeout(0.25)(N.engage)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Reads left→right:**
|
|
62
|
+
|
|
63
|
+
1. `failer()` → forces **FAILURE** once the child completes (whether the child succeeds or fails). While the child is RUNNING, RUNNING bubbles.
|
|
64
|
+
2. `gate(N.battery_ok)` → only ticks the child if `battery_ok` returns SUCCESS. If the condition returns RUNNING, the whole gate is RUNNING. If it returns FAILURE, the gate returns FAILURE without ticking the child.
|
|
65
|
+
3. `timeout(0.25)` → if the child does not finish before 0.25 (per the configured timebase), it is cancelled/reset and the decorator returns FAILURE.
|
|
66
|
+
4. Applied to `N.engage`.
|
|
67
|
+
|
|
68
|
+
### Wrapper Cheat‑Sheet
|
|
69
|
+
|
|
70
|
+
| Wrapper | Signature | RUNNING behavior | On child SUCCESS | On child FAILURE/ERROR/CANCELLED | Notes |
|
|
71
|
+
|---|---|---|---|---|---|
|
|
72
|
+
| `inverter()` | `bt.inverter()` | Pass-through | **FAILURE** | **SUCCESS** (for FAILURE); ERROR/CANCELLED pass through | Only flips SUCCESS/FAILURE; other statuses bubble |
|
|
73
|
+
| `succeeder()` | `bt.succeeder()` | Pass-through | **SUCCESS** | **SUCCESS** | Forces success once child completes |
|
|
74
|
+
| `failer()` | `bt.failer()` | Pass-through | **FAILURE** | **FAILURE** | Forces failure once child completes |
|
|
75
|
+
| `timeout()` | `bt.timeout(seconds: float)` | RUNNING until deadline; then **FAILURE** | Child status on completion | Child status on completion (unless deadline hit) | Uses the tree’s `Timebase`; cancels child when expired |
|
|
76
|
+
| `retry()` | `bt.retry(max_attempts: int, retry_on=(FAILURE, ERROR))` | Pass-through | **SUCCESS** and resets attempt count | Retries while child in `retry_on` until attempts exhausted → **FAILURE** | Does not retry on RUNNING or CANCELLED by default |
|
|
77
|
+
| `ratelimit()` | `bt.ratelimit(hz: float=None, period: float=None)` | If child is currently RUNNING, pass-through (no throttling) | Propagates child result and schedules next allowed start | If throttled: returns **RUNNING** (child not started) | Accepts exactly one of `hz` or `period` |
|
|
78
|
+
| `gate()` | `bt.gate(condition: NodeSpec | @bt.condition)` | If condition RUNNING → **RUNNING** | Ticks child and propagates its terminal status | If condition not SUCCESS → **FAILURE** | Condition is built owner‑aware under the hood |
|
|
79
|
+
|
|
80
|
+
> Tip: You can compose wrappers in any order. The chain is applied **outside‑in** (the leftmost wrapper becomes the outermost decorator).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Nodes & Composites
|
|
85
|
+
|
|
86
|
+
### Leaves
|
|
87
|
+
|
|
88
|
+
- `@bt.action` — wraps an async or sync function.
|
|
89
|
+
- May return `Status`, `bool`, or `None` (treated as `SUCCESS`).
|
|
90
|
+
- Exceptions become `ERROR` (except `asyncio.CancelledError` → `CANCELLED`).
|
|
91
|
+
|
|
92
|
+
- `@bt.condition` — wraps an async or sync predicate.
|
|
93
|
+
- Truthy → `SUCCESS`, falsy → `FAILURE`.
|
|
94
|
+
- Returning a `Status` is allowed but discouraged; use plain booleans.
|
|
95
|
+
|
|
96
|
+
### Composites (owner‑aware factories)
|
|
97
|
+
|
|
98
|
+
Define composites as **generator factories** that yield children. The factory receives the **owner class** `N`, so you reference members with `N.<member>` (no strings).
|
|
99
|
+
|
|
100
|
+
- `@bt.sequence(memory: bool = True)`
|
|
101
|
+
**AND**: ticks children in order.
|
|
102
|
+
- Fail/err/cancel **fast** (propagate child status).
|
|
103
|
+
- `RUNNING` bubbles.
|
|
104
|
+
- All `SUCCESS` → `SUCCESS`.
|
|
105
|
+
- With `memory=True`, on a subsequent tick it **resumes from the last RUNNING child**. With `memory=False`, it restarts from the first child each tick.
|
|
106
|
+
|
|
107
|
+
- `@bt.selector(memory: bool = True)`
|
|
108
|
+
**OR**: ticks children until one returns `SUCCESS`.
|
|
109
|
+
- `RUNNING` bubbles (and may set/resume index depending on `memory`/`reactive`).
|
|
110
|
+
- If none succeed → `FAILURE`.
|
|
111
|
+
- With `memory=True`, the selector **remembers the last RUNNING child** and resumes there next tick.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
- `@bt.parallel(success_threshold: int, failure_threshold: Optional[int] = None)`
|
|
115
|
+
Ticks all children concurrently per tick.
|
|
116
|
+
- Success if at least `success_threshold` children return `SUCCESS`.
|
|
117
|
+
- Failure if at least `failure_threshold` children are in {`FAILURE`, `ERROR`, `CANCELLED`}.
|
|
118
|
+
- Otherwise `RUNNING`.
|
|
119
|
+
- Default `failure_threshold = n - success_threshold + 1` (i.e., once success is impossible).
|
|
120
|
+
|
|
121
|
+
#### On owner‑aware expansion
|
|
122
|
+
|
|
123
|
+
Inside diagramming and building, composites are expanded with the correct **owner class** context. This lets you split behavior across modules while keeping references (`N.something`) type‑safe and string‑free.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Cross‑Tree Composition
|
|
128
|
+
|
|
129
|
+
- `bt.subtree(OtherTree)` — mounts another tree’s `ROOT` as a child; for Mermaid export the `ROOT` spec is attached so it expands visually.
|
|
130
|
+
- `bt.bind(Owner, Owner.Composite)` — borrows a composite from another class and expands it using that **Owner** as the context.
|
|
131
|
+
|
|
132
|
+
**Example**
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# Engage subtree
|
|
136
|
+
@bt.tree
|
|
137
|
+
def Engage():
|
|
138
|
+
@bt.condition
|
|
139
|
+
def threat_detected(bb): ...
|
|
140
|
+
|
|
141
|
+
@bt.action
|
|
142
|
+
def engage(bb): ...
|
|
143
|
+
|
|
144
|
+
@bt.root
|
|
145
|
+
@bt.sequence(memory=False)
|
|
146
|
+
def engage_threat(N):
|
|
147
|
+
yield N.threat_detected
|
|
148
|
+
yield bt.failer().timeout(0.25)(N.engage)
|
|
149
|
+
|
|
150
|
+
# Telemetry subtree
|
|
151
|
+
@bt.tree
|
|
152
|
+
def Telemetry():
|
|
153
|
+
@bt.action
|
|
154
|
+
def push(bb): ...
|
|
155
|
+
|
|
156
|
+
@bt.root
|
|
157
|
+
@bt.sequence()
|
|
158
|
+
def telemetry_seq(N):
|
|
159
|
+
yield bt.ratelimit(hz=2.0)(N.push)
|
|
160
|
+
|
|
161
|
+
# Main tree
|
|
162
|
+
@bt.tree
|
|
163
|
+
def Main():
|
|
164
|
+
@bt.root
|
|
165
|
+
@bt.selector(reactive=True, memory=True)
|
|
166
|
+
def root(N):
|
|
167
|
+
yield bt.subtree(Engage)
|
|
168
|
+
yield bt.bind(Telemetry, Telemetry.telemetry_seq)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Mermaid Export
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from rhizomorph.core import bt
|
|
177
|
+
|
|
178
|
+
@bt.tree
|
|
179
|
+
def MainTree():
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
diagram = MainTree.to_mermaid()
|
|
183
|
+
print(diagram)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Paste the output into the Mermaid Live Editor.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Runner & Tree
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
- **Tree** — a plain namespace (e.g., from `bt.tree(lambda: None)`) holding your decorated functions and a `root` composite.
|
|
197
|
+
- **Runner(tree, bb=None, tb=None)** — ticks the tree.
|
|
198
|
+
- `await runner.tick()` — ticks once; advances the timebase if it supports `.advance()`.
|
|
199
|
+
- `await runner.tick_until_complete(timeout: Optional[float]=None)` — loops until terminal status or timeout.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Status Values
|
|
204
|
+
|
|
205
|
+
- `SUCCESS`, `FAILURE`, `RUNNING`, `CANCELLED`, `ERROR`
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Design Principles
|
|
210
|
+
|
|
211
|
+
- **Async‑first** — every node can be `async def`; scheduling is `asyncio`‑based.
|
|
212
|
+
- **String‑free** — reference nodes via `N.<member>` (no magic strings).
|
|
213
|
+
- **Composable** — split and recombine trees across modules with `subtree`/`bind`.
|
|
214
|
+
- **Fluent‑only** — decorator chains read naturally left→right.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
|
File without changes
|