#![allow(dead_code)] //! Shared helpers for the `entropyk-solver` Phase-0 Criterion benchmarks. //! //! Provides: //! - A deterministic mock refrigeration cycle for micro-benchmarks (LU solve, //! residual/Jacobian assembly). //! - Three reference cycles built directly from the public component/solver APIs //! so the end-to-end benchmarks exercise real CoolProp solves without paying //! the cost of spawning a CLI process. use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Arc; use std::time::Duration; use criterion::Criterion; use entropyk_components::port::{Connected, FluidId, Port}; use entropyk_components::{ Component, ComponentError, Condenser, ConnectedPort, Evaporator, IsenthalpicExpansionValve, IsentropicCompressor, JacobianBuilder, ResidualVector, StateSlice, }; use entropyk_core::{Enthalpy, MassFlow, Pressure}; use entropyk_fluids::{CoolPropBackend, FluidBackend}; use entropyk_solver::system::{System, DEFAULT_MASS_FLOW_SEED_KG_S}; use entropyk_solver::{FallbackConfig, FallbackSolver, JacobianMatrix, NewtonConfig, Solver}; type CP = Port; // ── Mock components (copied from `refrigeration_cycle_integration.rs`) ─────── struct MockCompressor { port_suc: CP, port_disc: CP, } impl Component for MockCompressor { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_disc.pressure().to_pascals() - (self.port_suc.pressure().to_pascals() + 1_000_000.0); r[1] = self.port_disc.enthalpy().to_joules_per_kg() - (self.port_suc.enthalpy().to_joules_per_kg() + 75_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } struct MockCondenser { port_in: CP, port_out: CP, } impl Component for MockCondenser { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals(); r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() - 225_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } struct MockValve { port_in: CP, port_out: CP, } impl Component for MockValve { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - 1_000_000.0); r[1] = self.port_out.enthalpy().to_joules_per_kg() - self.port_in.enthalpy().to_joules_per_kg(); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } struct MockEvaporator { port_in: CP, port_out: CP, } impl Component for MockEvaporator { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals(); r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() + 150_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } fn port(p_pa: f64, h_j_kg: f64) -> CP { let (connected, _) = Port::new( FluidId::new("R134a"), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), ) .connect(Port::new( FluidId::new("R134a"), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), )) .unwrap(); connected } /// Builds the deterministic 4-component mock cycle used for Jacobian/assembly /// micro-benchmarks. The system is analytically closed and converges in one /// iteration from the exact initial state. pub fn build_mock_system() -> (System, Vec) { let p_lp = 350_000.0_f64; let p_hp = 1_350_000.0_f64; let comp = Box::new(MockCompressor { port_suc: port(p_lp, 410_000.0), port_disc: port(p_hp, 485_000.0), }); let cond = Box::new(MockCondenser { port_in: port(p_hp, 485_000.0), port_out: port(p_hp, 260_000.0), }); let valv = Box::new(MockValve { port_in: port(p_hp, 260_000.0), port_out: port(p_lp, 260_000.0), }); let evap = Box::new(MockEvaporator { port_in: port(p_lp, 260_000.0), port_out: port(p_lp, 410_000.0), }); let mut system = System::new(); let n_comp = system.add_component(comp); let n_cond = system.add_component(cond); let n_valv = system.add_component(valv); let n_evap = system.add_component(evap); system.add_edge(n_comp, n_cond).unwrap(); system.add_edge(n_cond, n_valv).unwrap(); system.add_edge(n_valv, n_evap).unwrap(); system.add_edge(n_evap, n_comp).unwrap(); system.finalize().unwrap(); let m = DEFAULT_MASS_FLOW_SEED_KG_S; let initial_state = vec![ m, p_hp, 485_000.0, p_hp, 260_000.0, p_lp, 260_000.0, p_lp, 410_000.0, ]; (system, initial_state) } /// Assembles a dense `JacobianMatrix` from the mock system at the given state. pub fn assemble_jacobian(system: &System, state: &[f64]) -> JacobianMatrix { let mut builder = JacobianBuilder::new(); system.assemble_jacobian(state, &mut builder).unwrap(); let n = system.full_state_vector_len(); let mut entries = Vec::new(); for (r, c, v) in builder.entries() { entries.push((*r, *c, *v)); } JacobianMatrix::from_builder(&entries, n, n) } // ── Reference-cycle construction (no CLI spawn) ────────────────────────────── fn make_connected_port(fluid: &str, p_pa: f64, h_j_kg: f64) -> ConnectedPort { let a = Port::new( FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), ); let b = Port::new( FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), ); a.connect(b).unwrap().0 } fn coolprop_backend() -> Arc { Arc::new(CoolPropBackend::new()) } /// Builds a real emergent-pressure R134a cycle. Three parameter sets give three /// distinct reference cycles that all converge reliably without secondary-side /// boundary components, keeping the benchmark focused on the Newton solver. fn build_emergent_cycle( cond_sec_temp_k: f64, evap_sec_temp_k: f64, ua_cond: f64, ua_evap: f64, ) -> System { use entropyk_components::isentropic_compressor::VolumetricEfficiency; let backend = coolprop_backend(); let fluid = "R134a"; let comp = Box::new( IsentropicCompressor::new(0.70, 318.15, 278.15, 5.0) .with_refrigerant(fluid) .with_fluid_backend(backend.clone()) .with_displacement(6.5e-5, 50.0, VolumetricEfficiency::Constant(0.92)), ); let cond = Box::new( Condenser::new(ua_cond) .with_refrigerant(fluid) .with_fluid_backend(backend.clone()) .with_secondary_stream(cond_sec_temp_k, 1500.0) .with_emergent_pressure(5.0), ); let exv = Box::new( IsenthalpicExpansionValve::new(278.15) .with_refrigerant(fluid) .with_fluid_backend(backend.clone()) .with_emergent_pressure(), ); let evap = Box::new( Evaporator::new(ua_evap) .with_refrigerant(fluid) .with_fluid_backend(backend.clone()) .with_secondary_stream(evap_sec_temp_k, 2000.0) .with_emergent_pressure(), ); let mut system = System::new(); let n_comp = system.add_component(comp); let n_cond = system.add_component(cond); let n_exv = system.add_component(exv); let n_evap = system.add_component(evap); system.add_edge(n_comp, n_cond).unwrap(); system.add_edge(n_cond, n_exv).unwrap(); system.add_edge(n_exv, n_evap).unwrap(); system.add_edge(n_evap, n_comp).unwrap(); system.finalize().unwrap(); system } pub fn build_reference_cycle_a() -> System { build_emergent_cycle(303.15, 285.15, 766.0, 1468.0) } pub fn build_reference_cycle_b() -> System { build_emergent_cycle(313.15, 283.15, 900.0, 1600.0) } pub fn build_reference_cycle_c() -> System { build_emergent_cycle(308.15, 291.15, 850.0, 1800.0) } /// Solves a reference cycle with the default fallback solver and a good initial seed. pub fn solve_reference_system(system: &mut System) { let initial_state = vec![ 0.05, // shared mass flow [kg/s] 11.6e5, // comp->cond pressure [Pa] 445e3, // comp->cond enthalpy [J/kg] 11.6e5, // cond->exv pressure [Pa] 262e3, // cond->exv enthalpy [J/kg] 3.5e5, // exv->evap pressure [Pa] 262e3, // exv->evap enthalpy [J/kg] 3.5e5, // evap->comp pressure [Pa] 405e3, // evap->comp enthalpy [J/kg] ]; let newton = NewtonConfig { max_iterations: 200, tolerance: 1e-6, initial_state: Some(initial_state), ..NewtonConfig::default() }; let mut solver = FallbackSolver::new(FallbackConfig::default()).with_newton_config(newton); solver.solve(system).expect("reference cycle must converge"); } // ── Criterion configuration ───────────────────────────────────────────────── /// Returns a Criterion configuration suitable for expensive end-to-end solves. /// /// Uses a small sample count and a bounded measurement window so that the /// reference-cycle benchmarks complete in a reasonable time while still /// producing stable Phase-0 baseline numbers. pub fn end_to_end_criterion() -> Criterion { Criterion::default() .sample_size(10) .measurement_time(Duration::from_secs(3)) .warm_up_time(Duration::from_secs(1)) } // ── CLI helpers (kept for optional manual verification) ────────────────────── /// Returns the path to the release `entropyk-cli` binary relative to the /// workspace root. Benchmarks are executed from `crates/solver`, so the /// workspace root is two directories up. pub fn cli_binary_path() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join("target/release/entropyk-cli.exe") .canonicalize() .unwrap_or_else(|_| { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/release/entropyk-cli") }) } /// Resolves a reference-cycle config path relative to the workspace root. pub fn reference_config_path(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join("crates/cli/examples") .join(name) } /// Runs one reference cycle through the release CLI and asserts convergence. pub fn run_cli_cycle(config_name: &str) { let binary = cli_binary_path(); let config = reference_config_path(config_name); assert!( binary.exists(), "release CLI binary not found at {}. Build it with: cargo build --release --bin entropyk-cli", binary.display() ); assert!(config.exists(), "config not found: {}", config.display()); let output = Command::new(&binary) .arg("run") .arg("--config") .arg(&config) .output() .expect("failed to execute CLI binary"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "CLI solve failed for {}\nstdout:\n{}\nstderr:\n{}", config_name, stdout, stderr ); assert!( stdout.contains("Status: CONVERGED"), "CLI did not report CONVERGED for {}\nstdout:\n{}", config_name, stdout ); }