use super::functions::*;
use std::f64::consts::PI;
pub struct ContactPatch {
pub radii: Vec<f64>,
pub pressures: Vec<f64>,
pub contact_radius: f64,
}
impl ContactPatch {
pub fn hertzian(contact_radius: f64, max_pressure: f64, n_points: usize) -> Self {
let mut radii = Vec::with_capacity(n_points);
let mut pressures = Vec::with_capacity(n_points);
for i in 0..n_points {
let r = contact_radius * (i as f64) / (n_points as f64 - 1.0).max(1.0);
let ratio = r / contact_radius;
let p = if ratio <= 1.0 {
max_pressure * (1.0 - ratio * ratio).max(0.0).sqrt()
} else {
0.0
};
radii.push(r);
pressures.push(p);
}
Self {
radii,
pressures,
contact_radius,
}
}
pub fn total_force(&self) -> f64 {
let n = self.radii.len();
if n < 2 {
return 0.0;
}
let mut force = 0.0;
for i in 1..n {
let dr = self.radii[i] - self.radii[i - 1];
let r_mid = 0.5 * (self.radii[i] + self.radii[i - 1]);
let p_mid = 0.5 * (self.pressures[i] + self.pressures[i - 1]);
force += 2.0 * PI * r_mid * p_mid * dr;
}
force
}
pub fn mean_pressure(&self) -> f64 {
let area = PI * self.contact_radius * self.contact_radius;
if area < 1e-30 {
return 0.0;
}
self.total_force() / area
}
pub fn max_pressure(&self) -> f64 {
self.pressures.iter().cloned().fold(0.0f64, f64::max)
}
}
pub struct ElasticSolid {
pub youngs_modulus: f64,
pub poisson_ratio: f64,
}
impl ElasticSolid {
pub fn shear_modulus(&self) -> f64 {
self.youngs_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn bulk_modulus(&self) -> f64 {
self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
}
pub fn reduced_modulus(s1: &ElasticSolid, s2: &ElasticSolid) -> f64 {
let inv = (1.0 - s1.poisson_ratio * s1.poisson_ratio) / s1.youngs_modulus
+ (1.0 - s2.poisson_ratio * s2.poisson_ratio) / s2.youngs_modulus;
1.0 / inv
}
pub fn plane_strain_modulus(&self) -> f64 {
self.youngs_modulus / (1.0 - self.poisson_ratio * self.poisson_ratio)
}
}
pub struct FrettingContact {
pub mindlin: MindlinContact,
pub cycles: u64,
pub amplitude: f64,
}
impl FrettingContact {
pub fn new(mindlin: MindlinContact, amplitude: f64) -> Self {
Self {
mindlin,
cycles: 0,
amplitude,
}
}
pub fn total_dissipated_energy(&self, normal_force: f64) -> f64 {
let per_cycle = self
.mindlin
.fretting_energy_per_cycle(self.amplitude, normal_force);
per_cycle * self.cycles as f64
}
pub fn advance_cycle(&mut self) {
self.cycles += 1;
}
pub fn advance_cycles(&mut self, n: u64) {
self.cycles += n;
}
pub fn wear_depth(
wear_coefficient: f64,
mean_pressure: f64,
sliding_distance_per_cycle: f64,
num_cycles: u64,
hardness: f64,
) -> f64 {
if hardness < 1e-30 {
return 0.0;
}
wear_coefficient * mean_pressure * sliding_distance_per_cycle * num_cycles as f64 / hardness
}
}
pub struct MaugisDugdaleContact {
pub effective_radius: f64,
pub reduced_modulus: f64,
pub work_of_adhesion: f64,
pub cohesive_stress: f64,
}
impl MaugisDugdaleContact {
pub fn maugis_parameter(&self) -> f64 {
let denom = PI * self.work_of_adhesion * self.reduced_modulus * self.reduced_modulus;
if denom < 1e-60 {
return 0.0;
}
self.cohesive_stress * (self.effective_radius / denom).cbrt()
}
pub fn jkr_pull_off_force(&self) -> f64 {
-1.5 * PI * self.work_of_adhesion * self.effective_radius
}
pub fn dmt_pull_off_force(&self) -> f64 {
-2.0 * PI * self.work_of_adhesion * self.effective_radius
}
pub fn pull_off_force(&self) -> f64 {
let lambda = self.maugis_parameter();
let f_dmt = self.dmt_pull_off_force();
let f_jkr = self.jkr_pull_off_force();
f_dmt + (f_jkr - f_dmt) * (1.0 - (-lambda).exp())
}
pub fn zero_force_contact_radius(&self) -> f64 {
let lambda = self.maugis_parameter();
let a_dmt = (6.0 * PI * self.work_of_adhesion * self.effective_radius.powi(2)
/ self.reduced_modulus)
.cbrt();
let a_jkr = (9.0 * PI * self.work_of_adhesion * self.effective_radius.powi(2)
/ (4.0 * self.reduced_modulus))
.cbrt();
let t = (1.0 - (-lambda).exp()).clamp(0.0, 1.0);
a_dmt + t * (a_jkr - a_dmt)
}
}
pub struct RollingContact {
pub hertz: HertzContact,
pub rolling_radius: f64,
pub rolling_friction: f64,
}
impl RollingContact {
pub fn new(hertz: HertzContact, rolling_radius: f64, rolling_friction: f64) -> Self {
Self {
hertz,
rolling_radius,
rolling_friction,
}
}
pub fn rolling_resistance(&self, normal_force: f64) -> f64 {
self.rolling_friction * normal_force
}
pub fn rolling_torque(&self, normal_force: f64) -> f64 {
self.rolling_friction * normal_force * self.rolling_radius
}
pub fn creep_ratio(slip_velocity: f64, rolling_velocity: f64) -> f64 {
if rolling_velocity.abs() < 1e-30 {
return 0.0;
}
slip_velocity / rolling_velocity
}
pub fn carter_creep_force(&self, creep: f64, normal_force: f64, friction: f64) -> f64 {
let a = self.hertz.contact_radius(normal_force);
let g_star = self.hertz.solid_a.shear_modulus();
let xi_s = if g_star * a > 1e-30 {
3.0 * friction * normal_force / (8.0 * g_star * a)
} else {
1e-6
};
let xi = creep.abs();
let force = if xi < xi_s {
friction * normal_force * (1.0 - (1.0 - xi / xi_s).powi(2))
} else {
friction * normal_force
};
-force * creep.signum()
}
pub fn line_contact_half_width(force_per_length: f64, radius: f64, e_star: f64) -> f64 {
(4.0 * force_per_length * radius / (PI * e_star)).sqrt()
}
}
pub struct MindlinContact {
pub hertz: HertzContact,
pub friction: f64,
}
impl MindlinContact {
fn reduced_shear_modulus(&self) -> f64 {
let g1 = self.hertz.solid_a.shear_modulus();
let g2 = self.hertz.solid_b.shear_modulus();
let nu1 = self.hertz.solid_a.poisson_ratio;
let nu2 = self.hertz.solid_b.poisson_ratio;
let inv = (2.0 - nu1) / (4.0 * g1) + (2.0 - nu2) / (4.0 * g2);
1.0 / inv
}
pub fn tangential_stiffness(&self, normal_force: f64) -> f64 {
let g_star = self.reduced_shear_modulus();
let a = self.hertz.contact_radius(normal_force);
8.0 * g_star * a
}
pub fn tangential_compliance(&self, tangential_force: f64, normal_force: f64) -> Option<f64> {
let limit = self.friction * normal_force;
if tangential_force >= limit {
return None;
}
let g_star = self.reduced_shear_modulus();
let a = self.hertz.contact_radius(normal_force);
let prefactor = 3.0 * limit / (8.0 * g_star * a);
let term = 1.0 - (tangential_force / limit);
Some(prefactor * (1.0 - term.powf(2.0 / 3.0)))
}
pub fn partial_slip_force_disp(&self, delta_t: f64, normal_force: f64) -> f64 {
let limit = self.friction * normal_force;
let g_star = self.reduced_shear_modulus();
let a = self.hertz.contact_radius(normal_force);
let prefactor = 3.0 * limit / (8.0 * g_star * a);
let ratio = (delta_t / prefactor).min(1.0);
limit * (1.0 - (1.0 - ratio).powf(1.5))
}
pub fn stick_radius(&self, tangential_force: f64, normal_force: f64) -> Option<f64> {
let limit = self.friction * normal_force;
if tangential_force >= limit || limit < 1e-30 {
return None;
}
let a = self.hertz.contact_radius(normal_force);
Some(a * (1.0 - tangential_force / limit).powf(1.0 / 3.0))
}
pub fn fretting_energy_per_cycle(&self, tangential_amplitude: f64, normal_force: f64) -> f64 {
let limit = self.friction * normal_force;
if limit < 1e-30 {
return 0.0;
}
let k_t = self.tangential_stiffness(normal_force);
if k_t < 1e-30 {
return 0.0;
}
let ratio = (tangential_amplitude / limit).min(1.0);
(9.0 * limit * limit) / (10.0 * k_t) * (1.0 - (1.0 - ratio).powf(5.0 / 3.0))
}
}
pub struct HertzianStressField;
impl HertzianStressField {
pub fn subsurface_max_shear_stress(depth: f64, a: f64, p0: f64) -> f64 {
if a < f64::EPSILON {
return 0.0;
}
let xi = depth / a;
let atan_inv_xi = if xi < 1e-10 {
PI / 2.0
} else {
(1.0 / xi).atan()
};
let denom = (1.0 + xi * xi).sqrt();
0.5 * p0 * ((1.0 + 2.0 * xi * xi) * atan_inv_xi / denom - 3.0 * xi / 2.0 / denom)
}
pub fn max_shear_depth(a: f64) -> f64 {
0.48 * a
}
pub fn axial_normal_stress(depth: f64, a: f64, p0: f64) -> f64 {
if a < f64::EPSILON {
return 0.0;
}
let xi = depth / a;
-p0 / (1.0 + xi * xi)
}
pub fn von_mises_at_surface(r: f64, a: f64, p0: f64, nu: f64) -> f64 {
if a < f64::EPSILON || r > a {
return 0.0;
}
let rho = r / a;
let rho2 = rho * rho;
let sq = (1.0 - rho2).sqrt();
let sigma_z = -p0 * sq;
let sigma_r;
let sigma_theta;
if rho < f64::EPSILON {
sigma_r = p0 * (-(1.0 - 2.0 * nu) / 2.0 - 1.0);
sigma_theta = sigma_r;
} else {
let factor = (1.0 - 2.0 * nu) / (3.0 * rho2);
let inner = 1.0 - (1.0 - rho2).powf(1.5);
sigma_r = p0 * (factor * inner - sq);
sigma_theta = -p0 * (2.0 * nu * sq + (1.0 - 2.0 * nu) * (1.0 - inner / (rho2)));
let diff_rz = sigma_r - sigma_z;
let diff_zt = sigma_z - sigma_theta;
let diff_tr = sigma_theta - sigma_r;
return (0.5 * (diff_rz * diff_rz + diff_zt * diff_zt + diff_tr * diff_tr)).sqrt();
}
let diff_rz = sigma_r - sigma_z;
let diff_zt = sigma_z - sigma_theta;
let diff_tr = sigma_theta - sigma_r;
(0.5 * (diff_rz * diff_rz + diff_zt * diff_zt + diff_tr * diff_tr)).sqrt()
}
pub fn max_subsurface_von_mises(a: f64, p0: f64) -> f64 {
0.31 * p0 * if a > 0.0 { 1.0 } else { 0.0 }
}
}
pub struct ContactLoadDisplacement {
pub hertz: HertzContact,
pub loads: Vec<f64>,
}
impl ContactLoadDisplacement {
pub fn displacements(&self) -> Vec<f64> {
self.loads
.iter()
.map(|&f| self.hertz.hertz_indentation(f))
.collect()
}
pub fn contact_radii(&self) -> Vec<f64> {
self.loads
.iter()
.map(|&f| self.hertz.contact_radius(f))
.collect()
}
pub fn stiffnesses(&self) -> Vec<f64> {
self.loads
.iter()
.map(|&f| self.hertz.contact_stiffness(f))
.collect()
}
pub fn peak_load(&self) -> f64 {
self.loads.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
}
pub fn elastic_work(&self) -> f64 {
let disps = self.displacements();
let n = self.loads.len();
if n < 2 {
return 0.0;
}
let mut work = 0.0;
for i in 1..n {
let dd = disps[i] - disps[i - 1];
let f_mid = 0.5 * (self.loads[i] + self.loads[i - 1]);
work += f_mid * dd;
}
work
}
}
pub struct PenaltyContactConstraint {
pub penalty: f64,
pub friction_coef: f64,
}
impl PenaltyContactConstraint {
pub fn normal_force(gap: f64, penalty: f64) -> f64 {
if gap < 0.0 { -gap * penalty } else { 0.0 }
}
pub fn contact_force(
gap: f64,
tangential_disp: f64,
penalty_n: f64,
penalty_t: f64,
mu: f64,
) -> ([f64; 3], f64) {
let fn_ = Self::normal_force(gap, penalty_n);
let ft_trial = penalty_t * tangential_disp;
let limit = mu * fn_;
let ft = if ft_trial.abs() <= limit {
ft_trial
} else {
limit * ft_trial.signum()
};
([ft, 0.0, 0.0], fn_)
}
pub fn augmented_lagrangian_force(lambda: f64, gap: f64, epsilon: f64) -> f64 {
(lambda - epsilon * gap).max(0.0)
}
pub fn update_lagrange_multiplier(lambda: f64, gap: f64, epsilon: f64) -> f64 {
(lambda - epsilon * gap).max(0.0)
}
}
pub struct DmtContact {
pub hertz: HertzContact,
pub work_of_adhesion: f64,
}
impl DmtContact {
pub fn contact_radius(&self, normal_force: f64) -> f64 {
let r_star = self.hertz.effective_radius();
let e_star = ElasticSolid::reduced_modulus(&self.hertz.solid_a, &self.hertz.solid_b);
let f_eff = normal_force + 2.0 * PI * self.work_of_adhesion * r_star;
if f_eff <= 0.0 {
return 0.0;
}
(3.0 * f_eff * r_star / (4.0 * e_star)).cbrt()
}
pub fn pull_off_force(&self) -> f64 {
let r_star = self.hertz.effective_radius();
-2.0 * PI * self.work_of_adhesion * r_star
}
pub fn zero_load_contact_area(&self) -> f64 {
let a = self.contact_radius(0.0);
PI * a * a
}
pub fn pressure_at_zero_force(&self) -> f64 {
let a = self.contact_radius(0.0);
if a < 1e-30 {
return 0.0;
}
let f_eff = 2.0 * PI * self.work_of_adhesion * self.hertz.effective_radius();
3.0 * f_eff / (2.0 * PI * a * a)
}
}
pub struct RoughSurfaceContact {
pub asperity_density: f64,
pub asperity_radius: f64,
pub asperity_height_std: f64,
}
impl RoughSurfaceContact {
fn phi(&self, z: f64) -> f64 {
let sigma = self.asperity_height_std;
(-0.5 * (z / sigma).powi(2)).exp() / (sigma * (2.0 * PI).sqrt())
}
fn prob_contact(&self, separation: f64) -> f64 {
let s = self.asperity_height_std;
let t = separation / (s * 2.0_f64.sqrt());
0.5 * erfc(t)
}
fn mean_overlap(&self, separation: f64) -> f64 {
let s = self.asperity_height_std;
let t = separation / (s * 2.0_f64.sqrt());
s / (2.0 * PI).sqrt() * (-t * t).exp() - separation * 0.5 * erfc(t)
}
fn mean_overlap_3_2(&self, separation: f64) -> f64 {
let s = self.asperity_height_std;
let n = 200usize;
let lo = separation;
let hi = separation + 8.0 * s;
let dz = (hi - lo) / n as f64;
let mut sum = 0.0;
for i in 0..n {
let z = lo + (i as f64 + 0.5) * dz;
let overlap = z - separation;
sum += overlap.powf(1.5) * self.phi(z) * dz;
}
sum
}
pub fn contact_area(&self, separation: f64, _solid: &ElasticSolid) -> f64 {
PI * self.asperity_density * self.asperity_radius * self.mean_overlap(separation)
}
pub fn normal_force(&self, separation: f64, solid: &ElasticSolid) -> f64 {
let e_star = solid.youngs_modulus / (1.0 - solid.poisson_ratio * solid.poisson_ratio);
(4.0 / 3.0)
* self.asperity_density
* e_star
* self.asperity_radius.sqrt()
* self.mean_overlap_3_2(separation)
}
pub fn real_contact_area_fraction(&self, separation: f64) -> f64 {
PI * self.asperity_radius
* self.prob_contact(separation)
* self.asperity_density
* self.asperity_height_std
}
pub fn plasticity_index(&self, e_star: f64, hardness: f64) -> f64 {
if hardness < 1e-30 || self.asperity_radius < 1e-30 {
return 0.0;
}
(e_star / hardness) * (self.asperity_height_std / self.asperity_radius).sqrt()
}
}
pub struct ExtendedGwContact {
pub gw: RoughSurfaceContact,
pub hardness: f64,
pub work_of_adhesion: f64,
}
impl ExtendedGwContact {
pub fn adhesive_force(&self, separation: f64, nominal_area: f64) -> f64 {
let prob = self.gw.real_contact_area_fraction(separation);
2.0 * PI
* self.work_of_adhesion
* self.gw.asperity_radius
* self.gw.asperity_density
* nominal_area
* prob
}
pub fn plastic_fraction(&self, separation: f64, e_star: f64) -> f64 {
if self.hardness < 1e-30 {
return 0.0;
}
let p_mean = self.gw.normal_force(
separation,
&ElasticSolid {
youngs_modulus: e_star * 0.9,
poisson_ratio: 0.3,
},
);
(p_mean / (self.hardness / 3.0)).clamp(0.0, 1.0)
}
pub fn friction_coefficient(&self, shear_strength: f64) -> f64 {
if self.hardness < 1e-30 {
return 0.0;
}
shear_strength / (self.hardness / 3.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdhesionModel {
Hertz,
Jkr,
Dmt,
}
pub struct JkrContact {
pub hertz: HertzContact,
pub work_of_adhesion: f64,
}
impl JkrContact {
pub fn contact_radius(&self, normal_force: f64) -> f64 {
let r_star = self.hertz.effective_radius();
let e_star = ElasticSolid::reduced_modulus(&self.hertz.solid_a, &self.hertz.solid_b);
let w = self.work_of_adhesion;
let term1 = 3.0 * PI * w * r_star;
let under_sqrt = 6.0 * PI * w * r_star * normal_force + term1 * term1;
let a3 = (r_star / e_star) * (normal_force + term1 + under_sqrt.sqrt());
if a3 <= 0.0 {
return 0.0;
}
a3.cbrt()
}
pub fn pull_off_force(&self) -> f64 {
let r_star = self.hertz.effective_radius();
-1.5 * PI * self.work_of_adhesion * r_star
}
pub fn zero_load_contact_area(&self) -> f64 {
let a = self.contact_radius(0.0);
PI * a * a
}
pub fn indentation(&self, normal_force: f64) -> f64 {
let r_star = self.hertz.effective_radius();
let e_star = ElasticSolid::reduced_modulus(&self.hertz.solid_a, &self.hertz.solid_b);
let a = self.contact_radius(normal_force);
if a <= 0.0 {
return 0.0;
}
a * a / r_star - (2.0 * PI * self.work_of_adhesion * a / e_star).sqrt()
}
pub fn stored_elastic_energy(&self, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
let e_star = ElasticSolid::reduced_modulus(&self.hertz.solid_a, &self.hertz.solid_b);
let r_star = self.hertz.effective_radius();
let hertz_energy = 8.0 * e_star * a.powi(5) / (15.0 * r_star);
let adhesion_energy = PI * self.work_of_adhesion * a * a;
hertz_energy - adhesion_energy
}
}
pub struct NodeToSegmentContact {
pub slave_node: usize,
pub master_edge: [usize; 2],
pub gap: f64,
pub contact_normal: [f64; 3],
pub lambda: f64,
}
impl NodeToSegmentContact {
pub fn detect_contact(
slave_pos: [f64; 3],
master_a: [f64; 3],
master_b: [f64; 3],
) -> (f64, [f64; 3]) {
let proj = Self::project_onto_segment(slave_pos, master_a, master_b);
let d = [
slave_pos[0] - proj[0],
slave_pos[1] - proj[1],
slave_pos[2] - proj[2],
];
let dist = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
if dist < f64::EPSILON {
return (0.0, [0.0, 0.0, 1.0]);
}
let normal = [d[0] / dist, d[1] / dist, d[2] / dist];
let seg = [
master_b[0] - master_a[0],
master_b[1] - master_a[1],
master_b[2] - master_a[2],
];
let seg_len = (seg[0] * seg[0] + seg[1] * seg[1] + seg[2] * seg[2]).sqrt();
let _ = seg_len;
(dist, normal)
}
pub fn project_onto_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
let ab_sq = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
if ab_sq < f64::EPSILON {
return a;
}
let t = ((ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / ab_sq).clamp(0.0, 1.0);
[a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]]
}
pub fn segment_segment_distance(a1: [f64; 3], a2: [f64; 3], b1: [f64; 3], b2: [f64; 3]) -> f64 {
let d1 = point_segment_distance(a1, b1, b2);
let d2 = point_segment_distance(a2, b1, b2);
let d3 = point_segment_distance(b1, a1, a2);
let d4 = point_segment_distance(b2, a1, a2);
d1.min(d2).min(d3).min(d4)
}
}
pub struct HertzContact {
pub radius_a: f64,
pub radius_b: f64,
pub solid_a: ElasticSolid,
pub solid_b: ElasticSolid,
}
impl HertzContact {
pub fn effective_radius(&self) -> f64 {
let inv = 1.0 / self.radius_a + 1.0 / self.radius_b;
1.0 / inv
}
pub fn contact_radius(&self, normal_force: f64) -> f64 {
let r_star = self.effective_radius();
let e_star = ElasticSolid::reduced_modulus(&self.solid_a, &self.solid_b);
(3.0 * normal_force * r_star / (4.0 * e_star)).cbrt()
}
pub fn max_pressure(&self, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
3.0 * normal_force / (2.0 * PI * a * a)
}
pub fn mean_pressure(&self, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
normal_force / (PI * a * a)
}
pub fn contact_stiffness(&self, normal_force: f64) -> f64 {
let e_star = ElasticSolid::reduced_modulus(&self.solid_a, &self.solid_b);
let a = self.contact_radius(normal_force);
2.0 * e_star * a
}
pub fn hertz_indentation(&self, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
let r_star = self.effective_radius();
a * a / r_star
}
pub fn contact_area(&self, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
PI * a * a
}
pub fn pressure_distribution(&self, r: f64, normal_force: f64) -> f64 {
let a = self.contact_radius(normal_force);
if r > a {
return 0.0;
}
let p0 = self.max_pressure(normal_force);
let ratio = r / a;
p0 * (1.0 - ratio * ratio).sqrt()
}
pub fn stored_energy(&self, normal_force: f64) -> f64 {
let delta = self.hertz_indentation(normal_force);
0.4 * normal_force * delta
}
pub fn force_from_indentation(&self, delta: f64) -> f64 {
if delta <= 0.0 {
return 0.0;
}
let r_star = self.effective_radius();
let e_star = ElasticSolid::reduced_modulus(&self.solid_a, &self.solid_b);
(4.0 / 3.0) * e_star * r_star.sqrt() * delta.powf(1.5)
}
pub fn pressure_profile(&self, normal_force: f64, n_points: usize) -> Vec<(f64, f64)> {
let a = self.contact_radius(normal_force);
let mut profile = Vec::with_capacity(n_points);
for i in 0..n_points {
let r = a * (i as f64) / (n_points as f64 - 1.0).max(1.0);
let p = self.pressure_distribution(r, normal_force);
profile.push((r, p));
}
profile
}
}