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.
La regla única: los imports bajan un nivel
Sección titulada «La regla única: los imports bajan un nivel»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__)) # 25print("laeq" in phonometry.__all__) # False
from phonometry.signals import laeq # esta es la grafía nuevaMedido 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)Encontrar la casa nueva de un nombre
Sección titulada «Encontrar la casa nueva de un nombre»En vez de buscar cada uno en la referencia, pregúntaselo al paquete instalado:
import importlibimport 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.signalsprint(whereis("reverberation_time")[0]) # phonometry.roomprint(whereis("sensitivity")[0]) # phonometry.metrologyprint(whereis("Signal")[0]) # phonometryRecorre 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.
Módulos que se partieron en vez de mudarse
Sección titulada «Módulos que se partieron en vez de mudarse»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.0 | Dónde está hoy su contenido |
|---|---|
phonometry.impedance_tube | materials.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_diffusion | materials.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_absorber | materials.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.spectra | signals.spectra, más signals.multitaper (multitaper_psd) y signals.windows (window_metrics) |
phonometry.compliance | filters.compliance (FilterComplianceResult, class_limits, verify_filter_class), filters.weighting_compliance (verify_weighting_class, weighting_class_limits), aircraft.measurement_system (verify_aircraft_noise_system) |
phonometry._plotting | phonometry._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.28784print(round(float(speed_of_sound_iso10534(temperature_c=293.15)), 5)) # 477.13003343,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.0 | 4.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.
El campo tl de un resultado submarino es pl
Sección titulada «El campo tl de un resultado submarino es pl»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 .tlEl mismo renombrado llega a underwater.sonar_equation, cuyo parámetro
transmission_loss es ahora propagation_loss.
El entorno se metió en una dataclase
Sección titulada «El entorno se metió en una dataclase»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.0 | 4.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.0 | 4.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 compuestos | diameter_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.0 | 4.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 givenEsa 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))Una grafía por concepto
Sección titulada «Una grafía por concepto»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.0rc1 | 4.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_level | speed_of_sound=, density= |
speed_of_sound_m_s= en thickness_critical_frequency_product | speed_of_sound= |
sample_rate en el SignalSource de FDTD, STOIResult e impact_force_exposure_level | fs |
freq= en linkwitz_riley y adaptation_term_kc | frequency= 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.plot | frequencies= |
.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_levels | levels=, 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.
Todo .plot() toma primero los ejes
Sección titulada «Todo .plot() toma primero los ejes»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.0 | 4.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.0 | 4.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.0 | 4.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 npfrom phonometry import filters
result = filters.octave_filter(np.zeros(8000) + 1e-6, 8000, fraction=3)print(len(result.frequencies), result.bands)# 25 Nonebands 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:
| Campo | Es un Signal cuando la entrada lo era |
|---|---|
envelope(...).signal | a la frecuencia de muestreo de la entrada |
time_synchronous_average(...).period_waveform | a la frecuencia de muestreo de la entrada |
resample_signal(...).signal | a fs_new, que entonces tiene que ser un número entero de hercios |
align_impulse_responses(...).aligned, .reference | cada uno si su propia entrada lo era |
underwater.pile_strike_metrics(...).pressure | a 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.
Un flujo por bloques entrega bloques Signal
Sección titulada «Un flujo por bloques entrega bloques Signal»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.
Las tablas publicadas no admiten escritura
Sección titulada «Las tablas publicadas no admiten escritura»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_CURVEfrom phonometry.speech.sii import BAND_IMPORTANCE
curve = dict(REFERENCE_CURVE)curve[4000] = 1.0weights = BAND_IMPORTANCE.copy()weights[0] = 0.0print(REFERENCE_CURVE[4000], curve[4000], weights[0], BAND_IMPORTANCE[0])# 0.9 1.0 0.0 0.0083Una 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.
| Antes | 4.0 |
|---|---|
SolidMaterial.estimated, OrthotropicWood.estimated | row.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ón | todos los campos por nombre, como en cualquier otra fila |
from phonometry._internal.catalogue import CatalogueRow | from 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"))# estimatedprint(ear.converted["youngs_modulus_max_pa"], ear.is_derived("youngs_modulus_max_pa"))# ('3e5', 'psi') FalseLas 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:
| Antes | 4.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 cadena | layer.attributed_to["row"], un mapeo como en todas las filas |
ResilientLayer(...) con la rigidez, la densidad y el espesor obligatorios | todas 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.6Una 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 aceptaba | 4.0 |
|---|---|
NaN, un infinito, True o un texto como "0,97" en un campo numérico | CatalogueError; 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 texto | CatalogueError |
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 texto | CatalogueError, 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 negativa | CatalogueError |
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 nada | CatalogueError: 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 100 | CatalogueError; 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 diccionario | un 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 incluidos | TypeError 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 nada | ValueError, 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 unidad | None, 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.
| Antes | 4.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 banda | se 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}_db | class 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 preguntar | requirements, 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 1Los diez nombres que ya no están
Sección titulada «Los diez nombres que ya no están»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, normalizedfreq | filters.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_sensitivity | metrology.sensitivity, el mismo renombrado de la 3.1 |
filter_class_compliance | filters.verify_filter_class |
air_density_iso, speed_of_sound_iso | materials.air_density_iso10534, materials.speed_of_sound_iso10534, y lee el aviso de los kelvin de más arriba |
speed_of_sound | fluids.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_pressure | fluids.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 |
TransmissionLossResult | PropagationLossResult, con .tl renombrado a .pl |
Lo que no ha cambiado
Sección titulada «Lo que no ha cambiado»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.