Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
633bb44
Limit energy test iterations
slarson Jun 28, 2025
a365ea6
Adjust OpenCL build flags and shorten energy test
slarson Jun 28, 2025
9e395d0
Enable configurable iterations for test mode
slarson Jun 28, 2025
934cb84
Merge pull request #192 from openworm/codex/create-unit-tests-for-eng…
slarson Jun 28, 2025
6158beb
Update makefile
slarson Jun 28, 2025
b00bc04
Merge branch 'ow-0.9.8' into 75pgw2-codex/create-unit-tests-for-engin…
slarson Jun 28, 2025
c98b21f
Merge pull request #193 from openworm/x2nqc4-codex/create-unit-tests-…
slarson Jun 28, 2025
e3df14a
Merge pull request #194 from openworm/75pgw2-codex/create-unit-tests-…
slarson Jun 28, 2025
4832ac6
Add repository guide and module docstrings
slarson Jun 29, 2025
9a1f2b9
Merge pull request #195 from openworm/codex/generate-agents.md-and-im…
slarson Jun 29, 2025
c92ba16
Make tests self-contained with OpenCL
slarson Jun 29, 2025
ec6d268
Merge pull request #196 from openworm/codex/enable-opencl-installatio…
slarson Jun 29, 2025
ea06f5d
Fix OpenCL solver destructor
slarson Jun 29, 2025
9b297ce
Merge pull request #197 from openworm/codex/add-unit-tests-for-opencl…
slarson Jun 29, 2025
2b72003
Remove duplicate import
slarson Jun 29, 2025
151d171
Merge pull request #198 from openworm/codex/add-unit-tests-for-opencl…
slarson Jun 29, 2025
9522b6f
Add setup script and update docs
slarson Jun 29, 2025
6688d18
Update pytorch_solver.py
slarson Jun 29, 2025
0febf02
Update pytorch_solver.py
slarson Jun 29, 2025
24931bf
Merge pull request #199 from openworm/codex/create-pytorch_solver.py-…
slarson Jun 29, 2025
1c4e35f
Enable OpenCL CPU driver and subprocess fallback
slarson Jun 29, 2025
7c016f1
Merge pull request #200 from openworm/codex/investigate-worm_motion-l…
slarson Jun 29, 2025
bf873a6
Add optional PyTorch solver backend
slarson Jun 29, 2025
c0024e4
Merge pull request #201 from openworm/codex/integrate-pytorch_solver-…
slarson Jun 29, 2025
e7fcec8
Update ci-build.yml
slarson Jun 30, 2025
5c472f0
Handle header lines in log loader
slarson Jun 30, 2025
c37e5e8
Merge branch 'ow-pytorch-0.0.1' into codex/update-readme-and-agents.m…
slarson Jun 30, 2025
6968339
Fix torch backend velocity column
slarson Jul 1, 2025
07dc1ec
Merge pull request #208 from openworm/codex/fix-test-failure-in-test_…
slarson Jul 1, 2025
0e8834b
test: relax torch backend tolerance
slarson Jul 1, 2025
f22ed39
Merge pull request #209 from openworm/codex/loosen-assertion-toleranc…
slarson Jul 1, 2025
5838a3d
Fix tests by using correct python and skipping torch when disabled
slarson Jul 1, 2025
b80701e
Merge pull request #210 from openworm/codex/adjust-code-to-pass-test-…
slarson Jul 1, 2025
e384166
Merge pull request #207 from openworm/codex/update-readme-and-agents.…
slarson Jul 1, 2025
162cd03
feat: add macOS support to setup script
slarson Jul 2, 2025
cfae8a9
Merge pull request #211 from openworm/codex/create-setup-script-for-m…
slarson Jul 2, 2025
d887137
Update README.md
slarson Aug 1, 2025
e9b8165
updates to get build working on MacOS
slarson Aug 1, 2025
1c5c30b
Disable OpenCL on Apple Silicon
slarson Aug 8, 2025
86c4bd1
Merge pull request #218 from openworm/codex/modify-build-script-for-a…
slarson Aug 8, 2025
b6c5d1d
Enable header Directory to be set
slarson Aug 8, 2025
36a4301
merging
slarson Aug 8, 2025
bcfbb7a
Successful build on MacOS
slarson Aug 8, 2025
451dcce
Fix boundary collision and pressure instability in PyTorch solver
slarson Dec 27, 2025
d3b0017
Add Taichi GPU backend for Apple Silicon and NVIDIA
slarson Dec 27, 2025
fe80f4c
Fix elastic body cohesion - maintain 90%+ height retention
slarson Dec 27, 2025
e34c070
Add membrane collision system for liquid containment
slarson Dec 30, 2025
41b8af0
Optimize neighbor search with hierarchical parallel prefix sum
slarson Jan 5, 2026
8c22cfa
Add fused neighbor search + density kernel for 3% speedup
slarson Jan 6, 2026
3ebfe58
Add CSR compact neighbor storage + fuse pressure into density
slarson Jan 6, 2026
592aa94
Update PyTorch solver, C++ simulator, Taichi solver, and docs
slarson Feb 13, 2026
6f71130
Add render_movie.py for MP4 video generation from simulation buffers
slarson Feb 13, 2026
f053a45
Make Taichi backend reproducible without PYTHONPATH or a hidden venv
slarson Apr 28, 2026
8041194
Pin Python to 3.10-3.13 and fail loudly if Taichi can't install
slarson Apr 28, 2026
1799485
render_movie.py: fix frame-boundary bug + add boundary/iso-camera opt…
slarson Apr 28, 2026
c896403
ci: add macOS job that builds via setup.sh and smoke-tests Taichi
slarson May 2, 2026
300030a
setup.sh: export NUMPYHEADERDIR from the venv before invoking make
slarson May 2, 2026
d96201b
makefile.OSX/setup.sh: link via python3.13-config instead of -framewo…
slarson May 2, 2026
0443daa
makefile + ci: install numpy and pass its include dir to the Linux build
slarson May 2, 2026
798500d
ci: gate Taichi-Metal step, add Linux Taichi smoke, retire Intel-on-push
slarson May 2, 2026
0129792
Add objective cube-stability test for the pancake failure mode
slarson May 2, 2026
a75b449
test_cube_stability: bump sim time to 5s so the cube actually impacts
slarson May 2, 2026
09a0c1c
Add backend=torch-{mps,cuda,cpu} selection for the PyTorch solver
slarson May 3, 2026
a5253cb
pytorch_solver: replace cdist neighbor search with hash-grid lookup (…
slarson May 3, 2026
61d8fd4
pytorch_solver: cap neighbor-finder candidate pairs to prevent runner…
slarson May 3, 2026
b6b2e83
DEVELOPMENT_LOG: open ow-native-gpu line; close out PyTorch experiment
slarson May 3, 2026
f6dca05
DEVELOPMENT_LOG: dt audit + Taichi-CUDA pancake-check results
slarson May 3, 2026
92183bd
scripts/cross_backend_regression.py: same scenario across multiple ba…
slarson May 3, 2026
9f92972
src/cuda/: scaffold native CUDA backend (skeleton + work plan)
slarson May 3, 2026
92d83fd
DEVELOPMENT_LOG: mark CUDA scaffold + cross-backend regression as done
slarson May 3, 2026
ae14386
DEVELOPMENT_LOG: Taichi solver fix attempt — bigger than the README's…
slarson May 3, 2026
b18c364
src/metal_diff/: hand-written Metal substrate for differentiable Sibe…
slarson May 3, 2026
9a1a027
src/metal_diff/: M8 — distance constraint backward + bond stiffness l…
slarson May 3, 2026
20c0959
src/metal_diff/: Sibernetic config loader — same scenarios as OpenCL
slarson May 3, 2026
3cef63f
DEVELOPMENT_LOG: capture M8 + Sibernetic config loader sessions
slarson May 3, 2026
fb4acc3
src/metal_diff/: M9.A — density value backward chain
slarson May 3, 2026
a61faad
DEVELOPMENT_LOG: capture M9.A success + M9 roadmap + M10 membrane plan
slarson May 3, 2026
85409ba
src/metal_diff/: M9 complete — density chain backward + SGD on rho_rest
slarson May 3, 2026
5b9f581
DEVELOPMENT_LOG: mark M9 complete (B/C/D/E all PASS)
slarson May 3, 2026
d4b9709
src/metal_diff/: multi-step xpbd_full_fwd / _bwd driver
slarson May 3, 2026
4a9ad4f
src/metal_diff/: extract shader source to shaders.metal
slarson May 3, 2026
66dc36b
src/metal_diff/: PERF — beat OpenCL on demo1 (1.42 vs 1.61 ms/step)
slarson May 3, 2026
ff6cb5b
src/metal_diff/: PERF squeeze — 1.0 ms/step (1.6× faster than OpenCL)
slarson May 3, 2026
a06184c
src/metal_diff/: spatial grid + smaller threadgroup (0.91 ms/step)
slarson May 3, 2026
e5814d2
DEVELOPMENT_LOG: apples-to-apples vs OpenCL on demo1 cube fall
slarson May 3, 2026
ea10596
src/metal_diff/: parameter translation — Metal trajectory matches Ope…
slarson May 3, 2026
d9e2b30
src/metal_diff/: add viscosity + surface tension pair-force kernel
slarson May 3, 2026
f975f07
src/metal_diff/: pair_forces_grid_backward + finite-diff validation
slarson May 3, 2026
00624f7
DEVELOPMENT_LOG: viscosity + surface tension forward + backward
slarson May 3, 2026
f1de71a
src/metal_diff/: wire pair_forces into xpbd_full_fwd/bwd (trainable p…
slarson May 4, 2026
40135dc
DEVELOPMENT_LOG: pair_forces wired into trainable path + sub-1% traje…
slarson May 4, 2026
6444ce6
src/metal_diff/: add Hooke spring bond forces (Sibernetic-equivalent)
slarson May 4, 2026
47ce7cb
src/metal_diff/: wire springs into xpbd_full_fwd/bwd (trainable path)
slarson May 4, 2026
79eea2a
DEVELOPMENT_LOG: Hooke springs (visco-elastic Sibernetic-equivalent b…
slarson May 4, 2026
75834f2
src/metal_diff/: analytic ∂L/∂(spring_K, visc_pair_coef) from xpbd_fu…
slarson May 4, 2026
b243d4e
src/metal_diff/: floor wiring + sim_scale_inv in predict_bw + truncat…
slarson May 4, 2026
9b7a30c
src/metal_diff/sgd_true.py: TBPTT + gradient clipping for analytic SGD
slarson May 4, 2026
fc67527
DEVELOPMENT_LOG: analytic-gradient SGD on cube fall
slarson May 5, 2026
1abd5e9
DEVELOPMENT_LOG: SGD Run 3 + cross-substrate transfer surprise
slarson May 5, 2026
3847d30
configuration/demo1_centered: cube shifted to inner-boundary geo center
slarson May 5, 2026
0d344fb
Cube-fall trajectory regression test + render harness
slarson May 5, 2026
5ed2f8e
demo1 backend-parity test (replaces idealized-physics check)
slarson May 5, 2026
f3c9bfe
src/metal_diff/dump_metal_trajectory.py: fix rho_rest default
slarson May 6, 2026
93f43f7
src/metal_diff/sgd_true.py: --target-dm/--target-ext-ratio + freeze f…
slarson May 6, 2026
555575e
scripts/render_demo1_parity.py: uniform sampling + t-max + hide-liquid
slarson May 6, 2026
30918e1
src/metal_diff: fix analytic-backward NaN + add per-step gradient cli…
slarson May 6, 2026
abfadd9
src/metal_diff: add ∂L/∂alpha_dens analytic backward
slarson May 6, 2026
1699fe0
src/metal_diff: elastic floor with tunable restitution + floor-height…
slarson May 6, 2026
8a6e612
purge taichi + pytorch backends; native Metal becomes the differentia…
slarson May 6, 2026
576a281
src/metal_diff: split sib_metal.mm (4570 lines) into 8 modules
slarson May 6, 2026
1de83e1
docs: bundle parity MP4 + add render quickstart, caveats, CUDA path
slarson May 6, 2026
ee3ffe5
docs: embed inline GIF preview of demo1 cube-drop comparison
slarson May 6, 2026
556eebf
docs/scripts: remove references to private sibernetic-runner Cloud Ru…
slarson May 6, 2026
f5f0be1
docs: bundle demo2 membrane reference (OpenCL) + GIF preview
slarson May 6, 2026
78e4199
src/metal_diff/load_config.py: parse [membranes] and [particleMemIndex]
slarson May 6, 2026
6e70264
src/metal_diff/shaders.metal: forward membrane kernels (M10)
slarson May 6, 2026
bf6b333
src/metal_diff: demo2 membrane permeability — full forward + analytic
slarson May 7, 2026
1c53047
configuration: surface configuration/test/one_sprig_test as configura…
slarson May 7, 2026
1192e00
src/metal_diff: one_sprig_test — single-elastic spring port + active …
slarson May 7, 2026
f6617ea
README: restructure top to combined preview + extracted docs/demos.md
slarson May 7, 2026
388caab
src/metal_diff: worm_swim_half_resolution port + M14 boundary kernel
slarson May 8, 2026
161d60a
Add Metal support to the implementation
slarson May 9, 2026
929ec8a
docs+ci: add worm_alone + worm_swim GIFs, fix macOS smoke-test dep
slarson May 9, 2026
c8d6df5
ci: smoke test must cwd at repo root, not src/metal_diff/
slarson May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .github/workflows/ci-build-intel.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
name: Build using Intel drivers

# Manual-only. This workflow validates that Sibernetic still links against
# the legacy 2018 Intel OpenCL CPU runtime (SRB5.0). It does not run the
# binary or any tests — Intel's runtime can't expose itself as a usable
# OpenCL device on the headless GitHub runner ("Seg faults" was the
# original maintainer note). Kept around as a reference for anyone
# resurrecting Intel-driver support; not run on every push/PR because it
# adds CI minutes (sourceforge fetch of the SDK) for zero coverage beyond
# what `Build using AMD drivers` already provides.
#
# Trigger via the Actions UI ("Run workflow") when explicitly needed.
on:
push:
branches: [ master, dev*, ow* ]
pull_request:
branches: [ master, dev, ow* ]
workflow_dispatch:

jobs:

Expand All @@ -14,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runs-on: [ ubuntu-22.04, ubuntu-24.04 ]
runs-on: [ ubuntu-22.04 ]

steps:
- name: Set git to use LF
Expand Down Expand Up @@ -56,7 +63,7 @@ jobs:
- name: Build Sibernetic
run: |

sudo apt install -y python3-dev freeglut3-dev libglu1-mesa-dev
sudo apt install -y python3-dev python3-numpy freeglut3-dev libglu1-mesa-dev
#sudo apt install -y --allow-downgrades libc-bin=2.27-3ubuntu1.5 # Fails with 2.27-3ubuntu1.6 for some reason...
python -V
ls -alt /usr/bin/python*
Expand Down
128 changes: 96 additions & 32 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ master, dev*, ow* ]
pull_request:
branches: [ master, dev, ow* ]
workflow_dispatch:

jobs:

Expand All @@ -21,39 +22,20 @@ jobs:
run: |
git config --global core.autocrlf false
git config --global core.eol lf

- uses: actions/checkout@v4

- name: Install AMD OpenCL libraries needed for Sibernetic
run: |

lscpu

echo "Installing OpenCL Drivers"

# Legacy install of Intel's OpenCL Drivers:
# Based on: https://github.com/openworm/OpenWorm/blob/master/Dockerfile
# mkdir intel-opencl-tmp
# cd intel-opencl-tmp
# mkdir intel-opencl
# wget https://github.com/openworm/OpenWorm/raw/dev_inte/SRB5.0_linux64.zip
# unzip SRB5.0_linux64.zip
# tar -C intel-opencl -Jxf intel-opencl-r5.0-63503.x86_64.tar.xz
# tar -C intel-opencl -Jxf intel-opencl-devel-r5.0-63503.x86_64.tar.xz
# tar -C intel-opencl -Jxf intel-opencl-cpu-r5.0-63503.x86_64.tar.xz
# sudo cp -R intel-opencl/* /
# sudo ldconfig
# cd ..
# sudo rm -r intel-opencl-tmp

# sudo cp -R /opt/intel/opencl/include/CL /usr/include/
# sudo apt install -y ocl-icd-opencl-dev vim

# Install AMD's OpenCL Drivers (AMD-APP-SDK 3.0):
wget https://master.dl.sourceforge.net/project/nicehashsgminerv5viptools/APP%20SDK%20A%20Complete%20Development%20Platform/AMD%20APP%20SDK%203.0%20for%2064-bit%20Linux/AMD-APP-SDKInstaller-v3.0.130.136-GA-linux64.tar.bz2
tar -xf AMD-APP-SDKInstaller-v3.0.130.136-GA-linux64.tar.bz2
printf 'Y\n\n' | sudo ./AMD-APP-SDK-v3.0.130.136-GA-linux64.sh

sudo ln -s /opt/AMDAPPSDK-3.0/lib/x86_64/sdk/libOpenCL.so.1 /usr/lib/libOpenCL.so.1
sudo ln -s /opt/AMDAPPSDK-3.0/lib/x86_64/sdk/libamdocl64.so /usr/lib/libamdocl64.so

Expand All @@ -66,9 +48,7 @@ jobs:

- name: Build Sibernetic
run: |

sudo apt install -y python3-dev freeglut3-dev libglu1-mesa-dev
#sudo apt install -y --allow-downgrades libc-bin=2.27-3ubuntu1.5 # Fails with 2.27-3ubuntu1.6 for some reason...
sudo apt install -y python3-dev python3-numpy freeglut3-dev libglu1-mesa-dev
python -V
ls -alt /usr/bin/python*
ls -alt
Expand All @@ -79,29 +59,113 @@ jobs:
run: |
./Release/Sibernetic -h

- name: Run quick Sibernetic test
- name: Run quick Sibernetic OpenCL test
run: |
ldd ./Release/Sibernetic

./Release/Sibernetic -no_g timelimit=0.001

- name: Run tests
run: |
pip install c302
pip install neuron
pip install c302 neuron pytest pyneuroml
pip list
which nrniv
export NEURON_HOME=/home/runner/.local
./run_all_tests.sh
./run_all_tests.sh

- name: Generated file info
run: |

ls -alt simulations/*
ls -alt simulations/*
more simulations/C1_*/report.json

- name: Final version info
run: |

python -V
pip list


build-macos:
# Native Metal substrate exercise. The main Sibernetic binary on Apple
# Silicon builds with OW_NO_OPENCL (no OpenCL driver available); the
# actual GPU simulation path lives in src/metal_diff/sib_metal, a
# separate Apple-Metal-shader-based binary that implements XPBD on the
# Apple GPU. This job builds and smoke-tests both: the main binary as a
# build-system regression check, the Metal substrate as the real GPU
# path.
runs-on: macos-latest

steps:
- uses: actions/checkout@v6

- name: Environment info
run: |
sw_vers
uname -m
python3 -V
which python3.13 || true
brew --version
xcrun --version

- name: Build main Sibernetic binary via setup.sh
run: ./setup.sh

- name: Print Sibernetic help
run: ./Release/Sibernetic -h

- name: Print main-binary linkage
run: otool -L ./Release/Sibernetic

- name: Build native Metal substrate (sib_metal)
run: |
cd src/metal_diff
./build.sh
ls -la sib_metal

- name: Install numpy for smoke-test driver
run: |
# The smoke test below uses load_config.py which `import numpy as np`.
# macos-latest's system python3 doesn't ship numpy by default; install it.
# `--break-system-packages` is required on Python 3.13+ runners that
# treat the system interpreter as PEP-668 externally-managed.
python3 -m pip install --break-system-packages --quiet numpy

- name: Native Metal substrate smoke test (xpbd_step on demo1)
run: |
# load_to_metal_buffers reads `configuration/<scenario>` relative
# to cwd, so the smoke test must run from the repo root, not from
# src/metal_diff/. Pre-locate the binary so we can call it after
# the cd.
export SIB_METAL=$(pwd)/src/metal_diff/sib_metal
$SIB_METAL --help 2>&1 | head -30 || true
# Exercise the per-step XPBD pipeline: load demo1, run 10 steps,
# check the output file exists. This catches regressions in any of
# the M6/M7 kernels (density solve, distance constraints, floor,
# spring forces, pair forces) because all of them are touched by
# a single xpbd_step call.
python3 - <<PY
import os, sys, subprocess
sys.path.insert(0, 'src/metal_diff')
from load_config import load_to_metal_buffers
info = load_to_metal_buffers('demo1', out_dir='/tmp/ci_demo1', h=3.34)
# Initial pos/vel
import numpy as np
pos = np.fromfile(info['paths']['pos_active'], dtype=np.float32).reshape(-1, 3)
vel = np.zeros_like(pos)
pos.astype(np.float32).tofile('/tmp/ci_pos_in.bin')
vel.astype(np.float32).tofile('/tmp/ci_vel_in.bin')
cmd = [os.environ['SIB_METAL'], 'xpbd_step',
str(info['n_active']), str(info['n_static']),
'3.34', '2e-12', '1000.0', '2e-5', '-9.8', '2.0', '1e-3', '3',
'/tmp/ci_pos_in.bin', '/tmp/ci_vel_in.bin',
info['paths']['pos_static'],
str(info['n_bonds']), info['paths']['bonds'],
'3.3e-9', '50', # alpha_dist, n_steps
'7.4e-6', '5e-5', '5500.0', '0.0'] # sim_scale, visc, K, restitution
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if r.returncode != 0:
print('STDOUT:', r.stdout); print('STDERR:', r.stderr); sys.exit(1)
out = np.fromfile('/tmp/xpbd_pos_out.bin', dtype=np.float32).reshape(-1, 3)
# Sanity: cube should still be in the box (no NaN/Inf, all y in [0, 100])
assert np.all(np.isfinite(out)), 'NaN/Inf in output positions'
assert np.all((out[:, 1] >= -10) & (out[:, 1] <= 100)), 'cube escaped box'
print(f'Native Metal smoke test PASSED ({info["n_active"]} active particles, 50 steps)')
PY
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
/src/main_sim2.py
/src/main_sim.py~
/Release/Sibernetic
/src/metal_diff/sib_metal
__pycache__/
*~
# Compiled Object files
*.slo
Expand Down Expand Up @@ -69,3 +71,6 @@ simulations
.vscode/*
.settings/*
.idea/*

# Project-local Python venv created by setup.sh
/.venv/
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Repository Guide

This repository contains the **Sibernetic** simulator along with Python
bindings and tests. The code base mixes C++ (in `src/` and `inc/`), OpenCL
kernels and a number of helper Python scripts.

## Layout
- `src/` – main C++ sources and OpenCL kernels (`sphFluid.cl`).
- `inc/` – C++ headers with physics constants, solver classes and helpers.
- `configuration/` – example configuration files for the simulator.
- `buffers/` – output data (created at runtime).
- `tests/` – Python tests; `run_all_tests.sh` wraps `sibernetic_c302.py`
for automated runs.
- Python utilities such as `main_sim.py`, `sibernetic_c302.py` and
`plot_positions.py` can drive the simulator or analyse its output.

## Building and Testing
Before building you should install dependencies via `./setup.sh`.
To compile the C++ code use `make`. A convenience script `test.sh`
runs code formatting, static checks via **ruff**, builds the simulator
and executes the test suite:

```bash
./test.sh
```

`test.sh` in turn calls `run_all_tests.sh` which performs multiple
runs of `sibernetic_c302.py` and verifies the produced output files.

For Apple Silicon GPU simulation, build the native Metal substrate
separately via `cd src/metal_diff && ./build.sh`; the resulting
`sib_metal` binary exposes the differentiable XPBD pipeline (see
README.md "Differentiable physics with native Metal").

## Contributing Notes
- Keep C++ headers and sources under `inc/` and `src/` respectively.
- OpenCL kernels reside in `src/*.cl`.
- When adding Python scripts ensure they pass `ruff format` and
`ruff check`.
- Test additions should be placed under `tests/` and runnable via
`test.sh`.

Loading
Loading