Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add DEPENDENCIES parameter for extra packages per implementation
Some elements need an additional package on top of the implementation's
own package (e.g. Ferrite.jl elements that live in FerriteInterpolations.jl).
Add a special parameter DEPENDENCIES to the implementation strings in
.def files, e.g.

  ferrite:
    triangle: Bernstein DEGREES=1:4 DEPENDENCIES=FerriteInterpolations

Like DEGREES and DEGREEMAP it is stripped before the implementation sees
the parameters. The dependencies are collected per element
(Element.implementation_dependencies) and passed to the implementation's
install and example_import methods, so that the install instructions and
the import line of the example snippets include them, and a note is
added to the element's implementation notes. install_implementations.py
loads the elements and installs the dependencies of every implementation,
and Ferrite's verification loads them before creating the interpolation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
  • Loading branch information
fredrikekre and claude committed Oct 8, 2026
commit fa2dcef0ecb1b4a0d3519711a94048d02f7a1b3a
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ There are a few special parameters (written in ALL CAPS) that can be used here:
</thead>
<tr><td>`DEGREES`</td><td>A list of degrees for which this element is defined in this implementation, eg `1,2,4:8` denotes that degrees 1 and 2 and 4 to 8 (including 4 but not including 8) are supported. Note that if `DEGREEMAP` is set, the degrees used here should correspond to the DefElement convention, not the degree after the degree map is applied.</td></tr>
<tr><td>`DEGREEMAP`</td><td>A map to apply to the degree used on DefElement to obtain the degree used by this library. The value here will be parsed using Sympy, with the variable `k` equal to the degree (using DefElement's convention).</td></tr>
<tr><td>`DEPENDENCIES`</td><td>A comma-separated list of additional packages that are needed to use this element in this implementation, eg `FerriteInterpolations`. These are collected over all the implementation strings of the element and passed to the implementation's `install` and `example_import` methods, so that the example snippets and install instructions on the element's page include them.</td></tr>
</table>

### Testing your contribution
Expand Down
2 changes: 1 addition & 1 deletion defelement/code_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def generate_examples(e: Element, impl: type[Implementation], language: str) ->
f"<div id='{jscodename}_{language}_eg' style='display:none'>"
)

info += languages[language].install(impl)
info += languages[language].install(impl, e.implementation_dependencies(impl.id))

info += (
"This element can then be created with the following lines of "
Expand Down
39 changes: 39 additions & 0 deletions defelement/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,9 @@ def get_implementation_string(
raise DegreeNotImplemented()
del params["DEGREES"]

# Dependencies are collected per element by implementation_dependencies
params.pop("DEPENDENCIES", None)

input_deg = degree
if "DEGREEMAP" in params:
if degree is not None:
Expand All @@ -810,6 +813,35 @@ def get_implementation_string(

return out, input_deg, params

def implementation_dependencies(self, lib: str) -> list[str]:
"""Get the additional packages needed to use this element in a library.

These are set using the DEPENDENCIES parameter of the implementation strings, and are
collected over all reference cells and variants.

Args:
lib: The library

Returns:
Sorted list of additional packages
"""
if "implementations" not in self.data or lib not in self.data["implementations"]:
return []

deps: set[str] = set()

def collect(data: str | dict[str, typing.Any]):
if isinstance(data, str):
for d in re.findall(r"DEPENDENCIES=(\S+)", data):
deps.update(d.split(","))
else:
for key, value in data.items():
if key != "display":
collect(value)

collect(self.data["implementations"][lib])
return sorted(deps)

def list_of_implementation_strings(
self,
lib: str,
Expand Down Expand Up @@ -951,6 +983,13 @@ def implementation_notes(self, lib: str) -> list[str]:
"This implementation uses an alternative value of "
"degree for this element"
)
dependencies = self.implementation_dependencies(lib)
if len(dependencies) > 0:
notes.append(
"This implementation requires the additional "
+ ("package " if len(dependencies) == 1 else "packages ")
+ ", ".join(dependencies)
)
return notes

def implementation_references(self, lib: str) -> list[dict[str, str]]:
Expand Down
2 changes: 1 addition & 1 deletion defelement/implementations/basix.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import basix"

Expand Down
4 changes: 2 additions & 2 deletions defelement/implementations/basix_ufl.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import basix\nimport basix.ufl"

Expand Down Expand Up @@ -144,7 +144,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
raise NotImplementedError()

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import basix\nimport basix.ufl\nimport numpy as np"

Expand Down
2 changes: 1 addition & 1 deletion defelement/implementations/bempp_cl.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return f'"{string}"'

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import bempp_cl.api\ngrid = bempp_cl.api.shapes.regular_sphere(1)"

Expand Down
19 changes: 12 additions & 7 deletions defelement/implementations/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def pypi_name(package_name: str, dependencies: list[str] | None = None):
Returns:
A wrapped class
"""
prerequisites = dependencies

def pypi_name(Cls):
class Wrapped(Cls):
Expand All @@ -37,14 +38,14 @@ def version(cls) -> str:
]["version"]

@classmethod
def install(cls, language: str) -> str | None:
def install(cls, language: str, dependencies: list[str] | None = None) -> str | None:
"""Get the command(s) to install this implementation."""
if language == "python":
return (
""
if dependencies is None
else "pip install " + " ".join(dependencies) + "\n"
) + f"pip install {package_name}"
if prerequisites is None
else "pip install " + " ".join(prerequisites) + "\n"
) + " ".join(["pip install", package_name, *(dependencies or [])])
return None

return Wrapped
Expand Down Expand Up @@ -74,13 +75,15 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
raise NotImplementedError()

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get code for imports to include at start of examples snippet.

This function must be implemented.

Args:
language: Programming language
dependencies: Additional packages that the element needs, as set using the
`DEPENDENCIES` parameter in the .def file

Returns:
Python code for imports
Expand Down Expand Up @@ -134,11 +137,13 @@ def version(cls) -> str:
raise NotImplementedError()

@classmethod
def install(cls, language: str) -> str | None:
def install(cls, language: str, dependencies: list[str] | None = None) -> str | None:
"""Get the command(s) to install this implementation.

Args:
language: Programming language
dependencies: Additional packages to install, as set using the `DEPENDENCIES`
parameter in the .def file

Returns:
Version number
Expand Down Expand Up @@ -240,7 +245,7 @@ def examples(cls, element: Element, language: str) -> str:
"""
if language not in cls.languages:
raise ValueError(f"Implementation cannot create snippets for language: {language}")
code = cls.example_import(language)
code = cls.example_import(language, element.implementation_dependencies(cls.id))
assert cls.id is not None
for eg in element.examples:
reference, defelement_degree, variant, kwargs = parse_example(eg)
Expand Down
13 changes: 9 additions & 4 deletions defelement/implementations/ferrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
if language != "julia":
raise ValueError(f"Unsupported language: {language}")
return "using Ferrite"
return "using " + ", ".join(["Ferrite", *(dependencies or [])])

@classmethod
def single_example(
Expand All @@ -72,10 +72,13 @@ def single_example(
return out

@classmethod
def install(cls, language: str) -> str | None:
def install(cls, language: str, dependencies: list[str] | None = None) -> str | None:
"""Get the command(s) to install this implementation."""
if language == "julia":
return "julia -e 'using Pkg; Pkg.add(\"Ferrite\")'"
if not dependencies:
return "julia -e 'using Pkg; Pkg.add(\"Ferrite\")'"
packages = ", ".join(f'"{p}"' for p in ["Ferrite", *dependencies])
return f"julia -e 'using Pkg; Pkg.add([{packages}])'"
return None

@classmethod
Expand Down Expand Up @@ -106,6 +109,8 @@ def verify(
from juliacall import Main as jl

jl.seval("using Ferrite")
for dependency in element.implementation_dependencies(cls.id):
jl.seval(f"using {dependency}")

shape = reference_shapes[reference]
vdim = None
Expand Down
2 changes: 1 addition & 1 deletion defelement/implementations/fiat.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import FIAT"

Expand Down
4 changes: 2 additions & 2 deletions defelement/implementations/ndelement.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def version(cls) -> str:
return requests.get("https://pypi.org/pypi/ndelement/json").json()["info"]["version"]

@classmethod
def install(cls, language: str) -> str | None:
def install(cls, language: str, dependencies: list[str] | None = None) -> str | None:
"""Get the command(s) to install this implementation."""
if language == "python":
return "pip install ndelement"
Expand All @@ -46,7 +46,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
if language == "python":
return (
Expand Down
4 changes: 2 additions & 2 deletions defelement/implementations/simplefem.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:

# <example>
@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import simplefem"

Expand All @@ -47,7 +47,7 @@ def single_example(

# <install>
@classmethod
def install(cls, language: str) -> str | None:
def install(cls, language: str, dependencies: list[str] | None = None) -> str | None:
"""Get the command(s) to install this implementation."""
if language == "python":
return "pip3 install git+https://github.com/DefElement/simplefem"
Expand Down
2 changes: 1 addition & 1 deletion defelement/implementations/symfem.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def format(cls, string: str, params: dict[str, typing.Any]) -> str:
return out

@classmethod
def example_import(cls, language: str) -> str:
def example_import(cls, language: str, dependencies: list[str] | None = None) -> str:
"""Get imports to include at start of example."""
return "import symfem"

Expand Down
9 changes: 5 additions & 4 deletions defelement/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,19 @@ def highlight(cls, code: str) -> str:
raise NotImplementedError()

@classmethod
def install(cls, impl: type[Implementation]) -> str:
def install(cls, impl: type[Implementation], dependencies: list[str] | None = None) -> str:
"""Generate installation information for a language.

Args:
impl: The implementation
dependencies: Additional packages that the element needs

Returns:
Installation info
"""
info = f"Before running this example, you must install <a href='{impl.url}'>{impl.name}</a>"

cmd = impl.install(cls.id)
cmd = impl.install(cls.id, dependencies)

if cmd is None:
info += ". "
Expand Down Expand Up @@ -92,14 +93,14 @@ def highlight(cls, code: str) -> str:
return rust_highlight(code)

@classmethod
def install(cls, impl: type[Implementation]) -> str:
def install(cls, impl: type[Implementation], dependencies: list[str] | None = None) -> str:
"""Generate installation information for a language."""
info = (
f"To running this snippet, you must add <a href='{impl.url}'>{impl.name}</a>"
" to your Cargo.toml file"
)

cmd = impl.install("rust")
cmd = impl.install("rust", dependencies)

if cmd is None:
info += ". "
Expand Down
14 changes: 12 additions & 2 deletions install_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import argparse
import os

from defelement import implementations
from defelement import implementations, settings
from defelement.element import Categoriser

parser = argparse.ArgumentParser(description="Install implementations")
parser.add_argument("--install-type", default="all", help="Type of installation.")
Expand All @@ -12,10 +13,19 @@
if args.install_type not in ["all", "verification"]:
raise RuntimeError(f"Unknown install type: {args.install_type}")

# Load elements from .def files to find the additional packages each implementation needs
categoriser = Categoriser()
categoriser.load_references(os.path.join(settings.data_path, "references"))
categoriser.load_families(os.path.join(settings.data_path, "families"))
categoriser.load_folder(settings.element_path)

for i in implementations.implementations.values():
if args.install_type == "all" or (args.install_type == "verification" and i.verification):
lang = i.languages[0] if len(i.languages) == 1 else i.install_language
assert lang is not None
cmd = i.install(lang)
dependencies = sorted(
{d for e in categoriser.elements for d in e.implementation_dependencies(i.id)}
)
cmd = i.install(lang, dependencies)
assert cmd is not None
assert os.system(cmd) == 0
7 changes: 5 additions & 2 deletions pages/adding-an-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ the element).
To generate example code that will be displayed on an element's page, DefElement will use the
class methods `example_import` and `single_example`. The class method `example_import` returns the
import statements to include at the start of the example code: this method takes
the programming language as its only input.
the programming language as an input, as well as an optional list of additional packages
(`dependencies`) that the element needs. These are set using the `DEPENDENCIES` special parameter
in the .def file; implementations that do not use this parameter can ignore this input.
The class method `single_example`
returns Python code that will create the element: the inputs to this method are
the string included in the .def file (`name`);
Expand All @@ -98,7 +100,8 @@ input that simplefem uses.

### `install`
The class method `install` can be implemented. It takes the programming language as an input
and returns the bash commands that can be used to install the implementation.
and returns the bash commands that can be used to install the implementation. Like `example_import`,
it also takes an optional list of additional packages (`dependencies`) that should be installed too.
For simplefem, this is implemented as follows:

{{snippet::defelement/implementations/simplefem.py::install}}
Expand Down
Loading