use super::*;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReactionSystem {
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<Reference>,
pub species: IndexMap<String, Species>,
pub parameters: IndexMap<String, Parameter>,
pub reactions: Vec<Reaction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraint_equations: Option<Vec<Equation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discrete_events: Option<Vec<DiscreteEvent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub continuous_events: Option<Vec<ContinuousEvent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subsystems: Option<IndexMap<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tolerance: Option<Tolerance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tests: Option<Vec<ModelTest>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub analyses: Option<Vec<ModelAnalysis>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Species {
#[serde(skip_serializing_if = "Option::is_none")]
pub units: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_units: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constant: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Parameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub units: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_units: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shape: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub distribution: Option<Distribution>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub update: Option<ParameterUpdateSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reaction {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub substrates: Option<Vec<StoichiometricEntry>>,
#[serde(default)]
pub products: Option<Vec<StoichiometricEntry>>,
pub rate: Expr,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<Reference>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoichiometricEntry {
pub species: String,
#[serde(
rename = "stoichiometry",
default = "default_stoichiometry",
serialize_with = "serialize_stoichiometry"
)]
pub coefficient: f64,
}
fn default_stoichiometry() -> f64 {
1.0
}
fn serialize_stoichiometry<S: serde::Serializer>(
n: &f64,
serializer: S,
) -> Result<S::Ok, S::Error> {
serialize_canonical_f64(*n, serializer)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataSource {
pub kind: DataSourceKind,
pub source: DataSourceLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal: Option<DataSourceTemporal>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub determinism: Option<DataSourceDeterminism>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reader_options: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub select: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub record_filter: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extent: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<DataSourceMetadata>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DataSourceKind {
Grid,
Points,
Static,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataSourceDeterminism {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub endian: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub float_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub integer_width: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataSourceLocation {
pub url_template: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mirrors: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataSourceTemporal {
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_period: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub records_per_file: Option<RecordsPerFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_variable: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RecordsPerFile {
Count(u32),
Auto(AutoRecords),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AutoRecords {
Auto,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum UnitConversion {
Factor(f64),
Expression(Expr),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataSourceMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Operator {
pub operator_id: String,
pub needed_vars: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modifies: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<Reference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum VariableMapTransform {
Named(String),
Expression(ExpressionNode),
}
impl VariableMapTransform {
pub fn as_named(&self) -> Option<&str> {
match self {
VariableMapTransform::Named(s) => Some(s.as_str()),
VariableMapTransform::Expression(_) => None,
}
}
pub fn as_expression(&self) -> Option<&ExpressionNode> {
match self {
VariableMapTransform::Named(_) => None,
VariableMapTransform::Expression(node) => Some(node),
}
}
pub fn is_expression(&self) -> bool {
matches!(self, VariableMapTransform::Expression(_))
}
}
impl std::fmt::Display for VariableMapTransform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariableMapTransform::Named(s) => write!(f, "{s}"),
VariableMapTransform::Expression(_) => write!(f, "expression"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum CouplingEntry {
OperatorCompose {
systems: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
translate: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
lifting: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
require_match: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
Couple {
systems: Vec<String>,
connector: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
lifting: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
VariableMap {
from: String,
to: String,
transform: VariableMapTransform,
#[serde(skip_serializing_if = "Option::is_none")]
factor: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
lifting: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
OperatorApply {
operator: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
Callback {
callback_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
Event {
event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
conditions: Option<Vec<Expr>>,
#[serde(skip_serializing_if = "Option::is_none")]
trigger: Option<DiscreteEventTrigger>,
#[serde(skip_serializing_if = "Option::is_none")]
affects: Option<Vec<AffectEquation>>,
#[serde(skip_serializing_if = "Option::is_none")]
affect_neg: Option<Vec<AffectEquation>>,
#[serde(skip_serializing_if = "Option::is_none")]
root_find: Option<RootFindDirection>,
#[serde(skip_serializing_if = "Option::is_none")]
reinitialize: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
CouplingImport {
#[serde(rename = "ref")]
reference: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
bind: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CouplingRole {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Domain {
#[serde(skip_serializing_if = "Option::is_none")]
pub independent_variable: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub element_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub array_type: Option<String>,
}
#[cfg(test)]
mod coupling_field_tests {
use super::*;
#[test]
fn test_operator_compose_new_fields() {
let json = r#"{
"type": "operator_compose",
"systems": ["system1", "system2"]
}"#;
let entry: CouplingEntry = serde_json::from_str(json).unwrap();
match entry {
CouplingEntry::OperatorCompose { systems, .. } => {
assert_eq!(systems, vec!["system1", "system2"]);
}
_ => panic!("Expected OperatorCompose variant"),
}
}
#[test]
fn test_couple_new_fields() {
let json = r#"{
"type": "couple",
"systems": ["system1", "system2"],
"connector": {
"equations": []
}
}"#;
let entry: CouplingEntry = serde_json::from_str(json).unwrap();
match entry {
CouplingEntry::Couple { systems, .. } => {
assert_eq!(systems, vec!["system1", "system2"]);
}
_ => panic!("Expected Couple variant"),
}
}
#[test]
fn test_variable_map_new_fields() {
let json = r#"{
"type": "variable_map",
"from": "source.var",
"to": "target.param",
"transform": "identity"
}"#;
let entry: CouplingEntry = serde_json::from_str(json).unwrap();
match entry {
CouplingEntry::VariableMap {
from,
to,
transform,
..
} => {
assert_eq!(from, "source.var");
assert_eq!(to, "target.param");
assert_eq!(
transform,
crate::types::VariableMapTransform::Named("identity".to_string())
);
}
_ => panic!("Expected VariableMap variant"),
}
}
#[test]
fn test_coupling_serialization_round_trip() {
let coupling = CouplingEntry::OperatorCompose {
lifting: None,
systems: vec!["sys1".to_string(), "sys2".to_string()],
translate: None,
require_match: None,
description: None,
};
let serialized = serde_json::to_string(&coupling).unwrap();
let deserialized: CouplingEntry = serde_json::from_str(&serialized).unwrap();
match deserialized {
CouplingEntry::OperatorCompose { systems, .. } => {
assert_eq!(systems, vec!["sys1", "sys2"]);
}
_ => panic!("Round-trip failed"),
}
}
}