Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e0a29e8
working on cpp verification
mscroggs Apr 1, 2026
996ae6f
simplefem++
mscroggs Apr 1, 2026
9da3b8d
working towards jittable C++
mscroggs Apr 1, 2026
83a0471
fix paths
mscroggs Apr 1, 2026
e4c7180
trying to get cpp working
mscroggs Apr 8, 2026
1da5a7d
working on cpp interface
mscroggs Apr 9, 2026
322f9f1
it works!!
mscroggs Apr 9, 2026
cf8505f
indentataion, and a little bit of simplifying
mscroggs Apr 9, 2026
f7ec08d
ruff format
mscroggs Apr 9, 2026
71d26b4
mypy
mscroggs Apr 9, 2026
2a90d14
simplify
mscroggs Apr 9, 2026
41eadb0
pydocstyle
mscroggs Apr 10, 2026
b75d20e
move simplefempp into simplefem.py
mscroggs Apr 10, 2026
d879e10
add test that C++ jit works
mscroggs Apr 10, 2026
8f03d84
mdpsan license
mscroggs Apr 10, 2026
a445eca
Merge branch 'main' into mscroggs/cpp
mscroggs Apr 10, 2026
5763bdb
website build tools version
mscroggs Apr 10, 2026
ad98ff2
simplefempp
mscroggs Apr 10, 2026
026826b
add documentation
mscroggs Apr 14, 2026
51de709
@classmethod, ruff
mscroggs Apr 14, 2026
746f763
list[]
mscroggs Apr 14, 2026
8238e13
rename file
mscroggs Apr 15, 2026
c9d674a
Merge branch 'main' into mscroggs/cpp
mscroggs Apr 23, 2026
90dc6cc
Merge branch 'main' into mscroggs/cpp
mscroggs May 5, 2026
4530ea8
Merge branch 'main' into mscroggs/cpp
mscroggs May 18, 2026
8a2c042
Merge branch 'main' into mscroggs/cpp
mscroggs Jun 25, 2026
f4615fd
Merge branch 'main' into mscroggs/cpp
mscroggs Jul 28, 2026
8088129
ruff updates
mscroggs Jul 28, 2026
eba8cbd
move replacer to function not lambda function
mscroggs Jul 28, 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
Prev Previous commit
Next Next commit
ruff format
  • Loading branch information
mscroggs committed Apr 9, 2026
commit f7ec08dee6252a13f4219c90a8992d7c98b89894
97 changes: 62 additions & 35 deletions defelement/implementations/jit/argument_types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Argument types."""

from abc import ABC, abstractmethod
from itertools import product
import numpy as np
import ctypes


class ArgType(ABC):
Expand Down Expand Up @@ -55,13 +55,15 @@ def in_out(self) -> bool:

def empty(self, lib, ffi, inputs):
"""Create an empty instance of this object to use as an in/out argument."""
raise NotImplmentedError("empty not implemented for this argument type.")
raise NotImplementedError("empty not implemented for this argument type.")


class NDArray(ArgType):
"""N-dimensional array."""

def __init__(self, variable: str, dimension: int, shape: tuple[int | str, ...] | None, dtype: str):
def __init__(
self, variable: str, dimension: int, shape: tuple[int | str, ...] | None, dtype: str
):
"""Initialise."""
self.variable = variable
self.dimension = dimension
Expand Down Expand Up @@ -114,7 +116,9 @@ def metadata_function_impl(self, language: str, function: str) -> str | None:

out += f"{self.shape_type_name} {self.shape_variable_name} = {{ "
if self.shape is None:
out += ", ".join(f".shape{i} = {self.raw_variable_name}.shape{i}" for i in range(self.dimension))
out += ", ".join(
f".shape{i} = {self.raw_variable_name}.shape{i}" for i in range(self.dimension)
)
else:
out += ", ".join(f".shape{i} = (int){j}" for i, j in enumerate(self.shape))
out += " };\n"
Expand Down Expand Up @@ -142,47 +146,64 @@ def function_output(self, language: str):
"""Output(s) from a function."""
match language:
case "cpp":
return "\n".join([
f"{self.raw_type_name} {self.variable}_out = {{ " + ", ".join([
f".data = {self.raw_variable_name}.data()"] + [
f".shape{i} = {self.shape_variable_name}[{i}]" for i in range(self.dimension)
]) + " };",
f"return {self.variable}_out;",
])
return "\n".join(
[
f"{self.raw_type_name} {self.variable}_out = {{ "
+ ", ".join(
[f".data = {self.raw_variable_name}.data()"]
+ [
f".shape{i} = {self.shape_variable_name}[{i}]"
for i in range(self.dimension)
]
)
+ " };",
f"return {self.variable}_out;",
]
)
case _:
raise ValueError(f"Unsupported language: {language}")

def define_custom_type(self, language: str) -> str | None:
"""Definition of type to include if necessary."""
match language:
case "cpp":
return "\n".join([
f"typedef struct {self.raw_type_name} {{",
f" {self.dtype}* data;",
] + [
f" int shape{i};" for i in range(self.dimension)
] + [
f"}} {self.raw_type_name};",
f"typedef struct {self.shape_type_name} {{",
] + [
f" int shape{i};" for i in range(self.dimension)
] + [
f"}} {self.shape_type_name};",
f"{self.raw_type_name} new_{self.raw_type_name}({self.dtype}* data, " + ", ".join(f"int shape{i}" for i in range(self.dimension)) + ");",
])
return "\n".join(
[
f"typedef struct {self.raw_type_name} {{",
f" {self.dtype}* data;",
]
+ [f" int shape{i};" for i in range(self.dimension)]
+ [
f"}} {self.raw_type_name};",
f"typedef struct {self.shape_type_name} {{",
]
+ [f" int shape{i};" for i in range(self.dimension)]
+ [
f"}} {self.shape_type_name};",
f"{self.raw_type_name} new_{self.raw_type_name}({self.dtype}* data, "
+ ", ".join(f"int shape{i}" for i in range(self.dimension))
+ ");",
]
)
case _:
return None

def init_custom_type(self, language: str) -> str | None:
"""Initialise of custom type(s) to include if necessary."""
match language:
case "cpp":
return "\n".join([
f"{self.raw_type_name} new_{self.raw_type_name}({self.dtype}* data, " + ", ".join(f"int shape{i}" for i in range(self.dimension)) + "){",
f" {self.raw_type_name} out = {{ .data = data, " + ", ".join(f".shape{i} = shape{i}" for i in range(self.dimension)) + " };",
" return out;",
"}",
])
return "\n".join(
[
f"{self.raw_type_name} new_{self.raw_type_name}({self.dtype}* data, "
+ ", ".join(f"int shape{i}" for i in range(self.dimension))
+ "){",
f" {self.raw_type_name} out = {{ .data = data, "
+ ", ".join(f".shape{i} = shape{i}" for i in range(self.dimension))
+ " };",
" return out;",
"}",
]
)
case _:
return None

Expand All @@ -193,15 +214,19 @@ def initialise(self, language: str, output: bool = False) -> str:
lines = []
line = f"std::array<int, {self.dimension}> {self.shape_variable_name} = {{"
if self.shape is None:
line += ", ".join(f"{self.raw_variable_name}.shape{i}" for i in range(self.dimension))
line += ", ".join(
f"{self.raw_variable_name}.shape{i}" for i in range(self.dimension)
)
else:
line += ", ".join(f"(int){i}" for i in self.shape)
line += "};"
lines.append(line)
if output:
assert self.shape is not None
lines += [
f"std::vector<{self.dtype}> {self.raw_variable_name}(" + " * ".join(f"{i}" for i in self.shape) + ");",
f"std::vector<{self.dtype}> {self.raw_variable_name}("
+ " * ".join(f"{i}" for i in self.shape)
+ ");",
f"mdspan<{self.dtype}, {self.dimension}> {self.variable}({self.raw_variable_name}.data(), {self.shape_variable_name});",
]
else:
Expand All @@ -214,7 +239,9 @@ def initialise(self, language: str, output: bool = False) -> str:

def to_raw(self, lib, ffi, obj):
"""Convert python object to a raw C type."""
return getattr(lib, f"new_{self.raw_type_name}")(ffi.cast("double*", obj.ctypes.data), *obj.shape)
return getattr(lib, f"new_{self.raw_type_name}")(
ffi.cast("double*", obj.ctypes.data), *obj.shape
)

def from_raw(self, lib, ffi, obj):
"""Convert raw C object to a Python type."""
Expand Down Expand Up @@ -276,7 +303,7 @@ def array(
dtype: str = "double",
) -> ArgType:
"""Create a one-dimensional array."""
return NDArray(variable, 1, None if length is None else (length, ), dtype)
return NDArray(variable, 1, None if length is None else (length,), dtype)


def int(
Expand Down
46 changes: 31 additions & 15 deletions defelement/implementations/jit/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
from collections.abc import Callable
from defelement.implementations.jit import tools
from defelement.implementations.jit.argument_types import ArgType
import numpy.typing as npt
import numpy as np
from webtools.tools import join

from cffi import FFI

here = os.path.dirname(os.path.realpath(__file__))


def add_indent(text: str, indentation: int) -> str:
return "\n".join(" " * indentation + i for i in text.split("\n"))


def compile(
*,
function: str,
Expand Down Expand Up @@ -100,30 +100,33 @@ def compile(
code += "\n"
if len(true_outputs) > 0:
if len(true_outputs) == 1:
code += add_indent(true_outputs[0].function_output('cpp'), 2)
code += add_indent(true_outputs[0].function_output("cpp"), 2)
code += "\n"
else:
raise NotImplementedError("Functions with more than one output not currently supported")
raise NotImplementedError(
"Functions with more than one output not currently supported"
)
code += "}\n"
for vars, out in [(inputs + in_out, False), (true_outputs, True)]:
for i in vars:
# TODO: indent
code = re.sub(
r"( *)INIT " + i.variable,
lambda matches: add_indent(i.initialise("cpp", output=out), len(matches[1])),
code
lambda matches: add_indent(
i.initialise("cpp", output=out), len(matches[1])
),
code,
)
with open(join(folder, "function.cpp"), "w") as f:
f.write(code)

# other files
find_packages = "\n".join(
f"find_package({libname} REQUIRED)"
for libname, _ in packages
f"find_package({libname} REQUIRED)" for libname, _ in packages
)
target_link_libraries = "\n".join(
f"target_link_libraries({library_name} {libname}::{namespace})"
for libname, namespace in packages
for libname, namespace in packages
)

for source, target in [
Expand All @@ -135,16 +138,18 @@ def compile(
template = f.read()

with open(join(folder, target), "w") as f:
f.write(template
.replace("<!project_name>", project_name)
f.write(
template.replace("<!project_name>", project_name)
.replace("<!library_name>", library_name)
.replace("<!find_packages>", find_packages)
.replace("<!target_link_libraries>", target_link_libraries)
)

if not os.path.isdir(join(folder, "build")):
os.mkdir(join(folder, "build"))
assert os.system(f"cmake -DCMAKE_INSTALL_PREFIX:PATH=. -B{folder}/build -S{folder}") == 0
assert (
os.system(f"cmake -DCMAKE_INSTALL_PREFIX:PATH=. -B{folder}/build -S{folder}") == 0
)
assert os.system(f"make -C {folder}/build") == 0
assert os.system(f"make -C {folder}/build install") == 0

Expand All @@ -154,12 +159,21 @@ def compile(
tools.check_for_error(folder)

ffi = FFI()
ffi.cdef("\n".join(custom_types) + "\n" + "\n".join(f"{mf};" for mf in metadata_functions) + "\n" + function_def + ";")
ffi.cdef(
"\n".join(custom_types)
+ "\n"
+ "\n".join(f"{mf};" for mf in metadata_functions)
+ "\n"
+ function_def
+ ";"
)
lib = ffi.dlopen(join(folder, "build", f"lib{library_name}.so"))

def function(*args):
if len(args) != len(inputs):
raise TypeError(f"Incorrect number of arguments passed to function: expecting {len(inputs)}, received {len(args)}")
raise TypeError(
f"Incorrect number of arguments passed to function: expecting {len(inputs)}, received {len(args)}"
)
c_inputs = [i.to_raw(lib, ffi, a) for i, a in zip(inputs, args)]
in_out_args = tuple(i.empty(lib, ffi, c_inputs) for i in in_out)
c_in_out = [i.to_raw(lib, ffi, a) for i, a in zip(in_out, in_out_args)]
Expand All @@ -174,7 +188,9 @@ def function(*args):
if len(true_outputs) == 1:
out.append(true_outputs[0].from_raw(lib, ffi, result))
else:
raise NotImplementedError("Functions with more than one output not currently supported")
raise NotImplementedError(
"Functions with more than one output not currently supported"
)
if len(out) > 0:
if len(out) == 1:
return out[0]
Expand Down
4 changes: 2 additions & 2 deletions defelement/implementations/jit/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import hashlib
import os
from appdirs import user_cache_dir

try:
os.mkdir(user_cache_dir())
except FileExistsError:
Expand Down Expand Up @@ -53,5 +54,4 @@ def check_for_error(folder: str):


def hash(text: str) -> str:
return hashlib.sha256(text.encode('utf-8')).hexdigest()

return hashlib.sha256(text.encode("utf-8")).hexdigest()
45 changes: 23 additions & 22 deletions defelement/implementations/simplefempp.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ def single_example(
def install(cls, language: str) -> str | None:
"""Get the command(s) to install this implementation."""
if language == "cpp":
return "\n".join([
"git clone https://github.com/DefElement/simplefempp",
"cd simplefempp",
"mkdir build",
"cd build",
"cmake ..",
"sudo make install",
])
return "\n".join(
[
"git clone https://github.com/DefElement/simplefempp",
"cd simplefempp",
"mkdir build",
"cd build",
"cmake ..",
"sudo make install",
]
)
return None

@classmethod
Expand All @@ -66,8 +68,6 @@ def verify(
example: str,
) -> tuple[list[list[list[int]]], typing.Callable[[NDArray[float64]], NDArray[float64]]]:
"""Get verification data."""
import simplefem
import numpy as np

if degree == 1:
entity_dofs = [
Expand All @@ -92,25 +92,26 @@ def verify(

tabulate = jit.cpp(
inputs=[jit.ndarray("pts", 2), jit.int("degree")],
function="\n".join([
f"auto element = {name}(degree);",
"INIT values;",
"std::vector<double> point(2);",
"for (std::size_t i = 0; i < pts.extent(0); ++i)",
" for (std::size_t j = 0; j < element.dim(); ++j) {",
" point[0] = pts(i, 0);",
" point[1] = pts(i, 1);",
" values(i, 0, j) = element.evaluate(j, point);",
" }",
]),
function="\n".join(
[
f"auto element = {name}(degree);",
"INIT values;",
"std::vector<double> point(2);",
"for (std::size_t i = 0; i < pts.extent(0); ++i)",
" for (std::size_t j = 0; j < element.dim(); ++j) {",
" point[0] = pts(i, 0);",
" point[1] = pts(i, 1);",
" values(i, 0, j) = element.evaluate(j, point);",
" }",
]
),
outputs=[jit.ndarray("values", 3, shape=("pts.extent(0)", 1, "element.dim()"))],
imports=cls.example_import("cpp") + "\n#include <vector>",
id=f"{cls.name}-{name}-{reference}",
packages=[("SimpleFem", "simplefem")],
)
return entity_dofs, lambda pts: tabulate(pts, degree)


id = "simplefempp"
name = "simplefem++"
url = "https://github.com/DefElement/simplefempp"
Expand Down