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
19 changes: 2 additions & 17 deletions adc/include/adc_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

#include <algorithm>

#include "component_interface.h"
#include "component_engine_base.h"
#include "node_graph_engine.h"
#include "signal_node.h"

class AdcEngine : public IComponentEngine {
class AdcEngine : public ComponentEngineBase {
public:
AdcEngine(int id, NodeGraphEngine &graph);

int id() const override { return m_id; }
int graphNodeId() const override { return m_graph_node_id; }
std::string_view type_name() const override { return "adc"; }
std::string hoverSummary() const override;
int inputPinId() const override;
int outputPinId() const override;

void update(double dt) override;
nlohmann::json serialize() const override;
Expand All @@ -37,18 +33,7 @@ class AdcEngine : public IComponentEngine {
}
}

SignalNode &node() override { return m_node; }
const SignalNode &node() const override { return m_node; }

private:
int m_id;
int m_graph_node_id = -1;
NodeGraphEngine *m_graph;
SignalNode m_node;

double m_fs_Hz = 1e9;
double m_nsd_dBm_per_Hz = -155.0;
bool m_dirty = true;
const Spectrum *m_cached_input_ptr = nullptr;
uint64_t m_cached_input_generation = 0;
};
16 changes: 2 additions & 14 deletions adc/src/adc_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,14 @@ static double alias_frequency(double f_RF, double Fs) {

// ---- Engine methods ----

AdcEngine::AdcEngine(int id, NodeGraphEngine &graph) : m_id(id), m_graph(&graph) {
m_graph_node_id = m_graph->addNode("ADC " + std::to_string(id), &m_node, 1, 1);
m_node.inputs.resize(1);
m_node.outputs.resize(1);
AdcEngine::AdcEngine(int id, NodeGraphEngine &graph) : ComponentEngineBase(id, graph, "ADC", 1, 1) {
LOG_INFO("ADC [adc%d] added.", id);
}

int AdcEngine::inputPinId() const { return m_graph ? m_graph->inputPinId(m_graph_node_id) : -1; }

int AdcEngine::outputPinId() const { return m_graph ? m_graph->outputPinId(m_graph_node_id) : -1; }

void AdcEngine::update(double /*dt*/) {
const Spectrum *input = m_node.inputs.empty() ? nullptr : m_node.inputs[0];
if (!m_dirty && input == m_cached_input_ptr &&
(!input || input->generation == m_cached_input_generation))
if (!beginUpdate(input))
return;
m_dirty = false;
m_cached_input_ptr = input;
if (input)
m_cached_input_generation = input->generation;

auto &out = m_node.outputs[0];

Expand Down
19 changes: 2 additions & 17 deletions amplifier/include/amplifier_engine.h
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
#pragma once

#include "component_interface.h"
#include "component_engine_base.h"
#include "node_graph_engine.h"
#include "nonlinear_model.h"
#include "s_parameter_data.h"
#include "signal_node.h"
#include <algorithm>

class AmplifierEngine : public IComponentEngine {
class AmplifierEngine : public ComponentEngineBase {
public:
AmplifierEngine(int id, NodeGraphEngine &graph);
int id() const override { return m_id; }
int graphNodeId() const override { return m_graph_node_id; }
std::string_view type_name() const override { return "amplifier"; }
std::string hoverSummary() const override;
int inputPinId() const override;
int outputPinId() const override;

void setGain_dB(double g) {
if (g != m_gain_dB) {
Expand All @@ -34,9 +30,6 @@ class AmplifierEngine : public IComponentEngine {
nlohmann::json serialize() const override;
void deserialize(const nlohmann::json &) override;

SignalNode &node() override { return m_node; }
const SignalNode &node() const override { return m_node; }

double gain_dB() const { return m_gain_dB; }
double nf_dB() const { return m_nf_dB; }
bool enableNonlinear() const { return m_nonlinear.enabled(); }
Expand Down Expand Up @@ -78,16 +71,8 @@ class AmplifierEngine : public IComponentEngine {
const SParameterData &sparamData() const { return m_sparam_data; }

private:
int m_id;
int m_graph_node_id = -1;
NodeGraphEngine *m_graph = nullptr;

SignalNode m_node;
double m_gain_dB = 0.0;
double m_nf_dB = 0.0;
bool m_dirty = true;
const Spectrum *m_cached_input_ptr = nullptr;
uint64_t m_cached_input_generation = 0;
NonlinearModel m_nonlinear;

// S-parameter state
Expand Down
15 changes: 2 additions & 13 deletions amplifier/src/amplifier_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
#include <nlohmann/json.hpp>
#include <numbers>

AmplifierEngine::AmplifierEngine(int id, NodeGraphEngine &graph) : m_id(id), m_graph(&graph) {
m_graph_node_id = graph.addNode("Amplifier " + std::to_string(id), &m_node, 1, 1);
m_node.inputs.resize(1);
m_node.outputs.resize(1);
}
AmplifierEngine::AmplifierEngine(int id, NodeGraphEngine &graph)
: ComponentEngineBase(id, graph, "Amplifier", 1, 1) {}

void AmplifierEngine::setSParamFilepath(const std::string &path) {
m_sparam_filepath = path;
Expand All @@ -17,14 +14,6 @@ void AmplifierEngine::setSParamFilepath(const std::string &path) {
m_dirty = true;
}

int AmplifierEngine::inputPinId() const {
return m_graph ? m_graph->inputPinId(m_graph_node_id) : -1;
}

int AmplifierEngine::outputPinId() const {
return m_graph ? m_graph->outputPinId(m_graph_node_id) : -1;
}

void AmplifierEngine::update(double dt) {
(void)dt;
const Spectrum *in_ptr = m_node.inputs.empty() ? nullptr : m_node.inputs[0];
Expand Down
2 changes: 1 addition & 1 deletion app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`,
- Destructor saves window state via `SessionState`

## Work Guidance
- Add a new component = one `ComponentTypeRegistry` row (`type`, `project_type`, `menu_label`, `label_prefix`, `kind`, `create`, `draw_inspector`) + a `NodeKind`/symbol entry in `node_graph` — the menu, add, duplicate, save/load, inspector, and form paths all dispatch through the registry, so no per-file edits in `RfSimulatorApp` are needed
- Add a new component = one `ComponentTypeRegistry` row (`type`, `project_type`, `menu_label`, `label_prefix`, `kind`, `create`, `draw_inspector`) + a `NodeKind`/symbol entry in `node_graph` + a drawer-map entry in `InspectorPanel::drawerMap()` (app/src/inspector_panel.cpp) — the menu, add, duplicate, save/load, and form paths all dispatch through the registry, so no per-file edits in `RfSimulatorApp` are needed. Missing drawers are logged at startup and caught by the registry/drawer consistency test in `test_component_dispatch.cpp`.

## Verification
- Round-trip tests in `tests/test_project_file.cpp`
Expand Down
7 changes: 7 additions & 0 deletions app/include/inspector_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class InspectorPanel {
// callbacks to this panel's property drawers.
void registerDrawers(ComponentTypeRegistry &registry);

// True if a property drawer is registered for the given canonical type
// key (e.g. "amplifier"). Exposed so tests can assert that every
// ComponentTypeRegistry row has inspector coverage; a registry type with
// no drawer logs an error from registerDrawers() instead of silently
// rendering an empty properties panel.
static bool hasDrawer(std::string_view type);

void drawAmplifierProperties(AmplifierEngine &engine, int index);
void drawCoaxCableProperties(CoaxCableEngine &engine, int index);
void drawEqualizerProperties(EqualizerEngine &engine, int index);
Expand Down
4 changes: 4 additions & 0 deletions app/src/component_type_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ ComponentTypeRegistry::ComponentTypeRegistry() {
att.label_prefix = "Attenuator";
att.kind = NodeKind::Attenuator;
att.authorable = true;
att.supports_sparam_file = true;
att.fields = {
{"attenuation_dB", "Attenuation", "dB", FieldKind::Number, true, 0.0, 100.0, {}, {}, ""},
};
Expand Down Expand Up @@ -101,6 +102,7 @@ ComponentTypeRegistry::ComponentTypeRegistry() {
flt.label_prefix = "IdealFilter";
flt.kind = NodeKind::IdealFilter;
flt.authorable = true;
flt.supports_sparam_file = true;
flt.fields = {
{"filter_type",
"Filter Type",
Expand Down Expand Up @@ -155,6 +157,7 @@ ComponentTypeRegistry::ComponentTypeRegistry() {
eq.label_prefix = "Equalizer";
eq.kind = NodeKind::Equalizer;
eq.authorable = true;
eq.supports_sparam_file = true;
eq.fields = {
{"ref_gain_dB", "Reference Gain", "dB", FieldKind::Number, false, -50.0, 50.0, {}, {}, ""},
{"ref_freq_Hz",
Expand Down Expand Up @@ -191,6 +194,7 @@ ComponentTypeRegistry::ComponentTypeRegistry() {
comb.label_prefix = "Combiner";
comb.kind = NodeKind::Combiner;
comb.authorable = true;
comb.supports_sparam_file = true;
comb.fields = {
{"manual_mode", "Manual Mode", "", FieldKind::Bool, false, 0, 0, {}, false, ""},
};
Expand Down
127 changes: 77 additions & 50 deletions app/src/inspector_panel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,63 +16,90 @@
#include "splitter_engine.h"
#include "utils.h"
#include <cstring>
#include <map>
#include <portable-file-dialogs.h>

#include "component_registry.h"

InspectorPanel::InspectorPanel(NodeGraphEngine &graph, ComponentRegistry &components)
: m_graph(graph), m_components(&components) {}

namespace {

using DrawerFn = std::function<void(InspectorPanel &, IComponentEngine &)>;

// Canonical type key -> property drawer. Built once; registerDrawers() copies
// each entry onto the matching ComponentTypeRegistry row, and hasDrawer()
// lets tests assert every registered type has inspector coverage.
const std::map<std::string_view, DrawerFn> &drawerMap() {
static const std::map<std::string_view, DrawerFn> drawers = {
{"generator",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawGeneratorProperties(static_cast<SignalGeneratorEngine &>(e), e.id());
}},
{"amplifier",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawAmplifierProperties(static_cast<AmplifierEngine &>(e), e.id());
}},
{"splitter",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawSplitterProperties(static_cast<SplitterEngine &>(e), e.id());
}},
{"mixer",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawMixerProperties(static_cast<MixerEngine &>(e), e.id());
}},
{"adc",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawAdcProperties(static_cast<AdcEngine &>(e), e.id());
}},
{"pfb",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawPFBProperties(static_cast<PFBChannelizerEngine &>(e));
}},
{"filter",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawIdealFilterProperties(static_cast<IdealFilterEngine &>(e), e.id());
}},
{"coax",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawCoaxCableProperties(static_cast<CoaxCableEngine &>(e), e.id());
}},
{"equalizer",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawEqualizerProperties(static_cast<EqualizerEngine &>(e), e.id());
}},
{"attenuator",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawAttenuatorProperties(static_cast<AttenuatorEngine &>(e), e.id());
}},
{"combiner",
[](InspectorPanel &p, IComponentEngine &e) {
p.drawCombinerProperties(static_cast<CombinerEngine &>(e), e.id());
}},
};
return drawers;
}

} // namespace

void InspectorPanel::registerDrawers(ComponentTypeRegistry &registry) {
const auto &drawers = drawerMap();
for (auto *d : registry.all()) {
if (d->type == "generator") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawGeneratorProperties(static_cast<SignalGeneratorEngine &>(e), e.id());
};
} else if (d->type == "amplifier") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawAmplifierProperties(static_cast<AmplifierEngine &>(e), e.id());
};
} else if (d->type == "splitter") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawSplitterProperties(static_cast<SplitterEngine &>(e), e.id());
};
} else if (d->type == "mixer") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawMixerProperties(static_cast<MixerEngine &>(e), e.id());
};
} else if (d->type == "adc") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawAdcProperties(static_cast<AdcEngine &>(e), e.id());
};
} else if (d->type == "pfb") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawPFBProperties(static_cast<PFBChannelizerEngine &>(e));
};
} else if (d->type == "filter") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawIdealFilterProperties(static_cast<IdealFilterEngine &>(e), e.id());
};
} else if (d->type == "coax") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawCoaxCableProperties(static_cast<CoaxCableEngine &>(e), e.id());
};
} else if (d->type == "equalizer") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawEqualizerProperties(static_cast<EqualizerEngine &>(e), e.id());
};
} else if (d->type == "attenuator") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawAttenuatorProperties(static_cast<AttenuatorEngine &>(e), e.id());
};
} else if (d->type == "combiner") {
d->draw_inspector = [](InspectorPanel &p, IComponentEngine &e) {
p.drawCombinerProperties(static_cast<CombinerEngine &>(e), e.id());
};
auto it = drawers.find(d->type);
if (it != drawers.end()) {
d->draw_inspector = it->second;
} else {
// A registry row with no drawer used to end up with an empty
// properties panel silently; fail loudly so adding a component
// type also registers a drawer here.
LOG_ERROR("No inspector drawer registered for component type '%s'", d->type.c_str());
}
}
}

bool InspectorPanel::hasDrawer(std::string_view type) { return drawerMap().count(type) > 0; }

InspectorPanel::Hit InspectorPanel::findSelected() const {
int n = ImNodes::NumSelectedNodes();
if (n != 1)
Expand Down Expand Up @@ -577,18 +604,18 @@ void InspectorPanel::drawAttenuatorProperties(AttenuatorEngine &engine, int inde
engine.setAttenuation(static_cast<double>(atten_f));
}

bool sparam_mode = engine.sParamMode();
bool sparam_mode = engine.sparamMode();
if (ImGui::Checkbox("S-param mode", &sparam_mode)) {
engine.setSParamMode(sparam_mode);
}

if (sparam_mode) {
std::string path = engine.sParamFile();
std::string path = engine.sparamFilepath();
char path_buf[512];
strncpy(path_buf, path.c_str(), sizeof(path_buf) - 1);
path_buf[sizeof(path_buf) - 1] = '\0';
if (ImGui::InputText("S-param file", path_buf, sizeof(path_buf))) {
engine.setSParamFile(path_buf);
engine.setSParamFilepath(path_buf);
}
}

Expand All @@ -603,18 +630,18 @@ void InspectorPanel::drawCombinerProperties(CombinerEngine &engine, int index) {

ImGui::TextDisabled("Combiner: 2 inputs → 1 output");

bool sparam_mode = engine.sParamMode();
bool sparam_mode = engine.sparamMode();
if (ImGui::Checkbox("S-parameter mode", &sparam_mode)) {
engine.setSParamMode(sparam_mode);
}

if (sparam_mode) {
std::string path = engine.sParamFile();
std::string path = engine.sparamFilepath();
char path_buf[512];
strncpy(path_buf, path.c_str(), sizeof(path_buf) - 1);
path_buf[sizeof(path_buf) - 1] = '\0';
if (ImGui::InputText("S-param file", path_buf, sizeof(path_buf))) {
engine.setSParamFile(path_buf);
engine.setSParamFilepath(path_buf);
}
}

Expand Down
Loading