Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 52 additions & 47 deletions ngraph/lib/demand.py
Original file line number Diff line number Diff line change
@@ -1,95 +1,100 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional, Tuple

from ngraph.lib.graph import NodeID, StrictMultiDiGraph
from ngraph.lib.flow_policy import FlowPolicy
from ngraph.lib.graph import NodeID, StrictMultiDiGraph


@dataclass
class Demand:
"""
Represents a network demand between two nodes.

A Demand can be realized through one or more flows.
Represents a network demand between two nodes. It is realized via one or more
flows through a single FlowPolicy.
"""

def __init__(
self,
src_node: NodeID,
dst_node: NodeID,
volume: float,
demand_class: int = 0,
) -> None:
src_node: NodeID
dst_node: NodeID
volume: float
demand_class: int = 0
flow_policy: Optional[FlowPolicy] = None
placed_demand: float = field(default=0.0, init=False)

def __lt__(self, other: Demand) -> bool:
"""
Initializes a Demand instance.
Compare Demands by their demand_class (priority). A lower demand_class
indicates higher priority, so it should come first in sorting.

Args:
src_node: The source node identifier.
dst_node: The destination node identifier.
volume: The total volume of the demand.
demand_class: An integer representing the demand's class or priority.
"""
self.src_node: NodeID = src_node
self.dst_node: NodeID = dst_node
self.volume: float = volume
self.demand_class: int = demand_class
self.placed_demand: float = 0.0
other (Demand): Demand to compare against.

def __lt__(self, other: Demand) -> bool:
"""Compares Demands based on their demand class."""
Returns:
bool: True if self has higher priority (lower class value).
"""
return self.demand_class < other.demand_class

def __str__(self) -> str:
"""Returns a string representation of the Demand."""
"""
String representation showing src, dst, volume, priority, and placed_demand.
"""
return (
f"Demand(src_node={self.src_node}, dst_node={self.dst_node}, "
f"volume={self.volume}, demand_class={self.demand_class}, placed_demand={self.placed_demand})"
f"volume={self.volume}, demand_class={self.demand_class}, "
f"placed_demand={self.placed_demand})"
)

def place(
self,
flow_graph: StrictMultiDiGraph,
flow_policy: FlowPolicy,
max_fraction: float = 1.0,
max_placement: Optional[float] = None,
) -> Tuple[float, float]:
"""
Places demand volume onto the network graph using the specified flow policy.

The function computes the remaining volume to place, applies any maximum
placement or fraction constraints, and delegates the flow placement to the
provided flow policy. It then updates the placed demand.
Places demand volume onto the network via self.flow_policy.

Args:
flow_graph: The network graph on which flows are placed.
flow_policy: The flow policy used to place the demand.
max_fraction: Maximum fraction of the total demand volume to place in this call.
max_placement: Optional absolute limit on the volume to place.
flow_graph (StrictMultiDiGraph): The graph to place flows onto.
max_fraction (float): The fraction of the remaining demand to place now.
max_placement (Optional[float]): An absolute upper bound on volume.

Returns:
A tuple (placed, remaining) where 'placed' is the volume successfully placed,
and 'remaining' is the volume that could not be placed.
Tuple[float, float]:
placed_now: Volume placed in this call.
remaining: Volume that could not be placed in this call.

Raises:
RuntimeError: If no FlowPolicy is set on this Demand.
ValueError: If max_fraction is outside [0, 1].
"""
to_place = self.volume - self.placed_demand
if self.flow_policy is None:
raise RuntimeError("No FlowPolicy set on this Demand.")

if not (0 <= max_fraction <= 1):
raise ValueError("max_fraction must be in the range [0, 1].")

to_place = self.volume - self.placed_demand
if max_placement is not None:
to_place = min(to_place, max_placement)

if max_fraction > 0:
to_place = min(to_place, self.volume * max_fraction)
else:
# When max_fraction is non-positive, place the entire volume only if infinite;
# otherwise, no placement is performed.
to_place = self.volume if self.volume == float("inf") else 0
# If max_fraction <= 0, do not place any new volume (unless volume is infinite).
to_place = self.volume if self.volume == float("inf") else 0.0

flow_policy.place_demand(
# Delegate flow placement
self.flow_policy.place_demand(
flow_graph,
self.src_node,
self.dst_node,
self.demand_class,
to_place,
)
placed = flow_policy.placed_demand - self.placed_demand
self.placed_demand = flow_policy.placed_demand
remaining = to_place - placed
return placed, remaining

# placed_now is the difference from the old placed_demand
placed_now = self.flow_policy.placed_demand - self.placed_demand
self.placed_demand = self.flow_policy.placed_demand
remaining = to_place - placed_now

return placed_now, remaining
8 changes: 4 additions & 4 deletions ngraph/lib/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ class FlowIndex(NamedTuple):
src_node (NodeID): The source node of the flow.
dst_node (NodeID): The destination node of the flow.
flow_class (int): Integer representing the 'class' of this flow (e.g., traffic class).
flow_id (int): A unique integer ID for this flow.
flow_id (str): A unique ID for this flow.
"""

src_node: NodeID
dst_node: NodeID
flow_class: int
flow_id: int
flow_id: str


class Flow:
"""
Represents a fraction of demand routed along a given PathBundle.

In traffic-engineering scenarios, a `Flow` object can model:
- An MPLS LSP/tunnel,
- IP forwarding behavior (with ECMP),
- MPLS LSPs/tunnels with explicit paths,
- IP forwarding behavior (with ECMP or UCMP),
- Or anything that follows a specific set of paths.
"""

Expand Down
Loading