Embedding with your Project
Overview
This guide shows how to integrate FluxGraph into your C++ application for real-time simulation.
Prerequisites
- C++20 compiler with
std::format- MSVC 19.32+ / Visual Studio 2022 17.2+ (Windows)
- GCC 13+ (Linux)
- Clang 17+ with libc++ 17+ or libstdc++ 13+ (macOS/Linux)
- CMake 3.20 or later; 4.2 or later for the
Visual Studio 18 2026generator (see Platform-Specific Notes)
FluxGraph's own CMakeLists.txt builds the library as C++20, so with add_subdirectory or FetchContent the library target is C++20 whatever your project sets. Your compiler still has to support C++20 std::format.
Method 1: CMake add_subdirectory (Recommended)
Step 1: Add FluxGraph to Your Project
your_project/
├── CMakeLists.txt
├── external/
│ └── fluxgraph/ <- Clone or submodule here
└── src/
└── main.cppClone FluxGraph:
cd your_project/external
git clone https://github.com/anolishq/fluxgraph.gitOr add as submodule:
git submodule add https://github.com/anolishq/fluxgraph.git external/fluxgraphStep 2: Update CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# v1.0.6 and earlier build their tests and examples by default, even when
# embedded, and the tests need GoogleTest
set(FLUXGRAPH_BUILD_TESTS OFF CACHE BOOL "")
set(FLUXGRAPH_BUILD_EXAMPLES OFF CACHE BOOL "")
# Add FluxGraph library
add_subdirectory(external/fluxgraph)
# Your executable
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Step 3: Use in Code
#include "fluxgraph/engine.hpp"
#include "fluxgraph/core/signal_store.hpp"
#include "fluxgraph/core/namespace.hpp"
#include "fluxgraph/graph/compiler.hpp"
int main() {
fluxgraph::Engine engine;
// ... your code ...
}Step 4: Build
mkdir build
cd build
cmake ..
cmake --build . --config ReleaseMethod 2: CMake FetchContent
Automatically download FluxGraph during configure:
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
# v1.0.6 and earlier build their tests and examples by default, even when
# embedded, and the tests need GoogleTest
set(FLUXGRAPH_BUILD_TESTS OFF CACHE BOOL "")
set(FLUXGRAPH_BUILD_EXAMPLES OFF CACHE BOOL "")
include(FetchContent)
FetchContent_Declare(
fluxgraph
GIT_REPOSITORY https://github.com/anolishq/fluxgraph.git
GIT_TAG 05a1564688a4b3fffbfd5e66b68fd7f9d587a0c5 # v1.0.6
)
FetchContent_MakeAvailable(fluxgraph)
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Pros:
- No manual download needed
- Easy version pinning
- Clean workspace
Cons:
- Slower first configure (downloads every time)
- Requires internet connection
Method 3: Manual Integration (Advanced)
If you can't use CMake or need full control:
Step 1: Copy Files
your_project/
└── fluxgraph/
├── include/fluxgraph/ <- Copy all headers
└── src/ <- Copy all .cpp filesStep 2: Compile
Add to your build:
- Include path:
your_project/fluxgraph/include - Source files: the
FLUXGRAPH_SOURCESlist influxgraph/CMakeLists.txt, which is every.cppfile underfluxgraph/src/outsidesrc/loaders/ - For a loader, also compile
src/loaders/json_loader.cpporsrc/loaders/yaml_loader.cpp, defineFLUXGRAPH_JSON_ENABLEDorFLUXGRAPH_YAML_ENABLED, and provide nlohmann/json or yaml-cpp
Example Makefile
CXX = g++
CXXFLAGS = -std=c++20 -O3 -Ifluxgraph/include
# FLUXGRAPH_SOURCES: every .cpp under fluxgraph/src/ outside src/loaders/
FLUXGRAPH_SOURCES = $(wildcard fluxgraph/src/*.cpp fluxgraph/src/core/*.cpp \
fluxgraph/src/model/*.cpp fluxgraph/src/graph/*.cpp \
fluxgraph/src/graph/compiler/*.cpp)
SOURCES = src/main.cpp $(FLUXGRAPH_SOURCES)
your_app: $(SOURCES)
$(CXX) $(CXXFLAGS) -o your_app $(SOURCES)Optional Loaders (JSON and YAML)
FluxGraph core has zero dependencies. Optional graph loaders can be enabled at CMake configure time.
Configuration 1: Core Only (No Loaders)
Zero external dependencies.
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
add_subdirectory(external/fluxgraph)
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Build graphs programmatically using GraphSpec:
fluxgraph::GraphSpec spec;
fluxgraph::EdgeSpec edge;
edge.source_path = "input.x";
edge.target_path = "output.y";
edge.transform.type = "linear";
edge.transform.params["scale"] = 2.0;
spec.edges.push_back(edge);Configuration 2: With JSON Loader
Adds nlohmann/json (header-only).
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
# Enable JSON loader
set(FLUXGRAPH_JSON_ENABLED ON CACHE BOOL "Enable JSON loader")
add_subdirectory(external/fluxgraph)
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Load graphs from JSON files:
#include "fluxgraph/loaders/json_loader.hpp"
auto spec = fluxgraph::loaders::load_json_file("graph.json");
fluxgraph::GraphCompiler compiler;
auto program = compiler.compile(spec, signal_ns, func_ns);
engine.load(std::move(program));Configuration 3: With YAML Loader
Adds yaml-cpp.
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
# Enable YAML loader
set(FLUXGRAPH_YAML_ENABLED ON CACHE BOOL "Enable YAML loader")
add_subdirectory(external/fluxgraph)
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Load graphs from YAML files:
#include "fluxgraph/loaders/yaml_loader.hpp"
auto spec = fluxgraph::loaders::load_yaml_file("graph.yaml");
fluxgraph::GraphCompiler compiler;
auto program = compiler.compile(spec, signal_ns, func_ns);
engine.load(std::move(program));Configuration 4: Both Loaders
Supports both JSON and YAML.
cmake_minimum_required(VERSION 3.20)
project(your_project)
set(CMAKE_CXX_STANDARD 20)
# Enable both loaders
set(FLUXGRAPH_JSON_ENABLED ON CACHE BOOL "Enable JSON loader")
set(FLUXGRAPH_YAML_ENABLED ON CACHE BOOL "Enable YAML loader")
add_subdirectory(external/fluxgraph)
add_executable(your_app src/main.cpp)
target_link_libraries(your_app PRIVATE fluxgraph)Use either loader as needed:
#include "fluxgraph/loaders/json_loader.hpp"
#include "fluxgraph/loaders/yaml_loader.hpp"
// Load JSON
auto spec1 = fluxgraph::loaders::load_json_file("graph.json");
// Or load YAML
auto spec2 = fluxgraph::loaders::load_yaml_file("graph.yaml");Dependency Details
| Option | Dependency | Linkage | Size Impact |
|---|---|---|---|
FLUXGRAPH_JSON_ENABLED | nlohmann/json 3.12.0 | Header-only, PUBLIC | ~5KB compiled |
FLUXGRAPH_YAML_ENABLED | yaml-cpp 0.9.0 | Static lib, PUBLIC | ~150KB compiled |
The versions are the ones the vcpkg baseline in vcpkg-configuration.json pins, which CI builds against.
Important: Both loaders link their dependency PUBLIC, so the fluxgraph target carries it onto your link line and you do not link it yourself. Your project does have to make the package findable by find_package(... CONFIG), for example through vcpkg or CMAKE_PREFIX_PATH:
- With
add_subdirectoryorFetchContent, FluxGraph's ownfind_package(nlohmann_json CONFIG REQUIRED)andfind_package(yaml-cpp CONFIG REQUIRED)run in your configure, which fails if the package is not found. - With an installed FluxGraph and
find_package(fluxgraph), the package config callsfind_dependencyfor each loader it was built with.
See Also:
- JSON_SCHEMA.md - JSON graph format reference
- YAML_SCHEMA.md - YAML graph format reference
- examples/03_json_graph/ - JSON example
- examples/04_yaml_graph/ - YAML example
Minimal Example
main.cpp:
#include "fluxgraph/core/signal_store.hpp"
#include "fluxgraph/core/namespace.hpp"
#include "fluxgraph/graph/compiler.hpp"
#include "fluxgraph/engine.hpp"
#include <iostream>
int main() {
using namespace fluxgraph;
// 1. Setup
SignalNamespace ns;
FunctionNamespace fn;
SignalStore store;
Engine engine;
// 2. Build graph: input -> (scale=2, offset=1) -> output
GraphSpec spec;
EdgeSpec edge;
edge.source_path = "input";
edge.target_path = "output";
edge.transform.type = "linear";
edge.transform.params["scale"] = 2.0;
edge.transform.params["offset"] = 1.0;
spec.edges.push_back(edge);
// 3. Compile and load
GraphCompiler compiler;
auto program = compiler.compile(spec, ns, fn);
engine.load(std::move(program));
// 4. Execute simulation
auto input_id = ns.resolve("input");
auto output_id = ns.resolve("output");
for (int i = 0; i < 10; ++i) {
store.write(input_id, 10.0 + i, "");
engine.tick(0.1, store); // 100ms time step
double result = store.read_value(output_id);
std::cout << "Tick " << i << ": output = " << result << std::endl;
// Expected: 2*(10+i) + 1 = 21, 23, 25, ...
}
return 0;
}Output:
Tick 0: output = 21
Tick 1: output = 23
Tick 2: output = 25
...Realistic Example: Thermal Chamber
chamber_sim.cpp:
#include "fluxgraph/engine.hpp"
#include "fluxgraph/core/signal_store.hpp"
#include "fluxgraph/core/namespace.hpp"
#include "fluxgraph/graph/compiler.hpp"
#include <iostream>
#include <fstream>
int main() {
using namespace fluxgraph;
SignalNamespace ns;
FunctionNamespace fn;
SignalStore store;
Engine engine;
// Build graph
GraphSpec spec;
// Thermal mass model
ModelSpec model;
model.id = "chamber";
model.type = "thermal_mass";
model.params["temp_signal"] = std::string("chamber.temp");
model.params["power_signal"] = std::string("chamber.power");
model.params["ambient_signal"] = std::string("ambient.temp");
model.params["thermal_mass"] = 1000.0; // 1000 J/degC
model.params["heat_transfer_coeff"] = 10.0; // 10 W/degC
model.params["initial_temp"] = 25.0; // 25 degC
spec.models.push_back(model);
// Filter temperature for display
EdgeSpec edge;
edge.source_path = "chamber.temp";
edge.target_path = "chamber.temp_filtered";
edge.transform.type = "first_order_lag";
edge.transform.params["tau_s"] = 1.0; // 1 second filter
spec.edges.push_back(edge);
// Compile
GraphCompiler compiler;
auto program = compiler.compile(spec, ns, fn);
engine.load(std::move(program));
// Get signal IDs
auto power_id = ns.resolve("chamber.power");
auto ambient_id = ns.resolve("ambient.temp");
auto temp_id = ns.resolve("chamber.temp");
auto filtered_id = ns.resolve("chamber.temp_filtered");
// Initialize
store.write(ambient_id, 20.0, "degC");
// Open log file
std::ofstream log("chamber_log.csv");
log << "time,power,temp,temp_filtered" << std::endl;
// Simulation: 1000 seconds, 0.1s time step
double time = 0.0;
for (int tick = 0; tick < 10000; ++tick) {
// Apply power for first 500 seconds
double power = (time < 500.0) ? 500.0 : 0.0;
store.write(power_id, power, "W");
// Tick engine
engine.tick(0.1, store);
time += 0.1;
// Log every 10 ticks (1 second)
if (tick % 10 == 0) {
double temp = store.read_value(temp_id);
double temp_filt = store.read_value(filtered_id);
log << time << "," << power << "," << temp << "," << temp_filt << std::endl;
std::cout << "t=" << time << "s, T=" << temp << " degC" << std::endl;
}
}
log.close();
std::cout << "Simulation complete. See chamber_log.csv" << std::endl;
return 0;
}Build and run:
cmake --build build --config Release
./build/chamber_simResult: CSV file with temperature evolution over time.
Thread Safety Considerations
FluxGraph Threading Model
Single-writer: One thread calls engine.tick()
Multi-reader: Multiple threads can call store.read() concurrently
Namespace: Thread-safe after setup (intern during init, resolve during execution)
Safe Pattern
// Thread 1: Simulation
void simulation_thread() {
while (running) {
engine.tick(dt, store);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// Thread 2: Monitoring
void monitor_thread() {
while (running) {
double temp = store.read_value(temp_id); // Safe concurrent read
std::cout << "Temp: " << temp << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}Unsafe Pattern (Don't Do This)
// BAD: Multiple threads calling tick()
std::thread t1([&]() { engine.tick(dt, store); });
std::thread t2([&]() { engine.tick(dt, store); }); // RACE CONDITION!Performance Optimization
1. Build in Release Mode
cmake -DCMAKE_BUILD_TYPE=Release ..Impact: 10-100x faster than Debug. Always benchmark in Release.
2. Reserve Signal IDs
If you know signal count upfront:
store.reserve(1000); // Pre-allocate for 1000 signalsBenefit: Avoids reallocation during simulation.
3. Cache Signal IDs
// Bad: Repeated resolve
for (int i = 0; i < 1000; ++i) {
auto id = ns.resolve("chamber.temp"); // Hash lookup every loop!
double temp = store.read_value(id);
}
// Good: Cache ID
auto temp_id = ns.resolve("chamber.temp"); // Once
for (int i = 0; i < 1000; ++i) {
double temp = store.read_value(temp_id); // Fast array index
}4. Choose Appropriate dt
// Too small: Wasted computation
engine.tick(0.001, store); // 1000 Hz, likely overkill
// Too large: Instability
engine.tick(1.0, store); // May violate model stability limits
// Just right: Match physics needs
engine.tick(0.1, store); // 10 Hz, good for most thermal systemsUse model.compute_stability_limit() as guide.
5. Profile Before Optimizing
# Linux
perf record ./your_app
perf report
# Windows
# Use Visual Studio Profiler
# macOS
instruments -t "Time Profiler" ./your_appCommon hotspots:
- Model physics (exp, sqrt)
- Transform chains (if very long)
- Memory access (cache misses)
Rarely bottleneck:
- Virtual calls (~2ns)
- Namespace lookups (done at compile)
Custom Transform Registration
Step 1: Implement ITransform
my_transform.hpp:
#pragma once
#include "fluxgraph/transform/interface.hpp"
#include <memory>
class MyGainTransform : public fluxgraph::ITransform {
private:
double m_gain;
public:
explicit MyGainTransform(double gain) : m_gain(gain) {}
double apply(double input, double dt) override {
return input * m_gain;
}
void reset() override {
// Stateless, nothing to reset
}
std::unique_ptr<fluxgraph::ITransform> clone() const override {
return std::make_unique<MyGainTransform>(*this);
}
};Step 2: Register with Compiler
main.cpp:
#include "my_transform.hpp"
#include "fluxgraph/graph/compiler.hpp"
GraphCompiler compiler;
// Register factory function
compiler.register_transform("my_gain",
[](const fluxgraph::ParamMap& params) -> std::unique_ptr<fluxgraph::ITransform> {
double gain = std::get<double>(params.at("gain"));
return std::make_unique<MyGainTransform>(gain);
}
);
// Now can use in GraphSpec
EdgeSpec edge;
edge.transform.type = "my_gain";
edge.transform.params["gain"] = 5.0;Custom Model Registration
Similar to transforms:
my_model.hpp:
#pragma once
#include "fluxgraph/model/interface.hpp"
class MyMotorModel : public fluxgraph::IModel {
private:
fluxgraph::SignalId m_speed_id, m_torque_id;
double m_inertia;
double m_speed;
public:
MyMotorModel(/* params */) { /* ... */ }
void tick(double dt, fluxgraph::SignalStore& store) override {
double torque = store.read_value(m_torque_id);
double accel = torque / m_inertia;
m_speed += accel * dt;
store.write(m_speed_id, m_speed, "rpm");
}
void reset() override {
m_speed = 0.0;
}
double compute_stability_limit() const override {
return 0.1; // Conservative 100ms max
}
std::string describe() const override {
return "Motor model with inertia";
}
std::vector<fluxgraph::SignalId> output_signal_ids() const override {
return {m_speed_id};
}
};Register:
compiler.register_model("motor",
[](const fluxgraph::ParamMap& params,
fluxgraph::SignalNamespace& ns) -> std::unique_ptr<fluxgraph::IModel> {
// Parse params, return model instance
}
);Debugging Tips
1. Validate Graph
try {
auto program = compiler.compile(spec, ns, fn);
} catch (const std::exception& e) {
std::cerr << "Compile error: " << e.what() << std::endl;
// e.what() includes cycle details, missing params, etc.
}2. Check Signal Units
auto signal = store.read(temp_id);
std::cout << "Value: " << signal.value << " "
<< "Unit: " << signal.unit << " "
<< "Physics: " << signal.physics_driven << std::endl;3. Compare to Known Good
Run analytical tests:
cd fluxgraph/build
ctest -R analyticalIf test passes but your sim doesn't, compare parameters.
Common Pitfalls
1. Forgot to Call tick()
engine.load(program);
// ... forgot engine.tick(dt, store) ...
double temp = store.read_value(temp_id); // Still initial value!2. Wrong Time Step Units
// WRONG: dt in milliseconds, but params expect seconds
engine.tick(100, store); // Thought this was 100ms, actually 100s!
// RIGHT:
engine.tick(0.1, store); // 100ms = 0.1 seconds3. Reusing Program Across Engines
auto program = compiler.compile(spec, ns, fn);
engine1.load(std::move(program)); // OK
engine2.load(std::move(program)); // ERROR: program already moved!
// FIX: Compile twice or clone program4. Not Resetting After Re-Run
// First run
for (int i = 0; i < 100; ++i) engine.tick(0.1, store);
// Second run (but state still from first run!)
for (int i = 0; i < 100; ++i) engine.tick(0.1, store); // WRONG
// FIX:
engine.reset();
store.clear();
// Now ready for second runPlatform-Specific Notes
Windows (MSVC)
The Visual Studio 18 2026 generator needs Visual Studio 2026 and CMake 4.2 or later.
cmake -G "Visual Studio 18 2026" -A x64 ..
cmake --build . --config ReleaseLinux (GCC/Clang)
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)macOS (Xcode)
cmake -G Xcode ..
cmake --build . --config ReleaseFurther Reading
- API.md - Complete API reference
- ARCHITECTURE.md - Design rationale
- TRANSFORMS.md - Transform details
- examples/ - Full working examples
Support
Issues: https://github.com/anolishq/fluxgraph/issues
Questions: Post issue with "question" label
Contributing: See CONTRIBUTING.md
