fix(nym-api): refactor simulation API to remove redundant data structures

- Remove old_method/new_method nested objects from NodeMethodComparison
- Use direct fields for production_performance and simulated_performance
- Update build_node_comparisons_from_single_dataset to create cleaner structure
- Fix calculate_summary_statistics to work with new data model
- Update all tests to match new structure

The API now returns a single dataset with production performance values
included directly, rather than separate old/new method objects.

- Deduplicate routes, to control for deterministic routing
This commit is contained in:
durch
2025-06-25 00:39:51 +02:00
parent a127a303f1
commit d842e7b89b
6 changed files with 60 additions and 83 deletions
Generated
+1 -1
View File
@@ -4739,7 +4739,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.67"
version = "1.1.68"
dependencies = [
"anyhow",
"async-trait",
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.68"
version = "1.1.69"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
+48 -78
View File
@@ -607,39 +607,15 @@ fn build_node_comparisons_from_single_dataset(
_ => None,
};
// Create comparison with old and new method data
let old_method = perf.production_performance.map(|prod_perf| NodePerformanceData {
node_id: perf.node_id,
node_type: perf.node_type.clone(),
identity_key: perf.identity_key.clone(),
reliability_score: prod_perf,
positive_samples: 0,
negative_samples: 0,
work_factor: perf.work_factor,
calculation_method: "production".to_string(),
calculated_at: perf.calculated_at,
production_performance: Some(prod_perf),
});
let new_method = NodePerformanceData {
node_id: perf.node_id,
node_type: perf.node_type.clone(),
identity_key: perf.identity_key.clone(),
reliability_score: perf.reliability_score,
positive_samples: perf.positive_samples,
negative_samples: perf.negative_samples,
work_factor: perf.work_factor,
calculation_method: perf.calculation_method,
calculated_at: perf.calculated_at,
production_performance: perf.production_performance,
};
comparisons.push(NodeMethodComparison {
node_id: perf.node_id,
node_type: perf.node_type,
identity_key: perf.identity_key,
old_method,
new_method: Some(new_method),
production_performance: perf.production_performance,
simulated_performance: perf.reliability_score,
positive_samples: perf.positive_samples,
negative_samples: perf.negative_samples,
work_factor: perf.work_factor,
reliability_difference,
performance_delta_percentage,
ranking_old_method: ranking_old,
@@ -661,12 +637,13 @@ fn calculate_summary_statistics(comparisons: &[NodeMethodComparison]) -> Compari
let mut max_degradation: f64 = 0.0;
for comparison in comparisons {
if let Some(old) = &comparison.old_method {
reliabilities_old.push(old.reliability_score);
}
if let Some(new) = &comparison.new_method {
reliabilities_new.push(new.reliability_score);
// Collect production (old method) reliability values
if let Some(old) = comparison.production_performance {
reliabilities_old.push(old);
}
// Collect simulated (new method) reliability values
reliabilities_new.push(comparison.simulated_performance);
if let Some(diff) = comparison.reliability_difference {
if diff > 0.001 {
@@ -882,52 +859,35 @@ mod tests {
use super::*;
use crate::{simulation_api::models::{NodeMethodComparison, NodePerformanceData}};
fn create_test_performance_data(
fn create_test_comparison(
node_id: NodeId,
reliability: f64,
method: &str,
) -> NodePerformanceData {
NodePerformanceData {
production_perf: Option<f64>,
simulated_perf: f64,
reliability_diff: Option<f64>,
perf_delta_pct: Option<f64>,
) -> NodeMethodComparison {
NodeMethodComparison {
node_id,
node_type: "mixnode".to_string(),
identity_key: Some("test_key".to_string()),
reliability_score: reliability,
identity_key: Some(format!("key{}", node_id)),
production_performance: production_perf,
simulated_performance: simulated_perf,
positive_samples: 100,
negative_samples: 10,
work_factor: Some(1.0),
calculation_method: method.to_string(),
calculated_at: 1234567890,
production_performance: None,
reliability_difference: reliability_diff,
performance_delta_percentage: perf_delta_pct,
ranking_old_method: None,
ranking_new_method: None,
ranking_delta: None,
}
}
#[test]
fn test_calculate_summary_statistics_basic() {
let comparisons = vec![
NodeMethodComparison {
node_id: 1,
node_type: "mixnode".to_string(),
identity_key: Some("key1".to_string()),
old_method: Some(create_test_performance_data(1, 80.0, "old")),
new_method: Some(create_test_performance_data(1, 90.0, "new")),
reliability_difference: Some(10.0),
performance_delta_percentage: Some(12.5),
ranking_old_method: Some(2),
ranking_new_method: Some(1),
ranking_delta: Some(-1),
},
NodeMethodComparison {
node_id: 2,
node_type: "mixnode".to_string(),
identity_key: Some("key2".to_string()),
old_method: Some(create_test_performance_data(2, 70.0, "old")),
new_method: Some(create_test_performance_data(2, 65.0, "new")),
reliability_difference: Some(-5.0),
performance_delta_percentage: Some(-7.14),
ranking_old_method: Some(1),
ranking_new_method: Some(2),
ranking_delta: Some(1),
},
create_test_comparison(1, Some(80.0), 90.0, Some(10.0), Some(12.5)),
create_test_comparison(2, Some(70.0), 65.0, Some(-5.0), Some(-7.14)),
];
let stats = calculate_summary_statistics(&comparisons);
@@ -1056,8 +1016,8 @@ mod tests {
// Find node 1 comparison
let node1_comparison = comparisons.iter().find(|c| c.node_id == 1).unwrap();
assert!(node1_comparison.old_method.is_some());
assert!(node1_comparison.new_method.is_some());
assert_eq!(node1_comparison.production_performance, Some(80.0));
assert_eq!(node1_comparison.simulated_performance, 90.0);
assert_eq!(node1_comparison.reliability_difference, Some(10.0)); // 90 - 80
assert_eq!(node1_comparison.performance_delta_percentage, Some(12.5)); // (90-80)/80 * 100
assert_eq!(node1_comparison.ranking_old_method, Some(1)); // 80% is best among nodes with production data
@@ -1065,16 +1025,16 @@ mod tests {
// Find node 2 comparison
let node2_comparison = comparisons.iter().find(|c| c.node_id == 2).unwrap();
assert!(node2_comparison.old_method.is_some());
assert!(node2_comparison.new_method.is_some());
assert_eq!(node2_comparison.production_performance, Some(70.0));
assert_eq!(node2_comparison.simulated_performance, 65.0);
assert_eq!(node2_comparison.reliability_difference, Some(-5.0)); // 65 - 70
assert_eq!(node2_comparison.ranking_old_method, Some(2)); // 70% is second
assert_eq!(node2_comparison.ranking_new_method, Some(3)); // 65% is worst
// Find node 3 comparison (no production data)
let node3_comparison = comparisons.iter().find(|c| c.node_id == 3).unwrap();
assert!(node3_comparison.old_method.is_none());
assert!(node3_comparison.new_method.is_some());
assert_eq!(node3_comparison.production_performance, None);
assert_eq!(node3_comparison.simulated_performance, 85.0);
assert_eq!(node3_comparison.reliability_difference, None);
assert_eq!(node3_comparison.ranking_old_method, None); // No production ranking
assert_eq!(node3_comparison.ranking_new_method, Some(2)); // 85% is second best
@@ -1096,8 +1056,18 @@ mod tests {
available_methods: vec!["old".to_string(), "new".to_string()],
},
node_performance: vec![
create_test_performance_data(1, 80.0, "old"),
create_test_performance_data(1, 90.0, "new"),
NodePerformanceData {
node_id: 1,
node_type: "mixnode".to_string(),
identity_key: Some("test_key".to_string()),
reliability_score: 90.0,
positive_samples: 100,
negative_samples: 10,
work_factor: Some(1.0),
calculation_method: "new".to_string(),
calculated_at: 1234567890,
production_performance: Some(80.0),
},
],
performance_comparisons: vec![PerformanceComparisonData {
node_id: 1,
@@ -1120,7 +1090,7 @@ mod tests {
assert!(csv.contains(
"data_type,node_id,node_type,reliability_score,reward_amount,calculation_method"
));
assert!(csv.contains("performance,1,mixnode,80,"));
assert!(csv.contains("performance,1,mixnode,90,"));
assert!(csv.contains("performance,1,mixnode,90,")); // New method score
assert!(csv.contains("performance_comparison,1,mixnode,,80,")); // Performance comparison score
}
}
+8 -2
View File
@@ -181,8 +181,14 @@ pub struct NodeMethodComparison {
pub node_id: NodeId,
pub node_type: String,
pub identity_key: Option<String>,
pub old_method: Option<NodePerformanceData>,
pub new_method: Option<NodePerformanceData>,
/// Production performance (old method) value
pub production_performance: Option<f64>,
/// Simulated performance (new method) value
pub simulated_performance: f64,
/// Sample counts from route-based calculation
pub positive_samples: u32,
pub negative_samples: u32,
pub work_factor: Option<f64>,
pub reliability_difference: Option<f64>, // new - old
pub performance_delta_percentage: Option<f64>, // (new - old) / old * 100
pub ranking_old_method: Option<i64>,
+1 -1
View File
@@ -1556,7 +1556,7 @@ impl StorageManager {
let db_routes = sqlx::query_as!(
RawRouteData,
r#"
SELECT
SELECT DISTINCT,
layer1 as "layer1",
layer2 as "layer2",
layer3 as "layer3",
+1
View File
@@ -333,5 +333,6 @@ fn mixnet_debug_config(min_gateway_performance: u8) -> nym_client_core::config::
debug_config.cover_traffic.disable_loop_cover_traffic_stream = true;
debug_config.topology.minimum_gateway_performance = min_gateway_performance;
debug_config.traffic.deterministic_route_selection = true;
debug_config.traffic.average_packet_delay = Duration::from_millis(0);
debug_config
}