Signal Synthesis¶
Parameterized synthetic waveforms as numpy arrays or WaveformData
Synthetic signal generation: parameterized waveforms as numpy arrays or WaveformData.
Public API for users who want synthetic data for testing and analysis, and the engine behind MockConnection's state-coupled waveform synthesis. Kinds live in a dispatch table of generator functions -- adding a kind is one new generator plus docs and tests; the mock coupling and code-conversion layers are kind-agnostic.
SignalSpec
dataclass
¶
SignalSpec(kind: str = 'sine', frequency: float = 1000.0, amplitude: float = 1.0, offset: float = 0.0, phase: float = 0.0, duty: float = 0.5, noise_rms: float = 0.0, seed: Optional[int] = None, drift_amplitude: float = 0.0, drift_frequency: float = 0.1, glitch_rate: float = 0.0, glitch_amplitude: float = 0.0, ringing_frequency: float = 0.0, ringing_damping: float = 5000.0, end_frequency: float = 10000.0, sweep_time: float = 0.01, sweep_log: bool = False, tau: float = 0.0001, pulse_width: float = 0.0002, edge_time: float = 1e-05, harmonics: Tuple[float, ...] = (0.1, 0.05), jitter_rms: float = 0.0, jitter_seed: Optional[int] = None, clip_level: float = 0.0, clip_softness: float = 0.0, distortion_h2: float = 0.0, distortion_h3: float = 0.0)
Parameters of one synthetic signal.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
One of "sine", "square", "triangle", "ramp", "dc", "noise", "chirp", "exponential", "pulse", "multitone". |
frequency |
float
|
Repetition rate in Hz (periodic kinds only). |
amplitude |
float
|
Peak amplitude in volts (Vpp = 2*amplitude); for "noise", the standard deviation. Ignored for "dc". |
offset |
float
|
DC offset in volts, added to every kind ("dc" outputs exactly this level). |
phase |
float
|
Phase in radians (periodic kinds only). |
duty |
float
|
High fraction of a "square" period, 0 < duty < 1 (pulse/PWM). |
noise_rms |
float
|
Std-dev of additive Gaussian noise laid on any kind. |
seed |
Optional[int]
|
None for fresh randomness per call; an int for reproducibility. |
drift_amplitude |
float
|
Volts of slow baseline wander (0 = off). |
drift_frequency |
float
|
Hz of that wander; only used when drift_amplitude > 0. |
glitch_rate |
float
|
Mean glitches per second (0 = off). |
glitch_amplitude |
float
|
Volts, peak height of a glitch. |
ringing_frequency |
float
|
Hz of post-edge oscillation (0 = off). Ringing is an EDGE impairment: it is PHYSICALLY meaningful on kinds with fast edges ("square", "pulse", or a pulse-like "ramp"). It is not, however, a no-op elsewhere -- edges are found as any nonzero sample-to-sample change, not only as a discontinuity, so on a continuous kind ("sine", "chirp", "exponential", "multitone") it acts as a derivative-weighted filter whose magnitude scales with the signal's slew rate: measurable, but usually small. Only "dc", whose sample-to-sample differences are all zero, is a true no-op. |
ringing_damping |
float
|
Decay rate per second of that oscillation; only used when ringing_frequency > 0. Defaults away from 0 for the same reason drift_frequency does: undamped ringing (decay rate 0) never actually decays, so the kernel would run for the entire buffer on every edge -- quadratic in the number of edges once ringing_frequency is switched on the most natural way, by setting only that field. |
end_frequency |
float
|
"chirp" sweep stop frequency in Hz. |
sweep_time |
float
|
"chirp" seconds per sweep, after which it retraces. |
sweep_log |
bool
|
"chirp" sweeps logarithmically rather than linearly. |
tau |
float
|
"exponential" RC time constant in seconds. |
pulse_width |
float
|
"pulse" 50%-to-50% width in seconds (FWHM), matching the
instrument convention and the threshold the repo's timing analyzer
measures at. The flat top therefore runs for pulse_width -
edge_time. "pulse" ignores |
edge_time |
float
|
"pulse" 0-to-100% transition time in seconds; 0 gives an ideal instantaneous edge. |
harmonics |
Tuple[float, ...]
|
"multitone" relative amplitudes of the 2nd, 3rd, ...
harmonic. |
jitter_rms |
float
|
Std-dev of period-to-period timing jitter, in seconds (0 =
off). PERIODIC_KINDS only (sine, square, triangle, ramp,
multitone, exponential, pulse): "dc"/"noise" have no cycle
structure and "chirp" has no stable period, so on those three
kinds this field is a no-op rather than an error. Each cycle
BOUNDARY gets its own independent Gaussian time-shift, and every
sample's shift is the LINEAR INTERPOLATION between the two
boundaries straddling it -- a continuous warp, not a per-cycle
hard step -- so an enabled What you actually measure back depends on WHERE your kind's
measurable edge sits within its cycle, because the interpolation
blends less of the "wrong" neighbor the closer the edge sits to a
boundary. Let f be that edge's fractional position in [0, 1)
(f=0 is the cycle boundary itself; _cycle_fraction computes the
same fraction for the underlying generator):
- An edge AT the cycle boundary (f=0 -- the ascending v50
crossing of "sine", "square", "multitone", and "pulse" (whose
edge sits within edge_time/2 of the boundary) at their default
phase=0.0) gets the full, unblended shift from the boundary on
each side, so period[n] = T + delta[n+1] - delta[n] exactly as
a per-cycle-constant model would give: measures |
jitter_seed |
Optional[int]
|
Decouples jitter's per-boundary randomness from |
clip_level |
float
|
Volts, symmetric clipping/saturation threshold (0 = off).
Models a non-linear output stage (an amplifier or probe front-end
driven into its rails) clipping whatever signal reaches it, so --
unlike every kind-specific field above -- it is KIND-AGNOSTIC,
same as noise_rms/drift_amplitude/glitch_rate: it applies equally
to "sine", "dc", a jittered/ringing edge, or anything else this
module can produce. It is also the LAST impairment |
clip_softness |
float
|
0 = hard clip (an exact flat top at ±clip_level,
|
distortion_h2 |
float
|
Fraction of |
distortion_h3 |
float
|
Fraction of |
SuperposedSignal
dataclass
¶
Two or more independently-synthesized signals summed into one trace.
Each component keeps its own full SignalSpec -- kind, impairments, seed -- and is synthesized in isolation; the combined waveform is their plain elementwise sum, with no state shared between components. Useful for modeling e.g. a tone riding on an independently-seeded noise floor, or two unrelated tones summed onto one channel.
Attributes:
| Name | Type | Description |
|---|---|---|
components |
Tuple[SignalSpec, ...]
|
Two or more SignalSpecs to sum. synthesize_combined() and make_waveform_combined() dispatch each one exactly as synthesize()/make_waveform() would on its own, so a bad component parameter surfaces as the same InvalidParameterError it always has. |
dut |
Optional[Any]
|
Optional device-under-test model (e.g. dut.RCLowPass), applied to the SUMMED signal rather than to any one component. Mirrors connection/mock/loopback.py's AwgLoopback.dut: stored here so a caller can carry a DUT alongside a signal source, but only connection/mock/synth.py's raw_volts actually applies it (it is the only layer that knows the sample rate and can render the filter's lead-in). |
synthesize
¶
Generate voltage samples for a signal spec.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
SignalSpec
|
Signal parameters. |
required |
sample_rate
|
float
|
Samples per second. |
required |
n_points
|
int
|
Number of samples. |
required |
t0
|
float
|
Time of the first sample in seconds (shifts periodic signals). |
0.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
float64 voltage array of length n_points. |
Source code in scpi_control/signal_synth.py
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 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 | |
synthesize_combined
¶
synthesize_combined(signal: SuperposedSignal, sample_rate: float, n_points: int, t0: float = 0.0) -> np.ndarray
Generate voltage samples for a SuperposedSignal: the sum of its components.
Each component is synthesized independently -- via synthesize(), so it keeps its own full impairments (noise, drift, glitches, jitter, seed) -- and the results are summed elementwise. No state is shared between components. A bad component parameter is caught by synthesize()'s own validate(), the same way it always is; the SuperposedSignal itself was already validated at construction (SuperposedSignal.__post_init_).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
SuperposedSignal
|
The components to sum. |
required |
sample_rate
|
float
|
Samples per second. |
required |
n_points
|
int
|
Number of samples. |
required |
t0
|
float
|
Time of the first sample in seconds (shifts periodic signals). |
0.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
float64 voltage array of length n_points. |
Source code in scpi_control/signal_synth.py
stream
¶
stream(spec: SignalSpec, sample_rate: float, chunk_size: int, *, start_time: float = 0.0, duration: Optional[float] = None, realtime: bool = False) -> Iterator[np.ndarray]
Yield phase-continuous voltage chunks for live/continuous simulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
SignalSpec
|
Signal parameters. A seeded spec uses seed + chunk_index per chunk (reproducible run-to-run, non-repeating across chunks); seed=None re-rolls noise freshly every chunk. |
required |
sample_rate
|
float
|
Samples per second. |
required |
chunk_size
|
int
|
Samples per yielded chunk. |
required |
start_time
|
float
|
Time of the very first sample in seconds. |
0.0
|
duration
|
Optional[float]
|
None streams forever (stop by breaking out); a positive number bounds the stream to round(duration * sample_rate) samples, truncating the final chunk. |
None
|
realtime
|
bool
|
When True, chunks arrive at wall-clock rate (chunk k is withheld until k * chunk_size / sample_rate seconds after the first chunk); scheduling is absolute, so timing error never accumulates, and a consumer slower than real time simply never waits. |
False
|
Returns:
| Type | Description |
|---|---|
Iterator[ndarray]
|
Iterator of float64 voltage arrays. Validation errors raise at call |
Iterator[ndarray]
|
time, before the first chunk. |
Source code in scpi_control/signal_synth.py
make_waveform
¶
Generate a WaveformData ready for analysis, saving, or reporting.
Source code in scpi_control/signal_synth.py
make_waveform_combined
¶
make_waveform_combined(signal: SuperposedSignal, sample_rate: float, n_points: int, channel: int = 1) -> WaveformData
Generate a WaveformData from a SuperposedSignal, mirroring make_waveform().