use super::wdm_system::ItuChannelPlan;
use crate::error::{OxiPhotonError, Result};
#[derive(Debug, Clone)]
pub struct WavelengthSelectiveSwitch {
pub n_ports: usize,
pub n_input_ports: usize,
pub channel_plan: ItuChannelPlan,
pub insertion_loss_db: f64,
pub channel_isolation_db: f64,
pub passband_width_ghz: f64,
pub switching_matrix: Vec<Vec<Option<usize>>>,
}
impl WavelengthSelectiveSwitch {
pub fn new_1xn(n: usize, plan: ItuChannelPlan) -> Self {
let n_channels = plan.n_channels;
Self {
n_ports: n,
n_input_ports: 1,
insertion_loss_db: 5.5,
channel_isolation_db: 40.0,
passband_width_ghz: plan.spacing_ghz() * 0.75,
channel_plan: plan,
switching_matrix: vec![vec![None; n_channels]; n],
}
}
pub fn new_mxn(m: usize, n: usize, plan: ItuChannelPlan) -> Self {
let n_channels = plan.n_channels;
Self {
n_ports: n,
n_input_ports: m,
insertion_loss_db: 7.0,
channel_isolation_db: 40.0,
passband_width_ghz: plan.spacing_ghz() * 0.75,
channel_plan: plan,
switching_matrix: vec![vec![None; n_channels]; n],
}
}
pub fn route(&mut self, channel: usize, input: usize, output: usize) -> Result<()> {
self.validate_channel(channel)?;
self.validate_input(input)?;
self.validate_output(output)?;
self.switching_matrix[output][channel] = Some(input);
Ok(())
}
pub fn drop_channel(&mut self, channel: usize, output: usize) -> Result<()> {
self.validate_channel(channel)?;
self.validate_output(output)?;
self.switching_matrix[output][channel] = None;
Ok(())
}
pub fn add_channel(&mut self, channel: usize, input: usize, output: usize) -> Result<()> {
self.route(channel, input, output)
}
pub fn passband_penalty_db(&self, n_cascades: usize) -> f64 {
0.5 * n_cascades as f64
}
pub fn max_cascades_before_penalty_exceeds(&self, max_penalty_db: f64) -> usize {
if max_penalty_db <= 0.0 {
return 0;
}
(max_penalty_db / 0.5).floor() as usize
}
pub fn active_connections(&self) -> usize {
self.switching_matrix
.iter()
.flat_map(|row| row.iter())
.filter(|c| c.is_some())
.count()
}
pub fn get_route(&self, channel: usize, output: usize) -> Option<usize> {
self.switching_matrix
.get(output)
.and_then(|row| row.get(channel))
.copied()
.flatten()
}
fn validate_channel(&self, channel: usize) -> Result<()> {
if channel >= self.channel_plan.n_channels {
Err(OxiPhotonError::InvalidLayer(format!(
"channel {channel} out of range (n_channels={})",
self.channel_plan.n_channels
)))
} else {
Ok(())
}
}
fn validate_input(&self, input: usize) -> Result<()> {
if input >= self.n_input_ports {
Err(OxiPhotonError::InvalidLayer(format!(
"input port {input} out of range (n_input_ports={})",
self.n_input_ports
)))
} else {
Ok(())
}
}
fn validate_output(&self, output: usize) -> Result<()> {
if output >= self.n_ports {
Err(OxiPhotonError::InvalidLayer(format!(
"output port {output} out of range (n_ports={})",
self.n_ports
)))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub struct RoadmNode {
pub degree: usize,
pub express_wss: Vec<WavelengthSelectiveSwitch>,
pub add_wss: WavelengthSelectiveSwitch,
pub drop_wss: WavelengthSelectiveSwitch,
pub node_loss_db: f64,
colorless: bool,
directionless: bool,
contentionless: bool,
add_channels: Vec<usize>,
drop_channels: Vec<usize>,
}
impl RoadmNode {
pub fn new(degree: usize, plan: ItuChannelPlan) -> Self {
let n_ch = plan.n_channels;
let express_wss = (0..degree)
.map(|_| WavelengthSelectiveSwitch::new_1xn(degree, plan.clone()))
.collect();
Self {
degree,
express_wss,
add_wss: WavelengthSelectiveSwitch::new_1xn(degree, plan.clone()),
drop_wss: WavelengthSelectiveSwitch::new_1xn(degree, plan),
node_loss_db: 3.0,
colorless: false,
directionless: false,
contentionless: false,
add_channels: Vec::with_capacity(n_ch),
drop_channels: Vec::with_capacity(n_ch),
}
}
pub fn with_cdc(mut self) -> Self {
self.colorless = true;
self.directionless = true;
self.contentionless = true;
self
}
pub fn add_drop_channels(&mut self, add_channels: &[usize], drop_channels: &[usize]) {
self.add_channels.clear();
self.add_channels.extend_from_slice(add_channels);
self.drop_channels.clear();
self.drop_channels.extend_from_slice(drop_channels);
}
pub fn express_channels(
&mut self,
channels: &[usize],
in_port: usize,
out_port: usize,
) -> Result<()> {
if in_port >= self.degree {
return Err(OxiPhotonError::InvalidLayer(format!(
"in_port {in_port} >= degree {}",
self.degree
)));
}
for &ch in channels {
self.express_wss[in_port].route(ch, 0, out_port)?;
}
Ok(())
}
pub fn total_loss_db(&self) -> f64 {
self.express_wss.first().map_or(self.node_loss_db, |w| {
w.insertion_loss_db + self.node_loss_db
})
}
pub fn add_drop_loss_db(&self) -> f64 {
self.add_wss.insertion_loss_db + self.drop_wss.insertion_loss_db + self.node_loss_db
}
pub fn is_colorless(&self) -> bool {
self.colorless
}
pub fn is_directionless(&self) -> bool {
self.directionless
}
pub fn is_contentionless(&self) -> bool {
self.contentionless
}
pub fn n_add_channels(&self) -> usize {
self.add_channels.len()
}
pub fn n_drop_channels(&self) -> usize {
self.drop_channels.len()
}
pub fn added_channels(&self) -> &[usize] {
&self.add_channels
}
pub fn dropped_channels(&self) -> &[usize] {
&self.drop_channels
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OxcGranularity {
Fiber,
Waveband,
Wavelength,
}
#[derive(Debug, Clone)]
pub struct OpticalCrossConnect {
pub n_ports: usize,
pub n_wavelengths: usize,
pub switching_granularity: OxcGranularity,
}
impl OpticalCrossConnect {
pub fn new(n_ports: usize, n_wl: usize, gran: OxcGranularity) -> Self {
Self {
n_ports,
n_wavelengths: n_wl,
switching_granularity: gran,
}
}
pub fn blocking_probability(&self, load_erlangs: f64) -> f64 {
let c = match self.switching_granularity {
OxcGranularity::Wavelength => self.n_wavelengths,
OxcGranularity::Waveband => (self.n_wavelengths / 4).max(1),
OxcGranularity::Fiber => 1,
};
erlang_b(load_erlangs, c)
}
pub fn port_count(&self) -> usize {
self.n_ports * 2
}
pub fn insertion_loss_db(&self) -> f64 {
3.0 + (self.n_ports as f64).log2()
}
pub fn max_throughput_tbps(&self, bits_per_channel_gbps: f64) -> f64 {
self.n_ports as f64 * self.n_wavelengths as f64 * bits_per_channel_gbps / 1e3
}
}
fn erlang_b(a: f64, c: usize) -> f64 {
if c == 0 {
return 1.0;
}
if a <= 0.0 {
return 0.0;
}
let mut b = 1.0_f64;
for k in 1..=(c as u64) {
b = a * b / (k as f64 + a * b);
}
b
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optical_network::wdm_system::ItuChannelPlan;
use approx::assert_abs_diff_eq;
fn test_plan() -> ItuChannelPlan {
ItuChannelPlan::new_c_band_100ghz()
}
#[test]
fn wss_1xn_creation() {
let wss = WavelengthSelectiveSwitch::new_1xn(8, test_plan());
assert_eq!(wss.n_ports, 8);
assert_eq!(wss.n_input_ports, 1);
assert_eq!(wss.active_connections(), 0);
}
#[test]
fn wss_route_and_get() {
let mut wss = WavelengthSelectiveSwitch::new_1xn(4, test_plan());
wss.route(5, 0, 2).expect("valid route");
assert_eq!(wss.get_route(5, 2), Some(0));
assert_eq!(wss.get_route(5, 1), None);
}
#[test]
fn wss_drop_channel() {
let mut wss = WavelengthSelectiveSwitch::new_1xn(4, test_plan());
wss.route(3, 0, 1).expect("ok");
wss.drop_channel(3, 1).expect("ok");
assert_eq!(wss.get_route(3, 1), None);
}
#[test]
fn wss_invalid_channel_returns_error() {
let mut wss = WavelengthSelectiveSwitch::new_1xn(4, test_plan());
let result = wss.route(999, 0, 0);
assert!(result.is_err());
}
#[test]
fn wss_passband_penalty_linear() {
let wss = WavelengthSelectiveSwitch::new_1xn(4, test_plan());
assert_abs_diff_eq!(wss.passband_penalty_db(10), 5.0, epsilon = 1e-9);
}
#[test]
fn wss_max_cascades() {
let wss = WavelengthSelectiveSwitch::new_1xn(4, test_plan());
assert_eq!(wss.max_cascades_before_penalty_exceeds(3.0), 6);
}
#[test]
fn roadm_node_creation() {
let node = RoadmNode::new(4, test_plan());
assert_eq!(node.degree, 4);
assert_eq!(node.express_wss.len(), 4);
assert!(!node.is_colorless());
}
#[test]
fn roadm_node_cdc() {
let node = RoadmNode::new(4, test_plan()).with_cdc();
assert!(node.is_colorless());
assert!(node.is_directionless());
assert!(node.is_contentionless());
}
#[test]
fn roadm_add_drop_channels() {
let mut node = RoadmNode::new(4, test_plan());
node.add_drop_channels(&[0, 1, 2], &[5, 6]);
assert_eq!(node.n_add_channels(), 3);
assert_eq!(node.n_drop_channels(), 2);
}
#[test]
fn roadm_express_valid_route() {
let mut node = RoadmNode::new(4, test_plan());
let result = node.express_channels(&[0, 1], 0, 2);
assert!(result.is_ok());
}
#[test]
fn roadm_total_loss_positive() {
let node = RoadmNode::new(4, test_plan());
assert!(node.total_loss_db() > 0.0);
}
#[test]
fn oxc_blocking_erlang_b() {
let oxc = OpticalCrossConnect::new(8, 40, OxcGranularity::Wavelength);
let bp = oxc.blocking_probability(10.0);
assert!((0.0..=1.0).contains(&bp));
assert!(bp < 0.01, "blocking={bp:.6}");
}
#[test]
fn oxc_blocking_increases_with_load() {
let oxc = OpticalCrossConnect::new(4, 4, OxcGranularity::Wavelength);
let bp_low = oxc.blocking_probability(1.0);
let bp_high = oxc.blocking_probability(10.0);
assert!(bp_high > bp_low);
}
#[test]
fn oxc_insertion_loss_positive() {
let oxc = OpticalCrossConnect::new(32, 80, OxcGranularity::Wavelength);
assert!(oxc.insertion_loss_db() > 0.0);
}
#[test]
fn erlang_b_zero_load() {
assert_abs_diff_eq!(erlang_b(0.0, 10), 0.0, epsilon = 1e-9);
}
#[test]
fn erlang_b_zero_circuits() {
assert_abs_diff_eq!(erlang_b(5.0, 0), 1.0, epsilon = 1e-9);
}
}