linoss-dynamics 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.
- linoss_dynamics-0.1.0/LICENSE +21 -0
- linoss_dynamics-0.1.0/PKG-INFO +231 -0
- linoss_dynamics-0.1.0/README.md +201 -0
- linoss_dynamics-0.1.0/pyproject.toml +53 -0
- linoss_dynamics-0.1.0/setup.cfg +4 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics/__init__.py +31 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics/py.typed +0 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics/solver.py +227 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics.egg-info/PKG-INFO +231 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics.egg-info/SOURCES.txt +12 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics.egg-info/dependency_links.txt +1 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics.egg-info/requires.txt +5 -0
- linoss_dynamics-0.1.0/src/linoss_dynamics.egg-info/top_level.txt +1 -0
- linoss_dynamics-0.1.0/tests/test_solver.py +112 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 linoss-dynamics 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.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linoss-dynamics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small NumPy runtime helpers for LinOSS-style oscillator dynamics with explicit damping.
|
|
5
|
+
Author-email: Mani Saint-Victor <drmani215@gmail.com>
|
|
6
|
+
Maintainer-email: Mani Saint-Victor <drmani215@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bionicbutterfly13/linoss-dynamics
|
|
9
|
+
Project-URL: Source, https://github.com/bionicbutterfly13/linoss-dynamics
|
|
10
|
+
Project-URL: Issues, https://github.com/bionicbutterfly13/linoss-dynamics/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Provenance, https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/PROVENANCE.md
|
|
13
|
+
Keywords: linoss,state-space-models,oscillator,dynamics,numpy
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7.4; extra == "test"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# linoss-dynamics
|
|
32
|
+
|
|
33
|
+
[](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
`linoss-dynamics` is a small NumPy runtime package for LinOSS-style oscillator
|
|
39
|
+
dynamics with explicit non-negative damping support.
|
|
40
|
+
|
|
41
|
+
Status: **public alpha**.
|
|
42
|
+
|
|
43
|
+
## How this differs from the upstream LinOSS reference
|
|
44
|
+
|
|
45
|
+
The original [tk-rusch/linoss](https://github.com/tk-rusch/linoss) is a
|
|
46
|
+
**training-time JAX/Equinox neural-network library** that exposes LinOSS as
|
|
47
|
+
trainable layers (`LinOSSLayer`, `LinOSSBlock`, `LinOSS`) inside a deep-learning
|
|
48
|
+
model. It is the right choice when you are building or training a sequence model
|
|
49
|
+
that uses LinOSS as a learnable encoder.
|
|
50
|
+
|
|
51
|
+
`linoss-dynamics` is a **runtime physics package**. Same mathematics, different
|
|
52
|
+
audience and abstraction:
|
|
53
|
+
|
|
54
|
+
| Dimension | Upstream `tk-rusch/linoss` | `linoss-dynamics` |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Use case | Training neural networks with LinOSS layers | Runtime simulation, control, replay-safe systems |
|
|
57
|
+
| Stack | JAX + Equinox | NumPy only — zero deep-learning dependencies |
|
|
58
|
+
| Granularity | Sequence-level via JAX `scan` (`apply_linoss_im`, `apply_linoss_imex`) | Single-step (`linoss_step`, `damped_linoss_step`) |
|
|
59
|
+
| Damping | Implicit (rolled into `A`) | **Explicit, non-negativity validated** (`G` parameter) |
|
|
60
|
+
| Energy diagnostics | Not exposed | `energy`, `delta_energy`, `convergence_window` |
|
|
61
|
+
| Errors | Generic shape errors | Typed hierarchy (`LinOSSError`, `InvalidShapeError`, `InvalidDampingError`, `UnsupportedModeError`) |
|
|
62
|
+
| Determinism contract | Best-effort (training-oriented) | Replay-safe — pure NumPy, deterministic, no hidden state |
|
|
63
|
+
| Install footprint | Heavy (JAX, Equinox, dataset deps) | Tiny (NumPy 1.24+) |
|
|
64
|
+
|
|
65
|
+
### Who needs `linoss-dynamics` specifically
|
|
66
|
+
|
|
67
|
+
- **Replay-safe agentic systems** — needed inside deterministic kernels where
|
|
68
|
+
every step must be byte-reproducible (e.g. consumed by
|
|
69
|
+
[`elume`](https://github.com/bionicbutterfly13/elume) at runtime).
|
|
70
|
+
- **Simulation and control** — robotics, signal processing, vibration analysis,
|
|
71
|
+
and any context that uses oscillatory dynamics as a runtime model rather than
|
|
72
|
+
a learnable layer.
|
|
73
|
+
- **Reference implementation for porting** — a clean NumPy baseline to validate
|
|
74
|
+
custom JAX/PyTorch ports of LinOSS against.
|
|
75
|
+
- **Embedded or minimal-dependency contexts** — anywhere a JAX install is too
|
|
76
|
+
heavy or unavailable.
|
|
77
|
+
- **Teaching and pedagogy** — a self-contained, well-tested, single-file solver
|
|
78
|
+
that students can read end-to-end in an afternoon.
|
|
79
|
+
- **Energy-based analysis** — applications that need to monitor energy drift
|
|
80
|
+
and detect convergence; these diagnostics are not exposed by the upstream
|
|
81
|
+
reference.
|
|
82
|
+
|
|
83
|
+
If you are training a neural network, use the upstream package. If you are
|
|
84
|
+
running a system that needs LinOSS as a deterministic physics step, use this one.
|
|
85
|
+
|
|
86
|
+
## What It Provides
|
|
87
|
+
|
|
88
|
+
- Classic implicit and implicit-explicit LinOSS step helpers.
|
|
89
|
+
- Explicit non-negative damping `G` for D-LinOSS-style runtime damping.
|
|
90
|
+
- Energy, energy-delta, and convergence-window helpers.
|
|
91
|
+
- A dependency-light package core with NumPy as the only runtime dependency.
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
From PyPI (recommended):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install linoss-dynamics
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
From GitHub (latest main):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m pip install "linoss-dynamics @ git+https://github.com/bionicbutterfly13/linoss-dynamics.git@main"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For local development:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/bionicbutterfly13/linoss-dynamics.git
|
|
111
|
+
cd linoss-dynamics
|
|
112
|
+
python -m pip install -e ".[test]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quickstart
|
|
116
|
+
|
|
117
|
+
Classic LinOSS-style step:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
from linoss_dynamics import linoss_step
|
|
122
|
+
|
|
123
|
+
y = np.array([0.2])
|
|
124
|
+
z = np.array([1.0])
|
|
125
|
+
A = np.array([1.0])
|
|
126
|
+
|
|
127
|
+
y_next, z_next, metrics = linoss_step(y, z, A, dt=0.1, mode="implicit")
|
|
128
|
+
|
|
129
|
+
print(y_next, z_next, metrics["energy_after"])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Damped step with explicit `G`:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import numpy as np
|
|
136
|
+
from linoss_dynamics import damped_linoss_step
|
|
137
|
+
|
|
138
|
+
y = np.array([0.2])
|
|
139
|
+
z = np.array([1.0])
|
|
140
|
+
A = np.array([1.0])
|
|
141
|
+
G = np.array([0.5])
|
|
142
|
+
|
|
143
|
+
y_next, z_next, metrics = damped_linoss_step(y, z, A, G, dt=0.1)
|
|
144
|
+
|
|
145
|
+
assert metrics["damping_mode"] == "explicit_g"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Numerical Contract
|
|
149
|
+
|
|
150
|
+
`A` controls oscillator stiffness/frequency.
|
|
151
|
+
|
|
152
|
+
`G` controls damping/forgetting.
|
|
153
|
+
|
|
154
|
+
The package deliberately keeps those controls separate. Damping is not modeled
|
|
155
|
+
as hidden `A` scaling, and negative `G` values are rejected.
|
|
156
|
+
|
|
157
|
+
Supported damping shapes:
|
|
158
|
+
|
|
159
|
+
- scalar `G`
|
|
160
|
+
- vector `G` with the same length as `y` and `z`
|
|
161
|
+
- diagonal matrix `G`
|
|
162
|
+
|
|
163
|
+
Supported step modes:
|
|
164
|
+
|
|
165
|
+
- `implicit` / `IM`
|
|
166
|
+
- `implicit_explicit` / `IMEX`
|
|
167
|
+
|
|
168
|
+
## Public API
|
|
169
|
+
|
|
170
|
+
| Name | Role |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `linoss_step(y, z, A, dt, mode="implicit", B=None, u=None, damping=None, G=None)` | Advance one step, optionally dispatching to explicit damping when `damping` or `G` is supplied. |
|
|
173
|
+
| `damped_linoss_step(y, z, A, G, dt, mode="implicit", B=None, u=None)` | Advance one step with explicit non-negative damping. |
|
|
174
|
+
| `energy(y, z, A)` | Return diagonal oscillator energy. |
|
|
175
|
+
| `delta_energy(previous_energy, next_energy)` | Return signed energy delta. |
|
|
176
|
+
| `convergence_window(deltas, threshold, window)` | Return true when recent absolute deltas are below a threshold. |
|
|
177
|
+
| `linoss_step_impl(...)` | Backward-compatible alias for callers using the implementation name. |
|
|
178
|
+
|
|
179
|
+
Public errors:
|
|
180
|
+
|
|
181
|
+
| Error | Raised when |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `InvalidShapeError` | Inputs cannot be broadcast to the oscillator state. |
|
|
184
|
+
| `InvalidDampingError` | Damping is outside the supported stable path, including negative `G`. |
|
|
185
|
+
| `UnsupportedModeError` | The discretization mode is unsupported. |
|
|
186
|
+
|
|
187
|
+
## Scope
|
|
188
|
+
|
|
189
|
+
The package does not implement JAX training loops, Discretax integrations,
|
|
190
|
+
active-inference runtimes, metacognitive policy, event buses, graph databases,
|
|
191
|
+
or web APIs.
|
|
192
|
+
|
|
193
|
+
Host applications should keep adapters outside this package and depend on
|
|
194
|
+
`linoss-dynamics` through the public API above.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
Run the full local check set:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python -m ruff check src tests
|
|
202
|
+
python -m pytest tests -v --tb=short
|
|
203
|
+
python -m compileall -q src tests
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [PROVENANCE.md](PROVENANCE.md), and
|
|
207
|
+
[CLAIMS.md](CLAIMS.md) before changing package behavior or public wording.
|
|
208
|
+
|
|
209
|
+
## Citation
|
|
210
|
+
|
|
211
|
+
If this package is useful in research or technical writing, cite the package
|
|
212
|
+
metadata in [CITATION.cff](CITATION.cff) and cite the upstream LinOSS and
|
|
213
|
+
D-LinOSS papers listed below.
|
|
214
|
+
|
|
215
|
+
Release and publishing steps are tracked in
|
|
216
|
+
[docs/release-checklist.md](docs/release-checklist.md).
|
|
217
|
+
|
|
218
|
+
## Attribution
|
|
219
|
+
|
|
220
|
+
This package is not the original LinOSS or D-LinOSS research implementation.
|
|
221
|
+
|
|
222
|
+
- LinOSS / Oscillatory State-Space Models: T. Konstantin Rusch and Daniela Rus,
|
|
223
|
+
`Oscillatory State-Space Models`, arXiv:2410.03943.
|
|
224
|
+
- D-LinOSS / learned damping: Jared Boyer, T. Konstantin Rusch, and Daniela Rus,
|
|
225
|
+
`Learning to Dissipate Energy in Oscillatory State-Space Models`,
|
|
226
|
+
arXiv:2505.12171.
|
|
227
|
+
- Official LinOSS ecosystem: <https://github.com/tk-rusch/linoss>.
|
|
228
|
+
- Discretax / former Linax ecosystem: <https://github.com/camail-official/discretax>.
|
|
229
|
+
|
|
230
|
+
See [PROVENANCE.md](PROVENANCE.md) and [CLAIMS.md](CLAIMS.md) before making
|
|
231
|
+
public claims.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# linoss-dynamics
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
`linoss-dynamics` is a small NumPy runtime package for LinOSS-style oscillator
|
|
9
|
+
dynamics with explicit non-negative damping support.
|
|
10
|
+
|
|
11
|
+
Status: **public alpha**.
|
|
12
|
+
|
|
13
|
+
## How this differs from the upstream LinOSS reference
|
|
14
|
+
|
|
15
|
+
The original [tk-rusch/linoss](https://github.com/tk-rusch/linoss) is a
|
|
16
|
+
**training-time JAX/Equinox neural-network library** that exposes LinOSS as
|
|
17
|
+
trainable layers (`LinOSSLayer`, `LinOSSBlock`, `LinOSS`) inside a deep-learning
|
|
18
|
+
model. It is the right choice when you are building or training a sequence model
|
|
19
|
+
that uses LinOSS as a learnable encoder.
|
|
20
|
+
|
|
21
|
+
`linoss-dynamics` is a **runtime physics package**. Same mathematics, different
|
|
22
|
+
audience and abstraction:
|
|
23
|
+
|
|
24
|
+
| Dimension | Upstream `tk-rusch/linoss` | `linoss-dynamics` |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Use case | Training neural networks with LinOSS layers | Runtime simulation, control, replay-safe systems |
|
|
27
|
+
| Stack | JAX + Equinox | NumPy only — zero deep-learning dependencies |
|
|
28
|
+
| Granularity | Sequence-level via JAX `scan` (`apply_linoss_im`, `apply_linoss_imex`) | Single-step (`linoss_step`, `damped_linoss_step`) |
|
|
29
|
+
| Damping | Implicit (rolled into `A`) | **Explicit, non-negativity validated** (`G` parameter) |
|
|
30
|
+
| Energy diagnostics | Not exposed | `energy`, `delta_energy`, `convergence_window` |
|
|
31
|
+
| Errors | Generic shape errors | Typed hierarchy (`LinOSSError`, `InvalidShapeError`, `InvalidDampingError`, `UnsupportedModeError`) |
|
|
32
|
+
| Determinism contract | Best-effort (training-oriented) | Replay-safe — pure NumPy, deterministic, no hidden state |
|
|
33
|
+
| Install footprint | Heavy (JAX, Equinox, dataset deps) | Tiny (NumPy 1.24+) |
|
|
34
|
+
|
|
35
|
+
### Who needs `linoss-dynamics` specifically
|
|
36
|
+
|
|
37
|
+
- **Replay-safe agentic systems** — needed inside deterministic kernels where
|
|
38
|
+
every step must be byte-reproducible (e.g. consumed by
|
|
39
|
+
[`elume`](https://github.com/bionicbutterfly13/elume) at runtime).
|
|
40
|
+
- **Simulation and control** — robotics, signal processing, vibration analysis,
|
|
41
|
+
and any context that uses oscillatory dynamics as a runtime model rather than
|
|
42
|
+
a learnable layer.
|
|
43
|
+
- **Reference implementation for porting** — a clean NumPy baseline to validate
|
|
44
|
+
custom JAX/PyTorch ports of LinOSS against.
|
|
45
|
+
- **Embedded or minimal-dependency contexts** — anywhere a JAX install is too
|
|
46
|
+
heavy or unavailable.
|
|
47
|
+
- **Teaching and pedagogy** — a self-contained, well-tested, single-file solver
|
|
48
|
+
that students can read end-to-end in an afternoon.
|
|
49
|
+
- **Energy-based analysis** — applications that need to monitor energy drift
|
|
50
|
+
and detect convergence; these diagnostics are not exposed by the upstream
|
|
51
|
+
reference.
|
|
52
|
+
|
|
53
|
+
If you are training a neural network, use the upstream package. If you are
|
|
54
|
+
running a system that needs LinOSS as a deterministic physics step, use this one.
|
|
55
|
+
|
|
56
|
+
## What It Provides
|
|
57
|
+
|
|
58
|
+
- Classic implicit and implicit-explicit LinOSS step helpers.
|
|
59
|
+
- Explicit non-negative damping `G` for D-LinOSS-style runtime damping.
|
|
60
|
+
- Energy, energy-delta, and convergence-window helpers.
|
|
61
|
+
- A dependency-light package core with NumPy as the only runtime dependency.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
From PyPI (recommended):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install linoss-dynamics
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
From GitHub (latest main):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m pip install "linoss-dynamics @ git+https://github.com/bionicbutterfly13/linoss-dynamics.git@main"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For local development:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/bionicbutterfly13/linoss-dynamics.git
|
|
81
|
+
cd linoss-dynamics
|
|
82
|
+
python -m pip install -e ".[test]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quickstart
|
|
86
|
+
|
|
87
|
+
Classic LinOSS-style step:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import numpy as np
|
|
91
|
+
from linoss_dynamics import linoss_step
|
|
92
|
+
|
|
93
|
+
y = np.array([0.2])
|
|
94
|
+
z = np.array([1.0])
|
|
95
|
+
A = np.array([1.0])
|
|
96
|
+
|
|
97
|
+
y_next, z_next, metrics = linoss_step(y, z, A, dt=0.1, mode="implicit")
|
|
98
|
+
|
|
99
|
+
print(y_next, z_next, metrics["energy_after"])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Damped step with explicit `G`:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import numpy as np
|
|
106
|
+
from linoss_dynamics import damped_linoss_step
|
|
107
|
+
|
|
108
|
+
y = np.array([0.2])
|
|
109
|
+
z = np.array([1.0])
|
|
110
|
+
A = np.array([1.0])
|
|
111
|
+
G = np.array([0.5])
|
|
112
|
+
|
|
113
|
+
y_next, z_next, metrics = damped_linoss_step(y, z, A, G, dt=0.1)
|
|
114
|
+
|
|
115
|
+
assert metrics["damping_mode"] == "explicit_g"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Numerical Contract
|
|
119
|
+
|
|
120
|
+
`A` controls oscillator stiffness/frequency.
|
|
121
|
+
|
|
122
|
+
`G` controls damping/forgetting.
|
|
123
|
+
|
|
124
|
+
The package deliberately keeps those controls separate. Damping is not modeled
|
|
125
|
+
as hidden `A` scaling, and negative `G` values are rejected.
|
|
126
|
+
|
|
127
|
+
Supported damping shapes:
|
|
128
|
+
|
|
129
|
+
- scalar `G`
|
|
130
|
+
- vector `G` with the same length as `y` and `z`
|
|
131
|
+
- diagonal matrix `G`
|
|
132
|
+
|
|
133
|
+
Supported step modes:
|
|
134
|
+
|
|
135
|
+
- `implicit` / `IM`
|
|
136
|
+
- `implicit_explicit` / `IMEX`
|
|
137
|
+
|
|
138
|
+
## Public API
|
|
139
|
+
|
|
140
|
+
| Name | Role |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `linoss_step(y, z, A, dt, mode="implicit", B=None, u=None, damping=None, G=None)` | Advance one step, optionally dispatching to explicit damping when `damping` or `G` is supplied. |
|
|
143
|
+
| `damped_linoss_step(y, z, A, G, dt, mode="implicit", B=None, u=None)` | Advance one step with explicit non-negative damping. |
|
|
144
|
+
| `energy(y, z, A)` | Return diagonal oscillator energy. |
|
|
145
|
+
| `delta_energy(previous_energy, next_energy)` | Return signed energy delta. |
|
|
146
|
+
| `convergence_window(deltas, threshold, window)` | Return true when recent absolute deltas are below a threshold. |
|
|
147
|
+
| `linoss_step_impl(...)` | Backward-compatible alias for callers using the implementation name. |
|
|
148
|
+
|
|
149
|
+
Public errors:
|
|
150
|
+
|
|
151
|
+
| Error | Raised when |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `InvalidShapeError` | Inputs cannot be broadcast to the oscillator state. |
|
|
154
|
+
| `InvalidDampingError` | Damping is outside the supported stable path, including negative `G`. |
|
|
155
|
+
| `UnsupportedModeError` | The discretization mode is unsupported. |
|
|
156
|
+
|
|
157
|
+
## Scope
|
|
158
|
+
|
|
159
|
+
The package does not implement JAX training loops, Discretax integrations,
|
|
160
|
+
active-inference runtimes, metacognitive policy, event buses, graph databases,
|
|
161
|
+
or web APIs.
|
|
162
|
+
|
|
163
|
+
Host applications should keep adapters outside this package and depend on
|
|
164
|
+
`linoss-dynamics` through the public API above.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
Run the full local check set:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python -m ruff check src tests
|
|
172
|
+
python -m pytest tests -v --tb=short
|
|
173
|
+
python -m compileall -q src tests
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [PROVENANCE.md](PROVENANCE.md), and
|
|
177
|
+
[CLAIMS.md](CLAIMS.md) before changing package behavior or public wording.
|
|
178
|
+
|
|
179
|
+
## Citation
|
|
180
|
+
|
|
181
|
+
If this package is useful in research or technical writing, cite the package
|
|
182
|
+
metadata in [CITATION.cff](CITATION.cff) and cite the upstream LinOSS and
|
|
183
|
+
D-LinOSS papers listed below.
|
|
184
|
+
|
|
185
|
+
Release and publishing steps are tracked in
|
|
186
|
+
[docs/release-checklist.md](docs/release-checklist.md).
|
|
187
|
+
|
|
188
|
+
## Attribution
|
|
189
|
+
|
|
190
|
+
This package is not the original LinOSS or D-LinOSS research implementation.
|
|
191
|
+
|
|
192
|
+
- LinOSS / Oscillatory State-Space Models: T. Konstantin Rusch and Daniela Rus,
|
|
193
|
+
`Oscillatory State-Space Models`, arXiv:2410.03943.
|
|
194
|
+
- D-LinOSS / learned damping: Jared Boyer, T. Konstantin Rusch, and Daniela Rus,
|
|
195
|
+
`Learning to Dissipate Energy in Oscillatory State-Space Models`,
|
|
196
|
+
arXiv:2505.12171.
|
|
197
|
+
- Official LinOSS ecosystem: <https://github.com/tk-rusch/linoss>.
|
|
198
|
+
- Discretax / former Linax ecosystem: <https://github.com/camail-official/discretax>.
|
|
199
|
+
|
|
200
|
+
See [PROVENANCE.md](PROVENANCE.md) and [CLAIMS.md](CLAIMS.md) before making
|
|
201
|
+
public claims.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "linoss-dynamics"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Small NumPy runtime helpers for LinOSS-style oscillator dynamics with explicit damping."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Mani Saint-Victor", email = "drmani215@gmail.com" }]
|
|
14
|
+
maintainers = [{ name = "Mani Saint-Victor", email = "drmani215@gmail.com" }]
|
|
15
|
+
keywords = ["linoss", "state-space-models", "oscillator", "dynamics", "numpy"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.24",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = [
|
|
32
|
+
"pytest>=7.4",
|
|
33
|
+
"ruff>=0.4",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/bionicbutterfly13/linoss-dynamics"
|
|
38
|
+
Source = "https://github.com/bionicbutterfly13/linoss-dynamics"
|
|
39
|
+
Issues = "https://github.com/bionicbutterfly13/linoss-dynamics/issues"
|
|
40
|
+
Changelog = "https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/CHANGELOG.md"
|
|
41
|
+
Provenance = "https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/PROVENANCE.md"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
addopts = "-v --tb=short"
|
|
53
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""NumPy runtime helpers for LinOSS-style oscillator dynamics.
|
|
2
|
+
|
|
3
|
+
The package keeps the core dependency-light and separates stiffness/frequency
|
|
4
|
+
(`A`) from explicit damping/forgetting (`G`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .solver import (
|
|
8
|
+
InvalidDampingError,
|
|
9
|
+
InvalidShapeError,
|
|
10
|
+
LinOSSError,
|
|
11
|
+
UnsupportedModeError,
|
|
12
|
+
convergence_window,
|
|
13
|
+
damped_linoss_step,
|
|
14
|
+
delta_energy,
|
|
15
|
+
energy,
|
|
16
|
+
linoss_step,
|
|
17
|
+
linoss_step_impl,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"InvalidDampingError",
|
|
22
|
+
"InvalidShapeError",
|
|
23
|
+
"LinOSSError",
|
|
24
|
+
"UnsupportedModeError",
|
|
25
|
+
"convergence_window",
|
|
26
|
+
"damped_linoss_step",
|
|
27
|
+
"delta_energy",
|
|
28
|
+
"energy",
|
|
29
|
+
"linoss_step",
|
|
30
|
+
"linoss_step_impl",
|
|
31
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""NumPy LinOSS stepping helpers with explicit damping support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LinOSSError(ValueError):
|
|
11
|
+
"""Base error for LinOSS dynamics failures."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidShapeError(LinOSSError):
|
|
15
|
+
"""Raised when LinOSS inputs cannot be broadcast to the oscillator state."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidDampingError(LinOSSError):
|
|
19
|
+
"""Raised when damping `G` is outside the supported stable path."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnsupportedModeError(LinOSSError):
|
|
23
|
+
"""Raised when a discretization mode is not supported."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_mode(mode: str) -> str:
|
|
27
|
+
normalized = str(mode or "implicit").lower().replace("-", "_")
|
|
28
|
+
if normalized in {"im", "implicit"}:
|
|
29
|
+
return "implicit"
|
|
30
|
+
if normalized in {"imex", "implicit_explicit"}:
|
|
31
|
+
return "implicit_explicit"
|
|
32
|
+
raise UnsupportedModeError(f"Unsupported LinOSS mode: {mode!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _state_vectors(y: Any, z: Any) -> tuple[np.ndarray, np.ndarray]:
|
|
36
|
+
y_arr = np.asarray(y, dtype=float).reshape(-1)
|
|
37
|
+
z_arr = np.asarray(z, dtype=float).reshape(-1)
|
|
38
|
+
if y_arr.shape != z_arr.shape:
|
|
39
|
+
raise InvalidShapeError(
|
|
40
|
+
f"y and z must have the same shape, got {y_arr.shape} and {z_arr.shape}"
|
|
41
|
+
)
|
|
42
|
+
return y_arr, z_arr
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _diagonal_vector(name: str, value: Any, size: int) -> np.ndarray:
|
|
46
|
+
arr = np.asarray(value, dtype=float)
|
|
47
|
+
if arr.ndim == 0:
|
|
48
|
+
return np.full(size, float(arr))
|
|
49
|
+
if arr.ndim == 1:
|
|
50
|
+
if arr.size == 1:
|
|
51
|
+
return np.full(size, float(arr[0]))
|
|
52
|
+
if arr.size == size:
|
|
53
|
+
return arr.astype(float, copy=True)
|
|
54
|
+
if arr.ndim == 2 and arr.shape == (size, size):
|
|
55
|
+
off_diag = arr - np.diag(np.diag(arr))
|
|
56
|
+
if np.allclose(off_diag, 0.0):
|
|
57
|
+
return np.diag(arr).astype(float, copy=True)
|
|
58
|
+
raise InvalidShapeError(
|
|
59
|
+
f"{name} must be scalar, length-{size} vector, or {size}x{size} diagonal matrix"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _forcing_vector(B: Any, u: Any, size: int) -> np.ndarray:
|
|
64
|
+
if B is None and u is None:
|
|
65
|
+
return np.zeros(size)
|
|
66
|
+
|
|
67
|
+
if B is None:
|
|
68
|
+
u_arr = np.asarray(u, dtype=float)
|
|
69
|
+
if u_arr.ndim == 0:
|
|
70
|
+
return np.full(size, float(u_arr))
|
|
71
|
+
u_flat = u_arr.reshape(-1)
|
|
72
|
+
if u_flat.size == size:
|
|
73
|
+
return u_flat
|
|
74
|
+
raise InvalidShapeError(f"u must be scalar or length-{size} when B is omitted")
|
|
75
|
+
|
|
76
|
+
if u is None:
|
|
77
|
+
return np.zeros(size)
|
|
78
|
+
|
|
79
|
+
B_arr = np.asarray(B, dtype=float)
|
|
80
|
+
u_arr = np.asarray(u, dtype=float)
|
|
81
|
+
|
|
82
|
+
if B_arr.ndim == 1:
|
|
83
|
+
if B_arr.size != size:
|
|
84
|
+
raise InvalidShapeError(f"B vector must have length {size}")
|
|
85
|
+
if u_arr.ndim == 0:
|
|
86
|
+
return B_arr * float(u_arr)
|
|
87
|
+
u_flat = u_arr.reshape(-1)
|
|
88
|
+
if u_flat.size == size:
|
|
89
|
+
return B_arr * u_flat
|
|
90
|
+
raise InvalidShapeError(f"u must be scalar or length-{size} for vector B")
|
|
91
|
+
|
|
92
|
+
if B_arr.ndim == 2 and B_arr.shape[0] == size:
|
|
93
|
+
try:
|
|
94
|
+
forcing = B_arr @ u_arr.reshape(-1)
|
|
95
|
+
except ValueError as exc:
|
|
96
|
+
raise InvalidShapeError(f"B and u shapes are incompatible: {exc}") from exc
|
|
97
|
+
return np.asarray(forcing, dtype=float).reshape(-1)
|
|
98
|
+
|
|
99
|
+
raise InvalidShapeError(f"B must be length-{size} vector or matrix with {size} rows")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def energy(y: Any, z: Any, A: Any, G: Any | None = None) -> float:
|
|
103
|
+
"""Return oscillator energy for diagonal LinOSS state parameters."""
|
|
104
|
+
|
|
105
|
+
del G
|
|
106
|
+
y_arr, z_arr = _state_vectors(y, z)
|
|
107
|
+
A_vec = _diagonal_vector("A", A, y_arr.size)
|
|
108
|
+
return float(0.5 * np.sum(A_vec * y_arr**2 + z_arr**2))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def delta_energy(previous_energy: float, next_energy: float) -> float:
|
|
112
|
+
"""Return signed energy delta."""
|
|
113
|
+
|
|
114
|
+
return float(next_energy) - float(previous_energy)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def convergence_window(deltas: list[float] | tuple[float, ...], threshold: float, window: int) -> bool:
|
|
118
|
+
"""Return true when the most recent absolute deltas are all below threshold."""
|
|
119
|
+
|
|
120
|
+
if window <= 0:
|
|
121
|
+
raise ValueError("window must be positive")
|
|
122
|
+
if len(deltas) < window:
|
|
123
|
+
return False
|
|
124
|
+
return all(abs(float(delta)) < float(threshold) for delta in deltas[-window:])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def linoss_step(
|
|
128
|
+
y: Any,
|
|
129
|
+
z: Any,
|
|
130
|
+
A: Any,
|
|
131
|
+
dt: float,
|
|
132
|
+
mode: str = "implicit",
|
|
133
|
+
B: Any | None = None,
|
|
134
|
+
u: Any | None = None,
|
|
135
|
+
damping: Any | None = None,
|
|
136
|
+
G: Any | None = None,
|
|
137
|
+
) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
|
|
138
|
+
"""Advance one LinOSS step with optional explicit damping."""
|
|
139
|
+
|
|
140
|
+
if damping is not None or G is not None:
|
|
141
|
+
return damped_linoss_step(
|
|
142
|
+
y=y,
|
|
143
|
+
z=z,
|
|
144
|
+
A=A,
|
|
145
|
+
G=damping if damping is not None else G,
|
|
146
|
+
dt=dt,
|
|
147
|
+
mode=mode,
|
|
148
|
+
B=B,
|
|
149
|
+
u=u,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
y_arr, z_arr = _state_vectors(y, z)
|
|
153
|
+
A_vec = _diagonal_vector("A", A, y_arr.size)
|
|
154
|
+
forcing = _forcing_vector(B, u, y_arr.size)
|
|
155
|
+
dt_f = float(dt)
|
|
156
|
+
mode_name = _normalize_mode(mode)
|
|
157
|
+
before = energy(y_arr, z_arr, A_vec)
|
|
158
|
+
|
|
159
|
+
if mode_name == "implicit":
|
|
160
|
+
schur = 1.0 / (1.0 + (dt_f**2) * A_vec)
|
|
161
|
+
z_next = schur * z_arr - dt_f * A_vec * schur * y_arr + dt_f * schur * forcing
|
|
162
|
+
y_next = schur * y_arr + dt_f * schur * z_arr + (dt_f**2) * schur * forcing
|
|
163
|
+
elif mode_name == "implicit_explicit":
|
|
164
|
+
z_next = z_arr - dt_f * A_vec * y_arr + dt_f * forcing
|
|
165
|
+
y_next = y_arr + dt_f * z_next
|
|
166
|
+
else: # pragma: no cover - _normalize_mode guards this
|
|
167
|
+
raise UnsupportedModeError(f"Unsupported LinOSS mode: {mode!r}")
|
|
168
|
+
|
|
169
|
+
after = energy(y_next, z_next, A_vec)
|
|
170
|
+
return y_next, z_next, {
|
|
171
|
+
"mode": mode_name,
|
|
172
|
+
"energy_before": before,
|
|
173
|
+
"energy_after": after,
|
|
174
|
+
"delta_energy": delta_energy(before, after),
|
|
175
|
+
"damping_mode": "none",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def damped_linoss_step(
|
|
180
|
+
y: Any,
|
|
181
|
+
z: Any,
|
|
182
|
+
A: Any,
|
|
183
|
+
G: Any,
|
|
184
|
+
dt: float,
|
|
185
|
+
mode: str = "implicit",
|
|
186
|
+
B: Any | None = None,
|
|
187
|
+
u: Any | None = None,
|
|
188
|
+
) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
|
|
189
|
+
"""Advance one LinOSS step with explicit non-negative damping `G`."""
|
|
190
|
+
|
|
191
|
+
y_arr, z_arr = _state_vectors(y, z)
|
|
192
|
+
A_vec = _diagonal_vector("A", A, y_arr.size)
|
|
193
|
+
G_vec = _diagonal_vector("G", G, y_arr.size)
|
|
194
|
+
if np.any(G_vec < 0.0):
|
|
195
|
+
raise InvalidDampingError("G damping must be non-negative")
|
|
196
|
+
|
|
197
|
+
forcing = _forcing_vector(B, u, y_arr.size)
|
|
198
|
+
dt_f = float(dt)
|
|
199
|
+
mode_name = _normalize_mode(mode)
|
|
200
|
+
before = energy(y_arr, z_arr, A_vec)
|
|
201
|
+
|
|
202
|
+
if mode_name == "implicit":
|
|
203
|
+
denom = 1.0 + dt_f * G_vec + (dt_f**2) * A_vec
|
|
204
|
+
z_next = (z_arr - dt_f * A_vec * y_arr + dt_f * forcing) / denom
|
|
205
|
+
y_next = y_arr + dt_f * z_next
|
|
206
|
+
elif mode_name == "implicit_explicit":
|
|
207
|
+
denom = 1.0 + dt_f * G_vec
|
|
208
|
+
z_next = (z_arr - dt_f * A_vec * y_arr + dt_f * forcing) / denom
|
|
209
|
+
y_next = y_arr + dt_f * z_next
|
|
210
|
+
else: # pragma: no cover - _normalize_mode guards this
|
|
211
|
+
raise UnsupportedModeError(f"Unsupported LinOSS mode: {mode!r}")
|
|
212
|
+
|
|
213
|
+
after = energy(y_next, z_next, A_vec)
|
|
214
|
+
return y_next, z_next, {
|
|
215
|
+
"mode": mode_name,
|
|
216
|
+
"energy_before": before,
|
|
217
|
+
"energy_after": after,
|
|
218
|
+
"delta_energy": delta_energy(before, after),
|
|
219
|
+
"damping_mode": "explicit_g",
|
|
220
|
+
"damping": G_vec,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def linoss_step_impl(*args: Any, **kwargs: Any) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
|
|
225
|
+
"""Backward-compatible implementation alias for callers using this name."""
|
|
226
|
+
|
|
227
|
+
return linoss_step(*args, **kwargs)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linoss-dynamics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small NumPy runtime helpers for LinOSS-style oscillator dynamics with explicit damping.
|
|
5
|
+
Author-email: Mani Saint-Victor <drmani215@gmail.com>
|
|
6
|
+
Maintainer-email: Mani Saint-Victor <drmani215@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bionicbutterfly13/linoss-dynamics
|
|
9
|
+
Project-URL: Source, https://github.com/bionicbutterfly13/linoss-dynamics
|
|
10
|
+
Project-URL: Issues, https://github.com/bionicbutterfly13/linoss-dynamics/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Provenance, https://github.com/bionicbutterfly13/linoss-dynamics/blob/main/PROVENANCE.md
|
|
13
|
+
Keywords: linoss,state-space-models,oscillator,dynamics,numpy
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7.4; extra == "test"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# linoss-dynamics
|
|
32
|
+
|
|
33
|
+
[](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
`linoss-dynamics` is a small NumPy runtime package for LinOSS-style oscillator
|
|
39
|
+
dynamics with explicit non-negative damping support.
|
|
40
|
+
|
|
41
|
+
Status: **public alpha**.
|
|
42
|
+
|
|
43
|
+
## How this differs from the upstream LinOSS reference
|
|
44
|
+
|
|
45
|
+
The original [tk-rusch/linoss](https://github.com/tk-rusch/linoss) is a
|
|
46
|
+
**training-time JAX/Equinox neural-network library** that exposes LinOSS as
|
|
47
|
+
trainable layers (`LinOSSLayer`, `LinOSSBlock`, `LinOSS`) inside a deep-learning
|
|
48
|
+
model. It is the right choice when you are building or training a sequence model
|
|
49
|
+
that uses LinOSS as a learnable encoder.
|
|
50
|
+
|
|
51
|
+
`linoss-dynamics` is a **runtime physics package**. Same mathematics, different
|
|
52
|
+
audience and abstraction:
|
|
53
|
+
|
|
54
|
+
| Dimension | Upstream `tk-rusch/linoss` | `linoss-dynamics` |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Use case | Training neural networks with LinOSS layers | Runtime simulation, control, replay-safe systems |
|
|
57
|
+
| Stack | JAX + Equinox | NumPy only — zero deep-learning dependencies |
|
|
58
|
+
| Granularity | Sequence-level via JAX `scan` (`apply_linoss_im`, `apply_linoss_imex`) | Single-step (`linoss_step`, `damped_linoss_step`) |
|
|
59
|
+
| Damping | Implicit (rolled into `A`) | **Explicit, non-negativity validated** (`G` parameter) |
|
|
60
|
+
| Energy diagnostics | Not exposed | `energy`, `delta_energy`, `convergence_window` |
|
|
61
|
+
| Errors | Generic shape errors | Typed hierarchy (`LinOSSError`, `InvalidShapeError`, `InvalidDampingError`, `UnsupportedModeError`) |
|
|
62
|
+
| Determinism contract | Best-effort (training-oriented) | Replay-safe — pure NumPy, deterministic, no hidden state |
|
|
63
|
+
| Install footprint | Heavy (JAX, Equinox, dataset deps) | Tiny (NumPy 1.24+) |
|
|
64
|
+
|
|
65
|
+
### Who needs `linoss-dynamics` specifically
|
|
66
|
+
|
|
67
|
+
- **Replay-safe agentic systems** — needed inside deterministic kernels where
|
|
68
|
+
every step must be byte-reproducible (e.g. consumed by
|
|
69
|
+
[`elume`](https://github.com/bionicbutterfly13/elume) at runtime).
|
|
70
|
+
- **Simulation and control** — robotics, signal processing, vibration analysis,
|
|
71
|
+
and any context that uses oscillatory dynamics as a runtime model rather than
|
|
72
|
+
a learnable layer.
|
|
73
|
+
- **Reference implementation for porting** — a clean NumPy baseline to validate
|
|
74
|
+
custom JAX/PyTorch ports of LinOSS against.
|
|
75
|
+
- **Embedded or minimal-dependency contexts** — anywhere a JAX install is too
|
|
76
|
+
heavy or unavailable.
|
|
77
|
+
- **Teaching and pedagogy** — a self-contained, well-tested, single-file solver
|
|
78
|
+
that students can read end-to-end in an afternoon.
|
|
79
|
+
- **Energy-based analysis** — applications that need to monitor energy drift
|
|
80
|
+
and detect convergence; these diagnostics are not exposed by the upstream
|
|
81
|
+
reference.
|
|
82
|
+
|
|
83
|
+
If you are training a neural network, use the upstream package. If you are
|
|
84
|
+
running a system that needs LinOSS as a deterministic physics step, use this one.
|
|
85
|
+
|
|
86
|
+
## What It Provides
|
|
87
|
+
|
|
88
|
+
- Classic implicit and implicit-explicit LinOSS step helpers.
|
|
89
|
+
- Explicit non-negative damping `G` for D-LinOSS-style runtime damping.
|
|
90
|
+
- Energy, energy-delta, and convergence-window helpers.
|
|
91
|
+
- A dependency-light package core with NumPy as the only runtime dependency.
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
From PyPI (recommended):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install linoss-dynamics
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
From GitHub (latest main):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m pip install "linoss-dynamics @ git+https://github.com/bionicbutterfly13/linoss-dynamics.git@main"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For local development:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/bionicbutterfly13/linoss-dynamics.git
|
|
111
|
+
cd linoss-dynamics
|
|
112
|
+
python -m pip install -e ".[test]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quickstart
|
|
116
|
+
|
|
117
|
+
Classic LinOSS-style step:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
from linoss_dynamics import linoss_step
|
|
122
|
+
|
|
123
|
+
y = np.array([0.2])
|
|
124
|
+
z = np.array([1.0])
|
|
125
|
+
A = np.array([1.0])
|
|
126
|
+
|
|
127
|
+
y_next, z_next, metrics = linoss_step(y, z, A, dt=0.1, mode="implicit")
|
|
128
|
+
|
|
129
|
+
print(y_next, z_next, metrics["energy_after"])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Damped step with explicit `G`:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import numpy as np
|
|
136
|
+
from linoss_dynamics import damped_linoss_step
|
|
137
|
+
|
|
138
|
+
y = np.array([0.2])
|
|
139
|
+
z = np.array([1.0])
|
|
140
|
+
A = np.array([1.0])
|
|
141
|
+
G = np.array([0.5])
|
|
142
|
+
|
|
143
|
+
y_next, z_next, metrics = damped_linoss_step(y, z, A, G, dt=0.1)
|
|
144
|
+
|
|
145
|
+
assert metrics["damping_mode"] == "explicit_g"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Numerical Contract
|
|
149
|
+
|
|
150
|
+
`A` controls oscillator stiffness/frequency.
|
|
151
|
+
|
|
152
|
+
`G` controls damping/forgetting.
|
|
153
|
+
|
|
154
|
+
The package deliberately keeps those controls separate. Damping is not modeled
|
|
155
|
+
as hidden `A` scaling, and negative `G` values are rejected.
|
|
156
|
+
|
|
157
|
+
Supported damping shapes:
|
|
158
|
+
|
|
159
|
+
- scalar `G`
|
|
160
|
+
- vector `G` with the same length as `y` and `z`
|
|
161
|
+
- diagonal matrix `G`
|
|
162
|
+
|
|
163
|
+
Supported step modes:
|
|
164
|
+
|
|
165
|
+
- `implicit` / `IM`
|
|
166
|
+
- `implicit_explicit` / `IMEX`
|
|
167
|
+
|
|
168
|
+
## Public API
|
|
169
|
+
|
|
170
|
+
| Name | Role |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `linoss_step(y, z, A, dt, mode="implicit", B=None, u=None, damping=None, G=None)` | Advance one step, optionally dispatching to explicit damping when `damping` or `G` is supplied. |
|
|
173
|
+
| `damped_linoss_step(y, z, A, G, dt, mode="implicit", B=None, u=None)` | Advance one step with explicit non-negative damping. |
|
|
174
|
+
| `energy(y, z, A)` | Return diagonal oscillator energy. |
|
|
175
|
+
| `delta_energy(previous_energy, next_energy)` | Return signed energy delta. |
|
|
176
|
+
| `convergence_window(deltas, threshold, window)` | Return true when recent absolute deltas are below a threshold. |
|
|
177
|
+
| `linoss_step_impl(...)` | Backward-compatible alias for callers using the implementation name. |
|
|
178
|
+
|
|
179
|
+
Public errors:
|
|
180
|
+
|
|
181
|
+
| Error | Raised when |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `InvalidShapeError` | Inputs cannot be broadcast to the oscillator state. |
|
|
184
|
+
| `InvalidDampingError` | Damping is outside the supported stable path, including negative `G`. |
|
|
185
|
+
| `UnsupportedModeError` | The discretization mode is unsupported. |
|
|
186
|
+
|
|
187
|
+
## Scope
|
|
188
|
+
|
|
189
|
+
The package does not implement JAX training loops, Discretax integrations,
|
|
190
|
+
active-inference runtimes, metacognitive policy, event buses, graph databases,
|
|
191
|
+
or web APIs.
|
|
192
|
+
|
|
193
|
+
Host applications should keep adapters outside this package and depend on
|
|
194
|
+
`linoss-dynamics` through the public API above.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
Run the full local check set:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python -m ruff check src tests
|
|
202
|
+
python -m pytest tests -v --tb=short
|
|
203
|
+
python -m compileall -q src tests
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [PROVENANCE.md](PROVENANCE.md), and
|
|
207
|
+
[CLAIMS.md](CLAIMS.md) before changing package behavior or public wording.
|
|
208
|
+
|
|
209
|
+
## Citation
|
|
210
|
+
|
|
211
|
+
If this package is useful in research or technical writing, cite the package
|
|
212
|
+
metadata in [CITATION.cff](CITATION.cff) and cite the upstream LinOSS and
|
|
213
|
+
D-LinOSS papers listed below.
|
|
214
|
+
|
|
215
|
+
Release and publishing steps are tracked in
|
|
216
|
+
[docs/release-checklist.md](docs/release-checklist.md).
|
|
217
|
+
|
|
218
|
+
## Attribution
|
|
219
|
+
|
|
220
|
+
This package is not the original LinOSS or D-LinOSS research implementation.
|
|
221
|
+
|
|
222
|
+
- LinOSS / Oscillatory State-Space Models: T. Konstantin Rusch and Daniela Rus,
|
|
223
|
+
`Oscillatory State-Space Models`, arXiv:2410.03943.
|
|
224
|
+
- D-LinOSS / learned damping: Jared Boyer, T. Konstantin Rusch, and Daniela Rus,
|
|
225
|
+
`Learning to Dissipate Energy in Oscillatory State-Space Models`,
|
|
226
|
+
arXiv:2505.12171.
|
|
227
|
+
- Official LinOSS ecosystem: <https://github.com/tk-rusch/linoss>.
|
|
228
|
+
- Discretax / former Linax ecosystem: <https://github.com/camail-official/discretax>.
|
|
229
|
+
|
|
230
|
+
See [PROVENANCE.md](PROVENANCE.md) and [CLAIMS.md](CLAIMS.md) before making
|
|
231
|
+
public claims.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/linoss_dynamics/__init__.py
|
|
5
|
+
src/linoss_dynamics/py.typed
|
|
6
|
+
src/linoss_dynamics/solver.py
|
|
7
|
+
src/linoss_dynamics.egg-info/PKG-INFO
|
|
8
|
+
src/linoss_dynamics.egg-info/SOURCES.txt
|
|
9
|
+
src/linoss_dynamics.egg-info/dependency_links.txt
|
|
10
|
+
src/linoss_dynamics.egg-info/requires.txt
|
|
11
|
+
src/linoss_dynamics.egg-info/top_level.txt
|
|
12
|
+
tests/test_solver.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linoss_dynamics
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from linoss_dynamics import (
|
|
7
|
+
InvalidDampingError,
|
|
8
|
+
convergence_window,
|
|
9
|
+
damped_linoss_step,
|
|
10
|
+
linoss_step,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_linoss_im_forced_closed_form_step():
|
|
15
|
+
y = np.array([0.2, -0.1])
|
|
16
|
+
z = np.array([0.3, 0.05])
|
|
17
|
+
A = np.array([2.0, 0.5])
|
|
18
|
+
dt = 0.1
|
|
19
|
+
B = np.array([[1.2, 0.4], [0.1, 0.9]])
|
|
20
|
+
u = np.array([0.5, -0.2])
|
|
21
|
+
|
|
22
|
+
y_next, z_next, metrics = linoss_step(y, z, A, dt, mode="IM", B=B, u=u)
|
|
23
|
+
|
|
24
|
+
forcing = B @ u
|
|
25
|
+
schur = 1.0 / (1.0 + (dt**2) * A)
|
|
26
|
+
z_expected = schur * z - dt * A * schur * y + dt * schur * forcing
|
|
27
|
+
y_expected = schur * y + dt * schur * z + (dt**2) * schur * forcing
|
|
28
|
+
|
|
29
|
+
np.testing.assert_allclose(y_next, y_expected)
|
|
30
|
+
np.testing.assert_allclose(z_next, z_expected)
|
|
31
|
+
assert metrics["mode"] == "implicit"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_linoss_imex_alias_and_forcing():
|
|
35
|
+
y = np.array([1.0])
|
|
36
|
+
z = np.array([0.0])
|
|
37
|
+
A = np.array([1.0])
|
|
38
|
+
dt = 0.05
|
|
39
|
+
B = np.array([[1.0]])
|
|
40
|
+
u = np.array([0.2])
|
|
41
|
+
|
|
42
|
+
y_next, z_next, metrics = linoss_step(y, z, A, dt, mode="IMEX", B=B, u=u)
|
|
43
|
+
|
|
44
|
+
forcing = (B @ u)[0]
|
|
45
|
+
z_expected = z[0] - dt * A[0] * y[0] + dt * forcing
|
|
46
|
+
y_expected = y[0] + dt * z_expected
|
|
47
|
+
|
|
48
|
+
assert z_next[0] == pytest.approx(z_expected)
|
|
49
|
+
assert y_next[0] == pytest.approx(y_expected)
|
|
50
|
+
assert metrics["mode"] == "implicit_explicit"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_damped_step_uses_g_without_mutating_a():
|
|
54
|
+
y = np.array([0.2])
|
|
55
|
+
z = np.array([1.0])
|
|
56
|
+
A = np.array([1.0])
|
|
57
|
+
A_before = A.copy()
|
|
58
|
+
dt = 0.1
|
|
59
|
+
|
|
60
|
+
_, _, low_metrics = damped_linoss_step(y, z, A, np.array([0.0]), dt)
|
|
61
|
+
_, _, high_metrics = damped_linoss_step(y, z, A, np.array([1.0]), dt)
|
|
62
|
+
|
|
63
|
+
np.testing.assert_array_equal(A, A_before)
|
|
64
|
+
assert high_metrics["energy_after"] < low_metrics["energy_after"]
|
|
65
|
+
assert high_metrics["damping_mode"] == "explicit_g"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("mode", ["implicit", "implicit_explicit"])
|
|
69
|
+
def test_zero_g_matches_classic_step_for_selected_mode(mode):
|
|
70
|
+
y = np.array([0.2, -0.1])
|
|
71
|
+
z = np.array([0.3, 0.05])
|
|
72
|
+
A = np.array([2.0, 0.5])
|
|
73
|
+
dt = 0.1
|
|
74
|
+
|
|
75
|
+
y_classic, z_classic, _ = linoss_step(y, z, A, dt, mode=mode)
|
|
76
|
+
y_damped, z_damped, metrics = damped_linoss_step(y, z, A, 0.0, dt, mode=mode)
|
|
77
|
+
|
|
78
|
+
np.testing.assert_allclose(y_damped, y_classic)
|
|
79
|
+
np.testing.assert_allclose(z_damped, z_classic)
|
|
80
|
+
assert metrics["damping_mode"] == "explicit_g"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_scalar_vector_and_diagonal_damping_are_equivalent():
|
|
84
|
+
y = np.array([0.1, -0.2])
|
|
85
|
+
z = np.array([0.4, 0.3])
|
|
86
|
+
A = np.array([1.0, 2.0])
|
|
87
|
+
dt = 0.05
|
|
88
|
+
|
|
89
|
+
y_scalar, z_scalar, _ = damped_linoss_step(y, z, A, 0.5, dt)
|
|
90
|
+
y_vector, z_vector, _ = damped_linoss_step(y, z, A, np.array([0.5, 0.5]), dt)
|
|
91
|
+
y_diag, z_diag, _ = damped_linoss_step(y, z, A, np.diag([0.5, 0.5]), dt)
|
|
92
|
+
|
|
93
|
+
np.testing.assert_allclose(y_scalar, y_vector)
|
|
94
|
+
np.testing.assert_allclose(z_scalar, z_vector)
|
|
95
|
+
np.testing.assert_allclose(y_scalar, y_diag)
|
|
96
|
+
np.testing.assert_allclose(z_scalar, z_diag)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_negative_damping_rejected():
|
|
100
|
+
with pytest.raises(InvalidDampingError):
|
|
101
|
+
damped_linoss_step(
|
|
102
|
+
y=np.array([1.0]),
|
|
103
|
+
z=np.array([0.0]),
|
|
104
|
+
A=np.array([1.0]),
|
|
105
|
+
G=np.array([-0.1]),
|
|
106
|
+
dt=0.1,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_convergence_window_uses_recent_absolute_deltas():
|
|
111
|
+
assert convergence_window([0.10, 0.01, 0.02, 0.03], threshold=0.05, window=3)
|
|
112
|
+
assert not convergence_window([0.01, 0.20, 0.02], threshold=0.05, window=3)
|