pub struct ThermalNetwork { /* private fields */ }Expand description
A network of lumped bodies joined by conductances.
Implementations§
Source§impl ThermalNetwork
impl ThermalNetwork
Sourcepub fn new(name: impl Into<String>) -> ThermalNetwork
pub fn new(name: impl Into<String>) -> ThermalNetwork
An empty network. Add nodes, then link them.
Sourcepub fn node(
&mut self,
label: impl Into<String>,
substance: Substance,
volume: Volume,
thickness: Length,
initial: Temperature,
) -> Node
pub fn node( &mut self, label: impl Into<String>, substance: Substance, volume: Volume, thickness: Length, initial: Temperature, ) -> Node
An interior node: it conducts to its neighbours and loses to nothing.
A winding inside a housing has no room to convect into — only metal to conduct through —
and giving it an Environment with the numbers zeroed would need two knobs right
(convection and area, or the radiative term still runs). Absence of a loss path
is the absence of a thing, and this workspace spells that with Option everywhere else:
Substance::thermal, Bar1D::boundary.
Sourcepub fn node_losing_to(
&mut self,
label: impl Into<String>,
substance: Substance,
volume: Volume,
thickness: Length,
initial: Temperature,
environment: Environment,
) -> Node
pub fn node_losing_to( &mut self, label: impl Into<String>, substance: Substance, volume: Volume, thickness: Length, initial: Temperature, environment: Environment, ) -> Node
A node that also loses heat to its surroundings, like a LumpedMass.
Sourcepub fn link(
&mut self,
a: Node,
b: Node,
ua: Conductance,
) -> Result<(), Violation>
pub fn link( &mut self, a: Node, b: Node, ua: Conductance, ) -> Result<(), Violation>
Join two nodes by a conductance, in W/K.
Refuses a self-link, a negative conductance and a handle from a different network. Two
links between the same pair accumulate, because parallel conductances add — the same
convention Exchange::publish follows for repeated
offers on one channel.
Sourcepub fn absorbing(&mut self, node: Node) -> Result<(), Violation>
pub fn absorbing(&mut self, node: Node) -> Result<(), Violation>
Where heat taken off the bus arrives.
A network takes a share of the HEAT channel like any other consumer, but the joules
have no place — so, exactly as Bar1D puts lumped heat in its first
cell, the network needs telling which node it lands on. Naming it is better than
defaulting to node zero, which would be a silent choice about the physics.
Sourcepub fn temperature(&self, node: Node) -> Temperature
pub fn temperature(&self, node: Node) -> Temperature
A node’s temperature. Total, because a handle cannot name a node that is not here.
Sourcepub fn rise(&self, node: Node) -> Temperature
pub fn rise(&self, node: Node) -> Temperature
How far a node has moved from where it started.
Sourcepub fn label(&self, node: Node) -> &str
pub fn label(&self, node: Node) -> &str
The label a node was given, for a violation or a legend.
Sourcepub fn node_named(&self, label: &str) -> Option<Node>
pub fn node_named(&self, label: &str) -> Option<Node>
A node by label, for a caller that built the network from a file and has names rather than handles. The seam between a name-shaped format and a handle-shaped API.
Sourcepub fn path_conductance(
&self,
node: Node,
at: Power,
) -> Result<Conductance, Violation>
pub fn path_conductance( &self, node: Node, at: Power, ) -> Result<Conductance, Violation>
The conductance of the whole heat path from a node to ambient, at an operating point.
ΔP/ΔT at the node: solve the balance twice a little apart and take the slope. For a
network with no radiation this is exact and independent of at — it is the series
conductance of the links and environments between this node and the air, so a winding
reaching air through 0.9 and 2.4 W/K of joints and then 0.294 W/K of convection reports
0.203 W/K. With radiation it is the local slope, which is the right thing rather than a
compromise: everything asking for this quantity is asking a derivative question.
The reason it exists is that the caller was computing it by hand. A sizing tool built
against 0.6.0 had to assemble 1/(1/0.9 + 1/2.4 + 1/(7·A)) out of numbers this network
already holds, in order to hand the result to
Winding::runaway_current — and a network with one
more joint, or an environment on a middle node, is a formula the caller would have got
wrong silently. See FRICTION.md 20.
Errors for the same reasons ThermalNetwork::steady_state does: no environment
anywhere, a singular balance, a solve that would not converge.
Sourcepub fn handles(&self) -> impl Iterator<Item = (Node, &str)> + '_
pub fn handles(&self) -> impl Iterator<Item = (Node, &str)> + '_
Every node, in the order they were added, with its label.
A Node can only come from node or
node_losing_to, which is what makes a dangling link
unrepresentable — and it also meant a caller holding a network it did not build could not
walk it at all. nodes returned a count and there was no way to
turn a count into anything. Found the first time the consumer app tried to print a
network’s temperatures, which is a smaller version of FRICTION.md’s recurring finding:
the API is comfortable when the parts are known at compile time and awkward the moment
they are not.
Sourcepub fn heat_flow(&self, a: Node, b: Node) -> Power
pub fn heat_flow(&self, a: Node, b: Node) -> Power
Watts crossing the joint between two nodes right now, positive from a to b.
Zero if they are not linked, which is a real answer rather than a missing one.
Sourcepub fn absorbed_energy(&self) -> Energy
pub fn absorbed_energy(&self) -> Energy
Heat taken off the bus over the run.
Sourcepub fn lost_energy(&self) -> Energy
pub fn lost_energy(&self) -> Energy
Heat shed to the environments over the run.
Sourcepub fn biot_number(&self, node: Node) -> Option<f64>
pub fn biot_number(&self, node: Node) -> Option<f64>
Whether a node’s lumped approximation applies, from its own conductivity and its own
environment. None for an interior node, which has no film coefficient to compare
against — and that is worth knowing rather than papering over.
Sourcepub fn time_constant(&self, node: Node) -> Time
pub fn time_constant(&self, node: Node) -> Time
A node’s local time constant: its capacity over everything that carries heat away from it, links included.
Sourcepub fn steady_state(&self, power: Power) -> Result<SteadyState, Violation>
pub fn steady_state(&self, power: Power) -> Result<SteadyState, Violation>
Everything conducting heat out of node i: its environment linearised at its current
temperature, plus every link on it.
Where it all ends up, without marching there.
Solves for the temperatures at which every node’s heat in equals its heat out, given
power arriving at the absorbing node. That is the number a designer actually wants —
will the winding survive — and stepping to it is both slow and approximate: the
assembly’s time constant is C/G over the whole thing, so reaching one part in a
thousand of the answer takes about seven of them, and every step of that is an explicit
Euler step accumulating its own error.
This is not the implicit stepping this workspace declines to have. Nothing about
Domain::step changes, the kernel is untouched, and no schedule learns anything: it is
a question asked of a network — where does this end up — answered by solving the same
balance the step loop converges to. The network is not modified; the temperatures come
back and what you do with them is yours.
§The nonlinearity, and why Newton rather than one solve
With emissivity zero the balance is linear and this converges in a single iteration, to
machine precision. Radiation makes it T⁴ and one solve would be an answer to the
linearised problem rather than to the problem — the mistake
LumpedMass::equilibrium_rise was written to
correct on a single body, and it is the same mistake here with more nodes. So: Newton,
with 4εσAT³ as the radiative part of the Jacobian, which is exactly the
linearised_loss_conductance the step limit already uses.
The linear cases exit after one solve. The radiative ones take more than they look like they should, and the bound on them was wrong at first: it was set to twice the worst case the mild tests exercised, and refused a kilowatt.
The cost is the overshoot on the first step. At ambient the radiative slope 4εσAT³ is
tiny against what the balance eventually needs, so the first solve lands far above the
answer and Newton walks down at the 3/4 error ratio a quartic gives. Counted on one
radiating node:
1 kW 12 iterations 2 037 K
100 kW 24 6 646 K
10 MW 36 21 039 K
1 GW 48 66 533 K
1 TW 66 374 142 KRoughly twelve more per factor of a hundred in power, so the limit of 100 covers anything
a caller could mean. Exhausting it returns a Violation rather than the last iterate,
because an unconverged guess is a plausible temperature for a balance that was never
struck.
§What it refuses
A network with no environment anywhere, when power is arriving. Heat has nowhere to go, so no steady state exists and the balance has no solution — the matrix is singular. Marching such a network is perfectly well defined; it simply heats up forever. Returning a plausible number here would be the worst outcome, so it is named instead.
let mut net = ThermalNetwork::new("motor");
let winding = net.node("winding", Substance::copper(), Volume::from_si(18e-6),
Length::mm(2.0), Temperature::celsius(25.0));
let case = net.node_losing_to("case", Substance::aluminium_6061(), Volume::from_si(220e-6),
Length::mm(4.0), Temperature::celsius(25.0),
Environment::still_air(Temperature::celsius(25.0),
Area::from_si(0.042)));
net.link(winding, case, Conductance::w_per_k(0.9)).unwrap();
net.absorbing(winding).unwrap();
let settled = net.steady_state(Power::w(6.0)).unwrap();
// The joint carries the full 6 W at steady state, so the drop across it is P/K.
let drop = settled.temperature(winding).to_si() - settled.temperature(case).to_si();
assert!((drop - 6.0 / 0.9).abs() < 1e-9, "{drop}");Trait Implementations§
Source§impl Domain for ThermalNetwork
impl Domain for ThermalNetwork
Source§fn max_stable_dt(&self, _now: Time) -> Time
fn max_stable_dt(&self, _now: Time) -> Time
A tenth of the fastest node’s time constant.
The explicit limit is set by whichever node empties quickest, and in a network that is
often not a node touching the outside: a small winding on a stiff link to a large
housing is far faster than the housing is, and its limit comes entirely from the link.
That is why ThermalNetwork’s violation names the node rather than only the network.
A tenth for the same reason LumpedMass reports one: explicit Euler
is stable to 2τ and accurate nowhere near it, so the number a scheduler should honour
is the accuracy limit. With one node and no links this is LumpedMass::max_stable_dt
exactly.
State-dependent, because the environment term is linearised at each node’s current temperature — so the limit tightens as the network heats.
Source§fn ledger(&self) -> Ledger
fn ledger(&self) -> Ledger
What every node is holding, plus what the environments have taken.
One add per node rather than one for the sum, so Ledger’s scale is the largest single
node’s holding instead of a near-zero net — which is what the scale exists for, and the
mistake NBody::ledger made until a test proved its momentum audit was inert.
Measured from each node’s initial temperature. An interior node has no ambient to
measure from, and Bar1D’s reasoning applies anyway: differencing absolute enthalpies
leaves a rounding floor that gets worse on refinement.
Source§fn checkpoint(&mut self)
fn checkpoint(&mut self)
Every node’s temperature and both running totals.
All of it, because ledger reads all of it. LumpedMass saved its temperature and not
its lost, and a rewound iterative sweep therefore reported heat it had already shed —
which went unnoticed for as long as it did because nothing in the workspace had a
residual, so the restore branch never ran.
Source§fn readings(&self) -> Vec<Reading>
fn readings(&self) -> Vec<Reading>
Every node, by the name it was given.
Not a summary. The number a network exists to produce is the drop across a joint, and a mean over the nodes reports neither end of it.
Source§fn as_field(&self) -> Option<&dyn ScalarField>
fn as_field(&self) -> Option<&dyn ScalarField>
None, and not as an oversight.
A network is a graph with no embedding. Its nodes have capacities, not positions, and a conductance is not a distance — two nodes joined by 0.8 W/K are not “0.8 apart” in any space a field could be sampled over. Interpolating between them would invent a continuum with less justification than a box of atoms or a set of orbits would have, and those decline too.
Source§fn books_balance(&self) -> bool
fn books_balance(&self) -> bool
Source§fn name(&self) -> &str
fn name(&self) -> &str
Source§fn kind(&self) -> Kind
fn kind(&self) -> Kind
Kind::Evolving.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 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 as_any_mut(&mut self) -> Option<&mut dyn Any>
fn as_any_mut(&mut self) -> Option<&mut dyn Any>
Source§fn residual(&self) -> f64
fn residual(&self) -> f64
Schedule::Iterative. Zero means converged.