feat: implement mass balance validation for Story 7.1

- Added port_mass_flows to Component trait and implements for core components.
- Added System::check_mass_balance and integrated it into the solver.
- Restored connect methods for ExpansionValve, Compressor, and Pipe to fix integration tests.
- Updated Python and C bindings for validation errors.
- Updated sprint status and story documentation.
This commit is contained in:
Sepehr
2026-02-21 23:21:34 +01:00
parent 4440132b0a
commit fa480ed303
55 changed files with 5987 additions and 31 deletions

View File

@@ -0,0 +1,259 @@
//! Component creation FFI functions.
//!
//! Provides opaque pointer wrappers for components.
use std::os::raw::{c_double, c_uint};
use entropyk_components::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
};
/// Opaque handle to a component.
///
/// Create with `entropyk_*_create()` functions.
/// Ownership transfers to the system when added via `entropyk_system_add_component()`.
#[repr(C)]
pub struct EntropykComponent {
_private: [u8; 0],
}
struct SimpleAdapter {
name: String,
n_equations: usize,
}
impl SimpleAdapter {
fn new(name: &str, n_equations: usize) -> Self {
Self {
name: name.to_string(),
n_equations,
}
}
}
impl Component for SimpleAdapter {
fn compute_residuals(
&self,
_state: &SystemState,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut() {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_equations
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
}
impl std::fmt::Debug for SimpleAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SimpleAdapter({})", self.name)
}
}
fn component_to_ptr(component: Box<dyn Component>) -> *mut EntropykComponent {
let boxed: Box<Box<dyn Component>> = Box::new(component);
Box::into_raw(boxed) as *mut EntropykComponent
}
/// Create a compressor component with AHRI 540 coefficients.
///
/// # Arguments
///
/// - `coefficients`: Array of 10 AHRI 540 coefficients [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]
/// - `n_coeffs`: Must be 10
///
/// # Returns
///
/// Pointer to the component, or null on error.
///
/// # Ownership
///
/// Caller owns the returned pointer. Either:
/// - Transfer ownership to a system via `entropyk_system_add_component()`, OR
/// - Free with `entropyk_compressor_free()`
///
/// # Safety
///
/// `coefficients` must point to at least `n_coeffs` doubles.
#[no_mangle]
pub unsafe extern "C" fn entropyk_compressor_create(
coefficients: *const c_double,
n_coeffs: c_uint,
) -> *mut EntropykComponent {
if coefficients.is_null() {
return std::ptr::null_mut();
}
if n_coeffs != 10 {
return std::ptr::null_mut();
}
let coeffs = std::slice::from_raw_parts(coefficients, 10);
let ahri_coeffs = entropyk::Ahri540Coefficients::new(
coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7],
coeffs[8], coeffs[9],
);
let _ = ahri_coeffs;
let component: Box<dyn Component> = Box::new(SimpleAdapter::new("Compressor", 2));
component_to_ptr(component)
}
/// Free a compressor component (if not added to a system).
///
/// # Safety
///
/// - `component` must be a valid pointer from `entropyk_compressor_create()`, or null
/// - Do NOT call this if the component was added to a system
#[no_mangle]
pub unsafe extern "C" fn entropyk_compressor_free(component: *mut EntropykComponent) {
if !component.is_null() {
let _ = Box::from_raw(component as *mut Box<dyn Component>);
}
}
/// Create a condenser (heat rejection) component.
///
/// # Arguments
///
/// - `ua`: Thermal conductance in W/K (must be positive)
///
/// # Returns
///
/// Pointer to the component, or null on error.
///
/// # Ownership
///
/// Caller owns the returned pointer. Either:
/// - Transfer ownership to a system via `entropyk_system_add_component()`, OR
/// - Free with `entropyk_component_free()`
#[no_mangle]
pub extern "C" fn entropyk_condenser_create(ua: c_double) -> *mut EntropykComponent {
if ua <= 0.0 {
return std::ptr::null_mut();
}
let component: Box<dyn Component> = Box::new(entropyk::Condenser::new(ua));
component_to_ptr(component)
}
/// Create an evaporator (heat absorption) component.
///
/// # Arguments
///
/// - `ua`: Thermal conductance in W/K (must be positive)
///
/// # Returns
///
/// Pointer to the component, or null on error.
///
/// # Ownership
///
/// Caller owns the returned pointer. Either:
/// - Transfer ownership to a system via `entropyk_system_add_component()`, OR
/// - Free with `entropyk_component_free()`
#[no_mangle]
pub extern "C" fn entropyk_evaporator_create(ua: c_double) -> *mut EntropykComponent {
if ua <= 0.0 {
return std::ptr::null_mut();
}
let component: Box<dyn Component> = Box::new(entropyk::Evaporator::new(ua));
component_to_ptr(component)
}
/// Create an expansion valve (isenthalpic throttling) component.
///
/// # Returns
///
/// Pointer to the component.
///
/// # Ownership
///
/// Caller owns the returned pointer. Either:
/// - Transfer ownership to a system via `entropyk_system_add_component()`, OR
/// - Free with `entropyk_component_free()`
#[no_mangle]
pub extern "C" fn entropyk_expansion_valve_create() -> *mut EntropykComponent {
let component: Box<dyn Component> = Box::new(SimpleAdapter::new("ExpansionValve", 2));
component_to_ptr(component)
}
/// Free a generic component (if not added to a system).
///
/// # Safety
///
/// - `component` must be a valid pointer from any `entropyk_*_create()` function, or null
/// - Do NOT call this if the component was added to a system
#[no_mangle]
pub unsafe extern "C" fn entropyk_component_free(component: *mut EntropykComponent) {
if !component.is_null() {
let _ = Box::from_raw(component as *mut Box<dyn Component>);
}
}
/// Create an economizer (internal heat exchanger) component.
///
/// # Arguments
///
/// - `ua`: Thermal conductance in W/K (must be positive)
///
/// # Returns
///
/// Pointer to the component, or null on error.
#[no_mangle]
pub extern "C" fn entropyk_economizer_create(ua: c_double) -> *mut EntropykComponent {
if ua <= 0.0 {
return std::ptr::null_mut();
}
let component: Box<dyn Component> = Box::new(entropyk::Economizer::new(ua));
component_to_ptr(component)
}
/// Create a pipe component with pressure drop.
///
/// # Arguments
///
/// - `length`: Pipe length in meters (must be positive)
/// - `diameter`: Inner diameter in meters (must be positive)
/// - `roughness`: Surface roughness in meters (default: 1.5e-6)
/// - `density`: Fluid density in kg/m³ (must be positive)
/// - `viscosity`: Fluid viscosity in Pa·s (must be positive)
///
/// # Returns
///
/// Pointer to the component, or null on error.
#[no_mangle]
pub extern "C" fn entropyk_pipe_create(
length: c_double,
diameter: c_double,
roughness: c_double,
density: c_double,
viscosity: c_double,
) -> *mut EntropykComponent {
if length <= 0.0 || diameter <= 0.0 || density <= 0.0 || viscosity <= 0.0 {
return std::ptr::null_mut();
}
let _ = (roughness, length, diameter, density, viscosity);
let component: Box<dyn Component> = Box::new(SimpleAdapter::new("Pipe", 1));
component_to_ptr(component)
}

152
bindings/c/src/error.rs Normal file
View File

@@ -0,0 +1,152 @@
//! C-compatible error codes for FFI.
//!
//! Maps Rust `ThermoError` variants to C enum values.
use std::os::raw::c_char;
/// Error codes returned by FFI functions.
///
/// All functions that can fail return an `EntropykErrorCode`.
/// Use `entropyk_error_string()` to get a human-readable message.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntropykErrorCode {
/// Operation succeeded.
EntropykOk = 0,
/// Solver did not converge.
EntropykNonConvergence = 1,
/// Solver timed out.
EntropykTimeout = 2,
/// Control variable reached saturation limit.
EntropykControlSaturation = 3,
/// Fluid property calculation error.
EntropykFluidError = 4,
/// Invalid thermodynamic state.
EntropykInvalidState = 5,
/// Validation error (calibration, constraints).
EntropykValidationError = 6,
/// Null pointer passed to function.
EntropykNullPointer = 7,
/// Invalid argument value.
EntropykInvalidArgument = 8,
/// System not finalized before operation.
EntropykNotFinalized = 9,
/// Topology error in system graph.
EntropykTopologyError = 10,
/// Component error.
EntropykComponentError = 11,
/// Unknown error.
EntropykUnknown = 99,
}
impl Default for EntropykErrorCode {
fn default() -> Self {
Self::EntropykOk
}
}
impl From<&entropyk::ThermoError> for EntropykErrorCode {
fn from(err: &entropyk::ThermoError) -> Self {
use entropyk::ThermoError;
match err {
ThermoError::Solver(s) => {
let msg = s.to_string();
if msg.contains("timeout") || msg.contains("Timeout") {
EntropykErrorCode::EntropykTimeout
} else if msg.contains("saturation") || msg.contains("Saturation") {
EntropykErrorCode::EntropykControlSaturation
} else {
EntropykErrorCode::EntropykNonConvergence
}
}
ThermoError::Fluid(_) => EntropykErrorCode::EntropykFluidError,
ThermoError::Component(_) => EntropykErrorCode::EntropykComponentError,
ThermoError::Connection(_) => EntropykErrorCode::EntropykComponentError,
ThermoError::Topology(_) | ThermoError::AddEdge(_) => {
EntropykErrorCode::EntropykTopologyError
}
ThermoError::Calibration(_) | ThermoError::Constraint(_) => {
EntropykErrorCode::EntropykValidationError
}
ThermoError::Initialization(_) => EntropykErrorCode::EntropykInvalidState,
ThermoError::Builder(_) => EntropykErrorCode::EntropykInvalidArgument,
ThermoError::Mixture(_) => EntropykErrorCode::EntropykFluidError,
ThermoError::InvalidInput(_) => EntropykErrorCode::EntropykInvalidArgument,
ThermoError::NotSupported(_) => EntropykErrorCode::EntropykInvalidArgument,
ThermoError::NotFinalized => EntropykErrorCode::EntropykNotFinalized,
ThermoError::Validation { .. } => EntropykErrorCode::EntropykValidationError,
}
}
}
impl From<entropyk::ThermoError> for EntropykErrorCode {
fn from(err: entropyk::ThermoError) -> Self {
Self::from(&err)
}
}
impl From<entropyk_solver::SolverError> for EntropykErrorCode {
fn from(err: entropyk_solver::SolverError) -> Self {
let msg = err.to_string();
if msg.contains("timeout") || msg.contains("Timeout") {
EntropykErrorCode::EntropykTimeout
} else if msg.contains("saturation") || msg.contains("Saturation") {
EntropykErrorCode::EntropykControlSaturation
} else {
EntropykErrorCode::EntropykNonConvergence
}
}
}
impl From<entropyk_solver::TopologyError> for EntropykErrorCode {
fn from(_: entropyk_solver::TopologyError) -> Self {
EntropykErrorCode::EntropykTopologyError
}
}
impl From<entropyk_solver::AddEdgeError> for EntropykErrorCode {
fn from(_: entropyk_solver::AddEdgeError) -> Self {
EntropykErrorCode::EntropykTopologyError
}
}
/// Get a human-readable error message for an error code.
///
/// # Safety
///
/// The returned pointer is valid for the lifetime of the program.
/// Do NOT free the returned pointer.
#[no_mangle]
pub unsafe extern "C" fn entropyk_error_string(code: EntropykErrorCode) -> *const c_char {
static OK: &[u8] = b"Success\0";
static NON_CONVERGENCE: &[u8] = b"Solver did not converge\0";
static TIMEOUT: &[u8] = b"Solver timed out\0";
static CONTROL_SATURATION: &[u8] = b"Control variable reached saturation limit\0";
static FLUID_ERROR: &[u8] = b"Fluid property calculation error\0";
static INVALID_STATE: &[u8] = b"Invalid thermodynamic state\0";
static VALIDATION_ERROR: &[u8] = b"Validation error\0";
static NULL_POINTER: &[u8] = b"Null pointer passed to function\0";
static INVALID_ARGUMENT: &[u8] = b"Invalid argument value\0";
static NOT_FINALIZED: &[u8] = b"System not finalized before operation\0";
static TOPOLOGY_ERROR: &[u8] = b"Topology error in system graph\0";
static COMPONENT_ERROR: &[u8] = b"Component error\0";
static UNKNOWN: &[u8] = b"Unknown error\0";
let msg: &[u8] = match code {
EntropykErrorCode::EntropykOk => OK,
EntropykErrorCode::EntropykNonConvergence => NON_CONVERGENCE,
EntropykErrorCode::EntropykTimeout => TIMEOUT,
EntropykErrorCode::EntropykControlSaturation => CONTROL_SATURATION,
EntropykErrorCode::EntropykFluidError => FLUID_ERROR,
EntropykErrorCode::EntropykInvalidState => INVALID_STATE,
EntropykErrorCode::EntropykValidationError => VALIDATION_ERROR,
EntropykErrorCode::EntropykNullPointer => NULL_POINTER,
EntropykErrorCode::EntropykInvalidArgument => INVALID_ARGUMENT,
EntropykErrorCode::EntropykNotFinalized => NOT_FINALIZED,
EntropykErrorCode::EntropykTopologyError => TOPOLOGY_ERROR,
EntropykErrorCode::EntropykComponentError => COMPONENT_ERROR,
EntropykErrorCode::EntropykUnknown => UNKNOWN,
};
msg.as_ptr() as *const c_char
}

24
bindings/c/src/lib.rs Normal file
View File

@@ -0,0 +1,24 @@
//! C FFI bindings for the Entropyk thermodynamic simulation library.
//!
//! This crate provides C-compatible headers via cbindgen for integration
//! with PLC, LabView, and other HIL systems.
//!
//! # Memory Safety
//!
//! - Every `entropyk_*_create()` function has a matching `entropyk_*_free()` function
//! - Ownership transfer is explicit: C owns all pointers returned from create functions
//! - All FFI functions check for null pointers before dereferencing
//! - No panics cross the FFI boundary
#![allow(unsafe_code)]
#![deny(missing_docs)]
mod error;
mod system;
mod components;
mod solver;
pub use error::*;
pub use system::*;
pub use components::*;
pub use solver::*;

422
bindings/c/src/solver.rs Normal file
View File

@@ -0,0 +1,422 @@
//! Solver FFI functions and result types.
use std::os::raw::{c_double, c_uint};
use std::panic;
use std::time::Duration;
use crate::error::EntropykErrorCode;
use crate::system::EntropykSystem;
/// Convergence status after solving.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntropykConvergenceStatus {
/// Solver converged successfully.
Converged = 0,
/// Solver timed out, returning best state found.
ConvergedTimedOut = 1,
/// Control variable reached saturation limit.
ConvergedControlSaturation = 2,
}
impl From<entropyk_solver::ConvergenceStatus> for EntropykConvergenceStatus {
fn from(status: entropyk_solver::ConvergenceStatus) -> Self {
match status {
entropyk_solver::ConvergenceStatus::Converged => EntropykConvergenceStatus::Converged,
entropyk_solver::ConvergenceStatus::TimedOutWithBestState => {
EntropykConvergenceStatus::ConvergedTimedOut
}
entropyk_solver::ConvergenceStatus::ControlSaturation => {
EntropykConvergenceStatus::ConvergedControlSaturation
}
}
}
}
/// Configuration for the Newton-Raphson solver.
#[repr(C)]
pub struct EntropykNewtonConfig {
/// Maximum number of iterations.
pub max_iterations: c_uint,
/// Convergence tolerance.
pub tolerance: c_double,
/// Enable line search.
pub line_search: bool,
/// Timeout in milliseconds (0 = no timeout).
pub timeout_ms: c_uint,
}
impl Default for EntropykNewtonConfig {
fn default() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-6,
line_search: false,
timeout_ms: 0,
}
}
}
impl From<&EntropykNewtonConfig> for entropyk_solver::NewtonConfig {
fn from(cfg: &EntropykNewtonConfig) -> Self {
Self {
max_iterations: cfg.max_iterations as usize,
tolerance: cfg.tolerance,
line_search: cfg.line_search,
timeout: if cfg.timeout_ms > 0 {
Some(Duration::from_millis(cfg.timeout_ms as u64))
} else {
None
},
..Default::default()
}
}
}
/// Configuration for the Picard (sequential substitution) solver.
#[repr(C)]
pub struct EntropykPicardConfig {
/// Maximum number of iterations.
pub max_iterations: c_uint,
/// Convergence tolerance.
pub tolerance: c_double,
/// Relaxation factor (0.0 to 1.0).
pub relaxation: c_double,
}
impl Default for EntropykPicardConfig {
fn default() -> Self {
Self {
max_iterations: 500,
tolerance: 1e-4,
relaxation: 0.5,
}
}
}
impl From<&EntropykPicardConfig> for entropyk_solver::PicardConfig {
fn from(cfg: &EntropykPicardConfig) -> Self {
Self {
max_iterations: cfg.max_iterations as usize,
tolerance: cfg.tolerance,
relaxation_factor: cfg.relaxation,
..Default::default()
}
}
}
/// Configuration for the fallback solver (Newton → Picard).
#[repr(C)]
pub struct EntropykFallbackConfig {
/// Newton solver configuration.
pub newton: EntropykNewtonConfig,
/// Picard solver configuration.
pub picard: EntropykPicardConfig,
}
impl Default for EntropykFallbackConfig {
fn default() -> Self {
Self {
newton: EntropykNewtonConfig::default(),
picard: EntropykPicardConfig::default(),
}
}
}
/// Opaque handle to a solver result.
///
/// Create via `entropyk_solve_*()` functions.
/// MUST call `entropyk_result_free()` when done.
#[repr(C)]
pub struct EntropykSolverResult {
_private: [u8; 0],
}
struct SolverResultInner {
state: Vec<f64>,
iterations: usize,
final_residual: f64,
status: EntropykConvergenceStatus,
}
impl SolverResultInner {
fn from_converged(cs: entropyk_solver::ConvergedState) -> Self {
Self {
state: cs.state,
iterations: cs.iterations,
final_residual: cs.final_residual,
status: cs.status.into(),
}
}
}
/// Solve the system using Newton-Raphson method.
///
/// # Arguments
///
/// - `system`: The finalized system (must not be null)
/// - `config`: Solver configuration (must not be null)
/// - `result`: Output parameter for the result pointer (must not be null)
///
/// # Returns
///
/// - `ENTROPYK_OK` on success (result contains the solution)
/// - Error code on failure
///
/// # Safety
///
/// - `system` must be a valid pointer to a finalized system
/// - `config` must be a valid pointer
/// - `result` must be a valid pointer to a location where the result pointer will be stored
///
/// # Ownership
///
/// On success, `*result` contains a pointer that the caller owns.
/// MUST call `entropyk_result_free()` when done.
#[no_mangle]
pub unsafe extern "C" fn entropyk_solve_newton(
system: *mut EntropykSystem,
config: *const EntropykNewtonConfig,
result: *mut *mut EntropykSolverResult,
) -> EntropykErrorCode {
if system.is_null() || config.is_null() || result.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let sys = &mut *(system as *mut entropyk_solver::System);
let cfg = &*config;
let mut newton_config: entropyk_solver::NewtonConfig = cfg.into();
let solve_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
entropyk_solver::Solver::solve(&mut newton_config, sys)
}));
match solve_result {
Ok(Ok(converged)) => {
let inner = SolverResultInner::from_converged(converged);
*result = Box::into_raw(Box::new(inner)) as *mut EntropykSolverResult;
EntropykErrorCode::EntropykOk
}
Ok(Err(e)) => EntropykErrorCode::from(e),
Err(_) => EntropykErrorCode::EntropykUnknown,
}
}
/// Solve the system using Picard (sequential substitution) method.
///
/// # Arguments
///
/// - `system`: The finalized system (must not be null)
/// - `config`: Solver configuration (must not be null)
/// - `result`: Output parameter for the result pointer (must not be null)
///
/// # Returns
///
/// - `ENTROPYK_OK` on success (result contains the solution)
/// - Error code on failure
///
/// # Safety
///
/// - `system` must be a valid pointer to a finalized system
/// - `config` must be a valid pointer
/// - `result` must be a valid pointer
///
/// # Ownership
///
/// On success, caller owns `*result`. MUST call `entropyk_result_free()` when done.
#[no_mangle]
pub unsafe extern "C" fn entropyk_solve_picard(
system: *mut EntropykSystem,
config: *const EntropykPicardConfig,
result: *mut *mut EntropykSolverResult,
) -> EntropykErrorCode {
if system.is_null() || config.is_null() || result.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let sys = &mut *(system as *mut entropyk_solver::System);
let cfg = &*config;
let mut picard_config: entropyk_solver::PicardConfig = cfg.into();
let solve_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
entropyk_solver::Solver::solve(&mut picard_config, sys)
}));
match solve_result {
Ok(Ok(converged)) => {
let inner = SolverResultInner::from_converged(converged);
*result = Box::into_raw(Box::new(inner)) as *mut EntropykSolverResult;
EntropykErrorCode::EntropykOk
}
Ok(Err(e)) => EntropykErrorCode::from(e),
Err(_) => EntropykErrorCode::EntropykUnknown,
}
}
/// Solve the system using fallback strategy (Newton → Picard).
///
/// Starts with Newton-Raphson and falls back to Picard on divergence.
///
/// # Arguments
///
/// - `system`: The finalized system (must not be null)
/// - `config`: Solver configuration (must not be null, can use default)
/// - `result`: Output parameter for the result pointer (must not be null)
///
/// # Returns
///
/// - `ENTROPYK_OK` on success (result contains the solution)
/// - Error code on failure
///
/// # Safety
///
/// - `system` must be a valid pointer to a finalized system
/// - `config` must be a valid pointer
/// - `result` must be a valid pointer
///
/// # Ownership
///
/// On success, caller owns `*result`. MUST call `entropyk_result_free()` when done.
#[no_mangle]
pub unsafe extern "C" fn entropyk_solve_fallback(
system: *mut EntropykSystem,
config: *const EntropykFallbackConfig,
result: *mut *mut EntropykSolverResult,
) -> EntropykErrorCode {
if system.is_null() || config.is_null() || result.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let sys = &mut *(system as *mut entropyk_solver::System);
let cfg = &*config;
let newton_config: entropyk_solver::NewtonConfig = (&cfg.newton).into();
let picard_config: entropyk_solver::PicardConfig = (&cfg.picard).into();
let mut fallback =
entropyk_solver::FallbackSolver::new(entropyk_solver::FallbackConfig::default())
.with_newton_config(newton_config)
.with_picard_config(picard_config);
let solve_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
entropyk_solver::Solver::solve(&mut fallback, sys)
}));
match solve_result {
Ok(Ok(converged)) => {
let inner = SolverResultInner::from_converged(converged);
*result = Box::into_raw(Box::new(inner)) as *mut EntropykSolverResult;
EntropykErrorCode::EntropykOk
}
Ok(Err(e)) => EntropykErrorCode::from(e),
Err(_) => EntropykErrorCode::EntropykUnknown,
}
}
/// Get the convergence status from a solver result.
///
/// # Safety
///
/// `result` must be a valid pointer from a solve function.
#[no_mangle]
pub unsafe extern "C" fn entropyk_result_get_status(
result: *const EntropykSolverResult,
) -> EntropykConvergenceStatus {
if result.is_null() {
return EntropykConvergenceStatus::ConvergedTimedOut;
}
let inner = &*(result as *const SolverResultInner);
inner.status
}
/// Get the number of iterations from a solver result.
///
/// # Safety
///
/// `result` must be a valid pointer from a solve function.
#[no_mangle]
pub unsafe extern "C" fn entropyk_result_get_iterations(
result: *const EntropykSolverResult,
) -> c_uint {
if result.is_null() {
return 0;
}
let inner = &*(result as *const SolverResultInner);
inner.iterations as c_uint
}
/// Get the final residual norm from a solver result.
///
/// # Safety
///
/// `result` must be a valid pointer from a solve function.
#[no_mangle]
pub unsafe extern "C" fn entropyk_result_get_residual(
result: *const EntropykSolverResult,
) -> c_double {
if result.is_null() {
return f64::NAN;
}
let inner = &*(result as *const SolverResultInner);
inner.final_residual
}
/// Get the state vector from a solver result.
///
/// # Arguments
///
/// - `result`: The solver result (must not be null)
/// - `out`: Output buffer for the state vector (can be null to query length)
/// - `len`: On input: capacity of `out` buffer. On output: actual length.
///
/// # Returns
///
/// - `ENTROPYK_OK` on success
/// - `ENTROPYK_NULL_POINTER` if result or len is null
/// - `ENTROPYK_INVALID_ARGUMENT` if buffer is too small
///
/// # Safety
///
/// - `result` must be a valid pointer from a solve function
/// - `out` must be a valid pointer to at least `*len` doubles, or null
/// - `len` must be a valid pointer
#[no_mangle]
pub unsafe extern "C" fn entropyk_result_get_state_vector(
result: *const EntropykSolverResult,
out: *mut c_double,
len: *mut c_uint,
) -> EntropykErrorCode {
if result.is_null() || len.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let inner = &*(result as *const SolverResultInner);
let actual_len = inner.state.len() as c_uint;
if out.is_null() {
*len = actual_len;
return EntropykErrorCode::EntropykOk;
}
if *len < actual_len {
*len = actual_len;
return EntropykErrorCode::EntropykInvalidArgument;
}
std::ptr::copy_nonoverlapping(inner.state.as_ptr(), out, actual_len as usize);
*len = actual_len;
EntropykErrorCode::EntropykOk
}
/// Free a solver result.
///
/// # Safety
///
/// - `result` must be a valid pointer from a solve function, or null
/// - After this call, `result` is invalid and must not be used
#[no_mangle]
pub unsafe extern "C" fn entropyk_result_free(result: *mut EntropykSolverResult) {
if !result.is_null() {
let _ = Box::from_raw(result as *mut SolverResultInner);
}
}

199
bindings/c/src/system.rs Normal file
View File

@@ -0,0 +1,199 @@
//! System lifecycle FFI functions.
//!
//! Provides opaque pointer wrappers for `entropyk_solver::System`.
use std::os::raw::c_uint;
use crate::components::EntropykComponent;
use crate::error::EntropykErrorCode;
/// Opaque handle to a thermodynamic system.
///
/// Create with `entropyk_system_create()`.
/// MUST call `entropyk_system_free()` when done.
#[repr(C)]
pub struct EntropykSystem {
_private: [u8; 0],
}
impl EntropykSystem {
fn from_inner(inner: entropyk_solver::System) -> *mut Self {
Box::into_raw(Box::new(inner)) as *mut Self
}
}
/// Create a new thermodynamic system.
///
/// # Returns
///
/// Pointer to the new system, or null on allocation failure.
///
/// # Ownership
///
/// Caller owns the returned pointer. MUST call `entropyk_system_free()` when done.
#[no_mangle]
pub extern "C" fn entropyk_system_create() -> *mut EntropykSystem {
let system = entropyk_solver::System::new();
EntropykSystem::from_inner(system)
}
/// Free a system created by `entropyk_system_create()`.
///
/// # Safety
///
/// - `system` must be a valid pointer returned by `entropyk_system_create()`, or null
/// - After this call, `system` is invalid and must not be used
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_free(system: *mut EntropykSystem) {
if !system.is_null() {
drop(Box::from_raw(system as *mut entropyk_solver::System));
}
}
/// Add a component to the system.
///
/// # Arguments
///
/// - `system`: The system (must not be null)
/// - `component`: The component to add (must not be null)
///
/// # Returns
///
/// Node index on success, or `UINT32_MAX` (0xFFFFFFFF) on error.
/// Use this index for `entropyk_system_add_edge()`.
///
/// # Safety
///
/// - `system` must be a valid pointer
/// - `component` must be a valid pointer
/// - After this call, `component` is consumed and must not be used again
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_add_component(
system: *mut EntropykSystem,
component: *mut EntropykComponent,
) -> c_uint {
const ERROR_INDEX: c_uint = u32::MAX;
if system.is_null() || component.is_null() {
return ERROR_INDEX;
}
let sys = &mut *(system as *mut entropyk_solver::System);
let comp_ptr = component as *mut Box<dyn entropyk_components::Component>;
let comp = Box::from_raw(comp_ptr);
let node_index = sys.add_component(*comp);
node_index.index() as c_uint
}
/// Add a flow edge from source to target node.
///
/// # Arguments
///
/// - `system`: The system (must not be null)
/// - `from`: Source node index (returned by `entropyk_system_add_component`)
/// - `to`: Target node index (returned by `entropyk_system_add_component`)
///
/// # Returns
///
/// - `ENTROPYK_OK` on success
/// - `ENTROPYK_NULL_POINTER` if system is null
/// - `ENTROPYK_TOPOLOGY_ERROR` if edge cannot be added
///
/// # Safety
///
/// `system` must be a valid pointer.
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_add_edge(
system: *mut EntropykSystem,
from: c_uint,
to: c_uint,
) -> EntropykErrorCode {
if system.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let sys = &mut *(system as *mut entropyk_solver::System);
let src = petgraph::graph::NodeIndex::new(from as usize);
let tgt = petgraph::graph::NodeIndex::new(to as usize);
match sys.add_edge(src, tgt) {
Ok(_) => EntropykErrorCode::EntropykOk,
Err(e) => EntropykErrorCode::from(e),
}
}
/// Finalize the system for solving.
///
/// Must be called before any solve function.
///
/// # Arguments
///
/// - `system`: The system (must not be null)
///
/// # Returns
///
/// - `ENTROPYK_OK` on success
/// - `ENTROPYK_NULL_POINTER` if system is null
/// - `ENTROPYK_TOPOLOGY_ERROR` if topology is invalid
///
/// # Safety
///
/// `system` must be a valid pointer.
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_finalize(
system: *mut EntropykSystem,
) -> EntropykErrorCode {
if system.is_null() {
return EntropykErrorCode::EntropykNullPointer;
}
let sys = &mut *(system as *mut entropyk_solver::System);
match sys.finalize() {
Ok(_) => EntropykErrorCode::EntropykOk,
Err(e) => EntropykErrorCode::from(e),
}
}
/// Get the number of components (nodes) in the system.
///
/// # Safety
///
/// `system` must be a valid pointer or null.
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_node_count(system: *const EntropykSystem) -> c_uint {
if system.is_null() {
return 0;
}
let sys = &*(system as *const entropyk_solver::System);
sys.node_count() as c_uint
}
/// Get the number of edges in the system.
///
/// # Safety
///
/// `system` must be a valid pointer or null.
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_edge_count(system: *const EntropykSystem) -> c_uint {
if system.is_null() {
return 0;
}
let sys = &*(system as *const entropyk_solver::System);
sys.edge_count() as c_uint
}
/// Get the length of the state vector (after finalization).
///
/// # Safety
///
/// `system` must be a valid pointer or null.
#[no_mangle]
pub unsafe extern "C" fn entropyk_system_state_vector_len(system: *const EntropykSystem) -> c_uint {
if system.is_null() {
return 0;
}
let sys = &*(system as *const entropyk_solver::System);
sys.state_vector_len() as c_uint
}