forked from 1b-agent/fieldcraft-upstream
engine: draw an obstacle face held at a temperature at that temperature
The coloured obstacle surface samples the fluid beside each face, and the solid cells carry no temperature, so a face held at 80 °C took the mean of the fluid cells around it, which mixes in the cooler ones further out: the heated-flow sample's bracket looked 50 °C beside 79.7 °C air. A face whose thermal condition holds it at a temperature is now drawn at that value, and the legend's range reaches it (the fluid never quite does). Adiabatic and flux faces keep showing the fluid beside them. Part of #157.
This commit is contained in:
1 parent
c61a8b9a4d
commit
7e3eaa14f2
2 files changed
+131
-3
No files matched your search
+116
-1
@@ -355,6 +355,34 @@ pub fn plot_with_surfaces(
|
||||
bodies: &[&str],
|
||||
) -> Result<FlowPlot> {
|
||||
let mut out = plot(view, result, traced)?;
|
||||
if view.contour == Some(Field::Temperature)
|
||||
&& let Kind::Fluid(r) = result.kind()
|
||||
{
|
||||
// An obstacle face held at a temperature is drawn at it: sampling the fluid beside it
|
||||
// would mix in the cooler cells further out.
|
||||
let held = held_temperatures(session, r, base, bodies);
|
||||
if held.iter().any(Option::is_some) {
|
||||
if let Some(c) = &mut out.contour {
|
||||
// A held face beyond the fluid's range is the extreme: the legend, its Max/Min
|
||||
// lines and the viewport's marker all name it. Its label continues past the
|
||||
// cells, into `wall_positions` (see [`FlowPlot::wall_positions`]).
|
||||
let cells = c.values.len();
|
||||
for (t, v) in base.surface_tris.iter().zip(&held) {
|
||||
let Some(v) = *v else { continue };
|
||||
let at = || (t.p[0] + t.p[1] + t.p[2]) * (1.0 / 3.0);
|
||||
if v > c.max.1 {
|
||||
c.max = (cells + out.wall_positions.len(), v);
|
||||
out.wall_positions.push(Some(at()));
|
||||
} else if v < c.min.1 {
|
||||
c.min = (cells + out.wall_positions.len(), v);
|
||||
out.wall_positions.push(Some(at()));
|
||||
}
|
||||
}
|
||||
c.bands = view.bands(c.min.1, c.max.1);
|
||||
}
|
||||
out.wall_values = base.surface_tris.iter().zip(held).map(|(t, v)| (t.p, v)).collect();
|
||||
}
|
||||
}
|
||||
if view.contour == Some(Field::WallHeatFlux) {
|
||||
let Kind::Fluid(r) = result.kind() else { return Ok(out) };
|
||||
let faces = wall::faces(session, r, view.pick())?;
|
||||
@@ -372,6 +400,37 @@ pub fn plot_with_surfaces(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Per displayed surface triangle, the temperature its obstacle face is held at, when the step's
|
||||
/// domain holds that face at one and the result is of the current model.
|
||||
fn held_temperatures(session: &crate::Session, r: &FluidResult, base: &fieldcraft_render::DisplayMesh, bodies: &[&str]) -> Vec<Option<f64>> {
|
||||
let none = || vec![None; base.surface_tris.len()];
|
||||
if session.latest_result(&r.step).is_some_and(|(_, current)| !current) {
|
||||
return none();
|
||||
}
|
||||
let Some(thermal) = session
|
||||
.model
|
||||
.step(&r.step)
|
||||
.and_then(|st| st.procedure.fluid_domain())
|
||||
.and_then(|d| session.model.fluid_domain(d))
|
||||
.and_then(|d| d.thermal.as_ref())
|
||||
else {
|
||||
return none();
|
||||
};
|
||||
let design = session.design.as_deref();
|
||||
let mut held = std::collections::BTreeMap::new();
|
||||
for sb in &thermal.surfaces {
|
||||
let (fieldcraft_model::ThermalCondition::Temperature { value }, fieldcraft_model::Entity::Face(face)) = (&sb.condition, &sb.face.entity)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let index = design.and_then(|d| d.bodies.iter().find(|b| b.name == sb.face.body)).and_then(|b| b.face_index(face));
|
||||
if let Some(index) = index {
|
||||
held.insert((sb.face.body.as_str(), index), *value);
|
||||
}
|
||||
}
|
||||
base.surface_tris.iter().map(|t| bodies.get(t.body as usize).and_then(|name| held.get(&(*name, t.face)).copied())).collect()
|
||||
}
|
||||
|
||||
/// The colours a flow plot is drawn in.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FlowColors<'a> {
|
||||
@@ -401,7 +460,15 @@ pub fn draw(plot: &FlowPlot, scene: &mut Scene, colors: &FlowColors) {
|
||||
}
|
||||
} else if plot.surface {
|
||||
let model = std::mem::take(&mut scene.tris);
|
||||
for t in model {
|
||||
// Faces held at a temperature (`wall_values`, matched by position as for wall heat).
|
||||
let held = (c.field == Field::Temperature && model.len() == plot.wall_values.len()).then_some(&plot.wall_values);
|
||||
for (k, t) in model.into_iter().enumerate() {
|
||||
if let Some((points, Some(v))) = held.and_then(|h| h.get(k))
|
||||
&& t.p == *points
|
||||
{
|
||||
banded(t.p, [*v; 3], t.normal, &c.bands, &color, &mut scene.tris);
|
||||
continue;
|
||||
}
|
||||
let v = t.p.map(|p| flow::sample(&plot.grid, &c.values, p));
|
||||
let (sum, known) = v.iter().filter(|x| !x.is_nan()).fold((0.0, 0u32), |(s, n), x| (s + x, n + 1));
|
||||
if known == 0 {
|
||||
@@ -542,6 +609,54 @@ pub(crate) mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// A face held at a temperature is drawn at that temperature, and the legend reaches it,
|
||||
/// rather than at the mean of the fluid cells around it (which mixes in the cooler ones
|
||||
/// further out: an 80 °C bracket looked 50 °C).
|
||||
#[test]
|
||||
fn a_face_held_at_a_temperature_is_drawn_at_it() {
|
||||
let mut s = plate_in_a_tank();
|
||||
let face = s.design.as_ref().unwrap().bodies[0].faces()[0].name.clone();
|
||||
s.execute(
|
||||
"model.material.edit",
|
||||
&json!({"name": "Water", "density": 1e-9, "fluid": {"type": "newtonian", "viscosity": 1e-7}, "thermal": {"conductivity": 400, "specific_heat": 4e9}}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute(
|
||||
"model.fluid_domain.edit",
|
||||
&json!({"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}},
|
||||
"thermal": {"initial_temperature": 20}}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute(
|
||||
"model.fluid_domain.thermal_boundary",
|
||||
&json!({"domain": "Tank", "at": {"body": "Body1", "face": face}, "condition": {"type": "temperature", "value": 80}}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("solve.fluid", &json!({})).unwrap();
|
||||
s.execute("results.surface", &json!({"field": "temperature", "show": true})).unwrap();
|
||||
let (result, current) = s.latest_result("Flow").unwrap();
|
||||
assert!(current);
|
||||
let view = s.results_view.clone().unwrap();
|
||||
let base = crate::view::display(&s).0;
|
||||
let names: Vec<&str> = s.design.as_ref().unwrap().bodies.iter().map(|b| b.name.as_str()).collect();
|
||||
let fp = plot_with_surfaces(&view, result, &s.traced, &s, &base, &names).unwrap();
|
||||
let c = fp.contour.as_ref().unwrap();
|
||||
// The fluid never quite reaches the wall's 80 °C; the legend does, and its maximum is the
|
||||
// held face (a label past the cells, at a point on that face).
|
||||
assert!(c.values.iter().all(|t| t.is_nan() || *t < 80.0));
|
||||
assert_eq!((c.max.1, c.bands.max), (80.0, 80.0), "{:?}", c.bands);
|
||||
let at = c.max.0.checked_sub(c.values.len()).and_then(|k| fp.wall_positions.get(k).copied().flatten());
|
||||
assert!(at.is_some(), "{:?}", c.max);
|
||||
let held: Vec<f64> = fp.wall_values.iter().filter_map(|(_, v)| *v).collect();
|
||||
assert!(!held.is_empty() && held.iter().all(|t| *t == 80.0), "{held:?}");
|
||||
// The headless picture draws the held face in the top band's colour.
|
||||
let p = crate::view::Palette::LIGHT;
|
||||
let top = bands_color(&p.contour, p.contour_over, &c.bands, c.bands.band(80.0).unwrap());
|
||||
let (scene, ..) = crate::view::scene(&s, &p);
|
||||
assert!(scene.tris.iter().any(|t| t.color == top));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fluid_result_is_first_shown_across_the_flow() {
|
||||
let r = channel();
|
||||
|
||||
@@ -118,7 +118,14 @@ impl Plot {
|
||||
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()?))),
|
||||
Kind::Fluid(f) => {
|
||||
let label = usize::try_from(label).ok()?;
|
||||
// Past the cells: a face held at a temperature that is the field's extreme.
|
||||
match label.checked_sub(f.grid.len()) {
|
||||
Some(k) => self.flow.as_ref()?.wall_positions.get(k).copied().flatten(),
|
||||
None => Some(Vec3::from(fieldcraft_engine::flow::cell_centre(f, label))),
|
||||
}
|
||||
}
|
||||
_ => self.node_position(label),
|
||||
}
|
||||
}
|
||||
@@ -492,7 +499,13 @@ pub(crate) mod tests {
|
||||
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");
|
||||
let at_rest = plot_at(&s, &base, None).unwrap();
|
||||
let c = at_rest.contour.as_ref().unwrap();
|
||||
let cells = &at_rest.flow.as_ref().unwrap().contour.as_ref().unwrap().values;
|
||||
assert!(cells.iter().filter(|t| t.is_finite()).all(|t| *t == 20.0), "the frame selector changes the temperature shown");
|
||||
// The faces are held at 60 °C from the start: the legend's maximum is a face, at a point on it.
|
||||
assert_eq!(c.max.1, 60.0);
|
||||
assert!(at_rest.extreme_position(c.max.0).is_some());
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user