use numpy::Element as NumpyElement;
use numpy::PyArray1;
use numpy::borrow::{PyReadonlyArray1, PyReadonlyArray2, PyReadonlyArray3, PyReadwriteArray1};
use pyo3::create_exception;
use pyo3::exceptions;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use std::ffi::CString;
use std::fmt::Debug;
use crate::{math, mesh, physics};
create_exception!(cfsem, DimensionalityError, exceptions::PyValueError);
#[derive(Debug)]
#[allow(dead_code)]
enum PyInteropError {
DimensionalityError { msg: String },
ValueError { msg: String },
}
impl From<PyInteropError> for PyErr {
fn from(val: PyInteropError) -> Self {
match val {
PyInteropError::DimensionalityError { msg } => DimensionalityError::new_err(msg),
PyInteropError::ValueError { msg } => exceptions::PyValueError::new_err(msg),
}
}
}
fn split_xyz_array2(
name: &str,
arr: PyReadonlyArray2<f64>,
) -> PyResult<(Vec<f64>, Vec<f64>, Vec<f64>)> {
let arr = arr.as_array();
let shape = arr.shape();
if shape.len() != 2 || shape[1] != 3 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (n, 3)"),
}
.into());
}
let n = shape[0];
let mut x = Vec::with_capacity(n);
let mut y = Vec::with_capacity(n);
let mut z = Vec::with_capacity(n);
for i in 0..n {
x.push(arr[[i, 0]]);
y.push(arr[[i, 1]]);
z.push(arr[[i, 2]]);
}
Ok((x, y, z))
}
fn split_triangle_index_array2(
name: &str,
arr: PyReadonlyArray2<i64>,
) -> PyResult<(Vec<usize>, Vec<usize>, Vec<usize>)> {
let arr = arr.as_array();
let shape = arr.shape();
if shape.len() != 2 || shape[1] != 3 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (n, 3)"),
}
.into());
}
let n = shape[0];
let mut i0 = Vec::with_capacity(n);
let mut i1 = Vec::with_capacity(n);
let mut i2 = Vec::with_capacity(n);
for i in 0..n {
let idx0 = arr[[i, 0]];
let idx1 = arr[[i, 1]];
let idx2 = arr[[i, 2]];
if idx0 < 0 || idx1 < 0 || idx2 < 0 {
return Err(PyInteropError::ValueError {
msg: format!("{name} must contain nonnegative node indices"),
}
.into());
}
i0.push(idx0 as usize);
i1.push(idx1 as usize);
i2.push(idx2 as usize);
}
Ok((i0, i1, i2))
}
fn parse_triangle_quadrature(quad: &str) -> PyResult<physics::boundary_element::QuadratureKind> {
match quad {
"dunavant1" => Ok(physics::boundary_element::QuadratureKind::Dunavant1),
"dunavant2" => Ok(physics::boundary_element::QuadratureKind::Dunavant2),
"dunavant3" => Ok(physics::boundary_element::QuadratureKind::Dunavant3),
"dunavant4" => Ok(physics::boundary_element::QuadratureKind::Dunavant4),
"dunavant5" => Ok(physics::boundary_element::QuadratureKind::Dunavant5),
_ => Err(PyInteropError::ValueError {
msg: format!("Unsupported triangle quadrature rule: {quad}"),
}
.into()),
}
}
fn py_hierarchical_error(
context: &str,
error: physics::hierarchical::kernel::HierarchicalError,
) -> PyErr {
PyInteropError::ValueError {
msg: format!("{context} failed with {error:?}"),
}
.into()
}
#[pyclass(module = "cfsem")]
struct HierarchicalDiagnostics {
construction_time: f64,
evaluation_time: f64,
source_count: usize,
target_count: usize,
source_tree: Option<Py<PyAny>>,
accepted_levels: Option<Py<PyArray1<f64>>>,
}
#[pymethods]
impl HierarchicalDiagnostics {
#[getter]
fn construction_time(&self) -> f64 {
self.construction_time
}
#[getter]
fn evaluation_time(&self) -> f64 {
self.evaluation_time
}
#[getter]
fn source_count(&self) -> usize {
self.source_count
}
#[getter]
fn target_count(&self) -> usize {
self.target_count
}
#[getter]
fn source_tree(&self, py: Python<'_>) -> Option<Py<PyAny>> {
self.source_tree.as_ref().map(|value| value.clone_ref(py))
}
#[getter]
fn accepted_levels(&self, py: Python<'_>) -> Option<Py<PyArray1<f64>>> {
self.accepted_levels
.as_ref()
.map(|value| value.clone_ref(py))
}
}
#[pyclass(module = "cfsem")]
struct SolveResult {
field: Py<PyAny>,
diagnostics: Py<HierarchicalDiagnostics>,
}
#[pymethods]
impl SolveResult {
#[getter]
fn field(&self, py: Python<'_>) -> Py<PyAny> {
self.field.clone_ref(py)
}
#[getter]
fn diagnostics(&self, py: Python<'_>) -> Py<HierarchicalDiagnostics> {
self.diagnostics.clone_ref(py)
}
}
fn warn_hierarchical_reallocation(py: Python<'_>, name: &str, direction: &str) -> PyResult<()> {
let message = CString::new(format!(
"Non-contiguous or misaligned hierarchical {direction} array {name:?} detected; \
reallocating a contiguous temporary. Use numpy.ascontiguousarray on the Python \
side before calling this method to avoid this copy."
))
.map_err(|err| PyInteropError::ValueError {
msg: format!("failed to construct warning message: {err}"),
})?;
PyErr::warn(
py,
&py.get_type::<exceptions::PyUserWarning>(),
message.as_c_str(),
1,
)
}
enum FloatInput<'a, T: Copy> {
Borrowed(&'a [T]),
Owned(Vec<T>),
}
impl<'a, T: Copy> FloatInput<'a, T> {
#[inline]
fn as_slice(&self) -> &[T] {
match self {
Self::Borrowed(slice) => slice,
Self::Owned(values) => values.as_slice(),
}
}
#[inline]
fn len(&self) -> usize {
self.as_slice().len()
}
}
enum Matrix3Input<'a, T: Copy> {
Borrowed(&'a [T], usize),
Owned(Vec<T>, usize),
}
impl<'a, T: Copy> Matrix3Input<'a, T> {
#[inline]
fn as_slice(&self) -> &[T] {
match self {
Self::Borrowed(slice, _) => slice,
Self::Owned(values, _) => values.as_slice(),
}
}
#[inline]
fn nrows(&self) -> usize {
match self {
Self::Borrowed(_, nrows) | Self::Owned(_, nrows) => *nrows,
}
}
}
struct XyzInput<'a, T: Copy> {
x: FloatInput<'a, T>,
y: FloatInput<'a, T>,
z: FloatInput<'a, T>,
}
impl<'a, T: Copy> XyzInput<'a, T> {
#[inline]
fn len(&self) -> usize {
self.x.len()
}
#[inline]
fn as_tuple(&self) -> (&[T], &[T], &[T]) {
(self.x.as_slice(), self.y.as_slice(), self.z.as_slice())
}
}
fn read_float_input_array1<'py, T>(
py: Python<'_>,
arr: &'py PyReadonlyArray1<'py, T>,
name: &str,
) -> PyResult<FloatInput<'py, T>>
where
T: NumpyElement + Copy,
{
match arr.as_slice() {
Ok(slice) => Ok(FloatInput::Borrowed(slice)),
Err(_) => {
warn_hierarchical_reallocation(py, name, "input")?;
Ok(FloatInput::Owned(arr.as_array().iter().copied().collect()))
}
}
}
fn read_matrix3_input<'py, T>(
py: Python<'_>,
arr: &'py PyReadonlyArray2<'py, T>,
name: &str,
) -> PyResult<Matrix3Input<'py, T>>
where
T: NumpyElement + Copy,
{
let view = arr.as_array();
let shape = view.shape();
if shape.len() != 2 || shape[1] != 3 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (n, 3)"),
}
.into());
}
match arr.as_slice() {
Ok(slice) => Ok(Matrix3Input::Borrowed(slice, shape[0])),
Err(_) => {
warn_hierarchical_reallocation(py, name, "input")?;
Ok(Matrix3Input::Owned(
view.iter().copied().collect(),
shape[0],
))
}
}
}
fn read_xyz_tuple<'py, T>(
py: Python<'_>,
xyz: &'py (
PyReadonlyArray1<'py, T>,
PyReadonlyArray1<'py, T>,
PyReadonlyArray1<'py, T>,
),
name: &str,
) -> PyResult<XyzInput<'py, T>>
where
T: NumpyElement + Copy,
{
let x = read_float_input_array1(py, &xyz.0, &format!("{name}.0"))?;
let y = read_float_input_array1(py, &xyz.1, &format!("{name}.1"))?;
let z = read_float_input_array1(py, &xyz.2, &format!("{name}.2"))?;
if x.len() != y.len() || x.len() != z.len() {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} component arrays must have matching lengths"),
}
.into());
}
Ok(XyzInput { x, y, z })
}
fn read_output_arrays<'py, T>(
out: (
PyReadwriteArray1<'py, T>,
PyReadwriteArray1<'py, T>,
PyReadwriteArray1<'py, T>,
),
n: usize,
name: &str,
) -> PyResult<(
numpy::PyReadwriteArray1<'py, T>,
numpy::PyReadwriteArray1<'py, T>,
numpy::PyReadwriteArray1<'py, T>,
)>
where
T: NumpyElement,
{
if out.0.len()? != n || out.1.len()? != n || out.2.len()? != n {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} output arrays must all have length {n}"),
}
.into());
}
if out.0.as_slice().is_err() || out.1.as_slice().is_err() || out.2.as_slice().is_err() {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} output arrays must be contiguous and aligned"),
}
.into());
}
Ok(out)
}
fn output_arrays_to_py_tuple(
out: &(
PyReadwriteArray1<'_, f64>,
PyReadwriteArray1<'_, f64>,
PyReadwriteArray1<'_, f64>,
),
) -> (Py<PyAny>, Py<PyAny>, Py<PyAny>) {
(
<pyo3::Bound<'_, PyArray1<f64>> as Clone>::clone(&out.0)
.unbind()
.into(),
<pyo3::Bound<'_, PyArray1<f64>> as Clone>::clone(&out.1)
.unbind()
.into(),
<pyo3::Bound<'_, PyArray1<f64>> as Clone>::clone(&out.2)
.unbind()
.into(),
)
}
fn component_vecs_to_py_tuple(
py: Python<'_>,
values: (Vec<f64>, Vec<f64>, Vec<f64>),
) -> (Py<PyAny>, Py<PyAny>, Py<PyAny>) {
(
PyArray1::from_vec(py, values.0).unbind().into(),
PyArray1::from_vec(py, values.1).unbind().into(),
PyArray1::from_vec(py, values.2).unbind().into(),
)
}
type PyVec3Output<'py> = (
PyReadwriteArray1<'py, f64>,
PyReadwriteArray1<'py, f64>,
PyReadwriteArray1<'py, f64>,
);
type PyVec3Field = (Py<PyAny>, Py<PyAny>, Py<PyAny>);
fn evaluate_hierarchical_vec3<'py, D, F>(
py: Python<'_>,
out: Option<PyVec3Output<'py>>,
n: usize,
name: &str,
evaluate: F,
) -> PyResult<(PyVec3Field, D)>
where
F: FnOnce((&mut [f64], &mut [f64], &mut [f64])) -> PyResult<D>,
{
match out {
Some(out) => {
let mut out = read_output_arrays(out, n, name)?;
let result = {
let (outx, outy, outz) = output_slices_mut(&mut out, name)?;
evaluate((outx, outy, outz))?
};
Ok((output_arrays_to_py_tuple(&out), result))
}
None => {
let mut outx = vec![0.0; n];
let mut outy = vec![0.0; n];
let mut outz = vec![0.0; n];
let result = evaluate((&mut outx, &mut outy, &mut outz))?;
Ok((component_vecs_to_py_tuple(py, (outx, outy, outz)), result))
}
}
}
fn output_slices_mut<'a, 'py, T>(
out: &'a mut (
PyReadwriteArray1<'py, T>,
PyReadwriteArray1<'py, T>,
PyReadwriteArray1<'py, T>,
),
name: &str,
) -> PyResult<(&'a mut [T], &'a mut [T], &'a mut [T])>
where
T: NumpyElement,
{
let outx = out
.0
.as_slice_mut()
.map_err(|_| PyInteropError::DimensionalityError {
msg: format!("{name}.0 output array must be contiguous and aligned"),
})?;
let outy = out
.1
.as_slice_mut()
.map_err(|_| PyInteropError::DimensionalityError {
msg: format!("{name}.1 output array must be contiguous and aligned"),
})?;
let outz = out
.2
.as_slice_mut()
.map_err(|_| PyInteropError::DimensionalityError {
msg: format!("{name}.2 output array must be contiguous and aligned"),
})?;
Ok((outx, outy, outz))
}
fn source_tree_aabbs_to_py_tuple(
py: Python<'_>,
tree: &physics::hierarchical::tree::ClusterTree<f64>,
) -> (
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
) {
let mut min_x = Vec::with_capacity(tree.node_aabb.len());
let mut min_y = Vec::with_capacity(tree.node_aabb.len());
let mut min_z = Vec::with_capacity(tree.node_aabb.len());
let mut max_x = Vec::with_capacity(tree.node_aabb.len());
let mut max_y = Vec::with_capacity(tree.node_aabb.len());
let mut max_z = Vec::with_capacity(tree.node_aabb.len());
let levels = source_tree_node_levels(tree);
for i in 0..tree.node_aabb.len() {
let aabb = tree.node_aabb[i];
min_x.push(aabb.min[0]);
min_y.push(aabb.min[1]);
min_z.push(aabb.min[2]);
max_x.push(aabb.max[0]);
max_y.push(aabb.max[1]);
max_z.push(aabb.max[2]);
}
let mut left_child = Vec::with_capacity(tree.node_left_child.len());
let mut right_child = Vec::with_capacity(tree.node_right_child.len());
for i in 0..tree.node_left_child.len() {
left_child.push(u64::from(tree.node_left_child[i]));
right_child.push(u64::from(tree.node_right_child[i]));
}
(
PyArray1::from_vec(py, min_x).unbind(),
PyArray1::from_vec(py, min_y).unbind(),
PyArray1::from_vec(py, min_z).unbind(),
PyArray1::from_vec(py, max_x).unbind(),
PyArray1::from_vec(py, max_y).unbind(),
PyArray1::from_vec(py, max_z).unbind(),
PyArray1::from_vec(py, levels).unbind(),
PyArray1::from_vec(py, left_child).unbind(),
PyArray1::from_vec(py, right_child).unbind(),
)
}
fn source_tree_diagnostics_object(
py: Python<'_>,
tree: &physics::hierarchical::tree::ClusterTree<f64>,
) -> PyResult<Py<PyAny>> {
let (min_x, min_y, min_z, max_x, max_y, max_z, levels, left_child, right_child) =
source_tree_aabbs_to_py_tuple(py, tree);
let items: Vec<Py<PyAny>> = vec![
min_x.into(),
min_y.into(),
min_z.into(),
max_x.into(),
max_y.into(),
max_z.into(),
levels.into(),
left_child.into(),
right_child.into(),
];
Ok(PyTuple::new(py, items)?.unbind().into())
}
fn solve_result_from_field(
py: Python<'_>,
field: (Py<PyAny>, Py<PyAny>, Py<PyAny>),
construction_time: f64,
evaluation_time: f64,
source_count: usize,
target_count: usize,
source_tree: Option<Py<PyAny>>,
accepted_levels: Option<Py<PyArray1<f64>>>,
) -> PyResult<Py<SolveResult>> {
let field = PyTuple::new(py, [field.0, field.1, field.2])?
.unbind()
.into();
let diagnostics = Py::new(
py,
HierarchicalDiagnostics {
construction_time,
evaluation_time,
source_count,
target_count,
source_tree,
accepted_levels,
},
)?;
Py::new(py, SolveResult { field, diagnostics })
}
fn source_tree_node_levels(tree: &physics::hierarchical::tree::ClusterTree<f64>) -> Vec<f64> {
let mut levels = vec![0.0; tree.node_aabb.len()];
let mut active = Vec::new();
if !tree.node_aabb.is_empty() {
active.push((0_usize, 0_u32));
}
while let Some((node, level)) = active.pop() {
levels[node] = f64::from(level);
let left = tree.node_left_child[node];
if left != physics::hierarchical::tree::ClusterTreeView::<f64>::invalid_index() {
active.push((left as usize, level + 1));
}
let right = tree.node_right_child[node];
if right != physics::hierarchical::tree::ClusterTreeView::<f64>::invalid_index() {
active.push((right as usize, level + 1));
}
}
levels
}
fn parse_build_method(
construction_method: &str,
) -> PyResult<physics::hierarchical::tree::BuildMethod> {
match construction_method {
"longest_axis" | "longest-axis" => {
Ok(physics::hierarchical::tree::BuildMethod::LongestAxis)
}
"morton_lbvh" | "morton-lbvh" | "lbvh" => {
Ok(physics::hierarchical::tree::BuildMethod::MortonLbvh)
}
_ => Err(PyInteropError::ValueError {
msg: format!(
"Unsupported hierarchical construction method: {construction_method}. \
Expected 'longest_axis' or 'morton_lbvh'."
),
}
.into()),
}
}
fn accepted_levels_diagnostic<K, S, C, M>(
kernel: K,
source_tree: &physics::hierarchical::tree::ClusterTree<f64>,
sources: S,
targets: C,
moments: M,
theta: f64,
) -> PyResult<Vec<f64>>
where
K: physics::hierarchical::kernel::HierarchicalKernel<Scalar = f64, Output = [f64; 3]> + Sync,
S: physics::hierarchical::kernel::SourceCollection<K> + Copy,
M: physics::hierarchical::kernel::SourceMomentCollection<K> + Copy,
K::TargetGeometry: Copy,
C: physics::hierarchical::kernel::TargetCollection<K>,
{
let mut source_summaries =
physics::hierarchical::evaluator::SourceNodeSummaries::<K>::new(source_tree.as_view());
let mut err = physics::hierarchical::evaluator::update_summaries(
&kernel,
source_tree.as_view(),
sources,
moments,
&mut source_summaries.node_summaries,
);
if err != physics::hierarchical::kernel::HierarchicalError::Ok {
return Err(py_hierarchical_error("source summary update", err));
}
let mut out = vec![0.0; physics::hierarchical::kernel::TargetCollection::<K>::len(targets)];
err = physics::hierarchical::evaluator::accepted_levels(
&kernel,
source_tree.as_view(),
&source_summaries.node_summaries,
targets,
theta,
&mut out,
);
if err != physics::hierarchical::kernel::HierarchicalError::Ok {
return Err(py_hierarchical_error("source-level diagnostic", err));
}
Ok(out)
}
type OptionalDiagnosticsPy = (Option<Py<PyAny>>, Option<Py<PyArray1<f64>>>);
struct HierarchicalDiagnosticRequest<'a, K, S, C, M> {
kernel: K,
source_tree: &'a physics::hierarchical::tree::ClusterTree<f64>,
sources: S,
targets: C,
moments: M,
theta: f64,
}
fn optional_hierarchical_diagnostics<K, S, C, M>(
py: Python<'_>,
extra_diagnostics: bool,
request: HierarchicalDiagnosticRequest<'_, K, S, C, M>,
) -> PyResult<OptionalDiagnosticsPy>
where
K: physics::hierarchical::kernel::HierarchicalKernel<Scalar = f64, Output = [f64; 3]> + Sync,
S: physics::hierarchical::kernel::SourceCollection<K> + Copy,
M: physics::hierarchical::kernel::SourceMomentCollection<K> + Copy,
K::TargetGeometry: Copy,
C: physics::hierarchical::kernel::TargetCollection<K>,
{
if !extra_diagnostics {
return Ok((None, None));
}
let levels = accepted_levels_diagnostic(
request.kernel,
request.source_tree,
request.sources,
request.targets,
request.moments,
request.theta,
)?;
Ok((
Some(source_tree_diagnostics_object(py, request.source_tree)?),
Some(PyArray1::from_vec(py, levels).unbind()),
))
}
#[pyfunction(signature = (loc, moment, obs, outer_radius, theta=0.01, construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn flux_density_dipole_hierarchical(
py: Python<'_>,
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
moment: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
outer_radius: PyReadonlyArray1<f64>,
theta: f64,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let loc = read_xyz_tuple(py, &loc, "loc")?;
let moment = read_xyz_tuple(py, &moment, "moment")?;
let outer_radius = read_float_input_array1(py, &outer_radius, "outer_radius")?;
let obs = read_xyz_tuple(py, &obs, "obs")?;
let construction_method = parse_build_method(construction_method)?;
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, obs.len(), "flux_density", |out| {
physics::hierarchical::flux_density_dipole_hierarchical(
loc.as_tuple(),
moment.as_tuple(),
obs.as_tuple(),
outer_radius.as_slice(),
construction_method,
theta,
par,
out,
)
.map_err(|err| py_hierarchical_error("hierarchical dipole flux density", err))
})?;
let sources = physics::hierarchical::kernels::DipoleSources::new(
loc.as_tuple().0,
loc.as_tuple().1,
loc.as_tuple().2,
outer_radius.as_slice(),
);
let targets = physics::hierarchical::kernels::DipoleTargets::new(
obs.as_tuple().0,
obs.as_tuple().1,
obs.as_tuple().2,
);
let moments = physics::hierarchical::kernels::DipoleMoments::new(
moment.as_tuple().0,
moment.as_tuple().1,
moment.as_tuple().2,
);
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel: physics::hierarchical::kernels::DipoleFluxDensityKernel::<f64>::new(),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments,
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
#[pyfunction(signature = (loc, moment, obs, outer_radius, theta=0.01, construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn vector_potential_dipole_hierarchical(
py: Python<'_>,
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
moment: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
outer_radius: PyReadonlyArray1<f64>,
theta: f64,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let loc = read_xyz_tuple(py, &loc, "loc")?;
let moment = read_xyz_tuple(py, &moment, "moment")?;
let outer_radius = read_float_input_array1(py, &outer_radius, "outer_radius")?;
let obs = read_xyz_tuple(py, &obs, "obs")?;
let construction_method = parse_build_method(construction_method)?;
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, obs.len(), "vector_potential", |out| {
physics::hierarchical::vector_potential_dipole_hierarchical(
loc.as_tuple(),
moment.as_tuple(),
obs.as_tuple(),
outer_radius.as_slice(),
construction_method,
theta,
par,
out,
)
.map_err(|err| py_hierarchical_error("hierarchical dipole vector potential", err))
})?;
let sources = physics::hierarchical::kernels::DipoleSources::new(
loc.as_tuple().0,
loc.as_tuple().1,
loc.as_tuple().2,
outer_radius.as_slice(),
);
let targets = physics::hierarchical::kernels::DipoleTargets::new(
obs.as_tuple().0,
obs.as_tuple().1,
obs.as_tuple().2,
);
let moments = physics::hierarchical::kernels::DipoleMoments::new(
moment.as_tuple().0,
moment.as_tuple().1,
moment.as_tuple().2,
);
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel: physics::hierarchical::kernels::DipoleVectorPotentialKernel::<f64>::new(),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments,
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, theta=0.05, construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn flux_density_linear_filament_hierarchical(
py: Python<'_>,
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
ifil: PyReadonlyArray1<f64>,
wire_radius: PyReadonlyArray1<f64>,
theta: f64,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let xyzp = read_xyz_tuple(py, &xyzp, "xyzp")?;
let xyzfil = read_xyz_tuple(py, &xyzfil, "xyzfil")?;
let dlxyzfil = read_xyz_tuple(py, &dlxyzfil, "dlxyzfil")?;
let ifil = read_float_input_array1(py, &ifil, "ifil")?;
let wire_radius = read_float_input_array1(py, &wire_radius, "wire_radius")?;
let construction_method = parse_build_method(construction_method)?;
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, xyzp.len(), "flux_density", |out| {
physics::hierarchical::flux_density_linear_filament_hierarchical(
xyzp.as_tuple(),
xyzfil.as_tuple(),
dlxyzfil.as_tuple(),
ifil.as_slice(),
wire_radius.as_slice(),
construction_method,
theta,
par,
out,
)
.map_err(|err| py_hierarchical_error("hierarchical linear-filament flux density", err))
})?;
let sources = physics::hierarchical::kernels::LinearFilamentSources::new(
xyzfil.as_tuple(),
dlxyzfil.as_tuple(),
wire_radius.as_slice(),
);
let targets = physics::hierarchical::kernels::DipoleTargets::new(
xyzp.as_tuple().0,
xyzp.as_tuple().1,
xyzp.as_tuple().2,
);
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel: physics::hierarchical::kernels::LinearFilamentFluxDensityKernel::<f64>::new(),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments: ifil.as_slice(),
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, theta=0.05, construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn vector_potential_linear_filament_hierarchical(
py: Python<'_>,
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
ifil: PyReadonlyArray1<f64>,
wire_radius: PyReadonlyArray1<f64>,
theta: f64,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let xyzp = read_xyz_tuple(py, &xyzp, "xyzp")?;
let xyzfil = read_xyz_tuple(py, &xyzfil, "xyzfil")?;
let dlxyzfil = read_xyz_tuple(py, &dlxyzfil, "dlxyzfil")?;
let ifil = read_float_input_array1(py, &ifil, "ifil")?;
let wire_radius = read_float_input_array1(py, &wire_radius, "wire_radius")?;
let construction_method = parse_build_method(construction_method)?;
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, xyzp.len(), "vector_potential", |out| {
physics::hierarchical::vector_potential_linear_filament_hierarchical(
xyzp.as_tuple(),
xyzfil.as_tuple(),
dlxyzfil.as_tuple(),
ifil.as_slice(),
wire_radius.as_slice(),
construction_method,
theta,
par,
out,
)
.map_err(|err| {
py_hierarchical_error("hierarchical linear-filament vector potential", err)
})
})?;
let sources = physics::hierarchical::kernels::LinearFilamentSources::new(
xyzfil.as_tuple(),
dlxyzfil.as_tuple(),
wire_radius.as_slice(),
);
let targets = physics::hierarchical::kernels::DipoleTargets::new(
xyzp.as_tuple().0,
xyzp.as_tuple().1,
xyzp.as_tuple().2,
);
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel: physics::hierarchical::kernels::LinearFilamentVectorPotentialKernel::<f64>::new(
),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments: ifil.as_slice(),
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
#[pyfunction(signature = (obs, nodes, triangles, s, theta=0.05, quad="dunavant3", construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn flux_density_triangle_mesh_hierarchical(
py: Python<'_>,
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
theta: f64,
quad: &str,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let obs = read_matrix3_input(py, &obs, "obs")?;
let nodes = read_matrix3_input(py, &nodes, "nodes")?;
let triangles = read_matrix3_input(py, &triangles, "triangles")?;
let mesh = borrowed_triangle_mesh_view(&nodes, &triangles)?;
let s = read_float_input_array1(py, &s, "s")?;
let quad = parse_triangle_quadrature(quad)?;
let construction_method = parse_build_method(construction_method)?;
let sources = physics::hierarchical::kernels::BoundaryElementTriangles::new(&mesh);
let targets = physics::hierarchical::kernels::DipoleTargetRows::new(obs.as_slice());
let moments =
physics::hierarchical::kernels::BoundaryElementNodalValues::new(sources, s.as_slice());
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, obs.nrows(), "flux_density", |out| {
physics::hierarchical::convenience::one_shot_vec3(
physics::hierarchical::kernels::BoundaryElementFluxDensityKernel::<f64>::new(quad),
sources,
moments,
targets,
construction_method,
theta,
par,
out,
)
.map_err(|err| py_hierarchical_error("hierarchical triangle-mesh flux density", err))
})?;
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel: physics::hierarchical::kernels::BoundaryElementFluxDensityKernel::<f64>::new(
quad,
),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments,
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
#[pyfunction(signature = (obs, nodes, triangles, s, theta=0.05, quad="dunavant3", construction_method="longest_axis", par=true, out=None, extra_diagnostics=false))]
fn vector_potential_triangle_mesh_hierarchical(
py: Python<'_>,
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
theta: f64,
quad: &str,
construction_method: &str,
par: bool,
out: Option<(
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
)>,
extra_diagnostics: bool,
) -> PyResult<Py<SolveResult>> {
let obs = read_matrix3_input(py, &obs, "obs")?;
let nodes = read_matrix3_input(py, &nodes, "nodes")?;
let triangles = read_matrix3_input(py, &triangles, "triangles")?;
let mesh = borrowed_triangle_mesh_view(&nodes, &triangles)?;
let s = read_float_input_array1(py, &s, "s")?;
let quad = parse_triangle_quadrature(quad)?;
let construction_method = parse_build_method(construction_method)?;
let sources = physics::hierarchical::kernels::BoundaryElementTriangles::new(&mesh);
let targets = physics::hierarchical::kernels::DipoleTargetRows::new(obs.as_slice());
let moments =
physics::hierarchical::kernels::BoundaryElementNodalValues::new(sources, s.as_slice());
let (field, diagnostics) =
evaluate_hierarchical_vec3(py, out, obs.nrows(), "vector_potential", |out| {
physics::hierarchical::convenience::one_shot_vec3(
physics::hierarchical::kernels::BoundaryElementVectorPotentialKernel::<f64>::new(
quad,
),
sources,
moments,
targets,
construction_method,
theta,
par,
out,
)
.map_err(|err| {
py_hierarchical_error("hierarchical triangle-mesh vector potential", err)
})
})?;
let (source_tree, accepted_levels) = optional_hierarchical_diagnostics(
py,
extra_diagnostics,
HierarchicalDiagnosticRequest {
kernel:
physics::hierarchical::kernels::BoundaryElementVectorPotentialKernel::<f64>::new(
quad,
),
source_tree: &diagnostics.source_tree,
sources,
targets,
moments,
theta,
},
)?;
solve_result_from_field(
py,
field,
diagnostics.construction_seconds,
diagnostics.evaluation_seconds,
diagnostics.source_count,
diagnostics.target_count,
source_tree,
accepted_levels,
)
}
fn read_axisym_nodes<F: NumpyElement + Copy>(
name: &str,
nodes: PyReadonlyArray2<'_, F>,
) -> PyResult<Vec<[F; 2]>> {
let view = nodes.as_array();
let shape = view.shape();
if shape.len() != 2 || shape[1] != 2 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nnode, 2)"),
}
.into());
}
let mut out = Vec::with_capacity(shape[0]);
for row in view.rows() {
out.push([row[0], row[1]]);
}
Ok(out)
}
fn read_axisym_elements<const NODES_PER_ELEMENT: usize>(
name: &str,
elements: PyReadonlyArray2<'_, u64>,
) -> PyResult<Vec<[usize; NODES_PER_ELEMENT]>> {
let view = elements.as_array();
let shape = view.shape();
if shape.len() != 2 || shape[1] != NODES_PER_ELEMENT {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nelem, {NODES_PER_ELEMENT})"),
}
.into());
}
let mut out = Vec::with_capacity(shape[0]);
for row in view.rows() {
let mut entry = [0usize; NODES_PER_ELEMENT];
for col in 0..NODES_PER_ELEMENT {
entry[col] = usize::try_from(row[col]).map_err(|_| PyInteropError::ValueError {
msg: format!("{name} contains an index that overflows usize"),
})?;
}
out.push(entry);
}
Ok(out)
}
fn read_usize_indices(name: &str, indices: PyReadonlyArray1<'_, u64>) -> PyResult<Vec<usize>> {
indices
.as_slice()?
.iter()
.copied()
.map(|index| {
usize::try_from(index).map_err(|_| {
PyInteropError::ValueError {
msg: format!("{name} contains an index that overflows usize"),
}
.into()
})
})
.collect()
}
fn read_axisym_material_ids(
name: &str,
material_ids: PyReadonlyArray1<'_, u64>,
) -> PyResult<Vec<usize>> {
let view = material_ids.as_array();
let shape = view.shape();
if shape.len() != 1 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nelem,)"),
}
.into());
}
view.iter()
.map(|&value| {
usize::try_from(value).map_err(|_| PyInteropError::ValueError {
msg: format!("{name} contains a value that overflows usize"),
})
})
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
}
fn read_axisym_material_table(
name: &str,
material_table: PyReadonlyArray3<'_, f64>,
) -> PyResult<Vec<[[f64; 4]; 4]>> {
let view = material_table.as_array();
let shape = view.shape();
if shape.len() != 3 || shape[1] != 4 || shape[2] != 4 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nmat, 4, 4)"),
}
.into());
}
let mut out = Vec::with_capacity(shape[0]);
for matrix in view.outer_iter() {
let mut entry = [[0.0; 4]; 4];
for row in 0..4 {
for col in 0..4 {
entry[row][col] = matrix[[row, col]];
}
}
out.push(entry);
}
Ok(out)
}
fn read_axisym_thermal_material_table(
name: &str,
thermal_material_table: PyReadonlyArray2<'_, f64>,
) -> PyResult<Vec<physics::solenoid_stress::ThermalMaterial>> {
let view = thermal_material_table.as_array();
let shape = view.shape();
if shape.len() != 2 || shape[1] != 5 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nmat, 5)"),
}
.into());
}
let mut out = Vec::with_capacity(shape[0]);
for row in view.rows() {
out.push(physics::solenoid_stress::ThermalMaterial {
alpha: [row[0], row[1], row[2], row[3]],
reference_temperature: row[4],
});
}
Ok(out)
}
fn read_axisym_material_orientation_angles(
name: &str,
angles: PyReadonlyArray1<'_, f64>,
) -> PyResult<Vec<f64>> {
let view = angles.as_array();
let shape = view.shape();
if shape.len() != 1 {
return Err(PyInteropError::DimensionalityError {
msg: format!("{name} must have shape (nelem,) or be empty"),
}
.into());
}
Ok(view.iter().copied().collect())
}
fn flatten_quad_points<F: Copy>(points: Vec<[F; 2]>) -> Vec<F> {
flatten_points(points)
}
fn flatten_sparse_operator<F: math::Scalar>(
operator: mesh::quad2d::QuadMeshSparseOperator<F>,
) -> (Vec<F>, Vec<u64>, Vec<u64>, u64, u64) {
(
operator.vals,
operator
.rows
.into_iter()
.map(|index| index as u64)
.collect(),
operator
.cols
.into_iter()
.map(|index| index as u64)
.collect(),
operator.nrow as u64,
operator.ncol as u64,
)
}
fn sparse_operator_to_py<'py, F>(
py: Python<'py>,
operator: mesh::quad2d::QuadMeshSparseOperator<F>,
) -> PyResult<(
Py<PyArray1<F>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
u64,
u64,
)>
where
F: math::Scalar + NumpyElement,
{
let (vals, rows, cols, nrow, ncol) = flatten_sparse_operator(operator);
Ok((
PyArray1::from_vec(py, vals).unbind(),
PyArray1::from_vec(py, rows).unbind(),
PyArray1::from_vec(py, cols).unbind(),
nrow,
ncol,
))
}
fn read_axisym_pressure_faces(
pressure_faces: PyReadonlyArray2<'_, u64>,
) -> PyResult<Vec<physics::solenoid_stress::PressureLoad>> {
let faces_view = pressure_faces.as_array();
let faces_shape = faces_view.shape();
if faces_shape.len() != 2 || faces_shape[1] != 2 {
return Err(PyInteropError::DimensionalityError {
msg: "pressure_faces must have shape (nload, 2)".to_string(),
}
.into());
}
let mut out = Vec::with_capacity(faces_shape[0]);
for row in faces_view.rows() {
out.push(physics::solenoid_stress::PressureLoad {
element: usize::try_from(row[0]).map_err(|_| PyInteropError::ValueError {
msg: "pressure_faces element index overflowed usize".to_string(),
})?,
local_face: u8::try_from(row[1]).map_err(|_| PyInteropError::ValueError {
msg: "pressure_faces local face overflowed u8".to_string(),
})?,
});
}
Ok(out)
}
fn read_axisym_traction_faces(
traction_faces: PyReadonlyArray2<'_, u64>,
) -> PyResult<Vec<physics::solenoid_stress::TractionLoad>> {
let faces_view = traction_faces.as_array();
let faces_shape = faces_view.shape();
if faces_shape.len() != 2 || faces_shape[1] != 2 {
return Err(PyInteropError::DimensionalityError {
msg: "traction_faces must have shape (nload, 2)".to_string(),
}
.into());
}
let mut out = Vec::with_capacity(faces_shape[0]);
for row in faces_view.rows() {
out.push(physics::solenoid_stress::TractionLoad {
element: usize::try_from(row[0]).map_err(|_| PyInteropError::ValueError {
msg: "traction_faces element index overflowed usize".to_string(),
})?,
local_face: u8::try_from(row[1]).map_err(|_| PyInteropError::ValueError {
msg: "traction_faces local face overflowed u8".to_string(),
})?,
});
}
Ok(out)
}
fn parse_solenoid_fem_quadrature(
quadrature: u8,
) -> PyResult<physics::solenoid_stress::QuadratureRule> {
physics::solenoid_stress::QuadratureRule::from_code(quadrature)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
fn flatten_points<F: Copy>(points: Vec<[F; 2]>) -> Vec<F> {
let mut points_flat = Vec::with_capacity(points.len() * 2);
for point in points {
points_flat.push(point[0]);
points_flat.push(point[1]);
}
points_flat
}
fn flatten_usize_pairs(pairs: &[[usize; 2]]) -> Vec<usize> {
let mut out = Vec::with_capacity(pairs.len() * 2);
for pair in pairs {
out.push(pair[0]);
out.push(pair[1]);
}
out
}
fn flatten_rank4_samples<F: Copy>(samples: Vec<[F; 4]>) -> Vec<F> {
let mut out = Vec::with_capacity(samples.len() * 4);
for sample in samples {
out.extend_from_slice(&sample);
}
out
}
fn read_axisym_prescribed(
prescribed_dofs: PyReadonlyArray1<'_, u64>,
prescribed_values: PyReadonlyArray1<'_, f64>,
) -> PyResult<Vec<(usize, f64)>> {
let dofs = prescribed_dofs.as_array();
let values = prescribed_values.as_array();
if dofs.ndim() != 1 {
return Err(PyInteropError::DimensionalityError {
msg: "prescribed_dofs must have shape (nfixed,)".to_string(),
}
.into());
}
if values.ndim() != 1 {
return Err(PyInteropError::DimensionalityError {
msg: "prescribed_values must have shape (nfixed,)".to_string(),
}
.into());
}
if dofs.len() != values.len() {
return Err(PyInteropError::ValueError {
msg: format!(
"prescribed_dofs has length {}, but prescribed_values has length {}",
dofs.len(),
values.len()
),
}
.into());
}
dofs.iter()
.zip(values.iter())
.map(|(&dof, &value)| {
usize::try_from(dof)
.map(|dof| (dof, value))
.map_err(|_| PyInteropError::ValueError {
msg: "prescribed_dofs contains a value that overflows usize".to_string(),
})
})
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
}
#[pyclass(module = "cfsem", unsendable)]
struct SolenoidStress2dModelF64 {
inner: physics::solenoid_stress::Structural2dModel,
}
macro_rules! impl_solenoid_stress_model_pyclass {
($name:ident, $ty:ty) => {
#[pymethods]
impl $name {
#[getter]
fn ndof_full(&self) -> usize {
self.inner.ndof_full
}
#[getter]
fn ndof_reduced(&self) -> usize {
self.inner.ndof_reduced
}
#[getter]
fn nelem(&self) -> usize {
self.inner.nelem
}
#[getter]
fn n_temperature_nodes(&self) -> usize {
self.inner.n_temperature_nodes
}
#[getter]
fn nq_per_element(&self) -> usize {
self.inner.nq_per_element
}
#[getter]
fn nodes_per_element(&self) -> usize {
self.inner.nodes_per_element
}
#[getter]
fn element_type(&self) -> String {
self.inner.element_type.as_str().to_string()
}
#[getter]
fn formulation(&self) -> String {
self.inner.formulation.as_str().to_string()
}
fn analysis_nodes_flat<'py>(&self, py: Python<'py>) -> Py<PyArray1<$ty>> {
PyArray1::from_vec(py, flatten_points(self.inner.analysis_nodes.clone())).unbind()
}
fn analysis_elements_flat<'py>(&self, py: Python<'py>) -> Py<PyArray1<usize>> {
PyArray1::from_vec(py, self.inner.analysis_elements_flat.clone()).unbind()
}
fn pressure_faces_flat<'py>(&self, py: Python<'py>) -> Py<PyArray1<usize>> {
PyArray1::from_vec(py, flatten_usize_pairs(&self.inner.pressure_faces)).unbind()
}
fn traction_faces_flat<'py>(&self, py: Python<'py>) -> Py<PyArray1<usize>> {
PyArray1::from_vec(py, flatten_usize_pairs(&self.inner.traction_faces)).unbind()
}
fn free_dofs<'py>(&self, py: Python<'py>) -> Py<PyArray1<usize>> {
PyArray1::from_vec(py, self.inner.free_dofs.clone()).unbind()
}
fn fixed_dofs<'py>(&self, py: Python<'py>) -> Py<PyArray1<usize>> {
PyArray1::from_vec(py, self.inner.fixed_dofs.clone()).unbind()
}
fn fixed_values<'py>(&self, py: Python<'py>) -> Py<PyArray1<$ty>> {
PyArray1::from_vec(py, self.inner.fixed_values.clone()).unbind()
}
fn constant_rhs<'py>(&self, py: Python<'py>) -> Py<PyArray1<$ty>> {
PyArray1::from_vec(py, self.inner.constant_rhs.clone()).unbind()
}
fn stiffness_csc<'py>(
&self,
py: Python<'py>,
) -> (
Py<PyArray1<$ty>>,
Py<PyArray1<usize>>,
Py<PyArray1<usize>>,
usize,
usize,
) {
let stiffness = &self.inner.stiffness;
(
PyArray1::from_vec(py, stiffness.val().to_vec()).unbind(),
PyArray1::from_vec(py, stiffness.row_idx().to_vec()).unbind(),
PyArray1::from_vec(py, stiffness.col_ptr().to_vec()).unbind(),
stiffness.nrows(),
stiffness.ncols(),
)
}
fn body_force_to_rhs_csr<'py>(
&self,
py: Python<'py>,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<usize>>,
Py<PyArray1<usize>>,
usize,
usize,
)> {
let operator = self
.inner
.body_force_to_rhs()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, operator.val().to_vec()).unbind(),
PyArray1::from_vec(py, operator.col_idx().to_vec()).unbind(),
PyArray1::from_vec(py, operator.row_ptr().to_vec()).unbind(),
operator.nrows(),
operator.ncols(),
))
}
fn pressure_to_rhs_csr<'py>(
&self,
py: Python<'py>,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<usize>>,
Py<PyArray1<usize>>,
usize,
usize,
)> {
let operator = self
.inner
.pressure_to_rhs()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, operator.val().to_vec()).unbind(),
PyArray1::from_vec(py, operator.col_idx().to_vec()).unbind(),
PyArray1::from_vec(py, operator.row_ptr().to_vec()).unbind(),
operator.nrows(),
operator.ncols(),
))
}
fn traction_to_rhs_csr<'py>(
&self,
py: Python<'py>,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<usize>>,
Py<PyArray1<usize>>,
usize,
usize,
)> {
let operator = self
.inner
.traction_to_rhs()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, operator.val().to_vec()).unbind(),
PyArray1::from_vec(py, operator.col_idx().to_vec()).unbind(),
PyArray1::from_vec(py, operator.row_ptr().to_vec()).unbind(),
operator.nrows(),
operator.ncols(),
))
}
fn temperature_to_rhs_csr<'py>(
&self,
py: Python<'py>,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<usize>>,
Py<PyArray1<usize>>,
usize,
usize,
)> {
let operator = self
.inner
.temperature_to_rhs()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, operator.val().to_vec()).unbind(),
PyArray1::from_vec(py, operator.col_idx().to_vec()).unbind(),
PyArray1::from_vec(py, operator.row_ptr().to_vec()).unbind(),
operator.nrows(),
operator.ncols(),
))
}
#[pyo3(signature = (body_force=None, pressure_values=None, traction_values=None, nodal_temperature=None))]
fn build_rhs<'py>(
&self,
py: Python<'py>,
body_force: Option<PyReadonlyArray1<'_, $ty>>,
pressure_values: Option<PyReadonlyArray1<'_, $ty>>,
traction_values: Option<PyReadonlyArray1<'_, $ty>>,
nodal_temperature: Option<PyReadonlyArray1<'_, $ty>>,
) -> PyResult<Py<PyArray1<$ty>>> {
let body_force = match &body_force {
Some(arr) => Some(arr.as_slice()?),
None => None,
};
let pressure_values = match &pressure_values {
Some(arr) => Some(arr.as_slice()?),
None => None,
};
let traction_values = match &traction_values {
Some(arr) => Some(arr.as_slice()?),
None => None,
};
let nodal_temperature = match &nodal_temperature {
Some(arr) => Some(arr.as_slice()?),
None => None,
};
let rhs = self
.inner
.build_rhs(body_force, pressure_values, traction_values, nodal_temperature)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, rhs).unbind())
}
fn solve<'py>(
&mut self,
py: Python<'py>,
rhs: PyReadonlyArray1<'_, $ty>,
) -> PyResult<Py<PyArray1<$ty>>> {
let rhs = rhs.as_slice()?;
let displacement = self
.inner
.solve(rhs)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, displacement).unbind())
}
fn quadrature<'py>(
&self,
py: Python<'py>,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<u64>>,
Py<PyArray1<$ty>>,
Py<PyArray1<$ty>>,
Py<PyArray1<$ty>>,
usize,
)> {
let quadrature = self
.inner
.quadrature()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, flatten_points(quadrature.locations.points)).unbind(),
PyArray1::from_vec(
py,
quadrature
.locations
.element_indices
.into_iter()
.map(|index| index as u64)
.collect(),
)
.unbind(),
PyArray1::from_vec(py, flatten_points(quadrature.locations.reference_points))
.unbind(),
PyArray1::from_vec(py, quadrature.weights_area).unbind(),
PyArray1::from_vec(py, quadrature.weights_volume).unbind(),
quadrature.points_per_element,
))
}
fn locate_points_in_elements<'py>(
&self,
py: Python<'py>,
points: PyReadonlyArray2<'_, $ty>,
element_indices: PyReadonlyArray1<'_, u64>,
max_iterations: usize,
) -> PyResult<(
Py<PyArray1<$ty>>,
Py<PyArray1<u64>>,
Py<PyArray1<$ty>>,
)> {
let points = read_axisym_nodes("points", points)?;
let element_indices = read_usize_indices("element_indices", element_indices)?;
let locations = self
.inner
.locate_points_in_elements(&points, &element_indices, max_iterations)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, flatten_points(locations.points)).unbind(),
PyArray1::from_vec(
py,
locations
.element_indices
.into_iter()
.map(|index| index as u64)
.collect(),
)
.unbind(),
PyArray1::from_vec(py, flatten_points(locations.reference_points)).unbind(),
))
}
fn element_measures<'py>(
&self,
py: Python<'py>,
) -> PyResult<(Py<PyArray1<$ty>>, Py<PyArray1<$ty>>)> {
let measures = self
.inner
.element_measures()
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok((
PyArray1::from_vec(py, measures.areas).unbind(),
PyArray1::from_vec(py, measures.volumes).unbind(),
))
}
fn strain<'py>(
&self,
py: Python<'py>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, $ty>,
displacements_full: PyReadonlyArray1<'_, $ty>,
) -> PyResult<Py<PyArray1<$ty>>> {
let element_indices = read_usize_indices("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let displacements_full = displacements_full.as_slice()?;
let strain = self
.inner
.strain(&element_indices, &reference_points, displacements_full)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, flatten_rank4_samples(strain)).unbind())
}
fn stress<'py>(
&self,
py: Python<'py>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, $ty>,
displacements_full: PyReadonlyArray1<'_, $ty>,
) -> PyResult<Py<PyArray1<$ty>>> {
let element_indices = read_usize_indices("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let displacements_full = displacements_full.as_slice()?;
let stress = self
.inner
.stress(&element_indices, &reference_points, displacements_full)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, flatten_rank4_samples(stress)).unbind())
}
#[pyo3(signature = (element_indices, reference_points, nodal_temperature=None))]
fn thermal_strain<'py>(
&self,
py: Python<'py>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, $ty>,
nodal_temperature: Option<PyReadonlyArray1<'_, $ty>>,
) -> PyResult<Py<PyArray1<$ty>>> {
let element_indices = read_usize_indices("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let empty: [$ty; 0] = [];
let nodal_temperature = match &nodal_temperature {
Some(arr) => arr.as_slice()?,
None => &empty,
};
let thermal_strain = self
.inner
.thermal_strain(&element_indices, &reference_points, nodal_temperature)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, flatten_rank4_samples(thermal_strain)).unbind())
}
#[pyo3(signature = (element_indices, reference_points, nodal_temperature=None))]
fn thermal_stress<'py>(
&self,
py: Python<'py>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, $ty>,
nodal_temperature: Option<PyReadonlyArray1<'_, $ty>>,
) -> PyResult<Py<PyArray1<$ty>>> {
let element_indices = read_usize_indices("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let empty: [$ty; 0] = [];
let nodal_temperature = match &nodal_temperature {
Some(arr) => arr.as_slice()?,
None => &empty,
};
let thermal_stress = self
.inner
.thermal_stress(&element_indices, &reference_points, nodal_temperature)
.map_err(|msg| PyInteropError::ValueError { msg })?;
Ok(PyArray1::from_vec(py, flatten_rank4_samples(thermal_stress)).unbind())
}
}
};
}
impl_solenoid_stress_model_pyclass!(SolenoidStress2dModelF64, f64);
fn assemble_structural_2d_model_low_level(
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
material_ids: PyReadonlyArray1<'_, u64>,
material_table: PyReadonlyArray3<'_, f64>,
pressure_faces: PyReadonlyArray2<'_, u64>,
traction_faces: PyReadonlyArray2<'_, u64>,
thermal_material_table: PyReadonlyArray2<'_, f64>,
material_orientation_angles: PyReadonlyArray1<'_, f64>,
prescribed_dofs: PyReadonlyArray1<'_, u64>,
prescribed_values: PyReadonlyArray1<'_, f64>,
element_type: u8,
formulation: u8,
thickness: f64,
quadrature: u8,
par: bool,
) -> PyResult<physics::solenoid_stress::Structural2dModel> {
let quadrature = parse_solenoid_fem_quadrature(quadrature)?;
let element_type = physics::solenoid_stress::Structural2dElementType::from_code(element_type)
.map_err(|msg| PyInteropError::ValueError { msg })?;
let formulation =
physics::solenoid_stress::Structural2dFormulation::from_code(formulation, thickness)
.map_err(|msg| PyInteropError::ValueError { msg })?;
let nodes = read_axisym_nodes("nodes", nodes)?;
let material_ids = read_axisym_material_ids("material_ids", material_ids)?;
let material_table = read_axisym_material_table("material_table", material_table)?;
let pressure_faces = read_axisym_pressure_faces(pressure_faces)?;
let traction_faces = read_axisym_traction_faces(traction_faces)?;
let thermal_material_table =
read_axisym_thermal_material_table("thermal_material_table", thermal_material_table)?;
let material_orientation_angles = read_axisym_material_orientation_angles(
"material_orientation_angles",
material_orientation_angles,
)?;
let prescribed = read_axisym_prescribed(prescribed_dofs, prescribed_values)?;
let thermal_material_table =
(!thermal_material_table.is_empty()).then_some(thermal_material_table.as_slice());
let material_orientation_angles =
(!material_orientation_angles.is_empty()).then_some(material_orientation_angles.as_slice());
match element_type {
physics::solenoid_stress::Structural2dElementType::Quad4 => {
let elements = read_axisym_elements::<4>("elements", elements)?;
physics::solenoid_stress::assemble_structural_2d(
&nodes,
physics::solenoid_stress::Structural2dElements::Quad4(&elements),
&material_ids,
&material_table,
&pressure_faces,
&traction_faces,
thermal_material_table,
material_orientation_angles,
&prescribed,
formulation,
quadrature,
par,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
physics::solenoid_stress::Structural2dElementType::Quad9 => {
let elements = read_axisym_elements::<9>("elements", elements)?;
physics::solenoid_stress::assemble_structural_2d(
&nodes,
physics::solenoid_stress::Structural2dElements::Quad9(&elements),
&material_ids,
&material_table,
&pressure_faces,
&traction_faces,
thermal_material_table,
material_orientation_angles,
&prescribed,
formulation,
quadrature,
par,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
}
}
#[pyfunction]
fn solenoid_stress_fem_assemble_model_2d_f64(
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
material_ids: PyReadonlyArray1<'_, u64>,
material_table: PyReadonlyArray3<'_, f64>,
pressure_faces: PyReadonlyArray2<'_, u64>,
traction_faces: PyReadonlyArray2<'_, u64>,
thermal_material_table: PyReadonlyArray2<'_, f64>,
material_orientation_angles: PyReadonlyArray1<'_, f64>,
prescribed_dofs: PyReadonlyArray1<'_, u64>,
prescribed_values: PyReadonlyArray1<'_, f64>,
element_type: u8,
formulation: u8,
thickness: f64,
quadrature: u8,
par: bool,
) -> PyResult<SolenoidStress2dModelF64> {
Ok(SolenoidStress2dModelF64 {
inner: assemble_structural_2d_model_low_level(
nodes,
elements,
material_ids,
material_table,
pressure_faces,
traction_faces,
thermal_material_table,
material_orientation_angles,
prescribed_dofs,
prescribed_values,
element_type,
formulation,
thickness,
quadrature,
par,
)?,
})
}
#[pyfunction]
fn solenoid_stress_fem_isotropic_axisymmetric_material_f64<'py>(
py: Python<'py>,
youngs_modulus: f64,
poisson_ratio: f64,
) -> Py<PyArray1<f64>> {
let material =
physics::solenoid_stress::isotropic_axisymmetric_material(youngs_modulus, poisson_ratio);
let mut flat = Vec::with_capacity(16);
for row in material {
flat.extend_from_slice(&row);
}
PyArray1::from_vec(py, flat).unbind()
}
#[pyfunction]
fn solenoid_stress_fem_isotropic_plane_strain_material_f64<'py>(
py: Python<'py>,
youngs_modulus: f64,
poisson_ratio: f64,
) -> Py<PyArray1<f64>> {
solenoid_stress_fem_isotropic_axisymmetric_material_f64(py, youngs_modulus, poisson_ratio)
}
#[pyfunction]
fn solenoid_stress_fem_isotropic_axisymmetric_thermal_material_f64<'py>(
py: Python<'py>,
alpha: f64,
reference_temperature: f64,
) -> Py<PyArray1<f64>> {
let material = physics::solenoid_stress::isotropic_axisymmetric_thermal_material(
alpha,
reference_temperature,
);
let mut flat = Vec::with_capacity(5);
flat.extend_from_slice(&material.alpha);
flat.push(material.reference_temperature);
PyArray1::from_vec(py, flat).unbind()
}
#[pyfunction]
fn solenoid_stress_fem_isotropic_plane_strain_thermal_material_f64<'py>(
py: Python<'py>,
alpha: f64,
reference_temperature: f64,
) -> Py<PyArray1<f64>> {
solenoid_stress_fem_isotropic_axisymmetric_thermal_material_f64(
py,
alpha,
reference_temperature,
)
}
#[pyfunction]
fn solenoid_stress_fem_orthotropic_axisymmetric_thermal_material_f64<'py>(
py: Python<'py>,
alpha_r: f64,
alpha_z: f64,
alpha_t: f64,
reference_temperature: f64,
) -> Py<PyArray1<f64>> {
let material = physics::solenoid_stress::orthotropic_axisymmetric_thermal_material(
alpha_r,
alpha_z,
alpha_t,
reference_temperature,
);
let mut flat = Vec::with_capacity(5);
flat.extend_from_slice(&material.alpha);
flat.push(material.reference_temperature);
PyArray1::from_vec(py, flat).unbind()
}
#[pyfunction]
fn solenoid_stress_fem_cfsem_radial_material_f64<'py>(
py: Python<'py>,
youngs_modulus: f64,
poisson_ratio: f64,
) -> Py<PyArray1<f64>> {
let material = physics::solenoid_stress::cfsem_radial_material(youngs_modulus, poisson_ratio);
let mut flat = Vec::with_capacity(16);
for row in material {
flat.extend_from_slice(&row);
}
PyArray1::from_vec(py, flat).unbind()
}
#[pyfunction]
fn solenoid_stress_fem_infer_quad9_mesh_f64<'py>(
py: Python<'py>,
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
) -> PyResult<(
Py<PyArray1<f64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
)> {
let nodes = read_axisym_nodes("nodes", nodes)?;
let elements = read_axisym_elements::<4>("elements", elements)?;
let elevated = physics::solenoid_stress::infer_quad9_mesh(&nodes, &elements)
.map_err(|msg| PyInteropError::ValueError { msg })?;
let analysis_nodes = flatten_points(elevated.analysis_nodes);
let mut analysis_elements = Vec::with_capacity(elevated.analysis_elements.len() * 9);
for conn in elevated.analysis_elements {
analysis_elements.extend(conn.into_iter().map(|node| node as u64));
}
Ok((
PyArray1::from_vec(py, analysis_nodes).unbind(),
PyArray1::from_vec(py, analysis_elements).unbind(),
PyArray1::from_vec(
py,
elevated
.corner_node_indices
.into_iter()
.map(|index| index as u64)
.collect(),
)
.unbind(),
PyArray1::from_vec(
py,
elevated
.midside_node_indices
.into_iter()
.map(|index| index as u64)
.collect(),
)
.unbind(),
PyArray1::from_vec(
py,
elevated
.center_node_indices
.into_iter()
.map(|index| index as u64)
.collect(),
)
.unbind(),
))
}
fn quad_mesh_query_for_element_type<F>(
nodes: &[[F; 2]],
elements: PyReadonlyArray2<'_, u64>,
points: &[[F; 2]],
element_type: &str,
max_iterations: usize,
) -> PyResult<mesh::quad2d::QuadMeshQueryResult<F>>
where
F: mesh::Scalar + NumpyElement,
{
match element_type {
"quad4" => {
let elements = read_axisym_elements::<4>("elements", elements)?;
mesh::quad2d::query_quad_mesh::<mesh::quad2d::Quad4ReferenceElement, _, 4>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
points,
max_iterations,
)
}
"quad9" => {
let elements = read_axisym_elements::<9>("elements", elements)?;
mesh::quad2d::query_quad_mesh::<mesh::quad2d::Quad9ReferenceElement, _, 9>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
points,
max_iterations,
)
}
_ => Err(format!(
"unsupported element_type {element_type:?}; use 'quad4' or 'quad9'"
)),
}
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
fn quad_mesh_query_to_py<'py, F>(
py: Python<'py>,
query: mesh::quad2d::QuadMeshQueryResult<F>,
) -> PyResult<Py<PyDict>>
where
F: math::Scalar + NumpyElement,
{
let dict = PyDict::new(py);
dict.set_item(
"nearest_node_indices",
PyArray1::from_vec(
py,
query
.nearest_node_indices
.into_iter()
.map(|index| index as u64)
.collect(),
),
)?;
dict.set_item(
"nearest_node_points",
PyArray1::from_vec(py, flatten_quad_points(query.nearest_node_points)),
)?;
dict.set_item(
"nearest_node_distances",
PyArray1::from_vec(py, query.nearest_node_distances),
)?;
dict.set_item(
"nearest_element_indices",
PyArray1::from_vec(
py,
query
.nearest_element_indices
.into_iter()
.map(|index| index as u64)
.collect(),
),
)?;
dict.set_item(
"nearest_element_reference_points",
PyArray1::from_vec(
py,
flatten_quad_points(query.nearest_element_reference_points),
),
)?;
dict.set_item(
"nearest_element_points",
PyArray1::from_vec(py, flatten_quad_points(query.nearest_element_points)),
)?;
dict.set_item(
"nearest_element_distances",
PyArray1::from_vec(py, query.nearest_element_distances),
)?;
dict.set_item(
"nearest_face_element_indices",
PyArray1::from_vec(
py,
query
.nearest_face_element_indices
.into_iter()
.map(|index| index as u64)
.collect(),
),
)?;
dict.set_item(
"nearest_face_local_faces",
PyArray1::from_vec(
py,
query
.nearest_face_local_faces
.into_iter()
.map(u64::from)
.collect(),
),
)?;
dict.set_item(
"nearest_face_reference_coordinates",
PyArray1::from_vec(py, query.nearest_face_reference_coordinates),
)?;
dict.set_item(
"nearest_face_points",
PyArray1::from_vec(py, flatten_quad_points(query.nearest_face_points)),
)?;
dict.set_item(
"nearest_face_distances",
PyArray1::from_vec(py, query.nearest_face_distances),
)?;
Ok(dict.unbind())
}
#[pyfunction]
fn solenoid_stress_fem_quad_mesh_query_f64<'py>(
py: Python<'py>,
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
points: PyReadonlyArray2<'_, f64>,
element_type: &str,
max_iterations: usize,
) -> PyResult<Py<PyDict>> {
let nodes = read_axisym_nodes("nodes", nodes)?;
let points = read_axisym_nodes("points", points)?;
let query =
quad_mesh_query_for_element_type(&nodes, elements, &points, element_type, max_iterations)?;
quad_mesh_query_to_py(py, query)
}
fn quad_mesh_interpolation_operator_for_element_type<F>(
nodes: &[[F; 2]],
elements: PyReadonlyArray2<'_, u64>,
element_indices: &[usize],
reference_points: &[[F; 2]],
element_type: &str,
) -> PyResult<mesh::quad2d::QuadMeshSparseOperator<F>>
where
F: mesh::Scalar + NumpyElement,
{
match element_type {
"quad4" => {
let elements = read_axisym_elements::<4>("elements", elements)?;
mesh::quad2d::quad_mesh_interpolation_operator::<
mesh::quad2d::Quad4ReferenceElement,
_,
4,
>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
"quad9" => {
let elements = read_axisym_elements::<9>("elements", elements)?;
mesh::quad2d::quad_mesh_interpolation_operator::<
mesh::quad2d::Quad9ReferenceElement,
_,
9,
>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
_ => Err(PyInteropError::ValueError {
msg: format!("unsupported element_type {element_type:?}; use 'quad4' or 'quad9'"),
}
.into()),
}
}
fn quad_mesh_strain_operator_for_element_type(
nodes: &[[f64; 2]],
elements: PyReadonlyArray2<'_, u64>,
element_indices: &[usize],
reference_points: &[[f64; 2]],
element_type: &str,
formulation: physics::solenoid_stress::Structural2dFormulation,
) -> PyResult<mesh::quad2d::QuadMeshSparseOperator<f64>> {
match element_type {
"quad4" => {
let elements = read_axisym_elements::<4>("elements", elements)?;
mesh::quad2d::quad_mesh_strain_operator::<mesh::quad2d::Quad4ReferenceElement, 4, 8>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
formulation,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
"quad9" => {
let elements = read_axisym_elements::<9>("elements", elements)?;
mesh::quad2d::quad_mesh_strain_operator::<mesh::quad2d::Quad9ReferenceElement, 9, 18>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
formulation,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
_ => Err(PyInteropError::ValueError {
msg: format!("unsupported element_type {element_type:?}; use 'quad4' or 'quad9'"),
}
.into()),
}
}
fn quad_mesh_stress_operator_for_element_type(
nodes: &[[f64; 2]],
elements: PyReadonlyArray2<'_, u64>,
element_indices: &[usize],
reference_points: &[[f64; 2]],
material_ids: &[usize],
material_table: &[[[f64; 4]; 4]],
material_orientation_angles: Option<&[f64]>,
element_type: &str,
formulation: physics::solenoid_stress::Structural2dFormulation,
) -> PyResult<mesh::quad2d::QuadMeshSparseOperator<f64>> {
match element_type {
"quad4" => {
let elements = read_axisym_elements::<4>("elements", elements)?;
mesh::quad2d::quad_mesh_stress_operator::<mesh::quad2d::Quad4ReferenceElement, 4, 8>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
material_ids,
material_table,
material_orientation_angles,
formulation,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
"quad9" => {
let elements = read_axisym_elements::<9>("elements", elements)?;
mesh::quad2d::quad_mesh_stress_operator::<mesh::quad2d::Quad9ReferenceElement, 9, 18>(
mesh::QuadMeshView2d {
nodes_rz: nodes,
elements: &elements,
},
element_indices,
reference_points,
material_ids,
material_table,
material_orientation_angles,
formulation,
)
.map_err(|msg| PyInteropError::ValueError { msg }.into())
}
_ => Err(PyInteropError::ValueError {
msg: format!("unsupported element_type {element_type:?}; use 'quad4' or 'quad9'"),
}
.into()),
}
}
#[pyfunction]
fn solenoid_stress_fem_quad_mesh_interpolation_operator_f64<'py>(
py: Python<'py>,
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, f64>,
element_type: &str,
) -> PyResult<(
Py<PyArray1<f64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
u64,
u64,
)> {
let nodes = read_axisym_nodes("nodes", nodes)?;
let element_indices = read_axisym_material_ids("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let operator = quad_mesh_interpolation_operator_for_element_type(
&nodes,
elements,
&element_indices,
&reference_points,
element_type,
)?;
sparse_operator_to_py(py, operator)
}
#[pyfunction]
fn solenoid_stress_fem_quad_mesh_strain_operator_f64<'py>(
py: Python<'py>,
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, f64>,
element_type: &str,
formulation: u8,
thickness: f64,
) -> PyResult<(
Py<PyArray1<f64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
u64,
u64,
)> {
let nodes = read_axisym_nodes("nodes", nodes)?;
let element_indices = read_axisym_material_ids("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let formulation =
physics::solenoid_stress::Structural2dFormulation::from_code(formulation, thickness)
.map_err(|msg| PyInteropError::ValueError { msg })?;
let operator = quad_mesh_strain_operator_for_element_type(
&nodes,
elements,
&element_indices,
&reference_points,
element_type,
formulation,
)?;
sparse_operator_to_py(py, operator)
}
#[pyfunction]
fn solenoid_stress_fem_quad_mesh_stress_operator_f64<'py>(
py: Python<'py>,
nodes: PyReadonlyArray2<'_, f64>,
elements: PyReadonlyArray2<'_, u64>,
element_indices: PyReadonlyArray1<'_, u64>,
reference_points: PyReadonlyArray2<'_, f64>,
material_ids: PyReadonlyArray1<'_, u64>,
material_table: PyReadonlyArray3<'_, f64>,
material_orientation_angles: PyReadonlyArray1<'_, f64>,
element_type: &str,
formulation: u8,
thickness: f64,
) -> PyResult<(
Py<PyArray1<f64>>,
Py<PyArray1<u64>>,
Py<PyArray1<u64>>,
u64,
u64,
)> {
let nodes = read_axisym_nodes("nodes", nodes)?;
let element_indices = read_axisym_material_ids("element_indices", element_indices)?;
let reference_points = read_axisym_nodes("reference_points", reference_points)?;
let material_ids = read_axisym_material_ids("material_ids", material_ids)?;
let material_table = read_axisym_material_table("material_table", material_table)?;
let material_orientation_angles = read_axisym_material_orientation_angles(
"material_orientation_angles",
material_orientation_angles,
)?;
let material_orientation_angles =
(!material_orientation_angles.is_empty()).then_some(material_orientation_angles.as_slice());
let formulation =
physics::solenoid_stress::Structural2dFormulation::from_code(formulation, thickness)
.map_err(|msg| PyInteropError::ValueError { msg })?;
let operator = quad_mesh_stress_operator_for_element_type(
&nodes,
elements,
&element_indices,
&reference_points,
&material_ids,
&material_table,
material_orientation_angles,
element_type,
formulation,
)?;
sparse_operator_to_py(py, operator)
}
#[pyfunction]
fn filament_helix_path(
path: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), helix_start_offset: (f64, f64, f64),
twist_pitch: f64,
angle_offset: f64,
mut out: (
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
),
) -> PyResult<()> {
_3tup_slice_ro!(path);
_3tup_slice_mut!(out);
match mesh::filament_helix_path(
path,
[
helix_start_offset.0,
helix_start_offset.1,
helix_start_offset.2,
],
twist_pitch,
angle_offset,
out,
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Ok(())
}
#[pyfunction]
fn rotate_filaments_about_path(
path: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), angle_offset: f64,
mut out: (
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
PyReadwriteArray1<f64>,
),
) -> PyResult<()> {
_3tup_slice_ro!(path);
_3tup_slice_mut!(out);
match mesh::rotate_filaments_about_path(path, angle_offset, out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Ok(())
}
#[pyfunction]
fn flux_circular_filament(
current: PyReadonlyArray1<f64>,
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
rprime: PyReadonlyArray1<f64>,
zprime: PyReadonlyArray1<f64>,
par: bool,
) -> PyResult<Py<PyArray1<f64>>> {
let rzifil = (rfil, zfil, current);
_3tup_slice_ro!(rzifil);
let obs = (rprime, zprime);
_2tup_slice_ro!(obs);
let mut psi = vec![0.0; obs.0.len()];
let func = match par {
true => physics::circular_filament::flux_circular_filament_par,
false => physics::circular_filament::flux_circular_filament,
};
match func(rzifil, obs, &mut psi[..]) {
Ok(_) => {}
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| {
Ok(PyArray1::from_vec(py, psi).unbind()) })
}
#[pyfunction]
fn vector_potential_circular_filament(
current: PyReadonlyArray1<f64>,
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
rprime: PyReadonlyArray1<f64>,
zprime: PyReadonlyArray1<f64>,
par: bool,
) -> PyResult<Py<PyArray1<f64>>> {
let rzifil = (rfil, zfil, current);
_3tup_slice_ro!(rzifil);
let obs = (rprime, zprime);
_2tup_slice_ro!(obs);
let mut out = vec![0.0; obs.0.len()];
let func = match par {
true => physics::circular_filament::vector_potential_circular_filament_par,
false => physics::circular_filament::vector_potential_circular_filament,
};
match func(rzifil, obs, &mut out[..]) {
Ok(_) => {}
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| {
Ok(PyArray1::from_vec(py, out).unbind()) })
}
#[pyfunction]
fn flux_density_circular_filament(
current: PyReadonlyArray1<f64>,
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
rprime: PyReadonlyArray1<f64>,
zprime: PyReadonlyArray1<f64>,
par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let rzifil = (rfil, zfil, current);
_3tup_slice_ro!(rzifil);
let obs = (rprime, zprime);
_2tup_slice_ro!(obs);
let n = obs.0.len();
let (mut br, mut bz) = (vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::circular_filament::flux_density_circular_filament_par,
false => physics::circular_filament::flux_density_circular_filament,
};
match func(rzifil, obs, (&mut br, &mut bz)) {
Ok(_) => {}
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| {
let br: Py<PyArray1<f64>> = PyArray1::from_slice(py, &br).unbind(); let bz: Py<PyArray1<f64>> = PyArray1::from_slice(py, &bz).unbind();
Ok((br, bz))
})
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, par=true))]
fn flux_density_linear_filament(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, wire_radius: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let wire_radius = wire_radius.as_slice()?;
let n = xyzp.0.len();
let (mut bx, mut by, mut bz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::linear_filament::flux_density_linear_filament_par,
false => physics::linear_filament::flux_density_linear_filament,
};
match func(
xyzp,
xyzfil,
dlxyzfil,
ifil,
wire_radius,
(&mut bx, &mut by, &mut bz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, par=true))]
fn flux_density_linear_filament_matrix(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, wire_radius: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let wire_radius = wire_radius.as_slice()?;
let nout = xyzp.0.len().checked_mul(xyzfil.0.len()).ok_or_else(|| {
PyInteropError::DimensionalityError {
msg: "Output size overflow in flux_density_linear_filament_matrix".to_string(),
}
})?;
let (mut bx, mut by, mut bz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::linear_filament::flux_density_linear_filament_matrix_par,
false => physics::linear_filament::flux_density_linear_filament_matrix,
};
match func(
xyzp,
xyzfil,
dlxyzfil,
ifil,
wire_radius,
(&mut bx, &mut by, &mut bz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction]
fn flux_density_point_segment(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let n = xyzp.0.len();
let (mut bx, mut by, mut bz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::point_source::segment::flux_density_point_segment_par,
false => physics::point_source::segment::flux_density_point_segment,
};
match func(xyzp, xyzfil, dlxyzfil, ifil, (&mut bx, &mut by, &mut bz)) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, par=true))]
fn vector_potential_linear_filament(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, wire_radius: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let wire_radius = wire_radius.as_slice()?;
let n = xyzp.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::linear_filament::vector_potential_linear_filament_par,
false => physics::linear_filament::vector_potential_linear_filament,
};
match func(
xyzp,
xyzfil,
dlxyzfil,
ifil,
wire_radius,
(&mut outx, &mut outy, &mut outz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction(signature = (xyzp, xyzfil, dlxyzfil, ifil, wire_radius, par=true))]
fn vector_potential_linear_filament_matrix(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, wire_radius: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let wire_radius = wire_radius.as_slice()?;
let nout = xyzp.0.len().checked_mul(xyzfil.0.len()).ok_or_else(|| {
PyInteropError::DimensionalityError {
msg: "Output size overflow in vector_potential_linear_filament_matrix".to_string(),
}
})?;
let (mut outx, mut outy, mut outz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::linear_filament::vector_potential_linear_filament_matrix_par,
false => physics::linear_filament::vector_potential_linear_filament_matrix,
};
match func(
xyzp,
xyzfil,
dlxyzfil,
ifil,
wire_radius,
(&mut outx, &mut outy, &mut outz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction]
fn vector_potential_point_segment(
xyzp: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzp);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
let n = xyzp.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::point_source::segment::vector_potential_point_segment_par,
false => physics::point_source::segment::vector_potential_point_segment,
};
match func(
xyzp,
xyzfil,
dlxyzfil,
ifil,
(&mut outx, &mut outy, &mut outz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction(signature = (xyzfil0, dlxyzfil0, xyzfil1, dlxyzfil1, wire_radius))]
fn inductance_piecewise_linear_filaments(
xyzfil0: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil0: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil1: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil1: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), wire_radius: PyReadonlyArray1<f64>, ) -> PyResult<f64> {
_3tup_slice_ro!(xyzfil0);
_3tup_slice_ro!(dlxyzfil0);
_3tup_slice_ro!(xyzfil1);
_3tup_slice_ro!(dlxyzfil1);
let wire_radius = wire_radius.as_slice()?;
let inductance = match physics::linear_filament::inductance_piecewise_linear_filaments(
xyzfil0,
dlxyzfil0,
xyzfil1,
dlxyzfil1,
wire_radius,
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
Ok(inductance)
}
#[pyfunction(signature = (xyzfil_tgt, dlxyzfil_tgt, xyzfil_src, dlxyzfil_src, wire_radius_src))]
fn inductance_linear_filaments(
py: Python<'_>,
xyzfil_tgt: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil_tgt: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil_src: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil_src: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), wire_radius_src: PyReadonlyArray1<f64>, ) -> PyResult<Py<PyArray1<f64>>> {
_3tup_slice_ro!(xyzfil_tgt);
_3tup_slice_ro!(dlxyzfil_tgt);
_3tup_slice_ro!(xyzfil_src);
_3tup_slice_ro!(dlxyzfil_src);
let wire_radius_src = wire_radius_src.as_slice()?;
let ntgt = xyzfil_tgt.0.len();
let mut out = vec![0.0; ntgt];
match physics::linear_filament::inductance_linear_filaments(
xyzfil_tgt,
dlxyzfil_tgt,
xyzfil_src,
dlxyzfil_src,
wire_radius_src,
&mut out,
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
Ok(PyArray1::from_vec(py, out).unbind())
}
#[pyfunction(signature = (xyzfil_tgt, dlxyzfil_tgt, xyzfil_src, dlxyzfil_src, wire_radius_src, par=true))]
fn inductance_linear_filaments_matrix(
py: Python<'_>,
xyzfil_tgt: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil_tgt: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), xyzfil_src: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil_src: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), wire_radius_src: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<Py<PyArray1<f64>>> {
_3tup_slice_ro!(xyzfil_tgt);
_3tup_slice_ro!(dlxyzfil_tgt);
_3tup_slice_ro!(xyzfil_src);
_3tup_slice_ro!(dlxyzfil_src);
let wire_radius_src = wire_radius_src.as_slice()?;
let nout = xyzfil_tgt
.0
.len()
.checked_mul(xyzfil_src.0.len())
.ok_or_else(|| PyInteropError::DimensionalityError {
msg: "Output size overflow in inductance_linear_filaments_matrix".to_string(),
})?;
let mut out = vec![0.0; nout];
let func = match par {
true => physics::linear_filament::inductance_linear_filaments_matrix_par,
false => physics::linear_filament::inductance_linear_filaments_matrix,
};
match func(
xyzfil_tgt,
dlxyzfil_tgt,
xyzfil_src,
dlxyzfil_src,
wire_radius_src,
&mut out,
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
Ok(PyArray1::from_vec(py, out).unbind())
}
#[pyfunction]
fn gs_operator_order2(
rs: PyReadonlyArray1<f64>,
zs: PyReadonlyArray1<f64>,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<usize>>, Py<PyArray1<usize>>)> {
let rs = rs.as_slice()?;
let zs = zs.as_slice()?;
let (vals, rows, cols) = physics::gradshafranov::gs_operator_order2(rs, zs);
_3tup_ret!((vals, f64), (rows, usize), (cols, usize))
}
#[pyfunction]
fn gs_operator_order4(
rs: PyReadonlyArray1<f64>,
zs: PyReadonlyArray1<f64>,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<usize>>, Py<PyArray1<usize>>)> {
let rs = rs.as_slice()?;
let zs = zs.as_slice()?;
let (vals, rows, cols) = physics::gradshafranov::gs_operator_order4(rs, zs);
_3tup_ret!((vals, f64), (rows, usize), (cols, usize))
}
#[pyfunction]
fn ellipe(x: f64) -> f64 {
math::ellipe(x)
}
#[pyfunction]
fn ellipk(x: f64) -> f64 {
math::ellipk(x)
}
#[pyfunction]
fn flux_density_circular_filament_cartesian(
current: PyReadonlyArray1<f64>,
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
xyzobs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let rzifil = (rfil, zfil, current);
_3tup_slice_ro!(rzifil);
let (rfil, zfil, current) = rzifil;
_3tup_slice_ro!(xyzobs);
let n = xyzobs.0.len();
let (mut bx, mut by, mut bz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::circular_filament::flux_density_circular_filament_cartesian_par,
false => physics::circular_filament::flux_density_circular_filament_cartesian,
};
match func(
(&rfil, &zfil, ¤t),
xyzobs,
(&mut bx, &mut by, &mut bz),
) {
Ok(_) => {}
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction]
fn mutual_inductance_circular_to_linear(
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
nfil: PyReadonlyArray1<f64>,
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), par: bool,
) -> PyResult<f64> {
let rznfil = (rfil, zfil, nfil);
_3tup_slice_ro!(rznfil);
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let func = match par {
true => physics::circular_filament::mutual_inductance_circular_to_linear_par,
false => physics::circular_filament::mutual_inductance_circular_to_linear,
};
let m = match func(rznfil, xyzfil, dlxyzfil) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
Ok(m)
}
#[pyfunction]
fn flux_density_dipole(
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), moment: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), outer_radius: PyReadonlyArray1<f64>,
par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(loc);
_3tup_slice_ro!(moment);
_3tup_slice_ro!(obs);
let n = obs.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::point_source::flux_density_dipole_par,
false => physics::point_source::flux_density_dipole,
};
match func(
loc,
moment,
outer_radius.as_slice()?,
obs,
(&mut outx, &mut outy, &mut outz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction]
fn vector_potential_dipole(
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), moment: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), outer_radius: PyReadonlyArray1<f64>,
par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(loc);
_3tup_slice_ro!(moment);
_3tup_slice_ro!(obs);
let n = obs.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::point_source::vector_potential_dipole_par,
false => physics::point_source::vector_potential_dipole,
};
match func(
loc,
moment,
outer_radius.as_slice()?,
obs,
(&mut outx, &mut outy, &mut outz),
) {
Ok(x) => x,
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction]
fn body_force_density_circular_filament_cartesian(
current: PyReadonlyArray1<f64>,
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), j: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let rzifil = (rfil, zfil, current);
_3tup_slice_ro!(rzifil);
let (rfil, zfil, current) = rzifil;
_3tup_slice_ro!(obs);
_3tup_slice_ro!(j);
let func = match par {
true => physics::circular_filament::body_force_density_circular_filament_cartesian_par,
false => physics::circular_filament::body_force_density_circular_filament_cartesian,
};
let n = obs.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let out = (&mut outx[..], &mut outy[..], &mut outz[..]);
match func((&rfil, &zfil, ¤t), obs, j, out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
#[pyfunction]
#[pyo3(signature = (xyzfil, dlxyzfil, ifil, obs, j, wire_radius, par=true))]
fn body_force_density_linear_filament(
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), ifil: PyReadonlyArray1<f64>, obs: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), j: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
), wire_radius: PyReadonlyArray1<f64>, par: bool,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let ifil = ifil.as_slice()?;
_3tup_slice_ro!(obs);
_3tup_slice_ro!(j);
let wire_radius = wire_radius.as_slice()?;
let func = match par {
true => physics::linear_filament::body_force_density_linear_filament_par,
false => physics::linear_filament::body_force_density_linear_filament,
};
let n = obs.0.len();
let (mut outx, mut outy, mut outz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let out = (&mut outx[..], &mut outy[..], &mut outz[..]);
match func(xyzfil, dlxyzfil, ifil, wire_radius, obs, j, out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
};
_3tup_ret!((outx, f64), (outy, f64), (outz, f64))
}
fn triangle_mesh_view<'a>(
nodes: &'a (Vec<f64>, Vec<f64>, Vec<f64>),
triangles: &'a (Vec<usize>, Vec<usize>, Vec<usize>),
) -> PyResult<mesh::TriangleMeshView<'a>> {
mesh::TriangleMeshView::new(
(&nodes.0, &nodes.1, &nodes.2),
(&triangles.0, &triangles.1, &triangles.2),
)
.map_err(|msg| {
PyInteropError::ValueError {
msg: msg.to_string(),
}
.into()
})
}
fn borrowed_triangle_mesh_view<'a>(
nodes: &'a Matrix3Input<'a, f64>,
triangles: &'a Matrix3Input<'a, i64>,
) -> PyResult<mesh::TriangleMeshView<'a>> {
mesh::TriangleMeshView::from_row_major_i64(nodes.as_slice(), triangles.as_slice()).map_err(
|msg| {
PyInteropError::ValueError {
msg: msg.to_string(),
}
.into()
},
)
}
#[pyfunction(signature = (obs, nodes, triangles, s, par=true, quad="dunavant3"))]
fn flux_density_triangle_mesh(
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let obs = split_xyz_array2("obs", obs)?;
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let s = s.as_slice()?;
let quad = parse_triangle_quadrature(&quad)?;
let n = obs.0.len();
let (mut bx, mut by, mut bz) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::boundary_element::flux_density_triangle_mesh_par,
false => physics::boundary_element::flux_density_triangle_mesh,
};
match func(
(&obs.0, &obs.1, &obs.2),
&mesh,
s,
quad,
(&mut bx, &mut by, &mut bz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction(signature = (obs, nodes, triangles, s, par=true, quad="dunavant3"))]
fn vector_potential_triangle_mesh(
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let obs = split_xyz_array2("obs", obs)?;
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let s = s.as_slice()?;
let quad = parse_triangle_quadrature(&quad)?;
let n = obs.0.len();
let (mut ax, mut ay, mut az) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
let func = match par {
true => physics::boundary_element::vector_potential_triangle_mesh_par,
false => physics::boundary_element::vector_potential_triangle_mesh,
};
match func(
(&obs.0, &obs.1, &obs.2),
&mesh,
s,
quad,
(&mut ax, &mut ay, &mut az),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((ax, f64), (ay, f64), (az, f64))
}
#[pyfunction(signature = (obs, nodes, triangles, par=true, quad="dunavant3"))]
fn flux_density_triangle_mesh_mapping(
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let obs = split_xyz_array2("obs", obs)?;
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let quad = parse_triangle_quadrature(&quad)?;
let nout =
obs.0
.len()
.checked_mul(mesh.nnode())
.ok_or(PyInteropError::DimensionalityError {
msg: "Flux-density mapping size overflow".to_string(),
})?;
let (mut bx, mut by, mut bz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::flux_density_triangle_mesh_mapping_par,
false => physics::boundary_element::flux_density_triangle_mesh_mapping,
};
match func(
(&obs.0, &obs.1, &obs.2),
&mesh,
quad,
(&mut bx, &mut by, &mut bz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((bx, f64), (by, f64), (bz, f64))
}
#[pyfunction(signature = (obs, nodes, triangles, par=true, quad="dunavant3"))]
fn vector_potential_triangle_mesh_mapping(
obs: PyReadonlyArray2<f64>,
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let obs = split_xyz_array2("obs", obs)?;
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let quad = parse_triangle_quadrature(&quad)?;
let nout =
obs.0
.len()
.checked_mul(mesh.nnode())
.ok_or(PyInteropError::DimensionalityError {
msg: "Vector-potential mapping size overflow".to_string(),
})?;
let (mut ax, mut ay, mut az) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::vector_potential_triangle_mesh_mapping_par,
false => physics::boundary_element::vector_potential_triangle_mesh_mapping,
};
match func(
(&obs.0, &obs.1, &obs.2),
&mesh,
quad,
(&mut ax, &mut ay, &mut az),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((ax, f64), (ay, f64), (az, f64))
}
#[pyfunction]
fn triangle_mesh_current_density(
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let s = s.as_slice()?;
let ntri = mesh.len();
let (mut jx, mut jy, mut jz) = (vec![0.0; ntri], vec![0.0; ntri], vec![0.0; ntri]);
match physics::boundary_element::triangle_mesh_current_density(
&mesh,
s,
(&mut jx, &mut jy, &mut jz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((jx, f64), (jy, f64), (jz, f64))
}
#[pyfunction(signature = (nodes, triangles, quad="dunavant3"))]
fn triangle_mesh_quadrature_points(
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
quad: &str,
) -> PyResult<(
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
Py<PyArray1<f64>>,
usize,
)> {
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let quad = parse_triangle_quadrature(&quad)?;
let ntri = mesh.len();
let nqp = physics::boundary_element::triangle_quadrature_count(quad);
let nout = ntri * nqp;
let (mut xq, mut yq, mut zq, mut wq) = (
vec![0.0; nout],
vec![0.0; nout],
vec![0.0; nout],
vec![0.0; nout],
);
match physics::boundary_element::triangle_mesh_quadrature_points(
&mesh,
quad,
(&mut xq, &mut yq, &mut zq),
&mut wq,
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| {
Ok((
PyArray1::from_vec(py, xq).unbind(),
PyArray1::from_vec(py, yq).unbind(),
PyArray1::from_vec(py, zq).unbind(),
PyArray1::from_vec(py, wq).unbind(),
nqp,
))
})
}
#[pyfunction(signature = (nodes, triangles, par=true, quad="dunavant3"))]
fn triangle_mesh_inductance_matrix(
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<Py<PyArray1<f64>>> {
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let quad = parse_triangle_quadrature(&quad)?;
let nnode = mesh.nnode();
let nout = nnode
.checked_mul(nnode)
.ok_or(PyInteropError::DimensionalityError {
msg: "Inductance matrix size overflow".to_string(),
})?;
let mut out = vec![0.0; nout];
let func = match par {
true => physics::boundary_element::triangle_mesh_inductance_matrix_par,
false => physics::boundary_element::triangle_mesh_inductance_matrix,
};
match func(&mesh, quad, &mut out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| Ok(PyArray1::from_vec(py, out).unbind()))
}
#[pyfunction(signature = (xyzfil, dlxyzfil, wire_radius, nodes_tgt, triangles_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_inductance_mapping_from_linear_filaments(
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
wire_radius: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<Py<PyArray1<f64>>> {
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let wire_radius = wire_radius.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let quad = parse_triangle_quadrature(&quad)?;
let nout = mesh_tgt.nnode().checked_mul(xyzfil.0.len()).ok_or(
PyInteropError::DimensionalityError {
msg: "Inductance mapping size overflow".to_string(),
},
)?;
let mut out = vec![0.0; nout];
let func = match par {
true => {
physics::boundary_element::triangle_mesh_inductance_mapping_from_linear_filaments_par
}
false => physics::boundary_element::triangle_mesh_inductance_mapping_from_linear_filaments,
};
match func(xyzfil, dlxyzfil, wire_radius, &mesh_tgt, quad, &mut out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| Ok(PyArray1::from_vec(py, out).unbind()))
}
#[pyfunction(signature = (rfil, zfil, nodes_tgt, triangles_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_inductance_mapping_from_circular_filaments(
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<Py<PyArray1<f64>>> {
let rfil = rfil.as_slice()?;
let zfil = zfil.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let quad = parse_triangle_quadrature(&quad)?;
let nout =
mesh_tgt
.nnode()
.checked_mul(rfil.len())
.ok_or(PyInteropError::DimensionalityError {
msg: "Inductance mapping size overflow".to_string(),
})?;
let mut out = vec![0.0; nout];
let func = match par {
true => {
physics::boundary_element::triangle_mesh_inductance_mapping_from_circular_filaments_par
}
false => {
physics::boundary_element::triangle_mesh_inductance_mapping_from_circular_filaments
}
};
match func(rfil, zfil, &mesh_tgt, quad, &mut out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| Ok(PyArray1::from_vec(py, out).unbind()))
}
#[pyfunction(signature = (loc, moment_dir, outer_radius, nodes_tgt, triangles_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_flux_linkage_mapping_from_dipoles(
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
moment_dir: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
outer_radius: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
par: bool,
quad: &str,
) -> PyResult<Py<PyArray1<f64>>> {
_3tup_slice_ro!(loc);
_3tup_slice_ro!(moment_dir);
let outer_radius = outer_radius.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let quad = parse_triangle_quadrature(&quad)?;
let nout =
mesh_tgt
.nnode()
.checked_mul(loc.0.len())
.ok_or(PyInteropError::DimensionalityError {
msg: "Flux-linkage mapping size overflow".to_string(),
})?;
let mut out = vec![0.0; nout];
let func = match par {
true => physics::boundary_element::triangle_mesh_flux_linkage_mapping_from_dipoles_par,
false => physics::boundary_element::triangle_mesh_flux_linkage_mapping_from_dipoles,
};
match func(loc, moment_dir, outer_radius, &mesh_tgt, quad, &mut out) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
Python::attach(|py| Ok(PyArray1::from_vec(py, out).unbind()))
}
#[pyfunction(signature = (nodes_src, triangles_src, nodes_tgt, triangles_tgt, s_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_force_mapping(
nodes_src: PyReadonlyArray2<f64>,
triangles_src: PyReadonlyArray2<i64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
s_tgt: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let nodes_src = split_xyz_array2("nodes_src", nodes_src)?;
let triangles_src = split_triangle_index_array2("triangles_src", triangles_src)?;
let mesh_src = triangle_mesh_view(&nodes_src, &triangles_src)?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let s_tgt = s_tgt.as_slice()?;
let quad = parse_triangle_quadrature(quad)?;
let nnode_src = mesh_src.nnode();
let ntri_tgt = mesh_tgt.len();
let nout = nnode_src
.checked_mul(ntri_tgt)
.ok_or(PyInteropError::DimensionalityError {
msg: "Force mapping size overflow".to_string(),
})?;
let (mut fx, mut fy, mut fz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::triangle_mesh_force_mapping_par,
false => physics::boundary_element::triangle_mesh_force_mapping,
};
match func(
&mesh_src,
&mesh_tgt,
s_tgt,
quad,
(&mut fx, &mut fy, &mut fz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((fx, f64), (fy, f64), (fz, f64))
}
#[pyfunction(signature = (nodes, triangles, s, par=true, quad="dunavant3"))]
fn triangle_mesh_self_force_mapping(
nodes: PyReadonlyArray2<f64>,
triangles: PyReadonlyArray2<i64>,
s: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let nodes = split_xyz_array2("nodes", nodes)?;
let triangles = split_triangle_index_array2("triangles", triangles)?;
let mesh = triangle_mesh_view(&nodes, &triangles)?;
let s = s.as_slice()?;
let quad = parse_triangle_quadrature(quad)?;
let nnode = mesh.nnode();
let ntri = mesh.len();
let nout = nnode
.checked_mul(ntri)
.ok_or(PyInteropError::DimensionalityError {
msg: "Force mapping size overflow".to_string(),
})?;
let (mut fx, mut fy, mut fz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::triangle_mesh_self_force_mapping_par,
false => physics::boundary_element::triangle_mesh_self_force_mapping,
};
match func(&mesh, s, quad, (&mut fx, &mut fy, &mut fz)) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((fx, f64), (fy, f64), (fz, f64))
}
#[pyfunction(signature = (xyzfil, dlxyzfil, wire_radius, nodes_tgt, triangles_tgt, s_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_force_mapping_from_linear_filaments(
xyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
dlxyzfil: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
wire_radius: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
s_tgt: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(xyzfil);
_3tup_slice_ro!(dlxyzfil);
let wire_radius = wire_radius.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let s_tgt = s_tgt.as_slice()?;
let quad = parse_triangle_quadrature(quad)?;
let nfil = xyzfil.0.len();
let ntri_tgt = mesh_tgt.len();
let nout = nfil
.checked_mul(ntri_tgt)
.ok_or(PyInteropError::DimensionalityError {
msg: "Force mapping size overflow".to_string(),
})?;
let (mut fx, mut fy, mut fz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::triangle_mesh_force_mapping_from_linear_filaments_par,
false => physics::boundary_element::triangle_mesh_force_mapping_from_linear_filaments,
};
match func(
xyzfil,
dlxyzfil,
wire_radius,
&mesh_tgt,
s_tgt,
quad,
(&mut fx, &mut fy, &mut fz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((fx, f64), (fy, f64), (fz, f64))
}
#[pyfunction(signature = (rfil, zfil, nodes_tgt, triangles_tgt, s_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_force_mapping_from_circular_filaments(
rfil: PyReadonlyArray1<f64>,
zfil: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
s_tgt: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
let rfil = rfil.as_slice()?;
let zfil = zfil.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let s_tgt = s_tgt.as_slice()?;
let quad = parse_triangle_quadrature(quad)?;
let nfil = rfil.len();
let ntri_tgt = mesh_tgt.len();
let nout = nfil
.checked_mul(ntri_tgt)
.ok_or(PyInteropError::DimensionalityError {
msg: "Force mapping size overflow".to_string(),
})?;
let (mut fx, mut fy, mut fz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::triangle_mesh_force_mapping_from_circular_filaments_par,
false => physics::boundary_element::triangle_mesh_force_mapping_from_circular_filaments,
};
match func(
rfil,
zfil,
&mesh_tgt,
s_tgt,
quad,
(&mut fx, &mut fy, &mut fz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((fx, f64), (fy, f64), (fz, f64))
}
#[pyfunction(signature = (loc, moment_dir, outer_radius, nodes_tgt, triangles_tgt, s_tgt, par=true, quad="dunavant3"))]
fn triangle_mesh_force_mapping_from_dipoles(
loc: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
moment_dir: (
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
PyReadonlyArray1<f64>,
),
outer_radius: PyReadonlyArray1<f64>,
nodes_tgt: PyReadonlyArray2<f64>,
triangles_tgt: PyReadonlyArray2<i64>,
s_tgt: PyReadonlyArray1<f64>,
par: bool,
quad: &str,
) -> PyResult<(Py<PyArray1<f64>>, Py<PyArray1<f64>>, Py<PyArray1<f64>>)> {
_3tup_slice_ro!(loc);
_3tup_slice_ro!(moment_dir);
let outer_radius = outer_radius.as_slice()?;
let nodes_tgt = split_xyz_array2("nodes_tgt", nodes_tgt)?;
let triangles_tgt = split_triangle_index_array2("triangles_tgt", triangles_tgt)?;
let mesh_tgt = triangle_mesh_view(&nodes_tgt, &triangles_tgt)?;
let s_tgt = s_tgt.as_slice()?;
let quad = parse_triangle_quadrature(quad)?;
let ndip = loc.0.len();
let ntri_tgt = mesh_tgt.len();
let nout = ndip
.checked_mul(ntri_tgt)
.ok_or(PyInteropError::DimensionalityError {
msg: "Force mapping size overflow".to_string(),
})?;
let (mut fx, mut fy, mut fz) = (vec![0.0; nout], vec![0.0; nout], vec![0.0; nout]);
let func = match par {
true => physics::boundary_element::triangle_mesh_force_mapping_from_dipoles_par,
false => physics::boundary_element::triangle_mesh_force_mapping_from_dipoles,
};
match func(
loc,
moment_dir,
outer_radius,
&mesh_tgt,
s_tgt,
quad,
(&mut fx, &mut fy, &mut fz),
) {
Ok(_) => (),
Err(x) => {
let err: PyErr = PyInteropError::DimensionalityError { msg: x.to_string() }.into();
return Err(err);
}
}
_3tup_ret!((fx, f64), (fy, f64), (fz, f64))
}
#[pymodule]
#[pyo3(name = "cfsem")]
fn _cfsem<'py>(_py: Python, m: Bound<'py, PyModule>) -> PyResult<()> {
m.add("DimensionalityError", _py.get_type::<DimensionalityError>())?;
m.add_function(wrap_pyfunction!(flux_circular_filament, m.clone())?)?;
m.add_function(wrap_pyfunction!(flux_density_circular_filament, m.clone())?)?;
m.add_function(wrap_pyfunction!(
flux_density_circular_filament_cartesian,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_circular_filament,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
mutual_inductance_circular_to_linear,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
body_force_density_circular_filament_cartesian,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(flux_density_linear_filament, m.clone())?)?;
m.add_function(wrap_pyfunction!(flux_density_triangle_mesh, m.clone())?)?;
m.add_function(wrap_pyfunction!(flux_density_point_segment, m.clone())?)?;
m.add_function(wrap_pyfunction!(triangle_mesh_current_density, m.clone())?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_quadrature_points,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(triangle_mesh_force_mapping, m.clone())?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_self_force_mapping,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_force_mapping_from_linear_filaments,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_force_mapping_from_circular_filaments,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_force_mapping_from_dipoles,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_inductance_matrix,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_inductance_mapping_from_linear_filaments,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_inductance_mapping_from_circular_filaments,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
triangle_mesh_flux_linkage_mapping_from_dipoles,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
flux_density_linear_filament_matrix,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_linear_filament,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(vector_potential_triangle_mesh, m.clone())?)?;
m.add_function(wrap_pyfunction!(
flux_density_triangle_mesh_mapping,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_triangle_mesh_mapping,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(vector_potential_point_segment, m.clone())?)?;
m.add_function(wrap_pyfunction!(
vector_potential_linear_filament_matrix,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
inductance_piecewise_linear_filaments,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(inductance_linear_filaments, m.clone())?)?;
m.add_function(wrap_pyfunction!(
inductance_linear_filaments_matrix,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
body_force_density_linear_filament,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(gs_operator_order2, m.clone())?)?;
m.add_function(wrap_pyfunction!(gs_operator_order4, m.clone())?)?;
m.add_function(wrap_pyfunction!(ellipe, m.clone())?)?;
m.add_function(wrap_pyfunction!(ellipk, m.clone())?)?;
m.add_function(wrap_pyfunction!(filament_helix_path, m.clone())?)?;
m.add_function(wrap_pyfunction!(rotate_filaments_about_path, m.clone())?)?;
m.add_function(wrap_pyfunction!(flux_density_dipole, m.clone())?)?;
m.add_function(wrap_pyfunction!(vector_potential_dipole, m.clone())?)?;
m.add_function(wrap_pyfunction!(
flux_density_dipole_hierarchical,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_dipole_hierarchical,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
flux_density_linear_filament_hierarchical,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_linear_filament_hierarchical,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
flux_density_triangle_mesh_hierarchical,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
vector_potential_triangle_mesh_hierarchical,
m.clone()
)?)?;
m.add_class::<SolveResult>()?;
m.add_class::<HierarchicalDiagnostics>()?;
m.add_class::<SolenoidStress2dModelF64>()?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_assemble_model_2d_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_isotropic_axisymmetric_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_isotropic_plane_strain_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_isotropic_axisymmetric_thermal_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_isotropic_plane_strain_thermal_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_orthotropic_axisymmetric_thermal_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_cfsem_radial_material_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_infer_quad9_mesh_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_quad_mesh_query_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_quad_mesh_interpolation_operator_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_quad_mesh_strain_operator_f64,
m.clone()
)?)?;
m.add_function(wrap_pyfunction!(
solenoid_stress_fem_quad_mesh_stress_operator_f64,
m.clone()
)?)?;
Ok(())
}
macro_rules! _3tup_slice_ro {
($x:ident) => {
let $x = ($x.0.as_slice()?, $x.1.as_slice()?, $x.2.as_slice()?);
};
}
macro_rules! _2tup_slice_ro {
($x:ident) => {
let $x = ($x.0.as_slice()?, $x.1.as_slice()?);
};
}
macro_rules! _3tup_slice_mut {
($x:ident) => {
let $x = (
$x.0.as_slice_mut()?,
$x.1.as_slice_mut()?,
$x.2.as_slice_mut()?,
);
};
}
macro_rules! _3tup_ret {
(($x:ident, $xt:ty), ($y:ident, $yt:ty), ($z:ident, $zt:ty)) => {
Python::attach(|py| {
let $x: Py<PyArray1<$xt>> = PyArray1::from_vec(py, $x).unbind();
let $y: Py<PyArray1<$yt>> = PyArray1::from_vec(py, $y).unbind();
let $z: Py<PyArray1<$zt>> = PyArray1::from_vec(py, $z).unbind();
Ok(($x, $y, $z))
})
};
}
pub(crate) use _2tup_slice_ro;
pub(crate) use _3tup_ret;
pub(crate) use _3tup_slice_mut;
pub(crate) use _3tup_slice_ro;