forked from 1b-agent/fieldcraft-upstream
engine: show temperature and wall heat in fluid results
Route saved temperature through slices, obstacle surfaces, streamlines and both result views, with availability and model units shared by commands and the UI. Show signed obstacle heat histories per complete face area at the selected frame, leaving unrecorded times uncoloured and retaining the saved slice settings. Part of #157.
This commit is contained in:
1 parent
8f8124dfa1
commit
1e5ff08c5f
13 files changed
+533
-43
No files matched your search
@@ -1130,7 +1130,8 @@ impl ModelApp {
|
||||
let still = key.as_ref().map(|k| PlotKey { frame: None, ..k.clone() });
|
||||
if self.prepared.0 != still {
|
||||
let pick = key.as_ref().map(|k| k.pick).unwrap_or_default();
|
||||
self.prepared = (still, results::prepare(&self.session, &self.display.1, pick).unwrap_or_default());
|
||||
let bodies: Vec<&str> = self.pick.names.iter().map(|n| n.name.as_str()).collect();
|
||||
self.prepared = (still, results::prepare(&self.session, &self.display.1, pick, &bodies).unwrap_or_default());
|
||||
}
|
||||
let frame = key.as_ref().and_then(|k| k.frame);
|
||||
let p = key.as_ref().and_then(|_| results::plot(&self.session, &self.display.1, frame, &self.prepared.1));
|
||||
|
||||
@@ -114,6 +114,9 @@ impl Plot {
|
||||
|
||||
/// Where a contour extreme is: its node's position, or a fluid step's cell centre.
|
||||
pub fn extreme_position(&self, label: u32) -> Option<Vec3> {
|
||||
if self.view.contour == Some(Field::WallHeatFlux) {
|
||||
return self.flow.as_ref()?.wall_positions.get(label as usize).copied().flatten();
|
||||
}
|
||||
match self.result.kind() {
|
||||
Kind::Fluid(f) => Some(Vec3::from(fieldcraft_engine::flow::cell_centre(f, usize::try_from(label).ok()?))),
|
||||
_ => self.node_position(label),
|
||||
@@ -175,10 +178,12 @@ pub fn scale(view: &ResultsView, result: &StepResult, pick: Pick, base: &Display
|
||||
|
||||
/// The part of the session's results plot that depends on what is drawn (`pick`), not on the
|
||||
/// animation's deformation multiplier: the deformation scale and the contour values.
|
||||
pub fn prepare(session: &Session, base: &DisplayMesh, pick: Pick) -> Option<Prepared> {
|
||||
pub fn prepare(session: &Session, base: &DisplayMesh, pick: Pick, bodies: &[&str]) -> Option<Prepared> {
|
||||
let (view, result, _) = shown(session)?;
|
||||
if matches!(result.kind(), Kind::Fluid(_)) {
|
||||
let flow = fieldcraft_engine::flow::plot(view, result, &session.traced).ok();
|
||||
let mut view = view.clone();
|
||||
view.frame = pick.frame;
|
||||
let flow = fieldcraft_engine::flow::plot_with_surfaces(&view, result, &session.traced, session, base, bodies).ok();
|
||||
let cell = |(k, v): (usize, f64)| (u32::try_from(k).unwrap_or(u32::MAX), v);
|
||||
let contour = flow
|
||||
.as_ref()
|
||||
@@ -336,7 +341,8 @@ pub(crate) mod tests {
|
||||
|
||||
fn plot_at(s: &Session, base: &DisplayMesh, frame: Option<u64>) -> Option<Plot> {
|
||||
let (view, result, _) = shown(s)?;
|
||||
plot(s, base, frame, &prepare(s, base, pick(view, result, frame))?)
|
||||
let bodies: Vec<&str> = s.design.iter().flat_map(|d| &d.bodies).map(|b| b.name.as_str()).collect();
|
||||
plot(s, base, frame, &prepare(s, base, pick(view, result, frame), &bodies)?)
|
||||
}
|
||||
|
||||
/// The sample cantilever, solved (static step `Load`, frequency step `Modes`).
|
||||
@@ -412,22 +418,39 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
/// A 40 × 30 × 10 mm plate in a tank of 5 mm cells, a liquid flowing past it along x, solved.
|
||||
fn plate_in_a_tank() -> Session {
|
||||
fn plate_in_a_tank(thermal: bool) -> Session {
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-ui-flow-{}", std::process::id()));
|
||||
let dir = std::env::temp_dir().join(format!("fieldcraft-ui-flow-{}-{thermal}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("plate.solidcraft");
|
||||
std::fs::write(&path, plate_design(&Plate::default()).unwrap()).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.execute("geometry.open", &json!({"path": path.to_str().unwrap()})).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
s.run_script(&json!([
|
||||
let mut script = json!([
|
||||
{"command": "model.material.create", "params": {"name": "Water", "density": 1e-9, "fluid": {"type": "newtonian", "viscosity": 1e-7}}},
|
||||
{"command": "model.fluid_domain.create", "params": {"name": "Tank", "material": "Water", "min": [-20, -20, -20], "max": [60, 50, 30], "spacing": 5, "obstacles": ["Body1"],
|
||||
"faces": {"x_min": {"type": "velocity", "velocity": [10, 0, 0]}, "x_max": {"type": "pressure", "pressure": 0}}}},
|
||||
{"command": "model.step.create", "params": {"name": "Flow", "procedure": {"type": "fluid", "domain": "Tank", "steps": 60, "frames": 2}}},
|
||||
]))
|
||||
.unwrap();
|
||||
]);
|
||||
if thermal {
|
||||
script[0]["params"]["thermal"] = json!({"conductivity": 1.0, "specific_heat": 1e8});
|
||||
let faces: Vec<_> = s.design.as_ref().unwrap().bodies[0]
|
||||
.faces()
|
||||
.iter()
|
||||
.map(|f| {
|
||||
json!({
|
||||
"face": {"body": "Body1", "face": f.name}, "condition": {"type": "temperature", "value": 60.0}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
script[1]["params"]["thermal"] =
|
||||
json!({"initial_temperature": 20.0, "surfaces": faces, "faces": {"x_min": {"type": "temperature", "value": 20.0}}});
|
||||
}
|
||||
s.run_script(&script).unwrap();
|
||||
if thermal {
|
||||
s.execute("model.units", &json!({"system": "mm-t-s-N-MPa"})).unwrap();
|
||||
}
|
||||
s.execute("solve.fluid", &json!({})).unwrap();
|
||||
s
|
||||
}
|
||||
@@ -436,7 +459,7 @@ pub(crate) mod tests {
|
||||
/// slice and on the part, with its legend, and streamlines (#193).
|
||||
#[test]
|
||||
fn a_fluid_step_keeps_the_part_and_draws_the_flow() {
|
||||
let mut s = plate_in_a_tank();
|
||||
let mut s = plate_in_a_tank(false);
|
||||
let (base, _) = fieldcraft_engine::view::display(&s);
|
||||
assert!(!base.surface_tris.is_empty());
|
||||
let p = plot_at(&s, &base, None).unwrap();
|
||||
@@ -457,6 +480,37 @@ pub(crate) mod tests {
|
||||
assert_eq!((p.contour.as_ref().map(|c| c.field), p.pick.frame), (Some(Field::Pressure), Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heated_flow_fields_have_units_colours_and_face_markers() {
|
||||
let mut s = plate_in_a_tank(true);
|
||||
let (base, _) = fieldcraft_engine::view::display(&s);
|
||||
s.execute("results.slice", &json!({"field": "temperature"})).unwrap();
|
||||
let p = plot_at(&s, &base, None).unwrap();
|
||||
assert_eq!(p.contour.as_ref().unwrap().field, Field::Temperature);
|
||||
assert!(Field::Temperature.title(&p.result, p.units).contains("°C or K"));
|
||||
assert!(Field::available(&p.result).any(|f| f == Field::Temperature));
|
||||
assert!(p.contour.as_ref().unwrap().max.1 > 20.0);
|
||||
let last = p.frame().unwrap().2 - 1;
|
||||
s.execute("results.frame", &json!({"frame": 0})).unwrap();
|
||||
assert_eq!(plot_at(&s, &base, None).unwrap().contour.as_ref().unwrap().max.1, 20.0, "the frame selector changes the temperature shown");
|
||||
s.execute("results.frame", &json!({"frame": last})).unwrap();
|
||||
s.execute("results.surface", &json!({"field": "wall_heat_flux"})).unwrap();
|
||||
let p = plot_at(&s, &base, None).unwrap();
|
||||
let c = p.contour.as_ref().unwrap();
|
||||
assert_eq!(c.field, Field::WallHeatFlux);
|
||||
assert_eq!(c.field.unit(&p.result, p.units).as_deref(), Some("mW/mm²"));
|
||||
assert!(c.min.1 < 0.0, "hot wall gives heat to fluid");
|
||||
assert!(p.extreme_position(c.min.0).is_some());
|
||||
assert!(p.flow.as_ref().unwrap().slice.is_none());
|
||||
for t in [Tokens::light(), Tokens::dark()] {
|
||||
let scene = scene(&p, Style::ShadedEdges, crate::scene::Show::DEFAULT, &t);
|
||||
assert_eq!(scene.tris.len(), base.surface_tris.len());
|
||||
assert!(scene.tris.windows(2).any(|t| t[0].color != t[1].color));
|
||||
}
|
||||
s.execute("results.frame", &json!({"frame": 0})).unwrap();
|
||||
assert!(plot_at(&s, &base, None).unwrap().contour.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modes_swing_and_static_steps_grow_when_animated() {
|
||||
let mut s = solved_cantilever();
|
||||
|
||||
@@ -41,7 +41,8 @@ pub fn show(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
|
||||
probe(app, &plot, &painter, rect, t);
|
||||
let mut focus = None;
|
||||
for (i, (line, node)) in extremes.into_iter().enumerate() {
|
||||
let r = ui.interact(line, ui.id().with(("fc_legend_extreme", i)), egui::Sense::click()).on_hover_text(format!("Show and probe node {node}"));
|
||||
let tip = if plot.is_fluid() { "Centre this field extreme".into() } else { format!("Show and probe node {node}") };
|
||||
let r = ui.interact(line, ui.id().with(("fc_legend_extreme", i)), egui::Sense::click()).on_hover_text(tip);
|
||||
if r.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
@@ -86,6 +87,7 @@ fn title(plot: &Plot) -> String {
|
||||
match (&plot.contour, plot.mode()) {
|
||||
(Some(l), _) => l.field.title(&plot.result, plot.units),
|
||||
(None, Some(_)) => "Mode shape (normalised)".into(),
|
||||
(None, None) if plot.is_fluid() => plot.view.contour.map_or_else(|| "Flow".into(), |f| f.title(&plot.result, plot.units)),
|
||||
(None, None) => "Deformed shape".into(),
|
||||
}
|
||||
}
|
||||
@@ -474,9 +476,23 @@ fn deformation_rows(plot: &Plot, ui: &mut egui::Ui, run: &mut Vec<(&'static str,
|
||||
fn flow_rows(plot: &Plot, ui: &mut egui::Ui, run: &mut Vec<(&'static str, Value)>) {
|
||||
use fieldcraft_engine::flow::{Axis, slice_position};
|
||||
let (Kind::Fluid(f), Some(flow)) = (plot.result.kind(), plot.flow.as_ref()) else { return };
|
||||
if plot.view.contour == Some(Field::WallHeatFlux) {
|
||||
ui.label("Wall heat");
|
||||
ui.label("Positive into wall; face average");
|
||||
ui.end_row();
|
||||
if plot.contour.is_none() {
|
||||
ui.label("History");
|
||||
ui.label("No wall heat recorded at this frame");
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
let view = &flow.view;
|
||||
ui.label("Slice");
|
||||
ui.horizontal(|ui| {
|
||||
if plot.view.contour == Some(Field::WallHeatFlux) {
|
||||
ui.label("Hidden for wall heat");
|
||||
return;
|
||||
}
|
||||
let shown = view.slice.as_ref().map_or("None", |s| s.axis.id());
|
||||
egui::ComboBox::from_id_salt("fc_results_slice").selected_text(format!("Normal {}", shown.to_uppercase())).width(90.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(view.slice.is_none(), "None").clicked() {
|
||||
@@ -504,14 +520,14 @@ fn flow_rows(plot: &Plot, ui: &mut egui::Ui, run: &mut Vec<(&'static str, Value)
|
||||
ui.end_row();
|
||||
ui.label("Obstacles");
|
||||
let mut surface = view.surface;
|
||||
if ui.checkbox(&mut surface, "Coloured").on_hover_text("Colour the part by the field in the fluid beside it").changed() {
|
||||
if ui.checkbox(&mut surface, "Coloured").on_hover_text("Colour the part by the selected fluid field or wall heat").changed() {
|
||||
run.push(("results.surface", json!({"show": surface})));
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("Streamlines");
|
||||
ui.horizontal(|ui| {
|
||||
let mut on = view.streamlines.is_some();
|
||||
if ui.checkbox(&mut on, "").on_hover_text("Streamlines from the inlet, coloured by speed when the legend shows velocity").changed() {
|
||||
if ui.checkbox(&mut on, "").on_hover_text("Streamlines from the inlet, coloured by speed or temperature when selected").changed() {
|
||||
run.push(("results.streamlines", json!({"show": on})));
|
||||
}
|
||||
if let Some(sl) = &view.streamlines {
|
||||
|
||||
Reference in new issue
Block a user