Python API¶
The Python API takes typed records and queries; YAML is parsed into the same records. This guide shows the library by task; the generated reference lists every public name, and Scenario YAML, Workflow Steps and Command Line describe the file and command boundaries.
Prepare and query¶
from ngraph import (
CapacityQuery, Link, Network, Node, Scenario, Selector, UnitsSettings,
capacity, prepare,
)
network = Network()
for name in ("A", "B", "C"):
network.add_node(Node(name))
network.add_link(Link("A", "B", capacity=10, cost=1))
network.add_link(Link("B", "C", capacity=5, cost=2))
prepared = prepare(Scenario(network, units=UnitsSettings(resolution=1)))
result = capacity(prepared, CapacityQuery(Selector("^A$"), Selector("^C$")))
row = result.cases[0].states["converged"].rows[0]
assert row.result.delivered == 5
assert prepared.topology.q == 1
Network keeps editable dicts of immutable records. prepare resolves the selected demand sets, builds one native graph and returns PreparedScenario; it does not change its input. Library callers name needed demand sets with prepare(scenario, demand_sets=("traffic",)); workflow-referenced sets are included automatically.
PreparedScenario holds the read-only scenario view, its topology, the bound demand_sets and the preparation's units_report. PreparedTopology holds the graph and its labels: graph, physical node names, read-only node-name and physical-link mappings, edge_refs (each engine edge's directed link or pool attachment, in edge order), disabled (the engine's eligibility mask of what the scenario disables), q (the bandwidth unit; Units and the resolution q) and pool_heads (each finite TE ingress pool's synthetic head node; Tunnels and TE ingress pools). Every analysis takes the prepared scenario, holds it as prepared and uses the preparation's units_report, with controller weight approximations added for a later-bound demand set. A physical link has two native directions; DirectedLink(link, direction) identifies one. Returned results stay readable after the scenario and the prepared scenario are deleted. The prepared view is read-only, and later edits need a new preparation (Scenario, preparation and run). The examples on this page fix q to 1, so that units equal physical bandwidth; for amounts this small that misses the default precision target, and their units reports say so (Precision and the choice of q).
Native models and capacity¶
CapacityQuery defaults to uncapped, lossless optimized traffic engineering. mode="combine" asks one source-set/destination-set question; "pairwise" asks one question per source-group and target-group pair, in canonical order (groups sorted by label). Selector(group_by="name", path="^source") makes one group per matching node. Overlapping source and target groups are refused, except a pair that is one and the same node, which has its own zero-valued SelfPair row.
Supply a native configuration to ask another routing question:
import netgraph_core as ngc
weighted = ngc.FlowPolicyConfig(routing=ngc.ShortestPaths(
allocation=ngc.RouteAllocation.WEIGHTED,
weight_basis=ngc.WeightBasis.CAPACITY,
))
query = CapacityQuery(Selector("^A$"), Selector("^C$"), config=weighted)
row = capacity(prepared, query).cases[0].states["converged"].rows[0]
assert row.result.delivered == 5
The normalized question records the effective model, finite offer/design inputs and failure request. How each model routes and admits traffic: Routing models and admission. offer and design_demand are optional physical inputs; the result names the question asked (MaxFlow). Native integer-valued configuration fields already contain units (Results and their units).
Demands and placement¶
from ngraph import DemandSet, DemandSpec, PlacementQuery, place
traffic = DemandSet("traffic", (
DemandSpec(Selector("^A$"), Selector("^C$"), volume=4),
))
loaded = prepare(Scenario(network, demand_sets={"traffic": traffic},
units=UnitsSettings(resolution=1)),
demand_sets=("traffic",))
placement = place(loaded, PlacementQuery(loaded.demand_sets["traffic"]))
assert placement.cases[0].states["converged"].totals.delivered_units == 4
DemandSpec accepts a declared preset, native configuration or named PinnedRoutes. Lower priorities are placed first; ties keep declaration order, then canonical expansion order. Ids are unique within a run; a demand without an id gets one from its set name and declaration index (Demand Fields). Splitting keeps each declared total: resolved plus discarded units equal declared units (Demands: how traffic enters).
With routing set to a RoutingDomain, wcmp_controller to a WCMPController (its scope) and
forwarding_groups of ForwardingGroup values, each a router and its root ControllerGroups
(ForwardingGroup(router, (ControllerGroup(children, share), ...)); children are GroupChild(share, edge=...) or
nested GroupChild(share, children=...); shares are exact), every SHORTEST_PATHS_WCMP demand forwards on the
controller's table toward its destination (Routing domains and the WCMP controller).
ResolvedDemandSet.controller holds the tables (programmed routers and their member weights, and for all_routers
each source's declared volume and weight). On such a set place accepts failure policies, and a dead group's cap is
reported under dead_caps (Failures: patterns, cases and states).
capacity refuses SHORTEST_PATHS_WCMP as its declaration.
MSD¶
maximum_supported_demand(prepared, MSDQuery(resolved_set)) searches for the largest tested feasible alpha (the demand-volume multiplier) of a resolved demand set (Maximum supported demand). alpha_feasible is the largest tested feasible alpha, or None when no probe was feasible; history holds every probe in order; search_status and stop_reason say how and why the search ended. export_probe_rows=True also exports every probe's rows (Python only).
Tunnel counts¶
from ngraph import DemandSet, DemandSpec, TunnelCountQuery, tunnel_counts
te = ngc.FlowPolicyConfig(routing=ngc.CentralizedTE(
source_allocation=ngc.SourceAllocation.EQUAL,
tunnel_count=ngc.TunnelCount(1),
))
tunnels = DemandSet("tunnels", (
DemandSpec(Selector("^A$"), Selector("^C$"), volume=0, policy=te),
))
counted = prepare(Scenario(network, demand_sets={"tunnels": tunnels},
units=UnitsSettings(resolution=1)),
demand_sets=("tunnels",))
counts = tunnel_counts(counted, TunnelCountQuery(
counted.demand_sets["tunnels"], counts=(2, 4), design_demand=4, offer=6,
))
two, four = counts.rows
assert (two.count, two.result.plan.tunnels_per_head, two.result.delivered) == (2, 2, 5)
assert two.lossless_at_design_units == 4
tunnel_counts(prepared, TunnelCountQuery(demands, counts, design_demand, offer, up_to=None, eligibility=None, ...)) evaluates each tunnelled centralized-TE demand once per count (and once up to up_to), each a fresh native evaluation lossy at offer and planned for design_demand, the same two amounts for every demand. Each TunnelCountRow holds the native result (its plan is the tunnel layout), the row's count and up_to, lossless_at_design_units (min(design, the plan's largest lossless offer)) and physical_tunnel_cost (a pool row's tunnel cost without its one cost-1 ingress hop per tunnel). An eligibility (the engine's mask of usable nodes and edges) asks the question on a failed network; Failure cases shows how to build one from a case's pattern. An offer above a TE ingress pool's prepared budget is refused: declare a TunnelCountAnalysis step with that offer and prepare again (Tunnels and TE ingress pools).
Failure cases¶
from ngraph import FailureMode, FailurePolicy, FailureRequest, FailureRule
policy = FailurePolicy(modes=[FailureMode(1, [
FailureRule("link", mode="choice", count=1),
])])
degraded = capacity(prepared, CapacityQuery(
Selector("^A$"), Selector("^C$"),
failures=FailureRequest(policy, iterations=20, seed=42),
workers=2,
))
assert sum(case.case.occurrences for case in degraded.cases) == 20
failure_cases(prepared, request) exposes the same typed cases to custom analyses (Failures: patterns, cases and states; seeds: Seeds). For capacity, placement and sensitivity, workers="auto" resolves to the CPU count; results and errors are the same for any number of workers. MSD runs serially. Explicit worker values must be positive integers.
A FailureCase holds its pattern, draw count and optional trace, without an eligibility mask or a reference to the prepared topology. To build a native mask for another query, use the preparation that produced the case: exclude the case's failed nodes and both directions of its failed links, then intersect with the preparation's disabled elements:
t = prepared.topology
case = degraded.cases[1].case
mask = ngc.Eligibility.intersect(
t.disabled,
t.graph.eligibility(
excluded_nodes=[t.node_id_by_name[n] for n in case.pattern.excluded_nodes],
excluded_edges=[e for link in case.pattern.excluded_links for e in t.edges_by_link[link]],
),
)
Paths and sensitivity¶
paths(prepared, PathQuery(source, target)) returns one row in combine mode and one per selected source-group/destination-group pair in pairwise mode, holding the engine's Path values. SPF uses forward search; selectors supply the endpoints, so SpfOptions.targets must be None. In combine mode SPF keeps the paths of every source-destination node pair whose cost is the smallest of all pairs. costs_only=True returns costs without paths. A row with no route has cost=None; a node paired with itself has a self row of cost 0.
For K shortest paths, pass options=ngc.KspOptions(k, cost_ceiling=...). In combine mode the engine finds up to k paths for each source-destination node pair, and the row keeps the k cheapest of them all; equal costs keep node-pair order (sources, then destinations, by name), then the engine's order within a pair. An AbsoluteCost ceiling is inclusive; a CostStretch takes an exact fraction. An eligibility mask belongs to the prepared graph and has one flag per node and per edge; a residual vector has one integer per edge. The question records SPF's min_residual; KSP records None.
edge_sensitivity(prepared, SensitivityQuery(source, target, config, ...)) reports how much delivery falls without each directed link the engine tests: the engine's baseline result and one signed reduction per tested link (negative when removing the link helps). Python keeps the zero reductions; JSON leaves them out. It takes an uncapped lossless question or a finite lossy offer, and every reduction is measured against the baseline the result reports.
Results and export¶
run(prepare(scenario)) returns RunResult with an ordered tuple of steps, the preparation's units report and producer metadata. Every analysis records its own typed query (CapacityQuery, PlacementQuery, SensitivityQuery, PathQuery, MSDQuery) as query, and a step records it as its question; a built-in domain step records itself. The JSON export writes each question in normalized form (selectors, the effective native model, failures, alpha and its source). A capacity case has states={"converged": ...}; a placement case has one entry per requested failure state (PlacementQuery.states, default ("converged",), "stale" and "repair" on request), and PlacementQuery.interaction selects "ordered" or "simultaneous" forwarding (workflow reference: TrafficMatrixPlacement). Each row holds the engine's result, with its endpoints, its reference request and any requested reports.
write_json(result, file) is the export path. It writes each plain record as its declared constants and fields, in declaration order; ngraph/schemas/results.json is generated from the same declarations. The file's format is in Output Format, and what its numbers mean in Results and their units.
CapacityQuery, PlacementQuery, SensitivityQuery, TunnelCountQuery and MSDQuery take export_details (default False), which requests source rows and adds the detail fields to JSON. The analysis records it as outputs.details. MSD applies it to its best rows and any exported probe rows; its history stays compact.
All five queries accept native outputs. A completed row.result or sensitivity baseline has program=None and by_source=None by default. The native program and by_source flags request those fields; selected details and reports also request their required fields internally. For direct access, pass outputs=ngc.OutputOptions(program=True, by_source=True) and check optional fields before reading them. A zero call that has not prepared a program can still return program=None. The result's plan and by_tunnel keep their model-specific defaults. Placement reads live policies for cost reports and failure replay; capacity tunnel-cost reports request a result program internally. Exported outputs.native echoes the user's seven flags, excluding internal requests.
Custom steps and exporters¶
A custom step implements run(context) -> StepOutput: it receives a StepContext, reads earlier successful steps through context.prior_results, and returns a StepOutput holding its question (a finite JSON mapping of its parameters) and its result. A step that needs a demand set bound, or asks a numerical question, says so with needs(), which returns a StepNeeds record (ngraph.workflow); the default None asks preparation for nothing. A custom result registers exactly one register_result_exporter(Type, encoder); plain JSON domain data is also accepted. Unknown result types refuse. The encoder runs once per requested export. Published results are immutable for later steps: plain JSON results and questions are stored read-only, and NetGraph's own records are immutable. A registered result is published as it is, with any native Core values it holds, so make its type immutable. A custom step type parses its own YAML keys by overriding the yaml_arguments(arguments, name) class method.
Records and editing¶
Domain records are immutable from construction: nodes, links, risk groups, components, conditions, selectors, failure rules, modes and policies, demand declarations and built-in workflow steps. Each constructor checks its fields when the record is built, by one rule per kind of field, the same for Python and YAML: text is a str (a name is nonempty; whitespace is part of it), a flag a bool, an integer an int or an exactly integral float such as 2.0 (stored as 2), and an amount or ratio any finite real number in the field's range (stored as a float); a NumPy scalar of the field's kind counts as that kind, a subclass of str, int or float is stored as its plain value, and booleans are never numbers. Each refusal names the field and the value. What only the whole network can check (names that must exist, sums over links) is checked at preparation. attrs and condition operands must be finite JSON data and are stored read-only; tuples and sets in them are refused, so use lists. Record collections are stored as tuples and frozensets. Edit through the Network methods (add_node, add_link, replace_node, replace_link, the enable/disable and risk-group toggles) or dataclasses.replace: assigning a record field raises dataclasses.FrozenInstanceError, and editing read-only attrs raises TypeError. Network.add_link assigns a link's id once; a link already in a network is refused by another, so add dataclasses.replace(link) there. replace_link keeps a link's endpoints; to move a link, add a new one. A YAML declaration's identity is declared data that dataclasses.replace keeps: DemandSpec.yaml_label, declaration_ordinal and expand_leaf_index, and a step's yaml_label and derived_seed (its seed_source is scenario-derived while its seed equals that derived seed).
Network and ComponentsLibrary keep editable dicts. prepare changes nothing in its input: the returned PreparedScenario holds read-only views of those dicts, each copied on its own under plain string keys, that share the immutable records, so later edits of the caller's collections never reach it, and editing a prepared view raises TypeError. A failed prepare leaves its input as it was.
A custom step's own data is its own: it is neither copied nor frozen, and the step can change it and its later answers. prepare records each step's name and seed, so a later edit of those does not reach the prepared scenario. copy.copy, copy.deepcopy and pickle of a record give an equal immutable value; records that hold native values are shared by copies, and native values cannot be pickled.
Domain tools and logging¶
BuildGraph exports the physical network in NetworkX node-link form. NetworkStats, CostPower and NetworkExplorer describe the network itself: its statistics, and its hardware cost and power. The risk-group methods of Network act on the group's members at the time of each call.
ngraph.hardware exports hardware_inventory, HardwareInventory, HardwareUse, link_active and resolve_link_end. ngraph.workflow.step_name(step) returns a step's effective run name. The public API is the __all__ of ngraph, ngraph.workflow, ngraph.hardware and ngraph.logging.
Importing the package installs a NullHandler. Applications can call ngraph.logging.set_global_log_level(level) or configure their own handler. Import the CLI explicitly when it is needed.