pub struct Substance {
pub name: String,
pub density: Density,
pub thermal: Option<ThermalProps>,
pub fusion: Option<FusionProps>,
pub mechanical: Option<MechanicalProps>,
pub acoustic: Option<AcousticProps>,
}Expand description
A material, as much of it as is known.
§A key this type does not know is refused, not dropped
deny_unknown_fields, here and on all four property blocks. serde discards unknown keys by
default, which is right for a wire protocol that must tolerate a newer peer and wrong for a
material somebody wrote down: a mistyped "thermalz" would leave the whole thermal block absent
and the substance would run as one whose conductivity is unknown rather than as one whose file
has a typo in it.
The same rule pantometry-world’s scene format has, for the same reason, and it was added after a
test asked whether a typo was caught and found that it was not.
Fields§
§name: StringWhat it is called. Free text: a catalogue designation, a common name, whatever the caller will recognise in a violation message.
density: Densitykg·m⁻³. The one property everything has, which is why it is not optional.
thermal: Option<ThermalProps>Conductivity, specific heat, emissivity and a service limit, if they are known.
Optional because a substance is often only known as far as it needed to be. A domain
asking for what is not here gets None rather than a plausible default, which is the
difference between “unknown” and “zero”.
fusion: Option<FusionProps>What it takes to melt it, if it is a substance that melts at a temperature.
Absent for most of them, and absent is not zero — it means the substance is being modelled as never changing phase, which is the right model for a heat sink and the wrong one for ice. A domain that finds it absent does not change phase; one that finds it present must account for the latent heat or its books will not balance.
mechanical: Option<MechanicalProps>Stiffness, restitution and friction, if they are known.
acoustic: Option<AcousticProps>Sound speed and absorption, if they are known.
Implementations§
Source§impl Substance
impl Substance
Sourcepub const CATALOGUE: [&'static str; 9]
pub const CATALOGUE: [&'static str; 9]
Every substance this crate ships, by the short name a file writes.
The constructors below are the Rust door and are enough for a caller who knows at compile time what the thing is made of. This is the other door: a name that arrived as text, from JSON, a command line, or a spreadsheet column.
§Why a slug and not Substance::name
name is free text meant for a human reading a violation message — "Al 6061",
"N-BK7". A key that a file is matched against has to be stable, lowercase and
unpunctuated, and the two are different jobs: renaming "Al 6061" to "Aluminium 6061-T6"
improves one and breaks every file that used the other.
§This list and Substance::from_name are checked against each other
Both directions, in any_material.rs: every slug here resolves, and every constructor is
reachable through some slug here. A pair of hand-written lists that agree until they do not
is how a catalogue grows an entry that exists and cannot be named — which is what had
happened to water, present in this crate since 0.1.0 and unreachable from a scene file for
eleven releases — every version in which a scene could name a material at all — because
the scene format kept its own eight-name copy of this.
Sourcepub fn diffusivity(&self) -> Option<Diffusivity>
pub fn diffusivity(&self) -> Option<Diffusivity>
Thermal diffusivity, α = k / (ρ c_p) — the m²/s that decides how fast a
temperature front moves, as opposed to how much heat flows.
This is the number that sets an explicit heat solver’s stability limit
(dt < dx²/2α), so a thermal domain asks for it before it can say how big
a step it can take.
Sourcepub fn heat_capacity(&self, volume: Volume) -> Option<HeatCapacity>
pub fn heat_capacity(&self, volume: Volume) -> Option<HeatCapacity>
Heat capacity of a given volume of this substance.
Sourcepub fn latent_energy(&self, volume: Volume) -> Option<Energy>
pub fn latent_energy(&self, volume: Volume) -> Option<Energy>
The joules a given volume absorbs changing phase, or None if it does not.
The companion to heat_capacity, and the pair is what a domain
needs to keep books that balance across a melting front: one buys kelvin and the other buys
none. For a cubic millimetre of ice they are 1.88 mJ/K and 306 mJ — so the phase change
is worth 163 K of warming, and a scheme that dropped it would run the front 163 times too
fast rather than slightly wrong.
Sourcepub fn expansion_of(&self, length: Length, rise: Temperature) -> Option<Length>
pub fn expansion_of(&self, length: Length, rise: Temperature) -> Option<Length>
How much a length of this substance grows for a temperature rise. Linear, which is a good approximation for the tens of kelvin an instrument sees and a poor one for hundreds.
Sourcepub fn constrained_stress(&self, rise: Temperature) -> Option<Pressure>
pub fn constrained_stress(&self, rise: Temperature) -> Option<Pressure>
Stress produced by preventing that expansion — the reason a lens bonded rigidly into a metal mount cracks when it warms up.
σ = E α ΔT, independent of size, which is why scaling the part down does
not help.
Sourcepub fn survives(&self, rise: Temperature) -> Option<bool>
pub fn survives(&self, rise: Temperature) -> Option<bool>
Whether that stress would break it.
Sourcepub fn from_name(name: &str) -> Option<Substance>
pub fn from_name(name: &str) -> Option<Substance>
Look one up by the name in Substance::CATALOGUE, or None.
None rather than a panic or a default: a name that arrived as text is a name that can be
wrong, and the caller is the one who knows what to say about it and where the text came from.
Substituting a plausible material for an unrecognised name is the failure this whole file is
arranged against.
A catalogue is not the answer to “any material”. Nine entries cannot be, and adding a
tenth does not change that. The answer is that a Substance is data — Deserialize and
Substance::check — so anything with a datasheet can be declared without this crate
learning it exists. This function is for the common case where the material is one of nine
and typing out its properties would be worse.
Sourcepub fn borosilicate_crown() -> Substance
pub fn borosilicate_crown() -> Substance
N-BK7, the borosilicate crown that most of an optical bench is made of.
Sourcepub fn aluminium_6061() -> Substance
pub fn aluminium_6061() -> Substance
6061 aluminium: what the mount holding the glass is made of, and the reason a mount-and-lens pair moves when the room does — its expansion is three times the glass’s.
Sourcepub fn with_emissivity(self, emissivity: f64) -> Substance
pub fn with_emissivity(self, emissivity: f64) -> Substance
The same substance with a different surface finish.
Emissivity is a property of the surface and not of the material, so anodised aluminium is
not a new entry in the catalogue — it is aluminium_6061().with_emissivity(0.9). The
factor of ten between polished and anodised 6061 lands squarely on the radiative loss
path, which is the same order as still-air convection at room temperature.
Clamped to 0..=1: a surface cannot radiate more than a blackbody, and a negative
emissivity would make a body warm itself.
Does nothing to a substance whose thermal properties are unknown, because None means
unknown rather than zero and inventing three of the four fields to set the fourth would
be worse than declining.
Sourcepub fn with_specific_heat(self, specific_heat: SpecificHeat) -> Substance
pub fn with_specific_heat(self, specific_heat: SpecificHeat) -> Substance
The same substance with a different heat capacity.
For the assembly case: a motor, a populated board, a printed part with infill. The bulk
c_p of a mixture is not the c_p of its main constituent, and this is the field where
that difference is worth a factor of two.
Does nothing to a substance whose thermal properties are unknown, for the reason in
Substance::with_emissivity.
Sourcepub fn stainless_304() -> Substance
pub fn stainless_304() -> Substance
Austenitic stainless, 304/18-8. What a portafilter basket, a boiler and most food-contact hardware is.
§It is a poor conductor and that is the point
16.2 W/m/K against aluminium’s 167 — a factor of ten — while holding more heat per unit volume, 4.0 MJ/m³/K against 2.4. So a steel part is a better reservoir and a worse spreader than an aluminium one of the same size, which is why a group head is brass and a basket is not.
For an explicit conduction solver that combination is also the difference between a step of 2.4 ms and one of 41 ms on a millimetre grid, because the limit goes as the diffusivity and steel’s is seventeen times lower. Reaching for aluminium because it is the metal already in the catalogue costs an order of magnitude in run time and understates the thermal mass.
Sourcepub fn copper() -> Substance
pub fn copper() -> Substance
Electrolytic tough-pitch copper: windings, heat spreaders, planes.
The values are uncontroversial to three figures. The emissivity is not: this is bright
polished copper at 0.04, and copper oxidises — a tarnished surface runs 0.4 to 0.8, a
factor of fifteen on the radiative path. If the part has been in air for a week, say so
with Substance::with_emissivity.
Sourcepub fn fr4() -> Substance
pub fn fr4() -> Substance
FR-4 glass-epoxy laminate: the board a driver sits on.
The conductivity is the through-plane one, 0.3 W/m/K, and that is the number a
designer wants because it is the one heat has to cross to reach the far side. In-plane it
is nearer 0.8, because the copper-free glass weave conducts better along its fibres —
a factor of about three, and ThermalProps carries one scalar, so the choice has to be
stated rather than averaged. Any real board is dominated by its copper pour anyway, which
is not laminate at all.
The expansion is likewise in-plane, 14 ppm/K. Through-thickness FR-4 expands four to five times faster and goes higher again above its glass transition, which is what breaks plated through-holes; that regime is not modelled here.
Sourcepub fn electrical_steel() -> Substance
pub fn electrical_steel() -> Substance
Non-oriented silicon electrical steel: motor and transformer laminations.
Grade-dependent, and the spread is wide. Silicon content trades core loss against conductivity: 25 W/m/K here is mid-range for non-oriented sheet, and grades run from about 20 to 30. Stacked laminations conduct far worse across the stack than the sheet does, because the interlaminar varnish dominates — a stack is not this substance at all, and treating it as one overstates the conduction out of a motor.
Emissivity 0.3 is varnished sheet; bare mill finish is lower and rusty is much higher.
Sourcepub fn pla() -> Substance
pub fn pla() -> Substance
Solid cast PLA: printed structure, if it were solid, which it is not.
A printed part is not this substance. Infill and layer adhesion move the effective conductivity and density more than the polymer chemistry does: at 20% infill the density is a fifth of this and the through-layer conductivity is lower again, because the path crosses voids and weld lines rather than bulk. Scale the density by the infill fraction at the very least, and treat the conductivity as an upper bound.
That is not a caveat about precision. It is the difference between a part that survives
and one that creeps: PLA softens around 60 °C, and Substance::survives is checking
against a number the print may not reach in practice.
Sourcepub fn ice() -> Substance
pub fn ice() -> Substance
Ice at 0 °C, and the only entry in this catalogue that changes phase.
The canonical Stefan material, and the numbers are the ones the closed-form tests need. 2.22 W/m·K is four times liquid water’s 0.598, which is the thing about ice that surprises people and the reason a lake freezes downward at all.
§This is the solid, and the one-phase model uses it for both sides
A domain given this for a melting problem is taking the liquid’s conductivity and specific heat to be the solid’s, which they are not — water conducts a quarter as well and holds twice as much. That is Stefan’s original one-phase problem, and it is exact when the liquid is already at the melting point so no heat flows through it: a lake freezing from a cold sky, where all the resistance is in the ice.
It is not right for melting a block of ice into water that then warms up. Use
Substance::water for the liquid and note that a cell cannot currently be both.
Sourcepub fn with_thermal(self, thermal: ThermalProps) -> Substance
pub fn with_thermal(self, thermal: ThermalProps) -> Substance
Give it thermal properties, or replace the ones it has.
§Why builders exist, when the fields are already public
Because a struct literal names every field, so it breaks the moment this type learns one.
fusion was added for latent heat and every literal outside this crate stopped compiling —
allowed in 0.x, and still a cost paid by exactly the callers this catalogue is least able to
help: the ones whose material is not in it.
A chain of with_* on bulk is immune to that, and it is how any real
material becomes expressible without waiting for it to be added here:
// Ti-6Al-4V, from a datasheet rather than from this crate.
let titanium = Substance::bulk("Ti-6Al-4V", Density::g_per_cm3(4.43))
.with_thermal(ThermalProps {
conductivity: ThermalConductivity::w_per_m_k(6.7),
specific_heat: SpecificHeat::j_per_kg_k(526.0),
expansion: ThermalExpansion::ppm_per_k(8.6),
emissivity: 0.30,
})
.with_mechanical(MechanicalProps {
youngs_modulus: Pressure::from_si(113.8e9),
poisson_ratio: 0.342,
yield_strength: Pressure::from_si(880.0e6),
});
assert!(titanium.check().is_ok());Enumeration does not reach “every material” and data does. This catalogue holds nine
entries because each is a set of numbers somebody has to be answerable for; a caller with a
datasheet is answerable for theirs. check is what the library can still
do for them.
Sourcepub fn with_mechanical(self, mechanical: MechanicalProps) -> Substance
pub fn with_mechanical(self, mechanical: MechanicalProps) -> Substance
Give it mechanical properties, or replace the ones it has.
Sourcepub fn with_acoustic(self, acoustic: AcousticProps) -> Substance
pub fn with_acoustic(self, acoustic: AcousticProps) -> Substance
Give it acoustic properties, or replace the ones it has.
Sourcepub fn with_fusion(self, fusion: FusionProps) -> Substance
pub fn with_fusion(self, fusion: FusionProps) -> Substance
Give it a phase change, or replace the one it has.
Sourcepub fn check(&self) -> Result<(), String>
pub fn check(&self) -> Result<(), String>
Every problem with this substance’s numbers, or Ok if there are none.
For a material that came from outside this crate — a datasheet, a JSON file, a builder chain — where nobody has checked the numbers against anything. It cannot tell whether a conductivity is right; it can tell whether it is possible, and an impossible one otherwise produces an answer that is plausible and wrong.
Reports all of them at once rather than the first, because a material transcribed from the wrong column is usually wrong in several places.
§The one check that is not a bound on a single field
If a substance states both a sound speed and elastic constants, those are three independent
numbers describing one thing, and they have to agree. A longitudinal wave in a solid is
bounded below by the rod speed sqrt(E/rho) — free to bulge sideways — and above by the bulk
speed sqrt((lambda+2mu)/rho), fully constrained, so a stated speed must sit near one of them.
15%, and it is measured rather than chosen: across this catalogue every entry is within 6.2% of whichever it means, and the gap is there because a tensile test and an ultrasonic measurement are not the same measurement — read as a bulk wave, copper’s stated speed implies 132 GPa against the 117 in its own entry. So the bound cannot be tighter than that, and at 15% it still catches a shear speed transcribed by mistake, which sits 45% below the rod speed.