1use std::f64::consts::E;
6
7pub(super) const R_GAS: f64 = 8.314_462_618;
11#[allow(dead_code)]
20pub fn avrami_transformation(time: f64, n: f64, k: f64) -> f64 {
21 if time <= 0.0 {
22 return 0.0;
23 }
24 1.0 - E.powf(-k * time.powf(n))
25}
26#[allow(dead_code)]
37#[allow(clippy::too_many_arguments)]
38pub fn cct_diagram_transformation_time(
39 t: f64,
40 t_start: f64,
41 t_end: f64,
42 time_0: f64,
43 time_f: f64,
44 _t_ref: f64,
45) -> f64 {
46 if (t_end - t_start).abs() < 1e-15 {
47 return time_0;
48 }
49 let frac = (t - t_start) / (t_end - t_start);
50 let frac = frac.clamp(0.0, 1.0);
51 let log_t0 = time_0.max(1e-30).ln();
52 let log_tf = time_f.max(1e-30).ln();
53 (log_t0 + frac * (log_tf - log_t0)).exp()
54}
55#[allow(dead_code)]
64pub fn landau_free_energy(phi: f64, a: f64, b: f64) -> f64 {
65 a / 2.0 * phi * phi - b / 4.0 * phi.powi(4)
66}
67#[allow(dead_code)]
72pub fn ternary_lever_rule(
73 x_b_overall: f64,
74 x_c_overall: f64,
75 x_b_alpha: f64,
76 x_c_alpha: f64,
77 x_b_beta: f64,
78 x_c_beta: f64,
79) -> f64 {
80 let db = x_b_beta - x_b_alpha;
81 let dc = x_c_beta - x_c_alpha;
82 let len2 = db * db + dc * dc;
83 if len2 < 1e-60 {
84 return 0.0;
85 }
86 let f = ((x_b_overall - x_b_alpha) * db + (x_c_overall - x_c_alpha) * dc) / len2;
87 f.clamp(0.0, 1.0)
88}
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::AusteniteMartensiteKinetics;
93 use crate::BainiteTransformation;
94 use crate::BrinsonModel;
95 use crate::CahnHilliardField;
96 use crate::CalphadModel;
97 use crate::CctDiagram;
98 use crate::ClausiusClapeyron;
99
100 use crate::JmakExtended;
101 use crate::JmatProModel;
102 use crate::JohnsonMehlAvramiKolmogorov;
103 use crate::KoistinenMarburger;
104 use crate::LandauFreeEnergy;
105 use crate::LatentHeatModel;
106 use crate::LeverRule;
107 use crate::MartensiteTransformation;
108 use crate::PearliteTransformation;
109 use crate::PhaseDependentProperties;
110 use crate::PhaseFieldModel2D;
111 use crate::PhaseFieldOrder;
112 use crate::PhaseFractionEvolution;
113 use crate::PhaseTransformType;
114
115 use crate::SpinocalDecomposition;
116
117 use crate::TransformationPlasticity;
118 use crate::TripEffect;
119
120 use crate::TttDiagram;
121
122 use crate::VolumeFractionTracker;
123 #[test]
124 fn test_landau_order_param_zero_above_tc() {
125 let landau = LandauFreeEnergy::new(1.0, 2.0, 500.0);
126 let eta = landau.equilibrium_order_parameter(600.0);
127 assert!(eta.abs() < 1.0e-12, "Expected eta=0 above Tc, got {eta}");
128 }
129 #[test]
130 fn test_landau_order_param_positive_below_tc() {
131 let landau = LandauFreeEnergy::new(1.0, 2.0, 500.0);
132 let eta = landau.equilibrium_order_parameter(400.0);
133 assert!(eta > 0.0, "Expected eta>0 below Tc, got {eta}");
134 let expected = (50.0_f64).sqrt();
135 assert!(
136 (eta - expected).abs() < 1.0e-10,
137 "Expected eta={expected}, got {eta}"
138 );
139 }
140 #[test]
141 fn test_martensite_fraction_increases_below_ms() {
142 let mt = MartensiteTransformation::new(500.0, 300.0, 550.0, 700.0, 0.011, 0.02);
143 let f1 = mt.martensite_fraction_koistinen_marburger(490.0);
144 let f2 = mt.martensite_fraction_koistinen_marburger(400.0);
145 assert!(f2 > f1, "Fraction should increase as T decreases below Ms");
146 }
147 #[test]
148 fn test_martensite_fraction_at_mf() {
149 let mt = MartensiteTransformation::new(500.0, 200.0, 550.0, 700.0, 0.05, 0.02);
150 let f = mt.martensite_fraction_koistinen_marburger(200.0);
151 assert!(f > 0.99, "Fraction at Mf should be close to 1, got {f}");
152 }
153 #[test]
154 fn test_jmak_fraction_at_zero() {
155 let jmak = JohnsonMehlAvramiKolmogorov::new(0.01, 2.0);
156 assert_eq!(jmak.fraction_transformed(0.0), 0.0);
157 }
158 #[test]
159 fn test_jmak_fraction_increases_with_time() {
160 let jmak = JohnsonMehlAvramiKolmogorov::new(0.01, 2.0);
161 let f1 = jmak.fraction_transformed(1.0);
162 let f2 = jmak.fraction_transformed(5.0);
163 let f3 = jmak.fraction_transformed(10.0);
164 assert!(f1 < f2, "JMAK fraction must increase with time");
165 assert!(f2 < f3, "JMAK fraction must increase with time");
166 }
167 #[test]
168 fn test_calphad_mixing_enthalpy_pure_components() {
169 let model = CalphadModel::new(-5000.0, -8000.0, 2000.0, 500.0);
170 let h0 = model.mixing_enthalpy(0.0);
171 let h1 = model.mixing_enthalpy(1.0);
172 assert!(h0.abs() < 1.0e-12, "H_mix at x_B=0 should be 0, got {h0}");
173 assert!(h1.abs() < 1.0e-12, "H_mix at x_B=1 should be 0, got {h1}");
174 }
175 #[test]
176 fn test_phase_field_solid_fraction_increases_after_step() {
177 let nx = 32;
178 let ny = 32;
179 let mut pf = PhaseFieldModel2D::new(nx, ny, 2.0, 1.0, 100.0);
180 for t in pf.temperature.iter_mut() {
181 *t = 1000.0;
182 }
183 pf.initialize_seed(16.0, 16.0, 4.0);
184 let f0 = pf.solid_fraction();
185 for _ in 0..10 {
186 pf.step(1.0e-4, 1.0);
187 }
188 let f1 = pf.solid_fraction();
189 assert!(
190 f1 >= f0,
191 "Solid fraction should not decrease after solidification steps: f0={f0}, f1={f1}"
192 );
193 }
194 #[test]
195 fn test_km_fraction_zero_at_ms() {
196 let km = KoistinenMarburger::new(500.0, 0.011);
197 assert!((km.fraction(500.0)).abs() < 1e-12);
198 }
199 #[test]
200 fn test_km_fraction_increases_below_ms() {
201 let km = KoistinenMarburger::new(500.0, 0.011);
202 let f1 = km.fraction(490.0);
203 let f2 = km.fraction(450.0);
204 assert!(f2 > f1, "fraction increases as T decreases");
205 }
206 #[test]
207 fn test_km_temp_for_fraction_roundtrip() {
208 let km = KoistinenMarburger::new(500.0, 0.011);
209 let t = km.temperature_for_fraction(0.5);
210 let f = km.fraction(t);
211 assert!((f - 0.5).abs() < 1e-9, "roundtrip: expected 0.5 got {f}");
212 }
213 #[test]
214 fn test_austenite_martensite_kinetics_cooling() {
215 let mut k = AusteniteMartensiteKinetics::new(500.0, 0.011, 550.0, 700.0, 600.0);
216 k.step(450.0);
217 assert!(k.f_martensite > 0.0, "martensite should form on cooling");
218 assert!(k.f_austenite() < 1.0);
219 }
220 #[test]
221 fn test_austenite_martensite_kinetics_heating_dissolves() {
222 let mut k = AusteniteMartensiteKinetics::new(500.0, 0.011, 550.0, 700.0, 600.0);
223 k.step(400.0);
224 let fm_before = k.f_martensite;
225 k.step(700.0);
226 assert!(
227 k.f_martensite < fm_before,
228 "martensite should dissolve on heating"
229 );
230 assert!(
231 k.f_martensite.abs() < 1e-9,
232 "should be fully austenitic above Af"
233 );
234 }
235 #[test]
236 fn test_trip_strain_increment_positive() {
237 let mut trip = TripEffect::new(1e-5);
238 let de = trip.trip_strain_increment(200e6, 0.3, 0.1);
239 assert!(de > 0.0);
240 assert!((trip.accumulated_strain - de).abs() < 1e-15);
241 }
242 #[test]
243 fn test_trip_no_increment_for_negative_df() {
244 let mut trip = TripEffect::new(1e-5);
245 let de = trip.trip_strain_increment(200e6, 0.3, -0.1);
246 assert_eq!(de, 0.0);
247 }
248 #[test]
249 fn test_trip_reset() {
250 let mut trip = TripEffect::new(1e-5);
251 trip.trip_strain_increment(200e6, 0.3, 0.1);
252 trip.reset();
253 assert_eq!(trip.accumulated_strain, 0.0);
254 }
255 #[test]
256 fn test_brinson_effective_modulus_austenite() {
257 let b = BrinsonModel::new(70e9, 30e9, 300.0, 250.0, 340.0, 380.0, 0.05);
258 assert!((b.effective_modulus() - 70e9).abs() < 1.0);
259 }
260 #[test]
261 fn test_brinson_update_temperature_below_mf() {
262 let mut b = BrinsonModel::new(70e9, 30e9, 300.0, 200.0, 340.0, 380.0, 0.05);
263 b.update_temperature(150.0);
264 assert!(
265 (b.xi_t - 1.0).abs() < 1e-9,
266 "should be fully martensitic below Mf"
267 );
268 }
269 #[test]
270 fn test_brinson_update_temperature_above_af() {
271 let mut b = BrinsonModel::new(70e9, 30e9, 300.0, 200.0, 340.0, 380.0, 0.05);
272 b.update_temperature(150.0);
273 b.update_temperature(400.0);
274 assert!(b.xi_t.abs() < 1e-9, "should be fully austenitic above Af");
275 }
276 #[test]
277 fn test_transformation_plasticity_increment() {
278 let mut tp = TransformationPlasticity::new(0.01, 300e6);
279 let de = tp.increment(100e6, 0.1);
280 assert!(de > 0.0);
281 assert!((tp.total_strain() - de).abs() < 1e-15);
282 }
283 #[test]
284 fn test_transformation_plasticity_no_dissolution_increment() {
285 let mut tp = TransformationPlasticity::new(0.01, 300e6);
286 let de = tp.increment(100e6, -0.01);
287 assert_eq!(de, 0.0);
288 }
289 #[test]
290 fn test_phase_fraction_evolution_record_and_query() {
291 let mut evo = PhaseFractionEvolution::new(3);
292 evo.record(0.0, &[1.0, 0.0, 0.0]);
293 evo.record(1.0, &[0.5, 0.3, 0.2]);
294 assert!((evo.current(0) - 0.5).abs() < 1e-12);
295 assert!((evo.current(1) - 0.3).abs() < 1e-12);
296 assert!((evo.current(2) - 0.2).abs() < 1e-12);
297 }
298 #[test]
299 fn test_phase_fraction_evolution_rate() {
300 let mut evo = PhaseFractionEvolution::new(2);
301 evo.record(0.0, &[1.0, 0.0]);
302 evo.record(2.0, &[0.6, 0.4]);
303 let rate_0 = evo.rate(0);
304 let rate_1 = evo.rate(1);
305 assert!((rate_0 + 0.2).abs() < 1e-9, "phase 0 rate = {rate_0}");
306 assert!((rate_1 - 0.2).abs() < 1e-9, "phase 1 rate = {rate_1}");
307 }
308 #[test]
309 fn test_phase_fraction_total() {
310 let mut evo = PhaseFractionEvolution::new(3);
311 evo.record(0.0, &[0.5, 0.3, 0.2]);
312 assert!((evo.total_fraction() - 1.0).abs() < 1e-12);
313 }
314 #[test]
315 fn test_koistinen_marburger_at_ms_gives_zero() {
316 let xm = JmatProModel::martensite_fraction_koistinen_marburger(500.0, 500.0);
317 assert!(xm.abs() < 1e-12, "X_m at T=Ms should be 0, got {xm}");
318 }
319 #[test]
320 fn test_koistinen_marburger_increases_below_ms() {
321 let xm1 = JmatProModel::martensite_fraction_koistinen_marburger(490.0, 500.0);
322 let xm2 = JmatProModel::martensite_fraction_koistinen_marburger(450.0, 500.0);
323 assert!(
324 xm2 > xm1,
325 "Martensite fraction should increase as T decreases below Ms"
326 );
327 }
328 #[test]
329 fn test_avrami_at_t_zero_gives_zero() {
330 let x = avrami_transformation(0.0, 2.0, 0.01);
331 assert_eq!(x, 0.0, "Avrami at t=0 should be 0");
332 }
333 #[test]
334 fn test_avrami_increases_with_time() {
335 let x1 = avrami_transformation(1.0, 2.0, 0.01);
336 let x2 = avrami_transformation(5.0, 2.0, 0.01);
337 let x3 = avrami_transformation(10.0, 2.0, 0.01);
338 assert!(
339 x1 < x2 && x2 < x3,
340 "Avrami fraction should increase with time"
341 );
342 }
343 #[test]
344 fn test_allen_cahn_reduces_free_energy() {
345 let mut pf = PhaseFieldOrder::new(1);
346 pf.phi[0] = 0.5;
347 let phi_initial = pf.phi[0];
348 let df_dphi = [1.0];
349 pf.update_allen_cahn(0.1, 1.0, &df_dphi);
350 assert!(
351 pf.phi[0] < phi_initial,
352 "Allen-Cahn should decrease φ when dF/dφ > 0"
353 );
354 }
355 #[test]
356 fn test_cahn_hilliard_conserves_mass() {
357 let nx = 32;
358 let c0 = 0.5;
359 let mut ch = CahnHilliardField::new(nx, c0);
360 let total_before = ch.total_concentration();
361 ch.update(0.01, 1.0, 1.0);
362 let total_after = ch.total_concentration();
363 assert!(
364 (total_after - total_before).abs() < 1e-10,
365 "Cahn-Hilliard should conserve total concentration: {total_before} → {total_after}"
366 );
367 }
368 #[test]
369 fn test_landau_free_energy_at_zero_phi() {
370 let f = landau_free_energy(0.0, -1.0, 1.0);
371 assert_eq!(f, 0.0, "F(φ=0) should be 0");
372 }
373 #[test]
374 fn test_landau_free_energy_double_well() {
375 let a = -1.0_f64;
376 let b = 1.0_f64;
377 let phi_test = 0.5_f64;
378 let f_pos = landau_free_energy(phi_test, a, b);
379 let f_neg = landau_free_energy(-phi_test, a, b);
380 assert!(
381 (f_pos - f_neg).abs() < 1e-12,
382 "Landau free energy should be even: {f_pos} vs {f_neg}"
383 );
384 }
385 #[test]
386 fn test_phase_transform_type_variants() {
387 let t1 = PhaseTransformType::Martensitic;
388 let t2 = PhaseTransformType::Bainitic;
389 assert_ne!(t1, t2);
390 assert_eq!(
391 PhaseTransformType::Austenitic,
392 PhaseTransformType::Austenitic
393 );
394 }
395 #[test]
396 fn test_jmat_pro_model_range() {
397 let model = JmatProModel::new(500.0, 300.0, 600.0);
398 assert!(
399 model.in_martensite_range(400.0),
400 "400K should be in martensite range [300, 500]"
401 );
402 assert!(
403 !model.in_martensite_range(200.0),
404 "200K should be below martensite range"
405 );
406 assert!(
407 model.above_bainite_start(650.0),
408 "650K should be above bainite start 600K"
409 );
410 }
411 #[test]
412 fn test_bainite_fraction_zero_at_time_zero() {
413 let bt = BainiteTransformation::new(2.0, 1e-3, 80_000.0, 750.0, 500.0);
414 assert_eq!(bt.fraction_at(0.0, 650.0), 0.0);
415 }
416 #[test]
417 fn test_bainite_no_fraction_above_bs() {
418 let bt = BainiteTransformation::new(2.0, 1e-3, 80_000.0, 750.0, 500.0);
419 assert_eq!(bt.fraction_at(1000.0, 800.0), 0.0, "no bainite above B_s");
420 }
421 #[test]
422 fn test_bainite_fraction_increases_with_time() {
423 let bt = BainiteTransformation::new(2.0, 1e-3, 80_000.0, 750.0, 500.0);
424 let f1 = bt.fraction_at(10.0, 650.0);
425 let f2 = bt.fraction_at(100.0, 650.0);
426 assert!(
427 f2 > f1,
428 "bainite fraction must increase with time: {f1} → {f2}"
429 );
430 }
431 #[test]
432 fn test_bainite_rate_constant_increases_with_temperature() {
433 let bt = BainiteTransformation::new(2.0, 1e-3, 50_000.0, 800.0, 400.0);
434 let k_low = bt.rate_constant(500.0);
435 let k_high = bt.rate_constant(700.0);
436 assert!(k_high > k_low, "k should increase with temperature");
437 }
438 #[test]
439 fn test_bainite_nose_temperature() {
440 let bt = BainiteTransformation::new(2.0, 1e-3, 80_000.0, 750.0, 550.0);
441 assert!(
442 (bt.nose_temperature() - 650.0).abs() < 1e-10,
443 "nose at midpoint of Bs–Bf"
444 );
445 }
446 #[test]
447 fn test_bainite_transformation_rate_positive() {
448 let bt = BainiteTransformation::new(2.0, 1e-3, 80_000.0, 750.0, 500.0);
449 let rate = bt.transformation_rate(50.0, 650.0);
450 assert!(
451 rate > 0.0,
452 "transformation rate should be positive, got {rate}"
453 );
454 }
455 #[test]
456 fn test_pearlite_fraction_zero_above_ae1() {
457 let pt = PearliteTransformation::new(2.5, 1e-4, 90_000.0, 700.0, 1000.0);
458 assert_eq!(
459 pt.fraction_at(1000.0, 1100.0),
460 0.0,
461 "no pearlite above A_e1"
462 );
463 }
464 #[test]
465 fn test_pearlite_fraction_zero_below_ps() {
466 let pt = PearliteTransformation::new(2.5, 1e-4, 90_000.0, 700.0, 1000.0);
467 assert_eq!(pt.fraction_at(1000.0, 600.0), 0.0, "no pearlite below P_s");
468 }
469 #[test]
470 fn test_pearlite_fraction_increases_with_time() {
471 let pt = PearliteTransformation::new(2.5, 1e-4, 50_000.0, 600.0, 1000.0);
472 let f1 = pt.fraction_at(10.0, 800.0);
473 let f2 = pt.fraction_at(100.0, 800.0);
474 assert!(f2 > f1, "pearlite fraction must increase with time");
475 }
476 #[test]
477 fn test_pearlite_max_fraction_capped_at_one() {
478 let pt = PearliteTransformation::new(2.5, 1.0, 1.0, 400.0, 1200.0);
479 let f = pt.max_fraction_at(1e10, 800.0);
480 assert!(f <= 1.0 + 1e-12, "fraction must not exceed 1.0, got {f}");
481 }
482 #[test]
483 fn test_pearlite_incubation_time_positive() {
484 let pt = PearliteTransformation::new(2.5, 1e-4, 50_000.0, 600.0, 1000.0);
485 let t_inc = pt.incubation_time(800.0);
486 assert!(
487 t_inc > 0.0,
488 "incubation time should be positive, got {t_inc}"
489 );
490 }
491 #[test]
492 fn test_cct_martensite_at_high_rate() {
493 let cct = CctDiagram::new(1.0, 5.0, 50.0, 500.0, 820.0, 1.0);
494 let micro = cct.predict_microstructure(100.0);
495 assert_eq!(micro, "martensite", "fast cooling → martensite");
496 }
497 #[test]
498 fn test_cct_pearlite_at_low_rate() {
499 let cct = CctDiagram::new(2.0, 5.0, 50.0, 500.0, 820.0, 1.0);
500 let micro = cct.predict_microstructure(0.5);
501 assert_eq!(micro, "pearlite", "slow cooling → pearlite");
502 }
503 #[test]
504 fn test_cct_bainite_intermediate_rate() {
505 let cct = CctDiagram::new(1.0, 5.0, 50.0, 500.0, 820.0, 1.0);
506 let micro = cct.predict_microstructure(8.0);
507 assert_eq!(micro, "bainite", "intermediate rate → bainite");
508 }
509 #[test]
510 fn test_cct_martensite_fraction_at_critical_rate() {
511 let cct = CctDiagram::new(1.0, 5.0, 50.0, 500.0, 820.0, 1.0);
512 let fm = cct.martensite_fraction(50.0);
513 assert!(
514 (fm - 1.0).abs() < 1e-12,
515 "at critical rate, full martensite"
516 );
517 }
518 #[test]
519 fn test_cct_martensite_fraction_zero_at_slow_rate() {
520 let cct = CctDiagram::new(1.0, 5.0, 50.0, 500.0, 820.0, 1.0);
521 let fm = cct.martensite_fraction(0.1);
522 assert!(fm < 1e-12, "slow cooling → no martensite");
523 }
524 #[test]
525 fn test_ttt_start_time_infinite_above_ae1() {
526 let ttt = TttDiagram::new(820.0, 1.0, 1000.0, 500.0, 750.0);
527 let t = ttt.start_time(1100.0);
528 assert!(t.is_infinite(), "no transformation above A_e1");
529 }
530 #[test]
531 fn test_ttt_start_time_infinite_at_ms() {
532 let ttt = TttDiagram::new(820.0, 1.0, 1000.0, 500.0, 750.0);
533 let t = ttt.start_time(500.0);
534 assert!(t.is_infinite(), "no austenite transformation at Ms");
535 }
536 #[test]
537 fn test_ttt_bainite_range() {
538 let ttt = TttDiagram::new(820.0, 1.0, 1000.0, 500.0, 750.0);
539 let (low, high) = ttt.bainite_range();
540 assert_eq!(low, 500.0);
541 assert_eq!(high, 750.0);
542 }
543 #[test]
544 fn test_ttt_pearlite_range() {
545 let ttt = TttDiagram::new(820.0, 1.0, 1000.0, 500.0, 750.0);
546 let (low, high) = ttt.pearlite_range();
547 assert_eq!(low, 820.0);
548 assert_eq!(high, 1000.0);
549 }
550 #[test]
551 fn test_ttt_transformation_avoided_fast_cooling() {
552 let ttt = TttDiagram::new(820.0, 10.0, 1000.0, 500.0, 750.0);
553 assert!(
554 ttt.transformation_avoided(1000.0),
555 "fast quench avoids transformation"
556 );
557 }
558 #[test]
559 fn test_volume_fraction_tracker_initial_state() {
560 let tracker = VolumeFractionTracker::new(vec![
561 "austenite".into(),
562 "martensite".into(),
563 "bainite".into(),
564 ]);
565 assert!(
566 (tracker.fraction(0) - 1.0).abs() < 1e-12,
567 "start fully austenitic"
568 );
569 assert!(tracker.fraction(1).abs() < 1e-12);
570 assert!(tracker.fraction(2).abs() < 1e-12);
571 }
572 #[test]
573 fn test_volume_fraction_tracker_update() {
574 let mut tracker = VolumeFractionTracker::new(vec!["austenite".into(), "martensite".into()]);
575 tracker.update(vec![0.3, 0.7], 10.0, 450.0);
576 assert!((tracker.fraction(0) - 0.3).abs() < 1e-12);
577 assert!((tracker.fraction(1) - 0.7).abs() < 1e-12);
578 }
579 #[test]
580 fn test_volume_fraction_tracker_total() {
581 let mut tracker = VolumeFractionTracker::new(vec!["a".into(), "m".into(), "b".into()]);
582 tracker.update(vec![0.5, 0.3, 0.2], 5.0, 600.0);
583 assert!((tracker.total() - 1.0).abs() < 1e-12);
584 }
585 #[test]
586 fn test_volume_fraction_dominant_phase() {
587 let mut tracker = VolumeFractionTracker::new(vec![
588 "austenite".into(),
589 "martensite".into(),
590 "bainite".into(),
591 ]);
592 tracker.update(vec![0.1, 0.8, 0.1], 20.0, 420.0);
593 assert_eq!(tracker.dominant_phase(), 1, "martensite should dominate");
594 }
595 #[test]
596 fn test_volume_fraction_history_recorded() {
597 let mut tracker = VolumeFractionTracker::new(vec!["a".into(), "m".into()]);
598 tracker.update(vec![0.8, 0.2], 1.0, 700.0);
599 tracker.update(vec![0.5, 0.5], 2.0, 600.0);
600 assert_eq!(tracker.history.len(), 2);
601 }
602 #[test]
603 fn test_phase_props_fully_martensitic() {
604 let props = PhaseDependentProperties::medium_carbon_steel();
605 let fractions = [1.0_f64, 0.0, 0.0, 0.0];
606 let e = props.effective_youngs_modulus(&fractions);
607 assert!(
608 (e - 205e9).abs() < 1.0,
609 "fully martensitic E = 205 GPa, got {e}"
610 );
611 let sy = props.effective_yield_strength(&fractions);
612 assert!(
613 (sy - 1500e6).abs() < 1.0,
614 "martensitic yield = 1500 MPa, got {sy}"
615 );
616 }
617 #[test]
618 fn test_phase_props_rule_of_mixtures() {
619 let props = PhaseDependentProperties::medium_carbon_steel();
620 let fractions = [0.5_f64, 0.5, 0.0, 0.0];
621 let e = props.effective_youngs_modulus(&fractions);
622 let expected = 0.5 * 205e9 + 0.5 * 200e9;
623 assert!((e - expected).abs() < 1.0, "rule of mixtures E, got {e}");
624 }
625 #[test]
626 fn test_phase_props_hardness_increases_with_martensite() {
627 let props = PhaseDependentProperties::medium_carbon_steel();
628 let f_martensite = [1.0_f64, 0.0, 0.0, 0.0];
629 let f_ferrite = [0.0_f64, 0.0, 1.0, 0.0];
630 let hv_m = props.effective_hardness(&f_martensite);
631 let hv_f = props.effective_hardness(&f_ferrite);
632 assert!(
633 hv_m > hv_f,
634 "martensite harder than ferrite: {hv_m} vs {hv_f}"
635 );
636 }
637 #[test]
638 fn test_phase_props_uts_from_hardness() {
639 let props = PhaseDependentProperties::medium_carbon_steel();
640 let fractions = [1.0_f64, 0.0, 0.0, 0.0];
641 let uts = props.uts_from_hardness(&fractions);
642 let expected = 3.45e6 * 700.0;
643 assert!((uts - expected).abs() < 1.0, "UTS from hardness, got {uts}");
644 }
645 #[test]
646 fn test_clausius_clapeyron_water_ice_negative_slope() {
647 let cc = ClausiusClapeyron::water_ice();
648 let slope = cc.slope(cc.t0);
649 assert!(
650 slope < 0.0,
651 "Water/ice dP/dT should be negative, got {slope}"
652 );
653 }
654 #[test]
655 fn test_clausius_clapeyron_melting_temperature_increases_with_pressure_for_normal_substance() {
656 let cc = ClausiusClapeyron::new(6000.0, 300.0, 101325.0, 1.0e-6);
657 let t_low = cc.melting_temperature(101325.0);
658 let t_high = cc.melting_temperature(1.0e6);
659 assert!(t_high > t_low, "For ΔV>0, higher P → higher T_m");
660 }
661 #[test]
662 fn test_clausius_clapeyron_equilibrium_pressure_at_t0_equals_p0() {
663 let cc = ClausiusClapeyron::water_ice();
664 let p = cc.equilibrium_pressure(cc.t0);
665 assert!(
666 (p - cc.p0).abs() < 1e-6,
667 "At Tâ‚€, equilibrium pressure = Pâ‚€, got {p}"
668 );
669 }
670 #[test]
671 fn test_clausius_clapeyron_latent_heat_at_reference() {
672 let cc = ClausiusClapeyron::water_ice();
673 let l = cc.latent_heat_at(cc.t0, 0.0);
674 assert!((l - cc.latent_heat).abs() < 1e-10, "L at Tâ‚€ = L, got {l}");
675 }
676 #[test]
677 fn test_clausius_clapeyron_latent_heat_increases_with_delta_cp_positive() {
678 let cc = ClausiusClapeyron::new(6000.0, 300.0, 101325.0, 1.0e-6);
679 let l0 = cc.latent_heat_at(300.0, 10.0);
680 let l1 = cc.latent_heat_at(350.0, 10.0);
681 assert!(l1 > l0, "L should increase with T when ΔCp > 0");
682 }
683 #[test]
684 fn test_clausius_clapeyron_slope_zero_at_zero_delta_v() {
685 let cc = ClausiusClapeyron::new(6000.0, 300.0, 101325.0, 0.0);
686 let slope = cc.slope(300.0);
687 assert_eq!(slope, 0.0, "Zero ΔV → zero slope");
688 }
689 #[test]
690 fn test_clausius_clapeyron_roundtrip_pressure_temperature() {
691 let cc = ClausiusClapeyron::new(6000.0, 300.0, 101325.0, 1.0e-6);
692 let t_test = 310.0_f64;
693 let p = cc.equilibrium_pressure(t_test);
694 let t_back = cc.melting_temperature(p);
695 assert!(
696 (t_back - t_test).abs() < 1e-6,
697 "P→T roundtrip: {t_back} vs {t_test}"
698 );
699 }
700 #[test]
701 fn test_lever_rule_at_alpha_composition() {
702 let lr = LeverRule::new(0.1, 0.4);
703 let f_beta = lr.beta_fraction(0.1);
704 assert!(f_beta.abs() < 1e-10, "At x_α, f_β = 0, got {f_beta}");
705 }
706 #[test]
707 fn test_lever_rule_at_beta_composition() {
708 let lr = LeverRule::new(0.1, 0.4);
709 let f_beta = lr.beta_fraction(0.4);
710 assert!(
711 (f_beta - 1.0).abs() < 1e-10,
712 "At x_β, f_β = 1, got {f_beta}"
713 );
714 }
715 #[test]
716 fn test_lever_rule_at_midpoint() {
717 let lr = LeverRule::new(0.1, 0.3);
718 let f_beta = lr.beta_fraction(0.2);
719 assert!(
720 (f_beta - 0.5).abs() < 1e-10,
721 "At midpoint, f_β = 0.5, got {f_beta}"
722 );
723 }
724 #[test]
725 fn test_lever_rule_fractions_sum_to_one() {
726 let lr = LeverRule::new(0.05, 0.25);
727 let x = 0.15;
728 let fa = lr.alpha_fraction(x);
729 let fb = lr.beta_fraction(x);
730 assert!(
731 (fa + fb - 1.0).abs() < 1e-10,
732 "Phase fractions must sum to 1"
733 );
734 }
735 #[test]
736 fn test_lever_rule_is_two_phase() {
737 let lr = LeverRule::new(0.1, 0.4);
738 assert!(lr.is_two_phase(0.2), "x=0.2 is in two-phase region");
739 assert!(!lr.is_two_phase(0.05), "x=0.05 is outside two-phase region");
740 assert!(!lr.is_two_phase(0.5), "x=0.5 is outside two-phase region");
741 }
742 #[test]
743 fn test_lever_rule_tie_line_length() {
744 let lr = LeverRule::new(0.1, 0.4);
745 assert!((lr.tie_line_length() - 0.3).abs() < 1e-10, "Tie line = 0.3");
746 }
747 #[test]
748 fn test_lever_rule_clamped_outside_range() {
749 let lr = LeverRule::new(0.1, 0.4);
750 assert_eq!(lr.beta_fraction(0.0), 0.0, "Below α: f_β = 0");
751 assert_eq!(lr.beta_fraction(0.5), 1.0, "Above β: f_β = 1");
752 }
753 #[test]
754 fn test_ternary_lever_rule_at_alpha() {
755 let f = ternary_lever_rule(0.1, 0.05, 0.1, 0.05, 0.3, 0.1);
756 assert!(f.abs() < 1e-10, "At α, f_β = 0");
757 }
758 #[test]
759 fn test_ternary_lever_rule_at_beta() {
760 let f = ternary_lever_rule(0.3, 0.1, 0.1, 0.05, 0.3, 0.1);
761 assert!((f - 1.0).abs() < 1e-10, "At β, f_β = 1");
762 }
763 #[test]
764 fn test_spinodal_f_double_prime_positive_at_low_omega() {
765 let spd = SpinocalDecomposition::new(1000.0, 1e-10, 1e-5);
766 let f2 = spd.f_double_prime(0.5, 1000.0);
767 assert!(f2 > 0.0, "f'' > 0 for stable mixture, got {f2}");
768 }
769 #[test]
770 fn test_spinodal_f_double_prime_negative_at_high_omega() {
771 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
772 let f2 = spd.f_double_prime(0.5, 300.0);
773 assert!(f2 < 0.0, "f'' < 0 in spinodal region, got {f2}");
774 }
775 #[test]
776 fn test_spinodal_is_spinodal_at_center_high_omega() {
777 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
778 assert!(
779 spd.is_spinodal(0.5, 300.0),
780 "c=0.5 should be in spinodal at high Ω"
781 );
782 }
783 #[test]
784 fn test_spinodal_boundary_symmetric() {
785 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
786 if let Some((c_low, c_high)) = spd.spinodal_boundary(300.0) {
787 assert!(
788 (c_low + c_high - 1.0).abs() < 1e-10,
789 "Spinodal boundary symmetric about 0.5"
790 );
791 assert!(c_low < 0.5 && c_high > 0.5, "Boundary straddles 0.5");
792 } else {
793 panic!("Expected spinodal boundary at T=300");
794 }
795 }
796 #[test]
797 fn test_spinodal_no_boundary_above_critical_temperature() {
798 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
799 let tc = spd.critical_temperature();
800 let result = spd.spinodal_boundary(tc * 1.5);
801 assert!(result.is_none(), "No spinodal above T_c");
802 }
803 #[test]
804 fn test_spinodal_critical_temperature_formula() {
805 let omega = 40000.0_f64;
806 let spd = SpinocalDecomposition::new(omega, 1e-10, 1e-5);
807 let tc = spd.critical_temperature();
808 let r = 8.314_462_618;
809 let expected = omega / (2.0 * r);
810 assert!(
811 (tc - expected).abs() / expected < 1e-10,
812 "T_c formula: {tc}"
813 );
814 }
815 #[test]
816 fn test_spinodal_fastest_wavelength_exists_in_spinodal() {
817 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
818 let lambda = spd.fastest_growing_wavelength(0.5, 300.0);
819 assert!(
820 lambda.is_some(),
821 "Should have fastest wavelength in spinodal region"
822 );
823 assert!(lambda.unwrap() > 0.0, "Wavelength must be positive");
824 }
825 #[test]
826 fn test_spinodal_amplification_positive_in_spinodal() {
827 let spd = SpinocalDecomposition::new(50000.0, 1e-10, 1e-5);
828 if let Some(lambda) = spd.fastest_growing_wavelength(0.5, 300.0) {
829 let q = 2.0 * std::f64::consts::PI / lambda;
830 let r = spd.amplification_factor(q, 0.5, 300.0, 1.0e-20);
831 assert!(
832 r > 0.0,
833 "Amplification factor should be positive at fastest q, got {r}"
834 );
835 }
836 }
837 #[test]
838 fn test_latent_heat_volumetric_steel() {
839 let lh = LatentHeatModel::steel_solidification();
840 let v = lh.volumetric_latent_heat();
841 let expected = 271_000.0 * 7800.0;
842 assert!(
843 (v - expected).abs() < 1.0,
844 "Volumetric latent heat steel: {v}"
845 );
846 }
847 #[test]
848 fn test_latent_heat_enthalpy_release_full_transformation() {
849 let lh = LatentHeatModel::aluminum_solidification();
850 let h = lh.enthalpy_release(1.0);
851 assert!(
852 (h - lh.volumetric_latent_heat()).abs() < 1.0,
853 "Full transformation releases all latent heat"
854 );
855 }
856 #[test]
857 fn test_latent_heat_enthalpy_release_half_transformation() {
858 let lh = LatentHeatModel::steel_solidification();
859 let h_half = lh.enthalpy_release(0.5);
860 let h_full = lh.enthalpy_release(1.0);
861 assert!(
862 (h_half - 0.5 * h_full).abs() < 1.0,
863 "Half transform → half latent heat"
864 );
865 }
866 #[test]
867 fn test_latent_heat_adiabatic_rise_positive() {
868 let lh = LatentHeatModel::steel_solidification();
869 let dt = lh.adiabatic_temperature_rise(1.0, 500.0);
870 assert!(dt > 0.0, "Adiabatic temperature rise should be positive");
871 }
872 #[test]
873 fn test_latent_heat_adiabatic_rise_formula() {
874 let lh = LatentHeatModel::new(300_000.0, 8000.0);
875 let cp = 500.0;
876 let dt = lh.adiabatic_temperature_rise(1.0, cp);
877 let expected = 300_000.0 / cp;
878 assert!(
879 (dt - expected).abs() < 1e-6,
880 "ΔT = L/Cp: {dt} vs {expected}"
881 );
882 }
883 #[test]
884 fn test_latent_heat_fraction_released() {
885 let lh = LatentHeatModel::steel_solidification();
886 let df = lh.fraction_released(0.2, 0.8);
887 assert!(
888 (df - 0.6).abs() < 1e-10,
889 "Fraction released = 0.6, got {df}"
890 );
891 }
892 #[test]
893 fn test_latent_heat_zero_cp_returns_zero() {
894 let lh = LatentHeatModel::steel_solidification();
895 let dt = lh.adiabatic_temperature_rise(1.0, 0.0);
896 assert_eq!(dt, 0.0, "Zero Cp → zero ΔT");
897 }
898 #[test]
899 fn test_jmak_extended_fraction_zero_at_time_zero() {
900 let jmak = JmakExtended::new(1e-10, 2.0, 80_000.0, 1.0);
901 assert_eq!(jmak.fraction(0.0, 900.0), 0.0, "f(0) = 0");
902 }
903 #[test]
904 fn test_jmak_extended_fraction_approaches_one() {
905 let jmak = JmakExtended::new(1e-5, 2.0, 1.0, 1.0);
906 let f = jmak.fraction(1e10, 1000.0);
907 assert!((f - 1.0).abs() < 1e-6, "f → 1 at large t, got {f}");
908 }
909 #[test]
910 fn test_jmak_extended_fraction_monotonically_increasing() {
911 let jmak = JmakExtended::new(1e-10, 2.0, 50_000.0, 1.0);
912 let f1 = jmak.fraction(10.0, 900.0);
913 let f2 = jmak.fraction(100.0, 900.0);
914 let f3 = jmak.fraction(1000.0, 900.0);
915 assert!(f2 > f1 && f3 > f2, "JMAK fraction must increase");
916 }
917 #[test]
918 fn test_jmak_extended_higher_temp_faster_transform() {
919 let jmak = JmakExtended::new(1e-10, 2.0, 80_000.0, 1.0);
920 let f_low = jmak.fraction(100.0, 700.0);
921 let f_high = jmak.fraction(100.0, 1200.0);
922 assert!(f_high > f_low, "Higher T → faster transformation");
923 }
924 #[test]
925 fn test_jmak_extended_time_to_fraction_roundtrip() {
926 let jmak = JmakExtended::new(1e-10, 2.0, 50_000.0, 1.0);
927 let target = 0.5;
928 let t = jmak.time_to_fraction(target, 1000.0);
929 let f_back = jmak.fraction(t, 1000.0);
930 assert!(
931 (f_back - target).abs() < 1e-6,
932 "JMAK roundtrip: {f_back} vs {target}"
933 );
934 }
935 #[test]
936 fn test_jmak_extended_ttt_start_before_finish() {
937 let jmak = JmakExtended::new(1e-10, 2.0, 50_000.0, 1.0);
938 let t1_pct = jmak.time_to_fraction(0.01, 1000.0);
939 let t99_pct = jmak.time_to_fraction(0.99, 1000.0);
940 assert!(
941 t1_pct.is_finite() && t99_pct.is_finite(),
942 "Times must be finite"
943 );
944 assert!(
945 t99_pct > t1_pct,
946 "99% transform takes longer than 1%: t1={t1_pct}, t99={t99_pct}"
947 );
948 }
949 #[test]
950 fn test_jmak_extended_transformation_rate_positive() {
951 let jmak = JmakExtended::new(1e-10, 2.0, 50_000.0, 1.0);
952 let rate = jmak.transformation_rate(100.0, 1000.0);
953 assert!(
954 rate > 0.0,
955 "Transformation rate must be positive, got {rate}"
956 );
957 }
958 #[test]
959 fn test_jmak_extended_impingement_reduces_fraction() {
960 let jmak_full = JmakExtended::new(1e-10, 2.0, 50_000.0, 1.0);
961 let jmak_partial = JmakExtended::new(1e-10, 2.0, 50_000.0, 0.5);
962 let f_full = jmak_full.fraction(1000.0, 1000.0);
963 let f_partial = jmak_partial.fraction(1000.0, 1000.0);
964 assert!(
965 f_partial < f_full,
966 "Lower impingement → lower fraction at same time"
967 );
968 }
969}
970#[cfg(test)]
971mod tests_new {
972
973 use crate::EutecticTransformation;
974
975 use crate::ScheilSolidification;
976
977 use crate::StressAidedMartensite;
978
979 use crate::TttCcurve;
980
981 use crate::TttExtended;
982
983 #[test]
984 fn test_scheil_liquid_composition_at_zero_fraction() {
985 let sch = ScheilSolidification::new(4.0, 0.17);
986 let c_l = sch.liquid_composition(0.0);
987 assert!((c_l - 4.0).abs() < 1e-6, "C_L(f_s=0) = C_0, got {c_l}");
988 }
989 #[test]
990 fn test_scheil_liquid_composition_increases_with_fs() {
991 let sch = ScheilSolidification::new(4.0, 0.17);
992 let c_l0 = sch.liquid_composition(0.0);
993 let c_l5 = sch.liquid_composition(0.5);
994 assert!(
995 c_l5 > c_l0,
996 "C_L increases with f_s for k < 1: {c_l0} → {c_l5}"
997 );
998 }
999 #[test]
1000 fn test_scheil_solid_front_composition_proportional_to_liquid() {
1001 let sch = ScheilSolidification::al_cu();
1002 let f_s = 0.3;
1003 let c_l = sch.liquid_composition(f_s);
1004 let c_s = sch.solid_composition_at_front(f_s);
1005 assert!(
1006 (c_s - sch.k_partition * c_l).abs() < 1e-10,
1007 "C_S* = k * C_L"
1008 );
1009 }
1010 #[test]
1011 fn test_scheil_mean_solid_composition_less_than_c0() {
1012 let sch = ScheilSolidification::new(4.0, 0.17);
1013 let c_s_mean = sch.mean_solid_composition(0.5);
1014 assert!(
1015 c_s_mean < sch.c0,
1016 "Mean solid composition < C_0 for k < 1: {c_s_mean}"
1017 );
1018 }
1019 #[test]
1020 fn test_scheil_eutectic_solid_fraction_increases_toward_one() {
1021 let sch = ScheilSolidification::al_cu();
1022 if let Some(f_s) = sch.eutectic_solid_fraction(33.0) {
1023 assert!(f_s > 0.0 && f_s < 1.0, "Eutectic f_s in (0,1): {f_s}");
1024 }
1025 }
1026 #[test]
1027 fn test_scheil_solidification_range() {
1028 let sch = ScheilSolidification::new(4.0, 0.17);
1029 let (t_l, t_e) = sch.solidification_range(933.0, -3.4, 33.0);
1030 assert!(t_l > t_e, "T_liquidus > T_eutectic: {t_l} > {t_e}");
1031 }
1032 #[test]
1033 fn test_scheil_fe_c_liquid_composition() {
1034 let sch = ScheilSolidification::fe_c();
1035 let c_l = sch.liquid_composition(0.3);
1036 assert!(c_l > sch.c0, "Fe-C: C_L increases with solidification");
1037 }
1038 #[test]
1039 fn test_ttt_extended_martensite_fraction_at_ms() {
1040 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1041 let fm = ttt.martensite_fraction(500.0);
1042 assert!(fm.abs() < 1e-10, "No martensite at Ms exactly, got {fm}");
1043 }
1044 #[test]
1045 fn test_ttt_extended_martensite_increases_below_ms() {
1046 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1047 let fm1 = ttt.martensite_fraction(490.0);
1048 let fm2 = ttt.martensite_fraction(400.0);
1049 assert!(fm2 > fm1, "More martensite at lower T: {fm1} vs {fm2}");
1050 }
1051 #[test]
1052 fn test_ttt_extended_pearlite_zero_above_ae1() {
1053 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1054 let fp = ttt.pearlite_fraction(1e6, 1100.0);
1055 assert!(fp.abs() < 1e-12, "No pearlite above A_e1");
1056 }
1057 #[test]
1058 fn test_ttt_extended_bainite_fraction_increases_with_time() {
1059 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1060 let fb1 = ttt.bainite_fraction(1.0, 650.0);
1061 let fb2 = ttt.bainite_fraction(100.0, 650.0);
1062 assert!(
1063 fb2 >= fb1,
1064 "Bainite fraction non-decreasing with time: {fb1} vs {fb2}"
1065 );
1066 }
1067 #[test]
1068 fn test_ttt_extended_dominant_product_austenite_above_ae1() {
1069 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1070 let product = ttt.dominant_product(1.0, 1100.0);
1071 assert_eq!(product, "austenite", "Above A_e1 → austenite");
1072 }
1073 #[test]
1074 fn test_ttt_extended_dominant_product_martensite_below_ms() {
1075 let ttt = TttExtended::new(820.0, 1.0, 650.0, 0.5, 500.0, 1000.0, 3.0, 2.5);
1076 let product = ttt.dominant_product(1.0, 400.0);
1077 assert_eq!(product, "martensite", "Below Ms → martensite");
1078 }
1079 #[test]
1080 fn test_eutectic_alpha_plus_beta_fractions_sum_to_one() {
1081 let eut = EutecticTransformation::al_si();
1082 let fa = eut.alpha_fraction();
1083 let fb = eut.beta_fraction();
1084 assert!(
1085 (fa + fb - 1.0).abs() < 1e-10,
1086 "Phase fractions sum to 1: {fa} + {fb}"
1087 );
1088 }
1089 #[test]
1090 fn test_eutectic_al_si_alpha_fraction_in_range() {
1091 let eut = EutecticTransformation::al_si();
1092 let fa = eut.alpha_fraction();
1093 assert!(
1094 fa > 0.0 && fa < 1.0,
1095 "Al-Si α fraction should be in (0,1): {fa}"
1096 );
1097 }
1098 #[test]
1099 fn test_eutectic_lamellar_spacing_decreases_with_velocity() {
1100 let eut = EutecticTransformation::al_si();
1101 let lam_slow = eut.lamellar_spacing(1e-6);
1102 let lam_fast = eut.lamellar_spacing(1e-4);
1103 assert!(
1104 lam_fast < lam_slow,
1105 "Faster growth → finer lamellar spacing"
1106 );
1107 }
1108 #[test]
1109 fn test_eutectic_growth_velocity_decreases_with_spacing() {
1110 let eut = EutecticTransformation::al_si();
1111 let v_fine = eut.growth_velocity(1e-7);
1112 let v_coarse = eut.growth_velocity(1e-6);
1113 assert!(v_fine > v_coarse, "Finer spacing → higher velocity");
1114 }
1115 #[test]
1116 fn test_eutectic_spacing_velocity_product_constant() {
1117 let eut = EutecticTransformation::al_si();
1118 let v = 1e-5;
1119 let lam = eut.lamellar_spacing(v);
1120 let product = lam * lam * v;
1121 assert!(
1122 (product - eut.k_jh).abs() / eut.k_jh < 1e-6,
1123 "λ²·v = K_JH: {product} vs {}",
1124 eut.k_jh
1125 );
1126 }
1127 #[test]
1128 fn test_eutectic_is_eutectic_composition() {
1129 let eut = EutecticTransformation::al_si();
1130 assert!(
1131 eut.is_eutectic_composition(eut.c_eutectic, 0.01),
1132 "Exact eutectic composition matches"
1133 );
1134 assert!(
1135 !eut.is_eutectic_composition(5.0, 0.01),
1136 "Non-eutectic composition does not match"
1137 );
1138 }
1139 #[test]
1140 fn test_ttt_ccurve_minimum_time_at_nose() {
1141 let ccurve = TttCcurve::new(820.0, 1.0, 0.001, 3.0);
1142 let tau_nose = ccurve.incubation_time(820.0);
1143 let tau_off = ccurve.incubation_time(700.0);
1144 assert!(
1145 tau_off > tau_nose,
1146 "Incubation time minimum at nose, τ_nose={tau_nose}, τ_off={tau_off}"
1147 );
1148 }
1149 #[test]
1150 fn test_ttt_ccurve_fraction_monotonically_increasing() {
1151 let ccurve = TttCcurve::new(820.0, 1.0, 0.001, 3.0);
1152 let f1 = ccurve.fraction(1.0, 820.0);
1153 let f2 = ccurve.fraction(10.0, 820.0);
1154 let f3 = ccurve.fraction(100.0, 820.0);
1155 assert!(f2 > f1 && f3 > f2, "Fraction increases with time");
1156 }
1157 #[test]
1158 fn test_ttt_ccurve_time_to_fraction_roundtrip() {
1159 let ccurve = TttCcurve::new(820.0, 1.0, 0.001, 3.0);
1160 let target = 0.5;
1161 let t = ccurve.time_to_fraction(target, 820.0);
1162 let f_back = ccurve.fraction(t, 820.0);
1163 assert!(
1164 (f_back - target).abs() < 1e-6,
1165 "TTT C-curve roundtrip: {f_back} vs {target}"
1166 );
1167 }
1168 #[test]
1169 fn test_ttt_ccurve_nose_temperature_correct() {
1170 let ccurve = TttCcurve::new(820.0, 1.0, 0.001, 3.0);
1171 assert!((ccurve.nose_temperature() - 820.0).abs() < 1e-10);
1172 }
1173 #[test]
1174 fn test_stress_aided_ms_shifts_with_stress() {
1175 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1176 let ms_no_stress = sam.ms_temperature_under_stress(0.0);
1177 let ms_stressed = sam.ms_temperature_under_stress(100.0e6);
1178 assert_eq!(ms_no_stress, 500.0, "No-stress Ms = 500 K");
1179 assert!(ms_stressed != ms_no_stress, "Ms shifts with stress");
1180 }
1181 #[test]
1182 fn test_stress_aided_shear_band_fraction_zero_at_zero_strain() {
1183 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1184 assert_eq!(sam.shear_band_fraction(0.0), 0.0);
1185 }
1186 #[test]
1187 fn test_stress_aided_shear_band_fraction_increases_with_strain() {
1188 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1189 let f1 = sam.shear_band_fraction(0.1);
1190 let f2 = sam.shear_band_fraction(0.5);
1191 assert!(f2 > f1, "Shear band fraction increases with strain");
1192 }
1193 #[test]
1194 fn test_stress_aided_martensite_from_strain_increases() {
1195 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1196 let fm1 = sam.martensite_fraction_from_strain(0.1);
1197 let fm2 = sam.martensite_fraction_from_strain(0.5);
1198 assert!(
1199 fm2 > fm1,
1200 "Martensite fraction increases with strain: {fm1} vs {fm2}"
1201 );
1202 }
1203 #[test]
1204 fn test_stress_aided_transformation_mode_thermal_below_ms() {
1205 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1206 let mode = sam.transformation_mode(400.0, 0.0);
1207 assert_eq!(mode, "thermal", "Below Msâ‚€: thermal transformation");
1208 }
1209 #[test]
1210 fn test_stress_aided_transformation_mode_none_above_both() {
1211 let sam = StressAidedMartensite::new(500.0, 1.0e-7, 4.0, 4.5, 4.0);
1212 let mode = sam.transformation_mode(600.0, 0.0);
1213 assert_eq!(mode, "none", "Above Ms(σ): no transformation");
1214 }
1215}