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);