import org.arl.fjage.*
println '''
2-node network
--------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/
'''
platform = RealTimePlatform
// run the simulation forever
simulate {
node 'A', location: [ 0.km, 0.km, -15.m], web: 8081, api: 1101, stack: "$home/etc/setup"
node 'B', location: [ 1.km, 0.km, -15.m], web: 8082, api: 1102, stack: "$home/etc/setup"
}34 Simulating networks
We have used simulations throughout this handbook to demonstrate and test commands, scripts and agents without setting up a real Unet. In this chapter we look at how to write our own simulations — describing the network, running it in realtime or in fast-forwarded discrete-event mode, collecting statistics, and modelling modems and channels.
34.1 Running a simulation
We have run the 2-node network simulation many times, but how did the simulator know where the nodes were and what agents to run on each? That information lives in the simulation script. Before we study one, let’s recall how a simulation is run.
A simulation is started from the command line by passing its script to bin/unet:
$ bin/unet samples/2-node-network.groovy
2-node network
--------------
Node A: tcp://localhost:1101, http://localhost:8081/
Node B: tcp://localhost:1102, http://localhost:8082/The script prints the addresses at which each simulated node can be reached. Open a node’s web interface at its http:// URL to access its shell, or connect a command-line shell to its tcp:// endpoint. This is a realtime simulation, so it keeps running until you stop it with Ctrl-C.
Now let’s study 2-node-network.groovy to see how it works.
34.2 Anatomy of a simulation script
Let’s study 2-node-network.groovy in detail:
The script is simple: it prints some documentation for the user, selects the realtime platform, and defines two nodes. Node attributes — name, location, ports and the stack to load — are given when describing each node.
The 0.km and -15.m in the node locations above are Groovy syntactic sugar: writing a value with a unit converts it to UnetStack’s preferred unit for that quantity (meters for distance, so 1.km → 1000). The same sugar works for time — 10.s → 10.0, 10.ms → 0.01, 1.minute → 60 — because wherever a time or duration can be a float, UnetStack uses seconds as its unit of time.
Not everywhere, though. Many Java and fjåge APIs (e.g. currentTimeMillis(), WakerBehavior, TickerBehavior) use the long data type for time in milliseconds, and where UnetStack inherits such an API, it has no choice but to stick with milliseconds. Be careful not to write values such as 10.ms there — those are really values in seconds.
The only time values in microseconds are the PHYSICAL service’s time parameter and the txTime/rxTime timestamps it drives (Chapter 17), a convention inherited by the synchronization offset between node clocks in the RANGING service (Chapter 25).
When defining a node, you can set many properties:
address— node address.web— TCP/IP port for the web interface (unique per node). By default the web interface is only reachable from the local machine; to expose it, useweb: ['0.0.0.0', port].shell—truefor a console shell (at most one node), or a TCP/IP port number to make the shell reachable overnc/telnet.api— TCP/IP port for the API (used by the gateway API or fjåge slave containers), unique per node.location— node location as a 3-tuple (see Chapter 14).mobility—truefor a mobile node,false(default) for a static one.yaw— initial yaw in degrees, anti-clockwise from east.stack— filename of a script, or a closure, that loads the agents in the network stack.model— class providing theNODE_INFOservice (defaultorg.arl.unet.nodeinfo.NodeInfo), loaded before the stack is initialized.
The stack property points at a script that loads the agents. The default etc/setup.groovy loads the standard stack; a minimal equivalent looks like:
container.add 'arp', new org.arl.unet.addr.AddressResolution()
container.add 'ranging', new org.arl.unet.localization.Ranging()
container.add 'mac', new org.arl.unet.mac.CSMA()
container.add 'uwlink', new org.arl.unet.link.ECLink()
container.add 'router', new org.arl.unet.net.Router()
container.add 'rdp', new org.arl.unet.net.RouteDiscoveryProtocol()
container.add 'caddy', new org.arl.unet.transport.CaddyLite()
container.add 'scheduler', new org.arl.unet.scheduler.Scheduler()
container.add 'bbmon', new org.arl.unet.bb.BasebandSignalMonitor(new File(home, 'logs/signals-0.txt').path)To customize the stack per node, point stack at a different script, or provide a closure inline:
simulate {
node 'A', location: [ 0.km, 0.km, -15.m], web: 8081, api: 1101, stack: "$home/scripts/custom.groovy"
node 'B', location: [ 1.km, 0.km, -15.m], web: 8082, api: 1102, stack: {
// only load 3 agents on node B
container.add 'arp', new org.arl.unet.addr.AddressResolution()
container.add 'mac', new org.arl.unet.mac.CSMA()
container.add 'uwlink', new org.arl.unet.link.ECLink()
}
}To preload the EchoDaemon we wrote in Chapter 33, just add container.add 'echo', new EchoDaemon() to a custom stack script or the closure above.
34.3 Geolocating nodes
The 2-node network was not geolocated. Specifying an origin (latitude, longitude) geolocates the network, giving every node a real-world latitude and longitude derived from its local coordinates. The netq-network.groovy script does this:
import org.arl.fjage.RealTimePlatform
import org.arl.unet.sim.channels.ProtocolChannelModel
platform = RealTimePlatform // use real-time mode
origin = [1.216, 103.851] // applies to all nodes
channel.model = ProtocolChannelModel // deterministic delivery (see channel models)
simulate {
node 'A', location: [121.m, 137.m, -10.m], web: 8081, api: 1101, stack: "$home/etc/setup"
node 'B', location: [160.m, -232.m, -15.m], web: 8082, api: 1102, stack: "$home/etc/setup"
node 'C', location: [651.m, 140.m, -5.m], web: 8083, api: 1103, stack: "$home/etc/setup"
}The three nodes are now geolocated in southern Singapore waters, relative to the specified origin.
Because the script is just Groovy, you can generate nodes programmatically. The mission2013-network.groovy script iterates over a list of nodes defined in a channel model class:
import org.arl.fjage.RealTimePlatform
import org.arl.unet.sim.channels.Mission2013a
platform = RealTimePlatform
channel = [ model: Mission2013a ]
origin = [1.217, 103.743]
simulate {
Mission2013a.nodes.each { addr ->
node "$addr", location: Mission2013a.nodeLocation[addr], web: 8000+addr, api: 1100+addr, stack: "$home/etc/setup"
}
}The channel property configures the simulated physical channel (see Channel models); here it selects Mission2013a, which also carries the channel measurements from that experiment and defines the node list the script iterates over.
34.4 Node mobility
Nodes can be mobile (e.g. AUVs). A mobile node is given a motion model:
// AUV-1 moving in a straight line at constant speed
def n1 = node 'AUV-1', location: [0, 0, 0], mobility: true
n1.motionModel = [speed: 1.mps, yaw: 30.deg]
// AUV-2 moving in a circle (constant speed, constant turn rate)
def n2 = node 'AUV-2', location: [0, 0, 0], mobility: true
n2.motionModel = [speed: 1.mps, yawRate: 1.dps]More complex motions can be described as a sequence of segments, keyed by absolute time or relative duration:
def n4 = node 'AUV-4', location: [-50.m, -50.m, 0], mobility: true
n4.motionModel = [
[time: 0.minutes, yaw: 60.deg, speed: 1.mps],
[time: 3.minutes, yawRate: 2.dps, diveRate: 0.1.mps],
[time: 4.minutes, yawRate: 0.dps, diveRate: 0.mps],
[time: 7.minutes, yawRate: 2.dps ],
[time: 8.minutes, yawRate: 0.dps ],
[duration: 3.minutes, yawRate: 2.dps ],
[duration: 1.minute, yawRate: 0.dps, diveRate: -0.1.mps],
[diveRate: 0.mps]
]The AUV dives to 6 m depth while turning, runs two more turns, then climbs back to the surface. The last segment has no time or duration key — it marks the end of the motion plan, and the node continues indefinitely with those final settings. There are also helpers for common patterns, and motion models can be concatenated with +=:
def n5 = node 'AUV-5', location: [-20.m, -150.m, 0], yaw: 0.deg, mobility: true
// dive to depth before starting the survey
n5.motionModel = [
[duration: 5.minutes, speed: 1.mps, diveRate: 0.1.mps],
[diveRate: 0.mps]
]
// then run a lawnmower survey
n5.motionModel += MotionModel.lawnmower(speed: 1.mps, leg: 200.m, spacing: 20.m, legs: 10)
// finally surface and stop
n5.motionModel += [
[duration: 5.minutes, speed: 1.mps, diveRate: -0.1.mps],
[diveRate: 0.mps, speed: 0.mps]
]Save such a script and run it with bin/unet; the AUV nodes will move through the network as the simulation progresses.
34.5 Discrete event simulation
Running in realtime feels just like a real Unet, which is great for interacting through a shell. But as a protocol developer you may need to simulate days or months of operation, perhaps many times over with different settings — impractical in realtime. The simulator can instead run in discrete-event mode, fast-forwarding the waiting time between events to produce hours of simulated time in seconds.
To illustrate, let’s simulate the classic ALOHA MAC protocol: transmit a frame as soon as data arrives, regardless of whether anyone else is transmitting. We want to measure throughput (successfully delivered chunks per unit time) as a function of offered load (total chunks generated per unit time). To compare against ALOHA’s well-known theory, the simulation must match its assumptions:
- Arrivals follow a Poisson process.
- Any time overlap between two frames at a receiver causes a collision; both are lost.
- Each node is half-duplex (cannot receive while transmitting).
- No frames are lost to noise or channel effects.
- There is no propagation delay between nodes.
Assumptions 2 and 4 together are the protocol channel model, selected with channel.model = ProtocolChannelModel. Assumption 3 is the default HalfDuplexModem, but that modem normally defers a transmission to avoid clobbering an ongoing one — which violates the assumption — so we abort any ongoing activity with a ClearReq before transmitting. Assumption 1 is met with a PoissonBehavior, and assumption 5 by placing all nodes at the same location. We normalize load and throughput by making each frame exactly one second long with no overheads:
import org.arl.fjage.*
import org.arl.unet.*
import org.arl.unet.phy.*
import org.arl.unet.sim.*
import org.arl.unet.sim.channels.*
println '''
Pure ALOHA simulation
=====================
TX Count\tRX Count\tOffered Load\tThroughput
--------\t--------\t------------\t----------'''
channel.model = ProtocolChannelModel // use the protocol channel model
modem.dataRate = [2400, 2400].bps // arbitrary data rate
modem.frameLength = [2400/8, 2400/8].bytes // 1 second worth of data per frame
modem.headerLength = 0 // no header overhead
modem.preambleDuration = 0 // no preamble overhead
modem.txDelay = 0 // no hardware delays
def nodes = 1..4 // 4 nodes
trace.warmup = 15.minutes // collect statistics after steady state
for (def load = 0.1; load <= 1.5; load += 0.1) {
simulate 2.hours, { // simulate 2 hours of elapsed time
nodes.each { myAddr ->
def myNode = node "${myAddr}", address: myAddr, location: [0, 0, 0]
myNode.startup = { // startup script run on each node
def phy = agentForService(Services.PHYSICAL)
def arrivalRate = load/nodes.size() // arrival rate per node
add new PoissonBehavior((long)(1000/arrivalRate), { // avg inter-arrival time (ms)
def dst = rnditem(nodes-myAddr) // random destination (excluding self)
phy << new ClearReq()
phy << new TxFrameReq(to: dst, type: Physical.DATA)
})
}
}
}
println sprintf('%6d\t\t%6d\t\t%7.3f\t\t%7.3f',
[trace.txCount, trace.rxCount, trace.offeredLoad, trace.throughput])
}The trace object is defined automatically by the simulator and collects common statistics for you; trace.warmup discards an initial transient so we measure steady-state behavior. A copy of this script is available as samples/aloha.groovy. Running it produces a table like:
TX Count RX Count Offered Load Throughput
-------- -------- ------------ ----------
674 522 0.109 0.083
1202 863 0.201 0.137
1802 1066 0.307 0.169
2311 1143 0.406 0.181
2847 1187 0.511 0.188
...
6949 470 1.493 0.075Fifteen two-hour simulations finish in a couple of minutes, because discrete-event mode is the default when platform is not set to RealTimePlatform (you can set it explicitly with platform = DiscreteEventSimulator). As expected, the maximum throughput of about 0.18 occurs near an offered load of 0.5, matching the theoretical ALOHA curve y = x·exp(−2x). In fact, this handbook runs the sweep every time it is built — Figure 34.1 plots the simulated points from the table against the theoretical curve:
Random variates use rnditem(list) to pick a random item, rnd(min, max) for a uniform real, and rndint(n) for a uniform integer in 0..n-1. These draw from the simulator’s repeatable random source.
34.6 Logs, traces and statistics
A simulation typically produces two outputs: a log file and a trace file.
The log file (logs/log-0.txt) holds detailed text logs from the Java logging framework; your agents and scripts can add to it with log.info() and log.fine(). The timestamp in the first column switches from clock time to discrete-event time when the simulation starts, and back when it ends.
34.6.1 Event tracing
The trace file records every event in the network stack. Since UnetStack v3.3, the default is a rich JSON trace (logs/trace.json) capturing, for each event, the time, component (agent and node), a thread ID, the stimulus message and the response message. The thread ID ties together events with a common root cause, even across agents and nodes. Tracing can be enabled on real nodes too (with EventTracer.enable(filename)), and traces from multiple nodes can be combined for analysis. Integrating tracing into your own agents is as simple as wrapping the messages you generate in response to a stimulus:
send trace(stimulus, new DatagramDeliveryNtf(stimulus))
request trace(stimulus, req), timeoutIf you plan to parse trace.json, its shape is straightforward: the file is one JSON object — a root group named EventTrace with a format version — whose events array contains one nested group per simulation run (or per trace session on a real node), each in turn holding the individual events. An extract:
{"version": "1.0","group":"EventTrace","events":[
{"group":"SIMULATION 1","events":[
{
"time": 1617877446718,
"component": "phy::org.arl.unet.sim.HalfDuplexModem/A",
"threadID": "5cf4e19c-d0d9-4bde-a390-d92bf00e29e3",
"stimulus": {
"clazz": "org.arl.unet.phy.TxFrameReq",
"messageID": "5cf4e19c-d0d9-4bde-a390-d92bf00e29e3",
"performative": "REQUEST",
"sender": "websh",
"recipient": "phy"
},
"response": {
"clazz": "org.arl.unet.phy.TxFrameNtf",
"messageID": "e9d0f4ff-1c1d-4a26-be34-6b98e51e3462",
"performative": "INFORM",
"recipient": "#phy__ntf"
}
}
]}
]}The component field is of the form agent::class/node, and the stimulus/response records identify each message by class, ID, performative and the sending/receiving agents.
You rarely need to parse the trace yourself, though. The viztrace tool turns a trace into sequence diagrams. Run it on a trace file to list the traces it contains — one per root stimulus, with the thread ID doing the work of grouping the resulting events:
$ julia --project viztrace.jl logs/trace.json
Specify a trace:
1: 1783326360842 [21] EditRouteReq ⟦ shell → router ⟧ (2 events)
2: 1783326361446 [21] EditRouteReq ⟦ shell → router ⟧ (2 events)
3: 1783326362035 [21] GetRouteReq ⟦ shell → router ⟧ (3 events)
4: 1783326362645 [21] DatagramReq ⟦ shell → router ⟧ (5 events)
5: 1783326362647 [28] DatagramNtf ⟦ → #org.arl.unet.Topics.DATAGRAM ⟧ (1 events)
6: 1783326363835 [21] DatagramReq ⟦ shell → router ⟧ (52 events)
⋮then pick a trace to render it as a Mermaid sequence diagram:
$ julia --project viztrace.jl -t 6 logs/trace.json > trace6.mmdThe diagram below is the (abridged) result for trace 6 above — a datagram from the route-failover example of Section 7.4, caught in the act of failing over. Reading down the lanes, you can watch the router try the UDP link, receive the failure notification, and quietly retry over the acoustic link, with every agent on both nodes accounted for:
sequenceDiagram participant shell_21 as shell/21 participant router_21 as router/21 participant udplink_21 as udplink/21 participant uwlink_21 as uwlink/21 participant phy_21 as phy/21 participant phy_28 as phy/28 participant uwlink_28 as uwlink/28 shell_21->>router_21: DatagramReq router_21->>udplink_21: DatagramReq udplink_21-->>router_21: AGREE router_21-->>shell_21: AGREE udplink_21->>router_21: DatagramFailureNtf router_21->>uwlink_21: DatagramReq uwlink_21-->>router_21: AGREE uwlink_21->>phy_21: TxFrameReq phy_21-->>uwlink_21: AGREE phy_21->>phy_28: HalfDuplexModem$TX phy_28->>uwlink_28: RxFrameNtf uwlink_28->>phy_28: TxFrameReq phy_28->>phy_21: HalfDuplexModem$TX phy_21->>uwlink_21: RxFrameNtf uwlink_21->>router_21: DatagramDeliveryNtf router_21->>shell_21: DatagramDeliveryNtf
Diagrams like this are invaluable when debugging a protocol: they answer “who sent what to whom, and why” at a glance, across agents and across nodes. (Do not confuse event tracing with the trace shell command, which traces the route a datagram takes through the network, Section 22.6.)
34.6.2 Legacy NAM trace
A legacy NS2-NAM-style trace can be enabled if you need it:
trace = new NamTracer()
trace.open(new File(home, 'logs/trace.nam'))This format logs packet creation (+), transmission (-), reception (r), drops with a reason (d ... -y COLLISION/BAD_FRAME/CLEAR) and node motion (n), and appends a statistics summary per simulation:
# STATS: q=621, t=621, r=506, d=115, O=0.099, L=0.099, D=0.000, T=0.080where q, t, r and d count the datagrams queued, transmitted, received and dropped, O and L are the offered load and the actual channel load, D is the average packet delay (s), and T the normalized network throughput. Note that the legacy tracer only monitors PHYSICAL service events; for events from other agents, use the JSON trace. You can also write a custom tracer by extending the Tracer class.
34.7 Modem models
In a simulated Unet, most agents are identical to those on a real Unet. The exception is the modem: there is no modem in water, so we need an agent that models a real modem’s behavior. The simulator ships with the HalfDuplexModem model, which is the default. You can configure it in the simulation script, either as a map or by assigning properties:
modem.dataRate = [800.bps, 2400.bps]
modem.frameLength = [16.bytes, 64.bytes]
modem.powerLevel = [0.dB, -10.dB]
modem.preambleDuration = 5.msA different modem model class can be selected with the model property (modem = [ model: MyModemModel, ... ]) — modem vendors may supply models that reproduce their hardware’s behavior more faithfully than the generic HalfDuplexModem. The indexed properties dataRate, frameLength, maxFrameLength, janus and powerLevel are 4-tuples — CONTROL, DATA, AUX and CUSTOM channels respectively (a shorter tuple suffices if the later channels aren’t needed). Key properties and their defaults:
dataRate = [256, 1024, 80, 1024]— link data rate (bps).frameLength = [18, 128, 8, 0]— default frame length (bytes).maxFrameLength = [128, 4096, 128, 4096]— maximum frame length (bytes).janus = [false, false, true, false]— JANUS frame support.powerLevel = [-10, -10, -10, -10]— transmit power (dB rerefPowerLevel).preambleDuration = 0.02— detection preamble duration (s).headerLength = 5— frame header length (bytes).txDelay = 0.05— receive-to-transmit switching delay (s).timestampLength = 6,timestampedTxDelay = 1.0— bytes consumed by an embedded timestamp, and the lead time (s) needed before a timestamped transmission.carrierFrequency = 24000,basebandRate = 24000— carrier frequency and baseband sampling rate (Hz).maxPowerLevel = 0,minPowerLevel = -96— allowed range forpowerLevel(dB rerefPowerLevel).refPowerLevel = 185— reference source level (dB re µPa @ 1 m) that all power levels are relative to.rxSensitivity = -200— receive sensitivity (dB re µPa).signalPowerLevel = -10— transmit power for baseband signal transmissions (dB rerefPowerLevel).basebandRxDuration = 1.0,maxSignalLength = 65536— defaultbbrecrecording duration (s), and the longest baseband signal accepted for transmission (samples).usbl = false— emulate a USBL modem: each reception also generates aBearingNtfwith the azimuth and elevation of the transmitter.
In a realtime simulation with modem.dataRate = [800.bps, 2400.bps], typing phy[CONTROL].dataRate on a node may show a lower number. The 800 bps is the signaling rate excluding overheads; the reported value is the effective data rate across the whole frame, including preamble and header.
A modem model simulates the half-duplex constraint, propagation delay, interference, detection and packet loss — and to do this accurately, it uses a channel model.
34.8 Channel models
Channel models implement the ChannelModel interface. The default is the BasicAcousticChannel, but it can be changed in the simulation script.
34.8.1 Protocol channel model
The ProtocolChannelModel is the simplest model. Despite its simplicity, it captures propagation delay, limited communication range, interference, collisions and the probabilistic nature of the channel — and it is amenable to mathematical analysis. It is parametrized by a sound speed, a communication range Rc, a detection range Rd, an interference range Ri, a detection probability pd and a decoding probability pc. A frame at range R ≤ Rc is successfully received with probability pd × pc; at Rc < R ≤ Rd it may be detected (probability pd) but not decoded; at R ≤ Ri it interferes with (collides with) any concurrent reception; beyond Ri it is neither detected nor interfering. The defaults are:
import org.arl.unet.sim.channels.*
channel.model = ProtocolChannelModel
channel.soundSpeed = 1500.mps // c
channel.communicationRange = 2000.m // Rc
channel.detectionRange = 2500.m // Rd
channel.interferenceRange = 3000.m // Ri
channel.pDetection = 1 // pd
channel.pDecoding = 1 // pc34.8.2 Basic acoustic channel model
The BasicAcousticChannel is the default, balancing accuracy, applicability and speed. It combines an acoustic model (UrickAcousticModel, average transmission loss) with a communication model (BPSKFadingModel, BPSK in a Rician/Rayleigh fading channel). The acoustic model’s parameters and defaults are:
import org.arl.unet.sim.channels.*
channel.model = BasicAcousticChannel
channel.carrierFrequency = 25.kHz // f
channel.bandwidth = 4096.Hz // B
channel.spreading = 2 // spreading loss factor
channel.temperature = 25.C // water temperature
channel.salinity = 35.ppt // salinity
channel.noiseLevel = 60.dB // noise PSD level
channel.waterDepth = 20.m // water depthFrom these it computes the sound speed (Mackenzie), transmission loss (Urick) and noise level, giving SNR = SL − TL − NL. The fading model uses this SNR to simulate detection (after pulse-compression gain over the preamble bandwidth-time product) and decoding (simulating per-bit errors under fading); its parameters are:
channel.ricianK = 10 // Rician fading parameter (0 = Rayleigh)
channel.fastFading = true // independent fading per bit if true
channel.pfa = 1e-6 // acceptable false-alarm probability
channel.processingGain = 0.dB // processing gainFor readers validating simulation results against theory, the model’s arithmetic is worth spelling out. Detection is based on the effective SNR after pulse compression of the preamble, SNR′ = SNR + 10 log₁₀(Bt), where B is the bandwidth and t the preamble duration; the detection threshold is derived from pfa. Decoding simulates BPSK bit by bit at Eb/N0 = SNR + 10 log₁₀(B/D) + G, where D is the frame’s data rate and G the processingGain, with Rician (or Rayleigh, for ricianK = 0) fading applied — independently per bit when fastFading is on, or as a single fading realization for the whole frame when it is off (slow fading). A single bit error destroys the frame: there is no forward error correction in the model, so FEC gains should be folded into processingGain.
34.8.3 Experiment-based channel models
Channel models can be more faithful than any first-principles approximation by being built from measurements. The Mission2012a, Mission2013a and Mission2013b models encode per-link detection and decoding probabilities measured during the MISSION 2012 and 2013 sea experiments in Singapore waters. A protocol simulated over one of these models predicts what would have happened had it been tested at sea during that experiment — a good way to benchmark under realistic conditions:
import org.arl.unet.sim.channels.*
channel.model = Mission2012a
simulate {
Mission2012a.nodes.each { addr ->
node "P$addr", address: addr, location: Mission2012a.nodeLocation[addr]
}
}34.8.4 HTTP channel model
The HttpChannel model lets you implement a channel model outside the simulator — in a different language, or even on a different machine. It extends the ProtocolChannelModel, retaining its propagation delay, range and collision behavior, but fetches the detection and decoding probabilities for each reception from an external channel server over HTTP:
import org.arl.unet.sim.channels.*
channel.model = HttpChannel
channel.ipaddr = 'localhost' // channel server hostname (default)
channel.port = 8811 // channel server port (default)For every reception, the simulator makes a request of the form:
http://localhost:8811/fidelity/[0.0,0.0,0.0]/[500.0,0.0,0.0]/2/1024/175.0with the transmitter location, receiver location, frame type, frame length in bits, and source level (dB re µPa @ 1 m) encoded in the URL path. The channel server responds with two comma-separated numbers — the probability of detection and the probability of decoding for that reception (e.g. 0.9,0.8). If the server cannot be contacted, frames are not detected.
34.8.5 Custom channel models
For special research needs, you can develop your own channel model. Two classes make good starting points.
The ProtocolChannelModel can be subclassed to provide per-link detection/decoding probabilities — exactly what the mission models do — by overriding getProbabilityDetection(Reception) and getProbabilityDecoding(Reception). Here is the essence of the MISSION 2012 model: the measured per-link probabilities are tabulated, and the two overrides look them up for each reception. Note the conditional probability in getProbabilityDecoding() — the tables record P(no detect) and P(no detect or no decode), so the probability of decoding given detection is their quotient:
import org.arl.unet.sim.*
import org.arl.unet.sim.channels.ProtocolChannelModel
class Mission2012Channel extends ProtocolChannelModel {
static final def nodes = [21, 22, 27, 28, 29]
static final def nodeLocation = [
21: [ 0, 0, -5],
22: [ 398, -105, -18],
27: [-434, -499, -12],
28: [ -32, 279, -20],
29: [-199, -307, -12]
]
static def pNoDetect = [
[ 0, 0.047, 0.095, 0.026, 0.056],
[0.032, 0, 0.228, 0.139, 0.081],
[0.047, 0.174, 0, 0.025, 0.011],
[0.019, 0.060, 0.040, 0, 0.420],
[0.026, 0.018, 0.009, 0.048, 0]
]
static def pNoDetectOrDecode = [
[ 0, 0.157, 0.643, 0.197, 0.239],
[0.184, 0, 0.870, 0.639, 0.435],
[0.326, 0.826, 0, 0.975, 0.023],
[0.038, 0.160, 0.760, 0, 0.900],
[0.070, 0.070, 0.018, 0.871, 0]
]
float getProbabilityDetection(Reception rx) {
int from = nodes.indexOf(rx.from)
int to = nodes.indexOf(rx.address)
if (from < 0 || to < 0) return 0
return 1 - pNoDetect[from][to]
}
float getProbabilityDecoding(Reception rx) {
int from = nodes.indexOf(rx.from)
int to = nodes.indexOf(rx.address)
if (from < 0 || to < 0) return 0
return (1 - pNoDetectOrDecode[from][to]) / (1 - pNoDetect[from][to])
}
}The AbstractAcousticChannel framework (which BasicAcousticChannel builds on) lets you extend or replace the acoustic and communication models — the default channel is nothing more than a pairing of the two:
class BasicAcousticChannel extends AbstractAcousticChannel {
@Delegate UrickAcousticModel acoustics = new UrickAcousticModel(this)
@Delegate BPSKFadingModel comms = new BPSKFadingModel(this)
}For example, to make noise power depend on sea state, extend UrickAcousticModel with a seaState parameter and override getNoisePower():
import org.arl.unet.sim.channels.*
class MyAcousticModel extends UrickAcousticModel {
MyAcousticModel(AbstractAcousticChannel parent) {
super(parent)
}
// map of sea state to noise power spectral density (dB re µPa²/Hz)
final def noiseLevel = [ 0: 20, 1: 30, 2: 35, 3: 40, 4: 42, 5: 44, 6: 46 ]
// sea state parameter, settable from the simulation script
int seaState = 2
@Override
double getNoisePower() {
return Math.pow(10, noiseLevel[seaState]/10) * model.bandwidth
}
}and pair your model with the standard communication model in an AbstractAcousticChannel subclass:
import org.arl.unet.sim.channels.*
class MyAcousticChannel extends AbstractAcousticChannel {
@Delegate UrickAcousticModel acoustics = new MyAcousticModel(this)
@Delegate BPSKFadingModel comms = new BPSKFadingModel(this)
}Selecting channel.model = MyAcousticChannel in the simulation script then runs the whole network over your custom channel. The communication model can be extended or replaced in just the same way.
34.9 Simulating non-acoustic links
Unets aren’t always purely acoustic: gateway buoys may talk to each other over RF, surfaced AUVs may have a WiFi link to a control station, and bottom nodes may be cabled together. Such links don’t need modem and channel models — the GenericLink agent simulates them directly at the LINK service level. Add one to the stack of every node sharing the link:
def rfstack = { c ->
c.add 'rf', new org.arl.unet.sim.GenericLink(
dataRate: 28800.bps, // link speed
broadcast: true, // support broadcast datagrams
shouldReceive: { // RF works up to 2.5 km, when both nodes are at the surface
it.range <= 2500 && it.txLocation[2] >= 0 && it.rxLocation[2] >= 0
}
)
}
simulate {
node 'buoy', address: 1, location: [0, 0, 0], stack: rfstack
node 'auv', address: 2, location: [500.m, 0, 0], mobility: true, stack: rfstack
}The shouldReceive closure decides whether each transmission is delivered: it is given the transmitter location, receiver location and range, so it can model range limits, line-of-sight constraints or surfaced-only connectivity. Datagrams sent to the agent arrive as DatagramNtf on the peer node, and since the agent provides the LINK service, a Router can route over it like over any other link. Key properties and their defaults:
dataRate = 0— link data rate (bps), used to compute transmission time (0 for an infinitely fast link).MTU = 1450,RTU = 1450— maximum and recommended transfer units (bytes).reliability = true— support reliable datagrams: deliveries are acknowledged and generate aDatagramDeliveryNtf, or aDatagramFailureNtfif no acknowledgement arrives withinackTimeout(ms).broadcast = false— deliver broadcast datagrams.shouldReceive = null— predicate deciding whether a potential reception is delivered (nulldelivers everything).keepAlive = 0— interval (s) at which idle peers are probed, generating aLinkStatusNtfwhenever the link to a peer goes up or down (0 disables; peers to monitor may be seeded via thepeersparameter).
Cabled and wireless IP links can be simulated using UdpLink, while non-IP radio modems and satellite communications are best simulated using the GenericLink. For example, the shouldReceive closure of the GenericLink can be used to automatically disable the a RF link once a simulated AUV submerges, and re-enable it when the AUV re-surfaces.