rusty_green_kernel/
lib.rs

1//! Welcome to `rusty-green-kernel`. This crate contains routines for the evaluation of sums of the form
2//!
3//! $$f(\mathbf{x}_i) = \sum_jg(\mathbf{x}_i, \mathbf{y}_j)c_j$$
4//!
5//! and the corresponding gradients
6//!
7//! $$\nabla_{\mathbf{x}}f(\mathbf{x}_i) = \sum_j\nabla\_{\mathbf{x}}g(\mathbf{x}_i, \mathbf{y}_j)c_j.$$
8//|
9//! The following kernels are supported.
10//!
11//! * The Laplace kernel: $g(\mathbf{x}, \mathbf{y}) = \frac{1}{4\pi|\mathbf{x} - \mathbf{y}|}$.
12//! * The Helmholtz kernel: $g(\mathbf{x}, \mathbf{y}) = \frac{e^{ik|\mathbf{x} - \mathbf{y}|}}{4\pi|\mathbf{x} - \mathbf{y}|}$
13//! * The modified Helmholtz kernel: $g(\mathbf{x}, \mathbf{y}) = \frac{e^{-\omega|\mathbf{x} - \mathbf{y}|}}{4\pi|\mathbf{x} - \mathbf{y}|}$
14//!
15//! Within the library the $\mathbf{x}_i$ are named `targets` and the $\mathbf{y}_j$ are named `sources`. We use
16//! the convention that $g(\mathbf{x}_i, \mathbf{y}_j) := 0$, whenever $\mathbf{x}_i = \mathbf{y}_j$.
17//!
18//! The library provides a Rust API, C API, and Python API.
19//!
20//! ### Installation hints
21//!
22//! The performance of the library strongly depends on being compiled with the right parameters for the underlying CPU. To make sure
23//! that all native CPU features are activated use
24//!
25//! ```
26//! export RUSTFLAGS="-C target-cpu=native"
27//! cargo build --release
28//! ```
29//!
30//! The activated compiler features can also be tested with `cargo rustc --release -- --print cfg`.
31//!
32//! To compile and install the Python module make sure that the wanted Python virtual environment is active.
33//! The installation is performed using `maturin`, which is available from Pypi and conda-forge.
34//!
35//! After compiling the library as described above use
36//!
37//! ``
38//! maturin develop --release -b cffi
39//! ``
40//!
41//! to compile and install the Python module. It is important that the `RUSTFLAGS` environment variable is set as stated above.
42//! The Python module is called `rusty_green_kernel`.
43//!
44//! ### Rust API
45//!
46//! The `sources` and `targets` are both arrays of type `ndarray<T>` with `T=f32` or `T=f64`. For `M` targets and `N` sources
47//! the `sources` are a `(3, N)` array and the `targets` are a `(3, M)` array.
48//!
49//! To evaluate the kernel matrix of all interactions between a vector of `sources` and a `vector` of targets for the Laplace kernel
50//! use
51//!
52//! ```kernel_matrix = f64::assemble_kernel(sources, targets, KernelType::Laplace, num_threads);```
53//! 
54//! The variable `num_threads` specifies how many CPU threads to use for the execution. For single precision
55//! evaluation replace `f64` by `f32`. For Helmholtz or modified Helmholtz problems use
56//! `KernelType::Helmholtz(wavenumber)` or `KernelType::ModifiedHelmholtz(omega)`. Note that for Helmholtz
57//! the type of the result is complex, and the corresponding routine would therefore by
58//! 
59//! ```kernel_matrix = c64::assemble_kernel(sources, targets, KernelType::Helmholtz(wavenumber), num_threads);```
60//!
61//! or the corresponding with `c32`.
62//! 
63//! To evaluate $f(\mathbf{x}_i) = \sum_jg(\mathbf{x}_i, \mathbf{y}_j)c_j$ we define the charges as `ndarray` of
64//! size `(ncharge_vecs, nsources)`, where `ncharge_vecs` is the number of charge vectors we want to evaluate and
65//! `nsources` is the number of sources. For Laplace and modified Helmholtz problems `charges` must be of type `f32`
66//! or `f64` and for Helmholtz problems it must be of type `c32` or `c64`.
67//!
68//! We can then evaluate the potential sum by
69//!
70//! ```potential_sum = f64::evaluate_kernel(sources, targets, charges, result, EvalMode::Value, num_threads)
71//! ```
72//!
73//! The result `potential_sum` is a real `ndarray` (for Laplace and modified Helmholtz) or a complex `ndarray` (for Helmholtz).
74//! It has the shape `(ncharge_vecs, ntargets, 1)`. For `EvalMode::Value` the function only computes the values $f(\mathbf{x}_i)$. For
75//! `EvalMode::ValueGrad` the array `potential_sum` is of shape `(ncharge_vecs, ntargets, 4)` and
76//! returns the function values and the three components of the gradient along the most-inner dimension. 
77//!
78//!
79//! ### C API
80//!
81//! The C API in [`c_api`] provides direct access to the functionality in a C compatible interface. All functions come in variants
82//! for `f32` and `f64` types. Details are explaineed in the documentation of the corresponding functions.
83//!
84//! ### Python API
85//!
86//! For details of the Python module see the Python documentation in the `rusty_green_kernel` module.
87//!
88
89//pub mod kernels;
90//pub mod evaluators;
91//pub mod c_api;
92
93//pub use evaluators::*;
94
95pub use ndarray::{Array2, Array3, ArrayView2, ArrayViewMut2, ArrayViewMut3, Axis};
96pub use ndarray_linalg::Scalar;
97pub use rayon;
98
99// Complex types
100pub use ndarray_linalg::c32;
101pub use ndarray_linalg::c64;
102
103pub mod c_api;
104pub(crate) mod helmholtz;
105pub(crate) mod laplace;
106pub(crate) mod modified_helmholtz;
107
108/// This enum defines the type of the kernel.
109pub enum KernelType {
110    /// The Laplace kernel defined as g(x, y) = 1 / (4 pi | x- y| )
111    Laplace,
112    /// The Helmholtz kernel defined as g(x, y) = exp( 1j * k * | x- y| ) / (4 pi | x- y| )
113    Helmholtz(c64),
114    /// The modified Helmholtz kernel defined as g(x, y) = exp( -omega * | x- y| ) / (4 * pi * | x- y |)
115    ModifiedHelmholtz(f64),
116}
117
118/// This enum provides the keywords scalar and vectorial
119
120pub enum DimensionType {
121    /// For scalar kernels
122    Scalar,
123
124    /// For vectorial kernels
125    Vectorial,
126}
127
128/// Return the dimension type (scalar or vectorial) for a kernel.
129pub fn kernel_dimension(kernel_type: &KernelType) -> DimensionType {
130    match kernel_type {
131        KernelType::Laplace => DimensionType::Scalar,
132        KernelType::Helmholtz(_) => DimensionType::Scalar,
133        KernelType::ModifiedHelmholtz(_) => DimensionType::Scalar,
134    }
135}
136
137/// This enum defines the Evaluation Mode for kernels.
138pub enum EvalMode {
139    /// Only evaluate Green's function values.
140    Value,
141    /// Evaluate values and derivatives.
142    ValueGrad,
143}
144
145/// Compute the number of scalar entries a single kernel evaluation requires.
146pub(crate) fn get_evaluation_dimension(kernel_type: &KernelType, eval_mode: &EvalMode) -> usize {
147    let dimension_type = kernel_dimension(&kernel_type);
148    match eval_mode {
149        EvalMode::Value => match dimension_type {
150            DimensionType::Scalar => 1,
151            DimensionType::Vectorial => 3,
152        },
153        EvalMode::ValueGrad => match dimension_type {
154            DimensionType::Scalar => 4,
155            DimensionType::Vectorial => {
156                panic!("Only EvalMode::Value allowed as evaluation mode for vectorial kernels.")
157            }
158        },
159    }
160}
161
162pub(crate) fn create_pool(num_threads: usize) -> rayon::ThreadPool {
163    rayon::ThreadPoolBuilder::new()
164        .num_threads(num_threads)
165        .build()
166        .unwrap()
167}
168
169pub trait EvaluateKernel: Scalar {
170    fn assemble_kernel(
171        sources: ArrayView2<<Self as Scalar>::Real>,
172        targets: ArrayView2<<Self as Scalar>::Real>,
173        kernel_type: KernelType,
174        num_threads: usize,
175    ) -> Array2<Self> {
176        let mut result = unsafe {
177            Array2::<Self>::uninit((targets.len_of(Axis(1)), sources.len_of(Axis(1)))).assume_init()
178        };
179
180        Self::assemble_kernel_in_place(
181            sources,
182            targets,
183            result.view_mut(),
184            kernel_type,
185            num_threads,
186        );
187        result
188    }
189
190    fn assemble_kernel_in_place(
191        sources: ArrayView2<<Self as Scalar>::Real>,
192        targets: ArrayView2<<Self as Scalar>::Real>,
193        result: ArrayViewMut2<Self>,
194        kernel_type: KernelType,
195        num_threads: usize,
196    );
197
198    fn evaluate_kernel(
199        sources: ArrayView2<<Self as Scalar>::Real>,
200        targets: ArrayView2<<Self as Scalar>::Real>,
201        charges: ArrayView2<Self>,
202        kernel_type: KernelType,
203        eval_mode: EvalMode,
204        num_threads: usize,
205    ) -> Array3<Self> {
206        let nvalues = get_evaluation_dimension(&kernel_type, &eval_mode);
207        // Use unsafe operation here as array will be filled with values during in place
208        // evaluation. So avoid initializing twice.
209
210        let mut result = unsafe {
211            Array3::<Self>::uninit((charges.len_of(Axis(0)), targets.len_of(Axis(1)), nvalues))
212                .assume_init()
213        };
214
215        Self::evaluate_kernel_in_place(
216            sources,
217            targets,
218            charges,
219            result.view_mut(),
220            kernel_type,
221            eval_mode,
222            num_threads,
223        );
224        result
225    }
226
227    fn evaluate_kernel_in_place(
228        sources: ArrayView2<<Self as Scalar>::Real>,
229        targets: ArrayView2<<Self as Scalar>::Real>,
230        charges: ArrayView2<Self>,
231        result: ArrayViewMut3<Self>,
232        kernel_type: KernelType,
233        eval_mode: EvalMode,
234        num_threads: usize,
235    );
236}
237
238macro_rules! evaluate_kernel_impl {
239    (f32) => {
240        evaluate_kernel_impl!(f32, f32);
241    };
242    (f64) => {
243        evaluate_kernel_impl!(f64, f64);
244    };
245    (c32) => {
246        evaluate_kernel_impl!(f32, c32);
247    };
248    (c64) => {
249        evaluate_kernel_impl!(f64, c64);
250    };
251
252    ($real:ty, $result:ty) => {
253        impl EvaluateKernel for $result {
254            fn assemble_kernel_in_place(
255                sources: ArrayView2<<Self as Scalar>::Real>,
256                targets: ArrayView2<<Self as Scalar>::Real>,
257                mut result: ArrayViewMut2<Self>,
258                kernel_type: KernelType,
259                num_threads: usize,
260            ) {
261                use crate::laplace::LaplaceEvaluator;
262                use crate::helmholtz::HelmholtzEvaluator;
263                use crate::modified_helmholtz::ModifiedHelmholtzEvaluator;
264                let dimension_type = kernel_dimension(&kernel_type);
265
266                let expected_shape = (targets.len_of(Axis(1)), sources.len_of(Axis(1)));
267                let actual_shape = (result.len_of(Axis(0)), result.len_of(Axis(1)));
268
269                assert!(
270                    expected_shape == actual_shape,
271                    "result array must have shape {:#?} but actual shape is {:#?}",
272                    expected_shape,
273                    actual_shape
274                );
275
276                match dimension_type {
277                    DimensionType::Vectorial => {
278                        panic!("Assembly of kernel matrices only allowed for scalar kernels.")
279                    }
280                    DimensionType::Scalar => (),
281                }
282
283                match kernel_type {
284                    KernelType::Laplace => <$result>::assemble_in_place_laplace(
285                        sources,
286                        targets,
287                        result.view_mut(),
288                        num_threads,
289                    ),
290                    KernelType::Helmholtz(wavenumber) => <$result>::assemble_in_place_helmholtz(
291                        sources,
292                        targets,
293                        result.view_mut(),
294                        wavenumber,
295                        num_threads,
296                    ),
297                    KernelType::ModifiedHelmholtz(omega) => <$result>::assemble_in_place_modified_helmholtz(
298                        sources,
299                        targets,
300                        result.view_mut(),
301                        omega,
302                        num_threads,
303                    ),
304                }
305            }
306
307            fn evaluate_kernel_in_place(
308                sources: ArrayView2<$real>,
309                targets: ArrayView2<$real>,
310                charges: ArrayView2<$result>,
311                result: ArrayViewMut3<$result>,
312                kernel_type: KernelType,
313                eval_mode: EvalMode,
314                num_threads: usize,
315            ) {
316                use crate::laplace::LaplaceEvaluator;
317                use crate::helmholtz::HelmholtzEvaluator;
318                use crate::modified_helmholtz::ModifiedHelmholtzEvaluator;
319
320                let nvalues = get_evaluation_dimension(&kernel_type, &eval_mode);
321
322                assert!(
323                    sources.len_of(Axis(1)) == charges.len_of(Axis(1)),
324                    "Charges and sources must have same length."
325                );
326                let expected_shape = (charges.len_of(Axis(0)), targets.len_of(Axis(1)), nvalues);
327                let actual_shape = result.shape();
328                let actual_shape = (actual_shape[0], actual_shape[1], actual_shape[2]);
329                assert!(
330                    expected_shape == actual_shape,
331                    "Result has shape {:#?}, but expected shape is {:#?}",
332                    actual_shape,
333                    expected_shape
334                );
335
336                match kernel_type {
337                    KernelType::Laplace => <$result>::evaluate_in_place_laplace(
338                        sources,
339                        targets,
340                        charges,
341                        result,
342                        &eval_mode,
343                        num_threads,
344                    ),
345                    KernelType::Helmholtz(wavenumber) => <$result>::evaluate_in_place_helmholtz(
346                        sources,
347                        targets,
348                        charges,
349                        result,
350                        wavenumber,
351                        &eval_mode,
352                        num_threads,
353                    ),
354                    KernelType::ModifiedHelmholtz(omega) => <$result>::evaluate_in_place_modified_helmholtz(
355                        sources,
356                        targets,
357                        charges,
358                        result,
359                        omega,
360                        &eval_mode,
361                        num_threads,
362                    ),
363                }
364            }
365        }
366    };
367}
368
369evaluate_kernel_impl!(f32);
370evaluate_kernel_impl!(f64);
371evaluate_kernel_impl!(c32);
372evaluate_kernel_impl!(c64);