Falcon Autotuner Language Tutorial¶
Introduction¶
This tutorial will guide you through creating your first autotuner from scratch using the current Falcon DSL. By the end you will have a working voltage-sweep autotuner backed by a real C++ hardware wrapper, running entirely through falcon-run — no CMake, no manual compilation.
Prerequisites¶
- Falcon DSL installed (
cd dsl && make install) - Basic understanding of state machines
Tutorial: Voltage Sweep Autotuner¶
We'll build an autotuner that sweeps a voltage range and measures current at each step.
Step 1: Define the State Machine¶
Create voltage_sweep.fal:
// voltage_sweep.fal
//
// Sweeps voltage from min_voltage to max_voltage in steps of 'step',
// measuring current at each point. Saves results and reports the count.
ffimport "voltage_sweep_impl.cpp" () ()
autotuner VoltageSweep (float min_voltage, float max_voltage, float step) -> (int measurement_count) {
float current_voltage = min_voltage;
measurement_count = 0;
start -> initialize;
state initialize {
bool init_success = initialize_device();
if (init_success == true) { -> measure_point; }
else { -> error_state; }
}
state measure_point {
float measured_current = measure_iv(current_voltage);
measurement_count = measurement_count + 1;
current_voltage = current_voltage + step;
if (current_voltage <= max_voltage) { -> measure_point; }
else { -> finalize; }
}
state finalize {
bool save_success = save_data(measurement_count);
if (save_success == true) { -> complete; }
else { -> error_state; }
}
state complete { terminal; }
state error_state { terminal; }
}
Key points:
- The
ffimportline at the top tells the engine to compilevoltage_sweep_impl.cppautomatically — no separate build step is needed. - The
useskeyword is not part of the language. Cross-module calls simply use theModule::symbolqualified form. - Inputs and outputs are declared directly in the autotuner signature.
- There are no
params {}ortemp {}blocks — variables are declared inline.
Step 2: Implement Measurements in C++¶
Create voltage_sweep_impl.cpp in the same directory as the .fal file:
// voltage_sweep_impl.cpp
//
// Falcon C ABI wrapper for the voltage sweep measurement routines.
// The engine compiles this file automatically via ffimport.
#include <falcon-typing/falcon_ffi.h>
#include <falcon-typing/FFIHelpers.hpp>
#include <iostream>
#include <fstream>
#include <vector>
// ── Mock hardware ─────────────────────────────────────────────────────────────
class MockDevice {
public:
bool initialize() { return true; }
void set_voltage(double v) { voltage_ = v; }
double measure_current() { return voltage_ * 1e-6; } // Ohm's law mock
private:
double voltage_ = 0.0;
};
static MockDevice g_device;
static std::vector<std::pair<double, double>> g_measurements;
// ── Falcon C ABI wrappers ─────────────────────────────────────────────────────
extern "C" void initialize_device(
const FalconParamEntry* /*in*/, int32_t /*in_count*/,
FalconResultSlot* out, int32_t* out_count)
{
std::cout << "Initializing device...\n";
g_measurements.clear();
bool ok = g_device.initialize();
falcon::typing::ffi::engine::set_result(out, out_count, 0, ok);
}
extern "C" void measure_iv(
const FalconParamEntry* in, int32_t in_count,
FalconResultSlot* out, int32_t* out_count)
{
auto params = falcon::typing::ffi::engine::unpack_params(in, in_count);
double voltage = std::get<double>(params.at("current_voltage"));
g_device.set_voltage(voltage);
double current = g_device.measure_current();
std::cout << " V=" << voltage << " I=" << current << "\n";
g_measurements.push_back({voltage, current});
falcon::typing::ffi::engine::set_result(out, out_count, 0, current);
}
extern "C" void save_data(
const FalconParamEntry* in, int32_t in_count,
FalconResultSlot* out, int32_t* out_count)
{
auto params = falcon::typing::ffi::engine::unpack_params(in, in_count);
int64_t count = std::get<int64_t>(params.at("measurement_count"));
std::cout << "Saving " << count << " measurement(s)...\n";
std::ofstream f("iv_curve.csv");
f << "Voltage (V),Current (A)\n";
for (auto& [v, i] : g_measurements)
f << v << "," << i << "\n";
f.close();
std::cout << "Saved to iv_curve.csv\n";
falcon::typing::ffi::engine::set_result(out, out_count, 0, true);
}
The engine discovers and compiles this file when voltage_sweep.fal is loaded. There is nothing else to build.
Step 3: Run¶
falcon-run VoltageSweep voltage_sweep.fal \
--param min_voltage=0.0 \
--param max_voltage=1.0 \
--param step=0.1
Expected output:
Initializing device...
V=0 I=0
V=0.1 I=1e-07
V=0.2 I=2e-07
V=0.3 I=3e-07
V=0.4 I=4e-07
V=0.5 I=5e-07
V=0.6 I=6e-07
V=0.7 I=7e-07
V=0.8 I=8e-07
V=0.9 I=9e-07
V=1 I=1e-06
Saving 11 measurement(s)...
Saved to iv_curve.csv
Autotuner 'VoltageSweep' completed.
Results (1):
[0] 11
Step 4: Add a Snapshot (Optional)¶
If you want to pre-seed parameters from a JSON snapshot instead of passing --param flags, create snapshot.json next to the .fal file:
{
"min_voltage": 0.0,
"max_voltage": 1.0,
"step": 0.1
}
Then run:
falcon-run VoltageSweep voltage_sweep.fal --snapshot snapshot.json
Step 5: Embed in C++¶
If you need to call the autotuner from a C++ application:
#include <falcon-dsl/AutotunerEngine.hpp>
int main() {
falcon::dsl::AutotunerEngine engine;
engine.load_fal_file("voltage_sweep.fal"); // ffimport compiled here
falcon::typing::ParameterMap inputs;
inputs["min_voltage"] = 0.0;
inputs["max_voltage"] = 1.0;
inputs["step"] = 0.1;
auto results = engine.run_autotuner("VoltageSweep", inputs);
int64_t count = std::get<int64_t>(results[0]);
std::cout << "Measured " << count << " point(s).\n";
return 0;
}
No cmake, no custom build step, no shared library to produce by hand — load_fal_file handles everything.
Step 6: Write a Test¶
Use falcon-test to regression-test the autotuner:
// voltage_sweep_tests.fal
import "/opt/falcon/libs/testing/testing.fal";
// The implementation under test
ffimport "voltage_sweep_impl.cpp" () ()
autotuner VoltageSweepTests -> (int passed, int failed) {
passed = 0; failed = 0;
start -> __init;
state test_basic_sweep (TestRunner runner, TestContext t) {
// 0.0, 0.5, 1.0 → 3 points
int count = VoltageSweep(0.0, 1.0, 0.5);
Error err = t.ExpectIntEq(count, 3, "3 measurements for 0→1 step 0.5");
-> __end(runner, t);
}
state test_single_point (TestRunner runner, TestContext t) {
int count = VoltageSweep(0.5, 0.5, 0.1);
Error err = t.ExpectIntEq(count, 1, "single point when begin == end");
-> __end(runner, t);
}
}
falcon-test voltage_sweep_tests.fal
Next Steps¶
- Add error handling states and retry logic
- Use generic structs to build reusable data structures (e.g.
Accumulator<float>for running statistics) - Use cross-module imports to share routines across multiple autotuners
- Use
falcon-testwith fixturesetup/teardownfor hardware-in-the-loop tests - See libs/testing/README.md for the full testing library reference
See LANGUAGE_REFERENCE.md for the complete language specification.