Examples¶
Basic Simulation¶
1"""Basic example demonstrating PyTwinNet usage.
2
3Steps
4-----
51. Build a DigitalTwin with a simple Network (3 nodes).
62. Define a Scenario that moves one node.
73. Run the Simulator and print final node positions.
8"""
9
10from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
11from pytwinnet.physics import Environment, FreeSpacePathLoss
12from pytwinnet.simulation import Scenario, MoveNodeEvent, Simulator
13
14
15def main() -> None:
16 # 1) Create twin and environment
17 twin = DigitalTwin()
18 twin.set_environment(Environment(dimensions_m=(200.0, 200.0, 30.0)))
19 twin.set_propagation_model(FreeSpacePathLoss())
20
21 # 2) Build a simple network
22 net = Network()
23 net.add_node(WirelessNode(node_id="gNB-1", position=(0.0, 0.0, 10.0),
24 transceiver_properties=TransceiverProperties(transmit_power_dbm=30.0)))
25 net.add_node(WirelessNode(node_id="UE-1", position=(10.0, 0.0, 1.5)))
26 net.add_node(WirelessNode(node_id="UE-2", position=(0.0, 20.0, 1.5)))
27 twin.network = net
28
29 # 3) Define a scenario: move UE-1
30 scenario = Scenario(duration_s=10.0)
31 scenario.add_event(MoveNodeEvent(timestamp=3.0, node_id="UE-1", new_position=(25.0, 5.0, 1.5)))
32
33 # 4) Run simulator (on a snapshot to protect live twin)
34 sim = Simulator(twin)
35 final_twin = sim.run(scenario, copy_twin=True)
36
37 # 5) Print final positions
38 for node in final_twin.network.list_nodes():
39 print(f"{node.node_id}: position={node.position}, tx_power={node.transceiver_properties.transmit_power_dbm:.1f} dBm")
40
41
42if __name__ == "__main__":
43 main()
What-If Throughput¶
1from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
2from pytwinnet.physics import Environment, FreeSpacePathLoss
3from pytwinnet.simulation import Scenario, MoveNodeEvent, what_if
4from pytwinnet.optimization.objective import SumThroughputObjective
5
6def build_twin():
7 twin = DigitalTwin()
8 twin.set_environment(Environment(dimensions_m=(300.0, 300.0, 30.0)))
9 twin.set_propagation_model(FreeSpacePathLoss())
10 net = Network()
11 net.add_node(WirelessNode(node_id="gNB-1", position=(0.0, 0.0, 10.0),
12 transceiver_properties=TransceiverProperties(transmit_power_dbm=30.0, antenna_gain_dbi=5.0)))
13 for i, pos in enumerate([(30.0, 0.0, 1.5), (0.0, 40.0, 1.5), (25.0, 25.0, 1.5), (60.0, 50.0, 1.5)]):
14 net.add_node(WirelessNode(node_id=f"UE-{i+1}", position=pos))
15 twin.network = net
16 return twin
17
18def scenario_move_gnb(new_position):
19 s = Scenario(duration_s=1.0)
20 s.add_event(MoveNodeEvent(timestamp=0.5, node_id="gNB-1", new_position=new_position))
21 return s
22
23def main():
24 twin = build_twin()
25 obj = SumThroughputObjective(tx_id="gNB-1", efficiency=0.75)
26 resA = what_if(twin, scenario_move_gnb((20.0, 20.0, 10.0)), objective=obj)
27 resB = what_if(twin, scenario_move_gnb((80.0, 80.0, 10.0)), objective=obj)
28 print("Candidate A score (bps):", f"{resA['score']:.2f}")
29 print("Candidate B score (bps):", f"{resB['score']:.2f}")
30 print("\nFinal positions for Candidate A:")
31 for n in resA["twin"].network.list_nodes():
32 print(f" {n.node_id}: {n.position}")
33
34if __name__ == "__main__":
35 main()
SINR Heatmap¶
1
2from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
3from pytwinnet.physics import Environment, FreeSpacePathLoss
4from pytwinnet.visualization import sinr_heatmap_2d
5
6def main():
7 twin = DigitalTwin()
8 twin.set_environment(Environment(dimensions_m=(200, 200, 20)))
9 twin.set_propagation_model(FreeSpacePathLoss())
10 net = Network()
11 net.add_node(WirelessNode(node_id="gNB-1", position=(50.0, 50.0, 10.0),
12 transceiver_properties=TransceiverProperties(transmit_power_dbm=30.0, antenna_gain_dbi=5.0),
13 metadata={"role":"gNB"}))
14 net.add_node(WirelessNode(node_id="gNB-2", position=(150.0, 150.0, 10.0),
15 transceiver_properties=TransceiverProperties(transmit_power_dbm=30.0, antenna_gain_dbi=5.0),
16 metadata={"role":"gNB"}))
17 twin.network = net
18 sinr_heatmap_2d(twin, tx_id="gNB-1", interferer_ids=["gNB-2"],
19 xlim=(0, 200), ylim=(0, 200), resolution=60, show=False)
20 print("Generated SINR heatmap (not displayed in non-GUI env).")
21
22if __name__ == "__main__":
23 main()
PF Scheduling and Association¶
1from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
2from pytwinnet.physics import Environment, FreeSpacePathLoss
3from pytwinnet.scheduling import max_rsrp_association, proportional_fair_allocation
4
5def main():
6 twin = DigitalTwin()
7 twin.set_environment(Environment(dimensions_m=(300, 300, 30)))
8 twin.set_propagation_model(FreeSpacePathLoss())
9
10 net = Network()
11 net.add_node(WirelessNode("gNB-1", position=(50, 50, 10),
12 transceiver_properties=TransceiverProperties(transmit_power_dbm=32, antenna_gain_dbi=5),
13 metadata={"role": "gNB"}))
14 net.add_node(WirelessNode("gNB-2", position=(250, 250, 10),
15 transceiver_properties=TransceiverProperties(transmit_power_dbm=32, antenna_gain_dbi=5),
16 metadata={"role": "gNB"}))
17 for i, pos in enumerate([(60,60,1.5),(80,80,1.5),(120,120,1.5),(200,210,1.5),(240,240,1.5),(260,260,1.5)]):
18 net.add_node(WirelessNode(f"UE-{i+1}", position=pos))
19 twin.network = net
20
21 tx_ids = ["gNB-1", "gNB-2"]
22 ue_ids = [f"UE-{i+1}" for i in range(6)]
23 assoc = max_rsrp_association(twin, tx_ids, ue_ids)
24 print("Association (UE -> TX):", assoc)
25
26 sched = proportional_fair_allocation(twin, assoc, rb_count=20)
27 for tx, ues in sched.items():
28 print(f"{tx}: scheduled RBs ->", ues[:10], "...")
29
30if __name__ == "__main__":
31 main()
Handover (Hysteresis + Time-to-Trigger)¶
1from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
2from pytwinnet.physics import Environment, FreeSpacePathLoss
3from pytwinnet.handover import HandoverController
4
5def main():
6 twin = DigitalTwin()
7 twin.set_environment(Environment(dimensions_m=(300,300,30)))
8 twin.set_propagation_model(FreeSpacePathLoss())
9
10 net = Network()
11 net.add_node(WirelessNode("gNB-1", position=(50,150,10),
12 transceiver_properties=TransceiverProperties(transmit_power_dbm=32, antenna_gain_dbi=5),
13 metadata={"role":"gNB"}))
14 net.add_node(WirelessNode("gNB-2", position=(250,150,10),
15 transceiver_properties=TransceiverProperties(transmit_power_dbm=32, antenna_gain_dbi=5),
16 metadata={"role":"gNB"}))
17 net.add_node(WirelessNode("UE-1", position=(60,150,1.5)))
18 twin.network = net
19
20 ho = HandoverController(hysteresis_db=3.0, time_to_trigger_s=0.64)
21 serving = "gNB-1"
22 t = 0.0
23 for step in range(11):
24 x = 60 + step * 20 # move east every step
25 ue = twin.network.get_node_by_id("UE-1")
26 ue.move_to((x, 150, 1.5))
27 serving_new = ho.step(twin, "UE-1", serving, timestamp_s=t)
28 print(f"t={t:.2f}s, UE-1 at x={x}, serving={serving} -> {serving_new}")
29 serving = serving_new
30 t += 0.2
31
32if __name__ == "__main__":
33 main()
RIS-Enhanced Propagation (Toy)¶
1from pytwinnet import DigitalTwin, Network, WirelessNode, TransceiverProperties
2from pytwinnet.physics import Environment, FreeSpacePathLoss
3from pytwinnet.physics.ris import RISPanel, RISAugmentedModel
4from pytwinnet.physics.link_budget import rx_power_dbm
5
6def main():
7 base = FreeSpacePathLoss()
8 ris = RISPanel(position=(100, 50, 10), gain_db=12.0)
9 twin = DigitalTwin()
10 twin.set_environment(Environment(dimensions_m=(200, 100, 30)))
11 twin.set_propagation_model(RISAugmentedModel(base, ris, extra_loss_db=3.0))
12
13 net = Network()
14 net.add_node(WirelessNode("gNB-1", position=(20, 50, 10),
15 transceiver_properties=TransceiverProperties(transmit_power_dbm=30, antenna_gain_dbi=5),
16 metadata={"role":"gNB"}))
17 net.add_node(WirelessNode("UE-1", position=(180, 50, 1.5)))
18 twin.network = net
19
20 pm = twin.propagation_model; env = twin.environment
21 tx = twin.network.get_node_by_id("gNB-1")
22 rx = twin.network.get_node_by_id("UE-1")
23
24 pl_ris = pm.calculate_path_loss(tx, rx, env)
25 prx_ris = rx_power_dbm(tx.transceiver_properties.transmit_power_dbm,
26 tx.transceiver_properties.antenna_gain_dbi,
27 rx.transceiver_properties.antenna_gain_dbi,
28 pl_ris)
29
30 direct_pm = FreeSpacePathLoss()
31 pl_direct = direct_pm.calculate_path_loss(tx, rx, env)
32 prx_direct = rx_power_dbm(tx.transceiver_properties.transmit_power_dbm,
33 tx.transceiver_properties.antenna_gain_dbi,
34 rx.transceiver_properties.antenna_gain_dbi,
35 pl_direct)
36
37 print(f"Direct-only RSRP: {prx_direct:.2f} dBm | RIS-augmented RSRP: {prx_ris:.2f} dBm (higher is better)")
38
39if __name__ == "__main__":
40 main()
Config-Driven Experiment¶
python -m pytwinnet.cli run configs/het_net_placement.yaml