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
+31
-5
@@ -142,20 +142,28 @@ pub enum BoundaryCondition {
|
||||
Displacement { target: Target, values: [Option<f64>; 6] },
|
||||
/// Mirror symmetry about a plane through the nodes.
|
||||
Symmetry { target: Target, plane: Symmetry },
|
||||
/// A prescribed temperature (degree of freedom [`TEMPERATURE_DOF`]) at the target's nodes,
|
||||
/// in heat transfer and coupled temperature–displacement steps.
|
||||
Temperature { target: Target, value: f64 },
|
||||
}
|
||||
|
||||
/// The model's number of the temperature degree of freedom (the deck formats' convention).
|
||||
pub const TEMPERATURE_DOF: u8 = 11;
|
||||
|
||||
impl BoundaryCondition {
|
||||
pub fn target(&self) -> &Target {
|
||||
match self {
|
||||
BoundaryCondition::Fixed { target } | BoundaryCondition::Displacement { target, .. } | BoundaryCondition::Symmetry { target, .. } => {
|
||||
target
|
||||
}
|
||||
BoundaryCondition::Fixed { target }
|
||||
| BoundaryCondition::Displacement { target, .. }
|
||||
| BoundaryCondition::Symmetry { target, .. }
|
||||
| BoundaryCondition::Temperature { target, .. } => target,
|
||||
}
|
||||
}
|
||||
|
||||
/// The constrained degrees of freedom.
|
||||
/// The constrained structural degrees of freedom (1–6); none for a temperature.
|
||||
pub fn dofs(&self) -> Dofs {
|
||||
match self {
|
||||
BoundaryCondition::Temperature { .. } => Dofs::none(),
|
||||
BoundaryCondition::Fixed { .. } => Dofs::all(),
|
||||
BoundaryCondition::Displacement { values, .. } => {
|
||||
Dofs(values.iter().enumerate().filter(|(_, v)| v.is_some()).fold(0, |m, (i, _)| m | (1 << i)))
|
||||
@@ -164,15 +172,28 @@ impl BoundaryCondition {
|
||||
}
|
||||
}
|
||||
|
||||
/// The prescribed value of a constrained degree of freedom (1–6); `None` when it is free.
|
||||
/// Whether it prescribes temperatures (rather than motion).
|
||||
pub fn is_thermal(&self) -> bool {
|
||||
matches!(self, BoundaryCondition::Temperature { .. })
|
||||
}
|
||||
|
||||
/// The prescribed value of a constrained degree of freedom (1–6, or [`TEMPERATURE_DOF`]);
|
||||
/// `None` when it is free.
|
||||
pub fn value(&self, dof: u8) -> Option<f64> {
|
||||
match self {
|
||||
BoundaryCondition::Temperature { value, .. } => (dof == TEMPERATURE_DOF).then_some(*value),
|
||||
BoundaryCondition::Displacement { values, .. } => values.get(usize::from(dof).checked_sub(1)?).copied().flatten(),
|
||||
_ => self.dofs().contains(dof).then_some(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if let BoundaryCondition::Temperature { target, value } = self {
|
||||
finite("temperature", *value)?;
|
||||
if let Target::ReferencePoint(r) = target {
|
||||
return Err(Error::Invalid(format!("reference points have no temperature (`{r}`); prescribe it on a set")));
|
||||
}
|
||||
}
|
||||
if let BoundaryCondition::Displacement { values, .. } = self {
|
||||
for (i, v) in values.iter().enumerate() {
|
||||
if let Some(v) = v {
|
||||
@@ -241,6 +262,11 @@ mod tests {
|
||||
assert_eq!(BoundaryCondition::Fixed { target: t.clone() }.value(6), Some(0.0));
|
||||
assert!(BoundaryCondition::Displacement { target: t.clone(), values: [None; 6] }.validate().is_err());
|
||||
assert!(BoundaryCondition::Displacement { target: t, values: [Some(f64::NAN), None, None, None, None, None] }.validate().is_err());
|
||||
let hot = BoundaryCondition::Temperature { target: Target::Set("s".into()), value: 80.0 };
|
||||
assert_eq!((hot.value(TEMPERATURE_DOF), hot.value(1), hot.dofs().list()), (Some(80.0), None, vec![]));
|
||||
assert!(hot.is_thermal());
|
||||
assert!(BoundaryCondition::Temperature { target: Target::Set("s".into()), value: f64::INFINITY }.validate().is_err());
|
||||
assert!(BoundaryCondition::Temperature { target: Target::ReferencePoint("RP".into()), value: 1.0 }.validate().is_err());
|
||||
let json = serde_json::json!({"type": "symmetry", "target": {"reference_point": "RP"}, "plane": "z"});
|
||||
let parsed: BoundaryCondition = serde_json::from_value(json.clone()).unwrap();
|
||||
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
|
||||
|
||||
Reference in new issue
Block a user