forked from 1b-agent/fieldcraft-upstream
FEM foundation B: heat transfer and thermal stress (solids, 2D, explicit), coupled temperature–displacement, solve.heat (#45)
* Heat transfer and thermal stress: thermal materials, temperature BCs, heat loads, heat transfer and coupled temperature–displacement steps, solve.heat
Model: Material.thermal (conductivity, specific heat, expansion, isotropic or
per axis; reference temperature), BoundaryCondition::Temperature (dof 11),
loads SurfaceFlux, Convection, HeatSource and Temperature (a field in static
steps), procedures HeatTransfer { transient (θ-method) } and
CoupledTemperatureDisplacement; checks for missing conductivity/specific
heat/expansion. fem: heat conduction kernels (conductivity, capacity,
source, face flux/film, heat flux) on the DofLayout (one T per node), thermal
strain load and stress D(ε − ε_th) for every solid formulation incl.
incompatible modes. Solver: steady and transient heat transfer, thermal loads
in static steps, coupled steps (exact one-way coupling). Engine: solve.heat,
temperature/heat-flux result fields, probes. Decks refuse heat transfer by
name. Analytic verification: slab, source+convection/flux, thick cylinder,
erfc semi-infinite solid, free/constrained expansion, bimetal strip, coupled
step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* thermal: strains in 2D elements and explicit dynamics
Plane::thermal_load and stresses_with for plane stress (εzz free), plane
strain (σzz = −D₃·ε_th) and axisymmetric elements, incompatible modes
included; the solver's 2D kernels take them. Explicit dynamics turns a
step's temperature loads into a per-element thermal strain rate (reached
linearly over the step) subtracted from each strain increment, so it feeds
J2 and Johnson–Cook updates. Verified: free expansion in every 2D theory
and element, a biaxially held plate, an explicit free bar (ΔL = αΔT L,
stress-free) and a clamped bar at −EαΔT, capped at the yield stress with
perfect plasticity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* clippy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* thermal: trusses take the axial thermal strain; keep the roadmap's M7 line
Truss::thermal_load and stresses_with project the thermal strain on the
member's tangent (Σ αₖ tₖ² ΔT); the static solver applies them to rod2/rod3
(#38). Explicit trusses take none yet and a warning says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
---------
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
37 files changed
+2576
-90
No files matched your search
+65
-4
@@ -29,7 +29,7 @@ use modelcraft_model::{Elastic, ElementKind};
|
||||
use crate::basis::Basis;
|
||||
use crate::dense::Matrix;
|
||||
use crate::dof::{DofSet, ElementDofs};
|
||||
use crate::element::PointStress;
|
||||
use crate::element::{PointStress, ThermalStrain};
|
||||
use crate::quadrature::gauss_legendre;
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -196,12 +196,51 @@ impl<'a> Truss<'a> {
|
||||
|
||||
/// Strain and stress at the integration points as global tensors (see the module docs).
|
||||
pub fn stresses(&self, young: f64, u: &[[f64; 3]]) -> Result<Vec<PointStress>> {
|
||||
let strains = self.axial_strains(u)?;
|
||||
self.stresses_with(young, u, None)
|
||||
}
|
||||
|
||||
/// The axial thermal strain at each integration point: the thermal strain tensor
|
||||
/// diag(α) ΔT along the tangent, Σ αₖ tₖ² ΔT, with ΔT interpolated from the nodes.
|
||||
fn thermal_strains(&self, thermal: &ThermalStrain) -> Result<Vec<f64>> {
|
||||
thermal.check(self.nodes())?;
|
||||
Ok(self
|
||||
.points()?
|
||||
.iter()
|
||||
.zip(strains)
|
||||
.map(|(p, e)| PointStress { xyz: p.xyz, strain: uniaxial(e, p.tangent, 2.0), stress: uniaxial(young * e, p.tangent, 1.0) })
|
||||
.map(|p| {
|
||||
let dt: f64 = p.n.iter().zip(&thermal.delta).map(|(a, b)| a * b).sum();
|
||||
(0..3).map(|k| thermal.alpha[k] * p.tangent[k] * p.tangent[k]).sum::<f64>() * dt
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The equivalent nodal forces of a thermal strain, ∫ Bᵀ E A ε_th ds.
|
||||
pub fn thermal_load(&self, young: f64, area: f64, thermal: &ThermalStrain) -> Result<Vec<[f64; 3]>> {
|
||||
let strains = self.thermal_strains(thermal)?;
|
||||
let mut f = vec![[0.0; 3]; self.nodes()];
|
||||
for (p, e) in self.points()?.iter().zip(strains) {
|
||||
let force = young * area * e * p.weight * p.jacobian;
|
||||
for (fi, ds) in f.iter_mut().zip(p.ds) {
|
||||
for k in 0..3 {
|
||||
fi[k] += force * ds * p.tangent[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Strain and stress at the integration points with an optional thermal strain: the
|
||||
/// stress is E (ε − ε_th), the strain reported the total.
|
||||
pub fn stresses_with(&self, young: f64, u: &[[f64; 3]], thermal: Option<&ThermalStrain>) -> Result<Vec<PointStress>> {
|
||||
let strains = self.axial_strains(u)?;
|
||||
let thermal = match thermal {
|
||||
Some(t) => self.thermal_strains(t)?,
|
||||
None => vec![0.0; strains.len()],
|
||||
};
|
||||
Ok(self
|
||||
.points()?
|
||||
.iter()
|
||||
.zip(strains.into_iter().zip(thermal))
|
||||
.map(|(p, (e, th))| PointStress { xyz: p.xyz, strain: uniaxial(e, p.tangent, 2.0), stress: uniaxial(young * (e - th), p.tangent, 1.0) })
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -401,4 +440,26 @@ mod tests {
|
||||
assert!(err < 0.01 * span.powi(4), "{span}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermal_strain_follows_the_axis() {
|
||||
// A skew rod3 with orthotropic α: the free expansion along the member, u = ε_t (x − x₀)
|
||||
// with ε_t = Σ αₖ tₖ² ΔT, is stress-free and balances the thermal load (K u = f_th).
|
||||
let t = [2.0f64, 1.0, 2.0].map(|c| c / 3.0);
|
||||
let xyz = [[0.0; 3], t.map(|c| 3.0 * c), t.map(|c| 1.5 * c)];
|
||||
let truss = Truss::new(ElementKind::Rod3, &xyz).unwrap();
|
||||
let alpha = [1.0e-5, 2.0e-5, 3.0e-5];
|
||||
let th = ThermalStrain { alpha, delta: vec![40.0; 3] };
|
||||
let eps: f64 = (0..3).map(|k| alpha[k] * t[k] * t[k]).sum::<f64>() * 40.0;
|
||||
let u: Vec<[f64; 3]> = xyz.iter().map(|x| x.map(|c| eps * c)).collect();
|
||||
for p in truss.stresses_with(2e5, &u, Some(&th)).unwrap() {
|
||||
assert!(p.stress.iter().all(|s| s.abs() < 1e-9), "{:?}", p.stress);
|
||||
}
|
||||
let f = truss.thermal_load(2e5, 2.0, &th).unwrap();
|
||||
let ku = truss.stiffness(2e5, 2.0).unwrap().mul_vec(&u.iter().flatten().copied().collect::<Vec<_>>());
|
||||
for (a, b) in f.iter().flatten().zip(&ku) {
|
||||
assert!((a - b).abs() < 1e-9 * 2e5 * 2.0 * eps, "{a} vs {b}");
|
||||
}
|
||||
assert!(truss.thermal_load(2e5, 2.0, &ThermalStrain { alpha, delta: vec![1.0] }).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user