Python Nanobind Migration Plan
This document tracks the plan for replacing GridDyn’s SWIG-generated Python interface with a first-class nanobind interface built directly on the C++ simulation API. It is intended to stay current as the work progresses.
Summary
GridDyn currently exposes Python through SWIG wrappers over the C API in
interfaces/python. The new direction is to build a Pythonic interface with
nanobind that calls GridDyn C++ directly. The C API and SWIG-generated wrappers
should not be part of the new Python implementation.
The first milestone is deliberately narrow: expose the simulation object and the minimum surface needed to load, initialize, solve, and run a GridDyn model from Python. Object editing, events, queries, solver internals, and detailed result access can follow after the simulation workflow is stable.
Goals
Provide a modern Python package for GridDyn using nanobind.
Bind directly to C++ classes and functions, not to the C API.
Make
Simulationthe first supported public concept.Replace return codes and output parameters with Python exceptions and return values.
Use modern Python conventions such as
pathlib.Path, properties, type-friendly APIs, and eventually NumPy arrays.Deprecate SWIG entirely and remove it as soon as replacement workflows are available.
Treat Matlab support, if needed later, as a separate interface problem rather than a SWIG requirement.
Non-goals For The First Milestone
Bind the entire GridDyn C++ class hierarchy.
Reproduce the SWIG function-name API.
Wrap the C API with nanobind.
Support Matlab, Octave, or Java through the new Python work.
Provide complete object construction and model-editing support.
Expose advanced solver internals, Jacobians, or state-vector APIs.
Current State
The existing Python interface is generated from interfaces/griddyn.i and
interfaces/python/griddynPython.i. It exposes the C API from
src/griddyn_shared/griddyn_export.h and
src/griddyn_shared/griddyn_export_advanced.h.
That API is intentionally C-shaped:
Opaque
void*handles represent objects, simulations, queries, events, and solver keys.Users call functions such as
gridDynSimulationLoadfile,gridDynSimulationRunTo, andgridDynObjectGetValue.Error reporting is based on
GridDynErroroutput structures and return values.Arrays are exposed as raw pointer buffers.
The generated Python files include SWIG pointer helpers and explicit object free functions.
This is useful for broad language binding generation, but it is not the surface we want to preserve for Python.
Target Package Shape
The preferred package shape is:
python/
griddyn/
__init__.py
py.typed
griddyn_python.cpp
pyproject.toml
The compiled extension should live under the package, for example:
griddyn/_core.pyd
Python users should import from griddyn, not from an implementation module:
import griddyn as gd
sim = gd.Simulation(name="case14")
sim.load("case14.xml")
sim.initialize()
sim.powerflow()
sim.run_until(10.0)
First Public API
The first public API should focus on loading and operating a simulation:
class Simulation:
def __init__(self, *, name: str = "", type: str = "default") -> None:
...
def load(self, path: str | Path, *, format: str | None = None) -> None:
...
def initialize(self, args: str | Sequence[str] | None = None) -> None:
...
def powerflow(self) -> None:
...
def run(self) -> float:
...
def run_until(self, time: float) -> float:
...
def step(self, time: float) -> float:
...
def reset(self) -> None:
...
@property
def time(self) -> float:
...
@property
def name(self) -> str:
...
@name.setter
def name(self, value: str) -> None:
...
Potential aliases can be added where they improve readability:
load_fileas an alias forloadrun_toas an alias forrun_untilcurrent_timeas an alias fortime
Aliases should be chosen carefully so the API remains small.
C++ Binding Design
The nanobind module should use small adapter classes owned by the binding layer. Those adapters should make Python ownership and error behavior explicit while calling GridDyn C++ directly.
A likely first adapter is:
class PySimulation {
public:
explicit PySimulation(std::string name = "", std::string type = "default");
void load(const std::filesystem::path& path,
std::optional<std::string> format = std::nullopt);
void initialize(std::optional<nb::object> args = std::nullopt);
void powerflow();
double run();
double runUntil(double time);
double step(double time);
void reset();
double time() const;
std::string name() const;
void setName(std::string_view name);
private:
std::shared_ptr<griddyn::GriddynRunner> runner_;
};
The adapter should call C++ APIs such as:
griddyn::GriddynRunnergriddyn::GridDynSimulationgriddyn::loadFileGridDynSimulation::powerflowGridDynSimulation::runGridSimulation::getSimulationTime
The nanobind module should not include or call:
griddyn_export.hgriddyn_export_advanced.hGridDynSimulationCreategridDynObjectCreateany other C API entry points
Error Handling
The Python layer should translate GridDyn C++ exceptions into Python exceptions. Python code should not inspect integer return codes for normal error handling.
Initial exception hierarchy:
GridDynError
InvalidObjectError
InvalidParameterError
FileLoadError
SolveError
ExecutionError
The first implementation can map all known GridDyn exceptions to the closest
Python exception and use GridDynError as the fallback. The mapping should live
in the nanobind source so it is shared by every wrapped method.
GIL Policy
Long-running simulation operations should release the GIL once they no longer need Python objects:
loadinitializepowerflowrunrun_untilstepresetif it can perform significant work
The wrapper should reacquire the GIL before throwing Python exceptions or interacting with Python-owned objects.
Build And Packaging
Use the rtunits Python package as the local model:
pyproject.tomlscikit-build-corenanobindCMake-driven extension build
package-local compiled extension
Likely CMake options:
GRIDDYN_BUILD_PYTHON_LIBRARY=ON
GRIDDYN_BUILD_SWIG_INTERFACE=OFF
The current GRIDDYN_BUILD_PYTHON_INTERFACE name is ambiguous because it points
to the SWIG interface today. As the new work lands, the CMake option names
should make the distinction clear:
GRIDDYN_BUILD_PYTHON_LIBRARYfor the nanobind packageGRIDDYN_BUILD_SWIG_INTERFACESor similar for the legacy SWIG bindings
The nanobind target should link to internal C++ targets needed by
GriddynRunner and GridDynSimulation, not to griddyn_shared_lib.
SWIG Deprecation And Removal
There are currently no known users relying on the SWIG bindings. That means SWIG can likely be removed sooner rather than carried through a long compatibility period.
Proposed policy:
Do not add new SWIG typemaps, generated files, or SWIG-based language interfaces.
Add deprecation messaging to the SWIG CMake path once the nanobind
SimulationAPI can load and run a model.Disable SWIG Python builds by default as soon as the nanobind package covers the basic simulation workflow.
Remove SWIG Python sources after a short transition period if no users appear.
Remove Matlab/Octave/Java SWIG paths unless a concrete user need appears.
If Matlab support is needed later, prefer a command-line, file-based, or purpose-built interface rather than reviving SWIG.
C API Removal Direction
The nanobind migration should not depend on the C API. Once the Python package and any other required workflows no longer need it, the C API can be retired.
Removal should be a separate cleanup phase because the C API currently packages several useful concepts:
opaque object handles
centralized exception-to-error-code translation
shared-library export boundaries
raw-array math and solver access
Those concepts should either disappear from the public surface or be replaced by clearer C++/Python mechanisms before the C API is deleted.
Future API Phases
Phase 2: Minimal Object Access
Expose enough object access to inspect and lightly edit loaded simulations:
obj = sim.find("bus1")
obj.get("voltage", units="pu")
obj.set("angle", 0.0, units="rad")
obj.flag("enabled")
This will require a GridObject adapter with explicit non-owning references
that keep the parent Simulation alive.
Phase 3: Results
Expose common simulation results in a Pythonic way:
scalar results as return values
table-like results as Python objects
vector data as NumPy arrays where useful
file save/load helpers only where they are part of normal workflows
Phase 4: Events And Queries
Expose event scheduling and query objects after the simulation and object surfaces are stable.
Phase 5: Solver And Advanced Math APIs
Expose advanced state, residual, derivative, and Jacobian APIs only after deciding the Python scientific stack expectations:
NumPy arrays
SciPy sparse matrices
typed result objects
ownership and mutability rules for state buffers
Testing Plan
The first tests should be small and workflow-oriented:
Import
griddyn.Construct
Simulation.Load a tiny existing test model.
Initialize.
Run
powerflow.Read
time.Run to a short target time if the model supports dynamics.
Confirm invalid file loads raise
FileLoadError.
The first tests should not depend on SWIG or the C API.
The CI workflow should build and smoke-test the Python wheel, but it should not publish to TestPyPI or PyPI until the Python API is more complete.
Open Decisions
Should the package source live in top-level
python/or underinterfaces/python_nb/during the transition?Should the extension be named
griddyn._core,griddyn._griddyn, or something else?What is the smallest existing model file suitable for a fast Python smoke test?
Should
initialize(args=...)accept a string, a sequence, or both in the first milestone?What minimum Python version should be required?
Should stable ABI wheels be attempted early, or deferred until packaging is otherwise stable?
Should the first nanobind build link static GridDyn components into the extension or ship dependent GridDyn shared libraries beside it?
Working Checklist
[ ] Choose final package location.
[x] Add
pyproject.tomlfor scikit-build-core.[x] Add nanobind CMake discovery and extension target.
[x] Add
griddynPython package directory.[x] Implement
PySimulationadapter.[x] Bind
Simulationin nanobind.[x] Add GridDyn exception translation.
[x] Add basic Python import and construction smoke test.
[x] Add CI wheel build and smoke test without package-index publishing.
[ ] Add load and powerflow smoke test.
[ ] Add documentation for the first Python API.
[ ] Add SWIG deprecation messaging.
[ ] Disable SWIG Python by default once the nanobind smoke tests pass.
[ ] Decide whether to remove Matlab/Octave/Java SWIG build paths.