Skip to main content

A modular, GPU-accelerated framework for Physical Reservoir Computing simulation and evaluation

Project description

OpenPRC: Physical Reservoir Computing Framework

Python 3.10+ License CUDA arXiv

OpenPRC is a modular, GPU-accelerated Python framework for simulating and evaluating physical reservoir computers — mechanical systems that process information through their intrinsic dynamics.

If you use OpenPRC in your research, please cite:

@article{phalak2026openprc,
  title={OpenPRC: A Unified Open-Source Framework for Physics-to-Task Evaluation in Physical Reservoir Computing},
  author={Phalak, Yogesh and Lor, Wen Sin and Khairnar, Apoorva and Jantzen, Benjamin and Naughton, Noel and Li, Suyi},
  journal={arXiv preprint arXiv:2604.07423},
  year={2026}
}

Simulation Capabilities

OpenPRC supports diverse mechanical substrates ranging from compliant mass-spring networks to rigid-foldable origami. Below are examples of validated simulation outputs:

DJI Matrice 600
DJI Matrice 600
Aerial platform dynamics
Soft Reservoir Network
Soft Reservoir Network
Mass-spring lattice under dynamic actuation
Miura-Ori Tessellation
Miura-Ori Tessellation
Rigid-foldable origami pattern
Kirigami Structure
Kirigami Structure
Compliant network with geometric cuts
K-Cone Origami
K-Cone Origami
Non-periodic origami configuration
Bistable Slab
Bistable Slab
Multistable mechanical metamaterial
Tapered Spring
Tapered Spring
Nonlinear elastic element dynamics

Installation

pip install openprc

# With GPU support
pip install openprc[cuda]

# With all optional dependencies
pip install openprc[full]

Dependencies

Package Purpose Extra
numpy, h5py, scipy Core numerics and I/O (always)
numba JIT-compiled CPU physics (always)
scikit-learn Ridge readout (always)
piviz-3d, imgui 3-D animator (Quick Start viewer) (always)
pycuda CUDA backend [cuda]
jax / jaxlib Differentiable JAX backend [jax]
opencv-python Vision utilities [vision]
trimesh, rosbags, yourdfpy Robot bundle tooling [automod]

Pipeline

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   demlat    │────▶│   reservoir │────▶│   analysis  │────▶│  optimize   │
│  (Physics)  │     │  (Readout)  │     │(Diagnostics)│     │(Calibration)│
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘

Quick Start

import numpy as np
from openprc.demlat import SimulationSetup, Simulation, Engine, ShowSimulation

EXP = "experiments/my_prc"

# ── 1. Build geometry ──────────────────────────────────────────────────────
setup = SimulationSetup(EXP, overwrite=True)
setup.set_simulation_params(duration=10.0, dt=0.001, save_interval=0.01)
setup.set_physics(gravity=-9.81, damping=0.1)

anchor = setup.add_node([0.0, 0.0, 0.0], fixed=True)
mass   = setup.add_node([1.0, 0.0, 0.0], mass=1.0)
setup.add_bar(anchor, mass, stiffness=1e4, rest_length=1.0, damping=5.0)

# ── 2. Drive with a force signal ──────────────────────────────────────────
t   = np.arange(0, 10.0, 0.001)
sig = np.stack([0.5 * np.sin(2 * np.pi * t),
                np.zeros_like(t),
                np.zeros_like(t)], axis=1).astype("float32")
setup.add_signal("drive", sig, dt=0.001)
setup.add_actuator(anchor, "drive", type="force")
setup.save()

# ── 3. Run (CUDA, or "cpu" / "jax") ──────────────────────────────────────
eng = Engine(backend="cuda")          # BarHingeModel is the default
eng.run(Simulation(EXP))

# ── 4. Animate ────────────────────────────────────────────────────────────
ShowSimulation(EXP)

API Reference

demlat — Physics Simulation

from openprc.demlat import SimulationSetup, Simulation, Engine, ShowSimulation

# ── Geometry ──────────────────────────────────────────────────────────────
setup = SimulationSetup("./experiments/my_exp", overwrite=True)
setup.set_simulation_params(duration=5.0, dt=0.001, save_interval=0.01)
setup.set_physics(
    gravity=-9.81, damping=0.1,
    enable_collision=True,          # node-level sphere collision
    collision_radius=0.02,
    collision_restitution=0.6,
    collision_iterations=3,
)

n0 = setup.add_node([0.0, 0.0, 0.0], fixed=True,  collidable=True)
n1 = setup.add_node([0.5, 0.0, 0.0], mass=1.0,    collidable=True)
n2 = setup.add_node([0.5, 0.5, 0.0], mass=1.0)

setup.add_bar(n0, n1, stiffness=1e4, rest_length=0.5, damping=5.0)
setup.add_hinge([n0, n1, n2, n2], stiffness=50.0, rest_angle=np.pi / 2)

# ── Signals & Actuators ───────────────────────────────────────────────────
t   = np.arange(0, 5.0, 0.001)
pos = np.stack([0.1 * np.sin(2 * np.pi * t),
                np.zeros_like(t),
                np.zeros_like(t)], axis=1).astype("float32")
setup.add_signal("wave", pos, dt=0.001)

setup.add_actuator(n0, "wave", type="position")              # full 3-DOF
setup.add_actuator(n1, "wave", type="force", dof=[0, 0, 1]) # z-axis only
setup.save()

# ── Run ───────────────────────────────────────────────────────────────────
eng = Engine(backend="cuda")   # or "cpu" / "jax"; BarHingeModel is default
eng.run(Simulation("./experiments/my_exp"))

ShowSimulation("./experiments/my_exp")

reservoir — Readout Training

from openprc.reservoir import StateLoader, Ridge, Trainer, features

loader = StateLoader("./experiments/my_exp/output/simulation.h5")

# Node position features for selected nodes
feat = features.NodePositions(node_ids=[0, 1, 2], dims="all")
X    = feat.transform(loader)          # (T, n_features)

# Or use bar strains as the observable (stored as ε = ΔL/L₀)
feat = features.BarStrains()

# Train ridge readout
readout = Ridge(regularization=1e-4)
trainer = Trainer(
    features=feat,
    readout=readout,
    experiment_dir="./experiments/my_exp",
    loader=loader,
    washout=2.0,          # seconds to discard at start
    train_duration=6.0,
    test_duration=2.0,
)

y_target = ...            # (T,) or (T, n_outputs) numpy array
result = trainer.train(y_target, task_name="NARMA10")
result.save()

analysis — Correlation Diagnostics, Multistability & Reach Spectroscopy

from openprc.analysis import correlation as corr
from openprc.analysis import EquilibriumFinder, reach_spectroscopy

# ── Correlation diagnostics (x: features, y: targets) ────────────────────
lin = corr.Linear(x, y, lag_sweep=True)
print(lin.pearson)         # zero-lag Pearson r per channel
lin.ccf.plot()             # cross-correlation lag profiles

nr = corr.Nonparametric(x, y)
print(nr.dcor)             # distance correlation (detects nonlinear deps)

# ── Find all mechanical equilibria ───────────────────────────────────────
finder  = EquilibriumFinder.from_experiment("./experiments/my_exp")
results = finder.find_all(num_random=50)
results.summary()
finder.save_results(results, "./experiments/my_exp/equilibria.h5")

# ── Reach spectroscopy: identify the "machine" behind a driven reservoir ──
# Works on *parsed timeseries* — decoupled from any experiment directory.
from openprc.reservoir import StateLoader, features as feat

loader = StateLoader("./experiments/my_exp/output/simulation.h5")
states = feat.NodeDisplacements(reference_node=0).transform(loader)  # (T, N) readout
drive  = loader.get_actuation_signal(actuator_idx=0, dof=1)          # (T,) actuation

M = reach_spectroscopy(states, t=loader.time, drive=drive)
print(M.D)                 # reach per tolerance, e.g. {0.01: 6, 0.001: 4}
print(M.phi_n)             # harmonic phases {φ_n} of the abstraction
M.summary()                # per-harmonic capacity / quadrature table
M.plot()                   # six-panel scientific figure
M.save("./experiments/my_exp/output")   # → machine_abstraction.json + .npz

# Mask any measurement for a faster / leaner run:
M = reach_spectroscopy(states, t=loader.time, drive=drive,
                       noise_floor=False, rank=False, input_spectrum=False)

optimize — JAX-Based Parameter Calibration

from openprc.optimize import Calibration

from openprc.demlat import BarHingeModel
cal = Calibration(BarHingeModel, backend="jax")

cal.load_geometry("./experiments/my_exp")
cal.load_reference("./experiments/my_exp/output/simulation.h5")

cal.optimize_params(bar_stiffness=True, hinge_stiffness=True)
cal.set_bounds(bar_stiffness=(10.0, 1e5))

result = cal.run(max_iterations=500, lr=0.01, cost="mse")
cal.save("./experiments/my_exp/optimized_geometry.h5")

automod — Robot PRC Pipeline

import openprc.automod as automod

# Stage 1: convert URDF links to spring-mass reservoirs
automod.batch_preprocess(
    bundle_dir="./robot_bundle",
    robot_name="go1",
    params=automod.PRESETS["small"],   # "small", "medium", "large"
)

# Stage 2: run DEMLAT simulations for all trajectories
automod.batch_simulate(
    bundle_dir="./robot_bundle",
    robot_name="go1",
    splits=("train", "test"),
    gravity=-9.81, damping_scale=2.0,
)

# Stage 3: train ridge readout with k-fold CV
run_dir = automod.run_training(
    bundle_dir="./robot_bundle",
    robot_name="go1",
    features="node_vel",              # see automod.FEATURE_LEVELS
    targets=["body_vel", "qvel"],
    n_folds=5,
)

# Stage 4: generate plots
automod.plot_run(run_dir)

License

Apache 2.0 — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openprc-0.1.1.tar.gz (341.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

openprc-0.1.1-py3-none-any.whl (399.2 kB view details)

Uploaded Python 3

File details

Details for the file openprc-0.1.1.tar.gz.

File metadata

  • Download URL: openprc-0.1.1.tar.gz
  • Upload date:
  • Size: 341.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for openprc-0.1.1.tar.gz
Algorithm Hash digest
SHA256 f5e77e2eefff86654dd0a9965e99023a2d5e7741d8c8080c4ba12b5a49ec3f34
MD5 34b88ad3715a0a471b1e153d10d17c71
BLAKE2b-256 7675b6ae90153c18cdf31e70ba594bf8fe22d86f98c6d060d31f2fb91548146d

See more details on using hashes here.

Provenance

The following attestation bundles were made for openprc-0.1.1.tar.gz:

Publisher: publish.yml on DARE-Lab-VT/OpenPRC-dev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file openprc-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: openprc-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 399.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for openprc-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f9378d900e9f4bb628c1528469a55048807983ef105725000dd684cb6f6a29e5
MD5 dd35f4927fed39788e44fab5bab37d7a
BLAKE2b-256 42dad36d02c620e7b42d6f7bd5efbd2f721c8395bc8b1f4dea37bdd75d0e7079

See more details on using hashes here.

Provenance

The following attestation bundles were made for openprc-0.1.1-py3-none-any.whl:

Publisher: publish.yml on DARE-Lab-VT/OpenPRC-dev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page