flybrainer 0.1.0__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.
@@ -0,0 +1,35 @@
1
+ # ─── Python ──────────────────────────────────────────────────────────────
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ *.egg
8
+
9
+ # ─── Virtual envs ────────────────────────────────────────────────────────
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # ─── IDE / OS ────────────────────────────────────────────────────────────
15
+ .idea/
16
+ .vscode/
17
+ .DS_Store
18
+ Thumbs.db
19
+ desktop.ini
20
+
21
+ # ─── Tooling caches ──────────────────────────────────────────────────────
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .mypy_cache/
25
+ .coverage
26
+ htmlcov/
27
+
28
+ # ─── Foliko runtime ──────────────────────────────────────────────────────
29
+ .foliko/
30
+
31
+ # ─── Flybrain local data (~/.flybrain/) ──────────────────────────────────
32
+ # Compiled graphs and downloaded datasets are user-specific and big.
33
+ # Users get these via `flybrain.connectome.compile.compile_graph()`.
34
+ *.npz.lock
35
+ *.npz.tmp
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenFly contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ NOTE — The MaleCNS connectome data set (downloaded via the [download] extra)
24
+ is licensed under CC-BY 4.0 by the FlyEM / MaleCNS team. See
25
+ https://www.malecns.org for terms.
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.5
2
+ Name: flybrainer
3
+ Version: 0.1.0
4
+ Summary: A reusable, Numba-JIT accelerated fruit-fly connectome (MaleCNS v1.0) extracted from OpenFly
5
+ Project-URL: Homepage, https://github.com/chnak/flybrain
6
+ Project-URL: Source, https://github.com/chnak/flybrain
7
+ Project-URL: Issues, https://github.com/chnak/flybrain/issues
8
+ Project-URL: Changelog, https://github.com/chnak/flybrain/blob/main/docs/USAGE.zh.md
9
+ Author: OpenFly contributors
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 OpenFly contributors
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+
32
+ NOTE — The MaleCNS connectome data set (downloaded via the [download] extra)
33
+ is licensed under CC-BY 4.0 by the FlyEM / MaleCNS team. See
34
+ https://www.malecns.org for terms.
35
+ License-File: LICENSE
36
+ Keywords: connectomics,fruit-fly,neural-simulation,neuroscience,numba
37
+ Classifier: Development Status :: 3 - Alpha
38
+ Classifier: Intended Audience :: Science/Research
39
+ Classifier: License :: OSI Approved :: MIT License
40
+ Classifier: Operating System :: MacOS
41
+ Classifier: Operating System :: Microsoft :: Windows
42
+ Classifier: Operating System :: POSIX :: Linux
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Programming Language :: Python :: 3.13
45
+ Classifier: Topic :: Scientific/Engineering :: Artificial Life
46
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
47
+ Requires-Python: <3.14,>=3.12
48
+ Requires-Dist: numba>=0.61
49
+ Requires-Dist: numpy<2.5,>=2.2
50
+ Requires-Dist: pillow>=10
51
+ Provides-Extra: dev
52
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
53
+ Requires-Dist: pytest>=8; extra == 'dev'
54
+ Requires-Dist: ruff>=0.6; extra == 'dev'
55
+ Provides-Extra: download
56
+ Requires-Dist: requests>=2.32; extra == 'download'
57
+ Provides-Extra: feather
58
+ Requires-Dist: pandas>=2.2; extra == 'feather'
59
+ Requires-Dist: pyarrow>=17; extra == 'feather'
60
+ Provides-Extra: readout
61
+ Requires-Dist: joblib>=1.4; extra == 'readout'
62
+ Requires-Dist: scikit-learn>=1.5; extra == 'readout'
63
+ Requires-Dist: scipy>=1.14; extra == 'readout'
64
+ Description-Content-Type: text/markdown
65
+
66
+ # flybrainer
67
+
68
+ A reusable, Numba-JIT accelerated **fruit-fly connectome** simulator —
69
+ extracted from [OpenFly](https://github.com/marketcalls/openfly).
70
+
71
+ * **Zero heavy deps** by default: `numpy` + `numba` + `pillow` only.
72
+ * **MaleCNS v1.0 connectome** loadable via `[feather,download]` extras.
73
+ * **3 encoders** + **2 readouts** + **optional plasticity**.
74
+ * **Closable LIF** kernel (Numba `@njit`), 18M+ spikes/sec.
75
+
76
+ ## Why?
77
+
78
+ OpenFly is a trading system. The brain model inside it is excellent but
79
+ inseparable from the broker / market / front-end code. This package
80
+ isolates the brain so it can be reused for **any** sensor → readout task:
81
+ trading, robotics, audio classification, RL environments, etc.
82
+
83
+ ## Install
84
+
85
+ ```bash
86
+ # Minimum (no MaleCNS, no sklearn):
87
+ pip install flybrainer
88
+
89
+ # With everything:
90
+ pip install flybrainer[feather,readout,download]
91
+ ```
92
+
93
+ ## 30-second example
94
+
95
+ ```python
96
+ import numpy as np
97
+ from flybrainer import Brain, Stimulus
98
+
99
+ # Build a tiny synthetic graph (no MaleCNS required)
100
+ n = 100
101
+ g = dict(
102
+ ptr=np.array([0, n], dtype=np.int64),
103
+ post=np.random.randint(0, n, size=n).astype(np.int32),
104
+ weight=np.where(np.random.rand(n) > 0.5, 0.275, -0.275),
105
+ modulatory=np.zeros(n, dtype=bool),
106
+ type=np.array(["L1"]*90 + ["KC"]*4 + ["MBON07"]*3 + ["MBON11"]*3),
107
+ superclass=np.array(["cb_int"]*n),
108
+ r16_uv=np.zeros((20, 2), dtype=np.float32),
109
+ r8_uv=np.zeros((20, 2), dtype=np.float32),
110
+ r16_eye=np.zeros(20, dtype=np.int8),
111
+ r8_eye=np.zeros(20, dtype=np.int8),
112
+ r8_channel=np.ones(20, dtype=np.int8),
113
+ r16=np.arange(20, dtype=np.int32),
114
+ r8=np.arange(20, dtype=np.int32),
115
+ )
116
+ brain = Brain(graph_path="<test>", graph=g)
117
+ stim = Stimulus(r16=np.random.rand(20), r8=np.random.rand(20))
118
+ result = brain.observe(stim, neural_ms=500.0)
119
+ print("total spikes:", result.counts.sum())
120
+ ```
121
+
122
+ See `examples/` for more.
123
+
124
+ ## Architecture
125
+
126
+ ```
127
+ src/flybrainer/
128
+ ├── interfaces.py # zero-dep contracts (Stimulus, BrainProtocol, SensorFrame)
129
+ ├── eyemap.py # EyeMap dataclass + resolver
130
+ ├── encoders.py # ChartEncoder, BarsEncoder, FeatureEncoder + make_encoder()
131
+ ├── kernel.py # Numba @njit LIF kernel + KernelState
132
+ ├── plasticity.py # KC→MBON plasticity (Ormond-style Oja)
133
+ ├── brain.py # Brain class (the orchestrator)
134
+ ├── readout/
135
+ │ ├── fixed.py # FixedDecoder (no training)
136
+ │ └── reservoir.py # ReservoirReadout (ridge / logistic; needs [readout])
137
+ └── connectome/
138
+ ├── normalize.py # NT sign rules (feather source → arrays; needs [feather])
139
+ ├── compile.py # arrays → graph.npz (multi-platform)
140
+ ├── sources.py # MaleCNS v1.0 metadata + SHA256
141
+ ├── download.py # resumable download (urllib only)
142
+ └── verify.py # sha256 file/array helpers
143
+ ```
144
+
145
+ ## Optional dependencies
146
+
147
+ | extra | adds | when needed |
148
+ |---|---|---|
149
+ | `feather` | `pandas`, `pyarrow` | `from flybrainer.connectome import normalize, compile_graph` |
150
+ | `readout` | `scikit-learn`, `scipy`, `joblib` | `from flybrainer.readout import ReservoirReadout` |
151
+ | `download` | `requests` | `download_source(...)` |
152
+ | `dev` | `pytest`, `pytest-cov`, `ruff` | testing |
153
+
154
+ Without `[feather]`, `compile_graph` still works if `.pre` arrays are
155
+ already on disk (you just can't re-derive from raw `.feather` files).
156
+
157
+ ## Tests
158
+
159
+ ```bash
160
+ pip install flybrainer[feather,readout,dev]
161
+ pytest tests/ -v
162
+ ```
163
+
164
+ ## Citation
165
+
166
+ * Brain model: Aso & Gorinov — *bioRxiv 2024*
167
+ * Connectome: MaleCNS v1.0 — *https://www.malecns.org*
168
+ * This extraction: OpenFly — *github.com/marketcalls/openfly*
169
+
170
+ ## Documentation
171
+
172
+ Detailed Chinese user manual (API contracts, 7 worked scenarios, FAQ):
173
+
174
+ * [`docs/USAGE.zh.md`](docs/USAGE.zh.md) — 991 lines / 10 chapters
175
+
176
+ Quick look-up order: 第 3 章 (core concepts) → 第 5 章 (full API) → 第 6 章 (worked scenarios) → 第 10 章 (FAQ).
177
+
178
+ ## License
179
+
180
+ MIT. The MaleCNS dataset is CC-BY 4.0 (separate license).
@@ -0,0 +1,115 @@
1
+ # flybrainer
2
+
3
+ A reusable, Numba-JIT accelerated **fruit-fly connectome** simulator —
4
+ extracted from [OpenFly](https://github.com/marketcalls/openfly).
5
+
6
+ * **Zero heavy deps** by default: `numpy` + `numba` + `pillow` only.
7
+ * **MaleCNS v1.0 connectome** loadable via `[feather,download]` extras.
8
+ * **3 encoders** + **2 readouts** + **optional plasticity**.
9
+ * **Closable LIF** kernel (Numba `@njit`), 18M+ spikes/sec.
10
+
11
+ ## Why?
12
+
13
+ OpenFly is a trading system. The brain model inside it is excellent but
14
+ inseparable from the broker / market / front-end code. This package
15
+ isolates the brain so it can be reused for **any** sensor → readout task:
16
+ trading, robotics, audio classification, RL environments, etc.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ # Minimum (no MaleCNS, no sklearn):
22
+ pip install flybrainer
23
+
24
+ # With everything:
25
+ pip install flybrainer[feather,readout,download]
26
+ ```
27
+
28
+ ## 30-second example
29
+
30
+ ```python
31
+ import numpy as np
32
+ from flybrainer import Brain, Stimulus
33
+
34
+ # Build a tiny synthetic graph (no MaleCNS required)
35
+ n = 100
36
+ g = dict(
37
+ ptr=np.array([0, n], dtype=np.int64),
38
+ post=np.random.randint(0, n, size=n).astype(np.int32),
39
+ weight=np.where(np.random.rand(n) > 0.5, 0.275, -0.275),
40
+ modulatory=np.zeros(n, dtype=bool),
41
+ type=np.array(["L1"]*90 + ["KC"]*4 + ["MBON07"]*3 + ["MBON11"]*3),
42
+ superclass=np.array(["cb_int"]*n),
43
+ r16_uv=np.zeros((20, 2), dtype=np.float32),
44
+ r8_uv=np.zeros((20, 2), dtype=np.float32),
45
+ r16_eye=np.zeros(20, dtype=np.int8),
46
+ r8_eye=np.zeros(20, dtype=np.int8),
47
+ r8_channel=np.ones(20, dtype=np.int8),
48
+ r16=np.arange(20, dtype=np.int32),
49
+ r8=np.arange(20, dtype=np.int32),
50
+ )
51
+ brain = Brain(graph_path="<test>", graph=g)
52
+ stim = Stimulus(r16=np.random.rand(20), r8=np.random.rand(20))
53
+ result = brain.observe(stim, neural_ms=500.0)
54
+ print("total spikes:", result.counts.sum())
55
+ ```
56
+
57
+ See `examples/` for more.
58
+
59
+ ## Architecture
60
+
61
+ ```
62
+ src/flybrainer/
63
+ ├── interfaces.py # zero-dep contracts (Stimulus, BrainProtocol, SensorFrame)
64
+ ├── eyemap.py # EyeMap dataclass + resolver
65
+ ├── encoders.py # ChartEncoder, BarsEncoder, FeatureEncoder + make_encoder()
66
+ ├── kernel.py # Numba @njit LIF kernel + KernelState
67
+ ├── plasticity.py # KC→MBON plasticity (Ormond-style Oja)
68
+ ├── brain.py # Brain class (the orchestrator)
69
+ ├── readout/
70
+ │ ├── fixed.py # FixedDecoder (no training)
71
+ │ └── reservoir.py # ReservoirReadout (ridge / logistic; needs [readout])
72
+ └── connectome/
73
+ ├── normalize.py # NT sign rules (feather source → arrays; needs [feather])
74
+ ├── compile.py # arrays → graph.npz (multi-platform)
75
+ ├── sources.py # MaleCNS v1.0 metadata + SHA256
76
+ ├── download.py # resumable download (urllib only)
77
+ └── verify.py # sha256 file/array helpers
78
+ ```
79
+
80
+ ## Optional dependencies
81
+
82
+ | extra | adds | when needed |
83
+ |---|---|---|
84
+ | `feather` | `pandas`, `pyarrow` | `from flybrainer.connectome import normalize, compile_graph` |
85
+ | `readout` | `scikit-learn`, `scipy`, `joblib` | `from flybrainer.readout import ReservoirReadout` |
86
+ | `download` | `requests` | `download_source(...)` |
87
+ | `dev` | `pytest`, `pytest-cov`, `ruff` | testing |
88
+
89
+ Without `[feather]`, `compile_graph` still works if `.pre` arrays are
90
+ already on disk (you just can't re-derive from raw `.feather` files).
91
+
92
+ ## Tests
93
+
94
+ ```bash
95
+ pip install flybrainer[feather,readout,dev]
96
+ pytest tests/ -v
97
+ ```
98
+
99
+ ## Citation
100
+
101
+ * Brain model: Aso & Gorinov — *bioRxiv 2024*
102
+ * Connectome: MaleCNS v1.0 — *https://www.malecns.org*
103
+ * This extraction: OpenFly — *github.com/marketcalls/openfly*
104
+
105
+ ## Documentation
106
+
107
+ Detailed Chinese user manual (API contracts, 7 worked scenarios, FAQ):
108
+
109
+ * [`docs/USAGE.zh.md`](docs/USAGE.zh.md) — 991 lines / 10 chapters
110
+
111
+ Quick look-up order: 第 3 章 (core concepts) → 第 5 章 (full API) → 第 6 章 (worked scenarios) → 第 10 章 (FAQ).
112
+
113
+ ## License
114
+
115
+ MIT. The MaleCNS dataset is CC-BY 4.0 (separate license).
@@ -0,0 +1,161 @@
1
+ """End-to-end flybrainer example using a stub Brain (no MaleCNS download).
2
+
3
+ This file is runnable as:
4
+
5
+ python examples/quickstart.py
6
+
7
+ It demonstrates the full pipeline:
8
+ SensorFrame --[Encoder]--> Stimulus --[Brain]--> counts --[Decoder]--> Prediction
9
+
10
+ The "Brain" here is a 256-neuron stub that satisfies ``BrainProtocol`` and
11
+ produces deterministic-ish Poisson counts. Replace it with the real
12
+ ``Brain.load_graph(...)`` once you have compiled a graph from the
13
+ MaleCNS dataset.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from datetime import datetime, timedelta, timezone
19
+ from typing import Any
20
+
21
+ import numpy as np
22
+
23
+ from flybrainer import (
24
+ REQUIRED_POPULATIONS,
25
+ BrainProtocol,
26
+ Decision,
27
+ ObservationResult,
28
+ Prediction,
29
+ SensorFrame,
30
+ Stimulus,
31
+ )
32
+ from flybrainer.encoders import make_encoder
33
+ from flybrainer.readout.fixed import FixedDecoder
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Stub implementation of BrainProtocol. Replace with:
37
+ # from flybrainer import Brain, load_graph
38
+ # brain = Brain(graph_path="connectome/...")
39
+ # once you have a real compiled graph.
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ @dataclass
44
+ class StubBrain(BrainProtocol):
45
+ """Tiny stub that satisfies BrainProtocol without a real connectome."""
46
+
47
+ n_neurons: int = 256
48
+ seed: int = 42
49
+
50
+ def __post_init__(self) -> None:
51
+ self.n: int = int(self.n_neurons)
52
+ self._rng = np.random.default_rng(self.seed)
53
+ self.populations: dict[str, np.ndarray] = {}
54
+ # Carve the neuron range into contiguous slices, one per population.
55
+ cursor = 0
56
+ for name in REQUIRED_POPULATIONS:
57
+ size = max(8, self.n // 32)
58
+ if cursor + size > self.n:
59
+ size = max(1, self.n - cursor)
60
+ self.populations[name] = np.arange(cursor, cursor + size, dtype=np.int32)
61
+ cursor += size
62
+ if cursor >= self.n:
63
+ cursor = self.n - 1
64
+
65
+ def observe(self, stimulus: Stimulus, neural_ms: float) -> ObservationResult:
66
+ """Fake 'spike' counts proportional to the stimulus drive."""
67
+ r16 = float(np.mean(stimulus.r16)) if stimulus.r16.size else 0.0
68
+ r8 = float(np.mean(stimulus.r8)) if stimulus.r8.size else 0.0
69
+ drive = 0.5 * (r16 + r8)
70
+ seconds = max(neural_ms, 1e-9) / 1000.0
71
+ lam = (2.0 + 25.0 * drive) * seconds
72
+ counts = self._rng.poisson(lam, size=self.n).astype(np.int32)
73
+ return ObservationResult(
74
+ counts=counts,
75
+ neural_ms=float(neural_ms),
76
+ sim_ms=0.0,
77
+ compute_seconds=0.0,
78
+ )
79
+
80
+ def checkpoint(self, path: str) -> None:
81
+ # Stub: nothing to do
82
+ pass
83
+
84
+ def restore(self, path: str) -> None:
85
+ pass
86
+
87
+ def provenance(self) -> dict[str, Any]:
88
+ return {"stub": True, "n": self.n}
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Build a synthetic SensorFrame (past-only, frozen).
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ def make_sensor_frame() -> SensorFrame:
97
+ IST = timezone(timedelta(hours=5, minutes=30))
98
+ bars = tuple(
99
+ Bar( # type: ignore[name-defined]
100
+ timestamp=datetime(2024, 1, 15, 9, 25 + i, tzinfo=IST),
101
+ open=21600 + i * 5,
102
+ high=21650 + i * 5,
103
+ low=21580 + i * 5,
104
+ close=21630 + i * 5,
105
+ volume=1000 + i * 50,
106
+ )
107
+ for i in range(3)
108
+ )
109
+ return SensorFrame(
110
+ timestamp=datetime(2024, 1, 15, 9, 30, tzinfo=IST),
111
+ index_bars=bars,
112
+ vix=14.2,
113
+ vix_bars=bars,
114
+ straddle_premium=120.5,
115
+ entry_credit=None,
116
+ days_to_expiry=4.0,
117
+ minutes_since_open=10,
118
+ position_lots=0,
119
+ )
120
+
121
+
122
+ # Need Bar from interfaces to build the sensor frame
123
+ from flybrainer.interfaces import Bar # noqa: E402
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Pipeline
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def main() -> Prediction:
131
+ # ---- 1. Set up the brain ----------------------------------------------
132
+ brain = StubBrain(n_neurons=256)
133
+
134
+ # ---- 2. Encode the market snapshot into photoreceptor currents --------
135
+ encoder = make_encoder("bars")
136
+ sf = make_sensor_frame()
137
+ stimulus = encoder.encode(sf, brain)
138
+ print(f"Stimulus: r16={stimulus.r16.shape} r8={stimulus.r8.shape}")
139
+
140
+ # ---- 3. Run 200 ms of neural dynamics --------------------------------
141
+ result = brain.observe(stimulus, neural_ms=200.0)
142
+ total_spikes = int(result.counts.sum())
143
+ print(f"Observed: {total_spikes} spikes across {brain.n} neurons "
144
+ f"in {result.neural_ms:.0f} ms")
145
+
146
+ # ---- 4. Decode into a Decision ---------------------------------------
147
+ decoder = FixedDecoder(neural_ms=200.0, threshold_hz=2.0)
148
+ pred = decoder.predict(result.counts, brain, sf)
149
+ print(f"Prediction: decision={pred.decision.value} "
150
+ f"roi={pred.realized_over_implied:.3f} "
151
+ f"confidence={pred.confidence:.3f}")
152
+
153
+ # Sanity check on the protocol types
154
+ assert isinstance(pred, Prediction)
155
+ assert pred.decision in {Decision.ENTER, Decision.EXIT, Decision.HOLD}
156
+
157
+ return pred
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()