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}
}OpenPRC supports diverse mechanical substrates ranging from compliant mass-spring networks to rigid-foldable origami. Below are examples of validated simulation outputs:
pip install openprc
# With GPU support
pip install openprc[cuda]
# With all optional dependencies
pip install openprc[full]| Package | Purpose | Extra |
|---|---|---|
numpy, h5py, scipy |
Core numerics and I/O | (always) |
numba |
JIT-compiled CPU physics | (always) |
scikit-learn |
Ridge readout | (always) |
pycuda |
CUDA backend | [cuda] |
jax / jaxlib |
Differentiable JAX backend | [jax] |
piviz-3d, imgui |
3-D animator | [viz] |
opencv-python |
Vision utilities | [vision] |
trimesh, rosbags, yourdfpy |
Robot bundle tooling | [automod] |
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β demlat ββββββΆβ reservoir ββββββΆβ analysis ββββββΆβ optimize β
β (Physics) β β (Readout) β β(Diagnostics)β β(Calibration)β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
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)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")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()from openprc.analysis import correlation as corr
from openprc.analysis import EquilibriumFinder
# ββ 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")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")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)Apache 2.0 β see LICENSE.






