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.
@@ -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
+ [![CI](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml/badge.svg)](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
36
+ ![Status](https://img.shields.io/badge/status-public%20alpha-orange)
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
+ [![CI](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml/badge.svg)](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
6
+ ![Status](https://img.shields.io/badge/status-public%20alpha-orange)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![CI](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml/badge.svg)](https://github.com/bionicbutterfly13/linoss-dynamics/actions/workflows/ci.yml)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
36
+ ![Status](https://img.shields.io/badge/status-public%20alpha-orange)
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,5 @@
1
+ numpy>=1.24
2
+
3
+ [test]
4
+ pytest>=7.4
5
+ ruff>=0.4
@@ -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)