Signalise

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.