Skip to main content

Hardware simulation and development platform

Project description

Hardwave

Hardware simulation and development platform: design circuits and systems in Python before touching any hardware.

Hardwave models hardware as a dataflow graph of components. Each component is a typed black box with input ports, output ports, and a solver that turns inputs into outputs. Connect components into a graph, run the simulation engine, and get results; from a simple voltage divider to a full robot drivetrain.


Contents

  1. What Hardwave Does
  2. Core Concepts
  3. Installation
  4. Quick Start
  5. Module Structure
  6. Working with Types
  7. Defining Components
  8. Building Simulation Graphs
  9. Running Simulations
  10. Composite Components
  11. Solver System
  12. Transient Simulation
  13. Hosted ML Models
  14. Standard Library Reference
  15. Component Health and Faults
  16. Saving Graphs and Cloud Snapshots
  17. Error Handling
  18. Design Principles

What Hardwave Does

Hardware prototyping is expensive and slow. You design something, order parts, assemble it, test it, and discover a motor draws twice the expected current. Then you iterate. This cycle takes days and costs money.

Hardwave lets you close that loop in software:

  • Simulate electrical, mechanical, and thermal behaviour before buying anything
  • Compose components into systems; a resistor, a motor driver, a microcontroller, all with the same interface
  • Calibrate simulation models against real telemetry collected from physical hardware
  • Swap solvers, start with a formula, replace it with an ML model trained on real data, all without changing any wiring

Core Concepts

Concept Description
Type A named, physically-meaningful signal (e.g. DCVoltage, Torque, Temperature)
Port A typed input or output slot on a component; input ports can aggregate multiple connections
Component A node in the simulation graph with ports and a solver
Solver The computation that transforms inputs to outputs (formula, ODE, lookup table, ML model)
Composite A component built from other connected sub-components; externally identical to a primitive
Graph A directed acyclic graph of connected component instances
Engine Runs the graph; propagates values through it and returns a result
Health Runtime component state: OK, WARNING, or FAULT

Everything is a component. A resistor is a component. An Arduino is a component. A robot arm is a component. The abstraction never breaks.


Installation

Requirements: Python 3.11+

pip install hardwave

Or install dependencies directly:

pip install numpy scipy scikit-learn pandas

Then in Python:

import hardwave           # core platform
import hardwave.stdlib    # standard library (types + components)

Importing hardwave.stdlib registers all 30 built-in types and 28 primitive components with the global registries. You do not need to import submodules individually.


Quick Start

Simulate a Voltage Divider

import hardwave
import hardwave.stdlib

from hardwave.simulation import SimulationGraph, SimulationEngine
from hardwave.stdlib.components.passive import VoltageSource, Resistor

# Build the graph
graph = SimulationGraph()
graph.add_component(VoltageSource("vs", param_values={"voltage": 9.0}))
graph.add_component(Resistor("r1", param_values={"resistance": 1_000.0}))
graph.add_component(Resistor("r2", param_values={"resistance": 2_000.0}))

graph.connect("vs", "voltage_out", "r1", "voltage")
graph.connect("vs", "voltage_out", "r2", "voltage")

# Validate, then simulate
graph.validate()
result = SimulationEngine(graph).run(inputs={})

print(result.get_output("r1", "current"))   # → 0.009  A  (9 mA)
print(result.get_output("r2", "current"))   # → 0.0045 A  (4.5 mA)
print(result.get_output("r1", "power"))     # → 0.081  W

The simulation matches hand calculation: I = V / R = 9 / 1000 = 9 mA, exactly.


Module Structure

hardwave/
├── types/           HardwaveType base class + TypeRegistry singleton
├── ports/           Port, PortDirection, PortAggregation, input_port(), aggregating_input_port(), output_port()
├── components/      Component ABC, ComponentMeta, Parameter, ComponentHealth, @component decorator
│   ├── composite.py CompositeComponent, BuildContext, Connection
│   └── registry.py  ComponentRegistry singleton
├── solvers/         Solver ABC + FormulaSolver, ODESolver, LookupTableSolver, MLModelSolver
├── simulation/      SimulationGraph, SimulationEngine, SimulationConfig, SimulationResult
├── premium/         Cloud auth, hosted ML models, remote solvers
├── serialization/   Serializable protocol, schema version constants
├── diagnostics/     Diagnostic, DiagnosticLevel, full exception hierarchy
└── stdlib/
    ├── types/       30 pre-registered hardware signal types
    └── components/  28 pre-built primitive components

Working with Types

Types define the semantic meaning of signals. DCVoltage and Current are both floats at runtime, but Hardwave treats them as incompatible, you cannot accidentally wire a voltage output into a current input.

Built-in Types (selected)

import hardwave.stdlib

from hardwave.types import TypeRegistry
reg = TypeRegistry.instance()

reg.get("DCVoltage")       # V, min=-10000, max=10000
reg.get("Current")         # A
reg.get("Resistance")      # Ω, min=0
reg.get("Power")           # W
reg.get("Torque")          # N·m
reg.get("AngularVelocity") # rad/s
reg.get("Temperature")     # °C, min=-273.15
reg.get("PWMSignal")       # dict {frequency_hz, duty_cycle}
reg.get("ControlSignal")   # dimensionless, 0.0–1.0

# List everything
for t in reg.list_types():
    print(f"{t.name:25s} {t.unit}")

Defining a Custom Type

from hardwave.types import HardwaveType, TypeRegistry

Luminosity = HardwaveType(
    name="Luminosity",
    unit="lux",
    description="Illuminance",
    min_value=0.0,
)
TypeRegistry.instance().register(Luminosity)

Composite / Bus Types

from dataclasses import dataclass
from typing import Any, Type
from hardwave.types import HardwaveType, TypeRegistry

@dataclass
class CANFrame(HardwaveType):
    name: str = "CANFrame"
    unit: str = "-"
    description: str = "CAN bus frame payload"
    python_type: Type = dict

    def validate(self, value: Any) -> bool:
        return (
            isinstance(value, dict)
            and "arbitration_id" in value
            and "data" in value
        )

TypeRegistry.instance().register(CANFrame())

Defining Components

The simplest way to define a component is with the @component decorator:

from hardwave import component, input_port, output_port, Component

@component(
    name="Thermistor",
    display_name="NTC Thermistor",
    version="1.0.0",
    description="NTC thermistor: resistance decreases with temperature",
    docs="Uses the Steinhart-Hart equation approximation.",
    category="Sensors",
    tags=["sensor", "temperature", "thermistor"],
)
class Thermistor(Component):
    ports = [
        input_port("temperature", "Temperature", "Ambient temperature (°C)"),
        output_port("resistance", "Resistance", "Thermistor resistance at given temperature"),
    ]

    parameters = [
        Parameter("r_nominal",  "Nominal Resistance", "Ω",   float, 10_000.0),
        Parameter("t_nominal",  "Nominal Temperature", "°C", float, 25.0),
        Parameter("beta",       "Beta Coefficient",    "K",  float, 3950.0),
    ]

    def solve(self, inputs: dict) -> dict:
        import math
        T_c = inputs["temperature"]
        T_k = T_c + 273.15
        T0 = self.get_param("t_nominal") + 273.15
        R0 = self.get_param("r_nominal")
        B  = self.get_param("beta")
        R  = R0 * math.exp(B * (1 / T_k - 1 / T0))
        return {"resistance": R}

The decorator automatically:

  • Attaches ComponentMeta to the class
  • Registers the class with ComponentRegistry

Manual definition (without decorator)

from hardwave.components import Component, ComponentMeta, ComponentRegistry, Parameter
from hardwave.ports import input_port, output_port

class MyGasSensor(Component):
    meta = ComponentMeta(
        name="MyGasSensor",
        display_name="Custom Gas Sensor",
        version="1.0.0",
        description="Reads CO2 concentration",
        docs="Returns PPM concentration from a voltage signal.",
        category="Sensors/Gas",
    )
    ports = [
        input_port("analog_in", "AnalogSignal", "Sensor output voltage (0–5V)"),
        output_port("co2_ppm", "AnalogSignal",  "CO2 concentration in PPM"),
    ]
    parameters = [
        Parameter("sensitivity", "Sensitivity", "V/PPM", float, 0.002),
    ]

    def solve(self, inputs: dict) -> dict:
        V = inputs["analog_in"]
        ppm = V / self.get_param("sensitivity")
        return {"co2_ppm": ppm}

ComponentRegistry.instance().register(MyGasSensor)

Aggregating input ports

Use aggregating_input_port() when multiple upstream outputs should feed the same input. The engine collects all connected values and combines them into a single value before calling solve().

from hardwave.ports import aggregating_input_port, output_port, PortAggregation

def net_current(values, component, port):
    return sum(values)

class CurrentJunction(Component):
    meta = ComponentMeta(
        name="CurrentJunction",
        display_name="Current Junction",
        version="1.0.0",
        description="Sums branch currents at a node",
        docs="",
        category="Electrical",
    )
    ports = [
        # Built-in sum
        aggregating_input_port(
            "current_in", "Current",
            aggregation=PortAggregation.SUM,
            description="Total current from all branches",
        ),
        output_port("total_current", "Current", "Net current"),
    ]
    parameters = []

    def solve(self, inputs: dict) -> dict:
        return {"total_current": inputs["current_in"]}

Custom aggregation with aggregate=my_fn fully replaces built-in modes. The callable receives (values, component, port) and must return a single value.

aggregating_input_port("current_in", "Current", aggregate=net_current)

Regular input_port() ports still accept only one connection. See Aggregating Input Ports for built-in modes, min_connections / max_connections, and disconnect APIs.


Building Simulation Graphs

from hardwave.simulation import SimulationGraph

graph = SimulationGraph()

# Add component instances
graph.add_component(VoltageSource("psu",  param_values={"voltage": 12.0}))
graph.add_component(VoltageRegulator("ldo", param_values={"output_voltage": 5.0}))
graph.add_component(Resistor("load", param_values={"resistance": 100.0}))

# Wire them together
graph.connect("psu",  "voltage_out",  "ldo",  "v_in")
graph.connect("ldo",  "v_out",        "load", "voltage")
# load_current must be provided externally or have a default

# Fan-out: one output → many inputs (e.g. same supply to two resistors)
# graph.connect("vs", "voltage_out", "r1", "voltage")
# graph.connect("vs", "voltage_out", "r2", "voltage")

# Fan-in: many outputs → one input requires aggregating_input_port() on the target
# graph.connect("src_a", "current_out", "junction", "current_in")
# graph.connect("src_b", "current_out", "junction", "current_in")

# Validate before running
diagnostics = graph.validate()
for d in diagnostics:
    print(d)   # warnings only; errors raise GraphValidationError

Connection rules enforced at wiring time

Error Cause
PortNotFoundError Port name does not exist on that component
PortDirectionError Connecting output→output or input→input
TypeMismatchError Source and target types are incompatible
DuplicateConnectionError Second wire to a single-connection input port
MaxConnectionsExceededError More connections than max_connections allows

Disconnect and inspect

graph.disconnect("load", "voltage")          # remove all connections feeding load.voltage
graph.disconnect("node", "current_in", "src_a", "current_out")  # remove one source
graph.connections_to("node", "current_in")   # list Connection objects feeding a port
graph.remove_component("load")               # remove instance and all its connections

Running Simulations

Steady-state

from hardwave.simulation import SimulationEngine, SimulationConfig
from hardwave.solvers import SimulationDomain

engine = SimulationEngine(graph)

result = engine.run(
    inputs={
        "ldo": {"load_current": 0.05},   # 50 mA load
    },
    config=SimulationConfig(domain=SimulationDomain.STEADY_STATE),
)

print(result.get_output("ldo",  "v_out"))        # 5.0
print(result.get_output("ldo",  "power_loss"))   # (12-5)*0.05 = 0.35 W
print(result.get_output("load", "current"))      # 0.05

Accessing results

# Scalar output
value = result.get_output("instance_id", "port_name")

# Dict (JSON-serializable)
d = result.to_dict()

# Simulation warnings, faults, and diagnostics
for diag in result.diagnostics():
    print(diag)

warnings = result.get_warnings()
faults = result.get_faults()

See Component Health and Faults for runtime fault detection on motors and batteries.


Composite Components

A composite component wraps an internal sub-graph. From the outside it looks identical to a primitive.

from hardwave.components import (
    CompositeComponent, BuildContext, Connection,
    ComponentMeta, ComponentRegistry, Parameter,
)
from hardwave.ports import input_port, output_port
from hardwave.stdlib.components.passive import Resistor

class VoltageDivider(CompositeComponent):
    meta = ComponentMeta(
        name="VoltageDivider",
        display_name="Voltage Divider",
        version="1.0.0",
        description="Two-resistor voltage divider",
        docs="Splits an input voltage across two resistors.",
        category="Electrical/Passive",
    )
    ports = [
        input_port("v_in",  "DCVoltage", "Input voltage"),
        output_port("i_r1", "Current",   "Current through R1"),
        output_port("i_r2", "Current",   "Current through R2"),
    ]
    parameters = [
        Parameter("r1", "R1", "Ω", float, 1_000.0),
        Parameter("r2", "R2", "Ω", float, 2_000.0),
    ]

    def build(self, ctx: BuildContext) -> None:
        ctx.add(Resistor("r1", param_values={"resistance": self.get_param("r1")}))
        ctx.add(Resistor("r2", param_values={"resistance": self.get_param("r2")}))

        # Fan the external input to both internal ports
        ctx.map_input("v_in", "r1", "voltage")
        ctx.map_input("v_in", "r2", "voltage")

        # Expose internal outputs externally
        ctx.map_output("i_r1", "r1", "current")
        ctx.map_output("i_r2", "r2", "current")

        # Internal connections (none needed here, both take the same input)

ComponentRegistry.instance().register(VoltageDivider)

# Use it like any other component
graph = SimulationGraph()
graph.add_component(VoltageSource("vs", param_values={"voltage": 9.0}))
graph.add_component(VoltageDivider("div"))
graph.connect("vs", "voltage_out", "div", "v_in")
result = SimulationEngine(graph).run(inputs={})
print(result.get_output("div", "i_r1"))  # 0.009 A
print(result.get_output("div", "i_r2"))  # 0.0045 A

Internal sub-components can also be connected to each other using ctx.connect(Connection(...)) for more complex topologies. When multiple internal connections target the same aggregating input port, the engine combines values before calling the sub-component's solver.


Solver System

Every component has a solver that computes outputs from inputs. The default solver is the component's own solve() method, but you can inject a more sophisticated one at any time without changing the component's interface.

FormulaSolver - wrap any callable

from hardwave.solvers import FormulaSolver

def my_formula(inputs, component):
    V = inputs["voltage"]
    R = component.get_param("resistance")
    return {"current": V / R, "power": V**2 / R}

r = Resistor("r1", param_values={"resistance": 470.0})
r.set_solver(FormulaSolver(my_formula))

LookupTableSolver - interpolate measured data

from hardwave.solvers import LookupTableSolver

# Motor torque-speed curve from datasheet
table = {
    "speed_rpm": [0,   500,  1000, 1500, 2000, 2500, 3000],
    "torque_nm": [1.2, 1.15, 1.05, 0.90, 0.70, 0.42, 0.0],
    "efficiency": [0.0, 0.55, 0.72, 0.80, 0.81, 0.75, 0.0],
}

motor.set_solver(LookupTableSolver(
    table,
    input_port="speed_rpm",
    output_ports=["torque_nm", "efficiency"],
    method="linear",   # or "cubic", "nearest"
))

ODESolver - physics-based transient models

from hardwave.solvers import ODESolver

# RC circuit: dV/dt = (V_in - V_c) / (R * C)
def state_fn(t, state, inputs, comp):
    R, C = comp.get_param("resistance"), comp.get_param("capacitance")
    return {"V_c": (inputs["v_in"] - state["V_c"]) / (R * C)}

def output_fn(t, state, inputs, comp):
    return {"v_out": state["V_c"]}

rc_solver = ODESolver(
    state_fn=state_fn,
    output_fn=output_fn,
    initial_state={"V_c": 0.0},
    integrator="rk4",          # "euler" | "rk4" | "scipy_ivp"
)
my_cap.set_solver(rc_solver)

MLModelSolver - learned from data

See Telemetry and ML Solvers below.


Transient Simulation

Transient simulation steps through time and returns a time series for every output port. Built-in components like DCMotor, Capacitor, Inductor, and LiPoCell use ODESolver internally and automatically participate in transient runs.

from hardwave.simulation import SimulationGraph, SimulationEngine, SimulationConfig
from hardwave.solvers import SimulationDomain
from hardwave.stdlib.components.motors import DCMotor

graph = SimulationGraph()
graph.add_component(DCMotor("motor", param_values={
    "winding_resistance": 2.0,
    "winding_inductance": 5e-4,
    "back_emf_constant":  0.05,
    "rotor_inertia":      1e-4,
}))

engine = SimulationEngine(graph)
config = SimulationConfig(
    domain=SimulationDomain.TRANSIENT,
    t_start=0.0,
    t_end=1.0,
    dt=0.0001,        # 0.1 ms - use small dt for stiff electrical dynamics
)
engine.configure(config)

result = engine.run(
    inputs={"motor": {"voltage": 12.0, "load_torque": 0.2}},
    config=config,
)

# Time series as numpy arrays
t, omega = result.get_time_series("motor", "angular_velocity")
t, current = result.get_time_series("motor", "current")

print(f"Final speed:   {omega[-1]:.1f} rad/s")
print(f"Final current: {current[-1]*1000:.1f} mA")

# Export to pandas for plotting
df = result.to_dataframe()
print(df.head())
# Columns: time, motor.angular_velocity, motor.current, motor.winding_temp

# Serialize the result
import json
print(json.dumps(result.to_dict(), indent=2)[:300])

Interactive / real-time step mode

state = {}
for step in range(1000):
    t = step * 0.001
    outputs, state, diagnostics = engine.step(
        inputs={"motor": {"voltage": 12.0, "load_torque": 0.2}},
        t=t,
        state=state,
        step_index=step,
    )
    omega = outputs["motor"]["angular_velocity"]
    for d in diagnostics:
        if d.level.value == "fault":
            print(f"Fault at t={t}: {d.message}")

Hosted ML Models

Each hosted model owns its training records and keeps trained versions. Append measurements, train (or retrain) in the cloud, then attach the model to a component. Requires a Builder/Crew plan.

import time
import hardwave.premium

hardwave.premium.configure(
    organization_id="<your-org-uuid>",
    secret_key=open("hardwave_private.pem").read(),
)

model = hardwave.premium.create_model(
    "dc-motor-from-bench",
    component_class="DCMotor",
    input_ports=["voltage", "load_torque"],
    output_ports=["angular_velocity", "winding_temp"],
)
model_id = model["id"]

records = []
for i in range(80):
    v = 6.0 + (i % 10) * 0.6
    load = 0.1 + (i % 5) * 0.05
    records.append({
        "timestamp": time.time() + i,
        "inputs": {"voltage": v, "load_torque": load},
        "outputs": {
            "angular_velocity": v * 15.0 - load * 40.0,
            "winding_temp": 25.0 + v * 1.2 + load * 8.0,
        },
        "source": "user",
    })

hardwave.premium.append_records(model_id, records)
hardwave.premium.train_model(model_id, min_samples=50)

# After status is ready:
hardwave.premium.attach_model(motor, model_id)
# or hardwave.premium.sync() to register ML_* components

Components still log per-run I/O during simulation (export_telemetry()), which you can map into append_records payloads. See the docs page Hosted ML models for versions and staleness.


Standard Library Reference

Types (30 registered)

Electrical

Name Unit Description
DCVoltage V DC electrical potential
ACVoltage V_rms AC RMS voltage
Current A Electrical current
Resistance Ω Electrical resistance
Capacitance F Capacitance
Inductance H Inductance
Power W Electrical power
Frequency Hz Signal frequency
AnalogSignal V Generic analog 0–5V
PWMSignal dict {frequency_hz, duty_cycle}
DigitalSignal 0 or 1
I2CBus dict {sda, scl, address, data}
SPIBus dict {mosi, miso, sclk, cs}
UARTStream dict {baud_rate, data}

Mechanical

Name Unit
Torque N·m
AngularVelocity rad/s
AngularPosition rad
LinearForce N
LinearVelocity m/s
LinearPosition m
Pressure Pa
MechanicalPower W

Thermal

Name Unit
Temperature °C
HeatFlux W/m²
ThermalResistance °C/W

Control & Data

Name Unit
ControlSignal — (0.0–1.0)
BooleanSignal
Timestamp s
ByteArray
JSONPayload

Components (28 registered)

Electrical / Passive

Component Key Ports Solver
Resistor voltage → current, power Formula (Ohm's Law)
Capacitor current → voltage, stored_energy ODE (dV/dt = I/C)
Inductor voltage → current, stored_energy ODE (dI/dt = V/L)
Diode anode_voltage, cathode_voltage → current, power_loss Formula (Shockley)
VoltageSource set_voltage → voltage_out Formula
CurrentSource set_current → current_out Formula
Ground — → voltage (0V) Formula

Electrical / Active

Component Key Ports Solver
NPNTransistor base_current, vce → collector_current, power Formula (piecewise)
NMOSFET gate_voltage, vds → drain_current, power Formula (square-law)
OpAmp v_plus, v_minus, supply → v_out Formula (ideal, rail-clamped)
VoltageRegulator v_in, load_current → v_out, power_loss Formula

Electromechanical

Component Key Ports Solver
DCMotor voltage, load_torque → angular_velocity, current, winding_temp, health, fault_code ODE (coupled E-M-T; thermal fault rules)
StepperMotor step_pulses, load_torque → angular_position, current Discrete
ServoMotor pwm_signal, load_torque → angular_position, current Discrete
BrushlessMotor phase_voltages, load_torque → angular_velocity, phase_currents ODE
LinearActuator pwm_signal, load_force → linear_position, current ODE

Sensors

Component Key Ports Solver
TemperatureSensor ambient_temperature → analog_out Formula + Gaussian noise
IMU linear_acceleration, angular_velocity → i2c_out Formula
Encoder angular_position → pulse_count, angular_velocity Formula
UltrasonicSensor target_distance → pulse_out Formula + noise + latency
Camera scene_description → image_frame Placeholder (V2)

Microcontrollers

Component Key Ports Solver
Arduino gpio_inputs → gpio_outputs, uart Firmware callable (signal-level)
ESP32 gpio_inputs, wifi_rx → gpio_outputs, wifi_tx, uart Firmware callable
RaspberryPi gpio_inputs → gpio_outputs, uart, i2c, spi Firmware callable

MCU firmware is modelled at the signal level; what values appear on pins at each time step. Actual code execution is a V2 feature. Pass a Python callable as the firmware parameter to drive GPIO outputs.

Power

Component Key Ports Solver
LiPoCell charge_current, discharge_current → terminal_voltage, soc, temperature, health, fault_code ODE (SOC/thermal; fault rules)
BatteryPack charge_current, discharge_current → terminal_voltage, capacity_ah Composite
BuckConverter v_in, duty_cycle, load_current → v_out, efficiency Formula
BoostConverter v_in, duty_cycle, load_current → v_out, efficiency Formula

Component Health and Faults

Components can emit runtime diagnostics when operating limits are exceeded during simulation. This makes it possible to stress-test designs, validate shutdown firmware, and explore failure modes before hardware breaks.

Diagnostic levels

Level When Behaviour
INFO Informational Continue
WARNING Out of spec (e.g. low SOC, high temp) Continue; surfaced in results
ERROR Invalid wiring / missing inputs Block at validate()
FAULT Catastrophic failure (e.g. motor burnout) Degrade outputs; optionally stop sim

Built-in fault-aware components

DCMotor monitors winding temperature and current:

  • MOTOR_THERMAL_HIGH (WARNING) — approaching max_winding_temp
  • MOTOR_OVER_CURRENT (WARNING) — above max_continuous_current
  • MOTOR_THERMAL_RUNAWAY (FAULT) — windings burned out; motor stops

LiPoCell monitors SOC, voltage, temperature, and discharge current:

  • CELL_LOW_SOC (WARNING) — SOC below min_soc_warning
  • CELL_HIGH_TEMP (WARNING) — approaching max_temperature
  • CELL_OVERDISCHARGE (FAULT) — discharge cut off
  • CELL_THERMAL_DAMAGE (FAULT) — permanent internal resistance increase

Both expose health (0=OK, 1=warning, 2=fault) and fault_code output ports for wiring into control logic.

Reading diagnostics

from hardwave.simulation import SimulationEngine, SimulationConfig, FaultMode

result = SimulationEngine(graph).run(inputs={...})

for d in result.get_warnings():
    print(d)

for d in result.get_faults():
    print(f"{d.code}: {d.message} (value={d.value}, limit={d.threshold})")

health = result.get_output("motor", "health")

Fault modes

# Default: continue simulating with degraded faulted outputs
SimulationConfig(fault_mode=FaultMode.CONTINUE)

# Abort on first FAULT (raises SimulationError)
SimulationConfig(fault_mode=FaultMode.STOP)

Adding faults to custom components

Override post_solve_diagnostics() to emit warnings/faults and apply_fault_outputs() to model failed behaviour:

from hardwave.components.health import make_diagnostic
from hardwave.diagnostics import DiagnosticLevel

def post_solve_diagnostics(self, inputs, outputs, *, t=None, step_index=None):
    diagnostics = []
    if outputs["winding_temp"] > self.get_param("max_temp"):
        self.set_fault_code(100)
        diagnostics.append(make_diagnostic(
            DiagnosticLevel.FAULT, "MOTOR_THERMAL_RUNAWAY",
            "Motor burned out", self.instance_id,
            value=outputs["winding_temp"], threshold=self.get_param("max_temp"),
        ))
    return diagnostics

def apply_fault_outputs(self, outputs):
    return {**outputs, "angular_velocity": 0.0, "current": 0.0}

Full documentation: docs/guides/component-faults.mdx


Saving Graphs and Cloud Snapshots

Local JSON

import json

graph_dict = graph.to_dict()
with open("my_graph.json", "w") as f:
    json.dump(graph_dict, f, indent=2)

# Reload later (all component classes must be registered first)
reloaded = SimulationGraph.from_dict(graph_dict)

Simulation result snapshots

Attach a run outcome when serializing so teammates or tooling can inspect outputs without re-executing:

from hardwave.simulation import build_simulation_snapshot

snapshot = build_simulation_snapshot(
    result,
    inputs={"ldo": {"load_current": 0.05}},
    config=config,
)
graph_dict = graph.to_dict()
graph_dict["simulation_snapshot"] = snapshot

result.to_dict() alone captures domain, outputs, diagnostics, and time_array (for transient runs).

Cloud dashboard (Builder / Crew)

Configure premium credentials once, then save from Python. Graphs appear under Graphs in the Hardwave dashboard.

import hardwave.premium

hardwave.premium.configure(
    organization_id="YOUR_ORG_ID",
    secret_key=open("hardwave_private.pem").read(),
)

# Structure only
graph_id = graph.save("psu-regulator", description="5 V rail model")

# With dashboard RESULTS tab (recommended)
graph_id = graph.save(
    "psu-regulator",
    description="5 V rail model",
    result=result,
    inputs={"ldo": {"load_current": 0.05}},
)

# Or run + snapshot in one call
graph_id = graph.save("psu-regulator", inputs={"ldo": {"load_current": 0.05}})

The snapshot is stored inside graph_data.simulation_snapshot in the cloud — no extra database columns. SimulationGraph.from_dict() ignores it when reloading. Re-saving without result= replaces the cloud graph and clears any previous snapshot.

Full details: docs/graphs/saving.mdx


Error Handling

Hardwave uses a typed exception hierarchy:

HardwaveError
├── TypeRegistrationError      # Registering a type name that already exists
├── TypeNotFoundError          # Looking up a type not in the registry
├── ComponentRegistrationError # Duplicate component name or invalid class
├── ComponentNotFoundError     # Looking up a component not in the registry
├── PortNotFoundError          # Port name not found on component
├── TypeMismatchError          # Incompatible types on a connection
├── PortDirectionError         # Connecting input→input or output→output
├── DuplicateConnectionError   # Second wire to a single-connection input port
├── MaxConnectionsExceededError # Port max_connections limit reached
├── PortConfigurationError     # Invalid port aggregation declaration at registration
├── GraphValidationError       # Graph fails validation (carries list of Diagnostics)
├── SolverError                # Solver computation failure
├── SimulationError            # Engine-level failure
├── SimulationResultError      # Accessing time-series on a steady-state result
└── DeserializationError       # Corrupt or incompatible serialized data

GraphValidationError carries a full list of Diagnostic objects. Runtime simulation also collects WARNING and FAULT diagnostics in SimulationResult (see Component Health and Faults).

from hardwave.diagnostics import GraphValidationError, DiagnosticLevel

try:
    graph.validate()
except GraphValidationError as e:
    for d in e.diagnostics:
        print(f"[{d.level.value}] {d.code}: {d.message}")
        if d.suggestion:
            print(f"  → {d.suggestion}")

Design Principles

  1. Everything is a component. A resistor, an Arduino, a robot arm; the abstraction never breaks.
  2. Components are defined in code. Python classes are the source of truth; a UI layer wraps them.
  3. Solvers are pluggable. Swap a formula for a lookup table or an ML model without touching any wiring.
  4. Strict typing at the boundary. Type errors are caught at graph construction time, not at simulation time.
  5. Composability is first-class. A composite component is indistinguishable from a primitive at its interface.
  6. Simulation semantics are explicit. Time domain is declared per solver, not inferred.
  7. Data drives fidelity. Components start as formulas and grow into empirically-calibrated ML models.

Development

# Install in editable mode (includes all dependencies)
pip install -e .

# Run a quick sanity check
python -c "import hardwave; import hardwave.stdlib; print('OK')"

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

hardwave-0.3.1.tar.gz (90.4 kB view details)

Uploaded Source

Built Distribution

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

hardwave-0.3.1-py3-none-any.whl (84.3 kB view details)

Uploaded Python 3

File details

Details for the file hardwave-0.3.1.tar.gz.

File metadata

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

File hashes

Hashes for hardwave-0.3.1.tar.gz
Algorithm Hash digest
SHA256 573d76a917194480abb2a45a2f8a88131ff9424c5e18fdde75bdf2b3d5e040ce
MD5 ca9aab6ae954f769aa34e52b86d5ff0d
BLAKE2b-256 cbb1f7471d9fb5d6bda1ff59083fe16487821413fa6b421ce3e683088e2796c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for hardwave-0.3.1.tar.gz:

Publisher: release.yaml on HardwaveDev/hardwave

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

File details

Details for the file hardwave-0.3.1-py3-none-any.whl.

File metadata

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

File hashes

Hashes for hardwave-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7585bfb8b0c00e80db77c3eedcc15d67ef2742e84cbed96f4d438761bf157875
MD5 62e8b5e07c982cc4ba918826e04eb802
BLAKE2b-256 814f639e343f50590174310a0d124b6bb81bcfb7707b1277bbfad85954df797d

See more details on using hashes here.

Provenance

The following attestation bundles were made for hardwave-0.3.1-py3-none-any.whl:

Publisher: release.yaml on HardwaveDev/hardwave

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