Add complete simulation API layer for reward method comparison
This completes Phase 3 of the simulation system implementation: - Add comprehensive REST API endpoints for simulation data access - Implement /v1/simulation/* routes with full CRUD operations - Support JSON/CSV export for external analysis - Add statistical comparison between old vs new methods - Provide node performance history tracking - Include proper error handling and response formatting - Simplify simulation coordinator to remove unused complex return types - Clean up dead code while maintaining all functionality - Pass clippy with no warnings The simulation API provides complete access to: - Simulation epoch listing and details - Method comparison analytics (old 24h vs new 1h) - Node performance analysis across epochs - Route reliability statistics - Export capabilities for further analysis All simulation data is persisted and accessible via REST endpoints.
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as count FROM simulated_node_performance WHERE simulated_epoch_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0557e64c547e147ef7eef713b49c62afde62b3b04ff817ae372ffe9fbeaee6b8"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as count FROM simulated_reward_epochs",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1411660a6456f6f5ca141db10d9a54857fc8b4a661e09ae6719d84ac2e77cf6d"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", epoch_id as \"epoch_id!: u32\", calculation_method as \"calculation_method!\", \n start_timestamp as \"start_timestamp!\", end_timestamp as \"end_timestamp!\", \n description, created_at as \"created_at!\"\n FROM simulated_reward_epochs\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id!: u32",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "start_timestamp!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "end_timestamp!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1e4daea4d5f87938cf98cb2b6facc5802e79b06f3d0d41f6467844265b26d0f7"
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT snp.id as \"id!\", snp.simulated_epoch_id as \"simulated_epoch_id!\", \n snp.node_id as \"node_id!: NodeId\", snp.node_type as \"node_type!\", \n snp.identity_key, snp.reliability_score as \"reliability_score!\", \n snp.positive_samples as \"positive_samples!: u32\", snp.negative_samples as \"negative_samples!: u32\", \n snp.final_fail_sequence as \"final_fail_sequence!: u32\", snp.work_factor, \n snp.calculation_method as \"calculation_method!\", snp.calculated_at as \"calculated_at!\"\n FROM simulated_node_performance snp\n JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id\n WHERE snp.node_id = ?\n ORDER BY sre.epoch_id DESC, snp.calculation_method\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "final_fail_sequence!: u32",
|
||||
"ordinal": 8,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 9,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3ffc479fe6913b137a59bf4c2801cb26191b2d606a359ffc165032e67975d40a"
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT sra.id as \"id!\", sra.simulated_epoch_id as \"simulated_epoch_id!\", \n sra.calculation_method as \"calculation_method!\", sra.total_routes_analyzed as \"total_routes_analyzed!: u32\", \n sra.successful_routes as \"successful_routes!: u32\", sra.failed_routes as \"failed_routes!: u32\", \n sra.average_route_reliability, sra.time_window_hours as \"time_window_hours!: u32\", \n sra.analysis_parameters, sra.calculated_at as \"calculated_at!\"\n FROM simulated_route_analysis sra\n JOIN simulated_reward_epochs sre ON sra.simulated_epoch_id = sre.id\n WHERE sre.epoch_id = ? AND sra.calculation_method = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "total_routes_analyzed!: u32",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "successful_routes!: u32",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "failed_routes!: u32",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "average_route_reliability",
|
||||
"ordinal": 6,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "time_window_hours!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "analysis_parameters",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4e2338362058e4356ac639fdf806e827761f879cab723cb2a649bc76b176fdad"
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT snp.id as \"id!\", snp.simulated_epoch_id as \"simulated_epoch_id!\", \n snp.node_id as \"node_id!: NodeId\", snp.node_type as \"node_type!\", \n snp.identity_key, snp.reliability_score as \"reliability_score!\", \n snp.positive_samples as \"positive_samples!: u32\", snp.negative_samples as \"negative_samples!: u32\", \n snp.final_fail_sequence as \"final_fail_sequence!: u32\", snp.work_factor, \n snp.calculation_method as \"calculation_method!\", snp.calculated_at as \"calculated_at!\"\n FROM simulated_node_performance snp\n JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id\n WHERE sre.epoch_id = ? AND snp.calculation_method = ?\n ORDER BY snp.node_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_id!: NodeId",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "node_type!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "identity_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reliability_score!",
|
||||
"ordinal": 5,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "positive_samples!: u32",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "negative_samples!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "final_fail_sequence!: u32",
|
||||
"ordinal": 8,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "work_factor",
|
||||
"ordinal": 9,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bc1b243cbd22a34e15dfefb84f99dcb421d336c53d9f35c66fb24fe5e579f0fa"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT DISTINCT calculation_method FROM simulated_reward_epochs WHERE epoch_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "calculation_method",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c9796e61c49a29ebd42dec46fb047f7a7e2f80aa4db4f963146cf56864a62347"
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id as \"id!\", simulated_epoch_id as \"simulated_epoch_id!\", \n calculation_method as \"calculation_method!\", total_routes_analyzed as \"total_routes_analyzed!: u32\", \n successful_routes as \"successful_routes!: u32\", failed_routes as \"failed_routes!: u32\", \n average_route_reliability, time_window_hours as \"time_window_hours!: u32\", \n analysis_parameters, calculated_at as \"calculated_at!\"\n FROM simulated_route_analysis\n WHERE simulated_epoch_id = ?\n ORDER BY calculation_method\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "simulated_epoch_id!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "total_routes_analyzed!: u32",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "successful_routes!: u32",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "failed_routes!: u32",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "average_route_reliability",
|
||||
"ordinal": 6,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "time_window_hours!: u32",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "analysis_parameters",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "calculated_at!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "db2b71d1dd33022d5c960053d6a0f5d7ccf52de6775d4f57744d1401d44fee62"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!\", epoch_id as \"epoch_id!: u32\", calculation_method as \"calculation_method!\", \n start_timestamp as \"start_timestamp!\", end_timestamp as \"end_timestamp!\", \n description, created_at as \"created_at!\"\n FROM simulated_reward_epochs \n ORDER BY created_at DESC \n LIMIT ? OFFSET ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id!: u32",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "calculation_method!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "start_timestamp!",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "end_timestamp!",
|
||||
"ordinal": 4,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dbc52e443ab600516b4f96ab2904d6b6379c06cfa1ff8598c29b67a2fe37055d"
|
||||
}
|
||||
@@ -41,22 +41,6 @@ impl Default for SimulationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Results from a single calculation method
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MethodResults {
|
||||
pub method_name: String,
|
||||
pub node_performance: Vec<SimulatedNodePerformance>,
|
||||
pub rewards: Vec<SimulatedReward>,
|
||||
pub route_analysis: SimulatedRouteAnalysis,
|
||||
}
|
||||
|
||||
/// Complete simulation results containing both methods
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SimulationResults {
|
||||
pub epoch_id: i64,
|
||||
pub old_method: Option<MethodResults>,
|
||||
pub new_method: Option<MethodResults>,
|
||||
}
|
||||
|
||||
/// Main simulation coordinator
|
||||
pub struct SimulationCoordinator<'a> {
|
||||
@@ -76,7 +60,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
rewarded_set: &EpochRewardedSet,
|
||||
reward_params: RewardingParams,
|
||||
current_epoch_id: u32,
|
||||
) -> Result<SimulationResults, RewardingError> {
|
||||
) -> Result<(), RewardingError> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let end_timestamp = now.unix_timestamp();
|
||||
let start_timestamp = end_timestamp - (24 * 3600); // 24 hours ago for baseline
|
||||
@@ -98,12 +82,6 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
let mut results = SimulationResults {
|
||||
epoch_id: epoch_db_id,
|
||||
old_method: None,
|
||||
new_method: None,
|
||||
};
|
||||
|
||||
// Run old method simulation (24h cache-based)
|
||||
if self.config.run_both_methods {
|
||||
match self.run_old_method_simulation(
|
||||
@@ -113,8 +91,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
epoch_db_id,
|
||||
end_timestamp,
|
||||
).await {
|
||||
Ok(old_results) => {
|
||||
results.old_method = Some(old_results);
|
||||
Ok(_) => {
|
||||
info!("Old method simulation completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -131,8 +108,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
epoch_db_id,
|
||||
end_timestamp,
|
||||
).await {
|
||||
Ok(new_results) => {
|
||||
results.new_method = Some(new_results);
|
||||
Ok(_) => {
|
||||
info!("New method simulation completed successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -140,14 +116,8 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Simulation completed for epoch {}. Methods run: old={}, new={}",
|
||||
current_epoch_id,
|
||||
results.old_method.is_some(),
|
||||
results.new_method.is_some()
|
||||
);
|
||||
|
||||
Ok(results)
|
||||
info!("Simulation completed for epoch {}", current_epoch_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run simulation using old method (24h cache-based)
|
||||
@@ -158,7 +128,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
end_timestamp: i64,
|
||||
) -> Result<MethodResults, RewardingError> {
|
||||
) -> Result<(), RewardingError> {
|
||||
debug!("Running old method simulation (24h cache-based)");
|
||||
|
||||
// Get 24h performance data using existing cache-based method
|
||||
@@ -239,12 +209,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
Ok(MethodResults {
|
||||
method_name: "old".to_string(),
|
||||
node_performance,
|
||||
rewards,
|
||||
route_analysis,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run simulation using new method (1h route-based)
|
||||
@@ -254,7 +219,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
reward_params: RewardingParams,
|
||||
epoch_db_id: i64,
|
||||
end_timestamp: i64,
|
||||
) -> Result<MethodResults, RewardingError> {
|
||||
) -> Result<(), RewardingError> {
|
||||
debug!("Running new method simulation ({}h route-based)", self.config.new_method_time_window_hours);
|
||||
|
||||
let time_window_secs = (self.config.new_method_time_window_hours as i64) * 3600;
|
||||
@@ -347,12 +312,7 @@ impl<'a> SimulationCoordinator<'a> {
|
||||
.await
|
||||
.map_err(|e| RewardingError::DatabaseError { source: e.into() })?;
|
||||
|
||||
Ok(MethodResults {
|
||||
method_name: "new".to_string(),
|
||||
node_performance,
|
||||
rewards,
|
||||
route_analysis,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate rewards for nodes using the provided performance data
|
||||
@@ -480,18 +440,18 @@ impl EpochAdvancer {
|
||||
reward_params: RewardingParams,
|
||||
current_epoch_id: u32,
|
||||
simulation_config: SimulationConfig,
|
||||
) -> Result<Option<SimulationResults>, RewardingError> {
|
||||
) -> Result<(), RewardingError> {
|
||||
let coordinator = SimulationCoordinator::new(&self.storage, simulation_config);
|
||||
|
||||
match coordinator.run_simulation(self, rewarded_set, reward_params, current_epoch_id).await {
|
||||
Ok(results) => {
|
||||
Ok(()) => {
|
||||
info!("Simulation completed successfully for epoch {}", current_epoch_id);
|
||||
Ok(Some(results))
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Simulation failed for epoch {}: {}", current_epoch_id, e);
|
||||
// Don't fail the entire epoch operation due to simulation failure
|
||||
Ok(None)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) mod node_describe_cache;
|
||||
pub(crate) mod node_status_api;
|
||||
pub(crate) mod nym_contract_cache;
|
||||
pub(crate) mod nym_nodes;
|
||||
pub(crate) mod simulation_api;
|
||||
mod status;
|
||||
pub(crate) mod support;
|
||||
mod unstable_routes;
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Handlers for simulation API endpoints
|
||||
|
||||
use crate::simulation_api::models::{
|
||||
SimulationApiError, SimulationEpochDetails, SimulationEpochSummary, SimulationEpochsResponse,
|
||||
SimulationListQuery, NodeComparisonQuery, MethodComparisonResponse, NodeMethodComparison,
|
||||
ComparisonSummaryStats, RouteAnalysisComparison, NodePerformanceData, NodeRewardData,
|
||||
RouteAnalysisData, ExportFormat,
|
||||
};
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Json, Response};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
type SimulationResult<T> = Result<T, SimulationApiError>;
|
||||
type AxumResult<T> = Result<T, (StatusCode, Json<SimulationApiError>)>;
|
||||
|
||||
/// Create the simulation API router
|
||||
pub(crate) fn simulation_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/epochs", get(list_simulation_epochs))
|
||||
.route("/epochs/:epoch_id", get(get_simulation_epoch_details))
|
||||
.route("/epochs/:epoch_id/comparison", get(compare_methods))
|
||||
.route("/epochs/:epoch_id/export", get(export_simulation_data))
|
||||
.route("/nodes/:node_id/performance", get(get_node_performance_history))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct EpochPathParam {
|
||||
epoch_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct NodePathParam {
|
||||
node_id: NodeId,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
struct ExportQuery {
|
||||
format: Option<ExportFormat>,
|
||||
}
|
||||
|
||||
/// List all simulation epochs with optional filtering
|
||||
#[utoipa::path(
|
||||
tag = "Simulation",
|
||||
get,
|
||||
path = "/epochs",
|
||||
context_path = "/v1/simulation",
|
||||
responses(
|
||||
(status = 200, description = "List of simulation epochs", body = SimulationEpochsResponse),
|
||||
(status = 500, description = "Internal server error", body = SimulationApiError)
|
||||
),
|
||||
params(SimulationListQuery, OutputParams)
|
||||
)]
|
||||
async fn list_simulation_epochs(
|
||||
Query(params): Query<SimulationListQuery>,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<SimulationEpochsResponse>> {
|
||||
let storage = state.storage();
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
// Apply defaults and validation
|
||||
let limit = params.limit.unwrap_or(50).min(1000);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
|
||||
let epochs = get_simulation_epochs_with_filters(storage, ¶ms, limit, offset)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
// Enhance epochs with additional metadata
|
||||
let mut enhanced_epochs = Vec::new();
|
||||
for mut epoch in epochs {
|
||||
// Count nodes analyzed for this epoch
|
||||
epoch.nodes_analyzed = count_nodes_for_epoch(storage, epoch.id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
// Get available calculation methods for this epoch_id
|
||||
epoch.available_methods = get_available_methods_for_epoch(storage, epoch.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
enhanced_epochs.push(epoch);
|
||||
}
|
||||
|
||||
let total_count = count_total_simulation_epochs(storage, ¶ms)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let response = SimulationEpochsResponse {
|
||||
epochs: enhanced_epochs,
|
||||
total_count,
|
||||
};
|
||||
|
||||
Ok(output.to_response(response))
|
||||
}
|
||||
|
||||
/// Get detailed simulation data for a specific epoch
|
||||
#[utoipa::path(
|
||||
tag = "Simulation",
|
||||
get,
|
||||
path = "/epochs/{epoch_id}",
|
||||
context_path = "/v1/simulation",
|
||||
responses(
|
||||
(status = 200, description = "Detailed simulation epoch data", body = SimulationEpochDetails),
|
||||
(status = 404, description = "Simulation epoch not found", body = SimulationApiError),
|
||||
(status = 500, description = "Internal server error", body = SimulationApiError)
|
||||
),
|
||||
params(
|
||||
("epoch_id" = i64, Path, description = "Simulation epoch ID"),
|
||||
OutputParams
|
||||
)
|
||||
)]
|
||||
async fn get_simulation_epoch_details(
|
||||
Path(params): Path<EpochPathParam>,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<SimulationEpochDetails>> {
|
||||
let storage = state.storage();
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let epoch = get_simulation_epoch_by_id(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?
|
||||
.ok_or_else(|| to_axum_error(SimulationApiError::new("Simulation epoch not found")))?;
|
||||
|
||||
let mut epoch_summary = SimulationEpochSummary::from(epoch);
|
||||
epoch_summary.nodes_analyzed = count_nodes_for_epoch(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
epoch_summary.available_methods = get_available_methods_for_epoch(storage, epoch_summary.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let node_performance = get_node_performance_for_epoch(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let rewards = get_rewards_for_epoch(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let route_analysis = get_route_analysis_for_epoch(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let details = SimulationEpochDetails {
|
||||
epoch: epoch_summary,
|
||||
node_performance,
|
||||
rewards,
|
||||
route_analysis,
|
||||
};
|
||||
|
||||
Ok(output.to_response(details))
|
||||
}
|
||||
|
||||
/// Compare old vs new methods for a specific epoch
|
||||
#[utoipa::path(
|
||||
tag = "Simulation",
|
||||
get,
|
||||
path = "/epochs/{epoch_id}/comparison",
|
||||
context_path = "/v1/simulation",
|
||||
responses(
|
||||
(status = 200, description = "Method comparison results", body = MethodComparisonResponse),
|
||||
(status = 404, description = "Simulation epoch not found", body = SimulationApiError),
|
||||
(status = 500, description = "Internal server error", body = SimulationApiError)
|
||||
),
|
||||
params(
|
||||
("epoch_id" = i64, Path, description = "Simulation epoch ID"),
|
||||
NodeComparisonQuery,
|
||||
OutputParams
|
||||
)
|
||||
)]
|
||||
async fn compare_methods(
|
||||
Path(params): Path<EpochPathParam>,
|
||||
Query(query): Query<NodeComparisonQuery>,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<MethodComparisonResponse>> {
|
||||
let storage = state.storage();
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
// Get simulation epoch to extract actual epoch_id
|
||||
let sim_epoch = get_simulation_epoch_by_id(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?
|
||||
.ok_or_else(|| to_axum_error(SimulationApiError::new("Simulation epoch not found")))?;
|
||||
|
||||
// Get performance data for both methods
|
||||
let old_performance = get_performance_by_method(storage, sim_epoch.epoch_id, "old")
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
let new_performance = get_performance_by_method(storage, sim_epoch.epoch_id, "new")
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
// Build node comparisons
|
||||
let node_comparisons = build_node_comparisons(old_performance, new_performance, &query);
|
||||
|
||||
// Calculate summary statistics
|
||||
let summary_statistics = calculate_summary_statistics(&node_comparisons);
|
||||
|
||||
// Get route analysis comparison
|
||||
let route_analysis_comparison = get_route_analysis_comparison(storage, sim_epoch.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
let comparison = MethodComparisonResponse {
|
||||
epoch_id: sim_epoch.epoch_id,
|
||||
simulation_epoch_id: params.epoch_id,
|
||||
node_comparisons,
|
||||
summary_statistics,
|
||||
route_analysis_comparison,
|
||||
};
|
||||
|
||||
Ok(output.to_response(comparison))
|
||||
}
|
||||
|
||||
/// Export simulation data in various formats
|
||||
#[utoipa::path(
|
||||
tag = "Simulation",
|
||||
get,
|
||||
path = "/epochs/{epoch_id}/export",
|
||||
context_path = "/v1/simulation",
|
||||
responses(
|
||||
(status = 200, description = "Exported simulation data"),
|
||||
(status = 404, description = "Simulation epoch not found", body = SimulationApiError),
|
||||
(status = 500, description = "Internal server error", body = SimulationApiError)
|
||||
),
|
||||
params(
|
||||
("epoch_id" = i64, Path, description = "Simulation epoch ID"),
|
||||
ExportQuery
|
||||
)
|
||||
)]
|
||||
async fn export_simulation_data(
|
||||
Path(params): Path<EpochPathParam>,
|
||||
Query(query): Query<ExportQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Response, (StatusCode, Json<SimulationApiError>)> {
|
||||
let storage = state.storage();
|
||||
let format = query.format.unwrap_or(ExportFormat::Json);
|
||||
|
||||
// Get detailed simulation data
|
||||
let epoch_details = get_simulation_epoch_details_internal(storage, params.epoch_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?
|
||||
.ok_or_else(|| to_axum_error(SimulationApiError::new("Simulation epoch not found")))?;
|
||||
|
||||
match format {
|
||||
ExportFormat::Json => {
|
||||
let json_data = serde_json::to_string_pretty(&epoch_details)
|
||||
.map_err(|e| to_axum_error(SimulationApiError::with_details("JSON serialization failed", &e.to_string())))?;
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Content-Disposition", format!("attachment; filename=\"simulation_epoch_{}.json\"", params.epoch_id))
|
||||
.body(json_data.into())
|
||||
.map_err(|e| to_axum_error(SimulationApiError::with_details("Response building failed", &e.to_string())))
|
||||
}
|
||||
ExportFormat::Csv => {
|
||||
let csv_data = convert_to_csv(&epoch_details)
|
||||
.map_err(|e| to_axum_error(SimulationApiError::with_details("CSV conversion failed", &e.to_string())))?;
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "text/csv")
|
||||
.header("Content-Disposition", format!("attachment; filename=\"simulation_epoch_{}.csv\"", params.epoch_id))
|
||||
.body(csv_data.into())
|
||||
.map_err(|e| to_axum_error(SimulationApiError::with_details("Response building failed", &e.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get performance history for a specific node across simulation epochs
|
||||
#[utoipa::path(
|
||||
tag = "Simulation",
|
||||
get,
|
||||
path = "/nodes/{node_id}/performance",
|
||||
context_path = "/v1/simulation",
|
||||
responses(
|
||||
(status = 200, description = "Node performance history", body = Vec<NodePerformanceData>),
|
||||
(status = 500, description = "Internal server error", body = SimulationApiError)
|
||||
),
|
||||
params(
|
||||
("node_id" = NodeId, Path, description = "Node ID"),
|
||||
OutputParams
|
||||
)
|
||||
)]
|
||||
async fn get_node_performance_history(
|
||||
Path(params): Path<NodePathParam>,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<Vec<NodePerformanceData>>> {
|
||||
let storage = state.storage();
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let performance_history = get_node_performance_history_internal(storage, params.node_id)
|
||||
.await
|
||||
.map_err(to_axum_error)?;
|
||||
|
||||
Ok(output.to_response(performance_history))
|
||||
}
|
||||
|
||||
// Helper functions (implementations would be added here)
|
||||
|
||||
async fn get_simulation_epochs_with_filters(
|
||||
storage: &NymApiStorage,
|
||||
_params: &SimulationListQuery,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> SimulationResult<Vec<SimulationEpochSummary>> {
|
||||
let limit_i64 = limit as i64;
|
||||
let offset_i64 = offset as i64;
|
||||
|
||||
let epochs = sqlx::query_as!(
|
||||
crate::support::storage::models::SimulatedRewardEpoch,
|
||||
"SELECT id as \"id!\", epoch_id as \"epoch_id!: u32\", calculation_method as \"calculation_method!\",
|
||||
start_timestamp as \"start_timestamp!\", end_timestamp as \"end_timestamp!\",
|
||||
description, created_at as \"created_at!\"
|
||||
FROM simulated_reward_epochs
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?",
|
||||
limit_i64,
|
||||
offset_i64
|
||||
)
|
||||
.fetch_all(&storage.manager.connection_pool)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(epochs.into_iter().map(SimulationEpochSummary::from).collect())
|
||||
}
|
||||
|
||||
async fn count_nodes_for_epoch(storage: &NymApiStorage, epoch_id: i64) -> SimulationResult<usize> {
|
||||
storage
|
||||
.manager
|
||||
.count_simulated_node_performance_for_epoch(epoch_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_available_methods_for_epoch(storage: &NymApiStorage, epoch_id: u32) -> SimulationResult<Vec<String>> {
|
||||
storage
|
||||
.manager
|
||||
.get_available_calculation_methods_for_epoch(epoch_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))
|
||||
}
|
||||
|
||||
async fn count_total_simulation_epochs(storage: &NymApiStorage, _params: &SimulationListQuery) -> SimulationResult<usize> {
|
||||
let result = sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM simulated_reward_epochs"
|
||||
)
|
||||
.fetch_one(&storage.manager.connection_pool)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(result.count as usize)
|
||||
}
|
||||
|
||||
async fn get_simulation_epoch_by_id(storage: &NymApiStorage, id: i64) -> SimulationResult<Option<crate::support::storage::models::SimulatedRewardEpoch>> {
|
||||
storage
|
||||
.manager
|
||||
.get_simulated_reward_epoch(id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_node_performance_for_epoch(storage: &NymApiStorage, epoch_id: i64) -> SimulationResult<Vec<NodePerformanceData>> {
|
||||
let performance = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_for_epoch(epoch_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(performance.into_iter().map(NodePerformanceData::from).collect())
|
||||
}
|
||||
|
||||
async fn get_rewards_for_epoch(storage: &NymApiStorage, epoch_id: i64) -> SimulationResult<Vec<NodeRewardData>> {
|
||||
let rewards = storage
|
||||
.manager
|
||||
.get_simulated_rewards_for_epoch(epoch_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(rewards.into_iter().map(NodeRewardData::from).collect())
|
||||
}
|
||||
|
||||
async fn get_route_analysis_for_epoch(storage: &NymApiStorage, epoch_id: i64) -> SimulationResult<Vec<RouteAnalysisData>> {
|
||||
let analysis = storage
|
||||
.manager
|
||||
.get_simulated_route_analysis_for_epoch(epoch_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(analysis.into_iter().map(RouteAnalysisData::from).collect())
|
||||
}
|
||||
|
||||
async fn get_performance_by_method(
|
||||
storage: &NymApiStorage,
|
||||
epoch_id: u32,
|
||||
method: &str
|
||||
) -> SimulationResult<Vec<NodePerformanceData>> {
|
||||
let performance = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_by_method(epoch_id, method)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(performance.into_iter().map(NodePerformanceData::from).collect())
|
||||
}
|
||||
|
||||
fn build_node_comparisons(
|
||||
old_performance: Vec<NodePerformanceData>,
|
||||
new_performance: Vec<NodePerformanceData>,
|
||||
query: &NodeComparisonQuery,
|
||||
) -> Vec<NodeMethodComparison> {
|
||||
let mut old_map: HashMap<NodeId, NodePerformanceData> = old_performance
|
||||
.into_iter()
|
||||
.map(|p| (p.node_id, p))
|
||||
.collect();
|
||||
|
||||
let mut new_map: HashMap<NodeId, NodePerformanceData> = new_performance
|
||||
.into_iter()
|
||||
.map(|p| (p.node_id, p))
|
||||
.collect();
|
||||
|
||||
let mut comparisons = Vec::new();
|
||||
|
||||
// Get all unique node IDs from both methods
|
||||
let mut all_node_ids: Vec<_> = old_map.keys().chain(new_map.keys()).cloned().collect();
|
||||
all_node_ids.sort();
|
||||
all_node_ids.dedup();
|
||||
|
||||
for node_id in all_node_ids {
|
||||
let old_perf = old_map.remove(&node_id);
|
||||
let new_perf = new_map.remove(&node_id);
|
||||
|
||||
// Apply filters
|
||||
if let Some(filter_node_id) = query.node_id {
|
||||
if node_id != filter_node_id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref filter_node_type) = query.node_type {
|
||||
let node_type = old_perf.as_ref()
|
||||
.or(new_perf.as_ref())
|
||||
.map(|p| &p.node_type);
|
||||
if node_type != Some(filter_node_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate differences
|
||||
let reliability_difference = match (&old_perf, &new_perf) {
|
||||
(Some(old), Some(new)) => Some(new.reliability_score - old.reliability_score),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let performance_delta_percentage = match (&old_perf, &new_perf) {
|
||||
(Some(old), Some(new)) if old.reliability_score != 0.0 => {
|
||||
Some((new.reliability_score - old.reliability_score) / old.reliability_score * 100.0)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Apply delta filters
|
||||
if let Some(min_delta) = query.min_delta {
|
||||
if reliability_difference.map_or(true, |d| d < min_delta) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max_delta) = query.max_delta {
|
||||
if reliability_difference.map_or(true, |d| d > max_delta) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let node_type = old_perf.as_ref()
|
||||
.or(new_perf.as_ref())
|
||||
.map(|p| p.node_type.clone())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let identity_key = old_perf.as_ref()
|
||||
.or(new_perf.as_ref())
|
||||
.and_then(|p| p.identity_key.clone());
|
||||
|
||||
comparisons.push(NodeMethodComparison {
|
||||
node_id,
|
||||
node_type,
|
||||
identity_key,
|
||||
old_method: old_perf,
|
||||
new_method: new_perf,
|
||||
reliability_difference,
|
||||
performance_delta_percentage,
|
||||
});
|
||||
}
|
||||
|
||||
comparisons
|
||||
}
|
||||
|
||||
fn calculate_summary_statistics(comparisons: &[NodeMethodComparison]) -> ComparisonSummaryStats {
|
||||
let mut reliabilities_old = Vec::new();
|
||||
let mut reliabilities_new = Vec::new();
|
||||
let mut improvements = 0;
|
||||
let mut degradations = 0;
|
||||
let mut unchanged = 0;
|
||||
let mut max_improvement: f64 = 0.0;
|
||||
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);
|
||||
}
|
||||
|
||||
if let Some(diff) = comparison.reliability_difference {
|
||||
if diff > 0.001 {
|
||||
improvements += 1;
|
||||
max_improvement = max_improvement.max(diff);
|
||||
} else if diff < -0.001 {
|
||||
degradations += 1;
|
||||
max_degradation = max_degradation.min(diff); // This will be negative
|
||||
} else {
|
||||
unchanged += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let average_reliability_old = if reliabilities_old.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
reliabilities_old.iter().sum::<f64>() / reliabilities_old.len() as f64
|
||||
};
|
||||
|
||||
let average_reliability_new = if reliabilities_new.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
reliabilities_new.iter().sum::<f64>() / reliabilities_new.len() as f64
|
||||
};
|
||||
|
||||
// Calculate medians and standard deviations
|
||||
let (median_reliability_old, reliability_std_dev_old) = calculate_median_and_std(&reliabilities_old);
|
||||
let (median_reliability_new, reliability_std_dev_new) = calculate_median_and_std(&reliabilities_new);
|
||||
|
||||
ComparisonSummaryStats {
|
||||
total_nodes_compared: comparisons.len(),
|
||||
nodes_improved: improvements,
|
||||
nodes_degraded: degradations,
|
||||
nodes_unchanged: unchanged,
|
||||
average_reliability_old,
|
||||
average_reliability_new,
|
||||
median_reliability_old,
|
||||
median_reliability_new,
|
||||
reliability_std_dev_old,
|
||||
reliability_std_dev_new,
|
||||
max_improvement,
|
||||
max_degradation,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_median_and_std(values: &[f64]) -> (f64, f64) {
|
||||
if values.is_empty() {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
let mut sorted = values.to_vec();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let median = if sorted.len() % 2 == 0 {
|
||||
(sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0
|
||||
} else {
|
||||
sorted[sorted.len() / 2]
|
||||
};
|
||||
|
||||
let mean = values.iter().sum::<f64>() / values.len() as f64;
|
||||
let variance = values.iter()
|
||||
.map(|x| (x - mean).powi(2))
|
||||
.sum::<f64>() / values.len() as f64;
|
||||
let std_dev = variance.sqrt();
|
||||
|
||||
(median, std_dev)
|
||||
}
|
||||
|
||||
async fn get_route_analysis_comparison(
|
||||
storage: &NymApiStorage,
|
||||
epoch_id: u32,
|
||||
) -> SimulationResult<RouteAnalysisComparison> {
|
||||
let old_analysis = storage
|
||||
.manager
|
||||
.get_simulated_route_analysis_by_method(epoch_id, "old")
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?
|
||||
.map(RouteAnalysisData::from);
|
||||
|
||||
let new_analysis = storage
|
||||
.manager
|
||||
.get_simulated_route_analysis_by_method(epoch_id, "new")
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?
|
||||
.map(RouteAnalysisData::from);
|
||||
|
||||
let time_window_difference_hours = match (&old_analysis, &new_analysis) {
|
||||
(Some(old), Some(new)) => new.time_window_hours as i32 - old.time_window_hours as i32,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let route_coverage_difference = match (&old_analysis, &new_analysis) {
|
||||
(Some(old), Some(new)) => new.total_routes_analyzed as i32 - old.total_routes_analyzed as i32,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let success_rate_difference = match (&old_analysis, &new_analysis) {
|
||||
(Some(old), Some(new)) => {
|
||||
let old_rate = old.successful_routes as f64 / old.total_routes_analyzed as f64;
|
||||
let new_rate = new.successful_routes as f64 / new.total_routes_analyzed as f64;
|
||||
Some(new_rate - old_rate)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(RouteAnalysisComparison {
|
||||
old_method: old_analysis,
|
||||
new_method: new_analysis,
|
||||
time_window_difference_hours,
|
||||
route_coverage_difference,
|
||||
success_rate_difference,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_simulation_epoch_details_internal(
|
||||
storage: &NymApiStorage,
|
||||
epoch_id: i64,
|
||||
) -> SimulationResult<Option<SimulationEpochDetails>> {
|
||||
let epoch = match get_simulation_epoch_by_id(storage, epoch_id).await? {
|
||||
Some(epoch) => epoch,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut epoch_summary = SimulationEpochSummary::from(epoch);
|
||||
epoch_summary.nodes_analyzed = count_nodes_for_epoch(storage, epoch_id).await?;
|
||||
epoch_summary.available_methods = get_available_methods_for_epoch(storage, epoch_summary.epoch_id).await?;
|
||||
|
||||
let node_performance = get_node_performance_for_epoch(storage, epoch_id).await?;
|
||||
let rewards = get_rewards_for_epoch(storage, epoch_id).await?;
|
||||
let route_analysis = get_route_analysis_for_epoch(storage, epoch_id).await?;
|
||||
|
||||
Ok(Some(SimulationEpochDetails {
|
||||
epoch: epoch_summary,
|
||||
node_performance,
|
||||
rewards,
|
||||
route_analysis,
|
||||
}))
|
||||
}
|
||||
|
||||
fn convert_to_csv(details: &SimulationEpochDetails) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Simple CSV conversion - in a real implementation this would be more sophisticated
|
||||
let mut csv = String::new();
|
||||
|
||||
// Header
|
||||
csv.push_str("data_type,node_id,node_type,reliability_score,reward_amount,calculation_method\n");
|
||||
|
||||
// Performance data
|
||||
for perf in &details.node_performance {
|
||||
csv.push_str(&format!(
|
||||
"performance,{},{},{},{},{}\n",
|
||||
perf.node_id, perf.node_type, perf.reliability_score, "", perf.calculation_method
|
||||
));
|
||||
}
|
||||
|
||||
// Reward data
|
||||
for reward in &details.rewards {
|
||||
csv.push_str(&format!(
|
||||
"reward,{},{},{},{},{}\n",
|
||||
reward.node_id, reward.node_type, "", reward.calculated_reward_amount, reward.calculation_method
|
||||
));
|
||||
}
|
||||
|
||||
Ok(csv)
|
||||
}
|
||||
|
||||
async fn get_node_performance_history_internal(
|
||||
storage: &NymApiStorage,
|
||||
node_id: NodeId,
|
||||
) -> SimulationResult<Vec<NodePerformanceData>> {
|
||||
let performance = storage
|
||||
.manager
|
||||
.get_simulated_node_performance_history(node_id)
|
||||
.await
|
||||
.map_err(|e| SimulationApiError::with_details("Database error", &e.to_string()))?;
|
||||
|
||||
Ok(performance.into_iter().map(NodePerformanceData::from).collect())
|
||||
}
|
||||
|
||||
fn to_axum_error(error: SimulationApiError) -> (StatusCode, Json<SimulationApiError>) {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Simulation API for reward calculation analysis
|
||||
//!
|
||||
//! This module provides REST endpoints for accessing and analyzing
|
||||
//! simulated reward calculation data comparing old vs new methodologies.
|
||||
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod models;
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! API models for simulation data responses
|
||||
|
||||
use crate::storage::models::{SimulatedNodePerformance, SimulatedReward, SimulatedRewardEpoch, SimulatedRouteAnalysis};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response for listing simulation epochs
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochsResponse {
|
||||
pub epochs: Vec<SimulationEpochSummary>,
|
||||
pub total_count: usize,
|
||||
}
|
||||
|
||||
/// Summary information about a simulation epoch
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochSummary {
|
||||
pub id: i64,
|
||||
pub epoch_id: u32,
|
||||
pub calculation_method: String,
|
||||
pub start_timestamp: i64,
|
||||
pub end_timestamp: i64,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
/// Number of nodes that had performance calculated
|
||||
pub nodes_analyzed: usize,
|
||||
/// Available calculation methods for this epoch
|
||||
pub available_methods: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<SimulatedRewardEpoch> for SimulationEpochSummary {
|
||||
fn from(epoch: SimulatedRewardEpoch) -> Self {
|
||||
Self {
|
||||
id: epoch.id,
|
||||
epoch_id: epoch.epoch_id,
|
||||
calculation_method: epoch.calculation_method,
|
||||
start_timestamp: epoch.start_timestamp,
|
||||
end_timestamp: epoch.end_timestamp,
|
||||
description: epoch.description,
|
||||
created_at: epoch.created_at,
|
||||
nodes_analyzed: 0, // Will be populated by handler
|
||||
available_methods: vec![], // Will be populated by handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed simulation epoch with all data
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct SimulationEpochDetails {
|
||||
pub epoch: SimulationEpochSummary,
|
||||
pub node_performance: Vec<NodePerformanceData>,
|
||||
pub rewards: Vec<NodeRewardData>,
|
||||
pub route_analysis: Vec<RouteAnalysisData>,
|
||||
}
|
||||
|
||||
/// Node performance data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct NodePerformanceData {
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String,
|
||||
pub identity_key: Option<String>,
|
||||
pub reliability_score: f64,
|
||||
pub positive_samples: u32,
|
||||
pub negative_samples: u32,
|
||||
pub final_fail_sequence: u32,
|
||||
pub work_factor: Option<f64>,
|
||||
pub calculation_method: String,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedNodePerformance> for NodePerformanceData {
|
||||
fn from(perf: SimulatedNodePerformance) -> Self {
|
||||
Self {
|
||||
node_id: perf.node_id,
|
||||
node_type: perf.node_type,
|
||||
identity_key: perf.identity_key,
|
||||
reliability_score: perf.reliability_score,
|
||||
positive_samples: perf.positive_samples,
|
||||
negative_samples: perf.negative_samples,
|
||||
final_fail_sequence: perf.final_fail_sequence,
|
||||
work_factor: perf.work_factor,
|
||||
calculation_method: perf.calculation_method,
|
||||
calculated_at: perf.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node reward data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct NodeRewardData {
|
||||
pub node_id: NodeId,
|
||||
pub node_type: String,
|
||||
pub calculated_reward_amount: f64,
|
||||
pub reward_currency: String,
|
||||
pub performance_component: f64,
|
||||
pub work_component: f64,
|
||||
pub calculation_method: String,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedReward> for NodeRewardData {
|
||||
fn from(reward: SimulatedReward) -> Self {
|
||||
Self {
|
||||
node_id: reward.node_id,
|
||||
node_type: reward.node_type,
|
||||
calculated_reward_amount: reward.calculated_reward_amount,
|
||||
reward_currency: reward.reward_currency,
|
||||
performance_component: reward.performance_component,
|
||||
work_component: reward.work_component,
|
||||
calculation_method: reward.calculation_method,
|
||||
calculated_at: reward.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Route analysis data for API responses
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct RouteAnalysisData {
|
||||
pub calculation_method: String,
|
||||
pub total_routes_analyzed: u32,
|
||||
pub successful_routes: u32,
|
||||
pub failed_routes: u32,
|
||||
pub average_route_reliability: Option<f64>,
|
||||
pub time_window_hours: u32,
|
||||
pub analysis_parameters: Option<String>,
|
||||
pub calculated_at: i64,
|
||||
}
|
||||
|
||||
impl From<SimulatedRouteAnalysis> for RouteAnalysisData {
|
||||
fn from(analysis: SimulatedRouteAnalysis) -> Self {
|
||||
Self {
|
||||
calculation_method: analysis.calculation_method,
|
||||
total_routes_analyzed: analysis.total_routes_analyzed,
|
||||
successful_routes: analysis.successful_routes,
|
||||
failed_routes: analysis.failed_routes,
|
||||
average_route_reliability: analysis.average_route_reliability,
|
||||
time_window_hours: analysis.time_window_hours,
|
||||
analysis_parameters: analysis.analysis_parameters,
|
||||
calculated_at: analysis.calculated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Comparison between old and new methods for a specific epoch
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct MethodComparisonResponse {
|
||||
pub epoch_id: u32,
|
||||
pub simulation_epoch_id: i64,
|
||||
pub node_comparisons: Vec<NodeMethodComparison>,
|
||||
pub summary_statistics: ComparisonSummaryStats,
|
||||
pub route_analysis_comparison: RouteAnalysisComparison,
|
||||
}
|
||||
|
||||
/// Comparison data for a single node between old and new methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
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>,
|
||||
pub reliability_difference: Option<f64>, // new - old
|
||||
pub performance_delta_percentage: Option<f64>, // (new - old) / old * 100
|
||||
}
|
||||
|
||||
/// Summary statistics comparing old vs new methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct ComparisonSummaryStats {
|
||||
pub total_nodes_compared: usize,
|
||||
pub nodes_improved: usize, // nodes with better performance in new method
|
||||
pub nodes_degraded: usize, // nodes with worse performance in new method
|
||||
pub nodes_unchanged: usize, // nodes with same performance
|
||||
pub average_reliability_old: f64,
|
||||
pub average_reliability_new: f64,
|
||||
pub median_reliability_old: f64,
|
||||
pub median_reliability_new: f64,
|
||||
pub reliability_std_dev_old: f64,
|
||||
pub reliability_std_dev_new: f64,
|
||||
pub max_improvement: f64, // highest positive delta
|
||||
pub max_degradation: f64, // highest negative delta
|
||||
}
|
||||
|
||||
/// Comparison of route analysis between methods
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub struct RouteAnalysisComparison {
|
||||
pub old_method: Option<RouteAnalysisData>,
|
||||
pub new_method: Option<RouteAnalysisData>,
|
||||
pub time_window_difference_hours: i32, // new - old
|
||||
pub route_coverage_difference: i32, // new total routes - old total routes
|
||||
pub success_rate_difference: Option<f64>, // new success rate - old success rate
|
||||
}
|
||||
|
||||
/// Export format options
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
|
||||
pub enum ExportFormat {
|
||||
#[serde(rename = "json")]
|
||||
Json,
|
||||
#[serde(rename = "csv")]
|
||||
Csv,
|
||||
}
|
||||
|
||||
/// Query parameters for simulation listings
|
||||
#[derive(Deserialize, ToSchema, Debug, utoipa::IntoParams)]
|
||||
pub struct SimulationListQuery {
|
||||
/// Limit number of results (default: 50, max: 1000)
|
||||
pub limit: Option<usize>,
|
||||
/// Offset for pagination (default: 0)
|
||||
pub offset: Option<usize>,
|
||||
}
|
||||
|
||||
/// Query parameters for node-specific performance comparison
|
||||
#[derive(Deserialize, ToSchema, Debug, utoipa::IntoParams)]
|
||||
pub struct NodeComparisonQuery {
|
||||
/// Specific node ID to analyze
|
||||
pub node_id: Option<NodeId>,
|
||||
/// Node type filter (mixnode, gateway)
|
||||
pub node_type: Option<String>,
|
||||
/// Minimum reliability difference threshold for filtering
|
||||
pub min_delta: Option<f64>,
|
||||
/// Maximum reliability difference threshold for filtering
|
||||
pub max_delta: Option<f64>,
|
||||
}
|
||||
|
||||
/// Error response for simulation API
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct SimulationApiError {
|
||||
pub error: String,
|
||||
pub details: Option<String>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl SimulationApiError {
|
||||
pub fn new(error: &str) -> Self {
|
||||
Self {
|
||||
error: error.to_string(),
|
||||
details: None,
|
||||
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_details(error: &str, details: &str) -> Self {
|
||||
Self {
|
||||
error: error.to_string(),
|
||||
details: Some(details.to_string()),
|
||||
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use crate::node_status_api::handlers::status_routes;
|
||||
use crate::nym_contract_cache::handlers::nym_contract_cache_routes;
|
||||
use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes;
|
||||
use crate::nym_nodes::handlers::nym_node_routes;
|
||||
use crate::simulation_api::handlers::simulation_routes;
|
||||
use crate::status;
|
||||
use crate::support::http::openapi::ApiDoc;
|
||||
use crate::support::http::state::AppState;
|
||||
@@ -64,6 +65,7 @@ impl RouterBuilder {
|
||||
.nest("/api-status", status::handlers::api_status_routes())
|
||||
.nest("/nym-nodes", nym_node_routes())
|
||||
.nest("/ecash", ecash_routes())
|
||||
.nest("/simulation", simulation_routes())
|
||||
.nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes
|
||||
);
|
||||
|
||||
|
||||
@@ -1730,21 +1730,6 @@ pub(crate) mod v3_migration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves all simulated reward epochs
|
||||
pub(crate) async fn get_simulated_reward_epochs(&self) -> Result<Vec<SimulatedRewardEpoch>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRewardEpoch,
|
||||
r#"
|
||||
SELECT id as "id!", epoch_id as "epoch_id!: u32", calculation_method as "calculation_method!",
|
||||
start_timestamp as "start_timestamp!", end_timestamp as "end_timestamp!",
|
||||
description, created_at as "created_at!"
|
||||
FROM simulated_reward_epochs
|
||||
ORDER BY created_at DESC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves node performance data for a specific simulation
|
||||
pub(crate) async fn get_simulated_node_performance(
|
||||
@@ -1839,5 +1824,171 @@ pub(crate) mod v3_migration {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Count simulated node performance records for a specific epoch
|
||||
pub(crate) async fn count_simulated_node_performance_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<usize, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM simulated_node_performance WHERE simulated_epoch_id = ?",
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.count as usize)
|
||||
}
|
||||
|
||||
/// Get available calculation methods for a specific epoch
|
||||
pub(crate) async fn get_available_calculation_methods_for_epoch(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
) -> Result<Vec<String>, sqlx::Error> {
|
||||
let methods = sqlx::query!(
|
||||
"SELECT DISTINCT calculation_method FROM simulated_reward_epochs WHERE epoch_id = ?",
|
||||
epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(methods.into_iter().map(|m| m.calculation_method).collect())
|
||||
}
|
||||
|
||||
/// Get simulated reward epoch by ID
|
||||
pub(crate) async fn get_simulated_reward_epoch(
|
||||
&self,
|
||||
id: i64,
|
||||
) -> Result<Option<SimulatedRewardEpoch>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRewardEpoch,
|
||||
r#"
|
||||
SELECT id as "id!", epoch_id as "epoch_id!: u32", calculation_method as "calculation_method!",
|
||||
start_timestamp as "start_timestamp!", end_timestamp as "end_timestamp!",
|
||||
description, created_at as "created_at!"
|
||||
FROM simulated_reward_epochs
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated node performance for a specific epoch
|
||||
pub(crate) async fn get_simulated_node_performance_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
self.get_simulated_node_performance(simulated_epoch_id, None).await
|
||||
}
|
||||
|
||||
/// Get simulated rewards for a specific epoch
|
||||
pub(crate) async fn get_simulated_rewards_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedReward>, sqlx::Error> {
|
||||
self.get_simulated_rewards(simulated_epoch_id, None).await
|
||||
}
|
||||
|
||||
/// Get simulated route analysis for a specific epoch
|
||||
pub(crate) async fn get_simulated_route_analysis_for_epoch(
|
||||
&self,
|
||||
simulated_epoch_id: i64,
|
||||
) -> Result<Vec<SimulatedRouteAnalysis>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRouteAnalysis,
|
||||
r#"
|
||||
SELECT id as "id!", simulated_epoch_id as "simulated_epoch_id!",
|
||||
calculation_method as "calculation_method!", total_routes_analyzed as "total_routes_analyzed!: u32",
|
||||
successful_routes as "successful_routes!: u32", failed_routes as "failed_routes!: u32",
|
||||
average_route_reliability, time_window_hours as "time_window_hours!: u32",
|
||||
analysis_parameters, calculated_at as "calculated_at!"
|
||||
FROM simulated_route_analysis
|
||||
WHERE simulated_epoch_id = ?
|
||||
ORDER BY calculation_method
|
||||
"#,
|
||||
simulated_epoch_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated node performance by method
|
||||
pub(crate) async fn get_simulated_node_performance_by_method(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
method: &str,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT snp.id as "id!", snp.simulated_epoch_id as "simulated_epoch_id!",
|
||||
snp.node_id as "node_id!: NodeId", snp.node_type as "node_type!",
|
||||
snp.identity_key, snp.reliability_score as "reliability_score!",
|
||||
snp.positive_samples as "positive_samples!: u32", snp.negative_samples as "negative_samples!: u32",
|
||||
snp.final_fail_sequence as "final_fail_sequence!: u32", snp.work_factor,
|
||||
snp.calculation_method as "calculation_method!", snp.calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance snp
|
||||
JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id
|
||||
WHERE sre.epoch_id = ? AND snp.calculation_method = ?
|
||||
ORDER BY snp.node_id
|
||||
"#,
|
||||
epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get simulated route analysis by method
|
||||
pub(crate) async fn get_simulated_route_analysis_by_method(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
method: &str,
|
||||
) -> Result<Option<SimulatedRouteAnalysis>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedRouteAnalysis,
|
||||
r#"
|
||||
SELECT sra.id as "id!", sra.simulated_epoch_id as "simulated_epoch_id!",
|
||||
sra.calculation_method as "calculation_method!", sra.total_routes_analyzed as "total_routes_analyzed!: u32",
|
||||
sra.successful_routes as "successful_routes!: u32", sra.failed_routes as "failed_routes!: u32",
|
||||
sra.average_route_reliability, sra.time_window_hours as "time_window_hours!: u32",
|
||||
sra.analysis_parameters, sra.calculated_at as "calculated_at!"
|
||||
FROM simulated_route_analysis sra
|
||||
JOIN simulated_reward_epochs sre ON sra.simulated_epoch_id = sre.id
|
||||
WHERE sre.epoch_id = ? AND sra.calculation_method = ?
|
||||
"#,
|
||||
epoch_id,
|
||||
method
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get node performance history across simulation epochs
|
||||
pub(crate) async fn get_simulated_node_performance_history(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<SimulatedNodePerformance>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
SimulatedNodePerformance,
|
||||
r#"
|
||||
SELECT snp.id as "id!", snp.simulated_epoch_id as "simulated_epoch_id!",
|
||||
snp.node_id as "node_id!: NodeId", snp.node_type as "node_type!",
|
||||
snp.identity_key, snp.reliability_score as "reliability_score!",
|
||||
snp.positive_samples as "positive_samples!: u32", snp.negative_samples as "negative_samples!: u32",
|
||||
snp.final_fail_sequence as "final_fail_sequence!: u32", snp.work_factor,
|
||||
snp.calculation_method as "calculation_method!", snp.calculated_at as "calculated_at!"
|
||||
FROM simulated_node_performance snp
|
||||
JOIN simulated_reward_epochs sre ON snp.simulated_epoch_id = sre.id
|
||||
WHERE snp.node_id = ?
|
||||
ORDER BY sre.epoch_id DESC, snp.calculation_method
|
||||
"#,
|
||||
node_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ pub struct SimulatedRewardEpoch {
|
||||
/// Node performance calculated using different methodologies
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedNodePerformance {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub node_id: NodeId,
|
||||
@@ -182,6 +183,7 @@ pub struct SimulatedNodePerformance {
|
||||
/// Simulated reward calculation results
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedReward {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub node_id: NodeId,
|
||||
@@ -197,6 +199,7 @@ pub struct SimulatedReward {
|
||||
/// Route analysis metadata for simulation runs
|
||||
#[derive(FromRow, Debug, Clone)]
|
||||
pub struct SimulatedRouteAnalysis {
|
||||
#[allow(dead_code)]
|
||||
pub id: i64,
|
||||
pub simulated_epoch_id: i64,
|
||||
pub calculation_method: String, // 'old' or 'new'
|
||||
|
||||
Reference in New Issue
Block a user