nominal-iads
Read an IADS archive from Python and upload its channel data to a Nominal dataset. The package handles extraction, temporary Parquet files, and concurrent uploads. You choose the archive, destination, and optional channel/time filters.
Windows x64 only · Python 3.11+ (tested on 3.11–3.14) · Licensed IADS required
Install · Inspect an archive · Upload to Nominal · Read windows and UTC · Options · Tables and metadata sources · Troubleshooting
1. Install
Run these commands in PowerShell with a 64-bit Python installation:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install nominal-iads
python -c "from nominal.iads import __version__; print(__version__)"
The wheel includes the extraction worker executable and installs the compatible
nominal SDK automatically. You do not need Rust, Cargo, or a separate worker download.
It does not include IADS or an IADS license.
IADS must be installed and its COM component registered. If your installation has not already registered it, run this once in an administrator PowerShell, adjusting the DLL path to your installation:
regsvr32 "C:\Program Files\IADs\IadsDataInterfaces.dll"
Importing the package checks the Python installation; the next example also exercises IADS access. Local archive inspection does not require Nominal credentials.
2. Check your archive
Replace the placeholder path with your local IADS archive path:
from nominal.iads import Archive
with Archive(r"C:\data\example-archive") as archive:
info = archive.info()
print("Archive time span:", info.start_time, "to", info.end_time)
names = archive.channel_names()
print(f"{len(names)} extractable channels")
print("First channels:", names[:10])
Keep archive operations inside the with block. Leaving it releases the archive's
COM connection; using the archive outside that block raises RuntimeError.
For channel metadata, use archive.channels() (a dictionary keyed by channel name).
archive.read_groups() shows the rate-aligned groups used for extraction.
3. Upload to Nominal
You need a Nominal API token, your tenant's API base URL, and access to the destination workspace. Find the API URL in Nominal's API-key settings; do not substitute the web-app URL. See the Nominal authentication guide for tenant and workspace setup.
Configure a connection profile once
Replace the placeholders with your Nominal credentials and destination workspace:
nom config profile add "<profile_name>" --token "<api_token>" --base-url "<url>" --workspace-rid "<workspace_rid>"
The command saves a local connection profile. Use the same profile name in Python with
NominalClient.from_profile("<profile_name>"). If you already use the SDK, reuse your
existing profile.
Run your first upload
This example creates a new dataset and uploads the archive's extractable channels. Replace the archive path and, if necessary, the profile name. Creating a dataset and uploading data both write to your Nominal workspace.
from nominal.core import NominalClient
from nominal.iads import Archive, enable_logging
enable_logging("info")
client = NominalClient.from_profile("<profile_name>")
dataset = client.create_dataset("Example IADS archive")
with Archive(r"C:\data\example-archive") as archive:
with archive.upload_to_nominal(client, dataset, raise_on_failure=True) as run:
stats = run.join()
print(f"Uploaded {stats.files_uploaded} files and {stats.points_uploaded} channel values")
print("Cancelled:", stats.cancelled)
print("Dataset:", dataset.rid)
Call join() inside the upload context. It waits for extraction and upload, returns
final counts, and surfaces errors. Exiting the context without joining cancels the run
and waits for cleanup; cleanup alone does not report its errors.
join() confirms that successful files were uploaded and ingest was requested. It does
not wait for Nominal's server-side ingest to finish. Open the destination dataset in
Nominal to check ingest status and view the channels.
To append to an existing dataset, replace create_dataset(...) with:
import os
dataset = client.get_dataset(os.environ["NOMINAL_DATASET_RID"])
Rerunning create_dataset(...) creates another dataset. Uploading again to an existing
dataset appends data; this library does not deduplicate repeated uploads.
Select channels and time windows
Options are keyword arguments; the defaults work for a first upload. Preview a channel
selection before uploading. This example uploads only names containing accel or gyro,
excluding names ending in _raw:
from nominal.iads import Archive
# Reuse the authenticated client and destination dataset from the quick start.
with Archive(r"C:\data\example-archive") as archive:
include = ["accel", "gyro"]
exclude = ["_raw$"]
names = archive.extract_channel_names(
channel_whitelist=include,
channel_blacklist=exclude,
)
print(f"Selected {len(names)} channels")
if not names:
raise ValueError("No channels matched; check the patterns before uploading")
with archive.upload_to_nominal(
client,
dataset,
channel_whitelist=include,
channel_blacklist=exclude,
raise_on_failure=True,
) as run:
stats = run.join()
Patterns are regular expressions with substring matching; use ^ and $ for exact
boundaries. An empty whitelist includes all extractable channels. priority_channels
accepts exact names, not regexes.
Read windows: archive IRIG time versus real UTC time
Use the timestamps recorded in the archive to select data. Use a separate UTC anchor when you know when that data actually occurred. These are two different decisions.
IADS archives can use a file-local time reference. Their IRIG start/end values and
flight_date metadata are not necessarily an accurate record of real-world time.
For example, an archive may start at 001:00:00:00.000 even though the recording was
made in July. Do not assume that this means the recording actually began on January 1.
| Value | What it controls |
|---|---|
archive.info().start_time / .end_time |
The archive's recorded IRIG coordinates; use these to understand its read range |
archive.info().flight_date |
Calendar metadata used for default date resolution; it may need independent verification |
between=(start, end) |
Which part of the archive to read, expressed in that archive's IRIG coordinates |
absolute_read_start_time |
The real UTC instant corresponding to the requested start in between |
IRIG strings use DDD:HH:MM:SS.sss. They carry a day number and time, but no year or
time zone. A read bound is a coordinate on the archive's clock, not an elapsed duration
from opening the file and not a UTC timestamp. Inspect the actual values first:
from nominal.iads import Archive
with Archive(r"C:\data\example-archive") as archive:
info = archive.info()
print("Recorded IRIG range:", info.start_time, "to", info.end_time)
print("Recorded flight date:", info.flight_date)
Select a range on the archive's clock
Suppose an archive starts at 001:00:00:00.000. To read from five minutes through
fifteen minutes into that recording, use:
from nominal.iads import IrigTimestamp
start = IrigTimestamp.from_irig("001:00:05:00.000")
end = IrigTimestamp.from_irig("001:00:15:00.000")
# Pass between=(start, end) to archive.upload_to_nominal(...).
If the archive instead starts at 123:10:00:00.000, the same elapsed window is
123:10:05:00.000 through 123:10:15:00.000. Copying the first example's day and clock
time would select the wrong range, potentially returning no data. Channel coverage
can further limit which samples exist within the requested window.
Put that window at the correct time in Nominal
Continuing the first example, suppose an independent source tells you the recording actually began at 2026-07-01 14:00:00 UTC. The selected window begins five minutes later, so its UTC anchor must be 14:05:00, not 14:00:00:
from datetime import datetime, timezone
from nominal.iads import Archive, IrigTimestamp
# Reuse client and dataset from the quick start.
# These are illustrative times: replace them with your archive's coordinates
# and an independently known UTC time for the START OF THE SELECTED WINDOW.
start = IrigTimestamp.from_irig("001:00:05:00.000")
end = IrigTimestamp.from_irig("001:00:15:00.000")
read_start_utc = datetime(2026, 7, 1, 14, 5, tzinfo=timezone.utc)
with Archive(r"C:\data\example-archive") as archive:
with archive.upload_to_nominal(
client,
dataset,
between=(start, end),
absolute_read_start_time=read_start_utc,
raise_on_failure=True,
) as run:
stats = run.join()
This maps archive coordinate 001:00:05:00.000 to 2026-07-01 14:05:00 UTC;
a sample at 001:00:06:00.000 maps to 14:06:00 UTC. The mapping is:
sample UTC = absolute_read_start_time + (sample IRIG - requested start IRIG)
The anchor changes the time assigned to samples in Nominal; it does not change which
archive samples are read. It anchors the requested start even if a particular channel's
first available sample occurs later. For a whole-archive read without between, the
anchor corresponds to the archive's recorded start instead.
Defaults and date validation
If you omit absolute_read_start_time, the library derives an absolute timeline from
the archive's IRIG values and flight-date metadata. Use that default only when those
metadata describe the timeline you intend to see in Nominal.
validate_flight_date=True checks calendar consistency when resolving the archive span.
It does not prove that the metadata match the real recording time. Setting it to
False skips that consistency check; it does not correct a wrong clock, select a
different read window, or provide a real UTC anchor. The flight-date year is still used
for calendar validation of read bounds, even with an explicit UTC anchor.
When the absolute date is unreliable but you know the real time of the selected window,
provide both between and absolute_read_start_time as above. If you do not know that
real time, the library cannot infer it from an arbitrary file-local clock.
Upload options
| Option | Default | Use |
|---|---|---|
between |
Entire archive | Select an IRIG time window |
absolute_read_start_time |
From archive metadata | Supply a known UTC read-window start |
validate_flight_date |
True |
Require the flight date within the archive's IRIG span |
priority_channels |
None | Read groups containing these exact channel names first |
tags |
None | Key/value tags on ingested files |
channel_prefix |
None | Prefix every ingested channel name |
files_in_flight |
4 |
Maximum concurrent file uploads |
num_workers |
0 (auto) |
Number of isolated extraction worker processes |
raise_on_failure |
False |
Make join() raise on partial upload/extraction failures |
Advanced tuning: read_batch_size and write_batch_size count channel values, not
bytes or rows. Rust coordinates uploads inside the Python extension. Each extraction
worker runs in its own process with its own IADS DLL instance.
Use help(Archive.upload_to_nominal) for the complete signature.
Progress, cancellation, and partial failures
Reuse client and dataset from above. Poll at a modest interval:
import time
from nominal.iads import Archive
with Archive(r"C:\data\example-archive") as archive:
with archive.upload_to_nominal(client, dataset, raise_on_failure=False) as run:
while not run.is_finished():
progress = run.upload_progress()
print(f"Uploaded {progress.files_uploaded} files; {progress.upload_mb_per_second:.1f} MiB/s")
time.sleep(0.5)
# To stop early: run.cancel()
stats = run.join()
print("Cancelled:", stats.cancelled)
for failure in stats.failures:
print("Upload failure:", failure)
for message in stats.extraction_failures:
print("Extraction failure:", message)
With raise_on_failure=False, partial failures are returned in stats.failures and
stats.extraction_failures; inspect both. Fatal infrastructure errors and an all-failed
upload still raise. With True (used in the quick start), any recorded upload or
extraction failure raises. Cancellation can still produce partial successful counts;
check stats.cancelled. Cancelling does not undo files already uploaded to Nominal.
Call join() only once. After it, use the returned stats; upload_progress() and
drain_results() are no longer available. Before joining, extract_progress() gives
an extraction snapshot and drain_results() returns newly uploaded files.
Extraction uses temporary disk space; allow room for Parquet files awaiting upload. Temporary files are cleaned up after extraction and upload have stopped.
Archive metadata, event logs, and configuration tables
An archive contains more than sampled channels. You can read event markers, time
segments, test points, analysis logs, and configuration records without uploading
anything to Nominal. All of these calls belong inside with Archive(...).
Read event markers and time segments
from nominal.iads import Archive
with Archive(r"C:\data\example-archive") as archive:
for event in archive.event_markers():
print(event.time, event.comment, event.group)
print("Additional columns:", event.extras)
for segment in archive.time_segments():
print(segment.start, segment.end, segment.description)
These methods return lists of typed records with read-only properties. Times are
IrigTimestamp values on the archive's clock, not automatically corrected UTC
values. The upload's absolute_read_start_time does not transform records returned
by these methods; use the same explicit time mapping described in the read-window
section when relating annotations to the uploaded channels.
Reading these tables does not create Nominal events, annotations, or test points.
upload_to_nominal() uploads channel samples; transferring log/configuration records
to Nominal is a separate application step.
Available tables
Archive method |
IADS table | Contents |
|---|---|---|
event_markers() |
EventMarkerLog |
Point-in-time comments, user and group information |
time_segments() |
TimeSegmentLog |
Labeled start/end ranges |
test_points() |
TestPointLog |
Recorded test points, maneuvers and time ranges |
actual_flutter_test_points() |
ActualFlutterTestPointsLog |
Recorded flutter test points and conditions |
actual_loads_test_points() |
ActualLoadsTestPointsLog |
Recorded loads test points and conditions |
analysis_log() |
AnalysisLog |
Parameter analysis, frequency and damping results |
threshold_log() |
ThresholdLog |
Parameter threshold criteria |
peaks_log() |
PeaksLog |
Peak records |
data_edits() |
DataEditLog |
Recorded data edits |
selections_log() |
SelectionsLog |
Saved selections |
planned_test_points() |
PlannedTestPoints |
Planned test points |
planned_flutter_test_points() |
PlannedFlutterTestPoints |
Planned flutter test points |
planned_loads_test_points() |
PlannedLoadsTestPoints |
Planned loads test points |
mission_attributes() |
MissionAttributes |
Mission attributes |
constants() |
Constants |
Configuration constants |
classifications() |
Classifications |
Classification records |
Not every archive populates every table. Empty, absent, and unknown tables all return
[]: IADS does not distinguish these cases in the query result. A misspelled table name
can therefore look like an empty table. Genuine COM/query failures still raise errors.
Typed properties can be None when the source column is absent, empty, or cannot be
converted to the expected type. Unmapped columns, including names that are awkward
Python identifiers, are retained in record.extras. Malformed mapped values are also
retained there rather than silently discarded. Values in extras are str, float,
or None. Use help(type(record)) to inspect the properties of a returned record.
Read a table without a typed wrapper
Use read_table(name) for another table exposed by IADS, or when you want the original
column names and values instead of the typed view:
from nominal.iads import Archive
with Archive(r"C:\data\example-archive") as archive:
for record in archive.read_table("EventMarkerLog"):
print(record.fields.get("Comment"))
print("Raw Time:", record.fields.get("Time"))
print("Parsed IRIG Time:", record.times.get("Time"))
Each IadsRecord exposes two dictionaries:
fields: every returned column under its original name, withstr,float, orNonevalues.times: successfully parsed IRIG values forTime,StartTime,StopTime, andEndTime. Unparseable values remain infields; the row is kept.
Pass a table name, not a SQL statement or a filename. This is a read-only table lookup,
not a general SQL API. Typed table reads are cached on the archive object; read_table()
queries IADS each time. Reopen the archive to refresh cached metadata after external changes.
Where the metadata comes from: pfconfig, iadsarchiveinfo.txt, and IADS
Pass the archive path to Archive, not separate paths to pfconfig or
iadsarchiveinfo.txt. This library does not contain direct parsers for those files.
It asks the installed IadsDataInterfaces.iadsread COM component for the archive's
metadata, configuration tables, and samples. IADS resolves the underlying archive files.
Consequently, the Python objects represent the values IADS returns, not a byte-for-byte
view of a particular sidecar file. If you are comparing them with pfconfig or
iadsarchiveinfo.txt, keep that boundary in mind; the library does not define its own
file-precedence or fallback rules between those sources.
| Python view | How it is obtained |
|---|---|
info() |
IADS archive-info response: StartTime → start_time, StopTime → end_time, Date → flight_date, plus DataDir, Flight, Test, and Tail |
raw_channel_names() |
IADS parameter-name enumeration, including names that may not be extractable |
channels() / channel_names() |
Parameter metadata from ParameterDefaults plus per-parameter queries, then extraction eligibility and update-rate resolution |
| Typed log/config methods | IADS table queries, mapped to named Python properties |
read_table(name) |
IADS select * from <table> query, retained as generic records |
| Uploaded channel samples | IADS sample reads for the selected channels and IRIG windows |
channels() is an extractable-channel view, not every row from the configuration.
Channels without recorded time bounds or a resolvable sample rate are excluded;
rate-zero derived channels can inherit a rate from their dependencies. Compare
raw_channel_names() with channel_names() when investigating missing channels.
Use read_table("ParameterDefaults") to inspect the raw configuration rows exposed by IADS.
info().flight_date comes from the returned Date field (parsed as month/day/two-digit
year). start_time and end_time retain the returned IRIG coordinates. Reading metadata
does not establish that these values describe real UTC time—see the read-window section.
Troubleshooting
Call enable_logging("info") once at startup for concise progress and terminal summaries.
Use enable_logging("debug") instead when diagnosing a problem. Logs go to stderr.
| Symptom | Check |
|---|---|
| No matching wheel / installation fails | Windows x64, 64-bit Python 3.11+, and an up-to-date pip |
Class not registered |
IADS COM registration and the DLL path in the setup section |
FileNotFoundError opening an archive |
The archive path exists and is accessible |
RuntimeError about an archive not entered |
Keep calls inside with Archive(...) |
| Missing bundled worker | Reinstall the wheel to restore the bundled extraction worker |
| Authentication / workspace error | Use the API base URL and a profile/token with access to the destination workspace |
| Flight-date / IRIG validation error | Check archive dates and the selected time range; supply a known UTC anchor when appropriate |
| Zero uploaded files | Check channel selection, the time window, and stats.extraction_failures |
| Uploads finished but data is not visible | Check server-side ingest status in Nominal |
Package-specific errors derive from nominal.iads.NominalIadsError, including ArchiveError,
ConfigError, WorkerError, and ExtractionError. Upload/client errors may use the base
class directly. Missing paths and invalid context-manager use raise Python's built-in
FileNotFoundError and RuntimeError respectively. Other filesystem and argument failures
can raise built-in exceptions such as PermissionError, FileExistsError, OSError, or ValueError.
Links
Metadata
Release files for nominal-iads 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| nominal_iads-0.2.0-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details for nominal_iads-0.2.0-cp311-abi3-win_amd64.whl |
Release files / nominal_iads-0.2.0-cp311-abi3-win_amd64.whl
| Download URL | nominal_iads-0.2.0-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 10.1 MB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
c73a4325d1a2ac016fe5410704f1135777b1ba782c9bf7eb0a2a90fada5061f0
|
|
BLAKE2b-256 checksum How to use checksums |
3b591e2531e131bc8b8b973f1cca715072690d0495a0d35881ab3cedc4335ce0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for nominal-iads 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.