Skip to content

Commit 999285c

Browse files
committed
demand placement implemented
1 parent a21bc56 commit 999285c

File tree

14 files changed

+1298
-164
lines changed

14 files changed

+1298
-164
lines changed

ngraph/lib/demand.py

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,100 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass, field
34
from typing import Optional, Tuple
45

5-
from ngraph.lib.graph import NodeID, StrictMultiDiGraph
66
from ngraph.lib.flow_policy import FlowPolicy
7+
from ngraph.lib.graph import NodeID, StrictMultiDiGraph
78

89

10+
@dataclass
911
class Demand:
1012
"""
11-
Represents a network demand between two nodes.
12-
13-
A Demand can be realized through one or more flows.
13+
Represents a network demand between two nodes. It is realized via one or more
14+
flows through a single FlowPolicy.
1415
"""
1516

16-
def __init__(
17-
self,
18-
src_node: NodeID,
19-
dst_node: NodeID,
20-
volume: float,
21-
demand_class: int = 0,
22-
) -> None:
17+
src_node: NodeID
18+
dst_node: NodeID
19+
volume: float
20+
demand_class: int = 0
21+
flow_policy: Optional[FlowPolicy] = None
22+
placed_demand: float = field(default=0.0, init=False)
23+
24+
def __lt__(self, other: Demand) -> bool:
2325
"""
24-
Initializes a Demand instance.
26+
Compare Demands by their demand_class (priority). A lower demand_class
27+
indicates higher priority, so it should come first in sorting.
2528
2629
Args:
27-
src_node: The source node identifier.
28-
dst_node: The destination node identifier.
29-
volume: The total volume of the demand.
30-
demand_class: An integer representing the demand's class or priority.
31-
"""
32-
self.src_node: NodeID = src_node
33-
self.dst_node: NodeID = dst_node
34-
self.volume: float = volume
35-
self.demand_class: int = demand_class
36-
self.placed_demand: float = 0.0
30+
other (Demand): Demand to compare against.
3731
38-
def __lt__(self, other: Demand) -> bool:
39-
"""Compares Demands based on their demand class."""
32+
Returns:
33+
bool: True if self has higher priority (lower class value).
34+
"""
4035
return self.demand_class < other.demand_class
4136

4237
def __str__(self) -> str:
43-
"""Returns a string representation of the Demand."""
38+
"""
39+
String representation showing src, dst, volume, priority, and placed_demand.
40+
"""
4441
return (
4542
f"Demand(src_node={self.src_node}, dst_node={self.dst_node}, "
46-
f"volume={self.volume}, demand_class={self.demand_class}, placed_demand={self.placed_demand})"
43+
f"volume={self.volume}, demand_class={self.demand_class}, "
44+
f"placed_demand={self.placed_demand})"
4745
)
4846

4947
def place(
5048
self,
5149
flow_graph: StrictMultiDiGraph,
52-
flow_policy: FlowPolicy,
5350
max_fraction: float = 1.0,
5451
max_placement: Optional[float] = None,
5552
) -> Tuple[float, float]:
5653
"""
57-
Places demand volume onto the network graph using the specified flow policy.
58-
59-
The function computes the remaining volume to place, applies any maximum
60-
placement or fraction constraints, and delegates the flow placement to the
61-
provided flow policy. It then updates the placed demand.
54+
Places demand volume onto the network via self.flow_policy.
6255
6356
Args:
64-
flow_graph: The network graph on which flows are placed.
65-
flow_policy: The flow policy used to place the demand.
66-
max_fraction: Maximum fraction of the total demand volume to place in this call.
67-
max_placement: Optional absolute limit on the volume to place.
57+
flow_graph (StrictMultiDiGraph): The graph to place flows onto.
58+
max_fraction (float): The fraction of the remaining demand to place now.
59+
max_placement (Optional[float]): An absolute upper bound on volume.
6860
6961
Returns:
70-
A tuple (placed, remaining) where 'placed' is the volume successfully placed,
71-
and 'remaining' is the volume that could not be placed.
62+
Tuple[float, float]:
63+
placed_now: Volume placed in this call.
64+
remaining: Volume that could not be placed in this call.
65+
66+
Raises:
67+
RuntimeError: If no FlowPolicy is set on this Demand.
68+
ValueError: If max_fraction is outside [0, 1].
7269
"""
73-
to_place = self.volume - self.placed_demand
70+
if self.flow_policy is None:
71+
raise RuntimeError("No FlowPolicy set on this Demand.")
7472

73+
if not (0 <= max_fraction <= 1):
74+
raise ValueError("max_fraction must be in the range [0, 1].")
75+
76+
to_place = self.volume - self.placed_demand
7577
if max_placement is not None:
7678
to_place = min(to_place, max_placement)
7779

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

85-
flow_policy.place_demand(
86+
# Delegate flow placement
87+
self.flow_policy.place_demand(
8688
flow_graph,
8789
self.src_node,
8890
self.dst_node,
8991
self.demand_class,
9092
to_place,
9193
)
92-
placed = flow_policy.placed_demand - self.placed_demand
93-
self.placed_demand = flow_policy.placed_demand
94-
remaining = to_place - placed
95-
return placed, remaining
94+
95+
# placed_now is the difference from the old placed_demand
96+
placed_now = self.flow_policy.placed_demand - self.placed_demand
97+
self.placed_demand = self.flow_policy.placed_demand
98+
remaining = to_place - placed_now
99+
100+
return placed_now, remaining

ngraph/lib/flow.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@ class FlowIndex(NamedTuple):
2020
src_node (NodeID): The source node of the flow.
2121
dst_node (NodeID): The destination node of the flow.
2222
flow_class (int): Integer representing the 'class' of this flow (e.g., traffic class).
23-
flow_id (int): A unique integer ID for this flow.
23+
flow_id (str): A unique ID for this flow.
2424
"""
2525

2626
src_node: NodeID
2727
dst_node: NodeID
2828
flow_class: int
29-
flow_id: int
29+
flow_id: str
3030

3131

3232
class Flow:
3333
"""
3434
Represents a fraction of demand routed along a given PathBundle.
3535
3636
In traffic-engineering scenarios, a `Flow` object can model:
37-
- An MPLS LSP/tunnel,
38-
- IP forwarding behavior (with ECMP),
37+
- MPLS LSPs/tunnels with explicit paths,
38+
- IP forwarding behavior (with ECMP or UCMP),
3939
- Or anything that follows a specific set of paths.
4040
"""
4141

ngraph/lib/flow_policy.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,15 @@ def place_demand(
426426
raise RuntimeError("Infinite loop detected in place_demand.")
427427

428428
# For EQUAL_BALANCED placement, rebalance flows to maintain equal volumes.
429-
if self.flow_placement == FlowPlacement.EQUAL_BALANCED:
430-
target_flow_volume = self.placed_demand / len(self.flows)
429+
if (
430+
self.flow_placement == FlowPlacement.EQUAL_BALANCED
431+
and len(self.flows) > 0 # must not rebalance if no flows
432+
):
433+
target_flow_volume = self.placed_demand / float(len(self.flows))
434+
# If the flows are not already near balanced
431435
if any(
432-
abs(target_flow_volume - flow.placed_flow) >= base.MIN_FLOW
433-
for flow in self.flows.values()
436+
abs(target_flow_volume - f.placed_flow) >= base.MIN_FLOW
437+
for f in self.flows.values()
434438
):
435439
total_placed_flow, excess_flow = self.rebalance_demand(
436440
flow_graph, src_node, dst_node, flow_class, target_flow_volume

ngraph/traffic_demand.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,58 @@
11
from __future__ import annotations
2+
23
from dataclasses import dataclass, field
34
from typing import Any, Dict
45

6+
from ngraph.lib.flow_policy import FlowPolicyConfig
7+
from ngraph.network import new_base64_uuid
8+
59

610
@dataclass(slots=True)
711
class TrafficDemand:
812
"""
913
Represents a single traffic demand in a network.
1014
15+
This class provides:
16+
- Source and sink regex patterns to match sets of nodes in the network.
17+
- A total demand volume and a priority (lower number = higher priority).
18+
- A flow policy configuration to specify routing/placement logic (if
19+
not supplied, defaults to SHORTEST_PATHS_ECMP).
20+
- A 'mode' that determines how the demand expands into per-node-pair
21+
demands. Supported modes include:
22+
* "node_to_node": default behavior (each (src, dst) pair shares
23+
the demand).
24+
* "combine": combine all matched sources and all matched sinks,
25+
then distribute the demand among the cross-product of nodes.
26+
* "pairwise": for each (src_label, dst_label) pair, split up the
27+
total demand so each label cross-product receives an equal fraction.
28+
* "one_to_one": match src_labels[i] to dst_labels[i], then split
29+
demand among node pairs in those matched labels.
30+
1131
Attributes:
12-
source (str): The name of the source node.
13-
target (str): The name of the target node.
14-
priority (int): The priority of this traffic demand. Lower values indicate higher priority (default=0).
32+
source_path (str): A regex pattern (string) for selecting source nodes.
33+
sink_path (str): A regex pattern (string) for selecting sink nodes.
34+
priority (int): A priority class for this demand (default=0).
1535
demand (float): The total demand volume (default=0.0).
16-
demand_placed (float): The placed portion of the demand (default=0.0).
17-
demand_unplaced (float): The unplaced portion of the demand (default=0.0).
18-
attrs (dict[str, Any]): A dictionary for any additional attributes (default={}).
36+
demand_placed (float): The portion of this demand that has been placed
37+
so far (default=0.0). This is updated when flows are placed.
38+
flow_policy_config (FlowPolicyConfig): The routing/placement policy.
39+
mode (str): Expansion mode for generating sub-demands (defaults to "node_to_node").
40+
attrs (Dict[str, Any]): Additional arbitrary attributes.
41+
id (str): Unique ID assigned at initialization.
1942
"""
2043

21-
source: str
22-
target: str
44+
source_path: str = ""
45+
sink_path: str = ""
2346
priority: int = 0
2447
demand: float = 0.0
2548
demand_placed: float = 0.0
26-
demand_unplaced: float = 0.0
49+
flow_policy_config: FlowPolicyConfig = FlowPolicyConfig.SHORTEST_PATHS_ECMP
50+
mode: str = "node_to_node"
2751
attrs: Dict[str, Any] = field(default_factory=dict)
52+
id: str = field(init=False)
53+
54+
def __post_init__(self) -> None:
55+
"""
56+
Generate a unique ID by combining source, sink, and a random Base64 UUID.
57+
"""
58+
self.id = f"{self.source_path}|{self.sink_path}|{new_base64_uuid()}"

0 commit comments

Comments
 (0)