From d6b55f049833a14f6123dc8cbe048abfd82ad384 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 05:45:47 +0000 Subject: [PATCH 1/4] Initial plan From 9448a5e378a420df9075e28dead52cd94829ff7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 05:50:23 +0000 Subject: [PATCH 2/4] release-prep: tests, CI, plot_trajectories fix, packaging, README polish --- .github/workflows/ci.yml | 30 +++ CITATION.cff | 1 + README.md | 78 ++++-- __pycache__/engine.cpython-312.pyc | Bin 0 -> 32552 bytes engine.py | 48 +++- jump_diffusion_engine.egg-info/PKG-INFO | 196 ++++++++++++++ jump_diffusion_engine.egg-info/SOURCES.txt | 10 + .../dependency_links.txt | 1 + jump_diffusion_engine.egg-info/requires.txt | 6 + jump_diffusion_engine.egg-info/top_level.txt | 1 + pyproject.toml | 26 ++ release/CHANGELOG.md | 18 +- .../test_engine.cpython-312-pytest-9.1.0.pyc | Bin 0 -> 41099 bytes tests/test_engine.py | 253 ++++++++++++++++++ 14 files changed, 644 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 __pycache__/engine.cpython-312.pyc create mode 100644 jump_diffusion_engine.egg-info/PKG-INFO create mode 100644 jump_diffusion_engine.egg-info/SOURCES.txt create mode 100644 jump_diffusion_engine.egg-info/dependency_links.txt create mode 100644 jump_diffusion_engine.egg-info/requires.txt create mode 100644 jump_diffusion_engine.egg-info/top_level.txt create mode 100644 pyproject.toml create mode 100644 tests/__pycache__/test_engine.cpython-312-pytest-9.1.0.pyc create mode 100644 tests/test_engine.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..edfa134 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + run: pytest tests/ -v diff --git a/CITATION.cff b/CITATION.cff index 1087cf2..34c2db8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,7 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." title: "Jump Diffusion Engine" +version: "0.1.0" abstract: "A universal framework for multistable stochastic control." type: software authors: diff --git a/README.md b/README.md index 326a53f..38c8496 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,87 @@ -# Jump-Diffusion-Engine – Perfectly Balanced Stochastic Control -**One set of inputs. Continuous, self-regulating outputs.** +# Jump-Diffusion-Engine – Stochastic Stability Analysis +**Simulate, analyse, and steer noisy nonlinear systems.** -A universal stability port for any dynamic system with a Source (Λ(t)), a Medium (Δ), and a Sink (f(Δ)). +A simulation framework for systems with a Source (Λ(t)), a Medium (Δ), and a nonlinear Sink (f(Δ)) +subject to continuous diffusion and discrete jump noise. ## Core Equation `dΔ = [Λ(t) − f(Δ)] dt + σ dW + J dN` -- `Λ(t) = ε₀ + A·sin(ωt)` — the source, steady and singing -- `f(Δ) = kΔ + gΔ²/(K²+Δ²)` — the sink, linear then saturating -- `Δ* : Λ = f(Δ), f′(Δ*) > 0` — the bowl, the stable held place +- `Λ(t) = ε₀ + A·sin(ωt)` — the source (constant or time-varying) +- `f(Δ) = kΔ + gΔ²/(K²+Δ²)` — the nonlinear sink (linear + saturating) +- `Δ* : Λ = f(Δ), f′(Δ*) > 0` — a stable equilibrium (basin centre) -Use `engine.py` to **force** any stochastic system into its stable basin, then **verify** its resilience: +Use `engine.py` to **analyse** stochastic systems and **steer** trajectories toward stable basins: | # | Action | Method | What it does | |:-|:---|:---|:---| -| 1 | **FORCE** it into the bowl | `seat_and_release()` | Applies a transient push, then **releases control to zero**. The basin holds it forever. | -| 2 | **BREATHE** with the noise | `adaptive_k()` | Dynamically stiffens when calm, relaxes when volatile. Prevents numerical blow-up while maximizing rejection. | -| 3 | **MAP** where the bowls are | `find_fixed_points()` | Finds all stable (`f′>0`) and unstable equilibria before you force it. | -| 4 | **MEASURE** how deep the bowl is | `basin_depth()` | Quantifies the energy barrier—how hard you'd have to push to knock it out. | -| 5 | **PREDICT** escape risk | `escape_probability()` | Empirical escape rate over Monte Carlo trials. Should be near-zero after seating. | -| 6 | **VISUALIZE** the long-term cloud | `stationary_density()` | The normalized PDF \( p(\Delta) \propto e^{-2V/\sigma^2} \)—where it lives once forced. | +| 1 | **Seat** into a stable basin | `seat_and_release()` | Applies a transient corrective push, then releases control. Basin strength determines how well it holds. | +| 2 | **Adapt** to volatility | `adaptive_k()` | Updates the reversion coefficient based on recent variance. | +| 3 | **Map** equilibria | `find_fixed_points()` | Finds stable (`f′>0`) and unstable fixed points for a given Λ. | +| 4 | **Measure** basin depth | `basin_depth()` | Quantifies the potential-energy barrier around each basin. | +| 5 | **Estimate** escape risk | `escape_probability()` | Empirical escape rate via Monte Carlo trials. | +| 6 | **Visualise** the stationary distribution | `stationary_density()` | Computes the Boltzmann-weighted PDF p(Δ) ∝ e^{−2V/σ²}. | ## Installation -This project requires Python 3 and the libraries used by `engine.py`: +**Standard install (recommended)** -- `numpy` -- `scipy` -- `matplotlib` +```bash +pip install -e . +``` + +This installs the package and all dependencies from the root of the repository. -Install them with pip: +**Manual dependency install** + +If you prefer not to use the package install, install dependencies directly: ```bash pip install numpy scipy matplotlib ``` -Then import the engine in your Python code: +Then add the repository root to your Python path and import: ```python from engine import JumpDiffusionEngine ``` +> **Note:** Packaging metadata lives in `packaging/pyproject.toml` (legacy) and the +> root `pyproject.toml` (standard). Use the root `pyproject.toml` for `pip install -e .`. + +## Quick Start + +```python +import numpy as np +from engine import JumpDiffusionEngine + +# Define a source: constant with a small oscillation +def lambda_func(t): + return 0.5 + 0.1 * np.sin(2 * np.pi * 0.1 * t) + +eng = JumpDiffusionEngine(lambda_func, sigma=0.3, jump_rate=0.05, seed=42) + +# 1. Find stable equilibria +fps = eng.find_fixed_points(lambda_val=0.5) +print("Fixed points:", fps) + +# 2. Simulate a few trajectories +results = eng.simulate(t_max=20.0, x0=0.0, n_realizations=3) +eng.plot_trajectories(results) + +# 3. Steer into a stable basin, then release control +result = eng.seat_and_release(t_max=15.0, x0=3.0, lambda_val=0.5) +print(f"Released at step {result['release_idx']}, seated at Δ* ≈ {result['x_star']:.3f}") + +# 4. Estimate escape probability from the basin +p_escape = eng.escape_probability(threshold=2.0, t_max=10.0, n_trials=200) +print(f"Empirical escape probability: {p_escape:.3f}") + +# 5. Stationary density +x, p = eng.stationary_density(lambda_val=0.5) +``` + ## Use Cases Use this on any system that needs to maintain a steady state while being bombarded by both constant noise and sudden, unpredictable shocks. diff --git a/__pycache__/engine.cpython-312.pyc b/__pycache__/engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20e88f319759e3c8458bc325727d38465b31dbcd GIT binary patch literal 32552 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 1 else None + ax_traj.plot(r['t'], r['x'], lw=0.8, alpha=0.7, label=label) + + ax_traj.set_xlabel("Time") + ax_traj.set_ylabel("Δ") + ax_traj.set_title(title) + ax_traj.grid(True, alpha=0.3) + if len(results) > 1: + ax_traj.legend(fontsize=8) + + if has_energy: + ax_en = axes[1, 0] + for i, r in enumerate(results): + if 'energy' in r: + label = f"Run {i + 1}" if len(results) > 1 else None + ax_en.plot(r['t'], r['energy'], lw=0.8, alpha=0.7, label=label) + ax_en.set_xlabel("Time") + ax_en.set_ylabel("Energy V(Δ)") + ax_en.set_title("Potential Energy") + ax_en.grid(True, alpha=0.3) + if len(results) > 1: + ax_en.legend(fontsize=8) + + plt.tight_layout() + plt.show() + return fig # Quick test / usage if __name__ == "__main__": diff --git a/jump_diffusion_engine.egg-info/PKG-INFO b/jump_diffusion_engine.egg-info/PKG-INFO new file mode 100644 index 0000000..43aa37c --- /dev/null +++ b/jump_diffusion_engine.egg-info/PKG-INFO @@ -0,0 +1,196 @@ +Metadata-Version: 2.4 +Name: jump-diffusion-engine +Version: 0.1.0 +Summary: Jump diffusion simulation engine for stochastic control systems. +Author: Sarah Marin +License: Apache-2.0 +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: numpy<3.0,>=1.23 +Requires-Dist: scipy<2.0,>=1.10 +Requires-Dist: matplotlib<4.0,>=3.7 +Provides-Extra: dev +Requires-Dist: pytest>=7.0; extra == "dev" +Dynamic: license-file + +# Jump-Diffusion-Engine – Stochastic Stability Analysis +**Simulate, analyse, and steer noisy nonlinear systems.** + +A simulation framework for systems with a Source (Λ(t)), a Medium (Δ), and a nonlinear Sink (f(Δ)) +subject to continuous diffusion and discrete jump noise. + +## Core Equation + +`dΔ = [Λ(t) − f(Δ)] dt + σ dW + J dN` + +- `Λ(t) = ε₀ + A·sin(ωt)` — the source (constant or time-varying) +- `f(Δ) = kΔ + gΔ²/(K²+Δ²)` — the nonlinear sink (linear + saturating) +- `Δ* : Λ = f(Δ), f′(Δ*) > 0` — a stable equilibrium (basin centre) + +Use `engine.py` to **analyse** stochastic systems and **steer** trajectories toward stable basins: + +| # | Action | Method | What it does | +|:-|:---|:---|:---| +| 1 | **Seat** into a stable basin | `seat_and_release()` | Applies a transient corrective push, then releases control. Basin strength determines how well it holds. | +| 2 | **Adapt** to volatility | `adaptive_k()` | Updates the reversion coefficient based on recent variance. | +| 3 | **Map** equilibria | `find_fixed_points()` | Finds stable (`f′>0`) and unstable fixed points for a given Λ. | +| 4 | **Measure** basin depth | `basin_depth()` | Quantifies the potential-energy barrier around each basin. | +| 5 | **Estimate** escape risk | `escape_probability()` | Empirical escape rate via Monte Carlo trials. | +| 6 | **Visualise** the stationary distribution | `stationary_density()` | Computes the Boltzmann-weighted PDF p(Δ) ∝ e^{−2V/σ²}. | + +## Installation + +**Standard install (recommended)** + +```bash +pip install -e . +``` + +This installs the package and all dependencies from the root of the repository. + +**Manual dependency install** + +If you prefer not to use the package install, install dependencies directly: + +```bash +pip install numpy scipy matplotlib +``` + +Then add the repository root to your Python path and import: + +```python +from engine import JumpDiffusionEngine +``` + +> **Note:** Packaging metadata lives in `packaging/pyproject.toml` (legacy) and the +> root `pyproject.toml` (standard). Use the root `pyproject.toml` for `pip install -e .`. + +## Quick Start + +```python +import numpy as np +from engine import JumpDiffusionEngine + +# Define a source: constant with a small oscillation +def lambda_func(t): + return 0.5 + 0.1 * np.sin(2 * np.pi * 0.1 * t) + +eng = JumpDiffusionEngine(lambda_func, sigma=0.3, jump_rate=0.05, seed=42) + +# 1. Find stable equilibria +fps = eng.find_fixed_points(lambda_val=0.5) +print("Fixed points:", fps) + +# 2. Simulate a few trajectories +results = eng.simulate(t_max=20.0, x0=0.0, n_realizations=3) +eng.plot_trajectories(results) + +# 3. Steer into a stable basin, then release control +result = eng.seat_and_release(t_max=15.0, x0=3.0, lambda_val=0.5) +print(f"Released at step {result['release_idx']}, seated at Δ* ≈ {result['x_star']:.3f}") + +# 4. Estimate escape probability from the basin +p_escape = eng.escape_probability(threshold=2.0, t_max=10.0, n_trials=200) +print(f"Empirical escape probability: {p_escape:.3f}") + +# 5. Stationary density +x, p = eng.stationary_density(lambda_val=0.5) +``` + +## Use Cases + +Use this on any system that needs to maintain a steady state while being bombarded by both constant noise and sudden, unpredictable shocks. + +--- + +### 1. Renewable Energy Grid Management + +Power grids are dissipative systems because electricity is used up as soon as it is made. + +- **The Source (`Λ`):** The fluctuating energy from wind turbines or solar panels. +- **The Sink (`f`):** The battery storage and consumer demand that drains the energy. +- **The Jumps (`J`):** Sudden lightning strikes or a power plant going offline. +- **Use Case:** Using your code to ensure the grid frequency stays in the bowl (for example, `60 Hz`) and does not crash during a sudden surge. + +--- + +### 2. Biological Homeostasis (Synthetic Biology) + +In genetic engineering, scientists create circuits inside cells to produce insulin or other chemicals. + +- **The Source (`Λ`):** The nutrients fed to the cell. +- **The Sink (`f`):** The metabolic rate at which the cell uses those nutrients. +- **The Jumps (`J`):** Sudden changes in temperature or pH levels. +- **Use Case:** Programming a cell to keep its internal chemical levels stable even if the environment becomes noisy or shocked. + +--- + +### 3. Automated Financial Trading (Hedge Funds) + +Markets are the definition of jump-diffusion. + +- **The Source (`Λ`):** The underlying growth or drift of an asset. +- **The Sink (`f`):** The mean-reverting force (investors selling when the price is too high). +- **The Jumps (`J`):** A flash crash or a sudden news event. +- **Use Case:** A trading bot that identifies the bowl (the fair value) and does not panic-sell during a Poisson shock, knowing the sink force will pull the price back. + +--- + +### 4. Spacecraft Orientation & Satellite Control + +Satellites must point their antennas precisely at Earth while floating in noisy space. + +- **The Source (`Λ`):** The thrusters or reaction wheels. +- **The Sink (`f`):** The friction of the gyroscopes or the magnetic pull of Earth. +- **The Jumps (`J`):** Tiny meteoroid impacts or solar flares. +- **Use Case:** An autopilot system that keeps the satellite centered in its orientation bowl regardless of constant vibrations or sudden bumps. + +--- + +### 5. Supply Chain & Inventory Control + +A warehouse needs to keep enough stock in the bowl without overflowing or running out. + +- **The Source (`Λ`):** The incoming shipments of goods. +- **The Sink (`f`):** The customer orders draining the inventory. +- **The Jumps (`J`):** A sudden viral trend causing a massive spike in orders. +- **Use Case:** An AI manager that uses your restoring-force logic to automatically adjust order rates so the warehouse does not stay empty after a shock. + +--- + +### 6. Climate Modeling (Resilience Thresholds) + +Ecologists use these models to see whether a forest or ocean can survive climate change. + +- **The Source (`Λ`):** Rainfall and sunlight. +- **The Sink (`f`):** Evaporation and consumption by animals. +- **The Jumps (`J`):** Forest fires or extreme heatwaves. +- **Use Case:** Determining the tipping point—how big a jump (`J`) it takes to push the system out of its bowl so that it can never recover. + +--- + +### 7. AI Training & Safe Learning + +When training a robot to walk, you do not want it to explore so far that it breaks itself. + +- **The Source (`Λ`):** The robot's drive to move forward. +- **The Sink (`f`):** A penalty in the code that gets stronger as the robot gets closer to falling. +- **The Jumps (`J`):** A person pushing the robot or an uneven floor. +- **Use Case:** Ensuring the robot's brain always stays within a safe operational basin. + +```bibtex +@software{SarahMarin_JumpDiffusionEngine_2026, + author = {Sarah Marin}, + title = {JumpDiffusionEngine: A Universal Framework for Multistable Stochastic Control}, + year = {2026}, + publisher = {GitHub}, + url = {https://github.com/beanapologist/Jump-Diffusion-Engine}, + note = {Versioned releases archived on Zenodo}, + doi = {10.5281/zenodo.XXXXXXX} +} +``` + +## License + +This project is licensed under the **Apache License 2.0**. See the `LICENSE` file for details. diff --git a/jump_diffusion_engine.egg-info/SOURCES.txt b/jump_diffusion_engine.egg-info/SOURCES.txt new file mode 100644 index 0000000..88e3137 --- /dev/null +++ b/jump_diffusion_engine.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +LICENSE +README.md +engine.py +pyproject.toml +jump_diffusion_engine.egg-info/PKG-INFO +jump_diffusion_engine.egg-info/SOURCES.txt +jump_diffusion_engine.egg-info/dependency_links.txt +jump_diffusion_engine.egg-info/requires.txt +jump_diffusion_engine.egg-info/top_level.txt +tests/test_engine.py \ No newline at end of file diff --git a/jump_diffusion_engine.egg-info/dependency_links.txt b/jump_diffusion_engine.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jump_diffusion_engine.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/jump_diffusion_engine.egg-info/requires.txt b/jump_diffusion_engine.egg-info/requires.txt new file mode 100644 index 0000000..12dbac7 --- /dev/null +++ b/jump_diffusion_engine.egg-info/requires.txt @@ -0,0 +1,6 @@ +numpy<3.0,>=1.23 +scipy<2.0,>=1.10 +matplotlib<4.0,>=3.7 + +[dev] +pytest>=7.0 diff --git a/jump_diffusion_engine.egg-info/top_level.txt b/jump_diffusion_engine.egg-info/top_level.txt new file mode 100644 index 0000000..45a160d --- /dev/null +++ b/jump_diffusion_engine.egg-info/top_level.txt @@ -0,0 +1 @@ +engine diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d101839 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "jump-diffusion-engine" +version = "0.1.0" +description = "Jump diffusion simulation engine for stochastic control systems." +readme = "README.md" +requires-python = ">=3.8" +license = { text = "Apache-2.0" } +authors = [{ name = "Sarah Marin" }] +dependencies = [ + "numpy>=1.23,<3.0", + "scipy>=1.10,<2.0", + "matplotlib>=3.7,<4.0", +] + +[project.optional-dependencies] +dev = ["pytest>=7.0"] + +[tool.setuptools] +py-modules = ["engine"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/release/CHANGELOG.md b/release/CHANGELOG.md index db90dc6..a2bfb77 100644 --- a/release/CHANGELOG.md +++ b/release/CHANGELOG.md @@ -8,9 +8,25 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] ### Added -- Initial release scaffolding files in `release/`. +- Root-level `pyproject.toml` for standard `pip install -e .` workflow. +- `tests/test_engine.py` — automated test suite covering `find_fixed_points`, + `escape_probability`, `stationary_density`, `seat_and_release`, `simulate`, + and `plot_trajectories`. +- `.github/workflows/ci.yml` — GitHub Actions CI running tests on Python 3.9–3.12. + +### Fixed +- Implemented `plot_trajectories()`, which was previously a stub (`pass`). + +### Changed +- README revised to use more precise, technically defensible language. +- `CITATION.cff` now includes the `version` field (`0.1.0`). ## [0.1.0] - TBD ### Added - Initial public release of Jump-Diffusion-Engine. +- Core SDE simulator (`simulate`), fixed-point analysis (`find_fixed_points`, + `basin_depth`, `potential`), escape probability estimation (`escape_probability`), + stationary density (`stationary_density`), and basin-seating control + (`seat_and_release`, `identify_boundary`). +- Packaging files under `packaging/` and root `pyproject.toml`. diff --git a/tests/__pycache__/test_engine.cpython-312-pytest-9.1.0.pyc b/tests/__pycache__/test_engine.cpython-312-pytest-9.1.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95d56f2a3ff250413e77a1c77d32deae1c20b132 GIT binary patch literal 41099 zcmeHw33MFCdFJ$-JqIuVh5!$d;_wuP;sxHMc#1SdU5qHwI_M~~L-YU~5Cov^0SSx; zvE-Fwa7C}d#A}Ws`2|Y+93o;8=GcUJ$!3*oURL|E$s2G$0v;Z|L||!sYr7MXW^F2g#D2ra!|71RJgZ{+ zx8puLY=tx>9d^vS$jV{=PLXr5ox=*H>)ygs(4l$Wa`TY&%UbX4tUujwyg_lLgB>gR zbs1{DS6pwI3CHW*u18#&{}q=OuX241(&qQO6AiiFJ(f1IM!c8?m3WYYwI;K-CdvRYR`0~<2}76 zIy0^DeNR2QHWb>|*%>#o-K{56Miz-#R=U?n$5ZjPOkY}y8)<;%O&bz%RH>&s`{UhR zD3Qu$s6eVSp6NQ1(le>9tP#qdOl9LIyF0b`*-Z9i{7hQ!kDtu6pN#kD-6v8fGM$-h z|LSonEK+c6Fuob!WTvQ|ss%*N9iNhQFgJQrX*CM=;5oy|$|?%5rDFvH&kDUNF)9YU>Da*u8;k$$l`C#Su#;uKhe-Q5spsOL?=Qu{QS5+81aAT z3i|x4qKfapdK!;0{UsV9uLqEJ+Af#@KiAx zGzT+X+Clo{pX$!wV{SF^0BUr(0{CYi+8Lx?bmf%}>z3>?2byx(ZQ+ptcb4+ABX6gA zG|zxH@5y^l9dNk@e0krHVnnh&D`$t_3faG!H}7jxG#`~}bH7F37!J*!r9Ndx2gA}< zjQ?8L=#0ylFV#=IUv*@~b6RcJEAXIF4wnVJSADyB+F{(E6~<* zXm3wH!(b`BT%QD<>P+hC9=)~ubWci8mm|2GJkgu!%x1cbvY%tJ^1P&Rvb(oaV_B2! zouDS2$*$DtbUBzbQf+Cf0kwhZola$wyg{dp_Hyk$);6Mjy56mqV^)@QUr%SM3q-8D z3*#RstFw(^Bb}&wIZTbm!BO5y_RM-Z+pBjONi>pm zXA<3+1i?AcndY68+u?wU*3PahAyMNpz)P-)+BxI3D@SWrURhSGU0JH#GU$0X+Ay^7 z)xM&-x3C0Yu&=P>Tz*{Li}MDL@`!+=>fZAPkCp78xO`3BJG4<;wt{0+6;DNm!V;0a zxQOmlb>bErnF}0*)>GdxT>D+X+2Pu}`+)1qo>q6etJVDiu;G9w4@^r`&rr?>^PUUt z^AWFWz&qfPu(BBE10V|n{=6^mXIMFq56l89`|<%`WmZbU%CpCTGLHShVda44xo22e z^w*47S>yy(_DFco2`l?7d^bZrzm-p5WmPIMJFKjlSUEUrtSm6Qb$1gh`+$`L_-4EZ zTrU*N?=3>#+dv54QaB&Vhb;- z8AX!04X0jLQEyQS9`20_MUHurT3jPN?XI+T~PTBN0 zIDBlApwmw%$^gv1V2kBO80TMO(b_x?~%ZZ7* ztw-NSu^j{+BCwOdE&{s&WTLWOj%<{P%4VYmc`c&CemP}nX4F9@B1V!L3xZ-~vKa)- zhtKVtlD5pEH1(sg8H|o2b(vVa^hW*0Lc{QhOY?rN^Cy=U>o*Si0f_2sdG)hJ^|8Vc zfWglej-314xcV5*8$8M*0*cz@OlP$66hg7**TU8qu&0k^Y&k#|c121p!` z&$oecyb@7UI)pHR8=6NR(hnggYdGfJIW)(LN#qY?cmU+j_|+^4JnZP8)>R!4Mx2RX z*vi=9K++_Uq*1Eh3B3uT08(SO!~tF9MNTFFOrKXKAsiX1Qtb^^+wEU1nD@Qz`kD~m z1bc525|UooA=T7mrP;svsR#*($ocOOI<(k(6AAVaSE#qI*}w3w|9s1WJ;dSfw2&9r z`S?!8ZiCk^t9shVXuYXUps{A$QC|g+i!(_!&KPfKZFml7Ub>;!(A(2H;#G|MX3n^8 zPDJ!|C|~xZPJsJvV{*Q}o~msjuz|ov0-FeICa{ITRs!1q5+MoyEumE6sP%&co+3b4 zP)2esGLoy{skg8}V`Dsy?p85$C&?8M0HBL%%cKn;y7Y_a2_wk`q}=W)5#FrI(=!^s z3;>kZIBzgybJeRDSG{Y{13kq}mpk}05Xrl97ZkSq%&ymVU23>=Xk=mOzAaZ5jNZ4u zH21*Zp^5vK79&T7wiUDsd&VP2a9-z89uaVqQhBUo2gf2u3Yy4MTzT;AzPCGy%c+{G z^CKU%Wg4FP1kl6mdFmne3m1vIVOk65EzU5Vh4Gq=a5}u`o~lD=EKow?XoNn*Nz0KJ zJmXzxHX5aNI3S$7kobW{roX$$$u$`!ujJQMpa5HzCj}Neq}rU2(p0j%7pOJ_{m&;o zc`tf0{7Fv^1hV&{CnKNq9R;TBaR6! zgfQxTr&dy_fimkyQ3IDD9;OhH)kKgHYKb;8Vq8U_ZL+LNWZ9<3k5F|?E_s$h&jC2- zt{NH11sBoVnFU{>_qIZdmChxmq^{LjN$4@Y0PfaVc4%n4iIlQ45TQMG?^E!|7NQojmC?E<9ZxENQpty_xs`w~~exA%8 z5Z}yHZe&559edDDN;Y@ia|%2sp^q7v0y1AHU=2{lW(qW)#5X%g8Wl2wO~Of-syb{_ zKn1aPVhaR64`T0=NV!v9!5C z?LH;}fZ=!V+GXJj;`n!QT|QWT&QTvh2k0*lAnY(19aLcgh8=8l5U5563<*3#)!CSU zJ`0om=Dhb(h6uX4E7bbX$#bQ4`gBjWKeuH{tT1i4{kWIW4e*ldL%%E9SgpREXqs14 z=M5S|C(iYctMhPP=TROJaFkMctYinr)OkZEL>>{irp}vYFnnXygTYx`*F3VgZhvTG zgd~l%^>JGtge=IDiIN2TNqKyEul~inPk$v3!#2}6LJMhz0(pNv!1Ru4UNt%;-Jp}+ zF_2d=AXO@34@jwu!*I-tk9dZBf&G+#H6^433ARF<2i^j&|E`?B?>jiL714qu&OZFye7GPVNME zwx_#bV$yf#eq_qwpusHzhN~`^f|#!XOkA0^EQDCHu5oJ+4EUBM->Q&qZY%)!E7w1~ zLh`H2;VEXw$*(-J&=5`OsYDOZdN+9iX?PVs6VnNzITRS?@r? zJ%89I=`Qxe&OLOg;p&E~Cx3fq!Clz)waDn)L*LU+QJbhGTv<$bL_Yvf!9-aq+C+e> zH~dDo4zp%8_hjS8u#F#~Jnn%;q5EuEr2zS}lzn|H!QaQCI%A^_(VZC@eUw6^-Zkhe zH!&#v$O_w8y#8qvwzDWrtuVd;0H&*9-gv{R(S}u5T8j;cjw|*{q_)_24+(V`MLxcWrhwCmpgK~u>7q%B8 z4-|TbkG$50z|i(W-)QWC@d%yQd7MW|6yd>ZDA31)G8~IM@S(@mv~X}UL&S#@!LpiU zmfd8s97-lncWb?{jV8;HWRg@nWb;iX^*wkj{V@X16ZkO#tpp4LK>{QgvHsTYrx1Z= z{2R*vUUA)YyM6nVo2t+EgmSai=X)B^?^}7Z&g+|hD-!TE^YT=R>J0Es_tIv`C#b3pbTri+nV^_(%VP7S&kJUvu`p&tKH? zy&sUv$B3sK%$|g7L>_WwUp9F<)mK)#l394qbsBm-vX})v+%w|7tDTrXTl%utNLgNUupDWjyt~3; z6VBloFxYK*jfNRJqQ*tHTwbd=+|Khpx>u)Le87mz07A@3ODE1n{}BRg zfRM13+aV0sU?$>x{K=lN+-a8CXW+YQ;)&Jq_U>$4f10WyL2o3a4^sGUY%(LVw8z@R z#_CMc-U>EIS$mSAJ;`=hlo>meKrV z8+>tST@`+^C*u9I7+EJRT~3oM8$9x7aS{o`kCUfXN&X96mbcGdp|f2 zd@?xkYQ^!-IXJ)cxBqeY_8(r^%?79bD%zmigL5gOP^~ae5K^shhe*<_I1N_jz&_Pz zg_mOg)Pq$&PdEM&fDr&Ni6AF|d~{LyNrQ{-IGfQmQ{fcdoyg$iPn)nkF=My>aay`# z;XZgIxOFw}*4!i*b8iA@&wvY|`VdSVE|~|s?#W@jYAC6sWy6S(Y0pf1W z%3CgXO=wVorn4^gFU5x*hB72e@1>W(83N}B{Cffy2z-S=l`!2$X#_}3Y|I1T!t~;s zs>k=dax+BmR><#L%q3{b+KPX10N=3Vt|e$V=8?O0O7B0A?t*7qe}x21&14Aes~8ls zF=6~DvCi;6wAnyg{**PbwxkQ>g}E5a;Nj2v;UoN#G?bl?l?ReFnLKC)hdpz{H*0zM zpT|cSdgNKRNS=@3#IOswyaFXUAy1K$JT9bubjmBxC(JeUp4st3G3y1!pjo$QroI!x z&WwF0a-#2Mn)PC9Kh!t;ruGB6cuD&)TXHg$_QTYCIFBPOJP^e9A7oYuj2h$brFZQ# zW+H6N)KHmE#*Fo?^Isc3-OZSh`rB!23CBcui1XP_AuX!a_KG0p6ioPG@`spt&XQ^UgaV6CZbHDE+yws!Hn|Dc2xt05g@1|rnEs9SyYH8t41xN^qptSsNp4=4L{=*mY^kYA6YF5;pyptNV6Kfek4BrX>@P-}OVMrTyo1VM z3;BMxyn3dnE`B3cIPyB=_{D=~3QeQY#p5cSH+Y;!1RPZtlLI(qDA_@A`I@?Ts6|}1 zf@4&b$ioBdz9Q!vv8v0ch#bYu`&dC#RA{Qoi6~kko@RbQDXR1FB+9wmxH=!_bspsr z0Y}yO^dOX>WCz9NYwCPzy?NORj!{)S6&VVO$SwlNF^xF44Ls2I$Nglzc%kgD=fLYt z683Pw)?_oV=AU=T^vy}Ik@A|NAowD3M92I};@sI`tK9|yux_Y%SgDzkGnfy8<64%j zv2u1eU^A#WgG|jywr!i^Qf(a8;UFkUBHw(eep1<29a*9Jv~V2e%Gxbzs>+V&Z&gihNMJ11HYlxlTowJk2skWb+J8S)9<0X7&P<3d7G36dhJ4)_Xq zD-^jArpU!CDU&XRvto4!siuWh)gYP|bD*quX3`}~3=V)k1qW2nr&@_V!BnHeV3)%> z1x(I0r^Ju!VX9pau7`+lf#`60gn%^J)|zb_Ut{_BC1~O;ciP_$J<~BXtgMoyqg_vR zLd0%H0=L|(q7acNnvb+Sx5N~7;$2V!chUUT&Ww@Pc3{~BUU*lozG4b)3ugH;I@Xei z>OW2O1hBS4Dx1~I?yesFXDG!-&z57Ykfd@anze;mV_SYAlI6!HT*H)!sg!q8h-^SI z9oweAPAM`8TVD~|V2~^!N$#ehd;t^PO$wXL7)ks2bL&ZRpMrKdmgvVVjDG@{l3smx zRm-n0y>e=7)x%$T_~H{6o*1ql-ZwU9<-2p5E*`pYsBmOh`KiN@-h=h;-H#cx&%;fz z^}=oh&KpC!OVOnW4Icw2MwbpB8;vH$)#q{EFcWx8z)>p3Vyw)OSxP4QbN{< z@8)K7AZ;!uxVdydST`595H6O2$)>OfLA)#74t5{2Kbf)F$URz#Zy`5u8bA_+Ndz`n zX$aVKVJ#Lr2n{ge0QrTam9OKD2`|I`gzh&mu|$83QX>p`vmBU>)!BJ>h+3Y7Wh3fm zP_aD&RmRcV`E=Abkil`*)xycBxGQOOOm4$$k+EIoCA!mp1(*^YzZNms^6hS_HAzUL9O<6uNKYw*>By~ zY${yOXlg^g)|4Hh*+o`KsaEpfs54xB&E4Z)BwGa zL=E!yVxoq>C!IQ#?5hwoF5@;_A;xrJwHDo@tnucYXYIze+~P#MEv3iX^zPHvqhN+D z)^WqqZM~=C&3BYUwfF%d$O%}T>1xZD`@tGJr!qYfP4iz6S4$9l0Vozkr}dn8}& z+}3G`;B*CF!A*@n0j35ete9myoGaCCz!c*tmR9&?wDH|Y!;O4(`Clv=cGaw#*7w~+3Sn(CIbeC%o7H`5+i#nS#a zwTYSzIWM}X@XYZ3!V|^jN8a(jW4xU&9z9l!eEPNrSy<-3w=%P*W=ZTm-@933t@uy~ zJ+jvOb6x-kYwOqOn;aqV%LIOt0GYSA;gpDy{#6T*U$d~bdVC9RhJ3!`@L6{I9#(Ft zVPA+BqWrD+7Y9Boc9<(9&TeJAet#lRZeX8tI+fkmr9GXd)vD4J23Nt}sMCs#lU@wf|o5Y zg4mH7PcTtV>tx(T3xqjz#ukX2XaNj))~qzC-40?P)M*|^b=p1hiyZKt|7*621ypE6 zY~SZyh~>TL#lR_z($u{vln zi%VfFJW5|+GNZv|+Kg!WXQ(c7PAv&v=A2p(!LsL6y1(q_6Hzg@wwZ266falvWX3RH z1c@WVj(F8lKJcezuG5K7xz511&wNG9+ufZ2Cc9kz7rOcH6LO! zC$B8K(pX$dmCGk?LsKM)OusZv6{4x|>wvU_^3_KU-=M|UX+HXF zfsEj?#^e}jVydkThuj25YUTk#%jEq1^pX+Wt!bGYnqeoe+$rnc9fqoeH0De7JIF;@ zVUe+(0&)ux5O#Dp=$R&$AS5z<8T;vH$S33gXp3x1#Pmi}B~^<+GH_b1$F6N!`OE5A*=7LUrAMp%t+Iv2{=k6c&uax$JCaghj{8WR!-#S0n|RcTa>bbawgpF zhptUG)RxKQn!a#GuD@|tG6DR`nObs4(YGSZ2=I{d@_K{=gl4TY&9wk>gKeO2BI+fXLP!|kfK0>BQxX`%DEZx*-S-^sR)#p)VAB>pl+RVNo_J0`3C&v z(gCK#QiY=R-y`hxHi7>{fSllROZRh0C4M#qt5A2>np|4PUIuNLuSh#1kbsi=bcJzf ztA)}Ln9(%U+SzTSP5Th6RI8cq0JMFUp{N|eik{h|DK1&MJf_BDP6J2LMzSJpO@tYW z=e8piwj)+ENAX+eX`C$mHj0;*3S!v(kEsm9?o5OB8+0G0LE}QqHiO2!r8ndHGQLuX z*;F#!Q&%NfS7E&n-c_5Y5@e<=_I2FL_%6Vd0?kCYerQ!Gyl8mkrA4Dlw~j8|KXKoZ z;l${Dt1mrs<;l@C4;R<$FWz@x@QHV0jTavRqK`a#=_tU3hYI;iM@um*1N!W>Sj%|i z*&C5(iyNOsKt4;6XTO4lb|_5>jzyjYmaoi-dJ1`w(+VQzrK3Fc8tP{^k_%psJPRe& zvI9ynyojS%Vi&A~K6!>>e*uB1w!DB92VU@W3@DOfz{}HQXiEfI;Ie9=%gruWjFb++ zBikjk;ALUU!OkdQ`m;(YuKw^cR}0qAIE%G?pGO^((&u^Jbyi74a@(r~>NsE1J+7zV z(GovZwqh%>)4ipoH~8`#yMK}HK^T&&+6V*ksWK#J>121okR&{$lRaB+BEa}7 zG9cw@ZDRmcpHRv^=8nmnixLV`kQ_z@^p5mj1Ay{^`o$=Ro}uZ{Z2fvRtf9@=PS-tf z;*^)*C@;>^#voHal4+)OV5g=oLtmNkBLFYCzFWKGoAXv+!As0!nRDggkA0qKXwdVq z>Z+ad zF}5%0`MscTUqirIwK#+?eQJ*slK3_6WuLWT0ruRO-Xle39fa(RXEP5-|0eTcG7y4D z4PbW*NPV=r4(>1jjVJtCgxwP!?&O=ONfIi3g;fXi0JPo?O<6*1G{(!930!u@h%MQrO7&X|iBb&&*b5KBxeV7-{uOn*!V z+A>uMqH#te6Vl%kis|fC&pV!pbuBQzH6(7ilm@IK|6R)u=-5|ITx=s3pkrj1d+jp_ zTxctNrqq-`=+eUg#iqoihew;%B1m3B$4b$4fY+ky##J~-9=m4d;4uM5Rn*L5B|A8# z9vf=osVGm687trAa~5ORhv_YSC(&T=2SjjPV$UVJFGb z_hHY_{6mWV5rI(x69oQ>0O3N;m@vcPZ&8%MSMhHw0-$M{Uf;Hx5ub0v&04STIprqS zIa+u0C-L%vM>aES zPlhs=r>0G{nn9c~!+gd`W_-&HKZv-jn4iJXsY2Q}*=y&8j2ua^UBW@io?HT?80$<* z$lHOXkXj^9zMv1F>acqYQv9?nv2&sY@)j!&t+sUoGt_BM;0#(@YO&=2?WMIzzGjD1 zlN0+P*Z_-VF;%YSHd$cc_%ZeTwIebZ$2O;nq!&C5s4zNwp{ zZ^TBSic9JjlJ%+%xT8fZ!8qe$Q(o$|`Pzbfy?KWQdkTu+`{$4UBLWB4Z>?3{rx#EHd+xg8D zth?m7;(2@D(7IB?!NCB{G2Z%WzNp3vF9Hna3onjFr1RPc4WOAeoB|9iC zUsK~lTg7E7I7U_RRAeZ;D6)$H>Y9kehW4F%Y*6{8x_r>q9^ z92!@b^V7V}BPGf#E{O~@8-udrvK6e#M3ss>h5c1maS!RT6|BlMIf@of#O4egy|AUI zCScKJ+mR)PmkjTJb0t#VXdPbp`Y8mkH`ysRZ4csI;s%c+B%h;H0Z%B|!7(*K8zSDITnXD9SAWLQ{wDM_2&9j@0RVqDViA86Ea| zv9eE;&@K_F&$}(r2}FOHL?oPvJ~6}aX{vC>{C`<^5`8rz+bwd=LU@vdqI)JF2nWW zmUmDQ^f{z|Ou1GNm|pp2>)lj|T>T#^Xwxi4t^_S|FoJR~PG~=}nVw1tV&oo}OgJ1f z{vqyZ{00Ebosk+WTstk&=p=pgG4?2%$k?220X3;n2cNHiDDqUF7yN|>c%;QEHJ8S>>PyA#&gBk1D4EDl~+uG6@te!fN0Y{T( z+1=Te+cf#}qPt;Z0op1Xi>kL~PGA~Ow%@jMcDDU7sx)Y%;#-R~acv@5n)EaQ5=}p# zyJE;WSU9E8O=Vsp;m*88!x$TdX?bZkFh@Avf&Mv zj$YmW_JMa^9E%tj z9$4^BuozD3Zqh`4Yr7E`Ke1J|B@1Jj7B)%LTBe10UI~fX)UVBK8l`1hg0$F+q(}JI zX0ZE#nLH)-z)3$ObbWAp=BClGRAP?xj_u!n^`=q5I_2djb?O`8tRhUKky)EYg#p?6 z)}eQcnz{?iEXo#-L7F-MJ@WbA#2lDtqAs`RSvZuCGfPMB=^i7F1xMl5qIYA`$?0^e z3r5H@Y2CnDmaF4^mJwyrhR7W_7p`wBETf`n-sc5fGMC&q1~3(H`65{pz|^tC~YHwO#q2n zNoZ%p=MlPehyc?zKT08{Xnux5Oe6eD6ncmN>4q5@GHMa3pg`to0DSTae`HV|qQa1@ z$b%KJ7kfdUgh!02JLjt5=}QouHg+&#R3YV0tluPv(H0P+=-i^ZqOf(i<+U9M4E7av z6r(F3OW?fD<2+KL2oGLEfgL<3L*$rltS`)o6yp6O;QF;*D`l+f*ah zLEXl8)G)^wno%_}Y5kENj7z>)mD9MKvdnjAWOf2TF(T{Vs@X&unTf*L;TK=akwKxa zkSo$UfOKBxaUOvkfJUZiF(_aeSu>Xiz&yadWo(F7?E-d(`sSYq1nEuG5~wFIhXAoD zTyqnl(Eqf+?;>C<1t32#pKsO8T90qZ%}@{?VNE{YQ7k3l#fEsdA|Br=UYts{;$Ix7 zogKI4;m3LFyM9l?Tb@Jw_fwtSSs`&`uzQMN1W;Iv$!n;SRDhbR({&n-3~)GccaQ?A z>VWbAO2JoL*3Nfqb^_H>b!c8}JOW>B%w#YbATXGY^BAC_3tSFbin#%wMr&e0t< zYOHk$wOTNQ_WWquYuLUkz3!^RW!ognhfKjxPyL>>ko5QD;b7EHki zQv*%;89OffGYoyf`p&Q!TTy}|G(v9h1SorOKm7<*_5uO&0T`(%*JCC}4?cJ-i?p_< zpF(B7`5V9@7PR&%Lwn_Vx@L>67#Fj(Ltxu4ff&o-)7@QI3ZjRW^Tvd)&MZfjB*$|8 zhH*8|QVYpEVGzq_voTzXjxj0PT492BBUhAG4_&7YC#K3626)N!r|JXmsj+eOzESl) z*l(Lg7LKV~s<31w|57`LZ|*#N_UeIR?aET^J}h#y;C{9bN@4ln=lIrn*J;1K0_?^2 z%jc-Q3!k(hAMLnTofD<5Sv&4IA|G0E>0b)@Si|P75f5~vKs>bjk&Jl4|H6nz-cxCM z#1j#ShxRnO*N6vG6PN%p53hE5yrVCm!NQ1T6~$zj=it~CUH1bX*JM=6Qg`+eI71*o{Y9*PH9={jGy;E%Y;1}HF+u^~;#&=3 zuOhmfSWpfmlUjEx_8eAEV5vLk6b&{(qdY&!m$F;S?N!jC1xH@%@bZ=xM)7;I{}cK94!1y-O0C5-mO1`r^D$oegWV^kD@3a zxxI?_cF3i`23vA9e9KkyEm!DUt{}FhQoLVOUsk^ueK|U$3~ji$^}^PRJ1^`UcK>|f z&A`t`-;7>SOY!YjH;{7k*|B*Ya_-}b!?z#^tbbEQT()^JN z;P#IxjmqMWTmZMXEmT4uxd3h-RF){8Rz7kOy#18YtgNEUtB|=)S$xw4zzF#N1u&4U AVE_OC literal 0 HcmV?d00001 diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..9084a5d --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,253 @@ +"""Tests for the JumpDiffusionEngine public API. + +All stochastic tests use a fixed seed (42) or rely on statistical invariants +that hold with very high probability, to avoid brittle flakiness. +""" +import numpy as np +import pytest +import sys +import os + +# Allow importing engine from the repo root when running with pytest from any cwd +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from engine import JumpDiffusionEngine + + +# --------------------------------------------------------------------------- +# Shared fixture +# --------------------------------------------------------------------------- + +@pytest.fixture +def engine(): + """Standard engine with a constant lambda and deterministic seed.""" + return JumpDiffusionEngine( + lambda_func=lambda t: 0.5, + sigma=0.3, + jump_rate=0.0, # no jumps — keeps tests deterministic + dt=0.01, + seed=42, + k=0.8, + g=0.5, + K=2.0, + ) + + +# --------------------------------------------------------------------------- +# find_fixed_points +# --------------------------------------------------------------------------- + +class TestFindFixedPoints: + def test_returns_at_least_one_stable_point(self, engine): + fps = engine.find_fixed_points(lambda_val=0.5) + assert len(fps) > 0, "Expected at least one fixed point" + + def test_stable_points_have_positive_f_prime(self, engine): + fps = engine.find_fixed_points(lambda_val=0.5) + stable = [fp for fp in fps if fp['stable']] + assert len(stable) > 0, "Expected at least one stable fixed point" + for fp in stable: + assert fp['f_prime'] > 0, "Stable fixed point must have f' > 0" + + def test_fixed_point_satisfies_f_equals_lambda(self, engine): + lambda_val = 0.5 + fps = engine.find_fixed_points(lambda_val=lambda_val) + for fp in fps: + residual = abs(engine.f_func(fp['x_star']) - lambda_val) + assert residual < 1e-4, f"Fixed point residual too large: {residual}" + + def test_result_structure(self, engine): + fps = engine.find_fixed_points(lambda_val=0.5) + for fp in fps: + assert 'x_star' in fp + assert 'stable' in fp + assert 'f_prime' in fp + assert 'lambda_val' in fp + + def test_no_fixed_points_returns_empty(self, engine): + # With a very large lambda_val that exceeds the sink's maximum, + # there should be no fixed points. + fps = engine.find_fixed_points(lambda_val=1e6) + assert fps == [] + + def test_different_lambda_shifts_fixed_point(self, engine): + fps_low = engine.find_fixed_points(lambda_val=0.1) + fps_high = engine.find_fixed_points(lambda_val=0.9) + x_low = sorted(fp['x_star'] for fp in fps_low if fp['stable']) + x_high = sorted(fp['x_star'] for fp in fps_high if fp['stable']) + # Higher lambda → fixed point shifts to higher x + if x_low and x_high: + assert x_high[0] > x_low[0] + + +# --------------------------------------------------------------------------- +# escape_probability +# --------------------------------------------------------------------------- + +class TestEscapeProbability: + def test_returns_value_in_unit_interval(self, engine): + p = engine.escape_probability(threshold=5.0, t_max=10.0, n_trials=20) + assert 0.0 <= p <= 1.0 + + def test_large_threshold_gives_low_escape(self, engine): + # With a very large threshold, very few (if any) trials should escape. + p = engine.escape_probability(threshold=50.0, t_max=5.0, n_trials=30) + assert p < 0.5, f"Expected low escape probability with large threshold, got {p}" + + def test_tiny_threshold_gives_high_escape(self, engine): + # With a near-zero threshold, almost every trial should "escape". + p = engine.escape_probability(threshold=1e-6, t_max=5.0, n_trials=30) + assert p > 0.5, f"Expected high escape probability with tiny threshold, got {p}" + + def test_explicit_x0_and_x_star(self, engine): + fps = engine.find_fixed_points(0.5) + x_star = fps[0]['x_star'] if fps else 0.0 + p = engine.escape_probability( + threshold=5.0, t_max=5.0, x0=x_star, x_star=x_star, n_trials=20 + ) + assert 0.0 <= p <= 1.0 + + +# --------------------------------------------------------------------------- +# stationary_density +# --------------------------------------------------------------------------- + +class TestStationaryDensity: + def test_returns_arrays_of_matching_length(self, engine): + x, p = engine.stationary_density(lambda_val=0.5) + assert len(x) == len(p) + assert len(x) > 0 + + def test_density_is_normalised(self, engine): + x, p = engine.stationary_density(lambda_val=0.5) + # Numerical integral should be approximately 1. + integral = (np.trapezoid(p, x) if hasattr(np, 'trapezoid') else + np.trapz(p, x)) + assert abs(integral - 1.0) < 0.05, f"Density not normalised: integral={integral}" + + def test_density_is_non_negative(self, engine): + x, p = engine.stationary_density(lambda_val=0.5) + assert np.all(p >= 0), "Density contains negative values" + + def test_peak_near_stable_fixed_point(self, engine): + lambda_val = 0.5 + fps = engine.find_fixed_points(lambda_val) + stable = [fp for fp in fps if fp['stable']] + if not stable: + pytest.skip("No stable fixed point to compare against") + x_star = stable[0]['x_star'] + x, p = engine.stationary_density(lambda_val=lambda_val) + peak_x = x[np.argmax(p)] + assert abs(peak_x - x_star) < 1.5, ( + f"Density peak ({peak_x:.3f}) far from stable equilibrium ({x_star:.3f})" + ) + + +# --------------------------------------------------------------------------- +# seat_and_release +# --------------------------------------------------------------------------- + +class TestSeatAndRelease: + def test_returns_expected_keys(self, engine): + result = engine.seat_and_release(t_max=5.0, x0=3.0, lambda_val=0.5) + for key in ('t', 'x', 'control', 'boundary', 'x_star', 'settle_tol', + 'release_idx', 'released'): + assert key in result, f"Missing key: {key}" + + def test_trajectory_length_matches_time(self, engine): + t_max = 5.0 + result = engine.seat_and_release(t_max=t_max, x0=3.0, lambda_val=0.5) + expected_steps = int(t_max / engine.dt) + 1 + assert len(result['t']) == expected_steps + assert len(result['x']) == expected_steps + + def test_control_is_zero_after_release(self, engine): + result = engine.seat_and_release(t_max=5.0, x0=3.0, lambda_val=0.5) + if result['released'] and result['release_idx'] is not None: + post_control = result['control'][result['release_idx']:] + assert np.allclose(post_control, 0.0), "Control was non-zero after release" + + def test_raises_without_stable_bowl(self): + """seat_and_release should raise when no stable bowl exists.""" + eng = JumpDiffusionEngine( + lambda_func=lambda t: 1e6, # extreme lambda → no fixed points + sigma=0.3, jump_rate=0.0, dt=0.01, seed=42, + ) + with pytest.raises(ValueError, match="No stable bowl"): + eng.seat_and_release(t_max=1.0) + + def test_trajectory_stays_bounded(self, engine): + # After seating, x should not wander to extreme values. + result = engine.seat_and_release(t_max=10.0, x0=1.0, lambda_val=0.5, + dwell=50) + assert np.all(np.abs(result['x']) < 30), "Trajectory left reasonable bounds" + + +# --------------------------------------------------------------------------- +# simulate (smoke / integration test) +# --------------------------------------------------------------------------- + +class TestSimulate: + def test_single_realization_structure(self, engine): + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=1) + assert len(results) == 1 + r = results[0] + assert 't' in r and 'x' in r + assert len(r['t']) == len(r['x']) + + def test_multiple_realizations(self, engine): + n = 5 + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=n) + assert len(results) == n + + def test_seeded_reproducibility(self): + """Same seed → identical trajectory.""" + def lf(t): + return 0.5 + + eng1 = JumpDiffusionEngine(lf, sigma=0.3, jump_rate=0.1, dt=0.01, seed=7) + eng2 = JumpDiffusionEngine(lf, sigma=0.3, jump_rate=0.1, dt=0.01, seed=7) + r1 = eng1.simulate(t_max=1.0, x0=0.0, n_realizations=1)[0] + r2 = eng2.simulate(t_max=1.0, x0=0.0, n_realizations=1)[0] + np.testing.assert_array_equal(r1['x'], r2['x']) + + def test_trajectory_mean_reverting(self, engine): + """With no jumps and strong mean reversion, x should stay bounded.""" + results = engine.simulate(t_max=20.0, x0=5.0, n_realizations=3, + record_energy=False) + for r in results: + assert np.all(np.abs(r['x']) < 50), "Trajectory diverged unexpectedly" + + def test_energy_recorded_when_requested(self, engine): + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=1, + record_energy=True) + assert 'energy' in results[0] + assert len(results[0]['energy']) == len(results[0]['x']) + + def test_energy_not_recorded_when_skipped(self, engine): + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=1, + record_energy=False) + assert 'energy' not in results[0] + + +# --------------------------------------------------------------------------- +# plot_trajectories (non-display smoke test) +# --------------------------------------------------------------------------- + +class TestPlotTrajectories: + def test_returns_figure(self, engine, monkeypatch): + import matplotlib.pyplot as plt + # Suppress display in CI + monkeypatch.setattr(plt, "show", lambda: None) + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=2, + record_energy=True) + fig = engine.plot_trajectories(results, show_energy=True) + assert fig is not None + + def test_accepts_single_realization(self, engine, monkeypatch): + import matplotlib.pyplot as plt + monkeypatch.setattr(plt, "show", lambda: None) + results = engine.simulate(t_max=1.0, x0=0.0, n_realizations=1, + record_energy=False) + fig = engine.plot_trajectories(results) + assert fig is not None From 086c7cb3c99a1225600fda384b3d6f47df4ef3ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 05:52:07 +0000 Subject: [PATCH 3/4] fix: align requires-python to >=3.9, add CI permissions, clarify README packaging note --- .github/workflows/ci.yml | 3 +++ README.md | 5 +++-- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edfa134..35e62df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 38c8496..0dbb632 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,9 @@ Then add the repository root to your Python path and import: from engine import JumpDiffusionEngine ``` -> **Note:** Packaging metadata lives in `packaging/pyproject.toml` (legacy) and the -> root `pyproject.toml` (standard). Use the root `pyproject.toml` for `pip install -e .`. +> **Note:** A legacy `packaging/pyproject.toml` is also present in the repository. +> For standard `pip install -e .` use the root `pyproject.toml`; the `packaging/` +> directory is kept for historical reference and may be removed in a future release. ## Quick Start diff --git a/pyproject.toml b/pyproject.toml index d101839..7e9cbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "jump-diffusion-engine" version = "0.1.0" description = "Jump diffusion simulation engine for stochastic control systems." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "Apache-2.0" } authors = [{ name = "Sarah Marin" }] dependencies = [ From 8a0e7cfc15b5494b0ab6e6331942ad0377e90bf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 06:00:12 +0000 Subject: [PATCH 4/4] ci: add build job with wheel verification and upload artifact --- .github/workflows/ci.yml | 29 ++++++++++++++++++ .gitignore | 23 ++++++++++++++ __pycache__/engine.cpython-312.pyc | Bin 32552 -> 32552 bytes jump_diffusion_engine.egg-info/PKG-INFO | 9 +++--- pyproject.toml | 2 +- .../test_engine.cpython-312-pytest-9.1.0.pyc | Bin 41099 -> 41099 bytes 6 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 .gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35e62df..167cb44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,32 @@ jobs: - name: Run tests run: pytest tests/ -v + + build: + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: python -m pip install --upgrade pip build + + - name: Build distributions + run: python -m build + + - name: Verify wheel installs cleanly + run: | + pip install dist/*.whl + python -c "from engine import JumpDiffusionEngine; print('import OK')" + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d793b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Build / distribution artifacts +*.egg-info/ +build/ +dist/ + +# Python cache +__pycache__/ +*.py[cod] +*.pyo + +# Test / coverage artifacts +.pytest_cache/ +.coverage +htmlcov/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Editor / OS +.DS_Store +*.swp diff --git a/__pycache__/engine.cpython-312.pyc b/__pycache__/engine.cpython-312.pyc index 20e88f319759e3c8458bc325727d38465b31dbcd..217d16f6c0c0b217e5fef27cb4e03520fc15c06f 100644 GIT binary patch delta 21 bcmZ4Sk8#C6My}Jmyj%=G&}P1oORgROQ@sXV delta 21 bcmZ4Sk8#C6My}Jmyj%=G&|$ifORgROQ@;jX diff --git a/jump_diffusion_engine.egg-info/PKG-INFO b/jump_diffusion_engine.egg-info/PKG-INFO index 43aa37c..b845ecf 100644 --- a/jump_diffusion_engine.egg-info/PKG-INFO +++ b/jump_diffusion_engine.egg-info/PKG-INFO @@ -3,8 +3,8 @@ Name: jump-diffusion-engine Version: 0.1.0 Summary: Jump diffusion simulation engine for stochastic control systems. Author: Sarah Marin -License: Apache-2.0 -Requires-Python: >=3.8 +License-Expression: Apache-2.0 +Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: numpy<3.0,>=1.23 @@ -63,8 +63,9 @@ Then add the repository root to your Python path and import: from engine import JumpDiffusionEngine ``` -> **Note:** Packaging metadata lives in `packaging/pyproject.toml` (legacy) and the -> root `pyproject.toml` (standard). Use the root `pyproject.toml` for `pip install -e .`. +> **Note:** A legacy `packaging/pyproject.toml` is also present in the repository. +> For standard `pip install -e .` use the root `pyproject.toml`; the `packaging/` +> directory is kept for historical reference and may be removed in a future release. ## Quick Start diff --git a/pyproject.toml b/pyproject.toml index 7e9cbc1..8521c24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Jump diffusion simulation engine for stochastic control systems." readme = "README.md" requires-python = ">=3.9" -license = { text = "Apache-2.0" } +license = "Apache-2.0" authors = [{ name = "Sarah Marin" }] dependencies = [ "numpy>=1.23,<3.0", diff --git a/tests/__pycache__/test_engine.cpython-312-pytest-9.1.0.pyc b/tests/__pycache__/test_engine.cpython-312-pytest-9.1.0.pyc index 95d56f2a3ff250413e77a1c77d32deae1c20b132..8f48966e277d328d1499e97de27593a4bd40dfea 100644 GIT binary patch delta 21 bcmeA^$kct1iR&~kFBbz4w3%<@YFq#SM~en8 delta 21 bcmeA^$kct1iR&~kFBbz4^qOwuYFq#SN2>-j