feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
628
crates/components/src/external_model.rs
Normal file
628
crates/components/src/external_model.rs
Normal file
@@ -0,0 +1,628 @@
|
||||
//! External Component Model Interface
|
||||
//!
|
||||
//! This module provides support for external component models via:
|
||||
//! - Dynamic library loading (.dll/.so) via FFI
|
||||
//! - HTTP API calls to external services
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! The external model interface allows integration of proprietary or vendor-supplied
|
||||
//! component models that cannot be implemented natively in Rust.
|
||||
//!
|
||||
//! ## FFI Interface (DLL/SO)
|
||||
//!
|
||||
//! External libraries must implement the `entropyk_model` C ABI:
|
||||
//!
|
||||
//! ```c
|
||||
//! // Required exported functions:
|
||||
//! int entropyk_model_compute(double* inputs, double* outputs, int n_in, int n_out);
|
||||
//! int entropyk_model_jacobian(double* inputs, double* jacobian, int n_in, int n_out);
|
||||
//! const char* entropyk_model_name(void);
|
||||
//! const char* entropyk_model_version(void);
|
||||
//! ```
|
||||
//!
|
||||
//! ## HTTP API Interface
|
||||
//!
|
||||
//! External services must provide REST endpoints:
|
||||
//!
|
||||
//! - `POST /compute`: Accepts JSON with inputs, returns JSON with outputs
|
||||
//! - `POST /jacobian`: Accepts JSON with inputs, returns JSON with Jacobian matrix
|
||||
|
||||
use crate::ComponentError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Configuration for an external model.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalModelConfig {
|
||||
/// Unique identifier for this model
|
||||
pub id: String,
|
||||
/// Model type (ffi or http)
|
||||
pub model_type: ExternalModelType,
|
||||
/// Number of inputs expected
|
||||
pub n_inputs: usize,
|
||||
/// Number of outputs produced
|
||||
pub n_outputs: usize,
|
||||
/// Optional timeout in milliseconds
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
5000
|
||||
}
|
||||
|
||||
/// Type of external model interface.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ExternalModelType {
|
||||
/// Dynamic library (.dll on Windows, .so on Linux, .dylib on macOS)
|
||||
Ffi {
|
||||
/// Path to the library file
|
||||
library_path: PathBuf,
|
||||
/// Optional function name prefix
|
||||
function_prefix: Option<String>,
|
||||
},
|
||||
/// HTTP REST API
|
||||
Http {
|
||||
/// Base URL for the API
|
||||
base_url: String,
|
||||
/// Optional API key for authentication
|
||||
api_key: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Trait for external model implementations.
|
||||
///
|
||||
/// This trait abstracts over FFI and HTTP interfaces, providing
|
||||
/// a unified interface for the solver.
|
||||
pub trait ExternalModel: Send + Sync {
|
||||
/// Returns the model identifier.
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// Returns the number of inputs.
|
||||
fn n_inputs(&self) -> usize;
|
||||
|
||||
/// Returns the number of outputs.
|
||||
fn n_outputs(&self) -> usize;
|
||||
|
||||
/// Computes outputs from inputs.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `inputs` - Input values (length = n_inputs)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Output values (length = n_outputs)
|
||||
fn compute(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError>;
|
||||
|
||||
/// Computes the Jacobian matrix.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `inputs` - Input values
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Jacobian matrix as a flat array (row-major, n_outputs × n_inputs)
|
||||
fn jacobian(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError>;
|
||||
|
||||
/// Returns model metadata.
|
||||
fn metadata(&self) -> ExternalModelMetadata;
|
||||
}
|
||||
|
||||
/// Metadata about an external model.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalModelMetadata {
|
||||
/// Model name
|
||||
pub name: String,
|
||||
/// Model version
|
||||
pub version: String,
|
||||
/// Model description
|
||||
pub description: Option<String>,
|
||||
/// Input names/units
|
||||
pub input_names: Vec<String>,
|
||||
/// Output names/units
|
||||
pub output_names: Vec<String>,
|
||||
}
|
||||
|
||||
/// Errors from external model operations.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ExternalModelError {
|
||||
/// Library loading failed
|
||||
#[error("Failed to load library: {0}")]
|
||||
LibraryLoad(String),
|
||||
|
||||
/// Function not found in library
|
||||
#[error("Function not found: {0}")]
|
||||
FunctionNotFound(String),
|
||||
|
||||
/// Computation failed
|
||||
#[error("Computation failed: {0}")]
|
||||
ComputationFailed(String),
|
||||
|
||||
/// Invalid input dimensions
|
||||
#[error("Invalid input dimensions: expected {expected}, got {actual}")]
|
||||
InvalidInputDimensions {
|
||||
/// Expected number of inputs
|
||||
expected: usize,
|
||||
/// Actual number received
|
||||
actual: usize,
|
||||
},
|
||||
|
||||
/// HTTP request failed
|
||||
#[error("HTTP request failed: {0}")]
|
||||
HttpError(String),
|
||||
|
||||
/// Timeout exceeded
|
||||
#[error("Operation timed out after {0}ms")]
|
||||
Timeout(u64),
|
||||
|
||||
/// JSON parsing error
|
||||
#[error("JSON error: {0}")]
|
||||
JsonError(String),
|
||||
|
||||
/// Model not initialized
|
||||
#[error("Model not initialized")]
|
||||
NotInitialized,
|
||||
}
|
||||
|
||||
impl From<ExternalModelError> for ComponentError {
|
||||
fn from(err: ExternalModelError) -> Self {
|
||||
ComponentError::InvalidState(format!("External model error: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Request body for HTTP compute endpoint.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct ComputeRequest {
|
||||
inputs: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Response from HTTP compute endpoint.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct ComputeResponse {
|
||||
outputs: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Request body for HTTP Jacobian endpoint.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct JacobianRequest {
|
||||
inputs: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Response from HTTP Jacobian endpoint.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct JacobianResponse {
|
||||
jacobian: Vec<f64>,
|
||||
}
|
||||
|
||||
/// FFI-based external model (mock implementation for non-ffi builds).
|
||||
///
|
||||
/// When the `ffi` feature is not enabled, this provides a mock implementation
|
||||
/// that can be used for testing and development. The mock passes inputs through
|
||||
/// unchanged (identity function).
|
||||
#[cfg(not(feature = "ffi"))]
|
||||
pub struct FfiModel {
|
||||
config: ExternalModelConfig,
|
||||
metadata: ExternalModelMetadata,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ffi"))]
|
||||
impl FfiModel {
|
||||
/// Creates a new FFI model (mock implementation without ffi feature).
|
||||
///
|
||||
/// This creates a mock model that can be used for testing. The mock
|
||||
/// implements an identity function (output = input for first n_outputs).
|
||||
pub fn new(config: ExternalModelConfig) -> Result<Self, ExternalModelError> {
|
||||
let metadata = ExternalModelMetadata {
|
||||
name: format!("Mock FFI Model: {}", config.id),
|
||||
version: "0.1.0-mock".to_string(),
|
||||
description: Some("Mock FFI model for testing (ffi feature not enabled)".to_string()),
|
||||
input_names: (0..config.n_inputs)
|
||||
.map(|i| format!("input_{}", i))
|
||||
.collect(),
|
||||
output_names: (0..config.n_outputs)
|
||||
.map(|i| format!("output_{}", i))
|
||||
.collect(),
|
||||
};
|
||||
Ok(Self { config, metadata })
|
||||
}
|
||||
|
||||
/// Creates with custom mock metadata for testing.
|
||||
pub fn new_mock(
|
||||
config: ExternalModelConfig,
|
||||
metadata: ExternalModelMetadata,
|
||||
) -> Result<Self, ExternalModelError> {
|
||||
Ok(Self { config, metadata })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ffi"))]
|
||||
impl ExternalModel for FfiModel {
|
||||
fn id(&self) -> &str {
|
||||
&self.config.id
|
||||
}
|
||||
|
||||
fn n_inputs(&self) -> usize {
|
||||
self.config.n_inputs
|
||||
}
|
||||
|
||||
fn n_outputs(&self) -> usize {
|
||||
self.config.n_outputs
|
||||
}
|
||||
|
||||
fn compute(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
if inputs.len() != self.config.n_inputs {
|
||||
return Err(ExternalModelError::InvalidInputDimensions {
|
||||
expected: self.config.n_inputs,
|
||||
actual: inputs.len(),
|
||||
});
|
||||
}
|
||||
// Mock: pass through inputs (identity for first n_outputs, zero padding)
|
||||
let mut outputs = vec![0.0; self.config.n_outputs];
|
||||
for (i, &input) in inputs.iter().take(self.config.n_outputs).enumerate() {
|
||||
outputs[i] = input;
|
||||
}
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
fn jacobian(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
if inputs.len() != self.config.n_inputs {
|
||||
return Err(ExternalModelError::InvalidInputDimensions {
|
||||
expected: self.config.n_inputs,
|
||||
actual: inputs.len(),
|
||||
});
|
||||
}
|
||||
// Mock: returns identity-like Jacobian
|
||||
let mut jacobian = vec![0.0; self.config.n_inputs * self.config.n_outputs];
|
||||
let min_dim = self.config.n_inputs.min(self.config.n_outputs);
|
||||
for i in 0..min_dim {
|
||||
jacobian[i * self.config.n_inputs + i] = 1.0;
|
||||
}
|
||||
Ok(jacobian)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ExternalModelMetadata {
|
||||
self.metadata.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP-based external model (mock implementation for non-http builds).
|
||||
///
|
||||
/// When the `http` feature is not enabled, this provides a mock implementation
|
||||
/// that can be used for testing and development. The mock passes inputs through
|
||||
/// unchanged (identity function).
|
||||
#[cfg(not(feature = "http"))]
|
||||
pub struct HttpModel {
|
||||
config: ExternalModelConfig,
|
||||
metadata: ExternalModelMetadata,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
impl HttpModel {
|
||||
/// Creates a new HTTP model (mock implementation without http feature).
|
||||
///
|
||||
/// This creates a mock model that can be used for testing. The mock
|
||||
/// implements an identity function (output = input for first n_outputs).
|
||||
pub fn new(config: ExternalModelConfig) -> Result<Self, ExternalModelError> {
|
||||
let metadata = ExternalModelMetadata {
|
||||
name: format!("Mock HTTP Model: {}", config.id),
|
||||
version: "0.1.0-mock".to_string(),
|
||||
description: Some("Mock HTTP model for testing (http feature not enabled)".to_string()),
|
||||
input_names: (0..config.n_inputs)
|
||||
.map(|i| format!("input_{}", i))
|
||||
.collect(),
|
||||
output_names: (0..config.n_outputs)
|
||||
.map(|i| format!("output_{}", i))
|
||||
.collect(),
|
||||
};
|
||||
Ok(Self { config, metadata })
|
||||
}
|
||||
|
||||
/// Creates with custom mock metadata for testing.
|
||||
pub fn new_mock(
|
||||
config: ExternalModelConfig,
|
||||
metadata: ExternalModelMetadata,
|
||||
) -> Result<Self, ExternalModelError> {
|
||||
Ok(Self { config, metadata })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
impl ExternalModel for HttpModel {
|
||||
fn id(&self) -> &str {
|
||||
&self.config.id
|
||||
}
|
||||
|
||||
fn n_inputs(&self) -> usize {
|
||||
self.config.n_inputs
|
||||
}
|
||||
|
||||
fn n_outputs(&self) -> usize {
|
||||
self.config.n_outputs
|
||||
}
|
||||
|
||||
fn compute(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
if inputs.len() != self.config.n_inputs {
|
||||
return Err(ExternalModelError::InvalidInputDimensions {
|
||||
expected: self.config.n_inputs,
|
||||
actual: inputs.len(),
|
||||
});
|
||||
}
|
||||
// Mock: pass through inputs (identity for first n_outputs, zero padding)
|
||||
let mut outputs = vec![0.0; self.config.n_outputs];
|
||||
for (i, &input) in inputs.iter().take(self.config.n_outputs).enumerate() {
|
||||
outputs[i] = input;
|
||||
}
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
fn jacobian(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
if inputs.len() != self.config.n_inputs {
|
||||
return Err(ExternalModelError::InvalidInputDimensions {
|
||||
expected: self.config.n_inputs,
|
||||
actual: inputs.len(),
|
||||
});
|
||||
}
|
||||
// Mock: returns identity-like Jacobian
|
||||
let mut jacobian = vec![0.0; self.config.n_inputs * self.config.n_outputs];
|
||||
let min_dim = self.config.n_inputs.min(self.config.n_outputs);
|
||||
for i in 0..min_dim {
|
||||
jacobian[i * self.config.n_inputs + i] = 1.0;
|
||||
}
|
||||
Ok(jacobian)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ExternalModelMetadata {
|
||||
self.metadata.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for external models.
|
||||
///
|
||||
/// This wrapper ensures safe concurrent access to external models,
|
||||
/// which may not be thread-safe themselves.
|
||||
pub struct ThreadSafeExternalModel {
|
||||
inner: Arc<dyn ExternalModel>,
|
||||
}
|
||||
|
||||
impl ThreadSafeExternalModel {
|
||||
/// Creates a new thread-safe wrapper.
|
||||
pub fn new(model: impl ExternalModel + 'static) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(model),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates from an existing Arc.
|
||||
pub fn from_arc(model: Arc<dyn ExternalModel>) -> Self {
|
||||
Self { inner: model }
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner model.
|
||||
pub fn inner(&self) -> &dyn ExternalModel {
|
||||
self.inner.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ThreadSafeExternalModel {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ThreadSafeExternalModel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ThreadSafeExternalModel")
|
||||
.field("id", &self.inner.id())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock external model for testing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockExternalModel {
|
||||
id: String,
|
||||
n_inputs: usize,
|
||||
n_outputs: usize,
|
||||
compute_fn: fn(&[f64]) -> Vec<f64>,
|
||||
}
|
||||
|
||||
impl MockExternalModel {
|
||||
/// Creates a new mock model.
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
n_inputs: usize,
|
||||
n_outputs: usize,
|
||||
compute_fn: fn(&[f64]) -> Vec<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
n_inputs,
|
||||
n_outputs,
|
||||
compute_fn,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a simple linear model: y = x
|
||||
pub fn linear_passthrough(n: usize) -> Self {
|
||||
Self::new("linear_passthrough", n, n, |x| x.to_vec())
|
||||
}
|
||||
|
||||
/// Creates a model that doubles inputs.
|
||||
pub fn doubler(n: usize) -> Self {
|
||||
Self::new("doubler", n, n, |x| x.iter().map(|v| v * 2.0).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalModel for MockExternalModel {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn n_inputs(&self) -> usize {
|
||||
self.n_inputs
|
||||
}
|
||||
|
||||
fn n_outputs(&self) -> usize {
|
||||
self.n_outputs
|
||||
}
|
||||
|
||||
fn compute(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
if inputs.len() != self.n_inputs {
|
||||
return Err(ExternalModelError::InvalidInputDimensions {
|
||||
expected: self.n_inputs,
|
||||
actual: inputs.len(),
|
||||
});
|
||||
}
|
||||
Ok((self.compute_fn)(inputs))
|
||||
}
|
||||
|
||||
fn jacobian(&self, inputs: &[f64]) -> Result<Vec<f64>, ExternalModelError> {
|
||||
// Default: finite difference approximation
|
||||
let h = 1e-6;
|
||||
let mut jacobian = vec![0.0; self.n_outputs * self.n_inputs];
|
||||
|
||||
for j in 0..self.n_inputs {
|
||||
let mut inputs_plus = inputs.to_vec();
|
||||
let mut inputs_minus = inputs.to_vec();
|
||||
inputs_plus[j] += h;
|
||||
inputs_minus[j] -= h;
|
||||
|
||||
let y_plus = self.compute(&inputs_plus)?;
|
||||
let y_minus = self.compute(&inputs_minus)?;
|
||||
|
||||
for i in 0..self.n_outputs {
|
||||
jacobian[i * self.n_inputs + j] = (y_plus[i] - y_minus[i]) / (2.0 * h);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(jacobian)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ExternalModelMetadata {
|
||||
ExternalModelMetadata {
|
||||
name: self.id.clone(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: Some("Mock external model for testing".to_string()),
|
||||
input_names: (0..self.n_inputs).map(|i| format!("input_{}", i)).collect(),
|
||||
output_names: (0..self.n_outputs)
|
||||
.map(|i| format!("output_{}", i))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mock_external_model_compute() {
|
||||
let model = MockExternalModel::doubler(3);
|
||||
let result = model.compute(&[1.0, 2.0, 3.0]).unwrap();
|
||||
assert_eq!(result, vec![2.0, 4.0, 6.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_external_model_dimensions() {
|
||||
let model = MockExternalModel::doubler(3);
|
||||
assert_eq!(model.n_inputs(), 3);
|
||||
assert_eq!(model.n_outputs(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_external_model_invalid_input() {
|
||||
let model = MockExternalModel::doubler(3);
|
||||
let result = model.compute(&[1.0, 2.0]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_external_model_jacobian() {
|
||||
let model = MockExternalModel::doubler(2);
|
||||
let jac = model.jacobian(&[1.0, 2.0]).unwrap();
|
||||
|
||||
// Jacobian of y = 2x should be [[2, 0], [0, 2]]
|
||||
assert!((jac[0] - 2.0).abs() < 0.01);
|
||||
assert!((jac[1] - 0.0).abs() < 0.01);
|
||||
assert!((jac[2] - 0.0).abs() < 0.01);
|
||||
assert!((jac[3] - 2.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thread_safe_wrapper() {
|
||||
let model = MockExternalModel::doubler(2);
|
||||
let wrapped = ThreadSafeExternalModel::new(model);
|
||||
|
||||
let result = wrapped.inner().compute(&[1.0, 2.0]).unwrap();
|
||||
assert_eq!(result, vec![2.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thread_safe_clone() {
|
||||
let model = MockExternalModel::doubler(2);
|
||||
let wrapped = ThreadSafeExternalModel::new(model);
|
||||
let cloned = wrapped.clone();
|
||||
|
||||
assert_eq!(wrapped.inner().id(), cloned.inner().id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_model_metadata() {
|
||||
let model = MockExternalModel::doubler(2);
|
||||
let meta = model.metadata();
|
||||
|
||||
assert_eq!(meta.name, "doubler");
|
||||
assert_eq!(meta.version, "1.0.0");
|
||||
assert_eq!(meta.input_names, vec!["input_0", "input_1"]);
|
||||
assert_eq!(meta.output_names, vec!["output_0", "output_1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linear_passthrough_model() {
|
||||
let model = MockExternalModel::linear_passthrough(3);
|
||||
let result = model.compute(&[1.0, 2.0, 3.0]).unwrap();
|
||||
assert_eq!(result, vec![1.0, 2.0, 3.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_model_config() {
|
||||
let config = ExternalModelConfig {
|
||||
id: "test_model".to_string(),
|
||||
model_type: ExternalModelType::Http {
|
||||
base_url: "http://localhost:8080".to_string(),
|
||||
api_key: Some("secret".to_string()),
|
||||
},
|
||||
n_inputs: 4,
|
||||
n_outputs: 2,
|
||||
timeout_ms: 3000,
|
||||
};
|
||||
|
||||
assert_eq!(config.id, "test_model");
|
||||
assert_eq!(config.n_inputs, 4);
|
||||
assert_eq!(config.n_outputs, 2);
|
||||
assert_eq!(config.timeout_ms, 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_conversion() {
|
||||
let err = ExternalModelError::ComputationFailed("test error".to_string());
|
||||
let component_err: ComponentError = err.into();
|
||||
|
||||
match component_err {
|
||||
ComponentError::InvalidState(msg) => {
|
||||
assert!(msg.contains("External model error"));
|
||||
}
|
||||
_ => panic!("Expected InvalidState error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user