API reference: the size-distribution pipeline
Generated from src/arrayprobe's own AST; never hand-typed. Regenerate with docs/_build/apidoc.py.
Shared tooling, readers and the command line. An earlier translation of UIOOPS v3.1.4 reproduced archived products and is not part of this package; its engine modules are not shipped.
See also CLI reference · Reference.
arrayprobe.jit_kernels
Opt-in accelerated kernels for the particle-image stage – `imgproc_sm(..., jit=True)`.
JitKernels
class JitKernels
The compiled kernels, and the drop-in functions imgproc calls through.
load
def load() -> JitKernels
Import numba and compile (or load from cache) the kernels, once per process.
FALLBACKS
FALLBACKS: dict
arrayprobe.sizing
The size-definition registry – an extensibility seam.
SizeDefinition
class SizeDefinition
One named way of reducing a particle image to a diameter.
Parameters
key: The registry name. Stamped into output provenance, so it is part of the file contract once the framing stage writes it: treat renames as breaking changes. func: The callable. Signatures deliberately differ between definitions – a chord-table definition and a min-enclosing-circle definition do not take the same arguments – so the registry carries no signature contract. phase: `"any", "liquid" or "ice". Anything but "any" is barred from universal_size_definition. description: One line, shown by available_size_definitions. provenance: file:line` in the MATLAB source, or the literature reference. outputs: The names this definition produces, in order.
PhaseValidity
PhaseValidity: object
register_size_definition
def register_size_definition(definition: SizeDefinition) -> SizeDefinition
Add a definition to the registry. Re-registering a key is an error.
get_size_definition
def get_size_definition(key: str) -> SizeDefinition
Look a definition up by name, phase restrictions and all.
This is the accessor that does reach liquid-only definitions – naming one is an explicit act, which is the whole point.
available_size_definitions
def available_size_definitions() -> dict[str, SizeDefinition]
Every registered definition, keyed by name.
universal_size_definition
def universal_size_definition(key: str) -> SizeDefinition
Look up a definition for use on particles of unknown phase.
Raises PhaseMismatchError for anything that declared a phase. A generic pipeline stage – one that does not know what it is looking at – calls this, so a liquid-tuned definition cannot become the default path by accident.
PhaseMismatchError
class PhaseMismatchError(ValueError)
Raised when a phase-restricted definition is asked to serve universally.
Not a MATLAB behaviour – MATLAB has no such check. This is the executable form of the phase-neutrality directive, and it can only refuse a call; it never changes a number.
arrayprobe.attributed_export
The attributed second output – a self-describing sibling of a faithful port product.
EXPORT_CONVENTION
EXPORT_CONVENTION: str
AttributedExportError
class AttributedExportError(RuntimeError)
Base class for every refusal this module raises.
MissingStampError
class MissingStampError(AttributedExportError)
The faithful file carries no `.call` sidecar, so its configuration is unknown.
Refused rather than exported with the provenance block left blank, for exactly the reason `scripts/run_flight.py's UnverifiableProvenance refuses an unstamped input: a file that *looks* attributed but states nothing about how it was made is worse than no file, because a reader cannot tell the difference. accept_unstamped=True overrides, and then the export says so in arrayprobe_call_stamp` rather than staying silent about it.
StampParseError
class StampParseError(AttributedExportError, ValueError)
A `.call line that does not parse as key=value` pairs.
UnknownProductError
class UnknownProductError(AttributedExportError, ValueError)
A netCDF file whose variables match no SHAPES entry.
Refused rather than copied with guessed units. Adding a shape means sourcing each variable's unit from the MATLAB that writes it, at `file:line -- see COMBINED_SIZEDIST`.
VariableAttributes
class VariableAttributes
One variable's attributes, and where each came from.
`units is a udunits-legal spelling of what the value *is*. source_units is the string sizeDist.m itself writes for the variable this one was copied from, kept verbatim where it differs from units -- the string is the thing under suspicion (the same reason arrayprobe_enhancements.cf_export keeps arrayprobe_source_units), and dropping it would hide the discrepancy rather than record it. basis` says how the unit was established: read (quoted from a cited line) or derived (computed from cited lines, with the arithmetic shown).
ProductShape
class ProductShape
One recognised faithful-product shape and its sourced attribute table.
COMBINED_SIZEDIST
(re-exported from elsewhere; not defined in this module)
SHAPES
(re-exported from elsewhere; not defined in this module)
identify_shape
def identify_shape(names: Iterable[str]) -> ProductShape
Which SHAPES entry a file's variable names are, or UnknownProductError.
parse_stamp
def parse_stamp(stamp: str) -> dict[str, str]
A `.call line -> its **top-level** key=value` pairs, in the order written.
Values may contain spaces (`area_ratio_z=0.5 (default)), nested = signs (reject=(rej_hollow=0,...)) and a whole nested stamp (inputs_built_with=(engine=3.1.4 profile=... default_int_arr_thresh=6e-06 (default))), so this is not a split(): each key's value runs to the start of the next key **at paren depth zero**. A duplicated key is refused rather than resolved by a last-wins rule -- sd_call_label's own docstring (src/arrayprobe/driver/run_flight.py:1667-1670) records a doubled engine=` as a real defect class, and silently keeping one of two contradictory values is how a stamp stops meaning anything.
The nested stamp stays nested, and the outer level wins. `engine and profile appear both at the top level and inside inputs_built_with; they are different facts and must not collide. The top-level keys describe *this* product; inputs_built_with is the provenance of the files it was stitched from (run_flight._combine_stamp), so it is returned as one value -- parenthesised, exactly as written; strip the outer () and call this function again if you want the inputs' own fields (parse_nested does that). Promoting the inner keys would either overwrite the outer ones or trip the duplicate guard; both are wrong, and in an earlier version that promotion is exactly what happened: inputs_built_with truncated to '(engine=3.1.4', iat_file/profile/tree were silently the *inputs'* values under the product's own names, and the closing paren landed on the last value, where parse_tagged_value finally raised and --attributed` skipped every real product.
A value that itself contains a space-separated `word= (a Windows path with a space in it, say) would split wrongly here. That is why global_attributes` writes the raw line verbatim as well: the parsed keys are a convenience, the raw line is the record.
parse_reject
def parse_reject(value: str) -> dict[str, str]
`"(rej_hollow=0,max_edge_img_ar=4)" -> {"rej_hollow": "0", ...}`.
The fragment is built off the ~arrayprobe_uiops.reject_core.RejectConfig dataclass's fields (`src/arrayprobe/driver/run_flight.py:1552-1554`, "read off the dataclass, so a field added later enters the stamp without anyone remembering to add it"), so this parses whatever is there rather than a fixed field list – a field added upstream reaches the exported file with no edit here.
parse_tagged_value
def parse_tagged_value(value: str) -> tuple[float, str]
`"0.5 (regime:liquid-warm)" -> (0.5, "regime:liquid-warm")`.
Both halves are returned because both are needed and neither implies the other: the number without the tag cannot be told apart from a value someone typed, and the tag without the number says nothing about what ran (`regimes.ResolvedField`'s own docstring: "three different epistemic objects that must never be blurred").
default_output_path
def default_output_path(source: Path | str) -> Path
`<source><DEFAULT_SUFFIX>` – beside the faithful file, same stem, new suffix.
This is the current layout and may change in a later release (`docs/developer/ATTRIBUTED_EXPORT.md §3). Co-location keeps the correspondence obvious in a directory listing and makes the pair sort adjacently; the .nc ending says NETCDF4 where the faithful archived product says .cdf`; and nothing about the faithful file's own name changes, which matters because every validated comparison addresses it by name.
global_attributes
def global_attributes(stamp: str | None, *, source_path: Path | str, source_sha256: str, source_bytes: int, shape: ProductShape, files_stitched: int | None=None, enhancements: Sequence[str]=(), export_time: str | None=None, enhancements_record: str | None=None, tas_qc: dict | None=None, existing_attributes: Mapping[str, object] | None=None) -> dict[str, object]
Every global attribute the export writes, as a plain dict.
`existing_attributes are the faithful file's own global attributes. Each name in ACDD_KEYS that it holds is carried over verbatim rather than replaced (acdd_attributes`).
Separated from export so the block can be asserted without writing a file, and so a caller assembling a product by another route can write the identical block. The run-level part is shared_attributes, placed here so the set and order of keys is unchanged; the rest (source file and hash, product shape, fidelity note, time and missing-value conventions, the exploded stamp) belongs to this file alone.
`stamp is the raw .call line. It is written **verbatim** as arrayprobe_call_stamp` and exploded into one attribute per key: the verbatim line is the record (nothing can be lost to a parsing edge case), the exploded keys are what a reader's tooling can actually filter on. Any key this module does not recognise is still written, under its own name – so a stamp key added later reaches the file without an edit here.
shared_attributes
def shared_attributes(*, engine: str | None, engine_basis: str | None=None, sd_configuration: str | None=None, enhancements: Sequence[str]=(), enhancements_record: str | None=None, tas_qc: dict | None=None) -> dict[str, object]
The run-level block every product of one run carries, identical in each.
The attributed combined (global_attributes) and the faithful ARM copy of the port's `sd (stamp_shared_attributes) each keep their own arrayprobe_call_stamp and arrayprobe_stage; what they share is this: the engine, the sd-level configuration (arrayprobe_inputs_built_with, the sd stamp in parentheses -- a combined's stamp carries it under that key, the ARM copy reads it from its sd), the enhancements applied and their record, the airspeed QC summary and the package version. engine=None (an unstamped source) and sd_configuration=None` leave those keys out rather than invent them.
stamp_shared_attributes
def stamp_shared_attributes(target: Path | str, sd: Path | str, combined: Path | str | None=None, *, enhancements: Sequence[str]=(), enhancements_record: str | None=None) -> dict[str, object]
Write shared_attributes into `target, a product made from the port's sd`.
`target is opened read-write and only global attributes are set. With combined (the run's combined product, OUTPUT 2's source), the block is the combined's -- its .call and its .tasqc.json, including the merged-parts key the per-sd record lacks -- so OUTPUT 3 carries exactly OUTPUT 2's run-level block. Without it, the block is read from sd. Either source is only read. enhancements and enhancements_record` name the corrections the run applied (a stamp does not), so the enhanced lineage's block is built whole rather than patched afterwards. Returns the block written.
storage_options
def storage_options(dtype: np.dtype, sizes: Sequence[int], *, analysis_ready: bool=True) -> StorageOptions
`createVariable storage keywords for one variable: empty (contiguous, uncompressed) when analysis_ready` is off, and for a variable that cannot be compressed losslessly here – a scalar, a zero-length dimension, or a non-numeric type. Storage only: no keyword returned here changes a value, a dtype or a dimension order.
export
def export(source: Path | str, out: Path | str | None=None, *, stamp: str | None=None, accept_unstamped: bool=False, enhancements: Sequence[str]=(), export_time: str | None=None, enhancements_record: str | None=None, analysis_ready: bool=True) -> Path
Write the attributed sibling of the faithful product `source`. Returns the path written.
`source is opened read-only and is never modified -- asserted by tests/test_attributed_export.py hashing it either side of this call. stamp defaults to the .call sidecar beside it; absent, the export is refused unless accept_unstamped`.
`enhancements names what a non-port producer applied. Empty (the default) writes enhancements_applied = "none"`, which is the port's own case by construction.
`export_time pins the timestamp, for tests that compare two exports byte for byte -- the same reason creation_time=` exists on the faithful writers.
`analysis_ready (default on) writes each variable chunked and losslessly compressed (storage_options), so the file is the cloud-optimised form as well as the attributed one; False writes it contiguous and uncompressed. Either way every value is unchanged, and the file states which under arrayprobe_encoding`.
main
def main(argv: Sequence[str] | None=None) -> int
`python -m arrayprobe.attributed_export FAITHFUL.cdf [OUT.nc] [--zarr] [--parquet]`.
arrayprobe.ai_ready
A machine check that a file is "AI-ready" in exactly the sense this package claims.
No public API declared (__all__ empty or absent).
arrayprobe.lwc
Liquid water content from a particle size distribution.
lwc_calc
(re-exported from elsewhere; not defined in this module)
arrayprobe.compare
Diff-triage: align two UIOOPS output files, then compare them by tolerance class.
ToleranceClass
class ToleranceClass
One of the A-G comparison regimes, and whether a difference in it fails.
A class bundles two things that must travel together: how two values are judged equal, and what a difference means. Splitting them is how a comparison stops meaning anything – a bit-equality assertion on an accumulated float fails for reasons nobody can act on, and a loose tolerance on an integer count throws away the strongest evidence in the file.
`rule is prose because it is written for the person reading a report, not consumed by code; the executable half lives in compare_variables, which switches on key. asserts is the part that decides a verdict: a class with asserts=False is reported and never blocks acceptance (see ComparisonReport.accepted`).
Instances are the values of CLASSES; nothing else constructs one.
CLASSES
(re-exported from elsewhere; not defined in this module)
PROC_CLASSES
(re-exported from elsewhere; not defined in this module)
PROC_CLASSES_V35
(re-exported from elsewhere; not defined in this module)
SD_CLASSES
(re-exported from elsewhere; not defined in this module)
JOIN_KEY
JOIN_KEY: tuple
SD_JOIN_KEY
SD_JOIN_KEY: tuple
JoinResult
class JoinResult
Which rows of two files are the same thing, decided before any value is read.
Produced by join_particles (proc files, joined on the identity tuple JOIN_KEY) or join_seconds (sd files, joined positionally on the `Time index). Everything in compare_variables is indexed through left_index/right_index`, so a bad join makes every subsequent verdict meaningless – which is why the join statistics are reported above the value table rather than below it.
The unmatched rows are kept as index arrays, not counts, so unmatched_runs can distinguish one abandoned record from a systematic framing difference.
VariableVerdict
class VariableVerdict
The comparison outcome for one netCDF variable, in its tolerance class.
`verdict is the coarse answer and detail the class-appropriate evidence behind it -- a drift slope for an accumulator, the worst absolute difference for a closed-form float, a per-value breakdown for a categorical. confusion is populated only for the categorical classes, mapping (left_value, right_value) to how often that substitution happened; for a reject-code or habit-code variable that table is the whole diagnosis, where n_diff` alone would say only that something moved.
Whether the verdict counts against the run is not stored here: it is failed, which asks the variable's ToleranceClass.
ComparisonReport
class ComparisonReport
One whole file-against-file comparison: the alignment, then every variable.
Returned by compare_proc_files, compare_sd_files and compare_files, and rendered for a human by format_report. The ordering of the fields is the ordering of the report on purpose: a reader must see how well the two files aligned, and any `notes` about that alignment, before a single value verdict – a 98 % join makes the value table describe a different population on each side.
accepted is the gate; failures and unclassified are the two ways it can be closed.
join_particles
def join_particles(ds_a, ds_b) -> JoinResult
Align two per-particle files on particle identity.
Returns index arrays into each side. Order is A's order, so a report can be read against the left-hand file.
join_seconds
def join_seconds(ds_a, ds_b) -> tuple[JoinResult, list[str]]
Align two size-distribution files on their `Time` index.
A sd file has no particle identity left in it – `sizeDist reduces every particle to a per-second row -- and, crucially, **the HHMMSS stamp in time does not identify a row.** Measured on an archived 2D-S flight's TAS sidecar: 4,110 rows carry 3,854 distinct stamps, because 256 rows are exact duplicates of their predecessor down to the millisecond (64923.4 twice, and so on). sizeDist` processes such a second twice and advances to the next particle block on the repeat, so the duplication is real in the output too.
So the join is positional: row i of one file is row i of the other, because both were produced from the same TAS vector and `Time is that vector's index. That is not a weaker join than a keyed one here -- it is the only correct one -- and it is checked rather than assumed: time is itself a compared variable in class A, so a positional misalignment shows up as a time` mismatch before anything else is believed.
Returns the join and any notes worth printing above the table.
compare_variables
def compare_variables(ds_a, ds_b, join: JoinResult, classes: dict[str, str] | None=None) -> list[VariableVerdict]
Compare every variable of A against B over the joined particles.
compare_proc_files
def compare_proc_files(path_a, path_b, classes=None) -> ComparisonReport
Join and compare two imgProc per-particle files. Requires `netCDF4`.
compare_sd_files
def compare_sd_files(path_a, path_b, classes=None) -> ComparisonReport
Join and compare two `sizeDist` size-distribution files.
compare_files
def compare_files(path_a, path_b, classes=None) -> ComparisonReport
Compare two UIOOPS files, choosing the join from what they are.
file_kind
def file_kind(path) -> str
`"proc" or "sd"`, from the dimensions rather than the filename.
A filename is not evidence – the standing rule from the decode stage, and the reason this reads the structure instead. A proc file dimensions its particle axis `time (lower case, one row per particle); a sd file dimensions its one-second axis Time alongside CIPcorrlen`.
format_report
def format_report(report: ComparisonReport, verbose: bool=False, accepted: bool | None=None) -> str
Render a report.
`accepted overrides the verdict line. The CLI passes the decision it computed over the WHOLE file, so that filtering the table with --var` cannot turn a failing comparison into a green one.
arrayprobe.timing_closure
The interarrival closure check – McFarquhar et al. (2017) ch. 11 p. 11.20.
ClosureResult
class ClosureResult
The two independent measurements of one file's time span, and their disagreement.
Neither field is a verdict. `ratio` near 1.0 is closer agreement; how far from 1.0 is acceptable is not defined by the source this check implements, and is not defined here either.
check_closure
def check_closure(decoded_path: str, proc_path: str, *, day_rollover_h: float=24.0) -> ClosureResult
Compare a proc file's particle-clock time span against its decoded file's own record-clock time span (McFarquhar et al. 2017, p. 11.20).
`decoded_path is the decode stage's output (.H.nc/.V.nc from engine 3.5) the particles in proc_path were framed from -- the caller is responsible for pairing them correctly; this function does not check that proc_path's parent_rec_num range is consistent with decoded_path`'s record count, though a caller who wants that assurance can read both and compare.
`day_rollover_h corrects a single midnight crossing between the decoded file's first and last record (added once if the raw difference is negative). Two valid hour/minute/second/millisec readings are always less than 24h apart, so the default always suffices; the check that the correction actually fixed the sign exists so a caller who overrides day_rollover_h` to something too small gets a clear error rather than a silently wrong, still-negative span.
arrayprobe.validate_by_condition
Per-second agreement against an archived product, stratified by cloud condition.
BANDS
BANDS: tuple
BandResult
class BandResult
One condition band's totals. Ratios are left to the caller.
`all_* covers every bin; used_* drops the first drop_bins`, which is what the project's headline figures quote – the low bins are where the probe's own response, not the port, dominates the disagreement.
Stratification
class Stratification
One comparison: the bands, the whole-window total, and how it was scoped.
Carries `drop_bins` and both second counts as well as the numbers, because a band table read without them is ambiguous – "used" means nothing until you know how many bins the band drops, and a window's totals read as a whole flight's unless the two second counts are visible side by side.
find_product
def find_product(path: str, product_dir: str | None=DEFAULT_PRODUCTS) -> str | None
The archived product whose stem's date matches `path`'s, if one exists.
Matched on the six-digit date in `base<YYMMDD>, not on the whole stem: a product's stem is the flight's summary-file timestamp and ours is a raw file's, so the two agree on the date and generally not on the time. product_dir falls back to resolve_product_dir`.
stratify
def stratify(ours_path: str, ref_path: str, window: str | None=None, drop_bins: int=5) -> Stratification
Compare `ours_path against ref_path` second by second, by band.
`window is HHMMSS-HHMMSS`, inclusive-exclusive, so a partially processed flight can be compared only where every contributing raw file is present – otherwise its missing hours land in the clear band and inflate the agreement.
arrayprobe.validation_report
Typed results for comparing a port output against a reference product.
No public API declared (__all__ empty or absent).
arrayprobe.diode_report
Per-diode occupancy for one flight, and SPEC's own noisy-diode test.
DiodeReport
class DiodeReport
One flight's per-diode occupancy, and what it implies.
NOT_BUILT
(re-exported from elsewhere; not defined in this module)
SPEC_NOISY_DIODE
SPEC_NOISY_DIODE: SpecNoisyDiodeCriterion
SpecNoisyDiodeCriterion
class SpecNoisyDiodeCriterion
The manufacturer's detector, with each number's provenance attached.
accumulate_occupancy
def accumulate_occupancy(shadows) -> DiodeReport
Count shadowed slices and images per diode over an iterable of silhouettes.
Each element is the boolean `(n_slices, n_diodes) array arrayprobe.render.reconstruct_particle_image` returns. An iterable rather than an array because a flight's particles do not fit in memory at once and a report must not require that they do.
A particle with no slices contributes nothing rather than raising: a proc file's own `position` bounds should prevent it, and a report is the wrong place to discover otherwise.
flag_noisy_diodes
def flag_noisy_diodes(report: DiodeReport, criterion: SpecNoisyDiodeCriterion=SPEC_NOISY_DIODE)
Diodes whose image count exceeds the manufacturer's threshold.
Returns `(flagged_indices_0based, threshold, mean_count)`.
The threshold is computed from the mean over all diodes, which is the whole-flight form of the manual's rolling window: with fewer images than `criterion.window_images` there is only one window, and with more, a whole-flight mean is the conservative reading – a diode noisy for part of the flight is diluted, so this under-reports rather than over-reports. A rolling implementation needs the per-image arrival order, which this accumulation deliberately does not keep; say so rather than implying the manual's exact procedure has been reproduced.
report_from_files
def report_from_files(image_path, proc_path, *, limit: int | None=None, stride: int=1) -> DiodeReport
Build a report straight from a decoded `.cdf and its proc` file.
`stride samples every n-th particle and limit` caps the count: a dense flight holds tens of millions of images, and a uniformity report converges long before all of them. Both are recorded by the caller, not here – a sampled report is still a report, but the sampling is part of the number.
arrayprobe.count_closure
Count closure: do a size-distribution product's binned counts sum to the particles S3 holds?
No public API declared (__all__ empty or absent).
arrayprobe.reproduce_report
Reproduction report: compare a product against an archived product, cell by cell.
No public API declared (__all__ empty or absent).