UPDE — PHA-C Event Timeline
PHACTimelineRecord is the trajectory-level PHA-C evidence surface. It consumes
phase and axial-position matrices from a moving-frame run, threads the
single-sample PHACHandoffRecord contract through time, and emits a compact
lock/loss/reset timeline for downstream review lanes.
The timeline is intentionally review-only. It never writes to actuators, changes
coupling, schedules hardware, or mutates supervisor state.
For the full PHA-C chain from spatial modulation through moving-frame dynamics
and timeline hashing, use
PHACAcceptanceRecord.
Use cases
Use the PHA-C event timeline when a downstream lane needs event evidence across
an entire trajectory instead of a single sample:
- MIF/FRC review where the first lock time, lock losses, and reset count must be
carried into chamber or field-reconstruction analysis;
- Studio panels that need a stable summary of when a moving-frame run entered
or left a reviewed merge window;
- replay ledgers where every sample handoff hash must roll into one canonical
trajectory hash;
- safety reviews that need to separate observed lock evidence from any later
policy or operator action;
- polyglot parity gates that compare Python, Rust, Go, Julia, and Mojo
source-contract behavior for the same trajectory.
Contract
Inputs are two real finite matrices with shape (time, oscillator):
phases_by_step[t, i] = wrapped or unwrapped phase for oscillator i at sample t
positions_by_step[t, i] = axial position for oscillator i at sample t
times[t] = strictly increasing sample time
For each sample, the builder calls build_pha_c_handoff_record(...) and carries
consecutive_lock_samples forward. The final timeline records:
first_lock_index and first_lock_time for the first achieved consecutive
joint lock;
final_lock_achieved for the last sample;
- phase-lock, spatial-lock, full-lock, lock-loss, and reset counts;
- maximum phase dispersion, maximum spatial dispersion, minimum Kuramoto order
parameter, maximum distance to the reference position, and minimum signed
phase/spatial margins over the trajectory;
- replay validation that each minimum signed margin equals the resolved
tolerance minus the corresponding maximum dispersion under
PHA_C_TIMELINE_MARGIN_REPLAY_TOLERANCE;
- tolerance profile name, multiplier, resolved phase tolerance, and resolved
spatial tolerance;
- SHA-256 digests for the time vector, all sample records, transition table,
and final timeline;
execution_disabled=True, actuating=False, and the fixed claim boundary
pha_c_event_timeline_review_only.
Minimal example
import numpy as np
from scpn_phase_orchestrator.upde.pha_c_timeline import (
build_pha_c_event_timeline,
verify_pha_c_event_timeline,
)
phases = np.array(
[
[-0.02, 0.0, 0.02],
[-0.002, 0.0, 0.002],
[-0.0015, 0.0, 0.0015],
[-0.001, 0.0, 0.001],
[-0.02, 0.0, 0.02],
]
)
positions = np.array(
[
[-0.003, 0.0, 0.003],
[-0.0005, 0.0, 0.0005],
[-0.0004, 0.0, 0.0004],
[-0.0003, 0.0, 0.0003],
[-0.003, 0.0, 0.003],
]
)
times = np.arange(phases.shape[0]) * 0.5
timeline = build_pha_c_event_timeline(
phases,
positions,
times=times,
phase_tol_rad=0.01,
spatial_tol_m=0.002,
required_consecutive_samples=3,
tolerance_profile="baseline_1x",
)
assert timeline.first_lock_index == 3
assert timeline.lock_loss_count == 1
assert timeline.execution_disabled
assert not timeline.actuating
evidence_payload = timeline.to_dict()
verify_pha_c_event_timeline(timeline)
Use verify_pha_c_event_timeline(...) when replaying a stored trajectory
record. It rechecks timeline counts, first-lock semantics, transition-count
bounds, review-only flags, signed margin equations, SHA-256 fields, and the
canonical timeline hash without requiring raw trajectory matrices. The signed
margin replay rejects records whose positive-looking phase or spatial margin no
longer matches tolerance - maximum_dispersion.
The Rust, Go, Julia, and Mojo source-contract rows are validated before
canonical dictionary projection. Raw PHACTimelineRecord fields must keep
their declared domains: numeric evidence must be finite real non-boolean
scalars, counts and indexes must be integers, observation/non-actuation flags
must be plain booleans, and provenance/hash fields must be strings. Numeric
strings, boolean aliases, and bytes-like string substitutes are rejected before
to_dict() can coerce them into a matching canonical payload.
Handoff versus timeline
| Surface |
Scope |
Main output |
Use when |
PHACHandoffRecord |
one phase/position sample |
scalar lock evidence plus sample hashes |
a downstream lane needs one reviewed event-state atom |
PHACTimelineRecord |
complete trajectory |
lock acquisition, loss, reset, profile, and trajectory hashes |
a downstream lane needs replayable event history |
The timeline is built from handoff records. If native accelerator kernels are
added later, they must preserve both the per-sample handoff hashes and the final
timeline hash, including the minimum signed margins.
Polyglot parity
The benchmark gate records Rust, Mojo, Julia, Go, and Python source-contract
slots. The current timeline path is evidence construction, not a numerical hot
loop, so the non-Python slots validate parity against the Python reference
contract. If native kernels are later added, they must preserve the same hashes
signed margins, signed-margin equations, and fail-closed input boundaries. The
benchmark payload publishes phase_margin_equation_validated,
spatial_margin_equation_validated, signed_margin_equations_validated, and
margin_replay_tolerance for every backend row. The benchmark's maximum-error
helper uses the same strict raw-field parser as the source-contract validator,
so malformed fields cannot be hidden by canonical payload coercion.
uv run python benchmarks/pha_c_timeline_benchmark.py \
--parity-gate \
--calls 1 \
--output benchmarks/results/pha_c_timeline.json
Committed benchmark JSON is local regression evidence only. It is not a
production timing claim unless rerun under the benchmark-isolation protocol.
Failure boundaries
The timeline fails closed on:
- empty, non-finite, complex, object-dtype, or boolean trajectory matrices;
- non-matrix phase or position inputs;
- mismatched phase and position shapes;
- missing, non-finite, non-vector, wrong-length, or non-increasing time vectors;
- negative tolerances;
- invalid consecutive-sample controls;
- unknown tolerance profile names.
- source-contract records with numeric strings, non-finite raw numeric fields,
boolean aliases, malformed integer fields, non-plain booleans, or non-string
provenance/hash fields.
PHACTimelineRecord
dataclass
PHACTimelineRecord(
sample_count: int,
oscillator_count: int,
start_time: float,
end_time: float,
duration_s: float,
first_lock_index: int,
first_lock_time: float,
first_lock_observed: bool,
final_lock_achieved: bool,
lock_sample_count: int,
phase_lock_sample_count: int,
spatial_lock_sample_count: int,
lock_loss_count: int,
reset_count: int,
max_consecutive_lock_samples: int,
max_phase_dispersion_rad: float,
max_spatial_dispersion_m: float,
min_phase_margin_rad: float,
min_spatial_margin_m: float,
min_phase_order_parameter: float,
max_distance_to_reference_m: float,
reference_phase: float,
reference_point: float,
phase_tol_rad: float,
spatial_tol_m: float,
tolerance_profile_name: str,
tolerance_profile_multiplier: float,
required_consecutive_samples: int,
claim_boundary: str,
evidence_kind: str,
execution_disabled: bool,
actuating: bool,
time_state_sha256: str,
sample_records_sha256: str,
transition_table_sha256: str,
timeline_sha256: str,
)
Audit-ready PHA-C trajectory event timeline.
The record stores scalar trajectory evidence plus SHA-256 digests of the
time vector, per-sample handoff records, transition table, and final
timeline. It is designed for review and replay lanes, not for direct
control output.
Methods:
to_dict
to_dict() -> dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
Returns
dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def to_dict(self) -> dict[str, float | int | bool | str]:
"""Return a JSON-safe canonical representation.
Returns
-------
dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
"""
return pha_c_event_timeline_to_dict(self)
|
build_pha_c_event_timeline
build_pha_c_event_timeline(
phases_by_step: ArrayLike,
positions_by_step: ArrayLike,
*,
times: ArrayLike | None = None,
reference_phase: object = 0.0,
reference_point: object = 0.0,
phase_tol_rad: object = DEFAULT_PHASE_TOL_RAD,
spatial_tol_m: object = DEFAULT_SPATIAL_TOL_M,
required_consecutive_samples: object = 3,
tolerance_profile: object | None = None,
) -> PHACTimelineRecord
Build deterministic PHA-C lock/loss timeline evidence.
Parameters mirror :func:build_pha_c_handoff_record, but consume complete
trajectory matrices with shape (time, oscillator). The consecutive-lock
counter is threaded through every sample so downstream consumers can review
acquisition, loss, and reset events without raw state arrays.
Parameters
phases_by_step : ArrayLike
Phase history, shape (n_steps, N).
positions_by_step : ArrayLike
Position history, shape (n_steps, N).
times : ArrayLike | None
Per-step timestamps in seconds, or None for unit spacing.
reference_phase : object
Reference phase for the lock criterion, in radians.
reference_point : object
Reference axial coordinate for the spatial-margin criterion.
phase_tol_rad : object
Phase lock tolerance in radians.
spatial_tol_m : object
Spatial lock tolerance in metres.
required_consecutive_samples : object
Consecutive in-tolerance samples required to declare lock.
tolerance_profile : object | None
Named tolerance profile, or None for the baseline profile.
Returns
PHACTimelineRecord
The deterministic PHA-C lock/loss timeline record.
Raises
ValueError
If any input is invalid.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def build_pha_c_event_timeline(
phases_by_step: ArrayLike,
positions_by_step: ArrayLike,
*,
times: ArrayLike | None = None,
reference_phase: object = 0.0,
reference_point: object = 0.0,
phase_tol_rad: object = DEFAULT_PHASE_TOL_RAD,
spatial_tol_m: object = DEFAULT_SPATIAL_TOL_M,
required_consecutive_samples: object = 3,
tolerance_profile: object | None = None,
) -> PHACTimelineRecord:
"""Build deterministic PHA-C lock/loss timeline evidence.
Parameters mirror :func:`build_pha_c_handoff_record`, but consume complete
trajectory matrices with shape ``(time, oscillator)``. The consecutive-lock
counter is threaded through every sample so downstream consumers can review
acquisition, loss, and reset events without raw state arrays.
Parameters
----------
phases_by_step : ArrayLike
Phase history, shape ``(n_steps, N)``.
positions_by_step : ArrayLike
Position history, shape ``(n_steps, N)``.
times : ArrayLike | None
Per-step timestamps in seconds, or ``None`` for unit spacing.
reference_phase : object
Reference phase for the lock criterion, in radians.
reference_point : object
Reference axial coordinate for the spatial-margin criterion.
phase_tol_rad : object
Phase lock tolerance in radians.
spatial_tol_m : object
Spatial lock tolerance in metres.
required_consecutive_samples : object
Consecutive in-tolerance samples required to declare lock.
tolerance_profile : object | None
Named tolerance profile, or ``None`` for the baseline profile.
Returns
-------
PHACTimelineRecord
The deterministic PHA-C lock/loss timeline record.
Raises
------
ValueError
If any input is invalid.
"""
phase_matrix = _as_float_matrix(phases_by_step, name="phases_by_step")
position_matrix = _as_float_matrix(positions_by_step, name="positions_by_step")
if position_matrix.shape != phase_matrix.shape:
raise ValueError("positions_by_step must have the same shape as phases_by_step")
sample_count, oscillator_count = phase_matrix.shape
time_vector = _as_time_vector(times, sample_count=sample_count)
records: list[PHACHandoffRecord] = []
consecutive = 0
for index, timestamp in enumerate(time_vector):
record = build_pha_c_handoff_record(
phase_matrix[index],
position_matrix[index],
t=float(timestamp),
reference_phase=reference_phase,
reference_point=reference_point,
phase_tol_rad=phase_tol_rad,
spatial_tol_m=spatial_tol_m,
required_consecutive_samples=required_consecutive_samples,
prior_consecutive_lock_samples=consecutive,
tolerance_profile=tolerance_profile,
)
consecutive = record.consecutive_lock_samples
records.append(record)
first = records[0]
first_lock_index = next(
(index for index, record in enumerate(records) if record.lock_achieved),
-1,
)
first_lock_observed = first_lock_index >= 0
first_lock_time = (
float(time_vector[first_lock_index]) if first_lock_observed else 0.0
)
transition_rows = _transition_table(records)
record_dicts = [record.to_dict() for record in records]
sample_records_sha256 = _sha256_json(record_dicts)
time_state_sha256 = _vector_sha256(time_vector)
transition_table_sha256 = _sha256_json(transition_rows)
timeline_payload = _timeline_dict_without_hash(
sample_count=sample_count,
oscillator_count=oscillator_count,
start_time=float(time_vector[0]),
end_time=float(time_vector[-1]),
duration_s=float(time_vector[-1] - time_vector[0]),
first_lock_index=first_lock_index,
first_lock_time=first_lock_time,
first_lock_observed=first_lock_observed,
final_lock_achieved=records[-1].lock_achieved,
lock_sample_count=sum(record.lock_achieved for record in records),
phase_lock_sample_count=sum(record.phase_locked for record in records),
spatial_lock_sample_count=sum(record.spatial_locked for record in records),
lock_loss_count=sum(row["lock_lost"] for row in transition_rows),
reset_count=sum(row["reset"] for row in transition_rows),
max_consecutive_lock_samples=max(
record.consecutive_lock_samples for record in records
),
max_phase_dispersion_rad=max(record.phase_dispersion_rad for record in records),
max_spatial_dispersion_m=max(record.spatial_dispersion_m for record in records),
min_phase_margin_rad=min(record.phase_margin_rad for record in records),
min_spatial_margin_m=min(record.spatial_margin_m for record in records),
min_phase_order_parameter=min(
record.phase_order_parameter for record in records
),
max_distance_to_reference_m=max(
record.distance_to_reference_max_m for record in records
),
reference_phase=first.reference_phase,
reference_point=first.reference_point,
phase_tol_rad=first.phase_tol_rad,
spatial_tol_m=first.spatial_tol_m,
tolerance_profile_name=first.tolerance_profile_name,
tolerance_profile_multiplier=first.tolerance_profile_multiplier,
required_consecutive_samples=first.required_consecutive_samples,
time_state_sha256=time_state_sha256,
sample_records_sha256=sample_records_sha256,
transition_table_sha256=transition_table_sha256,
)
return PHACTimelineRecord(
**cast(Any, timeline_payload),
timeline_sha256=_sha256_json(timeline_payload),
)
|
pha_c_event_timeline_to_dict
pha_c_event_timeline_to_dict(
timeline: PHACTimelineRecord,
) -> dict[str, float | int | bool | str]
Return the canonical JSON-safe PHA-C timeline payload.
Parameters
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
dict[str, float | int | bool | str]
The canonical JSON-safe PHA-C timeline payload.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def pha_c_event_timeline_to_dict(
timeline: PHACTimelineRecord,
) -> dict[str, float | int | bool | str]:
"""Return the canonical JSON-safe PHA-C timeline payload.
Parameters
----------
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
-------
dict[str, float | int | bool | str]
The canonical JSON-safe PHA-C timeline payload.
"""
payload = _timeline_dict_without_hash(
sample_count=timeline.sample_count,
oscillator_count=timeline.oscillator_count,
start_time=timeline.start_time,
end_time=timeline.end_time,
duration_s=timeline.duration_s,
first_lock_index=timeline.first_lock_index,
first_lock_time=timeline.first_lock_time,
first_lock_observed=timeline.first_lock_observed,
final_lock_achieved=timeline.final_lock_achieved,
lock_sample_count=timeline.lock_sample_count,
phase_lock_sample_count=timeline.phase_lock_sample_count,
spatial_lock_sample_count=timeline.spatial_lock_sample_count,
lock_loss_count=timeline.lock_loss_count,
reset_count=timeline.reset_count,
max_consecutive_lock_samples=timeline.max_consecutive_lock_samples,
max_phase_dispersion_rad=timeline.max_phase_dispersion_rad,
max_spatial_dispersion_m=timeline.max_spatial_dispersion_m,
min_phase_margin_rad=timeline.min_phase_margin_rad,
min_spatial_margin_m=timeline.min_spatial_margin_m,
min_phase_order_parameter=timeline.min_phase_order_parameter,
max_distance_to_reference_m=timeline.max_distance_to_reference_m,
reference_phase=timeline.reference_phase,
reference_point=timeline.reference_point,
phase_tol_rad=timeline.phase_tol_rad,
spatial_tol_m=timeline.spatial_tol_m,
tolerance_profile_name=timeline.tolerance_profile_name,
tolerance_profile_multiplier=timeline.tolerance_profile_multiplier,
required_consecutive_samples=timeline.required_consecutive_samples,
time_state_sha256=timeline.time_state_sha256,
sample_records_sha256=timeline.sample_records_sha256,
transition_table_sha256=timeline.transition_table_sha256,
)
payload["timeline_sha256"] = timeline.timeline_sha256
return payload
|
verify_pha_c_event_timeline
verify_pha_c_event_timeline(
timeline: PHACTimelineRecord,
) -> PHACTimelineRecord
Replay and validate a PHA-C event timeline hash and safety boundary.
Parameters
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
PHACTimelineRecord
The same timeline after replay and safety-boundary validation.
Raises
ValueError
If the timeline fails replay or safety-boundary validation.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def verify_pha_c_event_timeline(
timeline: PHACTimelineRecord,
) -> PHACTimelineRecord:
"""Replay and validate a PHA-C event timeline hash and safety boundary.
Parameters
----------
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
-------
PHACTimelineRecord
The same timeline after replay and safety-boundary validation.
Raises
------
ValueError
If the timeline fails replay or safety-boundary validation.
"""
if not isinstance(timeline, PHACTimelineRecord):
raise ValueError("timeline must be a PHACTimelineRecord")
_validate_sha256_hex(timeline.time_state_sha256, name="time_state_sha256")
_validate_sha256_hex(
timeline.sample_records_sha256,
name="sample_records_sha256",
)
_validate_sha256_hex(
timeline.transition_table_sha256,
name="transition_table_sha256",
)
timeline_hash = _validate_sha256_hex(
timeline.timeline_sha256,
name="timeline_sha256",
)
if timeline.claim_boundary != PHA_C_TIMELINE_CLAIM_BOUNDARY:
raise ValueError("claim_boundary must be the PHA-C timeline review boundary")
if timeline.evidence_kind != PHA_C_TIMELINE_EVIDENCE_KIND:
raise ValueError("evidence_kind must be deterministic timeline evidence")
if (
_validate_record_bool(
timeline.execution_disabled,
name="execution_disabled",
)
is not True
):
raise ValueError("execution_disabled must be true")
if _validate_record_bool(timeline.actuating, name="actuating") is not False:
raise ValueError("actuating must be false")
sample_count = _validate_record_int(
timeline.sample_count,
name="sample_count",
minimum=1,
)
_validate_record_int(timeline.oscillator_count, name="oscillator_count", minimum=1)
required = _validate_record_int(
timeline.required_consecutive_samples,
name="required_consecutive_samples",
minimum=1,
)
count_fields = (
"lock_sample_count",
"phase_lock_sample_count",
"spatial_lock_sample_count",
"lock_loss_count",
"reset_count",
"max_consecutive_lock_samples",
)
counts = {
field: _validate_record_int(getattr(timeline, field), name=field, minimum=0)
for field in count_fields
}
for field in (
"lock_sample_count",
"phase_lock_sample_count",
"spatial_lock_sample_count",
"max_consecutive_lock_samples",
):
if counts[field] > sample_count:
raise ValueError(f"{field} cannot exceed sample_count")
for field in ("lock_loss_count", "reset_count"):
if counts[field] > max(sample_count - 1, 0):
raise ValueError(f"{field} cannot exceed the transition count")
final_lock_achieved = _validate_record_bool(
timeline.final_lock_achieved,
name="final_lock_achieved",
)
if counts["max_consecutive_lock_samples"] < required and final_lock_achieved:
raise ValueError("final_lock_achieved requires the consecutive threshold")
start_time = _validate_real_scalar(timeline.start_time, name="start_time")
end_time = _validate_real_scalar(timeline.end_time, name="end_time")
duration_s = _validate_nonnegative_record_scalar(
timeline.duration_s,
name="duration_s",
)
if end_time < start_time:
raise ValueError("end_time must be greater than or equal to start_time")
if abs(duration_s - (end_time - start_time)) > 1.0e-12:
raise ValueError("duration_s must equal end_time - start_time")
first_lock_index = _validate_record_int(
timeline.first_lock_index,
name="first_lock_index",
minimum=-1,
)
first_lock_observed = _validate_record_bool(
timeline.first_lock_observed,
name="first_lock_observed",
)
first_lock_time = _validate_real_scalar(
timeline.first_lock_time,
name="first_lock_time",
)
if first_lock_observed:
if first_lock_index < 0 or first_lock_index >= sample_count:
raise ValueError("first_lock_index must refer to an observed sample")
if first_lock_time < start_time or first_lock_time > end_time:
raise ValueError("first_lock_time must be inside the timeline range")
else:
if first_lock_index != -1:
raise ValueError("first_lock_index must be -1 when no lock is observed")
if first_lock_time != 0.0:
raise ValueError("first_lock_time must be 0.0 when no lock is observed")
for field in (
"max_phase_dispersion_rad",
"max_spatial_dispersion_m",
"max_distance_to_reference_m",
"phase_tol_rad",
"spatial_tol_m",
):
_validate_nonnegative_record_scalar(getattr(timeline, field), name=field)
max_phase_dispersion = _validate_nonnegative_record_scalar(
timeline.max_phase_dispersion_rad,
name="max_phase_dispersion_rad",
)
max_spatial_dispersion = _validate_nonnegative_record_scalar(
timeline.max_spatial_dispersion_m,
name="max_spatial_dispersion_m",
)
phase_tol = _validate_nonnegative_record_scalar(
timeline.phase_tol_rad,
name="phase_tol_rad",
)
spatial_tol = _validate_nonnegative_record_scalar(
timeline.spatial_tol_m,
name="spatial_tol_m",
)
min_phase_margin = _validate_real_scalar(
timeline.min_phase_margin_rad,
name="min_phase_margin_rad",
)
min_spatial_margin = _validate_real_scalar(
timeline.min_spatial_margin_m,
name="min_spatial_margin_m",
)
if (
abs(min_phase_margin - (phase_tol - max_phase_dispersion))
> PHA_C_TIMELINE_MARGIN_REPLAY_TOLERANCE
):
raise ValueError(
"min_phase_margin_rad must equal phase_tol_rad - max_phase_dispersion_rad"
)
if (
abs(min_spatial_margin - (spatial_tol - max_spatial_dispersion))
> PHA_C_TIMELINE_MARGIN_REPLAY_TOLERANCE
):
raise ValueError(
"min_spatial_margin_m must equal spatial_tol_m - max_spatial_dispersion_m"
)
order_parameter = _validate_nonnegative_record_scalar(
timeline.min_phase_order_parameter,
name="min_phase_order_parameter",
)
if order_parameter > 1.0 + 1.0e-12:
raise ValueError("min_phase_order_parameter must be inside [0, 1]")
multiplier = _validate_real_scalar(
timeline.tolerance_profile_multiplier,
name="tolerance_profile_multiplier",
)
if multiplier <= 0.0:
raise ValueError("tolerance_profile_multiplier must be positive")
if (
not isinstance(timeline.tolerance_profile_name, str)
or not timeline.tolerance_profile_name
):
raise ValueError("tolerance_profile_name must be a non-empty string")
for field in ("reference_phase", "reference_point"):
_validate_real_scalar(getattr(timeline, field), name=field)
payload = pha_c_event_timeline_to_dict(timeline)
replay_payload = dict(payload)
replay_payload.pop("timeline_sha256")
if _sha256_json(replay_payload) != timeline_hash:
raise ValueError(
"timeline_sha256 does not match the canonical timeline payload",
)
return timeline
|
API documentation
pha_c_timeline
Deterministic PHA-C event timelines over moving-frame trajectories.
Single-sample PHA-C handoff records are useful review atoms. This module
builds the trajectory-level event timeline that downstream replay, MIF/FRC
review, and Studio panels need: first lock, lock loss, reset counts, tolerance
profile provenance, and stable hashes over every sample record. The timeline is
review-only and never enables actuation.
Classes
PHACTimelineRecord
dataclass
PHACTimelineRecord(
sample_count: int,
oscillator_count: int,
start_time: float,
end_time: float,
duration_s: float,
first_lock_index: int,
first_lock_time: float,
first_lock_observed: bool,
final_lock_achieved: bool,
lock_sample_count: int,
phase_lock_sample_count: int,
spatial_lock_sample_count: int,
lock_loss_count: int,
reset_count: int,
max_consecutive_lock_samples: int,
max_phase_dispersion_rad: float,
max_spatial_dispersion_m: float,
min_phase_margin_rad: float,
min_spatial_margin_m: float,
min_phase_order_parameter: float,
max_distance_to_reference_m: float,
reference_phase: float,
reference_point: float,
phase_tol_rad: float,
spatial_tol_m: float,
tolerance_profile_name: str,
tolerance_profile_multiplier: float,
required_consecutive_samples: int,
claim_boundary: str,
evidence_kind: str,
execution_disabled: bool,
actuating: bool,
time_state_sha256: str,
sample_records_sha256: str,
transition_table_sha256: str,
timeline_sha256: str,
)
Audit-ready PHA-C trajectory event timeline.
The record stores scalar trajectory evidence plus SHA-256 digests of the
time vector, per-sample handoff records, transition table, and final
timeline. It is designed for review and replay lanes, not for direct
control output.
Methods:
to_dict
to_dict() -> dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
Returns
dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def to_dict(self) -> dict[str, float | int | bool | str]:
"""Return a JSON-safe canonical representation.
Returns
-------
dict[str, float | int | bool | str]
Return a JSON-safe canonical representation.
"""
return pha_c_event_timeline_to_dict(self)
|
Functions:
build_pha_c_event_timeline
build_pha_c_event_timeline(
phases_by_step: ArrayLike,
positions_by_step: ArrayLike,
*,
times: ArrayLike | None = None,
reference_phase: object = 0.0,
reference_point: object = 0.0,
phase_tol_rad: object = DEFAULT_PHASE_TOL_RAD,
spatial_tol_m: object = DEFAULT_SPATIAL_TOL_M,
required_consecutive_samples: object = 3,
tolerance_profile: object | None = None,
) -> PHACTimelineRecord
Build deterministic PHA-C lock/loss timeline evidence.
Parameters mirror :func:build_pha_c_handoff_record, but consume complete
trajectory matrices with shape (time, oscillator). The consecutive-lock
counter is threaded through every sample so downstream consumers can review
acquisition, loss, and reset events without raw state arrays.
Parameters
phases_by_step : ArrayLike
Phase history, shape (n_steps, N).
positions_by_step : ArrayLike
Position history, shape (n_steps, N).
times : ArrayLike | None
Per-step timestamps in seconds, or None for unit spacing.
reference_phase : object
Reference phase for the lock criterion, in radians.
reference_point : object
Reference axial coordinate for the spatial-margin criterion.
phase_tol_rad : object
Phase lock tolerance in radians.
spatial_tol_m : object
Spatial lock tolerance in metres.
required_consecutive_samples : object
Consecutive in-tolerance samples required to declare lock.
tolerance_profile : object | None
Named tolerance profile, or None for the baseline profile.
Returns
PHACTimelineRecord
The deterministic PHA-C lock/loss timeline record.
Raises
ValueError
If any input is invalid.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def build_pha_c_event_timeline(
phases_by_step: ArrayLike,
positions_by_step: ArrayLike,
*,
times: ArrayLike | None = None,
reference_phase: object = 0.0,
reference_point: object = 0.0,
phase_tol_rad: object = DEFAULT_PHASE_TOL_RAD,
spatial_tol_m: object = DEFAULT_SPATIAL_TOL_M,
required_consecutive_samples: object = 3,
tolerance_profile: object | None = None,
) -> PHACTimelineRecord:
"""Build deterministic PHA-C lock/loss timeline evidence.
Parameters mirror :func:`build_pha_c_handoff_record`, but consume complete
trajectory matrices with shape ``(time, oscillator)``. The consecutive-lock
counter is threaded through every sample so downstream consumers can review
acquisition, loss, and reset events without raw state arrays.
Parameters
----------
phases_by_step : ArrayLike
Phase history, shape ``(n_steps, N)``.
positions_by_step : ArrayLike
Position history, shape ``(n_steps, N)``.
times : ArrayLike | None
Per-step timestamps in seconds, or ``None`` for unit spacing.
reference_phase : object
Reference phase for the lock criterion, in radians.
reference_point : object
Reference axial coordinate for the spatial-margin criterion.
phase_tol_rad : object
Phase lock tolerance in radians.
spatial_tol_m : object
Spatial lock tolerance in metres.
required_consecutive_samples : object
Consecutive in-tolerance samples required to declare lock.
tolerance_profile : object | None
Named tolerance profile, or ``None`` for the baseline profile.
Returns
-------
PHACTimelineRecord
The deterministic PHA-C lock/loss timeline record.
Raises
------
ValueError
If any input is invalid.
"""
phase_matrix = _as_float_matrix(phases_by_step, name="phases_by_step")
position_matrix = _as_float_matrix(positions_by_step, name="positions_by_step")
if position_matrix.shape != phase_matrix.shape:
raise ValueError("positions_by_step must have the same shape as phases_by_step")
sample_count, oscillator_count = phase_matrix.shape
time_vector = _as_time_vector(times, sample_count=sample_count)
records: list[PHACHandoffRecord] = []
consecutive = 0
for index, timestamp in enumerate(time_vector):
record = build_pha_c_handoff_record(
phase_matrix[index],
position_matrix[index],
t=float(timestamp),
reference_phase=reference_phase,
reference_point=reference_point,
phase_tol_rad=phase_tol_rad,
spatial_tol_m=spatial_tol_m,
required_consecutive_samples=required_consecutive_samples,
prior_consecutive_lock_samples=consecutive,
tolerance_profile=tolerance_profile,
)
consecutive = record.consecutive_lock_samples
records.append(record)
first = records[0]
first_lock_index = next(
(index for index, record in enumerate(records) if record.lock_achieved),
-1,
)
first_lock_observed = first_lock_index >= 0
first_lock_time = (
float(time_vector[first_lock_index]) if first_lock_observed else 0.0
)
transition_rows = _transition_table(records)
record_dicts = [record.to_dict() for record in records]
sample_records_sha256 = _sha256_json(record_dicts)
time_state_sha256 = _vector_sha256(time_vector)
transition_table_sha256 = _sha256_json(transition_rows)
timeline_payload = _timeline_dict_without_hash(
sample_count=sample_count,
oscillator_count=oscillator_count,
start_time=float(time_vector[0]),
end_time=float(time_vector[-1]),
duration_s=float(time_vector[-1] - time_vector[0]),
first_lock_index=first_lock_index,
first_lock_time=first_lock_time,
first_lock_observed=first_lock_observed,
final_lock_achieved=records[-1].lock_achieved,
lock_sample_count=sum(record.lock_achieved for record in records),
phase_lock_sample_count=sum(record.phase_locked for record in records),
spatial_lock_sample_count=sum(record.spatial_locked for record in records),
lock_loss_count=sum(row["lock_lost"] for row in transition_rows),
reset_count=sum(row["reset"] for row in transition_rows),
max_consecutive_lock_samples=max(
record.consecutive_lock_samples for record in records
),
max_phase_dispersion_rad=max(record.phase_dispersion_rad for record in records),
max_spatial_dispersion_m=max(record.spatial_dispersion_m for record in records),
min_phase_margin_rad=min(record.phase_margin_rad for record in records),
min_spatial_margin_m=min(record.spatial_margin_m for record in records),
min_phase_order_parameter=min(
record.phase_order_parameter for record in records
),
max_distance_to_reference_m=max(
record.distance_to_reference_max_m for record in records
),
reference_phase=first.reference_phase,
reference_point=first.reference_point,
phase_tol_rad=first.phase_tol_rad,
spatial_tol_m=first.spatial_tol_m,
tolerance_profile_name=first.tolerance_profile_name,
tolerance_profile_multiplier=first.tolerance_profile_multiplier,
required_consecutive_samples=first.required_consecutive_samples,
time_state_sha256=time_state_sha256,
sample_records_sha256=sample_records_sha256,
transition_table_sha256=transition_table_sha256,
)
return PHACTimelineRecord(
**cast(Any, timeline_payload),
timeline_sha256=_sha256_json(timeline_payload),
)
|
pha_c_event_timeline_to_dict
pha_c_event_timeline_to_dict(
timeline: PHACTimelineRecord,
) -> dict[str, float | int | bool | str]
Return the canonical JSON-safe PHA-C timeline payload.
Parameters
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
dict[str, float | int | bool | str]
The canonical JSON-safe PHA-C timeline payload.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def pha_c_event_timeline_to_dict(
timeline: PHACTimelineRecord,
) -> dict[str, float | int | bool | str]:
"""Return the canonical JSON-safe PHA-C timeline payload.
Parameters
----------
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
-------
dict[str, float | int | bool | str]
The canonical JSON-safe PHA-C timeline payload.
"""
payload = _timeline_dict_without_hash(
sample_count=timeline.sample_count,
oscillator_count=timeline.oscillator_count,
start_time=timeline.start_time,
end_time=timeline.end_time,
duration_s=timeline.duration_s,
first_lock_index=timeline.first_lock_index,
first_lock_time=timeline.first_lock_time,
first_lock_observed=timeline.first_lock_observed,
final_lock_achieved=timeline.final_lock_achieved,
lock_sample_count=timeline.lock_sample_count,
phase_lock_sample_count=timeline.phase_lock_sample_count,
spatial_lock_sample_count=timeline.spatial_lock_sample_count,
lock_loss_count=timeline.lock_loss_count,
reset_count=timeline.reset_count,
max_consecutive_lock_samples=timeline.max_consecutive_lock_samples,
max_phase_dispersion_rad=timeline.max_phase_dispersion_rad,
max_spatial_dispersion_m=timeline.max_spatial_dispersion_m,
min_phase_margin_rad=timeline.min_phase_margin_rad,
min_spatial_margin_m=timeline.min_spatial_margin_m,
min_phase_order_parameter=timeline.min_phase_order_parameter,
max_distance_to_reference_m=timeline.max_distance_to_reference_m,
reference_phase=timeline.reference_phase,
reference_point=timeline.reference_point,
phase_tol_rad=timeline.phase_tol_rad,
spatial_tol_m=timeline.spatial_tol_m,
tolerance_profile_name=timeline.tolerance_profile_name,
tolerance_profile_multiplier=timeline.tolerance_profile_multiplier,
required_consecutive_samples=timeline.required_consecutive_samples,
time_state_sha256=timeline.time_state_sha256,
sample_records_sha256=timeline.sample_records_sha256,
transition_table_sha256=timeline.transition_table_sha256,
)
payload["timeline_sha256"] = timeline.timeline_sha256
return payload
|
verify_pha_c_event_timeline
verify_pha_c_event_timeline(
timeline: PHACTimelineRecord,
) -> PHACTimelineRecord
Replay and validate a PHA-C event timeline hash and safety boundary.
Parameters
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
PHACTimelineRecord
The same timeline after replay and safety-boundary validation.
Raises
ValueError
If the timeline fails replay or safety-boundary validation.
Source code in src/scpn_phase_orchestrator/upde/pha_c_timeline.py
| def verify_pha_c_event_timeline(
timeline: PHACTimelineRecord,
) -> PHACTimelineRecord:
"""Replay and validate a PHA-C event timeline hash and safety boundary.
Parameters
----------
timeline : PHACTimelineRecord
The PHA-C event-timeline record to operate on.
Returns
-------
PHACTimelineRecord
The same timeline after replay and safety-boundary validation.
Raises
------
ValueError
If the timeline fails replay or safety-boundary validation.
"""
if not isinstance(timeline, PHACTimelineRecord):
raise ValueError("timeline must be a PHACTimelineRecord")
_validate_sha256_hex(timeline.time_state_sha256, name="time_state_sha256")
_validate_sha256_hex(
timeline.sample_records_sha256,
name="sample_records_sha256",
)
_validate_sha256_hex(
timeline.transition_table_sha256,
name="transition_table_sha256",
)
timeline_hash = _validate_sha256_hex(
timeline.timeline_sha256,
name="timeline_sha256",
)
if timeline.claim_boundary != PHA_C_TIMELINE_CLAIM_BOUNDARY:
raise ValueError("claim_boundary must be the PHA-C timeline review boundary")
if timeline.evidence_kind != PHA_C_TIMELINE_EVIDENCE_KIND:
raise ValueError("evidence_kind must be deterministic timeline evidence")
if (
_validate_record_bool(
timeline.execution_disabled,
name="execution_disabled",
)
is not True
):
raise ValueError("execution_disabled must be true")
if _validate_record_bool(timeline.actuating, name="actuating") is not False:
raise ValueError("actuating must be false")
sample_count = _validate_record_int(
timeline.sample_count,
name="sample_count",
minimum=1,
)
_validate_record_int(timeline.oscillator_count, name="oscillator_count", minimum=1)
required = _validate_record_int(
timeline.required_consecutive_samples,
name="required_consecutive_samples",
minimum=1,
)
count_fields = (
"lock_sample_count",
"phase_lock_sample_count",
"spatial_lock_sample_count",
"lock_loss_count",
"reset_count",
"max_consecutive_lock_samples",
)
counts = {
field: _validate_record_int(getattr(timeline, field), name=field, minimum=0)
for field in count_fields
}
for field in (
"lock_sample_count",
"phase_lock_sample_count",
"spatial_lock_sample_count",
"max_consecutive_lock_samples",
):
if counts[field] > sample_count:
raise ValueError(f"{field} cannot exceed sample_count")
for field in ("lock_loss_count", "reset_count"):
if counts[field] > max(sample_count - 1, 0):
raise ValueError(f"{field} cannot exceed the transition count")
final_lock_achieved = _validate_record_bool(
timeline.final_lock_achieved,
name="final_lock_achieved",
)
if counts["max_consecutive_lock_samples"] < required and final_lock_achieved:
raise ValueError("final_lock_achieved requires the consecutive threshold")
start_time = _validate_real_scalar(timeline.start_time, name="start_time")
end_time = _validate_real_scalar(timeline.end_time, name="end_time")
duration_s = _validate_nonnegative_record_scalar(
timeline.duration_s,
name="duration_s",
)
if end_time < start_time:
raise ValueError("end_time must be greater than or equal to start_time")
if abs(duration_s - (end_time - start_time)) > 1.0e-12:
raise ValueError("duration_s must equal end_time - start_time")
first_lock_index = _validate_record_int(
timeline.first_lock_index,
name="first_lock_index",
minimum=-1,
)
first_lock_observed = _validate_record_bool(
timeline.first_lock_observed,
name="first_lock_observed",
)
first_lock_time = _validate_real_scalar(
timeline.first_lock_time,
name="first_lock_time",
)
if first_lock_observed:
if first_lock_index < 0 or first_lock_index >= sample_count:
raise ValueError("first_lock_index must refer to an observed sample")
if first_lock_time < start_time or first_lock_time > end_time:
raise ValueError("first_lock_time must be inside the timeline range")
else:
if first_lock_index != -1:
raise ValueError("first_lock_index must be -1 when no lock is observed")
if first_lock_time != 0.0:
raise ValueError("first_lock_time must be 0.0 when no lock is observed")
for field in (
"max_phase_dispersion_rad",
"max_spatial_dispersion_m",
"max_distance_to_reference_m",
"phase_tol_rad",
"spatial_tol_m",
):
_validate_nonnegative_record_scalar(getattr(timeline, field), name=field)
max_phase_dispersion = _validate_nonnegative_record_scalar(
timeline.max_phase_dispersion_rad,
name="max_phase_dispersion_rad",
)
max_spatial_dispersion = _validate_nonnegative_record_scalar(
timeline.max_spatial_dispersion_m,
name="max_spatial_dispersion_m",
)
phase_tol = _validate_nonnegative_record_scalar(
timeline.phase_tol_rad,
name="phase_tol_rad",
)
spatial_tol = _validate_nonnegative_record_scalar(
timeline.spatial_tol_m,
name="spatial_tol_m",
)
min_phase_margin = _validate_real_scalar(
timeline.min_phase_margin_rad,
name="min_phase_margin_rad",
)
min_spatial_margin = _validate_real_scalar(
timeline.min_spatial_margin_m,
name="min_spatial_margin_m",
)
if (
abs(min_phase_margin - (phase_tol - max_phase_dispersion))
> PHA_C_TIMELINE_MARGIN_REPLAY_TOLERANCE
):
raise ValueError(
"min_phase_margin_rad must equal phase_tol_rad - max_phase_dispersion_rad"
)
if (
abs(min_spatial_margin - (spatial_tol - max_spatial_dispersion))
> PHA_C_TIMELINE_MARGIN_REPLAY_TOLERANCE
):
raise ValueError(
"min_spatial_margin_m must equal spatial_tol_m - max_spatial_dispersion_m"
)
order_parameter = _validate_nonnegative_record_scalar(
timeline.min_phase_order_parameter,
name="min_phase_order_parameter",
)
if order_parameter > 1.0 + 1.0e-12:
raise ValueError("min_phase_order_parameter must be inside [0, 1]")
multiplier = _validate_real_scalar(
timeline.tolerance_profile_multiplier,
name="tolerance_profile_multiplier",
)
if multiplier <= 0.0:
raise ValueError("tolerance_profile_multiplier must be positive")
if (
not isinstance(timeline.tolerance_profile_name, str)
or not timeline.tolerance_profile_name
):
raise ValueError("tolerance_profile_name must be a non-empty string")
for field in ("reference_phase", "reference_point"):
_validate_real_scalar(getattr(timeline, field), name=field)
payload = pha_c_event_timeline_to_dict(timeline)
replay_payload = dict(payload)
replay_payload.pop("timeline_sha256")
if _sha256_json(replay_payload) != timeline_hash:
raise ValueError(
"timeline_sha256 does not match the canonical timeline payload",
)
return timeline
|