[READ-ONLY] Mirror of https://github.com/jmrplens/PyOctaveBand. [Python3] Octave-Band and Fractional Octave-Band filter. For signal in time domain. jmrplens.github.io/PyOctaveBand/
acoustics audio filter frequency frequency-analysis frequency-domain octave python3 signal time-domain
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: EN/ES language option for the psychoacoustics/hearing/vibration plots (#253)

Add a `language` keyword ("en" default, "es") to the `.plot()` methods of
every psychoacoustics, hearing and vibration result. Spanish renders the axis
labels, titles and legends in Spanish and switches the decimal separator to a
comma; the English output is unchanged.

- New `phonometry._i18n` module with the shared locale helpers (language
validation, locale-aware number formatting and linear-axis tick localisation).
- Each `_plot` renderer gains a module-level string table and localises its
fixed text; the `_i18n` helpers are imported lazily inside the functions so
the render leaves keep no module-level domain imports.
- Per-domain tests covering a Spanish label and the unknown-language error.
- Regenerated the API reference for the new `.plot()` signatures.

authored by

José M. Requena Plens and committed by
GitHub
(Jul 20, 2026, 1:26 PM +0200) 36dc2268 50a195ea

+964 -221
+4
CHANGELOG.md
··· 21 21 objects accept a `language` keyword (`"en"`, the default, or `"es"`). Spanish 22 22 renders localised axis labels, titles and legends with comma decimal 23 23 separators; English output is unchanged. 24 + - The `.plot()` methods of the psychoacoustics, hearing and vibration results 25 + accept a `language` keyword (`"en"`, the default, or `"es"`). Spanish renders 26 + the axis labels, titles and legends in Spanish and switches the decimal 27 + separator to a comma; English output is unchanged. 24 28 - IEC 61260-1 filter class compliance can now be exported as a one-page PDF 25 29 fiche. A new `filter_class_compliance(bank, *, num_points=..., edition=...)` 26 30 runs the same verification as `verify_filter_class` and returns a frozen
+12 -2
site/src/content/docs/reference/api/hearing/noise-induced-hearing-loss.md
··· 102 102 ### HtlanResult.plot() 103 103 104 104 ```python 105 - HtlanResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 105 + HtlanResult.plot( 106 + ax: Axes | None = None, 107 + *, 108 + language: str = 'en', 109 + **kwargs: Any, 110 + ) -> Axes 106 111 ``` 107 112 108 113 Plot the age, noise and combined threshold components over frequency. ··· 183 188 ### NiptsResult.plot() 184 189 185 190 ```python 186 - NiptsResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 191 + NiptsResult.plot( 192 + ax: Axes | None = None, 193 + *, 194 + language: str = 'en', 195 + **kwargs: Any, 196 + ) -> Axes 187 197 ``` 188 198 189 199 Plot the NIPTS spectrum with the fractile band over frequency.
+6 -1
site/src/content/docs/reference/api/hearing/occupational-exposure.md
··· 89 89 ### ExposureResult.plot() 90 90 91 91 ```python 92 - ExposureResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 92 + ExposureResult.plot( 93 + ax: Axes | None = None, 94 + *, 95 + language: str = 'en', 96 + **kwargs: Any, 97 + ) -> Axes 93 98 ``` 94 99 95 100 Plot the per-task contributions with the `LEX,8h` line.
+6 -1
site/src/content/docs/reference/api/hearing/threshold.md
··· 99 99 ### AgeThresholdResult.plot() 100 100 101 101 ```python 102 - AgeThresholdResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 102 + AgeThresholdResult.plot( 103 + ax: Axes | None = None, 104 + *, 105 + language: str = 'en', 106 + **kwargs: Any, 107 + ) -> Axes 103 108 ``` 104 109 105 110 Plot the median threshold with the fractile band over frequency.
+2
site/src/content/docs/reference/api/psychoacoustics/fluctuation-strength-ecma.md
··· 125 125 ```python 126 126 EcmaFluctuationStrength.plot( 127 127 ax: Axes | None = None, 128 + *, 129 + language: str = 'en', 128 130 **kwargs: Any, 129 131 ) -> Axes | np.ndarray 130 132 ```
+2
site/src/content/docs/reference/api/psychoacoustics/fluctuation-strength.md
··· 176 176 ```python 177 177 FluctuationStrengthResult.plot( 178 178 ax: Axes | None = None, 179 + *, 180 + language: str = 'en', 179 181 **kwargs: Any, 180 182 ) -> Axes 181 183 ```
+2
site/src/content/docs/reference/api/psychoacoustics/loudness-ecma.md
··· 78 78 ```python 79 79 EcmaLoudness.plot( 80 80 ax: Axes | None = None, 81 + *, 82 + language: str = 'en', 81 83 **kwargs: Any, 82 84 ) -> Axes | np.ndarray 83 85 ```
+2
site/src/content/docs/reference/api/psychoacoustics/loudness-moore-glasberg-time.md
··· 136 136 ```python 137 137 MooreGlasbergTimeVaryingLoudness.plot( 138 138 ax: Axes | None = None, 139 + *, 140 + language: str = 'en', 139 141 **kwargs: Any, 140 142 ) -> Axes 141 143 ```
+6 -1
site/src/content/docs/reference/api/psychoacoustics/loudness-moore-glasberg.md
··· 167 167 ### MooreGlasbergLoudness.plot() 168 168 169 169 ```python 170 - MooreGlasbergLoudness.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 170 + MooreGlasbergLoudness.plot( 171 + ax: Axes | None = None, 172 + *, 173 + language: str = 'en', 174 + **kwargs: Any, 175 + ) -> Axes 171 176 ``` 172 177 173 178 Plot the specific loudness N'(i) over the ERB-number scale.
+2
site/src/content/docs/reference/api/psychoacoustics/loudness-zwicker.md
··· 128 128 ```python 129 129 ZwickerLoudness.plot( 130 130 ax: Axes | None = None, 131 + *, 132 + language: str = 'en', 131 133 **kwargs: Any, 132 134 ) -> Axes | np.ndarray 133 135 ```
+2
site/src/content/docs/reference/api/psychoacoustics/psychoacoustic-annoyance.md
··· 154 154 ```python 155 155 PsychoacousticAnnoyanceResult.plot( 156 156 ax: Axes | None = None, 157 + *, 158 + language: str = 'en', 157 159 **kwargs: Any, 158 160 ) -> Axes 159 161 ```
+2
site/src/content/docs/reference/api/psychoacoustics/roughness-ecma.md
··· 84 84 ```python 85 85 EcmaRoughness.plot( 86 86 ax: Axes | None = None, 87 + *, 88 + language: str = 'en', 87 89 **kwargs: Any, 88 90 ) -> Axes | np.ndarray 89 91 ```
+2
site/src/content/docs/reference/api/psychoacoustics/tonality-ecma.md
··· 64 64 ```python 65 65 EcmaTonality.plot( 66 66 ax: Axes | None = None, 67 + *, 68 + language: str = 'en', 67 69 **kwargs: Any, 68 70 ) -> Axes | np.ndarray 69 71 ```
+6 -1
site/src/content/docs/reference/api/psychoacoustics/tone-audibility.md
··· 745 745 ### ToneAudibilityResult.plot() 746 746 747 747 ```python 748 - ToneAudibilityResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 748 + ToneAudibilityResult.plot( 749 + ax: Axes | None = None, 750 + *, 751 + language: str = 'en', 752 + **kwargs: Any, 753 + ) -> Axes 749 754 ``` 750 755 751 756 Plot the per-tone audibility `ΔL` against tone frequency.
+6 -1
site/src/content/docs/reference/api/speech/objective-intelligibility.md
··· 110 110 ### STOIResult.plot() 111 111 112 112 ```python 113 - STOIResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 113 + STOIResult.plot( 114 + ax: Axes | None = None, 115 + *, 116 + language: str = 'en', 117 + **kwargs: Any, 118 + ) -> Axes 114 119 ``` 115 120 116 121 Plot the intermediate intelligibility that averages to the index.
+6 -1
site/src/content/docs/reference/api/speech/sii.md
··· 54 54 ### SIIResult.plot() 55 55 56 56 ```python 57 - SIIResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 57 + SIIResult.plot( 58 + ax: Axes | None = None, 59 + *, 60 + language: str = 'en', 61 + **kwargs: Any, 62 + ) -> Axes 58 63 ``` 59 64 60 65 Plot the per-band audibility weighted by importance, with the SII.
+6 -1
site/src/content/docs/reference/api/speech/sti.md
··· 179 179 ### STIResult.plot() 180 180 181 181 ```python 182 - STIResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 182 + STIResult.plot( 183 + ax: Axes | None = None, 184 + *, 185 + language: str = 'en', 186 + **kwargs: Any, 187 + ) -> Axes 183 188 ``` 184 189 185 190 Plot the per-band MTI bars with the STI and rating letter.
+18 -3
site/src/content/docs/reference/api/vibration/human-vibration.md
··· 220 220 ### DailyVibrationExposure.plot() 221 221 222 222 ```python 223 - DailyVibrationExposure.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 223 + DailyVibrationExposure.plot( 224 + ax: Axes | None = None, 225 + *, 226 + language: str = 'en', 227 + **kwargs: Any, 228 + ) -> Axes 224 229 ``` 225 230 226 231 Plot the partial exposures against the EAV / ELV thresholds. ··· 696 701 ### WeightedSpectrum.plot() 697 702 698 703 ```python 699 - WeightedSpectrum.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 704 + WeightedSpectrum.plot( 705 + ax: Axes | None = None, 706 + *, 707 + language: str = 'en', 708 + **kwargs: Any, 709 + ) -> Axes 700 710 ``` 701 711 702 712 Plot the unweighted and weighted band spectra with `a_w`. ··· 759 769 ### WeightingResponse.plot() 760 770 761 771 ```python 762 - WeightingResponse.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 772 + WeightingResponse.plot( 773 + ax: Axes | None = None, 774 + *, 775 + language: str = 'en', 776 + **kwargs: Any, 777 + ) -> Axes 763 778 ``` 764 779 765 780 Plot the weighting factor (dB) versus frequency.
+6 -1
site/src/content/docs/reference/api/vibration/mechanical-mobility.md
··· 151 151 ### MobilityResult.plot() 152 152 153 153 ```python 154 - MobilityResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 154 + MobilityResult.plot( 155 + ax: Axes | None = None, 156 + *, 157 + language: str = 'en', 158 + **kwargs: Any, 159 + ) -> Axes 155 160 ``` 156 161 157 162 Plot the mobility magnitude `|Y(f)|`.
+6 -1
site/src/content/docs/reference/api/vibration/multiple-shock-vibration.md
··· 285 285 ### MultipleShockResult.plot() 286 286 287 287 ```python 288 - MultipleShockResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 288 + MultipleShockResult.plot( 289 + ax: Axes | None = None, 290 + *, 291 + language: str = 'en', 292 + **kwargs: Any, 293 + ) -> Axes 289 294 ``` 290 295 291 296 Plot the injury-probability curve with this assessment's `R`.
+2
site/src/content/docs/reference/api/vibration/radiation-efficiency.md
··· 167 167 ```python 168 168 RadiationEfficiencyResult.plot( 169 169 ax: Axes | None = None, 170 + *, 171 + language: str = 'en', 170 172 **kwargs: Any, 171 173 ) -> Axes 172 174 ```
+6 -1
site/src/content/docs/reference/api/vibration/transfer-stiffness.md
··· 356 356 ### TransferStiffnessResult.plot() 357 357 358 358 ```python 359 - TransferStiffnessResult.plot(ax: Axes | None = None, **kwargs: Any) -> Axes 359 + TransferStiffnessResult.plot( 360 + ax: Axes | None = None, 361 + *, 362 + language: str = 'en', 363 + **kwargs: Any, 364 + ) -> Axes 360 365 ``` 361 366 362 367 Plot the transfer-stiffness level `L_k(f)`.
+206 -48
src/phonometry/_plot/hearing.py
··· 31 31 from ..hearing.sti import STIResult 32 32 from ..hearing.objective_intelligibility import STOIResult 33 33 34 - def plot_sti(result: STIResult, ax: Axes | None = None, **kwargs: Any) -> Axes: 34 + #: EN/ES text for every fixed label, title and legend of this module. The 35 + #: English entries are byte-for-byte the historical strings; ``_t`` returns 36 + #: them unchanged for ``language="en"``. 37 + _STRINGS: dict[str, dict[str, str]] = { 38 + "freq_hz": {"en": "Frequency [Hz]", "es": "Frecuencia [Hz]"}, 39 + "band": {"en": "Band", "es": "Banda"}, 40 + "mti": { 41 + "en": "Modulation transfer index MTI", 42 + "es": "Índice de transferencia de modulación MTI", 43 + }, 44 + "third_octave_band": { 45 + "en": "One-third-octave band [Hz]", 46 + "es": "Banda de tercio de octava [Hz]", 47 + }, 48 + "mean_corr": { 49 + "en": "Mean intermediate correlation", 50 + "es": "Correlación intermedia media", 51 + }, 52 + "spectral_corr": { 53 + "en": r"Spectral correlation $d_m$", 54 + "es": r"Correlación espectral $d_m$", 55 + }, 56 + "analysis_segment": {"en": "Analysis segment", "es": "Segmento de análisis"}, 57 + "band_audibility": {"en": "Band audibility", "es": "Audibilidad de banda"}, 58 + "band_audibility_ai": { 59 + "en": r"Band audibility $A_i$", 60 + "es": r"Audibilidad de banda $A_i$", 61 + }, 62 + "importance_weighted": { 63 + "en": r"Importance-weighted $I_i A_i$ (scaled)", 64 + "es": r"$I_i A_i$ ponderada por importancia (escalada)", 65 + }, 66 + "median": {"en": "Median", "es": "Mediana"}, 67 + "median_n50": {"en": r"Median $N_{50}$", "es": r"Mediana $N_{50}$"}, 68 + "threshold_dev_18": { 69 + "en": "Threshold deviation from age 18 [dB]", 70 + "es": "Desviación del umbral respecto a 18 años [dB]", 71 + }, 72 + "nipts_db": {"en": "NIPTS [dB]", "es": "NIPTS [dB]"}, 73 + "htla_age": {"en": "Age (HTLA, ISO 7029)", "es": "Edad (HTLA, ISO 7029)"}, 74 + "noise_nipts": {"en": "Noise (NIPTS)", "es": "Ruido (NIPTS)"}, 75 + "age_noise_htlan": { 76 + "en": "Age + noise (HTLAN)", 77 + "es": "Edad + ruido (HTLAN)", 78 + }, 79 + "htl_level": { 80 + "en": "Hearing threshold level [dB]", 81 + "es": "Nivel del umbral de audición [dB]", 82 + }, 83 + "a_weighted_level": { 84 + "en": "A-weighted level [dB]", 85 + "es": "Nivel ponderado A [dB]", 86 + }, 87 + # Templates (``.format`` fields hold already-localised numbers). 88 + "sti_title": { 89 + "en": "IEC 60268-16 STI = {sti} (rating {rating})", 90 + "es": "IEC 60268-16 STI = {sti} (calificación {rating})", 91 + }, 92 + "stoi_title": {"en": "{name} = {v}", "es": "{name} = {v}"}, 93 + "sii_title": {"en": "ANSI S3.5 SII = {sii}", "es": "ANSI S3.5 SII = {sii}"}, 94 + "fractile": {"en": "Fractile {v}", "es": "Fractil {v}"}, 95 + "iso7029_title": { 96 + "en": "ISO 7029 hearing threshold — {sex}, age {age}", 97 + "es": "ISO 7029 umbral de audición — {sex}, edad {age}", 98 + }, 99 + "nipts_title": { 100 + "en": "ISO 1999 NIPTS — $L_{{EX,8h}}$ = {lex} dB, {years} yr", 101 + "es": "ISO 1999 NIPTS — $L_{{EX,8h}}$ = {lex} dB, {years} años", 102 + }, 103 + "htlan_title": { 104 + "en": "ISO 1999 HTLAN — {sex}, age {age}, {lex} dB / {years} yr", 105 + "es": "ISO 1999 HTLAN — {sex}, edad {age}, {lex} dB / {years} años", 106 + }, 107 + "occupational_title": { 108 + "en": ("ISO 9612 daily noise exposure — $L_{{EX,8h}}$ = " 109 + "{lex} dB (U = {u} dB)"), 110 + "es": ("ISO 9612 exposición diaria al ruido — $L_{{EX,8h}}$ = " 111 + "{lex} dB (U = {u} dB)"), 112 + }, 113 + } 114 + 115 + 116 + def _t(key: str, language: str) -> str: 117 + """Look up the localised text for ``key`` (falls back to English).""" 118 + entry = _STRINGS[key] 119 + return entry.get(language, entry["en"]) 120 + 121 + 122 + def plot_sti( 123 + result: STIResult, ax: Axes | None = None, *, language: str = "en", 124 + **kwargs: Any, 125 + ) -> Axes: 35 126 """Per-band modulation transfer index bars with the STI and rating. 36 127 37 128 :param result: A :class:`~phonometry.sti.STIResult`. 38 129 :param ax: Existing axes, or ``None`` to create a figure. 130 + :param language: Label language, ``"en"`` (default) or ``"es"``. 39 131 :param kwargs: Forwarded to :meth:`~matplotlib.axes.Axes.bar`. 40 132 :return: The axes. 41 133 """ 134 + from .._i18n import format_number, localize_axes 135 + 42 136 ax = ax if ax is not None else _new_axes() 43 137 mti = np.asarray(result.mti, dtype=np.float64) 44 138 positions = np.arange(mti.size) 45 139 kwargs.setdefault("color", _C_PRIMARY) 46 140 ax.bar(positions, mti, **kwargs) 47 - if mti.size == len(_STI_BAND_CENTERS): 141 + banded = mti.size == len(_STI_BAND_CENTERS) 142 + if banded: 48 143 _band_axis(ax, np.asarray(_STI_BAND_CENTERS)) 144 + ax.set_xlabel(_t("freq_hz", language)) 49 145 else: 50 146 ax.set_xticks(positions) 51 - ax.set_xlabel("Band") 52 - ax.set_ylabel("Modulation transfer index MTI") 147 + ax.set_xlabel(_t("band", language)) 148 + ax.set_ylabel(_t("mti", language)) 53 149 ax.set_ylim(0.0, 1.0) 54 - ax.set_title(f"IEC 60268-16 STI = {result.sti:.2f} (rating {result.rating})") 150 + ax.set_title(_t("sti_title", language).format( 151 + sti=format_number(result.sti, language, decimals=2), rating=result.rating, 152 + )) 55 153 ax.grid(True, axis="y", alpha=0.3) 154 + # localize_axes leaves the categorical band axis (a FuncFormatter) alone. 155 + localize_axes(ax, language) 56 156 return ax 57 157 58 - def plot_stoi(result: "STOIResult", ax: Axes | None = None, **kwargs: Any) -> Axes: 158 + 159 + def plot_stoi( 160 + result: "STOIResult", ax: Axes | None = None, *, language: str = "en", 161 + **kwargs: Any, 162 + ) -> Axes: 59 163 """Intermediate intelligibility that averages to the STOI/ESTOI index. 60 164 61 165 For STOI the intermediate measure decomposes per one-third-octave band, so ··· 65 169 66 170 :param result: A :class:`~phonometry.hearing.objective_intelligibility.STOIResult`. 67 171 :param ax: Existing axes, or ``None`` to create a figure. 172 + :param language: Label language, ``"en"`` (default) or ``"es"``. 68 173 :param kwargs: Forwarded to the underlying :meth:`bar`/:meth:`plot`. 69 174 :return: The axes. 70 175 """ 176 + from .._i18n import decimal_comma, format_number, localize_axes 177 + 71 178 ax = ax if ax is not None else _new_axes() 72 179 name = "ESTOI" if result.extended else "STOI" 73 - if result.band_scores is not None: 180 + banded = result.band_scores is not None 181 + if banded: 74 182 freqs = np.asarray(result.band_frequencies, dtype=np.float64) 75 183 scores = np.asarray(result.band_scores, dtype=np.float64) 76 184 positions = np.arange(scores.size) 77 185 kwargs.setdefault("color", _C_PRIMARY) 78 186 ax.bar(positions, scores, **kwargs) 79 187 ax.set_xticks(positions) 80 - ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 81 - ax.set_xlabel("One-third-octave band [Hz]") 82 - ax.set_ylabel("Mean intermediate correlation") 188 + ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], 189 + rotation=45, ha="right") 190 + ax.set_xlabel(_t("third_octave_band", language)) 191 + ax.set_ylabel(_t("mean_corr", language)) 83 192 else: 84 193 scores = np.asarray(result.segment_scores, dtype=np.float64) 85 194 kwargs.setdefault("color", _C_PRIMARY) 86 195 ax.plot(np.arange(scores.size), scores, **kwargs) 87 - ax.set_xlabel("Analysis segment") 88 - ax.set_ylabel("Spectral correlation $d_m$") 196 + ax.set_xlabel(_t("analysis_segment", language)) 197 + ax.set_ylabel(_t("spectral_corr", language)) 89 198 # The intermediate correlations are cosine-similarity quantities in 90 199 # [-1, 1] (unlike the [0, 1] ratios of plot_sti/plot_sii), so keep room 91 200 # for anti-correlated bands rather than clipping them at zero. 92 201 ax.set_ylim(-1.0, 1.0) 93 - ax.set_title(f"{name} = {result.value:.3f}") 202 + ax.set_title(_t("stoi_title", language).format( 203 + name=name, v=format_number(result.value, language, decimals=3), 204 + )) 94 205 ax.grid(True, axis="y", alpha=0.3) 206 + # localize_axes leaves the categorical band labels (a FuncFormatter) alone, 207 + # so the comma-localized labels set above survive. 208 + localize_axes(ax, language) 95 209 return ax 96 210 97 - def plot_sii(result: "SIIResult", ax: Axes | None = None, **kwargs: Any) -> Axes: 211 + 212 + def plot_sii( 213 + result: "SIIResult", ax: Axes | None = None, *, language: str = "en", 214 + **kwargs: Any, 215 + ) -> Axes: 98 216 """Per-band audibility and its importance-weighted contribution to the SII. 99 217 100 218 :param result: A :class:`~phonometry.sii.SIIResult`. 101 219 :param ax: Existing axes, or ``None`` to create a figure. 220 + :param language: Label language, ``"en"`` (default) or ``"es"``. 102 221 :param kwargs: Forwarded to the weighted-contribution :meth:`bar`. 103 222 :return: The axes. 104 223 """ 224 + from .._i18n import format_number, localize_axes 225 + 105 226 ax = ax if ax is not None else _new_axes() 106 227 freqs = np.asarray(result.frequencies, dtype=np.float64) 107 228 audibility = np.asarray(result.band_audibility, dtype=np.float64) 108 229 contribution = audibility * np.asarray(result.band_importance, dtype=np.float64) 109 230 positions = _band_axis(ax, freqs) 231 + ax.set_xlabel(_t("freq_hz", language)) 110 232 ax.bar(positions, audibility, color=_C_PRIMARY_LIGHT, 111 - label="Band audibility $A_i$") 233 + label=_t("band_audibility_ai", language)) 112 234 kwargs.setdefault("color", _C_PRIMARY) 113 235 # A fully masked speech signal (SII = 0) has an all-zero contribution; 114 236 # keep the zero bars rather than dividing 0/0 into NaN. 115 237 peak = float(contribution.max()) if contribution.size else 0.0 116 238 scaled = contribution / peak if peak > 0.0 else contribution 117 239 ax.bar(positions, scaled, width=0.5, 118 - label=r"Importance-weighted $I_i A_i$ (scaled)", **kwargs) 119 - ax.set_ylabel("Band audibility") 240 + label=_t("importance_weighted", language), **kwargs) 241 + ax.set_ylabel(_t("band_audibility", language)) 120 242 ax.set_ylim(0.0, 1.0) 121 - ax.set_title(f"ANSI S3.5 SII = {result.sii:.3f}") 243 + ax.set_title(_t("sii_title", language).format( 244 + sii=format_number(result.sii, language, decimals=3), 245 + )) 122 246 ax.legend(loc=_LEGEND_UPPER_RIGHT, fontsize="small") 123 247 ax.grid(True, axis="y", alpha=0.3) 248 + # localize_axes leaves the categorical band axis (a FuncFormatter) alone. 249 + localize_axes(ax, language) 124 250 return ax 251 + 125 252 126 253 def plot_age_threshold( 127 - result: "AgeThresholdResult", ax: Axes | None = None, **kwargs: Any 254 + result: "AgeThresholdResult", ax: Axes | None = None, *, language: str = "en", 255 + **kwargs: Any, 128 256 ) -> Axes: 129 257 """Median age-related hearing threshold with the 10-90 % fractile band. 130 258 131 259 :param result: An :class:`~phonometry.hearing.AgeThresholdResult`. 132 260 :param ax: Existing axes, or ``None`` to create a figure. 261 + :param language: Label language, ``"en"`` (default) or ``"es"``. 133 262 :param kwargs: Forwarded to the median line ``plot``. 134 263 :return: The axes. 135 264 """ 265 + from .._i18n import decimal_comma, localize_axes 266 + 136 267 ax = ax if ax is not None else _new_axes() 137 268 freqs = np.asarray(result.frequencies, dtype=np.float64) 138 269 median = np.asarray(result.median, dtype=np.float64) ··· 141 272 142 273 _fractile_band(ax, freqs, median, sl, su, color=_C_PRIMARY_LIGHT) 143 274 kwargs.setdefault("color", _C_PRIMARY) 144 - ax.plot(freqs, median, "o-", label="Median", **kwargs) 275 + ax.plot(freqs, median, "o-", label=_t("median", language), **kwargs) 145 276 if abs(result.fractile - 0.5) > 1e-9: 146 277 ax.plot(freqs, np.asarray(result.threshold, dtype=np.float64), "s--", 147 - color=_C_REFERENCE, label=f"Fractile {result.fractile:g}") 278 + color=_C_REFERENCE, label=_t("fractile", language).format( 279 + v=decimal_comma(f"{result.fractile:g}", language))) 148 280 _freq_axis(ax, freqs) 149 - ax.set_ylabel("Threshold deviation from age 18 [dB]") 281 + ax.set_xlabel(_t("freq_hz", language)) 282 + ax.set_ylabel(_t("threshold_dev_18", language)) 150 283 ax.invert_yaxis() # audiogram convention: worse hearing downward 151 - ax.set_title(f"ISO 7029 hearing threshold — {result.sex}, age {result.age:g}") 284 + ax.set_title(_t("iso7029_title", language).format( 285 + sex=result.sex, age=decimal_comma(f"{result.age:g}", language))) 152 286 ax.legend(loc=_LEGEND_UPPER_RIGHT, fontsize="small") 153 287 ax.grid(True, which="both", alpha=0.3) 288 + localize_axes(ax, language) 154 289 return ax 155 290 291 + 156 292 def plot_nipts( 157 - result: "NiptsResult", ax: Axes | None = None, **kwargs: Any 293 + result: "NiptsResult", ax: Axes | None = None, *, language: str = "en", 294 + **kwargs: Any, 158 295 ) -> Axes: 159 296 """Median NIPTS spectrum with the 10-90 % fractile band (ISO 1999). 160 297 161 298 :param result: A :class:`~phonometry.noise_induced_hearing_loss.NiptsResult`. 162 299 :param ax: Existing axes, or ``None`` to create a figure. 300 + :param language: Label language, ``"en"`` (default) or ``"es"``. 163 301 :param kwargs: Forwarded to the median line ``plot``. 164 302 :return: The axes. 165 303 """ 304 + from .._i18n import decimal_comma, localize_axes 305 + 166 306 ax = ax if ax is not None else _new_axes() 167 307 freqs = np.asarray(result.frequencies, dtype=np.float64) 168 308 median = np.asarray(result.median, dtype=np.float64) ··· 171 311 172 312 _fractile_band(ax, freqs, median, dl, du, color=_C_SECONDARY_LIGHT, floor=0.0) 173 313 kwargs.setdefault("color", _C_SECONDARY) 174 - ax.plot(freqs, median, "o-", label="Median $N_{50}$", **kwargs) 314 + ax.plot(freqs, median, "o-", label=_t("median_n50", language), **kwargs) 175 315 if abs(result.fractile - 0.5) > 1e-9: 176 316 ax.plot(freqs, np.asarray(result.value, dtype=np.float64), "s--", 177 - color=_C_REFERENCE, label=f"Fractile {result.fractile:g}") 317 + color=_C_REFERENCE, label=_t("fractile", language).format( 318 + v=decimal_comma(f"{result.fractile:g}", language))) 178 319 _freq_axis(ax, freqs) 179 - ax.set_ylabel("NIPTS [dB]") 320 + ax.set_xlabel(_t("freq_hz", language)) 321 + ax.set_ylabel(_t("nipts_db", language)) 180 322 ax.invert_yaxis() # audiogram convention: worse hearing downward 181 - ax.set_title( 182 - f"ISO 1999 NIPTS — $L_{{EX,8h}}$ = {result.l_ex:g} dB, " 183 - f"{result.years:g} yr" 184 - ) 323 + ax.set_title(_t("nipts_title", language).format( 324 + lex=decimal_comma(f"{result.l_ex:g}", language), 325 + years=decimal_comma(f"{result.years:g}", language))) 185 326 ax.legend(loc=_LEGEND_UPPER_RIGHT, fontsize="small") 186 327 ax.grid(True, which="both", alpha=0.3) 328 + localize_axes(ax, language) 187 329 return ax 330 + 188 331 189 332 def plot_htlan( 190 - result: "HtlanResult", ax: Axes | None = None, **kwargs: Any 333 + result: "HtlanResult", ax: Axes | None = None, *, language: str = "en", 334 + **kwargs: Any, 191 335 ) -> Axes: 192 336 """Age, noise and combined hearing threshold components (ISO 1999, 6.1). 193 337 194 338 :param result: A :class:`~phonometry.noise_induced_hearing_loss.HtlanResult`. 195 339 :param ax: Existing axes, or ``None`` to create a figure. 340 + :param language: Label language, ``"en"`` (default) or ``"es"``. 196 341 :param kwargs: Forwarded to the combined-threshold line ``plot``. 197 342 :return: The axes. 198 343 """ 344 + from .._i18n import decimal_comma, localize_axes 345 + 199 346 ax = ax if ax is not None else _new_axes() 200 347 freqs = np.asarray(result.frequencies, dtype=np.float64) 201 348 ax.plot(freqs, np.asarray(result.htla, dtype=np.float64), "o-", 202 - color=_C_PRIMARY, label="Age (HTLA, ISO 7029)") 349 + color=_C_PRIMARY, label=_t("htla_age", language)) 203 350 ax.plot(freqs, np.asarray(result.nipts, dtype=np.float64), "^-", 204 - color=_C_SECONDARY, label="Noise (NIPTS)") 351 + color=_C_SECONDARY, label=_t("noise_nipts", language)) 205 352 kwargs.setdefault("color", _C_REFERENCE) 206 353 ax.plot(freqs, np.asarray(result.threshold, dtype=np.float64), "s--", 207 - label="Age + noise (HTLAN)", **kwargs) 354 + label=_t("age_noise_htlan", language), **kwargs) 208 355 _freq_axis(ax, freqs) 209 - ax.set_ylabel("Hearing threshold level [dB]") 356 + ax.set_xlabel(_t("freq_hz", language)) 357 + ax.set_ylabel(_t("htl_level", language)) 210 358 ax.invert_yaxis() # audiogram convention: worse hearing downward 211 - ax.set_title( 212 - f"ISO 1999 HTLAN — {result.sex}, age {result.age:g}, " 213 - f"{result.l_ex:g} dB / {result.years:g} yr" 214 - ) 359 + ax.set_title(_t("htlan_title", language).format( 360 + sex=result.sex, age=decimal_comma(f"{result.age:g}", language), 361 + lex=decimal_comma(f"{result.l_ex:g}", language), 362 + years=decimal_comma(f"{result.years:g}", language))) 215 363 ax.legend(loc="lower left", fontsize="small") 216 364 ax.grid(True, which="both", alpha=0.3) 365 + localize_axes(ax, language) 217 366 return ax 218 367 368 + 219 369 def plot_occupational_exposure( 220 - result: "ExposureResult", ax: Axes | None = None, **kwargs: Any 370 + result: "ExposureResult", ax: Axes | None = None, *, language: str = "en", 371 + **kwargs: Any, 221 372 ) -> Axes: 222 373 """Per-task contributions to the daily exposure level (ISO 9612). 223 374 ··· 229 380 :class:`~phonometry.occupational_exposure.ExposureResult` from the 230 381 task-based strategy (the one that carries per-task contributions). 231 382 :param ax: Existing axes, or ``None`` to create a figure. 383 + :param language: Label language, ``"en"`` (default) or ``"es"``. 232 384 :param kwargs: Forwarded to the task :meth:`~matplotlib.axes.Axes.bar`. 233 385 :return: The axes. 234 386 :raises ValueError: If the result carries no per-task contributions. 235 387 """ 388 + from .._i18n import format_number, localize_axes 389 + 236 390 if not result.tasks: 237 391 raise ValueError( 238 392 "plot() needs per-task contributions; only task_based_exposure() " ··· 248 402 ax.set_xticklabels(labels, rotation=45, ha="right") 249 403 250 404 ax.axhline(result.lex_8h, color=_C_REFERENCE, ls="--", 251 - label=f"$L_{{EX,8h}}$ = {result.lex_8h:.1f} dB") 405 + label="$L_{EX,8h}$ = " 406 + + format_number(result.lex_8h, language, decimals=1) + " dB") 252 407 ax.axhline(result.upper_limit, color=_C_MUTED, ls=":", 253 - label=f"$L_{{EX,8h}} + U$ = {result.upper_limit:.1f} dB") 408 + label="$L_{EX,8h} + U$ = " 409 + + format_number(result.upper_limit, language, decimals=1) 410 + + " dB") 254 411 top = max(result.upper_limit, max(contributions)) 255 412 bottom = min(0.0, min(contributions)) 256 413 ax.set_ylim(bottom * 1.12 if bottom < 0.0 else 0.0, top * 1.12) 257 - ax.set_ylabel("A-weighted level [dB]") 258 - ax.set_title( 259 - f"ISO 9612 daily noise exposure — $L_{{EX,8h}}$ = " 260 - f"{result.lex_8h:.1f} dB (U = {result.expanded_uncertainty:.1f} dB)" 261 - ) 414 + ax.set_ylabel(_t("a_weighted_level", language)) 415 + ax.set_title(_t("occupational_title", language).format( 416 + lex=format_number(result.lex_8h, language, decimals=1), 417 + u=format_number(result.expanded_uncertainty, language, decimals=1))) 262 418 ax.legend(loc="lower right", fontsize="small") 263 419 ax.grid(True, axis="y", alpha=0.3) 420 + # localize_axes leaves the categorical task-label axis (a FuncFormatter) alone. 421 + localize_axes(ax, language) 264 422 return ax
+275 -70
src/phonometry/_plot/psychoacoustics.py
··· 33 33 from ..psychoacoustics.fluctuation_strength_ecma import EcmaFluctuationStrength 34 34 from ..psychoacoustics.psychoacoustic_annoyance import PsychoacousticAnnoyanceResult 35 35 36 + #: EN/ES text for every fixed label, title and legend of this module. The 37 + #: English entries are byte-for-byte the historical strings; ``_t`` returns 38 + #: them unchanged for ``language="en"``. 39 + _STRINGS: dict[str, dict[str, str]] = { 40 + "cbr_bark": { 41 + "en": "Critical-band rate z [Bark]", 42 + "es": "Razón de banda crítica z [Bark]", 43 + }, 44 + "cbr_bark_hms": { 45 + "en": "Critical-band rate z [Bark_HMS]", 46 + "es": "Razón de banda crítica z [Bark_HMS]", 47 + }, 48 + "spec_loudness_bark": { 49 + "en": "Specific loudness N' [sone/Bark]", 50 + "es": "Sonoridad específica N' [sonios/Bark]", 51 + }, 52 + "spec_loudness_hms": { 53 + "en": "Specific loudness N' [sone_HMS/Bark_HMS]", 54 + "es": "Sonoridad específica N' [sonios_HMS/Bark_HMS]", 55 + }, 56 + "spec_loudness_cam": { 57 + "en": "Specific loudness N' [sone/Cam]", 58 + "es": "Sonoridad específica N' [sonios/Cam]", 59 + }, 60 + "erb_cam": {"en": "ERB number [Cam]", "es": "Número ERB [Cam]"}, 61 + "time_s": {"en": "Time [s]", "es": "Tiempo [s]"}, 62 + "loudness_n_sone": {"en": "Loudness N [sone]", "es": "Sonoridad N [sonios]"}, 63 + "loudness_n_sone_hms": { 64 + "en": "Loudness N [sone_HMS]", 65 + "es": "Sonoridad N [sonios_HMS]", 66 + }, 67 + "loudness_sone": {"en": "Loudness [sone]", "es": "Sonoridad [sonios]"}, 68 + "short_term_loudness": { 69 + "en": "Short-term loudness", 70 + "es": "Sonoridad a corto plazo", 71 + }, 72 + "long_term_loudness": { 73 + "en": "Long-term loudness", 74 + "es": "Sonoridad a largo plazo", 75 + }, 76 + "spec_tonality": { 77 + "en": "Specific tonality T' [tu_HMS]", 78 + "es": "Tonalidad específica T' [tu_HMS]", 79 + }, 80 + "tonality_t": {"en": "Tonality T [tu_HMS]", "es": "Tonalidad T [tu_HMS]"}, 81 + "roughness_r": {"en": "Roughness R [asper]", "es": "Aspereza R [asper]"}, 82 + "fluct_f_hms": { 83 + "en": "Fluctuation strength F [vacil_HMS]", 84 + "es": "Intensidad de fluctuación F [vacil_HMS]", 85 + }, 86 + "spec_fluct": { 87 + "en": r"Specific fluctuation strength $f'(z)$ [vacil/Bark]", 88 + "es": r"Intensidad de fluctuación específica $f'(z)$ [vacil/Bark]", 89 + }, 90 + "value": {"en": "Value", "es": "Valor"}, 91 + "tone_frequency": {"en": "Tone frequency [Hz]", "es": "Frecuencia del tono [Hz]"}, 92 + "audibility_dl": { 93 + "en": r"Audibility $\Delta L$ [dB]", 94 + "es": r"Audibilidad $\Delta L$ [dB]", 95 + }, 96 + "audibility_threshold": { 97 + "en": r"threshold $\Delta L=0$ dB", 98 + "es": r"umbral $\Delta L=0$ dB", 99 + }, 100 + "tone_audibility_title": { 101 + "en": "ISO/PAS 20065 tonal audibility", 102 + "es": "Audibilidad tonal ISO/PAS 20065", 103 + }, 104 + # Templates (``.format`` fields hold already-localised numbers). 105 + "zwicker_title": { 106 + "en": "ISO 532-1 loudness N = {n} sone ({ln} phon)", 107 + "es": "ISO 532-1 sonoridad N = {n} sonios ({ln} fonios)", 108 + }, 109 + "ecma_loudness_title": { 110 + "en": "ECMA-418-2 loudness N = {n} sone_HMS", 111 + "es": "ECMA-418-2 sonoridad N = {n} sonios_HMS", 112 + }, 113 + "mg_loudness_title": { 114 + "en": "ISO 532-2 loudness N = {n} sone ({ln} phon)", 115 + "es": "ISO 532-2 sonoridad N = {n} sonios ({ln} fonios)", 116 + }, 117 + "mg_time_title": { 118 + "en": "ISO 532-3 peak long-term loudness N = {n} sone ({ln} phon)", 119 + "es": "ISO 532-3 sonoridad a largo plazo máxima N = {n} sonios ({ln} fonios)", 120 + }, 121 + "ecma_tonality_title": { 122 + "en": "ECMA-418-2 tonality T = {t} tu_HMS", 123 + "es": "ECMA-418-2 tonalidad T = {t} tu_HMS", 124 + }, 125 + "ecma_roughness_title": { 126 + "en": "ECMA-418-2 roughness R = {r} asper", 127 + "es": "ECMA-418-2 aspereza R = {r} asper", 128 + }, 129 + "ecma_fluct_title": { 130 + "en": "ECMA-418-2 fluctuation strength F = {f} vacil_HMS", 131 + "es": "ECMA-418-2 intensidad de fluctuación F = {f} vacil_HMS", 132 + }, 133 + "fluct_title": { 134 + "en": "Fluctuation strength F = {f} vacil", 135 + "es": "Intensidad de fluctuación F = {f} vacil", 136 + }, 137 + "annoyance_title": { 138 + "en": "Psychoacoustic annoyance PA = {pa} (N5 = {n5} sone)", 139 + "es": "Molestia psicoacústica PA = {pa} (N5 = {n5} sonios)", 140 + }, 141 + "decisive_label": { 142 + "en": r"decisive $\Delta L$ = {da} dB @ {df} Hz", 143 + "es": r"decisiva $\Delta L$ = {da} dB @ {df} Hz", 144 + }, 145 + } 146 + 147 + 148 + def _t(key: str, language: str) -> str: 149 + """Look up the localised text for ``key`` (falls back to English).""" 150 + entry = _STRINGS[key] 151 + return entry.get(language, entry["en"]) 152 + 153 + 36 154 def plot_zwicker_loudness( 37 - result: ZwickerLoudness, ax: Axes | None = None, **kwargs: Any 155 + result: ZwickerLoudness, ax: Axes | None = None, *, language: str = "en", 156 + **kwargs: Any, 38 157 ) -> Axes | np.ndarray: 39 158 """Specific loudness N'(z) over the Bark scale (ISO 532-1). 40 159 ··· 46 165 47 166 :param result: A :class:`~phonometry.loudness_zwicker.ZwickerLoudness`. 48 167 :param ax: Existing axes to draw on, or ``None`` to create a figure. 168 + :param language: Label language, ``"en"`` (default) or ``"es"``. 49 169 :param kwargs: Forwarded to the specific-loudness line ``plot`` call. 50 170 :return: The axes, or an array of two axes for time-varying input. 51 171 """ 172 + from .._i18n import format_number, localize_axes 173 + 52 174 specific = np.asarray(result.specific, dtype=np.float64) 53 175 bark = np.arange(1, specific.size + 1) * 0.1 54 176 time_varying = ( ··· 64 186 kwargs.setdefault("color", _C_PRIMARY) 65 187 ax_specific.plot(bark, specific, **kwargs) 66 188 ax_specific.fill_between(bark, specific, color=kwargs["color"], alpha=0.25) 67 - ax_specific.set_xlabel("Critical-band rate z [Bark]") 68 - ax_specific.set_ylabel("Specific loudness N' [sone/Bark]") 189 + ax_specific.set_xlabel(_t("cbr_bark", language)) 190 + ax_specific.set_ylabel(_t("spec_loudness_bark", language)) 69 191 ax_specific.set_xlim(0.0, bark[-1]) 70 192 ax_specific.set_ylim(bottom=0.0) 71 - ax_specific.set_title( 72 - f"ISO 532-1 loudness N = {result.loudness:.2f} sone " 73 - f"({result.loudness_level:.1f} phon)" 74 - ) 193 + ax_specific.set_title(_t("zwicker_title", language).format( 194 + n=format_number(result.loudness, language, decimals=2), 195 + ln=format_number(result.loudness_level, language, decimals=1), 196 + )) 75 197 ax_specific.grid(True, alpha=0.3) 76 198 77 199 if not time_varying: 200 + localize_axes(ax_specific, language) 78 201 return ax_specific 79 202 80 203 ax_time = cast("Axes", axes[1]) ··· 83 206 ax_time.plot(time, lvt, color=_C_TERTIARY, label="N(t)") 84 207 if result.n5 is not None: 85 208 ax_time.axhline( 86 - result.n5, color=_C_REFERENCE, ls="--", lw=1, label=f"N5={result.n5:.2f}" 209 + result.n5, color=_C_REFERENCE, ls="--", lw=1, 210 + label="N5=" + format_number(result.n5, language, decimals=2), 87 211 ) 88 212 if result.n10 is not None: 89 213 ax_time.axhline( 90 - result.n10, color=_C_SECONDARY, ls=":", lw=1, label=f"N10={result.n10:.2f}" 214 + result.n10, color=_C_SECONDARY, ls=":", lw=1, 215 + label="N10=" + format_number(result.n10, language, decimals=2), 91 216 ) 92 - ax_time.set_xlabel("Time [s]") 93 - ax_time.set_ylabel("Loudness N [sone]") 217 + ax_time.set_xlabel(_t("time_s", language)) 218 + ax_time.set_ylabel(_t("loudness_n_sone", language)) 94 219 ax_time.set_ylim(bottom=0.0) 95 220 ax_time.grid(True, alpha=0.3) 96 221 ax_time.legend(loc="best", fontsize="small") 222 + localize_axes(ax_specific, language) 223 + localize_axes(ax_time, language) 97 224 return axes 225 + 98 226 99 227 def plot_ecma_loudness( 100 - result: EcmaLoudness, ax: Axes | None = None, **kwargs: Any 228 + result: EcmaLoudness, ax: Axes | None = None, *, language: str = "en", 229 + **kwargs: Any, 101 230 ) -> Axes | np.ndarray: 102 231 """Average specific loudness N'(z) and time-dependent loudness N(l). 103 232 ··· 108 237 109 238 :param result: An :class:`~phonometry.loudness_ecma.EcmaLoudness`. 110 239 :param ax: Existing axes to draw on, or ``None`` to create a figure. 240 + :param language: Label language, ``"en"`` (default) or ``"es"``. 111 241 :param kwargs: Forwarded to the specific-loudness line ``plot`` call. 112 242 :return: The axes, or an array of two axes. 113 243 """ 244 + from .._i18n import format_number, localize_axes 245 + 114 246 specific = np.asarray(result.specific_loudness, dtype=np.float64) 115 247 bark = np.asarray(result.bark, dtype=np.float64) 116 248 two_panel = ax is None ··· 124 256 kwargs.setdefault("color", _C_PRIMARY) 125 257 ax_specific.plot(bark, specific, **kwargs) 126 258 ax_specific.fill_between(bark, specific, color=kwargs["color"], alpha=0.25) 127 - ax_specific.set_xlabel("Critical-band rate z [Bark_HMS]") 128 - ax_specific.set_ylabel("Specific loudness N' [sone_HMS/Bark_HMS]") 259 + ax_specific.set_xlabel(_t("cbr_bark_hms", language)) 260 + ax_specific.set_ylabel(_t("spec_loudness_hms", language)) 129 261 ax_specific.set_xlim(0.0, bark[-1]) 130 262 ax_specific.set_ylim(bottom=0.0) 131 - ax_specific.set_title(f"ECMA-418-2 loudness N = {result.loudness:.2f} sone_HMS") 263 + ax_specific.set_title(_t("ecma_loudness_title", language).format( 264 + n=format_number(result.loudness, language, decimals=2), 265 + )) 132 266 ax_specific.grid(True, alpha=0.3) 133 267 134 268 if not two_panel: 269 + localize_axes(ax_specific, language) 135 270 return ax_specific 136 271 137 272 ax_time = cast("Axes", axes[1]) 138 273 time = np.asarray(result.time, dtype=np.float64) 139 274 lvt = np.asarray(result.loudness_vs_time, dtype=np.float64) 140 275 ax_time.plot(time, lvt, color=_C_TERTIARY, label="N(l)") 141 - ax_time.set_xlabel("Time [s]") 142 - ax_time.set_ylabel("Loudness N [sone_HMS]") 276 + ax_time.set_xlabel(_t("time_s", language)) 277 + ax_time.set_ylabel(_t("loudness_n_sone_hms", language)) 143 278 ax_time.set_ylim(bottom=0.0) 144 279 ax_time.grid(True, alpha=0.3) 145 280 ax_time.legend(loc="best", fontsize="small") 281 + localize_axes(ax_specific, language) 282 + localize_axes(ax_time, language) 146 283 return axes 147 284 285 + 148 286 def plot_moore_glasberg_loudness( 149 - result: MooreGlasbergLoudness, ax: Axes | None = None, **kwargs: Any 287 + result: MooreGlasbergLoudness, ax: Axes | None = None, *, language: str = "en", 288 + **kwargs: Any, 150 289 ) -> Axes: 151 290 """Specific loudness N'(i) over the ERB-number (Cam) scale (ISO 532-2). 152 291 153 292 :param result: A 154 293 :class:`~phonometry.loudness_moore_glasberg.MooreGlasbergLoudness`. 155 294 :param ax: Existing axes to draw on, or ``None`` to create a figure. 295 + :param language: Label language, ``"en"`` (default) or ``"es"``. 156 296 :param kwargs: Forwarded to the specific-loudness line ``plot`` call. 157 297 :return: The axes. 158 298 """ 299 + from .._i18n import format_number, localize_axes 300 + 159 301 specific = np.asarray(result.specific, dtype=np.float64) 160 302 erb_number = np.asarray(result.erb_number, dtype=np.float64) 161 303 ax = ax if ax is not None else _new_axes() ··· 163 305 kwargs.setdefault("color", _C_PRIMARY) 164 306 ax.plot(erb_number, specific, **kwargs) 165 307 ax.fill_between(erb_number, specific, color=kwargs["color"], alpha=0.25) 166 - ax.set_xlabel("ERB number [Cam]") 167 - ax.set_ylabel("Specific loudness N' [sone/Cam]") 308 + ax.set_xlabel(_t("erb_cam", language)) 309 + ax.set_ylabel(_t("spec_loudness_cam", language)) 168 310 ax.set_xlim(erb_number[0], erb_number[-1]) 169 311 ax.set_ylim(bottom=0.0) 170 - ax.set_title( 171 - f"ISO 532-2 loudness N = {result.loudness:.2f} sone " 172 - f"({result.loudness_level:.1f} phon)" 173 - ) 312 + ax.set_title(_t("mg_loudness_title", language).format( 313 + n=format_number(result.loudness, language, decimals=2), 314 + ln=format_number(result.loudness_level, language, decimals=1), 315 + )) 174 316 ax.grid(True, alpha=0.3) 317 + localize_axes(ax, language) 175 318 return ax 319 + 176 320 177 321 def plot_moore_glasberg_time_loudness( 178 - result: MooreGlasbergTimeVaryingLoudness, ax: Axes | None = None, **kwargs: Any 322 + result: MooreGlasbergTimeVaryingLoudness, ax: Axes | None = None, *, 323 + language: str = "en", **kwargs: Any, 179 324 ) -> Axes: 180 325 """Short-term and long-term loudness against time (ISO 532-3). 181 326 182 327 :param result: A 183 328 :class:`~phonometry.loudness_moore_glasberg_time.MooreGlasbergTimeVaryingLoudness`. 184 329 :param ax: Existing axes to draw on, or ``None`` to create a figure. 330 + :param language: Label language, ``"en"`` (default) or ``"es"``. 185 331 :param kwargs: Forwarded to the long-term-loudness line ``plot`` call. 186 332 :return: The axes. 187 333 """ 334 + from .._i18n import format_number, localize_axes 335 + 188 336 time = np.asarray(result.time, dtype=np.float64) 189 337 stl = np.asarray(result.short_term_loudness, dtype=np.float64) 190 338 ltl = np.asarray(result.long_term_loudness, dtype=np.float64) 191 339 ax = ax if ax is not None else _new_axes() 192 340 193 - ax.plot(time, stl, color=_C_PRIMARY_LIGHT, lw=1.0, label="Short-term loudness") 341 + ax.plot(time, stl, color=_C_PRIMARY_LIGHT, lw=1.0, 342 + label=_t("short_term_loudness", language)) 194 343 kwargs.setdefault("color", _C_PRIMARY) 195 344 kwargs.setdefault("lw", 1.8) 196 - ax.plot(time, ltl, label="Long-term loudness", **kwargs) 345 + ax.plot(time, ltl, label=_t("long_term_loudness", language), **kwargs) 197 346 ax.axhline(result.n_max, color=_C_REFERENCE, ls="--", lw=1.0, alpha=0.7) 198 - ax.set_xlabel("Time [s]") 199 - ax.set_ylabel("Loudness [sone]") 347 + ax.set_xlabel(_t("time_s", language)) 348 + ax.set_ylabel(_t("loudness_sone", language)) 200 349 if time.size: 201 350 ax.set_xlim(time[0], time[-1]) 202 351 ax.set_ylim(bottom=0.0) 203 - ax.set_title( 204 - f"ISO 532-3 peak long-term loudness N = {result.n_max:.2f} sone " 205 - f"({result.loudness_level_max:.1f} phon)" 206 - ) 352 + ax.set_title(_t("mg_time_title", language).format( 353 + n=format_number(result.n_max, language, decimals=2), 354 + ln=format_number(result.loudness_level_max, language, decimals=1), 355 + )) 207 356 ax.legend(loc="best", fontsize="small") 208 357 ax.grid(True, alpha=0.3) 358 + localize_axes(ax, language) 209 359 return ax 210 360 361 + 211 362 def plot_ecma_tonality( 212 - result: EcmaTonality, ax: Axes | None = None, **kwargs: Any 363 + result: EcmaTonality, ax: Axes | None = None, *, language: str = "en", 364 + **kwargs: Any, 213 365 ) -> Axes | np.ndarray: 214 366 """Average specific tonality T'(z) and time-dependent tonality T(l). 215 367 ··· 220 372 221 373 :param result: An :class:`~phonometry.tonality_ecma.EcmaTonality`. 222 374 :param ax: Existing axes to draw on, or ``None`` to create a figure. 375 + :param language: Label language, ``"en"`` (default) or ``"es"``. 223 376 :param kwargs: Forwarded to the specific-tonality line ``plot`` call. 224 377 :return: The axes, or an array of two axes. 225 378 """ 379 + from .._i18n import format_number, localize_axes 380 + 226 381 specific = np.asarray(result.specific_tonality, dtype=np.float64) 227 382 bark = np.asarray(result.bark, dtype=np.float64) 228 383 two_panel = ax is None ··· 239 394 kwargs.setdefault("color", "#d62728") 240 395 ax_specific.plot(bark, specific, **kwargs) 241 396 ax_specific.fill_between(bark, specific, color=kwargs["color"], alpha=0.25) 242 - ax_specific.set_xlabel("Critical-band rate z [Bark_HMS]") 243 - ax_specific.set_ylabel("Specific tonality T' [tu_HMS]") 397 + ax_specific.set_xlabel(_t("cbr_bark_hms", language)) 398 + ax_specific.set_ylabel(_t("spec_tonality", language)) 244 399 ax_specific.set_xlim(0.0, bark[-1]) 245 400 ax_specific.set_ylim(bottom=0.0) 246 - ax_specific.set_title(f"ECMA-418-2 tonality T = {result.tonality:.2f} tu_HMS") 401 + ax_specific.set_title(_t("ecma_tonality_title", language).format( 402 + t=format_number(result.tonality, language, decimals=2), 403 + )) 247 404 ax_specific.grid(True, alpha=0.3) 248 405 249 406 if not two_panel: 407 + localize_axes(ax_specific, language) 250 408 return ax_specific 251 409 252 410 ax_time = cast("Axes", axes[1]) 253 411 time = np.asarray(result.time, dtype=np.float64) 254 412 tvt = np.asarray(result.tonality_vs_time, dtype=np.float64) 255 413 ax_time.plot(time, tvt, color=_C_QUATERNARY, label="T(l)") 256 - ax_time.set_xlabel("Time [s]") 257 - ax_time.set_ylabel("Tonality T [tu_HMS]") 414 + ax_time.set_xlabel(_t("time_s", language)) 415 + ax_time.set_ylabel(_t("tonality_t", language)) 258 416 ax_time.set_ylim(bottom=0.0) 259 417 ax_time.grid(True, alpha=0.3) 260 418 ax_time.legend(loc="best", fontsize="small") 419 + localize_axes(ax_specific, language) 420 + localize_axes(ax_time, language) 261 421 return axes 262 422 423 + 263 424 def _plot_hms_time_and_heatmap( 264 425 time: np.ndarray, 265 426 vs_time: np.ndarray, ··· 271 432 title: str, 272 433 heat_label: str, 273 434 kwargs: dict[str, Any], 435 + language: str = "en", 274 436 ) -> Axes | np.ndarray: 275 437 """Shared renderer for the HMS time-trace + specific-value heatmaps. 276 438 ··· 279 441 critical-band-rate scale) is drawn and an array of two axes is returned; 280 442 otherwise only the time trace is drawn on ``ax`` and it is returned. 281 443 """ 444 + from .._i18n import localize_axes 445 + 282 446 two_panel = ax is None 283 447 if two_panel: 284 448 axes = _new_axes_column(2, figsize=(7.0, 6.0)) ··· 290 454 kwargs.setdefault("color", color) 291 455 (line,) = ax_time.plot(time, vs_time, **kwargs) 292 456 ax_time.fill_between(time, vs_time, color=line.get_color(), alpha=0.25) 293 - ax_time.set_xlabel("Time [s]") 457 + ax_time.set_xlabel(_t("time_s", language)) 294 458 ax_time.set_ylabel(ylabel) 295 459 ax_time.set_ylim(bottom=0.0) 296 460 ax_time.set_title(title) 297 461 ax_time.grid(True, alpha=0.3) 298 462 299 463 if not two_panel: 464 + localize_axes(ax_time, language) 300 465 return ax_time 301 466 302 467 ax_heat = cast("Axes", axes[1]) ··· 305 470 time, bark, spec_vs_time.T, cmap="magma", shading="auto" 306 471 ) 307 472 ax_heat.figure.colorbar(mesh, ax=ax_heat, label=heat_label) 308 - ax_heat.set_xlabel("Time [s]") 309 - ax_heat.set_ylabel("Critical-band rate z [Bark_HMS]") 473 + ax_heat.set_xlabel(_t("time_s", language)) 474 + ax_heat.set_ylabel(_t("cbr_bark_hms", language)) 475 + localize_axes(ax_time, language) 476 + localize_axes(ax_heat, language) 310 477 return axes 478 + 311 479 312 480 def plot_ecma_roughness( 313 - result: EcmaRoughness, ax: Axes | None = None, **kwargs: Any 481 + result: EcmaRoughness, ax: Axes | None = None, *, language: str = "en", 482 + **kwargs: Any, 314 483 ) -> Axes | np.ndarray: 315 484 """Time-dependent roughness R(l50) and a specific-roughness heatmap. 316 485 ··· 321 490 322 491 :param result: An :class:`~phonometry.roughness_ecma.EcmaRoughness`. 323 492 :param ax: Existing axes to draw on, or ``None`` to create a figure. 493 + :param language: Label language, ``"en"`` (default) or ``"es"``. 324 494 :param kwargs: Forwarded to the roughness-vs-time line ``plot`` call. 325 495 :return: The axes, or an array of two axes. 326 496 """ 497 + from .._i18n import format_number 498 + 327 499 # Roughness's per-metric identity color is brown across the documentation 328 500 # figures (tonality is red); kept literal on purpose, see the module 329 501 # color-constant note. ··· 334 506 np.asarray(result.bark, dtype=np.float64), 335 507 ax, 336 508 "#8c564b", 337 - "Roughness R [asper]", 338 - f"ECMA-418-2 roughness R = {result.roughness:.2f} asper", 509 + _t("roughness_r", language), 510 + _t("ecma_roughness_title", language).format( 511 + r=format_number(result.roughness, language, decimals=2), 512 + ), 339 513 "R' [asper/Bark_HMS]", 340 514 kwargs, 515 + language, 341 516 ) 342 517 518 + 343 519 def plot_ecma_fluctuation_strength( 344 - result: EcmaFluctuationStrength, ax: Axes | None = None, **kwargs: Any 520 + result: EcmaFluctuationStrength, ax: Axes | None = None, *, language: str = "en", 521 + **kwargs: Any, 345 522 ) -> Axes | np.ndarray: 346 523 """Time-dependent fluctuation strength F(l50) and a specific heatmap. 347 524 ··· 354 531 :param result: An :class:`~phonometry.fluctuation_strength_ecma. 355 532 EcmaFluctuationStrength`. 356 533 :param ax: Existing axes to draw on, or ``None`` to create a figure. 534 + :param language: Label language, ``"en"`` (default) or ``"es"``. 357 535 :param kwargs: Forwarded to the time-trace line ``plot`` call. 358 536 :return: The axes, or an array of two axes. 359 537 """ 538 + from .._i18n import format_number 539 + 360 540 # Fluctuation strength's per-metric identity color is teal across the 361 541 # documentation figures (roughness is brown, tonality red); kept literal 362 542 # on purpose, see the module color-constant note. ··· 369 549 np.asarray(result.bark, dtype=np.float64), 370 550 ax, 371 551 "#17becf", 372 - "Fluctuation strength F [vacil_HMS]", 373 - ( 374 - "ECMA-418-2 fluctuation strength " 375 - f"F = {result.fluctuation_strength:.2f} vacil_HMS" 552 + _t("fluct_f_hms", language), 553 + _t("ecma_fluct_title", language).format( 554 + f=format_number(result.fluctuation_strength, language, decimals=2), 376 555 ), 377 556 "F' [vacil_HMS/Bark_HMS]", 378 557 kwargs, 558 + language, 379 559 ) 560 + 380 561 381 562 def plot_fluctuation_strength( 382 - result: "FluctuationStrengthResult", ax: Axes | None = None, **kwargs: Any 563 + result: "FluctuationStrengthResult", ax: Axes | None = None, *, 564 + language: str = "en", **kwargs: Any, 383 565 ) -> Axes: 384 566 """Specific fluctuation strength ``f(z)`` against critical-band rate. 385 567 ··· 389 571 :param result: A 390 572 :class:`~phonometry.fluctuation_strength.FluctuationStrengthResult`. 391 573 :param ax: Existing axes, or ``None`` to create a figure. 574 + :param language: Label language, ``"en"`` (default) or ``"es"``. 392 575 :param kwargs: Forwarded to the ``plot`` call. 393 576 :return: The axes. 394 577 """ 578 + from .._i18n import format_number, localize_axes 579 + 395 580 ax = ax if ax is not None else _new_axes() 396 581 z = np.asarray(result.bark_axis, dtype=np.float64) 397 582 spec = np.asarray(result.specific, dtype=np.float64) 398 583 kwargs.setdefault("color", _C_PRIMARY) 399 584 ax.plot(z, spec, **kwargs) 400 585 ax.fill_between(z, spec, color=kwargs["color"], alpha=0.25) 401 - ax.set_xlabel("Critical-band rate z [Bark]") 402 - ax.set_ylabel(r"Specific fluctuation strength $f'(z)$ [vacil/Bark]") 403 - ax.set_title(f"Fluctuation strength F = {result.fluctuation_strength:.2f} vacil") 586 + ax.set_xlabel(_t("cbr_bark", language)) 587 + ax.set_ylabel(_t("spec_fluct", language)) 588 + ax.set_title(_t("fluct_title", language).format( 589 + f=format_number(result.fluctuation_strength, language, decimals=2), 590 + )) 404 591 ax.set_ylim(bottom=0.0) 405 592 ax.grid(True, alpha=0.3) 406 593 ax.set_axisbelow(True) 594 + localize_axes(ax, language) 407 595 return ax 596 + 408 597 409 598 def plot_psychoacoustic_annoyance( 410 - result: "PsychoacousticAnnoyanceResult", ax: Axes | None = None, **kwargs: Any 599 + result: "PsychoacousticAnnoyanceResult", ax: Axes | None = None, *, 600 + language: str = "en", **kwargs: Any, 411 601 ) -> Axes: 412 602 """Psychoacoustic annoyance with its ``wS`` and ``wFR`` term contributions. 413 603 ··· 417 607 :param result: A :class:`~phonometry.psychoacoustic_annoyance. 418 608 PsychoacousticAnnoyanceResult`. 419 609 :param ax: Existing axes, or ``None`` to create a figure. 610 + :param language: Label language, ``"en"`` (default) or ``"es"``. 420 611 :param kwargs: Forwarded to the bar ``bar`` call. 421 612 :return: The axes. 422 613 """ 614 + from .._i18n import format_number, localize_axes 615 + 423 616 ax = ax if ax is not None else _new_axes() 424 617 labels = ["PA", r"$w_S$", r"$w_{FR}$"] 425 618 values = [result.annoyance, result.w_s, result.w_fr] ··· 431 624 ax.bar(positions, values, **kwargs) 432 625 ax.set_xticks(positions) 433 626 ax.set_xticklabels(labels) 434 - ax.set_ylabel("Value") 435 - ax.set_title( 436 - f"Psychoacoustic annoyance PA = {result.annoyance:.1f} " 437 - f"(N5 = {result.n5:.1f} sone)" 438 - ) 627 + ax.set_ylabel(_t("value", language)) 628 + ax.set_title(_t("annoyance_title", language).format( 629 + pa=format_number(result.annoyance, language, decimals=1), 630 + n5=format_number(result.n5, language, decimals=1), 631 + )) 439 632 ax.grid(True, axis="y", alpha=0.3) 440 633 ax.set_axisbelow(True) 634 + # localize_axes leaves the categorical x-axis (a FuncFormatter) alone. 635 + localize_axes(ax, language) 441 636 return ax 637 + 442 638 443 639 def plot_tone_audibility( 444 - result: "ToneAudibilityResult", ax: Axes | None = None, **kwargs: Any 640 + result: "ToneAudibilityResult", ax: Axes | None = None, *, language: str = "en", 641 + **kwargs: Any, 445 642 ) -> Axes: 446 643 """Per-tone audibility ``ΔL`` against tone frequency (ISO/PAS 20065). 447 644 ··· 450 647 451 648 :param result: A :class:`~phonometry.tone_audibility.ToneAudibilityResult`. 452 649 :param ax: Existing axes, or ``None`` to create a figure. 650 + :param language: Label language, ``"en"`` (default) or ``"es"``. 453 651 :param kwargs: Forwarded to the bar ``bar`` call. 454 652 :return: The axes. 455 653 """ 654 + from .._i18n import decimal_comma, format_number, localize_axes 655 + 456 656 ax = ax if ax is not None else _new_axes() 457 657 freqs = np.asarray(result.tone_frequencies, dtype=np.float64) 458 658 delta = np.asarray(result.audibilities, dtype=np.float64) ··· 463 663 464 664 kwargs.setdefault("width", 0.7) 465 665 bars = ax.bar(positions, delta, color=colors, edgecolor=_C_EDGE, **kwargs) 466 - bars[decisive].set_label( 467 - rf"decisive $\Delta L$ = {result.decisive_audibility:.1f} dB " 468 - rf"@ {result.decisive_frequency:g} Hz" 469 - ) 470 - ax.axhline(0.0, color=_C_MUTED, ls="--", lw=1.0, label=r"threshold $\Delta L=0$ dB") 666 + bars[decisive].set_label(_t("decisive_label", language).format( 667 + da=format_number(result.decisive_audibility, language, decimals=1), 668 + df=decimal_comma(f"{result.decisive_frequency:g}", language), 669 + )) 670 + ax.axhline(0.0, color=_C_MUTED, ls="--", lw=1.0, 671 + label=_t("audibility_threshold", language)) 471 672 ax.set_xticks(positions) 472 - ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") 473 - ax.set_xlabel("Tone frequency [Hz]") 474 - ax.set_ylabel(r"Audibility $\Delta L$ [dB]") 475 - ax.set_title("ISO/PAS 20065 tonal audibility") 673 + ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], 674 + rotation=45, ha="right") 675 + ax.set_xlabel(_t("tone_frequency", language)) 676 + ax.set_ylabel(_t("audibility_dl", language)) 677 + ax.set_title(_t("tone_audibility_title", language)) 476 678 ax.legend(loc="best", fontsize="small") 477 679 ax.grid(True, axis="y", alpha=0.3) 478 680 ax.set_axisbelow(True) 681 + # localize_axes leaves the categorical x-axis (a FuncFormatter) alone, so the 682 + # comma-localized tick labels set above survive. 683 + localize_axes(ax, language) 479 684 return ax
+184 -40
src/phonometry/_plot/vibration.py
··· 29 29 from ..vibration.multiple_shock_vibration import MultipleShockResult 30 30 from ..vibration.radiation_efficiency import RadiationEfficiencyResult 31 31 32 + #: EN/ES text for every fixed label, title and legend of this module. The 33 + #: English entries are byte-for-byte the historical strings; ``_t`` returns 34 + #: them unchanged for ``language="en"``. 35 + _STRINGS: dict[str, dict[str, str]] = { 36 + "freq_hz": {"en": "Frequency [Hz]", "es": "Frecuencia [Hz]"}, 37 + "weighting_factor": { 38 + "en": "Weighting factor [dB]", 39 + "es": "Factor de ponderación [dB]", 40 + }, 41 + "unweighted_ai": {"en": r"Unweighted $a_i$", "es": r"Sin ponderar $a_i$"}, 42 + "rms_accel": { 43 + "en": r"r.m.s. acceleration [m/s$^2$]", 44 + "es": r"Aceleración eficaz [m/s$^2$]", 45 + }, 46 + "vib_exposure_a8": { 47 + "en": r"Vibration exposure A(8) [m/s$^2$]", 48 + "es": r"Exposición a vibración A(8) [m/s$^2$]", 49 + }, 50 + "mobility_driving_point": { 51 + "en": "driving-point mobility", 52 + "es": "movilidad en punto de excitación", 53 + }, 54 + "mobility_transfer": { 55 + "en": "transfer mobility", 56 + "es": "movilidad de transferencia", 57 + }, 58 + "mobility_y": { 59 + "en": r"Mobility $|Y|$ [m/(N·s)]", 60 + "es": r"Movilidad $|Y|$ [m/(N·s)]", 61 + }, 62 + "transfer_stiffness_level": { 63 + "en": r"Transfer stiffness level $L_k$ [dB re 1 N/m]", 64 + "es": r"Nivel de rigidez de transferencia $L_k$ [dB re 1 N/m]", 65 + }, 66 + "radiation_efficiency": { 67 + "en": r"Radiation efficiency $\sigma$", 68 + "es": r"Eficiencia de radiación $\sigma$", 69 + }, 70 + "stress_variable": { 71 + "en": r"Stress variable $R$", 72 + "es": r"Variable de tensión $R$", 73 + }, 74 + "lumbar_injury_prob": { 75 + "en": "Probability of lumbar injury [%]", 76 + "es": "Probabilidad de lesión lumbar [%]", 77 + }, 78 + "mobility_title": { 79 + "en": "ISO 7626-1 mechanical mobility", 80 + "es": "ISO 7626-1 movilidad mecánica", 81 + }, 82 + "transfer_stiffness_title": { 83 + "en": "ISO 10846 dynamic transfer stiffness", 84 + "es": "ISO 10846 rigidez dinámica de transferencia", 85 + }, 86 + "radiation_title": { 87 + "en": "Plate radiation efficiency (Leppington / Maidanik)", 88 + "es": "Eficiencia de radiación de placa (Leppington / Maidanik)", 89 + }, 90 + # Templates (``.format`` fields hold already-localised numbers / data). 91 + "vib_weighting_title": { 92 + "en": "Frequency weighting {name} (ISO 8041-1)", 93 + "es": "Ponderación en frecuencia {name} (ISO 8041-1)", 94 + }, 95 + "weighted_wia": { 96 + "en": r"Weighted $W_i a_i$ ({name})", 97 + "es": r"Ponderada $W_i a_i$ ({name})", 98 + }, 99 + "weighted_spectrum_title": { 100 + "en": "{designation} weighted acceleration spectrum ($a_w$ = {aw} m/s$^2$)", 101 + "es": "{designation} espectro de aceleración ponderada ($a_w$ = {aw} m/s$^2$)", 102 + }, 103 + "daily_exposure_title": { 104 + "en": ("Directive 2002/44/EC daily {kind} exposure " 105 + "(A(8) = {a8} m/s$^2$, {zone})"), 106 + "es": ("Directiva 2002/44/CE exposición diaria {kind} " 107 + "(A(8) = {a8} m/s$^2$, {zone})"), 108 + }, 109 + "mobility_peak": {"en": "peak at {v} Hz", "es": "máximo en {v} Hz"}, 110 + "injury_title": { 111 + "en": "ISO 2631-5 injury probability — {sex}", 112 + "es": "ISO 2631-5 probabilidad de lesión — {sex}", 113 + }, 114 + "injury_marker": { 115 + "en": r"$R$ = {r}, $\Pi$ = {p} %", 116 + "es": r"$R$ = {r}, $\Pi$ = {p} %", 117 + }, 118 + } 119 + 120 + 121 + def _t(key: str, language: str) -> str: 122 + """Look up the localised text for ``key`` (falls back to English).""" 123 + entry = _STRINGS[key] 124 + return entry.get(language, entry["en"]) 125 + 126 + 32 127 def plot_vibration_weighting( 33 - result: "WeightingResponse", ax: Axes | None = None, **kwargs: Any 128 + result: "WeightingResponse", ax: Axes | None = None, *, language: str = "en", 129 + **kwargs: Any, 34 130 ) -> Axes: 35 131 """Frequency-weighting factor (dB) versus frequency (ISO 8041-1). 36 132 ··· 38 134 :class:`~phonometry.human_vibration.WeightingResponse` exposing 39 135 ``name``, ``frequencies`` and ``magnitude_db``. 40 136 :param ax: Existing axes, or ``None`` to create a figure. 137 + :param language: Label language, ``"en"`` (default) or ``"es"``. 41 138 :param kwargs: Forwarded to the weighting curve ``plot`` call. 42 139 :return: The axes. 43 140 """ 141 + from .._i18n import localize_axes 142 + 44 143 ax = ax if ax is not None else _new_axes() 45 144 freqs = np.asarray(result.frequencies, dtype=np.float64) 46 145 mag_db = np.asarray(result.magnitude_db, dtype=np.float64) 47 146 kwargs.setdefault("color", _C_PRIMARY) 48 147 ax.semilogx(freqs, mag_db, **kwargs) 49 - ax.set_xlabel("Frequency [Hz]") 50 - ax.set_ylabel("Weighting factor [dB]") 51 - ax.set_title(f"Frequency weighting {result.name} (ISO 8041-1)") 148 + ax.set_xlabel(_t("freq_hz", language)) 149 + ax.set_ylabel(_t("weighting_factor", language)) 150 + ax.set_title(_t("vib_weighting_title", language).format(name=result.name)) 52 151 ax.grid(True, which="both", alpha=0.3) 53 152 format_frequency_axis(ax, float(freqs.min()), float(freqs.max())) 153 + localize_axes(ax, language) 54 154 return ax 155 + 55 156 56 157 def plot_weighted_spectrum( 57 - result: "WeightedSpectrum", ax: Axes | None = None, **kwargs: Any 158 + result: "WeightedSpectrum", ax: Axes | None = None, *, language: str = "en", 159 + **kwargs: Any, 58 160 ) -> Axes: 59 161 """Unweighted vs weighted one-third-octave acceleration spectrum. 60 162 ··· 66 168 ``frequencies``, ``band_accelerations``, ``weighted``, ``overall`` and 67 169 ``weighting_name``. 68 170 :param ax: Existing axes, or ``None`` to create a figure. 171 + :param language: Label language, ``"en"`` (default) or ``"es"``. 69 172 :param kwargs: Forwarded to the weighted (primary) bars. 70 173 :return: The axes. 71 174 """ 175 + from .._i18n import format_number, localize_axes 176 + 72 177 ax = ax if ax is not None else _new_axes() 73 178 freqs = np.asarray(result.frequencies, dtype=np.float64) 74 179 raw = np.asarray(result.band_accelerations, dtype=np.float64) 75 180 weighted = np.asarray(result.weighted, dtype=np.float64) 76 181 positions = _band_axis(ax, freqs) 182 + ax.set_xlabel(_t("freq_hz", language)) 77 183 width = 0.4 78 184 # The weighted bars are the primary artist; forward user kwargs there. 79 185 kwargs.setdefault("color", _C_PRIMARY) 80 186 ax.bar( 81 - positions - width / 2, raw, width, color=_C_MUTED, label="Unweighted $a_i$" 187 + positions - width / 2, raw, width, color=_C_MUTED, 188 + label=_t("unweighted_ai", language), 82 189 ) 83 190 ax.bar( 84 191 positions + width / 2, 85 192 weighted, 86 193 width, 87 - label=f"Weighted $W_i a_i$ ({result.weighting_name})", 194 + label=_t("weighted_wia", language).format(name=result.weighting_name), 88 195 **kwargs, 89 196 ) 90 - ax.set_ylabel(r"r.m.s. acceleration [m/s$^2$]") 197 + ax.set_ylabel(_t("rms_accel", language)) 91 198 # Wh is the hand-arm weighting of ISO 5349-1; the others (Wk, Wd, Wm...) 92 199 # are the whole-body weightings of ISO 2631. 93 200 designation = "ISO 5349-1" if str(result.weighting_name) == "Wh" else "ISO 2631" 94 - ax.set_title( 95 - f"{designation} weighted acceleration spectrum " 96 - f"($a_w$ = {float(result.overall):.3f} " r"m/s$^2$)" 97 - ) 201 + ax.set_title(_t("weighted_spectrum_title", language).format( 202 + designation=designation, 203 + aw=format_number(float(result.overall), language, decimals=3), 204 + )) 98 205 ax.legend(loc="best", fontsize="small") 99 206 ax.grid(True, axis="y", alpha=0.3) 207 + # localize_axes leaves the categorical band axis (a FuncFormatter) alone. 208 + localize_axes(ax, language) 100 209 return ax 101 210 211 + 102 212 def plot_daily_exposure( 103 - result: "DailyVibrationExposure", ax: Axes | None = None, **kwargs: Any 213 + result: "DailyVibrationExposure", ax: Axes | None = None, *, language: str = "en", 214 + **kwargs: Any, 104 215 ) -> Axes: 105 216 """Partial daily exposures against the EAV / ELV (Directive 2002/44/EC). 106 217 ··· 111 222 :class:`~phonometry.human_vibration.DailyVibrationExposure` exposing 112 223 ``labels``, ``partials``, ``a8`` and ``assessment``. 113 224 :param ax: Existing axes, or ``None`` to create a figure. 225 + :param language: Label language, ``"en"`` (default) or ``"es"``. 114 226 :param kwargs: Forwarded to the exposure :meth:`~matplotlib.axes.Axes.bar`. 115 227 :return: The axes. 116 228 """ 229 + from .._i18n import decimal_comma, format_number, localize_axes 230 + 117 231 ax = ax if ax is not None else _new_axes() 118 232 partials = np.asarray(result.partials, dtype=np.float64) 119 233 labels = [*result.labels, "A(8)"] ··· 124 238 ax.bar(positions, values, **kwargs) 125 239 ax.set_xticks(positions) 126 240 ax.set_xticklabels(labels, rotation=45, ha="right") 127 - ax.set_ylabel(r"Vibration exposure A(8) [m/s$^2$]") 241 + ax.set_ylabel(_t("vib_exposure_a8", language)) 128 242 129 243 assessment = result.assessment 130 244 eav = float(assessment.action_value) 131 245 elv = float(assessment.limit_value) 132 - ax.axhline(eav, color=_C_SECONDARY, ls="--", label=f"EAV = {eav:g}") 133 - ax.axhline(elv, color=_C_REFERENCE, ls="--", label=f"ELV = {elv:g}") 246 + ax.axhline(eav, color=_C_SECONDARY, ls="--", 247 + label="EAV = " + decimal_comma(f"{eav:g}", language)) 248 + ax.axhline(elv, color=_C_REFERENCE, ls="--", 249 + label="ELV = " + decimal_comma(f"{elv:g}", language)) 134 250 top = max(elv, float(np.max(values))) * 1.15 135 251 ax.set_ylim(0.0, top) 136 252 kind = str(assessment.kind).upper() 137 - ax.set_title( 138 - f"Directive 2002/44/EC daily {kind} exposure " 139 - f"(A(8) = {float(result.a8):.2f} " rf"m/s$^2$, {assessment.zone})" 140 - ) 253 + ax.set_title(_t("daily_exposure_title", language).format( 254 + kind=kind, a8=format_number(float(result.a8), language, decimals=2), 255 + zone=assessment.zone, 256 + )) 141 257 ax.legend(loc="best", fontsize="small") 142 258 ax.grid(True, axis="y", alpha=0.3) 259 + # localize_axes leaves the categorical operation-label axis (a FuncFormatter) alone. 260 + localize_axes(ax, language) 143 261 return ax 144 262 263 + 145 264 def plot_mobility( 146 - result: "MobilityResult", ax: Axes | None = None, **kwargs: Any 265 + result: "MobilityResult", ax: Axes | None = None, *, language: str = "en", 266 + **kwargs: Any, 147 267 ) -> Axes: 148 268 """Mobility magnitude ``|Y(f)|`` on log-log axes (ISO 7626-1). 149 269 150 270 :param result: A :class:`~phonometry.mechanical_mobility.MobilityResult`. 151 271 :param ax: Existing axes, or ``None`` to create a figure. 272 + :param language: Label language, ``"en"`` (default) or ``"es"``. 152 273 :param kwargs: Forwarded to the magnitude ``plot``. 153 274 :return: The axes. 154 275 """ 276 + from .._i18n import format_number, localize_axes 277 + 155 278 ax = ax if ax is not None else _new_axes() 156 279 freq = np.asarray(result.frequencies, dtype=np.float64) 157 280 mag = np.asarray(result.magnitude, dtype=np.float64) 158 281 kwargs.setdefault("color", _C_PRIMARY) 159 - label = "driving-point mobility" if result.driving_point else "transfer mobility" 282 + label = (_t("mobility_driving_point", language) if result.driving_point 283 + else _t("mobility_transfer", language)) 160 284 ax.loglog(freq, mag, label=label, **kwargs) 161 285 # Mark the mobility peak (a resonance for a driving-point FRF). 162 286 peak = int(np.argmax(mag)) 163 287 ax.plot(freq[peak], mag[peak], "o", color=_C_REFERENCE, zorder=5, 164 - label=f"peak at {freq[peak]:.1f} Hz") 288 + label=_t("mobility_peak", language).format( 289 + v=format_number(freq[peak], language, decimals=1))) 165 290 format_frequency_axis(ax, float(freq.min()), float(freq.max())) 166 - ax.set_xlabel("Frequency [Hz]") 167 - ax.set_ylabel("Mobility $|Y|$ [m/(N·s)]") 168 - ax.set_title("ISO 7626-1 mechanical mobility") 291 + ax.set_xlabel(_t("freq_hz", language)) 292 + ax.set_ylabel(_t("mobility_y", language)) 293 + ax.set_title(_t("mobility_title", language)) 169 294 ax.legend(loc="best", fontsize="small") 170 295 ax.grid(True, which="both", alpha=0.3) 296 + localize_axes(ax, language) 171 297 return ax 298 + 172 299 173 300 def plot_transfer_stiffness( 174 - result: "TransferStiffnessResult", ax: Axes | None = None, **kwargs: Any 301 + result: "TransferStiffnessResult", ax: Axes | None = None, *, 302 + language: str = "en", **kwargs: Any, 175 303 ) -> Axes: 176 304 """Dynamic transfer stiffness level ``L_k(f)`` on a log-frequency axis. 177 305 178 306 :param result: A :class:`~phonometry.transfer_stiffness.TransferStiffnessResult`. 179 307 :param ax: Existing axes, or ``None`` to create a figure. 308 + :param language: Label language, ``"en"`` (default) or ``"es"``. 180 309 :param kwargs: Forwarded to the level ``plot``. 181 310 :return: The axes. 182 311 """ 312 + from .._i18n import localize_axes 313 + 183 314 ax = ax if ax is not None else _new_axes() 184 315 freq = np.asarray(result.frequencies, dtype=np.float64) 185 316 level = np.asarray(result.level, dtype=np.float64) 186 317 kwargs.setdefault("color", _C_PRIMARY) 187 318 ax.semilogx(freq, level, label=r"$L_k = 20\,\lg(|k_{2,1}|/k_0)$", **kwargs) 188 319 format_frequency_axis(ax, float(freq.min()), float(freq.max())) 189 - ax.set_xlabel("Frequency [Hz]") 190 - ax.set_ylabel(r"Transfer stiffness level $L_k$ [dB re 1 N/m]") 191 - ax.set_title("ISO 10846 dynamic transfer stiffness") 320 + ax.set_xlabel(_t("freq_hz", language)) 321 + ax.set_ylabel(_t("transfer_stiffness_level", language)) 322 + ax.set_title(_t("transfer_stiffness_title", language)) 192 323 ax.legend(loc="best", fontsize="small") 193 324 ax.grid(True, which="both", alpha=0.3) 325 + localize_axes(ax, language) 194 326 return ax 327 + 195 328 196 329 def plot_radiation_efficiency( 197 - result: "RadiationEfficiencyResult", ax: Axes | None = None, **kwargs: Any 330 + result: "RadiationEfficiencyResult", ax: Axes | None = None, *, 331 + language: str = "en", **kwargs: Any, 198 332 ) -> Axes: 199 333 """Radiation efficiency ``sigma(f)`` on log-log axes (Hopkins 2.9.4). 200 334 201 335 :param result: A 202 336 :class:`~phonometry.vibration.radiation_efficiency.RadiationEfficiencyResult`. 203 337 :param ax: Existing axes, or ``None`` to create a figure. 338 + :param language: Label language, ``"en"`` (default) or ``"es"``. 204 339 :param kwargs: Forwarded to the ``sigma`` curve ``plot``. 205 340 :return: The axes. 206 341 """ 342 + from .._i18n import localize_axes 343 + 207 344 ax = ax if ax is not None else _new_axes() 208 345 freq = np.asarray(result.frequencies, dtype=np.float64) 209 346 sigma = np.asarray(result.radiation_efficiency, dtype=np.float64) ··· 220 357 label=f"$f_c$ = {result.critical_frequency:.0f} Hz", 221 358 ) 222 359 format_frequency_axis(ax, float(freq.min()), float(freq.max())) 223 - ax.set_xlabel("Frequency [Hz]") 224 - ax.set_ylabel(r"Radiation efficiency $\sigma$") 225 - ax.set_title("Plate radiation efficiency (Leppington / Maidanik)") 360 + ax.set_xlabel(_t("freq_hz", language)) 361 + ax.set_ylabel(_t("radiation_efficiency", language)) 362 + ax.set_title(_t("radiation_title", language)) 226 363 ax.legend(loc="best", fontsize="small") 227 364 ax.grid(True, which="both", alpha=0.3) 365 + localize_axes(ax, language) 228 366 return ax 229 367 230 368 231 369 def plot_multiple_shock( 232 - result: "MultipleShockResult", ax: Axes | None = None, **kwargs: Any 370 + result: "MultipleShockResult", ax: Axes | None = None, *, language: str = "en", 371 + **kwargs: Any, 233 372 ) -> Axes: 234 373 """Injury-probability curve ``P(R)`` with this assessment's ``R`` marked. 235 374 236 375 :param result: A :class:`~phonometry.multiple_shock_vibration.MultipleShockResult`. 237 376 :param ax: Existing axes, or ``None`` to create a figure. 377 + :param language: Label language, ``"en"`` (default) or ``"es"``. 238 378 :param kwargs: Forwarded to the ``R`` marker ``scatter``. 239 379 :return: The axes. 240 380 """ 241 381 from ..vibration.multiple_shock_vibration import injury_probability 382 + from .._i18n import format_number, localize_axes 242 383 243 384 ax = ax if ax is not None else _new_axes() 244 385 r10, r50, r90 = result.risk_thresholds ··· 255 396 kwargs.setdefault("zorder", 4) 256 397 kwargs.setdefault("s", 90) 257 398 ax.scatter([result.risk], [100.0 * result.probability], 258 - label=f"$R$ = {result.risk:.2f}, $\\Pi$ = " 259 - f"{100.0 * result.probability:.0f} %", **kwargs) 260 - ax.set_xlabel("Stress variable $R$") 261 - ax.set_ylabel("Probability of lumbar injury [%]") 262 - ax.set_title(f"ISO 2631-5 injury probability — {result.sex}") 399 + label=_t("injury_marker", language).format( 400 + r=format_number(result.risk, language, decimals=2), 401 + p=format_number(100.0 * result.probability, language, decimals=0)), 402 + **kwargs) 403 + ax.set_xlabel(_t("stress_variable", language)) 404 + ax.set_ylabel(_t("lumbar_injury_prob", language)) 405 + ax.set_title(_t("injury_title", language).format(sex=result.sex)) 263 406 ax.set_xlim(left=0.0) 264 407 ax.set_ylim(0.0, 100.0) 265 408 ax.legend(loc="lower right", fontsize="small") 266 409 ax.grid(True, alpha=0.3) 410 + localize_axes(ax, language) 267 411 return ax
+6 -4
src/phonometry/hearing/noise_induced_hearing_loss.py
··· 94 94 spread_upper: np.ndarray 95 95 spread_lower: np.ndarray 96 96 97 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 97 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 98 98 """Plot the NIPTS spectrum with the fractile band over frequency. 99 99 100 100 Requires matplotlib (``pip install phonometry[plot]``); returns the 101 101 :class:`~matplotlib.axes.Axes`. 102 102 """ 103 + from .._i18n import check_language 103 104 from .._plot.hearing import plot_nipts 104 105 105 - return plot_nipts(self, ax=ax, **kwargs) 106 + return plot_nipts(self, ax=ax, language=check_language(language), **kwargs) 106 107 107 108 108 109 @dataclass(frozen=True) ··· 132 133 nipts: np.ndarray 133 134 threshold: np.ndarray 134 135 135 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 136 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 136 137 """Plot the age, noise and combined threshold components over frequency. 137 138 138 139 Requires matplotlib (``pip install phonometry[plot]``); returns the 139 140 :class:`~matplotlib.axes.Axes`. 140 141 """ 142 + from .._i18n import check_language 141 143 from .._plot.hearing import plot_htlan 142 144 143 - return plot_htlan(self, ax=ax, **kwargs) 145 + return plot_htlan(self, ax=ax, language=check_language(language), **kwargs) 144 146 145 147 146 148 def _select(values: np.ndarray, frequencies: ArrayLike | None) -> np.ndarray:
+3 -2
src/phonometry/hearing/objective_intelligibility.py
··· 107 107 band_frequencies: NDArray[np.float64] 108 108 sample_rate: int 109 109 110 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 110 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 111 111 """Plot the intermediate intelligibility that averages to the index. 112 112 113 113 For STOI this is the mean correlation per one-third-octave band; for ··· 115 115 analysis segment. Requires matplotlib (``pip install phonometry[plot]``); 116 116 returns the :class:`~matplotlib.axes.Axes`. 117 117 """ 118 + from .._i18n import check_language 118 119 from .._plot.hearing import plot_stoi 119 120 120 - return plot_stoi(self, ax=ax, **kwargs) 121 + return plot_stoi(self, ax=ax, language=check_language(language), **kwargs) 121 122 122 123 123 124 def _third_octave_matrix() -> tuple[NDArray[np.float64], NDArray[np.float64]]:
+3 -2
src/phonometry/hearing/occupational_exposure.py
··· 281 281 """Upper limit ``LEX,8h + U`` of the one-sided 95 % interval, dB.""" 282 282 return self.lex_8h + self.expanded_uncertainty 283 283 284 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 284 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 285 285 """Plot the per-task contributions with the ``LEX,8h`` line. 286 286 287 287 Only task-based results carry per-task contributions (the job and ··· 289 289 (``pip install phonometry[plot]``); returns the 290 290 :class:`~matplotlib.axes.Axes`. 291 291 """ 292 + from .._i18n import check_language 292 293 from .._plot.hearing import plot_occupational_exposure 293 294 294 - return plot_occupational_exposure(self, ax=ax, **kwargs) 295 + return plot_occupational_exposure(self, ax=ax, language=check_language(language), **kwargs) 295 296 296 297 297 298 # --------------------------------------------------------------------------- #
+3 -2
src/phonometry/hearing/sii.py
··· 112 112 disturbance: np.ndarray 113 113 masking: np.ndarray 114 114 115 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 115 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 116 116 """Plot the per-band audibility weighted by importance, with the SII. 117 117 118 118 Requires matplotlib (``pip install phonometry[plot]``); returns the 119 119 :class:`~matplotlib.axes.Axes`. 120 120 """ 121 + from .._i18n import check_language 121 122 from .._plot.hearing import plot_sii 122 123 123 - return plot_sii(self, ax=ax, **kwargs) 124 + return plot_sii(self, ax=ax, language=check_language(language), **kwargs) 124 125 125 126 126 127 def standard_speech_spectrum(vocal_effort: str = "normal") -> np.ndarray:
+3 -2
src/phonometry/hearing/sti.py
··· 107 107 band_levels: np.ndarray | None 108 108 rating: str 109 109 110 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 110 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 111 111 """Plot the per-band MTI bars with the STI and rating letter. 112 112 113 113 Requires matplotlib (``pip install phonometry[plot]``); returns the 114 114 :class:`~matplotlib.axes.Axes`. 115 115 """ 116 + from .._i18n import check_language 116 117 from .._plot.hearing import plot_sti 117 118 118 - return plot_sti(self, ax=ax, **kwargs) 119 + return plot_sti(self, ax=ax, language=check_language(language), **kwargs) 119 120 120 121 121 122 def _rating(sti: float) -> str:
+3 -2
src/phonometry/hearing/threshold.py
··· 165 165 spread_lower: np.ndarray 166 166 threshold: np.ndarray 167 167 168 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 168 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 169 169 """Plot the median threshold with the fractile band over frequency. 170 170 171 171 Requires matplotlib (``pip install phonometry[plot]``); returns the 172 172 :class:`~matplotlib.axes.Axes`. 173 173 """ 174 + from .._i18n import check_language 174 175 from .._plot.hearing import plot_age_threshold 175 176 176 - return plot_age_threshold(self, ax=ax, **kwargs) 177 + return plot_age_threshold(self, ax=ax, language=check_language(language), **kwargs) 177 178 178 179 179 180 def _select(values: np.ndarray, frequencies: ArrayLike | None) -> np.ndarray:
+3 -2
src/phonometry/psychoacoustics/fluctuation_strength.py
··· 199 199 bark_axis: "NDArray[np.float64]" 200 200 time_dependent: "NDArray[np.float64]" 201 201 202 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 202 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 203 203 """Plot the specific fluctuation strength against critical-band rate.""" 204 + from .._i18n import check_language 204 205 from .._plot.psychoacoustics import plot_fluctuation_strength 205 206 206 - return plot_fluctuation_strength(self, ax=ax, **kwargs) 207 + return plot_fluctuation_strength(self, ax=ax, language=check_language(language), **kwargs) 207 208 208 209 209 210 def _bandpass_envelope_filter(fs: float) -> Any:
+3 -2
src/phonometry/psychoacoustics/fluctuation_strength_ecma.py
··· 209 209 specific_fluctuation_strength_vs_time: np.ndarray 210 210 field: str 211 211 212 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes | np.ndarray: 212 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes | np.ndarray: 213 213 """Plot the fluctuation-strength result (see :mod:`._plotting`). 214 214 215 215 Draws the time-dependent fluctuation strength F(l50) and a 216 216 specific-fluctuation-strength heatmap. Requires matplotlib 217 217 (``pip install phonometry[plot]``). 218 218 """ 219 + from .._i18n import check_language 219 220 from .._plot.psychoacoustics import plot_ecma_fluctuation_strength 220 221 221 - return plot_ecma_fluctuation_strength(self, ax=ax, **kwargs) 222 + return plot_ecma_fluctuation_strength(self, ax=ax, language=check_language(language), **kwargs) 222 223 223 224 224 225 # --------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/loudness_ecma.py
··· 196 196 loudness_vs_time: np.ndarray 197 197 field: str 198 198 199 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes | np.ndarray: 199 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes | np.ndarray: 200 200 """Plot the average specific loudness N'(z) (see :mod:`._plotting`). 201 201 202 202 Adds a loudness-vs-time panel. Requires matplotlib 203 203 (``pip install phonometry[plot]``). 204 204 """ 205 + from .._i18n import check_language 205 206 from .._plot.psychoacoustics import plot_ecma_loudness 206 207 207 - return plot_ecma_loudness(self, ax=ax, **kwargs) 208 + return plot_ecma_loudness(self, ax=ax, language=check_language(language), **kwargs) 208 209 209 210 210 211 # --------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/loudness_moore_glasberg.py
··· 526 526 field: str 527 527 presentation: str 528 528 529 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 529 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 530 530 """Plot the specific loudness N'(i) over the ERB-number scale. 531 531 532 532 Requires matplotlib (``pip install phonometry[plot]``); returns the 533 533 :class:`~matplotlib.axes.Axes`. See :mod:`._plotting`. 534 534 """ 535 + from .._i18n import check_language 535 536 from .._plot.psychoacoustics import plot_moore_glasberg_loudness 536 537 537 - return plot_moore_glasberg_loudness(self, ax=ax, **kwargs) 538 + return plot_moore_glasberg_loudness(self, ax=ax, language=check_language(language), **kwargs) 538 539 539 540 540 541 # ---------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/loudness_moore_glasberg_time.py
··· 369 369 field: str 370 370 presentation: str 371 371 372 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 372 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 373 373 """Plot the short-term and long-term loudness against time. 374 374 375 375 Requires matplotlib (``pip install phonometry[plot]``); returns the 376 376 :class:`~matplotlib.axes.Axes`. See :mod:`._plotting`. 377 377 """ 378 + from .._i18n import check_language 378 379 from .._plot.psychoacoustics import plot_moore_glasberg_time_loudness 379 380 380 - return plot_moore_glasberg_time_loudness(self, ax=ax, **kwargs) 381 + return plot_moore_glasberg_time_loudness(self, ax=ax, language=check_language(language), **kwargs) 381 382 382 383 383 384 # ---------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/loudness_zwicker.py
··· 112 112 time: np.ndarray | None = None 113 113 loudness_vs_time: np.ndarray | None = None 114 114 115 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes | np.ndarray: 115 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes | np.ndarray: 116 116 """Plot the specific loudness N'(z) over Bark (see :mod:`._plotting`). 117 117 118 118 Adds a loudness-vs-time panel when the time-varying trace is 119 119 present. Requires matplotlib (``pip install phonometry[plot]``); 120 120 returns the :class:`~matplotlib.axes.Axes` (or array thereof). 121 121 """ 122 + from .._i18n import check_language 122 123 from .._plot.psychoacoustics import plot_zwicker_loudness 123 124 124 - return plot_zwicker_loudness(self, ax=ax, **kwargs) 125 + return plot_zwicker_loudness(self, ax=ax, language=check_language(language), **kwargs) 125 126 126 127 def report( 127 128 self,
+3 -2
src/phonometry/psychoacoustics/psychoacoustic_annoyance.py
··· 89 89 w_s: float 90 90 w_fr: float 91 91 92 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 92 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 93 93 """Plot the PA value and the ``wS`` / ``wFR`` term contributions.""" 94 + from .._i18n import check_language 94 95 from .._plot.psychoacoustics import plot_psychoacoustic_annoyance 95 96 96 - return plot_psychoacoustic_annoyance(self, ax=ax, **kwargs) 97 + return plot_psychoacoustic_annoyance(self, ax=ax, language=check_language(language), **kwargs) 97 98 98 99 99 100 def psychoacoustic_annoyance(
+3 -2
src/phonometry/psychoacoustics/roughness_ecma.py
··· 174 174 specific_roughness_vs_time: np.ndarray 175 175 field: str 176 176 177 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes | np.ndarray: 177 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes | np.ndarray: 178 178 """Plot the roughness result (see :mod:`._plotting`). 179 179 180 180 Draws the time-dependent roughness R(l50) and a specific-roughness 181 181 heatmap. Requires matplotlib (``pip install phonometry[plot]``). 182 182 """ 183 + from .._i18n import check_language 183 184 from .._plot.psychoacoustics import plot_ecma_roughness 184 185 185 - return plot_ecma_roughness(self, ax=ax, **kwargs) 186 + return plot_ecma_roughness(self, ax=ax, language=check_language(language), **kwargs) 186 187 187 188 188 189 # --------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/tonality_ecma.py
··· 90 90 tonal_frequency_vs_time: np.ndarray 91 91 field: str 92 92 93 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes | np.ndarray: 93 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes | np.ndarray: 94 94 """Plot the average specific tonality T'(z) (see :mod:`._plotting`). 95 95 96 96 Adds a tonality-vs-time panel. Requires matplotlib 97 97 (``pip install phonometry[plot]``). 98 98 """ 99 + from .._i18n import check_language 99 100 from .._plot.psychoacoustics import plot_ecma_tonality 100 101 101 - return plot_ecma_tonality(self, ax=ax, **kwargs) 102 + return plot_ecma_tonality(self, ax=ax, language=check_language(language), **kwargs) 102 103 103 104 104 105 # --------------------------------------------------------------------------
+3 -2
src/phonometry/psychoacoustics/tone_audibility.py
··· 1030 1030 """Tone frequency of the decisive (most audible) tone, in Hz.""" 1031 1031 return float(self.tone_frequencies[int(np.argmax(self.audibilities))]) 1032 1032 1033 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 1033 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 1034 1034 """Plot the per-tone audibility ``ΔL`` against tone frequency.""" 1035 + from .._i18n import check_language 1035 1036 from .._plot.psychoacoustics import plot_tone_audibility 1036 1037 1037 - return plot_tone_audibility(self, ax=ax, **kwargs) 1038 + return plot_tone_audibility(self, ax=ax, language=check_language(language), **kwargs) 1038 1039 1039 1040 1040 1041 # Module named ``tone_audibility`` (distinct from the ISO 1996-2 Annex C
+9 -6
src/phonometry/vibration/human_vibration.py
··· 271 271 magnitude: Real 272 272 magnitude_db: Real 273 273 274 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 274 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 275 275 """Plot the weighting factor (dB) versus frequency. 276 276 277 277 Requires matplotlib (``pip install phonometry[plot]``); returns the 278 278 :class:`~matplotlib.axes.Axes` and never calls ``plt.show``. 279 279 """ 280 + from .._i18n import check_language 280 281 from .._plot.vibration import plot_vibration_weighting 281 282 282 - return plot_vibration_weighting(self, ax=ax, **kwargs) 283 + return plot_vibration_weighting(self, ax=ax, language=check_language(language), **kwargs) 283 284 284 285 285 286 def frequency_weighting(name: str, frequencies: ArrayLike) -> WeightingResponse: ··· 372 373 weighted: Real 373 374 overall: float 374 375 375 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 376 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 376 377 """Plot the unweighted and weighted band spectra with ``a_w``. 377 378 378 379 Requires matplotlib (``pip install phonometry[plot]``); returns the 379 380 :class:`~matplotlib.axes.Axes` and never calls ``plt.show``. 380 381 """ 382 + from .._i18n import check_language 381 383 from .._plot.vibration import plot_weighted_spectrum 382 384 383 - return plot_weighted_spectrum(self, ax=ax, **kwargs) 385 + return plot_weighted_spectrum(self, ax=ax, language=check_language(language), **kwargs) 384 386 385 387 386 388 def weighted_acceleration( ··· 805 807 partials: Real 806 808 assessment: ExposureAssessment 807 809 808 - def plot(self, ax: Axes | None = None, **kwargs: Any) -> Axes: 810 + def plot(self, ax: Axes | None = None, *, language: str = "en", **kwargs: Any) -> Axes: 809 811 """Plot the partial exposures against the EAV / ELV thresholds. 810 812 811 813 Requires matplotlib (``pip install phonometry[plot]``); returns the 812 814 :class:`~matplotlib.axes.Axes` and never calls ``plt.show``. 813 815 """ 816 + from .._i18n import check_language 814 817 from .._plot.vibration import plot_daily_exposure 815 818 816 - return plot_daily_exposure(self, ax=ax, **kwargs) 819 + return plot_daily_exposure(self, ax=ax, language=check_language(language), **kwargs) 817 820 818 821 819 822 def daily_vibration_exposure(
+3 -2
src/phonometry/vibration/mechanical_mobility.py
··· 402 402 """ 403 403 return convert_frf(self.mobility, self.frequencies, "mobility", target) 404 404 405 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 405 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 406 406 """Plot the mobility magnitude ``|Y(f)|``. 407 407 408 408 Requires matplotlib (``pip install phonometry[plot]``); returns the 409 409 :class:`~matplotlib.axes.Axes`. 410 410 """ 411 + from .._i18n import check_language 411 412 from .._plot.vibration import plot_mobility 412 413 413 - return plot_mobility(self, ax=ax, **kwargs) 414 + return plot_mobility(self, ax=ax, language=check_language(language), **kwargs) 414 415 415 416 416 417 def sdof_mobility_result(
+3 -2
src/phonometry/vibration/multiple_shock_vibration.py
··· 380 380 peaks: np.ndarray 381 381 risk_thresholds: tuple[float, float, float] 382 382 383 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 383 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 384 384 """Plot the injury-probability curve with this assessment's ``R``. 385 385 386 386 Requires matplotlib (``pip install phonometry[plot]``); returns the 387 387 :class:`~matplotlib.axes.Axes`. 388 388 """ 389 + from .._i18n import check_language 389 390 from .._plot.vibration import plot_multiple_shock 390 391 391 - return plot_multiple_shock(self, ax=ax, **kwargs) 392 + return plot_multiple_shock(self, ax=ax, language=check_language(language), **kwargs) 392 393 393 394 394 395 def multiple_shock_assessment(
+3 -2
src/phonometry/vibration/radiation_efficiency.py
··· 126 126 sigma = np.asarray(self.radiation_efficiency, dtype=np.float64) 127 127 return np.asarray(10.0 * np.log10(sigma), dtype=np.float64) 128 128 129 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 129 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 130 130 """Plot the radiation efficiency ``sigma(f)`` on log-log axes. 131 131 132 132 Requires matplotlib (``pip install phonometry[plot]``); returns the 133 133 :class:`~matplotlib.axes.Axes`. 134 134 """ 135 + from .._i18n import check_language 135 136 from .._plot.vibration import plot_radiation_efficiency 136 137 137 - return plot_radiation_efficiency(self, ax=ax, **kwargs) 138 + return plot_radiation_efficiency(self, ax=ax, language=check_language(language), **kwargs) 138 139 139 140 140 141 def _sigma_below(
+3 -2
src/phonometry/vibration/transfer_stiffness.py
··· 312 312 self.transfer_stiffness, self.frequencies, "dynamic_stiffness", target 313 313 ) 314 314 315 - def plot(self, ax: "Axes | None" = None, **kwargs: Any) -> "Axes": 315 + def plot(self, ax: "Axes | None" = None, *, language: str = "en", **kwargs: Any) -> "Axes": 316 316 """Plot the transfer-stiffness level ``L_k(f)``. 317 317 318 318 Requires matplotlib (``pip install phonometry[plot]``); returns the 319 319 :class:`~matplotlib.axes.Axes`. 320 320 """ 321 + from .._i18n import check_language 321 322 from .._plot.vibration import plot_transfer_stiffness 322 323 323 - return plot_transfer_stiffness(self, ax=ax, **kwargs) 324 + return plot_transfer_stiffness(self, ax=ax, language=check_language(language), **kwargs) 324 325 325 326 326 327 def indirect_transfer_stiffness_result(
+33
tests/hearing/test_hearing_plot_i18n.py
··· 1 + # Copyright (c) 2026. Jose M. Requena-Plens 2 + """EN/ES language option of the hearing ``.plot()`` renderers.""" 3 + 4 + from __future__ import annotations 5 + 6 + import pytest 7 + 8 + from phonometry import age_threshold 9 + 10 + 11 + def test_spanish_labels() -> None: 12 + pytest.importorskip("matplotlib") 13 + import matplotlib 14 + 15 + matplotlib.use("Agg") 16 + res = age_threshold(60.0, "male", fractile=0.9) 17 + 18 + ax_en = res.plot(language="en") 19 + assert ax_en.get_ylabel() == "Threshold deviation from age 18 [dB]" 20 + 21 + ax_es = res.plot(language="es") 22 + assert ax_es.get_ylabel() == "Desviación del umbral respecto a 18 años [dB]" 23 + assert "umbral de audición" in ax_es.get_title() 24 + 25 + 26 + def test_unknown_language_raises() -> None: 27 + pytest.importorskip("matplotlib") 28 + import matplotlib 29 + 30 + matplotlib.use("Agg") 31 + result = age_threshold(60.0, "male") 32 + with pytest.raises(ValueError, match="Unknown language"): 33 + result.plot(language="xx")
+43
tests/psychoacoustics/test_psychoacoustics_plot_i18n.py
··· 1 + # Copyright (c) 2026. Jose M. Requena-Plens 2 + """EN/ES language option of the psychoacoustics ``.plot()`` renderers.""" 3 + 4 + from __future__ import annotations 5 + 6 + import numpy as np 7 + import pytest 8 + 9 + from phonometry import loudness_zwicker 10 + 11 + 12 + def _result(): 13 + fs = 48000 14 + t = np.arange(int(fs * 0.3)) / fs 15 + x = np.sqrt(2.0) * 0.02 * np.sin(2.0 * np.pi * 1000.0 * t) 16 + return loudness_zwicker(x, fs, stationary=True) 17 + 18 + 19 + def test_spanish_labels_and_comma_decimals() -> None: 20 + pytest.importorskip("matplotlib") 21 + import matplotlib 22 + 23 + matplotlib.use("Agg") 24 + res = _result() 25 + 26 + ax_en = res.plot(language="en") 27 + assert ax_en.get_xlabel() == "Critical-band rate z [Bark]" 28 + 29 + ax_es = res.plot(language="es") 30 + assert ax_es.get_xlabel() == "Razón de banda crítica z [Bark]" 31 + assert "sonios" in ax_es.get_title() 32 + # Spanish uses a comma decimal separator in the annotated numbers. 33 + assert "," in ax_es.get_title() 34 + 35 + 36 + def test_unknown_language_raises() -> None: 37 + pytest.importorskip("matplotlib") 38 + import matplotlib 39 + 40 + matplotlib.use("Agg") 41 + result = _result() 42 + with pytest.raises(ValueError, match="Unknown language"): 43 + result.plot(language="xx")
+39
tests/vibration/test_vibration_plot_i18n.py
··· 1 + # Copyright (c) 2026. Jose M. Requena-Plens 2 + """EN/ES language option of the vibration ``.plot()`` renderers.""" 3 + 4 + from __future__ import annotations 5 + 6 + import numpy as np 7 + import pytest 8 + 9 + from phonometry import sdof_mobility_result 10 + 11 + 12 + def _result(): 13 + return sdof_mobility_result(np.linspace(1.0, 50.0, 200), 2.0, 8000.0, 5.0) 14 + 15 + 16 + def test_spanish_labels() -> None: 17 + pytest.importorskip("matplotlib") 18 + import matplotlib 19 + 20 + matplotlib.use("Agg") 21 + res = _result() 22 + 23 + ax_en = res.plot(language="en") 24 + assert ax_en.get_xlabel() == "Frequency [Hz]" 25 + assert ax_en.get_title() == "ISO 7626-1 mechanical mobility" 26 + 27 + ax_es = res.plot(language="es") 28 + assert ax_es.get_xlabel() == "Frecuencia [Hz]" 29 + assert ax_es.get_title() == "ISO 7626-1 movilidad mecánica" 30 + 31 + 32 + def test_unknown_language_raises() -> None: 33 + pytest.importorskip("matplotlib") 34 + import matplotlib 35 + 36 + matplotlib.use("Agg") 37 + result = _result() 38 + with pytest.raises(ValueError, match="Unknown language"): 39 + result.plot(language="xx")