visualdynamics.core.sine¶
sine
¶
Sine sweep specifications: named tones, each its own time-boxed sweep.
Phase B of the sine sweep arc (PLAN.md). A sine test's requirement is not a curve over one abscissa — it is a set of tones, each a breakpoint table with its own sweep law and its own start time, played simultaneously and only partially overlapping in the ordinary case (measured on the Phase A fixture: four tones playing 0-15, 2-13, 3-14 and 1-8 s of one 15 s environment). That shape does not fit a DataArray, so it gets its own class rather than a forced one.
The sweep law here is the one the Phase A runs pinned against the controller's own records, not assumed from documentation:
segment_rate[i]governs the segment fromfrequency[i]tofrequency[i+1]— the leading convention, matched to 0.0009 Hz over a 14 s sweep.- A linear segment's rate is in Hz/s; a logarithmic segment's rate is in oct/min (one octave at rate 10 transited in 5.87 s ~= 6 s; the controller's own docstrings disagree with each other and the measurement settled it).
- A descending segment takes a negative rate. A rate whose sign contradicts its breakpoints is refused by name here, because the controller refuses the same thing as a negative sweep time — at initialization, deep in its log.
Amplitude between breakpoints interpolates linearly in time, which
is linear in frequency on a linear segment and linear in log-frequency
on a logarithmic one — target() interpolates that way per segment,
so the compliance curve is the curve the controller was actually
chasing.
One more alignment fact for the extraction to lean on, measured on the
Phase A fixture: the controller ramps each tone up for the
environment's ramp_time before sweeping, so the recorded trajectory
leaves its start frequency at start_time + ramp_time — all four
fixture tones rebuilt against the controller's own record to 0.0000 Hz
with exactly that lag.
Everything is SI at rest, like every other object in core.
Classes:
| Name | Description |
|---|---|
SineTone |
One tone: breakpoints, a sweep law, and its own window in time. |
SineSweepSpecification |
What a sine test was controlled to: tones over control channels. |
SineTarget |
One tone's requirement laid over frequency, as a curve. |
SineLevel |
A sine sweep's measured level: amplitude against frequency. |
SineLevelSet |
One extraction, one object: the tones' levels, grouped the way |
Functions:
| Name | Description |
|---|---|
find_tone |
Where a tone's sweep begins in a recording, by matched filter. |
find_environment |
Where the tones' shared clock starts, by joint matched filter. |
vold_kalman |
Every tone's complex envelope at every sample, solved jointly. |
extract_sine |
Read each tone's level out of a recording, against its own sweep. |
Classes¶
SineTone
¶
SineTone(
name: str,
start_time: float,
frequency: Any,
amplitude: Any,
segment_type: Any,
segment_rate: Any,
phase: Any = None,
warning_lower: Any = None,
warning_upper: Any = None,
abort_lower: Any = None,
abort_upper: Any = None,
)
One tone: breakpoints, a sweep law, and its own window in time.
Attributes: name: What the controller called it ('Sine Tone 1'). start_time: Seconds after the environment started that this tone turns on. Tones are silent outside their own span — never held at an end frequency. frequency: Breakpoints, Hz, shape (n,). Ascending or descending per segment; each segment's rate sign must agree. amplitude: Target amplitude at each breakpoint per control channel, shape (n, m), SI. phase: Radians at each breakpoint per channel, shape (n, m). segment_type: (n-1,), LINEAR or LOG per segment. segment_rate: (n-1,), Hz/s for a linear segment, oct/min for a logarithmic one; negative for a descending segment. warning_lower, warning_upper, abort_lower, abort_upper: Band curves at the breakpoints, shape (n, m), or None where the specification carries no such limit.
Methods:
| Name | Description |
|---|---|
segment_seconds |
How long each segment takes, from its span and its rate. |
span |
When this tone plays, in environment time. |
trajectory |
Frequency versus time over the tone's own span. |
argument |
The cosine argument over the tone's span: 2piintegral(f). |
target |
The specified level at given frequencies, shape (len, m). |
Source code in src/visualdynamics/core/sine.py
Methods:¶
segment_seconds
¶
How long each segment takes, from its span and its rate.
Source code in src/visualdynamics/core/sine.py
span
¶
trajectory
¶
Frequency versus time over the tone's own span.
Returns (t, f): t in seconds from the tone's start (add
start_time for environment time), f in Hz. This is the
rebuild that matched the controller's own record to 0.0009 Hz.
Source code in src/visualdynamics/core/sine.py
argument
¶
target
¶
The specified level at given frequencies, shape (len, m).
curve is 'amplitude' or one of the limit names. Amplitude
interpolates linearly in time — linear in frequency on a
linear segment, linear in log-frequency on a logarithmic one —
because that is the target the controller chases. Frequencies
the tone never sweeps come back NaN: the specification says
nothing there, and NaN says so where zero would lie.
Source code in src/visualdynamics/core/sine.py
SineSweepSpecification
¶
SineSweepSpecification(
tones: Sequence[SineTone],
response_dof: Sequence[str],
ordinate_dim: str = "acceleration",
ordinate_unit: str | None = None,
comment: str = "",
)
What a sine test was controlled to: tones over control channels.
One object per sine environment, mirroring the controller's file: the tones (each with its own breakpoints, sweep law, bands and start time) and the control DOFs their amplitude columns belong to. Simultaneous tones are the ordinary case, not a variant.
Attributes: tones: The SineTone list, in the file's order. response_dof: The control channel DOF strings — the columns of every tone's amplitude table, in order. ordinate_dim: What the amplitudes are ('acceleration' for a controller run on accelerometers). ordinate_unit: The SI unit the amplitudes are stored in, or None while undeclared — the same convention every DataArray follows. comment: One line about the set as a whole.
Methods:
| Name | Description |
|---|---|
span |
When any tone is playing: first start to last end. |
tone_curve |
One tone's requirement as a plottable, comparable curve. |
Source code in src/visualdynamics/core/sine.py
Methods:¶
span
¶
tone_curve
¶
tone_curve(name: str, lines: int = 400) -> SineTarget
One tone's requirement as a plottable, comparable curve.
The abscissa follows the tone's own sweep — lines points
spaced the way the sweep dwells, ascending whichever way it
swept — and the bands ride along as Bounded limits.
Source code in src/visualdynamics/core/sine.py
SineTarget
¶
One tone's requirement laid over frequency, as a curve.
What SineSweepSpecification.tone_curve hands the plot and the
report: the tone's amplitude interpolated along its own sweep,
with the warning and abort bands as the four limit curves every
Bounded object carries — so the comparison against an extracted
level draws with the same zones, the same shading and the same
pairing a random specification gets, from the same machinery.
Derived on demand; the specification object stays the one source.
Source code in src/visualdynamics/core/sine.py
SineLevel
¶
Bases: Spectrum
A sine sweep's measured level: amplitude against frequency.
What extract_sine reads out of a recording for one tone — the
demodulated complex amplitude of that tone at each control channel,
sampled along the sweep and laid over frequency. The magnitude is
the tracked amplitude the controller was steering; the angle is the
phase relative to the reconstructed sweep argument, meaningful
between channels rather than absolutely.
The frequency coverage is the coverage: a run stopped early, or a tone the instructions windowed, extracts fewer lines, and the comparison against the specification sees exactly how much of the required range was actually run rather than being told everything was.
Attributes: tone: The specification tone this level was extracted for. onset: Seconds into the recording where the tone's sweep proper was found (matched filter, or the caller's override) — reported so the alignment is auditable. seconds: When each line was measured, in recording seconds — the sweep's own clock, one entry per abscissa line, which is what lets the level stand on the 3-D stage without the specification beside it. None on a level from before the clock was kept.
Source code in src/visualdynamics/core/sine.py
SineLevelSet
¶
SineLevelSet(levels: Sequence[SineLevel])
One extraction, one object: the tones' levels, grouped the way the specification groups its tones (Brandon, 2026-08-22).
Each tone sweeps its own frequencies on its own clock, so the
levels stay separate SineLevels inside — different abscissas
cannot share a DataArray — but the project holds one thing, it
expands into one row per tone, and picking rows plots a subset,
exactly as the specification does.
Attributes: levels: The per-tone SineLevels, in the specification's order.
Source code in src/visualdynamics/core/sine.py
Functions:¶
find_tone
¶
find_tone(
records: ndarray,
dt: float,
tone: SineTone,
search: tuple[float, float] | None = None,
) -> float
Where a tone's sweep begins in a recording, by matched filter.
Correlates the reconstructed sweep template against every record
and sums the correlation power across them — the processing gain
over a whole sweep is what finds a tone under a random excitation
much louder than it (measured: a clean find under +15.6 dB of
random). search bounds the onset in seconds when the caller
knows roughly where to look. Returns the onset in seconds.
Source code in src/visualdynamics/core/sine.py
find_environment
¶
find_environment(
records: ndarray, dt: float, tones: Sequence[SineTone]
) -> float
Where the tones' shared clock starts, by joint matched filter.
Every tone in one environment begins at its own start_time on
one clock, so there is one unknown — the clock's position in the
recording — and every tone's correlation votes on it at its own
lag. The sharp votes carry the ambiguous ones: a near-dwell or a
log sweep that mis-locks alone (measured: half a second off in a
four-tone mix) is pinned by the linear sweeps beside it. Returns
the clock origin in seconds; tone i's sweep begins at
origin + start_time_i.
Source code in src/visualdynamics/core/sine.py
vold_kalman
¶
vold_kalman(
signal: ndarray,
arguments: Sequence[ndarray],
frequencies: Sequence[ndarray],
starts: Sequence[int],
dt: float,
cycles: float = 10.0,
) -> list[ndarray]
Every tone's complex envelope at every sample, solved jointly.
The second-order Vold-Kalman filter: the record is modelled as the sum of the tones, each a slowly varying complex amplitude on its own known sweep argument, and the amplitudes are found by least squares — the data equation pulling the sum onto the record, a smoothness penalty on each envelope's second difference pulling it towards a slow curve. Solved at once for all tones, so where two sweeps cross the solver separates them by their different frequency histories on either side, rather than each reading the other as noise: a planted 1.0 crossed by a 3.0 read 0.4 dB low at the median under the tracking demodulation this replaced (Brandon, 2026-09-03: the last analysis before a report should use the best estimator there is).
The penalty weight follows each tone's instantaneous frequency so
the envelope is smoothed over cycles cycles everywhere — the
filter's -3 dB bandwidth matched to the centred average of the
same span, so the setting keeps its meaning. Outside a tone's own
span its envelope is pinned to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
One channel, evenly sampled. |
required |
arguments
|
sequences of ndarray
|
Per tone, the cosine argument and the instantaneous frequency
(Hz) over the tone's span, as |
required |
frequencies
|
sequences of ndarray
|
Per tone, the cosine argument and the instantaneous frequency
(Hz) over the tone's span, as |
required |
starts
|
sequence of int
|
Per tone, the sample at which its span begins in |
required |
dt
|
float
|
The sample interval in seconds. |
required |
cycles
|
float
|
Cycles of smoothing. |
10.0
|
Returns:
| Type | Description |
|---|---|
list of ndarray
|
Per tone, the complex envelope over its span (clipped to the record): magnitude is the peak amplitude, angle the phase against the reconstructed sweep. |
Source code in src/visualdynamics/core/sine.py
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 | |
extract_sine
¶
extract_sine(
history: Any,
specification: SineSweepSpecification,
tones: Sequence[str] | None = None,
onsets: dict[str, float] | None = None,
cycles: float = 10.0,
points_per_window: float = 2.0,
) -> SineLevelSet
Read each tone's level out of a recording, against its own sweep.
Reconstruct every wanted tone's sweep argument from the
specification's breakpoints, find where the environment's clock
begins in the recording (joint matched filter; onsets overrides
per tone name, and the found value rides the result as .onset),
then solve for every tone's complex envelope on every control
channel at once with the second-order Vold-Kalman filter
(vold_kalman): the record modelled as the sum of the tones on
their known sweeps, each envelope held to a slow curve over
cycles cycles of its own instantaneous frequency. Joint, so
crossing sweeps are separated by their frequency histories rather
than each reading the other as noise — the documented limit of the
tracking demodulation this replaced (2026-09-03).
The magnitude is debiased: a noisy envelope's magnitude reads high, so the noise power left in the residual around each sample, scaled by the filter's equivalent averaging length, is subtracted from the squared magnitude before the square root — a planted amplitude under 4x its own RMS of noise reads back within a fraction of a dB. The controller's own live tracker carries the raw bias, which is worth remembering when the two are compared.
The envelope is sampled every window/points_per_window along the
sweep, one (almost) independent reading each. Returns a
SineLevelSet — one object, one SineLevel per tone inside,
frequencies ascending whichever way the tone swept, each line
stamped with the second it was measured. A recording that ends
before a tone does yields the lines it reached — the coverage the
comparison reports.
Source code in src/visualdynamics/core/sine.py
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 | |