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.
Files changed (37) hide show
  1. mycorrhizal/__init__.py +3 -0
  2. mycorrhizal/common/__init__.py +68 -0
  3. mycorrhizal/common/interface_builder.py +203 -0
  4. mycorrhizal/common/interfaces.py +412 -0
  5. mycorrhizal/common/timebase.py +99 -0
  6. mycorrhizal/common/wrappers.py +532 -0
  7. mycorrhizal/enoki/__init__.py +0 -0
  8. mycorrhizal/enoki/core.py +1545 -0
  9. mycorrhizal/enoki/testing_utils.py +529 -0
  10. mycorrhizal/enoki/util.py +220 -0
  11. mycorrhizal/hypha/__init__.py +0 -0
  12. mycorrhizal/hypha/core/__init__.py +107 -0
  13. mycorrhizal/hypha/core/builder.py +404 -0
  14. mycorrhizal/hypha/core/runtime.py +890 -0
  15. mycorrhizal/hypha/core/specs.py +234 -0
  16. mycorrhizal/hypha/util.py +38 -0
  17. mycorrhizal/rhizomorph/README.md +220 -0
  18. mycorrhizal/rhizomorph/__init__.py +0 -0
  19. mycorrhizal/rhizomorph/core.py +1729 -0
  20. mycorrhizal/rhizomorph/util.py +45 -0
  21. mycorrhizal/spores/__init__.py +124 -0
  22. mycorrhizal/spores/cache.py +208 -0
  23. mycorrhizal/spores/core.py +419 -0
  24. mycorrhizal/spores/dsl/__init__.py +48 -0
  25. mycorrhizal/spores/dsl/enoki.py +514 -0
  26. mycorrhizal/spores/dsl/hypha.py +399 -0
  27. mycorrhizal/spores/dsl/rhizomorph.py +351 -0
  28. mycorrhizal/spores/encoder/__init__.py +11 -0
  29. mycorrhizal/spores/encoder/base.py +42 -0
  30. mycorrhizal/spores/encoder/json.py +159 -0
  31. mycorrhizal/spores/extraction.py +484 -0
  32. mycorrhizal/spores/models.py +288 -0
  33. mycorrhizal/spores/transport/__init__.py +10 -0
  34. mycorrhizal/spores/transport/base.py +46 -0
  35. mycorrhizal-0.1.0.dist-info/METADATA +198 -0
  36. mycorrhizal-0.1.0.dist-info/RECORD +37 -0
  37. 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