at main
26 folders
117 files
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Every catalogue row has one shape and says what its source claims for each cell (#862)
* Every published catalogue cell is pinned to its last digit
The 1982 rows of the 23 published catalogues are dumped once, every float by
its repr, into tests/data/published_catalogues/baseline.json. A test takes
that baseline through every change listed in tests/catalogue_fingerprint.py
and asserts that the result is, field for field, what the library builds
today. The page generator's check sees the numbers only as the site prints
them; this sees the last digit of a float and a hedge moved from one field to
another, so a change to how a row is built has to say what it moved.
* Every catalogue row has one shape and says what its source claims
The rows of every published catalogue share one base, and phonometry.io now
publishes it: CatalogueRow, BandedRow, CatalogueError and CATALOGUE_BASES.
A row gains basis, what the source claims a value is (measured, declared,
calculated, estimated or extended), read through basis_of. The estimate had
two spellings, SolidMaterial.estimated with is_estimate and
OrthotropicWood.estimated with is_estimated; both are gone, and the 35 cells
Hopkins Table A2 and Rossing Table 15.5 mark as estimates hold "estimated"
in basis.
A value the page prints in another unit is no longer derived: the 116 cells
of Ver & Beranek Table 14.1 in degrees Fahrenheit and psi and the nine of
Long Table 7.1 in sabins keep the printed figure and unit in converted. A
cell the page leaves blank and prints on another row (Ver & Beranek Table
8.7, ASHRAE Table 30, Harris Chapter 32) names that row in carried.
is_derived now answers only for what the library computes. The published
catalogues page marks converted and carried cells as such, with the printed
figure on a converted one, and a test walks every hedged cell of every
catalogue to hold the page to it.
OrthotropicWood and PlateauMaterial take their fields by name only, and no
row class is slotted, because on Python 3.13 a slotted dataclass cannot call
super() without arguments from its own methods. No published value changes:
the fingerprint lists the 54 rows the move touched and nothing else moved.
* A catalogue cell no longer claims a unit or a derivation its page does not print
Long prints the six figures of his musician with no unit. The row now says
so in its note and why they are read as sabins, and `converted` is described
everywhere as the page's figure and its unit rather than as what the page
prints; the published catalogues page words its tooltip the same way. A
figure a packaged table prints with another SI prefix, such as the
megapascals of Rossing Table 15.5, keeps no `converted` entry, and the
docstring says so.
`carried` covers a value the page gives by reference to another row, which
includes the three Harris rows that print "Parecido al anterior" and no row
number, not only a cell left blank under a block. The two ASHRAE rows that
carry a diameter down no longer call it a derivation in their notes, and the
damping paragraph of the catalogues page no longer says converted cells are
marked as worked out.
The fingerprint compares every row as the text the baseline holds, so a
float served as an integer, or a flag served as a number, is a change. The
page generator refuses a cell whose hedges it cannot show, two on one value
or an estimate, a conversion or a carried value on an interval, a list or a
word, instead of publishing it with one of them missing; the test walks
every hedge of every published row to hold it to that.
The older unreleased notes that still named `is_estimated`, the `estimated`
field and cells marked derived now name `basis`, `basis_of` and `converted`.
The scripts and tests that import `CatalogueRow` take it from
`phonometry.io`, and the Spanish pages spell the sabin one way.
* An approximate interval keeps its tilde on the catalogue page
The porosity Cox Table 6.5 prints for granular vermiculite is an interval with
a tilde, and the page showed the bare interval. The range and bound kinds have
no mark of their own, so the tilde now goes into the cell text.
The fingerprint's migration test also requires every field outside the hedges
the step moves to stay equal, basis_of names all five bases, the baseline's
README states the terms that cover it, and the test that probed the
interpreter for the slotted super() failure is gone: Python 3.13.15 carries
the fix and 3.13.5 does not, so it tested the Python patch release, not the
library.
* A list of readings printed with a tilde keeps it on the catalogue page
Like an interval, a cell holding several readings has no mark of its own for
a tilde, so it goes into the text. The generator now refuses an approximate
hedge only on a printed word, which has nowhere to show it. No published cell
changes.
* A catalogue row refuses a basis outside the five words
The catalogue page and every reader of basis_of act on measured, declared,
calculated, estimated and extended only, so a row built with any other word,
such as a misspelt estimate, now raises CatalogueError instead of reaching
the page as a printed number.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
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.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Verdicts on a printed limit that read the decimal value, not its last bits (#914)
A quantity computed from decimal readings (a difference, a mean, a ratio, a spread) and judged against a limit a standard prints in decimal is now settled to nine decimal places before it meets that limit, and a half is rounded on the settled value, so 32,3 - 26,3 dB and 32,2 - 26,2 dB are the same 6 dB margin on every machine. One private helper does this for the whole library, and the six local copies of the same nine-decimal rule now go through it. The settled value only decides the verdict; every result keeps the value it computed. Two results change shape at a degenerate input: room.open_plan_metrics returns NaN for a distance when the STI does not fall with distance, and a hammer and floor that are critically damped in decimal get the critically damped pulse instead of NaN.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Results keep their own copy of the arrays they were given (#939)
np.asarray hands back the very array it is given when that array is already
float64, and so does every validation helper built on it, so a result built
from such an array kept the caller's array, or a view of it: changing that
array afterwards changed a result already computed, and nothing raised. Every
array a function or a method of the library keeps from its arguments is now a
read-only copy of its own, through the private helper read_only_copy, and the
objects that keep arrays to hand out later (SilencerChain, AnpDatabase,
FDTD2D, ElasticFDTD2D, window_metrics) do the same; a Signal keeps a writeable
copy of its samples. The values do not move. scripts/check_array_aliasing.py
(make array-aliasing and a CI job of its own) follows each parameter of every
function of the package to the places an array is kept and fails on one kept
without a copy of its own.
Every catalogue row has one shape and says what its source claims for each cell (#862)
* Every published catalogue cell is pinned to its last digit
The 1982 rows of the 23 published catalogues are dumped once, every float by
its repr, into tests/data/published_catalogues/baseline.json. A test takes
that baseline through every change listed in tests/catalogue_fingerprint.py
and asserts that the result is, field for field, what the library builds
today. The page generator's check sees the numbers only as the site prints
them; this sees the last digit of a float and a hedge moved from one field to
another, so a change to how a row is built has to say what it moved.
* Every catalogue row has one shape and says what its source claims
The rows of every published catalogue share one base, and phonometry.io now
publishes it: CatalogueRow, BandedRow, CatalogueError and CATALOGUE_BASES.
A row gains basis, what the source claims a value is (measured, declared,
calculated, estimated or extended), read through basis_of. The estimate had
two spellings, SolidMaterial.estimated with is_estimate and
OrthotropicWood.estimated with is_estimated; both are gone, and the 35 cells
Hopkins Table A2 and Rossing Table 15.5 mark as estimates hold "estimated"
in basis.
A value the page prints in another unit is no longer derived: the 116 cells
of Ver & Beranek Table 14.1 in degrees Fahrenheit and psi and the nine of
Long Table 7.1 in sabins keep the printed figure and unit in converted. A
cell the page leaves blank and prints on another row (Ver & Beranek Table
8.7, ASHRAE Table 30, Harris Chapter 32) names that row in carried.
is_derived now answers only for what the library computes. The published
catalogues page marks converted and carried cells as such, with the printed
figure on a converted one, and a test walks every hedged cell of every
catalogue to hold the page to it.
OrthotropicWood and PlateauMaterial take their fields by name only, and no
row class is slotted, because on Python 3.13 a slotted dataclass cannot call
super() without arguments from its own methods. No published value changes:
the fingerprint lists the 54 rows the move touched and nothing else moved.
* A catalogue cell no longer claims a unit or a derivation its page does not print
Long prints the six figures of his musician with no unit. The row now says
so in its note and why they are read as sabins, and `converted` is described
everywhere as the page's figure and its unit rather than as what the page
prints; the published catalogues page words its tooltip the same way. A
figure a packaged table prints with another SI prefix, such as the
megapascals of Rossing Table 15.5, keeps no `converted` entry, and the
docstring says so.
`carried` covers a value the page gives by reference to another row, which
includes the three Harris rows that print "Parecido al anterior" and no row
number, not only a cell left blank under a block. The two ASHRAE rows that
carry a diameter down no longer call it a derivation in their notes, and the
damping paragraph of the catalogues page no longer says converted cells are
marked as worked out.
The fingerprint compares every row as the text the baseline holds, so a
float served as an integer, or a flag served as a number, is a change. The
page generator refuses a cell whose hedges it cannot show, two on one value
or an estimate, a conversion or a carried value on an interval, a list or a
word, instead of publishing it with one of them missing; the test walks
every hedge of every published row to hold it to that.
The older unreleased notes that still named `is_estimated`, the `estimated`
field and cells marked derived now name `basis`, `basis_of` and `converted`.
The scripts and tests that import `CatalogueRow` take it from
`phonometry.io`, and the Spanish pages spell the sabin one way.
* An approximate interval keeps its tilde on the catalogue page
The porosity Cox Table 6.5 prints for granular vermiculite is an interval with
a tilde, and the page showed the bare interval. The range and bound kinds have
no mark of their own, so the tilde now goes into the cell text.
The fingerprint's migration test also requires every field outside the hedges
the step moves to stay equal, basis_of names all five bases, the baseline's
README states the terms that cover it, and the test that probed the
interpreter for the slotted super() failure is gone: Python 3.13.15 carries
the fix and 3.13.5 does not, so it tested the Python patch release, not the
library.
* A list of readings printed with a tilde keeps it on the catalogue page
Like an interval, a cell holding several readings has no mark of its own for
a tilde, so it goes into the text. The generator now refuses an approximate
hedge only on a printed word, which has nowhere to show it. No published cell
changes.
* A catalogue row refuses a basis outside the five words
The catalogue page and every reader of basis_of act on measured, declared,
calculated, estimated and extended only, so a row built with any other word,
such as a misspelt estimate, now raises CatalogueError instead of reaching
the page as a printed number.
Pass the form of a survey's reverberation index and a calibration's tone estimate as a bool variable (#913)
estimate_reverberation_index(weighted=) and metrology.sensitivity(narrowband=)
had overloads for True and False only. mypy does not split a bool into the
two literals, so a flag read from a setting was refused although the call
runs.
estimate_reverberation_index gains a third overload that takes a bool and
returns the array or the float. For sensitivity every form returns a float
and the True overload only said that the coherent estimate needs fs, so that
overload now takes a bool with fs as a keyword, a second one takes it with
fs in position after the target level and the reference pressure, and a new
first overload takes a Signal, which carries its rate, with either estimate
and no fs. With fs in position, a bare array now takes narrowband=True as
well, which mypy refused before. A bare array with narrowband=True, or
with a bool that may hold it, still needs fs, and tests/static_typing pins
both refusals with ignores that strict mypy reports once they go unused.
A test reads every overload set in src through the parser and fails when one
types a parameter as Literal[True] or Literal[False] without a bool overload
for it. Nothing changes at run time.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
KB italic, Euler's number upright, and the subscripts of the guides, figures and diagrams set as their standards print them (#952)
The weighted vibration severity KB is italic in every diagram, figure, guide, docstring and errata entry, as DIN 4150-2 and DIN 45672 print it, and Euler's number is upright in every formula, as ISO 80000-2:2019 prints the mathematical constants. `make subscripts` fails on an upright KB or an upright running index and `scripts/check_euler_number.py` fails on an italic e, and both checks now read the row breaks, wrapped inline math and escaped strings they used to skip. Where a standard prints every subscript in one slope, as EN 15657, EN 12354-5 and RD 1367/2007 do, ISO 80000-2 decides: a running index is italic and a descriptive abbreviation upright, so the position levels of EN 15657 are L_{v,i} with an italic i. The standardized levels with their italic T, the ISO 7235 diagram with its sign put right, and the noise control, in-situ, exposure, aircraft and theory symbols follow their printed pages. No public name, signature or return value changes.
Take a solid's row into the elastic simulation and the detailed building model, a ground's into the outdoor models, and complete a catalogue file in your editor with its JSON Schema (#891)
* Take a solid's row into the elastic simulation and the detailed building model, a ground's into the outdoor models, and write the JSON Schema of a catalogue file
The elastic FDTD takes a solid's catalogue row wherever it takes a Material: scholte_speed and ElasticFDTD2D.from_regions read its bulk longitudinal speed, its transverse speed and its density through printed(), never its plate or bar speed, and refuse a cell the page does not print in the page's terms.
GroundSurface.medium turns a ground row into the porous half-space ground_effect, barrier_insertion_loss and atmospheric_parabolic_equation take, through the Delany and Bazley or the Miki model. A row Cox and D'Antonio mark as fitted with one model goes into that model only.
HomogeneousElement.from_solid builds the ISO 12354 element a layer of a solid's row makes, with m' = rho t and fc = c0^2 / (1.8 cL t) from the row's density and plate speed, and never picks one of the row's loss factors: the internal loss factor is the caller's to give.
io.catalogue_schema writes the JSON Schema (2020-12) of a catalogue document for the row classes it is given, and the site publishes the one of every published row class as schemas/phonometry-catalogue-1.json, held current by CI. A document may name it in a top-level $schema, which the reader takes and never reads.
* A dollar sign inside inline code opens no maths in the markdown check
The unclosed-maths rule counted every unescaped $ on a line, so the $schema key of a JSON document written as inline code in a table cell read as maths cut off by the next row. CommonMark reads a code span before any maths could open in it, so the rule now leaves code spans out of the count, and still finds maths that wraps onto a block marker, on a line with a code span or without one.
* The catalogues guide hands a solid, a ground and a wall of your own to their models, and completes a catalogue file in your editor
The guide on catalogues of your own shows a solid's row read from a data sheet going into the elastic simulation by its bulk speeds, a ground fitted to a site going into the outdoor models through medium(), with the refusal of a resistivity fitted with another model, and a block's density and plate speed building an ISO 12354 element with the internal loss factor you give. A new section writes the JSON Schema of a catalogue file with io.catalogue_schema, points a document at it with $schema, and sets an editor to the one the site publishes; the document's keys now list $schema. The elastic waves, ground effect and detailed prediction guides each take a row from a catalogue in their own terms, the published catalogues page draws its ground example through medium() and links the schema, and the Files overview and the guide indexes name it, in English, in Spanish and in the docs mirror.
* The catalogue schema describes a row class by the first line of its docstring, or by its name when it has none
* The JSON Schema writer is held to its place in io's public names
* The elastic FDTD reads a row's fields through the dataclasses module, which no local name of the module shadows
* A wetted sand of Cox Table 6.7 goes into neither outdoor ground model, as the text beside the table says
GroundSurface.medium took the eight rows of the two sands Cox and D'Antonio print at four water contents into the Delany and Bazley and the Miki models, because no footnote marks their fit. The text of Sect. 6.6.3 (PDF page 272, printed p. 215) says their parameters are those of the two-parameter model of Attenborough, so a row the text ties to that model is now refused by both, naming the page, as a row the footnotes mark with another model already was. The notes of the eight rows say so on the catalogues page, in both languages, and the docstring no longer says Cox names the fit on every row.
* The schema never marks a catalogue file the reader reads, a null in an optional key included
The reader took a null for basis, $schema and phonometry_version, and a $schema or a version of any length, all of which the schema refuses. It now refuses a null there as it does under every other key, and holds the two texts to the length of any other text. The schema holds the standards a row's own provenance names to the fields of its class, as it already held the document's. Each refused case is now checked against the place and the first words of the reader's refusal, so a case refused for another reason than its name fails; the one that was, a negative thickness written beside the same cell in another unit, now gives the negative thickness alone. A CSV file's header that names the schema is read as one that does not.
* The oracles of a solid's row in the building and elastic models cite the page each number is printed on
The detailed-model test took 600 kg/m3 for autoclaved aerated concrete from ISO 12354-1 Table B.3, which prints 400 to 800; it now reads the three materials off the Annex L element block (PDF page 84, printed p. 78), which prints the density and velocity of each, and says Annex L takes 600 from that range. The elastic test writes the modulus, Poisson ratio and density Bies Table C.1 prints for mild steel (PDF page 747, printed p. 718) as literals and holds the row's bulk and transverse speeds to their closed forms, rather than reading them back from the row under test. The elastic FDTD's docstrings say a row class is refused when it lacks any of the three cells, not only all of them.
* The catalogues guide cites ISO 12354-1 and JSON Schema 2020-12, and says why an outdoor ground raises a fit-range warning
The guide's references now carry ISO 12354-1:2017, whose Formula (B.2), Table B.3 and Annex L the wall section rests on, and the two JSON Schema 2020-12 drafts the schema is written in. After the ground example it says the call raises a PorousAbsorberWarning because an outdoor ground lies below the fit range of both models, and links to where the ground guide explains it. The catalogues page says the schema covers every row class the library publishes, not only the ones its tables use. The Spanish pages name the elastic FDTD rather than a solver object and call the bulk speed that of an extended solid, as the solids guides do.
* The catalogue schema takes the table's credit a document or a CSV header gives at its top, as one text that is not blank
* The Annex L test holds the element's constants on the left, as the rest of the file does
* The catalogue schema's patterns read as the reader does under every validator
The schema said a filled text with \S, which Python's re matches against
U+FEFF and ECMA-262 does not: a name or a credit that is only U+FEFF was read
by the reader and accepted by Python's jsonschema, and refused by an
editor's validator. The reader tells a blank text by str.strip, so the
schema moves to it: every class shorthand of a pattern the schema takes
from the reader is spelled out as the characters it matches under that
pattern's own flags, the white space str.strip strips for a filled text and
the ASCII letters and digits for a key, a column of your own and a reserved
name. Python's re and ECMA-262, with the u flag and without it, now read
each pattern alike, over every character of the basic plane.
* The catalogue schema's patterns that hold a whole text end where it ends, under Python's re and ECMA-262 alike
The seven patterns of the schema that hold a whole text were anchored with
a closing $, which Python's re and ECMA-262 read apart: jsonschema searches
with re.search, whose $ also matches before a line feed that ends the
text, so Python's validator accepted "k\n" as a row's key, "x-a\n" as a
column of your own, "mine\n" as a catalogue's name, and a day, a date or a
digest followed by a line feed, all of which the reader and an editor's
JavaScript validator refuse. Each of the seven now ends with (?![\s\S]),
which no character follows in either engine, with the u flag or without
it. The other two, the test of a text that is not blank and the reserved
form of a name, match a part of the text and are not anchored at its end,
as a test now holds. A validator built on RE2 compiles no lookahead, and so
none of the seven, which the comments and the entry now say instead of
claiming every validator. The comparison of Python and ECMA-262 covers every
pattern the schema holds, over texts that end in a line feed after a whole
key, day and digest, and the six texts are refused by both the schema and
the reader. The catalogues guide says a file the reader reads is never
marked by an editor whose validator reads regular expressions as JavaScript
or Python does, rather than by any editor.
* A catalogue is read as its reader reads the file before a byte of it is written, so nothing the reader refuses is written
write_catalogue held a document to none of the reader's limits: more than
50 000 rows, a JSON file or a CSV file past 16 MiB, a CSV header past
64 KiB, or a text longer than the reader takes (2 000 characters, 20 000
for an about or a note, from a row built in Python or an about passed in)
was written, and with overwrite=True replaced a good file with one that
read_catalogue then refused. The JSON writer now reads the text it is
about to write as parse_catalogue reads one, its UTF-8 bytes held to the
file's limit first, and the CSV writer reads the sheet and its header as
read_catalogue reads the two files, each held to its own limit first, so
whatever the reader refuses, for its size, its rows, the length of a text
or anything else, is refused before anything is written or replaced, in
the reader's own words and naming the file. The reader looks its size
limits up when it runs, and the CSV reader reads the bytes of the two files
through one function the writer shares. The calibration sidecar writer
already refused a sidecar past the 1 MiB its reader takes, before it
touched the file.
For each writer, a file of exactly the most rows, the most bytes of the
file and of a CSV header, and the longest text and prose the reader takes
is written and read back, and one more row, one more byte or one more
character is refused over it, the good file left byte for byte and nothing
beside it; the limits of rows and bytes are lowered for the test. Each of
the nine fails on the code before this change.
* A code span in the markdown check closes only on a run of backticks as long as its opener, and the size refusals share the name of a catalogue file
CommonMark closes a code span only on a whole run of backticks of the opener's length. The check took one tick of a longer run as the closer, so a dollar sign after an unmatched backtick was read as code and an unclosed maths delimiter went unreported; each run is now read whole. The three size refusals name the file with one constant.
Reach every function through the package that owns it, in code (#592)
* Give plot_excitation the module home the other twenty-three plots have
`plot_excitation` is defined in the private `_plot.room`, the top level
re-exported it, and `phonometry.room` did not, so it was the one of the
twenty-four plotting helpers reachable only through the flat shortcut and
through no module path at all. Every other one is published by the domain that
owns it. A reader following the domain, which is how the documentation teaches
the library, could not find it.
Two architecture rules now hold the invariant. One fails when the top level
publishes a name no domain package does, and names the orphan: reintroducing
this defect makes it fail with `['plot_excitation']`. The other fails when two
domains publish the same spelling, which is what forces a rename at the top
level and what would make a call through a domain ambiguous about the module it
reaches. That second rule was impossible to satisfy while the `environmental`
alias existed, since the same module answered to two names and 170 names had
two owners.
* Free five identifiers the domain packages are about to need
A local variable called `room`, `speech`, `aircraft` or `filters` shadows the
package of that name the moment a file imports it. These five bound one:
`room` for the result of `room_parameters` and for a matplotlib Rectangle,
`speech` for a spectrum, `aircraft` for a fleet entry, `filters` for a list of
filter designs. Each is renamed to what it actually holds, inside the one
function that binds it.
* Reach every function through the package that owns it, in code
The tests, the figure scripts and the conformance runner take their names from
the domain package now: `from phonometry import building` and then
`building.single_panel_transmission_loss(...)`, instead of importing the
function flat off the top level and calling it bare.
This is the form the published guidance points at and the one the
documentation already leads with. Google's Python style guide states it
outright ("use import statements for packages and modules only, not for
individual types, classes, or functions") and gives the reason: `x.Obj` says
where `Obj` is defined. SciPy, which is the library phonometry is shaped like,
recommends the same for its own subpackages. PEP 8 permits both and offers one
tie-breaker, name collision, and that tie-breaker is not hypothetical here:
`coherence`, `envelope`, `group_delay`, `minimum_phase`, `spectrogram` and
`zoom_fft` all exist in `scipy.signal` under the same spelling, `info` exists
in numpy, and every file that imported one of those flat also imports scipy.
The documentation was already paying for it by hand, aliasing the other side
as `from scipy import signal as sp_signal`.
Mechanical, and checked as such: 176 files, no behaviour touched. Ruff and
mypy are clean and the suite passes at 8957. Statements the requalification
pushed past the line limit were reflowed one at a time, and only those; a line
carrying `# type: ignore` was left alone whatever its length, because the
comment binds to the physical line and reflowing the statement would move the
suppression onto a different argument. Where qualifying a callee moved the
open parenthesis its arguments were aligned under, the continuation lines
moved with it.
* Re-stamp the clip fingerprints the rename moved, having proved no pixel did
The clip freshness gate hashes the AST of the code that draws each clip, and
this change rewrote every call site in the figure package, so all forty-two
fingerprints moved. Not one clip can have changed: `signals.leq(x)` and
`leq(x)` reach the same function object, and the five renamed variables are
local.
That is a claim worth checking rather than asserting, and the check is cheap.
Every module of `scripts/figures` was compared against its committed version
after erasing exactly two things: how a phonometry name is spelled (the
`from phonometry import` statements dropped, `<package>.<name>` reduced to
`<name>`, so calling a *different* function would still show) and the five
local renames, as an explicit table so a rename outside it would be a
difference. Forty-six modules, none differing. Whitespace and reflowed calls
are invisible to an AST comparison already.
The fingerprints are stamped from the current sources, which is what a render
would have written, without re-rendering forty-two FDTD simulations to
reproduce frames that cannot differ.
There is a follow-up worth taking separately: the gate stops at the library
boundary on purpose, "to avoid a constant stream of false positives from
refactors that change no pixel", and how a library name is spelled at the call
site is the same kind of thing. Teaching the fingerprint to normalize it would
keep this class of rename from marking every clip stale again. That is a change
to a safety gate and belongs in its own change, not smuggled into the one that
needed it.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
A flag is written by name everywhere, and the clip gate learns which changes cannot move a frame (#760)
#759 closed the flags a caller could reach. This closes the rest, so ruff's
FBT family is selected outright instead of documented as absent: 202
signatures take the star, and the 296 calls that passed a bare True name the
parameter.
Every one of the 156 call sites the change forced was named rather than left
positional. Two of them no static gate sees: _cached_weighting_sos and
_cached_weighting_filter are lru_cache'd, so mypy reads the call through
_lru_cache_wrapper and checks nothing, and only the suite found them.
The one place a keyword-only parameter is not a rewrite: Callable[[...], R]
cannot say that a parameter is keyword-only. ColumnsBuilder in
_report/_insulation_fiche.py and a new LeftCellBuilder in
_report/ansi_s12_2.py become protocols, which say what the alias could not.
FBT003 came down to ax.grid(True, alpha=0.3) in 295 of its 296 sites.
matplotlib declares that parameter visible, so the sites say visible=True and
a test pins the name, because a matplotlib release that moved it would
otherwise surface as a wall of unrelated red. The one that was left is
np.bool_(True) in a test, a cast rather than a flag; it is np.True_ now.
The clips are not re-rendered, and none of them needed to be.
animation_fingerprint already normalises away what cannot move a frame, the
docstrings, the import order, the TYPE_CHECKING blocks, with the reason on
each. Where the keyword-only star sits is the same kind of thing: it decides
how a caller may write the call, not what the function computes. With that
normalisation the 43 fingerprints are identical either side of the signature
sweep, measured, not assumed. The manifest is re-stamped because the
fingerprint itself changed, and the stamp records the same clips it always
described.
scripts/check_public_flags.py goes: ruff now says the same thing on every
definition, and a guard that cannot fire is noise.
Give the tree a formatter, and a gate that keeps it (#602)
* Give the tree a formatter, and a gate that keeps it
Ruff was the linter and nothing was the formatter. There was no
`[tool.ruff]`, no `ruff.toml`, no pre-commit and no `.editorconfig`, and CI
ran `ruff check .` on ruff's default rules alone, which are four families of
outright errors. Nothing looked at shape, so 673 of the 812 Python files had
drifted from any single style.
The line length is 88 because that is what the tooling defaults to and,
measured, what moves this tree least: 100 rewrites 708 files and 120 rewrites
717, since a longer limit rejoins lines that were split by hand. Only 0,8 %
of the lines here were over 88 to begin with.
Markdown stays out of it. Ruff formats fenced Python inside `.md` and does
not recognise `.mdx` at all, and every snippet in `docs/` is mirrored by hand
into the site in English and Spanish, so formatting one copy would drift the
three apart with nothing to notice. Those snippets also align their trailing
comments to show what each line prints, which is a teaching device.
The linter now selects the families this tree already satisfied or nearly
did, and the fifty-odd findings they raised are fixed. Two of those fixes are
worth naming because the formatter caused them: reflowing a call moves a
`# type: ignore` onto a line that no longer holds the error it silenced, and
mypy caught four such cases. One rename is reverted with the reason written
down, because renaming an unused binding moves the fingerprint the clip
freshness gate reads, and that is not worth re-rendering an FDTD animation.
* Keep the reformat out of git blame
A tree-wide reformat stands in front of every line it touched. This file
names it so `git blame` walks past, which GitHub honours without any
local configuration and which `git config blame.ignoreRevsFile` turns on
for a checkout.
* Stop selecting the rule that reads a constant as a literal
SIM300 calls any upper-case name a literal, and on that reading it gets
three idioms in this tree backwards.
`spectrum[OCTAVE_BANDS == f]` is a numpy mask, not a condition.
`res.transmission_loss[BANDS <= f0]` reads "the bands at or below f0" until
it becomes `f0 >= BANDS`. And `value == pytest.approx(x)` is how pytest is
written here: 1755 of the 2065 `pytest.raises`-adjacent assertions in the
suite already put the expected value on the right.
It is right about two of the nineteen it finds and wrong about the other
seventeen, and SonarCloud caught the result as `python:S3415`, assertions
whose argument order no longer agrees within a file. The nineteen are
reverted by rebuilding those files as they would have been had the rule
never been selected, which keeps the `C420` and formatting changes two of
them also carry.
Write the name in full outside bibliographic citations (#453)
* Write the name in full outside bibliographic citations
The hyphen is a citation form, not a spelling of the name: it stays in the APA
and BibTeX blocks, CITATION.cff and .zenodo.json, and the ORCID record and the
canonical Person document both hold the unhyphenated name. Everywhere the name
is a name it now reads José Manuel Requena Plens: the LICENSE copyright line,
the authors field of both pyproject files, and a documentation example.
The copyright header of every file under src/, scripts/ and tests/ takes the
same name without the accent, because a source file is where a mangled byte
does real damage; the generator in scripts/bench.py that writes that header was
updated too, or it would reintroduce the old form on the next run.
* Sweep the name form through the modules that landed today
Twenty new source files arrived with the superseded header while this branch
waited its turn, which is why it went last, and twelve report fiches carried
the citation form in their operator field, where the name is a name rather than
a citation of the work.
* Repair the duplicated import keyword that broke the docs build
Four guide pages carried 'import import ThemeImage', which MDX hands to acorn
and acorn rejects, so the site build failed on the first of them and the docs
deploy never produced a page. The pull-request pipeline did not catch it: the
figures, conformance and test jobs do not build the site, and the docs workflow
that does only runs on main.
Fixed in all four, English and Spanish, and the build now completes with the
math gate clean.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Reach a path through Path, and build a list by saying so (#611)
`PTH` and `PERF` are selected. The 193 sites they found were read one at a
time, because the two spellings of a path are not the same type: a `Path`
prints like its string and compares unequal to it, so the sites worth reading
were the ones that hand their value to a sort, a comparison or a dictionary
key. One did. The CI comment builder sorted a list of artifact paths, and
sorting `Path` objects compares them component by component rather than as
text, which reorders the table whenever one matrix name is a prefix of
another, as `3.13` is of `3.13-dev`.
Half of what the rules found was one pair of lines copied fifty-four times.
Every fiche test carried its own `_assert_one_page` and its own `_PDF_MAGIC`,
in eight spellings, and forty-six of those also asserted a non-zero file size,
which could not fail: finding `%PDF` in the first four bytes already proves
the file holds four bytes. That assertion lives once now.
Fifteen sites keep the older spelling behind a `noqa` that says why: they draw
the animations, and that code is hashed, so rewriting a line there asks for a
re-render of clips that would come back frame for frame identical.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
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.
The filter bank returns a result, not a tuple whose length depends on a keyword (#810)
* The filter bank returns a result, not a tuple whose length depends on a keyword
octave_filter() and OctaveFilterBank.filter() returned two items normally and
three when sigbands=True asked for the band waveforms. That is why the pair
carried twelve @overload declarations to be typable at all, and why
`_, _, bands = bank.filter(...)` was a line a reader had to count commas in.
Both return an OctaveFilterResult now, with levels, frequencies and bands, the
way every other computation in this library already did. require_levels() and
require_bands() return the value or raise naming the argument that was missing,
which beats the None a caller would otherwise meet several lines later. Like
every other result it draws itself, and the band spectrum is in the guide.
* Every snippet that mentions the filter bank runs against the new return
Four the snippet gate caught. The rewriter that converted the guides appended
the accessor to the end of the line rather than the end of the call, so a
trailing comment swallowed it and the variable kept the whole result;
README_PYPI.md is a separate file neither rewriter walked; the snippet that
demonstrates the missing-bands error raised it for real, and now catches it and
prints the message; and the header snippet of llms.txt is written by hand
inside generate_llms.py, where no documentation sweep would ever find it.
* The nominal labels carry the decimal comma, and the stale clip is redrawn
The renderer sets its own tick labels for a nominal spectrum, and
set_xticklabels installs fixed strings that localize_axes never reaches, so a
Spanish nominal plot showed 31.5 where the rest of the corpus writes 31,5. That
was the one way round the decimal-point gate, which reads the generated SVGs.
The bands docstring conflated the type that is preserved with the units it is
in: an uncalibrated Signal comes back as a Signal carrying None, a calibrated
one as a Signal carrying 1.0, and a dBFS bank hands back plain arrays.
One test indexed the result rather than unpacking it, which the AST sweep did
not see because it only looked at tuple targets; a second sweep for subscripts
found no others anywhere in the tree. anim_modulation_transfer is redrawn
because the code that draws it changed.
* The per-channel legend entry is a named constant, shared by the two renderers that use it
* The stateful block-processing snippets read bands through the accessor too
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
The filter bank returns a result, not a tuple whose length depends on a keyword (#810)
* The filter bank returns a result, not a tuple whose length depends on a keyword
octave_filter() and OctaveFilterBank.filter() returned two items normally and
three when sigbands=True asked for the band waveforms. That is why the pair
carried twelve @overload declarations to be typable at all, and why
`_, _, bands = bank.filter(...)` was a line a reader had to count commas in.
Both return an OctaveFilterResult now, with levels, frequencies and bands, the
way every other computation in this library already did. require_levels() and
require_bands() return the value or raise naming the argument that was missing,
which beats the None a caller would otherwise meet several lines later. Like
every other result it draws itself, and the band spectrum is in the guide.
* Every snippet that mentions the filter bank runs against the new return
Four the snippet gate caught. The rewriter that converted the guides appended
the accessor to the end of the line rather than the end of the call, so a
trailing comment swallowed it and the variable kept the whole result;
README_PYPI.md is a separate file neither rewriter walked; the snippet that
demonstrates the missing-bands error raised it for real, and now catches it and
prints the message; and the header snippet of llms.txt is written by hand
inside generate_llms.py, where no documentation sweep would ever find it.
* The nominal labels carry the decimal comma, and the stale clip is redrawn
The renderer sets its own tick labels for a nominal spectrum, and
set_xticklabels installs fixed strings that localize_axes never reaches, so a
Spanish nominal plot showed 31.5 where the rest of the corpus writes 31,5. That
was the one way round the decimal-point gate, which reads the generated SVGs.
The bands docstring conflated the type that is preserved with the units it is
in: an uncalibrated Signal comes back as a Signal carrying None, a calibrated
one as a Signal carrying 1.0, and a dBFS bank hands back plain arrays.
One test indexed the result rather than unpacking it, which the AST sweep did
not see because it only looked at tuple targets; a second sweep for subscripts
found no others anywhere in the tree. anim_modulation_transfer is redrawn
because the code that draws it changed.
* The per-channel legend entry is a named constant, shared by the two renderers that use it
* The stateful block-processing snippets read bands through the accessor too
The filter bank returns a result, not a tuple whose length depends on a keyword (#810)
* The filter bank returns a result, not a tuple whose length depends on a keyword
octave_filter() and OctaveFilterBank.filter() returned two items normally and
three when sigbands=True asked for the band waveforms. That is why the pair
carried twelve @overload declarations to be typable at all, and why
`_, _, bands = bank.filter(...)` was a line a reader had to count commas in.
Both return an OctaveFilterResult now, with levels, frequencies and bands, the
way every other computation in this library already did. require_levels() and
require_bands() return the value or raise naming the argument that was missing,
which beats the None a caller would otherwise meet several lines later. Like
every other result it draws itself, and the band spectrum is in the guide.
* Every snippet that mentions the filter bank runs against the new return
Four the snippet gate caught. The rewriter that converted the guides appended
the accessor to the end of the line rather than the end of the call, so a
trailing comment swallowed it and the variable kept the whole result;
README_PYPI.md is a separate file neither rewriter walked; the snippet that
demonstrates the missing-bands error raised it for real, and now catches it and
prints the message; and the header snippet of llms.txt is written by hand
inside generate_llms.py, where no documentation sweep would ever find it.
* The nominal labels carry the decimal comma, and the stale clip is redrawn
The renderer sets its own tick labels for a nominal spectrum, and
set_xticklabels installs fixed strings that localize_axes never reaches, so a
Spanish nominal plot showed 31.5 where the rest of the corpus writes 31,5. That
was the one way round the decimal-point gate, which reads the generated SVGs.
The bands docstring conflated the type that is preserved with the units it is
in: an uncalibrated Signal comes back as a Signal carrying None, a calibrated
one as a Signal carrying 1.0, and a dBFS bank hands back plain arrays.
One test indexed the result rather than unpacking it, which the AST sweep did
not see because it only looked at tuple targets; a second sweep for subscripts
found no others anywhere in the tree. anim_modulation_transfer is redrawn
because the code that draws it changed.
* The per-channel legend entry is a named constant, shared by the two renderers that use it
* The stateful block-processing snippets read bands through the accessor too
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Run the examples GitHub serves, and check the table's kind column (#715)
Two gates that pass while the thing they guard is wrong.
The 835 code blocks of the docs/ mirror were never executed. The site
pages run in reading order; the mirror was read for name shadowing only,
on the grounds that it is hand-written for GitHub and does not carry the
same examples. Both halves are true, and 105 of its 171 pages do carry
different code, which is the reason to run it rather than the reason not
to: it is the copy a GitHub reader pastes. It found three published
examples that do not work. One scored a 40000-sample signal against a
48000-sample one, having taken clean and degraded from two sections at
different rates. One called vibration.RISK_THRESHOLDS_MALE, which the
library does not publish. And the namespace example in the API quick
reference passed five names nothing had defined, so it is written as the
sketch it is. A skip entry names a guide, and a guide is written twice, so
it is stale only once both editions run.
The curated API table's kind column said the wrong thing for twenty-one
names: eight coefficient tables, a tuple and an array were "function", so
were seven result dataclasses and a warning class, and two functions were
"dataclass". A reader following that column writes code that cannot run.
The coverage gate now reads the column and asks the object, with a
vocabulary as wide as the table's own, so a row passes on any word that is
true of what it names.
Every verdict reads its pass and fail flags, grades and classes from the values it holds (#949)
A result no longer stores a verdict beside the readings it came from. Sixty-two result classes used to keep the flag, grade or class their function had worked out (`passes`, `complies`, `qualified`, the `*_met` and `*_ok` flags, the advisories, the per-band classes of the IEC 61260-1, IEC 61672-1 and IEC 61043 verdicts, the ISO 9614-2 grade, the ISO/PAS 1996-3 category, the IEC TS 61400-11-2 block status and the rest), so a result built by hand or rewritten with `dataclasses.replace` could hold readings that fail and a flag that says they pass. Each of them is now a read-only property, read from the values the result keeps and the limit the standard prints, and so is any figure that only fed it, such as the rounded level of an RD 1367/2007 period or the acceptance region of a trend test.
Fit each weighting at its own sample rate instead of oversampling around it (#646)
* Fit each weighting at its own sample rate instead of oversampling around it
The frequency weightings were realised by bilinear-transforming the printed
prototype at 3 to 8 times the sample rate and resampling around the result. The
transform's frequency compression is what forced the oversampling, and the
resampling then cost more than the compression it was bought to avoid: its
anti-alias filter sits with its cutoff on the input Nyquist, the signal crosses
it twice, and that is a fixed 1.65 dB at 20 kHz for a 44.1 kHz input however
high the factor goes.
Fitting the prototype at the sample rate removes both. Worst error over the
corpus goes from 18.4 dB to 0.060 dB, and A-weighting at 44.1 kHz from 2.24 dB
out at 20 kHz to 0.002. It also runs 25 to 50 times faster with an eighth of
the memory, because there is no longer an oversampled intermediate: the whole
path is one cascade of second-order sections and nothing else.
The deliverable is the design routine, not coefficients. Every parameter is a
frequency, each factor is Routh-Hurwitz stable by construction so no step can
leave the unit disc, and the residual is in decibels so the Jacobian stays
bounded. The result is bit-identical across processes and BLAS thread counts,
and a test re-derives it rather than pinning an array.
Three published tolerances that used to fail now pass, one of which nothing in
the tree documented: AU at 32 kHz was 13.3 dB outside IEC 61012's only mask and
verified to no class at all. A and C now earn IEC 61672-1 class 1 at every rate
the library supports, where they fell to class 2 at 32 kHz and to no class at
16 kHz.
Also here, because they arrived with it:
The conformance harness emits docs/conformance.json, and the report, the site
page, the Spanish twin, the quoted counts and the PR comment all derive from
it. They used to derive from the rendered markdown: three separate regexes
parsed the report's English headline as a wire format, each telling the reader
to go and update the other two. The published "369 standards" counted citation
strings rather than documents, so one book cited at two equations counted
twice; the artefact separates 114 normative designations from 84 further
sources. Outcome.passed was a numpy.bool_ on nine checks, which no consumer
noticed because the only use was an if.
IEC 651:1979 Table V brings the Type 0 laboratory class to the weightings, the
way IEC 61260:1995 brings class 0 to the band filters. It is worth having for a
reason beyond completeness: class 1 is +2.5/-16 dB at 16 kHz and +3/-inf at
20 kHz, so it cannot see an error at the top of the band, which is exactly
where the old design was wrong. Type 0 is +2/-3 at both. A and C pass it using
seven per cent of the budget.
* Take the seven review findings, and refuse the one that would open a hole
Six are what they look like. The pair matcher closed over its loop variable,
which is a hazard even where the closure is consumed before the next iteration,
so the distances are materialised and the lambda goes; four raises blocks
carried their fixture construction inside, so a failure there would have
satisfied a test about something else; and the determinism check on the
markdown renderer compared one expression against itself, which does call
twice and does test what it claims, but reads as a tautology.
The seventh is refused. The band guard is written `not low < high` and the
suggestion is `low >= high`. Those differ on NaN, which the negated comparison
refuses and the inequality admits, and a band of NaN reaching the fit is how a
silent wrong answer starts. It is spelled out with isfinite instead, which is
what the guard meant all along.
Refuse a conformance row that never runs the library (#710)
A row states what a standard prints and what the library computes. A row
that works out the expected value itself, from the same constants, is
comparing a formula with a second copy of it: the three VDI 2081 rows
found last week reported Pass whatever the function they certified did.
Counting them is the hard half. A row delegates, to a helper beside it, to
one imported from ..shared, or to the library under a name that is not ph,
so reading each row's own body reports 102 rows here of which 95 are fine.
Resolving the delegation reports seven, and six of those are legitimately
about something other than this library: the test-case geometry of
ISO/TR 17534-3, a printed total against the printed parts it sums, SciPy's
DPSS eigenvalue. Each says so in prose in ORACLE_ONLY, and an entry that
stops being needed fails the gate as loudly as a row missing one.
The seventh is now two rows. ARP 5534 splits the SAE Method at a mid-band
attenuation of 150 dB; the function takes a path length, so the split is
reached by scaling the path until the attenuation lands on it, and each
branch is judged against the other branch's printed formula.
A citation that names three documents is filed under three documents (#778)
* A citation that names three documents is filed under three documents
The reader turned a citation into one document and left whatever else it
named inside that document's clause. `ISO 16283-1:2014 Clause 8.1 /
-2:2020 Clause 8.1 / -3:2016 Clause 7.3.1` was recorded as part 1, and
the same reading put ANSI S1.11 inside a clause of IEC 61260, BS 5969
inside one of IEC 651, and the NASA table the numbers were actually read
from inside one of a withdrawn IEC 537. Nothing caught it, because the
only verification was that the split rebuilds the citation string, and a
second document sitting in the clause rebuilds perfectly.
A citation now carries every document it names, `reference.documents`,
in written order, each with the connector that introduced it and what
that connector says: corroborates, compares, via, mentions. There is no
headline document left to read, which is the point of the shape: four
artefact invariants and the gate itself looked only at the first
document, and a consumer that does not iterate now fails instead of
quietly reporting one of three. Each document has to account for its own
span of the string, so the rebuild is stricter than the one it replaces,
and a connector is a split only where what follows it opens like a
document, which leaves `ISO 7196:1995 Table 2 / A.3` and `Normal modes
vs ideal waveguide` naming one thing each.
Three series the reader cut in the wrong place come with it: `JIS A
1418-2` and `EBU R 98` by their prefixes, and `NASA CR-3406` by
anchoring an edition to a century, since a document number and a year
are written the same way. That anchor moves no other citation. A
citation may also write a work by the name a reader would know it by and
leave the rest of the title off, as `(NORAH2 Eq. 8)` does for the NORAH2
guidance, and that shorthand is expanded the way `-3` is: the record
names the document, the citation goes on writing what it wrote.
What sort of document a named work is is declared in one place with it.
The rules that read a work from its shape alone disagreed with each
other, so Mackenzie was an article where a citation wrote the year and a
book where it did not, Ainslie and Hopkins and Vigran were books read as
articles, and the NORAH2 guidance was a book to the parser and a report
to the file that records what the parser cannot. One work, one kind, and
the gate fails naming the citation that disagrees.
50 citations name more than one document, across 58 checks, and the
other 721 parse to exactly what they parsed to before. Distinct
normative designations go from 161 to 170 and further sources from 85 to
88, three of which were never works at all but a book, a separator and a
second author read as one name. `counts.standards` is 424 before and
after, so the banner, the badges, `llms.txt`, both READMEs and
`.zenodo.json` do not move, and no check id changes. The artefact is
schema 2, because `designations` and `sources` now mean documents named
anywhere in a citation rather than documents that open one.
Four tests hold the class shut. The net asks of every document of every
check that no clause and no designation carry a body with a document
number, a sibling part, an author with a date, a connector, or any
designation the artefact itself records for another citation, whole or
written the short way in front of a place in it. The list of works the
reader knows by name is a ratchet, counting only the heads that list
could have produced. The kind agreement is the gate check above. And the
Markdown report's own fixture has to be a document the gate accepts, so
it cannot go on declaring the shape of a release ago.
* A field the artefact leaves out is never written as null, and the gate says so
The builder writes an empty tail, a first document's lead and every other field a check does not have as None, then drops each null-valued key before the file is written, so docs/conformance.json carries no null at all. The site reads it through a schema that declares those fields optional and none of them nullable, which is right for the file as written, but nothing on the Python side held the file to it: the gate accepted a document with nulls in it, and the renderer fixture that is asserted to be a document the gate accepts carried twelve. A null that got past the builder would have failed first in the documentation build.
The gate now reports every null by its path, the fixture is written the way the artefact is, and a test pins both ends: a citation with nothing after its last document has no tail key, one that closes on a bracket keeps it, and a null tail or lead is named by the gate.
The same test file compared a tuple of what the reader read against the expected split in one place and the other way round everywhere else, which is the order it now uses throughout.
* The null guard is pinned through the gate that calls it, not only through its helper
* A null where the gate expects a list is named, not raised on
* Every relation the reader produces is accepted by the site and worded in both languages
Reading a citation as a list of documents gave the relation vocabulary a
fifth value, `supplies`, for the `with` and `and` that introduce a document
the check runs the clause with. Ten rows carry it. The Zod enum in
`site/src/content.config.ts` still listed the other four, so the artefact was
refused on its way into the content collection and the site build stopped with
an `InvalidContentEntryDataError` naming ten checks and no cause; and neither
label map in `Conformance.astro` had a word for it, so the Spanish table would
have printed `supplies` beside `corroborado por` and `comparado con`.
The schema now accepts all five, and the page words the new one `run with` in
English and `resuelto con` in Spanish: the citation's connector says the check
runs the clause with what that document prints, an input it supplies or a rule
it states, which is what ISO 11957 clause 8 does with the rating rule of
ISO 717-1 and what EN 16487 4.2.1 does with the air absorption of ISO 9613-1.
The two vocabularies drifted because nothing compared them.
`scripts/check_conformance_vocabulary.py` compares the five closed
vocabularies of the artefact (verdict, comparison shape, tolerance mode,
document kind, relation) across the three places that state them: the Python
enums that produce them, the Zod enums that validate them into the site, and
the label maps that word two of them in each language. It fails both ways, on
a value the site does not accept or word and on one the site still carries
after nothing produces it, and it reads the artefact too, so a hand-edited
value fails as well. `make conformance` runs it last, beside the artefact
gate, and the conformance job runs it in CI, which is cheaper than waiting for
the site build to say a row is invalid.
* The release note says which words the conformance table prints beside each document
The entry lists the five relations the reader produces and stops there. A
reader of the release notes cannot tell from it that the page prints one of
those words in front of every document a citation names, in English and in
Spanish, nor that the vocabulary is now held to the enums by a gate instead of
by the site build refusing every row that carries a value its schema does not
accept.
So the entry now words each connector in both languages, says that a qualifier
written inside a bracket is printed as the citation writes it, and names
`scripts/check_conformance_vocabulary.py` as what compares the artefact's five
closed vocabularies with the two places the site restates them, both ways.
* A value only the committed artefact carries is a hand edit, and a test says so
Two of the gate's tests read the drift one way round, with the site behind the
enums. The third direction it checks, a value in `docs/conformance.json` that
no enum defines, had no test of its own, and deleting that comparison left the
suite green. The artefact is generated, so the only way such a value gets in is
by hand, which is precisely when nothing else is looking.
* A work is read by the name the citation writes, not by the name it is filed under
The kind table is keyed by the designation a work is recorded under, and the splitters read it with the name the citation writes. For five of the six declared works those are the same string, so the miss went unseen. For the sixth they are not: the corpus files the NORAH2 report as "NORAH2 guidance", so "NORAH2 (2015) Eq. 8" came back as an article called "NORAH2", a kind the record contradicts and a designation nothing else in the corpus cites. The same name in front of a bare clause took the other splitter and came back a book.
The lookup now goes through the work record by the written name, which carries both the declared kind and the expansion to the full designation, exactly as the splitter for a bare work name already did. The three shapes that read a work name are all served by it, so a year, an edition mark and a plain clause now land on the same document. No citation in the corpus is written in the shape that was wrong, so the artefact does not move; the test covers all four shapes so it cannot come back.
The vocabulary gate's page test built its rewritten page inside the block that expects the failure, which leaves two calls that could raise where the test means one.
* The vocabulary gate says one row carries a value, not carry it
The cost line put the noun in the plural and left the verb alone, so a value on a single row of the artefact was reported as "1 row of the artefact carry it". Both now agree with the count, and a test holds the three cases.
* The conformance artefact is regenerated on top of the ISO 4866 re-citation
The same D.3 to D.2 re-citation that main brought in, carried through this layer's artefact, which the move onto main had kept as this layer wrote it.
Answer the audit: three defects in the code, one class of invisible byte, and sixteen claims the page does not support (#705)
* Grade the run, refuse the control character, ask only for what is used
Three things the audit found in the code.
industrial_machine_zone wrapped a function that has always taken an array and then applied the most-restrictive rule of 5.2.3 by comparing the two gradings as text. With an array in, that text was the array's repr, so a year of monthly readings came back as one eighteen-character string. The rule is applied to zone indices now and the letters looked up at the end, and a new figure draws what the array is for: twelve readings walking from zone A into zone D, with the month Criterion II sees before Criterion I does.
A form feed had reached the change-of-section docstring, and in a raw string that is exactly the backslash and the f of the command after it, so the published formula lost its fraction. check_control_characters.py refuses the whole class over the tracked text corpus, with an escape hatch for the two files that are copies of somebody else's and a ratchet on it.
section_change_loss demanded a rectangular duct's largest side whatever the ratio, and for a sudden reduction threw it away: Figure 26 prints no effect in the frequency column there, so no limit frequency and no dimension enter the answer. It is asked for now only where it is used. A round duct's diameter and its area are one fact stated twice, and a contradictory pair silently moved the limit frequency by a decade instead of being refused.
* Say what the printed page says, in both languages
Ten statements across the guides, the register and the module comments that the printed page does not support.
Clause numbers: the measurement band of ISO 10816-3 is stated in an unnumbered Annex A, which has no A.1; the 50 MW cap on group 1 is in the title of Table A.1 and not in clause 4.2; VDI 2081's duct attenuation uses Tables 5 and 7, Table 8 being the sound reduction index of building components; the spectral parameter and the blade-frequency allowance are printed in Section 4.3.4, not 4.3.3; and the Table C.1 ladder steps by 1,6, which is R5 and not R10.
Numbers: the fan caption said eight decibels between a radial machine and an axial one where its own figure prints 94 and 106; the ISO/TR 17534-3 cases are 194,16 m apart in ground projection, not 195; and the register holds five VDI 2081 defects, not four, the fifth being the cross-reference that sends a reader to Section 7.3.2 for coefficients printed in 7.2.3.2.
Descriptions: the Section 6.4 mistranslation is about whether a junction's reduction depends on frequency at all, not about which way a duct's attenuation runs with it; and the note about the three L_WSM values sitting inside their stated tolerance holds for two of the three, assembly T being inside the English tolerance and outside the normative German one.
The ISO/TR 17534-3 errata entry gets its Status line back in both languages, taken by the two VDI entries inserted above it. And one term, one word, on the Spanish pages: margen for the tolerance band and referencia for the established baseline, each of which the same page had been saying two ways.
* Name the parameter this signature has when it refuses a rating
gear_housing_velocity_limit passed the rating through to allowable_velocity, so a bad one surfaced under the delegate's 'constant_velocity_mm_s': a keyword the caller cannot see and no pytest.raises(match=) against this API can match. Validated locally now, the way its sibling already was.
* Carry the restored errata line into the pages the register feeds
* Count the letters the figure actually draws
* Let the section-change rows describe a duct that exists
The nine Figure 26 abscissae were checked through a round duct given both an area and a 0,4 m diameter that does not follow from it, which the consistency check now refuses. The diameter comes from the area, which is what keeps the 63 Hz octave below the limit frequency at every ratio the chart is drawn over.
* Write the fixture bytes the test means, on Windows too
path.write_text translates a newline into a carriage return and a newline on Windows, so a fixture written to carry one control character carried two there and the two tests that fix a position failed on both Windows runners. newline='' turns the translation off.
* Name a file the same way on every platform
The reports and the keys of the escape hatch were built with str() on a path, which gives backslashes on Windows and matches no key written with forward slashes. Both exempt files were therefore reported there and their entries called stale in the same run, which is what the Windows job caught. Named through as_posix() now, and the test that held the hatch against the tree asks what it can answer anywhere: that every entry names a file the scan reads.
* Take the corrections into the figure and the printed outputs
The quality-assurance figure still drew the 195 m the pages had stopped saying; it is 194,16 m in both languages now. The propagation snippet printed 96 and -1 at 8 kHz where it returns 95,8 and -0,8. The trend snippet imported numpy and used a name it never imported. And the Spanish errata bullet called Section 7.3 sound attenuation, where it is the airborne sound insulation of a building component.
* Attribute each claim to the clause that carries it
Section 7.6 of Blatt 1 is the vibration isolation the page called a different guideline. Equation (35) is written on the cross-section ratio, not on the flow, which is why split_loss takes areas. The three gear ratings are named under Table 5 and defined in Annex A; Table 1 gives their units and nothing else. The CNOSSOS cross-references still pointed at section 3, which is the quality-assurance section now, and the comparison they mean is in section 4. And the chapters the successors added were credited to a 2019 revision on a page that says twice the successors are the 2022 editions.
* Keep the 2019 revision, which is a real edition
The refuters were right: VDI 2081 Blatt 1:2019-03 exists, sits between the held 2001-07 and the 2022-04 successor, and is the revision that added the air-handling-unit and outdoor-propagation chapters. The sentence was correct and only its second half needed the Section 7.6 fix. The changelog also counted nine Spanish figures where this branch repoints eight, the ninth having travelled with the gear-unit work; it says which now.
* Read the corpus the gate says it reads, and let a lost Status be heard
The control-character gate named the text suffixes and read those, so it never opened twenty-nine tracked files, seven of which carry the very character it exists for: the CNOSSOS extracts are CRLF as the workbook and the Official Journal produced them. It names the binary suffixes now and reads everything else, 1807 files instead of 1778, and its scope test asks something that can fail: that nothing it skips is text.
The errata gate recognised a record by its Status bullet, so a record that lost one was not reported, it was dropped, and its page citation and ratio claims stopped being checked with it. That is exactly how the ISO/TR 17534-3 line went missing this morning. A record is recognised by its Location now and a missing Status is an error; the sweep found one more, the ISO 3747 E.4.2.3 entry, in both registers. The Spanish register is read too, and has to hold the same records as the English one.
* Answer the review: what Criterion II reads, and the cap that can be refused
Criterion II does not read the zone, it reads the change from the established baseline, and a machine can be well inside zone B and still owe an investigation, which the caption under the figure already said and the paragraph above it did not. The Spanish 'al margen del propio documento' reads as 'apart from the document'; it is 'dentro del margen'. And section_change_loss refuses a cap that is not positive, which its raises clause did not say.
* Recognise a record by either bullet, not only by its Location
Keying recognition on the Location alone made the gate's own fixtures invisible: they carry an Evidence and a Status and no Location, which is the shape a test probe wants. Either bullet marks a record now, so a section missing one is reported rather than dropped, whichever one it is.
Read every private constant, or stop keeping it (#704)
* Read every private constant, or stop keeping it
A private module-level constant is a number lifted out of a standard so the code that uses it can say where it came from. Nineteen of them were read by nothing, and the sweep was worth doing for the two that were not simply leftovers.
The docstring of dynamic_range promised that fundamental=None uses the 997 Hz the standard prescribes, and the code reads the fundamental off the captured spectrum. Detecting it is the better behaviour, because a capture that drifted is still notched where its fundamental actually is, so the docstring is what changes.
Clause 6.2 of ISO 9053-2 recommends a specimen flow velocity as well as a piston frequency, and only the frequency was checked. The velocity follows from the piston area and the specimen area and the function is handed neither, so it cannot check it; the recommendation moves into the docstring with that reason, which is more use to a reader than a constant nobody reads.
One more was a comment pointing the wrong way: ANNEX_A_AIR said it was built from the five values ISO 9053-2 Annex A.3 prints, and it is built from the IEC 61094-2 values at full precision. Two of the printed five are not air at all, which docs/ERRATA.md shows, so the comment invited exactly the wrong conclusion.
The rest were duplicates: the same absolute zero in two modules beside the shared validator that already enforces it, a reference speed in feet per second beside the metres-per-second copy that is used, two axis labels the underwater renderer already keeps its own copies of, a private onset-rate limit beside the public one in the domain module.
A guard goes with it, since a sweep does not stop the class coming back: scripts/check_dead_constants.py walks src for private upper-case constants and the whole tree for anything that reads one, counting names, attributes, import aliases and strings so getattr and __all__ both count. Its escape hatch starts empty and an entry has to carry its reason.
* Regenerate the two API pages the docstrings moved
* Resolve each read to the module that defines it, which found five more
The scan counted reads by bare name across the whole tree, so a private name read in one module cleared an unread namesake in another. That is not hypothetical, it is how a duplicated absolute zero and a duplicated model list both survived until they happened to be dead in every copy at once.
Reads are resolved per defining module now: a bare name counts only inside its own file, a from-import counts as a (module, name) pair with relative imports climbed to the importing file's package, and only attributes and strings stay credited to every definition of a name, which is the generous half the scan needs so that getattr and __all__ do not raise false alarms.
Five more came out of it, each a name defined twice where only one copy is read: an absolute zero in the absorption module, a minimum node count and a band count whose live namesakes are elsewhere, an air density left behind when that model moved to taking a Fluid, and a nominal band tuple beside the table its own validator reads directly.
Also the wording of the clause 6.2 note, which called the unchecked velocity range 'that half of it' where it meant the recommendation.
* Leave the escape hatch usable, which it was not
KEPT was folded into the deadness test, so an entry excluded its name from the dead set, the subtraction handed it back as live, and the run named it stale. Nothing could ever be kept. Deadness is settled first now and the hatch applied to the answer, with the four cases fixed in tests.
* Say what the sweep actually found, in the two places that said otherwise
Both the guard's docstring and its CI job named a narrowest-span rule that existed only as a comment. It does not: the rule is enforced in the report layer, and the constant beside it was a duplicate. What the sweep found was a recommended specimen velocity carried and never checked.
* Close the rest of what the audit found in the guard and the leftovers
The guard: a relative import in an __init__.py was credited one package too high, because a package's __init__ is its own package and the code stripped a component anyway; a constant assigned under a module-level if, try or with, or unpacked from a tuple, was never registered as defined at all, which is twenty-one of them here; and the escape hatch was keyed by the bare name, so one entry would have silenced four modules.
The leftovers: three comments outlived what they described. One still sent a reader to five constants the same sweep deleted, one had lost its constant and had merged into the doc comment of the next, and one documented nothing at all.
A long number cannot be split by a line break any more (#799)
ISO 80000-1 and the SI Brochure group long numbers in threes on both sides of the decimal marker, and the standards this library reads print them that way, so the corpus follows: 6,251 5, 0,647 829, 101 325. Every one of those groupings was held by an ordinary space, which means a line break could fall inside the number and leave 6,251 at the end of one line and 5 at the start of the next, where it reads as two numbers.
The 1 268 groupings in the published prose now use U+202F, the narrow no-break space. It is in the font the figures are drawn with and in the web fonts the site loads, and it survives the plain-Markdown twins and llms.txt unchanged.
`scripts/check_digit_grouping.py` keeps it that way, and most of its work is knowing what is not a grouping. Two shapes in this tree look exactly like one: the printed output of an array, where the space separates two values, and a value followed by its unit, where the digit belongs to the unit. A first pass took both for groupings and corrupted them, so the gate carries the closing conditions that tell them apart and the tests pin both.
Three places are left alone on purpose. A figure or diagram label, because the defect cannot occur there: the label is one line of drawn text in an SVG and SVG text does not reflow. A code fence or an inline code span, because a literal is quoted as it is written and a snippet has to be copyable. And every Python string that is not a docstring or a comment, because a string may be data: the CNOSSOS traction table is keyed by names such as "diesel locomotive, c. 2 200 kW" that a caller passes in, and the first pass put an invisible character inside that key and broke the lookup. The conformance run caught it, which is the third time this week a generated artefact has been the thing that noticed.
The criterion is written down in CONTRIBUTING beside the other typography rules.
Check an outdoor sound program against its certified results, and find the quantiles of its level differences, by ISO 17534-1 (#904)
A noise map comes from a program, often run in a faster configuration than its method describes, and ISO 17534-1:2015 says how that program shows it implements the method and what the shortcut costs. The new module phonometry.environment.propagation.software_quality fills the TRC form of Annex B against the limits of each certified result, reads the 0,1- and 0,9-quantiles of the level differences of Annex C by Table C.1 and Formulas (C.1) and (C.2), places the sample points of C.2 and C.3, and gives the precision of a method across programs from a round robin, each result with its .plot(). A guide in English and Spanish runs the TRC form on the library's own ISO 9613-2 chain with test case T03 of ISO/TR 17534-3, and the errata registry gains Table 69 of that report.
Check an outdoor sound program against its certified results, and find the quantiles of its level differences, by ISO 17534-1 (#904)
A noise map comes from a program, often run in a faster configuration than its method describes, and ISO 17534-1:2015 says how that program shows it implements the method and what the shortcut costs. The new module phonometry.environment.propagation.software_quality fills the TRC form of Annex B against the limits of each certified result, reads the 0,1- and 0,9-quantiles of the level differences of Annex C by Table C.1 and Formulas (C.1) and (C.2), places the sample points of C.2 and C.3, and gives the precision of a method across programs from a round robin, each result with its .plot(). A guide in English and Spanish runs the TRC form on the library's own ISO 9613-2 chain with test case T03 of ISO/TR 17534-3, and the errata registry gains Table 69 of that report.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
A Spanish figure writes its decimal comma on every panel it draws, and two guards keep it that way (#777)
Every helper that writes text takes a language and defaults to English, so a call that forgets to pass it on raises nothing and puts an English decimal point into a Spanish figure. The plotting modules, the figure generators and the fiches now hand the caller's language to every helper that takes one, and localize_axes is called once per axes a plot builds rather than once per figure: a twin axis, a colorbar and the z of a 3-D panel each carry a formatter of their own, and each was shipping in English beside a panel already in Spanish. What decides whether an axis is localised is its formatter and not its scale, so a log axis of distances still writing its own numbers is reached and a pinned frequency axis is left to format_frequency_axis. A zoom inset and a contour colorbar, which the save-time pass of the figure generators cannot reach, are localised where they are built. The band column of the room criterion sheet and of the whole sound-power fiche family is written in the sheet's language too.
scripts/check_language_forwarding.py fails on any call to a helper that takes a language, made from anywhere a language is in scope, that does not pass it on. It reads module and class bodies as well as functions, follows a helper through a local alias and a functools.partial, names the shapes it cannot reach, keys an exemption by its line so one approved call cannot cover another, and reports a method name only some of whose namesakes take the language, which silently switched the untyped-receiver check off.
scripts/check_figure_decimal_point.py reads the committed Spanish figures and fails on a tick label that is a number with a point in it, whichever pass was meant to write the comma. It found six figures, regenerated here. Both run from the Makefile and in CI.
A style keyword written in matplotlib's short spelling reaches the artist (#796)
* A style keyword written in matplotlib's short spelling reaches the artist
Seven artist properties have two names, and since matplotlib 3.3 a call that receives both spellings of one is a TypeError. Every renderer in the tree installed its own default under one spelling and then forwarded the caller's keywords, so `result.plot(c="red")` came back as "Got both 'color' and 'c', which are aliases of one another" instead of a red curve.
It was there in three shapes and all three are gone. The first is the obvious one, 195 `kwargs.setdefault("color", ...)` calls across 24 modules. The second is 25 places that read the default back out to draw a second artist in the same style, and it was the quiet one: `kwargs.pop("color", _C_PRIMARY)` takes one name only, so a caller who wrote `c=` got a figure whose curve was red and whose shading under it was the library's blue, with nothing to say why. The third is 35 literals of the form `{"color": _C_PRIMARY, "lw": 1.5, **kwargs}`, which reads like a default and is not one.
`style_default` was already there for the first shape and is now used everywhere. `style_get`, `style_pop` and `styled` are new and cover the other two, each honouring both spellings, and each returning what the caller asked for with the type of the default the renderer would have used.
`scripts/check_plot_style_defaults.py` refuses all four hand-written shapes on any mapping that is a function's own keyword parameter. A local dictionary the caller never sees is left alone, and so are the properties matplotlib gives one name. It also compares its alias table against the one the helpers read, so teaching the helpers an eighth pair without teaching the gate fails rather than passing quietly. It runs in `make plot-style-defaults` and as its own CI step.
The contract test that draws every result object now passes the short spelling too, which is what found the second and third shapes: the gate reads the source and cannot see a renderer that collides in a way nobody has written yet. Bar renderers are held to `lw` rather than `c`, because a Rectangle has no `c`.
The committed figures are unchanged, all 3194 of them. A default only moves when the caller expressed an opinion, and until now such a caller got an exception.
* The style gate reads the helper's alias table instead of importing it
The step it runs in installs nothing, and importing phonometry._plot.common pulls numpy in behind it, so the gate died on the import rather than on a finding. It parses the table out of the source now, which is also the honest comparison: what it checks itself against is the table as written.
Nineteen tables of porous parameters, and the hedges every catalogue row carries (#821)
* Nineteen tables of porous parameters, and the hedges every catalogue row carries
`materials.PUBLISHED_POROUS` is the porous rows of the nineteen parameter
tables Allard & Atalla print across eight chapters, keyed by the table they
came off and the row they are. It replaces the two specimens
`PUBLISHED_POROUS_MATERIALS` held, which are now
`'allard-2009-table-6-1/domisol_coffrage'` and
`'allard-2009-table-11-2/soft_fibrous'`, and it reaches the models the same
way they did.
The key names the table because the book prints one foam in four chapters and
one glass wool in two, sometimes with a column the other page leaves out, so a
flat name would have to pick between two published parameter sets in silence.
`porous_materials_named` gathers every printing of a name and leaves the
choice with the caller: five pages print a "Foam" and they are five different
foams.
Every quantity is optional, because no two of these tables print the same
columns, and a row that answers `None` says why. Table 11.5 prints the word
"model" in three cells of its screen row, because the lengths there were
derived from a measured flow resistance and the tortuosity taken as a
frequency-dependent expression; Table 12.1 prints "variable" for a thickness
its example sweeps. `why_missing` hands those words back, and `printed`
refuses with them rather than returning a float nobody published.
The units are the other half of the work. Table 9.1 prints its characteristic
lengths in millimetres and its thickness in centimetres, Table 13.1 prints its
lengths in metres, the rest are in micrometres, and Table 6.1 prints a complex
shear modulus in N/cm2 where Table 11.8 prints the same specimen's Young's
modulus in pascals. Every conversion is written into the oracle literal and
asserted against the printed digits, and the two glass-wool pages reconcile
through E = 2N(1 + nu), which is also what lets a row that printed one of the
two answer for both, marked as derived.
The fields that say what a cell was when it was not a value move to a shared
row in `_internal/catalogue.py`, so a solid and a porous specimen answer the
same way, and `reported` joins them: a cell that prints several readings from
several studies with no single value, which is how the compiled tables of the
absorber literature print a characteristic length.
Three tools were adjusted rather than worked around. The provenance gate
learns to read a data directory, because a catalogue of one file per published
table cannot list nineteen of them in a comment without that comment becoming
a second copy of the contents. The API reference generator learns to carry an
inherited attribute into the page it publishes, because factoring the shared
row out had silently emptied half of SolidMaterial's attribute table.
Membership there is read from the dataclass fields and not from `hasattr`: a
field with no default and a field built by a factory both leave the class
without an attribute of that name.
The plates, septa and impervious screens these pages also print are not porous
materials and are not in the catalogue; each data file says which of its
page's rows are absent and why.
Four entries join the errata register, found by reading the pages against each
other: a caption that names the wrong figure, two thicknesses the facing prose
contradicts, and a frame modulus three orders of magnitude from the resonance
the text reports for the same specimen. Every row keeps what its page printed.
* Regenerate the llms artefacts for the porous catalogue prose
* The banner reads in order, and the docstrings count what the catalogue holds
The ISO 1683 reference values are one published table, and every level reads it (#863)
* The ISO 1683 reference values are one published table that every level reads
phonometry.metrology.ISO1683_REFERENCE_VALUES holds Tables 1 to 3 of ISO 1683:2015 (sounds in gases, sounds in liquids, vibratory quantities) and the 50 nm/s that note b of Table 3 allows for structure-borne sound, keyed by medium and quantity. Each row is a frozen ReferenceValue with its quantity, medium, SI value and unit, the value as the table prints it, and the table it comes from.
The 20 µPa that was typed out seventeen times across the package, the 1 pW typed seven times and the other copies of the table now read it instead. _P0 no longer means a pressure in some modules and a power in others: the sound power references are _W0. A reference that deliberately comes from another document keeps its value and now names that document beside it: the 50 nm/s of ISO/TS 7849-1 and ISO 9611, the squared 20 µPa of ISO 532-1 and ISO 226, the 1 N of ISO 16283-2, the 1 kJ of DIN 4150-1, the kilopascals of ECAC Doc 29 and the inlet pressure of IEC 60534-8-4.
Every number the library returns is unchanged: the values the table stores are the same floats the copies held.
* A reference value in the package either reads the ISO 1683 table or names its source
scripts/check_reference_values.py walks the source tree and fails on a reference value that repeats a value of the table instead of reading it, and on one that differs from it without naming, in the comment above it or on its line, the document it comes from. What is a reference is decided by the name (a reference marker together with a quantity ISO 1683 lists, spelled out or as its symbol), and the places a name binds a number are all read: assignments at any scope, parameter defaults and keyword arguments. The bare 2e-05 and its square are caught wherever they stand, with a keyed exemption for the one place 2e-05 is a gravity gradient.
Run over the tree as it was before the table, it reports forty-three declarations; run over this one, none. It has a make target, reference-values, and a job of its own in CI.
* Every printed ISO 1683 reference value is a conformance row
Twenty rows join Levels & dosimetry: the eighteen values of Tables 1 to 3 and the 50 nm/s of note b of Table 3, each compared exactly with the published table (the float the library divides by has to be the float the printed decimal is), and note b of Table 2, the 10 lg(20²/1²) dB between a level re 1 µPa and the same pressure re 20 µPa, run through underwater.in_air_to_underwater_spl against the printed "approximately 26,0 dB". The joule, the newton and the pascal squared second join the unit vocabulary of the report.
The 1261 rows already in the report are unchanged to the last digit.
* The levels guide says which reference each level is counted from
The integrated-levels guide, in English, in Spanish and in its docs/ mirror, answers the question a reader of a level asks first: the ISO 1683:2015 reference of every quantity for gases, liquids and vibration, the 26.0 dB between the air and water pressure references, the modules that count from another value and why, and how to read the table from metrology.ISO1683_REFERENCE_VALUES. ISO 1683:2015 joins the page's references, the API reference gains its metrology.reference_values page, and the changelog records the table, the guard and the new conformance rows.
* A reference value typed wrong under its old comment now fails the check
scripts/check_reference_values.py counted any standard named in the comment beside a value as the document that value comes from, so a pointer replaced by a mistyped number kept its "(ISO 1683:2015 Table 1)" comment and passed. A value that differs from the table now has to name a document other than ISO 1683 itself and be listed in DIFFERENT_REFERENCES with the number that document prints, so a typo in one of the twelve deliberate departures fails too.
The check also reads the notation the standards print for the vibratory references (v0, a0, F0, xi0) and holds it to the values of the table only, sees a literal through float() or np.float64(), through tuple unpacking and through the string keys of a dict, and catches a value of the table written into the arithmetic of a level: the divisor inside a logarithm and the factor of an antilogarithm, which is where the two references that used to be written inline stood. A sentence that starts "In 2019" no longer reads as a citation, and a standard needs a number of two digits.
Run over the tree as it was before the table, it now reports forty-five places. On a copy of this tree with sixteen defects put back (old literals, pointers replaced by a wrong number with their comments left, a listed departure mistyped, a citation removed) it reports sixteen. On this tree, none. The tests put a wrong number back in seven places of the real modules, and removing any one branch of the check makes at least one test fail.
* Each reference value names the document it is actually read from
DIN 45672-2:1995-07 defines the 5·10⁻⁸ m/s of its velocity level itself, in Formula (2) on page 4, and names DIN EN 21683 only for the acceleration reference of Formula (3). The railway module and the ISO 1683 table said the velocity reference came from ISO 1683:1983 through DIN EN 21683; both now cite Formula (2) and say it is the 50 nm/s that note b of ISO 1683:2015 Table 3 says is also used for structure-borne sound.
The reference of the structure-borne power level of EN 15657 and of the power input into a floating floor is the 10⁻¹² W of EN 15657:2018 clause 7.1 and of Hopkins (2007) Table 2.1. ISO 1683 lists no structure-borne power, so the comments say where the number comes from and that it is read from the airborne 1 pW of Table 1.
The roex slope of the Moore and Glasberg loudness models is called p51, so _P_REF means a reference pressure everywhere in the package, and the wind-noise anchor of the ambient-noise model takes its 20 µPa to 1 µPa step from the table. Every number the library returns is unchanged; the anchor is the same float, 51.020599913279625.
* The guides say where each reference value comes from
The railway vibration guide, in English, in Spanish and in its docs/ mirror, no longer says DIN 45672-2 takes its 5·10⁻⁸ m/s from ISO 1683:1983: the standard defines it in Formula (2) without naming a source, and it is the 50 nm/s of note b of ISO 1683:2015 Table 3, 34 dB from the 1 nm/s of its main row. The integrated-levels guide and the curated API table tell the same story.
The underwater acoustics guide, where a reader meets the 1 µPa and 20 µPa references and the step between them, now says both are sound pressures of ISO 1683:2015 and points at metrology.ISO1683_REFERENCE_VALUES, with ISO 1683:2015 among its references. The metrology row of the namespace table lists the reference values, and the changelog entry describes the check as it now stands.
* Each ISO 1683 table name is spelled once in the reference-value module
The table labels are module constants, and the reference-value check resolves
a row field that names a module-level literal, so it still reads the table
from the source.
Check the example report fiches the way the figures are checked (#471)
* Check the example report fiches the way the figures are checked
Five committed fiches no longer matched the code that writes them, and one
had never been committed at all. Nothing in CI looked: the conformance
report, the generated API reference, the evidence pages and the documentation
figures each have a staleness gate, and .github/reports had none, so a fiche
could sit a release behind for weeks with nobody the wiser.
iso1999_nipts and iso1996_tone_audibility still carried their shaded regions
as they were drawn before fills started being derived from the page they sit
on: the fractile band as the old 50 % composite #ffddbb rather than the
#ffe7d2 wash, and the decisive tone's critical band as the hard-coded #eaf0f8
rather than #c8deed. The printed values and the verdicts were already right,
so nothing there could be misread, but the two fiches the documentation
offers as examples of what the library prints were showing a paler band than
the library prints. ebu_r128_loudness and the two iec61260_filter fiches had
the same kind of drift a shade smaller. All five are regenerated.
iec61043_intensity was registered in the generator and documented in the
intensity guide, and its PDF and preview were never committed. The example is
a good one, matching the worked class verification the guide already walks
through, so it is generated here and embedded in the guide in both languages
like every other worked example.
The gate itself is scripts/check_reports.py, run by a new Example report
fiches up to date job. A byte diff is not an option, for the reason it was
abandoned for the figures: every fiche embeds its plot as vector geometry,
and GitHub's heterogeneous runners compute a few of those coordinates a last
bit apart, which rewrites the page for no visible reason. Each fiche is
compared instead on what it says and on what it looks like: the extracted
text exactly, page by page, and the committed preview within the same
two-criteria pixel tolerance the figures use.
The tolerance was measured, not assumed. matplotlib writes SVG coordinates
through %f, so one unit in the last emitted decimal is the largest text
change a last-bit difference can make; re-rendering five fiches with every
plotted coordinate moved by that much, in a random direction, moved at most 8
pixels past the level threshold and the whole-page RMS by 0.074. The two
stale fiches moved 28710 and 27629 pixels, and the subtler restyled fills
moved the RMS to 0.79 and 1.33. The thresholds sit between the two
populations, at 64 pixels and an RMS of 0.5: about an order of magnitude
clear of the noise, and a factor of two under the smallest real change. They
are tighter than the figures' on purpose, since a fiche preview is a flat
document page rasterized by a pinned binary rasterizer rather than a plot
raster, and the figures' looser bound would have let the restyled fills
through.
Around it: requirements-reports.txt pins the svglib/reportlab/pypdfium2 half
of the chain the way requirements-figures.txt pins the matplotlib half, make
reports clears the directory first so a fiche that is no longer produced is
actually removed, and the generator pins the numerical thread pools like the
figure generator so a runner with a different core count cannot reorder a
sum into the rendered page. The staleness helpers the two checks share move
to scripts/generated_assets.py. A unit test also asserts every registered
fiche has a committed render, which fails in the same second as the commit
that forgets one and needs no regeneration to do it.
* Show the intensity class fiche in the markdown mirror too
The guide embeds it on the site in both languages; docs/intensity.md is the
copy GitHub renders with no build step, and every other guide with a fiche
carries the same preview there.
* Say where the fiche previews are now checked
Three places still described the committed previews as unchecked. The module
docstring, the Makefile and the workflow comment were brought into line with
the new job; the generator's own preview writer, the Astro component that
embeds the preview and the labeler's file lists were not.
The labeler entry matters on its own: scripts/generated_assets.py is what the
figures and the fiches now share, so a change to it belongs to both groups and
was landing in neither.
* State what the tolerance measurement actually showed
The calibration note generalised from five fiches to all 67. Measured
across the whole set, the upper-bound displacement pushes five of them
past the thresholds, all plots whose bar edges land on a pixel boundary,
and one character of a printed field moves 53 to 102 pixels rather than
the 89 quoted, so most single-character edits are caught by the RMS
alone. The thresholds stay: the bound is not what a real machine does,
raising it would stop catching the restyled fills, and the exact text
comparison covers every printed value regardless.
* Skip the fiche text checks when pypdfium2 is absent
The module already skips itself without Pillow, but the three tests that
drive the PDF half of the gate go through pypdfium2, and nothing said so.
Without it installed they do not skip: pdf_problem catches the ImportError
and reports "could not extract text", which is the right answer for a
corrupt fiche and the wrong one here, so two of the three fail on an
assertion about text that was never read. Blocking the import turns them
from two failures into a module-level skip.
* Label a fiche change when the figure pins move
The reports label listed requirements-reports.txt but not
requirements-figures.txt, and the fiche job installs both: the plot the
fiche embeds is drawn by the matplotlib and NumPy pinned in the figures
file, so a bump there can move the rendered PDF and its preview. Such a
pull request was going out with the figures label alone.
* Swap the regenerated fiches in only after a complete run
`make reports` deleted every committed PDF and preview before rendering a
single one, so a generator that died halfway left the working tree stripped
of the examples it had not reached yet. On CI that only fails the job, but
locally it is a real loss: 116 of the 134 committed files were gone twenty
seconds into an interrupted run, and the only way back is `git checkout`.
The set is now rendered into a scratch directory and moved into place once
it is complete. Clearing the output stays, because the generator overwrites
and never deletes and a fiche that is no longer produced has to disappear
from the tree; it just happens after a successful run rather than before an
attempted one. A trap removes the scratch directory on every exit path,
interrupted or not, and an empty one fails the target rather than emptying
the committed directory.
* Read the fiche filenames off the registry instead of rendering for them
The test that checks every registered fiche has a committed render only
needed the name of each file, and it got it by calling all 67 factories for
the third element of the tuple they return, computing and plotting every
worked example to learn where they were meant to land. The registry is now
keyed by the file each factory writes, so the check is a path lookup: 1.58 s
of factory calls down to under the 5 ms pytest bothers to report. The
per-example render test asserts the factory still writes the file it is
registered under, so the keys cannot drift away from what is produced.
* Rewrap the fiche calibration entry
* Swap the reports directory in one recoverable move
Deleting the committed renders and then moving the new ones in left a
window where an interrupted move emptied the directory. The recipe now
renames the current directory aside, renames the fresh one into place
and only then discards the copy it kept, and the trap restores that copy
whenever the swap started but did not finish. It restores nothing when
the generator failed first, which is when the directory was never
touched.
The pypdfium2 skip also moves off the module and onto the three tests
that read a PDF, so the raster comparisons still run without it.
* Mark the reports swap complete before discarding the copy
The backup still looked pending while the copy was being deleted, so an
interrupt in that stretch restored the previous renders over the ones
just generated. Renaming the copy aside first settles the question in a
single atomic rename, which leaves only two consecutive renames between
the new renders landing and the copy being disowned, rather than the
removal of 134 files.
* Assert the failure and its reason separately
A composite assertion reports only that the pair was false, so a check
that returned None and one that returned the wrong reason failed the
same way.
The six books held against each other, and one of them is wrong about tin (#816)
* The six books held against each other, and one of them is wrong about tin
The point of holding the same material from six published tables is that you
can put them side by side. Nothing did that until now, so a digit typed wrong
in one of the 219 rows would have sat there looking like a published value.
check_solid_agreement.py fails on density and only on density. That is not a
compromise, it is what the data supports: across these six tables the density
of a named material agrees to within 2,6 per cent, every time, because a
density is the one property a table cannot get very wrong without describing a
different material. The moduli, meanwhile, disagree by nineteen per cent on
aluminium and thirty on lead without anybody being wrong. So the tolerance sits
at 8 per cent, between the widest real spread and what one wrong digit does,
and a test states that band rather than leaving it to the comment.
Rows a page marked as a specimen of its own are reported and never failed on,
because Bies prints annealed, rolled and sheet lead and they are three things
under one name. What the default run prints is the handful of materials whose
undistinguished rows are more than half apart on something, which today is one
line; --report prints all 127 and never fails.
That one line is tin. Cremer and Mechel print a Young's modulus of 4,4 GPa and
Bies 54, a factor of twelve, while the three densities agree to 0,3 per cent so
nobody has confused the material. Each book is consistent with itself: Cremer's
4,4 reproduces his own printed 780 m/s, his shear modulus and his transverse
speed; Bies' 54 reproduces his printed 2 720 m/s exactly; and Mechel's f_cr d
of 81 m Hz follows from 4,4 and not from 54. No page contradicts itself, so
this is not an errata, and the accepted modulus of tin is near Bies' figure
rather than the one the other two share. Two books agreeing is not evidence.
The disagreement the gate does silence is written down with its reason, and the
silencing is a ratchet both ways: an entry whose books come to agree fails
until somebody deletes it. It holds one, polypropylene, where Mechel's 1 100
kg/m3 is what his page prints and what his own impedance column is computed
from, and Bies' 905 is what the material weighs.
Writing the tests found two holes in the gate itself and both are closed. The
staleness ratchet was keyed on the material groups rather than on what was
actually compared, so a reason for a material two books print and nobody can
compare would have stayed silenced forever; brick is already in that position,
because Bies prints his density as an interval. And the spread divided by the
smaller value without asking whether it was positive. Both tests fail against
the version without the fix.
* Three things the gate said that the gate itself disproved
Review found three, and they are all the same shape: the checker and its prose
asserted something about the catalogue that the catalogue contradicted.
The density claim was false, not merely loose. "A named material's density
agrees to within 2,6 per cent, every time" sat six lines below a registry
holding polypropylene at 21,5 per cent apart. The census says iron 2,63, steel
1,30, tin 0,27, the other nine at 0,00, and polypropylene at 21,55 with its
reason written down, so the true claim is about every density the gate does not
already accept. Fixed in the script docstring, beside DENSITY_TOLERANCE, in the
CHANGELOG, in the guide in both languages and in the docs twin. The Makefile and
workflow descriptions had drifted further still and said three per cent, which
was never the enforced number.
normalised stripped digits, and the catalogue was already in it: Bies prints
Nylon 6, Nylon 66 and Nylon 12, and all three folded into one group. That is the
defect the function exists to prevent, one step further in than the aerated
concrete case its own docstring cites. It also swallowed the 5/8 in of one of
Long's masonry blocks. Digits stay now, the eighteen cross-book groups are
unchanged, and two tests hold it: one on the names and one on the live rows, so
the day a second book prints a nylon they still cannot merge.
--report printed the clean summary over a real disagreement. The flag returns 0
by design, so its exit status carries no information, which leaves the summary
as the only thing saying the catalogue is sound. The decision now lives in
verdict(), outside main, and its three cases are tested without touching global
state.
The tin paragraph leaned on an outside figure and on the word accepted, which
collided with ACCEPTED the registry. Both are gone, and what replaces them is
stronger because it comes from the rows: a tin of 4,4 GPa carries sound at
780 m/s, which lands it among the soft plastics and below every lead row here,
and those run from 1 180 to 1 257 m/s although lead is the softer metal and half
again as dense. Writing that turned up my own error, since 780 is above soft
polyethylene and not below it, and the claim now has a test against the live
rows so it cannot rot the way the density claim did.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
A citation that names three documents is filed under three documents (#778)
* A citation that names three documents is filed under three documents
The reader turned a citation into one document and left whatever else it
named inside that document's clause. `ISO 16283-1:2014 Clause 8.1 /
-2:2020 Clause 8.1 / -3:2016 Clause 7.3.1` was recorded as part 1, and
the same reading put ANSI S1.11 inside a clause of IEC 61260, BS 5969
inside one of IEC 651, and the NASA table the numbers were actually read
from inside one of a withdrawn IEC 537. Nothing caught it, because the
only verification was that the split rebuilds the citation string, and a
second document sitting in the clause rebuilds perfectly.
A citation now carries every document it names, `reference.documents`,
in written order, each with the connector that introduced it and what
that connector says: corroborates, compares, via, mentions. There is no
headline document left to read, which is the point of the shape: four
artefact invariants and the gate itself looked only at the first
document, and a consumer that does not iterate now fails instead of
quietly reporting one of three. Each document has to account for its own
span of the string, so the rebuild is stricter than the one it replaces,
and a connector is a split only where what follows it opens like a
document, which leaves `ISO 7196:1995 Table 2 / A.3` and `Normal modes
vs ideal waveguide` naming one thing each.
Three series the reader cut in the wrong place come with it: `JIS A
1418-2` and `EBU R 98` by their prefixes, and `NASA CR-3406` by
anchoring an edition to a century, since a document number and a year
are written the same way. That anchor moves no other citation. A
citation may also write a work by the name a reader would know it by and
leave the rest of the title off, as `(NORAH2 Eq. 8)` does for the NORAH2
guidance, and that shorthand is expanded the way `-3` is: the record
names the document, the citation goes on writing what it wrote.
What sort of document a named work is is declared in one place with it.
The rules that read a work from its shape alone disagreed with each
other, so Mackenzie was an article where a citation wrote the year and a
book where it did not, Ainslie and Hopkins and Vigran were books read as
articles, and the NORAH2 guidance was a book to the parser and a report
to the file that records what the parser cannot. One work, one kind, and
the gate fails naming the citation that disagrees.
50 citations name more than one document, across 58 checks, and the
other 721 parse to exactly what they parsed to before. Distinct
normative designations go from 161 to 170 and further sources from 85 to
88, three of which were never works at all but a book, a separator and a
second author read as one name. `counts.standards` is 424 before and
after, so the banner, the badges, `llms.txt`, both READMEs and
`.zenodo.json` do not move, and no check id changes. The artefact is
schema 2, because `designations` and `sources` now mean documents named
anywhere in a citation rather than documents that open one.
Four tests hold the class shut. The net asks of every document of every
check that no clause and no designation carry a body with a document
number, a sibling part, an author with a date, a connector, or any
designation the artefact itself records for another citation, whole or
written the short way in front of a place in it. The list of works the
reader knows by name is a ratchet, counting only the heads that list
could have produced. The kind agreement is the gate check above. And the
Markdown report's own fixture has to be a document the gate accepts, so
it cannot go on declaring the shape of a release ago.
* A field the artefact leaves out is never written as null, and the gate says so
The builder writes an empty tail, a first document's lead and every other field a check does not have as None, then drops each null-valued key before the file is written, so docs/conformance.json carries no null at all. The site reads it through a schema that declares those fields optional and none of them nullable, which is right for the file as written, but nothing on the Python side held the file to it: the gate accepted a document with nulls in it, and the renderer fixture that is asserted to be a document the gate accepts carried twelve. A null that got past the builder would have failed first in the documentation build.
The gate now reports every null by its path, the fixture is written the way the artefact is, and a test pins both ends: a citation with nothing after its last document has no tail key, one that closes on a bracket keeps it, and a null tail or lead is named by the gate.
The same test file compared a tuple of what the reader read against the expected split in one place and the other way round everywhere else, which is the order it now uses throughout.
* The null guard is pinned through the gate that calls it, not only through its helper
* A null where the gate expects a list is named, not raised on
* Every relation the reader produces is accepted by the site and worded in both languages
Reading a citation as a list of documents gave the relation vocabulary a
fifth value, `supplies`, for the `with` and `and` that introduce a document
the check runs the clause with. Ten rows carry it. The Zod enum in
`site/src/content.config.ts` still listed the other four, so the artefact was
refused on its way into the content collection and the site build stopped with
an `InvalidContentEntryDataError` naming ten checks and no cause; and neither
label map in `Conformance.astro` had a word for it, so the Spanish table would
have printed `supplies` beside `corroborado por` and `comparado con`.
The schema now accepts all five, and the page words the new one `run with` in
English and `resuelto con` in Spanish: the citation's connector says the check
runs the clause with what that document prints, an input it supplies or a rule
it states, which is what ISO 11957 clause 8 does with the rating rule of
ISO 717-1 and what EN 16487 4.2.1 does with the air absorption of ISO 9613-1.
The two vocabularies drifted because nothing compared them.
`scripts/check_conformance_vocabulary.py` compares the five closed
vocabularies of the artefact (verdict, comparison shape, tolerance mode,
document kind, relation) across the three places that state them: the Python
enums that produce them, the Zod enums that validate them into the site, and
the label maps that word two of them in each language. It fails both ways, on
a value the site does not accept or word and on one the site still carries
after nothing produces it, and it reads the artefact too, so a hand-edited
value fails as well. `make conformance` runs it last, beside the artefact
gate, and the conformance job runs it in CI, which is cheaper than waiting for
the site build to say a row is invalid.
* The release note says which words the conformance table prints beside each document
The entry lists the five relations the reader produces and stops there. A
reader of the release notes cannot tell from it that the page prints one of
those words in front of every document a citation names, in English and in
Spanish, nor that the vocabulary is now held to the enums by a gate instead of
by the site build refusing every row that carries a value its schema does not
accept.
So the entry now words each connector in both languages, says that a qualifier
written inside a bracket is printed as the citation writes it, and names
`scripts/check_conformance_vocabulary.py` as what compares the artefact's five
closed vocabularies with the two places the site restates them, both ways.
* A value only the committed artefact carries is a hand edit, and a test says so
Two of the gate's tests read the drift one way round, with the site behind the
enums. The third direction it checks, a value in `docs/conformance.json` that
no enum defines, had no test of its own, and deleting that comparison left the
suite green. The artefact is generated, so the only way such a value gets in is
by hand, which is precisely when nothing else is looking.
* A work is read by the name the citation writes, not by the name it is filed under
The kind table is keyed by the designation a work is recorded under, and the splitters read it with the name the citation writes. For five of the six declared works those are the same string, so the miss went unseen. For the sixth they are not: the corpus files the NORAH2 report as "NORAH2 guidance", so "NORAH2 (2015) Eq. 8" came back as an article called "NORAH2", a kind the record contradicts and a designation nothing else in the corpus cites. The same name in front of a bare clause took the other splitter and came back a book.
The lookup now goes through the work record by the written name, which carries both the declared kind and the expansion to the full designation, exactly as the splitter for a bare work name already did. The three shapes that read a work name are all served by it, so a year, an edition mark and a plain clause now land on the same document. No citation in the corpus is written in the shape that was wrong, so the artefact does not move; the test covers all four shapes so it cannot come back.
The vocabulary gate's page test built its rewritten page inside the block that expects the failure, which leaves two calls that could raise where the test means one.
* The vocabulary gate says one row carries a value, not carry it
The cost line put the noun in the plural and left the verb alone, so a value on a single row of the artefact was reported as "1 row of the artefact carry it". Both now agree with the count, and a test holds the three cases.
* The conformance artefact is regenerated on top of the ISO 4866 re-citation
The same D.3 to D.2 re-citation that main brought in, carried through this layer's artefact, which the move onto main had kept as this layer wrote it.
The figures are written to two decimals, and the comparison knows what one quantum is (#820)
* The figures are written to two decimals, and the comparison knows what one quantum is
Matplotlib writes every coordinate of an SVG to six decimals. At one user
unit per point that is a hundredth of a micron; two decimals is a hundredth
of a point, below what any zoom of any display resolves, and the corpus of
2 704 committed figures was carrying the other four in every number of every
path. generated_assets.compact_svg cuts coordinates and translations to two
and drops the indentation, the DOCTYPE and the metadata block, none of which
anything reads. save_figure runs it on every matplotlib figure and the
diagram canvas runs it on every plate, one rule over the corpus. The 2 704
regenerated SVGs are 217,1 MB where they were 258,6, and the 68 lossless WebP
rasters came out byte-identical, which is the reproducibility of the pipeline
measured on the way past.
Two things are not rounded, and both were found by rasterising a figure
before and after and counting the pixels that moved. A scale() keeps every
digit: the glyph outlines are drawn at scale(0.015625), and two decimals of
that is 0.02, a twenty-eight per cent error in every letter, two hundred
thousand pixels different. The root element keeps its width and height: they
fix the canvas, and rounding them moved it by a pixel. With those two rules
in place a rasterisation of each version at three times display scale differs
only at glyph edges, at most 23 of 255 and one pixel at 224 where the
antialiasing of a "2" went the other way; at display scale the worst pixel is
24 of 255. The eight gates that read the SVGs pass over the regenerated
corpus, and so do the language and annotation audits of the run.
The third thing is check_figures.py. It compares the committed and the
regenerated file number by number within a tolerance meant for a 1 ULP
wobble between CPUs. Two machines that land either side of a rounding
boundary are now exactly one quantum apart, 0,01, and the absolute term was
0,01: it should have passed. It did not, for 45,6 per cent of the consecutive
two-decimal pairs below a hundred, because 0,01 is not representable in
binary and the difference of two such numbers comes out as
0,010000000000000002. The tolerance is one and a half quanta now, which
accepts every one-quantum step and rejects every two-quantum step, and a
test sweeps all ten thousand pairs both ways so the two constants cannot
drift apart.
* The relative term of the SVG tolerance is zero, because it let two quanta through above 150
Review found that the sweep proving "one quantum accepted, two rejected" had
only ever looked below a hundred, and that numbers_within_tolerance takes the
larger of the absolute and relative terms. With a relative 1e-4 the limit at
a coordinate of 200 was 0,02 and at 900 it was 0,09, two and nine quanta,
over most of the canvas. The claim in the previous commit was true where it
was tested and false where it was not.
Nothing legitimate needed the relative term any more. A rounded coordinate
moves by one quantum between machines or not at all, whatever its magnitude;
the glyph outlines are integers in font units; and the one number kept at
full precision, the root element's size, wobbles by a millionth. So the term
is zero, and the sweep now runs in four bands, below a hundred and around
200, 900 and 2 000, accepting every one-quantum step and rejecting every
two-quantum step in each. All four fail against the old value.
Measure the noise of railway rolling stock by ISO 3095, on a reference track judged by EN 15610 and EN 15461 (#909)
Three new modules under environment.sources carry the ISO 3095 type test of a railway vehicle: the pass-by level and transit exposure level of a pressure record, the stationary, constant-speed, starting and braking results from their runs, the Annex A, C, D, E and G steps, and a single verdict on the reference track from the rail roughness of EN 15610 and the decay rates of EN 15461. Every result has .plot(), a new guide in English and Spanish walks a pass-by and a reference track through, the conformance report pins the limits of Figures 2 and 3, the positions of Figure 10 and the budget of Table G.2, and four printed defects of ISO 3095 Table G.1 and the EN 15610 listing are in the errata register.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
The five verify_* returned a dictionary, and two of them had a twin that wrapped it (#758)
* The five verify_* return a result object, and the two twins that wrapped one are gone
Every verifier in the library returns one of the 194 result dataclasses
except these five, which returned a bare dict[str, Any]. Two of them had a
public twin whose only job was to run the verifier and package what it gave
back: filters.filter_class_compliance and emission.intensity_class_compliance.
The verifier now returns the object itself, so there is one door onto each
computation instead of two, and the rows a caller reads come back on a name
rather than on a string key.
verify_filter_class returns the FilterComplianceResult and
verify_intensity_class the IntensityInstrumentComplianceResult that their
twins used to build, with .plot() and the accredited .report() fiche on them
as before. The two twins are gone.
The other three gain a result class of their own: WeightingComplianceResult,
QuasiPeakDynamicsResult and AircraftSystemComplianceResult. Each one pins the
summary it states against the rows under it, because the summary is the
conjunction, the minimum or the strictest class of a column a reader prints
beside it, and a sheet whose box disagrees with its own table is the one
reading a caller cannot correct.
Everything after the argument the call is about is keyword-only:
verify_filter_class took num_points positionally where its twin did not, and
verify_quasi_peak_dynamics took the ballistics second.
* Pin the three new verdicts against the rows under them, and publish the weighting result
* Say keyword-only where the table says it, and name the verifier in the pages that pointed at its twin
* Every class the edition defines has to be on the row, and on the sweep
The same argument for the geometry that had two units on one name (#755)
* The same argument for the geometry that had two units on one name
#752 put the unit in the name where a pressure, a temperature or a
humidity could be read two ways. Measured over the imported API, four
geometric quantities were in the same state, and one of them was two
different dimensions.
diameter was metres in twenty public parameters and millimetres in one,
cnossos_rail.wheel_transfer, where CNOSSOS-EU Table G-3b tabulates 920,
840 and 680. angles was degrees in ten and radians in four while angle
was radians in eleven, so the plural and the singular of one word
disagreed. period was a lattice pitch in metres in the slit and
metadiffuser panels and a repetition time in seconds in the synchronous
average. gradient was a road slope in per cent in CNOSSOS-EU and a
vertical sound-speed gradient in s^-1 in refraction and sound speed.
They become diameter_m and diameter_mm, angle_rad, angles_rad and
angles_deg, period_m and period_s, road_slope_percent and gradient_per_s.
periods, a count of repetitions sitting beside a period that is a length,
becomes repetitions; the two places where it really means evaluation
periods keep the word and are in the guard's exemptions with the reason.
GaussianPulse.width is a half-width in seconds and the perfectly matched
layer of the same module measures its own width in cells. It says
half_width_s now.
No value changes: the conformance report is the same 995 checks with the
same numbers. The guard grows a second tuple, matched against the whole
name, and says in its own comment that the seventy-five compound names
are a second pass rather than an exemption: WestonRegimeBoundaries has
critical_angle in radians one screen above a function that takes it in
degrees. Against main the guard reports 71; here none.
* Re-render the three clips the renamed scenes drew
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
The same argument for the geometry that had two units on one name (#755)
* The same argument for the geometry that had two units on one name
#752 put the unit in the name where a pressure, a temperature or a
humidity could be read two ways. Measured over the imported API, four
geometric quantities were in the same state, and one of them was two
different dimensions.
diameter was metres in twenty public parameters and millimetres in one,
cnossos_rail.wheel_transfer, where CNOSSOS-EU Table G-3b tabulates 920,
840 and 680. angles was degrees in ten and radians in four while angle
was radians in eleven, so the plural and the singular of one word
disagreed. period was a lattice pitch in metres in the slit and
metadiffuser panels and a repetition time in seconds in the synchronous
average. gradient was a road slope in per cent in CNOSSOS-EU and a
vertical sound-speed gradient in s^-1 in refraction and sound speed.
They become diameter_m and diameter_mm, angle_rad, angles_rad and
angles_deg, period_m and period_s, road_slope_percent and gradient_per_s.
periods, a count of repetitions sitting beside a period that is a length,
becomes repetitions; the two places where it really means evaluation
periods keep the word and are in the guard's exemptions with the reason.
GaussianPulse.width is a half-width in seconds and the perfectly matched
layer of the same module measures its own width in cells. It says
half_width_s now.
No value changes: the conformance report is the same 995 checks with the
same numbers. The guard grows a second tuple, matched against the whole
name, and says in its own comment that the seventy-five compound names
are a second pass rather than an exemption: WestonRegimeBoundaries has
critical_angle in radians one screen above a function that takes it in
degrees. Against main the guard reports 71; here none.
* Re-render the three clips the renamed scenes drew
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Determine the sound energy level of a noise burst beside the sound power level (#671)
* Determine the sound energy level of a noise burst beside the sound power level
ISO 3744 clause 8.3 and ISO 3741 clause 9.2 are the sound power chains with the
single event time-integrated level in place of the time-averaged one, and they
now live beside them: sound_energy_pressure over the enveloping surface (ISO 3746
clause 8.4 for the survey grade), sound_energy_reverberation and
sound_energy_comparison in the reverberation room, each returning its own result
with .plot(). mean_single_event_level reduces N_e events measured one at a time
(Eq. 19) or one measurement encompassing them (Eq. 20); octave_band_levels sums
thirds into octaves per ISO 3741 Annex F; reference_atmosphere_correction carries
either level to the reference atmosphere per ISO 3744 Annex G.
The background of a burst is compared as its exposure over the same integration
time, L_p(B) + 10 lg(T/T0), so that the energies K1 subtracts share one reference
and the 8.3 chain is the exact twin of 8.2 for every T.
* Test the sound energy level against clause 3, and draw what a burst's L_E is
The oracle is the definitions: for a source steady over T the integral of p^2
is T p^2, so L_E = L_p + 10 lg(T/T0) and every 8.3 equation being its 8.2 twin
gives L_J = L_W + 10 lg(T/T0) with the same K1, K2, A-weighting and uncertainty.
The suite pins that identity on both families with background and room in
play, Eq. (20)'s 10 lg N_e, the exposure the background is compared as, the
frequency-dependent K1i of ISO 3741, Annex F's octave sums and Annex G's
corrections, including the H.4.2.7 statement that they vanish at 120 m and
23 C.
The figure puts an impact burst beside a steady 80 dB source over a 10 s
window: the burst's running L_E is all there within a second, the steady
source's climbs as 10 lg(t/T0) to the same 90 dB at T, and the right panel is
the L_J spectrum the burst gives over an ISO 3744 hemisphere.
* Keep the two chips of the burst figure off each other's arrows
* Document the sound energy level on the two pages that promised it, and register the reading of Eq. (21)
Both family pages gain a section on the noise burst: what L_J is, how the
single event level replaces the time-averaged one clause by clause, the five
events of Eq. (19)/(20), the background compared over the same window, Annex G
on the enveloping-surface page and Annex F in the reverberation room, in
English, in Spanish and in the docs mirror, with the identity L_J = L_W +
10 lg(T/T0) run in a fence on each. The family overview and the indexes stop
saying the quantity is not implemented.
The conformance report carries five closed-form rows for the two chains,
Annex F and Annex G, and the errata registry two entries on ISO 3744 8.3.4: the
time-integrated level Eq. (21) compares with a time-averaged one, which is a
ratio of energies only for T = 1 s, and the K_1i the prose names against the
K_1 the equation defines.
* Let the sound energy fences stand on their own names
The report examples further up each page rebind freqs, levels and res to
another geometry, so a fence that reached back for them compared a 1.5 m
hemisphere with a 4 m one and a 21-band room with a 7-band one. Each section
now builds the steady twin it compares against under its own names, and the
one printed value that was off by a hundredth reads what the code prints.
* Name the window on the burst figure, so its label is not one an FDTD clip also writes
The freshness gate reads every exact translation entry a clip assembles with
a number as that clip's own, and '$T$ = 10 s' with its digits taken out is
the readout the absorption-placement clip prints in milliseconds. The marker
now says which T it is.
* Say which members of the ISO 3740 family are missing without a dash
* Register what Annex H gets wrong about the correction it sizes
The branch leaned on H.4.2.7 for a conformance anchor and named it in the
CHANGELOG, the tests and two pages, but never wrote the entry it owed. Both
of that paragraph's figures are wrong, and both are now in the registry with
their arithmetic.
The clause says the Annex G correction reaches 0,6 dB at 500 m altitude.
Annex G, which is normative and which the same sentence points at, gives
0,394 dB at 500 m and 23,0 degC. The reading is self-validating: those same
two equations give -4,6e-05 dB at 120 m, which is the zero the same sentence
prints. 0,6 dB would need about 697 m, or 30,1 degC at 500 m. The clause
then divides 0,6 by the square root of 6 and prints 0,3, which is 0,6/2.
The English and Spanish pages repeated the 0,6 dB as the size of the term
while documenting the very function that returns 0,394 for it. They now say
which two of the three statements hold and where the arithmetic lives.
The background-noise entry gains the fourth member of its family: ISO 3746
Equation (15) builds the same correction from a time-integrated event level
and a time-averaged background, and that is the route taken for the survey
grade.
Also declared: bringing the reverberation room to the sound energy level
regrouped the terms so they sum as Equation (30) brackets them instead of
left to right. It is a reassociation and it shows, 34 of 84 bands moving by
at most 1,4e-14 dB, and one committed conformance residual going to zero for
the same reason. No figure moves. And the emission exports go back to
listing their constants first, the convention the rest of the tree keeps.
* Give the reflecting-plane refusal one wording
Three entry points take the number of reflecting planes and each spelled
its own refusal. A caller who reads one message and then meets another for
the same mistake has to work out whether it is the same mistake, so the
sentence is named once and used three times.
* Say which background carries a window, and which route is whose
Six of the seven review findings held up against the code and the print.
The wording about `integration_time` treated every background as an event
background. It is not: `sound_energy_comparison` corrects the source under
test against an exposure over the window, and the reference source, being
steady, by the time-averaged rule of 9.1.2, with no window at all. The
docstring already said so; the CHANGELOG and both editions of the errata
entry did not, and now do.
The introduction let a reader infer that ISO 3745 carries the burst route.
It does not define a sound energy level at all. The sentence named "the same
surface" right after describing the anechoic room, so it now names the
ISO 3744 and ISO 3746 enveloping surface and says the route is theirs alone.
The four index lines that repeated the phrase say it too.
`frequencies` is not required by both reverberation methods: it is optional
for the comparison one and becomes necessary only when a background is given,
because that correction is per band. The parameter table said both.
The method overview named the direct reverberation-room route and left out
the comparison one, which is a supported way to the same quantity.
In the quick table, `[s]` trailed `events, integration_time`, which puts the
event count in seconds. They are separate bullets now, and the count says it
is a count.
And an English page carried 101,325 kPa, which in English reads as a hundred
thousand. The tree already spelled it with a point eight times out of eleven;
the three that did not now match, the two outside this branch included.
Not taken: the request to spell "afterwards" the American way. The corpus
uses it thirty times and "afterward" none, so the change would make one page
disagree with the other thirty.
* Give emission's llms shard a second half, by measured quantity
Stacking the sound energy level on the in situ comparison put the emission
shard 10 910 bytes over the 200 000 the fetch budget allows, and the
generator says in as many words not to raise the limit.
Split the way the family already sorts itself, by the quantity actually
measured: the two intensity routes carve out, the pressure routes and the
surface velocity one stay. That leaves 135 KB and 76 KB, so a reader
fetching either gets a coherent set of guides rather than an arbitrary half,
and there is room for the next route to land in one of them.
* Stop a heading from claiming the guides after it
Four findings from the review, three of them defects and one of them mine.
The index of llms.txt emitted a shard heading the first time it met a route
from a different shard and never switched back, so any guide of the parent
shard that came after the carve-out in reading order was listed under the
carved-out heading. Splitting the emission shard exposed it: the
surface-velocity route, which measures no sound at all, ended up under a
heading about sound intensity, pointing a reader at a shard that does not
contain it. The runs are emitted shard by shard now. Nothing caught this,
because the artefact regenerates deterministically and the freshness job
compared a wrong file against itself, so there is a test that reads the
index back and holds every heading to its shard's own membership. It fails
on the old grouping.
A comparison against zero is not a guard: `nan <= 0` is False, so a room or a
reverberation time given as NaN walked past the positive-only checks and came
back as NaN bands, and an infinite T60 drove the absorption area to zero and
every level to -inf. An infinite band centre reached `round()` and raised
OverflowError, which is not the ValueError the function documents. All three
are refused now, with the tests that reproduce them.
And the English prose used comma decimals where the corpus uses points 255
times to 20. The values this branch computes read 0.394 dB and 23.0 °C; the
printed typography stays where it belongs, in the errata registry.
* Regenerate llms-full after the axis-label rebase
* Count events in whole events
The two energy results took the count of events on trust. `2.5` events and
`True` both passed, because `True` is an `int` in Python, and a string
reached the comparison and raised `TypeError` where the docstring promises
`ValueError`. The entry points already refused all three; a result can also
be built directly or rebuilt with `dataclasses.replace`, and then it could
not.
One check now serves both, beside the other pieces the ISO 3740 family
shares, and the tests pin the four refusals on each result.
* Give the surface result its frequency axis, and one decimal mark
The quick table's row for the surface sound-energy result did not list
`frequencies`, which it carries and which is `None` when no band centres
were supplied. Its reverberation-room twin lists it, and a reader comparing
the two would have concluded the surface one has no frequency axis.
And the same section printed the computed background correction as 1,26 dB
twice while printing it as 1.26 dB three times. The value is what the library
computes, not typography quoted from a standard, so it reads with a point in
both places now.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Reach a path through Path, and build a list by saying so (#611)
`PTH` and `PERF` are selected. The 193 sites they found were read one at a
time, because the two spellings of a path are not the same type: a `Path`
prints like its string and compares unequal to it, so the sites worth reading
were the ones that hand their value to a sort, a comparison or a dictionary
key. One did. The CI comment builder sorted a list of artifact paths, and
sorting `Path` objects compares them component by component rather than as
text, which reorders the table whenever one matrix name is a prefix of
another, as `3.13` is of `3.13-dev`.
Half of what the rules found was one pair of lines copied fifty-four times.
Every fiche test carried its own `_assert_one_page` and its own `_PDF_MAGIC`,
in eight spellings, and forty-six of those also asserted a non-zero file size,
which could not fail: finding `%PDF` in the first four bytes already proves
the file holds four bytes. That assertion lives once now.
Fifteen sites keep the older spelling behind a `noqa` that says why: they draw
the animations, and that code is hashed, so rewriting a line there asks for a
re-render of clips that would come back frame for frame identical.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Verdicts on a printed limit that read the decimal value, not its last bits (#914)
A quantity computed from decimal readings (a difference, a mean, a ratio, a spread) and judged against a limit a standard prints in decimal is now settled to nine decimal places before it meets that limit, and a half is rounded on the settled value, so 32,3 - 26,3 dB and 32,2 - 26,2 dB are the same 6 dB margin on every machine. One private helper does this for the whole library, and the six local copies of the same nine-decimal rule now go through it. The settled value only decides the verdict; every result keeps the value it computed. Two results change shape at a degenerate input: room.open_plan_metrics returns NaN for a distance when the STI does not fall with distance, and a hammer and floor that are critically damped in decimal get the critically damped pulse instead of NaN.
A catalogue row checks itself when it is built (#869)
* A catalogue row checks itself when it is built
Every io.CatalogueRow now holds itself, in its constructor, to one
contract, whoever builds it: a packaged loader or a caller writing a row by
hand. A name and a source that are not empty; a numeric cell that is None
or a finite number, never a bool, a text or a NaN; a whole-number field that
holds one, a flag that is True or False, a text field that is text; hedges
keyed only by the fields they can hedge; bounds only on fields with a range;
ranges that are finite, ordered and hold the end the page printed; readings
and uncertainties that are finite, the second not below zero; no value beside
a hedge that says there is none, and no conversion or carried cell with
nothing behind it; the basis vocabulary; and the physical limits by unit
suffix, a porosity from 0 to 1 and a share of a whole from 0 to 100 per cent.
A refusal is an io.CatalogueError naming the row and the field.
The fields are classified once per class from their resolved annotations,
and every set and mapping is frozen from its annotation all the way down.
That replaces the fifteen per-module lists of set fields and the frozen=
argument of take(). The solids' list had fallen behind its dataclass and
never froze bounded_below, and a loader with no list, or a row built by
hand such as Carpet(approximate=[...]), kept the list it was given. A
subclass field annotated with a type the contract has no check for raises
TypeError the first time the class is built.
The packaged data files are read as strict JSON: a NaN or an infinity, a
name written twice in one object, a slash in a row key or an unknown
top-level key is refused naming the file and the row.
PorousMaterial.frame_constants asks for the structural loss factor through
printed() instead of reading an empty cell as a lossless frame. Every
published porous row with both moduli prints one, so no result moves.
Every packaged row already met the contract except six ground surfaces of
Cox & D'Antonio Table 6.7, whose porosity the page prints in per cent in a
column that holds fractions everywhere else and states no unit. Their digits
move from GroundSurface.porosity to a new GroundSurface.porosity_percent, so
porosity is empty on those six rows; the catalogue page shows them in a
column of their own, the defect is in the errata registry, and the
fingerprint names the six cells that moved. No digit changes.
* A misprinted porosity says what the page prints, and an empty hedge is refused
The six porosities Cox & D'Antonio Table 6.7 prints in per cent, 26.9 to
58.1 in a column of fractions that states no unit, no longer get a field of
their own. GroundSurface.porosity_percent would have been a second spelling
of one quantity, and a field every user catalogue file would carry from
4.0. The six cells leave porosity empty and hold the defect as misprinted,
as every other cell the errata registry calls wrong does, so
why_missing("porosity") and printed("porosity") quote the figure the page
prints instead of saying the page gives nothing, which is also what they
answer for a row that prints a dash. The catalogue page shows the six cells
empty with that note, and the fingerprint step names exactly that move.
The contract also refuses three things it let through: a hedge whose text
is empty, which made why_missing answer "" for a misprinted cell; a
reported list with nothing in it, which read "the page lists and no single
value" and counted as something behind a converted or carried cell; and an
interval among the reported readings that runs backwards. A subclass field
annotated as a bare float or int raises TypeError, because every cell of a
row may be missing, and so does an annotation that is not valid Python,
which escaped as a SyntaxError. A negative value in a field such as
surface_density_g_m2 is refused naming the suffix it ends in, _m2, rather
than calling it a quantity in m2.
The fluid tables read their validity from the same strict parse as their
rows instead of a second json.loads, and the io package docstring says what
CatalogueError covers now. No other published cell changes.
* A set of field names is read only after its type is known
_names iterated a value the checker still typed as a plain object, because the
refusal before it returns nothing the analysis can follow. The branch that
reads the items now sits under the type test itself. Behaviour is unchanged.
* The frozenset fast path of a field-name set narrows by isinstance
A type() comparison does not tell a static checker the value is iterable, so
the generator over it read as iterating a plain object. isinstance narrows it
and admits the same frozensets.
The prediction before the measurement, and the two drafts that rewrite the railway (DIN 4150-1, E DIN 4150-2, E DIN 45672-3) (#767)
* The prediction before the measurement, and the two drafts that rewrite the railway (DIN 4150-1, E DIN 4150-2, E DIN 45672-3)
DIN 4150-1:2001-06 in vibration.immission.prediction: the decay law of
Formula (2) with the exponents of Figure 1 and the damping of Figure 2, the
building on its ground, the blast, the falling mass, the machine hall with
the nomogram of Figure 3 read off the page, and the track excitation.
E DIN 4150-2:2023-08 in vibration.immission.train_categories and behind
edition="2023" in the people module: one clock interval per passage, 1,5
times the r.m.s. of a category as its KB_Fmax, Formula (6) with the
weighting factors of Table 2, the night-time upper value of a new line, the
25 % rule of an altered one, the changed cell of Table 1, no shortcut inside
the 15 %, a railway compared with A_o and a road by night not, the existing
road and the induced seismic event. GuideValues carries the period and the
edition it was read for and the verdict follows it.
E DIN 45672-3:2023-02 in vibration.immission.railway_prediction: Formula (1)
as printed, the speed rescaling, the ground transmission, the six tables of
Annex A cell by cell, the chain of Clause 7 to the assessment quantities,
and the line source of Annex B.
Fifty-three conformance rows, twenty-one errata entries in both languages,
five figures, three guides in both languages with their mirrors, and the
family, topic and guide indexes.
* The two package lists stay sorted, the guide values are built and not copied, and the verdict reads its edition in one place
The names the three modules add went to the end of both __all__ lists as
blocks, one of them twice, which is what the analyser saw as the same fifty
lines in two files; the immission list is sorted again and the vibration
groups hold their own names. A guide value with one field changed is built
as a GuideValues and not through dataclasses.replace, whose return the
analyser cannot type; the edition a verdict is read under and the sources
judged without the upper value are two small functions; the positive
frequencies go through the shared guard instead of three copies of one
message; and the two exception tests build their guide values before the
block that raises.
Draw every label's text as its own outlines, and finish the corpus typography (#550)
A label with any mathematics in it was never set by the reader's font engine: matplotlib lays it out glyph by glyph from DejaVu metrics, and the SVG then asked the viewer for a generic sans, so the prose arrived letter-spaced on every machine without DejaVu. 1172 of the 2296 committed figures shipped that way, and the plates had the sibling defect, naming a Segoe UI that neither the maintainer's machine nor CI has ever had.
Both halves now draw their text as outlines: the figures from the DejaVu matplotlib ships, the plates from a vendored Liberation Sans whose metrics are the ones the hand-tuned compositions were adjusted against (1.000 width ratio over 6,550 strings, against 1.14 for DejaVu, which would have broken ninety-five plates), with a deterministic per-glyph fallback and a hard error for an uncovered character. Three plates rendered under an emptied fontconfig are pixel-identical to the normal render, and two consecutive regenerations are byte-identical across interpreters.
The corpus typography is finished with it: titles at text weight across 551 sites, the prime composed as a superscript, range hyphens restored inside mathematics where an ASCII hyphen sets as a spaced minus, negative readings signed with U+2212 in plots and fiches alike, and the library renderers' symbols moved into mathematics with descriptive subscripts upright, Spanish keys moving in lockstep in both translation tables.
Fifty-five layout collisions repaired across nineteen figures, found by opening all 427 in both languages; two systemic causes fixed at the root. All forty-two clips re-rendered, every one of the 168 WebM variants probed as AV1.
Two new gates gard what nothing could see before, both proven red against the real defect first: every dollar-delimited label must parse, and no Spanish value may keep a decimal the save-time comma pass cannot reach.
A flag is written by name everywhere, and the clip gate learns which changes cannot move a frame (#760)
#759 closed the flags a caller could reach. This closes the rest, so ruff's
FBT family is selected outright instead of documented as absent: 202
signatures take the star, and the 296 calls that passed a bare True name the
parameter.
Every one of the 156 call sites the change forced was named rather than left
positional. Two of them no static gate sees: _cached_weighting_sos and
_cached_weighting_filter are lru_cache'd, so mypy reads the call through
_lru_cache_wrapper and checks nothing, and only the suite found them.
The one place a keyword-only parameter is not a rewrite: Callable[[...], R]
cannot say that a parameter is keyword-only. ColumnsBuilder in
_report/_insulation_fiche.py and a new LeftCellBuilder in
_report/ansi_s12_2.py become protocols, which say what the alias could not.
FBT003 came down to ax.grid(True, alpha=0.3) in 295 of its 296 sites.
matplotlib declares that parameter visible, so the sites say visible=True and
a test pins the name, because a matplotlib release that moved it would
otherwise surface as a wall of unrelated red. The one that was left is
np.bool_(True) in a test, a cast rather than a flag; it is np.True_ now.
The clips are not re-rendered, and none of them needed to be.
animation_fingerprint already normalises away what cannot move a frame, the
docstrings, the import order, the TYPE_CHECKING blocks, with the reason on
each. Where the keyword-only star sits is the same kind of thing: it decides
how a caller may write the call, not what the function computes. With that
normalisation the 43 fingerprints are identical either side of the signature
sweep, measured, not assumed. The manifest is re-stamped because the
fingerprint itself changed, and the stamp records the same clips it always
described.
scripts/check_public_flags.py goes: ruff now says the same thing on every
definition, and a guard that cannot fire is noise.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Pass the form of a survey's reverberation index and a calibration's tone estimate as a bool variable (#913)
estimate_reverberation_index(weighted=) and metrology.sensitivity(narrowband=)
had overloads for True and False only. mypy does not split a bool into the
two literals, so a flag read from a setting was refused although the call
runs.
estimate_reverberation_index gains a third overload that takes a bool and
returns the array or the float. For sensitivity every form returns a float
and the True overload only said that the coherent estimate needs fs, so that
overload now takes a bool with fs as a keyword, a second one takes it with
fs in position after the target level and the reference pressure, and a new
first overload takes a Signal, which carries its rate, with either estimate
and no fs. With fs in position, a bare array now takes narrowband=True as
well, which mypy refused before. A bare array with narrowband=True, or
with a bool that may hold it, still needs fs, and tests/static_typing pins
both refusals with ignores that strict mypy reports once they go unused.
A test reads every overload set in src through the parser and fails when one
types a parameter as Literal[True] or Literal[False] without a bool overload
for it. Nothing changes at run time.
The clips move to a repository of their own, and publishing them is one make target (#818)
* The clips move to a repository of their own, and publishing them is one make target
Cloning this repository downloaded about 2 GB, and 1,8 GB of it was
.github/images. Not the figures: 2 704 SVGs cost 80 MB across eleven thousand
revisions, because they are text and git stores each revision as a delta.
The 430 WebM, GIF and JPEG files are what weighed, re-encoded whole on every
render and kept in every version, 1,55 GB between them. The Makefile already
drew the line between the two: `make graphs` regenerates the figures and CI
compares them, `make animations` renders the clips on a GPU and CI checks
only that they exist and that the code drawing them has not moved.
So the clips go to jmrplens/phonometry-assets and the code that draws them
stays here. scripts/assets_dir.py is the one place that knows where the
checkout of that repository is: PHONOMETRY_ASSETS_DIR in .env, beside the
GPU settings, or a sibling directory that `make assets` clones. Nothing falls
back to .github/images, because a clip written there would be reported as a
figure nobody committed, and the point is that no clip enters this
repository again.
`make animations` and `make posters` render into that checkout and then
publish: scripts/publish_assets.py stages images/, commits with a message
naming the clips and the commit here they were rendered from, pushes, and
writes the resulting commit into assets.lock. It refuses a checkout that is
off main or carries changes outside images/, so a stray file is never
committed under a message about clips, and it does nothing when a render
changed no bytes. A developer's commit here is then two text files, the
fingerprint manifest and the lock, and the video is already published.
Writing its test caught a defect in it: the helper stripped git's output,
and the porcelain status of an unstaged change begins with a space that is
part of it, so images/x read as mages/x and every render was refused as a
stray file. The fix is rstrip, and the test file's own helper had the same
bug until its test failed the same way.
CI never fetches a clip. check_animation_freshness.py takes a manifest of
names, and the quality job produces one from the tree of the locked commit
with a blobless fetch of that one commit: 148 KB and under a second, against
a third of a gigabyte of video the check only ever needed the names of. The
PyPI README pins its poster to the locked commit rather than to a release
tag, which the assets repository does not have; that commit never moves,
which is what the pin is for.
The 143 clip URLs in the README and the 33 hand-written guide twins now
point at the assets repository, and Video.astro builds its URLs from one
constant. The figures, the site content and the docs stay where they were.
The history that still carries the old clips is rewritten separately.
* The site stages the clips from the locked commit, so a branch's render cannot reach main's pages
The first commit left the site hotlinking the clips. It did not mean to: the
site serves its own copy of every figure, staged into public/media before the
build, and mediaUrl rewrites our raw URLs to that copy. The clips moved to a
prefix mediaUrl did not know, so the rewrite left them alone and the pages
went back to raw.githubusercontent, which stage-media.mjs explains at length
is not an asset host. The build passed, because nothing checked.
Now media.mjs knows the assets prefix and stage-media.mjs takes the WebM and
the posters from the assets checkout, resolved the way scripts/assets_dir.py
resolves it: PHONOMETRY_ASSETS_DIR in the environment or in .env, or the
sibling ../phonometry-assets. A missing checkout stops the build rather than
warning, because a site without its videos is broken and a warning at the
bottom of a prebuild log is how that would ship unseen.
The docs workflow makes that checkout at the commit assets.lock records:
sparse to /images/*.webm and /images/*.jpg, blobless so only those arrive,
344 files and 236 MB in ten seconds, measured. That is the pin. A clip
re-rendered on a branch is pushed to the assets repository the moment it is
rendered, and this is what keeps it out of main's pages until the branch
merges and the lock moves with it. assets.lock joins the workflow's trigger
paths for the same reason, since a re-render changes nothing else.
Built locally with the staged clips: no page hotlinks a clip, 81 pages serve
one from /media, all four variants of each with their posters, and the 344
files are in dist.
* The posters are WebP, cut from the clip directly, and make posters stamps
The poster stills were JPEG at ffmpeg's -q:v 3, 172 of them at 40,5 MB, and
they are loaded by every page that embeds a clip. Measured against those,
WebP at quality 85 is 46 per cent of the size at 44 to 48 dB PSNR, past where
a difference can be seen, so that is what _extract_poster writes now, with
libwebp straight from the WebM frame rather than through a JPEG, so nothing
is encoded twice. It removes the JPEG the old pipeline left beside a clip,
so a re-extraction cannot leave two posters of one clip behind. The 172
posters are 21 MB.
The site's optimize-images step leaves the animation posters alone. It
re-encodes every WebP at quality 82 and keeps the result when it is smaller,
which a quality-85 poster re-encoded at 82 would be, having lost twice. The
lossless raster figures are still re-encoded and, being smaller as they are,
still kept; that was measured too, lossy WebP comes out larger on them.
The poster extractor is inside every clip's fingerprint, so changing it
marked all 43 clips stale, and the honest answer is not a render: the frames
did not move. generate_posters now stamps the clips it re-extracted, the way
a render does, once per clip and not per variant, and its docstring says
what that is for and what it is not. The manifest is re-stamped here.
The assets repository holds the WebP posters at 76b9fdc588, published by
make posters through publish_assets.py, which wrote assets.lock; the PyPI
README pins to that commit. The docs workflow's sparse checkout and
stage-media.mjs take *_poster.webp, and the freshness check expects it.
A Spanish figure writes its decimal comma on every panel it draws, and two guards keep it that way (#777)
Every helper that writes text takes a language and defaults to English, so a call that forgets to pass it on raises nothing and puts an English decimal point into a Spanish figure. The plotting modules, the figure generators and the fiches now hand the caller's language to every helper that takes one, and localize_axes is called once per axes a plot builds rather than once per figure: a twin axis, a colorbar and the z of a 3-D panel each carry a formatter of their own, and each was shipping in English beside a panel already in Spanish. What decides whether an axis is localised is its formatter and not its scale, so a log axis of distances still writing its own numbers is reached and a pinned frequency axis is left to format_frequency_axis. A zoom inset and a contour colorbar, which the save-time pass of the figure generators cannot reach, are localised where they are built. The band column of the room criterion sheet and of the whole sound-power fiche family is written in the sheet's language too.
scripts/check_language_forwarding.py fails on any call to a helper that takes a language, made from anywhere a language is in scope, that does not pass it on. It reads module and class bodies as well as functions, follows a helper through a local alias and a functools.partial, names the shapes it cannot reach, keys an exemption by its line so one approved call cannot cover another, and reports a method name only some of whose namesakes take the language, which silently switched the untyped-receiver check off.
scripts/check_figure_decimal_point.py reads the committed Spanish figures and fails on a tick label that is a number with a point in it, whichever pass was meant to write the comma. It found six figures, regenerated here. Both run from the Makefile and in CI.
Fail the build on an annotation the reader cannot read (#649)
* Fail the build on an annotation the reader cannot read
Two ways a label fails the reader, and nothing in the pipeline could see
either. A label lying across a curve with nothing between them is hard to
read: the strokes of the letters and of the curve are the same weight, and on
the dark page close to the same lightness. A label something is drawn over is
not there at all. The staleness check compares a figure with its own
generator, the contrast checks measure fills and strokes rather than what
overlaps what, and `svg.fonttype = "path"` leaves no text node in the
committed file to look a label up in. So the question can only be put while
the matplotlib artists are alive.
`scripts/figure_annotation_audit.py` puts it there. During the generation run
it renders each figure a few more times with pieces of it taken away and
counts, for every label, the glyph pixels a stroke paints underneath and the
glyph pixels that never reach the page at all. Geometry cannot answer either:
a window extent is the font's line box, so a curve crossing it through the
ascender space above a row of lowercase letters touches nothing, and across
the corpus that is the dominant class of geometric hit. Both language editions
are measured, because Spanish prose is longer and grows into curves the
English strings clear; the light pass of each stands for its dark twin, which
is the same drawing in other colours and differs by one or two pixels.
What counts as ink is the part that took the measuring. A curve, a
`LineCollection` stem, a contour, a patch outline or its hatching, a curve on
a `twinx` sibling, and the arrow of a neighbouring annotation: measured on two
figures identical but for how the one stroke is drawn, the victim label scored
287 px as a `Line2D` and nothing at all as an `annotate(arrowstyle="-")`
across the same pixels, because hiding a label to build the ground takes its
arrow out of the render with it. The letters are switched off instead of the
label hidden, so the pointer stays. A label's own arrow reaching down to touch
its curve is the pattern this looks for and is kept out of its own count
twice: the arrow is a stroke, so it is suppressed alongside the curves, and
the glyph box it is measured in comes from an unbound `Text.get_window_extent`
and is the letters alone.
A patch is suppressed by taking its line width and hatch away rather than by
hiding it, so its fill stays: a filled region behind a label is a backing, and
the letters on it read perfectly. Counting fills as well scored the five
legible slit numbers of `metadiffuser_geometry` at 32 to 47 px.
An axes the figure does not list is walked too. `inset_axes` files its result
under the host, an annotation holds its own arrow, and a figure draws its own
text and legends after every panel, so each was invisible to a walk of
`fig.axes`. And a 3-D plate projects its labels inside their own draw and
restores the data coordinates on the way out, so a window extent asked for
afterwards put "Reflecting plane" at x = 12264.9 on a 1350 px canvas: all 72
labels of the two plates were dropped and the record said the drawings were
measured and clean. The box is now captured while the label draws, which is
the same answer for every label whose position does not move inside its own
draw, and the right one for the ones that do.
The measurement leaves no trace, which is what the whole design rests on: the
figure is written to disk immediately afterwards, and `check_figures.py`
compares that file against a fresh render of the same code, so a corruption
would appear on both sides. Every artist the measurement hides, unstrokes,
lifts or repaints goes back, and a test now pins it by comparing the rendered
SVG bytes and every mutable attribute of every artist family across a figure
built to hold one of each.
`scripts/check_figure_annotations.py` applies the rule and CI runs it after
`make graphs`. The two counts carry their own thresholds, because they are two
measurements: 45 px for a curve behind the letters, where the classes part one
pixel wide and the 32 to 44 band prints as an advisory rather than failing,
and 20 px for ink that never arrives, where the classes separate with two
pixels to spare. `figure_annotation_exemptions.txt` is where an annotation
that has to stay as it is gets recorded with its reason, and an entry that
stops being true fails the build as loudly as a new defect does.
Then the figures. Where a label was struck, it takes the opaque chip the
corpus already used in eighty-seven places, `COLOR_PANEL` with a `COLOR_GRID`
edge, with a zorder above the curves; where a label was painted over, the
zorder is the whole answer and the chip is not, since the chip is being
painted over too. Two labels moved to the axes that draws last instead, which
is the only thing that lifts a label over a `twinx` sibling.
A chip is not free, and a label that can move should move. It is opaque, so a
curve or a filled band behind it reads as passing behind it and loses nothing,
but a plotted point under it is a datum the reader no longer has. Thirteen
labels are therefore placed rather than backed.
Four of them stood on the data or on the frame to begin with. The hand-arm
band label of `vibration_weighting_family` sat over the 80 Hz end of the
whole-body bar and swallowed it, leaving the bar apparently stopping in
mid-air at the frequency the neighbouring label states; it now centres over
the part of its range that is its own. The RC-35 family label of
`room_noise_criteria` covered the 8 kHz marker of both series, so the family
labels now stand off the last band with the panel widened to hold them. The
`specimen face` note of `standing_wave_envelope` hung its chip over the bottom
of the panel and took the x-axis line and the 0,0 tick with it, and the first
ratio label of `image_source_anisotropy` broke the left spine for its own
height; both are pulled inside the panel.
Seven more are the same defect one step further in, and they were found by
widening the measure from the two ends of a stroke to every marker it draws,
interior points included. `structure_borne_conversion` swallowed the 125 Hz
cross and the 125 Hz triangle together, and that cross is one of the six wall
contact markers the figure's own corner note says reproduce the printed Table
I.8 columns; the note now stands above the head of the arrow it explains.
`sound_absorption_inversion` lay along the steep part of T2 and ate its 315 Hz
square, so the curve read as stopping at 250 Hz and starting again at 400 and
the shaded difference had a bite out of it; under both decays the panel is
empty for the whole width of the note, which therefore needs no backing at
all. `true_peak_intersample` brought the pad of its box down over two of the
twelve sample dots, on a figure whose title is that every sample misses the
peak. `sti_mtf_curves` lost the 10 Hz point of the 2.5 s curve to the note
along the floor of the left panel and the 8 and 10 Hz points of the SNR = 0 dB
curve to the note under the family on the right; the first moves into headroom
above the family, which both panels now carry, and the second above the curves
it scales. `duct_regenerated_noise` draws one curve shifted an octave per
flow, so all three have a point at 15.9 dB and the box sat on that row; it now
sits between that row and the one at 5.9. `enclosed_space_air_term` covered
the 1 kHz point of both office curves at once, because the two lie on top of
each other, and moves into the gap between the two rooms. The Spanish edition
of `ballistics_vs_duration` is wide enough to reach the 20 ms Table 4 circle
that the English one clears, and is lifted above it.
Two drawings this change never touched carry the same defect, and the same
measure finds them, so they are fixed here as well. The formula box of
`ship_source_level` was written into the corner both levels fall into and hid
the last five one-third-octave points of the source level and the last two of
the radiated noise level, so each curve read as stopping short of 20 kHz; it
moves to the top of the panel, which is empty by forty decibels above 2 kHz.
The formula box of `fluctuation_strength` covered the 0.5 Hz point where the
signal model's sweep begins, and is set clear of that corner.
Which of those is which was not a matter of opinion. Every chip in the corpus
was measured for the data ink under its footprint, for a stroke whose *end*
falls under it (a curve that runs out of both sides of a box reads as
continuous; one whose terminus is swallowed reads as stopping), for every
marker of every marked series, interior points as well as terminal ones, and
for a box that hangs over a panel that draws a frame. A marker is judged by
the ink it loses and not by where its coordinates fall, which is the
difference between a rounded box overlapping a point and a point that is gone:
the one label in the corpus a coordinate test still flags takes no ink off
anything. Afterwards no chip anywhere in the corpus takes ink off a plotted
marker, and none hangs over a framed panel; the four remaining buried
endpoints all predate this change.
In the library, a dimension written across hatched material now carries the
same chip, read off the page the plate is drawn on, and `page_is_dark` is the
one place that question is answered for the wave-field colormap and for the
chip alike.
* Retire the worked examples this branch's own figure moves invalidated
Four measurements in the audit's docstrings named figures the same branch then
moved, so each example describes a drawing that no longer looks like that.
ship_source_level was the second half of the argument that a clipped chip
corner is untidy rather than unreadable: 384 px of lost chip against 5 px of
lost letters. Its formula box now sits top right where nothing is drawn over
it and loses nothing at all. sii_masking_chain was the example of a label the
covered measure is silent on and the behind measure catches at 345 px; it is
silent on both now, because the annotation went to the twin axes and gained a
chip, so nothing nominates it. decay_range_bias appeared twice, as a covered
count of 0 on the light pass and as the corpus's one label-drawn-over-another,
and its corner box moved in this same change: the count is 5 px and the
label-over-label instance is gone.
The arguments all still hold, so they keep their paragraphs and lose the
instances that stopped supporting them. That a worked example can be
invalidated by a later commit in its own branch is worth the two sentences it
takes to say.
Two counts were also wrong. The chip is at a hundred and forty-seven call
sites under scripts/figures, not a hundred and forty-seven labels: several of
those sites sit in loops. And the gridline census is now given as an order
rather than a figure, because two independent counts of the same corpus gave
371 and 382 drawings and neither could say where they parted. The argument
never needed the digit.
* Make each process write its own recording, and read the page by its luminance
Two things here did not do what a reader would take them to do.
The two audit modules register their atexit writer once and remember that
they have. A process that arrives by fork inherits the flag but not the
handler: multiprocessing empties the child's atexit registry before the
target runs and puts its own exit function in place of it, so the inherited
writer is gone and the inherited flag stops the child ever registering
another. Everything such a child measured would go unwritten. Nothing in the
tree reaches that state today, because the figure run renders on spawned
workers, which start from a pristine interpreter, and the one fork, the four
language and theme variants of an animation clip, draws no figure and so
never calls audit(). It is guarded all the same, with an os.register_at_fork
hook that clears the tally and the flag: the failure is silent by
construction and the guard costs one call at import. The language recorder
gets the same hook, because its docstring already promised that each forked
animation variant writes its own fragment and that held only for as long as
the parent happened not to record first.
One test forks a child after the parent has recorded and demands the child's
fragment, holding the child's drawing and not the parent's. A second says
what a lost fragment would actually cost: the same recording passes whole
and is refused as "not a full run" with one fragment removed, so a dropped
fragment stops the run rather than letting a defect through.
page_is_dark weighted the raw sRGB channels and compared the sum against
0.5. Those weights belong to a linearised scale, so what it computed was
Rec. 709 luma, and the constant's comment called 0.5 the midpoint of a
luminance scale the arithmetic was not on. It now goes through
relative_luminance, and the threshold is derived rather than picked: white
ink on a page of luminance Y has a contrast ratio of 1.05 / (Y + 0.05) and
black ink has (Y + 0.05) / 0.05, so the page turns dark exactly where the
two are equal, at sqrt(1.05 * 0.05) - 0.05. That is the question both
callers are asking, since each chooses between one light answer and one dark
one: the centre of the wave-field colormap, and the chip behind a label. On
a grey the boundary moves from an sRGB channel value of 0.500 to 0.460.
Every page in the corpus sits far from it, and the figures regenerate to the
same bytes.
* Stop quoting counts that move, and retire one more rotted example
Five statements in this branch's own prose did not match the tree. Four are
counts and one is a reason, and between them they are the third round of the
same defect on this branch.
Two chip counts, "a hundred and forty-seven call sites" and "0.5 at
seventy-two of them, 0.4 at fifty-three", have now been written and corrected
twice each and were wrong both times. Nobody can recompute them the same way
twice, because the total moves with every label edited and depends on whether
a bare facecolor keyword counts beside the dict literals. They are gone: the
rule is what the reader needs, and the paragraph now says why the figure is
withheld so the next person does not helpfully put one back.
The covered threshold's worked example had rotted the same way the behind ones
did. two_tone_separation_es was the 21 px end of the boundary and this branch
moved that label, so it records clean now. The paragraph keeps the boundary and
the surviving example and says the group counts described the corpus before
the gate was applied to it.
The fork note gave the wrong reason for a right conclusion: it said the run
renders on spawned workers, which is false at --jobs 1 and on a two-core
machine, where the parent renders everything. What makes the case unreachable
is the other half of the sentence, that the fork draws no figure.
And the Makefile counted 2380 files where there are 2384. It no longer counts
them.
* Keep the fragment test blind to the exemptions file
A full audit marks every committed exemption without hits stale, and the
fragment test runs a full audit over synthetic fragments that hit nothing,
so the first legitimate entry in scripts/figure_annotation_exemptions.txt
would have failed a test about lost fragments. The test now audits against
an empty exemptions file of its own and says why.
A Spanish figure writes its decimal comma on every panel it draws, and two guards keep it that way (#777)
Every helper that writes text takes a language and defaults to English, so a call that forgets to pass it on raises nothing and puts an English decimal point into a Spanish figure. The plotting modules, the figure generators and the fiches now hand the caller's language to every helper that takes one, and localize_axes is called once per axes a plot builds rather than once per figure: a twin axis, a colorbar and the z of a 3-D panel each carry a formatter of their own, and each was shipping in English beside a panel already in Spanish. What decides whether an axis is localised is its formatter and not its scale, so a log axis of distances still writing its own numbers is reached and a pinned frequency axis is left to format_frequency_axis. A zoom inset and a contour colorbar, which the save-time pass of the figure generators cannot reach, are localised where they are built. The band column of the room criterion sheet and of the whole sound-power fiche family is written in the sheet's language too.
scripts/check_language_forwarding.py fails on any call to a helper that takes a language, made from anywhere a language is in scope, that does not pass it on. It reads module and class bodies as well as functions, follows a helper through a local alias and a functools.partial, names the shapes it cannot reach, keys an exemption by its line so one approved call cannot cover another, and reports a method name only some of whose namesakes take the language, which silently switched the untyped-receiver check off.
scripts/check_figure_decimal_point.py reads the committed Spanish figures and fails on a tick label that is a number with a point in it, whichever pass was meant to write the comma. It found six figures, regenerated here. Both run from the Makefile and in CI.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
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.
The five verify_* returned a dictionary, and two of them had a twin that wrapped it (#758)
* The five verify_* return a result object, and the two twins that wrapped one are gone
Every verifier in the library returns one of the 194 result dataclasses
except these five, which returned a bare dict[str, Any]. Two of them had a
public twin whose only job was to run the verifier and package what it gave
back: filters.filter_class_compliance and emission.intensity_class_compliance.
The verifier now returns the object itself, so there is one door onto each
computation instead of two, and the rows a caller reads come back on a name
rather than on a string key.
verify_filter_class returns the FilterComplianceResult and
verify_intensity_class the IntensityInstrumentComplianceResult that their
twins used to build, with .plot() and the accredited .report() fiche on them
as before. The two twins are gone.
The other three gain a result class of their own: WeightingComplianceResult,
QuasiPeakDynamicsResult and AircraftSystemComplianceResult. Each one pins the
summary it states against the rows under it, because the summary is the
conjunction, the minimum or the strictest class of a column a reader prints
beside it, and a sheet whose box disagrees with its own table is the one
reading a caller cannot correct.
Everything after the argument the call is about is keyword-only:
verify_filter_class took num_points positionally where its twin did not, and
verify_quasi_peak_dynamics took the ballistics second.
* Pin the three new verdicts against the rows under them, and publish the weighting result
* Say keyword-only where the table says it, and name the verifier in the pages that pointed at its twin
* Every class the edition defines has to be on the row, and on the sweep
A Spanish figure writes its decimal comma on every panel it draws, and two guards keep it that way (#777)
Every helper that writes text takes a language and defaults to English, so a call that forgets to pass it on raises nothing and puts an English decimal point into a Spanish figure. The plotting modules, the figure generators and the fiches now hand the caller's language to every helper that takes one, and localize_axes is called once per axes a plot builds rather than once per figure: a twin axis, a colorbar and the z of a 3-D panel each carry a formatter of their own, and each was shipping in English beside a panel already in Spanish. What decides whether an axis is localised is its formatter and not its scale, so a log axis of distances still writing its own numbers is reached and a pinned frequency axis is left to format_frequency_axis. A zoom inset and a contour colorbar, which the save-time pass of the figure generators cannot reach, are localised where they are built. The band column of the room criterion sheet and of the whole sound-power fiche family is written in the sheet's language too.
scripts/check_language_forwarding.py fails on any call to a helper that takes a language, made from anywhere a language is in scope, that does not pass it on. It reads module and class bodies as well as functions, follows a helper through a local alias and a functools.partial, names the shapes it cannot reach, keys an exemption by its line so one approved call cannot cover another, and reports a method name only some of whose namesakes take the language, which silently switched the untyped-receiver check off.
scripts/check_figure_decimal_point.py reads the committed Spanish figures and fails on a tick label that is a number with a point in it, whichever pass was meant to write the comma. It found six figures, regenerated here. Both run from the Makefile and in CI.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
A flag is written by name everywhere, and the clip gate learns which changes cannot move a frame (#760)
#759 closed the flags a caller could reach. This closes the rest, so ruff's
FBT family is selected outright instead of documented as absent: 202
signatures take the star, and the 296 calls that passed a bare True name the
parameter.
Every one of the 156 call sites the change forced was named rather than left
positional. Two of them no static gate sees: _cached_weighting_sos and
_cached_weighting_filter are lru_cache'd, so mypy reads the call through
_lru_cache_wrapper and checks nothing, and only the suite found them.
The one place a keyword-only parameter is not a rewrite: Callable[[...], R]
cannot say that a parameter is keyword-only. ColumnsBuilder in
_report/_insulation_fiche.py and a new LeftCellBuilder in
_report/ansi_s12_2.py become protocols, which say what the alias could not.
FBT003 came down to ax.grid(True, alpha=0.3) in 295 of its 296 sites.
matplotlib declares that parameter visible, so the sites say visible=True and
a test pins the name, because a matplotlib release that moved it would
otherwise surface as a wall of unrelated red. The one that was left is
np.bool_(True) in a test, a cast rather than a flag; it is np.True_ now.
The clips are not re-rendered, and none of them needed to be.
animation_fingerprint already normalises away what cannot move a frame, the
docstrings, the import order, the TYPE_CHECKING blocks, with the reason on
each. Where the keyword-only star sits is the same kind of thing: it decides
how a caller may write the call, not what the function computes. With that
normalisation the 43 fingerprints are identical either side of the signature
sweep, measured, not assumed. The manifest is re-stamped because the
fingerprint itself changed, and the stamp records the same clips it always
described.
scripts/check_public_flags.py goes: ruff now says the same thing on every
definition, and a guard that cannot fire is noise.
phonometry installs on Python 3.12 and on the numpy, scipy and matplotlib it really needs, and a CI job keeps those floors honest (#956)
phonometry now installs on Python 3.12, 3.13 and 3.14, and the version floors in the package metadata are the oldest releases the library's tests pass with on Python 3.12, each measured with every other floor installed beside it: NumPy 2.0.2, SciPy 1.14.0, matplotlib 3.10.7, reportlab 4.0.4, svglib 0.9.0, numba 0.60.0 and python-soundfile 0.12.0. A new minimum-versions job installs exactly those floors and runs the library's tests, Dependabot moves the pins and leaves the floors, and a one-element array passed where one number is asked for is now refused with a ValueError that names the parameter on every NumPy.
Every catalogue row has one shape and says what its source claims for each cell (#862)
* Every published catalogue cell is pinned to its last digit
The 1982 rows of the 23 published catalogues are dumped once, every float by
its repr, into tests/data/published_catalogues/baseline.json. A test takes
that baseline through every change listed in tests/catalogue_fingerprint.py
and asserts that the result is, field for field, what the library builds
today. The page generator's check sees the numbers only as the site prints
them; this sees the last digit of a float and a hedge moved from one field to
another, so a change to how a row is built has to say what it moved.
* Every catalogue row has one shape and says what its source claims
The rows of every published catalogue share one base, and phonometry.io now
publishes it: CatalogueRow, BandedRow, CatalogueError and CATALOGUE_BASES.
A row gains basis, what the source claims a value is (measured, declared,
calculated, estimated or extended), read through basis_of. The estimate had
two spellings, SolidMaterial.estimated with is_estimate and
OrthotropicWood.estimated with is_estimated; both are gone, and the 35 cells
Hopkins Table A2 and Rossing Table 15.5 mark as estimates hold "estimated"
in basis.
A value the page prints in another unit is no longer derived: the 116 cells
of Ver & Beranek Table 14.1 in degrees Fahrenheit and psi and the nine of
Long Table 7.1 in sabins keep the printed figure and unit in converted. A
cell the page leaves blank and prints on another row (Ver & Beranek Table
8.7, ASHRAE Table 30, Harris Chapter 32) names that row in carried.
is_derived now answers only for what the library computes. The published
catalogues page marks converted and carried cells as such, with the printed
figure on a converted one, and a test walks every hedged cell of every
catalogue to hold the page to it.
OrthotropicWood and PlateauMaterial take their fields by name only, and no
row class is slotted, because on Python 3.13 a slotted dataclass cannot call
super() without arguments from its own methods. No published value changes:
the fingerprint lists the 54 rows the move touched and nothing else moved.
* A catalogue cell no longer claims a unit or a derivation its page does not print
Long prints the six figures of his musician with no unit. The row now says
so in its note and why they are read as sabins, and `converted` is described
everywhere as the page's figure and its unit rather than as what the page
prints; the published catalogues page words its tooltip the same way. A
figure a packaged table prints with another SI prefix, such as the
megapascals of Rossing Table 15.5, keeps no `converted` entry, and the
docstring says so.
`carried` covers a value the page gives by reference to another row, which
includes the three Harris rows that print "Parecido al anterior" and no row
number, not only a cell left blank under a block. The two ASHRAE rows that
carry a diameter down no longer call it a derivation in their notes, and the
damping paragraph of the catalogues page no longer says converted cells are
marked as worked out.
The fingerprint compares every row as the text the baseline holds, so a
float served as an integer, or a flag served as a number, is a change. The
page generator refuses a cell whose hedges it cannot show, two on one value
or an estimate, a conversion or a carried value on an interval, a list or a
word, instead of publishing it with one of them missing; the test walks
every hedge of every published row to hold it to that.
The older unreleased notes that still named `is_estimated`, the `estimated`
field and cells marked derived now name `basis`, `basis_of` and `converted`.
The scripts and tests that import `CatalogueRow` take it from
`phonometry.io`, and the Spanish pages spell the sabin one way.
* An approximate interval keeps its tilde on the catalogue page
The porosity Cox Table 6.5 prints for granular vermiculite is an interval with
a tilde, and the page showed the bare interval. The range and bound kinds have
no mark of their own, so the tilde now goes into the cell text.
The fingerprint's migration test also requires every field outside the hedges
the step moves to stay equal, basis_of names all five bases, the baseline's
README states the terms that cover it, and the test that probed the
interpreter for the slotted super() failure is gone: Python 3.13.15 carries
the fix and 3.13.5 does not, so it tested the Python patch release, not the
library.
* A list of readings printed with a tilde keeps it on the catalogue page
Like an interval, a cell holding several readings has no mark of its own for
a tilde, so it goes into the text. The generator now refuses an approximate
hedge only on a printed word, which has nowhere to show it. No published cell
changes.
* A catalogue row refuses a basis outside the five words
The catalogue page and every reader of basis_of act on measured, declared,
calculated, estimated and extended only, so a row built with any other word,
such as a misspelt estimate, now raises CatalogueError instead of reaching
the page as a printed number.
Measure the noise of railway rolling stock by ISO 3095, on a reference track judged by EN 15610 and EN 15461 (#909)
Three new modules under environment.sources carry the ISO 3095 type test of a railway vehicle: the pass-by level and transit exposure level of a pressure record, the stationary, constant-speed, starting and braking results from their runs, the Annex A, C, D, E and G steps, and a single verdict on the reference track from the rail roughness of EN 15610 and the decay rates of EN 15461. Every result has .plot(), a new guide in English and Spanish walks a pass-by and a reference track through, the conformance report pins the limits of Figures 2 and 3, the positions of Figure 10 and the budget of Table G.2, and four printed defects of ISO 3095 Table G.1 and the EN 15610 listing are in the errata register.
4.0.0rc1, and the page a candidate needs to be tried (#761)
* 4.0.0rc1, and the page a candidate needs to be tried
Merging this bumps VERSION, which is what publishes: the workflow validates
the string, waits for a green CI on the same commit, builds, uploads and cuts
the tag. 4.0.0rc1 carries a pre-release marker, so it goes out marked as one
and pip leaves it alone without --pre.
A candidate exists to be tried, and until now anyone who tried this one met
an ImportError and a changelog of five and a half thousand lines. So the
candidate ships with the page that was missing, in English, in Spanish and in
the plain-Markdown mirror.
It is built on a measurement rather than on the changelog. Both trees were
imported and their public surfaces compared: 885 root names against 24, 46
public modules against 103, and every signature in common. That gives the
headline, which is that almost nothing was deleted. Ten names exist nowhere
any more. The rest moved, and the rule is one line: imports go down one
level. The page carries a resolver a reader can paste to find the new home of
any name rather than looking each one up.
What it spends its length on is the three places where the obvious fix is the
wrong one. air_density_iso and speed_of_sound_iso took kelvin and their
successors take Celsius: the name changed, so the import fails and you
notice, but the 293.15 you carry over does not fail, it returns 477.13 m/s
where the old call returned 343.29. Five aircraft and outdoor entry points no
longer take the ambient conditions as loose numbers at all, because the
parameter went into AerodromeAtmosphere, RotorcraftAtmosphere and
AtmosphericConditions. transmission_loss became propagation_loss and the
field inside the result went with it, .tl to .pl, which surfaces as an
AttributeError wherever the value is read rather than at the call.
Every mapping on the page was proved by running both trees. 394 were proposed
and 31 of those did not survive being checked, including all three above.
coverage_factor and expanded_uncertainty live in metrology/uncertainty.py,
which declares no __all__, so what is public there is exactly what
metrology/__init__.py re-exports by hand, and that list had nine of the
eleven. They are re-exported now, with their rows in the curated table.
The PyPI description still said the library was fractional octave-band
filters, weighting and sound level metrology. That was true of 3.x and names
none of the domains the library grew, and a description freezes at upload, so
it is rewritten here rather than left to the next release. license moves to
the PEP 639 form with license-files and the deprecated classifier goes, which
is four SetuptoolsDeprecationWarning per build down to zero.
[Unreleased] keeps the 5458 lines that describe 4.0.0. They are not moved
into the candidate's own heading, because they still promise things until 5.0
about shims this release removes, and that cleanup belongs to the final. The
candidate gets a short dated heading that says what it is and points at both.
This is also where the PyOctaveBand transition package stops. Its pin has
capped phonometry below the next major since it was published, so pip install
-U PyOctaveBand keeps resolving the 3.x line and nothing an installed user
has breaks. It cannot follow 4.0: it exists to keep import pyoctaveband
working, and 4.0 moved every name into the module of its domain. So
PyOctaveBand 2.2.0 is a page rather than a change of behaviour, much shorter
than the one it replaces, spending its length on the claim, on where the
bridge ends and on the links out. The shim's own FutureWarning now carries
the upgrade link too, which is the one line of this a user executes. The test
that failed the build the moment the tree left the pin is replaced by two
that hold the page to the decision: the requirement the prose prints has to
parse to the requirement the resolver reads, and while the tree is outside
the pin the page may not say that renaming the import is the whole of the
migration.
The version is stamped in six places: VERSION, CITATION.cff, the __version__
row of the curated API table, llms.txt, the conformance artefact and the
BibTeX block of the About page in three editions. The PyPI page is
regenerated so its repository links pin to v4.0.0rc1 rather than to the tree
of the previous release, which is also what closes the conformance badge that
has been a 404 on the published page since v3.3.0.
* Regenerate the llms artifacts after the About page was stamped
The citation blocks the About page carries are transplanted into
llms-full.txt and the start shard, and both were generated before the last
of the six version stamps landed, so they still said 3.3.0.
* The resolver answers for the names the root still publishes
whereis("Signal") came back empty, which the page reads as a name that no
longer exists anywhere. The loop only descended into the subpackages, so the
four names the root publishes on its own, Signal, PhonometryWarning,
ReportMetadata and __version__, fell through it. They resolve to phonometry
now, the printed example shows one of them, and the prose says so. A name that
really is gone still comes back empty: whereis("octavefilter") is [].
The Start index counted six pages and did not link the new one, in the three
editions. Seven now, with the question the guide answers.
Refuse a temperature at or below absolute zero across the tree (#683)
* Refuse a temperature below absolute zero, and the pole 0,15 degC above it
sea_water_sound_speed(-273.15, 35.0, 0.0) returned -31 457 m/s, a negative
speed of sound. sound_speed_profile checked only that its temperatures were
finite, so -300 degC reached [-44 562, -45 333]. And seawater_absorption at
-300 degC returned 0,495 dB/km, which is the one that matters: nothing about
that number says where it came from, beside the 0,0185 of real cold water.
require_above_absolute_zero says it once now, and the five sites that had
written it by hand use it. One was spelling its message with a Unicode minus
and a degree sign where the rest of the tree uses ASCII.
Three guards deliberately do not migrate, and a test pins them by name. They
protect the pole of the formula their own clause prints, sqrt(273 + theta) and
1245/(273 + t), with the 273 the standard prints and not the physical 273,15.
Their pole sits 0,15 degC higher, so migrating them would open a band of a
sixth of a degree that clears the check and then overflows. The triage that
found this listed one of the three for migration; reading it showed it uses
_ANNEX_A_KELVIN = 273.0.
Francois-Garrison needs both guards, for that reason. Its band was raising
OverflowError and ZeroDivisionError, neither of which names the argument.
* Take the array temperatures through the same guard
Five of the air helpers and the ocean sound-speed profile take ArrayLike
temperatures, so the scalar helper could not serve them and each had written
the bound out again. require_above_absolute_zero_array is the companion, and
the profile is where it matters most: one element below absolute zero among a
hundred good ones poisons the whole returned array, and nothing about the
result points at which element it was.
Also picks up a match= this branch's first pass missed, in the intensity sound
power tests.
* Reflow the two test files the message change made too wide
The match= patterns grew past the line length when the guards were unified,
and this branch committed them unformatted.
* Say that absolute zero itself is refused, not only what is below it
The guard rejects `<= -273.15 degC`, so the boundary is inside the refusal, but
five places described it as refusing what is *below* absolute zero. That reads
as if -273,15 degC itself computes, which is the one temperature where the
question is sharpest: Eq. (6) at exactly absolute zero returns a speed of sound
of zero, not a negative one, so a reader taking the prose at its word would
expect it through.
`impedance_adjustment` also gained the guard without gaining the line that says
so: its `:raises ValueError:` still listed only the pressure and finiteness.
* Refuse a complex temperature instead of taking its real part
`np.asarray(z, dtype=float64)` does not fail on a complex array. It drops the
imaginary part and emits a `ComplexWarning`, which is a warning and not an
error, so `np.array([1 + 2j])` reached the guards as `[1.0]` and satisfied
every one of them. A `[1 + 2j]` written as a Python list did raise, because
that path fails the conversion outright, so the two spellings of the same input
disagreed with each other.
The check sits in the shared conversion, which means it also closes the same
hole in `require_positive_array`, `require_finite_array`, `require_per_band`
and `require_1d_signal` rather than in the temperature guard alone. Fifteen
regression cases: five validators against the three spellings.