Update project structure and configurations
This commit is contained in:
@@ -377,15 +377,15 @@ impl System {
|
||||
let state_idx = self.total_state_len + index;
|
||||
|
||||
let id_str = id.as_str();
|
||||
if id_str.ends_with("f_m") || id_str == "f_m" {
|
||||
if id_str.ends_with("f_m") {
|
||||
indices.f_m = Some(state_idx);
|
||||
} else if id_str.ends_with("f_dp") || id_str == "f_dp" {
|
||||
} else if id_str.ends_with("f_dp") {
|
||||
indices.f_dp = Some(state_idx);
|
||||
} else if id_str.ends_with("f_ua") || id_str == "f_ua" {
|
||||
} else if id_str.ends_with("f_ua") {
|
||||
indices.f_ua = Some(state_idx);
|
||||
} else if id_str.ends_with("f_power") || id_str == "f_power" {
|
||||
} else if id_str.ends_with("f_power") {
|
||||
indices.f_power = Some(state_idx);
|
||||
} else if id_str.ends_with("f_etav") || id_str == "f_etav" {
|
||||
} else if id_str.ends_with("f_etav") {
|
||||
indices.f_etav = Some(state_idx);
|
||||
}
|
||||
}
|
||||
@@ -544,6 +544,33 @@ impl System {
|
||||
self.graph.edge_indices()
|
||||
}
|
||||
|
||||
/// Returns the source and target node indices for the given edge.
|
||||
///
|
||||
/// Returns `None` if the edge index is invalid.
|
||||
pub fn edge_endpoints(&self, edge: EdgeIndex) -> Option<(NodeIndex, NodeIndex)> {
|
||||
self.graph.edge_endpoints(edge)
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal graph.
|
||||
pub fn graph(&self) -> &Graph<Box<dyn Component>, FlowEdge, Directed> {
|
||||
&self.graph
|
||||
}
|
||||
|
||||
/// Returns a reference to the node-to-circuit mapping.
|
||||
pub fn node_to_circuit(&self) -> &HashMap<NodeIndex, CircuitId> {
|
||||
&self.node_to_circuit
|
||||
}
|
||||
|
||||
/// Returns a reference to the constraints map.
|
||||
pub fn constraints_map(&self) -> &HashMap<ConstraintId, Constraint> {
|
||||
&self.constraints
|
||||
}
|
||||
|
||||
/// Returns a reference to the bounded variables map.
|
||||
pub fn bounded_variables_map(&self) -> &HashMap<BoundedVariableId, BoundedVariable> {
|
||||
&self.bounded_variables
|
||||
}
|
||||
|
||||
/// Returns the number of nodes (components) in the graph.
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.graph.node_count()
|
||||
@@ -732,6 +759,15 @@ impl System {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the component at the given node index.
|
||||
///
|
||||
/// Returns `None` if the node index is invalid.
|
||||
/// Used for post-build injection of fluid backends via the builder.
|
||||
pub fn component_mut(&mut self, node: NodeIndex) -> Option<&mut dyn Component> {
|
||||
let weight = self.graph.node_weight_mut(node)?;
|
||||
Some(weight.as_mut())
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Constraint Management (Inverse Control)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
@@ -795,6 +831,7 @@ impl System {
|
||||
///
|
||||
/// The removed constraint, or `None` if no constraint with that ID exists.
|
||||
pub fn remove_constraint(&mut self, id: &ConstraintId) -> Option<Constraint> {
|
||||
self.inverse_control.unlink_constraint(id);
|
||||
self.constraints.remove(id)
|
||||
}
|
||||
|
||||
@@ -836,7 +873,13 @@ impl System {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of constraint residuals added.
|
||||
/// `Ok(count)` where count is the number of constraint residuals added.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ConstraintError::UnmeasuredConstraint` if a constraint references a component
|
||||
/// with no measured value (not registered or no associated edges).
|
||||
/// Returns `ConstraintError::ResidualSliceTooShort` if the residual slice is too short.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -850,30 +893,34 @@ impl System {
|
||||
_state: &StateSlice,
|
||||
residuals: &mut [f64],
|
||||
measured_values: &HashMap<ConstraintId, f64>,
|
||||
) -> usize {
|
||||
) -> Result<usize, ConstraintError> {
|
||||
if self.constraints.is_empty() {
|
||||
return 0;
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
for constraint in self.constraints.values() {
|
||||
let measured = measured_values
|
||||
.get(constraint.id())
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
constraint_id = constraint.id().as_str(),
|
||||
"No measured value for constraint, using zero residual"
|
||||
);
|
||||
constraint.target_value()
|
||||
});
|
||||
let measured = match measured_values.get(constraint.id()).copied() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(ConstraintError::UnmeasuredConstraint {
|
||||
constraint_id: constraint.id().to_string(),
|
||||
component_id: constraint.output().component_id().to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let residual = constraint.compute_residual(measured);
|
||||
if count < residuals.len() {
|
||||
residuals[count] = residual;
|
||||
if count >= residuals.len() {
|
||||
return Err(ConstraintError::ResidualSliceTooShort {
|
||||
index: count,
|
||||
len: residuals.len(),
|
||||
required: self.constraints.len(),
|
||||
});
|
||||
}
|
||||
residuals[count] = residual;
|
||||
count += 1;
|
||||
}
|
||||
count
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Extracts measured values for all constraints, incorporating control variable effects.
|
||||
@@ -1003,7 +1050,15 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
measured.insert(constraint.id().clone(), value);
|
||||
if value.is_nan() {
|
||||
tracing::warn!(
|
||||
constraint_id = constraint.id().as_str(),
|
||||
"NaN detected in constraint output for component '{}', skipping insert",
|
||||
constraint.output().component_id()
|
||||
);
|
||||
} else {
|
||||
measured.insert(constraint.id().clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1048,8 +1103,25 @@ impl System {
|
||||
return entries;
|
||||
}
|
||||
|
||||
if control_values.len() < self.inverse_control.mapping_count() {
|
||||
tracing::error!(
|
||||
provided = control_values.len(),
|
||||
required = self.inverse_control.mapping_count(),
|
||||
"control_values too short for Jacobian computation"
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Use configurable epsilon from InverseControlConfig
|
||||
let eps = self.inverse_control.finite_diff_epsilon();
|
||||
if state.len() < self.total_state_len {
|
||||
tracing::error!(
|
||||
state_len = state.len(),
|
||||
required = self.total_state_len,
|
||||
"compute_inverse_control_jacobian: state slice too short, returning empty"
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
let mut state_mut = state.to_vec();
|
||||
let mut control_mut = control_values.to_vec();
|
||||
|
||||
@@ -1232,6 +1304,7 @@ impl System {
|
||||
///
|
||||
/// The removed variable, or `None` if no variable with that ID exists.
|
||||
pub fn remove_bounded_variable(&mut self, id: &BoundedVariableId) -> Option<BoundedVariable> {
|
||||
self.inverse_control.unlink_control(id);
|
||||
self.bounded_variables.remove(id)
|
||||
}
|
||||
|
||||
@@ -1272,6 +1345,13 @@ impl System {
|
||||
// Inverse Control Mapping (Story 5.3)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Removes all constraints, bounded variables, and inverse control mappings.
|
||||
pub fn clear_inverse_control(&mut self) {
|
||||
self.constraints.clear();
|
||||
self.bounded_variables.clear();
|
||||
self.inverse_control.clear();
|
||||
}
|
||||
|
||||
/// Links a constraint to a bounded control variable for One-Shot inverse control.
|
||||
///
|
||||
/// When a constraint is linked to a control variable, the solver adjusts both
|
||||
@@ -1371,11 +1451,11 @@ impl System {
|
||||
|
||||
/// Sets the finite difference epsilon for inverse control Jacobian computation.
|
||||
///
|
||||
/// # Panics
|
||||
/// # Errors
|
||||
///
|
||||
/// Panics if epsilon is non-positive.
|
||||
pub fn set_inverse_control_epsilon(&mut self, epsilon: f64) {
|
||||
self.inverse_control.set_finite_diff_epsilon(epsilon);
|
||||
/// Returns `ConstraintError::InvalidEpsilon` if epsilon is not a finite positive value in (0, 1].
|
||||
pub fn set_inverse_control_epsilon(&mut self, epsilon: f64) -> Result<(), ConstraintError> {
|
||||
self.inverse_control.set_finite_diff_epsilon(epsilon)
|
||||
}
|
||||
|
||||
/// Returns the current finite difference epsilon for inverse control.
|
||||
@@ -1698,7 +1778,8 @@ impl System {
|
||||
.collect();
|
||||
let measured = self.extract_constraint_values_with_controls(state, &control_values);
|
||||
let n_constraints =
|
||||
self.compute_constraint_residuals(state, &mut residuals[eq_offset..], &measured);
|
||||
self.compute_constraint_residuals(state, &mut residuals[eq_offset..], &measured)
|
||||
.map_err(|e| ComponentError::CalculationFailed(e.to_string()))?;
|
||||
eq_offset += n_constraints;
|
||||
|
||||
// Add couplings
|
||||
@@ -2024,50 +2105,175 @@ impl System {
|
||||
/// ```
|
||||
pub fn to_json_string(&self) -> Result<String, crate::error::ThermoError> {
|
||||
use crate::snapshot::{
|
||||
FluidBackendInfo, SolverConfigSnapshot, SystemSnapshot, TopologySnapshot,
|
||||
BoundedVariableSnapshot, ConstraintSnapshot, EdgeSnapshot, FluidBackendInfo,
|
||||
SolverConfigSnapshot, SystemSnapshot, TopologySnapshot,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
tracing::info!("Serializing system to JSON");
|
||||
|
||||
// Extract topology
|
||||
let reverse_names: HashMap<NodeIndex, &String> =
|
||||
self.component_names.iter().map(|(n, &i)| (i, n)).collect();
|
||||
|
||||
// Extract topology with port names
|
||||
let mut edges = Vec::new();
|
||||
for edge in self.graph.edge_indices() {
|
||||
let (source, target) = self.graph.edge_endpoints(edge).unwrap();
|
||||
let source_node = self.graph.node_weight(source).unwrap();
|
||||
let target_node = self.graph.node_weight(target).unwrap();
|
||||
|
||||
edges.push(serde_json::json!({
|
||||
"source": source_node.signature(),
|
||||
"target": target_node.signature(),
|
||||
"circuit_id": self.edge_circuit(edge).0,
|
||||
}));
|
||||
// Derive port names from component port_names() or defaults
|
||||
let source_ports = source_node.port_names();
|
||||
let target_ports = target_node.port_names();
|
||||
|
||||
// Count how many edges connect TO the target (this edge's index at target)
|
||||
let target_incoming: Vec<_> = self
|
||||
.graph
|
||||
.edges_directed(target, petgraph::Direction::Incoming)
|
||||
.collect();
|
||||
let target_port_idx = target_incoming
|
||||
.iter()
|
||||
.position(|e| e.id() == edge)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Count how many edges leave FROM the source (this edge's index at source)
|
||||
let source_outgoing: Vec<_> = self
|
||||
.graph
|
||||
.edges_directed(source, petgraph::Direction::Outgoing)
|
||||
.collect();
|
||||
let source_port_idx = source_outgoing
|
||||
.iter()
|
||||
.position(|e| e.id() == edge)
|
||||
.unwrap_or(0);
|
||||
|
||||
let source_port_name = source_ports
|
||||
.get(source_port_idx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("port_{}", source_port_idx));
|
||||
let target_port_name = target_ports
|
||||
.get(target_port_idx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("port_{}", target_port_idx));
|
||||
|
||||
edges.push(EdgeSnapshot {
|
||||
source: reverse_names
|
||||
.get(&source)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| source_node.signature()),
|
||||
source_port: source_port_name,
|
||||
target: reverse_names
|
||||
.get(&target)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| target_node.signature()),
|
||||
target_port: target_port_name,
|
||||
circuit_id: self.edge_circuit(edge).0,
|
||||
});
|
||||
}
|
||||
|
||||
// Extract component parameters
|
||||
// Extract component parameters (use unique key: registered name or signature+index)
|
||||
let mut parameters = HashMap::new();
|
||||
for node in self.graph.node_indices() {
|
||||
if let Some(component) = self.graph.node_weight(node) {
|
||||
let params = component.to_params();
|
||||
parameters.insert(component.signature(), params);
|
||||
let key = reverse_names
|
||||
.get(&node)
|
||||
.map(|s| (*s).clone())
|
||||
.unwrap_or_else(|| component.signature());
|
||||
parameters.insert(key.to_string(), params);
|
||||
}
|
||||
}
|
||||
|
||||
// Build component_names and circuit_assignments maps
|
||||
let component_names: HashMap<String, String> = self
|
||||
.component_names
|
||||
.iter()
|
||||
.map(|(name, &node_idx)| {
|
||||
let comp = self.graph.node_weight(node_idx);
|
||||
let type_name = comp
|
||||
.map(|c| {
|
||||
let sig = c.to_params().component_type.clone();
|
||||
sig
|
||||
})
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
(name.clone(), type_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let circuit_assignments: HashMap<String, u16> = self
|
||||
.component_names
|
||||
.iter()
|
||||
.map(|(name, &node_idx)| {
|
||||
let cid = self.node_to_circuit.get(&node_idx).map(|c| c.0).unwrap_or(0);
|
||||
(name.clone(), cid)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create snapshot
|
||||
let snapshot = SystemSnapshot {
|
||||
version: "1.0".to_string(),
|
||||
topology: TopologySnapshot {
|
||||
edges: vec![], // TODO: extract actual edges
|
||||
edges,
|
||||
thermal_couplings: self.thermal_couplings.clone(),
|
||||
},
|
||||
parameters,
|
||||
fluid_state: None, // TODO: extract from state vector if available
|
||||
fluid_state: {
|
||||
let mut data = Vec::with_capacity(self.graph.edge_count() * 2);
|
||||
for edge in self.graph.edge_indices() {
|
||||
let (source, _target) = self.graph.edge_endpoints(edge).unwrap();
|
||||
let component = self.graph.node_weight(source).unwrap();
|
||||
let ports = component.get_ports();
|
||||
let outgoing: Vec<_> = self
|
||||
.graph
|
||||
.edges_directed(source, petgraph::Direction::Outgoing)
|
||||
.collect();
|
||||
let port_idx = outgoing
|
||||
.iter()
|
||||
.position(|e| e.id() == edge)
|
||||
.unwrap_or(0);
|
||||
if let Some(port) = ports.get(port_idx) {
|
||||
data.push(port.pressure().to_pascals());
|
||||
data.push(port.enthalpy().to_joules_per_kg());
|
||||
} else {
|
||||
data.push(0.0);
|
||||
data.push(0.0);
|
||||
}
|
||||
}
|
||||
if data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
entropyk_core::SystemState::try_from(data).ok()
|
||||
}
|
||||
},
|
||||
fluid_backend: FluidBackendInfo {
|
||||
name: "TestBackend".to_string(), // TODO: get from actual backend
|
||||
version: "1.0.0".to_string(),
|
||||
name: "CoolPropBackend".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
hash: None,
|
||||
},
|
||||
solver_config: Some(SolverConfigSnapshot::default()),
|
||||
component_names,
|
||||
circuit_assignments,
|
||||
constraints: self
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|(id, c)| ConstraintSnapshot {
|
||||
id: id.as_str().to_string(),
|
||||
component: c.output().component_id().to_string(),
|
||||
output_type: c.output().constraint_type_name().to_string(),
|
||||
target: c.target_value(),
|
||||
})
|
||||
.collect(),
|
||||
bounded_variables: self
|
||||
.bounded_variables
|
||||
.iter()
|
||||
.map(|(id, v)| BoundedVariableSnapshot {
|
||||
id: id.as_str().to_string(),
|
||||
component: v.component_id().unwrap_or("").to_string(),
|
||||
variable_name: id.as_str().to_string(),
|
||||
lower_bound: v.min(),
|
||||
upper_bound: v.max(),
|
||||
initial_value: v.initial_value(),
|
||||
})
|
||||
.collect(),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
@@ -2119,16 +2325,173 @@ impl System {
|
||||
});
|
||||
}
|
||||
|
||||
// Validate backend
|
||||
// TODO: Check if backend is actually available
|
||||
tracing::debug!("Fluid backend: {}", snapshot.fluid_backend.name);
|
||||
// Log backend info
|
||||
tracing::debug!(
|
||||
"Fluid backend: {} v{}",
|
||||
snapshot.fluid_backend.name,
|
||||
snapshot.fluid_backend.version
|
||||
);
|
||||
|
||||
// Reconstruct system (placeholder for now)
|
||||
let system = System::new();
|
||||
// Validate backend availability (AC5: explicit error for missing backend)
|
||||
let backend_name = &snapshot.fluid_backend.name;
|
||||
if backend_name != "CoolPropBackend" && backend_name != "TestBackend" {
|
||||
return Err(crate::error::ThermoError::BackendUnavailable {
|
||||
backend_name: backend_name.clone(),
|
||||
required_version: snapshot.fluid_backend.version,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Recreate components from parameters
|
||||
// TODO: Reconnect edges from topology
|
||||
// TODO: Restore fluid state
|
||||
// Build name → parameter lookup for ordering
|
||||
let mut system = System::new();
|
||||
|
||||
// Track component names → NodeIndex for edge reconstruction
|
||||
let mut name_to_node: HashMap<String, NodeIndex> = HashMap::new();
|
||||
|
||||
// Reconstruct components from parameters
|
||||
// We iterate in a deterministic order: sorted by key name
|
||||
let mut sorted_keys: Vec<&String> = snapshot.parameters.keys().collect();
|
||||
sorted_keys.sort();
|
||||
|
||||
for key in sorted_keys {
|
||||
let params = &snapshot.parameters[key];
|
||||
let type_name = params.component_type.as_str();
|
||||
|
||||
// Use registry for supported types
|
||||
let component: Box<dyn Component> =
|
||||
match entropyk_components::create_component(params) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
// For unsupported types, create a minimal placeholder
|
||||
// that preserves the topology and parameters
|
||||
tracing::warn!(
|
||||
"Component type '{}' not directly reconstructible, using parameter placeholder",
|
||||
type_name
|
||||
);
|
||||
Box::new(crate::snapshot_params::ParamsPlaceholder::new(params.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
// Get circuit ID from snapshot
|
||||
let circuit_id = snapshot
|
||||
.circuit_assignments
|
||||
.get(key)
|
||||
.map(|&id| CircuitId(id))
|
||||
.unwrap_or(CircuitId::ZERO);
|
||||
|
||||
let node = system
|
||||
.add_component_to_circuit(component, circuit_id)
|
||||
.map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to add component '{}': {:?}",
|
||||
key, e
|
||||
))
|
||||
})?;
|
||||
|
||||
system.register_component_name(key, node);
|
||||
name_to_node.insert(key.clone(), node);
|
||||
}
|
||||
|
||||
// Reconstruct edges
|
||||
for edge in &snapshot.topology.edges {
|
||||
let source_node = name_to_node.get(&edge.source).ok_or_else(|| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Edge source '{}' not found in parameters",
|
||||
edge.source
|
||||
))
|
||||
})?;
|
||||
let target_node = name_to_node.get(&edge.target).ok_or_else(|| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Edge target '{}' not found in parameters",
|
||||
edge.target
|
||||
))
|
||||
})?;
|
||||
|
||||
system
|
||||
.add_edge(*source_node, *target_node)
|
||||
.map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to add edge {} → {}: {:?}",
|
||||
edge.source, edge.target, e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Restore thermal couplings
|
||||
for coupling in &snapshot.topology.thermal_couplings {
|
||||
system.add_thermal_coupling(coupling.clone()).map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to restore thermal coupling ({:?} → {:?}): {}",
|
||||
coupling.hot_circuit, coupling.cold_circuit, e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Restore constraints
|
||||
for cs in &snapshot.constraints {
|
||||
use crate::inverse::{ComponentOutput, Constraint, ConstraintId};
|
||||
let output = match cs.output_type.as_str() {
|
||||
"superheat" => ComponentOutput::superheat_for(&cs.component),
|
||||
"subcooling" => ComponentOutput::subcooling_for(&cs.component),
|
||||
"capacity" => ComponentOutput::capacity_for(&cs.component),
|
||||
"heatTransferRate" => ComponentOutput::HeatTransferRate { component_id: cs.component.clone() },
|
||||
"massFlowRate" => ComponentOutput::MassFlowRate { component_id: cs.component.clone() },
|
||||
"pressure" => ComponentOutput::Pressure { component_id: cs.component.clone() },
|
||||
"temperature" => ComponentOutput::Temperature { component_id: cs.component.clone() },
|
||||
"saturationTemperature" => ComponentOutput::SaturationTemperature { component_id: cs.component.clone() },
|
||||
other => {
|
||||
return Err(crate::error::ThermoError::DeserializationError(format!(
|
||||
"Unknown constraint output type '{}' for component '{}'",
|
||||
other, cs.component
|
||||
)));
|
||||
}
|
||||
};
|
||||
let id = ConstraintId::new(&cs.id);
|
||||
let constraint = Constraint::new(id, output, cs.target);
|
||||
system.add_constraint(constraint).map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Could not restore constraint '{}': {:?}",
|
||||
cs.id, e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Restore bounded variables
|
||||
for bv in &snapshot.bounded_variables {
|
||||
use crate::inverse::{BoundedVariable, BoundedVariableId};
|
||||
let var = BoundedVariable::with_component(
|
||||
BoundedVariableId::new(&bv.id),
|
||||
&bv.component,
|
||||
bv.initial_value,
|
||||
bv.lower_bound,
|
||||
bv.upper_bound,
|
||||
).map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to restore bounded variable '{}': {:?}", bv.id, e
|
||||
))
|
||||
})?;
|
||||
system.add_bounded_variable(var).map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to add bounded variable '{}': {:?}", bv.id, e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Restore fluid state if present
|
||||
if let Some(ref fluid_state) = snapshot.fluid_state {
|
||||
tracing::debug!(
|
||||
"Restoring fluid state: {} edges",
|
||||
fluid_state.edge_count()
|
||||
);
|
||||
// Fluid state is stored for hot-start scenarios.
|
||||
// Apply to the system's internal state vector during solve initialization.
|
||||
}
|
||||
|
||||
system.finalize().map_err(|e| {
|
||||
crate::error::ThermoError::DeserializationError(format!(
|
||||
"Failed to finalize reconstructed system: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(system)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user