Skip to content

Commit 4815d9f

Browse files
committed
extended demand placement analysis
1 parent ab82045 commit 4815d9f

34 files changed

Lines changed: 1839 additions & 646 deletions

docs/reference/api-full.md

Lines changed: 287 additions & 46 deletions
Large diffs are not rendered by default.

ngraph/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from __future__ import annotations
99

1010
from . import cli, config, logging
11-
from .results.artifacts import CapacityEnvelope, PlacementResultSet, TrafficMatrixSet
11+
from .demand.matrix import TrafficMatrixSet
12+
from .results.artifacts import CapacityEnvelope, PlacementResultSet
1213

1314
__all__ = [
1415
"cli",

ngraph/demand/manager/builder.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,43 @@
1-
from __future__ import annotations
2-
31
"""Builders for traffic matrices.
42
5-
Public functions in this module assemble traffic matrices from higher-level
6-
inputs. This is a placeholder for future matrix construction utilities.
3+
Construct `TrafficMatrixSet` from raw dictionaries (e.g. parsed YAML).
4+
This logic was previously embedded in `Scenario.from_yaml`.
75
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Dict, List
10+
11+
from ngraph.demand.matrix import TrafficMatrixSet
12+
from ngraph.demand.spec import TrafficDemand
13+
from ngraph.yaml_utils import normalize_yaml_dict_keys
14+
15+
16+
def build_traffic_matrix_set(raw: Dict[str, List[dict]]) -> TrafficMatrixSet:
17+
"""Build a `TrafficMatrixSet` from a mapping of name -> list of dicts.
18+
19+
Args:
20+
raw: Mapping where each key is a matrix name and each value is a list of
21+
dictionaries with `TrafficDemand` constructor fields.
22+
23+
Returns:
24+
Initialized `TrafficMatrixSet` with constructed `TrafficDemand` objects.
25+
26+
Raises:
27+
ValueError: If ``raw`` is not a mapping of name -> list[dict].
28+
"""
29+
if not isinstance(raw, dict):
30+
raise ValueError(
31+
"'traffic_matrix_set' must be a mapping of name -> list[TrafficDemand]"
32+
)
33+
34+
normalized_raw = normalize_yaml_dict_keys(raw)
35+
tms = TrafficMatrixSet()
36+
for name, td_list in normalized_raw.items():
37+
if not isinstance(td_list, list):
38+
raise ValueError(
39+
f"Matrix '{name}' must map to a list of TrafficDemand dicts"
40+
)
41+
tms.add(name, [TrafficDemand(**d) for d in td_list])
42+
43+
return tms

ngraph/demand/manager/expand.py

Lines changed: 193 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,196 @@
1-
from __future__ import annotations
2-
31
"""Expansion helpers for traffic demand specifications.
42
5-
Functions in this module transform high-level demand specs into concrete
6-
`Demand` objects. This is a placeholder module; expansion is currently
7-
implemented in `TrafficManager`.
3+
Public functions here convert user-facing `TrafficDemand` specifications into
4+
concrete `Demand` objects that can be placed on a `StrictMultiDiGraph`.
5+
6+
This module provides the pure expansion logic that was previously embedded in
7+
`TrafficManager`.
88
"""
9+
10+
from __future__ import annotations
11+
12+
from typing import Dict, List, Tuple, Union
13+
14+
from ngraph.algorithms.flow_init import init_flow_graph
15+
from ngraph.demand import Demand
16+
from ngraph.demand.spec import TrafficDemand
17+
from ngraph.flows.policy import FlowPolicyConfig, get_flow_policy
18+
from ngraph.graph.strict_multidigraph import StrictMultiDiGraph
19+
from ngraph.model.network import Network, Node
20+
21+
try:
22+
# Avoid importing at runtime if not needed while keeping type hints precise
23+
from typing import TYPE_CHECKING
24+
25+
if TYPE_CHECKING: # pragma: no cover - typing only
26+
from ngraph.model.view import NetworkView
27+
except Exception: # pragma: no cover - defensive for environments without extras
28+
TYPE_CHECKING = False
29+
30+
31+
def expand_demands(
32+
network: Union[Network, "NetworkView"],
33+
graph: StrictMultiDiGraph | None,
34+
traffic_demands: List[TrafficDemand],
35+
default_flow_policy_config: FlowPolicyConfig,
36+
) -> Tuple[List[Demand], Dict[str, List[Demand]]]:
37+
"""Expand traffic demands into concrete `Demand` objects.
38+
39+
The result is a flat list of `Demand` plus a mapping from
40+
``TrafficDemand.id`` to the list of expanded demands for that entry.
41+
42+
Args:
43+
network: Network or NetworkView used for node group selection.
44+
graph: Flow graph to operate on. If ``None``, expansion that requires
45+
graph mutation (pseudo nodes/edges) is skipped.
46+
traffic_demands: List of high-level traffic demand specifications.
47+
default_flow_policy_config: Default policy to apply when a demand does
48+
not specify an explicit `flow_policy`.
49+
50+
Returns:
51+
A tuple ``(expanded, td_map)`` where:
52+
- ``expanded`` is the flattened, sorted list of all expanded demands
53+
(sorted by ascending ``demand_class``).
54+
- ``td_map`` maps ``TrafficDemand.id`` to its expanded demands.
55+
"""
56+
td_to_demands: Dict[str, List[Demand]] = {}
57+
expanded: List[Demand] = []
58+
59+
for td in traffic_demands:
60+
# Gather node groups for source and sink
61+
src_groups = network.select_node_groups_by_path(td.source_path)
62+
snk_groups = network.select_node_groups_by_path(td.sink_path)
63+
64+
if not src_groups or not snk_groups:
65+
td_to_demands[td.id] = []
66+
continue
67+
68+
demands_of_td: List[Demand] = []
69+
if td.mode == "combine":
70+
_expand_combine(
71+
demands_of_td,
72+
td,
73+
src_groups,
74+
snk_groups,
75+
graph,
76+
default_flow_policy_config,
77+
)
78+
elif td.mode == "pairwise":
79+
_expand_pairwise(
80+
demands_of_td,
81+
td,
82+
src_groups,
83+
snk_groups,
84+
default_flow_policy_config,
85+
)
86+
else:
87+
raise ValueError(f"Unknown mode: {td.mode}")
88+
89+
expanded.extend(demands_of_td)
90+
td_to_demands[td.id] = demands_of_td
91+
92+
# Sort final demands by ascending demand_class (i.e., priority)
93+
expanded.sort(key=lambda d: d.demand_class)
94+
return expanded, td_to_demands
95+
96+
97+
def _expand_combine(
98+
expanded: List[Demand],
99+
td: TrafficDemand,
100+
src_groups: Dict[str, List[Node]],
101+
snk_groups: Dict[str, List[Node]],
102+
graph: StrictMultiDiGraph | None,
103+
default_flow_policy_config: FlowPolicyConfig,
104+
) -> None:
105+
"""Expand a single demand using the ``combine`` mode.
106+
107+
Adds pseudo-source and pseudo-sink nodes, connects them to real nodes
108+
with infinite-capacity, zero-cost edges, and creates one aggregate
109+
`Demand` from pseudo-source to pseudo-sink with the full volume.
110+
"""
111+
# Flatten the source and sink node lists
112+
src_nodes = [node for group_nodes in src_groups.values() for node in group_nodes]
113+
dst_nodes = [node for group_nodes in snk_groups.values() for node in group_nodes]
114+
115+
if not src_nodes or not dst_nodes or graph is None:
116+
return
117+
118+
# Create pseudo-source / pseudo-sink names
119+
pseudo_source_name = f"combine_src::{td.id}"
120+
pseudo_sink_name = f"combine_snk::{td.id}"
121+
122+
# Add pseudo nodes to the graph (no-op if they already exist)
123+
graph.add_node(pseudo_source_name)
124+
graph.add_node(pseudo_sink_name)
125+
126+
# Link pseudo-source to real sources, and real sinks to pseudo-sink
127+
for s_node in src_nodes:
128+
graph.add_edge(pseudo_source_name, s_node.name, capacity=float("inf"), cost=0)
129+
for t_node in dst_nodes:
130+
graph.add_edge(t_node.name, pseudo_sink_name, capacity=float("inf"), cost=0)
131+
132+
init_flow_graph(graph) # Re-initialize flow-related attributes
133+
134+
# Create a single Demand with the full volume
135+
if td.flow_policy:
136+
flow_policy = td.flow_policy.deep_copy()
137+
else:
138+
fp_config = td.flow_policy_config or default_flow_policy_config
139+
flow_policy = get_flow_policy(fp_config)
140+
141+
expanded.append(
142+
Demand(
143+
src_node=pseudo_source_name,
144+
dst_node=pseudo_sink_name,
145+
volume=td.demand,
146+
demand_class=td.priority,
147+
flow_policy=flow_policy,
148+
)
149+
)
150+
151+
152+
def _expand_pairwise(
153+
expanded: List[Demand],
154+
td: TrafficDemand,
155+
src_groups: Dict[str, List[Node]],
156+
snk_groups: Dict[str, List[Node]],
157+
default_flow_policy_config: FlowPolicyConfig,
158+
) -> None:
159+
"""Expand a single demand using the ``pairwise`` mode.
160+
161+
Creates one `Demand` for each valid source-destination pair (excluding
162+
self-pairs) and splits total volume evenly across pairs.
163+
"""
164+
# Flatten the source and sink node lists
165+
src_nodes = [node for group_nodes in src_groups.values() for node in group_nodes]
166+
dst_nodes = [node for group_nodes in snk_groups.values() for node in group_nodes]
167+
168+
# Generate all valid (src, dst) pairs
169+
valid_pairs = [
170+
(s_node, t_node)
171+
for s_node in src_nodes
172+
for t_node in dst_nodes
173+
if s_node.name != t_node.name
174+
]
175+
pair_count = len(valid_pairs)
176+
if pair_count == 0:
177+
return
178+
179+
demand_per_pair = td.demand / float(pair_count)
180+
181+
for s_node, t_node in valid_pairs:
182+
if td.flow_policy:
183+
flow_policy = td.flow_policy.deep_copy()
184+
else:
185+
fp_config = td.flow_policy_config or default_flow_policy_config
186+
flow_policy = get_flow_policy(fp_config)
187+
188+
expanded.append(
189+
Demand(
190+
src_node=s_node.name,
191+
dst_node=t_node.name,
192+
volume=demand_per_pair,
193+
demand_class=td.priority,
194+
flow_policy=flow_policy,
195+
)
196+
)

0 commit comments

Comments
 (0)