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í.

Aire húmedo

Normas aplicables: IEC 61094

Una calibración por reciprocidad necesita conocer el aire en el que se hace con cinco o seis cifras, y por eso la norma que define esa calibración lleva también el modelo de aire húmedo mejor enunciado de la literatura acústica. El Anexo F es ese modelo, y esta página es lo que la biblioteca hace con él.

from phonometry import fluids
f = fluids.air(
temperature_c=23.0,
static_pressure_pa=101325.0,
relative_humidity_percent=50.0,
)
print(f.density) # 1.1860847889882964 kg/m3
print(f.speed_of_sound) # 345.86651725321 m/s
print(f.characteristic_impedance) # 410.2270151343906 Pa.s/m

Ésas son las condiciones de la primera fila de la Tabla F.1, y los números son los que imprime el anexo. Cada una de las diez cifras que tabula vuelve dentro del redondeo de su última posición impresa.

La biblioteca pide la temperatura y supone las otras dos. No es pereza en una dirección y rigor en la otra: se sigue de cuánto vale cada una. Barriendo cada condición por su rango plausible, con las propias ecuaciones del anexo:

CondiciónBarrida entreMueve la densidad un
Presión estática80 y 105 kPa25 %
Temperatura15 y 27 °C4,5 %
Humedad relativa0 y 100 %1,05 %

La presión domina, y además es la que la gente cree saber ya. Un emplazamiento de ensayo a 1000 metros está a unos 90 kPa. Eso es un 11 % por debajo de la atmósfera estándar que la biblioteca supondría, y deja la densidad un 13 % alta, que en un nivel de potencia sonora es medio decibelio sin nada en la página que lo diga.

La humedad es el caso contrario. Es la que menos mueve la densidad, y casi nadie conoce la suya sin medirla.

Así que las dos se suponen y las dos se anuncian, una sola vez, en el mismo aviso:

import warnings
from phonometry import fluids
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
fluids.air(temperature_c=20.0)
print(caught[0].category.__name__) # FluidAssumptionWarning
print(str(caught[0].message))
# air() assumed 101325 Pa and 50 % relative humidity. The pressure is the one
# worth measuring: a site 1000 m up sits near 90 kPa, and taking it for one
# standard atmosphere puts the density about 13 % high. The whole span of
# humidity, 0 % to 100 %, is worth about 1 % of the density. Pass the
# conditions to silence this.

Un aviso y no dos, porque quien no pasó ninguna de las dos tiene una cosa que arreglar, no dos.

Pasar las dos es silencioso, que es la propiedad que debe recibir quien midió su aire. Python muestra el aviso una vez por sitio de llamada, así que explorar en un intérprete no queda ahogado en él, y cuelga de PhonometryWarning, de modo que una sola regla lo convierte en error para quien prefiera que sus ejecuciones fallen en vez de avisar:

import warnings
from phonometry import PhonometryWarning
warnings.filterwarnings("error", category=PhonometryWarning)

La temperatura no tiene valor por defecto en absoluto. Es la única condición que quien llama ha medido de verdad, y no hay ningún valor defendible que inventarle.

La fracción de dióxido de carbono sí se pone por defecto, en silencio, a 0,000 4. Eso no es una suposición sobre el aire de nadie: el apartado F.2 la nombra como el valor a usar en condiciones de laboratorio a falta de una medida, así que la biblioteca está citando el anexo y no rellenando un hueco.

La Tabla F.1 imprime cinco magnitudes. El apartado F.6 da expresiones para dos más, y tres se siguen por identidad de las anteriores:

f.density # impresa en la Tabla F.1
f.speed_of_sound # impresa, a frecuencia cero
f.heat_capacity_ratio # impresa
f.viscosity # impresa
f.thermal_diffusivity # impresa
f.thermal_conductivity # expresión del apartado F.6
f.specific_heat_capacity # expresión del apartado F.6
f.characteristic_impedance # rho c
f.prandtl_number # eta / (rho alpha_t)
f.kinematic_viscosity # eta / rho

Una magnitud que este modelo no determinó lanza en vez de devolver un número que nadie imprimió. El error nombra el modelo, lo que sí determina y las dos salidas:

from phonometry.fluids import Fluid, FluidPropertyUnavailable
medido = Fluid(
temperature_c=10.0,
static_pressure_pa=1.1e6,
composition={"salinity_psu": 35.0},
model="a density and a sound speed, measured",
validity="",
properties={"density": 1027.0, "speed_of_sound": 1491.0},
)
try:
medido.heat_capacity_ratio
except FluidPropertyUnavailable as exc:
print(exc)
# 'a density and a sound speed, measured' does not determine
# 'heat_capacity_ratio'; it determines density, speed_of_sound. Supply the
# quantity yourself, or use a model that prints it.

Con un solo fluido en la biblioteca eso parece pedantería, y deja de parecerlo con dos: el agua de mar no tiene relación de calores específicos que dar, y un Fluid que se la inventara estaría entregando un número con la forma de una medida y nada de su sustancia.

Tres atributos no son magnitudes y nunca lanzan. Son el registro de dónde salieron los números:

f.properties # exactamente lo que este modelo determinó
f.model # 'IEC 61094-2:2009 Annex F (CIPM-2007)'
f.composition # la humedad y el CO2 con los que se calculó

El dominio impreso, y la diferencia entre un ajuste y un hecho

Sección titulada «El dominio impreso, y la diferencia entre un ajuste y un hecho»

El Anexo F enuncia, en su propia página, dónde se validaron sus ecuaciones: 15 °C a 27 °C, 60 kPa a 110 kPa y 10 % a 90 % de humedad relativa. No enuncia ningún rango para la fracción de dióxido de carbono.

Fuera de esa caja la biblioteca avisa y sigue respondiendo:

import warnings
from phonometry import fluids
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
hot = fluids.air(temperature_c=60.0, static_pressure_pa=101325.0,
relative_humidity_percent=50.0)
print(caught[0].category.__name__) # FluidWarning
print("temperature 60 degC (stated 15 to 27)" in str(caught[0].message)) # True
print(round(hot.speed_of_sound, 2)) # 371.52

La distinción importa y es la regla que sigue toda la biblioteca. El aire a 60 °C dentro de un conducto existe; un ajuste más allá de su rango validado sigue siendo aritmética, y rechazarlo sería rechazar una medida real. Lo que sí se rechaza es un estado que no puede darse, y esa es la lista completa: una temperatura igual o por debajo del cero absoluto, una presión no positiva, una humedad relativa fuera del 0 % al 100 %, una fracción molar de dióxido de carbono fuera de 0 a 1, y un valor no finito en cualquiera de las cuatro. Cada una lanza ValueError nombrando el argumento.

El oráculo es la Tabla F.1: cinco magnitudes en dos conjuntos de condiciones, diez cifras en total. La tolerancia no se elige. Un valor está enunciado con media unidad de la última posición hasta la que se imprimió, así que eso es lo que significa reproducirlo, y el test deriva esa cota de la cadena impresa en vez de llevar una constante que alguien escogió. Lo que fija la cota es esa posición, no un recuento de cifras significativas: 1,186 084 8 tiene ocho y 2,115 317 x 10^-5 siete, y la misma regla lee las dos:

La tolerancia, derivada de lo impreso
from decimal import Decimal
def half_of_the_last_printed_figure(printed: str) -> float:
"""Media unidad de la última posición que imprimió el anexo."""
exponent = int(Decimal(printed).as_tuple().exponent)
return float(Decimal(5) * Decimal(10) ** (exponent - 1))
half_of_the_last_printed_figure("1.1860848") # 5e-08
half_of_the_last_printed_figure("2.115317e-5") # 5e-12

Nadie puede aflojarla sin darse cuenta, y una edición futura que cite más cifras la aprieta sola.

Tres filas de conformidad registran cuánto de ese margen consume cada conjunto: un 55 % el primero, un 96 % el segundo y nada la identidad que ata la conductividad térmica y el calor específico a la difusividad impresa entre las dos. El 96 % es la densidad del segundo conjunto, y es el redondeo del propio anexo y no holgura de la biblioteca. Publicarlo como porcentaje significa que si algún cambio lo empuja fuera, se verá venir.

Pasar un Fluid a una función de medida es asunto de los paquetes que lo consumen, y viene con una regla que conviene enunciar aquí: donde una norma fija el aire de su propio procedimiento, usar un aire mejor es apartarse de la norma, y la biblioteca lo dice en vez de dejar que una medida deje de reproducir en silencio el documento que cita.

Por eso las fórmulas de aire simplificadas de ISO 10534-2, ASTM E2611 e ISO 17497-1 se quedan en sus módulos con sus cláusulas, y por eso el número de Prandtl 0,71 de Johnson-Champoux-Allard sigue siendo 0,71 aunque el aire en ese estado tenga 0,728.

Dos estados de ese aire se publican como constantes, porque no tiene sentido que quien llama los escriba dos veces. materials.ANNEX_A_AIR es el estado de referencia que fija el anexo A.3 de la ISO 9053-2:2020 para una medida de resistencia al flujo, 23 °C y 50 % de humedad relativa a una atmósfera estándar, evaluado con el mismo modelo CIPM-2007 que cualquier otro aire. simulation.SIMULATION_AIR es el medio por defecto del solver, y a propósito no es eso: 343 m/s y 1,2 kg/m³ planos, el par redondo con el que se monta una malla de diferencias finitas, llevado como Fluid para que una simulación diga en qué aire se hizo en lugar de dejar dos floats sueltos en una llamada.