Sensor catalog: schema, 80 implemented types, failure signatures, and evidence rules¶
Status: design spec, implementation contract. Written for TDD. Every capability named here has a named test or CI gate.
Depends on: UNS topic design (component 2), twin state vector (components 1, 1b, 6a9), LSS engine findings contract (component 5), Sparkplug B milestone (E3), schema registry (C3), config validation (C5).
0. Purpose and thesis¶
The catalog is the deliverable, not the sensor count.
A sensor type is a row of data describing physics rather than a class in a source tree. The generator engine composes a small set of registered parts: a baseline, a coupling expression, a per-device unit offset, a noise family, a drift family, a response model, a sampler, a quantizer, an encoder, and a failure injector. A catalog entry names those parts and supplies parameters. Adding sensor type number 300 means writing one catalog entry and wiring one capability edge; it means touching no Python, no topic strings, and no twin code.
This section specifies the schema, the 80 implemented types, the failure signature taxonomy, the UNS and Sparkplug B mapping, the README extract, the scaling argument, the phase placement, and the test suite that holds all of it honest.
0.1 Four rules that govern every entry¶
Rule 1 and rule 2 are here because they are the most commonly violated rules in simulated telemetry. Rule 3 and rule 4 are here because they are what stop this section from asserting physics it cannot support.
- Support is correct by construction. A quantity bounded to
[0, 1]is generated frombeta_scaled, not from a Normal that is clipped. A duration is generated fromlognormal,gamma, orweibull, not from a Normal with negatives thrown away. A count is generated frompoissonornegbin, not from a rounded Gaussian. An angle is generated from a circular family. Clipping is a bug rather than a safety net: it moves the moments of the distribution and flattens the tail the anomaly layer exists to find.no_clamp_in_samplerindocs/design/variability-and-faults.mdsection F is the gate. - Tails are not truncated. Rare extremes are real. A Weibull duration with shape 0.9 will occasionally return forty times its median. A
gp_tailcargo shock will occasionally return a magnitude far above the trigger threshold. The pipeline must survive those values without clamping, and downstream consumers are tested against them. Physical bounds (relative humidity at or below 100 percent, state of charge in[0, 1], cumulative metal loss monotone non-decreasing) are asserted in the property-based test suite, never enforced by a runtime clamp. - Every published number carries its source. A parameter in a catalog entry is a factual claim about a class of instrument, and this repository is never a reference for itself (D-11). Each entry carries a
provenanceblock naming the publisher, the edition or version, and the locator for every parameter group, plus a confidence tier. Section A.7 defines the tiers and the CI gate. A parameter with no external source cannot ship in an entry markedimplemented. - Randomness is not defined here.
docs/design/variability-and-faults.mdis the single source of truth for the RNG architecture, the stream naming grammar, the distribution catalog, and the fault catalog. This section names families and fault ids from those catalogs and never restates their definitions. Where this section needs a family that catalog does not carry, section A.4 records it as a registration request against that catalog rather than as a local primitive.
0.2 What this section owns and what it does not¶
twinflow-sensors owns the catalog loader and the signal-model registry. D-09 rules that every public symbol has exactly one owning package, and the same discipline applies to the documents: a contract stated twice is a contract that will drift.
| Surface | Owner |
|---|---|
| RNG mechanism, stream names, distribution families, fault ids | docs/design/variability-and-faults.md |
| Event envelope, determinism tiers, ports, packaging | docs/design/DOCTRINE.md |
| Package boundaries, device runtime, edge tiers, Sparkplug codec | docs/design/iot-fleet.md |
| Phase graph and milestone sequencing | docs/design/roadmap.md |
| Entry physics, failure-signature taxonomy, evidence tiers, test plan | this section |
The catalog entry schema itself is claimed by both this section and docs/design/iot-fleet.md section 3.1, and the two claims do not agree on field names, file layout, or the per-category split of the 80 types. That conflict is recorded as OQ-11 with the full field list, because choosing between two designs is a program decision and not a fact this section can settle by assertion.
A. The catalog schema¶
A.1 Layout and file convention¶
catalog/
sensors/
_schema/
sensor-type.schema.json # JSON Schema draft 2020-12, the normative contract
signal-models.md # the registered signal-model kinds and their parameters
industrial_equipment/
eq.vib.band_spectrum.yaml
eq.temp.thermocouple_k.yaml
...
warehouse_logistics/
whs.rfid.portal_read.yaml
...
capabilities/
capabilities.yaml # capability ids, owners, consuming subsystems
provenance/
sources.yaml # one row per cited source: publisher, edition, locator, tier
One file per type. The filename is the type id. The directory is the category, and CI asserts the directory matches the declared category field so the two cannot drift. The layout is one of the surfaces in dispute with docs/design/iot-fleet.md section 3.1, which places one file per category holding a list of entries; OQ-11 records the conflict.
provenance/sources.yaml is a leaf file with no dependency on the catalog. Entries reference a source by id, so one standard cited by nine entries is recorded once and its edition cannot drift between them.
A.2 Annotated example: a continuous, twin-coupled, spectral type¶
This is the fullest form of an entry. Every field in the schema appears here.
# catalog/sensors/industrial_equipment/eq.vib.band_spectrum.yaml
schema_version: "1.0.0" # version of sensor-type.schema.json this entry targets
id: eq.vib.band_spectrum # globally unique, dot-delimited, [a-z0-9_.]+, immutable once published
revision: 1 # bump on any semantic change to this entry; recorded in telemetry
status: implemented # implemented | planned | deprecated
phase: P3 # earliest roadmap phase whose subsystem consumes it; CI checks it exists in ROADMAP.md
name: Order-tracked vibration band spectrum
category: industrial_equipment # one of the eight enumerated categories
summary: >
Band-limited RMS velocity in shaft-order-referenced frequency bands. Emits a fixed
vector of band energies (1x, 2x, 3x, BPFO, BPFI, BSF, FTF, broadband residual) computed
on-device from a high-rate acceleration capture. This is the feature vector a vibration
analyst reads, not the raw waveform.
tags: [rotating_equipment, bearing, predictive_maintenance, tier0_edge_compute]
# ---------------------------------------------------------------- measurement
measurement:
quantity: velocity_rms # controlled vocabulary, used for MSA grouping
unit: mm/s # UCUM code; CI validates against the UCUM table
unit_display: "mm/s RMS"
channel_shape: # scalar | vector | matrix; drives the Sparkplug datatype
kind: vector
length: 8
labels: [ord_1x, ord_2x, ord_3x, bpfo, bpfi, bsf, ftf, broadband_residual]
range_of_interest: [0.0, 45.0] # instrument span, NOT a clamp; values outside are legal and flagged
resolution: 0.01
reference_standard: "ISO 20816-3, velocity RMS, 10 Hz to 1000 Hz band"
limit_source: SRC-ISO-20816-3 # required whenever a numeric alarm limit appears; resolves in sources.yaml
accuracy_spec:
linearity_pct_fs: 1.0
transverse_sensitivity_pct: 5.0
msa: # variance components the LSS engine's Gage R and R study is scored against
repeatability_sigma: 0.021 # equipment variation, one appraiser, repeated measures
reproducibility_sigma: 0.004 # appraiser variation; a fixed-mount sensor has almost none
msa_source: SRC-REPO-MODEL # a declared modeling assumption, not a gauge study; see A.7
# ---------------------------------------------------------------- signal model
signal_model:
# 1. Baseline: what the sensor reads when the asset is healthy.
baseline:
kind: state_function # constant | profile | state_function | event_process | state_machine
expression: |
# Safe, whitelisted expression language. Reads named twin state only.
# Healthy machine: 1x line dominated by residual unbalance, which scales with speed squared.
ord_1x = 0.35 * (shaft_speed_hz / 24.0) ** 2 * (0.6 + 0.4 * load_fraction)
ord_2x = 0.30 * ord_1x * (1.0 + 2.5 * coupling_misalignment)
ord_3x = 0.12 * ord_1x
bpfo = 0.06 + 3.20 * bearing_outer_race_wear ** 1.8
bpfi = 0.05 + 2.60 * bearing_inner_race_wear ** 1.8
bsf = 0.04 + 1.90 * bearing_element_wear ** 1.8
ftf = 0.03 + 0.80 * bearing_cage_wear
broadband_residual= 0.18 + 0.9 * bearing_stage4_fraction + 0.35 * mount_looseness
# 2. Physical coupling: which twin state variables the expression may read.
# Resolved by the state broker at sample time. CI fails if any name is not a
# registered twin state variable with a declared unit.
coupling:
reads:
- motor.shaft_speed_hz as shaft_speed_hz
- motor.load_fraction as load_fraction
- motor.coupling_misalignment as coupling_misalignment
- bearing.outer_race_wear as bearing_outer_race_wear
- bearing.inner_race_wear as bearing_inner_race_wear
- bearing.element_wear as bearing_element_wear
- bearing.cage_wear as bearing_cage_wear
- bearing.stage4_fraction as bearing_stage4_fraction
- mount.looseness as mount_looseness
# Defect-frequency geometry. The band center frequencies are computed from the bearing
# geometry by the standard rolling-element relations, never stored as recalled numbers,
# so a different bearing part number changes a config value and nothing else:
# BPFO = (n/2) * (1 - (d/D) cos(phi))
# BPFI = (n/2) * (1 + (d/D) cos(phi))
# BSF = (D / 2d) * (1 - ((d/D) cos(phi))^2)
# FTF = (1/2) * (1 - (d/D) cos(phi))
# with n rolling elements, ball diameter d, pitch diameter D, contact angle phi, and the
# result expressed as a multiple of shaft rate. For n = 9, d = 7.94 mm, D = 39.04 mm,
# phi = 0 these give 3.585, 5.415, 2.357, and 0.398. The identity BPFO + BPFI = n is
# asserted in CI, so a mistyped geometry cannot pass silently.
derived_constants:
bearing_geometry_ref: 6205_9ball # resolves in catalog/reference/bearings.yaml
band_center_hz: "order_multiplier * shaft_speed_hz"
band_halfwidth_pct: 4.0
# 3. Unit offset: the fixed per-device error drawn once at provisioning and held for the
# life of the device. Without it every device of a type reads identically at zero wear,
# which is the tell that separates simulated telemetry from a real fleet. The stream is
# provision.sensor.<device_id>.unit_offset, per variability-and-faults A.2.
unit_offset:
family: normal # named in the variability-and-faults distribution catalog
params: { sigma: 0.018 }
mode: multiplicative
# 4. Noise: measurement noise on top of the baseline. Support must match the quantity.
# Every family below is a row in the variability-and-faults distribution catalog.
noise:
- applies_to: [ord_1x, ord_2x, ord_3x, bpfo, bpfi, bsf, ftf]
family: lognormal # band RMS is strictly positive; multiplicative error is the physical form
params: { median: 1.0, sigma_log: 0.11 }
mode: multiplicative
- applies_to: [broadband_residual]
family: gamma
params: { shape: 14.0, mean_from_baseline: true }
- applies_to: all
family: ar1 # the catalog's colored-noise family, standing in for amplifier flicker
params: { phi: 0.94, sigma: 0.004 }
# 5. Drift: slow, non-stationary error that is NOT the asset degrading.
drift:
- family: ou # mean-reverting sensitivity error from mount preload relaxation
applies_to: all
params: { mean: 0.0, reversion_per_day: 0.09, sigma_per_sqrt_day: 0.006 }
mode: multiplicative
- family: arrhenius_accelerated # REGISTRATION REQUEST against the distribution catalog;
# see A.4, "families this section needs that the catalog does not yet carry"
applies_to: all
params:
activation_energy_ev: 0.65
reference_temp_c: 25.0
rate_at_reference_per_year: -0.012
temp_source: eq.temp.rtd_pt100@same_equipment
# 6. Sampling.
sampling:
mode: periodic # periodic | on_change | on_event | polled | burst
interval_s: 60
jitter:
family: gamma
params: { shape: 3.0, mean_s: 0.35 } # positive-support jitter; never a symmetric Normal
phase_offset_policy: hash_of_device_id # avoids a thundering herd of 500 devices on the same second
aggregation_window_s: 1.0 # on-device capture length feeding the band computation
edge_compute_tier: 0 # E36: FFT and banding run on the device
raw_capture:
available: true
sample_rate_hz: 25600
duration_s: 1.0
publish_policy: on_demand_or_on_alarm # never streamed; see open question OQ-5
# 7. Quantization and transport-level effects applied after noise and drift.
quantization:
adc_bits: 16
full_scale: 50.0
dither: true
response:
kind: none # this channel has no thermal lag; see thermocouple entry
# 8. RNG streams this entry draws from. Names follow the grammar in
# variability-and-faults A.2 and are content addressed, so adding a channel to this
# entry cannot shift any other entry's draws.
rng_streams:
- "provision.sensor.{device_id}.unit_offset"
- "sensor.{type_id}.{device_id}.noise"
- "sensor.{type_id}.{device_id}.drift"
- "sensor.{type_id}.{device_id}.jitter"
# ---------------------------------------------------------------- failure modes
failure_modes:
# Universal modes are inherited from the class profile and may be overridden here.
inherits_profile:
continuous_analog # pulls in drift, stuck_at, dropout, calibration_loss, bias_step,
# noise_inflation, saturation, clock_skew, crash_loop
overrides:
- id: calibration_loss
catalog_id: F-DEV-CALIB # resolves in the fault catalog, variability-and-faults C.4
onset:
family: weibull # time-to-onset; shape < 1 gives infant mortality, > 1 gives wear-out
params: { shape: 1.6, scale_days: 900 }
effect:
{
kind: gain_error,
gain_distribution:
{ family: beta_scaled, params: { lo: 0.55, hi: 1.0, mean: 0.86, conc: 8 } },
}
detectable_by: [msa_stability_study, spc_ewma_on_residual]
expected_mttd_hours: 72
severity: 6 # FMEA S rating, 1 to 10, engineering judgement, recorded not guessed
pf_interval_hours: 720 # P-F interval; this is what grounds the FMEA D rating
specific:
- id: mount_looseness
catalog_id: F-DEV-DRIFT
description: >
Loose sensor mount or a degraded magnetic base. Raises broadband_residual and adds
a smeared harmonic series, but does NOT raise the defect-frequency bands. That
asymmetry is how an analyst separates a sensor problem from a bearing problem.
onset: { family: weibull, params: { shape: 2.2, scale_days: 1400 } }
effect:
kind: expression_override
expression: |
broadband_residual = broadband_residual * (1.0 + 2.8 * severity_fraction)
ord_2x = ord_2x * (1.0 + 0.9 * severity_fraction)
# severity_fraction, not severity: the FMEA S rating above is an integer 1 to 10 and
# this is a bounded progression variable. One term per concept.
severity_fraction:
{ family: beta_scaled, params: { lo: 0.0, hi: 1.0, mean: 0.4, conc: 5 } }
detectable_by: [fleet_health_signature_classifier, pdm_trend_layer]
expected_mttd_hours: 24
severity: 5
pf_interval_hours: 336
confusable_with: [bearing_outer_race_defect] # drives the MSA and classifier test cases
- id: speed_reference_loss
catalog_id: F-DEV-STUCK
description: >
The tachometer or encoder reference disappears, so order tracking falls back to a
fixed assumed speed. Bands drift off the true defect frequencies and every defect
band collapses toward the noise floor. Reads as a sudden improvement,
which is the trap: the machine looks healthier the moment the measurement breaks.
onset: { family: exponential, params: { mean_days: 1111 } }
effect: { kind: band_decoherence, residual_fraction: 0.15 }
detectable_by:
[cross_sensor_consistency, fleet_health_signature_classifier]
expected_mttd_hours: 6
severity: 8
pf_interval_hours: 24
severity_floor: major
- id: cable_microphonics
catalog_id: F-DEV-NOISEUP
description: >
Triboelectric noise from a coax cable rubbing on a moving guard. Injects
low-frequency bursts correlated with machine motion, not with bearing condition.
onset: { family: weibull, params: { shape: 1.1, scale_days: 600 } }
effect:
{
kind: burst_noise,
arrival:
{
family: hawkes, # REGISTRATION REQUEST; see A.4
params: { mu_per_hour: 0.4, alpha: 0.6, decay_per_hour: 3.0 },
},
}
detectable_by: [spc_western_electric, alarm_rationalization]
expected_mttd_hours: 48
severity: 4
pf_interval_hours: 168
# ------------------------------------------------------- whole-device faults
# Modes that belong to the device rather than to any one channel. Every id resolves in the
# fault catalog, and the device runtime owns the injection point (iot-fleet section 5).
device_faults: [F-DEV-DROP, F-DEV-CLOCKDRIFT, F-DEV-CRASHLOOP, F-DEV-PROVISION]
# ---------------------------------------------------------------- attachment
attaches_to:
subsystem: conveyor_drive # primary twin subsystem key
also_serves: [palletizer_cell, asrs_crane, factory_mixer_drive]
binding:
per_equipment # per_site | per_area | per_line | per_equipment | per_asset_instance
# | per_vehicle | per_lot | per_worker
cardinality:
default_per_binding: 1
max_per_binding: 3 # drive end, non-drive end, gearbox
instance_naming: "vib-{equipment_id}-{position}"
positions: [de, nde, gearbox] # optional sub-position vocabulary, appended to the parameter level
# ---------------------------------------------------------------- UNS mapping
# Telemetry is published through the Network port, the MQTT-shaped one, because it needs
# retain, QoS, last will, and wildcard subscribe. It never uses EventBus (D-08).
uns:
topic_template: "{enterprise}/{site}/{area}/{line}/{equipment}/{parameter}"
parameter: "vibration/band_spectrum/{position}"
qos: 1
retain: false
payload_encoding: sparkplug_b # sparkplug_b | json_fallback
legacy_json_topic_suffix: "/json" # published only when the json_fallback profile is enabled
# ---------------------------------------------------------------- Sparkplug B
sparkplug:
metric_name: "{line}/{equipment}/vibration/band_spectrum/{position}"
datatype: FloatArray # enum 30; vector-of-8 float. Scalar types use Float (9) or Double (10)
element_datatype: Float
alias_policy: deterministic_at_birth # see section D.3
is_transient: false
is_historical_on_replay: true
properties:
engUnit: "mm/s"
engLow: 0.0
engHigh: 45.0
description: "ISO 20816-3 band RMS velocity, order tracked"
Quality: 192 # 192 is GOOD; 0 is BAD and 500 is STALE, per the Sparkplug specification
sensor_type_id: "eq.vib.band_spectrum" # the catalog type. Distinct from failure_modes[].catalog_id,
# which names a fault. One term per concept.
sensor_type_revision: 1
band_labels: "ord_1x,ord_2x,ord_3x,bpfo,bpfi,bsf,ftf,broadband_residual"
birth_defaults:
publish_on_dbirth: true
initial_value_policy: first_real_sample # never a fabricated zero
# ---------------------------------------------------------------- capability wiring
capability:
unlocks:
- pdm.bearing_defect_detection
- pdm.time_to_threshold_estimate
- lss.spc_on_vibration_bands
- energy.mechanical_loss_attribution
consumed_by: # CI cross-checks these against capabilities.yaml
- predictive_maintenance.trend_engine
- lss_engine.spc
- cmms_queue.work_order_generator
rationale: >
Broadband RMS alone cannot distinguish a bearing defect from unbalance or looseness.
Banding at the defect frequencies is what makes "motor 2 bearing trend crosses the
alarm limit in about 9 days" a defensible statement rather than a guess.
# ---------------------------------------------------------------- validation
validation:
physical_bounds: # property-test assertions; NEVER runtime clamps
- "all(v >= 0.0 for v in value)"
- "broadband_residual <= 8.0 * mean(ord_1x, ord_2x, ord_3x) or bearing_stage4_fraction > 0.6"
monotonicity: []
goodness_of_fit:
- channel: ord_1x
holding: { shaft_speed_hz: 24.0, load_fraction: 0.7, all_wear: 0.0 }
declared: { family: lognormal, sigma_log: 0.11 }
test: anderson_darling
n_samples: 200000
alpha: 0.01
- channel: bpfo
holding: { bearing_outer_race_wear: 0.35 }
assertion: "spectral_energy_fraction_in_band(bpfo_center, halfwidth=4%) >= 0.80"
cross_checks:
- "eq.vib.velocity_rms on the same equipment must equal quadrature sum of bands within 12 percent"
# Two determinism tiers, not one (D-05). Byte identity is claimed only on a pinned
# platform; across platforms the claim is value equivalence within a measured tolerance,
# and the cross-platform job reports the observed maximum divergence.
determinism:
same_platform: byte_identical
cross_platform: value_equivalent
cross_platform_tolerance_rel: 1.0e-9 # provisional; replaced by the measured figure at P3
budget:
max_generator_cost_us_per_sample: 900
max_payload_bytes: 96
# ---------------------------------------------------------------- provenance
# One row per parameter group. Every id resolves in catalog/provenance/sources.yaml, which
# carries publisher, title, edition or version, locator, retrieval date, and tier (A.7).
provenance:
- covers: [measurement.reference_standard, measurement.range_of_interest, validation.cross_checks]
source: SRC-ISO-20816-3
tier: C # named standard, body paywalled; the zone values are read at implementation time
- covers: [signal_model.coupling.derived_constants]
source: SRC-DERIVED-BEARING-GEOMETRY
tier: A # arithmetic from the stated relations and the stated geometry, checkable in place
- covers: [signal_model.noise, signal_model.drift, signal_model.unit_offset, msa]
source: SRC-REPO-MODEL
tier: D # a modeling assumption with no external source; blocks status: implemented
The last row is the one that matters. It is tier D, so CI refuses status: implemented on this entry until either an external source is found for the noise and drift parameters or those parameters are restated as a derivation from something that has one. That is rule 3 of section 0.1 with teeth, and it is why A.7 exists.
A.3 Two shorter forms, for shape variety¶
Both forms below omit the provenance, msa, rng_streams, and determinism blocks, which are required and identical in shape to A.2. They are dropped here so the structural difference stands out.
A.3.1 Probabilistic event type¶
The RFID portal read, which is not a periodic sample at all.
id: whs.rfid.portal_read
category: warehouse_logistics
phase: P1 # the walking skeleton instantiates one portal
measurement:
quantity: read_event
unit: "1" # dimensionless event; per-read payload carries RSSI in dBm
channel_shape: { kind: template } # Sparkplug Template: epc, antenna_port, rssi_dbm, phase_rad, read_count
signal_model:
baseline:
kind: event_process
expression: |
# Forward-link-limited Friis. Read probability PER INVENTORY ROUND, per tag.
p_orient = clamp01(cos_sq(tag_angle_to_antenna) * polarization_match)
p_range = sigmoid((eirp_dbm + tag_sensitivity_dbm - path_loss_db(range_m)) / 2.2)
p_material = material_detune_factor # per material class; values carry provenance
p_collide = 1.0 / (1.0 + population_in_field / q_slots)
p_round = p_orient * p_range * p_material * p_collide * (1.0 - vfd_interference_loss)
rounds = floor(dwell_s * reader_rounds_per_s) # dwell = portal_depth_m / pallet_speed_mps
# Reads per tag over the pass:
reads = betabinom(rounds, mean_p=p_round, conc=tag_conc)
coupling:
reads:
- conveyor.pallet_speed_mps as pallet_speed_mps
- pallet.tag_population as population_in_field
- pallet.material_class as material_class
- portal.eirp_dbm as eirp_dbm
- portal.antenna_vswr as antenna_vswr
- line.vfd_emission_index as vfd_interference_loss
# Regulatory ceiling on the forward link, so eirp_dbm cannot be set to a value no real
# portal is allowed to radiate. 47 CFR 15.247 covers the 902-928 MHz band, caps digital
# modulation at 1 W conducted, and permits an antenna of up to 6 dBi directional gain with
# no power reduction, which is 36 dBm EIRP.
constraints:
- "eirp_dbm <= 36.0" # source SRC-CFR-47-15-247
noise:
- applies_to: [rssi_dbm]
family: rician # REGISTRATION REQUEST; the catalog carries rayleigh, not rician. See A.4
params: { k_factor_db: 6.0, sigma_db: 3.2 }
- applies_to: [tag_angle_to_antenna]
family: von_mises # REGISTRATION REQUEST; angles need a circular family, not a Normal
params: { kappa: 2.5 }
sampling:
mode: on_event
trigger: "pallet enters read zone geometry"
failure_modes:
inherits_profile: probabilistic_event # dropout, duplicate_read, clock_skew, crash_loop, degraded_read_rate
specific:
- id: antenna_vswr_rise
catalog_id: F-DEV-READRATE
description: >
A corroding N-connector or a crushed coax raises VSWR. The reflected power fraction
is the square of the reflection coefficient (S-1)/(S+1), which is 1/9 at S = 2 and
1/4 at S = 3. Range for a forward-link-limited system scales with the square root of
radiated power, so S = 3 leaves sqrt(0.75) of the range, a loss of 13.4 percent. The
signature is a read-rate decline on ONE antenna port while its siblings hold, which
is how it is told apart from a tag or product problem.
onset: { family: weibull, params: { shape: 2.4, scale_days: 1100 } }
effect:
{
kind: parameter_ramp,
target: portal.antenna_vswr,
to: 3.2,
ramp_days: 40,
}
detectable_by:
[fleet_health_read_rate_control_chart, rf_physics_diagnostic]
expected_mttd_hours: 96
severity: 5
pf_interval_hours: 960
- id: cross_read
catalog_id: F-DEV-GHOST
description: >
A tag on the adjacent dock is caught by this portal. A FALSE POSITIVE, not a miss.
Generates a read event on the wrong equipment topic, which the receiving
reconciliation logic must reject via ASN cross-check or dwell-time plausibility.
onset: { family: always_on }
effect:
{
kind: spurious_event,
rate:
{ family: poisson, params: { rate_per_adjacent_pass: 0.035 } },
}
detectable_by: [erp_asn_reconciliation, cv_count_cross_check]
expected_mttd_hours: 2
severity: 7
pf_interval_hours: 4
- id: tag_detune
catalog_id: F-DEV-READRATE
description: >
Liquid or metal load shifts the tag antenna resonance off the 902 to 928 MHz band
that 47 CFR 15.247 allocates for this service, so the tag stops matching the reader.
onset: { family: bernoulli, params: { p: 0.06 }, per: pallet }
effect:
{
kind: parameter_set,
target: material_detune_factor,
value_distribution:
{ family: beta_scaled, params: { lo: 0.0, hi: 1.0, mean: 0.19, conc: 7.4 } },
}
detectable_by: [read_rate_by_sku_class, cv_count_cross_check]
severity: 4
pf_interval_hours: 24
device_faults: [F-DEV-DROP, F-DEV-DUPREAD, F-DEV-CLOCKDRIFT, F-DEV-CRASHLOOP]
A.3.2 Discrete state type¶
The e-stop, which is a state machine and not a sampled scalar.
id: saf.estop.circuit
category: safety_compliance
phase: 6a10 # the safety category lands with component 6a10
measurement:
quantity: safety_circuit_state
unit: "1"
channel_shape: { kind: scalar }
state_vocabulary:
[armed, tripped, fault_channel_a, fault_channel_b, cross_fault]
signal_model:
baseline:
kind: state_machine
initial: armed
transitions:
- {
from: armed,
to: tripped,
trigger: twin_event,
event: safety.estop_pressed,
}
- {
from: tripped,
to: armed,
trigger: twin_event,
event: safety.reset_pressed,
guard: "guard_closed and no_worker_in_zone",
}
dwell_time: # durations use positive-support families, always
tripped:
{ family: lognormal, params: { median_s: 145, sigma_log: 0.85 } }
noise: [] # a discrete safety circuit has no measurement noise; it has faults
unit_offset: { family: none, why: "a state has no offset to draw" }
sampling:
mode: on_change
heartbeat_s: 30 # a safety input republishes even when unchanged
failure_modes:
inherits_profile: discrete_state # stuck_at, dropout, clock_skew, crash_loop, chatter
specific:
- id: welded_contact
catalog_id: F-DEV-STUCK
description: >
A contact welds closed and the circuit reports armed forever. The dangerous
failure, because it is silent: the reading is the safe-looking one. Detected only
by proof testing or by the absence of expected state changes over a window.
onset: { family: weibull, params: { shape: 1.8, scale_days: 3200 } }
effect: { kind: stuck_at, value: armed }
detectable_by: [absence_of_expected_transition, scheduled_proof_test]
expected_mttd_hours: 720
severity: 10
pf_interval_hours: 2160 # the proof-test interval is the only thing that bounds this
severity_floor: critical
- id: contact_bounce
catalog_id: F-DEV-NOISEUP
description: >
Contact wear produces a burst of state changes faster than any physical actuation.
onset: { family: weibull, params: { shape: 1.4, scale_days: 2000 } }
effect:
{
kind: chatter,
burst_len: { family: poisson, params: { rate: 4 } },
interval_ms: 12,
}
detectable_by: [alarm_rationalization_debounce]
expected_mttd_hours: 1
severity: 3
pf_interval_hours: 8
device_faults: [F-DEV-DROP, F-DEV-CLOCKDRIFT, F-DEV-CRASHLOOP]
A.4 Field-by-field reference¶
A.4.1 Identity block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
schema_version |
semver string | yes | Version of sensor-type.schema.json this entry targets. Loader refuses a major mismatch. |
id |
string | yes | Globally unique, [a-z0-9_.]+, dot-delimited category_prefix.family.specific. Immutable once published. A breaking change is published as a new id, and the superseded id carries a deprecated marker. |
revision |
integer | yes | Increments on any semantic change. Stamped into every published payload as the sensor_type_revision Sparkplug property so a historian row always knows which model produced it. |
status |
enum | yes | implemented, planned, deprecated. Only implemented entries instantiate devices, and A.7 blocks that status while any parameter group is tier D. |
phase |
enum | yes | Earliest roadmap phase whose subsystem consumes the type. CI checks the value exists in ROADMAP.md. Section H holds the assignment. |
name |
string | yes | Human name for the README table and the dashboard. |
category |
enum | yes | One of the eight categories. Must equal the containing directory. |
summary |
string | yes | Two to four sentences. What it measures and what makes the model non-trivial. |
tags |
string list | no | Free-form facets for filtering (rotating_equipment, cold_chain, wearable, tier0_edge_compute). |
A.4.2 Measurement block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
measurement.quantity |
enum | yes | Controlled vocabulary (temperature, velocity_rms, mass, read_event, ...). Groups types for measurement system analysis so a Gage R&R can compare two technologies measuring the same quantity. |
measurement.unit |
UCUM code | yes | Machine-checkable. CI validates against the UCUM table and rejects free text. |
measurement.unit_display |
string | no | What the dashboard prints. |
measurement.channel_shape |
object | yes | scalar, vector with length and labels, matrix with rows/cols, or template with a named field list. Drives the Sparkplug datatype selection. |
measurement.range_of_interest |
[lo, hi] |
yes | Instrument span. Documentation and dashboard scaling only. Not a clamp. Out-of-span values are legal, published, and flagged by the plausibility validator. |
measurement.resolution |
number | no | Smallest meaningful increment before quantization is applied. |
measurement.reference_standard |
string | no | The standard the reading is referenced to (ISO 20816-3, ISO 4406, ASTM E230). Names the standard; it does not reproduce values from it. |
measurement.limit_source |
source id | cond. | Required whenever the entry carries a numeric alarm, warn, or trip limit. Resolves in provenance/sources.yaml. A limit with no source fails the build. |
measurement.accuracy_spec |
object | no | Datasheet-class error terms carried for the MSA layer to consume as a declared truth. Governed by the entry's provenance block. |
measurement.msa |
object | yes | repeatability_sigma and reproducibility_sigma, or an msa_note excluding the channel. These are the declared variance components the LSS engine's Gage R and R study is scored against. |
A.4.3 Signal model block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
signal_model.baseline.kind |
enum | yes | constant, profile (a named time profile: diurnal, shift-pattern, seasonal), state_function (expression over twin state), event_process (arrivals), state_machine (discrete states with dwell times). |
signal_model.baseline.expression |
expr | conditional | Whitelisted expression language: arithmetic, min/max/clamp01/exp/log/sqrt/sigmoid/cos_sq, named distribution draws, and registered twin state names only. No attribute access, no imports, no I/O. Parsed to an AST and statically checked at load. |
signal_model.baseline.profile |
object | conditional | For kind: profile: a named profile plus amplitude and phase. Diurnal profiles use sim-time, never wall-clock. |
signal_model.coupling.reads |
list | conditional | twin.state.path as local_name. CI fails if the path is not a registered twin state variable with a declared unit, and fails if the expression references a name not declared here. This is the field that stops a decorative sensor: no coupling means no twin linkage, and the entry then has to justify itself in capability.rationale. |
signal_model.coupling.derived_constants |
object | no | Physics constants resolved from a reference table (bearing geometry, pipe diameter, tank cross-section) so the same catalog entry serves different equipment by changing a reference key. |
signal_model.unit_offset |
object | yes | The fixed per-device error drawn once at provisioning from provision.sensor.{device_id}.unit_offset and held for the device's life. {family: none, why: ...} is the only way to omit it, so a fleet of identical readings is always a decision. |
signal_model.noise |
list | yes | Each element names a family from the distribution catalog in docs/design/variability-and-faults.md section B, an applies_to channel list, and params. May be empty only for discrete state types. |
signal_model.drift |
list | yes | Slow, non-stationary sensor error that is not asset degradation. May be an empty list, but the field must be present so the omission is a decision, not an oversight. |
signal_model.sampling |
object | yes | mode, interval_s or trigger, jitter distribution (positive support only), phase offset policy, aggregation window, edge compute tier. |
signal_model.quantization |
object | no | ADC bits, full scale, dither. Applied after noise and drift, in that order, matching the real signal chain. |
signal_model.response |
object | no | Sensor dynamics. first_order_lag with tau_s, second_order with wn/zeta, or none. A thermocouple, a pH electrode, and a fuel level float all need this and getting it wrong is the single most common tell in fake telemetry. |
signal_model.rng_streams |
list | yes | Every stream name the entry draws from, in the grammar of docs/design/variability-and-faults.md section A.2. Content addressed, so adding a channel to one entry cannot shift another entry's draws. CI checks each name against the stream registry. |
A.4.4 Distribution families and where they are defined¶
docs/design/variability-and-faults.md section B is the single source of truth for the distribution catalog. This section names families from it and never redefines them. The mapping below exists because the same physical need is described in two vocabularies, and one term per concept means the catalog's name wins.
| Family in the distribution catalog | Support | What the sensor catalog uses it for |
|---|---|---|
normal |
real | Johnson and amplifier thermal noise on a bipolar channel; the per-device unit_offset |
lognormal |
positive | Any strictly positive amplitude with proportional error; response and travel durations |
shifted_lognormal |
positive above shift | A duration with a physical floor, such as a door travel time |
gamma |
positive | Positive energies and sampling jitter |
weibull |
positive | Time-to-onset for wear-out and infant-mortality failures |
beta_scaled |
[lo, hi] |
Bounded ratios: state of charge, relative humidity, read rate, water activity |
poisson |
non-negative integer | Counts of events in a window |
negbin |
non-negative integer | Over-dispersed counts, such as particle counts and scan no-reads |
betabinom |
{0..n} |
Over-dispersed bounded counts, such as RFID reads over inventory rounds |
bernoulli |
{0, 1} |
Per-trial success, such as one barcode presentation |
exponential |
positive | Memoryless inter-arrival, and the strictly positive non-line-of-sight range bias |
rayleigh |
positive | Scattered multipath fading on RSSI where no dominant path exists |
gp_tail |
positive tail | Extreme shock, extreme demand, extreme transit delay. Never truncated. |
ar1 |
real, correlated | The catalog's colored-noise family, standing in for flicker and MEMS bias instability |
wiener |
real, correlated | Unbounded integrator drift, such as coulomb counting error and gyro bias |
ou |
real, correlated | Mean-reverting drift, such as mount preload relaxation and thermal offset |
semi_markov |
discrete states | State machines with per-state holding distributions |
nhpp |
event times | Arrival rates that follow a shift or diurnal profile |
hierarchical |
inherited | A per-device effect drawn once with per-event noise drawn on top |
Six families this section needs are not in that catalog. They are registration requests against it, not local primitives, and each carries the entry that forced it. Until a family is registered there, an entry naming it cannot reach status: implemented.
| Requested family | Support | Forced by |
|---|---|---|
von_mises |
angle | Tag orientation, load tilt heading, shaft phase: a Normal cannot wrap |
rician |
positive | RSSI fading at a portal, where a dominant path exists and Rayleigh is wrong |
hawkes |
event times | Self-exciting clustered arrivals: acoustic emission bursts, near-misses |
student_t |
real | Measurement error where outliers are physical rather than contamination |
arrhenius_accelerated |
monotone | Temperature-accelerated ageing: piezo sensitivity loss, electrolyte dry-out |
linear_ramp |
monotone | Slow leaks, cumulative metal loss, battery capacity fade |
Registering a family is the one change that needs code. Section F sets out why the set is close to closed.
A.4.5 Failure modes block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
failure_modes.inherits_profile |
enum | yes | One of the class profiles in section C.1. Pulls in the universal modes with class-appropriate defaults so no entry re-specifies drift and dropout. |
failure_modes.overrides |
list | no | Per-entry parameter changes to inherited modes. |
failure_modes.specific |
list | yes | Class-specific modes. Each carries id, catalog_id, description, onset, effect, detectable_by, expected_mttd_hours, severity, pf_interval_hours, and optional severity_floor and confusable_with. |
...catalog_id |
id | yes | The fault id in docs/design/variability-and-faults.md section C.4. CI checks it resolves. A mode with no fault id has no injector and cannot be tested. |
...onset |
object | yes | A time-to-onset family from the distribution catalog, or always_on, or bernoulli with a per scope naming the trial (pallet, pass, shift). |
...effect.kind |
enum | yes | gain_error, offset_bias, stuck_at, dropout, noise_inflation, parameter_ramp, parameter_set, expression_override, spurious_event, band_decoherence, burst_noise, chatter, saturation, response_slowdown. |
...effect.severity_fraction |
object | cond. | A bounded progression variable on [0, 1] that an expression_override reads. Named apart from severity because they are different quantities. |
...detectable_by |
id list | yes | Which analysis layer is expected to catch it. CI asserts each id is a registered detector, and the failure-injection test suite asserts that detector fires within expected_mttd_hours. |
...expected_mttd_hours |
number | yes | The mean-time-to-detect budget. This is the number the fleet-health layer is scored against, so it is a commitment, not a comment. |
...severity |
integer | yes | FMEA severity rating, 1 to 10. Engineering judgement, recorded so it can be argued with rather than left implicit. |
...pf_interval_hours |
number | yes | The potential-failure to functional-failure interval. This is what grounds the FMEA detection rating, and a detection budget longer than this interval is a defect the fleet-health layer reports. |
...severity_floor |
enum | no | Minimum finding severity. Safety class entries set this so a safety finding can never be ranked below a throughput finding. |
...confusable_with |
id list | no | Modes with a similar surface signature. Generates the hard cases for the signature classifier test set. |
device_faults |
id list | yes | Whole-device modes that belong to no single channel: link loss, clock drift, crash loop, misprovisioning. Each id resolves in the fault catalog and the device runtime owns the injection point. |
A.4.6 Attachment block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
attaches_to.subsystem |
id | yes | Primary twin subsystem key. Must exist in the twin's subsystem registry. |
attaches_to.also_serves |
id list | no | Other subsystems that may instantiate this type. |
attaches_to.binding |
enum | yes | The ISA-95 level (or non-ISA-95 asset class) an instance attaches to. Determines which topic levels are filled and which are literal. |
attaches_to.cardinality |
object | yes | Default and maximum instances per binding, so facility.yaml cannot silently over-instrument. |
attaches_to.instance_naming |
template | yes | Device id template. Must resolve to a unique string given the binding. |
attaches_to.positions |
string list | no | Sub-position vocabulary appended to the parameter level (drive end, non-drive end, per-phase, per-wheel). |
A.4.7 UNS and Sparkplug blocks¶
Specified in section D.
A.4.8 Capability block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
capability.unlocks |
id list | yes | Capability ids this type feeds. Must be non-empty. |
capability.consumed_by |
id list | yes | Subsystems that declare a dependency on those capabilities. Must be non-empty. This is the anti-orphan field. |
capability.rationale |
string | yes | Why this type exists rather than a cheaper one. Reviewed at PR time; this is where padding gets caught. |
A.4.9 Validation block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
validation.physical_bounds |
expr list | yes | Assertions evaluated by the property test suite over large sample counts. Never evaluated at runtime, never used to clamp. |
validation.monotonicity |
channel list | yes | Channels that must be monotone non-decreasing across a run (kWh totalizers, cumulative metal loss, odometer, engine hours). Empty list is allowed and explicit. |
validation.goodness_of_fit |
list | yes | Per-channel declared family plus the conditioning state to hold fixed, the test to run, sample count, and alpha. Section G describes the multiple-comparison handling. |
validation.cross_checks |
string list | no | Relationships to other catalog entries (redundant measurement pairs, derived-quantity consistency). |
validation.determinism |
object | yes | Two tiers, per D-05. same_platform: byte_identical and cross_platform: value_equivalent with a tolerance. The cross-platform job reports observed divergence. |
validation.budget |
object | yes | Per-sample generator cost ceiling and payload byte ceiling, so the A4 scaling curves stay honest. |
A.4.10 Provenance block¶
| Field | Type | Required | Meaning and rules |
|---|---|---|---|
provenance[].covers |
path list | yes | The dotted field paths this row accounts for. Every numeric parameter in the entry is covered by exactly one row, and CI reports the ones that are not. |
provenance[].source |
source id | yes | Resolves in catalog/provenance/sources.yaml, which carries publisher, title, edition or version, locator, and retrieval date. |
provenance[].tier |
enum | yes | A, B, C, or D, defined in A.7. Tier D blocks status: implemented. |
provenance[].note |
string | no | Why this source and not a closer one, or what was tried and blocked. |
A.5 How a new type is added¶
- Copy
catalog/sensors/_schema/TEMPLATE.yamlinto the right category directory, named for the new id. - Fill the entry. If the physics needs a distribution family that
docs/design/variability-and-faults.mdsection B does not carry, that is the one case that requires code: register the family there with its own unit tests, then name it here. - If the entry couples to a twin state variable that does not exist, register it in the twin state schema first. A coupling to a non-existent variable fails CI, which is deliberate: it forces the sensor and the thing it measures to be added together.
- Add or reference a capability id in
capabilities/capabilities.yaml, and declare at least one consuming subsystem. - Record every numeric parameter group in the
provenanceblock, adding rows toprovenance/sources.yamlfor any source not already cited. An entry whose parameters are all tier D staysplanned. - Set
phaseto the earliest roadmap phase whose subsystem consumes the type, and check that phase exists in ROADMAP.md. - Run
just catalog validatelocally, thenjust catalog fit --id <new_id>to run the goodness-of-fit suite for just that entry. - Reference the new type from a facility profile (
facility.yaml) when it must instantiate in a shipped profile. A type can landimplementedwithout being instantiated in any shipped profile, but the load test corpus must exercise it.
A.6 What CI validates about a new type¶
| Check | Failure mode it prevents |
|---|---|
JSON Schema validation, additionalProperties: false |
Typo'd field names silently ignored |
Filename equals id, directory equals category |
Catalog drift between filesystem and content |
id uniqueness across the whole catalog |
Two types publishing to the same metric |
| UCUM unit code resolves | Free-text units that no consumer can convert |
| Every expression parses, and every referenced name is either a declared local or a declared coupling read | Silent NameError at simulation time; hidden dependencies |
| Every coupling read resolves to a registered twin state variable with a compatible unit | Sensors measuring things the twin does not simulate |
Every family resolves in the distribution catalog, and its support is contained in the channel's declared bounds |
A Normal on a [0, 1] quantity |
Every rng_streams name parses under the stream grammar and is registered |
An unregistered stream, which breaks reproducibility |
Every failure_modes[].catalog_id and every device_faults id resolves in the fault catalog |
A failure mode with no injector, so it cannot be tested |
Every detectable_by id resolves to a registered detector |
A failure mode nobody is responsible for catching |
pf_interval_hours is present and expected_mttd_hours does not exceed it |
A detection budget that admits failure before detection |
capability.unlocks non-empty, resolves, and each capability has at least one declaring consumer |
Orphan sensors and orphan capabilities |
phase resolves to a phase in ROADMAP.md |
A type scheduled into a phase that does not exist |
Every numeric parameter group is covered by exactly one provenance row |
A number with no source, which is a claim nobody can check |
No implemented entry carries a tier-D provenance row |
Unverified physics shipping as fact |
| Topic template renders and is unique across all instantiated bindings in all shipped facility profiles | Two devices writing the same UNS address |
Sparkplug datatype matches the golden table for the entry's channel_shape |
A vector declared as a scalar Float |
| Goodness-of-fit suite passes for the entry | The generator does not produce what the entry claims |
| Physical-bounds property suite passes at the declared sample count | Wrong distribution family chosen |
| Stream hash identical across two runs at the same seed on the same platform | Hidden global RNG state |
| Generator cost and payload size within declared budget | A type that quietly breaks the scaling curve |
| Additive-only diff against the previous release within a major schema version | Breaking downstream historian schemas |
A.7 Parameter provenance and evidence tiers¶
D-11 rules that a validation gate names a specific external published reference, that this repository is never a reference for itself, and that a statistic with no valid external reference is recorded as an open question rather than as a passing gate. A catalog of eighty types is several thousand numbers, so that ruling needs machinery rather than good intentions.
Every numeric parameter belongs to exactly one provenance row, and every row carries a tier.
| Tier | Meaning | May an implemented entry carry it |
How it reads in the entry |
|---|---|---|---|
| A | Primary text retrieved and quotable, or arithmetic from stated relations and stated inputs | Yes | Plainly, with the source id |
| B | Two independent secondary sources agree | Yes | Plainly, with both source ids |
| C | One secondary source, or a named standard whose body could not be retrieved | Yes | With the source named in the text, never bare |
| D | Inferred, recalled, or plausible with no source | No | Blocks implemented; the entry stays planned |
Three consequences follow, and they are the point of the mechanism.
A named standard whose text is paywalled is tier C, not tier A. The entry names the standard, records what the retrieval returned in the source row, and does not reproduce numbers from it. The ISO 20816-3 vibration severity zones are the clearest case: this repository names the standard as the authority for the zone boundaries and reads the values from the purchased document at implementation time, because a boundary recalled rather than read is exactly the failure this rule exists to stop.
Arithmetic counts as tier A when the relation and its inputs are both stated in the entry. The bearing defect multipliers in A.2 are the model: the relations and the geometry are written down, so a reader recomputes 3.585 rather than trusting it. That is why the entry stores the geometry and derives the multipliers instead of storing the multipliers.
A tier-D parameter is not deleted. It is a planned entry with an open question attached, which is what rule 1 of the project means by sequencing rather than cutting. Section I carries the standing open question for the parameter backlog.
sources.yaml rows have this shape.
- id: SRC-CFR-47-15-247
publisher: "United States Government Publishing Office"
title: "47 CFR 15.247, Operation within the bands 902-928 MHz, 2400-2483.5 MHz, and 5725-5850 MHz"
edition: "current as published on eCFR"
locator: "https://www.ecfr.gov/current/title-47/part-15/section-15.247"
retrieved: "2026-08-09"
http_status: 200
tier: A
- id: SRC-ISO-20816-3
publisher: "International Organization for Standardization"
title: "ISO 20816-3, Mechanical vibration, measurement and evaluation of machine vibration, part 3"
edition: "cited by designation; the body was not retrieved"
locator: "https://www.iso.org/standard/50611.html"
retrieved: "2026-08-09"
http_status: 403
tier: C
note: "Standards body blocked automated retrieval. Zone values are read from the purchased document."
http_status is a required field. A source row that claims retrieval without recording what the server returned is the paper trail equivalent of an untested assertion, and test_every_source_records_retrieval fails the build on it.
B. The implemented type table¶
80 types across the eight categories. Column meanings: the signal model column summarizes baseline, noise, drift, and coupling; the primary failure modes column lists the class-specific ones on top of the inherited universal set; the capability column names what would be impossible without this type.
Numbers in these cells are summaries of the parameters in the entries, and they carry the tier of the entry's provenance row (A.7). Where a cell names a standard, this section is naming the authority for a value and not reproducing the value.
B.1 Industrial equipment (14 types) - feeds predictive maintenance¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 1 | eq.vib.velocity_rms |
Broadband vibration severity, 10 Hz to 1 kHz | mm/s | Baseline scales with shaft speed squared and load; lognormal multiplicative noise (strictly positive amplitude); OU drift on mount preload; couples to bearing.*_wear, motor.load_fraction, mount.looseness. ISO 20816 zone boundaries A/B/C/D come from config, not code |
Mount looseness (raises broadband only), sensor resonance at the mount, cable microphonics, transverse sensitivity error | conveyor_drive, palletizer_cell, asrs_crane, factory_mixer_drive |
ISO-referenced severity zones; the top-level PdM alarm limit |
| 2 | eq.vib.band_spectrum |
Order-tracked band energies at 1x, 2x, 3x, BPFO, BPFI, BSF, FTF, residual | mm/s per band | Defect band centers are computed from the bearing geometry by the relations written out in A.2, so a 9-ball 6205 geometry yields 3.585x, 5.415x, 2.357x, and 0.398x shaft rate as recomputable arithmetic rather than as recalled constants; band amplitude follows the four-stage bearing progression, so defect lines rise before broadband does and then collapse into a raised noise floor at stage 4; ar1 noise floor; Arrhenius sensitivity ageing |
Speed-reference loss (bands decohere and the machine looks healthier), mount looseness (broadband up, defect bands flat), band aliasing at VFD speed changes | conveyor_drive, palletizer_cell, asrs_crane |
Bearing defect localization (outer vs inner vs element vs cage), which is what makes a time-to-threshold estimate defensible |
| 3 | eq.vib.envelope_gse |
Demodulated envelope acceleration in the bearing resonance band | gSE | Band-pass 500 Hz to 10 kHz, rectify, low-pass, then FFT of the envelope. Detects stage-2 damage roughly 10x earlier than velocity spectrum. Gamma-distributed hit amplitude; hits arrive as a Hawkes process once a spall exists | Wrong demodulation band chosen for the machine, resonance shift after a rebuild, envelope saturation at high crest factor | conveyor_drive, asrs_crane |
Early bearing detection lead time; the difference between a 9-day warning and a 9-hour one |
| 4 | eq.accel.triaxial_g |
Peak and RMS acceleration, three axes, with shock capture | g | MEMS accelerometer with declared bias instability (Allan variance floor), temperature-dependent bias, vibration rectification error; shock events drawn from a generalized Pareto tail that is not truncated | Bias instability walk, axis swap on remount, saturation on hard impact, self-test failure | agv_amr_fleet, palletizer_cell, asrs_crane |
AMR collision and hard-stop detection; palletizer jam impact signature |
| 5 | eq.strain.bridge_ue |
Mechanical strain on a structural or drive member | microstrain | Full Wheatstone bridge, gauge factor 2.0, output in mV/V. Apparent strain from thermal output of a few microstrain per degC when the gauge is not matched to the substrate, so the reading is meaningless without the co-located temperature channel; adhesive creep as a slow OU drift | Thermal output error (mistaken for real load), adhesive creep, moisture ingress lowering insulation resistance, lead-wire desensitisation | palletizer_cell, asrs_crane, conveyor_drive |
Load-path verification on the palletizer arm; overload event capture |
| 6 | eq.load.shear_beam_kg |
Force or weight at an equipment mount | kg | Shear beam cell of an OIML R60 accuracy class, which is the standard that defines the class and supplies the temperature-effect and creep limits the entry parameterizes. The consequence the model reproduces: a zero temperature coefficient expressed as a fraction of rated output means a large-capacity cell wanders by a visible mass over a diurnal swing with no load change at all, and creep under a sustained load moves the reading in the same direction over tens of minutes; hysteresis on unload | Temperature-coefficient drift, creep under sustained load, permanent zero shift after overload, mechanical shunt (something touching the platform) | palletizer_cell, conveyor_drive |
Pallet mass verification against the ASN; overload protection |
| 7 | eq.torque.rotary_nm |
Shaft torque on a driven axis | N.m | Rotating strain-gauge transducer with telemetry link; baseline torque tracks load fraction and adds a per-revolution ripple from gear mesh; lognormal noise; slip-ring or inductive coupling adds occasional dropouts | Slip-ring wear (intermittent dropouts), zero drift with temperature, telemetry link loss, mechanical resonance in the coupling | palletizer_cell, asrs_crane, factory_mixer_drive |
Mechanical power computed independently of electrical power, which turns motor efficiency into a measured quantity |
| 8 | eq.encoder.quadrature_rpm |
Rotational speed and position | rpm, counts | 1024 PPR incremental quadrature with index; speed from counts per interval so resolution degrades at low speed; coupling slip appears as a slowly accumulating position error against the index pulse | Missed counts from VFD-induced EMI, index pulse loss, coupling slip (accumulating error), marginal signal at high speed, direction inversion after rewiring | conveyor_drive, asrs_crane, agv_amr_fleet |
The speed reference every order-tracked vibration band depends on; belt-slip detection when paired with belt speed |
| 9 | eq.temp.thermocouple_k |
Surface or process temperature, wide range | degC | Type K, with the Seebeck coefficient taken from the NIST ITS-90 thermocouple reference functions rather than stated here, because a coefficient recalled instead of read is the error this catalog's provenance rule exists to stop. The first-order lag is the load-bearing part: an exposed junction settles in seconds and a grounded sheath in tens of seconds, so a step in process temperature is never a step in the reading; cold-junction compensation error appears as a bias correlated with enclosure ambient | Cold-junction compensation error (ambient-correlated bias, the classic), open circuit driving upscale burnout, wrong TC type wired, extension-wire polarity reversal, loss of thermal contact lengthening tau | conveyor_drive, factory_oven, factory_mixer_drive |
Motor and process surface temperature trending; the tau makes lag-compensated trend estimation a real problem worth solving |
| 10 | eq.temp.rtd_pt100 |
Precision temperature, narrow range | degC | 4-wire Pt100 evaluated by the Callendar-Van Dusen relation, with the temperature coefficient and the polynomial constants taken from IEC 60751, which is the standard that defines them. Self-heating is a real error at practical excitation currents in still air, and drift is far lower than a thermocouple's, which is why this is the reference in a Gage R and R against the thermocouple | Lead-resistance error in 3-wire mode, self-heating, insulation resistance fall in high humidity, element strain after thermal cycling | conveyor_drive, factory_reactor, env_reference |
The reference standard in the measurement system analysis; the temperature source for every temperature-compensated channel |
| 11 | eq.acoustic.ae_hits |
Acoustic emission hit rate and absolute energy | hits/s, dB_AE | Two regimes as a mixture: continuous emission from friction and leakage raises the RMS floor, burst emission from crack initiation and propagation arrives as a self-exciting Hawkes process. Kaiser effect modeled: emissions on reload are suppressed until the previous peak load is exceeded | Sensor couplant dry-out (silent sensitivity loss), background noise from adjacent machinery, threshold set wrong so hits are all noise or all missed, waveguide contact loss | asrs_crane, palletizer_cell, structural_rack |
Crack initiation detected before any dimensional change exists to measure; the earliest signal in the PdM stack |
| 12 | eq.oil.condition |
Lubricant dielectric constant, water activity, ferrous debris | rel. permittivity, aw, ppm | Dielectric shifts with oxidation and contamination; water activity is bounded 0 to 1 so it is drawn from a Beta, never a clipped Normal; ferrous debris is an over-dispersed count drawn from a negative binomial, coded to ISO 4406 bins; couples to bearing.*_wear and to oil temperature |
Sensor cell coating, water activity saturating during a washdown (real, not a fault), debris sensor magnetic saturation, sampling port drawing from a dead leg | conveyor_drive, asrs_crane, factory_mixer_drive |
Lubrication-cause attribution: separates "the bearing is failing" from "the bearing is failing because the oil has water in it" |
| 13 | eq.motor.current_mcsa |
Motor phase current and sideband ratio | A, dB | RMS current tracks load; the diagnostic is the sideband structure: broken rotor bars produce sidebands at (1 +/- 2s)f_line where s is slip, eccentricity at f_line +/- k*f_rotor, bearing defects modulate at the defect frequencies. Healthy sideband ratio below -50 dB, degraded -45 to -35 dB. VFD carrier adds harmonics that must be excluded from the analysis band |
CT saturation on inrush, phase mis-assignment on install, VFD carrier bleeding into the analysis band, sideband masking at light load (below about 40 percent load MCSA is not diagnostic, which is itself a finding) | conveyor_drive, palletizer_cell, agv_amr_fleet |
Non-intrusive rotor and eccentricity diagnosis; the energy-per-pallet KPI (E7) also reads this channel |
| 14 | eq.motor.winding_temp |
Stator winding temperature | degC | Embedded RTD in the winding; thermal model is a first-order lag on I-squared-R heating with a time constant of 15 to 45 minutes for a fan-cooled frame, so overload shows in current minutes before it shows in temperature; ambient offset couples to env.temp.ambient |
Thermal contact loss (reads ambient, looks fine), open winding RTD, insulation class exceeded without an alarm because the trip point was set for a different frame | conveyor_drive, palletizer_cell |
Insulation-life consumption estimate; the thermal constraint in every throughput what-if that pushes line speed |
B.2 Environmental and facility (7 types) - feeds facility health and worker comfort¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 15 | env.temp.ambient |
Zone dry-bulb air temperature | degC | Diurnal profile plus a seasonal profile from the shared weather state (E40), modified by dock door open events (an open door pulls the zone toward outside air with a time constant set by zone volume and infiltration rate) and by HVAC control cycling; Gaussian noise plus pink drift | Solar loading on a badly sited sensor, self-heating in an enclosure, stratification (a sensor at 8 m reads nothing like the pick face), sensor placed in the HVAC discharge | facility_zone, hvac |
Zone thermal map; the ambient reference for every temperature-compensated channel; heat-stress comfort findings when paired with humidity |
| 16 | env.rh.relative_pct |
Relative humidity | percent | Bounded 0 to 100, so generated from a Beta scaled to the range, never a clipped Normal. Inversely coupled to zone temperature at constant absolute humidity; dock door events inject outside absolute humidity | Capacitive polymer sensor saturation hysteresis after condensation (reads high for hours), contamination drift, filter cap clogged so the response time lengthens by an order of magnitude | facility_zone, hvac |
Condensation risk on cold inbound product; corrugated strength derating; the comfort index input |
| 17 | env.dewpoint.derived |
Dew point temperature | degC | Derived channel, computed from the co-located temperature and humidity pair via the Magnus formula. Carries the schema's derived-entry form: no independent noise, and error propagated analytically from its two sources. Physical bound dewpoint <= ambient_temp is a property-test assertion |
Inherits both parent failure modes and amplifies them: a 3 percent RH error near saturation moves dew point by roughly 0.5 degC, and a stuck parent silently freezes the derived channel | facility_zone, hvac, cold_chain |
Condensation prediction on cold-chain product moved into a warm dock, which is where cold-chain damage actually happens |
| 18 | env.co2.ndir_ppm |
Carbon dioxide concentration | ppm | NDIR. The outdoor baseline tracks the NOAA Global Monitoring Laboratory global annual mean, which was 425.62 ppm for 2025, so a hardcoded 400 ppm would be a decade out of date and would bias every ventilation finding low. Indoor concentration rises with occupancy at a configured per-worker generation rate and falls with ventilation rate; the rate couples to the roster from the HR layer, so shift changes are visible in the trace | Automatic baseline calibration drift when the space is never unoccupied (the classic NDIR failure in a 24-hour operation), lamp ageing, pressure sensitivity at altitude | facility_zone, hvac |
Ventilation adequacy and cognitive-performance findings; an independent occupancy estimate that cross-checks the roster |
| 19 | env.pm.optical_ugm3 |
Particulate mass concentration, PM2.5 and PM10 | ug/m3 | Optical particle counter. Counts are Poisson at the detector, converted to mass by an assumed density and size distribution, which is exactly where the accuracy goes; forklift traffic and dock door events drive spikes; humidity above about 75 percent causes hygroscopic growth that inflates the optical reading | Hygroscopic overestimate in high humidity, optical chamber fouling, fan degradation changing the sample flow rate, size-bin miscalibration | facility_zone, hvac |
Air-quality worker comfort findings; a leading indicator for optical sensor fouling elsewhere in the building |
| 20 | env.leak.water_rope |
Water presence and distance along a sensing cable | boolean, m | Discrete state machine with a distance-to-leak measurement from cable resistance. Onset is a rare Poisson event coupled to roof load, rainfall from the weather state, and sprinkler system state | Conductive dust bridging the conductors (false positive), cable damage reading as a permanent leak, dried residue leaving the alarm latched, distance calibration off after a cable splice | facility_zone, structural_roof |
Water intrusion findings tied to the structural layer; product-damage blast radius through lot genealogy |
| 21 | env.thermal.ir_array |
Low-resolution thermal image | degC per pixel, 32x24 | Microbolometer array. Publishes max, mean, and a per-pixel matrix; scene temperature couples to equipment surface temperatures and to ambient. Emissivity is the dominant error: a shiny bus bar reads far below its true temperature, so the entry carries a per-target emissivity constant | Emissivity error (systematic and invisible), lens contamination, ambient drift without shutter calibration, reflected apparent temperature from a nearby hot source | electrical_panel, conveyor_drive, facility_zone |
Non-contact electrical connection hot-spot detection; a thermal cross-check on the contact temperature sensors |
B.3 Warehouse and logistics (15 types) - feeds the DC flows¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 22 | whs.rfid.portal_read |
UHF RFID tag read events at a fixed portal | event, dBm | Probabilistic rather than deterministic. Per-tag, per-inventory-round read probability is the product of orientation (a dipole pattern, so cos^2 alignment with polarization match), range (forward-link-limited Friis against tag sensitivity), material detune factor per material class, and slot-collision loss that falls roughly as 1/N in tag population. Reads over a pass are a bounded count over rounds = dwell / round_time, where dwell is portal depth divided by pallet speed, so slowing the conveyor raises the read rate. RSSI is Rician-faded near the portal and Rayleigh in a metal-rich aisle, and 47 CFR 15.247 caps the forward link at 1 W conducted with up to 6 dBi of antenna gain, which is the 36 dBm EIRP ceiling the entry enforces |
Antenna VSWR rise on one port (read rate falls on one port while siblings hold), cross-read from the adjacent dock (a false positive, not a miss), tag detune by product, VFD RF interference, reader thermal throttling, duplicate reads from multipath | receiving_portal, shipping_portal, cross_dock |
Pallet-level identity and the whole genealogy graph; the read-rate control chart per portal; RF physics what-ifs (E46) |
| 23 | whs.rfid.handheld_read |
Handheld reader cycle-count reads | event, dBm | Same physical model with operator-controlled geometry: dwell and angle are drawn from operator-specific distributions, and a fatigued operator (from the ergonomics layer) sweeps faster and reads less. This is the operator variance term that makes a Gage R&R meaningful | Operator technique variance, battery-driven power reduction as the pack depletes, antenna damage from drops, trigger discipline (a partial sweep counted as complete) | cycle_count, inventory_control |
Reproducibility (operator-to-operator) in the Gage R&R against the fixed portal; inventory record accuracy measurement |
| 24 | whs.smartshelf.rfid_inventory |
Continuous shelf-level tag census | count, event | A near-field or low-power shelf antenna running a continuous inventory; the census is a Binomial draw per tag per cycle, and tags at the shelf edge or behind metal are systematically under-read, producing a persistent negative bias rather than random error | Persistent blind spot from a shelf reorganisation, neighboring shelf cross-read inflating the count, antenna coupling change when the shelf is fully loaded | pick_face, inventory_control |
Real-time on-hand per pick face; the book-versus-physical accuracy KPI the finance layer consumes |
| 25 | whs.barcode.scan_event |
1D and 2D symbology scan events with no-read tracking | event | Read success is Bernoulli per presentation with probability driven by print contrast, label damage, angle, and motion blur at conveyor speed; a laser 1D scanner and a 2D imager have different sensitivity profiles, carried as a symbology dimension. No-reads are counted, not silently dropped, because the no-read rate is the KPI |
Lens fouling reducing contrast margin, ambient light saturation near a dock door, label print quality degradation from a failing printer (an upstream cause visible downstream), retro-reflector misalignment, decode of a wrong-but-valid check-digit symbol | receiving, pick_pack, sortation |
The scan-step compliance audit the CV channel cross-checks; label print quality as a supplier scorecard input |
| 26 | whs.weight.shelf_load |
Distributed load on a shelf or flow rack | kg | Array of four cells summed; the sum is accurate but the individual cell readings reveal load position, and an off-center load causes corner-load error if the shelf is not properly shimmed. Temperature drift as in the industrial load cell entry, plus creep under a load that sits for weeks | Corner-load error from an unshimmed shelf, creep under long-dwell inventory, a cell going open-circuit so the sum silently drops by roughly a quarter, mechanical shunt from a carton wedged against the frame | pick_face, inventory_control |
Weight-based unit count for small parts; a physical cross-check on the RFID census |
| 27 | whs.weight.pallet_scale |
Pallet gross weight on the conveyor | kg | In-motion weighing: the reading is a filtered average over the time the pallet is fully on the scale deck, so it degrades as speed rises and is invalid if the pallet spans the deck edge. Belt vibration adds noise that the filter length trades against throughput | Partial-pallet-on-deck reading (invalid, must be rejected not published), vibration coupling from the adjacent drive, zero drift with temperature, debris on the deck offsetting zero | conveyor_transport, receiving |
Received weight versus ASN expected weight reconciliation; short-ship and over-ship detection at the pallet level |
| 28 | whs.weight.dock_scale |
Vehicle or pallet weight at the dock | kg | Static or in-motion floor scale with a much larger capacity and coarser resolution; legal-for-trade class means the quantisation is real and visible in the data (a 30000 kg scale with 10 kg divisions) | Quantisation mistaken for stability, deck contamination, load cell moisture ingress at a wet dock, unlevel deck after a forklift strike | dock_door, yard |
Inbound and outbound weight reconciliation; trailer cube-versus-weight utilization |
| 29 | whs.uwb.tag_position |
Real-time position of AMRs, forklifts, and high-value assets | m (x, y, z) | UWB time-difference-of-arrival across anchors. Accuracy 10 to 30 cm in clear line of sight; non-line-of-sight adds a strictly positive range bias drawn from an exponential, never a symmetric error, because a blocked path is always longer; geometric dilution of precision from anchor placement is computed from the actual anchor geometry in facility.yaml, so a badly placed anchor set produces visibly worse positions |
NLOS bias in dense racking, anchor clock sync loss (positions shear in one direction), anchor obstruction after a rack reconfiguration, tag battery depletion lowering the update rate | agv_amr_fleet, worker_tracking, asset_tracking |
Congestion mapping and AMR traffic analysis; worker-to-AMR proximity for the safety layer; travel-distance measurement for the slotting optimizer |
| 30 | whs.ble.beacon_rssi |
Zone-level presence of low-cost tagged assets | dBm | Log-distance path loss RSSI = A - 10 n log10(d) with n between 2.0 and 3.5 indoors, plus log-normal shadowing of 4 to 8 dB standard deviation. The honest consequence is that distance from RSSI is unusable and BLE gives zone presence only, which is why the catalog carries both BLE and UWB rather than pretending one covers both jobs |
Shadowing from a human body between tag and receiver, battery depletion lowering advertisement rate, 2.4 GHz contention with the Wi-Fi network, beacon MAC randomization breaking identity | asset_tracking, tote_tracking |
Cheap coverage of the tote and tool population where centimetre accuracy is not worth the anchor cost; the cost-versus-accuracy what-if against UWB |
| 31 | whs.door.dock_state |
Dock door position and travel time | state, s | State machine over closed, opening, open, closing, obstructed. Travel time is a lognormal duration, and its slow increase is the maintenance signal: a door that took 11 s new and takes 17 s now has a spring or chain problem. Couples to the inbound and outbound schedule | Limit switch drift (door reports closed while a gap remains, which is a thermal loss and a security finding), obstruction detection failure, chain stretch lengthening travel time, control relay chatter | dock_door, yard |
Dock utilization and door contention between inbound and outbound; the thermal load event that drives cold-chain and HVAC findings |
| 32 | whs.conveyor.speed_mps |
Belt surface speed | m/s | Tach roller on the belt surface, read against the drive encoder. The diagnostic is the divergence: motor speed rising while belt speed holds is belt slip, and belt speed drifting slowly downward at constant motor speed is belt stretch. Small Gaussian noise on a smooth setpoint-tracking baseline | Tach roller slip on a dusty belt, roller bearing seizure (reads zero while the belt runs), encoder EMI, roller wear changing the circumference calibration | conveyor_transport, sortation |
Belt slip detection independent of the motor; the throughput totalizer when integrated with belt load; the dwell time every RFID read probability depends on |
| 33 | whs.conveyor.load_kgm |
Carried load per meter of belt | kg/m | Weigh idler under the carry side. Integrated with belt speed to produce a mass throughput totalizer, which is monotone non-decreasing and property-tested as such; material buildup on the idler produces a slow positive zero offset that inflates every subsequent total | Idler buildup inflating zero, idler bearing drag adding a load-dependent error, belt tension change altering the weighing geometry, splice passing over the idler causing a periodic spike | conveyor_transport |
Mass throughput independent of unit counts; drive torque demand for the energy KPI |
| 34 | whs.ptl.confirm_event |
Pick-to-light confirmation events with response time | event, ms | Response time is a lognormal duration whose median shifts with operator fatigue from the ergonomics layer and with training tenure from the HR layer, so a new hire's response distribution is visibly different and converges along a learning curve | Button contact wear (double confirms), light module failure (operator picks from an unlit location), confirm-before-pick gaming (a short response time below physical possibility, which is itself the detection), network latency inflating measured response time | pick_face, pick_pack |
Operator cycle-time measurement at the task level; the learning curve the HR layer needs; confirm-gaming as an SOP violation finding |
| 35 | whs.photoeye.presence |
Through-beam occupancy and gap between units | boolean, mm | Response time 1 to 5 ms; the derived quantity is the gap between consecutive units, which is what singulation and divert logic depend on. Occupancy dwell exceeding a threshold is a jam | Lens fouling reducing excess gain until it fails at the first dusty week, alignment drift, ambient IR from sun through an open dock door, reflection from a shiny carton faking a clear beam | conveyor_transport, sortation |
Jam detection and jam localization; singulation gap measurement that the sorter divert window depends on |
| 36 | whs.trailer.presence |
Trailer at the door, and trailer identity where tagged | boolean, id | Ultrasonic ranging to the trailer face plus an inductive loop for metal presence; two independent technologies deliberately, so a disagreement is a diagnosable finding rather than an ambiguity | Ultrasonic false echo from the dock leveller, loop detuning after a floor repair, snow or ice absorbing the ultrasonic pulse, a trailer parked short so ultrasonic sees it and the loop does not | dock_door, yard |
Yard occupancy and detention time measurement; the dock-availability constraint in the scheduling optimizer |
B.4 Transportation and fleet (12 types) - enables cold-chain integrity¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 37 | veh.gnss.position |
Vehicle position, speed, and fix quality | deg, m/s, HDOP | Position error is a function of satellite geometry (HDOP) plus a multipath term that rises in urban canyon and at the dock; speed from Doppler is far more accurate than differentiated position, so the two are separate channels with different error models; a stationary receiver exhibits a bounded random walk of 1 to 3 m, which naive geofencing reads as movement | Multipath at the dock, HDOP spike under a bridge or canopy, cold-start time-to-first-fix around 30 s after a power cycle, position hold snapping (a firmware feature that hides real slow movement), jamming | transport_network, yard |
Lane transit time measurement; geofence arrival and departure events; the location half of every cold-chain excursion record |
| 38 | veh.imu.6dof |
Vehicle acceleration and angular rate | g, deg/s | MEMS with declared bias instability from an Allan variance floor (roughly 0.05 mg accel, 10 deg/hr gyro for automotive grade), temperature-dependent bias, and vibration rectification error that biases the accelerometer in a way the gyro does not; harsh-event thresholds around 0.3 g are config, not code | Bias instability random walk, mounting angle error making every event read on the wrong axis, saturation on a real hard brake, temperature bias shift on a cold start | transport_network, agv_amr_fleet |
Harsh-driving events for the carrier scorecard; road shock context that explains a cargo shock event |
| 39 | veh.fuel.level_pct |
Fuel remaining | percent | Bounded, so Beta-distributed around the true level. Raw signal is dominated by slosh, so the honest model is a heavily filtered value with a several-minute effective time constant; refuel events are step increases, and theft is a step decrease while stationary, which is precisely the signature the fraud check looks for | Stuck float giving a plateau, tank geometry non-linearity near full and empty, slosh filter masking a genuine fast drop, sender resistance drift | transport_network |
Fuel economy per lane and the carrier cost model; fuel theft detection |
| 40 | veh.tire.tpms |
Tire pressure and temperature per wheel | kPa, degC | Pressure and temperature are physically coupled through Gay-Lussac's law at fixed volume, so the sensitivity is the absolute pressure divided by the absolute temperature and is not a single constant: a passenger tire near 240 kPa gauge moves about 1.2 kPa per degC and a heavy-truck tire near 700 kPa gauge moves about 2.7 kPa per degC. A cold-morning reading is legitimately low and alarming on it is the classic false positive. The catalog carries the temperature-normalized pressure as the alarmable quantity. A slow leak is a linear ramp, a puncture is a fast exponential decay | Sensor battery end-of-life (typically 5 to 7 years, then silent), wheel position mis-learn after a tire rotation, RF dropout at speed, valve stem sensor corrosion | transport_network |
Preventable roadside failure avoidance; the fuel economy penalty of underinflation in the transport cost model |
| 41 | veh.engine.coolant_temp |
Engine coolant temperature | degC | First-order thermal lag with a warm-up ramp from cold start; thermostat opening produces a characteristic plateau then a step, and a stuck-open thermostat shows as a warm-up that never completes, which is a distinct and recognizable trace | Sensor in the wrong port reading a cooler point, air pocket in the cooling system causing erratic reads, thermostat stuck open (never reaches operating temperature) or closed (overheats), gauge damping hiding a real spike | transport_network |
Engine health for the fleet maintenance queue; a cause explanation for a reefer power loss on a diesel-driven unit |
| 42 | veh.cargo.temp_probe |
Cargo temperature at supply air, return air, and product pulp | degC | Three deliberately different measurement points with different time constants: supply air responds in seconds, return air in minutes, and pulp temperature in tens of minutes because product thermal mass dominates. Door-open events drive an exponential approach toward ambient with a time constant set by insulation and load mass. Defrost cycles raise return air by several degC on a schedule, which is not an excursion, and discriminating the two is the whole problem | Probe placed in the air stream instead of the product (reads recovery, misses the real excursion), probe pulled out of the pulp by load shift, ice buildup insulating the probe, calibration drift below zero where it matters most | transport_network, cold_chain |
Cold-chain integrity: excursions become findings, traced through lot genealogy to every affected pallet, order, and customer |
| 43 | veh.cargo.humidity |
Cargo compartment relative humidity | percent | Beta-bounded as in the facility entry; couples to cargo temperature so that a cooling load approaching its dew point condenses, which is the physical mechanism behind wet-carton damage claims | Condensation hysteresis after a door-open event, sensor saturation in a produce load, filter fouling | transport_network, cold_chain |
Moisture-damage claims (E38); packaging integrity findings on the supplier scorecard |
| 44 | veh.cargo.shock_event |
Impact events on the load | g, ms | Triaxial shock recorder with a trigger threshold. Peak magnitude is drawn from a generalized Pareto tail that is never truncated, because the rare 40 g forklift strike is the event that matters and a truncated tail would erase it. Drop height is inferred from the velocity change over the pulse | Threshold set too high (silent misses), sensor mounted on the trailer wall instead of the load (records road input, not load impact), battery depletion mid-transit, timestamp skew placing the event on the wrong lane leg | transport_network, cold_chain |
Damage attribution to a specific carrier and leg; cargo claims generation (E38) |
| 45 | veh.cargo.tilt |
Load tilt and roll angle | deg | Angles come from a von Mises distribution, not a Normal, so wraparound is handled correctly. Sustained tilt beyond a threshold indicates load shift; transient tilt correlates with cornering from the IMU, which is how a real shift is separated from a hard turn | Sensor mounted on a pallet that itself shifts, gravity vector confusion during sustained acceleration, mounting adhesive failure | transport_network |
Load shift detection and load-securing findings; the tip-over risk input to the safety layer |
| 46 | veh.reefer.unit_status |
Refrigeration unit setpoint, mode, defrost state, run hours | degC, state, h | State machine over cooling, heating, defrost, null-cycle, off, with dwell distributions per mode. Run hours are monotone non-decreasing and property-tested as such. Fuel or power draw couples to mode | Setpoint changed in transit (a human cause of an excursion, and one that only this channel can distinguish from an equipment failure), defrost stuck on, refrigerant charge loss causing longer cooling cycles and a slowly rising return air floor, controller clock drift misaligning defrost schedules | transport_network, cold_chain |
The cause half of a cold-chain finding: equipment failure, door event, or human setpoint change, which is the difference between a claim and a corrective action |
| 47 | veh.door.trailer_open |
Trailer door and seal state | boolean, id | Discrete state with a magnetic reed or hall switch, plus optional seal identity. Open events are the dominant real cause of temperature excursions, and the pairing of this channel with cargo temperature is what makes the excursion diagnosable | Magnet misalignment after a door repair (reads closed while open), reed switch welding, seal id transposed at a transfer point | transport_network, cold_chain, security |
Excursion cause attribution; chain-of-custody integrity for the traceability ledger (E35) |
| 48 | veh.telematics.gateway |
Odometer, engine hours, idle time, fault codes | km, h, s, code | A gateway aggregating a vehicle bus. Odometer and engine hours are monotone non-decreasing; idle time accumulates only when speed is zero and the engine runs; fault codes are discrete events with a code vocabulary. The gateway itself has a store-and-forward buffer, so a cellular outage produces a burst of backdated messages on reconnect, which is the correct behavior and the historian must handle it | Store-and-forward replay arriving out of order, odometer rollover, clock drift on a device that has been offline for days, parameter-group misdecoding after a firmware change | transport_network, fleet_registry |
Utilization and idle-waste KPIs; the store-and-forward replay case the historian is tested against |
B.5 Electrical and power (10 types) - feeds energy KPIs and AMR charging physics¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 49 | pwr.voltage.three_phase |
Line and phase voltage RMS, per phase | V | Nominal with slow supply variation plus event-driven sags: a large motor start pulls a dip whose depth scales with starting current and source impedance, recovering over hundreds of milliseconds. Phase imbalance is a separate derived channel because a 2 percent voltage imbalance produces roughly a 15 percent current imbalance in an induction motor, which is a real and non-obvious motor-damage mechanism | PT ratio misconfigured (everything reads by a constant factor), neutral reference loss, aliasing of the sag transient by too slow a sample rate, phase rotation reversed after a panel change | electrical_panel, site_power |
Power quality findings; sag-induced production stops correlated to the twin's downtime record |
| 50 | pwr.current.phase_ct |
Per-phase current | A | Current transformer with a declared ratio and burden. Baseline tracks mechanical load; inrush on start is 5 to 8 times rated for a few hundred milliseconds, which saturates a CT sized for running current, so the recorded inrush is an underestimate and the entry says so | CT saturation on inrush, ratio misconfiguration, secondary open-circuit (a genuine hazard and a hard-fail condition), phase mis-assignment on install | electrical_panel, conveyor_drive, agv_amr_fleet |
Per-circuit load attribution; the current half of every power and power factor computation |
| 51 | pwr.meter.active_power |
Real power and cumulative energy | kW, kWh | Computed from synchronized voltage and current, so it inherits both error models. kWh is monotone non-decreasing and property-tested; a rollback is a hard failure. Idle power is a first-class quantity because idle energy is the eighth-waste finding (E7) | Register rollover mistaken for a reset, energy accumulation gap during a device reboot (the value must resume, not restart), reversed CT giving negative power on a load, sub-metering that does not sum to the site meter | electrical_panel, conveyor_drive, hvac |
Energy per pallet; idle-energy waste findings; the energy delta reported alongside every what-if throughput delta |
| 52 | pwr.meter.power_factor |
Displacement power factor | ratio, -1 to 1 | Bounded, so Beta-distributed on the magnitude with a sign channel. The physics that matters: an induction motor at 25 percent load runs a power factor near 0.4 to 0.6 while the same motor at rated load runs near 0.85, so an oversized conveyor motor running lightly loaded is visible as a poor power factor and that is a real, actionable, money-saving finding | Sign convention confusion between leading and lagging, harmonic distortion making displacement and true power factor diverge (they are different quantities and conflating them is common), measurement at too coarse an interval to see the load cycle | electrical_panel, site_power |
Motor right-sizing findings; the reactive power charge in the energy cost model |
| 53 | pwr.smartmeter.site_interval |
Site import energy and demand, on the utility interval | kWh, kW | Fixed 15-minute interval aggregation with a demand peak per interval. The peak, not the total, drives the demand charge, so the model reproduces the fact that a single coincident start can cost more than an hour of steady running. Sub-meter sum must reconcile to the site meter within a tolerance, and the residual is the unmetered load | Interval boundary misalignment against the utility clock, meter clock drift shifting the peak into the wrong interval, missing interval backfill, tariff schedule change not reflected in the cost model | site_power |
Demand-charge optimization what-ifs (staggering AMR charging, shifting the palletizer start); the site-level energy KPI |
| 54 | pwr.battery.soc |
AMR pack state of charge | percent | Bounded 0 to 1, Beta-distributed around the true value. LFP chemistry has a flat open-circuit voltage between roughly 20 and 90 percent SOC, which is exactly why coulomb counting is used and exactly why the estimate accumulates a random-walk integration error until a full charge resets it. Charging follows constant current to a voltage knee then constant voltage taper, so the last 15 percent takes disproportionately long and a fleet charging plan that assumes linear charging will be wrong | Coulomb counting integration drift between full-charge resets, current sensor offset integrating into a large SOC error over a shift, SOC jump at the reset (real, and it looks like a fault), charge acceptance derated below 5 degC and above 45 degC | agv_amr_fleet, battery_bank |
Realistic AMR charging physics: opportunity-charge scheduling, fleet availability under a charging constraint, and the AMR count what-if that must account for charge time |
| 55 | pwr.battery.soh |
Pack capacity fade and internal resistance rise | percent, mOhm | Capacity fade as a function of equivalent full cycles and depth of discharge, with a calendar ageing term; internal resistance rise is the earlier and more sensitive indicator, and it also reduces usable capacity at high discharge rates, so an aged pack fails first under peak load rather than at rest | Capacity estimate only refreshable on a full discharge cycle that operations never allows, resistance measurement confounded by temperature, cell imbalance hidden by pack-level measurement | agv_amr_fleet, battery_bank |
Battery replacement scheduling and its capital cost in the AMR fleet what-if; end-of-life prediction for the CMMS queue |
| 56 | pwr.battery.pack_temp |
Pack cell temperature | degC | Thermal model driven by I-squared-R heating during charge and discharge with a cooling term; charge current is derated by the BMS outside 5 to 45 degC, so a hot pack charges more slowly, which propagates directly into fleet availability. Cell-to-cell spread is a separate channel because a widening spread is the imbalance signal | Sensor on the pack case rather than a cell (under-reads the hot cell), single sensor missing the hot spot, thermal runaway precursor masked by averaging | agv_amr_fleet, battery_bank |
Charge-rate derating in the AMR availability model; a thermal safety finding with a severity floor |
| 57 | pwr.ups.status |
UPS mode, runtime remaining, self-test result | state, min | State machine over on-line, on-battery, bypass, fault, with dwell distributions. Runtime remaining uses a Peukert-derated capacity, so a VRLA string delivers less at high load than a linear estimate suggests, and a naive runtime readout is optimistic in exactly the situation where it matters | VRLA end-of-life showing as a runtime estimate that falls faster than the load explains, self-test skipped or failed silently, bypass left engaged after maintenance (no protection, and everything looks normal), battery string open circuit found only at the next outage | it_infrastructure, electrical_panel |
Ride-through capability for the OT stack during a sag; the recovery-time input to the IT and cyber operations layer |
| 58 | pwr.meter.thd |
Total harmonic distortion, voltage and current | percent | Harmonic content dominated by the VFD population; the 5th and 7th current harmonics are the characteristic six-pulse drive signature. Distortion rises as more drives run, so THD is coupled to the count of active drives, which makes it a genuine load-mix indicator rather than a static number | Measurement bandwidth too low to capture higher harmonics, aliasing producing phantom harmonics, single-point measurement not representative of the whole panel | electrical_panel, site_power |
Power quality findings tied to drive population; capacitor bank resonance risk when a power factor correction what-if is proposed |
B.6 Safety and compliance (8 types) - severity floor outranks throughput by definition¶
Every entry in this category carries a severity_floor on its safety-relevant failure modes, and the findings contract enforces that a safety finding cannot be ranked below a throughput finding regardless of computed impact.
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 59 | saf.smoke.photoelectric |
Smoke obscuration | percent per m | Photoelectric chambers respond well to smouldering smoke and poorly to fast flaming fires, which is a real and material limitation. Baseline obscuration rises slowly with dust accumulation, which is why the alarm threshold and the chamber contamination trend are separate channels | Dust accumulation raising the baseline until it nuisance-alarms (the dominant real failure in a DC), insect intrusion, chamber contamination sensitivity loss, alarm inhibited during a known dusty operation and never re-enabled | fire_safety, facility_zone |
Fire detection findings; the chamber-contamination trend that predicts a nuisance alarm before it happens |
| 60 | saf.gas.lel_catalytic |
Combustible gas concentration | percent LEL | Catalytic bead sensor at the lead-acid charger bank, where hydrogen off-gasses during equalize charge; the PubChem record for hydrogen gives a lower flammable limit of 4.0 percent by volume, which is what percent-LEL is referenced to. Concentration couples to charger state, ventilation rate, and bank size. The critical physics: catalytic beads are poisoned by silicones and sulphides, and a poisoned bead reads LOW, not high, so the failure is silent and only bump testing finds it. That is why the entry carries a scheduled bump test as a detector | Silicone or H2S poisoning (silent sensitivity loss), flame arrestor blockage slowing response, zero drift with temperature, saturation above the measuring range reading back down through zero | battery_bank, facility_zone |
Hydrogen accumulation findings at the charge bank; a demonstration of why proof testing exists for fail-silent sensors |
| 61 | saf.gas.co_electrochem |
Carbon monoxide concentration | ppm | Electrochemical cell. Baseline couples to propane forklift population and run time and inversely to ventilation. Cross-sensitivity to hydrogen is real and means the CO sensor reads high near the charging bank, which is a cross-sensor artifact the correlation layer must learn rather than a fault | Electrolyte dry-out ending sensor life at roughly 2 years, hydrogen cross-sensitivity false positive, baseline shift with humidity, temperature-dependent sensitivity | facility_zone, fire_safety |
CO exposure findings; forklift fleet electrification business case |
| 62 | saf.estop.circuit |
Emergency stop circuit state, dual channel | state | Dual-channel state machine with cross-monitoring; trip duration is a lognormal, and the reset requires guard-closed and zone-clear guards. Publishes on change with a heartbeat, because a safety input that never republishes is indistinguishable from a dead one | Welded contact stuck armed (the dangerous, silent failure), contact bounce producing chatter, channel discrepancy latching a fault, reset defeated by a jumper (detectable as a reset that happens faster than a human can walk to the button) | safety_system, all machine subsystems |
Machine stop events with cause; the safety severity floor; proof-test scheduling for fail-silent safety devices |
| 63 | saf.guard.interlock |
Machine guard door interlock and safety relay state | state | Coded magnetic or tongue interlock feeding a safety relay. Opening cycles couple to the twin's maintenance and jam-clearing events, so a guard opened far more often than the jam rate explains means someone is working around it | Interlock defeated by a spare actuator taped to the switch (visible as guard-closed during a period the twin knows the machine was accessed), actuator misalignment causing intermittent faults, relay contact welding, guard opened during motion because the stop time exceeds the access time | palletizer_cell, sortation, safety_system |
Guard defeat detection as a compliance finding; the safe-access time calculation feeding the machine layout what-if |
| 64 | saf.proximity.worker_amr |
Separation distance between a worker and the nearest AMR | m | Fused from the UWB tag position and the AMR's own obstacle sensing, so the two sources disagree sometimes and the disagreement is informative. Near-miss events feed a Heinrich-pyramid ratio into the incident model, and their arrival is a Hawkes process because near-misses cluster in place and time (a congested aisle produces bursts, not a uniform rate) | UWB NLOS bias understating distance risk, worker without a tag (invisible to the fused estimate, which is the real gap in every deployment), AMR sensor blind spot behind the payload, latency making the reported separation stale at the moment it matters | agv_amr_fleet, worker_safety |
Near-miss Pareto by location and cause; the AMR speed-versus-separation what-if with an injury-cost term |
| 65 | saf.ppe.vision_compliance |
PPE presence per detected worker | class, confidence | From the CV channel on synthetic frames, and labeled as such everywhere. Detection confidence is a bounded ratio so it is Beta-distributed; the failure modes are model failure modes rather than transducer failure modes, which is why this entry exists in the catalog: it forces the schema to represent an inference channel honestly | Occlusion by a pallet or a turned back, motion blur at aisle speed, class confusion (a bump cap read as a hard hat), lighting domain shift near the dock door, confidence calibration drift after a model update | worker_safety, cv_audit |
PPE compliance findings citing the specific SOP clause (E8); the measured comparison between the classical CV channel and the VLM channel (E29) |
| 66 | saf.fall.wearable |
Worker fall detection from a wearable | event, g | Free-fall detection (sustained near-zero acceleration) followed by an impact peak, then a stillness window. Each stage has its own distribution, and the three-stage structure is what separates a fall from a dropped device, which is the entire engineering problem in fall detection | Dropped device false positive, sitting down hard false positive, a genuine slow slump that never reaches free fall (the false negative that matters most), battery depletion, worker not wearing the device | worker_safety |
Fall incidents with location from the co-located UWB tag; the TRIR-style rate the ESG report consumes |
B.7 Process and chemical (9 types) - feeds batch quality and golden-batch scoring¶
These attach to the upstream factory (component 6a9), specifically the continuous and batch stage feeding discrete forming.
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 67 | prc.ph.glass_electrode |
Process pH | pH | Nernst response, whose ideal slope is ln(10) RT/F and evaluates to 59.16 mV per pH at 25 degC using the CODATA 2022 gas and Faraday constants, so the number is recomputable rather than recalled. Slope degrades with electrode age: a healthy electrode gives 95 to 102 percent of theoretical, and below about 85 percent it is finished. Temperature compensation is mandatory. Response time lengthens as the glass membrane ages, modeled as a first-order lag whose tau grows over the electrode life, and that slowing is measurable before the slope failure is | Slope decay (the primary ageing mode), reference junction poisoning shifting the offset, coating slowing the response, dry-out during a shutdown, calibration performed at the wrong temperature | factory_reactor, cip_system |
Batch pH profile for golden-batch scoring; electrode replacement prediction from the response-time trend rather than from a fixed calendar |
| 68 | prc.conductivity.toroidal |
Solution conductivity | mS/cm | Toroidal (inductive) sensor, so no wetted electrodes to foul. Temperature coefficient of roughly 2 percent per degC for most aqueous solutions means uncompensated conductivity is nearly meaningless, and the entry carries the compensation reference explicitly. Cell constant shifts if the sensor is installed too close to a pipe wall | Coating changing the effective cell constant, installation too close to the wall (a systematic error present from day one), temperature compensation applied with the wrong coefficient for the solution, air bubbles in the bore | factory_reactor, cip_system |
CIP rinse endpoint verification; concentration tracking for the batch recipe |
| 69 | prc.viscosity.inline_vibrating |
Process viscosity | cP | Vibrating-element viscometer. Viscosity is strongly temperature dependent by an Arrhenius relation, ln(mu) = A + B/T, so a reading without its temperature is not a measurement, and the golden-batch comparison uses the temperature-corrected value. Shear rate dependence for non-Newtonian product is carried as a declared model type |
Temperature correction using the wrong constants for the product, buildup on the vibrating element raising the apparent viscosity, flow-induced error at high line velocity, non-Newtonian shear thinning misread as a formulation change | factory_mixer, factory_reactor |
Viscosity profile in the golden-batch score; the cure-stage endpoint that drives batch cycle time |
| 70 | prc.flow.coriolis |
Mass flow and fluid density | kg/h, kg/m3 | True mass flow, accuracy 0.1 to 0.5 percent of rate. Zero drift with temperature is the dominant error and it matters most at low flow, since a fixed zero offset is a growing percentage error as rate falls. Entrained air is diagnosable: drive gain spikes and density reads low simultaneously, and that pair of symptoms together is the signature | Zero drift at low flow, two-phase flow (drive gain spike plus low density), coating changing the tube stiffness, external vibration coupling at the tube resonance, mounting stress after a pipe modification | factory_reactor, factory_mixer |
Recipe mass balance and material yield through genealogy; the reference in a redundant-flow measurement comparison |
| 71 | prc.flow.magnetic |
Volumetric flow of a conductive fluid | m3/h | No moving parts, no pressure drop, requires conductivity above roughly 5 uS/cm so it works on water-based product and fails silently on a solvent. Electrode coating produces a slow negative drift; an empty pipe reads noise rather than zero, which is why empty-pipe detection is a separate channel | Electrode coating (slow negative drift), empty pipe reading noise, conductivity dropping below the sensor's floor during a solvent flush, grounding ring corrosion introducing a common-mode error | factory_reactor, cip_system, utility_water |
Volumetric balance cross-check against the Coriolis mass flow, which is a genuine two-technology measurement comparison the MSA layer can run |
| 72 | prc.level.radar_fmcw |
Vessel level, non-contact | m | FMCW radar. Reflected amplitude depends on the product dielectric constant, so a low-dielectric product returns a weak echo. Agitator blades produce false echoes that must be mapped out during commissioning, and a false-echo map that goes stale after an internals change is a real and common failure | Agitator false echo after an internals change, condensation or product buildup on the antenna attenuating the signal, low-dielectric product losing the echo entirely, multiple-reflection ghost at exactly twice the true distance | factory_reactor, bulk_tank |
Vessel inventory and batch charge verification; the level reference for the redundancy comparison against ultrasonic |
| 73 | prc.level.ultrasonic |
Vessel or sump level, non-contact | m | Time of flight. The speed of sound in air follows c = 331.3 sqrt(1 + T/273.15) m/s, which differentiates to about 0.17 percent per degC near room temperature, so temperature compensation is mandatory and an uncompensated sensor drifts with the diurnal cycle in a way that looks exactly like a slow leak. Foam absorbs the pulse and produces a lost echo rather than a wrong reading, which is at least honest. There is a dead zone near the transducer | Foam absorption causing lost echo, temperature compensation missing or using the wrong reference, vapor changing the speed of sound, condensation on the transducer face, target within the blanking distance | bulk_tank, sump, utility_water |
The second technology in a deliberate redundant level pair; the disagreement between radar and ultrasonic on the same vessel is a first-class MSA and cross-check exercise |
| 74 | prc.do.optical |
Dissolved oxygen | mg/L | Luminescent lifetime measurement, which unlike a Clark cell has no flow dependence and consumes no oxygen. The luminophore cap has a stated 12 to 24 month life and degrades gradually, so the failure is a slow sensitivity loss rather than a step. Temperature and salinity compensation are required; solubility falls with temperature, so an uncompensated reading tracks the diurnal cycle | Luminophore cap ageing (slow sensitivity loss), cap fouling by biofilm, temperature compensation missing, ambient light leakage through a damaged cap, salinity compensation using a default value | factory_reactor, wastewater |
Aerobic process control in the batch stage; an environmental discharge parameter for the ESG report (E39) |
| 75 | prc.corrosion.er_probe |
Cumulative metal loss and corrosion rate | mils, mpy | Electrical resistance probe. The raw signal is cumulative metal loss and is monotone non-decreasing, property-tested as such; the useful quantity is its derivative, and differentiating a noisy monotone signal is a real estimation problem that the trend layer has to solve properly rather than by first differences. The element has a finite life and must be replaced when consumed. Probe temperature affects resistance directly, so compensation is essential | Element consumed (end of life, reads flat and looks like the corrosion stopped), temperature compensation error swamping the real signal, probe in a location unrepresentative of the worst corrosion, deposit on the element insulating it | factory_piping, utility_water, wastewater |
Asset integrity trending; the inspection-interval what-if that trades inspection cost against failure risk |
B.8 Structural (5 types) - facility integrity under snow load¶
| # | id | Measures | Units | Signal model | Primary failure modes | Twin subsystem | Capability unlocked |
|---|---|---|---|---|---|---|---|
| 76 | str.strain.column_vw |
Strain in a building column or primary member | microstrain | Vibrating-wire gauge: the measurement is the resonant frequency of a tensioned wire, which is why it is exceptionally stable over years and the technology of choice for structures. A built-in thermistor supplies the thermal correction, and the uncorrected signal has a large annual thermal cycle that must be removed before any trend claim is made | Wire slack after an overload event (permanent zero shift), thermistor failure removing the correction, lead-wire damage, load path change after a facility modification making the historical baseline invalid | structural_frame, facility_zone |
Structural load trending under snow accumulation; the load path check after a rack or mezzanine change |
| 77 | str.crack.displacement |
Crack width across a monitored crack | mm | LVDT or vibrating-wire crackmeter with sub-0.01 mm resolution. Seasonal thermal cycling produces an annual sine of the same order as a slow real trend, so the seasonal component must be modeled and removed before a growth rate is estimated, and doing that wrong is how facilities either panic annually or miss a real crack | Seasonal cycle mistaken for growth, anchor debonding, sensor spanning a repaired crack so it measures the repair rather than the structure, LVDT core binding | structural_frame |
Crack growth rate with a confidence interval; structural findings escalated to the safety severity floor |
| 78 | str.load.roof_snow |
Roof load from snow, water, and equipment | kPa | Load pad or a snow pillow. Load couples to the shared weather state (E40): accumulation from snowfall, densification from a freeze-thaw cycle (which raises load without adding depth, the mechanism behind most roof collapses), and shedding from melt. Wind scour and drift make the load spatially non-uniform, so a single sensor systematically under-reads a drift zone, and sensor placement at valleys and behind parapets is part of the model | Single-point measurement missing a drift zone, ice bridging the pad so load transfers around it, drain blockage adding ponded water the pad does not see, thermal expansion of the pad structure | structural_roof, facility_zone |
The snow-load scenario: roof capacity margin, the evacuation and snow-removal decision, and its production cost |
| 79 | str.tilt.rack_upright |
Rack upright inclination | deg | MEMS inclinometer with 0.01 degree resolution. Two mechanisms with different signatures: a forklift strike produces a step change plus a co-timed shock event, while overload creep produces a slow monotone lean. Separating them is the diagnostic, and it drives different corrective actions | Sensor mounting drift on the upright, temperature-induced apparent tilt, sensor on a beam rather than an upright measuring deflection instead of lean, a strike below the sensor position that does not register as tilt | structural_rack, storage |
Rack damage findings; the aisle-by-aisle damage rate that drives the rack protection investment what-if |
| 80 | str.impact.rack_leg |
Impact events on a rack leg or column guard | g, ms | Threshold-triggered accelerometer. Arrivals are Poisson with a rate coupled to forklift and AMR traffic density in that aisle from the UWB layer, so a high-traffic aisle genuinely accumulates more strikes and the model reproduces that rather than assuming a uniform rate. Peak magnitude is generalized-Pareto tailed and not truncated | Threshold set above the damaging impact level (silent misses), sensor knocked off by the impact it was recording, adjacent rack coupling registering a neighbor's strike, battery depletion in a location nobody inspects | structural_rack, agv_amr_fleet |
Strike-to-damage correlation with the inclinometer; traffic-pattern findings that feed the slotting and layout what-ifs |
B.9 Count¶
Row numbers run continuously from 1 to 80 across the eight tables with no gaps.
One candidate type, a pick-face illuminance sensor, is sequenced rather than dropped. Worker comfort is already served by the temperature and humidity pair through a derived heat index, and the remaining use, correlating pick error with lighting, needs the ergonomics layer before it has a consumer. It is carried in ROADMAP.md against component 6a10 and it is not counted here, because the count in this section is the count of implemented entries.
Counts taken directly from the tables above:
| Category | Rows in table | Count |
|---|---|---|
| B.1 Industrial equipment | 1 to 14 | 14 |
| B.2 Environmental and facility | 15 to 21 | 7 |
| B.3 Warehouse and logistics | 22 to 36 | 15 |
| B.4 Transportation and fleet | 37 to 48 | 12 |
| B.5 Electrical and power | 49 to 58 | 10 |
| B.6 Safety and compliance | 59 to 66 | 8 |
| B.7 Process and chemical | 67 to 75 | 9 |
| B.8 Structural | 76 to 80 | 5 |
| Total | 80 |
The catalog ships 80 implemented types, at the top of the 60 to 80 target range the source sets. test_category_counts_match_catalog recomputes every row of this table from the entries on disk and fails on any disagreement, counting only entries with status: implemented, so a planned entry can never inflate a published number.
docs/design/iot-fleet.md section 3.2 publishes a different split of 80 across the same eight categories. Both totals are 80 and neither is arithmetic error; they are two designs for what counts as one type, and OQ-11 records the conflict rather than papering over it.
C. Failure signatures by class¶
C.1 Class profiles¶
Every entry inherits one profile. The profile supplies the universal modes with class-appropriate defaults so no entry restates them.
Each universal mode name below is a shorthand for a fault id in docs/design/variability-and-faults.md section C.4, which owns the injectors. The mapping is one to one and CI checks it, so a mode named in a profile always has something that can inject it.
| Universal mode | Fault id |
|---|---|
drift |
F-DEV-DRIFT |
stuck_at |
F-DEV-STUCK |
dropout |
F-DEV-DROP |
calibration_loss |
F-DEV-CALIB |
noise_inflation |
F-DEV-NOISEUP |
clock_skew |
F-DEV-CLOCKDRIFT |
crash_loop |
F-DEV-CRASHLOOP |
duplicate_event |
F-DEV-DUPREAD |
degraded_read_rate |
F-DEV-READRATE |
spurious_event |
F-DEV-GHOST |
model_version_skew |
F-DEV-FIRMWARE |
Four profile modes have no fault id yet: bias_step, saturation, transition_loss, and array_dead_pixel. Each is a registration request against the fault catalog on the same terms as a distribution family, and an entry whose profile supplies an unregistered mode cannot reach status: implemented. OQ-12 tracks them.
| Profile | Applies to | Universal modes supplied |
|---|---|---|
continuous_analog |
Analog scalars: temperature, pressure, level, current, strain, load | drift, stuck_at, dropout, calibration_loss, bias_step, noise_inflation, saturation, clock_skew, crash_loop |
spectral_vector |
Vibration bands, thermal arrays, spectra | all of the above plus band_decoherence, channel_dropout, array_dead_pixel |
probabilistic_event |
RFID, barcode, photo eye, scan events | dropout, duplicate_event, degraded_read_rate, spurious_event, clock_skew, crash_loop |
discrete_state |
E-stop, guard, door, UPS mode, reefer mode | stuck_at, chatter, dropout, clock_skew, crash_loop, transition_loss |
electrochemical |
pH, DO, CO, LEL, conductivity | drift, sensitivity_decay, response_slowdown, end_of_life, poisoning, dropout, calibration_loss |
optical |
PM, DO optical, barcode imager, photo eye, thermal array, CV | fouling, ambient_saturation, source_ageing, alignment_drift, plus the analog universals |
rf_positional |
RFID, UWB, BLE, GNSS | multipath_bias, nlos_bias, interference, antenna_degradation, identity_collision, dropout |
counter_totalizer |
kWh, odometer, engine hours, metal loss, mass throughput | rollover, reset_loss, accumulation_gap, backfill_disorder, plus monotonicity violation as a hard failure |
inference_channel |
CV-derived channels (PPE compliance) | class_confusion, occlusion, domain_shift, confidence_miscalibration, model_version_skew |
C.2 Signature table¶
| Signature | Affected classes | What it looks like in the data | Layer expected to catch it |
|---|---|---|---|
| Drift | all analog, electrochemical, structural | Slow monotone or mean-reverting movement of the mean with unchanged variance. Against a redundant sensor, the residual walks | MSA stability study; SPC EWMA on the residual against a reference channel |
| Stuck-at | all | Variance collapses to zero (or to the quantization step) while the twin state that drives the channel continues to move. The value can be plausible, which is why plausibility checks alone miss it | Fleet health variance monitor; twin-divergence detector |
| Dropout | all | Missing samples against the declared interval; a gap distribution rather than a single gap. Distinguish a device dropout from a broker outage by whether siblings on the same edge node also stopped | Fleet health heartbeat monitor; broker session tracking |
| Calibration loss | all analog, electrochemical | Gain error (a proportional divergence that grows with signal magnitude) or offset error (a constant shift). Gain and offset are separable by regressing against a reference over a range | MSA linearity and bias study; scheduled calibration audit |
| Bias step | all analog | A discontinuity with no corresponding twin state change. Frequently follows a maintenance event, which is why the change log is correlated against the finding | SPC Western Electric rule 1 and Nelson rule 3; change-correlation in the CMMS queue |
| Noise inflation | all analog | Mean unchanged, variance up. Often the first sign of a loose connection or a failing amplifier | I-MR chart moving-range component; MSA repeatability study |
| Saturation / rail | analog, spectral | The value pins at a limit and stops responding. Distinguish from a genuine extreme by checking whether the value sits exactly at the ADC full scale | Plausibility validator at ingest; fleet health signature classifier |
| Clock drift / timestamp skew | all | Samples arrive out of order, or a device's inter-sample interval diverges from sim-time. Cross-device event ordering breaks, which corrupts genealogy | Historian ingest ordering check; Sparkplug sequence-number gap detection |
| Crash loop | all | Repeated birth certificates from the same edge node at a high rate; sequence number resets to zero repeatedly | Sparkplug NBIRTH rate monitor; fleet health uptime score |
| Duplicate reads | probabilistic_event, rf_positional | The same identity read twice within a physically impossible interval, or read at two portals at once | Deduplication at ingest; genealogy consistency check |
| Degraded read rate | probabilistic_event, rf_positional | Read rate per pass falls while the tag population and product mix hold constant. Per-antenna decomposition separates a physics cause from a device cause | Read-rate control chart by portal and by antenna port; RF physics diagnostic (E46) |
| Monotonicity violation | counter_totalizer | A totalizer decreases. Always a fault, never physical | Property test in CI; hard ingest rejection with a critical finding at runtime |
| Accumulation gap | counter_totalizer | A totalizer resumes at the right value after an outage but the interval deltas do not sum to the difference | Historian reconciliation job |
| Sensitivity decay | electrochemical, optical, spectral | Response amplitude to a known stimulus falls over months. The fail-silent class: the reading looks fine and is too low | Scheduled bump test or span check; MSA stability study over a long window |
| Response slowdown | electrochemical, thermal | The time constant lengthens. Step responses become sluggish; the reading lags the twin state by a growing interval | Cross-correlation lag estimation against the twin state; the tau trend is its own PdM channel |
| Poisoning | electrochemical (catalytic bead especially) | A permanent, irreversible sensitivity loss with no visible signature at all in normal operation | Scheduled bump test only. This is the entry that justifies proof testing existing at all |
| Fouling | optical, wetted process | Baseline shift plus reduced dynamic range, often with a step recovery after a cleaning event, which makes the cleaning log a valuable label source | Baseline trend versus cleaning event log; excess gain margin monitor |
| Multipath / NLOS bias | rf_positional | A strictly positive, spatially structured position or range error. Never symmetric | Anchor geometry residual analysis; UWB-versus-AMR-odometry cross-check |
| Interference | rf_positional | Read rate or RSSI degradation correlated in time with an interfering source (VFD switching, a 2.4 GHz load). The correlation is the evidence | Cross-correlation of read rate against drive state; spectrum occupancy trend |
| Antenna degradation | rf_positional | VSWR rise on one port; read range falls as the fourth root of nothing simple, and specifically as the square root of radiated power for a forward-link-limited system. One port declines while siblings hold | Per-antenna read-rate control chart; VSWR trend where the reader reports it |
| Chatter | discrete_state | Rapid state oscillation faster than any physical process. Floods the findings stream if unhandled, which is exactly what alarm rationalization exists to prevent | Alarm rationalization debounce and shelving; chatter-rate metric per device |
| Transition loss | discrete_state | An expected state change never appears; the device reports the same state for longer than the twin says is possible | Absence-of-expected-transition detector; scheduled proof test |
| Welded contact | discrete_state, safety | Stuck-at in the safe-looking direction. The reading is the good one, which is why it is only detectable by proof testing or by the absence of expected transitions | Scheduled proof test; absence-of-expected-transition detector. Severity floor: critical |
| Cross-read / false positive | probabilistic_event, rf_positional | An event appears on the wrong equipment topic. The identity is real; the location is wrong | ASN reconciliation; CV count cross-check; dwell-time plausibility |
| Cross-sensitivity artifact | electrochemical | One sensor responds to a species it is not measuring (a CO cell responding to hydrogen). Not a fault, and treating it as one is the error | Multi-sensor correlation model; documented in the entry so the correlation layer expects it |
| Emissivity / compensation error | thermal array, conductivity, ultrasonic level, viscosity, TPMS | A systematic error introduced by a missing or wrong compensation constant. Invisible without a reference, and constant so it does not trend | Two-technology cross-check; MSA bias study against a reference |
| Array dead pixel | spectral_vector, thermal array | One element of a vector holds constant while its neighbors move | Per-element variance monitor; array health score |
| Band decoherence | spectral_vector | Every defect band collapses toward the noise floor simultaneously and the machine appears to improve overnight. Sudden improvement across all bands is a measurement failure, not a repair | Fleet health signature classifier; speed-reference availability check |
| Class confusion / domain shift | inference_channel | Detection distribution shifts after a lighting or layout change with no corresponding physical change; confidence calibration degrades | Model drift monitor in the MLOps layer (E43); CV-versus-RFID count disagreement |
| Model version skew | inference_channel | Two devices running different model versions produce systematically different rates for the same scene | Fleet configuration compliance audit (E48) |
| Store-and-forward replay disorder | all, especially telematics | A burst of backdated messages after a reconnect, out of order and with historical timestamps | Historian ingest ordering; Sparkplug is_historical flag handling |
C.3 Detection responsibility¶
Each signature names its owning layer so no signature is unowned:
| Layer | Owns |
|---|---|
| Ingest plausibility validator | Range violations, monotonicity violations, saturation at exact full scale, timestamp ordering |
| Fleet health and device registry | Dropout, crash loop, heartbeat loss, uptime scoring, variance collapse, per-device health score with FMEA severity-occurrence-detection |
| Measurement system analysis (Gage R&R) | Bias, repeatability, reproducibility, linearity, stability. Owns drift, calibration loss, and sensitivity decay over long windows |
| SPC (LSS engine) | Bias steps, noise inflation, trends, and every Western Electric and Nelson rule violation on any channel or residual |
| Predictive maintenance trend layer | Time-to-threshold on degrading assets; distinguishes asset degradation from sensor degradation using the entry's confusable_with list |
| Cross-sensor consistency | Redundant pairs (radar versus ultrasonic level, Coriolis versus magmeter, RFID versus CV count, UWB versus AMR odometry) |
| Twin divergence detector | Stuck-at where the value stays plausible; any channel that stops tracking the twin state it is coupled to |
| Alarm rationalization | Chatter, flapping, duplicate findings, severity ranking, shelving |
| Scheduled proof and bump testing | Fail-silent modes: welded contacts, catalytic bead poisoning, transition loss. The only detector for these, which is the point |
| Fleet configuration compliance audit (E48) | Model version skew, config drift, firmware inconsistency |
D. UNS and Sparkplug B mapping¶
D.1 The ISA-95 topic hierarchy¶
| Level | Source | Example | Rules |
|---|---|---|---|
enterprise |
Fixed per deployment, from facility.yaml |
twinflow |
Single literal, lowercase |
site |
Facility profile | site-a |
One per facility; multi-site (E13) bridges across this level |
area |
Facility profile | receiving, storage, outbound, factory |
ISA-95 area |
line |
Facility profile | line-1, dock-bank-a |
ISA-95 work center |
equipment |
Device instance binding | conv-drive-02 |
Resolved from attaches_to.instance_naming |
parameter |
Catalog entry uns.parameter |
vibration/band_spectrum/de |
May contain additional slashes; this is the only level allowed to be multi-segment |
Charset for every level: [a-z0-9-]+, plus / in the parameter level only. No wildcards, no empty levels, no leading or trailing slash, no + or #. Maximum total topic length 512 bytes, asserted in CI.
Every topic in this section is published through the Network port, the MQTT-shaped one that carries retain, quality of service, last will, and wildcard subscribe. D-08 separates that port from EventBus, which is subject-addressed fan-out with none of those, and the device fleet never uses EventBus. At the enterprise tier the broker bridges MQTT at the operational edge into the partitioned log at the information layer, so a catalog entry's uns.qos and uns.retain fields keep their meaning at every tier.
D.1.1 Mobile assets do not fit ISA-95 cleanly¶
Trucks, trailers, and AMRs are not in an area or on a line. The catalog resolves this by binding mobile assets to a dedicated area under the site they are dispatched from:
twinflow/site-a/fleet/{fleet_group}/{asset_id}/{parameter}
twinflow/site-a/fleet/tractors/tractor-114/cargo/temperature/pulp
twinflow/site-a/fleet/amr/amr-07/battery/soc
This keeps the six-level shape intact while being honest that fleet is an area only by convention. It is flagged as OQ-3 because a defensible alternative exists.
D.2 Worked example: one catalog entry to N topics¶
Catalog entry eq.vib.band_spectrum with binding: per_equipment, positions: [de, nde], cardinality.default_per_binding: 2.
Facility profile declares three conveyor drives in receiving/line-1. The instantiator produces:
twinflow/site-a/receiving/line-1/conv-drive-01/vibration/band_spectrum/de
twinflow/site-a/receiving/line-1/conv-drive-01/vibration/band_spectrum/nde
twinflow/site-a/receiving/line-1/conv-drive-02/vibration/band_spectrum/de
twinflow/site-a/receiving/line-1/conv-drive-02/vibration/band_spectrum/nde
twinflow/site-a/receiving/line-1/conv-drive-03/vibration/band_spectrum/de
twinflow/site-a/receiving/line-1/conv-drive-03/vibration/band_spectrum/nde
Six topics from one catalog entry, zero lines of code, and the same entry produces different topics in a different facility profile without modification.
D.3 Sparkplug B mapping¶
The UNS topic above is the semantic address and is what a human or a subscriber reasons about. Sparkplug B has its own topic namespace, and both are published: the Sparkplug payload carries the UNS path as the metric name, so the two views stay reconciled.
Everything this subsection states about Sparkplug is read from the Eclipse Sparkplug Specification, version 3.0.0, dated 2022-11-16, which is a freely published specification under the Eclipse Public License. Section references below point into that document. It is a tier-A source under A.7, recorded as SRC-SPARKPLUG-3-0-0.
D.3.1 Sparkplug topic namespace¶
| Element | Derivation | Example |
|---|---|---|
group_id |
{enterprise}:{site}:{area} |
twinflow:site-a:receiving |
message_type |
NBIRTH, NDATA, NDEATH, DBIRTH, DDATA, DDEATH, NCMD, DCMD |
DDATA |
edge_node_id |
The gateway serving the line; tier 1 in the E36 compute placement model | gw-line-1 |
device_id |
The physical device instance | vib-conv-drive-02 |
Metric name inside the payload is the UNS remainder below the group:
D.3.2 Datatype selection¶
The datatype is derived from measurement.channel_shape and the value semantics, not chosen per entry by hand. Enum values are those of the DataType enumeration in the specification's payload definition.
| Channel shape and semantics | Sparkplug datatype | Enum |
|---|---|---|
| Scalar physical quantity where single precision is enough | Float |
9 |
| Scalar physical quantity needing double precision (GNSS latitude and longitude, cumulative totalizers) | Double |
10 |
| Boolean state | Boolean |
11 |
| Enumerated state (reefer mode, UPS mode, e-stop state) | String |
12 |
| Monotone counter | UInt64 |
8 |
| Discrete count in a window | UInt32 |
7 |
| Fixed-length vector (vibration bands) | FloatArray |
30 |
| Matrix (thermal array) | FloatArray flattened with rows and cols carried as metric properties, or DataSet |
30 or 16, see OQ-1 |
| Composite event (an RFID read with EPC, port, RSSI, phase) | Template |
19 |
| Timestamped instant | DateTime |
13 |
The full enumeration runs Unknown at 0, Int8 through Text at 1 to 14, UUID at 15, DataSet at 16, Bytes at 17, File at 18, Template at 19, PropertySet at 20, PropertySetList at 21, and the array types at 22 to 34, of which FloatArray is 30, DoubleArray 31, BooleanArray 32, StringArray 33, and DateTimeArray 34. The code refers to these by name through a table generated from the specification's protobuf definition, never by a literal written by hand, and test_datatype_enum_matches_generated_table fails if the two disagree.
D.3.3 Alias assignment at birth¶
Sparkplug aliases exist so a DDATA message can omit the metric name and send a small integer instead. The specification makes an alias, where used, unique across the edge node's entire set of metrics, requires a birth message to carry both name and alias, and requires NDATA, DDATA, NCMD, and DCMD to carry the alias with the name excluded. The policy below is what this repository adds on top so that a replayed run reproduces the same payloads:
- At
DBIRTH, collect every metric this device will publish during the session. - Sort metric names with a stable byte-wise ordering over their UTF-8 encoding. The sort is explicit rather than incidental, because a set iterated in hash order would produce a different alias map in a second process (D-03).
- Assign aliases starting at 1, in sorted order. Alias 0 is never assigned, so a zero alias is always a bug and is detectable as one. That is a repository rule, not a specification rule.
- The
DBIRTHpayload carries every metric with both its name and its alias, plus its full property set (engUnit,engLow,engHigh,description,sensor_type_id,sensor_type_revision,Quality). - Every subsequent
DDATAcarries the alias only. - Aliases are valid for the life of the session. A rebirth, whether after
DDEATH, a reconnect, or aNode Control/Rebirthcommand, may reassign, so a consumer that caches aliases across a birth boundary is broken. The historian ingest path is tested against a rebirth with a changed metric set.
The payload byte identity this produces is a same-platform claim only, per D-05. Alias assignment and metric ordering are integer and string operations and are identical everywhere; the floating-point values inside the payload are not, so the cross-platform claim is value equivalence within the tolerance the cross-platform job measures.
D.3.4 Sequence, session, and quality¶
bdSeq in NBIRTH and NDEATH ties a session together, and the specification requires the will-message bdSeq to increment on each connect and wrap to 0 after 255.
An NBIRTH payload must carry a seq between 0 and 255 inclusive, and that value becomes the starting number for the session; every following message carries one more than the previous, wrapping back to zero after 255. An edge node is not required to start a session at zero, so a consumer that assumes seq 0 in NBIRTH is reading a rule the specification does not state. The historian anchors on the NBIRTH value it receives rather than on an assumed origin, and test_nbirth_seq_is_not_assumed_zero runs a session that starts at a non-zero seq and asserts the gap detector stays quiet.
A gap in seq is a data-loss signature and feeds the fleet health layer directly.
Metric quality rides in the property set under the key Quality, whose value is a signed 32-bit integer and must be one of three codes: 0 for BAD, 192 for GOOD, and 500 for STALE. The property is optional and is required only when quality is not GOOD. This catalog writes it on every metric anyway, because an absent property and a GOOD property are indistinguishable to a consumer that has to decide whether the producer knows about quality at all.
D.3.5 Worked payload sketch¶
For DBIRTH of vib-conv-drive-02:
topic: spBv1.0/twinflow:site-a:receiving/DBIRTH/gw-line-1/vib-conv-drive-02
payload:
timestamp: <sim-time ms since epoch>
seq: 3
metrics:
- name: "line-1/conv-drive-02/vibration/band_spectrum/de"
alias: 1
datatype: FloatArray # enum 30
value: [0.42, 0.13, 0.05, 0.07, 0.06, 0.04, 0.03, 0.19]
properties:
engUnit: "mm/s"
engLow: 0.0
engHigh: 45.0
Quality: 192
sensor_type_id: "eq.vib.band_spectrum"
sensor_type_revision: 1
band_labels: "ord_1x,ord_2x,ord_3x,bpfo,bpfi,bsf,ftf,broadband_residual"
uns_path: "twinflow/site-a/receiving/line-1/conv-drive-02/vibration/band_spectrum/de"
- name: "line-1/conv-drive-02/vibration/band_spectrum/nde"
alias: 2
...
The seq of 3 is not special. It is one more than whatever the previous message in this session carried, and the session's origin was set by the NBIRTH.
Subsequent DDATA for the same metric carries alias: 1 and the value, and nothing else.
D.4 The JSON fallback profile¶
Sparkplug B is the default payload encoding, and the repository also ships a json_fallback profile that publishes a plain JSON payload on {uns_topic}/json. Two reasons: the walking skeleton in Phase 1 comes before the Sparkplug milestone (E3), and a reader browsing an MQTT client for ninety seconds must be able to read a payload without a protobuf decoder. The profile is a config switch, and the catalog entry does not change. CI runs the topic-uniqueness and round-trip tests under both profiles.
D.5 Events this section publishes¶
D-07 settles the envelope before Phase 0 freezes schemas, and every section that declares an event carries it. Sensor telemetry is not an event log entry, but the catalog layer does publish events, and those events are subject to the envelope.
Every event below carries run_id, producer_id, seq, sim_ts, schema_version, and the payload. The sequence number is dense per (run_id, producer_id) and never global, because the garage tier already runs several containers plus the Rust agent and no allocator for a global counter exists. The canonical total order is (sim_ts, producer_id, seq), and the replay reader and the pagination cursor both use it.
| Subject | Producer | Payload | Consumed by |
|---|---|---|---|
sensor.catalog.loaded |
catalog loader | Entry count by status and category, catalog content hash, schema version, the sorted list of type ids | Run manifest hashed core, fleet registry |
sensor.device.provisioned |
device instantiator | device_id, type_id, type_revision, UNS path, edge node, the drawn unit_offset |
Fleet registry, MSA layer |
sensor.type.deprecated |
catalog loader | type_id, superseding type_id, the run in which the deprecation takes effect |
Historian schema migration |
sensor.fault.injected |
fault injector | catalog_id, instance_id, device_id, onset sim_ts, effect parameters |
Ground-truth writer, scoring harness |
sensor.plausibility.violated |
ingest validator | device_id, channel, observed value, the span it left, and which bound |
Findings stream, fleet health |
sensor.provenance.degraded |
catalog loader | type_id, the parameter group, its tier, and the source id |
Release gate; blocks a release on tier D |
sensor.catalog.loaded carries the sorted type id list rather than a set, because that payload reaches the log hash and set iteration order is not stable across processes (D-03). The catalog content hash is computed over entries sorted by type_id, and test_catalog_hash_is_load_order_independent loads the same catalog from a shuffled directory listing and asserts the hash is unchanged.
No event on this list carries a wall-clock value. The provenance sidecar carries started_wall_utc and the platform fingerprint, and the hashed core carries the catalog content hash instead (D-01). The catalog loader is not one of the four places allowed to read a wall clock (D-02).
D.6 The Rust agent draws from the same streams¶
A Rust agent that generated its own randomness would put a hole at exactly the boundary this project is proudest of. D-06 rules that the agent derives its stream from the run seed and its device id using the same name-addressed derivation the Python side uses, specified byte for byte in docs/design/variability-and-faults.md section A.1 in terms of a named hash function and a named bit generator rather than in terms of whichever library either language happens to have.
For the catalog that means three things. The agent reads the same entry, so the stream names in signal_model.rng_streams are the agent's stream names too. The agent's unit_offset for a device comes from provision.sensor.{device_id}.unit_offset, which is drawn at provisioning on the Python side, so an agent restart does not redraw it. A cross-language conformance test asserts that the two implementations produce identical draws for the same stream name and seed, and the catalog entry for whichever type the Rust agent serves is the fixture that test uses.
E. README table¶
Compact form for the repository README, with counts.
## Implemented sensor types
80 physics-modeled sensor types, defined as data in `catalog/sensors/`. Each entry
declares its signal model, failure modes, UNS topic, Sparkplug metric, twin attachment,
and the capability it unlocks. Adding a type is a YAML file, not a code change.
| Category | Types | Examples | What it unlocks |
| -------------------------- | ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Industrial equipment | 14 | Order-tracked vibration bands, acoustic emission, motor current signature, oil condition | Predictive maintenance with time-to-threshold estimates |
| Environmental and facility | 7 | Ambient temperature, humidity, dew point, CO2, particulate, water leak, thermal array | Facility health and worker comfort findings |
| Warehouse and logistics | 15 | RFID portals and handhelds, scan events, shelf and dock scales, UWB, BLE, conveyor speed and load | The DC material flows and lot genealogy |
| Transportation and fleet | 12 | GNSS, IMU, TPMS, reefer status, cargo temperature, shock, tilt, telematics gateway | Cold-chain integrity, excursions traced through genealogy |
| Electrical and power | 10 | Three-phase voltage and current, power and power factor, smart meter, battery SOC/SOH, UPS | Energy KPIs and AMR charging physics |
| Safety and compliance | 8 | Smoke, LEL, CO, e-stop, guard interlock, worker proximity, PPE vision, fall detection | Worker-safety findings with a severity floor above throughput |
| Process and chemical | 9 | pH, conductivity, viscosity, Coriolis and magnetic flow, radar and ultrasonic level, DO, corrosion | Batch quality and golden-batch scoring |
| Structural | 5 | Column strain, crack width, roof load, rack tilt, rack impact | Facility integrity under snow load |
| **Total** | **80** | | |
The design pattern scales to the full 500-type industrial sensor landscape, and the
schema is the argument. See `docs/design/sensor-catalog.md` for the schema, the
scaling argument, and the limits that argument runs into.
Parameter values come from published standards, datasheets, and derivations, each
recorded with its source in `catalog/provenance/sources.yaml`. No parameter is
fitted to any real installed base, and no client data is in this repository.
CI regenerates the counts column from the catalog directory and fails if the README drifts. It counts only entries with status: implemented, so the published number is the implemented number and a planned entry cannot inflate it.
F. The scaling argument¶
F.1 Why type 300 is a config entry¶
Seven properties of the schema, each the reason a specific kind of code change is unnecessary.
F.1.1 The generator composes registered parts rather than subclassing¶
There is no VibrationSensor class. A generator engine reads signal_model and assembles a pipeline in a fixed order: resolve the coupling reads from the twin state broker, evaluate the baseline expression, add the per-device unit offset, apply each noise family, apply each drift family, apply the response dynamics, apply quantization, apply any active failure effects, then emit. A new type names parts and supplies parameters. The engine does not know what a bearing is.
F.1.2 The family catalog is small and close to closed¶
The nineteen families the distribution catalog already carries, plus the six this section requests in A.4.4, cover the physics of all eighty types. They are not sensor-specific: they are standard distributions and stochastic processes. A 500-type catalog does not need 500 families, because a pH electrode's slope decay and a battery's capacity fade are both linear_ramp with different parameters, and a piezoelectric sensitivity loss and an electrolyte dry-out are both arrhenius_accelerated. The families are the physics vocabulary and the entries are sentences in it.
F.1.3 Topic and metric mapping are derived, never written¶
A catalog entry declares a binding level and a parameter suffix. The instantiator walks facility.yaml, resolves the ISA-95 levels, and produces topics. The Sparkplug datatype is derived from the channel shape. The alias is derived from the sorted metric set at birth. Nobody types a topic string, so nobody can typo one, and adding a type cannot collide with an existing topic without CI catching it.
F.1.4 Twin coupling is declarative and validated¶
The coupling.reads list is the entire interface between a sensor and the simulation. The twin exposes a registered state vector with names and units; the sensor names what it reads. The twin has no knowledge of sensors at all. Adding a sensor never edits the twin, and the CI check that every coupling read resolves means the two cannot drift apart silently.
F.1.5 Failure modes inherit from class profiles¶
A new type gets drift, stuck-at, dropout, calibration loss, clock skew, and crash loop by naming its profile. It only writes the modes that are specific to its physics, which is typically two to four. That is the difference between a 200-line entry and a 40-line one, and it is why the marginal cost of a type falls as the catalog grows rather than rising.
F.1.6 Capability wiring is a checked graph edge¶
unlocks and consumed_by form a bipartite graph between types and subsystems. CI walks it. A type with no consumer fails the build, and a capability with no producer fails the build. This is the mechanism that keeps a 500-type catalog from becoming 430 types of padding: a type that unlocks nothing cannot be merged.
F.1.7 Validation is generated from the declaration¶
The entry declares its distribution families, its bounds, and its budget. The test suite reads those declarations and generates the goodness-of-fit test, the property-based bounds test, the topic uniqueness case, and the budget assertion. Writing a new type writes its own tests. Nobody has to remember to add them, which is the only reason a large catalog stays trustworthy.
F.2 The honest limits¶
F.2.1 A new stochastic family needs code¶
The Hawkes family is requested once for acoustic emission and then reused for near-misses and cable microphonics. The next novel physics, whether hysteresis with memory, a coupled multi-body resonance, or a spatially correlated random field for a distributed fiber sensor, needs a family registered in the distribution catalog with its own unit tests. The claim is that the marginal rate of new-family requests falls sharply after the first fifty or so types, not that it reaches zero, and the six requests in A.4.4 against a catalog of eighty types are the measurement of that rate so far.
F.2.2 Array and image channels do not scale like scalars¶
A 32x24 thermal array at 1 Hz is 768 floats per second per device. Five hundred of those does not run on a laptop, and the load-test curve (A4) will show exactly where the knee is. The catalog handles this by making the sampling rate and the publish policy config, and by supporting on-demand raw capture rather than continuous streaming, but the underlying constraint is real and stated rather than hidden.
F.2.3 The real ceiling is the twin state vector, not the catalog¶
A sensor can only be coupled to a phenomenon the twin simulates. Adding a dissolved-ozone sensor to a twin that does not model ozone chemistry produces a sensor with no coupling.reads, which means an uncoupled random-number generator wearing a sensor's name. The schema makes this visible (an empty coupling list is legal but must be justified in capability.rationale and is flagged in review) but it cannot make it untrue. Scaling the catalog to 500 types means scaling the twin, and the twin is the expensive half. That is the version of the scaling claim the README carries.
F.2.4 Test suite cost grows linearly and statistical power does not come free¶
Eighty types with a goodness-of-fit test each, at 200000 samples per test, is already a meaningful CI budget, and G.8 does that arithmetic against the job budget rather than assuming it fits. At 500 types one of three things gives: the sample counts fall and with them the power to detect a wrong distribution, the wall-time budget grows, or the fit suite moves to a nightly job with a fast smoke subset on every pull request. The third is the plan, and the price is that a wrong distribution can merge and be caught within 24 hours rather than within the pull request.
F.2.5 Parameters are sourced, and sourcing is the slowest part¶
Every numeric parameter is covered by a provenance row naming a publisher, an edition, and a locator, and A.7 blocks status: implemented while any group sits at tier D. That is the strongest form of the claim this catalog can make: not that the numbers are measured on an installed base, which they are not and never will be because no client data is in this repository, but that each one names a source a reader can go and check. The generators produce data with the right structure, the right coupling, and the right failure signatures. They do not reproduce any particular real machine.
The cost of that rule is the honest limit. Sourcing a parameter group takes longer than writing one, several standards bodies paywall the documents this catalog cites, and a paywalled standard caps a group at tier C no matter how much effort goes into it. OQ-13 carries the backlog of groups still at tier D, and the number of them is a published figure rather than a private one.
F.2.6 Birth certificate size grows with metric count¶
The alias field is a 64-bit unsigned integer, so the alias space is not the constraint. The constraint is that a single edge node with several hundred metrics produces a large NBIRTH, and every rebirth republishes all of it. Sharding devices across more edge nodes rather than growing metrics per node is what production deployments do, and the load test measures the rebirth storm cost directly rather than assuming it.
F.2.7 The determinism claim has two tiers, and only one of them is byte identity¶
D-05 scopes the determinism claim honestly, and this catalog is the part of the system where the scoping bites hardest. Distributions sample floats and round to ticks, which makes a stream sensitive to a one-unit-in-last-place difference in log, exp, or the inverse error function across platforms and SIMD dispatch. Eighty types drawing from nineteen families is a large surface for that sensitivity.
So the catalog claims byte identity only for the same seed, the same config, the same platform, and the same pinned dependency set, checked by hash equality. Across platforms it claims value equivalence: the business events are identical, and continuous fields agree within a tolerance derived from measured divergence. The cross-platform job reports the observed maximum divergence rather than asserting a number chosen in advance, and when the observed figure exceeds the tolerance the gate names which of the two possibilities it is, a wrong tolerance or a real defect.
The validation.determinism block in every entry carries both tiers, and the provisional cross-platform tolerance in A.2 is marked provisional because no divergence has been measured yet. Replacing it with the measured figure is a Phase 3 deliverable, not an editorial pass.
G. Testing¶
Test tiers map onto the repository's C4 tiering: fast unit, property-based invariants, and seeded end-to-end scenarios.
Every test below states the observation that would fail it, because a test whose failure condition cannot be described is not a test (D-12).
G.1 Schema validation tests¶
| Test | Assertion |
|---|---|
test_all_entries_validate |
Every file in catalog/sensors/** validates against sensor-type.schema.json |
test_no_additional_properties |
Schema has additionalProperties: false at every object level; a fixture with a typo'd field fails validation |
test_filename_matches_id |
Filename stem equals id for every entry |
test_directory_matches_category |
Containing directory equals category for every entry |
test_ids_unique |
No duplicate ids across the catalog |
test_units_resolve |
Every measurement.unit is a valid UCUM code |
test_expressions_parse |
Every expression parses to the restricted AST, contains no disallowed node type, and references only declared names |
test_coupling_reads_resolve |
Every coupling.reads path exists in the twin state registry with a compatible unit |
test_families_registered |
Every family name resolves in the distribution catalog of docs/design/variability-and-faults.md section B |
test_distribution_support_matches_bounds |
For every channel with declared bounds, the noise family's support is contained in those bounds. A Normal on a bounded channel fails here |
test_fault_ids_resolve |
Every failure_modes[].catalog_id and every device_faults id resolves in the fault catalog |
test_rng_streams_registered |
Every name in signal_model.rng_streams parses under the stream grammar and is present in the stream registry |
test_detectors_registered |
Every detectable_by id resolves to a registered detector |
test_mttd_within_pf_interval |
For every mode, expected_mttd_hours does not exceed pf_interval_hours. A budget that admits failure before detection fails here |
test_phase_resolves |
Every phase value names a phase that exists in ROADMAP.md |
test_every_parameter_has_provenance |
Every numeric leaf under measurement, signal_model, and failure_modes is covered by exactly one provenance row, and uncovered leaves are named |
test_no_tier_d_in_implemented |
No entry with status: implemented carries a provenance row at tier D |
test_every_source_records_retrieval |
Every row in sources.yaml carries retrieved and http_status. A source claiming retrieval without a status fails |
test_bearing_frequency_identity |
For every bearing geometry in the reference table, the computed BPFO and BPFI sum to the rolling-element count. A mistyped geometry fails here |
test_schema_backward_compatible |
Diff against the previous released schema is additive-only within a major version |
test_config_errors_are_helpful |
A fixture catalog with six seeded errors produces six line-numbered messages, each naming the field and suggesting a fix (requirement C5) |
G.2 Per-type goodness-of-fit tests¶
Each entry declares what its generator must produce. The suite checks that it does.
| Test | Method |
|---|---|
| Continuous family fit | Hold the declared conditioning state fixed, draw n_samples, run Anderson-Darling (preferred over Kolmogorov-Smirnov because the tails are the part that matters and A-D weights them) against the declared family with parameters estimated from the entry, not from the sample |
| Count family fit | Chi-square goodness of fit against the declared Poisson or negative binomial, with a dispersion test to confirm the choice between them |
| Bounded family fit | Anderson-Darling against the declared Beta, plus a boundary-mass test asserting no point mass at either bound (a point mass is the signature of a clip) |
| Duration family fit | Anderson-Darling against lognormal, gamma, or Weibull as declared, plus a tail test on the upper decile |
| Moment agreement | Sample mean, variance, skewness, and excess kurtosis within a bootstrap confidence interval of the declared values |
| Autocorrelation structure | For the correlated families (ou, ar1, wiener), the sample autocorrelation matches the declared family. An ou with reversion_per_day: 0.09 shows the corresponding decay, and a white-noise generator wearing an ou label fails here |
| Allan variance | For declared bias-instability channels (MEMS IMU, accelerometer), the Allan deviation curve shows the declared bias-instability floor at the declared averaging time |
| Spectral content | For spectral_vector types, a Welch power spectral density of the raw capture places at least the declared fraction of band energy within the declared halfwidth of the derived defect frequency. This is what turns "vibration is not white noise" into a checked claim |
| Coupling sensitivity | Sweep each coupled twin state variable across its range and assert the channel responds with the declared sign and approximate magnitude. Catches an expression that compiles but ignores its input |
| Response dynamics | Apply a step to the coupled twin state and fit the observed time constant; assert it matches the declared tau_s within tolerance. Catches a thermocouple with no lag |
G.2.1 Multiple comparisons¶
Eighty types with several channels each produce several hundred hypothesis tests per CI run. At alpha 0.01 uncorrected, roughly three fail by chance every run and the suite is ignored within a week.
The suite applies a Benjamini-Hochberg false discovery rate control across the whole family at q = 0.01, reports the achieved power per test at the configured sample size, and fails the build only on a discovery. Individual test alphas and sample counts are declared per entry so a channel needing more power can ask for it. The fit suite runs in full nightly and as a fast subset, with reduced n_samples and smoke-level power, on every pull request.
G.2.2 The noise floor of each gate, and what falsifies it¶
D-11 requires a gate over a stochastic quantity to state its noise floor, to set its tolerance above that floor, and to state what result would falsify it. A goodness-of-fit test at a fixed sample count has a measurable noise floor, so the suite measures it rather than assuming it.
Before a gate is admitted, a calibration job draws the declared sample count from the declared family a thousand times and records the empirical distribution of the test statistic under the null. That distribution is the noise floor. Three things come out of it and all three are published in the gate's record.
| Quantity | How it is obtained | What it is used for |
|---|---|---|
| Null distribution | 1000 draws of n_samples from the declared family, statistic recorded each time |
The critical value, taken as an empirical quantile rather than an asymptotic approximation |
| Achieved power | The same draws with a stated departure injected: a 10 percent shift in the family parameter | The gate states what size of error it catches at this sample count |
| Falsification condition | Stated per gate as the observation that fails it | A gate with no describable failure is deleted and replaced (D-12) |
The falsification conditions for the ten methods above are the same in shape: the gate fails when the test statistic exceeds the empirical critical value of its own calibrated null after false discovery rate control. Two gates are worth stating separately because their falsification is not a p-value.
The spectral content gate fails when less than the declared fraction of band energy falls within the declared halfwidth of the computed defect frequency. The declared fraction is a repository-set threshold and not an external statistic, so it is recorded as a design parameter with a SRC-REPO-MODEL provenance row rather than presented as a validated figure, and OQ-14 carries the question of what external reference could ground it.
The response dynamics gate fails when the fitted time constant falls outside the tolerance around the declared tau_s. The tolerance is never tighter than the precision of the declared value: a tau_s written to two significant figures is checked to two significant figures, which is D-11's second condition applied to a parameter rather than to a published constant.
G.2.3 Seeding¶
Every fit test runs at a fixed seed recorded in the test output, so a failure is reproducible exactly. A separate scheduled job runs the same suite across a rotating seed set to catch a generator that is correct at one seed and wrong at others.
G.3 Failure-mode injection tests¶
| Test | Assertion |
|---|---|
test_every_mode_injectable |
Every declared failure mode across every entry can be injected by id and produces an observable change in the stream. A mode that cannot be injected is dead config |
test_mode_detected_within_mttd |
Inject each mode by its catalog_id in a seeded scenario and assert the named detectable_by layer emits a finding within expected_mttd_hours of sim-time. This is what makes expected_mttd_hours a commitment rather than a comment |
test_clean_run_false_positive_budget |
A clean run of the same length with no injected mode produces findings below a declared false-positive budget per detector. Without this test the previous one is trivially satisfiable by a detector that always fires |
test_confusable_modes_separable |
For each confusable_with pair, the signature classifier's confusion matrix on a labeled set meets a declared minimum. Where it does not, and mount looseness against an early outer race defect on broadband alone is the case that does not, the test asserts the classifier abstains rather than guessing, and asserts that adding the band spectrum channel resolves it |
test_fail_silent_modes_need_proof_test |
For every mode whose detector is only a scheduled proof test, assert that no other detector fires. This is a negative test that protects an honest claim: the repo must not accidentally imply it can detect a poisoned catalytic bead from the data stream |
test_severity_floor_enforced |
Any finding originating from an entry with a severity_floor is ranked at or above that floor in the findings stream, and above every throughput finding present in the same window. The scenario seeds a throughput finding with a higher computed impact than the safety finding, so a ranking that merely sorts by impact fails |
test_no_runtime_clamping |
Inject a mode that drives a channel outside range_of_interest and assert the published value is the true out-of-range value and that a plausibility finding is raised. Complements no_clamp_in_sampler, which is a static check on the sampling path; this one is dynamic and catches a clamp added downstream of the sampler |
test_tails_not_truncated |
For every gp_tail channel, draw 10^7 samples and assert the count of exceedances above the declared extreme quantile falls inside the Poisson interval its rate implies. A truncating implementation produces zero exceedances and fails; an inflated tail produces too many and also fails |
G.4 Topic and payload tests¶
| Test | Assertion |
|---|---|
test_topic_uniqueness |
Instantiate every shipped facility profile; assert every rendered UNS topic is unique across all devices and all catalog entries. Fails on collision with both colliding entries named |
test_topic_well_formed |
Every rendered topic matches the level regex, has exactly six levels (with the parameter level allowed to be multi-segment), contains no MQTT wildcard, has no empty level, no leading or trailing slash, and is under 512 bytes |
test_sparkplug_topic_well_formed |
Every Sparkplug topic matches spBv1.0/{group}/{type}/{node}[/{device}] with a valid message type |
test_alias_uniqueness_and_determinism |
Within an edge node session, aliases are unique across the node's whole metric set, start at 1, and never include 0. Two runs at the same seed on the same platform produce identical alias assignments, and a third run under a different PYTHONHASHSEED produces the same assignments, which is what proves the sort is explicit (D-03) |
test_alias_reassignment_on_rebirth |
A rebirth with a changed metric set is handled by the consumer; a consumer caching aliases across the birth boundary is shown to break, and the historian does not |
test_datatype_matches_shape |
For every entry, the datatype the derivation function produces equals the datatype recorded in a hand-written golden table checked into the test fixtures. Comparing the derivation against itself would pass whatever the function did (D-12), so the expected value is written by a person and reviewed |
test_payload_roundtrip |
Encode a sample, decode it, and assert the value, datatype, unit property, Quality, sensor_type_id, and sensor_type_revision survive intact under both the Sparkplug and JSON fallback profiles |
test_payload_within_budget |
Encoded payload size is within validation.budget.max_payload_bytes for every entry |
test_sequence_gap_detected |
Drop a message from a stream and assert the Sparkplug seq gap detector raises a data-loss finding, including across the 255 to 0 wrap, where a naive comparison sees a gap of 254 |
G.5 Capability wiring tests¶
| Test | Assertion |
|---|---|
test_no_orphan_sensors |
Every entry's capability.unlocks is non-empty, every id resolves in capabilities.yaml, and every capability is declared as a dependency by at least one subsystem in the subsystem registry. A sensor whose capability nobody consumes fails the build |
test_no_orphan_capabilities |
Every capability in capabilities.yaml has at least one producing sensor type. A capability nothing feeds fails the build |
test_consumed_by_is_truthful |
Each subsystem named in consumed_by declares the capability in its own manifest. Catches a one-directional claim |
test_capability_graph_acyclic |
The type-to-capability-to-subsystem graph has no cycles |
test_every_subsystem_has_sensors |
Every twin subsystem that declares sensor dependencies has at least one instantiated type serving it in at least one shipped facility profile |
G.6 Property-based invariants¶
Run under Hypothesis-class generation, at high sample counts, in the C4 property tier.
| Invariant | Scope |
|---|---|
| Declared physical bounds hold | Every entry's validation.physical_bounds expressions, over 10^6 samples per entry per shift of the conditioning state |
| Monotone channels never decrease | Every counter_totalizer channel across a full simulated run including a device restart |
| Dew point never exceeds ambient | The derived channel and its two parents |
State of charge stays in [0, 1] and the pack energy balance closes |
Battery entries, across charge and discharge cycles |
Relative humidity stays in [0, 100] |
All humidity channels |
| Read counts never exceed inventory rounds, and never fall below zero | RFID entries; a bounded count family cannot leave [0, n] |
| Angles wrap correctly | All circular channels, across the wraparound boundary |
| Sim-time is monotone per device | All entries; requirement C2 |
| Same seed on the same platform yields an identical stream hash | All entries; the byte-identical tier of C1 under D-05 |
| Same seed across platforms yields identical business events and continuous fields inside the measured tolerance | All entries; the value-equivalent tier of C1 under D-05. The job reports observed maximum divergence rather than asserting it |
G.7 Budget and scaling tests¶
| Test | Assertion |
|---|---|
test_generator_cost |
Per-sample generator cost within validation.budget.max_generator_cost_us_per_sample on the stated reference hardware, measured over a warm loop |
test_full_fleet_fits_budget |
The full shipped enterprise profile's aggregate event rate and byte rate are within the declared A4 load-test envelope |
test_ci_walltime |
The pull-request-tier catalog suite completes within the declared CI budget |
test_ci_budget_arithmetic |
The declared per-test sample counts, multiplied out across the shipped catalog, produce a projected wall time inside the job budget. A catalog that grows past its budget fails as a defect rather than as a timeout (D-13) |
G.8 The CI budget arithmetic¶
D-13 rules that a timing test is scoped to fit its budget, that the clamp lives in the generator rather than only in the config validator, and that a budget test asserts the arithmetic so a suite that grows past its job budget fails as a defect rather than as a timeout. The fit suite is the part of this section that can outgrow its budget, so the arithmetic is written down.
| Quantity | Pull-request tier | Nightly tier | Where it comes from |
|---|---|---|---|
| Entries exercised | 80 | 80 | The implemented count, recomputed from disk |
| Fit gates per entry, mean | 3 | 3 | Counted from validation.goodness_of_fit across entries |
| Samples per gate | 5000 | 200000 | n_samples, with the pull-request tier scaled down |
| Calibration draws per gate | 0 | 1000 | G.2.2; the null distribution is recalibrated nightly |
| Total draws | 1.2e6 | 4.8e10 | Entries times gates times samples times draws |
The pull-request tier is a smoke tier and its power is stated rather than implied: at 5000 samples a gate detects a 10 percent parameter shift at the power the calibration job measures and publishes, and it does not detect smaller shifts. A defect smaller than that reaches the nightly tier and is caught within 24 hours. That is the trade F.2.4 describes, made numeric.
Two clamps hold the arithmetic. n_samples has a generator-side ceiling in the pull-request tier, so an entry cannot request a sample count that blows the job budget by writing a large number in its own file. And test_ci_budget_arithmetic recomputes the table above from the shipped catalog and fails when the projection exceeds the job budget, which makes catalog growth a reviewable event rather than a slow slide into a timing-out job.
The nightly draw count is large because calibration is expensive, which is why it is nightly. The calibrated null distributions are cached by (family, parameter set, sample count) and recomputed only when one of those three changes, so the steady-state nightly cost is far below the figure above and the figure above is the cold-cache worst case.
H. Phase placement¶
Sensor categories grow with the subsystems that consume them. A category that lands before its consumer is a fleet of devices publishing into nothing, which is the specific failure this table exists to stop. The phase ids are those of docs/design/roadmap.md and test_phase_resolves checks each one against it.
| Category | Phase | The subsystem that makes it consumable |
|---|---|---|
| Warehouse and logistics, first two types | P1 | The walking skeleton: one RFID portal and one temperature probe on UNS topics |
| Industrial equipment | P3 | Component 3 fleet management and predictive maintenance |
| Environmental and facility | P3 | Facility health findings, and the ambient reference every compensated channel needs |
| Warehouse and logistics, rest | P3b | Component 1b: AMR fleet, palletizer, ASRS, sortation, slotting |
| Electrical and power | P3b | The same, plus the energy KPIs that E7 puts ahead of it |
| Transportation and fleet | P3h | Component 6a7 transportation network and the cold-chain capability |
| Process and chemical | P3i | The upstream factory's continuous and batch stage |
| Safety and compliance | 6a10 | Component 6a10 safety and ergonomics, which owns the severity floor |
| Structural | 6a10 | The same, plus the snow-load scenario that the roof load type serves |
Three consequences of that ordering are worth stating, because each is a dependency that would otherwise be discovered late.
The schema itself lands at P3 with the first two categories, but the two P1 types are instantiated before the schema exists as a loader. They are hand-written in the walking skeleton and migrated onto the loader when it arrives, and the migration is a work package rather than an afterthought: test_p1_types_survive_migration asserts the two types publish the same topics and the same payload fields before and after.
The safety category is the last to land and it carries the severity floor, which several earlier layers reference. The floor is a findings-contract feature from component 5 onward, not a safety-category feature, and the safety entries set a field the contract already understands.
Every entry's phase field is the phase of the earliest subsystem that consumes it, not the phase of its category. rtd_pt100 is an industrial-equipment type with phase: P1, because the walking skeleton's temperature probe is one, and the category it belongs to lands at P3 around it.
H.1 When the fleet reaches fifty devices¶
The source requires at least fifty simulated edge devices, and that count is a product of this section rather than a number set somewhere else: a device exists because a catalog entry's attaches_to.binding and attaches_to.cardinality cause a facility profile to instantiate one.
test_shipped_profile_device_count instantiates each shipped facility profile and asserts the device count. The micro-fulfillment profile at P1 is below fifty on purpose, because a walking skeleton with fifty devices is not a walking skeleton. The growth-tier profile crosses fifty at P3b, once the warehouse and electrical categories have landed and component 1b has the subsystems for them to attach to, and the test asserts the floor from that phase onward rather than from the start.
The count is asserted per profile and per phase, so a profile that quietly loses devices when a category is re-binned fails the build instead of drifting under the requirement.
I. Open questions¶
These are genuinely undecided. Each has at least two defensible answers and none is resolved by asserting a preference here.
OQ-1. Matrix representation for the thermal array¶
The numeric half of this question is settled. The array datatype enum values are read from the Sparkplug Specification 3.0.0 payload definition and recorded in D.3.2: FloatArray is 30, DataSet is 16, and the scalar assignments this section already used were correct.
The modeling half is open. For the 32x24 thermal array, a flattened FloatArray with rows and cols in the metric properties is compact and simple. A DataSet is self-describing and idiomatic for tabular data and heavier on the wire. A third option publishes only the derived scalars, maximum, mean, and hot-spot coordinates, on the UNS and keeps the full frame in the historian as a side channel, which is the only one of the three that changes the payload budget by an order of magnitude rather than a percentage.
The decision belongs to the E3 milestone and it moves max_payload_bytes for the thermal entry, so the budget test is written against whichever option is chosen rather than against a number picked first.
OQ-2. Are derived channels catalog entries or historian computations¶
env.dewpoint.derived is computed from a temperature and humidity pair. Publishing it as a first-class UNS topic makes it visible to any subscriber and keeps the semantic layer uniform, at the cost of publishing redundant information and of two sources of truth if a consumer computes it independently. Computing it in the historian keeps the wire clean but means the dashboard and the agent have to know a derivation rule that is not in the catalog. The current draft publishes it as a catalog entry with a derived marker, which is a defensible default and not a settled one. The same question applies to power (derived from voltage and current), dew point, temperature-normalized tire pressure, and corrosion rate.
OQ-3. UNS placement of mobile assets¶
Section D.1 places trucks, trailers, and AMRs under a fleet pseudo-area within the dispatching site. This preserves the six-level shape at the cost of calling something an area that is not one, and it means a trailer's topic changes when it is reassigned between sites. The alternative is a parallel top-level branch (twinflow/fleet/...) that is honest about being outside the ISA-95 hierarchy but breaks the uniform six-level structure every subscriber and every wildcard subscription depends on. A third option follows the asset (topic changes as it moves), which is the most physically accurate and the worst for historians. This is a modeling decision with no clean answer, and reviewers who run industrial IoT will have a view on it.
OQ-4. Is a vision-derived channel a sensor¶
saf.ppe.vision_compliance publishes like a sensor and attaches to the twin like a sensor, but its failure modes are model failure modes (class confusion, domain shift, calibration drift) rather than transducer failure modes, and its "calibration" is a training run. Putting it in the catalog keeps one uniform representation and lets the same machinery handle it. Keeping it out draws a cleaner line between measurement and inference. The current draft includes it with an inference_channel profile, and flags that this decision determines whether the MLOps layer (E43) or the fleet health layer owns its lifecycle.
OQ-5. Raw waveform capture policy for vibration¶
Genuine bearing analysis needs sampling in the tens of kilohertz. Streaming that from even fifty devices is not laptop-scale, so the catalog computes band features on-device (tier 0 in the E36 model) and publishes those at 1/60 Hz, with raw capture available on demand or on alarm. Three things are unresolved: whether the on-demand capture is published to the UNS at all or fetched over the REST API, whether the tier-0 FFT claim is honest given that the simulated device is a Python process rather than a constrained MCU, and whether the ESP32 hardware-in-the-loop device (E47) can do the same computation, because if it cannot then the tier-0 claim is aspirational for the one device that would prove it.
OQ-6. Independent or correlated failure onset¶
Failure onset is currently drawn per device independently. Real fleets fail in correlated ways: a bad batch of sensors, a firmware regression, a humid summer, a single contractor who mounted forty gauges the same wrong way. Modeling a shared latent health state would be markedly more realistic and would give the fleet-health layer genuinely hard work. It would also break the independence assumption the goodness-of-fit tests rely on, requiring the fit suite to condition on the latent state or to test the joint distribution instead. The realism gain is large and the test-complexity cost is real.
OQ-7. Unit representation¶
UCUM codes are machine-checkable and support automatic conversion, which is what makes the cross-check and MSA layers possible without hand-written conversions. They are also verbose and unfamiliar (mm/s is fine, ug/m3 and mS/cm less so, and dimensionless quantities are genuinely awkward). The alternative is a small curated unit enum with a hand-maintained conversion table, which is friendlier to read and requires maintenance. The current draft uses UCUM.
OQ-8. Device profile layer above the catalog¶
A Type K thermocouple from vendor A and one from vendor B have the same physics and different tolerance classes, drift rates, and time constants. The current schema puts those numbers in the type entry, which means a facility cannot model a mixed-vendor fleet without duplicating the type. A separate device_profile layer (type plus vendor-specific parameter overrides) solves it and roughly doubles the config surface. Eighty types do not need it. Three hundred might, and the decision costs less now than after three hundred entries carry vendor numbers inline.
OQ-9. Where calibration state lives¶
Calibration due dates, last-calibration values, and as-found and as-left readings are instance properties, not type properties. They belong somewhere, and the candidates are the device registry (which is where a fleet manager would look), the catalog instance binding in facility.yaml, or a separate calibration record store that the MSA layer owns. This matters because the MSA stability study needs the calibration history to interpret a drift finding, and because a calibration event is exactly the kind of change the SPC layer needs correlated against a bias step.
OQ-10. Whether planned entries belong in the catalog¶
The schema allows status: planned so the roadmap for later sensor categories lives beside the implemented ones. This is good for the "system under construction with a known destination" story that the repository is deliberately telling. It also means the catalog directory count is not the implemented count, which makes the README count assertion slightly more complex and creates a way for the catalog to look larger than it is. The current draft permits it and makes CI count only implemented entries for every published number.
OQ-11. Which section owns the catalog entry schema¶
docs/design/iot-fleet.md section 3.1 and this section both specify the catalog entry, and they do not agree. Neither is wrong on its own terms and only one can ship.
| Surface | This section | iot-fleet 3.1 |
|---|---|---|
| File layout | One file per type under a category directory | One file per category holding a list of entries |
| Identifier field | id, dot-delimited, lowercase with dots |
type_id, snake case, no dots |
| Unit of a type | One measurement, with measurement.channel_shape |
One device, with a channels list |
| Capability field | capability.unlocks and capability.consumed_by |
capability_unlocks |
| Attachment | attaches_to.subsystem plus a binding level |
attaches_to as a single resource-selector string |
| Category split | 14, 7, 15, 12, 10, 8, 9, 5 | 12, 8, 18, 10, 8, 9, 10, 5 |
| Model vocabulary | Distribution families per channel | Twelve named signal-model kinds, capped by INV-CAT-3 |
The two category splits both total 80 and both cover the categories the source requires, so the disagreement is not arithmetic. It is the third row: whether a type is a measurement or a device. Every other row follows from that one.
Three ways to settle it. Adopt the device-shaped entry, which matches the package that owns the loader and gives a multi-channel device one identity, at the cost of re-binning this section's eighty rows and folding three vibration measurements into one type's channels. Adopt the measurement-shaped entry, which keeps the per-measurement physics intact, at the cost of contradicting the section that owns the package boundaries. Or keep both, with the device-shaped entry as the file format and the measurement-shaped entry as a generated view, which costs a generator and a round-trip test.
This is a program decision. It is recorded rather than settled by assertion, because choosing between two designed schemas on no evidence is how a contradiction turns into a silent drift. It belongs in DOCTRINE.md as a ruling, since it is the class of defect that document exists to settle: one contract stated twice in different words.
OQ-12. Four failure modes have no fault id¶
bias_step, saturation, transition_loss, and array_dead_pixel appear in the class profiles of C.1 and have no entry in the fault catalog of docs/design/variability-and-faults.md section C.4, so nothing can inject them and no test can score them.
Each is a registration request against that catalog, and each carries a design question. A bias step and a calibration loss differ only in whether the change is a step or a ramp, so one parameterized fault may cover both. Saturation is arguably an effect of an existing fault at extreme parameters rather than a fault of its own. Transition loss on a discrete state is the same observation as stuck-at seen through a different detector, and whether it earns a separate id depends on whether the scoring harness needs to tell the two apart. An array dead pixel has no analog in the current catalog at all.
Until they are registered, an entry whose profile supplies one of them cannot reach status: implemented, which is the rule A.7 applies to parameters, applied here for the same reason.
OQ-13. The tier-D parameter backlog¶
A.7 blocks status: implemented on any entry carrying a tier-D parameter group, and at the time of writing most entries carry one. The noise and drift parameters across the catalog are modeling assumptions with no external source, and the annotated entry in A.2 says so in its own provenance block rather than hiding it.
That backlog is large and it is the real cost of rule 3. Three questions are open. Whether a modeling assumption with a stated derivation from a sourced quantity counts as tier B or stays tier D, which decides how much of the backlog closes by writing derivations rather than by finding sources. Whether a vendor datasheet, a primary text but a commercial one that can be withdrawn, is tier A or tier C. And what the release gate does when one category is fully sourced and another is not, since a release that ships half the catalog as planned is honest and awkward at once.
The count of tier-D groups goes in the release notes rather than staying private, so the backlog shrinking is visible and the backlog not shrinking is visible too.
OQ-14. What external reference grounds the spectral energy threshold¶
The spectral content gate in G.2 asserts that at least a declared fraction of band energy falls within a declared halfwidth of the computed defect frequency. That threshold is a repository-set design parameter. D-11 permits a design parameter to be recorded as one and does not permit it to be presented as a validated figure.
So the question is what would ground it. A published vibration-analysis reference stating the expected energy concentration at a defect frequency for a given bearing condition would do it. A measured figure from an open bearing-fault dataset with labeled conditions would also do it, and would give the signature classifier a real external benchmark as well, which is worth more than the threshold itself. Absent either, the gate stays a design parameter with a SRC-REPO-MODEL provenance row, and G.2.2 says so where the gate is defined.