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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ add_subdirectory("iq_plot")
add_subdirectory("ideal_filter")
add_subdirectory("attenuator")
add_subdirectory("combiner")
add_subdirectory("network_analyzer")
add_subdirectory("help")
add_subdirectory("layout")
add_subdirectory("tutorial")
Expand Down
2 changes: 2 additions & 0 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`,
- `InspectorPanel` — property editing panel with dirty tracking
- `ComponentTypeRegistry` — single dispatch table (11 rows) for canvas menu, add, duplicate, save/load, and inspector drawing: each row carries the canonical `type` + `.rfsim` `project_type` keys, `menu_label`/`label_prefix`, `NodeKind`, a `create()` factory, and a `draw_inspector` callback; also drives `ComponentLibrary::instantiate()`/`validate()` and the component authoring form
- `PFBViewManager` — owns the per-PFB IQ Plot / Channelizer Grid widget lifecycle (replaces the app's four lockstep vectors `m_iq_widgets`/`m_show_iq_pfbs`/`m_pfb_grid_widgets`/`m_show_pfb_grids`, which were rebuilt by hand at six call sites and caused issue #37); all add/rebuild/clear/draw and visibility state funnel through this class
- `NetworkAnalyzerEngine` / `NetworkAnalyzerWidget` — singleton instrument panel owned directly by `RfSimulatorApp` (like `m_spectrum_engine`/`m_spectrum_widget`): the engine is a plain value member (not an `IComponentEngine`, no registry row, no graph node) constructed with `m_graph_engine` and a small app-owned `INetworkAnalyzerHost` adapter (`NaHost`, see `network_analyzer_engine.h`'s layering comment) that resolves live engines via `ComponentRegistry::find` and builds private, throwaway scratch clone passes; `m_na_widget` renders the Point A/B pickers + sweep fields + gain/NF plot
- `ProjectSerializer` — owns the `.rfsim` save/load/new JSON logic (extracted from `RfSimulatorApp`, issue #51)
- `ComponentFormModel` / `ComponentFormWidget` — pure-logic + ImGui rendering pair for the New/Edit Component form
- `ExtensionManager` — extension manifest discovery and status tracking across built-in/global/project-local roots
Expand All @@ -30,6 +31,7 @@ Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`,
- App-level integration tests may use `testExtensionManager()` and `testExtensionResultMessage()` with an ImGui/ImPlot/ImNodes fixture
- `load_window_states()` runs on construction to restore persisted window visibility toggles
- Per-PFB IQ Plot / Channelizer Grid window visibility lives in `PFBViewManager::iqVisibility()`/`gridVisibility()` (indexed in lockstep with the manager's widgets) and has no View-menu entry since instances are dynamic; closed windows are reopened via "Show IQ Plot"/"Show Channelizer Grid" checkboxes in the PFB properties panel, wired each frame through `InspectorPanel::setPFBWindowVisibility()` (stores the stable vector pointers, not element pointers, since the vectors are rebuilt on add/remove)
- The Network Analyzer panel is a singleton like the Spectrum Analyzer: `View > Network Analyzer` toggles `m_show_na`, which persists through `SessionState` (`WindowState`/`NetworkAnalyzer`) and drives `m_na_engine.update()` + `m_na_widget->draw(...)` each frame while visible
- `update_dsp()`'s signal-routing pass is factored into `rewireInputs()` (sets every component's `node().inputs[k]` from current graph links, binding the resolved output port's `Spectrum` — `&source->outputs[source.output_index]` — and nulling severed ones); `onRemoveNode` calls it synchronously right after `ComponentRegistry::remove()` so no surviving component is left holding a dangling `Spectrum*` into the just-destroyed engine's `SignalNode` while the rest of that frame's `draw_ui()` still runs — widgets that dereference `node().inputs[]` directly during draw (e.g. `PFBChannelizerWidget`) would otherwise use-after-free (issue #37)
- Destructor saves window state via `SessionState`

Expand Down
2 changes: 2 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ target_link_libraries(app
simulator::ideal_filter_engine
simulator::attenuator_engine
simulator::combiner_engine
simulator::network_analyzer_engine
simulator::network_analyzer_widget
simulator::node_graph_engine
simulator::node_graph_widget
simulator::help_widget
Expand Down
44 changes: 43 additions & 1 deletion app/include/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include "library_browser_widget.h"
#include "logging_widget.h"
#include "mixer_engine.h"
#include "network_analyzer_engine.h"
#include "network_analyzer_widget.h"
#include "node_graph_engine.h"
#include "node_graph_widget.h"
#include "pfb_channelizer_engine.h"
Expand Down Expand Up @@ -54,6 +56,7 @@ class RfSimulatorApp {
LoggingWidget m_log_widget;
bool m_show_log = true;
bool m_show_spectrum = true;
bool m_show_na = false;
bool m_show_properties = true;
bool m_show_node_editor = true;
bool m_show_help = false;
Expand Down Expand Up @@ -91,6 +94,7 @@ class RfSimulatorApp {
// Test helpers exposed for project file round-trip tests
NodeGraphEngine &testGraphEngine() { return m_graph_engine; }
ComponentRegistry &testComponents() { return m_components; }
NetworkAnalyzerEngine &testNetworkAnalyzerEngine() { return m_na_engine; }
NodeGraphWidget &testGraphWidget() { return *m_graph_widget; }
LayoutManager &testLayoutManager() { return m_layout_manager; }
TutorialState &testTutorialState() { return m_tutorial_state; }
Expand All @@ -114,10 +118,40 @@ class RfSimulatorApp {
void openEditComponentForm(const ComponentDefinition &def);
void drawComponentFormModal();
bool saveComponentForm();

// --- Network Analyzer host adapter --------------------------------------
// The engine lives in the DSP-engines layer below app/ and never sees app
// types; RfSimulatorApp implements its two injected lookups (see
// network_analyzer_engine.h's layering comment). componentForNode wraps
// ComponentRegistry::find; beginScratchPass hands out one private,
// throwaway scratch graph+registry per measurement pass whose clones are
// destroyed with it (RAII), so a pass never touches the real graph/registry.
class NaScratch;
class NaHost final : public INetworkAnalyzerHost {
public:
explicit NaHost(ComponentRegistry &components);
IComponentEngine *componentForNode(int graph_node_id) const override;
std::unique_ptr<INetworkAnalyzerScratch> beginScratchPass() const override;

private:
ComponentRegistry &m_components;
};
class NaScratch final : public INetworkAnalyzerScratch {
public:
NaScratch();
IComponentEngine *createClone(std::string_view type, int id) override;

private:
NodeGraphEngine m_graph;
ViewManager m_view;
ComponentRegistry m_registry; // constructed with (m_graph, m_view)
};

NodeGraphEngine m_graph_engine;
ViewManager m_view_manager;
SpectrumAnalyzerEngine m_spectrum_engine;
std::unique_ptr<SpectrumAnalyzerWidget> m_spectrum_widget;
std::unique_ptr<NetworkAnalyzerWidget> m_na_widget;
std::unique_ptr<NodeGraphWidget> m_graph_widget;

std::vector<std::unique_ptr<SignalGeneratorWidget>> m_generator_widgets;
Expand All @@ -136,8 +170,16 @@ class RfSimulatorApp {
// Declared after m_components so the manager (and its widget references to
// engines) is destroyed before the engines themselves.
PFBViewManager m_pfb_views;
// Adapter implementing the engine's injected lookups; declared after
// m_components (which it references) and before m_na_engine (which holds a
// reference to it — the adapter must outlive the engine).
NaHost m_na_host{m_components};
// Singleton Network Analyzer instrument engine — a plain value member
// exactly like m_spectrum_engine (not an IComponentEngine, no registry
// row, no graph node).
NetworkAnalyzerEngine m_na_engine{m_graph_engine, m_na_host};
// Owns .rfsim save/load/new; declared after m_graph_widget and m_pfb_views
// so it is destroyed before them (it holds references to both).
// so it is destroyed before both (it holds references to them).
std::unique_ptr<ProjectSerializer> m_serializer;
int m_next_component_id = 100;
PendingAction m_pending_action = PendingAction::None;
Expand Down
5 changes: 4 additions & 1 deletion app/include/project_serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string>

class ComponentRegistry;
class NetworkAnalyzerEngine;
class NodeGraphEngine;
class NodeGraphWidget;
class PFBViewManager;
Expand All @@ -15,7 +16,8 @@ class ProjectSerializer {
ProjectSerializer(ComponentRegistry &components, NodeGraphEngine &graph,
NodeGraphWidget &graph_widget, PFBViewManager &pfb_views, SessionState &state,
int &next_component_id, bool &show_log, bool &show_spectrum,
bool &show_properties, bool &show_node_editor);
bool &show_properties, bool &show_node_editor,
NetworkAnalyzerEngine &na_engine);

void save(const std::string &path);
bool load(const std::string &path); // false on parse/unknown-type failure (logged)
Expand All @@ -32,4 +34,5 @@ class ProjectSerializer {
bool &m_show_spectrum;
bool &m_show_properties;
bool &m_show_node_editor;
NetworkAnalyzerEngine &m_na_engine;
};
41 changes: 40 additions & 1 deletion app/src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ RfSimulatorApp::RfSimulatorApp() : m_components(m_graph_engine, m_view_manager)
m_graph_widget = std::make_unique<NodeGraphWidget>(m_graph_engine);
m_serializer = std::make_unique<ProjectSerializer>(
m_components, m_graph_engine, *m_graph_widget, m_pfb_views, m_state, m_next_component_id,
m_show_log, m_show_spectrum, m_show_properties, m_show_node_editor);
m_show_log, m_show_spectrum, m_show_properties, m_show_node_editor, m_na_engine);
std::vector<NodeGraphWidget::AddableComponent> addable;
for (const auto *desc : ComponentTypeRegistry::instance().all()) {
addable.push_back(
Expand Down Expand Up @@ -137,6 +137,11 @@ RfSimulatorApp::RfSimulatorApp() : m_components(m_graph_engine, m_view_manager)
};

m_spectrum_widget = std::make_unique<SpectrumAnalyzerWidget>(m_spectrum_engine, m_view_manager);
m_na_widget = std::make_unique<NetworkAnalyzerWidget>(m_na_engine, m_graph_engine);
// Sweep-param/Point A/B edits in the Network Analyzer panel are project
// state (persisted by ProjectSerializer) — mark the project dirty exactly
// like InspectorPanel::onParamChange does for component params.
m_na_widget->onParamChange = [this]() { markDirty(); };

// Ensure all engine nodes are registered with the widget's imnodes context
// so saveProject() can read node positions (GetNodeEditorSpacePos) without
Expand All @@ -151,6 +156,32 @@ RfSimulatorApp::RfSimulatorApp() : m_components(m_graph_engine, m_view_manager)
m_show_tutorial_first_run_prompt = !m_tutorial_state.completed();
}

// --- Network Analyzer host adapter -----------------------------------------
// RfSimulatorApp implements the engine's injected lookups (see app.h and
// network_analyzer_engine.h's layering comment): componentForNode wraps
// ComponentRegistry::find; beginScratchPass hands out one throwaway scratch
// graph+registry per measurement pass, destroyed (RAII) at pass end so the
// real graph/registry are never touched by the clone-chain measurement.

RfSimulatorApp::NaHost::NaHost(ComponentRegistry &components) : m_components(components) {}

IComponentEngine *RfSimulatorApp::NaHost::componentForNode(int graph_node_id) const {
return m_components.find(graph_node_id);
}

std::unique_ptr<INetworkAnalyzerScratch> RfSimulatorApp::NaHost::beginScratchPass() const {
return std::make_unique<RfSimulatorApp::NaScratch>();
}

RfSimulatorApp::NaScratch::NaScratch() : m_registry(m_graph, m_view) {}

IComponentEngine *RfSimulatorApp::NaScratch::createClone(std::string_view type, int id) {
const auto *desc = ComponentTypeRegistry::instance().find(type);
if (!desc)
return nullptr;
return desc->create(m_registry, m_graph, id);
}

void RfSimulatorApp::addComponent(const ComponentTypeDescriptor *desc, ImVec2 pos) {
IComponentEngine *comp = desc->create(m_components, m_graph_engine, m_next_component_id++);
ImNodes::EditorContextSet(m_graph_widget->context());
Expand All @@ -164,6 +195,7 @@ void RfSimulatorApp::addComponent(const ComponentTypeDescriptor *desc, ImVec2 po
void RfSimulatorApp::load_window_states() {
m_show_log = m_state.loadBool("WindowState", "Log", true);
m_show_spectrum = m_state.loadBool("WindowState", "SpectrumAnalyzer", true);
m_show_na = m_state.loadBool("WindowState", "NetworkAnalyzer", false);
m_show_properties = m_state.loadBool("WindowState", "Properties", true);
m_show_node_editor = m_state.loadBool("WindowState", "NodeEditor", true);
m_show_help = m_state.loadBool("WindowState", "Help", false);
Expand Down Expand Up @@ -689,6 +721,7 @@ void RfSimulatorApp::draw_ui() {
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Log", nullptr, &m_show_log);
ImGui::MenuItem("Spectrum Analyzer", nullptr, &m_show_spectrum);
ImGui::MenuItem("Network Analyzer", nullptr, &m_show_na);
ImGui::MenuItem("Properties", nullptr, &m_show_properties);
ImGui::MenuItem("Node Editor", nullptr, &m_show_node_editor);
ImGui::MenuItem("Component Library", nullptr, &m_show_library);
Expand Down Expand Up @@ -967,6 +1000,11 @@ void RfSimulatorApp::draw_ui() {
if (m_show_spectrum)
m_spectrum_widget->draw("Spectrum Analyzer", &m_show_spectrum);

if (m_show_na) {
m_na_engine.update();
m_na_widget->draw("Network Analyzer", &m_show_na);
}

m_pfb_views.draw();

for (size_t i = 0; i < m_generator_widgets.size(); ++i) {
Expand Down Expand Up @@ -998,6 +1036,7 @@ void RfSimulatorApp::draw_ui() {
RfSimulatorApp::~RfSimulatorApp() {
m_state.saveBool("WindowState", "Log", m_show_log);
m_state.saveBool("WindowState", "SpectrumAnalyzer", m_show_spectrum);
m_state.saveBool("WindowState", "NetworkAnalyzer", m_show_na);
m_state.saveBool("WindowState", "Properties", m_show_properties);
m_pfb_views.saveVisibility(m_components, m_state);
m_state.saveBool("WindowState", "NodeEditor", m_show_node_editor);
Expand Down
78 changes: 76 additions & 2 deletions app/src/project_serializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "imgui.h"
#include "imnodes.h"
#include "logging_core.h"
#include "network_analyzer_engine.h"
#include "node_graph_engine.h"
#include "node_graph_widget.h"
#include "pfb_channelizer_engine.h"
Expand Down Expand Up @@ -128,11 +129,11 @@ ProjectSerializer::ProjectSerializer(ComponentRegistry &components, NodeGraphEng
NodeGraphWidget &graph_widget, PFBViewManager &pfb_views,
SessionState &state, int &next_component_id, bool &show_log,
bool &show_spectrum, bool &show_properties,
bool &show_node_editor)
bool &show_node_editor, NetworkAnalyzerEngine &na_engine)
: m_components(components), m_graph(graph), m_graph_widget(graph_widget),
m_pfb_views(pfb_views), m_state(state), m_next_component_id(next_component_id),
m_show_log(show_log), m_show_spectrum(show_spectrum), m_show_properties(show_properties),
m_show_node_editor(show_node_editor) {}
m_show_node_editor(show_node_editor), m_na_engine(na_engine) {}

void ProjectSerializer::save(const std::string &path) {
nlohmann::json root;
Expand Down Expand Up @@ -228,6 +229,29 @@ void ProjectSerializer::save(const std::string &path) {
}
root["probe_pins"] = probes_arr;

// Save the singleton Network Analyzer instrument state: the four sweep
// params plus Point A/B as {comp, port, is_output} pairs, using the same
// pin_map machinery as probe_pins (the engine-level serialize() stores raw
// pin ids, which are not portable across graph rebuilds on load).
nlohmann::json na_json;
na_json["start_freq_hz"] = m_na_engine.startFrequency();
na_json["stop_freq_hz"] = m_na_engine.stopFrequency();
na_json["points"] = m_na_engine.points();
na_json["stimulus_power_dBm"] = m_na_engine.stimulusPower();
const auto pin_as_comp_port = [&](int pin_id) -> nlohmann::json {
auto it = pin_map.find(pin_id);
if (it == pin_map.end())
return nullptr;
nlohmann::json pj;
pj["comp"] = it->second.comp;
pj["port"] = it->second.port;
pj["is_output"] = it->second.is_output;
return pj;
};
na_json["point_a"] = pin_as_comp_port(m_na_engine.pointAPin());
na_json["point_b"] = pin_as_comp_port(m_na_engine.pointBPin());
root["network_analyzer"] = na_json;

// Save groups
nlohmann::json groups_arr = nlohmann::json::array();
// Build node_id \u2192 comp_index map
Expand Down Expand Up @@ -408,6 +432,46 @@ bool ProjectSerializer::load(const std::string &path) {
m_graph.addProbePin(pin);
}

// Restore the singleton Network Analyzer instrument state: the four
// sweep params plus Point A/B {comp, port, is_output} pairs. Points
// resolve through new_node_ids (like the links pass) so a skipped
// component elsewhere in the file cannot shift the index mapping.
// Absent keys keep the engine's current (default or last-set) value.
auto &saved_na = root["network_analyzer"];
if (!saved_na.is_null()) {
m_na_engine.setStartFrequency(
saved_na.value("start_freq_hz", m_na_engine.startFrequency()));
m_na_engine.setStopFrequency(
saved_na.value("stop_freq_hz", m_na_engine.stopFrequency()));
m_na_engine.setPoints(saved_na.value("points", m_na_engine.points()));
m_na_engine.setStimulusPower(
saved_na.value("stimulus_power_dBm", m_na_engine.stimulusPower()));
const auto restore_point = [&](const nlohmann::json &pj,
void (NetworkAnalyzerEngine::*set)(int)) {
if (!pj.is_object())
return; // unset point (saved as JSON null)
int comp_idx = pj.value("comp", -1);
int port = pj.value("port", 0);
bool is_output = pj.value("is_output", true);
if (comp_idx < 0 || static_cast<size_t>(comp_idx) >= new_node_ids.size())
return;
const int node_id = new_node_ids[static_cast<size_t>(comp_idx)];
if (node_id < 0)
return;
auto *comp = m_components.find(node_id);
if (!comp)
return;
const int pin = is_output ? comp->outputPinId(port) : comp->inputPinId(port);
if (pin >= 0)
(m_na_engine.*set)(pin);
};
const nlohmann::json no_pin = nullptr;
restore_point(saved_na.contains("point_a") ? saved_na["point_a"] : no_pin,
&NetworkAnalyzerEngine::setPointA);
restore_point(saved_na.contains("point_b") ? saved_na["point_b"] : no_pin,
&NetworkAnalyzerEngine::setPointB);
}

// Restore groups
auto &saved_groups = root["groups"];
for (const auto &gj : saved_groups) {
Expand Down Expand Up @@ -466,6 +530,16 @@ void ProjectSerializer::reset() {
// Clear probes
m_graph.clearProbes();

// Clear the Network Analyzer's probe points too — otherwise a stale pin
// id survives into the next project, and since pin ids are reallocated
// deterministically from the same base below, it can silently alias an
// unrelated pin belonging to a different component (issue found in
// review: load()'s restore only *sets* Point A/B when present in the
// save file, so an absent/unset point left the previous project's pin
// id in place).
m_na_engine.setPointA(-1);
m_na_engine.setPointB(-1);

// Reset IQ / PFB widgets
m_pfb_views.clear();

Expand Down
Loading