forked from 1b-agent/fieldcraft-upstream
solver: heat flow through each heat load, and the imbalance (#362)
solve.heat's reply and the results report give heat_flows: each convection, flux and source load's heat into the body, the prescribed temperatures' total and their sum; the results panel lists them for a heat step. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
0e0d8bd114
commit
464a4d50c1
4 files changed
+127
-18
No files matched your search
@@ -303,6 +303,7 @@ fn run_heat(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"max_temperature": largest(r.nodes.iter().copied().zip(t.iter().copied())),
|
||||
"heat_supplied": r.applied_total,
|
||||
"heat_removed": r.reaction_total,
|
||||
"heat_flows": crate::results::heat_flows(&r),
|
||||
"warnings": r.warnings,
|
||||
});
|
||||
s.store_result(StepResult::Implicit(ImplicitResult::Heat(r)));
|
||||
@@ -1477,6 +1478,30 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A heat balance per load: the convection `Air` carries away what the heater's prescribed
|
||||
/// temperature supplies (#362).
|
||||
#[test]
|
||||
fn heat_flows_balance_per_load() {
|
||||
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}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Heater", "type": "temperature", "target": {"set": "root"}, "value": 100}},
|
||||
{"command": "model.set.create", "params": {"name": "skin", "faces": (1..=6).map(|f| json!({"element": 10, "face": f})).collect::<Vec<_>>()}},
|
||||
{"command": "model.load.create", "params": {"name": "Air", "type": "convection", "set": "skin", "film": 0.5, "sink": 20}},
|
||||
{"command": "model.step.create", "params": {"name": "Therm", "procedure": {"type": "heat_transfer"}, "boundary_conditions": ["Heater"], "loads": ["Air"]}},
|
||||
]);
|
||||
s.run_script(&script).unwrap();
|
||||
let r = s.execute("solve.heat", &json!({"step": "Therm"})).unwrap();
|
||||
let flows = &r["heat_flows"];
|
||||
let air = flows["loads"].as_array().unwrap().iter().find(|l| l["name"] == "Air").unwrap();
|
||||
assert_eq!(air["type"], "convection");
|
||||
let (out, supplied) = (air["heat"].as_f64().unwrap(), flows["reactions"].as_f64().unwrap());
|
||||
assert!(out < 0.0 && supplied > 0.0, "{flows}");
|
||||
assert!(flows["imbalance"].as_f64().unwrap().abs() < 1e-6 * supplied, "{flows}");
|
||||
assert_eq!(s.results_report()["heat_flows"], *flows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_heat_and_probe_temperatures() {
|
||||
// The cantilever bar as a conductor: 0 at the root, 100 at the tip, linear between.
|
||||
|
||||
@@ -497,6 +497,7 @@ impl Session {
|
||||
out["frames"] = f.frames.iter().map(|f| f.time).collect();
|
||||
}
|
||||
Kind::Heat(h) => {
|
||||
out["heat_flows"] = heat_flows(h);
|
||||
out["frames"] = json!(h.times);
|
||||
if let Ok((t, _)) = heat_frame(h, v.pick()) {
|
||||
out["time"] = json!(t);
|
||||
@@ -519,6 +520,16 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// The heat balance of a heat transfer result: each heat load's flow into the body (`loads`:
|
||||
/// `{name, type, heat}`, negative out of it), the prescribed temperatures' total (`reactions`),
|
||||
/// and `imbalance`, their sum: about 0 in a steady state, the heat stored per time in a
|
||||
/// transient one.
|
||||
pub fn heat_flows(h: &HeatResult) -> Value {
|
||||
let loads: Vec<Value> = h.loads.iter().map(|l| json!({"name": l.name, "type": l.kind, "heat": l.heat})).collect();
|
||||
let applied: f64 = h.loads.iter().map(|l| l.heat).sum();
|
||||
json!({"loads": loads, "reactions": h.reaction_total, "imbalance": applied + h.reaction_total})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user