#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkTopology {
Bus,
Ring,
Mesh,
}
#[derive(Debug, Clone)]
pub struct NetworkNode {
pub id: usize,
pub loss_db: f64,
}
impl NetworkNode {
pub fn new(id: usize, loss_db: f64) -> Self {
Self { id, loss_db }
}
pub fn ring_add_drop(id: usize) -> Self {
Self::new(id, 3.0)
}
}
#[derive(Debug, Clone)]
pub struct PhotonicNetwork {
pub topology: NetworkTopology,
pub nodes: Vec<NetworkNode>,
pub link_loss_db: f64,
pub tx_power_dbm: f64,
}
impl PhotonicNetwork {
pub fn new(topology: NetworkTopology, tx_power_dbm: f64, link_loss_db: f64) -> Self {
Self {
topology,
nodes: Vec::new(),
link_loss_db,
tx_power_dbm,
}
}
pub fn add_node(&mut self, node: NetworkNode) {
self.nodes.push(node);
}
pub fn bus(n_nodes: usize, tx_power_dbm: f64, link_loss_db: f64, node_loss_db: f64) -> Self {
let mut net = Self::new(NetworkTopology::Bus, tx_power_dbm, link_loss_db);
for i in 0..n_nodes {
net.add_node(NetworkNode::new(i, node_loss_db));
}
net
}
pub fn ring(n_nodes: usize, tx_power_dbm: f64, link_loss_db: f64, node_loss_db: f64) -> Self {
let mut net = Self::new(NetworkTopology::Ring, tx_power_dbm, link_loss_db);
for i in 0..n_nodes {
net.add_node(NetworkNode::new(i, node_loss_db));
}
net
}
pub fn n_nodes(&self) -> usize {
self.nodes.len()
}
pub fn power_at_node_bus(&self, k: usize) -> f64 {
if k >= self.nodes.len() {
return f64::NEG_INFINITY;
}
let mut power = self.tx_power_dbm;
for i in 0..k {
power -= self.link_loss_db;
power -= self.nodes[i].loss_db;
}
power -= self.link_loss_db; power
}
pub fn power_profile_bus(&self) -> Vec<f64> {
(0..self.nodes.len())
.map(|k| self.power_at_node_bus(k))
.collect()
}
pub fn max_nodes_bus(&self, rx_sensitivity_dbm: f64, node_loss_db: f64) -> usize {
let loss_per_hop = self.link_loss_db + node_loss_db;
let budget = self.tx_power_dbm - rx_sensitivity_dbm;
if loss_per_hop <= 0.0 {
return usize::MAX;
}
(budget / loss_per_hop).floor() as usize
}
pub fn tx_power_mw(&self) -> f64 {
10.0_f64.powf(self.tx_power_dbm / 10.0)
}
pub fn power_efficiency(&self) -> f64 {
match self.topology {
NetworkTopology::Bus => {
let tx_mw = self.tx_power_mw();
if tx_mw <= 0.0 {
return 0.0;
}
let mut power_mw = tx_mw;
let mut total_consumed_mw = 0.0f64;
let link_factor = 10.0_f64.powf(-self.link_loss_db / 10.0);
for node in &self.nodes {
power_mw *= link_factor; let node_factor = 10.0_f64.powf(-node.loss_db / 10.0);
total_consumed_mw += power_mw * (1.0 - node_factor);
power_mw *= node_factor; }
(total_consumed_mw / tx_mw).min(1.0)
}
NetworkTopology::Ring => {
let tx_mw = self.tx_power_mw();
if tx_mw <= 0.0 {
return 0.0;
}
let link_factor = 10.0_f64.powf(-self.link_loss_db / 10.0);
let mut power_mw = tx_mw;
let mut total_consumed_mw = 0.0f64;
for node in &self.nodes {
power_mw *= link_factor; let node_factor = 10.0_f64.powf(-node.loss_db / 10.0);
total_consumed_mw += power_mw * (1.0 - node_factor);
power_mw *= node_factor;
}
(total_consumed_mw / tx_mw).min(1.0)
}
NetworkTopology::Mesh => {
0.0
}
}
}
}
#[derive(Debug, Clone)]
pub struct TopologyNode {
pub id: usize,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct TopologyEdge {
pub src: usize,
pub dst: usize,
pub length_km: f64,
pub loss_db_per_km: f64,
}
impl TopologyEdge {
pub fn total_loss_db(&self) -> f64 {
self.length_km * self.loss_db_per_km
}
}
#[derive(Debug, Clone)]
pub struct PhotonicTopology {
pub nodes: Vec<TopologyNode>,
pub edges: Vec<TopologyEdge>,
pub default_loss_db_per_km: f64,
}
impl PhotonicTopology {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
default_loss_db_per_km: 0.2,
}
}
pub fn add_node(&mut self, label: &str) -> usize {
let id = self.nodes.len();
self.nodes.push(TopologyNode {
id,
label: label.to_owned(),
});
id
}
pub fn add_link(&mut self, src: usize, dst: usize, length_km: f64) {
self.edges.push(TopologyEdge {
src,
dst,
length_km,
loss_db_per_km: self.default_loss_db_per_km,
});
}
pub fn ring_topology(n_nodes: usize, link_length_km: f64) -> Self {
let mut topo = Self::new();
for i in 0..n_nodes {
topo.add_node(&format!("node-{i}"));
}
for i in 0..n_nodes {
let next = (i + 1) % n_nodes;
topo.add_link(i, next, link_length_km);
}
topo
}
pub fn star_topology(n_leaf: usize, link_length_km: f64) -> Self {
let mut topo = Self::new();
topo.add_node("hub");
for i in 0..n_leaf {
topo.add_node(&format!("leaf-{i}"));
topo.add_link(0, i + 1, link_length_km);
}
topo
}
pub fn mesh_topology(rows: usize, cols: usize, link_length_km: f64) -> Self {
let mut topo = Self::new();
for r in 0..rows {
for c in 0..cols {
topo.add_node(&format!("n{r}-{c}"));
}
}
for r in 0..rows {
for c in 0..cols {
let idx = r * cols + c;
if c + 1 < cols {
topo.add_link(idx, idx + 1, link_length_km);
}
if r + 1 < rows {
topo.add_link(idx, idx + cols, link_length_km);
}
}
}
topo
}
pub fn n_nodes(&self) -> usize {
self.nodes.len()
}
pub fn n_edges(&self) -> usize {
self.edges.len()
}
fn adjacency_list(&self) -> Vec<Vec<(usize, f64)>> {
let n = self.nodes.len();
let mut adj = vec![Vec::new(); n];
for e in &self.edges {
let loss = e.total_loss_db();
if e.src < n && e.dst < n {
adj[e.src].push((e.dst, loss));
adj[e.dst].push((e.src, loss));
}
}
adj
}
pub fn shortest_path_loss(&self, src: usize, dst: usize) -> Option<f64> {
let n = self.nodes.len();
if src >= n || dst >= n {
return None;
}
if src == dst {
return Some(0.0);
}
let adj = self.adjacency_list();
let mut dist = vec![f64::INFINITY; n];
dist[src] = 0.0;
let mut queue: Vec<(f64, usize)> = Vec::with_capacity(n);
queue.push((0.0, src));
while !queue.is_empty() {
let min_pos = queue
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)?;
let (d, u) = queue.remove(min_pos);
if d > dist[u] {
continue;
}
if u == dst {
return Some(d);
}
for &(v, w) in &adj[u] {
let nd = d + w;
if nd < dist[v] {
dist[v] = nd;
queue.push((nd, v));
}
}
}
if dist[dst].is_finite() {
Some(dist[dst])
} else {
None
}
}
}
impl Default for PhotonicTopology {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct WavelengthPath {
pub src: usize,
pub dst: usize,
pub wavelength_nm: f64,
pub data_rate_gbps: f64,
}
impl PhotonicNetwork {
pub fn add_wavelength_path(
&mut self,
_src: usize,
_dst: usize,
_lambda_nm: f64,
) -> crate::error::Result<WavelengthPath> {
let n = self.nodes.len();
if _src >= n || _dst >= n {
return Err(crate::error::OxiPhotonError::NumericalError(format!(
"node index out of range: src={_src}, dst={_dst}, n_nodes={n}"
)));
}
Ok(WavelengthPath {
src: _src,
dst: _dst,
wavelength_nm: _lambda_nm,
data_rate_gbps: 100.0, })
}
pub fn compute_capacity_gbps(&self) -> f64 {
let noise_floor_dbm = -30.0_f64; self.nodes
.iter()
.enumerate()
.map(|(k, _)| {
let p_dbm = self.power_at_node_bus(k);
let snr_db = p_dbm - noise_floor_dbm;
let snr_linear = 10.0_f64.powf(snr_db / 10.0);
let bw_ghz = 100.0_f64; bw_ghz * (1.0 + snr_linear).log2()
})
.sum()
}
pub fn spectral_efficiency(&self) -> f64 {
let n = self.nodes.len();
if n == 0 {
return 0.0;
}
let noise_floor_dbm = -30.0_f64;
let avg_snr_linear: f64 = self
.nodes
.iter()
.enumerate()
.map(|(k, _)| {
let p_dbm = self.power_at_node_bus(k);
let snr_db = p_dbm - noise_floor_dbm;
10.0_f64.powf(snr_db / 10.0)
})
.sum::<f64>()
/ n as f64;
(1.0 + avg_snr_linear).log2()
}
pub fn shortest_path_loss(&self, src: usize, dst: usize) -> Option<f64> {
let n = self.nodes.len();
if src >= n || dst >= n {
return None;
}
if src == dst {
return Some(0.0);
}
let hops = (dst as isize - src as isize).unsigned_abs();
match self.topology {
NetworkTopology::Bus => {
let loss = hops as f64
* (self.link_loss_db + self.nodes.first().map(|nd| nd.loss_db).unwrap_or(0.0));
Some(loss)
}
NetworkTopology::Ring => {
let forward = hops;
let backward = n - hops;
let min_hops = forward.min(backward);
let loss = min_hops as f64
* (self.link_loss_db + self.nodes.first().map(|nd| nd.loss_db).unwrap_or(0.0));
Some(loss)
}
NetworkTopology::Mesh => None, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bus_power_decreases_with_distance() {
let net = PhotonicNetwork::bus(5, 0.0, 1.0, 1.0);
let profile = net.power_profile_bus();
for i in 1..profile.len() {
assert!(
profile[i] < profile[i - 1],
"Power should decrease: p[{}]={:.1} p[{}]={:.1}",
i - 1,
profile[i - 1],
i,
profile[i]
);
}
}
#[test]
fn bus_first_node_max_power() {
let net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let profile = net.power_profile_bus();
for &p in &profile[1..] {
assert!(profile[0] >= p);
}
}
#[test]
fn max_nodes_finite_and_positive() {
let net = PhotonicNetwork::bus(10, 0.0, 0.5, 1.0);
let n = net.max_nodes_bus(-20.0, 1.0);
assert!(n > 0 && n < 1000);
}
#[test]
fn network_power_efficiency_between_0_and_1() {
let net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let eta = net.power_efficiency();
assert!((0.0..=1.0).contains(&eta), "η={eta:.4}");
}
#[test]
fn ring_power_efficiency_nonzero() {
let net = PhotonicNetwork::ring(3, 0.0, 1.0, 0.5);
let eff = net.power_efficiency();
assert!(eff > 0.0, "ring efficiency must be > 0, got {eff}");
assert!(eff <= 1.0, "ring efficiency must be ≤ 1, got {eff}");
}
#[test]
fn ring_network_has_correct_node_count() {
let net = PhotonicNetwork::ring(8, 3.0, 0.3, 0.5);
assert_eq!(net.n_nodes(), 8);
}
#[test]
fn tx_power_mw_conversion() {
let net = PhotonicNetwork::bus(1, 0.0, 0.0, 0.0);
assert!((net.tx_power_mw() - 1.0).abs() < 1e-10); }
#[test]
fn power_at_node_zero_is_near_tx() {
let net = PhotonicNetwork::bus(3, 10.0, 0.0, 0.0);
let p = net.power_at_node_bus(0);
assert!((p - 10.0).abs() < 1e-10);
}
#[test]
fn ring_topology_node_count() {
let topo = PhotonicTopology::ring_topology(6, 10.0);
assert_eq!(topo.n_nodes(), 6);
assert_eq!(topo.n_edges(), 6); }
#[test]
fn star_topology_node_count() {
let topo = PhotonicTopology::star_topology(4, 5.0);
assert_eq!(topo.n_nodes(), 5); assert_eq!(topo.n_edges(), 4);
}
#[test]
fn mesh_topology_node_count() {
let topo = PhotonicTopology::mesh_topology(3, 4, 2.0);
assert_eq!(topo.n_nodes(), 12); assert_eq!(topo.n_edges(), 17);
}
#[test]
fn shortest_path_loss_same_node_zero() {
let topo = PhotonicTopology::ring_topology(5, 10.0);
let loss = topo.shortest_path_loss(2, 2);
assert_eq!(loss, Some(0.0));
}
#[test]
fn shortest_path_loss_adjacent_nodes() {
let topo = PhotonicTopology::ring_topology(5, 10.0);
let loss = topo.shortest_path_loss(0, 1).expect("should find path");
assert!((loss - 2.0).abs() < 1e-10, "loss={loss}");
}
#[test]
fn shortest_path_loss_unreachable_returns_none() {
let topo = PhotonicTopology::new(); let loss = topo.shortest_path_loss(0, 1);
assert!(loss.is_none());
}
#[test]
fn ring_shortest_path_uses_shorter_arc() {
let topo = PhotonicTopology::ring_topology(5, 10.0);
let loss_0_4 = topo.shortest_path_loss(0, 4).expect("path exists");
let loss_0_2 = topo.shortest_path_loss(0, 2).expect("path exists");
assert!(
loss_0_4 < loss_0_2,
"shorter arc should have less loss: {loss_0_4} vs {loss_0_2}"
);
}
#[test]
fn compute_capacity_gbps_positive() {
let net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let cap = net.compute_capacity_gbps();
assert!(cap > 0.0, "capacity should be positive");
}
#[test]
fn spectral_efficiency_positive() {
let net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let se = net.spectral_efficiency();
assert!(se > 0.0);
}
#[test]
fn add_wavelength_path_valid() {
let mut net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let path = net.add_wavelength_path(0, 2, 1550.0).expect("valid nodes");
assert_eq!(path.src, 0);
assert_eq!(path.dst, 2);
}
#[test]
fn add_wavelength_path_invalid_node_error() {
let mut net = PhotonicNetwork::bus(4, 0.0, 0.5, 1.0);
let result = net.add_wavelength_path(0, 10, 1550.0); assert!(result.is_err());
}
#[test]
fn network_shortest_path_loss_bus_same_node() {
let net = PhotonicNetwork::bus(5, 0.0, 1.0, 1.0);
assert_eq!(net.shortest_path_loss(2, 2), Some(0.0));
}
#[test]
fn network_shortest_path_loss_bus_adjacent() {
let net = PhotonicNetwork::bus(5, 0.0, 1.0, 1.0);
let loss = net.shortest_path_loss(0, 1).expect("path exists");
assert!((loss - 2.0).abs() < 1e-10, "loss={loss}");
}
}