# Copyright 2026 Capytaine developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Abstract structure of a class used to compute the Green function"""
from abc import ABC, abstractmethod
import numpy as np
from capytaine.tools.array_backend import array_namespace
[docs]
class GreenFunctionEvaluationError(Exception):
pass
[docs]
class AbstractGreenFunction(ABC):
"""Abstract method to evaluate the Green function."""
floating_point_precision: str
# Array library (supporting the array API standard) and device of the
# matrices returned by `evaluate`. NumPy arrays on the CPU by default.
matrices_array_backend = array_namespace(np.empty(0))
matrices_device = "cpu"
# Settings of the Green function, as a dict of hashable values. It is used
# for the string representation, the hash and the export in the outputs.
exportable_settings: dict
# Default values of the settings that can be passed to the constructor
# (subset of `exportable_settings`). Only those are shown in `repr` and `str`.
_default_parameters: dict = {}
def __str__(self):
# Print only the non-default values.
to_be_printed = [f"{name}={value!r}" for name, value in self.exportable_settings.items()
if name in self._default_parameters and value != self._default_parameters[name]]
return f"{self.__class__.__name__}({', '.join(to_be_printed)})"
def __repr__(self):
# Same as __str__ except all values are printed even when they are the
# default value.
to_be_printed = [f"{name}={value!r}" for name, value in self.exportable_settings.items()
if name in self._default_parameters]
return f"{self.__class__.__name__}({', '.join(to_be_printed)})"
def _repr_pretty_(self, p, cycle):
p.text(self.__repr__())
def __hash__(self):
return hash(tuple(self.exportable_settings.items()))
def _get_colocation_points_and_normals(self, mesh1, mesh2, adjoint_double_layer):
try:
collocation_points = mesh1.faces_centers
nb_collocation_points = mesh1.nb_faces
if not adjoint_double_layer: # Computing the D matrix
early_dot_product_normals = mesh2.faces_normals
else: # Computing the K matrix
early_dot_product_normals = mesh1.faces_normals
return collocation_points, early_dot_product_normals
except AttributeError:
pass
if isinstance(mesh1, np.ndarray) and mesh1.ndim == 2 and mesh1.shape[1] == 3:
# This is used when computing potential or velocity at given points in postprocessing
collocation_points = mesh1
nb_collocation_points = mesh1.shape[0]
if not adjoint_double_layer: # Computing the D matrix
early_dot_product_normals = mesh2.faces_normals
else: # Computing the K matrix
early_dot_product_normals = np.zeros((nb_collocation_points, 3))
# Dummy argument since this method is meant to be used either
# - to compute potential, then only S is needed and early_dot_product_normals is irrelevant,
# - to compute velocity, then the adjoint full gradient is needed and early_dot_product is False and this value is unused.
# TODO: add an only_S argument and return an error here if (early_dot_product and not only_S)
return collocation_points, early_dot_product_normals
else:
raise ValueError(f"Unrecognized first input for {self.__class__.__name__}.evaluate:\n{mesh1}")
def _init_matrices(self, shape, early_dot_product):
# Helper for the Fortran-based implementations, which fill
# Fortran-ordered NumPy arrays in place. Green functions using another
# array library do not need to call it.
if self.floating_point_precision == "float32":
dtype = "complex64"
elif self.floating_point_precision == "float64":
dtype = "complex128"
else:
raise NotImplementedError(
f"Unsupported floating point precision: {self.floating_point_precision}"
)
S = np.zeros(shape, order="F", dtype=dtype)
K = np.zeros((1 if early_dot_product else 3, shape[0], shape[1]), order="F", dtype=dtype)
return S, K
[docs]
@abstractmethod
def evaluate(
self,
mesh1,
mesh2,
free_surface,
water_depth,
wavenumber,
adjoint_double_layer=True,
early_dot_product=True,
diagonal_term_in_double_layer=True,
):
"""Assemble the influence matrices between mesh1 and mesh2.
Parameters
----------
mesh1: MeshLike or array of shape (n, 3)
mesh of the receiving body (where the potential is measured), or
an array of points (only supported if only S is wanted or
early_dot_product is False).
mesh2: MeshLike
mesh of the source body (over which the source distribution is integrated)
free_surface: float
position of the free surface
water_depth: float
constant depth of water
wavenumber: float
wavenumber
adjoint_double_layer: bool, optional
compute double layer for direct method (D) or adjoint double layer
for indirect method (K) matrices (default: True)
early_dot_product: bool, optional
if True, K is the matrix of ∫∇G·n, if False it is the gradient ∫∇G
(default: True)
diagonal_term_in_double_layer: bool, optional
if True, add the I/2 term in the double layer operator (default: True)
Returns
-------
tuple of arrays
the matrices S and K, with shapes (n, m) for S and
(n, m) for K if early_dot_product is True or (3, n, m) otherwise,
where n is the number of receiving points of mesh1 and m the
number of faces of mesh2.
"""
pass