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, fluid_name: Option, } 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) -> 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, ) -> Result { 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 { 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 { &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 { 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 { 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); } }