Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions adc/src/adc_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void AdcEngine::update(double /*dt*/) {
out.noise_total_W.clear();
out.phase_deg.clear();
out.fs_Hz = m_fs_Hz / 2.0;
out.is_complex_baseband = true;
out.bumpGeneration();
return;
}
Expand All @@ -67,6 +68,7 @@ void AdcEngine::update(double /*dt*/) {
for (int i = 0; i < N; ++i)
out.frequencies[i] = -m_fs_Hz / 4.0 + i * df_out;
out.fs_Hz = m_fs_Hz / 2.0;
out.is_complex_baseband = true;

// -- Noise mapping + NSD --
double nsd_W_per_Hz = 0.001 * dbToLinear(m_nsd_dBm_per_Hz);
Expand Down
2 changes: 2 additions & 0 deletions amplifier/src/amplifier_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ void AmplifierEngine::update(double dt) {
buildDefaultFrequencyGrid(out.frequencies);

out.tones = in_ptr ? in_ptr->tones : std::vector<Spectrum::Tone>{};
out.is_complex_baseband = in_ptr ? in_ptr->is_complex_baseband : false;
const size_t N = out.frequencies.size();

// Apply S21 complex gain to tones
Expand Down Expand Up @@ -136,6 +137,7 @@ void AmplifierEngine::update(double dt) {
}

out.tones = in_ptr ? in_ptr->tones : std::vector<Spectrum::Tone>{};
out.is_complex_baseband = in_ptr ? in_ptr->is_complex_baseband : false;
for (auto &t : out.tones) {
t.power_dBm += m_gain_dB;
}
Expand Down
2 changes: 2 additions & 0 deletions attenuator/src/attenuator_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ void AttenuatorEngine::update(double dt) {
int idx = 1 * m_sparam.numPorts() + 0; // S21 index

out.tones = in_ptr ? in_ptr->tones : std::vector<Spectrum::Tone>{};
out.is_complex_baseband = in_ptr ? in_ptr->is_complex_baseband : false;
for (auto &t : out.tones) {
auto S = m_sparam.interpolate(t.freq_Hz, idx);
t.power_dBm += 20.0 * std::log10(std::abs(S));
Expand Down Expand Up @@ -132,6 +133,7 @@ void AttenuatorEngine::update(double dt) {

// Attenuate tones
out.tones = in_ptr ? in_ptr->tones : std::vector<Spectrum::Tone>{};
out.is_complex_baseband = in_ptr ? in_ptr->is_complex_baseband : false;
for (auto &t : out.tones) {
t.power_dBm -= m_atten_dB;
// Phase unchanged in manual mode
Expand Down
1 change: 1 addition & 0 deletions coax/src/coax_cable_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ void CoaxCableEngine::update(double dt) {
}

out.tones = in_ptr ? in_ptr->tones : std::vector<Spectrum::Tone>{};
out.is_complex_baseband = in_ptr ? in_ptr->is_complex_baseband : false;

const CableSpec &p = preset();
const double max_f_Hz = p.max_freq_GHz * 1e9;
Expand Down
4 changes: 4 additions & 0 deletions combiner/src/combiner_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ void CombinerEngine::update(double dt) {
}

out.tones = combined_tones;
out.is_complex_baseband =
(in0 && in0->is_complex_baseband) || (in1 && in1->is_complex_baseband);

// Noise: scale by |S21|^2 and |S31|^2 respectively
const double k = 1.3806e-23;
Expand Down Expand Up @@ -229,6 +231,8 @@ void CombinerEngine::update(double dt) {
}

out.tones = combined_tones;
out.is_complex_baseband =
(in0 && in0->is_complex_baseband) || (in1 && in1->is_complex_baseband);

// Noise: incoherent power sum, then apply loss
out.noise_W.assign(N, 0.0);
Expand Down
3 changes: 2 additions & 1 deletion common/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Own the header-only data model shared by all RF Simulator modules: `SignalNode`,

- `common/common.h` — `MIN_FREQ`, `MAX_FREQ`, `MIN_POWER`, `MAX_POWER`, `DEFAULT_VBW`, `DEFAULT_RBW`, physical constants (`k`, `T`, `R`), `dbToLinear`, `calculateNoiseTemp`, `addedNoiseDensity_W_per_Hz`, `addedNoisePerBin_W`, `buildDefaultFrequencyGrid`
- `common/signal_node.h` — `SignalNode` (input + output spectra + view_enabled)
- `common/spectrum.h` — `Spectrum` (frequencies, tones, noise vectors, phase, generation counter, `fs_Hz`) and `Peak`
- `common/spectrum.h` — `Spectrum` (frequencies, tones, noise vectors, phase, generation counter, `fs_Hz`, `is_complex_baseband`) and `Peak`; also the free helper `conjugateSymmetricExpand()` for expanding real-domain tones into +-fc conjugate-symmetric pairs
- `common/component_interface.h` — `IComponentEngine` (DSP engine contract)
- `common/view_manager.h` — `ViewManager` (registry of `SignalNode*`)
- `common/include/group.h` — `Group` and `GroupBoundaryPin` (subcircuit grouping data)
Expand All @@ -26,6 +26,7 @@ Own the header-only data model shared by all RF Simulator modules: `SignalNode`,
## Work Guidance

- Changes to `SignalNode` or `Spectrum` affect every engine. Update all engines' `update()` and tests.
- `Spectrum::is_complex_baseband` (default `false`) marks spectra downstream of an ADC's DDC (complex baseband/IQ); every pass-through engine propagates it from its input exactly like `fs_Hz`. Only `AdcEngine`'s output sets it to `true`. `conjugateSymmetricExpand()` must stay render-only (used by the spectrum-analyzer render path for real-domain spectra) — never call it from interior DSP (generator, `nonlinear_model.h`, gain/filter/S-param stages, mixer), which must keep operating on the collapsed single-entry-per-tone representation.
- New fields on `IComponentEngine` must keep a default implementation that preserves backward compat for all existing engines.
- New files in `common/` or `common/include/` are automatically picked up by `common/CMakeLists.txt`'s glob.

Expand Down
37 changes: 37 additions & 0 deletions common/spectrum.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ struct Spectrum {
// Set by ADCs, propagated through most components, read by PFB channelizer.
double fs_Hz = 0.0;

// True for spectra downstream of an ADC's DDC (complex baseband/IQ). False (default) for
// real-domain (analog) spectra. Set only by AdcEngine's output; propagated downstream from
// there exactly like fs_Hz already is. Determines whether the spectrum-analyzer render path
// needs to apply conjugateSymmetricExpand() before binning tone power (see that function).
bool is_complex_baseband = false;

// Generation counter for dirty-flag tracking. Producers increment after
// recomputation so consumers can detect upstream changes.
uint64_t generation = 0;
Expand All @@ -48,6 +54,37 @@ struct Spectrum {
}
};

// Expands real-domain tones into their conjugate-symmetric (+-fc) representation per Euler's
// formula: cos(2*pi*fc*t) = 0.5*(exp(j*2*pi*fc*t) + exp(-j*2*pi*fc*t)). Each non-DC tone becomes
// two entries at +freq_Hz and -freq_Hz, each at half the linear power (-3.0103 dB = 10*log10(2)
// below the input). DC tones (freq_Hz == 0) are self-conjugate and pass through unchanged (no
// mirror, no power split).
//
// Used only where +-fc content is physically meaningful: rendering a real-domain (pre-ADC)
// spectrum. NOT used by interior DSP (generator, nonlinear_model.h, gain/filter/S-param stages,
// mixer), which must stay on the collapsed single-entry-per-tone representation — splitting there
// would corrupt nonlinear_model.h's harmonic/IM math (calibrated against full real-tone power).
inline std::vector<Spectrum::Tone>
conjugateSymmetricExpand(const std::vector<Spectrum::Tone> &tones) {
std::vector<Spectrum::Tone> out;
out.reserve(tones.size() * 2);
for (const auto &t : tones) {
if (t.freq_Hz == 0.0) {
out.push_back(t);
continue;
}
Spectrum::Tone half = t;
half.power_dBm = t.power_dBm - 10.0 * std::log10(2.0);
Spectrum::Tone at_f = half;
at_f.freq_Hz = t.freq_Hz;
Spectrum::Tone at_negf = half;
at_negf.freq_Hz = -t.freq_Hz;
out.push_back(at_f);
out.push_back(at_negf);
}
return out;
}

struct Peak {
int index;
double freq_Hz;
Expand Down
Loading
Loading