chirppy
Python library for reading and processing CHIRP datasets. It also has a PyTorch Dataloader to generate a CHIRP dataset ready to be used with PyTorch.
CHIRP (Cosys-Lab Hybrid In-air sonar Recording Package) is a HDF5-based dataset format designed for in-air active acoustic sensing. It provides a unified, easy-to-use standard for multi-channel ultrasonic array data, tightly fused with other sensors like LiDAR, Radar, Camera, IMU, GNSS, and Event Camera modalities for robotics research.
CHIRP is acoustic-focused: the acoustic sensor is the reference sensor, defining the grid of "reference captures" every other modality is synchronized to.
Installation
Dependencies
pip install -r requirements.txt
Quick Start
Basic Dataset Loading
from chirppy import CHIRPDataset
# Load dataset
dataset = CHIRPDataset('path/to/dataset.h5')
# Access sensors by property (if using standard formats)
camera = dataset.get_camera(0)
lidar = dataset.get_lidar(0)
acoustic = dataset.get_acoustic_sensor(0)
imu = dataset.get_imu(0)
gnss = dataset.get_gnss(0)
radar = dataset.get_radar(0)
event_camera = dataset.get_event_camera(0)
# Or access directly if sensor exists
camera = dataset.sensors['camera_00']
# Get specific capture. Indexing a property/array like this indexes whatever's actually stored -
# by default camera/lidar/radar/gnss are full_capture, so capture_idx=0 is just each sensor's own
# first raw sample, NOT necessarily acoustic reference capture 0. Use at_reference_capture(idx)
# when you want the sample nearest acoustic reference capture idx.
capture_idx = 0
raw_image = camera.images[capture_idx] # raw: camera's own first sample
image = camera.at_reference_capture(capture_idx)['image'] # synced: nearest sample to reference capture 0
points = lidar.points[capture_idx] # (max_points, 4) float32 [x, y, z, intensity]
signals = acoustic.signals[capture_idx] # (samples, channels) float32
accel = imu.acceleration[capture_idx] # (3,) float32
lat, lon = gnss.latitude[capture_idx], gnss.longitude[capture_idx] # WGS84 decimal degrees
heading = gnss.heading[capture_idx] # degrees, dual-antenna attitude (NaN without a solution)
radar_points = radar.get_all_points(capture_idx) # (N, 7) float32 [x, y, z, doppler_velocity, rcs, motion_status, mode]
# Event camera captures work like any other full_capture sensor, each capture holds a variable-length list of events instead of a fixed-shape image:
capture = event_camera.at_reference_capture(capture_idx) # nearest capture's events, synced
print(capture['event_count'], 'events:', capture['x'], capture['y'], capture['p'], capture['t'])
# Or query by time window directly, independent of capture boundaries:
t = acoustic.get_timestamps()[capture_idx]
events = event_camera.events_in_window(t - 0.05, t + 0.05) # dict of x, y, p, t arrays
# Get calibration data
K = camera.get_intrinsics() # (3, 3) intrinsic matrix
D = camera.get_distortion() # (5,) distortion coefficients
to_origin = lidar.get_transform_to_origin() # (3, 4) sensor→world
to_cam = lidar.get_transform_to_camera() # (3, 4) sensor→cam_00
mic_positions = acoustic.get_receiver_relative_translations() # (N_mics, 3)
# Get all sensors of a type
all_cameras = dataset.get_sensors_by_type('camera')
all_lidars = dataset.get_sensors_by_type('lidar')
all_radars = dataset.get_sensors_by_type('radar')
all_event_cameras = dataset.get_sensors_by_type('event_camera')
# Iterate through acoustic-synced captures (use at_reference_capture, not camera[i] - len(camera)
# is the native sample count when sync_mode == 'full_capture', not the reference-capture count)
for i in range(camera.num_reference_captures):
capture_data = camera.at_reference_capture(i)
image = capture_data['image']
timestamp = capture_data['timestamp']
# Process capture...
PyTorch DataLoader
from chirppy import CHIRPDatasetPyTorch
from torch.utils.data import DataLoader
# Create dataset with selected sensors
dataset = CHIRPDatasetPyTorch(
'path/to/dataset.h5',
sensors=['camera_00', 'lidar_00', 'acoustic_sensor_00', 'imu_00']
)
# Create dataloader with batching
dataloader = DataLoader(
dataset,
batch_size=4,
shuffle=True,
num_workers=2
)
# Iterate through batches
for batch in dataloader:
images = batch['camera_00']['image'] # (B, H, W, C)
points = batch['lidar_00']['points'] # (B, N, 4)
masks = batch['lidar_00']['mask'] # (B, N) bool - valid points
signals = batch['acoustic_sensor_00']['signals'] # (B, samples, channels)
accel = batch['imu_00']['acceleration'] # (B, 3)
# Process batch...
Generic HDF5 Access
# Read arbitrary datasets by path
custom_data = dataset.read_dataset('/sensors/custom_sensor/data/values')
metadata = dataset.read_attribute('/sensors/camera_00', 'sensor_type')
# List available datasets and groups
datasets = dataset.list_datasets('/sensors/camera_00/data')
groups = dataset.list_groups('/sensors')
Flexible Sensor Loading
# Access sensors by name (even if not standard)
custom_sensor = dataset.get_sensor('custom_sensor', create_if_missing=True)
if custom_sensor:
# Access data through generic sensor interface
timestamps = custom_sensor.timestamps
Examples
See the examples/ directory for complete usage examples:
-
basic_example.py - Dataset loading, sensor access, visualization, coordinate frame plotting
-
gnss_example.py - GNSS track visualization (lon/lat scatter colored by fix mode, dual-antenna heading arrows)
-
radar_example.py - Radar point cloud visualization (3D scatter colored by Doppler, RCS, or motion status)
-
event_camera_example.py - Event camera windowed queries and an accumulated-frame visualization
-
pytorch_example.py - PyTorch DataLoader with batching and multi-worker loading
-
odometry_example.py - Robot EKF vs. wheel odometry trajectories from the recording start Analysis tools (applications built on chirppy, not part of the library; Python only) are in tools/:
-
projection.py -
project_points(K, points, D, model)(pinhole, plumb-bob or fisheye) andproject_to_camera(ds, points, source, camera, ...), which projects points of any sensor into a calibrated lens camera using the stored transforms and intrinsics -
verify_sync_motion.py - Checks the sensors' timestamps against each other by comparing how each one sees the robot move (the odometry's pose differentiated into speed and yaw rate - its reported velocity fields are an averaged, late copy - the IMU gyroscope, radar Doppler, LiDAR scan rotation, camera/GoPro frame change, event rate) and cross-correlating them to a per-sensor time offset;
--against imutells a late odometry from early sensors -
verify_transforms.py - Cross-checks the sensor transforms by projecting between sensors (LiDAR vs. depth image, radar vs. LiDAR, overlays on every camera, ground plane, IMU gravity, sync report)
Coordinate Systems
Every sensor stores its data in one data frame, and to_origin maps that frame to the origin:
1. Robot axes (origin frame, LiDAR, radar, IMU, GNSS, acoustic, odometry)
- Convention: Right-handed, X-forward, Y-left, Z-up
- Origin: the acoustic array center
2. Optical frame (every lens camera: RGB, thermal, event, video)
- Convention: Right-handed, Z-forward, X-right, Y-down (KITTI / OpenCV), so
Kapplies to a point in this frame directly - Origin: the camera's optical center
to_cam_00 is inv(camera_00 to_origin) @ sensor to_origin: the sensor's data in the reference camera's optical frame (the reference camera's own to_cam_00 is the identity).
Transform example:
# Any pair of sensors: 4x4 T with p_target = T @ p_source, via the origin frame
T = dataset.get_transform('lidar_00', 'thermal_camera_00')
points_in_thermal_frame = dataset.transform_points(lidar_points, 'lidar_00', 'thermal_camera_00')
# projecting points into a camera is application code: see tools/projection.py
# the same by hand with the stored matrices
P_cam = lidar.get_transform_to_camera() @ [x, y, z, 1]
u, v = (camera.get_intrinsics() @ P_cam[:3])[:2] / P_cam[2]
To check that all transforms are right, run tools/verify_transforms.py dataset.h5: it cross-checks sensors against each other (LiDAR range vs. the RealSense depth image, radar vs. LiDAR, ground plane, IMU gravity, sync) and writes LiDAR/radar overlays on every camera.
Data Types
- Images:
uint8RGB format(N, H, W, 3) - LiDAR point clouds:
float32[x, y, z, intensity], NaN-padded to fixed size - Radar point clouds:
float32[x, y, z, doppler_velocity, rcs, motion_status, mode], NaN-padded to fixed size - Event camera data:
x/yuint16,puint8(1=ON/0=OFF),tfloat64Unix epoch seconds - one flat, time-ordered array per field across all captures, withcapture_event_start/capture_event_countlocating each capture's slice - Acoustic signals:
float32(N, samples, channels)format - IMU data:
float32[ax, ay, az]or[wx, wy, wz] - GNSS data:
float64latitude/longitude/height, velocity_neu, heading/pitch/roll (NaN without a dual-antenna solution), course_over_ground (NaN when stationary/no fix), position/velocity/attitude covariancesuint8fix_mode/num_satellites - Timestamps:
float64Unix epoch seconds - Transforms:
float64(4, 4)homogeneous transformation matrices
API Reference
CHIRPDataset
Main dataset class for reading CHIRP HDF5 files.
Attributes:
format- Format name (should be "chirp")version- Format version stringn_reference_captures- Number of acoustic reference capturesreference_sensor- Name of reference sensor for synchronizationsensors- Dictionary of all loaded sensor objectsh5file- Underlying h5py.File object
Methods:
Sensor Access:
get_sensor(name, create_if_missing=True)- Get sensor by name, optionally loading if not already loadedget_sensors_by_type(sensor_type)- Get all sensors of specific type (e.g., 'camera', 'lidar')get_camera(idx=0)- Get camera sensor by index (e.g., camera_00)get_lidar(idx=0)- Get LiDAR sensor by indexget_acoustic_sensor(idx=0)- Get acoustic receiver array by indexget_acoustic_transmitter(idx=0)- Get acoustic transmitter by indexget_imu(idx=0)- Get IMU sensor by indexget_gnss(idx=0)- Get GNSS sensor by indexget_radar(idx=0)- Get radar sensor by indexget_event_camera(idx=0)- Get event camera sensor by indexget_video(idx=0)- Get video sensor by index (frames live in an MP4 beside the.h5)get_thermal_camera(idx=0)- Get thermal camera sensor by index (raw radiometric frames)get_odometry(idx=0)- Get odometry sensor by index (0 = EKF, 1 = wheel)
Reference-capture access (the acoustic sensor defines the grid):
dataset[i]/get_reference_capture(i, sensors=None)- every sensor's sample nearest reference capturei, keyed by sensor namereference_timestamps- Unix-epoch times of the reference captures
Frames and projection:
get_transform(source, target)- 4x4Twithp_target = T @ p_sourcebetween any two sensors (names or objects) or the frames'origin','base_link'and'ground'(the last two need format v2.1), in each sensor's data frame (see Coordinate Systems)transform_points(points, source, target)- the same for(N, >=3)points; extra columns (intensity, doppler, ...) are keptorigin_height_above_ground,base_link_height_above_ground- heights in meters (nominal),Nonefor v2.0 datasets without a ground frame
Generic HDF5 Access:
read_dataset(path, selection=...)- Read arbitrary HDF5 dataset by path, optionally only a slice (e.g.slice(0, 10))read_attribute(path, attr_name)- Read HDF5 attributelist_datasets(group_path)- List all datasets under pathlist_groups(group_path)- List all groups under path
Special Methods:
__len__()- Returns number of reference captures__enter__(),__exit__()- Context manager supportclose()- Close HDF5 file
Sensor Classes
Every sensor also has (see BaseSensor):
indices_in_window(t_start, t_end)/get_window(t_start, t_end)- native captures with timestamp in[t_start, t_end);ImuSensor.get_windowreturns stacked arrays,EventCameraSensor.events_in_windowreturns eventspoints_to_origin(points)- express(N, >=3)points from the sensor frame in the origin frame (T_to_origin @ [x, y, z, 1]), extra columns keptcan_align_to_reference- False for afull_capturesensor withoutsync_to_reference_index- Unknown sensor types load as
GenericSensor(timestamps, transforms, raw datasets) - PyTorch:
CHIRPDatasetPyTorchworks withDataLoader(num_workers>0); passcollate_fn=chirp_collateto batch variable-length data such as event-camera captures
All sensors inherit from BaseSensor and provide common interface:
BaseSensor (Abstract Base):
sensor_type(property) - Type string (e.g., 'camera', 'lidar')reference_frame(property) - Reference coordinate frametimestamps(property) - Capture timestamps arraycalibration(property) - Calibration data namespaceget_timestamps()- Get timestamps arrayget_transform_to_origin()- Get sensor→world transform (3, 4)get_transform_to_camera()- Get sensor→cam_00 transform (3, 4)__len__()- Number of captures (native sample count ifis_full_capture, elsen_reference_captures)__getitem__(idx)- Get capture data dictionary (indexes the array as actually stored - seesync_mode)sync_mode(property) -'full_capture'(default) or'reference_capture'; see README.md's "Full-Capture vs. Reference-Capture Storage"is_full_capture(property) - Shorthand forsync_mode == 'full_capture'num_reference_captures(property) - Number of acoustic reference captures this sensor aligns to (alwaysn_reference_captures, regardless ofsync_mode)at_reference_capture(capture_idx, **kwargs)- Get the sample nearest acoustic reference capturecapture_idx, regardless ofsync_mode- use this instead ofsensor[idx]whenever you specifically want the acoustic-synced view (**kwargsforwarded to__getitem__, e.g.keep_paddingfor LiDAR/radar)
Lens cameras (CameraSensor, ThermalCameraSensor, EventCameraSensor, VideoSensor) share get_intrinsics() (K), get_distortion() (D), get_image_size(), distortion_model ('plumb_bob' or 'equidistant' fisheye), and is_calibrated. Intrinsics appear once a calibration file is set for that camera in transforms/sensor_objects_description.json (cam_file); until then is_calibrated is False.
CameraSensor:
images(property) - Image array (N, H, W, 3)has_depth(property) - Whether this recording captured depth (--camera-depth) alongside color - most don't, it's off by defaultdepth(property) - Depth array (N, H, W),uint16millimeters, aligned toimages, orNoneifhas_depthisFalseget_depth_meters(idx)- Depth frame at index converted tofloat32meters, orNoneifhas_depthisFalse__getitem__(idx)'s'depth'key isNonewhenhas_depthisFalse, otherwise the same(H, W)uint16-millimeter array asdepth[idx]calibration.K- Intrinsic matrix (3, 3)calibration.D- Distortion coefficients (5,)calibration.image_size- Image dimensions (2,)get_intrinsics()- Get K matrixget_distortion()- Get distortion coefficientsget_image_size()- Get [height, width]get_factory_calibration()- The calibration the camera itself reported when this was recorded, as a dict of arrays, orNonefor older recordings:color_K/color_D(intrinsics of the stored images, adjusted for the 180° mounting rotation),color_K_sensor/color_D_sensor/depth_K_sensor/depth_D_sensor(as the SDK reports them), 4×4depth_to_color/accel_to_depth/gyro_to_depth(p_dst = T @ p_src, meters), andcalibration_table(raw bytes that can be written back to a camera). Separate fromcalibration.K/D, which are your own calibrationget_device_info()- Full snapshot taken at the start of the recording (device name, serial, firmware, stream settings, depth-sensor options such as visual preset and laser power, depth filter chain) as a dict, orNonefor older recordingsdevice_serial(property) - Serial number of the camera that recorded this, if known
LidarSensor:
points(property) - Point cloud array (N, max_points, 4)point_counts(property) - Valid points per capture (N,)max_points(property) - Maximum points capacityget_all_points(idx, remove_padding=True)- Get points for capture with optional padding removal__getitem__(idx, keep_padding=False)- Get capture with control over NaN padding
RadarSensor:
points(property) - Point cloud array (N, max_points, 7)[x, y, z, doppler_velocity, rcs, motion_status, mode]point_counts(property) - Valid points per capture (N,)max_points(property) - Maximum points capacityget_all_points(idx, remove_padding=True)- Get points for capture with optional padding removal__getitem__(idx, keep_padding=False)- Get capture with control over NaN paddingmotion_statusvalues:-1moving towards,0standing,1moving away - classified per-point against a per-capture ego-velocity estimate (see the altos-v2-py driver, which computes this at record time)mode: which of the sensor's two interleaved range-profile configs (short/long range) this point came from - each capture already combines the nearest-in-time frame of both (see convert_raw_to_chirp's_pair_radar_frames), so points from different modes can differ in resolution/unambiguous velocity even within one capture
EventCameraSensor: a capture holds a variable-length list of events, so __len__/__getitem__/at_reference_capture/sync_mode all work exactly like
every other sensor.
__len__()- Number of captures__getitem__(idx)- Get capture at index:{x, y, p, t, event_count, timestamp}(x/y/p/tare that capture's events;timestampis the capture's own host timestamp, nott[0]/t[-1].at_reference_capture(capture_idx)- Get the events from the capture nearest acoustic reference capturecapture_idx, regardless ofsync_mode.x,y,p(properties) - Direct access to the flat, all-captures-concatenated event datasets (N_events,)capture_event_start,capture_event_count(properties) - Direct access to the per-capture index arrays (N_captures,)image_width,image_height(properties) - Sensor resolution in pixelsevents_in_window(t_start, t_end)- Get every event withtin[t_start, t_end), independent of capture boundaries, useful when you want a time window that doesn't line up with a single capture. Note: each capture is timestamped independently against the host clock rather than one shared anchor for the whole recording
ThermalCameraSensor: raw radiometric frames from a FLIR A70. __getitem__(i) returns temperature_c (float32, degC), the stored raw uint16 (temperature_K = raw * kelvin_per_count), timestamp, device_timestamp_ns (the camera's own free-running clock in nanoseconds - not Unix time, and -1 when the camera delivered a corrupt reading), repeated and frozen. Frames inside a recalibration (NUC) freeze are kept and flagged; filter with the frozen mask. get_temperature_c(i) returns temperatures only. colorize(i, palette, temperature_range) produces a false-color image with the iron, inferno or gray palette, with no extra dependency. Pass a fixed temperature_range to keep colors comparable between frames; the default stretches each frame on its own. measurement_parameters gives the emissivity etc. the camera used.
OdometrySensor: the robot's own odometry, one sensor per stream: odometry_00 = EKF, odometry_01 = wheel (source says which). __getitem__(i) returns base_link's position (3,) and orientation [x, y, z, w] in the odom frame, yaw (radians), linear_velocity/angular_velocity in base_link, pose_covariance/twist_covariance (6, 6), timestamp and ros_timestamp. Right-handed X forward, Y left, Z up. Timing: on the Husky, linear_velocity and angular_velocity are averaged over roughly 0.8-1 s and stamped at the end of the window, so they trail the robot's real motion by about 0.4 s (measured against the LiDAR IMU), whereas position and orientation are in sync with the other sensors to within ~50 ms. For anything that depends on timing, differentiate the poses (poses()) instead of using the reported velocities. get_pose_matrix(i) and poses() give 4x4 poses; relative_poses(start=0) makes the trajectory start at the identity (the odom frame starts where the driver started, not the recording). path_length() is the distance driven. Example: examples/odometry_example.py.
VideoSensor: the payload is an MP4 sibling of the .h5, not data inside it, so this class stores no
frames, it only holds configuration parameters and time-sync information. __getitem__(i)
returns {frame_index, timestamp, pts_s, video_path} and deliberately does not decode, so iterating is cheap; get_image(i) decodes a single frame by shelling out to ffmpeg.
frame_for_reference_capture(i) returns (frame_index, timestamp, valid, dt_s), and
at_reference_capture(i) adds sync_valid/sync_dt_s/reference_timestamp to the returned dict.
set_manual_offset(seconds, persist=False) corrects the alignment without re-running a conversion: the
offset is applied at read time, and the mapping is re-derived from the stored reference_timestamps.
Positive means the video actually started later than estimated - so a fixed wall-clock capture then
lands on an earlier video frame.
video = dataset.get_video()
frame_idx, t, valid, dt = video.frame_for_reference_capture(137)
if valid:
image = video.get_image(frame_idx)
AcousticSensor (Microphone Array):
signals(property) - Audio signals (N, samples, channels)num_channels(property) - Number of microphonescalibration.receiver_relative_translations- Microphone positions (N_mics, 3)get_receiver_positions()- Alias for receiver_relative_translationsget_receiver_relative_translations()- Get microphone 3D positions
AcousticTransmitter:
signals(property) - Emitted signal waveform (N_samples,)get_emitted_signal()- Get emitted chirp/sweep signal__getitem__(idx)- Get emission timestamp for capture
ImuSensor:
acceleration(property) - Linear acceleration (N, 3)angular_velocity(property) - Angular velocity (N, 3)
GnssSensor: curated to the fields useful for navigation/SLAM/robotics fusion - GNSS-diagnostic-only fields (DOP, protection levels, MSL/geoid height, GNSS-derived attitude rates, scalar accuracy, error flags) aren't exposed here; the raw per-epoch JSON in record_raw's gnss/ folder still has the full field set.
latitude/longitude(property) - WGS84 position (N,) decimal degreesheight(property) - Height above the WGS84 ellipsoid (N,) metersvelocity_neu(property) - NEU velocity (N, 3) m/sheading/pitch/roll(property) - Dual-antenna attitude (N,) degrees, NaN without a solutioncourse_over_ground(property) - Movement-derived heading (N,) degrees, NaN when stationary/no fix;Noneif absent (datasets converted before this field was added)position_covariance/velocity_covariance/attitude_covariance(property) - Covariance arrays (N, 10) / (N, 10) / (N, 6)fix_mode(property) - Fix quality (N,): 0=No Fix, 1=Stand-Alone, 2=DGPS, 3=Fixed, 4=RTK Fixed, 5=RTK Float, 6=SBAS, 7=MB RTK Fixed, 8=MB RTK Float, 9=PPPfix_mode_string(property) - Human-readable fix label derived fromfix_modenum_satellites(property) - Satellites used in fix (N,)gnss_time(property) - Unix epoch UTC (N,), from GPS time-of-week/week number
CHIRPDatasetPyTorch
PyTorch-compatible dataset with fixed-size tensor outputs and validity masks. Always yields the acoustic-synced view (n_reference_captures samples) regardless of whether any included sensor has sync_mode == 'full_capture' - it resolves each sample internally via at_reference_capture(), so training loops never need to deal with full-capture/reference-capture mismatches.
Constructor:
CHIRPDatasetPyTorch(
h5_path: str,
sensors: Optional[List[str]] = None,
transform: Optional[Callable] = None,
target_transform: Optional[Callable] = None
)
Parameters:
h5_path- Path to HDF5 filesensors- List of sensor names to load (None = all sensors)transform- Optional transform to apply to datatarget_transform- Optional transform for targets/labels
Methods:
__len__()- Number of samples__getitem__(idx)- Get multi-modal sample as PyTorch tensorsget_sensor_list()- Get list of loaded sensor namesget_base_dataset()- Get underlying CHIRPDataset object
Returns:
- Dictionary with sensor data as PyTorch tensors
- Note that LiDAR and radar include an additional field
mask(bool tensor) for valid points (masks NaN padding)
License
This project is released under the CC-BY-NC-SA-4.0 License. Please review the License file for more details.
Metadata
Release files for cosys-chirppy 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cosys_chirppy-2.1.0.tar.gz | 76.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cosys_chirppy-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 125.8 kB
Release files / cosys_chirppy-2.1.0.tar.gz
| Download URL | cosys_chirppy-2.1.0.tar.gz |
|---|---|
| Size | 76.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca50354db1a78cc6d03e80c8398a2468a710362aba4d1e92d0187bc346899869
|
|
BLAKE2b-256 checksum How to use checksums |
018b4cf26637ec12c19c12cfb526deed06a38a0a8d00ef8859a4f98de3a421a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.9
|
Release files / cosys_chirppy-2.1.0-py3-none-any.whl
| Download URL | cosys_chirppy-2.1.0-py3-none-any.whl |
|---|---|
| Size | 49.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cf926bb8f27ae6360f6380f36a0e0751fba013c81076f805d9957937128ffe47
|
|
BLAKE2b-256 checksum How to use checksums |
5715bf583517791ead9ab7661e4d47d0601c0905a200249f55f8ffc8efccd9b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.9
|