Skip to content
This documentation describes version 4.0.0, which is not released yet. The current version on PyPI is 3.3.0 and does not carry everything described here.

Upgrading from 3.3 to 4.0

4.0 is a hard break with no compatibility layer. Almost nothing was deleted: of the 885 names the root exported in 3.3.0, ten exist nowhere any more and the rest simply live at a named address instead of a flat one. So most of the work is an import sweep, and this page is mostly a map.

Read the first two sections and you can fix the imports. Read the third even if you are in a hurry, because in three places the obvious fix is the wrong one, and one of those three changes a number without raising anything.

In 3.3.0 the root re-exported the whole library, so from phonometry import laeq worked for almost every name. Today the root exports twenty-five things: the twenty-one subpackages, the three classes that belong to no single domain (Signal, ReportMetadata, PhonometryWarning) and __version__.

import phonometry
print(len(phonometry.__all__)) # 25
print("laeq" in phonometry.__all__) # False
from phonometry.signals import laeq # this is the new spelling

Measured across the whole surface, 872 of those 885 root names are published today by exactly one subpackage, none by two, and none needs a path deeper than phonometry.<subpackage>. Three more are still at the root, where they always were. So the swap is unambiguous: find the subpackage, import from it.

The attribute form was never removed, which is the smallest possible diff for code that reached through the root:

from phonometry import filters
_, _, _, labels = filters.nominal_frequencies(1)
print(labels)

Rather than search the reference for each one, ask the installed package:

import importlib
import pkgutil
import phonometry
def whereis(name):
"""Every public module that publishes `name`, shallowest first."""
found = ["phonometry"] if name in phonometry.__all__ else []
for attribute in phonometry.__all__:
package = getattr(phonometry, attribute)
if not hasattr(package, "__path__"):
continue
modules = [package.__name__]
modules += [m.name for m in pkgutil.walk_packages(package.__path__, package.__name__ + ".")]
for module_name in modules:
if any(part.startswith("_") for part in module_name.split(".")):
continue
try:
module = importlib.import_module(module_name)
except ImportError:
continue
if name in getattr(module, "__all__", ()):
found.append(module_name)
return sorted(found, key=lambda path: path.count("."))
print(whereis("laeq")[0]) # phonometry.signals
print(whereis("reverberation_time")[0]) # phonometry.room
print(whereis("sensitivity")[0]) # phonometry.metrology
print(whereis("Signal")[0]) # phonometry

It walks the tree in about two seconds. When it returns several paths the shallowest one is the canonical import: a subpackage re-exports what its own modules publish, and that shallow path is the one the guides and the tests use. The four names the root still publishes on its own, Signal, PhonometryWarning, ReportMetadata and __version__, come back as phonometry itself. An empty list means the name is in the last section of this page.

Five of the thirty modules that changed path did not move to one new address. They were divided, and a reader who follows only the first half of the split gets an ImportError for the rest. These are worth checking one by one, and the private renderer package is listed with them because the same thing happened to it:

3.3.0 moduleWhere its contents are now
phonometry.impedance_tubematerials.absorbers.impedance_tube (two-microphone transfer function), materials.absorbers.four_microphone (TransferMatrix, transfer_matrix_one_load, wave_decomposition, face_quantities, air_layer_transfer_matrix), materials.absorbers.standing_wave, and fluids
phonometry.scattering_diffusionmaterials.diffusers.reverberation_room_scattering for the reverberation-room part (ScatteringResult, ScatteringUncertainty, BASE_PLATE_BANDS and the rest), materials.diffusers.scattering_diffusion for the nine free-field names
phonometry.materials.porous_absorbermaterials.absorbers.porous for the bulk-material models, materials.absorbers.layered for the whole multilayer API (layered_absorber, AirLayer, MembraneLayer, PerforatedPlateLayer, MicroperforatedPlateLayer, LayeredAbsorberResult)
phonometry.metrology.spectrasignals.spectra, plus signals.multitaper (multitaper_psd) and signals.windows (window_metrics)
phonometry.compliancefilters.compliance (FilterComplianceResult, class_limits, verify_filter_class), filters.weighting_compliance (verify_weighting_class, weighting_class_limits), aircraft.measurement_system (verify_aircraft_noise_system)
phonometry._plottingphonometry._plot.<domain>: the 82 renderers were split per domain, so plot_airborne_insulation is in _plot.building and plot_age_threshold in _plot.hearing. Private either way, and .plot() on the result is the supported route

The other twenty-five moved whole, and the resolver above finds them without reading this table. Each of the first three was reachable by two paths in 3.3.0, the flat one and the one under its package, and both are gone.

Three where the obvious fix is the wrong one

Section titled “Three where the obvious fix is the wrong one”

Everything else on this page announces itself the moment you run it, and the fix is the one it looks like. These three are not that.

air_density_iso and speed_of_sound_iso documented and consumed their temperature in kelvin. Their successors take degrees Celsius, like the rest of the library. The names changed, so the import fails and you will notice, but the argument you carry over is the problem: 293.15 is a legitimate-looking number in either unit.

from phonometry.materials import speed_of_sound_iso10534
print(round(float(speed_of_sound_iso10534(temperature_c=20.0)), 5)) # 343.28784
print(round(float(speed_of_sound_iso10534(temperature_c=293.15)), 5)) # 477.13003

343.29 m/s against 477.13 m/s: a 39 % error, no exception, and every absorption coefficient computed from it is wrong by a plausible-looking amount. Convert the value, do not just rename the keyword.

3.3.04.0
impedance_tube.air_density_iso(T_kelvin, p_kPa)materials.air_density_iso10534(temperature_c=T_kelvin - 273.15, atmospheric_pressure_kpa=p_kPa)
impedance_tube.speed_of_sound_iso(T_kelvin)materials.speed_of_sound_iso10534(temperature_c=T_kelvin - 273.15)

With the conversion applied both return exactly what they returned in 3.3.0, bit for bit.

scattering_diffusion.speed_of_sound took Celsius, and its replacement answers differently

Section titled “scattering_diffusion.speed_of_sound took Celsius, and its replacement answers differently”

The ISO 17497-1 helper of the same name took degrees Celsius, so no conversion is needed there. The catch is the replacement. fluids.air() uses a fuller model of the medium than the ISO 17497-1 one-liner, and at 20 °C it answers 343.99 m/s where the old helper answered 343.20 m/s. That is small, and it is not nothing if you are reproducing a measurement made with the old number. ISO 17497-1 clause 8 prints the formula if you need that clause exactly.

The tl field of an underwater result is pl

Section titled “The tl field of an underwater result is pl”

transmission_loss became propagation_loss, which the import catches, and the field inside the result was renamed with it. That one does not raise: a PropagationLossResult has no .tl, so the AttributeError arrives wherever you read it, which may be far from the call.

from phonometry.underwater.propagation.closed_form import propagation_loss
result = propagation_loss([1000.0], 1000.0)
print(round(float(result.pl[0]), 5)) # 60.06013, the number .tl used to hold

The same rename reaches underwater.sonar_equation, whose transmission_loss parameter is now propagation_loss.

Five entry points no longer take the ambient conditions as loose numbers. Renaming the keyword is not enough there, because the parameter is gone entirely:

3.3.04.0
airport_noise.event_level(..., temperature=15.0, pressure=101.325)event_level(..., atmosphere=AerodromeAtmosphere(temperature_c=15.0, atmospheric_pressure_kpa=101.325))
airport_noise.noise_contour(...)the same, with AerodromeAtmosphere
rotorcraft_noise.rotorcraft_event_level(..., temperature=15.0, relative_humidity=70.0)rotorcraft_event_level(..., atmosphere=RotorcraftAtmosphere(temperature_c=15.0, relative_humidity_percent=70.0))
rotorcraft_noise.rotorcraft_noise_contour(...)the same, with RotorcraftAtmosphere
outdoor_propagation.predicted_receiver_level(..., humidity=70.0)predicted_receiver_level(..., atmosphere=AtmosphericConditions(relative_humidity_percent=70.0))

The per-point rotorcraft overrides went the same way: bank_angle and path_angle are now RotorcraftTrackState(bank_angle_deg=..., path_angle_deg=...).

from phonometry.aircraft.airport_noise import AerodromeAtmosphere
atmosphere = AerodromeAtmosphere(temperature_c=15.0, atmospheric_pressure_kpa=101.325)
print(atmosphere.temperature_c, atmosphere.atmospheric_pressure_kpa)

A parameter that names a quantity carries its unit

Section titled “A parameter that names a quantity carries its unit”

Everywhere else, the ambient conditions kept their place and gained a suffix. The rule is that a public parameter naming a dimensional quantity ends in the unit the caller types, because a number is where a unit is lost and no guard can catch it by magnitude: 101.325 and 101325 are both legitimate pressures in this library.

3.3.04.0
temperature=temperature_c=
pressure=, static_pressure=, barometric_pressure=, ambient_pressure=, atmospheric_pressure=atmospheric_pressure_kpa=, or _pa= where the clause is written in pascals
humidity=, relative_humidity=relative_humidity_percent=
diameter= and its thirteen compoundsdiameter_m=
angle=, angles=, bank_angle=, path_angle=, grazing_angle=, launch_angles=_deg= or _rad=, whichever the function reads
runway_gradient=runway_gradient_ratio=
sound_speed_gradient=sound_speed_gradient_per_s=

One case is worth naming because it was a trap rather than an inconvenience. critical_angle was a field in radians on one result and a parameter in degrees on the two functions one screen below it, which is the same word for two things 57 times apart. It is critical_angle_rad on the result and critical_angle_deg on the parameters now.

transmission_loss= became propagation_loss= only in the underwater sonar and numerical-propagation results. ReactiveSilencerResult and SoundReductionResult still call theirs transmission_loss=, correctly: that is what the standards behind them call it.

The EN 29052-1 functions of materials follow the same rule, with the unit of each quantity at the end of its name:

3.3.04.0
apparent_dynamic_stiffness(resonant_frequency=, total_mass_per_area=)resonant_frequency_hz=, total_mass_per_area_kg_m2=
enclosed_gas_stiffness(thickness=)thickness_m=
installed_dynamic_stiffness(apparent_stiffness, airflow_resistivity, gas_stiffness=)apparent_stiffness_n_m3, airflow_resistivity_kpa_s_m2= by name only, gas_stiffness_n_m3=
natural_frequency(dynamic_stiffness=, mass_per_area=)dynamic_stiffness_n_m3=, mass_per_area_kg_m2=
floating_floor_resonance(resonant_frequency=, total_mass_per_area=, floor_mass_per_area=, airflow_resistivity=, thickness=)resonant_frequency_hz=, total_mass_per_area_kg_m2=, floor_mass_per_area_kg_m2=, airflow_resistivity_kpa_s_m2=, thickness_m=

The airflow resistivity is the one that earns the keyword. EN 29052-1 thresholds it in kPa·s/m², and every other flow resistivity in the library is in Pa·s/m², a unit a thousand times smaller, so a Pa·s/m² figure passed by position landed in the wrong branch of clause 8.2 without a word; it is written by name now. And below 100 kPa·s/m², installed_dynamic_stiffness no longer reads a missing gas term as zero: 3.3.0 returned from installed_dynamic_stiffness(20e6, 50.0), Formula 6 with its second term dropped, and 4.0 raises and asks for .

Two rules made 37 signatures keyword-only. A condition or an option that carries a default is written by name, and every public boolean flag is keyword-only. Both exist because the call site is where the meaning is lost: air_attenuation(freqs, 20.0, 50.0, 101.325) is a temperature, a humidity and a pressure in an order nobody remembers, and bank.filter(x, True) says nothing about which of five flags was thrown.

The failure is loud, which is the point:

import numpy as np
from phonometry.signals import leq
x = np.random.default_rng(1).standard_normal(4800) * 0.01
try:
leq(x, 48000)
except TypeError as error:
print(error) # leq() takes 1 positional argument but 2 were given

That call is worth a second look. leq never had an fs, because an energy average does not need one, so in 3.3.0 the 48000 silently bound to calibration_factor and the answer came back 93.6 dB high with no complaint. Now it is a TypeError, which is what it always should have been.

print(round(float(leq(x, dbfs=True)), 3))

The public names had drifted into several spellings of the same quantity, and 4.0 keeps the one the majority already used. The rule for the quantities counted in the plural is the one most of the API followed: a field or a parameter that holds an array is frequencies, times, distances or levels, and one that holds a single value is singular. A vectorised argument that takes either a number or an array keeps its name.

3.3.0 or 4.0.0rc14.0
sound_speed (control valves, silencer measurement, Weston regimes, ambient noise, FluidSeabed, SoundSpeedProfile, ShipSourceLevelResult)speed_of_sound
c= and rho= on sound_intensity, c= on the two phase-mismatch conversions, phase_mismatch(), absorption_coefficient_uncertainty and monopole_source_levelspeed_of_sound=, density=
speed_of_sound_m_s= on thickness_critical_frequency_productspeed_of_sound=
sample_rate on the FDTD SignalSource, STOIResult and impact_force_exposure_levelfs
freq= on linkwitz_riley and adaptation_term_kcfrequency= and frequencies=
.frequency on the results that carry a frequency axis: IntensityResult, FieldIndicators, IntensityInstrumentComplianceResult, RoomAcousticsResult, the five auditorium results, ImpedanceTubeResult, PorousMediumResult, LayeredAbsorberResult, DiffuseFieldAbsorptionResult, BiotWavesResult, SlitResonatorAbsorberResult, MetadiffuserResult, TransferMatrix, the two valve-noise results, AircraftBandAttenuation, AmbientNoiseResult, ShipTrafficSpectrum.frequencies
frequency= holding an array: the flanking-transmission functions, internal_spectrum, pipe_transmission_loss, expander_noise, transfer_matrix_one_load, transfer_matrix_two_load, TransferMatrix.plotfrequencies=
.time on ZwickerLoudness, EcmaLoudness, MooreGlasbergTimeVaryingLoudness, the ECMA-418-2 tonality, roughness and fluctuation-strength results, DecayCurve, QuasiPeakResult.times
.level on DecayCurve, NpdLevelResult, NoiseContourResult, RotorcraftNoiseContourResult, LowFrequencyResult, DuctPathStage, LateLateralResult, TransferStiffnessResult.levels
.distance on NpdLevelResult and RotorcraftEventResult.distances
level=, measured_level=, operational_level= on sti_from_impulse_response, stipa, sti_adjusted_for_levels and STIResult.adjusted_for_levelslevels=, measured_levels=, operational_levels=
.passed on AircraftSystemComplianceResult, QuasiPeakDynamicsResult, HeavyImpactSourceCheck, RigidMassCalibrationResult.passes, as on every other verifier
MicrophoneNoise(weighting="CCIR")MicrophoneNoise(weighting="468"), the spelling weighting_filter and weighted_thd use for the ITU-R BS.468-4 curve

times, levels = decay_curve(ir, fs) still unpacks the decay curve, in the same order. The FDTD solvers keep c and rho, which name the maps their equations are written in.

Every result’s .plot() now reads plot(ax=None, *, ...): the axes first, and every other argument by name, language included. Three did not start with the axes in 3.3.0, so plot(ax) on them handed the axes to the wrong parameter:

3.3.04.0
LoudspeakerCharacteristics.plot("impedance", ax).plot(ax, quantity="impedance")
MicrophoneCharacteristics.plot("noise", ax).plot(ax, quantity="noise")
TransferMatrix.plot(f, rho_c, ax).plot(ax, frequencies=f, characteristic_impedance=rho_c)

Six results that are newer than 3.3.0 took language by position in 4.0.0rc1: the four hearing-protector ratings and the two low-frequency intensity results. plot(ax, "es") on those is plot(ax, language="es") now, as it is everywhere else.

Verdicts are result objects, not dictionaries

Section titled “Verdicts are result objects, not dictionaries”

The five verify_* functions returned dict[str, Any] and now return a frozen dataclass, like every other result in the library. Two public wrappers that existed only to package that dictionary went with the change, since the verifier now returns the object itself.

3.3.04.0
verify_filter_class(bank)["overall_class"]verify_filter_class(bank).overall_class
verify_weighting_class(wf)["overall_class"]verify_weighting_class(wf).overall_class
verify_intensity_class(...)["bands"]verify_intensity_class(...).bands
verify_quasi_peak_dynamics()["passed"]verify_quasi_peak_dynamics().passes
verify_aircraft_noise_system(...)["passed"]verify_aircraft_noise_system(...).passes
filter_class_compliance(bank)verify_filter_class(bank), which returns the same FilterComplianceResult
from phonometry.filters import OctaveFilterBank, verify_filter_class
verdict = verify_filter_class(OctaveFilterBank(48000, fraction=1, limits=[125, 4000]))
print(verdict.overall_class, len(verdict.bands))

verify_running_rms_decay is newer than 3.3.0, and in 4.0.0rc1 it returned a bare bool, which threw away the printed interval and the measured time it had judged. It now returns a RunningRmsDecayVerification that keeps both: the verdict is .passes, beside .measured_time_s, .printed_time_s, .tolerance_s and the interval as .lower_time_s and .upper_time_s. The object refuses to be read as a truth value, so an if verify_running_rms_decay(...): written against the release candidate raises TypeError instead of passing every meter.

The filter bank returns a result, not a tuple

Section titled “The filter bank returns a result, not a tuple”

octave_filter() and OctaveFilterBank.filter() returned two items normally and three when sigbands=True asked for the band waveforms. The length of the answer depended on a keyword, which is why the pair needed twelve @overload declarations to be typed at all, and why _, _, bands = bank.filter(...) was a line you had to count commas in.

They now return an OctaveFilterResult with named fields, like every other computation in the library.

3.3.04.0
spl, freq = octave_filter(x, fs)r = octave_filter(x, fs), then r.levels and r.frequencies
spl, _ = octave_filter(x, fs)octave_filter(x, fs).levels
_, freq = octave_filter(x, fs)octave_filter(x, fs).frequencies
_, _, bands = bank.filter(x, sigbands=True)bank.filter(x, sigbands=True).require_bands()
import numpy as np
from phonometry import filters
result = filters.octave_filter(np.zeros(8000) + 1e-6, 8000, fraction=3)
print(len(result.frequencies), result.bands)
# 25 None

bands is None unless the call asked for sigbands=True, and levels is None for a call made with calculate_level=False. Code that needs either one can say so with require_bands() or require_levels(), which return the value or raise naming the argument that was missing, rather than handing back a None that fails several lines later.

The result also draws itself, so the band spectrum is one call:

ax = result.plot()

Waveforms inside results come back as the Signal they came from

Section titled “Waveforms inside results come back as the Signal they came from”

A transform handed a Signal has returned a Signal since the contract was written, but five result objects still stored their waveform as a bare array next to a loose fs. They now keep the type the input arrived as:

FieldIs a Signal when the input was one
envelope(...).signalat the input’s rate
time_synchronous_average(...).period_waveformat the input’s rate
resample_signal(...).signalat fs_new, which then has to be a whole number of hertz
align_impulse_responses(...).aligned, .referenceeach one if its own input was
underwater.pile_strike_metrics(...).pressureat the input’s rate

A bare array in still gives a bare array out, so code that passes arrays does not notice. Code that passes a Signal and does arithmetic on one of these fields goes through np.asarray, as it does for any Signal, and a calibrated input comes back with calibration_factor=1.0, so the field feeds the next function in pascals without the factor being applied again.

io.read_blocks is newer than 3.3.0, and in 4.0.0rc1 it yielded bare float64 arrays. It now yields a Signal per block, carrying the rate, the calibration, the channel labels and the provenance io.read gives, and it takes the same calibration_factor= and reads the same sidecar. Two things change for a loop written against the release candidate. A block from a calibrated file reaches the filters and the level functions in pascals, so a calibration_factor applied by hand on top of it counts twice. And arithmetic on a block, or on what a filter returns for one, goes through np.asarray, as it does for any Signal.

Every table the library publishes at module level is read-only in 4.0. In 3.3.0 published tables were plain dictionaries and published arrays took in-place arithmetic, so a line such as REFERENCE_CURVE[500] = 0.9 or BAND_IMPORTANCE *= 2 changed the number for every caller that ran after it in the same process, and nothing raised. The dictionaries are now types.MappingProxyType, including the ones nested inside another table, and the arrays have their writeable flag cleared. Reading is unchanged; writing raises TypeError on a table and ValueError on an array.

Code that adjusted a published table has to copy it first:

from phonometry.materials.absorbers import REFERENCE_CURVE
from phonometry.speech.sii import BAND_IMPORTANCE
curve = dict(REFERENCE_CURVE)
curve[4000] = 1.0
weights = BAND_IMPORTANCE.copy()
weights[0] = 0.0
print(REFERENCE_CURVE[4000], curve[4000], weights[0], BAND_IMPORTANCE[0])
# 0.9 1.0 0.0 0.0083

A table with tables inside it needs each level copied, as in {area: dict(rows) for area, rows in GUIDE_VALUES.items()}. The same copy is needed before a table is pickled, deep-copied or written out as JSON, because a read-only mapping supports none of the three.

A result keeps its own copy of the arrays it was given

Section titled “A result keeps its own copy of the arrays it was given”

In 3.3.0 and in 4.0.0rc1 a result kept the very array it was handed whenever that array was already of the type the function reads, float64 for levels and frequencies, because np.asarray hands its argument back unchanged in that case. Changing the caller’s array afterwards, levels[0] = 0.0 or frequencies *= 2, changed a result already computed and perhaps already printed. In 4.0 every array that a function or a method of the library keeps from the arguments it was given is a copy of its own, made read-only like the published tables, and the caller’s array is left as it was. That holds for the objects that keep arrays to hand out later as well: the frequency grid of a noise_control.SilencerChain, the axes and flight paths an aircraft.AnpDatabase is built from, and the medium maps of simulation.FDTD2D and simulation.ElasticFDTD2D, whose c, rho, c_p and c_s now stay the ones the time step was computed from. A Signal keeps a copy of its own samples too, which stays writeable for processing in place, as the samples of a file read with io.read always were. A result record built by hand, by calling its class directly, makes the same copy: every public record that can hold an array copies the arrays it is handed when it is built, whoever builds it, also inside a tuple, a list or any mapping. A record held inside another one is kept whole and answers for its own arrays, and a Signal is no exception: the seven results that hand a waveform back as a Signal (filters.OctaveFilterResult, signals.EnvelopeResult, signals.ResampledSignalResult, signals.AlignedImpulseResponseResult, signals.SynchronousAverageResult, environment.DirectSoundSubtraction and underwater.PileStrikeResult) hold one of their own when the library builds them, whose samples stay writeable like any Signal’s, and hold the caller’s own when built by hand around it. Every other array a result holds refuses in-place writes, the ones it computed as well as the ones it was given, where in 3.3.0 most computed arrays took them. Writing into one now raises ValueError, so code that edited one in place has to copy it first:

import numpy as np
from phonometry import io, room
levels = np.array([60.0, 58.0, 55.0, 52.0, 50.0, 48.0, 45.0, 42.0, 40.0, 38.0])
nc = room.noise_criterion(levels)
levels[0] = 0.0
samples = np.zeros(4)
signal = io.Signal(samples, fs=48_000)
samples[0] = 1.0
measured = np.array([1.0, 0.5, 0.25, 0.125])
response = room.ImpulseResponseResult(ir=measured, fs=48_000, method="farina")
measured[0] = 0.0
edited = nc.levels.copy()
edited[0] = 62.0
print(nc.levels[0], nc.levels.flags.writeable, response.ir[0], response.ir.flags.writeable)
print(signal.data[0, 0], signal.data.flags.writeable)
# 60.0 False 1.0 False
# 0.0 True

A catalogue row says what its source claims for each cell

Section titled “A catalogue row says what its source claims for each cell”

The published catalogues of materials are new in 4.0: neither 3.3.0 nor 4.0.0rc1 had them. They changed shape once on the way to the final release, and code written against the main branch in between needs the changes below. Every row of every catalogue now shares one base, io.CatalogueRow (with io.BandedRow for a row that prints one value per band), and the same three questions have the same answer on any of them.

Before4.0
SolidMaterial.estimated, OrthotropicWood.estimatedrow.basis, which maps each footnoted field to "estimated"
SolidMaterial.is_estimate(field), OrthotropicWood.is_estimated(field)row.basis_of(field) == "estimated"
row.is_derived(field) on a value the page gives in a unit the row does not hold (the °F and psi of Ver & Beranek Table 14.1, the sabins of Long Table 7.1)field in row.converted; row.converted[field] is the page’s figure and its unit, such as ("3e5", "psi")
row.is_derived(field) on a value the page gives by reference to another of its rows: a cell left blank under a block (Ver & Beranek Table 8.7, ASHRAE Table 30) or a description that reads “Parecido al anterior” (Harris Chapter 32)field in row.carried; row.carried[field] names the row
OrthotropicWood(...) and PlateauMaterial(...) with their own fields by positionevery field by name, as on every other row
from phonometry._internal.catalogue import CatalogueRowfrom phonometry.io import CatalogueRow

is_derived now answers only for what the library computes and could compute again from the row’s own cells, such as a bar speed worked out from a modulus and a density. A value the page gives in another unit is still the page’s number, and so is one it gives by reference to another of its rows, so neither is derived any more. basis_of answers with the field’s own entry, else the row’s, else an empty string, which means the source does not say how the number was obtained; the five things a source can claim are in io.CATALOGUE_BASES.

from phonometry import solids
board = solids.PUBLISHED_SOLIDS["hopkins-2007-table-a2/plasterboard_natural_gypsum"]
ear = solids.damping_named("EAR C-1002")[0]
print(board.basis_of("poisson_ratio"))
# estimated
print(ear.converted["youngs_modulus_max_pa"], ear.is_derived("youngs_modulus_max_pa"))
# ('3e5', 'psi') False

The resilient layers became catalogue rows in the same change. A ResilientLayer is an io.CatalogueRow like every other row, keyed and credited the way the others are:

Before4.0
PUBLISHED_RESILIENT_LAYERS["mineral_wool_rock_60_30"], resilient_layer("mineral_wool_rock_60_30")"hopkins-2007-table-a3/mineral_wool_rock_60_30", the "<table>/<row>" key of every catalogue
layer.attributed_to, a stringlayer.attributed_to["row"], a mapping as on every row
ResilientLayer(...) with the stiffness, the density and the thickness requiredevery quantity optional, and apparent_dynamic_stiffness_n_m3 beside dynamic_stiffness_n_m3 for a source that gives only the apparent
layer.natural_frequency(mass_per_area=...)layer.natural_frequency(mass_per_area_kg_m2=...); a layer that holds only also takes airflow_resistivity_pa_s_m2= and gas_stiffness_n_m3=, and raises io.CatalogueError without the first

Hopkins Table A3 prints , the stiffness of the installed layer as the book’s own list of symbols defines it, so its fifteen rows still hold dynamic_stiffness_n_m3 and return the natural frequencies they returned before. Four of them hold a density the page prints once for their block and leaves blank on their own line, and layer.carried["density_kg_m3"] now names the row that prints it; the density is the same.

from phonometry import materials
rebond = materials.resilient_layer("hopkins-2007-table-a3/rebond_foam_64_20")
print(rebond.attributed_to["row"], round(rebond.natural_frequency(mass_per_area_kg_m2=120.0), 1))
# Hopkins and Hall (2006) 43.6

A catalogue row checks itself when it is built

Section titled “A catalogue row checks itself when it is built”

Every catalogue row is checked in its constructor, whether the library builds it from a packaged table or a caller builds it by hand, and a cell nothing downstream could read raises io.CatalogueError, naming the row and the field. Code written against the main branch in between that relied on any of these being accepted has to change:

Accepted before4.0
NaN, an infinity, True or a text such as "0,97" in a numeric fieldCatalogueError; a cell the page leaves empty is None
a fraction in a whole-number field (year), 1 in a flag field (has_section_drawing), a number in a text fieldCatalogueError
an empty name or source, or a hedge whose text is empty, as in misprinted={"porosity": ""}CatalogueError; why_missing answered "" for that cell, which hid the hedge
a hedge keyed by a field the class does not have, as in ranges={"no_such_field": ...}, or a hedge on a number (ranges, approximate, unquantified, …) keyed by a text fieldCatalogueError, naming the hedge and the key
bounded_above or bounded_below on a field with no range; a range, or an interval among the reported readings, with an end that is not finite or with its low end above its high one; a reported list with nothing in it; an uncertainty below zeroCatalogueError
a value beside misprinted, unquantified, not_derivable or reported on the same field; a converted or carried entry on a field that holds nothingCatalogueError: the first four say there is no value to serve, the last two have nothing to describe
a value below zero in a field whose name ends in _kg_m3, _kg_m2, _kg, _kg_mol, _m_s, _pa, _pa_s_m2, _pa_s_m, _n_m3, _mm, _um, _m, _m2, _m_hz or _per_cm (a density, a mass per area, a mass, a molar mass, a speed, a pressure or modulus, a flow resistivity, a specific flow resistance, a stiffness per area, a length, an area, a thickness-frequency product, a count per centimetre); a porosity outside 0 to 1; shot_content_percent, binder_content_percent or adhered_area_percent outside 0 to 100CatalogueError; a Celsius temperature, a decay rate per metre and a level in decibels stay signed
approximate=[...] kept as the list it was given, a mapping kept as the dicta frozenset, and every mapping frozen all the way down, its pairs into tuples
a subclass field annotated with a type the contract has no check for, a bare float or int includedTypeError the first time the class is built; a field is float | None (Optional[float] is the same), int | None, bool, str, frozenset[str] or a Mapping[str, ...] of those, because every cell of a row may be missing
PorousMaterial.frame_constants() on a row without structural_loss_factor, read as a frame that dissipates nothingValueError, saying what the page had in that cell
GroundSurface.porosity holding 26.9 to 58.1 on six rows of Cox & D’Antonio Table 6.7, which the page prints in per cent in a column of fractions that states no unitNone, with misprinted["porosity"] quoting the figure the page prints; why_missing("porosity") and printed("porosity") say so

Every other packaged row already met the contract, so those six porosities are the only published cells that change, and every published porous row with both moduli prints its loss factor.

from phonometry import io, materials
try:
materials.PorousMaterial(name="Panel core", source="a datasheet", porosity=97.0)
except io.CatalogueError as error:
print(error)
# 'Panel core': porosity is 97.0, and a porosity is a fraction from 0 to 1
carpet = materials.Carpet(
name="Loop pile", source="a datasheet", pile_height_mm=6.0, approximate=["pile_height_mm"]
)
print(carpet.approximate)
# frozenset({'pile_height_mm'})

A filter bank’s class reads every requirement of IEC 61260-1

Section titled “A filter bank’s class reads every requirement of IEC 61260-1”

verify_filter_class graded the Table 1 mask alone. For the 2014 edition it now also grades the effective bandwidth deviation of 5.12 and the summation of output signals of 5.16, computed as IEC 61260-2:2016 computes them, and a band’s class and the bank’s overall_class are the strictest class met on all three. For the 1995 edition it grades the filter integrated response of 4.5.3 and the summation of output signals of 4.9 beside Table 1, as that edition’s clause 5 tests them. In both editions every band is graded at the bank’s input rate up to half of it, so a decimated band is graded on the alias images its decimation folds onto it, and the bank’s decimator keeps them more than 125 dB down.

Before4.0
each band of a decimated bank decimated until its processing Nyquist frequency is 1.25 times its upper band edgedecimated only as far as leaves it sixteen times that edge, so the bilinear transform no longer bends the band: a band level moves by up to 0.07 dB on white noise, most in the lowest bands, and the octave bank, which would sum its outputs up to +0.94 dB and read class 2 on 5.16, is class 1 on every requirement
a band’s class read off margin_class{c}_dbclass over every requirement; margin_class{c}_db is still the Table 1 margin, beside bandwidth_margin_class{c}_db and summation_margin_class{c}_db, which is None on the two end bands
one requirement to ask aboutrequirements, requirement_class(name) and binding_margin_db(name, cls) read each one on its own, and plot(requirement="summation") draws it
an edition="1995" verdict on Table 1 aloneTable 1, "effective_bandwidth" (the filter integrated response of 4.5.3) and "summation" (4.9, run from the lowest to the highest mid-band frequency, so the end bands carry it too), both at the S that 5.3.3 raises in steps of 12 until the integrated response settles, which the result’s points_per_bandwidth reports (36 for the default octave bank)
a decimated band graded up to the Nyquist frequency of its decimated rate, checked_to_omega stopping theregraded at the input rate up to half of it, alias images included; checked_to_omega is half the input rate over the mid-band frequency, and range_limited marks a Table 1 mask running on past it
the anti-aliasing filter scipy.signal.resample_poly designs by default, which left the images of the octave bank at 8, 16 and 32 kHz 68.9 dB down, under the 70 dB of class 1a filter of the same length sized for a 120 dB stopband, its images at least 125.4 dB down; a band level moves by under 0.01 dB

The octave and one-third-octave banks keep class 1.

from phonometry import filters
octave = filters.verify_filter_class(
filters.OctaveFilterBank(48000, fraction=1, limits=[125, 4000]))
print(octave.overall_class, octave.requirement_class("relative_attenuation"))
# 1 1

A verdict reads its printed limit from the standard

Section titled “A verdict reads its printed limit from the standard”

A verdict used to carry the limit it was judged against as a constructor field. In 3.3.0 three did: ToneAssessment.criterion_db, ExposureAssessment.action_value and limit_value, and RigidMassCalibrationResult.tolerance. Six more that are newer than 3.3.0 did in 4.0.0rc1, and the rest of the table arrived after 4.0.0rc1. Anything that built one by hand, or rewrote one with dataclasses.replace, could judge a measurement against any number and still print the standard’s name over the result. In 4.0 every limit a standard prints is a read-only property, read from the fields that select it: the bands, the condition, the source, the class, the room. A verdict that was stored beside its limit is read from the measured fields too, or checked against them when the result is built, so it cannot contradict the limit. The functions that return these verdicts take the same arguments and give the same answers, and every name below still reads the same value; only code that constructs a verdict directly, or replaces one of these fields, has to change. The last column says where each verdict first shipped.

VerdictNo longer constructor fieldsFields it keeps or gainsSince
building.PositionSpreadChecklimit_db, stage, spread_db, action, next_positions, corner_standard_deviation_dblevels_db, which all of them are read fromafter 4.0.0rc1
building.HeavyImpactSourceCheckfrequencies, nominal, tolerance, deviation, within_tolerance, passessource, measured4.0.0rc1
noise_control.BandFlatnessChecklimit_db, satisfiedoctave_centres_hz, spread_dbafter 4.0.0rc1
noise_control.SourcePositionCheckrequired_positions, satisfied, exceeds_maximumpositions_used, max_octave_spread_dbafter 4.0.0rc1
noise_control.TestEnvironmentApplicabilityenvironmental_correction_limit_db, background_margin_limit_db, required_area_ratio, applicablebase_standard, actual_area_ratio, mean_absorption_coefficientafter 4.0.0rc1
environment.ReflectionGridChecknominal_m, deviations_m, withincheck, path_differences_mafter 4.0.0rc1
emission.HardWalledRoomChecklimit_db, minimum_volume_m3, box_dimension_limit_m, minimum_microphone_distance_mthe bands, the room and the boxafter 4.0.0rc1
emission.ReferenceSoundSourceVerdictevery *_limit_db, adjacent_step_db, core_range_db, extended_range_db, frequency_range_met, not_judgedthe bands, the levels and the measured figuresafter 4.0.0rc1
emission.ReferenceSourceDriftResult, emission.SpecialRoomSuitabilityChecklimit_dbthe bandsafter 4.0.0rc1
emission.SpecialRoomReverberationChecklower_limit, upper_limitthe bandsafter 4.0.0rc1
emission.IntensityInstrumentComplianceResultlimit_class1, limit_class2, spacing_offset_dbdevice, spacing; the band rows are read from them, see the next section4.0.0rc1
emission.InverseSquareLawResult, emission.SourceDirectionalityResulttolerance_dbroom selects it; the qualified radii must be the ones the levels reachafter 4.0.0rc1
hearing.AmbientNoiseChecklimits_db, table_bands_hzgains earphone_attenuation_db and allowed_threshold_shift_dbafter 4.0.0rc1
hearing.FixtureIsolationCheckrequired_dbthe bandsafter 4.0.0rc1
hearing.ReatSoundFieldCheckallowable_variation_dbgains free_field_rejection_dbafter 4.0.0rc1
hearing.DiffuseSoundFieldCheckallowable_variation_dbgains front_to_random_index_dbafter 4.0.0rc1
hearing.FreeSoundFieldCheckaxis_offset_m, inverse_distance_difference_dbfield and loudspeaker_distance_m select themafter 4.0.0rc1
electroacoustics.ProgrammeSignalCheckoffset_db, relative_levels_db, tolerance_plus_db, tolerance_minus_dbfrequencies_hz, band_levels_dbafter 4.0.0rc1
electroacoustics.NeckLoopVerificationlimitsneck_loop_type selects itafter 4.0.0rc1
psychoacoustics.ToneAssessmentcriterion_db, prominentgains method, "tone_to_noise_ratio" or "prominence_ratio"3.3.0
underwater.WeightedExposureResultcriteriagains impulsive4.0.0rc1
vibration.ExposureAssessmentaction_value, limit_value, exceeds_action, exceeds_limit, zonevalue, kind, metric3.3.0
vibration.PhaseVerification, vibration.WeightingVerificationtolerance_deg (phase), within_tolerancethe weighting and the frequencies4.0.0rc1
vibration.RunningRmsDecayVerificationprinted_time_s, tolerance_smethod, integration_time_s, measured_time_safter 4.0.0rc1
vibration.SignalBurstVerificationprinted, deviation_percent, tolerance_percent, within_tolerancethe table row, the columns and measured4.0.0rc1
vibration.VibrationMeterVerificationlower_percent, upper_percent, within_tolerancegains measured_responseafter 4.0.0rc1
vibration.AssessmentVelocityguide_value_mm_sbuilding_class selects itafter 4.0.0rc1
vibration.MountingCheckacceptable, frequency_limit_hz, peak_acceleration_limit_m_s2, devicegains peak_acceleration_m_s2 and upper_frequency_hzafter 4.0.0rc1
vibration.DamageAssessmentguideline_mm_sgains massive_structure, by name only4.0.0rc1
vibration.RigidMassCalibrationResultexpected, deviation, within_tolerance, passes, tolerancefrequencies, measured, mass, quantity3.3.0
vibration.LevelDifferenceChecklimit_dbcondition selects itafter 4.0.0rc1
vibration.OutputMassCheckmass_limit_kggains output_apparent_mass_kg, the measured output force over output accelerationafter 4.0.0rc1

Two calls change too. rigid_mass_calibration_check no longer takes tolerance: ISO 7626-2:2015 7.5.2 says the block “shall agree within ±5 %”, so a tighter in-house acceptance is read from .deviation instead. And HeavyImpactSourceCheck and RigidMassCalibrationResult, whose passes was a stored field, refuse to be read as a truth value like every other verdict, so if check: raises TypeError and the verdict is .passes.

import dataclasses
from phonometry import vibration
check = vibration.check_blocked_output([100.0, 200.0], [90.0, 95.0], [60.0, 70.0])
print(check.limit_db, check.passes)
# 20.0 True
try:
dataclasses.replace(check, limit_db=10.0)
except TypeError as error:
print(type(error).__name__)
# TypeError
calibration = vibration.rigid_mass_calibration_check([0.104, 0.093], [50.0, 500.0], 10.0)
print(calibration.tolerance, calibration.within_tolerance.tolist())
# 0.05 [True, False]

The limits a standard leaves to the user stay fields: the “default” roughness and decay-rate limits of ISO 3095, the tolerance ISO/DIS 16032 7.6 does not print for “approximately 30 s”, the curing period a product specification sets under ISO 10140-1 G.4, the threshold shift ISO 8253-1 lets the tester accept, and the specification limits, test-case limits and limit rows the caller passes to the conformance, ISO 17534-1 and RD 1367/2007 checks. The requirement records of ISO 8297 and IEC 60118-4, PlantRequirement and LoopRequirement, also keep each row’s limit as a field: one record type holds requirements whose limits mix printed figures with the plant’s geometry, the specified field strength or the noise measured with the loop off.

A verdict is read from the values it holds

Section titled “A verdict is read from the values it holds”

The section above took the printed limits out of the constructors. The verdict flags stayed in: passes, complies, qualified, every *_met and *_ok, the advisories, and the class of each band of a filter, weighting or intensity instrument verdict. The function that built a result worked them out and stored them beside the readings they came from, so a result built by hand, or rewritten with dataclasses.replace, could hold readings that fail and a flag that says they pass. In 4.0 every one of them is a read-only property, read from the values the result holds and the limit the standard prints. A result keeps what its verdict is read from: the readings, the margins, the positions, the measured figures. A figure that only fed the verdict, such as the rounded level of an RD 1367/2007 period or the share of raindrops inside their tolerance, is a property too. The functions that return these results take the same arguments and return the same verdicts, and every name in the table still reads the same value; only code that builds one of these results directly, or replaces one of the listed fields, has to change. “By name only” marks a field that is keyword-only.

ResultNo longer constructor fieldsRead fromSince
building.JointTestElementChecklength_ok, width_ok, passesjoint_length_m, joint_width_mm, window_or_door_gapafter 4.0.0rc1
building.GapWidthCheckgap_width_mm, spread_mm, enough_positions, uniform, passesreadings_mmafter 4.0.0rc1
building.JointGapSeriesChecknominal_measured, minimum_measured, working_range_measured, passesgap_widths_mm, nominal_gap_mm, minimum_gap_mmafter 4.0.0rc1
building.LiningCuringCheckcured, lag_within_third, passescuring_time_days, time_lag_days, required_curing_daysafter 4.0.0rc1
building.RainGeneratorVerificationrate_deviation_mm_h, rate_ok, drop_share, drops_ok, velocity_share, velocities_ok, passesrain_type, rainfall_rate_mm_h, drop_diameters_mm, fall_velocities_m_safter 4.0.0rc1
building.ServiceEquipmentPositionCheckseparation_m, surface_distance_m, source_distance_m, heights_m, corner_height_m, corner_wall_distances_m and the six *_okthe room, the corner and the positionsafter 4.0.0rc1
building.DbHrCheckreported, margin, compliesrequirement, value4.0.0rc1
building.LowFrequencyIntensityResult, building.LowFrequencyElementResultqualifiedsurface_pressure_intensity_indicator, against the 6.4.2 limit absorbing_specimen_surface selects4.0.0rc1
electroacoustics.BackgroundNoiseAssessmentreference_signal_to_noise_ratio_db, category, report_requirednoise_levels_db, noise_is_tonalafter 4.0.0rc1
electroacoustics.FeedbackStabilityResultis_stableloop_gain, stability_margin4.0.0rc1
emission.FreeFieldCheckroom, frequencies_hz, band_radius_m, maximum_qualified_radius_m, every *_met, full_frequency_range, conforming_range_hz, conforming_radius_m, not_judgedthe arguments of check_free_field: gains source_directionality, reflecting_plane_absorption_coefficient, reflecting_plane_margin_m, paths_in_working_area, speed_of_soundafter 4.0.0rc1
emission.IntensityInstrumentComplianceResultbands, overall_class, range_limitedfrequencies, residual_index, device, spacing4.0.0rc1
emission.ReferenceSourceCalibrationsound_power_level_db, intensity_bands, intensity_agreement, room_qualifiedgains intensity_sound_power_level_db and room_qualificationafter 4.0.0rc1
emission.HardWalledSoundPowerResultbackground_requirement_met, upper_boundgains background_margin_db, background_margin_ref_dbafter 4.0.0rc1
emission.InSituSoundPowerResultbackground_requirement_met, upper_bound, grade, sigma_r0, sigma_tot, expanded_uncertaintygains background_margin_db, background_margin_ref_db, the smallest over the positions, and excess_levels_db, directivity_range_db, the two indicators Table 2 grades by4.0.0rc1
emission.SpecialRoomSoundPowerResultbackground_requirement_met, background_requirement_met_agains background_margin_db, background_margin_ref_db, background_margin_a_dbafter 4.0.0rc1
emission.EmissionPressureResultupper_boundgains background_margin_db, by name only4.0.0rc1
emission.PrecisionCriteriacriterion_1 to criterion_5, qualifiedgains the readings: indicators, scan_intensity_level_1, scan_intensity_level_2, frequencies, repeatability_limit_db, pressure_residual_index_db, field_nonuniformity_1, field_nonuniformity_23.3.0
emission.DiscretePointIntensityResulteverything but the indicators and the grade: partial_power, sound_power, sound_power_level, achieved_grade, both criteria, the A-weighted figures and the restgains normal_intensity, segment_areas_m2, pressure_residual_index_db, band_type4.0.0rc1
emission.TurbineTestEnvironmentCheckcorrection_ok, wind_ok, windscreen_advisedenvironmental_correction_db, ratio, wind_speed_m_safter 4.0.0rc1
environment.ImpulseOnsetlevel_difference, prominence, qualifieslevel_start, level_end, onset_rate3.3.0
environment.ImpulseProminenceResultper_impulse, qualifies, prominence, adjustmentonset_rates, level_differences, assessment_period_min3.3.0
environment.ResidualCorrectionResultreliablemargin3.3.0
environment.PeriodAssessmentreported_level, reported_long_term, max_phase_level, phase_pass, daily_pass, long_term_passphases, the two levels and limit; gains new_activity, by name only4.0.0rc1
environment.ModulationPeriodmodulation_depths_db, fundamental_frequencies_hz, valid_blocks, rated, rating_db, mean_modulation_frequency_hz, mode_modulation_frequency_hzblocksafter 4.0.0rc1
environment.SoundRelevantTurbinesrelevant, total_level_db, relevant_level_dbpredicted_levels_dbafter 4.0.0rc1
environment.WindShearProfiletypicalshear_exponentafter 4.0.0rc1
environment.WindTurbineTonalityResultis_audibletonal_audibility, has_identified_tone, itself read-only (next section)3.3.0
filters.FilterComplianceResultbands, overall_class, range_limitedgains band_margins, the rows without their class3.3.0
filters.WeightingComplianceResultbands, overall_class, range_limitedgains band_margins, the rows without their class4.0.0rc1
hearing.AscendingThresholdResultthreshold_db, determined, series_exhaustedascent_levels_db, shortenedafter 4.0.0rc1
hearing.TaskContributionspread_advisoryn_samples, sample_range_db3.3.0
hearing.ExposureResultsampling_advisorygains sample_range_db, n_workers, sample_duration_hours, by name only3.3.0
hearing.AttenuationDifferenceResultdifference_db, criterion_db, significantthe two means and their expanded uncertaintiesafter 4.0.0rc1
materials.CeilingSpecimenCheckarea_error_m2, deflection_ok, substructure_ok, fixture_ok, supports_ok, humidity_ok, ce_marking_depth, satisfiedgains the measured figures: deflection_mm, substructure_width_mm, substructure_height_mm, fixture_density_kg_m2, support_width_mm, support_height_mm, support_centre_distance_m, lowest_relative_humidity_percentafter 4.0.0rc1
metrology.TrendTestResulttrend_free, boundsstatistic, against the region values, method, alpha and median give3.3.0
metrology.StationarityTestResultstationary, boundscount, against the region segment_values, method and alpha give3.3.0
metrology.CouplerCheckratio_recommended, approximation_valid, full_solution_advised, conditions_validgains heat_conduction_method, temperature_c, static_pressure_pa, relative_humidity_percent, in_air, by name onlyafter 4.0.0rc1
metrology.FreeFieldArrangementCheckdistances_ok, support_ok, annex_a_range, attenuation_accuracygains air_attenuationafter 4.0.0rc1
noise_control.CabinUncertaintyratio_satisfied, stateable, stated_band_range_hz, increased_uncertainty_band_range_hz, excess_standard_deviation_dbmethod, volume_ratioafter 4.0.0rc1
room.RoomAcousticsResultedt_valid, t20_valid, t30_validdynamic_range and the decay times3.3.0
room.BackgroundMarginCheckneeds_correction, unusable, satisfiedmargins_dbafter 4.0.0rc1
room.DetailVerdictsatisfied, room_ok, fittings_ok, sources_okgains room_detail, fitting_detail, source_detailafter 4.0.0rc1
underwater.WeightedExposureResultexceeds_injury, exceeds_ttssel_margin, peak_margin, tts_margin, tts_peak_margin4.0.0rc1
vibration.PeopleAssessmentcomplies, criterion, within_uncertaintykb_fmax, kb_ftr, guide, source; gains rare_short_events, by name onlyafter 4.0.0rc1
vibration.RailwayChangecomplies, kb_fmax_met, kb_ftr_met, kb_fmax_increase_percent, kb_ftr_increase_percentgains kb_fmax_before, kb_fmax_after, kb_ftr_before, kb_ftr_afterafter 4.0.0rc1
vibration.TransferStiffnessResultvalidgains transmissibility, |T| of the indirect method3.3.0
vibration.DrivingPointStiffnessResultadequategains blocked_output_difference_db, unwanted_input_difference_dbafter 4.0.0rc1
aircraft.AircraftSystemComplianceResultpasses, checksgains directional, frequency_response, linearity, resolution, the arguments of verify_aircraft_noise_system4.0.0rc1
broadcast.QuasiPeakDynamicsResultpasses, worst_margin_db, worst_deviation_db, stimuligains readings_percent, the eleven readings in the order of Tables 2 and 34.0.0rc1
building.FloorCoveringImprovementResultlimitedgains background_margin_db, the smallest margin over the positions and both floors3.1.0
building.VibrationReductionResultsingle_number, bracketedfrequencies, k_ij; gains modal_overlap, band_type3.1.0
building.ServiceEquipmentBackgroundResultdifference_db, correction_db, corrected_db, regimemeasured_db, background_dbafter 4.0.0rc1
building.ServiceEquipmentResultstandardizablefrequencies_hz, bandafter 4.0.0rc1
emission.SoundPowerIntensityResultpartial_power_level, sound_power, sound_power_level, negative_band, negative_partial_power_index, dynamic_capability_index, achieved_grade, a_weighting_omitted_bands, sound_power_level_apartial_power and the indicators; gains pressure_residual_index_db, band_type, repeatability_limit_db3.1.0
emission.PrecisionIntensityResultsound_power, sound_power_level, sound_power_level_normalized, not_applicable_band, sound_power_level_apartial_power; gains temperature_c, barometric_pressure_pa, by name only3.1.0
emission.InDuctSoundPowerResultinformation_only_bandfrequencies, flow_velocity4.0.0rc1
emission.SourceLocationPlanspectral_character, a_weighted_spectral_characterstandard, standard_deviation_db, a_weighted_standard_deviation_dbafter 4.0.0rc1
environment.ImpulsiveSoundResultprominence, adjustment, category, adjusted_laeqonsets, laeq3.3.0
environment.ModulationBlockstatusprominence; gains excluded, by name onlyafter 4.0.0rc1
environment.ReferenceTrackCheckconditionsthe roughness, the decay rates and the limits; gains curve_radius_m, track_gradient_ratio, by name onlyafter 4.0.0rc1

FilterComplianceResult and WeightingComplianceResult hold their per-band rows as band_margins: the rows bands held, without the "class" key. A row that states a class is refused, and the bands property adds to each row the class its margins earn, so result.bands[i]["class"] reads as before. IntensityInstrumentComplianceResult takes no band rows at all: it holds the frequencies and the measured pressure-residual intensity index, and reads the rows, the class and range_limited from them, the device and the microphone separation.

The rows a verdict is read from are read-only. band_margins and between_nominals are copied into read-only mappings when the result is built, so a write into one raises TypeError and a later write into the dictionaries you built it from does not move the class; copy a row with dict(row) to change it, since copy.deepcopy cannot copy a read-only mapping. The rows of AircraftSystemComplianceResult.checks, QuasiPeakDynamicsResult.stimuli and ReferenceTrackCheck.conditions are built from what the result holds and the tables of the standard each time they are read.

import dataclasses
from phonometry import building, filters
gap = building.check_gap_width([5.0, 5.1, 5.2, 5.1])
print(gap.passes, round(gap.spread_mm, 1))
# True 0.2
try:
dataclasses.replace(gap, passes=False)
except TypeError as error:
print(type(error).__name__)
# TypeError
wider = dataclasses.replace(gap, readings_mm=(5.0, 5.1, 5.6, 5.1))
print(wider.uniform, wider.passes)
# False False
bank = filters.OctaveFilterBank(fs=48000, fraction=1, order=6, limits=[125, 4000])
result = filters.verify_filter_class(bank)
print(result.overall_class, result.bands[0]["class"], "class" in result.band_margins[0])
# 1 1 False

A flag stays a field where it is no verdict against a limit and nothing the result holds decides it: an option the caller sets, such as small_room or noise_is_tonal; a branch the caller chose by what they gave, such as InverseSquareLawResult.origin_fitted; a fact of an input the result does not keep, such as TimeWeightedEnvelope.calibrated; a property of the file read; whether an iterative solver converged; and a column of a published table row. CriticalCouplingResult.converged keeps the outcome of its solve, which the result cannot run again, but it can no longer claim convergence beside an absorption that does not exceed 0.999, the third condition of the flag. A regime the result could read from its own values is a verdict, and the next section lists the ones that moved. PlantRequirement and LoopRequirement do not change: their rows keep each limit as a field for the reason the section above gives, and PlantRequirement.holds stays beside it. TrackCondition keeps its holds as a row of the report, but ReferenceTrackCheck builds its conditions from the track it holds, so no verdict reads one built elsewhere. The yes/no checks of SoundLevelMeterPeriodicRequirement stay as well: they are what the tester saw, an overload indicated or an indicator latched, not a limit judged.

A result reads its regime from the values it holds

Section titled “A result reads its regime from the values it holds”

Some results stored a regime or a classification beside the values it was read from: the regime of a valve, whether a hammer rebounds, whether terrain screens a path, whether a tone was found, whether a synchronous average had to interpolate. The function compared values the result holds, or could hold, with a constant or a threshold its source prints, and stored the answer, so a result built by hand could hold the stiffness of an under-critical hammer and say it is over-critical. In 4.0 each of them is a read-only property, read from the values the result keeps, and a result that did not keep what the test reads now keeps it, with the unit in the name. “By name only” marks a field that is keyword-only.

A value exactly on a boundary falls where the source puts it. Hopkins prints the over-critical case as K m ≥ 4 Zdp², so critical damping is over-critical, and IEC TS 61400-11-2 asks for a total “at least 3 dB” above the background before the logarithmic subtraction, so a difference of 3 dB takes it. IEC 60534-8-4 does not settle its cavitation threshold one way: the test of 5.1 leaves the threshold itself to neither side, the region of Equation (9) and Equation (19b) put it with the cavitating points, and 4.1, Equation (18b) and 6.3 with the turbulent ones. The library reads it as turbulent, where Equation (9) returns zero and the two branches meet without a step.

The functions take the same arguments and return the same numbers. Code that builds one of these results directly, or replaces one of the fields its regime is read from, has to change, and so does code that reads band_type from vibration_reduction_index (see below the table).

ResultNo longer a constructor fieldRead fromSince
building.TappingForceResultover_critical, K m ≥ 4 Zdp² (Hopkins Eq. 3.95)contact_stiffness, impedance; gains mass_kg, by name only4.0.0rc1
noise_control.HydrodynamicValveNoiseregime, cavitating when Δp exceeds x_Fzp1 (p1 - pv) (IEC 60534-8-4 5.1)differential, corrected_ratio; gains inlet_pressure_pa, vapour_pressure_pa4.0.0rc1
noise_control.AerodynamicValveNoiseregime, the five intervals of IEC 60534-8-3 5.2pressure_ratio, boundaries; gains specific_heat_ratio, pressure_recovery, efficiency_correction, by name only4.0.0rc1
aircraft.TerrainScreeningResultscreened, terrain strictly above the line of sightsource, receiver, distances, heights3.3.0
hearing.AutomaticThresholdResultis_peak, retained (ISO 8253-1 6.3.5 a))reversal_levels_dbafter 4.0.0rc1
hearing.SweepThresholdResultis_peakreversal_levels_dbafter 4.0.0rc1
metrology.WaveMotionCorrectioninterpolatedfrequencies_hz, speed_of_sound_ratioafter 4.0.0rc1
noise_control.CabinInsulationResultapparent (ISO 11957 3.6)methodafter 4.0.0rc1
environment.WindTurbineTonalityResulthas_identified_tone (IEC 61400-11 9.5.2 to 9.5.4)levels, frequencies; gains candidate_frequency_hz, by name only3.3.0
building.LabJointInsulationResultregime (ISO 10140-1 J.1)r_s_measured_db, r_s_max_db; gains limit_at_maximum, by name onlyafter 4.0.0rc1
environment.TurbineSoundLevelsregimes (IEC TS 61400-11-2 11.7)total_levels_db, background_levels_dbafter 4.0.0rc1
underwater.WestonPropagationResultregimerange_m, boundaries4.0.0rc1
signals.SynchronousAverageResultinterpolated, f_s T not a whole number of samplesfs, period_s4.0.0rc1

building.VibrationReductionResult.band_type stays a field, as the band set the caller states: octave_bands() states "octave", since a single octave band cannot be told from a one-third-octave band by its spacing. vibration_reduction_index no longer fills it with the band set it read from the frequencies; it leaves None, and the result reads the spacing each time it takes the Annex A mean, which gives the same mean as before.

The numbers a regime selects are held to it, so a result built by hand can no longer carry one regime beside the values of another. When it is built, each of these results refuses:

  • TappingForceResult: a cut-off frequency, a limiting frequency, a force spectrum or a power input that is not what K, Zdp and m give in their regime, and limits of Eqs. (3.99) and (3.100) that are not a factor of 2 apart.
  • HydrodynamicValveNoise: a vapour pressure at or above the inlet pressure; an x_F or an x_Fzp1 that is not what Equation (1) or (3c) gives from the fields beside it; a point at or past flashing, x_F = 1; cavitation fields on a turbulent point, or none on a cavitating one.
  • AerodynamicValveNoise: boundaries that are not those of its gas and its trim, and a Mach number or an acoustical efficiency of another regime.
  • TerrainScreeningResult: a source or a receiver that is not a finite point, a source at or past the receiver, a section that does not run from one to the other, and a path difference or diffracting edges that are not those of the rubber band over that section.
  • AutomaticThresholdResult and SweepThresholdResult: reversals that do not alternate, a tracing that keeps no peak or no valley, and a mean, a threshold or a running threshold that is not what the reversals give.
  • WaveMotionCorrection: a frequency beyond the last row of Table C.3, and a correction the table does not give at its frequency.
  • CabinInsulationResult: an A-weighted insulation under any method but the actual noise.
  • WindTurbineTonalityResult: a candidate that is not a line of the spectrum at or above 20 Hz, and a Formula 30 to 34 chain that is not the one the spectrum gives about it.
  • LabJointInsulationResult: a corrected index of another rule of J.1.
  • TurbineSoundLevels: a level difference other than the total less the background, a turbine level of another rule of 11.7, and an uncertainty on a bin whose level cannot be determined.
  • WestonPropagationResult: a composite loss that is not the law of the regime in force.
  • SynchronousAverageResult: a period grid other than round(fs * period_s) samples.
import dataclasses
import numpy as np
from phonometry import building
bands = np.array([100.0, 200.0, 400.0, 800.0])
force = building.tapping_force_spectrum(bands, 1.0e6, 3.0e4)
print(force.over_critical)
# False
print(building.tapping_force_spectrum(bands, 1.0e6, 100.0).over_critical)
# True
try:
dataclasses.replace(force, impedance=100.0)
except ValueError as error:
print("cut_off_frequency" in str(error))
# True

Everything else moved. These ten exist in no public module:

GoneWhat to write instead
octavefilter, getansifrequencies, normalizedfreqfilters.octave_filter, filters.nominal_frequencies, filters.normalized_frequencies. These were the original PyOctaveBand spellings, renamed in 3.1 and kept as aliases until 4.0
calculate_sensitivitymetrology.sensitivity, the same 3.1 rename
filter_class_compliancefilters.verify_filter_class
air_density_iso, speed_of_sound_isomaterials.air_density_iso10534, materials.speed_of_sound_iso10534, and read the kelvin warning above
speed_of_soundfluids.air(temperature_c=...).speed_of_sound for the medium. It uses a fuller model than the ISO 17497-1 one-liner did, so the number moves: 343.99 m/s against 343.20 m/s at 20 °C
depth_to_pressurefluids.depth_to_gauge_pressure_mpa(depth_m=...), which returns the same number, or fluids.depth_to_absolute_pressure_pa for the absolute one. Both are keyword-only and both take an optional latitude_deg
TransmissionLossResultPropagationLossResult, with .tl renamed to .pl

Numbers. The conformance report is the same 995 checks against the same standards with the same expected values it had before the reorganisation, and every rename above was checked to return what its predecessor returned. If a result moves after you finish the upgrade, that is a defect and it is worth reporting.

One verdict is the exception, and the section on filter classes above says how: verify_filter_class grades the effective bandwidth of 5.12 and the summation of 5.16 as well as the Table 1 mask for the 2014 edition, and the filter integrated response of 4.5.3 and the summation of 4.9 for edition="1995", so a bank’s class is the strictest it meets on all three. Every band is graded at the bank’s input rate up to half of it, so the Table 1 margins of a decimated band take in the alias images its decimation folds onto it.

The decimated filter banks are the other: each band now keeps its processing Nyquist frequency sixteen times its upper edge and decimates through an anti-aliasing filter sized for a 120 dB stopband, so a band level moves by up to 0.07 dB on white noise, as the same section says.

What each .plot() draws is untouched, and so is what each .report() prints but one label: the microphone fiche writes dB(468) where it wrote dB(CCIR). The Signal contract is the one it was, now kept in two more places, the blocks of read_blocks and the waveforms inside five results. Every formula is untouched too. 4.0 rearranged the furniture and put the unit on the label; apart from those two, it did not recompute anything.