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
- What Hardwave Does
- Core Concepts
- Installation
- Quick Start
- Module Structure
- Working with Types
- Defining Components
- Building Simulation Graphs
- Running Simulations
- Composite Components
- Solver System
- Transient Simulation
- Telemetry and ML Solvers
- Standard Library Reference
- Error Handling
- 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
ComponentMetato 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
firmwareparameter 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
- Everything is a component. A resistor, an Arduino, a robot arm; the abstraction never breaks.
- Components are defined in code. Python classes are the source of truth; a UI layer wraps them.
- Solvers are pluggable. Swap a formula for a lookup table or an ML model without touching any wiring.
- Strict typing at the boundary. Type errors are caught at graph construction time, not at simulation time.
- Composability is first-class. A composite component is indistinguishable from a primitive at its interface.
- Simulation semantics are explicit. Time domain is declared per solver, not inferred.
- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
045a96ef7858cba801eb2b3a8113097f657a41de7ac28f196542775121535ca7
|
|
| MD5 |
a862ba6c5e8cea2d4d22d9f345e8859e
|
|
| BLAKE2b-256 |
4e43f68291571356f13d9b264f2ca2dd6ce59ba8c4aaa2b40666377c61e3b2d9
|
Provenance
The following attestation bundles were made for hardwave-0.1.0.tar.gz:
Publisher:
release.yaml on HardwaveDev/hardwave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hardwave-0.1.0.tar.gz -
Subject digest:
045a96ef7858cba801eb2b3a8113097f657a41de7ac28f196542775121535ca7 - Sigstore transparency entry: 2091887911
- Sigstore integration time:
-
Permalink:
HardwaveDev/hardwave@f2c53d65293148446719e748d000aae542d18ce3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/HardwaveDev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@f2c53d65293148446719e748d000aae542d18ce3 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
898c7924b930f5e342e37a02869f755861ca8a37e16edda82eaad98ec34348ec
|
|
| MD5 |
1f859ccdc34cf5fc5831ce633fa4bfb3
|
|
| BLAKE2b-256 |
42bb9b441b85738e89defaaaf5d0eb9dff2c782a58a89f803a8fbca95cf2ab7f
|
Provenance
The following attestation bundles were made for hardwave-0.1.0-py3-none-any.whl:
Publisher:
release.yaml on HardwaveDev/hardwave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hardwave-0.1.0-py3-none-any.whl -
Subject digest:
898c7924b930f5e342e37a02869f755861ca8a37e16edda82eaad98ec34348ec - Sigstore transparency entry: 2091888061
- Sigstore integration time:
-
Permalink:
HardwaveDev/hardwave@f2c53d65293148446719e748d000aae542d18ce3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/HardwaveDev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@f2c53d65293148446719e748d000aae542d18ce3 -
Trigger Event:
release
-
Statement type: