opalib 0.4.0__tar.gz → 0.4.2__tar.gz

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. {opalib-0.4.0 → opalib-0.4.2}/PKG-INFO +1 -6
  2. opalib-0.4.2/README.md +2 -0
  3. {opalib-0.4.0 → opalib-0.4.2}/pyproject.toml +1 -14
  4. opalib-0.4.2/src/opalib/__init__.py +25 -0
  5. opalib-0.4.2/src/opalib/bio_clients.py +328 -0
  6. opalib-0.4.2/src/opalib/enum_extender.py +47 -0
  7. opalib-0.4.2/src/opalib/examples/example_1.py +387 -0
  8. opalib-0.4.2/src/opalib/examples/example_2.py +52 -0
  9. opalib-0.4.2/src/opalib/examples/mesh_formats.py +65 -0
  10. opalib-0.4.2/src/opalib/format.py +199 -0
  11. opalib-0.4.2/src/opalib/http.py +202 -0
  12. opalib-0.4.2/src/opalib/ieee754.py +25 -0
  13. opalib-0.4.2/src/opalib/libdeflate.py +59 -0
  14. opalib-0.4.2/src/opalib/physics.py +72 -0
  15. opalib-0.4.2/src/opalib/promise.py +240 -0
  16. opalib-0.4.2/src/opalib/tests/bio_clients_test.py +10 -0
  17. opalib-0.4.2/src/opalib/tests/enum_test.py +6 -0
  18. opalib-0.4.2/src/opalib/tests/format_test.py +46 -0
  19. opalib-0.4.2/src/opalib/tests/http_test.py +56 -0
  20. opalib-0.4.2/src/opalib/tests/ieee754_test.py +15 -0
  21. opalib-0.4.2/src/opalib/tests/libdeflate_test.py +19 -0
  22. opalib-0.4.2/src/opalib/tests/physics_test.py +43 -0
  23. opalib-0.4.2/src/opalib/tests/units_test.py +33 -0
  24. opalib-0.4.2/src/opalib/tests/util_test.py +37 -0
  25. opalib-0.4.2/src/opalib/tests/web_test.py +400 -0
  26. opalib-0.4.2/src/opalib/units.py +93 -0
  27. opalib-0.4.2/src/opalib/util.py +475 -0
  28. opalib-0.4.2/src/opalib/web.py +639 -0
  29. {opalib-0.4.0 → opalib-0.4.2}/src/opalib.egg-info/PKG-INFO +1 -6
  30. opalib-0.4.2/src/opalib.egg-info/SOURCES.txt +32 -0
  31. opalib-0.4.2/src/opalib.egg-info/top_level.txt +1 -0
  32. opalib-0.4.0/README.md +0 -7
  33. opalib-0.4.0/src/opalib.egg-info/SOURCES.txt +0 -7
  34. opalib-0.4.0/src/opalib.egg-info/top_level.txt +0 -1
  35. {opalib-0.4.0 → opalib-0.4.2}/LICENSE +0 -0
  36. {opalib-0.4.0 → opalib-0.4.2}/setup.cfg +0 -0
  37. {opalib-0.4.0 → opalib-0.4.2}/src/opalib.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opalib
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A library to do multiple things
5
5
  Author-email: Donovan <donovandelisle7@gmail.com>
6
6
  License-Expression: MIT
@@ -23,8 +23,3 @@ Dynamic: license-file
23
23
 
24
24
  # opalib
25
25
  A library to do multiple things
26
-
27
- ## New modules
28
-
29
- - `src/physics.py`: gravity, force, kinetic energy, potential energy, and projectile motion helpers.
30
- - `src/units.py`: length unit conversions including `studs`, meters, feet, and custom units.
opalib-0.4.2/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # opalib
2
+ A library to do multiple things
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opalib"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "A library to do multiple things"
5
5
  authors = [
6
6
  { name="Donovan", email="donovandelisle7@gmail.com" }
@@ -34,16 +34,3 @@ package-dir = {"" = "src"}
34
34
 
35
35
  [tool.setuptools.packages.find]
36
36
  where = ["src"]
37
- include = ["src*"]
38
-
39
- [tool.bumpver]
40
- current_version = "0.4.0"
41
- version_pattern = "MAJOR.MINOR.PATCH"
42
- commit_message = "bump version {old_version} -> {new_version}"
43
- tag_message = "v{new_version}"
44
- tag_scope = "default"
45
-
46
- [tool.bumpver.file_patterns]
47
- "pyproject.toml" = [
48
- "version = \"{version}\"",
49
- ]
@@ -0,0 +1,25 @@
1
+ # __init__.py
2
+
3
+ """opalib package API."""
4
+
5
+ import os
6
+ import importlib
7
+
8
+ __all__ = []
9
+
10
+ pkg_dir = os.path.dirname(__file__)
11
+ for file in os.listdir(pkg_dir):
12
+ if file.endswith(".py") and file != "__init__.py":
13
+ module_name = file[:-3]
14
+ module = importlib.import_module(f".{module_name}", package=__name__)
15
+
16
+ for attribute in dir(module):
17
+ if attribute.startswith("_"):
18
+ continue
19
+ globals()[attribute] = getattr(module, attribute)
20
+ if attribute not in __all__:
21
+ __all__.append(attribute)
22
+
23
+ globals()[module_name] = module
24
+ if module_name not in __all__:
25
+ __all__.append(module_name)
@@ -0,0 +1,328 @@
1
+ """Biological-culture client layer for the BioWML substrate.
2
+
3
+ A BioCultureClient sends a discrete stimulus (a small list of
4
+ alphabet codes, 0..63) to a neural culture and reads back the
5
+ resulting multi-channel spiking activity. The substrate
6
+ (track_w.bio_wml.BioWML) is provider-agnostic: it talks only to
7
+ this Protocol. Three implementations live here:
8
+
9
+ - MockBioCultureClient — offline numpy spike simulation with
10
+ realistic latency, jitter, and additive noise (Task 2).
11
+ - CL1Adapter — Cortical Labs CL1, env-gated (Task 5).
12
+ - FinalSparkAdapter — FinalSpark Neuroplatform, env-gated.
13
+
14
+ Plan C (bio substrate). See docs/superpowers/plans/
15
+ 2026-05-19-bio-substrate-wml.md.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json as _json
20
+ import os as _os
21
+ import time as _time
22
+ import urllib.error as _urlerr
23
+ import urllib.request as _urlreq
24
+ from dataclasses import dataclass
25
+ from typing import Protocol, runtime_checkable
26
+
27
+ import numpy as np
28
+
29
+ ALPHABET_SIZE = 64
30
+
31
+
32
+ class BioApiKeyMissingError(RuntimeError):
33
+ """Raised when a real adapter is built without NERVE_WML_BIO_API_KEY."""
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class StimulusFrame:
38
+ """A stimulus delivered to a culture.
39
+
40
+ codes: the alphabet codes (0..63) being stimulated this tick.
41
+ channels: float32 [n_codes, n_stim_channels] electrode pattern,
42
+ one row per code. Values in [0, 1] = stimulation
43
+ amplitude per channel.
44
+ """
45
+
46
+ codes: tuple[int, ...]
47
+ channels: np.ndarray
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ActivityFrame:
52
+ """Spiking activity read back from a culture.
53
+
54
+ spikes: float32 [n_read_channels, n_bins] spike-count
55
+ raster over a short post-stimulus window.
56
+ latency_ms: wall-clock round-trip latency for this exchange.
57
+ """
58
+
59
+ spikes: np.ndarray
60
+ latency_ms: float
61
+
62
+
63
+ @runtime_checkable
64
+ class BioCultureClient(Protocol):
65
+ """Provider-agnostic contract for a biological-culture backend."""
66
+
67
+ n_stim_channels: int
68
+ n_read_channels: int
69
+ n_bins: int
70
+
71
+ def encode_stimulus(self, codes: list[int]) -> StimulusFrame:
72
+ """Map alphabet codes to an electrode stimulation pattern."""
73
+ ...
74
+
75
+ def decode_activity(self, frame: ActivityFrame) -> list[int]:
76
+ """Map read-back spiking activity to alphabet codes."""
77
+ ...
78
+
79
+ def roundtrip(self, codes: list[int]) -> ActivityFrame:
80
+ """Stimulate with `codes`, read back, return the activity."""
81
+ ...
82
+
83
+ def close(self) -> None:
84
+ """Release any underlying connection. Safe to call twice."""
85
+ ...
86
+
87
+
88
+ class MockBioCultureClient:
89
+ """Offline numpy simulation of a neural culture.
90
+
91
+ The simulation is a deterministic noisy channel. A stimulus
92
+ code is written as a Gaussian "bump" on a code-dependent set
93
+ of read channels; the culture's read-back spikes are that bump
94
+ plus per-bin Poisson-like baseline firing plus additive
95
+ Gaussian noise. `decode_activity` correlates the read raster
96
+ against the same per-code channel templates and argmax-picks a
97
+ code. With low `noise` round-trip fidelity is high (>70 %),
98
+ which lets tests assert real behaviour rather than mock stubs.
99
+
100
+ Latency: every roundtrip reports `base_latency_ms` plus uniform
101
+ jitter in [-jitter_ms, +jitter_ms]. With `simulate_wall_clock`
102
+ the call actually sleeps that long (used by the latency
103
+ integration test); CI keeps it False so unit tests stay fast.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ *,
109
+ n_stim_channels: int = 8,
110
+ n_read_channels: int = 32,
111
+ n_bins: int = 16,
112
+ base_latency_ms: float = 12.0,
113
+ jitter_ms: float = 4.0,
114
+ noise: float = 0.15,
115
+ baseline_rate: float = 0.20,
116
+ simulate_wall_clock: bool = False,
117
+ seed: int | None = None,
118
+ ) -> None:
119
+ self.n_stim_channels = n_stim_channels
120
+ self.n_read_channels = n_read_channels
121
+ self.n_bins = n_bins
122
+ self.base_latency_ms = base_latency_ms
123
+ self.jitter_ms = jitter_ms
124
+ self.noise = noise
125
+ self.baseline_rate = baseline_rate
126
+ self.simulate_wall_clock = simulate_wall_clock
127
+ self._rng = np.random.default_rng(seed)
128
+ self._closed = False
129
+
130
+ # Fixed per-code channel templates: a stable, seed-derived
131
+ # map from each alphabet code to a soft pattern over the
132
+ # read channels. Built from an independent generator so it
133
+ # does not consume the roundtrip RNG stream.
134
+ tpl_rng = np.random.default_rng(
135
+ (seed if seed is not None else 0) + 104729
136
+ )
137
+ self._templates = tpl_rng.random(
138
+ (ALPHABET_SIZE, n_read_channels), dtype=np.float32
139
+ )
140
+ # Sharpen: each code lights up ~25 % of channels strongly.
141
+ thresh = np.quantile(self._templates, 0.75, axis=1, keepdims=True)
142
+ self._templates = (self._templates >= thresh).astype(np.float32)
143
+
144
+ def encode_stimulus(self, codes: list[int]) -> StimulusFrame:
145
+ if self._closed:
146
+ raise RuntimeError("client is closed")
147
+ channels = np.zeros(
148
+ (len(codes), self.n_stim_channels), dtype=np.float32
149
+ )
150
+ for i, code in enumerate(codes):
151
+ c = int(code) % ALPHABET_SIZE
152
+ # Deterministic electrode pattern: which stim channels
153
+ # this code drives, derived from the code bits.
154
+ for ch in range(self.n_stim_channels):
155
+ channels[i, ch] = float((c >> ch) & 1)
156
+ return StimulusFrame(codes=tuple(int(c) for c in codes),
157
+ channels=channels)
158
+
159
+ def decode_activity(self, frame: ActivityFrame) -> list[int]:
160
+ # Sum spikes over time bins → per-channel rate vector,
161
+ # correlate against every code template, argmax per row.
162
+ rates = frame.spikes.sum(axis=-1) # [n_read_channels] or [k, n]
163
+ rates = np.atleast_2d(rates)
164
+ # rates rows are stacked per code in roundtrip; correlate.
165
+ scores = rates @ self._templates.T # [k, ALPHABET_SIZE]
166
+ return [int(row.argmax()) for row in scores]
167
+
168
+ def roundtrip(self, codes: list[int]) -> ActivityFrame:
169
+ if self._closed:
170
+ raise RuntimeError("client is closed")
171
+ k = max(len(codes), 1)
172
+ # Per-code read rasters, stacked: [k, n_read_channels, n_bins].
173
+ rasters = np.zeros(
174
+ (k, self.n_read_channels, self.n_bins), dtype=np.float32
175
+ )
176
+ for i, code in enumerate(codes):
177
+ c = int(code) % ALPHABET_SIZE
178
+ template = self._templates[c] # [n_read_channels]
179
+ # Evoked response: template bump in the early bins.
180
+ evoked = np.outer(
181
+ template,
182
+ np.exp(-np.arange(self.n_bins) / 4.0).astype(np.float32),
183
+ )
184
+ baseline = np.asarray(
185
+ self._rng.poisson(self.baseline_rate, size=evoked.shape),
186
+ dtype=np.float32,
187
+ )
188
+ noise = np.asarray(
189
+ self._rng.normal(0.0, self.noise, size=evoked.shape),
190
+ dtype=np.float32,
191
+ )
192
+ rasters[i] = np.clip(evoked + baseline + noise, 0.0, None)
193
+ # decode_activity wants [k, n_read_channels] after a sum over
194
+ # bins; collapse the per-code rasters into a [k, ch, bins]
195
+ # spikes array and let decode sum the last axis.
196
+ spikes = rasters.reshape(k, self.n_read_channels, self.n_bins)
197
+ latency = self.base_latency_ms + float(
198
+ self._rng.uniform(-self.jitter_ms, self.jitter_ms)
199
+ )
200
+ if self.simulate_wall_clock:
201
+ _time.sleep(max(latency, 0.0) / 1e3)
202
+ # ActivityFrame.spikes is documented as [n_read, n_bins];
203
+ # here we keep the per-code first axis so decode is exact.
204
+ return ActivityFrame(spikes=spikes, latency_ms=latency)
205
+
206
+ def close(self) -> None:
207
+ self._closed = True
208
+
209
+
210
+ class _RealBioAdapter:
211
+ """Shared env-gating + HTTP plumbing for real bio adapters.
212
+
213
+ Reads NERVE_WML_BIO_API_KEY at construction and raises
214
+ BioApiKeyMissingError if it is unset/empty — mirroring the
215
+ env-gate discipline of bridge.kiki_nerve_advisor.NerveWmlAdvisor.
216
+ No network call happens in __init__; the first network touch is
217
+ in roundtrip(), which is only reached by @pytest.mark.slow tests.
218
+ """
219
+
220
+ _DEFAULT_ENDPOINT = "" # set by subclass
221
+
222
+ def __init__(self) -> None:
223
+ key = _os.environ.get("NERVE_WML_BIO_API_KEY", "")
224
+ if not key:
225
+ raise BioApiKeyMissingError(
226
+ "NERVE_WML_BIO_API_KEY is unset — real bio adapters "
227
+ "require it; inject a MockBioCultureClient for "
228
+ "offline use."
229
+ )
230
+ self._key = key
231
+ self.endpoint = _os.environ.get(
232
+ "NERVE_WML_BIO_ENDPOINT", self._DEFAULT_ENDPOINT
233
+ )
234
+ self.n_stim_channels = 8
235
+ self.n_read_channels = 32
236
+ self.n_bins = 16
237
+ self._closed = False
238
+
239
+ def _post(self, path: str, payload: dict) -> dict: # pragma: no cover - network
240
+ url = self.endpoint.rstrip("/") + path
241
+ data = _json.dumps(payload).encode("utf-8")
242
+ req = _urlreq.Request(
243
+ url, data=data, method="POST",
244
+ headers={
245
+ "Authorization": f"Bearer {self._key}",
246
+ "Content-Type": "application/json",
247
+ },
248
+ )
249
+ try:
250
+ with _urlreq.urlopen(req, timeout=30.0) as resp:
251
+ return _json.loads(resp.read().decode("utf-8"))
252
+ except _urlerr.URLError as exc:
253
+ raise RuntimeError(f"bio API request failed: {exc}") from exc
254
+
255
+ def encode_stimulus(self, codes: list[int]) -> StimulusFrame:
256
+ channels = np.zeros(
257
+ (len(codes), self.n_stim_channels), dtype=np.float32
258
+ )
259
+ for i, code in enumerate(codes):
260
+ c = int(code) % ALPHABET_SIZE
261
+ for ch in range(self.n_stim_channels):
262
+ channels[i, ch] = float((c >> ch) & 1)
263
+ return StimulusFrame(
264
+ codes=tuple(int(c) for c in codes), channels=channels
265
+ )
266
+
267
+ def decode_activity(self, frame: ActivityFrame) -> list[int]:
268
+ # Threshold per-channel rate, fold the 32-channel binary
269
+ # vector into a 6-bit code per stimulated row.
270
+ rates = np.atleast_2d(frame.spikes.sum(axis=-1))
271
+ codes: list[int] = []
272
+ for row in rates:
273
+ bits = (row > row.mean()).astype(int)[:6]
274
+ codes.append(int(sum(b << i for i, b in enumerate(bits))))
275
+ return codes
276
+
277
+ def close(self) -> None:
278
+ self._closed = True
279
+
280
+
281
+ class FinalSparkAdapter(_RealBioAdapter):
282
+ """FinalSpark Neuroplatform adapter — remote human brain organoids.
283
+
284
+ Free for research. Set NERVE_WML_BIO_API_KEY to your platform
285
+ token. The wire shape below is the documented stimulate/read
286
+ contract; adjust _DEFAULT_ENDPOINT if FinalSpark revises it.
287
+ """
288
+
289
+ _DEFAULT_ENDPOINT = "https://neuroplatform.finalspark.com/api/v1"
290
+
291
+ def roundtrip(self, codes: list[int]) -> ActivityFrame: # pragma: no cover - network
292
+ if self._closed:
293
+ raise RuntimeError("client is closed")
294
+ stim = self.encode_stimulus(codes)
295
+ t0 = _time.perf_counter()
296
+ body = self._post(
297
+ "/stimulate-read",
298
+ {"channels": stim.channels.tolist(),
299
+ "read_bins": self.n_bins},
300
+ )
301
+ latency_ms = (_time.perf_counter() - t0) * 1e3
302
+ spikes = np.asarray(body["spikes"], dtype=np.float32)
303
+ return ActivityFrame(spikes=spikes, latency_ms=latency_ms)
304
+
305
+
306
+ class CL1Adapter(_RealBioAdapter):
307
+ """Cortical Labs CL1 adapter — real-time closed-loop CL API.
308
+
309
+ Set NERVE_WML_BIO_API_KEY to your CL API token. CL1 supports
310
+ low-latency closed-loop access; the contract below posts a
311
+ stimulus and reads the post-stimulus raster in one call.
312
+ """
313
+
314
+ _DEFAULT_ENDPOINT = "https://api.corticallabs.com/cl/v1"
315
+
316
+ def roundtrip(self, codes: list[int]) -> ActivityFrame: # pragma: no cover - network
317
+ if self._closed:
318
+ raise RuntimeError("client is closed")
319
+ stim = self.encode_stimulus(codes)
320
+ t0 = _time.perf_counter()
321
+ body = self._post(
322
+ "/closed-loop/step",
323
+ {"stim": stim.channels.tolist(),
324
+ "bins": self.n_bins},
325
+ )
326
+ latency_ms = (_time.perf_counter() - t0) * 1e3
327
+ spikes = np.asarray(body["raster"], dtype=np.float32)
328
+ return ActivityFrame(spikes=spikes, latency_ms=latency_ms)
@@ -0,0 +1,47 @@
1
+ """
2
+ enum_extender - a Python version of https://github.com/buildthomas/EnumExtender
3
+ """
4
+
5
+ from enum import Enum as PyEnum
6
+
7
+ class EnumRegistry:
8
+ def __init__(self):
9
+ self._enums = {}
10
+
11
+ def new(self, name, items):
12
+ if not isinstance(name, str):
13
+ raise TypeError("Enum name must be a string")
14
+
15
+ if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
16
+ raise TypeError("Enum items must be a list of strings")
17
+
18
+ if name in self._enums:
19
+ raise ValueError(f"Enum '{name}' already exists")
20
+
21
+ # Create a real Python Enum class dynamically
22
+ enum_class = PyEnum(name, {item: index for index, item in enumerate(items)})
23
+
24
+ self._enums[name] = enum_class
25
+ return enum_class
26
+
27
+ def __getattr__(self, name):
28
+ if name in self._enums:
29
+ return self._enums[name]
30
+ raise AttributeError(f"Enum '{name}' does not exist")
31
+
32
+ def find(self, name):
33
+ return self._enums.get(name)
34
+
35
+ def from_value(self, enum_name, value):
36
+ enum_class = self._enums.get(enum_name)
37
+ if enum_class is None:
38
+ return None
39
+
40
+ for item in enum_class:
41
+ if item.value == value:
42
+ return item
43
+ return None
44
+
45
+
46
+ # Exported instance
47
+ Enums = EnumRegistry()