module
Encoder
Data's implementation of the transform: flowsheet records plus per-minute telemetry in, observation/action/reward vectors out, in the plant's own engineering units. Pure and deterministic — the same flowsheet and the same telemetry produce byte-identical arrays in an identical order, which is why a re-identified plant encodes the same.
The boundary
One implementation, one encode path. `build_steps` is its only caller — over a 30-day backfill and over the last two minutes, the same function with a different window — so the runtime does not encode at all. Order comes from records, never from geometry.
Decisions on record (3)
Inferred: decisions whose title names this module, or whose body references it as code. ADRs carry no declared statement of what they govern, so a decision that governs this module without naming it will be missing.
Public surface
DataEncoderencoder.py · 3 public methods
encode_chunk · operation_membership · operation_of
DatasetFingerprintBuilderfingerprints.py · 1 public methods
build_flowsheet_fingerprint
SignalLayoutmodels.py · 1 public methods
observation_dim
Enforced invariants (78)
The plant is reporting; one actuator's tag is not. That step is not complete.
test_a_dropped_action_tag_is_not_reported_as_a_complete_step
0.0 is a fabricated extreme in engineering units, not a neutral 'no reading'.
test_a_dropped_action_falls_back_to_its_nominal_not_to_zero
A closed valve reads 0.0. That is a measurement, not an absence.
test_a_genuine_zero_reading_is_kept_and_not_treated_as_missing
returns one step per minute
test_returns_one_step_per_minute
step ids are unique
test_step_ids_are_unique
step start iso strings
test_step_start_iso_strings
step end one minute after start
test_step_end_one_minute_after_start
plant id in every step
test_plant_id_in_every_step
flowsheet fingerprint consistent
test_flowsheet_fingerprint_consistent
encode_chunk v2.0.0 must not include an observation_layout key.
test_no_observation_layout_key
observation values is float list
test_observation_values_is_float_list
action values is float list
test_action_values_is_float_list
reward values is float list
test_reward_values_is_float_list
Spec 17: a constrained CV's dispersion is an ORDINARY observation dimension.
test_observation_values_is_base_dims_then_one_dispersion_dim_per_constrained_cv
`cv_ids` must be a subset of the observation vector — `std_index` indexes the PREDICTED
test_a_constrained_cv_is_an_observation_node_carrying_a_limit
missing count is int
test_missing_count_is_int
A minute with no telemetry is missing every node it needed, not just its observations.
test_missing_count_all_missing_when_no_telemetry
missing count zero when all present
test_missing_count_zero_when_all_present
Every node the step is built from counts — observations, actions AND rewards.
test_missing_count_partial
normalise ts trims to minute
test_normalise_ts_trims_to_minute
normalise ts distinct minutes
test_normalise_ts_distinct_minutes
Bulk (build) mode and runtime (serve) mode encode the same surface → identical fingerprint.
test_build_and_serve_produce_identical_fingerprint
Two independent Encoders over the same surface pin the same id orders (no ordering nondeterminism).
test_fingerprint_is_stable_across_encoder_instances
A different flowsheet must yield a different fingerprint (skew is detectable, not masked).
test_fingerprint_changes_when_flowsheet_changes
One write per node per backfill chunk. This is what churned the identity mid-run.
test_the_satellites_resume_cursor_is_not_the_flowsheet
a finished backfill is not the flowsheet
test_a_finished_backfill_is_not_the_flowsheet
A per-run historian URL and a controller mode say where data comes from, not how it encodes.
test_the_edges_coordinates_and_mode_are_not_the_flowsheet
touching a row is not changing it
test_touching_a_row_is_not_changing_it
a node becoming an action changes the flowsheet
test_a_node_becoming_an_action_changes_the_flowsheet
Amends ADR 0063, by ADR 0064: geometry is presentation, and the encoder cannot see it.
test_moving_equipment_does_not_change_the_ENCODING
The map's own freshness is a different question, and it keeps its own key — otherwise the
test_moving_equipment_DOES_change_the_drawing
Membership is a stored column since ADR 0062 and decides which operation a signal reaches.
test_reattributing_an_entity_changes_the_flowsheet
remapping a signal to another entity changes the flowsheet
test_remapping_a_signal_to_another_entity_changes_the_flowsheet
adding a node changes the flowsheet
test_adding_a_node_changes_the_flowsheet
the surface keeps every field the encoder reads
test_the_surface_keeps_every_field_the_encoder_reads
config hash is deterministic
test_config_hash_is_deterministic
config hash length
test_config_hash_length
config hash default is stable
test_config_hash_default_is_stable
encoder config defaults
test_encoder_config_defaults
`EncoderConfig` held exactly one thing: the colour ramp's clip (ADR 0064 deleted it).
test_the_config_carries_nothing_the_encoder_still_needs
nominal centered anchors
test_nominal_centered_anchors
clamps out of range
test_clamps_out_of_range
vector uses per index bounds and fallbacks
test_vector_uses_per_index_bounds_and_fallbacks
denormalize ∘ normalize == identity (within the bounds). Planner works in normalized space;
test_denormalize_is_inverse
The planner's setpoint recovery must be the shared function — no competing inverse.
test_planner_dispatch_uses_canonical_denormalize
The autopilot's runtime normalizer must be the same function — no competing encoding.
test_autopilot_runtime_delegates_to_canonical
material internal when both endpoints in one op
test_material_internal_when_both_endpoints_in_one_op
material transfer src to dst
test_material_transfer_src_to_dst
material plant input when src outside
test_material_plant_input_when_src_outside
material plant output when dst outside
test_material_plant_output_when_dst_outside
disjoint operations do not overlap
test_disjoint_operations_do_not_overlap
overlapping operations detected
test_overlapping_operations_detected
nested operations detected as overlap
test_nested_operations_detected_as_overlap
every reward is operation attributed
test_every_reward_is_operation_attributed
action features exist and are attributed
test_action_features_exist_and_are_attributed
operation of uses entity not node
test_operation_of_uses_entity_not_node
The decode block carries the STORED membership (ADR 0062) — the column, not the geometry.
test_operation_membership_block_reports_what_the_rows_say
The point of the column: absent membership is absent, not quietly filled in from geometry.
test_an_entity_with_no_stored_operation_attributes_to_nothing
D2: geometry proposes. Cleared columns change the record and leave the proposal untouched.
test_the_geometry_is_still_available_as_a_proposal
The property the column exists for: a polygon is a drawing, not a claim about the process.
test_moving_the_geometry_does_not_move_the_membership
The other half of D2: the proposal is the geometry's opinion, so it must track the drawing.
test_the_proposal_does_move_with_the_geometry
re identified rows produce the same ordered signals
test_re_identified_rows_produce_the_same_ordered_signals
The whole point: a clone or a restore must not invalidate a trained model.
test_the_fingerprint_survives_re_identification
The entity index is the primary sort key, and it used to come from raw DB row order.
test_entity_order_does_not_depend_on_row_order
node row order does not move a dimension
test_node_row_order_does_not_move_a_dimension
A tie on node name is broken by the controller, not by row identity.
test_mirrored_tag_names_stay_distinguishable
mirrored tag names order deterministically under row shuffle
test_mirrored_tag_names_order_deterministically_under_row_shuffle
Keyed on node name ALONE these two distinct surfaces hash identically — the controller half
test_the_fingerprint_separates_surfaces_that_share_a_node_name
Same tag names, different DCS attribution → a genuinely different surface.
test_swapping_the_controllers_changes_the_fingerprint
Same node name AND same controller: the key cannot identify the surface, so refuse rather
test_a_genuinely_ambiguous_surface_is_refused
This used to raise: a degree-scale polygon rastered to ~5e10 voxel cells at 0.5 m, so the
test_an_oversized_entity_is_no_longer_a_problem_to_guard
The guard must not fire on real plant geometry — an 11 m vessel is fine.
test_a_realistic_entity_still_encodes
the geometry does not propose an operation for the plant
test_the_geometry_does_not_propose_an_operation_for_the_plant
the plant keeps its signals
test_the_plant_keeps_its_signals
plant scope is read off the class not the name
test_plant_scope_is_read_off_the_class_not_the_name
the class comparison ignores case and padding
test_the_class_comparison_ignores_case_and_padding
`class DataEncoder` is defined exactly once, and that one definition is in signalise.data.encoder.
test_exactly_one_encoder_implementation_and_it_is_the_shared_library
The Data job (bulk mode) and the autopilot kernel (runtime mode) both import signalise.data.encoder —
test_both_a_job_and_the_kernel_import_the_shared_encoder
Sources
Generated from the repository. Every section is derived from an artifact the build enforces — ADR frontmatter, the architecture tests, the module's AST, its test docstrings, and the CLI's own help. Nothing here is hand-written prose, so nothing here can drift from the code without the code changing first.