pub struct Bar1D { /* private fields */ }Expand description
One-dimensional explicit heat conduction on a uniform grid.
Exists to exercise the thing LumpedMass cannot: a real stability limit that a
scheduler has to subcycle around. The explicit update
T'ᵢ = Tᵢ + α dt/dx² (Tᵢ₊₁ - 2Tᵢ + Tᵢ₋₁) is stable only for
α dt/dx² ≤ 1/2, and exceeding it does not degrade the answer gracefully — it
oscillates and diverges within a few steps.
Ends are insulated, so the total heat is conserved exactly and the audit has something sharp to check.
Implementations§
Source§impl Bar1D
impl Bar1D
Sourcepub fn new(
name: impl Into<String>,
substance: Substance,
cells: usize,
dx: Length,
area: Area,
initial: Temperature,
) -> Bar1D
pub fn new( name: impl Into<String>, substance: Substance, cells: usize, dx: Length, area: Area, initial: Temperature, ) -> Bar1D
A bar of cells cells, each dx long, all starting at initial.
Sourcepub fn exposing(self, boundary: impl Into<String>, face_area: Area) -> Bar1D
pub fn exposing(self, boundary: impl Into<String>, face_area: Area) -> Bar1D
Expose the bar’s long side as an Interface, one face per cell.
Without this the bar can only be heated lumpedly, and the heat lands in cell 0
because that is where a surface absorbing light would put it if the bar knew where
the surface was. It does not: Exchange::publish carries an amount and no place, so
“the light hit the middle” is unsayable.
With it, whoever illuminates the bar publishes a Flux over these faces and the
heat appears where it landed. One face per cell deliberately: the two sides then
share a discretisation, so nothing has to interpolate, and interpolation is where a
coupling loses energy. A publisher on a different grid resamples explicitly with
Flux::resample, which the bus insists on rather than doing quietly.
face_area is the area of one cell’s exposed side, which is not the bar’s
cross-section — a bar conducts along its length and is illuminated across it. It is
only used to turn a lumped total into a distribution, so it does not enter the
conduction at all.
Sourcepub fn boundary(&self) -> Option<&Interface>
pub fn boundary(&self) -> Option<&Interface>
The boundary other domains publish onto, if Bar1D::exposing gave it one.
Sourcepub fn temperature_at(&self, index: usize) -> Temperature
pub fn temperature_at(&self, index: usize) -> Temperature
Temperature of one cell, clamped to the ends of the bar.
Sourcepub fn cell_count(&self) -> usize
pub fn cell_count(&self) -> usize
How many cells the bar is cut into.
Sourcepub fn mean_temperature(&self) -> Temperature
pub fn mean_temperature(&self) -> Temperature
Mean temperature along the bar.
Sourcepub fn end_to_end(&self) -> Temperature
pub fn end_to_end(&self) -> Temperature
Temperature difference between the ends — what a gradient looks like from outside, and what a lumped model reports as zero.
Sourcepub fn absorbed_energy(&self) -> Energy
pub fn absorbed_energy(&self) -> Energy
Heat taken from the bus over the run.
Sourcepub fn fourier_number(&self, dt: Time) -> f64
pub fn fourier_number(&self, dt: Time) -> f64
α dt/dx², the number that must stay at or under 1/2.
Trait Implementations§
Source§impl Domain for Bar1D
impl Domain for Bar1D
Source§fn max_stable_dt(&self, _now: Time) -> Time
fn max_stable_dt(&self, _now: Time) -> Time
dx²/(2α) — the explicit diffusion limit, exactly.
Source§fn ledger(&self) -> Ledger
fn ledger(&self) -> Ledger
Heat gained since the start. The ends are insulated, so this is exactly what
came in over the bus — see the note on LumpedMass::ledger for why the
absorbed total is not subtracted here as well.
Source§fn readings(&self) -> Vec<Reading>
fn readings(&self) -> Vec<Reading>
Mean and peak in celsius, and what it has absorbed.
Both ends of the profile, because a bar’s whole reason to exist rather than a lumped mass is that those two differ — reporting the mean alone would describe it as the thing it is not.
Source§fn as_any_mut(&mut self) -> Option<&mut dyn Any>
fn as_any_mut(&mut self) -> Option<&mut dyn Any>
And mutably, because a domain that can be read and not written is one a coupling can
only fail at silently — Simulation::domain_as_mut returns None when this is missing.
Source§fn as_field(&self) -> Option<&dyn ScalarField>
fn as_field(&self) -> Option<&dyn ScalarField>
The bar reads as a temperature field, so a renderer never has to know it is a bar.
Source§fn books_balance(&self) -> bool
fn books_balance(&self) -> bool
Source§fn name(&self) -> &str
fn name(&self) -> &str
Source§fn step(
&mut self,
_t: Time,
dt: Time,
bus: &mut Exchange,
) -> Result<(), Violation>
fn step( &mut self, _t: Time, dt: Time, bus: &mut Exchange, ) -> Result<(), Violation>
dt from t, reading inputs from bus and publishing outputs
to it. A quasi-static domain ignores dt. Read moreSource§fn checkpoint(&mut self)
fn checkpoint(&mut self)
Schedule::Iterative, and Simulation::advance says so rather than
silently iterating from the wrong state.Source§fn restore(&mut self)
fn restore(&mut self)
Domain::checkpoint.Source§fn supports_restore(&self) -> bool
fn supports_restore(&self) -> bool
Source§fn as_any(&self) -> Option<&dyn Any>
fn as_any(&self) -> Option<&dyn Any>
Any, so a caller can get the concrete type back out of a
Simulation — see Simulation::domain_as. Read moreSource§fn kind(&self) -> Kind
fn kind(&self) -> Kind
Kind::Evolving.Source§fn residual(&self) -> f64
fn residual(&self) -> f64
Schedule::Iterative. Zero means converged.Source§impl ScalarField for Bar1D
The bar as a temperature field, in kelvin.
impl ScalarField for Bar1D
The bar as a temperature field, in kelvin.
The first implementation of ScalarField in the workspace, and it exists to answer a
question rather than to be used internally: the trait was written as the interface a
visualiser would read a simulation through, and until something implemented it, whether
it was the right interface was a guess. Two things came out of implementing it.
§The bar lies along x, and is uniform in y and z
Cell i is centred at (i + ½)·dx, so the bar occupies 0 to n·dx. Off the ends the
value is held constant, which is not a fudge: the ends are insulated, so the temperature
really does stop changing there. Off-axis it is uniform, which is not an approximation
being hidden either — a one-dimensional model is the claim that nothing varies across
the bar, and LumpedMass::biot_number is where you check whether that claim holds.
§at ignores the time it is given, and that is the interface’s one rough edge
ScalarField::at takes a Time, because a closed-form field like
Motion can answer for any instant. A marched domain cannot: it
holds now and nothing else. So the argument is ignored here, and a caller wanting a
different instant has to have recorded one.
The default ScalarField::rate would then read zero — it differences at across two
times — which would be wrong rather than merely unavailable, since the bar is visibly
heating. It is overridden below, and the fix is not a workaround: a diffusive field’s
time derivative is α∇²T, so the governing equation supplies from the present state
exactly what the finite difference wanted history for.
§The derivatives use the domain’s own stencil
ScalarField offers central differences over a step you choose, and its documentation
says a field that knows better should override them. This one does: gradient and
laplacian use the same mirrored three-point stencil that Domain::step integrates,
evaluated on the cells rather than by re-sampling the interpolated field. So the field
reports what the domain actually believes, and the h argument is ignored — asking for
a derivative on a scale finer than dx is asking for information the bar does not have.
They have to come from the cells rather than from at, and the reason is worth stating:
at interpolates linearly, so its exact second derivative is zero between nodes and
infinite at them. A Laplacian read off the interpolant would be useless. The cost is that
gradient is not quite the derivative of at — it is the derivative the scheme uses —
and that is an unavoidable property of sampling a discrete field, not a rough edge that
could be polished out.
Source§fn unit(&self) -> &'static str
fn unit(&self) -> &'static str
Kelvin, because that is what the cells hold.
Not celsius. readings reports celsius and a picture of a bar usually wants celsius, but
both of those are conversions a view chooses; the field returns what it stores. Labelling
this “C” would have put 293.15 under a degrees-celsius header, which is the failure a unit
on a legend exists to prevent.
Source§fn rate(&self, p: LengthVec, t: Time, _dt: Time) -> f64
fn rate(&self, p: LengthVec, t: Time, _dt: Time) -> f64
∂T/∂t = α∇²T — the heat equation, evaluated rather than differenced.
Conduction only. Heat arriving over the bus is a source term the field cannot see, so during illumination this reports how fast the bar is spreading what it has, not how fast it is warming. Away from the beam those are the same number.