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:
from_engine_props()extracts and validates all configuration values__post_init__()validates configuration consistencyThe 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:
NetworkState creates
LinkSpectrumobjects for each link in topologyEach LinkSpectrum initializes spectrum arrays (all zeros = all free)
Lightpath registry is empty
next_lightpath_idstarts 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 |
|---|---|
|
Skip to allocation, use existing lightpath |
|
Continue to routing with |
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_gbpson lightpathConfirm grooming is enabled in config
Spectrum “leak” (not being released):
Verify
remove_lightpath()is called when lightpath emptiesCheck that all links in path are having spectrum released
See Also
Domain Objects - Detailed field documentation
Orchestrator Guide - Pipeline implementation details
Core Module - How core uses these domain objects