HOA7 Spatial Field Decoder (hoa64 v0.5.0): 7th-order Ambisonics encode/decode, Wigner-D rotation, DOA analysis, vision fuse, diffusion conditioning
570b87b verified | """RNN integrator stub — Phase 0 interface for iterative motion in a field. | |
| No learned weights yet: explicit Euler integration of pose + field rotation. | |
| Validates the "calculator + loop" hypothesis before training dynamics. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| import numpy as np | |
| from .basis import N_CHANNELS | |
| from .encode import encode_points | |
| from .rotate import rotate_matrix_order1, rotate_yaw_pitch_roll | |
| from .analysis import doa_from_intensity, peak_direction, field_energy | |
| def _pad64(a: np.ndarray) -> np.ndarray: | |
| a = np.asarray(a, dtype=np.float64).reshape(-1) | |
| out = np.zeros(N_CHANNELS, dtype=np.float64) | |
| out[: min(N_CHANNELS, a.shape[0])] = a[:N_CHANNELS] | |
| return out | |
| class SpatialState: | |
| """Agent + field state for iterative spatial calculation.""" | |
| hoa: np.ndarray # (64,) world field in listener frame after last step | |
| yaw: float = 0.0 # degrees, agent heading | |
| pitch: float = 0.0 | |
| roll: float = 0.0 | |
| history: list = field(default_factory=list) | |
| def __post_init__(self) -> None: | |
| self.hoa = _pad64(self.hoa) | |
| def snapshot(self) -> dict: | |
| az, el = doa_from_intensity(self.hoa) | |
| paz, pel, pval = peak_direction(self.hoa, n_azi=72, n_el=36) | |
| return { | |
| "yaw": self.yaw, | |
| "pitch": self.pitch, | |
| "roll": self.roll, | |
| "energy": field_energy(self.hoa), | |
| "doa_intensity_az_el": (az, el), | |
| "doa_peak_az_el": (paz, pel), | |
| "peak_power": pval, | |
| } | |
| def step_rotate( | |
| state: SpatialState, | |
| d_yaw: float = 0.0, | |
| d_pitch: float = 0.0, | |
| d_roll: float = 0.0, | |
| *, | |
| max_order: int = 7, | |
| dense: bool = True, | |
| ) -> SpatialState: | |
| """Integrate a pose increment: rotate the field opposite agent turn. | |
| If the agent yaws +θ (turns left), the world field in head frame yaws −θ. | |
| """ | |
| # Agent pose update | |
| yaw = state.yaw + d_yaw | |
| pitch = state.pitch + d_pitch | |
| roll = state.roll + d_roll | |
| # Field in listener frame: rotate by -d_* | |
| if dense and max_order > 1: | |
| hoa = rotate_yaw_pitch_roll( | |
| state.hoa, | |
| yaw=-d_yaw, | |
| pitch=-d_pitch, | |
| roll=-d_roll, | |
| degrees=True, | |
| max_order=max_order, | |
| ) | |
| else: | |
| hoa = rotate_matrix_order1( | |
| state.hoa, yaw=-d_yaw, pitch=-d_pitch, roll=-d_roll, degrees=True | |
| ) | |
| hoa = _pad64(hoa) | |
| new = SpatialState(hoa=hoa, yaw=yaw, pitch=pitch, roll=roll, history=list(state.history)) | |
| new.history.append(new.snapshot()) | |
| return new | |
| def world_from_sources( | |
| azimuths, | |
| elevations, | |
| gains=None, | |
| *, | |
| max_order: int = 7, | |
| ) -> SpatialState: | |
| """Build initial state from world-frame sources (listener at origin, identity pose).""" | |
| a = encode_points(azimuths, elevations, gains, degrees=True, max_order=max_order) | |
| st = SpatialState(hoa=a) | |
| st.history.append(st.snapshot()) | |
| return st | |