use crate::twobody::{GridType, SplineCoeffs, SplinedPotential};
use bytemuck::{Pod, Zeroable};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[repr(C)]
pub struct GpuSplineCoeffs {
pub u: [f32; 4],
pub f: [f32; 4],
}
unsafe impl Zeroable for GpuSplineCoeffs {}
unsafe impl Pod for GpuSplineCoeffs {}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[repr(C)]
pub struct GpuAngleSplineParams {
pub angle_min: f32,
pub angle_max: f32,
pub n_coeffs: u32,
pub coeff_offset: u32,
pub inv_delta: f32,
_pad0: f32,
_pad1: f32,
_pad2: f32,
}
unsafe impl Zeroable for GpuAngleSplineParams {}
unsafe impl Pod for GpuAngleSplineParams {}
pub trait GpuGridType {
type Params: Pod + Zeroable + Default + Copy + Clone + std::fmt::Debug + PartialEq;
const SPLINE_WGSL: &'static str;
fn make_params(spline: &SplinedPotential, coeff_offset: u32, n_coeffs: u32) -> Self::Params;
}
pub struct PowerLaw2;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[repr(C)]
pub struct PowerLaw2Params {
pub r_min: f32,
pub r_max: f32,
pub n_coeffs: u32,
pub coeff_offset: u32,
pub f_at_rmin: f32,
_pad0: f32,
_pad1: f32,
_pad2: f32,
}
unsafe impl Zeroable for PowerLaw2Params {}
unsafe impl Pod for PowerLaw2Params {}
impl GpuGridType for PowerLaw2 {
type Params = PowerLaw2Params;
const SPLINE_WGSL: &'static str = include_str!("shaders/spline_powerlaw2.wgsl");
fn make_params(spline: &SplinedPotential, coeff_offset: u32, n_coeffs: u32) -> PowerLaw2Params {
assert!(
matches!(spline.grid_type(), GridType::PowerLaw2),
"Expected PowerLaw2 grid, got {:?}",
spline.grid_type()
);
PowerLaw2Params {
r_min: spline.r_min() as f32,
r_max: spline.r_max() as f32,
n_coeffs,
coeff_offset,
f_at_rmin: spline.f_at_rmin() as f32,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
}
}
}
pub struct InverseRsq;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[repr(C)]
pub struct InverseRsqParams {
pub r_min: f32,
pub r_max: f32,
pub n_coeffs: u32,
pub coeff_offset: u32,
pub inv_delta: f32,
pub w_min: f32,
pub f_at_rmin: f32,
_pad0: f32,
}
unsafe impl Zeroable for InverseRsqParams {}
unsafe impl Pod for InverseRsqParams {}
impl GpuGridType for InverseRsq {
type Params = InverseRsqParams;
const SPLINE_WGSL: &'static str = include_str!("shaders/spline_inversersq.wgsl");
fn make_params(
spline: &SplinedPotential,
coeff_offset: u32,
n_coeffs: u32,
) -> InverseRsqParams {
assert!(
matches!(spline.grid_type(), GridType::InverseRsq),
"Expected InverseRsq grid, got {:?}",
spline.grid_type()
);
let r_max = spline.r_max();
InverseRsqParams {
r_min: spline.r_min() as f32,
r_max: r_max as f32,
n_coeffs,
coeff_offset,
inv_delta: spline.inv_delta() as f32,
w_min: (1.0 / (r_max * r_max)) as f32,
f_at_rmin: spline.f_at_rmin() as f32,
_pad0: 0.0,
}
}
}
fn pack_coeffs(dst: &mut Vec<GpuSplineCoeffs>, src: &[SplineCoeffs]) {
dst.extend(src.iter().map(|c| GpuSplineCoeffs {
u: [c.u[0] as f32, c.u[1] as f32, c.u[2] as f32, c.u[3] as f32],
f: [c.f[0] as f32, c.f[1] as f32, c.f[2] as f32, c.f[3] as f32],
}));
}
#[derive(Clone, Debug)]
pub struct GpuSplineData<G: GpuGridType> {
pub coefficients: Vec<GpuSplineCoeffs>,
pub params: Vec<G::Params>,
pub angle_params: Vec<GpuAngleSplineParams>,
}
impl<G: GpuGridType> Default for GpuSplineData<G> {
fn default() -> Self {
Self {
coefficients: Vec::new(),
params: Vec::new(),
angle_params: Vec::new(),
}
}
}
impl<G: GpuGridType> GpuSplineData<G> {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, spline: &SplinedPotential) {
let coeff_offset = self.coefficients.len() as u32;
let n_coeffs = spline.coefficients().len() as u32;
pack_coeffs(&mut self.coefficients, spline.coefficients());
self.params
.push(G::make_params(spline, coeff_offset, n_coeffs));
}
pub fn push_angle(&mut self, spline: &crate::threebody::SplinedAngle) {
self.push_angle_impl(
spline.coefficients(),
spline.angle_min(),
spline.angle_max(),
spline.inv_delta(),
);
}
pub fn push_dihedral(&mut self, spline: &crate::fourbody::SplinedDihedral) {
self.push_angle_impl(
spline.coefficients(),
spline.angle_min(),
spline.angle_max(),
spline.inv_delta(),
);
}
fn push_angle_impl(
&mut self,
coefficients: &[SplineCoeffs],
angle_min: f64,
angle_max: f64,
inv_delta: f64,
) {
let coeff_offset = self.coefficients.len() as u32;
let n_coeffs = coefficients.len() as u32;
pack_coeffs(&mut self.coefficients, coefficients);
self.angle_params.push(GpuAngleSplineParams {
angle_min: angle_min as f32,
angle_max: angle_max as f32,
n_coeffs,
coeff_offset,
inv_delta: inv_delta as f32,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
});
}
pub fn from_potentials<'a>(iter: impl IntoIterator<Item = &'a SplinedPotential>) -> Self {
let mut data = Self::new();
for spline in iter {
data.push(spline);
}
data
}
pub fn params_as_bytes(&self) -> &[u8] {
bytemuck::cast_slice(&self.params)
}
pub fn coefficients_as_bytes(&self) -> &[u8] {
bytemuck::cast_slice(&self.coefficients)
}
pub fn angle_params_as_bytes(&self) -> &[u8] {
bytemuck::cast_slice(&self.angle_params)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::twobody::{LennardJones, SplineConfig};
#[test]
fn struct_sizes() {
assert_eq!(std::mem::size_of::<PowerLaw2Params>(), 32);
assert_eq!(std::mem::size_of::<InverseRsqParams>(), 32);
assert_eq!(std::mem::size_of::<GpuSplineCoeffs>(), 32);
assert_eq!(std::mem::size_of::<GpuAngleSplineParams>(), 32);
}
#[test]
fn powerlaw2_conversion() {
let lj = LennardJones::new(1.0, 1.0);
let splined = SplinedPotential::with_cutoff(
&lj,
2.5,
SplineConfig::default().with_grid_type(GridType::PowerLaw2),
);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push(&splined);
assert_eq!(gpu.params.len(), 1);
assert_eq!(gpu.params[0].coeff_offset, 0);
assert_eq!(gpu.params[0].n_coeffs, gpu.coefficients.len() as u32);
}
#[test]
fn inverse_rsq_conversion() {
let lj = LennardJones::new(1.0, 1.0);
let splined = SplinedPotential::with_cutoff(
&lj,
2.5,
SplineConfig::default().with_grid_type(GridType::InverseRsq),
);
let mut gpu = GpuSplineData::<InverseRsq>::new();
gpu.push(&splined);
assert_eq!(gpu.params.len(), 1);
let expected_w_min = 1.0f32 / (2.5f32 * 2.5f32);
assert!((gpu.params[0].w_min - expected_w_min).abs() < 1e-6);
}
#[test]
fn multi_potential_offsets() {
let lj1 = LennardJones::new(1.0, 1.0);
let lj2 = LennardJones::new(2.0, 0.5);
let config = SplineConfig {
n_points: 100,
..SplineConfig::default()
};
let s1 = SplinedPotential::with_cutoff(&lj1, 2.5, config.clone());
let s2 = SplinedPotential::with_cutoff(&lj2, 3.0, config);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push(&s1);
let n1 = gpu.coefficients.len() as u32;
gpu.push(&s2);
assert_eq!(gpu.params[0].coeff_offset, 0);
assert_eq!(gpu.params[1].coeff_offset, n1);
assert_eq!(gpu.params.len(), 2);
}
#[test]
fn byte_roundtrip() {
let lj = LennardJones::new(1.0, 1.0);
let splined = SplinedPotential::with_cutoff(
&lj,
2.5,
SplineConfig::default().with_grid_type(GridType::PowerLaw2),
);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push(&splined);
let params_bytes = gpu.params_as_bytes();
let coeffs_bytes = gpu.coefficients_as_bytes();
assert_eq!(params_bytes.len(), 32);
assert_eq!(coeffs_bytes.len(), gpu.coefficients.len() * 32);
let params_back: &[PowerLaw2Params] = bytemuck::cast_slice(params_bytes);
assert_eq!(params_back[0], gpu.params[0]);
let coeffs_back: &[GpuSplineCoeffs] = bytemuck::cast_slice(coeffs_bytes);
assert_eq!(coeffs_back.len(), gpu.coefficients.len());
assert_eq!(coeffs_back[0], gpu.coefficients[0]);
}
#[test]
#[should_panic(expected = "Expected PowerLaw2 grid")]
fn reject_mismatched_grid() {
let lj = LennardJones::new(1.0, 1.0);
let splined = SplinedPotential::with_cutoff(
&lj,
2.5,
SplineConfig::default().with_grid_type(GridType::UniformR),
);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push(&splined);
}
#[test]
fn from_potentials() {
let lj = LennardJones::new(1.0, 1.0);
let config = SplineConfig::default();
let s1 = SplinedPotential::with_cutoff(&lj, 2.5, config.clone());
let s2 = SplinedPotential::with_cutoff(&lj, 3.0, config);
let potentials = vec![s1, s2];
let gpu = GpuSplineData::<PowerLaw2>::from_potentials(&potentials);
assert_eq!(gpu.params.len(), 2);
}
#[test]
fn angle_spline_gpu() {
use crate::threebody::{HarmonicTorsion, SplinedAngle};
let pot = HarmonicTorsion::new(std::f64::consts::FRAC_PI_2, 100.0);
let splined = SplinedAngle::new(&pot, 200);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push_angle(&splined);
assert_eq!(gpu.angle_params.len(), 1);
assert!((gpu.angle_params[0].angle_min - 0.0).abs() < 1e-6);
assert!((gpu.angle_params[0].angle_max - std::f32::consts::PI).abs() < 1e-5);
assert!(!gpu.coefficients.is_empty());
}
#[test]
fn dihedral_spline_gpu() {
use crate::fourbody::{HarmonicDihedral, SplinedDihedral};
let pot = HarmonicDihedral::new(0.0, 50.0);
let splined = SplinedDihedral::new(&pot, 200);
let mut gpu = GpuSplineData::<PowerLaw2>::new();
gpu.push_dihedral(&splined);
assert_eq!(gpu.angle_params.len(), 1);
assert!((gpu.angle_params[0].angle_min - (-std::f32::consts::PI)).abs() < 1e-5);
assert!((gpu.angle_params[0].angle_max - std::f32::consts::PI).abs() < 1e-5);
}
}