Skip to content

How to Add a New Element

Jgocunha edited this page Jun 4, 2026 · 4 revisions

This guide covers every step required to add a new element to dnf-composer. Complete the steps in order and keep the element's 1D/2D counterpart as a reference.


Step 1 — Create the header

File: include/elements/your_element.h

Declare the parameter struct and the class. Every element follows this pattern exactly:

#pragma once

#include <cmath>
#include <sstream>
#include <iomanip>

#include "element.h"

namespace dnf_composer::element
{
    /**
     * @brief Parameters for YourElement.
     *
     * Describe what the parameters control and their units/ranges.
     */
    struct YourElementParameters final : ElementSpecificParameters
    {
        double param1;
        bool   flag1;

        explicit YourElementParameters(double param1 = 1.0, bool flag1 = true)
            : param1(param1), flag1(flag1)
        {}

        bool operator==(const YourElementParameters& other) const
        {
            constexpr double epsilon = 1e-6;
            return std::abs(param1 - other.param1) < epsilon
                && flag1 == other.flag1;
        }

        [[nodiscard]] std::string toString() const override
        {
            std::ostringstream result;
            result << std::fixed << std::setprecision(2);
            result << "Parameters: [Param1: " << param1
                   << ", Flag1: " << (flag1 ? "true" : "false") << "]";
            return result.str();
        }
    };

    /**
     * @brief One-line description of what this element does in DFT.
     *
     * Longer description: the DFT role, typical connections, and any
     * implementation notes (e.g., separable kernel decomposition).
     *
     * @param elementCommonParameters  Common parameters (id, dimensions).
     * @param parameters               Element-specific parameters.
     */
    class YourElement final : public Element
    {
    private:
        YourElementParameters parameters;
        // declare any private working arrays here
    public:
        YourElement(const ElementCommonParameters& elementCommonParameters,
                    const YourElementParameters& parameters);

        void init() override;
        void step(double t, double deltaT) override;
        [[nodiscard]] std::string toString() const override;
        [[nodiscard]] std::shared_ptr<Element> clone() const override;

        void setParameters(const YourElementParameters& parameters);
        [[nodiscard]] YourElementParameters getParameters() const;
    };
}

Step 2 — Implement the source

File: src/elements/your_element.cpp

Required method contracts:

Method Contract
constructor Call Element(commonParams, specificParams). Assign parameters.
init() Resize and zero-initialise every component vector.
step(t, deltaT) Pull inputs with getInput("output"), compute, write to the "output" component.
toString() Return a human-readable summary of the element and its current parameters.
clone() return std::make_shared<YourElement>(*this);
setParameters() Assign the new parameters, then call init() to re-initialise working buffers.
getParameters() Return the current parameters copy.

Typical init() body — resize every component used in step():

void YourElement::init()
{
    // "output" is the standard component name kernels and fields pull from.
    components["output"].resize(commonParameters.dimensionParameters.size, 0.0);
    // add more components if needed, e.g.:
    // components["kernel"].resize(...);
    Element::init();
}

Step 3 — Register the label

File: include/element_parameters/element_parameters.h

3a. Add to the ElementLabel enum before the closing brace:

enum ElementLabel : int
{
    // ... existing labels ...
    NORMAL_NOISE_2D,
    YOUR_ELEMENT_NAME,   // <-- add here
};

3b. Add to the ElementLabelToString map:

inline const std::map<ElementLabel, std::string> ElementLabelToString = {
    // ... existing entries ...
    { YOUR_ELEMENT_NAME, "your element name" },
};

Step 4 — Register in the factory

File: include/elements/element_factory.h

Add the include near the end of the existing list:

#include "elements/your_element.h"

File: src/elements/element_factory.cpp

4a. In ElementFactory::setupElementCreators(), add:

elementCreators[ElementLabel::YOUR_ELEMENT_NAME] =
    [](const ElementCommonParameters& cp, const ElementSpecificParameters& sp)
    {
        const auto params = dynamic_cast<const YourElementParameters*>(&sp);
        return std::make_shared<YourElement>(cp, *params);
    };

4b. In ElementFactory::createElement(ElementLabel type) switch, add before case ElementLabel::UNINITIALIZED:

case ElementLabel::YOUR_ELEMENT_NAME:
    return creator->second(ElementCommonParameters(type), YourElementParameters());

Step 5 — Add to ElementWindow (parameter editing UI)

File: include/user_interface/element_window.h

Add the include at the top:

#include "elements/your_element.h"

Add a private static method declaration:

static void modifyElementYourElement(const std::shared_ptr<element::Element>& element);

File: src/user_interface/element_window.cpp

5a. Panel height — add a case to the PanelHeightFor lambda (use h(N) where N is the number of parameter rows):

case element::ElementLabel::YOUR_ELEMENT_NAME: return h(3);

5b. Dispatch — add a case to switchElementToModify():

case element::ElementLabel::YOUR_ELEMENT_NAME:
    modifyElementYourElement(element);
    break;

5c. Implement the modify method following the modifyElementGaussKernel2D pattern:

void ElementWindow::modifyElementYourElement(const std::shared_ptr<element::Element>& element)
{
    const float ui = ImGui::GetIO().FontGlobalScale;
    const auto elem = std::dynamic_pointer_cast<element::YourElement>(element);
    element::YourElementParameters p = elem->getParameters();

    auto param1 = static_cast<float>(p.param1);
    bool flag1  = p.flag1;

    std::string label = "##" + element->getUniqueName() + "Param1";
    ImGui::SetNextItemWidth(150.0f * ui);
    ImGui::DragFloat(label.c_str(), &param1, 0.1f, 0.0f, 30.0f);
    ImGui::SameLine(); ImGui::Text("Param1");

    label = "##" + element->getUniqueName() + "Flag1";
    ImGui::Checkbox(label.c_str(), &flag1);
    ImGui::SameLine(); ImGui::Text("Flag1");

    static constexpr double epsilon = 1e-6;
    if (std::abs(param1 - static_cast<float>(p.param1)) > epsilon || flag1 != p.flag1)
    {
        p.param1 = param1;
        p.flag1  = flag1;
        elem->setParameters(p);
    }
}

5d. Color — add a case to getColorForElementType():

case element::ElementLabel::YOUR_ELEMENT_NAME:
    return ImVec4(0.5f, 0.6f, 0.7f, 1.0f);

5e. Display name — add a case to getElementTypeDisplayName():

case element::ElementLabel::YOUR_ELEMENT_NAME:
    return "Your Element Display Name";

Step 6 — Add to SimulationWindow (element creation UI)

File: src/user_interface/simulation_window.cpp

Add a case in renderAddElementCard(). Follow the GAUSS_KERNEL_2D block as a template. Use static local variables so the widget state persists between frames:

case element::ElementLabel::YOUR_ELEMENT_NAME:
{
    static char   id[CHAR_SIZE] = "your element";
    static int    x_max = 100;
    static double d_x   = 1.0;
    static double param1 = 1.0;
    static bool   flag1  = true;

    ImGui::InputTextWithHint("ID", "enter text here", id, IM_ARRAYSIZE(id));
    ImGui::PushItemWidth(80.0f * ImGui::GetIO().FontGlobalScale);
    ImGui::InputInt("Size",    &x_max,  0, 0);
    ImGui::InputDouble("Step", &d_x,    0.0, 0.0, "%.2f");
    ImGui::InputDouble("Param1", &param1, 0.0, 0.0, "%.2f");
    ImGui::Checkbox("Flag1", &flag1);
    ImGui::PopItemWidth();

    if (addRequested)
    {
        const element::YourElementParameters p(param1, flag1);
        const element::ElementCommonParameters common{ std::string(id),
            element::ElementDimensions{ x_max, d_x } };
        simulation->addElement(std::make_shared<element::YourElement>(common, p));
    }
    break;
}

For 2D elements use ElementDimensions{ x_max, y_max, d_x, d_y } and add matching y_max/d_y fields.


Step 7 — Add to NodeGraphWindow

File: src/user_interface/node_graph_window.cpp

7a. Column assignment — add a case to getColumnForElement(). The column determines the initial horizontal position in the node graph:

Column Element types
0 Sources (stimuli, noise)
1 Kernels
2 Couplings
3 Fields
// Example: kernel goes in column 1
case element::ElementLabel::YOUR_ELEMENT_NAME:
    return 1;

7b. 2D plot size — if the element output is a 2D matrix, add its label to both is2DField checks in renderElementNode() and renderElementNodeConnections():

const bool is2DField = (label == element::ElementLabel::NEURAL_FIELD_2D ||
                        label == element::ElementLabel::GAUSS_STIMULUS_2D ||
                        label == element::ElementLabel::GAUSS_KERNEL_2D  ||
                        label == element::ElementLabel::MEXICAN_HAT_KERNEL_2D ||
                        label == element::ElementLabel::NORMAL_NOISE_2D  ||
                        label == element::ElementLabel::YOUR_ELEMENT_NAME);  // <-- add

7c. Inspector panel — add a case to renderNodeInspectorContent() to show the parameters when a node is double-clicked:

case element::ElementLabel::YOUR_ELEMENT_NAME:
{
    const auto e = std::dynamic_pointer_cast<element::YourElement>(element);
    const auto& p = e->getParameters();
    ImGui::Text("Param1: %.2f", p.param1);
    ImGui::Text("Flag1: %s",    p.flag1 ? "true" : "false");
    break;
}

Step 8 — Add JSON serialization (SimulationFileManager)

File: src/simulation/simulation_file_manager.cpp Header (include): include/simulation/simulation_file_manager.h

This step is mandatory. Without it, saving a simulation that contains your element writes it with no element-specific parameters, and loading it skips the element entirely — which silently breaks (and can crash) any architecture that uses it. There is no compile error to catch this; only the round-trip test (Step 10) does.

8a. Add your element header to the include list at the top of simulation_file_manager.h.

8b. In elementToJson() (the save switch), add a case before default: that writes every element-specific parameter:

case element::YOUR_ELEMENT_NAME:
{
    const auto e = std::dynamic_pointer_cast<element::YourElement>(element);
    const auto p = e->getParameters();
    elementJson["param1"] = p.param1;
    elementJson["flag1"]  = p.flag1;
    // Store enums as their int value: static_cast<int>(p.someEnum)
}
break;

8c. In jsonToElements() (the load switch), add a matching case before default: that reconstructs the element and calls simulation->addElement(...):

case element::YOUR_ELEMENT_NAME:
{
    const double param1 = elementJson["param1"];
    const bool   flag1  = elementJson["flag1"];
    auto e = std::make_shared<element::YourElement>(
        element::ElementCommonParameters(uniqueName, element::ElementDimensions(x_max, d_x)),
        element::YourElementParameters(param1, flag1));
    simulation->addElement(e);
}
break;

Dimension-bridging elements (input dims differ from output dims): the common x_max/d_x/y_max/d_y fields only describe the element's own (output) dimensions. You must also save and load the element's input dimensions (e.g. input_x_max, input_d_x, and input_y_max/input_d_y if the input is 2D) and rebuild the ElementDimensions for the parameter struct on load. This is the part most likely to be missed — see RESIZE / COLLAPSE / EXPAND for reference.


Step 9 — Update CMakeLists.txt

File: dynamic-neural-field-composer/CMakeLists.txt

Add the new source file to the library target's source list (find the block with the other element .cpp files):

src/elements/your_element.cpp

Step 10 — Update the public header

File: include/dynamic-neural-field-composer.h

Add the include near the other element headers:

#include "elements/your_element.h"

Step 11 — Write tests

File: tests/elements/test_your_element.cpp

Register in tests/CMakeLists.txt by adding the file to the test executable sources.

Every element test suite must cover:

Test group What to verify
Construction Valid parameters do not throw; label matches
Initialisation Component sizes equal the declared dimensions after init()
Step output Numerical correctness: after one step with a known input, output values match a hand-computed reference
Parameter update setParameters() followed by step() reflects the new values
Edge cases Zero amplitude → all-zero output; circular vs. non-circular boundary behaviour
Clone Cloned element produces identical output after the same step sequence
Serialization round-trip In tests/simulation/test_simulation_file_manager.cpp: add a RoundTripPreserves<YourElement>Parameters test (construct with non-default params — and non-default input dimensions for dimension-bridging elements — save, load into a fresh sim, assert getParameters() == ...), and add the element to the all-element-types aggregate test. This is the only check that catches a missing Step 8 case.

Minimal structure:

#include <gtest/gtest.h>
#include <memory>
#include "elements/your_element.h"

using namespace dnf_composer;
using namespace dnf_composer::element;

static ElementCommonParameters makeCP(const std::string& name, int x = 20)
{
    return ElementCommonParameters{ name, ElementDimensions(x, 1.0) };
}

TEST(YourElementConstruction, ValidDoesNotThrow)
{
    EXPECT_NO_THROW(YourElement(makeCP("ye"), YourElementParameters{}));
}

TEST(YourElementConstruction, LabelIsCorrect)
{
    YourElement ye(makeCP("ye"), YourElementParameters{});
    EXPECT_EQ(ye.getLabel(), ElementLabel::YOUR_ELEMENT_NAME);
}

TEST(YourElementStep, OutputSizeMatchesDimension)
{
    auto ye = std::make_shared<YourElement>(makeCP("ye", 20), YourElementParameters{});
    ye->init();
    ye->step(0.0, 1.0);
    EXPECT_EQ(static_cast<int>(ye->getComponent("output").size()), 20);
}

TEST(YourElementClone, CloneHasSameParameters)
{
    YourElement ye(makeCP("ye"), YourElementParameters{1.5, false});
    ye->init();
    const auto cloned = std::dynamic_pointer_cast<YourElement>(ye.clone());
    ASSERT_NE(cloned, nullptr);
    EXPECT_EQ(cloned->getParameters(), ye.getParameters());
}

Step 12 — Write an example executable

File: examples/ex_your_element.cpp

Register in examples/CMakeLists.txt.

#include "visualization/visualization.h"
#include "application/application.h"
#include "user_interface/static_layout_window.h"
#include "user_interface/main_menu_bar.h"

int main()
{
    try
    {
        using namespace dnf_composer;

        const auto simulation    = std::make_shared<Simulation>("ex your element", 10.0, 0.0, 0.0);
        const auto visualization = std::make_shared<Visualization>(simulation);
        const Application app{ simulation, visualization };

        app.addWindow<user_interface::MainMenuBar>();
        app.addWindow<user_interface::StaticLayoutWindow>(simulation, visualization);

        // --- build the architecture ---
        const element::ElementCommonParameters cp{ "your element",
            element::ElementDimensions(100, 1.0) };
        const auto ye = std::make_shared<element::YourElement>(cp, element::YourElementParameters{});

        simulation->addElement(ye);
        visualization->plot({ {ye->getUniqueName(), "output"} });

        app.init();
        while (!app.hasGUIBeenClosed())
            app.step();
        app.close();
    }
    catch (const dnf_composer::Exception& ex)
    {
        log(dnf_composer::tools::logger::LogLevel::FATAL,
            "Exception: " + std::string(ex.what()),
            dnf_composer::tools::logger::LogOutputMode::CONSOLE);
        return static_cast<int>(ex.getErrorCode());
    }
    catch (const std::exception& ex)
    {
        log(dnf_composer::tools::logger::LogLevel::FATAL,
            "Exception caught: " + std::string(ex.what()),
            dnf_composer::tools::logger::LogOutputMode::CONSOLE);
        return 1;
    }
}

Step 13 — Update README.md

Add the element to the elements table in the project README with:

  • Name
  • Dimensionality (1D / 2D / 1D+2D)
  • One-line description

Quick checklist

Copy this checklist into the PR description for each new element:

- [ ] include/elements/your_element.h
- [ ] src/elements/your_element.cpp
- [ ] Doxygen comments on the struct and class
- [ ] ElementLabel enum + ElementLabelToString entry
- [ ] ElementFactory: include, setupElementCreators, createElement switch
- [ ] ElementWindow: include, declare method, PanelHeightFor, switchElementToModify, modifyElement method, color, display name
- [ ] SimulationWindow: renderAddElementCard case
- [ ] NodeGraphWindow: getColumnForElement, is2DField (if 2D), renderNodeInspectorContent
- [ ] SimulationFileManager: elementToJson + jsonToElements cases (incl. input dimensions for dimension-bridging elements)
- [ ] CMakeLists.txt: source file added
- [ ] include/dynamic-neural-field-composer.h: include added
- [ ] tests/elements/test_your_element.cpp + registered in tests/CMakeLists.txt
- [ ] test_simulation_file_manager.cpp: round-trip test + added to all-element-types aggregate
- [ ] examples/your_element.cpp + registered in examples/CMakeLists.txt
- [ ] main README.md Elements section
- [ ] wiki folder update all pages related with element suite (Element-Reference.md, Elements.md, Examples.md)

Clone this wiki locally