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

Tus propios catálogos

Normas aplicables: RFC 8259RFC 6901RFC 4180EN 29052ISO 12354

La biblioteca publica las tablas de sus libros y de sus normas, y nada de lo que imprime un fabricante. Una hoja de características se revisa sin aviso bajo el mismo título, sus condiciones de uso rara vez permiten copiarla, y el valor que necesita un proyecto es el de la hoja con la que se especificó, no el de la revisión de hoy. Así que las hojas de características, las declaraciones de prestaciones, los informes de ensayo y las medidas tuyas viven en un archivo de catálogo tuyo, y phonometry.io lee ese archivo en filas de las mismas clases que entregan todos los catálogos PUBLISHED_*. El núcleo poroso de tu hoja entra en los modelos de porosos como entra un material poroso de un libro, un espectro de tu informe de ensayo entra en un modelo de sala, y todas las búsquedas por nombre buscan en tu catálogo, en el publicado o en los dos a la vez.

Las filas son tuyas y se quedan en tus objetos: la biblioteca no guarda ninguna copia, no abre ningún enlace que nombre un archivo y no envía nada a ninguna parte. Por la misma razón, una tabla de un fabricante pegada en una issue o en una pull request nunca se incorpora a la biblioteca.

La forma más rápida de ver cómo es un archivo es escribir uno. write_catalogue escribe cualquier tabla publicada en la forma que lee read_catalogue, como plantilla para copiar y editar: aquí la espuma de la Tabla 13.1 de Allard & Atalla.

import json
import pathlib
from phonometry import io, materials
allard = {
key: row
for key, row in materials.PUBLISHED_POROUS.items()
if row.table == "allard-2009-table-13-1"
}
written = io.write_catalogue(allard, "template.json", catalogue="template-allard")
print([path.name for path in written]) # ['template.json']
template = json.loads(pathlib.Path("template.json").read_text(encoding="utf-8"))
print(list(template))
# ['schema', 'schema_version', 'catalogue', 'row_type', 'about', 'provenance',
# 'phonometry_version', 'rows']
print(template["provenance"]["kind"]) # publication
print(template["rows"][0]["key"], template["rows"][0]["flow_resistivity_pa_s_m2"])
# foam 12569.0

Una tabla de filas de una misma clase sale entera, con el libro como procedencia y el día de la exportación como día de consulta. Lo que la biblioteca calcula a partir de las celdas impresas (aquí el módulo de cizalla, a partir del módulo de Young y del coeficiente de Poisson) no se escribe nunca: se vuelve a calcular al leer el archivo. Si ya hay un archivo en esa ruta, se rechaza con FileExistsError salvo que pases overwrite=True.

Un archivo de catálogo es un único objeto JSON, en UTF-8, con un conjunto cerrado de claves en el primer nivel:

ClaveQué guarda
schema, schema_version"phonometry-catalogue" y 1. Una versión posterior se rechaza con «upgrade phonometry».
catalogueEl nombre del catálogo, la primera mitad de cada clave: hasta 64 caracteres entre a-z, 0-9, ., _ y -.
row_typeEl nombre de la clase de fila. Se compara con la clase que pasas y nunca se importa.
aboutQué es el documento, cómo lo leíste y en qué unidades imprime.
provenanceEl documento: son obligatorios su tipo en kind, su título en document, su versión en version (o null si no imprime ninguna) y el día en que lo consultaste en consulted, como AAAA-MM-DD. El tipo es uno de io.PROVENANCE_KINDS: datasheet, declaration_of_performance, test_report, measurement, calculation, publication u other.
basisOpcional: qué dice el documento que es cada valor, salvo que una fila diga otra cosa.
attributed_toOpcional: a quién se atribuye la tabla entera, como un solo texto. Cada fila lo guarda como su crédito "table", y ninguna vuelve a atribuir la tabla.
conventionsOpcional: las notas y leyendas que el documento imprime para toda la tabla.
phonometry_versionOpcional: la versión de la biblioteca que escribió el archivo, que rellena write_catalogue. No se lee nunca.
$schemaOpcional: dónde encuentra un editor el esquema JSON del documento (véase Autocompletado en tu editor). No se lee nunca.
rowsLas filas, cada una con una clave propia en key y un nombre en name.

Aquí va una hoja de características ficticia con dos filas. La primera es la resistividad al flujo que la hoja declara bajo su marcado CE, como cota inferior; la segunda es la caracterización de laboratorio que cita la página 3, con su propio informe.

panel_40 = """{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "panel-40",
"row_type": "PorousMaterial",
"about": "Core of the Panel 40 absorber as its data sheet gives it. Page 2 declares the airflow resistivity class under CE marking; page 3 quotes the laboratory characterisation of a 40 mm specimen.",
"provenance": {
"kind": "datasheet",
"document": "Panel 40 technical data sheet",
"publisher": "Example Acoustics Ltd",
"version": "Rev. 4",
"issued": "2026-03",
"consulted": "2026-09-23",
"page": "2"
},
"basis": "declared",
"rows": [
{
"key": "core-declared",
"name": "Panel 40 core",
"variant": "as declared",
"thickness_mm": 40,
"frame_density_kg_m3": 40,
"ranges": {"flow_resistivity_kpa_s_m2": [5, null]},
"bounded_below": ["flow_resistivity_kpa_s_m2"],
"note": "the sheet prints the designation code AFr5"
},
{
"key": "core-lab",
"name": "Panel 40 core",
"variant": "40 mm specimen",
"provenance": {
"page": "3",
"printed_table": "Table 2",
"laboratory": "Example Lab",
"report": "26-014",
"test_date": "2025-11-04",
"test_standard": "ISO 9053-1:2018"
},
"basis": {"row": "measured", "poisson_ratio": "estimated"},
"flow_resistivity_pa_s_m2": 12500,
"uncertainty": {"flow_resistivity_pa_s_m2": 900},
"porosity": 0.97,
"tortuosity": 1.02,
"approximate": ["tortuosity"],
"viscous_length_um": 95,
"thermal_length_um": 190,
"frame_density_kg_m3": 40,
"thickness_mm": 40,
"youngs_modulus_pa": 140000,
"poisson_ratio": 0.0,
"unquantified": {"structural_loss_factor": "n.m."},
"x-product-code": "P40-C"
}
]
}"""
pathlib.Path("panel-40.json").write_text(panel_40, encoding="utf-8")
mine = io.read_catalogue("panel-40.json", row_type=materials.PorousMaterial)
print(list(mine)) # ['panel-40/core-declared', 'panel-40/core-lab']
declared = mine["panel-40/core-declared"]
lab = mine["panel-40/core-lab"]
print(lab.source)
# Panel 40 technical data sheet (Example Acoustics Ltd), Rev. 4, p. 3, Table 2;
# report 26-014 (Example Lab); consulted 2026-09-23
print(lab.basis_of("porosity"), lab.basis_of("poisson_ratio")) # measured estimated
print(declared.basis_of("frame_density_kg_m3")) # declared
print(declared.ranges, declared.converted)
# {'flow_resistivity_pa_s_m2': (5000.0, None)}
# {'flow_resistivity_pa_s_m2': ('5', 'kPa s/m2')}
print(mine.extras["panel-40/core-lab"]["x-product-code"]) # P40-C

Cada clave es "<catalogue>/<key>", la forma que usan todos los catálogos publicados, y el table de cada fila es el nombre del catálogo. El nombre de un catálogo no puede tener nunca la forma del de una tabla empaquetada, un año de cuatro cifras seguido de una palabra (allard-2009-table-13-1), así que una clave tuya y una clave de la biblioteca no coinciden nunca, ni hoy ni después de que una actualización añada una tabla: acme-2026-rev4 se rechaza y acme-rev4-2026 no.

Una fila puede acotar la procedencia del documento a su propia página, tabla, laboratorio, acreditación, informe, fecha de ensayo y norma, como hace core-lab, y una norma dada para un solo campo (field_test_standards) se suma a las del documento. Un documento distinto es otro archivo: una fila no puede cambiar el documento, su tipo ni su versión. Para todo tipo salvo publication, que se cita como se cita su documento, el source de la fila nombra el documento, su editor y su versión, la página y la tabla, el informe y el laboratorio, y el día de la consulta; la acreditación, la fecha de ensayo y las normas se quedan en row.provenance. Un rechazo nombra el documento por su tipo: «the datasheet», «the declaration of performance», «the test report», «the measurement record» para measurement, «the calculation note» para calculation, y «the source» para publication u other.

Las columnas propias van bajo un nombre que empieza por x-: se guardan, como texto, en Catalogue.extras y no intervienen en ningún cálculo. Una fila nunca escribe derived, table, source ni estimated: las tres primeras las escribe la biblioteca, y una estimación es una basis.

Todos los problemas de forma de un archivo se comunican a la vez, en un solo io.CatalogueError, cada uno un io.CatalogueIssue con el archivo, su lugar como puntero JSON, la fila y el campo. Aquí la porosidad está escrita con coma decimal y el espesor en pulgadas, una unidad que el lector no convierte:

broken = panel_40.replace('"porosity": 0.97', '"porosity": "0,97"')
broken = broken.replace('"thickness_mm": 40,\n "youngs', '"thickness_inch": 1.6,\n "youngs')
try:
io.parse_catalogue(broken, row_type=materials.PorousMaterial, label="panel-40.json")
except io.CatalogueError as error:
for issue in error.issues:
print(issue)
# panel-40.json: /rows/1/porosity (row 'core-lab'): expected a number, got the
# text '0,97'; JSON numbers use a decimal point
# panel-40.json: /rows/1/thickness_inch (row 'core-lab'): 'inch' is not a unit
# this reader converts; write thickness_mm (mm), thickness_cm (cm),
# thickness_m (m) or thickness_um (µm)

parse_catalogue lee lo mismo de un texto o de un diccionario que ya está en memoria, y label nombra el documento en cada problema. Un campo mal escrito se contesta con el campo al que más se parece; una fila bien escrita que incumple el contrato que cumple toda fila (una densidad negativa, una porosidad mayor que 1, un valor junto a una palabra que dice que no lo hay) se comunica con la primera regla que incumple. El lector rechaza un NaN o un Infinity, un nombre escrito dos veces en un mismo objeto, un carácter de control o una marca que reordena el texto, cualquier cosa que no sea un archivo regular con el nombre de un catálogo o de su cabecera (una tubería, un dispositivo, un socket o un directorio, rechazados antes de abrirlos), un archivo de más de 16 MiB (comprobado antes de leer un solo byte), una cabecera de archivo CSV de más de 64 KiB, más de 50 000 filas y un anidamiento de más de seis niveles.

Lo que solo merece una segunda mirada no es un error. Un valor cuya base es measured sin informe ni laboratorio que lo respalden, o una palabra en lugar de un número tan larga que es una frase, se guarda tal como lo escribe el archivo, se anota en Catalogue.notes y se anuncia una sola vez con un io.CatalogueWarning. Una nota nunca cambia una fila.

Una fila leída de un archivo es de la misma clase, y se construye de la misma manera, que una publicada: la aceptan los mismos métodos, y lo que devuelven lo aceptan las mismas funciones.

printed() da un número que imprime el documento, o lo rechaza con lo que el documento tiene en su lugar. Es la manera de pasar una celda a cualquier función que reciba un float. PorousMaterial.medium() lee sus cinco parámetros del mismo modo, así que la fila de laboratorio entra en el modelo de matriz de transferencia y la fila declarada se rechaza en los términos de la propia hoja:

import numpy as np
print(lab.printed("flow_resistivity_pa_s_m2")) # 12500.0
frequencies = np.array([250.0, 500.0, 1000.0, 2000.0])
core = materials.PorousLayer(thickness=0.040, medium=lab.medium(frequencies))
print(materials.layered_absorber(frequencies, [core]).absorption.round(2))
# [0.15 0.4 0.73 0.98]
try:
declared.medium(frequencies)
except ValueError as error:
print(error)
# 'Panel 40 core' has no flow_resistivity_pa_s_m2, which 'johnson_champoux_allard'
# needs: the datasheet prints a lower bound of 5 kPa s/m2 (5000 Pa s/m2) and no
# value (Panel 40 technical data sheet (Example Acoustics Ltd), Rev. 4, p. 2;
# consulted 2026-09-23).

El rechazo cita primero la cifra y la unidad que imprime la hoja, y después el valor que guarda la fila: una cota inferior AFr5 no es una resistividad al flujo de 5000 Pa·s/m², y aquí nada finge que lo sea.

Una fila que imprime un valor por banda se lee en las frecuencias que pide un modelo con values_at(), que rechaza una banda que el documento no imprime en lugar de leerla como cero. Las funciones que reciben sus coeficientes por posición, como room.sabine_reverberation_time, reciben un array en el que cada valor está impreso:

from phonometry import room
tiles = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "ceiling-tiles",
"row_type": "AbsorptionSpectrum",
"about": "Octave-band Sabine coefficients a fictitious data sheet prints.",
"provenance": {
"kind": "datasheet",
"document": "Example tile data sheet",
"publisher": "Example Acoustics Ltd",
"version": "Rev. 1",
"consulted": "2026-09-25",
"laboratory": "Example Lab",
"report": "26-031",
"test_standard": "ASTM C423-17",
},
"basis": "measured",
"rows": [
{
"key": "tile-e400",
"name": "Example ceiling tile",
"mounting": "E400",
"absorption_coefficient_125": 0.45,
"absorption_coefficient_250": 0.62,
"absorption_coefficient_500": 0.78,
"absorption_coefficient_1000": 0.90,
"absorption_coefficient_2000": 0.94,
"absorption_coefficient_4000": 0.91,
}
],
},
row_type=materials.AbsorptionSpectrum,
)
tile = tiles["ceiling-tiles/tile-e400"]
bands = np.array([125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0])
t60 = room.sabine_reverberation_time(180.0, [(60.0, tile.values_at(bands)), (190.0, 0.04)])
print(t60.round(2)) # [0.84 0.65 0.53 0.47 0.45 0.47]
try:
tile.values_at([63.0, 125.0])
except ValueError as error:
print(error)
# 'Example ceiling tile' has no absorption_coefficient_63, which 'the 63 Hz band'
# needs: the datasheet does not give it, and it does not follow from the cells
# that it does (Example tile data sheet (Example Acoustics Ltd), Rev. 1;
# report 26-031 (Example Lab); consulted 2026-09-25).

building.plateau_transmission_loss admite una fila PlateauMaterial como material, una del libro o una tuya, y lee sus tres números a través de printed(). Un número que pases explícitamente sustituye al de la fila, que entonces ni siquiera se lee.

from phonometry import building, solids
boards = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "boards",
"row_type": "PlateauMaterial",
"about": "Plateau-method constants of a fictitious board, as its data sheet prints them.",
"provenance": {
"kind": "datasheet",
"document": "Example board data sheet",
"version": "Rev. 2",
"consulted": "2026-09-25",
},
"rows": [
{
"key": "board",
"name": "Example board",
"surface_density_per_mm_kg_m2": 0.9,
"coincidence_height_db": 28,
"plateau_frequency_ratio": 7,
}
],
},
row_type=solids.PlateauMaterial,
)
board = boards["boards/board"]
tl = building.plateau_transmission_loss(bands, material=board, thickness_mm=12.5)
print(round(tl.plateau_start), round(tl.plateau_end)) # 522 3656
print(tl.transmission_loss.round(1)) # [15.6 21.6 27.6 28. 28. 29.3]

La hoja de características de una capa resiliente imprime una de dos magnitudes distintas. La rigidez dinámica de la capa instalada es la que usa la frecuencia natural de un suelo flotante; la rigidez dinámica aparente es la que mide la EN 29052-1 sobre una probeta, y solo se convierte en a través de la resistividad al flujo lateral del apartado 8.2. ResilientLayer las guarda en dos campos, dynamic_stiffness_n_m3 y apparent_dynamic_stiffness_n_m3, y un «s’ ≤ 9 MN/m³» declarado es una cota, no una rigidez:

layers = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "underlays",
"row_type": "ResilientLayer",
"about": "Two underlays of a fictitious data sheet, as it prints them.",
"provenance": {
"kind": "datasheet",
"document": "Example underlay data sheet",
"publisher": "Example Acoustics Ltd",
"version": None,
"consulted": "2026-09-25",
},
"basis": "declared",
"rows": [
{
"key": "declared",
"name": "Underlay 8",
"thickness_mm": 8,
"ranges": {"dynamic_stiffness_mn_m3": [None, 9]},
"bounded_above": ["dynamic_stiffness_mn_m3"],
},
{
"key": "tested",
"name": "Underlay 20",
"thickness_mm": 20,
"apparent_dynamic_stiffness_mn_m3": 7,
"basis": {"row": "measured"},
"provenance": {"laboratory": "Example Lab", "report": "26-044"},
},
],
},
row_type=materials.ResilientLayer,
)
try:
layers["underlays/declared"].natural_frequency(mass_per_area_kg_m2=100.0)
except ValueError as error:
print(error)
# 'Underlay 8' has no dynamic_stiffness_n_m3, which 'the natural frequency of
# EN 29052-1 Formula 2' needs: the datasheet prints an upper bound of 9 MN/m3
# (9000000 N/m3) and no value (Example underlay data sheet (Example Acoustics
# Ltd), no version printed; consulted 2026-09-25).
tested = layers["underlays/tested"]
f0 = tested.natural_frequency(mass_per_area_kg_m2=100.0, airflow_resistivity_pa_s_m2=150_000.0)
print(round(float(f0), 1)) # 42.1

La capa ensayada solo da , así que su frecuencia natural necesita la resistividad al flujo lateral del apartado 8.2: a partir de 100 kPa·s/m², es , y por debajo la llamada necesita además la rigidez del gas encerrado de la Fórmula 7, que se pasa con gas_stiffness_n_m3. Sin la llamada se rechaza con esa explicación en lugar de tomar una magnitud por la otra.

La simulación elástica acepta la fila de un sólido allí donde acepta un Material: simulation.scholte_speed y ElasticFDTD2D.from_regions leen los tres números que integra el FDTD elástico: la velocidad longitudinal de un sólido extenso, la velocidad transversal y la densidad. Una hoja que imprime un módulo, un coeficiente de Poisson y una densidad ha impreso también esas velocidades, y la fila las guarda marcadas como derivadas. Las velocidades de placa y de barra que una página imprime a su lado no se leen nunca: el FDTD elástico no integra ninguna de las dos, y una velocidad de placa tomada por la de un sólido extenso se equivoca en torno a un diez por ciento.

from phonometry import simulation, solids
alloys = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "alloys",
"row_type": "SolidMaterial",
"about": "Elastic constants of two fictitious alloys, as their data sheet prints them.",
"provenance": {
"kind": "datasheet",
"document": "Example alloy data sheet",
"version": "Rev. 1",
"consulted": "2026-09-25",
},
"rows": [
{
"key": "plate",
"name": "Example alloy",
"youngs_modulus_gpa": 70,
"poisson_ratio": 0.33,
"density_kg_m3": 2700,
},
{
"key": "sheet",
"name": "Example sheet",
"plate_longitudinal_speed_m_s": 5400,
"density_kg_m3": 2700,
},
],
},
row_type=solids.SolidMaterial,
)
alloy = alloys["alloys/plate"]
print(round(alloy.bulk_longitudinal_speed_m_s), round(alloy.transverse_speed_m_s))
# 6198 3122
print(round(simulation.scholte_speed(simulation.WATER, alloy), 1)) # 1476.6
try:
simulation.scholte_speed(simulation.WATER, alloys["alloys/sheet"])
except ValueError as error:
print(error)
# 'Example sheet' has no bulk_longitudinal_speed_m_s, which 'the elastic FDTD'
# needs: the datasheet does not give it, and it does not follow from the cells
# that it does (Example alloy data sheet, Rev. 1; consulted 2026-09-25).

Una fila GroundSurface, de los suelos publicados o de los tuyos, entra en los modelos de exteriores a través de medium(): el semiespacio poroso, por el modelo de Delany y Bazley o por el de Miki, que environment.ground_effect recibe como impedance, barrier_insertion_loss como ground_impedance y atmospheric_parabolic_equation como impedance. La resistividad se lee a través de printed(), así que el intervalo de un pastizal se rechaza en lugar de promediarse. Una resistividad al flujo efectiva es un parámetro del modelo con el que se ajustó, así que una fila que la página marca como ajustada con un modelo solo entra en ese modelo:

from phonometry import environment
site = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "site-survey",
"row_type": "GroundSurface",
"about": "An effective flow resistivity fitted to a site's own level differences.",
"provenance": {
"kind": "measurement",
"document": "Site survey 12",
"version": None,
"consulted": "2026-09-25",
"laboratory": "Example Lab",
"report": "26-051",
},
"basis": "measured",
"rows": [{"key": "pasture", "name": "Pasture", "flow_resistivity_kpa_s_m2": 180}],
},
row_type=environment.GroundSurface,
)
pasture = site["site-survey/pasture"]
octaves = np.array([125.0, 250.0, 500.0, 1000.0])
ground = pasture.medium(octaves, model="miki")
over_pasture = environment.ground_effect(octaves, 1.0, 1.5, 50.0, impedance=ground)
print(over_pasture.excess_attenuation.round(1)) # [ 3.6 -4.2 -12.8 -2.8]
fitted = environment.PUBLISHED_GROUND["cox-2017-table-6-7/sports_field_semi_phenomenological"]
try:
fitted.medium(octaves)
except ValueError as error:
print(error)
# 'Sports field' is the row the page marks “Fitted using semi-phenomenological
# model” (Cox & D'Antonio 3e Table 6.7, PDF pages 257-258 (printed pp.
# 200-201)): its effective flow resistivity is a parameter of that model and
# not of 'delany_bazley'; neither 'delany_bazley' nor 'miki' is that model, or
# take row.printed('flow_resistivity_pa_s_m2') into another model yourself.

Un suelo exterior es mucho más resistivo que los materiales fibrosos con los que se ajustaron los dos modelos, así que todas las octavas de aquí quedan por debajo de su rango de ajuste publicado, y la llamada emite un PorousAbsorberWarning que dice que la regresión es ahí una extrapolación; Elección de la impedancia del suelo explica lo que eso cuesta y por qué Miki es el modelo que conviene preferir por debajo del rango.

Una pared en el modelo detallado de edificación

Sección titulada «Una pared en el modelo detallado de edificación»

building.HomogeneousElement.from_solid construye el elemento de la ISO 12354-1 que forma una capa de un sólido, a partir de la densidad y la velocidad de placa de la fila, que es la velocidad de fase cuasilongitudinal de la Tabla B.3 de la norma, y del espesor que le das: y . El factor de pérdidas interno lo das tú y nunca se lee de la fila, que puede guardar cuatro (a flexión, longitudinal, in situ y uno que la página no precisa), porque solo tú sabes cuál de ellos, si alguno, es el de la norma:

from phonometry import building
blocks = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "blocks",
"row_type": "SolidMaterial",
"about": "A fictitious calcium-silicate block, as its data sheet prints it.",
"provenance": {
"kind": "datasheet",
"document": "Example block data sheet",
"version": "Rev. 3",
"consulted": "2026-09-25",
},
"basis": "declared",
"rows": [
{
"key": "block",
"name": "Example block",
"density_kg_m3": 1800,
"plate_longitudinal_speed_m_s": 2500,
"flexural_loss_factor": 0.012,
}
],
},
row_type=solids.SolidMaterial,
)
wall = building.HomogeneousElement.from_solid(
blocks["blocks/block"],
thickness_m=0.2,
internal_loss_factor=0.01,
area_m2=13.75,
length1_m=5.0,
length2_m=2.75,
)
print(round(wall.mass_per_area, 1), round(wall.critical_frequency, 1)) # 360.0 128.4
print(wall.internal_loss_factor) # 0.01, no el 0.012 a flexión de la fila

Son los números de las paredes interiores del edificio resuelto del Anexo L de la ISO 12354-1, 200 mm de bloques de silicato cálcico con 360 kg/m² y 128,4 Hz, y el elemento entra en building.in_situ_element como uno construido a mano.

Todas las búsquedas *_named admiten catalogue=, las filas en las que buscar en lugar de su propio PUBLISHED_*. PUBLISHED_POROUS | mine une los dos en un único mapping de solo lectura que rechaza una clave que tengan los dos, así que una sola búsqueda contesta desde ambos:

both = materials.PUBLISHED_POROUS | mine
for row in materials.porous_materials_named("panel 40 core", catalogue=both):
print(row.table, row.variant)
# panel-40 as declared
# panel-40 40 mm specimen
print(len(materials.porous_materials_named("Foam", catalogue=both))) # 5

Cada búsqueda conserva su propia forma de casar un nombre, contesta con todas las coincidencias en el orden en que las guarda el catálogo y nunca elige una. Casa sin distinguir mayúsculas ni la forma en que se guarda una letra acentuada: un nombre pegado de un documento que guarda «é» como «e» seguida de un acento combinante encuentra la fila escrita con «é», y al revés. Una fila de otra clase dentro del catálogo se rechaza con TypeError, que nombra su clave.

Un documento imprime más que números, y un archivo escribe cada cosa como lo que es, con los mismos matices que usan las tablas publicadas. El sujeto de cada frase de abajo es el tipo de documento: «the datasheet», «the test report».

El documento imprimeSe escribe en el archivo comowhy_missing contesta
un númeroel campo, en su unidad o en otra de la misma magnitudnada: el valor está
nadael campo omitido, o null«the datasheet does not give it, and it does not follow from the cells that it does»
~0,85el valor, y el campo en "approximate"nada
≤ 30 o < 30"ranges": {"f": [null, 30]} y "bounded_above": ["f"]«the datasheet prints an upper bound of 30 and no value»
≥ 5 o > 5"ranges": {"f": [5, null]} y "bounded_below": ["f"]«… a lower bound of 5 and no value»
0,30 a 0,50"ranges": {"f": [0.30, 0.50]}«the datasheet prints 0.3 to 0.5 and no value»
varias lecturas y ningún valor único"reported": {"f": [0.30, 0.35, [0.30, 0.40]]}«the datasheet lists 0.3, 0.35, 0.3 to 0.4 and no single value»
0,85 ± 0,05el valor, y "uncertainty": {"f": 0.05}nada
una palabra o un código donde iría el número"unquantified": {"f": "AFr5"}«the datasheet prints “AFr5” where the number would be»
un valor que sabes que está mal"misprinted": {"f": "por qué"}, sin valortu motivo, tal como lo escribiste
un valor al que llega la aritmética pero que no debe calcularse"not_derivable": {"f": "por qué"}tu motivo
una celda en blanco que el documento da por referencia a otra filael valor, y "carried": {"f": "de la fila de arriba"}nada
una cifra en una unidad que no está en ninguna familiael valor en la unidad del propio campo, y "converted": {"f": ["3e5", "psi"]}nada, y una cota cita primero la cifra
el crédito a un laboratorio o a un autor"attributed_to": {"f": "..."}, o "row", o "table"; o el de la tabla una sola vez, como attributed_to del propio documentono se pregunta
un sí o un notrue o false, en un campo que guarde unono se pregunta

Una cota declarada bajo un marcado CE, AFr5 para una resistividad al flujo de al menos 5 kPa·s/m², se escribe como la cota, con el código en note, como hace core-declared. Escrita como el número 5, afirmaría una medida que la hoja no hace.

Una celda puede escribirse en la unidad que lleva el nombre de su campo, o en otra unidad de la misma magnitud, con el mismo nombre y el sufijo de la otra unidad: thickness_cm en lugar de thickness_mm. La cifra se convierte sobre los dígitos que escribe el archivo, con el factor exacto, y se redondea una sola vez, así que 0,067 GPa son 67 000 000 Pa, cosa que no da el producto de dos flotantes. converted guarda la cifra y la unidad exactamente como las escribe el archivo.

MagnitudSufijos
Longitud_um, _mm, _cm, _m
Masa por unidad de superficie_g_m2, _kg_m2
Densidad_g_cm3, _kg_m3
Presión y módulo_pa, _kpa, _mpa (megapascales), _n_mm2, _gpa
Rigidez por unidad de superficie_n_m3, _mn_m3
Resistividad al flujo_pa_s_m2, _kpa_s_m2
Temperatura_c, _k, desplazada por su origen en un valor, un intervalo y una lectura, y solo por el factor en un más-menos
Área de absorción_m2, _ft2, _ft2_per_1000_ft3, solo en AbsorptionAreaSpectrum, y la última solo con per al lado

La unidad se reconoce por la coincidencia más larga con todas las unidades que usan los campos de la biblioteca, así que una unidad compuesta nunca se toma por una longitud: una resistencia específica al flujo en Pa·s/m o una tasa por metro no admiten otra unidad, y un nombre que podrían tomar dos campos de la misma magnitud se rechaza por ambiguo. Una unidad que no está en la tabla se rechaza con las grafías que admite el campo. Todos los números de una misma celda se escriben en una sola unidad.

Una hoja de características se teclea más a menudo en una hoja de cálculo que en un archivo JSON, y una hoja de cálculo guarda CSV. Un catálogo puede ser uno: las filas van en el archivo CSV, cada una empezando en una línea propia bajo una primera línea que nombra las columnas, y el documento sin sus filas va en una cabecera JSON al lado, con el nombre del archivo CSV seguido de .phonometry.json. La cabecera declara cómo escribe el archivo sus celdas, "csv": {"delimiter": ";", "decimal": ","} para una hoja de cálculo configurada con coma decimal, y nada del dialecto se adivina a partir del archivo. write_catalogue escribe el par cuando el nombre termina en .csv, así que una tabla publicada vuelve a ser la plantilla más rápida, aquí los coeficientes de absorción de la Tabla 6.1 de Arau-Puchades (1999):

arau = {
key: row
for key, row in materials.PUBLISHED_ABSORPTION.items()
if row.table == "arau-1999-table-6-1"
}
written = io.write_catalogue(
arau, "template.csv", catalogue="template-arau", delimiter=";", decimal=","
)
print([path.name for path in written])
# ['template.csv', 'template.csv.phonometry.json']
lines = pathlib.Path("template.csv").read_text(encoding="utf-8-sig").splitlines()
print(lines[0].split(";")[:5])
# ['key', 'name', 'variant', 'note', 'absorption_coefficient_125']
print(lines[1].split(";")[:5])
# ['01_pared_de_ladrillo', 'Pared de ladrillo', '', '', '0,025']
header = json.loads(pathlib.Path("template.csv.phonometry.json").read_text("utf-8"))
print(header["csv"]) # {'delimiter': ';', 'decimal': ','}

Abre el archivo en una hoja de cálculo, edítalo y guárdalo como «CSV UTF-8». El lector admite UTF-8 con o sin la marca de orden de bytes que escribe ahí la hoja de cálculo, y rechaza cualquier otra codificación con ese consejo. Conserva la cabecera al lado del archivo, con el delimitador y la marca decimal con los que guardó la hoja de cálculo. El escritor pone un apóstrofo delante de un texto que una hoja de cálculo ejecutaría como fórmula (el que empieza por =, +, -, @, un tabulador o un retorno de carro), y el lector lo vuelve a quitar. Un archivo CSV y su cabecera son dos archivos: cada uno se escribe entero, pero no los dos como uno solo.

Cada celda numérica guarda una sola cosa, con una gramática cerrada que escribe los mismos matices que el documento JSON:

La celdaLo que dice
vacíael documento no imprime nada ahí
0,85un valor, con la marca decimal que declara la cabecera
~0,85un valor aproximado
<=30, <30 o ≤30una cota superior y ningún valor
>=5, >5 o ≥5una cota inferior y ningún valor
0,30..0,50un intervalo y ningún valor; ~0,30..0,50 si es aproximado
0,85±0,05 o 0,85+/-0,05un valor y su más-menos
[AFr5]lo que imprime el documento donde iría el número
true o falseun sí o un no, en una columna que guarde uno

Un número nunca lleva separador de miles, y su signo menos puede ser el tipográfico. Las columnas son key, los campos de la clase de fila (en la unidad que lleva el nombre de cada uno, o en otra de la misma magnitud, como flow_resistivity_kpa_s_m2), basis para la fila entera (una celda vacía toma la del documento), la página, la tabla, el laboratorio, la acreditación, el informe, la fecha de ensayo y la norma de la fila como provenance.page, provenance.printed_table, provenance.laboratory, provenance.accreditation, provenance.report, provenance.test_date y provenance.test_standard, el crédito de la fila entera como attributed_to.row, y columnas propias con nombre x-.... El crédito de la tabla entera va una sola vez en la cabecera, como "attributed_to": "...", igual que en un documento JSON. Una gama ficticia de placas tecleada en una hoja:

header = {
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "tile-range",
"row_type": "AbsorptionSpectrum",
"about": "Octave-band Sabine coefficients of a fictitious tile range, typed from its data sheet.",
"provenance": {
"kind": "datasheet",
"document": "Example tile range data sheet",
"publisher": "Example Acoustics Ltd",
"version": "Rev. 2",
"consulted": "2026-09-25",
"laboratory": "Example Lab",
},
"basis": "measured",
"csv": {"delimiter": ";", "decimal": ","},
}
pathlib.Path("tiles.csv.phonometry.json").write_text(json.dumps(header), encoding="utf-8")
pathlib.Path("tiles.csv").write_text(
"key;name;mounting;absorption_coefficient_125;absorption_coefficient_250;"
"absorption_coefficient_500;absorption_coefficient_1000;"
"absorption_coefficient_2000;absorption_coefficient_4000;provenance.report;x-code\n"
"e400;Tile 15;E400;0,45;~0,62;0,78±0,03;0,90;0,94;0,91;26-031;T15\n"
"a;Tile 15;A;[n.m.];0,25..0,30;0,55;0,80;>=0,85;0,80;26-032;T15\n",
encoding="utf-8",
)
sheet = io.read_catalogue("tiles.csv", row_type=materials.AbsorptionSpectrum)
e400, mounted_a = sheet["tile-range/e400"], sheet["tile-range/a"]
print(e400.absorption_coefficient_1000, e400.approximate, e400.uncertainty)
# 0.9 frozenset({'absorption_coefficient_250'}) {'absorption_coefficient_500': 0.03}
print(mounted_a.ranges)
# {'absorption_coefficient_250': (0.25, 0.3), 'absorption_coefficient_2000': (0.85, None)}
print(mounted_a.why_missing("absorption_coefficient_2000"))
# the datasheet prints a lower bound of 0.85 and no value
print(mounted_a.source)
# Example tile range data sheet (Example Acoustics Ltd), Rev. 2; report 26-032
# (Example Lab); consulted 2026-09-25
print(sheet.extras["tile-range/a"]["x-code"]) # T15

Las filas pasan las mismas comprobaciones que las de un documento JSON, y todos los problemas se comunican a la vez en su línea y su columna, con las letras con que las nombra una hoja de cálculo. Una columna que el lector no conoce se contesta con el nombre más parecido, y un texto donde va un número nunca se lee como una palabra, un NaN o un cero:

pathlib.Path("tiles.csv").write_text(
"key;name;absorption_coefficent_500;absorption_coefficient_1000\n"
"e400;Tile 15;0,78;0.^G\n",
encoding="utf-8",
)
try:
io.read_catalogue("tiles.csv", row_type=materials.AbsorptionSpectrum)
except io.CatalogueError as error:
for issue in error.issues:
print(issue)
# tiles.csv, line 1, column C: no field 'absorption_coefficent_500' on
# AbsorptionSpectrum; did you mean 'absorption_coefficient_500'?
# tiles.csv, line 2, column D (absorption_coefficient_1000), row 'e400': '0.^G'
# is not a number; if the datasheet prints this text where the number would
# be, write it as [0.^G]

0.^G es lo que puede dejar la capa de texto rota de un PDF: si la página imprime ahí una palabra, va entre corchetes; si imprime un número, teclea el número. Un intervalo tecleado con un guion, un más-menos tecleado como +-, una unidad tecleada detrás del número y un número agrupado en miles reciben cada uno la forma de escribirlos. Cuando todos los números que fallan están escritos con la otra marca decimal (un número agrupado en miles, que podría ser cualquiera de las dos, no cuenta), o la primera línea se parte en columnas con otro delimitador, el rechazo dice además cuál hay que declarar.

El crédito también viaja en el archivo: write_catalogue escribe el crédito de una fila en su columna attributed_to.row, y el que todas las filas dan a la tabla entera en la cabecera. La Tabla 6.2 de Bies atribuye su primera fila a Beranek e Hidaka (1998):

bies = {
key: row
for key, row in materials.PUBLISHED_ABSORPTION.items()
if row.table == "bies-2017-table-6-2"
}
io.write_catalogue(bies, "bies.csv", catalogue="bies")
lines = pathlib.Path("bies.csv").read_text(encoding="utf-8-sig").splitlines()
print(lines[0].split(",")[-1]) # attributed_to.row
back = io.read_catalogue("bies.csv", row_type=materials.AbsorptionSpectrum)
print(back["bies/unoccupied_heavily_upholstered_seats"].attributed_to)
# {'row': 'Beranek and Hidaka (1998)'}

Lo que no cabe en una celda se escribe en un documento JSON: varias lecturas sin un valor único, una errata, un valor al que llega la aritmética pero que no debe calcularse, una celda arrastrada de otra fila, una cifra en una unidad que no está en ninguna familia, un crédito, una base o una norma para una sola celda, una cota o un intervalo con más-menos, una cota aproximada, un texto vacío donde el valor por defecto del campo dice algo y una tabla cuyas filas la atribuyen por separado. write_catalogue se niega a escribir una tabla así como CSV, nombra cada celda que no puede guardar con el puntero con que la escribiría el documento JSON y no escribe nada. La Tabla A2 de Hopkins imprime la fuente de tres celdas de su fila del acero dentro de la propia celda, como «5270 (Fahy, 1985)» para la velocidad de placa: un crédito para una sola celda, así que esa fila solo va a JSON:

from phonometry import solids
steel = "hopkins-2007-table-a2/steel"
try:
io.write_catalogue(
{steel: solids.PUBLISHED_SOLIDS[steel]}, "steel.csv", catalogue="hopkins"
)
except io.CatalogueError as error:
print(error.issues[0].location)
# /rows/0/attributed_to/plate_longitudinal_speed_m_s
print(pathlib.Path("steel.csv").exists()) # False

Una hoja de características y un libro pueden imprimir magnitudes que se parecen y no son la misma. Las clases de fila las separan, y el archivo no puede impedir que escribas una en el campo de otra, así que lee esto antes de elegir un row_type.

  • AbsorptionSpectrum guarda coeficientes de absorción de Sabine por octavas de 63 Hz a 8 kHz, tal como los imprime la tabla de un libro o tal como una hoja de características imprime por octavas el resultado de un ensayo en cámara reverberante. El coeficiente práctico de absorción sonora de la ISO 11654 no es uno de ellos: es una valoración, redondeada a pasos de 0,05 y limitada a 1,00, y la fila por octavas de una hoja de características europea suele ser eso. PracticalAbsorptionSpectrum guarda y ThirdOctaveAbsorptionSpectrum el por tercios de octava de un informe de ensayo, cada uno con la valoración que imprime la hoja; no escribas ninguno de los dos en AbsorptionSpectrum. Cómo leer la hoja de características de un material distingue los tres en la página.
  • TransmissionLossSpectrum guarda la pérdida por transmisión de una construcción tal como la recopila un libro, por bandas de octava, a partir de ensayos que el libro rara vez nombra: Bies califica sus valores de incidencia de campo, las filas de ASHRAE salen de ensayos de laboratorio y Rossing no nombra ninguna fuente. El índice de reducción acústica de un informe de ensayo según la ISO 10140-2 se mide por tercios de octava sobre una probeta de un producto, en un laboratorio donde se ha comprobado que el sonido transmitido por flancos es despreciable, y se valora con la ISO 717-1. Es otro registro: lo guarda SoundReductionSpectrum, con los índices globales que imprime el informe, e ImpactImprovementSpectrum guarda la reducción del nivel de impactos de un revestimiento; no escribas ninguno de los dos en TransmissionLossSpectrum.
  • ResilientLayer guarda en dynamic_stiffness_n_m3 y en apparent_dynamic_stiffness_n_m3. Un informe de ensayo según la EN 29052-1 imprime y, cuando puede, ; una hoja de características a menudo solo imprime «s’». Escribe cada una en su campo.
  • PorousMaterial guarda los parámetros de un esqueleto poroso, PlateauMaterial los tres números del método de la meseta y SolidMaterial las constantes elásticas de un sólido. La página de catálogos publicados muestra cada clase con sus columnas.

Qué es un valor: basis, converted y carried

Sección titulada «Qué es un valor: basis, converted y carried»

basis dice qué afirma el documento que es un valor, uno de io.CATALOGUE_BASES, y basis_of(field) lo contesta para cualquier celda:

PalabraSignificado
measuredun resultado de ensayo que da el documento
declaredun valor, una cota o una clase declarados bajo una norma de producto o un marcado CE
calculateduna cifra que calculó el propio documento, con el modelo de una norma, un programa o una fórmula
estimateduna estimación del propio documento
extendedel resultado de una aplicación extendida de un ensayo

Una celda sin palabra es una que el documento no califica, que es la respuesta honesta en la mayoría de las hojas de características. La basis del documento es la de todas sus filas, la entrada "row" de una fila es la de todas sus celdas, y la entrada propia de una celda manda sobre las dos.

basis es lo que dice el documento; derived es lo que calculó esta biblioteca, a partir de las celdas impresas, al construir la fila. Son independientes: el módulo de cizalla de core-lab es derivado, y su texto en derived nombra las bases en las que se apoya, un módulo de Young medido y un coeficiente de Poisson estimado. converted guarda la cifra y la unidad de un valor convertido desde otra unidad, y carried dice de dónde da el documento una celda en blanco. Un archivo puede escribir converted y carried junto a un valor; derived, nunca.

Una fila es inmutable. Para cambiar una celda, toma las celdas que imprime el documento con printed_fields(), cambia una y vuelve a construir la fila con from_printed, que vuelve a calcular lo que se sigue de ellas. dataclasses.replace copia los valores derivados tal como estaban, junto a una celda de la que ya no se siguen:

cells = lab.printed_fields()
cells["youngs_modulus_pa"] = 200_000.0
edited = materials.PorousMaterial.from_printed(**cells)
print(lab.shear_modulus_pa, edited.shear_modulus_pa) # 70000.0 100000.0

Una magnitud que no guarda ninguna clase de la biblioteca puede vivir igual en un archivo de catálogo, en una clase de fila propia: una dataclass inmutable y keyword-only construida sobre io.CatalogueRow, con cada campo anotado como float | None, int | None, bool, str, frozenset[str] o un Mapping de ellos. El lector la lee como cualquier otra, y las demás unidades de una magnitud se aplican también a tus campos cuando el nombre no deja lugar a dudas, salvo una temperatura, que se escribe en la unidad que nombra su campo:

import dataclasses
@dataclasses.dataclass(frozen=True, kw_only=True)
class Plasterboard(io.CatalogueRow):
surface_density_kg_m2: float | None = None
reaction_to_fire: str = ""
boards_of_mine = io.parse_catalogue(
{
"schema": "phonometry-catalogue",
"schema_version": 1,
"catalogue": "plasterboards",
"row_type": "Plasterboard",
"about": "One board of a fictitious data sheet.",
"provenance": {
"kind": "datasheet",
"document": "Example board range",
"version": None,
"consulted": "2026-09-25",
},
"rows": [
{
"key": "board-12",
"name": "Board 12.5",
"surface_density_g_m2": 8600,
"reaction_to_fire": "A2-s1,d0",
}
],
},
row_type=Plasterboard,
)
row = boards_of_mine["plasterboards/board-12"]
print(row.surface_density_kg_m2, row.converted)
# 8.6 {'surface_density_kg_m2': ('8600', 'g/m2')}

No pongas slots=True en una clase así: en Python 3.12, y en las versiones de 3.13 anteriores a la corrección, falla un método suyo que llame a super() sin argumentos, que es como un __post_init__ tuyo llega al contrato de fila. Una anotación float o int a secas se rechaza con TypeError la primera vez que se construye la clase, porque cualquier celda de una fila puede faltar. Una clase construida sobre BandedRow no está soportada: sus bandas se declaran en variables de clase privadas.

Autocompletado en tu editor: el esquema JSON

Sección titulada «Autocompletado en tu editor: el esquema JSON»

Un archivo de catálogo se escribe a mano, y un editor que conoce su forma completa el nombre de un campo mientras lo escribes, ofrece las palabras que admiten basis y kind y marca un texto donde va un número antes de que el lector vea el archivo. JSON Schema es la manera en que un editor aprende una forma, y io.catalogue_schema escribe una, en el dialecto 2020-12, a partir de las clases de fila que le das: cada campo con su propio nombre y con cada otra unidad que convierte el lector, los campos que puede nombrar cada matiz, la procedencia y sus tipos, los límites que impone la unidad de un campo (una densidad nunca es negativa, una porosidad va de 0 a 1) y la cabecera de un archivo CSV con su dialecto. Una clase de fila propia se describe con sus campos:

schema = io.catalogue_schema(materials.PorousMaterial, Plasterboard)
print(schema["$id"]) # urn:phonometry:schema:catalogue:1
print(schema["properties"]["row_type"]["enum"]) # ['Plasterboard', 'PorousMaterial']
board = schema["$defs"]["Plasterboard"]["properties"]
print(sorted(name for name in board if name.startswith("surface_density")))
# ['surface_density_g_m2', 'surface_density_kg_m2']
pathlib.Path("catalogue.schema.json").write_text(json.dumps(schema, indent=2), encoding="utf-8")
pointed = json.loads(panel_40)
pointed["$schema"] = "./catalogue.schema.json"
print(io.parse_catalogue(pointed, row_type=materials.PorousMaterial) == mine) # True

Un documento nombra su esquema en una clave $schema de primer nivel, que el lector admite y no lee nunca: un archivo a su lado, como arriba, o el esquema de todas las clases de fila que publica la biblioteca, que sirve el sitio de la documentación en https://docs.jmrp.io/phonometry/schemas/phonometry-catalogue-1.json. También se le puede decir a un editor qué archivos son catálogos, y dejarlos sin esa clave; en Visual Studio Code basta un ajuste:

{
"json.schemas": [
{
"fileMatch": ["*.catalogue.json", "*.csv.phonometry.json"],
"url": "https://docs.jmrp.io/phonometry/schemas/phonometry-catalogue-1.json"
}
]
}

El esquema exige a un archivo su forma: qué claves pueden tener un documento, una procedencia y una fila, qué clase de valor admite cada una, el patrón de un nombre y de una clave. Lo que depende de dos celdas a la vez, un valor junto a una palabra que dice que no lo hay o una cota en un campo sin intervalo, lo rechaza el lector, así que un archivo que el editor acepta aún puede rechazarse, y el rechazo dice dónde. Un archivo que el lector lee nunca aparece marcado en un editor cuyo validador lee las expresiones regulares como JavaScript o Python, como hace el de Visual Studio Code. El nombre del esquema, urn:phonometry:schema:catalogue:1, sigue a la versión del formato, así que un formato posterior será un esquema nuevo al lado de este.

El lector y las filas guardan lo que imprime el documento y nada más.

  • Ninguna banda que no imprima el documento se extrapola, se interpola o se rellena con sus vecinas, y ninguna se lee como cero.
  • Nada se recorta ni se redondea: un coeficiente de 1,05 sigue siendo 1,05, y una valoración nunca se lleva a una rejilla.
  • Ninguna cifra se adivina por su tamaño, y ninguna unidad se lee de dentro de una celda.
  • Nada se sube, se guarda en caché ni se copia, y no se abre ningún enlace que nombre un archivo.
  • Dos filas con el mismo nombre se guardan las dos y se contestan las dos, y ninguna búsqueda elige entre ellas por ti.
  • Nada de lo que nombra el archivo se importa: solo lo leen los lectores de JSON y de CSV de la biblioteca estándar, y el nombre de clase que escribe solo se compara con la clase que pasas.
  • Ningún dialecto se adivina: el delimitador y la marca decimal de un archivo CSV son los que declara su cabecera, y nunca se lee un separador de miles.
  • Cubierto

    Leer un catálogo propio de un archivo JSON o de un archivo CSV con su cabecera JSON (io.read_catalogue), o de un texto o un mapping (io.parse_catalogue), en las filas que entregan todos los catálogos publicados, con su io.Provenance, sus notas y todos los problemas de forma comunicados a la vez con un puntero JSON o con una línea y una columna; escribir filas, entre ellas una tabla publicada, de vuelta a un archivo JSON o CSV (io.write_catalogue); buscar en el tuyo y en los publicados con todas las búsquedas *_named; pasar las filas a los modelos de porosos, a un modelo de sala, al método de la meseta, a un suelo flotante, a la simulación elástica, a los modelos de suelo de exteriores y al modelo detallado de la ISO 12354; y el esquema JSON de un archivo de catálogo, para que un editor lo complete (io.catalogue_schema).

  • No cubierto

    El formato propio de una hoja de cálculo (XLSX, ODS) no se lee: guarda la hoja como «CSV UTF-8», y escribe en JSON lo que no cabe en una celda. Una valoración que ninguna función de esta biblioteca calcula (un NRC, un STC, un IIC) se guarda tal como se imprime y no se comprueba nunca. La biblioteca no trae datos de ningún fabricante, y una tabla pegada en una issue o en una pull request nunca se incorpora.