Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f7c9956
feat: cppyy codegen backend working changes
Legend101Zz Feb 13, 2026
a16280b
remove: unneeded files
Legend101Zz Feb 13, 2026
d390875
fix: template
Legend101Zz Feb 13, 2026
9431c44
feat: add cppyy introspector and dynamic array fix
Legend101Zz Feb 14, 2026
69fe747
add: jupyter notebook for tests
Legend101Zz Feb 14, 2026
5a2320b
fix: remove unneeded code
Legend101Zz Feb 14, 2026
62a6bbf
fix: ratemonitor template, RNG seeding, parameter assertions
Legend101Zz Mar 19, 2026
e2918e4
feat: add cppyy-backed DynamicArray with automatic fallback
Legend101Zz Mar 19, 2026
35109cf
feat: add cppyy SpikeQueue and synapse templates
Legend101Zz Mar 19, 2026
ca3daf0
Merge branch 'master' into experimental-cppyy
Legend101Zz Mar 20, 2026
a151be5
perf: cppyy audit fixes — buffered synapse creation, cached capsule e…
Legend101Zz Mar 20, 2026
9a1f106
docs: comprehensive cppyy backend architecture doc with Mermaid diagrams
Legend101Zz Mar 20, 2026
4fd4a73
feat: add spikegenerator/spatialstateupdate templates, fix int64 buff…
Legend101Zz Mar 20, 2026
5c26a88
chore: update cppyy docs
Legend101Zz Mar 20, 2026
837d1ab
chore: delete old test
Legend101Zz Mar 20, 2026
6e06517
fix: resolve 14 test failures and remove unnecessary cppyy wrappers
Legend101Zz Apr 14, 2026
ab06b18
fix: skip Python-side synapse bookkeeping for standalone device
Legend101Zz Apr 15, 2026
0d210b5
fix: guard old_num_synapses inside RuntimeDevice check to fix standal…
Legend101Zz Apr 15, 2026
6e8a1b3
feat: add group_get_indices template and wire cppyy into test suite
Legend101Zz Apr 15, 2026
4afd587
fix: guard SynapticPathway.initialise_queue() behind RuntimeDevice check
Legend101Zz Apr 15, 2026
ec9ff26
ci: add cppyy to CI test matrix and pyproject optional extras
Legend101Zz Apr 16, 2026
5c9d4da
ci: skip cppyy install on Windows due to upstream ABI mismatch
Legend101Zz Apr 16, 2026
628e39d
fix: prevent Cling redefinition when GC recycles TimedArray function …
Legend101Zz Apr 17, 2026
ae11d3b
fix: scope create_cond block to prevent _post_idx redefinition in syn…
Legend101Zz Apr 17, 2026
ea6cf33
fix: add ALLOWS_SCALAR_WRITE to group_variable_set templates
Legend101Zz Apr 17, 2026
40c8fa3
fix: use constant_or_scalar for N_pre/N_post in synapses_create_gener…
Legend101Zz Apr 17, 2026
78a1d59
fix: raise IndexError from C++ bounds errors in synapses_create_gener…
Legend101Zz Apr 17, 2026
801a9b4
fix: rewrite threshold/group_variable_get templates, add cppyy key to…
Legend101Zz Apr 17, 2026
bb73bac
chore: remove dev-only scratch files and draft docs from PR
Legend101Zz Apr 17, 2026
934869f
fix(cppyy): fix RNG reproducibility, seeding, state save/restore, and…
Legend101Zz May 6, 2026
dd21662
perf(cppyy): cache args tuple, drop diagnostic, guard ascontiguousarr…
Legend101Zz May 25, 2026
e7f65bc
perf(cppyy): per-block fast-dispatch and process-level Cling compile …
Legend101Zz May 25, 2026
c90b053
chore: add testing scripts
Legend101Zz Jun 8, 2026
24c2228
fix(cppyy): treat cppyy as no-disk-cache target in test_clear_cache
Legend101Zz Jun 9, 2026
e29a0e8
fix(cppyy): add `long` template specializations for mod/floordiv on m…
Legend101Zz Jun 10, 2026
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
2 changes: 1 addition & 1 deletion brian2/codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
from . import _prefs
from . import cpp_prefs as _cpp_prefs

__all__ = ["NumpyCodeObject", "CythonCodeObject"]
__all__ = ["NumpyCodeObject", "CythonCodeObject", "CppyyCodeObject"]
5 changes: 4 additions & 1 deletion brian2/codegen/_prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
Can be a string, in which case it should be one of:

* ``'auto'`` the default, automatically chose the best code generation
target available.
target available. Priority order: cython > cppyy > numpy.
* ``'cython'``, uses the Cython package to generate C++ code. Needs a
working installation of Cython and a C++ compiler.
* ``'cppyy'``, uses cppyy for JIT compilation via LLVM/Cling. Needs
cppyy installed but no external C++ compiler. Provides fast in-memory
compilation without filesystem I/O.
* ``'numpy'`` works on all platforms and doesn't need a C compiler but
is often less efficient.

Expand Down
261 changes: 261 additions & 0 deletions brian2/codegen/generators/cppyy_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
"""
C++ code generator for the cppyy runtime target.

Inherits CPPCodeGenerator's full translation pipeline (expressions, the
read→declare→execute→write phases, scalar hoisting, boolean optimization).
Overrides array naming and keyword generation so data arrives from Python
as function parameters rather than global C++ variables.
"""

from __future__ import annotations

from typing import Any

from brian2.codegen.generators.cpp_generator import (
CPPCodeGenerator,
c_data_type,
stripped_deindented_lines,
)
from brian2.core.functions import DEFAULT_FUNCTIONS, Function
from brian2.core.variables import (
ArrayVariable,
AuxiliaryVariable,
Constant,
DynamicArrayVariable,
Subexpression,
)

# (c_type, param_name, namespace_key)
FunctionParam = tuple[str, str, str]


def _cppyy_c_data_type(dtype: type | Any) -> str:
"""
Like c_data_type but maps bool→int8_t instead of char.

cppyy is strict about buffer types: numpy int8 maps to signed char (int8_t),
not char. Using int8_t in the signature lets the buffer protocol match.
The function body still uses char for locals — implicit conversion handles it.
"""
ctype: str = c_data_type(dtype)
if ctype == "char":
return "int8_t"
return ctype


class CppyyCodeGenerator(CPPCodeGenerator):
"""
C++ code generator targeting cppyy's JIT runtime.

All C++ translation logic (expressions, 4-phase pattern, etc.) is inherited.
We only change how arrays are named and how keywords/params are assembled.
"""

class_name: str = "cppyy"

@staticmethod
def get_array_name(var: ArrayVariable, access_data: bool = True) -> str:
"""
Globally unique name for an array variable.

access_data=True → "_ptr_array_{owner}_{name}" (data pointer)
access_data=False → "_dynamic_array_{owner}_{name}" (container object)
"""
owner_name: str = getattr(var.owner, "name", "temporary")

if isinstance(var, DynamicArrayVariable):
if access_data:
return f"_ptr_array_{owner_name}_{var.name}"
else:
return f"_dynamic_array_{owner_name}_{var.name}"
elif isinstance(var, ArrayVariable):
return f"_ptr_array_{owner_name}_{var.name}"
else:
raise TypeError(
f"get_array_name called with non-array variable: {type(var)}"
)

def determine_keywords(self) -> dict[str, Any]:
"""
Build template keywords: function params, support code, hash defines.

This runs at the end of translate_statement_sequence(). The returned
dict gets merged with scalar_code/vector_code and passed to templates.

We iterate sorted(self.variables.items()) — the code object's
_build_param_mapping does the same, so parameter order is guaranteed
to match between the signature and the call site.
"""

support_code_parts: list[str] = []
hash_define_parts: list[str] = []
user_functions: list[Any] = []
user_func_namespaces: dict[
str, Any
] = {} # for setting C++ globals post-compile
added: set[str] = set()

function_params: list[FunctionParam] = []
handled_pointers: set[str] = set()

for varname, var in sorted(self.variables.items()):
if isinstance(var, (AuxiliaryVariable, Subexpression)):
continue

# --- User functions (TimedArray, BinomialFunction, etc.) ---
if isinstance(var, Function):
if self.codeobj_class in var.implementations:
result: tuple | None = self._add_user_function(varname, var, added)
if result is not None:
hd, _pointers, sc, uf = result
hash_define_parts.extend(hd)
support_code_parts.extend(sc)
user_functions.extend(uf)

# Grab namespace values (actual numpy arrays) for C++ globals
impl = var.implementations[self.codeobj_class]
func_ns: dict[str, Any] | None = impl.get_namespace(self.owner)
if func_ns:
user_func_namespaces.update(func_ns)
continue

# --- Constants: scalar typed parameters ---
if isinstance(var, Constant):
c_type: str = _cppyy_c_data_type(type(var.value))
function_params.append((c_type, varname, varname))
continue

# --- Array variables: pointer + size parameters ---
if isinstance(var, ArrayVariable):
pointer_name = self.get_array_name(var)
if pointer_name in handled_pointers:
continue
handled_pointers.add(pointer_name)

if getattr(var, "ndim", 1) > 1:
# 2D dynamic arrays: pass the capsule instead of a data pointer,
# because monitors need to resize them. The C++ code extracts
# the DynamicArray2D<T>* from the capsule and calls methods on it.
if isinstance(var, DynamicArrayVariable):
dyn_name = self.get_array_name(var, access_data=False)
capsule_key = f"{dyn_name}_capsule"
function_params.append(("PyObject*", capsule_key, capsule_key))
continue

c_type = _cppyy_c_data_type(var.dtype)
namespace_key = self.get_array_name(var)
function_params.append((f"{c_type}*", pointer_name, namespace_key))

if not var.scalar:
function_params.append(("int", f"_num{varname}", f"_num{varname}"))

# For 1D dynamic arrays, ALSO pass the capsule so monitors can resize
if isinstance(var, DynamicArrayVariable):
dyn_name = self.get_array_name(var, access_data=False)
capsule_key = f"{dyn_name}_capsule"
function_params.append(("PyObject*", capsule_key, capsule_key))

# Optional denormals flushing (gcc/clang x86)
denormals_code: str = ""
if self.flush_denormals:
denormals_code = """
#define CSR_FLUSH_TO_ZERO (1 << 15)
unsigned csr = __builtin_ia32_stmxcsr();
csr |= CSR_FLUSH_TO_ZERO;
__builtin_ia32_ldmxcsr(csr);
"""

return {
"support_code_lines": "\n".join(
stripped_deindented_lines("\n".join(support_code_parts))
),
"hashdefine_lines": "\n".join(
stripped_deindented_lines("\n".join(hash_define_parts))
),
"denormals_code_lines": "\n".join(
stripped_deindented_lines(denormals_code)
),
"function_params": function_params,
"user_func_namespaces": user_func_namespaces,
"user_functions": user_functions,
}


# --- Function implementations ---
#
# We get sin/cos/exp/log/etc. for free via MRO (registered on CPPCodeGenerator).
# Same for arcsin→asin, int→int_, exprel, TimedArray, BinomialFunction.
#
# We must explicitly register clip/sign/timestep/poisson — they're only on
# CythonCodeGenerator which isn't in our MRO chain.

_clip_code: str = """
template<typename T>
inline T _clip(T value, double a_min, double a_max) {
if (value < (T)a_min) return (T)a_min;
if (value > (T)a_max) return (T)a_max;
return value;
}
"""
DEFAULT_FUNCTIONS["clip"].implementations.add_implementation(
CppyyCodeGenerator, code=_clip_code, name="_clip"
)

_sign_code: str = """
template<typename T>
inline int _sign(T x) {
return (T(0) < x) - (x < T(0));
}
"""
DEFAULT_FUNCTIONS["sign"].implementations.add_implementation(
CppyyCodeGenerator, code=_sign_code, name="_sign"
)

_timestep_code: str = """
inline int64_t _timestep(double t, double dt) {
return (int64_t)((t + 1e-3*dt)/dt);
}
"""
DEFAULT_FUNCTIONS["timestep"].implementations.add_implementation(
CppyyCodeGenerator, code=_timestep_code, name="_timestep"
)

_poisson_code: str = """
#include <random>
inline int32_t _poisson(double lam, int _vectorisation_idx) {
std::poisson_distribution<int32_t> _poisson_dist(lam);
return _poisson_dist(_brian_cppyy_rng);
}
"""
DEFAULT_FUNCTIONS["poisson"].implementations.add_implementation(
CppyyCodeGenerator, code=_poisson_code, name="_poisson"
)

# rand/randn use the shared MT19937 engine from _ensure_support_code()
_rand_support: str = """
inline double _rand(const int _vectorisation_idx) {
static std::uniform_real_distribution<double> _dist_rand(0.0, 1.0);
return _dist_rand(_brian_cppyy_rng);
}
"""

_randn_support: str = """
inline double _randn(const int _vectorisation_idx) {
static std::normal_distribution<double> _dist_randn(0.0, 1.0);
return _dist_randn(_brian_cppyy_rng);
}
"""

DEFAULT_FUNCTIONS["rand"].implementations.add_dynamic_implementation(
CppyyCodeGenerator,
code=lambda owner: {"support_code": _rand_support},
namespace=lambda owner: {},
name="_rand",
)

DEFAULT_FUNCTIONS["randn"].implementations.add_dynamic_implementation(
CppyyCodeGenerator,
code=lambda owner: {"support_code": _randn_support},
namespace=lambda owner: {},
name="_randn",
)
14 changes: 12 additions & 2 deletions brian2/codegen/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Runtime targets for code generation.
"""

# Register the base category before importing the indivial codegen targets with
# Register the base category before importing the individual codegen targets with
# their subcategories

from brian2.core.preferences import prefs
Expand All @@ -15,12 +15,22 @@

logger = get_logger(__name__)

# Always available
from .numpy_rt import *

# Optional: Cython (requires Cython + C++ compiler)
try:
from .cython_rt import *
except ImportError:
pass # todo: raise a warning?
logger.debug("Cython runtime not available", exc_info=True)

# Optional: cppyy (requires cppyy, no external compiler needed)
try:
from .cppyy_rt import *
except ImportError:
logger.debug("cppyy runtime not available", exc_info=True)

# Optional: GSL integration
try:
from .GSLcython_rt import *
except ImportError:
Expand Down
24 changes: 24 additions & 0 deletions brian2/codegen/runtime/cppyy_rt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
cppyy Runtime Backend for Brian2.
"""

from __future__ import annotations

from brian2.utils.logger import get_logger

logger = get_logger(__name__)

try:
from brian2.codegen.runtime.cppyy_rt.cppyy_rt import CppyyCodeObject
from brian2.codegen.targets import codegen_targets

# Register the target (same pattern as numpy_rt and cython_rt)
codegen_targets.add(CppyyCodeObject)

__all__ = ["CppyyCodeObject"]
logger.debug("cppyy runtime backend registered")

except ImportError as e:
logger.debug(f"cppyy runtime backend not available: {e}")
__all__ = []
CppyyCodeObject = None
Loading