Skip to main content

Bar1D

Struct Bar1D 

Source
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

Source

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.

Source

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.

Source

pub fn boundary(&self) -> Option<&Interface>

The boundary other domains publish onto, if Bar1D::exposing gave it one.

Source

pub fn temperature_at(&self, index: usize) -> Temperature

Temperature of one cell, clamped to the ends of the bar.

Source

pub fn cell_count(&self) -> usize

How many cells the bar is cut into.

Source

pub fn mean_temperature(&self) -> Temperature

Mean temperature along the bar.

Source

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.

Source

pub fn absorbed_energy(&self) -> Energy

Heat taken from the bus over the run.

Source

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

Source§

fn max_stable_dt(&self, _now: Time) -> Time

dx²/(2α) — the explicit diffusion limit, exactly.

Source§

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>

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>

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>

The bar reads as a temperature field, so a renderer never has to know it is a bar.

Source§

fn books_balance(&self) -> bool

Whether this domain’s books are exact: its ledger changes by precisely what it takes from the bus minus what it publishes, every step. Read more
Source§

fn name(&self) -> &str

What this domain is called. Used to look it up and to name it in a violation. Read more
Source§

fn step( &mut self, _t: Time, dt: Time, bus: &mut Exchange, ) -> Result<(), Violation>

Advance by dt from t, reading inputs from bus and publishing outputs to it. A quasi-static domain ignores dt. Read more
Source§

fn checkpoint(&mut self)

Save state so an iterative sweep can be re-run from the same starting point. A domain that does not implement this cannot take part in Schedule::Iterative, and Simulation::advance says so rather than silently iterating from the wrong state.
Source§

fn restore(&mut self)

Restore the last Domain::checkpoint.
Source§

fn supports_restore(&self) -> bool

Whether Domain::checkpoint and Domain::restore actually do something. Read more
Source§

fn as_any(&self) -> Option<&dyn Any>

This domain as Any, so a caller can get the concrete type back out of a Simulation — see Simulation::domain_as. Read more
Source§

fn kind(&self) -> Kind

Whether it has state to roll forward. Defaults to Kind::Evolving.
Source§

fn residual(&self) -> f64

How far this domain still is from agreeing with its neighbours, for Schedule::Iterative. Zero means converged.
Source§

fn as_bodies(&self) -> Option<&dyn Bodies>

This domain as a countable set of bodies, if that is what it is. Read more
Source§

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

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

∂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.

Source§

fn at(&self, p: LengthVec, _t: Time) -> f64

The value at a place and time, in this field’s SI base unit.
Source§

fn gradient(&self, p: LengthVec, _t: Time, _h: Length) -> DVec3

∇f, by central differences over h. Units are the field’s per metre.
Source§

fn laplacian(&self, p: LengthVec, _t: Time, _h: Length) -> f64

∇²f — the operator that makes diffusion diffuse. A point hotter than the average of its neighbours has a negative Laplacian and cools; that is the whole content of the heat equation.

Auto Trait Implementations§

§

impl Freeze for Bar1D

§

impl RefUnwindSafe for Bar1D

§

impl Send for Bar1D

§

impl Sync for Bar1D

§

impl Unpin for Bar1D

§

impl UnsafeUnpin for Bar1D

§

impl UnwindSafe for Bar1D

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.