1use crate::{CouplingEntry, EsmFile};
4
5fn sorted_keys<V>(map: &indexmap::IndexMap<String, V>) -> Vec<&String> {
11 let mut keys: Vec<&String> = map.keys().collect();
12 keys.sort();
13 keys
14}
15
16const DEFAULT_SYSTEM: &str = "default";
21
22const NON_EQUATION_INDEX: i64 = -1;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum Direction {
31 Both,
33 Incoming,
35 Outgoing,
37}
38
39fn neighbours<'a, I>(known: bool, edges: I, node: &str, direction: Direction) -> Vec<String>
47where
48 I: Iterator<Item = (&'a str, &'a str)>,
49{
50 if !known {
51 return Vec::new();
52 }
53 let mut out = std::collections::BTreeSet::new();
54 for (source, target) in edges {
55 if direction != Direction::Incoming && source == node {
56 out.insert(target.to_string());
57 }
58 if direction != Direction::Outgoing && target == node {
59 out.insert(source.to_string());
60 }
61 }
62 out.into_iter().collect()
63}
64
65fn render_json_graph<N, E>(
73 nodes: &[N],
74 node_key: impl Fn(&N) -> String,
75 edges: &[E],
76 edge_endpoints: impl Fn(&E) -> (String, String),
77 adjacency: impl Fn(&str) -> Vec<String>,
78) -> String
79where
80 N: serde::Serialize,
81 E: serde::Serialize,
82{
83 let json_nodes: Vec<serde_json::Value> = nodes
84 .iter()
85 .map(|node| {
86 let key = node_key(node);
87 let mut value = serde_json::to_value(node).unwrap_or(serde_json::Value::Null);
88 if let Some(object) = value.as_object_mut() {
89 object.insert("id".to_string(), serde_json::Value::String(key));
90 }
91 value
92 })
93 .collect();
94
95 let json_edges: Vec<serde_json::Value> = edges
96 .iter()
97 .map(|edge| {
98 let (source, target) = edge_endpoints(edge);
99 serde_json::json!({
100 "source": source,
101 "target": target,
102 "data": serde_json::to_value(edge).unwrap_or(serde_json::Value::Null),
103 })
104 })
105 .collect();
106
107 let mut json_adjacency = serde_json::Map::new();
108 for node in nodes {
109 let key = node_key(node);
110 let neighbours = adjacency(&key);
111 json_adjacency.insert(key, serde_json::to_value(neighbours).unwrap_or_default());
112 }
113
114 let graph = serde_json::json!({
115 "nodes": json_nodes,
116 "edges": json_edges,
117 "adjacency": serde_json::Value::Object(json_adjacency),
118 });
119 serde_json::to_string_pretty(&graph).unwrap_or_else(|_| "{}".to_string())
120}
121
122#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
124pub struct ComponentGraph {
125 pub nodes: Vec<ComponentNode>,
127 pub edges: Vec<CouplingEdge>,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
142pub struct ComponentMetadata {
143 pub var_count: usize,
145 pub eq_count: usize,
147 pub species_count: usize,
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
153pub struct ComponentNode {
154 pub id: String,
156 pub component_type: ComponentType,
158 pub name: Option<String>,
160 pub metadata: ComponentMetadata,
162}
163
164#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ComponentType {
173 Model,
175 ReactionSystem,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum CouplingEdgeKind {
183 OperatorCompose,
185 Couple,
187 VariableMap,
189}
190
191impl std::fmt::Display for CouplingEdgeKind {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.write_str(match self {
194 CouplingEdgeKind::OperatorCompose => "operator_compose",
195 CouplingEdgeKind::Couple => "couple",
196 CouplingEdgeKind::VariableMap => "variable_map",
197 })
198 }
199}
200
201#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
203pub struct CouplingEdge {
204 pub from: String,
206 pub to: String,
208 pub coupling_type: CouplingEdgeKind,
210 pub label: String,
214 pub data: serde_json::Value,
216}
217
218pub fn component_graph(esm_file: &EsmFile) -> ComponentGraph {
228 let mut nodes = Vec::new();
229 let mut edges = Vec::new();
230
231 if let Some(ref models) = esm_file.models {
233 for id in sorted_keys(models) {
234 let model = &models[id];
235 nodes.push(ComponentNode {
236 id: id.clone(),
237 component_type: ComponentType::Model,
238 name: model.name.clone(),
239 metadata: ComponentMetadata {
240 var_count: model.variables.len(),
241 eq_count: model.equations.len(),
242 species_count: 0,
243 },
244 });
245 }
246 }
247
248 if let Some(ref reaction_systems) = esm_file.reaction_systems {
250 for id in sorted_keys(reaction_systems) {
251 let rs = &reaction_systems[id];
252 nodes.push(ComponentNode {
253 id: id.clone(),
254 component_type: ComponentType::ReactionSystem,
255 name: None,
256 metadata: ComponentMetadata {
257 var_count: rs.parameters.len(),
258 eq_count: rs.reactions.len(),
259 species_count: rs.species.len(),
260 },
261 });
262 }
263 }
264
265 let node_ids: std::collections::HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
277
278 if let Some(ref coupling_entries) = esm_file.coupling {
279 for entry in coupling_entries {
280 let (from, to, kind, label) = match entry {
288 CouplingEntry::OperatorCompose { systems, .. } => {
289 if systems.len() >= 2 {
290 (
291 systems[0].clone(),
292 systems[1].clone(),
293 CouplingEdgeKind::OperatorCompose,
294 "compose".to_string(),
295 )
296 } else {
297 continue; }
299 }
300 CouplingEntry::Couple { systems, .. } => {
301 if systems.len() == 2 {
302 (
303 systems[0].clone(),
304 systems[1].clone(),
305 CouplingEdgeKind::Couple,
306 "couple".to_string(),
307 )
308 } else {
309 continue; }
311 }
312 CouplingEntry::VariableMap { from, to, .. } => {
313 let (Some((from_system, from_var)), Some((to_system, _))) =
318 (from.split_once('.'), to.split_once('.'))
319 else {
320 continue;
321 };
322 (
323 from_system.to_string(),
324 to_system.to_string(),
325 CouplingEdgeKind::VariableMap,
326 from_var.to_string(),
327 )
328 }
329 CouplingEntry::OperatorApply { .. }
332 | CouplingEntry::Callback { .. }
333 | CouplingEntry::Event { .. }
334 | CouplingEntry::CouplingImport { .. } => continue,
335 };
336
337 if node_ids.contains(from.as_str()) && node_ids.contains(to.as_str()) {
339 edges.push(CouplingEdge {
340 from,
341 to,
342 coupling_type: kind,
343 label,
344 data: serde_json::Value::Null,
345 });
346 }
347 }
348 }
349
350 ComponentGraph { nodes, edges }
351}
352
353pub fn component_exists(esm_file: &EsmFile, component_id: &str) -> bool {
364 get_component_type(esm_file, component_id).is_some()
365}
366
367pub fn get_component_type(esm_file: &EsmFile, component_id: &str) -> Option<ComponentType> {
382 fn contains<V>(map: &Option<indexmap::IndexMap<String, V>>, key: &str) -> bool {
383 map.as_ref().is_some_and(|m| m.contains_key(key))
384 }
385
386 if contains(&esm_file.models, component_id) {
387 Some(ComponentType::Model)
388 } else if contains(&esm_file.reaction_systems, component_id) {
389 Some(ComponentType::ReactionSystem)
390 } else {
391 None
392 }
393}
394
395#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
397pub struct ExpressionGraph {
398 pub nodes: Vec<VariableNode>,
400 pub edges: Vec<DependencyEdge>,
402}
403
404#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
406pub struct VariableNode {
407 pub name: String,
411 pub kind: VariableKind,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub units: Option<String>,
416 pub system: String,
418}
419
420#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
426#[serde(rename_all = "snake_case")]
427pub enum VariableKind {
428 State,
430 Algebraic,
433 Parameter,
435 Observed,
437 Brownian,
440 Discrete,
443 Species,
445}
446
447fn variable_kind(class: &crate::classification::Classification, name: &str) -> VariableKind {
450 if class.is_observed(name) {
451 VariableKind::Observed
452 } else if class.is_brownian(name) {
453 VariableKind::Brownian
454 } else if class.is_discrete_parameter(name) {
455 VariableKind::Discrete
456 } else if class.ode_states.iter().any(|s| s == name) {
457 VariableKind::State
458 } else if class.algebraic_unknowns.iter().any(|s| s == name) {
459 VariableKind::Algebraic
460 } else {
461 VariableKind::Parameter
462 }
463}
464
465#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
467pub struct DependencyEdge {
468 pub source: String,
470 pub target: String,
472 pub relationship: DependencyRelationship,
474 #[serde(skip_serializing_if = "Option::is_none")]
477 pub equation_index: Option<i64>,
478 #[serde(skip_serializing_if = "Option::is_none")]
480 pub expression: Option<crate::Expr>,
481}
482
483#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
488#[serde(rename_all = "snake_case")]
489pub enum DependencyRelationship {
490 Additive,
492 Multiplicative,
495 Rate,
497 Stoichiometric,
499}
500
501#[derive(Debug, Clone, Default)]
503pub struct ExpressionGraphOptions {
504 pub merge_coupled: bool,
508}
509
510pub fn expression_graph<T>(input: &T) -> ExpressionGraph
520where
521 T: ExpressionGraphInput,
522{
523 input.build_expression_graph()
524}
525
526pub fn expression_graph_with_options<T>(
537 input: &T,
538 options: &ExpressionGraphOptions,
539) -> ExpressionGraph
540where
541 T: ExpressionGraphInput,
542{
543 input.build_expression_graph_with_options(options)
544}
545
546pub trait ExpressionGraphInput {
548 fn build_expression_graph(&self) -> ExpressionGraph;
550
551 fn build_expression_graph_with_options(
555 &self,
556 _options: &ExpressionGraphOptions,
557 ) -> ExpressionGraph {
558 self.build_expression_graph()
559 }
560}
561
562struct ExprGraphBuilder {
565 nodes: Vec<VariableNode>,
566 edges: Vec<DependencyEdge>,
567 seen: std::collections::HashSet<String>,
568}
569
570impl ExprGraphBuilder {
571 fn new() -> Self {
572 ExprGraphBuilder {
573 nodes: Vec::new(),
574 edges: Vec::new(),
575 seen: std::collections::HashSet::new(),
576 }
577 }
578
579 fn add_node(
585 &mut self,
586 name: &str,
587 kind: VariableKind,
588 units: Option<String>,
589 system: &str,
590 ) -> String {
591 let scoped = if system == DEFAULT_SYSTEM {
592 name.to_string()
593 } else {
594 format!("{system}.{name}")
595 };
596 if self.seen.insert(scoped.clone()) {
597 self.nodes.push(VariableNode {
598 name: scoped.clone(),
599 kind,
600 units,
601 system: system.to_string(),
602 });
603 }
604 scoped
605 }
606
607 fn add_dependency(
608 &mut self,
609 source: String,
610 target: String,
611 relationship: DependencyRelationship,
612 equation_index: i64,
613 expression: Option<crate::Expr>,
614 ) {
615 self.edges.push(DependencyEdge {
616 source,
617 target,
618 relationship,
619 equation_index: Some(equation_index),
620 expression,
621 });
622 }
623
624 fn finish(self) -> ExpressionGraph {
625 ExpressionGraph {
626 nodes: self.nodes,
627 edges: self.edges,
628 }
629 }
630}
631
632impl ExpressionGraphInput for crate::EsmFile {
633 fn build_expression_graph(&self) -> ExpressionGraph {
634 self.build_expression_graph_with_options(&ExpressionGraphOptions::default())
635 }
636
637 fn build_expression_graph_with_options(
638 &self,
639 options: &ExpressionGraphOptions,
640 ) -> ExpressionGraph {
641 let mut b = ExprGraphBuilder::new();
642
643 if let Some(ref models) = self.models {
646 for model_id in sorted_keys(models) {
647 process_model_tree(&mut b, &models[model_id], model_id);
648 }
649 }
650
651 if let Some(ref reaction_systems) = self.reaction_systems {
652 for rs_id in sorted_keys(reaction_systems) {
653 process_reaction_system_tree(&mut b, &reaction_systems[rs_id], rs_id);
654 }
655 }
656
657 if options.merge_coupled
658 && let Some(ref coupling) = self.coupling
659 {
660 process_coupling(&mut b, coupling);
661 }
662
663 b.finish()
664 }
665}
666
667impl ExpressionGraphInput for crate::Model {
668 fn build_expression_graph(&self) -> ExpressionGraph {
669 let mut b = ExprGraphBuilder::new();
670 process_model_tree(&mut b, self, DEFAULT_SYSTEM);
671 b.finish()
672 }
673}
674
675impl ExpressionGraphInput for crate::ReactionSystem {
676 fn build_expression_graph(&self) -> ExpressionGraph {
677 let mut b = ExprGraphBuilder::new();
678 process_reaction_system_tree(&mut b, self, DEFAULT_SYSTEM);
679 b.finish()
680 }
681}
682
683impl ExpressionGraphInput for crate::Equation {
684 fn build_expression_graph(&self) -> ExpressionGraph {
685 let mut b = ExprGraphBuilder::new();
690 process_equation(&mut b, self, 0, DEFAULT_SYSTEM);
691 b.finish()
692 }
693}
694
695impl ExpressionGraphInput for crate::Reaction {
696 fn build_expression_graph(&self) -> ExpressionGraph {
697 let mut b = ExprGraphBuilder::new();
702 process_reaction(&mut b, self, 0, DEFAULT_SYSTEM);
703 b.finish()
704 }
705}
706
707impl ExpressionGraphInput for crate::Expr {
708 fn build_expression_graph(&self) -> ExpressionGraph {
709 let mut b = ExprGraphBuilder::new();
714 process_expression(
718 &mut b,
719 self,
720 "expr_result",
721 NON_EQUATION_INDEX,
722 DEFAULT_SYSTEM,
723 );
724 b.finish()
725 }
726}
727
728fn is_reference_stub(entry: &serde_json::Value) -> bool {
732 entry.get("ref").is_some()
733}
734
735fn as_model(entry: &serde_json::Value) -> Option<crate::Model> {
739 let mut value = entry.clone();
740 let object = value.as_object_mut()?;
741 object
742 .entry("variables")
743 .or_insert_with(|| serde_json::json!({}));
744 object
745 .entry("equations")
746 .or_insert_with(|| serde_json::json!([]));
747 serde_json::from_value(value).ok()
748}
749
750fn as_reaction_system(entry: &serde_json::Value) -> Option<crate::ReactionSystem> {
753 let mut value = entry.clone();
754 let object = value.as_object_mut()?;
755 object
756 .entry("species")
757 .or_insert_with(|| serde_json::json!({}));
758 object
759 .entry("parameters")
760 .or_insert_with(|| serde_json::json!({}));
761 object
762 .entry("reactions")
763 .or_insert_with(|| serde_json::json!([]));
764 serde_json::from_value(value).ok()
765}
766
767fn child_scope(system_id: &str, child_name: &str) -> String {
771 if system_id == DEFAULT_SYSTEM {
772 child_name.to_string()
773 } else {
774 format!("{system_id}.{child_name}")
775 }
776}
777
778fn process_model_tree(b: &mut ExprGraphBuilder, model: &crate::Model, system_id: &str) {
781 process_model(b, model, system_id);
782
783 let Some(ref subsystems) = model.subsystems else {
784 return;
785 };
786 for child_name in sorted_keys(subsystems) {
787 let entry = &subsystems[child_name];
788 if is_reference_stub(entry) {
789 continue;
790 }
791 if let Some(child) = as_model(entry) {
792 process_model_tree(b, &child, &child_scope(system_id, child_name));
793 }
794 }
795}
796
797fn process_reaction_system_tree(
800 b: &mut ExprGraphBuilder,
801 rs: &crate::ReactionSystem,
802 system_id: &str,
803) {
804 process_reaction_system(b, rs, system_id);
805
806 let Some(ref subsystems) = rs.subsystems else {
807 return;
808 };
809 for child_name in sorted_keys(subsystems) {
810 let entry = &subsystems[child_name];
811 if is_reference_stub(entry) {
812 continue;
813 }
814 if let Some(child) = as_reaction_system(entry) {
815 process_reaction_system_tree(b, &child, &child_scope(system_id, child_name));
816 }
817 }
818}
819
820fn process_model(b: &mut ExprGraphBuilder, model: &crate::Model, system_id: &str) {
823 let class = crate::classification::Classification::of(model);
828 for var_name in sorted_keys(&model.variables) {
829 let kind = variable_kind(&class, var_name);
830 b.add_node(
831 var_name,
832 kind,
833 model.variables[var_name].units.clone(),
834 system_id,
835 );
836 }
837
838 for (eq_idx, equation) in model.equations.iter().enumerate() {
839 process_equation(b, equation, eq_idx as i64, system_id);
840 }
841}
842
843fn process_reaction_system(b: &mut ExprGraphBuilder, rs: &crate::ReactionSystem, system_id: &str) {
846 for species_name in sorted_keys(&rs.species) {
847 b.add_node(
848 species_name,
849 VariableKind::Species,
850 rs.species[species_name].units.clone(),
851 system_id,
852 );
853 }
854
855 for param_name in sorted_keys(&rs.parameters) {
859 b.add_node(
860 param_name,
861 VariableKind::Parameter,
862 rs.parameters[param_name].units.clone(),
863 system_id,
864 );
865 }
866
867 for (rxn_idx, reaction) in rs.reactions.iter().enumerate() {
868 process_reaction(b, reaction, rxn_idx as i64, system_id);
869 }
870
871 if let Some(ref constraints) = rs.constraint_equations {
873 for (idx, equation) in constraints.iter().enumerate() {
874 process_equation(b, equation, (idx + rs.reactions.len()) as i64, system_id);
875 }
876 }
877}
878
879fn lhs_target_name(lhs: &crate::Expr) -> Option<String> {
889 match lhs {
890 crate::Expr::Variable(name) => Some(name.clone()),
891 crate::Expr::Operator(node) => match node.op.as_str() {
892 "D" | "index" => node.args.first().and_then(lhs_target_name),
893 "faq" => node.expr.as_deref().and_then(lhs_target_name),
894 _ => None,
895 },
896 crate::Expr::Number(_) | crate::Expr::Integer(_) => None,
897 }
898}
899
900fn process_equation(
907 b: &mut ExprGraphBuilder,
908 equation: &crate::Equation,
909 eq_idx: i64,
910 system_id: &str,
911) {
912 let Some(target_name) = lhs_target_name(&equation.lhs) else {
913 return;
914 };
915 let lhs_var = b.add_node(&target_name, VariableKind::State, None, system_id);
916
917 for rhs_var in extract_variables_from_expr(&equation.rhs) {
918 let source = b.add_node(&rhs_var, VariableKind::Parameter, None, system_id);
919 b.add_dependency(
920 source,
921 lhs_var.clone(),
922 DependencyRelationship::Additive,
923 eq_idx,
924 Some(equation.rhs.clone()),
925 );
926 }
927}
928
929fn process_reaction(
933 b: &mut ExprGraphBuilder,
934 reaction: &crate::Reaction,
935 rxn_idx: i64,
936 system_id: &str,
937) {
938 let rate_vars = extract_variables_from_expr(&reaction.rate);
939
940 for substrate in reaction.substrates.iter().flatten() {
942 let substrate_var = b.add_node(&substrate.species, VariableKind::Species, None, system_id);
943 for rate_var in &rate_vars {
944 let param = b.add_node(rate_var, VariableKind::Parameter, None, system_id);
945 b.add_dependency(
946 param,
947 substrate_var.clone(),
948 DependencyRelationship::Rate,
949 rxn_idx,
950 Some(reaction.rate.clone()),
951 );
952 }
953 }
954
955 for product in reaction.products.iter().flatten() {
957 let product_var = b.add_node(&product.species, VariableKind::Species, None, system_id);
958 for rate_var in &rate_vars {
959 let param = b.add_node(rate_var, VariableKind::Parameter, None, system_id);
960 b.add_dependency(
961 param,
962 product_var.clone(),
963 DependencyRelationship::Rate,
964 rxn_idx,
965 Some(reaction.rate.clone()),
966 );
967 }
968 for substrate in reaction.substrates.iter().flatten() {
969 let substrate_var =
970 b.add_node(&substrate.species, VariableKind::Species, None, system_id);
971 b.add_dependency(
972 substrate_var,
973 product_var.clone(),
974 DependencyRelationship::Stoichiometric,
975 rxn_idx,
976 Some(reaction.rate.clone()),
977 );
978 }
979 }
980}
981
982fn process_expression(
984 b: &mut ExprGraphBuilder,
985 expr: &crate::Expr,
986 target_var: &str,
987 eq_idx: i64,
988 system_id: &str,
989) {
990 let target = b.add_node(target_var, VariableKind::Observed, None, system_id);
991 for free_var in extract_variables_from_expr(expr) {
992 let source = b.add_node(&free_var, VariableKind::Parameter, None, system_id);
993 b.add_dependency(
994 source,
995 target.clone(),
996 DependencyRelationship::Multiplicative,
997 eq_idx,
998 Some(expr.clone()),
999 );
1000 }
1001}
1002
1003fn process_coupling(b: &mut ExprGraphBuilder, coupling: &[CouplingEntry]) {
1008 for entry in coupling {
1009 let CouplingEntry::VariableMap { from, to, .. } = entry else {
1010 continue;
1011 };
1012 let (Some((from_system, from_var)), Some((to_system, to_var))) =
1013 (from.split_once('.'), to.split_once('.'))
1014 else {
1015 continue;
1016 };
1017
1018 let source = b.add_node(from_var, VariableKind::Parameter, None, from_system);
1019 let target = b.add_node(to_var, VariableKind::Parameter, None, to_system);
1020 b.add_dependency(
1021 source,
1022 target,
1023 DependencyRelationship::Multiplicative,
1024 NON_EQUATION_INDEX,
1025 Some(crate::Expr::Variable(from.clone())),
1026 );
1027 }
1028}
1029
1030fn graph_bound_index_symbols(node: &crate::types::ExpressionNode) -> Vec<String> {
1039 let mut bound = Vec::new();
1040 if let Some(ranges) = &node.ranges {
1041 bound.extend(ranges.keys().cloned());
1042 }
1043 if let Some(idx) = &node.output_idx {
1044 bound.extend(idx.iter().cloned());
1045 }
1046 if let Some(v) = &node.int_var {
1047 bound.push(v.clone());
1048 }
1049 bound
1050}
1051
1052fn collect_graph_variables(expr: &crate::Expr, out: &mut std::collections::HashSet<String>) {
1055 match expr {
1056 crate::Expr::Variable(name) => {
1057 out.insert(name.clone());
1058 }
1059 crate::Expr::Operator(node) => {
1060 let mut local = std::collections::HashSet::new();
1063 node.for_each_child(&mut |child| collect_graph_variables(child, &mut local));
1064 for symbol in graph_bound_index_symbols(node) {
1065 local.remove(&symbol);
1066 }
1067 out.extend(local);
1068 }
1069 crate::Expr::Number(_) | crate::Expr::Integer(_) => {}
1070 }
1071}
1072
1073fn extract_variables_from_expr(expr: &crate::Expr) -> Vec<String> {
1095 let mut set = std::collections::HashSet::new();
1096 collect_graph_variables(expr, &mut set);
1097 let mut vars: Vec<String> = set.into_iter().collect();
1098 vars.sort();
1099 vars
1100}
1101
1102fn escape_dot(s: &str) -> String {
1108 s.replace('\\', "\\\\").replace('"', "\\\"")
1109}
1110
1111fn mermaid_label(s: &str) -> String {
1122 if s.contains(['"', '(', ')', '[', ']', '{', '}', '<', '>', '|']) {
1123 format!("\"{}\"", s.replace('"', "#quot;"))
1124 } else {
1125 s.to_string()
1126 }
1127}
1128
1129impl ComponentGraph {
1130 fn has_node(&self, id: &str) -> bool {
1132 self.nodes.iter().any(|n| n.id == id)
1133 }
1134
1135 fn endpoints(&self) -> impl Iterator<Item = (&str, &str)> {
1137 self.edges.iter().map(|e| (e.from.as_str(), e.to.as_str()))
1138 }
1139
1140 pub fn adjacency(&self, id: &str) -> Vec<String> {
1144 neighbours(self.has_node(id), self.endpoints(), id, Direction::Both)
1145 }
1146
1147 pub fn predecessors(&self, id: &str) -> Vec<String> {
1149 neighbours(self.has_node(id), self.endpoints(), id, Direction::Incoming)
1150 }
1151
1152 pub fn successors(&self, id: &str) -> Vec<String> {
1154 neighbours(self.has_node(id), self.endpoints(), id, Direction::Outgoing)
1155 }
1156
1157 pub fn to_dot(&self) -> String {
1163 let mut dot = String::from("digraph ComponentGraph {\n");
1164 dot.push_str(" rankdir=LR;\n");
1165 dot.push_str(" node [shape=box];\n\n");
1166
1167 for node in &self.nodes {
1169 let shape = match node.component_type {
1170 ComponentType::Model => "ellipse",
1171 ComponentType::ReactionSystem => "box",
1172 };
1173
1174 let label = node.name.as_ref().unwrap_or(&node.id);
1175 dot.push_str(&format!(
1176 " \"{}\" [label=\"{}\" shape={}];\n",
1177 escape_dot(&node.id),
1178 escape_dot(label),
1179 shape
1180 ));
1181 }
1182
1183 dot.push('\n');
1184
1185 for edge in &self.edges {
1187 dot.push_str(&format!(
1188 " \"{}\" -> \"{}\" [label=\"{}\"];\n",
1189 escape_dot(&edge.from),
1190 escape_dot(&edge.to),
1191 escape_dot(&edge.coupling_type.to_string())
1192 ));
1193 }
1194
1195 dot.push_str("}\n");
1196 dot
1197 }
1198
1199 pub fn to_mermaid(&self) -> String {
1205 let mut mermaid = String::from("graph TD\n");
1213
1214 for node in &self.nodes {
1216 let shape = match node.component_type {
1217 ComponentType::Model => ("(", ")"),
1218 ComponentType::ReactionSystem => ("[", "]"),
1219 };
1220
1221 let label = node.name.as_ref().unwrap_or(&node.id);
1222 mermaid.push_str(&format!(
1223 " {}{}{}{}\n",
1224 node.id,
1225 shape.0,
1226 mermaid_label(label),
1227 shape.1
1228 ));
1229 }
1230
1231 for edge in &self.edges {
1233 mermaid.push_str(&format!(
1234 " {} -->|{}| {}\n",
1235 edge.from,
1236 mermaid_label(&edge.coupling_type.to_string()),
1237 edge.to
1238 ));
1239 }
1240
1241 mermaid
1242 }
1243
1244 pub fn to_json_graph(&self) -> String {
1250 render_json_graph(
1251 &self.nodes,
1252 |node| node.id.clone(),
1253 &self.edges,
1254 |edge| (edge.from.clone(), edge.to.clone()),
1255 |id| self.adjacency(id),
1256 )
1257 }
1258}
1259
1260impl ExpressionGraph {
1261 fn has_node(&self, name: &str) -> bool {
1263 self.nodes.iter().any(|n| n.name == name)
1264 }
1265
1266 fn endpoints(&self) -> impl Iterator<Item = (&str, &str)> {
1268 self.edges
1269 .iter()
1270 .map(|e| (e.source.as_str(), e.target.as_str()))
1271 }
1272
1273 pub fn adjacency(&self, name: &str) -> Vec<String> {
1277 neighbours(self.has_node(name), self.endpoints(), name, Direction::Both)
1278 }
1279
1280 pub fn predecessors(&self, name: &str) -> Vec<String> {
1282 neighbours(
1283 self.has_node(name),
1284 self.endpoints(),
1285 name,
1286 Direction::Incoming,
1287 )
1288 }
1289
1290 pub fn successors(&self, name: &str) -> Vec<String> {
1292 neighbours(
1293 self.has_node(name),
1294 self.endpoints(),
1295 name,
1296 Direction::Outgoing,
1297 )
1298 }
1299
1300 pub fn to_dot(&self) -> String {
1306 let mut dot = String::from("digraph ExpressionGraph {\n");
1307 dot.push_str(" rankdir=TB;\n");
1308 dot.push_str(" node [shape=ellipse];\n\n");
1309
1310 for node in &self.nodes {
1314 let shape = match node.kind {
1315 VariableKind::State | VariableKind::Algebraic => "ellipse",
1316 VariableKind::Parameter => "box",
1317 VariableKind::Observed => "diamond",
1318 VariableKind::Brownian => "doubleoctagon",
1319 VariableKind::Discrete => "hexagon",
1320 VariableKind::Species => "circle",
1321 };
1322
1323 dot.push_str(&format!(
1324 " \"{}\" [label=\"{}\" shape={}];\n",
1325 escape_dot(&node.name),
1326 escape_dot(&node.name),
1327 shape
1328 ));
1329 }
1330
1331 dot.push('\n');
1332
1333 for edge in &self.edges {
1335 let label = match edge.relationship {
1336 DependencyRelationship::Additive => "additive",
1337 DependencyRelationship::Multiplicative => "mult",
1338 DependencyRelationship::Rate => "rate",
1339 DependencyRelationship::Stoichiometric => "stoich",
1340 };
1341 dot.push_str(&format!(
1342 " \"{}\" -> \"{}\" [label=\"{}\"];\n",
1343 escape_dot(&edge.source),
1344 escape_dot(&edge.target),
1345 label
1346 ));
1347 }
1348
1349 dot.push_str("}\n");
1350 dot
1351 }
1352
1353 pub fn to_mermaid(&self) -> String {
1359 let mut mermaid = String::from("graph TD\n");
1360
1361 for node in &self.nodes {
1363 let (shape_start, shape_end) = match node.kind {
1364 VariableKind::State | VariableKind::Algebraic => ("(", ")"),
1365 VariableKind::Parameter => ("[", "]"),
1366 VariableKind::Observed => ("{", "}"),
1367 VariableKind::Brownian => ("{{", "}}"),
1368 VariableKind::Discrete => ("[/", "/]"),
1369 VariableKind::Species => ("((", "))"),
1370 };
1371
1372 mermaid.push_str(&format!(
1373 " {}{}{}{}\n",
1374 node.name,
1375 shape_start,
1376 mermaid_label(&node.name),
1377 shape_end
1378 ));
1379 }
1380
1381 for edge in &self.edges {
1383 mermaid.push_str(&format!(" {} --> {}\n", edge.source, edge.target));
1384 }
1385
1386 mermaid
1387 }
1388
1389 pub fn to_json_graph(&self) -> String {
1395 render_json_graph(
1396 &self.nodes,
1397 |node| node.name.clone(),
1398 &self.edges,
1399 |edge| (edge.source.clone(), edge.target.clone()),
1400 |name| self.adjacency(name),
1401 )
1402 }
1403}
1404
1405pub trait Graph {
1418 fn render_dot(&self) -> String;
1420 fn render_mermaid(&self) -> String;
1422 fn render_json_graph(&self) -> String;
1424}
1425
1426impl Graph for ComponentGraph {
1427 fn render_dot(&self) -> String {
1428 ComponentGraph::to_dot(self)
1429 }
1430 fn render_mermaid(&self) -> String {
1431 ComponentGraph::to_mermaid(self)
1432 }
1433 fn render_json_graph(&self) -> String {
1434 ComponentGraph::to_json_graph(self)
1435 }
1436}
1437
1438impl Graph for ExpressionGraph {
1439 fn render_dot(&self) -> String {
1440 ExpressionGraph::to_dot(self)
1441 }
1442 fn render_mermaid(&self) -> String {
1443 ExpressionGraph::to_mermaid(self)
1444 }
1445 fn render_json_graph(&self) -> String {
1446 ExpressionGraph::to_json_graph(self)
1447 }
1448}
1449
1450pub fn to_dot<G: Graph>(graph: &G) -> String {
1456 graph.render_dot()
1457}
1458
1459pub fn to_mermaid<G: Graph>(graph: &G) -> String {
1461 graph.render_mermaid()
1462}
1463
1464pub fn to_json_graph<G: Graph>(graph: &G) -> String {
1468 graph.render_json_graph()
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474 use crate::test_support::test_file;
1475 use crate::{Expr, ExpressionNode as ExprNode, Model, ReactionSystem};
1476 use indexmap::IndexMap;
1477
1478 fn test_model(name: &str) -> Model {
1480 Model {
1481 name: Some(name.to_string()),
1482 ..Default::default()
1483 }
1484 }
1485
1486 #[test]
1487 fn test_component_graph_empty() {
1488 let esm_file = test_file();
1489
1490 let graph = component_graph(&esm_file);
1491 assert_eq!(graph.nodes.len(), 0);
1492 assert_eq!(graph.edges.len(), 0);
1493 }
1494
1495 #[test]
1496 fn test_component_graph_with_models() {
1497 let mut models = IndexMap::new();
1498 models.insert("model1".to_string(), test_model("Test Model 1"));
1499 models.insert("model2".to_string(), test_model("Test Model 2"));
1500
1501 let esm_file = EsmFile {
1502 models: Some(models),
1503 ..test_file()
1504 };
1505
1506 let graph = component_graph(&esm_file);
1507 assert_eq!(graph.nodes.len(), 2);
1508
1509 let node1 = graph.nodes.iter().find(|n| n.id == "model1").unwrap();
1510 assert_eq!(node1.component_type, ComponentType::Model);
1511 assert_eq!(node1.name, Some("Test Model 1".to_string()));
1512
1513 let node2 = graph.nodes.iter().find(|n| n.id == "model2").unwrap();
1514 assert_eq!(node2.component_type, ComponentType::Model);
1515 assert_eq!(node2.name, Some("Test Model 2".to_string()));
1516 }
1517
1518 #[test]
1519 fn test_component_exists() {
1520 let mut models = IndexMap::new();
1521 models.insert("test_model".to_string(), test_model("Test Model"));
1522
1523 let esm_file = EsmFile {
1524 models: Some(models),
1525 ..test_file()
1526 };
1527
1528 assert!(component_exists(&esm_file, "test_model"));
1529 assert!(!component_exists(&esm_file, "nonexistent"));
1530 }
1531
1532 #[test]
1533 fn test_get_component_type() {
1534 let mut models = IndexMap::new();
1535 models.insert("test_model".to_string(), test_model("Test Model"));
1536
1537 let mut reaction_systems = IndexMap::new();
1538 reaction_systems.insert("test_rs".to_string(), ReactionSystem::default());
1539
1540 let esm_file = EsmFile {
1541 models: Some(models),
1542 reaction_systems: Some(reaction_systems),
1543 ..test_file()
1544 };
1545
1546 assert_eq!(
1547 get_component_type(&esm_file, "test_model"),
1548 Some(ComponentType::Model)
1549 );
1550 assert_eq!(
1551 get_component_type(&esm_file, "test_rs"),
1552 Some(ComponentType::ReactionSystem)
1553 );
1554 assert_eq!(get_component_type(&esm_file, "nonexistent"), None);
1555 }
1556
1557 #[test]
1558 fn test_expression_graph() {
1559 let expr = Expr::operator(ExprNode {
1560 op: "+".to_string(),
1561 args: vec![Expr::Variable("x".to_string()), Expr::Number(1.0)],
1562 wrt: None,
1563 dim: None,
1564 ..Default::default()
1565 });
1566
1567 let graph = expression_graph(&expr);
1568 assert_eq!(graph.nodes.len(), 2);
1572 assert_eq!(graph.edges.len(), 1);
1573
1574 let result = graph
1575 .nodes
1576 .iter()
1577 .find(|n| n.name == "expr_result")
1578 .expect("synthetic result node");
1579 assert_eq!(result.kind, VariableKind::Observed);
1580 assert_eq!(result.system, "default");
1581
1582 let x = graph
1583 .nodes
1584 .iter()
1585 .find(|n| n.name == "x")
1586 .expect("free variable node");
1587 assert_eq!(x.kind, VariableKind::Parameter);
1588
1589 assert_eq!(graph.edges[0].source, "x");
1590 assert_eq!(graph.edges[0].target, "expr_result");
1591 assert_eq!(
1592 graph.edges[0].relationship,
1593 DependencyRelationship::Multiplicative
1594 );
1595 assert_eq!(graph.edges[0].equation_index, Some(NON_EQUATION_INDEX));
1598 }
1599
1600 #[test]
1601 fn test_component_graph_to_dot() {
1602 let graph = ComponentGraph {
1603 nodes: vec![ComponentNode {
1604 id: "model1".to_string(),
1605 component_type: ComponentType::Model,
1606 name: Some("Test Model".to_string()),
1607 metadata: ComponentMetadata::default(),
1608 }],
1609 edges: vec![],
1610 };
1611
1612 let dot = graph.to_dot();
1613 assert!(dot.contains("digraph ComponentGraph"));
1614 assert!(dot.contains("model1"));
1615 assert!(dot.contains("Test Model"));
1616 }
1617
1618 #[test]
1619 fn test_component_graph_to_mermaid() {
1620 let graph = ComponentGraph {
1621 nodes: vec![ComponentNode {
1622 id: "model1".to_string(),
1623 component_type: ComponentType::Model,
1624 name: Some("Test Model".to_string()),
1625 metadata: ComponentMetadata::default(),
1626 }],
1627 edges: vec![],
1628 };
1629
1630 let mermaid = graph.to_mermaid();
1631 assert!(mermaid.contains("graph TD"));
1632 assert!(mermaid.contains("model1(Test Model)"));
1633 }
1634
1635 #[test]
1636 fn test_expression_graph_to_mermaid() {
1637 let expr = Expr::operator(ExprNode {
1638 op: "+".to_string(),
1639 args: vec![Expr::Variable("x".to_string()), Expr::Number(1.0)],
1640 wrt: None,
1641 dim: None,
1642 ..Default::default()
1643 });
1644
1645 let graph = expression_graph(&expr);
1646 let mermaid = graph.to_mermaid();
1647
1648 assert!(mermaid.contains("graph TD"));
1649 assert!(mermaid.contains("x[x]")); assert!(!mermaid.contains("const_")); assert!(!mermaid.contains("{+}")); assert!(mermaid.contains("expr_result{expr_result}"));
1655 assert!(mermaid.contains("x --> expr_result"));
1656 }
1657
1658 #[test]
1659 fn test_component_graph_variable_map_edge_extraction() {
1660 let mut models = IndexMap::new();
1661 models.insert("source".to_string(), test_model("Source System"));
1662 models.insert("target".to_string(), test_model("Target System"));
1663
1664 let coupling_entries = vec![crate::CouplingEntry::VariableMap {
1665 from: "source.var".to_string(),
1666 to: "target.param".to_string(),
1667 transform: crate::types::VariableMapTransform::Named("identity".to_string()),
1668 factor: None,
1669 lifting: None,
1670 description: None,
1671 }];
1672
1673 let esm_file = EsmFile {
1674 models: Some(models),
1675 coupling: Some(coupling_entries),
1676 ..test_file()
1677 };
1678
1679 let graph = component_graph(&esm_file);
1680
1681 assert_eq!(graph.nodes.len(), 2);
1683
1684 assert_eq!(graph.edges.len(), 1);
1686
1687 let edge = &graph.edges[0];
1688
1689 assert_eq!(edge.from, "source"); assert_eq!(edge.to, "target"); assert_eq!(edge.coupling_type, CouplingEdgeKind::VariableMap);
1693 assert_eq!(edge.label, "var");
1695 }
1696
1697 #[test]
1700 fn test_component_graph_variable_map_requires_scoped_endpoints() {
1701 let mut models = IndexMap::new();
1702 for id in ["source", "target"] {
1703 models.insert(id.to_string(), test_model(id));
1704 }
1705
1706 let esm_file = EsmFile {
1707 coupling: Some(vec![crate::CouplingEntry::VariableMap {
1708 from: "source".to_string(),
1709 to: "target".to_string(),
1710 transform: crate::types::VariableMapTransform::Named("identity".to_string()),
1711 factor: None,
1712 lifting: None,
1713 description: None,
1714 }]),
1715 models: Some(models),
1716 ..test_file()
1717 };
1718
1719 let graph = component_graph(&esm_file);
1720 assert_eq!(graph.nodes.len(), 2);
1721 assert!(graph.edges.is_empty());
1722 }
1723
1724 #[test]
1727 fn test_data_sources_contribute_no_nodes() {
1728 let json = r#"{
1729 "esm": "1.0.0",
1730 "metadata": { "name": "ds" },
1731 "data_sources": {
1732 "GEOSFP": {
1733 "kind": "grid",
1734 "source": { "url_template": "file:///data/GEOSFP/{date:%Y%m%d_%H%M}.nc" }
1735 }
1736 }
1737 }"#;
1738 let esm_file: EsmFile = serde_json::from_str(json).expect("parses");
1739
1740 let graph = component_graph(&esm_file);
1741 assert!(graph.nodes.is_empty());
1742 assert!(graph.edges.is_empty());
1743 assert_eq!(get_component_type(&esm_file, "GEOSFP"), None);
1744 assert!(!component_exists(&esm_file, "GEOSFP"));
1745 }
1746
1747 #[test]
1749 fn test_component_graph_closure() {
1750 let mut models = IndexMap::new();
1751 for id in ["source", "target"] {
1752 models.insert(id.to_string(), test_model(id));
1753 }
1754
1755 let esm_file = EsmFile {
1756 coupling: Some(vec![crate::CouplingEntry::VariableMap {
1757 from: "source.var".to_string(),
1758 to: "target.param".to_string(),
1759 transform: crate::types::VariableMapTransform::Named("identity".to_string()),
1760 factor: None,
1761 lifting: None,
1762 description: None,
1763 }]),
1764 models: Some(models),
1765 ..test_file()
1766 };
1767
1768 let graph = component_graph(&esm_file);
1769 assert_eq!(graph.adjacency("source"), vec!["target".to_string()]);
1770 assert_eq!(graph.successors("source"), vec!["target".to_string()]);
1771 assert!(graph.predecessors("source").is_empty());
1772 assert_eq!(graph.adjacency("target"), vec!["source".to_string()]);
1773 assert_eq!(graph.predecessors("target"), vec!["source".to_string()]);
1774 assert!(graph.successors("target").is_empty());
1775 assert!(graph.adjacency("nonexistent").is_empty());
1777 }
1778
1779 #[test]
1782 fn test_to_json_graph_is_an_adjacency_list() {
1783 let mut models = IndexMap::new();
1784 for id in ["source", "target"] {
1785 models.insert(id.to_string(), test_model(id));
1786 }
1787
1788 let esm_file = EsmFile {
1789 coupling: Some(vec![crate::CouplingEntry::VariableMap {
1790 from: "source.var".to_string(),
1791 to: "target.param".to_string(),
1792 transform: crate::types::VariableMapTransform::Named("identity".to_string()),
1793 factor: None,
1794 lifting: None,
1795 description: None,
1796 }]),
1797 models: Some(models),
1798 ..test_file()
1799 };
1800
1801 let json: serde_json::Value =
1802 serde_json::from_str(&component_graph(&esm_file).to_json_graph()).expect("valid JSON");
1803
1804 let mut keys: Vec<&String> = json.as_object().expect("object").keys().collect();
1805 keys.sort();
1806 assert_eq!(keys, vec!["adjacency", "edges", "nodes"]);
1807
1808 let nodes = json["nodes"].as_array().expect("nodes array");
1809 assert_eq!(nodes[0]["id"], "source");
1810 let edges = json["edges"].as_array().expect("edges array");
1811 assert_eq!(edges[0]["source"], "source");
1812 assert_eq!(edges[0]["target"], "target");
1813 assert_eq!(edges[0]["data"]["coupling_type"], "variable_map");
1814 assert_eq!(json["adjacency"]["source"][0], "target");
1815 assert_eq!(json["adjacency"]["target"][0], "source");
1816 }
1817
1818 #[test]
1822 fn canonical_renderers_match_the_inherent_methods() {
1823 let esm_file = EsmFile {
1824 models: Some(IndexMap::from([("m".to_string(), test_model("M"))])),
1825 ..test_file()
1826 };
1827
1828 let cg = component_graph(&esm_file);
1829 assert_eq!(super::to_dot(&cg), cg.to_dot());
1830 assert_eq!(super::to_mermaid(&cg), cg.to_mermaid());
1831 assert_eq!(super::to_json_graph(&cg), cg.to_json_graph());
1832
1833 let eg = expression_graph(&esm_file);
1834 assert_eq!(super::to_dot(&eg), eg.to_dot());
1835 assert_eq!(super::to_mermaid(&eg), eg.to_mermaid());
1836 assert_eq!(super::to_json_graph(&eg), eg.to_json_graph());
1837 }
1838}