Add phonometry.fluids, and compute the air instead of assuming it (#680)
* Add phonometry.fluids, and compute the air instead of assuming it
A twentieth domain package holding the state of the medium a sound travels
through. `fluids.air()` computes humid air from IEC 61094-2:2009 Annex F, the
CIPM-2007 formulation, and returns a frozen `Fluid`.
All ten figures Table F.1 prints reproduce, at both printed condition sets,
inside the rounding of the last figure the annex gives. The tolerance is
derived rather than chosen: the printed values are kept as strings and half a
unit in their last place is computed from them, so nobody can loosen it by
accident and a future citation with more figures tightens it by itself. Three
conformance rows record how much of that allowance each set uses; set B uses
96 % of it, on the density, which is the annex's own rounding and not slack of
ours.
`fluids` joins the transverse toolbox, so every domain may import it with no
architecture edge, as they already import filters, signals and metrology. A
medium is not a domain of application but something every domain needs, and
eleven identical edges would have recorded nothing. It is the fourth
transverse package and the first added since 4.0 split the original one in
three; the whole package registration lands in this one commit because the
orphan, owner and taxonomy gates each fail on a package publishing an unowned
name.
The Fluid says what it was computed for and what fixed it: the conditions, the
composition, the model by name and the domain that model states for itself.
Reading a quantity the model does not determine raises
FluidPropertyUnavailable naming the model, rather than returning a plausible
number nobody printed. That matters as soon as a second fluid arrives, since
sea water has no ratio of specific heats to give.
`temperature_c` is required: there is no defensible default for the one
condition a caller has actually measured. The pressure and the humidity
default and say so, once, in a single FluidAssumptionWarning naming both and
what each is worth. Measured with the annex's own equations: over 80 kPa to
105 kPa the density moves by a quarter, so a site 1000 m up sits about 11 %
from the assumed pressure, while the whole span of humidity is worth about
1 %. The two are defaulted for opposite reasons, one because nobody knows it
without measuring and the other because everybody assumes they know it.
Supplying both is silent. The carbon dioxide fraction defaults without
warning, because Clause F.2 names 0,000 4 for laboratory conditions.
Nothing is refused but what cannot exist. Annex F states 15 degC to 27 degC,
60 kPa to 110 kPa and 10 % to 90 % relative humidity, and a state outside that
warns and still answers: the annex states where its equations were validated,
not what air can be.
Thermal conductivity and specific heat capacity come from the two expressions
of Clause F.6. Table F.1 tabulates only the diffusivity they form, so they have
no printed value of their own; what is checked is that they close Formula
(F.5) with the diffusivity that is printed, which is the guard against
transcribing either expression wrongly in a way their ratio would hide.
* Say the pressure and the density apart in the assumption warning
The message named the density and then gave a percentage of the pressure in
the same sentence, which is true read carefully and misleading read once. It
now states the case a caller actually meets: a site 1000 m up sits near
90 kPa, and taking that for one standard atmosphere puts the density about
13 % high.
* Let a model's own Prandtl number win over the identity
The property promised that a published fit carrying its own Prandtl number
would keep it, and then closed eta/(rho alpha_t) regardless. A carried value
now wins.
It is the difference between correcting a model and changing one.
Johnson-Champoux-Allard was fitted with 0,71; air at that state has 0,728, and
substituting it moves the characteristic impedance and the wavenumber the model
computes by 1,5 parts in a thousand. A fit keeps the constant it was fitted
with; a model that prints none has it closed from the three that it did.
* Refuse the air that cannot exist, not only the argument that cannot
Each of the three conditions can be a state and the combination still not be
one. At 20 degC and 1 kPa, 50 % relative humidity asks for a water vapour mole
fraction of 1,17: more water vapour than total pressure. Every argument passed
its own guard, so nothing caught it, and the CIPM equations carried on and
returned a density of 0,0066 kg/m3 and a speed of sound of 403 m/s, both with
the shape of a measurement.
The guard is on the fraction, because no bound on any single argument can
express it. It has to let real air through: saturated air at 60 degC reaches
0,198 and saturated thin air at 50 degC and 60 kPa reaches 0,207, so the only
defensible bound is the one physics sets, a mole fraction below 1. The message
says how much pressure that temperature would need.
It caught a test asserting something false. `test_only_the_impossible_is_refused`
listed 200 degC at 50 % relative humidity as real air outside the annex domain;
saturation there is 1 592 kPa, so at one atmosphere the most that air can hold
is 6,4 %. It stays in the list at a humidity it can have.
Separately, the assumption warning was raised before the optional values were
validated, so a caller who promotes it to an error received it in place of the
ValueError a bad humidity earns and never learned which argument was wrong. It
now comes after the refusals.
Six more printed tables, and two kinds of row that are not an isotropic solid (#847)
* Six more printed tables: Norton & Karczub, Vigran and Rossing
Norton & Karczub's Appendix 4a (21 solids), Table 6.1 (11 loss factors)
and Appendices 4b and 4c (18 liquids and gases), and Vigran's Table 3.1
(9 building materials), join the solids and fluids catalogues. Two tables
that are not isotropic solids get a row type of their own: Norton &
Karczub's Table 3.1, the plateau-method constants, and Rossing's Table
15.5, the four plate stiffnesses of spruce and maple.
Three cells are registered as errata and held as printed but not served:
the cork modulus of Norton & Karczub, the aluminium Poisson ratio of
Vigran, and maple's scaling factor in Rossing, which is 1.4 where the
page's own stiffnesses give 1.50. The fluid loader now carries the
validity line of each table instead of one fixed sentence.
* Read the wood constants and the plateau height the way the page defines them
Rossing's equation (15.86) makes D1 and D3 the two directions, D2 the
Poisson coupling and D4 the twisting stiffness; the module, the page and
the catalogue headings had D2, D3 and D4 shifted, which is also where the
"sixteen times stiffer" came from (D1 over D3 is thirteen). None of the
four carries the plate thickness, and the scaling factor stretches one
wood's two directions, not one wood against the other.
Norton & Karczub's coincidence height is the level of the plateau, a
transmission loss, not the depth of a dip. building.PLATEAU_MATERIALS
typed the same table a second time; it is now built from the catalogue.
The appendix's critical-frequency column is held as printed rather than
replaced by this library's plate-speed product, which differs from it.
The static modulus of aerated concrete derives no dynamic quantity. The
fluid loader carries a row's note into the state's validity, the page
publishes fluids under their printed names with the note on the density,
and the two estimated maple cells are marked on the page. Three counts
in the table descriptions are corrected.
* Format the plateau constants helper
* The wave speeds guide counts nine tables and four tins
The steel lookup now answers with seven books and the tin lookup with
four, one of them a loss factor with no modulus, which the example no
longer divides. Norton & Karczub's 45 GPa sides with Bies against the two
books at 4.4, and the prose says so in both languages and in the mirror.
* Name the fluids package once in its catalogue loader
* Say which two cork cells carry a dash
A gas that is not air or water is computed now, not typed (#807)
* A gas that is not air or water is computed now, not typed
A ratio of specific heats and a molar mass close the ideal-gas state
completely, which is exactly the pair a gas table prints, so fluids.ideal_gas
turns those two columns into a Fluid the same way air and sea water already
are.
How far the closure goes is measured against Hopkins Table A1, which prints
both the inputs and the outputs for six gases at stated conditions: the speeds
land within half a metre per second for all six, the densities within two parts
in a thousand for the four light ones, and carbon dioxide and sulphur
hexafluoride come out 0,7 and 2,1 per cent light. That is the compressibility
factor, the model says so in its own validity string, and a test holds it to
those numbers.
* The fluids index says which three media it builds
* The gas module is on the API-reference map
* The gas module is listed in the llms index
* A ratio of specific heats at or below one is not a gas
* The gas row's new bound reaches the llms mirror
* The generated reference and the llms mirrors follow the rebase
* The density shortfall is named as one minus Z, and every transport property it lacks is listed
* The reference pressure is written the way the page that prints it is
Two books print the gases, and six cells of one of them are not served (#827)
* Two books print the gases, and six cells of one of them are not served
A gas table prints what a gas is rather than what one sample of it was
doing: the molar mass and the ratio of specific heats, which between them
fix every state the gas can be in. So the thirty-seven rows of Bies Table
C.2 and the six of Hopkins Table A1 become a catalogue of their own,
`fluids.PUBLISHED_GASES`, and `Gas.ideal_state` walks from that pair to a
`Fluid` at whichever conditions are asked for, with the page carried along.
Six cells of Table C.2 are not served as values. Four molar masses do not
belong to the molecule their row names, and two ratios of specific heats
are outside what the quantity can be. What makes those six a defect rather
than a convention is the other thirty-one: they reproduce their formula
mass to better than a twentieth of a per cent, so the table's own precision
is two parts in a thousand, against which the four exceptions are 1.6, 6.9,
50 and 110 per cent out. `CatalogueRow.misprinted` is the hedge for it, and
reading such a cell raises, quoting what the page prints and pointing at
the registry entry that argues it. The derivations of all three catalogues
treat it the way they treat a cell the page left empty.
* The pair closes an ideal-gas state, and the two books are a per cent apart on the speed
Three from the review, and the middle one is an arithmetic slip worth
stating plainly: the four per cent I claimed between the two carbon
dioxides is the difference on the ratio of specific heats, not on the
speed of sound, which goes as its square root and is 1,2 per cent apart.
The guides and the module said the two constants fix every state the gas
can be in. They close the ideal-gas state at a given temperature and
pressure, which is a narrower claim and the true one; the text now says so
and points at IDEAL_GAS_VALIDITY for what that closure is worth.
And the filter on the gases table said "Filter by material" over a
placeholder reading methane, argon, steam.
* A filter example that finds nothing is worse than no example
The Spanish placeholders named materials the filter cannot find. It
matches what a row carries, which is the material name as its book prints
it, and every book behind these tables prints English: "hierba" found
none of the eleven grasses, "metano" none of the methane. The gas
placeholder I added yesterday had the same defect, and so did the ground
one already there.
All six now carry terms that resolve, with a comment saying why they are
not translated, because translating them is the obvious thing to do to a
string in a copy object and it is what breaks them.
Keep a catalogue of your own in a spreadsheet: read it from the CSV file the sheet saves, with its JSON header beside it, and write one back (#889)
* Read a catalogue of your own from the CSV file a spreadsheet saves, with its JSON header beside it, and write one back
io.read_catalogue reads a .csv file whose header, the catalogue's JSON document without its rows, sits beside it as <name>.phonometry.json (or where header_path says) and declares the delimiter and the decimal mark; nothing about the dialect is guessed. A cell holds one value, bound, range or word in a closed grammar (0.85, ~0.85, <=30, >=5, 0.30..0.50, 0.85±0.05, [AFr5], true or false in a column of flags), the columns are the row class's fields in any unit of their family, basis, the provenance.* columns, attributed_to.row and columns of your own named x-, and the lines go through the same pass as a JSON document's rows, so every problem is raised at once and placed at its line and its column as a spreadsheet letters them. Text where a number goes is never read as a word, a NaN or zero, a thousands separator is never read, and when every failing number is written with the other decimal mark, or the first line splits at another delimiter, the refusal says which to declare. A cell typed the way a spreadsheet or a data sheet writes it (a range with a dash, +- for a plus-or-minus, a unit after the number, a number grouped in thousands) is told how to write it, and a control character in it is named, never shown. A line break inside a quoted cell reads as a line feed in every column, written CRLF or LF, and a carriage return alone is refused there.
io.write_catalogue writes a .csv file with a byte order mark and its header, in the delimiter and decimal mark asked for, CRLF at the end of every line, refuses what one cell cannot hold at the pointer a JSON document would write it at, and puts an apostrophe before any text a spreadsheet would run as a formula, which the reader takes off again. Catalogue.header_sha256 is the hash of the header a CSV file was read with.
A row's credit travels in the attributed_to.row column, and the credit of the whole table is the document's own attributed_to, one text beside the provenance, which a JSON document may now write as well. Rossing's tables of B/A credit each paper to its row, or to the whole table on Table 8.2, so they go into a sheet whole; only a credit for one cell stays in a JSON document.
The guide Your own catalogues gains a section, in both languages and in docs/, that writes a published table as a CSV file and its header, reads a sheet in the closed grammar of cells, shows refusals at their lines and columns, and says what only a JSON document can hold.
* A CSV row is said to start on a line of its own, since a quoted line break carries it over the next
* A CSV file and its header are read as a JSON catalogue is, from a regular file only and never past their limit, and named by their escapes
* The read_catalogue row says again that a catalogue is read only from a regular file, and the CSV wording is reflowed
The row of read_catalogue in the API table said, before the CSV file came,
that the file is a regular file of 16 MiB at most and that a pipe, a device
or a directory at the name is refused before it is opened. It says so again,
for the JSON file and for the CSV file with its header, which is read the
same way. The paragraphs that now say a CSV row starts on a line of its own
are wrapped again, in the guide in both languages, its mirror, the
CHANGELOG and the docstrings, and the test of a file name that is not UTF-8
pins the message each of its two cases writes.
* Each step of reading and writing a CSV catalogue is a function of its own
A cell's grammar reads a bound, a range and a value in helpers, and the
diagnosis of a cell that is none hands a miswritten plus-or-minus or range
to one. A column's role is its own column, a misplaced one or a field's; a
cell goes into the row as a word or as a text; the writer spells a word, a
range and a bound in helpers, and a row's texts, hedges and provenance in
methods of their own. The CSV branch of write_catalogue is a helper, and the
credit's pointer is named once. Nothing any of them reads, writes or refuses
changes.
* A CSV row that empties an entry of its document's provenance is refused, and goes into a JSON catalogue
A row writes a provenance entry only where it says other than its
document, so an empty one clears what the document fills. The CSV writer
wrote it as an empty cell, which the reader takes for the document's own:
a row read from a JSON catalogue with "page": "", or written against a
page given by provenance=, read back from the sheet citing the document's
page, and nothing said so. It is now refused at the entry's pointer, as an
empty text whose default says something is, with the words that the row is
written only in a JSON catalogue, and nothing is written. A test for each
of the two ways a row comes to empty the page fails on the code before this
change, and the same rows written as JSON read back with the empty page.
* The test of a symbolic link at a CSV header skips where the system makes no link
Windows makes a symbolic link only for a process that holds the privilege,
and without it Path.symlink_to raised OSError before the writer ran, so the
test failed rather than skipped there. It now skips when the link cannot be
made, as the tests of a hard link to a sidecar do and as the test of a
symbolic link at a JSON catalogue now does.
* A CSV file holds an empty text only where an empty cell reads back as one
An empty cell reads as nothing written: a text field takes its default, a
provenance entry the document's, and a column of the caller's is left out
of the row. The writer refused an empty text whose default says something
and, since the change before this one, a provenance entry a row empties,
but two more went into the sheet and came back as something else. An empty
text in a column of the caller's was written and read back as a row
without the column, so the catalogue's extras lost the entry, and one in a
text field of the caller's row class that has no default was written and
then refused on every line by the reader, as a row without a field its
class needs, so the sheet could not be read back at all. The writer now
writes an empty text only in a field whose default is empty and refuses one
anywhere else at its pointer, with the words that a row holding it is
written only in a JSON catalogue, and nothing is written.
Every value a sheet writes as a cell was emptied in turn in a one-row
catalogue of each published row class: each text field, a column of the
caller's, the row's credit, each provenance entry against a document that
fills it and one that does not, the basis, a word for a number and a number
left out. Each now reads back or is refused, the JSON reader refusing the
empty credit, basis and word and an empty name. A test for the column of
the caller's and one for the field with no default, from a document and
from rows built in Python, fail on the code before this change, and the
same rows written as JSON keep the empty text. A guard empties every text
field, a column of the caller's and every provenance entry of every
published row class, and fails on the code before this change for each of
them.
Take the medium as a fluid, not as four floats each model re-checks (#690)
* Take the medium as a fluid, not as four floats each model re-checks
Ten visco-thermal models took the air as loose floats:
`johnson_champoux_allard` six of them, `effective_kappa` five. They take a
`phonometry.fluids.Fluid` now, so a caller can predict an absorber in the air
of the room by passing `fluids.air(...)` instead of copying four numbers.
The rule is that a function needing two or more properties of the medium takes
the fluid, and one needing only a speed of sound keeps the scalar: a wavelength,
a delay or a cut-on frequency wants a number, not a state. Twenty
single-property entry points are unchanged by that rule.
`environment` follows because it must: `ground_effect` feeds the porous ground
models. It also had its own copy of the air, `343.0` and `1.205`, under a
comment reading "matches the materials domain". It does not need one.
Two things came out of the sweep that were not renaming.
The parabolic equation passed `c_ref`, the speed the profile has at the ground,
as though it were the air's own. That is the right number for the ground
impedance and the wrong description of it, so it now builds the caller's air at
that local speed explicitly.
And `thermal_boundary_layer_thickness` and `effective_kappa` do not default to
the published air at all: theirs is the ISO 9053-2 Annex A.3 state at 23 degC,
whose five constants are transcribed Table F.1 cells. Recomputing them from
`fluids.air(temperature_c=23.0, ...)` agrees only to the eighth figure, so
`ANNEX_A_AIR` carries the cells rather than the computation. The oracle for the
printed air stays in `tests/reference_data`, where the conformance rows can
still be independent of the library.
`Fluid` now requires every property to be positive and finite, which is what
lets the models stop re-checking three or four floats each. The guards they
dropped are tested where the check now lives.
Verified against the committed evidence: 619 of 619 conformance checks, and the
report regenerates without a single digit changing.
* Read the ratio of specific heats from the fluid, and stop documenting what is gone
Nine findings from review of the fluid conversion, four of them closed as a class
rather than as the case reported.
The one that changed a number: `membrane_resonance_frequency` divided the
isothermal air-spring stiffness by the published 1.4 instead of the fluid's own
ratio, so an air the caller computed came back with the published air's resonance
under the caller's density and speed of sound, which is neither air. Warm humid
air carries 1.3806 and the frequency moves 0.70 per cent, invisible in the
result. A sweep of the tree for a function that takes a `fluid` and still reads
an air constant found this one and no other.
The conversion also left twenty-five `:param` entries for arguments the
signatures no longer have, six of them on `johnson_champoux_allard` alone, so a
reader following the docstring got a `TypeError`; and thirteen public functions
took a `fluid` without documenting it, two of them describing the withdrawn
floats in prose instead. Nothing caught either: a docstring is prose to every
gate here, and the generated tables are built from the signature rather than from
the text beside it, so the two drifted without either looking wrong alone. An
architecture test now fails on any `:param` naming an argument that does not
exist, checked by reintroducing one and watching it name the function and line.
The curated reference table had the same drift in the other direction. Ten of
its rows still offered `speed_of_sound` and `air_density` to functions that take
a `Fluid`, because the gate asks only that every public name have a row, never
that a row describe the function it names. A second architecture test closes
that half, over the closed vocabulary of floats a `Fluid` replaced so that no
row is flagged for prose; reverting one row makes it name the row and the
argument.
The reference generator printed `phonometry.fluids._state.Fluid`, offering an
import that works and is not supported. It resolves to the package that
publishes the class now, so the fix holds for every future private module.
The rest: a duplicate `### Changed` heading.
Two branches the conversion added had no test: the composition fraction a
`Fluid` refuses on construction, and the Miki ground the flow-resistivity path
can be asked for. Miki predicts between 0.58 and 0.96 of the Delany-Bazley
surface impedance across 50 Hz to 5 kHz over a 200 kPa.s/m2 ground, which moves
the interference dip from 481 Hz to 381 Hz; that is the reason to offer the
choice, so that is what the test pins.
Move sea water to the medium, with the density the library never had (#685)
* Move sea water to the medium, with the density the library never had
fluids.sea_water() returns a Fluid carrying a density and a speed of sound.
The density is new: nothing in this library computed one. It implements
Ainslie, Principles of Sonar Performance Modelling (Springer 2010), Equation
(4.6) on printed folio 127, attributed there to Pierce (1989, p. 34), with the
units its Equations (4.7) to (4.10) fix and the absolute pressure its Equation
(4.4) defines.
The four sound-speed fits move with it, because a medium's own properties
belong with the medium. The addition and the withdrawal are one commit: one
name published by two packages is what the taxonomy gate refuses, even for a
single commit in between.
The profile stays in underwater. A profile describes a place rather than a
substance, and it belongs with the marcher that walks it. It now asks for the
whole column in one call, where it used to restate the model dispatch and the
pressure conversion a second time; making sea_water_sound_speed array-capable
is what allowed that, and overloads keep a scalar call typed as a float.
The two depth-to-pressure conversions are told apart by name because they are
not the same quantity. Leroy & Parthiot is gauge, in megapascals, zero at the
surface, and it is what the UNESCO and Del Grosso fits want. Ainslie's Equation
(4.11) is absolute, in pascals, an atmosphere at the surface, and it is what
the density wants. Between them lie a factor of a million and an offset of an
atmosphere.
Sea water has no ratio of specific heats, viscosity or thermal diffusivity
here, because no source in this library prints one. Reading any of them raises
FluidPropertyUnavailable naming the model. That is the case the accessor was
built for, and the reason air and water can share a type at all.
Two errata registered against Ainslie (2010), verified from the printed pages:
folio 177 quotes 1024,2 kg/m3 where Equation (4.6) read with Equation (4.4)
gives 1024,287 9, reproducing only with the pressure term dropped; and Equation
(4.13) prints the pressure coefficient as 4,3e-5 where (4.6) has 4,3e-7.
* Wrap the Ainslie equation so no line starts with an operator
Markdown reads a line beginning with '-' or '+' as a list marker, so the
continuation of the inline maths closed the span early and every '$' after it
paired off wrong: one bad wrap, two reported hazards. Breaking after the
operator instead of before it leaves the equation reading the same and the
parity intact.
* Move the sea-water row to the package that now owns it, and drop a ghost
The curated reference table is hand-maintained, and its gate asks only that
every public name have a row somewhere. It does not ask that the row sit in the
right package, nor that every row still name something that exists, so the move
left two kinds of drift behind.
`sea_water_sound_speed` stayed in the underwater block among the ISO 18405 and
18406 rows while fourteen of the fifteen `fluids` names sat together further
down. It joins them, and its example says where it lives now.
`depth_to_pressure` was withdrawn by this move, and its row survived: a reader
following the table would have imported a name that is gone. What replaced it,
`depth_to_gauge_pressure_mpa`, already has its own row two lines above.
Regenerating the llms artifacts was also outstanding on this branch, which is
what the `llms artifacts up to date` job was reporting. The source pages were
already correct; only the generated copies still said `underwater.`.
* Say whose atmosphere the surface pressure is
The reference table called 101 989,16 Pa "one atmosphere at the surface" three
rows above calling 101 325,0 Pa "one standard atmosphere". They differ by
664 Pa, and both rows were in the same table, so the reader had to pick.
The Ainslie value is deliberate: Equation (4.11) puts 98 066,5 x 1,04 at the
surface, which is that book's own reference and not the standard atmosphere.
The row says which it is and by how much they differ.
The sea-water bullet also opened without a main verb, so it named neither
function as the thing being added.