feat(python): implement python bindings for all components and solvers

This commit is contained in:
Sepehr
2026-02-21 20:34:56 +01:00
parent 8ef8cd2eba
commit 4440132b0a
310 changed files with 11577 additions and 397 deletions

View File

@@ -0,0 +1,2 @@
[build]
rustdocflags = ["--html-in-header", "../../../docs/katex-header.html"]

View File

@@ -0,0 +1,26 @@
[package]
name = "entropyk"
description = "A thermodynamic cycle simulation library with type-safe APIs"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
readme = "README.md"
keywords = ["thermodynamics", "simulation", "hvac", "refrigeration", "engineering"]
categories = ["science", "simulation"]
[dependencies]
entropyk-core = { path = "../core" }
entropyk-components = { path = "../components" }
entropyk-fluids = { path = "../fluids" }
entropyk-solver = { path = "../solver" }
thiserror = { workspace = true }
petgraph = "0.6"
[dev-dependencies]
approx = "0.5"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--html-in-header", "docs/katex-header.html"]

63
crates/entropyk/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Entropyk
A thermodynamic cycle simulation library with type-safe APIs and idiomatic Rust design.
## Features
- **Type-safe physical quantities**: Never mix up units with NewType wrappers for Pressure, Temperature, Enthalpy, and MassFlow
- **Component-based modeling**: Build complex systems from reusable blocks (Compressor, Condenser, Evaporator, etc.)
- **Multiple solver strategies**: Newton-Raphson with automatic fallback to Sequential Substitution
- **Multi-fluid support**: CoolProp integration, tabular interpolation, incompressible fluids
- **Zero-panic policy**: All errors return `Result<T, ThermoError>`
## Quick Start
Add to your `Cargo.toml`:
```toml
[dependencies]
entropyk = "0.1"
```
## Example
```rust,ignore
use entropyk::{
System, Solver, NewtonConfig,
Compressor, Condenser, Evaporator, ExpansionValve,
Ahri540Coefficients, ThermalConductance,
};
// Build a simple refrigeration cycle
let mut system = System::new();
// Define component parameters (see API docs for details)
let coeffs = Ahri540Coefficients { /* ... */ };
let ua = ThermalConductance::new(5000.0);
// Add components
let comp = system.add_component(Box::new(Compressor::new(coeffs)));
let cond = system.add_component(Box::new(Condenser::new(ua)));
let evap = system.add_component(Box::new(Evaporator::new(ua)));
let valve = system.add_component(Box::new(ExpansionValve::new()));
// Connect components
system.add_edge(comp, cond)?;
system.add_edge(cond, valve)?;
system.add_edge(valve, evap)?;
system.add_edge(evap, comp)?;
// Finalize and solve
system.finalize()?;
let solver = NewtonConfig::default();
let result = solver.solve(&system)?;
```
## Documentation
See the [API documentation](https://docs.rs/entropyk) for full details.
## License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.

View File

@@ -0,0 +1,311 @@
use std::collections::HashMap;
use thiserror::Error;
use crate::ThermoError;
/// Error type for system builder operations.
#[derive(Error, Debug, Clone)]
pub enum SystemBuilderError {
/// A component with the given name already exists in the builder.
#[error("Component '{0}' already exists")]
ComponentExists(String),
/// The specified component name was not found in the builder.
#[error("Component '{0}' not found")]
ComponentNotFound(String),
/// Failed to create an edge between two components.
#[error("Failed to create edge from '{from}' to '{to}': {reason}")]
EdgeFailed {
/// Name of the source component.
from: String,
/// Name of the target component.
to: String,
/// Reason for the failure.
reason: String,
},
/// The system must be finalized before this operation.
#[error("System must be finalized before solving")]
NotFinalized,
/// Cannot build a system with no components.
#[error("Cannot build an empty system")]
EmptySystem,
}
/// A builder for creating thermodynamic systems with a fluent API.
///
/// The `SystemBuilder` provides an ergonomic way to construct thermodynamic
/// systems by adding components and edges with human-readable names.
///
/// # Example
///
/// ```
/// use entropyk::SystemBuilder;
///
/// let builder = SystemBuilder::new();
/// assert_eq!(builder.component_count(), 0);
/// ```
///
/// For real components, see the crate-level documentation.
pub struct SystemBuilder {
system: entropyk_solver::System,
component_names: HashMap<String, petgraph::graph::NodeIndex>,
fluid_name: Option<String>,
}
impl SystemBuilder {
/// Creates a new empty system builder.
pub fn new() -> Self {
Self {
system: entropyk_solver::System::new(),
component_names: HashMap::new(),
fluid_name: None,
}
}
/// Sets the default fluid for the system.
///
/// This stores the fluid name for reference. The actual fluid assignment
/// to components is handled at the component/port level.
///
/// # Arguments
///
/// * `fluid` - The fluid name (e.g., "R134a", "R410A", "CO2")
#[inline]
pub fn with_fluid(mut self, fluid: impl Into<String>) -> Self {
self.fluid_name = Some(fluid.into());
self
}
/// Adds a named component to the system.
///
/// The name is used for later reference when creating edges.
/// Returns an error if a component with the same name already exists.
///
/// # Arguments
///
/// * `name` - A unique identifier for this component
/// * `component` - The component to add
#[inline]
pub fn component(
mut self,
name: &str,
component: Box<dyn entropyk_components::Component>,
) -> Result<Self, SystemBuilderError> {
if self.component_names.contains_key(name) {
return Err(SystemBuilderError::ComponentExists(name.to_string()));
}
let idx = self.system.add_component(component);
self.component_names.insert(name.to_string(), idx);
Ok(self)
}
/// Creates an edge between two named components.
///
/// The edge represents a fluid connection from the source component's
/// outlet to the target component's inlet.
///
/// # Arguments
///
/// * `from` - Name of the source component
/// * `to` - Name of the target component
///
/// # Errors
///
/// Returns an error if either component name is not found.
#[inline]
pub fn edge(mut self, from: &str, to: &str) -> Result<Self, SystemBuilderError> {
let from_idx = self
.component_names
.get(from)
.ok_or_else(|| SystemBuilderError::ComponentNotFound(from.to_string()))?;
let to_idx = self
.component_names
.get(to)
.ok_or_else(|| SystemBuilderError::ComponentNotFound(to.to_string()))?;
self.system
.add_edge(*from_idx, *to_idx)
.map_err(|e| SystemBuilderError::EdgeFailed {
from: from.to_string(),
to: to.to_string(),
reason: e.to_string(),
})?;
Ok(self)
}
/// Gets the underlying system without finalizing.
///
/// This is useful when you need to perform additional operations
/// on the system before finalizing.
pub fn into_inner(self) -> entropyk_solver::System {
self.system
}
/// Gets a reference to the component name to index mapping.
pub fn component_names(&self) -> &HashMap<String, petgraph::graph::NodeIndex> {
&self.component_names
}
/// Returns the number of components added so far.
pub fn component_count(&self) -> usize {
self.component_names.len()
}
/// Returns the number of edges created so far.
pub fn edge_count(&self) -> usize {
self.system.edge_count()
}
/// Builds and finalizes the system.
///
/// This method consumes the builder and returns a finalized [`entropyk_solver::System`]
/// ready for solving.
///
/// # Errors
///
/// Returns an error if:
/// - The system is empty (no components)
/// - Finalization fails (e.g., invalid topology)
pub fn build(self) -> Result<entropyk_solver::System, ThermoError> {
if self.component_names.is_empty() {
return Err(ThermoError::Builder(SystemBuilderError::EmptySystem));
}
let mut system = self.system;
system.finalize()?;
Ok(system)
}
/// Builds the system without finalizing.
///
/// Use this when you need to perform additional operations
/// that require an unfinalized system.
pub fn build_unfinalized(self) -> Result<entropyk_solver::System, SystemBuilderError> {
if self.component_names.is_empty() {
return Err(SystemBuilderError::EmptySystem);
}
Ok(self.system)
}
}
impl Default for SystemBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use entropyk_components::ComponentError;
struct MockComponent {
n_eqs: usize,
}
impl entropyk_components::Component for MockComponent {
fn compute_residuals(
&self,
_state: &entropyk_components::SystemState,
_residuals: &mut entropyk_components::ResidualVector,
) -> Result<(), ComponentError> {
Ok(())
}
fn jacobian_entries(
&self,
_state: &entropyk_components::SystemState,
_jacobian: &mut entropyk_components::JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eqs
}
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
&[]
}
}
#[test]
fn test_builder_creates_system() {
let builder = SystemBuilder::new();
assert_eq!(builder.component_count(), 0);
assert_eq!(builder.edge_count(), 0);
}
#[test]
fn test_add_component() {
let builder = SystemBuilder::new()
.component("comp1", Box::new(MockComponent { n_eqs: 2 }))
.unwrap();
assert_eq!(builder.component_count(), 1);
}
#[test]
fn test_duplicate_component_error() {
let result = SystemBuilder::new()
.component("comp", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.component("comp", Box::new(MockComponent { n_eqs: 1 }));
assert!(result.is_err());
if let Err(SystemBuilderError::ComponentExists(name)) = result {
assert_eq!(name, "comp");
} else {
panic!("Expected ComponentExists error");
}
}
#[test]
fn test_add_edge() {
let builder = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.component("b", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge("a", "b")
.unwrap();
assert_eq!(builder.edge_count(), 1);
}
#[test]
fn test_edge_missing_component() {
let result = SystemBuilder::new()
.component("a", Box::new(MockComponent { n_eqs: 1 }))
.unwrap()
.edge("a", "nonexistent");
assert!(result.is_err());
if let Err(SystemBuilderError::ComponentNotFound(name)) = result {
assert_eq!(name, "nonexistent");
} else {
panic!("Expected ComponentNotFound error");
}
}
#[test]
fn test_build_empty_system() {
let result = SystemBuilder::new().build();
assert!(result.is_err());
}
#[test]
fn test_default() {
let builder = SystemBuilder::default();
assert_eq!(builder.component_count(), 0);
}
}

View File

@@ -0,0 +1,160 @@
use thiserror::Error;
use crate::builder::SystemBuilderError;
/// Unified error type for all Entropyk operations.
///
/// This enum wraps all possible errors that can occur when using the library,
/// providing a single error type for the public API.
#[derive(Error, Debug)]
pub enum ThermoError {
/// Error from component operations.
#[error("Component error: {0}")]
Component(entropyk_components::ComponentError),
/// Error from solver operations.
#[error("Solver error: {0}")]
Solver(entropyk_solver::SolverError),
/// Error from fluid property calculations.
#[error("Fluid error: {0}")]
Fluid(entropyk_fluids::FluidError),
/// Error from topology operations.
#[error("Topology error: {0}")]
Topology(entropyk_solver::TopologyError),
/// Error adding an edge to the system.
#[error("Edge error: {0}")]
AddEdge(entropyk_solver::AddEdgeError),
/// Error from connection operations.
#[error("Connection error: {0}")]
Connection(entropyk_components::ConnectionError),
/// Error from constraint operations.
#[error("Constraint error: {0}")]
Constraint(entropyk_solver::ConstraintError),
/// Error from initialization.
#[error("Initialization error: {0}")]
Initialization(entropyk_solver::InitializerError),
/// Error from calibration validation.
#[error("Calibration error: {0}")]
Calibration(entropyk_core::CalibValidationError),
/// Error from mixture operations.
#[error("Mixture error: {0}")]
Mixture(entropyk_fluids::MixtureError),
/// Error from system builder operations.
#[error("Builder error: {0}")]
Builder(SystemBuilderError),
/// Invalid input was provided.
#[error("Invalid input: {0}")]
InvalidInput(String),
/// Operation is not supported.
#[error("Operation not supported: {0}")]
NotSupported(String),
/// System was not finalized before an operation.
#[error("System must be finalized before this operation")]
NotFinalized,
}
impl ThermoError {
/// Creates a new `InvalidInput` error with the given message.
#[inline]
pub fn invalid_input(msg: impl Into<String>) -> Self {
Self::InvalidInput(msg.into())
}
/// Creates a new `NotSupported` error with the given message.
#[inline]
pub fn not_supported(msg: impl Into<String>) -> Self {
Self::NotSupported(msg.into())
}
}
impl From<entropyk_components::ComponentError> for ThermoError {
#[inline]
fn from(e: entropyk_components::ComponentError) -> Self {
Self::Component(e)
}
}
impl From<entropyk_solver::SolverError> for ThermoError {
#[inline]
fn from(e: entropyk_solver::SolverError) -> Self {
Self::Solver(e)
}
}
impl From<entropyk_fluids::FluidError> for ThermoError {
#[inline]
fn from(e: entropyk_fluids::FluidError) -> Self {
Self::Fluid(e)
}
}
impl From<entropyk_solver::TopologyError> for ThermoError {
#[inline]
fn from(e: entropyk_solver::TopologyError) -> Self {
Self::Topology(e)
}
}
impl From<entropyk_solver::AddEdgeError> for ThermoError {
#[inline]
fn from(e: entropyk_solver::AddEdgeError) -> Self {
Self::AddEdge(e)
}
}
impl From<entropyk_components::ConnectionError> for ThermoError {
#[inline]
fn from(e: entropyk_components::ConnectionError) -> Self {
Self::Connection(e)
}
}
impl From<entropyk_solver::ConstraintError> for ThermoError {
#[inline]
fn from(e: entropyk_solver::ConstraintError) -> Self {
Self::Constraint(e)
}
}
impl From<entropyk_solver::InitializerError> for ThermoError {
#[inline]
fn from(e: entropyk_solver::InitializerError) -> Self {
Self::Initialization(e)
}
}
impl From<entropyk_core::CalibValidationError> for ThermoError {
#[inline]
fn from(e: entropyk_core::CalibValidationError) -> Self {
Self::Calibration(e)
}
}
impl From<entropyk_fluids::MixtureError> for ThermoError {
#[inline]
fn from(e: entropyk_fluids::MixtureError) -> Self {
Self::Mixture(e)
}
}
impl From<SystemBuilderError> for ThermoError {
#[inline]
fn from(e: SystemBuilderError) -> Self {
Self::Builder(e)
}
}
/// A specialized `Result` type for Entropyk operations.
pub type ThermoResult<T> = Result<T, ThermoError>;

172
crates/entropyk/src/lib.rs Normal file
View File

@@ -0,0 +1,172 @@
//! # Entropyk
//!
//! A thermodynamic cycle simulation library with type-safe APIs and idiomatic Rust design.
//!
//! Entropyk provides a complete toolkit for simulating refrigeration cycles, heat pumps,
//! and other thermodynamic systems. Built with a focus on type safety, performance, and
//! developer ergonomics.
//!
//! ## Features
//!
//! - **Type-safe physical quantities**: Never mix up units with NewType wrappers
//! - **Component-based modeling**: Build complex systems from reusable blocks
//! - **Multiple solver strategies**: Newton-Raphson with automatic fallback
//! - **Multi-fluid support**: CoolProp, tabular interpolation, incompressible fluids
//! - **Zero-panic policy**: All errors return `Result<T, E>`
//!
//! ## Quick Start
//!
//! The [`SystemBuilder`] provides an ergonomic way to construct thermodynamic systems:
//!
//! ```
//! use entropyk::SystemBuilder;
//!
//! let builder = SystemBuilder::new();
//! assert_eq!(builder.component_count(), 0);
//! ```
//!
//! For a complete refrigeration cycle example with real components:
//!
//! ```ignore
//! use entropyk::{
//! System, Solver, NewtonConfig,
//! Compressor, Condenser, Evaporator, ExpansionValve,
//! Pressure, Temperature,
//! };
//!
//! // Build a simple refrigeration cycle
//! let mut system = System::new();
//!
//! // Add components
//! let comp = system.add_component(Box::new(Compressor::new(coeffs)));
//! let cond = system.add_component(Box::new(Condenser::new(ua)));
//! let evap = system.add_component(Box::new(Evaporator::new(ua)));
//! let valve = system.add_component(Box::new(ExpansionValve::new()));
//!
//! // Connect components
//! system.add_edge(comp, cond)?;
//! system.add_edge(cond, valve)?;
//! system.add_edge(valve, evap)?;
//! system.add_edge(evap, comp)?;
//!
//! // Finalize and solve
//! system.finalize()?;
//!
//! let solver = NewtonConfig::default();
//! let result = solver.solve(&system)?;
//! ```
//!
//! ## Architecture
//!
//! The library re-exports types from these source crates:
//!
//! - **Core types**: [`Pressure`], [`Temperature`], [`Enthalpy`], [`MassFlow`], [`Power`]
//! - **Components**: [`Component`], [`Compressor`], [`Condenser`], [`Evaporator`], etc.
//! - **Fluids**: [`FluidBackend`], [`CoolPropBackend`], [`TabularBackend`]
//! - **Solver**: [`System`], [`Solver`], [`NewtonConfig`], [`PicardConfig`]
//!
//! ## Error Handling
//!
//! All operations return `Result<T, ThermoError>` with comprehensive error types.
//! The library follows a zero-panic policy - no operation should ever panic.
//!
//! ## Documentation
//!
//! Mathematical formulas in the documentation use LaTeX notation:
//!
//! $$ W = \dot{m} \cdot (h_{out} - h_{in}) $$
//!
//! where $W$ is work, $\dot{m}$ is mass flow rate, and $h$ is specific enthalpy.
#![deny(unsafe_code)]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
// =============================================================================
// Core Types Re-exports
// =============================================================================
pub use entropyk_core::{
Calib, CalibIndices, CalibValidationError, Enthalpy, MassFlow, Power, Pressure, Temperature,
ThermalConductance, MIN_MASS_FLOW_REGULARIZATION_KG_S,
};
// =============================================================================
// Components Re-exports
// =============================================================================
pub use entropyk_components::{
friction_factor, roughness, AffinityLaws, Ahri540Coefficients, CircuitId, Component,
ComponentError, CompressibleMerger, CompressibleSink, CompressibleSource, CompressibleSplitter,
Compressor, CompressorModel, Condenser, CondenserCoil, ConnectedPort, ConnectionError,
Economizer, EpsNtuModel, Evaporator, EvaporatorCoil, ExchangerType, ExpansionValve,
ExternalModel, ExternalModelConfig, ExternalModelError, ExternalModelMetadata,
ExternalModelType, Fan, FanCurves, FlowConfiguration, FlowMerger, FlowSink, FlowSource,
FlowSplitter, FluidKind, HeatExchanger, HeatExchangerBuilder, HeatTransferModel,
HxSideConditions, IncompressibleMerger, IncompressibleSink, IncompressibleSource,
IncompressibleSplitter, JacobianBuilder, LmtdModel, MockExternalModel, OperationalState,
PerformanceCurves, PhaseRegion, Pipe, PipeGeometry, Polynomial1D, Polynomial2D, Pump,
PumpCurves, ResidualVector, SstSdtCoefficients, StateHistory, StateManageable,
StateTransitionError, SystemState, ThreadSafeExternalModel,
};
pub use entropyk_components::port::{Connected, Disconnected, FluidId as ComponentFluidId, Port};
// =============================================================================
// Fluids Re-exports
// =============================================================================
pub use entropyk_fluids::{
CachedBackend, CoolPropBackend, CriticalPoint, DampedBackend, DampingParams, DampingState,
Entropy, FluidBackend, FluidError, FluidId, FluidResult, FluidState, IncompFluid,
IncompressibleBackend, Mixture, MixtureError, Phase, Property, Quality, TabularBackend,
TestBackend, ThermoState, ValidRange,
};
// =============================================================================
// Solver Re-exports
// =============================================================================
pub use entropyk_solver::{
antoine_pressure, compute_coupling_heat, coupling_groups, has_circular_dependencies,
AddEdgeError, AntoineCoefficients, CircuitConvergence, CircuitId as SolverCircuitId,
ComponentOutput, Constraint, ConstraintError, ConstraintId, ConvergedState,
ConvergenceCriteria, ConvergenceReport, ConvergenceStatus, FallbackConfig, FallbackSolver,
FlowEdge, InitializerConfig, InitializerError, JacobianFreezingConfig, JacobianMatrix,
MacroComponent, MacroComponentSnapshot, NewtonConfig, PicardConfig, PortMapping,
SmartInitializer, Solver, SolverError, SolverStrategy, System, ThermalCoupling, TimeoutConfig,
TopologyError,
};
// =============================================================================
// Error Types (must come before builder)
// =============================================================================
mod error;
pub use error::{ThermoError, ThermoResult};
// =============================================================================
// Builder Pattern
// =============================================================================
mod builder;
pub use builder::{SystemBuilder, SystemBuilderError};
// =============================================================================
// Prelude
// =============================================================================
/// Common imports for Entropyk users.
///
/// This module re-exports the most commonly used types and traits
/// for convenience. Import it with:
///
/// ```
/// use entropyk::prelude::*;
/// ```
pub mod prelude {
pub use crate::ThermoError;
pub use entropyk_components::Component;
pub use entropyk_core::{Enthalpy, MassFlow, Power, Pressure, Temperature};
pub use entropyk_solver::{NewtonConfig, Solver, System};
}

View File

@@ -0,0 +1,158 @@
//! Integration tests for the Entropyk public API.
//!
//! These tests verify the builder pattern, error propagation, and overall
//! API ergonomics using real component types.
use entropyk::{System, SystemBuilder, ThermoError};
use entropyk_components::{
Component, ComponentError, JacobianBuilder, ResidualVector, SystemState,
};
struct MockComponent {
name: &'static str,
n_eqs: usize,
}
impl Component for MockComponent {
fn compute_residuals(
&self,
_state: &SystemState,
_residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eqs
}
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
&[]
}
}
#[test]
fn test_builder_creates_empty_system() {
let builder = SystemBuilder::new();
assert_eq!(builder.component_count(), 0);
assert_eq!(builder.edge_count(), 0);
}
#[test]
fn test_builder_adds_components() {
let builder = SystemBuilder::new()
.component(
"comp1",
Box::new(MockComponent {
name: "comp1",
n_eqs: 2,
}),
)
.expect("should add component");
assert_eq!(builder.component_count(), 1);
}
#[test]
fn test_builder_rejects_duplicate_names() {
let result = SystemBuilder::new()
.component(
"dup",
Box::new(MockComponent {
name: "dup",
n_eqs: 1,
}),
)
.expect("first add should succeed")
.component(
"dup",
Box::new(MockComponent {
name: "dup",
n_eqs: 1,
}),
);
assert!(result.is_err());
}
#[test]
fn test_builder_creates_edges() {
let builder = SystemBuilder::new()
.component(
"a",
Box::new(MockComponent {
name: "a",
n_eqs: 1,
}),
)
.expect("add a")
.component(
"b",
Box::new(MockComponent {
name: "b",
n_eqs: 1,
}),
)
.expect("add b")
.edge("a", "b")
.expect("edge a->b");
assert_eq!(builder.edge_count(), 1);
}
#[test]
fn test_builder_rejects_missing_edge_component() {
let result = SystemBuilder::new()
.component(
"a",
Box::new(MockComponent {
name: "a",
n_eqs: 1,
}),
)
.expect("add a")
.edge("a", "nonexistent");
assert!(result.is_err());
}
#[test]
fn test_builder_into_inner() {
let system = SystemBuilder::new()
.component(
"c",
Box::new(MockComponent {
name: "c",
n_eqs: 1,
}),
)
.expect("add c")
.into_inner();
assert_eq!(system.node_count(), 1);
}
#[test]
fn test_direct_system_api() {
let mut system = System::new();
let idx = system.add_component(Box::new(MockComponent {
name: "test",
n_eqs: 2,
}));
assert_eq!(system.node_count(), 1);
}
#[test]
fn test_error_types_are_compatible() {
fn _assert_thermo_error_from_component(e: ComponentError) -> ThermoError {
e.into()
}
}