From 319f6c9d4f3532bfef4c8ac8640c4d09dbbe1b34 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Sat, 14 Mar 2026 02:33:21 -0700 Subject: [PATCH 1/5] allow custom runners --- packages/noob/src/noob/cli/run.py | 17 ++++++--- packages/noob/src/noob/runner/__init__.py | 44 ++++++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/noob/src/noob/cli/run.py b/packages/noob/src/noob/cli/run.py index 06506e9f..1eb491d6 100644 --- a/packages/noob/src/noob/cli/run.py +++ b/packages/noob/src/noob/cli/run.py @@ -3,6 +3,7 @@ import json import sys from collections.abc import Generator +from time import sleep from typing import Literal as L import click @@ -15,7 +16,7 @@ @click.command("run") @click.argument("tube") -@click.option("--runner", type=click.Choice(("sync", "async", "zmq")), default="sync") +@click.option("--runner", default="sync") @click.option("--n", "-n", type=click.INT) @click.option( "--input-format", @@ -40,7 +41,7 @@ "--output-format", "-of", type=click.Choice(("json", "jsonl")), - default="json", + default=None, help="""Output format (to stdout). json outputs results as a single array of results from all run epochs. jsonl emits each result separately as they are completed on newlines. @@ -55,7 +56,7 @@ def run( n: int | None = None, input_format: L["json", "jsonl"] = "json", inparams: tuple[tuple[str, str]] | None = None, - output_format: L["json", "jsonl"] = "json", + output_format: L["json", "jsonl"] | None = None, progress: bool = False, ) -> None: input_dict = {} @@ -102,7 +103,7 @@ def _run_sync( n: int | None, piped_input: bool, input_format: L["json", "jsonl"], - output_format: L["json", "jsonl"], + output_format: L["json", "jsonl"] | None, ) -> list: results = [] runner_cls = get_runner(runner) @@ -118,6 +119,14 @@ def _run_sync( results.append(result) progress.advance(task) else: + if n is None and output_format is None: + runner_.run() + try: + while runner_.running: + sleep(1) + except KeyboardInterrupt: + runner_.stop() + return results for result in runner_.iter(n=n): if output_format == "jsonl": click.echo(json.dumps(result)) diff --git a/packages/noob/src/noob/runner/__init__.py b/packages/noob/src/noob/runner/__init__.py index 4a4163c9..cb6ffd64 100644 --- a/packages/noob/src/noob/runner/__init__.py +++ b/packages/noob/src/noob/runner/__init__.py @@ -1,6 +1,9 @@ # ruff: noqa I001 - import order meaningful to avoid cycles +import warnings +from importlib.metadata import entry_points from typing import TYPE_CHECKING, Literal, overload +from noob.exceptions import EntrypointImportWarning from noob.runner.base import TubeRunner from noob.runner.asyncio import AsyncRunner @@ -26,7 +29,7 @@ def get_runner(runner: Literal["zmq"] = "zmq") -> type["ZMQRunner"]: ... def get_runner(runner: Literal["sync", "async", "zmq"] = "sync") -> type[TubeRunner]: ... -def get_runner(runner: Literal["sync", "async", "zmq"] = "sync") -> type[TubeRunner]: +def get_runner(runner: str = "sync") -> type[TubeRunner]: """Get a runner by its short name""" if runner == "sync": return SynchronousRunner @@ -37,6 +40,45 @@ def get_runner(runner: Literal["sync", "async", "zmq"] = "sync") -> type[TubeRun from noob.runner.zmq import ZMQRunner return ZMQRunner + else: + runner_cls = _get_entrypoint_runners().get(runner) + if runner_cls is not None: + return runner_cls + else: + raise KeyError(f"Unknown runner type: {runner}") + + +def _get_entrypoint_runners() -> dict[str, type[TubeRunner]]: + """ + Get runners provided by package entrypoints like: + + [project.entry-points."noob.add_runners"] + runners = "my_package.something:add_runners + + """ + runners = {} + for ext in entry_points(group="noob.add_runners"): + try: + add_runners_fn = ext.load() + except (ImportError, AttributeError): + warnings.warn( + f"Runner entrypoint {ext.name}, {ext.value} " + f"could not be imported, or the function could not be found. Ignoring", + EntrypointImportWarning, + stacklevel=1, + ) + continue + try: + runners.update(add_runners_fn()) + except Exception as e: + # bare exception is fine here - we're calling external code and can't know. + warnings.warn( + f"Config source entrypoint {ext.name}, {ext.value} " + f"threw an error, or returned an invalid list of paths, ignoring.\n{str(e)}", + EntrypointImportWarning, + stacklevel=1, + ) + return runners __all__ = ["AsyncRunner", "SynchronousRunner", "TubeRunner", "get_runner"] From a2f23140428d317786e9178dce48e801bd67bd7d Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Sat, 14 Mar 2026 02:33:54 -0700 Subject: [PATCH 2/5] make all_nodes_optional convenience function --- packages/noob/src/noob/node/base.py | 12 ++++++++++++ packages/noob/src/noob/node/return_.py | 12 ++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/noob/src/noob/node/base.py b/packages/noob/src/noob/node/base.py index 3749fa3e..e13965d4 100644 --- a/packages/noob/src/noob/node/base.py +++ b/packages/noob/src/noob/node/base.py @@ -537,3 +537,15 @@ def _collect_signals(self) -> list[Signal]: def _collect_slots(self) -> dict[str, Slot]: return Slot.from_callable(self.fn) + + +def all_slots_optional(spec: NodeSpecification) -> dict[str, Slot]: + if isinstance(spec.depends, str): + return {} + slots = {} + for dep in spec.depends: + if isinstance(dep, str): + continue + name = list(dep.keys())[0] + slots[name] = Slot(name=name, annotation=Any, required=False) + return slots diff --git a/packages/noob/src/noob/node/return_.py b/packages/noob/src/noob/node/return_.py index 75913b60..b11fbcd7 100644 --- a/packages/noob/src/noob/node/return_.py +++ b/packages/noob/src/noob/node/return_.py @@ -8,7 +8,7 @@ from pydantic import PrivateAttr from noob.event import MetaSignal -from noob.node.base import Node, Slot +from noob.node.base import Node, Slot, all_slots_optional from noob.types import Epoch, EventMap @@ -76,12 +76,4 @@ def get(self, keep: bool) -> Any | None: def _collect_slots(self) -> dict[str, Slot]: if self.spec is None or not self.spec.depends: raise ValueError("Return nodes must have a specification that defines what they return") - if isinstance(self.spec.depends, str): - return {} - slots = {} - for dep in self.spec.depends: - if isinstance(dep, str): - continue - name = list(dep.keys())[0] - slots[name] = Slot(name=name, annotation=Any, required=False) - return slots + return all_slots_optional(self.spec) From 33a5b39336845d04c98623e9b26e88cba1860814 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Sat, 14 Mar 2026 02:34:30 -0700 Subject: [PATCH 3/5] allow noderunner to be overridden --- packages/noob/src/noob/runner/zmq/node.py | 2 +- packages/noob/src/noob/runner/zmq/runner.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/noob/src/noob/runner/zmq/node.py b/packages/noob/src/noob/runner/zmq/node.py index ad9b580f..47b525b4 100644 --- a/packages/noob/src/noob/runner/zmq/node.py +++ b/packages/noob/src/noob/runner/zmq/node.py @@ -267,7 +267,7 @@ def run(cls, spec: NodeSpecification, **kwargs: Any) -> None: # ensure that events and conditions are bound to the eventloop created in the process async def _run_inner() -> None: nonlocal spec, kwargs - runner = NodeRunner(spec=spec, **kwargs) + runner = cls(spec=spec, **kwargs) await runner._run() asyncio.run(_run_inner()) diff --git a/packages/noob/src/noob/runner/zmq/runner.py b/packages/noob/src/noob/runner/zmq/runner.py index bc6ec80d..c1d128ed 100644 --- a/packages/noob/src/noob/runner/zmq/runner.py +++ b/packages/noob/src/noob/runner/zmq/runner.py @@ -7,7 +7,7 @@ from datetime import UTC, datetime from multiprocessing.synchronize import Event as EventType from time import time -from typing import Any, cast, overload +from typing import Any, ClassVar, cast, overload from uuid import uuid4 from noob.event import Event, MetaEvent, MetaEventType, MetaSignal @@ -42,6 +42,8 @@ class ZMQRunner(TubeRunner): See :class:`.NodeRunner` for documentation about how Assets are handled in the ZMQRunner """ + noderunner_cls: ClassVar[type[NodeRunner]] = NodeRunner + node_procs: dict[NodeID, mp.Process] = field(default_factory=dict) command: CommandNode | None = None quit_timeout: float = 10 @@ -90,7 +92,7 @@ def init(self) -> None: self._return_node = node continue self.node_procs[node_id] = mp.Process( - target=NodeRunner.run, + target=self.noderunner_cls.run, args=(node.spec,), kwargs={ "asset_specs": self.tube.state.specs, From e9834dd5f3313b24a4fa54fe083006030b014ed4 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Sat, 14 Mar 2026 02:36:02 -0700 Subject: [PATCH 4/5] allow cycles in tubes when requested --- packages/noob/src/noob/tube.py | 17 +++++++++++++++-- schema/tube.schema.json | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/noob/src/noob/tube.py b/packages/noob/src/noob/tube.py index df99cf76..e40e0b9e 100644 --- a/packages/noob/src/noob/tube.py +++ b/packages/noob/src/noob/tube.py @@ -28,6 +28,10 @@ from noob.yaml import ConfigYAMLMixin +class TubeParams(BaseModel): + cycles_allowed: bool = False + + class TubeSpecification(ConfigYAMLMixin): """ Configuration for the nodes within a tube. @@ -81,6 +85,8 @@ class TubeSpecification(ConfigYAMLMixin): description: str | None = None """An optional description of the tube""" + params: TubeParams = Field(default_factory=TubeParams) + model_config = ConfigDict(extra="forbid") @field_validator("nodes", "assets", "input", mode="before") @@ -247,8 +253,6 @@ def _create_scheduler(cls, value: Scheduler | None, info: ValidationInfo) -> Sch else: scheduler = value - assert not scheduler.has_cycle() - return scheduler def in_edges(self, node: Node | str) -> list[Edge]: @@ -493,6 +497,15 @@ def no_cross_map_dependencies(self) -> Self: ) return self + @model_validator(mode="after") + def no_cycles(self) -> Self: + if self.spec and self.spec.params.cycles_allowed: + return self + + assert not self.scheduler.has_cycle(), "Theres a cycle in my boots" + + return self + def downstream_nodes(edges: list[Edge], node_id: str, exclude: set[str] | None = None) -> set[str]: """ diff --git a/schema/tube.schema.json b/schema/tube.schema.json index 6dfa356d..d58e604f 100644 --- a/schema/tube.schema.json +++ b/schema/tube.schema.json @@ -212,6 +212,17 @@ "required": [], "title": "NodeSpecification", "type": "object" + }, + "TubeParams": { + "properties": { + "cycles_allowed": { + "default": false, + "title": "Cycles Allowed", + "type": "boolean" + } + }, + "title": "TubeParams", + "type": "object" } }, "additionalProperties": false, @@ -332,6 +343,9 @@ ], "default": null, "title": "Description" + }, + "params": { + "$ref": "#/$defs/TubeParams" } }, "title": "TubeSpecification", From c52feda33b4bc6a8e66cf5b444b284fcfb508a16 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Sat, 14 Mar 2026 02:38:04 -0700 Subject: [PATCH 5/5] the rest of the owl - a fully connected LIF network --- examples/snn-fully-connected.yaml | 71 ++++++++++++++++++++ packages/nobes-snn/.copier-answers.yml | 6 ++ packages/nobes-snn/README.md | 3 + packages/nobes-snn/nobes/snn/__init__.py | 10 +++ packages/nobes-snn/nobes/snn/hooks.py | 6 ++ packages/nobes-snn/nobes/snn/lif.py | 67 +++++++++++++++++++ packages/nobes-snn/nobes/snn/monitor.py | 55 ++++++++++++++++ packages/nobes-snn/nobes/snn/py.typed | 0 packages/nobes-snn/nobes/snn/runner.py | 83 ++++++++++++++++++++++++ packages/nobes-snn/pyproject.toml | 53 +++++++++++++++ packages/nobes-snn/tests/__init__.py | 0 11 files changed, 354 insertions(+) create mode 100644 examples/snn-fully-connected.yaml create mode 100644 packages/nobes-snn/.copier-answers.yml create mode 100644 packages/nobes-snn/README.md create mode 100644 packages/nobes-snn/nobes/snn/__init__.py create mode 100644 packages/nobes-snn/nobes/snn/hooks.py create mode 100644 packages/nobes-snn/nobes/snn/lif.py create mode 100644 packages/nobes-snn/nobes/snn/monitor.py create mode 100644 packages/nobes-snn/nobes/snn/py.typed create mode 100644 packages/nobes-snn/nobes/snn/runner.py create mode 100644 packages/nobes-snn/pyproject.toml create mode 100644 packages/nobes-snn/tests/__init__.py diff --git a/examples/snn-fully-connected.yaml b/examples/snn-fully-connected.yaml new file mode 100644 index 00000000..3281049f --- /dev/null +++ b/examples/snn-fully-connected.yaml @@ -0,0 +1,71 @@ +noob_id: snn-fully-connected +noob_model: noob.TubeSpecification +noob_version: 1000.0.0 +description: | + A fully connected LIF spiking neural network with one neuron with constant input + + Demonstration of: + - using a custom runner! this requires the ZMQFreeRunner in nobes.snn + - completely ignoring noob's epoch structure and just using its eventing backbone + - time-based, queue-driven non-alignment, where all nodes just run as fast as possible + but still accept input whenever it exists + - cyclic graphs!!!! + - tube params! + + Note that this demo does **not make a whole lot of sense** - + in no universe would you want to actually run a neural net where every neuron runs in a separate process + and 99.999999% of the compute effort is spent serializing and deserializing messages between neurons + whose internal operation takes a microsecond. + This is just a demo of how you can use and abuse noob by messing with its internals, + taking bits and pieces as needed. +params: + cycles_allowed: true + + +nodes: + a: + type: nobes.snn.LIFNeuron + params: + I_static: 1 + weights: {I_static: 1} + depends: + - b: b.spike + - c: c.spike + - d: d.spike + - e: e.spike + - f: f.spike + - g: g.spike + - h: h.spike + - i: i.spike + - j: j.spike + b: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, c: c.spike, d: d.spike, e: e.spike, f: f.spike, g: g.spike, h: h.spike, i: i.spike, j: j.spike] + c: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, d: d.spike, e: e.spike, f: f.spike, g: g.spike, h: h.spike, i: i.spike, j: j.spike] + d: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, e: e.spike, f: f.spike, g: g.spike, h: h.spike, i: i.spike, j: j.spike] + e: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, f: f.spike, g: g.spike, h: h.spike, i: i.spike, j: j.spike] + f: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, e: e.spike, g: g.spike, h: h.spike, i: i.spike, j: j.spike] + g: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, e: e.spike, f: f.spike, h: h.spike, i: i.spike, j: j.spike] + h: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, e: e.spike, f: f.spike, g: g.spike, i: i.spike, j: j.spike] + i: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, e: e.spike, f: f.spike, g: g.spike, h: h.spike, j: j.spike] + j: + type: nobes.snn.LIFNeuron + depends: [a: a.spike, b: b.spike, c: c.spike, d: d.spike, e: e.spike, f: f.spike, g: g.spike, h: h.spike, i: i.spike] + + monitor: + type: nobes.snn.Monitor + depends: [a: a.voltage, b: b.voltage, c: c.voltage, d: d.voltage, e: e.voltage, f: f.voltage, g: g.voltage, h: h.voltage, i: i.voltage, j: j.voltage] diff --git a/packages/nobes-snn/.copier-answers.yml b/packages/nobes-snn/.copier-answers.yml new file mode 100644 index 00000000..8d7d6123 --- /dev/null +++ b/packages/nobes-snn/.copier-answers.yml @@ -0,0 +1,6 @@ +# Changes here will be overwritten by Copier +_src_path: templates/nobes +author_email: sneakers-the-rat@protonmail.com +author_name: sneakers-the-rat +description: spiking neural nets +package_name: snn diff --git a/packages/nobes-snn/README.md b/packages/nobes-snn/README.md new file mode 100644 index 00000000..a2f1f23b --- /dev/null +++ b/packages/nobes-snn/README.md @@ -0,0 +1,3 @@ +# nobes-snn + +spiking neural nets \ No newline at end of file diff --git a/packages/nobes-snn/nobes/snn/__init__.py b/packages/nobes-snn/nobes/snn/__init__.py new file mode 100644 index 00000000..c03deb07 --- /dev/null +++ b/packages/nobes-snn/nobes/snn/__init__.py @@ -0,0 +1,10 @@ +from nobes.snn.lif import LIFNeuron +from nobes.snn.monitor import Monitor +from nobes.snn.runner import NodeFreeRunner, ZMQFreeRunner + +__all__ = [ + "LIFNeuron", + "Monitor", + "NodeFreeRunner", + "ZMQFreeRunner", +] diff --git a/packages/nobes-snn/nobes/snn/hooks.py b/packages/nobes-snn/nobes/snn/hooks.py new file mode 100644 index 00000000..4f5df90d --- /dev/null +++ b/packages/nobes-snn/nobes/snn/hooks.py @@ -0,0 +1,6 @@ +from nobes.snn.runner import ZMQFreeRunner +from noob.runner import TubeRunner + + +def add_runners() -> dict[str, type[TubeRunner]]: + return {"zmq-freerun": ZMQFreeRunner} diff --git a/packages/nobes-snn/nobes/snn/lif.py b/packages/nobes-snn/nobes/snn/lif.py new file mode 100644 index 00000000..a8677fe7 --- /dev/null +++ b/packages/nobes-snn/nobes/snn/lif.py @@ -0,0 +1,67 @@ +import time +from random import random +from typing import Annotated as A + +from pydantic import Field + +from noob import Name, Node, NodeSpecification +from noob.event import MetaSignal +from noob.node import Slot +from noob.node.base import all_slots_optional + + +class LIFNeuron(Node): + """ + A leaky integrate and fire neuron!!! + + I strongly doubt this implementation is right, for example it doesn't exactly leak, + but hey. + """ + + spec: NodeSpecification # must have a spec to run LIFNeuron + weights: dict[str, float] = Field(default_factory=dict) + resistance: float = 10_000_000 + capacitance: float = 10 + v_membrane: float = -70e-3 + v_rest: float = -70e-3 + v_reset: float = -80e-3 + I_static: float = 0 + threshold: float = 10e-3 + spike_current: float = 10 + last_step: float = 0 + + def init(self) -> None: + for dep in self.spec.depends: + key, val = next(iter(dep.items())) + if key not in self.weights: + self.weights[key] = random() + + def process( + self, **kwargs: float + ) -> tuple[A[float, Name("voltage")], A[float | MetaSignal, Name("spike")]]: + # ignore the first step, nothing matters in the first step + if self.last_step == 0: + self.last_step = time.time() + return self.v_membrane, MetaSignal.NoEvent + + current = self.I_static + for k, v in kwargs.items(): + current += self.weights.get(k, 1) * v + + now = time.time() + dt = now - self.last_step + tau = self.resistance * self.capacitance + self.last_step = now + self.v_membrane += ( + ((self.v_rest - self.v_membrane) + (self.resistance * current)) * dt + ) / tau + + spike = MetaSignal.NoEvent + if self.v_membrane > self.threshold: + self.v_membrane = self.v_reset + spike = self.spike_current + + return self.v_membrane, spike + + def _collect_slots(self) -> dict[str, Slot]: + return all_slots_optional(self.spec) diff --git a/packages/nobes-snn/nobes/snn/monitor.py b/packages/nobes-snn/nobes/snn/monitor.py new file mode 100644 index 00000000..385c70ea --- /dev/null +++ b/packages/nobes-snn/nobes/snn/monitor.py @@ -0,0 +1,55 @@ +import logging + +from pydantic import PrivateAttr +from rich.console import Console +from rich.progress import BarColumn, Progress, TaskID, TextColumn +from rich.table import Column + +from noob import Node, NodeSpecification +from noob.logging import init_logger +from noob.node import Slot +from noob.node.base import all_slots_optional + + +class Monitor(Node): + """A monitor that can display numerical values on the CLI baby""" + + spec: NodeSpecification # specs not optional for the monitor + range: tuple[float, float] = (-70e-3, 10e-3) + + _console: Console | None = None + _progress: Progress | None = None + _tasks: dict[str, TaskID] = PrivateAttr(default_factory=dict) + _logger: logging.Logger = init_logger("monitor") + + def process(self, **kwargs: float) -> None: + for key, val in kwargs.items(): + if key not in self._tasks: + continue + self._progress.update( + self._tasks[key], completed=val - self.range[0], mv=round(val * 1000, ndigits=2) + ) + self._progress.refresh() + + def init(self) -> None: + self._console = Console() + self._progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=None, table_column=Column(ratio=4)), + TextColumn("{task.fields[mv]} mV", table_column=Column(ratio=1)), + auto_refresh=False, + console=self._console, + ) + for dep in self.spec.depends: + key = list(dep.keys())[0] + self._tasks[key] = self._progress.add_task( + key, total=self.range[1] - self.range[0], mv=0 + ) + + self._progress.start() + + def deinit(self) -> None: + self._progress.stop() + + def _collect_slots(self) -> dict[str, Slot]: + return all_slots_optional(self.spec) diff --git a/packages/nobes-snn/nobes/snn/py.typed b/packages/nobes-snn/nobes/snn/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/packages/nobes-snn/nobes/snn/runner.py b/packages/nobes-snn/nobes/snn/runner.py new file mode 100644 index 00000000..4b48b162 --- /dev/null +++ b/packages/nobes-snn/nobes/snn/runner.py @@ -0,0 +1,83 @@ +import asyncio +import contextlib +import time +from collections import defaultdict, deque +from collections.abc import AsyncGenerator, Generator +from typing import Any, ClassVar + +from noob.event import MetaSignal +from noob.network.message import EventMsg +from noob.runner.base import TubeRunner +from noob.runner.zmq import NodeRunner, ZMQRunner +from noob.types import Epoch, ReturnNodeType + + +class NodeFreeRunner(NodeRunner): + def __init__(self, interval: float = 0.01, *args: Any, **kwargs: Any): + """ + Args: + interval (float): minimum time (in seconds) that each iteration should take. + (i.e., if the iteration takes less time than this, sleep for the difference) + """ + super().__init__(*args, **kwargs) + self._interval = interval + self._signals = defaultdict(deque) + self._event_lock = asyncio.Lock() + + async def await_inputs(self) -> AsyncGenerator[tuple[tuple[Any], dict[str, Any], Epoch]]: + """epochs are meaningless to free runner""" + current_epoch = 0 + + while not self._quitting.is_set(): + await self._freerun.wait() + epoch = Epoch(current_epoch) + input_events = [] + async with self._event_lock: + for dep in self.depends: + with contextlib.suppress(IndexError): + input_events.append(self._signals[dep].popleft()) + inputs = self.store.transform_events(self._node.edges, input_events) + args, kwargs = self.store.split_args_kwargs(inputs) + yielded_time = time.time() + + yield args, kwargs, epoch + + runtime = time.time() - yielded_time + await asyncio.sleep(max(0.0, self._interval - runtime)) + current_epoch += 1 + + async def on_event(self, msg: EventMsg) -> None: + """Just stack the events up in queues, we don't care about epochs here""" + depended_events = [ + e + for e in msg.value + if (e["node_id"], e["signal"]) in self.depends and e["value"] is not MetaSignal.NoEvent + ] + + async with self._event_lock: + for e in depended_events: + self._signals[(e["node_id"], e["signal"])].append(e) + + +class ZMQFreeRunner(ZMQRunner): + noderunner_cls: ClassVar[type[TubeRunner]] = NodeFreeRunner + + def run(self, n: int | None = None) -> None | list[ReturnNodeType]: + if n is not None: + raise NotImplementedError( + "What do you mean freerun for only a little bit you are either in or you're out" + ) + super().run() + + def iter(self, n: int | None = None) -> Generator[ReturnNodeType, None, None]: + raise NotImplementedError( + "iteration mode doesn't really make sense for the free runner! use run!" + ) + + def process(self, **kwargs: Any) -> ReturnNodeType: + raise NotImplementedError( + "process mode doesn't really make sense for the free runner! use run!" + ) + + async def on_event(self, msg: EventMsg) -> None: + pass diff --git a/packages/nobes-snn/pyproject.toml b/packages/nobes-snn/pyproject.toml new file mode 100644 index 00000000..886c850a --- /dev/null +++ b/packages/nobes-snn/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "nobes-snn" +description = """spiking neural nets""" +authors = [ + {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, +] +dynamic=["version"] +requires-python = ">=3.11" +readme = "README.md" +license = {text = "EUPL-1.2"} +keywords = [ + "noob", + "noob-nodes", + "nobes", +] +dependencies = [ + "noob[zmq]", + "rich>=14.3.3", +] + +[project.optional-dependencies] +tests = [ + "pytest", +] +dev = [ + "nobes-snn[tests]", +] + +[project.entry-points."noob.add_runners"] +runners = "nobes.snn.hooks:add_runners" + +[project.urls] +homepage = "https://noob.readthedocs.io" +documentation = "https://noob.readthedocs.io" +repository = "https://github.com/miniscope/noob" +changelog = "https://noob.readthedocs.io/en/latest/changelog.html" + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[tool.pdm] +distribution = true + +[tool.pdm.build] +includes = ["nobes"] + +[tool.pdm.version] +# versions from tags like "v0.1.2" +fallback_version = "0.0.0" +source = "scm" +tag_filter="nobes-snn-v*" +tag_regex = '^nobes-snn-v(?:\D*)?(?P([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$' diff --git a/packages/nobes-snn/tests/__init__.py b/packages/nobes-snn/tests/__init__.py new file mode 100644 index 00000000..e69de29b