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. Telemetry and ML Solvers
  14. Standard Library Reference
  15. Error Handling
  16. 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
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

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, input_port(), output_port()
├── components/      Component ABC, ComponentMeta, Parameter, @component decorator
│   ├── composite.py CompositeComponent, BuildContext, Connection
│   └── registry.py  ComponentRegistry singleton
├── solvers/         Solver ABC + FormulaSolver, ODESolver, LookupTableSolver, MLModelSolver
├── simulation/      SimulationGraph, SimulationEngine, SimulationConfig, SimulationResult
├── visualization/   VisualizationSpec, VisualizationKind
├── telemetry/       TelemetryRecord, TelemetryStore, ModelTrainer
├── 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)

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

# 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 Input port already has a connected source

Disconnect and remove

graph.disconnect("load", "voltage")          # remove connection feeding load.voltage
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
for diag in result.diagnostics():
    print(diag)

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.


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 = engine.step(
        inputs={"motor": {"voltage": 12.0, "load_torque": 0.2}},
        t=t,
        state=state,
    )
    omega = outputs["motor"]["angular_velocity"]
    # Send omega to a display, controller, etc.

Telemetry and ML Solvers

Recording telemetry

from hardwave.telemetry import TelemetryRecord, TelemetryStore

store = TelemetryStore()

# Log a measurement from a physical motor
store.append(TelemetryRecord(
    component_class="DCMotor",
    timestamp=1_700_000_000.0,
    inputs={"voltage": 12.0, "load_torque": 0.3},
    outputs={"angular_velocity": 190.5, "winding_temp": 42.1},
    environment={"ambient_temp": 22.0},
    source="user",
))

# Components log their own telemetry automatically during simulation
# and can be read back:
motor_instance.export_telemetry()   # list of dicts

Training an ML solver

Once you have collected enough real-world data you can train a model to replace the physics formula:

from hardwave.telemetry import ModelTrainer

trainer = ModelTrainer(store, component_class="DCMotor")

solver = trainer.train(
    input_ports=["voltage", "load_torque"],
    output_ports=["angular_velocity", "winding_temp"],
    min_samples=500,
    model_type="gradient_boosting",   # "linear" | "gradient_boosting" | "neural_net"
)

# Evaluate on a held-out test set
metrics = trainer.evaluate(solver, test_fraction=0.2)
print(metrics["overall_r2"])   # e.g. 0.998

# Inject into any motor instance, no other code changes needed
motor.set_solver(solver)

Export / import telemetry

# Save to newline-delimited JSON
store.export_jsonl("DCMotor", "data/dc_motor_telemetry.jsonl")

# Load back
store2 = TelemetryStore.from_jsonl("data/dc_motor_telemetry.jsonl")

# Convert to pandas DataFrame for analysis
df = store.to_dataframe("DCMotor")
# Columns: timestamp, component_variant, instance_id, source,
#          input.voltage, input.load_torque, output.angular_velocity, ...

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 ODE (coupled E-M-T)
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 ODE
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

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   # Input port already has a source
├── 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:

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.1.0.tar.gz (68.0 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.1.0-py3-none-any.whl (77.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for hardwave-0.1.0.tar.gz
Algorithm Hash digest
SHA256 045a96ef7858cba801eb2b3a8113097f657a41de7ac28f196542775121535ca7
MD5 a862ba6c5e8cea2d4d22d9f345e8859e
BLAKE2b-256 4e43f68291571356f13d9b264f2ca2dd6ce59ba8c4aaa2b40666377c61e3b2d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for hardwave-0.1.0.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: hardwave-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 77.5 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 898c7924b930f5e342e37a02869f755861ca8a37e16edda82eaad98ec34348ec
MD5 1f859ccdc34cf5fc5831ce633fa4bfb3
BLAKE2b-256 42bb9b441b85738e89defaaaf5d0eb9dff2c782a58a89f803a8fbca95cf2ab7f

See more details on using hashes here.

Provenance

The following attestation bundles were made for hardwave-0.1.0-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