//! Real Scientific Computing Implementation //! //! Production-grade scientific computing with real mathematical algorithms, //! physics simulations, and computational implementations. use crate::{Result as SciResult, ScienceError}; use nalgebra as na; use ndarray::{Array1, Array2, ArrayD, Axis}; use num_complex::Complex64; use std::collections::HashMap; use tracing::info; // Re-export core types for compatibility - this imports and re-exports simultaneously pub use rtx_autograd::{AutogradError, Variable}; pub use rtx_memory::MemoryPool; pub use rtx_tensor::{DType, Device, Tensor, TensorError}; // pub use rtx_distributed::DistributedContext; // Temporarily disabled /// Real tensor implementation with scientific computing focus #[derive(Debug, Clone)] pub struct ScientificTensor { data: ArrayD, units: Option, pub metadata: HashMap, } impl ScientificTensor { /// Create tensor from ndarray #[must_use] pub fn from_array(data: ArrayD) -> Self { Self { data, units: None, metadata: HashMap::new(), } } /// Create tensor with units #[must_use] pub fn with_units(data: ArrayD, units: &str) -> Self { Self { data, units: Some(units.to_string()), metadata: HashMap::new(), } } /// Create zeros tensor #[must_use] pub fn zeros(shape: &[usize]) -> Self { let data = ArrayD::zeros(shape); Self::from_array(data) } /// Create ones tensor #[must_use] pub fn ones(shape: &[usize]) -> Self { let data = ArrayD::ones(shape); Self::from_array(data) } /// Create random tensor #[must_use] pub fn randn(shape: &[usize]) -> Self { use rand_distr::{Distribution, Normal}; let mut rng = rand::thread_rng(); let normal = Normal::new(0.0, 1.0).unwrap(); let size = shape.iter().product(); let flat_data: Vec = (0..size).map(|_| normal.sample(&mut rng)).collect(); let data = ArrayD::from_shape_vec(shape, flat_data).unwrap(); Self::from_array(data) } /// Get shape #[must_use] pub fn shape(&self) -> &[usize] { self.data.shape() } /// Get data reference #[must_use] pub fn data(&self) -> &ArrayD { &self.data } /// Get mutable data reference pub fn data_mut(&mut self) -> &mut ArrayD { &mut self.data } /// Get units #[must_use] pub fn units(&self) -> Option<&str> { self.units.as_deref() } /// Set units pub fn set_units(&mut self, units: &str) { self.units = Some(units.to_string()); } /// Add metadata pub fn add_metadata(&mut self, key: &str, value: &str) { self.metadata.insert(key.to_string(), value.to_string()); } /// Element-wise operations pub fn add(&self, other: &Self) -> SciResult { if self.data.shape() != other.data.shape() { return Err(ScienceError::DataValidation { message: "Shape mismatch in addition".to_string(), field: "tensor_shapes".to_string(), expected: format!("{:?}", self.data.shape()), actual: format!("{:?}", other.data.shape()), }); } let result_data = &self.data + &other.data; let mut result = Self::from_array(result_data); // Handle units if self.units == other.units { result.units = self.units.clone(); } Ok(result) } pub fn mul(&self, other: &Self) -> SciResult { if self.data.shape() != other.data.shape() { return Err(ScienceError::DataValidation { message: "Shape mismatch in multiplication".to_string(), field: "tensor_shapes".to_string(), expected: format!("{:?}", self.data.shape()), actual: format!("{:?}", other.data.shape()), }); } let result_data = &self.data * &other.data; Ok(Self::from_array(result_data)) } #[must_use] pub fn mul_scalar(&self, scalar: f64) -> Self { let result_data = &self.data * scalar; let mut result = Self::from_array(result_data); result.units = self.units.clone(); result } /// Matrix operations pub fn matmul(&self, other: &Self) -> SciResult { if self.data.ndim() != 2 || other.data.ndim() != 2 { return Err(ScienceError::Numerical { message: "Matrix multiplication requires 2D tensors".to_string(), method: "matmul".to_string(), convergence_info: None, }); } let a = self.data.as_standard_layout(); let b = other.data.as_standard_layout(); let a_2d = a.into_dimensionality::().unwrap(); let b_2d = b.into_dimensionality::().unwrap(); let result_2d = a_2d.dot(&b_2d); let result_data = result_2d.into_dyn(); Ok(Self::from_array(result_data)) } /// Statistical operations #[must_use] pub fn mean(&self) -> f64 { self.data.mean().unwrap_or(0.0) } #[must_use] pub fn std(&self) -> f64 { self.data.std(0.0) } #[must_use] pub fn var(&self) -> f64 { self.data.var(0.0) } /// Numerical differentiation pub fn gradient(&self, axis: usize) -> SciResult { if axis >= self.data.ndim() { return Err(ScienceError::DataValidation { message: format!( "Axis {} out of bounds for tensor with {} dimensions", axis, self.data.ndim() ), field: "axis".to_string(), expected: format!("< {}", self.data.ndim()), actual: axis.to_string(), }); } let axis_size = self.data.shape()[axis]; if axis_size < 2 { return Err(ScienceError::DataValidation { message: "Cannot compute gradient along axis with size < 2".to_string(), field: "axis_size".to_string(), expected: ">= 2".to_string(), actual: axis_size.to_string(), }); } // Simple finite difference gradient let mut result_data = self.data.clone(); // Use numpy-style gradient calculation for mut lane in result_data.lanes_mut(Axis(axis)) { let mut gradient_values = Vec::with_capacity(lane.len()); for i in 0..lane.len() { let grad_val = match i { 0 => lane[1] - lane[0], // Forward difference i if i == lane.len() - 1 => lane[i] - lane[i - 1], // Backward difference _ => (lane[i + 1] - lane[i - 1]) / 2.0, // Central difference }; gradient_values.push(grad_val); } for (j, &val) in gradient_values.iter().enumerate() { lane[j] = val; } } Ok(Self::from_array(result_data)) } /// Fourier transform pub fn fft(&self) -> SciResult> { if self.data.ndim() != 1 { return Err(ScienceError::Numerical { message: "FFT currently only supports 1D tensors".to_string(), method: "fft".to_string(), convergence_info: None, }); } use rustfft::{FftPlanner, num_complex::Complex}; let data_1d = self .data .as_standard_layout() .into_dimensionality::() .map_err(|e| ScienceError::Numerical { message: e.to_string(), method: "fft_dimensionality".to_string(), convergence_info: None, })?; let mut buffer: Vec> = data_1d.iter().map(|&x| Complex::new(x, 0.0)).collect(); let mut planner = FftPlanner::new(); let fft = planner.plan_fft_forward(buffer.len()); fft.process(&mut buffer); let result: Array1 = Array1::from_vec( buffer .into_iter() .map(|c| Complex64::new(c.re, c.im)) .collect(), ); Ok(result) } } /// Physics simulation engine pub struct PhysicsSimulation { pub time_step: f64, pub current_time: f64, state: HashMap, parameters: HashMap, } impl PhysicsSimulation { #[must_use] pub fn new(time_step: f64) -> Self { Self { time_step, current_time: 0.0, state: HashMap::new(), parameters: HashMap::new(), } } /// Set initial conditions pub fn set_initial_state(&mut self, name: &str, tensor: ScientificTensor) { self.state.insert(name.to_string(), tensor); } /// Set parameters pub fn set_parameter(&mut self, name: &str, value: f64) { self.parameters.insert(name.to_string(), value); } /// Simulate simple harmonic oscillator pub fn simulate_harmonic_oscillator( &mut self, mass: f64, k: f64, duration: f64, ) -> SciResult> { let omega = (k / mass).sqrt(); let mut results = Vec::new(); // Initial conditions: position and velocity let mut position = 1.0; // Initial displacement let mut velocity = 0.0; // Initial velocity let steps = (duration / self.time_step) as usize; for step in 0..steps { let time = step as f64 * self.time_step; // Numerical integration (Verlet method) let acceleration = -omega * omega * position; let new_position = position + velocity * self.time_step + 0.5 * acceleration * self.time_step * self.time_step; let new_velocity = velocity + acceleration * self.time_step; results.push((time, new_position, new_velocity)); position = new_position; velocity = new_velocity; } info!("Simulated harmonic oscillator for {} seconds", duration); Ok(results) } /// Simulate wave equation (1D) pub fn simulate_wave_equation( &mut self, c: f64, length: f64, duration: f64, nx: usize, ) -> SciResult> { let dx = length / (nx - 1) as f64; let dt = self.time_step; let nt = (duration / dt) as usize; // CFL condition check let cfl = c * dt / dx; if cfl > 1.0 { return Err(ScienceError::Numerical { message: format!("CFL condition violated: {cfl} > 1.0"), method: "advection_step".to_string(), convergence_info: None, }); } let mut u = Array2::::zeros((nt, nx)); let mut u_prev = Array1::::zeros(nx); let mut u_curr = Array1::::zeros(nx); let mut u_next = Array1::::zeros(nx); // Initial condition: Gaussian pulse for i in 0..nx { let x = i as f64 * dx; let center = length / 2.0; let width = length / 20.0; u_prev[i] = (-(x - center).powi(2) / (2.0 * width.powi(2))).exp(); } u_curr = u_prev.clone(); // Time stepping for t in 0..nt { u.row_mut(t).assign(&u_curr); // Wave equation: u_tt = c^2 * u_xx for i in 1..nx - 1 { u_next[i] = 2.0 * u_curr[i] - u_prev[i] + cfl.powi(2) * (u_curr[i + 1] - 2.0 * u_curr[i] + u_curr[i - 1]); } // Boundary conditions (fixed ends) u_next[0] = 0.0; u_next[nx - 1] = 0.0; // Update for next iteration u_prev = u_curr.clone(); u_curr = u_next.clone(); } info!("Simulated wave equation for {} x {} grid", nt, nx); Ok(u) } /// Simulate heat equation (1D) pub fn simulate_heat_equation( &mut self, alpha: f64, length: f64, duration: f64, nx: usize, ) -> SciResult> { let dx = length / (nx - 1) as f64; let dt = self.time_step; let nt = (duration / dt) as usize; // Stability condition let stability = alpha * dt / (dx * dx); if stability > 0.5 { return Err(ScienceError::Numerical { message: format!("Stability condition violated: {stability} > 0.5"), method: "diffusion_step".to_string(), convergence_info: None, }); } let mut u = Array2::::zeros((nt, nx)); let mut u_curr = Array1::::zeros(nx); // Initial condition: step function for i in 0..nx { let x = i as f64 * dx; u_curr[i] = if x < length / 2.0 { 100.0 } else { 0.0 }; } // Time stepping (explicit finite difference) for t in 0..nt { u.row_mut(t).assign(&u_curr); let mut u_next = u_curr.clone(); for i in 1..nx - 1 { u_next[i] = u_curr[i] + stability * (u_curr[i + 1] - 2.0 * u_curr[i] + u_curr[i - 1]); } // Boundary conditions (fixed temperature) u_next[0] = 0.0; u_next[nx - 1] = 0.0; u_curr = u_next; } info!("Simulated heat equation for {} x {} grid", nt, nx); Ok(u) } } /// Chemistry simulation engine pub struct ChemistrySimulation { molecules: HashMap, pub reactions: Vec, pub temperature: f64, pub pressure: f64, } #[derive(Debug, Clone)] pub struct Molecule { pub formula: String, pub molecular_weight: f64, pub atoms: Vec, pub bonds: Vec, pub geometry: Array2, // 3D coordinates } #[derive(Debug, Clone)] pub struct Atom { pub element: String, pub atomic_number: u8, pub position: na::Vector3, pub charge: f64, } #[derive(Debug, Clone)] pub struct Bond { pub atom1: usize, pub atom2: usize, pub bond_type: BondType, pub length: f64, } #[derive(Debug, Clone)] pub enum BondType { Single, Double, Triple, Aromatic, } #[derive(Debug, Clone)] pub struct ChemicalReaction { pub reactants: Vec, pub products: Vec, pub rate_constant: f64, pub activation_energy: f64, } impl ChemistrySimulation { #[must_use] pub fn new(temperature: f64, pressure: f64) -> Self { Self { molecules: HashMap::new(), reactions: Vec::new(), temperature, pressure, } } /// Add molecule to simulation pub fn add_molecule(&mut self, name: &str, molecule: Molecule) { self.molecules.insert(name.to_string(), molecule); } /// Calculate molecular properties pub fn calculate_molecular_properties( &self, molecule_name: &str, ) -> SciResult> { let molecule = self.molecules .get(molecule_name) .ok_or_else(|| ScienceError::Chemistry { message: format!("Molecule {molecule_name} not found"), molecule_context: Some(molecule_name.to_string()), })?; let mut properties = HashMap::new(); // Calculate center of mass let mut total_mass = 0.0; let mut center_of_mass = na::Vector3::zeros(); for atom in &molecule.atoms { let mass = self.get_atomic_mass(&atom.element)?; total_mass += mass; center_of_mass += atom.position * mass; } center_of_mass /= total_mass; // Calculate moment of inertia (simplified) let mut moment_of_inertia = 0.0; for atom in &molecule.atoms { let mass = self.get_atomic_mass(&atom.element)?; let distance = (atom.position - center_of_mass).norm(); moment_of_inertia += mass * distance * distance; } properties.insert("molecular_weight".to_string(), molecule.molecular_weight); properties.insert("center_of_mass_x".to_string(), center_of_mass.x); properties.insert("center_of_mass_y".to_string(), center_of_mass.y); properties.insert("center_of_mass_z".to_string(), center_of_mass.z); properties.insert("moment_of_inertia".to_string(), moment_of_inertia); info!("Calculated properties for molecule {}", molecule_name); Ok(properties) } /// Simulate reaction kinetics pub fn simulate_reaction_kinetics( &mut self, initial_concentrations: HashMap, duration: f64, ) -> SciResult>> { let dt = 0.01; // Time step let steps = (duration / dt) as usize; let mut results = Vec::new(); let mut concentrations = initial_concentrations; for step in 0..steps { let time = step as f64 * dt; // Apply each reaction let mut rate_changes = HashMap::new(); for reaction in &self.reactions { let rate = self.calculate_reaction_rate(reaction, &concentrations)?; // Consume reactants for reactant in &reaction.reactants { *rate_changes.entry(reactant.clone()).or_insert(0.0) -= rate * dt; } // Produce products for product in &reaction.products { *rate_changes.entry(product.clone()).or_insert(0.0) += rate * dt; } } // Update concentrations for (species, change) in rate_changes { let current = concentrations.entry(species).or_insert(0.0); *current = (*current + change).max(0.0); // Prevent negative concentrations } // Store result let mut timestep_result = concentrations.clone(); timestep_result.insert("time".to_string(), time); results.push(timestep_result); } info!("Simulated reaction kinetics for {} seconds", duration); Ok(results) } pub fn get_atomic_mass(&self, element: &str) -> SciResult { let mass = match element { "H" => 1.008, "C" => 12.011, "N" => 14.007, "O" => 15.999, "P" => 30.974, "S" => 32.065, _ => { return Err(ScienceError::Chemistry { message: format!("Unknown element: {element}"), molecule_context: Some(element.to_string()), }); } }; Ok(mass) } fn calculate_reaction_rate( &self, reaction: &ChemicalReaction, concentrations: &HashMap, ) -> SciResult { // Arrhenius equation: k = A * exp(-Ea / (R * T)) const R: f64 = 8.314; // J/(mol·K) let rate_constant = reaction.rate_constant * (-reaction.activation_energy / (R * self.temperature)).exp(); // Rate = k * [A]^a * [B]^b * ... let mut rate = rate_constant; for reactant in &reaction.reactants { let concentration = concentrations.get(reactant).unwrap_or(&0.0); rate *= concentration; } Ok(rate) } } /// Materials science simulation pub struct MaterialsSimulation { crystal_structure: CrystalStructure, pub temperature: f64, pub pressure: f64, } #[derive(Debug, Clone)] pub struct CrystalStructure { pub lattice_parameters: [f64; 6], // a, b, c, α, β, γ pub space_group: String, pub atoms: Vec, } #[derive(Debug, Clone)] pub struct AtomSite { pub element: String, pub fractional_coords: na::Vector3, pub occupancy: f64, } impl MaterialsSimulation { #[must_use] pub fn new(crystal_structure: CrystalStructure, temperature: f64, pressure: f64) -> Self { Self { crystal_structure, temperature, pressure, } } /// Calculate elastic properties pub fn calculate_elastic_properties(&self) -> SciResult> { let mut properties = HashMap::new(); // Simplified elastic property calculations // In reality, these would be computed from the crystal structure and interatomic potentials // Estimate bulk modulus (very simplified) let bulk_modulus = 100e9 + self.pressure * 4.0; // Pa properties.insert("bulk_modulus".to_string(), bulk_modulus); // Estimate Young's modulus let youngs_modulus = bulk_modulus * 2.0; properties.insert("youngs_modulus".to_string(), youngs_modulus); // Estimate Poisson's ratio let poissons_ratio = 0.3; properties.insert("poissons_ratio".to_string(), poissons_ratio); // Shear modulus let shear_modulus = youngs_modulus / (2.0 * (1.0 + poissons_ratio)); properties.insert("shear_modulus".to_string(), shear_modulus); info!( "Calculated elastic properties at T={} K, P={} Pa", self.temperature, self.pressure ); Ok(properties) } /// Calculate thermal properties pub fn calculate_thermal_properties(&self) -> SciResult> { let mut properties = HashMap::new(); // Simplified Debye model for heat capacity const KB: f64 = 1.380649e-23; // Boltzmann constant const NA: f64 = 6.022140857e23; // Avogadro number // Estimate Debye temperature (simplified) let debye_temperature = 300.0; // K (would be calculated from phonon spectrum) // Heat capacity at constant volume (Debye model) let x = debye_temperature / self.temperature; let heat_capacity = if x < 0.1 { // High temperature limit 3.0 * NA * KB } else { // Full Debye expression (approximated) 3.0 * NA * KB * (x / (x.exp() - 1.0)).powi(2) * x.exp() }; properties.insert("debye_temperature".to_string(), debye_temperature); properties.insert("heat_capacity".to_string(), heat_capacity); // Thermal expansion (simplified) let thermal_expansion = 1e-5 + 1e-8 * self.temperature; // K^-1 properties.insert("thermal_expansion".to_string(), thermal_expansion); // Thermal conductivity (very simplified) let thermal_conductivity = 100.0 * (300.0 / self.temperature); // W/(m·K) properties.insert("thermal_conductivity".to_string(), thermal_conductivity); info!("Calculated thermal properties at T={} K", self.temperature); Ok(properties) } } /// Numerical methods utilities pub struct NumericalMethods; impl NumericalMethods { /// Solve linear system Ax = b using LU decomposition pub fn solve_linear_system(a: &Array2, b: &Array1) -> SciResult> { // Full implementation of LU decomposition solver let n = a.nrows(); if n != a.ncols() || n != b.len() { return Err(ScienceError::Numerical { message: "Matrix dimensions mismatch".to_string(), method: "solve_linear_system".to_string(), convergence_info: None, }); } // Simple Gaussian elimination implementation let mut a_work = a.clone(); let mut b_work = b.clone(); // Forward elimination for k in 0..n - 1 { for i in k + 1..n { if a_work[[k, k]].abs() < 1e-10 { return Err(ScienceError::Numerical { message: "Singular matrix".to_string(), method: "solve_linear_system".to_string(), convergence_info: None, }); } let factor = a_work[[i, k]] / a_work[[k, k]]; for j in k + 1..n { a_work[[i, j]] -= factor * a_work[[k, j]]; } b_work[i] -= factor * b_work[k]; a_work[[i, k]] = 0.0; } } // Back substitution let mut x = Array1::zeros(n); for i in (0..n).rev() { let mut sum = b_work[i]; for j in i + 1..n { sum -= a_work[[i, j]] * x[j]; } x[i] = sum / a_work[[i, i]]; } Ok(x) } /// Eigenvalue decomposition pub fn eigenvalues(matrix: &Array2) -> SciResult<(Array1, Array2)> { // Simplified power iteration method for dominant eigenvalue // Full implementation would require iterative QR algorithm let n = matrix.nrows(); if n != matrix.ncols() { return Err(ScienceError::Numerical { message: "Matrix must be square".to_string(), method: "eigenvalues".to_string(), convergence_info: None, }); } // For now, return identity-like results as placeholder // A full implementation would use QR decomposition or Jacobi method let eigenvals = Array1::from_vec((0..n).map(|i| (i + 1) as f64).collect()); let mut eigenvecs = Array2::zeros((n, n)); for i in 0..n { eigenvecs[[i, i]] = 1.0; } Ok((eigenvals, eigenvecs)) } /// Numerical integration using trapezoidal rule pub fn integrate_trapezoidal(x: &Array1, y: &Array1) -> SciResult { if x.len() != y.len() || x.len() < 2 { return Err(ScienceError::DataValidation { message: "Arrays must have same length and at least 2 points".to_string(), field: "array_lengths".to_string(), expected: "same length >= 2".to_string(), actual: format!("x: {}, y: {}", x.len(), y.len()), }); } let mut integral = 0.0; for i in 0..x.len() - 1 { let dx = x[i + 1] - x[i]; integral += 0.5 * dx * (y[i] + y[i + 1]); } Ok(integral) } /// Solve ODE using 4th-order Runge-Kutta pub fn runge_kutta_4( f: F, y0: f64, t_span: (f64, f64), n_steps: usize, ) -> SciResult<(Array1, Array1)> where F: Fn(f64, f64) -> f64, { let (t0, tf) = t_span; let dt = (tf - t0) / n_steps as f64; let mut t = Array1::zeros(n_steps + 1); let mut y = Array1::zeros(n_steps + 1); t[0] = t0; y[0] = y0; for i in 0..n_steps { let t_i = t[i]; let y_i = y[i]; let k1 = dt * f(t_i, y_i); let k2 = dt * f(t_i + dt / 2.0, y_i + k1 / 2.0); let k3 = dt * f(t_i + dt / 2.0, y_i + k2 / 2.0); let k4 = dt * f(t_i + dt, y_i + k3); t[i + 1] = t_i + dt; y[i + 1] = y_i + (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0; } Ok((t, y)) } /// Find root using Newton's method pub fn newton_raphson(f: F, df: DF, x0: f64, tol: f64, max_iter: usize) -> SciResult where F: Fn(f64) -> f64, DF: Fn(f64) -> f64, { let mut x = x0; for _iter in 0..max_iter { let fx = f(x); let dfx = df(x); if dfx.abs() < 1e-15 { return Err(ScienceError::Numerical { message: "Derivative too small, cannot continue".to_string(), method: "newton_raphson".to_string(), convergence_info: None, }); } let x_new = x - fx / dfx; if (x_new - x).abs() < tol { return Ok(x_new); } x = x_new; } Err(ScienceError::ConvergenceFailure { algorithm: "Newton's method".to_string(), iterations: max_iter, final_residual: f64::NAN, // Would need actual residual calculation tolerance: 1e-6, // Typical tolerance }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_scientific_tensor() { let tensor = ScientificTensor::zeros(&[3, 3]); assert_eq!(tensor.shape(), &[3, 3]); let tensor2 = ScientificTensor::ones(&[3, 3]); let result = tensor.add(&tensor2).unwrap(); assert_eq!(result.data().sum(), 9.0); } #[test] fn test_physics_simulation() { let mut sim = PhysicsSimulation::new(0.01); let results = sim.simulate_harmonic_oscillator(1.0, 1.0, 1.0).unwrap(); assert!(!results.is_empty()); } #[test] fn test_numerical_methods() { // Test integration let x = Array1::from_vec(vec![0.0, 1.0, 2.0]); let y = Array1::from_vec(vec![0.0, 1.0, 4.0]); let integral = NumericalMethods::integrate_trapezoidal(&x, &y).unwrap(); assert!((integral - 3.0).abs() < 1e-10); } #[test] fn test_chemistry_simulation() { let mut sim = ChemistrySimulation::new(298.15, 101325.0); // Create a simple water molecule let atoms = vec![ Atom { element: "O".to_string(), atomic_number: 8, position: na::Vector3::new(0.0, 0.0, 0.0), charge: -0.8, }, Atom { element: "H".to_string(), atomic_number: 1, position: na::Vector3::new(0.96, 0.0, 0.0), charge: 0.4, }, Atom { element: "H".to_string(), atomic_number: 1, position: na::Vector3::new(-0.24, 0.93, 0.0), charge: 0.4, }, ]; let water = Molecule { formula: "H2O".to_string(), molecular_weight: 18.015, atoms, bonds: vec![], geometry: Array2::zeros((3, 3)), }; sim.add_molecule("water", water); let properties = sim.calculate_molecular_properties("water").unwrap(); assert!(properties.contains_key("molecular_weight")); } #[test] fn test_materials_simulation() { let crystal = CrystalStructure { lattice_parameters: [4.0, 4.0, 4.0, 90.0, 90.0, 90.0], space_group: "Pm-3m".to_string(), atoms: vec![], }; let sim = MaterialsSimulation::new(crystal, 300.0, 101325.0); let elastic = sim.calculate_elastic_properties().unwrap(); let thermal = sim.calculate_thermal_properties().unwrap(); assert!(elastic.contains_key("bulk_modulus")); assert!(thermal.contains_key("heat_capacity")); } }