""" Main simulation entry point. Simulation flow: 1. Create SimPy environment 2. Create wireless channel 3. Randomly deploy nodes 4. Designate sink node 5. Start node processes 6. Run simulation 7. Output metrics """ import random import json import simpy from sim.node.node import Node from sim.radio.channel import Channel from sim.core.metrics import MetricsCollector from sim import config def deploy_nodes( env: simpy.Environment, channel: Channel, num_nodes: int = None, area_size: float = None, metrics_collector: MetricsCollector = None, ) -> list: """ Deploy nodes randomly in the area. Args: env: SimPy environment channel: Wireless channel num_nodes: Number of nodes (default from config) area_size: Area size (default from config) metrics_collector: Metrics collector for observability Returns: List of Node objects """ if num_nodes is None: num_nodes = config.NODE_COUNT if area_size is None: area_size = config.AREA_SIZE nodes = [] # Deploy sink node at center sink_x = area_size / 2 sink_y = area_size / 2 sink = Node( env=env, node_id=config.SINK_NODE_ID, x=sink_x, y=sink_y, channel=channel, is_sink=True, metrics_collector=metrics_collector, ) nodes.append(sink) # Deploy other nodes randomly for i in range(1, num_nodes): x = random.uniform(0, area_size) y = random.uniform(0, area_size) node = Node( env=env, node_id=i, x=x, y=y, channel=channel, metrics_collector=metrics_collector, ) nodes.append(node) return nodes def setup_receive_callback(nodes: list, channel: Channel): """ Setup receive callback from channel to nodes. Args: nodes: List of nodes channel: Wireless channel """ def receive_dispatcher(node_id: int, received): """Dispatch received packet to appropriate node.""" for node in nodes: if node.node_id == node_id: node.on_receive(received) break channel.receive_callback = receive_dispatcher def run_simulation( num_nodes: int = None, area_size: float = None, sim_time: float = None, seed: int = None, ) -> dict: """ Run the LoRa network simulation. Args: num_nodes: Number of nodes area_size: Area size in meters sim_time: Simulation time in seconds seed: Random seed for reproducibility Returns: Simulation results including metrics """ # Set random seed if seed is not None: random.seed(seed) # Create environment env = simpy.Environment() # Create channel channel = Channel(env) # Create metrics collector first (before deploying nodes) metrics = MetricsCollector() metrics.set_start_time(0.0) # Deploy nodes with metrics collector if num_nodes is None: num_nodes = config.NODE_COUNT if area_size is None: area_size = config.AREA_SIZE if sim_time is None: sim_time = config.SIM_TIME nodes = deploy_nodes(env, channel, num_nodes, area_size, metrics) # Setup receive callbacks setup_receive_callback(nodes, channel) # Add collision callback initial_collisions = channel.collision_count # Start all nodes for node in nodes: node.start() # Run simulation env.run(until=sim_time) # Collect metrics convergence_time = config.HELLO_PERIOD * 3 # Estimate convergence metrics.set_convergence_time(convergence_time) # First add stats for non-sink nodes for node in nodes: if node.is_sink: continue stats = node.get_stats() metrics.add_node_stats(node.node_id, stats) # Then add sink stats for node in nodes: if node.is_sink: stats = node.get_stats() metrics.add_sink_stats(node.node_id, stats) break metrics.add_collision(channel.collision_count - initial_collisions) # Get results results = { "config": { "num_nodes": num_nodes, "area_size": area_size, "sim_time": sim_time, "seed": seed, }, "metrics": metrics.get_metrics().get_summary(), "topology": [], } # Add topology info for node in nodes: results["topology"].append( { "node_id": node.node_id, "is_sink": node.is_sink, "x": node.x, "y": node.y, "cost": node.routing.cost if node.routing.cost != float("inf") else -1, "parent": node.routing.parent, } ) return results def main(): """Main entry point.""" print("=" * 60) print("LoRa Multi-Hop Network Simulation") print("=" * 60) # Run simulation results = run_simulation() # Print results print("\n--- Configuration ---") print(f"Nodes: {results['config']['num_nodes']}") print( f"Area: {results['config']['area_size']}m x {results['config']['area_size']}m" ) print(f"Simulation time: {results['config']['sim_time']}s") print("\n--- Metrics ---") metrics = results["metrics"] print(f"Total sent: {metrics['total_sent']}") print(f"Total received: {metrics['total_received']}") print(f"Packet Delivery Ratio: {metrics['pdr']}%") print(f"Average hops: {metrics['avg_hop']}") print(f"Average retries: {metrics['avg_retries']}") print(f"Convergence time: {metrics['convergence_time']}s") print(f"Collisions: {metrics['collisions']}") print("\n--- Topology ---") for node_info in results["topology"]: parent_str = ( f"-> {node_info['parent']}" if node_info["parent"] is not None else "" ) print( f"Node {node_info['node_id']:2d}: cost={node_info['cost']:3d} {parent_str}" ) # Save results with open("simulation_results.json", "w") as f: json.dump(results, f, indent=2) print("\nResults saved to simulation_results.json") if __name__ == "__main__": main()