From 77b3f02386cae8c7292f3c673501e650013bdb64 Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 30 Apr 2024 12:04:28 +0100 Subject: [PATCH 001/106] Updates to 'language' support --- ffcx/codegeneration/C/__init__.py | 2 + ffcx/codegeneration/codegeneration.py | 21 +- ffcx/codegeneration/cpp/__init__.py | 3 + ffcx/codegeneration/cpp/cpp_implementation.py | 246 ++++++++++++++++++ ffcx/codegeneration/cpp/expressions.py | 147 +++++++++++ .../cpp/expressions_template.py | 60 +++++ ffcx/codegeneration/cpp/file.py | 48 ++++ ffcx/codegeneration/cpp/file_template.py | 37 +++ ffcx/codegeneration/cpp/form.py | 23 ++ ffcx/codegeneration/cpp/form_template.py | 48 ++++ ffcx/codegeneration/cpp/integrals.py | 64 +++++ ffcx/codegeneration/cpp/integrals_template.py | 51 ++++ ffcx/main.py | 1 + 13 files changed, 747 insertions(+), 4 deletions(-) create mode 100644 ffcx/codegeneration/cpp/__init__.py create mode 100644 ffcx/codegeneration/cpp/cpp_implementation.py create mode 100644 ffcx/codegeneration/cpp/expressions.py create mode 100644 ffcx/codegeneration/cpp/expressions_template.py create mode 100644 ffcx/codegeneration/cpp/file.py create mode 100644 ffcx/codegeneration/cpp/file_template.py create mode 100644 ffcx/codegeneration/cpp/form.py create mode 100644 ffcx/codegeneration/cpp/form_template.py create mode 100644 ffcx/codegeneration/cpp/integrals.py create mode 100644 ffcx/codegeneration/cpp/integrals_template.py diff --git a/ffcx/codegeneration/C/__init__.py b/ffcx/codegeneration/C/__init__.py index e4d6c1f41..214752d4e 100644 --- a/ffcx/codegeneration/C/__init__.py +++ b/ffcx/codegeneration/C/__init__.py @@ -1 +1,3 @@ """Generation of C code.""" +from ffcx.codegeneration.C import form, expressions, integrals, file # noqa +suffixes = (".h", ".c") \ No newline at end of file diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 9564c3837..860ba9ab2 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -14,13 +14,11 @@ import logging import typing +import sys +from importlib import import_module import numpy.typing as npt -from ffcx.codegeneration.C.expressions import generator as expression_generator -from ffcx.codegeneration.C.file import generator as file_generator -from ffcx.codegeneration.C.form import generator as form_generator -from ffcx.codegeneration.C.integrals import generator as integral_generator from ffcx.ir.representation import DataIR logger = logging.getLogger("ffcx") @@ -45,6 +43,21 @@ def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) - logger.info("Compiler stage 3: Generating code") logger.info(79 * "*") + lang = options.get("language", "C") + try: + # Built-in + mod = import_module(f"ffcx.codegeneration.{lang}") + + except ImportError: + # User defined, in current directory + sys.path.append(".") + mod = import_module(f"{lang}") + + integral_generator = mod.integrals.generator + form_generator = mod.form.generator + expression_generator = mod.expressions.generator + file_generator = mod.file.generator + code_integrals = [integral_generator(integral_ir, options) for integral_ir in ir.integrals] code_forms = [form_generator(form_ir, options) for form_ir in ir.forms] code_expressions = [ diff --git a/ffcx/codegeneration/cpp/__init__.py b/ffcx/codegeneration/cpp/__init__.py new file mode 100644 index 000000000..3616783fa --- /dev/null +++ b/ffcx/codegeneration/cpp/__init__.py @@ -0,0 +1,3 @@ +from . import form, expressions, integrals, file # noqa + +suffixes = (".hpp", ".cpp") diff --git a/ffcx/codegeneration/cpp/cpp_implementation.py b/ffcx/codegeneration/cpp/cpp_implementation.py new file mode 100644 index 000000000..0c3aa0996 --- /dev/null +++ b/ffcx/codegeneration/cpp/cpp_implementation.py @@ -0,0 +1,246 @@ +# Copyright (C) 2023 Chris Richardson +# +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import ffcx.codegeneration.lnodes as L + +math_table = { + "sqrt": "std::sqrt", + "abs": "std::abs", + "cos": "std::cos", + "sin": "std::sin", + "tan": "std::tan", + "acos": "std::acos", + "asin": "std::asin", + "atan": "std::atan", + "cosh": "std::cosh", + "sinh": "std::sinh", + "tanh": "std::tanh", + "acosh": "std::acosh", + "asinh": "std::asinh", + "atanh": "std::atanh", + "power": "std::pow", + "exp": "std::exp", + "ln": "std::log", + "erf": "std::erf", + "atan_2": "std::atan2", + "min_value": "std::fmin", + "max_value": "std::fmax", + "bessel_y": "std::cyl_bessel_i", + "bessel_j": "std::cyl_bessel_j", + "conj": "std::conj", + "real": "std::real", + "imag": "std::imag"} + + +def build_initializer_lists(values): + arr = "{" + if len(values.shape) == 1: + return "{" + ", ".join(str(v) for v in values) + "}" + elif len(values.shape) > 1: + arr += ",\n".join(build_initializer_lists(v) for v in values) + arr += "}" + return arr + + +class CppFormatter(object): + def __init__(self, scalar) -> None: + self.scalar_type = "T" + self.real_type = "U" + + def format_statement_list(self, slist) -> str: + return "".join(self.c_format(s) for s in slist.statements) + + def format_section(self, section) -> str: + """Format a section.""" + # add new line before section + comments = "// ------------------------ \n" + comments += "// Section: " + section.name + "\n" + comments += "// Inputs: " + ", ".join(w.name for w in section.input) + "\n" + comments += "// Outputs: " + ", ".join(w.name for w in section.output) + "\n" + declarations = "".join(self.c_format(s) for s in section.declarations) + + body = "" + if len(section.statements) > 0: + declarations += "{\n " + body = "".join(self.c_format(s) for s in section.statements) + body = body.replace("\n", "\n ") + body = body[:-2] + "}\n" + + body += "// ------------------------ \n" + return comments + declarations + body + + def format_comment(self, c) -> str: + return "// " + c.comment + "\n" + + def format_array_decl(self, arr) -> str: + dtype = arr.symbol.dtype + assert dtype is not None + + if dtype == L.DataType.SCALAR: + typename = self.scalar_type + elif dtype == L.DataType.REAL: + typename = self.real_type + elif dtype == L.DataType.INT: + typename = "int" + else: + raise ValueError("Invalid datatype") + + symbol = self.c_format(arr.symbol) + dims = "".join([f"[{i}]" for i in arr.sizes]) + if arr.values is None: + assert arr.const is False + return f"{typename} {symbol}{dims};\n" + + vals = build_initializer_lists(arr.values) + cstr = "static const " if arr.const else "" + return f"{cstr}{typename} {symbol}{dims} = {vals};\n" + + def format_array_access(self, arr) -> str: + name = self.c_format(arr.array) + indices = f"[{']['.join(self.c_format(i) for i in arr.indices)}]" + return f"{name}{indices}" + + def format_multi_index(self, index) -> str: + return self.c_format(index.global_index) + + def format_variable_decl(self, v) -> str: + val = self.c_format(v.value) + symbol = self.c_format(v.symbol) + assert v.symbol.dtype + if v.symbol.dtype == L.DataType.SCALAR: + typename = self.scalar_type + elif v.symbol.dtype == L.DataType.REAL: + typename = self.real_type + return f"{typename} {symbol} = {val};\n" + + def format_nary_op(self, oper) -> str: + # Format children + args = [self.c_format(arg) for arg in oper.args] + + # Apply parentheses + for i in range(len(args)): + if oper.args[i].precedence >= oper.precedence: + args[i] = "(" + args[i] + ")" + + # Return combined string + return f" {oper.op} ".join(args) + + def format_binary_op(self, oper) -> str: + # Format children + lhs = self.c_format(oper.lhs) + rhs = self.c_format(oper.rhs) + + # Apply parentheses + if oper.lhs.precedence >= oper.precedence: + lhs = f"({lhs})" + if oper.rhs.precedence >= oper.precedence: + rhs = f"({rhs})" + + # Return combined string + return f"{lhs} {oper.op} {rhs}" + + def format_neg(self, val) -> str: + arg = self.c_format(val.arg) + return f"-{arg}" + + def format_not(self, val) -> str: + arg = self.c_format(val.arg) + return f"{val.op}({arg})" + + def format_literal_float(self, val) -> str: + return f"{val.value}" + + def format_literal_int(self, val) -> str: + return f"{val.value}" + + def format_for_range(self, r) -> str: + begin = self.c_format(r.begin) + end = self.c_format(r.end) + index = self.c_format(r.index) + output = f"for (int {index} = {begin}; {index} < {end}; ++{index})\n" + output += "{\n" + body = self.c_format(r.body) + for line in body.split("\n"): + if len(line) > 0: + output += f" {line}\n" + output += "}\n" + return output + + def format_statement(self, s) -> str: + return self.c_format(s.expr) + + def format_assign(self, expr) -> str: + rhs = self.c_format(expr.rhs) + lhs = self.c_format(expr.lhs) + return f"{lhs} {expr.op} {rhs};\n" + + def format_conditional(self, s) -> str: + # Format children + c = self.c_format(s.condition) + t = self.c_format(s.true) + f = self.c_format(s.false) + + # Apply parentheses + if s.condition.precedence >= s.precedence: + c = "(" + c + ")" + if s.true.precedence >= s.precedence: + t = "(" + t + ")" + if s.false.precedence >= s.precedence: + f = "(" + f + ")" + + # Return combined string + return c + " ? " + t + " : " + f + + def format_symbol(self, s) -> str: + return f"{s.name}" + + def format_math_function(self, c) -> str: + # Get a function from the table, if available, else just use bare name + func = math_table.get(c.function, c.function) + args = ", ".join(self.c_format(arg) for arg in c.args) + return f"{func}({args})" + + c_impl = { + "Section": format_section, + "StatementList": format_statement_list, + "Comment": format_comment, + "ArrayDecl": format_array_decl, + "ArrayAccess": format_array_access, + "MultiIndex": format_multi_index, + "VariableDecl": format_variable_decl, + "ForRange": format_for_range, + "Statement": format_statement, + "Assign": format_assign, + "AssignAdd": format_assign, + "Product": format_nary_op, + "Neg": format_neg, + "Sum": format_nary_op, + "Add": format_binary_op, + "Sub": format_binary_op, + "Mul": format_binary_op, + "Div": format_binary_op, + "Not": format_not, + "LiteralFloat": format_literal_float, + "LiteralInt": format_literal_int, + "Symbol": format_symbol, + "Conditional": format_conditional, + "MathFunction": format_math_function, + "And": format_binary_op, + "Or": format_binary_op, + "NE": format_binary_op, + "EQ": format_binary_op, + "GE": format_binary_op, + "LE": format_binary_op, + "GT": format_binary_op, + "LT": format_binary_op, + } + + def c_format(self, s) -> str: + name = s.__class__.__name__ + try: + return self.c_impl[name](self, s) + except KeyError: + raise RuntimeError("Unknown statement: ", name) diff --git a/ffcx/codegeneration/cpp/expressions.py b/ffcx/codegeneration/cpp/expressions.py new file mode 100644 index 000000000..9800620e6 --- /dev/null +++ b/ffcx/codegeneration/cpp/expressions.py @@ -0,0 +1,147 @@ +# Copyright (C) 2019 Michal Habera +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import logging + +from ffcx.codegeneration.C import expressions_template +from ffcx.codegeneration.backend import FFCXBackend +from ffcx.codegeneration.expression_generator import ExpressionGenerator +from ffcx.codegeneration.C.c_implementation import CFormatter + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + """Generate UFC code for an expression.""" + logger.info("Generating code for expression:") + logger.info(f"--- points: {ir.points}") + logger.info(f"--- name: {ir.name}") + + factory_name = ir.name + + # Format declaration + declaration = expressions_template.declaration.format( + factory_name=factory_name, name_from_uflfile=ir.name_from_uflfile + ) + + backend = FFCXBackend(ir, options) + eg = ExpressionGenerator(ir, backend) + + d = {} + d["name_from_uflfile"] = ir.name_from_uflfile + d["factory_name"] = ir.name + + parts = eg.generate() + + CF = CFormatter(options["scalar_type"]) + d["tabulate_expression"] = CF.c_format(parts) + + if len(ir.original_coefficient_positions) > 0: + d[ + "original_coefficient_positions" + ] = f"original_coefficient_positions_{ir.name}" + n = len(ir.original_coefficient_positions) + originals = ", ".join(str(i) for i in ir.original_coefficient_positions) + d[ + "original_coefficient_positions_init" + ] = f"static int original_coefficient_positions_{ir.name}[{n}] = {{{originals}}};" + + else: + d["original_coefficient_positions"] = "NULL" + d["original_coefficient_positions_init"] = "" + + points = ", ".join(str(p) for p in ir.points.flatten()) + n = ir.points.size + d["points_init"] = f"static double points_{ir.name}[{n}] = {{{points}}};" + d["points"] = f"points_{ir.name}" + + if len(ir.expression_shape) > 0: + n = len(ir.expression_shape) + shape = ", ".join(str(i) for i in ir.expression_shape) + d["value_shape_init"] = f"static int value_shape_{ir.name}[{n}] = {{{shape}}};" + d["value_shape"] = f"value_shape_{ir.name}" + else: + d["value_shape_init"] = "" + d["value_shape"] = "NULL" + + d["num_components"] = len(ir.expression_shape) + d["num_coefficients"] = len(ir.coefficient_numbering) + d["num_constants"] = len(ir.constant_names) + d["num_points"] = ir.points.shape[0] + d["topological_dimension"] = ir.points.shape[1] + d["rank"] = len(ir.tensor_shape) + + if len(ir.coefficient_names) > 0: + names = ", ".join(f'"{name}"' for name in ir.coefficient_names) + n = len(ir.coefficient_names) + d[ + "coefficient_names_init" + ] = f"static const char* coefficient_names_{ir.name}[{n}] = {{{names}}};" + + d["coefficient_names"] = f"coefficient_names_{ir.name}" + else: + d["coefficient_names_init"] = "" + d["coefficient_names"] = "NULL" + + if len(ir.constant_names) > 0: + names = ", ".join(f'"{name}"' for name in ir.constant_names) + n = len(ir.constant_names) + d[ + "constant_names_init" + ] = f"static const char* constant_names_{ir.name}[{n}] = {{{names}}};" + d["constant_names"] = f"constant_names_{ir.name}" + else: + d["constant_names_init"] = "" + d["constant_names"] = "NULL" + + code = [] + + # FIXME: Should be handled differently, revise how + # ufcx_function_space is generated (also for ufcx_form) + for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): + code += [ + f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} =" + ] + code += ["{"] + code += [f".finite_element = &{element},"] + code += [f".dofmap = &{dofmap},"] + code += [f'.geometry_family = "{cmap_family}",'] + code += [f".geometry_degree = {cmap_degree}"] + code += ["};"] + + d["function_spaces_alloc"] = "\n".join(code) + d["function_spaces"] = "" + + if len(ir.function_spaces) > 0: + d["function_spaces"] = f"function_spaces_{ir.name}" + fs_list = ", ".join( + f"&function_space_{name}_{ir.name_from_uflfile}" + for (name, _) in ir.function_spaces.items() + ) + n = len(ir.function_spaces.items()) + d[ + "function_spaces_init" + ] = f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" + else: + d["function_spaces"] = "NULL" + d["function_spaces_init"] = "" + + # Check that no keys are redundant or have been missed + from string import Formatter + + fields = [ + fname + for _, fname, _, _ in Formatter().parse(expressions_template.factory) + if fname + ] + assert set(fields) == set( + d.keys() + ), "Mismatch between keys in template and in formatting dict" + + # Format implementation code + implementation = expressions_template.factory.format_map(d) + + return declaration, implementation diff --git a/ffcx/codegeneration/cpp/expressions_template.py b/ffcx/codegeneration/cpp/expressions_template.py new file mode 100644 index 000000000..939d3140f --- /dev/null +++ b/ffcx/codegeneration/cpp/expressions_template.py @@ -0,0 +1,60 @@ +# Copyright (C) 2019 Michal Habera +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +declaration = """ +extern ufcx_expression {factory_name}; + +// Helper used to create expression using name which was given to the +// expression in the UFL file. +// This helper is called in user c++ code. +// +extern ufcx_expression* {name_from_uflfile}; +""" + +factory = """ +// Code for expression {factory_name} + +void tabulate_tensor_{factory_name}({scalar_type}* restrict A, + const {scalar_type}* restrict w, + const {scalar_type}* restrict c, + const {geom_type}* restrict coordinate_dofs, + const int* restrict entity_local_index, + const uint8_t* restrict quadrature_permutation) +{{ +{tabulate_expression} +}} + +{points_init} +{value_shape_init} +{original_coefficient_positions_init} +{function_spaces_alloc} +{function_spaces_init} +{coefficient_names_init} +{constant_names_init} + + +ufcx_expression {factory_name} = +{{ + .tabulate_tensor_{np_scalar_type} = tabulate_tensor_{factory_name}, + .num_coefficients = {num_coefficients}, + .num_constants = {num_constants}, + .original_coefficient_positions = {original_coefficient_positions}, + .coefficient_names = {coefficient_names}, + .constant_names = {constant_names}, + .num_points = {num_points}, + .topological_dimension = {topological_dimension}, + .points = {points}, + .value_shape = {value_shape}, + .num_components = {num_components}, + .rank = {rank}, + .function_spaces = {function_spaces} +}}; + +// Alias name +ufcx_expression* {name_from_uflfile} = &{factory_name}; + +// End of code for expression {factory_name} +""" diff --git a/ffcx/codegeneration/cpp/file.py b/ffcx/codegeneration/cpp/file.py new file mode 100644 index 000000000..983754ee3 --- /dev/null +++ b/ffcx/codegeneration/cpp/file.py @@ -0,0 +1,48 @@ +# Copyright (C) 2009-2018 Anders Logg, Martin Sandve Alnæs and Garth N. Wells +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Note: Most of the code in this file is a direct translation from the +# old implementation in FFC + +import logging +import pprint +import textwrap + +from ffcx.codegeneration.cpp import file_template +from ffcx import __version__ as FFCX_VERSION +from ffcx.codegeneration import __version__ as UFC_VERSION + + +logger = logging.getLogger("ffcx") + + +def generator(options): + """Generate UFC code for file output.""" + logger.info("Generating code for file") + + # Attributes + d = {"ffcx_version": FFCX_VERSION, "ufcx_version": UFC_VERSION} + d["options"] = textwrap.indent(pprint.pformat(options), "// ") + extra_includes = [] + if "_Complex" in options["scalar_type"]: + extra_includes += ["complex"] + d["extra_includes"] = "\n".join( + f"#include <{header}>" for header in extra_includes + ) + + # Format declaration code + code_pre = ( + file_template.declaration_pre.format_map(d), + file_template.implementation_pre.format_map(d), + ) + + # Format implementation code + code_post = ( + file_template.declaration_post.format_map(d), + file_template.implementation_post.format_map(d), + ) + + return code_pre, code_post diff --git a/ffcx/codegeneration/cpp/file_template.py b/ffcx/codegeneration/cpp/file_template.py new file mode 100644 index 000000000..4a4affc89 --- /dev/null +++ b/ffcx/codegeneration/cpp/file_template.py @@ -0,0 +1,37 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2018. + +declaration_pre = """ +// This code conforms with the UFC specification version {ufcx_version} +// and was automatically generated by FFCx version {ffcx_version}. +// +// This code was generated with the following options: +// +{options} + +#pragma once +#include +#include + +""" + +declaration_post = """ +""" + +implementation_pre = """ +// This code conforms with the UFC specification version {ufcx_version} +// and was automatically generated by FFCx version {ffcx_version}. +// +// This code was generated with the following options: +// +{options} + +#include +#include +{extra_includes} + +""" + +implementation_post = "" diff --git a/ffcx/codegeneration/cpp/form.py b/ffcx/codegeneration/cpp/form.py new file mode 100644 index 000000000..1d056b986 --- /dev/null +++ b/ffcx/codegeneration/cpp/form.py @@ -0,0 +1,23 @@ +# Copyright (C) 2009-2017 Anders Logg and Martin Sandve Alnæs +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +# Note: Most of the code in this file is a direct translation from the +# old implementation in FFC + +import logging + +# from ffcx.codegeneration.cpp import form_template + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + """Generate UFC code for a form.""" + logger.info("Generating code for form:") + logger.info(f"--- rank: {ir.rank}") + logger.info(f"--- name: {ir.name}") + + return ("", "") diff --git a/ffcx/codegeneration/cpp/form_template.py b/ffcx/codegeneration/cpp/form_template.py new file mode 100644 index 000000000..35d4829af --- /dev/null +++ b/ffcx/codegeneration/cpp/form_template.py @@ -0,0 +1,48 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2020. + +declaration = """ +extern ufcx_form {factory_name}; + +// Helper used to create form using name which was given to the +// form in the UFL file. +// This helper is called in user c++ code. +// +extern ufcx_form* {name_from_uflfile}; + +// Helper used to create function space using function name +// i.e. name of the Python variable. +// +ufcx_function_space* functionspace_{name_from_uflfile}(const char* function_name); +""" + +factory = """ +// Code for form {factory_name} + +{dofmaps_init} +{finite_elements_init} + +{name_from_uflfile}::constant_name = {constant_name_map}; +{name_from_uflfile}::coefficient_name = {coefficient_name_map}; +{name_from_uflfile}::signature ={signature}; +{name_from_uflfile}::rank = {rank}; +{name_from_uflfile}::num_coefficients = {num_coefficients}; +{name_from_uflfile}::num_constants = {num_constants}; +{name_from_uflfile}::original_coefficient_position = {original_coefficient_position}; +{name_from_uflfile}::coefficient_name_map = coefficient_name_{factory_name}; +{name_from_uflfile}::constant_name_map = constant_name_{factory_name}; +{name_from_uflfile}::finite_elements = {finite_elements}; +{name_from_uflfile}::dofmaps = {dofmaps}; +{name_from_uflfile}::form_integrals = {form_integrals}; +{name_from_uflfile}::form_integral_ids = {form_integral_ids}; +{name_from_uflfile}::form_integral_offsets = {form_integral_offsets}; + +ufcx_function_space* functionspace_{name_from_uflfile}(const char* function_name) +{{ +{functionspace} +}} + +// End of code for form {factory_name} +""" diff --git a/ffcx/codegeneration/cpp/integrals.py b/ffcx/codegeneration/cpp/integrals.py new file mode 100644 index 000000000..8f758f946 --- /dev/null +++ b/ffcx/codegeneration/cpp/integrals.py @@ -0,0 +1,64 @@ +# Copyright (C) 2015-2021 Martin Sandve Alnæs, Michal Habera, Igor Baratta +# +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import logging + +from ffcx.codegeneration.integral_generator import IntegralGenerator +from ffcx.codegeneration.cpp import integrals_template as ufcx_integrals +from ffcx.codegeneration.backend import FFCXBackend +from ffcx.codegeneration.cpp.cpp_implementation import CppFormatter + + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + logger.info("Generating code for integral:") + logger.info(f"--- type: {ir.integral_type}") + logger.info(f"--- name: {ir.name}") + + """Generate code for an integral.""" + factory_name = ir.name + + # Format declaration + declaration = ufcx_integrals.declaration.format(factory_name=factory_name) + + # Create FFCx C backend + backend = FFCXBackend(ir, options) + + # Configure kernel generator + ig = IntegralGenerator(ir, backend) + + # Generate code ast for the tabulate_tensor body + parts = ig.generate() + + # Format code as string + CF = CppFormatter(options["scalar_type"]) + body = CF.c_format(parts) + + # Generate generic FFCx code snippets and add specific parts + code = {} + code["class_type"] = ir.integral_type + "_integral" + code["name"] = ir.name + + vals = ", ".join("true" if i else "false" for i in ir.enabled_coefficients) + code["enabled_coefficients"] = f"{{{vals}}}" + + code["additional_includes_set"] = set() # FIXME: Get this out of code[] + code["tabulate_tensor"] = body + + implementation = ufcx_integrals.factory.format( + factory_name=factory_name, + enabled_coefficients=code["enabled_coefficients"], + tabulate_tensor=code["tabulate_tensor"], + needs_facet_permutations="true" if ir.needs_facet_permutations else "false", + scalar_type=options["scalar_type"], + geom_type=options["scalar_type"], + np_scalar_type=options["scalar_type"], + coordinate_element=ir.coordinate_element, + ) + + return declaration + implementation, "" diff --git a/ffcx/codegeneration/cpp/integrals_template.py b/ffcx/codegeneration/cpp/integrals_template.py new file mode 100644 index 000000000..a07b49132 --- /dev/null +++ b/ffcx/codegeneration/cpp/integrals_template.py @@ -0,0 +1,51 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2018 + +declaration = """ +class {factory_name} +{{ +public: + +// Constructor +{factory_name}(); + +// Kernel +template +void tabulate_tensor(T* A, + const T* w, + const T* c, + const U* coordinate_dofs, + const int* entity_local_index, + const uint8_t* quadrature_permutation); + +// Data +std::vector enabled_coefficients; +bool needs_facet_permutations; + +}}; +""" + +factory = """ +// Code for integral {factory_name} + +template +void {factory_name}::tabulate_tensor(T* A, + const T* w, + const T* c, + const U* coordinate_dofs, + const int* entity_local_index, + const uint8_t* quadrature_permutation) +{{ +{tabulate_tensor} +}} + +{factory_name}::{factory_name}() +{{ + enabled_coefficients = {enabled_coefficients}; + needs_facet_permutations = {needs_facet_permutations}; +}} + +// End of code for integral {factory_name} +""" diff --git a/ffcx/main.py b/ffcx/main.py index 2cb2551c8..15b0cb900 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -28,6 +28,7 @@ ) parser.add_argument("--version", action="version", version=f"%(prog)s (version {FFCX_VERSION})") parser.add_argument("-o", "--output-directory", type=str, default=".", help="output directory") +parser.add_argument("-L", "--language", type=str, default="C", help="target language") parser.add_argument("--visualise", action="store_true", help="visualise the IR graph") parser.add_argument("-p", "--profile", action="store_true", help="enable profiling") From 620321a4c5545a5389c2091bb9217c096e9144a6 Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 30 Apr 2024 12:19:06 +0100 Subject: [PATCH 002/106] Fixes for ruff --- ffcx/codegeneration/C/__init__.py | 2 +- ffcx/codegeneration/codegeneration.py | 2 +- ffcx/codegeneration/cpp/cpp_implementation.py | 25 ++++++++++++++++++- ffcx/codegeneration/cpp/expressions.py | 5 ++-- .../cpp/expressions_template.py | 3 ++- ffcx/codegeneration/cpp/file.py | 4 +-- ffcx/codegeneration/cpp/file_template.py | 1 + ffcx/codegeneration/cpp/form.py | 1 + ffcx/codegeneration/cpp/form_template.py | 1 + ffcx/codegeneration/cpp/integrals.py | 8 +++--- ffcx/codegeneration/cpp/integrals_template.py | 1 + 11 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ffcx/codegeneration/C/__init__.py b/ffcx/codegeneration/C/__init__.py index 214752d4e..94ed6e940 100644 --- a/ffcx/codegeneration/C/__init__.py +++ b/ffcx/codegeneration/C/__init__.py @@ -1,3 +1,3 @@ """Generation of C code.""" from ffcx.codegeneration.C import form, expressions, integrals, file # noqa -suffixes = (".h", ".c") \ No newline at end of file +suffixes = (".h", ".c") diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 860ba9ab2..c233ebdae 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -13,8 +13,8 @@ from __future__ import annotations import logging -import typing import sys +import typing from importlib import import_module import numpy.typing as npt diff --git a/ffcx/codegeneration/cpp/cpp_implementation.py b/ffcx/codegeneration/cpp/cpp_implementation.py index 0c3aa0996..a39220ee6 100644 --- a/ffcx/codegeneration/cpp/cpp_implementation.py +++ b/ffcx/codegeneration/cpp/cpp_implementation.py @@ -3,6 +3,7 @@ # This file is part of FFCx. (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""C++ implementation.""" import ffcx.codegeneration.lnodes as L @@ -36,6 +37,7 @@ def build_initializer_lists(values): + """Build initializer lists.""" arr = "{" if len(values.shape) == 1: return "{" + ", ".join(str(v) for v in values) + "}" @@ -45,12 +47,15 @@ def build_initializer_lists(values): return arr -class CppFormatter(object): +class CppFormatter: + """C++ formatter.""" def __init__(self, scalar) -> None: + """Initialise.""" self.scalar_type = "T" self.real_type = "U" def format_statement_list(self, slist) -> str: + """Format statement list.""" return "".join(self.c_format(s) for s in slist.statements) def format_section(self, section) -> str: @@ -73,9 +78,11 @@ def format_section(self, section) -> str: return comments + declarations + body def format_comment(self, c) -> str: + """Format a comment.""" return "// " + c.comment + "\n" def format_array_decl(self, arr) -> str: + """Format an array declaration.""" dtype = arr.symbol.dtype assert dtype is not None @@ -99,14 +106,17 @@ def format_array_decl(self, arr) -> str: return f"{cstr}{typename} {symbol}{dims} = {vals};\n" def format_array_access(self, arr) -> str: + """Format array access.""" name = self.c_format(arr.array) indices = f"[{']['.join(self.c_format(i) for i in arr.indices)}]" return f"{name}{indices}" def format_multi_index(self, index) -> str: + """Format a multi-index.""" return self.c_format(index.global_index) def format_variable_decl(self, v) -> str: + """Format a variable declaration.""" val = self.c_format(v.value) symbol = self.c_format(v.symbol) assert v.symbol.dtype @@ -117,6 +127,7 @@ def format_variable_decl(self, v) -> str: return f"{typename} {symbol} = {val};\n" def format_nary_op(self, oper) -> str: + """Format an n-argument operation.""" # Format children args = [self.c_format(arg) for arg in oper.args] @@ -129,6 +140,7 @@ def format_nary_op(self, oper) -> str: return f" {oper.op} ".join(args) def format_binary_op(self, oper) -> str: + """Format a binary operation.""" # Format children lhs = self.c_format(oper.lhs) rhs = self.c_format(oper.rhs) @@ -143,20 +155,25 @@ def format_binary_op(self, oper) -> str: return f"{lhs} {oper.op} {rhs}" def format_neg(self, val) -> str: + """Format negation.""" arg = self.c_format(val.arg) return f"-{arg}" def format_not(self, val) -> str: + """Format 'not' statement.""" arg = self.c_format(val.arg) return f"{val.op}({arg})" def format_literal_float(self, val) -> str: + """Format a literal float number.""" return f"{val.value}" def format_literal_int(self, val) -> str: + """Format a literal int number.""" return f"{val.value}" def format_for_range(self, r) -> str: + """Format a loop over a range.""" begin = self.c_format(r.begin) end = self.c_format(r.end) index = self.c_format(r.index) @@ -170,14 +187,17 @@ def format_for_range(self, r) -> str: return output def format_statement(self, s) -> str: + """Format a statement.""" return self.c_format(s.expr) def format_assign(self, expr) -> str: + """Format an assignment statement.""" rhs = self.c_format(expr.rhs) lhs = self.c_format(expr.lhs) return f"{lhs} {expr.op} {rhs};\n" def format_conditional(self, s) -> str: + """Format a conditional.""" # Format children c = self.c_format(s.condition) t = self.c_format(s.true) @@ -195,9 +215,11 @@ def format_conditional(self, s) -> str: return c + " ? " + t + " : " + f def format_symbol(self, s) -> str: + """Format a symbol.""" return f"{s.name}" def format_math_function(self, c) -> str: + """Format a math function.""" # Get a function from the table, if available, else just use bare name func = math_table.get(c.function, c.function) args = ", ".join(self.c_format(arg) for arg in c.args) @@ -239,6 +261,7 @@ def format_math_function(self, c) -> str: } def c_format(self, s) -> str: + """Formatting function.""" name = s.__class__.__name__ try: return self.c_impl[name](self, s) diff --git a/ffcx/codegeneration/cpp/expressions.py b/ffcx/codegeneration/cpp/expressions.py index 9800620e6..9288eb50a 100644 --- a/ffcx/codegeneration/cpp/expressions.py +++ b/ffcx/codegeneration/cpp/expressions.py @@ -3,13 +3,14 @@ # This file is part of FFCx.(https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Generate code for an expression.""" import logging -from ffcx.codegeneration.C import expressions_template from ffcx.codegeneration.backend import FFCXBackend -from ffcx.codegeneration.expression_generator import ExpressionGenerator +from ffcx.codegeneration.C import expressions_template from ffcx.codegeneration.C.c_implementation import CFormatter +from ffcx.codegeneration.expression_generator import ExpressionGenerator logger = logging.getLogger("ffcx") diff --git a/ffcx/codegeneration/cpp/expressions_template.py b/ffcx/codegeneration/cpp/expressions_template.py index 939d3140f..9b25cbee2 100644 --- a/ffcx/codegeneration/cpp/expressions_template.py +++ b/ffcx/codegeneration/cpp/expressions_template.py @@ -3,6 +3,7 @@ # This file is part of FFCx.(https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Code generation strings for an expression.""" declaration = """ extern ufcx_expression {factory_name}; @@ -45,7 +46,7 @@ .coefficient_names = {coefficient_names}, .constant_names = {constant_names}, .num_points = {num_points}, - .topological_dimension = {topological_dimension}, + .entity_dimension = {entity_dimension}, .points = {points}, .value_shape = {value_shape}, .num_components = {num_components}, diff --git a/ffcx/codegeneration/cpp/file.py b/ffcx/codegeneration/cpp/file.py index 983754ee3..259a56673 100644 --- a/ffcx/codegeneration/cpp/file.py +++ b/ffcx/codegeneration/cpp/file.py @@ -6,15 +6,15 @@ # # Note: Most of the code in this file is a direct translation from the # old implementation in FFC +"""Generate code for file output.""" import logging import pprint import textwrap -from ffcx.codegeneration.cpp import file_template from ffcx import __version__ as FFCX_VERSION from ffcx.codegeneration import __version__ as UFC_VERSION - +from ffcx.codegeneration.cpp import file_template logger = logging.getLogger("ffcx") diff --git a/ffcx/codegeneration/cpp/file_template.py b/ffcx/codegeneration/cpp/file_template.py index 4a4affc89..ed8128f21 100644 --- a/ffcx/codegeneration/cpp/file_template.py +++ b/ffcx/codegeneration/cpp/file_template.py @@ -2,6 +2,7 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2018. +"""Templates for C++ file output.""" declaration_pre = """ // This code conforms with the UFC specification version {ufcx_version} diff --git a/ffcx/codegeneration/cpp/form.py b/ffcx/codegeneration/cpp/form.py index 1d056b986..ac8f05969 100644 --- a/ffcx/codegeneration/cpp/form.py +++ b/ffcx/codegeneration/cpp/form.py @@ -6,6 +6,7 @@ # Note: Most of the code in this file is a direct translation from the # old implementation in FFC +"""Generate code for a form.""" import logging diff --git a/ffcx/codegeneration/cpp/form_template.py b/ffcx/codegeneration/cpp/form_template.py index 35d4829af..2552db6c7 100644 --- a/ffcx/codegeneration/cpp/form_template.py +++ b/ffcx/codegeneration/cpp/form_template.py @@ -2,6 +2,7 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2020. +"""Templates for C++ form output.""" declaration = """ extern ufcx_form {factory_name}; diff --git a/ffcx/codegeneration/cpp/integrals.py b/ffcx/codegeneration/cpp/integrals.py index 8f758f946..4f14bc713 100644 --- a/ffcx/codegeneration/cpp/integrals.py +++ b/ffcx/codegeneration/cpp/integrals.py @@ -3,24 +3,24 @@ # This file is part of FFCx. (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Integral generation.""" import logging -from ffcx.codegeneration.integral_generator import IntegralGenerator -from ffcx.codegeneration.cpp import integrals_template as ufcx_integrals from ffcx.codegeneration.backend import FFCXBackend +from ffcx.codegeneration.cpp import integrals_template as ufcx_integrals from ffcx.codegeneration.cpp.cpp_implementation import CppFormatter - +from ffcx.codegeneration.integral_generator import IntegralGenerator logger = logging.getLogger("ffcx") def generator(ir, options): + """Generate code for an integral.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.integral_type}") logger.info(f"--- name: {ir.name}") - """Generate code for an integral.""" factory_name = ir.name # Format declaration diff --git a/ffcx/codegeneration/cpp/integrals_template.py b/ffcx/codegeneration/cpp/integrals_template.py index a07b49132..110c0cb1f 100644 --- a/ffcx/codegeneration/cpp/integrals_template.py +++ b/ffcx/codegeneration/cpp/integrals_template.py @@ -2,6 +2,7 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2018 +"""Templates for C++ integral output.""" declaration = """ class {factory_name} From c8ac53a2d90c71aaa4550e5b64d5e09d38b3c51b Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 30 Apr 2024 13:56:11 +0100 Subject: [PATCH 003/106] Add numba --- ffcx/codegeneration/codegeneration.py | 11 +- ffcx/codegeneration/numba/__init__.py | 3 + ffcx/codegeneration/numba/expressions.py | 130 +++++++++++ .../numba/expressions_template.py | 37 +++ ffcx/codegeneration/numba/file.py | 42 ++++ ffcx/codegeneration/numba/file_template.py | 56 +++++ ffcx/codegeneration/numba/form.py | 104 +++++++++ ffcx/codegeneration/numba/form_template.py | 32 +++ ffcx/codegeneration/numba/integrals.py | 73 ++++++ .../numba/integrals_template.py | 20 ++ .../numba/numba_implementation.py | 217 ++++++++++++++++++ 11 files changed, 719 insertions(+), 6 deletions(-) create mode 100644 ffcx/codegeneration/numba/__init__.py create mode 100644 ffcx/codegeneration/numba/expressions.py create mode 100644 ffcx/codegeneration/numba/expressions_template.py create mode 100644 ffcx/codegeneration/numba/file.py create mode 100644 ffcx/codegeneration/numba/file_template.py create mode 100644 ffcx/codegeneration/numba/form.py create mode 100644 ffcx/codegeneration/numba/form_template.py create mode 100644 ffcx/codegeneration/numba/integrals.py create mode 100644 ffcx/codegeneration/numba/integrals_template.py create mode 100644 ffcx/codegeneration/numba/numba_implementation.py diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index c233ebdae..64a9ba355 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -44,14 +44,13 @@ def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) - logger.info(79 * "*") lang = options.get("language", "C") - try: - # Built-in - mod = import_module(f"ffcx.codegeneration.{lang}") + # Built-in + mod = import_module(f"ffcx.codegeneration.{lang}") except ImportError: - # User defined, in current directory - sys.path.append(".") - mod = import_module(f"{lang}") + # User defined, in current directory + sys.path.append(".") + mod = import_module(f"{lang}") integral_generator = mod.integrals.generator form_generator = mod.form.generator diff --git a/ffcx/codegeneration/numba/__init__.py b/ffcx/codegeneration/numba/__init__.py new file mode 100644 index 000000000..354a02e9d --- /dev/null +++ b/ffcx/codegeneration/numba/__init__.py @@ -0,0 +1,3 @@ +from ffcx.codegeneration.numba import form, expressions, integrals, file # noqa + +suffixes = (None, "_numba.py") diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py new file mode 100644 index 000000000..1b7b45554 --- /dev/null +++ b/ffcx/codegeneration/numba/expressions.py @@ -0,0 +1,130 @@ +# Copyright (C) 2019 Michal Habera +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import logging + +from ffcx.codegeneration.numba import expressions_template +from ffcx.codegeneration.backend import FFCXBackend +from ffcx.codegeneration.expression_generator import ExpressionGenerator +from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + """Generate UFC code for an expression.""" + logger.info("Generating code for expression:") + logger.info(f"--- points: {ir.points}") + logger.info(f"--- name: {ir.name}") + + factory_name = ir.name + + # Format declaration + declaration = expressions_template.declaration.format( + factory_name=factory_name, name_from_uflfile=ir.name_from_uflfile + ) + + backend = FFCXBackend(ir, options) + eg = ExpressionGenerator(ir, backend) + + d = {} + d["name_from_uflfile"] = ir.name_from_uflfile + d["factory_name"] = ir.name + + parts = eg.generate() + + tensor_size = 1 + for dim in ir.tensor_shape: + tensor_size *= dim + n_coeff = 1000 + n_const = 1000 + header = f""" + A = numba.carray(_A, ({tensor_size})) + w = numba.carray(_w, ({n_coeff})) + c = numba.carray(_c, ({n_const})) + coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) + entity_local_index = numba.carray(_entity_local_index, (1000)) + quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) + """ + F = NumbaFormatter(options["scalar_type"]) + body = F.c_format(parts) + body = [" " + line for line in body.split("\n")] + body = "\n".join(body) + + d["tabulate_expression"] = header + body + + originals = ", ".join(str(i) for i in ir.original_coefficient_positions) + d["original_coefficient_positions"] = f"[{originals}]" + points = ", ".join(str(p) for p in ir.points.flatten()) + n = ir.points.size + d["points"] = f"[{points}]" + + shape = ", ".join(str(i) for i in ir.expression_shape) + d["value_shape"] = f"[{shape}]" + d["num_components"] = len(ir.expression_shape) + d["num_coefficients"] = len(ir.coefficient_numbering) + d["num_constants"] = len(ir.constant_names) + d["num_points"] = ir.points.shape[0] + d["topological_dimension"] = ir.points.shape[1] + d["scalar_type"] = options["scalar_type"] + + d["rank"] = len(ir.tensor_shape) + + names = ", ".join(f'"{name}"' for name in ir.coefficient_names) + d["coefficient_names"] = f"[{names}]" + names = ", ".join(f'"{name}"' for name in ir.constant_names) + d["constant_names"] = f"[{names}]" + + code = [] + + # FIXME: Should be handled differently, revise how + # ufcx_function_space is generated (also for ufcx_form) + for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): + code += [ + f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} =" + ] + code += ["{"] + code += [f".finite_element = &{element},"] + code += [f".dofmap = &{dofmap},"] + code += [f'.geometry_family = "{cmap_family}",'] + code += [f".geometry_degree = {cmap_degree}"] + code += ["};"] + + d["function_spaces_alloc"] = "\n".join(code) + d["function_spaces"] = "" + + if len(ir.function_spaces) > 0: + d["function_spaces"] = f"function_spaces_{ir.name}" + fs_list = ", ".join( + f"&function_space_{name}_{ir.name_from_uflfile}" + for (name, _) in ir.function_spaces.items() + ) + n = len(ir.function_spaces.items()) + d[ + "function_spaces_init" + ] = f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" + else: + d["function_spaces"] = "NULL" + d["function_spaces_init"] = "" + + d["function_spaces"] = "0" + + # Check that no keys are redundant or have been missed + # from string import Formatter + + # fields = [ + # fname + # for _, fname, _, _ in Formatter().parse(expressions_template.factory) + # if fname + # ] + # assert set(fields) == set( + # d.keys() + # ), "Mismatch between keys in template and in formatting dict" + + # Format implementation code + implementation = expressions_template.factory.format_map(d) + + return declaration, implementation diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py new file mode 100644 index 000000000..693961313 --- /dev/null +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -0,0 +1,37 @@ +# Copyright (C) 2019 Michal Habera +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +declaration = """ +""" + +factory = """ +# Code for expression {factory_name} + +def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, + _entity_local_index, + _quadrature_permutation): +{tabulate_expression} + + + +class {factory_name}: + + tabulate_tensor_{np_scalar_type} = tabulate_tensor_{factory_name} + num_coefficients = {num_coefficients} + num_constants = {num_constants} + original_coefficient_positions = {original_coefficient_positions} + coefficient_names = {coefficient_names} + constant_names = {constant_names} + num_points = {num_points} + topological_dimension = {topological_dimension} + points = {points} + value_shape = {value_shape} + num_components = {num_components} + rank = {rank} + function_spaces = {function_spaces} + +# End of code for expression {factory_name} +""" diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py new file mode 100644 index 000000000..6e4d45cae --- /dev/null +++ b/ffcx/codegeneration/numba/file.py @@ -0,0 +1,42 @@ +# Copyright (C) 2009-2018 Anders Logg, Martin Sandve Alnæs and Garth N. Wells +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Note: Most of the code in this file is a direct translation from the +# old implementation in FFC + +import logging +import pprint +import textwrap + +from ffcx.codegeneration.numba import file_template +from ffcx import __version__ as FFCX_VERSION +from ffcx.codegeneration import __version__ as UFC_VERSION + + +logger = logging.getLogger("ffcx") + + +def generator(options): + """Generate UFC code for file output.""" + logger.info("Generating code for file") + + # Attributes + d = {"ffcx_version": FFCX_VERSION, "ufcx_version": UFC_VERSION} + d["options"] = textwrap.indent(pprint.pformat(options), "# ") + + # Format declaration code + code_pre = ( + file_template.declaration_pre.format_map(d), + file_template.implementation_pre.format_map(d), + ) + + # Format implementation code + code_post = ( + file_template.declaration_post.format_map(d), + file_template.implementation_post.format_map(d), + ) + + return code_pre, code_post diff --git a/ffcx/codegeneration/numba/file_template.py b/ffcx/codegeneration/numba/file_template.py new file mode 100644 index 000000000..d5a284ab2 --- /dev/null +++ b/ffcx/codegeneration/numba/file_template.py @@ -0,0 +1,56 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2018. + +declaration_pre = """ +""" + +declaration_post = "" + +implementation_pre = """ +# This code conforms with the UFC specification version {ufcx_version} +# and was automatically generated by FFCx version {ffcx_version}. +# +# This code was generated with the following options: +# +{options} + +import numba +import numpy as np +import math + +# ufcx enums +interval = 10 +triangle = 20 +quadrilateral = 30 +tetrahedron = 40 +hexahedron = 50 +vertex = 60 +prism = 70 +pyramid = 80 + +cell = 0 +exterior_facet = 1 +interior_facet = 2 + +ufcx_basix_element = 0 +ufcx_mixed_element = 1 +ufcx_quadrature_element = 2 +ufcx_basix_custom_element = 3 + +def wrapper(scalar_type, real_type): + + c_signature = numba.types.void( + numba.types.CPointer(numba.typeof(scalar_type())), + numba.types.CPointer(numba.typeof(scalar_type())), + numba.types.CPointer(numba.typeof(scalar_type())), + numba.types.CPointer(numba.typeof(real_type())), + numba.types.CPointer(numba.types.int32), + numba.types.CPointer(numba.types.int32)) + return numba.cfunc(c_signature, nopython=True) + +""" + +implementation_post = """ +""" diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py new file mode 100644 index 000000000..823897889 --- /dev/null +++ b/ffcx/codegeneration/numba/form.py @@ -0,0 +1,104 @@ +# Copyright (C) 2009-2017 Anders Logg and Martin Sandve Alnæs +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +# Note: Most of the code in this file is a direct translation from the +# old implementation in FFC + +import logging + +from ffcx.codegeneration.numba import form_template + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + """Generate UFC code for a form.""" + logger.info("Generating code for form:") + logger.info(f"--- rank: {ir.rank}") + logger.info(f"--- name: {ir.name}") + + d = {} + d["factory_name"] = ir.name + d["name_from_uflfile"] = ir.name_from_uflfile + d["signature"] = f'"{ir.signature}"' + d["rank"] = ir.rank + d["num_coefficients"] = ir.num_coefficients + d["num_constants"] = ir.num_constants + + orig_coeff = ', '.join(str(i) for i in ir.original_coefficient_position) + d["original_coefficient_position"] = f"[{orig_coeff}]" + + cnames = ir.coefficient_names + assert ir.num_coefficients == len(cnames) + names = ", ".join(f'"{name}"' for name in cnames) + d["coefficient_name_map"] = f"[{names}]" + + cstnames = ir.constant_names + names = ", ".join(f'"{name}"' for name in cstnames) + d["constant_name_map"] = f"[{names}]" + + integrals = [] + integral_ids = [] + integral_offsets = [0] + for itg_type in ("cell", "exterior_facet", "interior_facet"): + integrals += ir.integral_names[itg_type] + integral_ids += ir.subdomain_ids[itg_type] + integral_offsets.append(len(integrals)) + + integrals_str = ", ".join(itg for itg in integrals) + integral_ids_str = ", ".join(str(i) for i in integral_ids) + d["form_integrals"] = f"[{integrals_str}]" + d["form_integral_ids"] = f"[{integral_ids_str}]" + offsets = ", ".join(str(i) for i in integral_offsets) + d["form_integral_offsets"] = f"[{offsets}]" + + code = [] + + # FIXME: Should be handled differently, revise how + # ufcx_function_space is generated + for name, ( + element, + dofmap, + cmap_family, + cmap_degree, + cmap_celltype, + cmap_variant, + ) in ir.function_spaces.items(): + code += [f" functionspace_{name} = {{"] + code += [f" finite_element: {element},"] + code += [f" dofmap: {dofmap},"] + code += [f' geometry_family: "{cmap_family}",'] + code += [f" geometry_degree: {cmap_degree},"] + code += [f" geometry_basix_cell: {int(cmap_celltype)},"] + code += [" }"] + + for name in ir.function_spaces.keys(): + code += [f' if function_name=="{name}": return functionspace_{name}'] + + d["functionspace"] = "\n".join(code) + + # Check that no keys are redundant or have been missed + from string import Formatter + + fields = [ + fname for _, fname, _, _ in Formatter().parse(form_template.factory) if fname + ] + + for f in fields: + if f not in d.keys(): + print(f, "not in d.keys()") + + for f in d.keys(): + if f not in fields: + print(f, "not in fields") + + if set(fields) != set(d.keys()): + print("Mismatch between keys in template and in formatting dict") + + # Format implementation code + implementation = form_template.factory.format_map(d) + + return "", implementation diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py new file mode 100644 index 000000000..bc8625cd7 --- /dev/null +++ b/ffcx/codegeneration/numba/form_template.py @@ -0,0 +1,32 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2020. + + +factory = """ +# Code for form {factory_name} + +class {factory_name}(object): + + signature = {signature} + rank = {rank} + num_coefficients = {num_coefficients} + num_constants = {num_constants} + original_coefficient_position = {original_coefficient_position} + + coefficient_name_map = {coefficient_name_map} + constant_name_map = {constant_name_map} + + form_integrals = {form_integrals} + form_integral_ids = {form_integral_ids} + form_integral_offsets = {form_integral_offsets} + +def function_space_{name_from_uflfile}(name): +{functionspace} + +{name_from_uflfile} = {factory_name} + +# Name: {name_from_uflfile} +# End of code for form {factory_name} +""" diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py new file mode 100644 index 000000000..e6c7a7c9c --- /dev/null +++ b/ffcx/codegeneration/numba/integrals.py @@ -0,0 +1,73 @@ +# Copyright (C) 2015-2021 Martin Sandve Alnæs, Michal Habera, Igor Baratta +# +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import logging + +from ffcx.codegeneration.integral_generator import IntegralGenerator +from ffcx.codegeneration.numba import integrals_template as ufcx_integrals +from ffcx.codegeneration.backend import FFCXBackend +from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter + + +logger = logging.getLogger("ffcx") + + +def generator(ir, options): + logger.info("Generating code for integral:") + logger.info(f"--- type: {ir.integral_type}") + logger.info(f"--- name: {ir.name}") + + """Generate code for an integral.""" + factory_name = ir.name + + # Create FFCx backend + backend = FFCXBackend(ir, options) + + # Configure kernel generator + ig = IntegralGenerator(ir, backend) + + # Generate code AST for the tabulate_tensor body + parts = ig.generate() + + # Format code as string + F = NumbaFormatter(options["scalar_type"]) + body = F.c_format(parts) + body = [" " + line for line in body.split("\n")] + body = "\n".join(body) + + # Generate generic FFCx code snippets and add specific parts + code = {} + code["class_type"] = ir.integral_type + "_integral" + code["name"] = ir.name + + vals = ", ".join("1" if i else "0" for i in ir.enabled_coefficients) + code["enabled_coefficients"] = f"[{vals}]" + + tensor_size = 1 + for dim in ir.tensor_shape: + tensor_size *= dim + n_coeff = len(ir.enabled_coefficients) + n_const = len(ir.original_constant_offsets) + header = f""" + A = numba.carray(_A, ({tensor_size})) + w = numba.carray(_w, ({n_coeff})) + c = numba.carray(_c, ({n_const})) + coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) + entity_local_index = numba.carray(_entity_local_index, (1000)) + quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) + """ + code["tabulate_tensor"] = header + body + + implementation = ufcx_integrals.factory.format( + factory_name=factory_name, + enabled_coefficients=code["enabled_coefficients"], + tabulate_tensor=code["tabulate_tensor"], + needs_facet_permutations="True" if ir.needs_facet_permutations else "False", + scalar_type=options["scalar_type"], + coordinate_element=ir.coordinate_element, + ) + + return "", implementation diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py new file mode 100644 index 000000000..5538c4412 --- /dev/null +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -0,0 +1,20 @@ +# Code generation format strings for UFC (Unified Form-assembly Code) +# This code is released into the public domain. +# +# The FEniCS Project (http://www.fenicsproject.org/) 2018 + +factory = """ +# Code for integral {factory_name} +import numba + +def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, _entity_local_index, _quadrature_permutation): +{tabulate_tensor} + +class {factory_name}(object): + enabled_coefficients = {enabled_coefficients} + tabulate_tensor = tabulate_tensor_{factory_name} + needs_facet_permutations = {needs_facet_permutations} + coordinate_element = {coordinate_element} + +# End of code for integral {factory_name} +""" diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py new file mode 100644 index 000000000..074cd2809 --- /dev/null +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -0,0 +1,217 @@ + +import ffcx.codegeneration.lnodes as L + + +def build_initializer_lists(values): + arr = "[" + if len(values.shape) == 1: + return "[" + ", ".join(str(v) for v in values) + "]" + elif len(values.shape) > 1: + arr += ",\n".join(build_initializer_lists(v) for v in values) + arr += "]" + return arr + + +class NumbaFormatter(object): + def __init__(self, scalar) -> None: + self.scalar_type = scalar + + def format_section(self, section): + # add new line before section + comments = "# ------------------------ \n" + comments += "# Section: " + section.name + "\n" + comments += "# Inputs: " + ", ".join(w.name for w in section.input) + "\n" + comments += "# Outputs: " + ", ".join(w.name for w in section.output) + "\n" + declarations = "".join(self.c_format(s) for s in section.declarations) + + body = "" + if len(section.statements) > 0: + body = "".join(self.c_format(s) for s in section.statements) + + body += "# ------------------------ \n" + return comments + declarations + body + + def format_statement_list(self, slist): + output = "" + for s in slist.statements: + output += self.c_format(s) + return output + + def format_comment(self, c): + return "# " + c.comment + "\n" + + def format_array_decl(self, arr): + if arr.symbol.dtype == L.DataType.SCALAR: + dtype = "A.dtype" + elif arr.symbol.dtype == L.DataType.REAL: + dtype = "coordinate_dofs.dtype" + elif arr.symbol.dtype == L.DataType.INT: + dtype = "np.int32" + symbol = self.c_format(arr.symbol) + if arr.values is None: + return f"{symbol} = np.empty({arr.sizes}, dtype={dtype})\n" + av = build_initializer_lists(arr.values) + av = "np.array(" + av + f", dtype={dtype})" + return f"{symbol} = {av}\n" + + def format_array_access(self, arr): + array = self.c_format(arr.array) + idx = ", ".join(self.c_format(ix) for ix in arr.indices) + return f"{array}[{idx}]" + + def format_multi_index(self, index): + return self.c_format(index.global_index) + + def format_variable_decl(self, v): + sym = self.c_format(v.symbol) + val = self.c_format(v.value) + return f"{sym} = {val}\n" + + def format_nary_op(self, oper): + # Format children + args = [self.c_format(arg) for arg in oper.args] + + # Apply parentheses + for i in range(len(args)): + if oper.args[i].precedence >= oper.precedence: + args[i] = "(" + args[i] + ")" + + # Return combined string + return f" {oper.op} ".join(args) + + def format_binary_op(self, oper): + # Format children + lhs = self.c_format(oper.lhs) + rhs = self.c_format(oper.rhs) + + # Apply parentheses + if oper.lhs.precedence >= oper.precedence: + lhs = f"({lhs})" + if oper.rhs.precedence >= oper.precedence: + rhs = f"({rhs})" + + # Return combined string + return f"{lhs} {oper.op} {rhs}" + + def format_neg(self, val): + arg = self.c_format(val.arg) + return f"-{arg}" + + def format_not(self, val): + arg = self.c_format(val.arg) + return f"not({arg})" + + def format_andor(self, oper): + # Format children + lhs = self.c_format(oper.lhs) + rhs = self.c_format(oper.rhs) + + # Apply parentheses + if oper.lhs.precedence >= oper.precedence: + lhs = f"({lhs})" + if oper.rhs.precedence >= oper.precedence: + rhs = f"({rhs})" + + opstr = {"||": "or", "&&": "and"}[oper.op] + + # Return combined string + return f"{lhs} {opstr} {rhs}" + + def format_literal_float(self, val): + return f"{val.value}" + + def format_literal_int(self, val): + return f"{val.value}" + + def format_for_range(self, r): + begin = self.c_format(r.begin) + end = self.c_format(r.end) + index = self.c_format(r.index) + output = f"for {index} in range({begin}, {end}):\n" + b = self.c_format(r.body).split("\n") + for line in b: + output += f" {line}\n" + return output + + def format_statement(self, s): + return self.c_format(s.expr) + + def format_assign(self, expr): + rhs = self.c_format(expr.rhs) + lhs = self.c_format(expr.lhs) + return f"{lhs} {expr.op} {rhs}\n" + + def format_conditional(self, s): + # Format children + c = self.c_format(s.condition) + t = self.c_format(s.true) + f = self.c_format(s.false) + + # Apply parentheses + if s.condition.precedence >= s.precedence: + c = "(" + c + ")" + if s.true.precedence >= s.precedence: + t = "(" + t + ")" + if s.false.precedence >= s.precedence: + f = "(" + f + ")" + + # Return combined string + return f"({t} if {c} else {f})" + + def format_symbol(self, s): + return f"{s.name}" + + def format_mathfunction(self, f): + function_map = {"ln": "log", "acos": "arccos", "asin": "arcsin", + "atan": "arctan", "atan2": "arctan2", "acosh": "arccosh", + "asinh": "arcsinh", "atanh": "arctanh"} + function = function_map.get(f.function, f.function) + args = [self.c_format(arg) for arg in f.args] + if "bessel" in function: + return "0" + if function == "erf": + return f"math.erf({args[0]})" + argstr = ", ".join(args) + return f"np.{function}({argstr})" + + c_impl = { + "StatementList": format_statement_list, + "Comment": format_comment, + "Section": format_section, + "ArrayDecl": format_array_decl, + "ArrayAccess": format_array_access, + "MultiIndex": format_multi_index, + "VariableDecl": format_variable_decl, + "ForRange": format_for_range, + "Statement": format_statement, + "Assign": format_assign, + "AssignAdd": format_assign, + "Product": format_nary_op, + "Sum": format_nary_op, + "Add": format_binary_op, + "Sub": format_binary_op, + "Mul": format_binary_op, + "Div": format_binary_op, + "Neg": format_neg, + "Not": format_not, + "LiteralFloat": format_literal_float, + "LiteralInt": format_literal_int, + "Symbol": format_symbol, + "Conditional": format_conditional, + "MathFunction": format_mathfunction, + "And": format_andor, + "Or": format_andor, + "NE": format_binary_op, + "EQ": format_binary_op, + "GE": format_binary_op, + "LE": format_binary_op, + "GT": format_binary_op, + "LT": format_binary_op, + } + + def c_format(self, s): + name = s.__class__.__name__ + try: + return self.c_impl[name](self, s) + except KeyError: + raise RuntimeError("Unknown statement: ", name) From 4b3d85cc70b9b01d3a9bde1a83a0fbbd6b7d6d75 Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 7 May 2024 10:00:15 +0100 Subject: [PATCH 004/106] Add suffix support --- ffcx/codegeneration/codegeneration.py | 19 +++++++++++-------- ffcx/codegeneration/cpp/__init__.py | 2 +- ffcx/codegeneration/jit.py | 2 +- ffcx/compiler.py | 6 +++--- ffcx/formatting.py | 12 +++++++----- ffcx/main.py | 4 ++-- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 64a9ba355..eb8c8d1f6 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -37,20 +37,23 @@ class CodeBlocks(typing.NamedTuple): file_post: list[tuple[str, str]] -def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) -> CodeBlocks: +def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) -> tuple[CodeBlocks, tuple[str, str]]: """Generate code blocks from intermediate representation.""" logger.info(79 * "*") logger.info("Compiler stage 3: Generating code") logger.info(79 * "*") lang = options.get("language", "C") - # Built-in - mod = import_module(f"ffcx.codegeneration.{lang}") - + + try: + # Built-in + mod = import_module(f"ffcx.codegeneration.{lang}") except ImportError: - # User defined, in current directory - sys.path.append(".") - mod = import_module(f"{lang}") + # User defined language (experimental) + store_path = sys.path + sys.path = ["."] + mod = import_module(f"{lang}") + sys.path = store_path integral_generator = mod.integrals.generator form_generator = mod.form.generator @@ -69,4 +72,4 @@ def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) - forms=code_forms, expressions=code_expressions, file_post=[code_file_post], - ) + ), mod.suffixes diff --git a/ffcx/codegeneration/cpp/__init__.py b/ffcx/codegeneration/cpp/__init__.py index 3616783fa..741976267 100644 --- a/ffcx/codegeneration/cpp/__init__.py +++ b/ffcx/codegeneration/cpp/__init__.py @@ -1,3 +1,3 @@ from . import form, expressions, integrals, file # noqa -suffixes = (".hpp", ".cpp") +suffixes = (".hpp", None) diff --git a/ffcx/codegeneration/jit.py b/ffcx/codegeneration/jit.py index aa903c496..b5f6c494f 100644 --- a/ffcx/codegeneration/jit.py +++ b/ffcx/codegeneration/jit.py @@ -292,7 +292,7 @@ def _compile_objects( # JIT uses module_name as prefix, which is needed to make names of all struct/function # unique across modules - _, code_body = ffcx.compiler.compile_ufl_objects( + _, code_body, _ = ffcx.compiler.compile_ufl_objects( ufl_objects, prefix=module_name, options=options, visualise=visualise ) diff --git a/ffcx/compiler.py b/ffcx/compiler.py index 39b5b6018..6316e5718 100644 --- a/ffcx/compiler.py +++ b/ffcx/compiler.py @@ -89,7 +89,7 @@ def compile_ufl_objects( object_names: dict[int, str] | None = None, prefix: str | None = None, visualise: bool = False, -) -> tuple[str, str]: +) -> tuple[str, str, tuple[str, str]]: """Generate UFC code for a given UFL objects. Args: @@ -115,7 +115,7 @@ def compile_ufl_objects( # Stage 3: code generation cpu_time = time() - code = generate_code(ir, options) + code, suffixes = generate_code(ir, options) _print_timing(3, time() - cpu_time) # Stage 4: format code @@ -123,4 +123,4 @@ def compile_ufl_objects( code_h, code_c = format_code(code) _print_timing(4, time() - cpu_time) - return code_h, code_c + return code_h, code_c, suffixes diff --git a/ffcx/formatting.py b/ffcx/formatting.py index 57dff1566..935366a0e 100644 --- a/ffcx/formatting.py +++ b/ffcx/formatting.py @@ -38,14 +38,16 @@ def format_code(code: CodeBlocks) -> tuple[str, str]: return code_h, code_c -def write_code(code_h, code_c, prefix, output_dir): +def write_code(code_h, code_c, prefix, suffixes, output_dir): """Write code to files.""" - _write_file(code_h, prefix, ".h", output_dir) - _write_file(code_c, prefix, ".c", output_dir) + if suffixes[0] is not None: + _write_file(code_h, prefix, suffixes[0], output_dir) + if suffixes[1] is not None: + _write_file(code_c, prefix, suffixes[1], output_dir) -def _write_file(output, prefix, postfix, output_dir): +def _write_file(output, prefix, suffix, output_dir): """Write generated code to file.""" - filename = os.path.join(output_dir, prefix + postfix) + filename = os.path.join(output_dir, prefix + suffix) with open(filename, "w") as hfile: hfile.write(output) diff --git a/ffcx/main.py b/ffcx/main.py index 15b0cb900..cd7d1b52b 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -73,7 +73,7 @@ def main(args=None): ufd = ufl.algorithms.load_ufl_file(filename) # Generate code - code_h, code_c = compiler.compile_ufl_objects( + code_h, code_c, suffixes = compiler.compile_ufl_objects( ufd.forms + ufd.expressions + ufd.elements, options=options, object_names=ufd.object_names, @@ -82,7 +82,7 @@ def main(args=None): ) # Write to file - formatting.write_code(code_h, code_c, prefix, xargs.output_directory) + formatting.write_code(code_h, code_c, prefix, suffixes, xargs.output_directory) # Turn off profiling and write status to file if xargs.profile: From 0d4d4b4a9860c65276a19c8a6d0e5ad1391c2e56 Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 7 May 2024 13:18:06 +0100 Subject: [PATCH 005/106] updates --- ffcx/codegeneration/cpp/integrals.py | 2 +- ffcx/codegeneration/numba/form.py | 25 ---------------------- ffcx/codegeneration/numba/form_template.py | 3 --- ffcx/codegeneration/numba/integrals.py | 2 +- 4 files changed, 2 insertions(+), 30 deletions(-) diff --git a/ffcx/codegeneration/cpp/integrals.py b/ffcx/codegeneration/cpp/integrals.py index 4f14bc713..f821a084d 100644 --- a/ffcx/codegeneration/cpp/integrals.py +++ b/ffcx/codegeneration/cpp/integrals.py @@ -58,7 +58,7 @@ def generator(ir, options): scalar_type=options["scalar_type"], geom_type=options["scalar_type"], np_scalar_type=options["scalar_type"], - coordinate_element=ir.coordinate_element, + coordinate_element=ir.coordinate_element_hash, ) return declaration + implementation, "" diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 823897889..79b0e8a40 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -55,31 +55,6 @@ def generator(ir, options): offsets = ", ".join(str(i) for i in integral_offsets) d["form_integral_offsets"] = f"[{offsets}]" - code = [] - - # FIXME: Should be handled differently, revise how - # ufcx_function_space is generated - for name, ( - element, - dofmap, - cmap_family, - cmap_degree, - cmap_celltype, - cmap_variant, - ) in ir.function_spaces.items(): - code += [f" functionspace_{name} = {{"] - code += [f" finite_element: {element},"] - code += [f" dofmap: {dofmap},"] - code += [f' geometry_family: "{cmap_family}",'] - code += [f" geometry_degree: {cmap_degree},"] - code += [f" geometry_basix_cell: {int(cmap_celltype)},"] - code += [" }"] - - for name in ir.function_spaces.keys(): - code += [f' if function_name=="{name}": return functionspace_{name}'] - - d["functionspace"] = "\n".join(code) - # Check that no keys are redundant or have been missed from string import Formatter diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py index bc8625cd7..332bf83c3 100644 --- a/ffcx/codegeneration/numba/form_template.py +++ b/ffcx/codegeneration/numba/form_template.py @@ -22,9 +22,6 @@ class {factory_name}(object): form_integral_ids = {form_integral_ids} form_integral_offsets = {form_integral_offsets} -def function_space_{name_from_uflfile}(name): -{functionspace} - {name_from_uflfile} = {factory_name} # Name: {name_from_uflfile} diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index e6c7a7c9c..8dcc0e718 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -67,7 +67,7 @@ def generator(ir, options): tabulate_tensor=code["tabulate_tensor"], needs_facet_permutations="True" if ir.needs_facet_permutations else "False", scalar_type=options["scalar_type"], - coordinate_element=ir.coordinate_element, + coordinate_element=ir.coordinate_element_hash, ) return "", implementation From f316110903f74fd36438c3554d194a2ca604e80a Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Tue, 7 May 2024 13:29:56 +0100 Subject: [PATCH 006/106] Formatting fixes --- ffcx/codegeneration/codegeneration.py | 5 ++-- ffcx/codegeneration/numba/expressions.py | 4 +-- .../numba/expressions_template.py | 1 + ffcx/codegeneration/numba/file.py | 5 ++-- ffcx/codegeneration/numba/file_template.py | 1 + ffcx/codegeneration/numba/form.py | 3 +- ffcx/codegeneration/numba/form_template.py | 2 +- ffcx/codegeneration/numba/integrals.py | 5 ++-- .../numba/integrals_template.py | 4 ++- .../numba/numba_implementation.py | 30 +++++++++++++++++-- 10 files changed, 46 insertions(+), 14 deletions(-) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index eb8c8d1f6..69b9b82d0 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -37,14 +37,15 @@ class CodeBlocks(typing.NamedTuple): file_post: list[tuple[str, str]] -def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) -> tuple[CodeBlocks, tuple[str, str]]: +def generate_code(ir: DataIR, + options: dict[str, int | float | npt.DTypeLike]) -> tuple[CodeBlocks, + tuple[str, str]]: """Generate code blocks from intermediate representation.""" logger.info(79 * "*") logger.info("Compiler stage 3: Generating code") logger.info(79 * "*") lang = options.get("language", "C") - try: # Built-in mod = import_module(f"ffcx.codegeneration.{lang}") diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 1b7b45554..4fb546204 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -3,12 +3,13 @@ # This file is part of FFCx.(https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Generate UFC code for an expression.""" import logging -from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.expression_generator import ExpressionGenerator +from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter logger = logging.getLogger("ffcx") @@ -70,7 +71,6 @@ def generator(ir, options): d["num_points"] = ir.points.shape[0] d["topological_dimension"] = ir.points.shape[1] d["scalar_type"] = options["scalar_type"] - d["rank"] = len(ir.tensor_shape) names = ", ".join(f'"{name}"' for name in ir.coefficient_names) diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index 693961313..c135daef1 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -3,6 +3,7 @@ # This file is part of FFCx.(https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Template for expression output.""" declaration = """ """ diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py index 6e4d45cae..fbe48f433 100644 --- a/ffcx/codegeneration/numba/file.py +++ b/ffcx/codegeneration/numba/file.py @@ -6,15 +6,16 @@ # # Note: Most of the code in this file is a direct translation from the # old implementation in FFC +"""Generate file output for numba.""" + import logging import pprint import textwrap -from ffcx.codegeneration.numba import file_template from ffcx import __version__ as FFCX_VERSION from ffcx.codegeneration import __version__ as UFC_VERSION - +from ffcx.codegeneration.numba import file_template logger = logging.getLogger("ffcx") diff --git a/ffcx/codegeneration/numba/file_template.py b/ffcx/codegeneration/numba/file_template.py index d5a284ab2..eccc99486 100644 --- a/ffcx/codegeneration/numba/file_template.py +++ b/ffcx/codegeneration/numba/file_template.py @@ -2,6 +2,7 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2018. +"""Template for file output.""" declaration_pre = """ """ diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 79b0e8a40..e617a2073 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -6,6 +6,7 @@ # Note: Most of the code in this file is a direct translation from the # old implementation in FFC +"""Template for form output.""" import logging @@ -70,7 +71,7 @@ def generator(ir, options): if f not in fields: print(f, "not in fields") - if set(fields) != set(d.keys()): + if set(fields) != set(d.keys()): print("Mismatch between keys in template and in formatting dict") # Format implementation code diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py index 332bf83c3..aa99c407a 100644 --- a/ffcx/codegeneration/numba/form_template.py +++ b/ffcx/codegeneration/numba/form_template.py @@ -2,7 +2,7 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2020. - +"""Template for file output.""" factory = """ # Code for form {factory_name} diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 8dcc0e718..8a3a69ca2 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -3,19 +3,20 @@ # This file is part of FFCx. (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later +"""Generate UFC code for an integral.""" import logging +from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.numba import integrals_template as ufcx_integrals -from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter - logger = logging.getLogger("ffcx") def generator(ir, options): + """Integral generator.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.integral_type}") logger.info(f"--- name: {ir.name}") diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py index 5538c4412..2efaf74a4 100644 --- a/ffcx/codegeneration/numba/integrals_template.py +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -2,12 +2,14 @@ # This code is released into the public domain. # # The FEniCS Project (http://www.fenicsproject.org/) 2018 +"""Template for integral output.""" factory = """ # Code for integral {factory_name} import numba -def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, _entity_local_index, _quadrature_permutation): +def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, + _entity_local_index, _quadrature_permutation): {tabulate_tensor} class {factory_name}(object): diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 074cd2809..2cd025ef7 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -1,8 +1,9 @@ - +"""Numba implementation for output.""" import ffcx.codegeneration.lnodes as L def build_initializer_lists(values): + """Build list of values.""" arr = "[" if len(values.shape) == 1: return "[" + ", ".join(str(v) for v in values) + "]" @@ -12,11 +13,14 @@ def build_initializer_lists(values): return arr -class NumbaFormatter(object): +class NumbaFormatter: + """Implementation for numba output backend.""" def __init__(self, scalar) -> None: + """Initialise.""" self.scalar_type = scalar def format_section(self, section): + """Format a section.""" # add new line before section comments = "# ------------------------ \n" comments += "# Section: " + section.name + "\n" @@ -27,20 +31,23 @@ def format_section(self, section): body = "" if len(section.statements) > 0: body = "".join(self.c_format(s) for s in section.statements) - + body += "# ------------------------ \n" return comments + declarations + body def format_statement_list(self, slist): + """Format a list of statements.""" output = "" for s in slist.statements: output += self.c_format(s) return output def format_comment(self, c): + """Format a comment.""" return "# " + c.comment + "\n" def format_array_decl(self, arr): + """Format an array declaration.""" if arr.symbol.dtype == L.DataType.SCALAR: dtype = "A.dtype" elif arr.symbol.dtype == L.DataType.REAL: @@ -55,19 +62,23 @@ def format_array_decl(self, arr): return f"{symbol} = {av}\n" def format_array_access(self, arr): + """Format array access.""" array = self.c_format(arr.array) idx = ", ".join(self.c_format(ix) for ix in arr.indices) return f"{array}[{idx}]" def format_multi_index(self, index): + """Format a multi-index.""" return self.c_format(index.global_index) def format_variable_decl(self, v): + """Format a variable declaration.""" sym = self.c_format(v.symbol) val = self.c_format(v.value) return f"{sym} = {val}\n" def format_nary_op(self, oper): + """Format a n argument operation.""" # Format children args = [self.c_format(arg) for arg in oper.args] @@ -80,6 +91,7 @@ def format_nary_op(self, oper): return f" {oper.op} ".join(args) def format_binary_op(self, oper): + """Format a binary operation.""" # Format children lhs = self.c_format(oper.lhs) rhs = self.c_format(oper.rhs) @@ -94,14 +106,17 @@ def format_binary_op(self, oper): return f"{lhs} {oper.op} {rhs}" def format_neg(self, val): + """Format unary negation.""" arg = self.c_format(val.arg) return f"-{arg}" def format_not(self, val): + """Format not operation.""" arg = self.c_format(val.arg) return f"not({arg})" def format_andor(self, oper): + """Format and or or operation.""" # Format children lhs = self.c_format(oper.lhs) rhs = self.c_format(oper.rhs) @@ -118,12 +133,15 @@ def format_andor(self, oper): return f"{lhs} {opstr} {rhs}" def format_literal_float(self, val): + """Format a literal float.""" return f"{val.value}" def format_literal_int(self, val): + """Format a literal int.""" return f"{val.value}" def format_for_range(self, r): + """Format a loop over a range.""" begin = self.c_format(r.begin) end = self.c_format(r.end) index = self.c_format(r.index) @@ -134,14 +152,17 @@ def format_for_range(self, r): return output def format_statement(self, s): + """Format a statement.""" return self.c_format(s.expr) def format_assign(self, expr): + """Format assignment.""" rhs = self.c_format(expr.rhs) lhs = self.c_format(expr.lhs) return f"{lhs} {expr.op} {rhs}\n" def format_conditional(self, s): + """Format a conditional.""" # Format children c = self.c_format(s.condition) t = self.c_format(s.true) @@ -159,9 +180,11 @@ def format_conditional(self, s): return f"({t} if {c} else {f})" def format_symbol(self, s): + """Format a symbol.""" return f"{s.name}" def format_mathfunction(self, f): + """Format a math function.""" function_map = {"ln": "log", "acos": "arccos", "asin": "arcsin", "atan": "arctan", "atan2": "arctan2", "acosh": "arccosh", "asinh": "arcsinh", "atanh": "arctanh"} @@ -210,6 +233,7 @@ def format_mathfunction(self, f): } def c_format(self, s): + """Format output.""" name = s.__class__.__name__ try: return self.c_impl[name](self, s) From 705254185cceba78ab98dfb52a89d21cf005f46c Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Thu, 30 May 2024 14:55:01 +0100 Subject: [PATCH 007/106] fixes --- ffcx/codegeneration/cpp/integrals.py | 13 +++++++------ ffcx/codegeneration/numba/form.py | 2 +- ffcx/codegeneration/numba/integrals.py | 16 ++++++++-------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ffcx/codegeneration/cpp/integrals.py b/ffcx/codegeneration/cpp/integrals.py index f821a084d..b1d979ded 100644 --- a/ffcx/codegeneration/cpp/integrals.py +++ b/ffcx/codegeneration/cpp/integrals.py @@ -18,10 +18,11 @@ def generator(ir, options): """Generate code for an integral.""" logger.info("Generating code for integral:") - logger.info(f"--- type: {ir.integral_type}") - logger.info(f"--- name: {ir.name}") + logger.info(f"--- type: {ir.expression.integral_type}") + logger.info(f"--- name: {ir.expression.name}") - factory_name = ir.name + + factory_name = ir.expression.name # Format declaration declaration = ufcx_integrals.declaration.format(factory_name=factory_name) @@ -41,8 +42,8 @@ def generator(ir, options): # Generate generic FFCx code snippets and add specific parts code = {} - code["class_type"] = ir.integral_type + "_integral" - code["name"] = ir.name + code["class_type"] = ir.expression.integral_type + "_integral" + code["name"] = ir.expression.name vals = ", ".join("true" if i else "false" for i in ir.enabled_coefficients) code["enabled_coefficients"] = f"{{{vals}}}" @@ -54,7 +55,7 @@ def generator(ir, options): factory_name=factory_name, enabled_coefficients=code["enabled_coefficients"], tabulate_tensor=code["tabulate_tensor"], - needs_facet_permutations="true" if ir.needs_facet_permutations else "false", + needs_facet_permutations="true" if ir.expression.needs_facet_permutations else "false", scalar_type=options["scalar_type"], geom_type=options["scalar_type"], np_scalar_type=options["scalar_type"], diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index e617a2073..10433c29e 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -29,7 +29,7 @@ def generator(ir, options): d["num_coefficients"] = ir.num_coefficients d["num_constants"] = ir.num_constants - orig_coeff = ', '.join(str(i) for i in ir.original_coefficient_position) + orig_coeff = ', '.join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_position"] = f"[{orig_coeff}]" cnames = ir.coefficient_names diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 8a3a69ca2..5908f79b0 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -18,11 +18,11 @@ def generator(ir, options): """Integral generator.""" logger.info("Generating code for integral:") - logger.info(f"--- type: {ir.integral_type}") - logger.info(f"--- name: {ir.name}") + logger.info(f"--- type: {ir.expression.integral_type}") + logger.info(f"--- name: {ir.expression.name}") """Generate code for an integral.""" - factory_name = ir.name + factory_name = ir.expression.name # Create FFCx backend backend = FFCXBackend(ir, options) @@ -41,17 +41,17 @@ def generator(ir, options): # Generate generic FFCx code snippets and add specific parts code = {} - code["class_type"] = ir.integral_type + "_integral" - code["name"] = ir.name + code["class_type"] = ir.expression.integral_type + "_integral" + code["name"] = ir.expression.name vals = ", ".join("1" if i else "0" for i in ir.enabled_coefficients) code["enabled_coefficients"] = f"[{vals}]" tensor_size = 1 - for dim in ir.tensor_shape: + for dim in ir.expression.tensor_shape: tensor_size *= dim n_coeff = len(ir.enabled_coefficients) - n_const = len(ir.original_constant_offsets) + n_const = len(ir.expression.original_constant_offsets) header = f""" A = numba.carray(_A, ({tensor_size})) w = numba.carray(_w, ({n_coeff})) @@ -66,7 +66,7 @@ def generator(ir, options): factory_name=factory_name, enabled_coefficients=code["enabled_coefficients"], tabulate_tensor=code["tabulate_tensor"], - needs_facet_permutations="True" if ir.needs_facet_permutations else "False", + needs_facet_permutations="True" if ir.expression.needs_facet_permutations else "False", scalar_type=options["scalar_type"], coordinate_element=ir.coordinate_element_hash, ) From aacc715c74ee19b36029d58d8b5da6975209229a Mon Sep 17 00:00:00 2001 From: Chris Richardson Date: Thu, 30 May 2024 15:18:38 +0100 Subject: [PATCH 008/106] Remove wrapper --- ffcx/codegeneration/numba/file_template.py | 11 ----------- ffcx/codegeneration/numba/integrals.py | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/ffcx/codegeneration/numba/file_template.py b/ffcx/codegeneration/numba/file_template.py index eccc99486..1fb9016b2 100644 --- a/ffcx/codegeneration/numba/file_template.py +++ b/ffcx/codegeneration/numba/file_template.py @@ -40,17 +40,6 @@ ufcx_quadrature_element = 2 ufcx_basix_custom_element = 3 -def wrapper(scalar_type, real_type): - - c_signature = numba.types.void( - numba.types.CPointer(numba.typeof(scalar_type())), - numba.types.CPointer(numba.typeof(scalar_type())), - numba.types.CPointer(numba.typeof(scalar_type())), - numba.types.CPointer(numba.typeof(real_type())), - numba.types.CPointer(numba.types.int32), - numba.types.CPointer(numba.types.int32)) - return numba.cfunc(c_signature, nopython=True) - """ implementation_post = """ diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 5908f79b0..2c6b6b3d8 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -52,6 +52,7 @@ def generator(ir, options): tensor_size *= dim n_coeff = len(ir.enabled_coefficients) n_const = len(ir.expression.original_constant_offsets) + header = f""" A = numba.carray(_A, ({tensor_size})) w = numba.carray(_w, ({n_coeff})) From 9c2ad9d804a6f75dab4cc4fac2114bb3fc32dfa6 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:03:01 +0100 Subject: [PATCH 009/106] Fix --- ffcx/codegeneration/codegeneration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 20efa55f2..22d72bed1 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -62,7 +62,7 @@ def generate_code( file_generator = mod.file.generator code_integrals = [ - integral_generator(integral_ir, options) + integral_generator(integral_ir, domain, options) for integral_ir in ir.integrals for domain in set(i[0] for i in integral_ir.expression.integrand.keys()) ] From 2f3c3d5ca99ca84ff5e1d646bc9c8bd0abd835e3 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:26:21 +0100 Subject: [PATCH 010/106] [tmp] no mypy --- .github/workflows/pythonapp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 1bb7c4707..6feb2dfa9 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -85,8 +85,8 @@ jobs: if: runner.os != 'Linux' run: pip install .[ci] - - name: Static check with mypy - run: mypy -p ffcx + # - name: Static check with mypy + # run: mypy -p ffcx - name: ruff checks run: | From a41f7d7eae0f6c68b0ae5791137982f698911f98 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:29:04 +0100 Subject: [PATCH 011/106] [tmp] no ruff --- .github/workflows/pythonapp.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 6feb2dfa9..e7ffbe9c0 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -88,10 +88,10 @@ jobs: # - name: Static check with mypy # run: mypy -p ffcx - - name: ruff checks - run: | - ruff check . - ruff format --check . + # - name: ruff checks + # run: | + # ruff check . + # ruff format --check . - name: Run unit tests run: > From 8b73bc3789e1ad8596e38888523df8455b050982 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:26:07 +0100 Subject: [PATCH 012/106] Start with numba --- demo/test_demos.py | 7 ++++--- ffcx/codegeneration/numba/expressions.py | 14 ++++++++------ ffcx/codegeneration/numba/integrals.py | 8 +++++--- ffcx/formatting.py | 4 ++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index bebfec2a2..8c5c290f8 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -15,7 +15,8 @@ @pytest.mark.parametrize("file", ufl_files) @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) -def test_demo(file, scalar_type): +@pytest.mark.parametrize("language", ["C", "numba"]) +def test_demo(file, scalar_type, language): """Test a demo.""" if sys.platform.startswith("win32") and "complex" in scalar_type: # Skip complex demos on win32 @@ -32,8 +33,9 @@ def test_demo(file, scalar_type): # Skip demos that are only implemented for complex scalars pytest.skip(reason="Not implemented for real types") + opts = f"--scalar_type {scalar_type} -L {language}" + if sys.platform.startswith("win32"): - opts = f"--scalar_type {scalar_type}" extra_flags = "/std:c17" assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 assert ( @@ -49,7 +51,6 @@ def test_demo(file, scalar_type): ) == 0 else: cc = os.environ.get("CC", "cc") - opts = f"--scalar_type {scalar_type}" extra_flags = ( "-std=c17 -Wunused-variable -Werror -fPIC -Wno-error=implicit-function-declaration" ) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 4fb546204..e2a0617c0 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -11,17 +11,19 @@ from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter +from ffcx.ir.representation import ExpressionIR logger = logging.getLogger("ffcx") -def generator(ir, options): +def generator(ir: ExpressionIR, options): """Generate UFC code for an expression.""" logger.info("Generating code for expression:") - logger.info(f"--- points: {ir.points}") - logger.info(f"--- name: {ir.name}") - - factory_name = ir.name + assert len(ir.expression.integrand) == 1, "Expressions only support single quadrature rule" + points = next(iter(ir.expression.integrand))[1].points + logger.info(f"--- points: {points}") + factory_name = ir.expression.name + logger.info(f"--- name: {factory_name}") # Format declaration declaration = expressions_template.declaration.format( @@ -33,7 +35,7 @@ def generator(ir, options): d = {} d["name_from_uflfile"] = ir.name_from_uflfile - d["factory_name"] = ir.name + d["factory_name"] = factory_name parts = eg.generate() diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 2c6b6b3d8..f7241c218 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -7,6 +7,8 @@ import logging +import basix + from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.numba import integrals_template as ufcx_integrals @@ -15,7 +17,7 @@ logger = logging.getLogger("ffcx") -def generator(ir, options): +def generator(ir, domain: basix.CellType, options): """Integral generator.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") @@ -31,7 +33,7 @@ def generator(ir, options): ig = IntegralGenerator(ir, backend) # Generate code AST for the tabulate_tensor body - parts = ig.generate() + parts = ig.generate(domain) # Format code as string F = NumbaFormatter(options["scalar_type"]) @@ -69,7 +71,7 @@ def generator(ir, options): tabulate_tensor=code["tabulate_tensor"], needs_facet_permutations="True" if ir.expression.needs_facet_permutations else "False", scalar_type=options["scalar_type"], - coordinate_element=ir.coordinate_element_hash, + coordinate_element=ir.expression.coordinate_element_hash, ) return "", implementation diff --git a/ffcx/formatting.py b/ffcx/formatting.py index 05757d9bb..3f1c4e203 100644 --- a/ffcx/formatting.py +++ b/ffcx/formatting.py @@ -38,7 +38,7 @@ def format_code(code: CodeBlocks) -> tuple[str, str]: return code_h, code_c -def write_code(code_h: str, code_c: str, prefix: str, output_dir: str) -> None: +def write_code(code_h: str, code_c: str, prefix: str, suffixes, output_dir: str) -> None: """Write code to files.""" if suffixes[0] is not None: _write_file(code_h, prefix, suffixes[0], output_dir) @@ -46,7 +46,7 @@ def write_code(code_h: str, code_c: str, prefix: str, output_dir: str) -> None: _write_file(code_c, prefix, suffixes[1], output_dir) -def _write_file(output: str, prefix: str, postfix: str, output_dir: str) -> None: +def _write_file(output: str, prefix: str, suffix: str, output_dir: str) -> None: """Write generated code to file.""" filename = os.path.join(output_dir, prefix + suffix) with open(filename, "w") as hfile: From 218ce8dfd310b1916ed4392dc0d81917ea0c7dc6 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:32:28 +0100 Subject: [PATCH 013/106] drop cpp/ --- ffcx/codegeneration/cpp/__init__.py | 3 - ffcx/codegeneration/cpp/cpp_implementation.py | 269 ------------------ ffcx/codegeneration/cpp/expressions.py | 148 ---------- .../cpp/expressions_template.py | 61 ---- ffcx/codegeneration/cpp/file.py | 48 ---- ffcx/codegeneration/cpp/file_template.py | 38 --- ffcx/codegeneration/cpp/form.py | 24 -- ffcx/codegeneration/cpp/form_template.py | 49 ---- ffcx/codegeneration/cpp/integrals.py | 65 ----- ffcx/codegeneration/cpp/integrals_template.py | 52 ---- 10 files changed, 757 deletions(-) delete mode 100644 ffcx/codegeneration/cpp/__init__.py delete mode 100644 ffcx/codegeneration/cpp/cpp_implementation.py delete mode 100644 ffcx/codegeneration/cpp/expressions.py delete mode 100644 ffcx/codegeneration/cpp/expressions_template.py delete mode 100644 ffcx/codegeneration/cpp/file.py delete mode 100644 ffcx/codegeneration/cpp/file_template.py delete mode 100644 ffcx/codegeneration/cpp/form.py delete mode 100644 ffcx/codegeneration/cpp/form_template.py delete mode 100644 ffcx/codegeneration/cpp/integrals.py delete mode 100644 ffcx/codegeneration/cpp/integrals_template.py diff --git a/ffcx/codegeneration/cpp/__init__.py b/ffcx/codegeneration/cpp/__init__.py deleted file mode 100644 index 741976267..000000000 --- a/ffcx/codegeneration/cpp/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import form, expressions, integrals, file # noqa - -suffixes = (".hpp", None) diff --git a/ffcx/codegeneration/cpp/cpp_implementation.py b/ffcx/codegeneration/cpp/cpp_implementation.py deleted file mode 100644 index a39220ee6..000000000 --- a/ffcx/codegeneration/cpp/cpp_implementation.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright (C) 2023 Chris Richardson -# -# This file is part of FFCx. (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -"""C++ implementation.""" - -import ffcx.codegeneration.lnodes as L - -math_table = { - "sqrt": "std::sqrt", - "abs": "std::abs", - "cos": "std::cos", - "sin": "std::sin", - "tan": "std::tan", - "acos": "std::acos", - "asin": "std::asin", - "atan": "std::atan", - "cosh": "std::cosh", - "sinh": "std::sinh", - "tanh": "std::tanh", - "acosh": "std::acosh", - "asinh": "std::asinh", - "atanh": "std::atanh", - "power": "std::pow", - "exp": "std::exp", - "ln": "std::log", - "erf": "std::erf", - "atan_2": "std::atan2", - "min_value": "std::fmin", - "max_value": "std::fmax", - "bessel_y": "std::cyl_bessel_i", - "bessel_j": "std::cyl_bessel_j", - "conj": "std::conj", - "real": "std::real", - "imag": "std::imag"} - - -def build_initializer_lists(values): - """Build initializer lists.""" - arr = "{" - if len(values.shape) == 1: - return "{" + ", ".join(str(v) for v in values) + "}" - elif len(values.shape) > 1: - arr += ",\n".join(build_initializer_lists(v) for v in values) - arr += "}" - return arr - - -class CppFormatter: - """C++ formatter.""" - def __init__(self, scalar) -> None: - """Initialise.""" - self.scalar_type = "T" - self.real_type = "U" - - def format_statement_list(self, slist) -> str: - """Format statement list.""" - return "".join(self.c_format(s) for s in slist.statements) - - def format_section(self, section) -> str: - """Format a section.""" - # add new line before section - comments = "// ------------------------ \n" - comments += "// Section: " + section.name + "\n" - comments += "// Inputs: " + ", ".join(w.name for w in section.input) + "\n" - comments += "// Outputs: " + ", ".join(w.name for w in section.output) + "\n" - declarations = "".join(self.c_format(s) for s in section.declarations) - - body = "" - if len(section.statements) > 0: - declarations += "{\n " - body = "".join(self.c_format(s) for s in section.statements) - body = body.replace("\n", "\n ") - body = body[:-2] + "}\n" - - body += "// ------------------------ \n" - return comments + declarations + body - - def format_comment(self, c) -> str: - """Format a comment.""" - return "// " + c.comment + "\n" - - def format_array_decl(self, arr) -> str: - """Format an array declaration.""" - dtype = arr.symbol.dtype - assert dtype is not None - - if dtype == L.DataType.SCALAR: - typename = self.scalar_type - elif dtype == L.DataType.REAL: - typename = self.real_type - elif dtype == L.DataType.INT: - typename = "int" - else: - raise ValueError("Invalid datatype") - - symbol = self.c_format(arr.symbol) - dims = "".join([f"[{i}]" for i in arr.sizes]) - if arr.values is None: - assert arr.const is False - return f"{typename} {symbol}{dims};\n" - - vals = build_initializer_lists(arr.values) - cstr = "static const " if arr.const else "" - return f"{cstr}{typename} {symbol}{dims} = {vals};\n" - - def format_array_access(self, arr) -> str: - """Format array access.""" - name = self.c_format(arr.array) - indices = f"[{']['.join(self.c_format(i) for i in arr.indices)}]" - return f"{name}{indices}" - - def format_multi_index(self, index) -> str: - """Format a multi-index.""" - return self.c_format(index.global_index) - - def format_variable_decl(self, v) -> str: - """Format a variable declaration.""" - val = self.c_format(v.value) - symbol = self.c_format(v.symbol) - assert v.symbol.dtype - if v.symbol.dtype == L.DataType.SCALAR: - typename = self.scalar_type - elif v.symbol.dtype == L.DataType.REAL: - typename = self.real_type - return f"{typename} {symbol} = {val};\n" - - def format_nary_op(self, oper) -> str: - """Format an n-argument operation.""" - # Format children - args = [self.c_format(arg) for arg in oper.args] - - # Apply parentheses - for i in range(len(args)): - if oper.args[i].precedence >= oper.precedence: - args[i] = "(" + args[i] + ")" - - # Return combined string - return f" {oper.op} ".join(args) - - def format_binary_op(self, oper) -> str: - """Format a binary operation.""" - # Format children - lhs = self.c_format(oper.lhs) - rhs = self.c_format(oper.rhs) - - # Apply parentheses - if oper.lhs.precedence >= oper.precedence: - lhs = f"({lhs})" - if oper.rhs.precedence >= oper.precedence: - rhs = f"({rhs})" - - # Return combined string - return f"{lhs} {oper.op} {rhs}" - - def format_neg(self, val) -> str: - """Format negation.""" - arg = self.c_format(val.arg) - return f"-{arg}" - - def format_not(self, val) -> str: - """Format 'not' statement.""" - arg = self.c_format(val.arg) - return f"{val.op}({arg})" - - def format_literal_float(self, val) -> str: - """Format a literal float number.""" - return f"{val.value}" - - def format_literal_int(self, val) -> str: - """Format a literal int number.""" - return f"{val.value}" - - def format_for_range(self, r) -> str: - """Format a loop over a range.""" - begin = self.c_format(r.begin) - end = self.c_format(r.end) - index = self.c_format(r.index) - output = f"for (int {index} = {begin}; {index} < {end}; ++{index})\n" - output += "{\n" - body = self.c_format(r.body) - for line in body.split("\n"): - if len(line) > 0: - output += f" {line}\n" - output += "}\n" - return output - - def format_statement(self, s) -> str: - """Format a statement.""" - return self.c_format(s.expr) - - def format_assign(self, expr) -> str: - """Format an assignment statement.""" - rhs = self.c_format(expr.rhs) - lhs = self.c_format(expr.lhs) - return f"{lhs} {expr.op} {rhs};\n" - - def format_conditional(self, s) -> str: - """Format a conditional.""" - # Format children - c = self.c_format(s.condition) - t = self.c_format(s.true) - f = self.c_format(s.false) - - # Apply parentheses - if s.condition.precedence >= s.precedence: - c = "(" + c + ")" - if s.true.precedence >= s.precedence: - t = "(" + t + ")" - if s.false.precedence >= s.precedence: - f = "(" + f + ")" - - # Return combined string - return c + " ? " + t + " : " + f - - def format_symbol(self, s) -> str: - """Format a symbol.""" - return f"{s.name}" - - def format_math_function(self, c) -> str: - """Format a math function.""" - # Get a function from the table, if available, else just use bare name - func = math_table.get(c.function, c.function) - args = ", ".join(self.c_format(arg) for arg in c.args) - return f"{func}({args})" - - c_impl = { - "Section": format_section, - "StatementList": format_statement_list, - "Comment": format_comment, - "ArrayDecl": format_array_decl, - "ArrayAccess": format_array_access, - "MultiIndex": format_multi_index, - "VariableDecl": format_variable_decl, - "ForRange": format_for_range, - "Statement": format_statement, - "Assign": format_assign, - "AssignAdd": format_assign, - "Product": format_nary_op, - "Neg": format_neg, - "Sum": format_nary_op, - "Add": format_binary_op, - "Sub": format_binary_op, - "Mul": format_binary_op, - "Div": format_binary_op, - "Not": format_not, - "LiteralFloat": format_literal_float, - "LiteralInt": format_literal_int, - "Symbol": format_symbol, - "Conditional": format_conditional, - "MathFunction": format_math_function, - "And": format_binary_op, - "Or": format_binary_op, - "NE": format_binary_op, - "EQ": format_binary_op, - "GE": format_binary_op, - "LE": format_binary_op, - "GT": format_binary_op, - "LT": format_binary_op, - } - - def c_format(self, s) -> str: - """Formatting function.""" - name = s.__class__.__name__ - try: - return self.c_impl[name](self, s) - except KeyError: - raise RuntimeError("Unknown statement: ", name) diff --git a/ffcx/codegeneration/cpp/expressions.py b/ffcx/codegeneration/cpp/expressions.py deleted file mode 100644 index 9288eb50a..000000000 --- a/ffcx/codegeneration/cpp/expressions.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (C) 2019 Michal Habera -# -# This file is part of FFCx.(https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -"""Generate code for an expression.""" - -import logging - -from ffcx.codegeneration.backend import FFCXBackend -from ffcx.codegeneration.C import expressions_template -from ffcx.codegeneration.C.c_implementation import CFormatter -from ffcx.codegeneration.expression_generator import ExpressionGenerator - -logger = logging.getLogger("ffcx") - - -def generator(ir, options): - """Generate UFC code for an expression.""" - logger.info("Generating code for expression:") - logger.info(f"--- points: {ir.points}") - logger.info(f"--- name: {ir.name}") - - factory_name = ir.name - - # Format declaration - declaration = expressions_template.declaration.format( - factory_name=factory_name, name_from_uflfile=ir.name_from_uflfile - ) - - backend = FFCXBackend(ir, options) - eg = ExpressionGenerator(ir, backend) - - d = {} - d["name_from_uflfile"] = ir.name_from_uflfile - d["factory_name"] = ir.name - - parts = eg.generate() - - CF = CFormatter(options["scalar_type"]) - d["tabulate_expression"] = CF.c_format(parts) - - if len(ir.original_coefficient_positions) > 0: - d[ - "original_coefficient_positions" - ] = f"original_coefficient_positions_{ir.name}" - n = len(ir.original_coefficient_positions) - originals = ", ".join(str(i) for i in ir.original_coefficient_positions) - d[ - "original_coefficient_positions_init" - ] = f"static int original_coefficient_positions_{ir.name}[{n}] = {{{originals}}};" - - else: - d["original_coefficient_positions"] = "NULL" - d["original_coefficient_positions_init"] = "" - - points = ", ".join(str(p) for p in ir.points.flatten()) - n = ir.points.size - d["points_init"] = f"static double points_{ir.name}[{n}] = {{{points}}};" - d["points"] = f"points_{ir.name}" - - if len(ir.expression_shape) > 0: - n = len(ir.expression_shape) - shape = ", ".join(str(i) for i in ir.expression_shape) - d["value_shape_init"] = f"static int value_shape_{ir.name}[{n}] = {{{shape}}};" - d["value_shape"] = f"value_shape_{ir.name}" - else: - d["value_shape_init"] = "" - d["value_shape"] = "NULL" - - d["num_components"] = len(ir.expression_shape) - d["num_coefficients"] = len(ir.coefficient_numbering) - d["num_constants"] = len(ir.constant_names) - d["num_points"] = ir.points.shape[0] - d["topological_dimension"] = ir.points.shape[1] - d["rank"] = len(ir.tensor_shape) - - if len(ir.coefficient_names) > 0: - names = ", ".join(f'"{name}"' for name in ir.coefficient_names) - n = len(ir.coefficient_names) - d[ - "coefficient_names_init" - ] = f"static const char* coefficient_names_{ir.name}[{n}] = {{{names}}};" - - d["coefficient_names"] = f"coefficient_names_{ir.name}" - else: - d["coefficient_names_init"] = "" - d["coefficient_names"] = "NULL" - - if len(ir.constant_names) > 0: - names = ", ".join(f'"{name}"' for name in ir.constant_names) - n = len(ir.constant_names) - d[ - "constant_names_init" - ] = f"static const char* constant_names_{ir.name}[{n}] = {{{names}}};" - d["constant_names"] = f"constant_names_{ir.name}" - else: - d["constant_names_init"] = "" - d["constant_names"] = "NULL" - - code = [] - - # FIXME: Should be handled differently, revise how - # ufcx_function_space is generated (also for ufcx_form) - for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): - code += [ - f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} =" - ] - code += ["{"] - code += [f".finite_element = &{element},"] - code += [f".dofmap = &{dofmap},"] - code += [f'.geometry_family = "{cmap_family}",'] - code += [f".geometry_degree = {cmap_degree}"] - code += ["};"] - - d["function_spaces_alloc"] = "\n".join(code) - d["function_spaces"] = "" - - if len(ir.function_spaces) > 0: - d["function_spaces"] = f"function_spaces_{ir.name}" - fs_list = ", ".join( - f"&function_space_{name}_{ir.name_from_uflfile}" - for (name, _) in ir.function_spaces.items() - ) - n = len(ir.function_spaces.items()) - d[ - "function_spaces_init" - ] = f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" - else: - d["function_spaces"] = "NULL" - d["function_spaces_init"] = "" - - # Check that no keys are redundant or have been missed - from string import Formatter - - fields = [ - fname - for _, fname, _, _ in Formatter().parse(expressions_template.factory) - if fname - ] - assert set(fields) == set( - d.keys() - ), "Mismatch between keys in template and in formatting dict" - - # Format implementation code - implementation = expressions_template.factory.format_map(d) - - return declaration, implementation diff --git a/ffcx/codegeneration/cpp/expressions_template.py b/ffcx/codegeneration/cpp/expressions_template.py deleted file mode 100644 index 9b25cbee2..000000000 --- a/ffcx/codegeneration/cpp/expressions_template.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2019 Michal Habera -# -# This file is part of FFCx.(https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -"""Code generation strings for an expression.""" - -declaration = """ -extern ufcx_expression {factory_name}; - -// Helper used to create expression using name which was given to the -// expression in the UFL file. -// This helper is called in user c++ code. -// -extern ufcx_expression* {name_from_uflfile}; -""" - -factory = """ -// Code for expression {factory_name} - -void tabulate_tensor_{factory_name}({scalar_type}* restrict A, - const {scalar_type}* restrict w, - const {scalar_type}* restrict c, - const {geom_type}* restrict coordinate_dofs, - const int* restrict entity_local_index, - const uint8_t* restrict quadrature_permutation) -{{ -{tabulate_expression} -}} - -{points_init} -{value_shape_init} -{original_coefficient_positions_init} -{function_spaces_alloc} -{function_spaces_init} -{coefficient_names_init} -{constant_names_init} - - -ufcx_expression {factory_name} = -{{ - .tabulate_tensor_{np_scalar_type} = tabulate_tensor_{factory_name}, - .num_coefficients = {num_coefficients}, - .num_constants = {num_constants}, - .original_coefficient_positions = {original_coefficient_positions}, - .coefficient_names = {coefficient_names}, - .constant_names = {constant_names}, - .num_points = {num_points}, - .entity_dimension = {entity_dimension}, - .points = {points}, - .value_shape = {value_shape}, - .num_components = {num_components}, - .rank = {rank}, - .function_spaces = {function_spaces} -}}; - -// Alias name -ufcx_expression* {name_from_uflfile} = &{factory_name}; - -// End of code for expression {factory_name} -""" diff --git a/ffcx/codegeneration/cpp/file.py b/ffcx/codegeneration/cpp/file.py deleted file mode 100644 index 259a56673..000000000 --- a/ffcx/codegeneration/cpp/file.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2009-2018 Anders Logg, Martin Sandve Alnæs and Garth N. Wells -# -# This file is part of FFCx.(https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -# -# Note: Most of the code in this file is a direct translation from the -# old implementation in FFC -"""Generate code for file output.""" - -import logging -import pprint -import textwrap - -from ffcx import __version__ as FFCX_VERSION -from ffcx.codegeneration import __version__ as UFC_VERSION -from ffcx.codegeneration.cpp import file_template - -logger = logging.getLogger("ffcx") - - -def generator(options): - """Generate UFC code for file output.""" - logger.info("Generating code for file") - - # Attributes - d = {"ffcx_version": FFCX_VERSION, "ufcx_version": UFC_VERSION} - d["options"] = textwrap.indent(pprint.pformat(options), "// ") - extra_includes = [] - if "_Complex" in options["scalar_type"]: - extra_includes += ["complex"] - d["extra_includes"] = "\n".join( - f"#include <{header}>" for header in extra_includes - ) - - # Format declaration code - code_pre = ( - file_template.declaration_pre.format_map(d), - file_template.implementation_pre.format_map(d), - ) - - # Format implementation code - code_post = ( - file_template.declaration_post.format_map(d), - file_template.implementation_post.format_map(d), - ) - - return code_pre, code_post diff --git a/ffcx/codegeneration/cpp/file_template.py b/ffcx/codegeneration/cpp/file_template.py deleted file mode 100644 index ed8128f21..000000000 --- a/ffcx/codegeneration/cpp/file_template.py +++ /dev/null @@ -1,38 +0,0 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. -# -# The FEniCS Project (http://www.fenicsproject.org/) 2018. -"""Templates for C++ file output.""" - -declaration_pre = """ -// This code conforms with the UFC specification version {ufcx_version} -// and was automatically generated by FFCx version {ffcx_version}. -// -// This code was generated with the following options: -// -{options} - -#pragma once -#include -#include - -""" - -declaration_post = """ -""" - -implementation_pre = """ -// This code conforms with the UFC specification version {ufcx_version} -// and was automatically generated by FFCx version {ffcx_version}. -// -// This code was generated with the following options: -// -{options} - -#include -#include -{extra_includes} - -""" - -implementation_post = "" diff --git a/ffcx/codegeneration/cpp/form.py b/ffcx/codegeneration/cpp/form.py deleted file mode 100644 index ac8f05969..000000000 --- a/ffcx/codegeneration/cpp/form.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2009-2017 Anders Logg and Martin Sandve Alnæs -# -# This file is part of FFCx.(https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -# Note: Most of the code in this file is a direct translation from the -# old implementation in FFC -"""Generate code for a form.""" - -import logging - -# from ffcx.codegeneration.cpp import form_template - -logger = logging.getLogger("ffcx") - - -def generator(ir, options): - """Generate UFC code for a form.""" - logger.info("Generating code for form:") - logger.info(f"--- rank: {ir.rank}") - logger.info(f"--- name: {ir.name}") - - return ("", "") diff --git a/ffcx/codegeneration/cpp/form_template.py b/ffcx/codegeneration/cpp/form_template.py deleted file mode 100644 index 2552db6c7..000000000 --- a/ffcx/codegeneration/cpp/form_template.py +++ /dev/null @@ -1,49 +0,0 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. -# -# The FEniCS Project (http://www.fenicsproject.org/) 2020. -"""Templates for C++ form output.""" - -declaration = """ -extern ufcx_form {factory_name}; - -// Helper used to create form using name which was given to the -// form in the UFL file. -// This helper is called in user c++ code. -// -extern ufcx_form* {name_from_uflfile}; - -// Helper used to create function space using function name -// i.e. name of the Python variable. -// -ufcx_function_space* functionspace_{name_from_uflfile}(const char* function_name); -""" - -factory = """ -// Code for form {factory_name} - -{dofmaps_init} -{finite_elements_init} - -{name_from_uflfile}::constant_name = {constant_name_map}; -{name_from_uflfile}::coefficient_name = {coefficient_name_map}; -{name_from_uflfile}::signature ={signature}; -{name_from_uflfile}::rank = {rank}; -{name_from_uflfile}::num_coefficients = {num_coefficients}; -{name_from_uflfile}::num_constants = {num_constants}; -{name_from_uflfile}::original_coefficient_position = {original_coefficient_position}; -{name_from_uflfile}::coefficient_name_map = coefficient_name_{factory_name}; -{name_from_uflfile}::constant_name_map = constant_name_{factory_name}; -{name_from_uflfile}::finite_elements = {finite_elements}; -{name_from_uflfile}::dofmaps = {dofmaps}; -{name_from_uflfile}::form_integrals = {form_integrals}; -{name_from_uflfile}::form_integral_ids = {form_integral_ids}; -{name_from_uflfile}::form_integral_offsets = {form_integral_offsets}; - -ufcx_function_space* functionspace_{name_from_uflfile}(const char* function_name) -{{ -{functionspace} -}} - -// End of code for form {factory_name} -""" diff --git a/ffcx/codegeneration/cpp/integrals.py b/ffcx/codegeneration/cpp/integrals.py deleted file mode 100644 index b1d979ded..000000000 --- a/ffcx/codegeneration/cpp/integrals.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (C) 2015-2021 Martin Sandve Alnæs, Michal Habera, Igor Baratta -# -# This file is part of FFCx. (https://www.fenicsproject.org) -# -# SPDX-License-Identifier: LGPL-3.0-or-later -"""Integral generation.""" - -import logging - -from ffcx.codegeneration.backend import FFCXBackend -from ffcx.codegeneration.cpp import integrals_template as ufcx_integrals -from ffcx.codegeneration.cpp.cpp_implementation import CppFormatter -from ffcx.codegeneration.integral_generator import IntegralGenerator - -logger = logging.getLogger("ffcx") - - -def generator(ir, options): - """Generate code for an integral.""" - logger.info("Generating code for integral:") - logger.info(f"--- type: {ir.expression.integral_type}") - logger.info(f"--- name: {ir.expression.name}") - - - factory_name = ir.expression.name - - # Format declaration - declaration = ufcx_integrals.declaration.format(factory_name=factory_name) - - # Create FFCx C backend - backend = FFCXBackend(ir, options) - - # Configure kernel generator - ig = IntegralGenerator(ir, backend) - - # Generate code ast for the tabulate_tensor body - parts = ig.generate() - - # Format code as string - CF = CppFormatter(options["scalar_type"]) - body = CF.c_format(parts) - - # Generate generic FFCx code snippets and add specific parts - code = {} - code["class_type"] = ir.expression.integral_type + "_integral" - code["name"] = ir.expression.name - - vals = ", ".join("true" if i else "false" for i in ir.enabled_coefficients) - code["enabled_coefficients"] = f"{{{vals}}}" - - code["additional_includes_set"] = set() # FIXME: Get this out of code[] - code["tabulate_tensor"] = body - - implementation = ufcx_integrals.factory.format( - factory_name=factory_name, - enabled_coefficients=code["enabled_coefficients"], - tabulate_tensor=code["tabulate_tensor"], - needs_facet_permutations="true" if ir.expression.needs_facet_permutations else "false", - scalar_type=options["scalar_type"], - geom_type=options["scalar_type"], - np_scalar_type=options["scalar_type"], - coordinate_element=ir.coordinate_element_hash, - ) - - return declaration + implementation, "" diff --git a/ffcx/codegeneration/cpp/integrals_template.py b/ffcx/codegeneration/cpp/integrals_template.py deleted file mode 100644 index 110c0cb1f..000000000 --- a/ffcx/codegeneration/cpp/integrals_template.py +++ /dev/null @@ -1,52 +0,0 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. -# -# The FEniCS Project (http://www.fenicsproject.org/) 2018 -"""Templates for C++ integral output.""" - -declaration = """ -class {factory_name} -{{ -public: - -// Constructor -{factory_name}(); - -// Kernel -template -void tabulate_tensor(T* A, - const T* w, - const T* c, - const U* coordinate_dofs, - const int* entity_local_index, - const uint8_t* quadrature_permutation); - -// Data -std::vector enabled_coefficients; -bool needs_facet_permutations; - -}}; -""" - -factory = """ -// Code for integral {factory_name} - -template -void {factory_name}::tabulate_tensor(T* A, - const T* w, - const T* c, - const U* coordinate_dofs, - const int* entity_local_index, - const uint8_t* quadrature_permutation) -{{ -{tabulate_tensor} -}} - -{factory_name}::{factory_name}() -{{ - enabled_coefficients = {enabled_coefficients}; - needs_facet_permutations = {needs_facet_permutations}; -}} - -// End of code for integral {factory_name} -""" From 1c3e77dc959b7dcdcf62756df4f688af52d09968 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:32:39 +0100 Subject: [PATCH 014/106] ruff --- ffcx/codegeneration/C/__init__.py | 4 +++- ffcx/codegeneration/numba/__init__.py | 2 +- ffcx/codegeneration/numba/expressions.py | 10 ++++------ ffcx/codegeneration/numba/file.py | 1 - ffcx/codegeneration/numba/form.py | 6 ++---- ffcx/codegeneration/numba/numba_implementation.py | 15 ++++++++++++--- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/ffcx/codegeneration/C/__init__.py b/ffcx/codegeneration/C/__init__.py index 94ed6e940..7b72e881d 100644 --- a/ffcx/codegeneration/C/__init__.py +++ b/ffcx/codegeneration/C/__init__.py @@ -1,3 +1,5 @@ """Generation of C code.""" -from ffcx.codegeneration.C import form, expressions, integrals, file # noqa + +from ffcx.codegeneration.C import form, expressions, integrals, file # noqa + suffixes = (".h", ".c") diff --git a/ffcx/codegeneration/numba/__init__.py b/ffcx/codegeneration/numba/__init__.py index 354a02e9d..7f1f37928 100644 --- a/ffcx/codegeneration/numba/__init__.py +++ b/ffcx/codegeneration/numba/__init__.py @@ -1,3 +1,3 @@ -from ffcx.codegeneration.numba import form, expressions, integrals, file # noqa +from ffcx.codegeneration.numba import form, expressions, integrals, file # noqa suffixes = (None, "_numba.py") diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index e2a0617c0..fef895349 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -85,9 +85,7 @@ def generator(ir: ExpressionIR, options): # FIXME: Should be handled differently, revise how # ufcx_function_space is generated (also for ufcx_form) for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): - code += [ - f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} =" - ] + code += [f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} ="] code += ["{"] code += [f".finite_element = &{element},"] code += [f".dofmap = &{dofmap},"] @@ -105,9 +103,9 @@ def generator(ir: ExpressionIR, options): for (name, _) in ir.function_spaces.items() ) n = len(ir.function_spaces.items()) - d[ - "function_spaces_init" - ] = f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" + d["function_spaces_init"] = ( + f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" + ) else: d["function_spaces"] = "NULL" d["function_spaces_init"] = "" diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py index fbe48f433..7e58cb284 100644 --- a/ffcx/codegeneration/numba/file.py +++ b/ffcx/codegeneration/numba/file.py @@ -8,7 +8,6 @@ # old implementation in FFC """Generate file output for numba.""" - import logging import pprint import textwrap diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 10433c29e..b1706810b 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -29,7 +29,7 @@ def generator(ir, options): d["num_coefficients"] = ir.num_coefficients d["num_constants"] = ir.num_constants - orig_coeff = ', '.join(str(i) for i in ir.original_coefficient_positions) + orig_coeff = ", ".join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_position"] = f"[{orig_coeff}]" cnames = ir.coefficient_names @@ -59,9 +59,7 @@ def generator(ir, options): # Check that no keys are redundant or have been missed from string import Formatter - fields = [ - fname for _, fname, _, _ in Formatter().parse(form_template.factory) if fname - ] + fields = [fname for _, fname, _, _ in Formatter().parse(form_template.factory) if fname] for f in fields: if f not in d.keys(): diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 2cd025ef7..7c788b18c 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -1,4 +1,5 @@ """Numba implementation for output.""" + import ffcx.codegeneration.lnodes as L @@ -15,6 +16,7 @@ def build_initializer_lists(values): class NumbaFormatter: """Implementation for numba output backend.""" + def __init__(self, scalar) -> None: """Initialise.""" self.scalar_type = scalar @@ -185,9 +187,16 @@ def format_symbol(self, s): def format_mathfunction(self, f): """Format a math function.""" - function_map = {"ln": "log", "acos": "arccos", "asin": "arcsin", - "atan": "arctan", "atan2": "arctan2", "acosh": "arccosh", - "asinh": "arcsinh", "atanh": "arctanh"} + function_map = { + "ln": "log", + "acos": "arccos", + "asin": "arcsin", + "atan": "arctan", + "atan2": "arctan2", + "acosh": "arccosh", + "asinh": "arcsinh", + "atanh": "arctanh", + } function = function_map.get(f.function, f.function) args = [self.c_format(arg) for arg in f.args] if "bessel" in function: From a2079c37a842b8d544dc4866817aed78f18cb184 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:41:04 +0100 Subject: [PATCH 015/106] add __all__ --- ffcx/codegeneration/C/__init__.py | 4 +++- ffcx/codegeneration/numba/__init__.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/C/__init__.py b/ffcx/codegeneration/C/__init__.py index 7b72e881d..030b8ca62 100644 --- a/ffcx/codegeneration/C/__init__.py +++ b/ffcx/codegeneration/C/__init__.py @@ -1,5 +1,7 @@ """Generation of C code.""" -from ffcx.codegeneration.C import form, expressions, integrals, file # noqa +from ffcx.codegeneration.C import expressions, file, form, integrals suffixes = (".h", ".c") + +__all__ = ["expressions", "file", "form", "integrals", "suffixes"] diff --git a/ffcx/codegeneration/numba/__init__.py b/ffcx/codegeneration/numba/__init__.py index 7f1f37928..cc6364fb9 100644 --- a/ffcx/codegeneration/numba/__init__.py +++ b/ffcx/codegeneration/numba/__init__.py @@ -1,3 +1,7 @@ -from ffcx.codegeneration.numba import form, expressions, integrals, file # noqa +"""Generation of numba code.""" + +from ffcx.codegeneration.numba import expressions, file, form, integrals suffixes = (None, "_numba.py") + +__all__ = ["expressions", "file", "form", "integrals", "suffixes"] From d651099a32163a31e94d9d0a969da381fb20f20a Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:45:53 +0100 Subject: [PATCH 016/106] Test numba standalone --- demo/test_demos.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 8c5c290f8..f7284470e 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -15,8 +15,7 @@ @pytest.mark.parametrize("file", ufl_files) @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) -@pytest.mark.parametrize("language", ["C", "numba"]) -def test_demo(file, scalar_type, language): +def test_C(file, scalar_type): """Test a demo.""" if sys.platform.startswith("win32") and "complex" in scalar_type: # Skip complex demos on win32 @@ -33,7 +32,7 @@ def test_demo(file, scalar_type, language): # Skip demos that are only implemented for complex scalars pytest.skip(reason="Not implemented for real types") - opts = f"--scalar_type {scalar_type} -L {language}" + opts = f"--scalar_type {scalar_type}" if sys.platform.startswith("win32"): extra_flags = "/std:c17" @@ -59,3 +58,13 @@ def test_demo(file, scalar_type, language): os.system(f"cd {demo_dir} && {cc} -I../ffcx/codegeneration {extra_flags} -c {file}.c") == 0 ) + + +@pytest.mark.parametrize("file", ufl_files) +# TODO: scalar_type +def test_numba(file): + """Test numba generation.""" + opts = "-L numba" + + assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 + assert os.system(f"python {file}.py") == 0 From 5b3c7ad132d843ebdac2f61fcdae780ebc92ece8 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:04:58 +0100 Subject: [PATCH 017/106] Get CI up --- demo/MassAction.py | 1 + demo/test_demos.py | 10 ++- ffcx/codegeneration/numba/expressions.py | 71 ++++++++++--------- .../numba/expressions_template.py | 2 +- 4 files changed, 47 insertions(+), 37 deletions(-) diff --git a/demo/MassAction.py b/demo/MassAction.py index 59f42ecd3..2a02e6b8a 100644 --- a/demo/MassAction.py +++ b/demo/MassAction.py @@ -6,6 +6,7 @@ """Mass action demo.""" import basix +import basix.ufl import ufl P = 3 diff --git a/demo/test_demos.py b/demo/test_demos.py index f7284470e..2f2fa8775 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -61,10 +61,14 @@ def test_C(file, scalar_type): @pytest.mark.parametrize("file", ufl_files) -# TODO: scalar_type -def test_numba(file): +@pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) +def test_numba(file, scalar_type): """Test numba generation.""" opts = "-L numba" + if "Complex" in file and scalar_type in ["float64", "float32"]: + # Skip demos that are only implemented for complex scalars + pytest.skip(reason="Not implemented for real types") + assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 - assert os.system(f"python {file}.py") == 0 + assert os.system(f"cd {demo_dir} && python {file}.py") == 0 diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index fef895349..ce5ba2690 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -7,10 +7,13 @@ import logging +import numpy as np + from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter +from ffcx.codegeneration.utils import dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR logger = logging.getLogger("ffcx") @@ -40,7 +43,7 @@ def generator(ir: ExpressionIR, options): parts = eg.generate() tensor_size = 1 - for dim in ir.tensor_shape: + for dim in ir.expression.tensor_shape: tensor_size *= dim n_coeff = 1000 n_const = 1000 @@ -61,19 +64,21 @@ def generator(ir: ExpressionIR, options): originals = ", ".join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_positions"] = f"[{originals}]" - points = ", ".join(str(p) for p in ir.points.flatten()) - n = ir.points.size - d["points"] = f"[{points}]" + n = points.size + d["points"] = f"[{', '.join(str(p) for p in points.flatten())}]" - shape = ", ".join(str(i) for i in ir.expression_shape) + shape = ", ".join(str(i) for i in ir.expression.shape) d["value_shape"] = f"[{shape}]" - d["num_components"] = len(ir.expression_shape) - d["num_coefficients"] = len(ir.coefficient_numbering) + d["num_components"] = len(ir.expression.shape) + d["num_coefficients"] = len(ir.expression.coefficient_numbering) d["num_constants"] = len(ir.constant_names) - d["num_points"] = ir.points.shape[0] - d["topological_dimension"] = ir.points.shape[1] + d["num_points"] = points.shape[0] + d["topological_dimension"] = points.shape[1] d["scalar_type"] = options["scalar_type"] - d["rank"] = len(ir.tensor_shape) + d["geom_type"] = dtype_to_scalar_dtype(options["scalar_type"]) + + d["rank"] = len(ir.expression.tensor_shape) + d["np_scalar_type"] = np.dtype(options["scalar_type"]).names names = ", ".join(f'"{name}"' for name in ir.coefficient_names) d["coefficient_names"] = f"[{names}]" @@ -84,33 +89,33 @@ def generator(ir: ExpressionIR, options): # FIXME: Should be handled differently, revise how # ufcx_function_space is generated (also for ufcx_form) - for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): - code += [f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} ="] - code += ["{"] - code += [f".finite_element = &{element},"] - code += [f".dofmap = &{dofmap},"] - code += [f'.geometry_family = "{cmap_family}",'] - code += [f".geometry_degree = {cmap_degree}"] - code += ["};"] + # for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): + # code += [f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} ="] + # code += ["{"] + # code += [f".finite_element = &{element},"] + # code += [f".dofmap = &{dofmap},"] + # code += [f'.geometry_family = "{cmap_family}",'] + # code += [f".geometry_degree = {cmap_degree}"] + # code += ["};"] d["function_spaces_alloc"] = "\n".join(code) d["function_spaces"] = "" - if len(ir.function_spaces) > 0: - d["function_spaces"] = f"function_spaces_{ir.name}" - fs_list = ", ".join( - f"&function_space_{name}_{ir.name_from_uflfile}" - for (name, _) in ir.function_spaces.items() - ) - n = len(ir.function_spaces.items()) - d["function_spaces_init"] = ( - f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" - ) - else: - d["function_spaces"] = "NULL" - d["function_spaces_init"] = "" - - d["function_spaces"] = "0" + # if len(ir.function_spaces) > 0: + # d["function_spaces"] = f"function_spaces_{ir.name}" + # fs_list = ", ".join( + # f"&function_space_{name}_{ir.name_from_uflfile}" + # for (name, _) in ir.function_spaces.items() + # ) + # n = len(ir.function_spaces.items()) + # d["function_spaces_init"] = ( + # f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" + # ) + # else: + # d["function_spaces"] = "NULL" + # d["function_spaces_init"] = "" + + # d["function_spaces"] = "0" # Check that no keys are redundant or have been missed # from string import Formatter diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index c135daef1..bf8912a55 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -32,7 +32,7 @@ class {factory_name}: value_shape = {value_shape} num_components = {num_components} rank = {rank} - function_spaces = {function_spaces} + # function_spaces = {function_spaces} # End of code for expression {factory_name} """ From 336332484ca02b36baffb8c752453f7106e33bf9 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:18:33 +0100 Subject: [PATCH 018/106] dependency numba --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4793872a1..0904cc70b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ authors = [ dependencies = [ "numpy>=1.21", "cffi", + "numba", "setuptools>=77.0.3", # cffi with compilation support requires setuptools "fenics-basix>=0.11.0.dev0", "fenics-ufl>=2025.2.0", @@ -33,7 +34,7 @@ ffcx = "ffcx:__main__.main" [project.optional-dependencies] lint = ["ruff"] docs = ["sphinx", "sphinx_rtd_theme"] -optional = ["numba", "pygraphviz==1.9"] +optional = ["pygraphviz==1.9"] test = ["pytest >= 6.0", "sympy", "numba"] ci = [ "coveralls", From b98b94514b6b30fd289564b2712b439c13980c9f Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:29:46 +0100 Subject: [PATCH 019/106] Explicit --- demo/test_demos.py | 2 +- ffcx/codegeneration/codegeneration.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 2f2fa8775..01be97ddb 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -9,7 +9,7 @@ ufl_files = [] for file in os.listdir(demo_dir): - if file.endswith(".py") and not file == "test_demos.py": + if file.endswith(".py") and not file.endswith("_numba.py") and not file == "test_demos.py": ufl_files.append(file[:-3]) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 22d72bed1..cb8bce3fd 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -46,15 +46,15 @@ def generate_code( logger.info(79 * "*") lang = options.get("language", "C") - try: + # try: # Built-in - mod = import_module(f"ffcx.codegeneration.{lang}") - except ImportError: - # User defined language (experimental) - store_path = sys.path - sys.path = ["."] - mod = import_module(f"{lang}") - sys.path = store_path + mod = import_module(f"ffcx.codegeneration.{lang}") + # except ImportError: + # # User defined language (experimental) + # store_path = sys.path + # sys.path = ["."] + # mod = import_module(f"{lang}") + # sys.path = store_path integral_generator = mod.integrals.generator form_generator = mod.form.generator From 4724e1ac5a63f0ec45b11949ef1622948fe1728b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:43:01 +0100 Subject: [PATCH 020/106] install numba module --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0904cc70b..ef67f049b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ packages = [ "ffcx", "ffcx.codegeneration", "ffcx.codegeneration.C", + "ffcx.codegeneration.numba", "ffcx.ir", "ffcx.ir.analysis", ] From 1543c5e3ab1172ffe624c17a0cea8ece612ae743 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:48:21 +0100 Subject: [PATCH 021/106] parallel test execution --- .github/workflows/pythonapp.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index e7ffbe9c0..f3f1fd81b 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -122,8 +122,10 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 - name: Run FFCx demos - run: | + run: > pytest demo/test_demos.py + -n auto + -W error - name: Build documentation run: | From 1420eb198ccfa0ec7ebaaff8a14a56fbb68d0c3e Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:57:42 +0100 Subject: [PATCH 022/106] Fix complex --- demo/test_demos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 01be97ddb..00a18e742 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -64,7 +64,7 @@ def test_C(file, scalar_type): @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) def test_numba(file, scalar_type): """Test numba generation.""" - opts = "-L numba" + opts = f"-L numba --scalar_type {scalar_type}" if "Complex" in file and scalar_type in ["float64", "float32"]: # Skip demos that are only implemented for complex scalars From 96eafeee81e9e74da3556da829190cd046753289 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:01:48 +0100 Subject: [PATCH 023/106] Make optional --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef67f049b..74bd10836 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ authors = [ dependencies = [ "numpy>=1.21", "cffi", - "numba", "setuptools>=77.0.3", # cffi with compilation support requires setuptools "fenics-basix>=0.11.0.dev0", "fenics-ufl>=2025.2.0", From c4aeb22e82063def7f64cc6fb1b9a6c074229538 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:09:29 +0100 Subject: [PATCH 024/106] Skip real only demos --- demo/test_demos.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 00a18e742..001fb9f3c 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -12,6 +12,8 @@ if file.endswith(".py") and not file.endswith("_numba.py") and not file == "test_demos.py": ufl_files.append(file[:-3]) +skip_complex = ["BiharmonicHHJ", "BiharmonicRegge", "StabilisedStokes"] + @pytest.mark.parametrize("file", ufl_files) @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) @@ -21,11 +23,7 @@ def test_C(file, scalar_type): # Skip complex demos on win32 pytest.skip(reason="_Complex not supported on Windows") - if "complex" in scalar_type and file in [ - "BiharmonicHHJ", - "BiharmonicRegge", - "StabilisedStokes", - ]: + if "complex" in scalar_type and file in skip_complex: # Skip demos that are not implemented for complex scalars pytest.skip(reason="Not implemented for complex types") elif "Complex" in file and scalar_type in ["float64", "float32"]: @@ -66,7 +64,10 @@ def test_numba(file, scalar_type): """Test numba generation.""" opts = f"-L numba --scalar_type {scalar_type}" - if "Complex" in file and scalar_type in ["float64", "float32"]: + if "complex" in scalar_type and file in skip_complex: + # Skip demos that are not implemented for complex scalars + pytest.skip(reason="Not implemented for complex types") + elif "Complex" in file and scalar_type in ["float64", "float32"]: # Skip demos that are only implemented for complex scalars pytest.skip(reason="Not implemented for real types") From bd1d9330f41cb89480973299446b179418c782fe Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:29:09 +0100 Subject: [PATCH 025/106] Reactivate ruff --- .github/workflows/pythonapp.yml | 8 ++++---- ffcx/codegeneration/codegeneration.py | 3 +-- ffcx/codegeneration/numba/expressions.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index f3f1fd81b..66965d6b4 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -88,10 +88,10 @@ jobs: # - name: Static check with mypy # run: mypy -p ffcx - # - name: ruff checks - # run: | - # ruff check . - # ruff format --check . + - name: ruff checks + run: | + ruff check . + ruff format --check . - name: Run unit tests run: > diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index cb8bce3fd..2e5331df0 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -13,7 +13,6 @@ from __future__ import annotations import logging -import sys import typing from importlib import import_module @@ -47,7 +46,7 @@ def generate_code( lang = options.get("language", "C") # try: - # Built-in + # Built-in mod = import_module(f"ffcx.codegeneration.{lang}") # except ImportError: # # User defined language (experimental) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index ce5ba2690..ad7daf315 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -64,7 +64,7 @@ def generator(ir: ExpressionIR, options): originals = ", ".join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_positions"] = f"[{originals}]" - n = points.size + # n = points.size d["points"] = f"[{', '.join(str(p) for p in points.flatten())}]" shape = ", ".join(str(i) for i in ir.expression.shape) From f85afdaad30665c53f0f84365f7ae9c875300118 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:45:10 +0100 Subject: [PATCH 026/106] Start on unit testing (almost passing) --- test/laplace_numba.py | 181 ++++++++++++++++++++++++++++++++++++++++++ test/test_numba.py | 63 +++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 test/laplace_numba.py create mode 100644 test/test_numba.py diff --git a/test/laplace_numba.py b/test/laplace_numba.py new file mode 100644 index 000000000..9fd89b082 --- /dev/null +++ b/test/laplace_numba.py @@ -0,0 +1,181 @@ + +# This code conforms with the UFC specification version 2018.2.0.dev0 +# and was automatically generated by FFCx version 0.11.0.dev0. +# +# This code was generated with the following options: +# +# {'epsilon': 1e-14, +# 'language': 'numba', +# 'output_directory': '.', +# 'part': 'full', +# 'profile': False, +# 'scalar_type': 'float64', +# 'sum_factorization': False, +# 'table_atol': 1e-09, +# 'table_rtol': 1e-06, +# 'ufl_file': ['/Users/paul.kuehner/dev/ffcx/test/laplace.py'], +# 'verbosity': 30, +# 'visualise': False} + +import numba +import numpy as np +import math + +# ufcx enums +interval = 10 +triangle = 20 +quadrilateral = 30 +tetrahedron = 40 +hexahedron = 50 +vertex = 60 +prism = 70 +pyramid = 80 + +cell = 0 +exterior_facet = 1 +interior_facet = 2 + +ufcx_basix_element = 0 +ufcx_mixed_element = 1 +ufcx_quadrature_element = 2 +ufcx_basix_custom_element = 3 + + +# Code for integral integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 +import numba + +def tabulate_tensor_integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086(_A, _w, _c, _coordinate_dofs, + _entity_local_index, _quadrature_permutation, void): + + A = numba.carray(_A, (9)) + w = numba.carray(_w, (0)) + c = numba.carray(_c, (4)) + coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) + entity_local_index = numba.carray(_entity_local_index, (1000)) + quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) + # Quadrature rules + weights_083 = np.array([0.5], dtype=coordinate_dofs.dtype) + # Precomputed values of basis functions and precomputations + # FE* dimensions: [permutation][entities][points][dofs] + FE0_C0_D01_Q083 = np.array([[[[-1.0, 0.0, 1.0]]]], dtype=coordinate_dofs.dtype) + FE1_C0_D10_Q083 = np.array([[[[-1.0, 1.0, 0.0]]]], dtype=coordinate_dofs.dtype) + # ------------------------ + # Section: Jacobian + # Inputs: FE0_C0_D01_Q083, coordinate_dofs, FE1_C0_D10_Q083 + # Outputs: J0_c1, J0_c3, J0_c2, J0_c0 + J0_c0 = 0.0 + J0_c3 = 0.0 + J0_c1 = 0.0 + J0_c2 = 0.0 + for ic in range(0, 3): + J0_c0 += coordinate_dofs[(ic) * 3] * FE1_C0_D10_Q083[0, 0, 0, ic] + J0_c3 += coordinate_dofs[(ic) * 3 + 1] * FE0_C0_D01_Q083[0, 0, 0, ic] + J0_c1 += coordinate_dofs[(ic) * 3] * FE0_C0_D01_Q083[0, 0, 0, ic] + J0_c2 += coordinate_dofs[(ic) * 3 + 1] * FE1_C0_D10_Q083[0, 0, 0, ic] + + # ------------------------ + sp_083_0 = c[0] + c[3] + sp_083_1 = J0_c0 * J0_c3 + sp_083_2 = J0_c1 * J0_c2 + sp_083_3 = -sp_083_2 + sp_083_4 = sp_083_1 + sp_083_3 + sp_083_5 = J0_c0 / sp_083_4 + sp_083_6 = -J0_c1 + sp_083_7 = sp_083_6 / sp_083_4 + sp_083_8 = sp_083_5 * sp_083_5 + sp_083_9 = sp_083_5 * sp_083_7 + sp_083_10 = sp_083_7 * sp_083_7 + sp_083_11 = J0_c3 / sp_083_4 + sp_083_12 = -J0_c2 + sp_083_13 = sp_083_12 / sp_083_4 + sp_083_14 = sp_083_13 * sp_083_13 + sp_083_15 = sp_083_11 * sp_083_13 + sp_083_16 = sp_083_11 * sp_083_11 + sp_083_17 = sp_083_8 + sp_083_14 + sp_083_18 = sp_083_9 + sp_083_15 + sp_083_19 = sp_083_16 + sp_083_10 + sp_083_20 = sp_083_0 * sp_083_17 + sp_083_21 = sp_083_0 * sp_083_18 + sp_083_22 = sp_083_0 * sp_083_19 + sp_083_23 = np.abs(sp_083_4) + sp_083_24 = sp_083_20 * sp_083_23 + sp_083_25 = sp_083_21 * sp_083_23 + sp_083_26 = sp_083_22 * sp_083_23 + for iq in range(0, 1): + # ------------------------ + # Section: Intermediates + # Inputs: + # Outputs: fw0, fw1, fw2 + fw0 = 0 + fw1 = 0 + fw2 = 0 + fw0 = sp_083_26 * weights_083[iq] + fw1 = sp_083_25 * weights_083[iq] + fw2 = sp_083_24 * weights_083[iq] + # ------------------------ + # ------------------------ + # Section: Tensor Computation + # Inputs: fw2, fw0, FE1_C0_D10_Q083, fw1, FE0_C0_D01_Q083 + # Outputs: A + # temp_0 = np.array([3], dtype=A.dtype) + temp_0 = np.zeros(3, dtype=A.dtype) + for j in range(0, 3): + temp_0[j] = fw0 * FE1_C0_D10_Q083[0, 0, 0, j] + + # temp_1 = np.array([0], dtype=A.dtype) + temp_1 = np.zeros(3, dtype=A.dtype) + for j in range(0, 3): + temp_1[j] = fw1 * FE0_C0_D01_Q083[0, 0, 0, j] + + # temp_2 = np.array([0], dtype=A.dtype) + temp_2 = np.zeros(3, dtype=A.dtype) + for j in range(0, 3): + temp_2[j] = fw1 * FE1_C0_D10_Q083[0, 0, 0, j] + + # temp_3 = np.array([0], dtype=A.dtype) + temp_3 = np.zeros(3, dtype=A.dtype) + for j in range(0, 3): + temp_3[j] = fw2 * FE0_C0_D01_Q083[0, 0, 0, j] + + for j in range(0, 3): + for i in range(0, 3): + A[3 * (i) + (j)] += FE1_C0_D10_Q083[0, 0, 0, i] * temp_0[j] + A[3 * (i) + (j)] += FE1_C0_D10_Q083[0, 0, 0, i] * temp_1[j] + A[3 * (i) + (j)] += FE0_C0_D01_Q083[0, 0, 0, i] * temp_2[j] + A[3 * (i) + (j)] += FE0_C0_D01_Q083[0, 0, 0, i] * temp_3[j] + + + # ------------------------ + + + +class integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086(object): + enabled_coefficients = [] + tabulate_tensor = tabulate_tensor_integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 + needs_facet_permutations = False + coordinate_element = 16933917890882727400 + +# End of code for integral integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 + +# Code for form form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd + +class form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd(object): + + signature = "427bcdcec006203ebb42559e15801e26779e7ce2f608ec02be89b550281415956f79607a4f6aae91dfaa53179dd2229ece5a149d6c5f82d33c28397879fade32" + rank = 2 + num_coefficients = 0 + num_constants = 1 + original_coefficient_position = [] + + coefficient_name_map = [] + constant_name_map = ["kappa"] + + form_integrals = [integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086] + form_integral_ids = [-1] + form_integral_offsets = [0, 1, 1, 1] + +form_laplace_a = form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd + +# Name: form_laplace_a +# End of code for form form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd + diff --git a/test/test_numba.py b/test/test_numba.py new file mode 100644 index 000000000..e298d3309 --- /dev/null +++ b/test/test_numba.py @@ -0,0 +1,63 @@ +# Copyright (C) 2025 Paul T. Kühner +# +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +import ctypes +import importlib +import subprocess +from pathlib import Path + +import cffi +import numba +import numpy as np + +from ffcx.codegeneration.utils import dtype_to_scalar_dtype, numba_ufcx_kernel_signature + + +def test_poisson(): + subprocess.run( + ["ffcx", Path(__file__).parent / "laplace.py", "--language", "numba"], check=True + ) + + laplace = importlib.import_module("laplace_numba") + + print(dir(laplace)) + + def wrapper(scalar_type, real_type): + c_signature = numba_ufcx_kernel_signature(scalar_type, real_type) + return numba.cfunc(c_signature, nopython=True) + + dtype = np.float64 + realtype = np.dtype(dtype(0).real) + a_kernel = wrapper(dtype, realtype)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) + + A = np.zeros((3, 3), dtype=dtype) + w = np.array([], dtype=dtype) + kappa_value = np.array([[1.0, 2.0], [3.0, 4.0]]) + c = np.array(kappa_value.flatten(), dtype=dtype) + + xdtype = dtype_to_scalar_dtype(dtype) + ffi = cffi.FFI() + coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=xdtype) + + empty = np.empty((0,), dtype=realtype) + + c_type = "double" + # c_xtype = "double" + print(a_kernel) + print(type(ffi.cast(f"{c_type} *", A.ctypes.data))) + print(dir(ffi.cast(f"{c_type} *", A.ctypes.data))) + a_kernel( + A.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + coords.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + 0, + ) + + A_expected = np.array([[1.0, -0.5, -0.5], [-0.5, 0.5, 0.0], [-0.5, 0.0, 0.5]], dtype=np.float64) + + assert np.allclose(A, np.trace(kappa_value) * A_expected) From 655d92dd757b25178e182d0fb79e8ad0e6720b7d Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:49:15 +0100 Subject: [PATCH 027/106] Commit ufl, not kernel --- test/laplace.py | 28 +++++++ test/laplace_numba.py | 181 ------------------------------------------ 2 files changed, 28 insertions(+), 181 deletions(-) create mode 100644 test/laplace.py delete mode 100644 test/laplace_numba.py diff --git a/test/laplace.py b/test/laplace.py new file mode 100644 index 000000000..2360e0f4b --- /dev/null +++ b/test/laplace.py @@ -0,0 +1,28 @@ + +import basix.ufl +from ufl import ( + Coefficient, + Constant, + FunctionSpace, + Mesh, + TestFunction, + TrialFunction, + dx, + grad, + inner, + tr, +) + +mesh = Mesh(basix.ufl.element("P", "triangle", 1, shape=(2,))) + +e = basix.ufl.element("Lagrange", "triangle", 1) +space = FunctionSpace(mesh, e) + +u = TrialFunction(space) +v = TestFunction(space) +f = Coefficient(space) + +kappa = Constant(mesh, shape=(2, 2)) + +a = tr(kappa) * inner(grad(u), grad(v)) * dx +# L = f * v * dx diff --git a/test/laplace_numba.py b/test/laplace_numba.py deleted file mode 100644 index 9fd89b082..000000000 --- a/test/laplace_numba.py +++ /dev/null @@ -1,181 +0,0 @@ - -# This code conforms with the UFC specification version 2018.2.0.dev0 -# and was automatically generated by FFCx version 0.11.0.dev0. -# -# This code was generated with the following options: -# -# {'epsilon': 1e-14, -# 'language': 'numba', -# 'output_directory': '.', -# 'part': 'full', -# 'profile': False, -# 'scalar_type': 'float64', -# 'sum_factorization': False, -# 'table_atol': 1e-09, -# 'table_rtol': 1e-06, -# 'ufl_file': ['/Users/paul.kuehner/dev/ffcx/test/laplace.py'], -# 'verbosity': 30, -# 'visualise': False} - -import numba -import numpy as np -import math - -# ufcx enums -interval = 10 -triangle = 20 -quadrilateral = 30 -tetrahedron = 40 -hexahedron = 50 -vertex = 60 -prism = 70 -pyramid = 80 - -cell = 0 -exterior_facet = 1 -interior_facet = 2 - -ufcx_basix_element = 0 -ufcx_mixed_element = 1 -ufcx_quadrature_element = 2 -ufcx_basix_custom_element = 3 - - -# Code for integral integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 -import numba - -def tabulate_tensor_integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086(_A, _w, _c, _coordinate_dofs, - _entity_local_index, _quadrature_permutation, void): - - A = numba.carray(_A, (9)) - w = numba.carray(_w, (0)) - c = numba.carray(_c, (4)) - coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) - entity_local_index = numba.carray(_entity_local_index, (1000)) - quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) - # Quadrature rules - weights_083 = np.array([0.5], dtype=coordinate_dofs.dtype) - # Precomputed values of basis functions and precomputations - # FE* dimensions: [permutation][entities][points][dofs] - FE0_C0_D01_Q083 = np.array([[[[-1.0, 0.0, 1.0]]]], dtype=coordinate_dofs.dtype) - FE1_C0_D10_Q083 = np.array([[[[-1.0, 1.0, 0.0]]]], dtype=coordinate_dofs.dtype) - # ------------------------ - # Section: Jacobian - # Inputs: FE0_C0_D01_Q083, coordinate_dofs, FE1_C0_D10_Q083 - # Outputs: J0_c1, J0_c3, J0_c2, J0_c0 - J0_c0 = 0.0 - J0_c3 = 0.0 - J0_c1 = 0.0 - J0_c2 = 0.0 - for ic in range(0, 3): - J0_c0 += coordinate_dofs[(ic) * 3] * FE1_C0_D10_Q083[0, 0, 0, ic] - J0_c3 += coordinate_dofs[(ic) * 3 + 1] * FE0_C0_D01_Q083[0, 0, 0, ic] - J0_c1 += coordinate_dofs[(ic) * 3] * FE0_C0_D01_Q083[0, 0, 0, ic] - J0_c2 += coordinate_dofs[(ic) * 3 + 1] * FE1_C0_D10_Q083[0, 0, 0, ic] - - # ------------------------ - sp_083_0 = c[0] + c[3] - sp_083_1 = J0_c0 * J0_c3 - sp_083_2 = J0_c1 * J0_c2 - sp_083_3 = -sp_083_2 - sp_083_4 = sp_083_1 + sp_083_3 - sp_083_5 = J0_c0 / sp_083_4 - sp_083_6 = -J0_c1 - sp_083_7 = sp_083_6 / sp_083_4 - sp_083_8 = sp_083_5 * sp_083_5 - sp_083_9 = sp_083_5 * sp_083_7 - sp_083_10 = sp_083_7 * sp_083_7 - sp_083_11 = J0_c3 / sp_083_4 - sp_083_12 = -J0_c2 - sp_083_13 = sp_083_12 / sp_083_4 - sp_083_14 = sp_083_13 * sp_083_13 - sp_083_15 = sp_083_11 * sp_083_13 - sp_083_16 = sp_083_11 * sp_083_11 - sp_083_17 = sp_083_8 + sp_083_14 - sp_083_18 = sp_083_9 + sp_083_15 - sp_083_19 = sp_083_16 + sp_083_10 - sp_083_20 = sp_083_0 * sp_083_17 - sp_083_21 = sp_083_0 * sp_083_18 - sp_083_22 = sp_083_0 * sp_083_19 - sp_083_23 = np.abs(sp_083_4) - sp_083_24 = sp_083_20 * sp_083_23 - sp_083_25 = sp_083_21 * sp_083_23 - sp_083_26 = sp_083_22 * sp_083_23 - for iq in range(0, 1): - # ------------------------ - # Section: Intermediates - # Inputs: - # Outputs: fw0, fw1, fw2 - fw0 = 0 - fw1 = 0 - fw2 = 0 - fw0 = sp_083_26 * weights_083[iq] - fw1 = sp_083_25 * weights_083[iq] - fw2 = sp_083_24 * weights_083[iq] - # ------------------------ - # ------------------------ - # Section: Tensor Computation - # Inputs: fw2, fw0, FE1_C0_D10_Q083, fw1, FE0_C0_D01_Q083 - # Outputs: A - # temp_0 = np.array([3], dtype=A.dtype) - temp_0 = np.zeros(3, dtype=A.dtype) - for j in range(0, 3): - temp_0[j] = fw0 * FE1_C0_D10_Q083[0, 0, 0, j] - - # temp_1 = np.array([0], dtype=A.dtype) - temp_1 = np.zeros(3, dtype=A.dtype) - for j in range(0, 3): - temp_1[j] = fw1 * FE0_C0_D01_Q083[0, 0, 0, j] - - # temp_2 = np.array([0], dtype=A.dtype) - temp_2 = np.zeros(3, dtype=A.dtype) - for j in range(0, 3): - temp_2[j] = fw1 * FE1_C0_D10_Q083[0, 0, 0, j] - - # temp_3 = np.array([0], dtype=A.dtype) - temp_3 = np.zeros(3, dtype=A.dtype) - for j in range(0, 3): - temp_3[j] = fw2 * FE0_C0_D01_Q083[0, 0, 0, j] - - for j in range(0, 3): - for i in range(0, 3): - A[3 * (i) + (j)] += FE1_C0_D10_Q083[0, 0, 0, i] * temp_0[j] - A[3 * (i) + (j)] += FE1_C0_D10_Q083[0, 0, 0, i] * temp_1[j] - A[3 * (i) + (j)] += FE0_C0_D01_Q083[0, 0, 0, i] * temp_2[j] - A[3 * (i) + (j)] += FE0_C0_D01_Q083[0, 0, 0, i] * temp_3[j] - - - # ------------------------ - - - -class integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086(object): - enabled_coefficients = [] - tabulate_tensor = tabulate_tensor_integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 - needs_facet_permutations = False - coordinate_element = 16933917890882727400 - -# End of code for integral integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086 - -# Code for form form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd - -class form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd(object): - - signature = "427bcdcec006203ebb42559e15801e26779e7ce2f608ec02be89b550281415956f79607a4f6aae91dfaa53179dd2229ece5a149d6c5f82d33c28397879fade32" - rank = 2 - num_coefficients = 0 - num_constants = 1 - original_coefficient_position = [] - - coefficient_name_map = [] - constant_name_map = ["kappa"] - - form_integrals = [integral_a2f268a7bbb3d2df737ca07b130e11219a4f8086] - form_integral_ids = [-1] - form_integral_offsets = [0, 1, 1, 1] - -form_laplace_a = form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd - -# Name: form_laplace_a -# End of code for form form_d599f40fd66a26e75317e6f9ae2b76df53aba2cd - From f7d308bdb47730247de463f234b969f138759498 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:53:44 +0100 Subject: [PATCH 028/106] Add custom_data input --- ffcx/codegeneration/numba/integrals_template.py | 2 +- test/laplace.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py index 2efaf74a4..b9731214f 100644 --- a/ffcx/codegeneration/numba/integrals_template.py +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -9,7 +9,7 @@ import numba def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, - _entity_local_index, _quadrature_permutation): + _entity_local_index, _quadrature_permutation, custom_data): {tabulate_tensor} class {factory_name}(object): diff --git a/test/laplace.py b/test/laplace.py index 2360e0f4b..131b5b202 100644 --- a/test/laplace.py +++ b/test/laplace.py @@ -1,4 +1,3 @@ - import basix.ufl from ufl import ( Coefficient, From 09a0576abe31dd656adb1c1c0817f2c8e92d848b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:03:42 +0100 Subject: [PATCH 029/106] Fix n_const computation --- ffcx/codegeneration/numba/integrals.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index f7241c218..3aa323170 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -8,6 +8,7 @@ import logging import basix +import numpy as np from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator @@ -53,7 +54,10 @@ def generator(ir, domain: basix.CellType, options): for dim in ir.expression.tensor_shape: tensor_size *= dim n_coeff = len(ir.enabled_coefficients) - n_const = len(ir.expression.original_constant_offsets) + n_const = sum( + np.prod(constant.ufl_shape, dtype=int) + for constant in ir.expression.original_constant_offsets.keys() + ) header = f""" A = numba.carray(_A, ({tensor_size})) From 5b9253af53c9f73dfe06b7c936a6e7d853b4a0ee Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:22:18 +0100 Subject: [PATCH 030/106] Add array creation by (full) scalar value --- ffcx/codegeneration/numba/numba_implementation.py | 4 ++++ test/test_numba.py | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 7c788b18c..2dec3b7e6 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -1,5 +1,7 @@ """Numba implementation for output.""" +import numpy as np + import ffcx.codegeneration.lnodes as L @@ -59,6 +61,8 @@ def format_array_decl(self, arr): symbol = self.c_format(arr.symbol) if arr.values is None: return f"{symbol} = np.empty({arr.sizes}, dtype={dtype})\n" + elif arr.values.size == 1: + return f"{symbol} = np.full({arr.sizes}, {arr.values[0]}, dtype={dtype})\n" av = build_initializer_lists(arr.values) av = "np.array(" + av + f", dtype={dtype})" return f"{symbol} = {av}\n" diff --git a/test/test_numba.py b/test/test_numba.py index e298d3309..66441b4ce 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -8,7 +8,6 @@ import subprocess from pathlib import Path -import cffi import numba import numpy as np @@ -38,16 +37,12 @@ def wrapper(scalar_type, real_type): c = np.array(kappa_value.flatten(), dtype=dtype) xdtype = dtype_to_scalar_dtype(dtype) - ffi = cffi.FFI() coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=xdtype) empty = np.empty((0,), dtype=realtype) - c_type = "double" + # c_type = "double" # c_xtype = "double" - print(a_kernel) - print(type(ffi.cast(f"{c_type} *", A.ctypes.data))) - print(dir(ffi.cast(f"{c_type} *", A.ctypes.data))) a_kernel( A.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), From be5f6b54e0d72b9efa99ccf512a68abbcb0332ee Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:08:09 +0100 Subject: [PATCH 031/106] n_coeff computation --- ffcx/codegeneration/numba/integrals.py | 3 ++- ffcx/codegeneration/numba/numba_implementation.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 3aa323170..2899ae8e7 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -53,7 +53,8 @@ def generator(ir, domain: basix.CellType, options): tensor_size = 1 for dim in ir.expression.tensor_shape: tensor_size *= dim - n_coeff = len(ir.enabled_coefficients) + + n_coeff = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) n_const = sum( np.prod(constant.ufl_shape, dtype=int) for constant in ir.expression.original_constant_offsets.keys() diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 2dec3b7e6..67104f0c0 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -1,7 +1,5 @@ """Numba implementation for output.""" -import numpy as np - import ffcx.codegeneration.lnodes as L From e02c4af654fbb757934a0a98dd0c10852e55e360 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:08:19 +0100 Subject: [PATCH 032/106] Linear form + coefficient kernel --- test/laplace.py | 2 +- test/test_numba.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/test/laplace.py b/test/laplace.py index 131b5b202..63dd10f9d 100644 --- a/test/laplace.py +++ b/test/laplace.py @@ -24,4 +24,4 @@ kappa = Constant(mesh, shape=(2, 2)) a = tr(kappa) * inner(grad(u), grad(v)) * dx -# L = f * v * dx +L = f * v * dx diff --git a/test/test_numba.py b/test/test_numba.py index 66441b4ce..d25a75773 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -29,7 +29,7 @@ def wrapper(scalar_type, real_type): dtype = np.float64 realtype = np.dtype(dtype(0).real) - a_kernel = wrapper(dtype, realtype)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) + kernel_a = wrapper(dtype, realtype)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) A = np.zeros((3, 3), dtype=dtype) w = np.array([], dtype=dtype) @@ -43,7 +43,7 @@ def wrapper(scalar_type, real_type): # c_type = "double" # c_xtype = "double" - a_kernel( + kernel_a( A.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), @@ -56,3 +56,30 @@ def wrapper(scalar_type, real_type): A_expected = np.array([[1.0, -0.5, -0.5], [-0.5, 0.5, 0.0], [-0.5, 0.0, 0.5]], dtype=np.float64) assert np.allclose(A, np.trace(kappa_value) * A_expected) + + kernel_L = wrapper(dtype, realtype)(laplace.form_laplace_L.form_integrals[0].tabulate_tensor) + + b = np.zeros((3,), dtype=dtype) + w = np.full((3,), 0.5, dtype=dtype) + c = np.empty((0,), dtype=dtype) + + xdtype = dtype_to_scalar_dtype(dtype) + coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=xdtype) + + empty = np.empty((0,), dtype=realtype) + + # c_type = "double" + # c_xtype = "double" + kernel_L( + b.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + coords.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + 0, + ) + + b_expected = np.full((3,), 1 / 6, dtype=np.float64) + + assert np.allclose(b, 0.5 * b_expected) From 6233d19f6556b5d230d33034c4b15f1a99a7a587 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:52:18 +0100 Subject: [PATCH 033/106] Introduce number_coordinate_dofs to CommonExpressionIR --- ffcx/ir/integral.py | 1 + ffcx/ir/representation.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ffcx/ir/integral.py b/ffcx/ir/integral.py index aaed1e63b..a89a91082 100644 --- a/ffcx/ir/integral.py +++ b/ffcx/ir/integral.py @@ -80,6 +80,7 @@ class CommonExpressionIR(typing.NamedTuple): needs_facet_permutations: bool shape: list[int] coordinate_element_hash: str + number_coordinate_dofs: int class ModifiedArgumentDataT(typing.NamedTuple): diff --git a/ffcx/ir/representation.py b/ffcx/ir/representation.py index 627db639d..b23144307 100644 --- a/ffcx/ir/representation.py +++ b/ffcx/ir/representation.py @@ -234,6 +234,7 @@ def _compute_integral_ir( "entity_type": entity_type, "shape": (), "coordinate_element_hash": itg_data.domain.ufl_coordinate_element().basix_hash(), + "number_coordinate_dofs": itg_data.domain.ufl_coordinate_element().dim, } ir = { "rank": form_data.rank, @@ -613,6 +614,7 @@ def _compute_expression_ir( base_ir["coordinate_element_hash"] = ( expr_domain.ufl_coordinate_element().basix_hash() if expr_domain is not None else 0 ) + base_ir["number_coordinate_dofs"] = expr_domain.ufl_coordinate_element().dim weights = np.array([1.0] * points.shape[0]) rule = QuadratureRule(points, weights) From 67c1522b2030f9691a37bf32a068e89ca2b125a1 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:55:02 +0100 Subject: [PATCH 034/106] Fix remaining shape computations --- ffcx/codegeneration/numba/integrals.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 2899ae8e7..a869b67e4 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -60,13 +60,17 @@ def generator(ir, domain: basix.CellType, options): for constant in ir.expression.original_constant_offsets.keys() ) + n_coord_dofs = ir.expression.number_coordinate_dofs * 3 + n_entity_local_index = 2 # TODO: this is just an upper bound, harmful? + n_quad_perm = 2 if ir.expression.needs_facet_permutations else 0 + header = f""" A = numba.carray(_A, ({tensor_size})) w = numba.carray(_w, ({n_coeff})) c = numba.carray(_c, ({n_const})) - coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) - entity_local_index = numba.carray(_entity_local_index, (1000)) - quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) + coordinate_dofs = numba.carray(_coordinate_dofs, ({n_coord_dofs})) + entity_local_index = numba.carray(_entity_local_index, ({n_entity_local_index})) + quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) """ code["tabulate_tensor"] = header + body From e0fc38518ef5d677d3cc6f91b296d733c80f9c96 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:00:59 +0100 Subject: [PATCH 035/106] Fix no coordinate element case --- ffcx/ir/representation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ffcx/ir/representation.py b/ffcx/ir/representation.py index b23144307..b0c7fdf00 100644 --- a/ffcx/ir/representation.py +++ b/ffcx/ir/representation.py @@ -614,7 +614,9 @@ def _compute_expression_ir( base_ir["coordinate_element_hash"] = ( expr_domain.ufl_coordinate_element().basix_hash() if expr_domain is not None else 0 ) - base_ir["number_coordinate_dofs"] = expr_domain.ufl_coordinate_element().dim + base_ir["number_coordinate_dofs"] = ( + 0 if expr_domain is None else expr_domain.ufl_coordinate_element().dim + ) weights = np.array([1.0] * points.shape[0]) rule = QuadratureRule(points, weights) From 9ed41a1b3d25c043da054175b3e4ae90d0e3b1d7 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:12:17 +0100 Subject: [PATCH 036/106] Parametrize over scalar type --- test/test_numba.py | 89 ++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/test/test_numba.py b/test/test_numba.py index d25a75773..42ad8a3b3 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -10,76 +10,81 @@ import numba import numpy as np +import numpy.typing as npt +import pytest from ffcx.codegeneration.utils import dtype_to_scalar_dtype, numba_ufcx_kernel_signature -def test_poisson(): - subprocess.run( - ["ffcx", Path(__file__).parent / "laplace.py", "--language", "numba"], check=True - ) +def wrap_kernel(scalar_type, real_type): + c_signature = numba_ufcx_kernel_signature(scalar_type, real_type) + return numba.cfunc(c_signature, nopython=True) - laplace = importlib.import_module("laplace_numba") - print(dir(laplace)) +def as_C_array(np_array: npt.NDArray): + dtype_C = np.ctypeslib.as_ctypes_type(np_array.dtype) + return np_array.ctypes.data_as(ctypes.POINTER(dtype_C)) - def wrapper(scalar_type, real_type): - c_signature = numba_ufcx_kernel_signature(scalar_type, real_type) - return numba.cfunc(c_signature, nopython=True) - dtype = np.float64 - realtype = np.dtype(dtype(0).real) - kernel_a = wrapper(dtype, realtype)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) +@pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes +def test_poisson(scalar_type): + opts = f"--language numba --scalar_type {scalar_type}" + subprocess.run(["ffcx", Path(__file__).parent / "laplace.py", *opts.split(" ")], check=True) + + laplace = importlib.import_module("laplace_numba") + + dtype = np.dtype(scalar_type).type + dtype_r = dtype_to_scalar_dtype(dtype) + + kernel_a = wrap_kernel(dtype, dtype_r)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) A = np.zeros((3, 3), dtype=dtype) w = np.array([], dtype=dtype) kappa_value = np.array([[1.0, 2.0], [3.0, 4.0]]) c = np.array(kappa_value.flatten(), dtype=dtype) + coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=dtype_r) + empty = np.empty((0,), dtype=dtype_r) - xdtype = dtype_to_scalar_dtype(dtype) - coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=xdtype) - - empty = np.empty((0,), dtype=realtype) - - # c_type = "double" - # c_xtype = "double" kernel_a( - A.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - coords.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + as_C_array(A), + as_C_array(w), + as_C_array(c), + as_C_array(coords), + as_C_array(empty), + as_C_array(empty), 0, ) - - A_expected = np.array([[1.0, -0.5, -0.5], [-0.5, 0.5, 0.0], [-0.5, 0.0, 0.5]], dtype=np.float64) + A_expected = np.array( + [[1.0, -0.5, -0.5], [-0.5, 0.5, 0.0], [-0.5, 0.0, 0.5]], dtype=scalar_type + ) assert np.allclose(A, np.trace(kappa_value) * A_expected) - kernel_L = wrapper(dtype, realtype)(laplace.form_laplace_L.form_integrals[0].tabulate_tensor) + kernel_L = wrap_kernel(dtype, dtype_r)(laplace.form_laplace_L.form_integrals[0].tabulate_tensor) b = np.zeros((3,), dtype=dtype) w = np.full((3,), 0.5, dtype=dtype) c = np.empty((0,), dtype=dtype) + coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=dtype_r) + empty = np.empty((0,), dtype=dtype_r) - xdtype = dtype_to_scalar_dtype(dtype) - coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=xdtype) - - empty = np.empty((0,), dtype=realtype) - - # c_type = "double" - # c_xtype = "double" kernel_L( - b.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - w.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - coords.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - empty.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + as_C_array(b), + as_C_array(w), + as_C_array(c), + as_C_array(coords), + as_C_array(empty), + as_C_array(empty), 0, ) b_expected = np.full((3,), 1 / 6, dtype=np.float64) - assert np.allclose(b, 0.5 * b_expected) + + subprocess.run( + [ + "rm", + Path(__file__).parent / "laplace_numba.py", + ], + check=True, + ) From 0761b7089b46566187456f83fd37751a6e965ed7 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:25:31 +0100 Subject: [PATCH 037/106] remove in pwd --- test/test_numba.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/test_numba.py b/test/test_numba.py index 42ad8a3b3..2f01a00c8 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -81,10 +81,4 @@ def test_poisson(scalar_type): b_expected = np.full((3,), 1 / 6, dtype=np.float64) assert np.allclose(b, 0.5 * b_expected) - subprocess.run( - [ - "rm", - Path(__file__).parent / "laplace_numba.py", - ], - check=True, - ) + subprocess.run(["rm", "laplace_numba.py"], check=True) From b146cfa0b2c912da060412fbdafb96418d733038 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:11:05 +0100 Subject: [PATCH 038/106] Fix expressions template --- ffcx/codegeneration/numba/expressions_template.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index bf8912a55..ca63bf5d3 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -13,14 +13,14 @@ def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, _entity_local_index, - _quadrature_permutation): + _quadrature_permutation, custom_data): {tabulate_expression} class {factory_name}: - tabulate_tensor_{np_scalar_type} = tabulate_tensor_{factory_name} + tabulate_tensor = tabulate_tensor_{factory_name} num_coefficients = {num_coefficients} num_constants = {num_constants} original_coefficient_positions = {original_coefficient_positions} @@ -34,5 +34,8 @@ class {factory_name}: rank = {rank} # function_spaces = {function_spaces} +{name_from_uflfile} = {factory_name} + +# Name: {name_from_uflfile} # End of code for expression {factory_name} """ From 8576e6b2c25eeb08ab988cea0f85a21132362a56 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:13:30 +0100 Subject: [PATCH 039/106] Add expression tensor size computations --- ffcx/codegeneration/numba/expressions.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index ad7daf315..fd0bff1bd 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -43,17 +43,27 @@ def generator(ir: ExpressionIR, options): parts = eg.generate() tensor_size = 1 - for dim in ir.expression.tensor_shape: + for dim in ir.expression.shape: tensor_size *= dim - n_coeff = 1000 - n_const = 1000 + + tensor_size *= 3 # TODO: number of evaluation points - where to get? + + n_coeff = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) + n_const = sum( + np.prod(constant.ufl_shape, dtype=int) + for constant in ir.expression.original_constant_offsets.keys() + ) + n_coord_dofs = ir.expression.number_coordinate_dofs * 3 + n_entity_local_index = 2 # TODO: this is just an upper bound, harmful? + n_quad_perm = 2 if ir.expression.needs_facet_permutations else 0 + header = f""" A = numba.carray(_A, ({tensor_size})) w = numba.carray(_w, ({n_coeff})) c = numba.carray(_c, ({n_const})) - coordinate_dofs = numba.carray(_coordinate_dofs, (1000)) - entity_local_index = numba.carray(_entity_local_index, (1000)) - quadrature_permutation = numba.carray(_quadrature_permutation, (1000)) + coordinate_dofs = numba.carray(_coordinate_dofs, ({n_coord_dofs})) + entity_local_index = numba.carray(_entity_local_index, ({n_entity_local_index})) + quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) """ F = NumbaFormatter(options["scalar_type"]) body = F.c_format(parts) From d3e1d35da99f88208798ae052cf2cf34b96fd952 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:13:43 +0100 Subject: [PATCH 040/106] Add expression test --- test/laplace.py | 8 ++++++++ test/test_numba.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/test/laplace.py b/test/laplace.py index 63dd10f9d..4db7194f7 100644 --- a/test/laplace.py +++ b/test/laplace.py @@ -14,6 +14,7 @@ mesh = Mesh(basix.ufl.element("P", "triangle", 1, shape=(2,))) +# Forms e = basix.ufl.element("Lagrange", "triangle", 1) space = FunctionSpace(mesh, e) @@ -25,3 +26,10 @@ a = tr(kappa) * inner(grad(u), grad(v)) * dx L = f * v * dx + +# Expressions +e_vec = basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)) +space_vec = FunctionSpace(mesh, e_vec) +f_vec = Coefficient(space_vec) + +expressions = [(kappa * f_vec, e_vec.basix_element.points)] diff --git a/test/test_numba.py b/test/test_numba.py index 2f01a00c8..9c07d8e2d 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -3,6 +3,7 @@ # This file is part of FFCx.(https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later + import ctypes import importlib import subprocess @@ -27,7 +28,7 @@ def as_C_array(np_array: npt.NDArray): @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes -def test_poisson(scalar_type): +def test_integral(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" subprocess.run(["ffcx", Path(__file__).parent / "laplace.py", *opts.split(" ")], check=True) @@ -82,3 +83,36 @@ def test_poisson(scalar_type): assert np.allclose(b, 0.5 * b_expected) subprocess.run(["rm", "laplace_numba.py"], check=True) + + +@pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes +def test_expression(scalar_type: str) -> None: + opts = f"--language numba --scalar_type {scalar_type}" + subprocess.run(["ffcx", Path(__file__).parent / "laplace.py", *opts.split(" ")], check=True) + + laplace = importlib.import_module("laplace_numba") + + dtype = np.dtype(scalar_type).type + dtype_r = dtype_to_scalar_dtype(dtype) + + kernel_expr = wrap_kernel(dtype, dtype_r)(laplace.expression_laplace_0.tabulate_tensor) + + e = np.zeros((2 * 3,), dtype=dtype) + w = np.array([1, 1, 2, 2, 3, 3], dtype=dtype) + kappa_value = np.array([[1.0, 2.0], [3.0, 4.0]]) + c = np.array(kappa_value.flatten(), dtype=dtype) + coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=dtype_r) + empty = np.empty((0,), dtype=dtype_r) + + kernel_expr( + as_C_array(e), + as_C_array(w), + as_C_array(c), + as_C_array(coords), + as_C_array(empty), + as_C_array(empty), + 0, + ) + # TODO: check + e_expected = np.array([3, 7, 6, 14, 9, 21], dtype=dtype) + assert np.allclose(e, e_expected) From dd0f0738ab0c0fb8e92d226b88ccdd6c9c8f9499 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:35:47 +0100 Subject: [PATCH 041/106] Start fine tuning --- ffcx/codegeneration/numba/integrals_template.py | 1 - test/test_numba.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py index b9731214f..f21269715 100644 --- a/ffcx/codegeneration/numba/integrals_template.py +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -6,7 +6,6 @@ factory = """ # Code for integral {factory_name} -import numba def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, _entity_local_index, _quadrature_permutation, custom_data): diff --git a/test/test_numba.py b/test/test_numba.py index 9c07d8e2d..cd68aa738 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -82,8 +82,6 @@ def test_integral(scalar_type: str) -> None: b_expected = np.full((3,), 1 / 6, dtype=np.float64) assert np.allclose(b, 0.5 * b_expected) - subprocess.run(["rm", "laplace_numba.py"], check=True) - @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_expression(scalar_type: str) -> None: From e6333eb9139447c5c514ee7946900b588ca0d138 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:46:42 +0100 Subject: [PATCH 042/106] modernize cmdline test --- pyproject.toml | 2 +- test/test_cmdline.py | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74bd10836..bff7afc91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ ffcx = "ffcx:__main__.main" [project.optional-dependencies] lint = ["ruff"] docs = ["sphinx", "sphinx_rtd_theme"] -optional = ["pygraphviz==1.9"] +optional = ["numba", "pygraphviz==1.9"] test = ["pytest >= 6.0", "sympy", "numba"] ci = [ "coveralls", diff --git a/test/test_cmdline.py b/test/test_cmdline.py index 35b29b47c..bf44c19b7 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -1,28 +1,26 @@ -# Copyright (C) 2018 Chris N. Richardson +# Copyright (C) 2018-2025 Chris N. Richardson and Paul T. Kühner # # This file is part of FFCx. (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later -import os -import os.path +import importlib import subprocess +from pathlib import Path import pytest def test_cmdline_simple(): - os.chdir(os.path.dirname(__file__)) - subprocess.run(["ffcx", "Poisson.py"], check=True) + dir = Path(__file__).parent + subprocess.run(["ffcx", dir / "Poisson.py", "-o", dir], check=True) +@pytest.mark.skipif( + importlib.util.find_spec("pygraphviz") is None, reason="pygraphviz not installed." +) def test_visualise(): - try: - import pygraphviz # noqa: F401 - except ImportError: - pytest.skip("pygraphviz not installed") - - os.chdir(os.path.dirname(__file__)) - subprocess.run(["ffcx", "--visualise", "Poisson.py"]) - assert os.path.isfile("S.pdf") - assert os.path.isfile("F.pdf") + dir = Path(__file__).parent + subprocess.run(["ffcx", "--visualise", dir / "Poisson.py", "-o", dir]) + assert (dir / "S.pdf").is_file() + assert (dir / "F.pdf").is_file() From 8730fa54382307d8ca1d48b26be81afc3f63d409 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:49:09 +0100 Subject: [PATCH 043/106] Merge with poisson test --- test/Poisson.py | 20 ++++++++++++++------ test/laplace.py | 35 ----------------------------------- test/test_cmdline.py | 4 ++-- test/test_numba.py | 14 +++++++------- 4 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 test/laplace.py diff --git a/test/Poisson.py b/test/Poisson.py index b9c1ba503..df0bc5561 100644 --- a/test/Poisson.py +++ b/test/Poisson.py @@ -1,4 +1,4 @@ -# Copyright (C) 2004-2007 Anders Logg +# Copyright (C) 2004-20025 Anders Logg and Paul T. Kühner # # This file is part of FFCx. # @@ -31,19 +31,27 @@ dx, grad, inner, + tr, ) -mesh = Mesh(basix.ufl.element("P", "triangle", 2, shape=(2,))) +mesh = Mesh(basix.ufl.element("P", "triangle", 1, shape=(2,))) -e = basix.ufl.element("Lagrange", "triangle", 2) +# Forms +e = basix.ufl.element("Lagrange", "triangle", 1) space = FunctionSpace(mesh, e) u = TrialFunction(space) v = TestFunction(space) f = Coefficient(space) -kappa1 = Constant(mesh, shape=(2, 2)) -kappa2 = Constant(mesh, shape=(2, 2)) +kappa = Constant(mesh, shape=(2, 2)) -a = inner(kappa1, kappa2) * inner(grad(u), grad(v)) * dx +a = tr(kappa) * inner(grad(u), grad(v)) * dx L = f * v * dx + +# Expressions +e_vec = basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)) +space_vec = FunctionSpace(mesh, e_vec) +f_vec = Coefficient(space_vec) + +expressions = [(kappa * f_vec, e_vec.basix_element.points)] diff --git a/test/laplace.py b/test/laplace.py deleted file mode 100644 index 4db7194f7..000000000 --- a/test/laplace.py +++ /dev/null @@ -1,35 +0,0 @@ -import basix.ufl -from ufl import ( - Coefficient, - Constant, - FunctionSpace, - Mesh, - TestFunction, - TrialFunction, - dx, - grad, - inner, - tr, -) - -mesh = Mesh(basix.ufl.element("P", "triangle", 1, shape=(2,))) - -# Forms -e = basix.ufl.element("Lagrange", "triangle", 1) -space = FunctionSpace(mesh, e) - -u = TrialFunction(space) -v = TestFunction(space) -f = Coefficient(space) - -kappa = Constant(mesh, shape=(2, 2)) - -a = tr(kappa) * inner(grad(u), grad(v)) * dx -L = f * v * dx - -# Expressions -e_vec = basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)) -space_vec = FunctionSpace(mesh, e_vec) -f_vec = Coefficient(space_vec) - -expressions = [(kappa * f_vec, e_vec.basix_element.points)] diff --git a/test/test_cmdline.py b/test/test_cmdline.py index bf44c19b7..80b887545 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -13,7 +13,7 @@ def test_cmdline_simple(): dir = Path(__file__).parent - subprocess.run(["ffcx", dir / "Poisson.py", "-o", dir], check=True) + subprocess.run(["ffcx", dir / "poisson.py", "-o", dir], check=True) @pytest.mark.skipif( @@ -21,6 +21,6 @@ def test_cmdline_simple(): ) def test_visualise(): dir = Path(__file__).parent - subprocess.run(["ffcx", "--visualise", dir / "Poisson.py", "-o", dir]) + subprocess.run(["ffcx", "--visualise", dir / "poisson.py", "-o", dir]) assert (dir / "S.pdf").is_file() assert (dir / "F.pdf").is_file() diff --git a/test/test_numba.py b/test/test_numba.py index cd68aa738..e0b9278ef 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -30,14 +30,14 @@ def as_C_array(np_array: npt.NDArray): @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_integral(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - subprocess.run(["ffcx", Path(__file__).parent / "laplace.py", *opts.split(" ")], check=True) + subprocess.run(["ffcx", Path(__file__).parent / "poisson.py", *opts.split(" ")], check=True) - laplace = importlib.import_module("laplace_numba") + poisson = importlib.import_module("poisson_numba") dtype = np.dtype(scalar_type).type dtype_r = dtype_to_scalar_dtype(dtype) - kernel_a = wrap_kernel(dtype, dtype_r)(laplace.form_laplace_a.form_integrals[0].tabulate_tensor) + kernel_a = wrap_kernel(dtype, dtype_r)(poisson.form_poisson_a.form_integrals[0].tabulate_tensor) A = np.zeros((3, 3), dtype=dtype) w = np.array([], dtype=dtype) @@ -61,7 +61,7 @@ def test_integral(scalar_type: str) -> None: assert np.allclose(A, np.trace(kappa_value) * A_expected) - kernel_L = wrap_kernel(dtype, dtype_r)(laplace.form_laplace_L.form_integrals[0].tabulate_tensor) + kernel_L = wrap_kernel(dtype, dtype_r)(poisson.form_poisson_L.form_integrals[0].tabulate_tensor) b = np.zeros((3,), dtype=dtype) w = np.full((3,), 0.5, dtype=dtype) @@ -86,14 +86,14 @@ def test_integral(scalar_type: str) -> None: @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_expression(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - subprocess.run(["ffcx", Path(__file__).parent / "laplace.py", *opts.split(" ")], check=True) + subprocess.run(["ffcx", Path(__file__).parent / "poisson.py", *opts.split(" ")], check=True) - laplace = importlib.import_module("laplace_numba") + poisson = importlib.import_module("poisson_numba") dtype = np.dtype(scalar_type).type dtype_r = dtype_to_scalar_dtype(dtype) - kernel_expr = wrap_kernel(dtype, dtype_r)(laplace.expression_laplace_0.tabulate_tensor) + kernel_expr = wrap_kernel(dtype, dtype_r)(poisson.expression_poisson_0.tabulate_tensor) e = np.zeros((2 * 3,), dtype=dtype) w = np.array([1, 1, 2, 2, 3, 3], dtype=dtype) From 1bfe3324f583911233e68d05f5be68010d363bfe Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:01:06 +0100 Subject: [PATCH 044/106] Licensing headers --- ffcx/codegeneration/numba/expressions.py | 2 +- ffcx/codegeneration/numba/expressions_template.py | 2 +- ffcx/codegeneration/numba/file.py | 3 ++- ffcx/codegeneration/numba/file_template.py | 7 ++++--- ffcx/codegeneration/numba/form.py | 2 +- ffcx/codegeneration/numba/form_template.py | 7 ++++--- ffcx/codegeneration/numba/integrals.py | 3 ++- ffcx/codegeneration/numba/integrals_template.py | 7 ++++--- ffcx/codegeneration/numba/numba_implementation.py | 5 +++++ 9 files changed, 24 insertions(+), 14 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index fd0bff1bd..fd85da59b 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 Michal Habera +# Copyright (C) 2019-2025 Michal Habera, Chris Richardson and Paul T. Kühner # # This file is part of FFCx.(https://www.fenicsproject.org) # diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index ca63bf5d3..649b6b0df 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019 Michal Habera +# Copyright (C) 2019-2025 Michal Habera, Chris Richardson and Paul T.Kühner # # This file is part of FFCx.(https://www.fenicsproject.org) # diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py index 7e58cb284..d46771c25 100644 --- a/ffcx/codegeneration/numba/file.py +++ b/ffcx/codegeneration/numba/file.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2018 Anders Logg, Martin Sandve Alnæs and Garth N. Wells +# Copyright (C) 2009-2025 Anders Logg, Martin Sandve Alnæs, Garth N. Wells, Chris Richardson and +# Paul T. Kühner # # This file is part of FFCx.(https://www.fenicsproject.org) # diff --git a/ffcx/codegeneration/numba/file_template.py b/ffcx/codegeneration/numba/file_template.py index 1fb9016b2..8b5b67d7d 100644 --- a/ffcx/codegeneration/numba/file_template.py +++ b/ffcx/codegeneration/numba/file_template.py @@ -1,7 +1,8 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. +# Copyright (C) 2025 Chris Richardson and Paul T. Kühner # -# The FEniCS Project (http://www.fenicsproject.org/) 2018. +# This file is part of FFCx.(https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later """Template for file output.""" declaration_pre = """ diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index b1706810b..339924e2c 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2017 Anders Logg and Martin Sandve Alnæs +# Copyright (C) 2009-2025 Anders Logg, Martin Sandve Alnæs and Chris Richardson # # This file is part of FFCx.(https://www.fenicsproject.org) # diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py index aa99c407a..a5e008b8b 100644 --- a/ffcx/codegeneration/numba/form_template.py +++ b/ffcx/codegeneration/numba/form_template.py @@ -1,7 +1,8 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. +# Copyright (C) 2025 Chris Richardson # -# The FEniCS Project (http://www.fenicsproject.org/) 2020. +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later """Template for file output.""" factory = """ diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index a869b67e4..6bc5c26fb 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -1,4 +1,5 @@ -# Copyright (C) 2015-2021 Martin Sandve Alnæs, Michal Habera, Igor Baratta +# Copyright (C) 2015-2021 Martin Sandve Alnæs, Michal Habera, Igor Baratta, Chris Richardson and +# Paul T. Kühner # # This file is part of FFCx. (https://www.fenicsproject.org) # diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py index f21269715..ba2386e71 100644 --- a/ffcx/codegeneration/numba/integrals_template.py +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -1,7 +1,8 @@ -# Code generation format strings for UFC (Unified Form-assembly Code) -# This code is released into the public domain. +# Copyright (C) 2025 Chris Richardson and Paul T. Kühner # -# The FEniCS Project (http://www.fenicsproject.org/) 2018 +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later """Template for integral output.""" factory = """ diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 67104f0c0..9ff53ce09 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -1,3 +1,8 @@ +# Copyright (C) 2025 Chris Richardson +# +# This file is part of FFCx. (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later """Numba implementation for output.""" import ffcx.codegeneration.lnodes as L From 453d15fab4bb21a77e9a3464b65681063762b41b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:07:52 +0100 Subject: [PATCH 045/106] Resolve path --- test/test_cmdline.py | 4 ++-- test/test_numba.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_cmdline.py b/test/test_cmdline.py index 80b887545..f8a4d4d01 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -12,7 +12,7 @@ def test_cmdline_simple(): - dir = Path(__file__).parent + dir = Path(__file__).parent.resolve() subprocess.run(["ffcx", dir / "poisson.py", "-o", dir], check=True) @@ -20,7 +20,7 @@ def test_cmdline_simple(): importlib.util.find_spec("pygraphviz") is None, reason="pygraphviz not installed." ) def test_visualise(): - dir = Path(__file__).parent + dir = Path(__file__).parent.resolve() subprocess.run(["ffcx", "--visualise", dir / "poisson.py", "-o", dir]) assert (dir / "S.pdf").is_file() assert (dir / "F.pdf").is_file() diff --git a/test/test_numba.py b/test/test_numba.py index e0b9278ef..25836de52 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -30,7 +30,8 @@ def as_C_array(np_array: npt.NDArray): @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_integral(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - subprocess.run(["ffcx", Path(__file__).parent / "poisson.py", *opts.split(" ")], check=True) + dir = Path(__file__).parent.resolve() + subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) poisson = importlib.import_module("poisson_numba") @@ -86,7 +87,8 @@ def test_integral(scalar_type: str) -> None: @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_expression(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - subprocess.run(["ffcx", Path(__file__).parent / "poisson.py", *opts.split(" ")], check=True) + dir = Path(__file__).parent.resolve() + subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) poisson = importlib.import_module("poisson_numba") From 70552cffa8506bd2888e5fa73fc60a24e2965577 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:24:53 +0100 Subject: [PATCH 046/106] Fix files system mess --- test/{Poisson.py => poisson.py} | 0 test/test_cmdline.py | 8 ++++---- test/test_numba.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename test/{Poisson.py => poisson.py} (100%) diff --git a/test/Poisson.py b/test/poisson.py similarity index 100% rename from test/Poisson.py rename to test/poisson.py diff --git a/test/test_cmdline.py b/test/test_cmdline.py index f8a4d4d01..8991adaf3 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -12,15 +12,15 @@ def test_cmdline_simple(): - dir = Path(__file__).parent.resolve() - subprocess.run(["ffcx", dir / "poisson.py", "-o", dir], check=True) + dir = Path(__file__).parent + subprocess.run(["ffcx", "-o", dir, dir / "poisson.py"], check=True) @pytest.mark.skipif( importlib.util.find_spec("pygraphviz") is None, reason="pygraphviz not installed." ) def test_visualise(): - dir = Path(__file__).parent.resolve() - subprocess.run(["ffcx", "--visualise", dir / "poisson.py", "-o", dir]) + dir = Path(__file__).parent + subprocess.run(["ffcx", "-o", dir, "--visualise", dir / "poisson.py"], check=True) assert (dir / "S.pdf").is_file() assert (dir / "F.pdf").is_file() diff --git a/test/test_numba.py b/test/test_numba.py index 25836de52..b8b9099d9 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -30,7 +30,7 @@ def as_C_array(np_array: npt.NDArray): @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_integral(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - dir = Path(__file__).parent.resolve() + dir = Path(__file__).parent subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) poisson = importlib.import_module("poisson_numba") @@ -87,7 +87,7 @@ def test_integral(scalar_type: str) -> None: @pytest.mark.parametrize("scalar_type", ["float32", "float64"]) # TODO: complex limited by ctypes def test_expression(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" - dir = Path(__file__).parent.resolve() + dir = Path(__file__).parent subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) poisson = importlib.import_module("poisson_numba") From 4e3b2bdf94a5dacbe3eeaaccbcabde0cd98414ba Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:33:27 +0100 Subject: [PATCH 047/106] Language agnostic formatter name: c_format -> format --- ffcx/codegeneration/C/c_implementation.py | 50 ++++++++--------- ffcx/codegeneration/C/expressions.py | 2 +- ffcx/codegeneration/C/integrals.py | 2 +- ffcx/codegeneration/numba/expressions.py | 2 +- ffcx/codegeneration/numba/integrals.py | 2 +- .../numba/numba_implementation.py | 56 +++++++++---------- test/test_lnodes.py | 4 +- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/ffcx/codegeneration/C/c_implementation.py b/ffcx/codegeneration/C/c_implementation.py index 1d6eafa23..fa2c502ff 100644 --- a/ffcx/codegeneration/C/c_implementation.py +++ b/ffcx/codegeneration/C/c_implementation.py @@ -186,7 +186,7 @@ def _build_initializer_lists(self, values): def format_statement_list(self, slist) -> str: """Format a statement list.""" - return "".join(self.c_format(s) for s in slist.statements) + return "".join(self.format(s) for s in slist.statements) def format_section(self, section) -> str: """Format a section.""" @@ -197,12 +197,12 @@ def format_section(self, section) -> str: f"// Inputs: {', '.join(w.name for w in section.input)}\n" f"// Outputs: {', '.join(w.name for w in section.output)}\n" ) - declarations = "".join(self.c_format(s) for s in section.declarations) + declarations = "".join(self.format(s) for s in section.declarations) body = "" if len(section.statements) > 0: declarations += "{\n " - body = "".join(self.c_format(s) for s in section.statements) + body = "".join(self.format(s) for s in section.statements) body = body.replace("\n", "\n ") body = body[:-2] + "}\n" @@ -218,7 +218,7 @@ def format_array_decl(self, arr) -> str: dtype = arr.symbol.dtype typename = self._dtype_to_name(dtype) - symbol = self.c_format(arr.symbol) + symbol = self.format(arr.symbol) dims = "".join([f"[{i}]" for i in arr.sizes]) if arr.values is None: assert arr.const is False @@ -230,21 +230,21 @@ def format_array_decl(self, arr) -> str: def format_array_access(self, arr) -> str: """Format an array access.""" - name = self.c_format(arr.array) - indices = f"[{']['.join(self.c_format(i) for i in arr.indices)}]" + name = self.format(arr.array) + indices = f"[{']['.join(self.format(i) for i in arr.indices)}]" return f"{name}{indices}" def format_variable_decl(self, v) -> str: """Format a variable declaration.""" - val = self.c_format(v.value) - symbol = self.c_format(v.symbol) + val = self.format(v.value) + symbol = self.format(v.symbol) typename = self._dtype_to_name(v.symbol.dtype) return f"{typename} {symbol} = {val};\n" def format_nary_op(self, oper) -> str: """Format an n-ary operation.""" # Format children - args = [self.c_format(arg) for arg in oper.args] + args = [self.format(arg) for arg in oper.args] # Apply parentheses for i in range(len(args)): @@ -257,8 +257,8 @@ def format_nary_op(self, oper) -> str: def format_binary_op(self, oper) -> str: """Format a binary operation.""" # Format children - lhs = self.c_format(oper.lhs) - rhs = self.c_format(oper.rhs) + lhs = self.format(oper.lhs) + rhs = self.format(oper.rhs) # Apply parentheses if oper.lhs.precedence >= oper.precedence: @@ -271,7 +271,7 @@ def format_binary_op(self, oper) -> str: def format_unary_op(self, oper) -> str: """Format a unary operation.""" - arg = self.c_format(oper.arg) + arg = self.format(oper.arg) if oper.arg.precedence >= oper.precedence: return f"{oper.op}({arg})" return f"{oper.op}{arg}" @@ -287,12 +287,12 @@ def format_literal_int(self, val) -> str: def format_for_range(self, r) -> str: """Format a for loop over a range.""" - begin = self.c_format(r.begin) - end = self.c_format(r.end) - index = self.c_format(r.index) + begin = self.format(r.begin) + end = self.format(r.end) + index = self.format(r.index) output = f"for (int {index} = {begin}; {index} < {end}; ++{index})\n" output += "{\n" - body = self.c_format(r.body) + body = self.format(r.body) for line in body.split("\n"): if len(line) > 0: output += f" {line}\n" @@ -301,20 +301,20 @@ def format_for_range(self, r) -> str: def format_statement(self, s) -> str: """Format a statement.""" - return self.c_format(s.expr) + return self.format(s.expr) def format_assign(self, expr) -> str: """Format an assignment.""" - rhs = self.c_format(expr.rhs) - lhs = self.c_format(expr.lhs) + rhs = self.format(expr.rhs) + lhs = self.format(expr.lhs) return f"{lhs} {expr.op} {rhs};\n" def format_conditional(self, s) -> str: """Format a conditional.""" # Format children - c = self.c_format(s.condition) - t = self.c_format(s.true) - f = self.c_format(s.false) + c = self.format(s.condition) + t = self.format(s.true) + f = self.format(s.false) # Apply parentheses if s.condition.precedence >= s.precedence: @@ -333,7 +333,7 @@ def format_symbol(self, s) -> str: def format_multi_index(self, mi) -> str: """Format a multi-index.""" - return self.c_format(mi.global_index) + return self.format(mi.global_index) def format_math_function(self, c) -> str: """Format a mathematical function.""" @@ -349,7 +349,7 @@ def format_math_function(self, c) -> str: # Get a function from the table, if available, else just use bare name func = dtype_math_table.get(c.function, c.function) - args = ", ".join(self.c_format(arg) for arg in c.args) + args = ", ".join(self.format(arg) for arg in c.args) return f"{func}({args})" c_impl = { @@ -387,7 +387,7 @@ def format_math_function(self, c) -> str: "LT": format_binary_op, } - def c_format(self, s) -> str: + def format(self, s) -> str: """Format as C.""" name = s.__class__.__name__ try: diff --git a/ffcx/codegeneration/C/expressions.py b/ffcx/codegeneration/C/expressions.py index 177e8a0be..b9f6f2ea0 100644 --- a/ffcx/codegeneration/C/expressions.py +++ b/ffcx/codegeneration/C/expressions.py @@ -44,7 +44,7 @@ def generator(ir: ExpressionIR, options): parts = eg.generate() CF = CFormatter(options["scalar_type"]) - d["tabulate_expression"] = CF.c_format(parts) + d["tabulate_expression"] = CF.format(parts) if len(ir.original_coefficient_positions) > 0: d["original_coefficient_positions"] = f"original_coefficient_positions_{factory_name}" diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 9fb64ccc7..ccad8dc76 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -43,7 +43,7 @@ def generator(ir: IntegralIR, domain: basix.CellType, options): # Format code as string CF = CFormatter(options["scalar_type"]) - body = CF.c_format(parts) + body = CF.format(parts) # Generate generic FFCx code snippets and add specific parts code = {} diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index fd85da59b..781047609 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -66,7 +66,7 @@ def generator(ir: ExpressionIR, options): quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) """ F = NumbaFormatter(options["scalar_type"]) - body = F.c_format(parts) + body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 6bc5c26fb..36d324cb5 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -39,7 +39,7 @@ def generator(ir, domain: basix.CellType, options): # Format code as string F = NumbaFormatter(options["scalar_type"]) - body = F.c_format(parts) + body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index 9ff53ce09..ce073af8c 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -33,11 +33,11 @@ def format_section(self, section): comments += "# Section: " + section.name + "\n" comments += "# Inputs: " + ", ".join(w.name for w in section.input) + "\n" comments += "# Outputs: " + ", ".join(w.name for w in section.output) + "\n" - declarations = "".join(self.c_format(s) for s in section.declarations) + declarations = "".join(self.format(s) for s in section.declarations) body = "" if len(section.statements) > 0: - body = "".join(self.c_format(s) for s in section.statements) + body = "".join(self.format(s) for s in section.statements) body += "# ------------------------ \n" return comments + declarations + body @@ -46,7 +46,7 @@ def format_statement_list(self, slist): """Format a list of statements.""" output = "" for s in slist.statements: - output += self.c_format(s) + output += self.format(s) return output def format_comment(self, c): @@ -61,7 +61,7 @@ def format_array_decl(self, arr): dtype = "coordinate_dofs.dtype" elif arr.symbol.dtype == L.DataType.INT: dtype = "np.int32" - symbol = self.c_format(arr.symbol) + symbol = self.format(arr.symbol) if arr.values is None: return f"{symbol} = np.empty({arr.sizes}, dtype={dtype})\n" elif arr.values.size == 1: @@ -72,24 +72,24 @@ def format_array_decl(self, arr): def format_array_access(self, arr): """Format array access.""" - array = self.c_format(arr.array) - idx = ", ".join(self.c_format(ix) for ix in arr.indices) + array = self.format(arr.array) + idx = ", ".join(self.format(ix) for ix in arr.indices) return f"{array}[{idx}]" def format_multi_index(self, index): """Format a multi-index.""" - return self.c_format(index.global_index) + return self.format(index.global_index) def format_variable_decl(self, v): """Format a variable declaration.""" - sym = self.c_format(v.symbol) - val = self.c_format(v.value) + sym = self.format(v.symbol) + val = self.format(v.value) return f"{sym} = {val}\n" def format_nary_op(self, oper): """Format a n argument operation.""" # Format children - args = [self.c_format(arg) for arg in oper.args] + args = [self.format(arg) for arg in oper.args] # Apply parentheses for i in range(len(args)): @@ -102,8 +102,8 @@ def format_nary_op(self, oper): def format_binary_op(self, oper): """Format a binary operation.""" # Format children - lhs = self.c_format(oper.lhs) - rhs = self.c_format(oper.rhs) + lhs = self.format(oper.lhs) + rhs = self.format(oper.rhs) # Apply parentheses if oper.lhs.precedence >= oper.precedence: @@ -116,19 +116,19 @@ def format_binary_op(self, oper): def format_neg(self, val): """Format unary negation.""" - arg = self.c_format(val.arg) + arg = self.format(val.arg) return f"-{arg}" def format_not(self, val): """Format not operation.""" - arg = self.c_format(val.arg) + arg = self.format(val.arg) return f"not({arg})" def format_andor(self, oper): """Format and or or operation.""" # Format children - lhs = self.c_format(oper.lhs) - rhs = self.c_format(oper.rhs) + lhs = self.format(oper.lhs) + rhs = self.format(oper.rhs) # Apply parentheses if oper.lhs.precedence >= oper.precedence: @@ -151,31 +151,31 @@ def format_literal_int(self, val): def format_for_range(self, r): """Format a loop over a range.""" - begin = self.c_format(r.begin) - end = self.c_format(r.end) - index = self.c_format(r.index) + begin = self.format(r.begin) + end = self.format(r.end) + index = self.format(r.index) output = f"for {index} in range({begin}, {end}):\n" - b = self.c_format(r.body).split("\n") + b = self.format(r.body).split("\n") for line in b: output += f" {line}\n" return output def format_statement(self, s): """Format a statement.""" - return self.c_format(s.expr) + return self.format(s.expr) def format_assign(self, expr): """Format assignment.""" - rhs = self.c_format(expr.rhs) - lhs = self.c_format(expr.lhs) + rhs = self.format(expr.rhs) + lhs = self.format(expr.lhs) return f"{lhs} {expr.op} {rhs}\n" def format_conditional(self, s): """Format a conditional.""" # Format children - c = self.c_format(s.condition) - t = self.c_format(s.true) - f = self.c_format(s.false) + c = self.format(s.condition) + t = self.format(s.true) + f = self.format(s.false) # Apply parentheses if s.condition.precedence >= s.precedence: @@ -205,7 +205,7 @@ def format_mathfunction(self, f): "atanh": "arctanh", } function = function_map.get(f.function, f.function) - args = [self.c_format(arg) for arg in f.args] + args = [self.format(arg) for arg in f.args] if "bessel" in function: return "0" if function == "erf": @@ -248,7 +248,7 @@ def format_mathfunction(self, f): "LT": format_binary_op, } - def c_format(self, s): + def format(self, s): """Format output.""" name = s.__class__.__name__ try: diff --git a/test/test_lnodes.py b/test/test_lnodes.py index 1be46d8d0..9c7460d6c 100644 --- a/test/test_lnodes.py +++ b/test/test_lnodes.py @@ -35,7 +35,7 @@ def test_gemm(dtype): Q = CFormatter(dtype=dtype) c_scalar = dtype_to_c_type(dtype) decl = f"void gemm({c_scalar} *A, {c_scalar} *B, {c_scalar} *C)" - c_code = decl + "{\n" + Q.c_format(L.StatementList(code)) + "\n}\n" + c_code = decl + "{\n" + Q.format(L.StatementList(code)) + "\n}\n" ffibuilder = FFI() ffibuilder.cdef(decl + ";") @@ -78,7 +78,7 @@ def test_gemv(dtype): Q = CFormatter(dtype=dtype) c_scalar = dtype_to_c_type(dtype) decl = f"void gemm({c_scalar} *y, {c_scalar} *A, {c_scalar} *x)" - c_code = decl + "{\n" + Q.c_format(L.StatementList(code)) + "\n}\n" + c_code = decl + "{\n" + Q.format(L.StatementList(code)) + "\n}\n" ffibuilder = FFI() ffibuilder.cdef(decl + ";") From 098328dc7910312d689e59003469cf19bc19bbb7 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:34:46 +0100 Subject: [PATCH 048/106] Language agnostic formatter name: c_impl -> impl --- ffcx/codegeneration/C/c_implementation.py | 4 ++-- ffcx/codegeneration/numba/numba_implementation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ffcx/codegeneration/C/c_implementation.py b/ffcx/codegeneration/C/c_implementation.py index fa2c502ff..066440c8c 100644 --- a/ffcx/codegeneration/C/c_implementation.py +++ b/ffcx/codegeneration/C/c_implementation.py @@ -352,7 +352,7 @@ def format_math_function(self, c) -> str: args = ", ".join(self.format(arg) for arg in c.args) return f"{func}({args})" - c_impl = { + impl = { "Section": format_section, "StatementList": format_statement_list, "Comment": format_comment, @@ -391,6 +391,6 @@ def format(self, s) -> str: """Format as C.""" name = s.__class__.__name__ try: - return self.c_impl[name](self, s) + return self.impl[name](self, s) except KeyError: raise RuntimeError("Unknown statement: ", name) diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/numba_implementation.py index ce073af8c..c87e66959 100644 --- a/ffcx/codegeneration/numba/numba_implementation.py +++ b/ffcx/codegeneration/numba/numba_implementation.py @@ -213,7 +213,7 @@ def format_mathfunction(self, f): argstr = ", ".join(args) return f"np.{function}({argstr})" - c_impl = { + impl = { "StatementList": format_statement_list, "Comment": format_comment, "Section": format_section, @@ -252,6 +252,6 @@ def format(self, s): """Format output.""" name = s.__class__.__name__ try: - return self.c_impl[name](self, s) + return self.impl[name](self, s) except KeyError: raise RuntimeError("Unknown statement: ", name) From 1fc494422923b18f2cea185bad06f9c6f8c0323d Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:37:07 +0100 Subject: [PATCH 049/106] Language agnostic naming: c/numba_implementation -> implementation --- ffcx/codegeneration/C/expressions.py | 2 +- .../codegeneration/C/{c_implementation.py => implementation.py} | 0 ffcx/codegeneration/C/integrals.py | 2 +- ffcx/codegeneration/numba/expressions.py | 2 +- .../numba/{numba_implementation.py => implementation.py} | 0 ffcx/codegeneration/numba/integrals.py | 2 +- test/test_lnodes.py | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename ffcx/codegeneration/C/{c_implementation.py => implementation.py} (100%) rename ffcx/codegeneration/numba/{numba_implementation.py => implementation.py} (100%) diff --git a/ffcx/codegeneration/C/expressions.py b/ffcx/codegeneration/C/expressions.py index b9f6f2ea0..2a4d16f64 100644 --- a/ffcx/codegeneration/C/expressions.py +++ b/ffcx/codegeneration/C/expressions.py @@ -13,7 +13,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.C import expressions_template -from ffcx.codegeneration.C.c_implementation import CFormatter +from ffcx.codegeneration.C.implementation import CFormatter from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR diff --git a/ffcx/codegeneration/C/c_implementation.py b/ffcx/codegeneration/C/implementation.py similarity index 100% rename from ffcx/codegeneration/C/c_implementation.py rename to ffcx/codegeneration/C/implementation.py diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index ccad8dc76..1a2799e3d 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -13,7 +13,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.C import integrals_template as ufcx_integrals -from ffcx.codegeneration.C.c_implementation import CFormatter +from ffcx.codegeneration.C.implementation import CFormatter from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype from ffcx.ir.representation import IntegralIR diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 781047609..216f25d47 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -12,7 +12,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template -from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter +from ffcx.codegeneration.numba.implementation import NumbaFormatter from ffcx.codegeneration.utils import dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR diff --git a/ffcx/codegeneration/numba/numba_implementation.py b/ffcx/codegeneration/numba/implementation.py similarity index 100% rename from ffcx/codegeneration/numba/numba_implementation.py rename to ffcx/codegeneration/numba/implementation.py diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 36d324cb5..2940b5a33 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -14,7 +14,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.numba import integrals_template as ufcx_integrals -from ffcx.codegeneration.numba.numba_implementation import NumbaFormatter +from ffcx.codegeneration.numba.implementation import NumbaFormatter logger = logging.getLogger("ffcx") diff --git a/test/test_lnodes.py b/test/test_lnodes.py index 9c7460d6c..b9d58d477 100644 --- a/test/test_lnodes.py +++ b/test/test_lnodes.py @@ -5,7 +5,7 @@ from cffi import FFI from ffcx.codegeneration import lnodes as L -from ffcx.codegeneration.C.c_implementation import CFormatter +from ffcx.codegeneration.C.implementation import CFormatter from ffcx.codegeneration.utils import dtype_to_c_type From 2774e7c7adfab1c0513e68bc40b9b497c6c8d2dc Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:40:05 +0100 Subject: [PATCH 050/106] Language agnostic naming: c/numbaFormatter -> Formatter --- ffcx/codegeneration/C/expressions.py | 10 ++++++---- ffcx/codegeneration/C/implementation.py | 2 +- ffcx/codegeneration/C/integrals.py | 4 ++-- ffcx/codegeneration/numba/expressions.py | 4 ++-- ffcx/codegeneration/numba/implementation.py | 2 +- ffcx/codegeneration/numba/integrals.py | 4 ++-- test/test_lnodes.py | 6 +++--- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/ffcx/codegeneration/C/expressions.py b/ffcx/codegeneration/C/expressions.py index 2a4d16f64..7ad470ccf 100644 --- a/ffcx/codegeneration/C/expressions.py +++ b/ffcx/codegeneration/C/expressions.py @@ -8,12 +8,13 @@ from __future__ import annotations import logging +import string import numpy as np from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.C import expressions_template -from ffcx.codegeneration.C.implementation import CFormatter +from ffcx.codegeneration.C.implementation import Formatter from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR @@ -43,7 +44,7 @@ def generator(ir: ExpressionIR, options): d["factory_name"] = factory_name parts = eg.generate() - CF = CFormatter(options["scalar_type"]) + CF = Formatter(options["scalar_type"]) d["tabulate_expression"] = CF.format(parts) if len(ir.original_coefficient_positions) > 0: @@ -106,9 +107,10 @@ def generator(ir: ExpressionIR, options): d["coordinate_element_hash"] = f"UINT64_C({ir.expression.coordinate_element_hash})" # Check that no keys are redundant or have been missed - from string import Formatter - fields = [fname for _, fname, _, _ in Formatter().parse(expressions_template.factory) if fname] + fields = [ + fname for _, fname, _, _ in string.Formatter().parse(expressions_template.factory) if fname + ] assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting dict" # Format implementation code diff --git a/ffcx/codegeneration/C/implementation.py b/ffcx/codegeneration/C/implementation.py index 066440c8c..6bdde8eec 100644 --- a/ffcx/codegeneration/C/implementation.py +++ b/ffcx/codegeneration/C/implementation.py @@ -142,7 +142,7 @@ } -class CFormatter: +class Formatter: """C formatter.""" scalar_type: np.dtype diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 1a2799e3d..5ea2f5c65 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -13,7 +13,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.C import integrals_template as ufcx_integrals -from ffcx.codegeneration.C.implementation import CFormatter +from ffcx.codegeneration.C.implementation import Formatter from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype from ffcx.ir.representation import IntegralIR @@ -42,7 +42,7 @@ def generator(ir: IntegralIR, domain: basix.CellType, options): parts = ig.generate(domain) # Format code as string - CF = CFormatter(options["scalar_type"]) + CF = Formatter(options["scalar_type"]) body = CF.format(parts) # Generate generic FFCx code snippets and add specific parts diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 216f25d47..798abb01f 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -12,7 +12,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template -from ffcx.codegeneration.numba.implementation import NumbaFormatter +from ffcx.codegeneration.numba.implementation import Formatter from ffcx.codegeneration.utils import dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR @@ -65,7 +65,7 @@ def generator(ir: ExpressionIR, options): entity_local_index = numba.carray(_entity_local_index, ({n_entity_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) """ - F = NumbaFormatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index c87e66959..2eac7925e 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -19,7 +19,7 @@ def build_initializer_lists(values): return arr -class NumbaFormatter: +class Formatter: """Implementation for numba output backend.""" def __init__(self, scalar) -> None: diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 2940b5a33..33f39359a 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -14,7 +14,7 @@ from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.numba import integrals_template as ufcx_integrals -from ffcx.codegeneration.numba.implementation import NumbaFormatter +from ffcx.codegeneration.numba.implementation import Formatter logger = logging.getLogger("ffcx") @@ -38,7 +38,7 @@ def generator(ir, domain: basix.CellType, options): parts = ig.generate(domain) # Format code as string - F = NumbaFormatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) diff --git a/test/test_lnodes.py b/test/test_lnodes.py index b9d58d477..e8187bbd9 100644 --- a/test/test_lnodes.py +++ b/test/test_lnodes.py @@ -5,7 +5,7 @@ from cffi import FFI from ffcx.codegeneration import lnodes as L -from ffcx.codegeneration.C.implementation import CFormatter +from ffcx.codegeneration.C.implementation import Formatter from ffcx.codegeneration.utils import dtype_to_c_type @@ -32,7 +32,7 @@ def test_gemm(dtype): code += [L.ForRange(k, 0, r, body=body)] # Format into C and compile with CFFI - Q = CFormatter(dtype=dtype) + Q = Formatter(dtype=dtype) c_scalar = dtype_to_c_type(dtype) decl = f"void gemm({c_scalar} *A, {c_scalar} *B, {c_scalar} *C)" c_code = decl + "{\n" + Q.format(L.StatementList(code)) + "\n}\n" @@ -75,7 +75,7 @@ def test_gemv(dtype): code += [L.ForRange(j, 0, q, body=body)] # Format into C and compile with CFFI - Q = CFormatter(dtype=dtype) + Q = Formatter(dtype=dtype) c_scalar = dtype_to_c_type(dtype) decl = f"void gemm({c_scalar} *y, {c_scalar} *A, {c_scalar} *x)" c_code = decl + "{\n" + Q.format(L.StatementList(code)) + "\n}\n" From ec1f538aa174d6dcb3f3607fe3ab933b60a41030 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:42:51 +0100 Subject: [PATCH 051/106] One more visualise fix --- test/test_cmdline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_cmdline.py b/test/test_cmdline.py index 8991adaf3..1c36930e7 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -22,5 +22,5 @@ def test_cmdline_simple(): def test_visualise(): dir = Path(__file__).parent subprocess.run(["ffcx", "-o", dir, "--visualise", dir / "poisson.py"], check=True) - assert (dir / "S.pdf").is_file() - assert (dir / "F.pdf").is_file() + assert (Path.cwd() / "S.pdf").is_file() + assert (Path.cwd() / "F.pdf").is_file() From 77280a81925e868dfdf0ab8fb6bc3b26b28f7d85 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:04:05 +0100 Subject: [PATCH 052/106] Prepare alignment of expressions to C --- ffcx/codegeneration/numba/expressions.py | 62 +++++-------------- .../numba/expressions_template.py | 3 +- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 798abb01f..1b1b4a7c6 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -36,16 +36,13 @@ def generator(ir: ExpressionIR, options): backend = FFCXBackend(ir, options) eg = ExpressionGenerator(ir, backend) - d = {} + d: dict[str, str | int] = {} d["name_from_uflfile"] = ir.name_from_uflfile d["factory_name"] = factory_name - parts = eg.generate() - tensor_size = 1 - for dim in ir.expression.shape: - tensor_size *= dim - + # tabulate_expression + tensor_size = np.prod(ir.expression.shape) tensor_size *= 3 # TODO: number of evaluation points - where to get? n_coeff = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) @@ -72,72 +69,45 @@ def generator(ir: ExpressionIR, options): d["tabulate_expression"] = header + body + # TODO: original_coefficient_positions_init originals = ", ".join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_positions"] = f"[{originals}]" # n = points.size + # TODO: points_init d["points"] = f"[{', '.join(str(p) for p in points.flatten())}]" + # TODO: value_shape_init shape = ", ".join(str(i) for i in ir.expression.shape) d["value_shape"] = f"[{shape}]" d["num_components"] = len(ir.expression.shape) d["num_coefficients"] = len(ir.expression.coefficient_numbering) d["num_constants"] = len(ir.constant_names) d["num_points"] = points.shape[0] - d["topological_dimension"] = points.shape[1] + d["entity_dimension"] = points.shape[1] d["scalar_type"] = options["scalar_type"] d["geom_type"] = dtype_to_scalar_dtype(options["scalar_type"]) + d["np_scalar_type"] = np.dtype(options["scalar_type"]).names d["rank"] = len(ir.expression.tensor_shape) - d["np_scalar_type"] = np.dtype(options["scalar_type"]).names + # TODO: coefficient_names_init names = ", ".join(f'"{name}"' for name in ir.coefficient_names) d["coefficient_names"] = f"[{names}]" + + # TODO: constant_names_init names = ", ".join(f'"{name}"' for name in ir.constant_names) d["constant_names"] = f"[{names}]" - code = [] - - # FIXME: Should be handled differently, revise how - # ufcx_function_space is generated (also for ufcx_form) - # for name, (element, dofmap, cmap_family, cmap_degree) in ir.function_spaces.items(): - # code += [f"static ufcx_function_space function_space_{name}_{ir.name_from_uflfile} ="] - # code += ["{"] - # code += [f".finite_element = &{element},"] - # code += [f".dofmap = &{dofmap},"] - # code += [f'.geometry_family = "{cmap_family}",'] - # code += [f".geometry_degree = {cmap_degree}"] - # code += ["};"] - - d["function_spaces_alloc"] = "\n".join(code) - d["function_spaces"] = "" - - # if len(ir.function_spaces) > 0: - # d["function_spaces"] = f"function_spaces_{ir.name}" - # fs_list = ", ".join( - # f"&function_space_{name}_{ir.name_from_uflfile}" - # for (name, _) in ir.function_spaces.items() - # ) - # n = len(ir.function_spaces.items()) - # d["function_spaces_init"] = ( - # f"ufcx_function_space* function_spaces_{ir.name}[{n}] = {{{fs_list}}};" - # ) - # else: - # d["function_spaces"] = "NULL" - # d["function_spaces_init"] = "" - - # d["function_spaces"] = "0" + # TODO: coordinate_element_hash # Check that no keys are redundant or have been missed - # from string import Formatter # fields = [ - # fname - # for _, fname, _, _ in Formatter().parse(expressions_template.factory) - # if fname + # fname for _, fname, _, _ in string.Formatter().parse(expressions_template.factory) if + # fname # ] - # assert set(fields) == set( - # d.keys() - # ), "Mismatch between keys in template and in formatting dict" + # assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting + # dict" # Format implementation code implementation = expressions_template.factory.format_map(d) diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index 649b6b0df..fe1ad1f9e 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -27,12 +27,11 @@ class {factory_name}: coefficient_names = {coefficient_names} constant_names = {constant_names} num_points = {num_points} - topological_dimension = {topological_dimension} + entity_dimension = {entity_dimension} points = {points} value_shape = {value_shape} num_components = {num_components} rank = {rank} - # function_spaces = {function_spaces} {name_from_uflfile} = {factory_name} From 77ac932c8a4086f64864bce875bd552df1c876f6 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:19:01 +0100 Subject: [PATCH 053/106] Integrals alost completely aligned --- ffcx/codegeneration/C/integrals.py | 2 +- .../numba/expressions_template.py | 4 ++-- ffcx/codegeneration/numba/integrals.py | 19 +++++++++++++------ .../numba/integrals_template.py | 3 ++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 5ea2f5c65..6d667adef 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -32,7 +32,7 @@ def generator(ir: IntegralIR, domain: basix.CellType, options): # Format declaration declaration = ufcx_integrals.declaration.format(factory_name=factory_name) - # Create FFCx C backend + # Create FFCx backend backend = FFCXBackend(ir, options) # Configure kernel generator diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index fe1ad1f9e..7ea8978f7 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -19,7 +19,6 @@ def tabulate_tensor_{factory_name}(_A, _w, _c, _coordinate_dofs, class {factory_name}: - tabulate_tensor = tabulate_tensor_{factory_name} num_coefficients = {num_coefficients} num_constants = {num_constants} @@ -32,9 +31,10 @@ class {factory_name}: value_shape = {value_shape} num_components = {num_components} rank = {rank} + # coordinate_element_hash = coordinate_element_hash +# Alias name {name_from_uflfile} = {factory_name} -# Name: {name_from_uflfile} # End of code for expression {factory_name} """ diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 33f39359a..6f5d0e327 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -20,14 +20,18 @@ def generator(ir, domain: basix.CellType, options): - """Integral generator.""" + """Generate numba code for an integral.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") logger.info(f"--- name: {ir.expression.name}") - """Generate code for an integral.""" + # Note: in contrast to the C implementation the actual code is type agnostic, thus not part of + # naming. factory_name = ir.expression.name + # Format declaration + declaration = "" + # Create FFCx backend backend = FFCXBackend(ir, options) @@ -45,12 +49,12 @@ def generator(ir, domain: basix.CellType, options): # Generate generic FFCx code snippets and add specific parts code = {} - code["class_type"] = ir.expression.integral_type + "_integral" - code["name"] = ir.expression.name + # TODO: enabled_coefficients_init vals = ", ".join("1" if i else "0" for i in ir.enabled_coefficients) code["enabled_coefficients"] = f"[{vals}]" + # tabulate_tensor tensor_size = 1 for dim in ir.expression.tensor_shape: tensor_size *= dim @@ -75,13 +79,16 @@ def generator(ir, domain: basix.CellType, options): """ code["tabulate_tensor"] = header + body + assert ir.expression.coordinate_element_hash is not None implementation = ufcx_integrals.factory.format( factory_name=factory_name, enabled_coefficients=code["enabled_coefficients"], + # enabled_coefficients_init=code["enabled_coefficients_init"], tabulate_tensor=code["tabulate_tensor"], needs_facet_permutations="True" if ir.expression.needs_facet_permutations else "False", scalar_type=options["scalar_type"], - coordinate_element=ir.expression.coordinate_element_hash, + coordinate_element_hash=ir.expression.coordinate_element_hash, + domain=int(domain), ) - return "", implementation + return declaration, implementation diff --git a/ffcx/codegeneration/numba/integrals_template.py b/ffcx/codegeneration/numba/integrals_template.py index ba2386e71..28dccd0ea 100644 --- a/ffcx/codegeneration/numba/integrals_template.py +++ b/ffcx/codegeneration/numba/integrals_template.py @@ -16,7 +16,8 @@ class {factory_name}(object): enabled_coefficients = {enabled_coefficients} tabulate_tensor = tabulate_tensor_{factory_name} needs_facet_permutations = {needs_facet_permutations} - coordinate_element = {coordinate_element} + coordinate_element_hash = {coordinate_element_hash} + domain = {domain} # End of code for integral {factory_name} """ From 0bff1fc1d3245bce0f2101ca11eb0abf65a16f6c Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:31:40 +0100 Subject: [PATCH 054/106] Finalise cleanup --- ffcx/codegeneration/numba/integrals.py | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 6f5d0e327..7f770a3c7 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -25,8 +25,9 @@ def generator(ir, domain: basix.CellType, options): logger.info(f"--- type: {ir.expression.integral_type}") logger.info(f"--- name: {ir.expression.name}") - # Note: in contrast to the C implementation the actual code is type agnostic, thus not part of - # naming. + # Note: In contrast to the C implementation the actual code is type agnostic, thus not part of + # naming. This is only true for the numba code prior to compilation - the JITed versions + # are different. factory_name = ir.expression.name # Format declaration @@ -50,32 +51,30 @@ def generator(ir, domain: basix.CellType, options): # Generate generic FFCx code snippets and add specific parts code = {} - # TODO: enabled_coefficients_init + # TODO: enabled_coefficients_init - required? vals = ", ".join("1" if i else "0" for i in ir.enabled_coefficients) code["enabled_coefficients"] = f"[{vals}]" # tabulate_tensor - tensor_size = 1 - for dim in ir.expression.tensor_shape: - tensor_size *= dim - - n_coeff = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) - n_const = sum( + # Note: In contrast to the C implementation we actually need to provide/compute the sizes of the + # array. + size_A = np.prod(ir.expression.tensor_shape) + size_w = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) + size_c = sum( np.prod(constant.ufl_shape, dtype=int) for constant in ir.expression.original_constant_offsets.keys() ) - - n_coord_dofs = ir.expression.number_coordinate_dofs * 3 - n_entity_local_index = 2 # TODO: this is just an upper bound, harmful? - n_quad_perm = 2 if ir.expression.needs_facet_permutations else 0 + size_coords = ir.expression.number_coordinate_dofs * 3 + size_local_index = 2 # TODO: this is just an upper bound + size_permutation = 2 if ir.expression.needs_facet_permutations else 0 header = f""" - A = numba.carray(_A, ({tensor_size})) - w = numba.carray(_w, ({n_coeff})) - c = numba.carray(_c, ({n_const})) - coordinate_dofs = numba.carray(_coordinate_dofs, ({n_coord_dofs})) - entity_local_index = numba.carray(_entity_local_index, ({n_entity_local_index})) - quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) + A = numba.carray(_A, ({size_A})) + w = numba.carray(_w, ({size_w})) + c = numba.carray(_c, ({size_c})) + coordinate_dofs = numba.carray(_coordinate_dofs, ({size_coords})) + entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) + quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ code["tabulate_tensor"] = header + body From 07a7e141e52996baeee498aff35cdc7fbc57b3c1 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:42:24 +0100 Subject: [PATCH 055/106] checked --- test/test_numba.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_numba.py b/test/test_numba.py index b8b9099d9..afcf98285 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -113,6 +113,5 @@ def test_expression(scalar_type: str) -> None: as_C_array(empty), 0, ) - # TODO: check e_expected = np.array([3, 7, 6, 14, 9, 21], dtype=dtype) assert np.allclose(e, e_expected) From f6be02bb8552013d628d937fc1b0fc99852b4cd1 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:45:29 +0100 Subject: [PATCH 056/106] Reactivate mypy --- .github/workflows/pythonapp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 3d85108a9..679954958 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -85,8 +85,8 @@ jobs: if: runner.os != 'Linux' run: pip install .[ci] - # - name: Static check with mypy - # run: mypy -p ffcx + - name: Static check with mypy + run: mypy -p ffcx - name: ruff checks run: | From 664bb1fc091576a93c02af8946a8bc3a98da0563 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:55:39 +0100 Subject: [PATCH 057/106] Fix mypy --- ffcx/codegeneration/numba/expressions.py | 5 +++-- ffcx/formatting.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 1b1b4a7c6..a91249d1a 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -13,7 +13,8 @@ from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.numba.implementation import Formatter -from ffcx.codegeneration.utils import dtype_to_scalar_dtype + +# from ffcx.codegeneration.utils import dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR logger = logging.getLogger("ffcx") @@ -85,7 +86,7 @@ def generator(ir: ExpressionIR, options): d["num_points"] = points.shape[0] d["entity_dimension"] = points.shape[1] d["scalar_type"] = options["scalar_type"] - d["geom_type"] = dtype_to_scalar_dtype(options["scalar_type"]) + # d["geom_type"] = dtype_to_scalar_dtype(options["scalar_type"]) d["np_scalar_type"] = np.dtype(options["scalar_type"]).names d["rank"] = len(ir.expression.tensor_shape) diff --git a/ffcx/formatting.py b/ffcx/formatting.py index 3f1c4e203..f8de9b79d 100644 --- a/ffcx/formatting.py +++ b/ffcx/formatting.py @@ -38,7 +38,9 @@ def format_code(code: CodeBlocks) -> tuple[str, str]: return code_h, code_c -def write_code(code_h: str, code_c: str, prefix: str, suffixes, output_dir: str) -> None: +def write_code( + code_h: str, code_c: str, prefix: str, suffixes: tuple[str, str], output_dir: str +) -> None: """Write code to files.""" if suffixes[0] is not None: _write_file(code_h, prefix, suffixes[0], output_dir) From 161ad40bc2c4fbf225210218833c7d6956ab7eb1 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:33:35 +0100 Subject: [PATCH 058/106] Fix: tensor size --- ffcx/codegeneration/numba/expressions.py | 27 +++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index a91249d1a..dc251c03f 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -43,25 +43,22 @@ def generator(ir: ExpressionIR, options): parts = eg.generate() # tabulate_expression - tensor_size = np.prod(ir.expression.shape) - tensor_size *= 3 # TODO: number of evaluation points - where to get? - - n_coeff = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) - n_const = sum( + size_w = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) + size_c = sum( np.prod(constant.ufl_shape, dtype=int) for constant in ir.expression.original_constant_offsets.keys() ) - n_coord_dofs = ir.expression.number_coordinate_dofs * 3 - n_entity_local_index = 2 # TODO: this is just an upper bound, harmful? - n_quad_perm = 2 if ir.expression.needs_facet_permutations else 0 + size_coords = ir.expression.number_coordinate_dofs * 3 + size_local_index = 2 # TODO: this is just an upper bound, harmful? + size_permutation = 2 if ir.expression.needs_facet_permutations else 0 header = f""" - A = numba.carray(_A, ({tensor_size})) - w = numba.carray(_w, ({n_coeff})) - c = numba.carray(_c, ({n_const})) - coordinate_dofs = numba.carray(_coordinate_dofs, ({n_coord_dofs})) - entity_local_index = numba.carray(_entity_local_index, ({n_entity_local_index})) - quadrature_permutation = numba.carray(_quadrature_permutation, ({n_quad_perm})) + A = numba.carray(_A, ({points.size})) + w = numba.carray(_w, ({size_w})) + c = numba.carray(_c, ({size_c})) + coordinate_dofs = numba.carray(_coordinate_dofs, ({size_coords})) + entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) + quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ F = Formatter(options["scalar_type"]) body = F.format(parts) @@ -73,7 +70,7 @@ def generator(ir: ExpressionIR, options): # TODO: original_coefficient_positions_init originals = ", ".join(str(i) for i in ir.original_coefficient_positions) d["original_coefficient_positions"] = f"[{originals}]" - # n = points.size + # TODO: points_init d["points"] = f"[{', '.join(str(p) for p in points.flatten())}]" From ffdd3c63ef8ff13cec72d6abcfad3aa806b67011 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:37:43 +0100 Subject: [PATCH 059/106] Activate redundant check, drop uneccessary args --- ffcx/codegeneration/numba/expressions.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index dc251c03f..2592c2984 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -6,6 +6,7 @@ """Generate UFC code for an expression.""" import logging +import string import numpy as np @@ -82,9 +83,6 @@ def generator(ir: ExpressionIR, options): d["num_constants"] = len(ir.constant_names) d["num_points"] = points.shape[0] d["entity_dimension"] = points.shape[1] - d["scalar_type"] = options["scalar_type"] - # d["geom_type"] = dtype_to_scalar_dtype(options["scalar_type"]) - d["np_scalar_type"] = np.dtype(options["scalar_type"]).names d["rank"] = len(ir.expression.tensor_shape) @@ -99,13 +97,10 @@ def generator(ir: ExpressionIR, options): # TODO: coordinate_element_hash # Check that no keys are redundant or have been missed - - # fields = [ - # fname for _, fname, _, _ in string.Formatter().parse(expressions_template.factory) if - # fname - # ] - # assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting - # dict" + fields = [ + fname for _, fname, _, _ in string.Formatter().parse(expressions_template.factory) if fname + ] + assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting dict" # Format implementation code implementation = expressions_template.factory.format_map(d) From cab8da109f702550908b87fcd49c07a8381e15bc Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:42:01 +0100 Subject: [PATCH 060/106] Add coordinate_element_hash to expression --- ffcx/codegeneration/numba/expressions.py | 4 +--- ffcx/codegeneration/numba/expressions_template.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 2592c2984..45f90d042 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -14,8 +14,6 @@ from ffcx.codegeneration.expression_generator import ExpressionGenerator from ffcx.codegeneration.numba import expressions_template from ffcx.codegeneration.numba.implementation import Formatter - -# from ffcx.codegeneration.utils import dtype_to_scalar_dtype from ffcx.ir.representation import ExpressionIR logger = logging.getLogger("ffcx") @@ -94,7 +92,7 @@ def generator(ir: ExpressionIR, options): names = ", ".join(f'"{name}"' for name in ir.constant_names) d["constant_names"] = f"[{names}]" - # TODO: coordinate_element_hash + d["coordinate_element_hash"] = ir.expression.coordinate_element_hash # Check that no keys are redundant or have been missed fields = [ diff --git a/ffcx/codegeneration/numba/expressions_template.py b/ffcx/codegeneration/numba/expressions_template.py index 7ea8978f7..0e8b84b64 100644 --- a/ffcx/codegeneration/numba/expressions_template.py +++ b/ffcx/codegeneration/numba/expressions_template.py @@ -31,7 +31,7 @@ class {factory_name}: value_shape = {value_shape} num_components = {num_components} rank = {rank} - # coordinate_element_hash = coordinate_element_hash + coordinate_element_hash = {coordinate_element_hash} # Alias name {name_from_uflfile} = {factory_name} From 48bd21f846983e301d4404a620dc9f8398608a60 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:55:42 +0100 Subject: [PATCH 061/106] Extend keys check and tidy of integrals --- ffcx/codegeneration/C/integrals.py | 2 ++ ffcx/codegeneration/numba/integrals.py | 27 +++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 6d667adef..eb0b78105 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -91,4 +91,6 @@ def generator(ir: IntegralIR, domain: basix.CellType, options): domain=int(domain), ) + # TODO: Check that no keys are redundant or have been missed (ref. numba/integrals.py) + return declaration, implementation diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 7f770a3c7..90aa1a920 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -7,6 +7,7 @@ """Generate UFC code for an integral.""" import logging +import string import basix import numpy as np @@ -49,11 +50,13 @@ def generator(ir, domain: basix.CellType, options): body = "\n".join(body) # Generate generic FFCx code snippets and add specific parts - code = {} + d = {} + + d["factory_name"] = factory_name # TODO: enabled_coefficients_init - required? vals = ", ".join("1" if i else "0" for i in ir.enabled_coefficients) - code["enabled_coefficients"] = f"[{vals}]" + d["enabled_coefficients"] = f"[{vals}]" # tabulate_tensor # Note: In contrast to the C implementation we actually need to provide/compute the sizes of the @@ -76,18 +79,16 @@ def generator(ir, domain: basix.CellType, options): entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ - code["tabulate_tensor"] = header + body + d["tabulate_tensor"] = header + body + d["needs_facet_permutations"] = "True" if ir.expression.needs_facet_permutations else "False" + d["coordinate_element_hash"] = ir.expression.coordinate_element_hash + d["domain"] = int(domain) assert ir.expression.coordinate_element_hash is not None - implementation = ufcx_integrals.factory.format( - factory_name=factory_name, - enabled_coefficients=code["enabled_coefficients"], - # enabled_coefficients_init=code["enabled_coefficients_init"], - tabulate_tensor=code["tabulate_tensor"], - needs_facet_permutations="True" if ir.expression.needs_facet_permutations else "False", - scalar_type=options["scalar_type"], - coordinate_element_hash=ir.expression.coordinate_element_hash, - domain=int(domain), - ) + implementation = ufcx_integrals.factory.format_map(d) + + # Check that no keys are redundant or have been missed + fields = [fname for _, fname, _, _ in string.Formatter().parse(ufcx_integrals.factory) if fname] + assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting dict" return declaration, implementation From 021a603794e8a6bac296c036580236d425fbf822 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:51:18 +0100 Subject: [PATCH 062/106] Tidy up test_demos and allow for further extensions --- demo/test_demos.py | 88 ++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 001fb9f3c..40d14b426 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -1,75 +1,87 @@ """Test demos.""" import os +import subprocess import sys +from pathlib import Path import pytest -demo_dir = os.path.dirname(os.path.realpath(__file__)) +demo_dir = Path(__file__).parent -ufl_files = [] -for file in os.listdir(demo_dir): - if file.endswith(".py") and not file.endswith("_numba.py") and not file == "test_demos.py": - ufl_files.append(file[:-3]) +ufl_files = [ + f + for f in demo_dir.iterdir() + if f.suffix == ".py" and not f.stem.endswith("_numba") and f != Path(__file__) +] skip_complex = ["BiharmonicHHJ", "BiharmonicRegge", "StabilisedStokes"] +def skip_unsupported(test): + """Dcecorate test case to skip unsupported cases.""" + + def check_skip(file, scalar_type): + """Skip scalar_type file combinations not supported.""" + if "complex" in scalar_type and file.stem in skip_complex: + pytest.skip(reason="Not implemented for complex types") + elif "Complex" in file.stem and scalar_type in ["float64", "float32"]: + pytest.skip(reason="Not implemented for real types") + + return test(file, scalar_type) + + return check_skip + + @pytest.mark.parametrize("file", ufl_files) @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) +@skip_unsupported def test_C(file, scalar_type): """Test a demo.""" if sys.platform.startswith("win32") and "complex" in scalar_type: # Skip complex demos on win32 pytest.skip(reason="_Complex not supported on Windows") - if "complex" in scalar_type and file in skip_complex: - # Skip demos that are not implemented for complex scalars - pytest.skip(reason="Not implemented for complex types") - elif "Complex" in file and scalar_type in ["float64", "float32"]: - # Skip demos that are only implemented for complex scalars - pytest.skip(reason="Not implemented for real types") - - opts = f"--scalar_type {scalar_type}" + subprocess.run(["ffcx", "--scalar_type", scalar_type, file], cwd=demo_dir, check=True) if sys.platform.startswith("win32"): extra_flags = "/std:c17" - assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 - assert ( - os.system( - f'cd {demo_dir} && cl.exe /I "../ffcx/codegeneration" {extra_flags} /c {file}.c' - ) - ) == 0 - assert ( - os.system( - f"cd {demo_dir} && " - f'clang-cl.exe /I "../ffcx/codegeneration" {extra_flags} /c {file}.c' + for compiler in ["cl.exe", "clang-cl.exe"]: + subprocess.run( + [ + compiler, + "/I", + f"{demo_dir.parent / 'ffcx/codegeneration'}", + *extra_flags.split(" "), + "/c", + file.with_suffix(".c"), + ], + cwd=demo_dir, + check=True, ) - ) == 0 else: cc = os.environ.get("CC", "cc") extra_flags = ( "-std=c17 -Wunused-variable -Werror -fPIC -Wno-error=implicit-function-declaration" ) - assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 - assert ( - os.system(f"cd {demo_dir} && {cc} -I../ffcx/codegeneration {extra_flags} -c {file}.c") - == 0 + subprocess.run( + [ + cc, + f"-I{demo_dir.parent / 'ffcx/codegeneration'}", + *extra_flags.split(" "), + "-c", + file.with_suffix(".c"), + ], + cwd=demo_dir, + check=True, ) @pytest.mark.parametrize("file", ufl_files) @pytest.mark.parametrize("scalar_type", ["float64", "float32", "complex128", "complex64"]) +@skip_unsupported def test_numba(file, scalar_type): """Test numba generation.""" opts = f"-L numba --scalar_type {scalar_type}" - - if "complex" in scalar_type and file in skip_complex: - # Skip demos that are not implemented for complex scalars - pytest.skip(reason="Not implemented for complex types") - elif "Complex" in file and scalar_type in ["float64", "float32"]: - # Skip demos that are only implemented for complex scalars - pytest.skip(reason="Not implemented for real types") - - assert os.system(f"cd {demo_dir} && ffcx {opts} {file}.py") == 0 - assert os.system(f"cd {demo_dir} && python {file}.py") == 0 + subprocess.run(["ffcx", *opts.split(" "), file], cwd=demo_dir, check=True) + subprocess.run(["python", file], cwd=demo_dir, check=True) From 52491240ad4720d5e0447a0588ae2068cdbb1fb1 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:28:37 +0100 Subject: [PATCH 063/106] Add key checking to form --- ffcx/codegeneration/numba/form.py | 17 +++-------------- ffcx/codegeneration/numba/form_template.py | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 339924e2c..303f7b135 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -9,6 +9,7 @@ """Template for form output.""" import logging +import string from ffcx.codegeneration.numba import form_template @@ -57,20 +58,8 @@ def generator(ir, options): d["form_integral_offsets"] = f"[{offsets}]" # Check that no keys are redundant or have been missed - from string import Formatter - - fields = [fname for _, fname, _, _ in Formatter().parse(form_template.factory) if fname] - - for f in fields: - if f not in d.keys(): - print(f, "not in d.keys()") - - for f in d.keys(): - if f not in fields: - print(f, "not in fields") - - if set(fields) != set(d.keys()): - print("Mismatch between keys in template and in formatting dict") + fields = [fname for _, fname, _, _ in string.Formatter().parse(form_template.factory) if fname] + assert set(fields) == set(d.keys()), "Mismatch between keys in template and in formatting dict" # Format implementation code implementation = form_template.factory.format_map(d) diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py index a5e008b8b..e830f308b 100644 --- a/ffcx/codegeneration/numba/form_template.py +++ b/ffcx/codegeneration/numba/form_template.py @@ -25,6 +25,5 @@ class {factory_name}(object): {name_from_uflfile} = {factory_name} -# Name: {name_from_uflfile} # End of code for form {factory_name} """ From ca6a9e865d9852569e640f365abfc88df6d3fec8 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:22:51 +0100 Subject: [PATCH 064/106] Try with Path --- ffcx/formatting.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ffcx/formatting.py b/ffcx/formatting.py index f8de9b79d..7400f1b05 100644 --- a/ffcx/formatting.py +++ b/ffcx/formatting.py @@ -16,7 +16,7 @@ from __future__ import annotations import logging -import os +from pathlib import Path from ffcx.codegeneration.codegeneration import CodeBlocks @@ -50,6 +50,5 @@ def write_code( def _write_file(output: str, prefix: str, suffix: str, output_dir: str) -> None: """Write generated code to file.""" - filename = os.path.join(output_dir, prefix + suffix) - with open(filename, "w") as hfile: - hfile.write(output) + with open(Path(output_dir) / (prefix + suffix), "w") as file: + file.write(output) From a45b46412b23f951a239362b62b8c310c1637fc5 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:15:53 +0100 Subject: [PATCH 065/106] Add choices --- ffcx/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ffcx/main.py b/ffcx/main.py index e0c91012a..d06a3339f 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -29,7 +29,9 @@ ) parser.add_argument("--version", action="version", version=f"%(prog)s (version {FFCX_VERSION})") parser.add_argument("-o", "--output-directory", type=str, default=".", help="output directory") -parser.add_argument("-L", "--language", type=str, default="C", help="target language") +parser.add_argument( + "-L", "--language", type=str, default="C", choices=("C", "numba"), help="target language" +) parser.add_argument("--visualise", action="store_true", help="visualise the IR graph") parser.add_argument("-p", "--profile", action="store_true", help="enable profiling") From 85464377169ed745bca65a75aa8d60ee34de1ecc Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:17:50 +0100 Subject: [PATCH 066/106] only import basix.ufl --- demo/MassAction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/MassAction.py b/demo/MassAction.py index 2a02e6b8a..17ca2e868 100644 --- a/demo/MassAction.py +++ b/demo/MassAction.py @@ -5,7 +5,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Mass action demo.""" -import basix import basix.ufl import ufl From d234b84adfe7b23d72d12dbdc68f7ed9c5349c4b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:49:21 +0100 Subject: [PATCH 067/106] type hints --- ffcx/codegeneration/C/integrals.py | 5 ++- ffcx/codegeneration/numba/expressions.py | 3 +- ffcx/codegeneration/numba/file.py | 6 ++- ffcx/codegeneration/numba/form.py | 4 +- ffcx/codegeneration/numba/implementation.py | 42 ++++++++++----------- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index eb0b78105..926c0ad96 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -10,6 +10,7 @@ import basix import numpy as np +from numpy import typing as npt from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.C import integrals_template as ufcx_integrals @@ -21,7 +22,9 @@ logger = logging.getLogger("ffcx") -def generator(ir: IntegralIR, domain: basix.CellType, options): +def generator( + ir: IntegralIR, domain: basix.CellType, options: dict[str, int | float | npt.DTypeLike] +): """Generate C code for an integral.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 45f90d042..ae7d1eead 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -9,6 +9,7 @@ import string import numpy as np +import numpy.typing as npt from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.expression_generator import ExpressionGenerator @@ -19,7 +20,7 @@ logger = logging.getLogger("ffcx") -def generator(ir: ExpressionIR, options): +def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) -> tuple[str, str]: """Generate UFC code for an expression.""" logger.info("Generating code for expression:") assert len(ir.expression.integrand) == 1, "Expressions only support single quadrature rule" diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py index d46771c25..7eaaee051 100644 --- a/ffcx/codegeneration/numba/file.py +++ b/ffcx/codegeneration/numba/file.py @@ -13,6 +13,8 @@ import pprint import textwrap +from numpy import typing as npt + from ffcx import __version__ as FFCX_VERSION from ffcx.codegeneration import __version__ as UFC_VERSION from ffcx.codegeneration.numba import file_template @@ -20,7 +22,9 @@ logger = logging.getLogger("ffcx") -def generator(options): +def generator( + options: dict[str, int | float | npt.DTypeLike], +) -> tuple[tuple[str, str], tuple[str, str]]: """Generate UFC code for file output.""" logger.info("Generating code for file") diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 303f7b135..df9a43fd6 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -11,12 +11,14 @@ import logging import string +from numpy import typing as npt + from ffcx.codegeneration.numba import form_template logger = logging.getLogger("ffcx") -def generator(ir, options): +def generator(ir, options: dict[str, int | float | npt.DTypeLike]) -> tuple[str, str]: """Generate UFC code for a form.""" logger.info("Generating code for form:") logger.info(f"--- rank: {ir.rank}") diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 2eac7925e..b0fb73d93 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -26,7 +26,7 @@ def __init__(self, scalar) -> None: """Initialise.""" self.scalar_type = scalar - def format_section(self, section): + def format_section(self, section: L.Section) -> str: """Format a section.""" # add new line before section comments = "# ------------------------ \n" @@ -42,18 +42,18 @@ def format_section(self, section): body += "# ------------------------ \n" return comments + declarations + body - def format_statement_list(self, slist): + def format_statement_list(self, slist: L.StatementList) -> str: """Format a list of statements.""" output = "" for s in slist.statements: output += self.format(s) return output - def format_comment(self, c): + def format_comment(self, c: L.Comment) -> str: """Format a comment.""" return "# " + c.comment + "\n" - def format_array_decl(self, arr): + def format_array_decl(self, arr: L.ArrayDecl) -> str: """Format an array declaration.""" if arr.symbol.dtype == L.DataType.SCALAR: dtype = "A.dtype" @@ -70,23 +70,23 @@ def format_array_decl(self, arr): av = "np.array(" + av + f", dtype={dtype})" return f"{symbol} = {av}\n" - def format_array_access(self, arr): + def format_array_access(self, arr: L.ArrayAccess) -> str: """Format array access.""" array = self.format(arr.array) idx = ", ".join(self.format(ix) for ix in arr.indices) return f"{array}[{idx}]" - def format_multi_index(self, index): + def format_multi_index(self, index: L.MultiIndex) -> str: """Format a multi-index.""" return self.format(index.global_index) - def format_variable_decl(self, v): + def format_variable_decl(self, v: L.VariableDecl) -> str: """Format a variable declaration.""" sym = self.format(v.symbol) val = self.format(v.value) return f"{sym} = {val}\n" - def format_nary_op(self, oper): + def format_nary_op(self, oper: L.NaryOp) -> str: """Format a n argument operation.""" # Format children args = [self.format(arg) for arg in oper.args] @@ -99,7 +99,7 @@ def format_nary_op(self, oper): # Return combined string return f" {oper.op} ".join(args) - def format_binary_op(self, oper): + def format_binary_op(self, oper: L.BinOp) -> str: """Format a binary operation.""" # Format children lhs = self.format(oper.lhs) @@ -114,17 +114,17 @@ def format_binary_op(self, oper): # Return combined string return f"{lhs} {oper.op} {rhs}" - def format_neg(self, val): + def format_neg(self, val: L.Neg) -> str: """Format unary negation.""" arg = self.format(val.arg) return f"-{arg}" - def format_not(self, val): + def format_not(self, val: L.Not) -> str: """Format not operation.""" arg = self.format(val.arg) return f"not({arg})" - def format_andor(self, oper): + def format_andor(self, oper: L.And | L.Or) -> str: """Format and or or operation.""" # Format children lhs = self.format(oper.lhs) @@ -141,15 +141,15 @@ def format_andor(self, oper): # Return combined string return f"{lhs} {opstr} {rhs}" - def format_literal_float(self, val): + def format_literal_float(self, val: L.LiteralFloat) -> str: """Format a literal float.""" return f"{val.value}" - def format_literal_int(self, val): + def format_literal_int(self, val: L.LiteralInt) -> str: """Format a literal int.""" return f"{val.value}" - def format_for_range(self, r): + def format_for_range(self, r: L.ForRange) -> str: """Format a loop over a range.""" begin = self.format(r.begin) end = self.format(r.end) @@ -160,17 +160,17 @@ def format_for_range(self, r): output += f" {line}\n" return output - def format_statement(self, s): + def format_statement(self, s: L.Statement) -> str: """Format a statement.""" return self.format(s.expr) - def format_assign(self, expr): + def format_assign(self, expr: L.Assign) -> str: """Format assignment.""" rhs = self.format(expr.rhs) lhs = self.format(expr.lhs) return f"{lhs} {expr.op} {rhs}\n" - def format_conditional(self, s): + def format_conditional(self, s: L.Conditional) -> str: """Format a conditional.""" # Format children c = self.format(s.condition) @@ -188,11 +188,11 @@ def format_conditional(self, s): # Return combined string return f"({t} if {c} else {f})" - def format_symbol(self, s): + def format_symbol(self, s: L.Symbol) -> str: """Format a symbol.""" return f"{s.name}" - def format_mathfunction(self, f): + def format_mathfunction(self, f: L.MathFunction) -> str: """Format a math function.""" function_map = { "ln": "log", @@ -248,7 +248,7 @@ def format_mathfunction(self, f): "LT": format_binary_op, } - def format(self, s): + def format(self, s: L.LNode) -> str: """Format output.""" name = s.__class__.__name__ try: From 4dc78afccda0ed94385fffeb3ddf6b649bdd4a03 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:56:57 +0100 Subject: [PATCH 068/106] fixes --- ffcx/codegeneration/lnodes.py | 8 ++++++++ ffcx/codegeneration/numba/implementation.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ffcx/codegeneration/lnodes.py b/ffcx/codegeneration/lnodes.py index e846b0579..debd4441e 100644 --- a/ffcx/codegeneration/lnodes.py +++ b/ffcx/codegeneration/lnodes.py @@ -268,18 +268,24 @@ def __rdiv__(self, other): class LExprOperator(LExpr): """Base class for all expression operators.""" + precedence: int + sideeffect = False class LExprTerminal(LExpr): """Base class for all expression terminals.""" + precedence: int + sideeffect = False class LiteralFloat(LExprTerminal): """A floating point literal value.""" + precedence: int + precedence = PRECEDENCE.LITERAL def __init__(self, value): @@ -446,6 +452,8 @@ def __eq__(self, other): class BinOp(LExprOperator): """A binary operator.""" + op: str + def __init__(self, lhs, rhs): """Initialise.""" self.lhs = as_lexpr(lhs) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index b0fb73d93..204fbcf5f 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -5,6 +5,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Numba implementation for output.""" +from typing import Callable, Self import ffcx.codegeneration.lnodes as L @@ -213,7 +214,7 @@ def format_mathfunction(self, f: L.MathFunction) -> str: argstr = ", ".join(args) return f"np.{function}({argstr})" - impl = { + impl: dict[str, Callable[[Self, L.LNode], str]] = { "StatementList": format_statement_list, "Comment": format_comment, "Section": format_section, From fa455317cccccd453088deb03456305245444b80 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:59:36 +0100 Subject: [PATCH 069/106] no self --- ffcx/codegeneration/numba/implementation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 204fbcf5f..c8a39548c 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -214,7 +214,7 @@ def format_mathfunction(self, f: L.MathFunction) -> str: argstr = ", ".join(args) return f"np.{function}({argstr})" - impl: dict[str, Callable[[Self, L.LNode], str]] = { + impl: dict[str, Callable[["Formatter", L.LNode], str]] = { "StatementList": format_statement_list, "Comment": format_comment, "Section": format_section, From ff6498c6cfc927ee83fd75bc5ef16a101187870f Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:03:03 +0100 Subject: [PATCH 070/106] derived --- ffcx/codegeneration/numba/implementation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index c8a39548c..bebe95d34 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -214,7 +214,7 @@ def format_mathfunction(self, f: L.MathFunction) -> str: argstr = ", ".join(args) return f"np.{function}({argstr})" - impl: dict[str, Callable[["Formatter", L.LNode], str]] = { + impl: dict[str, Callable[["Formatter", type[L.LNode]], str]] = { "StatementList": format_statement_list, "Comment": format_comment, "Section": format_section, From 3eeb26d25f08ec2ffd2749710710e60ac2b1a0ad Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:11:46 +0100 Subject: [PATCH 071/106] more --- ffcx/codegeneration/C/expressions.py | 2 +- ffcx/codegeneration/C/integrals.py | 2 +- ffcx/codegeneration/numba/expressions.py | 2 +- ffcx/codegeneration/numba/implementation.py | 5 ++--- ffcx/codegeneration/numba/integrals.py | 7 +++++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ffcx/codegeneration/C/expressions.py b/ffcx/codegeneration/C/expressions.py index 7ad470ccf..fb0a3aeb6 100644 --- a/ffcx/codegeneration/C/expressions.py +++ b/ffcx/codegeneration/C/expressions.py @@ -44,7 +44,7 @@ def generator(ir: ExpressionIR, options): d["factory_name"] = factory_name parts = eg.generate() - CF = Formatter(options["scalar_type"]) + CF = Formatter(options["scalar_type"]) # type: ignore d["tabulate_expression"] = CF.format(parts) if len(ir.original_coefficient_positions) > 0: diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 926c0ad96..ec8d6617d 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -45,7 +45,7 @@ def generator( parts = ig.generate(domain) # Format code as string - CF = Formatter(options["scalar_type"]) + CF = Formatter(options["scalar_type"]) # type: ignore body = CF.format(parts) # Generate generic FFCx code snippets and add specific parts diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index ae7d1eead..f58801977 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -60,7 +60,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ - F = Formatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) # type: ignore body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index bebe95d34..5ec3d5a3f 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -5,7 +5,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Numba implementation for output.""" -from typing import Callable, Self import ffcx.codegeneration.lnodes as L @@ -214,7 +213,7 @@ def format_mathfunction(self, f: L.MathFunction) -> str: argstr = ", ".join(args) return f"np.{function}({argstr})" - impl: dict[str, Callable[["Formatter", type[L.LNode]], str]] = { + impl = { "StatementList": format_statement_list, "Comment": format_comment, "Section": format_section, @@ -253,6 +252,6 @@ def format(self, s: L.LNode) -> str: """Format output.""" name = s.__class__.__name__ try: - return self.impl[name](self, s) + return self.impl[name](self, s) # type: ignore except KeyError: raise RuntimeError("Unknown statement: ", name) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 90aa1a920..fbe2d0553 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -11,6 +11,7 @@ import basix import numpy as np +from numpy import typing as npt from ffcx.codegeneration.backend import FFCXBackend from ffcx.codegeneration.integral_generator import IntegralGenerator @@ -20,7 +21,9 @@ logger = logging.getLogger("ffcx") -def generator(ir, domain: basix.CellType, options): +def generator( + ir, domain: basix.CellType, options: dict[str, int | float | npt.DTypeLike] +) -> tuple[str, str]: """Generate numba code for an integral.""" logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") @@ -44,7 +47,7 @@ def generator(ir, domain: basix.CellType, options): parts = ig.generate(domain) # Format code as string - F = Formatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) # type: ignore body = F.format(parts) body = [" " + line for line in body.split("\n")] body = "\n".join(body) From fcb563628653e2e8237c8449e71c7cbe8f26eabc Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:16:25 +0100 Subject: [PATCH 072/106] . --- ffcx/codegeneration/numba/integrals.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index fbe2d0553..f57afcee3 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -47,10 +47,9 @@ def generator( parts = ig.generate(domain) # Format code as string - F = Formatter(options["scalar_type"]) # type: ignore + F = Formatter(options["scalar_type"]) body = F.format(parts) - body = [" " + line for line in body.split("\n")] - body = "\n".join(body) + body = "\n".join([" " + line for line in body.split("\n")]) # Generate generic FFCx code snippets and add specific parts d = {} From 43c46d280bb636d8e47c8084e007435793d2d18b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:19:39 +0100 Subject: [PATCH 073/106] +1 --- ffcx/codegeneration/C/expressions.py | 2 +- ffcx/codegeneration/C/integrals.py | 6 +++--- ffcx/codegeneration/numba/expressions.py | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ffcx/codegeneration/C/expressions.py b/ffcx/codegeneration/C/expressions.py index fb0a3aeb6..7ad470ccf 100644 --- a/ffcx/codegeneration/C/expressions.py +++ b/ffcx/codegeneration/C/expressions.py @@ -44,7 +44,7 @@ def generator(ir: ExpressionIR, options): d["factory_name"] = factory_name parts = eg.generate() - CF = Formatter(options["scalar_type"]) # type: ignore + CF = Formatter(options["scalar_type"]) d["tabulate_expression"] = CF.format(parts) if len(ir.original_coefficient_positions) > 0: diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index ec8d6617d..cb8f8d6e1 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -72,7 +72,7 @@ def generator( else: code["tabulate_tensor_complex64"] = ".tabulate_tensor_complex64 = NULL," code["tabulate_tensor_complex128"] = ".tabulate_tensor_complex128 = NULL," - np_scalar_type = np.dtype(options["scalar_type"]).name + np_scalar_type = np.dtype(options["scalar_type"]).name # type: ignore code[f"tabulate_tensor_{np_scalar_type}"] = ( f".tabulate_tensor_{np_scalar_type} = tabulate_tensor_{factory_name}," ) @@ -84,8 +84,8 @@ def generator( enabled_coefficients_init=code["enabled_coefficients_init"], tabulate_tensor=code["tabulate_tensor"], needs_facet_permutations="true" if ir.expression.needs_facet_permutations else "false", - scalar_type=dtype_to_c_type(options["scalar_type"]), - geom_type=dtype_to_c_type(dtype_to_scalar_dtype(options["scalar_type"])), + scalar_type=dtype_to_c_type(options["scalar_type"]), # type: ignore + geom_type=dtype_to_c_type(dtype_to_scalar_dtype(options["scalar_type"])), # type: ignore coordinate_element_hash=f"UINT64_C({ir.expression.coordinate_element_hash})", tabulate_tensor_float32=code["tabulate_tensor_float32"], tabulate_tensor_float64=code["tabulate_tensor_float64"], diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index f58801977..932787296 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -60,10 +60,9 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ - F = Formatter(options["scalar_type"]) # type: ignore + F = Formatter(options["scalar_type"]) body = F.format(parts) - body = [" " + line for line in body.split("\n")] - body = "\n".join(body) + body = "\n".join([" " + line for line in body.split("\n")]) d["tabulate_expression"] = header + body From 2b2fab44142afeab760ebb4aaa089e7cd3e2f51c Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:24:25 +0100 Subject: [PATCH 074/106] format --- ffcx/codegeneration/C/integrals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index cb8f8d6e1..2d7474219 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -84,8 +84,8 @@ def generator( enabled_coefficients_init=code["enabled_coefficients_init"], tabulate_tensor=code["tabulate_tensor"], needs_facet_permutations="true" if ir.expression.needs_facet_permutations else "false", - scalar_type=dtype_to_c_type(options["scalar_type"]), # type: ignore - geom_type=dtype_to_c_type(dtype_to_scalar_dtype(options["scalar_type"])), # type: ignore + scalar_type=dtype_to_c_type(options["scalar_type"]), # type: ignore + geom_type=dtype_to_c_type(dtype_to_scalar_dtype(options["scalar_type"])), # type: ignore coordinate_element_hash=f"UINT64_C({ir.expression.coordinate_element_hash})", tabulate_tensor_float32=code["tabulate_tensor_float32"], tabulate_tensor_float64=code["tabulate_tensor_float64"], From 32e5f66959e538416b840825c64649f4e2ce5d43 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:22:39 +0100 Subject: [PATCH 075/106] race condition on windows? --- .github/workflows/pythonapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 679954958..0b728cf12 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -124,8 +124,8 @@ jobs: - name: Run FFCx demos run: > pytest demo/test_demos.py - -n auto -W error + # -n auto - name: Build documentation run: | From e196d8690902a6d7ab6312adb636b5422ee1a325 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:27:35 +0100 Subject: [PATCH 076/106] No subprocess for FFCx call - should result in accurate coverage reports --- demo/test_demos.py | 8 +++++--- test/test_numba.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 40d14b426..b8db14cf2 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -7,6 +7,8 @@ import pytest +import ffcx.main + demo_dir = Path(__file__).parent ufl_files = [ @@ -42,7 +44,7 @@ def test_C(file, scalar_type): # Skip complex demos on win32 pytest.skip(reason="_Complex not supported on Windows") - subprocess.run(["ffcx", "--scalar_type", scalar_type, file], cwd=demo_dir, check=True) + assert ffcx.main.main(["--scalar_type", scalar_type, str(file)]) == 0 if sys.platform.startswith("win32"): extra_flags = "/std:c17" @@ -82,6 +84,6 @@ def test_C(file, scalar_type): @skip_unsupported def test_numba(file, scalar_type): """Test numba generation.""" - opts = f"-L numba --scalar_type {scalar_type}" - subprocess.run(["ffcx", *opts.split(" "), file], cwd=demo_dir, check=True) + opts = f"--language numba --scalar_type {scalar_type}" + assert ffcx.main.main([*opts.split(" "), str(file)]) == 0 subprocess.run(["python", file], cwd=demo_dir, check=True) diff --git a/test/test_numba.py b/test/test_numba.py index afcf98285..8106ab1b5 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -14,6 +14,7 @@ import numpy.typing as npt import pytest +import ffcx.main from ffcx.codegeneration.utils import dtype_to_scalar_dtype, numba_ufcx_kernel_signature @@ -31,7 +32,7 @@ def as_C_array(np_array: npt.NDArray): def test_integral(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" dir = Path(__file__).parent - subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) + assert ffcx.main.main([str(dir / "poisson.py"), *opts.split(" ")]) == 0 poisson = importlib.import_module("poisson_numba") @@ -88,7 +89,7 @@ def test_integral(scalar_type: str) -> None: def test_expression(scalar_type: str) -> None: opts = f"--language numba --scalar_type {scalar_type}" dir = Path(__file__).parent - subprocess.run(["ffcx", dir / "poisson.py", *opts.split(" ")], check=True) + assert ffcx.main.main([str(dir / "poisson.py"), *opts.split(" ")]) == 0 poisson = importlib.import_module("poisson_numba") From c808bfd9444f6cc52382ae2f81ae7a03fa2a97bb Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:29:28 +0100 Subject: [PATCH 077/106] ruff --- test/test_numba.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_numba.py b/test/test_numba.py index 8106ab1b5..6a441c65b 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -6,7 +6,6 @@ import ctypes import importlib -import subprocess from pathlib import Path import numba From 880c44855c1e366981c26aa42f246d4ae09ed668 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:35:49 +0100 Subject: [PATCH 078/106] Revert for demos, cwd more important than coverage --- demo/test_demos.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index b8db14cf2..40d14b426 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -7,8 +7,6 @@ import pytest -import ffcx.main - demo_dir = Path(__file__).parent ufl_files = [ @@ -44,7 +42,7 @@ def test_C(file, scalar_type): # Skip complex demos on win32 pytest.skip(reason="_Complex not supported on Windows") - assert ffcx.main.main(["--scalar_type", scalar_type, str(file)]) == 0 + subprocess.run(["ffcx", "--scalar_type", scalar_type, file], cwd=demo_dir, check=True) if sys.platform.startswith("win32"): extra_flags = "/std:c17" @@ -84,6 +82,6 @@ def test_C(file, scalar_type): @skip_unsupported def test_numba(file, scalar_type): """Test numba generation.""" - opts = f"--language numba --scalar_type {scalar_type}" - assert ffcx.main.main([*opts.split(" "), str(file)]) == 0 + opts = f"-L numba --scalar_type {scalar_type}" + subprocess.run(["ffcx", *opts.split(" "), file], cwd=demo_dir, check=True) subprocess.run(["python", file], cwd=demo_dir, check=True) From ef0e3a2188fe2d7cc283e42efa5af73eb1e6cd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20T=2E=20K=C3=BChner?= <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:37:48 +0100 Subject: [PATCH 079/106] Update test/poisson.py Co-authored-by: Michal Habera --- test/poisson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/poisson.py b/test/poisson.py index df0bc5561..e9424176e 100644 --- a/test/poisson.py +++ b/test/poisson.py @@ -1,4 +1,4 @@ -# Copyright (C) 2004-20025 Anders Logg and Paul T. Kühner +# Copyright (C) 2004-2025 Anders Logg and Paul T. Kühner # # This file is part of FFCx. # From a6b4113aa090c647131ee937cadcaae29ad3bf98 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:47:08 +0100 Subject: [PATCH 080/106] Drop user defined import --- ffcx/codegeneration/codegeneration.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 2e5331df0..85c55b5a0 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -45,15 +45,7 @@ def generate_code( logger.info(79 * "*") lang = options.get("language", "C") - # try: - # Built-in mod = import_module(f"ffcx.codegeneration.{lang}") - # except ImportError: - # # User defined language (experimental) - # store_path = sys.path - # sys.path = ["."] - # mod = import_module(f"{lang}") - # sys.path = store_path integral_generator = mod.integrals.generator form_generator = mod.form.generator From e6766804cba4dac33337b389e533b859805e9bae Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:56:42 +0100 Subject: [PATCH 081/106] Move --language option to options.py --- demo/test_demos.py | 2 +- ffcx/main.py | 3 --- ffcx/options.py | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 40d14b426..88e93c116 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -82,6 +82,6 @@ def test_C(file, scalar_type): @skip_unsupported def test_numba(file, scalar_type): """Test numba generation.""" - opts = f"-L numba --scalar_type {scalar_type}" + opts = f"--language numba --scalar_type {scalar_type}" subprocess.run(["ffcx", *opts.split(" "), file], cwd=demo_dir, check=True) subprocess.run(["python", file], cwd=demo_dir, check=True) diff --git a/ffcx/main.py b/ffcx/main.py index d06a3339f..44bd7238c 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -29,9 +29,6 @@ ) parser.add_argument("--version", action="version", version=f"%(prog)s (version {FFCX_VERSION})") parser.add_argument("-o", "--output-directory", type=str, default=".", help="output directory") -parser.add_argument( - "-L", "--language", type=str, default="C", choices=("C", "numba"), help="target language" -) parser.add_argument("--visualise", action="store_true", help="visualise the IR graph") parser.add_argument("-p", "--profile", action="store_true", help="enable profiling") diff --git a/ffcx/options.py b/ffcx/options.py index cd6ec8d1a..d9908cc88 100644 --- a/ffcx/options.py +++ b/ffcx/options.py @@ -20,6 +20,7 @@ logger = logging.getLogger("ffcx") FFCX_DEFAULT_OPTIONS = { + "language": (str, "C", "language to output kernel in", ("C", "numba")), "epsilon": (float, 1e-14, "machine precision, used for dropping zero terms in tables.", None), "scalar_type": ( str, From d8f317a85a6118e3ede2cb1c11652d35e108be35 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:10:06 +0100 Subject: [PATCH 082/106] Fix: size of tensor, extend test to tensor valued expression --- ffcx/codegeneration/numba/expressions.py | 12 ++++++++---- test/poisson.py | 2 +- test/test_numba.py | 10 +++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 932787296..2fb06bebe 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -43,6 +43,10 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) parts = eg.generate() # tabulate_expression + num_points = points.shape[0] + num_components = np.prod(ir.expression.shape) + num_coefficients = len(ir.expression.coefficient_numbering) + size_A = num_points * num_components * num_coefficients # ref. ufcx.h size_w = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) size_c = sum( np.prod(constant.ufl_shape, dtype=int) @@ -53,7 +57,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) size_permutation = 2 if ir.expression.needs_facet_permutations else 0 header = f""" - A = numba.carray(_A, ({points.size})) + A = numba.carray(_A, ({size_A})) w = numba.carray(_w, ({size_w})) c = numba.carray(_c, ({size_c})) coordinate_dofs = numba.carray(_coordinate_dofs, ({size_coords})) @@ -76,10 +80,10 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) # TODO: value_shape_init shape = ", ".join(str(i) for i in ir.expression.shape) d["value_shape"] = f"[{shape}]" - d["num_components"] = len(ir.expression.shape) - d["num_coefficients"] = len(ir.expression.coefficient_numbering) + d["num_components"] = num_components + d["num_coefficients"] = num_coefficients d["num_constants"] = len(ir.constant_names) - d["num_points"] = points.shape[0] + d["num_points"] = num_points d["entity_dimension"] = points.shape[1] d["rank"] = len(ir.expression.tensor_shape) diff --git a/test/poisson.py b/test/poisson.py index e9424176e..c7468a5c7 100644 --- a/test/poisson.py +++ b/test/poisson.py @@ -50,7 +50,7 @@ L = f * v * dx # Expressions -e_vec = basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)) +e_vec = basix.ufl.element("Lagrange", "triangle", 1, shape=(2, 3)) space_vec = FunctionSpace(mesh, e_vec) f_vec = Coefficient(space_vec) diff --git a/test/test_numba.py b/test/test_numba.py index 6a441c65b..335b5f0e6 100644 --- a/test/test_numba.py +++ b/test/test_numba.py @@ -97,8 +97,10 @@ def test_expression(scalar_type: str) -> None: kernel_expr = wrap_kernel(dtype, dtype_r)(poisson.expression_poisson_0.tabulate_tensor) - e = np.zeros((2 * 3,), dtype=dtype) - w = np.array([1, 1, 2, 2, 3, 3], dtype=dtype) + e = np.zeros((6 * 3,), dtype=dtype) + w = np.array( + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11], dtype=dtype + ) kappa_value = np.array([[1.0, 2.0], [3.0, 4.0]]) c = np.array(kappa_value.flatten(), dtype=dtype) coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=dtype_r) @@ -113,5 +115,7 @@ def test_expression(scalar_type: str) -> None: as_C_array(empty), 0, ) - e_expected = np.array([3, 7, 6, 14, 9, 21], dtype=dtype) + e_expected = np.array( + [5, 7, 8, 11, 15, 18, 14, 16, 17, 32, 36, 39, 23, 25, 26, 53, 57, 60], dtype=dtype + ) assert np.allclose(e, e_expected) From 9ce56a1f5c4cb0c4613f5b0400349d69268c261a Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:18:41 +0100 Subject: [PATCH 083/106] off by one --- ffcx/codegeneration/numba/expressions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 2fb06bebe..f0a53a231 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -45,8 +45,8 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) # tabulate_expression num_points = points.shape[0] num_components = np.prod(ir.expression.shape) - num_coefficients = len(ir.expression.coefficient_numbering) - size_A = num_points * num_components * num_coefficients # ref. ufcx.h + num_argument_dofs = np.prod(ir.expression.tensor_shape, dtype=np.int32) + size_A = num_points * num_components * num_argument_dofs # ref. ufcx.h size_w = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) size_c = sum( np.prod(constant.ufl_shape, dtype=int) @@ -81,7 +81,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) shape = ", ".join(str(i) for i in ir.expression.shape) d["value_shape"] = f"[{shape}]" d["num_components"] = num_components - d["num_coefficients"] = num_coefficients + d["num_coefficients"] = len(ir.expression.coefficient_numbering) d["num_constants"] = len(ir.constant_names) d["num_points"] = num_points d["entity_dimension"] = points.shape[1] From 80b53d6e8b772ea3dd8c2f538529557c51445e58 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:19:37 +0100 Subject: [PATCH 084/106] int --- ffcx/codegeneration/numba/expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index f0a53a231..5db97bcc6 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -44,7 +44,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) # tabulate_expression num_points = points.shape[0] - num_components = np.prod(ir.expression.shape) + num_components = np.prod(ir.expression.shape, dtype=np.int32) num_argument_dofs = np.prod(ir.expression.tensor_shape, dtype=np.int32) size_A = num_points * num_components * num_argument_dofs # ref. ufcx.h size_w = sum(coeff.ufl_element().dim for coeff in ir.expression.coefficient_offsets.keys()) From c2c6da401e7a42dba1e82cb2884a3bfbbc7d30ef Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:21:25 +0100 Subject: [PATCH 085/106] mypy --- ffcx/codegeneration/numba/expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 5db97bcc6..66195176a 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -80,7 +80,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) # TODO: value_shape_init shape = ", ".join(str(i) for i in ir.expression.shape) d["value_shape"] = f"[{shape}]" - d["num_components"] = num_components + d["num_components"] = int(num_components) d["num_coefficients"] = len(ir.expression.coefficient_numbering) d["num_constants"] = len(ir.constant_names) d["num_points"] = num_points From 6b71383da8f8601de56cefce3ab40d874bbd6507 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:18:53 +0100 Subject: [PATCH 086/106] Get type info from formatter - use _dtype_to_name logic --- ffcx/codegeneration/numba/implementation.py | 41 +++++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 5ec3d5a3f..0e263f50d 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -1,11 +1,15 @@ -# Copyright (C) 2025 Chris Richardson +# Copyright (C) 2025 Chris Richardson and Paul T. Kühner # # This file is part of FFCx. (https://www.fenicsproject.org) # # SPDX-License-Identifier: LGPL-3.0-or-later """Numba implementation for output.""" +import numpy as np +from numpy import typing as npt + import ffcx.codegeneration.lnodes as L +from ffcx.codegeneration.utils import dtype_to_scalar_dtype def build_initializer_lists(values): @@ -22,9 +26,25 @@ def build_initializer_lists(values): class Formatter: """Implementation for numba output backend.""" - def __init__(self, scalar) -> None: + scalar_type: np.dtype + real_type: np.dtype + + def __init__(self, dtype: npt.DTypeLike) -> None: """Initialise.""" - self.scalar_type = scalar + self.scalar_type = dtype + self.real_type = dtype_to_scalar_dtype(dtype) + + def _dtype_to_name(self, dtype) -> str: + """Convert dtype to Python name.""" + if dtype == L.DataType.SCALAR: + return f"np.{self.scalar_type}" + if dtype == L.DataType.REAL: + return f"np.{self.real_type}" + if dtype == L.DataType.INT: + return f"np.{np.int32}" + if dtype == L.DataType.BOOL: + return f"np.{np.bool}" + raise ValueError(f"Invalid dtype: {dtype}") def format_section(self, section: L.Section) -> str: """Format a section.""" @@ -55,19 +75,16 @@ def format_comment(self, c: L.Comment) -> str: def format_array_decl(self, arr: L.ArrayDecl) -> str: """Format an array declaration.""" - if arr.symbol.dtype == L.DataType.SCALAR: - dtype = "A.dtype" - elif arr.symbol.dtype == L.DataType.REAL: - dtype = "coordinate_dofs.dtype" - elif arr.symbol.dtype == L.DataType.INT: - dtype = "np.int32" + dtype = arr.symbol.dtype + typename = self._dtype_to_name(dtype) + symbol = self.format(arr.symbol) if arr.values is None: - return f"{symbol} = np.empty({arr.sizes}, dtype={dtype})\n" + return f"{symbol} = np.empty({arr.sizes}, dtype={typename})\n" elif arr.values.size == 1: - return f"{symbol} = np.full({arr.sizes}, {arr.values[0]}, dtype={dtype})\n" + return f"{symbol} = np.full({arr.sizes}, {arr.values[0]}, dtype={typename})\n" av = build_initializer_lists(arr.values) - av = "np.array(" + av + f", dtype={dtype})" + av = "np.array(" + av + f", dtype={typename})" return f"{symbol} = {av}\n" def format_array_access(self, arr: L.ArrayAccess) -> str: From 1251d16f8ea8547d380f87eed7e4cffbc8723dde Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:22:26 +0100 Subject: [PATCH 087/106] mypy --- ffcx/codegeneration/numba/implementation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 0e263f50d..5a8b3a7ed 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -31,7 +31,7 @@ class Formatter: def __init__(self, dtype: npt.DTypeLike) -> None: """Initialise.""" - self.scalar_type = dtype + self.scalar_type = np.dtype(dtype) self.real_type = dtype_to_scalar_dtype(dtype) def _dtype_to_name(self, dtype) -> str: From 7916b2dcb1d299e77587ffa1f4e172b6493be6d3 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:25:23 +0100 Subject: [PATCH 088/106] ignore --- ffcx/codegeneration/numba/expressions.py | 2 +- ffcx/codegeneration/numba/integrals.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 66195176a..6478d0944 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -64,7 +64,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ - F = Formatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) # type: ignore body = F.format(parts) body = "\n".join([" " + line for line in body.split("\n")]) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index f57afcee3..747daa16a 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -47,7 +47,7 @@ def generator( parts = ig.generate(domain) # Format code as string - F = Formatter(options["scalar_type"]) + F = Formatter(options["scalar_type"]) # type: ignore body = F.format(parts) body = "\n".join([" " + line for line in body.split("\n")]) From 8c5d7cd563ca27a7b5c5cc17ede4d91b7ad88a9b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:28:54 +0100 Subject: [PATCH 089/106] format --- ffcx/codegeneration/numba/expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/expressions.py b/ffcx/codegeneration/numba/expressions.py index 6478d0944..7908e08a0 100644 --- a/ffcx/codegeneration/numba/expressions.py +++ b/ffcx/codegeneration/numba/expressions.py @@ -64,7 +64,7 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike]) entity_local_index = numba.carray(_entity_local_index, ({size_local_index})) quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation})) """ - F = Formatter(options["scalar_type"]) # type: ignore + F = Formatter(options["scalar_type"]) # type: ignore body = F.format(parts) body = "\n".join([" " + line for line in body.split("\n")]) From 611ebb9eedb23161d59503d447446b1167d9ef56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20T=2E=20K=C3=BChner?= <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:55:33 +0100 Subject: [PATCH 090/106] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jørgen Schartum Dokken --- ffcx/codegeneration/numba/implementation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 5a8b3a7ed..657c3cb77 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -34,7 +34,7 @@ def __init__(self, dtype: npt.DTypeLike) -> None: self.scalar_type = np.dtype(dtype) self.real_type = dtype_to_scalar_dtype(dtype) - def _dtype_to_name(self, dtype) -> str: + def _dtype_to_name(self, dtype: L.DataType) -> str: """Convert dtype to Python name.""" if dtype == L.DataType.SCALAR: return f"np.{self.scalar_type}" @@ -71,7 +71,7 @@ def format_statement_list(self, slist: L.StatementList) -> str: def format_comment(self, c: L.Comment) -> str: """Format a comment.""" - return "# " + c.comment + "\n" + return f"# {c.comment} \n" def format_array_decl(self, arr: L.ArrayDecl) -> str: """Format an array declaration.""" @@ -111,7 +111,7 @@ def format_nary_op(self, oper: L.NaryOp) -> str: # Apply parentheses for i in range(len(args)): if oper.args[i].precedence >= oper.precedence: - args[i] = "(" + args[i] + ")" + args[i] = f"({args[i]})" # Return combined string return f" {oper.op} ".join(args) @@ -196,11 +196,11 @@ def format_conditional(self, s: L.Conditional) -> str: # Apply parentheses if s.condition.precedence >= s.precedence: - c = "(" + c + ")" + c = f"({c})" if s.true.precedence >= s.precedence: - t = "(" + t + ")" + t = f"({t})" if s.false.precedence >= s.precedence: - f = "(" + f + ")" + f = f"({f})" # Return combined string return f"({t} if {c} else {f})" From a5df6ab6d8f2dfbd6ce6beb897033c8da177be08 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:03:18 +0100 Subject: [PATCH 091/106] Add docstrings and types to integrals --- ffcx/codegeneration/C/integrals.py | 14 ++++++++++++-- ffcx/codegeneration/numba/integrals.py | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index 2d7474219..c48db6012 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -24,8 +24,18 @@ def generator( ir: IntegralIR, domain: basix.CellType, options: dict[str, int | float | npt.DTypeLike] -): - """Generate C code for an integral.""" +) -> tuple[str, str]: + """Generate C code for an integral. + + Args: + ir: IR of the integral + domain: BASIx cell type + options: dict of kernel generation options + + Returns: + Tuple of declaration (header) and implementation (source) strings. + + """ logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") logger.info(f"--- name: {ir.expression.name}") diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 747daa16a..7d8df0702 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -17,14 +17,28 @@ from ffcx.codegeneration.integral_generator import IntegralGenerator from ffcx.codegeneration.numba import integrals_template as ufcx_integrals from ffcx.codegeneration.numba.implementation import Formatter +from ffcx.ir.representation import IntegralIR logger = logging.getLogger("ffcx") def generator( - ir, domain: basix.CellType, options: dict[str, int | float | npt.DTypeLike] + ir: IntegralIR, domain: basix.CellType, options: dict[str, int | float | npt.DTypeLike] ) -> tuple[str, str]: - """Generate numba code for an integral.""" + """Generate numba code for an integral. + + Args: + ir: IR of the integral + domain: BASIx cell type + options: dict of kernel generation options + + Returns: + Tuple of declaration (header) and implementation (source) strings. + + Note: + numba backend only provides a declaration. Implementation string will always be empty. + + """ logger.info("Generating code for integral:") logger.info(f"--- type: {ir.expression.integral_type}") logger.info(f"--- name: {ir.expression.name}") From 3faefbd14c36ce46055dc25d09491da83f3fef0f Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:06:56 +0100 Subject: [PATCH 092/106] String/comment formatting --- ffcx/codegeneration/numba/implementation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 657c3cb77..7de69ba06 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -49,17 +49,17 @@ def _dtype_to_name(self, dtype: L.DataType) -> str: def format_section(self, section: L.Section) -> str: """Format a section.""" # add new line before section - comments = "# ------------------------ \n" - comments += "# Section: " + section.name + "\n" - comments += "# Inputs: " + ", ".join(w.name for w in section.input) + "\n" - comments += "# Outputs: " + ", ".join(w.name for w in section.output) + "\n" + comments = self.format_comment("------------------------") + comments += self.format_comment(f"Section: {section.name}") + comments += self.format_comment(f"Inputs: {', '.join(w.name for w in section.input)}") + comments += self.format_comment(f"Outputs: {', '.join(w.name for w in section.output)}") declarations = "".join(self.format(s) for s in section.declarations) body = "" if len(section.statements) > 0: body = "".join(self.format(s) for s in section.statements) - body += "# ------------------------ \n" + body += self.format_comment("------------------------") return comments + declarations + body def format_statement_list(self, slist: L.StatementList) -> str: @@ -84,7 +84,7 @@ def format_array_decl(self, arr: L.ArrayDecl) -> str: elif arr.values.size == 1: return f"{symbol} = np.full({arr.sizes}, {arr.values[0]}, dtype={typename})\n" av = build_initializer_lists(arr.values) - av = "np.array(" + av + f", dtype={typename})" + av = f"np.array({av}, dtype={typename})" return f"{symbol} = {av}\n" def format_array_access(self, arr: L.ArrayAccess) -> str: From d66293111b1097dc5acd2a4e76d71b64c456cab5 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:08:49 +0100 Subject: [PATCH 093/106] mypy --- ffcx/codegeneration/numba/integrals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 7d8df0702..08da0f24d 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -66,7 +66,7 @@ def generator( body = "\n".join([" " + line for line in body.split("\n")]) # Generate generic FFCx code snippets and add specific parts - d = {} + d: dict[str, str] = {} d["factory_name"] = factory_name @@ -98,7 +98,7 @@ def generator( d["tabulate_tensor"] = header + body d["needs_facet_permutations"] = "True" if ir.expression.needs_facet_permutations else "False" d["coordinate_element_hash"] = ir.expression.coordinate_element_hash - d["domain"] = int(domain) + d["domain"] = str(int(domain)) assert ir.expression.coordinate_element_hash is not None implementation = ufcx_integrals.factory.format_map(d) From 3ccd079310a992e0a05738c20e779f53d5ece320 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:13:43 +0100 Subject: [PATCH 094/106] Raise bessel --- ffcx/codegeneration/numba/implementation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 7de69ba06..cfdf71863 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -12,7 +12,7 @@ from ffcx.codegeneration.utils import dtype_to_scalar_dtype -def build_initializer_lists(values): +def build_initializer_lists(values: npt.NDArray) -> str: """Build list of values.""" arr = "[" if len(values.shape) == 1: @@ -224,7 +224,8 @@ def format_mathfunction(self, f: L.MathFunction) -> str: function = function_map.get(f.function, f.function) args = [self.format(arg) for arg in f.args] if "bessel" in function: - return "0" + # TODO: fix with https://github.com/numba/numba/issues/2312. + raise NotImplementedError("Bessel function not supported.") if function == "erf": return f"math.erf({args[0]})" argstr = ", ".join(args) From eefeb6cecdd8d037129cf496904af5236e6326fa Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:17:46 +0100 Subject: [PATCH 095/106] Introduce _format_comment_str --- ffcx/codegeneration/numba/implementation.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index cfdf71863..e8671a796 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -49,17 +49,17 @@ def _dtype_to_name(self, dtype: L.DataType) -> str: def format_section(self, section: L.Section) -> str: """Format a section.""" # add new line before section - comments = self.format_comment("------------------------") - comments += self.format_comment(f"Section: {section.name}") - comments += self.format_comment(f"Inputs: {', '.join(w.name for w in section.input)}") - comments += self.format_comment(f"Outputs: {', '.join(w.name for w in section.output)}") + comments = self._format_comment_str("------------------------") + comments += self._format_comment_str(f"Section: {section.name}") + comments += self._format_comment_str(f"Inputs: {', '.join(w.name for w in section.input)}") + comments += self._format_comment_str(f"Outputs: {', '.join(w.name for w in section.output)}") declarations = "".join(self.format(s) for s in section.declarations) body = "" if len(section.statements) > 0: body = "".join(self.format(s) for s in section.statements) - body += self.format_comment("------------------------") + body += self._format_comment_str("------------------------") return comments + declarations + body def format_statement_list(self, slist: L.StatementList) -> str: @@ -69,9 +69,13 @@ def format_statement_list(self, slist: L.StatementList) -> str: output += self.format(s) return output + def _format_comment_str(self, comment: str) -> str: + """Format str to comment string.""" + return f"# {comment} \n" + def format_comment(self, c: L.Comment) -> str: """Format a comment.""" - return f"# {c.comment} \n" + return self._format_comment_str(c.comment) def format_array_decl(self, arr: L.ArrayDecl) -> str: """Format an array declaration.""" From fe3b1deaf3b3315b285b80679594b9326f2e9a12 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:46:44 +0100 Subject: [PATCH 096/106] docstring --- ffcx/codegeneration/numba/implementation.py | 4 +++- ffcx/compiler.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index e8671a796..38a480b62 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -52,7 +52,9 @@ def format_section(self, section: L.Section) -> str: comments = self._format_comment_str("------------------------") comments += self._format_comment_str(f"Section: {section.name}") comments += self._format_comment_str(f"Inputs: {', '.join(w.name for w in section.input)}") - comments += self._format_comment_str(f"Outputs: {', '.join(w.name for w in section.output)}") + comments += self._format_comment_str( + f"Outputs: {', '.join(w.name for w in section.output)}" + ) declarations = "".join(self.format(s) for s in section.declarations) body = "" diff --git a/ffcx/compiler.py b/ffcx/compiler.py index b6952a637..9fb6d928a 100644 --- a/ffcx/compiler.py +++ b/ffcx/compiler.py @@ -93,12 +93,15 @@ def compile_ufl_objects( """Generate UFC code for a given UFL objects. Args: - ufl_objects: Objects to be compiled. Accepts elements, forms, - integrals or coordinate mappings. - object_names: Map from object Python id to object name - prefix: Prefix - options: Options - visualise: Toggle visualisation + ufl_objects: Objects to be compiled. Accepts elements, forms, + integrals or coordinate mappings. + object_names: Map from object Python id to object name + prefix: Prefix + options: Options + visualise: Toggle visualisation + + Returns: tuple of declaration, implementation and tuple of file suffixes. + """ _object_names = object_names if object_names is not None else {} _prefix = prefix if prefix is not None else "" From 547628856ce85cfd0457178c283990bb6131312c Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:58:58 +0100 Subject: [PATCH 097/106] Docstring --- ffcx/codegeneration/numba/file.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ffcx/codegeneration/numba/file.py b/ffcx/codegeneration/numba/file.py index 7eaaee051..b4a08472e 100644 --- a/ffcx/codegeneration/numba/file.py +++ b/ffcx/codegeneration/numba/file.py @@ -25,7 +25,15 @@ def generator( options: dict[str, int | float | npt.DTypeLike], ) -> tuple[tuple[str, str], tuple[str, str]]: - """Generate UFC code for file output.""" + """Generate UFC code for file output. + + Args: + options: Dict of options specified the kenerl generation, these will be documented in the + generated file. + + Returns: tuple of file start- and end sections, each for declaration and implementation. + + """ logger.info("Generating code for file") # Attributes From ec82f17b9b63babed11308dc3807c5f56789557b Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:30:26 +0100 Subject: [PATCH 098/106] We do need bessel :) --- ffcx/codegeneration/numba/implementation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/implementation.py b/ffcx/codegeneration/numba/implementation.py index 38a480b62..1afd38486 100644 --- a/ffcx/codegeneration/numba/implementation.py +++ b/ffcx/codegeneration/numba/implementation.py @@ -229,9 +229,10 @@ def format_mathfunction(self, f: L.MathFunction) -> str: } function = function_map.get(f.function, f.function) args = [self.format(arg) for arg in f.args] - if "bessel" in function: - # TODO: fix with https://github.com/numba/numba/issues/2312. - raise NotImplementedError("Bessel function not supported.") + if "bessel_y" in function: + return "scipy.special.yn" + if "bessel_j" in function: + return "scipy.special.jn" if function == "erf": return f"math.erf({args[0]})" argstr = ", ".join(args) From 2921d329905b984e5a0ec247005183d04ad3cbf2 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:40:39 +0100 Subject: [PATCH 099/106] Remove API breaking chane with suffixes --- ffcx/codegeneration/C/__init__.py | 2 -- ffcx/codegeneration/codegeneration.py | 2 +- ffcx/codegeneration/numba/__init__.py | 2 -- ffcx/compiler.py | 4 ++-- ffcx/main.py | 9 ++++++++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ffcx/codegeneration/C/__init__.py b/ffcx/codegeneration/C/__init__.py index 030b8ca62..0cc7d4a8b 100644 --- a/ffcx/codegeneration/C/__init__.py +++ b/ffcx/codegeneration/C/__init__.py @@ -2,6 +2,4 @@ from ffcx.codegeneration.C import expressions, file, form, integrals -suffixes = (".h", ".c") - __all__ = ["expressions", "file", "form", "integrals", "suffixes"] diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 85c55b5a0..1d6eb8dc5 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -68,4 +68,4 @@ def generate_code( forms=code_forms, expressions=code_expressions, file_post=[code_file_post], - ), mod.suffixes + ) #, mod.suffixes diff --git a/ffcx/codegeneration/numba/__init__.py b/ffcx/codegeneration/numba/__init__.py index cc6364fb9..65b8c4f2a 100644 --- a/ffcx/codegeneration/numba/__init__.py +++ b/ffcx/codegeneration/numba/__init__.py @@ -2,6 +2,4 @@ from ffcx.codegeneration.numba import expressions, file, form, integrals -suffixes = (None, "_numba.py") - __all__ = ["expressions", "file", "form", "integrals", "suffixes"] diff --git a/ffcx/compiler.py b/ffcx/compiler.py index 9fb6d928a..798522c03 100644 --- a/ffcx/compiler.py +++ b/ffcx/compiler.py @@ -118,7 +118,7 @@ def compile_ufl_objects( # Stage 3: code generation cpu_time = time() - code, suffixes = generate_code(ir, options) + code = generate_code(ir, options) _print_timing(3, time() - cpu_time) # Stage 4: format code @@ -126,4 +126,4 @@ def compile_ufl_objects( code_h, code_c = format_code(code) _print_timing(4, time() - cpu_time) - return code_h, code_c, suffixes + return code_h, code_c #, suffixes diff --git a/ffcx/main.py b/ffcx/main.py index 44bd7238c..c99cac47b 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -75,7 +75,7 @@ def main(args: Sequence[str] | None = None) -> int: ufd = ufl.algorithms.load_ufl_file(filename) # Generate code - code_h, code_c, suffixes = compiler.compile_ufl_objects( + code_h, code_c = compiler.compile_ufl_objects( ufd.forms + ufd.expressions + ufd.elements, options=options, object_names=ufd.object_names, @@ -83,6 +83,13 @@ def main(args: Sequence[str] | None = None) -> int: visualise=xargs.visualise, ) + # File suffixes + # TODO: this needs to be moved into the language backends + if options["language"] == "C": + suffixes = (".h", ".c") + else: # numba: + suffixes = (None, "_numba.py") + # Write to file formatting.write_code(code_h, code_c, prefix, suffixes, xargs.output_directory) From d6a59962733d139e4213dee683a249b6ea1a04af Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:45:09 +0100 Subject: [PATCH 100/106] Tidy --- ffcx/codegeneration/codegeneration.py | 6 ++---- ffcx/compiler.py | 6 +++--- ffcx/formatting.py | 2 +- ffcx/main.py | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ffcx/codegeneration/codegeneration.py b/ffcx/codegeneration/codegeneration.py index 1d6eb8dc5..053fea70c 100644 --- a/ffcx/codegeneration/codegeneration.py +++ b/ffcx/codegeneration/codegeneration.py @@ -36,9 +36,7 @@ class CodeBlocks(typing.NamedTuple): file_post: list[tuple[str, str]] -def generate_code( - ir: DataIR, options: dict[str, int | float | npt.DTypeLike] -) -> tuple[CodeBlocks, tuple[str, str]]: +def generate_code(ir: DataIR, options: dict[str, int | float | npt.DTypeLike]) -> CodeBlocks: """Generate code blocks from intermediate representation.""" logger.info(79 * "*") logger.info("Compiler stage 3: Generating code") @@ -68,4 +66,4 @@ def generate_code( forms=code_forms, expressions=code_expressions, file_post=[code_file_post], - ) #, mod.suffixes + ) diff --git a/ffcx/compiler.py b/ffcx/compiler.py index 798522c03..61f94e61b 100644 --- a/ffcx/compiler.py +++ b/ffcx/compiler.py @@ -89,7 +89,7 @@ def compile_ufl_objects( object_names: dict[int, str] | None = None, prefix: str | None = None, visualise: bool = False, -) -> tuple[str, str, tuple[str, str]]: +) -> tuple[str, str]: """Generate UFC code for a given UFL objects. Args: @@ -100,7 +100,7 @@ def compile_ufl_objects( options: Options visualise: Toggle visualisation - Returns: tuple of declaration, implementation and tuple of file suffixes. + Returns: tuple of declaration and implementation. """ _object_names = object_names if object_names is not None else {} @@ -126,4 +126,4 @@ def compile_ufl_objects( code_h, code_c = format_code(code) _print_timing(4, time() - cpu_time) - return code_h, code_c #, suffixes + return code_h, code_c diff --git a/ffcx/formatting.py b/ffcx/formatting.py index 7400f1b05..5396c7417 100644 --- a/ffcx/formatting.py +++ b/ffcx/formatting.py @@ -39,7 +39,7 @@ def format_code(code: CodeBlocks) -> tuple[str, str]: def write_code( - code_h: str, code_c: str, prefix: str, suffixes: tuple[str, str], output_dir: str + code_h: str, code_c: str, prefix: str, suffixes: tuple[str | None, str | None], output_dir: str ) -> None: """Write code to files.""" if suffixes[0] is not None: diff --git a/ffcx/main.py b/ffcx/main.py index c99cac47b..0a72e2eaf 100644 --- a/ffcx/main.py +++ b/ffcx/main.py @@ -85,6 +85,7 @@ def main(args: Sequence[str] | None = None) -> int: # File suffixes # TODO: this needs to be moved into the language backends + suffixes: tuple[str | None, str | None] if options["language"] == "C": suffixes = (".h", ".c") else: # numba: From bdf95cc60f8e430100a1014f83fb1471e5f0bdd4 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:47:18 +0100 Subject: [PATCH 101/106] last one? --- ffcx/codegeneration/jit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcx/codegeneration/jit.py b/ffcx/codegeneration/jit.py index 1a04616cc..299e867d3 100644 --- a/ffcx/codegeneration/jit.py +++ b/ffcx/codegeneration/jit.py @@ -346,7 +346,7 @@ def _compile_objects( # JIT uses module_name as prefix, which is needed to make names of all struct/function # unique across modules - _, code_body, _ = ffcx.compiler.compile_ufl_objects( + _, code_body = ffcx.compiler.compile_ufl_objects( ufl_objects, prefix=module_name, options=options, visualise=visualise ) From 6061a88ba16ddf9ab4618e8a73b0d6095556ef99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20T=2E=20K=C3=BChner?= <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:37:22 +0100 Subject: [PATCH 102/106] Apply suggestions from code review Co-authored-by: Michal Habera --- demo/test_demos.py | 2 +- ffcx/codegeneration/C/integrals.py | 2 +- ffcx/codegeneration/numba/integrals.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/test_demos.py b/demo/test_demos.py index 88e93c116..c376b09a0 100644 --- a/demo/test_demos.py +++ b/demo/test_demos.py @@ -19,7 +19,7 @@ def skip_unsupported(test): - """Dcecorate test case to skip unsupported cases.""" + """Decorate test case to skip unsupported cases.""" def check_skip(file, scalar_type): """Skip scalar_type file combinations not supported.""" diff --git a/ffcx/codegeneration/C/integrals.py b/ffcx/codegeneration/C/integrals.py index c48db6012..279121e29 100644 --- a/ffcx/codegeneration/C/integrals.py +++ b/ffcx/codegeneration/C/integrals.py @@ -29,7 +29,7 @@ def generator( Args: ir: IR of the integral - domain: BASIx cell type + domain: basix cell type options: dict of kernel generation options Returns: diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index 08da0f24d..b726a0da7 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -29,7 +29,7 @@ def generator( Args: ir: IR of the integral - domain: BASIx cell type + domain: basix cell type options: dict of kernel generation options Returns: From 28a47a62fb17b7058a0f615ab0e1d9a8ea849b2d Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:41:32 +0100 Subject: [PATCH 103/106] Consistent factory_name with C --- ffcx/codegeneration/numba/integrals.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ffcx/codegeneration/numba/integrals.py b/ffcx/codegeneration/numba/integrals.py index b726a0da7..c72224c68 100644 --- a/ffcx/codegeneration/numba/integrals.py +++ b/ffcx/codegeneration/numba/integrals.py @@ -43,10 +43,7 @@ def generator( logger.info(f"--- type: {ir.expression.integral_type}") logger.info(f"--- name: {ir.expression.name}") - # Note: In contrast to the C implementation the actual code is type agnostic, thus not part of - # naming. This is only true for the numba code prior to compilation - the JITed versions - # are different. - factory_name = ir.expression.name + factory_name = f"{ir.expression.name}_{domain.name}" # Format declaration declaration = "" From 38edf48c6919404a929d44bd40327d074641f561 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:29:17 +0100 Subject: [PATCH 104/106] Template of form was unallgined with C, rewrite --- ffcx/codegeneration/numba/form.py | 133 +++++++++++++++++---- ffcx/codegeneration/numba/form_template.py | 26 +++- 2 files changed, 131 insertions(+), 28 deletions(-) diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index df9a43fd6..83de7ec2d 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2025 Anders Logg, Martin Sandve Alnæs and Chris Richardson +# Copyright (C) 2009-2025 Anders Logg, Martin Sandve Alnæs, Chris Richardson and Paul T. Kühner # # This file is part of FFCx.(https://www.fenicsproject.org) # @@ -11,6 +11,7 @@ import logging import string +import numpy as np from numpy import typing as npt from ffcx.codegeneration.numba import form_template @@ -24,40 +25,124 @@ def generator(ir, options: dict[str, int | float | npt.DTypeLike]) -> tuple[str, logger.info(f"--- rank: {ir.rank}") logger.info(f"--- name: {ir.name}") - d = {} + d: dict[str, int | str] = {} d["factory_name"] = ir.name d["name_from_uflfile"] = ir.name_from_uflfile d["signature"] = f'"{ir.signature}"' d["rank"] = ir.rank d["num_coefficients"] = ir.num_coefficients - d["num_constants"] = ir.num_constants - - orig_coeff = ", ".join(str(i) for i in ir.original_coefficient_positions) - d["original_coefficient_position"] = f"[{orig_coeff}]" - cnames = ir.coefficient_names - assert ir.num_coefficients == len(cnames) - names = ", ".join(f'"{name}"' for name in cnames) - d["coefficient_name_map"] = f"[{names}]" + if len(ir.original_coefficient_positions) > 0: + values = ", ".join(str(i) for i in ir.original_coefficient_positions) + + d["original_coefficient_position_init"] = ( + f"original_coefficient_position_{ir.name} = [{values}]" + ) + d["original_coefficient_positions"] = f"original_coefficient_position_{ir.name}" + else: + d["original_coefficient_position_init"] = "" + d["original_coefficient_positions"] = "None" + + if len(ir.coefficient_names) > 0: + values = ", ".join(f'"{name}"' for name in ir.coefficient_names) + d["coefficient_names_init"] = f"coefficient_names_{ir.name} = [{values}]" + d["coefficient_names"] = f"coefficient_names_{ir.name}" + else: + d["coefficient_names_init"] = "" + d["coefficient_names"] = "None" - cstnames = ir.constant_names - names = ", ".join(f'"{name}"' for name in cstnames) - d["constant_name_map"] = f"[{names}]" + d["num_constants"] = ir.num_constants + if ir.num_constants > 0: + d["constant_ranks_init"] = f"constant_ranks_{ir.name} = [{str(ir.constant_ranks)[1:-1]}]" + d["constant_ranks"] = f"constant_ranks_{ir.name}" + + shapes = [ + f"constant_shapes_{ir.name}_{i} = [{str(shape)[1:-1]}]" + for i, shape in enumerate(ir.constant_shapes) + if len(shape) > 0 + ] + names = [f"constant_shapes_{ir.name}_{i}" for i in range(ir.num_constants)] + shapes1 = f"constant_shapes_{ir.name} = [" + for rank, name in zip(ir.constant_ranks, names): + if rank > 0: + shapes1 += f"{name},\n" + else: + shapes1 += "None,\n" + shapes1 += "]" + shapes.append(shapes1) + + d["constant_shapes_init"] = "\n".join(shapes) + d["constant_shapes"] = f"constant_shapes_{ir.name}" + else: + d["constant_ranks_init"] = "" + d["constant_ranks"] = "None" + d["constant_shapes_init"] = "" + d["constant_shapes"] = "None" + + if len(ir.constant_names) > 0: + values = ", ".join(f'"{name}"' for name in ir.constant_names) + d["constant_names_init"] = f"constant_names_{ir.name} = [{values}]" + d["constant_names"] = f"constant_names_{ir.name}" + else: + d["constant_names_init"] = "" + d["constant_names"] = "None" + + if len(ir.finite_element_hashes) > 0: + d["finite_element_hashes"] = f"finite_element_hashes_{ir.name}" + values = ", ".join(f"{0 if el is None else el}" for el in ir.finite_element_hashes) + d["finite_element_hashes_init"] = f"finite_element_hashes_{ir.name} = [{values}]" + else: + d["finite_element_hashes"] = "NULL" + d["finite_element_hashes_init"] = "" integrals = [] integral_ids = [] integral_offsets = [0] - for itg_type in ("cell", "exterior_facet", "interior_facet"): - integrals += ir.integral_names[itg_type] - integral_ids += ir.subdomain_ids[itg_type] - integral_offsets.append(len(integrals)) - - integrals_str = ", ".join(itg for itg in integrals) - integral_ids_str = ", ".join(str(i) for i in integral_ids) - d["form_integrals"] = f"[{integrals_str}]" - d["form_integral_ids"] = f"[{integral_ids_str}]" - offsets = ", ".join(str(i) for i in integral_offsets) - d["form_integral_offsets"] = f"[{offsets}]" + integral_domains = [] + # Note: the order of this list is defined by the enum ufcx_integral_type in ufcx.h + for itg_type in ("cell", "exterior_facet", "interior_facet", "vertex", "ridge"): + unsorted_integrals = [] + unsorted_ids = [] + unsorted_domains = [] + for name, domains, id in zip( + ir.integral_names[itg_type], + ir.integral_domains[itg_type], + ir.subdomain_ids[itg_type], + ): + unsorted_integrals += [name] + unsorted_ids += [id] + unsorted_domains += [domains] + + id_sort = np.argsort(unsorted_ids) + integrals += [unsorted_integrals[i] for i in id_sort] + integral_ids += [unsorted_ids[i] for i in id_sort] + integral_domains += [unsorted_domains[i] for i in id_sort] + + integral_offsets.append(sum(len(d) for d in integral_domains)) + + if len(integrals) > 0: + values = ", ".join( + [ + f"{i}_{domain.name}" + for i, domains in zip(integrals, integral_domains) + for domain in domains + ] + ) + d["form_integrals_init"] = f"form_integrals_{ir.name} = [{values}]" + d["form_integrals"] = f"form_integrals_{ir.name}" + values = ", ".join( + f"{i}" for i, domains in zip(integral_ids, integral_domains) for _ in domains + ) + d["form_integral_ids_init"] = f"form_integral_ids_{ir.name} = [{values}]" + d["form_integral_ids"] = f"form_integral_ids_{ir.name}" + else: + d["form_integrals_init"] = "" + d["form_integrals"] = "NULL" + d["form_integral_ids_init"] = "" + d["form_integral_ids"] = "NULL" + + values = ", ".join(str(i) for i in integral_offsets) + d["form_integral_offsets_init"] = f"form_integral_offsets_{ir.name} = [{values}]" # Check that no keys are redundant or have been missed fields = [fname for _, fname, _, _ in string.Formatter().parse(form_template.factory) if fname] diff --git a/ffcx/codegeneration/numba/form_template.py b/ffcx/codegeneration/numba/form_template.py index e830f308b..1e4337f3f 100644 --- a/ffcx/codegeneration/numba/form_template.py +++ b/ffcx/codegeneration/numba/form_template.py @@ -8,21 +8,39 @@ factory = """ # Code for form {factory_name} +{original_coefficient_position_init} +{finite_element_hashes_init} +{form_integral_offsets_init} +{form_integrals_init} +{form_integral_ids_init} + +{coefficient_names_init} +{constant_names_init} +{constant_ranks_init} +{constant_shapes_init} + class {factory_name}(object): signature = {signature} rank = {rank} + num_coefficients = {num_coefficients} + original_coefficient_positions = {original_coefficient_positions} + coefficient_name_map = {coefficient_names} + num_constants = {num_constants} - original_coefficient_position = {original_coefficient_position} + constant_ranks = {constant_ranks} + constant_shapes = {constant_shapes} + constant_name_map = {constant_names} - coefficient_name_map = {coefficient_name_map} - constant_name_map = {constant_name_map} + finite_element_hashes = {finite_element_hashes} form_integrals = {form_integrals} form_integral_ids = {form_integral_ids} - form_integral_offsets = {form_integral_offsets} + form_integral_offsets = form_integral_offsets_{factory_name} + +# Alias name {name_from_uflfile} = {factory_name} # End of code for form {factory_name} From d46d49f37d1f128896d6722cf2f820cecb0ce013 Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:31:30 +0100 Subject: [PATCH 105/106] NULL -> None --- ffcx/codegeneration/numba/form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ffcx/codegeneration/numba/form.py b/ffcx/codegeneration/numba/form.py index 83de7ec2d..f04752352 100644 --- a/ffcx/codegeneration/numba/form.py +++ b/ffcx/codegeneration/numba/form.py @@ -92,7 +92,7 @@ def generator(ir, options: dict[str, int | float | npt.DTypeLike]) -> tuple[str, values = ", ".join(f"{0 if el is None else el}" for el in ir.finite_element_hashes) d["finite_element_hashes_init"] = f"finite_element_hashes_{ir.name} = [{values}]" else: - d["finite_element_hashes"] = "NULL" + d["finite_element_hashes"] = "None" d["finite_element_hashes_init"] = "" integrals = [] @@ -137,9 +137,9 @@ def generator(ir, options: dict[str, int | float | npt.DTypeLike]) -> tuple[str, d["form_integral_ids"] = f"form_integral_ids_{ir.name}" else: d["form_integrals_init"] = "" - d["form_integrals"] = "NULL" + d["form_integrals"] = "None" d["form_integral_ids_init"] = "" - d["form_integral_ids"] = "NULL" + d["form_integral_ids"] = "None" values = ", ".join(str(i) for i in integral_offsets) d["form_integral_offsets_init"] = f"form_integral_offsets_{ir.name} = [{values}]" From af5078d247c75e714279f1ef31114e0cec204f8f Mon Sep 17 00:00:00 2001 From: schnellerhase <56360279+schnellerhase@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:44:14 +0100 Subject: [PATCH 106/106] Add warning to file template --- ffcx/codegeneration/numba/file_template.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ffcx/codegeneration/numba/file_template.py b/ffcx/codegeneration/numba/file_template.py index 8b5b67d7d..0dcbd8ce3 100644 --- a/ffcx/codegeneration/numba/file_template.py +++ b/ffcx/codegeneration/numba/file_template.py @@ -14,6 +14,9 @@ # This code conforms with the UFC specification version {ufcx_version} # and was automatically generated by FFCx version {ffcx_version}. # +# WARNING: The numba backend is not production ready. Sub-optimal performance compared to the +# C-backend is to be expected. +# # This code was generated with the following options: # {options}