added ruff test
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -204,9 +204,9 @@ def _paint_shared_flow_arrows(
|
||||
canvas: np.ndarray,
|
||||
flow: np.ndarray,
|
||||
*,
|
||||
valid_mask: Optional[np.ndarray],
|
||||
valid_mask: np.ndarray | None,
|
||||
policy,
|
||||
colormap: Optional[np.ndarray],
|
||||
colormap: np.ndarray | None,
|
||||
below_threshold_color: tuple[int, int, int],
|
||||
) -> np.ndarray:
|
||||
"""Draw arrows from :func:`select_flow_samples` so 2D matches 3D sample points."""
|
||||
@@ -240,14 +240,14 @@ def _paint_shared_flow_arrows(
|
||||
|
||||
def draw_arrow_display(
|
||||
processed_frame: np.ndarray,
|
||||
flow: Optional[np.ndarray],
|
||||
flow: np.ndarray | None,
|
||||
*,
|
||||
background: str = "pure_color",
|
||||
canvas_bgr: Sequence[int] | None = None,
|
||||
draw_style: str = "box",
|
||||
valid_mask: Optional[np.ndarray] = None,
|
||||
domain_mask: Optional[np.ndarray] = None,
|
||||
colormap: Optional[np.ndarray] = None,
|
||||
valid_mask: np.ndarray | None = None,
|
||||
domain_mask: np.ndarray | None = None,
|
||||
colormap: np.ndarray | None = None,
|
||||
exterior_bgr: Sequence[int] | None = None,
|
||||
island_bgr: Sequence[int] | None = None,
|
||||
flow_on_px: float | None = None,
|
||||
@@ -333,12 +333,12 @@ def draw_arrow_display(
|
||||
|
||||
|
||||
def depth_map_to_bgr(
|
||||
depth: Optional[np.ndarray],
|
||||
colormap: Optional[np.ndarray] = None,
|
||||
depth: np.ndarray | None,
|
||||
colormap: np.ndarray | None = None,
|
||||
*,
|
||||
value_range: Optional[tuple[float, float]] = None,
|
||||
invalid_mask: Optional[np.ndarray] = None,
|
||||
) -> Optional[np.ndarray]:
|
||||
value_range: tuple[float, float] | None = None,
|
||||
invalid_mask: np.ndarray | None = None,
|
||||
) -> np.ndarray | None:
|
||||
"""Convert a float depth/divergence map to a colormap BGR image.
|
||||
|
||||
``value_range`` pins the colour scale. Per-frame re-normalisation (the old
|
||||
|
||||
@@ -5,23 +5,26 @@ from __future__ import annotations
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.acquisition.sinks import RecordSinkMixin
|
||||
# The packet and the geometry come from ``models``, so the finger worker does not
|
||||
# depend on the box worker; only genuinely shared helpers are imported from it.
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
ContactObservation,
|
||||
FixedCameraMode,
|
||||
SdkPacket,
|
||||
ensure_bgr as _ensure_bgr,
|
||||
is_file_source as _is_file_source,
|
||||
load_fixed_mode,
|
||||
orisys_importable,
|
||||
)
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
ensure_bgr as _ensure_bgr,
|
||||
)
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
is_file_source as _is_file_source,
|
||||
)
|
||||
from orisys_app_shared.acquisition.sdk_worker import (
|
||||
CAMERA_DISCONNECT_TIMEOUT_S,
|
||||
FILE_FIRST_FRAME_TIMEOUT_S,
|
||||
@@ -38,6 +41,7 @@ from orisys_app_shared.acquisition.sdk_worker import (
|
||||
process_chain,
|
||||
read_forces_from_sensor,
|
||||
)
|
||||
from orisys_app_shared.acquisition.sinks import RecordSinkMixin
|
||||
from orisys_app_shared.force.filters import ALL_KEYS, FilterSettings, effective_filters
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -74,10 +78,10 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
on_error: Optional[Callable[[str], None]] = None,
|
||||
on_status: Optional[Callable[[str], None]] = None,
|
||||
on_calibration_applied: Optional[Callable[[str, str, int, float, int], None]] = None,
|
||||
on_reset_applied: Optional[Callable[[int, float, int], None]] = None,
|
||||
on_error: Callable[[str], None] | None = None,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
on_calibration_applied: Callable[[str, str, int, float, int], None] | None = None,
|
||||
on_reset_applied: Callable[[int, float, int], None] | None = None,
|
||||
):
|
||||
self._init_sinks()
|
||||
self._on_error = on_error
|
||||
@@ -87,23 +91,23 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
self._on_calibration_applied = on_calibration_applied
|
||||
#: Called from the acquisition thread once a queued zeroing took effect.
|
||||
self._on_reset_applied = on_reset_applied
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stop = threading.Event()
|
||||
#: Set while the operator has paused: the camera stays open and the source keeps
|
||||
#: being read, but no frame is processed and no packet is emitted.
|
||||
self._pause = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
self._latest: Optional[SdkPacket] = None
|
||||
self._latest: SdkPacket | None = None
|
||||
self._session_id = 0
|
||||
# Keyed by session so a session that is still winding down can only
|
||||
# ever close its own runtime, never the runtime of a newer session.
|
||||
self._runtimes: dict[int, object] = {}
|
||||
self._mode: Optional[FixedCameraMode] = None
|
||||
self._video_source: Optional[str | int] = None
|
||||
self._export_dir: Optional[Path] = None
|
||||
self._end_reason: Optional[str] = None
|
||||
self._mode: FixedCameraMode | None = None
|
||||
self._video_source: str | int | None = None
|
||||
self._export_dir: Path | None = None
|
||||
self._end_reason: str | None = None
|
||||
self._phase: str = "idle"
|
||||
self._pending_calibration: Optional[str] = None
|
||||
self._pending_calibration: str | None = None
|
||||
#: A zeroing requested from another thread, applied by this one.
|
||||
self._pending_reset: bool = False
|
||||
self._mono_t0: float = 0.0
|
||||
@@ -119,7 +123,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
#: reported rate is an echo of the request and cannot be trusted).
|
||||
self._measured_capture_fps: float = 0.0
|
||||
|
||||
def set_filter_settings(self, settings: Optional[FilterSettings]) -> None:
|
||||
def set_filter_settings(self, settings: FilterSettings | None) -> None:
|
||||
"""Choose the filters for the **next** session."""
|
||||
with self._lock:
|
||||
self._filter_settings = (settings or FilterSettings()).normalised()
|
||||
@@ -159,11 +163,11 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
with self._lock:
|
||||
return self._phase
|
||||
|
||||
def latest(self) -> Optional[SdkPacket]:
|
||||
def latest(self) -> SdkPacket | None:
|
||||
with self._lock:
|
||||
return self._latest
|
||||
|
||||
def consume_end_reason(self) -> Optional[str]:
|
||||
def consume_end_reason(self) -> str | None:
|
||||
with self._lock:
|
||||
reason = self._end_reason
|
||||
self._end_reason = None
|
||||
@@ -233,7 +237,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
self._settle(owner=thread)
|
||||
# Otherwise the thread settles itself on the way out.
|
||||
|
||||
def _settle(self, *, owner: Optional[threading.Thread]) -> None:
|
||||
def _settle(self, *, owner: threading.Thread | None) -> None:
|
||||
"""Release worker state once no thread owns the runtime any more."""
|
||||
if owner is not None and self._thread is not owner:
|
||||
return # a newer session owns the worker now
|
||||
@@ -352,7 +356,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
if self._on_status is not None:
|
||||
self._on_status(message)
|
||||
|
||||
def _run(self, session: int, previous: Optional[threading.Thread] = None) -> None:
|
||||
def _run(self, session: int, previous: threading.Thread | None = None) -> None:
|
||||
# A new session always starts unpaused: pausing is a statement about the session the
|
||||
# operator is watching, not a sticky property of the device.
|
||||
self._pause.clear()
|
||||
@@ -368,7 +372,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def _run_session(self, session: int, previous: Optional[threading.Thread]) -> None:
|
||||
def _run_session(self, session: int, previous: threading.Thread | None) -> None:
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
if previous is not None and previous.is_alive():
|
||||
@@ -443,14 +447,14 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
capture_fps = 0.0
|
||||
saw_frame = False
|
||||
last_frame_mono = 0.0
|
||||
none_streak_start: Optional[float] = None
|
||||
none_streak_start: float | None = None
|
||||
# Per-session monotonic frame index: the join key shared by the video,
|
||||
# the CSV and the calibration records.
|
||||
frame_id = 0
|
||||
#: The last frame each derived array was *actually* produced for, so a stale
|
||||
#: array can say which frame it belongs to instead of being read as new data.
|
||||
last_flow_frame_id: Optional[int] = None
|
||||
last_depth_frame_id: Optional[int] = None
|
||||
last_flow_frame_id: int | None = None
|
||||
last_depth_frame_id: int | None = None
|
||||
#: When this session's loop started, for the file-source deadline below.
|
||||
session_started = time.perf_counter()
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Mapping, Optional
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import ExpectedGeometry, error
|
||||
from orisys_app_shared.scene3d.digest import source_geometry_digest
|
||||
@@ -56,7 +56,7 @@ class DeviceGeometry:
|
||||
detail: Mapping[str, str]
|
||||
|
||||
|
||||
def map_path_for(assets_dir: Path, source_scale: Optional[float]) -> Path:
|
||||
def map_path_for(assets_dir: Path, source_scale: float | None) -> Path:
|
||||
"""The unroll map the SDK would load for this scale, inside ``assets_dir``."""
|
||||
if source_scale is None or abs(float(source_scale) - 1.0) < 1e-9:
|
||||
return Path(assets_dir) / f"{_MAP_STEM}{_MAP_SUFFIX}"
|
||||
@@ -75,7 +75,7 @@ def _read_json(path: Path) -> dict:
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _bundle_root(config_ref: str | Path) -> Optional[Path]:
|
||||
def _bundle_root(config_ref: str | Path) -> Path | None:
|
||||
path = Path(str(config_ref))
|
||||
if path.is_file():
|
||||
return path.parent
|
||||
|
||||
@@ -22,8 +22,8 @@ NumPy only: the Qt conversion belongs to the UI layer (``ui/arrow_panel.bgr_to_q
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -113,16 +113,16 @@ class DomainMaskLatch:
|
||||
array a frame carries, which is what makes holding it safe.
|
||||
"""
|
||||
|
||||
__slots__ = ("_mask", "min_iou", "adoptions", "held")
|
||||
__slots__ = ("_mask", "adoptions", "held", "min_iou")
|
||||
|
||||
def __init__(self, *, min_iou: float = DOMAIN_MASK_MIN_IOU) -> None:
|
||||
self._mask: Optional[np.ndarray] = None
|
||||
self._mask: np.ndarray | None = None
|
||||
self.min_iou = float(min_iou)
|
||||
self.adoptions = 0
|
||||
self.held = 0
|
||||
|
||||
@property
|
||||
def mask(self) -> Optional[np.ndarray]:
|
||||
def mask(self) -> np.ndarray | None:
|
||||
return self._mask
|
||||
|
||||
def reset(self) -> None:
|
||||
@@ -131,7 +131,7 @@ class DomainMaskLatch:
|
||||
self.adoptions = 0
|
||||
self.held = 0
|
||||
|
||||
def update(self, mask: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
||||
def update(self, mask: np.ndarray | None) -> np.ndarray | None:
|
||||
"""The mask to draw the boundary with, given this frame's own mask.
|
||||
|
||||
A frame that carries no mask cannot move the boundary, so it inherits the
|
||||
@@ -161,10 +161,10 @@ class ImageFrame:
|
||||
would have to invent them.
|
||||
"""
|
||||
|
||||
image_bgr: Optional[np.ndarray] = None
|
||||
flow: Optional[np.ndarray] = None
|
||||
depth: Optional[np.ndarray] = None
|
||||
valid_mask: Optional[np.ndarray] = None
|
||||
image_bgr: np.ndarray | None = None
|
||||
flow: np.ndarray | None = None
|
||||
depth: np.ndarray | None = None
|
||||
valid_mask: np.ndarray | None = None
|
||||
source: str = SOURCE_LIVE
|
||||
frame_id: int = 0
|
||||
t_rel_s: float = 0.0
|
||||
@@ -172,10 +172,10 @@ class ImageFrame:
|
||||
#: coordinator's "stale" state); the panel can say so under the picture.
|
||||
stale: bool = False
|
||||
#: Per-field provenance mirroring ``SdkPacket``; ``None`` means unstated.
|
||||
flow_valid: Optional[bool] = None
|
||||
flow_frame_id: Optional[int] = None
|
||||
depth_valid: Optional[bool] = None
|
||||
depth_frame_id: Optional[int] = None
|
||||
flow_valid: bool | None = None
|
||||
flow_frame_id: int | None = None
|
||||
depth_valid: bool | None = None
|
||||
depth_frame_id: int | None = None
|
||||
|
||||
def has_flow(self) -> bool:
|
||||
if self.flow is None:
|
||||
@@ -216,7 +216,7 @@ class ImagePlan:
|
||||
wanted = str(mode or "")
|
||||
return wanted if self.accepts(wanted) else self.default_mode
|
||||
|
||||
def needs(self, mode: str) -> Optional[str]:
|
||||
def needs(self, mode: str) -> str | None:
|
||||
"""The field a mode needs that this frame does not have, if any."""
|
||||
wanted = str(mode or "")
|
||||
if wanted in (ARROW_PURE, ARROW_STITCHED) and not self.has_flow():
|
||||
@@ -277,15 +277,15 @@ def plan_image(frame: ImageFrame) -> ImagePlan:
|
||||
|
||||
def render_image(
|
||||
frame: ImageFrame,
|
||||
plan: Optional[ImagePlan] = None,
|
||||
plan: ImagePlan | None = None,
|
||||
*,
|
||||
mode: str = "",
|
||||
theme: str = "dark",
|
||||
draw_style: str = "box",
|
||||
depth_range: Optional[tuple[float, float]] = None,
|
||||
canvas_bgr: Optional[Sequence[int]] = None,
|
||||
domain_mask: Optional[np.ndarray] = None,
|
||||
) -> Optional[np.ndarray]:
|
||||
depth_range: tuple[float, float] | None = None,
|
||||
canvas_bgr: Sequence[int] | None = None,
|
||||
domain_mask: np.ndarray | None = None,
|
||||
) -> np.ndarray | None:
|
||||
"""Draw ``frame`` in ``mode``; ``None`` when there is nothing to draw.
|
||||
|
||||
The mode is resolved through the plan first, so a caller that asks for a view the
|
||||
|
||||
@@ -11,7 +11,6 @@ import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -41,7 +40,7 @@ class ContactObservation:
|
||||
without this field do not invent a yellow contact arrow.
|
||||
"""
|
||||
|
||||
centroid_uv_xy: Optional[tuple[float, float]] = None
|
||||
centroid_uv_xy: tuple[float, float] | None = None
|
||||
found: bool = False
|
||||
valid: bool = False
|
||||
frame_id: int = 0
|
||||
@@ -82,9 +81,9 @@ class SdkPacket:
|
||||
#: clock that steps backwards cannot reorder history. ``None`` means the
|
||||
#: producer did not measure one, and the wall clock is used instead — a
|
||||
#: default of ``0.0`` would have made every frame look simultaneous.
|
||||
mono_t: Optional[float] = None
|
||||
mono_t: float | None = None
|
||||
#: Wall clock at hand-off, for metadata only. ``None`` means unmeasured.
|
||||
wall_t: Optional[float] = None
|
||||
wall_t: float | None = None
|
||||
#: False when the force readout failed. Invalid packets carry NaN forces and
|
||||
#: must never be averaged into calibration data or written as zeros.
|
||||
valid: bool = True
|
||||
@@ -99,9 +98,9 @@ class SdkPacket:
|
||||
#: False when the sensor exposes no RAW channels; the raw fields are then
|
||||
#: NaN rather than a copy of the calibrated values.
|
||||
raw_available: bool = True
|
||||
flow: Optional[np.ndarray] = None
|
||||
depth: Optional[np.ndarray] = None
|
||||
valid_mask: Optional[np.ndarray] = None
|
||||
flow: np.ndarray | None = None
|
||||
depth: np.ndarray | None = None
|
||||
valid_mask: np.ndarray | None = None
|
||||
# --- per-field provenance -------------------------------------------------
|
||||
# The 3D view colours *one* array, and "the readout failed" is not the same
|
||||
# statement as "the tracker's field is stale". ``valid`` above is about the force
|
||||
@@ -109,18 +108,18 @@ class SdkPacket:
|
||||
# frame may claim. ``None`` means the producer made no statement, and a consumer
|
||||
# must not invent one: an unstated field is unverified, not fresh.
|
||||
#: Whether ``flow`` was produced for this frame's tracker pass.
|
||||
flow_valid: Optional[bool] = None
|
||||
flow_valid: bool | None = None
|
||||
#: The frame id ``flow`` belongs to, when it is not this frame's.
|
||||
flow_frame_id: Optional[int] = None
|
||||
flow_frame_id: int | None = None
|
||||
#: Whether ``depth`` is this frame's deformation output (a failed chain leaves the
|
||||
#: previous frame's depth in place).
|
||||
depth_valid: Optional[bool] = None
|
||||
depth_frame_id: Optional[int] = None
|
||||
depth_valid: bool | None = None
|
||||
depth_frame_id: int | None = None
|
||||
#: Whether the force triplet belongs to this frame (False when the chain or the
|
||||
#: readout failed, whatever ``valid`` says about the record).
|
||||
forces_valid: Optional[bool] = None
|
||||
forces_valid: bool | None = None
|
||||
#: Contact centre from ``compute_contact``; optional so old call sites stay valid.
|
||||
contact: Optional[ContactObservation] = None
|
||||
contact: ContactObservation | None = None
|
||||
|
||||
|
||||
def load_fixed_mode(config_ref: str | Path) -> FixedCameraMode:
|
||||
|
||||
@@ -4,31 +4,32 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force.filters import FilterSettings, effective_filters
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
FixedCameraMode,
|
||||
SdkPacket,
|
||||
ensure_bgr as _ensure_bgr,
|
||||
is_file_source as _is_file_source,
|
||||
load_fixed_mode,
|
||||
orisys_importable,
|
||||
)
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
ensure_bgr as _ensure_bgr,
|
||||
)
|
||||
from orisys_app_shared.acquisition.models import (
|
||||
is_file_source as _is_file_source,
|
||||
)
|
||||
from orisys_app_shared.acquisition.sinks import RecordSinkMixin
|
||||
from orisys_app_shared.force.filters import FilterSettings, effective_filters
|
||||
|
||||
VIDEO_EOF_TIMEOUT_S = 0.5
|
||||
#: How long a file source may deliver *nothing at all* before the session gives up.
|
||||
@@ -94,8 +95,8 @@ def _high_resolution_timer():
|
||||
# definitions live in ``models`` so neither worker depends on the other.
|
||||
__all__ = [
|
||||
"FixedCameraMode",
|
||||
"SdkPacket",
|
||||
"SdkAcquisitionWorker",
|
||||
"SdkPacket",
|
||||
"calibration_digest",
|
||||
"capture_status_of",
|
||||
"load_fixed_mode",
|
||||
@@ -127,7 +128,7 @@ def _sensor_force_calibrated(sensor, *, config_ref: str | Path | None = None) ->
|
||||
return False
|
||||
|
||||
|
||||
def _capture_sequence(sensor) -> Optional[int]:
|
||||
def _capture_sequence(sensor) -> int | None:
|
||||
"""The capture source's delivered-frame counter, when it can mark a repeat.
|
||||
|
||||
``CaptureSource.read_latest`` returns the newest frame from its background
|
||||
@@ -148,7 +149,7 @@ def _capture_sequence(sensor) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def _capture_dropped(sensor) -> Optional[int]:
|
||||
def _capture_dropped(sensor) -> int | None:
|
||||
"""Frames the capture source published that nobody took in time.
|
||||
|
||||
The SDK's background reader counts one of these every time it replaces a
|
||||
@@ -252,7 +253,7 @@ def short_capture_fallacy(
|
||||
)
|
||||
|
||||
|
||||
def process_chain(sensor) -> tuple[Optional[np.ndarray], str]:
|
||||
def process_chain(sensor) -> tuple[np.ndarray | None, str]:
|
||||
"""Run the live processing chain for one already-read frame.
|
||||
|
||||
``get_img`` alone is not the pipeline: the SDK's depth map is produced by
|
||||
@@ -276,7 +277,7 @@ def process_chain(sensor) -> tuple[Optional[np.ndarray], str]:
|
||||
return read_depth(sensor), compute_error
|
||||
|
||||
|
||||
def read_depth(sensor) -> Optional[np.ndarray]:
|
||||
def read_depth(sensor) -> np.ndarray | None:
|
||||
"""The current depth channel of ``sensor``, or ``None`` when unavailable."""
|
||||
try:
|
||||
info = sensor.info
|
||||
@@ -455,7 +456,7 @@ def calibration_digest(path: str | Path) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def loaded_path(sensor) -> Optional[str]:
|
||||
def loaded_path(sensor) -> str | None:
|
||||
"""Where the sensor's active calibration came from, if it knows."""
|
||||
calibration = getattr(sensor, "calibration", None)
|
||||
if calibration is None:
|
||||
@@ -488,10 +489,10 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
on_error: Optional[Callable[[str], None]] = None,
|
||||
on_status: Optional[Callable[[str], None]] = None,
|
||||
on_calibration_applied: Optional[Callable[[str, str, int, float, int], None]] = None,
|
||||
on_reset_applied: Optional[Callable[[int, float, int], None]] = None,
|
||||
on_error: Callable[[str], None] | None = None,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
on_calibration_applied: Callable[[str, str, int, float, int], None] | None = None,
|
||||
on_reset_applied: Callable[[int, float, int], None] | None = None,
|
||||
):
|
||||
self._init_sinks()
|
||||
self._on_error = on_error
|
||||
@@ -504,22 +505,22 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
#: Called from the acquisition thread once a queued zeroing took effect,
|
||||
#: with ``(frame_id, mono_t, session_id)``.
|
||||
self._on_reset_applied = on_reset_applied
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stop = threading.Event()
|
||||
#: Set while the operator has paused: the device stays open and the capture keeps
|
||||
#: publishing, but nothing is computed and no packet is emitted.
|
||||
self._pause = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
self._latest: Optional[SdkPacket] = None
|
||||
self._latest: SdkPacket | None = None
|
||||
self._session_id = 0
|
||||
# Keyed by session so a session that is still winding down can only
|
||||
# ever close its own device, never the device of a newer session.
|
||||
self._sensors: dict[int, object] = {}
|
||||
self._mode: Optional[FixedCameraMode] = None
|
||||
self._video_source: Optional[str | int] = None
|
||||
self._end_reason: Optional[str] = None
|
||||
self._mode: FixedCameraMode | None = None
|
||||
self._video_source: str | int | None = None
|
||||
self._end_reason: str | None = None
|
||||
self._phase: str = "idle"
|
||||
self._pending_calibration: Optional[str] = None
|
||||
self._pending_calibration: str | None = None
|
||||
#: A zeroing requested from another thread, applied by this one.
|
||||
self._pending_reset: bool = False
|
||||
#: Monotonic origin for ``SdkPacket.mono_t``, reset per session.
|
||||
@@ -540,7 +541,7 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
#: reported rate is an echo of the request, so only this can be trusted.
|
||||
self._measured_capture_fps: float = 0.0
|
||||
|
||||
def set_filter_settings(self, settings: Optional[FilterSettings]) -> None:
|
||||
def set_filter_settings(self, settings: FilterSettings | None) -> None:
|
||||
"""Choose the filters for the **next** session.
|
||||
|
||||
Not applied to a running one on purpose: the filter controls are locked
|
||||
@@ -595,11 +596,11 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
with self._lock:
|
||||
return self._phase
|
||||
|
||||
def latest(self) -> Optional[SdkPacket]:
|
||||
def latest(self) -> SdkPacket | None:
|
||||
with self._lock:
|
||||
return self._latest
|
||||
|
||||
def consume_end_reason(self) -> Optional[str]:
|
||||
def consume_end_reason(self) -> str | None:
|
||||
with self._lock:
|
||||
reason = self._end_reason
|
||||
self._end_reason = None
|
||||
@@ -671,7 +672,7 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
# Otherwise the thread settles itself on the way out. Leaving
|
||||
# ``self._thread`` set is what stops the next ``start`` from racing it.
|
||||
|
||||
def _settle(self, *, owner: Optional[threading.Thread]) -> None:
|
||||
def _settle(self, *, owner: threading.Thread | None) -> None:
|
||||
"""Release worker state once no thread owns the device any more."""
|
||||
if owner is not None and self._thread is not owner:
|
||||
return # a newer session owns the worker now
|
||||
@@ -796,7 +797,7 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
if self._on_status is not None:
|
||||
self._on_status(message)
|
||||
|
||||
def _run(self, session: int, previous: Optional[threading.Thread] = None) -> None:
|
||||
def _run(self, session: int, previous: threading.Thread | None = None) -> None:
|
||||
# A new session always starts unpaused: pausing is a statement about the
|
||||
# session the operator is watching, not a sticky property of the device.
|
||||
self._pause.clear()
|
||||
@@ -816,7 +817,7 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def _run_session(self, session: int, previous: Optional[threading.Thread]) -> None:
|
||||
def _run_session(self, session: int, previous: threading.Thread | None) -> None:
|
||||
import orisys
|
||||
|
||||
if previous is not None and previous.is_alive():
|
||||
@@ -874,10 +875,10 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
frame_times: list[float] = []
|
||||
saw_frame = False
|
||||
last_frame_mono = 0.0
|
||||
none_streak_start: Optional[float] = None
|
||||
none_streak_start: float | None = None
|
||||
#: The capture sequence of the last frame that was actually *processed*:
|
||||
#: an unchanged counter means the same frame came back again.
|
||||
last_delivered: Optional[int] = None
|
||||
last_delivered: int | None = None
|
||||
#: When that frame arrived, and how far apart this device's frames are
|
||||
#: meant to be: the wait below never steps over the next frame's arrival.
|
||||
last_frame_at = 0.0
|
||||
@@ -887,7 +888,7 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
#: two rates says whether a low frame rate is the camera's or ours.
|
||||
source_times: list[tuple[float, int]] = []
|
||||
source_dropped = 0
|
||||
last_source_seq: Optional[int] = None
|
||||
last_source_seq: int | None = None
|
||||
capture_fps = 0.0
|
||||
frame_period = 1.0 / max(1.0, float(mode.fps or 30.0))
|
||||
# Per-session monotonic frame index: the join key shared by the video,
|
||||
@@ -895,8 +896,8 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
frame_id = 0
|
||||
#: The last frame each derived array was *actually* produced for (see the
|
||||
#: finger worker: a stale array must name the frame it belongs to).
|
||||
last_flow_frame_id: Optional[int] = None
|
||||
last_depth_frame_id: Optional[int] = None
|
||||
last_flow_frame_id: int | None = None
|
||||
last_depth_frame_id: int | None = None
|
||||
|
||||
#: When this session's loop started, for the file-source deadline below.
|
||||
session_started = time.perf_counter()
|
||||
|
||||
@@ -15,7 +15,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Any, Callable, Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""Camera discovery and raw preview helpers."""
|
||||
|
||||
from orisys_app_shared.camera.discovery import CameraInfo, list_cameras, pick_default_camera
|
||||
from orisys_app_shared.camera.discovery import (
|
||||
CameraInfo,
|
||||
list_cameras,
|
||||
pick_default_camera,
|
||||
)
|
||||
from orisys_app_shared.camera.preview import RawPreviewSession
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -114,7 +114,7 @@ def list_cameras(max_index: int = DEFAULT_MAX_CAMERA_INDEX) -> list[CameraInfo]:
|
||||
backends = _backend_candidates()
|
||||
|
||||
with _suppress_opencv_noise():
|
||||
for index in range(0, max_index + 1):
|
||||
for index in range(max_index + 1):
|
||||
for backend_name, backend_api in backends:
|
||||
info = _try_open(index, backend_name, backend_api)
|
||||
if info is None:
|
||||
|
||||
@@ -4,7 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from orisys_app_shared import API_VERSION, __version__ as shared_version
|
||||
from orisys_app_shared import API_VERSION
|
||||
from orisys_app_shared import __version__ as shared_version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ from orisys_app_shared.force.filters import (
|
||||
settings_from_mapping,
|
||||
)
|
||||
from orisys_app_shared.force.history import MetricHistory, PlotState, lowpass_filter
|
||||
from orisys_app_shared.force.presentation import (
|
||||
DisplayForce,
|
||||
ForcePresentation,
|
||||
ZeroEstimate,
|
||||
)
|
||||
from orisys_app_shared.force.session import (
|
||||
NA,
|
||||
SelectionStats,
|
||||
@@ -30,11 +35,6 @@ from orisys_app_shared.force.session import (
|
||||
summarise,
|
||||
)
|
||||
from orisys_app_shared.force.units import DisplayForces, select_display_forces
|
||||
from orisys_app_shared.force.presentation import (
|
||||
DisplayForce,
|
||||
ForcePresentation,
|
||||
ZeroEstimate,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CHANNEL_KEYS",
|
||||
|
||||
@@ -7,17 +7,26 @@ one format. ``CalSessionStore`` remains the UI sampling table.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass, field, replace
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Literal, Mapping
|
||||
from typing import Any, Literal
|
||||
|
||||
from orisys.calibration import (
|
||||
CalSample as SdkCalSample,
|
||||
)
|
||||
from orisys.calibration import (
|
||||
build as sdk_build,
|
||||
)
|
||||
from orisys.calibration import (
|
||||
check as sdk_check,
|
||||
)
|
||||
from orisys.calibration import (
|
||||
pipeline_from_sensor_json,
|
||||
)
|
||||
from orisys.calibration import (
|
||||
write as sdk_write,
|
||||
)
|
||||
|
||||
@@ -76,20 +85,6 @@ def merge_samples(first: CalSample, second: CalSample) -> CalSample:
|
||||
)
|
||||
|
||||
|
||||
def build_export_model(samples: Mapping[str, list[CalSample]]):
|
||||
"""Build exactly the model that export and application would use.
|
||||
|
||||
Diagnostics must be computed on *this* curve. A separate least-squares fit
|
||||
can look healthy while the piecewise curve the device actually applies is
|
||||
the one with the error.
|
||||
"""
|
||||
payload = {
|
||||
key: [sample.to_sdk() for sample in (samples.get(key) or [])]
|
||||
for key in CHANNEL_KEYS
|
||||
}
|
||||
return sdk_build(payload, origin="diagnostics")
|
||||
|
||||
|
||||
def predict_channel(cal, channel: str, raw: float) -> float:
|
||||
"""Apply ``cal`` to one axis exactly as the runtime conversion does."""
|
||||
triple = [0.0, 0.0, 0.0]
|
||||
@@ -424,7 +419,14 @@ def prepare_calibration(
|
||||
|
||||
|
||||
def build_export_model(samples: dict[str, list[CalSample]]):
|
||||
"""The SDK model for ``samples`` — the same preparation the panel uses."""
|
||||
"""Build exactly the model that export and application would use.
|
||||
|
||||
This is the panel's own preparation, so it is the curve the device will
|
||||
actually apply: diagnostics are computed on *this* model, and a separate
|
||||
least-squares fit can look healthy while the piecewise curve in force is what
|
||||
carries the error. It also merges repeated loads, so a caller may hand in raw
|
||||
(unmerged) samples and still get the exported model.
|
||||
"""
|
||||
return prepare_from_samples(samples).model
|
||||
|
||||
|
||||
|
||||
@@ -23,18 +23,16 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.force.models import (
|
||||
CHANNEL_KEYS,
|
||||
MAX_TRACE_POINTS,
|
||||
NA,
|
||||
SAMPLE_FIELDS,
|
||||
STATUS_CANCELLED,
|
||||
STATUS_FEW_SAMPLES,
|
||||
STATUS_OK,
|
||||
AxisStats,
|
||||
@@ -48,11 +46,11 @@ from orisys_app_shared.force.session import SessionSample, summarise
|
||||
# Re-exported: the models live in ``force.models`` so the session file format can
|
||||
# describe a point without importing the sampling state machine.
|
||||
__all__ = [
|
||||
"SAMPLE_FIELDS",
|
||||
"AxisStats",
|
||||
"CalibrationPoint",
|
||||
"CalibrationRecorder",
|
||||
"FitResult",
|
||||
"SAMPLE_FIELDS",
|
||||
"trace_dt",
|
||||
"trace_from",
|
||||
]
|
||||
@@ -99,8 +97,8 @@ class CalibrationRecorder:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
on_finished: Optional[Callable[[CalibrationPoint], None]] = None,
|
||||
on_cancelled: Optional[Callable[[], None]] = None,
|
||||
on_finished: Callable[[CalibrationPoint], None] | None = None,
|
||||
on_cancelled: Callable[[], None] | None = None,
|
||||
clock: Callable[[], float] = time.perf_counter,
|
||||
wall_clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
|
||||
) -> None:
|
||||
@@ -211,7 +209,7 @@ class CalibrationRecorder:
|
||||
)
|
||||
)
|
||||
|
||||
def tick(self) -> Optional[CalibrationPoint]:
|
||||
def tick(self) -> CalibrationPoint | None:
|
||||
"""Finish on the UI thread once the deadline has passed."""
|
||||
with self._lock:
|
||||
if not self._recording:
|
||||
@@ -221,7 +219,7 @@ class CalibrationRecorder:
|
||||
return None
|
||||
return self._finish()
|
||||
|
||||
def finish_now(self) -> Optional[CalibrationPoint]:
|
||||
def finish_now(self) -> CalibrationPoint | None:
|
||||
"""Stop early and keep whatever has been captured so far."""
|
||||
with self._lock:
|
||||
if not self._recording:
|
||||
@@ -229,7 +227,7 @@ class CalibrationRecorder:
|
||||
return self._finish()
|
||||
|
||||
# --------------------------------------------------------------- internals
|
||||
def _finish(self) -> Optional[CalibrationPoint]:
|
||||
def _finish(self) -> CalibrationPoint | None:
|
||||
with self._lock:
|
||||
samples = self._samples
|
||||
self._samples = []
|
||||
@@ -289,7 +287,7 @@ class CalibrationRecorder:
|
||||
with self._lock:
|
||||
return list(self.points)
|
||||
|
||||
def _resolve(self, point: CalibrationPoint) -> Optional[CalibrationPoint]:
|
||||
def _resolve(self, point: CalibrationPoint) -> CalibrationPoint | None:
|
||||
"""Find the recorded point that is ``point``, by identity.
|
||||
|
||||
Callers pass the object they are looking at. An integer row index would
|
||||
@@ -335,7 +333,7 @@ class CalibrationRecorder:
|
||||
self.points.remove(target)
|
||||
return True
|
||||
|
||||
def clear_points(self, channel: Optional[ChannelKey] = None) -> int:
|
||||
def clear_points(self, channel: ChannelKey | None = None) -> int:
|
||||
"""Drop every point, or every point of one channel. Returns the count."""
|
||||
with self._lock:
|
||||
if channel is None:
|
||||
@@ -368,7 +366,7 @@ class CalibrationRecorder:
|
||||
with self._lock:
|
||||
return [point for point in self.points if point.channel == channel]
|
||||
|
||||
def prepared(self, *, meta: Optional[dict] = None):
|
||||
def prepared(self, *, meta: dict | None = None):
|
||||
"""The session prepared into one model plus its diagnostics.
|
||||
|
||||
Everything that shows or writes a calibration goes through this, so the
|
||||
|
||||
@@ -18,10 +18,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.force.models import (
|
||||
SAMPLE_FIELDS,
|
||||
@@ -79,10 +81,19 @@ class CalibrationSessionRecord:
|
||||
return " / ".join(parts)
|
||||
|
||||
|
||||
def _is_nan(value: Any) -> bool:
|
||||
"""Whether ``value`` is a NaN float.
|
||||
|
||||
``None`` is not one: a blank sample in a row is an absent reading, not a
|
||||
number that failed, so it has to survive as it is.
|
||||
"""
|
||||
return isinstance(value, float) and math.isnan(value)
|
||||
|
||||
|
||||
def _jsonable(value: Any) -> Any:
|
||||
"""NaN/Inf are not valid JSON; they are written as ``null``."""
|
||||
if isinstance(value, float):
|
||||
if value != value or value in (float("inf"), float("-inf")):
|
||||
if math.isnan(value) or value in (float("inf"), float("-inf")):
|
||||
return None
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
@@ -95,8 +106,8 @@ def _jsonable(value: Any) -> Any:
|
||||
def build_session_document(
|
||||
points: Iterable,
|
||||
*,
|
||||
meta: Optional[dict] = None,
|
||||
created_at: Optional[str] = None,
|
||||
meta: dict | None = None,
|
||||
created_at: str | None = None,
|
||||
) -> dict:
|
||||
"""Build the document for ``points`` without touching the disk."""
|
||||
documents = []
|
||||
@@ -105,7 +116,7 @@ def build_session_document(
|
||||
# The full series is the audit data; the trace stays as a quick preview.
|
||||
entry["sample_fields"] = list(SAMPLE_FIELDS)
|
||||
entry["samples"] = [
|
||||
[None if value != value else value for value in row]
|
||||
[None if _is_nan(value) else value for value in row]
|
||||
for row in getattr(point, "samples", ())
|
||||
]
|
||||
documents.append(entry)
|
||||
@@ -120,7 +131,7 @@ def build_session_document(
|
||||
}
|
||||
|
||||
|
||||
def session_filename(sensor_type: str, *, stamp: Optional[str] = None) -> str:
|
||||
def session_filename(sensor_type: str, *, stamp: str | None = None) -> str:
|
||||
when = stamp or datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"calibration_session_{sensor_type or 'unknown'}_{when}.json"
|
||||
|
||||
@@ -129,7 +140,7 @@ def write_calibration_session(
|
||||
path: str | Path,
|
||||
points: Iterable,
|
||||
*,
|
||||
meta: Optional[dict] = None,
|
||||
meta: dict | None = None,
|
||||
) -> Path:
|
||||
"""Write the session document. Returns the written path."""
|
||||
target = Path(path)
|
||||
@@ -271,7 +282,7 @@ def restore_diagnostics(point) -> None:
|
||||
point.trace_dt_s = trace_dt(valid_block)
|
||||
if not point.duration_s and block.shape[0] > 1:
|
||||
span = float(block[-1, 1] - block[0, 1])
|
||||
if span == span and span >= 0.0:
|
||||
if not math.isnan(span) and span >= 0.0:
|
||||
point.duration_s = span
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any, Iterable, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -159,10 +160,10 @@ class FilterSettings:
|
||||
show_unfiltered: bool = False
|
||||
#: The preset this came from, when it came from one (kept so a record can say
|
||||
#: "balanced" instead of only the numbers it expanded to).
|
||||
preset: Optional[str] = None
|
||||
preset: str | None = None
|
||||
|
||||
# ------------------------------------------------------------- resolving
|
||||
def normalised(self) -> "FilterSettings":
|
||||
def normalised(self) -> FilterSettings:
|
||||
"""Clamp to what the SDK can run, with unusable values meaning "off"."""
|
||||
alpha = _as_float(self.flow_ema_alpha, 1.0)
|
||||
if not 0.0 < alpha < 1.0:
|
||||
@@ -241,7 +242,7 @@ class FilterSettings:
|
||||
"""A short line for the record, upstream stage first."""
|
||||
return describe_state(self.as_state())
|
||||
|
||||
def nyquist_issue(self, sample_rate_hz: Optional[float]) -> Optional[dict[str, float]]:
|
||||
def nyquist_issue(self, sample_rate_hz: float | None) -> dict[str, float] | None:
|
||||
"""The cutoff/Nyquist problem as numbers, for a UI to phrase itself.
|
||||
|
||||
The SDK drops a low-pass whose cutoff is at or above Nyquist instead of
|
||||
@@ -262,7 +263,7 @@ class FilterSettings:
|
||||
"rate_hz": float(rate),
|
||||
}
|
||||
|
||||
def problems(self, sample_rate_hz: Optional[float] = None) -> tuple[str, ...]:
|
||||
def problems(self, sample_rate_hz: float | None = None) -> tuple[str, ...]:
|
||||
"""Why the device would not do what these settings ask (English, for logs)."""
|
||||
issue = self.nyquist_issue(sample_rate_hz)
|
||||
if issue is None:
|
||||
@@ -321,7 +322,7 @@ class FilterSettings:
|
||||
@classmethod
|
||||
def from_filters_state(
|
||||
cls, state: Mapping[str, Any], *, show_unfiltered: bool = False
|
||||
) -> "FilterSettings":
|
||||
) -> FilterSettings:
|
||||
"""The settings a device reports as in effect.
|
||||
|
||||
This is what the dialog displays and what a record stores: a config file
|
||||
@@ -342,7 +343,7 @@ class FilterSettings:
|
||||
preset=state.get("preset"),
|
||||
).normalised()
|
||||
|
||||
def metadata(self, sensor_state: Optional[Mapping[str, Any]] = None) -> dict[str, Any]:
|
||||
def metadata(self, sensor_state: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""The ``filters`` block for ``session.json`` / a recording header.
|
||||
|
||||
With a sensor state this records what the device was doing
|
||||
@@ -396,7 +397,7 @@ def describe_state(state: Mapping[str, Any]) -> str:
|
||||
return " + ".join(parts) if parts else "none"
|
||||
|
||||
|
||||
def settings_from_mapping(data: Optional[Mapping[str, Any]]) -> FilterSettings:
|
||||
def settings_from_mapping(data: Mapping[str, Any] | None) -> FilterSettings:
|
||||
"""Build settings from a preferences/metadata mapping, with defaults."""
|
||||
source = dict(data or {})
|
||||
base = FilterSettings()
|
||||
@@ -442,12 +443,12 @@ class ForceFilterChain:
|
||||
``zi``). Order is median then low-pass, so a spike cannot ring the IIR.
|
||||
"""
|
||||
|
||||
def __init__(self, settings: Optional[FilterSettings] = None) -> None:
|
||||
def __init__(self, settings: FilterSettings | None = None) -> None:
|
||||
self._settings = (settings or FilterSettings()).normalised()
|
||||
self._median: list[list[float]] = [[] for _ in range(CHANNELS)]
|
||||
self._sos = None
|
||||
#: One IIR state per channel: the six readings are independent signals.
|
||||
self._zi: Optional[list[np.ndarray]] = None
|
||||
self._zi: list[np.ndarray] | None = None
|
||||
self._build()
|
||||
|
||||
# ------------------------------------------------------------------ setup
|
||||
@@ -485,7 +486,7 @@ class ForceFilterChain:
|
||||
def active(self) -> bool:
|
||||
return self._settings.active()
|
||||
|
||||
def reset(self, first: Optional[Iterable[float]] = None) -> None:
|
||||
def reset(self, first: Iterable[float] | None = None) -> None:
|
||||
"""Drop the filter memory (a new session must not inherit the old one)."""
|
||||
self._median = [[] for _ in range(CHANNELS)]
|
||||
self._zi = None
|
||||
|
||||
@@ -8,7 +8,6 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -97,8 +96,8 @@ class MetricHistory:
|
||||
(self._capacity, 1 + self.channels), np.nan, dtype=np.float64
|
||||
)
|
||||
self.sample_count = 0
|
||||
self._t0: Optional[float] = None
|
||||
self._y_range_smooth: list[Optional[tuple[float, float]]] = [
|
||||
self._t0: float | None = None
|
||||
self._y_range_smooth: list[tuple[float, float] | None] = [
|
||||
None for _ in range(self.channels)
|
||||
]
|
||||
|
||||
@@ -176,7 +175,7 @@ class MetricHistory:
|
||||
pad = max((smoothed[1] - smoothed[0]) * 0.12, 0.01)
|
||||
return smoothed[0] - pad, smoothed[1] + pad
|
||||
|
||||
def state(self) -> Optional[PlotState]:
|
||||
def state(self) -> PlotState | None:
|
||||
n = self.sample_count
|
||||
if n == 0:
|
||||
return None
|
||||
|
||||
@@ -33,8 +33,8 @@ contact present at session start stays visible instead of being zeroed away.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -55,7 +55,7 @@ DEFAULT_MIN_DEADBAND_N = 0.0
|
||||
DEFAULT_PROVISIONAL_SAMPLES = 3
|
||||
|
||||
|
||||
def normalise_unit(unit: Optional[str]) -> str:
|
||||
def normalise_unit(unit: str | None) -> str:
|
||||
"""``"N"`` or ``"raw"`` — the two units a force reading can be shown in."""
|
||||
return "N" if str(unit or "").upper().startswith("N") else "raw"
|
||||
|
||||
@@ -88,16 +88,16 @@ class _UnitEstimate:
|
||||
collecting: bool = False
|
||||
samples: list[tuple[float, float, float]] = field(default_factory=list)
|
||||
sample_times: list[float] = field(default_factory=list)
|
||||
t0: Optional[float] = None
|
||||
t0: float | None = None
|
||||
#: Frozen estimate: set once the quiet window closes with quality ``ok``.
|
||||
offsets: Optional[tuple[float, float, float]] = None
|
||||
deadbands: Optional[tuple[float, float, float]] = None
|
||||
estimate: Optional[ZeroEstimate] = None
|
||||
offsets: tuple[float, float, float] | None = None
|
||||
deadbands: tuple[float, float, float] | None = None
|
||||
estimate: ZeroEstimate | None = None
|
||||
#: Provisional estimate, published from the first few quiet samples and
|
||||
#: re-fitted on each further one. It is what the display uses while the
|
||||
#: window is still open, and what it keeps if the window closes badly.
|
||||
provisional_offsets: Optional[tuple[float, float, float]] = None
|
||||
provisional_deadbands: Optional[tuple[float, float, float]] = None
|
||||
provisional_offsets: tuple[float, float, float] | None = None
|
||||
provisional_deadbands: tuple[float, float, float] | None = None
|
||||
provisional_samples: int = 0
|
||||
#: A quiet sample was refused because a contact was present or the flow was
|
||||
#: too high: this window is not a trustworthy idle window any more.
|
||||
@@ -109,12 +109,12 @@ class _UnitEstimate:
|
||||
return self.offsets is not None and self.deadbands is not None
|
||||
|
||||
@property
|
||||
def display_offsets(self) -> Optional[tuple[float, float, float]]:
|
||||
def display_offsets(self) -> tuple[float, float, float] | None:
|
||||
"""What the display is centred on: frozen if there is one, else provisional."""
|
||||
return self.offsets if self.offsets is not None else self.provisional_offsets
|
||||
|
||||
@property
|
||||
def display_deadbands(self) -> Optional[tuple[float, float, float]]:
|
||||
def display_deadbands(self) -> tuple[float, float, float] | None:
|
||||
"""The deadband the display is using (frozen if there is one)."""
|
||||
return (
|
||||
self.deadbands if self.deadbands is not None else self.provisional_deadbands
|
||||
@@ -190,12 +190,12 @@ class ForcePresentation:
|
||||
"""Operator-triggered restart of the idle window."""
|
||||
self.reset_session(unit=self.unit)
|
||||
|
||||
def set_enabled(self, enabled: bool, *, unit: Optional[str] = None) -> None:
|
||||
def set_enabled(self, enabled: bool, *, unit: str | None = None) -> None:
|
||||
"""The master switch; switching it on restarts the quiet window."""
|
||||
self.enabled = bool(enabled)
|
||||
self.reset_session(unit=self.unit if unit is None else unit)
|
||||
|
||||
def min_deadband_for(self, unit: Optional[str] = None) -> float:
|
||||
def min_deadband_for(self, unit: str | None = None) -> float:
|
||||
"""The floor under the estimated deadband, for one display unit."""
|
||||
chosen = self.unit if unit is None else unit
|
||||
return (
|
||||
@@ -279,7 +279,7 @@ class ForcePresentation:
|
||||
return "provisional"
|
||||
|
||||
@property
|
||||
def last_estimate(self) -> Optional[ZeroEstimate]:
|
||||
def last_estimate(self) -> ZeroEstimate | None:
|
||||
"""The window's outcome: the frozen estimate, the failed one, or the
|
||||
provisional estimate while the window is still open."""
|
||||
state = self._states.get(self.unit)
|
||||
@@ -318,10 +318,10 @@ class ForcePresentation:
|
||||
*,
|
||||
t_rel_s: float,
|
||||
valid: bool = True,
|
||||
flow_p95: Optional[float] = None,
|
||||
flow_p95: float | None = None,
|
||||
contact_found: bool = False,
|
||||
unit: Optional[str] = None,
|
||||
) -> Optional[ZeroEstimate]:
|
||||
unit: str | None = None,
|
||||
) -> ZeroEstimate | None:
|
||||
"""Feed one live sample into the startup estimator.
|
||||
|
||||
``values`` must already be expressed in ``unit`` (the displayed one).
|
||||
@@ -406,7 +406,7 @@ class ForcePresentation:
|
||||
state.provisional_samples = estimate.sample_count
|
||||
|
||||
def fit_zero(
|
||||
self, samples: Sequence[Sequence[float]], *, unit: Optional[str] = None
|
||||
self, samples: Sequence[Sequence[float]], *, unit: str | None = None
|
||||
) -> ZeroEstimate:
|
||||
"""Estimate offsets and deadbands from quiet samples (does not apply them)."""
|
||||
floor = self.min_deadband_for(unit)
|
||||
@@ -456,7 +456,7 @@ class ForcePresentation:
|
||||
)
|
||||
|
||||
def apply_estimate(
|
||||
self, estimate: ZeroEstimate, *, unit: Optional[str] = None
|
||||
self, estimate: ZeroEstimate, *, unit: str | None = None
|
||||
) -> None:
|
||||
"""Explicitly freeze an estimate (e.g. after user confirmation)."""
|
||||
if estimate.quality != "ok":
|
||||
@@ -472,7 +472,7 @@ class ForcePresentation:
|
||||
self,
|
||||
values: Sequence[float],
|
||||
*,
|
||||
unit: Optional[str] = None,
|
||||
unit: str | None = None,
|
||||
available: tuple[bool, bool, bool] = (True, True, True),
|
||||
) -> DisplayForce:
|
||||
"""Return a display copy of one live sample; never mutates ``values``.
|
||||
@@ -530,7 +530,7 @@ class ForcePresentation:
|
||||
)
|
||||
|
||||
def present(
|
||||
self, values, *, unit: Optional[str] = None
|
||||
self, values, *, unit: str | None = None
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Present a whole block of readings: ``(display copy, suppressed mask)``.
|
||||
|
||||
@@ -568,7 +568,7 @@ class ForcePresentation:
|
||||
suppressed[finite, axis] = quiet
|
||||
return block, suppressed
|
||||
|
||||
def format_force(self, value: float, unit: Optional[str] = None) -> str:
|
||||
def format_force(self, value: float, unit: str | None = None) -> str:
|
||||
"""Stable digit formatting for value cards."""
|
||||
unit = normalise_unit(self.unit if unit is None else unit)
|
||||
if not np.isfinite(value):
|
||||
|
||||
@@ -24,8 +24,8 @@ Design notes
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Iterable, Iterator, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -71,7 +71,7 @@ class SessionSample:
|
||||
calibrated_axes: tuple[str, ...] = ()
|
||||
|
||||
@classmethod
|
||||
def from_record(cls, record) -> "SessionSample":
|
||||
def from_record(cls, record) -> SessionSample:
|
||||
"""Build from the canonical :class:`RecordSample`.
|
||||
|
||||
The record channel has one conversion from a packet (see
|
||||
@@ -221,8 +221,8 @@ class SessionStore:
|
||||
self._dropped = 0
|
||||
self._session_id = 0
|
||||
self._calibration_id = ""
|
||||
self._t0: Optional[float] = None
|
||||
self._last_t: Optional[float] = None
|
||||
self._t0: float | None = None
|
||||
self._last_t: float | None = None
|
||||
self._invalid = 0
|
||||
|
||||
# ------------------------------------------------------------- lifecycle
|
||||
@@ -421,7 +421,7 @@ class SessionStore:
|
||||
return np.empty((0, _COLUMNS), dtype=np.float64)
|
||||
return np.vstack(selected) if len(selected) > 1 else selected[0]
|
||||
|
||||
def statistics(self, t0: float, t1: float, unit: str = "raw") -> "SelectionStats":
|
||||
def statistics(self, t0: float, t1: float, unit: str = "raw") -> SelectionStats:
|
||||
"""Duration, peak, mean and std over a selection, per channel."""
|
||||
t0 = float(t0)
|
||||
t1 = float(t1)
|
||||
|
||||
@@ -11,6 +11,7 @@ reported as such.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from orisys_app_shared.record.sample import AXES
|
||||
@@ -48,7 +49,7 @@ class DisplayForces:
|
||||
|
||||
def _finite(value: float) -> bool:
|
||||
try:
|
||||
return value == value and value not in (float("inf"), float("-inf"))
|
||||
return not math.isnan(value) and value not in (float("inf"), float("-inf"))
|
||||
except TypeError: # pragma: no cover - defensive
|
||||
return False
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class QtLogHandler(logging.Handler):
|
||||
|
||||
@@ -24,9 +24,15 @@ from orisys_app_shared.record.playback import (
|
||||
)
|
||||
from orisys_app_shared.record.visualization3d import (
|
||||
BLOCK_VERSION as VISUALIZATION3D_BLOCK_VERSION,
|
||||
)
|
||||
from orisys_app_shared.record.visualization3d import (
|
||||
VISUALIZATION3D_KEY,
|
||||
Visualization3DRecord,
|
||||
)
|
||||
from orisys_app_shared.record.visualization3d import (
|
||||
build_block as build_visualization3d_block,
|
||||
)
|
||||
from orisys_app_shared.record.visualization3d import (
|
||||
read_block as read_visualization3d_block,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field, replace
|
||||
from uuid import uuid4
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -198,7 +198,7 @@ class RecordStats:
|
||||
class RecordReport:
|
||||
"""Returned by :meth:`ExperimentRecorder.stop`."""
|
||||
|
||||
directory: Optional[Path]
|
||||
directory: Path | None
|
||||
stats: RecordStats
|
||||
problems: list[str] = field(default_factory=list)
|
||||
#: False when the writer had not finished closing the files.
|
||||
@@ -217,7 +217,7 @@ class RecordReport:
|
||||
class ExperimentRecorder:
|
||||
"""Writes one experiment directory. Sink-compatible with the workers."""
|
||||
|
||||
def __init__(self, config: Optional[RecordConfig] = None) -> None:
|
||||
def __init__(self, config: RecordConfig | None = None) -> None:
|
||||
self.config = config or RecordConfig()
|
||||
#: One ordered stream for rows and events. It is **unbounded** on count:
|
||||
#: the producer checks ``ROW_BACKLOG``/``max_queue`` itself, so a slow
|
||||
@@ -229,7 +229,7 @@ class ExperimentRecorder:
|
||||
self._gaps: deque = deque(maxlen=GAP_BACKLOG)
|
||||
self._queued_bytes = 0
|
||||
self._seq = 0
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stop = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
#: Serialises session.json writes: the UI thread writes at start and the
|
||||
@@ -237,26 +237,26 @@ class ExperimentRecorder:
|
||||
self._json_lock = threading.Lock()
|
||||
#: Explicit lifecycle. A boolean could not express "the writer died".
|
||||
self._state: str = STATE_IDLE
|
||||
self._directory: Optional[Path] = None
|
||||
self._directory: Path | None = None
|
||||
self._stats = RecordStats()
|
||||
self._session_id = 0
|
||||
self._calibration_id = ""
|
||||
self._calibration_version = ""
|
||||
self._metadata: dict[str, Any] = {}
|
||||
self._events: list[dict] = []
|
||||
self._t0: Optional[float] = None
|
||||
self._last_t: Optional[float] = None
|
||||
self._t0: float | None = None
|
||||
self._last_t: float | None = None
|
||||
#: When the next video frame is due, in the same acquisition clock.
|
||||
self._next_video_t: Optional[float] = None
|
||||
self._next_video_t: float | None = None
|
||||
self._started_at = ""
|
||||
self._consecutive_errors = 0
|
||||
self._on_alarm = None
|
||||
self._report: Optional[RecordReport] = None
|
||||
self._report: RecordReport | None = None
|
||||
self._accepting = False
|
||||
#: Outcome of the pre-roll write, published for session.json.
|
||||
self._preroll: list = []
|
||||
self._preroll_frames = 0
|
||||
self._preroll_segment: Optional[str] = None
|
||||
self._preroll_segment: str | None = None
|
||||
#: Problems the writer found while closing, carried into the report.
|
||||
self._writer_problems: list[str] = []
|
||||
#: Coefficient files stored inside the recording directory.
|
||||
@@ -281,7 +281,7 @@ class ExperimentRecorder:
|
||||
return self._state in (STATE_STARTING, STATE_RECORDING)
|
||||
|
||||
@property
|
||||
def report(self) -> Optional[RecordReport]:
|
||||
def report(self) -> RecordReport | None:
|
||||
"""The terminal report, available once the writer has finished."""
|
||||
return self._report
|
||||
|
||||
@@ -300,7 +300,7 @@ class ExperimentRecorder:
|
||||
return self._accepting and self._state in (STATE_STARTING, STATE_RECORDING)
|
||||
|
||||
@property
|
||||
def directory(self) -> Optional[Path]:
|
||||
def directory(self) -> Path | None:
|
||||
return self._directory
|
||||
|
||||
def stats(self) -> RecordStats:
|
||||
@@ -329,7 +329,7 @@ class ExperimentRecorder:
|
||||
root: Path,
|
||||
session_id: int,
|
||||
metadata: dict[str, Any],
|
||||
config: Optional[RecordConfig] = None,
|
||||
config: RecordConfig | None = None,
|
||||
calibration_id: str = "",
|
||||
calibration_version: str = "",
|
||||
preroll=(),
|
||||
@@ -461,7 +461,7 @@ class ExperimentRecorder:
|
||||
version: str,
|
||||
name: str,
|
||||
frame_id: int = -1,
|
||||
mono_t: Optional[float] = None,
|
||||
mono_t: float | None = None,
|
||||
) -> None:
|
||||
"""Remember a coefficient file stored inside the recording directory."""
|
||||
entry = {
|
||||
@@ -480,9 +480,9 @@ class ExperimentRecorder:
|
||||
kind: str,
|
||||
detail: str = "",
|
||||
*,
|
||||
mono_t: Optional[float] = None,
|
||||
mono_t: float | None = None,
|
||||
frame_id: int = -1,
|
||||
wall_at: Optional[str] = None,
|
||||
wall_at: str | None = None,
|
||||
) -> None:
|
||||
"""Record an event, timestamped when it actually happened.
|
||||
|
||||
@@ -704,7 +704,7 @@ class ExperimentRecorder:
|
||||
inside the guard for that reason: a full disk used to kill this thread
|
||||
before any statistics or alarm existed.
|
||||
"""
|
||||
writer: Optional[_SessionWriter] = None
|
||||
writer: _SessionWriter | None = None
|
||||
failure = ""
|
||||
try:
|
||||
writer = _SessionWriter(
|
||||
@@ -1022,9 +1022,9 @@ class _SessionWriter:
|
||||
self.bytes_written = 0
|
||||
#: Frames captured before recording started, written as their own file.
|
||||
self.preroll_frames = 0
|
||||
self.preroll_segment: Optional[str] = None
|
||||
self.preroll_segment: str | None = None
|
||||
self._last_flush = time.perf_counter()
|
||||
self._t0: Optional[float] = None
|
||||
self._t0: float | None = None
|
||||
self._problems: list[str] = []
|
||||
#: The video side, in its own component: one rolling segment at a time.
|
||||
self._video = VideoSegments(
|
||||
|
||||
@@ -20,9 +20,9 @@ from __future__ import annotations
|
||||
import csv
|
||||
import logging
|
||||
from array import array
|
||||
from dataclasses import dataclass, field
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Iterator, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -82,7 +82,7 @@ class ForceWindow:
|
||||
mask = plottable_axis_mask(values, self.valid)[:, index]
|
||||
return self.times[mask], values[mask, index]
|
||||
|
||||
def statistics(self, unit: str) -> "WindowStats":
|
||||
def statistics(self, unit: str) -> WindowStats:
|
||||
values = self.value_for_unit(unit)
|
||||
mask = np.isfinite(values) & self.valid[:, None]
|
||||
peaks: list[float] = []
|
||||
@@ -172,7 +172,7 @@ class RecordingIndex:
|
||||
"""Size of the data file the index describes."""
|
||||
return self._byte_size
|
||||
|
||||
def build(self) -> "RecordingIndex":
|
||||
def build(self) -> RecordingIndex:
|
||||
"""One streaming pass: times, flags, frame ids and byte offsets."""
|
||||
path = self.path
|
||||
times: list[float] = []
|
||||
@@ -240,7 +240,7 @@ class RecordingIndex:
|
||||
except (KeyError, IndexError, ValueError, StopIteration, csv.Error):
|
||||
return None
|
||||
|
||||
def ensure_built(self) -> "RecordingIndex":
|
||||
def ensure_built(self) -> RecordingIndex:
|
||||
if not self._built:
|
||||
self.build()
|
||||
return self
|
||||
@@ -361,7 +361,7 @@ class RecordingIndex:
|
||||
decimated=bucket > 1,
|
||||
)
|
||||
|
||||
def iter_rows(self, start: int = 0, stop: Optional[int] = None) -> Iterator[tuple]:
|
||||
def iter_rows(self, start: int = 0, stop: int | None = None) -> Iterator[tuple]:
|
||||
"""Yield logical rows in stable time order, seeking only the selection."""
|
||||
self.ensure_built()
|
||||
start = max(0, int(start))
|
||||
@@ -457,13 +457,17 @@ class RecordingIndex:
|
||||
values = newtons if newton else raw
|
||||
for axis in range(3):
|
||||
value = values[axis]
|
||||
if value != value: # NaN
|
||||
if np.isnan(value):
|
||||
continue
|
||||
current_low = lows[bucket, axis]
|
||||
current_high = highs[bucket, axis]
|
||||
lows[bucket, axis] = value if current_low != current_low else min(current_low, value)
|
||||
highs[bucket, axis] = value if current_high != current_high else max(current_high, value)
|
||||
if starts[bucket] != starts[bucket]:
|
||||
lows[bucket, axis] = (
|
||||
value if np.isnan(current_low) else min(current_low, value)
|
||||
)
|
||||
highs[bucket, axis] = (
|
||||
value if np.isnan(current_high) else max(current_high, value)
|
||||
)
|
||||
if np.isnan(starts[bucket]):
|
||||
starts[bucket] = t_rel
|
||||
counts[bucket] += 1
|
||||
return Overview(
|
||||
|
||||
@@ -16,9 +16,9 @@ import csv
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -140,8 +140,8 @@ class RecordingReader:
|
||||
self.directory = Path(directory)
|
||||
if not self.directory.is_dir():
|
||||
raise FileNotFoundError(f"Not a recording directory: {self.directory}")
|
||||
self._info: Optional[RecordingInfo] = None
|
||||
self._events: Optional[list[dict]] = None
|
||||
self._info: RecordingInfo | None = None
|
||||
self._events: list[dict] | None = None
|
||||
self.max_rows = int(max_rows or self.DEFAULT_MAX_ROWS)
|
||||
self._index = RecordingIndex(self.directory, checkpoint_rows=checkpoint_rows)
|
||||
self._overviews: dict[tuple[str, int], Overview] = {}
|
||||
@@ -259,7 +259,7 @@ class RecordingReader:
|
||||
window.frame_ids, index.dropped_rows, index.ordered)
|
||||
|
||||
# ------------------------------------------------------------------ queries
|
||||
def frame_at(self, t_rel_s: float) -> Optional[RecordingFrame]:
|
||||
def frame_at(self, t_rel_s: float) -> RecordingFrame | None:
|
||||
"""The recorded frame at or just before ``t_rel_s``.
|
||||
|
||||
The index is sorted at load time, so the binary search is valid; the
|
||||
@@ -277,7 +277,7 @@ class RecordingReader:
|
||||
index = max(0, min(index, len(frames) - 1))
|
||||
return frames[index]
|
||||
|
||||
def segment_path(self, segment: int) -> Optional[Path]:
|
||||
def segment_path(self, segment: int) -> Path | None:
|
||||
if segment <= 0:
|
||||
return None
|
||||
path = self.directory / f"raw_{segment:04d}.mp4"
|
||||
@@ -312,7 +312,7 @@ class RecordingReader:
|
||||
found.append(frame_id)
|
||||
return found
|
||||
|
||||
def last_reset_frame(self, at_or_before: int) -> Optional[int]:
|
||||
def last_reset_frame(self, at_or_before: int) -> int | None:
|
||||
"""The most recent reset at or before a frame — a tracker state origin.
|
||||
|
||||
``Sensor.reset`` clears the optical-flow tracker and the force filters, so
|
||||
@@ -328,13 +328,13 @@ class RecordingReader:
|
||||
]
|
||||
return max(candidates) if candidates else None
|
||||
|
||||
def frame_of(self, frame_id: int) -> Optional[RecordingFrame]:
|
||||
def frame_of(self, frame_id: int) -> RecordingFrame | None:
|
||||
for frame in self.info().frames:
|
||||
if frame.frame_id == int(frame_id):
|
||||
return frame
|
||||
return None
|
||||
|
||||
def segment_start_frame(self, segment: int) -> Optional[RecordingFrame]:
|
||||
def segment_start_frame(self, segment: int) -> RecordingFrame | None:
|
||||
"""The first indexed frame that lives in ``segment``."""
|
||||
rows = [frame for frame in self.info().frames if frame.segment == int(segment)]
|
||||
if not rows:
|
||||
@@ -344,7 +344,7 @@ class RecordingReader:
|
||||
def preroll_frames(self) -> int:
|
||||
return int(self.info().session.get("preroll_frames") or 0)
|
||||
|
||||
def preroll_segment(self) -> Optional[Path]:
|
||||
def preroll_segment(self) -> Path | None:
|
||||
"""The pre-roll video recorded just before this recording started."""
|
||||
name = str(self.info().session.get("preroll_segment") or "")
|
||||
if not name:
|
||||
@@ -643,7 +643,7 @@ class VideoOnlySource:
|
||||
self._index = -1
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def video_frame_at(self, t_rel_s: float) -> tuple[Optional[np.ndarray], None]:
|
||||
def video_frame_at(self, t_rel_s: float) -> tuple[np.ndarray | None, None]:
|
||||
"""The frame at ``t_rel_s``: ``(bgr, None)`` or ``(None, None)``.
|
||||
|
||||
The second element is always ``None``: there is no frame *record* to hand back,
|
||||
|
||||
@@ -25,8 +25,12 @@ The rules are therefore:
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING: # numpy is imported lazily, by the one function that needs it
|
||||
import numpy as np
|
||||
|
||||
AXES: tuple[str, ...] = ("fn", "fx", "fy")
|
||||
|
||||
@@ -221,7 +225,7 @@ def plottable_mask(values: Iterable[float], valid: Iterable[bool]) -> list[bool]
|
||||
]
|
||||
|
||||
|
||||
def plottable_axis_mask(values, valid) -> "np.ndarray":
|
||||
def plottable_axis_mask(values, valid) -> np.ndarray:
|
||||
"""``(n, axes)`` mask: one column per axis, decided for that axis alone.
|
||||
|
||||
The rule has to be per axis. A single-axis calibration is legitimate — the
|
||||
|
||||
@@ -10,7 +10,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
|
||||
@@ -27,8 +26,8 @@ class VideoSegments:
|
||||
self.segment_seconds = float(segment_seconds)
|
||||
self.segments = 0
|
||||
self.video_frames = 0
|
||||
self.frame_shape: Optional[tuple[int, int]] = None
|
||||
self._writer: Optional[cv2.VideoWriter] = None
|
||||
self.frame_shape: tuple[int, int] | None = None
|
||||
self._writer: cv2.VideoWriter | None = None
|
||||
self._index = 0
|
||||
self._frames_in_segment = 0
|
||||
self._started = 0.0
|
||||
|
||||
@@ -27,8 +27,9 @@ timestamps and which keeps "what was configured" and "what happened" apart.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
#: The key under which the block lives in ``session.json``.
|
||||
VISUALIZATION3D_KEY = "visualization3d"
|
||||
@@ -42,10 +43,10 @@ def build_block(
|
||||
enabled: bool,
|
||||
available: bool,
|
||||
reason: str = "",
|
||||
scene: Optional[Mapping[str, Any]] = None,
|
||||
gl: Optional[Mapping[str, Any]] = None,
|
||||
display: Optional[Mapping[str, Any]] = None,
|
||||
stream: Optional[Mapping[str, Any]] = None,
|
||||
scene: Mapping[str, Any] | None = None,
|
||||
gl: Mapping[str, Any] | None = None,
|
||||
display: Mapping[str, Any] | None = None,
|
||||
stream: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""One block for ``session.json``. Plain JSON types only, no failures."""
|
||||
return {
|
||||
@@ -62,7 +63,7 @@ def build_block(
|
||||
}
|
||||
|
||||
|
||||
def _clean(value: Optional[Mapping[str, Any]]) -> Optional[dict[str, Any]]:
|
||||
def _clean(value: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""Drop nothing silently: nulls stay nulls, and unknown types become text."""
|
||||
if value is None:
|
||||
return None
|
||||
@@ -109,7 +110,7 @@ class Visualization3DRecord:
|
||||
) not in ("", "display_only")
|
||||
|
||||
|
||||
def read_block(session: Optional[Mapping[str, Any]]) -> Visualization3DRecord:
|
||||
def read_block(session: Mapping[str, Any] | None) -> Visualization3DRecord:
|
||||
"""Read the block out of a ``session.json`` mapping, tolerantly.
|
||||
|
||||
Never raises: a broken recording is still a recording, and a consumer asking
|
||||
|
||||
@@ -12,8 +12,12 @@ their stage lands.
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
DIAGNOSTIC_SEVERITY,
|
||||
F32,
|
||||
F64,
|
||||
I32,
|
||||
LENGTH_UNIT_M,
|
||||
SENSOR_FRAME,
|
||||
U32,
|
||||
UNAVAILABLE_DESCRIPTOR,
|
||||
ArrowInstances,
|
||||
CameraMatrices,
|
||||
@@ -25,12 +29,9 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
DisplayField,
|
||||
DisplaySettings,
|
||||
ExpectedGeometry,
|
||||
F32,
|
||||
F64,
|
||||
FieldDescriptor,
|
||||
FrameIdentity,
|
||||
GLCapabilities,
|
||||
I32,
|
||||
LatestFrameMailbox,
|
||||
Mask,
|
||||
Material,
|
||||
@@ -45,8 +46,6 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
SurfaceFieldFrame,
|
||||
SurfaceMapping,
|
||||
ThreeDError,
|
||||
U32,
|
||||
|
||||
diagnostic,
|
||||
error,
|
||||
freeze,
|
||||
@@ -63,8 +62,12 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
|
||||
__all__ = [
|
||||
"DIAGNOSTIC_SEVERITY",
|
||||
"F32",
|
||||
"F64",
|
||||
"I32",
|
||||
"LENGTH_UNIT_M",
|
||||
"SENSOR_FRAME",
|
||||
"U32",
|
||||
"UNAVAILABLE_DESCRIPTOR",
|
||||
"ArrowInstances",
|
||||
"CameraMatrices",
|
||||
@@ -76,12 +79,9 @@ __all__ = [
|
||||
"DisplayField",
|
||||
"DisplaySettings",
|
||||
"ExpectedGeometry",
|
||||
"F32",
|
||||
"F64",
|
||||
"FieldDescriptor",
|
||||
"FrameIdentity",
|
||||
"GLCapabilities",
|
||||
"I32",
|
||||
"LatestFrameMailbox",
|
||||
"Mask",
|
||||
"Material",
|
||||
@@ -96,7 +96,6 @@ __all__ = [
|
||||
"SurfaceFieldFrame",
|
||||
"SurfaceMapping",
|
||||
"ThreeDError",
|
||||
"U32",
|
||||
"diagnostic",
|
||||
"error",
|
||||
"freeze",
|
||||
|
||||
@@ -20,7 +20,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
CompileOptions,
|
||||
@@ -38,8 +38,8 @@ from orisys_app_shared.scene3d.package import (
|
||||
ENVELOPE_VERSION,
|
||||
MAGIC,
|
||||
SCHEMA_VERSION,
|
||||
PackagedAssetLoader,
|
||||
PackageContents,
|
||||
PackagedAssetLoader,
|
||||
build_manifest,
|
||||
describe_package,
|
||||
read_package,
|
||||
@@ -53,8 +53,8 @@ __all__ = [
|
||||
"MAGIC",
|
||||
"SCHEMA_VERSION",
|
||||
"LegacyFingerImporter",
|
||||
"PackagedAssetLoader",
|
||||
"PackageContents",
|
||||
"PackagedAssetLoader",
|
||||
"SceneCompiler",
|
||||
"build_manifest",
|
||||
"describe_package",
|
||||
@@ -78,7 +78,7 @@ class SceneCompiler:
|
||||
def compile(
|
||||
self,
|
||||
surface: SceneAssets,
|
||||
housing_glb: Optional[Path],
|
||||
housing_glb: Path | None,
|
||||
output: Path,
|
||||
options: CompileOptions,
|
||||
) -> CompileReport:
|
||||
@@ -156,10 +156,10 @@ def load_or_import(
|
||||
path: Path,
|
||||
*,
|
||||
expected: ExpectedGeometry,
|
||||
keys: Optional[KeyProvider] = None,
|
||||
legacy_map: Optional[Path] = None,
|
||||
keys: KeyProvider | None = None,
|
||||
legacy_map: Path | None = None,
|
||||
transform_source_from_sensor: Any = None,
|
||||
meters_per_source_unit: Optional[float] = None,
|
||||
meters_per_source_unit: float | None = None,
|
||||
) -> SceneAssets:
|
||||
"""Load a package, or import a legacy directory, behind one entry point.
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from §3.2 of the design document remain its reason to exist:
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -38,7 +37,6 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
DisplayField,
|
||||
DisplaySettings,
|
||||
FrameIdentity,
|
||||
Quality,
|
||||
RenderFrame,
|
||||
SceneAssets,
|
||||
SurfaceFieldFrame,
|
||||
@@ -92,7 +90,7 @@ class SceneComposer:
|
||||
#: every frame was pure cost (measured as the bulk of a 20k-vertex compose).
|
||||
self._rest_geometry: dict[str, tuple[np.ndarray, np.ndarray]] = {}
|
||||
#: Last UV grid of "arrow was on" samples for flow hysteresis.
|
||||
self._flow_previously_on: Optional[np.ndarray] = None
|
||||
self._flow_previously_on: np.ndarray | None = None
|
||||
#: The resolved ``#RRGGBB,...`` ramp both views build their lookup table from.
|
||||
#: Set by the view when the theme changes; empty means this build's default.
|
||||
self.ramp = str(ramp)
|
||||
|
||||
@@ -19,9 +19,10 @@ Two rules from the design document are enforced here rather than trusted:
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Generic, Iterable, Literal, Mapping, Optional, Protocol, TypeVar
|
||||
from typing import Any, Generic, Literal, Protocol, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
@@ -149,7 +150,7 @@ class FrameIdentity:
|
||||
generation: int
|
||||
request_id: int = 0
|
||||
|
||||
def is_from(self, other: "FrameIdentity") -> bool:
|
||||
def is_from(self, other: FrameIdentity) -> bool:
|
||||
return (
|
||||
self.source_id == other.source_id
|
||||
and self.session_id == other.session_id
|
||||
@@ -165,7 +166,7 @@ class FieldDescriptor:
|
||||
unit: str
|
||||
quality: Quality
|
||||
signed: bool
|
||||
calibration_id: Optional[str] = None
|
||||
calibration_id: str | None = None
|
||||
|
||||
|
||||
#: The descriptor a reference frame carries: no measurement is claimed at all.
|
||||
@@ -190,20 +191,20 @@ class SurfaceFieldFrame:
|
||||
scalar: F32 # [H,W]
|
||||
scalar_descriptor: FieldDescriptor
|
||||
valid: Mask # [H,W]
|
||||
displacement_sensor_m: Optional[F32] = None # [H,W,3], before display gain
|
||||
displacement_quality: Optional[Quality] = None
|
||||
diagnostic: Optional[Diagnostic] = None
|
||||
displacement_sensor_m: F32 | None = None # [H,W,3], before display gain
|
||||
displacement_quality: Quality | None = None
|
||||
diagnostic: Diagnostic | None = None
|
||||
#: The acquisition frame the scalar array was actually computed for, when the
|
||||
#: producer can say so. ``None`` means "unstated" (the frame's own identity is the
|
||||
#: only claim). When it is older than ``identity.frame_id`` the array is a stale
|
||||
#: measurement: a consumer must not colour it as the current instant.
|
||||
computed_frame_id: Optional[int] = None
|
||||
computed_frame_id: int | None = None
|
||||
#: Optional flow field kept alongside a depth scalar so overlays can draw arrows
|
||||
#: without forcing the colour channel to switch away from depth.
|
||||
flow_uv: Optional[F32] = None # [H,W,2]
|
||||
flow_valid_mask: Optional[Mask] = None # [H,W]
|
||||
flow_uv: F32 | None = None # [H,W,2]
|
||||
flow_valid_mask: Mask | None = None # [H,W]
|
||||
#: Contact centre in the same UV domain as ``scalar``; absent on older packets.
|
||||
contact_uv_xy: Optional[tuple[float, float]] = None
|
||||
contact_uv_xy: tuple[float, float] | None = None
|
||||
contact_found: bool = False
|
||||
contact_valid: bool = False
|
||||
#: Normal-force reading used only for the contact-arrow length mapping.
|
||||
@@ -228,7 +229,7 @@ class ArrowInstances:
|
||||
colors: F32 # [M,4], RGBA 0–1
|
||||
kind: ArrowKind
|
||||
#: Optional per-arrow thickness scale relative to a template (contact arrows).
|
||||
thickness_scales: Optional[F32] = None # [M]
|
||||
thickness_scales: F32 | None = None # [M]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -343,7 +344,7 @@ class PlatformLock:
|
||||
|
||||
os_family: tuple[str, ...] = ()
|
||||
machine: tuple[str, ...] = ()
|
||||
gl_profile: Optional[Profile] = None
|
||||
gl_profile: Profile | None = None
|
||||
allowed_vendors: tuple[str, ...] = ()
|
||||
allowed_renderers: tuple[str, ...] = ()
|
||||
forbid_software_renderer: bool = True
|
||||
@@ -380,7 +381,7 @@ class PlatformLock:
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Optional[Mapping[str, Any]]) -> Optional["PlatformLock"]:
|
||||
def from_dict(cls, data: Mapping[str, Any] | None) -> PlatformLock | None:
|
||||
"""Read one back. A malformed lock is an error, not a silently open one."""
|
||||
if data is None:
|
||||
return None
|
||||
@@ -427,14 +428,14 @@ class PlatformInfo:
|
||||
|
||||
os_family: str
|
||||
machine: str
|
||||
gl_profile: Optional[Profile] = None
|
||||
gl_profile: Profile | None = None
|
||||
vendor: str = ""
|
||||
renderer: str = ""
|
||||
software_renderer: bool = False
|
||||
max_texture_size: int = 0
|
||||
|
||||
@classmethod
|
||||
def here(cls, capabilities: Optional[GLCapabilities] = None) -> "PlatformInfo":
|
||||
def here(cls, capabilities: GLCapabilities | None = None) -> PlatformInfo:
|
||||
import platform as _platform
|
||||
|
||||
family = _platform.system().lower()
|
||||
@@ -454,7 +455,7 @@ class PlatformInfo:
|
||||
|
||||
|
||||
def check_platform_lock(
|
||||
lock: Optional[PlatformLock], info: PlatformInfo
|
||||
lock: PlatformLock | None, info: PlatformInfo
|
||||
) -> tuple[Diagnostic, ...]:
|
||||
"""Every way this machine violates the lock. Empty means "allowed".
|
||||
|
||||
@@ -548,7 +549,7 @@ class SceneAssets:
|
||||
#: the map back to the artist's file is many-to-one. Kept because "which
|
||||
#: triangle is this?" is a question an operator will ask, and re-deriving it
|
||||
#: from UVs alone is a guess.
|
||||
source_vertex_of_render: Optional[I32] = None
|
||||
source_vertex_of_render: I32 | None = None
|
||||
#: Unit-free identity of the source geometry this asset was built from, and what the
|
||||
#: package says it is compatible with. The first is what a device can verify about
|
||||
#: the surface; the second catches a package released for another sensor.
|
||||
@@ -557,12 +558,12 @@ class SceneAssets:
|
||||
compatible_hardware_revisions: tuple[str, ...] = ()
|
||||
#: Where this package may be displayed (stage 5). ``None`` means "not locked",
|
||||
#: which is what every package written before the lock existed carries.
|
||||
platform_lock: Optional[PlatformLock] = None
|
||||
platform_lock: PlatformLock | None = None
|
||||
#: UV→scene positions/normals for overlays. Optional so older packages degrade
|
||||
#: to scalar-only rendering without failing to load.
|
||||
surface_mapping: Optional[SurfaceMapping] = None
|
||||
surface_mapping: SurfaceMapping | None = None
|
||||
|
||||
def part(self, part_id: str) -> Optional[MeshPart]:
|
||||
def part(self, part_id: str) -> MeshPart | None:
|
||||
for item in self.parts:
|
||||
if item.part_id == part_id:
|
||||
return item
|
||||
@@ -600,7 +601,7 @@ class SceneCapabilities:
|
||||
unit: str = ""
|
||||
#: Whether the source can ever publish ``displacement_sensor_m``.
|
||||
displacement_available: bool = False
|
||||
displacement_quality: Optional[Quality] = None
|
||||
displacement_quality: Quality | None = None
|
||||
source_kind: str = "live"
|
||||
#: Whether flow / contact overlays can be built for this source.
|
||||
flow_arrows_available: bool = False
|
||||
@@ -613,7 +614,7 @@ class DisplaySettings:
|
||||
|
||||
display_gain: float = 0.0
|
||||
deform_enabled: bool = False
|
||||
color_scale: Optional[ColorScale] = None
|
||||
color_scale: ColorScale | None = None
|
||||
#: Non-sensing parts (the housing/frame) start hidden: the measurement is what the pane is
|
||||
#: for, and on the finger sensor the frame is ~33% wider than the sensing surface, so it
|
||||
#: dominated the picture. Operators who want it for a mechanical check turn it on in the 3D
|
||||
@@ -689,7 +690,7 @@ class RenderFrame:
|
||||
diagnostics: tuple[Diagnostic, ...] = ()
|
||||
#: The acquisition frame the coloured values belong to, carried through from the
|
||||
#: field frame so the *display* side can re-check it before presenting.
|
||||
computed_frame_id: Optional[int] = None
|
||||
computed_frame_id: int | None = None
|
||||
#: Whether ``positions_sensor_m``/``normals_sensor`` differ from the asset's own
|
||||
#: rest geometry. ``False`` for every path the application can take today (no
|
||||
#: adapter publishes a displacement), and the GPU then uploads the vertex buffer
|
||||
@@ -699,8 +700,8 @@ class RenderFrame:
|
||||
#: rather than read from the composer's shared counter, which the next task rewrites.
|
||||
compose_ms: float = 0.0
|
||||
#: Optional overlays; absent on older packages / frames without mapping.
|
||||
flow_arrows: Optional[ArrowInstances] = None
|
||||
contact_arrow: Optional[ArrowInstances] = None
|
||||
flow_arrows: ArrowInstances | None = None
|
||||
contact_arrow: ArrowInstances | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -758,13 +759,13 @@ class Sealing:
|
||||
|
||||
#: Which content key to use, and where the key comes from. ``None`` = plain.
|
||||
key_id: str = ""
|
||||
provider: Optional["KeyProvider"] = None
|
||||
provider: KeyProvider | None = None
|
||||
#: Build-time only. The private key never reaches the runtime path.
|
||||
signing_key_pem: Optional[bytes] = None
|
||||
signing_key_pem: bytes | None = None
|
||||
signing_key_reference: str = ""
|
||||
#: Optional: the writer's own public key, so a build can prove the package it just
|
||||
#: wrote opens again instead of discovering the problem at the customer.
|
||||
verify_key_pem: Optional[bytes] = None
|
||||
verify_key_pem: bytes | None = None
|
||||
|
||||
def encrypts(self) -> bool:
|
||||
return bool(self.key_id) and self.provider is not None
|
||||
@@ -782,12 +783,12 @@ class CompileOptions:
|
||||
max_surface_vertices: int
|
||||
max_housing_triangles: int
|
||||
signing_key_reference: str
|
||||
encryption_key_id: Optional[str] = None
|
||||
encryption_key_id: str | None = None
|
||||
#: Where the released package may be displayed. ``None`` leaves it unlocked, which
|
||||
#: is the right default for an engineering build and the wrong one for a release.
|
||||
platform_lock: Optional[PlatformLock] = None
|
||||
platform_lock: PlatformLock | None = None
|
||||
#: The resolved key material for signing and sealing (see :class:`Sealing`).
|
||||
sealing: Optional[Sealing] = None
|
||||
sealing: Sealing | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -1074,7 +1075,7 @@ def stale_housing_shading(assets: SceneAssets) -> tuple[Diagnostic, ...]:
|
||||
|
||||
|
||||
def validate_assets(
|
||||
assets: SceneAssets, expected: Optional[ExpectedGeometry] = None
|
||||
assets: SceneAssets, expected: ExpectedGeometry | None = None
|
||||
) -> tuple[Diagnostic, ...]:
|
||||
"""Everything a loader must check before an asset is published.
|
||||
|
||||
@@ -1319,7 +1320,7 @@ class LatestFrameMailbox(Generic[T]):
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._frame: Optional[T] = None
|
||||
self._frame: T | None = None
|
||||
self._superseded = 0
|
||||
|
||||
def offer(self, frame: T) -> OfferResult:
|
||||
@@ -1332,12 +1333,12 @@ class LatestFrameMailbox(Generic[T]):
|
||||
self._superseded += 1
|
||||
return "replaced" if replaced else "accepted"
|
||||
|
||||
def take(self) -> Optional[T]:
|
||||
def take(self) -> T | None:
|
||||
with self._lock:
|
||||
frame, self._frame = self._frame, None
|
||||
return frame
|
||||
|
||||
def peek(self) -> Optional[T]:
|
||||
def peek(self) -> T | None:
|
||||
with self._lock:
|
||||
return self._frame
|
||||
|
||||
@@ -1360,7 +1361,7 @@ class KeyProvider(Protocol):
|
||||
|
||||
class AssetLoader(Protocol):
|
||||
def load(
|
||||
self, path: Path, expected: ExpectedGeometry, keys: Optional[KeyProvider] = None
|
||||
self, path: Path, expected: ExpectedGeometry, keys: KeyProvider | None = None
|
||||
) -> SceneAssets:
|
||||
"""CPU/IO thread: authenticate and validate before publishing."""
|
||||
...
|
||||
@@ -1387,7 +1388,7 @@ class SceneCompiler(Protocol):
|
||||
def compile(
|
||||
self,
|
||||
surface: SceneAssets,
|
||||
housing_glb: Optional[Path],
|
||||
housing_glb: Path | None,
|
||||
output: Path,
|
||||
options: CompileOptions,
|
||||
) -> CompileReport: ...
|
||||
@@ -1396,7 +1397,7 @@ class SceneCompiler(Protocol):
|
||||
class FingerFrameAdapter(Protocol):
|
||||
def snapshot(
|
||||
self, packet: Any, runtime: Any, identity: FrameIdentity
|
||||
) -> Optional[SurfaceFieldFrame]:
|
||||
) -> SurfaceFieldFrame | None:
|
||||
"""Acquisition thread only: copy what is needed; no GL, no GUI."""
|
||||
...
|
||||
|
||||
@@ -1404,7 +1405,7 @@ class FingerFrameAdapter(Protocol):
|
||||
class BoxFrameAdapter(Protocol):
|
||||
def snapshot(
|
||||
self, packet: Any, identity: FrameIdentity, domain: ExpectedGeometry
|
||||
) -> Optional[SurfaceFieldFrame]: ...
|
||||
) -> SurfaceFieldFrame | None: ...
|
||||
|
||||
|
||||
class SceneComposer(Protocol):
|
||||
|
||||
@@ -20,7 +20,8 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Iterable, Mapping
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -71,7 +72,7 @@ def _array_bytes(array: Any) -> bytes:
|
||||
return data.tobytes()
|
||||
|
||||
|
||||
def _feed(hasher: "hashlib._Hash", label: str, payload: bytes) -> None:
|
||||
def _feed(hasher: hashlib._Hash, label: str, payload: bytes) -> None:
|
||||
hasher.update(label.encode("utf-8"))
|
||||
hasher.update(b"\0")
|
||||
hasher.update(str(len(payload)).encode("ascii"))
|
||||
|
||||
@@ -29,9 +29,10 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -81,7 +82,7 @@ def read_glb(path: Path) -> tuple[dict[str, Any], bytes]:
|
||||
f"{path}: the header declares {length} bytes but the file has {len(raw)}",
|
||||
)
|
||||
offset = 12
|
||||
document: Optional[dict[str, Any]] = None
|
||||
document: dict[str, Any] | None = None
|
||||
binary = b""
|
||||
while offset + 8 <= len(raw):
|
||||
chunk_length, chunk_type = struct.unpack_from("<II", raw, offset)
|
||||
@@ -101,7 +102,7 @@ def read_glb(path: Path) -> tuple[dict[str, Any], bytes]:
|
||||
raise error("asset_rejected", f"{path}: no JSON chunk")
|
||||
if int((document.get("asset") or {}).get("version", "0").split(".")[0]) != 2:
|
||||
raise error("asset_rejected", f"{path}: only glTF 2.0 is supported")
|
||||
used = set((document.get("extensionsUsed") or []))
|
||||
used = set(document.get("extensionsUsed") or [])
|
||||
refused = sorted(used & set(REFUSED_EXTENSIONS))
|
||||
if refused:
|
||||
raise error(
|
||||
|
||||
@@ -30,9 +30,10 @@ scores a relative residual of 0.006, and the wrong flip scores 0.59.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -86,24 +87,24 @@ class LegacyFieldDomain:
|
||||
|
||||
shape_hw: tuple[int, int]
|
||||
flip_v: bool
|
||||
source_scale: Optional[float]
|
||||
source_scale: float | None
|
||||
valid_mask: np.ndarray # bool [H,W], read-only
|
||||
domain_id: str
|
||||
origin: str = "top-left"
|
||||
index_order: str = "row-major"
|
||||
mapping_digest: str = ""
|
||||
world_xyz: Optional[np.ndarray] = None # float32 [H,W,3], for verification only
|
||||
world_xyz: np.ndarray | None = None # float32 [H,W,3], for verification only
|
||||
#: The unroll map itself: for every output pixel, the source pixel it came from
|
||||
#: (``-1`` where there is none). The adapters need it to bring a live field into
|
||||
#: this domain, and the digest above already covers it.
|
||||
map_x: Optional[np.ndarray] = None
|
||||
map_y: Optional[np.ndarray] = None
|
||||
map_x: np.ndarray | None = None
|
||||
map_y: np.ndarray | None = None
|
||||
#: The grid the map samples from, as the file states it (0 when unstated).
|
||||
source_width: int = 0
|
||||
source_height: int = 0
|
||||
|
||||
@property
|
||||
def source_hw(self) -> Optional[tuple[int, int]]:
|
||||
def source_hw(self) -> tuple[int, int] | None:
|
||||
"""The grid the map samples from, when the file states it."""
|
||||
if self.source_width <= 0 or self.source_height <= 0:
|
||||
return None
|
||||
@@ -172,7 +173,7 @@ def load_legacy_domain(map_path: Path) -> LegacyFieldDomain:
|
||||
mapping_digest=mapping_digest,
|
||||
)
|
||||
|
||||
def _optional_array(name: str) -> Optional[np.ndarray]:
|
||||
def _optional_array(name: str) -> np.ndarray | None:
|
||||
value = data.get(name)
|
||||
return None if value is None else np.asarray(value)
|
||||
|
||||
@@ -377,7 +378,7 @@ def vertex_normals(
|
||||
positions: np.ndarray,
|
||||
triangles: np.ndarray,
|
||||
*,
|
||||
crease_angle_deg: Optional[float] = None,
|
||||
crease_angle_deg: float | None = None,
|
||||
) -> np.ndarray:
|
||||
"""Area-weighted vertex normals: unit length, computed from the geometry.
|
||||
|
||||
@@ -624,7 +625,7 @@ def build_binding(
|
||||
|
||||
|
||||
def estimate_meters_per_source_unit(
|
||||
positions: np.ndarray, expected_extent_m: Optional[float] = None
|
||||
positions: np.ndarray, expected_extent_m: float | None = None
|
||||
) -> float:
|
||||
"""What the mesh's units would have to be for its extent to be a real size.
|
||||
|
||||
@@ -706,8 +707,8 @@ class LegacyFingerImporter:
|
||||
asset_id: str = "finger-display",
|
||||
asset_version: str = "1.0.0",
|
||||
lod_id: str = "lod0",
|
||||
camera_presets: Optional[Iterable[CameraPreset]] = None,
|
||||
housing_path: Optional[Any] = None,
|
||||
camera_presets: Iterable[CameraPreset] | None = None,
|
||||
housing_path: Any | None = None,
|
||||
) -> SceneAssets:
|
||||
export_dir = Path(export_dir)
|
||||
map_path = Path(map_path)
|
||||
@@ -864,7 +865,7 @@ class LegacyFingerImporter:
|
||||
units: float,
|
||||
transform: np.ndarray,
|
||||
lod_id: str,
|
||||
) -> Optional[MeshPart]:
|
||||
) -> MeshPart | None:
|
||||
"""The optional non-sensing part (the housing / mounting frame), or ``None``.
|
||||
|
||||
The file is expected in the **same source frame and units as** ``mesh.npz``: an
|
||||
@@ -960,10 +961,10 @@ def sensing_surface_normal(
|
||||
|
||||
def default_finger_presets(
|
||||
bounds_sensor_m: np.ndarray,
|
||||
positions_sensor_m: Optional[np.ndarray] = None,
|
||||
triangles: Optional[np.ndarray] = None,
|
||||
positions_sensor_m: np.ndarray | None = None,
|
||||
triangles: np.ndarray | None = None,
|
||||
*,
|
||||
surface_normal_sensor: Optional[np.ndarray] = None,
|
||||
surface_normal_sensor: np.ndarray | None = None,
|
||||
) -> tuple[CameraPreset, ...]:
|
||||
"""The default views for a flat sensing surface, in sensor metres.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ examples tree. Outputs :class:`ArrowInstances` for the GL backend.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -118,7 +118,7 @@ def build_flow_overlay(
|
||||
samples: FlowSamples,
|
||||
mapping: SurfaceMapping,
|
||||
*,
|
||||
style: Optional[VisualPolicy] = None,
|
||||
style: VisualPolicy | None = None,
|
||||
normal_offset_m: float = 0.0,
|
||||
) -> tuple[ArrowInstances, tuple[Diagnostic, ...]]:
|
||||
"""Map shared UV flow samples onto the sensing surface."""
|
||||
@@ -194,7 +194,7 @@ def build_flow_overlay(
|
||||
|
||||
def build_normal_overlay(
|
||||
*,
|
||||
contact_uv_xy: Optional[tuple[float, float]],
|
||||
contact_uv_xy: tuple[float, float] | None,
|
||||
contact_found: bool,
|
||||
contact_valid: bool,
|
||||
fn: float,
|
||||
|
||||
@@ -36,12 +36,14 @@ from __future__ import annotations
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d import protect as _protect
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
CameraPreset,
|
||||
CompileReport,
|
||||
@@ -59,18 +61,19 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
)
|
||||
from orisys_app_shared.scene3d.digest import (
|
||||
DIGEST_SCHEMA,
|
||||
asset_hash as compute_asset_hash,
|
||||
binding_id,
|
||||
content_digest,
|
||||
)
|
||||
from orisys_app_shared.scene3d.digest import (
|
||||
asset_hash as compute_asset_hash,
|
||||
)
|
||||
from orisys_app_shared.scene3d.digest import (
|
||||
geometry_id as compute_geometry_id,
|
||||
)
|
||||
from orisys_app_shared.scene3d.legacy import (
|
||||
crease_aware_mesh,
|
||||
default_finger_presets,
|
||||
normals_disagree_with_faces,
|
||||
sensing_surface_normal,
|
||||
)
|
||||
from orisys_app_shared.scene3d import protect as _protect
|
||||
|
||||
#: Re-exported so a caller can ask "can this build seal a package at all?" without
|
||||
#: importing the crypto module directly.
|
||||
@@ -303,7 +306,7 @@ def write_package(
|
||||
*,
|
||||
sensor_model: str = "finger",
|
||||
hardware_revisions: Iterable[str] = ("*",),
|
||||
sealing: Optional[Sealing] = None,
|
||||
sealing: Sealing | None = None,
|
||||
) -> CompileReport:
|
||||
"""Write a runtime package atomically: a half-written package never appears.
|
||||
|
||||
@@ -477,7 +480,7 @@ def _read_envelope(archive: zipfile.ZipFile) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _verify_members(
|
||||
members: Mapping[str, bytes], *, expected_hash: Optional[str] = None
|
||||
members: Mapping[str, bytes], *, expected_hash: str | None = None
|
||||
) -> PackageContents:
|
||||
"""The stage-2 verification, over a name→bytes mapping from either envelope."""
|
||||
try:
|
||||
@@ -568,7 +571,7 @@ def read_package(
|
||||
path: Path,
|
||||
*,
|
||||
keys: Any = None,
|
||||
verify_key_pem: Optional[bytes] = None,
|
||||
verify_key_pem: bytes | None = None,
|
||||
) -> PackageContents:
|
||||
"""Open, verify and hand back the arrays. Refuses rather than repairs.
|
||||
|
||||
@@ -646,7 +649,7 @@ def read_package(
|
||||
|
||||
|
||||
def scene_from_contents(
|
||||
contents: PackageContents, expected: Optional[ExpectedGeometry] = None
|
||||
contents: PackageContents, expected: ExpectedGeometry | None = None
|
||||
) -> SceneAssets:
|
||||
"""Turn verified arrays into a :class:`SceneAssets` and validate it."""
|
||||
manifest = contents.manifest
|
||||
@@ -864,7 +867,7 @@ class PackagedAssetLoader:
|
||||
expected: ExpectedGeometry,
|
||||
keys: Any = None,
|
||||
*,
|
||||
verify_key_pem: Optional[bytes] = None,
|
||||
verify_key_pem: bytes | None = None,
|
||||
) -> SceneAssets:
|
||||
return scene_from_contents(
|
||||
read_package(path, keys=keys, verify_key_pem=verify_key_pem), expected
|
||||
@@ -880,7 +883,7 @@ def describe_package(
|
||||
path: Path,
|
||||
*,
|
||||
keys: Any = None,
|
||||
verify_key_pem: Optional[bytes] = None,
|
||||
verify_key_pem: bytes | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""A report on a package without building a scene: for the validate tool."""
|
||||
contents = read_package(path, keys=keys, verify_key_pem=verify_key_pem)
|
||||
|
||||
@@ -53,8 +53,9 @@ from __future__ import annotations
|
||||
import base64
|
||||
import json
|
||||
import secrets
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
@@ -141,7 +142,7 @@ def _b64(data: bytes) -> str:
|
||||
return base64.b64encode(data).decode("ascii")
|
||||
|
||||
|
||||
def _unb64(text: Any, *, field: str, length: Optional[int] = None) -> bytes:
|
||||
def _unb64(text: Any, *, field: str, length: int | None = None) -> bytes:
|
||||
if not isinstance(text, str) or not text:
|
||||
raise error("asset_auth_failed", f"the envelope's {field} is missing")
|
||||
try:
|
||||
@@ -245,7 +246,7 @@ class MappingKeyProvider:
|
||||
``get_content_key(key_id)``.
|
||||
"""
|
||||
|
||||
def __init__(self, keys: Optional[Mapping[str, Any]] = None) -> None:
|
||||
def __init__(self, keys: Mapping[str, Any] | None = None) -> None:
|
||||
self.keys: dict[str, bytes] = {
|
||||
str(key_id): _key_bytes(value, key_id=str(key_id))
|
||||
for key_id, value in (keys or {}).items()
|
||||
@@ -328,9 +329,9 @@ def seal_payload(
|
||||
*,
|
||||
key_id: str,
|
||||
provider: KeyProvider,
|
||||
signing_key_pem: Optional[bytes] = None,
|
||||
signing_key_pem: bytes | None = None,
|
||||
signing_key_reference: str = "",
|
||||
extra: Optional[Mapping[str, Any]] = None,
|
||||
extra: Mapping[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], bytes]:
|
||||
"""Encrypt (and optionally sign) one plaintext payload.
|
||||
|
||||
@@ -441,8 +442,8 @@ def open_payload(
|
||||
envelope: Mapping[str, Any],
|
||||
ciphertext: bytes,
|
||||
*,
|
||||
provider: Optional[KeyProvider],
|
||||
public_key_pem: Optional[bytes] = None,
|
||||
provider: KeyProvider | None,
|
||||
public_key_pem: bytes | None = None,
|
||||
) -> bytes:
|
||||
"""Verify, decrypt and hand back the plaintext payload.
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ touches the frame's own arrays and never copies them (``tests/test_scene3d_live.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -84,7 +82,7 @@ def check_field_domain(frame: SurfaceFieldFrame, binding: SurfaceBinding) -> Non
|
||||
|
||||
def sample_vertex_displacement(
|
||||
frame: SurfaceFieldFrame, binding: SurfaceBinding
|
||||
) -> Optional[np.ndarray]:
|
||||
) -> np.ndarray | None:
|
||||
"""The frame's displacement, gathered onto the render vertices.
|
||||
|
||||
``None`` when the frame carries no displacement — the common case, and the only
|
||||
|
||||
@@ -8,11 +8,16 @@ measurement difference rather than a display bug.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import Diagnostic, Mask, diagnostic, error, freeze
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
Mask,
|
||||
diagnostic,
|
||||
error,
|
||||
freeze,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -32,7 +37,7 @@ class VisualPolicy:
|
||||
island_bgr: tuple[int, int, int] = (0x16, 0x13, 0x12)
|
||||
exterior_bgr: tuple[int, int, int] = (0x0A, 0x09, 0x08)
|
||||
|
||||
def normalised(self) -> "VisualPolicy":
|
||||
def normalised(self) -> VisualPolicy:
|
||||
on = max(0.0, float(self.flow_on_px))
|
||||
off = max(0.0, float(self.flow_off_px))
|
||||
if on < off:
|
||||
@@ -65,11 +70,11 @@ class FlowSamples:
|
||||
|
||||
|
||||
def build_domain_mask(
|
||||
geometry_mask: Optional[np.ndarray],
|
||||
texture_roi: Optional[np.ndarray] = None,
|
||||
geometry_mask: np.ndarray | None,
|
||||
texture_roi: np.ndarray | None = None,
|
||||
*,
|
||||
shape_hw: Optional[tuple[int, int]] = None,
|
||||
) -> tuple[Optional[Mask], tuple[Diagnostic, ...]]:
|
||||
shape_hw: tuple[int, int] | None = None,
|
||||
) -> tuple[Mask | None, tuple[Diagnostic, ...]]:
|
||||
"""Strict intersection of geometry and optional texture ROI.
|
||||
|
||||
Shape mismatches raise rather than silently resize: a wrong mask invents
|
||||
@@ -115,10 +120,10 @@ def build_domain_mask(
|
||||
|
||||
def select_flow_samples(
|
||||
flow: np.ndarray,
|
||||
mask: Optional[np.ndarray],
|
||||
mask: np.ndarray | None,
|
||||
policy: VisualPolicy,
|
||||
*,
|
||||
previously_on: Optional[np.ndarray] = None,
|
||||
previously_on: np.ndarray | None = None,
|
||||
) -> FlowSamples:
|
||||
"""Sample optical flow on a UV grid with hysteresis and mask clipping.
|
||||
|
||||
@@ -207,7 +212,7 @@ def select_flow_samples(
|
||||
|
||||
def paint_domain_background(
|
||||
shape_hw: tuple[int, int],
|
||||
mask: Optional[np.ndarray],
|
||||
mask: np.ndarray | None,
|
||||
*,
|
||||
island_bgr: tuple[int, int, int],
|
||||
exterior_bgr: tuple[int, int, int],
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from orisys_app_shared.paths import sensors_dir
|
||||
|
||||
#: Product types the UI offers.
|
||||
|
||||
@@ -61,7 +61,7 @@ class ZmotionResult:
|
||||
def ok(self) -> bool:
|
||||
return self.code == 0
|
||||
|
||||
def raise_for_status(self, detail: str = "") -> "ZmotionResult":
|
||||
def raise_for_status(self, detail: str = "") -> ZmotionResult:
|
||||
if not self.ok:
|
||||
raise ZAuxError(self.code, detail)
|
||||
return self
|
||||
@@ -88,7 +88,7 @@ class ZmotionValue:
|
||||
"""
|
||||
return self.value if self.ok else None
|
||||
|
||||
def raise_for_status(self, detail: str = "") -> "ZmotionValue":
|
||||
def raise_for_status(self, detail: str = "") -> ZmotionValue:
|
||||
if not self.ok:
|
||||
raise ZAuxError(self.code, detail)
|
||||
return self
|
||||
@@ -122,7 +122,7 @@ class ZmotionClient:
|
||||
"""Directory the runtime was loaded from, for logs and About boxes."""
|
||||
return self._dll_dir
|
||||
|
||||
def load(self) -> "ZmotionClient":
|
||||
def load(self) -> ZmotionClient:
|
||||
"""Resolve and load the native runtime. Idempotent."""
|
||||
if self._dll is None:
|
||||
resolved = resolve_dll_dir()
|
||||
|
||||
Reference in New Issue
Block a user