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
@@ -418,7 +418,14 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
deck("deck.write.nastran", "Nastran Deck", "nastran", "Write Nastran bulk data (SOL 101, 103)"),
|
||||
deck("deck.write.lsdyna", "LS-DYNA Deck", "lsdyna", "Write an LS-DYNA keyword file"),
|
||||
soon("deck.read", "Read Deck", Solve, "Decks", "M9", "Read a deck, keeping keywords ModelCraft does not know"),
|
||||
ready("solve.static", "Static", Solve, "Implicit", "Linear static solve of a step with ModelCraft's own solver"),
|
||||
ready_more(
|
||||
"solve.static",
|
||||
"Static",
|
||||
Solve,
|
||||
"Implicit",
|
||||
"Linear static (or coupled temperature–displacement) solve of a step with ModelCraft's own solver; heat transfer in the drop-down",
|
||||
&["solve.heat"],
|
||||
),
|
||||
ready("solve.frequency", "Frequency", Solve, "Implicit", "Natural frequencies and mode shapes of a frequency step"),
|
||||
ready(
|
||||
"solve.explicit",
|
||||
|
||||
@@ -90,6 +90,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
solve::RUN,
|
||||
solve::STATIC,
|
||||
solve::FREQUENCY,
|
||||
solve::HEAT,
|
||||
solve::EXPLICIT,
|
||||
solve::HISTORY,
|
||||
solve::FLUID,
|
||||
|
||||
@@ -5,7 +5,9 @@ use modelcraft_explicit::{ExplicitResult, solve_explicit};
|
||||
use modelcraft_lbm::{FluidResult, Obstacle};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
use modelcraft_model::FluidDomain;
|
||||
use modelcraft_solver::{FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_static};
|
||||
use modelcraft_solver::{
|
||||
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_static,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{labels, opt, points, req};
|
||||
@@ -22,11 +24,19 @@ pub const RUN: CommandSpec = CommandSpec {
|
||||
pub const STATIC: CommandSpec = CommandSpec {
|
||||
id: "solve.static",
|
||||
label: "Solve Static Step",
|
||||
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000} — solves a linear static step (the only static step when `step` is left out) and keeps the result in the session; the caps may only be lowered"#,
|
||||
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000} — solves a linear static or coupled temperature–displacement step (the only one when `step` is left out) and keeps the result in the session; the caps may only be lowered"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_static,
|
||||
};
|
||||
|
||||
pub const HEAT: CommandSpec = CommandSpec {
|
||||
id: "solve.heat",
|
||||
label: "Solve Heat Transfer Step",
|
||||
params: r#"{"step": "Heat", "max_dofs": 6000000, "max_factor_entries": 400000000} — steady or transient heat conduction of a heat transfer step (the only one when `step` is left out): temperatures, heat flux and the heat flow at prescribed temperatures, kept in the session; the caps may only be lowered"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_heat,
|
||||
};
|
||||
|
||||
pub const FREQUENCY: CommandSpec = CommandSpec {
|
||||
id: "solve.frequency",
|
||||
label: "Solve Frequency Step",
|
||||
@@ -97,8 +107,9 @@ fn limits(p: &Value) -> Result<Limits> {
|
||||
})
|
||||
}
|
||||
|
||||
fn largest(values: impl Iterator<Item = (u32, f64)>) -> Value {
|
||||
match values.fold(None, |best: Option<(u32, f64)>, (l, v)| if best.is_none_or(|(_, b)| v > b) { Some((l, v)) } else { best }) {
|
||||
/// The node with the value that `better` prefers, as `{"node", "value"}`.
|
||||
fn extreme(values: impl Iterator<Item = (u32, f64)>, better: fn(f64, f64) -> bool) -> Value {
|
||||
match values.fold(None, |best: Option<(u32, f64)>, (l, v)| if best.is_none_or(|(_, b)| better(v, b)) { Some((l, v)) } else { best }) {
|
||||
Some((node, value)) => json!({"node": node, "value": value}),
|
||||
None => Value::Null,
|
||||
}
|
||||
@@ -113,7 +124,8 @@ fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
.iter()
|
||||
.map(|st| {
|
||||
let solve: Solve = match st.procedure {
|
||||
Procedure::Static {} => run_static,
|
||||
Procedure::Static {} | Procedure::CoupledTemperatureDisplacement {} => run_static,
|
||||
Procedure::HeatTransfer { .. } => run_heat,
|
||||
Procedure::Frequency { .. } => run_frequency,
|
||||
Procedure::ExplicitDynamic(_) => run_explicit,
|
||||
Procedure::Fluid { .. } => run_fluid,
|
||||
@@ -132,6 +144,14 @@ fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!({"steps": done}))
|
||||
}
|
||||
|
||||
fn largest(values: impl Iterator<Item = (u32, f64)>) -> Value {
|
||||
extreme(values, |v, b| v > b)
|
||||
}
|
||||
|
||||
fn smallest(values: impl Iterator<Item = (u32, f64)>) -> Value {
|
||||
extreme(values, |v, b| v < b)
|
||||
}
|
||||
|
||||
fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let limits = limits(p)?;
|
||||
let step = opt::<String>(p, "step")?;
|
||||
@@ -152,12 +172,35 @@ fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"displacement": p.displacement,
|
||||
"reaction": p.reaction,
|
||||
})).collect::<Vec<_>>(),
|
||||
"max_temperature": r.temperature.as_ref().map(|t| largest(r.nodes.iter().copied().zip(t.iter().copied()))),
|
||||
"warnings": r.warnings,
|
||||
});
|
||||
s.store_result(StepResult::Implicit(ImplicitResult::Static(r)));
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn run_heat(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let limits = limits(p)?;
|
||||
let step = opt::<String>(p, "step")?;
|
||||
let r = solve_heat(&s.model, step.as_deref(), &limits)?;
|
||||
let t = r.temperature();
|
||||
let summary = json!({
|
||||
"step": r.step,
|
||||
"dofs": r.stats.dofs,
|
||||
"equations": r.stats.equations,
|
||||
"factor_entries": r.stats.factor_entries,
|
||||
"frames": r.frames.len(),
|
||||
"end_time": r.times.last(),
|
||||
"min_temperature": smallest(r.nodes.iter().copied().zip(t.iter().copied())),
|
||||
"max_temperature": largest(r.nodes.iter().copied().zip(t.iter().copied())),
|
||||
"heat_supplied": r.applied_total,
|
||||
"heat_removed": r.reaction_total,
|
||||
"warnings": r.warnings,
|
||||
});
|
||||
s.store_result(StepResult::Implicit(ImplicitResult::Heat(r)));
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn run_frequency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let limits = limits(p)?;
|
||||
let step = opt::<String>(p, "step")?;
|
||||
@@ -452,6 +495,7 @@ fn probe_implicit(result: &ImplicitResult, step: &str, nodes: &[u32], p: &Value)
|
||||
"reaction_moment": r.reaction_moment.get(i),
|
||||
"stress": r.stress.get(i),
|
||||
"von_mises": r.von_mises.get(i),
|
||||
"temperature": r.temperature.as_ref().and_then(|t| t.get(i)),
|
||||
}),
|
||||
})
|
||||
})
|
||||
@@ -477,6 +521,17 @@ fn probe_implicit(result: &ImplicitResult, step: &str, nodes: &[u32], p: &Value)
|
||||
})
|
||||
.collect::<Result<_>>()?
|
||||
}
|
||||
ImplicitResult::Heat(r) => {
|
||||
let (time, t) = crate::results::heat_frame(r, crate::results::Pick { mode: 1, frame: opt::<usize>(p, "frame")? })?;
|
||||
let out = nodes
|
||||
.iter()
|
||||
.map(|&l| {
|
||||
let i = r.nodes.binary_search(&l).map_err(|_| EngineError::Params(format!("node {l} has no temperature (it belongs to no solid element)")))?;
|
||||
Ok(json!({"node": l, "temperature": t.get(i), "heat_flux": r.flux.get(i), "reaction": r.reaction.get(i)}))
|
||||
})
|
||||
.collect::<Result<Vec<Value>>>()?;
|
||||
return Ok(json!({"step": step, "time": time, "nodes": out}));
|
||||
}
|
||||
};
|
||||
let mut response = json!({"step": step, "nodes": out});
|
||||
if let ImplicitResult::Static(r) = result
|
||||
@@ -1172,4 +1227,40 @@ pub(crate) mod tests {
|
||||
assert!(s.execute("solve.probe", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_heat_and_probe_temperatures() {
|
||||
// The cantilever bar as a conductor: 0 at the root, 100 at the tip, linear between.
|
||||
let mut s = Session::default();
|
||||
cantilever(&mut s);
|
||||
let script = json!([
|
||||
{"command": "model.material.edit", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000, "poisson": 0}, "density": 1, "thermal": {"conductivity": 2, "specific_heat": 3, "expansion": 1e-5}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "cold", "type": "temperature", "target": {"set": "root"}, "value": 0}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "hot", "type": "temperature", "target": {"set": "tip"}, "value": 100}},
|
||||
{"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "heat_transfer"}, "boundary_conditions": ["cold", "hot"]}},
|
||||
{"command": "model.step.create", "params": {"name": "Warm", "procedure": {"type": "heat_transfer", "transient": {"end_time": 10, "increment": 1}}, "boundary_conditions": ["cold", "hot"]}},
|
||||
]);
|
||||
s.run_script(&script).unwrap();
|
||||
let r = s.execute("solve.heat", &json!({"step": "Heat"})).unwrap();
|
||||
assert!((r["max_temperature"]["value"].as_f64().unwrap() - 100.0).abs() < 1e-9, "{r}");
|
||||
assert!(r["min_temperature"]["value"].as_f64().unwrap().abs() < 1e-9, "{r}");
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Heat", "nodes": [6]})).unwrap();
|
||||
assert!((probe["nodes"][0]["temperature"].as_f64().unwrap() - 50.0).abs() < 1e-9, "{probe}");
|
||||
assert!((probe["nodes"][0]["heat_flux"][0].as_f64().unwrap() + 20.0).abs() < 1e-9, "{probe}");
|
||||
let w = s.execute("solve.heat", &json!({"step": "Warm"})).unwrap();
|
||||
assert_eq!(w["frames"], 11, "{w}");
|
||||
let early = s.execute("solve.probe", &json!({"step": "Warm", "nodes": [6], "frame": 1})).unwrap();
|
||||
assert_eq!(early["time"], 1.0);
|
||||
assert!(s.execute("solve.probe", &json!({"step": "Warm", "nodes": [6], "frame": 99})).is_err());
|
||||
assert!(s.execute("solve.probe", &json!({"step": "Warm", "nodes": [999]})).is_err());
|
||||
assert!(s.execute("solve.heat", &json!({"step": "Bend"})).is_err(), "not a heat step");
|
||||
assert!(s.execute("solve.heat", &json!({"step": "Heat", "max_dofs": 0})).is_err());
|
||||
// The results display shows temperatures, undeformed.
|
||||
let view = crate::results::ResultsView::for_result(&s.latest_result("Heat").unwrap().0.clone());
|
||||
assert_eq!((view.contour, view.deformed), (Some(crate::results::Field::Temperature), false));
|
||||
let (heat, _) = s.latest_result("Heat").unwrap();
|
||||
let fields: Vec<_> = crate::results::Field::available(heat).collect();
|
||||
assert_eq!(fields, vec![crate::results::Field::Temperature, crate::results::Field::HeatFlux]);
|
||||
assert!(crate::results::displacements(heat, crate::results::Pick::default()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -428,6 +428,22 @@ mod tests {
|
||||
json!({"name": "A", "procedure": {"type": "fluid", "domain": "A", "end_time": -1, "steps": 3}}),
|
||||
json!({"name": "Bend", "procedure": {"type": "fluid", "domain": "nope", "end_time": 1}}),
|
||||
json!({"step": "Bend", "max_cells": 1e300, "max_steps": -3}),
|
||||
// Heat transfer.
|
||||
json!({"name": "A", "thermal": {"conductivity": -1}}),
|
||||
json!({"name": "A", "thermal": {"conductivity": [1e308, f64::MIN_POSITIVE, 0]}, "density": 1}),
|
||||
json!({"name": "A", "thermal": {"conductivity": "hot", "specific_heat": 0, "expansion": [1, 2]}}),
|
||||
json!({"name": "A", "thermal": {}}),
|
||||
json!({"name": "A", "thermal": {"expansion": 1e308, "reference_temperature": -1e308}}),
|
||||
json!({"name": "A", "type": "temperature", "target": {"reference_point": "RP"}, "value": 1}),
|
||||
json!({"name": "A", "type": "temperature", "target": {"set": "root"}, "value": "warm"}),
|
||||
json!({"name": "A", "type": "surface_flux", "set": "end", "flux": 0}),
|
||||
json!({"name": "A", "type": "convection", "set": "end", "film": -1, "sink": 1e308}),
|
||||
json!({"name": "A", "type": "heat_source", "set": "all", "power": -1e308}),
|
||||
json!({"name": "A", "procedure": {"type": "heat_transfer", "transient": {"end_time": 1e308, "increment": 1e-308}}}),
|
||||
json!({"name": "A", "procedure": {"type": "heat_transfer", "transient": {"end_time": 1, "increment": 0.1, "theta": 0.1}}}),
|
||||
json!({"name": "A", "procedure": {"type": "heat_transfer", "transient": {"end_time": -1, "increment": 0}}}),
|
||||
json!({"name": "A", "procedure": {"type": "coupled_temperature_displacement", "extra": 1}}),
|
||||
json!({"step": "Heat", "frame": u64::MAX, "nodes": [1]}),
|
||||
json!({"geometry": {"body": "Bar"}, "elements": [1, 99], "faces": [{"element": 1, "face": 200}]}),
|
||||
json!({"geometry": {"body": "Bar", "vertex": "v"}, "nodes": [1], "edges": [{"element": 1, "edge": 0}]}),
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use modelcraft_explicit::{ExplicitResult, Frame};
|
||||
use modelcraft_model::Model;
|
||||
use modelcraft_solver::{FrequencyResult, StaticResult};
|
||||
use modelcraft_solver::{FrequencyResult, HeatResult, StaticResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -22,6 +22,7 @@ pub enum Kind<'a> {
|
||||
Explicit(&'a ExplicitResult),
|
||||
/// Per cell, not per node: no nodal fields to display yet.
|
||||
Fluid(&'a modelcraft_lbm::FluidResult),
|
||||
Heat(&'a HeatResult),
|
||||
}
|
||||
|
||||
impl StepResult {
|
||||
@@ -29,6 +30,7 @@ impl StepResult {
|
||||
match self {
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Heat(r)) => Kind::Heat(r),
|
||||
StepResult::Explicit(r) => Kind::Explicit(r),
|
||||
StepResult::Fluid(r) => Kind::Fluid(r),
|
||||
}
|
||||
@@ -43,6 +45,16 @@ pub struct Pick {
|
||||
pub frame: Option<usize>,
|
||||
}
|
||||
|
||||
/// The temperature frame of a heat result `pick` asks for.
|
||||
pub fn heat_frame(r: &HeatResult, pick: Pick) -> Result<(f64, &[f64])> {
|
||||
let last = r.frames.len().checked_sub(1).ok_or_else(|| EngineError::Params(format!("step `{}` has no temperatures", r.step)))?;
|
||||
let k = pick.frame.unwrap_or(last);
|
||||
match (r.times.get(k), r.frames.get(k)) {
|
||||
(Some(t), Some(f)) => Ok((*t, f)),
|
||||
_ => Err(EngineError::Params(format!("step `{}` has frames 0 to {last}; there is no frame {k}", r.step))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The field frame of an explicit result `pick` asks for.
|
||||
pub fn frame(r: &ExplicitResult, pick: Pick) -> Result<&Frame> {
|
||||
let last = r.frames.len().checked_sub(1).ok_or_else(|| EngineError::Params(format!("step `{}` has no field frames", r.step)))?;
|
||||
@@ -71,10 +83,14 @@ pub enum Field {
|
||||
StressXy,
|
||||
StressXz,
|
||||
StressYz,
|
||||
/// Temperature (heat transfer and coupled steps).
|
||||
Temperature,
|
||||
/// Heat flux magnitude (heat transfer steps, at the end of the step).
|
||||
HeatFlux,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub const ALL: [Field; 15] = [
|
||||
pub const ALL: [Field; 17] = [
|
||||
Field::Displacement,
|
||||
Field::DisplacementX,
|
||||
Field::DisplacementY,
|
||||
@@ -90,6 +106,8 @@ impl Field {
|
||||
Field::StressXy,
|
||||
Field::StressXz,
|
||||
Field::StressYz,
|
||||
Field::Temperature,
|
||||
Field::HeatFlux,
|
||||
];
|
||||
|
||||
/// The id commands take (`von_mises`…).
|
||||
@@ -110,6 +128,8 @@ impl Field {
|
||||
Field::StressXy => "stress_xy",
|
||||
Field::StressXz => "stress_xz",
|
||||
Field::StressYz => "stress_yz",
|
||||
Field::Temperature => "temperature",
|
||||
Field::HeatFlux => "heat_flux",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +150,8 @@ impl Field {
|
||||
Field::StressXy => "Stress XY",
|
||||
Field::StressXz => "Stress XZ",
|
||||
Field::StressYz => "Stress YZ",
|
||||
Field::Temperature => "Temperature",
|
||||
Field::HeatFlux => "Heat flux magnitude",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,20 +161,46 @@ impl Field {
|
||||
|
||||
/// Whether it is a stress.
|
||||
pub fn is_stress(self) -> bool {
|
||||
!matches!(self, Field::Displacement | Field::DisplacementX | Field::DisplacementY | Field::DisplacementZ | Field::Velocity)
|
||||
!matches!(
|
||||
self,
|
||||
Field::Displacement
|
||||
| Field::DisplacementX
|
||||
| Field::DisplacementY
|
||||
| Field::DisplacementZ
|
||||
| Field::Velocity
|
||||
| Field::Temperature
|
||||
| Field::HeatFlux
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether it is a heat transfer quantity.
|
||||
pub fn is_thermal(self) -> bool {
|
||||
matches!(self, Field::Temperature | Field::HeatFlux)
|
||||
}
|
||||
|
||||
/// The fields a result has: displacements everywhere, stresses for static steps and explicit
|
||||
/// frames that kept them (mode shapes have no stress scale), velocity for explicit frames.
|
||||
pub fn available(r: &StepResult) -> impl Iterator<Item = Field> + '_ {
|
||||
let (stress, velocity) = match r.kind() {
|
||||
Kind::Static(_) => (true, false),
|
||||
Kind::Frequency(_) => (false, false),
|
||||
Kind::Explicit(e) => e.frames.last().map_or((false, false), |f| (!f.stress.is_empty(), !f.velocity.is_empty())),
|
||||
let (stress, velocity, temperature, flux) = match r.kind() {
|
||||
Kind::Static(s) => (true, false, s.temperature.is_some(), false),
|
||||
Kind::Frequency(_) => (false, false, false, false),
|
||||
Kind::Explicit(e) => {
|
||||
e.frames.last().map_or((false, false, false, false), |f| (!f.stress.is_empty(), !f.velocity.is_empty(), false, false))
|
||||
}
|
||||
// Cell fields have their own display (M12 streamlines); no nodal field.
|
||||
Kind::Fluid(_) => return None.into_iter().flatten(),
|
||||
Kind::Fluid(_) => return Vec::new().into_iter(),
|
||||
Kind::Heat(_) => return Field::ALL.into_iter().filter(|f| f.is_thermal()).collect::<Vec<_>>().into_iter(),
|
||||
};
|
||||
Some(Field::ALL.into_iter().filter(move |f| if *f == Field::Velocity { velocity } else { stress || !f.is_stress() })).into_iter().flatten()
|
||||
Field::ALL
|
||||
.into_iter()
|
||||
.filter(move |f| match f {
|
||||
Field::Velocity => velocity,
|
||||
Field::Temperature => temperature,
|
||||
Field::HeatFlux => flux,
|
||||
f => stress || !f.is_stress(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +238,18 @@ impl ResultsView {
|
||||
/// (and an explicit step that kept stresses), the first mode with displacement contours for
|
||||
/// a frequency step; an explicit step at its last frame, to true scale.
|
||||
pub fn for_result(r: &StepResult) -> ResultsView {
|
||||
let contour = if Field::available(r).any(|f| f == Field::VonMises) { Field::VonMises } else { Field::Displacement };
|
||||
let heat = matches!(r.kind(), Kind::Heat(_));
|
||||
let contour = if heat {
|
||||
Field::Temperature
|
||||
} else if Field::available(r).any(|f| f == Field::VonMises) {
|
||||
Field::VonMises
|
||||
} else {
|
||||
Field::Displacement
|
||||
};
|
||||
let scale = matches!(r.kind(), Kind::Explicit(_)).then_some(1.0);
|
||||
ResultsView {
|
||||
step: r.step().to_string(),
|
||||
deformed: true,
|
||||
deformed: !heat,
|
||||
scale,
|
||||
contour: Some(contour),
|
||||
bands: DEFAULT_BANDS,
|
||||
@@ -228,6 +283,7 @@ pub fn displacements(r: &StepResult, pick: Pick) -> Result<&[[f64; 3]]> {
|
||||
Ok(&f.displacement)
|
||||
}
|
||||
Kind::Fluid(f) => Err(EngineError::Params(format!("step `{}` is a fluid step: its fields are per cell (solve.probe)", f.step))),
|
||||
Kind::Heat(h) => Err(EngineError::Params(format!("step `{}` is heat transfer and has no displacements", h.step))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +295,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
let stresses = || -> Result<std::borrow::Cow<'_, [[f64; 6]]>> {
|
||||
match r.kind() {
|
||||
Kind::Static(s) => Ok(std::borrow::Cow::Borrowed(s.stress.as_slice())),
|
||||
Kind::Frequency(_) | Kind::Fluid(_) => Err(unavailable()),
|
||||
Kind::Frequency(_) | Kind::Fluid(_) | Kind::Heat(_) => Err(unavailable()),
|
||||
Kind::Explicit(e) => {
|
||||
let f = frame(e, pick)?;
|
||||
if f.stress.is_empty() {
|
||||
@@ -261,7 +317,16 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
let f = frame(e, pick)?;
|
||||
if f.velocity.is_empty() { Err(unavailable()) } else { Ok(f.velocity.iter().map(|v| magnitude(*v)).collect()) }
|
||||
}
|
||||
Kind::Static(_) | Kind::Frequency(_) | Kind::Fluid(_) => Err(unavailable()),
|
||||
Kind::Static(_) | Kind::Frequency(_) | Kind::Fluid(_) | Kind::Heat(_) => Err(unavailable()),
|
||||
},
|
||||
Field::Temperature => match r.kind() {
|
||||
Kind::Heat(h) => Ok(heat_frame(h, pick)?.1.to_vec()),
|
||||
Kind::Static(StaticResult { temperature: Some(t), .. }) => Ok(t.clone()),
|
||||
_ => Err(unavailable()),
|
||||
},
|
||||
Field::HeatFlux => match r.kind() {
|
||||
Kind::Heat(h) => Ok(h.flux.iter().map(|v| magnitude(*v)).collect()),
|
||||
_ => Err(unavailable()),
|
||||
},
|
||||
Field::VonMises => each(&modelcraft_fem::von_mises),
|
||||
Field::MaxPrincipal => principal(0),
|
||||
@@ -347,6 +412,12 @@ impl Session {
|
||||
Kind::Fluid(f) => {
|
||||
out["frames"] = f.frames.iter().map(|f| f.time).collect();
|
||||
}
|
||||
Kind::Heat(h) => {
|
||||
out["frames"] = json!(h.times);
|
||||
if let Ok((t, _)) = heat_frame(h, v.pick()) {
|
||||
out["time"] = json!(t);
|
||||
}
|
||||
}
|
||||
Kind::Static(_) => {}
|
||||
}
|
||||
out
|
||||
@@ -390,7 +461,7 @@ mod tests {
|
||||
}
|
||||
let (modes, _) = s.latest_result("Modes").unwrap();
|
||||
assert_eq!(Field::available(modes).count(), 4);
|
||||
assert_eq!(Field::available(bend).count(), Field::ALL.len() - 1, "all but velocity");
|
||||
assert_eq!(Field::available(bend).count(), Field::ALL.len() - 3, "all but velocity and the thermal fields");
|
||||
assert!(field_values(&s.model, modes, Field::VonMises, Pick { mode: 1, frame: None }).is_err());
|
||||
assert!(field_values(&s.model, modes, Field::Displacement, Pick { mode: 9, frame: None }).is_err());
|
||||
assert_eq!(field_values(&s.model, modes, Field::Displacement, Pick { mode: 2, frame: None }).unwrap().len(), modes.nodes().len());
|
||||
|
||||
Reference in new issue
Block a user