Skip to content

Commit 7b306cd

Browse files
authored
[WIP] Draft of generalized RustBCA python wrapper(s) (#324)
* Draft of generalized RustBCA python wrapper(s) * Added tests of rustbca_py to test_different_options.py * Implemented better python error handling * Add different interaction potentials to scattering_integrals() * used macros to streamline both lib.rs geometry types and main.rs geometry types * Attempt to fix module imports across lib, main
1 parent 1dd8209 commit 7b306cd

9 files changed

Lines changed: 301 additions & 80 deletions

File tree

‎Cargo.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ rcpr = { git = "https://github.com/drobnyjt/rcpr", optional = true}
2929
ndarray = {version = "0.17.2", features = ["serde"], optional = true}
3030
parry3d-f64 = {optional = true, version="0.2.0"}
3131
pyo3 = {version = "0.29.0", optional=true}
32+
pythonize = {version = "0.29.0", optional=true}
3233

3334
[dev-dependencies]
3435
float-cmp = "0.10.0"
@@ -45,5 +46,5 @@ cpr_rootfinder = ["rcpr"]
4546
distributions = ["ndarray"]
4647
no_list_output = []
4748
parry3d = ["parry3d-f64"]
48-
python = ["pyo3"]
49+
python = ["pyo3", "pythonize"]
4950
extended_max_z = []

‎examples/make_input_file_and_run.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@
255255
'geometry_input': geometry_0D
256256
}
257257

258+
input_data['options']['name'] = 'rustbca_input_file'
259+
rustbca_py(input_data, mode)
260+
s = np.genfromtxt('rustbca_input_filesputtered.output', delimiter=',')
261+
262+
arrays = rustbca_local_py(input_data, mode)
263+
sputtered = arrays['sputtered']
264+
265+
np.testing.assert_approx_equal(s[0, 2], np.array(arrays['energy'])[sputtered][0])
266+
267+
input_data['options']['name'] = 'input_file'
268+
258269
# Attempt to cleanup line endings
259270
input_string = dumps(input_data).replace('\r', '')
260271
with open('examples/input_file.toml', 'w') as input_file:
@@ -265,6 +276,9 @@
265276

266277
# Read output files - ensure arrays are at least 2D for indexing
267278
sputtered = np.atleast_2d(np.genfromtxt('input_filesputtered.output', delimiter=','))
279+
280+
np.testing.assert_approx_equal(sputtered[0, 2], s[0, 2])
281+
268282
reflected = np.atleast_2d(np.genfromtxt('input_filereflected.output', delimiter=','))
269283
implanted = np.atleast_2d(np.genfromtxt('input_filedeposited.output', delimiter=','))
270284

‎examples/test_different_options.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
a TOML file using tomlkit.
5252
5353
It runs the input file with cargo run --release and reads the output files.
54+
55+
It also runs with rustbca_py and ensures the output files are identical.
5456
'''
5557

5658
def run_test(
@@ -275,6 +277,19 @@ def run_test(
275277
reflected = np.atleast_2d(np.genfromtxt(f'input_file_{index}reflected.output', delimiter=','))
276278
implanted = np.atleast_2d(np.genfromtxt(f'input_file_{index}deposited.output', delimiter=','))
277279

280+
input_data['options']['name'] = f'python_{index}'
281+
if run_sim:
282+
rustbca_py(input_data, geometry_mode=mode)
283+
284+
# Read output files - ensure arrays are at least 2D for indexing
285+
sputtered_py = np.atleast_2d(np.genfromtxt(f'python_{index}sputtered.output', delimiter=','))
286+
reflected_py = np.atleast_2d(np.genfromtxt(f'python_{index}reflected.output', delimiter=','))
287+
implanted_py = np.atleast_2d(np.genfromtxt(f'python_{index}deposited.output', delimiter=','))
288+
289+
np.testing.assert_allclose(sputtered, sputtered_py)
290+
np.testing.assert_allclose(reflected, reflected_py)
291+
np.testing.assert_allclose(implanted, implanted_py)
292+
278293
return sputtered, reflected, implanted
279294

280295
num_bins = 75

‎examples/test_scattering_integrals.py‎

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from formulas import *
1111

1212
energies = np.logspace(0, 4, 4)
13-
impact_parameters = np.logspace(-3, 3, 100)
13+
impact_parameters = np.logspace(-3, 2, 100)
1414

1515
ion = helium
1616
target = boron
@@ -24,23 +24,28 @@
2424

2525
linestyles = ['-', '--', ':', '-.']
2626

27-
for linestyle, energy in zip(linestyles, energies):
28-
gm = np.zeros(100)
29-
gl = np.zeros(100)
30-
mw = np.zeros(100)
31-
magic = np.zeros(100)
32-
for index, p in enumerate(impact_parameters):
33-
gm[index], gl[index], mw[index], magic[index] = scattering_integrals(Za, Zb, Ma, Mb, energy, p)
34-
plt.semilogx(impact_parameters, gm, label=f'Gauss-Mehler, E={np.round(energy/1000)} keV', linestyle=linestyle)
35-
plt.semilogx(impact_parameters, gl, label=f'Gauss-Legendre, E={np.round(energy/1000)} keV', linestyle=linestyle)
36-
plt.semilogx(impact_parameters, mw, label=f'Mendenhall-Weller, E={np.round(energy/1000)} keV', linestyle=linestyle)
37-
plt.semilogx(impact_parameters, magic, label=f'MAGIC, E={np.round(energy/1000)} keV', linestyle=linestyle)
38-
plt.gca().set_prop_cycle(None)
39-
40-
np.testing.assert_allclose(gm, gl, atol=5e-3) # 0.5% seems reasonable? max is ~0.3%
41-
np.testing.assert_allclose(gm, mw, atol=5e-3)
42-
np.testing.assert_allclose(mw, gl, atol=5e-3)
43-
plt.legend()
44-
plt.xlabel('p [A]')
45-
plt.ylabel('theta [rad]')
27+
for potential in ["KR_C", "MOLIERE", "ZBL"]:
28+
plt.figure()
29+
plt.title(f'Scattering Angles for {potential}')
30+
for linestyle, energy in zip(linestyles, energies):
31+
gm = np.zeros(100)
32+
gl = np.zeros(100)
33+
mw = np.zeros(100)
34+
magic = np.zeros(100)
35+
for index, p in enumerate(impact_parameters):
36+
gm[index], gl[index], mw[index], magic[index] = scattering_integrals(Za, Zb, Ma, Mb, energy, p, interaction_potential=potential)
37+
38+
plt.semilogx(impact_parameters, gm, label=f'Gauss-Mehler, E={np.round(energy/1000, 3)} keV', linestyle=linestyle)
39+
plt.semilogx(impact_parameters, gl, label=f'Gauss-Legendre, E={np.round(energy/1000, 3)} keV', linestyle=linestyle)
40+
plt.semilogx(impact_parameters, mw, label=f'Mendenhall-Weller, E={np.round(energy/1000, 3)} keV', linestyle=linestyle)
41+
plt.semilogx(impact_parameters, magic, label=f'MAGIC, E={np.round(energy/1000, 3)} keV', linestyle=linestyle)
42+
plt.gca().set_prop_cycle(None)
43+
44+
np.testing.assert_allclose(gm, gl, atol=5e-3) # 0.5% seems reasonable? max is ~0.3%
45+
np.testing.assert_allclose(gm, mw, atol=5e-3)
46+
np.testing.assert_allclose(mw, gl, atol=5e-3)
47+
plt.legend()
48+
plt.xlabel('p [A]')
49+
plt.ylabel('theta [rad]')
50+
4651
if show_plots: plt.show()

‎src/input.rs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,7 @@ impl Options {
381381
}
382382
}
383383

384-
pub fn input<T: Geometry>(input_file: String) -> (Vec<particle::ParticleInput>, material::Material<T>, Options, OutputUnits)
385-
where <T as Geometry>::InputFileFormat: Deserialize<'static> + 'static {
386-
384+
pub fn read_input_file<T: Geometry>(input_file: String) -> <T as Geometry>::InputFileFormat {
387385
//Read input file, convert to string, and open with toml
388386
let mut input_toml = String::new();
389387
let mut file = OpenOptions::new()
@@ -394,7 +392,19 @@ where <T as Geometry>::InputFileFormat: Deserialize<'static> + 'static {
394392
.unwrap_or_else(|_| panic!("Input errror: could not open input file {}.", &input_file));
395393
file.read_to_string(&mut input_toml).context("Could not convert TOML file to string.").unwrap();
396394

397-
let input: <T as Geometry>::InputFileFormat = InputFile::new(&input_toml);
395+
InputFile::new(&input_toml)
396+
}
397+
398+
pub fn input<T: Geometry>(input_file: String) -> (Vec<particle::ParticleInput>, material::Material<T>, Options, OutputUnits)
399+
where <T as Geometry>::InputFileFormat: Deserialize<'static> + 'static {
400+
401+
let input: <T as Geometry>::InputFileFormat = read_input_file::<T>(input_file);
402+
403+
process_input_file(input)
404+
405+
}
406+
407+
pub fn process_input_file<T: Geometry>(input: <T as Geometry>::InputFileFormat) -> (Vec<particle::ParticleInput>, material::Material<T>, Options, OutputUnits) {
398408

399409
//Unpack toml information into structs
400410
let options = (*input.get_options()).clone();

‎src/lib.rs‎

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ use std::alloc::{dealloc, Layout};
99
use std::mem::align_of;
1010

1111
//Parallelization - currently only used in python library functions
12+
//#[cfg(feature = "python")]
13+
//use rayon::ThreadPoolBuilder;
1214
#[cfg(feature = "python")]
13-
use rayon::prelude::*;
14-
#[cfg(feature = "python")]
15-
use rayon::*;
15+
use rayon::iter::{IndexedParallelIterator, ParallelExtend, IntoParallelIterator, ParallelIterator};
1616

1717
//Error handling crate
1818
use anyhow::{Result, Context, anyhow};
@@ -53,6 +53,10 @@ use std::f64::consts::SQRT_2;
5353
use pyo3::prelude::*;
5454
#[cfg(feature = "python")]
5555
use pyo3::types::*;
56+
#[cfg(feature = "python")]
57+
use pythonize::*;
58+
#[cfg(feature = "python")]
59+
use pyo3::exceptions::{PyValueError, PyRuntimeError};
5660

5761
//Load internal modules
5862
pub mod material;
@@ -68,9 +72,7 @@ pub mod consts;
6872
pub mod structs;
6973
pub mod sphere;
7074
pub mod math;
71-
72-
#[cfg(feature = "parry3d")]
73-
pub mod parry;
75+
pub mod physics;
7476

7577
pub use crate::enums::*;
7678
pub use crate::consts::*;
@@ -81,6 +83,10 @@ pub use crate::geometry::{Geometry, GeometryElement, Mesh0D, Mesh1D, Mesh2D};
8183
pub use crate::sphere::{Sphere, SphereInput, InputSphere};
8284
pub use crate::math::*;
8385
pub use crate::material::*;
86+
pub use crate::physics::*;
87+
88+
#[cfg(feature = "parry3d")]
89+
pub mod parry;
8490

8591
#[cfg(feature = "parry3d")]
8692
pub use crate::parry::{ParryBall, ParryBallInput, InputParryBall, ParryTriMesh, ParryTriMeshInput, InputParryTriMesh};
@@ -139,6 +145,12 @@ mod libRustBCA {
139145

140146
#[pymodule_export]
141147
use super::scattering_integrals;
148+
149+
#[pymodule_export]
150+
use super::rustbca_py;
151+
152+
#[pymodule_export]
153+
use super::rustbca_local_py;
142154
}
143155

144156
#[derive(Debug)]
@@ -2131,7 +2143,6 @@ pub fn compound_reflection_coefficient<'py>(ion: &Bound<'py, PyDict>, targets: V
21312143

21322144
let mut residue = residue.lock().unwrap();
21332145
*residue = *residue + residue_part;
2134-
21352146
}
21362147
}
21372148
});
@@ -2161,20 +2172,118 @@ fn moller_knuth_two_sum(a: f64, b: f64) -> (f64, f64) {
21612172
let r = delta_a + delta_b;
21622173
(s, r)
21632174
}
2175+
21642176
#[cfg(feature = "python")]
21652177
#[pyfunction]
2166-
#[pyo3(signature = (Za, Zb, Ma, Mb, E0, p, n_gl_points=100))]
2167-
fn scattering_integrals(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, p: f64, n_gl_points: usize) -> (f64, f64, f64, f64) {
2178+
#[pyo3(signature = (Za, Zb, Ma, Mb, E0, p, n_gl_points=100, interaction_potential="KR_C"))]
2179+
fn scattering_integrals(Za: f64, Zb: f64, Ma: f64, Mb: f64, E0: f64, p: f64, n_gl_points: usize, interaction_potential: &str) -> PyResult<(f64, f64, f64, f64)> {
21682180
let E0 = E0*EV;
21692181
let p = p*ANGSTROM;
21702182

2171-
let x0_newton = bca::newton_rootfinder(Za, Zb, Ma, Mb, E0, p, InteractionPotential::KR_C, 1000, 1E-12).unwrap();
2183+
let potential = match interaction_potential {
2184+
"KR_C" => InteractionPotential::KR_C,
2185+
"LENZ_JENSEN" => InteractionPotential::LENZ_JENSEN,
2186+
"MOLIERE" => InteractionPotential::MOLIERE,
2187+
"ZBL" => InteractionPotential::ZBL,
2188+
_ => return Err(PyValueError::new_err(format!("Unimplemented interaction potential {}; try 'KR_C'", interaction_potential)))
2189+
};
2190+
2191+
let x0_newton = bca::newton_rootfinder(Za, Zb, Ma, Mb, E0, p, potential, 1000, 1E-12).map_err(
2192+
|error| PyRuntimeError::new_err(format!("Rootfinder failed to find distance of closest approach; check input values."))
2193+
)?;
21722194

21732195
//Compute center of mass deflection angle with each algorithm
2174-
let theta_gm = bca::gauss_mehler(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C, n_gl_points);
2175-
let theta_gl = bca::gauss_legendre(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C);
2176-
let theta_mw = bca::mendenhall_weller(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C);
2177-
let theta_magic = bca::magic(Za, Zb, Ma, Mb, E0, p, x0_newton, InteractionPotential::KR_C);
2196+
let theta_gm = bca::gauss_mehler(Za, Zb, Ma, Mb, E0, p, x0_newton, potential, n_gl_points);
2197+
let theta_gl = bca::gauss_legendre(Za, Zb, Ma, Mb, E0, p, x0_newton, potential);
2198+
let theta_mw = bca::mendenhall_weller(Za, Zb, Ma, Mb, E0, p, x0_newton, potential);
2199+
let theta_magic = bca::magic(Za, Zb, Ma, Mb, E0, p, x0_newton, potential);
2200+
2201+
Ok((theta_gm, theta_gl, theta_mw, theta_magic))
2202+
}
2203+
#[cfg(feature = "python")]
2204+
macro_rules! geometry_typed_loops {
2205+
($geometry_type:ty, $input:expr, $python:expr) => {
2206+
{
2207+
let input: <$geometry_type as geometry::Geometry>::InputFileFormat = depythonize(&$input).unwrap();
2208+
let (particle_input_array, material, options, output_units) = input::process_input_file(input);
2209+
let pool = rayon::ThreadPoolBuilder::new().num_threads(options.num_threads).build().unwrap();
2210+
pool.install( ||
2211+
physics::physics_loop::<$geometry_type>(particle_input_array, material, options, output_units)
2212+
);
2213+
Ok(())
2214+
}
2215+
}
2216+
}
2217+
2218+
#[cfg(feature = "python")]
2219+
#[pyfunction]
2220+
#[pyo3(signature=(input, geometry_mode="1D"))]
2221+
fn rustbca_py<'py>(python: Python<'py>, input: &Bound<'py, PyDict>, geometry_mode: &str) -> PyResult<()> {
2222+
match geometry_mode {
2223+
"0D" => geometry_typed_loops!(Mesh0D, input, python),
2224+
"1D" => geometry_typed_loops!(Mesh1D, input, python),
2225+
"2D" => geometry_typed_loops!(Mesh2D, input, python),
2226+
"HOMOGENEOUS2D" => geometry_typed_loops!(Mesh2D, input, python),
2227+
"SPHERE" => geometry_typed_loops!(Sphere, input, python),
2228+
#[cfg(feature="parry3d")]
2229+
"BALL" => geometry_typed_loops!(ParryBall, input, python),
2230+
#[cfg(feature="parry3d")]
2231+
"TRIMESH" => geometry_typed_loops!(ParryTriMesh, input, python),
2232+
_ => Err(PyValueError::new_err(format!("Input Error: Unimplemented geometry mode {}; try '1D'", geometry_mode)))
2233+
}
2234+
}
21782235

2179-
(theta_gm, theta_gl, theta_mw, theta_magic)
2236+
/*
2237+
Notes on macros - this is the first I have written, so I'm taking notes here as I go.
2238+
macro_rules! makes a macro - here, the macro is called geometry_types_silent_loops
2239+
macros pattern match an argument and replace it with anything you want
2240+
I want it to take a tuple of a string (e.g., "1D") and a type (e.g., Mesh1D)
2241+
and plop those into corresponding match arms.
2242+
The first line tells the macro to expect an argument with that pattern.
2243+
arguments are $<name>:<designator>. Designators:
2244+
block
2245+
expr is used for expressions
2246+
ident is used for variable/function names
2247+
item
2248+
literal is used for literal constants
2249+
pat (pattern)
2250+
path
2251+
stmt (statement)
2252+
tt (token tree)
2253+
ty (type)
2254+
vis (visibility qualifier)
2255+
*/
2256+
#[cfg(feature = "python")]
2257+
macro_rules! geometry_typed_silent_loops {
2258+
($geometry_type:ty, $input:expr, $python:expr) => {
2259+
{
2260+
let input: <$geometry_type as geometry::Geometry>::InputFileFormat = depythonize(&$input).unwrap();
2261+
let (particle_input_array, material, options, output_units) = input::process_input_file(input);
2262+
let pool = rayon::ThreadPoolBuilder::new().num_threads(options.num_threads).build().unwrap();
2263+
let finished_particles = pool.install( ||
2264+
physics::silent_physics_loop::<$geometry_type>(particle_input_array, material, options, output_units.clone())
2265+
);
2266+
let finished_particles_container = physics::process_finished_particles_to_arrays(finished_particles, output_units);
2267+
Ok(pythonize($python, &finished_particles_container)?)
2268+
}
2269+
}
2270+
}
2271+
2272+
#[cfg(feature = "python")]
2273+
#[pyfunction]
2274+
#[pyo3(signature=(input, geometry_mode="1D"))]
2275+
fn rustbca_local_py<'py>(python: Python<'py>, input: &Bound<'py, PyDict>, geometry_mode: &str) -> PyResult<Bound<'py, PyAny>> {
2276+
2277+
match geometry_mode {
2278+
"0D" => geometry_typed_silent_loops!(Mesh0D, input, python),
2279+
"1D" => geometry_typed_silent_loops!(Mesh1D, input, python),
2280+
"2D" => geometry_typed_silent_loops!(Mesh2D, input, python),
2281+
"HOMOGENEOUS2D" => geometry_typed_silent_loops!(Mesh2D, input, python),
2282+
"SPHERE" => geometry_typed_silent_loops!(Sphere, input, python),
2283+
#[cfg(feature="parry3d")]
2284+
"BALL" => geometry_typed_silent_loops!(ParryBall, input, python),
2285+
#[cfg(feature="parry3d")]
2286+
"TRIMESH" => geometry_typed_silent_loops!(ParryTriMesh, input, python),
2287+
_ => Err(PyValueError::new_err(format!("Input Error: Unimplemented geometry mode {}; try '1D'", geometry_mode)))
2288+
}
21802289
}

0 commit comments

Comments
 (0)