some fixes
This commit is contained in:
@@ -13,7 +13,10 @@ comparable S21 trace is a fixed three-stage pipeline:
|
||||
f(phase) = freq0 + (phase - phase0) * (freq1 - freq0) / (phase1 - phase0)
|
||||
|
||||
Trigger jitter shifts every sample's absolute phase together, so the measured
|
||||
band floats from sweep to sweep around the fixed calibration.
|
||||
band floats from sweep to sweep around the fixed calibration. When that float
|
||||
carries the unwrap anchor (sample 0) across the +/-pi branch cut, a stray sweep
|
||||
is offset by a whole 2*pi turn; it is snapped back onto the branch nearest the
|
||||
calibration before mapping (see ``_anchor_phase_to_calibration_branch``).
|
||||
|
||||
2. **Amplitude normalization.** ``S = main / |reference|`` divides out the
|
||||
stimulus amplitude. Only the magnitude is removed; the reference phase is used
|
||||
@@ -47,6 +50,13 @@ _REFERENCE_AMPLITUDE_FLOOR = 1e-9
|
||||
# a sweep yielding fewer usable points is malformed and rejected.
|
||||
_MIN_USABLE_POINTS = 2
|
||||
|
||||
# One full turn of phase. ``np.unwrap`` reconstructs each sweep's phase ramp but
|
||||
# anchors it to the raw ``np.angle`` of the first sample, which lives on the
|
||||
# (-pi, pi] branch. Trigger jitter occasionally lands that anchor on the far side
|
||||
# of the +/-pi branch cut for a stray sweep or two, rigidly offsetting the whole
|
||||
# ramp by exactly this much before it settles back onto the physical branch.
|
||||
_PHASE_BRANCH_PERIOD_RAD = 2.0 * np.pi
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KamilAdcProcessingParams:
|
||||
@@ -133,9 +143,45 @@ class KamilAdcSweepProcessor:
|
||||
Returns frequencies in *step order* (not sorted); see the module docstring
|
||||
for the calibration law.
|
||||
"""
|
||||
phase = np.unwrap(np.angle(np.asarray(reference)))
|
||||
phase = self._anchor_phase_to_calibration_branch(
|
||||
np.unwrap(np.angle(np.asarray(reference)))
|
||||
)
|
||||
return self._params.freq0_hz + (phase - self._params.phase0_rad) * self._params.hz_per_rad
|
||||
|
||||
def _anchor_phase_to_calibration_branch(self, phase: np.ndarray) -> np.ndarray:
|
||||
"""Collapse a stray 2*pi branch excursion back onto the physical branch.
|
||||
|
||||
``np.unwrap`` reconstructs a continuous phase ramp but pins its absolute
|
||||
level to the raw angle of the first sample, which lives on the (-pi, pi]
|
||||
branch. Trigger jitter occasionally lands that anchor on the wrong side of
|
||||
the +/-pi cut, rigidly shifting the whole sweep by one
|
||||
:data:`_PHASE_BRANCH_PERIOD_RAD` (~157 MHz on the rig) until it settles back
|
||||
a sweep or two later. Such an excursion would otherwise wreck the frequency
|
||||
axis, the band-coverage check, and the normalization.
|
||||
|
||||
The calibration's ``phase0_rad`` is the expected first-sample phase (its
|
||||
median across many sweeps), so the physical branch is the one nearest it.
|
||||
We round the first sample onto that branch and shift the whole ramp by the
|
||||
same whole number of turns. This is:
|
||||
|
||||
* **Stateless** — each sweep is judged only against the fixed calibration,
|
||||
so a glitch can never propagate into, or latch, later sweeps.
|
||||
* **Self-correcting** — a glitched sweep is pulled back onto the band and
|
||||
yields usable data instead of being rejected.
|
||||
* **Span-invariant** — it keys on the first sample (a fixed sweep start),
|
||||
not on how much band the sweep happens to span.
|
||||
|
||||
Genuine sweep-to-sweep float (well under pi against a calibration centered
|
||||
on its median) rounds to zero turns and is left untouched. A float that
|
||||
ever drifts past pi is a recalibration concern, not a per-sweep glitch.
|
||||
"""
|
||||
if phase.size == 0:
|
||||
return phase
|
||||
branch_turns = np.round((phase[0] - self._params.phase0_rad) / _PHASE_BRANCH_PERIOD_RAD)
|
||||
if branch_turns:
|
||||
phase = phase - branch_turns * _PHASE_BRANCH_PERIOD_RAD
|
||||
return phase
|
||||
|
||||
def process(self, main: np.ndarray, reference: np.ndarray) -> np.ndarray | None:
|
||||
"""Return the S21 trace resampled onto the fixed grid, or ``None`` to reject.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user