Skip to content

Session 9

Saturating the bottleneck and plotting cwnd

Updated View as Markdown

Raising the UDP rate until it fills the whole bridge starves TCP completely. The plot of congestion window against time, annotated with both UDP rates, is the deliverable.

Objectives

Do not copy. Read for understanding and the viva
  • Complete questions 23 to 24 of the manual: saturating the bottleneck and plotting cwnd
  • Prepare the deliverable before the lab and finish it during the session
  • Be ready to explain every step in the viva

Questions Covered

Do not copy. Read for understanding and the viva
QuestionRequirementStatus
Q23In the last session 8, Increase the UDP rate at 30 second to Rate2 such that it clogs…Complete
Q24Use MatPlotlLib or GNUPlot to visualize cwnd vs time, also mention Rate1 and Rate2Complete

Preparation

Do not copy. Read for understanding and the viva
  • Schedule the rate change with Simulator::Schedule(Seconds(30.0), ...) that sets the OnOff application’s DataRate attribute to Rate2.
  • Plot with matplotlib: time on x, cwnd in bytes on y, vertical lines at 20 s and 30 s labelled Rate1 and Rate2.
  • Explain each phase of the curve in the record: slow start, steady state, halving after 20 s, collapse after 30 s.

Question 23

Problem Statement

Write in lab record

In the last session 8, Increase the UDP rate at 30 second to Rate2 such that it clogs whole of the dumbbell bridge capacity.

Solution

Write in lab record

dumbbell-rate2.cc is the Session 8 script with one scheduled event added and the run extended to 45 s. Bridge 1 Mbit/s, Rate1 500 kbit/s from 20 s, Rate2 1 Mbit/s from 30 s.

Steps

  1. Copy the listing into scratch/dumbbell-rate2.cc and run ./ns3 run scratch/dumbbell-rate2.
  2. The UDP OnOffApplication is installed once with DataRate Rate1. Its DataRate attribute can be changed while it runs; the application reads it every time it schedules the next packet.
  3. ChangeRate(Ptr<Application> app, DataRate rate) calls app->SetAttribute("DataRate", DataRateValue(rate)) and prints the time. It is scheduled with Simulator::Schedule(Seconds(30.0), &ChangeRate, udpSrc.Get(0), DataRate("1Mbps")).
  4. Rate1, Rate2 and both times are command-line options (--rate1, --rate2, --udpStart, --rate2Time), so the same binary can show the examiner other cases.
  5. The cwnd trace is identical to Session 8: connect at 1.001 s to /NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow and write time cwnd to cwnd.txt.

Program

dumbbell-rate2.cccpp
/*
 * dumbbell-rate2.cc  --  MCSL-223 Session 9, questions 23 and 24
 *
 * Purpose of the program:
 *   The Session 8 dumbbell, extended: TCP starts at 1 s, UDP starts at 20 s
 *   at Rate1 (500 kb/s, half the 1 Mbit/s bridge) and at 30 s the UDP rate
 *   is raised to Rate2 (1 Mbit/s, the whole bridge) by a scheduled event
 *   that changes the OnOff application's DataRate attribute (Q23).  The
 *   congestion window of the TCP socket is traced to cwnd.txt and plotted
 *   with plot_cwnd.py, which marks Rate1 and Rate2 (Q24).
 *
 * Topology (Session 2 dumbbell, all links point-to-point):
 *   n0 (TCP src) --10Mbps,1ms--\                     /--10Mbps,1ms-- n4 (TCP sink :8080)
 *                               n2 --1Mbps,10ms-- n3
 *   n1 (UDP src) --10Mbps,1ms--/    (the bridge)    \--10Mbps,1ms-- n5 (UDP sink :9000)
 *
 * Build and run (ns-3.36 or later):
 *   cp dumbbell-rate2.cc scratch/
 *   ./ns3 run scratch/dumbbell-rate2
 *   python3 plot_cwnd.py cwnd.txt cwnd.png
 */

#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("DumbbellRate2");

/*
 * CwndChange: trace sink for the CongestionWindow attribute of the TCP
 * socket.  Writes "time newCwnd" (bytes) on every change.
 */
static void
CwndChange(Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, uint32_t newCwnd)
{
    (void)oldCwnd;
    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << newCwnd << std::endl;
}

/*
 * TraceCwnd: connects CwndChange to the first TCP socket of node 0.
 * Scheduled at 1.001 s, just after BulkSend has created the socket.
 */
static void
TraceCwnd(Ptr<OutputStreamWrapper> stream)
{
    Config::ConnectWithoutContext("/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow",
                                  MakeBoundCallback(&CwndChange, stream));
}

/*
 * ChangeRate: scheduled at 30 s.  Sets the DataRate attribute of the
 * running OnOff application; the next packet interval uses the new rate.
 */
static void
ChangeRate(Ptr<Application> app, DataRate rate)
{
    app->SetAttribute("DataRate", DataRateValue(rate));
    std::cout << Simulator::Now().GetSeconds() << " s: UDP rate changed to " << rate
              << std::endl;
}

/*
 * main: builds the dumbbell, installs the flows, schedules the rate change
 * and the cwnd tracer, and prints the totals at the end.
 */
int
main(int argc, char* argv[])
{
    std::string bridgeRate = "1Mbps";
    std::string rate1 = "500kb/s";  // half of the bridge
    std::string rate2 = "1Mbps";    // whole bridge
    double udpStart = 20.0;
    double rate2Time = 30.0;
    double simTime = 45.0;

    CommandLine cmd(__FILE__);
    cmd.AddValue("bridgeRate", "Data rate of the n2-n3 bridge", bridgeRate);
    cmd.AddValue("rate1", "UDP rate from udpStart (Rate1)", rate1);
    cmd.AddValue("rate2", "UDP rate from rate2Time (Rate2)", rate2);
    cmd.AddValue("udpStart", "Time in seconds at which UDP starts", udpStart);
    cmd.AddValue("rate2Time", "Time in seconds at which UDP switches to Rate2", rate2Time);
    cmd.AddValue("simTime", "Simulation time in seconds", simTime);
    cmd.Parse(argc, argv);

    Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(1000));
    // NewReno halves cwnd on loss, as in the formula sheet (ns-3.35+ defaults to Cubic)
    Config::SetDefault("ns3::TcpL4Protocol::SocketType", TypeIdValue(TcpNewReno::GetTypeId()));

    // ---- nodes: left first so the TCP source is NodeList/0 ---------------
    NodeContainer left;
    left.Create(2);  // n0, n1
    NodeContainer routers;
    routers.Create(2);  // n2, n3
    NodeContainer right;
    right.Create(2);  // n4, n5

    PointToPointHelper access;
    access.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
    access.SetChannelAttribute("Delay", StringValue("1ms"));

    PointToPointHelper bridge;
    bridge.SetDeviceAttribute("DataRate", StringValue(bridgeRate));
    bridge.SetChannelAttribute("Delay", StringValue("10ms"));

    NetDeviceContainer d02 = access.Install(left.Get(0), routers.Get(0));
    NetDeviceContainer d12 = access.Install(left.Get(1), routers.Get(0));
    NetDeviceContainer d23 = bridge.Install(routers.Get(0), routers.Get(1));
    NetDeviceContainer d34 = access.Install(routers.Get(1), right.Get(0));
    NetDeviceContainer d35 = access.Install(routers.Get(1), right.Get(1));

    InternetStackHelper stack;
    stack.Install(left);
    stack.Install(routers);
    stack.Install(right);

    Ipv4AddressHelper address;
    address.SetBase("10.1.1.0", "255.255.255.0");
    address.Assign(d02);
    address.SetBase("10.1.2.0", "255.255.255.0");
    address.Assign(d12);
    address.SetBase("10.1.3.0", "255.255.255.0");
    address.Assign(d23);
    address.SetBase("10.1.4.0", "255.255.255.0");
    Ipv4InterfaceContainer i34 = address.Assign(d34);
    address.SetBase("10.1.5.0", "255.255.255.0");
    Ipv4InterfaceContainer i35 = address.Assign(d35);

    Ipv4GlobalRoutingHelper::PopulateRoutingTables();

    // ---- TCP n0 -> n4 from 1 s ------------------------------------------
    uint16_t tcpPort = 8080;
    PacketSinkHelper tcpSinkHelper("ns3::TcpSocketFactory",
                                   InetSocketAddress(Ipv4Address::GetAny(), tcpPort));
    ApplicationContainer tcpSink = tcpSinkHelper.Install(right.Get(0));
    tcpSink.Start(Seconds(0.0));
    tcpSink.Stop(Seconds(simTime));

    BulkSendHelper bulk("ns3::TcpSocketFactory", InetSocketAddress(i34.GetAddress(1), tcpPort));
    bulk.SetAttribute("MaxBytes", UintegerValue(0));
    ApplicationContainer tcpSrc = bulk.Install(left.Get(0));
    tcpSrc.Start(Seconds(1.0));
    tcpSrc.Stop(Seconds(simTime));

    // ---- UDP n1 -> n5 at Rate1 from 20 s --------------------------------
    uint16_t udpPort = 9000;
    PacketSinkHelper udpSinkHelper("ns3::UdpSocketFactory",
                                   InetSocketAddress(Ipv4Address::GetAny(), udpPort));
    ApplicationContainer udpSink = udpSinkHelper.Install(right.Get(1));
    udpSink.Start(Seconds(0.0));
    udpSink.Stop(Seconds(simTime));

    OnOffHelper onoff("ns3::UdpSocketFactory", InetSocketAddress(i35.GetAddress(1), udpPort));
    onoff.SetConstantRate(DataRate(rate1), 1000);
    ApplicationContainer udpSrc = onoff.Install(left.Get(1));
    udpSrc.Start(Seconds(udpStart));
    udpSrc.Stop(Seconds(simTime));

    // ---- Q23: switch the same OnOff application to Rate2 at 30 s ---------
    Simulator::Schedule(Seconds(rate2Time), &ChangeRate, udpSrc.Get(0), DataRate(rate2));

    // ---- Q24: cwnd trace, connected just after the socket exists ---------
    AsciiTraceHelper ascii;
    Simulator::Schedule(Seconds(1.001), &TraceCwnd, ascii.CreateFileStream("cwnd.txt"));

    Simulator::Stop(Seconds(simTime));
    Simulator::Run();

    // ---- report ---------------------------------------------------------
    Ptr<PacketSink> ts = DynamicCast<PacketSink>(tcpSink.Get(0));
    Ptr<PacketSink> us = DynamicCast<PacketSink>(udpSink.Get(0));
    std::cout << "TCP sink n4: " << ts->GetTotalRx() << " bytes over " << simTime - 1.0
              << " s = " << ts->GetTotalRx() * 8.0 / (simTime - 1.0) / 1000.0 << " kbit/s"
              << std::endl;
    std::cout << "UDP sink n5: " << us->GetTotalRx() << " bytes over " << simTime - udpStart
              << " s = " << us->GetTotalRx() * 8.0 / (simTime - udpStart) / 1000.0 << " kbit/s"
              << std::endl;

    Simulator::Destroy();
    return 0;
}

Configuration

The defaults reproduce the question. These runs are worth keeping in the record because the examiner can ask what happens if Rate2 is not the whole bridge:

CommandRate2 on the wireExpected cwnd after 30 s
./ns3 run scratch/dumbbell-rate21.03 Mbit/s, above the bridgeresets to 1 kB, stays there
./ns3 run "scratch/dumbbell-rate2 --rate2=750kb/s"0.77 Mbit/ssawtooth between about 10 kB and 20 kB, TCP keeps about 230 kbit/s
./ns3 run "scratch/dumbbell-rate2 --rate2=500kb/s"same as Rate1no change at 30 s; the plot shows only the 20 s step
./ns3 run "scratch/dumbbell-rate2 --rate2Time=25 --simTime=40"1.03 Mbit/s from 25 scollapse five seconds earlier; move the second marker in the plot script

Diagram

   n0 (TCP BulkSend from 1 s) --10 Mbps, 1 ms--\                                  /--10 Mbps, 1 ms-- n4 (TCP sink :8080)
                                                n2 ---- 1 Mbps, 10 ms bridge ---- n3
   n1 (UDP OnOff: Rate1 at 20 s,  --10 Mbps, 1 ms--/        DropTail 100 packets      \--10 Mbps, 1 ms-- n5 (UDP sink :9000)
       Rate2 at 30 s)

Output

Expected output (ns-3 is not installed here; the listing was checked by reading against the ns-3.36 helper API):

$ ./ns3 run scratch/dumbbell-rate2
30 s: UDP rate changed to 1000000bps
TCP sink n4: 2880000 bytes over 44 s = 523.636 kbit/s
UDP sink n5: 2455000 bytes over 25 s = 785.6 kbit/s

Expected cwnd.txt around the change:

$ awk '$1>29.5 && $1<34' cwnd.txt
29.6104 44000
29.6112 22000
30.4257 23000
30.9917 1000
31.9917 2000
32.4133 1000
34.4133 1000

Explanation

At Rate2 the UDP source offers 1000-byte datagrams every 8 ms. On the wire each is 1030 bytes, so the offered load is

1030×80.008=1.03 Mbit/s

slightly more than the bridge can carry even with no TCP at all. The queue at n2 is now permanently full of UDP datagrams. A TCP segment is accepted only if it arrives in the short gap after a departure and before the next UDP arrival, so almost every TCP segment is tail-dropped, and even the UDP flow loses about 3 percent (979 of 1000 kbit/s delivered). TCP sees repeated losses without enough duplicate ACKs for fast retransmit, so it falls back to retransmission timeouts: cwnd is reset to one segment, the timeout doubles after each failure, and the flow delivers almost nothing. Between 30 s and 45 s the TCP sink gains only about 30 kB, against 2.27 MB in the first 19 s and 580 kB during the Rate1 phase. The UDP average printed over 25 s mixes the two phases: 10 s at about 495 kbit/s and 15 s at about 979 kbit/s.

Question 24

Problem Statement

Write in lab record

Use MatPlotlLib or GNUPlot to visualize cwnd vs time, also mention Rate1 and Rate2.

Solution

Write in lab record

Steps

  1. Install matplotlib once: pip install matplotlib.
  2. Run python3 plot_cwnd.py cwnd.txt cwnd.png in the ns-3 directory after the simulation. The script reads the two-column trace, draws the window as a step plot and adds two dashed vertical lines at 20 s and 30 s labelled Rate1 and Rate2.
  3. For gnuplot instead: gnuplot plot_cwnd.gp. It uses with steps, two set arrow ... nohead lines at 20 and 30 and set label for the rates; the output file is the same cwnd.png.
  4. Paste cwnd.png in the record with the phase table below written under it.

Program

Lab record: every tab is one file of the answer. Write all of them.

plot_cwnd.pypython
#!/usr/bin/env python3
"""plot_cwnd.py -- MCSL-223 Session 9, Q24.

Reads cwnd.txt ("time cwnd" per line, written by dumbbell-rate2.cc) and
draws the congestion window against time with vertical markers at the two
UDP rate changes.

Usage:  python3 plot_cwnd.py [cwnd.txt] [cwnd.png]
Needs:  matplotlib  (pip install matplotlib)
"""

import sys

import matplotlib

matplotlib.use("Agg")  # write a file, no display needed
import matplotlib.pyplot as plt  # noqa: E402

RATE1_TIME, RATE1 = 20.0, "Rate1 = 500 kbit/s (half the bridge)"
RATE2_TIME, RATE2 = 30.0, "Rate2 = 1 Mbit/s (whole bridge)"


def read_trace(path):
    """Return two lists, time in seconds and cwnd in bytes."""
    times, cwnds = [], []
    with open(path) as f:
        for line in f:
            parts = line.split()
            if len(parts) == 2:
                times.append(float(parts[0]))
                cwnds.append(int(parts[1]))
    return times, cwnds


def main():
    src = sys.argv[1] if len(sys.argv) > 1 else "cwnd.txt"
    out = sys.argv[2] if len(sys.argv) > 2 else "cwnd.png"
    times, cwnds = read_trace(src)
    if not times:
        sys.exit(f"{src}: no samples found")

    top = max(cwnds)
    plt.figure(figsize=(9, 5))
    plt.step(times, cwnds, where="post", lw=1.2, label="cwnd (bytes)")
    plt.axvline(RATE1_TIME, color="tab:orange", ls="--")
    plt.text(RATE1_TIME + 0.3, top * 0.95, RATE1, color="tab:orange")
    plt.axvline(RATE2_TIME, color="tab:red", ls="--")
    plt.text(RATE2_TIME + 0.3, top * 0.85, RATE2, color="tab:red")
    plt.xlabel("Time (s)")
    plt.ylabel("Congestion window (bytes)")
    plt.title("TCP cwnd on the 1 Mbit/s dumbbell bridge under UDP load")
    plt.grid(alpha=0.3)
    plt.legend(loc="upper left")
    plt.tight_layout()
    plt.savefig(out, dpi=120)
    print(f"wrote {out}: {len(times)} samples, max cwnd {top} bytes, last {cwnds[-1]} bytes")


if __name__ == "__main__":
    main()
plot_cwnd.gptext
# plot_cwnd.gp -- MCSL-223 Session 9, Q24 (gnuplot alternative to plot_cwnd.py)
# Run after the simulation:  gnuplot plot_cwnd.gp
# cwnd.txt columns: time (s), congestion window (bytes)

set terminal pngcairo size 900,540
set output "cwnd.png"
set title "TCP cwnd on the 1 Mbit/s dumbbell bridge under UDP load"
set xlabel "Time (s)"
set ylabel "Congestion window (bytes)"
set key left top
set grid
set arrow from 20, graph 0 to 20, graph 1 nohead dt 2 lc rgb "dark-orange"
set label "Rate1 = 500 kbit/s (half the bridge)" at 20.3, graph 0.95 tc rgb "dark-orange"
set arrow from 30, graph 0 to 30, graph 1 nohead dt 2 lc rgb "red"
set label "Rate2 = 1 Mbit/s (whole bridge)" at 30.3, graph 0.85 tc rgb "red"

plot "cwnd.txt" using 1:2 with steps lw 1.5 title "cwnd (bytes)"

Output

The script was run here on a synthetic cwnd.txt with the expected shape (matplotlib 3.x, Python 3.13) to check that it draws and labels correctly:

$ python3 plot_cwnd.py cwnd.txt cwnd.png
wrote cwnd.png: 981 samples, max cwnd 105000 bytes, last 3000 bytes

Expected figure: a step curve of cwnd in bytes against time from 1 s to 45 s, an orange dashed line at 20 s labelled “Rate1 = 500 kbit/s (half the bridge)” and a red dashed line at 30 s labelled “Rate2 = 1 Mbit/s (whole bridge)”.

Explanation

Phase by phase, tie each part of the curve to the formula sheet:

PhaseTimeWhat the curve doesFormula-sheet rule
Slow start1.0 to 1.9 s10 kB doubling each RTT to about 110 kBcwnd doubles every RTT
First lossabout 1.9 sdrop to about 55 kBqueue of 100 packets overflows; cwnd halves
Congestion avoidance2 to 20 sslow straight climb to about 82 kB, no lossplus one segment per RTT; RTT is about 0.5 s because of the queue
Rate120 to 30 shalves at about 20.7 s, then a sawtooth between about 25 kB and 45 kBUDP takes half the bridge and half the queue; TCP loses every few seconds and halves each time
Rate230 to 45 shalves once more, then collapses to 1 kB with rare steps to 2 kBqueue is always full of UDP; every loss is a timeout, cwnd resets to one segment and the timeout doubles

Two points to make in the viva. First, during phases 2 and 3 the TCP throughput is constant even though cwnd changes a lot, because cwnd is above the 3000-byte bandwidth-delay product; the window only decides how deep the queue is. Second, the throughput bound in the formula sheet, cwnd over RTT, explains the collapse: with cwnd at 1000 bytes and an RTT that has grown to the retransmission timeout (a second or more), the bound is under 8 kbit/s, which is what the sink sees.

Why UDP wins: the OnOff source has no feedback loop. TCP measures loss and reduces its window; UDP measures nothing and keeps its rate, so at Rate2 it takes the bridge and TCP gets only the gaps. This is the argument for congestion control, fair queueing at routers, or rate limits on UDP applications.

Formula Sheet

Do not copy. Read for understanding and the viva

Throughput

Bytes received at the sink divided by the time the flow was active, converted to bits per second:

Throughput=8×bytes receivedtend−tstartbit/s

Bandwidth-delay product

The amount of data in flight on a link of capacity B and round-trip time RTT. A TCP window smaller than this cannot fill the link:

BDP=B×RTT

For the Session 10 defaults, B=5Mbit/s and one-way delay 5ms give RTT=10ms and BDP=5×106×0.010=50,000bits≈6.25kB.

Transmission and propagation delay

ttrans=LB,tprop=dv,ttotal=ttrans+tprop+tqueue+tproc

where L is packet size in bits, B link rate, d distance and v signal speed (about 2×108m/s in copper or fibre).

Constant bit rate traffic

An OnOff application with packet size L bytes and rate R bit/s sends one packet every

Δt=8LR seconds

TCP congestion window

Slow start doubles the window every RTT until the threshold; congestion avoidance adds one segment per RTT; a loss halves it (TCP NewReno):

cwndss(n)=2nMSS,cwndca←cwnd+MSS2cwnd,cwndloss←cwnd2

The maximum throughput of one TCP flow is bounded by

Throughputmax⁡=cwndmax⁡RTT

Packet loss

Loss rate=packets sent−packets receivedpackets sent

Viva Questions

Do not copy. Read for understanding and the viva
  • Q: How is the UDP rate changed at 30 s without a second application? A: A scheduled event calls SetAttribute("DataRate", ...) on the running OnOffApplication; it reads the attribute when it schedules each next packet.
  • Q: What are Rate1 and Rate2 and where do they come from? A: Half and all of the bridge capacity: 500 kbit/s and 1 Mbit/s for a 1 Mbit/s bridge.
  • Q: Why does UDP at exactly 1 Mbit/s still overload a 1 Mbit/s link? A: The rate counts payload; UDP, IP and PPP headers add 30 bytes per 1000-byte datagram, so the wire load is 1.03 Mbit/s.
  • Q: Why does cwnd go to 1000 bytes after 30 s instead of halving? A: With nearly every segment dropped there are no duplicate ACKs, so the loss is detected by timeout, which resets cwnd to one segment.
  • Q: What does plt.step show that plt.plot would not? A: cwnd changes in jumps at ACK or loss events; a step plot draws it as it is, a line plot draws false slopes.
  • Q: Why is the TCP throughput flat between 2 s and 20 s while cwnd climbs? A: cwnd is above the bandwidth-delay product, so the bridge is already full; extra window only fills the queue.
  • Q: How would you make TCP survive Rate2? A: A queue discipline that isolates flows (fair queueing, FqCoDel in ns-3) or a smaller shared queue with early drops (RED, CoDel) so UDP cannot monopolise the buffer.
  • Q: Where is the bandwidth-delay product of the bridge? A: 1 Mbit/s times 24 ms, 3000 bytes, three segments of 1000 bytes.

Common Mistakes

Do not copy. Read for understanding and the viva
  • Installing a second OnOff application at 30 s instead of changing the rate of the first; the two sources then overlap and the offered load is Rate1 plus Rate2.
  • Passing the attribute as StringValue("1Mbps") to a function that expects DataRate; both work with SetAttribute, but mixing them in Simulator::Schedule arguments gives a compile error about the callback signature.
  • Running only to 40 s, which leaves too little of the Rate2 phase to show the collapse; use 45 s or more.
  • Drawing the marker lines but not labelling them; the question explicitly asks to mention Rate1 and Rate2 on the plot.
  • Describing the Rate2 phase as “cwnd halves” when it actually resets to one segment on timeout; the record should say which loss detection happened.
  • Plotting cwnd in segments after tracing it in bytes without saying so; the axis label must match the trace.

Session Summary

Write in lab record
  • Source listing of dumbbell-rate2.cc with the header comment and the dumbbell diagram marking Rate1 at 20 s and Rate2 at 30 s
  • Run output with the rate-change line and the TCP and UDP totals
  • cwnd.txt excerpt around 30 s showing the reset to 1000 bytes
  • plot_cwnd.py (or plot_cwnd.gp) and the figure cwnd.png with both vertical markers labelled
  • The five-phase table linking each part of the curve to the formula sheet
  • The wire-load calculation showing why Rate2 exceeds the bridge
Navigation

Type to search…

↑↓ navigate↵ selectEsc close