Source code for firedrake.utils

# Some generic python utilities not really specific to our work.
import collections.abc
import warnings
import functools
from typing import Callable, Self, Hashable
from decorator import decorator
from pyop2.datatypes import ScalarType, as_cstr
from pyop2.datatypes import RealType     # noqa: F401
from pyop2.datatypes import IntType      # noqa: F401
from pyop2.datatypes import as_ctypes    # noqa: F401
from pyop2.mpi import MPI
from petsc4py import PETSc
from functools import cache
from firedrake.exceptions import UnrecognisedDeviceError
import petsctools


# MPI key value for storing a per communicator universal identifier
FIREDRAKE_UID = MPI.Comm.Create_keyval()

RealType_c = as_cstr(RealType)
ScalarType_c = as_cstr(ScalarType)
IntType_c = as_cstr(IntType)

complex_mode = (petsctools.get_petscvariables()["PETSC_SCALAR"].lower() == "complex")


[docs] @cache def get_device_type() -> str | None: r"""Get PETSc device type. Attempt to initialise a GPU and return the type of GPU identified by PETSc. Returns ------- str | None The PETSc device type, or `None` if no device is found. """ try: dev = PETSc.Device.create() except PETSc.Error: # Could not initialise device - not a failure condition as this could # be a GPU-enabled PETSc installation running on a CPU-only host. return None dev_type = dev.getDeviceType() dev.destroy() return dev_type
[docs] @cache def device_matrix_type(*, warn: bool = True) -> str | None: r"""Get device matrix type Attempt to initialise a GPU device and return the PETSc mat_type compatible with that device, or None if no device is detected. Typical Usage Example: mat_type = device_matrix_type(pc.comm.rank == 0) Parameters ---------- warn Emit a warning containing the reason a device mat_type has not been returned. Defaults to True. Raises ------ UnrecognisedDeviceError Raised when PETSc initialises a GPU device that Firedrake does not understand Returns ------- str | None The PETSc mat_type compatible with the GPU device detected on this system or None """ _device_mat_type_map = {"HOST": None, "CUDA": "aijcusparse", "HIP": "aijhipsparse"} dev_type = get_device_type() if dev_type is None: if warn: warnings.warn( "This installation of Firedrake is GPU-enabled, but no GPU device has been detected" ) return None if dev_type not in _device_mat_type_map: raise UnrecognisedDeviceError( f"Unknown device type: {dev_type} initialised by PETSc. Firedrake " f"currently understands {', '.join([k for k in _device_mat_type_map if k != 'HOST'])}" "devices" ) if warn: if dev_type == "HOST": warnings.warn( "This installation of Firedrake is not GPU-enabled, to enable GPU functionality " "PETSc will need to be rebuilt with some GPU capability appropriate for this system " "(e.g. '--with-cuda=1')." ) return _device_mat_type_map[dev_type]
def _new_uid(comm): uid = comm.Get_attr(FIREDRAKE_UID) if uid is None: uid = 0 comm.Set_attr(FIREDRAKE_UID, uid + 1) return uid def _init(): """Cause :func:`pyop2.init` to be called in case the user has not done it for themselves. The result of this is that the user need only call :func:`pyop2.init` if she wants to set a non-default option, for example to switch the debug or log level.""" from pyop2 import op2 from firedrake.parameters import parameters if not op2.initialised(): op2.init(**parameters["pyop2_options"])
[docs] def unique(iterable): """ Return tuple of unique items in iterable, items must be hashable """ # Use dict to preserve order and compare by hash unique_dict = {} for item in iterable: unique_dict[item] = None return tuple(unique_dict.keys())
[docs] def unique_name(name, nameset): """Return name if name is not in nameset, or a deterministic uniquified name if name is in nameset. The new name is inserted into nameset to prevent further name clashes.""" if name not in nameset: nameset.add(name) return name idx = 0 while True: newname = "%s_%d" % (name, idx) if newname in nameset: idx += 1 else: nameset.add(name) return newname
[docs] def known_pyop2_safe(f): """Decorator to mark a function as being PyOP2 type-safe. This switches the current PyOP2 type checking mode to the value given by the parameter "type_check_safe_par_loops", and restores it after the function completes.""" from firedrake.parameters import parameters def wrapper(f, *args, **kwargs): opts = parameters["pyop2_options"] check = opts["type_check"] safe = parameters["type_check_safe_par_loops"] if check == safe: return f(*args, **kwargs) opts["type_check"] = safe try: return f(*args, **kwargs) finally: opts["type_check"] = check return decorator(wrapper, f)
[docs] def tuplify(item): """Convert an object into a hashable equivalent. This is particularly useful for caching dictionaries of parameters such as `form_compiler_parameters` from :func:`firedrake.assemble.assemble`. :arg item: The object to attempt to 'tuplify'. :returns: The object interpreted as a tuple. For hashable objects this is simply a 1-tuple containing `item`. For dictionaries the function is called recursively on the values of the dict. For example, `{"a": 5, "b": 8}` returns `(("a", (5,)), ("b", (8,)))`. """ if isinstance(item, collections.abc.Hashable): return (item,) if not isinstance(item, dict): raise ValueError(f"tuplify does not know how to handle objects of type {type(item)}") return tuple((k, tuplify(item[k])) for k in sorted(item))
[docs] def split_by(condition, items): """Split an iterable in two according to some condition. :arg condition: Callable applied to each item in ``items``, returning ``True`` or ``False``. :arg items: Iterable to split apart. :returns: A 2-tuple of the form ``(yess, nos)``, where ``yess`` is a tuple containing the entries of ``items`` where ``condition`` is ``True`` and ``nos`` is a tuple of those where ``condition`` is ``False``. """ result = [], [] for item in items: if condition(item): result[0].append(item) else: result[1].append(item) return tuple(result[0]), tuple(result[1])
[docs] def assert_empty(iterator): """Check that an iterator has been fully consumed. Raises ------ AssertionError If the provided iterator is not empty. Notes ----- This function should only be used for assertions (where the program is immediately terminated on failure). If the iterator is not empty then the latest value is discarded. """ try: next(iterator) raise AssertionError("Iterator is not empty") except StopIteration: pass
# NOTE: Python 3.13 has warnings.deprecated which does exactly this
[docs] def deprecated(prefer=None, internal=False): """Decorator that emits a warning saying that the function is deprecated.""" def decorator(fn): def wrapper(*args, **kwargs): msg = f"{fn.__qualname__} is deprecated and will be removed" if prefer: msg += f", please use {prefer} instead" warning_type = DeprecationWarning if internal else FutureWarning warnings.warn(msg, warning_type) return fn(*args, **kwargs) return wrapper return decorator
[docs] def check_netgen_installed() -> None: """Check that netgen and ngsPETSc are available. If they are not an import error is raised. """ try: import netgen # noqa: F401 import ngsPETSc # noqa: F401 except ImportError: raise ImportError( "Unable to import netgen and ngsPETSc. Please ensure that they " "are installed and available to Firedrake (see " "https://www.firedrakeproject.org/install.html#netgen)." )
[docs] def cached_property_until(key: Callable[[Self], Hashable]): """Decorator for a property that is cached until some value changes. For example, the ``expensive_property`` below will be cached until ``self.value`` changes, and will be recomputed with the new ``self.value`` and cached when accessed again. .. code-block:: python class MyClass: def __init__(self, value): self.value = value @cached_property_until(lambda self: self.value) def expensive_property(self): # Some expensive computation that depends on self.value ... """ def decorator(func): @property @functools.wraps(func) def wrapper(self): cache_attribute = f"_{func.__name__}_cache" current_value = key(self) cached_value = getattr(self, cache_attribute, None) if cached_value is None or cached_value[0] != current_value: result = func(self) setattr(self, cache_attribute, (current_value, result)) return result return cached_value[1] return wrapper return decorator