diff --git a/.github/badges/conformance-summary.svg b/.github/badges/conformance-summary.svg
index 9147e42ae..263e5977b 100644
--- a/.github/badges/conformance-summary.svg
+++ b/.github/badges/conformance-summary.svg
@@ -1 +1 @@
-
+
diff --git a/.github/badges/conformance-summary_dark.svg b/.github/badges/conformance-summary_dark.svg
index 6b7ea74cb..3d1c73397 100644
--- a/.github/badges/conformance-summary_dark.svg
+++ b/.github/badges/conformance-summary_dark.svg
@@ -1 +1 @@
-
+
diff --git a/.github/images/comparison_budget.svg b/.github/images/comparison_budget.svg
new file mode 100644
index 000000000..e647ad92d
--- /dev/null
+++ b/.github/images/comparison_budget.svg
@@ -0,0 +1,1327 @@
+
diff --git a/.github/images/comparison_budget_dark.svg b/.github/images/comparison_budget_dark.svg
new file mode 100644
index 000000000..dc0721614
--- /dev/null
+++ b/.github/images/comparison_budget_dark.svg
@@ -0,0 +1,1327 @@
+
diff --git a/.github/images/comparison_budget_es.svg b/.github/images/comparison_budget_es.svg
new file mode 100644
index 000000000..de82e8d22
--- /dev/null
+++ b/.github/images/comparison_budget_es.svg
@@ -0,0 +1,1190 @@
+
diff --git a/.github/images/comparison_budget_es_dark.svg b/.github/images/comparison_budget_es_dark.svg
new file mode 100644
index 000000000..c9145e4ff
--- /dev/null
+++ b/.github/images/comparison_budget_es_dark.svg
@@ -0,0 +1,1190 @@
+
diff --git a/.github/images/comparison_calibration.svg b/.github/images/comparison_calibration.svg
new file mode 100644
index 000000000..92272ed95
--- /dev/null
+++ b/.github/images/comparison_calibration.svg
@@ -0,0 +1,1383 @@
+
diff --git a/.github/images/comparison_calibration_dark.svg b/.github/images/comparison_calibration_dark.svg
new file mode 100644
index 000000000..462b9d8a5
--- /dev/null
+++ b/.github/images/comparison_calibration_dark.svg
@@ -0,0 +1,1383 @@
+
diff --git a/.github/images/comparison_calibration_es.svg b/.github/images/comparison_calibration_es.svg
new file mode 100644
index 000000000..d9a0b1c9a
--- /dev/null
+++ b/.github/images/comparison_calibration_es.svg
@@ -0,0 +1,1332 @@
+
diff --git a/.github/images/comparison_calibration_es_dark.svg b/.github/images/comparison_calibration_es_dark.svg
new file mode 100644
index 000000000..789665002
--- /dev/null
+++ b/.github/images/comparison_calibration_es_dark.svg
@@ -0,0 +1,1332 @@
+
diff --git a/.github/images/comparison_jig_correction.svg b/.github/images/comparison_jig_correction.svg
new file mode 100644
index 000000000..de305d290
--- /dev/null
+++ b/.github/images/comparison_jig_correction.svg
@@ -0,0 +1,1116 @@
+
diff --git a/.github/images/comparison_jig_correction_dark.svg b/.github/images/comparison_jig_correction_dark.svg
new file mode 100644
index 000000000..b2ec00017
--- /dev/null
+++ b/.github/images/comparison_jig_correction_dark.svg
@@ -0,0 +1,1116 @@
+
diff --git a/.github/images/comparison_jig_correction_es.svg b/.github/images/comparison_jig_correction_es.svg
new file mode 100644
index 000000000..62a5dac15
--- /dev/null
+++ b/.github/images/comparison_jig_correction_es.svg
@@ -0,0 +1,1128 @@
+
diff --git a/.github/images/comparison_jig_correction_es_dark.svg b/.github/images/comparison_jig_correction_es_dark.svg
new file mode 100644
index 000000000..767a30de9
--- /dev/null
+++ b/.github/images/comparison_jig_correction_es_dark.svg
@@ -0,0 +1,1128 @@
+
diff --git a/.github/images/diagram_comparison_calibration_setup.svg b/.github/images/diagram_comparison_calibration_setup.svg
new file mode 100644
index 000000000..13c13d77d
--- /dev/null
+++ b/.github/images/diagram_comparison_calibration_setup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/images/diagram_comparison_calibration_setup_dark.svg b/.github/images/diagram_comparison_calibration_setup_dark.svg
new file mode 100644
index 000000000..fa3343d8b
--- /dev/null
+++ b/.github/images/diagram_comparison_calibration_setup_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/images/diagram_comparison_calibration_setup_es.svg b/.github/images/diagram_comparison_calibration_setup_es.svg
new file mode 100644
index 000000000..863695e32
--- /dev/null
+++ b/.github/images/diagram_comparison_calibration_setup_es.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/images/diagram_comparison_calibration_setup_es_dark.svg b/.github/images/diagram_comparison_calibration_setup_es_dark.svg
new file mode 100644
index 000000000..4e19484d4
--- /dev/null
+++ b/.github/images/diagram_comparison_calibration_setup_es_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/images/free_field_region.svg b/.github/images/free_field_region.svg
new file mode 100644
index 000000000..377afff05
--- /dev/null
+++ b/.github/images/free_field_region.svg
@@ -0,0 +1,1371 @@
+
diff --git a/.github/images/free_field_region_dark.svg b/.github/images/free_field_region_dark.svg
new file mode 100644
index 000000000..76ccecb05
--- /dev/null
+++ b/.github/images/free_field_region_dark.svg
@@ -0,0 +1,1371 @@
+
diff --git a/.github/images/free_field_region_es.svg b/.github/images/free_field_region_es.svg
new file mode 100644
index 000000000..3485aef1c
--- /dev/null
+++ b/.github/images/free_field_region_es.svg
@@ -0,0 +1,1360 @@
+
diff --git a/.github/images/free_field_region_es_dark.svg b/.github/images/free_field_region_es_dark.svg
new file mode 100644
index 000000000..6762dfc12
--- /dev/null
+++ b/.github/images/free_field_region_es_dark.svg
@@ -0,0 +1,1360 @@
+
diff --git a/.zenodo.json b/.zenodo.json
index 457dce052..90d105e6a 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -1,6 +1,6 @@
{
"title": "phonometry: acoustic measurement, analysis and prediction for Python (formerly PyOctaveBand)",
- "description": "
phonometry is a Python library for acoustic measurement, analysis and prediction. Every metric is implemented from the text of its governing standard, and that standard's own reference values and acceptance limits are transcribed into the test suite and enforced in CI: 1774 numerical conformance checks across 108 domains against 516 IEC, ISO, EN, ANSI, ECMA, DIN, ITU, EBU, SAE, ICAO, ECAC and VDI standards, each pinning a clause, table or worked example to the value the library computes.
Nine documented areas. Sound level metrology: fractional octave filter banks (ANSI S1.11, IEC 61260-1), frequency and time weighting (IEC 61672-1), Leq, LAeq, percentile levels, SEL, LCpeak, noise dose (IEC 61252), calibration (IEC 60942) and GUM uncertainty. Hearing and perception: loudness (ISO 532-1/-2/-3), tonality and sound quality (ECMA-418-1/-2, DIN 45692), equal-loudness contours (ISO 226), occupational exposure (ISO 9612) and speech intelligibility (IEC 60268-16, ANSI S3.5). Rooms and buildings: room acoustic parameters (ISO 3382), field and laboratory sound insulation (ISO 16283, ISO 10140), single-number ratings (ISO 717) and prediction (EN 12354). Materials and surfaces: absorption (ISO 354, ISO 11654), impedance tube (ISO 10534-2), airflow resistivity (EN 29052) and scattering (ISO 17497). Vibration and structure-borne sound: mechanical mobility (ISO 7626), transfer stiffness (ISO 10846) and human vibration (ISO 2631, ISO 5349, ISO 8041). Environment and transport: outdoor propagation (ISO 9613), environmental noise (ISO 1996), aircraft noise certification (ICAO Annex 16) with airport contours (ECAC Doc 29/32) and wind turbine noise (IEC 61400-11). Underwater acoustics: terminology and metrics (ISO 18405), ship source levels (ISO 17208), pile driving (ISO 18406) and numerical propagation. Sources and devices: sound power, sound intensity, electroacoustics (IEC 60268) and programme loudness (ITU-R BS.1770, EBU R 128). Wave simulation: FDTD.
Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line .plot() in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with .report(). Pure Python on NumPy and SciPy, requiring Python 3.13 or newer.
It is not a certified instrument: the conformance report is the library checking its own output against values published in the standards, not an accredited third-party calibration.
",
+ "description": "
phonometry is a Python library for acoustic measurement, analysis and prediction. Every metric is implemented from the text of its governing standard, and that standard's own reference values and acceptance limits are transcribed into the test suite and enforced in CI: 1786 numerical conformance checks across 109 domains against 518 IEC, ISO, EN, ANSI, ECMA, DIN, ITU, EBU, SAE, ICAO, ECAC and VDI standards, each pinning a clause, table or worked example to the value the library computes.
Nine documented areas. Sound level metrology: fractional octave filter banks (ANSI S1.11, IEC 61260-1), frequency and time weighting (IEC 61672-1), Leq, LAeq, percentile levels, SEL, LCpeak, noise dose (IEC 61252), calibration (IEC 60942) and GUM uncertainty. Hearing and perception: loudness (ISO 532-1/-2/-3), tonality and sound quality (ECMA-418-1/-2, DIN 45692), equal-loudness contours (ISO 226), occupational exposure (ISO 9612) and speech intelligibility (IEC 60268-16, ANSI S3.5). Rooms and buildings: room acoustic parameters (ISO 3382), field and laboratory sound insulation (ISO 16283, ISO 10140), single-number ratings (ISO 717) and prediction (EN 12354). Materials and surfaces: absorption (ISO 354, ISO 11654), impedance tube (ISO 10534-2), airflow resistivity (EN 29052) and scattering (ISO 17497). Vibration and structure-borne sound: mechanical mobility (ISO 7626), transfer stiffness (ISO 10846) and human vibration (ISO 2631, ISO 5349, ISO 8041). Environment and transport: outdoor propagation (ISO 9613), environmental noise (ISO 1996), aircraft noise certification (ICAO Annex 16) with airport contours (ECAC Doc 29/32) and wind turbine noise (IEC 61400-11). Underwater acoustics: terminology and metrics (ISO 18405), ship source levels (ISO 17208), pile driving (ISO 18406) and numerical propagation. Sources and devices: sound power, sound intensity, electroacoustics (IEC 60268) and programme loudness (ITU-R BS.1770, EBU R 128). Wave simulation: FDTD.
Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line .plot() in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with .report(). Pure Python on NumPy and SciPy, requiring Python 3.13 or newer.
It is not a certified instrument: the conformance report is the library checking its own output against values published in the standards, not an accredited third-party calibration.
",
"license": "MIT",
"upload_type": "software",
"access_right": "open",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 002d66856..c6096cb6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -949,6 +949,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
by what each reading is of. A new guide under Calibration and uncertainty runs every
method on a synthetic meter and on the two worked budgets, in English and
Spanish.
+- **The calibration of a working standard microphone by comparison with a
+ reference microphone, in a pressure field and in a free field
+ (IEC 61094-5:2016, IEC 61094-8:2012).** The new
+ `metrology.comparison_calibration` module carries a reference microphone's
+ sensitivity over to the microphone under test through the ratio of their
+ output voltages, $M_\mathrm{test} = M_\mathrm{ref} R_V / R_P$ of IEC
+ 61094-5 D.2 written in levels. `metrology.simultaneous_comparison` takes the
+ reading difference between the two channels before and after the
+ microphones are interchanged, whose difference cancels the gains of the
+ channels, a drift of the source and the asymmetry of the field (Annex C,
+ Formula (C.3)), and refuses a pressure calibration without the interchange
+ 5.1.2 requires; `metrology.sequential_comparison` takes the two outputs in
+ turn and, when a monitor microphone watches the source, each against the
+ monitor reading taken with it, the quotient IEC 61094-8 A.2 corrects a
+ drifting source with. Both average one row per
+ determination and return a `metrology.ComparisonCalibration` with the
+ sensitivity level and the sensitivity in mV/Pa, the corrections it carries
+ by name, the part it follows and, when given, its expanded uncertainty.
+ `metrology.environmental_sensitivity_correction` corrects a sensitivity
+ level for the static pressure, the temperature and the humidity to first
+ order, from the microphone's own coefficients in the units IEC 61094-2
+ Annex D gives them; the reference's is added and the test microphone's
+ subtracted to refer the result to the reference conditions of clause 4.
+ `metrology.jig_diameter_correction` reads Table A.1 for a WS3 microphone
+ against an LS2aP in the jig of Figure A.4, with its expanded uncertainty of
+ a tenth of the correction (printed without a coverage factor, and read with
+ the $k = 2$ of 7.9 and D.2), and a free-field calibration against a
+ pressure-calibrated reference takes the reference's IEC/TS 61094-7
+ difference. `metrology.comparison_uncertainty_budget` combines the
+ components of Table D.1 or of IEC 61094-8 Table 2 on
+ `metrology.combine_uncertainty` with $k = 2$, and
+ `metrology.free_field_region` gives the effective free-field region of a
+ time window, $A = d + \tau c$ (B.1), with the speed of sound of the
+ IEC 61094-2 Annex F air, and the clearance it asks of the room and of the
+ mounting rod. Tables A.1 and D.1 of IEC 61094-5 and Tables 1 and 2 of
+ IEC 61094-8 are published read-only, every result has `.plot()`, and the
+ IEC 61183 diffuse-field comparison now computes through the same model. The
+ tables, the budget and the three measurement models are conformance rows.
+ Four printed defects are in the errata: D.3 of IEC 61094-5 states 0,040 dB
+ and 0,08 dB where its own eight components give 0,0437 dB and 0,087 dB; the
+ Spanish UNE-EN 61094-5:2017 leaves six of the eight values of Table D.1
+ blank and prints 0,004 dB for the combined uncertainty; IEC 61094-8 prints
+ an unresolved cross-reference in 8.4; and its Formula (B.10) is the
+ spectrum of a pulse of duration $2b$ that the text calls one of duration
+ $b$. A new guide under Calibration and uncertainty runs a pressure and a
+ free-field calibration with their budgets, in English and Spanish.
- **What a sound level meter reads when sound arrives from every direction
(IEC 61183:1994).** `metrology.directivity_factor` takes the levels a meter
indicates at equal angular steps in two planes (or four, as NOTE 2 of A.6
diff --git a/README.md b/README.md
index 05cf8184e..8e6024735 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ materials, vibration, environmental, aircraft and underwater acoustics,
electroacoustics and wave simulation. Every metric is implemented from its
governing standard and numerically checked against it: the auto-generated
[conformance report](https://github.com/jmrplens/phonometry/blob/main/docs/CONFORMANCE.md)
-runs 1774 conformance checks across 108 domains and 516 standards, each pinning
+runs 1786 conformance checks across 109 domains and 518 standards, each pinning
an expected normative value to the value the library computes, and CI
regenerates it on every pull request. Filters meet the class 1 mask of
**IEC 61260-1:2014 / ANSI S1.11-2004**, and the octave and one-third-octave
@@ -32,7 +32,7 @@ banks are class 1 on every requirement the 2014 edition grades on a design.
Weightings and levels are class 1 per
**IEC 61672-1:2013**.
-
+
diff --git a/README_PYPI.md b/README_PYPI.md
index c9d00893b..80705e706 100644
--- a/README_PYPI.md
+++ b/README_PYPI.md
@@ -36,7 +36,7 @@ materials, vibration, environmental, aircraft and underwater acoustics,
electroacoustics and wave simulation. Every metric is implemented from its
governing standard and numerically checked against it: the auto-generated
[conformance report](https://github.com/jmrplens/phonometry/blob/v4.0.0rc1/docs/CONFORMANCE.md)
-runs 1774 conformance checks across 108 domains and 516 standards, each pinning
+runs 1786 conformance checks across 109 domains and 518 standards, each pinning
an expected normative value to the value the library computes, and CI
regenerates it on every pull request. Filters meet the class 1 mask of
**IEC 61260-1:2014 / ANSI S1.11-2004**, and the octave and one-third-octave
@@ -44,7 +44,7 @@ banks are class 1 on every requirement the 2014 edition grades on a design.
Weightings and levels are class 1 per
**IEC 61672-1:2013**.
-
+
diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md
index ee5cb2a6f..1d966dc96 100644
--- a/docs/CONFORMANCE.md
+++ b/docs/CONFORMANCE.md
@@ -19,7 +19,7 @@
## Numerical conformance report
-**1774/1774 conformance checks pass** across 108 domains and 516 standards - filters class 1 - weightings within IEC 61672-1 class 1.
+**1786/1786 conformance checks pass** across 109 domains and 518 standards - filters class 1 - weightings within IEC 61672-1 class 1.
Δ is the difference between the computed value and the one the standard publishes. Used is how much of that clause's published tolerance the difference consumes: 100 % means it sits exactly on the limit, 5 % means it uses a twentieth of the allowance, and a dash means the clause states no two-sided tolerance for the quantity, so there is no budget to spend. It is reported and never used to decide a verdict, which is settled at full precision before any rounding.
@@ -2223,6 +2223,26 @@ Only **Butterworth** (the library default) and **Chebyshev-II** are class-compli
+
+Microphone calibration by comparison (IEC 61094-5, IEC 61094-8): 100% (12/12)
+
+| Standard | Quantity | Expected (norm) | Computed | Δ | Used | Status |
+|:---|:---|:---|:---|:---|:---:|:---:|
+| IEC 61094-5:2016 Table D.1 | Standard uncertainty at 2 kHz of the 7 components whose row states a value | 7/7 components of Table D.1 | 7/7 components of Table D.1 | exact | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 D.3 | Combined standard uncertainty of the example budget at 2 kHz | 0.0437 dB (printed 0,040, an erratum) | 0.0437 dB | 0 dB | 62 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 D.2, D.3 | Expanded uncertainty (k = 2) of the example budget at 2 kHz | 0.087 dB (printed 0,08, an erratum) | 0.087 dB | 0 dB | 68 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 D.3 | Combined standard uncertainty by the strict calculation in linear form | 0.043614 dB (0.043669 dB in decibels) | 0.043614 dB | 0 dB | 24 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 Table A.1 | Correction of a WS3 microphone against an LS2aP in the jig of Figure A.4, 14 frequencies | 14/14 corrections of Table A.1 | 14/14 corrections of Table A.1 | exact | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 Table A.1 NOTE | Expanded uncertainty of the correction at 20 kHz, a tenth of its value | 0.1443 dB (+/-0 dB) | 0.1443 dB | 0 dB | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 Formulas (C.1) to (C.3) | Sensitivity level of the test microphone through the interchange, whatever the channel gains, source drift and field asymmetry | the test microphone's level at all 5 frequencies | max deviation 0.000000000 dB | 0 dB | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-5:2016 D.2 | M_test = M_ref x R_V / R_P in linear form against the level form | M_ref x R_V / R_P at all 5 frequencies | max relative deviation 0.000000000000 | 0 | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-8:2012 A.2 | Monitor ratios cancel a source that drifts between the two measurements | the test microphone's level at all 5 frequencies | max deviation 0.000000000 dB | 0 dB | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-8:2012 Table 1 | Typical expanded uncertainty of the 5 reference calibration options at 1 kHz and 10 kHz | 10/10 cells of Table 1 | 10/10 cells of Table 1 | exact | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-8:2012 Table 2 | Source of uncertainty and subclause references of the 12 typical components | 12/12 rows of Table 2 | 12/12 rows of Table 2 | exact | 0 % | ![Pass][cv-pass] Pass |
+| IEC 61094-8:2012 Formula (B.1), Figure B.1 | A reflection from the boundary of the effective free-field region arrives at the end of a 5 ms window | 5 ms (+/-0 ms) | 5 ms | 0 ms | 0 % | ![Pass][cv-pass] Pass |
+
+
+
Audio-frequency induction loops (IEC 60118-4:2014+A1, IEC 62489-1:2010+A1): 100% (67/67)
diff --git a/docs/ERRATA.es.md b/docs/ERRATA.es.md
index adae59770..7999b4dfb 100644
--- a/docs/ERRATA.es.md
+++ b/docs/ERRATA.es.md
@@ -2488,6 +2488,127 @@ dos ediciones con las mismas entradas y en el mismo orden.
- **Estado:** sin notificar (defecto de referencia cruzada, sin consecuencia
numérica).
+## IEC 61094-5:2016, D.3 (una incertidumbre combinada que sus propias componentes no dan)
+
+- **Ubicación:** Anexo D, apartado D.3 «Combined and expanded uncertainties»
+ (folio impreso 21), que combina las ocho componentes de la Tabla D.1 (folio
+ impreso 20).
+- **El impreso:** «The combined standard uncertainty is found from the
+ root-sum-square of the uncertainty components, which gives a value of
+ **0,040 dB** [...]. The expanded uncertainty with a coverage factor of 2 is
+ then **0,08 dB**.» La Tabla D.1 imprime las ocho incertidumbres típicas
+ 0,025 (sensibilidad del micrófono de referencia), 0,006 (capacidad), 0,017
+ (no linealidad), 0,003 (impedancia), 0,005 (tensión de polarización), 0,025
+ (repetibilidad), 0,017 (deriva desde la última calibración) y 0,003
+ (redondeo) dB, y D.2 dice que la incertidumbre «arises from eight different
+ sources».
+- **El problema:** la raíz de la suma de cuadrados de la columna impresa es
+ $\sqrt{2 \times 0{,}025^2 + 2 \times 0{,}017^2 + 0{,}006^2 + 0{,}005^2 + 2
+ \times 0{,}003^2} = \sqrt{0{,}001\,907} = 0{,}043\,67$ dB, que se lee
+ **0,044 dB**, no 0,040 dB; a partir de los valores que cada fila enuncia antes
+ de redondear (0,05/2, y 0,01, 0,03, 0,005, $20\lg(200{,}2/200)$, 0,03 y 0,005
+ entre $\sqrt{3}$) es 0,043 88 dB. Con $k = 2$ la incertidumbre expandida es
+ 0,087 dB, **0,09 dB** con los dos decimales que da el apartado, no 0,08 dB.
+ El 0,040 dB impreso es la raíz de la suma de cuadrados de siete de las ocho
+ componentes, dejando fuera una de las dos filas de 0,017 dB:
+ $\sqrt{0{,}001\,907 - 0{,}017^2} = 0{,}040\,2$ dB.
+- **Evidencia:** los ocho valores impresos recombinados, y los valores
+ enunciados divididos entre sus divisores. Verificado en las páginas 22 y 23
+ del PDF (pp. 20 y 21 impresas) de IEC 61094-5:2016, edición 2.0 (2016-05),
+ inglés-francés.
+- **Comportamiento de la biblioteca:** `metrology.comparison_uncertainty_budget`
+ combina las componentes que recibe, 0,0437 dB y 0,087 dB para las de la
+ Tabla D.1, y las filas de conformidad de D.3 fijan esos valores con los
+ impresos señalados como errata
+ ([`tests/metrology/test_comparison_calibration.py`](../tests/metrology/test_comparison_calibration.py)).
+- **Estado:** sin notificar.
+
+## UNE-EN 61094-5:2017, Tabla D.1 y D.3 (seis valores perdidos y la incertidumbre combinada mal impresa en la traducción)
+
+- **Ubicación:** Anexo D, Tabla D.1 «Ejemplo de balance de incertidumbres»
+ (pp. 25 y 26 impresas) y D.3 «Incertidumbres combinada y expandida» (p. 27
+ impresa) de UNE-EN 61094-5 (febrero de 2017), la versión española de
+ EN 61094-5:2016, que adopta IEC 61094-5:2016.
+- **El impreso:** la columna «Incertidumbre típica dB» de la Tabla D.1 está
+ **vacía** en seis de sus ocho filas, «No linealidad», «Impedancia del
+ micrófono», «Voltaje de polarización», «Repetibilidad», «Deriva en la
+ sensibilidad del micrófono de referencia desde la última calibración» y
+ «Redondeo de los resultados presentados»; sólo las dos primeras filas
+ imprimen un valor, 0,025 y 0,006. D.3 dice «lo que da un valor de
+ **0,004 dB**». El texto de la primera fila dice «Esto es equivalente a una
+ incertidumbre típica de **0,025/2 dB** = 0,025 dB», y el primer caso
+ especial habla de «un micrófono de tipo **WG3**».
+- **El problema:** el texto inglés imprime los seis valores que la traducción
+ pierde, 0,017, 0,003, 0,005, 0,025, 0,017 y 0,003 dB (IEC 61094-5:2016,
+ p. 20 impresa), y «0,040 dB» en D.3 (p. 21 impresa), así que el 0,004 dB
+ español es la décima parte del valor inglés e incoherente con su propia
+ incertidumbre expandida de «0,08 dB» dos líneas más abajo. El texto de la
+ fila divide entre 2 los 0,05 dB, no 0,025 dB («0,05/2 dB = 0,025 dB» en el
+ inglés), y el caso especial es un micrófono de tipo **WS3**, como dicen el
+ inglés, la Tabla A.1 de la misma traducción y el último párrafo del mismo
+ caso especial. Quien lea sólo el texto español no puede reconstruir el
+ balance: faltan seis de sus ocho componentes, y el valor combinado que
+ enuncia no es la suma de nada impreso. El valor combinado inglés es a su vez
+ una errata (la entrada sobre IEC 61094-5:2016 D.3, más arriba).
+- **Evidencia:** los dos impresos leídos en paralelo. Verificado en las
+ páginas 25 a 27 del PDF (pp. 25 a 27 impresas) de UNE-EN 61094-5:2017 y en
+ las páginas 22 y 23 del PDF (pp. 20 y 21 impresas) de IEC 61094-5:2016.
+- **Comportamiento de la biblioteca:** implementa el texto inglés, cuya Tabla
+ D.1 leen los tests y las filas de conformidad; no hizo falta ningún cambio.
+- **Estado:** sin notificar (traducción nacional, no el texto del organismo
+ emisor).
+
+## IEC 61094-8:2012, 8.4 (una referencia cruzada sin resolver)
+
+- **Ubicación:** subapartado 8.4 «Differences between the sound pressure
+ applied to the reference microphone and to the microphone under test»
+ (folio impreso 15), su primera frase.
+- **El impreso:** «As stated in **Error! Reference source not found.** the
+ basis of a comparison method is that the test and reference microphones are
+ exposed to a sound field having the same modulus, phase and angle of
+ incidence.»
+- **El problema:** en lugar de la referencia cruzada se imprimió un campo sin
+ resolver del procesador de textos, así que la frase no remite a nada. Lo que
+ parafrasea es el principio general de 5.1: «When a calibrated reference
+ microphone and a microphone under test are exposed to the same free-field
+ sound pressure [...]» (folio impreso 8).
+- **Evidencia:** la frase tal como se imprime, leída contra 5.1. Verificado en
+ la página 17 del PDF (p. 15 impresa) de BS EN 61094-8:2012, el texto inglés
+ de EN 61094-8:2012, que es IEC 61094-8:2012 sin cambios.
+- **Comportamiento de la biblioteca:** nada del texto depende de la
+ referencia; no hizo falta ningún cambio.
+- **Estado:** sin notificar (defecto de referencia cruzada, sin consecuencia
+ numérica).
+
+## IEC 61094-8:2012, B.6.1, Fórmula (B.10) (el espectro de un pulso de duración 2b llamado de duración b)
+
+- **Ubicación:** Anexo B, B.6.1 «Outline of methods» del método de excitación
+ por impulso directo (folio impreso 28), la Fórmula (B.10) y las frases que
+ la rodean.
+- **El impreso:** «The Fourier transform, $X(f)$, of a rectangular pulse of
+ **duration $b$** and amplitude $a$ is $X(f) = \dfrac{2ab\sin(2\pi f b)}{2\pi
+ f b}$ (B.10). The first zero in the spectrum is at $f = 1/(2b)$. [...]
+ leading to a requirement for the duration, $b$ of just a few microseconds.»
+- **El problema:** la Fórmula (B.10) es la transformada de un pulso de
+ amplitud $a$ que dura de $-b$ a $b$, es decir, de duración **$2b$**: su valor
+ en $f = 0$ es el área del pulso, $2ab$, y su primer cero, donde $2\pi f b =
+ \pi$, está en $1/(2b)$, como dice el texto. Un pulso de duración $b$ tiene la
+ transformada $ab \sin(\pi f b)/(\pi f b)$ y su primer cero en $1/b$. La
+ fórmula y el primer cero concuerdan entre sí; las palabras «duration $b$»
+ discrepan de ambos, y han de leerse «semiduración $b$» o «duración $2b$». La
+ conclusión práctica se mantiene con cualquiera de las dos lecturas: un primer
+ cero diez veces por encima de un límite superior de 20 kHz, en 200 kHz, pide
+ un pulso de 5 µs ($b$ = 2,5 µs según la fórmula), «just a few
+ microseconds».
+- **Evidencia:** la transformada del pulso rectangular evaluada en frecuencia
+ cero y en su primer cero. Verificado en la página 30 del PDF (p. 28 impresa)
+ de BS EN 61094-8:2012, el texto inglés de EN 61094-8:2012, que es
+ IEC 61094-8:2012 sin cambios.
+- **Comportamiento de la biblioteca:** el método de impulso directo, que B.6.2
+ da por «largely superseded», no está implementado; no hizo falta ningún
+ cambio.
+- **Estado:** sin notificar.
+
## UNE-EN ISO 9614-1:2010, apartado 9.1 (el signo perdido de «signed magnitude» en la traducción)
- **Ubicación:** apartado 9.1, la lista de símbolos bajo la Fórmula (11)
diff --git a/docs/ERRATA.md b/docs/ERRATA.md
index 0e37192f2..fa85a76c3 100644
--- a/docs/ERRATA.md
+++ b/docs/ERRATA.md
@@ -2313,6 +2313,122 @@ in the same order.
Annex F, so no change was required.
- **Status:** unreported (cross-reference defect, no numerical consequence).
+## IEC 61094-5:2016, D.3 (a combined uncertainty its own components do not give)
+
+- **Location:** Annex D, clause D.3 "Combined and expanded uncertainties"
+ (printed folio 21), which combines the eight components of Table D.1
+ (printed folio 20).
+- **The print:** "The combined standard uncertainty is found from the
+ root-sum-square of the uncertainty components, which gives a value of
+ **0,040 dB** [...]. The expanded uncertainty with a coverage factor of 2 is
+ then **0,08 dB**." Table D.1 prints the eight standard uncertainties 0,025
+ (sensitivity of the reference microphone), 0,006 (capacitance), 0,017
+ (non-linearity), 0,003 (impedance), 0,005 (polarising voltage), 0,025
+ (repeatability), 0,017 (drift since the last calibration) and 0,003
+ (rounding) dB, and D.2 says the uncertainty "arises from eight different
+ sources".
+- **The problem:** the root-sum-square of the printed column is
+ $\sqrt{2 \times 0{,}025^2 + 2 \times 0{,}017^2 + 0{,}006^2 + 0{,}005^2 + 2
+ \times 0{,}003^2} = \sqrt{0{,}001\,907} = 0{,}043\,67$ dB, which reads
+ **0,044 dB**, not 0,040 dB; from the values each row states before rounding
+ (0,05/2, and 0,01, 0,03, 0,005, $20\lg(200{,}2/200)$, 0,03 and 0,005 over
+ $\sqrt{3}$) it is 0,043 88 dB. With $k = 2$ the expanded uncertainty is
+ 0,087 dB, **0,09 dB** to the two decimals the clause quotes, not 0,08 dB.
+ The printed 0,040 dB is the root-sum-square of seven of the eight
+ components, with one of the two 0,017 dB rows left out:
+ $\sqrt{0{,}001\,907 - 0{,}017^2} = 0{,}040\,2$ dB.
+- **Evidence:** the eight printed values recombined, and the stated values
+ divided by their divisors. Verified on PDF pages 22 and 23 (printed pp. 20
+ and 21) of IEC 61094-5:2016, Edition 2.0 (2016-05), English-French.
+- **Library behaviour:** `metrology.comparison_uncertainty_budget` combines
+ the components it is given, 0,0437 dB and 0,087 dB for those of Table D.1,
+ and the conformance rows on D.3 pin those values with the printed ones named
+ as the erratum
+ ([`tests/metrology/test_comparison_calibration.py`](../tests/metrology/test_comparison_calibration.py)).
+- **Status:** unreported.
+
+## UNE-EN 61094-5:2017, Table D.1 and D.3 (six values dropped and the combined uncertainty misprinted in translation)
+
+- **Location:** Anexo D, Tabla D.1 "Ejemplo de balance de incertidumbres"
+ (printed pp. 25 and 26) and D.3 "Incertidumbres combinada y expandida"
+ (printed p. 27) of UNE-EN 61094-5 (February 2017), the Spanish version of
+ EN 61094-5:2016, which adopts IEC 61094-5:2016.
+- **The print:** the "Incertidumbre típica dB" column of Table D.1 is **empty**
+ for six of its eight rows, "No linealidad", "Impedancia del micrófono",
+ "Voltaje de polarización", "Repetibilidad", "Deriva en la sensibilidad del
+ micrófono de referencia desde la última calibración" and "Redondeo de los
+ resultados presentados"; only the first two rows print a value, 0,025 and
+ 0,006. D.3 reads "lo que da un valor de **0,004 dB**". The first row's text
+ reads "Esto es equivalente a una incertidumbre típica de **0,025/2 dB** =
+ 0,025 dB", and the first special case speaks of "un micrófono de tipo
+ **WG3**".
+- **The problem:** the English text prints the six values the translation
+ drops, 0,017, 0,003, 0,005, 0,025, 0,017 and 0,003 dB (IEC 61094-5:2016,
+ printed p. 20), and "0,040 dB" in D.3 (printed p. 21), so the Spanish
+ 0,004 dB is a tenth of the English value and inconsistent with its own
+ "0,08 dB" expanded uncertainty two lines further on. The row text divides
+ 0,05 dB, not 0,025 dB, by 2 ("0,05/2 dB = 0,025 dB" in the English), and
+ the special case is a type **WS3** microphone, as the English, Table A.1 of
+ the same translation and the last paragraph of the same special case say. A
+ reader of the Spanish text alone cannot rebuild the budget: six of its eight
+ components are missing, and the combined value it states is not the sum of
+ anything printed. The English combined value is itself an erratum (the
+ entry on IEC 61094-5:2016 D.3 above).
+- **Evidence:** the two prints read side by side. Verified on PDF pages 25
+ to 27 (printed pp. 25 to 27) of UNE-EN 61094-5:2017 and on PDF pages 22 and
+ 23 (printed pp. 20 and 21) of IEC 61094-5:2016.
+- **Library behaviour:** implements the English text, whose Table D.1 the
+ tests and the conformance rows read; no change was required.
+- **Status:** unreported (national translation, not the issuing body's text).
+
+## IEC 61094-8:2012, 8.4 (an unresolved cross-reference)
+
+- **Location:** subclause 8.4 "Differences between the sound pressure applied
+ to the reference microphone and to the microphone under test" (printed
+ folio 15), its first sentence.
+- **The print:** "As stated in **Error! Reference source not found.** the
+ basis of a comparison method is that the test and reference microphones are
+ exposed to a sound field having the same modulus, phase and angle of
+ incidence."
+- **The problem:** a word processor's unresolved field was printed in place
+ of the cross-reference, so the sentence refers to nothing. The statement it
+ paraphrases is the general principle of 5.1: "When a calibrated reference
+ microphone and a microphone under test are exposed to the same free-field
+ sound pressure [...]" (printed folio 8).
+- **Evidence:** the sentence as printed, read against 5.1. Verified on PDF
+ page 17 (printed p. 15) of BS EN 61094-8:2012, the English text of
+ EN 61094-8:2012, which is IEC 61094-8:2012 unchanged.
+- **Library behaviour:** none of the text depends on the reference; no change
+ was required.
+- **Status:** unreported (cross-reference defect, no numerical consequence).
+
+## IEC 61094-8:2012, B.6.1, Formula (B.10) (the spectrum of a pulse of duration 2b called one of duration b)
+
+- **Location:** Annex B, B.6.1 "Outline of methods" of the direct impulse
+ excitation method (printed folio 28), Formula (B.10) and the sentences
+ around it.
+- **The print:** "The Fourier transform, $X(f)$, of a rectangular pulse of
+ **duration $b$** and amplitude $a$ is $X(f) = \dfrac{2ab\sin(2\pi f b)}{2\pi
+ f b}$ (B.10). The first zero in the spectrum is at $f = 1/(2b)$. [...]
+ leading to a requirement for the duration, $b$ of just a few microseconds."
+- **The problem:** Formula (B.10) is the transform of a pulse of amplitude
+ $a$ that lasts from $-b$ to $b$, that is of duration **$2b$**: its value at
+ $f = 0$ is the area of the pulse, $2ab$, and its first zero, where $2\pi f b
+ = \pi$, is at $1/(2b)$, as the text says. A pulse of duration $b$ has the
+ transform $ab \sin(\pi f b)/(\pi f b)$ and its first zero at $1/b$. The
+ formula and the first zero agree with each other; the words "duration $b$"
+ disagree with both, and read "half-duration $b$" or "duration $2b$". The
+ practical conclusion stands on either reading: a first zero ten times above
+ a 20 kHz upper limit, at 200 kHz, asks for a pulse of 5 µs ($b$ = 2,5 µs by
+ the formula), "just a few microseconds".
+- **Evidence:** the transform of the rectangular pulse evaluated at zero
+ frequency and at its first zero. Verified on PDF page 30 (printed p. 28) of
+ BS EN 61094-8:2012, the English text of EN 61094-8:2012, which is
+ IEC 61094-8:2012 unchanged.
+- **Library behaviour:** the direct impulse method, which B.6.2 says has
+ "been largely superseded", is not implemented; no change was required.
+- **Status:** unreported.
+
## UNE-EN ISO 9614-1:2010, clause 9.1 (the sign dropped from "signed magnitude" in translation)
- **Location:** clause 9.1, the symbol list under Formula (11)
diff --git a/docs/README.md b/docs/README.md
index 92f0ad5e0..9ef3aae42 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -62,6 +62,7 @@ Every other section consumes it.
- [Random-incidence and diffuse-field response (IEC 61183)](signals/metrology/random-incidence.md): the directivity factor of a sound level meter from readings in two planes, one plane or 38 equal-area elements with the Table A.1 adjustment factors, its random-incidence sensitivity level, and its diffuse-field sensitivity level by comparison with a reference whose Table B.1 characteristics are built in
- [Free-field corrections of a sound level meter (IEC 62585)](signals/metrology/free-field-corrections.md): the adjustment value at the calibration check frequency, the free-field corrections for a multi-frequency calibrator, a comparison coupler and an electrostatic actuator, the uncertainty budget of Annex I with its Welch-Satterthwaite coverage factor, and the maxima of clauses 9 to 14 as a verdict and a fiche
- [Periodic tests of a sound level meter (IEC 61672-3)](signals/metrology/sound-level-meter-periodic-tests.md): the verdict on a laboratory's periodic-test results, clause by clause against the acceptance limits of IEC 61672-1 and the maxima of its Table B.1, what a complete test holds, and the statement of Clause 22.
+- [Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)](signals/metrology/comparison-calibration.md): a working standard microphone calibrated against a reference in a coupler (simultaneous, with the interchange of Annex C) or in a free field (by substitution against a monitor microphone), the environmental and WS3 jig corrections, the uncertainty budgets of IEC 61094-5 Table D.1 and IEC 61094-8 Table 2 with k = 2, and the effective free-field region of a time window
### [The medium](fluids/index.md)
diff --git a/docs/conformance.json b/docs/conformance.json
index cd2391f0f..892dae953 100644
--- a/docs/conformance.json
+++ b/docs/conformance.json
@@ -3,13 +3,13 @@
"library": "4.0.0rc1",
"generator": "scripts/conformance_report.py",
"counts": {
- "checks": 1774,
- "passing": 1774,
+ "checks": 1786,
+ "passing": 1786,
"failing": 0,
- "domains": 108,
- "standards": 516,
- "citations": 1232,
- "designations": 220,
+ "domains": 109,
+ "standards": 518,
+ "citations": 1243,
+ "designations": 222,
"sources": 123
},
"units": [
@@ -662,6 +662,12 @@
"checks": 16,
"passing": 16
},
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "title": "Microphone calibration by comparison (IEC 61094-5, IEC 61094-8)",
+ "checks": 12,
+ "passing": 12
+ },
{
"id": "audio-frequency-induction-loops-iec-60118-4-2014-a1-iec-62489-1-2010-a1",
"title": "Audio-frequency induction loops (IEC 60118-4:2014+A1, IEC 62489-1:2010+A1)",
@@ -48570,6 +48576,414 @@
"precision": 3,
"verdict": "pass"
},
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-table-d-1/standard-uncertainty-at-2-khz-of-the-7-components-whose-row-states-a-value",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 Table D.1",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "Table D.1"
+ }
+ ]
+ },
+ "quantity": "Standard uncertainty at 2 kHz of the 7 components whose row states a value",
+ "kind": "count",
+ "expected": {
+ "value": 7.0,
+ "label": "7/7 components of Table D.1"
+ },
+ "computed": {
+ "value": 7.0,
+ "label": "7/7 components of Table D.1"
+ },
+ "tolerance": {
+ "mode": "absolute",
+ "value": 0.0
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 0,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-d-3/combined-standard-uncertainty-of-the-example-budget-at-2-khz",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 D.3",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "D.3"
+ }
+ ]
+ },
+ "quantity": "Combined standard uncertainty of the example budget at 2 kHz",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.0437,
+ "label": "0.0437 dB (printed 0,040, an erratum)"
+ },
+ "computed": {
+ "value": 0.0437
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 5e-05
+ },
+ "deviation": {
+ "value": -3.08e-05
+ },
+ "precision": 4,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-d-2-d-3/expanded-uncertainty-k-2-of-the-example-budget-at-2-khz",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 D.2, D.3",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "D.2, D.3"
+ }
+ ]
+ },
+ "quantity": "Expanded uncertainty (k = 2) of the example budget at 2 kHz",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.087,
+ "label": "0.087 dB (printed 0,08, an erratum)"
+ },
+ "computed": {
+ "value": 0.087
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 0.0005
+ },
+ "deviation": {
+ "value": 0.000338
+ },
+ "precision": 3,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-d-3/combined-standard-uncertainty-by-the-strict-calculation-in-linear-form",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 D.3",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "D.3"
+ }
+ ]
+ },
+ "quantity": "Combined standard uncertainty by the strict calculation in linear form",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.043614,
+ "label": "0.043614 dB (0.043669 dB in decibels)"
+ },
+ "computed": {
+ "value": 0.043614
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-06
+ },
+ "deviation": {
+ "value": 2.45e-07
+ },
+ "precision": 6,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-table-a-1/correction-of-a-ws3-microphone-against-an-ls2ap-in-the-jig-of-figure-a-4-14-frequencies",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 Table A.1",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "Table A.1"
+ }
+ ]
+ },
+ "quantity": "Correction of a WS3 microphone against an LS2aP in the jig of Figure A.4, 14 frequencies",
+ "kind": "count",
+ "expected": {
+ "value": 14.0,
+ "label": "14/14 corrections of Table A.1"
+ },
+ "computed": {
+ "value": 14.0,
+ "label": "14/14 corrections of Table A.1"
+ },
+ "tolerance": {
+ "mode": "absolute",
+ "value": 0.0
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 0,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-table-a-1-note/expanded-uncertainty-of-the-correction-at-20-khz-a-tenth-of-its-value",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 Table A.1 NOTE",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "Table A.1 NOTE"
+ }
+ ]
+ },
+ "quantity": "Expanded uncertainty of the correction at 20 kHz, a tenth of its value",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.1443
+ },
+ "computed": {
+ "value": 0.1443
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-12
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 4,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-formulas-c-1-to-c-3/sensitivity-level-of-the-test-microphone-through-the-interchange-whatever-the-channel-gains-source-drift-and-field-asymmetry",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 Formulas (C.1) to (C.3)",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "Formulas (C.1) to (C.3)"
+ }
+ ]
+ },
+ "quantity": "Sensitivity level of the test microphone through the interchange, whatever the channel gains, source drift and field asymmetry",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.0,
+ "label": "the test microphone's level at all 5 frequencies"
+ },
+ "computed": {
+ "value": 0.0,
+ "label": "max deviation 0.000000000 dB"
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-09
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 6,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-5-2016-d-2/m-test-m-ref-x-r-v-r-p-in-linear-form-against-the-level-form",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-5:2016 D.2",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-5",
+ "edition": "2016",
+ "clause": "D.2"
+ }
+ ]
+ },
+ "quantity": "M_test = M_ref x R_V / R_P in linear form against the level form",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.0,
+ "label": "M_ref x R_V / R_P at all 5 frequencies"
+ },
+ "computed": {
+ "value": 0.0,
+ "label": "max relative deviation 0.000000000000"
+ },
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-12
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 12,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-8-2012-a-2/monitor-ratios-cancel-a-source-that-drifts-between-the-two-measurements",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-8:2012 A.2",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-8",
+ "edition": "2012",
+ "clause": "A.2"
+ }
+ ]
+ },
+ "quantity": "Monitor ratios cancel a source that drifts between the two measurements",
+ "kind": "scalar",
+ "expected": {
+ "value": 0.0,
+ "label": "the test microphone's level at all 5 frequencies"
+ },
+ "computed": {
+ "value": 0.0,
+ "label": "max deviation 0.000000000 dB"
+ },
+ "unit": "dB",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-09
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 6,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-8-2012-table-1/typical-expanded-uncertainty-of-the-5-reference-calibration-options-at-1-khz-and-10-khz",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-8:2012 Table 1",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-8",
+ "edition": "2012",
+ "clause": "Table 1"
+ }
+ ]
+ },
+ "quantity": "Typical expanded uncertainty of the 5 reference calibration options at 1 kHz and 10 kHz",
+ "kind": "count",
+ "expected": {
+ "value": 10.0,
+ "label": "10/10 cells of Table 1"
+ },
+ "computed": {
+ "value": 10.0,
+ "label": "10/10 cells of Table 1"
+ },
+ "tolerance": {
+ "mode": "absolute",
+ "value": 0.0
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 0,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-8-2012-table-2/source-of-uncertainty-and-subclause-references-of-the-12-typical-components",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-8:2012 Table 2",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-8",
+ "edition": "2012",
+ "clause": "Table 2"
+ }
+ ]
+ },
+ "quantity": "Source of uncertainty and subclause references of the 12 typical components",
+ "kind": "count",
+ "expected": {
+ "value": 12.0,
+ "label": "12/12 rows of Table 2"
+ },
+ "computed": {
+ "value": 12.0,
+ "label": "12/12 rows of Table 2"
+ },
+ "tolerance": {
+ "mode": "absolute",
+ "value": 0.0
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 0,
+ "verdict": "pass"
+ },
+ {
+ "id": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8/iec-61094-8-2012-formula-b-1-figure-b-1/a-reflection-from-the-boundary-of-the-effective-free-field-region-arrives-at-the-end-of-a-5-ms-window",
+ "domain": "microphone-calibration-by-comparison-iec-61094-5-iec-61094-8",
+ "reference": {
+ "cite": "IEC 61094-8:2012 Formula (B.1), Figure B.1",
+ "documents": [
+ {
+ "kind": "standard",
+ "designation": "IEC 61094-8",
+ "edition": "2012",
+ "clause": "Formula (B.1), Figure B.1"
+ }
+ ]
+ },
+ "quantity": "A reflection from the boundary of the effective free-field region arrives at the end of a 5 ms window",
+ "kind": "scalar",
+ "expected": {
+ "value": 5.0
+ },
+ "computed": {
+ "value": 5.0
+ },
+ "unit": "ms",
+ "tolerance": {
+ "mode": "absolute",
+ "value": 1e-09
+ },
+ "deviation": {
+ "value": 0.0
+ },
+ "precision": 6,
+ "verdict": "pass"
+ },
{
"id": "audio-frequency-induction-loops-iec-60118-4-2014-a1-iec-62489-1-2010-a1/iec-60118-4-2014-3-1/level-of-400-ma-m-the-reference-magnetic-field-strength",
"domain": "audio-frequency-induction-loops-iec-60118-4-2014-a1-iec-62489-1-2010-a1",
diff --git a/docs/reference/api/index.md b/docs/reference/api/index.md
index 66be1476b..d28b2f71f 100644
--- a/docs/reference/api/index.md
+++ b/docs/reference/api/index.md
@@ -1844,6 +1844,23 @@ documents, one page per module.
| `ELECTRICAL_TEST_FREQUENCIES_HZ` | `mapping` | **IEC 61672-3:2013 13.4 by class**: the nine octaves from 63 Hz to 16 kHz for class 1, the eight to 8 kHz for class 2 | `metrology.ELECTRICAL_TEST_FREQUENCIES_HZ[2][-1] # 8000.0` |
| `TONEBURST_TEST_DURATIONS_MS` | `mapping` | **IEC 61672-3:2013 18.5 to 18.7**: 200 ms, 2 ms and 0.25 ms for `'F'` and `'E'`, 200 ms and 2 ms for `'S'` | `metrology.TONEBURST_TEST_DURATIONS_MS['S'] # (200.0, 2.0)` |
| `PERIODIC_TEST_ENVIRONMENT` | `mapping` | **IEC 61672-3:2013 7.1**: 80 kPa to 105 kPa, 20 °C to 26 °C, 25 % to 70 %, keyed by the record field of each | `metrology.PERIODIC_TEST_ENVIRONMENT['air_temperatures_c'] # (20.0, 26.0)` |
+| `simultaneous_comparison` | `function` | **Sensitivity level by comparison with both microphones in the field at once (IEC 61094-5 5.1.2 and Annex C; IEC 61094-8 5.3).** • `frequencies_hz` [Hz]; `reference_sensitivity_level_db` L_ref [dB re 1 V/Pa] • `channel_difference_db` L_C12, reference on channel 1, and `interchanged_channel_difference_db` L_C21 [dB], one row per determination; 20 lg R_V = -(L_C12 - L_C21)/2 (C.3); required for `field="pressure"` (Default: None) • keyword-only: `field` ('pressure' / 'free_field', Default: 'pressure'); `pressure_level_difference_db` 20 lg R_P [dB] (Default: 0); `corrections_db` by name [dB]; `reference_environment` (added) and `test_environment` (subtracted), `EnvironmentalSensitivityCorrection`; `reference_free_field_difference_db` [dB], free field only; `expanded_uncertainty_db` [dB] (all Default: None) | `c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)`
• `ComparisonCalibration` |
+| `sequential_comparison` | `function` | **Sensitivity level by comparison with the microphones in the field in turn (IEC 61094-5 5.1.3 and Annex B; IEC 61094-8 5.2 and A.2).** • `frequencies_hz` [Hz]; `reference_sensitivity_level_db` [dB re 1 V/Pa]; `reference_output_level_db` 20 lg V_ref, `test_output_level_db` 20 lg V_test [dB], one row per determination • keyword-only: `reference_monitor_level_db` 20 lg V_mon,1 and `test_monitor_level_db` 20 lg V_mon,2 [dB], both or neither (Default: None); 20 lg R_V = 20 lg(V_test/V_mon,2) - 20 lg(V_ref/V_mon,1) • `field`, `pressure_level_difference_db`, `corrections_db`, `reference_environment`, `test_environment`, `reference_free_field_difference_db`, `expanded_uncertainty_db` as in `simultaneous_comparison` | `metrology.sequential_comparison(f, l_ref, v_ref, v_test, field="free_field")`
• `ComparisonCalibration` |
+| `ComparisonCalibration` | `dataclass` | **The sensitivity level of a microphone calibrated by comparison, L_test = L_ref + 20 lg R_V - 20 lg R_P + Σ C_j (IEC 61094-5 D.2).** • `frequencies_hz` [Hz], `reference_sensitivity_level_db`, `output_level_differences_db` (determinations × frequencies), `pressure_level_difference_db` [dB], `corrections_db` (read-only mapping) [dB], `field`, `excitation`, `expanded_uncertainty_db` [dB] or None • `.sensitivity_level_db` [dB re 1 V/Pa], `.sensitivity_mv_per_pa` [mV/Pa], `.output_level_difference_db`, `.correction_db` [dB], `.determinations`, `.standard` • `.plot()`: L_test with its U band and the reference's level (its free-field level, L_ref plus the free-field difference, against a pressure-calibrated reference in a free field) | `c.sensitivity_level_db, c.standard # ..., 'IEC 61094-5:2016'` |
+| `environmental_sensitivity_correction` | `function` | **First-order correction of a sensitivity level between two sets of environmental conditions (IEC 61094-5 6.6, IEC 61094-8 7.6).** • `frequencies_hz` [Hz] • keyword-only: `static_pressure_kpa` [kPa], `temperature_c` [°C], `relative_humidity_percent` [%], where the level is wanted; `static_pressure_coefficient_db_per_kpa` [dB/kPa] and `temperature_coefficient_db_per_k` [dB/K], the microphone's own (IEC 61094-2 Annex D units); `humidity_coefficient_db_per_percent` [dB/%] (Default: 0); `reference_static_pressure_kpa`, `reference_temperature_c`, `reference_relative_humidity_percent` (Default: 101.325 kPa, 23.0 °C, 50 %, clause 4) | `metrology.environmental_sensitivity_correction(f, static_pressure_kpa=97.0, temperature_c=26.0, relative_humidity_percent=40.0, static_pressure_coefficient_db_per_kpa=-0.005, temperature_coefficient_db_per_k=0.003)`
• `EnvironmentalSensitivityCorrection` |
+| `EnvironmentalSensitivityCorrection` | `dataclass` | **C_env = δp (p_s - p_s,0) + δt (t - t_0) + δH (H - H_0), at each frequency.** • `frequencies_hz` [Hz], the conditions and the reference conditions, the three coefficient columns, read-only • `.static_pressure_term_db`, `.temperature_term_db`, `.humidity_term_db`, `.correction_db` [dB] • `.plot()`: the three terms and the total | `e.correction_db` |
+| `jig_diameter_correction` | `function` | **Corrections of a WS3 microphone against an LS2aP in the jig of Figure A.4 (IEC 61094-5 Table A.1).** • `frequencies_hz` [Hz], rows of the table, an exact base-ten frequency within 2 % reading as its row (Default: None, the 14 rows from 1 kHz to 20 kHz) • refuses a frequency the table does not print | `metrology.jig_diameter_correction([8000.0]).correction_db # [-0.235]`
• `JigDiameterCorrection` |
+| `JigDiameterCorrection` | `dataclass` | **The Table A.1 corrections at the frequencies asked for.** • `frequencies_hz` [Hz], `correction_db` [dB], read-only • `.expanded_uncertainty_db` [dB], a tenth of the correction (NOTE); `.standard_uncertainty_db` [dB], its half, read with the k = 2 of 7.9 and D.2 since the 10 % comes without one, the special-case component of Table D.1 • `.plot()`: the corrections and their band | `j.expanded_uncertainty_db` |
+| `comparison_uncertainty_budget` | `function` | **The uncertainty budget of a comparison calibration at one frequency (IEC 61094-5 Annex D; IEC 61094-8 8.8, Table 2).** • `standard_uncertainties_db`: each component a standard uncertainty [dB] or a `Quantity`, keyed as `IEC61094_5_TABLE_D1` (pressure) or `IEC61094_8_TABLE_2` (free field); a subset is allowed • keyword-only: `frequency_hz` [Hz]; `field` (Default: 'pressure'); `additional_components`: named `Quantity` objects (Default: none); `coverage_factor` (Default: 2) • on `combine_uncertainty`, sensitivities 1, U = k u_c | `b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000.0)`
• `b.expanded_uncertainty_db # 0.087 (D.3 prints 0,08, an erratum)` |
+| `ComparisonUncertaintyBudget` | `dataclass` | **One comparison budget at one frequency.** • `frequency_hz` [Hz], `field`, `names`, `components`, `standard_uncertainties_db` [dB], `uncertainty` (the `UncertaintyResult`, refused unless it is their root-sum-square), `coverage_factor` • `.combined_uncertainty_db`, `.expanded_uncertainty_db`, `.linear_combined_uncertainty_db` [dB], `.standard` • `.plot()`: one bar per component, with u_c, k and U | `b.combined_uncertainty_db # 0.0437` |
+| `IEC61094_5_TABLE_D1` | `mapping` | **IEC 61094-5:2016 Table D.1, the example budget at 2 kHz, keyed 'reference', 'capacitance', 'non_linearity', 'impedance', 'polarizing_voltage', 'repeatability', 'drift', 'rounding'.** • each value a `ComparisonUncertaintyRow` with the printed standard uncertainty and, for the seven rows that state one, the stated value and its divisor (√3 or 2); the repeatability states none | `metrology.IEC61094_5_TABLE_D1["drift"].standard_uncertainty_db # 0.017` |
+| `IEC61094_8_TABLE_2` | `mapping` | **IEC 61094-8:2012 Table 2, the 12 typical uncertainty components, keyed 'reference' to 'repeatability'.** • each value a `ComparisonUncertaintyRow` with its subclause references; the table prints no values | `metrology.IEC61094_8_TABLE_2["positioning"].subclauses # ('7.3', '8.4')` |
+| `ComparisonUncertaintyRow` | `dataclass` | **One row of Table D.1 or Table 2.** • `component` as printed, `subclauses`, `stated_db` [dB], `divisor`, `standard_uncertainty_db` [dB] (the last three None for Table 2; `stated_db` and `divisor` None for the repeatability of Table D.1) | `metrology.IEC61094_5_TABLE_D1["reference"].divisor # 2.0` |
+| `IEC61094_5_TABLE_A1` | `mapping` | **IEC 61094-5:2016 Table A.1, the WS3 corrections in the jig of Figure A.4, keyed by frequency [Hz].** • 1 kHz to 20 kHz, -0.004 dB to -1.443 dB | `metrology.IEC61094_5_TABLE_A1[20000.0] # -1.443` |
+| `IEC61094_8_TABLE_1` | `mapping` | **IEC 61094-8:2012 Table 1, the ways the reference microphone can be calibrated, keyed 'primary_free_field', 'primary_pressure', 'secondary_pressure', 'secondary_free_field', 'electrostatic_actuator'.** • each value a `ReferenceCalibrationRow` | `metrology.IEC61094_8_TABLE_1["primary_pressure"].expanded_uncertainty_10khz_db # 0.4` |
+| `ReferenceCalibrationRow` | `dataclass` | **One row of Table 1.** • `microphone_types`, `method`, `references`, `expanded_uncertainty_1khz_db`, `expanded_uncertainty_10khz_db` [dB, k = 2] | `metrology.IEC61094_8_TABLE_1["primary_free_field"].references # ('IEC 61094-3',)` |
+| `free_field_region` | `function` | **The effective free-field region of a time window, A = d + τc (IEC 61094-8 B.1).** • `source_distance_m` d [m], `window_time_s` τ [s] • keyword-only: `temperature_c` [°C], `static_pressure_kpa` [kPa], `relative_humidity_percent` [%] (Default: 23.0, 101.325, 50, clause 4); c from the IEC 61094-2 Annex F air | `metrology.free_field_region(1.0, 0.005).major_axis_m # 2.73`
• `FreeFieldRegion` |
+| `FreeFieldRegion` | `dataclass` | **The prolate spheroid with the source and the microphone at its foci.** • `source_distance_m` [m], `window_time_s` [s], `speed_of_sound` [m/s] • `.major_axis_m` A, `.semi_minor_axis_m` b, `.rod_clearance_m` τc/2 [m] • `.plot()`: the region as Figure B.1 draws it | `r.semi_minor_axis_m # 1.27` |
| `combine_uncertainty` | `function` | **GUM law of propagation of uncertainty (ISO/IEC Guide 98-3 clause 5).** • `model`: measurement function f(x1…xN) • `quantities`: input `Quantity` objects, in argument order • `correlation`: optional N×N matrix r_ij (Default: None → uncorrelated; non-identity with finite input dof → effective dof undefined, `expanded()` needs `coverage_factor_override`) | `u = metrology.combine_uncertainty(lambda a, b: a*b, [metrology.Quantity(10.0, 0.1), metrology.Quantity(5.0, 0.2)])`
• `MonteCarloResult` (mean, u, coverage interval) |
| `rectangular` | `function` | **Type B quantity, rectangular PDF (GUM 4.3.7).** • `value`, `half_width` a, `name` | `rectangular(20.0, 0.5)`
• `Quantity` with u = a/√3 = 0.289 |
@@ -2285,7 +2302,7 @@ documents, one page per module.
| `TonalityWarning` | `warning class` | **Tonality advisory.** Emitted for biased tonality estimates (e.g. coarse FFT resolution) | `warnings.simplefilter('error', TonalityWarning)` |
| `STIWarning` | `warning class` | **STI/STIPA advisory.** Emitted for suspect speech-intelligibility measurements or inputs | `warnings.simplefilter('error', STIWarning)` |
| `__version__` | `str` | **Package version string.** (no parameters) | `phonometry.__version__ # '4.0.0rc1'` |
-| `.plot()` | `method` | **One-line canonical figure on every result object (soft matplotlib dependency).** Available on `ZwickerLoudness`, `MooreGlasbergLoudness`, `MooreGlasbergTimeVaryingLoudness`, `EcmaLoudness`, `EcmaTonality`, `EcmaRoughness`, `PsychoacousticAnnoyanceResult`, `FluctuationStrengthResult`, `ProgramLoudnessResult`, `KWeightingResponse`, `STIResult`, `SIIResult`, `SIIProcedure`, `StandardSpeechSpectrum`, `NCResult`, `RCResult`, `AgeThresholdResult`, `NiptsResult`, `HtlanResult`, `ImpulseProminenceResult`, `ImpulsiveSoundResult`, `SelDistribution`, `LevelDifferenceQuantiles`, `RoundRobinPrecision`, `CalculationVerification`, `DirectivityFactor`, `RandomIncidenceSensitivity`, `DiffuseFieldSensitivity`, `AdjustmentValue`, `FreeFieldCorrection`, `CorrectionUncertaintyBudget`, `CorrectionUncertaintyVerification`, `SoundLevelMeterPeriodicRequirement`, `SoundLevelMeterPeriodicVerification`, `FieldStrengthReading`, `BackgroundNoiseAssessment`, `InductionLoopVerification`, `LoopRequirement`, `AmplifierOverloadVerification`, `LoopField`, `LoopImpedance`, `AmplifierFrequencyResponse`, `AgcCharacteristic`, `QuadraturePhaseError`, `MaximumOutputCurrent`, `NeckLoopCharacteristics`, `NeckLoopVerification`, `MultipleShockResult`, `ImpulseResponseResult`, `DecayCurve`, `RoomAcousticsResult`, `ReverberationResult`, `ReverberationModelResult`, `DynamicStiffnessResult`, `MobilityResult`, `TransferStiffnessResult`, `BandAveragedStiffness`, `LevelDifferenceCheck`, `OutputMassCheck`, `EffectiveBlockingMass`, `DrivingPointStiffnessResult`, `DrivingPointUncertainty`, `VibrationSoundPowerResult`, `StructureBornePowerResult`, `InstalledSourceResult`, `WeightedRatingResult`, `ImpactRatingResult`, `ImpactImprovementRatingResult`, `FacadeInsulationResult`, `LabAirborneInsulationResult`, `LabImpactInsulationResult`, `SoundPowerResult`, `SoundEnergyResult`, `ReverberationSoundPowerResult`, `ReverberationSoundEnergyResult`, `InSituSoundPowerResult`, `InDuctSoundPowerResult`, `HighFrequencySoundPowerResult`, `InverseSquareLawResult`, `FreeFieldCheck`, `SourceDirectionalityResult`, `ReferenceSourceCalibration`, `ReferenceSoundSourceVerdict`, `ReferenceSourceDriftResult`, `TurbineMeasurementSurface`, `TurbineMicrophoneArray`, `TurbineEnvironmentalCorrection`, `TurbineTestEnvironmentCheck`, `TurbineSoundPowerResult`, `TurbineNoiseDeclaration`, `SoundPowerIntensityResult`, `DiscretePointIntensityResult`, `PrecisionSoundPowerResult`, `PrecisionIntensityResult`, `IntensityResult`, `UncertaintyResult`, `AbsorptionRatingResult`, `ScatteringResult`, `DiffusionResult`, `DiffusionSpectrum`, `InsituAbsorptionResult`, `WeightingResponse`, `WeightedSpectrum`, `Signal` and `DailyVibrationExposure`. • `ax`: existing Axes, or None to build a fresh figure (Default: None) • returns the Matplotlib `Axes` (an array of Axes for multi-panel figures); never calls `plt.show()` • needs matplotlib (`pip install phonometry[plot]`) | `res.plot()` `decay_curve(ir, fs).plot()` |
+| `.plot()` | `method` | **One-line canonical figure on every result object (soft matplotlib dependency).** Available on `ZwickerLoudness`, `MooreGlasbergLoudness`, `MooreGlasbergTimeVaryingLoudness`, `EcmaLoudness`, `EcmaTonality`, `EcmaRoughness`, `PsychoacousticAnnoyanceResult`, `FluctuationStrengthResult`, `ProgramLoudnessResult`, `KWeightingResponse`, `STIResult`, `SIIResult`, `SIIProcedure`, `StandardSpeechSpectrum`, `NCResult`, `RCResult`, `AgeThresholdResult`, `NiptsResult`, `HtlanResult`, `ImpulseProminenceResult`, `ImpulsiveSoundResult`, `SelDistribution`, `LevelDifferenceQuantiles`, `RoundRobinPrecision`, `CalculationVerification`, `DirectivityFactor`, `RandomIncidenceSensitivity`, `DiffuseFieldSensitivity`, `AdjustmentValue`, `FreeFieldCorrection`, `CorrectionUncertaintyBudget`, `CorrectionUncertaintyVerification`, `ComparisonCalibration`, `EnvironmentalSensitivityCorrection`, `JigDiameterCorrection`, `ComparisonUncertaintyBudget`, `FreeFieldRegion`, `SoundLevelMeterPeriodicRequirement`, `SoundLevelMeterPeriodicVerification`, `FieldStrengthReading`, `BackgroundNoiseAssessment`, `InductionLoopVerification`, `LoopRequirement`, `AmplifierOverloadVerification`, `LoopField`, `LoopImpedance`, `AmplifierFrequencyResponse`, `AgcCharacteristic`, `QuadraturePhaseError`, `MaximumOutputCurrent`, `NeckLoopCharacteristics`, `NeckLoopVerification`, `MultipleShockResult`, `ImpulseResponseResult`, `DecayCurve`, `RoomAcousticsResult`, `ReverberationResult`, `ReverberationModelResult`, `DynamicStiffnessResult`, `MobilityResult`, `TransferStiffnessResult`, `BandAveragedStiffness`, `LevelDifferenceCheck`, `OutputMassCheck`, `EffectiveBlockingMass`, `DrivingPointStiffnessResult`, `DrivingPointUncertainty`, `VibrationSoundPowerResult`, `StructureBornePowerResult`, `InstalledSourceResult`, `WeightedRatingResult`, `ImpactRatingResult`, `ImpactImprovementRatingResult`, `FacadeInsulationResult`, `LabAirborneInsulationResult`, `LabImpactInsulationResult`, `SoundPowerResult`, `SoundEnergyResult`, `ReverberationSoundPowerResult`, `ReverberationSoundEnergyResult`, `InSituSoundPowerResult`, `InDuctSoundPowerResult`, `HighFrequencySoundPowerResult`, `InverseSquareLawResult`, `FreeFieldCheck`, `SourceDirectionalityResult`, `ReferenceSourceCalibration`, `ReferenceSoundSourceVerdict`, `ReferenceSourceDriftResult`, `TurbineMeasurementSurface`, `TurbineMicrophoneArray`, `TurbineEnvironmentalCorrection`, `TurbineTestEnvironmentCheck`, `TurbineSoundPowerResult`, `TurbineNoiseDeclaration`, `SoundPowerIntensityResult`, `DiscretePointIntensityResult`, `PrecisionSoundPowerResult`, `PrecisionIntensityResult`, `IntensityResult`, `UncertaintyResult`, `AbsorptionRatingResult`, `ScatteringResult`, `DiffusionResult`, `DiffusionSpectrum`, `InsituAbsorptionResult`, `WeightingResponse`, `WeightedSpectrum`, `Signal` and `DailyVibrationExposure`. • `ax`: existing Axes, or None to build a fresh figure (Default: None) • returns the Matplotlib `Axes` (an array of Axes for multi-panel figures); never calls `plt.show()` • needs matplotlib (`pip install phonometry[plot]`) | `res.plot()` `decay_curve(ir, fs).plot()` |
## Notes
diff --git a/docs/signals/index.md b/docs/signals/index.md
index 212388389..05b06ca47 100644
--- a/docs/signals/index.md
+++ b/docs/signals/index.md
@@ -175,6 +175,10 @@ What the numbers mean and how much to trust them.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](metrology/sound-level-meter-periodic-tests.md):
the verdict on a laboratory's periodic-test results for a working meter,
clause by clause, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](metrology/comparison-calibration.md):
+ a working standard microphone calibrated against a reference in a pressure
+ field or a free field, with the corrections and the uncertainty budget each
+ part asks for.
## What this section does not cover
diff --git a/docs/signals/metrology/comparison-calibration.md b/docs/signals/metrology/comparison-calibration.md
new file mode 100644
index 000000000..52126c45d
--- /dev/null
+++ b/docs/signals/metrology/comparison-calibration.md
@@ -0,0 +1,282 @@
+← [Documentation index](../../README.md)
+
+# Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside a reference microphone whose sensitivity is already known, or in its
+place, both are exposed to the same sound pressure, and the ratio of their
+output voltages carries the reference's sensitivity over to it. IEC
+61094-5:2016 does this in a **pressure field**, in a coupler or a jig; IEC
+61094-8:2012 does it in a **free field**, in an anechoic room or behind a
+time window. Annex D of the first part writes the model once for both,
+`M_test = M_ref · R_V / R_P`, and `metrology.comparison_calibration` computes
+it in sensitivity levels, `L_test = L_ref + 20 lg R_V - 20 lg R_P + sum(C_j)`,
+with `R_V` the ratio of the output voltages, `R_P` that of the effective sound
+pressures (reduced to unity by the procedure) and `C_j` the corrections each
+part asks for. This page runs a pressure calibration of a WS2P against an
+LS2P and a free-field calibration of a WS2F on synthetic readings, so every
+step can be checked against the sensitivities that built them.
+
+
+
+## How the measurement goes
+
+With **simultaneous excitation** both microphones sit in the field at once,
+each on its own measuring channel; in a coupler or a jig they face each other
+about 1 mm apart (IEC 61094-5 5.1.2). With **sequential excitation** they take
+the same place in turn: either the exchange does not change the sound pressure
+significantly, or any change is detected and corrected, for example with a
+monitor microphone near the source (IEC 61094-5 5.1.3, IEC 61094-8 5.2, 6.5
+and A.2).
+
+
+
+| Requirement | Value | Clause |
+| :--- | :--- | :--- |
+| Reference conditions | 23,0 °C, 101,325 kPa, 50 % relative humidity | 61094-5 and 61094-8, 4 |
+| Simultaneous, in a coupler or a jig | Diaphragms about 1 mm apart; the microphones interchanged and the measurement repeated | 61094-5, 5.1.2 |
+| Sequential | The exchange does not change the sound pressure significantly, or any change is detected and corrected, for example with a monitor microphone; a monitor, when used, senses the changes at the test position | 61094-5, 5.1.3; 61094-8, 5.2, 6.5 |
+| Free-field mounting | On a semi-infinite rod of the microphone's diameter | 61094-8, 6.7 |
+| Reported uncertainty | Expanded, with `k = 2`, at each frequency | 61094-5, 7.9; 61094-8, 8.8 |
+
+## A pressure calibration by simultaneous excitation
+
+Annex C removes the gains of the two channels and the asymmetry of the
+coupler: with the reference on channel 1 the level reading difference is
+`L_C12`, after the interchange it is `L_C21`, and Formula (C.3) gives
+`L_ref - L_test = (L_C12 - L_C21) / 2`. Here the channels differ by 0,55 dB,
+port A hears up to 0,13 dB more than port B, and each reading is repeated
+three times:
+
+```python
+import numpy as np
+from phonometry import metrology
+
+f = metrology.exact_frequencies(250, 20000, fraction=3) # 20 exact one-third octaves
+x = f / 1000
+l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2 # the LS2P's certificate, dB re 1 V/Pa
+l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4 # the WS2P: unknown
+
+gain_1, gain_2 = 0.35, -0.20 # the two measuring channels, dB
+field_a = 0.03 * np.sqrt(x) # port A of the coupler hears 0.015 dB to 0.13 dB more
+rng = np.random.default_rng(61094)
+noise = rng.normal(0.0, 0.004, (2, 3, f.size)) # three repeats of each reading
+l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0] # reference on channel 1
+l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1] # interchanged
+
+c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)
+print(c.determinations, c.sensitivity_level_db[[2, 8, 19]].round(2)) # 3 [-38.63 -38.58 -38.44]
+print(np.abs(c.sensitivity_level_db - l_true).max().round(3)) # 0.003
+```
+
+A pressure calibration without the interchange is refused, since 5.1.2
+requires it. The "mean of the two ratios" 5.1.2 asks for is taken in
+decibels, as Annex C does: the geometric mean, the only one in which the gains
+cancel exactly (the arithmetic mean of the two ratios here would sit 0,018 dB
+to 0,027 dB high).
+
+IEC 61094-5 6.6 corrects the reference's sensitivity to the conditions of the
+test when the two microphones are different models, and IEC 61094-8 7.6 does
+it whatever the models. `metrology.environmental_sensitivity_correction`
+corrects a sensitivity level for the static pressure, the temperature and the
+humidity to first order, from the microphone's own coefficients in the units
+IEC 61094-2 Annex D gives them:
+
+```python
+env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2, # the laboratory during the test
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005, # the LS2P's own coefficients
+ temperature_coefficient_db_per_k=0.002,
+) # from its certificate's 101.325 kPa, 23.0 °C and 50 %
+print(env.correction_db[0].round(4)) # 0.0076
+```
+
+## The uncertainty budget of Annex D
+
+Table D.1 is a worked budget for a set-up like this one at 2 kHz, in the
+coupler of Figure A.1, which is for frequencies up to 10 kHz (A.1; above, the
+jig of A.2 reaches 20 kHz). `metrology.IEC61094_5_TABLE_D1` holds each of its
+eight rows with its printed standard uncertainty, and the stated value and
+divisor of the seven that state one; the repeatability row states no value
+and prints 0,025 dB as a standard uncertainty.
+`metrology.comparison_uncertainty_budget` combines the components on
+`metrology.combine_uncertainty` and multiplies by `k = 2`:
+
+```python
+d1 = {
+ "reference": 0.025, "capacitance": 0.006, "non_linearity": 0.017,
+ "impedance": 0.003, "polarizing_voltage": 0.005, "repeatability": 0.025,
+ "drift": 0.017, "rounding": 0.003,
+} # Table D.1, standard uncertainties at 2 kHz
+b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000)
+print(round(b.combined_uncertainty_db, 4), round(b.expanded_uncertainty_db, 3)) # 0.0437 0.087
+print(round(b.linear_combined_uncertainty_db, 4)) # 0.0436
+```
+
+D.3 prints 0,040 dB and 0,08 dB, which are not the root-sum-square of its own
+eight components (an [erratum](../../ERRATA.md)). The strict calculation in
+linear form D.3 mentions, and does not print, gives 0,043 614 dB; the Spanish
+UNE-EN 61094-5:2017 leaves six of the eight values of Table D.1 blank and prints
+0,004 dB in D.3. With one budget per frequency, the expanded uncertainty goes
+to the calibration, which draws it as a band:
+
+```python
+budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**d1, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2}, frequency_hz=fx
+ )
+ for fx in f
+]
+u = [bb.expanded_uncertainty_db for bb in budgets]
+c = metrology.simultaneous_comparison(
+ f, l_ref, l_c12, l_c21, reference_environment=env, expanded_uncertainty_db=u
+)
+print(np.round(u, 3)[[0, 16, 19]]) # [0.087 0.101 0.205]
+print(c.sensitivity_mv_per_pa[[2, 8, 19]].round(2)) # [11.72 11.78 11.98]
+```
+
+
+
+## A WS3 microphone in the jig
+
+Table A.1 gives the corrections to add to the sensitivity level of a WS3
+microphone calibrated against an LS2aP in the jig of Figure A.4, with an
+expanded uncertainty of a tenth of each:
+
+```python
+j = metrology.jig_diameter_correction()
+print(j.correction_db[[0, 9, 13]]) # [-0.004 -0.235 -1.443]
+print(j.standard_uncertainty_db[13].round(4)) # 0.0722
+```
+
+
+
+## A free-field calibration by substitution
+
+By IEC 61094-8 A.2 each microphone is read against the monitor, and the
+quotient of the two ratios is the output ratio corrected for any change of
+the source: `20 lg R_V = 20 lg(V_test / V_mon,2) - 20 lg(V_ref / V_mon,1)`,
+with `V_ref` and `V_mon,1` the output voltages of the reference and the
+monitor in the first reading and `V_test` and `V_mon,2` those of the second.
+The reference here is an LS2P calibrated in a pressure field, so it takes
+the free-field to pressure difference of IEC/TS 61094-7 (illustrative values
+below), and the source drifts by 0,25 dB between the readings:
+
+```python
+ff = metrology.exact_frequencies(500, 20000, fraction=3)
+xf = ff / 1000
+c_ff = 0.05 * xf**1.3 # the LS2P's free-field to pressure difference: illustrative
+l_ref_p = -38.0 + 0.04 * np.log10(xf) - 0.25 * (xf / 20) ** 2 # its pressure calibration
+l_ws2f = -38.3 + 0.1 * np.log10(xf) - 0.3 * (xf / 20) ** 3 # the WS2F: unknown
+field_1 = 74.0 + 0.05 * np.sin(xf) # the free field while the reference is in place
+field_2 = field_1 + 0.25 # the source has drifted by the time the WS2F is
+monitor = -40.0 # the monitor microphone near the source, dB re 1 V/Pa
+
+
+def free_field_budget(fx):
+ x20 = fx / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x20, # pressure calibration and IEC/TS 61094-7
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x20**2,
+ "free_field": 0.03 + 0.12 * x20**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=fx,
+ field="free_field",
+ )
+
+
+free = metrology.sequential_comparison(
+ ff,
+ l_ref_p,
+ l_ref_p + c_ff + field_1, # the reference's output, dB re 1 V
+ l_ws2f + field_2, # the WS2F's output in its place
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[free_field_budget(fx).expanded_uncertainty_db for fx in ff],
+)
+print(bool(np.abs(free.sensitivity_level_db - l_ws2f).max() < 1e-9)) # True
+print(free.expanded_uncertainty_db[[0, 12, 16]].round(2)) # [0.16 0.26 0.51]
+```
+
+Table 2 of IEC 61094-8 lists the components and prints no values; the ones
+above are illustrative. `metrology.IEC61094_8_TABLE_1` holds the typical
+expanded uncertainty of each way of calibrating the reference:
+
+```python
+row = metrology.IEC61094_8_TABLE_1["primary_pressure"]
+print(row.expanded_uncertainty_1khz_db, row.expanded_uncertainty_10khz_db) # 0.12 0.4
+```
+
+## The effective free-field region of a time window
+
+A time window of length `tau` simulates a free field inside a prolate
+spheroid with the source and the microphone at its foci, `d` apart, and the
+major diameter `A = d + tau c` (Formula (B.1)), at the speed of sound of the
+IEC 61094-2 Annex F air:
+
+```python
+r = metrology.free_field_region(1.0, 0.005) # 1 m, a 5 ms window, at 23.0 °C
+print(round(r.speed_of_sound, 1), round(r.major_axis_m, 2)) # 345.9 2.73
+print(round(r.semi_minor_axis_m, 2), round(r.rod_clearance_m, 2)) # 1.27 0.86
+```
+
+
+
+## The diffuse-field comparison of IEC 61183
+
+The diffuse-field method of IEC 61183 clause 5 is a sequential comparison
+without a monitor, and the library computes it through the same model:
+
+```python
+d = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0], [94.3, 94.6], [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+)
+s = metrology.sequential_comparison([1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6])
+print(d.diffuse_field_level_db, s.sensitivity_level_db) # [-25.7 -25.7] [-25.7 -25.7]
+```
+
+## What this guide covers
+
+Implemented: the level model of IEC 61094-5 D.2; the simultaneous excitation
+with the interchange of Annex C (Formulas (C.1) to (C.3)), required for a
+pressure calibration; the sequential excitation against a monitor microphone;
+the average over the determinations; the environmental correction to first
+order (this library's reading of 6.6 and 7.6, which print no formula); the
+reference's free-field to pressure difference in a free-field calibration;
+the WS3 corrections of Table A.1, whose 10 % expanded uncertainty, printed
+without a coverage factor, is read with the `k = 2` of 7.9 and D.2; the
+budgets of Table D.1 and IEC 61094-8 Table 2 with `k = 2`; Tables A.1, D.1,
+1 and 2 as published data; and the effective free-field region of Formula
+(B.1). Not implemented: the
+measurements themselves and the phase of the sensitivity; the corrections for
+the difference of the acoustic impedances (7.4, 7.5) and for non-uniform
+pressure beyond Table A.1 (6.5); the time-selective processing of IEC 61094-8
+Annex B and the qualification of the free field by ISO 26101; the values of
+IEC/TS 61094-7 and the reciprocity calibration of the reference, which are
+inputs; the second special case of Table D.1, a microphone calibrated as a
+system with its preamplifier (0,002 dB, and under 0,02 dB above 200 Hz),
+which a budget takes through `additional_components`.
+
+## See also
+
+- [Free-field corrections of a sound level meter (IEC 62585)](free-field-corrections.md):
+ a meter compared with an LS2P reference in the same way.
+- [Random-incidence and diffuse-field response (IEC 61183)](random-incidence.md):
+ the diffuse-field comparison that computes through the same model.
+- [Measurement uncertainty](gum-uncertainty.md): the GUM law of propagation
+ the budgets are combined by.
+- [Errata in published sources](../../ERRATA.md): IEC 61094-5 D.3, UNE-EN
+ 61094-5:2017 Table D.1, IEC 61094-8 8.4 and Formula (B.10).
+- API reference: [`metrology.comparison_calibration`](https://jmrplens.github.io/phonometry/reference/api/metrology/comparison-calibration/).
diff --git a/docs/signals/metrology/index.md b/docs/signals/metrology/index.md
index e7e0696a6..62f653ca7 100644
--- a/docs/signals/metrology/index.md
+++ b/docs/signals/metrology/index.md
@@ -74,6 +74,13 @@ clause from the frequency weightings to the high-level stability against the
acceptance limits of IEC 61672-1 and the maxima of its Table B.1, what a
complete test holds, and the statement Clause 22 prescribes for the outcome.
+[Calibration by comparison](comparison-calibration.md)
+goes one step up the chain, to the microphone itself: a working standard
+microphone takes the sensitivity of a reference through the ratio of their
+output voltages, in a coupler by IEC 61094-5 or in a free field by IEC
+61094-8, with the environmental, jig and free-field corrections each part asks
+for and the uncertainty budget of Table D.1 or Table 2.
+
The same discipline extends into the frequency domain: the
[Signals and spectra](../spectra/index.md) pages
apply the Bendat & Piersol error analysis to Welch spectral estimates, so
@@ -112,6 +119,11 @@ budgets that are specialisations of the GUM machinery described here.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](sound-level-meter-periodic-tests.md):
the verdict on a laboratory's results, clause by clause, with Tables 4, 5
and B.1 of IEC 61672-1, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](comparison-calibration.md):
+ the simultaneous comparison with the interchange of Annex C, the sequential
+ one against a monitor, the environmental and WS3 jig corrections, the
+ budgets of Table D.1 and Table 2 with k = 2, and the free-field region of a
+ time window.
## What this section does not cover
diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md
index b6e2115dc..550ea208b 100644
--- a/docs/start/getting-started.md
+++ b/docs/start/getting-started.md
@@ -194,6 +194,6 @@ the normative PDF fiche.
- [Filter Architecture Gallery](../signals/filters/filter-gallery.md): choose an architecture and inspect responses
- [Calibration and dBFS](../signals/metrology/calibration.md): get real-world SPL values
- [Why phonometry](why-phonometry.md): the conformance-first design philosophy
-- [Conformance report](../CONFORMANCE.md): the expected and computed value of all 1774 checks
+- [Conformance report](../CONFORMANCE.md): the expected and computed value of all 1786 checks
- [API Reference](../reference/api/index.md): every parameter of every function
- [Bibliography](../reference/bibliography.md): the books and papers behind every guide, each with a verified link
diff --git a/docs/start/why-phonometry.md b/docs/start/why-phonometry.md
index b46d41919..272352782 100644
--- a/docs/start/why-phonometry.md
+++ b/docs/start/why-phonometry.md
@@ -187,7 +187,7 @@ sample from the metrology core:
| IEC 61252:1993 | The personal sound exposure quantities, `sound_exposure()` and the normalized 8 h level `lex_8h()`; IEC 61252:2025 has since superseded this print, and the transcription still targets the 1993+A2 text | `tests/signals/test_levels.py` |
The same discipline applies far beyond the metrology core: today the suite runs
-1774 numerical conformance checks across 108 domains and 516 standards, covering
+1786 numerical conformance checks across 109 domains and 518 standards, covering
psychoacoustics and speech intelligibility, room, building and materials
acoustics, human and machine vibration, environmental, aircraft, rotorcraft
and underwater noise, electroacoustics, broadcast loudness, industrial noise
diff --git a/llms-full.txt b/llms-full.txt
index 5839b6566..482d92a9e 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -1,8 +1,8 @@
# phonometry
-> Acoustic measurement, analysis and prediction in Python. Sound level metrology, psychoacoustics, room and building acoustics, materials, vibration, environmental and transport noise, underwater acoustics, electroacoustics and wave simulation, with every metric implemented from the text of its governing standard and 1774 numerical conformance checks against 516 standards enforced in CI.
+> Acoustic measurement, analysis and prediction in Python. Sound level metrology, psychoacoustics, room and building acoustics, materials, vibration, environmental and transport noise, underwater acoustics, electroacoustics and wave simulation, with every metric implemented from the text of its governing standard and 1786 numerical conformance checks against 518 standards enforced in CI.
-phonometry v4.0.0rc1 is a pure-Python library built on NumPy/SciPy (Python >= 3.13). Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line `.plot()` in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with `.report()`. The conformance report pins each of the 1774 checks to a standard, a clause or table, the normative expected value and the value the library computes, across 108 domains.
+phonometry v4.0.0rc1 is a pure-Python library built on NumPy/SciPy (Python >= 3.13). Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line `.plot()` in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with `.report()`. The conformance report pins each of the 1786 checks to a standard, a clause or table, the normative expected value and the value the library computes, across 109 domains.
The project was published as **PyOctaveBand** until version 3.0.0. Third-party references and dependency pins still using that name refer to this library.
@@ -90,6 +90,7 @@ If you are an AI assistant setting this up for a user: install from PyPI (no sys
- [Random-incidence and diffuse-field response (IEC 61183)](https://jmrplens.github.io/phonometry/signals/metrology/random-incidence/)
- [Free-field corrections of a sound level meter (IEC 62585)](https://jmrplens.github.io/phonometry/signals/metrology/free-field-corrections/)
- [Periodic tests of a sound level meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/)
+- [Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/)
## The medium
@@ -617,6 +618,7 @@ The generated API reference, one page per module. Fetch these only when a specif
- [materials/suspended-ceilings](https://jmrplens.github.io/phonometry/reference/api/materials/suspended-ceilings/)
- [materials/uncertainty](https://jmrplens.github.io/phonometry/reference/api/materials/uncertainty/)
- [metrology/calibration](https://jmrplens.github.io/phonometry/reference/api/metrology/calibration/)
+- [metrology/comparison-calibration](https://jmrplens.github.io/phonometry/reference/api/metrology/comparison-calibration/)
- [metrology/conformance](https://jmrplens.github.io/phonometry/reference/api/metrology/conformance/)
- [metrology/data-qualification](https://jmrplens.github.io/phonometry/reference/api/metrology/data-qualification/)
- [metrology/free-field-corrections](https://jmrplens.github.io/phonometry/reference/api/metrology/free-field-corrections/)
@@ -51989,6 +51991,23 @@ documents, one page per module.
| `ELECTRICAL_TEST_FREQUENCIES_HZ` | `mapping` | **IEC 61672-3:2013 13.4 by class**: the nine octaves from 63 Hz to 16 kHz for class 1, the eight to 8 kHz for class 2 | `metrology.ELECTRICAL_TEST_FREQUENCIES_HZ[2][-1] # 8000.0` |
| `TONEBURST_TEST_DURATIONS_MS` | `mapping` | **IEC 61672-3:2013 18.5 to 18.7**: 200 ms, 2 ms and 0.25 ms for `'F'` and `'E'`, 200 ms and 2 ms for `'S'` | `metrology.TONEBURST_TEST_DURATIONS_MS['S'] # (200.0, 2.0)` |
| `PERIODIC_TEST_ENVIRONMENT` | `mapping` | **IEC 61672-3:2013 7.1**: 80 kPa to 105 kPa, 20 °C to 26 °C, 25 % to 70 %, keyed by the record field of each | `metrology.PERIODIC_TEST_ENVIRONMENT['air_temperatures_c'] # (20.0, 26.0)` |
+| `simultaneous_comparison` | `function` | **Sensitivity level by comparison with both microphones in the field at once (IEC 61094-5 5.1.2 and Annex C; IEC 61094-8 5.3).** • `frequencies_hz` [Hz]; `reference_sensitivity_level_db` L_ref [dB re 1 V/Pa] • `channel_difference_db` L_C12, reference on channel 1, and `interchanged_channel_difference_db` L_C21 [dB], one row per determination; 20 lg R_V = -(L_C12 - L_C21)/2 (C.3); required for `field="pressure"` (Default: None) • keyword-only: `field` ('pressure' / 'free_field', Default: 'pressure'); `pressure_level_difference_db` 20 lg R_P [dB] (Default: 0); `corrections_db` by name [dB]; `reference_environment` (added) and `test_environment` (subtracted), `EnvironmentalSensitivityCorrection`; `reference_free_field_difference_db` [dB], free field only; `expanded_uncertainty_db` [dB] (all Default: None) | `c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)`
• `ComparisonCalibration` |
+| `sequential_comparison` | `function` | **Sensitivity level by comparison with the microphones in the field in turn (IEC 61094-5 5.1.3 and Annex B; IEC 61094-8 5.2 and A.2).** • `frequencies_hz` [Hz]; `reference_sensitivity_level_db` [dB re 1 V/Pa]; `reference_output_level_db` 20 lg V_ref, `test_output_level_db` 20 lg V_test [dB], one row per determination • keyword-only: `reference_monitor_level_db` 20 lg V_mon,1 and `test_monitor_level_db` 20 lg V_mon,2 [dB], both or neither (Default: None); 20 lg R_V = 20 lg(V_test/V_mon,2) - 20 lg(V_ref/V_mon,1) • `field`, `pressure_level_difference_db`, `corrections_db`, `reference_environment`, `test_environment`, `reference_free_field_difference_db`, `expanded_uncertainty_db` as in `simultaneous_comparison` | `metrology.sequential_comparison(f, l_ref, v_ref, v_test, field="free_field")`
• `ComparisonCalibration` |
+| `ComparisonCalibration` | `dataclass` | **The sensitivity level of a microphone calibrated by comparison, L_test = L_ref + 20 lg R_V - 20 lg R_P + Σ C_j (IEC 61094-5 D.2).** • `frequencies_hz` [Hz], `reference_sensitivity_level_db`, `output_level_differences_db` (determinations × frequencies), `pressure_level_difference_db` [dB], `corrections_db` (read-only mapping) [dB], `field`, `excitation`, `expanded_uncertainty_db` [dB] or None • `.sensitivity_level_db` [dB re 1 V/Pa], `.sensitivity_mv_per_pa` [mV/Pa], `.output_level_difference_db`, `.correction_db` [dB], `.determinations`, `.standard` • `.plot()`: L_test with its U band and the reference's level (its free-field level, L_ref plus the free-field difference, against a pressure-calibrated reference in a free field) | `c.sensitivity_level_db, c.standard # ..., 'IEC 61094-5:2016'` |
+| `environmental_sensitivity_correction` | `function` | **First-order correction of a sensitivity level between two sets of environmental conditions (IEC 61094-5 6.6, IEC 61094-8 7.6).** • `frequencies_hz` [Hz] • keyword-only: `static_pressure_kpa` [kPa], `temperature_c` [°C], `relative_humidity_percent` [%], where the level is wanted; `static_pressure_coefficient_db_per_kpa` [dB/kPa] and `temperature_coefficient_db_per_k` [dB/K], the microphone's own (IEC 61094-2 Annex D units); `humidity_coefficient_db_per_percent` [dB/%] (Default: 0); `reference_static_pressure_kpa`, `reference_temperature_c`, `reference_relative_humidity_percent` (Default: 101.325 kPa, 23.0 °C, 50 %, clause 4) | `metrology.environmental_sensitivity_correction(f, static_pressure_kpa=97.0, temperature_c=26.0, relative_humidity_percent=40.0, static_pressure_coefficient_db_per_kpa=-0.005, temperature_coefficient_db_per_k=0.003)`
• `EnvironmentalSensitivityCorrection` |
+| `EnvironmentalSensitivityCorrection` | `dataclass` | **C_env = δp (p_s - p_s,0) + δt (t - t_0) + δH (H - H_0), at each frequency.** • `frequencies_hz` [Hz], the conditions and the reference conditions, the three coefficient columns, read-only • `.static_pressure_term_db`, `.temperature_term_db`, `.humidity_term_db`, `.correction_db` [dB] • `.plot()`: the three terms and the total | `e.correction_db` |
+| `jig_diameter_correction` | `function` | **Corrections of a WS3 microphone against an LS2aP in the jig of Figure A.4 (IEC 61094-5 Table A.1).** • `frequencies_hz` [Hz], rows of the table, an exact base-ten frequency within 2 % reading as its row (Default: None, the 14 rows from 1 kHz to 20 kHz) • refuses a frequency the table does not print | `metrology.jig_diameter_correction([8000.0]).correction_db # [-0.235]`
• `JigDiameterCorrection` |
+| `JigDiameterCorrection` | `dataclass` | **The Table A.1 corrections at the frequencies asked for.** • `frequencies_hz` [Hz], `correction_db` [dB], read-only • `.expanded_uncertainty_db` [dB], a tenth of the correction (NOTE); `.standard_uncertainty_db` [dB], its half, read with the k = 2 of 7.9 and D.2 since the 10 % comes without one, the special-case component of Table D.1 • `.plot()`: the corrections and their band | `j.expanded_uncertainty_db` |
+| `comparison_uncertainty_budget` | `function` | **The uncertainty budget of a comparison calibration at one frequency (IEC 61094-5 Annex D; IEC 61094-8 8.8, Table 2).** • `standard_uncertainties_db`: each component a standard uncertainty [dB] or a `Quantity`, keyed as `IEC61094_5_TABLE_D1` (pressure) or `IEC61094_8_TABLE_2` (free field); a subset is allowed • keyword-only: `frequency_hz` [Hz]; `field` (Default: 'pressure'); `additional_components`: named `Quantity` objects (Default: none); `coverage_factor` (Default: 2) • on `combine_uncertainty`, sensitivities 1, U = k u_c | `b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000.0)`
• `b.expanded_uncertainty_db # 0.087 (D.3 prints 0,08, an erratum)` |
+| `ComparisonUncertaintyBudget` | `dataclass` | **One comparison budget at one frequency.** • `frequency_hz` [Hz], `field`, `names`, `components`, `standard_uncertainties_db` [dB], `uncertainty` (the `UncertaintyResult`, refused unless it is their root-sum-square), `coverage_factor` • `.combined_uncertainty_db`, `.expanded_uncertainty_db`, `.linear_combined_uncertainty_db` [dB], `.standard` • `.plot()`: one bar per component, with u_c, k and U | `b.combined_uncertainty_db # 0.0437` |
+| `IEC61094_5_TABLE_D1` | `mapping` | **IEC 61094-5:2016 Table D.1, the example budget at 2 kHz, keyed 'reference', 'capacitance', 'non_linearity', 'impedance', 'polarizing_voltage', 'repeatability', 'drift', 'rounding'.** • each value a `ComparisonUncertaintyRow` with the printed standard uncertainty and, for the seven rows that state one, the stated value and its divisor (√3 or 2); the repeatability states none | `metrology.IEC61094_5_TABLE_D1["drift"].standard_uncertainty_db # 0.017` |
+| `IEC61094_8_TABLE_2` | `mapping` | **IEC 61094-8:2012 Table 2, the 12 typical uncertainty components, keyed 'reference' to 'repeatability'.** • each value a `ComparisonUncertaintyRow` with its subclause references; the table prints no values | `metrology.IEC61094_8_TABLE_2["positioning"].subclauses # ('7.3', '8.4')` |
+| `ComparisonUncertaintyRow` | `dataclass` | **One row of Table D.1 or Table 2.** • `component` as printed, `subclauses`, `stated_db` [dB], `divisor`, `standard_uncertainty_db` [dB] (the last three None for Table 2; `stated_db` and `divisor` None for the repeatability of Table D.1) | `metrology.IEC61094_5_TABLE_D1["reference"].divisor # 2.0` |
+| `IEC61094_5_TABLE_A1` | `mapping` | **IEC 61094-5:2016 Table A.1, the WS3 corrections in the jig of Figure A.4, keyed by frequency [Hz].** • 1 kHz to 20 kHz, -0.004 dB to -1.443 dB | `metrology.IEC61094_5_TABLE_A1[20000.0] # -1.443` |
+| `IEC61094_8_TABLE_1` | `mapping` | **IEC 61094-8:2012 Table 1, the ways the reference microphone can be calibrated, keyed 'primary_free_field', 'primary_pressure', 'secondary_pressure', 'secondary_free_field', 'electrostatic_actuator'.** • each value a `ReferenceCalibrationRow` | `metrology.IEC61094_8_TABLE_1["primary_pressure"].expanded_uncertainty_10khz_db # 0.4` |
+| `ReferenceCalibrationRow` | `dataclass` | **One row of Table 1.** • `microphone_types`, `method`, `references`, `expanded_uncertainty_1khz_db`, `expanded_uncertainty_10khz_db` [dB, k = 2] | `metrology.IEC61094_8_TABLE_1["primary_free_field"].references # ('IEC 61094-3',)` |
+| `free_field_region` | `function` | **The effective free-field region of a time window, A = d + τc (IEC 61094-8 B.1).** • `source_distance_m` d [m], `window_time_s` τ [s] • keyword-only: `temperature_c` [°C], `static_pressure_kpa` [kPa], `relative_humidity_percent` [%] (Default: 23.0, 101.325, 50, clause 4); c from the IEC 61094-2 Annex F air | `metrology.free_field_region(1.0, 0.005).major_axis_m # 2.73`
• `FreeFieldRegion` |
+| `FreeFieldRegion` | `dataclass` | **The prolate spheroid with the source and the microphone at its foci.** • `source_distance_m` [m], `window_time_s` [s], `speed_of_sound` [m/s] • `.major_axis_m` A, `.semi_minor_axis_m` b, `.rod_clearance_m` τc/2 [m] • `.plot()`: the region as Figure B.1 draws it | `r.semi_minor_axis_m # 1.27` |
| `combine_uncertainty` | `function` | **GUM law of propagation of uncertainty (ISO/IEC Guide 98-3 clause 5).** • `model`: measurement function f(x1…xN) • `quantities`: input `Quantity` objects, in argument order • `correlation`: optional N×N matrix r_ij (Default: None → uncorrelated; non-identity with finite input dof → effective dof undefined, `expanded()` needs `coverage_factor_override`) | `u = metrology.combine_uncertainty(lambda a, b: a*b, [metrology.Quantity(10.0, 0.1), metrology.Quantity(5.0, 0.2)])`
• `MonteCarloResult` (mean, u, coverage interval) |
| `rectangular` | `function` | **Type B quantity, rectangular PDF (GUM 4.3.7).** • `value`, `half_width` a, `name` | `rectangular(20.0, 0.5)`
• `Quantity` with u = a/√3 = 0.289 |
@@ -52430,7 +52449,7 @@ documents, one page per module.
| `TonalityWarning` | `warning class` | **Tonality advisory.** Emitted for biased tonality estimates (e.g. coarse FFT resolution) | `warnings.simplefilter('error', TonalityWarning)` |
| `STIWarning` | `warning class` | **STI/STIPA advisory.** Emitted for suspect speech-intelligibility measurements or inputs | `warnings.simplefilter('error', STIWarning)` |
| `__version__` | `str` | **Package version string.** (no parameters) | `phonometry.__version__ # '4.0.0rc1'` |
-| `.plot()` | `method` | **One-line canonical figure on every result object (soft matplotlib dependency).** Available on `ZwickerLoudness`, `MooreGlasbergLoudness`, `MooreGlasbergTimeVaryingLoudness`, `EcmaLoudness`, `EcmaTonality`, `EcmaRoughness`, `PsychoacousticAnnoyanceResult`, `FluctuationStrengthResult`, `ProgramLoudnessResult`, `KWeightingResponse`, `STIResult`, `SIIResult`, `SIIProcedure`, `StandardSpeechSpectrum`, `NCResult`, `RCResult`, `AgeThresholdResult`, `NiptsResult`, `HtlanResult`, `ImpulseProminenceResult`, `ImpulsiveSoundResult`, `SelDistribution`, `LevelDifferenceQuantiles`, `RoundRobinPrecision`, `CalculationVerification`, `DirectivityFactor`, `RandomIncidenceSensitivity`, `DiffuseFieldSensitivity`, `AdjustmentValue`, `FreeFieldCorrection`, `CorrectionUncertaintyBudget`, `CorrectionUncertaintyVerification`, `SoundLevelMeterPeriodicRequirement`, `SoundLevelMeterPeriodicVerification`, `FieldStrengthReading`, `BackgroundNoiseAssessment`, `InductionLoopVerification`, `LoopRequirement`, `AmplifierOverloadVerification`, `LoopField`, `LoopImpedance`, `AmplifierFrequencyResponse`, `AgcCharacteristic`, `QuadraturePhaseError`, `MaximumOutputCurrent`, `NeckLoopCharacteristics`, `NeckLoopVerification`, `MultipleShockResult`, `ImpulseResponseResult`, `DecayCurve`, `RoomAcousticsResult`, `ReverberationResult`, `ReverberationModelResult`, `DynamicStiffnessResult`, `MobilityResult`, `TransferStiffnessResult`, `BandAveragedStiffness`, `LevelDifferenceCheck`, `OutputMassCheck`, `EffectiveBlockingMass`, `DrivingPointStiffnessResult`, `DrivingPointUncertainty`, `VibrationSoundPowerResult`, `StructureBornePowerResult`, `InstalledSourceResult`, `WeightedRatingResult`, `ImpactRatingResult`, `ImpactImprovementRatingResult`, `FacadeInsulationResult`, `LabAirborneInsulationResult`, `LabImpactInsulationResult`, `SoundPowerResult`, `SoundEnergyResult`, `ReverberationSoundPowerResult`, `ReverberationSoundEnergyResult`, `InSituSoundPowerResult`, `InDuctSoundPowerResult`, `HighFrequencySoundPowerResult`, `InverseSquareLawResult`, `FreeFieldCheck`, `SourceDirectionalityResult`, `ReferenceSourceCalibration`, `ReferenceSoundSourceVerdict`, `ReferenceSourceDriftResult`, `TurbineMeasurementSurface`, `TurbineMicrophoneArray`, `TurbineEnvironmentalCorrection`, `TurbineTestEnvironmentCheck`, `TurbineSoundPowerResult`, `TurbineNoiseDeclaration`, `SoundPowerIntensityResult`, `DiscretePointIntensityResult`, `PrecisionSoundPowerResult`, `PrecisionIntensityResult`, `IntensityResult`, `UncertaintyResult`, `AbsorptionRatingResult`, `ScatteringResult`, `DiffusionResult`, `DiffusionSpectrum`, `InsituAbsorptionResult`, `WeightingResponse`, `WeightedSpectrum`, `Signal` and `DailyVibrationExposure`. • `ax`: existing Axes, or None to build a fresh figure (Default: None) • returns the Matplotlib `Axes` (an array of Axes for multi-panel figures); never calls `plt.show()` • needs matplotlib (`pip install phonometry[plot]`) | `res.plot()` `decay_curve(ir, fs).plot()` |
+| `.plot()` | `method` | **One-line canonical figure on every result object (soft matplotlib dependency).** Available on `ZwickerLoudness`, `MooreGlasbergLoudness`, `MooreGlasbergTimeVaryingLoudness`, `EcmaLoudness`, `EcmaTonality`, `EcmaRoughness`, `PsychoacousticAnnoyanceResult`, `FluctuationStrengthResult`, `ProgramLoudnessResult`, `KWeightingResponse`, `STIResult`, `SIIResult`, `SIIProcedure`, `StandardSpeechSpectrum`, `NCResult`, `RCResult`, `AgeThresholdResult`, `NiptsResult`, `HtlanResult`, `ImpulseProminenceResult`, `ImpulsiveSoundResult`, `SelDistribution`, `LevelDifferenceQuantiles`, `RoundRobinPrecision`, `CalculationVerification`, `DirectivityFactor`, `RandomIncidenceSensitivity`, `DiffuseFieldSensitivity`, `AdjustmentValue`, `FreeFieldCorrection`, `CorrectionUncertaintyBudget`, `CorrectionUncertaintyVerification`, `ComparisonCalibration`, `EnvironmentalSensitivityCorrection`, `JigDiameterCorrection`, `ComparisonUncertaintyBudget`, `FreeFieldRegion`, `SoundLevelMeterPeriodicRequirement`, `SoundLevelMeterPeriodicVerification`, `FieldStrengthReading`, `BackgroundNoiseAssessment`, `InductionLoopVerification`, `LoopRequirement`, `AmplifierOverloadVerification`, `LoopField`, `LoopImpedance`, `AmplifierFrequencyResponse`, `AgcCharacteristic`, `QuadraturePhaseError`, `MaximumOutputCurrent`, `NeckLoopCharacteristics`, `NeckLoopVerification`, `MultipleShockResult`, `ImpulseResponseResult`, `DecayCurve`, `RoomAcousticsResult`, `ReverberationResult`, `ReverberationModelResult`, `DynamicStiffnessResult`, `MobilityResult`, `TransferStiffnessResult`, `BandAveragedStiffness`, `LevelDifferenceCheck`, `OutputMassCheck`, `EffectiveBlockingMass`, `DrivingPointStiffnessResult`, `DrivingPointUncertainty`, `VibrationSoundPowerResult`, `StructureBornePowerResult`, `InstalledSourceResult`, `WeightedRatingResult`, `ImpactRatingResult`, `ImpactImprovementRatingResult`, `FacadeInsulationResult`, `LabAirborneInsulationResult`, `LabImpactInsulationResult`, `SoundPowerResult`, `SoundEnergyResult`, `ReverberationSoundPowerResult`, `ReverberationSoundEnergyResult`, `InSituSoundPowerResult`, `InDuctSoundPowerResult`, `HighFrequencySoundPowerResult`, `InverseSquareLawResult`, `FreeFieldCheck`, `SourceDirectionalityResult`, `ReferenceSourceCalibration`, `ReferenceSoundSourceVerdict`, `ReferenceSourceDriftResult`, `TurbineMeasurementSurface`, `TurbineMicrophoneArray`, `TurbineEnvironmentalCorrection`, `TurbineTestEnvironmentCheck`, `TurbineSoundPowerResult`, `TurbineNoiseDeclaration`, `SoundPowerIntensityResult`, `DiscretePointIntensityResult`, `PrecisionSoundPowerResult`, `PrecisionIntensityResult`, `IntensityResult`, `UncertaintyResult`, `AbsorptionRatingResult`, `ScatteringResult`, `DiffusionResult`, `DiffusionSpectrum`, `InsituAbsorptionResult`, `WeightingResponse`, `WeightedSpectrum`, `Signal` and `DailyVibrationExposure`. • `ax`: existing Axes, or None to build a fresh figure (Default: None) • returns the Matplotlib `Axes` (an array of Axes for multi-panel figures); never calls `plt.show()` • needs matplotlib (`pip install phonometry[plot]`) | `res.plot()` `decay_curve(ir, fs).plot()` |
## Notes
@@ -59083,6 +59102,10 @@ What the numbers mean and how much to trust them.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's periodic-test results for a working meter,
clause by clause, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/):
+ a working standard microphone calibrated against a reference in a pressure
+ field or a free field, with the corrections and the uncertainty budget each
+ part asks for.
## What this section does not cover
@@ -61288,6 +61311,293 @@ A.1 to A.5, 5.9.4.2, A.6.4.7) by the conformance rule of 5.1.15.
---
+
+Source: https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/
+
+# Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside a reference microphone whose sensitivity is already known, or in its
+place, both are exposed to the same sound pressure, and the ratio of their
+output voltages carries the reference's sensitivity over to it. IEC
+61094-5:2016 does this in a **pressure field**, in a coupler or a jig; IEC
+61094-8:2012 does it in a **free field**, in an anechoic room or behind a
+time window. Annex D of the first part writes the model once for both,
+`M_test = M_ref · R_V / R_P`, and `metrology.comparison_calibration` computes
+it in sensitivity levels, `L_test = L_ref + 20 lg R_V - 20 lg R_P + sum(C_j)`,
+with `R_V` the ratio of the output voltages, `R_P` that of the effective sound
+pressures (reduced to unity by the procedure) and `C_j` the corrections each
+part asks for. This page runs a pressure calibration of a WS2P against an
+LS2P and a free-field calibration of a WS2F on synthetic readings, so every
+step can be checked against the sensitivities that built them.
+
+
+
+## How the measurement goes
+
+With **simultaneous excitation** both microphones sit in the field at once,
+each on its own measuring channel; in a coupler or a jig they face each other
+about 1 mm apart (IEC 61094-5 5.1.2). With **sequential excitation** they take
+the same place in turn: either the exchange does not change the sound pressure
+significantly, or any change is detected and corrected, for example with a
+monitor microphone near the source (IEC 61094-5 5.1.3, IEC 61094-8 5.2, 6.5
+and A.2).
+
+
+
+| Requirement | Value | Clause |
+| :--- | :--- | :--- |
+| Reference conditions | 23,0 °C, 101,325 kPa, 50 % relative humidity | 61094-5 and 61094-8, 4 |
+| Simultaneous, in a coupler or a jig | Diaphragms about 1 mm apart; the microphones interchanged and the measurement repeated | 61094-5, 5.1.2 |
+| Sequential | The exchange does not change the sound pressure significantly, or any change is detected and corrected, for example with a monitor microphone; a monitor, when used, senses the changes at the test position | 61094-5, 5.1.3; 61094-8, 5.2, 6.5 |
+| Free-field mounting | On a semi-infinite rod of the microphone's diameter | 61094-8, 6.7 |
+| Reported uncertainty | Expanded, with `k = 2`, at each frequency | 61094-5, 7.9; 61094-8, 8.8 |
+
+## A pressure calibration by simultaneous excitation
+
+Annex C removes the gains of the two channels and the asymmetry of the
+coupler: with the reference on channel 1 the level reading difference is
+`L_C12`, after the interchange it is `L_C21`, and Formula (C.3) gives
+`L_ref - L_test = (L_C12 - L_C21) / 2`. Here the channels differ by 0,55 dB,
+port A hears up to 0,13 dB more than port B, and each reading is repeated
+three times:
+
+```python
+import numpy as np
+from phonometry import metrology
+
+f = metrology.exact_frequencies(250, 20000, fraction=3) # 20 exact one-third octaves
+x = f / 1000
+l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2 # the LS2P's certificate, dB re 1 V/Pa
+l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4 # the WS2P: unknown
+
+gain_1, gain_2 = 0.35, -0.20 # the two measuring channels, dB
+field_a = 0.03 * np.sqrt(x) # port A of the coupler hears 0.015 dB to 0.13 dB more
+rng = np.random.default_rng(61094)
+noise = rng.normal(0.0, 0.004, (2, 3, f.size)) # three repeats of each reading
+l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0] # reference on channel 1
+l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1] # interchanged
+
+c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)
+print(c.determinations, c.sensitivity_level_db[[2, 8, 19]].round(2)) # 3 [-38.63 -38.58 -38.44]
+print(np.abs(c.sensitivity_level_db - l_true).max().round(3)) # 0.003
+```
+
+A pressure calibration without the interchange is refused, since 5.1.2
+requires it. The "mean of the two ratios" 5.1.2 asks for is taken in
+decibels, as Annex C does: the geometric mean, the only one in which the gains
+cancel exactly (the arithmetic mean of the two ratios here would sit 0,018 dB
+to 0,027 dB high).
+
+IEC 61094-5 6.6 corrects the reference's sensitivity to the conditions of the
+test when the two microphones are different models, and IEC 61094-8 7.6 does
+it whatever the models. `metrology.environmental_sensitivity_correction`
+corrects a sensitivity level for the static pressure, the temperature and the
+humidity to first order, from the microphone's own coefficients in the units
+IEC 61094-2 Annex D gives them:
+
+```python
+env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2, # the laboratory during the test
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005, # the LS2P's own coefficients
+ temperature_coefficient_db_per_k=0.002,
+) # from its certificate's 101.325 kPa, 23.0 °C and 50 %
+print(env.correction_db[0].round(4)) # 0.0076
+```
+
+## The uncertainty budget of Annex D
+
+Table D.1 is a worked budget for a set-up like this one at 2 kHz, in the
+coupler of Figure A.1, which is for frequencies up to 10 kHz (A.1; above, the
+jig of A.2 reaches 20 kHz). `metrology.IEC61094_5_TABLE_D1` holds each of its
+eight rows with its printed standard uncertainty, and the stated value and
+divisor of the seven that state one; the repeatability row states no value
+and prints 0,025 dB as a standard uncertainty.
+`metrology.comparison_uncertainty_budget` combines the components on
+`metrology.combine_uncertainty` and multiplies by `k = 2`:
+
+```python
+d1 = {
+ "reference": 0.025, "capacitance": 0.006, "non_linearity": 0.017,
+ "impedance": 0.003, "polarizing_voltage": 0.005, "repeatability": 0.025,
+ "drift": 0.017, "rounding": 0.003,
+} # Table D.1, standard uncertainties at 2 kHz
+b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000)
+print(round(b.combined_uncertainty_db, 4), round(b.expanded_uncertainty_db, 3)) # 0.0437 0.087
+print(round(b.linear_combined_uncertainty_db, 4)) # 0.0436
+```
+
+D.3 prints 0,040 dB and 0,08 dB, which are not the root-sum-square of its own
+eight components (an [erratum](https://jmrplens.github.io/phonometry/reference/errata/)). The strict calculation in
+linear form D.3 mentions, and does not print, gives 0,043 614 dB; the Spanish
+UNE-EN 61094-5:2017 leaves six of the eight values of Table D.1 blank and prints
+0,004 dB in D.3. With one budget per frequency, the expanded uncertainty goes
+to the calibration, which draws it as a band:
+
+```python
+budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**d1, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2}, frequency_hz=fx
+ )
+ for fx in f
+]
+u = [bb.expanded_uncertainty_db for bb in budgets]
+c = metrology.simultaneous_comparison(
+ f, l_ref, l_c12, l_c21, reference_environment=env, expanded_uncertainty_db=u
+)
+print(np.round(u, 3)[[0, 16, 19]]) # [0.087 0.101 0.205]
+print(c.sensitivity_mv_per_pa[[2, 8, 19]].round(2)) # [11.72 11.78 11.98]
+```
+
+
+
+## A WS3 microphone in the jig
+
+Table A.1 gives the corrections to add to the sensitivity level of a WS3
+microphone calibrated against an LS2aP in the jig of Figure A.4, with an
+expanded uncertainty of a tenth of each:
+
+```python
+j = metrology.jig_diameter_correction()
+print(j.correction_db[[0, 9, 13]]) # [-0.004 -0.235 -1.443]
+print(j.standard_uncertainty_db[13].round(4)) # 0.0722
+```
+
+
+
+## A free-field calibration by substitution
+
+By IEC 61094-8 A.2 each microphone is read against the monitor, and the
+quotient of the two ratios is the output ratio corrected for any change of
+the source: `20 lg R_V = 20 lg(V_test / V_mon,2) - 20 lg(V_ref / V_mon,1)`,
+with `V_ref` and `V_mon,1` the output voltages of the reference and the
+monitor in the first reading and `V_test` and `V_mon,2` those of the second.
+The reference here is an LS2P calibrated in a pressure field, so it takes
+the free-field to pressure difference of IEC/TS 61094-7 (illustrative values
+below), and the source drifts by 0,25 dB between the readings:
+
+```python
+ff = metrology.exact_frequencies(500, 20000, fraction=3)
+xf = ff / 1000
+c_ff = 0.05 * xf**1.3 # the LS2P's free-field to pressure difference: illustrative
+l_ref_p = -38.0 + 0.04 * np.log10(xf) - 0.25 * (xf / 20) ** 2 # its pressure calibration
+l_ws2f = -38.3 + 0.1 * np.log10(xf) - 0.3 * (xf / 20) ** 3 # the WS2F: unknown
+field_1 = 74.0 + 0.05 * np.sin(xf) # the free field while the reference is in place
+field_2 = field_1 + 0.25 # the source has drifted by the time the WS2F is
+monitor = -40.0 # the monitor microphone near the source, dB re 1 V/Pa
+
+
+def free_field_budget(fx):
+ x20 = fx / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x20, # pressure calibration and IEC/TS 61094-7
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x20**2,
+ "free_field": 0.03 + 0.12 * x20**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=fx,
+ field="free_field",
+ )
+
+
+free = metrology.sequential_comparison(
+ ff,
+ l_ref_p,
+ l_ref_p + c_ff + field_1, # the reference's output, dB re 1 V
+ l_ws2f + field_2, # the WS2F's output in its place
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[free_field_budget(fx).expanded_uncertainty_db for fx in ff],
+)
+print(bool(np.abs(free.sensitivity_level_db - l_ws2f).max() < 1e-9)) # True
+print(free.expanded_uncertainty_db[[0, 12, 16]].round(2)) # [0.16 0.26 0.51]
+```
+
+Table 2 of IEC 61094-8 lists the components and prints no values; the ones
+above are illustrative. `metrology.IEC61094_8_TABLE_1` holds the typical
+expanded uncertainty of each way of calibrating the reference:
+
+```python
+row = metrology.IEC61094_8_TABLE_1["primary_pressure"]
+print(row.expanded_uncertainty_1khz_db, row.expanded_uncertainty_10khz_db) # 0.12 0.4
+```
+
+## The effective free-field region of a time window
+
+A time window of length `tau` simulates a free field inside a prolate
+spheroid with the source and the microphone at its foci, `d` apart, and the
+major diameter `A = d + tau c` (Formula (B.1)), at the speed of sound of the
+IEC 61094-2 Annex F air:
+
+```python
+r = metrology.free_field_region(1.0, 0.005) # 1 m, a 5 ms window, at 23.0 °C
+print(round(r.speed_of_sound, 1), round(r.major_axis_m, 2)) # 345.9 2.73
+print(round(r.semi_minor_axis_m, 2), round(r.rod_clearance_m, 2)) # 1.27 0.86
+```
+
+
+
+## The diffuse-field comparison of IEC 61183
+
+The diffuse-field method of IEC 61183 clause 5 is a sequential comparison
+without a monitor, and the library computes it through the same model:
+
+```python
+d = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0], [94.3, 94.6], [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+)
+s = metrology.sequential_comparison([1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6])
+print(d.diffuse_field_level_db, s.sensitivity_level_db) # [-25.7 -25.7] [-25.7 -25.7]
+```
+
+## What this guide covers
+
+Implemented: the level model of IEC 61094-5 D.2; the simultaneous excitation
+with the interchange of Annex C (Formulas (C.1) to (C.3)), required for a
+pressure calibration; the sequential excitation against a monitor microphone;
+the average over the determinations; the environmental correction to first
+order (this library's reading of 6.6 and 7.6, which print no formula); the
+reference's free-field to pressure difference in a free-field calibration;
+the WS3 corrections of Table A.1, whose 10 % expanded uncertainty, printed
+without a coverage factor, is read with the `k = 2` of 7.9 and D.2; the
+budgets of Table D.1 and IEC 61094-8 Table 2 with `k = 2`; Tables A.1, D.1,
+1 and 2 as published data; and the effective free-field region of Formula
+(B.1). Not implemented: the
+measurements themselves and the phase of the sensitivity; the corrections for
+the difference of the acoustic impedances (7.4, 7.5) and for non-uniform
+pressure beyond Table A.1 (6.5); the time-selective processing of IEC 61094-8
+Annex B and the qualification of the free field by ISO 26101; the values of
+IEC/TS 61094-7 and the reciprocity calibration of the reference, which are
+inputs; the second special case of Table D.1, a microphone calibrated as a
+system with its preamplifier (0,002 dB, and under 0,02 dB above 200 Hz),
+which a budget takes through `additional_components`.
+
+## See also
+
+- [Free-field corrections of a sound level meter (IEC 62585)](https://jmrplens.github.io/phonometry/signals/metrology/free-field-corrections/):
+ a meter compared with an LS2P reference in the same way.
+- [Random-incidence and diffuse-field response (IEC 61183)](https://jmrplens.github.io/phonometry/signals/metrology/random-incidence/):
+ the diffuse-field comparison that computes through the same model.
+- [Measurement uncertainty](https://jmrplens.github.io/phonometry/signals/metrology/gum-uncertainty/): the GUM law of propagation
+ the budgets are combined by.
+- [Errata in published sources](https://jmrplens.github.io/phonometry/reference/errata/): IEC 61094-5 D.3, UNE-EN
+ 61094-5:2017 Table D.1, IEC 61094-8 8.4 and Formula (B.10).
+- API reference: [`metrology.comparison_calibration`](https://jmrplens.github.io/phonometry/reference/api/metrology/comparison-calibration/).
+
+---
+
+
Source: https://jmrplens.github.io/phonometry/signals/metrology/compliance-verification/
@@ -62603,6 +62913,13 @@ clause from the frequency weightings to the high-level stability against the
acceptance limits of IEC 61672-1 and the maxima of its Table B.1, what a
complete test holds, and the statement Clause 22 prescribes for the outcome.
+[Calibration by comparison](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/)
+goes one step up the chain, to the microphone itself: a working standard
+microphone takes the sensitivity of a reference through the ratio of their
+output voltages, in a coupler by IEC 61094-5 or in a free field by IEC
+61094-8, with the environmental, jig and free-field corrections each part asks
+for and the uncertainty budget of Table D.1 or Table 2.
+
The same discipline extends into the frequency domain: the
[Signals and spectra](https://jmrplens.github.io/phonometry/signals/spectra/) pages
apply the Bendat & Piersol error analysis to Welch spectral estimates, so
@@ -62641,6 +62958,11 @@ budgets that are specialisations of the GUM machinery described here.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's results, clause by clause, with Tables 4, 5
and B.1 of IEC 61672-1, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/):
+ the simultaneous comparison with the interchange of Annex C, the sequential
+ one against a monitor, the environmental and WS3 jig corrections, the
+ budgets of Table D.1 and Table 2 with k = 2, and the free-field region of a
+ time window.
## What this section does not cover
@@ -68762,7 +69084,7 @@ the normative PDF fiche.
- [Filter Architecture Gallery](https://jmrplens.github.io/phonometry/signals/filters/filter-gallery/): choose an architecture and inspect responses
- [Calibration and dBFS](https://jmrplens.github.io/phonometry/signals/metrology/calibration/): get real-world SPL values
- [Why phonometry](https://jmrplens.github.io/phonometry/start/why-phonometry/): the conformance-first design philosophy
-- [Conformance report](https://jmrplens.github.io/phonometry/reference/conformance/): the expected and computed value of all 1774 checks
+- [Conformance report](https://jmrplens.github.io/phonometry/reference/conformance/): the expected and computed value of all 1786 checks
- [API Reference](https://jmrplens.github.io/phonometry/reference/api/): every parameter of every function
- [Bibliography](https://jmrplens.github.io/phonometry/reference/bibliography/): the books and papers behind every guide, each with a verified link
@@ -69770,7 +70092,7 @@ sample from the metrology core:
| IEC 61252:1993 | The personal sound exposure quantities, `sound_exposure()` and the normalized 8 h level `lex_8h()`; IEC 61252:2025 has since superseded this print, and the transcription still targets the 1993+A2 text | `tests/signals/test_levels.py` |
The same discipline applies far beyond the metrology core: today the suite runs
-1774 numerical conformance checks across 108 domains and 516 standards, covering
+1786 numerical conformance checks across 109 domains and 518 standards, covering
psychoacoustics and speech intelligibility, room, building and materials
acoustics, human and machine vibration, environmental, aircraft, rotorcraft
and underwater noise, electroacoustics, broadcast loudness, industrial noise
diff --git a/llms.txt b/llms.txt
index 3cab04c17..2b012735a 100644
--- a/llms.txt
+++ b/llms.txt
@@ -1,8 +1,8 @@
# phonometry
-> Acoustic measurement, analysis and prediction in Python. Sound level metrology, psychoacoustics, room and building acoustics, materials, vibration, environmental and transport noise, underwater acoustics, electroacoustics and wave simulation, with every metric implemented from the text of its governing standard and 1774 numerical conformance checks against 516 standards enforced in CI.
+> Acoustic measurement, analysis and prediction in Python. Sound level metrology, psychoacoustics, room and building acoustics, materials, vibration, environmental and transport noise, underwater acoustics, electroacoustics and wave simulation, with every metric implemented from the text of its governing standard and 1786 numerical conformance checks against 518 standards enforced in CI.
-phonometry v4.0.0rc1 is a pure-Python library built on NumPy/SciPy (Python >= 3.13). Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line `.plot()` in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with `.report()`. The conformance report pins each of the 1774 checks to a standard, a clause or table, the normative expected value and the value the library computes, across 108 domains.
+phonometry v4.0.0rc1 is a pure-Python library built on NumPy/SciPy (Python >= 3.13). Each result is a typed, frozen dataclass that carries the inputs it was computed from, draws its own figure with a one-line `.plot()` in English or Spanish, and, where a standard defines a reporting format, renders that format as a PDF with `.report()`. The conformance report pins each of the 1786 checks to a standard, a clause or table, the normative expected value and the value the library computes, across 109 domains.
The project was published as **PyOctaveBand** until version 3.0.0. Third-party references and dependency pins still using that name refer to this library.
@@ -90,6 +90,7 @@ If you are an AI assistant setting this up for a user: install from PyPI (no sys
- [Random-incidence and diffuse-field response (IEC 61183)](https://jmrplens.github.io/phonometry/signals/metrology/random-incidence/)
- [Free-field corrections of a sound level meter (IEC 62585)](https://jmrplens.github.io/phonometry/signals/metrology/free-field-corrections/)
- [Periodic tests of a sound level meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/)
+- [Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/)
## The medium
@@ -617,6 +618,7 @@ The generated API reference, one page per module. Fetch these only when a specif
- [materials/suspended-ceilings](https://jmrplens.github.io/phonometry/reference/api/materials/suspended-ceilings/)
- [materials/uncertainty](https://jmrplens.github.io/phonometry/reference/api/materials/uncertainty/)
- [metrology/calibration](https://jmrplens.github.io/phonometry/reference/api/metrology/calibration/)
+- [metrology/comparison-calibration](https://jmrplens.github.io/phonometry/reference/api/metrology/comparison-calibration/)
- [metrology/conformance](https://jmrplens.github.io/phonometry/reference/api/metrology/conformance/)
- [metrology/data-qualification](https://jmrplens.github.io/phonometry/reference/api/metrology/data-qualification/)
- [metrology/free-field-corrections](https://jmrplens.github.io/phonometry/reference/api/metrology/free-field-corrections/)
diff --git a/scripts/api_taxonomy.py b/scripts/api_taxonomy.py
index 2d7f6f6cd..e7c4db487 100644
--- a/scripts/api_taxonomy.py
+++ b/scripts/api_taxonomy.py
@@ -100,6 +100,7 @@ class Section:
"phonometry.metrology.random_incidence",
"phonometry.metrology.free_field_corrections",
"phonometry.metrology.sound_level_meter",
+ "phonometry.metrology.comparison_calibration",
),
),
Section(
@@ -708,6 +709,12 @@ class Section:
"PERIODIC_TEST_ENVIRONMENT": "phonometry.metrology.sound_level_meter",
"SLM_PERIODIC_REQUIREMENTS": "phonometry.metrology.sound_level_meter",
"TONEBURST_TEST_DURATIONS_MS": "phonometry.metrology.sound_level_meter",
+ # The IEC 61094-5 and IEC 61094-8 tables of the comparison calibration are
+ # read-only mappings too, owned by the module whose functions read them.
+ "IEC61094_5_TABLE_A1": "phonometry.metrology.comparison_calibration",
+ "IEC61094_5_TABLE_D1": "phonometry.metrology.comparison_calibration",
+ "IEC61094_8_TABLE_1": "phonometry.metrology.comparison_calibration",
+ "IEC61094_8_TABLE_2": "phonometry.metrology.comparison_calibration",
# Defined in phonometry._internal.warnings, exported at the top level.
"PhonometryWarning": "phonometry",
# The io subpackage keeps its implementation modules private and
diff --git a/scripts/check_figure_language.py b/scripts/check_figure_language.py
index 6d2ea05d4..3ecc2dcd6 100755
--- a/scripts/check_figure_language.py
+++ b/scripts/check_figure_language.py
@@ -188,6 +188,9 @@
"follows its source (lab_lining_improvement)",
r"$L_{I,\mathrm{m,ref}}$": "the symbol of ISO 10140-5:2021 Formula (I.2); the m,ref subscript is "
"the one the standard prints, and a subscript follows its source (rainfall_sound)",
+ r"$L_\mathrm{test} \pm U$ ($k$ = 2)": "the level of the test microphone, whose test subscript "
+ "is the one IEC 61094-5 D.2 prints in M_test, with its expanded uncertainty; a subscript "
+ "follows its source (ComparisonCalibration.plot)",
}
# A run of at least three letters, in any alphabet: shorter runs are unit and
diff --git a/scripts/check_markdown_hazards.py b/scripts/check_markdown_hazards.py
index bcce6245b..e90edcafb 100644
--- a/scripts/check_markdown_hazards.py
+++ b/scripts/check_markdown_hazards.py
@@ -7,9 +7,9 @@
quote or an ordered list, and it ends the paragraph there. The author sees a
paragraph; the reader gets two blocks, or worse.
-Four checks. The first two are the same defect seen twice, and they are not
-equally visible; the third and fourth are different ones with the same shape,
-a source that looks right and renders wrong.
+Five checks. The first two are the same defect seen twice, and they are not
+equally visible; the other three are different ones with the same shape, a
+source that looks right and renders wrong.
1. **Unclosed inline maths.** An inline ``$...$`` that wraps onto a block
marker never closes: the marker ends the paragraph first. The maths is then
@@ -55,6 +55,31 @@
Only that opening form is a hazard: a ``$$`` alone on its line is exactly what
the convention wants, opening and closing.
+5. **A less-than sign glued to what follows it, in MDX.** MDX reads ``<`` as
+ the start of a JSX tag, and a tag name has to start with a letter. So a
+ quoted "<0,02 above 200 Hz" in the prose of an ``.mdx`` page does not
+ reach the reader as text: the build fails with ``Unexpected character `0`
+ before name``, and only the site build sees it. A ``<`` followed by a
+ space, one in inline code or in maths, and an escaped ``\<`` are all
+ fine. So is one inside a quoted attribute value of a JSX tag, on however
+ many lines the tag takes: an ``alt`` text such as "a band of <0.1 dB" is
+ a string, and MDX never reads it as prose. One between the attributes of
+ a tag is not, and MDX fails there too. A plain ``.md`` page is not read
+ as MDX, so only the ``<`` of MDX prose followed by something no tag name
+ can start with is reported.
+
+ The page is read from left to right, as MDX reads it, so whichever of
+ these starts first wins: a ``<`` inside a code span starts no tag, and a
+ backtick inside an attribute value opens no code span. A code span,
+ inline maths and a tag in the middle of a paragraph run across line
+ breaks until the paragraph ends, at a blank line or at a line that opens
+ a block, as they do in MDX. A tag that has its lines to itself is a
+ block of its own, and MDX lets it run across blank lines as well.
+
+The YAML frontmatter at the top of a page is not markdown: it is read as
+data, and Astro blanks it before MDX compiles the body. No rule reads it, so
+a ``description`` may say "<0,02 dB".
+
Usage::
python scripts/check_markdown_hazards.py
@@ -69,6 +94,8 @@
import re
import sys
+from markdown_fences import code_lines
+
_ROOT = pathlib.Path(__file__).resolve().parent.parent
#: Trees of hand-written markdown. The API reference is generated from the
@@ -79,8 +106,11 @@
)
_SKIP = ("reference/api/", "superpowers/")
+#: The markers that open a new CommonMark block at the start of a line.
+_BLOCK_MARKER = r"[-*+]\s|>|#{1,6}\s|\d{1,9}[.)]\s|\||```|~~~"
+
#: A line that opens a new CommonMark block rather than continuing a paragraph.
-_BLOCK = re.compile(r"^\s{0,3}(?:[-*+]\s|>|#{1,6}\s|\d{1,9}[.)]\s|\||```)")
+_BLOCK = re.compile(rf"^\s{{0,3}}(?:{_BLOCK_MARKER})")
#: ``>`` opening a line with content after it: a wrapped comparison operator,
#: unless the paragraph deliberately starts a quotation, which is always
@@ -98,6 +128,58 @@
#: is the fence's meta, so the block does not end where the author thinks.
_MATH_META = re.compile(r"^\$\$\s*\S")
+#: The line that opens and closes the YAML frontmatter, and nothing else.
+_FRONTMATTER = "---"
+
+#: In MDX prose, a ``<`` followed by a character no JSX tag can start with:
+#: MDX still reads it as a tag and the build fails. An escaped ``\<`` never
+#: gets here, since the escapes are blanked with the rest of what is not prose.
+_MDX_LESS_THAN = re.compile(r"<(?=[^A-Za-z/!_$>\s])")
+
+#: One character of a paragraph: anything but a line break, or a line break
+#: that neither leaves a blank line nor starts a line that opens a block.
+_IN_PARAGRAPH = rf"(?:[^\n]|\n(?![ \t]*\n)(?![ \t]{{0,3}}(?:{_BLOCK_MARKER})))"
+
+
+def _span(name: str, delimiter: str) -> str:
+ """Inline code or inline maths, as CommonMark and remark-math read them.
+
+ A run of ``delimiter`` closed by a run of the same length, within one
+ paragraph. Nothing inside is escaped.
+ """
+ mark = re.escape(delimiter)
+ return (
+ rf"(?P<{name}>{mark}+)(?!{mark}){_IN_PARAGRAPH}*?"
+ rf"(? str:
+ """A JSX tag, opening or closing, every character of it a ``char``.
+
+ A name, then everything up to the first ``>`` outside a quoted value.
+ """
+ between = rf"(?:(?![\"'>]){char})*"
+ value = rf"\"(?:(?!\"){char})*\"|'(?:(?!'){char})*'"
+ return rf"?[A-Za-z_$]{between}(?:(?:{value}){between})*>"
+
+
+#: What MDX does not read as prose, found from left to right so that the
+#: first one to start wins, as in MDX. A tag with its lines to itself is a
+#: block and may run across blank lines; one within a paragraph may not. An
+#: escape, a code span and inline maths are blanked whole, a tag only in its
+#: quoted values, and a run of backticks or dollars that nothing closes is
+#: plain text.
+_MDX_READING = re.compile(
+ rf"(?P^[ \t]*{_jsx_tag(r'[\s\S]')}(?=[ \t]*$)|{_jsx_tag(_IN_PARAGRAPH)})"
+ rf"|(?P\\[!-/:-@\[-`{{-~]|{_span('code', '`')}|{_span('maths', '$')})"
+ r"|`+|\$+",
+ re.MULTILINE,
+)
+
+#: A quoted attribute value, which MDX reads as a string, never as prose.
+_JSX_VALUE = re.compile(r"\"[^\"]*\"|'[^']*'")
+
#: An inline code span: a run of backticks, its text, and a run of the same
#: length again. Each run is a whole run, as CommonMark reads it, so one tick
@@ -113,69 +195,170 @@ def _unescaped_dollars(line: str) -> int:
return len(re.findall(r"(? list[str]:
- lines = path.read_text(encoding="utf-8").splitlines()
- rel = path.relative_to(_ROOT).as_posix()
- problems: list[str] = []
- fenced = False
- open_math = False
+def _blank(match: re.Match[str]) -> str:
+ """The matched text with every character but its line breaks a space."""
+ return re.sub(r"[^\n]", " ", match.group())
- for number, line in enumerate(lines, start=1):
- if line.lstrip().startswith("```"):
- fenced = not fenced
- open_math = False
- continue
- if fenced:
+
+def _as_mdx_reads_it(found: re.Match[str]) -> str:
+ """One find of ``_MDX_READING``, blanked where MDX reads no prose.
+
+ A tag keeps all but its quoted values; an escape, a code span and inline
+ maths go whole; plain text stays.
+ """
+ if found["tag"] is not None:
+ return _JSX_VALUE.sub(_blank, found.group())
+ if found["literal"] is not None:
+ return _blank(found)
+ return found.group()
+
+
+def _body_start(lines: list[str]) -> int:
+ """Index of the first line after the YAML frontmatter, 0 with none."""
+ if not lines or lines[0].rstrip() != _FRONTMATTER:
+ return 0
+ for index in range(1, len(lines)):
+ if lines[index].rstrip() == _FRONTMATTER:
+ return index + 1
+ return 0
+
+
+def _mdx_prose(lines: list[str], start: int) -> list[str]:
+ """The lines as MDX reads their prose, everything else blanked.
+
+ The frontmatter, the code fences and the display maths go whole, line by
+ line; escapes, inline code, inline maths and the quoted attribute values
+ of a JSX tag go where they stand, read from left to right over the whole
+ page, since each can run over several lines. Blanking rather than
+ dropping keeps every character on its line and column.
+ """
+ kept = [""] * start
+ display = False
+ body = lines[start:]
+ for line, code in zip(body, code_lines(body), strict=True):
+ if not code and line.strip() == "$$":
+ display = not display
+ elif not (code or display):
+ kept.append(line)
continue
+ kept.append("")
+ return _MDX_READING.sub(_as_mdx_reads_it, "\n".join(kept)).split("\n")
+
+
+def _less_than_problems(rel: str, lines: list[str], start: int) -> list[str]:
+ """Rule 5: a ``<`` in MDX prose that MDX reads as the start of a tag."""
+ return [
+ f"{rel}:{number}: {prose[glued.start() : glued.start() + 12]!r} "
+ f"puts a '<' before a character no tag name starts with, and MDX "
+ f"reads it as a tag: the site build fails. Write it in words, put "
+ f"a space after '<', or escape it as '\\<'."
+ for number, prose in enumerate(_mdx_prose(lines, start), start=1)
+ for glued in _MDX_LESS_THAN.finditer(prose)
+ ]
+
+
+def _cut_maths_problems(
+ where: str, line: str, line_before: int, *, open_math: bool
+) -> list[str]:
+ """Rule 1: inline maths still open when a block marker ends the paragraph."""
+ if not (open_math and _BLOCK.match(line)):
+ return []
+ return [
+ f"{where}: inline maths opened on line {line_before} is cut off by a "
+ f"block marker; rewrap so the line does not start with "
+ f"{line.lstrip()[:2]!r}"
+ ]
+
+
+def _wrapped_quote_problems(where: str, line: str, before: str) -> list[str]:
+ """Rule 2: a ``>`` that wrapped to the start of a line of a paragraph."""
+ if not (_QUOTE.match(line) and _PROSE.match(before)):
+ return []
+ return [
+ f"{where}: {line.strip()[:40]!r} continues the sentence above but "
+ f"starts a block quote; rewrap so '>' does not start the line"
+ ]
+
+
+def _anchor_problems(where: str, line: str) -> list[str]:
+ """Rule 3: a same-page link that the href will percent-encode."""
+ return [
+ f"{where}: the same-page link "
+ f"'#{anchor.group(1)}' will be percent-encoded in the href, and the "
+ f"accessibility audit compares the raw attribute against the ids, so it "
+ f'reads as a dangling anchor. Put above '
+ f"the heading and link to that, as the pages that already hit this do."
+ for anchor in _ANCHOR.finditer(line)
+ if not anchor.group(1).isascii()
+ ]
+
+
+def _math_meta_problems(where: str, line: str) -> list[str]:
+ """Rule 4: an opening ``$$`` with the formula on the same line."""
+ if not _MATH_META.match(line):
+ return []
+ if line.rstrip().endswith("$$"):
+ consequence = (
+ "It closes on this line, so it ships as inline maths instead of a "
+ "centred display block."
+ )
+ else:
+ consequence = (
+ "Nothing closes it until a line that is exactly '$$', so it "
+ "swallows the prose in between and breaks the build far from here."
+ )
+ return [
+ f"{where}: {line.strip()[:40]!r} puts the formula on the opening "
+ f"'$$', where it is read as the fence's meta, not as maths. "
+ f"{consequence} Put '$$' alone on its own line, above and below."
+ ]
- if open_math and _BLOCK.match(line):
- problems.append(
- f"{rel}:{number}: inline maths opened on line {number - 1} is cut off by a "
- f"block marker; rewrap so the line does not start with "
- f"{line.lstrip()[:2]!r}"
- )
- open_math = False
- for anchor in _ANCHOR.finditer(line):
- target = anchor.group(1)
- if not target.isascii():
- problems.append(
- f"{rel}:{number}: the same-page link "
- f"'#{target}' will be percent-encoded in the href, and the accessibility "
- f"audit compares the raw attribute against the ids, so it reads as a "
- f'dangling anchor. Put above the heading '
- f"and link to that, as the pages that already hit this do."
- )
-
- if _MATH_META.match(line):
- closes_here = line.rstrip().endswith("$$")
- problems.append(
- f"{rel}:{number}: {line.strip()[:40]!r} puts the formula on the opening "
- f"'$$', where it is read as the fence's meta, not as maths. "
- + (
- "It closes on this line, so it ships as inline maths instead of a "
- "centred display block."
- if closes_here
- else "Nothing closes it until a line that is exactly '$$', so it "
- "swallows the prose in between and breaks the build far from here."
- )
- + " Put '$$' alone on its own line, above and below."
- )
-
- if number > 1 and _QUOTE.match(line) and _PROSE.match(lines[number - 2]):
- problems.append(
- f"{rel}:{number}: {line.strip()[:40]!r} continues the sentence above but "
- f"starts a block quote; rewrap so '>' does not start the line"
- )
-
- if not line.strip() or _BLOCK.match(line):
+def _maths_left_open(line: str, *, open_math: bool) -> bool:
+ """Whether an inline ``$`` is still open once this line is read."""
+ if not line.strip() or _BLOCK.match(line):
+ open_math = False
+ if _unescaped_dollars(line) % 2 == 1:
+ open_math = not open_math
+ return open_math
+
+
+def _markdown_problems(rel: str, lines: list[str], start: int) -> list[str]:
+ """Rules 1 to 4, which read the page line by line as CommonMark does."""
+ problems: list[str] = []
+ open_math = False
+ code = [False] * start + list(code_lines(lines[start:]))
+ for index in range(start, len(lines)):
+ line = lines[index]
+ if code[index]:
open_math = False
- if _unescaped_dollars(line) % 2 == 1:
- open_math = not open_math
+ continue
+ where = f"{rel}:{index + 1}"
+ before = lines[index - 1] if index else ""
+ problems.extend(_cut_maths_problems(where, line, index, open_math=open_math))
+ problems.extend(_wrapped_quote_problems(where, line, before))
+ problems.extend(_anchor_problems(where, line))
+ problems.extend(_math_meta_problems(where, line))
+ open_math = _maths_left_open(line, open_math=open_math)
+ return problems
+
+def check_text(text: str, rel: str) -> list[str]:
+ """Every hazard of one page; ``rel`` names it, and an ``.mdx`` one is MDX."""
+ lines = text.splitlines()
+ start = _body_start(lines)
+ problems = _markdown_problems(rel, lines, start)
+ if rel.endswith(".mdx"):
+ problems.extend(_less_than_problems(rel, lines, start))
return problems
+def _check(path: pathlib.Path) -> list[str]:
+ return check_text(
+ path.read_text(encoding="utf-8"), path.relative_to(_ROOT).as_posix()
+ )
+
+
def main() -> int:
problems: list[str] = []
checked = 0
diff --git a/scripts/check_reference_values.py b/scripts/check_reference_values.py
index 0bee59ce5..0d2473916 100644
--- a/scripts/check_reference_values.py
+++ b/scripts/check_reference_values.py
@@ -207,6 +207,11 @@
101.325,
"ECAC Doc 29 Eq. 4-7, the mean-sea-level static pressure in kPa",
),
+ ("phonometry.metrology.comparison_calibration", "_REFERENCE_STATIC_PRESSURE_KPA"): (
+ 101.325,
+ "IEC 61094-5:2016 and IEC 61094-8:2012 clause 4, the reference static "
+ "pressure in kPa",
+ ),
("phonometry.building.measurement.heavy_impact", "_FORCE_REFERENCE"): (
1.0,
"ISO 16283-2:2020 Formula (A.1), a force level re 1 N",
diff --git a/scripts/conformance/domains/comparison_calibration.py b/scripts/conformance/domains/comparison_calibration.py
new file mode 100644
index 000000000..7e93966df
--- /dev/null
+++ b/scripts/conformance/domains/comparison_calibration.py
@@ -0,0 +1,335 @@
+# Copyright (c) 2026. Jose Manuel Requena Plens
+"""Calibration of working standard microphones by comparison.
+
+IEC 61094-5:2016 prints the corrections of a WS3 microphone in the jig of
+Figure A.4 (Table A.1) and a worked uncertainty budget at 2 kHz (Table D.1,
+combined in D.3); IEC 61094-8:2012 prints the typical expanded uncertainty of
+each way of calibrating the reference microphone (Table 1) and the typical
+components of a free-field budget (Table 2). The rows below reproduce each from
+the library, and check the measurement models against the text they are
+derived from: readings built by Formulas (C.1) and (C.2), or by the monitor
+ratios of IEC 61094-8 A.2, from known sensitivities, gains and fields have to
+give back the sensitivity those quantities define, and the effective
+free-field region of Formula (B.1) has to put a reflection from its boundary
+at the end of the time window.
+
+Oracle: IEC 61094-5:2016 (Edition 2.0, English-French): Table A.1 on printed
+folio 15 (PDF page 17), Annex C on folio 18 (PDF page 20), D.2 on folio 19
+(PDF page 21), Table D.1 on folio 20 (PDF page 22) and its continuation and
+D.3 on folio 21 (PDF page 23). BS EN 61094-8:2012, the English text of
+EN 61094-8:2012 which is IEC 61094-8:2012 unchanged: Table 1 on folio 12 (PDF
+page 14), Table 2 on folio 17 (PDF page 19) and B.1 on folio 23 (PDF page 25).
+
+One printed defect sits in this oracle and is recorded in ``docs/ERRATA.md``:
+D.3 of IEC 61094-5 states a combined standard uncertainty of 0,040 dB and an
+expanded one of 0,08 dB, where the root-sum-square of the eight components
+Table D.1 prints is 0,0437 dB and twice it 0,087 dB. The rows pin the values
+the page's own components give.
+"""
+
+from __future__ import annotations
+
+import math
+
+import numpy as np
+import reference_data as ref
+
+from phonometry import metrology
+
+from ..registry import Outcome, count, numeric, register
+
+_IEC61094 = "Microphone calibration by comparison (IEC 61094-5, IEC 61094-8)"
+
+#: A reference and a test microphone, sensitivity levels in dB re 1 V/Pa, at
+#: five frequencies, from which the derivation rows build their readings.
+_F = np.array([250.0, 1000.0, 2000.0, 4000.0, 8000.0])
+_L_REF = np.array([-38.02, -38.00, -37.98, -37.95, -38.10])
+_L_TEST = np.array([-26.40, -26.35, -26.30, -26.10, -25.80])
+
+
+def _budget_d1() -> metrology.ComparisonUncertaintyBudget:
+ return metrology.comparison_uncertainty_budget(
+ ref.IEC61094_5_TABLE_D1_STANDARD_DB, frequency_hz=2000.0
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 Table D.1",
+ "Standard uncertainty at 2 kHz of the 7 components whose row states a value",
+)
+def _chk_d1_components() -> Outcome:
+ """Each semi-range over the square root of 3 and the reference's expanded
+ uncertainty over 2, to the three decimals the table prints. The eighth
+ row, the repeatability, states no value and prints its standard
+ uncertainty alone, so there is nothing to derive it from.
+ """
+ table = metrology.IEC61094_5_TABLE_D1
+ matching = 0
+ for key, (stated, divisor) in ref.IEC61094_5_TABLE_D1_STATED.items():
+ row = table[key]
+ printed = ref.IEC61094_5_TABLE_D1_STANDARD_DB[key]
+ if row.stated_db is None or row.divisor is None:
+ continue
+ if (
+ math.isclose(row.stated_db, stated, rel_tol=1e-12)
+ and math.isclose(row.divisor, divisor, rel_tol=1e-12)
+ and math.isclose(
+ round(row.stated_db / row.divisor, 3), printed, abs_tol=1e-12
+ )
+ ):
+ matching += 1
+ return count(
+ matching,
+ len(ref.IEC61094_5_TABLE_D1_STATED),
+ subject="components of Table D.1",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 D.3",
+ "Combined standard uncertainty of the example budget at 2 kHz",
+)
+def _chk_d3_combined() -> Outcome:
+ """The root-sum-square of the printed column, 0,043 67 dB.
+
+ D.3 prints 0,040 dB, which is not the root-sum-square of the eight
+ components Table D.1 prints (docs/ERRATA.md); the row pins the value the
+ page's own components give.
+ """
+ return numeric(
+ ref.IEC61094_5_D3_COMBINED_DB,
+ _budget_d1().combined_uncertainty_db,
+ 0.00005,
+ unit="dB",
+ places=4,
+ expected_label="0.0437 dB (printed 0,040, an erratum)",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 D.2, D.3",
+ "Expanded uncertainty (k = 2) of the example budget at 2 kHz",
+)
+def _chk_d3_expanded() -> Outcome:
+ """Twice 0,043 67 dB is 0,087 dB; D.3 prints 0,08 dB (docs/ERRATA.md)."""
+ return numeric(
+ ref.IEC61094_5_D3_EXPANDED_DB,
+ _budget_d1().expanded_uncertainty_db,
+ 0.0005,
+ unit="dB",
+ places=3,
+ expected_label="0.087 dB (printed 0,08, an erratum)",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 D.3",
+ "Combined standard uncertainty by the strict calculation in linear form",
+)
+def _chk_d3_linear() -> Outcome:
+ """D.3: "a strict calculation would require each component to be converted
+ from logarithmic to linear form before doing the combination but as the
+ values are very small, the result would be essentially the same". The
+ page prints no value for it; the printed column, each component taken to
+ 10**(u/20) - 1, combined in quadrature and taken back by 20 lg(1 + r),
+ gives 0,043 614 dB, 0,000 055 dB below the combination in decibels.
+ """
+ return numeric(
+ ref.IEC61094_5_D3_LINEAR_COMBINED_DB,
+ _budget_d1().linear_combined_uncertainty_db,
+ 0.000001,
+ unit="dB",
+ places=6,
+ expected_label="0.043614 dB (0.043669 dB in decibels)",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 Table A.1",
+ "Correction of a WS3 microphone against an LS2aP in the jig of Figure A.4, 14 frequencies",
+)
+def _chk_table_a1() -> Outcome:
+ """Each of the 14 corrections from 1 kHz to 20 kHz, to the thousandth of a
+ decibel the table prints.
+ """
+ printed = {1000.0 * khz: value for khz, value in ref.IEC61094_5_TABLE_A1_DB.items()}
+ jig = metrology.jig_diameter_correction(list(printed))
+ matching = sum(
+ 1
+ for value, computed in zip(printed.values(), jig.correction_db, strict=True)
+ if math.isclose(float(computed), value, abs_tol=1e-12)
+ )
+ return count(matching, len(printed), subject="corrections of Table A.1")
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 Table A.1 NOTE",
+ "Expanded uncertainty of the correction at 20 kHz, a tenth of its value",
+)
+def _chk_table_a1_uncertainty() -> Outcome:
+ """A tenth of 1,443 dB."""
+ jig = metrology.jig_diameter_correction([20000.0])
+ return numeric(
+ ref.IEC61094_5_TABLE_A1_RELATIVE_EXPANDED * 1.443,
+ float(jig.expanded_uncertainty_db[0]),
+ 1e-12,
+ unit="dB",
+ places=4,
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 Formulas (C.1) to (C.3)",
+ "Sensitivity level of the test microphone through the interchange, whatever the channel gains, source drift and field asymmetry",
+)
+def _chk_annex_c() -> Outcome:
+ """Readings built by (C.1) and (C.2) with channel gains of +0,7 dB and
+ -1,3 dB, a source 0,3 dB louder after the interchange and a field 0,06 dB
+ stronger at position A than at B give back the test microphone's level.
+ """
+ level_1 = 94.0 + 0.2 * np.log10(_F / 1000.0)
+ level_2 = level_1 + 0.3
+ gain_1, gain_2, at_a, at_b = 0.7, -1.3, 0.04, -0.02
+ first = (_L_REF + gain_1 + level_1 + at_a) - (_L_TEST + gain_2 + level_1 + at_b)
+ second = (_L_TEST + gain_1 + level_2 + at_a) - (_L_REF + gain_2 + level_2 + at_b)
+ result = metrology.simultaneous_comparison(_F, _L_REF, first, second)
+ worst = float(np.max(np.abs(result.sensitivity_level_db - _L_TEST)))
+ return numeric(
+ 0.0,
+ worst,
+ 1e-9,
+ unit="dB",
+ places=6,
+ expected_label="the test microphone's level at all 5 frequencies",
+ computed_label=f"max deviation {worst:.9f} dB",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-5:2016 D.2",
+ "M_test = M_ref x R_V / R_P in linear form against the level form",
+)
+def _chk_d2() -> Outcome:
+ """The sensitivity in mV/Pa from the level model against the product of
+ D.2, with R_V from 3,9 to 4,3 and R_P from 0,999 to 1,01.
+ """
+ m_ref = 10.0 ** (_L_REF / 20.0)
+ r_v = np.array([3.9, 3.95, 4.0, 4.1, 4.3])
+ r_p = np.array([1.001, 1.0, 0.999, 1.002, 1.01])
+ result = metrology.simultaneous_comparison(
+ _F,
+ _L_REF,
+ np.zeros(_F.size),
+ 2.0 * 20.0 * np.log10(r_v),
+ pressure_level_difference_db=20.0 * np.log10(r_p),
+ )
+ expected = 1000.0 * m_ref * r_v / r_p
+ worst = float(np.max(np.abs(result.sensitivity_mv_per_pa / expected - 1.0)))
+ return numeric(
+ 0.0,
+ worst,
+ 1e-12,
+ places=12,
+ expected_label="M_ref x R_V / R_P at all 5 frequencies",
+ computed_label=f"max relative deviation {worst:.12f}",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-8:2012 A.2",
+ "Monitor ratios cancel a source that drifts between the two measurements",
+)
+def _chk_monitor() -> Outcome:
+ """The field changes by up to 0,4 dB between the reference and the test
+ microphone; the quotient of the two ratios to the monitor gives the test
+ microphone's level regardless.
+ """
+ field_ref = 94.0 + np.array([0.0, 0.1, -0.1, 0.05, 0.2])
+ field_test = field_ref + np.array([0.3, -0.2, 0.15, 0.4, -0.35])
+ result = metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ _L_REF + field_ref,
+ _L_TEST + field_test,
+ reference_monitor_level_db=field_ref - 12.5,
+ test_monitor_level_db=field_test - 12.5,
+ field="free_field",
+ )
+ worst = float(np.max(np.abs(result.sensitivity_level_db - _L_TEST)))
+ return numeric(
+ 0.0,
+ worst,
+ 1e-9,
+ unit="dB",
+ places=6,
+ expected_label="the test microphone's level at all 5 frequencies",
+ computed_label=f"max deviation {worst:.9f} dB",
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-8:2012 Table 1",
+ "Typical expanded uncertainty of the 5 reference calibration options at 1 kHz and 10 kHz",
+)
+def _chk_table_1() -> Outcome:
+ """Ten cells, from 0,10 dB for primary free-field reciprocity at 10 kHz to
+ 0,6 dB for an electrostatic actuator, each counted only on the row whose
+ microphone type, method and references are the printed ones.
+ """
+ matching = 0
+ for key, (at_1k, at_10k) in ref.IEC61094_8_TABLE_1_DB.items():
+ row = metrology.IEC61094_8_TABLE_1[key]
+ if (row.microphone_types, row.method, row.references) != (
+ ref.IEC61094_8_TABLE_1_TEXT[key]
+ ):
+ continue
+ matching += math.isclose(row.expanded_uncertainty_1khz_db, at_1k)
+ matching += math.isclose(row.expanded_uncertainty_10khz_db, at_10k)
+ return count(
+ matching, 2 * len(ref.IEC61094_8_TABLE_1_DB), subject="cells of Table 1"
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-8:2012 Table 2",
+ "Source of uncertainty and subclause references of the 12 typical components",
+)
+def _chk_table_2() -> Outcome:
+ """Each component as printed and the subclauses the table refers it to."""
+ matching = sum(
+ 1
+ for key, subclauses in ref.IEC61094_8_TABLE_2_SUBCLAUSES.items()
+ if metrology.IEC61094_8_TABLE_2[key].subclauses == subclauses
+ and metrology.IEC61094_8_TABLE_2[key].component
+ == ref.IEC61094_8_TABLE_2_SOURCES[key]
+ )
+ return count(
+ matching, len(ref.IEC61094_8_TABLE_2_SUBCLAUSES), subject="rows of Table 2"
+ )
+
+
+@register(
+ _IEC61094,
+ "IEC 61094-8:2012 Formula (B.1), Figure B.1",
+ "A reflection from the boundary of the effective free-field region arrives at the end of a 5 ms window",
+)
+def _chk_region() -> Outcome:
+ """A plane parallel to the axis at the semi-minor axis of the spheroid
+ reflects along the image path sqrt(d**2 + 4 b**2), which is A = d + tau c,
+ so its reflection arrives tau after the direct sound, at 1 m and 23 °C.
+ """
+ region = metrology.free_field_region(1.0, 0.005)
+ reflected = math.hypot(region.source_distance_m, 2.0 * region.semi_minor_axis_m)
+ delay_ms = 1000.0 * (reflected - region.source_distance_m) / region.speed_of_sound
+ return numeric(5.0, delay_ms, 1e-9, unit="ms", places=6)
diff --git a/scripts/conformance_report.py b/scripts/conformance_report.py
index d985fc039..5ecc7a9b1 100644
--- a/scripts/conformance_report.py
+++ b/scripts/conformance_report.py
@@ -105,6 +105,7 @@
electroacoustics,
random_incidence,
free_field_corrections,
+ comparison_calibration,
induction_loop,
noise_control,
enclosure_cabin_insulation,
@@ -228,6 +229,7 @@
"building_prediction",
"calibrators",
"cnossos_rail",
+ "comparison_calibration",
"control_valves",
"control_valves_hydrodynamic",
"count",
diff --git a/scripts/diagrams/canvas.py b/scripts/diagrams/canvas.py
index 7a7ee6983..d63e71cc2 100644
--- a/scripts/diagrams/canvas.py
+++ b/scripts/diagrams/canvas.py
@@ -188,6 +188,10 @@ class Theme:
# printed upright in C_FF,RM.
"FF",
"RM",
+ # test/mon: the microphone under test of IEC 61094-5:2016 D.2, printed
+ # upright in M_test, and the monitor microphone of IEC 61094-8:2012.
+ "test",
+ "mon",
"rms",
"tot",
"TOT",
diff --git a/scripts/diagrams/i18n.py b/scripts/diagrams/i18n.py
index 810cc12ad..c3af3df3b 100644
--- a/scripts/diagrams/i18n.py
+++ b/scripts/diagrams/i18n.py
@@ -4698,6 +4698,30 @@
"a design verifier reads the first half off a computed response; a laboratory has to meet both,": "un verificador de diseño lee la primera mitad en una respuesta calculada; un laboratorio tiene que cumplir las dos,",
"and verify_sound_level_meter_periodic and verify_filter_periodic grade both on periodic results": "y verify_sound_level_meter_periodic y verify_filter_periodic califican las dos en resultados periódicos",
"IEC 61043 keeps all three in one document: requirements in clauses 6 to 10, type tests in 11 to 13, periodic verification in Annex A": "IEC 61043 reúne las tres partes: requisitos en los apartados 6 a 10, ensayos de tipo en 11 a 13, verificación periódica en el Anexo A",
+ "Calibration by comparison: the readings of IEC 61094-5 and IEC 61094-8": "Calibración por comparación: las lecturas de IEC 61094-5 e IEC 61094-8",
+ "Pressure field, simultaneous": "Campo de presión, simultánea",
+ "IEC 61094-5, Annex C": "IEC 61094-5, Anexo C",
+ "Free field, sequential": "Campo libre, secuencial",
+ "IEC 61094-8, Annex A": "IEC 61094-8, Anexo A",
+ "channel 1": "canal 1",
+ "channel 2": "canal 2",
+ "$L_{C12}$": "$L_{C12}$",
+ "$L_{C21}$": "$L_{C21}$",
+ "interchanged in the ports and on the preamplifiers": "intercambiados en los puertos y en los preamplificadores",
+ "Formula (C.3)": "Fórmula (C.3)",
+ "$L_{test} = L_{ref} − ½(L_{C12} − L_{C21})$": "$L_{test} = L_{ref} − ½(L_{C12} − L_{C21})$",
+ "the gains of the channels and the field's asymmetry cancel": "las ganancias de los canales y la asimetría del campo se anulan",
+ "$V_{ref}$": "$V_{ref}$",
+ "$V_{test}$": "$V_{test}$",
+ "$V_{mon,1}$": "$V_{mon,1}$",
+ "$V_{mon,2}$": "$V_{mon,2}$",
+ "the same point, in turn": "el mismo punto, por turnos",
+ "IEC 61094-8 A.2": "IEC 61094-8, A.2",
+ "$L_{test} = L_{ref} + 20 lg(V_{test}/V_{mon,2}) − 20 lg(V_{ref}/V_{mon,1})$": "$L_{test} = L_{ref} + 20 lg(V_{test}/V_{mon,2}) − 20 lg(V_{ref}/V_{mon,1})$",
+ "the monitor takes out any drift of the source": "el monitor elimina cualquier deriva de la fuente",
+ "microphone under test": "micrófono en ensayo",
+ "monitor microphone": "micrófono monitor",
+ "Then the corrections (environment, the WS3 jig, the reference's free-field difference) and $U$ with $k$ = 2 (D.3, 8.8).": "Después, las correcciones (ambiente, el soporte del WS3, la diferencia de campo libre de la referencia) y $U$ con $k$ = 2 (D.3, 8.8).",
}
diff --git a/scripts/diagrams/registry.py b/scripts/diagrams/registry.py
index 704110da1..7394df7be 100644
--- a/scripts/diagrams/registry.py
+++ b/scripts/diagrams/registry.py
@@ -163,6 +163,7 @@
_d_calibration_coupling,
_d_calibration_dataflow,
_d_cepstrum_echoes,
+ _d_comparison_calibration_setup,
_d_correlation_delay,
_d_data_qualification,
_d_echo_geometry,
@@ -250,6 +251,11 @@
"The readings of IEC 62585: a meter compared with a reference",
522,
),
+ "diagram_comparison_calibration_setup": (
+ _d_comparison_calibration_setup,
+ "Calibration by comparison: the readings of IEC 61094-5 and IEC 61094-8",
+ 470,
+ ),
"diagram_env_measurement": (
_d_env_positions,
"Environmental noise measurement positions (ISO 1996-2)",
diff --git a/scripts/diagrams/signals.py b/scripts/diagrams/signals.py
index 3565b7408..0a0cc51e2 100644
--- a/scripts/diagrams/signals.py
+++ b/scripts/diagrams/signals.py
@@ -4166,3 +4166,133 @@ def _d_free_field_corrections_setup(s: SVG, th: Theme) -> None:
13,
th.muted,
)
+
+
+# ---------------------------------------------------------------------------
+# IEC 61094-5 and IEC 61094-8: a microphone calibrated by comparison
+# ---------------------------------------------------------------------------
+
+
+def _test_mic_side(s: SVG, th: Theme, x: float, y: float, facing: float) -> None:
+ """A working standard microphone on its preamplifier, the diaphragm at
+ (x, y), facing left (``facing`` = -1) or right (+1).
+ """
+ back = -facing
+ capsule_end = x + back * 12
+ body_end = x + back * 80
+ s.rect(min(x, capsule_end), y - 6, 12, 12, th.primary, th.fg, rx=2, sw=1.3)
+ s.rect(min(capsule_end, body_end), y - 4, 68, 8, th.panel, th.fg, rx=2, sw=1.2)
+
+
+def _monitor_mic(s: SVG, th: Theme, x: float, y: float) -> None:
+ """A small monitor microphone near the source, facing down to the field."""
+ s.rect(x - 4, y - 4, 8, 8, th.accent, th.fg, rx=1.5, sw=1.2)
+ s.line(x, y - 4, x, y - 22, th.fg, 1.4)
+
+
+def _coupler_pair(
+ s: SVG, th: Theme, cx: float, y: float, *, reference_left: bool
+) -> None:
+ """Two microphones face to face in a coupler, each on its own channel."""
+ left, right = cx - 20.0, cx + 20.0
+ s.rect(left, y - 18, right - left, 36, th.panel, th.fg, rx=3, sw=1.5)
+ if reference_left:
+ _reference_side(s, th, left - 2, y, 1.0)
+ _test_mic_side(s, th, right + 2, y, -1.0)
+ else:
+ _test_mic_side(s, th, left - 2, y, 1.0)
+ _reference_side(s, th, right + 2, y, -1.0)
+ s.text(cx - 64, y + 28, "channel 1", 12, th.muted)
+ s.text(cx + 64, y + 28, "channel 2", 12, th.muted)
+
+
+def _field_position(
+ s: SVG, th: Theme, y: float, *, reference: bool, reading: str, monitor: str
+) -> None:
+ """The source, its monitor and one microphone at the measuring point."""
+ _speaker(s, th, 492, y, 1.0)
+ _monitor_mic(s, th, 532, y - 26)
+ s.text(544, y - 22, monitor, 14, th.fg, anchor="start")
+ for dy in (-8.0, 0.0, 8.0):
+ s.arrow(526, y + dy, 600, y + dy, th.muted, 1.3)
+ if reference:
+ _reference_side(s, th, 640, y, -1.0)
+ else:
+ _test_mic_side(s, th, 640, y, -1.0)
+ s.text(748, y + 5, reading, 15, th.fg, anchor="start")
+
+
+def _d_comparison_calibration_setup(s: SVG, th: Theme) -> None:
+ """IEC 61094-5 and IEC 61094-8: the readings of a comparison calibration.
+
+ Left, a pressure calibration by simultaneous excitation: the reference and
+ the microphone under test face to face in a coupler, read on two channels,
+ then interchanged (Annex C). Right, a free-field calibration by
+ sequential excitation: the reference and then the microphone under test at
+ the same point, each read against a monitor microphone near the source
+ (A.2).
+ """
+ s.text(225, 66, "Pressure field, simultaneous", 15, th.fg, bold=True)
+ s.text(225, 86, "IEC 61094-5, Annex C", 13, th.muted)
+ s.text(675, 66, "Free field, sequential", 15, th.fg, bold=True)
+ s.text(675, 86, "IEC 61094-8, Annex A", 13, th.muted)
+ s.line(450, 54, 450, 374, th.muted, 1.0, dash="4,4")
+
+ # ----- Left: the coupler, then the interchange ------------------------
+ rows_y = (146.0, 262.0)
+ for k, (y, label, reference_left) in enumerate(
+ zip(rows_y, ("$L_{C12}$", "$L_{C21}$"), (True, False), strict=True)
+ ):
+ _step(s, th, 22, y, str(k + 1))
+ _coupler_pair(s, th, 225, y, reference_left=reference_left)
+ s.text(225, y - 28, label, 15, th.fg)
+ s.text(225, 202, "interchanged in the ports and on the preamplifiers", 12, th.muted)
+ s.text(225, 318, "Formula (C.3)", 13, th.fg, bold=True)
+ s.text(225, 338, "$L_{test} = L_{ref} − ½(L_{C12} − L_{C21})$", 14, th.fg)
+ s.text(
+ 225,
+ 358,
+ "the gains of the channels and the field's asymmetry cancel",
+ 12,
+ th.muted,
+ )
+
+ # ----- Right: the reference, then the microphone under test -----------
+ for k, (y, reference, reading, monitor) in enumerate(
+ zip(
+ rows_y,
+ (True, False),
+ ("$V_{ref}$", "$V_{test}$"),
+ ("$V_{mon,1}$", "$V_{mon,2}$"),
+ strict=True,
+ )
+ ):
+ _step(s, th, 470, y, str(k + 1))
+ _field_position(s, th, y, reference=reference, reading=reading, monitor=monitor)
+ s.text(675, 202, "the same point, in turn", 12, th.muted)
+ s.text(675, 318, "IEC 61094-8 A.2", 13, th.fg, bold=True)
+ s.text(
+ 675,
+ 338,
+ "$L_{test} = L_{ref} + 20 lg(V_{test}/V_{mon,2}) − 20 lg(V_{ref}/V_{mon,1})$",
+ 14,
+ th.fg,
+ )
+ s.text(675, 358, "the monitor takes out any drift of the source", 12, th.muted)
+
+ # ----- Key and what follows -------------------------------------------
+ ky = 404.0
+ _reference_side(s, th, 30, ky, -1.0)
+ s.text(132, ky + 5, "reference microphone", 13, th.fg, anchor="start")
+ _test_mic_side(s, th, 322, ky, -1.0)
+ s.text(412, ky + 5, "microphone under test", 13, th.fg, anchor="start")
+ _monitor_mic(s, th, 640, ky + 8)
+ s.text(656, ky + 5, "monitor microphone", 13, th.fg, anchor="start")
+ s.text(
+ 450,
+ 446,
+ "Then the corrections (environment, the WS3 jig, the reference's "
+ "free-field difference) and $U$ with $k$ = 2 (D.3, 8.8).",
+ 13,
+ th.muted,
+ )
diff --git a/scripts/figures/metrology.py b/scripts/figures/metrology.py
index 29228b47d..05384c66b 100644
--- a/scripts/figures/metrology.py
+++ b/scripts/figures/metrology.py
@@ -32,6 +32,8 @@
from collections.abc import Callable
from phonometry.metrology import (
+ ComparisonCalibration,
+ ComparisonUncertaintyBudget,
DirectivityFactor,
FreeFieldCorrection,
RandomIncidenceSensitivity,
@@ -1710,3 +1712,153 @@ def generate_free_field_verification(output_dir: str) -> None:
fig.tight_layout()
save_figure(output_dir, "free_field_verification.svg")
plt.close()
+
+
+#: IEC 61094-5 Table D.1, the eight standard uncertainties at 2 kHz, dB.
+_TABLE_D1_DB = {
+ "reference": 0.025,
+ "capacitance": 0.006,
+ "non_linearity": 0.017,
+ "impedance": 0.003,
+ "polarizing_voltage": 0.005,
+ "repeatability": 0.025,
+ "drift": 0.017,
+ "rounding": 0.003,
+}
+
+
+def _pressure_comparison() -> "ComparisonCalibration":
+ """The guide's ``c``: a WS2P against an LS2P, interchanged in a coupler."""
+ from phonometry import metrology
+
+ f = metrology.exact_frequencies(250, 20000, fraction=3)
+ x = f / 1000
+ l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2
+ l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4
+ gain_1, gain_2 = 0.35, -0.20
+ field_a = 0.03 * np.sqrt(x)
+ rng = np.random.default_rng(61094)
+ noise = rng.normal(0.0, 0.004, (2, 3, f.size))
+ l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0]
+ l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1]
+ env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2,
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005,
+ temperature_coefficient_db_per_k=0.002,
+ )
+ budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**_TABLE_D1_DB, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2},
+ frequency_hz=fx,
+ )
+ for fx in f
+ ]
+ return metrology.simultaneous_comparison(
+ f,
+ l_ref,
+ l_c12,
+ l_c21,
+ reference_environment=env,
+ expanded_uncertainty_db=[b.expanded_uncertainty_db for b in budgets],
+ )
+
+
+def _free_field_budget(frequency_hz: float) -> "ComparisonUncertaintyBudget":
+ """The guide's free-field budget: the components of IEC 61094-8 Table 2."""
+ from phonometry import metrology
+
+ x = frequency_hz / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x,
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x**2,
+ "free_field": 0.03 + 0.12 * x**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=frequency_hz,
+ field="free_field",
+ )
+
+
+def _free_field_comparison() -> "ComparisonCalibration":
+ """The guide's ``free``: a WS2F by substitution, with a monitor."""
+ from phonometry import metrology
+
+ f = metrology.exact_frequencies(500, 20000, fraction=3)
+ x = f / 1000
+ c_ff = 0.05 * x**1.3
+ l_ref_p = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2
+ l_ws2f = -38.3 + 0.1 * np.log10(x) - 0.3 * (x / 20) ** 3
+ field_1 = 74.0 + 0.05 * np.sin(x)
+ field_2 = field_1 + 0.25
+ monitor = -40.0
+ return metrology.sequential_comparison(
+ f,
+ l_ref_p,
+ l_ref_p + c_ff + field_1,
+ l_ws2f + field_2,
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[
+ _free_field_budget(fx).expanded_uncertainty_db for fx in f
+ ],
+ )
+
+
+def generate_comparison_calibration(output_dir: str) -> None:
+ """IEC 61094-5 and IEC 61094-8: a pressure and a free-field calibration."""
+ print("Generating comparison_calibration...")
+ fig, (ax_pressure, ax_free) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+ _pressure_comparison().plot(ax_pressure, language=_LANG)
+ _free_field_comparison().plot(ax_free, language=_LANG)
+ fig.tight_layout()
+ save_figure(output_dir, "comparison_calibration.svg")
+ plt.close()
+
+
+def generate_comparison_budget(output_dir: str) -> None:
+ """IEC 61094-5 Table D.1 at 2 kHz and an IEC 61094-8 budget at 8 kHz."""
+ print("Generating comparison_budget...")
+ from phonometry import metrology
+
+ pressure = metrology.comparison_uncertainty_budget(_TABLE_D1_DB, frequency_hz=2000)
+ free = _free_field_budget(8000)
+ fig, (ax_pressure, ax_free) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+ pressure.plot(ax_pressure, language=_LANG)
+ free.plot(ax_free, language=_LANG)
+ fig.tight_layout()
+ save_figure(output_dir, "comparison_budget.svg")
+ plt.close()
+
+
+def generate_comparison_jig_correction(output_dir: str) -> None:
+ """IEC 61094-5 Table A.1: a WS3 microphone in the jig of Figure A.4."""
+ print("Generating comparison_jig_correction...")
+ from phonometry import metrology
+
+ fig, ax = plt.subplots(figsize=(10, 6))
+ metrology.jig_diameter_correction().plot(ax, language=_LANG)
+ fig.tight_layout()
+ save_figure(output_dir, "comparison_jig_correction.svg")
+ plt.close()
+
+
+def generate_free_field_region(output_dir: str) -> None:
+ """IEC 61094-8 B.1: the effective free-field region of a 5 ms window."""
+ print("Generating free_field_region...")
+ from phonometry import metrology
+
+ fig, ax = plt.subplots(figsize=(10, 6))
+ metrology.free_field_region(1.0, 0.005).plot(ax, language=_LANG)
+ fig.tight_layout()
+ save_figure(output_dir, "free_field_region.svg")
+ plt.close()
diff --git a/scripts/figures/registry.py b/scripts/figures/registry.py
index b3dc868e9..a18e07272 100644
--- a/scripts/figures/registry.py
+++ b/scripts/figures/registry.py
@@ -412,11 +412,15 @@
generate_calibration_narrowband_bias,
generate_calibration_stability,
generate_calibrator_verification,
+ generate_comparison_budget,
+ generate_comparison_calibration,
+ generate_comparison_jig_correction,
generate_conformance_rule_examples,
generate_dbfs_versus_spl,
generate_diffuse_field_sensitivity,
generate_free_field_adjustment,
generate_free_field_correction,
+ generate_free_field_region,
generate_free_field_uncertainty,
generate_free_field_verification,
generate_random_incidence_correction,
@@ -1133,6 +1137,12 @@
generate_free_field_correction,
generate_free_field_uncertainty,
generate_free_field_verification,
+ # IEC 61094-5 and IEC 61094-8: a working standard microphone calibrated
+ # by comparison with a reference, in a pressure and in a free field.
+ generate_comparison_calibration,
+ generate_comparison_budget,
+ generate_comparison_jig_correction,
+ generate_free_field_region,
generate_stationarity_glide_blind_spot,
generate_rice_nongaussian_screen,
# Psychoacoustics / open-plan plots (sharpness weighting, spatial decay)
diff --git a/scripts/generate_graphs.py b/scripts/generate_graphs.py
index 7fcd9d3be..e7eb86662 100644
--- a/scripts/generate_graphs.py
+++ b/scripts/generate_graphs.py
@@ -269,9 +269,13 @@
)
from figures.metrology import (
generate_calibration_stability,
+ generate_comparison_budget,
+ generate_comparison_calibration,
+ generate_comparison_jig_correction,
generate_diffuse_field_sensitivity,
generate_free_field_adjustment,
generate_free_field_correction,
+ generate_free_field_region,
generate_free_field_uncertainty,
generate_free_field_verification,
generate_random_incidence_correction,
@@ -581,6 +585,9 @@
"generate_cnossos_road_emission",
"generate_cnossos_road_speed_law",
"generate_coherent_output_snr",
+ "generate_comparison_budget",
+ "generate_comparison_calibration",
+ "generate_comparison_jig_correction",
"generate_correlation_normalizations",
"generate_cross_spectral_density_delay",
"generate_crossover_plot",
@@ -634,6 +641,7 @@
"generate_free_field_adjustment",
"generate_free_field_correction",
"generate_free_field_uncertainty",
+ "generate_free_field_region",
"generate_free_field_verification",
"generate_frequency_response",
"generate_g_weighting_response",
diff --git a/site/public/llms/llms-signals-metrology.txt b/site/public/llms/llms-signals-metrology.txt
index 6e7b0596a..f5ece9bb5 100644
--- a/site/public/llms/llms-signals-metrology.txt
+++ b/site/public/llms/llms-signals-metrology.txt
@@ -81,6 +81,13 @@ clause from the frequency weightings to the high-level stability against the
acceptance limits of IEC 61672-1 and the maxima of its Table B.1, what a
complete test holds, and the statement Clause 22 prescribes for the outcome.
+[Calibration by comparison](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/)
+goes one step up the chain, to the microphone itself: a working standard
+microphone takes the sensitivity of a reference through the ratio of their
+output voltages, in a coupler by IEC 61094-5 or in a free field by IEC
+61094-8, with the environmental, jig and free-field corrections each part asks
+for and the uncertainty budget of Table D.1 or Table 2.
+
The same discipline extends into the frequency domain: the
[Signals and spectra](https://jmrplens.github.io/phonometry/signals/spectra/) pages
apply the Bendat & Piersol error analysis to Welch spectral estimates, so
@@ -119,6 +126,11 @@ budgets that are specialisations of the GUM machinery described here.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's results, clause by clause, with Tables 4, 5
and B.1 of IEC 61672-1, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/):
+ the simultaneous comparison with the interchange of Annex C, the sequential
+ one against a monitor, the environmental and WS3 jig corrections, the
+ budgets of Table D.1 and Table 2 with k = 2, and the free-field region of a
+ time window.
## What this section does not cover
@@ -2392,3 +2404,290 @@ free-field correction data come without an uncertainty, is not written.
- API reference: [`metrology.sound_level_meter`](https://jmrplens.github.io/phonometry/reference/api/metrology/sound-level-meter/).
---
+
+
+
+Source: https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/
+
+# Microphone calibration by comparison (IEC 61094-5 and IEC 61094-8)
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside a reference microphone whose sensitivity is already known, or in its
+place, both are exposed to the same sound pressure, and the ratio of their
+output voltages carries the reference's sensitivity over to it. IEC
+61094-5:2016 does this in a **pressure field**, in a coupler or a jig; IEC
+61094-8:2012 does it in a **free field**, in an anechoic room or behind a
+time window. Annex D of the first part writes the model once for both,
+`M_test = M_ref · R_V / R_P`, and `metrology.comparison_calibration` computes
+it in sensitivity levels, `L_test = L_ref + 20 lg R_V - 20 lg R_P + sum(C_j)`,
+with `R_V` the ratio of the output voltages, `R_P` that of the effective sound
+pressures (reduced to unity by the procedure) and `C_j` the corrections each
+part asks for. This page runs a pressure calibration of a WS2P against an
+LS2P and a free-field calibration of a WS2F on synthetic readings, so every
+step can be checked against the sensitivities that built them.
+
+
+
+## How the measurement goes
+
+With **simultaneous excitation** both microphones sit in the field at once,
+each on its own measuring channel; in a coupler or a jig they face each other
+about 1 mm apart (IEC 61094-5 5.1.2). With **sequential excitation** they take
+the same place in turn: either the exchange does not change the sound pressure
+significantly, or any change is detected and corrected, for example with a
+monitor microphone near the source (IEC 61094-5 5.1.3, IEC 61094-8 5.2, 6.5
+and A.2).
+
+
+
+| Requirement | Value | Clause |
+| :--- | :--- | :--- |
+| Reference conditions | 23,0 °C, 101,325 kPa, 50 % relative humidity | 61094-5 and 61094-8, 4 |
+| Simultaneous, in a coupler or a jig | Diaphragms about 1 mm apart; the microphones interchanged and the measurement repeated | 61094-5, 5.1.2 |
+| Sequential | The exchange does not change the sound pressure significantly, or any change is detected and corrected, for example with a monitor microphone; a monitor, when used, senses the changes at the test position | 61094-5, 5.1.3; 61094-8, 5.2, 6.5 |
+| Free-field mounting | On a semi-infinite rod of the microphone's diameter | 61094-8, 6.7 |
+| Reported uncertainty | Expanded, with `k = 2`, at each frequency | 61094-5, 7.9; 61094-8, 8.8 |
+
+## A pressure calibration by simultaneous excitation
+
+Annex C removes the gains of the two channels and the asymmetry of the
+coupler: with the reference on channel 1 the level reading difference is
+`L_C12`, after the interchange it is `L_C21`, and Formula (C.3) gives
+`L_ref - L_test = (L_C12 - L_C21) / 2`. Here the channels differ by 0,55 dB,
+port A hears up to 0,13 dB more than port B, and each reading is repeated
+three times:
+
+```python
+import numpy as np
+from phonometry import metrology
+
+f = metrology.exact_frequencies(250, 20000, fraction=3) # 20 exact one-third octaves
+x = f / 1000
+l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2 # the LS2P's certificate, dB re 1 V/Pa
+l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4 # the WS2P: unknown
+
+gain_1, gain_2 = 0.35, -0.20 # the two measuring channels, dB
+field_a = 0.03 * np.sqrt(x) # port A of the coupler hears 0.015 dB to 0.13 dB more
+rng = np.random.default_rng(61094)
+noise = rng.normal(0.0, 0.004, (2, 3, f.size)) # three repeats of each reading
+l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0] # reference on channel 1
+l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1] # interchanged
+
+c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)
+print(c.determinations, c.sensitivity_level_db[[2, 8, 19]].round(2)) # 3 [-38.63 -38.58 -38.44]
+print(np.abs(c.sensitivity_level_db - l_true).max().round(3)) # 0.003
+```
+
+A pressure calibration without the interchange is refused, since 5.1.2
+requires it. The "mean of the two ratios" 5.1.2 asks for is taken in
+decibels, as Annex C does: the geometric mean, the only one in which the gains
+cancel exactly (the arithmetic mean of the two ratios here would sit 0,018 dB
+to 0,027 dB high).
+
+IEC 61094-5 6.6 corrects the reference's sensitivity to the conditions of the
+test when the two microphones are different models, and IEC 61094-8 7.6 does
+it whatever the models. `metrology.environmental_sensitivity_correction`
+corrects a sensitivity level for the static pressure, the temperature and the
+humidity to first order, from the microphone's own coefficients in the units
+IEC 61094-2 Annex D gives them:
+
+```python
+env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2, # the laboratory during the test
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005, # the LS2P's own coefficients
+ temperature_coefficient_db_per_k=0.002,
+) # from its certificate's 101.325 kPa, 23.0 °C and 50 %
+print(env.correction_db[0].round(4)) # 0.0076
+```
+
+## The uncertainty budget of Annex D
+
+Table D.1 is a worked budget for a set-up like this one at 2 kHz, in the
+coupler of Figure A.1, which is for frequencies up to 10 kHz (A.1; above, the
+jig of A.2 reaches 20 kHz). `metrology.IEC61094_5_TABLE_D1` holds each of its
+eight rows with its printed standard uncertainty, and the stated value and
+divisor of the seven that state one; the repeatability row states no value
+and prints 0,025 dB as a standard uncertainty.
+`metrology.comparison_uncertainty_budget` combines the components on
+`metrology.combine_uncertainty` and multiplies by `k = 2`:
+
+```python
+d1 = {
+ "reference": 0.025, "capacitance": 0.006, "non_linearity": 0.017,
+ "impedance": 0.003, "polarizing_voltage": 0.005, "repeatability": 0.025,
+ "drift": 0.017, "rounding": 0.003,
+} # Table D.1, standard uncertainties at 2 kHz
+b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000)
+print(round(b.combined_uncertainty_db, 4), round(b.expanded_uncertainty_db, 3)) # 0.0437 0.087
+print(round(b.linear_combined_uncertainty_db, 4)) # 0.0436
+```
+
+D.3 prints 0,040 dB and 0,08 dB, which are not the root-sum-square of its own
+eight components (an [erratum](https://jmrplens.github.io/phonometry/reference/errata/)). The strict calculation in
+linear form D.3 mentions, and does not print, gives 0,043 614 dB; the Spanish
+UNE-EN 61094-5:2017 leaves six of the eight values of Table D.1 blank and prints
+0,004 dB in D.3. With one budget per frequency, the expanded uncertainty goes
+to the calibration, which draws it as a band:
+
+```python
+budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**d1, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2}, frequency_hz=fx
+ )
+ for fx in f
+]
+u = [bb.expanded_uncertainty_db for bb in budgets]
+c = metrology.simultaneous_comparison(
+ f, l_ref, l_c12, l_c21, reference_environment=env, expanded_uncertainty_db=u
+)
+print(np.round(u, 3)[[0, 16, 19]]) # [0.087 0.101 0.205]
+print(c.sensitivity_mv_per_pa[[2, 8, 19]].round(2)) # [11.72 11.78 11.98]
+```
+
+
+
+## A WS3 microphone in the jig
+
+Table A.1 gives the corrections to add to the sensitivity level of a WS3
+microphone calibrated against an LS2aP in the jig of Figure A.4, with an
+expanded uncertainty of a tenth of each:
+
+```python
+j = metrology.jig_diameter_correction()
+print(j.correction_db[[0, 9, 13]]) # [-0.004 -0.235 -1.443]
+print(j.standard_uncertainty_db[13].round(4)) # 0.0722
+```
+
+
+
+## A free-field calibration by substitution
+
+By IEC 61094-8 A.2 each microphone is read against the monitor, and the
+quotient of the two ratios is the output ratio corrected for any change of
+the source: `20 lg R_V = 20 lg(V_test / V_mon,2) - 20 lg(V_ref / V_mon,1)`,
+with `V_ref` and `V_mon,1` the output voltages of the reference and the
+monitor in the first reading and `V_test` and `V_mon,2` those of the second.
+The reference here is an LS2P calibrated in a pressure field, so it takes
+the free-field to pressure difference of IEC/TS 61094-7 (illustrative values
+below), and the source drifts by 0,25 dB between the readings:
+
+```python
+ff = metrology.exact_frequencies(500, 20000, fraction=3)
+xf = ff / 1000
+c_ff = 0.05 * xf**1.3 # the LS2P's free-field to pressure difference: illustrative
+l_ref_p = -38.0 + 0.04 * np.log10(xf) - 0.25 * (xf / 20) ** 2 # its pressure calibration
+l_ws2f = -38.3 + 0.1 * np.log10(xf) - 0.3 * (xf / 20) ** 3 # the WS2F: unknown
+field_1 = 74.0 + 0.05 * np.sin(xf) # the free field while the reference is in place
+field_2 = field_1 + 0.25 # the source has drifted by the time the WS2F is
+monitor = -40.0 # the monitor microphone near the source, dB re 1 V/Pa
+
+
+def free_field_budget(fx):
+ x20 = fx / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x20, # pressure calibration and IEC/TS 61094-7
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x20**2,
+ "free_field": 0.03 + 0.12 * x20**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=fx,
+ field="free_field",
+ )
+
+
+free = metrology.sequential_comparison(
+ ff,
+ l_ref_p,
+ l_ref_p + c_ff + field_1, # the reference's output, dB re 1 V
+ l_ws2f + field_2, # the WS2F's output in its place
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[free_field_budget(fx).expanded_uncertainty_db for fx in ff],
+)
+print(bool(np.abs(free.sensitivity_level_db - l_ws2f).max() < 1e-9)) # True
+print(free.expanded_uncertainty_db[[0, 12, 16]].round(2)) # [0.16 0.26 0.51]
+```
+
+Table 2 of IEC 61094-8 lists the components and prints no values; the ones
+above are illustrative. `metrology.IEC61094_8_TABLE_1` holds the typical
+expanded uncertainty of each way of calibrating the reference:
+
+```python
+row = metrology.IEC61094_8_TABLE_1["primary_pressure"]
+print(row.expanded_uncertainty_1khz_db, row.expanded_uncertainty_10khz_db) # 0.12 0.4
+```
+
+## The effective free-field region of a time window
+
+A time window of length `tau` simulates a free field inside a prolate
+spheroid with the source and the microphone at its foci, `d` apart, and the
+major diameter `A = d + tau c` (Formula (B.1)), at the speed of sound of the
+IEC 61094-2 Annex F air:
+
+```python
+r = metrology.free_field_region(1.0, 0.005) # 1 m, a 5 ms window, at 23.0 °C
+print(round(r.speed_of_sound, 1), round(r.major_axis_m, 2)) # 345.9 2.73
+print(round(r.semi_minor_axis_m, 2), round(r.rod_clearance_m, 2)) # 1.27 0.86
+```
+
+
+
+## The diffuse-field comparison of IEC 61183
+
+The diffuse-field method of IEC 61183 clause 5 is a sequential comparison
+without a monitor, and the library computes it through the same model:
+
+```python
+d = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0], [94.3, 94.6], [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+)
+s = metrology.sequential_comparison([1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6])
+print(d.diffuse_field_level_db, s.sensitivity_level_db) # [-25.7 -25.7] [-25.7 -25.7]
+```
+
+## What this guide covers
+
+Implemented: the level model of IEC 61094-5 D.2; the simultaneous excitation
+with the interchange of Annex C (Formulas (C.1) to (C.3)), required for a
+pressure calibration; the sequential excitation against a monitor microphone;
+the average over the determinations; the environmental correction to first
+order (this library's reading of 6.6 and 7.6, which print no formula); the
+reference's free-field to pressure difference in a free-field calibration;
+the WS3 corrections of Table A.1, whose 10 % expanded uncertainty, printed
+without a coverage factor, is read with the `k = 2` of 7.9 and D.2; the
+budgets of Table D.1 and IEC 61094-8 Table 2 with `k = 2`; Tables A.1, D.1,
+1 and 2 as published data; and the effective free-field region of Formula
+(B.1). Not implemented: the
+measurements themselves and the phase of the sensitivity; the corrections for
+the difference of the acoustic impedances (7.4, 7.5) and for non-uniform
+pressure beyond Table A.1 (6.5); the time-selective processing of IEC 61094-8
+Annex B and the qualification of the free field by ISO 26101; the values of
+IEC/TS 61094-7 and the reciprocity calibration of the reference, which are
+inputs; the second special case of Table D.1, a microphone calibrated as a
+system with its preamplifier (0,002 dB, and under 0,02 dB above 200 Hz),
+which a budget takes through `additional_components`.
+
+## See also
+
+- [Free-field corrections of a sound level meter (IEC 62585)](https://jmrplens.github.io/phonometry/signals/metrology/free-field-corrections/):
+ a meter compared with an LS2P reference in the same way.
+- [Random-incidence and diffuse-field response (IEC 61183)](https://jmrplens.github.io/phonometry/signals/metrology/random-incidence/):
+ the diffuse-field comparison that computes through the same model.
+- [Measurement uncertainty](https://jmrplens.github.io/phonometry/signals/metrology/gum-uncertainty/): the GUM law of propagation
+ the budgets are combined by.
+- [Errata in published sources](https://jmrplens.github.io/phonometry/reference/errata/): IEC 61094-5 D.3, UNE-EN
+ 61094-5:2017 Table D.1, IEC 61094-8 8.4 and Formula (B.10).
+- API reference: [`metrology.comparison_calibration`](https://jmrplens.github.io/phonometry/reference/api/metrology/comparison-calibration/).
+
+---
diff --git a/site/public/llms/llms-signals.txt b/site/public/llms/llms-signals.txt
index 32ed2116e..e35da37d0 100644
--- a/site/public/llms/llms-signals.txt
+++ b/site/public/llms/llms-signals.txt
@@ -182,6 +182,10 @@ What the numbers mean and how much to trust them.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](https://jmrplens.github.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's periodic-test results for a working meter,
clause by clause, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](https://jmrplens.github.io/phonometry/signals/metrology/comparison-calibration/):
+ a working standard microphone calibrated against a reference in a pressure
+ field or a free field, with the corrections and the uncertainty budget each
+ part asks for.
## What this section does not cover
diff --git a/site/public/llms/llms-start.txt b/site/public/llms/llms-start.txt
index 73710beb2..bc8735731 100644
--- a/site/public/llms/llms-start.txt
+++ b/site/public/llms/llms-start.txt
@@ -309,7 +309,7 @@ the normative PDF fiche.
- [Filter Architecture Gallery](https://jmrplens.github.io/phonometry/signals/filters/filter-gallery/): choose an architecture and inspect responses
- [Calibration and dBFS](https://jmrplens.github.io/phonometry/signals/metrology/calibration/): get real-world SPL values
- [Why phonometry](https://jmrplens.github.io/phonometry/start/why-phonometry/): the conformance-first design philosophy
-- [Conformance report](https://jmrplens.github.io/phonometry/reference/conformance/): the expected and computed value of all 1774 checks
+- [Conformance report](https://jmrplens.github.io/phonometry/reference/conformance/): the expected and computed value of all 1786 checks
- [API Reference](https://jmrplens.github.io/phonometry/reference/api/): every parameter of every function
- [Bibliography](https://jmrplens.github.io/phonometry/reference/bibliography/): the books and papers behind every guide, each with a verified link
@@ -506,7 +506,7 @@ sample from the metrology core:
| IEC 61252:1993 | The personal sound exposure quantities, `sound_exposure()` and the normalized 8 h level `lex_8h()`; IEC 61252:2025 has since superseded this print, and the transcription still targets the 1993+A2 text | `tests/signals/test_levels.py` |
The same discipline applies far beyond the metrology core: today the suite runs
-1774 numerical conformance checks across 108 domains and 516 standards, covering
+1786 numerical conformance checks across 109 domains and 518 standards, covering
psychoacoustics and speech intelligibility, room, building and materials
acoustics, human and machine vibration, environmental, aircraft, rotorcraft
and underwater noise, electroacoustics, broadcast loudness, industrial noise
diff --git a/site/src/content/docs/es/index.mdx b/site/src/content/docs/es/index.mdx
index 53158618f..268584d46 100644
--- a/site/src/content/docs/es/index.mdx
+++ b/site/src/content/docs/es/index.mdx
@@ -1,6 +1,6 @@
---
title: phonometry
-description: "Biblioteca de medición acústica para Python: niveles, psicoacústica, acústica de la edificación y acústica submarina; 1774 comprobaciones de conformidad frente a 516 normas."
+description: "Biblioteca de medición acústica para Python: niveles, psicoacústica, acústica de la edificación y acústica submarina; 1786 comprobaciones de conformidad frente a 518 normas."
# Starlight titula cada página " | ",
# que en esta produce "phonometry | phonometry" y gasta el elemento con más
# peso de la página de entrada sin decir nada. La sustitución también la
diff --git a/site/src/content/docs/es/reference/conformance.mdx b/site/src/content/docs/es/reference/conformance.mdx
index c7b60208d..b6e63b475 100644
--- a/site/src/content/docs/es/reference/conformance.mdx
+++ b/site/src/content/docs/es/reference/conformance.mdx
@@ -9,7 +9,7 @@ head:
attrs:
type: application/ld+json
content: |
- {"@context":"https://schema.org","@type":"Dataset","@id":"https://jmrplens.github.io/phonometry/es/reference/conformance/#dataset","name":"phonometry numerical conformance report","description":"1774 comprobaciones numéricas de conformidad que anclan cada métrica acústica implementada a un apartado, tabla o ejemplo resuelto de la norma que la rige, con el valor normativo esperado, el valor que calcula phonometry, la diferencia y un veredicto, en 108 dominios y 516 normas. Se regenera y se verifica en CI en cada pull request.","url":"https://jmrplens.github.io/phonometry/es/reference/conformance/","inLanguage":"es","license":"https://opensource.org/licenses/MIT","isAccessibleForFree":true,"creator":{"@id":"https://jmrp.io/#person"},"publisher":{"@id":"https://jmrp.io/#person"},"about":{"@id":"https://github.com/jmrplens/phonometry#software"},"measurementTechnique":"Comparison of computed values against the normative expected values published in the governing standards (tolerance tables and worked examples).","variableMeasured":[{"@type":"PropertyValue","name":"Standard","description":"Governing standard and clause or table"},{"@type":"PropertyValue","name":"Quantity","description":"Acoustic quantity under test"},{"@type":"PropertyValue","name":"Expected","description":"Normative value published in the standard"},{"@type":"PropertyValue","name":"Computed","description":"Value computed by phonometry"},{"@type":"PropertyValue","name":"Delta","description":"Signed difference between computed and expected"},{"@type":"PropertyValue","name":"Limit","description":"Published tolerance the deviation is judged against"},{"@type":"PropertyValue","name":"Used","description":"Fraction of the published tolerance the deviation consumes"},{"@type":"PropertyValue","name":"Status","description":"Pass or fail verdict against the standard's tolerance"}],"distribution":[{"@type":"DataDownload","encodingFormat":"application/json","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/conformance.json"},{"@type":"DataDownload","encodingFormat":"text/markdown","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/CONFORMANCE.md"}],"isBasedOn":"https://github.com/jmrplens/phonometry/blob/main/scripts/conformance_report.py","identifier":{"@type":"PropertyValue","propertyID":"DOI","value":"10.5281/zenodo.21215280"}}
+ {"@context":"https://schema.org","@type":"Dataset","@id":"https://jmrplens.github.io/phonometry/es/reference/conformance/#dataset","name":"phonometry numerical conformance report","description":"1786 comprobaciones numéricas de conformidad que anclan cada métrica acústica implementada a un apartado, tabla o ejemplo resuelto de la norma que la rige, con el valor normativo esperado, el valor que calcula phonometry, la diferencia y un veredicto, en 109 dominios y 518 normas. Se regenera y se verifica en CI en cada pull request.","url":"https://jmrplens.github.io/phonometry/es/reference/conformance/","inLanguage":"es","license":"https://opensource.org/licenses/MIT","isAccessibleForFree":true,"creator":{"@id":"https://jmrp.io/#person"},"publisher":{"@id":"https://jmrp.io/#person"},"about":{"@id":"https://github.com/jmrplens/phonometry#software"},"measurementTechnique":"Comparison of computed values against the normative expected values published in the governing standards (tolerance tables and worked examples).","variableMeasured":[{"@type":"PropertyValue","name":"Standard","description":"Governing standard and clause or table"},{"@type":"PropertyValue","name":"Quantity","description":"Acoustic quantity under test"},{"@type":"PropertyValue","name":"Expected","description":"Normative value published in the standard"},{"@type":"PropertyValue","name":"Computed","description":"Value computed by phonometry"},{"@type":"PropertyValue","name":"Delta","description":"Signed difference between computed and expected"},{"@type":"PropertyValue","name":"Limit","description":"Published tolerance the deviation is judged against"},{"@type":"PropertyValue","name":"Used","description":"Fraction of the published tolerance the deviation consumes"},{"@type":"PropertyValue","name":"Status","description":"Pass or fail verdict against the standard's tolerance"}],"distribution":[{"@type":"DataDownload","encodingFormat":"application/json","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/conformance.json"},{"@type":"DataDownload","encodingFormat":"text/markdown","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/CONFORMANCE.md"}],"isBasedOn":"https://github.com/jmrplens/phonometry/blob/main/scripts/conformance_report.py","identifier":{"@type":"PropertyValue","propertyID":"DOI","value":"10.5281/zenodo.21215280"}}
---
import Conformance from '../../../../components/Conformance.astro';
diff --git a/site/src/content/docs/es/reference/errata.md b/site/src/content/docs/es/reference/errata.md
index a4d33c857..cd18ce20b 100644
--- a/site/src/content/docs/es/reference/errata.md
+++ b/site/src/content/docs/es/reference/errata.md
@@ -2527,6 +2527,127 @@ dos ediciones con las mismas entradas y en el mismo orden.
- **Estado:** sin notificar (defecto de referencia cruzada, sin consecuencia
numérica).
+## IEC 61094-5:2016, D.3 (una incertidumbre combinada que sus propias componentes no dan)
+
+- **Ubicación:** Anexo D, apartado D.3 «Combined and expanded uncertainties»
+ (folio impreso 21), que combina las ocho componentes de la Tabla D.1 (folio
+ impreso 20).
+- **El impreso:** «The combined standard uncertainty is found from the
+ root-sum-square of the uncertainty components, which gives a value of
+ **0,040 dB** [...]. The expanded uncertainty with a coverage factor of 2 is
+ then **0,08 dB**.» La Tabla D.1 imprime las ocho incertidumbres típicas
+ 0,025 (sensibilidad del micrófono de referencia), 0,006 (capacidad), 0,017
+ (no linealidad), 0,003 (impedancia), 0,005 (tensión de polarización), 0,025
+ (repetibilidad), 0,017 (deriva desde la última calibración) y 0,003
+ (redondeo) dB, y D.2 dice que la incertidumbre «arises from eight different
+ sources».
+- **El problema:** la raíz de la suma de cuadrados de la columna impresa es
+ $\sqrt{2 \times 0{,}025^2 + 2 \times 0{,}017^2 + 0{,}006^2 + 0{,}005^2 + 2
+ \times 0{,}003^2} = \sqrt{0{,}001\,907} = 0{,}043\,67$ dB, que se lee
+ **0,044 dB**, no 0,040 dB; a partir de los valores que cada fila enuncia antes
+ de redondear (0,05/2, y 0,01, 0,03, 0,005, $20\lg(200{,}2/200)$, 0,03 y 0,005
+ entre $\sqrt{3}$) es 0,043 88 dB. Con $k = 2$ la incertidumbre expandida es
+ 0,087 dB, **0,09 dB** con los dos decimales que da el apartado, no 0,08 dB.
+ El 0,040 dB impreso es la raíz de la suma de cuadrados de siete de las ocho
+ componentes, dejando fuera una de las dos filas de 0,017 dB:
+ $\sqrt{0{,}001\,907 - 0{,}017^2} = 0{,}040\,2$ dB.
+- **Evidencia:** los ocho valores impresos recombinados, y los valores
+ enunciados divididos entre sus divisores. Verificado en las páginas 22 y 23
+ del PDF (pp. 20 y 21 impresas) de IEC 61094-5:2016, edición 2.0 (2016-05),
+ inglés-francés.
+- **Comportamiento de la biblioteca:** `metrology.comparison_uncertainty_budget`
+ combina las componentes que recibe, 0,0437 dB y 0,087 dB para las de la
+ Tabla D.1, y las filas de conformidad de D.3 fijan esos valores con los
+ impresos señalados como errata
+ ([`tests/metrology/test_comparison_calibration.py`](https://github.com/jmrplens/phonometry/blob/main/tests/metrology/test_comparison_calibration.py)).
+- **Estado:** sin notificar.
+
+## UNE-EN 61094-5:2017, Tabla D.1 y D.3 (seis valores perdidos y la incertidumbre combinada mal impresa en la traducción)
+
+- **Ubicación:** Anexo D, Tabla D.1 «Ejemplo de balance de incertidumbres»
+ (pp. 25 y 26 impresas) y D.3 «Incertidumbres combinada y expandida» (p. 27
+ impresa) de UNE-EN 61094-5 (febrero de 2017), la versión española de
+ EN 61094-5:2016, que adopta IEC 61094-5:2016.
+- **El impreso:** la columna «Incertidumbre típica dB» de la Tabla D.1 está
+ **vacía** en seis de sus ocho filas, «No linealidad», «Impedancia del
+ micrófono», «Voltaje de polarización», «Repetibilidad», «Deriva en la
+ sensibilidad del micrófono de referencia desde la última calibración» y
+ «Redondeo de los resultados presentados»; sólo las dos primeras filas
+ imprimen un valor, 0,025 y 0,006. D.3 dice «lo que da un valor de
+ **0,004 dB**». El texto de la primera fila dice «Esto es equivalente a una
+ incertidumbre típica de **0,025/2 dB** = 0,025 dB», y el primer caso
+ especial habla de «un micrófono de tipo **WG3**».
+- **El problema:** el texto inglés imprime los seis valores que la traducción
+ pierde, 0,017, 0,003, 0,005, 0,025, 0,017 y 0,003 dB (IEC 61094-5:2016,
+ p. 20 impresa), y «0,040 dB» en D.3 (p. 21 impresa), así que el 0,004 dB
+ español es la décima parte del valor inglés e incoherente con su propia
+ incertidumbre expandida de «0,08 dB» dos líneas más abajo. El texto de la
+ fila divide entre 2 los 0,05 dB, no 0,025 dB («0,05/2 dB = 0,025 dB» en el
+ inglés), y el caso especial es un micrófono de tipo **WS3**, como dicen el
+ inglés, la Tabla A.1 de la misma traducción y el último párrafo del mismo
+ caso especial. Quien lea sólo el texto español no puede reconstruir el
+ balance: faltan seis de sus ocho componentes, y el valor combinado que
+ enuncia no es la suma de nada impreso. El valor combinado inglés es a su vez
+ una errata (la entrada sobre IEC 61094-5:2016 D.3, más arriba).
+- **Evidencia:** los dos impresos leídos en paralelo. Verificado en las
+ páginas 25 a 27 del PDF (pp. 25 a 27 impresas) de UNE-EN 61094-5:2017 y en
+ las páginas 22 y 23 del PDF (pp. 20 y 21 impresas) de IEC 61094-5:2016.
+- **Comportamiento de la biblioteca:** implementa el texto inglés, cuya Tabla
+ D.1 leen los tests y las filas de conformidad; no hizo falta ningún cambio.
+- **Estado:** sin notificar (traducción nacional, no el texto del organismo
+ emisor).
+
+## IEC 61094-8:2012, 8.4 (una referencia cruzada sin resolver)
+
+- **Ubicación:** subapartado 8.4 «Differences between the sound pressure
+ applied to the reference microphone and to the microphone under test»
+ (folio impreso 15), su primera frase.
+- **El impreso:** «As stated in **Error! Reference source not found.** the
+ basis of a comparison method is that the test and reference microphones are
+ exposed to a sound field having the same modulus, phase and angle of
+ incidence.»
+- **El problema:** en lugar de la referencia cruzada se imprimió un campo sin
+ resolver del procesador de textos, así que la frase no remite a nada. Lo que
+ parafrasea es el principio general de 5.1: «When a calibrated reference
+ microphone and a microphone under test are exposed to the same free-field
+ sound pressure [...]» (folio impreso 8).
+- **Evidencia:** la frase tal como se imprime, leída contra 5.1. Verificado en
+ la página 17 del PDF (p. 15 impresa) de BS EN 61094-8:2012, el texto inglés
+ de EN 61094-8:2012, que es IEC 61094-8:2012 sin cambios.
+- **Comportamiento de la biblioteca:** nada del texto depende de la
+ referencia; no hizo falta ningún cambio.
+- **Estado:** sin notificar (defecto de referencia cruzada, sin consecuencia
+ numérica).
+
+## IEC 61094-8:2012, B.6.1, Fórmula (B.10) (el espectro de un pulso de duración 2b llamado de duración b)
+
+- **Ubicación:** Anexo B, B.6.1 «Outline of methods» del método de excitación
+ por impulso directo (folio impreso 28), la Fórmula (B.10) y las frases que
+ la rodean.
+- **El impreso:** «The Fourier transform, $X(f)$, of a rectangular pulse of
+ **duration $b$** and amplitude $a$ is $X(f) = \dfrac{2ab\sin(2\pi f b)}{2\pi
+ f b}$ (B.10). The first zero in the spectrum is at $f = 1/(2b)$. [...]
+ leading to a requirement for the duration, $b$ of just a few microseconds.»
+- **El problema:** la Fórmula (B.10) es la transformada de un pulso de
+ amplitud $a$ que dura de $-b$ a $b$, es decir, de duración **$2b$**: su valor
+ en $f = 0$ es el área del pulso, $2ab$, y su primer cero, donde $2\pi f b =
+ \pi$, está en $1/(2b)$, como dice el texto. Un pulso de duración $b$ tiene la
+ transformada $ab \sin(\pi f b)/(\pi f b)$ y su primer cero en $1/b$. La
+ fórmula y el primer cero concuerdan entre sí; las palabras «duration $b$»
+ discrepan de ambos, y han de leerse «semiduración $b$» o «duración $2b$». La
+ conclusión práctica se mantiene con cualquiera de las dos lecturas: un primer
+ cero diez veces por encima de un límite superior de 20 kHz, en 200 kHz, pide
+ un pulso de 5 µs ($b$ = 2,5 µs según la fórmula), «just a few
+ microseconds».
+- **Evidencia:** la transformada del pulso rectangular evaluada en frecuencia
+ cero y en su primer cero. Verificado en la página 30 del PDF (p. 28 impresa)
+ de BS EN 61094-8:2012, el texto inglés de EN 61094-8:2012, que es
+ IEC 61094-8:2012 sin cambios.
+- **Comportamiento de la biblioteca:** el método de impulso directo, que B.6.2
+ da por «largely superseded», no está implementado; no hizo falta ningún
+ cambio.
+- **Estado:** sin notificar.
+
## UNE-EN ISO 9614-1:2010, apartado 9.1 (el signo perdido de «signed magnitude» en la traducción)
- **Ubicación:** apartado 9.1, la lista de símbolos bajo la Fórmula (11)
diff --git a/site/src/content/docs/es/signals/index.mdx b/site/src/content/docs/es/signals/index.mdx
index 83c81a623..7b9646db8 100644
--- a/site/src/content/docs/es/signals/index.mdx
+++ b/site/src/content/docs/es/signals/index.mdx
@@ -193,6 +193,10 @@ Qué significan los números y cuánto fiarse de ellos.
- [Ensayos periódicos de un sonómetro (IEC 61672-3)](/phonometry/es/signals/metrology/sound-level-meter-periodic-tests/):
el dictamen sobre los resultados de los ensayos periódicos de un sonómetro
en servicio, apartado por apartado, y la declaración del apartado 22.
+- [Calibrar un micrófono por comparación (IEC 61094-5/-8)](/phonometry/es/signals/metrology/comparison-calibration/):
+ un micrófono patrón de trabajo calibrado frente a una referencia en campo de
+ presión o en campo libre, con las correcciones y el balance de
+ incertidumbre que pide cada parte.
## Qué no cubre esta sección
diff --git a/site/src/content/docs/es/signals/metrology/comparison-calibration.mdx b/site/src/content/docs/es/signals/metrology/comparison-calibration.mdx
new file mode 100644
index 000000000..e109a8e73
--- /dev/null
+++ b/site/src/content/docs/es/signals/metrology/comparison-calibration.mdx
@@ -0,0 +1,533 @@
+---
+title: "Calibrar un micrófono por comparación (IEC 61094-5/-8)"
+description: "Cómo la IEC 61094-5:2016 y la IEC 61094-8:2012 trasladan la sensibilidad de un micrófono de referencia a un micrófono patrón de trabajo: el intercambio del Anexo C en un acoplador, los cocientes frente al micrófono monitor de una sustitución en campo libre, las correcciones ambiental, del soporte y de campo libre, los balances de incertidumbre de la Tabla D.1 y de la Tabla 2 con k = 2, y la región de campo libre efectiva de una ventana temporal."
+references:
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2016
+ title: "Electroacoustics – Measurement microphones – Part 5: Methods for pressure calibration of working standard microphones by comparison"
+ designation: "IEC 61094-5:2016"
+ primary: true
+ note: "Los métodos implementados: el modelo en niveles del D.2, la excitación simultánea con el intercambio del apartado 5.1.2 y del Anexo C (Fórmulas (C.1) a (C.3)), la excitación secuencial con micrófono monitor del apartado 5.1.3 y del Anexo B, la corrección ambiental del apartado 6.6, las correcciones del WS3 de la Tabla A.1 y el balance de ejemplo de la Tabla D.1 combinado como pide el D.3. Leída en la edición 2.0 (2016-05), inglés y francés."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2012
+ title: "Electroacoustics – Measurement microphones – Part 8: Methods for determining the free-field sensitivity of working standard microphones by comparison"
+ designation: "IEC 61094-8:2012"
+ primary: true
+ note: "La parte de campo libre: la sustitución secuencial con micrófono monitor de los apartados 5.2 y A.2, la excitación simultánea del apartado 5.3, las opciones de calibración de la referencia de la Tabla 1, las componentes de incertidumbre de la Tabla 2 con la incertidumbre expandida del apartado 8.8, y la región de campo libre efectiva de la Fórmula (B.1). Leída en BS EN 61094-8:2012, el texto inglés de EN 61094-8:2012, que es IEC 61094-8:2012 sin cambios."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2009
+ title: "Electroacoustics – Measurement microphones – Part 2: Primary method for pressure calibration of laboratory standard microphones by the reciprocity technique"
+ designation: "IEC 61094-2:2009"
+ note: "El Anexo D da los coeficientes de presión estática y de temperatura de los micrófonos patrón de laboratorio en dB/kPa y dB/K, las unidades en que los toma la corrección ambiental; el Anexo F es el aire húmedo cuya velocidad del sonido fija la región de campo libre de una ventana temporal."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2006
+ title: "Measurement microphones – Part 7: Values for the difference between free-field and pressure sensitivity levels of laboratory standard microphones"
+ designation: "IEC/TS 61094-7:2006"
+ note: "La diferencia entre los niveles de sensibilidad en campo libre y en presión que toma una referencia calibrada en presión en una calibración en campo libre (Tabla 1 y apartado 8.2 de la IEC 61094-8). Los valores de esta página son ilustrativos, no están leídos de ella."
+ - type: standard
+ organization: "Joint Committee for Guides in Metrology"
+ year: 2008
+ title: "Evaluation of measurement data – Guide to the expression of uncertainty in measurement"
+ designation: "JCGM 100:2008, the GUM"
+ publisher: "BIPM"
+ url: "https://www.bipm.org/documents/20126/2071204/JCGM_100_2008_E.pdf"
+ doi: "10.59161/JCGM100-2008E"
+ note: "La ley de propagación de la incertidumbre con que se combinan los dos balances, y el factor de cobertura k = 2 con que informan las dos partes. Publicada también como ISO/IEC Guide 98-3:2008."
+---
+
+import Scope from '../../../../../components/Scope.astro';
+import ScopeClaim from '../../../../../components/ScopeClaim.astro';
+import SeeAlso from '../../../../../components/SeeAlso.astro';
+import ThemeImage from '../../../../../components/ThemeImage.astro';
+
+Un micrófono patrón de trabajo no se calibra por reciprocidad. Se coloca junto
+a un micrófono de referencia cuya sensibilidad ya se conoce, o en su lugar,
+los dos se exponen a la misma presión acústica, y el cociente de sus
+tensiones de salida traslada a él la sensibilidad de la referencia. La
+IEC 61094-5 lo hace en un **campo de presión**, en un acoplador o en un
+soporte posicionador; la IEC 61094-8 lo hace en un **campo libre**, en una
+cámara anecoica o tras una ventana temporal. El Anexo D de la primera parte
+escribe el modelo una sola vez para las dos,
+
+$$
+M_\mathrm{test} = M_\mathrm{ref}\,\frac{R_V}{R_P},
+\qquad
+L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+$$
+
+donde $R_V$ es el cociente de las tensiones de salida del micrófono en ensayo
+y del de referencia, $R_P$ el de las presiones acústicas efectivas que actúan
+sobre ellos, que el procedimiento reduce a la unidad, y $C_j$ las
+correcciones que pide cada parte. `metrology.comparison_calibration` calcula la
+forma de la derecha, en niveles de sensibilidad en dB re 1 V/Pa.
+
+Esta página recorre una calibración en presión de un WS2P frente a un LS2P y
+una calibración en campo libre de un WS2F, con sus balances; las lecturas
+son sintéticas, construidas a partir de sensibilidades que la página conoce,
+así que cada paso se puede comprobar frente a ellas.
+
+
+
+*Una calibración en presión por excitación simultánea (izquierda) y una
+calibración en campo libre por sustitución (derecha), con el propio `.plot()`
+de los resultados.*
+
+## 1. Cómo se hace la medida
+
+Las dos partes dan dos maneras de exponer los micrófonos a la misma presión
+acústica. Con **excitación simultánea** están en el campo a la vez, cada uno
+leído en su propio canal de medida; en un acoplador o en un soporte
+posicionador quedan enfrentados a aproximadamente 1 mm, una fracción pequeña
+de la longitud de onda hasta 20 kHz (apartado 5.1.2). Con **excitación
+secuencial** ocupan el mismo sitio por turnos: o el cambio de micrófono no
+altera apreciablemente la presión acústica, o cualquier cambio se detecta y
+se corrige, por ejemplo con un micrófono monitor cerca de la fuente
+(apartado 5.1.3; apartados 5.2, 6.5 y A.2 de la IEC 61094-8).
+
+
+
+| Requisito | Valor | Apartado |
+| :--- | :--- | :--- |
+| Condiciones de referencia | 23,0 °C, 101,325 kPa, 50 % de humedad relativa | 61094-5 y 61094-8, 4 |
+| Simultánea, en acoplador o soporte | Diafragmas a aproximadamente 1 mm; los micrófonos se intercambian y la medición se repite, y el resultado sale de las dos | 61094-5, 5.1.2 |
+| Secuencial | El cambio de micrófono no altera apreciablemente la presión acústica, o cualquier cambio se detecta y se corrige, por ejemplo con un micrófono monitor; el monitor, cuando se usa, detecta los cambios en la posición de ensayo | 61094-5, 5.1.3; 61094-8, 5.2, 6.5 |
+| Ancho de banda de análisis | 1/6 de octava o más estrecho mantiene el error por debajo de 0,01 dB; líneas FFT mucho más estrechas pueden mostrar ondas estacionarias | 61094-5, NOTA de 5.1.1 |
+| Tensión de salida | En circuito abierto, por tensión de inserción o con un preamplificador de alta impedancia; el método se declara en el certificado | 61094-5, 5.2 |
+| Fuente en campo libre | De 70 dB a 80 dB en el micrófono suelen bastar; ondas planas progresivas sobre el micrófono | 61094-8, 6.3 |
+| Montaje en campo libre | Sobre una varilla semiinfinita del diámetro del micrófono; referencias LS1P sin su rejilla | 61094-8, 6.4, 6.7 |
+| Incertidumbre declarada | Expandida, con $k = 2$, a cada frecuencia | 61094-5, 7.9; 61094-8, 8.8 |
+
+## 2. Una calibración en presión por excitación simultánea
+
+En un acoplador los dos micrófonos se leen en dos canales cuyas ganancias
+nunca son exactamente iguales, y los dos puertos de un acoplador nunca ven
+del todo la misma presión. El Anexo C elimina las dos cosas. Con la
+referencia como micrófono 1 en el canal 1, la diferencia de lectura entre
+canales es la Fórmula (C.1); tras el intercambio es la Fórmula (C.2); y su
+diferencia deja solo los dos niveles de sensibilidad,
+
+$$
+L_\mathrm{C12} = (L_1 + L_\mathrm{m1} + L_\mathrm{d1} + L_\mathrm{WA})
+- (L_2 + L_\mathrm{m2} + L_\mathrm{d1} + L_\mathrm{WB}),
+\qquad
+L_1 - L_2 = \tfrac12\,(L_\mathrm{C12} - L_\mathrm{C21})
+\tag{C.3}
+$$
+
+así que $20\lg R_V = -\tfrac12(L_\mathrm{C12} - L_\mathrm{C21})$. Aquí los
+canales difieren en 0,55 dB, el puerto A oye hasta 0,13 dB más que el B, y
+cada lectura se repite tres veces con algo de ruido:
+
+```python
+import numpy as np
+from phonometry import metrology
+
+f = metrology.exact_frequencies(250, 20000, fraction=3) # 20 tercios de octava exactos
+x = f / 1000
+l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2 # el certificado del LS2P, dB re 1 V/Pa
+l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4 # el WS2P: desconocido
+
+gain_1, gain_2 = 0.35, -0.20 # los dos canales de medida, dB
+field_a = 0.03 * np.sqrt(x) # el puerto A del acoplador oye de 0.015 dB a 0.13 dB más
+rng = np.random.default_rng(61094)
+noise = rng.normal(0.0, 0.004, (2, 3, f.size)) # tres repeticiones de cada lectura
+l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0] # referencia en el canal 1
+l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1] # intercambiados
+
+c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)
+print(c.determinations, c.sensitivity_level_db[[2, 8, 19]].round(2)) # 3 [-38.63 -38.58 -38.44]
+print(np.abs(c.sensitivity_level_db - l_true).max().round(3)) # 0.003
+```
+
+Ni las ganancias ni la asimetría llegan al resultado; lo que queda, 0,003 dB
+como mucho, es el ruido de tres repeticiones. El apartado 5.1.2 hace del
+intercambio un requisito en un acoplador o un soporte, así que una
+calibración en presión sin `interchanged_channel_difference_db` se rechaza.
+El texto pide «the mean of the two ratios», y el Anexo C la toma en
+decibelios: la media de las dos diferencias de nivel, que es la media
+geométrica de los cocientes y la única en la que las ganancias se anulan
+exactamente. Los dos cocientes leídos antes y después del intercambio
+distan aquí de 1,1 dB a 1,4 dB, el doble de la diferencia entre canales y
+entre puertos, y su media aritmética quedaría de 0,018 dB a 0,027 dB alta.
+
+**El ambiente.** La sensibilidad de un micrófono depende de la presión
+estática, de la temperatura y, en algunos micrófonos, de la humedad. Cuando
+la referencia y el micrófono en ensayo son modelos distintos, la sensibilidad
+de la referencia se tiene que corregir a las condiciones del ensayo
+(apartado 6.6); en campo libre, la IEC 61094-8 pide esa corrección sean
+cuales sean los modelos (apartado 7.6). Ninguna de las dos partes imprime una
+fórmula.
+`metrology.environmental_sensitivity_correction` toma la de primer orden, un
+coeficiente por una desviación para cada condición, con los coeficientes en
+las unidades en que los da el Anexo D de la IEC 61094-2:
+
+$$
+C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0)
++ \delta_H\,(H - H_0)
+$$
+
+Los coeficientes son los del propio micrófono, y por eso son obligatorios: el
+Anexo D de la IEC 61094-2 sitúa el coeficiente de presión estática a baja
+frecuencia de los micrófonos LS2P entre −0,003 dB/kPa y −0,008 dB/kPa y el de
+temperatura dentro de ±0,005 dB/K, y los dos varían con la frecuencia y de un
+micrófono a otro. El apartado 6.5.3 de esa misma parte no observa influencia
+de la humedad en un micrófono patrón de laboratorio, así que ese coeficiente
+vale 0 por defecto.
+
+```python
+env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2, # el laboratorio durante el ensayo
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005, # los coeficientes propios del LS2P
+ temperature_coefficient_db_per_k=0.002,
+) # desde los 101.325 kPa, 23.0 °C y 50 % de su certificado
+print(env.correction_db[0].round(4)) # 0.0076
+```
+
+Pasada como `reference_environment=`, la corrección se suma al nivel de la
+referencia; pasada como `test_environment=`, la corrección propia del
+micrófono en ensayo se resta del resultado, lo que lo refiere a las
+condiciones de referencia del apartado 4 cuando se conocen coeficientes
+fiables.
+
+## 3. El balance de incertidumbre del Anexo D
+
+La Tabla D.1 es un balance resuelto para un montaje como este, un WS2P de
+baja sensibilidad frente a un LS2P en el acoplador de la Figura A.1 con tres
+repeticiones, a 2 kHz. Ese acoplador sirve para frecuencias de hasta 10 kHz
+(apartado A.1); por encima, un WS2 va en el soporte del apartado A.2 y de la
+Figura A.3, que llega a 20 kHz. Siete de las ocho filas declaran un valor y
+cómo se lee: el certificado de la referencia da ±0,05 dB con $k = 2$; la
+capacidad, la no linealidad, la impedancia, la deriva y el redondeo son
+semiamplitudes de distribuciones rectangulares; y la tensión de polarización
+de (200,0 ± 0,2) V da una semiamplitud de
+$20\lg(200{,}2/200) = 0{,}008\,68$ dB. La fila de la repetibilidad no
+declara ningún valor, solo que se obtuvo de las incertidumbres típicas de un
+gran número de mediciones similares, e imprime 0,025 dB como incertidumbre
+típica. `metrology.IEC61094_5_TABLE_D1` guarda cada fila con su valor
+declarado, su divisor y la incertidumbre típica que imprime, y las siete que
+declaran un valor lo reproducen.
+`metrology.comparison_uncertainty_budget` las combina con
+`metrology.combine_uncertainty`, cada componente con un coeficiente de
+sensibilidad de 1 en el modelo en niveles, y multiplica por $k = 2$:
+
+```python
+d1 = {
+ "reference": 0.025, "capacitance": 0.006, "non_linearity": 0.017,
+ "impedance": 0.003, "polarizing_voltage": 0.005, "repeatability": 0.025,
+ "drift": 0.017, "rounding": 0.003,
+} # Tabla D.1, incertidumbres típicas a 2 kHz
+b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000)
+print(round(b.combined_uncertainty_db, 4), round(b.expanded_uncertainty_db, 3)) # 0.0437 0.087
+print(round(b.linear_combined_uncertainty_db, 4)) # 0.0436
+```
+
+La raíz de la suma de cuadrados de los ocho valores impresos es 0,0437 dB, y
+la incertidumbre expandida 0,087 dB. El D.3 imprime **0,040 dB** y
+**0,08 dB**, que no son la combinación de sus propias componentes: 0,040 dB es
+lo que da la suma dejando fuera una de las dos filas de 0,017 dB. El caso está
+en el [registro de erratas](/phonometry/es/reference/errata/). El D.3 advierte
+también que un cálculo estricto pasaría cada componente a forma lineal antes
+de combinarlas, con un resultado esencialmente igual, y no imprime su valor;
+`linear_combined_uncertainty_db` lo hace y da 0,043 614 dB, 0,000 055 dB por
+debajo de la combinación en decibelios. La
+adopción española, UNE-EN 61094-5:2017, deja en blanco seis de los ocho
+valores de la Tabla D.1 e imprime 0,004 dB en el D.3, lo que también está en
+el registro.
+
+A otras frecuencias las componentes cambian. El apartado 7.9 estima
+aproximadamente 0,1 dB a frecuencias bajas y medias para micrófonos del mismo
+diámetro, que sube a unos 0,2 dB a 20 kHz para los WS2P, y los apartados 7.4
+y 7.5 señalan la diferencia entre las impedancias acústicas de los
+micrófonos como lo que crece. Aquí esa componente sube con la frecuencia, y
+la incertidumbre expandida de cada balance pasa a la calibración, que el
+gráfico dibuja como una banda:
+
+```python
+budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**d1, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2}, frequency_hz=fx
+ )
+ for fx in f
+]
+u = [bb.expanded_uncertainty_db for bb in budgets]
+c = metrology.simultaneous_comparison(
+ f, l_ref, l_c12, l_c21, reference_environment=env, expanded_uncertainty_db=u
+)
+print(np.round(u, 3)[[0, 16, 19]]) # [0.087 0.101 0.205]
+print(c.sensitivity_mv_per_pa[[2, 8, 19]].round(2)) # [11.72 11.78 11.98]
+c.plot(language="es")
+```
+
+
+
+*El balance de la Tabla D.1 a 2 kHz (izquierda) y un balance en campo
+libre a 8 kHz (derecha).*
+
+## 4. Un micrófono WS3 en el soporte
+
+Un micrófono WS3 es más pequeño que la referencia LS2, y en el soporte de la
+Figura A.4 los dos diafragmas quedan enfrentados a 0,5 mm: la presión sobre
+el diafragma pequeño no es la media sobre el grande. La Tabla A.1 da las
+correcciones que se suman al nivel de sensibilidad del WS3, calculadas para
+una referencia LS2aP y un campo con simetría radial y válidas solo para esa
+separación, con una incertidumbre expandida de la décima parte de cada
+corrección. `metrology.jig_diameter_correction` las lee, y una frecuencia
+exacta en base diez se lee como su fila:
+
+```python
+j = metrology.jig_diameter_correction()
+print(j.correction_db[[0, 9, 13]]) # [-0.004 -0.235 -1.443]
+print(j.standard_uncertainty_db[13].round(4)) # 0.0722
+```
+
+`j.correction_db` llega a una calibración por `corrections_db=`, con un nombre
+propio, y `j.standard_uncertainty_db` a su balance como componente
+adicional, el primer caso especial de la Tabla D.1.
+
+
+
+*La Tabla A.1 con su incertidumbre expandida del 10 %.*
+
+## 5. Una calibración en campo libre por sustitución
+
+El apartado A.2 de la IEC 61094-8 describe el método básico: la referencia se
+coloca en un punto del campo libre, después el micrófono en ensayo en su
+lugar, cada uno leído frente a un micrófono monitor cerca de la fuente, y «the
+quotient of these two gives the ratio of the microphone under test output
+voltage to the reference microphone output voltage, corrected for any
+variation in the sound pressure generated by the source». Con
+$V_\mathrm{ref}$ y $V_\mathrm{mon,1}$ las tensiones de salida de la
+referencia y del monitor en la primera lectura, y $V_\mathrm{test}$ y
+$V_\mathrm{mon,2}$ las de la segunda,
+
+$$
+20\lg R_V = 20\lg\frac{V_\mathrm{test}}{V_\mathrm{mon,2}} - 20\lg\frac{V_\mathrm{ref}}{V_\mathrm{mon,1}}
+$$
+
+Aquí la referencia es un LS2P calibrado en campo de presión, así que su
+sensibilidad en campo libre es su sensibilidad en presión más la diferencia
+entre campo libre y presión de la IEC/TS 61094-7 (Tabla 1, apartado 8.2).
+Los valores de abajo ocupan el lugar de esa diferencia; tome los suyos de la
+especificación o de la calibración del micrófono. Entre las dos lecturas la
+fuente deriva 0,25 dB:
+
+```python
+ff = metrology.exact_frequencies(500, 20000, fraction=3)
+xf = ff / 1000
+c_ff = 0.05 * xf**1.3 # la diferencia de campo libre a presión del LS2P: ilustrativa
+l_ref_p = -38.0 + 0.04 * np.log10(xf) - 0.25 * (xf / 20) ** 2 # su calibración en presión
+l_ws2f = -38.3 + 0.1 * np.log10(xf) - 0.3 * (xf / 20) ** 3 # el WS2F: desconocido
+field_1 = 74.0 + 0.05 * np.sin(xf) # el campo libre mientras está la referencia
+field_2 = field_1 + 0.25 # la fuente ha derivado cuando llega el WS2F
+monitor = -40.0 # el micrófono monitor junto a la fuente, dB re 1 V/Pa
+
+
+def free_field_budget(fx):
+ x20 = fx / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x20, # calibración en presión e IEC/TS 61094-7
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x20**2,
+ "free_field": 0.03 + 0.12 * x20**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=fx,
+ field="free_field",
+ )
+
+
+free = metrology.sequential_comparison(
+ ff,
+ l_ref_p,
+ l_ref_p + c_ff + field_1, # la salida de la referencia, dB re 1 V
+ l_ws2f + field_2, # la salida del WS2F en su lugar
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[free_field_budget(fx).expanded_uncertainty_db for fx in ff],
+)
+print(bool(np.abs(free.sensitivity_level_db - l_ws2f).max() < 1e-9)) # True
+print(free.expanded_uncertainty_db[[0, 12, 16]].round(2)) # [0.16 0.26 0.51]
+```
+
+El monitor elimina la deriva exactamente; sin él el resultado saldría
+0,25 dB alto a todas las frecuencias. La Tabla 2 de la IEC 61094-8 enumera
+las componentes típicas con el apartado que trata cada una y no imprime
+valores: las de arriba son ilustrativas, elegidas para que la incertidumbre
+expandida quede cerca de los 0,2 dB que estima el apartado 8.8 a frecuencias
+bajas y medias y de los 0,5 dB que estima en el límite superior de
+frecuencia de una referencia LS2. `metrology.IEC61094_8_TABLE_2` guarda la
+lista, y `metrology.IEC61094_8_TABLE_1` la incertidumbre expandida típica de
+cada manera de calibrar la referencia:
+
+```python
+row = metrology.IEC61094_8_TABLE_1["primary_pressure"]
+print(row.expanded_uncertainty_1khz_db, row.expanded_uncertainty_10khz_db) # 0.12 0.4
+```
+
+## 6. La región de campo libre efectiva de una ventana temporal
+
+Donde la sala no es suficientemente anecoica, una ventana temporal puede
+quedarse con el sonido directo y descartar las reflexiones (apartado 6.2.3,
+Anexo B). La ventana define la región en la que simula un campo libre: un
+esferoide alargado con la fuente y el micrófono en sus focos, a una distancia
+$d$, y el diámetro mayor
+
+$$
+A = d + \tau c
+\tag{B.1}
+$$
+
+donde $\tau$ va desde la llegada del sonido al micrófono hasta el final de la
+ventana y $c$ es la velocidad del sonido en las condiciones del ensayo, aquí
+la del aire húmedo del Anexo F de la IEC 61094-2. Una reflexión desde
+cualquier punto de su superficie llega $\tau$ después del sonido directo, así
+que toda superficie reflectante tiene que quedar fuera, y también el extremo
+lejano de la varilla de montaje:
+
+```python
+r = metrology.free_field_region(1.0, 0.005) # 1 m, una ventana de 5 ms, a 23.0 °C
+print(round(r.speed_of_sound, 1), round(r.major_axis_m, 2)) # 345.9 2.73
+print(round(r.semi_minor_axis_m, 2), round(r.rod_clearance_m, 2)) # 1.27 0.86
+```
+
+Una ventana de 5 ms a 1 m necesita 1,27 m de holgura alrededor del eje a mitad
+de camino entre fuente y micrófono, y una varilla que se prolongue más de
+0,86 m por detrás del micrófono.
+
+
+
+*La región de una ventana de 5 ms a 1 m, con la varilla que tiene que salir de
+ella.*
+
+## 7. La comparación en campo difuso de la IEC 61183
+
+El método en campo difuso del apartado 5 de la IEC 61183 es una comparación
+secuencial sin monitor: un sonómetro y un sonómetro de referencia leen por
+turnos en las mismas posiciones de una cámara reverberante, y la diferencia
+de sus lecturas se suma al nivel de sensibilidad en campo difuso de la
+referencia. La biblioteca lo calcula con el mismo modelo, así que los dos no
+pueden separarse:
+
+```python
+d = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0], [94.3, 94.6], [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+)
+s = metrology.sequential_comparison([1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6])
+print(d.diffuse_field_level_db, s.sensitivity_level_db) # [-25.7 -25.7] [-25.7 -25.7]
+```
+
+
+Mostrar el código de estas figuras
+
+```python
+import matplotlib.pyplot as plt
+
+fig, (ax_pressure, ax_free) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+c.plot(ax_pressure, language="es")
+free.plot(ax_free, language="es")
+fig.tight_layout()
+
+fig, (ax_d1, ax_ff) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+b.plot(ax_d1, language="es")
+free_field_budget(8000).plot(ax_ff, language="es")
+fig.tight_layout()
+
+fig, ax = plt.subplots(figsize=(10, 6))
+j.plot(ax, language="es")
+fig.tight_layout()
+
+fig, ax = plt.subplots(figsize=(10, 6))
+r.plot(ax, language="es")
+fig.tight_layout()
+plt.show()
+```
+
+
+
+## Qué cubre esta guía
+
+
+
+**IEC 61094-5:2016 e IEC 61094-8:2012, los cálculos:** el modelo en niveles
+del D.2; la excitación simultánea con el intercambio del Anexo C (Fórmulas
+(C.1) a (C.3)), obligatoria en una calibración en presión, y sin él en una en
+campo libre (apartado 5.3); la excitación secuencial frente a un micrófono
+monitor (5.1.3 de la 61094-5, A.2 de la 61094-8); la media entre las
+determinaciones; la diferencia de campo libre a presión de la referencia en
+una calibración en campo libre (Tabla 1, apartado 8.2); las correcciones del
+WS3 de la Tabla A.1 con su incertidumbre; los balances de la Tabla D.1 y
+de la Tabla 2 sobre la GUM con $k = 2$ (7.9, D.2, D.3, 8.8), y la combinación
+en forma lineal que menciona el D.3; las Tablas A.1, D.1, 1 y 2 como datos
+publicados; y la región de campo libre efectiva de la Fórmula (B.1), con la
+velocidad del sonido del Anexo F de la IEC 61094-2. La comparación del
+apartado 5 de la IEC 61183 se calcula con el mismo modelo. La Tabla D.1 y la
+Tabla A.1 se reproducen.
+
+
+
+**La corrección ambiental.** Los apartados 6.6 y 7.6 piden corregir la
+referencia a las condiciones del ensayo, o el resultado a las condiciones de
+referencia, y no imprimen ninguna fórmula; la suma de primer orden de un
+coeficiente por una desviación para cada condición es la lectura de esta
+biblioteca, con los coeficientes propios del micrófono. **La media de los dos
+cocientes** del apartado 5.1.2 se toma en decibelios, como hace el Anexo C.
+**El factor de cobertura de las correcciones del WS3.** El apartado A.2, la
+NOTA de la Tabla A.1 y el caso especial de la Tabla D.1 dan su incertidumbre
+expandida como el 10 % de cada corrección sin factor de cobertura; se lee con
+el $k = 2$ con que los apartados 7.9 y D.2 informan de toda incertidumbre
+expandida, así que la incertidumbre típica es el 5 % de la corrección.
+
+
+
+Las mediciones en sí, y la fase de la sensibilidad, que admiten el apartado
+5.1.1 de la IEC 61094-5 y el 5.1 de la IEC 61094-8. Las correcciones por la diferencia entre las
+impedancias acústicas de los micrófonos (7.4, 7.5) y por una presión no
+uniforme sobre diafragmas de distinto diámetro fuera de la Tabla A.1 (6.5),
+que las normas dejan a modelos de la bibliografía; la validación de un
+soporte o de un acoplador frente a otros montajes (6.7). El procesado con
+ventana temporal del Anexo B (seno por pasos, barridos, ruido, MLS, impulsos
+directos) y la cualificación del campo libre según la ISO 26101. Los valores
+de la IEC/TS 61094-7 y la calibración por reciprocidad de la referencia
+(IEC 61094-2, -3), que aquí son entradas. El segundo caso especial de la
+Tabla D.1, un micrófono calibrado como sistema con su preamplificador
+(0,002 dB, y para la tensión de polarización y la capacidad, dependiente de
+la frecuencia y menos de 0,02 dB por encima de 200 Hz), no se guarda como
+dato; un balance que lo
+necesite lo toma por `additional_components`.
+
+
+
+
+
+## Véase también
+
+- [Correcciones de campo libre de un sonómetro](/phonometry/es/signals/metrology/free-field-corrections/):
+ la IEC 62585, que compara un sonómetro con una referencia LS2P de la misma
+ manera.
+- [Incidencia aleatoria y campo difuso](/phonometry/es/signals/metrology/random-incidence/):
+ la IEC 61183, cuyo método en campo difuso es una comparación con el mismo
+ modelo.
+- [Incertidumbre de medida](/phonometry/es/signals/metrology/gum-uncertainty/):
+ la ley de propagación de la GUM con que se combinan los balances.
+- [Aire húmedo](/phonometry/es/fluids/humid-air/): el aire del Anexo F de la
+ IEC 61094-2 cuya velocidad del sonido fija la región de campo libre.
+- [Calibración y dBFS](/phonometry/es/signals/metrology/calibration/): el tono
+ del calibrador que convierte una grabación en pascales una vez que se conoce
+ la sensibilidad del micrófono.
+- Referencia de la API: [`metrology.comparison_calibration`](/phonometry/es/reference/api/metrology/comparison-calibration/).
+
+
diff --git a/site/src/content/docs/es/signals/metrology/index.mdx b/site/src/content/docs/es/signals/metrology/index.mdx
index 2a19b9636..7a7f547f6 100644
--- a/site/src/content/docs/es/signals/metrology/index.mdx
+++ b/site/src/content/docs/es/signals/metrology/index.mdx
@@ -86,6 +86,14 @@ estabilidad a niveles elevados, frente a los límites de aceptación de la
IEC 61672-1 y los máximos de su Tabla B.1, lo que contiene un ensayo completo
y la declaración que el apartado 22 prescribe para el resultado.
+[La calibración por comparación](/phonometry/es/signals/metrology/comparison-calibration/)
+sube un escalón en la cadena, hasta el propio micrófono: un micrófono patrón
+de trabajo toma la sensibilidad de una referencia a través del cociente de sus
+tensiones de salida, en un acoplador según la IEC 61094-5 o en campo libre
+según la IEC 61094-8, con las correcciones ambiental, del soporte y de campo
+libre que pide cada parte y el balance de incertidumbre de la Tabla D.1 o
+de la Tabla 2.
+
La misma disciplina se extiende al dominio de la frecuencia: las páginas de
[Señales y espectros](/phonometry/es/signals/spectra/)
aplican el análisis de error de Bendat y Piersol a las estimaciones
@@ -129,6 +137,11 @@ la maquinaria GUM descrita aquí.
el dictamen sobre los resultados de un laboratorio, apartado por apartado,
con las Tablas 4, 5 y B.1 de la IEC 61672-1, y la declaración del
apartado 22.
+- [Calibrar un micrófono por comparación (IEC 61094-5/-8)](/phonometry/es/signals/metrology/comparison-calibration/):
+ la comparación simultánea con el intercambio del Anexo C, la secuencial
+ frente a un micrófono monitor, las correcciones ambiental y del soporte del
+ WS3, los balances de la Tabla D.1 y de la Tabla 2 con k = 2, y la región
+ de campo libre de una ventana temporal.
## Qué no cubre esta sección
diff --git a/site/src/content/docs/es/start/guides.md b/site/src/content/docs/es/start/guides.md
index cf509645a..522b1d469 100644
--- a/site/src/content/docs/es/start/guides.md
+++ b/site/src/content/docs/es/start/guides.md
@@ -1,6 +1,6 @@
---
title: "Guías"
-description: "Las 160 guías de phonometry, agrupadas en los trece temas que cubre la biblioteca: para qué sirve cada área, las normas que implementa y un resumen de una línea de cada guía que contiene."
+description: "Las 161 guías de phonometry, agrupadas en los trece temas que cubre la biblioteca: para qué sirve cada área, las normas que implementa y un resumen de una línea de cada guía que contiene."
head:
- tag: script
attrs:
@@ -118,7 +118,7 @@ hay ninguna panorámica del campo: cada página es la documentación de trabajo
un módulo, escrita para que un resultado se pueda defender apartado por
apartado en lugar de darlo por bueno.
-Esta página es el mapa. Ciento sesenta guías repartidas en trece temas, y cada
+Esta página es el mapa. Ciento sesenta y una guías repartidas en trece temas, y cada
área tiene su propio índice con el relato largo de cómo encajan sus piezas. Si
llegas sin una pregunta concreta, lee primero la
[introducción](/phonometry/es/start/getting-started/): recorre una señal por toda la
@@ -157,7 +157,8 @@ sonoridad necesita niveles de banda calibrados, un parámetro de sala necesita
una respuesta al impulso filtrada, una valoración ambiental es un $L_\mathrm{eq}$
ajustado.
Implementa IEC 61260-1, IEC 61260-3, ANSI S1.11, IEC 61672-1, IEC 61672-3,
-ISO 7196, IEC 61252, ISO 1996-1, IEC 60942, IEC 61183, IEC 62585 y la GUM.
+ISO 7196, IEC 61252, ISO 1996-1, IEC 60942, IEC 61183, IEC 62585,
+IEC 61094-5, IEC 61094-8 y la GUM.
- [Construye un sonómetro](/phonometry/es/signals/sound-level-meter/): el área
entera montada de principio a fin en una sola página ejecutable, del tono del
@@ -276,6 +277,11 @@ ISO 7196, IEC 61252, ISO 1996-1, IEC 60942, IEC 61183, IEC 62585 y la GUM.
el dictamen sobre los resultados de los ensayos periódicos de un
laboratorio, apartado por apartado frente a los límites de la IEC 61672-1 y
los máximos de su Tabla B.1, y la declaración del apartado 22.
+- [Calibrar un micrófono por comparación (IEC 61094-5/-8)](/phonometry/es/signals/metrology/comparison-calibration/):
+ un micrófono patrón de trabajo calibrado frente a una referencia en un
+ acoplador o en campo libre, con el intercambio del Anexo C, el monitor de
+ una sustitución, las correcciones y los balances de la Tabla D.1 y de la
+ Tabla 2.
## [El medio](/phonometry/es/fluids/)
diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx
index 739547435..9624b81a2 100644
--- a/site/src/content/docs/index.mdx
+++ b/site/src/content/docs/index.mdx
@@ -1,6 +1,6 @@
---
title: phonometry
-description: "Acoustic measurement toolkit for Python: levels, psychoacoustics, building and underwater acoustics; 1774 conformance checks against 516 standards."
+description: "Acoustic measurement toolkit for Python: levels, psychoacoustics, building and underwater acoustics; 1786 conformance checks against 518 standards."
# Starlight titles a page " | ", which on this
# one page produces "phonometry | phonometry" and spends the most weighted
# element on the entry page saying nothing. The override also disambiguates
diff --git a/site/src/content/docs/reference/api/index.md b/site/src/content/docs/reference/api/index.md
index 0e827f597..ce045b23d 100644
--- a/site/src/content/docs/reference/api/index.md
+++ b/site/src/content/docs/reference/api/index.md
@@ -67,6 +67,7 @@ La referencia de la API se genera a partir de los docstrings del código (en ing
| [`metrology.random_incidence`](/phonometry/reference/api/metrology/random-incidence/) | Random-incidence and diffuse-field sensitivity of a sound level meter (IEC 61183:1994). |
| [`metrology.free_field_corrections`](/phonometry/reference/api/metrology/free-field-corrections/) | Corrections that bring a sound level meter to its free-field response (IEC 62585:2012). |
| [`metrology.sound_level_meter`](/phonometry/reference/api/metrology/sound-level-meter/) | Periodic tests of a sound level meter (IEC 61672-3:2013): the verdict. |
+| [`metrology.comparison_calibration`](/phonometry/reference/api/metrology/comparison-calibration/) | Calibration of a measurement microphone by comparison with a reference microphone (IEC 61094-5:2016, pressure; IEC 61094-8:2012, free field). |
## Fluids
diff --git a/site/src/content/docs/reference/api/metrology/comparison-calibration.md b/site/src/content/docs/reference/api/metrology/comparison-calibration.md
new file mode 100644
index 000000000..ad4327dab
--- /dev/null
+++ b/site/src/content/docs/reference/api/metrology/comparison-calibration.md
@@ -0,0 +1,970 @@
+---
+title: "metrology.comparison_calibration"
+description: "Calibration of a measurement microphone by comparison with a reference microphone (IEC 61094-5:2016, pressure; IEC 61094-8:2012, free field)."
+sidebar:
+ label: "comparison_calibration"
+---
+
+Calibration of a measurement microphone by comparison with a reference
+microphone (IEC 61094-5:2016, pressure; IEC 61094-8:2012, free field).
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside, or in place of, a reference microphone whose sensitivity is already
+known, both are exposed to the same sound pressure, and the ratio of their
+open-circuit output voltages carries the reference's sensitivity over to it.
+IEC 61094-5 does this in a pressure field, in a coupler or a jig; IEC 61094-8
+does it in a free field, in an anechoic room or behind a time window. Annex D
+of the first part writes the model once for both:
+
+$$
+M_\mathrm{test} = M_\mathrm{ref}\,\frac{R_V}{R_P}
+$$
+
+where $R_V$ is the ratio of the output voltages of the test and
+reference microphones and $R_P$ that of the effective sound pressures
+acting on them, reduced to unity by the procedure and the corrections. In
+sensitivity levels, the form every function here computes,
+
+$$
+L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+$$
+
+with $C_j$ the corrections the part asks for.
+
+**Simultaneous excitation** (IEC 61094-5 5.1.2 and Annex C, IEC 61094-8 5.3).
+Both microphones sit in the field at once, each on its own measuring channel.
+The level reading difference between the channels, reference on channel 1, is
+Formula (C.1); after the microphones are interchanged it is Formula (C.2); and
+their difference cancels the gains of the two channels and the asymmetry of
+the field, Formula (C.3):
+
+$$
+L_\mathrm{ref} - L_\mathrm{test} = \tfrac12\left(L_\mathrm{C12} - L_\mathrm{C21}\right)
+$$
+
+IEC 61094-5 requires the interchange in a coupler or a jig ("shall be used",
+5.1.2); [`simultaneous_comparison`](/phonometry/reference/api/metrology/comparison-calibration/#simultaneous_comparison) refuses a pressure calibration without
+it. IEC 61094-8 does not, and without it the channel difference is taken as
+read.
+
+**Sequential excitation** (IEC 61094-5 5.1.3 and Annex B, IEC 61094-8 5.2 and
+Annex A). The microphones take the same place in turn. Either the exchange
+does not change the sound pressure significantly, or any change is detected
+and corrected, for example with a monitor microphone near the source: the
+ratio of each microphone's output to the monitor's, and the quotient of the
+two ratios, is the output ratio corrected for the drift (IEC 61094-8 A.2).
+[`sequential_comparison`](/phonometry/reference/api/metrology/comparison-calibration/#sequential_comparison) computes it, with the monitor or without.
+
+**Corrections.** IEC 61094-8 7.6 always corrects the reference's sensitivity
+to the environmental conditions of the test. IEC 61094-5 6.6 does so when the
+two microphones are different models; for two of the same model it allows the
+result to be referred instead to the conditions at which the reference's
+calibration is valid. Both parts allow the result to be referred to the
+reference conditions of clause 4 (23,0 °C, 101,325 kPa, 50 %) when reliable
+correction data are available.
+[`environmental_sensitivity_correction`](/phonometry/reference/api/metrology/comparison-calibration/#environmental_sensitivity_correction) writes that correction to first
+order from the coefficients of the microphone, in the units IEC 61094-2
+Annex D gives them. [`jig_diameter_correction`](/phonometry/reference/api/metrology/comparison-calibration/#jig_diameter_correction) returns the corrections of
+IEC 61094-5 Table A.1 for a type WS3 microphone calibrated against an LS2aP in
+the jig of Figure A.4. A free-field calibration against a reference calibrated
+in a pressure field takes the reference's free-field to pressure sensitivity
+level difference of IEC/TS 61094-7 (IEC 61094-8 Table 1 and 8.2).
+
+**Uncertainty.** [`comparison_uncertainty_budget`](/phonometry/reference/api/metrology/comparison-calibration/#comparison_uncertainty_budget) combines, on
+[`combine_uncertainty`](/phonometry/reference/api/metrology/uncertainty/#combine_uncertainty), the components of IEC
+61094-5 Table D.1 or IEC 61094-8 Table 2, each with a sensitivity of 1 in the
+level model above, and multiplies the combined standard uncertainty by the
+coverage factor $k = 2$ both parts report with (IEC 61094-5 7.9 and D.2,
+IEC 61094-8 8.8). The expanded uncertainty is then passed to the calibration,
+which draws it as a band.
+
+**The effective free-field region** (IEC 61094-8 B.1).
+[`free_field_region`](/phonometry/reference/api/metrology/comparison-calibration/#free_field_region) gives the prolate spheroid inside which a time
+window of length $\tau$ simulates a free field, with the source and the
+microphone at its foci and the major diameter
+
+$$
+A = d + \tau c
+$$
+
+at the speed of sound $c$ of IEC 61094-2 Annex F for the conditions of
+the test ([`air`](/phonometry/reference/api/fluids/air/)).
+
+Two printed values the library does not follow
+----------------------------------------------
+
+**IEC 61094-5 D.3.** The root-sum-square of the eight components Table D.1
+prints is 0,0437 dB, not the 0,040 dB D.3 states; with $k = 2$ it is
+0,087 dB rather than 0,08 dB. [`comparison_uncertainty_budget`](/phonometry/reference/api/metrology/comparison-calibration/#comparison_uncertainty_budget) gives the
+sum of the printed components, and the defect is in `docs/ERRATA.md`.
+
+**IEC 61094-8 B.10.** The spectrum of Formula (B.10) is that of a rectangular
+pulse of duration $2b$, although the text calls $b$ the duration;
+the library does not implement the direct impulse method of B.6, which the
+standard itself calls largely superseded, and the defect is in
+`docs/ERRATA.md`.
+
+The IEC 61183 diffuse-field comparison of clause 5
+([`diffuse_field_sensitivity`](/phonometry/reference/api/metrology/random-incidence/#diffuse_field_sensitivity)) is the same
+sequential comparison without a monitor, and computes its level difference and
+its sensitivity level through this module.
+
+> Auto-generated from the source docstrings by `scripts/generate_api_docs.py` (`make api-docs`). Do not edit by hand.
+
+## comparison_uncertainty_budget
+
+```python
+comparison_uncertainty_budget(
+ standard_uncertainties_db: Mapping[str, float | Quantity],
+ *,
+ frequency_hz: float,
+ field: str = 'pressure',
+ additional_components: Sequence[Quantity] = (),
+ coverage_factor: float = 2.0,
+) -> ComparisonUncertaintyBudget
+```
+
+The uncertainty budget of a comparison calibration at one frequency
+(IEC 61094-5:2016 Annex D; IEC 61094-8:2012 8.8 and Table 2).
+
+Each component is given as a standard uncertainty in dB, the column
+Table D.1 prints, or as a [`Quantity`](/phonometry/reference/api/metrology/uncertainty/#quantity) whose
+standard uncertainty it takes: `metrology.rectangular(0, 0.03)` for a
+semi-range of 0,03 dB, say. They are keyed by the names of
+[`IEC61094_5_TABLE_D1`](/phonometry/reference/api/metrology/comparison-calibration/#iec61094_5_table_d1) for a pressure calibration and of
+[`IEC61094_8_TABLE_2`](/phonometry/reference/api/metrology/comparison-calibration/#iec61094_8_table_2) for a free-field one; neither table is
+exhaustive (D.1, 8.1), so a component may be left out and
+`additional_components` adds the ones a set-up needs, such as the
+diameter correction of a WS3 microphone in the jig (Table D.1, special
+cases). The combination is [`combine_uncertainty`](/phonometry/reference/api/metrology/uncertainty/#combine_uncertainty)
+on the level model, every component with a sensitivity of 1, which is the
+root-sum-square of D.3; the expanded uncertainty is $k = 2$ times
+it, the coverage factor 7.9 and D.2 of the first part and 8.8 of the
+second report with.
+
+With the eight components of Table D.1 at 2 kHz the combined standard
+uncertainty is 0,0437 dB and the expanded one 0,087 dB. D.3 prints
+0,040 dB and 0,08 dB, which are not the root-sum-square of its own
+components (an erratum; see `docs/ERRATA.md`).
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `standard_uncertainties_db` | The components, keyed by name, each a standard uncertainty in dB or a [`Quantity`](/phonometry/reference/api/metrology/uncertainty/#quantity). |
+| `frequency_hz` | The frequency the budget is for, in Hz. |
+| `field` | `"pressure"` (Table D.1, default) or `"free_field"` (Table 2). |
+| `additional_components` | Further components, as named [`Quantity`](/phonometry/reference/api/metrology/uncertainty/#quantity) objects (Default: none). |
+| `coverage_factor` | $k$ (Default: 2). |
+
+**Returns:** The [`ComparisonUncertaintyBudget`](/phonometry/reference/api/metrology/comparison-calibration/#comparisonuncertaintybudget).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for a key that is not a component of the field's table, an additional component without a name or with the name of another component, no component at all, or a value that is negative or not finite. |
+
+## ComparisonCalibration
+
+```python
+ComparisonCalibration(
+ frequencies_hz: NDArray[np.float64],
+ reference_sensitivity_level_db: NDArray[np.float64],
+ output_level_differences_db: NDArray[np.float64],
+ pressure_level_difference_db: NDArray[np.float64],
+ corrections_db: Mapping[str, NDArray[np.float64]],
+ field: str,
+ excitation: str,
+ expanded_uncertainty_db: NDArray[np.float64] | None = None,
+)
+```
+
+The sensitivity level of a microphone calibrated by comparison
+(IEC 61094-5:2016 D.2 for a pressure field, IEC 61094-8:2012 for a free
+field).
+
+$$
+L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+$$
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies, in Hz. |
+| `reference_sensitivity_level_db` | $L_\mathrm{ref}$, the reference microphone's sensitivity level as calibrated, in dB re 1 V/Pa. |
+| `output_level_differences_db` | $20\lg R_V$ of each determination, one row per determination, in dB: the output of the test microphone re that of the reference. |
+| `pressure_level_difference_db` | $20\lg R_P$, the effective sound pressure on the test microphone re that on the reference, in dB. |
+| `corrections_db` | The corrections $C_j$ added to the level, by name, each in dB at every frequency. |
+| `field` | `"pressure"` (IEC 61094-5) or `"free_field"` (IEC 61094-8). |
+| `excitation` | `"simultaneous"` or `"sequential"`. |
+| `expanded_uncertainty_db` | The expanded uncertainty ($k = 2$) of the sensitivity level at each frequency, in dB, or `None`. |
+
+### ComparisonCalibration.correction_db
+
+*property*
+
+$\sum_j C_j$, in dB (0 without corrections).
+
+### ComparisonCalibration.determinations
+
+*property*
+
+The number of determinations averaged.
+
+### ComparisonCalibration.output_level_difference_db
+
+*property*
+
+$20\lg R_V$, the mean over the determinations, in dB.
+
+### ComparisonCalibration.plot()
+
+```python
+ComparisonCalibration.plot(
+ ax: Axes | None = None,
+ *,
+ language: str = 'en',
+ **kwargs: Any,
+) -> Axes
+```
+
+Plot $L_\mathrm{test}$ and the reference's level against
+frequency, with the expanded uncertainty as a band when it is known.
+
+The reference's level is $L_\mathrm{ref}$, or, in a free field
+against a pressure-calibrated reference, $L_\mathrm{ref}$ plus
+its free-field difference: the level the test microphone was
+compared with.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `ax` | Existing axes to draw on, or `None` to create a figure. |
+| `language` | Label language, `"en"` (default) or `"es"`. |
+| `kwargs` | Forwarded to the $L_\mathrm{test}$ curve. |
+
+**Returns:** The axes. Requires matplotlib (`pip install phonometry[plot]`).
+
+### ComparisonCalibration.sensitivity_level_db
+
+*property*
+
+$L_\mathrm{test}$, in dB re 1 V/Pa.
+
+### ComparisonCalibration.sensitivity_mv_per_pa
+
+*property*
+
+$M_\mathrm{test} = 10^{L_\mathrm{test}/20}$ V/Pa, in mV/Pa.
+
+### ComparisonCalibration.standard
+
+*property*
+
+The designation the calibration follows.
+
+## ComparisonUncertaintyBudget
+
+```python
+ComparisonUncertaintyBudget(
+ frequency_hz: float,
+ field: str,
+ names: tuple[str, ...],
+ components: tuple[str, ...],
+ standard_uncertainties_db: NDArray[np.float64],
+ uncertainty: UncertaintyResult,
+ coverage_factor: float = 2.0,
+)
+```
+
+The uncertainty budget of a comparison calibration at one frequency
+(IEC 61094-5:2016 Annex D, IEC 61094-8:2012 8.8).
+
+Every component is a standard uncertainty in dB and enters the level model
+with a sensitivity of 1, so the combined standard uncertainty is their
+root-sum-square (D.3), and the expanded uncertainty is $k = 2$
+times it (7.9 of the first part, 8.8 of the second).
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `frequency_hz` | The frequency, in Hz. |
+| `field` | `"pressure"` (Table D.1) or `"free_field"` (Table 2). |
+| `names` | The key of each component, then the names of any additional ones. |
+| `components` | The component as the table prints it, or the name of an additional one. |
+| `standard_uncertainties_db` | $u_i$, in dB. |
+| `uncertainty` | The [`UncertaintyResult`](/phonometry/reference/api/metrology/uncertainty/#uncertaintyresult) of the combination, which has to be the combination of `standard_uncertainties_db`. |
+| `coverage_factor` | $k$, 2 by both parts. |
+
+### ComparisonUncertaintyBudget.combined_uncertainty_db
+
+*property*
+
+$u_\mathrm{c}$, the combined standard uncertainty, in dB.
+
+### ComparisonUncertaintyBudget.expanded_uncertainty_db
+
+*property*
+
+$U = k\,u_\mathrm{c}$, in dB.
+
+### ComparisonUncertaintyBudget.linear_combined_uncertainty_db
+
+*property*
+
+$u_\mathrm{c}$ combined in linear form, in dB.
+
+Each component converted to a relative uncertainty,
+$r_i = 10^{u_i/20} - 1$, combined in quadrature and converted
+back, $20\lg(1 + r_\mathrm{c})$: the "strict calculation" D.3
+mentions and 8.8 prefers. For components of a few hundredths of a
+decibel it differs from `combined_uncertainty_db` in the fourth
+decimal at most, which is why both parts accept the logarithmic form.
+
+### ComparisonUncertaintyBudget.plot()
+
+```python
+ComparisonUncertaintyBudget.plot(
+ ax: Axes | None = None,
+ *,
+ language: str = 'en',
+ **kwargs: Any,
+) -> Axes
+```
+
+Plot the standard uncertainty of each component, with
+$u_\mathrm{c}$, $k$ and $U$.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `ax` | Existing axes to draw on, or `None` to create a figure. |
+| `language` | Label language, `"en"` (default) or `"es"`. |
+| `kwargs` | Forwarded to `barh`. |
+
+**Returns:** The axes. Requires matplotlib (`pip install phonometry[plot]`).
+
+### ComparisonUncertaintyBudget.standard
+
+*property*
+
+The designation whose table the budget follows.
+
+## ComparisonUncertaintyRow
+
+```python
+ComparisonUncertaintyRow(
+ component: str,
+ subclauses: tuple[str, ...] = (),
+ stated_db: float | None = None,
+ divisor: float | None = None,
+ standard_uncertainty_db: float | None = None,
+)
+```
+
+One row of the uncertainty table of IEC 61094-5 (Table D.1) or
+IEC 61094-8 (Table 2).
+
+Table D.1 is a worked example at 2 kHz: seven of its eight rows state a
+value, the distribution it is read with and the standard uncertainty that
+follows; the repeatability row prints its standard uncertainty alone.
+Table 2 lists the typical components with the subclause that discusses
+each and prints no value.
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `component` | The component as the table prints it. |
+| `subclauses` | The subclauses the table refers the component to (Table 2); empty for Table D.1, which prints none. |
+| `stated_db` | The value the row's text states, in dB: a semi-range or an expanded uncertainty with $k = 2$, as `divisor` says; `None` for the repeatability of Table D.1, whose text states none, and for Table 2. |
+| `divisor` | What turns `stated_db` into a standard uncertainty: $\sqrt{3}$ for the semi-range of a rectangular distribution, 2 for an expanded uncertainty with $k = 2$; `None` where `stated_db` is. |
+| `standard_uncertainty_db` | The standard uncertainty the table prints, in dB; `None` for Table 2. |
+
+## environmental_sensitivity_correction
+
+```python
+environmental_sensitivity_correction(
+ frequencies_hz: ArrayLike,
+ *,
+ static_pressure_kpa: float,
+ temperature_c: float,
+ relative_humidity_percent: float,
+ static_pressure_coefficient_db_per_kpa: ArrayLike,
+ temperature_coefficient_db_per_k: ArrayLike,
+ humidity_coefficient_db_per_percent: ArrayLike = 0.0,
+ reference_static_pressure_kpa: float = 101.325,
+ reference_temperature_c: float = 23.0,
+ reference_relative_humidity_percent: float = 50.0,
+) -> EnvironmentalSensitivityCorrection
+```
+
+The correction of a sensitivity level from one set of environmental
+conditions to another (IEC 61094-5 6.6, IEC 61094-8 7.6).
+
+IEC 61094-8 7.6 asks for the reference microphone's sensitivity to be
+corrected to the conditions of the test in every case. IEC 61094-5 6.6
+asks for it when the two microphones are different models, and for two of
+the same model allows the result to be referred instead to the conditions
+at which the reference's calibration is valid. Both allow the result to be
+referred to the reference conditions of clause 4 when reliable correction
+data are available; neither prints a formula. This is the first-order
+one, a coefficient times a deviation for each of the three conditions,
+
+$$
+C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0) + \delta_H\,(H - H_0)
+$$
+
+with the coefficients in the units IEC 61094-2:2009 Annex D gives them:
+dB/kPa for the static pressure, dB/K for the temperature. Annex D of that
+part puts the low-frequency static pressure coefficient of LS1P
+microphones between -0,01 dB/kPa and -0,02 dB/kPa and of LS2P between
+-0,003 dB/kPa and -0,008 dB/kPa, the temperature coefficient within
+±0,005 dB/K, and all three vary with frequency and from one microphone to
+another, so they are the microphone's own and are required here. 6.5.3 of
+the same part observes no influence of humidity on a laboratory standard
+microphone, hence the default of 0 for its coefficient.
+
+The reference conditions default to clause 4 of IEC 61094-5 and IEC
+61094-8, 101,325 kPa, 23,0 °C and 50 %. The correction adds to a level
+known at the reference conditions to give it at the conditions of the
+test; [`simultaneous_comparison`](/phonometry/reference/api/metrology/comparison-calibration/#simultaneous_comparison) and [`sequential_comparison`](/phonometry/reference/api/metrology/comparison-calibration/#sequential_comparison)
+add it to the reference microphone's level (`reference_environment=`)
+or subtract it from the result (`test_environment=`).
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies, in Hz, increasing. |
+| `static_pressure_kpa` | $p_s$, the static pressure the level is wanted at, in kPa. |
+| `temperature_c` | $t$, in °C. |
+| `relative_humidity_percent` | $H$, in %. |
+| `static_pressure_coefficient_db_per_kpa` | $\delta_p$, in dB/kPa, one value or one per frequency. |
+| `temperature_coefficient_db_per_k` | $\delta_t$, in dB/K. |
+| `humidity_coefficient_db_per_percent` | $\delta_H$, in dB per percentage point (Default: 0). |
+| `reference_static_pressure_kpa` | $p_{s,0}$, where the level is known, in kPa (Default: 101,325). |
+| `reference_temperature_c` | $t_0$, in °C (Default: 23,0). |
+| `reference_relative_humidity_percent` | $H_0$, in % (Default: 50). |
+
+**Returns:** The [`EnvironmentalSensitivityCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#environmentalsensitivitycorrection).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for frequencies that are not positive and increasing, a pressure that is not positive, a temperature that is not finite or not above absolute zero, a relative humidity outside 0 % to 100 %, a coefficient that is not finite, or a coefficient column that is not one value per frequency. |
+
+## EnvironmentalSensitivityCorrection
+
+```python
+EnvironmentalSensitivityCorrection(
+ frequencies_hz: NDArray[np.float64],
+ static_pressure_kpa: float,
+ temperature_c: float,
+ relative_humidity_percent: float,
+ reference_static_pressure_kpa: float,
+ reference_temperature_c: float,
+ reference_relative_humidity_percent: float,
+ static_pressure_coefficient_db_per_kpa: NDArray[np.float64],
+ temperature_coefficient_db_per_k: NDArray[np.float64],
+ humidity_coefficient_db_per_percent: NDArray[np.float64],
+)
+```
+
+The change of a microphone's sensitivity level between two sets of
+environmental conditions, to first order in each.
+
+$$
+C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0) + \delta_H\,(H - H_0)
+$$
+
+with the static pressure coefficient $\delta_p$ in dB/kPa, the
+temperature coefficient $\delta_t$ in dB/K and the humidity
+coefficient $\delta_H$ in dB per percentage point, each one value
+or one per frequency. Added to a sensitivity level valid at
+$(p_{s,0}, t_0, H_0)$, it gives the level at $(p_s, t, H)$.
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies, in Hz. |
+| `static_pressure_kpa` | $p_s$, where the level is wanted, in kPa. |
+| `temperature_c` | $t$, in °C. |
+| `relative_humidity_percent` | $H$, in %. |
+| `reference_static_pressure_kpa` | $p_{s,0}$, where the level is known, in kPa. |
+| `reference_temperature_c` | $t_0$, in °C. |
+| `reference_relative_humidity_percent` | $H_0$, in %. |
+| `static_pressure_coefficient_db_per_kpa` | $\delta_p$ at each frequency, in dB/kPa. |
+| `temperature_coefficient_db_per_k` | $\delta_t$, in dB/K. |
+| `humidity_coefficient_db_per_percent` | $\delta_H$, in dB/%. |
+
+### EnvironmentalSensitivityCorrection.correction_db
+
+*property*
+
+$C_\mathrm{env}$, the sum of the three terms, in dB.
+
+### EnvironmentalSensitivityCorrection.humidity_term_db
+
+*property*
+
+$\delta_H\,(H - H_0)$, in dB.
+
+### EnvironmentalSensitivityCorrection.plot()
+
+```python
+EnvironmentalSensitivityCorrection.plot(
+ ax: Axes | None = None,
+ *,
+ language: str = 'en',
+ **kwargs: Any,
+) -> Axes
+```
+
+Plot $C_\mathrm{env}$ and its three terms against frequency.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `ax` | Existing axes to draw on, or `None` to create a figure. |
+| `language` | Label language, `"en"` (default) or `"es"`. |
+| `kwargs` | Forwarded to the $C_\mathrm{env}$ curve. |
+
+**Returns:** The axes. Requires matplotlib (`pip install phonometry[plot]`).
+
+### EnvironmentalSensitivityCorrection.static_pressure_term_db
+
+*property*
+
+$\delta_p\,(p_s - p_{s,0})$, in dB.
+
+### EnvironmentalSensitivityCorrection.temperature_term_db
+
+*property*
+
+$\delta_t\,(t - t_0)$, in dB.
+
+## free_field_region
+
+```python
+free_field_region(
+ source_distance_m: float,
+ window_time_s: float,
+ *,
+ temperature_c: float = 23.0,
+ static_pressure_kpa: float = 101.325,
+ relative_humidity_percent: float = 50.0,
+) -> FreeFieldRegion
+```
+
+The effective free-field region of a time window (IEC 61094-8:2012
+B.1).
+
+Formula (B.1), $A = d + \tau c$, with $c$ "the speed of sound
+at the prevailing environmental conditions", which is taken from the
+IEC 61094-2:2009 Annex F air of [`air`](/phonometry/reference/api/fluids/air/). The
+conditions default to the reference conditions of clause 4, 23,0 °C,
+101,325 kPa and 50 %.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `source_distance_m` | $d$, the separation of the acoustic centres of the source and the microphone, in m. |
+| `window_time_s` | $\tau$, from the arrival of the sound at the microphone to the end of the time window, in s. |
+| `temperature_c` | The air temperature, in °C (Default: 23,0). |
+| `static_pressure_kpa` | The static pressure, in kPa (Default: 101,325). |
+| `relative_humidity_percent` | The relative humidity, in % (Default: 50). |
+
+**Returns:** The [`FreeFieldRegion`](/phonometry/reference/api/metrology/comparison-calibration/#freefieldregion).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for a separation or window that is not positive, or conditions Annex F refuses. |
+
+## FreeFieldRegion
+
+```python
+FreeFieldRegion(
+ source_distance_m: float,
+ window_time_s: float,
+ speed_of_sound: float,
+)
+```
+
+The effective free-field region of a time-selective calibration
+(IEC 61094-8:2012 B.1, Formula (B.1) and Figure B.1).
+
+A prolate spheroid generated by an ellipse with the acoustic centres of
+the source and of the microphone at its foci, $d$ apart, and the
+major diameter $A = d + \tau c$. A reflection from any point on its
+surface arrives $\tau$ after the direct sound, at the end of the
+window; the device measured is inside, and any reflecting surface or
+obstacle has to be outside.
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `source_distance_m` | $d$, the source to receiver separation, in m. |
+| `window_time_s` | $\tau$, from the arrival of the sound at the microphone to the end of the window, in s. |
+| `speed_of_sound` | $c$ at the conditions of the test, in m/s. |
+
+### FreeFieldRegion.major_axis_m
+
+*property*
+
+$A = d + \tau c$, Formula (B.1), in m.
+
+### FreeFieldRegion.plot()
+
+```python
+FreeFieldRegion.plot(
+ ax: Axes | None = None,
+ *,
+ language: str = 'en',
+ **kwargs: Any,
+) -> Axes
+```
+
+Plot the region in a plane through the axis, as Figure B.1 draws it.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `ax` | Existing axes to draw on, or `None` to create a figure. |
+| `language` | Label language, `"en"` (default) or `"es"`. |
+| `kwargs` | Forwarded to the boundary of the region. |
+
+**Returns:** The axes. Requires matplotlib (`pip install phonometry[plot]`).
+
+### FreeFieldRegion.rod_clearance_m
+
+*property*
+
+$(A - d)/2 = \tau c/2$, how far the region reaches behind the
+microphone along the axis, in m: the mounting rod "should be
+sufficiently long so that the end opposite to the microphone is
+completely outside" it (B.1).
+
+### FreeFieldRegion.semi_minor_axis_m
+
+*property*
+
+$b = \sqrt{(A/2)^2 - (d/2)^2}$, the radius of the region at
+mid-way between the source and the microphone, in m: the clearance a
+surface parallel to the axis needs to be outside it.
+
+## IEC61094_5_TABLE_A1
+
+*Constant* (`mapping`).
+
+```python
+IEC61094_5_TABLE_A1 = {1000.0: -0.004, 1250.0: -0.006, 1600.0: -0.009, 2000.0: -0.015, 2500.0: -0.023, 3150.0: -0.036, 4000.0: -0.059, 5000.0: -0.092, 6300.0: -0.146, 8000.0: -0.235, 10000.0: -0.367, 12500.0: -0.572, 16000.0: -0.933, 20000.0: -1.443}
+```
+
+## IEC61094_5_TABLE_D1
+
+*Constant* (`mapping`).
+
+```python
+IEC61094_5_TABLE_D1 = {'reference': ComparisonUncertaintyRow(component='Sensitivity of reference microphone', subclauses=(), stated_db=0.05, divisor=2.0, standard_uncertainty_db=0.025), 'capacitance': ComparisonUncertaintyRow(component='Microphone capacitance', subclauses=(), stated_db=0.01, divisor=1.7320508075688772, standard_uncertainty_db=0.006), 'non_linearity': ComparisonUncertaintyRow(component='Non-linearity', subclauses=(), stated_db=0.03, divisor=1.7320508075688772, standard_uncertainty_db=0.017), 'impedance': ComparisonUncertaintyRow(component='Microphone impedance', subclauses=(), stated_db=0.005, divisor=1.7320508075688772, standard_uncertainty_db=0.003), 'polarizing_voltage': ComparisonUncertaintyRow(component='Polarising voltage', subclauses=(), stated_db=0.008681549586371858, divisor=1.7320508075688772, standard_uncertainty_db=0.005), 'repeatability': ComparisonUncertaintyRow(component='Repeatability', subclauses=(), stated_db=None, divisor=None, standard_uncertainty_db=0.025), 'drift': ComparisonUncertaintyRow(component='Drift in reference microphone sensitivity since last calibration', subclauses=(), stated_db=0.03, divisor=1.7320508075688772, standard_uncertainty_db=0.017), 'rounding': ComparisonUncertaintyRow(component='Rounding of reported results', subclauses=(), stated_db=0.005, divisor=1.7320508075688772, standard_uncertainty_db=0.003)}
+```
+
+## IEC61094_8_TABLE_1
+
+*Constant* (`mapping`).
+
+```python
+IEC61094_8_TABLE_1 = {'primary_free_field': ReferenceCalibrationRow(microphone_types='LS', method='Primary free-field calibration', references=('IEC 61094-3',), expanded_uncertainty_1khz_db=0.25, expanded_uncertainty_10khz_db=0.1), 'primary_pressure': ReferenceCalibrationRow(microphone_types='LS', method='Primary pressure calibration with the addition of a free-field to pressure sensitivity level difference', references=('IEC 61094-2', 'IEC/TS 61094-7'), expanded_uncertainty_1khz_db=0.12, expanded_uncertainty_10khz_db=0.4), 'secondary_pressure': ReferenceCalibrationRow(microphone_types='LS', method='Secondary pressure calibration with the addition of a free-field to pressure sensitivity level difference', references=('IEC 61094-5', 'IEC/TS 61094-7'), expanded_uncertainty_1khz_db=0.15, expanded_uncertainty_10khz_db=0.5), 'secondary_free_field': ReferenceCalibrationRow(microphone_types='LS and WS', method='Secondary free-field calibration', references=('IEC 61094-8',), expanded_uncertainty_1khz_db=0.2, expanded_uncertainty_10khz_db=0.5), 'electrostatic_actuator': ReferenceCalibrationRow(microphone_types='LS and WS', method='Electrostatic actuator calibration with the addition of a free-field to actuator response level difference', references=('IEC 61094-6',), expanded_uncertainty_1khz_db=0.3, expanded_uncertainty_10khz_db=0.6)}
+```
+
+## IEC61094_8_TABLE_2
+
+*Constant* (`mapping`).
+
+## jig_diameter_correction
+
+```python
+jig_diameter_correction(
+ frequencies_hz: ArrayLike | None = None,
+) -> JigDiameterCorrection
+```
+
+The corrections of IEC 61094-5:2016 Table A.1 for a type WS3 microphone
+against a type LS2aP reference in the jig of Figure A.4.
+
+The table gives them at the preferred one-third-octave frequencies from
+1 kHz, -0,004 dB, to 20 kHz, -1,443 dB, calculated for a radially
+symmetrical field and the 0,5 mm diaphragm separation of Figure A.4, "the
+only one for which the corrections specified in Table A.1 are valid". A
+frequency within 2 % of a row, such as an exact base-ten one-third-octave
+frequency, reads as that row. Pass `.correction_db` to the comparison as
+one of its `corrections_db` and `.standard_uncertainty_db` to the
+budget as an additional component.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | Frequencies of the table, in Hz, increasing (Default: None, the 14 rows). |
+
+**Returns:** The [`JigDiameterCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#jigdiametercorrection).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for a frequency the table does not print. |
+
+## JigDiameterCorrection
+
+```python
+JigDiameterCorrection(
+ frequencies_hz: NDArray[np.float64],
+ correction_db: NDArray[np.float64],
+)
+```
+
+The corrections of IEC 61094-5 Table A.1 at the frequencies asked for.
+
+Added to the sensitivity level of a type WS3 microphone calibrated against
+a type LS2aP reference in the jig of Figure A.4, they account for the
+radial sensitivity of the two diaphragms and for the smaller one of the
+test microphone. Their expanded uncertainty is 10 % of their value
+(A.2 and the NOTE to Table A.1), a component of the budget of Table D.1.
+None of the three places that give the 10 % states its coverage factor;
+the library reads it as $k = 2$, the factor 7.9 and D.2 report every
+expanded uncertainty with.
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies of the table asked for, in Hz. |
+| `correction_db` | The correction at each, in dB. |
+
+### JigDiameterCorrection.expanded_uncertainty_db
+
+*property*
+
+The expanded uncertainty of each correction, one tenth of its
+magnitude, in dB (read as $k = 2$, 7.9 and D.2).
+
+### JigDiameterCorrection.plot()
+
+```python
+JigDiameterCorrection.plot(
+ ax: Axes | None = None,
+ *,
+ language: str = 'en',
+ **kwargs: Any,
+) -> Axes
+```
+
+Plot the corrections with their expanded uncertainty.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `ax` | Existing axes to draw on, or `None` to create a figure. |
+| `language` | Label language, `"en"` (default) or `"es"`. |
+| `kwargs` | Forwarded to the correction curve. |
+
+**Returns:** The axes. Requires matplotlib (`pip install phonometry[plot]`).
+
+### JigDiameterCorrection.standard_uncertainty_db
+
+*property*
+
+The standard uncertainty of each correction, the expanded one over
+$k = 2$ (a reading of 7.9 and D.2, since the 10 % comes without
+a coverage factor), in dB: the special-case component of Table D.1.
+
+## ReferenceCalibrationRow
+
+```python
+ReferenceCalibrationRow(
+ microphone_types: str,
+ method: str,
+ references: tuple[str, ...],
+ expanded_uncertainty_1khz_db: float,
+ expanded_uncertainty_10khz_db: float,
+)
+```
+
+One row of IEC 61094-8:2012 Table 1: a way the reference microphone's
+free-field sensitivity can be known, and the expanded uncertainty
+($k = 2$) it typically carries.
+
+**Attributes**
+
+| Name | Description |
+| :--- | :--- |
+| `microphone_types` | The reference microphone types the row is for, `"LS"` or `"LS and WS"`. |
+| `method` | The calibration method as the table prints it. |
+| `references` | The documents that define it. |
+| `expanded_uncertainty_1khz_db` | The typical expanded uncertainty at 1 kHz, in dB. |
+| `expanded_uncertainty_10khz_db` | The same at 10 kHz, in dB. |
+
+## sequential_comparison
+
+```python
+sequential_comparison(
+ frequencies_hz: ArrayLike,
+ reference_sensitivity_level_db: ArrayLike,
+ reference_output_level_db: ArrayLike,
+ test_output_level_db: ArrayLike,
+ *,
+ reference_monitor_level_db: ArrayLike | None = None,
+ test_monitor_level_db: ArrayLike | None = None,
+ field: str = 'pressure',
+ pressure_level_difference_db: ArrayLike = 0.0,
+ corrections_db: Mapping[str, ArrayLike] | None = None,
+ reference_environment: EnvironmentalSensitivityCorrection | None = None,
+ test_environment: EnvironmentalSensitivityCorrection | None = None,
+ reference_free_field_difference_db: ArrayLike | None = None,
+ expanded_uncertainty_db: ArrayLike | None = None,
+) -> ComparisonCalibration
+```
+
+Calibration by comparison with the microphones put in the field in
+turn (IEC 61094-5:2016 5.1.3 and Annex B; IEC 61094-8:2012 5.2 and
+Annex A).
+
+The reference and the test microphone take the same place one after the
+other. Either the exchange does not change the sound pressure
+significantly, or any change is detected and corrected (IEC 61094-5
+5.1.3, IEC 61094-8 5.2), for example with a monitor microphone near the
+source: the ratio of each microphone's output to the monitor's output,
+and the quotient of the two ratios, "gives the ratio of the microphone
+under test output voltage to the reference microphone output voltage,
+corrected for any variation in the sound pressure generated by the
+source" (IEC 61094-8 A.2). With $V_\mathrm{ref}$ and
+$V_\mathrm{mon,1}$ the outputs of the reference and the monitor in
+the first measurement, and $V_\mathrm{test}$ and
+$V_\mathrm{mon,2}$ those of the second,
+
+$$
+20\lg R_V = 20\lg\frac{V_\mathrm{test}}{V_\mathrm{mon,2}} - 20\lg\frac{V_\mathrm{ref}}{V_\mathrm{mon,1}}
+$$
+
+and without a monitor $20\lg(V_\mathrm{test}/V_\mathrm{ref})$,
+the plain difference of the two output levels. The sensitivity level of
+the test microphone then follows by D.2.
+
+Each reading may be one value per frequency or a matrix of one row per
+determination; the calibration averages the determinations.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies, in Hz, increasing. |
+| `reference_sensitivity_level_db` | $L_\mathrm{ref}$, the reference microphone's sensitivity level, in dB re 1 V/Pa. |
+| `reference_output_level_db` | $20\lg V_\mathrm{ref}$, the reference microphone's output level, in dB re any voltage the test microphone is read against too. |
+| `test_output_level_db` | $20\lg V_\mathrm{test}$, the test microphone's output level, in dB re the same voltage. |
+| `reference_monitor_level_db` | $20\lg V_\mathrm{mon,1}$, the monitor's output level in the measurement of the reference, in dB (Default: None, no monitor). |
+| `test_monitor_level_db` | $20\lg V_\mathrm{mon,2}$, the monitor's output level in the measurement of the test microphone, in dB (Default: None; given with the other or not at all). |
+| `field` | `"pressure"` (IEC 61094-5, default) or `"free_field"` (IEC 61094-8). |
+| `pressure_level_difference_db` | $20\lg R_P$, in dB (Default: 0). |
+| `corrections_db` | Further corrections added to the level, by name, in dB (Default: none). |
+| `reference_environment` | The [`EnvironmentalSensitivityCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#environmentalsensitivitycorrection) of the reference, added (Default: None). |
+| `test_environment` | The [`EnvironmentalSensitivityCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#environmentalsensitivitycorrection) of the test microphone, subtracted (Default: None). |
+| `reference_free_field_difference_db` | The reference's free-field to pressure sensitivity level difference (IEC/TS 61094-7), free field only, in dB; carried in `corrections_db` as `"reference free-field difference"` (Default: None). |
+| `expanded_uncertainty_db` | The expanded uncertainty ($k = 2$) of the result at each frequency, in dB (Default: None). |
+
+**Returns:** The [`ComparisonCalibration`](/phonometry/reference/api/metrology/comparison-calibration/#comparisoncalibration).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for an unknown field, one monitor reading without the other, a free-field correction on a pressure calibration, an environmental correction made at other frequencies, readings whose numbers of determinations disagree, or any column that is not one finite value per frequency. |
+
+## simultaneous_comparison
+
+```python
+simultaneous_comparison(
+ frequencies_hz: ArrayLike,
+ reference_sensitivity_level_db: ArrayLike,
+ channel_difference_db: ArrayLike,
+ interchanged_channel_difference_db: ArrayLike | None = None,
+ *,
+ field: str = 'pressure',
+ pressure_level_difference_db: ArrayLike = 0.0,
+ corrections_db: Mapping[str, ArrayLike] | None = None,
+ reference_environment: EnvironmentalSensitivityCorrection | None = None,
+ test_environment: EnvironmentalSensitivityCorrection | None = None,
+ reference_free_field_difference_db: ArrayLike | None = None,
+ expanded_uncertainty_db: ArrayLike | None = None,
+) -> ComparisonCalibration
+```
+
+Calibration by comparison with both microphones in the field at once
+(IEC 61094-5:2016 5.1.2 and Annex C; IEC 61094-8:2012 5.3).
+
+Each microphone is read on its own measuring channel. With the reference
+on channel 1, the level reading difference between the channels is
+Formula (C.1),
+
+$$
+L_\mathrm{C12} = (L_1 + L_\mathrm{m1} + L_\mathrm{d1} + L_\mathrm{WA}) - (L_2 + L_\mathrm{m2} + L_\mathrm{d1} + L_\mathrm{WB})
+$$
+
+and after the microphones are interchanged, in the coupler ports and on
+the preamplifiers, it is Formula (C.2). Their difference leaves the two
+sensitivity levels alone, whatever the gains of the channels and the
+asymmetry of the field, Formula (C.3):
+
+$$
+L_\mathrm{ref} - L_\mathrm{test} = \tfrac12\left(L_\mathrm{C12} - L_\mathrm{C21}\right)
+$$
+
+so $20\lg R_V = -\tfrac12(L_\mathrm{C12} - L_\mathrm{C21})$, and the
+sensitivity level of the test microphone follows by D.2. A pressure
+calibration requires the interchange ("the microphones shall be
+interchanged, and the measurement repeated", 5.1.2). A free-field one does
+not (5.3 relies on a symmetrical field instead); without it, the channel
+difference is taken as $-20\lg R_V$, which assumes channels of equal
+gain.
+
+Each reading may be one value per frequency or a matrix of one row per
+determination (the three repeats of D.2, say); the calibration averages
+the determinations.
+
+**Parameters**
+
+| Name | Description |
+| :--- | :--- |
+| `frequencies_hz` | The frequencies, in Hz, increasing. |
+| `reference_sensitivity_level_db` | $L_\mathrm{ref}$, the reference microphone's sensitivity level, in dB re 1 V/Pa, one value or one per frequency. For a free-field calibration it is the reference's free-field level, or its pressure level with `reference_free_field_difference_db`. |
+| `channel_difference_db` | $L_\mathrm{C12}$, the reading of channel 1 less that of channel 2 with the reference on channel 1, in dB. |
+| `interchanged_channel_difference_db` | $L_\mathrm{C21}$, the same difference after the interchange, the test microphone on channel 1, in dB (Default: None; required for `field="pressure"`). |
+| `field` | `"pressure"` (IEC 61094-5, default) or `"free_field"` (IEC 61094-8). |
+| `pressure_level_difference_db` | $20\lg R_P$, the effective sound pressure on the test microphone re that on the reference, in dB (Default: 0, the ratio reduced to unity). |
+| `corrections_db` | Further corrections added to the level, by name, in dB, such as `jig_diameter_correction(f).correction_db` (Default: none). |
+| `reference_environment` | The [`EnvironmentalSensitivityCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#environmentalsensitivitycorrection) that takes the reference's level from the conditions of its calibration to those of the test, added (Default: None). |
+| `test_environment` | The [`EnvironmentalSensitivityCorrection`](/phonometry/reference/api/metrology/comparison-calibration/#environmentalsensitivitycorrection) of the test microphone from the reference conditions to those of the test, subtracted to refer the result to the reference conditions (Default: None, the result is at the conditions of the test). |
+| `reference_free_field_difference_db` | The reference's free-field to pressure sensitivity level difference (IEC/TS 61094-7), added to a pressure-calibrated reference in a free-field calibration only, in dB; the result carries it in `corrections_db` as `"reference free-field difference"` (Default: None). |
+| `expanded_uncertainty_db` | The expanded uncertainty ($k = 2$) of the result at each frequency, in dB, as [`comparison_uncertainty_budget`](/phonometry/reference/api/metrology/comparison-calibration/#comparison_uncertainty_budget) gives it (Default: None). |
+
+**Returns:** The [`ComparisonCalibration`](/phonometry/reference/api/metrology/comparison-calibration/#comparisoncalibration).
+
+**Raises**
+
+| Exception | When |
+| :--- | :--- |
+| ValueError | for an unknown field, a pressure calibration without the interchange, a free-field correction on a pressure calibration, an environmental correction made at other frequencies, readings whose numbers of determinations disagree, or any column that is not one finite value per frequency. |
diff --git a/site/src/content/docs/reference/api/metrology/random-incidence.md b/site/src/content/docs/reference/api/metrology/random-incidence.md
index 14a21f4e9..88928a31b 100644
--- a/site/src/content/docs/reference/api/metrology/random-incidence.md
+++ b/site/src/content/docs/reference/api/metrology/random-incidence.md
@@ -78,7 +78,11 @@ any of the three and returns a [`DiffuseFieldSensitivity`](/phonometry/reference
prints both the directivity factor and that difference for a type LS2aP/LS2F
laboratory standard microphone, one of the two types Annex B
recommends for the reference (the other is LS2bP);
-[`IEC61183_TABLE_B1`](/phonometry/reference/api/metrology/random-incidence/#iec61183_table_b1) holds it and supplies them by default.
+[`IEC61183_TABLE_B1`](/phonometry/reference/api/metrology/random-incidence/#iec61183_table_b1) holds it and supplies them by default. The method is
+a sequential comparison calibration without a monitor, so Formula (8) and the
+sum of Formulas (9) to (11) are computed by the level model of IEC 61094-5 D.2
+in [`phonometry.metrology.comparison_calibration`](/phonometry/reference/api/metrology/comparison-calibration/), the one the microphone
+calibrations by comparison use.
Two readings the text leaves to the implementer
-----------------------------------------------
diff --git a/site/src/content/docs/reference/conformance.mdx b/site/src/content/docs/reference/conformance.mdx
index 793863866..4dcc8cd78 100644
--- a/site/src/content/docs/reference/conformance.mdx
+++ b/site/src/content/docs/reference/conformance.mdx
@@ -9,7 +9,7 @@ head:
attrs:
type: application/ld+json
content: |
- {"@context":"https://schema.org","@type":"Dataset","@id":"https://jmrplens.github.io/phonometry/reference/conformance/#dataset","name":"phonometry numerical conformance report","description":"1774 numerical conformance checks pinning each implemented acoustic metric to a clause, table or worked example of the governing standard, with the normative expected value, the value phonometry computes, the delta and a pass/fail verdict, across 108 domains and 516 standards. Regenerated and enforced by CI on every pull request.","url":"https://jmrplens.github.io/phonometry/reference/conformance/","inLanguage":"en","license":"https://opensource.org/licenses/MIT","isAccessibleForFree":true,"creator":{"@id":"https://jmrp.io/#person"},"publisher":{"@id":"https://jmrp.io/#person"},"about":{"@id":"https://github.com/jmrplens/phonometry#software"},"measurementTechnique":"Comparison of computed values against the normative expected values published in the governing standards (tolerance tables and worked examples).","variableMeasured":[{"@type":"PropertyValue","name":"Standard","description":"Governing standard and clause or table"},{"@type":"PropertyValue","name":"Quantity","description":"Acoustic quantity under test"},{"@type":"PropertyValue","name":"Expected","description":"Normative value published in the standard"},{"@type":"PropertyValue","name":"Computed","description":"Value computed by phonometry"},{"@type":"PropertyValue","name":"Delta","description":"Signed difference between computed and expected"},{"@type":"PropertyValue","name":"Limit","description":"Published tolerance the deviation is judged against"},{"@type":"PropertyValue","name":"Used","description":"Fraction of the published tolerance the deviation consumes"},{"@type":"PropertyValue","name":"Status","description":"Pass or fail verdict against the standard's tolerance"}],"distribution":[{"@type":"DataDownload","encodingFormat":"application/json","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/conformance.json"},{"@type":"DataDownload","encodingFormat":"text/markdown","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/CONFORMANCE.md"}],"isBasedOn":"https://github.com/jmrplens/phonometry/blob/main/scripts/conformance_report.py","identifier":{"@type":"PropertyValue","propertyID":"DOI","value":"10.5281/zenodo.21215280"}}
+ {"@context":"https://schema.org","@type":"Dataset","@id":"https://jmrplens.github.io/phonometry/reference/conformance/#dataset","name":"phonometry numerical conformance report","description":"1786 numerical conformance checks pinning each implemented acoustic metric to a clause, table or worked example of the governing standard, with the normative expected value, the value phonometry computes, the delta and a pass/fail verdict, across 109 domains and 518 standards. Regenerated and enforced by CI on every pull request.","url":"https://jmrplens.github.io/phonometry/reference/conformance/","inLanguage":"en","license":"https://opensource.org/licenses/MIT","isAccessibleForFree":true,"creator":{"@id":"https://jmrp.io/#person"},"publisher":{"@id":"https://jmrp.io/#person"},"about":{"@id":"https://github.com/jmrplens/phonometry#software"},"measurementTechnique":"Comparison of computed values against the normative expected values published in the governing standards (tolerance tables and worked examples).","variableMeasured":[{"@type":"PropertyValue","name":"Standard","description":"Governing standard and clause or table"},{"@type":"PropertyValue","name":"Quantity","description":"Acoustic quantity under test"},{"@type":"PropertyValue","name":"Expected","description":"Normative value published in the standard"},{"@type":"PropertyValue","name":"Computed","description":"Value computed by phonometry"},{"@type":"PropertyValue","name":"Delta","description":"Signed difference between computed and expected"},{"@type":"PropertyValue","name":"Limit","description":"Published tolerance the deviation is judged against"},{"@type":"PropertyValue","name":"Used","description":"Fraction of the published tolerance the deviation consumes"},{"@type":"PropertyValue","name":"Status","description":"Pass or fail verdict against the standard's tolerance"}],"distribution":[{"@type":"DataDownload","encodingFormat":"application/json","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/conformance.json"},{"@type":"DataDownload","encodingFormat":"text/markdown","contentUrl":"https://raw.githubusercontent.com/jmrplens/phonometry/main/docs/CONFORMANCE.md"}],"isBasedOn":"https://github.com/jmrplens/phonometry/blob/main/scripts/conformance_report.py","identifier":{"@type":"PropertyValue","propertyID":"DOI","value":"10.5281/zenodo.21215280"}}
---
import Conformance from '../../../components/Conformance.astro';
diff --git a/site/src/content/docs/reference/errata.md b/site/src/content/docs/reference/errata.md
index f75bc0eb5..6f9f83349 100644
--- a/site/src/content/docs/reference/errata.md
+++ b/site/src/content/docs/reference/errata.md
@@ -2339,6 +2339,122 @@ in the same order.
Annex F, so no change was required.
- **Status:** unreported (cross-reference defect, no numerical consequence).
+## IEC 61094-5:2016, D.3 (a combined uncertainty its own components do not give)
+
+- **Location:** Annex D, clause D.3 "Combined and expanded uncertainties"
+ (printed folio 21), which combines the eight components of Table D.1
+ (printed folio 20).
+- **The print:** "The combined standard uncertainty is found from the
+ root-sum-square of the uncertainty components, which gives a value of
+ **0,040 dB** [...]. The expanded uncertainty with a coverage factor of 2 is
+ then **0,08 dB**." Table D.1 prints the eight standard uncertainties 0,025
+ (sensitivity of the reference microphone), 0,006 (capacitance), 0,017
+ (non-linearity), 0,003 (impedance), 0,005 (polarising voltage), 0,025
+ (repeatability), 0,017 (drift since the last calibration) and 0,003
+ (rounding) dB, and D.2 says the uncertainty "arises from eight different
+ sources".
+- **The problem:** the root-sum-square of the printed column is
+ $\sqrt{2 \times 0{,}025^2 + 2 \times 0{,}017^2 + 0{,}006^2 + 0{,}005^2 + 2
+ \times 0{,}003^2} = \sqrt{0{,}001\,907} = 0{,}043\,67$ dB, which reads
+ **0,044 dB**, not 0,040 dB; from the values each row states before rounding
+ (0,05/2, and 0,01, 0,03, 0,005, $20\lg(200{,}2/200)$, 0,03 and 0,005 over
+ $\sqrt{3}$) it is 0,043 88 dB. With $k = 2$ the expanded uncertainty is
+ 0,087 dB, **0,09 dB** to the two decimals the clause quotes, not 0,08 dB.
+ The printed 0,040 dB is the root-sum-square of seven of the eight
+ components, with one of the two 0,017 dB rows left out:
+ $\sqrt{0{,}001\,907 - 0{,}017^2} = 0{,}040\,2$ dB.
+- **Evidence:** the eight printed values recombined, and the stated values
+ divided by their divisors. Verified on PDF pages 22 and 23 (printed pp. 20
+ and 21) of IEC 61094-5:2016, Edition 2.0 (2016-05), English-French.
+- **Library behaviour:** `metrology.comparison_uncertainty_budget` combines
+ the components it is given, 0,0437 dB and 0,087 dB for those of Table D.1,
+ and the conformance rows on D.3 pin those values with the printed ones named
+ as the erratum
+ ([`tests/metrology/test_comparison_calibration.py`](https://github.com/jmrplens/phonometry/blob/main/tests/metrology/test_comparison_calibration.py)).
+- **Status:** unreported.
+
+## UNE-EN 61094-5:2017, Table D.1 and D.3 (six values dropped and the combined uncertainty misprinted in translation)
+
+- **Location:** Anexo D, Tabla D.1 "Ejemplo de balance de incertidumbres"
+ (printed pp. 25 and 26) and D.3 "Incertidumbres combinada y expandida"
+ (printed p. 27) of UNE-EN 61094-5 (February 2017), the Spanish version of
+ EN 61094-5:2016, which adopts IEC 61094-5:2016.
+- **The print:** the "Incertidumbre típica dB" column of Table D.1 is **empty**
+ for six of its eight rows, "No linealidad", "Impedancia del micrófono",
+ "Voltaje de polarización", "Repetibilidad", "Deriva en la sensibilidad del
+ micrófono de referencia desde la última calibración" and "Redondeo de los
+ resultados presentados"; only the first two rows print a value, 0,025 and
+ 0,006. D.3 reads "lo que da un valor de **0,004 dB**". The first row's text
+ reads "Esto es equivalente a una incertidumbre típica de **0,025/2 dB** =
+ 0,025 dB", and the first special case speaks of "un micrófono de tipo
+ **WG3**".
+- **The problem:** the English text prints the six values the translation
+ drops, 0,017, 0,003, 0,005, 0,025, 0,017 and 0,003 dB (IEC 61094-5:2016,
+ printed p. 20), and "0,040 dB" in D.3 (printed p. 21), so the Spanish
+ 0,004 dB is a tenth of the English value and inconsistent with its own
+ "0,08 dB" expanded uncertainty two lines further on. The row text divides
+ 0,05 dB, not 0,025 dB, by 2 ("0,05/2 dB = 0,025 dB" in the English), and
+ the special case is a type **WS3** microphone, as the English, Table A.1 of
+ the same translation and the last paragraph of the same special case say. A
+ reader of the Spanish text alone cannot rebuild the budget: six of its eight
+ components are missing, and the combined value it states is not the sum of
+ anything printed. The English combined value is itself an erratum (the
+ entry on IEC 61094-5:2016 D.3 above).
+- **Evidence:** the two prints read side by side. Verified on PDF pages 25
+ to 27 (printed pp. 25 to 27) of UNE-EN 61094-5:2017 and on PDF pages 22 and
+ 23 (printed pp. 20 and 21) of IEC 61094-5:2016.
+- **Library behaviour:** implements the English text, whose Table D.1 the
+ tests and the conformance rows read; no change was required.
+- **Status:** unreported (national translation, not the issuing body's text).
+
+## IEC 61094-8:2012, 8.4 (an unresolved cross-reference)
+
+- **Location:** subclause 8.4 "Differences between the sound pressure applied
+ to the reference microphone and to the microphone under test" (printed
+ folio 15), its first sentence.
+- **The print:** "As stated in **Error! Reference source not found.** the
+ basis of a comparison method is that the test and reference microphones are
+ exposed to a sound field having the same modulus, phase and angle of
+ incidence."
+- **The problem:** a word processor's unresolved field was printed in place
+ of the cross-reference, so the sentence refers to nothing. The statement it
+ paraphrases is the general principle of 5.1: "When a calibrated reference
+ microphone and a microphone under test are exposed to the same free-field
+ sound pressure [...]" (printed folio 8).
+- **Evidence:** the sentence as printed, read against 5.1. Verified on PDF
+ page 17 (printed p. 15) of BS EN 61094-8:2012, the English text of
+ EN 61094-8:2012, which is IEC 61094-8:2012 unchanged.
+- **Library behaviour:** none of the text depends on the reference; no change
+ was required.
+- **Status:** unreported (cross-reference defect, no numerical consequence).
+
+## IEC 61094-8:2012, B.6.1, Formula (B.10) (the spectrum of a pulse of duration 2b called one of duration b)
+
+- **Location:** Annex B, B.6.1 "Outline of methods" of the direct impulse
+ excitation method (printed folio 28), Formula (B.10) and the sentences
+ around it.
+- **The print:** "The Fourier transform, $X(f)$, of a rectangular pulse of
+ **duration $b$** and amplitude $a$ is $X(f) = \dfrac{2ab\sin(2\pi f b)}{2\pi
+ f b}$ (B.10). The first zero in the spectrum is at $f = 1/(2b)$. [...]
+ leading to a requirement for the duration, $b$ of just a few microseconds."
+- **The problem:** Formula (B.10) is the transform of a pulse of amplitude
+ $a$ that lasts from $-b$ to $b$, that is of duration **$2b$**: its value at
+ $f = 0$ is the area of the pulse, $2ab$, and its first zero, where $2\pi f b
+ = \pi$, is at $1/(2b)$, as the text says. A pulse of duration $b$ has the
+ transform $ab \sin(\pi f b)/(\pi f b)$ and its first zero at $1/b$. The
+ formula and the first zero agree with each other; the words "duration $b$"
+ disagree with both, and read "half-duration $b$" or "duration $2b$". The
+ practical conclusion stands on either reading: a first zero ten times above
+ a 20 kHz upper limit, at 200 kHz, asks for a pulse of 5 µs ($b$ = 2,5 µs by
+ the formula), "just a few microseconds".
+- **Evidence:** the transform of the rectangular pulse evaluated at zero
+ frequency and at its first zero. Verified on PDF page 30 (printed p. 28) of
+ BS EN 61094-8:2012, the English text of EN 61094-8:2012, which is
+ IEC 61094-8:2012 unchanged.
+- **Library behaviour:** the direct impulse method, which B.6.2 says has
+ "been largely superseded", is not implemented; no change was required.
+- **Status:** unreported.
+
## UNE-EN ISO 9614-1:2010, clause 9.1 (the sign dropped from "signed magnitude" in translation)
- **Location:** clause 9.1, the symbol list under Formula (11)
diff --git a/site/src/content/docs/signals/index.mdx b/site/src/content/docs/signals/index.mdx
index d9eea62a1..06317becf 100644
--- a/site/src/content/docs/signals/index.mdx
+++ b/site/src/content/docs/signals/index.mdx
@@ -179,6 +179,10 @@ What the numbers mean and how much to trust them.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's periodic-test results for a working meter,
clause by clause, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](/phonometry/signals/metrology/comparison-calibration/):
+ a working standard microphone calibrated against a reference in a pressure
+ field or a free field, with the corrections and the uncertainty budget each
+ part asks for.
## What this section does not cover
diff --git a/site/src/content/docs/signals/metrology/comparison-calibration.mdx b/site/src/content/docs/signals/metrology/comparison-calibration.mdx
new file mode 100644
index 000000000..a1b4a0df5
--- /dev/null
+++ b/site/src/content/docs/signals/metrology/comparison-calibration.mdx
@@ -0,0 +1,514 @@
+---
+title: "Microphone Calibration by Comparison (IEC 61094-5/-8)"
+description: "How IEC 61094-5:2016 and IEC 61094-8:2012 carry a reference microphone's sensitivity over to a working standard microphone: the interchange of Annex C in a coupler, the monitor ratios of a free-field substitution, the environmental, jig and free-field corrections, the uncertainty budgets of Table D.1 and Table 2 with k = 2, and the effective free-field region of a time window."
+references:
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2016
+ title: "Electroacoustics – Measurement microphones – Part 5: Methods for pressure calibration of working standard microphones by comparison"
+ designation: "IEC 61094-5:2016"
+ primary: true
+ note: "The implemented methods: the level model of D.2, the simultaneous excitation with the interchange of 5.1.2 and Annex C (Formulas (C.1) to (C.3)), the sequential excitation with a monitor of 5.1.3 and Annex B, the environmental correction of 6.6, the WS3 corrections of Table A.1 and the example budget of Table D.1 combined as D.3 asks. Read from Edition 2.0 (2016-05), English-French."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2012
+ title: "Electroacoustics – Measurement microphones – Part 8: Methods for determining the free-field sensitivity of working standard microphones by comparison"
+ designation: "IEC 61094-8:2012"
+ primary: true
+ note: "The free-field counterpart: the sequential substitution with a monitor microphone of 5.2 and A.2, the simultaneous excitation of 5.3, the reference calibration options of Table 1, the uncertainty components of Table 2 with the expanded uncertainty of 8.8, and the effective free-field region of Formula (B.1). Read from BS EN 61094-8:2012, the English text of EN 61094-8:2012, which is IEC 61094-8:2012 unchanged."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2009
+ title: "Electroacoustics – Measurement microphones – Part 2: Primary method for pressure calibration of laboratory standard microphones by the reciprocity technique"
+ designation: "IEC 61094-2:2009"
+ note: "Annex D gives the static pressure and temperature coefficients of laboratory standard microphones in dB/kPa and dB/K, the units the environmental correction takes them in; Annex F is the humid air whose speed of sound sets the free-field region of a time window."
+ - type: standard
+ organization: "International Electrotechnical Commission"
+ year: 2006
+ title: "Measurement microphones – Part 7: Values for the difference between free-field and pressure sensitivity levels of laboratory standard microphones"
+ designation: "IEC/TS 61094-7:2006"
+ note: "The free-field to pressure sensitivity level difference a pressure-calibrated reference takes in a free-field calibration (IEC 61094-8 Table 1 and 8.2). The values on this page are illustrative, not read from it."
+ - type: standard
+ organization: "Joint Committee for Guides in Metrology"
+ year: 2008
+ title: "Evaluation of measurement data – Guide to the expression of uncertainty in measurement"
+ designation: "JCGM 100:2008, the GUM"
+ publisher: "BIPM"
+ url: "https://www.bipm.org/documents/20126/2071204/JCGM_100_2008_E.pdf"
+ doi: "10.59161/JCGM100-2008E"
+ note: "The law of propagation of uncertainty both budgets are combined by, and the coverage factor k = 2 both parts report with. Also published as ISO/IEC Guide 98-3:2008."
+---
+
+import Scope from '../../../../components/Scope.astro';
+import ScopeClaim from '../../../../components/ScopeClaim.astro';
+import SeeAlso from '../../../../components/SeeAlso.astro';
+import ThemeImage from '../../../../components/ThemeImage.astro';
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside a reference microphone whose sensitivity is already known, or in its
+place, both are exposed to the same sound pressure, and the ratio of their
+output voltages carries the reference's sensitivity over to it. IEC 61094-5
+does this in a **pressure field**, in a coupler or a jig; IEC 61094-8 does it
+in a **free field**, in an anechoic room or behind a time window. Annex D of
+the first part writes the model once for both,
+
+$$
+M_\mathrm{test} = M_\mathrm{ref}\,\frac{R_V}{R_P},
+\qquad
+L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+$$
+
+where $R_V$ is the ratio of the output voltages of the test and reference
+microphones, $R_P$ that of the effective sound pressures acting on them, which
+the procedure reduces to unity, and $C_j$ the corrections the part asks for.
+`metrology.comparison_calibration` computes the right-hand form, in
+sensitivity levels in dB re 1 V/Pa.
+
+This page runs a pressure calibration of a WS2P against an LS2P and a
+free-field calibration of a WS2F, with their budgets; the readings are
+synthetic, built from sensitivities the page knows, so every step can be
+checked against them.
+
+
+
+*A pressure calibration by simultaneous excitation (left) and a free-field
+calibration by substitution (right), from the results' own `.plot()`.*
+
+## 1. How the measurement goes
+
+Both parts give two ways of exposing the microphones to the same sound
+pressure. With **simultaneous excitation** they sit in the field at once,
+each read on its own measuring channel; in a coupler or a jig they face each
+other about 1 mm apart, a small fraction of the wavelength up to 20 kHz
+(5.1.2). With **sequential excitation** they take the same place in turn:
+either the exchange does not change the sound pressure significantly, or any
+change is detected and corrected, for example with a monitor microphone near
+the source (5.1.3; IEC 61094-8 5.2, 6.5 and A.2).
+
+
+
+| Requirement | Value | Clause |
+| :--- | :--- | :--- |
+| Reference conditions | 23,0 °C, 101,325 kPa, 50 % relative humidity | 61094-5 and 61094-8, 4 |
+| Simultaneous, in a coupler or a jig | Diaphragms about 1 mm apart; the microphones interchanged and the measurement repeated, the result from the two | 61094-5, 5.1.2 |
+| Sequential | The exchange does not change the sound pressure significantly, or any change is detected and corrected, for example with a monitor microphone; a monitor, when used, senses the changes at the test position | 61094-5, 5.1.3; 61094-8, 5.2, 6.5 |
+| Analysis bandwidth | 1/6 octave or narrower keeps the error below 0,01 dB; much narrower FFT lines can show standing waves | 61094-5, 5.1.1 NOTE |
+| Output voltage | Open-circuit, by insert voltage or a high-impedance preamplifier; the method is stated on the certificate | 61094-5, 5.2 |
+| Free-field source | 70 dB to 80 dB at the microphone is usually enough; plane progressive waves across the microphone | 61094-8, 6.3 |
+| Free-field mounting | On a semi-infinite rod of the microphone's diameter; LS1P references without their grid | 61094-8, 6.4, 6.7 |
+| Reported uncertainty | Expanded, with $k = 2$, at each frequency | 61094-5, 7.9; 61094-8, 8.8 |
+
+## 2. A pressure calibration by simultaneous excitation
+
+In a coupler the two microphones are read on two channels whose gains are
+never exactly equal, and the two ports of a coupler never quite see the same
+pressure. Annex C removes both. With the reference as microphone 1 on channel
+1, the level reading difference between the channels is Formula (C.1); after
+the interchange it is Formula (C.2); and their difference leaves only the two
+sensitivity levels,
+
+$$
+L_\mathrm{C12} = (L_1 + L_\mathrm{m1} + L_\mathrm{d1} + L_\mathrm{WA})
+- (L_2 + L_\mathrm{m2} + L_\mathrm{d1} + L_\mathrm{WB}),
+\qquad
+L_1 - L_2 = \tfrac12\,(L_\mathrm{C12} - L_\mathrm{C21})
+\tag{C.3}
+$$
+
+so $20\lg R_V = -\tfrac12(L_\mathrm{C12} - L_\mathrm{C21})$. Here the
+channels differ by 0,55 dB, port A hears up to 0,13 dB more than port B,
+and each reading is repeated three times with a little noise:
+
+```python
+import numpy as np
+from phonometry import metrology
+
+f = metrology.exact_frequencies(250, 20000, fraction=3) # 20 exact one-third octaves
+x = f / 1000
+l_ref = -38.0 + 0.04 * np.log10(x) - 0.25 * (x / 20) ** 2 # the LS2P's certificate, dB re 1 V/Pa
+l_true = -38.6 + 0.08 * np.log10(x) + 0.2 * (x / 12) ** 2 - 0.5 * (x / 20) ** 4 # the WS2P: unknown
+
+gain_1, gain_2 = 0.35, -0.20 # the two measuring channels, dB
+field_a = 0.03 * np.sqrt(x) # port A of the coupler hears 0.015 dB to 0.13 dB more
+rng = np.random.default_rng(61094)
+noise = rng.normal(0.0, 0.004, (2, 3, f.size)) # three repeats of each reading
+l_c12 = (l_ref + gain_1) - (l_true + gain_2) + field_a + noise[0] # reference on channel 1
+l_c21 = (l_true + gain_1) - (l_ref + gain_2) + field_a + noise[1] # interchanged
+
+c = metrology.simultaneous_comparison(f, l_ref, l_c12, l_c21)
+print(c.determinations, c.sensitivity_level_db[[2, 8, 19]].round(2)) # 3 [-38.63 -38.58 -38.44]
+print(np.abs(c.sensitivity_level_db - l_true).max().round(3)) # 0.003
+```
+
+Neither the gains nor the asymmetry reach the result; what is left, 0,003 dB
+at most, is the noise of three repeats. 5.1.2 makes the interchange a
+requirement for a coupler or a jig, so a pressure calibration without
+`interchanged_channel_difference_db` is refused. The text asks for "the mean
+of the two ratios", and Annex C takes it in decibels: the mean of the two
+level differences, which is the geometric mean of the ratios and the only one
+in which the gains cancel exactly. The two ratios read before and after the
+interchange are 1,1 dB to 1,4 dB apart here, twice the difference of the
+channels and of the ports, and their arithmetic mean would sit 0,018 dB to
+0,027 dB high.
+
+**The environment.** A microphone's sensitivity depends on the static
+pressure, the temperature and, for some microphones, the humidity. When the
+reference and the test microphone are different models, the reference's
+sensitivity has to be corrected to the conditions of the test (6.6); in a
+free field, IEC 61094-8 asks for that correction whatever the models (7.6).
+Neither part prints a formula. `metrology.environmental_sensitivity_correction`
+takes the first-order one, a coefficient times a deviation for each
+condition, with the coefficients in the units IEC 61094-2 Annex D gives them:
+
+$$
+C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0)
++ \delta_H\,(H - H_0)
+$$
+
+The coefficients are the microphone's own, which is why they are required:
+Annex D of IEC 61094-2 puts the low-frequency static pressure coefficient of
+LS2P microphones between −0,003 dB/kPa and −0,008 dB/kPa and the temperature
+coefficient within ±0,005 dB/K, and both vary with frequency and from one
+microphone to the next. 6.5.3 of the same part observes no influence of
+humidity on a laboratory standard microphone, so that coefficient defaults
+to 0.
+
+```python
+env = metrology.environmental_sensitivity_correction(
+ f,
+ static_pressure_kpa=99.2, # the laboratory during the test
+ temperature_c=21.5,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.005, # the LS2P's own coefficients
+ temperature_coefficient_db_per_k=0.002,
+) # from its certificate's 101.325 kPa, 23.0 °C and 50 %
+print(env.correction_db[0].round(4)) # 0.0076
+```
+
+Passed as `reference_environment=`, the correction is added to the
+reference's level; passed as `test_environment=`, the test microphone's own
+correction is subtracted from the result, which refers it to the reference
+conditions of clause 4 when reliable coefficients are known.
+
+## 3. The uncertainty budget of Annex D
+
+Table D.1 is a worked budget for a set-up like this one, a low-sensitivity
+WS2P against an LS2P in the coupler of Figure A.1 with three repeats, at
+2 kHz. That coupler is for frequencies up to 10 kHz (A.1); above, a WS2 goes
+in the jig of A.2 and Figure A.3, which reaches 20 kHz. Seven of the eight
+rows state a value and how it is read: the reference's certificate quotes
+±0,05 dB with $k = 2$, the capacitance, non-linearity, impedance, drift and
+rounding are semi-ranges of rectangular distributions, and the polarising
+voltage of (200,0 ± 0,2) V gives a semi-range of
+$20\lg(200{,}2/200) = 0{,}008\,68$ dB. The repeatability row states no
+value, only that it was found from the standard uncertainties of a large
+number of similar measurements, and prints 0,025 dB as a standard
+uncertainty. `metrology.IEC61094_5_TABLE_D1` holds each row with its stated
+value, its divisor and the standard uncertainty it prints, and the seven that
+state a value reproduce it. `metrology.comparison_uncertainty_budget` combines them on
+`metrology.combine_uncertainty`, every component with a sensitivity of 1 in
+the level model, and multiplies by $k = 2$:
+
+```python
+d1 = {
+ "reference": 0.025, "capacitance": 0.006, "non_linearity": 0.017,
+ "impedance": 0.003, "polarizing_voltage": 0.005, "repeatability": 0.025,
+ "drift": 0.017, "rounding": 0.003,
+} # Table D.1, standard uncertainties at 2 kHz
+b = metrology.comparison_uncertainty_budget(d1, frequency_hz=2000)
+print(round(b.combined_uncertainty_db, 4), round(b.expanded_uncertainty_db, 3)) # 0.0437 0.087
+print(round(b.linear_combined_uncertainty_db, 4)) # 0.0436
+```
+
+The root-sum-square of the eight printed values is 0,0437 dB, and the
+expanded uncertainty 0,087 dB. D.3 prints **0,040 dB** and **0,08 dB**,
+which are not the combination of its own components: 0,040 dB is what the
+sum gives with one of the two 0,017 dB rows left out. The case is in the
+[errata register](/phonometry/reference/errata/). D.3 also notes that a
+strict calculation would convert each component to linear form before
+combining, with essentially the same result, and prints no value for it;
+`linear_combined_uncertainty_db` does it and gives 0,043 614 dB, 0,000 055 dB
+below the combination in decibels. The Spanish adoption, UNE-EN
+61094-5:2017, leaves six of the eight values of Table D.1 blank and prints
+0,004 dB in D.3, which is also in the register.
+
+At other frequencies the components change. 7.9 estimates about 0,1 dB at
+low and middle frequencies for microphones of the same diameter, rising to
+about 0,2 dB at 20 kHz for WS2P, and 7.4 and 7.5 name the difference of the
+microphones' acoustic impedances as what grows. Here that component rises
+with frequency, and each budget's expanded uncertainty goes to the
+calibration, which the plot draws as a band:
+
+```python
+budgets = [
+ metrology.comparison_uncertainty_budget(
+ {**d1, "impedance": 0.003 + 0.09 * (fx / 20000) ** 2}, frequency_hz=fx
+ )
+ for fx in f
+]
+u = [bb.expanded_uncertainty_db for bb in budgets]
+c = metrology.simultaneous_comparison(
+ f, l_ref, l_c12, l_c21, reference_environment=env, expanded_uncertainty_db=u
+)
+print(np.round(u, 3)[[0, 16, 19]]) # [0.087 0.101 0.205]
+print(c.sensitivity_mv_per_pa[[2, 8, 19]].round(2)) # [11.72 11.78 11.98]
+c.plot()
+```
+
+
+
+*The budget of Table D.1 at 2 kHz (left) and a free-field budget at 8 kHz
+(right).*
+
+## 4. A WS3 microphone in the jig
+
+A WS3 microphone is smaller than the LS2 reference, and in the jig of Figure
+A.4 the two diaphragms face each other 0,5 mm apart: the pressure over the
+smaller diaphragm is not the mean over the larger one. Table A.1 gives the
+corrections to add to the WS3's sensitivity level, calculated for an LS2aP
+reference and a radially symmetrical field and valid for that separation
+only, with an expanded uncertainty of a tenth of each correction.
+`metrology.jig_diameter_correction` reads them, and an exact base-ten
+frequency reads as its row:
+
+```python
+j = metrology.jig_diameter_correction()
+print(j.correction_db[[0, 9, 13]]) # [-0.004 -0.235 -1.443]
+print(j.standard_uncertainty_db[13].round(4)) # 0.0722
+```
+
+`j.correction_db` goes to a calibration through `corrections_db=`, under a
+name of its own, and `j.standard_uncertainty_db` to its budget as an
+additional component, the first special case of Table D.1.
+
+
+
+*Table A.1 with its expanded uncertainty of 10 %.*
+
+## 5. A free-field calibration by substitution
+
+IEC 61094-8 A.2 describes the basic method: the reference is put at a point
+of the free field, then the microphone under test in its place, each read
+against a monitor microphone near the source, and "the quotient of these two
+gives the ratio of the microphone under test output voltage to the reference
+microphone output voltage, corrected for any variation in the sound pressure
+generated by the source". With $V_\mathrm{ref}$ and $V_\mathrm{mon,1}$ the
+output voltages of the reference and the monitor in the first reading, and
+$V_\mathrm{test}$ and $V_\mathrm{mon,2}$ those of the second,
+
+$$
+20\lg R_V = 20\lg\frac{V_\mathrm{test}}{V_\mathrm{mon,2}} - 20\lg\frac{V_\mathrm{ref}}{V_\mathrm{mon,1}}
+$$
+
+The reference here is an LS2P calibrated in a pressure field, so its
+free-field sensitivity is its pressure sensitivity plus the free-field to
+pressure difference of IEC/TS 61094-7 (Table 1, 8.2). The values below stand
+in for that difference; take yours from the specification or the
+microphone's calibration. Between the two readings the source drifts by
+0,25 dB:
+
+```python
+ff = metrology.exact_frequencies(500, 20000, fraction=3)
+xf = ff / 1000
+c_ff = 0.05 * xf**1.3 # the LS2P's free-field to pressure difference: illustrative
+l_ref_p = -38.0 + 0.04 * np.log10(xf) - 0.25 * (xf / 20) ** 2 # its pressure calibration
+l_ws2f = -38.3 + 0.1 * np.log10(xf) - 0.3 * (xf / 20) ** 3 # the WS2F: unknown
+field_1 = 74.0 + 0.05 * np.sin(xf) # the free field while the reference is in place
+field_2 = field_1 + 0.25 # the source has drifted by the time the WS2F is
+monitor = -40.0 # the monitor microphone near the source, dB re 1 V/Pa
+
+
+def free_field_budget(fx):
+ x20 = fx / 20000
+ return metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.06 + 0.14 * x20, # pressure calibration and IEC/TS 61094-7
+ "source_stability": 0.01,
+ "positioning": 0.02,
+ "alignment": 0.01 + 0.04 * x20**2,
+ "free_field": 0.03 + 0.12 * x20**2,
+ "non_linearity": 0.017,
+ "rounding": 0.003,
+ "repeatability": 0.02,
+ },
+ frequency_hz=fx,
+ field="free_field",
+ )
+
+
+free = metrology.sequential_comparison(
+ ff,
+ l_ref_p,
+ l_ref_p + c_ff + field_1, # the reference's output, dB re 1 V
+ l_ws2f + field_2, # the WS2F's output in its place
+ reference_monitor_level_db=monitor + field_1,
+ test_monitor_level_db=monitor + field_2,
+ field="free_field",
+ reference_free_field_difference_db=c_ff,
+ expanded_uncertainty_db=[free_field_budget(fx).expanded_uncertainty_db for fx in ff],
+)
+print(bool(np.abs(free.sensitivity_level_db - l_ws2f).max() < 1e-9)) # True
+print(free.expanded_uncertainty_db[[0, 12, 16]].round(2)) # [0.16 0.26 0.51]
+```
+
+The monitor takes the drift out exactly; without it the result would be
+0,25 dB high at every frequency. Table 2 of IEC 61094-8 lists the typical
+components with the subclause that discusses each, and prints no values:
+the ones above are illustrative, chosen so that the expanded uncertainty is
+near the 0,2 dB 8.8 estimates at low and middle frequencies and the 0,5 dB
+it estimates at the upper frequency limit of an LS2 reference.
+`metrology.IEC61094_8_TABLE_2` holds the list, and
+`metrology.IEC61094_8_TABLE_1` the typical expanded uncertainty of each way
+the reference can be calibrated:
+
+```python
+row = metrology.IEC61094_8_TABLE_1["primary_pressure"]
+print(row.expanded_uncertainty_1khz_db, row.expanded_uncertainty_10khz_db) # 0.12 0.4
+```
+
+## 6. The effective free-field region of a time window
+
+Where the room is not anechoic enough, a time window can keep the direct
+sound and drop the reflections (6.2.3, Annex B). The window defines the region
+it simulates a free field in: a prolate spheroid with the source and the
+microphone at its foci, $d$ apart, and the major diameter
+
+$$
+A = d + \tau c
+\tag{B.1}
+$$
+
+where $\tau$ runs from the arrival of the sound at the microphone to the end
+of the window and $c$ is the speed of sound at the prevailing conditions,
+here from the humid air of IEC 61094-2 Annex F. A reflection from any point
+of its surface arrives $\tau$ after the direct sound, so every reflecting
+surface has to be outside it, and so does the far end of the mounting rod:
+
+```python
+r = metrology.free_field_region(1.0, 0.005) # 1 m, a 5 ms window, at 23.0 °C
+print(round(r.speed_of_sound, 1), round(r.major_axis_m, 2)) # 345.9 2.73
+print(round(r.semi_minor_axis_m, 2), round(r.rod_clearance_m, 2)) # 1.27 0.86
+```
+
+A 5 ms window at 1 m needs 1,27 m of clearance round the axis mid-way between
+source and microphone, and a rod that runs more than 0,86 m behind the
+microphone.
+
+
+
+*The region of a 5 ms window at 1 m, with the rod that has to leave it.*
+
+## 7. The diffuse-field comparison of IEC 61183
+
+The diffuse-field method of IEC 61183 clause 5 is a sequential comparison
+without a monitor: a sound level meter and a reference meter read in turn at
+the same positions of a reverberation room, the difference of their readings
+added to the reference's diffuse-field sensitivity level. The library computes
+it through the same model, so the two cannot drift apart:
+
+```python
+d = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0], [94.3, 94.6], [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+)
+s = metrology.sequential_comparison([1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6])
+print(d.diffuse_field_level_db, s.sensitivity_level_db) # [-25.7 -25.7] [-25.7 -25.7]
+```
+
+
+Show the code for these figures
+
+```python
+import matplotlib.pyplot as plt
+
+fig, (ax_pressure, ax_free) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+c.plot(ax_pressure)
+free.plot(ax_free)
+fig.tight_layout()
+
+fig, (ax_d1, ax_ff) = plt.subplots(1, 2, figsize=(13.5, 5.6))
+b.plot(ax_d1)
+free_field_budget(8000).plot(ax_ff)
+fig.tight_layout()
+
+fig, ax = plt.subplots(figsize=(10, 6))
+j.plot(ax)
+fig.tight_layout()
+
+fig, ax = plt.subplots(figsize=(10, 6))
+r.plot(ax)
+fig.tight_layout()
+plt.show()
+```
+
+
+
+## What this guide covers
+
+
+
+**IEC 61094-5:2016 and IEC 61094-8:2012, the calculations:** the level
+model of D.2; the simultaneous excitation with the interchange of Annex C
+(Formulas (C.1) to (C.3)), required for a pressure calibration, and without
+it for a free-field one (5.3); the sequential excitation against a monitor
+microphone (61094-5 5.1.3, 61094-8 A.2); the average over the
+determinations; the reference's free-field to pressure difference in a
+free-field calibration (Table 1, 8.2); the WS3 corrections of Table A.1 with
+their uncertainty; the budgets of Table D.1 and Table 2 on the GUM with
+$k = 2$ (7.9, D.2, D.3, 8.8), and the combination in linear form D.3
+mentions; Tables A.1, D.1, 1 and 2 as published data; and the effective
+free-field region of Formula (B.1), with the speed of sound of IEC 61094-2
+Annex F. The IEC 61183 clause 5 comparison computes through the same model.
+Table D.1 and Table A.1 are reproduced.
+
+
+
+**The environmental correction.** 6.6 and 7.6 ask for the reference to be
+corrected to the conditions of the test, or the result to the reference
+conditions, and print no formula; the first-order sum of a coefficient times
+a deviation for each condition is this library's reading, with the
+coefficients the microphone's own. **The mean of the two ratios** of 5.1.2
+is taken in decibels, as Annex C does. **The coverage factor of the WS3
+corrections.** A.2, the NOTE to Table A.1 and the special case of Table D.1
+give their expanded uncertainty as 10 % of each correction without a coverage
+factor; it is read as the $k = 2$ that 7.9 and D.2 report every expanded
+uncertainty with, so the standard uncertainty is 5 % of the correction.
+
+
+
+The measurements themselves, and the phase of the sensitivity, which 5.1.1
+of IEC 61094-5 and 5.1 of IEC 61094-8 allow. The corrections for the difference of the microphones'
+acoustic impedances (7.4, 7.5) and for a non-uniform pressure over
+diaphragms of different diameters beyond Table A.1 (6.5), which the
+standards leave to models in the literature; the validation of a jig or a
+coupler against other set-ups (6.7). The time-selective processing of
+Annex B (stepped sine, sweeps, noise, MLS, direct impulses) and the
+qualification of the free field by ISO 26101. The values of IEC/TS 61094-7
+and the reciprocity calibration of the reference (IEC 61094-2, -3), which
+are inputs here. The second special case of Table D.1, a microphone
+calibrated as a system with its preamplifier (0,002 dB, and for the
+polarising voltage and the capacitance "frequency dependent" and under
+0,02 dB above 200 Hz), is not held as data; a budget that needs it takes it through
+`additional_components`.
+
+
+
+
+
+## See also
+
+- [Free-field corrections of a sound level meter](/phonometry/signals/metrology/free-field-corrections/):
+ IEC 62585, which compares a meter with an LS2P reference in the same way.
+- [Random-incidence and diffuse-field response](/phonometry/signals/metrology/random-incidence/):
+ IEC 61183, whose diffuse-field method is a comparison through the same
+ model.
+- [Measurement uncertainty](/phonometry/signals/metrology/gum-uncertainty/):
+ the GUM law of propagation the budgets are combined by.
+- [Humid air](/phonometry/fluids/humid-air/): the IEC 61094-2 Annex F air
+ whose speed of sound sets the free-field region.
+- [Calibration and dBFS](/phonometry/signals/metrology/calibration/): the
+ calibrator tone that turns a recording into pascals once a microphone's
+ sensitivity is known.
+- API reference: [`metrology.comparison_calibration`](/phonometry/reference/api/metrology/comparison-calibration/).
+
+
diff --git a/site/src/content/docs/signals/metrology/index.mdx b/site/src/content/docs/signals/metrology/index.mdx
index c1c433af6..e3a147eda 100644
--- a/site/src/content/docs/signals/metrology/index.mdx
+++ b/site/src/content/docs/signals/metrology/index.mdx
@@ -78,6 +78,13 @@ clause from the frequency weightings to the high-level stability against the
acceptance limits of IEC 61672-1 and the maxima of its Table B.1, what a
complete test holds, and the statement Clause 22 prescribes for the outcome.
+[Calibration by comparison](/phonometry/signals/metrology/comparison-calibration/)
+goes one step up the chain, to the microphone itself: a working standard
+microphone takes the sensitivity of a reference through the ratio of their
+output voltages, in a coupler by IEC 61094-5 or in a free field by IEC
+61094-8, with the environmental, jig and free-field corrections each part asks
+for and the uncertainty budget of Table D.1 or Table 2.
+
The same discipline extends into the frequency domain: the
[Signals and spectra](/phonometry/signals/spectra/) pages
apply the Bendat & Piersol error analysis to Welch spectral estimates, so
@@ -116,6 +123,11 @@ budgets that are specialisations of the GUM machinery described here.
- [Periodic Tests of a Sound Level Meter (IEC 61672-3)](/phonometry/signals/metrology/sound-level-meter-periodic-tests/):
the verdict on a laboratory's results, clause by clause, with Tables 4, 5
and B.1 of IEC 61672-1, and the statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](/phonometry/signals/metrology/comparison-calibration/):
+ the simultaneous comparison with the interchange of Annex C, the sequential
+ one against a monitor, the environmental and WS3 jig corrections, the
+ budgets of Table D.1 and Table 2 with k = 2, and the free-field region of a
+ time window.
## What this section does not cover
diff --git a/site/src/content/docs/start/guides.md b/site/src/content/docs/start/guides.md
index 232ef12f3..e5eed42ca 100644
--- a/site/src/content/docs/start/guides.md
+++ b/site/src/content/docs/start/guides.md
@@ -1,6 +1,6 @@
---
title: "Guides"
-description: "The 160 guides of phonometry, grouped into the thirteen topics the library covers: what each area is for, the standards implemented in it, and a one-line summary of every guide inside it."
+description: "The 161 guides of phonometry, grouped into the thirteen topics the library covers: what each area is for, the standards implemented in it, and a one-line summary of every guide inside it."
head:
- tag: script
attrs:
@@ -117,7 +117,7 @@ makes, then runnable code and the figure it draws. Nothing here is a survey of
the field; each page is the working documentation of a module, written so that
a result can be defended clause by clause rather than trusted.
-This page is the map. A hundred and sixty guides sit in thirteen topics, and each topic has its
+This page is the map. A hundred and sixty-one guides sit in thirteen topics, and each topic has its
own overview page with the longer story of how its pieces fit together. If you
are arriving without a specific question, read
[Getting Started](/phonometry/start/getting-started/) first: it runs one signal
@@ -154,7 +154,7 @@ every other area consumes it: a loudness model needs calibrated band levels, a
room parameter needs a filtered impulse response, an environmental rating is an
adjusted $L_\mathrm{eq}$. Implements IEC 61260-1, IEC 61260-3, ANSI S1.11,
IEC 61672-1, IEC 61672-3, ISO 7196, IEC 61252, ISO 1996-1, IEC 60942,
-IEC 61183, IEC 62585 and the GUM.
+IEC 61183, IEC 62585, IEC 61094-5, IEC 61094-8 and the GUM.
- [Build a sound level meter](/phonometry/signals/sound-level-meter/):
the whole area assembled end to end on one runnable page, from the
@@ -264,6 +264,10 @@ IEC 61183, IEC 62585 and the GUM.
the verdict on a laboratory's periodic-test results, clause by clause
against the limits of IEC 61672-1 and the maxima of its Table B.1, and the
statement of Clause 22.
+- [Microphone Calibration by Comparison (IEC 61094-5/-8)](/phonometry/signals/metrology/comparison-calibration/):
+ a working standard microphone calibrated against a reference in a coupler
+ or a free field, with the interchange of Annex C, the monitor of a
+ substitution, the corrections and the budgets of Table D.1 and Table 2.
## [The medium](/phonometry/fluids/)
diff --git a/site/src/data/topics.mjs b/site/src/data/topics.mjs
index 387389c04..8ba96f9c4 100644
--- a/site/src/data/topics.mjs
+++ b/site/src/data/topics.mjs
@@ -123,6 +123,7 @@ export const topics = [
'signals/metrology/random-incidence',
'signals/metrology/free-field-corrections',
'signals/metrology/sound-level-meter-periodic-tests',
+ 'signals/metrology/comparison-calibration',
],
},
apiGroup('filters', 'signals', 'metrology'),
diff --git a/site/src/generated/api-sidebar.mjs b/site/src/generated/api-sidebar.mjs
index 232d0efa4..28e8415d4 100644
--- a/site/src/generated/api-sidebar.mjs
+++ b/site/src/generated/api-sidebar.mjs
@@ -51,6 +51,7 @@ export const apiSections = {
'reference/api/metrology/random-incidence',
'reference/api/metrology/free-field-corrections',
'reference/api/metrology/sound-level-meter',
+ 'reference/api/metrology/comparison-calibration',
],
},
'fluids': {
diff --git a/src/phonometry/_plot/metrology.py b/src/phonometry/_plot/metrology.py
index fadb2321a..865c64d72 100644
--- a/src/phonometry/_plot/metrology.py
+++ b/src/phonometry/_plot/metrology.py
@@ -15,6 +15,13 @@
from matplotlib.ticker import FuncFormatter
from numpy.typing import NDArray
+ from ..metrology.comparison_calibration import (
+ ComparisonCalibration,
+ ComparisonUncertaintyBudget,
+ EnvironmentalSensitivityCorrection,
+ FreeFieldRegion,
+ JigDiameterCorrection,
+ )
from ..metrology.conformance import ConformanceVerification
from ..metrology.data_qualification import (
LevelCrossingResult,
@@ -85,6 +92,10 @@
_CORRECTION_AXIS_LABEL = "Correction [dB]"
_REFERENCE_MIC_LABEL = r"$C_\mathrm{FF,RM}$, reference microphone"
+#: The axis of the uncertainty budgets of IEC 62585 Annex I, IEC 61094-5
+#: Table D.1 and IEC 61094-8 Table 2, written once.
+_STANDARD_UNCERTAINTY_LABEL = r"Standard uncertainty $u_i$ [dB]"
+
#: The legend entry of a periodic-test result IEC 61672-3:2013 4.3 forbids
#: using, and the title of one requirement's figure: the clause and its
#: verdict, then the clause's heading as it reads, on a line of its own so
@@ -227,7 +238,7 @@
"Range of the {n} determinations": "Intervalo de las {n} determinaciones",
"{label}, mean": "{label}, media",
r"$f_0$ = {f} Hz, where it is zero": r"$f_0$ = {f} Hz, donde es nula",
- r"Standard uncertainty $u_i$ [dB]": r"Incertidumbre típica $u_i$ [dB]",
+ _STANDARD_UNCERTAINTY_LABEL: r"Incertidumbre típica $u_i$ [dB]",
"Uncertainty budget at {f} Hz (IEC 62585 Annex I)": "Presupuesto de incertidumbre a {f} Hz (IEC 62585, anexo I)",
"Type B": "Tipo B",
"Type A, from repeat measurements": "Tipo A, de medidas repetidas",
@@ -2226,7 +2237,7 @@ def plot_correction_budget(
)
ax.invert_yaxis()
ax.set_xlim(0.0, 1.15 * max(float(values.max()), result.combined_uncertainty_db))
- ax.set_xlabel(_t(r"Standard uncertainty $u_i$ [dB]", language))
+ ax.set_xlabel(_t(_STANDARD_UNCERTAINTY_LABEL, language))
frequency = format_number(result.frequency_hz, language, decimals=0)
dof = result.effective_dof
nu = "∞" if math.isinf(dof) else format_number(dof, language, decimals=2)
@@ -2376,3 +2387,497 @@ def plot_correction_uncertainty_verification(
place_legend_clear(ax.legend(fontsize="small"))
localize_axes(ax, language)
return ax
+
+
+# ---------------------------------------------------------------------------
+# IEC 61094-5 and IEC 61094-8: microphone calibration by comparison
+# ---------------------------------------------------------------------------
+
+#: The title of a calibration, by field.
+_COMPARISON_TITLES: dict[str, str] = {
+ "pressure": "Pressure sensitivity by comparison (IEC 61094-5)",
+ "free_field": "Free-field sensitivity by comparison (IEC 61094-8)",
+}
+
+#: The table a budget follows, named in its title, by field.
+_COMPARISON_BUDGET_TITLES: dict[str, str] = {
+ "pressure": "Uncertainty budget at {f} Hz (IEC 61094-5 Table D.1)",
+ "free_field": "Uncertainty budget at {f} Hz (IEC 61094-8 Table 2)",
+}
+
+#: Short tick labels of the components of IEC 61094-5 Table D.1 and
+#: IEC 61094-8 Table 2, by the key the budget takes them by.
+_COMPARISON_COMPONENT_LABELS: dict[str, str] = {
+ "reference": "Reference microphone",
+ "capacitance": "Microphone capacitance",
+ "non_linearity": "Non-linearity",
+ "impedance": "Microphone impedance",
+ "polarizing_voltage": "Polarizing voltage",
+ "repeatability": "Repeatability",
+ "drift": "Drift of the reference",
+ "rounding": "Rounding",
+ "source_stability": "Stability of the source",
+ "positioning": "Positioning",
+ "alignment": "Alignment",
+ "free_field": "Free-field quality",
+ "non_plane_wave": "Non-plane wave",
+ "environment": "Environmental conditions",
+}
+
+#: The three terms of an environmental correction: label, colour, line style
+#: and marker, in the order the result names them.
+_ENVIRONMENT_TERMS: tuple[tuple[str, str, str, str], ...] = (
+ (r"Static pressure, $\delta_p\,(p_s - p_{s,0})$", _C_SECONDARY, "--", "s"),
+ (r"Temperature, $\delta_t\,(t - t_0)$", _C_TERTIARY, ":", "^"),
+ (r"Humidity, $\delta_H\,(H - H_0)$", _C_QUATERNARY, "-.", "v"),
+)
+
+#: Labels the comparison plots share with the translation table, the same
+#: in both languages: the band of the expanded uncertainty of a calibration,
+#: the coverage line of a budget's title and the total environmental
+#: correction.
+_LEVEL_UNCERTAINTY_BAND_LABEL = r"$L_\mathrm{test} \pm U$ ($k$ = 2)"
+_COVERAGE_TITLE_LINE = r"$k$ = {k}, $U$ = {u} dB"
+_ENVIRONMENT_TOTAL_LABEL = r"$C_\mathrm{env}$, total"
+
+#: Head-room above the tallest bar of a budget, as a fraction of it.
+_COMPARISON_BUDGET_HEADROOM = 1.18
+
+#: How far past the region the drawn rod runs, as a fraction of the major
+#: axis, so that its end is visibly outside it.
+_ROD_OVERHANG = 0.12
+
+#: Points drawn along the boundary of the free-field region.
+_ELLIPSE_POINTS = 361
+
+#: The margin round the free-field region, as a multiple of its semi-axes.
+_REGION_MARGIN = 1.15
+
+#: The size of a figure the free-field region draws on its own, in inches:
+#: wide enough for the legend beside the axes and the two-line title.
+_REGION_FIGURE_SIZE_IN = (10.0, 6.0)
+
+
+_STRINGS.update(
+ {
+ _COMPARISON_TITLES[
+ "pressure"
+ ]: "Sensibilidad en presión por comparación (IEC 61094-5)",
+ _COMPARISON_TITLES[
+ "free_field"
+ ]: "Sensibilidad en campo libre por comparación (IEC 61094-8)",
+ _COMPARISON_BUDGET_TITLES[
+ "pressure"
+ ]: "Balance de incertidumbre a {f} Hz (IEC 61094-5, tabla D.1)",
+ _COMPARISON_BUDGET_TITLES[
+ "free_field"
+ ]: "Balance de incertidumbre a {f} Hz (IEC 61094-8, tabla 2)",
+ r"$L_\mathrm{test}$, microphone under test": r"$L_\mathrm{test}$, micrófono en ensayo",
+ r"$L_\mathrm{ref}$, reference microphone": r"$L_\mathrm{ref}$, micrófono de referencia",
+ "Reference microphone, free-field level": "Micrófono de referencia, nivel en campo libre",
+ _LEVEL_UNCERTAINTY_BAND_LABEL: _LEVEL_UNCERTAINTY_BAND_LABEL,
+ "Sensitivity level [dB re 1 V/Pa]": "Nivel de sensibilidad [dB re 1 V/Pa]",
+ "Reference microphone": "Micrófono de referencia",
+ "Microphone capacitance": "Capacidad del micrófono",
+ "Non-linearity": "No linealidad",
+ "Microphone impedance": "Impedancia del micrófono",
+ "Polarizing voltage": "Tensión de polarización",
+ "Drift of the reference": "Deriva de la referencia",
+ "Stability of the source": "Estabilidad de la fuente",
+ "Positioning": "Posicionamiento",
+ "Alignment": "Alineación",
+ "Free-field quality": "Calidad del campo libre",
+ "Non-plane wave": "Onda no plana",
+ "Environmental conditions": "Condiciones ambientales",
+ _COVERAGE_TITLE_LINE: _COVERAGE_TITLE_LINE,
+ _ENVIRONMENT_TOTAL_LABEL: _ENVIRONMENT_TOTAL_LABEL,
+ _ENVIRONMENT_TERMS[0][0]: r"Presión estática, $\delta_p\,(p_s - p_{s,0})$",
+ _ENVIRONMENT_TERMS[1][0]: r"Temperatura, $\delta_t\,(t - t_0)$",
+ _ENVIRONMENT_TERMS[2][0]: r"Humedad, $\delta_H\,(H - H_0)$",
+ "Environmental correction: {p} kPa, {t} °C, {h} % re {p0} kPa, {t0} °C, {h0} %": "Corrección ambiental: {p} kPa, {t} °C, {h} % re {p0} kPa, {t0} °C, {h0} %",
+ "Correction, Table A.1": "Corrección, tabla A.1",
+ "Expanded uncertainty, 10 % of the correction": "Incertidumbre expandida, 10 % de la corrección",
+ "WS3 microphone in the jig of Figure A.4 (IEC 61094-5 Table A.1)": "Micrófono WS3 en el soporte de la figura A.4 (IEC 61094-5, tabla A.1)",
+ "Boundary of the effective free-field region": "Límite de la región de campo libre efectiva",
+ "Sound source, F1": "Fuente sonora, F1",
+ "Microphone, F2": "Micrófono, F2",
+ "Direct path, $d$ = {d} m": "Trayecto directo, $d$ = {d} m",
+ "Mounting rod": "Varilla de montaje",
+ "Along the axis [m]": "A lo largo del eje [m]",
+ "Across the axis [m]": "Transversal al eje [m]",
+ r"Effective free-field region (IEC 61094-8 B.1): $A = d + \tau c$ = {a} m": r"Región de campo libre efectiva (IEC 61094-8, B.1): $A = d + \tau c$ = {a} m",
+ r"$\tau$ = {tau} ms, $c$ = {c} m/s; clearance {b} m across, {r} m behind the microphone": r"$\tau$ = {tau} ms, $c$ = {c} m/s; holgura {b} m transversal, {r} m tras el micrófono",
+ }
+)
+
+
+def plot_comparison_calibration(
+ result: ComparisonCalibration,
+ ax: Axes | None = None,
+ *,
+ language: str = "en",
+ **kwargs: Any,
+) -> Axes:
+ r"""The sensitivity level of a microphone calibrated by comparison.
+
+ Draws :math:`L_\mathrm{test}` with its expanded uncertainty as a band when
+ the calibration carries one, and the reference's level it was compared
+ with: :math:`L_\mathrm{ref}` as given, or, for a free-field calibration
+ against a pressure-calibrated reference, :math:`L_\mathrm{ref}` plus the
+ reference's free-field to pressure difference, its free-field level.
+
+ :param result: A
+ :class:`~phonometry.metrology.comparison_calibration.ComparisonCalibration`.
+ :param ax: Existing axes, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the :math:`L_\mathrm{test}` curve.
+ :return: The axes.
+ """
+ from .._i18n import localize_axes
+ from ..metrology.comparison_calibration import _FREE_FIELD_DIFFERENCE
+
+ ax = ax if ax is not None else _new_axes()
+ frequencies = np.asarray(result.frequencies_hz, dtype=np.float64)
+ level = np.asarray(result.sensitivity_level_db, dtype=np.float64)
+ if result.expanded_uncertainty_db is not None:
+ uncertainty = np.asarray(result.expanded_uncertainty_db, dtype=np.float64)
+ ax.fill_between(
+ frequencies,
+ level - uncertainty,
+ level + uncertainty,
+ color=theme_fill(_C_PRIMARY, ax),
+ lw=0.0,
+ label=_t(_LEVEL_UNCERTAINTY_BAND_LABEL, language),
+ )
+ style_default(kwargs, "color", _C_PRIMARY)
+ style_default(kwargs, "lw", 1.6)
+ style_default(kwargs, "marker", "o")
+ style_default(kwargs, "ms", 3.5)
+ kwargs.setdefault(
+ "label", _t(r"$L_\mathrm{test}$, microphone under test", language)
+ )
+ ax.plot(frequencies, level, **kwargs)
+ reference = np.asarray(result.reference_sensitivity_level_db, dtype=np.float64)
+ reference_label = r"$L_\mathrm{ref}$, reference microphone"
+ difference = result.corrections_db.get(_FREE_FIELD_DIFFERENCE)
+ if difference is not None:
+ # The level the free field compared the test microphone with, not the
+ # pressure level the reference was calibrated at.
+ reference = reference + np.asarray(difference, dtype=np.float64)
+ reference_label = "Reference microphone, free-field level"
+ ax.plot(
+ frequencies,
+ reference,
+ color=_C_SECONDARY,
+ lw=1.2,
+ ls="--",
+ marker="s",
+ ms=4.0,
+ mfc="none",
+ label=_t(reference_label, language),
+ )
+ ax.set_xscale("log")
+ format_frequency_axis(ax, language=language)
+ ax.set_xlabel(_t(_FREQUENCY_LABEL, language))
+ ax.set_ylabel(_t("Sensitivity level [dB re 1 V/Pa]", language))
+ ax.set_title(_t(_COMPARISON_TITLES[result.field], language))
+ ax.grid(visible=True, which="both", alpha=0.3)
+ place_legend_clear(ax.legend(fontsize="small"))
+ localize_axes(ax, language)
+ return ax
+
+
+def plot_environmental_sensitivity_correction(
+ result: EnvironmentalSensitivityCorrection,
+ ax: Axes | None = None,
+ *,
+ language: str = "en",
+ **kwargs: Any,
+) -> Axes:
+ r"""The environmental correction of a sensitivity level and its three
+ terms against frequency.
+
+ :param result: An
+ :class:`~phonometry.metrology.comparison_calibration.EnvironmentalSensitivityCorrection`.
+ :param ax: Existing axes, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the :math:`C_\mathrm{env}` curve.
+ :return: The axes.
+ """
+ from .._i18n import format_number, localize_axes
+
+ ax = ax if ax is not None else _new_axes()
+ frequencies = np.asarray(result.frequencies_hz, dtype=np.float64)
+ ax.axhline(0.0, color=_C_MUTED, lw=0.8)
+ values = (
+ result.static_pressure_term_db,
+ result.temperature_term_db,
+ result.humidity_term_db,
+ )
+ for (label, colour, style, marker), term in zip(
+ _ENVIRONMENT_TERMS, values, strict=True
+ ):
+ ax.plot(
+ frequencies,
+ term,
+ color=colour,
+ lw=1.1,
+ ls=style,
+ marker=marker,
+ ms=4.0,
+ mfc="none",
+ label=_t(label, language),
+ )
+ style_default(kwargs, "color", _C_PRIMARY)
+ style_default(kwargs, "lw", 1.7)
+ style_default(kwargs, "marker", "o")
+ style_default(kwargs, "ms", 3.5)
+ kwargs.setdefault("label", _t(_ENVIRONMENT_TOTAL_LABEL, language))
+ ax.plot(frequencies, result.correction_db, **kwargs)
+ ax.set_xscale("log")
+ format_frequency_axis(ax, language=language)
+ ax.set_xlabel(_t(_FREQUENCY_LABEL, language))
+ ax.set_ylabel(_t(_CORRECTION_AXIS_LABEL, language))
+ ax.set_title(
+ _t(
+ "Environmental correction: {p} kPa, {t} °C, {h} % re {p0} kPa, {t0} °C, {h0} %",
+ language,
+ p=format_number(result.static_pressure_kpa, language, decimals=3),
+ t=format_number(result.temperature_c, language, decimals=1),
+ h=format_number(result.relative_humidity_percent, language, decimals=0),
+ p0=format_number(
+ result.reference_static_pressure_kpa, language, decimals=3
+ ),
+ t0=format_number(result.reference_temperature_c, language, decimals=1),
+ h0=format_number(
+ result.reference_relative_humidity_percent, language, decimals=0
+ ),
+ )
+ )
+ ax.grid(visible=True, which="both", alpha=0.3)
+ place_legend_clear(ax.legend(fontsize="small"))
+ localize_axes(ax, language)
+ return ax
+
+
+def plot_jig_diameter_correction(
+ result: JigDiameterCorrection,
+ ax: Axes | None = None,
+ *,
+ language: str = "en",
+ **kwargs: Any,
+) -> Axes:
+ """The corrections of IEC 61094-5 Table A.1 with their expanded
+ uncertainty.
+
+ :param result: A
+ :class:`~phonometry.metrology.comparison_calibration.JigDiameterCorrection`.
+ :param ax: Existing axes, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the correction curve.
+ :return: The axes.
+ """
+ from .._i18n import localize_axes
+
+ ax = ax if ax is not None else _new_axes()
+ frequencies = np.asarray(result.frequencies_hz, dtype=np.float64)
+ correction = np.asarray(result.correction_db, dtype=np.float64)
+ uncertainty = np.asarray(result.expanded_uncertainty_db, dtype=np.float64)
+ ax.fill_between(
+ frequencies,
+ correction - uncertainty,
+ correction + uncertainty,
+ color=theme_fill(_C_PRIMARY, ax),
+ lw=0.0,
+ label=_t("Expanded uncertainty, 10 % of the correction", language),
+ )
+ ax.axhline(0.0, color=_C_MUTED, lw=0.8)
+ style_default(kwargs, "color", _C_PRIMARY)
+ style_default(kwargs, "lw", 1.6)
+ style_default(kwargs, "marker", "o")
+ style_default(kwargs, "ms", 3.5)
+ kwargs.setdefault("label", _t("Correction, Table A.1", language))
+ ax.plot(frequencies, correction, **kwargs)
+ ax.set_xscale("log")
+ format_frequency_axis(ax, language=language)
+ ax.set_xlabel(_t(_FREQUENCY_LABEL, language))
+ ax.set_ylabel(_t(_CORRECTION_AXIS_LABEL, language))
+ ax.set_title(
+ _t("WS3 microphone in the jig of Figure A.4 (IEC 61094-5 Table A.1)", language)
+ )
+ ax.grid(visible=True, which="both", alpha=0.3)
+ place_legend_clear(ax.legend(fontsize="small"))
+ localize_axes(ax, language)
+ return ax
+
+
+def _comparison_component_label(name: str, language: str) -> str:
+ """The tick label of one component: its short name, or its own name."""
+ label = _COMPARISON_COMPONENT_LABELS.get(name)
+ return name if label is None else _t(label, language)
+
+
+def plot_comparison_budget(
+ result: ComparisonUncertaintyBudget,
+ ax: Axes | None = None,
+ *,
+ language: str = "en",
+ **kwargs: Any,
+) -> Axes:
+ r"""The standard uncertainty of each component of a comparison budget.
+
+ One bar per component in the order of its table, then the additional
+ ones, with the combined standard uncertainty marked and the coverage
+ factor and expanded uncertainty in the title.
+
+ :param result: A
+ :class:`~phonometry.metrology.comparison_calibration.ComparisonUncertaintyBudget`.
+ :param ax: Existing axes, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to :meth:`~matplotlib.axes.Axes.barh`.
+ :return: The axes.
+ """
+ from .._i18n import format_number, localize_axes
+
+ ax = ax if ax is not None else _new_axes()
+ values = np.asarray(result.standard_uncertainties_db, dtype=np.float64)
+ positions = np.arange(values.size)
+ style_default(kwargs, "color", _C_PRIMARY)
+ ax.barh(positions, values, **kwargs)
+ uc = format_number(result.combined_uncertainty_db, language, decimals=4)
+ combined = ax.axvline(
+ result.combined_uncertainty_db,
+ color=_C_REFERENCE,
+ ls="--",
+ lw=1.2,
+ label=_t(r"$u_\mathrm{{c}}$ = {uc} dB", language, uc=uc),
+ )
+ ax.set_yticks(positions)
+ ax.set_yticklabels(
+ [_comparison_component_label(name, language) for name in result.names]
+ )
+ ax.invert_yaxis()
+ top = max(float(values.max()), result.combined_uncertainty_db)
+ ax.set_xlim(0.0, _COMPARISON_BUDGET_HEADROOM * top)
+ ax.set_xlabel(_t(_STANDARD_UNCERTAINTY_LABEL, language))
+ frequency = format_number(result.frequency_hz, language, decimals=0)
+ k = format_number(result.coverage_factor, language, decimals=0)
+ expanded = format_number(result.expanded_uncertainty_db, language, decimals=3)
+ ax.set_title(
+ _t(_COMPARISON_BUDGET_TITLES[result.field], language, f=frequency)
+ + "\n"
+ + _t(_COVERAGE_TITLE_LINE, language, k=k, u=expanded)
+ )
+ place_legend_clear(ax.legend(handles=[combined], fontsize="small"))
+ ax.grid(visible=True, axis="x", alpha=0.3)
+ localize_axes(ax, language)
+ return ax
+
+
+def plot_free_field_region(
+ result: FreeFieldRegion,
+ ax: Axes | None = None,
+ *,
+ language: str = "en",
+ **kwargs: Any,
+) -> Axes:
+ r"""The effective free-field region of IEC 61094-8 B.1 in a plane through
+ its axis, as Figure B.1 draws it.
+
+ The ellipse with the source and the microphone at its foci, the direct
+ path between them and the mounting rod behind the microphone, running out
+ of the region.
+
+ :param result: A
+ :class:`~phonometry.metrology.comparison_calibration.FreeFieldRegion`.
+ :param ax: Existing axes, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the boundary of the region.
+ :return: The axes.
+ """
+ from .._i18n import format_number, localize_axes
+
+ if ax is None:
+ # The legend sits beside the axes and the title runs to two long
+ # lines: a figure of its own is sized and laid out to keep both.
+ _fig, ax = _import_pyplot().subplots(
+ figsize=_REGION_FIGURE_SIZE_IN, layout="constrained"
+ )
+ # The equal aspect shrinks the axes inside the room the layout gave
+ # them; held to the left, they keep the legend beside them in view.
+ ax.set_anchor("W")
+ half_major = result.major_axis_m / 2.0
+ half_minor = result.semi_minor_axis_m
+ half_distance = result.source_distance_m / 2.0
+ angle = np.linspace(0.0, 2.0 * np.pi, _ELLIPSE_POINTS)
+ style_default(kwargs, "color", _C_PRIMARY)
+ style_default(kwargs, "lw", 1.6)
+ kwargs.setdefault(
+ "label", _t("Boundary of the effective free-field region", language)
+ )
+ ax.plot(half_major * np.cos(angle), half_minor * np.sin(angle), **kwargs)
+ rod_end = half_major + _ROD_OVERHANG * result.major_axis_m
+ ax.plot(
+ [half_distance, rod_end],
+ [0.0, 0.0],
+ color=_C_MUTED,
+ lw=4.0,
+ solid_capstyle="butt",
+ label=_t("Mounting rod", language),
+ )
+ distance = format_number(result.source_distance_m, language, decimals=2)
+ ax.plot(
+ [-half_distance, half_distance],
+ [0.0, 0.0],
+ color=_C_TERTIARY,
+ lw=1.2,
+ ls="--",
+ label=_t("Direct path, $d$ = {d} m", language, d=distance),
+ )
+ ax.plot(
+ [-half_distance],
+ [0.0],
+ ls="none",
+ marker="s",
+ ms=8.0,
+ color=_C_SECONDARY,
+ label=_t("Sound source, F1", language),
+ )
+ ax.plot(
+ [half_distance],
+ [0.0],
+ ls="none",
+ marker="o",
+ ms=7.0,
+ color=_C_REFERENCE,
+ label=_t("Microphone, F2", language),
+ )
+ ax.set_ylim(-_REGION_MARGIN * half_minor, _REGION_MARGIN * half_minor)
+ ax.set_xlim(
+ -_REGION_MARGIN * half_major, rod_end + (_REGION_MARGIN - 1.0) * half_major
+ )
+ ax.set_aspect("equal", adjustable="box")
+ ax.set_xlabel(_t("Along the axis [m]", language))
+ ax.set_ylabel(_t("Across the axis [m]", language))
+ ax.set_title(
+ _t(
+ r"Effective free-field region (IEC 61094-8 B.1): $A = d + \tau c$ = {a} m",
+ language,
+ a=format_number(result.major_axis_m, language, decimals=2),
+ )
+ + "\n"
+ + _t(
+ r"$\tau$ = {tau} ms, $c$ = {c} m/s; clearance {b} m across, {r} m behind the microphone",
+ language,
+ tau=format_number(1000.0 * result.window_time_s, language, decimals=1),
+ c=format_number(result.speed_of_sound, language, decimals=1),
+ b=format_number(half_minor, language, decimals=2),
+ r=format_number(result.rod_clearance_m, language, decimals=2),
+ )
+ )
+ ax.grid(visible=True, alpha=0.3)
+ # Beside the axes rather than on them: inside, a legend covers the region
+ # it describes or the source and the microphone on its axis.
+ ax.legend(fontsize="small", loc="center left", bbox_to_anchor=(1.02, 0.5))
+ localize_axes(ax, language)
+ return ax
diff --git a/src/phonometry/metrology/__init__.py b/src/phonometry/metrology/__init__.py
index 844459fa8..d7f4a635c 100644
--- a/src/phonometry/metrology/__init__.py
+++ b/src/phonometry/metrology/__init__.py
@@ -8,8 +8,10 @@
every calibration starts from, the IEC 61183 random-incidence and
diffuse-field sensitivity of a sound level meter, the IEC 62585
corrections that bring a sound level meter on a calibrator, coupler or
-actuator to its free-field response, and the IEC 61672-3 verdict on the
-periodic tests of a sound level meter. The filter banks and weightings moved to
+actuator to its free-field response, the IEC 61672-3 verdict on the periodic
+tests of a sound level meter, and the calibration of a measurement microphone
+by comparison with a reference microphone, in a pressure field by IEC 61094-5
+and in a free field by IEC 61094-8. The filter banks and weightings moved to
:mod:`phonometry.filters`, the general signal analysis to
:mod:`phonometry.signals` and the IEC 61043 intensity-instrument class check
to :mod:`phonometry.emission.intensity_compliance`, which is what it verifies.
@@ -18,6 +20,25 @@
from __future__ import annotations
from .calibration import CalibrationWarning, sensitivity
+from .comparison_calibration import (
+ IEC61094_5_TABLE_A1,
+ IEC61094_5_TABLE_D1,
+ IEC61094_8_TABLE_1,
+ IEC61094_8_TABLE_2,
+ ComparisonCalibration,
+ ComparisonUncertaintyBudget,
+ ComparisonUncertaintyRow,
+ EnvironmentalSensitivityCorrection,
+ FreeFieldRegion,
+ JigDiameterCorrection,
+ ReferenceCalibrationRow,
+ comparison_uncertainty_budget,
+ environmental_sensitivity_correction,
+ free_field_region,
+ jig_diameter_correction,
+ sequential_comparison,
+ simultaneous_comparison,
+)
from .conformance import ConformanceVerification, verify_conformance
from .data_qualification import (
LevelCrossingResult,
@@ -140,6 +161,10 @@
"FLUCTUATION_MAX_UNCERTAINTY_DB",
"FREQUENCY_ACCEPTANCE_LIMITS_PERCENT",
"FREQUENCY_MAX_UNCERTAINTY_PERCENT",
+ "IEC61094_5_TABLE_A1",
+ "IEC61094_5_TABLE_D1",
+ "IEC61094_8_TABLE_1",
+ "IEC61094_8_TABLE_2",
"IEC61183_TABLE_B1",
"IEC61672_TABLE_4",
"IEC61672_TABLE_5",
@@ -156,12 +181,18 @@
"AdjustmentValue",
"CalibrationWarning",
"CalibratorTableRow",
+ "ComparisonCalibration",
+ "ComparisonUncertaintyBudget",
+ "ComparisonUncertaintyRow",
"ConformanceVerification",
"CorrectionUncertaintyBudget",
"CorrectionUncertaintyVerification",
"DiffuseFieldSensitivity",
"DirectivityFactor",
+ "EnvironmentalSensitivityCorrection",
"FreeFieldCorrection",
+ "FreeFieldRegion",
+ "JigDiameterCorrection",
"LevelCrossingResult",
"MaxUncertaintyRow",
"MonteCarloResult",
@@ -169,6 +200,7 @@
"PeakStatisticsResult",
"Quantity",
"RandomIncidenceSensitivity",
+ "ReferenceCalibrationRow",
"ReferenceMicrophoneRow",
"ReferenceValue",
"SoundCalibratorMeasurements",
@@ -190,15 +222,19 @@
"axisymmetric_directivity_factor",
"combine_uncertainty",
"comparison_coupler_correction",
+ "comparison_uncertainty_budget",
"correction_uncertainty_budget",
"coverage_factor",
"diffuse_field_sensitivity",
"directivity_factor",
"electrostatic_actuator_correction",
+ "environmental_sensitivity_correction",
"equal_area_directivity_factor",
"equal_area_incidence_angles",
"exact_frequencies",
"expanded_uncertainty",
+ "free_field_region",
+ "jig_diameter_correction",
"largest_element_fraction",
"level_crossing_rate",
"maximum_expanded_uncertainty",
@@ -207,6 +243,8 @@
"random_incidence_sensitivity",
"rectangular",
"sensitivity",
+ "sequential_comparison",
+ "simultaneous_comparison",
"sound_calibrator_correction",
"stationarity_test",
"trend_test",
diff --git a/src/phonometry/metrology/comparison_calibration.py b/src/phonometry/metrology/comparison_calibration.py
new file mode 100644
index 000000000..9d904f221
--- /dev/null
+++ b/src/phonometry/metrology/comparison_calibration.py
@@ -0,0 +1,1703 @@
+# Copyright (c) 2026. Jose Manuel Requena Plens
+r"""Calibration of a measurement microphone by comparison with a reference
+microphone (IEC 61094-5:2016, pressure; IEC 61094-8:2012, free field).
+
+A working standard microphone is not calibrated by reciprocity. It is put
+beside, or in place of, a reference microphone whose sensitivity is already
+known, both are exposed to the same sound pressure, and the ratio of their
+open-circuit output voltages carries the reference's sensitivity over to it.
+IEC 61094-5 does this in a pressure field, in a coupler or a jig; IEC 61094-8
+does it in a free field, in an anechoic room or behind a time window. Annex D
+of the first part writes the model once for both:
+
+.. math::
+
+ M_\mathrm{test} = M_\mathrm{ref}\,\frac{R_V}{R_P}
+
+where :math:`R_V` is the ratio of the output voltages of the test and
+reference microphones and :math:`R_P` that of the effective sound pressures
+acting on them, reduced to unity by the procedure and the corrections. In
+sensitivity levels, the form every function here computes,
+
+.. math::
+
+ L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+
+with :math:`C_j` the corrections the part asks for.
+
+**Simultaneous excitation** (IEC 61094-5 5.1.2 and Annex C, IEC 61094-8 5.3).
+Both microphones sit in the field at once, each on its own measuring channel.
+The level reading difference between the channels, reference on channel 1, is
+Formula (C.1); after the microphones are interchanged it is Formula (C.2); and
+their difference cancels the gains of the two channels and the asymmetry of
+the field, Formula (C.3):
+
+.. math::
+
+ L_\mathrm{ref} - L_\mathrm{test} = \tfrac12\left(L_\mathrm{C12} - L_\mathrm{C21}\right)
+
+IEC 61094-5 requires the interchange in a coupler or a jig ("shall be used",
+5.1.2); :func:`simultaneous_comparison` refuses a pressure calibration without
+it. IEC 61094-8 does not, and without it the channel difference is taken as
+read.
+
+**Sequential excitation** (IEC 61094-5 5.1.3 and Annex B, IEC 61094-8 5.2 and
+Annex A). The microphones take the same place in turn. Either the exchange
+does not change the sound pressure significantly, or any change is detected
+and corrected, for example with a monitor microphone near the source: the
+ratio of each microphone's output to the monitor's, and the quotient of the
+two ratios, is the output ratio corrected for the drift (IEC 61094-8 A.2).
+:func:`sequential_comparison` computes it, with the monitor or without.
+
+**Corrections.** IEC 61094-8 7.6 always corrects the reference's sensitivity
+to the environmental conditions of the test. IEC 61094-5 6.6 does so when the
+two microphones are different models; for two of the same model it allows the
+result to be referred instead to the conditions at which the reference's
+calibration is valid. Both parts allow the result to be referred to the
+reference conditions of clause 4 (23,0 °C, 101,325 kPa, 50 %) when reliable
+correction data are available.
+:func:`environmental_sensitivity_correction` writes that correction to first
+order from the coefficients of the microphone, in the units IEC 61094-2
+Annex D gives them. :func:`jig_diameter_correction` returns the corrections of
+IEC 61094-5 Table A.1 for a type WS3 microphone calibrated against an LS2aP in
+the jig of Figure A.4. A free-field calibration against a reference calibrated
+in a pressure field takes the reference's free-field to pressure sensitivity
+level difference of IEC/TS 61094-7 (IEC 61094-8 Table 1 and 8.2).
+
+**Uncertainty.** :func:`comparison_uncertainty_budget` combines, on
+:func:`~phonometry.metrology.combine_uncertainty`, the components of IEC
+61094-5 Table D.1 or IEC 61094-8 Table 2, each with a sensitivity of 1 in the
+level model above, and multiplies the combined standard uncertainty by the
+coverage factor :math:`k = 2` both parts report with (IEC 61094-5 7.9 and D.2,
+IEC 61094-8 8.8). The expanded uncertainty is then passed to the calibration,
+which draws it as a band.
+
+**The effective free-field region** (IEC 61094-8 B.1).
+:func:`free_field_region` gives the prolate spheroid inside which a time
+window of length :math:`\tau` simulates a free field, with the source and the
+microphone at its foci and the major diameter
+
+.. math::
+
+ A = d + \tau c
+
+at the speed of sound :math:`c` of IEC 61094-2 Annex F for the conditions of
+the test (:func:`~phonometry.fluids.air`).
+
+Two printed values the library does not follow
+----------------------------------------------
+
+**IEC 61094-5 D.3.** The root-sum-square of the eight components Table D.1
+prints is 0,0437 dB, not the 0,040 dB D.3 states; with :math:`k = 2` it is
+0,087 dB rather than 0,08 dB. :func:`comparison_uncertainty_budget` gives the
+sum of the printed components, and the defect is in ``docs/ERRATA.md``.
+
+**IEC 61094-8 B.10.** The spectrum of Formula (B.10) is that of a rectangular
+pulse of duration :math:`2b`, although the text calls :math:`b` the duration;
+the library does not implement the direct impulse method of B.6, which the
+standard itself calls largely superseded, and the defect is in
+``docs/ERRATA.md``.
+
+The IEC 61183 diffuse-field comparison of clause 5
+(:func:`~phonometry.metrology.diffuse_field_sensitivity`) is the same
+sequential comparison without a monitor, and computes its level difference and
+its sensitivity level through this module.
+"""
+
+from __future__ import annotations
+
+import math
+from dataclasses import dataclass
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any
+
+import numpy as np
+
+from .._internal.frozen import read_only
+from .._internal.validation import (
+ require_above_absolute_zero,
+ require_choice,
+ require_positive,
+)
+from .free_field_corrections import (
+ _band_column,
+ _common_rows,
+ _determinations,
+ _frequency_axis,
+)
+from .uncertainty import Quantity, UncertaintyResult, combine_uncertainty
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping, Sequence
+
+ from matplotlib.axes import Axes
+ from numpy.typing import ArrayLike, NDArray
+
+__all__ = [
+ "IEC61094_5_TABLE_A1",
+ "IEC61094_5_TABLE_D1",
+ "IEC61094_8_TABLE_1",
+ "IEC61094_8_TABLE_2",
+ "ComparisonCalibration",
+ "ComparisonUncertaintyBudget",
+ "ComparisonUncertaintyRow",
+ "EnvironmentalSensitivityCorrection",
+ "FreeFieldRegion",
+ "JigDiameterCorrection",
+ "ReferenceCalibrationRow",
+ "comparison_uncertainty_budget",
+ "environmental_sensitivity_correction",
+ "free_field_region",
+ "jig_diameter_correction",
+ "sequential_comparison",
+ "simultaneous_comparison",
+]
+
+# ---------------------------------------------------------------------------
+# Vocabulary and reference conditions
+# ---------------------------------------------------------------------------
+
+#: The two sound fields, one part each: a pressure field (IEC 61094-5) and a
+#: free field (IEC 61094-8).
+_FIELDS = ("pressure", "free_field")
+
+#: The designation each field is calibrated by.
+_STANDARDS: Mapping[str, str] = MappingProxyType(
+ {"pressure": "IEC 61094-5:2016", "free_field": "IEC 61094-8:2012"}
+)
+
+#: The two ways of exposing the microphones: at once, or in turn.
+_EXCITATIONS = ("simultaneous", "sequential")
+
+#: IEC 61094-5:2016 and IEC 61094-8:2012 clause 4, the reference
+#: environmental conditions: "temperature 23,0 °C".
+_REFERENCE_TEMPERATURE_C = 23.0
+#: IEC 61094-5:2016 and IEC 61094-8:2012 clause 4: "static pressure
+#: 101,325 kPa".
+_REFERENCE_STATIC_PRESSURE_KPA = 101.325
+#: IEC 61094-5:2016 and IEC 61094-8:2012 clause 4: "relative humidity 50 %".
+_REFERENCE_RELATIVE_HUMIDITY_PERCENT = 50.0
+
+#: A relative humidity is a percentage of saturation: 0 % is dry air, 100 %
+#: saturated air, and no state of the air lies outside them.
+_SATURATED_PERCENT = 100.0
+
+#: Relative agreement asked of two frequency axes for them to be the same
+#: frequencies: the rounding of a value that went through arithmetic, nothing
+#: more. A nominal 3150 Hz against an exact 3162 Hz is another frequency.
+_SAME_FREQUENCY_REL_TOL = 1e-9
+
+#: The name under which a free-field calibration carries the reference's
+#: free-field to pressure sensitivity level difference in ``corrections_db``.
+_FREE_FIELD_DIFFERENCE = "reference free-field difference"
+
+#: The coverage factor both parts report the expanded uncertainty with
+#: (IEC 61094-5 7.9 and D.2, IEC 61094-8 8.8).
+_COVERAGE_FACTOR = 2.0
+
+#: IEC 61094-5 Table A.1 NOTE: "The expanded uncertainty is estimated to be
+#: 1/10th of the value of the correction (in decibels)." Neither the NOTE, A.2
+#: nor the special case of Table D.1 gives its coverage factor; it is read as
+#: the k = 2 that 7.9 and D.2 report every expanded uncertainty with.
+_JIG_CORRECTION_RELATIVE_EXPANDED = 0.1
+
+#: How far a frequency may sit from a frequency Table A.1 prints and still be
+#: read as it: an exact base-ten one-third-octave frequency is within 1 % of
+#: its nominal value, and adjacent rows of the table are 25 % apart.
+_NOMINAL_TOLERANCE = 0.02
+
+#: Relative agreement asked of a budget's stated combination.
+_COMBINATION_REL_TOL = 1e-9
+
+#: The same, absolute, for a budget whose every component is zero.
+_COMBINATION_ABS_TOL_DB = 1e-15
+
+_SQRT3 = math.sqrt(3.0)
+
+
+# ---------------------------------------------------------------------------
+# The shared core: the level model of IEC 61094-5 D.2
+# ---------------------------------------------------------------------------
+
+
+def _output_level_difference_db(
+ test_output_level_db: ArrayLike,
+ reference_output_level_db: ArrayLike,
+ test_monitor_level_db: ArrayLike | None = None,
+ reference_monitor_level_db: ArrayLike | None = None,
+) -> NDArray[np.float64]:
+ r""":math:`20\lg R_V`, the output of the test microphone re the reference.
+
+ Each reading is taken against the monitor reading made with it when there
+ is one (IEC 61094-8 A.2): the quotient of the two ratios to the monitor is
+ the ratio of the two outputs corrected for any change of the field.
+ Without a monitor it is the plain difference of the two readings, which is
+ also Formula (8) of IEC 61183.
+ """
+ test = np.asarray(test_output_level_db, dtype=np.float64)
+ reference = np.asarray(reference_output_level_db, dtype=np.float64)
+ if test_monitor_level_db is not None:
+ test = test - np.asarray(test_monitor_level_db, dtype=np.float64)
+ if reference_monitor_level_db is not None:
+ reference = reference - np.asarray(reference_monitor_level_db, dtype=np.float64)
+ return test - reference
+
+
+def _interchange_level_difference_db(
+ channel_difference_db: ArrayLike, interchanged_channel_difference_db: ArrayLike
+) -> NDArray[np.float64]:
+ r""":math:`20\lg R_V` from the two channel differences of Annex C.
+
+ Formula (C.3) with the reference as microphone 1:
+ :math:`L_\mathrm{test} - L_\mathrm{ref} = -\tfrac12(L_\mathrm{C12} -
+ L_\mathrm{C21})`.
+ """
+ first = np.asarray(channel_difference_db, dtype=np.float64)
+ second = np.asarray(interchanged_channel_difference_db, dtype=np.float64)
+ return -0.5 * (first - second)
+
+
+def _compared_level_db(
+ reference_level_db: ArrayLike,
+ output_level_difference_db: ArrayLike,
+ pressure_level_difference_db: ArrayLike = 0.0,
+ correction_db: ArrayLike = 0.0,
+) -> NDArray[np.float64]:
+ r"""The sensitivity level of the microphone under test.
+
+ IEC 61094-5 D.2, :math:`M_\mathrm{test} = M_\mathrm{ref} R_V / R_P`, in
+ levels: :math:`L_\mathrm{ref} + 20\lg R_V - 20\lg R_P` plus the sum of the
+ corrections. Formulas (9) to (11) of IEC 61183 are the same sum with no
+ pressure ratio and no correction.
+ """
+ return (
+ np.asarray(reference_level_db, dtype=np.float64)
+ + np.asarray(output_level_difference_db, dtype=np.float64)
+ - np.asarray(pressure_level_difference_db, dtype=np.float64)
+ + np.asarray(correction_db, dtype=np.float64)
+ )
+
+
+# ---------------------------------------------------------------------------
+# The printed tables
+# ---------------------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class ComparisonUncertaintyRow:
+ r"""One row of the uncertainty table of IEC 61094-5 (Table D.1) or
+ IEC 61094-8 (Table 2).
+
+ Table D.1 is a worked example at 2 kHz: seven of its eight rows state a
+ value, the distribution it is read with and the standard uncertainty that
+ follows; the repeatability row prints its standard uncertainty alone.
+ Table 2 lists the typical components with the subclause that discusses
+ each and prints no value.
+
+ :ivar component: The component as the table prints it.
+ :ivar subclauses: The subclauses the table refers the component to
+ (Table 2); empty for Table D.1, which prints none.
+ :ivar stated_db: The value the row's text states, in dB: a semi-range or
+ an expanded uncertainty with :math:`k = 2`, as :attr:`divisor` says;
+ ``None`` for the repeatability of Table D.1, whose text states none,
+ and for Table 2.
+ :ivar divisor: What turns :attr:`stated_db` into a standard uncertainty:
+ :math:`\sqrt{3}` for the semi-range of a rectangular distribution, 2
+ for an expanded uncertainty with :math:`k = 2`; ``None`` where
+ :attr:`stated_db` is.
+ :ivar standard_uncertainty_db: The standard uncertainty the table prints,
+ in dB; ``None`` for Table 2.
+ """
+
+ component: str
+ subclauses: tuple[str, ...] = ()
+ stated_db: float | None = None
+ divisor: float | None = None
+ standard_uncertainty_db: float | None = None
+
+
+def _d1_row(
+ component: str, stated_db: float, divisor: float, printed_db: float
+) -> ComparisonUncertaintyRow:
+ return ComparisonUncertaintyRow(
+ component=component,
+ stated_db=stated_db,
+ divisor=divisor,
+ standard_uncertainty_db=printed_db,
+ )
+
+
+#: IEC 61094-5:2016 Table D.1, "Example uncertainty budget", for a
+#: simultaneous calibration of a low-sensitivity WS2P microphone against an
+#: LS2P reference in the coupler of Figure A.1, at 2 kHz. Keyed by the name
+#: :func:`comparison_uncertainty_budget` takes each component by, in the
+#: table's order. The polarising voltage of (200,0 ± 0,2) V states a
+#: semi-range of :math:`20\lg(200{,}2/200)` dB. The repeatability row states no
+#: value, only that it was "found from the standard uncertainties of a large
+#: number of similar measurements", so it holds the printed standard
+#: uncertainty alone.
+#:
+#: The table goes on with two "additional components for special cases",
+#: which are not rows here. The first, the diaphragm diameter correction of a
+#: WS3 microphone against an LS2 reference, prints "frequency dependent (see
+#: Table A.1)" and an expanded uncertainty of 10 % of the correction: it is
+#: :attr:`JigDiameterCorrection.standard_uncertainty_db`. The second, the
+#: uncertainties of a calibration made as a system with a preamplifier, prints
+#: 0,002 dB, "frequency dependent" and "<0,02 above 200 Hz" in its column,
+#: beside a heading and two notes: the effect of a polarising voltage that
+#: deviates from 200 V, and the capacitance component to be reconsidered. A
+#: set-up that needs them passes them to :func:`comparison_uncertainty_budget`
+#: as ``additional_components``.
+IEC61094_5_TABLE_D1: Mapping[str, ComparisonUncertaintyRow] = MappingProxyType(
+ {
+ "reference": _d1_row("Sensitivity of reference microphone", 0.05, 2.0, 0.025),
+ "capacitance": _d1_row("Microphone capacitance", 0.01, _SQRT3, 0.006),
+ "non_linearity": _d1_row("Non-linearity", 0.03, _SQRT3, 0.017),
+ "impedance": _d1_row("Microphone impedance", 0.005, _SQRT3, 0.003),
+ "polarizing_voltage": _d1_row(
+ "Polarising voltage", 20.0 * math.log10(200.2 / 200.0), _SQRT3, 0.005
+ ),
+ "repeatability": ComparisonUncertaintyRow(
+ component="Repeatability", standard_uncertainty_db=0.025
+ ),
+ "drift": _d1_row(
+ "Drift in reference microphone sensitivity since last calibration",
+ 0.03,
+ _SQRT3,
+ 0.017,
+ ),
+ "rounding": _d1_row("Rounding of reported results", 0.005, _SQRT3, 0.003),
+ }
+)
+
+#: IEC 61094-8:2012 Table 2, "Typical uncertainty components", keyed by the
+#: name :func:`comparison_uncertainty_budget` takes each component by, with
+#: the subclause references the table prints ("-" for the rounding error).
+#: The table prints no values: 8.8 asks for each as a standard uncertainty at
+#: each frequency.
+IEC61094_8_TABLE_2: Mapping[str, ComparisonUncertaintyRow] = MappingProxyType(
+ {
+ "reference": ComparisonUncertaintyRow(
+ "Free-field sensitivity of the reference microphone", ("8.2",)
+ ),
+ "source_stability": ComparisonUncertaintyRow(
+ "Stability of sound source", ("8.4",)
+ ),
+ "positioning": ComparisonUncertaintyRow(
+ "Positioning accuracy (including acoustic centre uncertainty)",
+ ("7.3", "8.4"),
+ ),
+ "alignment": ComparisonUncertaintyRow(
+ "Alignment between source and receiver", ("7.4", "8.4")
+ ),
+ "free_field": ComparisonUncertaintyRow(
+ "Quality of free-field environment or influence of signal processing",
+ ("8.5", "8.6"),
+ ),
+ "non_plane_wave": ComparisonUncertaintyRow(
+ "Influence of non-plane wave", ("6.3",)
+ ),
+ "environment": ComparisonUncertaintyRow(
+ "Influence of environmental conditions", ("7.6",)
+ ),
+ "polarizing_voltage": ComparisonUncertaintyRow(
+ "Polarizing voltage", ("7.2", "8.2")
+ ),
+ "capacitance": ComparisonUncertaintyRow("Microphone capacitance", ("8.7.1",)),
+ "non_linearity": ComparisonUncertaintyRow(
+ "Measurement system non-linearity", ("8.7.2",)
+ ),
+ "rounding": ComparisonUncertaintyRow("Rounding error"),
+ "repeatability": ComparisonUncertaintyRow(
+ "Measurement repeatability", ("8.3",)
+ ),
+ }
+)
+
+#: The table each field's budget draws its components from.
+_BUDGET_TABLES: Mapping[str, Mapping[str, ComparisonUncertaintyRow]] = MappingProxyType(
+ {"pressure": IEC61094_5_TABLE_D1, "free_field": IEC61094_8_TABLE_2}
+)
+
+
+@dataclass(frozen=True)
+class ReferenceCalibrationRow:
+ """One row of IEC 61094-8:2012 Table 1: a way the reference microphone's
+ free-field sensitivity can be known, and the expanded uncertainty
+ (:math:`k = 2`) it typically carries.
+
+ :ivar microphone_types: The reference microphone types the row is for,
+ ``"LS"`` or ``"LS and WS"``.
+ :ivar method: The calibration method as the table prints it.
+ :ivar references: The documents that define it.
+ :ivar expanded_uncertainty_1khz_db: The typical expanded uncertainty at
+ 1 kHz, in dB.
+ :ivar expanded_uncertainty_10khz_db: The same at 10 kHz, in dB.
+ """
+
+ microphone_types: str
+ method: str
+ references: tuple[str, ...]
+ expanded_uncertainty_1khz_db: float
+ expanded_uncertainty_10khz_db: float
+
+
+#: IEC 61094-8:2012 Table 1, "Calibration options for the reference
+#: microphone and associated typical measurement uncertainty", in the table's
+#: order. "This part of IEC 61094" is written out as IEC 61094-8.
+IEC61094_8_TABLE_1: Mapping[str, ReferenceCalibrationRow] = MappingProxyType(
+ {
+ "primary_free_field": ReferenceCalibrationRow(
+ "LS", "Primary free-field calibration", ("IEC 61094-3",), 0.25, 0.10
+ ),
+ "primary_pressure": ReferenceCalibrationRow(
+ "LS",
+ "Primary pressure calibration with the addition of a free-field to "
+ "pressure sensitivity level difference",
+ ("IEC 61094-2", "IEC/TS 61094-7"),
+ 0.12,
+ 0.4,
+ ),
+ "secondary_pressure": ReferenceCalibrationRow(
+ "LS",
+ "Secondary pressure calibration with the addition of a free-field to "
+ "pressure sensitivity level difference",
+ ("IEC 61094-5", "IEC/TS 61094-7"),
+ 0.15,
+ 0.5,
+ ),
+ "secondary_free_field": ReferenceCalibrationRow(
+ "LS and WS", "Secondary free-field calibration", ("IEC 61094-8",), 0.2, 0.5
+ ),
+ "electrostatic_actuator": ReferenceCalibrationRow(
+ "LS and WS",
+ "Electrostatic actuator calibration with the addition of a free-field "
+ "to actuator response level difference",
+ ("IEC 61094-6",),
+ 0.3,
+ 0.6,
+ ),
+ }
+)
+
+#: IEC 61094-5:2016 Table A.1, "Calculated corrections to be added to the
+#: sensitivity level of the WS3 microphone when using the arrangement in
+#: Figure A.4", keyed by frequency in Hz, in dB. They assume an LS2aP
+#: reference and a radially symmetrical field, and hold for the 0,5 mm
+#: separation of Figure A.4 only.
+IEC61094_5_TABLE_A1: Mapping[float, float] = MappingProxyType(
+ {
+ 1000.0: -0.004,
+ 1250.0: -0.006,
+ 1600.0: -0.009,
+ 2000.0: -0.015,
+ 2500.0: -0.023,
+ 3150.0: -0.036,
+ 4000.0: -0.059,
+ 5000.0: -0.092,
+ 6300.0: -0.146,
+ 8000.0: -0.235,
+ 10000.0: -0.367,
+ 12500.0: -0.572,
+ 16000.0: -0.933,
+ 20000.0: -1.443,
+ }
+)
+
+
+# ---------------------------------------------------------------------------
+# The environmental correction (IEC 61094-5 6.6, IEC 61094-8 7.6)
+# ---------------------------------------------------------------------------
+
+
+def _relative_humidity(value: float, name: str) -> float:
+ """Refuse a relative humidity that is not finite or not within 0 % to
+ 100 %.
+
+ :raises ValueError: for a humidity outside the closed range from dry to
+ saturated air.
+ """
+ if not math.isfinite(value) or not 0.0 <= value <= _SATURATED_PERCENT:
+ msg = f"'{name}' must be between 0 and 100."
+ raise ValueError(msg)
+ return float(value)
+
+
+@dataclass(frozen=True)
+class EnvironmentalSensitivityCorrection:
+ r"""The change of a microphone's sensitivity level between two sets of
+ environmental conditions, to first order in each.
+
+ .. math::
+
+ C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0)
+ + \delta_H\,(H - H_0)
+
+ with the static pressure coefficient :math:`\delta_p` in dB/kPa, the
+ temperature coefficient :math:`\delta_t` in dB/K and the humidity
+ coefficient :math:`\delta_H` in dB per percentage point, each one value
+ or one per frequency. Added to a sensitivity level valid at
+ :math:`(p_{s,0}, t_0, H_0)`, it gives the level at :math:`(p_s, t, H)`.
+
+ :ivar frequencies_hz: The frequencies, in Hz.
+ :ivar static_pressure_kpa: :math:`p_s`, where the level is wanted, in kPa.
+ :ivar temperature_c: :math:`t`, in °C.
+ :ivar relative_humidity_percent: :math:`H`, in %.
+ :ivar reference_static_pressure_kpa: :math:`p_{s,0}`, where the level is
+ known, in kPa.
+ :ivar reference_temperature_c: :math:`t_0`, in °C.
+ :ivar reference_relative_humidity_percent: :math:`H_0`, in %.
+ :ivar static_pressure_coefficient_db_per_kpa: :math:`\delta_p` at each
+ frequency, in dB/kPa.
+ :ivar temperature_coefficient_db_per_k: :math:`\delta_t`, in dB/K.
+ :ivar humidity_coefficient_db_per_percent: :math:`\delta_H`, in dB/%.
+ """
+
+ frequencies_hz: NDArray[np.float64]
+ static_pressure_kpa: float
+ temperature_c: float
+ relative_humidity_percent: float
+ reference_static_pressure_kpa: float
+ reference_temperature_c: float
+ reference_relative_humidity_percent: float
+ static_pressure_coefficient_db_per_kpa: NDArray[np.float64]
+ temperature_coefficient_db_per_k: NDArray[np.float64]
+ humidity_coefficient_db_per_percent: NDArray[np.float64]
+
+ def __post_init__(self) -> None:
+ """Refuse conditions that are no state of the air or coefficients that
+ are not finite, and publish the columns read-only.
+
+ :raises ValueError: for frequencies that are not positive and
+ increasing, a pressure that is not positive, a temperature that is
+ not finite or not above absolute zero, a relative humidity outside
+ 0 % to 100 %, or a coefficient column that is not one finite value
+ per frequency.
+ """
+ frequencies = _frequency_axis(self.frequencies_hz)
+ object.__setattr__(self, "frequencies_hz", read_only(frequencies.copy()))
+ for name in ("static_pressure_kpa", "reference_static_pressure_kpa"):
+ object.__setattr__(self, name, require_positive(getattr(self, name), name))
+ for name in ("temperature_c", "reference_temperature_c"):
+ value = require_above_absolute_zero(getattr(self, name), name)
+ object.__setattr__(self, name, value)
+ for name in (
+ "relative_humidity_percent",
+ "reference_relative_humidity_percent",
+ ):
+ object.__setattr__(
+ self, name, _relative_humidity(getattr(self, name), name)
+ )
+ for name in (
+ "static_pressure_coefficient_db_per_kpa",
+ "temperature_coefficient_db_per_k",
+ "humidity_coefficient_db_per_percent",
+ ):
+ column = _band_column(getattr(self, name), name, frequencies.size)
+ object.__setattr__(self, name, read_only(column.copy()))
+
+ @property
+ def static_pressure_term_db(self) -> NDArray[np.float64]:
+ r""":math:`\delta_p\,(p_s - p_{s,0})`, in dB."""
+ return self.static_pressure_coefficient_db_per_kpa * (
+ self.static_pressure_kpa - self.reference_static_pressure_kpa
+ )
+
+ @property
+ def temperature_term_db(self) -> NDArray[np.float64]:
+ r""":math:`\delta_t\,(t - t_0)`, in dB."""
+ return self.temperature_coefficient_db_per_k * (
+ self.temperature_c - self.reference_temperature_c
+ )
+
+ @property
+ def humidity_term_db(self) -> NDArray[np.float64]:
+ r""":math:`\delta_H\,(H - H_0)`, in dB."""
+ return self.humidity_coefficient_db_per_percent * (
+ self.relative_humidity_percent - self.reference_relative_humidity_percent
+ )
+
+ @property
+ def correction_db(self) -> NDArray[np.float64]:
+ r""":math:`C_\mathrm{env}`, the sum of the three terms, in dB."""
+ pressure = self.static_pressure_term_db
+ return pressure + self.temperature_term_db + self.humidity_term_db
+
+ def plot(
+ self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any
+ ) -> Axes:
+ r"""Plot :math:`C_\mathrm{env}` and its three terms against frequency.
+
+ :param ax: Existing axes to draw on, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the :math:`C_\mathrm{env}` curve.
+ :return: The axes. Requires matplotlib
+ (``pip install phonometry[plot]``).
+ """
+ from .._i18n import check_language
+ from .._plot.metrology import plot_environmental_sensitivity_correction
+
+ return plot_environmental_sensitivity_correction(
+ self, ax=ax, language=check_language(language), **kwargs
+ )
+
+
+def environmental_sensitivity_correction(
+ frequencies_hz: ArrayLike,
+ *,
+ static_pressure_kpa: float,
+ temperature_c: float,
+ relative_humidity_percent: float,
+ static_pressure_coefficient_db_per_kpa: ArrayLike,
+ temperature_coefficient_db_per_k: ArrayLike,
+ humidity_coefficient_db_per_percent: ArrayLike = 0.0,
+ reference_static_pressure_kpa: float = _REFERENCE_STATIC_PRESSURE_KPA,
+ reference_temperature_c: float = _REFERENCE_TEMPERATURE_C,
+ reference_relative_humidity_percent: float = _REFERENCE_RELATIVE_HUMIDITY_PERCENT,
+) -> EnvironmentalSensitivityCorrection:
+ r"""The correction of a sensitivity level from one set of environmental
+ conditions to another (IEC 61094-5 6.6, IEC 61094-8 7.6).
+
+ IEC 61094-8 7.6 asks for the reference microphone's sensitivity to be
+ corrected to the conditions of the test in every case. IEC 61094-5 6.6
+ asks for it when the two microphones are different models, and for two of
+ the same model allows the result to be referred instead to the conditions
+ at which the reference's calibration is valid. Both allow the result to be
+ referred to the reference conditions of clause 4 when reliable correction
+ data are available; neither prints a formula. This is the first-order
+ one, a coefficient times a deviation for each of the three conditions,
+
+ .. math::
+
+ C_\mathrm{env} = \delta_p\,(p_s - p_{s,0}) + \delta_t\,(t - t_0)
+ + \delta_H\,(H - H_0)
+
+ with the coefficients in the units IEC 61094-2:2009 Annex D gives them:
+ dB/kPa for the static pressure, dB/K for the temperature. Annex D of that
+ part puts the low-frequency static pressure coefficient of LS1P
+ microphones between -0,01 dB/kPa and -0,02 dB/kPa and of LS2P between
+ -0,003 dB/kPa and -0,008 dB/kPa, the temperature coefficient within
+ ±0,005 dB/K, and all three vary with frequency and from one microphone to
+ another, so they are the microphone's own and are required here. 6.5.3 of
+ the same part observes no influence of humidity on a laboratory standard
+ microphone, hence the default of 0 for its coefficient.
+
+ The reference conditions default to clause 4 of IEC 61094-5 and IEC
+ 61094-8, 101,325 kPa, 23,0 °C and 50 %. The correction adds to a level
+ known at the reference conditions to give it at the conditions of the
+ test; :func:`simultaneous_comparison` and :func:`sequential_comparison`
+ add it to the reference microphone's level (``reference_environment=``)
+ or subtract it from the result (``test_environment=``).
+
+ :param frequencies_hz: The frequencies, in Hz, increasing.
+ :param static_pressure_kpa: :math:`p_s`, the static pressure the level is
+ wanted at, in kPa.
+ :param temperature_c: :math:`t`, in °C.
+ :param relative_humidity_percent: :math:`H`, in %.
+ :param static_pressure_coefficient_db_per_kpa: :math:`\delta_p`, in dB/kPa,
+ one value or one per frequency.
+ :param temperature_coefficient_db_per_k: :math:`\delta_t`, in dB/K.
+ :param humidity_coefficient_db_per_percent: :math:`\delta_H`, in dB per
+ percentage point (Default: 0).
+ :param reference_static_pressure_kpa: :math:`p_{s,0}`, where the level is
+ known, in kPa (Default: 101,325).
+ :param reference_temperature_c: :math:`t_0`, in °C (Default: 23,0).
+ :param reference_relative_humidity_percent: :math:`H_0`, in % (Default:
+ 50).
+ :return: The :class:`EnvironmentalSensitivityCorrection`.
+ :raises ValueError: for frequencies that are not positive and increasing,
+ a pressure that is not positive, a temperature that is not finite or
+ not above absolute zero, a relative humidity outside 0 % to 100 %, a
+ coefficient that is not finite, or a coefficient column that is not
+ one value per frequency.
+ """
+ return EnvironmentalSensitivityCorrection(
+ frequencies_hz=np.asarray(frequencies_hz, dtype=np.float64),
+ static_pressure_kpa=static_pressure_kpa,
+ temperature_c=temperature_c,
+ relative_humidity_percent=relative_humidity_percent,
+ reference_static_pressure_kpa=reference_static_pressure_kpa,
+ reference_temperature_c=reference_temperature_c,
+ reference_relative_humidity_percent=reference_relative_humidity_percent,
+ static_pressure_coefficient_db_per_kpa=np.asarray(
+ static_pressure_coefficient_db_per_kpa, dtype=np.float64
+ ),
+ temperature_coefficient_db_per_k=np.asarray(
+ temperature_coefficient_db_per_k, dtype=np.float64
+ ),
+ humidity_coefficient_db_per_percent=np.asarray(
+ humidity_coefficient_db_per_percent, dtype=np.float64
+ ),
+ )
+
+
+# ---------------------------------------------------------------------------
+# The WS3 correction of the jig (IEC 61094-5 Table A.1)
+# ---------------------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class JigDiameterCorrection:
+ r"""The corrections of IEC 61094-5 Table A.1 at the frequencies asked for.
+
+ Added to the sensitivity level of a type WS3 microphone calibrated against
+ a type LS2aP reference in the jig of Figure A.4, they account for the
+ radial sensitivity of the two diaphragms and for the smaller one of the
+ test microphone. Their expanded uncertainty is 10 % of their value
+ (A.2 and the NOTE to Table A.1), a component of the budget of Table D.1.
+ None of the three places that give the 10 % states its coverage factor;
+ the library reads it as :math:`k = 2`, the factor 7.9 and D.2 report every
+ expanded uncertainty with.
+
+ :ivar frequencies_hz: The frequencies of the table asked for, in Hz.
+ :ivar correction_db: The correction at each, in dB.
+ """
+
+ frequencies_hz: NDArray[np.float64]
+ correction_db: NDArray[np.float64]
+
+ def __post_init__(self) -> None:
+ """Publish the columns read-only.
+
+ :raises ValueError: for columns of different lengths or values that
+ are not finite.
+ """
+ frequencies = _frequency_axis(self.frequencies_hz)
+ correction = _band_column(self.correction_db, "correction_db", frequencies.size)
+ object.__setattr__(self, "frequencies_hz", read_only(frequencies.copy()))
+ object.__setattr__(self, "correction_db", read_only(correction.copy()))
+
+ @property
+ def expanded_uncertainty_db(self) -> NDArray[np.float64]:
+ """The expanded uncertainty of each correction, one tenth of its
+ magnitude, in dB (read as :math:`k = 2`, 7.9 and D.2).
+ """
+ return _JIG_CORRECTION_RELATIVE_EXPANDED * np.abs(self.correction_db)
+
+ @property
+ def standard_uncertainty_db(self) -> NDArray[np.float64]:
+ """The standard uncertainty of each correction, the expanded one over
+ :math:`k = 2` (a reading of 7.9 and D.2, since the 10 % comes without
+ a coverage factor), in dB: the special-case component of Table D.1.
+ """
+ return self.expanded_uncertainty_db / _COVERAGE_FACTOR
+
+ def plot(
+ self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any
+ ) -> Axes:
+ """Plot the corrections with their expanded uncertainty.
+
+ :param ax: Existing axes to draw on, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the correction curve.
+ :return: The axes. Requires matplotlib
+ (``pip install phonometry[plot]``).
+ """
+ from .._i18n import check_language
+ from .._plot.metrology import plot_jig_diameter_correction
+
+ return plot_jig_diameter_correction(
+ self, ax=ax, language=check_language(language), **kwargs
+ )
+
+
+def _table_a1_value(frequency: float) -> float:
+ """The Table A.1 correction at a frequency the table prints.
+
+ :raises ValueError: for a frequency that is not one of its rows.
+ """
+ keys = np.array(sorted(IEC61094_5_TABLE_A1), dtype=np.float64)
+ nearest = float(keys[np.argmin(np.abs(np.log(keys / frequency)))])
+ if not math.isclose(frequency, nearest, rel_tol=_NOMINAL_TOLERANCE):
+ msg = (
+ f"{frequency:g} Hz is not a frequency of IEC 61094-5 Table A.1, which "
+ "prints the one-third octaves from 1 kHz to 20 kHz."
+ )
+ raise ValueError(msg)
+ return IEC61094_5_TABLE_A1[nearest]
+
+
+def jig_diameter_correction(
+ frequencies_hz: ArrayLike | None = None,
+) -> JigDiameterCorrection:
+ r"""The corrections of IEC 61094-5:2016 Table A.1 for a type WS3 microphone
+ against a type LS2aP reference in the jig of Figure A.4.
+
+ The table gives them at the preferred one-third-octave frequencies from
+ 1 kHz, -0,004 dB, to 20 kHz, -1,443 dB, calculated for a radially
+ symmetrical field and the 0,5 mm diaphragm separation of Figure A.4, "the
+ only one for which the corrections specified in Table A.1 are valid". A
+ frequency within 2 % of a row, such as an exact base-ten one-third-octave
+ frequency, reads as that row. Pass ``.correction_db`` to the comparison as
+ one of its ``corrections_db`` and ``.standard_uncertainty_db`` to the
+ budget as an additional component.
+
+ :param frequencies_hz: Frequencies of the table, in Hz, increasing
+ (Default: None, the 14 rows).
+ :return: The :class:`JigDiameterCorrection`.
+ :raises ValueError: for a frequency the table does not print.
+ """
+ frequencies = (
+ np.array(sorted(IEC61094_5_TABLE_A1), dtype=np.float64)
+ if frequencies_hz is None
+ else _frequency_axis(frequencies_hz)
+ )
+ correction = np.array([_table_a1_value(float(f)) for f in frequencies])
+ return JigDiameterCorrection(frequencies_hz=frequencies, correction_db=correction)
+
+
+# ---------------------------------------------------------------------------
+# The calibration: IEC 61094-5 D.2 in levels
+# ---------------------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class ComparisonCalibration:
+ r"""The sensitivity level of a microphone calibrated by comparison
+ (IEC 61094-5:2016 D.2 for a pressure field, IEC 61094-8:2012 for a free
+ field).
+
+ .. math::
+
+ L_\mathrm{test} = L_\mathrm{ref} + 20\lg R_V - 20\lg R_P + \sum_j C_j
+
+ :ivar frequencies_hz: The frequencies, in Hz.
+ :ivar reference_sensitivity_level_db: :math:`L_\mathrm{ref}`, the
+ reference microphone's sensitivity level as calibrated, in dB re
+ 1 V/Pa.
+ :ivar output_level_differences_db: :math:`20\lg R_V` of each
+ determination, one row per determination, in dB: the output of the
+ test microphone re that of the reference.
+ :ivar pressure_level_difference_db: :math:`20\lg R_P`, the effective sound
+ pressure on the test microphone re that on the reference, in dB.
+ :ivar corrections_db: The corrections :math:`C_j` added to the level, by
+ name, each in dB at every frequency.
+ :ivar field: ``"pressure"`` (IEC 61094-5) or ``"free_field"``
+ (IEC 61094-8).
+ :ivar excitation: ``"simultaneous"`` or ``"sequential"``.
+ :ivar expanded_uncertainty_db: The expanded uncertainty (:math:`k = 2`) of
+ the sensitivity level at each frequency, in dB, or ``None``.
+ """
+
+ frequencies_hz: NDArray[np.float64]
+ reference_sensitivity_level_db: NDArray[np.float64]
+ output_level_differences_db: NDArray[np.float64]
+ pressure_level_difference_db: NDArray[np.float64]
+ corrections_db: Mapping[str, NDArray[np.float64]]
+ field: str
+ excitation: str
+ expanded_uncertainty_db: NDArray[np.float64] | None = None
+
+ def __post_init__(self) -> None:
+ """Refuse columns that disagree or an unknown field or excitation, and
+ publish everything read-only.
+
+ :raises ValueError: for frequencies that are not positive and
+ increasing, a column or a correction that is not one finite value
+ per frequency, determinations that are not one row per
+ determination, a negative uncertainty, or an unknown field or
+ excitation.
+ """
+ require_choice(self.field, "field", _FIELDS)
+ require_choice(self.excitation, "excitation", _EXCITATIONS)
+ frequencies = _frequency_axis(self.frequencies_hz)
+ count = frequencies.size
+ object.__setattr__(self, "frequencies_hz", read_only(frequencies.copy()))
+ for name in ("reference_sensitivity_level_db", "pressure_level_difference_db"):
+ column = _band_column(getattr(self, name), name, count)
+ object.__setattr__(self, name, read_only(column.copy()))
+ rows = _determinations(
+ self.output_level_differences_db, "output_level_differences_db", count
+ )
+ object.__setattr__(self, "output_level_differences_db", read_only(rows.copy()))
+ corrections = {
+ str(name): read_only(
+ _band_column(value, f"corrections_db[{name!r}]", count).copy()
+ )
+ for name, value in self.corrections_db.items()
+ }
+ object.__setattr__(self, "corrections_db", MappingProxyType(corrections))
+ if self.expanded_uncertainty_db is not None:
+ uncertainty = _band_column(
+ self.expanded_uncertainty_db, "expanded_uncertainty_db", count
+ )
+ if np.any(uncertainty < 0.0):
+ msg = "ComparisonCalibration: 'expanded_uncertainty_db' must be non-negative."
+ raise ValueError(msg)
+ object.__setattr__(
+ self, "expanded_uncertainty_db", read_only(uncertainty.copy())
+ )
+
+ @property
+ def standard(self) -> str:
+ """The designation the calibration follows."""
+ return _STANDARDS[self.field]
+
+ @property
+ def determinations(self) -> int:
+ """The number of determinations averaged."""
+ return int(self.output_level_differences_db.shape[0])
+
+ @property
+ def output_level_difference_db(self) -> NDArray[np.float64]:
+ r""":math:`20\lg R_V`, the mean over the determinations, in dB."""
+ return np.mean(self.output_level_differences_db, axis=0)
+
+ @property
+ def correction_db(self) -> NDArray[np.float64]:
+ r""":math:`\sum_j C_j`, in dB (0 without corrections)."""
+ total = np.zeros(self.frequencies_hz.size)
+ for value in self.corrections_db.values():
+ total = total + value
+ return total
+
+ @property
+ def sensitivity_level_db(self) -> NDArray[np.float64]:
+ r""":math:`L_\mathrm{test}`, in dB re 1 V/Pa."""
+ return _compared_level_db(
+ self.reference_sensitivity_level_db,
+ self.output_level_difference_db,
+ self.pressure_level_difference_db,
+ self.correction_db,
+ )
+
+ @property
+ def sensitivity_mv_per_pa(self) -> NDArray[np.float64]:
+ r""":math:`M_\mathrm{test} = 10^{L_\mathrm{test}/20}` V/Pa, in mV/Pa."""
+ return 1000.0 * 10.0 ** (self.sensitivity_level_db / 20.0)
+
+ def plot(
+ self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any
+ ) -> Axes:
+ r"""Plot :math:`L_\mathrm{test}` and the reference's level against
+ frequency, with the expanded uncertainty as a band when it is known.
+
+ The reference's level is :math:`L_\mathrm{ref}`, or, in a free field
+ against a pressure-calibrated reference, :math:`L_\mathrm{ref}` plus
+ its free-field difference: the level the test microphone was
+ compared with.
+
+ :param ax: Existing axes to draw on, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the :math:`L_\mathrm{test}` curve.
+ :return: The axes. Requires matplotlib
+ (``pip install phonometry[plot]``).
+ """
+ from .._i18n import check_language
+ from .._plot.metrology import plot_comparison_calibration
+
+ return plot_comparison_calibration(
+ self, ax=ax, language=check_language(language), **kwargs
+ )
+
+
+def _frequencies_of(
+ correction: EnvironmentalSensitivityCorrection,
+ name: str,
+ frequencies: NDArray[np.float64],
+) -> None:
+ """Refuse an environmental correction made at other frequencies.
+
+ The coefficients are per frequency, so a correction made at as many
+ frequencies as the calibration but not the same ones would apply each
+ coefficient at the wrong frequency: the axes themselves have to agree,
+ not their lengths alone.
+
+ :raises ValueError: for a correction at another number of frequencies, or
+ at as many but not the same ones.
+ """
+ made = correction.frequencies_hz
+ if made.size != frequencies.size:
+ msg = (
+ f"'{name}' was made at {made.size} frequencies; the calibration "
+ f"has {frequencies.size}."
+ )
+ raise ValueError(msg)
+ same = np.isclose(made, frequencies, rtol=_SAME_FREQUENCY_REL_TOL, atol=0.0)
+ if not np.all(same):
+ first = int(np.flatnonzero(~same)[0])
+ msg = (
+ f"'{name}' was made at {made[first]:g} Hz where the calibration "
+ f"has {frequencies[first]:g} Hz; make it at the calibration's "
+ "frequencies."
+ )
+ raise ValueError(msg)
+
+
+def _calibration(
+ frequencies: NDArray[np.float64],
+ reference_sensitivity_level_db: ArrayLike,
+ output_level_differences_db: NDArray[np.float64],
+ *,
+ field: str,
+ excitation: str,
+ pressure_level_difference_db: ArrayLike,
+ corrections_db: Mapping[str, ArrayLike] | None,
+ reference_environment: EnvironmentalSensitivityCorrection | None,
+ test_environment: EnvironmentalSensitivityCorrection | None,
+ reference_free_field_difference_db: ArrayLike | None,
+ expanded_uncertainty_db: ArrayLike | None,
+) -> ComparisonCalibration:
+ """Gather the corrections by name and build the result.
+
+ The field is checked first, so that an unknown one is named as such and
+ not taken for a pressure calibration by the free-field check below.
+ """
+ field = require_choice(field, "field", _FIELDS)
+ named: dict[str, ArrayLike] = {}
+ if reference_free_field_difference_db is not None:
+ if field != "free_field":
+ msg = (
+ "'reference_free_field_difference_db' belongs to a free-field "
+ "calibration (IEC 61094-8 Table 1), not to a pressure one."
+ )
+ raise ValueError(msg)
+ named[_FREE_FIELD_DIFFERENCE] = reference_free_field_difference_db
+ if reference_environment is not None:
+ _frequencies_of(reference_environment, "reference_environment", frequencies)
+ named["reference environment"] = reference_environment.correction_db
+ if test_environment is not None:
+ _frequencies_of(test_environment, "test_environment", frequencies)
+ named["reference conditions"] = -test_environment.correction_db
+ for name, value in (corrections_db or {}).items():
+ if name in named:
+ msg = f"'corrections_db' repeats the correction {name!r}."
+ raise ValueError(msg)
+ named[str(name)] = value
+ return ComparisonCalibration(
+ frequencies_hz=frequencies,
+ reference_sensitivity_level_db=np.asarray(
+ reference_sensitivity_level_db, dtype=np.float64
+ ),
+ output_level_differences_db=output_level_differences_db,
+ pressure_level_difference_db=np.asarray(
+ pressure_level_difference_db, dtype=np.float64
+ ),
+ corrections_db=named, # type: ignore[arg-type]
+ field=field,
+ excitation=excitation,
+ expanded_uncertainty_db=(
+ None
+ if expanded_uncertainty_db is None
+ else np.asarray(expanded_uncertainty_db, dtype=np.float64)
+ ),
+ )
+
+
+def simultaneous_comparison(
+ frequencies_hz: ArrayLike,
+ reference_sensitivity_level_db: ArrayLike,
+ channel_difference_db: ArrayLike,
+ interchanged_channel_difference_db: ArrayLike | None = None,
+ *,
+ field: str = "pressure",
+ pressure_level_difference_db: ArrayLike = 0.0,
+ corrections_db: Mapping[str, ArrayLike] | None = None,
+ reference_environment: EnvironmentalSensitivityCorrection | None = None,
+ test_environment: EnvironmentalSensitivityCorrection | None = None,
+ reference_free_field_difference_db: ArrayLike | None = None,
+ expanded_uncertainty_db: ArrayLike | None = None,
+) -> ComparisonCalibration:
+ r"""Calibration by comparison with both microphones in the field at once
+ (IEC 61094-5:2016 5.1.2 and Annex C; IEC 61094-8:2012 5.3).
+
+ Each microphone is read on its own measuring channel. With the reference
+ on channel 1, the level reading difference between the channels is
+ Formula (C.1),
+
+ .. math::
+
+ L_\mathrm{C12} = (L_1 + L_\mathrm{m1} + L_\mathrm{d1} + L_\mathrm{WA})
+ - (L_2 + L_\mathrm{m2} + L_\mathrm{d1} + L_\mathrm{WB})
+
+ and after the microphones are interchanged, in the coupler ports and on
+ the preamplifiers, it is Formula (C.2). Their difference leaves the two
+ sensitivity levels alone, whatever the gains of the channels and the
+ asymmetry of the field, Formula (C.3):
+
+ .. math::
+
+ L_\mathrm{ref} - L_\mathrm{test}
+ = \tfrac12\left(L_\mathrm{C12} - L_\mathrm{C21}\right)
+
+ so :math:`20\lg R_V = -\tfrac12(L_\mathrm{C12} - L_\mathrm{C21})`, and the
+ sensitivity level of the test microphone follows by D.2. A pressure
+ calibration requires the interchange ("the microphones shall be
+ interchanged, and the measurement repeated", 5.1.2). A free-field one does
+ not (5.3 relies on a symmetrical field instead); without it, the channel
+ difference is taken as :math:`-20\lg R_V`, which assumes channels of equal
+ gain.
+
+ Each reading may be one value per frequency or a matrix of one row per
+ determination (the three repeats of D.2, say); the calibration averages
+ the determinations.
+
+ :param frequencies_hz: The frequencies, in Hz, increasing.
+ :param reference_sensitivity_level_db: :math:`L_\mathrm{ref}`, the
+ reference microphone's sensitivity level, in dB re 1 V/Pa, one value
+ or one per frequency. For a free-field calibration it is the
+ reference's free-field level, or its pressure level with
+ ``reference_free_field_difference_db``.
+ :param channel_difference_db: :math:`L_\mathrm{C12}`, the reading of
+ channel 1 less that of channel 2 with the reference on channel 1, in
+ dB.
+ :param interchanged_channel_difference_db: :math:`L_\mathrm{C21}`, the
+ same difference after the interchange, the test microphone on
+ channel 1, in dB (Default: None; required for ``field="pressure"``).
+ :param field: ``"pressure"`` (IEC 61094-5, default) or ``"free_field"``
+ (IEC 61094-8).
+ :param pressure_level_difference_db: :math:`20\lg R_P`, the effective
+ sound pressure on the test microphone re that on the reference, in dB
+ (Default: 0, the ratio reduced to unity).
+ :param corrections_db: Further corrections added to the level, by name,
+ in dB, such as ``jig_diameter_correction(f).correction_db`` (Default:
+ none).
+ :param reference_environment: The :class:`EnvironmentalSensitivityCorrection` that
+ takes the reference's level from the conditions of its calibration to
+ those of the test, added (Default: None).
+ :param test_environment: The :class:`EnvironmentalSensitivityCorrection` of the test
+ microphone from the reference conditions to those of the test,
+ subtracted to refer the result to the reference conditions (Default:
+ None, the result is at the conditions of the test).
+ :param reference_free_field_difference_db: The reference's free-field to
+ pressure sensitivity level difference (IEC/TS 61094-7), added to a
+ pressure-calibrated reference in a free-field calibration only, in dB;
+ the result carries it in ``corrections_db`` as ``"reference free-field
+ difference"`` (Default: None).
+ :param expanded_uncertainty_db: The expanded uncertainty (:math:`k = 2`)
+ of the result at each frequency, in dB, as
+ :func:`comparison_uncertainty_budget` gives it (Default: None).
+ :return: The :class:`ComparisonCalibration`.
+ :raises ValueError: for an unknown field, a pressure calibration without
+ the interchange, a free-field correction on a pressure calibration, an
+ environmental correction made at other frequencies, readings whose
+ numbers of determinations disagree, or any column that is not one
+ finite value per frequency.
+ """
+ field = require_choice(field, "field", _FIELDS)
+ frequencies = _frequency_axis(frequencies_hz)
+ count = frequencies.size
+ first = _determinations(channel_difference_db, "channel_difference_db", count)
+ if interchanged_channel_difference_db is None:
+ if field == "pressure":
+ msg = (
+ "A pressure calibration by simultaneous excitation needs the "
+ "reading after the microphones are interchanged (IEC 61094-5 "
+ "5.1.2); pass 'interchanged_channel_difference_db'."
+ )
+ raise ValueError(msg)
+ ratios = -first
+ else:
+ second = _determinations(
+ interchanged_channel_difference_db,
+ "interchanged_channel_difference_db",
+ count,
+ )
+ _common_rows(
+ {
+ "channel_difference_db": first,
+ "interchanged_channel_difference_db": second,
+ }
+ )
+ ratios = _interchange_level_difference_db(first, second)
+ return _calibration(
+ frequencies,
+ reference_sensitivity_level_db,
+ ratios,
+ field=field,
+ excitation="simultaneous",
+ pressure_level_difference_db=pressure_level_difference_db,
+ corrections_db=corrections_db,
+ reference_environment=reference_environment,
+ test_environment=test_environment,
+ reference_free_field_difference_db=reference_free_field_difference_db,
+ expanded_uncertainty_db=expanded_uncertainty_db,
+ )
+
+
+def sequential_comparison(
+ frequencies_hz: ArrayLike,
+ reference_sensitivity_level_db: ArrayLike,
+ reference_output_level_db: ArrayLike,
+ test_output_level_db: ArrayLike,
+ *,
+ reference_monitor_level_db: ArrayLike | None = None,
+ test_monitor_level_db: ArrayLike | None = None,
+ field: str = "pressure",
+ pressure_level_difference_db: ArrayLike = 0.0,
+ corrections_db: Mapping[str, ArrayLike] | None = None,
+ reference_environment: EnvironmentalSensitivityCorrection | None = None,
+ test_environment: EnvironmentalSensitivityCorrection | None = None,
+ reference_free_field_difference_db: ArrayLike | None = None,
+ expanded_uncertainty_db: ArrayLike | None = None,
+) -> ComparisonCalibration:
+ r"""Calibration by comparison with the microphones put in the field in
+ turn (IEC 61094-5:2016 5.1.3 and Annex B; IEC 61094-8:2012 5.2 and
+ Annex A).
+
+ The reference and the test microphone take the same place one after the
+ other. Either the exchange does not change the sound pressure
+ significantly, or any change is detected and corrected (IEC 61094-5
+ 5.1.3, IEC 61094-8 5.2), for example with a monitor microphone near the
+ source: the ratio of each microphone's output to the monitor's output,
+ and the quotient of the two ratios, "gives the ratio of the microphone
+ under test output voltage to the reference microphone output voltage,
+ corrected for any variation in the sound pressure generated by the
+ source" (IEC 61094-8 A.2). With :math:`V_\mathrm{ref}` and
+ :math:`V_\mathrm{mon,1}` the outputs of the reference and the monitor in
+ the first measurement, and :math:`V_\mathrm{test}` and
+ :math:`V_\mathrm{mon,2}` those of the second,
+
+ .. math::
+
+ 20\lg R_V = 20\lg\frac{V_\mathrm{test}}{V_\mathrm{mon,2}}
+ - 20\lg\frac{V_\mathrm{ref}}{V_\mathrm{mon,1}}
+
+ and without a monitor :math:`20\lg(V_\mathrm{test}/V_\mathrm{ref})`,
+ the plain difference of the two output levels. The sensitivity level of
+ the test microphone then follows by D.2.
+
+ Each reading may be one value per frequency or a matrix of one row per
+ determination; the calibration averages the determinations.
+
+ :param frequencies_hz: The frequencies, in Hz, increasing.
+ :param reference_sensitivity_level_db: :math:`L_\mathrm{ref}`, the
+ reference microphone's sensitivity level, in dB re 1 V/Pa.
+ :param reference_output_level_db: :math:`20\lg V_\mathrm{ref}`, the
+ reference microphone's output level, in dB re any voltage the test
+ microphone is read against too.
+ :param test_output_level_db: :math:`20\lg V_\mathrm{test}`, the test
+ microphone's output level, in dB re the same voltage.
+ :param reference_monitor_level_db: :math:`20\lg V_\mathrm{mon,1}`, the
+ monitor's output level in the measurement of the reference, in dB
+ (Default: None, no monitor).
+ :param test_monitor_level_db: :math:`20\lg V_\mathrm{mon,2}`, the
+ monitor's output level in the measurement of the test microphone, in
+ dB (Default: None; given with the other or not at all).
+ :param field: ``"pressure"`` (IEC 61094-5, default) or ``"free_field"``
+ (IEC 61094-8).
+ :param pressure_level_difference_db: :math:`20\lg R_P`, in dB (Default: 0).
+ :param corrections_db: Further corrections added to the level, by name,
+ in dB (Default: none).
+ :param reference_environment: The :class:`EnvironmentalSensitivityCorrection` of the
+ reference, added (Default: None).
+ :param test_environment: The :class:`EnvironmentalSensitivityCorrection` of the test
+ microphone, subtracted (Default: None).
+ :param reference_free_field_difference_db: The reference's free-field to
+ pressure sensitivity level difference (IEC/TS 61094-7), free field
+ only, in dB; carried in ``corrections_db`` as ``"reference free-field
+ difference"`` (Default: None).
+ :param expanded_uncertainty_db: The expanded uncertainty (:math:`k = 2`)
+ of the result at each frequency, in dB (Default: None).
+ :return: The :class:`ComparisonCalibration`.
+ :raises ValueError: for an unknown field, one monitor reading without the
+ other, a free-field correction on a pressure calibration, an
+ environmental correction made at other frequencies, readings whose
+ numbers of determinations disagree, or any column that is not one
+ finite value per frequency.
+ """
+ field = require_choice(field, "field", _FIELDS)
+ frequencies = _frequency_axis(frequencies_hz)
+ count = frequencies.size
+ if (reference_monitor_level_db is None) != (test_monitor_level_db is None):
+ msg = (
+ "Give the monitor reading taken with each microphone, "
+ "'reference_monitor_level_db' and 'test_monitor_level_db', or neither."
+ )
+ raise ValueError(msg)
+ readings = {
+ "reference_output_level_db": _determinations(
+ reference_output_level_db, "reference_output_level_db", count
+ ),
+ "test_output_level_db": _determinations(
+ test_output_level_db, "test_output_level_db", count
+ ),
+ }
+ if reference_monitor_level_db is not None and test_monitor_level_db is not None:
+ readings["reference_monitor_level_db"] = _determinations(
+ reference_monitor_level_db, "reference_monitor_level_db", count
+ )
+ readings["test_monitor_level_db"] = _determinations(
+ test_monitor_level_db, "test_monitor_level_db", count
+ )
+ _common_rows(readings)
+ ratios = _output_level_difference_db(
+ readings["test_output_level_db"],
+ readings["reference_output_level_db"],
+ readings.get("test_monitor_level_db"),
+ readings.get("reference_monitor_level_db"),
+ )
+ return _calibration(
+ frequencies,
+ reference_sensitivity_level_db,
+ np.atleast_2d(ratios),
+ field=field,
+ excitation="sequential",
+ pressure_level_difference_db=pressure_level_difference_db,
+ corrections_db=corrections_db,
+ reference_environment=reference_environment,
+ test_environment=test_environment,
+ reference_free_field_difference_db=reference_free_field_difference_db,
+ expanded_uncertainty_db=expanded_uncertainty_db,
+ )
+
+
+# ---------------------------------------------------------------------------
+# The uncertainty budget (IEC 61094-5 Annex D, IEC 61094-8 8.8 and Table 2)
+# ---------------------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class ComparisonUncertaintyBudget:
+ r"""The uncertainty budget of a comparison calibration at one frequency
+ (IEC 61094-5:2016 Annex D, IEC 61094-8:2012 8.8).
+
+ Every component is a standard uncertainty in dB and enters the level model
+ with a sensitivity of 1, so the combined standard uncertainty is their
+ root-sum-square (D.3), and the expanded uncertainty is :math:`k = 2`
+ times it (7.9 of the first part, 8.8 of the second).
+
+ :ivar frequency_hz: The frequency, in Hz.
+ :ivar field: ``"pressure"`` (Table D.1) or ``"free_field"`` (Table 2).
+ :ivar names: The key of each component, then the names of any additional
+ ones.
+ :ivar components: The component as the table prints it, or the name of an
+ additional one.
+ :ivar standard_uncertainties_db: :math:`u_i`, in dB.
+ :ivar uncertainty: The :class:`~phonometry.metrology.UncertaintyResult` of
+ the combination, which has to be the combination of
+ :attr:`standard_uncertainties_db`.
+ :ivar coverage_factor: :math:`k`, 2 by both parts.
+ """
+
+ frequency_hz: float
+ field: str
+ names: tuple[str, ...]
+ components: tuple[str, ...]
+ standard_uncertainties_db: NDArray[np.float64]
+ uncertainty: UncertaintyResult
+ coverage_factor: float = _COVERAGE_FACTOR
+
+ def __post_init__(self) -> None:
+ """Refuse columns that disagree, and publish them read-only.
+
+ :raises ValueError: for a frequency or coverage factor that is not
+ positive, an unknown field, columns that do not hold one entry per
+ component, a negative uncertainty, or an :attr:`uncertainty` that
+ is not the root-sum-square of the components.
+ """
+ require_positive(self.frequency_hz, "frequency_hz")
+ require_positive(self.coverage_factor, "coverage_factor")
+ require_choice(self.field, "field", _FIELDS)
+ count = len(self.names)
+ if len(self.components) != count:
+ msg = "ComparisonUncertaintyBudget: 'components' must hold one per name."
+ raise ValueError(msg)
+ column = np.array(self.standard_uncertainties_db, dtype=np.float64, ndmin=1)
+ if column.shape != (count,):
+ msg = (
+ "ComparisonUncertaintyBudget: 'standard_uncertainties_db' must hold "
+ f"one value per component ({count}); got shape {column.shape}."
+ )
+ raise ValueError(msg)
+ if not np.all(np.isfinite(column)) or np.any(column < 0.0):
+ msg = (
+ "ComparisonUncertaintyBudget: 'standard_uncertainties_db' must be "
+ "finite and non-negative."
+ )
+ raise ValueError(msg)
+ object.__setattr__(self, "standard_uncertainties_db", read_only(column))
+ combined = math.sqrt(float(np.sum(column**2)))
+ if not math.isclose(
+ float(self.uncertainty.combined_uncertainty),
+ combined,
+ rel_tol=_COMBINATION_REL_TOL,
+ abs_tol=_COMBINATION_ABS_TOL_DB,
+ ):
+ msg = (
+ "ComparisonUncertaintyBudget: 'uncertainty' must be the "
+ "root-sum-square of 'standard_uncertainties_db'; build the budget "
+ "with comparison_uncertainty_budget()."
+ )
+ raise ValueError(msg)
+
+ @property
+ def standard(self) -> str:
+ """The designation whose table the budget follows."""
+ return _STANDARDS[self.field]
+
+ @property
+ def combined_uncertainty_db(self) -> float:
+ r""":math:`u_\mathrm{c}`, the combined standard uncertainty, in dB."""
+ return float(self.uncertainty.combined_uncertainty)
+
+ @property
+ def expanded_uncertainty_db(self) -> float:
+ r""":math:`U = k\,u_\mathrm{c}`, in dB."""
+ return self.coverage_factor * self.combined_uncertainty_db
+
+ @property
+ def linear_combined_uncertainty_db(self) -> float:
+ r""":math:`u_\mathrm{c}` combined in linear form, in dB.
+
+ Each component converted to a relative uncertainty,
+ :math:`r_i = 10^{u_i/20} - 1`, combined in quadrature and converted
+ back, :math:`20\lg(1 + r_\mathrm{c})`: the "strict calculation" D.3
+ mentions and 8.8 prefers. For components of a few hundredths of a
+ decibel it differs from :attr:`combined_uncertainty_db` in the fourth
+ decimal at most, which is why both parts accept the logarithmic form.
+ """
+ relative = 10.0 ** (self.standard_uncertainties_db / 20.0) - 1.0
+ return 20.0 * math.log10(1.0 + math.sqrt(float(np.sum(relative**2))))
+
+ def plot(
+ self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any
+ ) -> Axes:
+ r"""Plot the standard uncertainty of each component, with
+ :math:`u_\mathrm{c}`, :math:`k` and :math:`U`.
+
+ :param ax: Existing axes to draw on, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to :meth:`~matplotlib.axes.Axes.barh`.
+ :return: The axes. Requires matplotlib
+ (``pip install phonometry[plot]``).
+ """
+ from .._i18n import check_language
+ from .._plot.metrology import plot_comparison_budget
+
+ return plot_comparison_budget(
+ self, ax=ax, language=check_language(language), **kwargs
+ )
+
+
+def _component(value: float | Quantity, name: str) -> Quantity:
+ """A component as a :class:`Quantity` of estimate 0.
+
+ :raises ValueError: for a value that is negative or not finite.
+ """
+ if isinstance(value, Quantity):
+ return Quantity(0.0, value.uncertainty, value.distribution, name=name)
+ number = float(value)
+ if not (math.isfinite(number) and number >= 0.0):
+ msg = (
+ f"The standard uncertainty of {name!r} must be finite and "
+ f"non-negative; got {value!r}."
+ )
+ raise ValueError(msg)
+ return Quantity(0.0, number, name=name)
+
+
+def comparison_uncertainty_budget(
+ standard_uncertainties_db: Mapping[str, float | Quantity],
+ *,
+ frequency_hz: float,
+ field: str = "pressure",
+ additional_components: Sequence[Quantity] = (),
+ coverage_factor: float = _COVERAGE_FACTOR,
+) -> ComparisonUncertaintyBudget:
+ r"""The uncertainty budget of a comparison calibration at one frequency
+ (IEC 61094-5:2016 Annex D; IEC 61094-8:2012 8.8 and Table 2).
+
+ Each component is given as a standard uncertainty in dB, the column
+ Table D.1 prints, or as a :class:`~phonometry.metrology.Quantity` whose
+ standard uncertainty it takes: ``metrology.rectangular(0, 0.03)`` for a
+ semi-range of 0,03 dB, say. They are keyed by the names of
+ :data:`IEC61094_5_TABLE_D1` for a pressure calibration and of
+ :data:`IEC61094_8_TABLE_2` for a free-field one; neither table is
+ exhaustive (D.1, 8.1), so a component may be left out and
+ ``additional_components`` adds the ones a set-up needs, such as the
+ diameter correction of a WS3 microphone in the jig (Table D.1, special
+ cases). The combination is :func:`~phonometry.metrology.combine_uncertainty`
+ on the level model, every component with a sensitivity of 1, which is the
+ root-sum-square of D.3; the expanded uncertainty is :math:`k = 2` times
+ it, the coverage factor 7.9 and D.2 of the first part and 8.8 of the
+ second report with.
+
+ With the eight components of Table D.1 at 2 kHz the combined standard
+ uncertainty is 0,0437 dB and the expanded one 0,087 dB. D.3 prints
+ 0,040 dB and 0,08 dB, which are not the root-sum-square of its own
+ components (an erratum; see ``docs/ERRATA.md``).
+
+ :param standard_uncertainties_db: The components, keyed by name, each a
+ standard uncertainty in dB or a :class:`~phonometry.metrology.Quantity`.
+ :param frequency_hz: The frequency the budget is for, in Hz.
+ :param field: ``"pressure"`` (Table D.1, default) or ``"free_field"``
+ (Table 2).
+ :param additional_components: Further components, as named
+ :class:`~phonometry.metrology.Quantity` objects (Default: none).
+ :param coverage_factor: :math:`k` (Default: 2).
+ :return: The :class:`ComparisonUncertaintyBudget`.
+ :raises ValueError: for a key that is not a component of the field's
+ table, an additional component without a name or with the name of
+ another component, no component at all, or a value that is negative
+ or not finite.
+ """
+ field = require_choice(field, "field", _FIELDS)
+ table = _BUDGET_TABLES[field]
+ unknown = [key for key in standard_uncertainties_db if key not in table]
+ if unknown:
+ msg = (
+ f"{unknown} are not components of {_STANDARDS[field]}'s table; the "
+ f"keys are {tuple(table)}. Pass other components through "
+ "'additional_components'."
+ )
+ raise ValueError(msg)
+ names: list[str] = []
+ components: list[str] = []
+ quantities: list[Quantity] = []
+ for key, row in table.items():
+ if key in standard_uncertainties_db:
+ quantities.append(_component(standard_uncertainties_db[key], key))
+ names.append(key)
+ components.append(row.component)
+ for extra in additional_components:
+ if not extra.name or extra.name in names:
+ msg = (
+ "Each additional component needs a name of its own; got "
+ f"{extra.name!r}."
+ )
+ raise ValueError(msg)
+ quantities.append(_component(extra, extra.name))
+ names.append(extra.name)
+ components.append(extra.name)
+ if not quantities:
+ msg = "The budget needs at least one component."
+ raise ValueError(msg)
+
+ def model(*values: float) -> float:
+ return float(sum(values))
+
+ result = combine_uncertainty(model, quantities)
+ return ComparisonUncertaintyBudget(
+ frequency_hz=require_positive(frequency_hz, "frequency_hz"),
+ field=field,
+ names=tuple(names),
+ components=tuple(components),
+ standard_uncertainties_db=np.array([q.uncertainty for q in quantities]),
+ uncertainty=result,
+ coverage_factor=require_positive(coverage_factor, "coverage_factor"),
+ )
+
+
+# ---------------------------------------------------------------------------
+# The effective free-field region of a time window (IEC 61094-8 B.1)
+# ---------------------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class FreeFieldRegion:
+ r"""The effective free-field region of a time-selective calibration
+ (IEC 61094-8:2012 B.1, Formula (B.1) and Figure B.1).
+
+ A prolate spheroid generated by an ellipse with the acoustic centres of
+ the source and of the microphone at its foci, :math:`d` apart, and the
+ major diameter :math:`A = d + \tau c`. A reflection from any point on its
+ surface arrives :math:`\tau` after the direct sound, at the end of the
+ window; the device measured is inside, and any reflecting surface or
+ obstacle has to be outside.
+
+ :ivar source_distance_m: :math:`d`, the source to receiver separation, in
+ m.
+ :ivar window_time_s: :math:`\tau`, from the arrival of the sound at the
+ microphone to the end of the window, in s.
+ :ivar speed_of_sound: :math:`c` at the conditions of the test, in m/s.
+ """
+
+ source_distance_m: float
+ window_time_s: float
+ speed_of_sound: float
+
+ def __post_init__(self) -> None:
+ """Refuse a separation, window or speed that is not positive.
+
+ :raises ValueError: for any of the three not finite and positive.
+ """
+ for name in ("source_distance_m", "window_time_s", "speed_of_sound"):
+ object.__setattr__(self, name, require_positive(getattr(self, name), name))
+
+ @property
+ def major_axis_m(self) -> float:
+ r""":math:`A = d + \tau c`, Formula (B.1), in m."""
+ return self.source_distance_m + self.window_time_s * self.speed_of_sound
+
+ @property
+ def semi_minor_axis_m(self) -> float:
+ r""":math:`b = \sqrt{(A/2)^2 - (d/2)^2}`, the radius of the region at
+ mid-way between the source and the microphone, in m: the clearance a
+ surface parallel to the axis needs to be outside it.
+ """
+ half_major = self.major_axis_m / 2.0
+ half_distance = self.source_distance_m / 2.0
+ return math.sqrt(half_major**2 - half_distance**2)
+
+ @property
+ def rod_clearance_m(self) -> float:
+ r""":math:`(A - d)/2 = \tau c/2`, how far the region reaches behind the
+ microphone along the axis, in m: the mounting rod "should be
+ sufficiently long so that the end opposite to the microphone is
+ completely outside" it (B.1).
+ """
+ return (self.major_axis_m - self.source_distance_m) / 2.0
+
+ def plot(
+ self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any
+ ) -> Axes:
+ """Plot the region in a plane through the axis, as Figure B.1 draws it.
+
+ :param ax: Existing axes to draw on, or ``None`` to create a figure.
+ :param language: Label language, ``"en"`` (default) or ``"es"``.
+ :param kwargs: Forwarded to the boundary of the region.
+ :return: The axes. Requires matplotlib
+ (``pip install phonometry[plot]``).
+ """
+ from .._i18n import check_language
+ from .._plot.metrology import plot_free_field_region
+
+ return plot_free_field_region(
+ self, ax=ax, language=check_language(language), **kwargs
+ )
+
+
+def free_field_region(
+ source_distance_m: float,
+ window_time_s: float,
+ *,
+ temperature_c: float = _REFERENCE_TEMPERATURE_C,
+ static_pressure_kpa: float = _REFERENCE_STATIC_PRESSURE_KPA,
+ relative_humidity_percent: float = _REFERENCE_RELATIVE_HUMIDITY_PERCENT,
+) -> FreeFieldRegion:
+ r"""The effective free-field region of a time window (IEC 61094-8:2012
+ B.1).
+
+ Formula (B.1), :math:`A = d + \tau c`, with :math:`c` "the speed of sound
+ at the prevailing environmental conditions", which is taken from the
+ IEC 61094-2:2009 Annex F air of :func:`~phonometry.fluids.air`. The
+ conditions default to the reference conditions of clause 4, 23,0 °C,
+ 101,325 kPa and 50 %.
+
+ :param source_distance_m: :math:`d`, the separation of the acoustic
+ centres of the source and the microphone, in m.
+ :param window_time_s: :math:`\tau`, from the arrival of the sound at the
+ microphone to the end of the time window, in s.
+ :param temperature_c: The air temperature, in °C (Default: 23,0).
+ :param static_pressure_kpa: The static pressure, in kPa (Default:
+ 101,325).
+ :param relative_humidity_percent: The relative humidity, in % (Default:
+ 50).
+ :return: The :class:`FreeFieldRegion`.
+ :raises ValueError: for a separation or window that is not positive, or
+ conditions Annex F refuses.
+ """
+ from ..fluids.air import air
+
+ pressure_kpa = require_positive(static_pressure_kpa, "static_pressure_kpa")
+ medium = air(
+ temperature_c=temperature_c,
+ static_pressure_pa=1000.0 * pressure_kpa,
+ relative_humidity_percent=relative_humidity_percent,
+ )
+ return FreeFieldRegion(
+ source_distance_m=source_distance_m,
+ window_time_s=window_time_s,
+ speed_of_sound=medium.speed_of_sound,
+ )
diff --git a/src/phonometry/metrology/random_incidence.py b/src/phonometry/metrology/random_incidence.py
index 1dae68685..80f9089b4 100644
--- a/src/phonometry/metrology/random_incidence.py
+++ b/src/phonometry/metrology/random_incidence.py
@@ -80,7 +80,11 @@
prints both the directivity factor and that difference for a type LS2aP/LS2F
laboratory standard microphone, one of the two types Annex B
recommends for the reference (the other is LS2bP);
-:data:`IEC61183_TABLE_B1` holds it and supplies them by default.
+:data:`IEC61183_TABLE_B1` holds it and supplies them by default. The method is
+a sequential comparison calibration without a monitor, so Formula (8) and the
+sum of Formulas (9) to (11) are computed by the level model of IEC 61094-5 D.2
+in :mod:`phonometry.metrology.comparison_calibration`, the one the microphone
+calibrations by comparison use.
Two readings the text leaves to the implementer
-----------------------------------------------
@@ -135,6 +139,7 @@
require_positive_array,
)
from .._internal.warnings import PhonometryWarning
+from .comparison_calibration import _compared_level_db, _output_level_difference_db
if TYPE_CHECKING:
from collections.abc import Mapping
@@ -1106,7 +1111,9 @@ def level_difference_db(self) -> NDArray[np.float64]:
r""":math:`\Delta G_\mathrm{D} = L_\mathrm{D} - L_\mathrm{D,ref}`, in dB
(Formula (8)).
"""
- return self.indicated_level_db - self.reference_indicated_level_db
+ return _output_level_difference_db(
+ self.indicated_level_db, self.reference_indicated_level_db
+ )
@property
def reference_diffuse_field_level_db(self) -> NDArray[np.float64]:
@@ -1120,7 +1127,9 @@ def diffuse_field_level_db(self) -> NDArray[np.float64]:
r""":math:`G_\mathrm{D}`, the diffuse-field sensitivity level of the
instrument under test, in dB (Formulas (9) to (11)).
"""
- return self.level_difference_db + self.reference_diffuse_field_level_db
+ return _compared_level_db(
+ self.reference_diffuse_field_level_db, self.level_difference_db
+ )
def plot(
self,
diff --git a/tests/metrology/test_comparison_calibration.py b/tests/metrology/test_comparison_calibration.py
new file mode 100644
index 000000000..c23a4c38d
--- /dev/null
+++ b/tests/metrology/test_comparison_calibration.py
@@ -0,0 +1,960 @@
+# Copyright (c) 2026. Jose Manuel Requena Plens
+"""IEC 61094-5:2016 and IEC 61094-8:2012: microphone calibration by comparison.
+
+The oracle is the printed page: IEC 61094-5:2016 Table A.1 on folio 15 (PDF
+page 17), Annex C on folio 18 (PDF page 20) and Annex D on folios 19 to 21
+(PDF pages 21 to 23); BS EN 61094-8:2012 Table 1 on folio 12 (PDF page 14),
+Table 2 on folio 17 (PDF page 19) and B.1 on folio 23 (PDF page 25). The
+printed values are in ``tests/reference_data``. The measurement models print
+no worked example, so they are checked against the equations they come from:
+readings built from known sensitivities, gains and fields by Formulas (C.1)
+and (C.2), or by the monitor ratios of IEC 61094-8 A.2, have to give back the
+sensitivity those quantities define.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import math
+import re
+import types
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+
+import matplotlib.pyplot as plt
+import numpy as np
+import pytest
+import reference_data as ref
+
+from phonometry import fluids, metrology
+from phonometry.metrology import comparison_calibration as cc
+from phonometry.metrology import random_incidence
+
+_F = np.array([250.0, 1000.0, 2000.0, 4000.0, 8000.0])
+
+#: A reference and a test microphone, sensitivity levels in dB re 1 V/Pa.
+_L_REF = np.array([-38.02, -38.00, -37.98, -37.95, -38.10])
+_L_TEST = np.array([-26.40, -26.35, -26.30, -26.10, -25.80])
+
+
+def _d1_budget(**kwargs: object) -> metrology.ComparisonUncertaintyBudget:
+ return metrology.comparison_uncertainty_budget(
+ ref.IEC61094_5_TABLE_D1_STANDARD_DB,
+ frequency_hz=2000.0,
+ **kwargs, # type: ignore[arg-type]
+ )
+
+
+def _interchange_readings(
+ gain_1: float = 0.7, gain_2: float = -1.3, asymmetry: float = 0.04
+) -> tuple[np.ndarray, np.ndarray]:
+ """Formulas (C.1) and (C.2) for the reference as microphone 1.
+
+ Two channels of different gain, a source that drifts between the two
+ configurations and a field that is not the same at the two positions:
+ none of it may reach the result.
+ """
+ level_1 = 94.0 + 0.2 * np.log10(_F / 1000.0)
+ level_2 = level_1 + 0.3
+ at_a = asymmetry
+ at_b = -asymmetry / 2.0
+ first = (_L_REF + gain_1 + level_1 + at_a) - (_L_TEST + gain_2 + level_1 + at_b)
+ second = (_L_TEST + gain_1 + level_2 + at_a) - (_L_REF + gain_2 + level_2 + at_b)
+ return first, second
+
+
+# --------------------------------------------------------------------------
+# The printed tables
+# --------------------------------------------------------------------------
+def test_table_a1_is_the_printed_table() -> None:
+ table = metrology.IEC61094_5_TABLE_A1
+ assert isinstance(table, types.MappingProxyType)
+ printed = {1000.0 * khz: value for khz, value in ref.IEC61094_5_TABLE_A1_DB.items()}
+ assert tuple(table) == tuple(printed)
+ for frequency, value in printed.items():
+ assert table[frequency] == pytest.approx(value, abs=1e-12)
+
+
+def test_table_d1_standard_uncertainties_follow_from_their_stated_values() -> None:
+ """Each of the seven rows that state a value: the semi-range or expanded
+ value over its divisor, rounded to the three decimals the table prints.
+ """
+ table = metrology.IEC61094_5_TABLE_D1
+ assert tuple(table) == tuple(ref.IEC61094_5_TABLE_D1_STANDARD_DB)
+ assert len(ref.IEC61094_5_TABLE_D1_STATED) == 7
+ for key, (stated, divisor) in ref.IEC61094_5_TABLE_D1_STATED.items():
+ row = table[key]
+ assert row.stated_db == pytest.approx(stated, rel=1e-12)
+ assert row.divisor == pytest.approx(divisor, rel=1e-12)
+ printed = ref.IEC61094_5_TABLE_D1_STANDARD_DB[key]
+ assert row.standard_uncertainty_db == pytest.approx(printed, abs=1e-12)
+ assert round(stated / divisor, 3) == pytest.approx(printed, abs=1e-12)
+
+
+def test_table_d1_repeatability_is_printed_as_a_standard_uncertainty() -> None:
+ """The repeatability row states no value, only the standard uncertainty in
+ its column.
+ """
+ row = metrology.IEC61094_5_TABLE_D1["repeatability"]
+ assert row.stated_db is None
+ assert row.divisor is None
+ printed = ref.IEC61094_5_TABLE_D1_STANDARD_DB["repeatability"]
+ assert row.standard_uncertainty_db == pytest.approx(printed, abs=1e-12)
+
+
+def test_table_1_and_table_2_of_61094_8_are_the_printed_tables() -> None:
+ assert tuple(metrology.IEC61094_8_TABLE_1) == tuple(ref.IEC61094_8_TABLE_1_DB)
+ for key, (at_1k, at_10k) in ref.IEC61094_8_TABLE_1_DB.items():
+ row = metrology.IEC61094_8_TABLE_1[key]
+ assert row.expanded_uncertainty_1khz_db == pytest.approx(at_1k, abs=1e-12)
+ assert row.expanded_uncertainty_10khz_db == pytest.approx(at_10k, abs=1e-12)
+ text = {
+ key: (row.microphone_types, row.method, row.references)
+ for key, row in metrology.IEC61094_8_TABLE_1.items()
+ }
+ assert text == ref.IEC61094_8_TABLE_1_TEXT
+ table = metrology.IEC61094_8_TABLE_2
+ assert tuple(table) == tuple(ref.IEC61094_8_TABLE_2_SOURCES)
+ assert {key: row.component for key, row in table.items()} == (
+ ref.IEC61094_8_TABLE_2_SOURCES
+ )
+ assert {key: row.subclauses for key, row in table.items()} == (
+ ref.IEC61094_8_TABLE_2_SUBCLAUSES
+ )
+ assert all(row.standard_uncertainty_db is None for row in table.values())
+
+
+def test_published_tables_are_immutable() -> None:
+ for table in (
+ metrology.IEC61094_5_TABLE_A1,
+ metrology.IEC61094_5_TABLE_D1,
+ metrology.IEC61094_8_TABLE_1,
+ metrology.IEC61094_8_TABLE_2,
+ ):
+ assert isinstance(table, types.MappingProxyType)
+ row = metrology.IEC61094_5_TABLE_D1["drift"]
+ with pytest.raises(dataclasses.FrozenInstanceError):
+ row.stated_db = 0.0 # type: ignore[misc]
+
+
+# --------------------------------------------------------------------------
+# Annex D: the budget
+# --------------------------------------------------------------------------
+def test_d3_combines_the_printed_column_to_0_0437_not_0_040() -> None:
+ """D.3: the root-sum-square of Table D.1 is 0,0437 dB; the page prints
+ 0,040 dB and 0,08 dB, an erratum.
+ """
+ budget = _d1_budget()
+ assert budget.combined_uncertainty_db == pytest.approx(
+ ref.IEC61094_5_D3_COMBINED_DB, abs=5e-5
+ )
+ assert budget.expanded_uncertainty_db == pytest.approx(
+ ref.IEC61094_5_D3_EXPANDED_DB, abs=5e-4
+ )
+ assert budget.coverage_factor == 2.0
+ gap = budget.combined_uncertainty_db - ref.IEC61094_5_D3_PRINTED_COMBINED_DB
+ assert gap > 0.003
+
+
+def test_d3_linear_and_logarithmic_combination_are_essentially_the_same() -> None:
+ """The strict calculation of D.3 is 0,043 614 dB, 0,000 055 dB below the
+ root-sum-square in decibels: essentially the same, and not the same.
+ """
+ budget = _d1_budget()
+ assert budget.linear_combined_uncertainty_db == pytest.approx(
+ ref.IEC61094_5_D3_LINEAR_COMBINED_DB, abs=1e-6
+ )
+ assert budget.linear_combined_uncertainty_db < budget.combined_uncertainty_db
+ difference = budget.linear_combined_uncertainty_db - budget.combined_uncertainty_db
+ assert abs(difference) < 1e-4
+
+
+def test_budget_takes_quantities_and_additional_components() -> None:
+ jig = metrology.jig_diameter_correction([8000.0])
+ budget = metrology.comparison_uncertainty_budget(
+ {
+ "reference": metrology.rectangular(0.0, 0.03),
+ "repeatability": 0.02,
+ },
+ frequency_hz=8000.0,
+ additional_components=[
+ metrology.Quantity(
+ 0.0, float(jig.standard_uncertainty_db[0]), name="WS3 diameter"
+ )
+ ],
+ )
+ assert budget.names == ("reference", "repeatability", "WS3 diameter")
+ assert budget.components[0] == "Sensitivity of reference microphone"
+ expected = math.hypot(0.03 / math.sqrt(3.0), 0.02, 0.05 * 0.235)
+ assert budget.combined_uncertainty_db == pytest.approx(expected, rel=1e-12)
+ assert budget.standard == "IEC 61094-5:2016"
+
+
+def test_free_field_budget_takes_table_2_keys() -> None:
+ budget = metrology.comparison_uncertainty_budget(
+ {"reference": 0.1, "free_field": 0.05, "positioning": 0.03},
+ frequency_hz=4000.0,
+ field="free_field",
+ )
+ assert budget.names == ("reference", "positioning", "free_field")
+ assert budget.standard == "IEC 61094-8:2012"
+ assert budget.expanded_uncertainty_db == pytest.approx(
+ 2.0 * math.sqrt(0.1**2 + 0.05**2 + 0.03**2), rel=1e-12
+ )
+
+
+def test_budget_refuses_a_component_of_the_other_table() -> None:
+ components = {"drift": 0.017}
+ with pytest.raises(ValueError, match=r"not components of IEC 61094-8"):
+ metrology.comparison_uncertainty_budget(
+ components, frequency_hz=1000.0, field="free_field"
+ )
+
+
+def test_budget_refuses_an_unnamed_additional_component() -> None:
+ extra = [metrology.Quantity(0.0, 0.01)]
+ with pytest.raises(ValueError, match=r"needs a name of its own"):
+ metrology.comparison_uncertainty_budget(
+ {"reference": 0.025}, frequency_hz=1000.0, additional_components=extra
+ )
+
+
+def test_budget_refuses_a_negative_uncertainty() -> None:
+ components = {"reference": -0.01}
+ with pytest.raises(
+ ValueError, match=r"'reference' must be finite and non-negative"
+ ):
+ metrology.comparison_uncertainty_budget(components, frequency_hz=1000.0)
+
+
+def test_budget_refuses_no_component() -> None:
+ with pytest.raises(ValueError, match=r"at least one component"):
+ metrology.comparison_uncertainty_budget({}, frequency_hz=1000.0)
+
+
+def test_budget_refuses_a_combination_its_columns_do_not_give() -> None:
+ budget = _d1_budget()
+ wrong = budget.standard_uncertainties_db * 2.0
+ with pytest.raises(ValueError, match=r"root-sum-square"):
+ dataclasses.replace(budget, standard_uncertainties_db=wrong)
+
+
+# --------------------------------------------------------------------------
+# Annex C and D.2: simultaneous excitation
+# --------------------------------------------------------------------------
+def test_interchange_cancels_channel_gains_source_drift_and_asymmetry() -> None:
+ first, second = _interchange_readings()
+ result = metrology.simultaneous_comparison(_F, _L_REF, first, second)
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+ assert result.excitation == "simultaneous"
+ assert result.standard == "IEC 61094-5:2016"
+
+
+def test_d2_in_linear_form_is_the_level_form() -> None:
+ """M_test = M_ref R_V / R_P, D.2, against the level model."""
+ m_ref = 10.0 ** (_L_REF / 20.0)
+ r_v = np.array([3.9, 3.95, 4.0, 4.1, 4.3])
+ r_p = np.array([1.001, 1.0, 0.999, 1.002, 1.01])
+ first = np.zeros(_F.size)
+ second = 2.0 * 20.0 * np.log10(r_v)
+ result = metrology.simultaneous_comparison(
+ _F,
+ _L_REF,
+ first,
+ second,
+ pressure_level_difference_db=20.0 * np.log10(r_p),
+ )
+ np.testing.assert_allclose(
+ result.sensitivity_mv_per_pa, 1000.0 * m_ref * r_v / r_p, rtol=1e-12
+ )
+
+
+def test_repeats_are_averaged_and_counted() -> None:
+ first, second = _interchange_readings()
+ noise = np.array([[0.004], [-0.002], [-0.002]])
+ result = metrology.simultaneous_comparison(
+ _F, _L_REF, first + noise, second - noise
+ )
+ assert result.determinations == 3
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+
+
+def test_pressure_calibration_refuses_to_skip_the_interchange() -> None:
+ first, _ = _interchange_readings()
+ with pytest.raises(ValueError, match=r"IEC 61094-5 5\.1\.2"):
+ metrology.simultaneous_comparison(_F, _L_REF, first)
+
+
+def test_free_field_simultaneous_without_interchange_reads_the_difference() -> None:
+ difference = _L_REF - _L_TEST
+ result = metrology.simultaneous_comparison(
+ _F, _L_REF, difference, field="free_field"
+ )
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+ assert result.standard == "IEC 61094-8:2012"
+
+
+def test_repeats_that_disagree_in_number_are_refused() -> None:
+ first, second = _interchange_readings()
+ rows_first = np.vstack([first, first])
+ rows_second = np.vstack([second, second, second])
+ with pytest.raises(ValueError, match=r"same number of determinations"):
+ metrology.simultaneous_comparison(_F, _L_REF, rows_first, rows_second)
+
+
+# --------------------------------------------------------------------------
+# Sequential excitation and the monitor microphone
+# --------------------------------------------------------------------------
+def test_monitor_ratio_cancels_a_drifting_source() -> None:
+ """IEC 61094-8 A.2: the quotient of the two ratios to the monitor."""
+ field_ref = 94.0 + np.array([0.0, 0.1, -0.1, 0.05, 0.2])
+ field_test = field_ref + np.array([0.3, -0.2, 0.15, 0.4, -0.35])
+ monitor_gain = -12.5
+ result = metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ _L_REF + field_ref,
+ _L_TEST + field_test,
+ reference_monitor_level_db=monitor_gain + field_ref,
+ test_monitor_level_db=monitor_gain + field_test,
+ field="free_field",
+ )
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+ assert result.excitation == "sequential"
+
+
+def test_without_a_monitor_the_readings_are_differenced() -> None:
+ result = metrology.sequential_comparison(_F, _L_REF, _L_REF + 90.0, _L_TEST + 90.0)
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+
+
+def test_one_monitor_reading_without_the_other_is_refused() -> None:
+ reference_output = _L_REF + 94.0
+ test_output = _L_TEST + 94.0
+ with pytest.raises(ValueError, match=r"or neither"):
+ metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ reference_output,
+ test_output,
+ reference_monitor_level_db=80.0,
+ )
+
+
+# --------------------------------------------------------------------------
+# Corrections
+# --------------------------------------------------------------------------
+def test_environmental_correction_is_first_order_in_each_condition() -> None:
+ env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=97.0,
+ temperature_c=26.0,
+ relative_humidity_percent=40.0,
+ static_pressure_coefficient_db_per_kpa=[-0.005, -0.005, -0.006, -0.008, 0.01],
+ temperature_coefficient_db_per_k=0.003,
+ humidity_coefficient_db_per_percent=0.0001,
+ )
+ pressure = np.array([-0.005, -0.005, -0.006, -0.008, 0.01]) * (97.0 - 101.325)
+ np.testing.assert_allclose(env.static_pressure_term_db, pressure, rtol=1e-12)
+ np.testing.assert_allclose(env.temperature_term_db, 0.009, rtol=1e-12)
+ np.testing.assert_allclose(env.humidity_term_db, -0.001, rtol=1e-12)
+ np.testing.assert_allclose(env.correction_db, pressure + 0.008, rtol=1e-12)
+ temperature, pressure_kpa, humidity = ref.IEC61094_REFERENCE_CONDITIONS
+ assert env.reference_temperature_c == temperature
+ assert env.reference_static_pressure_kpa == pressure_kpa
+ assert env.reference_relative_humidity_percent == humidity
+
+
+def test_environmental_corrections_go_on_the_reference_and_off_the_result() -> None:
+ first, second = _interchange_readings()
+ reference_env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ )
+ test_env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.012,
+ temperature_coefficient_db_per_k=-0.004,
+ )
+ result = metrology.simultaneous_comparison(
+ _F,
+ _L_REF,
+ first,
+ second,
+ reference_environment=reference_env,
+ test_environment=test_env,
+ )
+ expected = _L_TEST + reference_env.correction_db - test_env.correction_db
+ np.testing.assert_allclose(result.sensitivity_level_db, expected, atol=1e-12)
+ assert tuple(result.corrections_db) == (
+ "reference environment",
+ "reference conditions",
+ )
+
+
+def test_environmental_correction_at_other_frequencies_is_refused() -> None:
+ first, second = _interchange_readings()
+ env = metrology.environmental_sensitivity_correction(
+ [1000.0, 2000.0],
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ )
+ with pytest.raises(ValueError, match=r"'reference_environment' was made at 2"):
+ metrology.simultaneous_comparison(
+ _F, _L_REF, first, second, reference_environment=env
+ )
+
+
+def _compare(excitation: str, **kwargs: object) -> metrology.ComparisonCalibration:
+ """A calibration of the test microphone at ``_F`` by either excitation."""
+ if excitation == "simultaneous":
+ first, second = _interchange_readings()
+ return metrology.simultaneous_comparison(
+ _F,
+ _L_REF,
+ first,
+ second,
+ **kwargs, # type: ignore[arg-type]
+ )
+ return metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ _L_REF + 94.0,
+ _L_TEST + 94.0,
+ **kwargs, # type: ignore[arg-type]
+ )
+
+
+@pytest.mark.parametrize("excitation", ["simultaneous", "sequential"])
+@pytest.mark.parametrize("name", ["reference_environment", "test_environment"])
+def test_environmental_correction_at_as_many_other_frequencies_is_refused(
+ excitation: str, name: str
+) -> None:
+ # As many frequencies as the calibration, one of them different: 10 kHz
+ # where the calibration has 8 kHz. A check on the count alone applies the
+ # 10 kHz coefficient at 8 kHz and moves the result by 0,5 dB in silence.
+ env = metrology.environmental_sensitivity_correction(
+ [250.0, 1000.0, 2000.0, 4000.0, 10000.0],
+ static_pressure_kpa=96.325,
+ temperature_c=23.0,
+ relative_humidity_percent=50.0,
+ static_pressure_coefficient_db_per_kpa=[-0.005, -0.005, -0.006, -0.008, 0.1],
+ temperature_coefficient_db_per_k=0.0,
+ )
+ with pytest.raises(
+ ValueError,
+ match=rf"'{name}' was made at 10000 Hz where the calibration has 8000 Hz",
+ ):
+ _compare(excitation, **{name: env})
+
+
+def test_environmental_correction_at_the_same_frequencies_is_taken() -> None:
+ env = metrology.environmental_sensitivity_correction(
+ [250.0, 1000.0, 2000.0, 4000.0, 8000.0],
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ )
+ result = _compare("sequential", reference_environment=env)
+ np.testing.assert_allclose(
+ result.sensitivity_level_db, _L_TEST + env.correction_db, atol=1e-12
+ )
+
+
+#: The nominal one-third-octave midband frequencies from 25 Hz to 20 kHz
+#: (IEC 61260-1), and the exact base-ten frequencies they label. The two agree
+#: at 100 Hz, 1 kHz and 10 kHz; everywhere else they are between 0.15 % (63,
+#: 630 and 6300 Hz) and 0.95 % (160 Hz, 1600 Hz and 16 kHz) apart.
+_NOMINAL_THIRDS = (
+ 25.0, 31.5, 40.0, 50.0, 63.0, 80.0, 100.0, 125.0, 160.0, 200.0,
+ 250.0, 315.0, 400.0, 500.0, 630.0, 800.0, 1000.0, 1250.0, 1600.0, 2000.0,
+ 2500.0, 3150.0, 4000.0, 5000.0, 6300.0, 8000.0, 10000.0, 12500.0, 16000.0,
+ 20000.0,
+) # fmt: skip
+_EXACT_THIRDS = metrology.exact_frequencies(25.0, 20000.0, fraction=3)
+
+
+def test_nominal_and_exact_thirds_are_as_far_apart_as_stated() -> None:
+ apart = np.abs(np.asarray(_NOMINAL_THIRDS) / _EXACT_THIRDS - 1.0)
+ agree = apart < 1e-12
+ closest, widest = np.min(apart[~agree]), np.max(apart)
+
+ def bands(offset: float) -> list[float]:
+ return [
+ nominal
+ for nominal, off in zip(_NOMINAL_THIRDS, apart, strict=True)
+ if math.isclose(off, offset, rel_tol=1e-9)
+ ]
+
+ assert [f for f, same in zip(_NOMINAL_THIRDS, agree, strict=True) if same] == [
+ 100.0,
+ 1000.0,
+ 10000.0,
+ ]
+ assert 100.0 * closest == pytest.approx(0.15, abs=0.005)
+ assert bands(closest) == [63.0, 630.0, 6300.0]
+ assert 100.0 * widest == pytest.approx(0.95, abs=0.005)
+ assert bands(widest) == [160.0, 1600.0, 16000.0]
+
+
+def _environment_at(
+ frequencies_hz: np.ndarray,
+) -> cc.EnvironmentalSensitivityCorrection:
+ return metrology.environmental_sensitivity_correction(
+ frequencies_hz,
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ )
+
+
+def _compare_on_thirds(
+ env: cc.EnvironmentalSensitivityCorrection,
+) -> metrology.ComparisonCalibration:
+ """A calibration at the exact one-third-octave frequencies."""
+ level = np.full(_EXACT_THIRDS.size, -38.0)
+ return metrology.sequential_comparison(
+ _EXACT_THIRDS, level, level + 94.0, level + 106.0, reference_environment=env
+ )
+
+
+@pytest.mark.parametrize(
+ "band",
+ [
+ index
+ for index, nominal in enumerate(_NOMINAL_THIRDS)
+ if not math.isclose(nominal, _EXACT_THIRDS[index], rel_tol=1e-12)
+ ],
+ ids=lambda index: f"{_NOMINAL_THIRDS[index]:g}Hz",
+)
+def test_environmental_correction_at_a_nominal_frequency_is_refused(band: int) -> None:
+ # A correction labelled with the nominal 3150 Hz where the calibration is
+ # at the exact 3162.28 Hz is 0.4 % off, and 630 Hz against 630.96 Hz only
+ # 0.15 %: a tolerance loose enough to let either through applies one
+ # frequency's coefficient at another.
+ made = _EXACT_THIRDS.copy()
+ made[band] = _NOMINAL_THIRDS[band]
+ env = _environment_at(made)
+ message = re.escape(
+ f"'reference_environment' was made at {_NOMINAL_THIRDS[band]:g} Hz "
+ f"where the calibration has {_EXACT_THIRDS[band]:g} Hz"
+ )
+ with pytest.raises(ValueError, match=message):
+ _compare_on_thirds(env)
+
+
+def test_environmental_correction_off_by_rounding_alone_is_taken() -> None:
+ # The same exact frequencies after arithmetic that moved them in the last
+ # digits are the same frequencies.
+ env = _environment_at(_EXACT_THIRDS * (1.0 + 1e-12))
+ result = _compare_on_thirds(env)
+ np.testing.assert_allclose(
+ result.sensitivity_level_db, -26.0 + env.correction_db, atol=1e-12
+ )
+
+
+@pytest.mark.parametrize(
+ ("name", "value", "message"),
+ [
+ ("temperature_c", -300.0, r"'temperature_c' must be finite and above"),
+ ("temperature_c", -273.15, r"'temperature_c' must be finite and above"),
+ (
+ "reference_temperature_c",
+ -300.0,
+ r"'reference_temperature_c' must be finite and above",
+ ),
+ (
+ "relative_humidity_percent",
+ 150.0,
+ r"'relative_humidity_percent' must be between 0 and 100",
+ ),
+ (
+ "relative_humidity_percent",
+ -5.0,
+ r"'relative_humidity_percent' must be between 0 and 100",
+ ),
+ (
+ "reference_relative_humidity_percent",
+ 100.5,
+ r"'reference_relative_humidity_percent' must be between 0 and 100",
+ ),
+ (
+ "reference_relative_humidity_percent",
+ math.nan,
+ r"'reference_relative_humidity_percent' must be between 0 and 100",
+ ),
+ ],
+)
+def test_environmental_correction_refuses_conditions_that_are_no_state_of_air(
+ name: str, value: float, message: str
+) -> None:
+ conditions: dict[str, float] = {
+ "temperature_c": 21.0,
+ "relative_humidity_percent": 45.0,
+ }
+ conditions[name] = value
+ with pytest.raises(ValueError, match=message):
+ metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=99.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ humidity_coefficient_db_per_percent=0.001,
+ **conditions, # type: ignore[arg-type]
+ )
+
+
+def test_environmental_correction_takes_dry_and_saturated_air() -> None:
+ env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=101.325,
+ temperature_c=23.0,
+ relative_humidity_percent=100.0,
+ reference_relative_humidity_percent=0.0,
+ static_pressure_coefficient_db_per_kpa=0.0,
+ temperature_coefficient_db_per_k=0.0,
+ humidity_coefficient_db_per_percent=0.001,
+ )
+ np.testing.assert_allclose(env.correction_db, 0.1, rtol=1e-12)
+
+
+@pytest.mark.parametrize("excitation", ["simultaneous", "sequential"])
+def test_an_unknown_field_is_named_before_the_free_field_difference(
+ excitation: str,
+) -> None:
+ with pytest.raises(ValueError, match=r"'field' must be one of"):
+ _compare(excitation, field="freefield", reference_free_field_difference_db=0.1)
+
+
+def test_free_field_difference_is_added_to_a_pressure_calibrated_reference() -> None:
+ difference = np.array([0.0, 0.08, 0.2, 0.85, 2.45])
+ result = metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ _L_REF + difference + 94.0,
+ _L_TEST + 94.0,
+ field="free_field",
+ reference_free_field_difference_db=difference,
+ )
+ np.testing.assert_allclose(result.sensitivity_level_db, _L_TEST, atol=1e-12)
+
+
+def test_free_field_difference_on_a_pressure_calibration_is_refused() -> None:
+ first, second = _interchange_readings()
+ with pytest.raises(ValueError, match=r"IEC 61094-8 Table 1"):
+ metrology.simultaneous_comparison(
+ _F, _L_REF, first, second, reference_free_field_difference_db=0.1
+ )
+
+
+def test_a_correction_named_twice_is_refused() -> None:
+ first, second = _interchange_readings()
+ env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ temperature_coefficient_db_per_k=0.002,
+ )
+ named = {"reference environment": 0.01}
+ with pytest.raises(ValueError, match=r"repeats the correction"):
+ metrology.simultaneous_comparison(
+ _F,
+ _L_REF,
+ first,
+ second,
+ reference_environment=env,
+ corrections_db=named,
+ )
+
+
+def test_jig_correction_reads_table_a1_at_exact_frequencies() -> None:
+ exact = 1000.0 * 10.0 ** (np.array([3, 6, 9, 12]) / 10.0) # 1995 Hz ... 15849 Hz
+ jig = metrology.jig_diameter_correction(exact)
+ np.testing.assert_allclose(jig.correction_db, [-0.015, -0.059, -0.235, -0.933])
+ np.testing.assert_allclose(
+ jig.expanded_uncertainty_db,
+ ref.IEC61094_5_TABLE_A1_RELATIVE_EXPANDED
+ * np.array([0.015, 0.059, 0.235, 0.933]),
+ )
+ np.testing.assert_allclose(
+ jig.standard_uncertainty_db, jig.expanded_uncertainty_db / 2
+ )
+
+
+def test_jig_correction_defaults_to_the_fourteen_rows() -> None:
+ jig = metrology.jig_diameter_correction()
+ assert jig.frequencies_hz.size == 14
+ assert jig.correction_db[-1] == pytest.approx(-1.443, abs=1e-12)
+
+
+def test_jig_correction_refuses_a_frequency_it_does_not_print() -> None:
+ frequencies = [500.0]
+ with pytest.raises(ValueError, match=r"not a frequency of IEC 61094-5 Table A.1"):
+ metrology.jig_diameter_correction(frequencies)
+
+
+def test_jig_correction_enters_as_a_named_correction() -> None:
+ first, second = _interchange_readings()
+ frequencies = np.array([1000.0, 2000.0, 4000.0, 8000.0])
+ keep = slice(1, None)
+ jig = metrology.jig_diameter_correction(frequencies)
+ result = metrology.simultaneous_comparison(
+ frequencies,
+ _L_REF[keep],
+ first[keep],
+ second[keep],
+ corrections_db={"WS3 in the jig (Table A.1)": jig.correction_db},
+ )
+ np.testing.assert_allclose(
+ result.sensitivity_level_db, _L_TEST[keep] + jig.correction_db, atol=1e-12
+ )
+
+
+# --------------------------------------------------------------------------
+# IEC 61094-8 B.1: the effective free-field region
+# --------------------------------------------------------------------------
+def test_region_major_axis_is_d_plus_tau_c_with_annex_f_air() -> None:
+ region = metrology.free_field_region(1.2, 0.004, temperature_c=21.0)
+ c = fluids.air(
+ temperature_c=21.0, static_pressure_pa=101325.0, relative_humidity_percent=50.0
+ ).speed_of_sound
+ assert region.speed_of_sound == pytest.approx(c, rel=1e-12)
+ assert region.major_axis_m == pytest.approx(1.2 + 0.004 * c, rel=1e-12)
+ assert region.rod_clearance_m == pytest.approx(0.004 * c / 2.0, rel=1e-12)
+
+
+def test_a_reflector_at_the_semi_minor_axis_arrives_at_the_end_of_the_window() -> None:
+ """A plane parallel to the axis at distance b reflects by its image source
+ along a path of length sqrt(d**2 + 4 b**2), which is A.
+ """
+ region = metrology.FreeFieldRegion(1.0, 0.005, 343.0)
+ b = region.semi_minor_axis_m
+ reflected = math.hypot(region.source_distance_m, 2.0 * b)
+ assert reflected == pytest.approx(region.major_axis_m, rel=1e-12)
+ delay = (reflected - region.source_distance_m) / region.speed_of_sound
+ assert delay == pytest.approx(region.window_time_s, rel=1e-12)
+
+
+def test_region_refuses_a_window_that_is_not_positive() -> None:
+ with pytest.raises(ValueError, match=r"window_time_s"):
+ metrology.free_field_region(1.0, 0.0)
+
+
+# --------------------------------------------------------------------------
+# The results and their validation
+# --------------------------------------------------------------------------
+def test_results_publish_read_only_columns() -> None:
+ first, second = _interchange_readings()
+ result = metrology.simultaneous_comparison(
+ _F, _L_REF, first, second, expanded_uncertainty_db=0.09
+ )
+ for column in (
+ result.frequencies_hz,
+ result.reference_sensitivity_level_db,
+ result.output_level_differences_db,
+ result.expanded_uncertainty_db,
+ ):
+ assert column is not None
+ assert not column.flags.writeable
+ assert isinstance(result.corrections_db, types.MappingProxyType)
+ jig = metrology.jig_diameter_correction()
+ assert not jig.correction_db.flags.writeable
+
+
+def test_calibration_refuses_a_negative_uncertainty() -> None:
+ first, second = _interchange_readings()
+ with pytest.raises(
+ ValueError, match=r"'expanded_uncertainty_db' must be non-negative"
+ ):
+ metrology.simultaneous_comparison(
+ _F, _L_REF, first, second, expanded_uncertainty_db=-0.1
+ )
+
+
+def test_calibration_refuses_an_unknown_field() -> None:
+ first, second = _interchange_readings()
+ with pytest.raises(ValueError, match=r"'field' must be one of"):
+ metrology.simultaneous_comparison(_F, _L_REF, first, second, field="diffuse")
+
+
+def test_environmental_correction_needs_the_coefficients_of_the_microphone() -> None:
+ with pytest.raises(TypeError, match=r"temperature_coefficient_db_per_k"):
+ metrology.environmental_sensitivity_correction( # type: ignore[call-arg]
+ _F,
+ static_pressure_kpa=99.0,
+ temperature_c=21.0,
+ relative_humidity_percent=45.0,
+ static_pressure_coefficient_db_per_kpa=-0.006,
+ )
+
+
+# --------------------------------------------------------------------------
+# IEC 61183 clause 5 computes through the same model
+# --------------------------------------------------------------------------
+def test_iec61183_diffuse_field_comparison_uses_the_shared_model() -> None:
+ assert random_incidence._compared_level_db is cc._compared_level_db
+ assert (
+ random_incidence._output_level_difference_db is cc._output_level_difference_db
+ )
+ result = metrology.diffuse_field_sensitivity(
+ [1000.0, 2000.0],
+ [94.3, 94.6],
+ [94.0, 94.1],
+ reference_random_incidence_level_db=[-26.0, -26.2],
+ )
+ expected = metrology.sequential_comparison(
+ [1000.0, 2000.0], [-26.0, -26.2], [94.0, 94.1], [94.3, 94.6]
+ )
+ np.testing.assert_allclose(
+ result.diffuse_field_level_db, expected.sensitivity_level_db, atol=1e-12
+ )
+
+
+# --------------------------------------------------------------------------
+# What each figure says
+# --------------------------------------------------------------------------
+def test_calibration_plot_draws_the_band_and_both_levels() -> None:
+ first, second = _interchange_readings()
+ result = metrology.simultaneous_comparison(
+ _F, _L_REF, first, second, expanded_uncertainty_db=0.09
+ )
+ fig, ax = plt.subplots()
+ result.plot(ax)
+ labels = [text.get_text() for text in ax.get_legend().get_texts()]
+ assert labels == [
+ r"$L_\mathrm{test} \pm U$ ($k$ = 2)",
+ r"$L_\mathrm{test}$, microphone under test",
+ r"$L_\mathrm{ref}$, reference microphone",
+ ]
+ assert "IEC 61094-5" in ax.get_title()
+ np.testing.assert_allclose(ax.get_lines()[0].get_ydata(), _L_TEST, atol=1e-12)
+ plt.close(fig)
+ fig, ax = plt.subplots()
+ metrology.simultaneous_comparison(
+ _F, _L_REF, _L_REF - _L_TEST, field="free_field"
+ ).plot(ax, language="es")
+ assert ax.get_title() == "Sensibilidad en campo libre por comparación (IEC 61094-8)"
+ assert len(ax.collections) == 0
+ plt.close(fig)
+
+
+def test_free_field_plot_draws_the_reference_free_field_level() -> None:
+ """Against a pressure-calibrated reference, the dashed curve is the level
+ the free field compared the test microphone with: the reference's
+ pressure level plus its free-field difference.
+ """
+ difference = np.array([0.0, 0.05, 0.12, 0.3, 0.7])
+ result = metrology.sequential_comparison(
+ _F,
+ _L_REF,
+ _L_REF + difference + 74.0,
+ _L_TEST + 74.0,
+ field="free_field",
+ reference_free_field_difference_db=difference,
+ )
+ fig, ax = plt.subplots()
+ result.plot(ax)
+ reference = ax.get_lines()[1]
+ np.testing.assert_allclose(reference.get_ydata(), _L_REF + difference, atol=1e-12)
+ assert reference.get_label() == "Reference microphone, free-field level"
+ plt.close(fig)
+ fig, ax = plt.subplots()
+ result.plot(ax, language="es")
+ labels = [text.get_text() for text in ax.get_legend().get_texts()]
+ assert labels[-1] == "Micrófono de referencia, nivel en campo libre"
+ plt.close(fig)
+
+
+def test_budget_plot_names_the_components_and_the_expanded_uncertainty() -> None:
+ fig, ax = plt.subplots()
+ _d1_budget().plot(ax)
+ ticks = [label.get_text() for label in ax.get_yticklabels()]
+ assert ticks[0] == "Reference microphone"
+ assert ticks[6] == "Drift of the reference"
+ assert "$U$ = 0.087 dB" in ax.get_title()
+ assert "Table D.1" in ax.get_title()
+ plt.close(fig)
+ fig, ax = plt.subplots()
+ _d1_budget().plot(ax, language="es")
+ assert "$U$ = 0,087 dB" in ax.get_title()
+ assert ax.get_yticklabels()[0].get_text() == "Micrófono de referencia"
+ plt.close(fig)
+
+
+def test_environmental_plot_draws_three_terms_and_the_total() -> None:
+ env = metrology.environmental_sensitivity_correction(
+ _F,
+ static_pressure_kpa=97.0,
+ temperature_c=26.0,
+ relative_humidity_percent=40.0,
+ static_pressure_coefficient_db_per_kpa=-0.005,
+ temperature_coefficient_db_per_k=0.003,
+ )
+ fig, ax = plt.subplots()
+ env.plot(ax)
+ labels = [text.get_text() for text in ax.get_legend().get_texts()]
+ assert labels[-1] == r"$C_\mathrm{env}$, total"
+ assert len(labels) == 4
+ assert "97.000 kPa" in ax.get_title()
+ np.testing.assert_allclose(ax.get_lines()[-1].get_ydata(), env.correction_db)
+ plt.close(fig)
+
+
+def test_jig_plot_draws_the_table_and_its_band() -> None:
+ fig, ax = plt.subplots()
+ metrology.jig_diameter_correction().plot(ax, language="es")
+ assert "tabla A.1" in ax.get_title()
+ assert len(ax.collections) == 1
+ plt.close(fig)
+
+
+def test_region_plot_draws_figure_b1() -> None:
+ region = metrology.FreeFieldRegion(1.0, 0.005, 343.0)
+ fig, ax = plt.subplots()
+ region.plot(ax)
+ boundary = ax.get_lines()[0]
+ x = np.asarray(boundary.get_xdata())
+ y = np.asarray(boundary.get_ydata())
+ assert x.max() == pytest.approx(region.major_axis_m / 2.0, rel=1e-9)
+ assert y.max() == pytest.approx(region.semi_minor_axis_m, rel=1e-3)
+ assert "B.1" in ax.get_title()
+ plt.close(fig)
+
+
+@pytest.mark.parametrize("language", ["en", "es"])
+def test_region_plot_on_its_own_figure_keeps_its_legend_and_title(
+ language: str,
+) -> None:
+ """Without axes the renderer makes a figure wide enough for the legend
+ beside the axes and the two-line title.
+ """
+ ax = metrology.free_field_region(1.0, 0.005).plot(language=language)
+ fig = ax.figure
+ fig.canvas.draw()
+ renderer = fig.canvas.get_renderer() # type: ignore[attr-defined]
+ page = fig.bbox
+ for artist in (ax.get_legend(), ax.title):
+ box = artist.get_window_extent(renderer)
+ assert box.x0 >= page.x0
+ assert box.x1 <= page.x1
+ assert box.y0 >= page.y0
+ assert box.y1 <= page.y1
+ plt.close(fig)
diff --git a/tests/reference_data/__init__.py b/tests/reference_data/__init__.py
index 28005e115..31a443ea2 100644
--- a/tests/reference_data/__init__.py
+++ b/tests/reference_data/__init__.py
@@ -420,6 +420,46 @@
from .calibrators import IEC60942_TABLE_E1 as IEC60942_TABLE_E1
from .calibrators import IEC61672_1_TABLE_C1 as IEC61672_1_TABLE_C1
from .calibrators import TC29_REASONS as TC29_REASONS
+from .comparison_calibration import (
+ IEC61094_5_D3_COMBINED_DB as IEC61094_5_D3_COMBINED_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_D3_EXPANDED_DB as IEC61094_5_D3_EXPANDED_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_D3_LINEAR_COMBINED_DB as IEC61094_5_D3_LINEAR_COMBINED_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_D3_PRINTED_COMBINED_DB as IEC61094_5_D3_PRINTED_COMBINED_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_D3_PRINTED_EXPANDED_DB as IEC61094_5_D3_PRINTED_EXPANDED_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_TABLE_A1_DB as IEC61094_5_TABLE_A1_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_TABLE_A1_RELATIVE_EXPANDED as IEC61094_5_TABLE_A1_RELATIVE_EXPANDED,
+)
+from .comparison_calibration import (
+ IEC61094_5_TABLE_D1_STANDARD_DB as IEC61094_5_TABLE_D1_STANDARD_DB,
+)
+from .comparison_calibration import (
+ IEC61094_5_TABLE_D1_STATED as IEC61094_5_TABLE_D1_STATED,
+)
+from .comparison_calibration import IEC61094_8_TABLE_1_DB as IEC61094_8_TABLE_1_DB
+from .comparison_calibration import (
+ IEC61094_8_TABLE_1_TEXT as IEC61094_8_TABLE_1_TEXT,
+)
+from .comparison_calibration import (
+ IEC61094_8_TABLE_2_SOURCES as IEC61094_8_TABLE_2_SOURCES,
+)
+from .comparison_calibration import (
+ IEC61094_8_TABLE_2_SUBCLAUSES as IEC61094_8_TABLE_2_SUBCLAUSES,
+)
+from .comparison_calibration import (
+ IEC61094_REFERENCE_CONDITIONS as IEC61094_REFERENCE_CONDITIONS,
+)
from .damping import VER_BERANEK_14_1 as VER_BERANEK_14_1
from .damping import VER_BERANEK_14_1_COLUMNS as VER_BERANEK_14_1_COLUMNS
from .diffusion import COX_B_BANDS_HZ as COX_B_BANDS_HZ
diff --git a/tests/reference_data/comparison_calibration.py b/tests/reference_data/comparison_calibration.py
new file mode 100644
index 000000000..4ab4d83df
--- /dev/null
+++ b/tests/reference_data/comparison_calibration.py
@@ -0,0 +1,181 @@
+# Copyright (c) 2026. Jose Manuel Requena Plens
+"""Calibration of working standard microphones by comparison
+(IEC 61094-5:2016, pressure; IEC 61094-8:2012, free field).
+
+IEC 61094-5 prints the corrections for a WS3 microphone in the jig of Figure
+A.4 (Table A.1) and a worked uncertainty budget at 2 kHz (Table D.1, combined
+in D.3). IEC 61094-8 prints the typical expanded uncertainty of each way of
+calibrating the reference microphone (Table 1) and the list of typical
+uncertainty components with their subclauses (Table 2), without values. Every
+number was read on the rasterized printed page: IEC 61094-5:2016 (Edition
+2.0, 2016-05, English-French) Table A.1 on printed folio 15 (PDF page 17),
+Annex D on folios 19 to 21 (PDF pages 21 to 23); BS EN 61094-8:2012, the
+English text of EN 61094-8:2012 which is IEC 61094-8:2012 unchanged, Table 1
+on folio 12 (PDF page 14) and Table 2 on folio 17 (PDF page 19).
+
+Stdlib only, like every module of this package.
+"""
+
+from __future__ import annotations
+
+import math
+
+#: IEC 61094-5:2016 Table A.1, folio 15 (PDF page 17): the correction, in
+#: dB, keyed by frequency in kHz as the table prints it.
+IEC61094_5_TABLE_A1_DB: dict[float, float] = {
+ 1.0: -0.004,
+ 1.25: -0.006,
+ 1.6: -0.009,
+ 2.0: -0.015,
+ 2.5: -0.023,
+ 3.15: -0.036,
+ 4.0: -0.059,
+ 5.0: -0.092,
+ 6.3: -0.146,
+ 8.0: -0.235,
+ 10.0: -0.367,
+ 12.5: -0.572,
+ 16.0: -0.933,
+ 20.0: -1.443,
+}
+
+#: Table A.1 NOTE: the expanded uncertainty is one tenth of the correction.
+IEC61094_5_TABLE_A1_RELATIVE_EXPANDED = 0.1
+
+#: IEC 61094-5:2016 Table D.1, folio 20 (PDF page 22): the standard
+#: uncertainty column, in dB, keyed by component.
+IEC61094_5_TABLE_D1_STANDARD_DB: dict[str, float] = {
+ "reference": 0.025,
+ "capacitance": 0.006,
+ "non_linearity": 0.017,
+ "impedance": 0.003,
+ "polarizing_voltage": 0.005,
+ "repeatability": 0.025,
+ "drift": 0.017,
+ "rounding": 0.003,
+}
+
+#: Table D.1: the value each row's text states and what it is divided by,
+#: for the seven rows that state one. The reference is quoted "as ± 0,05 dB
+#: with a coverage factor of k = 2", and the polarising voltage is set to
+#: (200,0 ± 0,2) V "giving a semi-range for this component of
+#: 20 lg (200,2/200) dB with a rectangular distribution". The repeatability
+#: row states no value ("Found from the standard uncertainties of a large
+#: number of similar measurements."), so it is not here.
+IEC61094_5_TABLE_D1_STATED: dict[str, tuple[float, float]] = {
+ "reference": (0.05, 2.0),
+ "capacitance": (0.01, math.sqrt(3.0)),
+ "non_linearity": (0.03, math.sqrt(3.0)),
+ "impedance": (0.005, math.sqrt(3.0)),
+ "polarizing_voltage": (20.0 * math.log10(200.2 / 200.0), math.sqrt(3.0)),
+ "drift": (0.03, math.sqrt(3.0)),
+ "rounding": (0.005, math.sqrt(3.0)),
+}
+
+#: IEC 61094-5:2016 D.3, folio 21 (PDF page 23): the combined standard
+#: uncertainty and the expanded one (k = 2) as printed. They are not the
+#: root-sum-square of the eight components of Table D.1 (docs/ERRATA.md).
+IEC61094_5_D3_PRINTED_COMBINED_DB = 0.040
+IEC61094_5_D3_PRINTED_EXPANDED_DB = 0.08
+
+#: The root-sum-square of the printed standard uncertainty column, the value
+#: D.3 describes, to four decimals: 0,043 669 dB.
+IEC61094_5_D3_COMBINED_DB = 0.0437
+
+#: The same with k = 2, to three decimals: 0,087 34 dB.
+IEC61094_5_D3_EXPANDED_DB = 0.087
+
+#: The "strict calculation" D.3 mentions and does not print: each component
+#: of the printed column converted to a relative uncertainty
+#: 10**(u/20) - 1, combined in quadrature and converted back by
+#: 20 lg(1 + r), to six decimals: 0,043 614 dB, 0,000 055 dB below the
+#: combination in decibels.
+IEC61094_5_D3_LINEAR_COMBINED_DB = 0.043614
+
+#: BS EN 61094-8:2012 Table 1, folio 12 (PDF page 14): the typical expanded
+#: uncertainty (k = 2) at 1 kHz and at 10 kHz, in dB, of each way of
+#: calibrating the reference microphone, in the table's order.
+IEC61094_8_TABLE_1_DB: dict[str, tuple[float, float]] = {
+ "primary_free_field": (0.25, 0.10),
+ "primary_pressure": (0.12, 0.4),
+ "secondary_pressure": (0.15, 0.5),
+ "secondary_free_field": (0.2, 0.5),
+ "electrostatic_actuator": (0.3, 0.6),
+}
+
+#: BS EN 61094-8:2012 Table 1, folio 12 (PDF page 14): the text cells of each
+#: row, the reference microphone type, the calibration method and the
+#: references, in the table's order. The type is printed once for the rows it
+#: spans; "This part of IEC 61094" is IEC 61094-8, and "IEC 61094-2 and
+#: IEC/TS 61094-7" is two references.
+IEC61094_8_TABLE_1_TEXT: dict[str, tuple[str, str, tuple[str, ...]]] = {
+ "primary_free_field": (
+ "LS",
+ "Primary free-field calibration",
+ ("IEC 61094-3",),
+ ),
+ "primary_pressure": (
+ "LS",
+ "Primary pressure calibration with the addition of a free-field to "
+ "pressure sensitivity level difference",
+ ("IEC 61094-2", "IEC/TS 61094-7"),
+ ),
+ "secondary_pressure": (
+ "LS",
+ "Secondary pressure calibration with the addition of a free-field to "
+ "pressure sensitivity level difference",
+ ("IEC 61094-5", "IEC/TS 61094-7"),
+ ),
+ "secondary_free_field": (
+ "LS and WS",
+ "Secondary free-field calibration",
+ ("IEC 61094-8",),
+ ),
+ "electrostatic_actuator": (
+ "LS and WS",
+ "Electrostatic actuator calibration with the addition of a free-field "
+ "to actuator response level difference",
+ ("IEC 61094-6",),
+ ),
+}
+
+#: BS EN 61094-8:2012 Table 2, folio 17 (PDF page 19): the source of
+#: uncertainty of each row as printed, in the table's order.
+IEC61094_8_TABLE_2_SOURCES: dict[str, str] = {
+ "reference": "Free-field sensitivity of the reference microphone",
+ "source_stability": "Stability of sound source",
+ "positioning": "Positioning accuracy (including acoustic centre uncertainty)",
+ "alignment": "Alignment between source and receiver",
+ "free_field": (
+ "Quality of free-field environment or influence of signal processing"
+ ),
+ "non_plane_wave": "Influence of non-plane wave",
+ "environment": "Influence of environmental conditions",
+ "polarizing_voltage": "Polarizing voltage",
+ "capacitance": "Microphone capacitance",
+ "non_linearity": "Measurement system non-linearity",
+ "rounding": "Rounding error",
+ "repeatability": "Measurement repeatability",
+}
+
+#: BS EN 61094-8:2012 Table 2, folio 17 (PDF page 19): the subclause
+#: references of the twelve typical components, in the table's order ("-"
+#: for the rounding error).
+IEC61094_8_TABLE_2_SUBCLAUSES: dict[str, tuple[str, ...]] = {
+ "reference": ("8.2",),
+ "source_stability": ("8.4",),
+ "positioning": ("7.3", "8.4"),
+ "alignment": ("7.4", "8.4"),
+ "free_field": ("8.5", "8.6"),
+ "non_plane_wave": ("6.3",),
+ "environment": ("7.6",),
+ "polarizing_voltage": ("7.2", "8.2"),
+ "capacitance": ("8.7.1",),
+ "non_linearity": ("8.7.2",),
+ "rounding": (),
+ "repeatability": ("8.3",),
+}
+
+#: IEC 61094-5 clause 4 and IEC 61094-8 clause 4: the reference environmental
+#: conditions, 23,0 °C, 101,325 kPa and 50 %.
+IEC61094_REFERENCE_CONDITIONS: tuple[float, float, float] = (23.0, 101.325, 50.0)
diff --git a/tests/result_factories.py b/tests/result_factories.py
index 7b59e143e..525a01bc2 100644
--- a/tests/result_factories.py
+++ b/tests/result_factories.py
@@ -763,6 +763,60 @@ def _correction_verification() -> ph.metrology.CorrectionUncertaintyVerification
)
+def _comparison_calibration() -> ph.metrology.ComparisonCalibration:
+ """A WS2P against an LS2P reference, interchanged in a coupler
+ (IEC 61094-5 Annex C).
+ """
+ return ph.metrology.simultaneous_comparison(
+ [1000.0, 2000.0, 4000.0],
+ [-38.0, -37.98, -37.95],
+ [-11.2, -11.3, -11.4],
+ [11.4, 11.5, 11.6],
+ expanded_uncertainty_db=[0.08, 0.09, 0.12],
+ )
+
+
+def _comparison_budget() -> ph.metrology.ComparisonUncertaintyBudget:
+ """The eight components of IEC 61094-5 Table D.1, at 2 kHz."""
+ return ph.metrology.comparison_uncertainty_budget(
+ {
+ "reference": 0.025,
+ "capacitance": 0.006,
+ "non_linearity": 0.017,
+ "impedance": 0.003,
+ "polarizing_voltage": 0.005,
+ "repeatability": 0.025,
+ "drift": 0.017,
+ "rounding": 0.003,
+ },
+ frequency_hz=2000.0,
+ )
+
+
+def _environmental_sensitivity_correction() -> (
+ ph.metrology.EnvironmentalSensitivityCorrection
+):
+ """A reference microphone from its certificate to the test conditions."""
+ return ph.metrology.environmental_sensitivity_correction(
+ [250.0, 1000.0, 4000.0],
+ static_pressure_kpa=97.0,
+ temperature_c=26.0,
+ relative_humidity_percent=40.0,
+ static_pressure_coefficient_db_per_kpa=[-0.005, -0.005, -0.008],
+ temperature_coefficient_db_per_k=0.003,
+ )
+
+
+def _jig_diameter_correction() -> ph.metrology.JigDiameterCorrection:
+ """The corrections of IEC 61094-5 Table A.1."""
+ return ph.metrology.jig_diameter_correction()
+
+
+def _free_field_region() -> ph.metrology.FreeFieldRegion:
+ """A 5 ms window at 1 m in air at the reference conditions."""
+ return ph.metrology.free_field_region(1.0, 0.005)
+
+
def _static_airflow() -> ph.materials.StaticAirflowResult:
u = np.array([0.2e-3, 0.4e-3, 0.6e-3, 0.8e-3, 1.0e-3])
dp = 30000.0 * u + 4.0e6 * u**2
diff --git a/tests/test_check_markdown_hazards.py b/tests/test_check_markdown_hazards.py
index ac83080a0..9ab57331e 100644
--- a/tests/test_check_markdown_hazards.py
+++ b/tests/test_check_markdown_hazards.py
@@ -1,11 +1,22 @@
# Copyright (c) 2026. Jose Manuel Requena Plens
-"""The unclosed-maths rule of ``scripts/check_markdown_hazards.py``.
+"""The gate for markdown that does not render the way it reads.
-A ``$`` inside an inline code span is code, and CommonMark reads the span
-before any maths could open in it, so the ``$schema`` key of a JSON
-document in a table cell opens nothing. The rule has to keep catching the
-defect it exists for, maths that wraps onto a block marker, whether or not
-the line also holds a code span.
+``scripts/check_markdown_hazards.py`` reads a page the way CommonMark and MDX
+will, so it can go wrong in both directions: miss a source that breaks the
+page, or refuse one that builds. The fifth rule is where the second is easy.
+MDX fails on a ``<`` of prose glued to a digit or an operator, and builds the
+same characters inside a quoted attribute value of a JSX tag, such as an
+``alt`` text, and in the YAML frontmatter, which Astro blanks before MDX
+reads the body. The site's own compiler, ``@mdx-js/mdx`` 3.1.1 with
+``remark-math``, fails on every case the fifth rule refuses below and
+compiles every case it accepts, once the frontmatter is blanked as Astro
+blanks it. The first four rules get one case each, since each one has a
+function of its own, and the first gets three more. A ``$`` inside an
+inline code span is code, and CommonMark reads the span before any maths
+could open in it, so the ``$schema`` key of a JSON document in a table cell
+opens nothing. The rule has to keep catching the defect it exists for,
+maths that wraps onto a block marker, whether or not the line also holds a
+code span.
"""
from __future__ import annotations
@@ -13,22 +24,232 @@
import pathlib
import sys
+import pytest
+
_SCRIPTS = str(pathlib.Path(__file__).resolve().parent.parent / "scripts")
if _SCRIPTS not in sys.path:
sys.path.insert(0, _SCRIPTS)
-import check_markdown_hazards as checker # noqa: E402
+import check_markdown_hazards as cmh
+
+_GLUED = "no tag name starts with"
+
+
+def _glued(text: str, name: str = "page.mdx") -> list[str]:
+ """The fifth rule's reports for a page, and only those."""
+ return [problem for problem in cmh.check_text(text, name) if _GLUED in problem]
+
+
+@pytest.mark.parametrize(
+ ("text", "seen"),
+ [
+ ('A quoted "<0,02 above 200 Hz" in prose.\n', "'<0,02 above '"),
+ ("The bound holds for x <=5 and no more.\n", "'<=5 and no m'"),
+ (' and then <0,02 here.\n', "'<0,02 here.'"),
+ ],
+ ids=["digit", "operator", "after-a-closed-tag"],
+)
+def test_a_less_than_glued_to_what_follows_in_mdx_prose_is_refused(
+ text: str, seen: str
+) -> None:
+ problems = _glued(text)
+ assert len(problems) == 1
+ assert problems[0].startswith(f"page.mdx:1: {seen} puts a '<'")
+
+
+@pytest.mark.parametrize(
+ "text",
+ [
+ "A bound of < b holds.\n",
+ "The code `<0.1` stays code.\n",
+ "The maths $a <0.1$ stays maths.\n",
+ "An escaped \\<0.1 stays text.\n",
+ "```python\nx <0\n```\n",
+ "$$\na <0,1\n$$\n",
+ '\n',
+ '\n',
+ "\n",
+ '\n',
+ '\n',
+ '---\ntitle: Probe\ndescription: "The special case prints <0,02 dB above 200 Hz."\n---\n',
+ ],
+ ids=[
+ "space",
+ "inline-code",
+ "inline-maths",
+ "escaped",
+ "code-fence",
+ "display-maths",
+ "alt-text",
+ "alt-text-of-a-tag-on-five-lines",
+ "single-quoted-value",
+ "expression-value",
+ "value-on-two-lines",
+ "frontmatter",
+ ],
+)
+def test_a_less_than_mdx_does_not_read_as_a_tag_is_accepted(text: str) -> None:
+ assert _glued(text) == []
+
+
+#: A JSX element further down the page, whose ``>`` a ``<`` read as the start
+#: of a tag would run on to, taking every quoted string on the way with it.
+_LATER_TAG = '\n\n\n'
+
+
+@pytest.mark.parametrize(
+ ("text", "line", "seen"),
+ [
+ (
+ "A quiet room reads `below the\nlabel here.\n', 1, "'<0.1\" y> her'"),
+ ("A path \\\\<0 here.\n", 1, "'<0 here.'"),
+ (
+ ' and text after it.\n',
+ 3,
+ "'<0.1 dB\" /> '",
+ ),
+ ("A span `a\n- b <0 c` here.\n", 2, "'<0 c` here.'"),
+ ("A span `a\\` <0 b` here.\n", 1, "'<0 b` here.'"),
+ ],
+ ids=[
+ "after-a-code-span-on-two-lines",
+ "after-inline-maths-on-two-lines",
+ "a-paragraph-after-a-code-span-on-two-lines",
+ "between-apostrophes",
+ "in-quotes-after-an-escaped-less-than",
+ "after-an-escaped-backslash",
+ "after-a-tag-with-text-after-it-on-its-line",
+ "in-a-code-span-a-list-item-cuts",
+ "after-a-code-span-a-backslash-does-not-escape",
+ ],
+)
+def test_a_less_than_that_no_span_or_tag_holds_is_refused(
+ text: str, line: int, seen: str
+) -> None:
+ r"""MDX reads a page from left to right, and so does the fifth rule.
+
+ A ``<`` and a letter inside a code span or inline maths starts no tag,
+ even when the span runs on to a second line, and an escaped ``\<``
+ starts none either. So none of them hides a ``<0`` further on: MDX fails
+ on each of these, where a ``<`` read as the start of a tag would have
+ swallowed every quoted string, and every pair of apostrophes, up to the
+ ``>`` of the element at the end.
+ """
+ problems = _glued(text)
+ assert len(problems) == 1
+ assert problems[0].startswith(f"page.mdx:{line}: {seen} puts a '<'")
+
+
+@pytest.mark.parametrize(
+ "text",
+ [
+ "A span `a\nb <0 c` here.\n",
+ "A span $a\nb <0 c$ here.\n",
+ "A span `` a ` <0 `` here.\n",
+ "A `` run nothing closes, then `code <0` here.\n",
+ "A $$a\nb <0$$ z.\n",
+ 'See x and `c <0` d.\n',
+ '\n',
+ '\n',
+ 'A paragraph.\n\n',
+ ],
+ ids=[
+ "code-span-on-two-lines",
+ "inline-maths-on-two-lines",
+ "code-span-of-two-backticks",
+ "backticks-nothing-closes",
+ "inline-maths-of-two-dollars",
+ "backtick-in-a-value",
+ "tag-with-a-blank-line-between-its-attributes",
+ "value-with-a-blank-line",
+ "tag-starting-a-line-under-a-paragraph",
+ ],
+)
+def test_a_less_than_in_a_span_or_a_value_over_several_lines_is_accepted(
+ text: str,
+) -> None:
+ """A span runs on to the next line of its paragraph, a tag across blanks.
+
+ A code span and inline maths end with their paragraph; a tag that has
+ its lines to itself is a block, and MDX lets it run across blank lines.
+ """
+ assert _glued(text) == []
+
+
+def test_prose_after_a_tag_on_several_lines_is_read_on_its_own_line() -> None:
+ """The tag ends at its ``>``: the prose after it is read, on its own line."""
+ text = '\n\nThen x <=5 in prose.\n'
+ problems = _glued(text)
+ assert len(problems) == 1
+ assert problems[0].startswith("page.mdx:5: '<=5 in prose'")
+
+
+def test_a_less_than_between_the_attributes_of_a_tag_is_still_refused() -> None:
+ """Only the quoted values of a tag are blanked, not the tag itself.
+
+ MDX fails on this one too, reading the ``<`` where an attribute name has
+ to start.
+ """
+ problems = _glued('\n')
+ assert len(problems) == 1
+ assert problems[0].startswith("page.mdx:2: '<0,1 dB'")
+
+
+def test_the_body_after_the_frontmatter_is_read_on_its_own_lines() -> None:
+ text = '---\ndescription: "<0,02 dB"\n---\n\nA quoted "<0,02 dB" here.\n'
+ problems = _glued(text)
+ assert len(problems) == 1
+ assert problems[0].startswith("page.mdx:5: ")
+
+
+def test_a_plain_markdown_page_is_not_read_as_mdx() -> None:
+ assert _glued('A quoted "<0,02 above 200 Hz" in prose.\n', "page.md") == []
+
+
+def test_a_dollar_in_the_frontmatter_opens_no_maths() -> None:
+ """A price in a YAML description does not leak into the first rule."""
+ text = "---\ndescription: costs $5 a band\n---\nIntro line\n- a list item\n"
+ assert cmh.check_text(text, "page.mdx") == []
+
+
+def test_inline_maths_cut_off_by_a_list_marker_is_refused() -> None:
+ problems = cmh.check_text("The level $L_{n,ij,w} = a\n- K_{ij}$ holds.\n", "p.md")
+ assert len(problems) == 1
+ assert problems[0].startswith("p.md:2: inline maths opened on line 1")
def _problems(tmp_path: pathlib.Path, text: str) -> list[str]:
page = tmp_path / "page.md"
page.write_text(text, encoding="utf-8")
- original = checker._ROOT
- checker._ROOT = tmp_path
+ original = cmh._ROOT
+ cmh._ROOT = tmp_path
try:
- return checker._check(page)
+ return cmh._check(page)
finally:
- checker._ROOT = original
+ cmh._ROOT = original
def test_a_dollar_in_a_code_span_opens_no_maths(tmp_path: pathlib.Path) -> None:
@@ -58,9 +279,47 @@ def test_a_code_span_beside_open_maths_does_not_close_it(
def test_counting_skips_escaped_display_and_code_dollars() -> None:
- assert checker._unescaped_dollars("`$a` and ``b $ c`` and \\$ and $$") == 0
- assert checker._unescaped_dollars("`$a` then $x$") == 2
+ assert cmh._unescaped_dollars("`$a` and ``b $ c`` and \\$ and $$") == 0
+ assert cmh._unescaped_dollars("`$a` then $x$") == 2
# A closing run is a whole run of the opener's length: one tick of `` never
# closes a single ` , so the $ after an unmatched ` is prose.
- assert checker._unescaped_dollars("a `$x`` b") == 1
- assert checker._unescaped_dollars("a ``$x` b``") == 0
+ assert cmh._unescaped_dollars("a `$x`` b") == 1
+ assert cmh._unescaped_dollars("a ``$x` b``") == 0
+
+
+def test_a_wrapped_greater_than_sign_is_refused() -> None:
+ problems = cmh.check_text("The level is always\n> 5 dB above it.\n", "p.md")
+ assert len(problems) == 1
+ assert "starts a block quote" in problems[0]
+
+
+def test_a_same_page_link_to_an_accented_heading_is_refused() -> None:
+ problems = cmh.check_text("See [it](#la-medición).\n", "p.md")
+ assert len(problems) == 1
+ assert "'#la-medición' will be percent-encoded" in problems[0]
+
+
+def test_a_formula_on_the_opening_display_fence_is_refused() -> None:
+ problems = cmh.check_text("$$x = 1\n", "p.md")
+ assert len(problems) == 1
+ assert "Nothing closes it" in problems[0]
+
+
+def test_a_hazard_in_a_tilde_fence_is_code() -> None:
+ assert (
+ cmh.check_text("~~~\nThe level is always\n> 5 dB above it.\n~~~\n", "p.md")
+ == []
+ )
+
+
+def test_a_fence_shown_inside_another_closes_nothing() -> None:
+ text = "````\n```\nexample\n````\nThe level is always\n> 5 dB above it.\n"
+ problems = cmh.check_text(text, "p.md")
+ assert len(problems) == 1
+ assert "p.md:6:" in problems[0]
+ assert "starts a block quote" in problems[0]
+
+
+def test_the_shipped_pages_pass(capsys: pytest.CaptureFixture[str]) -> None:
+ assert cmh.main() == 0
+ assert "Markdown renders the way it reads" in capsys.readouterr().out
diff --git a/tests/test_result_plots.py b/tests/test_result_plots.py
index 3237445ae..db8ac58f7 100644
--- a/tests/test_result_plots.py
+++ b/tests/test_result_plots.py
@@ -61,6 +61,8 @@
_channel_match,
_cnossos_road,
_coherence_check,
+ _comparison_budget,
+ _comparison_calibration,
_conformance_verification,
_correction_budget,
_correction_verification,
@@ -73,6 +75,7 @@
_driving_point_stiffness,
_effective_blocking_mass,
_emergence,
+ _environmental_sensitivity_correction,
_exp_ir,
_exposure,
_extended_impact_rating,
@@ -83,6 +86,7 @@
_force_spectrum,
_free_field_check,
_free_field_correction,
+ _free_field_region,
_hard_walled_power,
_hard_walled_room_check,
_heavy_impact_improvement,
@@ -97,6 +101,7 @@
_intensity,
_intensity_power_negative,
_inverse_square_law,
+ _jig_diameter_correction,
_lab_floor_covering_improvement,
_lab_lining_improvement,
_layered_absorber,
@@ -410,6 +415,15 @@ def blocked(name: str, *args: object, **kwargs: object) -> ModuleType:
("free_field_correction", _free_field_correction, "line"),
("correction_budget", _correction_budget, "bar"),
("correction_verification", _correction_verification, "line"),
+ ("comparison_calibration", _comparison_calibration, "line"),
+ ("comparison_budget", _comparison_budget, "bar"),
+ (
+ "environmental_sensitivity_correction",
+ _environmental_sensitivity_correction,
+ "line",
+ ),
+ ("jig_diameter_correction", _jig_diameter_correction, "line"),
+ ("free_field_region", _free_field_region, "line"),
("vibration_meter_reading", _vibration_meter_reading, "line"),
("vibration_meter_verification", _vibration_meter_verification, "line"),
(
@@ -958,6 +972,11 @@ def test_single_axes_plots_accept_external_ax() -> None:
ph.hearing.audiometric_uncertainty(1000.0),
ph.hearing.EARMUFF_INSERTION_LOSS_UNCERTAINTY,
ph.hearing.verify_fixture_isolation(np.full(22, 120.0), np.full(22, 55.0)),
+ _comparison_calibration(),
+ _comparison_budget(),
+ _environmental_sensitivity_correction(),
+ _jig_diameter_correction(),
+ _free_field_region(),
_static_airflow(),
_airborne_prediction(),
_impact_prediction(),