diff --git a/PLANO_PARIDADE_PYGLENN.md b/PLANO_PARIDADE_PYGLENN.md new file mode 100644 index 0000000..80a343b --- /dev/null +++ b/PLANO_PARIDADE_PYGLENN.md @@ -0,0 +1,494 @@ +# Glenn.jl -> pyglenn: paridade funcional e plano de implementacao + +**Data da auditoria:** 2026-08-19 +**Repositorios analisados:** [Glenn.jl](https://github.com/ProfLeao/Glenn.jl) e [pyglenn](https://github.com/ProfLeao/pyglenn) +**Versao Julia auditada:** `0.4.0` (release integrada ao `main` em 2026-08-19) +**Versao Python auditada:** `0.1.13` + +## 1. Objetivo e escopo + +Este documento registra as funcionalidades implementadas no Glenn.jl, compara-as com a implementacao atual do pyglenn e define um plano tecnico detalhado para portar ao pyglenn as capacidades existentes apenas no Glenn.jl. + +Os dois projetos compartilham o modelo de dados NASA-7 e o banco SQLite gerado a partir de `thermo.inp` no formato FORTRAN da NASA. A comparacao separa: + +1. **Funcionalidades exclusivas do Glenn.jl:** comportamento ou API publica presente no Julia e ausente atualmente no pyglenn. +2. **Funcionalidades equivalentes:** presentes nos dois projetos, embora com nomes, tipos de retorno ou ciclo de vida diferentes. +3. **Lacunas de paridade:** comportamentos presentes no pyglenn que tambem devem ser considerados no Glenn.jl. +4. **Recursos nao-runtime:** documentacao, CI, notebooks e ferramentas de qualidade, acompanhados separadamente da API cientifica. + +O objetivo nao e fazer Python parecer Julia. O objetivo e oferecer as mesmas capacidades cientificas, preservando as convencoes do Python, a compatibilidade com usuarios existentes e o formato SQLite compartilhado. + +## 0. Plano de saneamento da versao 0.2.0 + +Esta release corrige primeiro os problemas de integridade e consistencia cientifica do dataset incluido. Esse trabalho deve preceder os modelos tipados e as melhorias de desempenho, que de outro modo formalizariam dados ou resultados numericos incorretos. + +### 0.1 Evidencias e escopo + +O banco incluido apresenta as contagens esperadas (2.030 especies e 3.772 intervalos), mas sua massa molar media e implausivel. A inspecao encontrou tres registros cujo campo `name` e um token de coeficiente FORTRAN (por exemplo, `-1.302004763D+06`) e cujas massas molares chegam a `5.6168201e12 g/mol`. Trata-se de uma falha de recuperacao do parser: apos um bloco de intervalos incompleto ou malformado, uma linha posterior de coeficientes pode ser reinterpretada como cabecalho de especie. A correcao exige reconstruir e validar os dados; filtrar linhas ruins do banco atual nao e aceitavel. + +A implementacao de referencia e o Glenn.jl v0.4.0, integrado em 2026-08-19. O commit `77ff931` (`fix: use dataset reference gas constant (R_GLENN) for denormalization`) estabelece o comportamento de G2. O changelog registra que os dados NASA Glenn/CEA foram normalizados com a constante de referencia CODATA 1986 `8.314510`; usar o valor universal CODATA introduziu vies sistematico de aproximadamente -5,7 ppm. + +### 0.2 Frente A — parser e reconstrucao seguros + +1. Substituir a recuperacao implicita e tolerante de indice em `ThermoDBBuilder.parse_and_load()` por um parser com estados explicitos: `cabecalho da especie -> informacoes gerais -> N triplas completas de intervalo`. Cada estado deve consumir somente o tipo de registro esperado; uma linha de coeficientes nunca pode ser candidata a cabecalho de especie. +2. Validar cada bloco completo antes da insercao: nome de especie nao vazio, numero de intervalos positivo, limites de temperatura finitos e ordenados e nove coeficientes finitos. Registrar linha de origem e motivo de cada bloco rejeitado. +3. Tratar uma sequencia declarada, mas incompleta, de intervalos como bloco de especie rejeitado e resincronizar somente em um limite de cabecalho de especie confirmado. Nao inserir especies, intervalos ou coeficientes parciais. +4. Construir em um arquivo SQLite temporario, dentro de uma unica transacao. Executar a validacao pos-carga nele e substituir o `thermo.db` incluido somente depois que todos os gates da release passarem. Uma reconstrucao com falha deve preservar o banco anterior. +5. Adicionar fixtures para dados validos com multiplos intervalos, cabecalhos de intervalo malformados, linhas de coeficientes ausentes, notacao FORTRAN `D`/`d` e a forma exata da dessincronizacao que antes convertia um coeficiente em especie. + +### 0.3 Frente B — integridade do banco + +1. Adicionar um relatorio reutilizavel `validate_database()` (e um comando CLI somente de leitura, se util) que verifique chaves estrangeiras, uma linha de coeficientes por intervalo, ausencia de orfaos, valores finitos, limites ordenados e concordancia entre `num_intervals` e as linhas de intervalo reais. +2. Adicionar verificacoes semanticas que rejeitem nomes de especie com formato de coeficiente, exijam massas molares finitas e positivas e sinalizem valores fora de uma faixa de dominio documentada. O limite e apenas guarda diagnostica, nao substitui um parser correto. +3. Exigir que os dados canonicos reconstruidos coincidam com o baseline validado do arquivo-fonte (atualmente 2.035 especies validas, 3.779 intervalos e 3.779 conjuntos de coeficientes); verificar a divisao gas/condensed e comparar valores de O2, CO2, H2O, NH3 e SO2 com a auditoria NIST. Atualizar a documentacao legada de 2.030/3.772 somente depois que essa validacao for reproduzivel na CI. +4. Recalcular e testar estatisticas agregadas plausiveis apos a reconstrucao. Em particular, a massa molar media deve vir dos dados-fonte corrigidos, nunca de esconder linhas corrompidas em `get_statistics()`. +5. Armazenar o relatorio de validacao com os artefatos de release e fazer a CI construir um banco temporario novo a partir de `thermo.inp` antes dos testes de integridade. + +### 0.4 Frente C — especificacao de implementacao de G2 + +Implementar G2 antes de regenerar o banco incluido, seguindo o commit `77ff931` do Glenn.jl e o commit posterior `da0d833`, de documentacao da API: + +1. Em `database.py`, expor `R_UNIVERSAL = 8.31446261815324` e `R_GLENN = 8.314510`. Manter `R` como alias de compatibilidade documentado para `R_UNIVERSAL` na linha 0.2.x; nao alterar seu significado silenciosamente. +2. Criar o schema aditivo `metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL)` em `ThermoDBBuilder.create_tables()`. Implementar `write_metadata()` parametrizado e idempotente, persistindo exatamente `gas_constant_ref = "8.314510"` e `gas_constant_ref_source = "NASA Glenn/CEA thermo.inp (CODATA 1986)"`. +3. Adicionar `get_gas_constant_ref()` a `ThermoDBQuery`. Verificar explicitamente a existencia da tabela, ler a chave com consulta parametrizada, converter o valor e aceitar somente numeros finitos no intervalo `(8.0, 9.0)`. Tabela/chave ausente, texto invalido e valor implausivel devem emitir aviso e retornar `R_UNIVERSAL`; erros de conexao ou consulta nao podem ser ocultados por esse fallback. +4. Adicionar `migrate_metadata()` explicito e idempotente para bancos legados. Ele cria a tabela e grava as duas chaves canonicas sem reconstruir nem alterar especies, intervalos ou coeficientes. Conectar a um banco de usuario permanece uma operacao de leitura; a migracao e opt-in. +5. Adicionar `r_ref` a `ThermochemicalCalculator`, preenchendo-o somente apos conexao bem-sucedida, e usa-lo em todas as conversoes dimensionais: `cp_r * r_ref`, `h_rt * r_ref * T` e `s_r * r_ref`. Isso inclui caminhos escalares e de varias temperaturas. +6. Regenerar o banco incluido com metadados e verificar que ele seleciona `R_GLENN`. Um banco sem metadados deve continuar abrindo com o fallback da constante universal ate ser migrado. +7. Adicionar testes para constantes, metadados de um banco novo, escritas e migracoes idempotentes, metadados ausentes/invalidos/implausiveis, fallback legado e a razao exata de escala `R_GLENN / R_UNIVERSAL`. Adicionar valores de regressao entre linguagens para Cp, H e S de O2 em 298,15 K e 1000 K, mais uma especie com entalpia de formacao nao nula. +8. Atualizar README, referencia de banco do Sphinx, changelog, exportacoes publicas da API e notas de release para identificar a constante do dataset e quantificar a correcao intencional de ~5,7 ppm. + +### 0.5 Ordem e gates de release + +1. Escrever os testes de parser e integridade antes de alterar os dados canonicos. +2. Implementar e testar G2 em fixture minimo, incluindo o fallback legado. +3. Reconstruir o banco incluido em local temporario, valida-lo, substituir o asset do pacote e regenerar a auditoria NIST. +4. Construir wheel e sdist em ambiente limpo e verificar que ambos incluem `pyglenn/data/thermo.db` e `pyglenn/data/thermo.inp`. +5. Executar a matriz completa de Python (3.9–3.12), Ruff, mypy, documentacao com avisos como erro e comparacoes entre linguagens contra Glenn.jl v0.4.0. +6. Somente apos esses gates, continuar com G1/G3/G4/G5. Atualizar juntos para `0.2.0` o pacote, a receita Conda, as distribuicoes geradas e a documentacao de release. + +## 2. Inventario de funcionalidades do Glenn.jl + +### 2.1 Pacote e ciclo de vida + +- `Calculator()` sem configuracao, usando o `data/thermo.db` incluido no pacote. +- `Calculator(path)` para um banco SQLite personalizado. +- Gerenciador de contexto `Calculator(f::Function, path)`, com sintaxe do-block e fechamento garantido em `finally`. +- `Base.close(calc)` e representacoes `Base.show` para calculadoras e conexoes de banco. +- `default_db_path()` e `default_inp_path()` para localizar os arquivos de dados incluidos. +- Metadados publicos do pacote: `Glenn.__version__` e `Glenn.__author__`. + +### 2.2 Modelo de dominio tipado + +O Glenn.jl expoe structs Julia imutaveis em vez de dicionarios sem tipo: + +- `ThermoProperties`: temperatura, Cp, entalpia relativa, entropia, limites do intervalo, nome da especie e fase. +- `SpeciesInfo`: ID, nome, formula, fase, massa molecular, entalpia de formacao e quantidade de intervalos. +- `NASACoefficients`: os nove valores NASA-7 `a1`-`a7`, `b1` e `b2`, todos como `Float64`. +- `IntervalData`: identidade, limites, entalpia de referencia e coeficientes tipados. +- `DatabaseStats`: total de especies, intervalos, conjuntos de coeficientes, contagem por fase e massa molecular media. + +`NASACoefficients(::Dict)` e mantido como construtor de compatibilidade. Os metodos antigos de calculo que recebem `Dict` estao deprecated em favor dos coeficientes tipados. + +### 2.3 API de banco de dados + +A API de baixo nivel `ThermoDatabase` exporta: + +- `ThermoDB(path)` e `close(tdb)`. +- `get_statistics(tdb) -> DatabaseStats`. +- `find_species(tdb, name; exact_match=false)`. +- Busca por substring no nome ou formula da especie, priorizando correspondencias exatas de nome. +- Correspondencia exata sem diferenciar maiusculas e minusculas quando `exact_match=true`. +- `list_species_page(tdb; page=1, page_size=20)`. +- `list_all_species(tdb)` como listagem completa em uma unica consulta. +- `get_species_info(tdb, species_id)` como consulta leve apenas dos metadados. +- `get_species_data(tdb, species_id)` com todos os intervalos e coeficientes. +- `get_species_for_temperature(tdb, species_id, temperature)`. +- Avaliadores NASA-7 independentes: `calculate_cp`, `calculate_h` e `calculate_s`. +- Tipos de excecao de dominio: `ThermoCalcError`, `DatabaseNotConnectedError`, `SpeciesNotFoundError` e `TemperatureOutOfRangeError`. + +### 2.4 API da calculadora + +A API de alto nivel exporta: + +- `get_available_species(calc, pattern; exact_match=false)`. +- `calculate_properties(calc, species_id, T) -> ThermoProperties`. +- `calculate_properties(calc, species_id, T_range) -> Vector{ThermoProperties}`. +- `calculate_formation_enthalpy(calc, species_id)`. +- `calculate_enthalpy_change(calc, species_id, T1, T2)`. +- `get_properties_range(calc, species_id, T_range)`. + +As APIs escalar e de intervalo convertem as grandezas NASA adimensionais para unidades SI. A sobrecarga de intervalo carrega os intervalos uma vez e avalia, em memoria, todas as temperaturas validas. + +### 2.5 Constante do gas e metadados do dataset + +O Glenn.jl possui um sistema de constante de referencia especifico do dataset: + +- `R_UNIVERSAL = 8.31446261815324` para o valor universal CODATA. +- `R_GLENN = 8.314510` para a constante de referencia dos polinomios NASA Glenn/CEA. +- Tabela `metadata(key TEXT PRIMARY KEY, value TEXT)`. +- `write_metadata(builder)` durante a construcao do banco. +- `get_gas_constant_ref(db)` ao abrir o banco. +- `migrate_metadata!(db)` para bancos legados sem a tabela de metadados. +- `Calculator` armazena `R_ref` e usa essa constante para desnormalizar Cp, H e S. + +### 2.6 Construtor do banco + +`ThermoDBBuilder` implementa: + +- Ciclo de vida da conexao SQLite com foreign keys e modo WAL. +- Criacao das tabelas `species`, `temperature_intervals`, `coefficients`, `file_metadata` e `metadata`. +- Leitura de registros NASA FORTRAN em largura fixa. +- Conversao da notacao FORTRAN `D`/`d`. +- Parsers para especie, informacoes gerais, intervalos de temperatura e coeficientes. +- Deteccao de linhas de temperatura e coeficientes. +- Carga transacional com rollback em caso de falha. +- Tratamento idempotente de especies que ja existem no banco. +- Relatorio separado para especies novas, existentes e linhas ignoradas. +- Persistencia dos metadados da constante de gas do dataset. + +### 2.7 CLI e recursos do projeto + +- `cli_main()` com os comandos `build` e `query`. +- Script de conveniencia `bin/glenn.jl`. +- Saida de consulta com estatisticas, busca de especies, propriedades de exemplo e entalpia de formacao. +- Documentacao Documenter.jl e paginas de referencia da API. +- Exemplos executaveis e notebooks compativeis com Pluto. +- Auditoria de validacao cruzada NIST-JANAF/Shomate para CO2, N2, CO, H2O, O2, NH3 e SO2. +- Integracao de Aqua.jl e JuliaFormatter.jl para testes e desenvolvimento. +- Workflows de CI, documentacao, TagBot e CompatHelper. + +## 3. Comparacao com o pyglenn + +### 3.1 Funcionalidades ja equivalentes + +| Capacidade | Glenn.jl | pyglenn | Observacoes | +|---|---|---|---| +| Dataset SQLite incluido | `Calculator()` | `ThermochemicalCalculator()` mais `connect()` | O ciclo de vida e diferente. | +| Banco personalizado | `Calculator(path)` | `ThermochemicalCalculator(path)` | Ambos aceitam bancos externos. | +| Construtor NASA FORTRAN | `ThermoDBBuilder` | `ThermoDBBuilder` | Mesmo schema normalizado principal. | +| Equacoes NASA-7 Cp/H/S | `calculate_cp/h/s` | `ThermoDBQuery.calculate_cp/h/s` | Julia usa coeficientes tipados; Python usa dicionarios. | +| Busca por substring | `find_species` / `get_available_species` | Equivalente | Ambos pesquisam nome e formula. | +| Busca exata sem diferenciar caixa | `exact_match=true` | `exact_match=True` | Mesmo comportamento pretendido. | +| Listagem paginada | `list_species_page` | `list_species_page` | Mesmo modelo de pagina e total de paginas. | +| Dados completos de especie | `get_species_data` | `get_species_data` | A representacao do retorno e diferente. | +| Busca do intervalo por temperatura | `get_species_for_temperature` | Equivalente | O comportamento nos limites nao e identico. | +| Entalpia de formacao | `calculate_formation_enthalpy` | Equivalente | As convencoes de retorno sao diferentes. | +| Variacao de entalpia | `calculate_enthalpy_change` | Equivalente | Ambos avaliam as duas temperaturas. | +| Propriedades em varias temperaturas | `get_properties_range` | Mesmo nome | Julia retorna vetor; Python retorna dicionario indexado por temperatura. | +| Estatisticas do banco | Mesmo nome | Mesmo nome | Julia retorna `DatabaseStats`; Python retorna dicionario. | +| Gerenciador de contexto | do-block Julia | `with` Python | Ambos garantem limpeza. | +| CLI build/query | `cli_main`, `bin/glenn.jl` | comando instalado `pyglenn` | Ambos expoem os dois comandos principais. | +| Erros de dominio | Tipos exportados em Julia | Excecoes lancadas em Python | O Julia ainda nao usa consistentemente os subtipos especializados. | + +### 3.2 Funcionalidades do Glenn.jl a implementar no pyglenn + +Estas sao as diferencas principais identificadas pela auditoria. + +#### G1. Modelo tipado de resultados + +**Comportamento atual do Glenn.jl:** + +- Calculos retornam `ThermoProperties`. +- Consultas de especies retornam `SpeciesInfo`. +- Consultas de intervalos retornam `IntervalData` contendo `NASACoefficients`. +- Estatisticas retornam `DatabaseStats`. + +**Comportamento atual do pyglenn:** + +- Calculos e consultas retornam `dict[str, Any]`. +- Coeficientes sao dicionarios comuns. +- Estatisticas retornam um dicionario. + +**Plano de implementacao:** + +1. Adicionar dataclasses imutaveis em `src/pyglenn/models.py`: + - `NASACoefficients`. + - `SpeciesInfo`. + - `IntervalData`. + - `ThermoProperties`. + - `DatabaseStats`. +2. Usar tipos opcionais precisos para valores nulos vindos do SQLite. +3. Adicionar conversores `from_mapping` ou helpers explicitos na fronteira SQLite. +4. Alterar os metodos publicos para retornarem dataclasses, mantendo compatibilidade com mapeamentos por um ciclo de deprecacao, ou introduzir primeiro uma API tipada opt-in. +5. Exportar os modelos por `pyglenn.__init__` e documentar campos e unidades. +6. Adicionar testes de igualdade e representacao. + +**Criterios de aceitacao:** + +- Os resultados tipados expoem atributos equivalentes aos campos do Julia. +- `asdict()` ou conversor equivalente e documentado para JSON e usuarios legados. +- `formula`, massa molecular, entalpia de formacao e valores de referencia continuam podendo ser nulos. +- Usuarios existentes de dicionarios continuam funcionando por uma camada de compatibilidade documentada ou recebem um aviso de deprecacao planejado. + +#### G2. Constante de referencia do dataset e metadados + +**Comportamento atual do Glenn.jl:** + +- Diferencia `R_UNIVERSAL` de `R_GLENN`. +- Armazena `gas_constant_ref` e sua fonte na tabela `metadata`. +- Le o valor ao abrir a calculadora. +- Migra explicitamente bancos antigos. + +**Comportamento atual do pyglenn:** + +- Usa uma unica constante de modulo, `R = 8.314462618`. +- O construtor nao cria a tabela `metadata` nem grava a constante de referencia do dataset. +- Os coeficientes NASA sao portanto desnormalizados com a constante universal, e nao com a constante original `R_GLENN` do dataset incluido. + +**Plano de implementacao:** + +1. Adicionar `R_UNIVERSAL = 8.31446261815324` e `R_GLENN = 8.314510` em `database.py`. +2. Adicionar a criacao da tabela `metadata` em `ThermoDBBuilder.create_tables()`. +3. Adicionar `write_metadata()` usando SQL parametrizado e upsert idempotente. +4. Adicionar `migrate_metadata(connection)` para bancos existentes. +5. Adicionar `get_gas_constant_ref()` com fallback explicito para bancos legados. +6. Fazer `ThermochemicalCalculator` armazenar `R_ref` apos abrir a conexao. +7. Usar `R_ref` consistentemente nos calculos escalares e em intervalos. +8. Adicionar uma chave de fonte, como `gas_constant_ref_source`. +9. Atualizar a documentacao do schema e as notas da release. +10. Decidir se a migracao ocorre automaticamente ou somente por API explicita. A recomendacao e usar migracao explicita para evitar alterar arquivos do usuario silenciosamente. + +**Criterios de aceitacao:** + +- Um banco novo contem a tabela de metadados e `gas_constant_ref = 8.314510`. +- Um banco legado sem a tabela continua abrindo e possui fallback documentado. +- O banco incluido produz os mesmos valores desnormalizados do Glenn.jl dentro da tolerancia definida. +- As escritas de metadados sao idempotentes. + +#### G3. Listagem completa em uma unica consulta + +**Comportamento atual do Glenn.jl:** + +- `list_all_species(tdb)` carrega todas as especies com uma consulta ordenada. +- `get_available_species(calc)` usa esse caminho sem padrao de busca. + +**Comportamento atual do pyglenn:** + +- `get_available_species()` percorre paginas de 100 registros. +- `list_species_page()` existe, mas nao ha equivalente publico a `list_all_species()`. + +**Plano de implementacao:** + +1. Adicionar `ThermoDBQuery.list_all_species() -> list[SpeciesInfo]`. +2. Selecionar os mesmos campos de `find_species`, ordenar por nome e converter as linhas na fronteira do banco. +3. Fazer `get_available_species()` chamar o novo metodo quando o padrao estiver vazio. +4. Manter a paginacao para datasets grandes e para consumidores que precisam limitar memoria. +5. Adicionar teste garantindo que a busca sem padrao nao executa repetidas consultas de pagina. + +**Criterios de aceitacao:** + +- A busca sem padrao retorna todos os registros em ordem alfabetica. +- O tipo e os campos do retorno estao documentados. +- As APIs de lista completa e paginada continuam disponiveis separadamente. + +#### G4. Consulta leve de metadados de especie + +**Comportamento atual do Glenn.jl:** + +- `get_species_info(tdb, species_id)` retorna metadados basicos sem carregar intervalos ou coeficientes. +- O calculo escalar usa essa consulta leve. + +**Comportamento atual do pyglenn:** + +- `calculate_properties()` chama primeiro `get_species_data()`, carregando todos os intervalos. +- Nao ha consulta publica equivalente. + +**Plano de implementacao:** + +1. Adicionar `ThermoDBQuery.get_species_info(species_id) -> SpeciesInfo | None`. +2. Selecionar somente as colunas necessarias para identidade e metadados de saida. +3. Usar o metodo em `calculate_properties()` e `calculate_formation_enthalpy()`. +4. Manter `get_species_data()` para consumidores que realmente precisam dos intervalos completos. +5. Testar a diferenca entre especie inexistente e especie sem linhas de coeficientes. + +**Criterios de aceitacao:** + +- Calculos escalares nao carregam todos os intervalos sem necessidade. +- A busca de entalpia continua correta sem dados de intervalos. +- A API de dados completos permanece disponivel. + +#### G5. Modelo tipado para resultados em intervalo + +**Comportamento atual do Glenn.jl:** + +- `calculate_properties(calc, species_id, T_range)` e uma sobrecarga dedicada. +- `get_properties_range()` retorna um `Vector{ThermoProperties}` ordenado. +- Os intervalos sao carregados uma vez e reutilizados. + +**Comportamento atual do pyglenn:** + +- `get_properties_range()` percorre as temperaturas e retorna `dict[float, dict]`. +- Temperaturas fora do intervalo sao ignoradas; retorna `None` quando todas falham. + +**Plano de implementacao:** + +1. Manter a API Python baseada em dicionario por compatibilidade. +2. Adicionar `calculate_properties_range()` ou `get_properties_range_typed()` retornando uma lista ordenada de `ThermoProperties`. +3. Carregar os intervalos uma vez por chamada e selecionar em memoria. +4. Definir o comportamento de temperaturas duplicadas; a recomendacao e preservar a ordem de entrada na lista tipada e manter a deduplicacao na API legada. +5. Definir o comportamento de valores fora do intervalo; a recomendacao e manter o skip na API legada e oferecer `strict=True` para lancar erro. +6. Adicionar benchmark comparando uma chamada em lote com chamadas escalares repetidas. + +**Criterios de aceitacao:** + +- Resultados ordenados estao disponiveis sem quebrar consumidores do dicionario. +- Cada temperatura valida usa o intervalo correto. +- A constante de referencia e aplicada consistentemente. +- A performance melhora em relacao a chamadas repetidas que carregam todos os dados. + +#### G6. API de migracao dos metadados do dataset + +Este item e separado de G2 por ser uma capacidade operacional. + +**Comportamento atual do Glenn.jl:** `migrate_metadata!(db)` atualiza um banco compartilhado antigo sem reconstruir todas as especies. + +**Plano de implementacao:** + +1. Disponibilizar `migrate_metadata(connection_or_query) -> None` no pyglenn. +2. Tornar a operacao segura para execucao repetida. +3. Nao reconstruir especies nem linhas de coeficientes. +4. Retornar um pequeno relatorio ou documentar o retorno `None`. +5. Adicionar uma opcao CLI, como `pyglenn migrate-metadata -d thermo.db`, somente se houver necessidade operacional. + +## 4. Comportamentos do pyglenn a considerar no Glenn.jl + +Estas nao sao funcionalidades exclusivas do Glenn.jl, mas apareceram na comparacao e devem ser consideradas para paridade. + +### P1. Estado explicito da conexao + +O pyglenn expoe `connect() -> bool` e `connected`, permitindo criar uma calculadora antes de conectar. O Glenn.jl abre o banco em `Calculator(path)` e nao possui propriedade publica de conexao. + +Decisao recomendada para Julia: manter a construcao imediata, idiomatica para gerenciamento de recursos, mas adicionar `isopen(calc)` e `isopen(tdb)`. Evitar uma calculadora desconectada sem caso de uso concreto. + +### P2. Tolerancia nos limites dos intervalos + +O pyglenn usa `temp_max + 1e-9` e ordena por numero do intervalo, tornando deterministica uma fronteira compartilhada. O Glenn.jl usa comparacoes inclusivas diretas e atualmente nao possui `ORDER BY` explicito nessa consulta. + +Mudanca recomendada no Julia: + +- Adicionar `ORDER BY ti.interval_number LIMIT 1`. +- Usar uma tolerancia documentada, de preferencia uma constante nomeada. +- Testar limite inferior, fronteira compartilhada, limite superior e valores dentro da tolerancia. + +Este item e importante para correcao e reproducibilidade entre linguagens. + +### P3. Comportamento das excecoes especializadas + +Os dois projetos definem conceitos de erro especializados, mas os calculos de alto nivel do Glenn.jl atualmente lancam `ThermoCalcError` diretamente para especie invalida e temperatura fora do intervalo. O pyglenn usa `SpeciesNotFoundError` e `TemperatureOutOfRangeError` nesses casos. + +Mudanca recomendada no Julia: + +- Lancar `SpeciesNotFoundError(species_id)` para IDs inexistentes. +- Lancar `TemperatureOutOfRangeError(T, info.name)` para temperaturas invalidas. +- Reservar `ThermoCalcError` para erros sem tipo de dominio mais especifico. +- Atualizar testes e documentacao. + +### P4. Contrato de instalacao da CLI + +O pyglenn instala um executavel `pyglenn` por `pyproject.toml`. O Glenn.jl possui `cli_main()` e `bin/glenn.jl`, mas nao possui declaracao equivalente de executavel global. + +Decisao recomendada no Julia: manter o script, comum em projetos Julia, e documentar uma invocacao estavel. Se for necessario um comando global, fornecer um wrapper de aplicacao Julia sem alterar a API da biblioteca. + +### P5. Caminhos padrao do builder + +As CLIs usam padroes diferentes: o Glenn.jl resolve caminhos incluidos no pacote no parser leve, enquanto a CLI do pyglenn usa `thermo.inp` e `thermo.db` relativos ao diretorio atual no comando de build. Uma futura migracao de metadados deve definir se opera sobre os arquivos incluidos ou sobre o diretorio atual. + +## 5. Fases propostas para implementar no pyglenn + +### Fase 0 - Contrato de compatibilidade + +- Publicar esta comparacao e decidir se dataclasses tipadas serao aditivas ou implicarao mudanca de retorno. +- Definir politica de compatibilidade para dicionarios. +- Decidir migracao automatica ou explicita de metadados. +- Definir tolerancias numericas para comparacoes entre linguagens. + +### Fase 1 - Modelos e fronteira de conversao + +- Adicionar as dataclasses em `models.py`. +- Adicionar conversores de linhas para modelos. +- Documentar unidades e campos opcionais. +- Adicionar testes sem alterar inicialmente os retornos publicos existentes. + +### Fase 2 - Metadados e constante do gas + +- Adicionar constantes e schema de metadados. +- Gravar metadados no builder e implementar migracao. +- Adicionar `R_ref` na calculadora. +- Regenerar ou migrar o banco incluido. +- Adicionar valores de regressao para O2 em 298.15 K, 1000 K e uma segunda especie. + +### Fase 3 - Paridade das consultas + +- Adicionar `list_all_species()`. +- Adicionar `get_species_info()`. +- Usar os caminhos de lista completa e consulta leve. +- Adicionar testes de quantidade de consultas e especie inexistente. + +### Fase 4 - API tipada de intervalos + +- Adicionar metodo tipado de intervalo de forma aditiva. +- Preservar o metodo atual baseado em dicionario indexado por temperatura. +- Documentar ordem, temperaturas duplicadas, valores ignorados e modo estrito. +- Adicionar testes de performance e numericos. + +### Fase 5 - Exports e documentacao + +- Exportar constantes, modelos, helpers de metadados e novas consultas em `pyglenn.__init__`. +- Atualizar README, paginas Sphinx, exemplos e ajuda da CLI. +- Adicionar guia de migracao com exemplos antigos e novos usando dataclasses. + +### Fase 6 - Release e interoperabilidade + +- Executar testes, Ruff, mypy e cobertura do pyglenn. +- Executar os testes do Glenn.jl contra o banco migrado. +- Comparar schemas SQLite e quantidades de registros. +- Comparar Cp, H e S em especies e temperaturas compartilhadas. +- Publicar uma release menor se os adaptadores de compatibilidade permanecerem; definir a politica adequada ao estado pre-1.0 caso os tipos de retorno mudem. + +## 6. Matriz de aceitacao entre linguagens + +A implementacao somente estara concluida quando estas verificacoes passarem: + +| Area | Verificacao obrigatoria | +|---|---| +| Schema | Os dois builders criam as mesmas tabelas principais; metadados sao aditivos e compreendidos pelas duas bibliotecas. | +| Constantes | O dataset NASA incluido e desnormalizado com `8.314510` nas duas implementacoes. | +| Busca exata | `O2`, `o2` e o parcial `O` produzem a mesma semantica de busca. | +| Formula | O comportamento de busca por nome/formula e equivalente. | +| Limites | 200 K, 1000 K e 6000 K selecionam os mesmos intervalos para O2. | +| Especie ausente | As duas bibliotecas expoem um resultado/erro claro e documentado. | +| Temperatura invalida | As duas bibliotecas expoem uma politica clara para temperatura fora do intervalo. | +| Valores escalares | Cp, H e S concordam dentro de tolerancia explicita. | +| Valores em intervalo | Temperaturas validas, ordem, valores ignorados e duplicatas sao documentados e testados. | +| Entalpia de formacao | O valor de referencia de O2 e uma especie com valor nao nulo concordam. | +| Builder | Um `thermo.inp` minimo e carregado com sucesso e grava metadados. | +| Banco legado | Um banco anterior aos metadados continua legivel e pode ser migrado sem rebuild. | +| CLI | Os comandos build/query reportam erros uteis e terminam com sucesso. | + +## 7. Fixtures de teste sugeridas + +Use um fixture SQLite pequeno e um fixture FORTRAN minimo: + +- Especie `O2`, com intervalos `[200, 1000]` e `[1000, 6000]`. +- Pelo menos uma especie com entalpia de formacao nao nula, como `CO2`. +- Um caso de busca por formula diferente do nome exibido. +- Um banco sem `metadata` para testes de migracao/fallback. +- Um banco com `metadata` contendo valor invalido ou implausivel. +- Temperaturas `200.0`, `1000.0`, `6000.0`, `100.0` e `7000.0`. +- Um intervalo de entrada com temperaturas validas, invalidas, duplicadas e inteiras. + +Os testes numericos devem usar tolerancias absoluta e relativa adequadas a avaliacao dos polinomios e a constante de gas selecionada. Nao compare a saida arredondada exibida no terminal. + +## 8. Riscos e decisoes + +- **Compatibilidade de retorno:** substituir dicionarios diretamente pode quebrar usuarios Python. Prefira APIs tipadas aditivas ou um periodo de deprecacao. +- **Tipagem do SQLite:** mantenha `metadata.value` como `TEXT`, pois a tabela e chave/valor generica; converta a constante na leitura e valide seu intervalo. +- **Migracao automatica:** alterar o banco do usuario silenciosamente e surpreendente. Prefira uma API explicita com aviso ou comando CLI. +- **Correcao cientifica:** a constante de gas e dado cientifico, nao apenas configuracao. A fonte e o fallback devem ser documentados e testados. +- **Limites de intervalo:** uma tolerancia pode aceitar valores ligeiramente fora do intervalo declarado. Use tolerancia pequena, nomeada e testada. +- **Performance:** a conversao tipada nao deve causar uma consulta por temperatura. Calculos em lote devem reutilizar os intervalos carregados. +- **Versionamento:** embora o pyglenn seja pre-1.0, usuarios precisam de nota de migracao quando tipos de retorno ou constantes mudarem. + +## 9. Recomendacao de curto prazo + +Implemente G2 primeiro, pois ele afeta todos os resultados termodinamicos desnormalizados. Em seguida implemente G1 e G4, que estabelecem uma fronteira tipada e estavel para modelos e consultas. Depois implemente G3 e G5 como melhorias de performance e API. Em paralelo, aplique P2 e P3 no Glenn.jl para que as duas bibliotecas concordem nos limites de intervalo e na semantica das excecoes antes de usar a validacao numerica como gate de release. diff --git a/PYGLENN_PARITY_PLAN.md b/PYGLENN_PARITY_PLAN.md new file mode 100644 index 0000000..f62425c --- /dev/null +++ b/PYGLENN_PARITY_PLAN.md @@ -0,0 +1,495 @@ +# Glenn.jl -> pyglenn: functional parity and implementation plan + +**Audit date:** 2026-08-19 +**Source repositories:** [Glenn.jl](https://github.com/ProfLeao/Glenn.jl) and [pyglenn](https://github.com/ProfLeao/pyglenn) +**Audited Julia version:** `0.4.0` (release merged into `main` on 2026-08-19) +**Audited Python version:** `0.1.13` + +## 1. Purpose and scope + +This document records the functionality implemented in Glenn.jl, compares it with the current pyglenn implementation, and defines a detailed plan for porting Glenn-only capabilities to pyglenn. + +The two projects share the NASA-7 data model and the SQLite database generated from NASA FORTRAN `thermo.inp`. The comparison therefore distinguishes: + +1. **Glenn-only functionality:** behavior or public API present in Glenn.jl and not currently present in pyglenn. +2. **Equivalent functionality:** present in both projects, although names, return types, or lifecycle differ. +3. **pyglenn-only or parity gaps:** behavior present in pyglenn that should be considered for Glenn.jl as well, but is not part of the Glenn-to-pyglenn port. +4. **Non-runtime project features:** documentation, CI, notebooks, and quality tooling. These are tracked separately from the scientific API. + +The goal is not to make Python look like Julia. The goal is to expose the same scientific capabilities while preserving Python conventions, backward compatibility, and the existing SQLite format. + +## 0. Version 0.2.0 remediation plan + +This release first corrects the integrity and scientific-consistency issues in the bundled dataset. It must precede the broader typed-model and performance work, because those changes would otherwise formalize incorrect data or numerical output. + +### 0.1 Evidence and scope + +The bundled database reports the expected row counts (2,030 species and 3,772 intervals), but its molecular-weight average is implausible. Inspection found three records whose `name` values are FORTRAN coefficient tokens (for example, `-1.302004763D+06`) and whose molecular weights reach `5.6168201e12 g/mol`. This is a parser recovery failure: after an incomplete or malformed interval block, a later coefficient line can be reconsidered as a new species header. The release must rebuild and validate the data; filtering bad rows from an existing database is not an acceptable correction. + +The reference implementation is Glenn.jl v0.4.0, merged on 2026-08-19. Its commit `77ff931` (`fix: use dataset reference gas constant (R_GLENN) for denormalization`) establishes the G2 behavior. The changelog documents that NASA Glenn/CEA data are normalized with the CODATA 1986 reference constant `8.314510`; use of the universal CODATA value caused an approximately -5.7 ppm systematic bias. + +### 0.2 Workstream A — safe parser and rebuild + +1. Replace the implicit, best-effort index recovery in `ThermoDBBuilder.parse_and_load()` with an explicit record-state parser: `species header -> general information -> N complete interval triplets`. A state may only consume its expected record type; coefficient lines must never be eligible as a species header. +2. Validate each complete block before insertion: non-empty species name, positive interval count, finite ordered temperature bounds, and all nine finite coefficient values. Record a source line number and reason for every rejected block. +3. Treat a declared-but-incomplete interval sequence as a rejected species block, then resynchronize only at a verified species-header boundary. Do not insert partial species, intervals, or coefficients. +4. Build into a temporary SQLite file in one transaction. Run post-load validation there and replace the bundled `thermo.db` only after all release gates pass. A failed rebuild must leave the previous database untouched. +5. Add parser fixtures for valid multi-interval data, malformed interval headers, missing coefficient lines, FORTRAN `D`/`d` notation, and the exact desynchronization shape that previously turned a coefficient into a species. + +### 0.3 Workstream B — database integrity + +1. Add a reusable `validate_database()` report (and a read-only CLI command if useful) that checks foreign keys, one coefficient row per interval, no orphan rows, finite values, ordered bounds, and `num_intervals` agreement with actual interval rows. +2. Add semantic checks that reject coefficient-like species names, require positive finite molecular weights, and flag values outside a documented domain range. The threshold is a diagnostic guard, not a substitute for correct parsing. +3. Require the rebuilt canonical data to match the validated source baseline (currently 2,035 valid species, 3,779 intervals, and 3,779 coefficient sets); verify the gas/condensed split and compare representative O2, CO2, H2O, NH3, and SO2 values with the NIST audit. Update legacy 2,030/3,772 documentation only after this validation is made reproducible in CI. +4. Recompute and assert plausible aggregate statistics after the rebuild. In particular, average molecular weight must be derived from corrected source data, never hidden by excluding corrupt rows in `get_statistics()`. +5. Store the validation report with release artifacts and make CI build a fresh temporary database from `thermo.inp` before integrity tests. + +### 0.4 Workstream C — G2 implementation specification + +Implement G2 before regenerating the bundled database, following Glenn.jl commit `77ff931` and its subsequent `da0d833` API-documentation commit: + +1. In `database.py`, expose `R_UNIVERSAL = 8.31446261815324` and `R_GLENN = 8.314510`. Keep `R` as a documented compatibility alias for `R_UNIVERSAL` for the 0.2.x line; do not silently change its meaning. +2. Create additive schema `metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL)` in `ThermoDBBuilder.create_tables()`. Implement an idempotent, parameterized `write_metadata()` that persists exactly `gas_constant_ref = "8.314510"` and `gas_constant_ref_source = "NASA Glenn/CEA thermo.inp (CODATA 1986)"`. +3. Add `get_gas_constant_ref()` to `ThermoDBQuery`. Explicitly check for the table, read the key with a parameterized query, parse the value, and accept only a finite value in `(8.0, 9.0)`. Missing table/key, invalid text, and implausible values must warn and return `R_UNIVERSAL`; connection/query errors must not be swallowed as fallback. +4. Add an explicit, idempotent `migrate_metadata()` for legacy databases. It creates the table and writes the two canonical keys without rebuilding or altering species, intervals, or coefficients. Connecting to a user database remains read-only; migration is opt-in. +5. Add `r_ref` to `ThermochemicalCalculator`, populate it only after a successful connection, and use it for every dimensional conversion: `cp_r * r_ref`, `h_rt * r_ref * T`, and `s_r * r_ref`. This includes scalar and multi-temperature paths. +6. Regenerate the bundled database with metadata and verify it selects `R_GLENN`. A pre-metadata database must continue to open using the universal-constant fallback until migrated. +7. Add tests for constants, new-builder metadata, idempotent writes and migration, missing/invalid/implausible metadata, legacy fallback, and the exact scaling ratio `R_GLENN / R_UNIVERSAL`. Add cross-language regression values for Cp, H, and S of O2 at 298.15 K and 1000 K plus one nonzero-formation-enthalpy species. +8. Update the README, Sphinx database reference, changelog, API exports, and release notes to identify the dataset constant and quantify the intentional ~5.7 ppm correction. + +### 0.5 Release gates and ordering + +1. Write parser and integrity tests before modifying the canonical data. +2. Implement and test G2 on a minimal fixture, including legacy fallback. +3. Rebuild the bundled database in a temporary location, validate it, then replace the package asset and regenerate the NIST audit. +4. Build both wheel and sdist from a clean environment and assert that each includes `pyglenn/data/thermo.db` and `pyglenn/data/thermo.inp`. +5. Run the full Python support matrix (3.9–3.12), Ruff, mypy, docs with warnings-as-errors, and cross-language comparisons against Glenn.jl v0.4.0. +6. Only after these gates pass, continue with G1/G3/G4/G5. Bump the package, Conda recipe, generated distributions, and release documentation together to `0.2.0`. + +## 2. Glenn.jl functionality inventory + +### 2.1 Package and lifecycle + +- Zero-configuration `Calculator()` using the bundled `data/thermo.db`. +- `Calculator(path)` for a custom SQLite database. +- `Calculator(f::Function, path)` do-block context manager that always closes the calculator in `finally`. +- `Base.close(calc)` and `Base.show` representations for calculator and database handles. +- `default_db_path()` and `default_inp_path()` for resolving packaged data files. +- Public package metadata: `Glenn.__version__` and `Glenn.__author__`. + +### 2.2 Typed domain model + +Glenn.jl exposes immutable Julia structs instead of untyped dictionaries: + +- `ThermoProperties`: temperature, Cp, relative enthalpy, entropy, interval bounds, species name, and phase. +- `SpeciesInfo`: ID, name, formula, phase, molecular weight, formation enthalpy, and interval count. +- `NASACoefficients`: the nine NASA-7 values `a1`-`a7`, `b1`, and `b2`, all as `Float64`. +- `IntervalData`: interval identity, bounds, reference enthalpy, and typed coefficients. +- `DatabaseStats`: total species, intervals, coefficient sets, phase counts, and average molecular weight. + +`NASACoefficients(::Dict)` is retained as a compatibility constructor. The old `Dict` calculation methods are deprecated in favor of typed coefficients. + +### 2.3 Database API + +The low-level `ThermoDatabase` API exports: + +- `ThermoDB(path)` and `close(tdb)`. +- `get_statistics(tdb) -> DatabaseStats`. +- `find_species(tdb, name; exact_match=false)` with case-insensitive exact-name matching when requested. +- Substring search by species name or formula, with exact-name results prioritized. +- `list_species_page(tdb; page=1, page_size=20)`. +- `list_all_species(tdb)` as a single-query full listing. +- `get_species_info(tdb, species_id)` as a lightweight metadata-only lookup. +- `get_species_data(tdb, species_id)` with all intervals and coefficients. +- `get_species_for_temperature(tdb, species_id, temperature)`. +- Standalone NASA-7 evaluators: `calculate_cp`, `calculate_h`, and `calculate_s`. +- Domain exception types: `ThermoCalcError`, `DatabaseNotConnectedError`, `SpeciesNotFoundError`, and `TemperatureOutOfRangeError`. + +### 2.4 Calculator API + +The high-level API exports: + +- `get_available_species(calc, pattern; exact_match=false)`. +- `calculate_properties(calc, species_id, T) -> ThermoProperties`. +- `calculate_properties(calc, species_id, T_range) -> Vector{ThermoProperties}`. +- `calculate_formation_enthalpy(calc, species_id)`. +- `calculate_enthalpy_change(calc, species_id, T1, T2)`. +- `get_properties_range(calc, species_id, T_range)`. + +The scalar and range APIs denormalize dimensionless NASA quantities into SI units. The range overload loads species intervals once and evaluates all valid temperatures in memory. + +### 2.5 Gas-constant and dataset metadata + +Glenn.jl has a dataset-specific reference-constant system: + +- `R_UNIVERSAL = 8.31446261815324` for the universal CODATA value. +- `R_GLENN = 8.314510` for the NASA Glenn/CEA polynomial reference value. +- `metadata(key TEXT PRIMARY KEY, value TEXT)` table. +- `write_metadata(builder)` when building a database. +- `get_gas_constant_ref(db)` when opening a database. +- `migrate_metadata!(db)` for legacy databases without the metadata table. +- `Calculator` stores `R_ref` and uses it for Cp, H, and S denormalization. + +### 2.6 Database builder + +`ThermoDBBuilder` implements: + +- SQLite connection lifecycle with foreign keys and WAL mode. +- Creation of `species`, `temperature_intervals`, `coefficients`, `file_metadata`, and `metadata` tables. +- Parsing of NASA FORTRAN fixed-width records. +- FORTRAN `D`/`d` notation conversion. +- Species, general-information, temperature-interval, and coefficient parsing helpers. +- Temperature and coefficient line detection. +- Transactional loading with rollback on failure. +- Idempotent handling of species already present in the database. +- Distinct reporting of new species, existing species, and skipped lines. +- Dataset gas-constant metadata persistence. + +### 2.7 CLI and project assets + +- `cli_main()` with `build` and `query` commands. +- `bin/glenn.jl` convenience entry point. +- Query output containing statistics, species search results, sample properties, and formation enthalpy. +- Documenter.jl documentation and API pages. +- Executable examples and Pluto-compatible notebooks. +- NIST-JANAF/Shomate cross-validation audit for CO2, N2, CO, H2O, O2, NH3, and SO2. +- Aqua.jl and JuliaFormatter.jl test/development integration. +- CI, documentation, TagBot, and CompatHelper workflows. + +## 3. Comparison with pyglenn + +### 3.1 Functionality already equivalent + +These capabilities are already available in both projects: + +| Capability | Glenn.jl | pyglenn | Notes | +|---|---|---|---| +| Bundled SQLite dataset | `Calculator()` | `ThermochemicalCalculator()` plus `connect()` | Lifecycle differs. | +| Custom database path | `Calculator(path)` | `ThermochemicalCalculator(path)` | Both support external databases. | +| NASA FORTRAN builder | `ThermoDBBuilder` | `ThermoDBBuilder` | Same normalized core schema. | +| NASA-7 Cp/H/S equations | `calculate_cp/h/s` | `ThermoDBQuery.calculate_cp/h/s` | Julia uses typed coefficients; Python uses dicts. | +| Species substring search | `find_species` / `get_available_species` | Same | Both search name and formula. | +| Case-insensitive exact match | `exact_match=true` | `exact_match=True` | Same intended behavior. | +| Paginated species listing | `list_species_page` | `list_species_page` | Same page/total-pages model. | +| Complete species data | `get_species_data` | `get_species_data` | Return representation differs. | +| Temperature-specific interval lookup | `get_species_for_temperature` | Same | Boundary behavior is not identical; see Section 3.3. | +| Formation enthalpy | `calculate_formation_enthalpy` | Same | Return conventions differ for disconnected/missing data. | +| Enthalpy change | `calculate_enthalpy_change` | Same | Both evaluate two endpoint properties. | +| Multiple-temperature properties | `get_properties_range` | Same name | Julia returns a vector; Python returns a temperature-keyed dict. | +| Database statistics | `get_statistics` | Same name | Julia returns `DatabaseStats`; Python returns a dict. | +| Context manager | Julia do-block | Python `with` | Both guarantee cleanup. | +| CLI build/query | `cli_main`, `bin/glenn.jl` | installed `pyglenn` command | Both expose the same two primary commands. | +| Domain errors | exported in Julia | raised in Python | Julia currently does not consistently throw the specialized subtypes; see Section 3.3. | + +### 3.2 Glenn-only capabilities to implement in pyglenn + +These are the primary differences requested by this audit. + +#### G1. Typed domain result model + +**Current Glenn.jl behavior:** + +- Public calculations return `ThermoProperties`. +- Species queries return `SpeciesInfo`. +- Interval queries return `IntervalData` containing `NASACoefficients`. +- Statistics return `DatabaseStats`. + +**Current pyglenn behavior:** + +- Calculations and queries return `dict[str, Any]`. +- Coefficients are ordinary dictionaries. +- Statistics return a dictionary. + +**Implementation plan:** + +1. Add frozen dataclasses in `src/pyglenn/models.py`: + - `NASACoefficients`. + - `SpeciesInfo`. + - `IntervalData`. + - `ThermoProperties`. + - `DatabaseStats`. +2. Use precise optional types for nullable SQLite values. +3. Add `from_mapping` or explicit conversion helpers only at the SQLite boundary. +4. Change public methods to return dataclasses while retaining mapping compatibility for one deprecation cycle, or add an opt-in typed API first if v0.2 compatibility risk is unacceptable. +5. Export the models from `pyglenn.__init__` and document field names and units. +6. Add equality and representation tests; dataclass equality should make scientific regression tests easier to read. + +**Acceptance criteria:** + +- Public typed results expose attributes equivalent to the Julia fields. +- `asdict()` or an equivalent documented conversion is available for JSON/legacy callers. +- Nullable `formula`, molecular weight, formation enthalpy, and interval reference values remain nullable rather than silently becoming zero. +- Existing dictionary consumers either continue to work through a documented compatibility layer or receive a planned, documented deprecation warning. + +#### G2. Dataset reference gas constant and metadata + +**Current Glenn.jl behavior:** + +- Distinguishes `R_UNIVERSAL` from `R_GLENN`. +- Stores `gas_constant_ref` and its source in the `metadata` table. +- Reads the value when a calculator opens a database. +- Migrates old databases explicitly. + +**Current pyglenn behavior:** + +- Uses one module constant, `R = 8.314462618`. +- The builder creates no `metadata` table and writes no dataset reference constant. +- Calculations therefore denormalize NASA coefficients with the universal constant, not the bundled dataset's original `R_GLENN` reference. + +**Implementation plan:** + +1. Add `R_UNIVERSAL = 8.31446261815324` and `R_GLENN = 8.314510` in `database.py`. +2. Add the `metadata` table creation to `ThermoDBBuilder.create_tables()`. +3. Add `write_metadata()` using parameterized SQL and idempotent upserts. +4. Add `migrate_metadata(connection)` for existing databases. +5. Add `get_gas_constant_ref()` with explicit fallback behavior for legacy databases. +6. Make `ThermochemicalCalculator` store `R_ref` after connecting/opening the database. +7. Use `R_ref` consistently for Cp, H, S, and range calculations. +8. Add a source metadata key such as `gas_constant_ref_source`. +9. Update the database-structure documentation and release notes. +10. Decide whether legacy databases should be migrated automatically on connect or only through an explicit API. The recommended default is explicit migration to avoid silently modifying user files. + +**Acceptance criteria:** + +- A newly built database contains the metadata table and `gas_constant_ref = 8.314510`. +- A legacy database without the table still opens and has a documented fallback. +- The bundled database produces the same denormalized values as Glenn.jl within a documented tolerance. +- Metadata writes are idempotent and do not duplicate rows. + +#### G3. Single-query complete species listing + +**Current Glenn.jl behavior:** + +- `list_all_species(tdb)` loads all species with one ordered query. +- `get_available_species(calc)` uses it when no pattern is provided. + +**Current pyglenn behavior:** + +- `get_available_species()` walks through pages of 100 records. +- `list_species_page()` is available, but there is no equivalent single-query `list_all_species()` method. + +**Implementation plan:** + +1. Add `ThermoDBQuery.list_all_species() -> list[SpeciesInfo]`. +2. Select the same fields as `find_species`, order by name, and convert rows at the database boundary. +3. Change `ThermochemicalCalculator.get_available_species()` with an empty pattern to call the new method. +4. Keep pagination for large datasets and expose it for callers that need bounded memory. +5. Add a test proving the no-pattern path does not make repeated page queries; use a small query spy or a dedicated method-level test. + +**Acceptance criteria:** + +- Empty-pattern lookup returns all records in alphabetical order. +- The result type and fields are documented. +- Paginated and full-list APIs remain separately available. + +#### G4. Lightweight species metadata lookup + +**Current Glenn.jl behavior:** + +- `get_species_info(tdb, species_id)` returns basic metadata without loading intervals or coefficients. +- The high-level calculator uses this for scalar property calculations. + +**Current pyglenn behavior:** + +- `calculate_properties()` calls `get_species_data()` first, loading all intervals before selecting one. +- There is no equivalent public lightweight lookup. + +**Implementation plan:** + +1. Add `ThermoDBQuery.get_species_info(species_id) -> SpeciesInfo | None`. +2. Select only species columns needed for identity and output metadata. +3. Use it in scalar `calculate_properties()` and `calculate_formation_enthalpy()`. +4. Preserve `get_species_data()` for callers explicitly requesting all intervals. +5. Add tests that distinguish not-found from a species with no coefficient rows. + +**Acceptance criteria:** + +- Scalar calculations do not load every interval unnecessarily. +- Formation-enthalpy lookup remains correct when interval data is absent. +- The complete-data API remains unchanged for existing callers. + +#### G5. Vectorized range result model + +**Current Glenn.jl behavior:** + +- `calculate_properties(calc, species_id, T_range)` is a dedicated overload. +- `get_properties_range()` returns an ordered `Vector{ThermoProperties}`. +- Interval data is loaded once and reused. + +**Current pyglenn behavior:** + +- `get_properties_range()` loops through temperatures and returns `dict[float, dict]`. +- It skips out-of-range temperatures and returns `None` if every temperature fails. + +**Implementation plan:** + +1. Keep the existing Python dictionary API for backward compatibility. +2. Add a typed `calculate_properties_range()` or `get_properties_range_typed()` returning an ordered list of `ThermoProperties`. +3. Load species intervals once per call and select intervals in memory. +4. Define duplicate-temperature behavior explicitly; recommended behavior is to preserve input order in the typed list and retain dictionary deduplication for the legacy method. +5. Define out-of-range behavior explicitly; recommended behavior is to skip invalid values in the legacy method and offer `strict=True` for raising an error. +6. Add benchmark coverage comparing one bulk query with repeated scalar calls. + +**Acceptance criteria:** + +- Ordered range results are available without breaking existing dictionary callers. +- All valid temperatures use the correct interval. +- The reference gas constant is applied consistently to every result. +- Performance is measurably better than repeated complete-data lookups. + +#### G6. Explicit dataset metadata migration API + +This is separated from G2 because it is an operational capability. + +**Current Glenn.jl behavior:** `migrate_metadata!(db)` can upgrade an old shared database without rebuilding all species. + +**Implementation plan:** + +1. Provide `migrate_metadata(connection_or_query) -> None` in pyglenn. +2. Make it safe to run repeatedly. +3. Do not rebuild species or coefficient rows. +4. Return a small migration report or document that the function returns `None`. +5. Add a CLI option, for example `pyglenn migrate-metadata -d thermo.db`, only if users need a command-line migration path. + +## 4. pyglenn behavior that should also be considered for Glenn.jl + +These are not Glenn-only features, but the audit found them while comparing the implementations. + +### P1. Explicit connection state + +pyglenn exposes `connect() -> bool` and `connected -> bool`, and supports a calculator object that can exist before connection. Glenn.jl opens the database in `Calculator(path)` and has no public connected property. + +Recommended Julia decision: keep eager construction for idiomatic Julia resource handling, but add `isopen(calc)` and `isopen(tdb)` methods. Avoid introducing a disconnected calculator unless there is a concrete use case. + +### P2. Interval-boundary tolerance and deterministic selection + +pyglenn uses `temp_max + 1e-9` and orders by interval number, making shared boundaries deterministic. Glenn.jl currently uses direct inclusive comparisons without an explicit `ORDER BY` in `get_species_for_temperature`. + +Recommended Julia change: + +- Add `ORDER BY ti.interval_number LIMIT 1`. +- Use a documented tolerance, preferably a named constant. +- Add tests at the lower boundary, shared boundary, upper boundary, and values within one tolerance of a boundary. + +This is a correctness and cross-language reproducibility item. + +### P3. Specialized exception behavior + +Both projects define specialized error concepts, but Glenn.jl high-level calculations currently throw `ThermoCalcError` directly for invalid species and out-of-range temperatures. pyglenn raises `SpeciesNotFoundError` and `TemperatureOutOfRangeError` in those cases. + +Recommended Julia change: + +- Throw `SpeciesNotFoundError(species_id)` for missing IDs. +- Throw `TemperatureOutOfRangeError(T, info.name)` for invalid temperatures. +- Reserve `ThermoCalcError` for errors without a more specific domain type. +- Update tests and documentation. + +### P4. Public CLI installation contract + +pyglenn installs a `pyglenn` executable through `pyproject.toml`. Glenn.jl has `cli_main()` and `bin/glenn.jl`, but no Julia package executable declaration equivalent to Python console scripts. + +Recommended Julia decision: retain the script because Julia package executables are commonly project scripts, and document a stable invocation. If a global command is required, provide a Julia application wrapper rather than changing the library API. + +### P5. Builder default-path behavior + +The two CLIs use different defaults: Glenn.jl resolves bundled paths in its lightweight parser, while pyglenn's CLI defaults to relative `thermo.inp` and `thermo.db` for the build command. The implementation plan should define whether a future pyglenn metadata migration/build command operates on the bundled file or the current working directory. + +## 5. Proposed pyglenn implementation phases + +### Phase 0 - Compatibility contract + +- Publish the API comparison and decide whether typed dataclasses are additive or a breaking return-type change. +- Add a compatibility policy for dictionary results. +- Decide automatic versus explicit metadata migration. +- Define numerical tolerances for cross-language comparisons. + +### Phase 1 - Models and conversion boundary + +- Add `models.py` dataclasses. +- Add row-to-model conversion helpers. +- Add unit and nullable-field documentation. +- Add tests without changing existing public return types yet. + +### Phase 2 - Metadata and gas constant + +- Add constants and metadata schema. +- Add builder writes and migration. +- Add calculator `R_ref` handling. +- Regenerate or migrate the bundled database. +- Add cross-language regression values for O2 at 298.15 K, 1000 K, and a second species. + +### Phase 3 - Database query parity + +- Add `list_all_species()`. +- Add `get_species_info()`. +- Switch empty-pattern lookup and scalar calculations to the lightweight paths. +- Add query-count and not-found tests. + +### Phase 4 - Typed range API + +- Add an additive typed range method. +- Preserve the existing temperature-keyed dictionary method. +- Document ordering, duplicate temperatures, skipped values, and strict mode. +- Add performance and numerical tests. + +### Phase 5 - Public exports and documentation + +- Export constants, models, metadata helpers, and new query methods from `pyglenn.__init__`. +- Update README, Sphinx pages, examples, and the CLI help. +- Add a migration guide with old dictionary examples and new dataclass examples. + +### Phase 6 - Release and interoperability verification + +- Run pyglenn tests, Ruff, mypy, and coverage. +- Run Glenn.jl tests against the migrated database. +- Compare SQLite schemas and row counts. +- Compare Cp, H, and S at shared species/temperature points. +- Release under a minor version if compatibility adapters remain; use a major/minor policy appropriate to pyglenn's pre-1.0 status if return types change. + +## 6. Cross-language acceptance matrix + +The implementation is complete only when the following checks pass: + +| Area | Required check | +|---|---| +| Schema | Both builders create the same core tables; metadata is additive and understood by both libraries. | +| Constants | Bundled NASA data is denormalized with `8.314510` in both implementations. | +| Exact lookup | `O2`, `o2`, and partial `O` produce the same match semantics. | +| Formula lookup | Name/formula substring behavior is equivalent. | +| Boundaries | 200 K, 1000 K, and 6000 K select the same intervals for O2. | +| Missing species | Both libraries expose a clear, documented not-found result/error. | +| Out-of-range T | Both libraries expose a clear, documented range error/result policy. | +| Scalar values | Cp, H, and S agree within an explicit tolerance at shared points. | +| Range values | Valid temperatures, ordering, skipped values, and duplicate handling are documented and tested. | +| Formation enthalpy | O2 reference value and a nonzero formation-enthalpy species agree. | +| Builder | A minimal `thermo.inp` builds successfully and writes metadata. | +| Legacy DB | A pre-metadata database remains readable and can be migrated without rebuilding. | +| CLI | Build/query commands report useful errors and complete successfully. | + +## 7. Suggested test fixtures + +Use one shared small SQLite fixture and one minimal FORTRAN input fixture: + +- Species `O2`, with intervals `[200, 1000]` and `[1000, 6000]`. +- At least one species with nonzero formation enthalpy, such as `CO2`. +- A formula-search case where the formula differs from the display name. +- A database without `metadata` for migration/fallback tests. +- A database with `metadata` containing an invalid or implausible value for fallback tests. +- Temperatures `200.0`, `1000.0`, `6000.0`, `100.0`, and `7000.0`. +- A range containing valid, invalid, duplicate, and integer-valued temperatures. + +Numerical comparison tests should use absolute and relative tolerances appropriate to the polynomial evaluation and the selected gas constant. Do not compare rounded display output. + +## 8. Risks and decisions + +- **Return-type compatibility:** replacing dictionaries outright can break downstream Python users. Prefer additive typed APIs or a deprecation period. +- **SQLite metadata typing:** keep `metadata.value` as `TEXT` because it is a generic key/value table; parse the gas constant at read time and validate its range. +- **Automatic migration:** silently changing a user's database is surprising. Prefer an explicit migration API, with a clear warning or CLI command. +- **Shared database correctness:** the gas constant is scientific data, not merely configuration. The metadata source and fallback policy must be documented and tested. +- **Boundary behavior:** a tolerance can accept values slightly outside the declared interval. Keep it small, name it, and test it explicitly. +- **Performance:** typed conversion at the database boundary should not cause one query per temperature. Range calculations must reuse loaded intervals. +- **Versioning:** pyglenn is pre-1.0, but its users still need a migration note when return shapes or constants change. + +## 9. Short-term recommendation + +Implement G2 first, because it affects every denormalized thermodynamic result. Then implement G1 and G4, which establish a stable typed/query boundary. Implement G3 and G5 afterward as performance and API improvements. In parallel, apply P2 and P3 to Glenn.jl so both libraries agree on interval boundaries and exception semantics before numerical cross-validation is used as a release gate. diff --git a/README.md b/README.md index 323a291..03a49c3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Computes **Cp(T)**, **H°(T)**, **S°(T)** from NASA polynomial coefficients sto - Enthalpy of formation lookup - Enthalpy change between two temperatures - Command-line interface -- ~2030 species, 3772 temperature intervals +- 2,035 species, 3,779 temperature intervals ## Installation @@ -60,10 +60,10 @@ species = calc.get_available_species('O2', exact_match=True) o2 = species[0] # Calculate properties at 1000 K -props = calc.calculate_properties(o2['id'], 1000.0) -print(f"Cp = {props['cp']:.2f} J/(mol·K)") -print(f"H° = {props['h_relative']:.1f} J/mol") -print(f"S° = {props['s']:.3f} J/(mol·K)") +props = calc.calculate_properties(o2.id, 1000.0) +print(f"Cp = {props.cp:.2f} J/(mol·K)") +print(f"H° = {props.h_relative:.1f} J/mol") +print(f"S° = {props.s:.3f} J/(mol·K)") calc.close() ``` @@ -76,8 +76,8 @@ from pyglenn import ThermochemicalCalculator with ThermochemicalCalculator() as calc: # exact_match=True guarantees the correct species (case-insensitive) species = calc.get_available_species('CH4', exact_match=True) - props = calc.calculate_properties(species[0]['id'], 500.0) - print(f"Cp = {props['cp']:.2f} J/(mol·K)") + props = calc.calculate_properties(species[0].id, 500.0) + print(f"Cp = {props.cp:.2f} J/(mol·K)") ``` ### CLI @@ -102,6 +102,16 @@ pyglenn build -i thermo.inp -o thermo.db | `temperature_intervals` | Valid T ranges per species | | `coefficients` | NASA-7 polynomial coefficients (a1–a7, b1, b2) | | `file_metadata` | Global file metadata | +| `metadata` | Dataset metadata, including its reference gas constant | + +## Gas Constants + +`R_UNIVERSAL = 8.31446261815324 J/(mol·K)` is the universal CODATA value and +remains available through the compatibility alias `R`. NASA Glenn/CEA +polynomials in newly built databases declare their fitted reference constant as +`R_GLENN = 8.314510 J/(mol·K)` in SQLite metadata. The calculator reads that +value automatically when it connects; legacy databases fall back to +`R_UNIVERSAL` until explicitly migrated. ## Citing diff --git a/docs/COMPATIBILITY_CONTRACT.md b/docs/COMPATIBILITY_CONTRACT.md new file mode 100644 index 0000000..29ca648 --- /dev/null +++ b/docs/COMPATIBILITY_CONTRACT.md @@ -0,0 +1,93 @@ +# Contrato de Compatibilidade — pyglenn 0.2.0 + +**Data:** 2026-08-22 +**Base:** auditoria Glenn.jl v0.4.0 → pyglenn (`PYGLENN_PARITY_PLAN.md` / `PLANO_PARIDADE_PYGLENN.md`) + +Este documento registra as decisões da **Etapa 0** (contrato de compatibilidade). Ele +é autoritativo sobre as etapas seguintes: qualquer implementação que contradiga uma +decisão aqui deve ser considerada bug. + +--- + +## D1. API tipada — BREAKING (G1) + +Os métodos públicos passam a retornar **dataclasses** (`src/pyglenn/models.py`) em +vez de `dict`. Não haverá camada de compatibilidade que mantenha `dict` como retorno +padrão. + +| Método atual | Retorno novo | +|---|---| +| `ThermochemicalCalculator.calculate_properties` | `ThermoProperties` | +| `ThermochemicalCalculator.calculate_properties_range` (novo) | `list[ThermoProperties]` | +| `ThermochemicalCalculator.get_properties_range` | `dict[float, ThermoProperties]` | +| `ThermochemicalCalculator.get_available_species` | `list[SpeciesInfo]` | +| `ThermoDBQuery.find_species` | `list[SpeciesInfo]` | +| `ThermoDBQuery.get_species_data` | `SpeciesData` (espécie + `list[IntervalData]`) | +| `ThermoDBQuery.get_species_info` (novo) | `SpeciesInfo | None` | +| `ThermoDBQuery.get_species_for_temperature` | `IntervalData | None` | +| `ThermoDBQuery.list_species_page` | `tuple[list[SpeciesInfo], int]` | +| `ThermoDBQuery.list_all_species` (novo) | `list[SpeciesInfo]` | +| `ThermoDBQuery.get_statistics` | `DatabaseStats` | + +- Dataclasses `frozen` e com `Optional` preciso para campos nulos (`formula`, + `molecular_weight`, `heat_of_formation_298K`, `h_298_to_0`). +- Campos nulos permanecem `None`; nunca viram `0.0` implicitamente. +- `dataclasses.asdict()` é o mecanismo documentado para JSON/serialização. +- **Entradas** de coeficientes (ex.: `calculate_cp/h/s`) continuam aceitando + `dict` **ou** `NASACoefficients` — a mudança é só no **retorno**. + +## D2. Política de compatibilidade para resultados dict + +- Resultados `dict` são **removidos** dos métodos públicos; não há período de + deprecação por retorno duplo. +- Migração de consumidores via `CHANGELOG` + guia de migração na documentação + (exemplos "antes/depois" com `dict` e com dataclass). +- `dict` continua existindo apenas como formato de **entrada** aceito (coeficientes). + +## D3. Migração de metadados — AUTOMÁTICA no connect (G2/G6) + +- `ThermoDBQuery.connect()` detecta banco legado (sem a tabela `metadata`) e aplica + a migração **automaticamente**, gravando as chaves canônicas. +- Escopo da escrita: **somente** a tabela aditiva `metadata`. Nunca altera + `species`, `temperature_intervals` ou `coefficients`. +- O fallback para `R_UNIVERSAL` passa a ocorrer **apenas** quando o valor de + `gas_constant_ref` está ausente, inválido ou implausível — não mais por ausência + da tabela. +- `migrate_metadata()` permanece público e idempotente (a automação o invoca). +- **Impacto em testes existentes:** `test_reference_gas_constant_legacy_fallback_and_migration` + deve ser atualizado (o fallback legado por ausência de tabela deixa de existir). + +## D4. Tolerâncias numéricas cross-language + +| Grandeza | Tolerância | +|---|---| +| Cp, S (J/(mol·K)) | `rel_tol=1e-9`, `abs_tol=1e-6` | +| H (J/mol) | `rel_tol=1e-9`, `abs_tol=1e-6` | +| ΔH°f de O2 (= 0) | `abs_tol=1.0` J/mol | + +- Comparações usam `math.isclose(..., rel_tol=..., abs_tol=...)`; nunca comparam + saída arredondada de display. +- `rel_tol=1e-9` é folgado o bastante para diferenças de ordem de avaliação entre + Julia e Python, e estrito o bastante para detectar o viés de ~5,7 ppm (5,7e-6). + +## D5. Valor canônico da constante de referência + +- A chave `gas_constant_ref` deve ser gravada com a string literal **`'8.314510'`** + (não `str(R_GLENN)`, que produziria `'8.31451'`). +- `gas_constant_ref_source = 'NASA Glenn/CEA thermo.inp (CODATA 1986)'`. +- `R_GLENN = 8.314510` (float) continua sendo o valor numérico usado nos cálculos; + a decisão aqui é sobre a **representação textual persistida**. + +## Versionamento + +- Pré-1.0: mudança de retorno é **breaking** → bump de minor: `0.1.13 → 0.2.0`. +- `pyproject.toml` (dynamic via `__init__.__version__`), `conda.recipe/meta.yaml` e + distribuições geradas sobem juntos. + +## Critérios de aceite do contrato + +1. Métodos públicos retornam as dataclasses listadas em D1. +2. `dataclasses.asdict()` reconstrói o `dict` equivalente antigo (mesmas chaves). +3. `connect()` migra automaticamente banco legado sem tocar dados termoquímicos. +4. Tolerâncias D4 aplicadas nos testes cross-language. +5. `gas_constant_ref` persistida como `'8.314510'`. diff --git a/docs/DATABASE_DOCUMENTATION.txt b/docs/DATABASE_DOCUMENTATION.txt index 4a5c9e8..6781e88 100644 --- a/docs/DATABASE_DOCUMENTATION.txt +++ b/docs/DATABASE_DOCUMENTATION.txt @@ -115,10 +115,10 @@ SQL QUERY EXAMPLES: LOADING INFORMATION: -• Total Species Loaded: 2030 -• Total Temperature Intervals: 3772 -• Total Coefficient Sets: 3772 -• Skipped Data Lines: 100 (malformed or inconsistent lines) +• Total Species Loaded: 2035 +• Total Temperature Intervals: 3779 +• Total Coefficient Sets: 3779 +• Duplicate Species Skipped: 11 (already present) ================================================================================ diff --git a/docs/README.txt b/docs/README.txt index 60e8487..4622df5 100644 --- a/docs/README.txt +++ b/docs/README.txt @@ -21,7 +21,7 @@ PROJECT FILES: 3. thermo.inp - Original file with thermochemical data (FORTRAN format) - - Contains ~2030 species and their polynomial coefficients + - Contains 2,035 species and their polynomial coefficients 4. thermo_to_sqlite.py - Python script that converts thermo.inp to SQLite3 @@ -30,8 +30,8 @@ PROJECT FILES: 5. thermo.db - Generated SQLite3 database - - 2030 species, 3772 temperature intervals - - 3772 polynomial coefficient sets + - 2,035 species, 3,779 temperature intervals + - 3,779 polynomial coefficient sets 6. query_thermo_db.py - Example script with queries and calculations @@ -53,17 +53,17 @@ DATABASE STRUCTURE: └─────────────────┘ ↓ ┌─────────────────┐ - │ species │ (2030 chemical species) + │ species │ (2,035 chemical species) │ (id, name...) │ └─────────────────┘ ↓ ┌────────────────────────────┐ - │ temperature_intervals │ (3772 intervals) + │ temperature_intervals │ (3,779 intervals) │ (species_id, temp_min/max) │ └────────────────────────────┘ ↓ ┌──────────────────────────────┐ - │ coefficients │ (3772 sets) + │ coefficients │ (3,779 sets) │ (a1-a7, b1, b2 coefficients) │ └──────────────────────────────┘ @@ -101,15 +101,15 @@ HOW TO USE: DATA LOADED: -✓ Species: 2030 - - Gaseous: 1264 - - Condensed: 766 +✓ Species: 2035 + - Gaseous: 1266 + - Condensed: 769 -✓ Temperature Intervals: 3772 +✓ Temperature Intervals: 3779 - Global range: 200 K to 20000 K - Multiple intervals per species for better accuracy -✓ Polynomial Coefficients: 3772 sets +✓ Polynomial Coefficients: 3779 sets - 7 coefficients (a1-a7) for Cp - 2 integration constants (b1, b2) @@ -162,13 +162,13 @@ db.connect() # Search for species species = db.find_species('O2') -print(species[0]['name']) # O2 +print(species[0].name) # O2 # Get species data -data = db.get_species_data(species[0]['id']) +data = db.get_species_data(species[0].id) # Calculate Cp at 1000 K -coeffs = data['intervals'][0]['coefficients'] +coeffs = data.intervals[0].coefficients cp_r = db.calculate_cp(coeffs, 1000) print(f"Cp(1000K)/R = {cp_r}") diff --git a/docs/audit/audit_code.py b/docs/audit/audit_code.py index d47ecc8..336b8ba 100644 --- a/docs/audit/audit_code.py +++ b/docs/audit/audit_code.py @@ -166,7 +166,7 @@ def collect_data() -> dict[str, dict[str, Any]]: log.info('Connected to pyglenn database (bundled thermo.db)') stats = calc.db.get_statistics() log.info('DB stats: %d species, %d intervals', - stats['total_species'], stats['total_intervals']) + stats.total_species, stats.total_intervals) for py_name, plot_label, _ in SPECIES: log.info('▶ Processing %s (%s)', py_name, plot_label) @@ -175,14 +175,14 @@ def collect_data() -> dict[str, dict[str, Any]]: found = calc.get_available_species(py_name, exact_match=True) # Filter by phase in case multiple phases exist (e.g. gas + liquid) target = next( - (sp for sp in found if sp['phase'] == 'gas'), None + (sp for sp in found if sp.phase == 'gas'), None ) if target is None: log.warning(' ✗ %s not found in pyglenn database!', py_name) continue - sid = target['id'] - log.info(' ✓ ID: %d | MW: %s', sid, target['molecular_weight']) + sid = target.id + log.info(' ✓ ID: %d | MW: %s', sid, target.molecular_weight) for T in TEMPERATURES: Tk = round(float(T), 2) @@ -196,11 +196,11 @@ def collect_data() -> dict[str, dict[str, Any]]: except ThermoCalcError: continue - dh_py = props['h_relative'] - ref['coef']['H'] * 1000.0 + dh_py = props.h_relative - ref['coef']['H'] * 1000.0 results[py_name][Tk] = { 'nist': {'cp': ref['cp'], 'dh_298': ref['dh_298'], 's': ref['s']}, - 'pyglenn': {'cp': props['cp'], 'dh_298': dh_py, 's': props['s']}, + 'pyglenn': {'cp': props.cp, 'dh_298': dh_py, 's': props.s}, } n_pts = len(results[py_name]) diff --git a/docs/audit/pyglenn_vs_nist.csv b/docs/audit/pyglenn_vs_nist.csv index a1577c2..c3ca1e7 100644 --- a/docs/audit/pyglenn_vs_nist.csv +++ b/docs/audit/pyglenn_vs_nist.csv @@ -1,272 +1,382 @@ Species,T(K),Cp_NIST,Cp_pyglenn,Cp_abs_err,Cp_rel_err(%),dH_NIST,dH_pyglenn,dH_abs_err,dH_rel_err(%),S_NIST,S_pyglenn,S_abs_err,S_rel_err(%) -CO,300.00,29.145012,29.142680,-0.002332,-0.0080,57.148703,46.446755,-10.701948,-18.7265,197.843179,197.838958,-0.004221,-0.0021 -CO,350.00,29.152958,29.210656,0.057698,0.1979,1513.824988,1505.044071,-8.780917,-0.5800,202.334115,202.335700,0.001586,0.0008 -CO,400.00,29.302705,29.341159,0.038454,0.1312,2974.776902,2968.562813,-6.214088,-0.2089,206.235550,206.244021,0.008471,0.0041 -CO,450.00,29.535511,29.537485,0.001974,0.0067,4445.466052,4440.262112,-5.203939,-0.1171,209.699726,209.710618,0.010893,0.0052 -CO,500.00,29.819490,29.793517,-0.025974,-0.0871,5929.174172,5923.309104,-5.865068,-0.0989,212.825948,212.835473,0.009526,0.0045 -CO,550.00,30.135685,30.098093,-0.037592,-0.1247,7427.948442,7420.423974,-7.524468,-0.1013,215.682677,215.689049,0.006371,0.0030 -CO,600.00,30.471890,30.437939,-0.033951,-0.1114,8943.074146,8933.706304,-9.367842,-0.1047,218.319129,218.322290,0.003161,0.0014 -CO,650.00,30.819669,30.799564,-0.020106,-0.0652,10475.329180,10464.580256,-10.748924,-0.1026,220.771860,220.772802,0.000943,0.0004 -CO,700.00,31.172796,31.170487,-0.002309,-0.0074,12025.129486,12013.816615,-11.312871,-0.0941,223.068749,223.068848,0.000099,0.0000 -CO,750.00,31.526403,31.540055,0.013653,0.0433,13592.616313,13581.605065,-11.011248,-0.0810,225.231517,225.232026,0.000508,0.0002 -CO,800.00,31.876488,31.899981,0.023493,0.0737,15177.710752,15167.659899,-10.050852,-0.0662,227.277389,227.279134,0.001745,0.0008 -CO,850.00,32.219621,32.244723,0.025103,0.0779,16780.149027,16771.348434,-8.800593,-0.0524,229.220228,229.223488,0.003260,0.0014 -CO,900.00,32.552754,32.571747,0.018992,0.0583,18399.506023,18391.835140,-7.670882,-0.0417,231.071335,231.075888,0.004553,0.0020 -CO,950.00,32.873109,32.881708,0.008599,0.0262,20035.211376,20028.236878,-6.974498,-0.0348,232.840018,232.845327,0.005309,0.0023 -CO,1000.00,33.178096,33.178593,0.000497,0.0015,21686.560750,21679.786090,-6.774660,-0.0312,234.534014,234.539529,0.005516,0.0024 -CO,1050.00,33.465260,33.453519,-0.011741,-0.0351,23352.723876,23345.668425,-7.055451,-0.0302,236.159804,236.165048,0.005244,0.0022 -CO,1100.00,33.732247,33.709763,-0.022484,-0.0667,25032.750413,25024.826464,-7.923950,-0.0317,237.722853,237.727291,0.004438,0.0019 -CO,1150.00,33.976778,33.948277,-0.028501,-0.0839,26725.574270,26716.349120,-9.225150,-0.0345,239.227793,239.231075,0.003282,0.0014 -CO,1200.00,34.196629,34.170143,-0.026486,-0.0775,28430.016851,28419.376642,-10.640209,-0.0374,240.678568,240.680646,0.002077,0.0009 -CO,1250.00,34.389621,34.376491,-0.013130,-0.0382,30144.789523,30133.104812,-11.684711,-0.0388,242.078550,242.079773,0.001223,0.0005 -CO,1300.00,34.553607,34.568443,0.014836,0.0429,31868.495513,31856.785850,-11.709663,-0.0367,243.430629,243.431829,0.001200,0.0005 -CO,1350.00,34.762624,34.747084,-0.015540,-0.0447,33601.816100,33589.727294,-12.088806,-0.0360,244.740107,244.739843,-0.000265,-0.0001 -CO,1400.00,34.929521,34.913449,-0.016073,-0.0460,35344.180302,35331.289709,-12.890594,-0.0365,246.007403,246.006555,-0.000848,-0.0003 -CO,1450.00,35.082828,35.068505,-0.014323,-0.0408,37094.541904,37080.883728,-13.658176,-0.0368,247.235837,247.234450,-0.001387,-0.0006 -CO,1500.00,35.224238,35.213156,-0.011083,-0.0315,38852.264969,38837.966802,-14.298167,-0.0368,248.427614,248.425793,-0.001821,-0.0007 -CO,1550.00,35.355179,35.348237,-0.006942,-0.0196,40616.791349,40602.039842,-14.751508,-0.0363,249.584773,249.582654,-0.002118,-0.0008 -CO,1600.00,35.476859,35.474520,-0.002339,-0.0066,42387.628615,42372.643916,-14.984698,-0.0354,250.709198,250.706931,-0.002267,-0.0009 -CO,1650.00,35.590309,35.592713,0.002404,0.0068,44164.340169,44149.357065,-14.983104,-0.0339,251.802636,251.800370,-0.002266,-0.0009 -CO,1700.00,35.696411,35.703467,0.007056,0.0198,45946.537115,45931.791287,-14.745828,-0.0321,252.866706,252.864581,-0.002125,-0.0008 -CO,1750.00,35.795925,35.807377,0.011452,0.0320,47733.871513,47719.589720,-14.281792,-0.0299,253.902907,253.901051,-0.001856,-0.0007 -CO,1800.00,35.889507,35.904989,0.015482,0.0431,49526.030763,49512.424028,-13.606734,-0.0275,254.912635,254.911159,-0.001476,-0.0006 -CO,1850.00,35.977729,35.996802,0.019074,0.0530,51322.732896,51309.991982,-12.740915,-0.0248,255.897185,255.896183,-0.001002,-0.0004 -CO,1900.00,36.061088,36.083274,0.022186,0.0615,53123.722615,53112.015241,-11.707374,-0.0220,256.857765,256.857314,-0.000451,-0.0002 -CO,1950.00,36.140021,36.164822,0.024801,0.0686,54928.767934,54918.237321,-10.530613,-0.0192,257.795499,257.795659,0.000160,0.0001 -CO,2000.00,36.214911,36.241828,0.026917,0.0743,56737.657333,56728.421731,-9.235603,-0.0163,258.711437,258.712253,0.000816,0.0003 -CO,2050.00,36.286097,36.314643,0.028545,0.0787,58550.197323,58542.350272,-7.847051,-0.0134,259.606561,259.608062,0.001502,0.0006 -CO,2100.00,36.353880,36.383586,0.029706,0.0817,60366.210358,60359.821489,-6.388869,-0.0106,260.481787,260.483991,0.002204,0.0008 -CO,2150.00,36.418525,36.448951,0.030426,0.0835,62185.533042,62180.649255,-4.883787,-0.0079,261.337975,261.340888,0.002913,0.0011 -CO,2200.00,36.480271,36.511007,0.030736,0.0843,64008.014578,64004.661477,-3.353101,-0.0052,262.175932,262.179548,0.003616,0.0014 -CO,2250.00,36.539331,36.570000,0.030668,0.0839,65833.515425,65831.698923,-1.816502,-0.0028,262.996414,263.000721,0.004307,0.0016 -CO,2300.00,36.595896,36.626154,0.030258,0.0827,67661.906130,67661.614150,-0.291980,-0.0004,263.800132,263.805109,0.004977,0.0019 -CO,2350.00,36.650137,36.679678,0.029541,0.0806,69493.066309,69494.270522,1.204213,0.0017,264.587756,264.593377,0.005621,0.0021 -CO,2400.00,36.702211,36.730762,0.028550,0.0778,71326.883752,71329.541317,2.657565,0.0037,265.359916,265.366149,0.006233,0.0023 -CO,2450.00,36.752258,36.779578,0.027321,0.0743,73163.253647,73167.308917,4.055270,0.0055,266.117207,266.124017,0.006809,0.0026 -CO,2500.00,36.800405,36.826289,0.025884,0.0703,75002.077885,75007.464059,5.386174,0.0072,266.860190,266.867537,0.007347,0.0028 -CO,2550.00,36.846770,36.871040,0.024271,0.0659,76843.264457,76849.905162,6.640704,0.0086,267.589395,267.597239,0.007844,0.0029 -CO,2600.00,36.891457,36.913968,0.022510,0.0610,78686.726914,78694.537698,7.810784,0.0099,268.305324,268.313623,0.008298,0.0031 -CO,2650.00,36.934566,36.955196,0.020630,0.0559,80532.383889,80541.273633,8.889745,0.0110,269.008452,269.017161,0.008709,0.0032 -CO,2700.00,36.976184,36.994840,0.018655,0.0505,82380.158677,82390.030908,9.872230,0.0120,269.699227,269.708304,0.009077,0.0034 -CO,2750.00,37.016395,37.033004,0.016609,0.0449,84229.978857,84240.732959,10.754102,0.0128,270.378078,270.387479,0.009400,0.0035 -CO,2800.00,37.055273,37.069787,0.014514,0.0392,86081.775951,86093.308292,11.532341,0.0134,271.045410,271.055091,0.009681,0.0036 -CO,2850.00,37.092889,37.105277,0.012388,0.0334,87935.485128,87947.690083,12.204956,0.0139,271.701606,271.711525,0.009919,0.0037 -CO,2900.00,37.129308,37.139558,0.010249,0.0276,89791.044928,89803.815816,12.770889,0.0142,272.347034,272.357150,0.010116,0.0037 -CO,2950.00,37.164591,37.172705,0.008114,0.0218,91648.397023,91661.626952,13.229929,0.0144,272.982040,272.992313,0.010273,0.0038 -CO,3000.00,37.198793,37.204790,0.005998,0.0161,93507.486000,93521.068627,13.582627,0.0145,273.606958,273.617350,0.010392,0.0038 -CO2,300.00,37.217621,37.219897,0.002276,0.0061,65.851354,83.420960,17.569607,26.6807,214.017582,214.016154,-0.001429,-0.0007 -CO2,350.00,39.410968,39.386623,-0.024345,-0.0618,1982.881127,1999.572651,16.691524,0.8418,219.923470,219.919384,-0.004086,-0.0019 -CO2,400.00,41.336224,41.324893,-0.011331,-0.0274,4002.512250,4018.263481,15.751230,0.3935,225.314294,225.307676,-0.006617,-0.0029 -CO2,450.00,43.058521,43.061259,0.002738,0.0064,6113.135222,6128.696894,15.561671,0.2546,230.284214,230.277134,-0.007080,-0.0031 -CO2,500.00,44.614984,44.624202,0.009218,0.0207,8305.607464,8321.501587,15.894124,0.1914,234.902775,234.896389,-0.006386,-0.0027 -CO2,550.00,46.029256,46.038029,0.008773,0.0191,10572.267471,10588.634538,16.367066,0.1548,239.222420,239.216936,-0.005484,-0.0023 -CO2,600.00,47.317934,47.322286,0.004352,0.0092,12906.441899,12923.147229,16.705330,0.1294,243.283601,243.278709,-0.004892,-0.0020 -CO2,650.00,48.493692,48.492498,-0.001194,-0.0025,15302.180135,15318.964104,16.783970,0.1097,247.118201,247.113438,-0.004763,-0.0019 -CO2,700.00,49.566894,49.561082,-0.005812,-0.0117,17754.102636,17770.704519,16.601883,0.0935,250.751850,250.746819,-0.005031,-0.0020 -CO2,750.00,50.546488,50.538133,-0.008355,-0.0165,20257.309925,20273.547694,16.237770,0.0802,254.205531,254.199998,-0.005533,-0.0022 -CO2,800.00,51.440518,51.432051,-0.008467,-0.0165,22807.325715,22823.132974,15.807259,0.0693,257.496705,257.490617,-0.006088,-0.0024 -CO2,850.00,52.256436,52.250011,-0.006425,-0.0123,25400.060111,25415.487337,15.427226,0.0607,260.640126,260.633577,-0.006550,-0.0025 -CO2,900.00,53.001290,52.998319,-0.002970,-0.0056,28031.785068,28046.973540,15.188472,0.0542,263.648428,263.641604,-0.006824,-0.0026 -CO2,950.00,53.681849,53.682686,0.000836,0.0016,30699.117611,30714.253785,15.136173,0.0493,266.532567,266.525686,-0.006881,-0.0026 -CO2,1000.00,54.304689,54.308423,0.003734,0.0069,33399.008083,33414.265099,15.257015,0.0457,269.302156,269.295399,-0.006758,-0.0025 -CO2,1050.00,54.876239,54.890133,0.013894,0.0253,36128.731749,36144.461067,15.729318,0.0435,271.965716,271.959417,-0.006299,-0.0023 -CO2,1100.00,55.402827,55.419708,0.016881,0.0305,38885.882692,38902.410291,16.527599,0.0425,274.530875,274.525319,-0.005557,-0.0020 -CO2,1150.00,55.890702,55.903492,0.012789,0.0229,41668.369304,41685.669250,17.299945,0.0415,277.004524,276.999654,-0.004869,-0.0018 -CO2,1200.00,56.346054,56.346845,0.000791,0.0014,44474.410901,44492.086069,17.675168,0.0397,279.392936,279.388388,-0.004548,-0.0016 -CO2,1250.00,56.746881,56.754336,0.007455,0.0131,47297.887558,47319.756424,21.868867,0.0462,281.695869,281.696954,0.001085,0.0004 -CO2,1300.00,57.140892,57.129873,-0.011019,-0.0193,50145.241627,50166.987375,21.745748,0.0434,283.929324,283.930315,0.000991,0.0003 -CO2,1350.00,57.499255,57.476826,-0.022428,-0.0390,53011.383156,53032.267562,20.884405,0.0394,286.092664,286.093006,0.000342,0.0001 -CO2,1400.00,57.826760,57.798115,-0.028645,-0.0495,55894.653245,55914.242479,19.589235,0.0350,288.189783,288.189183,-0.000600,-0.0002 -CO2,1450.00,58.127392,58.096279,-0.031113,-0.0535,58793.611610,58811.693824,18.082214,0.0308,290.224317,290.222660,-0.001657,-0.0006 -CO2,1500.00,58.404486,58.373540,-0.030945,-0.0530,61707.000396,61723.522140,16.521745,0.0268,292.199659,292.196944,-0.002715,-0.0009 -CO2,1550.00,58.660854,58.631852,-0.029002,-0.0494,64633.714983,64648.732163,15.017180,0.0232,294.118965,294.115263,-0.003702,-0.0013 -CO2,1600.00,58.898880,58.872937,-0.025942,-0.0440,67572.780273,67586.420361,13.640088,0.0202,295.985175,295.980598,-0.004577,-0.0015 -CO2,1650.00,59.120596,59.098322,-0.022274,-0.0377,70523.331287,70535.764309,12.433022,0.0176,297.801024,297.795704,-0.005320,-0.0018 -CO2,1700.00,59.327744,59.309363,-0.018381,-0.0310,73484.597182,73496.013567,11.416385,0.0155,299.569060,299.563133,-0.005927,-0.0020 -CO2,1750.00,59.521825,59.507273,-0.014553,-0.0244,76455.888013,76466.481829,10.593816,0.0139,301.291655,301.285250,-0.006404,-0.0021 -CO2,1800.00,59.704140,59.693136,-0.011004,-0.0184,79436.583702,79446.540132,9.956429,0.0125,302.971019,302.964255,-0.006764,-0.0022 -CO2,1850.00,59.875817,59.867927,-0.007890,-0.0132,82426.124788,82435.610948,9.486159,0.0115,304.609215,304.602194,-0.007021,-0.0023 -CO2,1900.00,60.037845,60.032527,-0.005318,-0.0089,85424.004648,85433.163053,9.158405,0.0107,306.208170,306.200973,-0.007196,-0.0024 -CO2,1950.00,60.191088,60.187728,-0.003360,-0.0056,88429.762905,88438.707025,8.944120,0.0101,307.769682,307.762374,-0.007308,-0.0024 -CO2,2000.00,60.336311,60.334253,-0.002058,-0.0034,91442.979833,91451.791298,8.811464,0.0096,309.295435,309.288060,-0.007375,-0.0024 -CO2,2050.00,60.474187,60.472757,-0.001429,-0.0024,94463.271576,94471.998691,8.727115,0.0092,310.787006,310.779589,-0.007417,-0.0024 -CO2,2100.00,60.605314,60.603839,-0.001475,-0.0024,97490.286050,97498.943343,8.657293,0.0089,312.245872,312.238422,-0.007450,-0.0024 -CO2,2150.00,60.730226,60.728045,-0.002182,-0.0036,100523.699419,100532.267998,8.568579,0.0085,313.673421,313.665929,-0.007492,-0.0024 -CO2,2200.00,60.849400,60.845877,-0.003523,-0.0058,103563.213052,103571.641594,8.428542,0.0081,315.070955,315.063398,-0.007557,-0.0024 -CO2,2250.00,60.963262,60.957796,-0.005466,-0.0090,106608.550880,106616.757123,8.206243,0.0077,316.439699,316.432042,-0.007656,-0.0024 -CO2,2300.00,61.072196,61.064227,-0.007969,-0.0130,109659.457107,109667.329719,7.872612,0.0072,317.780806,317.773003,-0.007803,-0.0025 -CO2,2350.00,61.176551,61.165564,-0.010987,-0.0180,112715.694205,112723.094949,7.400744,0.0066,319.095363,319.087357,-0.008006,-0.0025 -CO2,2400.00,61.276641,61.262170,-0.014471,-0.0236,115777.041165,115783.807280,6.766115,0.0058,320.384395,320.376122,-0.008273,-0.0026 -CO2,2450.00,61.372750,61.354382,-0.018368,-0.0299,118843.291956,118849.238708,5.946751,0.0050,321.648870,321.640259,-0.008611,-0.0027 -CO2,2500.00,61.465139,61.442516,-0.022624,-0.0368,121914.254179,121919.177521,4.923342,0.0040,322.889702,322.880678,-0.009024,-0.0028 -CO2,2550.00,61.554047,61.526863,-0.027183,-0.0442,124989.747868,124993.427187,3.679320,0.0029,324.107756,324.098240,-0.009517,-0.0029 -CO2,2600.00,61.639689,61.607700,-0.031990,-0.0519,128069.604436,128071.805350,2.200914,0.0017,325.303852,325.293761,-0.010091,-0.0031 -CO2,2650.00,61.722268,61.685281,-0.036987,-0.0599,131153.665741,131154.142918,0.477177,0.0004,326.478765,326.468018,-0.010747,-0.0033 -CO2,2700.00,61.801967,61.759849,-0.042118,-0.0681,134241.783252,134240.283247,-1.500006,-0.0011,327.633233,327.621747,-0.011486,-0.0035 -CO2,2750.00,61.878957,61.831630,-0.047327,-0.0765,137333.817308,137330.081391,-3.735917,-0.0027,328.767954,328.755648,-0.012306,-0.0037 -CO2,2800.00,61.953396,61.900840,-0.052557,-0.0848,140429.636455,140423.403430,-6.233025,-0.0044,329.883593,329.870387,-0.013206,-0.0040 -CO2,2850.00,62.025431,61.967679,-0.057752,-0.0931,143529.116856,143520.125852,-8.991004,-0.0063,330.980781,330.966598,-0.014182,-0.0043 -CO2,2900.00,62.095198,62.032340,-0.062859,-0.1012,146632.141758,146620.134995,-12.006763,-0.0082,332.060119,332.044888,-0.015231,-0.0046 -CO2,2950.00,62.162826,62.095004,-0.067822,-0.1091,149738.601017,149723.326539,-15.274478,-0.0102,333.122181,333.105832,-0.016348,-0.0049 -CO2,3000.00,62.228433,62.155846,-0.072587,-0.1166,152848.390667,152829.605039,-18.785628,-0.0123,334.167511,334.149983,-0.017528,-0.0052 -H2O,500.00,35.218362,35.224633,0.006272,0.0178,6924.644792,6926.490153,1.845361,0.0266,206.534070,206.528276,-0.005794,-0.0028 -H2O,550.00,35.754757,35.762118,0.007362,0.0206,8698.846499,8701.042541,2.196042,0.0252,209.915654,209.910527,-0.005127,-0.0024 -H2O,600.00,36.317861,36.323688,0.005827,0.0160,10500.564355,10503.101322,2.536968,0.0242,213.050698,213.046165,-0.004532,-0.0021 -H2O,650.00,36.901741,36.903723,0.001982,0.0054,12330.978122,12333.717798,2.739676,0.0222,215.980611,215.976405,-0.004206,-0.0019 -H2O,700.00,37.501847,37.498953,-0.002893,-0.0077,14191.008408,14193.726144,2.717736,0.0192,218.737198,218.732962,-0.004236,-0.0019 -H2O,750.00,38.114484,38.107415,-0.007069,-0.0185,16081.371365,16083.833368,2.462003,0.0153,221.345386,221.340799,-0.004587,-0.0021 -H2O,800.00,38.736498,38.727687,-0.008811,-0.0227,18002.612885,18004.664497,2.051612,0.0114,223.825052,223.819936,-0.005116,-0.0023 -H2O,850.00,39.365093,39.358348,-0.006745,-0.0171,19955.130721,19956.775450,1.644729,0.0082,226.192273,226.186662,-0.005610,-0.0025 -H2O,900.00,39.997715,39.997579,-0.000137,-0.0003,21939.189237,21940.642523,1.453286,0.0066,228.460213,228.454382,-0.005831,-0.0026 -H2O,950.00,40.631976,40.642868,0.010892,0.0268,23954.929498,23956.634802,1.705304,0.0071,230.639768,230.634207,-0.005561,-0.0024 -H2O,1000.00,41.265608,41.290801,0.025193,0.0611,26002.376333,26004.973994,2.597660,0.0100,232.740035,232.735386,-0.004649,-0.0020 -H2O,1050.00,41.896427,41.931099,0.034672,0.0828,28081.443384,28085.495851,4.052467,0.0144,234.768671,234.765439,-0.003232,-0.0014 -H2O,1100.00,42.522312,42.573747,0.051434,0.1210,30191.936771,30198.122023,6.185252,0.0205,236.732161,236.730911,-0.001250,-0.0005 -H2O,1150.00,43.141191,43.212401,0.071211,0.1651,32333.557810,32342.802991,9.245181,0.0286,238.636036,238.637503,0.001467,0.0006 -H2O,1200.00,43.751023,43.842522,0.091499,0.2091,34505.905041,34519.219207,13.314166,0.0386,240.485029,240.489957,0.004927,0.0020 -H2O,1250.00,44.349797,44.460878,0.111081,0.2505,36708.475779,36726.858572,18.382793,0.0501,242.283213,242.292276,0.009063,0.0037 -H2O,1300.00,44.935524,45.065203,0.129679,0.2886,38940.667300,38965.072775,24.405474,0.0627,244.034101,244.047885,0.013785,0.0056 -H2O,1350.00,45.506227,45.653940,0.147712,0.3246,41201.777768,41233.118785,31.341017,0.0761,245.740734,245.759751,0.019018,0.0077 -H2O,1400.00,46.059947,46.226060,0.166114,0.3606,43491.006960,43530.189606,39.182645,0.0901,247.405750,247.430469,0.024719,0.0100 -H2O,1450.00,46.594731,46.780932,0.186201,0.3996,45807.456845,45855.437181,47.980336,0.1047,249.031441,249.062333,0.030892,0.0124 -H2O,1500.00,47.108636,47.318216,0.209580,0.4449,48150.132042,48207.989545,57.857503,0.1202,250.619798,250.657385,0.037586,0.0150 -H2O,1550.00,47.599727,47.837798,0.238072,0.5002,50517.940193,50586.963669,69.023477,0.1366,252.172554,252.217461,0.044906,0.0178 -H2O,1600.00,48.066071,48.339729,0.273658,0.5693,52909.692266,52991.475115,81.782849,0.1546,253.691216,253.744221,0.053005,0.0209 -H2O,1650.00,48.505744,48.824184,0.318440,0.6565,55324.102803,55420.645265,96.542462,0.1745,255.177089,255.239175,0.062085,0.0243 -H2O,1700.00,48.916823,49.291433,0.374610,0.7658,57759.790121,57873.606726,113.816605,0.1971,256.631308,256.703703,0.072395,0.0282 -H2O,1750.00,49.342322,49.741812,0.399490,0.8096,60213.235688,60349.507348,136.271659,0.2263,258.053758,258.139077,0.085319,0.0331 -H2O,1800.00,49.753182,50.175709,0.422526,0.8492,62690.713932,62847.513169,156.799237,0.2501,259.449586,259.546468,0.096883,0.0373 -H2O,1850.00,50.143229,50.593547,0.450318,0.8981,65188.207152,65366.810558,178.603406,0.2740,260.818136,260.926966,0.108829,0.0417 -H2O,1900.00,50.514194,50.995776,0.481582,0.9534,67704.718919,67906.607715,201.888797,0.2982,262.160333,262.281581,0.121247,0.0462 -H2O,1950.00,50.867599,51.382859,0.515259,1.0129,70239.333999,70466.135701,226.801702,0.3229,263.477071,263.611258,0.134188,0.0509 -H2O,2000.00,51.204788,51.755271,0.550483,1.0751,72791.208667,73044.649087,253.440420,0.3482,264.769210,264.916885,0.147675,0.0558 -H2O,2050.00,51.526950,52.113493,0.586543,1.1383,75359.562430,75641.426316,281.863887,0.3740,266.037581,266.199291,0.161710,0.0608 -H2O,2100.00,51.835144,52.458003,0.622859,1.2016,77943.670941,78255.769847,312.098906,0.4004,267.282980,267.459260,0.176280,0.0660 -H2O,2150.00,52.130317,52.789282,0.658965,1.2641,80542.859897,80887.006117,344.146220,0.4273,268.506171,268.697530,0.191360,0.0713 -H2O,2200.00,52.413315,53.107802,0.694487,1.3250,83156.499770,83534.485386,377.985616,0.4545,269.707884,269.914802,0.206917,0.0767 -H2O,2250.00,52.684901,53.414032,0.729130,1.3839,85784.001243,86197.581470,413.580227,0.4821,270.888822,271.111736,0.222914,0.0823 -H2O,2300.00,52.945765,53.708432,0.762667,1.4405,88424.811235,88875.691401,450.880167,0.5099,272.049654,272.288963,0.239309,0.0880 -H2O,2350.00,53.196529,53.991454,0.794925,1.4943,91078.409441,91568.235031,489.825590,0.5378,273.191021,273.447080,0.256059,0.0937 -H2O,2400.00,53.437761,54.263542,0.825781,1.5453,93744.305300,94274.654592,530.349291,0.5657,274.313536,274.586657,0.273121,0.0996 -H2O,2450.00,53.669979,54.525129,0.855150,1.5933,96422.035337,96994.414224,572.378888,0.5936,275.417785,275.708238,0.290453,0.1055 -H2O,2500.00,53.893654,54.776637,0.882983,1.6384,99111.160818,99726.999488,615.838670,0.6214,276.504330,276.812342,0.308012,0.1114 -H2O,2550.00,54.109224,55.018481,0.909257,1.6804,101811.265689,102471.916852,660.651163,0.6489,277.573707,277.899466,0.325759,0.1174 -H2O,2600.00,54.317086,55.251062,0.933976,1.7195,104521.954750,105228.693182,706.738431,0.6762,278.626428,278.970085,0.343657,0.1233 -H2O,2650.00,54.517612,55.474773,0.957162,1.7557,107242.852030,107996.875214,754.023184,0.7031,279.662985,280.024655,0.361670,0.1293 -H2O,2700.00,54.711143,55.689996,0.978854,1.7891,109973.599351,110776.029039,802.429688,0.7297,280.683850,281.063615,0.379766,0.1353 -H2O,2750.00,54.897998,55.897103,0.999105,1.8199,112713.855044,113565.739578,851.884534,0.7558,281.689471,282.087385,0.397914,0.1413 -H2O,2800.00,55.078473,56.096454,1.017980,1.8482,115463.292804,116365.610063,902.317260,0.7815,282.680281,283.096369,0.416088,0.1472 -H2O,2850.00,55.252846,56.288400,1.035553,1.8742,118221.600662,119175.261527,953.660865,0.8067,283.656694,284.090957,0.434263,0.1531 -H2O,2900.00,55.421377,56.473282,1.051904,1.8980,120988.480072,121994.332291,1005.852219,0.8314,284.619107,285.071524,0.452416,0.1590 -H2O,2950.00,55.584310,56.651430,1.067121,1.9198,123763.645079,124822.477467,1058.832387,0.8555,285.567901,286.038430,0.470529,0.1648 -H2O,3000.00,55.741874,56.823167,1.081293,1.9398,126546.821583,127659.368459,1112.546876,0.8792,286.503441,286.992025,0.488585,0.1705 -NH3,300.00,35.696953,35.678058,-0.018894,-0.0529,64.709005,24.281926,-40.427079,-62.4752,192.990667,192.989738,-0.000928,-0.0005 -NH3,350.00,37.158635,37.096563,-0.062073,-0.1670,1885.307603,1842.732967,-42.574636,-2.2582,198.600700,198.593238,-0.007462,-0.0038 -NH3,400.00,38.749240,38.677380,-0.071860,-0.1854,3782.676555,3736.623886,-46.052669,-1.2175,203.665503,203.648766,-0.016737,-0.0082 -NH3,450.00,40.388287,40.328718,-0.059568,-0.1475,5761.019098,5711.627688,-49.391410,-0.8573,208.323911,208.299295,-0.024616,-0.0118 -NH3,500.00,42.034070,41.991464,-0.042605,-0.1014,7821.607750,7769.672411,-51.935339,-0.6640,212.664482,212.634490,-0.029993,-0.0141 -NH3,550.00,43.663550,43.630818,-0.032732,-0.0750,9964.149201,9910.375356,-53.773845,-0.5397,216.747369,216.713864,-0.033505,-0.0155 -NH3,600.00,45.263443,45.228271,-0.035172,-0.0777,12187.466763,12132.050274,-55.416489,-0.4547,220.615438,220.579076,-0.036362,-0.0165 -NH3,650.00,46.825898,46.775648,-0.050249,-0.1073,14489.867837,14432.364527,-57.503310,-0.3969,224.300412,224.260718,-0.039694,-0.0177 -NH3,700.00,48.346258,48.270950,-0.075308,-0.1558,16869.353898,16808.745407,-60.608491,-0.3593,227.826494,227.782209,-0.044285,-0.0194 -NH3,750.00,49.821830,49.715470,-0.106360,-0.2135,19323.746484,19258.612463,-65.134022,-0.3371,231.212621,231.162103,-0.050517,-0.0218 -NH3,800.00,51.251171,51.111806,-0.139364,-0.2719,21850.765943,21779.489442,-71.276501,-0.3262,234.473926,234.415492,-0.058435,-0.0249 -NH3,850.00,52.633659,52.462452,-0.171207,-0.3253,24448.082380,24369.032220,-79.050160,-0.3233,237.622730,237.554879,-0.067850,-0.0286 -NH3,900.00,53.969229,53.768799,-0.200430,-0.3714,27113.349628,27024.997341,-88.352287,-0.3259,240.669215,240.590739,-0.078476,-0.0326 -NH3,950.00,55.258202,55.030419,-0.227783,-0.4122,29844.228488,29745.167993,-99.060496,-0.3319,243.621922,243.531873,-0.090049,-0.0370 -NH3,1000.00,56.501172,56.244532,-0.256640,-0.4542,32638.403000,32527.249010,-111.153990,-0.3406,246.488097,246.385649,-0.102449,-0.0416 -NH3,1050.00,57.698929,57.399972,-0.298957,-0.5181,35493.592060,35368.492794,-125.099266,-0.3525,249.273963,249.157914,-0.116048,-0.0466 -NH3,1100.00,58.852408,58.520292,-0.332116,-0.5643,38407.557874,38266.658823,-140.899050,-0.3669,251.984909,251.854167,-0.130742,-0.0519 -NH3,1150.00,59.962651,59.600291,-0.362360,-0.6043,41378.112203,41219.848617,-158.263587,-0.3825,254.625654,254.479479,-0.146175,-0.0574 -NH3,1200.00,61.030782,60.637236,-0.393545,-0.6448,44403.121051,44225.969438,-177.151613,-0.3990,257.200357,257.038109,-0.162248,-0.0631 -NH3,1250.00,62.057986,61.630060,-0.427926,-0.6896,47480.508222,47282.836261,-197.671961,-0.4163,259.712716,259.533719,-0.178997,-0.0689 -NH3,1300.00,63.045497,62.578814,-0.466684,-0.7402,50608.258060,50388.240530,-220.017530,-0.4347,262.166041,261.969521,-0.196520,-0.0750 -NH3,1350.00,63.994588,63.484290,-0.510298,-0.7974,53784.417580,53539.996072,-244.421507,-0.4544,264.563314,264.348379,-0.214935,-0.0812 -NH3,1400.00,64.906558,64.347760,-0.558798,-0.8609,57007.098141,56735.969230,-271.128912,-0.4756,266.907240,266.672885,-0.234355,-0.0878 -NH3,1450.00,65.748089,65.170795,-0.577294,-0.8780,60270.054065,59974.098044,-295.956022,-0.4910,269.197277,268.945404,-0.251873,-0.0936 -NH3,1500.00,66.586688,65.955138,-0.631550,-0.9485,63578.643620,63252.403829,-326.239790,-0.5131,271.440523,271.168122,-0.272401,-0.1004 -NH3,1550.00,67.375073,66.702618,-0.672456,-0.9981,66927.886457,66568.997475,-358.888982,-0.5362,273.636874,273.343066,-0.293808,-0.1074 -NH3,1600.00,68.117980,67.415085,-0.702895,-1.0319,70315.393362,69922.082084,-393.311278,-0.5594,275.787790,275.472127,-0.315663,-0.1145 -NH3,1650.00,68.819465,68.094378,-0.725087,-1.0536,73738.994405,73309.953119,-429.041286,-0.5818,277.894733,277.557082,-0.337651,-0.1215 -NH3,1700.00,69.483026,68.742287,-0.740738,-1.0661,77196.708062,76730.996853,-465.711209,-0.6033,279.959143,279.599600,-0.359544,-0.1284 -NH3,1750.00,70.111692,69.360545,-0.751147,-1.0714,80686.715634,80183.687709,-503.027925,-0.6234,281.982434,281.601256,-0.381177,-0.1352 -NH3,1800.00,70.708106,69.950810,-0.757295,-1.0710,84207.339924,83666.584887,-540.755037,-0.6422,283.965976,283.563543,-0.402433,-0.1417 -NH3,1850.00,71.274580,70.514663,-0.759917,-1.0662,87757.027376,87178.328585,-578.698791,-0.6594,285.911096,285.487871,-0.423225,-0.1480 -NH3,1900.00,71.813153,71.053602,-0.759551,-1.0577,91334.333033,90717.635990,-616.697043,-0.6752,287.819074,287.375581,-0.443492,-0.1541 -NH3,1950.00,72.325628,71.569045,-0.756583,-1.0461,94937.907808,94283.297211,-654.610598,-0.6895,289.691137,289.227948,-0.463189,-0.1599 -NH3,2000.00,72.813609,72.062330,-0.751279,-1.0318,98566.487667,97874.171227,-692.316439,-0.7024,291.528465,291.046183,-0.482282,-0.1654 -NH3,2050.00,73.278528,72.534716,-0.743812,-1.0150,102218.884390,101489.181941,-729.702450,-0.7139,293.332186,292.831440,-0.500745,-0.1707 -NH3,2100.00,73.721674,72.987389,-0.734284,-0.9960,105893.977660,105127.314352,-766.663308,-0.7240,295.103379,294.584820,-0.518559,-0.1757 -NH3,2150.00,74.144205,73.421462,-0.722743,-0.9748,109590.708246,108787.610912,-803.097334,-0.7328,296.843078,296.307372,-0.535706,-0.1805 -NH3,2200.00,74.547172,73.837981,-0.709191,-0.9513,113308.072125,112469.168042,-838.904083,-0.7404,298.552268,298.000098,-0.552170,-0.1849 -NH3,2250.00,74.931532,74.237928,-0.693604,-0.9257,117045.115379,116171.132848,-873.982531,-0.7467,300.231893,299.663956,-0.567937,-0.1892 -NH3,2300.00,75.298157,74.622225,-0.675932,-0.8977,120800.929765,119892.700023,-908.229743,-0.7518,301.882852,301.299860,-0.582992,-0.1931 -NH3,2350.00,75.647848,74.991737,-0.656111,-0.8673,124574.648848,123633.108927,-941.539921,-0.7558,303.506006,302.908686,-0.597320,-0.1968 -NH3,2400.00,75.981342,75.347277,-0.634065,-0.8345,128365.444613,127391.640856,-973.803757,-0.7586,305.102177,304.491271,-0.610906,-0.2002 -NH3,2450.00,76.299322,75.689608,-0.609714,-0.7991,132172.524500,131167.616479,-1004.908020,-0.7603,306.672151,306.048417,-0.623734,-0.2034 -NH3,2500.00,76.602420,76.019446,-0.582974,-0.7610,135995.128786,134960.393439,-1034.735348,-0.7609,308.216680,307.580893,-0.635787,-0.2063 -NH3,2550.00,76.891225,76.337465,-0.553760,-0.7202,139832.528286,138769.364112,-1063.164175,-0.7603,309.736482,309.089435,-0.647047,-0.2089 -NH3,2600.00,77.166289,76.644297,-0.521992,-0.6765,143684.022306,142593.953512,-1090.068794,-0.7587,311.232247,310.574750,-0.657497,-0.2113 -NH3,2650.00,77.428129,76.940538,-0.487591,-0.6297,147548.936838,146433.617335,-1115.319503,-0.7559,312.704632,312.037514,-0.667118,-0.2133 -NH3,2700.00,77.677231,77.226747,-0.450485,-0.5799,151426.622952,150287.840129,-1138.782823,-0.7520,314.154269,313.478379,-0.675890,-0.2151 -NH3,2750.00,77.914056,77.503450,-0.410606,-0.5270,155316.455361,154156.133583,-1160.321778,-0.7471,315.581764,314.897968,-0.683796,-0.2167 -NH3,2800.00,78.139037,77.771143,-0.367895,-0.4708,159217.831147,158038.034936,-1179.796210,-0.7410,316.987697,316.296882,-0.690815,-0.2179 -NH3,2850.00,78.352590,78.030293,-0.322297,-0.4113,163130.168610,161933.105482,-1197.063128,-0.7338,318.372626,317.675697,-0.696929,-0.2189 -NH3,2900.00,78.555108,78.281340,-0.273769,-0.3485,167052.906249,165840.929175,-1211.977074,-0.7255,319.737085,319.034968,-0.702117,-0.2196 -NH3,2950.00,78.746969,78.524698,-0.222271,-0.2823,170985.501835,169761.111325,-1224.390509,-0.7161,321.081591,320.375228,-0.706363,-0.2200 -NH3,3000.00,78.928534,78.760759,-0.167775,-0.2126,174927.431583,173693.277381,-1234.154203,-0.7055,322.406636,321.696990,-0.709646,-0.2201 -SO2,300.00,39.943349,39.909230,-0.034119,-0.0854,72.586000,107.661817,35.075817,48.3231,248.457266,248.467038,0.009771,0.0039 -SO2,350.00,41.787922,41.707124,-0.080797,-0.1934,2116.247035,2148.230970,31.983935,1.5114,254.754247,254.754578,0.000331,0.0001 -SO2,400.00,43.519329,43.432195,-0.087135,-0.2002,4249.471583,4277.143629,27.672046,0.6512,260.448723,260.437547,-0.011175,-0.0043 -SO2,450.00,45.112280,45.034410,-0.077870,-0.1726,6465.863246,6489.388625,23.525379,0.3638,265.667947,265.646993,-0.020954,-0.0079 -SO2,500.00,46.559259,46.489003,-0.070256,-0.1509,8758.262271,8778.114560,19.852289,0.2267,270.497176,270.468474,-0.028701,-0.0106 -SO2,550.00,47.861309,47.788866,-0.072443,-0.1514,11119.371311,11135.703968,16.332657,0.1469,274.996944,274.961535,-0.035409,-0.0129 -SO2,600.00,49.023946,48.938102,-0.085844,-0.1751,13542.068309,13554.487135,12.418825,0.0917,279.212255,279.170043,-0.042211,-0.0151 -SO2,650.00,50.055175,49.947412,-0.107763,-0.2153,16019.574975,16027.180218,7.605243,0.0475,283.177831,283.127925,-0.049906,-0.0176 -SO2,700.00,50.964468,50.830940,-0.133528,-0.2620,18545.553052,18547.131294,1.578243,0.0085,286.921317,286.862490,-0.058827,-0.0205 -SO2,750.00,51.762197,51.604118,-0.158079,-0.3054,21114.162085,21108.435199,-5.726885,-0.0271,290.465315,290.396418,-0.068897,-0.0237 -SO2,800.00,52.459304,52.282176,-0.177128,-0.3376,23720.095519,23705.958461,-14.137059,-0.0596,293.828735,293.748989,-0.079746,-0.0271 -SO2,850.00,53.067112,52.879112,-0.188000,-0.3543,26358.604063,26335.301909,-23.302154,-0.0884,297.027720,296.936865,-0.090855,-0.0306 -SO2,900.00,53.597196,53.406949,-0.190247,-0.3550,29025.511256,28992.719447,-32.791809,-0.1130,300.076300,299.974597,-0.101703,-0.0339 -SO2,950.00,54.061310,53.875210,-0.186100,-0.3442,31717.224130,31675.005507,-42.218623,-0.1331,302.986855,302.874958,-0.111898,-0.0369 -SO2,1000.00,54.471331,54.290527,-0.180804,-0.3319,34430.740667,34379.359797,-51.380870,-0.1492,305.770469,305.649172,-0.121298,-0.0397 -SO2,1050.00,54.839228,54.662880,-0.176348,-0.3216,37163.655122,37103.373282,-60.281840,-0.1622,308.437183,308.307199,-0.129984,-0.0421 -SO2,1100.00,55.177034,54.995633,-0.181401,-0.3288,39914.161903,39844.988075,-69.173829,-0.1733,310.996195,310.857939,-0.138256,-0.0445 -SO2,1150.00,55.496835,55.294610,-0.202225,-0.3644,42681.058435,42602.374247,-78.684188,-0.1844,313.456011,313.309303,-0.146708,-0.0468 -SO2,1200.00,55.810751,55.564587,-0.246164,-0.4411,45463.747303,45373.966224,-89.781079,-0.1975,315.824567,315.668420,-0.156147,-0.0494 -SO2,1250.00,56.045386,55.809513,-0.235873,-0.4209,48258.898290,48158.415777,-100.482513,-0.2082,318.106466,317.941722,-0.164744,-0.0518 -SO2,1300.00,56.282715,56.032685,-0.250030,-0.4442,51067.197738,50954.555235,-112.642503,-0.2206,320.309306,320.135025,-0.174281,-0.0544 -SO2,1350.00,56.498465,56.236872,-0.261593,-0.4630,53886.810505,53761.368089,-125.442417,-0.2328,322.437542,322.253601,-0.183941,-0.0570 -SO2,1400.00,56.695628,56.424418,-0.271210,-0.4784,56716.734772,56577.965294,-138.769479,-0.2447,324.495874,324.302241,-0.193634,-0.0597 -SO2,1450.00,56.876689,56.597319,-0.279369,-0.4912,59556.105175,59403.565999,-152.539176,-0.2561,326.488601,326.285304,-0.203297,-0.0623 -SO2,1500.00,57.043726,56.757284,-0.286442,-0.5021,62404.170094,62237.481740,-166.688353,-0.2671,328.419661,328.206772,-0.212890,-0.0648 -SO2,1550.00,57.198489,56.905783,-0.292706,-0.5117,65260.273338,65079.103391,-181.169947,-0.2776,330.292670,330.070284,-0.222386,-0.0673 -SO2,1600.00,57.342460,57.044092,-0.298369,-0.5203,68123.839267,67927.890306,-195.948960,-0.2876,332.110947,331.879178,-0.231770,-0.0698 -SO2,1650.00,57.476900,57.173316,-0.303584,-0.5282,70994.360613,70783.361238,-210.999375,-0.2972,333.877551,333.636519,-0.241032,-0.0722 -SO2,1700.00,57.602887,57.294423,-0.308464,-0.5355,73871.388458,73645.086688,-226.301770,-0.3063,335.595298,335.345130,-0.250168,-0.0745 -SO2,1750.00,57.721348,57.408260,-0.313088,-0.5424,76754.523923,76512.682430,-241.841494,-0.3151,337.266789,337.007612,-0.259177,-0.0768 -SO2,1800.00,57.833085,57.515573,-0.317512,-0.5490,79643.411246,79385.803998,-257.607248,-0.3235,338.894432,338.626372,-0.268059,-0.0791 -SO2,1850.00,57.938792,57.617020,-0.321773,-0.5554,82537.731980,82264.141977,-273.590003,-0.3315,340.480453,340.203636,-0.276817,-0.0813 -SO2,1900.00,58.039075,57.713184,-0.325891,-0.5615,85437.200112,85147.417950,-289.782162,-0.3392,342.026922,341.741469,-0.285453,-0.0835 -SO2,1950.00,58.134462,57.804584,-0.329878,-0.5674,88341.557929,88035.381007,-306.176921,-0.3466,343.535759,343.241789,-0.293970,-0.0856 -SO2,2000.00,58.225418,57.891683,-0.333735,-0.5732,91250.572500,90927.804717,-322.767783,-0.3537,345.008752,344.706380,-0.302371,-0.0876 -SO2,2050.00,58.312351,57.974892,-0.337458,-0.5787,94164.032684,93824.484497,-339.548187,-0.3606,346.447566,346.136908,-0.310658,-0.0897 -SO2,2100.00,58.395622,58.054583,-0.341039,-0.5840,97081.746556,96725.235314,-356.511242,-0.3672,347.853757,347.534924,-0.318833,-0.0917 -SO2,2150.00,58.475551,58.131087,-0.344465,-0.5891,100003.539194,99629.889674,-373.649520,-0.3736,349.228778,348.901880,-0.326898,-0.0936 -SO2,2200.00,58.552424,58.204703,-0.347721,-0.5939,102929.250770,102538.295856,-390.954914,-0.3798,350.573989,350.239134,-0.334855,-0.0955 -SO2,2250.00,58.626493,58.275702,-0.350792,-0.5983,105858.734896,105450.316355,-408.418541,-0.3858,351.890663,351.547959,-0.342704,-0.0974 -SO2,2300.00,58.697986,58.344328,-0.353659,-0.6025,108791.857183,108365.826502,-426.030682,-0.3916,353.179997,352.829551,-0.350446,-0.0992 -SO2,2350.00,58.767106,58.410801,-0.356305,-0.6063,111728.493984,111284.713248,-443.780736,-0.3972,354.443112,354.085032,-0.358080,-0.1010 -SO2,2400.00,58.834036,58.475323,-0.358713,-0.6097,114668.531296,114206.874081,-461.657215,-0.4026,355.681066,355.315458,-0.365608,-0.1028 -SO2,2450.00,58.898940,58.538076,-0.360864,-0.6127,117611.863796,117132.216058,-479.647738,-0.4078,356.894852,356.521825,-0.373027,-0.1045 -SO2,2500.00,58.961967,58.599226,-0.362741,-0.6152,120558.393994,120060.654947,-497.739047,-0.4129,358.085408,357.705071,-0.380336,-0.1062 -SO2,2550.00,59.023253,58.658925,-0.364328,-0.6173,123508.031480,122992.114453,-515.917028,-0.4177,359.253617,358.866081,-0.387536,-0.1079 -SO2,2600.00,59.082919,58.717311,-0.365608,-0.6188,126460.692267,125926.525530,-534.166737,-0.4224,360.400316,360.005692,-0.394623,-0.1095 -SO2,2650.00,59.141076,58.774511,-0.366565,-0.6198,129416.298199,128863.825760,-552.472439,-0.4269,361.526293,361.124696,-0.401597,-0.1111 -SO2,2700.00,59.197827,58.830641,-0.367185,-0.6203,132374.776430,131803.958792,-570.817638,-0.4312,362.632297,362.223842,-0.408455,-0.1126 -SO2,2750.00,59.253263,58.885809,-0.367454,-0.6201,135336.058962,134746.873844,-589.185118,-0.4353,363.719035,363.303839,-0.415196,-0.1142 -SO2,2800.00,59.307469,58.940111,-0.367359,-0.6194,138300.082225,137692.525240,-607.556985,-0.4393,364.787179,364.365362,-0.421816,-0.1156 -SO2,2850.00,59.360524,58.993638,-0.366886,-0.6181,141266.786709,140640.872008,-625.914701,-0.4431,365.837366,365.409051,-0.428315,-0.1171 -SO2,2900.00,59.412498,59.046473,-0.366025,-0.6161,144236.116629,143591.877502,-644.239128,-0.4467,366.870201,366.435512,-0.434689,-0.1185 -SO2,2950.00,59.463458,59.098693,-0.364765,-0.6134,147208.019627,146545.509065,-662.510562,-0.4501,367.886260,367.445324,-0.440936,-0.1199 -SO2,3000.00,59.513463,59.150368,-0.363095,-0.6101,150182.446500,149501.737725,-680.708775,-0.4533,368.886089,368.439036,-0.447053,-0.1212 +CO,300.00,29.145012,29.142846,-0.002166,-0.0074,57.148703,45.817154,-11.331549,-19.8282,197.843179,197.840086,-0.003094,-0.0016 +CO,350.00,29.152958,29.210822,0.057864,0.1985,1513.824988,1504.422782,-9.402205,-0.6211,202.334115,202.336853,0.002739,0.0014 +CO,400.00,29.302705,29.341326,0.038621,0.1318,2974.776902,2967.949865,-6.827037,-0.2295,206.235550,206.245196,0.009646,0.0047 +CO,450.00,29.535511,29.537653,0.002142,0.0073,4445.466052,4439.657550,-5.808501,-0.1307,209.699726,209.711813,0.012088,0.0058 +CO,500.00,29.819490,29.793687,-0.025804,-0.0865,5929.174172,5922.712994,-6.461178,-0.1090,212.825948,212.836686,0.010738,0.0050 +CO,550.00,30.135685,30.098264,-0.037420,-0.1242,7427.948442,7419.836396,-8.112047,-0.1092,215.682677,215.690278,0.007600,0.0035 +CO,600.00,30.471890,30.438112,-0.033778,-0.1108,8943.074146,8933.127349,-9.946797,-0.1112,218.319129,218.323534,0.004405,0.0020 +CO,650.00,30.819669,30.799739,-0.019930,-0.0647,10475.329180,10464.010025,-11.319155,-0.1081,220.771860,220.774061,0.002201,0.0010 +CO,700.00,31.172796,31.170665,-0.002131,-0.0068,12025.129486,12013.255213,-11.874273,-0.0987,223.068749,223.070119,0.001370,0.0006 +CO,750.00,31.526403,31.540235,0.013832,0.0439,13592.616313,13581.052597,-11.563716,-0.0851,225.231517,225.233309,0.001792,0.0008 +CO,800.00,31.876488,31.900163,0.023675,0.0743,15177.710752,15167.116470,-10.594282,-0.0698,227.277389,227.280429,0.003040,0.0013 +CO,850.00,32.219621,32.244907,0.025286,0.0785,16780.149027,16770.814144,-9.334884,-0.0556,229.220228,229.224794,0.004566,0.0020 +CO,900.00,32.552754,32.571932,0.019178,0.0589,18399.506023,18391.310085,-8.195938,-0.0445,231.071335,231.077205,0.005870,0.0025 +CO,950.00,32.873109,32.881895,0.008786,0.0267,20035.211376,20027.721148,-7.490228,-0.0374,232.840018,232.846653,0.006636,0.0028 +CO,1000.00,33.178096,33.178782,0.000686,0.0021,21686.560750,21679.279772,-7.280978,-0.0336,234.534014,234.540866,0.006852,0.0029 +CO,1050.00,33.465260,33.453710,-0.011550,-0.0345,23352.723876,23345.171600,-7.552276,-0.0323,236.159804,236.166394,0.006590,0.0028 +CO,1100.00,33.732247,33.709955,-0.022292,-0.0661,25032.750413,25024.339208,-8.411205,-0.0336,237.722853,237.728646,0.005793,0.0024 +CO,1150.00,33.976778,33.948470,-0.028308,-0.0833,26725.574270,26715.871504,-9.702766,-0.0363,239.227793,239.232439,0.004646,0.0019 +CO,1200.00,34.196629,34.170338,-0.026291,-0.0769,28430.016851,28418.908731,-11.108120,-0.0391,240.678568,240.682017,0.003449,0.0014 +CO,1250.00,34.389621,34.376687,-0.012934,-0.0376,30144.789523,30132.646667,-12.142856,-0.0403,242.078550,242.081153,0.002602,0.0011 +CO,1300.00,34.553607,34.568640,0.015033,0.0435,31868.495513,31856.337528,-12.157985,-0.0382,243.430629,243.433216,0.002587,0.0011 +CO,1350.00,34.762624,34.747282,-0.015342,-0.0441,33601.816100,33589.288848,-12.527252,-0.0373,244.740107,244.741238,0.001130,0.0005 +CO,1400.00,34.929521,34.913648,-0.015874,-0.0454,35344.180302,35330.861187,-13.319115,-0.0377,246.007403,246.007957,0.000554,0.0002 +CO,1450.00,35.082828,35.068705,-0.014123,-0.0403,37094.541904,37080.465177,-14.076728,-0.0379,247.235837,247.235859,0.000022,0.0000 +CO,1500.00,35.224238,35.213356,-0.010882,-0.0309,38852.264969,38837.558264,-14.706705,-0.0379,248.427614,248.427209,-0.000405,-0.0002 +CO,1550.00,35.355179,35.348439,-0.006740,-0.0191,40616.791349,40601.641357,-15.149993,-0.0373,249.584773,249.584076,-0.000696,-0.0003 +CO,1600.00,35.476859,35.474722,-0.002137,-0.0060,42387.628615,42372.255522,-15.373093,-0.0363,250.709198,250.708360,-0.000838,-0.0003 +CO,1650.00,35.590309,35.592916,0.002607,0.0073,44164.340169,44148.978795,-15.361374,-0.0348,251.802636,251.801805,-0.000831,-0.0003 +CO,1700.00,35.696411,35.703670,0.007259,0.0203,45946.537115,45931.423175,-15.113940,-0.0329,252.866706,252.866022,-0.000684,-0.0003 +CO,1750.00,35.795925,35.807581,0.011656,0.0326,47733.871513,47719.231796,-14.639716,-0.0307,253.902907,253.902498,-0.000409,-0.0002 +CO,1800.00,35.889507,35.905193,0.015687,0.0437,49526.030763,49512.076321,-13.954441,-0.0282,254.912635,254.912611,-0.000024,-0.0000 +CO,1850.00,35.977729,35.997007,0.019279,0.0536,51322.732896,51309.654519,-13.078378,-0.0255,255.897185,255.897641,0.000456,0.0002 +CO,1900.00,36.061088,36.083479,0.022392,0.0621,53123.722615,53111.688047,-12.034568,-0.0227,256.857765,256.858778,0.001013,0.0004 +CO,1950.00,36.140021,36.165028,0.025007,0.0692,54928.767934,54917.920420,-10.847514,-0.0197,257.795499,257.797129,0.001629,0.0006 +CO,2000.00,36.214911,36.242034,0.027123,0.0749,56737.657333,56728.115146,-9.542188,-0.0168,258.711437,258.713728,0.002290,0.0009 +CO,2050.00,36.286097,36.314850,0.028752,0.0792,58550.197323,58542.054024,-8.143299,-0.0139,259.606561,259.609542,0.002981,0.0011 +CO,2100.00,36.353880,36.383793,0.029914,0.0823,60366.210358,60359.535599,-6.674759,-0.0111,260.481787,260.485476,0.003689,0.0014 +CO,2150.00,36.418525,36.449159,0.030634,0.0841,62185.533042,62180.373741,-5.159301,-0.0083,261.337975,261.342377,0.004402,0.0017 +CO,2200.00,36.480271,36.511215,0.030944,0.0848,64008.014578,64004.396357,-3.618221,-0.0057,262.175932,262.181042,0.005110,0.0019 +CO,2250.00,36.539331,36.570208,0.030877,0.0845,65833.515425,65831.444216,-2.071209,-0.0031,262.996414,263.002220,0.005806,0.0022 +CO,2300.00,36.595896,36.626363,0.030467,0.0833,67661.906130,67661.369871,-0.536259,-0.0008,263.800132,263.806613,0.006481,0.0025 +CO,2350.00,36.650137,36.679887,0.029750,0.0812,69493.066309,69494.036686,0.970377,0.0014,264.587756,264.594885,0.007129,0.0027 +CO,2400.00,36.702211,36.730971,0.028760,0.0784,71326.883752,71329.317940,2.434188,0.0034,265.359916,265.367661,0.007745,0.0029 +CO,2450.00,36.752258,36.779788,0.027530,0.0749,73163.253647,73167.096013,3.842366,0.0053,266.117207,266.125533,0.008326,0.0031 +CO,2500.00,36.800405,36.826499,0.026094,0.0709,75002.077885,75007.261642,5.183757,0.0069,266.860190,266.869058,0.008868,0.0033 +CO,2550.00,36.846770,36.871250,0.024481,0.0664,76843.264457,76849.713244,6.448786,0.0084,267.589395,267.598764,0.009369,0.0035 +CO,2600.00,36.891457,36.914178,0.022721,0.0616,78686.726914,78694.356292,7.629378,0.0097,268.305324,268.315152,0.009827,0.0037 +CO,2650.00,36.934566,36.955407,0.020841,0.0564,80532.383889,80541.102752,8.718863,0.0108,269.008452,269.018694,0.010243,0.0038 +CO,2700.00,36.976184,36.995051,0.018866,0.0510,82380.158677,82389.870562,9.711884,0.0118,269.699227,269.709841,0.010614,0.0039 +CO,2750.00,37.016395,37.033215,0.016820,0.0454,84229.978857,84240.583159,10.604303,0.0126,270.378078,270.389020,0.010941,0.0040 +CO,2800.00,37.055273,37.069998,0.014725,0.0397,86081.775951,86093.169050,11.393099,0.0132,271.045410,271.056635,0.011226,0.0041 +CO,2850.00,37.092889,37.105488,0.012599,0.0340,87935.485128,87947.561409,12.076281,0.0137,271.701606,271.713074,0.011468,0.0042 +CO,2900.00,37.129308,37.139769,0.010461,0.0282,89791.044928,89803.697719,12.652792,0.0141,272.347034,272.358702,0.011668,0.0043 +CO,2950.00,37.164591,37.172917,0.008326,0.0224,91648.397023,91661.519443,13.122419,0.0143,272.982040,272.993869,0.011829,0.0043 +CO,3000.00,37.198793,37.205002,0.006210,0.0167,93507.486000,93520.971714,13.485714,0.0144,273.606958,273.618909,0.011951,0.0044 +CO2,300.00,37.217621,37.220109,0.002488,0.0067,65.851354,81.178852,15.327499,23.2759,214.017582,214.017373,-0.000209,-0.0001 +CO2,350.00,39.410968,39.386847,-0.024121,-0.0612,1982.881127,1997.341462,14.460335,0.7293,219.923470,219.920638,-0.002833,-0.0013 +CO2,400.00,41.336224,41.325129,-0.011095,-0.0268,4002.512250,4016.043796,13.531546,0.3381,225.314294,225.308960,-0.005333,-0.0024 +CO2,450.00,43.058521,43.061504,0.002983,0.0069,6113.135222,6126.489236,13.354014,0.2184,230.284214,230.278446,-0.005768,-0.0025 +CO2,500.00,44.614984,44.624456,0.009473,0.0212,8305.607464,8319.306426,13.698962,0.1649,234.902775,234.897728,-0.005047,-0.0021 +CO2,550.00,46.029256,46.038291,0.009036,0.0196,10572.267471,10586.452296,14.184825,0.1342,239.222420,239.218299,-0.004121,-0.0017 +CO2,600.00,47.317934,47.322556,0.004621,0.0098,12906.441899,12920.978291,14.536392,0.1126,243.283601,243.280095,-0.003506,-0.0014 +CO2,650.00,48.493692,48.492775,-0.000918,-0.0019,15302.180135,15316.808820,14.628685,0.0956,247.118201,247.114846,-0.003355,-0.0014 +CO2,700.00,49.566894,49.561364,-0.005530,-0.0112,17754.102636,17768.563206,14.460570,0.0814,250.751850,250.748248,-0.003602,-0.0014 +CO2,750.00,50.546488,50.538421,-0.008067,-0.0160,20257.309925,20271.420644,14.110720,0.0697,254.205531,254.201447,-0.004084,-0.0016 +CO2,800.00,51.440518,51.432345,-0.008174,-0.0159,22807.325715,22821.020454,13.694738,0.0600,257.496705,257.492084,-0.004621,-0.0018 +CO2,850.00,52.256436,52.250309,-0.006127,-0.0117,25400.060111,25413.389589,13.329479,0.0525,260.640126,260.635062,-0.005064,-0.0019 +CO2,900.00,53.001290,52.998621,-0.002668,-0.0050,28031.785068,28044.890788,13.105721,0.0468,263.648428,263.643107,-0.005321,-0.0020 +CO2,950.00,53.681849,53.682992,0.001142,0.0021,30699.117611,30712.186234,13.068622,0.0426,266.532567,266.527205,-0.005362,-0.0020 +CO2,1000.00,54.304689,54.308733,0.004044,0.0074,33399.008083,33412.212934,13.204851,0.0395,269.302156,269.296933,-0.005223,-0.0019 +CO2,1050.00,54.876239,54.890446,0.014206,0.0259,36128.731749,36142.424462,13.692713,0.0379,271.965716,271.960967,-0.004749,-0.0017 +CO2,1100.00,55.402827,55.420024,0.017197,0.0310,38885.882692,38900.389402,14.506710,0.0373,274.530875,274.526883,-0.003992,-0.0015 +CO2,1150.00,55.890702,55.903810,0.013108,0.0235,41668.369304,41683.664222,15.294918,0.0367,277.004524,277.001233,-0.003291,-0.0012 +CO2,1200.00,56.346054,56.347167,0.001112,0.0020,44474.410901,44490.097035,15.686134,0.0353,279.392936,279.389980,-0.002956,-0.0011 +CO2,1250.00,56.746881,56.754659,0.007778,0.0137,47297.887558,47317.783504,19.895946,0.0421,281.695869,281.698559,0.002690,0.0010 +CO2,1300.00,57.140892,57.130198,-0.010693,-0.0187,50145.241627,50165.030680,19.789053,0.0395,283.929324,283.931933,0.002609,0.0009 +CO2,1350.00,57.499255,57.477154,-0.022101,-0.0384,53011.383156,53030.327195,18.944039,0.0357,286.092664,286.094636,0.001972,0.0007 +CO2,1400.00,57.826760,57.798444,-0.028316,-0.0490,55894.653245,55912.318537,17.665292,0.0316,288.189783,288.190825,0.001043,0.0004 +CO2,1450.00,58.127392,58.096610,-0.030782,-0.0530,58793.611610,58809.786393,16.174783,0.0275,290.224317,290.224314,-0.000003,-0.0000 +CO2,1500.00,58.404486,58.373873,-0.030613,-0.0524,61707.000396,61721.631303,14.630907,0.0237,292.199659,292.198609,-0.001050,-0.0004 +CO2,1550.00,58.660854,58.632186,-0.028668,-0.0489,64633.714983,64646.857996,13.143013,0.0203,294.118965,294.116939,-0.002026,-0.0007 +CO2,1600.00,58.898880,58.873273,-0.025607,-0.0435,67572.780273,67584.562935,11.782662,0.0174,295.985175,295.982285,-0.002890,-0.0010 +CO2,1650.00,59.120596,59.098659,-0.021937,-0.0371,70523.331287,70533.923690,10.592403,0.0150,297.801024,297.797402,-0.003623,-0.0012 +CO2,1700.00,59.327744,59.309701,-0.018043,-0.0304,73484.597182,73494.189818,9.592636,0.0131,299.569060,299.564840,-0.004220,-0.0014 +CO2,1750.00,59.521825,59.507612,-0.014214,-0.0239,76455.888013,76464.675009,8.786995,0.0115,301.291655,301.286967,-0.004687,-0.0016 +CO2,1800.00,59.704140,59.693476,-0.010664,-0.0179,79436.583702,79444.750293,8.166591,0.0103,302.971019,302.965982,-0.005037,-0.0017 +CO2,1850.00,59.875817,59.868269,-0.007548,-0.0126,82426.124788,82433.838144,7.713355,0.0094,304.609215,304.603930,-0.005286,-0.0017 +CO2,1900.00,60.037845,60.032869,-0.004976,-0.0083,85424.004648,85431.407331,7.402683,0.0087,306.208170,306.202718,-0.005451,-0.0018 +CO2,1950.00,60.191088,60.188071,-0.003017,-0.0050,88429.762905,88436.968431,7.205525,0.0081,307.769682,307.764128,-0.005554,-0.0018 +CO2,2000.00,60.336311,60.334597,-0.001714,-0.0028,91442.979833,91450.069874,7.090041,0.0078,309.295435,309.289822,-0.005613,-0.0018 +CO2,2050.00,60.474187,60.473102,-0.001085,-0.0018,94463.271576,94470.294479,7.022903,0.0074,310.787006,310.781360,-0.005646,-0.0018 +CO2,2100.00,60.605314,60.604184,-0.001130,-0.0019,97490.286050,97497.256381,6.970331,0.0071,312.245872,312.240201,-0.005671,-0.0018 +CO2,2150.00,60.730226,60.728391,-0.001836,-0.0030,100523.699419,100530.598322,6.898903,0.0069,313.673421,313.667716,-0.005705,-0.0018 +CO2,2200.00,60.849400,60.846223,-0.003176,-0.0052,103563.213052,103569.989238,6.776187,0.0065,315.070955,315.065194,-0.005761,-0.0018 +CO2,2250.00,60.963262,60.958143,-0.005118,-0.0084,106608.550880,106615.122121,6.571241,0.0062,316.439699,316.433846,-0.005853,-0.0018 +CO2,2300.00,61.072196,61.064575,-0.007621,-0.0125,109659.457107,109665.712102,6.254995,0.0057,317.780806,317.774814,-0.005992,-0.0019 +CO2,2350.00,61.176551,61.165913,-0.010639,-0.0174,112715.694205,112721.494745,5.800540,0.0051,319.095363,319.089176,-0.006187,-0.0019 +CO2,2400.00,61.276641,61.262519,-0.014122,-0.0230,115777.041165,115782.224518,5.183353,0.0045,320.384395,320.377948,-0.006447,-0.0020 +CO2,2450.00,61.372750,61.354732,-0.018018,-0.0294,118843.291956,118847.673415,4.381459,0.0037,321.648870,321.642092,-0.006778,-0.0021 +CO2,2500.00,61.465139,61.442866,-0.022274,-0.0362,121914.254179,121917.629723,3.375544,0.0028,322.889702,322.882518,-0.007184,-0.0022 +CO2,2550.00,61.554047,61.527214,-0.026832,-0.0436,124989.747868,124991.896909,2.149041,0.0017,324.107756,324.100087,-0.007670,-0.0024 +CO2,2600.00,61.639689,61.608051,-0.031639,-0.0513,128069.604436,128070.292614,0.688179,0.0005,325.303852,325.295615,-0.008237,-0.0025 +CO2,2650.00,61.722268,61.685633,-0.036635,-0.0594,131153.665741,131152.647748,-1.017993,-0.0008,326.478765,326.469879,-0.008887,-0.0027 +CO2,2700.00,61.801967,61.760201,-0.041766,-0.0676,134241.783252,134238.805664,-2.977588,-0.0022,327.633233,327.623614,-0.009619,-0.0029 +CO2,2750.00,61.878957,61.831983,-0.046974,-0.0759,137333.817308,137328.621417,-5.195892,-0.0038,328.767954,328.757521,-0.010433,-0.0032 +CO2,2800.00,61.953396,61.901192,-0.052204,-0.0843,140429.636455,140421.961084,-7.675371,-0.0055,329.883593,329.872266,-0.011326,-0.0034 +CO2,2850.00,62.025431,61.968032,-0.057399,-0.0925,143529.116856,143518.701153,-10.415703,-0.0073,330.980781,330.968484,-0.012296,-0.0037 +CO2,2900.00,62.095198,62.032693,-0.062505,-0.1007,146632.141758,146618.727962,-13.413796,-0.0091,332.060119,332.046780,-0.013339,-0.0040 +CO2,2950.00,62.162826,62.095358,-0.067468,-0.1085,149738.601017,149721.937190,-16.663827,-0.0111,333.122181,333.107731,-0.014450,-0.0043 +CO2,3000.00,62.228433,62.156200,-0.072233,-0.1161,152848.390667,152828.233392,-20.157274,-0.0132,334.167511,334.151887,-0.015624,-0.0047 +H2O,500.00,35.218362,35.224834,0.006472,0.0184,6924.644792,6925.151518,0.506727,0.0073,206.534070,206.529453,-0.004617,-0.0022 +H2O,550.00,35.754757,35.762322,0.007565,0.0212,8698.846499,8699.714019,0.867520,0.0100,209.915654,209.911723,-0.003930,-0.0019 +H2O,600.00,36.317861,36.323895,0.006034,0.0166,10500.564355,10501.783070,1.218715,0.0116,213.050698,213.047379,-0.003318,-0.0016 +H2O,650.00,36.901741,36.903933,0.002192,0.0059,12330.978122,12332.409978,1.431856,0.0116,215.980611,215.977636,-0.002975,-0.0014 +H2O,700.00,37.501847,37.499167,-0.002680,-0.0071,14191.008408,14192.428924,1.420516,0.0100,218.737198,218.734208,-0.002990,-0.0014 +H2O,750.00,38.114484,38.107632,-0.006852,-0.0180,16081.371365,16082.546918,1.175554,0.0073,221.345386,221.342060,-0.003326,-0.0015 +H2O,800.00,38.736498,38.727908,-0.008590,-0.0222,18002.612885,18003.388994,0.776109,0.0043,223.825052,223.821211,-0.003841,-0.0017 +H2O,850.00,39.365093,39.358572,-0.006521,-0.0166,19955.130721,19955.511072,0.380351,0.0019,226.192273,226.187951,-0.004321,-0.0019 +H2O,900.00,39.997715,39.997807,0.000091,0.0002,21939.189237,21939.389451,0.200213,0.0009,228.460213,228.455684,-0.004529,-0.0020 +H2O,950.00,40.631976,40.643100,0.011123,0.0274,23954.929498,23955.393218,0.463719,0.0019,230.639768,230.635521,-0.004247,-0.0018 +H2O,1000.00,41.265608,41.291036,0.025428,0.0616,26002.376333,26003.744082,1.367749,0.0053,232.740035,232.736713,-0.003323,-0.0014 +H2O,1050.00,41.896427,41.931338,0.034911,0.0833,28081.443384,28084.277797,2.834413,0.0101,234.768671,234.766777,-0.001894,-0.0008 +H2O,1100.00,42.522312,42.573989,0.051677,0.1215,30191.936771,30196.916008,4.979236,0.0165,236.732161,236.732260,0.000099,0.0000 +H2O,1150.00,43.141191,43.212648,0.071457,0.1656,32333.557810,32341.609197,8.051387,0.0249,238.636036,238.638863,0.002827,0.0012 +H2O,1200.00,43.751023,43.842772,0.091749,0.2097,34505.905041,34518.037817,12.132775,0.0352,240.485029,240.491327,0.006298,0.0026 +H2O,1250.00,44.349797,44.461132,0.111334,0.2510,36708.475779,36725.689762,17.213983,0.0469,242.283213,242.293656,0.010444,0.0043 +H2O,1300.00,44.935524,45.065460,0.129936,0.2892,38940.667300,38963.916720,23.249419,0.0597,244.034101,244.049276,0.015175,0.0062 +H2O,1350.00,45.506227,45.654200,0.147973,0.3252,41201.777768,41231.975656,30.197887,0.0733,245.740734,245.761152,0.020418,0.0083 +H2O,1400.00,46.059947,46.226324,0.166377,0.3612,43491.006960,43529.059566,38.052606,0.0875,247.405750,247.431879,0.026129,0.0106 +H2O,1450.00,46.594731,46.781198,0.186468,0.4002,45807.456845,45854.320393,46.863547,0.1023,249.031441,249.063752,0.032311,0.0130 +H2O,1500.00,47.108636,47.318486,0.209850,0.4455,48150.132042,48206.886163,56.754121,0.1179,250.619798,250.658813,0.039015,0.0156 +H2O,1550.00,47.599727,47.838071,0.238344,0.5007,50517.940193,50585.873845,67.933652,0.1345,252.172554,252.218898,0.046344,0.0184 +H2O,1600.00,48.066071,48.340005,0.273933,0.5699,52909.692266,52990.398993,80.706727,0.1525,253.691216,253.745667,0.054451,0.0215 +H2O,1650.00,48.505744,48.824463,0.318718,0.6571,55324.102803,55419.582986,95.480183,0.1726,255.177089,255.240629,0.063540,0.0249 +H2O,1700.00,48.916823,49.291714,0.374891,0.7664,57759.790121,57872.558426,112.768305,0.1952,256.631308,256.705166,0.073858,0.0288 +H2O,1750.00,49.342322,49.742095,0.399773,0.8102,60213.235688,60348.473157,135.237469,0.2246,258.053758,258.140548,0.086790,0.0336 +H2O,1800.00,49.753182,50.175995,0.422812,0.8498,62690.713932,62846.493214,155.779282,0.2485,259.449586,259.547948,0.098362,0.0379 +H2O,1850.00,50.143229,50.593836,0.450607,0.8986,65188.207152,65365.804960,177.597808,0.2724,260.818136,260.928453,0.110316,0.0423 +H2O,1900.00,50.514194,50.996066,0.481872,0.9539,67704.718919,67905.616591,200.897672,0.2967,262.160333,262.283075,0.122742,0.0468 +H2O,1950.00,50.867599,51.383152,0.515552,1.0135,70239.333999,70465.159163,225.825164,0.3215,263.477071,263.612761,0.135690,0.0515 +H2O,2000.00,51.204788,51.755566,0.550778,1.0756,72791.208667,73043.687243,252.478576,0.3469,264.769210,264.918394,0.149184,0.0563 +H2O,2050.00,51.526950,52.113790,0.586840,1.1389,75359.562430,75640.479271,280.916841,0.3728,266.037581,266.200808,0.163227,0.0614 +H2O,2100.00,51.835144,52.458302,0.623158,1.2022,77943.670941,78254.837699,311.166759,0.3992,267.282980,267.460784,0.177804,0.0665 +H2O,2150.00,52.130317,52.789583,0.659266,1.2646,80542.859897,80886.088964,343.229067,0.4261,268.506171,268.699062,0.192891,0.0718 +H2O,2200.00,52.413315,53.108105,0.694790,1.3256,83156.499770,83533.583321,377.083550,0.4535,269.707884,269.916340,0.208455,0.0773 +H2O,2250.00,52.684901,53.414336,0.729435,1.3845,85784.001243,86196.694581,412.693338,0.4811,270.888822,271.113281,0.224459,0.0829 +H2O,2300.00,52.945765,53.708738,0.762973,1.4410,88424.811235,88874.819774,450.008540,0.5089,272.049654,272.290515,0.240861,0.0885 +H2O,2350.00,53.196529,53.991762,0.795233,1.4949,91078.409441,91567.378748,488.969307,0.5369,273.191021,273.448638,0.257617,0.0943 +H2O,2400.00,53.437761,54.263851,0.826090,1.5459,93744.305300,94273.813732,529.508432,0.5648,274.313536,274.588222,0.274686,0.1001 +H2O,2450.00,53.669979,54.525439,0.855461,1.5939,96422.035337,96993.588864,571.553527,0.5928,275.417785,275.709809,0.292024,0.1060 +H2O,2500.00,53.893654,54.776949,0.883295,1.6390,99111.160818,99726.189700,615.028882,0.6205,276.504330,276.813919,0.309589,0.1120 +H2O,2550.00,54.109224,55.018794,0.909571,1.6810,101811.265689,102471.122707,659.857017,0.6481,277.573707,277.901049,0.327343,0.1179 +H2O,2600.00,54.317086,55.251377,0.934291,1.7201,104521.954750,105227.914746,705.959996,0.6754,278.626428,278.971674,0.345247,0.1239 +H2O,2650.00,54.517612,55.475089,0.957478,1.7563,107242.852030,107996.112554,753.260524,0.7024,279.662985,280.026251,0.363266,0.1299 +H2O,2700.00,54.711143,55.690314,0.979171,1.7897,109973.599351,110775.282217,801.682865,0.7290,280.683850,281.065217,0.381367,0.1359 +H2O,2750.00,54.897998,55.897421,0.999424,1.8205,112713.855044,113565.008653,851.153609,0.7551,281.689471,282.088993,0.399522,0.1418 +H2O,2800.00,55.078473,56.096773,1.018300,1.8488,115463.292804,116364.895094,901.602291,0.7809,282.680281,283.097983,0.417701,0.1478 +H2O,2850.00,55.252846,56.288720,1.035874,1.8748,118221.600662,119174.562569,952.961907,0.8061,283.656694,284.092576,0.435882,0.1537 +H2O,2900.00,55.421377,56.473603,1.052226,1.8986,120988.480072,121993.649398,1005.169327,0.8308,284.619107,285.073148,0.454041,0.1595 +H2O,2950.00,55.584310,56.651753,1.067443,1.9204,123763.645079,124821.810691,1058.165612,0.8550,285.567901,286.040060,0.472159,0.1653 +H2O,3000.00,55.741874,56.823491,1.081616,1.9404,126546.821583,127658.717850,1111.896267,0.8786,286.503441,286.993661,0.490220,0.1711 +N2,300.00,29.124787,29.125188,0.000401,0.0014,53.907503,53.880824,-0.026679,-0.0495,191.789077,191.789870,0.000794,0.0004 +N2,350.00,29.167685,29.164513,-0.003172,-0.0109,1511.081729,1510.972360,-0.109369,-0.0072,196.281481,196.282027,0.000546,0.0003 +N2,400.00,29.249803,29.248725,-0.001078,-0.0037,2971.329649,2971.086992,-0.242657,-0.0082,200.181138,200.181325,0.000187,0.0001 +N2,450.00,29.383566,29.387766,0.004201,0.0143,4436.922741,4436.764047,-0.158693,-0.0036,203.633424,203.633802,0.000378,0.0002 +N2,500.00,29.581423,29.582000,0.000577,0.0019,5910.754448,5910.786887,0.032439,0.0005,206.738915,206.739699,0.000784,0.0004 +N2,550.00,29.814684,29.825577,0.010893,0.0365,7396.121796,7395.788277,-0.333519,-0.0045,209.569940,209.570230,0.000289,0.0001 +N2,600.00,30.104019,30.109230,0.005211,0.0173,8893.916482,8894.013266,0.096785,0.0011,212.176261,212.177303,0.001042,0.0005 +N2,650.00,30.424468,30.422249,-0.002220,-0.0073,10407.037493,10407.200718,0.163225,0.0016,214.598376,214.599528,0.001152,0.0005 +N2,700.00,30.760055,30.753825,-0.006231,-0.0203,11936.612652,11936.546935,-0.065717,-0.0006,216.865297,216.866111,0.000815,0.0004 +N2,750.00,31.100277,31.093969,-0.006307,-0.0203,13483.118383,13482.725168,-0.393215,-0.0029,218.999119,218.999482,0.000363,0.0002 +N2,800.00,31.438114,31.434144,-0.003970,-0.0126,15046.599325,15045.943298,-0.656027,-0.0044,221.017097,221.017119,0.000023,0.0000 +N2,850.00,31.768843,31.767698,-0.001145,-0.0036,16626.810437,16626.027966,-0.782471,-0.0047,222.932990,222.932858,-0.000132,-0.0001 +N2,900.00,32.089293,32.090184,0.000890,0.0028,18223.311732,18222.527264,-0.784469,-0.0043,224.757972,224.757838,-0.000134,-0.0001 +N2,950.00,32.397368,32.399576,0.002209,0.0068,19835.533098,19834.826669,-0.706428,-0.0036,226.501263,226.501213,-0.000050,-0.0000 +N2,1000.00,32.691730,32.696441,0.004711,0.0144,21462.819667,21462.274595,-0.545071,-0.0025,228.170577,228.170691,0.000115,0.0001 +N2,1050.00,32.971594,32.977315,0.005721,0.0174,23104.464230,23104.185168,-0.279062,-0.0012,229.772443,229.772817,0.000374,0.0002 +N2,1100.00,33.236574,33.242097,0.005523,0.0166,24759.730804,24759.737569,0.006765,0.0000,231.312456,231.313096,0.000640,0.0003 +N2,1150.00,33.486587,33.490913,0.004326,0.0129,26427.872056,26428.128549,0.256493,0.0010,232.795451,232.796314,0.000862,0.0004 +N2,1200.00,33.721776,33.724240,0.002464,0.0073,28108.142366,28108.570621,0.428255,0.0015,234.225649,234.226658,0.001009,0.0004 +N2,1250.00,33.942456,33.942757,0.000301,0.0009,29799.807760,29800.305653,0.497893,0.0017,235.606761,235.607827,0.001066,0.0005 +N2,1300.00,34.149076,34.147255,-0.001821,-0.0053,31502.153549,31502.612623,0.459074,0.0015,236.942079,236.943115,0.001036,0.0004 +N2,1350.00,34.342193,34.338578,-0.003615,-0.0105,33214.490269,33214.811559,0.321290,0.0010,238.234539,238.235471,0.000932,0.0004 +N2,1400.00,34.522443,34.517577,-0.004866,-0.0141,34936.158350,34936.264999,0.106649,0.0003,239.486780,239.487556,0.000776,0.0003 +N2,1450.00,34.690530,34.685091,-0.005439,-0.0157,36666.531816,36666.377844,-0.153972,-0.0004,240.701184,240.701777,0.000593,0.0002 +N2,1500.00,34.847212,34.841929,-0.005282,-0.0152,38405.021250,38404.596215,-0.425035,-0.0011,241.879919,241.880328,0.000409,0.0002 +N2,1550.00,34.993287,34.988860,-0.004427,-0.0126,40151.076182,40150.405689,-0.670492,-0.0017,243.024962,243.025211,0.000248,0.0001 +N2,1600.00,35.129590,35.126608,-0.002982,-0.0085,41904.187027,41903.329197,-0.857831,-0.0020,244.138131,244.138260,0.000129,0.0001 +N2,1650.00,35.256985,35.255851,-0.001134,-0.0032,43663.886677,43662.924748,-0.961929,-0.0022,245.221098,245.221163,0.000065,0.0000 +N2,1700.00,35.376359,35.377222,0.000863,0.0024,45429.751806,45428.783117,-0.968688,-0.0021,246.275416,246.275476,0.000061,0.0000 +N2,1750.00,35.488618,35.491307,0.002689,0.0076,47201.403951,47200.525539,-0.878412,-0.0019,247.302524,247.302637,0.000113,0.0000 +N2,1800.00,35.594687,35.598650,0.003964,0.0111,48978.510423,48977.801494,-0.708928,-0.0014,248.303770,248.303979,0.000208,0.0001 +N2,1850.00,35.695502,35.699753,0.004251,0.0119,50760.785054,50760.286589,-0.498466,-0.0010,249.280414,249.280738,0.000324,0.0001 +N2,1900.00,35.792012,35.795079,0.003067,0.0086,52547.988841,52547.680554,-0.308287,-0.0006,250.233640,250.234065,0.000425,0.0002 +N2,1950.00,35.885176,35.885056,-0.000120,-0.0003,54339.930482,54339.705380,-0.225102,-0.0004,251.164566,251.165035,0.000469,0.0002 +N2,2000.00,35.975962,35.970077,-0.005886,-0.0164,56136.466833,56136.103568,-0.363266,-0.0006,252.074248,252.074647,0.000399,0.0002 +N2,2050.00,36.051221,36.050504,-0.000717,-0.0020,57937.137851,57936.636517,-0.501333,-0.0009,252.963478,252.963840,0.000362,0.0001 +N2,2100.00,36.127421,36.126673,-0.000748,-0.0021,59741.621432,59741.083031,-0.538401,-0.0009,253.833147,253.833492,0.000344,0.0001 +N2,2150.00,36.199586,36.198889,-0.000697,-0.0019,61549.812707,61549.237936,-0.574771,-0.0009,254.684097,254.684424,0.000327,0.0001 +N2,2200.00,36.268045,36.267437,-0.000608,-0.0017,63361.518291,63360.910813,-0.607478,-0.0010,255.517099,255.517411,0.000312,0.0001 +N2,2250.00,36.333088,36.332576,-0.000511,-0.0014,65176.560261,65175.924832,-0.635429,-0.0010,256.332879,256.333179,0.000299,0.0001 +N2,2300.00,36.394978,36.394548,-0.000430,-0.0012,66994.774530,66994.115675,-0.658855,-0.0010,257.132124,257.132413,0.000289,0.0001 +N2,2350.00,36.453951,36.453573,-0.000377,-0.0010,68816.009438,68815.330553,-0.678884,-0.0010,257.915479,257.915760,0.000281,0.0001 +N2,2400.00,36.510218,36.509856,-0.000362,-0.0010,70640.124509,70639.427302,-0.697206,-0.0010,258.683554,258.683827,0.000273,0.0001 +N2,2450.00,36.563973,36.563583,-0.000389,-0.0011,72466.989372,72466.273550,-0.715822,-0.0010,259.436925,259.437190,0.000265,0.0001 +N2,2500.00,36.615388,36.614930,-0.000459,-0.0013,74296.482802,74295.745956,-0.736846,-0.0010,260.176137,260.176394,0.000257,0.0001 +N2,2550.00,36.664624,36.664055,-0.000569,-0.0016,76128.491878,76127.729513,-0.762365,-0.0010,260.901708,260.901954,0.000247,0.0001 +N2,2600.00,36.711823,36.711107,-0.000716,-0.0019,77962.911237,77962.116905,-0.794332,-0.0010,261.614124,261.614359,0.000234,0.0001 +N2,2650.00,36.757117,36.756222,-0.000896,-0.0024,79799.642414,79798.807921,-0.834494,-0.0010,262.313851,262.314070,0.000219,0.0001 +N2,2700.00,36.800628,36.799526,-0.001102,-0.0030,81638.593253,81637.708910,-0.884343,-0.0011,263.001328,263.001529,0.000200,0.0001 +N2,2750.00,36.842467,36.841136,-0.001330,-0.0036,83479.677384,83478.732296,-0.945088,-0.0011,263.676973,263.677151,0.000178,0.0001 +N2,2800.00,36.882734,36.881161,-0.001574,-0.0043,85322.813755,85321.796114,-1.017641,-0.0012,264.341184,264.341335,0.000152,0.0001 +N2,2850.00,36.921525,36.919699,-0.001826,-0.0049,87167.926215,87166.823600,-1.102615,-0.0013,264.994337,264.994458,0.000122,0.0000 +N2,2900.00,36.958927,36.956844,-0.002082,-0.0056,89014.943137,89013.742806,-1.200331,-0.0013,265.636793,265.636880,0.000088,0.0000 +N2,2950.00,36.995019,36.992683,-0.002337,-0.0063,90863.797083,90862.486256,-1.310828,-0.0014,266.268894,266.268944,0.000050,0.0000 +N2,3000.00,37.029878,37.027294,-0.002584,-0.0070,92714.424500,92712.990615,-1.433885,-0.0015,266.890967,266.890976,0.000009,0.0000 +NH3,300.00,35.696953,35.678262,-0.018691,-0.0524,64.709005,24.020503,-40.688502,-62.8792,192.990667,192.990838,0.000172,0.0001 +NH3,350.00,37.158635,37.096774,-0.061861,-0.1665,1885.307603,1842.481907,-42.825696,-2.2715,198.600700,198.594370,-0.006331,-0.0032 +NH3,400.00,38.749240,38.677600,-0.071640,-0.1849,3782.676555,3736.383618,-46.292937,-1.2238,203.665503,203.649926,-0.015576,-0.0076 +NH3,450.00,40.388287,40.328948,-0.059339,-0.1469,5761.019098,5711.398676,-49.620422,-0.8613,208.323911,208.300482,-0.023429,-0.0112 +NH3,500.00,42.034070,41.991704,-0.042366,-0.1008,7821.607750,7769.455127,-52.152623,-0.6668,212.664482,212.635702,-0.028781,-0.0135 +NH3,550.00,43.663550,43.631067,-0.032483,-0.0744,9964.149201,9910.170272,-53.978930,-0.5417,216.747369,216.715099,-0.032270,-0.0149 +NH3,600.00,45.263443,45.228529,-0.034914,-0.0771,12187.466763,12131.857850,-55.608913,-0.4563,220.615438,220.580333,-0.035105,-0.0159 +NH3,650.00,46.825898,46.775915,-0.049983,-0.1067,14489.867837,14432.185212,-57.682625,-0.3981,224.300412,224.261996,-0.038416,-0.0171 +NH3,700.00,48.346258,48.271225,-0.075033,-0.1552,16869.353898,16808.579634,-60.774264,-0.3603,227.826494,227.783507,-0.042987,-0.0189 +NH3,750.00,49.821830,49.715754,-0.106077,-0.2129,19323.746484,19258.460651,-65.285833,-0.3379,231.212621,231.163420,-0.049200,-0.0213 +NH3,800.00,51.251171,51.112098,-0.139073,-0.2714,21850.765943,21779.351997,-71.413946,-0.3268,234.473926,234.416828,-0.057099,-0.0244 +NH3,850.00,52.633659,52.462751,-0.170908,-0.3247,24448.082380,24368.909531,-79.172849,-0.3238,237.622730,237.556233,-0.066497,-0.0280 +NH3,900.00,53.969229,53.769106,-0.200123,-0.3708,27113.349628,27024.889788,-88.459840,-0.3263,240.669215,240.592110,-0.077105,-0.0320 +NH3,950.00,55.258202,55.030733,-0.227469,-0.4116,29844.228488,29745.075942,-99.152547,-0.3322,243.621922,243.533261,-0.088661,-0.0364 +NH3,1000.00,56.501172,56.244853,-0.256319,-0.4537,32638.403000,32527.172814,-111.230186,-0.3408,246.488097,246.387053,-0.101045,-0.0410 +NH3,1050.00,57.698929,57.400299,-0.298630,-0.5176,35493.592060,35368.432788,-125.159271,-0.3526,249.273963,249.159334,-0.114628,-0.0460 +NH3,1100.00,58.852408,58.520625,-0.331783,-0.5638,38407.557874,38266.615334,-140.942540,-0.3670,251.984909,251.855602,-0.129307,-0.0513 +NH3,1150.00,59.962651,59.600631,-0.362020,-0.6037,41378.112203,41219.821957,-158.290246,-0.3825,254.625654,254.480929,-0.144725,-0.0568 +NH3,1200.00,61.030782,60.637582,-0.393200,-0.6443,44403.121051,44225.959909,-177.161142,-0.3990,257.200357,257.039574,-0.160783,-0.0625 +NH3,1250.00,62.057986,61.630411,-0.427575,-0.6890,47480.508222,47282.844152,-197.664070,-0.4163,259.712716,259.535198,-0.177518,-0.0684 +NH3,1300.00,63.045497,62.579170,-0.466327,-0.7397,50608.258060,50388.266118,-219.991941,-0.4347,262.166041,261.971014,-0.195027,-0.0744 +NH3,1350.00,63.994588,63.484651,-0.509936,-0.7968,53784.417580,53540.039622,-244.377958,-0.4544,264.563314,264.349886,-0.213428,-0.0807 +NH3,1400.00,64.906558,64.348127,-0.558431,-0.8604,57007.098141,56736.030992,-271.067149,-0.4755,266.907240,266.674404,-0.232836,-0.0872 +NH3,1450.00,65.748089,65.171167,-0.576922,-0.8775,60270.054065,59974.178259,-295.875806,-0.4909,269.197277,268.946937,-0.250340,-0.0930 +NH3,1500.00,66.586688,65.955514,-0.631174,-0.9479,63578.643620,63252.502727,-326.140892,-0.5130,271.440523,271.169668,-0.270856,-0.0998 +NH3,1550.00,67.375073,66.702998,-0.672076,-0.9975,66927.886457,66569.115273,-358.771183,-0.5361,273.636874,273.344623,-0.292251,-0.1068 +NH3,1600.00,68.117980,67.415470,-0.702510,-1.0313,70315.393362,69922.218991,-393.174371,-0.5592,275.787790,275.473697,-0.314093,-0.1139 +NH3,1650.00,68.819465,68.094766,-0.724699,-1.0530,73738.994405,73310.109332,-428.885072,-0.5816,277.894733,277.558664,-0.336069,-0.1209 +NH3,1700.00,69.483026,68.742679,-0.740347,-1.0655,77196.708062,76731.172562,-465.535500,-0.6031,279.959143,279.601193,-0.357950,-0.1279 +NH3,1750.00,70.111692,69.360941,-0.750752,-1.0708,80686.715634,80183.883094,-502.832540,-0.6232,281.982434,281.602861,-0.379573,-0.1346 +NH3,1800.00,70.708106,69.951209,-0.756897,-1.0705,84207.339924,83666.800121,-540.539803,-0.6419,283.965976,283.565159,-0.400817,-0.1411 +NH3,1850.00,71.274580,70.515065,-0.759515,-1.0656,87757.027376,87178.563831,-578.463545,-0.6592,285.911096,285.489498,-0.421598,-0.1475 +NH3,1900.00,71.813153,71.054007,-0.759146,-1.0571,91334.333033,90717.891406,-616.441627,-0.6749,287.819074,287.377219,-0.441855,-0.1535 +NH3,1950.00,72.325628,71.569453,-0.756175,-1.0455,94937.907808,94283.572946,-654.334862,-0.6892,289.691137,289.229596,-0.461541,-0.1593 +NH3,2000.00,72.813609,72.062741,-0.750868,-1.0312,98566.487667,97874.467426,-692.020241,-0.7021,291.528465,291.047842,-0.480623,-0.1649 +NH3,2050.00,73.278528,72.535130,-0.743398,-1.0145,102218.884390,101489.498740,-729.385650,-0.7136,293.332186,292.833109,-0.499077,-0.1701 +NH3,2100.00,73.721674,72.987805,-0.733868,-0.9955,105893.977660,105127.651885,-766.325775,-0.7237,295.103379,294.586499,-0.516880,-0.1752 +NH3,2150.00,74.144205,73.421881,-0.722324,-0.9742,109590.708246,108787.969303,-802.738943,-0.7325,296.843078,296.309060,-0.534017,-0.1799 +NH3,2200.00,74.547172,73.838402,-0.708770,-0.9508,113308.072125,112469.547413,-838.524712,-0.7400,298.552268,298.001796,-0.550472,-0.1844 +NH3,2250.00,74.931532,74.238351,-0.693181,-0.9251,117045.115379,116171.533316,-873.582063,-0.7464,300.231893,299.665663,-0.566229,-0.1886 +NH3,2300.00,75.298157,74.622650,-0.675507,-0.8971,120800.929765,119893.121699,-907.808066,-0.7515,301.882852,301.301577,-0.581275,-0.1925 +NH3,2350.00,75.647848,74.992164,-0.655683,-0.8668,124574.648848,123633.551919,-941.096929,-0.7554,303.506006,302.910412,-0.595594,-0.1962 +NH3,2400.00,75.981342,75.347706,-0.633636,-0.8339,128365.444613,127392.105267,-973.339346,-0.7583,305.102177,304.493006,-0.609171,-0.1997 +NH3,2450.00,76.299322,75.690039,-0.609283,-0.7985,132172.524500,131168.102409,-1004.422091,-0.7599,306.672151,306.050161,-0.621990,-0.2028 +NH3,2500.00,76.602420,76.019879,-0.582541,-0.7605,135995.128786,134960.900982,-1034.227804,-0.7605,308.216680,307.582646,-0.634034,-0.2057 +NH3,2550.00,76.891225,76.337900,-0.553325,-0.7196,139832.528286,138769.893361,-1062.634925,-0.7599,309.736482,309.091197,-0.645286,-0.2083 +NH3,2600.00,77.166289,76.644734,-0.521555,-0.6759,143684.022306,142594.504557,-1089.517749,-0.7583,311.232247,310.576520,-0.655727,-0.2107 +NH3,2650.00,77.428129,76.940976,-0.487153,-0.6292,147548.936838,146434.190262,-1114.746576,-0.7555,312.704632,312.039292,-0.665339,-0.2128 +NH3,2700.00,77.677231,77.227187,-0.450045,-0.5794,151426.622952,150288.435020,-1138.187932,-0.7516,314.154269,313.480165,-0.674104,-0.2146 +NH3,2750.00,77.914056,77.503891,-0.410164,-0.5264,155316.455361,154156.750518,-1159.704843,-0.7467,315.581764,314.899762,-0.682001,-0.2161 +NH3,2800.00,78.139037,77.771586,-0.367451,-0.4703,159217.831147,158038.673993,-1179.157153,-0.7406,316.987697,316.298684,-0.689012,-0.2174 +NH3,2850.00,78.352590,78.030737,-0.321853,-0.4108,163130.168610,161933.766736,-1196.401874,-0.7334,318.372626,317.677507,-0.695118,-0.2183 +NH3,2900.00,78.555108,78.281786,-0.273323,-0.3479,167052.906249,165841.612698,-1211.293550,-0.7251,319.737085,319.036786,-0.700299,-0.2190 +NH3,2950.00,78.746969,78.525146,-0.221824,-0.2817,170985.501835,169761.817189,-1223.684645,-0.7157,321.081591,320.377054,-0.704537,-0.2194 +NH3,3000.00,78.928534,78.761208,-0.167326,-0.2120,174927.431583,173694.005653,-1233.425930,-0.7051,322.406636,321.698823,-0.707813,-0.2195 +O2,300.00,29.392125,29.387511,-0.004614,-0.0157,54.094874,54.358383,0.263509,0.4871,205.329062,205.331223,0.002161,0.0011 +O2,350.00,29.703219,29.700985,-0.002234,-0.0075,1531.056265,1531.068091,0.011827,0.0008,209.881938,209.883319,0.001380,0.0007 +O2,400.00,30.104360,30.114673,0.010313,0.0343,3025.919317,3026.127834,0.208517,0.0069,213.873565,213.875452,0.001886,0.0009 +O2,450.00,30.571359,30.589091,0.017732,0.0580,4542.590643,4543.543170,0.952526,0.0210,217.445788,217.449419,0.003630,0.0017 +O2,500.00,31.078519,31.091646,0.013127,0.0422,6083.724917,6085.501268,1.776352,0.0292,220.692813,220.698184,0.005371,0.0024 +O2,550.00,31.599415,31.597825,-0.001591,-0.0050,7650.671673,7652.764266,2.092593,0.0274,223.679338,223.685323,0.005985,0.0027 +O2,600.00,32.107241,32.090082,-0.017159,-0.0534,9243.448704,9245.048582,1.599878,0.0173,226.450812,226.455949,0.005137,0.0023 +O2,650.00,32.574980,32.556509,-0.018471,-0.0567,10860.727739,10861.339449,0.611710,0.0056,229.039590,229.043146,0.003556,0.0016 +O2,700.00,32.975489,32.989736,0.014247,0.0432,12499.826253,12500.144109,0.317856,0.0025,231.468811,231.471917,0.003106,0.0013 +O2,750.00,33.382926,33.386102,0.003177,0.0095,14157.305129,14159.696992,2.391864,0.0169,233.754654,233.761706,0.007053,0.0030 +O2,800.00,33.743190,33.745052,0.001862,0.0055,15835.631676,15838.129267,2.497592,0.0158,235.920863,235.928052,0.007190,0.0030 +O2,850.00,34.065638,34.068692,0.003054,0.0090,17530.995065,17533.612427,2.617362,0.0149,237.976379,237.983714,0.007335,0.0031 +O2,900.00,34.356763,34.361468,0.004705,0.0137,19241.674270,19244.482924,2.808654,0.0146,239.931894,239.939447,0.007553,0.0031 +O2,950.00,34.621547,34.629928,0.008381,0.0242,20966.232931,20969.352912,3.119981,0.0149,241.796672,241.804560,0.007889,0.0033 +O2,1000.00,34.863903,34.882545,0.018642,0.0535,22703.455583,22707.210697,3.755114,0.0165,243.578776,243.587314,0.008538,0.0035 +O2,1050.00,35.086973,35.122539,0.035566,0.1014,24452.302100,24457.477468,5.175368,0.0212,245.285262,245.295182,0.009920,0.0040 +O2,1100.00,35.293331,35.333397,0.040065,0.1135,26211.874561,26218.980417,7.105856,0.0271,246.922331,246.934047,0.011716,0.0047 +O2,1150.00,35.485132,35.522398,0.037266,0.1050,27981.392763,27990.453869,9.061106,0.0324,248.495464,248.508918,0.013454,0.0054 +O2,1200.00,35.664210,35.694947,0.030737,0.0862,29760.175858,29770.946731,10.770873,0.0362,250.009523,250.024434,0.014911,0.0060 +O2,1250.00,35.832158,35.855081,0.022924,0.0640,31547.628391,31559.742201,12.113810,0.0384,251.468848,251.484856,0.016008,0.0064 +O2,1300.00,35.990377,36.005827,0.015451,0.0429,33343.229565,33356.298804,13.069238,0.0392,252.877323,252.894081,0.016759,0.0066 +O2,1350.00,36.140122,36.149461,0.009339,0.0258,35146.524890,35160.206718,13.681828,0.0389,254.238444,254.255666,0.017222,0.0068 +O2,1400.00,36.282534,36.287696,0.005162,0.0142,36957.119619,36971.155191,14.035571,0.0380,255.555369,255.572849,0.017480,0.0068 +O2,1450.00,36.418658,36.421822,0.003164,0.0087,38774.673548,38788.908049,14.234501,0.0367,256.830963,256.848582,0.017619,0.0069 +O2,1500.00,36.549466,36.552807,0.003342,0.0091,40598.896849,40613.285203,14.388354,0.0354,258.067830,258.085554,0.017724,0.0069 +O2,1550.00,36.675868,36.681376,0.005508,0.0150,42429.546715,42444.148601,14.601886,0.0344,259.268353,259.286216,0.017864,0.0069 +O2,1600.00,36.798728,36.808065,0.009337,0.0254,44266.424632,44281.391529,14.966898,0.0338,260.434712,260.452807,0.018095,0.0069 +O2,1650.00,36.918866,36.933271,0.014405,0.0390,46109.374144,46124.930436,15.556292,0.0337,261.568915,261.587372,0.018457,0.0071 +O2,1700.00,37.037071,37.057278,0.020207,0.0546,47958.279021,47974.698674,16.419653,0.0342,262.672812,262.691784,0.018972,0.0072 +O2,1750.00,37.154104,37.180290,0.026186,0.0705,49813.061731,49830.641708,17.579977,0.0353,263.748115,263.767760,0.019645,0.0074 +O2,1800.00,37.270702,37.302445,0.031744,0.0852,51673.682170,51692.713445,19.031275,0.0368,264.796414,264.816876,0.020462,0.0077 +O2,1850.00,37.387582,37.423832,0.036250,0.0970,53540.136604,53560.873437,20.736833,0.0387,265.819185,265.840581,0.021396,0.0080 +O2,1900.00,37.505446,37.544504,0.039058,0.1041,55412.456769,55435.084748,22.627979,0.0408,266.817808,266.840213,0.022405,0.0084 +O2,1950.00,37.624983,37.664486,0.039503,0.1050,57290.709117,57315.312343,24.603226,0.0429,267.793572,267.817003,0.023431,0.0087 +O2,2000.00,37.746866,37.783781,0.036915,0.0978,59174.994167,59201.521877,26.527710,0.0448,268.747685,268.772090,0.024406,0.0091 +O2,2050.00,37.859152,37.902379,0.043227,0.1142,61066.538327,61093.678808,27.140481,0.0444,269.681743,269.706530,0.024787,0.0092 +O2,2100.00,37.967005,38.020257,0.053252,0.1403,62962.184161,62991.747741,29.563580,0.0470,270.595347,270.621301,0.025954,0.0096 +O2,2150.00,38.076506,38.137385,0.060879,0.1599,64863.266217,64895.691974,32.425757,0.0500,271.490010,271.517310,0.027301,0.0101 +O2,2200.00,38.187124,38.253730,0.066606,0.1744,66769.853313,66805.473183,35.619870,0.0533,272.366635,272.395404,0.028769,0.0106 +O2,2250.00,38.298397,38.369252,0.070855,0.1850,68681.989489,68721.051228,39.061739,0.0569,273.226054,273.256370,0.030316,0.0111 +O2,2300.00,38.409927,38.483914,0.073987,0.1926,70599.697296,70642.384043,42.686747,0.0605,274.069032,274.100941,0.031909,0.0116 +O2,2350.00,38.521367,38.597675,0.076309,0.1981,72522.980669,72569.427598,46.446929,0.0640,274.896278,274.929804,0.033526,0.0122 +O2,2400.00,38.632417,38.710498,0.078081,0.2021,74451.827435,74502.135908,50.308473,0.0676,275.708450,275.743602,0.035152,0.0127 +O2,2450.00,38.742819,38.822344,0.079524,0.2053,76386.211519,76440.461085,54.249566,0.0710,276.506158,276.542936,0.036778,0.0133 +O2,2500.00,38.852354,38.933178,0.080824,0.2080,78326.094882,78384.353415,58.258533,0.0744,277.289972,277.328370,0.038397,0.0138 +O2,2550.00,38.960830,39.042967,0.082137,0.2108,80271.429235,80333.761460,62.332225,0.0777,278.060424,278.100434,0.040011,0.0144 +O2,2600.00,39.068087,39.151680,0.083592,0.2140,82222.157550,82288.632173,66.474623,0.0808,278.818008,278.859628,0.041619,0.0149 +O2,2650.00,39.173991,39.259288,0.085297,0.2177,84178.215402,84248.911019,70.695617,0.0840,279.563193,279.606420,0.043227,0.0155 +O2,2700.00,39.278427,39.365765,0.087338,0.2224,86139.532166,86214.542105,75.009940,0.0871,280.296413,280.341254,0.044840,0.0160 +O2,2750.00,39.381303,39.471089,0.089786,0.2280,88106.032076,88185.468318,79.436242,0.0902,281.018082,281.064547,0.046465,0.0165 +O2,2800.00,39.482543,39.575239,0.092695,0.2348,90077.635177,90161.631449,83.996272,0.0932,281.728586,281.776694,0.048108,0.0171 +O2,2850.00,39.582090,39.678196,0.096106,0.2428,92054.258173,92142.972332,88.714159,0.0964,282.428291,282.478069,0.049778,0.0176 +O2,2900.00,39.679898,39.779947,0.100049,0.2521,94035.815189,94129.430969,93.615780,0.0996,283.117544,283.169026,0.051483,0.0182 +O2,2950.00,39.775936,39.880476,0.104541,0.2628,96022.218453,96120.946654,98.728201,0.1028,283.796670,283.849900,0.053230,0.0188 +O2,3000.00,39.870183,39.979775,0.109592,0.2749,98013.378917,98117.458096,104.079179,0.1062,284.465981,284.521010,0.055029,0.0193 +SO2,300.00,39.943349,39.909457,-0.033891,-0.0848,72.586000,105.970803,33.384803,45.9934,248.457266,248.468454,0.011187,0.0045 +SO2,350.00,41.787922,41.707362,-0.080560,-0.1928,2116.247035,2146.551584,30.304549,1.4320,254.754247,254.756030,0.001783,0.0007 +SO2,400.00,43.519329,43.432442,-0.086887,-0.1997,4249.471583,4275.476375,26.004793,0.6120,260.448723,260.439032,-0.009691,-0.0037 +SO2,450.00,45.112280,45.034667,-0.077613,-0.1720,6465.863246,6487.733978,21.870733,0.3382,265.667947,265.648507,-0.019440,-0.0073 +SO2,500.00,46.559259,46.489268,-0.069991,-0.1503,8758.262271,8776.472956,18.210685,0.2079,270.497176,270.470015,-0.027160,-0.0100 +SO2,550.00,47.861309,47.789138,-0.072171,-0.1508,11119.371311,11134.075800,14.704489,0.1322,274.996944,274.963102,-0.033842,-0.0123 +SO2,600.00,49.023946,48.938381,-0.085565,-0.1745,13542.068309,13552.872750,10.804441,0.0798,279.212255,279.171634,-0.040621,-0.0145 +SO2,650.00,50.055175,49.947697,-0.107478,-0.2147,16019.574975,16025.579925,6.004950,0.0375,283.177831,283.129539,-0.048292,-0.0171 +SO2,700.00,50.964468,50.831230,-0.133238,-0.2614,18545.553052,18545.545362,-0.007690,-0.0000,286.921317,286.864125,-0.057192,-0.0199 +SO2,750.00,51.762197,51.604412,-0.157785,-0.3048,21114.162085,21106.863863,-7.298221,-0.0346,290.465315,290.398073,-0.067242,-0.0231 +SO2,800.00,52.459304,52.282474,-0.176830,-0.3371,23720.095519,23704.401927,-15.693592,-0.0662,293.828735,293.750663,-0.078072,-0.0266 +SO2,850.00,53.067112,52.879414,-0.187699,-0.3537,26358.604063,26333.760359,-24.843703,-0.0943,297.027720,296.938557,-0.089163,-0.0300 +SO2,900.00,53.597196,53.407254,-0.189943,-0.3544,29025.511256,28991.193041,-34.318214,-0.1182,300.076300,299.976306,-0.099993,-0.0333 +SO2,950.00,54.061310,53.875517,-0.185793,-0.3437,31717.224130,31673.494387,-43.729744,-0.1379,302.986855,302.876684,-0.110172,-0.0364 +SO2,1000.00,54.471331,54.290836,-0.180495,-0.3314,34430.740667,34377.864088,-52.876578,-0.1536,305.770469,305.650913,-0.119556,-0.0391 +SO2,1050.00,54.839228,54.663191,-0.176036,-0.3210,37163.655122,37101.893096,-61.762025,-0.1662,308.437183,308.308956,-0.128227,-0.0416 +SO2,1100.00,55.177034,54.995947,-0.181088,-0.3282,39914.161903,39843.523513,-70.638390,-0.1770,310.996195,310.859710,-0.136485,-0.0439 +SO2,1150.00,55.496835,55.294926,-0.201909,-0.3638,42681.058435,42600.925399,-80.133035,-0.1877,313.456011,313.311088,-0.144922,-0.0462 +SO2,1200.00,55.810751,55.564903,-0.245847,-0.4405,45463.747303,45372.533170,-91.214132,-0.2006,315.824567,315.670219,-0.154348,-0.0489 +SO2,1250.00,56.045386,55.809831,-0.235555,-0.4203,48258.898290,48156.998592,-101.899698,-0.2112,318.106466,317.943534,-0.162933,-0.0512 +SO2,1300.00,56.282715,56.033004,-0.249711,-0.4437,51067.197738,50953.153984,-114.043754,-0.2233,320.309306,320.136849,-0.172457,-0.0538 +SO2,1350.00,56.498465,56.237193,-0.261273,-0.4624,53886.810505,53759.982833,-126.827672,-0.2354,322.437542,322.255438,-0.182105,-0.0565 +SO2,1400.00,56.695628,56.424740,-0.270888,-0.4778,56716.734772,56576.596089,-140.138683,-0.2471,324.495874,324.304089,-0.191785,-0.0591 +SO2,1450.00,56.876689,56.597642,-0.279047,-0.4906,59556.105175,59402.212897,-153.892278,-0.2584,326.488601,326.287163,-0.201437,-0.0617 +SO2,1500.00,57.043726,56.757607,-0.286119,-0.5016,62404.170094,62236.144788,-168.025306,-0.2693,328.419661,328.208642,-0.211019,-0.0643 +SO2,1550.00,57.198489,56.906108,-0.292382,-0.5112,65260.273338,65077.782632,-182.490705,-0.2796,330.292670,330.072165,-0.220505,-0.0668 +SO2,1600.00,57.342460,57.044417,-0.298044,-0.5198,68123.839267,67926.585782,-197.253484,-0.2896,332.110947,331.881069,-0.229879,-0.0692 +SO2,1650.00,57.476900,57.173642,-0.303258,-0.5276,70994.360613,70782.072987,-212.287626,-0.2990,333.877551,333.638420,-0.239131,-0.0716 +SO2,1700.00,57.602887,57.294750,-0.308137,-0.5349,73871.388458,73643.814745,-227.573714,-0.3081,335.595298,335.347041,-0.248257,-0.0740 +SO2,1750.00,57.721348,57.408587,-0.312761,-0.5418,76754.523923,76511.426828,-243.097095,-0.3167,337.266789,337.009533,-0.257256,-0.0763 +SO2,1800.00,57.833085,57.515900,-0.317185,-0.5484,79643.411246,79384.564769,-258.846476,-0.3250,338.894432,338.628302,-0.266130,-0.0785 +SO2,1850.00,57.938792,57.617348,-0.321444,-0.5548,82537.731980,82262.919151,-274.812828,-0.3330,340.480453,340.205575,-0.274879,-0.0807 +SO2,1900.00,58.039075,57.713513,-0.325562,-0.5609,85437.200112,85146.211556,-290.988557,-0.3406,342.026922,341.743416,-0.283506,-0.0829 +SO2,1950.00,58.134462,57.804914,-0.329548,-0.5669,88341.557929,88034.191071,-307.366858,-0.3479,343.535759,343.243745,-0.292014,-0.0850 +SO2,2000.00,58.225418,57.892013,-0.333405,-0.5726,91250.572500,90926.631264,-323.941236,-0.3550,345.008752,344.708345,-0.300407,-0.0871 +SO2,2050.00,58.312351,57.975223,-0.337128,-0.5781,94164.032684,93823.327551,-340.705133,-0.3618,346.447566,346.138881,-0.308685,-0.0891 +SO2,2100.00,58.395622,58.054914,-0.340708,-0.5834,97081.746556,96724.094898,-357.651658,-0.3684,347.853757,347.536905,-0.316853,-0.0911 +SO2,2150.00,58.475551,58.131418,-0.344134,-0.5885,100003.539194,99628.765811,-374.773382,-0.3748,349.228778,348.903868,-0.324910,-0.0930 +SO2,2200.00,58.552424,58.205035,-0.347389,-0.5933,102929.250770,102537.188568,-392.062202,-0.3809,350.573989,350.241130,-0.332859,-0.0949 +SO2,2250.00,58.626493,58.276034,-0.350459,-0.5978,105858.734896,105449.225662,-409.509235,-0.3868,351.890663,351.549962,-0.340701,-0.0968 +SO2,2300.00,58.697986,58.344660,-0.353326,-0.6019,108791.857183,108364.752423,-427.104760,-0.3926,353.179997,352.831561,-0.348435,-0.0987 +SO2,2350.00,58.767106,58.411134,-0.355972,-0.6057,111728.493984,111283.655803,-444.838181,-0.3981,354.443112,354.087050,-0.356063,-0.1005 +SO2,2400.00,58.834036,58.475656,-0.358380,-0.6091,114668.531296,114205.833289,-462.698007,-0.4035,355.681066,355.317483,-0.363583,-0.1022 +SO2,2450.00,58.898940,58.538409,-0.360530,-0.6121,117611.863796,117131.191937,-480.671859,-0.4087,356.894852,356.523857,-0.370995,-0.1040 +SO2,2500.00,58.961967,58.599560,-0.362407,-0.6146,120558.393994,120059.647514,-498.746480,-0.4137,358.085408,357.707110,-0.378298,-0.1056 +SO2,2550.00,59.023253,58.659259,-0.363994,-0.6167,123508.031480,122991.123725,-516.907755,-0.4185,359.253617,358.868126,-0.385491,-0.1073 +SO2,2600.00,59.082919,58.717645,-0.365273,-0.6182,126460.692267,125925.551525,-535.140742,-0.4232,360.400316,360.007744,-0.392572,-0.1089 +SO2,2650.00,59.141076,58.774846,-0.366230,-0.6192,129416.298199,128862.868494,-553.429705,-0.4276,361.526293,361.126754,-0.399539,-0.1105 +SO2,2700.00,59.197827,58.830977,-0.366850,-0.6197,132374.776430,131803.018282,-571.758148,-0.4319,362.632297,362.225906,-0.406391,-0.1121 +SO2,2750.00,59.253263,58.886144,-0.367119,-0.6196,135336.058962,134745.950104,-590.108858,-0.4360,363.719035,363.305909,-0.413125,-0.1136 +SO2,2800.00,59.307469,58.940447,-0.367023,-0.6188,138300.082225,137691.618286,-608.463939,-0.4400,364.787179,364.367439,-0.419740,-0.1151 +SO2,2850.00,59.360524,58.993974,-0.366550,-0.6175,141266.786709,140639.981856,-626.804853,-0.4437,365.837366,365.411133,-0.426233,-0.1165 +SO2,2900.00,59.412498,59.046810,-0.365689,-0.6155,144236.116629,143591.004167,-645.112462,-0.4473,366.870201,366.437600,-0.432601,-0.1179 +SO2,2950.00,59.463458,59.099030,-0.364428,-0.6129,147208.019627,146544.652563,-663.367064,-0.4506,367.886260,367.447418,-0.438842,-0.1193 +SO2,3000.00,59.513463,59.150705,-0.362758,-0.6095,150182.446500,149500.898069,-681.548431,-0.4538,368.886089,368.441136,-0.444953,-0.1206 diff --git a/docs/audit/validation_01_Cp.png b/docs/audit/validation_01_Cp.png index ae544f8..8e6d27b 100644 Binary files a/docs/audit/validation_01_Cp.png and b/docs/audit/validation_01_Cp.png differ diff --git a/docs/audit/validation_02_Enthalpy.png b/docs/audit/validation_02_Enthalpy.png index b93ca04..9c9fa83 100644 Binary files a/docs/audit/validation_02_Enthalpy.png and b/docs/audit/validation_02_Enthalpy.png differ diff --git a/docs/audit/validation_03_Entropy.png b/docs/audit/validation_03_Entropy.png index 335463e..8be8d4d 100644 Binary files a/docs/audit/validation_03_Entropy.png and b/docs/audit/validation_03_Entropy.png differ diff --git a/docs/audit/validation_errors.png b/docs/audit/validation_errors.png index da9e6ab..bf2ae48 100644 Binary files a/docs/audit/validation_errors.png and b/docs/audit/validation_errors.png differ diff --git a/docs/audit/validation_summary.txt b/docs/audit/validation_summary.txt index 0989638..eeb9553 100644 --- a/docs/audit/validation_summary.txt +++ b/docs/audit/validation_summary.txt @@ -10,29 +10,39 @@ NH₃ and SO₂ added as replacements. ─── CO ─── - Cp(T): Mean=+0.0243% Max|err|=0.1979% RMSE=0.0670% - ΔH(T): Mean=-0.3813% Max|err|=18.7265% RMSE=2.5268% - S°(T): Mean=+0.0014% Max|err|=0.0052% RMSE=0.0022% + Cp(T): Mean=+0.0249% Max|err|=0.1985% RMSE=0.0672% + ΔH(T): Mean=-0.4042% Max|err|=19.8282% RMSE=2.6755% + S°(T): Mean=+0.0020% Max|err|=0.0058% RMSE=0.0026% ─── CO2 ─── - Cp(T): Mean=-0.0253% Max|err|=0.1166% RMSE=0.0427% - ΔH(T): Mean=+0.5413% Max|err|=26.6807% RMSE=3.6003% - S°(T): Mean=-0.0023% Max|err|=0.0052% RMSE=0.0026% + Cp(T): Mean=-0.0247% Max|err|=0.1161% RMSE=0.0424% + ΔH(T): Mean=+0.4716% Max|err|=23.2759% RMSE=3.1408% + S°(T): Mean=-0.0018% Max|err|=0.0047% RMSE=0.0021% ─── H2O ─── - Cp(T): Mean=+0.8449% Max|err|=1.9398% RMSE=1.0935% - ΔH(T): Mean=+0.3098% Max|err|=0.8792% RMSE=0.4231% - S°(T): Mean=+0.0526% Max|err|=0.1705% RMSE=0.0768% + Cp(T): Mean=+0.8454% Max|err|=1.9404% RMSE=1.0940% + ΔH(T): Mean=+0.3065% Max|err|=0.8786% RMSE=0.4223% + S°(T): Mean=+0.0532% Max|err|=0.1711% RMSE=0.0772% + + ─── N2 ─── + Cp(T): Mean=-0.0008% Max|err|=0.0365% RMSE=0.0105% + ΔH(T): Mean=-0.0023% Max|err|=0.0495% RMSE=0.0071% + S°(T): Mean=+0.0002% Max|err|=0.0005% RMSE=0.0002% ─── NH3 ─── - Cp(T): Mean=-0.6316% Max|err|=1.0714% RMSE=0.7157% - ΔH(T): Mean=-1.7565% Max|err|=62.4752% RMSE=8.4522% - S°(T): Mean=-0.1174% Max|err|=0.2201% RMSE=0.1405% + Cp(T): Mean=-0.6311% Max|err|=1.0708% RMSE=0.7152% + ΔH(T): Mean=-1.7643% Max|err|=62.8792% RMSE=8.5066% + S°(T): Mean=-0.1169% Max|err|=0.2195% RMSE=0.1401% + + ─── O2 ─── + Cp(T): Mean=+0.1011% Max|err|=0.2749% RMSE=0.1357% + ΔH(T): Mean=+0.0530% Max|err|=0.4871% RMSE=0.0840% + S°(T): Mean=+0.0083% Max|err|=0.0193% RMSE=0.0098% ─── SO2 ─── - Cp(T): Mean=-0.4636% Max|err|=0.6203% RMSE=0.4901% - ΔH(T): Mean=+0.6769% Max|err|=48.3231% RMSE=6.5269% - S°(T): Mean=-0.0676% Max|err|=0.1212% RMSE=0.0770% + Cp(T): Mean=-0.4630% Max|err|=0.6197% RMSE=0.4896% + ΔH(T): Mean=+0.6290% Max|err|=45.9934% RMSE=6.2129% + S°(T): Mean=-0.0670% Max|err|=0.1206% RMSE=0.0765% ─── Notes ─── * ΔH max|err| near 298 K is a mathematical artefact: diff --git a/docs/cheat_sheet/cheat_sheet.md b/docs/cheat_sheet/cheat_sheet.md index e4234e3..76dcb25 100644 --- a/docs/cheat_sheet/cheat_sheet.md +++ b/docs/cheat_sheet/cheat_sheet.md @@ -1,6 +1,6 @@ # pyglenn — Cheat Sheet -> **v0.1.13** · 2030 species · 3772 intervals · zero dependencies +> **v0.2.0** · 2035 species · 3779 intervals · zero dependencies > **Author:** Dr. Reginaldo G. Leão Jr. · [ProfLeao/pyglenn](https://github.com/ProfLeao/pyglenn) --- @@ -15,12 +15,12 @@ pip install pyglenn from pyglenn import ThermochemicalCalculator with ThermochemicalCalculator() as calc: - sid = calc.get_available_species('CH4', exact_match=True)[0]['id'] + sid = calc.get_available_species('CH4', exact_match=True)[0].id prop = calc.calculate_properties(sid, 1000.0) - print(f"Cp = {prop['cp']:.2f} J/(mol·K)") - print(f"H° = {prop['h_relative']:.1f} J/mol") - print(f"S° = {prop['s']:.3f} J/(mol·K)") + print(f"Cp = {prop.cp:.2f} J/(mol·K)") + print(f"H° = {prop.h_relative:.1f} J/mol") + print(f"S° = {prop.s:.3f} J/(mol·K)") ``` --- @@ -31,7 +31,7 @@ with ThermochemicalCalculator() as calc: ```python # Exact match (recommended) — 'N2' returns N₂, not Be₃N₂ -calc.get_available_species('N2', exact_match=True)[0]['id'] +calc.get_available_species('N2', exact_match=True)[0].id # Substring search (legacy) calc.get_available_species('CH4') @@ -41,20 +41,20 @@ calc.get_available_species('CH4') ```python p = calc.calculate_properties(species_id, 1000.0) -# p['cp'] → J/(mol·K) -# p['h_relative'] → J/mol (relative to 0 K) -# p['s'] → J/(mol·K) -# p['species_name'] → 'CH4' -# p['phase'] → 'gas' | 'condensed' -# p['temp_interval']→ [T_min, T_max] +# p.cp → J/(mol·K) +# p.h_relative → J/mol (absolute H°(T), NASA-7 convention) +# p.s → J/(mol·K) +# p.species_name → 'CH4' +# p.phase → 'gas' | 'condensed' +# p.temp_interval → (T_min, T_max) ``` ### 📈 Temperature sweep ```python -sid = calc.get_available_species('CO2', exact_match=True)[0]['id'] +sid = calc.get_available_species('CO2', exact_match=True)[0].id results = calc.get_properties_range(sid, [300, 500, 1000, 1500]) -# → {300.0: {...}, 500.0: {...}, ...} +# → {300.0: ThermoProperties, 500.0: ThermoProperties, ...} ``` ### 🧪 Enthalpy diff --git a/docs/cheat_sheet/cheat_sheet.pdf b/docs/cheat_sheet/cheat_sheet.pdf index ef7e433..4c46ae8 100644 Binary files a/docs/cheat_sheet/cheat_sheet.pdf and b/docs/cheat_sheet/cheat_sheet.pdf differ diff --git a/docs/paper_zenodo/paper.md b/docs/paper_zenodo/paper.md index 7f7cfe6..5e04608 100644 --- a/docs/paper_zenodo/paper.md +++ b/docs/paper_zenodo/paper.md @@ -117,10 +117,10 @@ from pyglenn import ThermochemicalCalculator with ThermochemicalCalculator() as calc: species = calc.get_available_species("CH4", exact_match=True) - props = calc.calculate_properties(species[0]["id"], 1000.0) - print(f"Cp = {props['cp']:.2f} J/(mol·K)") - print(f"H° = {props['h_relative']:.1f} J/mol") - print(f"S° = {props['s']:.3f} J/(mol·K)") + props = calc.calculate_properties(species[0].id, 1000.0) + print(f"Cp = {props.cp:.2f} J/(mol·K)") + print(f"H° = {props.h_relative:.1f} J/mol") + print(f"S° = {props.s:.3f} J/(mol·K)") ``` No explicit database path is required — the package ships with a pre-built diff --git a/docs/source/database.rst b/docs/source/database.rst index bca829f..01ff398 100644 --- a/docs/source/database.rst +++ b/docs/source/database.rst @@ -37,6 +37,15 @@ The database contains three linked tables: - ``a1`` … ``a7`` — polynomial coefficients - ``b1``, ``b2`` — integration constants +**metadata** — additive dataset metadata + +- ``key`` — metadata key +- ``value`` — metadata value + +Newly built NASA Glenn/CEA databases store ``gas_constant_ref = 8.314510`` and +its provenance in this table. Legacy databases without this table remain +readable and use the universal fallback constant. + NASA Polynomial Format ======================= @@ -79,25 +88,29 @@ You can query the database directly using :class:`~pyglenn.ThermoDBQuery`: # Search species by name species = query.find_species('CH4', exact_match=True) - print(species[0]['name'], species[0]['phase']) + print(species[0].name, species[0].phase) # Get complete data (metadata + all intervals + coefficients) - data = query.get_species_data(species[0]['id']) - for interval in data['intervals']: - print(f" {interval['temp_min']}–{interval['temp_max']} K") - coeffs = interval['coefficients'] - print(f" a1={coeffs['a1']}, …, a7={coeffs['a7']}") + data = query.get_species_data(species[0].id) + for interval in data.intervals: + print(f" {interval.temp_min}–{interval.temp_max} K") + coeffs = interval.coefficients + print(f" a1={coeffs.a1}, …, a7={coeffs.a7}") query.close() -Universal Gas Constant -====================== +Gas Constants +============= -The universal gas constant is available as :data:`pyglenn.R`: +The universal CODATA gas constant and the NASA Glenn/CEA dataset reference +constant are available as :data:`pyglenn.R_UNIVERSAL` and +:data:`pyglenn.R_GLENN`. :data:`pyglenn.R` remains a compatibility alias for +the universal value. .. code-block:: python - from pyglenn import R + from pyglenn import R_GLENN, R_UNIVERSAL - print(f"R = {R} J/(mol·K)") + print(f"R (universal) = {R_UNIVERSAL} J/(mol·K)") + print(f"R (NASA Glenn) = {R_GLENN} J/(mol·K)") diff --git a/docs/source/index.rst b/docs/source/index.rst index d2af89d..3a35200 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,17 +44,17 @@ The database is **bundled** — no manual setup required: with ThermochemicalCalculator() as calc: result = calc.calculate_properties( - calc.get_available_species('CH4', exact_match=True)[0]['id'], 298.15 + calc.get_available_species('CH4', exact_match=True)[0].id, 298.15 ) - print(f"Cp = {result['cp']:.3f} J/mol·K") - print(f"H° = {result['h_relative']:.3f} J/mol") - print(f"S° = {result['s']:.3f} J/mol·K") + print(f"Cp = {result.cp:.3f} J/mol·K") + print(f"H° = {result.h_relative:.3f} J/mol") + print(f"S° = {result.s:.3f} J/mol·K") Features ======== - Compute :math:`C_p(T)`, :math:`H^\circ(T)`, :math:`S^\circ(T)` for any species -- SQLite database bundled with **~2030 species, 3772 temperature intervals** — no setup needed +- SQLite database bundled with **2,035 species, 3,779 temperature intervals** — no setup needed - CLI interface for quick lookups - Temperature range validation diff --git a/docs/source/usage.rst b/docs/source/usage.rst index dcae496..b6a6771 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -43,10 +43,10 @@ Simply instantiate with no arguments — the bundled database is used automatica # Find methane — exact_match=True avoids confusion with # species like CH3CHCH4 or C2H4 that contain 'CH4' species = calc.get_available_species('CH4', exact_match=True) - result = calc.calculate_properties(species[0]['id'], 500.0) - print(f"Cp = {result['cp']:.3f} J/(mol·K)") - print(f"H° = {result['h_relative']:.3f} J/mol") - print(f"S° = {result['s']:.3f} J/(mol·K)") + result = calc.calculate_properties(species[0].id, 500.0) + print(f"Cp = {result.cp:.3f} J/(mol·K)") + print(f"H° = {result.h_relative:.3f} J/mol") + print(f"S° = {result.s:.3f} J/(mol·K)") You can also specify a custom database file: @@ -64,3 +64,36 @@ The calculator raises specific exceptions for common errors: - :class:`~pyglenn.SpeciesNotFoundError` — species not found in database - :class:`~pyglenn.TemperatureOutOfRangeError` — temperature outside valid range - :class:`~pyglenn.ThermoCalcError` — base exception for all calculator errors + +Migration from dictionaries (v0.1.x) +==================================== + +As of **v0.2.0**, public methods return typed dataclasses instead of plain +dictionaries. Convert old code by replacing ``result['key']`` with +``result.key``: + +.. code-block:: python + + # v0.1.x (dict) + props = calc.calculate_properties(species_id, 500.0) + cp = props['cp'] + name = props['species_name'] + + # v0.2.0 (dataclass) + props = calc.calculate_properties(species_id, 500.0) + cp = props.cp + name = props.species_name + +To recover the previous dictionary shape (for JSON or legacy consumers), +use :func:`dataclasses.asdict` or the ``to_dict()`` method: + +.. code-block:: python + + from dataclasses import asdict + + payload = asdict(calc.calculate_properties(species_id, 500.0)) + +The typed models are :class:`~pyglenn.ThermoProperties`, +:class:`~pyglenn.SpeciesInfo`, :class:`~pyglenn.SpeciesData`, +:class:`~pyglenn.IntervalData`, :class:`~pyglenn.NASACoefficients` and +:class:`~pyglenn.DatabaseStats`. diff --git a/examples/01_basic_usage.ipynb b/examples/01_basic_usage.ipynb index 83f0109..90cf29f 100644 --- a/examples/01_basic_usage.ipynb +++ b/examples/01_basic_usage.ipynb @@ -49,10 +49,10 @@ "outputs": [], "source": [ "with ThermochemicalCalculator() as calc:\n", - " species = calc.get_available_species('CH4', exact_match=True)\n", + " species = calc.get_available_species('CO2', exact_match=True)\n", "\n", "for s in species:\n", - " print(f\"id={s['id']:>5} {s['name']:<12} phase={s['phase']}\")" + " print(f\"id={s.id:>5} {s.name:<12} phase={s.phase}\")" ] }, { @@ -63,7 +63,7 @@ "## Computing thermochemical properties\n", "\n", "With the `id` in hand, `calculate_properties(species_id, temperature)`\n", - "returns a dictionary with $C_p$, $H^\\circ$ (relative to 0 K) and $S^\\circ$." + "returns a `ThermoProperties` dataclass with $C_p$, $H^\\circ(T)$ and $S^\\circ$." ] }, { @@ -74,14 +74,14 @@ "outputs": [], "source": [ "with ThermochemicalCalculator() as calc:\n", - " species_id = calc.get_available_species('CH4', exact_match=True)[0]['id']\n", + " species_id = calc.get_available_species('CO2', exact_match=True)[0].id\n", " result = calc.calculate_properties(species_id, 298.15)\n", "\n", - "print(f\"Species : {result['species_name']} ({result['phase']})\")\n", - "print(f\"T : {result['temperature']:.2f} K\")\n", - "print(f\"Cp : {result['cp']:.3f} J/(mol·K)\")\n", - "print(f\"H° : {result['h_relative']:.3f} J/mol\")\n", - "print(f\"S° : {result['s']:.3f} J/(mol·K)\")" + "print(f\"Species : {result.species_name} ({result.phase})\")\n", + "print(f\"T : {result.temperature:.2f} K\")\n", + "print(f\"Cp : {result.cp:.3f} J/(mol·K)\")\n", + "print(f\"H° : {result.h_relative:.3f} J/mol\")\n", + "print(f\"S° : {result.s:.3f} J/(mol·K)\")" ] }, { @@ -104,12 +104,12 @@ "temperatures = [300.0, 500.0, 800.0, 1000.0, 1500.0]\n", "\n", "with ThermochemicalCalculator() as calc:\n", - " species_id = calc.get_available_species('CH4', exact_match=True)[0]['id']\n", + " species_id = calc.get_available_species('CO2', exact_match=True)[0].id\n", " print(f\"{'T (K)':>8} | {'Cp (J/mol·K)':>14}\")\n", " print('-' * 27)\n", " for T in temperatures:\n", " r = calc.calculate_properties(species_id, T)\n", - " print(f\"{T:>8.1f} | {r['cp']:>14.3f}\")" + " print(f\"{T:>8.1f} | {r.cp:>14.3f}\")" ] } ], diff --git a/examples/02_fuel_comparison.ipynb b/examples/02_fuel_comparison.ipynb index 98aeb96..e67f79e 100644 --- a/examples/02_fuel_comparison.ipynb +++ b/examples/02_fuel_comparison.ipynb @@ -20,7 +20,11 @@ "$\\Delta H(298.15\\,\\mathrm{K} \\to T)$ over a temperature range relevant to\n", "combustion.\n", "\n", - "> **Requires** `matplotlib` — install with `pip install -e \".[examples]\"`." + "> **Requires** `matplotlib` — install with `pip install -e \".[examples]\"`.\n", + ">\n", + "> **Note on CH₄:** the bundled NASA-7 coefficients for methane show a\n", + "> systematic $C_p$ bias (>5% vs NIST-JANAF); CH₄ is therefore excluded from\n", + "> the NIST cross-validation audit. The comparison below is illustrative." ] }, { @@ -62,7 +66,7 @@ "source": [ "with ThermochemicalCalculator() as calc:\n", " ids = {\n", - " name: calc.get_available_species(name, exact_match=True)[0]['id']\n", + " name: calc.get_available_species(name, exact_match=True)[0].id\n", " for name in FUELS\n", " }\n", "\n", @@ -97,8 +101,8 @@ " Ts = sorted(span)\n", " data[name] = {\n", " 'T': Ts,\n", - " 'cp': [span[T]['cp'] for T in Ts],\n", - " 's': [span[T]['s'] for T in Ts],\n", + " 'cp': [span[T].cp for T in Ts],\n", + " 's': [span[T].s for T in Ts],\n", " }\n", " # Sensible ΔH relative to 298.15 K\n", " data[name]['dh'] = [\n", diff --git a/examples/README.md b/examples/README.md index 2fd81b8..7c3a0a5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,8 +39,3 @@ Since `pyglenn` uses a *src-layout*, `pip install -e .` is what makes |----------|-------------| | [`01_basic_usage.ipynb`](01_basic_usage.ipynb) | First steps: look up species and compute $C_p$, $H^\circ$, $S^\circ$. | | [`02_fuel_comparison.ipynb`](02_fuel_comparison.ipynb) | Compares CH4, ethanol, and propane with plots of $C_p(T)$, $S^\circ(T)$ and sensible enthalpy (requires matplotlib). | - -### Additional examples - -Extra notebooks live in [`extra/`](extra/) — drop new `.ipynb` files there -and they will appear in the documentation on the next build. diff --git a/examples/extra/README.md b/examples/extra/README.md deleted file mode 100644 index 8b84835..0000000 --- a/examples/extra/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Additional examples - -This folder is intended for extra Jupyter notebooks that extend the -core examples. Place new `.ipynb` files here — they will be picked up -automatically by the Sphinx documentation build (via the `:glob:` directive -in `examples.md`). - diff --git a/pyglenn_atualizacao.md b/pyglenn_atualizacao.md new file mode 100644 index 0000000..c42f3ba --- /dev/null +++ b/pyglenn_atualizacao.md @@ -0,0 +1,58 @@ +# Guia de Atualização para o `pyglenn` + +Este documento lista as melhorias implementadas nas versões `0.4.0` e `0.4.1` do **Glenn.jl**, bem como as vantagens arquiteturais e de performance que o pacote Julia possui atualmente sobre o pacote Python (`pyglenn`). O objetivo é servir de roteiro para a atualização e paridade de features no `pyglenn`. + +--- + +## 1. Melhorias Recentes a Serem Portadas (v0.4.0 e v0.4.1) + +### 🔴 Correção Crítica: Constante Universal dos Gases (Viés de -5.7 ppm) +A mudança mais importante introduzida na `v0.4.0` corrige um erro silencioso de precisão termodinâmica. +Os coeficientes NASA-7 do arquivo `thermo.inp` original (NASA CEA) são adimensionais ($C_p/R_0$, $H/R_0T$, $S/R_0$) e foram ajustados utilizando a constante de gás do **CODATA 1986** ($R = 8.314510$ J/mol·K). Desnormalizar esses dados usando o valor moderno (CODATA 2018, $R = 8.314462618$ J/mol·K) introduz um viés sistemático de aproximadamente -5.7 ppm. + +**O que fazer no `pyglenn`:** +1. Adicionar uma nova constante `R_GLENN = 8.314510` (CODATA 1986). +2. Adicionar uma tabela `metadata` ao schema do SQLite contendo: + - `key = 'gas_constant_ref'` + - `value = '8.314510'` + - `key = 'gas_constant_ref_source'` + - `value = 'NASA TP-2002-211556 (CODATA 1986)'` +3. Atualizar as equações polinomiais para usar `R_GLENN` na desnormalização caso o banco possua essa tabela, com fallback para `R_UNIVERSAL` com aviso (`Warning`) caso seja um banco de dados antigo. +4. Implementar funções equivalentes a `get_gas_constant_ref` e `migrate_metadata!` (da v0.4.1) para garantir retrocompatibilidade. + +### ✨ Tipagem Forte em Estatísticas +Na versão `v0.4.0`, o retorno de `get_statistics()` deixou de ser um dicionário arbitrário e passou a ser uma estrutura de dados definida (`DatabaseStats`). + +**O que fazer no `pyglenn`:** +- Converter o retorno do dicionário genérico de estatísticas do banco de dados em uma `dataclass` (ex: `DatabaseStats` em Python) para melhor suporte a autocompletar e validação estática. + +--- + +## 2. Diferenças Arquiteturais (Que favorecem o Glenn.jl) + +As seguintes características foram desenhadas no **Glenn.jl** de forma mais otimizada e robusta do que a implementação original do `pyglenn`. Elas devem ser alvo de refatoração no pacote Python: + +### Tipos Fortes e Imutáveis +- **Glenn.jl:** Utiliza `structs` tipados (`ThermoProperties`, `SpeciesInfo`, `NASACoefficients`). +- **pyglenn:** Utiliza dicionários puros (`dict`), que são suscetíveis a erros de digitação de chaves (`KeyError`) e não oferecem boa documentação via Type Hints da IDE. +- **Ação sugerida:** Migrar as respostas de cálculo e lookups do Python para `@dataclass` (ou `pydantic`, `NamedTuple`). + +### Cálculo Vetorizado Otimizado +- **Glenn.jl:** Na chamada de cálculo para um vetor de temperaturas (varredura térmica), o pacote busca as informações no banco *apenas uma vez*, guarda na memória e resolve a alocação dos polinômios para todos os `T`. +- **pyglenn:** Realiza um loop ingênuo, fazendo um *round-trip* no banco de dados SQLite para cada temperatura, o que degrada a performance de varreduras longas. +- **Ação sugerida:** Implementar em Python um método vetorizado de varredura térmica no nível da API, agrupando os lookups de DB. + +### Exceções com Contexto Rico +- **Glenn.jl:** Exceções customizadas detalhadas, como `SpeciesNotFoundError` e `TemperatureOutOfRangeError`, que carregam dentro da exceção o `species_id`, a temperatura requisitada e o limite do intervalo real. +- **pyglenn:** Lança erros em formato de mensagens simples e genéricas (muitas vezes via um `ValueError` simples). +- **Ação sugerida:** Desenhar uma hierarquia formal de classes `Exception` em Python que armazenem o contexto da falha. + +### Builder Transacional de Banco de Dados +- **Glenn.jl:** O parser de arquivos FORTRAN (`thermo.inp`) faz a varredura e todas as milhares de inserções SQL encapsuladas dentro de um único comando de transação explícita (`BEGIN` ... `COMMIT`). Se ocorrer falha no meio da leitura, a transação é cancelada (ROLLBACK). +- **pyglenn:** Opera com o *Autocommit* do módulo sqlite3 nativo, que é extremamente mais lento para inserções em massa e pode deixar o banco corrompido em caso de interrupção (Crash). +- **Ação sugerida:** Envolver a criação do DB Python (`parse_and_load`) em um bloco transacional massivo ou usando executemany com geradores. + +### Busca Ampla (`list_all_species`) +- **Glenn.jl:** Possui uma função em nível de usuário que extrai uma query eficiente de listagem com informações consolidadas da tabela de forma imediata. +- **pyglenn:** Estruturado excessivamente ao redor de buscas paginadas e interações indiretas, forçando o usuário a fazer loops repetitivos. +- **Ação sugerida:** Exportar uma função que resgate uma lista em memória das instâncias de espécies disponíveis. diff --git a/pyproject.toml b/pyproject.toml index 002fd4c..30b3e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ select = [ [tool.ruff.lint.per-file-ignores] "cli.py" = ["T201"] # allow print() in CLI module +"scripts/rebuild_database.py" = ["T201"] # allow print() in rebuild script [tool.ruff.format] quote-style = "single" diff --git a/scripts/rebuild_database.py b/scripts/rebuild_database.py new file mode 100644 index 0000000..8f714de --- /dev/null +++ b/scripts/rebuild_database.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Rebuild the bundled pyglenn database from thermo.inp, atomically. + +The database is built into a temporary file, validated, and only replaces the +bundled ``thermo.db`` after every release gate passes. A failed rebuild leaves +the previous database untouched. +""" + +from __future__ import annotations + +import argparse +import os +import sys +import tempfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_INP = REPO_ROOT / 'src' / 'pyglenn' / 'data' / 'thermo.inp' +DEFAULT_DB = REPO_ROOT / 'src' / 'pyglenn' / 'data' / 'thermo.db' + +# Release gates for the canonical NASA Glenn/CEA dataset. +EXPECTED_SPECIES = 2035 +EXPECTED_INTERVALS = 3779 +EXPECTED_COEFFS = 3779 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('-i', '--input', type=Path, default=DEFAULT_INP) + parser.add_argument('-o', '--output', type=Path, default=DEFAULT_DB) + args = parser.parse_args(argv) + + sys.path.insert(0, str(REPO_ROOT / 'src')) + + from pyglenn.builder import ThermoDBBuilder + from pyglenn.database import ThermoDBQuery + + inp: Path = args.input + out: Path = args.output + if not inp.exists(): + print(f'ERROR: input file not found: {inp}', file=sys.stderr) + return 1 + + tmp_dir = tempfile.mkdtemp(prefix='pyglenn-rebuild-') + tmp_db = Path(tmp_dir) / 'thermo.db' + + try: + builder = ThermoDBBuilder(str(inp), str(tmp_db)) + builder.connect() + builder.create_tables() + builder.parse_and_load() + builder.close() + except Exception: + print( + 'ERROR: build failed; bundled database left untouched', + file=sys.stderr, + ) + raise + + query = ThermoDBQuery(str(tmp_db)) + if not query.connect(): + print('ERROR: could not reopen temporary database', file=sys.stderr) + return 1 + report = query.validate_database() + query.close() + + if not report['valid']: + print('ERROR: validation failed', file=sys.stderr) + for err in report['errors']: + print(f' - {err}', file=sys.stderr) + return 1 + + counts = report['counts'] + gate_failures = [] + if counts['total_species'] != EXPECTED_SPECIES: + gate_failures.append( + f"species {counts['total_species']} != {EXPECTED_SPECIES}" + ) + if counts['total_intervals'] != EXPECTED_INTERVALS: + gate_failures.append( + f"intervals {counts['total_intervals']} != {EXPECTED_INTERVALS}" + ) + if counts['total_coeff_sets'] != EXPECTED_COEFFS: + gate_failures.append( + f"coefficients {counts['total_coeff_sets']} != {EXPECTED_COEFFS}" + ) + if gate_failures: + print('ERROR: release gates failed', file=sys.stderr) + for failure in gate_failures: + print(f' - {failure}', file=sys.stderr) + return 1 + + os.replace(tmp_db, out) + print(f'OK: rebuilt database -> {out}') + print( + f" species={counts['total_species']} " + f"intervals={counts['total_intervals']} " + f"coefficients={counts['total_coeff_sets']}" + ) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/src/pyglenn/__init__.py b/src/pyglenn/__init__.py index 09bae78..cf723ce 100644 --- a/src/pyglenn/__init__.py +++ b/src/pyglenn/__init__.py @@ -5,7 +5,7 @@ stored in a SQLite database, converted from FORTRAN thermo.inp files. """ -__version__ = '0.1.13' +__version__ = '0.2.0' __author__ = 'Dr. Reginaldo G. Leão Jr.' from .builder import ThermoDBBuilder @@ -16,15 +16,31 @@ ThermoCalcError, ThermochemicalCalculator, ) -from .database import R, ThermoDBQuery +from .database import R_GLENN, R_UNIVERSAL, R, ThermoDBQuery +from .models import ( + DatabaseStats, + IntervalData, + NASACoefficients, + SpeciesData, + SpeciesInfo, + ThermoProperties, +) __all__ = [ - 'ThermochemicalCalculator', - 'ThermoDBQuery', - 'ThermoDBBuilder', - 'R', - 'ThermoCalcError', + 'DatabaseStats', 'DatabaseNotConnectedError', + 'IntervalData', + 'NASACoefficients', + 'R', + 'R_GLENN', + 'R_UNIVERSAL', + 'SpeciesData', + 'SpeciesInfo', 'SpeciesNotFoundError', 'TemperatureOutOfRangeError', + 'ThermoCalcError', + 'ThermoDBBuilder', + 'ThermoDBQuery', + 'ThermoProperties', + 'ThermochemicalCalculator', ] diff --git a/src/pyglenn/builder.py b/src/pyglenn/builder.py index 4af1514..0de7ff5 100644 --- a/src/pyglenn/builder.py +++ b/src/pyglenn/builder.py @@ -15,11 +15,19 @@ from __future__ import annotations import logging +import math import re import sqlite3 from pathlib import Path from typing import Any +from .database import ( + GAS_CONSTANT_REF_KEY, + GAS_CONSTANT_REF_SOURCE_KEY, + GAS_CONSTANT_REF_VALUE, + R_GLENN_SOURCE, +) + logger = logging.getLogger(__name__) # Regex: match FORTRAN double-precision scientific notation (e.g. 1.234567890D+05) @@ -115,9 +123,36 @@ def create_tables(self) -> None: ) """) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """) + assert self.conn is not None self.conn.commit() + def write_metadata(self) -> None: + """Write canonical dataset metadata using idempotent upserts. + + The method intentionally does not commit; callers can include metadata + writes in the same transaction as a database build. + """ + if self.cursor is None: + raise RuntimeError('Database not connected') + self.cursor.executemany( + """ + INSERT INTO metadata (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """, + [ + (GAS_CONSTANT_REF_KEY, GAS_CONSTANT_REF_VALUE), + (GAS_CONSTANT_REF_SOURCE_KEY, R_GLENN_SOURCE), + ], + ) + # ------------------------------------------------------------------ # Low-level parsers # ------------------------------------------------------------------ @@ -165,19 +200,16 @@ def parse_general_info_record(self, line: str) -> dict[str, Any]: data: dict[str, Any] = {} try: - num_int_str = line[0:2].strip() if len(line) > 2 else '' + num_int_str = line[0:2].strip() data['num_intervals'] = int(num_int_str) if num_int_str.isdigit() else 0 - data['ref_code'] = line[3:9].strip() if len(line) > 9 else '' + data['ref_code'] = line[3:9].strip() - phase_code = line[50:52].strip() if len(line) > 52 else '0' + phase_code = line[50:52].strip() data['phase'] = 'condensed' if phase_code and phase_code != '0' else 'gas' - mw_str = line[52:65].strip() if len(line) > 65 else '' - data['molecular_weight'] = self.parse_float(mw_str) - - hf_str = line[65:80].strip() if len(line) > 80 else '' - data['heat_of_formation'] = self.parse_float(hf_str) + data['molecular_weight'] = self.parse_float(line[52:65]) + data['heat_of_formation'] = self.parse_float(line[65:80]) except Exception as e: logger.warning('Error parsing RECORD 2: %s', e) @@ -288,75 +320,156 @@ def is_coefficient_line(line: str) -> bool: """ return bool(_FORTRAN_D_RE.search(line)) + @staticmethod + def is_species_header(line: str) -> bool: + """Return whether a line can be a species-identification record. + + A coefficient line can fit entirely inside the fixed-width species-name + field. It must therefore be rejected explicitly, rather than relying + on whitespace in the apparent name. + """ + name, _ = ThermoDBBuilder.parse_species_record(line) + return ( + bool(name) + and len(name.split()) == 1 + and not ThermoDBBuilder.is_temperature_line(line) + and not ThermoDBBuilder.is_coefficient_line(line) + ) + + @staticmethod + def _has_finite_coefficients(coeffs: dict[str, Any]) -> bool: + """Return whether a parsed NASA-7 coefficient set is complete.""" + return all( + isinstance(value, (int, float)) and math.isfinite(value) + for value in coeffs.values() + ) + + def _parse_intervals( + self, lines: list[str], start: int, num_intervals: int + ) -> tuple[list[tuple[dict[str, Any], dict[str, Any]]], int] | None: + """Parse a complete set of interval records without modifying the DB. + + Returning ``None`` leaves the caller free to resynchronise at a later + verified species header. This prevents a malformed block from creating + a partial species or from turning coefficient lines into new species. + """ + intervals: list[tuple[dict[str, Any], dict[str, Any]]] = [] + index = start + + for _ in range(num_intervals): + if index + 2 >= len(lines): + logger.warning('Incomplete interval records at line %d', index + 1) + return None + if not self.is_temperature_line(lines[index]): + logger.warning( + 'Invalid temperature interval record at line %d', index + 1 + ) + return None + + interval = self.parse_temp_interval_record(lines[index]) + coefficients_lines = lines[index + 1 : index + 3] + if not all(self.is_coefficient_line(line) for line in coefficients_lines): + logger.warning('Invalid coefficient records at line %d', index + 2) + return None + + coefficients = self.parse_coefficients_record(coefficients_lines) + temp_min = interval['temp_min'] + temp_max = interval['temp_max'] + if ( + not isinstance(temp_min, float) + or not isinstance(temp_max, float) + or not math.isfinite(temp_min) + or not math.isfinite(temp_max) + or temp_min >= temp_max + or not self._has_finite_coefficients(coefficients) + ): + logger.warning('Invalid interval values at line %d', index + 1) + return None + + intervals.append((interval, coefficients)) + index += 3 + + return intervals, index + # ------------------------------------------------------------------ # Main parse & load # ------------------------------------------------------------------ def parse_and_load(self) -> None: - """Parse the thermo.inp file and populate the database.""" + """Parse the thermo.inp file and populate the database atomically. + + A species is inserted only after every declared interval and its two + coefficient records have been parsed and validated. Malformed blocks + are skipped without leaving partial rows behind. + """ if self.cursor is None: raise RuntimeError('Database not connected') + if self.conn is None: + raise RuntimeError('Database not connected') lines = self.read_thermo_file() if not lines: logger.warning('Empty thermo.inp file!') return - # --- Global metadata (line index 1) --- metadata_line = lines[1] if len(lines) > 1 else '' - parts = metadata_line.split() - if len(parts) >= 4: - self.cursor.execute( - """ - INSERT INTO file_metadata - (id, temp_min_global, temp_500_K, temp_1500_K, - temp_max_global, reference_date) - VALUES (1, ?, ?, ?, ?, ?) - """, - ( - self.parse_float(parts[0]), - self.parse_float(parts[1]), - self.parse_float(parts[2]), - self.parse_float(parts[3]), - parts[4] if len(parts) > 4 else None, - ), - ) - - # --- Species loop --- - i = 2 + metadata_parts = metadata_line.split() species_count = 0 + existing_count = 0 skipped = 0 - while i < len(lines): - try: - # RECORD 1 – species name - if i >= len(lines): - break - - species_name, comments = self.parse_species_record(lines[i]) - - if ( - not species_name - or len(species_name.split()) > 1 - or self.is_temperature_line(lines[i]) - ): - i += 1 - skipped += 1 + try: + self.conn.execute('BEGIN') + self.write_metadata() + if len(metadata_parts) >= 4: + self.cursor.execute( + """ + INSERT INTO file_metadata + (id, temp_min_global, temp_500_K, temp_1500_K, + temp_max_global, reference_date) + VALUES (1, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + temp_min_global = excluded.temp_min_global, + temp_500_K = excluded.temp_500_K, + temp_1500_K = excluded.temp_1500_K, + temp_max_global = excluded.temp_max_global, + reference_date = excluded.reference_date + """, + ( + self.parse_float(metadata_parts[0]), + self.parse_float(metadata_parts[1]), + self.parse_float(metadata_parts[2]), + self.parse_float(metadata_parts[3]), + metadata_parts[4] if len(metadata_parts) > 4 else None, + ), + ) + + index = 2 + while index < len(lines): + if not self.is_species_header(lines[index]) or index + 1 >= len(lines): + index += 1 continue - logger.info('Processing species: %s', species_name) - i += 1 - - # RECORD 2 – general info - if i >= len(lines): - break - general_info = self.parse_general_info_record(lines[i]) - i += 1 + species_name, comments = self.parse_species_record(lines[index]) + general_info = self.parse_general_info_record(lines[index + 1]) + num_intervals = general_info.get('num_intervals', 0) + if not isinstance(num_intervals, int) or num_intervals <= 0: + index += 1 + continue - if general_info.get('num_intervals', 0) <= 0: + parsed_intervals = self._parse_intervals( + lines, index + 2, num_intervals + ) + if parsed_intervals is None: + logger.warning( + 'Skipping malformed species block at line %d: %s', + index + 1, + species_name, + ) skipped += 1 + index += 1 continue - # Insert species + intervals, next_index = parsed_intervals try: self.cursor.execute( """ @@ -373,121 +486,60 @@ def parse_and_load(self) -> None: general_info.get('phase'), general_info.get('molecular_weight'), general_info.get('heat_of_formation'), - general_info.get('num_intervals'), + num_intervals, ), ) - species_id = self.cursor.lastrowid - species_count += 1 except sqlite3.IntegrityError: + logger.info('Species already exists, skipping: %s', species_name) + existing_count += 1 + index = next_index + continue + + species_id = self.cursor.lastrowid + for interval_number, (interval, coefficients) in enumerate( + intervals, 1 + ): self.cursor.execute( - 'SELECT id FROM species WHERE name = ?', - (species_name,), + """ + INSERT INTO temperature_intervals + (species_id, interval_number, temp_min, + temp_max, h_298_to_0) + VALUES (?, ?, ?, ?, ?) + """, + ( + species_id, + interval_number, + interval['temp_min'], + interval['temp_max'], + interval['h_298_to_0'], + ), ) - result = self.cursor.fetchone() - if result: - species_id = result[0] - else: - skipped += 1 - continue - - # --- Temperature intervals --- - num_intervals = general_info.get('num_intervals', 0) - for interval_num in range(num_intervals): - if i >= len(lines): - break - - if not self.is_temperature_line(lines[i]): - break - - temp_interval = self.parse_temp_interval_record(lines[i]) - i += 1 - - if i + 1 >= len(lines): - break - - if not ( - self.is_coefficient_line(lines[i]) - and self.is_coefficient_line(lines[i + 1]) - ): - break - - coeffs = self.parse_coefficients_record([lines[i], lines[i + 1]]) - i += 2 - - if ( - temp_interval.get('temp_min') is None - or temp_interval.get('temp_max') is None - ): - continue - - try: - self.cursor.execute( - """ - INSERT INTO temperature_intervals - (species_id, interval_number, temp_min, - temp_max, h_298_to_0) - VALUES (?, ?, ?, ?, ?) - """, - ( - species_id, - interval_num + 1, - temp_interval.get('temp_min'), - temp_interval.get('temp_max'), - temp_interval.get('h_298_to_0'), - ), - ) - interval_id = self.cursor.lastrowid - - self.cursor.execute( - """ - INSERT INTO coefficients - (interval_id, a1, a2, a3, a4, a5, - a6, a7, b1, b2) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - interval_id, - coeffs.get('a1'), - coeffs.get('a2'), - coeffs.get('a3'), - coeffs.get('a4'), - coeffs.get('a5'), - coeffs.get('a6'), - coeffs.get('a7'), - coeffs.get('b1'), - coeffs.get('b2'), - ), - ) - - logger.debug( - ' Interval %d: %sK - %sK', - interval_num + 1, - temp_interval.get('temp_min'), - temp_interval.get('temp_max'), - ) - except Exception as e: - logger.warning( - 'Error inserting interval %d: %s', - interval_num + 1, - e, - ) - - except Exception as e: - logger.warning('Error processing line %d: %s', i, e) - i += 1 - - # Final metadata update - if self.cursor is None: - raise RuntimeError('Database not connected') - self.cursor.execute( - 'UPDATE file_metadata SET total_species = ? WHERE id = 1', - (species_count,), - ) - if self.conn is None: - raise RuntimeError('Database not connected') - self.conn.commit() + interval_id = self.cursor.lastrowid + self.cursor.execute( + """ + INSERT INTO coefficients + (interval_id, a1, a2, a3, a4, a5, + a6, a7, b1, b2) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (interval_id, *coefficients.values()), + ) + + logger.info('Loaded species: %s', species_name) + species_count += 1 + index = next_index + + self.cursor.execute( + 'UPDATE file_metadata SET total_species = ? WHERE id = 1', + (species_count,), + ) + self.conn.commit() + except Exception: + self.conn.rollback() + raise logger.info('=' * 70) - logger.info('Total species loaded: %d', species_count) - logger.info('Skipped lines: %d', skipped) + logger.info('New species loaded: %d', species_count) + logger.info('Already existing: %d', existing_count) + logger.info('Malformed species blocks skipped: %d', skipped) logger.info('Database: %s', self.db_file) diff --git a/src/pyglenn/calculator.py b/src/pyglenn/calculator.py index 40dbf76..bef4cd7 100644 --- a/src/pyglenn/calculator.py +++ b/src/pyglenn/calculator.py @@ -13,10 +13,12 @@ from __future__ import annotations import logging +from collections.abc import Sequence from importlib import resources from typing import Any -from .database import R, ThermoDBQuery +from .database import R_UNIVERSAL, ThermoDBQuery +from .models import IntervalData, SpeciesInfo, ThermoProperties logger = logging.getLogger(__name__) @@ -36,13 +38,33 @@ class DatabaseNotConnectedError(ThermoCalcError): class SpeciesNotFoundError(ThermoCalcError): """Raised when a species ID is not found in the database.""" - pass + def __init__(self, species_id: int) -> None: + self.species_id = species_id + super().__init__(f'Species ID {species_id} not found in database') class TemperatureOutOfRangeError(ThermoCalcError): - """Raised when the requested temperature is outside valid intervals.""" + """Raised when the requested temperature is outside valid intervals. - pass + Carries the requested temperature, the species name, and the overall + valid temperature bounds (when known). + """ + + def __init__( + self, + temperature: float, + species_name: str | None = None, + temp_bounds: tuple[float, float] | None = None, + ) -> None: + self.temperature = temperature + self.species_name = species_name + self.temp_bounds = temp_bounds + message = f'Temperature {temperature:.1f} K out of valid range' + if species_name: + message += f" for species '{species_name}'" + if temp_bounds is not None: + message += f' (valid range: {temp_bounds[0]:.1f}-{temp_bounds[1]:.1f} K)' + super().__init__(message) class ThermochemicalCalculator: @@ -63,6 +85,7 @@ def __init__(self, db_file: str | None = None) -> None: db_file = str(db_path) self.db: ThermoDBQuery = ThermoDBQuery(db_file) self._connected: bool = False + self.r_ref: float = R_UNIVERSAL # ------------------------------------------------------------------ # Context manager protocol @@ -96,6 +119,7 @@ def connect(self) -> bool: True if connection succeeded, False otherwise. """ if self.db.connect(): + self.r_ref = self.db.get_gas_constant_ref() self._connected = True return True return False @@ -104,13 +128,14 @@ def close(self) -> None: """Close the database connection.""" self.db.close() self._connected = False + self.r_ref = R_UNIVERSAL # ------------------------------------------------------------------ # Species lookup # ------------------------------------------------------------------ def get_available_species( self, search_pattern: str = '', exact_match: bool = False - ) -> list[dict[str, Any]]: + ) -> list[SpeciesInfo]: """ Return a list of available species, optionally filtered by name. @@ -122,7 +147,7 @@ def get_available_species( backward compatibility. Returns: - List of species dicts with id, name, phase, molecular_weight. + List of SpeciesInfo with id, name, phase, molecular_weight. """ if not self._connected: logger.warning('get_available_species called without connection') @@ -131,27 +156,14 @@ def get_available_species( if search_pattern: return self.db.find_species(search_pattern, exact_match=exact_match) - # Paginate through all species - species_list: list[dict[str, Any]] = [] - page = 1 - while True: - species_page, total_pages = self.db.list_species_page( - page=page, page_size=100 - ) - if not species_page: - break - species_list.extend(species_page) - if page >= total_pages: - break - page += 1 - return species_list + return self.db.list_all_species() # ------------------------------------------------------------------ # Core calculations # ------------------------------------------------------------------ def calculate_properties( self, species_id: int, temperature: float - ) -> dict[str, Any]: + ) -> ThermoProperties: """ Calculate thermochemical properties at a given temperature. @@ -160,14 +172,8 @@ def calculate_properties( temperature: Temperature in Kelvin. Returns: - Dictionary with keys: - - temperature: Input temperature (K) - - cp: Heat capacity in J/(mol·K) - - h_relative: Enthalpy relative to 0 K in J/mol - - s: Absolute entropy in J/(mol·K) - - temp_interval: [T_min, T_max] - - species_name: Species name - - phase: Phase ('gas' or 'condensed') + ThermoProperties with temperature, cp, h_relative, s, + temp_interval, species_name, and phase. Raises: DatabaseNotConnectedError: If not connected to database. @@ -180,22 +186,16 @@ def calculate_properties( 'calculate_properties called without database connection' ) - species_data = self.db.get_species_data(species_id) - if not species_data or 'intervals' not in species_data: - raise SpeciesNotFoundError(f'Species ID {species_id} not found in database') + species_info = self.db.get_species_info(species_id) + if species_info is None: + raise SpeciesNotFoundError(species_id) interval_data = self.db.get_species_for_temperature(species_id, temperature) - if not interval_data: - intervals = [ - (i['temp_min'], i['temp_max']) for i in species_data['intervals'] - ] - raise TemperatureOutOfRangeError( - f'Temperature {temperature:.1f} K out of valid range ' - f"for species '{species_data['name']}'. " - f'Available intervals: {intervals}' - ) + if interval_data is None: + bounds = self.db.get_temperature_bounds(species_id) + raise TemperatureOutOfRangeError(temperature, species_info.name, bounds) - coeffs: dict[str, float] = interval_data['coefficients'] + coeffs = interval_data.coefficients # Dimensionless properties (÷ R) cp_r = self.db.calculate_cp(coeffs, temperature) @@ -203,22 +203,19 @@ def calculate_properties( s_r = self.db.calculate_s(coeffs, temperature) # Convert to absolute units - cp = cp_r * R # J/(mol·K) - h_relative = h_rt * R * temperature # J/mol - s = s_r * R # J/(mol·K) - - return { - 'temperature': temperature, - 'cp': cp, - 'h_relative': h_relative, - 's': s, - 'temp_interval': [ - interval_data['temp_min'], - interval_data['temp_max'], - ], - 'species_name': species_data['name'], - 'phase': species_data['phase'], - } + cp = cp_r * self.r_ref # J/(mol·K) + h_relative = h_rt * self.r_ref * temperature # J/mol + s = s_r * self.r_ref # J/(mol·K) + + return ThermoProperties( + temperature=temperature, + cp=cp, + h_relative=h_relative, + s=s, + temp_interval=(interval_data.temp_min, interval_data.temp_max), + species_name=species_info.name, + phase=species_info.phase, + ) def calculate_formation_enthalpy(self, species_id: int) -> float | None: """ @@ -234,11 +231,11 @@ def calculate_formation_enthalpy(self, species_id: int) -> float | None: logger.warning('calculate_formation_enthalpy called without connection') return None - species_data = self.db.get_species_data(species_id) - if not species_data: + species_info = self.db.get_species_info(species_id) + if species_info is None: return None - return species_data.get('heat_of_formation_298K') + return species_info.heat_of_formation_298K def calculate_enthalpy_change( self, species_id: int, T1: float, T2: float @@ -246,7 +243,7 @@ def calculate_enthalpy_change( """ Calculate ΔH°(T₂) − ΔH°(T₁) in J/mol. - Uses H°(T) values relative to 0 K. + Uses absolute H°(T) values (NASA-7 convention). Args: species_id: Database ID of the species. @@ -269,11 +266,11 @@ def calculate_enthalpy_change( props_t1 = self.calculate_properties(species_id, T1) props_t2 = self.calculate_properties(species_id, T2) - return float(props_t2['h_relative'] - props_t1['h_relative']) + return float(props_t2.h_relative - props_t1.h_relative) def get_properties_range( self, species_id: int, temps: list[float] - ) -> dict[float, dict[str, Any]] | None: + ) -> dict[float, ThermoProperties] | None: """ Calculate properties at multiple temperatures. @@ -282,13 +279,13 @@ def get_properties_range( temps: List of temperatures in Kelvin. Returns: - Dict mapping temperature → property dict, or None if all fail. + Dict mapping temperature → ThermoProperties, or None if all fail. """ if not self._connected: logger.warning('get_properties_range called without connection') return None - results: dict[float, dict[str, Any]] = {} + results: dict[float, ThermoProperties] = {} for temp in temps: try: props = self.calculate_properties(species_id, temp) @@ -297,3 +294,95 @@ def get_properties_range( pass # skip temperatures outside valid range return results if results else None + + def calculate_properties_range( + self, + species_id: int, + temps: Sequence[float], + *, + strict: bool = False, + ) -> list[ThermoProperties]: + """Calculate properties for many temperatures, loading data once. + + Unlike :meth:`get_properties_range`, this loads the species intervals + a single time and evaluates all temperatures in memory, preserving + input order. + + Args: + species_id: Database ID of the species. + temps: Sequence of temperatures in Kelvin. + strict: If True, raise on an out-of-range temperature. + If False (default), out-of-range values are skipped. + + Returns: + Ordered list of ThermoProperties. + + Raises: + DatabaseNotConnectedError: If not connected to database. + SpeciesNotFoundError: If species_id is not in the database. + TemperatureOutOfRangeError: If ``strict`` and a temperature is + outside the valid intervals. + """ + if not self._connected: + raise DatabaseNotConnectedError( + 'calculate_properties_range called without database connection' + ) + + species_info = self.db.get_species_info(species_id) + if species_info is None: + raise SpeciesNotFoundError(species_id) + + species_data = self.db.get_species_data(species_id) + if species_data is None: + raise SpeciesNotFoundError(species_id) + + intervals = species_data.intervals + bounds = ThermochemicalCalculator._overall_bounds(intervals) + + results: list[ThermoProperties] = [] + for temp in temps: + interval = ThermochemicalCalculator._select_interval(intervals, temp) + if interval is None: + if strict: + raise TemperatureOutOfRangeError(temp, species_info.name, bounds) + continue + + coeffs = interval.coefficients + cp_r = self.db.calculate_cp(coeffs, temp) + h_rt = self.db.calculate_h(coeffs, temp) + s_r = self.db.calculate_s(coeffs, temp) + + results.append( + ThermoProperties( + temperature=temp, + cp=cp_r * self.r_ref, + h_relative=h_rt * self.r_ref * temp, + s=s_r * self.r_ref, + temp_interval=(interval.temp_min, interval.temp_max), + species_name=species_info.name, + phase=species_info.phase, + ) + ) + return results + + @staticmethod + def _select_interval( + intervals: tuple[IntervalData, ...], temperature: float + ) -> IntervalData | None: + """Return the first interval covering ``temperature``, or None.""" + for interval in intervals: + if interval.temp_min <= temperature <= interval.temp_max + 1e-9: + return interval + return None + + @staticmethod + def _overall_bounds( + intervals: tuple[IntervalData, ...], + ) -> tuple[float, float] | None: + """Return the overall [min, max] temperature covered by intervals.""" + if not intervals: + return None + return ( + min(interval.temp_min for interval in intervals), + max(interval.temp_max for interval in intervals), + ) diff --git a/src/pyglenn/cli.py b/src/pyglenn/cli.py index 19c0181..201daea 100644 --- a/src/pyglenn/cli.py +++ b/src/pyglenn/cli.py @@ -15,6 +15,7 @@ from .builder import ThermoDBBuilder from .calculator import ThermoCalcError, ThermochemicalCalculator +from .database import ThermoDBQuery def _setup_logging(verbose: bool = False) -> None: @@ -76,11 +77,11 @@ def cmd_query(args: argparse.Namespace) -> None: print('\n1. DATABASE STATISTICS:') print('-' * 70) stats = calc.db.get_statistics() - print(f' Total species: {stats["total_species"]}') - print(f' Total intervals: {stats["total_intervals"]}') - print(f' Total coefficient sets: {stats["total_coeff_sets"]}') - print(f' Species by phase: {stats["species_by_phase"]}') - print(f' Average molecular weight: {stats["avg_molecular_weight"]:.2f} g/mol') + print(f' Total species: {stats.total_species}') + print(f' Total intervals: {stats.total_intervals}') + print(f' Total coefficient sets: {stats.total_coeff_sets}') + print(f' Species by phase: {stats.species_by_phase}') + print(f' Average molecular weight: {stats.avg_molecular_weight:.2f} g/mol') # Search pattern = args.species or 'O2' @@ -89,14 +90,14 @@ def cmd_query(args: argparse.Namespace) -> None: species_list = calc.get_available_species(pattern) for sp in species_list[:5]: print( - f' ID: {sp["id"]:4d} | Name: {sp["name"]:20s} | ' - f'Phase: {sp["phase"]:10s} | MW: {sp["molecular_weight"]}' + f' ID: {sp.id:4d} | Name: {sp.name:20s} | ' + f'Phase: {sp.phase:10s} | MW: {sp.molecular_weight}' ) # Properties if species_list: - species_id = species_list[0]['id'] - species_name = species_list[0]['name'] + species_id = species_list[0].id + species_name = species_list[0].name print(f'\n3. PROPERTIES FOR {species_name}:') print('-' * 70) print( @@ -110,10 +111,10 @@ def cmd_query(args: argparse.Namespace) -> None: except ThermoCalcError: continue print( - f'{props["temperature"]:>8.2f} | ' - f'{props["cp"]:>14.3f} | ' - f'{props["h_relative"]:>14.1f} | ' - f'{props["s"]:>14.3f}' + f'{props.temperature:>8.2f} | ' + f'{props.cp:>14.3f} | ' + f'{props.h_relative:>14.1f} | ' + f'{props.s:>14.3f}' ) h_f = calc.calculate_formation_enthalpy(species_id) @@ -126,6 +127,54 @@ def cmd_query(args: argparse.Namespace) -> None: calc.close() +def cmd_migrate_metadata(args: argparse.Namespace) -> None: + """Ensure a database carries the canonical dataset metadata.""" + query = ThermoDBQuery(args.database) + if not query.connect(): + print('Error: Could not connect to database') + return + try: + r_ref = query.get_gas_constant_ref() + print('=' * 70) + print('METADATA MIGRATION') + print('=' * 70) + print(f' Database: {args.database}') + print(f' gas_constant_ref: {r_ref}') + print(' gas_constant_ref_source: NASA Glenn/CEA thermo.inp (CODATA 1986)') + print('\n[SUCCESS] Metadata is present and canonical.') + finally: + query.close() + + +def cmd_validate(args: argparse.Namespace) -> None: + """Run read-only integrity checks against a database.""" + query = ThermoDBQuery(args.database) + if not query.connect(auto_migrate=False): + print('Error: Could not connect to database') + return + try: + report = query.validate_database() + print('=' * 70) + print('DATABASE VALIDATION') + print('=' * 70) + print(f' Valid: {report["valid"]}') + if report['errors']: + print(' Errors:') + for err in report['errors']: + print(f' - {err}') + if report['warnings']: + print(' Warnings:') + for warn in report['warnings']: + print(f' - {warn}') + print(' Counts:', report['counts']) + if report['valid']: + print('\n[SUCCESS] Validation completed.') + else: + print('\n[FAIL] Validation failed.') + finally: + query.close() + + def main() -> None: """Entry point for the pyglenn CLI.""" parser = argparse.ArgumentParser( @@ -172,6 +221,30 @@ def main() -> None: ) p_query.set_defaults(func=cmd_query) + # migrate-metadata + p_migrate = sub.add_parser( + 'migrate-metadata', help='Add canonical dataset metadata to a database' + ) + p_migrate.add_argument( + '-d', + '--database', + default='thermo.db', + help='SQLite database file (default: thermo.db)', + ) + p_migrate.set_defaults(func=cmd_migrate_metadata) + + # validate + p_validate = sub.add_parser( + 'validate', help='Run read-only database integrity checks' + ) + p_validate.add_argument( + '-d', + '--database', + default='thermo.db', + help='SQLite database file (default: thermo.db)', + ) + p_validate.set_defaults(func=cmd_validate) + args = parser.parse_args() if args.command is None: parser.print_help() diff --git a/src/pyglenn/data/thermo.db b/src/pyglenn/data/thermo.db index 1c7a0bf..d8c1464 100644 Binary files a/src/pyglenn/data/thermo.db and b/src/pyglenn/data/thermo.db differ diff --git a/src/pyglenn/database.py b/src/pyglenn/database.py index 88ceee4..f57b3a5 100644 --- a/src/pyglenn/database.py +++ b/src/pyglenn/database.py @@ -9,13 +9,30 @@ import logging import math import sqlite3 +from dataclasses import asdict from pathlib import Path from typing import Any +from .models import ( + DatabaseStats, + IntervalData, + NASACoefficients, + SpeciesData, + SpeciesInfo, +) + logger = logging.getLogger(__name__) -# Physical constant: Universal gas constant -R: float = 8.314462618 # J/(mol·K) — CODATA 2018 value +# Gas constants in J/(mol·K). The bundled NASA Glenn/CEA polynomials were +# fitted with R_GLENN; R remains a compatibility alias for the universal value. +R_UNIVERSAL: float = 8.31446261815324 # CODATA 2018/2022 +R_GLENN: float = 8.314510 # NASA Glenn/CEA thermo.inp, CODATA 1986 +R: float = R_UNIVERSAL + +GAS_CONSTANT_REF_KEY = 'gas_constant_ref' +GAS_CONSTANT_REF_SOURCE_KEY = 'gas_constant_ref_source' +GAS_CONSTANT_REF_VALUE = '8.314510' # canonical text form of R_GLENN +R_GLENN_SOURCE = 'NASA Glenn/CEA thermo.inp (CODATA 1986)' class ThermoDBQuery: @@ -26,9 +43,13 @@ def __init__(self, db_file: str = 'thermo.db') -> None: self.conn: sqlite3.Connection | None = None self.cursor: sqlite3.Cursor | None = None - def connect(self) -> bool: + def connect(self, *, auto_migrate: bool = True) -> bool: """Connect to database. + Args: + auto_migrate: If True (default), add canonical dataset metadata + to legacy databases automatically (see D3). + Returns: True if connection succeeded, False otherwise. """ @@ -39,6 +60,8 @@ def connect(self) -> bool: self.conn = sqlite3.connect(str(self.db_file)) self.conn.row_factory = sqlite3.Row self.cursor = self.conn.cursor() + if auto_migrate: + self._auto_migrate_metadata() return True def close(self) -> None: @@ -48,46 +71,273 @@ def close(self) -> None: self.conn = None self.cursor = None + # ------------------------------------------------------------------ + # Dataset metadata + # ------------------------------------------------------------------ + def _metadata_table_exists(self) -> bool: + """Return whether this database has the additive metadata table.""" + if self.cursor is None: + raise RuntimeError('Database not connected') + self.cursor.execute( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", + ('metadata',), + ) + return self.cursor.fetchone() is not None + + def _auto_migrate_metadata(self) -> None: + """Add canonical metadata to legacy databases on connect (D3). + + Only the additive ``metadata`` table is written. Read-only databases + (or any migration failure) degrade to the documented fallback without + raising. + """ + if self.cursor is None or self.conn is None: + return + if self._metadata_table_exists(): + return + try: + self.migrate_metadata() + logger.info('migrated legacy database metadata on connect') + except sqlite3.OperationalError as exc: + logger.warning('could not auto-migrate metadata: %s', exc) + + def get_gas_constant_ref(self) -> float: + """Return the dataset gas constant, with a safe legacy fallback. + + Legacy databases without valid metadata retain the historic universal + constant. Database errors are deliberately allowed to propagate instead + of being mistaken for a legacy-database condition. + """ + if self.cursor is None: + raise RuntimeError('Database not connected') + if not self._metadata_table_exists(): + logger.warning( + "database has no 'metadata' table; falling back to R_UNIVERSAL" + ) + return R_UNIVERSAL + + self.cursor.execute( + 'SELECT value FROM metadata WHERE key = ?', (GAS_CONSTANT_REF_KEY,) + ) + row = self.cursor.fetchone() + if row is None: + logger.warning( + "metadata key 'gas_constant_ref' not found; falling back to R_UNIVERSAL" + ) + return R_UNIVERSAL + + try: + r_ref = float(row[0]) + except (TypeError, ValueError): + logger.warning( + "metadata key 'gas_constant_ref' is invalid; " + 'falling back to R_UNIVERSAL' + ) + return R_UNIVERSAL + + if math.isfinite(r_ref) and 8.0 < r_ref < 9.0: + return r_ref + + logger.warning( + 'metadata gas_constant_ref=%r is implausible; falling back to R_UNIVERSAL', + r_ref, + ) + return R_UNIVERSAL + + def migrate_metadata(self) -> None: + """Add canonical dataset metadata to a connected legacy database. + + This explicit, idempotent operation changes only the additive + ``metadata`` table. It never rebuilds or alters thermochemical rows. + """ + if self.cursor is None or self.conn is None: + raise RuntimeError('Database not connected') + + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + self.cursor.executemany( + """ + INSERT INTO metadata (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """, + [ + (GAS_CONSTANT_REF_KEY, GAS_CONSTANT_REF_VALUE), + (GAS_CONSTANT_REF_SOURCE_KEY, R_GLENN_SOURCE), + ], + ) + self.conn.commit() + + # ------------------------------------------------------------------ + # Database validation + # ------------------------------------------------------------------ + def validate_database(self) -> dict[str, Any]: + """Run integrity and semantic checks and return a validation report. + + Returns: + Dict with ``valid`` (bool), ``errors`` (list[str]), + ``warnings`` (list[str]), and ``counts`` (dict[str, int]). + """ + if self.cursor is None or self.conn is None: + raise RuntimeError('Database not connected') + + cur = self.cursor + errors: list[str] = [] + warnings: list[str] = [] + counts: dict[str, int] = {} + + def _scalar(sql: str, params: tuple = ()) -> int: + cur.execute(sql, params) + return int(cur.fetchone()[0]) + + missing = [] + for table in ('species', 'temperature_intervals', 'coefficients'): + cur.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", + (table,), + ) + if cur.fetchone() is None: + missing.append(table) + + if missing: + errors.append('missing required table(s): ' + ', '.join(missing)) + return { + 'valid': False, + 'errors': errors, + 'warnings': warnings, + 'counts': {}, + } + + counts['total_species'] = _scalar('SELECT COUNT(*) FROM species') + counts['total_intervals'] = _scalar( + 'SELECT COUNT(*) FROM temperature_intervals' + ) + counts['total_coeff_sets'] = _scalar('SELECT COUNT(*) FROM coefficients') + + orphans = _scalar( + 'SELECT COUNT(*) FROM coefficients c ' + 'LEFT JOIN temperature_intervals ti ON c.interval_id = ti.id ' + 'WHERE ti.id IS NULL' + ) + if orphans: + errors.append(f'{orphans} coefficient row(s) reference a missing interval') + + orphans = _scalar( + 'SELECT COUNT(*) FROM temperature_intervals ti ' + 'LEFT JOIN species s ON ti.species_id = s.id ' + 'WHERE s.id IS NULL' + ) + if orphans: + errors.append(f'{orphans} interval row(s) reference a missing species') + + dup = _scalar( + 'SELECT COUNT(*) FROM (SELECT interval_id, COUNT(*) AS n ' + 'FROM coefficients GROUP BY interval_id) WHERE n != 1' + ) + if dup: + errors.append(f'{dup} interval(s) do not have exactly one coefficient row') + + mismatch = _scalar( + 'SELECT COUNT(*) FROM species s WHERE s.num_intervals != ' + '(SELECT COUNT(*) FROM temperature_intervals ti ' + 'WHERE ti.species_id = s.id)' + ) + if mismatch: + errors.append( + f'{mismatch} species have num_intervals disagreeing ' + 'with actual interval rows' + ) + + bad_bounds = _scalar( + 'SELECT COUNT(*) FROM temperature_intervals ' + 'WHERE temp_min IS NULL OR temp_max IS NULL OR temp_min >= temp_max' + ) + if bad_bounds: + errors.append( + f'{bad_bounds} interval(s) have non-finite or unordered bounds' + ) + + bad_coeffs = _scalar( + 'SELECT COUNT(*) FROM coefficients WHERE a1 IS NULL OR a2 IS NULL ' + 'OR a3 IS NULL OR a4 IS NULL OR a5 IS NULL OR a6 IS NULL ' + 'OR a7 IS NULL OR b1 IS NULL OR b2 IS NULL' + ) + if bad_coeffs: + errors.append(f'{bad_coeffs} coefficient set(s) contain NULL values') + + bad_names = _scalar( + "SELECT COUNT(*) FROM species WHERE name LIKE '%.%D%' OR name LIKE '%.%E%'" + ) + if bad_names: + errors.append( + f'{bad_names} species name(s) look like FORTRAN coefficient tokens' + ) + + bad_mw = _scalar( + 'SELECT COUNT(*) FROM species ' + 'WHERE molecular_weight IS NULL OR molecular_weight <= 0' + ) + if bad_mw: + warnings.append( + f'{bad_mw} species have NULL or non-positive molecular weight' + ) + + return { + 'valid': not errors, + 'errors': errors, + 'warnings': warnings, + 'counts': counts, + } + # ------------------------------------------------------------------ # Statistics # ------------------------------------------------------------------ - def get_statistics(self) -> dict[str, Any]: + def get_statistics(self) -> DatabaseStats: """Get database statistics. Returns: - Dict with total_species, total_intervals, total_coeff_sets, - species_by_phase, avg_molecular_weight. + DatabaseStats with totals, phase counts, and average + molecular weight. """ if self.cursor is None: raise RuntimeError('Database not connected') - stats: dict[str, Any] = {} self.cursor.execute('SELECT COUNT(*) FROM species') - stats['total_species'] = self.cursor.fetchone()[0] + total_species = int(self.cursor.fetchone()[0]) self.cursor.execute('SELECT COUNT(*) FROM temperature_intervals') - stats['total_intervals'] = self.cursor.fetchone()[0] + total_intervals = int(self.cursor.fetchone()[0]) self.cursor.execute('SELECT COUNT(*) FROM coefficients') - stats['total_coeff_sets'] = self.cursor.fetchone()[0] + total_coeff_sets = int(self.cursor.fetchone()[0]) self.cursor.execute('SELECT phase, COUNT(*) FROM species GROUP BY phase') - stats['species_by_phase'] = dict(self.cursor.fetchall()) + species_by_phase = dict(self.cursor.fetchall()) self.cursor.execute( 'SELECT AVG(molecular_weight) FROM species ' 'WHERE molecular_weight IS NOT NULL' ) - stats['avg_molecular_weight'] = self.cursor.fetchone()[0] - - return stats + avg_molecular_weight = self.cursor.fetchone()[0] + + return DatabaseStats( + total_species=total_species, + total_intervals=total_intervals, + total_coeff_sets=total_coeff_sets, + species_by_phase=species_by_phase, + avg_molecular_weight=avg_molecular_weight, + ) # ------------------------------------------------------------------ # Species lookup # ------------------------------------------------------------------ - def find_species( - self, name: str, exact_match: bool = False - ) -> list[dict[str, Any]]: + def find_species(self, name: str, exact_match: bool = False) -> list[SpeciesInfo]: """Find species by name. Args: @@ -96,7 +346,7 @@ def find_species( If False (default), use substring match (LIKE). Returns: - List of matching species dicts (max 20). + List of matching SpeciesInfo (max 20). """ if self.cursor is None: raise RuntimeError('Database not connected') @@ -132,17 +382,16 @@ def find_species( (f'%{name}%', f'%{name}%', name), ) - columns = [d[0] for d in self.cursor.description] - return [dict(zip(columns, row)) for row in self.cursor.fetchall()] + return [SpeciesInfo.from_mapping(row) for row in self.cursor.fetchall()] - def get_species_data(self, species_id: int) -> dict[str, Any] | None: + def get_species_data(self, species_id: int) -> SpeciesData | None: """Get complete data for a species with all its intervals. Args: species_id: Database ID of the species. Returns: - Species dict with 'intervals' list, or None if not found. + SpeciesData with its ``intervals`` tuple, or None if not found. """ if self.cursor is None: raise RuntimeError('Database not connected') @@ -151,7 +400,7 @@ def get_species_data(self, species_id: int) -> dict[str, Any] | None: if not row: return None - species_data = dict(zip([d[0] for d in self.cursor.description], row)) + species_info = SpeciesInfo.from_mapping(row) self.cursor.execute( """ @@ -168,40 +417,98 @@ def get_species_data(self, species_id: int) -> dict[str, Any] | None: intervals = [] for row in self.cursor.fetchall(): - interval = { - 'interval_number': row[0], - 'temp_min': row[1], - 'temp_max': row[2], - 'h_298_to_0': row[3], - 'coefficients': { - 'a1': row[4], - 'a2': row[5], - 'a3': row[6], - 'a4': row[7], - 'a5': row[8], - 'a6': row[9], - 'a7': row[10], - 'b1': row[11], - 'b2': row[12], - }, - } - intervals.append(interval) + intervals.append( + IntervalData( + interval_number=row['interval_number'], + temp_min=row['temp_min'], + temp_max=row['temp_max'], + h_298_to_0=row['h_298_to_0'], + coefficients=NASACoefficients( + a1=row['a1'], + a2=row['a2'], + a3=row['a3'], + a4=row['a4'], + a5=row['a5'], + a6=row['a6'], + a7=row['a7'], + b1=row['b1'], + b2=row['b2'], + ), + ) + ) + + return SpeciesData( + id=species_info.id, + name=species_info.name, + formula=species_info.formula, + phase=species_info.phase, + molecular_weight=species_info.molecular_weight, + heat_of_formation_298K=species_info.heat_of_formation_298K, + num_intervals=species_info.num_intervals, + comments=species_info.comments, + reference_code=species_info.reference_code, + intervals=tuple(intervals), + ) + + def get_species_info(self, species_id: int) -> SpeciesInfo | None: + """Get lightweight species metadata without loading intervals. + + Args: + species_id: Database ID of the species. + + Returns: + SpeciesInfo, or None if the species is not found. + """ + if self.cursor is None: + raise RuntimeError('Database not connected') + self.cursor.execute( + """ + SELECT id, name, formula, phase, molecular_weight, + heat_of_formation_298K, num_intervals, comments, + reference_code + FROM species + WHERE id = ? + """, + (species_id,), + ) + row = self.cursor.fetchone() + if not row: + return None + return SpeciesInfo.from_mapping(row) + + def get_temperature_bounds(self, species_id: int) -> tuple[float, float] | None: + """Return the overall [temp_min, temp_max] covered by a species. - species_data['intervals'] = intervals - return species_data + Args: + species_id: Database ID of the species. + + Returns: + Tuple of (min_temp, max_temp), or None if the species has no + intervals. + """ + if self.cursor is None: + raise RuntimeError('Database not connected') + self.cursor.execute( + 'SELECT MIN(temp_min), MAX(temp_max) FROM temperature_intervals ' + 'WHERE species_id = ?', + (species_id,), + ) + row = self.cursor.fetchone() + if row is None or row[0] is None or row[1] is None: + return None + return (row[0], row[1]) def get_species_for_temperature( self, species_id: int, temperature: float - ) -> dict[str, Any] | None: - """Get valid coefficients for a specific temperature. + ) -> IntervalData | None: + """Get the interval and coefficients valid at a specific temperature. Args: species_id: Database ID of the species. temperature: Temperature in Kelvin. Returns: - Dict with interval_number, temp_min, temp_max, coefficients, - or None if temperature is out of range. + IntervalData, or None if the temperature is out of range. """ if self.cursor is None: raise RuntimeError('Database not connected') @@ -213,6 +520,7 @@ def get_species_for_temperature( self.cursor.execute( """ SELECT ti.interval_number, ti.temp_min, ti.temp_max, + ti.h_298_to_0, c.a1, c.a2, c.a3, c.a4, c.a5, c.a6, c.a7, c.b1, c.b2 FROM temperature_intervals ti LEFT JOIN coefficients c ON ti.id = c.interval_id @@ -229,26 +537,27 @@ def get_species_for_temperature( if not row: return None - return { - 'interval_number': row[0], - 'temp_min': row[1], - 'temp_max': row[2], - 'coefficients': { - 'a1': row[3], - 'a2': row[4], - 'a3': row[5], - 'a4': row[6], - 'a5': row[7], - 'a6': row[8], - 'a7': row[9], - 'b1': row[10], - 'b2': row[11], - }, - } + return IntervalData( + interval_number=row['interval_number'], + temp_min=row['temp_min'], + temp_max=row['temp_max'], + h_298_to_0=row['h_298_to_0'], + coefficients=NASACoefficients( + a1=row['a1'], + a2=row['a2'], + a3=row['a3'], + a4=row['a4'], + a5=row['a5'], + a6=row['a6'], + a7=row['a7'], + b1=row['b1'], + b2=row['b2'], + ), + ) def list_species_page( self, page: int = 1, page_size: int = 20 - ) -> tuple[list[dict[str, Any]], int]: + ) -> tuple[list[SpeciesInfo], int]: """List species with pagination. Args: @@ -275,17 +584,45 @@ def list_species_page( (page_size, offset), ) - columns = [d[0] for d in self.cursor.description] - species = [dict(zip(columns, row)) for row in self.cursor.fetchall()] + species = [SpeciesInfo.from_mapping(row) for row in self.cursor.fetchall()] total_pages = (total + page_size - 1) // page_size return species, total_pages + def list_all_species(self) -> list[SpeciesInfo]: + """List every species in a single ordered query. + + Returns: + List of all SpeciesInfo ordered by name. + """ + if self.cursor is None: + raise RuntimeError('Database not connected') + self.cursor.execute( + """ + SELECT id, name, formula, phase, molecular_weight, + heat_of_formation_298K, num_intervals, comments + FROM species + ORDER BY name + """ + ) + return [SpeciesInfo.from_mapping(row) for row in self.cursor.fetchall()] + # ------------------------------------------------------------------ # NASA polynomial calculations (Cp/R, H/RT, S/R) # ------------------------------------------------------------------ @staticmethod - def calculate_cp(coeffs: dict[str, float], temperature: float) -> float: + def _coeff_dict( + coeffs: NASACoefficients | dict[str, float], + ) -> dict[str, float]: + """Normalise coefficients to a plain dict (accepts NASACoefficients).""" + if isinstance(coeffs, NASACoefficients): + return asdict(coeffs) + return coeffs + + @staticmethod + def calculate_cp( + coeffs: NASACoefficients | dict[str, float], temperature: float + ) -> float: """Calculate Cp(T)/R using NASA-7 polynomial coefficients. Args: @@ -296,7 +633,7 @@ def calculate_cp(coeffs: dict[str, float], temperature: float) -> float: Dimensionless Cp/R. """ T = temperature - a = coeffs + a = ThermoDBQuery._coeff_dict(coeffs) return ( a['a1'] / T**2 + a['a2'] / T @@ -308,7 +645,9 @@ def calculate_cp(coeffs: dict[str, float], temperature: float) -> float: ) @staticmethod - def calculate_h(coeffs: dict[str, float], temperature: float) -> float: + def calculate_h( + coeffs: NASACoefficients | dict[str, float], temperature: float + ) -> float: """Calculate H°(T)/RT using NASA-7 polynomial coefficients. Args: @@ -319,7 +658,7 @@ def calculate_h(coeffs: dict[str, float], temperature: float) -> float: Dimensionless H/(RT). """ T = temperature - a = coeffs + a = ThermoDBQuery._coeff_dict(coeffs) return ( -a['a1'] / T**2 + a['a2'] * math.log(T) / T @@ -332,7 +671,9 @@ def calculate_h(coeffs: dict[str, float], temperature: float) -> float: ) @staticmethod - def calculate_s(coeffs: dict[str, float], temperature: float) -> float: + def calculate_s( + coeffs: NASACoefficients | dict[str, float], temperature: float + ) -> float: """Calculate S°(T)/R using NASA-7 polynomial coefficients. Args: @@ -343,7 +684,7 @@ def calculate_s(coeffs: dict[str, float], temperature: float) -> float: Dimensionless S/R. """ T = temperature - a = coeffs + a = ThermoDBQuery._coeff_dict(coeffs) return ( -a['a1'] / (2 * T**2) - a['a2'] / T diff --git a/src/pyglenn/models.py b/src/pyglenn/models.py new file mode 100644 index 0000000..c37e84e --- /dev/null +++ b/src/pyglenn/models.py @@ -0,0 +1,158 @@ +"""Typed domain model for pyglenn thermochemical results. + +These frozen dataclasses replace plain ``dict`` returns. They are converted at +the SQLite boundary via the ``from_mapping`` constructors and serialised back +with :func:`dataclasses.asdict` (or the ``to_dict`` convenience method). +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import asdict, dataclass +from typing import Any + + +@dataclass(frozen=True) +class NASACoefficients: + """The nine NASA-7 polynomial coefficients (dimensionless).""" + + a1: float + a2: float + a3: float + a4: float + a5: float + a6: float + a7: float + b1: float + b2: float + + @classmethod + def from_mapping(cls, mapping: Mapping[str, Any]) -> NASACoefficients: + """Build from a mapping keyed by ``a1``..``a7``, ``b1``, ``b2``.""" + keys = ('a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'b1', 'b2') + return cls(*(float(mapping[k]) for k in keys)) + + def to_dict(self) -> dict[str, float]: + """Return the coefficients as a plain dict.""" + return asdict(self) + + +@dataclass(frozen=True) +class SpeciesInfo: + """Basic metadata for a single species (no interval or coefficient data).""" + + id: int + name: str + formula: str | None = None + phase: str | None = None + molecular_weight: float | None = None + heat_of_formation_298K: float | None = None + num_intervals: int | None = None + comments: str | None = None + reference_code: str | None = None + + @classmethod + def from_mapping(cls, mapping: Mapping[str, Any]) -> SpeciesInfo: + """Build from a ``dict`` or ``sqlite3.Row``, tolerating missing keys.""" + + def _get(key: str) -> Any: + try: + return mapping[key] # type: ignore[index] + except (KeyError, IndexError): + return None + + return cls( + id=_get('id'), + name=_get('name'), + formula=_get('formula'), + phase=_get('phase'), + molecular_weight=_get('molecular_weight'), + heat_of_formation_298K=_get('heat_of_formation_298K'), + num_intervals=_get('num_intervals'), + comments=_get('comments'), + reference_code=_get('reference_code'), + ) + + def to_dict(self) -> dict[str, Any]: + """Return the species metadata as a plain dict.""" + return asdict(self) + + +@dataclass(frozen=True) +class IntervalData: + """A single temperature interval with its NASA-7 coefficients.""" + + interval_number: int + temp_min: float + temp_max: float + h_298_to_0: float | None + coefficients: NASACoefficients + + def to_dict(self) -> dict[str, Any]: + """Return the interval as a plain dict (nested coefficients included).""" + return asdict(self) + + +@dataclass(frozen=True) +class ThermoProperties: + """Thermochemical properties at a single temperature (SI units). + + ``cp`` and ``s`` are in J/(mol·K); ``h_relative`` is in J/mol. + """ + + temperature: float + cp: float + h_relative: float + s: float + temp_interval: tuple[float, float] + species_name: str + phase: str | None = None + + def to_dict(self) -> dict[str, Any]: + """Return the properties as a plain dict (tuple serialised as list).""" + return { + 'temperature': self.temperature, + 'cp': self.cp, + 'h_relative': self.h_relative, + 's': self.s, + 'temp_interval': list(self.temp_interval), + 'species_name': self.species_name, + 'phase': self.phase, + } + + +@dataclass(frozen=True) +class SpeciesData: + """Complete species record including all intervals and coefficients.""" + + id: int + name: str + formula: str | None = None + phase: str | None = None + molecular_weight: float | None = None + heat_of_formation_298K: float | None = None + num_intervals: int | None = None + comments: str | None = None + reference_code: str | None = None + intervals: tuple[IntervalData, ...] = () + + def to_dict(self) -> dict[str, Any]: + """Return the complete record as a plain dict with an ``intervals`` key.""" + data = asdict(self) + data['intervals'] = [IntervalData.to_dict(i) for i in self.intervals] + return data + + +@dataclass(frozen=True) +class DatabaseStats: + """Aggregate database statistics.""" + + total_species: int + total_intervals: int + total_coeff_sets: int + species_by_phase: Mapping[str, int] + avg_molecular_weight: float | None = None + + def to_dict(self) -> dict[str, Any]: + """Return the statistics as a plain dict.""" + return asdict(self) diff --git a/tests/test_calculator.py b/tests/test_calculator.py index 628402f..11af32a 100644 --- a/tests/test_calculator.py +++ b/tests/test_calculator.py @@ -10,6 +10,8 @@ import pytest from pyglenn import ( + R_GLENN, + R_UNIVERSAL, DatabaseNotConnectedError, R, SpeciesNotFoundError, @@ -137,6 +139,40 @@ def calc(sample_db_path: Path) -> Generator[ThermochemicalCalculator, None, None c.close() +def _fortran_field(value: float) -> str: + """Format one fixed-width FORTRAN double-precision field.""" + return f'{value:16.9E}'.replace('E', 'D') + + +def _species_block(name: str, num_intervals: int = 1) -> str: + """Create a minimal valid fixed-width NASA thermo species block.""" + general_info = [' '] * 80 + general_info[0:2] = f'{num_intervals:2d}' + general_info[3:9] = 'test ' + general_info[50:52] = ' 0' + general_info[52:65] = f'{31.9988:13.7f}' + general_info[65:80] = f'{0.0:15.3f}' + + temperature = f'{200.0:11.3f}{1000.0:11.3f}7' + ' ' * 42 + f'{0.0:15.3f}' + coefficient_line_1 = ''.join(_fortran_field(value) for value in range(1, 6)) + coefficient_line_2 = ( + _fortran_field(6.0) + + _fortran_field(7.0) + + ' ' * 16 + + _fortran_field(8.0) + + _fortran_field(9.0) + ) + return '\n'.join( + [ + f'{name:<16} test species', + ''.join(general_info), + temperature, + coefficient_line_1, + coefficient_line_2, + ] + ) + + # --------------------------------------------------------------------------- # Test: version and package metadata # --------------------------------------------------------------------------- @@ -168,8 +204,10 @@ def test_imports() -> None: def test_gas_constant_value() -> None: - """Verify R is close to the CODATA 2018 value.""" - assert math.isclose(R, 8.314462618, rel_tol=1e-9) + """Verify the universal and NASA Glenn reference constants.""" + assert R == R_UNIVERSAL + assert math.isclose(R_UNIVERSAL, 8.31446261815324, rel_tol=1e-15) + assert R_GLENN == 8.314510 # --------------------------------------------------------------------------- @@ -190,6 +228,49 @@ def test_connect_to_nonexistent_db() -> None: assert q.connect() is False +def test_reference_gas_constant_auto_migration( + sample_db_path: Path, +) -> None: + """Legacy databases are migrated automatically on connect (D3).""" + q = ThermoDBQuery(str(sample_db_path)) + assert q.connect() is True + + assert q.get_gas_constant_ref() == R_GLENN + q.migrate_metadata() # idempotent + + assert q.cursor is not None + metadata = dict(q.cursor.execute('SELECT key, value FROM metadata').fetchall()) + assert metadata['gas_constant_ref'] == '8.314510' + assert ( + metadata['gas_constant_ref_source'] == 'NASA Glenn/CEA thermo.inp (CODATA 1986)' + ) + q.close() + + +def test_reference_gas_constant_invalid_metadata_falls_back( + sample_db_path: Path, +) -> None: + """Invalid metadata cannot silently alter thermochemical calculations.""" + q = ThermoDBQuery(str(sample_db_path)) + assert q.connect() is True + q.migrate_metadata() + assert q.cursor is not None + assert q.conn is not None + + q.cursor.execute( + "UPDATE metadata SET value = 'not-a-number' WHERE key = 'gas_constant_ref'" + ) + q.conn.commit() + assert q.get_gas_constant_ref() == R_UNIVERSAL + + q.cursor.execute( + "UPDATE metadata SET value = '10.0' WHERE key = 'gas_constant_ref'" + ) + q.conn.commit() + assert q.get_gas_constant_ref() == R_UNIVERSAL + q.close() + + def test_calculator_connect(calc: ThermochemicalCalculator) -> None: """Test calculator connection property.""" assert calc.connected is True @@ -232,14 +313,14 @@ def test_get_all_species(calc: ThermochemicalCalculator) -> None: """Test retrieving all species.""" species = calc.get_available_species() assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' def test_find_species_by_name(calc: ThermochemicalCalculator) -> None: """Test searching species by name.""" species = calc.get_available_species('O2') assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' def test_find_nonexistent_species(calc: ThermochemicalCalculator) -> None: @@ -256,49 +337,49 @@ def test_find_nonexistent_species(calc: ThermochemicalCalculator) -> None: def test_calculate_properties_o2_298K(calc: ThermochemicalCalculator) -> None: """Test O2 properties at 298.15 K — verifies structure and correctness.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id props = calc.calculate_properties(species_id, 298.15) assert props is not None - assert props['species_name'] == 'O2' - assert props['phase'] == 'gas' + assert props.species_name == 'O2' + assert props.phase == 'gas' # Verify Cp is positive and within physically reasonable bounds - assert props['cp'] > 0 - assert props['cp'] < 200 # J/(mol·K) — well above any reasonable upper bound + assert props.cp > 0 + assert props.cp < 200 # J/(mol·K) — well above any reasonable upper bound # Temperature interval check - assert props['temp_interval'][0] == 200.0 - assert props['temp_interval'][1] == 1000.0 + assert props.temp_interval[0] == 200.0 + assert props.temp_interval[1] == 1000.0 # All properties should be finite numbers - assert math.isfinite(props['cp']) - assert math.isfinite(props['h_relative']) - assert math.isfinite(props['s']) + assert math.isfinite(props.cp) + assert math.isfinite(props.h_relative) + assert math.isfinite(props.s) def test_calculate_properties_o2_500K(calc: ThermochemicalCalculator) -> None: """Test O2 properties at 500 K.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id props = calc.calculate_properties(species_id, 500.0) assert props is not None # Cp should be positive and finite - assert props['cp'] > 0 - assert math.isfinite(props['cp']) + assert props.cp > 0 + assert math.isfinite(props.cp) # H should increase with T (check vs 298 K) props_298 = calc.calculate_properties(species_id, 298.15) assert props_298 is not None - assert props['h_relative'] > props_298['h_relative'] + assert props.h_relative > props_298.h_relative def test_calculate_properties_out_of_range(calc: ThermochemicalCalculator) -> None: """Test requesting properties outside valid temperature range.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id # Below valid range with pytest.raises(TemperatureOutOfRangeError): @@ -323,7 +404,7 @@ def test_calculate_properties_invalid_species(calc: ThermochemicalCalculator) -> def test_formation_enthalpy(calc: ThermochemicalCalculator) -> None: """Test formation enthalpy retrieval.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id h_f = calc.calculate_formation_enthalpy(species_id) # O2(g) reference element → ΔH°f = 0 @@ -339,7 +420,7 @@ def test_formation_enthalpy(calc: ThermochemicalCalculator) -> None: def test_enthalpy_change(calc: ThermochemicalCalculator) -> None: """Test enthalpy change between two temperatures.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id delta_h = calc.calculate_enthalpy_change(species_id, 298.15, 500.0) assert delta_h is not None @@ -352,7 +433,7 @@ def test_enthalpy_change(calc: ThermochemicalCalculator) -> None: def test_enthalpy_change_zero_delta(calc: ThermochemicalCalculator) -> None: """Test enthalpy change between same temperature is zero.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id delta_h = calc.calculate_enthalpy_change(species_id, 400.0, 400.0) assert delta_h is not None @@ -367,7 +448,7 @@ def test_enthalpy_change_zero_delta(calc: ThermochemicalCalculator) -> None: def test_get_properties_range(calc: ThermochemicalCalculator) -> None: """Test calculating properties at multiple temperatures.""" species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id results = calc.get_properties_range(species_id, [298.15, 500.0, 800.0]) assert results is not None @@ -377,7 +458,80 @@ def test_get_properties_range(calc: ThermochemicalCalculator) -> None: assert 800.0 in results # Cp should increase with temperature - assert results[500.0]['cp'] > results[298.15]['cp'] + assert results[500.0].cp > results[298.15].cp + + +def test_calculate_properties_range_typed(calc: ThermochemicalCalculator) -> None: + """Typed range API loads data once and preserves input order.""" + species = calc.get_available_species('O2', exact_match=True) + species_id = species[0].id + + results = calc.calculate_properties_range(species_id, [500.0, 298.15, 1500.0]) + assert [r.temperature for r in results] == [500.0, 298.15, 1500.0] + + scalar = calc.calculate_properties(species_id, 500.0) + assert results[0] == scalar + + +def test_calculate_properties_range_skips_out_of_range( + calc: ThermochemicalCalculator, +) -> None: + """Non-strict range API skips out-of-range temperatures.""" + species = calc.get_available_species('O2', exact_match=True) + species_id = species[0].id + + results = calc.calculate_properties_range(species_id, [100.0, 500.0, 7000.0]) + assert [r.temperature for r in results] == [500.0] + + +def test_calculate_properties_range_strict_raises( + calc: ThermochemicalCalculator, +) -> None: + """Strict range API raises on the first out-of-range temperature.""" + species = calc.get_available_species('O2', exact_match=True) + species_id = species[0].id + + with pytest.raises(TemperatureOutOfRangeError): + calc.calculate_properties_range(species_id, [500.0, 7000.0], strict=True) + + +def test_exception_context(calc: ThermochemicalCalculator) -> None: + """Specialised exceptions carry diagnostic context attributes.""" + species = calc.get_available_species('O2', exact_match=True) + species_id = species[0].id + + with pytest.raises(SpeciesNotFoundError) as exc_info: + calc.calculate_properties(99999, 300.0) + assert exc_info.value.species_id == 99999 + + with pytest.raises(TemperatureOutOfRangeError) as exc_info: + calc.calculate_properties(species_id, 100.0) + assert exc_info.value.temperature == 100.0 + assert exc_info.value.species_name == 'O2' + assert exc_info.value.temp_bounds == (200.0, 6000.0) + + +def test_bundled_reference_values_glenn() -> None: + """Cross-language regression anchors for the bundled NASA Glenn/CEA data. + + Values computed with R_GLENN = 8.314510 and cross-checked against the + NIST-JANAF audit (docs/audit). O2 has zero formation enthalpy; CO2 + carries a nonzero formation enthalpy. + """ + with ThermochemicalCalculator() as calc: + o2 = calc.get_available_species('O2', exact_match=True)[0] + + props = calc.calculate_properties(o2.id, 298.15) + assert props.cp == pytest.approx(29.378353377156, rel=1e-9) + assert props.s == pytest.approx(205.149467367314, rel=1e-9) + + props = calc.calculate_properties(o2.id, 1000.0) + assert props.cp == pytest.approx(34.882545009141, rel=1e-9) + assert props.h_relative == pytest.approx(22707.210697094983, rel=1e-9) + assert props.s == pytest.approx(243.587313877898, rel=1e-9) + + co2 = calc.get_available_species('CO2', exact_match=True)[0] + assert co2.heat_of_formation_298K == pytest.approx(-393510.0, rel=1e-9) # --------------------------------------------------------------------------- @@ -388,11 +542,45 @@ def test_get_properties_range(calc: ThermochemicalCalculator) -> None: def test_database_statistics(calc: ThermochemicalCalculator) -> None: """Test database statistics retrieval.""" stats = calc.db.get_statistics() - assert stats['total_species'] == 1 - assert stats['total_intervals'] == 2 - assert stats['total_coeff_sets'] == 2 - assert stats['species_by_phase'] == {'gas': 1} - assert stats['avg_molecular_weight'] == pytest.approx(31.9988) + assert stats.total_species == 1 + assert stats.total_intervals == 2 + assert stats.total_coeff_sets == 2 + assert stats.species_by_phase == {'gas': 1} + assert stats.avg_molecular_weight == pytest.approx(31.9988) + + +def test_validate_database_valid(calc: ThermochemicalCalculator) -> None: + """A well-formed database passes all integrity checks.""" + report = calc.db.validate_database() + assert report['valid'] is True + assert report['errors'] == [] + assert report['counts']['total_species'] == 1 + + +def test_validate_database_detects_orphan_coefficients( + sample_db_path: Path, +) -> None: + """Integrity checks flag coefficients without a matching interval.""" + conn = sqlite3.connect(str(sample_db_path)) + try: + conn.execute('PRAGMA foreign_keys = OFF') + conn.execute( + 'INSERT INTO coefficients ' + '(interval_id, a1, a2, a3, a4, a5, a6, a7, b1, b2) ' + 'VALUES (99999, 1, 2, 3, 4, 5, 6, 7, 8, 9)' + ) + conn.commit() + finally: + conn.close() + + q = ThermoDBQuery(str(sample_db_path)) + q.connect() + try: + report = q.validate_database() + assert report['valid'] is False + assert any('missing interval' in e for e in report['errors']) + finally: + q.close() # --------------------------------------------------------------------------- @@ -407,8 +595,8 @@ def test_thermodbquery_find_species(sample_db_path: Path) -> None: species = q.find_species('O2') assert len(species) == 1 - assert species[0]['name'] == 'O2' - assert species[0]['phase'] == 'gas' + assert species[0].name == 'O2' + assert species[0].phase == 'gas' q.close() @@ -420,13 +608,38 @@ def test_thermodbquery_get_species_data(sample_db_path: Path) -> None: data = q.get_species_data(1) assert data is not None - assert data['name'] == 'O2' - assert 'intervals' in data - assert len(data['intervals']) == 2 - assert data['intervals'][0]['temp_min'] == 200.0 - assert data['intervals'][0]['temp_max'] == 1000.0 - assert data['intervals'][1]['temp_min'] == 1000.0 - assert data['intervals'][1]['temp_max'] == 6000.0 + assert data.name == 'O2' + assert len(data.intervals) == 2 + assert data.intervals[0].temp_min == 200.0 + assert data.intervals[0].temp_max == 1000.0 + assert data.intervals[1].temp_min == 1000.0 + assert data.intervals[1].temp_max == 6000.0 + + q.close() + + +def test_thermodbquery_list_all_species(sample_db_path: Path) -> None: + """list_all_species returns every species in one ordered query.""" + q = ThermoDBQuery(str(sample_db_path)) + q.connect() + + species = q.list_all_species() + assert [sp.name for sp in species] == ['O2'] + assert all(isinstance(sp.id, int) for sp in species) + + q.close() + + +def test_thermodbquery_get_species_info(sample_db_path: Path) -> None: + """get_species_info returns metadata and None for missing IDs.""" + q = ThermoDBQuery(str(sample_db_path)) + q.connect() + + info = q.get_species_info(1) + assert info is not None + assert info.name == 'O2' + assert info.molecular_weight == 31.9988 + assert q.get_species_info(99999) is None q.close() @@ -443,12 +656,12 @@ def test_polynomial_consistency(calc: ThermochemicalCalculator) -> None: which simplifies to: d(H/RT)/dT = (Cp/R - H/RT) / T """ species = calc.get_available_species('O2') - species_id = species[0]['id'] + species_id = species[0].id # Get coefficients for 500 K interval = calc.db.get_species_for_temperature(species_id, 500.0) assert interval is not None - coeffs = interval['coefficients'] + coeffs = interval.coefficients T = 500.0 @@ -509,6 +722,82 @@ def test_is_temperature_line() -> None: assert not ThermoDBBuilder.is_temperature_line(' 5.0000E+02 5.0000E+02') +def test_coefficient_line_is_not_a_species_header() -> None: + """A NASA coefficient must never be reinterpreted as a species name.""" + coefficient_line = ( + '-1.302004763D+06 3.166984180D+04-3.031242152D+02 ' + '1.602231130D+00-4.594507340D-03' + ) + + assert ThermoDBBuilder.is_coefficient_line(coefficient_line) + assert not ThermoDBBuilder.is_species_header(coefficient_line) + + +def test_builder_rejects_partial_block_and_resynchronizes(tmp_path: Path) -> None: + """A malformed species cannot leave rows behind or hide the next species.""" + inp_path = tmp_path / 'malformed_thermo.inp' + db_path = tmp_path / 'malformed_thermo.db' + inp_path.write_text( + '\n'.join( + [ + 'THERMO', + ' 200.000 500.000 1500.000 6000.000', + _species_block('BROKEN', num_intervals=2), + _species_block('O2'), + ] + ) + ) + + builder = ThermoDBBuilder(str(inp_path), str(db_path)) + builder.connect() + builder.create_tables() + builder.parse_and_load() + builder.close() + + conn = sqlite3.connect(db_path) + try: + assert conn.execute('SELECT name FROM species').fetchall() == [('O2',)] + assert ( + conn.execute('SELECT COUNT(*) FROM temperature_intervals').fetchone()[0] + == 1 + ) + assert conn.execute('SELECT COUNT(*) FROM coefficients').fetchone()[0] == 1 + finally: + conn.close() + + +def test_builder_writes_metadata_and_calculator_uses_it(tmp_path: Path) -> None: + """New databases persist R_GLENN and use it for dimensional properties.""" + inp_path = tmp_path / 'metadata_thermo.inp' + db_path = tmp_path / 'metadata_thermo.db' + inp_path.write_text( + '\n'.join( + [ + 'THERMO', + ' 200.000 500.000 1500.000 6000.000', + _species_block('O2'), + ] + ) + ) + + builder = ThermoDBBuilder(str(inp_path), str(db_path)) + builder.connect() + builder.create_tables() + builder.parse_and_load() + builder.close() + + calc = ThermochemicalCalculator(str(db_path)) + assert calc.connect() is True + assert calc.r_ref == R_GLENN + species = calc.get_available_species('O2', exact_match=True) + properties = calc.calculate_properties(species[0].id, 500.0) + interval = calc.db.get_species_for_temperature(species[0].id, 500.0) + assert interval is not None + cp_r = calc.db.calculate_cp(interval.coefficients, 500.0) + assert properties.cp == pytest.approx(cp_r * R_GLENN) + calc.close() + + def test_parse_species_record() -> None: """Test RECORD 1 species name and comments parsing.""" name, comments = ThermoDBBuilder.parse_species_record( @@ -530,6 +819,19 @@ def test_parse_general_info_record() -> None: assert info['phase'] == 'gas' +def test_parse_general_info_record_heat_of_formation() -> None: + """RECORD 2 extracts molecular weight and heat of formation correctly.""" + builder = ThermoDBBuilder('dummy.inp', 'dummy.db') + line = ( + ' 3 g 9/99 C 1.00O 2.00 0.00 0.00 0.00 0 44.0095000' + ' -393510.000' + ) + info = builder.parse_general_info_record(line) + assert info['num_intervals'] == 3 + assert info['molecular_weight'] == pytest.approx(44.0095) + assert info['heat_of_formation'] == pytest.approx(-393510.0) + + def test_parse_temp_interval_record() -> None: """Test RECORD 3 temperature interval parsing.""" # FORTRAN format: cols 0-10 (temp_min), 11-21 (temp_max), 65-79 (h_298) @@ -576,7 +878,7 @@ def test_list_species_page(sample_db_path: Path) -> None: species, total_pages = q.list_species_page(page=1, page_size=10) assert len(species) == 1 assert total_pages == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Page beyond available data species, total_pages = q.list_species_page(page=2, page_size=10) @@ -649,13 +951,13 @@ def test_calculate_properties_lower_boundary( ) -> None: """O2 properties at T = 200.0 K — exactly the lower interval boundary.""" species = calc.get_available_species('O2', exact_match=True) - species_id = species[0]['id'] + species_id = species[0].id props = calc.calculate_properties(species_id, 200.0) assert props is not None - assert props['temp_interval'] == [200.0, 1000.0] - assert math.isfinite(props['cp']) - assert math.isfinite(props['h_relative']) + assert props.temp_interval == (200.0, 1000.0) + assert math.isfinite(props.cp) + assert math.isfinite(props.h_relative) def test_calculate_properties_shared_boundary( @@ -668,15 +970,15 @@ def test_calculate_properties_shared_boundary( floating-point edge cases in the SQL query. """ species = calc.get_available_species('O2', exact_match=True) - species_id = species[0]['id'] + species_id = species[0].id props = calc.calculate_properties(species_id, 1000.0) assert props is not None # The lower interval (interval_number=1) should be selected when both # intervals cover the same temperature (ORDER BY interval_number). - assert props['temp_interval'] == [200.0, 1000.0] - assert math.isfinite(props['cp']) - assert math.isfinite(props['h_relative']) + assert props.temp_interval == (200.0, 1000.0) + assert math.isfinite(props.cp) + assert math.isfinite(props.h_relative) def test_calculate_properties_upper_boundary( @@ -684,13 +986,13 @@ def test_calculate_properties_upper_boundary( ) -> None: """O2 properties at T = 6000.0 K — exactly the upper interval boundary.""" species = calc.get_available_species('O2', exact_match=True) - species_id = species[0]['id'] + species_id = species[0].id props = calc.calculate_properties(species_id, 6000.0) assert props is not None - assert props['temp_interval'] == [1000.0, 6000.0] - assert math.isfinite(props['cp']) - assert math.isfinite(props['h_relative']) + assert props.temp_interval == (1000.0, 6000.0) + assert math.isfinite(props.cp) + assert math.isfinite(props.h_relative) # --------------------------------------------------------------------------- @@ -705,17 +1007,17 @@ def test_get_available_species_exact_match( # Substring search (default) — works fine for 'O2' in this small test db species = calc.get_available_species('O2') assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Exact match — also works species = calc.get_available_species('O2', exact_match=True) assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Exact match — case insensitive species = calc.get_available_species('o2', exact_match=True) assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Exact match — no match species = calc.get_available_species('O3', exact_match=True) @@ -732,12 +1034,12 @@ def test_thermodbquery_find_species_exact_match( # Substring search (default) species = q.find_species('O2') assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Exact match species = q.find_species('O2', exact_match=True) assert len(species) == 1 - assert species[0]['name'] == 'O2' + assert species[0].name == 'O2' # Exact match — no result for partial name species = q.find_species('O', exact_match=True) @@ -812,3 +1114,33 @@ def test_cli_query_smoke( captured = capsys.readouterr() assert 'SUCCESS' in captured.out + + +def test_cli_migrate_metadata_smoke( + sample_db_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Test migrate-metadata command runs without errors.""" + import argparse + + from pyglenn.cli import cmd_migrate_metadata + + args = argparse.Namespace(database=str(sample_db_path)) + cmd_migrate_metadata(args) + + captured = capsys.readouterr() + assert 'SUCCESS' in captured.out + + +def test_cli_validate_smoke( + sample_db_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Test validate command runs without errors.""" + import argparse + + from pyglenn.cli import cmd_validate + + args = argparse.Namespace(database=str(sample_db_path)) + cmd_validate(args) + + captured = capsys.readouterr() + assert 'SUCCESS' in captured.out diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..8ee2790 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,130 @@ +"""Tests for the typed domain model (pyglenn.models).""" + +from __future__ import annotations + +import sqlite3 +from dataclasses import asdict + +from pyglenn.models import ( + DatabaseStats, + IntervalData, + NASACoefficients, + SpeciesData, + SpeciesInfo, + ThermoProperties, +) + + +def _coeffs() -> NASACoefficients: + return NASACoefficients(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0) + + +def test_nasa_coefficients_from_mapping_round_trip() -> None: + """from_mapping and to_dict round-trip the nine coefficients.""" + mapping = { + 'a1': 1.0, + 'a2': 2.0, + 'a3': 3.0, + 'a4': 4.0, + 'a5': 5.0, + 'a6': 6.0, + 'a7': 7.0, + 'b1': 8.0, + 'b2': 9.0, + } + coeffs = NASACoefficients.from_mapping(mapping) + assert coeffs == _coeffs() + assert coeffs.to_dict() == mapping + assert asdict(coeffs) == mapping + + +def test_species_info_from_mapping_tolerates_missing_keys() -> None: + """Missing columns become None rather than raising.""" + info = SpeciesInfo.from_mapping({'id': 1, 'name': 'O2'}) + assert info.id == 1 + assert info.name == 'O2' + assert info.formula is None + assert info.phase is None + assert info.molecular_weight is None + + +def test_species_info_from_sqlite_row() -> None: + """from_mapping accepts a sqlite3.Row directly.""" + conn = sqlite3.connect(':memory:') + conn.row_factory = sqlite3.Row + try: + conn.execute( + 'CREATE TABLE species (id INTEGER, name TEXT, formula TEXT, ' + 'phase TEXT, molecular_weight REAL)' + ) + conn.execute("INSERT INTO species VALUES (1, 'O2', 'O2', 'gas', 31.9988)") + conn.commit() + row = conn.execute('SELECT * FROM species').fetchone() + finally: + conn.close() + + info = SpeciesInfo.from_mapping(row) + assert info.id == 1 + assert info.name == 'O2' + assert info.formula == 'O2' + assert info.phase == 'gas' + assert info.molecular_weight == 31.9988 + + +def test_thermo_properties_to_dict_serialises_interval_as_list() -> None: + """to_dict keeps the legacy dict shape (temp_interval as a list).""" + props = ThermoProperties( + temperature=298.15, + cp=29.0, + h_relative=8680.0, + s=205.0, + temp_interval=(200.0, 1000.0), + species_name='O2', + phase='gas', + ) + data = props.to_dict() + assert data['temp_interval'] == [200.0, 1000.0] + assert data['species_name'] == 'O2' + assert data['cp'] == 29.0 + + +def test_species_data_to_dict_preserves_nested_intervals() -> None: + """SpeciesData serialises intervals with nested coefficient dicts.""" + data = SpeciesData( + id=1, + name='O2', + formula='O2', + phase='gas', + molecular_weight=31.9988, + heat_of_formation_298K=0.0, + num_intervals=1, + intervals=(IntervalData(1, 200.0, 1000.0, 8680.0, _coeffs()),), + ) + payload = data.to_dict() + assert payload['intervals'][0]['temp_min'] == 200.0 + assert payload['intervals'][0]['coefficients']['a1'] == 1.0 + + +def test_database_stats_to_dict() -> None: + """DatabaseStats serialises phase counts and average molecular weight.""" + stats = DatabaseStats( + total_species=2035, + total_intervals=3779, + total_coeff_sets=3779, + species_by_phase={'gas': 1266, 'condensed': 769}, + avg_molecular_weight=132.03, + ) + assert stats.total_species == 2035 + assert stats.to_dict()['species_by_phase'] == {'gas': 1266, 'condensed': 769} + + +def test_models_exported_from_package() -> None: + """All public models are importable from the top-level package.""" + import pyglenn + + assert pyglenn.ThermoProperties is ThermoProperties + assert pyglenn.SpeciesInfo is SpeciesInfo + assert pyglenn.NASACoefficients is NASACoefficients + assert pyglenn.IntervalData is IntervalData + assert pyglenn.SpeciesData is SpeciesData + assert pyglenn.DatabaseStats is DatabaseStats