feat(python): implement python bindings for all components and solvers
This commit is contained in:
2
crates/entropyk/.cargo/config.toml
Normal file
2
crates/entropyk/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
rustdocflags = ["--html-in-header", "../../../docs/katex-header.html"]
|
||||
26
crates/entropyk/Cargo.toml
Normal file
26
crates/entropyk/Cargo.toml
Normal 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
63
crates/entropyk/README.md
Normal 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.
|
||||
311
crates/entropyk/src/builder.rs
Normal file
311
crates/entropyk/src/builder.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
160
crates/entropyk/src/error.rs
Normal file
160
crates/entropyk/src/error.rs
Normal 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
172
crates/entropyk/src/lib.rs
Normal 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};
|
||||
}
|
||||
158
crates/entropyk/tests/api_usage.rs
Normal file
158
crates/entropyk/tests/api_usage.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user