Ir al contenido
Esta documentación describe la versión 4.0.0, todavía sin publicar. La versión actual en PyPI es la 3.3.0 y no incluye todo lo que se describe aquí.

Actualizar de la 3.3 a la 4.0

La 4.0 rompe duro y no lleva capa de compatibilidad. Casi nada se ha borrado: de los 885 nombres que la raíz exportaba en la 3.3.0, diez no existen ya en ningún sitio y el resto simplemente vive en una dirección con nombre en vez de en una plana. Así que el trabajo es sobre todo un barrido de imports, y esta página es sobre todo un mapa.

Con las dos primeras secciones arreglas los imports. La tercera léela aunque tengas prisa, porque en tres sitios el arreglo evidente es el equivocado, y uno de esos tres cambia un número sin levantar nada.

En la 3.3.0 la raíz reexportaba la biblioteca entera, así que from phonometry import laeq funcionaba para casi cualquier nombre. Hoy la raíz exporta veinticinco cosas: los veintiún subpaquetes, las tres clases que no pertenecen a ningún dominio concreto (Signal, ReportMetadata, PhonometryWarning) y __version__.

import phonometry
print(len(phonometry.__all__)) # 25
print("laeq" in phonometry.__all__) # False
from phonometry.signals import laeq # esta es la grafía nueva

Medido sobre la superficie entera, 872 de aquellos 885 nombres de la raíz los publica hoy exactamente un subpaquete, ninguno dos, y ninguno necesita una ruta más profunda que phonometry.<subpaquete>. Otros tres siguen en la raíz, donde siempre estuvieron. Así que el cambio no tiene ambigüedad: busca el subpaquete e importa de él.

La forma de atributo no se retiró nunca, que es el cambio más pequeño posible para el código que tiraba de la raíz:

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

En vez de buscar cada uno en la referencia, pregúntaselo al paquete instalado:

import importlib
import pkgutil
import phonometry
def whereis(name):
"""Cada módulo público que publica `name`, del menos profundo al que más."""
found = ["phonometry"] if name in phonometry.__all__ else []
for attribute in phonometry.__all__:
package = getattr(phonometry, attribute)
if not hasattr(package, "__path__"):
continue
modules = [package.__name__]
modules += [m.name for m in pkgutil.walk_packages(package.__path__, package.__name__ + ".")]
for module_name in modules:
if any(part.startswith("_") for part in module_name.split(".")):
continue
try:
module = importlib.import_module(module_name)
except ImportError:
continue
if name in getattr(module, "__all__", ()):
found.append(module_name)
return sorted(found, key=lambda path: path.count("."))
print(whereis("laeq")[0]) # phonometry.signals
print(whereis("reverberation_time")[0]) # phonometry.room
print(whereis("sensitivity")[0]) # phonometry.metrology
print(whereis("Signal")[0]) # phonometry

Recorre el árbol en unos dos segundos. Cuando devuelve varias rutas, la menos profunda es el import canónico: un subpaquete reexporta lo que publican sus propios módulos, y esa ruta corta es la que usan las guías y los tests. Los cuatro nombres que la raíz sigue publicando por su cuenta, Signal, PhonometryWarning, ReportMetadata y __version__, vuelven como phonometry a secas. Una lista vacía significa que el nombre está en la última sección de esta página.

Cinco de los treinta módulos que cambiaron de ruta no se mudaron a una dirección nueva. Se dividieron, y quien siga sólo la primera mitad de la partición se lleva un ImportError por el resto. Merece la pena revisarlos uno a uno, y el paquete privado de renderizado va con ellos porque le pasó lo mismo:

Módulo en 3.3.0Dónde está hoy su contenido
phonometry.impedance_tubematerials.absorbers.impedance_tube (función de transferencia de dos micrófonos), materials.absorbers.four_microphone (TransferMatrix, transfer_matrix_one_load, wave_decomposition, face_quantities, air_layer_transfer_matrix), materials.absorbers.standing_wave y fluids
phonometry.scattering_diffusionmaterials.diffusers.reverberation_room_scattering para la parte de cámara reverberante (ScatteringResult, ScatteringUncertainty, BASE_PLATE_BANDS y el resto), materials.diffusers.scattering_diffusion para los nueve nombres de campo libre
phonometry.materials.porous_absorbermaterials.absorbers.porous para los modelos de material en masa, materials.absorbers.layered para toda la API multicapa (layered_absorber, AirLayer, MembraneLayer, PerforatedPlateLayer, MicroperforatedPlateLayer, LayeredAbsorberResult)
phonometry.metrology.spectrasignals.spectra, más signals.multitaper (multitaper_psd) y signals.windows (window_metrics)
phonometry.compliancefilters.compliance (FilterComplianceResult, class_limits, verify_filter_class), filters.weighting_compliance (verify_weighting_class, weighting_class_limits), aircraft.measurement_system (verify_aircraft_noise_system)
phonometry._plottingphonometry._plot.<dominio>: los 82 renderizadores se repartieron por dominio, así que plot_airborne_insulation está en _plot.building y plot_age_threshold en _plot.hearing. Privado en ambos casos, y la vía soportada es el .plot() del resultado

Los otros veinticinco se mudaron enteros, y el resolutor de arriba los encuentra sin leer esta tabla. Cada uno de los tres primeros era alcanzable por dos rutas en la 3.3.0, la plana y la de dentro de su paquete, y las dos han desaparecido.

Tres sitios donde el arreglo evidente es el equivocado

Sección titulada «Tres sitios donde el arreglo evidente es el equivocado»

Todo lo demás de esta página se anuncia en cuanto lo ejecutas, y el arreglo es el que parece. Estos tres no son así.

Los auxiliares del tubo de impedancia tomaban kelvin

Sección titulada «Los auxiliares del tubo de impedancia tomaban kelvin»

air_density_iso y speed_of_sound_iso documentaban y consumían su temperatura en kelvin. Sus sucesores toman grados Celsius, como el resto de la biblioteca. El nombre cambió, así que el import falla y te enteras, pero el problema es el argumento que arrastras: 293,15 es un número de aspecto legítimo en cualquiera de las dos unidades.

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

343,29 m/s frente a 477,13 m/s: un 39 % de error, sin excepción ninguna, y cada coeficiente de absorción calculado a partir de ahí sale mal en una cantidad de aspecto plausible. Convierte el valor, no te limites a renombrar la palabra clave.

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

Con la conversión aplicada, los dos devuelven exactamente lo que devolvían en la 3.3.0, bit a bit.

scattering_diffusion.speed_of_sound tomaba Celsius, y su sustituto responde distinto

Sección titulada «scattering_diffusion.speed_of_sound tomaba Celsius, y su sustituto responde distinto»

El auxiliar de ISO 17497-1 que se llamaba igual tomaba grados Celsius, así que ahí no hay conversión que hacer. La trampa es el sustituto. fluids.air() usa un modelo del medio más completo que aquella fórmula de una línea de la ISO 17497-1, y a 20 °C responde 343,99 m/s donde el auxiliar viejo respondía 343,20 m/s. Es poco, y no es nada despreciable si estás reproduciendo una medición hecha con el número antiguo. El apartado 8 de la ISO 17497-1 imprime la fórmula si necesitas ese apartado exacto.

transmission_loss pasó a propagation_loss, cosa que el import detecta, y con él se renombró el campo de dentro del resultado. Ese no levanta nada: un PropagationLossResult no tiene .tl, así que el AttributeError aparece allí donde lo leas, que puede quedar lejos de la llamada.

from phonometry.underwater.propagation.closed_form import propagation_loss
result = propagation_loss([1000.0], 1000.0)
print(round(float(result.pl[0]), 5)) # 60.06013, el número que guardaba .tl

El mismo renombrado llega a underwater.sonar_equation, cuyo parámetro transmission_loss es ahora propagation_loss.

Cinco puntos de entrada ya no toman las condiciones ambientales como números sueltos. Ahí no basta con renombrar la palabra clave, porque el parámetro ha desaparecido del todo:

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

Las sustituciones punto a punto del rotorcraft fueron por el mismo camino: bank_angle y path_angle son ahora RotorcraftTrackState(bank_angle_deg=..., path_angle_deg=...).

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

Un parámetro que nombra una magnitud lleva su unidad

Sección titulada «Un parámetro que nombra una magnitud lleva su unidad»

En todo lo demás, las condiciones ambientales conservaron su sitio y ganaron un sufijo. La regla es que un parámetro público que nombra una magnitud dimensional termina en la unidad que teclea quien llama, porque el número es donde se pierde la unidad y ninguna guardia puede cazarlo por magnitud: 101,325 y 101325 son las dos presiones legítimas en esta biblioteca.

3.3.04.0
temperature=temperature_c=
pressure=, static_pressure=, barometric_pressure=, ambient_pressure=, atmospheric_pressure=atmospheric_pressure_kpa=, o _pa= donde el apartado esté escrito en pascales
humidity=, relative_humidity=relative_humidity_percent=
diameter= y sus trece compuestosdiameter_m=
angle=, angles=, bank_angle=, path_angle=, grazing_angle=, launch_angles=_deg= o _rad=, según lo que lea la función
runway_gradient=runway_gradient_ratio=
sound_speed_gradient=sound_speed_gradient_per_s=

Un caso merece mención propia porque era una trampa y no una incomodidad. critical_angle era un campo en radianes en un resultado y un parámetro en grados en las dos funciones que estaban una pantalla más abajo, o sea la misma palabra para dos cosas separadas por un factor 57. Ahora es critical_angle_rad en el resultado y critical_angle_deg en los parámetros.

transmission_loss= pasó a propagation_loss= sólo en los resultados de sónar y de propagación numérica submarina. ReactiveSilencerResult y SoundReductionResult siguen llamando al suyo transmission_loss=, y con razón: es como lo llaman las normas que hay detrás.

Las funciones de la EN 29052-1 de materials siguen la misma regla, con la unidad de cada magnitud al final de su nombre:

3.3.04.0
apparent_dynamic_stiffness(resonant_frequency=, total_mass_per_area=)resonant_frequency_hz=, total_mass_per_area_kg_m2=
enclosed_gas_stiffness(thickness=)thickness_m=
installed_dynamic_stiffness(apparent_stiffness, airflow_resistivity, gas_stiffness=)apparent_stiffness_n_m3, airflow_resistivity_kpa_s_m2= sólo por nombre, gas_stiffness_n_m3=
natural_frequency(dynamic_stiffness=, mass_per_area=)dynamic_stiffness_n_m3=, mass_per_area_kg_m2=
floating_floor_resonance(resonant_frequency=, total_mass_per_area=, floor_mass_per_area=, airflow_resistivity=, thickness=)resonant_frequency_hz=, total_mass_per_area_kg_m2=, floor_mass_per_area_kg_m2=, airflow_resistivity_kpa_s_m2=, thickness_m=

La resistividad al flujo es la que se gana la palabra clave. La EN 29052-1 pone sus umbrales en kPa·s/m², y todas las demás resistividades al flujo de la biblioteca van en Pa·s/m², una unidad mil veces menor, así que una cifra en Pa·s/m² pasada por posición caía sin aviso en la rama equivocada del apartado 8.2; ahora se escribe por su nombre. Y por debajo de 100 kPa·s/m², installed_dynamic_stiffness ya no toma como cero un término de gas ausente: la 3.3.0 devolvía con installed_dynamic_stiffness(20e6, 50.0), la Fórmula 6 sin su segundo término, y la 4.0 lanza un error y pide .

Las condiciones y las banderas se escriben por su nombre

Sección titulada «Las condiciones y las banderas se escriben por su nombre»

Dos reglas dejaron 37 firmas keyword-only. Una condición o una opción que lleva valor por defecto se escribe por su nombre, y toda bandera booleana pública es keyword-only. Las dos existen porque el sitio donde se pierde el significado es la llamada: air_attenuation(freqs, 20.0, 50.0, 101.325) son una temperatura, una humedad y una presión en un orden que no recuerda nadie, y bank.filter(x, True) no dice cuál de cinco banderas se ha activado.

El fallo es ruidoso, que es de lo que se trata:

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

Esa llamada merece una segunda mirada. leq nunca tuvo fs, porque una media energética no lo necesita, así que en la 3.3.0 el 48000 se ligaba en silencio a calibration_factor y la respuesta salía 93,6 dB alta sin una queja. Ahora es un TypeError, que es lo que siempre debió ser.

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

Los nombres públicos se habían ido repartiendo entre varias grafías de la misma magnitud, y la 4.0 se queda con la que ya usaba la mayoría. La regla de las magnitudes que se cuentan en plural es la que seguía casi toda la API: un campo o un parámetro que guarda un array es frequencies, times, distances o levels, y uno que guarda un solo valor va en singular. Un argumento vectorizado que admite un número o un array conserva su nombre.

3.3.0 o 4.0.0rc14.0
sound_speed (válvulas de control, medición de silenciadores, regímenes de Weston, ruido ambiente, FluidSeabed, SoundSpeedProfile, ShipSourceLevelResult)speed_of_sound
c= y rho= en sound_intensity, c= en las dos conversiones de desajuste de fase, phase_mismatch(), absorption_coefficient_uncertainty y monopole_source_levelspeed_of_sound=, density=
speed_of_sound_m_s= en thickness_critical_frequency_productspeed_of_sound=
sample_rate en el SignalSource de FDTD, STOIResult e impact_force_exposure_levelfs
freq= en linkwitz_riley y adaptation_term_kcfrequency= y frequencies=
.frequency en los resultados que llevan un eje de frecuencias: IntensityResult, FieldIndicators, IntensityInstrumentComplianceResult, RoomAcousticsResult, los cinco resultados de auditorio, ImpedanceTubeResult, PorousMediumResult, LayeredAbsorberResult, DiffuseFieldAbsorptionResult, BiotWavesResult, SlitResonatorAbsorberResult, MetadiffuserResult, TransferMatrix, los dos resultados de ruido de válvulas, AircraftBandAttenuation, AmbientNoiseResult, ShipTrafficSpectrum.frequencies
frequency= con un array: las funciones de transmisión por flancos, internal_spectrum, pipe_transmission_loss, expander_noise, transfer_matrix_one_load, transfer_matrix_two_load, TransferMatrix.plotfrequencies=
.time en ZwickerLoudness, EcmaLoudness, MooreGlasbergTimeVaryingLoudness, los resultados de tonalidad, aspereza y fuerza de fluctuación de ECMA-418-2, DecayCurve, QuasiPeakResult.times
.level en DecayCurve, NpdLevelResult, NoiseContourResult, RotorcraftNoiseContourResult, LowFrequencyResult, DuctPathStage, LateLateralResult, TransferStiffnessResult.levels
.distance en NpdLevelResult y RotorcraftEventResult.distances
level=, measured_level=, operational_level= en sti_from_impulse_response, stipa, sti_adjusted_for_levels y STIResult.adjusted_for_levelslevels=, measured_levels=, operational_levels=
.passed en AircraftSystemComplianceResult, QuasiPeakDynamicsResult, HeavyImpactSourceCheck, RigidMassCalibrationResult.passes, como en todos los demás verificadores
MicrophoneNoise(weighting="CCIR")MicrophoneNoise(weighting="468"), la grafía que usan weighting_filter y weighted_thd para la curva UIT-R BS.468-4

times, levels = decay_curve(ir, fs) sigue desempaquetando la curva de caída, en el mismo orden. Los solvers FDTD conservan c y rho, que nombran los mapas en los que están escritas sus ecuaciones.

El .plot() de cada resultado se lee ahora plot(ax=None, *, ...): primero los ejes, y todo lo demás por su nombre, language incluido. Tres no empezaban por los ejes en la 3.3.0, así que plot(ax) les entregaba los ejes al parámetro equivocado:

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

Seis resultados posteriores a la 3.3.0 tomaban language por posición en la 4.0.0rc1: las cuatro valoraciones de protectores auditivos y los dos resultados de intensimetría en baja frecuencia. plot(ax, "es") sobre ellos es ahora plot(ax, language="es"), como en todos los demás.

Los veredictos son objetos de resultado, no diccionarios

Sección titulada «Los veredictos son objetos de resultado, no diccionarios»

Las cinco funciones verify_* devolvían dict[str, Any] y ahora devuelven una dataclase congelada, como cualquier otro resultado de la biblioteca. Dos envoltorios públicos que existían sólo para empaquetar ese diccionario se fueron con el cambio, ya que el verificador devuelve ahora el objeto él mismo.

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

verify_running_rms_decay es posterior a la 3.3.0, y en la 4.0.0rc1 devolvía un bool desnudo, que tiraba el intervalo impreso y el tiempo medido que había juzgado. Ahora devuelve un RunningRmsDecayVerification que conserva los dos: el veredicto es .passes, junto a .measured_time_s, .printed_time_s, .tolerance_s y el intervalo como .lower_time_s y .upper_time_s. El objeto se niega a leerse como valor de verdad, así que un if verify_running_rms_decay(...): escrito contra la versión candidata levanta TypeError en vez de dar por buenos todos los vibrómetros.

El banco de filtros devuelve un resultado, no una tupla

Sección titulada «El banco de filtros devuelve un resultado, no una tupla»

octave_filter() y OctaveFilterBank.filter() devolvían dos elementos normalmente y tres cuando sigbands=True pedía las señales por banda. La longitud de la respuesta dependía de una palabra clave, que es la razón por la que ese par necesitaba doce declaraciones @overload para poder tiparse, y por la que _, _, bands = bank.filter(...) era una línea en la que había que contar comas.

Ahora devuelven un OctaveFilterResult con campos con nombre, como cualquier otro cálculo de la biblioteca.

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

bands es None salvo que la llamada haya pedido sigbands=True, y levels es None en una llamada hecha con calculate_level=False. El código que necesita cualquiera de los dos puede decirlo con require_bands() o require_levels(), que devuelven el valor o fallan nombrando el argumento que faltaba, en vez de entregar un None que revienta unas líneas más adelante.

El resultado también se dibuja solo, así que el espectro de bandas es una llamada:

ax = result.plot()

Las formas de onda dentro de los resultados vuelven como el Signal del que salieron

Sección titulada «Las formas de onda dentro de los resultados vuelven como el Signal del que salieron»

Una transformación a la que se le entrega un Signal devuelve un Signal desde que se escribió el contrato, pero cinco objetos de resultado todavía guardaban su forma de onda como un array desnudo junto a un fs suelto. Ahora conservan el tipo con el que llegó la entrada:

CampoEs un Signal cuando la entrada lo era
envelope(...).signala la frecuencia de muestreo de la entrada
time_synchronous_average(...).period_waveforma la frecuencia de muestreo de la entrada
resample_signal(...).signala fs_new, que entonces tiene que ser un número entero de hercios
align_impulse_responses(...).aligned, .referencecada uno si su propia entrada lo era
underwater.pile_strike_metrics(...).pressurea la frecuencia de muestreo de la entrada

Un array desnudo sigue devolviendo un array desnudo, así que el código que pasa arrays no lo nota. El código que pasa un Signal y hace aritmética con uno de estos campos pasa por np.asarray, como con cualquier Signal, y una entrada calibrada vuelve con calibration_factor=1.0, así que el campo alimenta a la función siguiente en pascales sin que el factor se aplique otra vez.

io.read_blocks es posterior a la 3.3.0, y en la 4.0.0rc1 entregaba arrays float64 desnudos. Ahora entrega un Signal por bloque, con la frecuencia de muestreo, la calibración, las etiquetas de canal y la procedencia que da io.read, y toma el mismo calibration_factor= y lee el mismo sidecar. Para un bucle escrito contra la versión candidata cambian dos cosas. Un bloque de un archivo calibrado llega a los filtros y a las funciones de nivel en pascales, así que un calibration_factor aplicado a mano encima cuenta dos veces. Y la aritmética sobre un bloque, o sobre lo que un filtro devuelve para él, pasa por np.asarray, como con cualquier Signal.

Toda tabla que la biblioteca publica a nivel de módulo es de sólo lectura en la 4.0. En la 3.3.0 las tablas publicadas eran diccionarios corrientes y los arrays publicados admitían aritmética sobre el propio array, así que una línea como REFERENCE_CURVE[500] = 0.9 o BAND_IMPORTANCE *= 2 cambiaba el número para todo lo que se ejecutara después en el mismo proceso, sin que nada lo avisara. Los diccionarios son ahora types.MappingProxyType, también los que van anidados dentro de otra tabla, y los arrays tienen desactivada la bandera writeable. Leer no cambia; escribir lanza TypeError en una tabla y ValueError en un array.

El código que retocaba una tabla publicada tiene que copiarla antes:

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

Una tabla que lleva tablas dentro necesita copiar cada nivel, como en {area: dict(rows) for area, rows in GUIDE_VALUES.items()}. La misma copia hace falta antes de serializar una tabla con pickle, copiarla en profundidad o escribirla como JSON, porque un mapeo de sólo lectura no admite ninguna de las tres cosas.

Una fila de catálogo dice lo que su fuente afirma de cada celda

Sección titulada «Una fila de catálogo dice lo que su fuente afirma de cada celda»

Los catálogos publicados de materiales son nuevos en la 4.0: ni la 3.3.0 ni la 4.0.0rc1 los tenían. Cambiaron de forma una vez de camino a la versión final, y el código escrito contra la rama principal entretanto necesita los cambios de abajo. Todas las filas de todos los catálogos comparten ahora una base, io.CatalogueRow (con io.BandedRow para una fila que imprime un valor por banda), y las mismas tres preguntas tienen la misma respuesta en cualquiera de ellas.

Antes4.0
SolidMaterial.estimated, OrthotropicWood.estimatedrow.basis, que asigna "estimated" a cada campo con la nota al pie
SolidMaterial.is_estimate(field), OrthotropicWood.is_estimated(field)row.basis_of(field) == "estimated"
row.is_derived(field) en un valor que la página da en una unidad que la fila no guarda (los °F y psi de la Tabla 14.1 de Ver & Beranek, los sabines de la Tabla 7.1 de Long)field in row.converted; row.converted[field] es la cifra de la página y su unidad, como ("3e5", "psi")
row.is_derived(field) en un valor que la página da por referencia a otra de sus filas: una celda en blanco bajo un bloque (Tabla 8.7 de Ver & Beranek, Tabla 30 de ASHRAE) o una descripción que dice «Parecido al anterior» (capítulo 32 de Harris)field in row.carried; row.carried[field] nombra la fila
OrthotropicWood(...) y PlateauMaterial(...) con sus propios campos por posicióntodos los campos por nombre, como en cualquier otra fila
from phonometry._internal.catalogue import CatalogueRowfrom phonometry.io import CatalogueRow

is_derived responde ahora sólo por lo que calcula la biblioteca y podría volver a calcular con las celdas de la propia fila, como una velocidad de barra obtenida de un módulo y una densidad. Un valor que la página da en otra unidad sigue siendo el número de la página, y también lo es uno que da por referencia a otra de sus filas, así que ninguno de los dos es ya derivado. basis_of responde con la entrada del propio campo, si no con la de la fila, y si no con una cadena vacía, que quiere decir que la fuente no dice cómo se obtuvo el número; las cinco cosas que puede afirmar una fuente están en io.CATALOGUE_BASES.

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

Las capas elásticas pasaron a ser filas de catálogo en el mismo cambio. Una ResilientLayer es una io.CatalogueRow como cualquier otra fila, con la clave y la atribución de las demás:

Antes4.0
PUBLISHED_RESILIENT_LAYERS["mineral_wool_rock_60_30"], resilient_layer("mineral_wool_rock_60_30")"hopkins-2007-table-a3/mineral_wool_rock_60_30", la clave "<tabla>/<fila>" de todos los catálogos
layer.attributed_to, una cadenalayer.attributed_to["row"], un mapeo como en todas las filas
ResilientLayer(...) con la rigidez, la densidad y el espesor obligatoriostodas las magnitudes opcionales, y apparent_dynamic_stiffness_n_m3 junto a dynamic_stiffness_n_m3 para una fuente que sólo da la aparente
layer.natural_frequency(mass_per_area=...)layer.natural_frequency(mass_per_area_kg_m2=...); una capa que sólo guarda toma además airflow_resistivity_pa_s_m2= y gas_stiffness_n_m3=, y sin el primero lanza io.CatalogueError

La Tabla A3 de Hopkins imprime , la rigidez de la capa instalada tal como la define la lista de símbolos del propio libro, así que sus quince filas siguen guardando dynamic_stiffness_n_m3 y devuelven las frecuencias naturales que devolvían. Cuatro de ellas guardan una densidad que la página imprime una sola vez para su bloque y deja en blanco en su propia línea, y layer.carried["density_kg_m3"] nombra ahora la fila que la imprime; la densidad es la misma.

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

Una fila de catálogo se comprueba al construirse

Sección titulada «Una fila de catálogo se comprueba al construirse»

Toda fila de catálogo se comprueba en su constructor, la construya la biblioteca a partir de una tabla empaquetada o quien llama a mano, y una celda que nada de lo que viene después podría leer lanza io.CatalogueError, nombrando la fila y el campo. El código escrito entretanto contra la rama principal que contara con que se aceptara algo de lo siguiente tiene que cambiar:

Antes se aceptaba4.0
NaN, un infinito, True o un texto como "0,97" en un campo numéricoCatalogueError; una celda que la página deja vacía es None
una fracción en un campo entero (year), 1 en un campo booleano (has_section_drawing), un número en un campo de textoCatalogueError
un name o un source vacíos, o un matiz cuyo texto está vacío, como misprinted={"porosity": ""}CatalogueError; why_missing respondía "" para esa celda, lo que escondía el matiz
un matiz con la clave de un campo que la clase no tiene, como en ranges={"no_such_field": ...}, o un matiz sobre un número (ranges, approximate, unquantified, …) con la clave de un campo de textoCatalogueError, que nombra el matiz y la clave
bounded_above o bounded_below sobre un campo sin intervalo; un intervalo, o uno de los intervalos de las lecturas de reported, con un extremo que no es finito o con el extremo bajo por encima del alto; una lista de reported sin nada dentro; una uncertainty negativaCatalogueError
un valor junto a misprinted, unquantified, not_derivable o reported en el mismo campo; una entrada de converted o carried en un campo que no guarda nadaCatalogueError: los cuatro primeros dicen que no hay valor que servir, y los dos últimos no tienen nada que describir
un valor negativo en un campo cuyo nombre acaba en _kg_m3, _kg_m2, _kg, _kg_mol, _m_s, _pa, _pa_s_m2, _pa_s_m, _n_m3, _mm, _um, _m, _m2, _m_hz o _per_cm (una densidad, una masa por unidad de superficie, una masa, una masa molar, una velocidad, una presión o un módulo, una resistividad al flujo, una resistencia al flujo específica, una rigidez por unidad de superficie, una longitud, un área, un producto espesor por frecuencia, una cuenta por centímetro); una porosidad fuera de 0 a 1; shot_content_percent, binder_content_percent o adhered_area_percent fuera de 0 a 100CatalogueError; una temperatura en grados Celsius, una tasa de decaimiento por metro y un nivel en decibelios siguen pudiendo ser negativos
approximate=[...] guardado como la lista que se le dio, un mapeo guardado como el diccionarioun frozenset, y todo mapeo congelado hasta el fondo, con sus pares en tuplas
un campo de una subclase anotado con un tipo para el que el contrato no tiene comprobación, un float o un int a secas incluidosTypeError la primera vez que se construye la clase; un campo es float | None (Optional[float] es lo mismo), int | None, bool, str, frozenset[str] o un Mapping[str, ...] de ellos, porque cualquier celda de una fila puede faltar
PorousMaterial.frame_constants() sobre una fila sin structural_loss_factor, leída como un esqueleto que no disipa nadaValueError, que dice qué tenía la página en esa celda
GroundSurface.porosity con valores de 26,9 a 58,1 en seis filas de la Tabla 6.7 de Cox y D’Antonio, que la página imprime en tanto por ciento en una columna de fracciones que no dice su unidadNone, con misprinted["porosity"] citando la cifra que imprime la página; why_missing("porosity") y printed("porosity") lo dicen

Todas las demás filas empaquetadas cumplían ya el contrato, así que esas seis porosidades son las únicas celdas publicadas que cambian, y todas las filas porosas publicadas con los dos módulos imprimen su factor de pérdidas.

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

La clase de un banco de filtros lee todos los requisitos de IEC 61260-1

Sección titulada «La clase de un banco de filtros lee todos los requisitos de IEC 61260-1»

verify_filter_class calificaba solo la máscara de la Tabla 1. Para la edición de 2014 califica ahora también la desviación del ancho de banda efectivo del 5.12 y la suma de las señales de salida del 5.16, calculadas como las calcula IEC 61260-2:2016, y la class de una banda y la overall_class del banco son la clase más estricta que se cumple en los tres. Un veredicto con edition="1995" no cambia.

Antes4.0
cada banda de un banco diezmado se diezmaba hasta que su frecuencia de Nyquist de proceso era 1,25 veces su borde superior de bandase diezma sólo hasta dejarla en dieciséis veces ese borde, así que la transformación bilineal ya no deforma la banda: el nivel de una banda se mueve hasta 0,07 dB con ruido blanco, sobre todo en las bandas más bajas, y el banco de octavas, que sumaría sus salidas hasta +0,94 dB y leería clase 2 en el 5.16, es clase 1 en todos los requisitos
la class de una banda leída de margin_class{c}_dbclass sobre todos los requisitos; margin_class{c}_db sigue siendo el margen de la Tabla 1, junto a bandwidth_margin_class{c}_db y summation_margin_class{c}_db, que vale None en las dos bandas de los extremos
un solo requisito por el que preguntarrequirements, requirement_class(name) y binding_margin_db(name, cls) leen cada uno por separado, y plot(requirement="summation") lo dibuja

Los bancos de octavas y de tercios de octava siguen siendo clase 1.

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

Todo lo demás se movió. Estos diez no existen en ningún módulo público:

Ya no estáQué escribir en su lugar
octavefilter, getansifrequencies, normalizedfreqfilters.octave_filter, filters.nominal_frequencies, filters.normalized_frequencies. Eran las grafías originales de PyOctaveBand, renombradas en la 3.1 y conservadas como alias hasta la 4.0
calculate_sensitivitymetrology.sensitivity, el mismo renombrado de la 3.1
filter_class_compliancefilters.verify_filter_class
air_density_iso, speed_of_sound_isomaterials.air_density_iso10534, materials.speed_of_sound_iso10534, y lee el aviso de los kelvin de más arriba
speed_of_soundfluids.air(temperature_c=...).speed_of_sound para el medio. Usa un modelo más completo que aquella fórmula de una línea de la ISO 17497-1, así que el número se mueve: 343,99 m/s frente a 343,20 m/s a 20 °C
depth_to_pressurefluids.depth_to_gauge_pressure_mpa(depth_m=...), que devuelve el mismo número, o fluids.depth_to_absolute_pressure_pa para la absoluta. Las dos son keyword-only y las dos toman un latitude_deg opcional
TransmissionLossResultPropagationLossResult, con .tl renombrado a .pl

Los números. El informe de conformidad son las mismas 995 comprobaciones contra las mismas normas con los mismos valores esperados que tenía antes de la reorganización, y de cada renombrado de esta página se comprobó que devuelve lo que devolvía su predecesor. Si un resultado se mueve después de que termines de actualizar, eso es un defecto y merece la pena avisar.

Un veredicto es la excepción, y la sección sobre las clases de filtro de más arriba explica cómo: para la edición de 2014, verify_filter_class califica el ancho de banda efectivo del 5.12 y la suma de salidas del 5.16 además de la máscara de la Tabla 1, así que la clase de un banco es la más estricta que cumple en los tres. Sus márgenes de la Tabla 1 y un veredicto con edition="1995" son los números que eran.

Los bancos de filtros diezmados son la otra: cada banda conserva ahora su frecuencia de Nyquist de proceso en dieciséis veces su borde superior, así que el nivel de una banda se mueve hasta 0,07 dB con ruido blanco, como dice la misma sección.

Lo que dibuja cada .plot() está intacto, y también lo que imprime cada .report() salvo una etiqueta: la ficha del micrófono escribe dB(468) donde escribía dB(CCIR). El contrato de Signal es el que era, ahora cumplido en dos sitios más, los bloques de read_blocks y las formas de onda dentro de cinco resultados. Cada fórmula está intacta también. La 4.0 recolocó los muebles y puso la unidad en la etiqueta; salvo esas dos, no recalculó nada.