Skip to main content

Loadable scenarios and the runbook

Three pairs of scenarios and one single, one task each, then the runbook that walks the whole demo in narrative order, then how to put the default branch back. Each task makes its own branch, loads its own scenario input and runs its own check, so nothing here asks you for a branch name. Every number below is produced by the engine and asserted by a unit test.

The demo guide carries the run order and links the other scenario pages.

Two loadable scenarios for the ODU layer​

Both are scenario input under demo/. They share one branch because they touch no common wavelength, and one task owns it:

uv run invoke demo-odu

It makes odu-demo if it is not there, loads both files, provisions the eleven London to Milan circuits the first one asks for, and runs the capacity check. Run it again and it continues onto the same branch.

demo/04_odu_ten_in_one.yml, ten circuits in one wavelength. Provision svc-lon-mil-sdh-01 through -10 and each maps into an ODU2 of 8 slots that grooms into the same ODU4 line container. Ten eights are eighty, so the tenth takes it to 80 of 80 and none of the ten lights a wavelength of its own. svc-lon-mil-sdh-11 is then refused, reason no-slots, naming the container it did not fit with both slot figures and then saying that no wavelength could be lit either. Grooming is tried first and lighting second, so the file has to close both escapes. It spends the last usable block on oms-fra-mil, leaving 532,400 MHz in 29 blocks. The widest of those, 38,000 MHz, is narrower than the 44,400 MHz the narrowest mode in the catalog occupies.

This one ends in an accepted refusal too. The file carries refusal_accepted: true on svc-lon-mil-sdh-11 and on none of the ten that provisioned. provisionable reads the refusal, sees it signed for and passes. The scenario's point is that grooming is tried before lighting and a refusal has to close both escapes, and none of that needs a red pipeline. The flag also survives the generator: a rerun that refuses the service again leaves the signature alone, and only the path where the service actually provisions clears it.

The band split on that branch is odu4 5, full 1, no-odu 15. Only oms-ams-lon turns red, and the two other sections the corridor crosses stay green because they still carry empty ODUC4 wavelengths at 320 free.

demo/05_odu_mixed_fill.yml, every band at once. No service and no generator run. It writes client containers directly, spread so that at least one section lands in each of the five bands. The measured split is odu4 2, odu2 1, odu0 1, full 1, no-odu 16, over 21 sections. Load it, fetch odu-map from any PoP, and the legend can be read against the picture.

Read the map, then check it. Two minutes, and it is the step that says whether the drawing works without its author standing next to it. Give someone the rendered odu-map and nothing else, and ask for two lists: the sections that can still take a 100G circuit, and the sections where nothing fits. Both come off the colours, green for the first and red for the second. Then check the answer against the containers on the branch rather than against the map, which is the last thing demo-odu does.

The check passes, and its summary line counts parents that fit, parents with no known figure and overfilled ones. Those counts come from the same slot table the map coloured itself with. A reader who names a green section the containers say is full has found a real disagreement. There is nowhere for one to hide: the map, the check and the generator all call one implementation of the capacity rule.

Two loadable scenarios for the regenerator​

Both are scenario input under demo/, and they are a before and after. They need two branches, because the point is the comparison: one branch keeps the refusal, and the other is the proposal a reviewer holds against it.

uv run invoke demo-regenerator

One command tells both halves. It makes oeo-refused, loads 06, asks for svc-mad-waw-400g and runs provisionable, which fails. Then it makes oeo-closed, loads 06 and 07, asks again, and runs the same check, which passes.

demo/06_mad_waw_16qam.yml, three regenerators and three refusals. Madrid to Warsaw is 2970 km over four sections, and no single wavelength closes it on any of the ten modes. The best is -0.021 dB at DP-QPSK 128GBd 400G. The file puts an OtnOduSwitch at each of the three sites on that route where one could go, oeo-par-01, oeo-fra-02 and oeo-prg-01, each holding the wavelength pair its split needs. All three splits are refused, reason budget: Paris returns -0.535 and -2.439 dB, Frankfurt -2.755, Prague -4.004. One run reports all three.

The two later splits show one figure each because the generator stops budgeting a chain at its first failing segment. A verdict is a conjunction, so the second half cannot rescue the first and computing it would only produce a number nobody may act on. Both halves of all four attempts are in the link budget table, measured segment by segment.

That refusal is the scenario and not a fault in the file. A route's verdict is a conjunction over its segments, so one half short of OSNR refuses the whole circuit however much margin the other half has.

This scenario ends in a blocked merge, and it is the only one that does. Nothing in demo/06_mad_waw_16qam.yml sets refusal_accepted, so a proposed change from oeo-refused goes red. The check the task runs on that branch says so:

svc-mad-waw-400g cannot be provisioned and was refused for budget: neither a
direct wavelength nor a chain serves this route. Direct:
oms-par-mad|oms-par-fra|oms-prg-fra|oms-prg-waw closes on no mode: best margin
-0.021 dB on DP-QPSK 128GBd 400G. Chain:
oc-ch070-mad-par|oeo-par-01|oc-ch070-par-waw does not close: segment 1 -0.535
dB, segment 2 -2.439 dB. It has not been accepted, so this branch does not
merge. Fix the network or the request, or set refusal_accepted on the service to
keep the refusal on the record

The service, the code and the detail are all in the one message, because a proposed change shows a reader this line and nothing else. The same run adds Judged 1 service(s). 1 refused and unaccepted, 0 refused with no readable code, 0 accepting a refusal that does not exist, 0 refused and signed for, 0 carrying an optical path.

Every other refusal in this guide is signed for. This one is deliberately not, so the gate is seen firing at least once. tests/unit/test_demo_scenarios.py::test_madrid_to_warsaw_signs_for_nothing_so_one_scenario_still_blocks fails if somebody adds the flag here, because a feature every scenario accepts its way past is a feature nobody watching the demo ever sees.

Two other checks are red on this branch for reasons that predate the gate. osnr_margin fails on a Paris to Madrid deficit that is on every branch including the default one. channel_collision fails because the three regenerator wavelength pairs this file loads overlap in spectrum on purpose. Neither is provisionable, and neither is this feature's doing.

demo/07_mad_waw_qpsk.yml, the mode that finishes the job. One more wavelength pair on the same Frankfurt split, at DP-QPSK 128GBd 400G, on oeo-fra-03. No new site, no new section, no edited margin. The service then provisions as two segments:

no direct wavelength serves
oms-par-mad|oms-par-fra|oms-prg-fra|oms-prg-waw, so it takes the chain
oc-ch073-mad-fra|oeo-fra-03|oc-ch073-fra-waw in 2 segments regenerated at
oeo-fra-03 at Frankfurt, 2970 km, segment 1 +2.745 dB, segment 2
+5.740 dB, 14558.963 us including the framing delay

Because 06 is loaded underneath, the same run still refuses all three 16QAM splits and says so. So one provisioning run tells the whole story: the fix for this route is a regenerator and a mode change, and three attempts at the regenerator alone were not enough. The link budget page has the table and what a chain costs.

This scenario refuses nothing, and it is the trap in the pair. The run logs three discarded 16QAM splits. A gate reading the log would block a branch on which the service ended up fine. It reads the node instead: svc-mad-waw-400g on oeo-closed is active, carries two path segments, and holds no reason code, no detail and refusal_accepted false. provisionable passes, and it is measured rather than argued. tests/unit/test_demo_scenarios.py::test_the_qpsk_fix_accepts_nothing_because_it_refuses_nothing holds the file to it, because a signature here would be one on a refusal that does not exist, which is its own error.

channel_collision is red on oeo-closed for the same reason it is red on oeo-refused: 06 is loaded underneath and its wavelength pairs overlap. That is the branch below, not the fix on top of it.

Why oeo-fra-03 and not oeo-fra-02. One device terminating all four Frankfurt wavelengths is a valid plant and it changes the answer, which was measured. With the 16QAM pair and the QPSK pair on one device, the generator takes the mixed cover and closes at +2.745 and +0.240 dB. Both halves close, so it is a real circuit, and it is a poorer demonstration. The second half survives on a quarter of a decibel, and a reader is left thinking the modulation mattered on one side only. A junction needs one device terminating both of its wavelengths, so keeping the QPSK pair on its own device makes that cover unavailable.

Two loadable scenarios for declared diversity​

These two share one branch, because the point is one run reporting a satisfied promise and a broken one side by side.

uv run invoke demo-diversity

It makes diversity-demo, loads both files, provisions all four members and then runs the check.

demo/08_diversity_mil_feeds.yml, a promise that holds. Milan's two northern feeds arrive through different trenches. Vienna to Milan takes oms-vie-mil, whose ducted span sits in cd-mil-northeast, and Geneva to Milan takes oms-gva-mil, in cd-mil-northwest. No conduit is on both routes, so the check passes. It passes because the routes are disjoint and not because nobody asked, which is the distinction the file exists to draw. A satisfied requirement and an absent requirement both look like a green check, and only one of them is worth anything.

demo/09_diversity_fra_feeds.yml, a promise that does not. Amsterdam to Frankfurt and Paris to Frankfurt are different cities on different sections, and both enter Frankfurt through cd-fra-north. The check fails, which is the expected result:

svc-fra-feed-ams-100g and svc-fra-feed-par-100g are both in diversity
group dg-frankfurt-feeds and their routes share cd-fra-north. One cut in
that duct takes both, so the diversity this group declares does not hold

With both files loaded the same run adds Checked 2 diversity group(s) over 4 routed service(s). 1 pair(s) share a conduit against a declaration and 0 member(s) have no route yet. Services declaring no group are not judged here. The Milan pair is judged and passes in the same breath as the Frankfurt pair is judged and fails. The last sentence is the check saying out loud what it did not look at.

Run the same check against the demo branch and it reports PASSED in silence. Those five services share four ducts between them, which the exposure report lists in scenario seven. None of them declared a group, so this check has nothing to say about any of it.

Provision before checking. A member with no route yet is reported as undetermined, and undetermined is not a pass. Running the check against one provisioned circuit proves nothing about the pair.

One loadable scenario for the missing monitor​

A single rather than a pair, because there is no second file worth reading beside it. The other scenarios on this page load something the network then refuses. This one loads a record an operator would sign off on, and the fault is that something else was never written. Deleting the file is the whole of the fix, so there is no "after" half to compare against.

uv run invoke demo-monitor-gap

demo/10_amplifier_without_monitor.yml, a stage nobody can measure. One OtnAmplifier, amp-ham-ber-11, written in the same fields as the 306 the dataset ships: every mandatory attribute set, every bound respected, no site because an amplifier hut is not a PoP. The load succeeds, and the success is the scenario. What is missing is an OtnAmplifierMonitor among its ports, and no constraint in schemas/ can refuse that. A schema constrains what is written, so it cannot notice a gap. ports peers the generic OtnGenericPort as well, and Infrahub cannot filter a relationship to a generic by the kind of its peers, so the schema has no way to say "at least one of these ports is a monitor" even in principle.

The check fails, with exactly one finding:

amp-ham-ber-11 is an OtnAmplifier and carries no OtnAmplifierMonitor, so nothing
can compare its configured gain against what it is delivering. The drift report
skips a stage with no monitor rather than reporting it

The message names what stops working and not only what is absent. A reader who meets "no monitor" has to work out whether that matters; a reader who meets this one already knows that the gain drift report is now quietly one stage short.

The drift report is a task of its own, and this branch is the interesting place to run it:

uv run invoke demo-drift --branch monitor-gap

It compares the gain each amplifier and Raman pump is configured for against the gain its monitor last reported, and lists the stages outside tolerance. The dataset seeds a droop, so the report always has something to say. What it cannot say anything about is amp-ham-ber-11: a stage with no monitor is a row the report never prints, which is why the check above has to be the thing that catches it.

The same run says what it counted:

Monitor coverage: 306/307 amplifiers, 9/9 Raman pumps, 59/59 transponders,
16/16 multiplexers, 42/42 ROADM degrees. Routers, patch panels and ODU switches
carry no monitor and are not judged here

Per kind rather than as one total, because the totals differ by two orders of magnitude and 306 covered amplifiers would hide nine uncovered Raman pumps inside a single percentage. The last sentence is the check naming the boundary of what it judged, so a reader is not left inferring it from silence. Run the same check against the default branch and the first figure reads 306/306 and the opening word changes to Monitor coverage complete.

59/59 transponders counts monitors, not light. Sixteen of those fifty-nine monitors sit on a transponder with no wavelength on either line port, and each reports loss of signal: received power at the floor of its range, and no OSNR, Q factor, dispersion or differential group delay. Before this dataset moved, every receiver reported the same healthy 25.1 dB whether or not anything was lit. This check has nothing to say about that either way. It asks whether a device carries a monitor, and a monitor honestly reporting darkness is covered. Concepts says why eight PoPs terminate nothing.

The new amplifier is in no section chain, and that is deliberate. oms-ham-ber has four spans and five amplifiers in each direction, because the budget engine pairs amplifier k with span k and puts a pre-amplifier after the last span. A sixth amplifier on a four-span section raises before a decibel is computed, so osnr_margin would turn red on a section this scenario has nothing to say about, and the branch would carry two failures with only one of them the point. The check does not need the relationship: it asks whether a device carries a monitor, and that question never goes through a section.

Negative result: no other check moves on this branch. channel_count_consistency still passes, because an amplifier lights no wavelength and no channel count changes. container_capacity, diversity and provisionable see no service and no container. A check that fired on an unrelated change would be a check nobody reads twice.

The runbook​

About thirty minutes, one branch until step 12, in narrative order.

Setup, once. The object load takes a few minutes; start it before the audience arrives.

uv run invoke start
uv run invoke demo-setup

Every step below runs on the demo branch and none of them needs you to say so. Each one ends by naming the step that follows, and a step run out of order says which task to run first instead of failing inside the client. To watch the whole thing without typing between steps:

uv run invoke demo

1. The network, on one page. Two minutes, in the browser, before any command. Open OTN Sites, pick Frankfurt, open its Artifacts tab, and open network-map.

Fourteen PoPs in their real positions, 21 routes coloured by how much OSNR margin a 400G DP-16QAM carrier has on each. Point at the red route into Madrid, then at the chip on Frankfurt to Milan. Several of the findings the next twelve steps compute are already on this drawing, which is what makes it worth two minutes. The network map page has the rest of the legend.

odu-map is on the same Artifacts tab and is mostly grey here. Say what it is, leave it closed, and come back to it at step 12.

2. What is in the network, and what is left. Two minutes.

uv run invoke demo-capacity

Cover the 4,134,400 of 4,800,000 MHz first, then the eight-against-one gap between free spectrum and usable anchors. Then the sixteen empty sections, and the difference between a section's free spectrum and a route's.

3. Where the cheap parts reach. Two minutes. Run it before anybody asks.

uv run invoke demo-reach

Zero of twenty-one. Pause here: this is the step where the model returns an answer nobody wanted, computed from data rather than assumed.

4. Ask for a service and watch it choose. Four minutes.

uv run invoke demo-provision

Six candidate routes, Hamburg wins on fewest sections then margin. Read the discarded ones out loud: each one names the route, the mode, the margin and the reason it lost.

5. Provision the rest. One minute, and it is setup for what follows.

uv run invoke demo-provision-all

Two groom and two light, in that order, every time. svc-fra-mil-ai-400g and svc-ams-mil-ai-400g take tributary slots in wavelengths that are already up, oc-ch047-fra-mil and oc-ch002-ams-mil. Then svc-fra-gva-hpc-400g and svc-vie-mil-hpc-400g find no line container with room and each light channel 2, on oms-fra-gva and oms-vie-mil. Channel 2 and not channel 1, because a 79,600 MHz carrier centred on channel 1 falls off the bottom of the band. Say the grooming out loud: it is the ODU layer choosing not to buy spectrum, and it is why the cut in step 7 drops seven wavelengths and not eight.

6. Trace one of them. Two minutes.

uv run invoke demo-trace

Thirty-five hops, router to router. Point at the conduit column; it sets up step 8.

7. Cut the fiber. Three minutes. Leave room for questions on this one.

uv run invoke demo-impact

Seven wavelengths, 2.8 Tbps, one AI service, one customer, and six wavelengths the model holds with no customer behind them. Then the duct breakdown, and the fact that cd-fra-north also carries Paris to Frankfurt.

Then mistype it, deliberately:

uv run invoke demo-impact --section oms-ams-frs

It refuses. An impact tool that answers "no impact" to a typo is worse than one that answers nothing.

8. Who is not diverse. Two minutes.

uv run invoke demo-srlg

Frankfurt to Geneva and Amsterdam to Milan, no shared section, one shared duct.

9. The latency budgets. Two minutes.

uv run invoke demo-latency

Then the electronics column, and the sentence about there being no trade-off.

10. The one service that states its handover. Two minutes.

uv run invoke demo-infiniband

Frankfurt to Prague at 212 Gbps with client_signal: IB-HDR-4X, and the container comes back ODUflex. Drop that one line from the file and the same request provisions 400GBASE-FR4, with no error. The rate rule picks only from catalog rows marked auto_selectable, which the two InfiniBand rows are not.

212 Gbps and not 200: HDR signals at 212.5, which is 170 tributary slots, and a 200G wavelength offers 160. The request lands on DP-16QAM 64GBd 400G, whose ODUC4 offers 320, so the ODUflex takes 170 and leaves 150 free.

11. Congestion in two layers. Three minutes. Fill both layers of the Frankfurt to Milan corridor, then ask twice.

uv run invoke demo-refusal

The 400G is refused on slots: it needs a whole ODUC4 and the roomiest wavelength there has 240 of 320 free. The 100G then provisions on the same corridor without lighting anything. 532,400 MHz free in 29 blocks none of which reaches 44,400 says no new wavelength can be lit; it says nothing about room inside the ones already lit.

12. The other map, and one question off it. Three minutes, in the browser. This one wants its own branch, which the task makes:

uv run invoke demo-odu

Open odu-map on any PoP on odu-demo. Same coastline, same fourteen discs, different question. Ask the room for two lists off the colours alone: which sections can still take a 100G circuit, and which can take nothing. Green is the first, red is the second, and the panel's FITS column spells both out. The task finishes by checking the answer against the containers instead of against the map.

The two cannot disagree. The map, the check and the provisioning generator all call one implementation of the capacity rule. Finish on the grey: the sections carrying no wavelength at all, which the map says "not known" about rather than "available".

13. The branch. One minute. Open the proposed change and show the diff and the checks. Everything in the last thirty minutes happened on a branch, the default branch is untouched, and the checks decide whether any of it merges.

Clean up​

uv run invoke demo-clean

With no argument it deletes every branch a scenario task creates, naming each one as it goes and reporting how many of them it found. Nothing is removed without saying so, and --branch still takes a single name.

The default branch is back to the state it shipped in: the fourteen PoPs and their plant, the customer campus on the end of its coarse tail, no services, and the OSNR check still failing on Paris to Madrid. Every service in this guide was provisioned on a branch, which is the point.