Object Lifecycle in the Pipeline

This document explains how domain objects are created, modified, and accessed as a request flows through the simulation pipeline. Understanding this lifecycle is essential for debugging, extending the simulator, and implementing new features.

Overview: The Big Picture

The following diagram shows the complete lifecycle of domain objects during request processing:

STARTUP PHASE
=============

engine_props (dict)
      |
      v
+-------------------+
| SimulationConfig  |  <-- Created once, immutable
+-------------------+
      |
      | from_config()
      v
+-------------------+
| NetworkState      |  <-- Created with empty spectrum
+-------------------+


REQUEST PROCESSING PHASE (for each request)
===========================================

+-------------------+
|     Request       |  <-- Generated with source, dest, bandwidth
+-------------------+
      |
      v
+-------------------+     +-------------------+
| SDNOrchestrator   |<--->| NetworkState      |  <-- Passed per-call
+-------------------+     | (mutable)         |
      |                   +-------------------+
      |
      +-----> Stage 1: Grooming -----> GroomingResult
      |
      +-----> Stage 2: Routing ------> RouteResult
      |
      +-----> Stage 3: Spectrum -----> SpectrumResult
      |
      +-----> Stage 4: SNR ----------> SNRResult
      |
      +-----> Stage 5: Slicing ------> SlicingResult
      |
      +-----> Stage 6: Protection ---> ProtectionResult
      |
      v
+-------------------+
| AllocationResult  |  <-- Final outcome (immutable)
+-------------------+
      |
      | if success
      v
+-------------------+     +-------------------+
| NetworkState      |---->|    Lightpath      |  <-- New lightpath created
| .create_lightpath |     | (added to state)  |
+-------------------+     +-------------------+


DEPARTURE PHASE
===============

+-------------------+
| Request (DEPART)  |
+-------------------+
      |
      v
+-------------------+     +-------------------+
| NetworkState      |---->|    Lightpath      |
| .release_bandwidth|     | (capacity freed)  |
+-------------------+     +-------------------+
      |
      | if lightpath empty
      v
+-------------------+
| NetworkState      |  <-- Lightpath removed, spectrum released
| .remove_lightpath |
+-------------------+

Startup Phase

At simulation startup, configuration is parsed and network state is initialized.

Step 1: Configuration Creation

# CLI/Config parsing produces engine_props dict
engine_props = {
    'network': 'USbackbone60',
    'cores_per_link': 7,
    'c_band': 320,
    # ... many more options
}

# SimulationConfig is created once (immutable)
from fusion.domain import SimulationConfig
config = SimulationConfig.from_engine_props(engine_props)

What happens:

  1. from_engine_props() extracts and validates all configuration values

  2. __post_init__() validates configuration consistency

  3. The frozen dataclass prevents any future modification

Step 2: NetworkState Initialization

from fusion.domain import NetworkState

# Create from configuration and topology
state = NetworkState.from_config(config, topology)

What happens:

  1. NetworkState creates LinkSpectrum objects for each link in topology

  2. Each LinkSpectrum initializes spectrum arrays (all zeros = all free)

  3. Lightpath registry is empty

  4. next_lightpath_id starts at 1

Object state after startup:

SimulationConfig (frozen)
├── network_name: "USbackbone60"
├── cores_per_link: 7
├── band_slots: {"c": 320}
└── ... (all read-only)

NetworkState (mutable)
├── _topology: NetworkX Graph
├── _spectrum: {
│     ("0", "1"): LinkSpectrum(all zeros),
│     ("0", "2"): LinkSpectrum(all zeros),
│     ...
│   }
├── _lightpaths: {}  (empty)
└── _next_lightpath_id: 1

Request Arrival Processing

When a request arrives, it flows through the orchestrator’s pipeline stages.

Stage 0: Request Generation

from fusion.domain import Request, RequestType

# Request is generated by traffic generator
request = Request(
    request_id=42,
    source="0",
    destination="5",
    bandwidth_gbps=100,
    arrive=0.5,
    depart=3600.5,
    request_type=RequestType.ARRIVAL,
)

Request state: status=PENDING

Stage 1: Grooming Pipeline

Purpose: Check if existing lightpaths can serve this request.

Input Objects:
├── Request (read-only)
├── NetworkState (read-only query)
└── SimulationConfig (read-only)

Output Object:
└── GroomingResult (immutable)

Object interactions:

# Pipeline queries NetworkState for existing lightpaths
existing_lps = state.get_lightpaths_for_endpoints(
    request.source, request.destination
)

# Check capacity on each lightpath
for lp in existing_lps:
    if lp.can_accommodate(request.bandwidth_gbps):
        # Found capacity - can groom!
        return GroomingResult(
            fully_groomed=True,
            bandwidth_groomed_gbps=request.bandwidth_gbps,
            lightpaths_used=(lp.lightpath_id,),
        )

Possible outcomes:

Outcome

Next Step

fully_groomed=True

Skip to allocation, use existing lightpath

partially_groomed=True

Continue to routing with remaining_bandwidth_gbps

No grooming possible

Continue to routing with full bandwidth

Stage 2: Routing Pipeline

Purpose: Find candidate paths from source to destination.

Input Objects:
├── Request (read-only)
├── NetworkState.topology (read-only)
├── GroomingResult.forced_path (if partial grooming)
└── SimulationConfig (read-only)

Output Object:
└── RouteResult (immutable)

Object interactions:

# If partial grooming, path is forced
if grooming_result.forced_path:
    paths = (grooming_result.forced_path,)
else:
    # Query topology for k-shortest paths
    paths = routing_algorithm.find_paths(
        state.topology,
        request.source,
        request.destination,
        k=config.k_paths,
    )

# Calculate path weights and modulation options
return RouteResult(
    paths=paths,
    weights_km=(...),
    modulations=(...),
    strategy_name="k_shortest_path",
)

RouteResult structure:

RouteResult
├── paths: (("0", "2", "5"), ("0", "3", "5"))
├── weights_km: (150.0, 180.0)
├── modulations: (("QPSK", "16-QAM"), ("QPSK",))
└── is_empty: False

Stage 3: Spectrum Assignment Pipeline

Purpose: Find contiguous free spectrum on the selected path.

Input Objects:
├── Request (read-only)
├── NetworkState (read-only query)
├── RouteResult (path selection)
└── SimulationConfig (read-only)

Output Object:
└── SpectrumResult (immutable)

Object interactions:

# Try each path until one succeeds
for path_idx, path in enumerate(route_result.paths):
    modulation = route_result.modulations[path_idx][0]  # Best modulation

    # Calculate slots needed
    slots_needed = calculate_slots(request.bandwidth_gbps, modulation)

    # Query NetworkState for free spectrum
    for core in range(config.cores_per_link):
        slot = state.find_first_fit(path, core, config.band, slots_needed)
        if slot is not None:
            return SpectrumResult(
                is_free=True,
                start_slot=slot,
                end_slot=slot + slots_needed,
                core=core,
                band="c",
                modulation=modulation,
                slots_needed=slots_needed,
            )

# No spectrum found on any path
return SpectrumResult(is_free=False, slots_needed=slots_needed)

NetworkState spectrum query:

For path ["0", "2", "5"]:

Link ("0", "2"):
├── Core 0, Band "c": [0,0,0,1,1,1,1,0,0,0,...]
└── Slots 0-3 are free

Link ("2", "5"):
├── Core 0, Band "c": [0,0,0,0,0,0,0,0,1,1,...]
└── Slots 0-8 are free

Common free range: slots 0-3 (need intersection across all links)

Stage 4: SNR Validation Pipeline

Purpose: Verify signal quality meets modulation threshold.

Input Objects:
├── Request (read-only)
├── SpectrumResult (allocation details)
├── NetworkState (link parameters)
└── SimulationConfig (read-only)

Output Object:
└── SNRResult (immutable)

Object interactions:

# Calculate SNR for the allocated spectrum
snr_db = calculate_snr(
    path=route_result.paths[selected_path_idx],
    modulation=spectrum_result.modulation,
    core=spectrum_result.core,
    network_state=state,
)

# Check against threshold
required_snr = get_required_snr(spectrum_result.modulation)

if snr_db >= required_snr:
    return SNRResult(
        passed=True,
        snr_db=snr_db,
        required_snr_db=required_snr,
        margin_db=snr_db - required_snr,
    )
else:
    return SNRResult(
        passed=False,
        snr_db=snr_db,
        required_snr_db=required_snr,
    )

Stage 5: Slicing Pipeline (if enabled)

Purpose: Split request across multiple lightpaths if standard allocation fails.

Input Objects:
├── Request (bandwidth requirement)
├── RouteResult (all candidate paths)
├── NetworkState (spectrum queries)
└── SimulationConfig (read-only)

Output Object:
└── SlicingResult (immutable)

Object interactions:

# If standard allocation failed, try slicing
slice_bandwidth = request.bandwidth_gbps // max_slices

lightpath_ids = []
for slice_num in range(max_slices):
    # Try to allocate each slice
    spectrum = find_spectrum_for_slice(...)
    if spectrum.is_free:
        lp_id = state.create_lightpath(...)  # Creates lightpath
        lightpath_ids.append(lp_id)

if len(lightpath_ids) == max_slices:
    return SlicingResult(
        success=True,
        num_slices=max_slices,
        slice_bandwidth_gbps=slice_bandwidth,
        lightpath_ids=tuple(lightpath_ids),
    )

Stage 6: Protection Pipeline (if enabled)

Purpose: Establish disjoint primary and backup paths for 1+1 protection.

Input Objects:
├── Request (protection requirement)
├── RouteResult (must include backup_paths)
├── NetworkState (spectrum for both paths)
└── SimulationConfig (read-only)

Output Object:
└── ProtectionResult (immutable)

Final: AllocationResult Assembly

# Assemble final result from all pipeline outputs
if spectrum_result.is_free and snr_result.passed:
    return AllocationResult(
        success=True,
        lightpaths_created=(new_lightpath_id,),
        total_bandwidth_allocated_gbps=request.bandwidth_gbps,
        route_result=route_result,
        spectrum_result=spectrum_result,
        snr_result=snr_result,
    )
else:
    return AllocationResult(
        success=False,
        block_reason=determine_block_reason(...),
    )

State Updates on Success

When allocation succeeds, NetworkState is updated with the new lightpath.

Creating a New Lightpath

if result.success:
    lightpath_id = state.create_lightpath(
        path=selected_path,
        start_slot=spectrum_result.start_slot,
        end_slot=spectrum_result.end_slot,
        core=spectrum_result.core,
        band=spectrum_result.band,
        modulation=spectrum_result.modulation,
        total_bandwidth_gbps=request.bandwidth_gbps,
    )

What happens inside NetworkState.create_lightpath():

1. Create Lightpath object:
   Lightpath(
       lightpath_id=1,
       path=["0", "2", "5"],
       start_slot=0,
       end_slot=8,
       ...
   )

2. Update spectrum arrays on each link:
   For link ("0", "2"):
       spectrum[core=0][band="c"][0:8] = lightpath_id

   For link ("2", "5"):
       spectrum[core=0][band="c"][0:8] = lightpath_id

3. Add to lightpath registry:
   _lightpaths[1] = lightpath

4. Increment next_lightpath_id:
   _next_lightpath_id = 2

5. Return lightpath_id

Grooming onto Existing Lightpath

if grooming_result.fully_groomed:
    # Get the existing lightpath
    lp = state.get_lightpath(grooming_result.lightpaths_used[0])

    # Allocate bandwidth to this request
    lp.allocate_bandwidth(
        request_id=request.request_id,
        bandwidth_gbps=request.bandwidth_gbps,
    )

What happens inside Lightpath.allocate_bandwidth():

Before:
├── remaining_bandwidth_gbps: 100
└── request_allocations: {}

After:
├── remaining_bandwidth_gbps: 0
└── request_allocations: {42: 100}

Request Departure Processing

When a request’s holding time expires, resources must be released.

+-------------------+
| Request (DEPART)  |
| request_id=42     |
+-------------------+
      |
      v
+-------------------+
| Find lightpath    |
| for request       |
+-------------------+
      |
      v
+-------------------+     +-------------------+
| Lightpath         |     | NetworkState      |
| .release_bandwidth|     | (may remove LP)   |
+-------------------+     +-------------------+

Releasing Bandwidth

# Find the lightpath serving this request
lp = find_lightpath_for_request(request.request_id, state)

# Release the bandwidth
released_bw = lp.release_bandwidth(request.request_id)

# Check if lightpath is now empty
if lp.is_empty:
    # Remove lightpath entirely
    state.remove_lightpath(lp.lightpath_id)

What happens inside NetworkState.remove_lightpath():

1. Get lightpath from registry:
   lp = _lightpaths[1]

2. Release spectrum on each link:
   For link ("0", "2"):
       spectrum[core=0][band="c"][0:8] = 0  (free)

   For link ("2", "5"):
       spectrum[core=0][band="c"][0:8] = 0  (free)

3. Remove from registry:
   del _lightpaths[1]

Complete Scenario Diagrams

Scenario A: Standard Allocation (Success)

Request(id=1, src="0", dst="5", bw=100)
      |
      v
[Grooming] --> GroomingResult(fully_groomed=False)
      |
      v
[Routing] --> RouteResult(paths=(("0","2","5"),), ...)
      |
      v
[Spectrum] --> SpectrumResult(is_free=True, slots=0-8, core=0)
      |
      v
[SNR] --> SNRResult(passed=True, snr=18.5)
      |
      v
AllocationResult(success=True, lightpaths_created=(1,))
      |
      v
NetworkState.create_lightpath()
      |
      v
Lightpath(id=1, path=["0","2","5"], slots=0-8)
+ Spectrum marked on links ("0","2") and ("2","5")

Scenario B: Grooming (Success)

Request(id=2, src="0", dst="5", bw=50)
      |
      v
[Grooming] --> Found LP #1 with 100 remaining
      |
      v
GroomingResult(fully_groomed=True, lightpaths_used=(1,))
      |
      v
AllocationResult(success=True, is_groomed=True)
      |
      v
Lightpath #1:
  Before: remaining=100, request_allocations={}
  After:  remaining=50,  request_allocations={2: 50}

Scenario C: Blocking (No Spectrum)

Request(id=3, src="0", dst="5", bw=400)
      |
      v
[Grooming] --> GroomingResult(fully_groomed=False)
      |
      v
[Routing] --> RouteResult(paths=(("0","2","5"),), ...)
      |
      v
[Spectrum] --> SpectrumResult(is_free=False)
      |
      v
[Slicing] --> SlicingResult(success=False)  (if enabled)
      |
      v
AllocationResult(success=False, block_reason=NO_SPECTRUM)
      |
      v
Request.status = BLOCKED
NetworkState unchanged

Scenario D: Slicing (Success)

Request(id=4, src="0", dst="5", bw=400)
      |
      v
[Grooming] --> No capacity
      |
      v
[Routing] --> 3 candidate paths
      |
      v
[Spectrum] --> is_free=False (can't fit 400 Gbps)
      |
      v
[Slicing] --> Split into 4 x 100 Gbps
      |
      +-- Create LP #2 on path 1, slots 0-8
      +-- Create LP #3 on path 1, slots 8-16
      +-- Create LP #4 on path 2, slots 0-8
      +-- Create LP #5 on path 3, slots 0-8
      |
      v
SlicingResult(success=True, num_slices=4, lightpath_ids=(2,3,4,5))
      |
      v
AllocationResult(success=True, is_sliced=True, lightpaths_created=(2,3,4,5))

Scenario E: Protection (1+1)

Request(id=5, src="0", dst="5", bw=100, is_protected=True)
      |
      v
[Routing] --> RouteResult(
      paths=(("0","2","5"),),
      backup_paths=(("0","3","4","5"),)
    )
      |
      v
[Spectrum Primary] --> SpectrumResult(is_free=True, slots=0-8)
      |
      v
[Spectrum Backup] --> SpectrumResult(is_free=True, slots=0-8)
      |
      v
[Protection] --> ProtectionResult(
      primary_established=True,
      backup_established=True,
    )
      |
      v
Lightpath #6 (working):
  path=["0","2","5"], is_protected=True, backup_path=["0","3","4","5"]

Spectrum allocated on BOTH paths

Debugging Tips

Understanding Object State

When debugging, examine these key objects:

# Check request state
print(f"Request {req.request_id}: {req.status}, blocked={req.block_reason}")

# Check lightpath state
lp = state.get_lightpath(lp_id)
print(f"LP {lp.lightpath_id}: util={lp.utilization:.1%}, reqs={lp.request_allocations}")

# Check spectrum utilization
util = state.get_spectrum_utilization()
print(f"Network utilization: {util:.1%}")

Common Issues

Request stuck at PENDING:

  • Check if routing found any paths (route_result.is_empty)

  • Check if spectrum was found (spectrum_result.is_free)

  • Check if SNR passed (snr_result.passed)

Lightpath not being groomed:

  • Verify lightpath exists for endpoint pair

  • Check remaining_bandwidth_gbps on lightpath

  • Confirm grooming is enabled in config

Spectrum “leak” (not being released):

  • Verify remove_lightpath() is called when lightpath empties

  • Check that all links in path are having spectrum released

See Also