new routing algorithm

This commit is contained in:
kdee committed 2026-07-22 17:37:04 -07:00
1 parent 37598479cc
commit acfb2719e2
11 files changed
+671 -351

No files matched your search

+37 -12
View File
@@ -1,4 +1,4 @@
function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, minAlt, timestep, maxIter, obstacles, makePlots, makeVideo, useDoubleIntegrator, dampingCoeff, useFixedTopology, optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology, routingFlowThreshold) function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, minAlt, timestep, maxIter, obstacles, makePlots, makeVideo, useDoubleIntegrator, dampingCoeff, useFixedTopology, optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology)
arguments (Input) arguments (Input)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
domain (1, 1) {mustBeGeometry}; domain (1, 1) {mustBeGeometry};
@@ -22,7 +22,6 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
centerFreq (1, 1) double = 2.4e9; centerFreq (1, 1) double = 2.4e9;
bandwidth (1, 1) double = 20e6; bandwidth (1, 1) double = 20e6;
useRoutingTopology (1, 1) logical = false; useRoutingTopology (1, 1) logical = false;
routingFlowThreshold (1, 1) double = 0.02;
end end
arguments (Output) arguments (Output)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
@@ -114,23 +113,49 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
obj.centerFreq = centerFreq; obj.centerFreq = centerFreq;
obj.bandwidth = bandwidth; obj.bandwidth = bandwidth;
% Set topology-selection algorithm and routing parameters % Set topology-selection algorithm; fail fast on invalid mode combinations
assert(~(useRoutingTopology && useFixedTopology), ...
"useRoutingTopology and useFixedTopology are mutually exclusive topology selectors");
assert(~useRoutingTopology || useSinrComms, ...
"useRoutingTopology requires the SINR comms model (useSinrComms)");
obj.useRoutingTopology = useRoutingTopology; obj.useRoutingTopology = useRoutingTopology;
obj.routingFlowThreshold = routingFlowThreshold;
% Compute adjacency matrix and network topology. Lesser-neighbor always % Compute adjacency matrix and network topology. The Capacity-Aware
% provides the low-level connectivity topology (basic traffic must be % Lesser Sink Neighbor algorithm replaces lesser-neighbor when selected
% routable between any two drones at all times); routing mode overlays % (MATLAB sim only; the compiled path never sets the flag).
% the bulk-data links on top of it. The routing LP uses linprog (no
% codegen support); the compiled path never sets the flag, so linprog
% is never compiled.
obj = obj.updateAdjacency(); obj = obj.updateAdjacency();
if obj.useFixedTopology if obj.useFixedTopology
obj.constraintAdjacencyMatrix = obj.adjacency; obj.constraintAdjacencyMatrix = obj.adjacency;
elseif coder.target('MATLAB') && obj.useRoutingTopology
obj = obj.lesserSinkNeighbor();
else else
obj = obj.lesserNeighbor(); obj = obj.lesserNeighbor();
if coder.target('MATLAB') && obj.useRoutingTopology end
obj = obj.routeTopology();
% Guard against the SINR CBF deadlock band. Connectivity admits links
% with SINR anywhere above sinrThreshold, but constrainMotion maintains
% every selected link at sinrThreshold + 3 dB (paddingDb — keep in
% sync) in BOTH directions. A link selected inside that 3 dB band is
% born with a violated barrier; the QP can then go infeasible and hold
% positions, and since a frozen geometry re-selects the same link, the
% fleet deadlocks permanently. This catches the initial configuration;
% geometry evolution can still enter the band mid-run (fixing that
% requires pad-aware selection or a bounded-recovery barrier floor).
if coder.target('MATLAB') && obj.useSinrComms
gammaCbfLin = 10^((obj.sinrThreshold + 3.0) / 10);
posChk = zeros(size(obj.agents, 1), 3);
for kk = 1:size(obj.agents, 1)
posChk(kk, :) = obj.agents{kk}.pos;
end
for ii = 1:(size(obj.agents, 1) - 1)
for jj = (ii + 1):size(obj.agents, 1)
if obj.constraintAdjacencyMatrix(ii, jj)
sinrBinding = min(obj.sinrLink(posChk, ii, jj), obj.sinrLink(posChk, jj, ii));
assert(sinrBinding >= gammaCbfLin, ...
"Initial topology link %d-%d has SINR %.1f dB, inside the CBF maintenance band (sinrThreshold %.1f dB + 3 dB padding): the QP starts with a violated comms barrier and can deadlock. Lower sinrThreshold or adjust the initial geometry.", ...
ii, jj, 10 * log10(sinrBinding), obj.sinrThreshold);
end
end
end end
end end
+2 -7
View File
@@ -147,17 +147,12 @@ else
BANDWIDTH = 20e6; BANDWIDTH = 20e6;
end end
% Routing topology selection (optional columns; MATLAB sim only) % Routing topology selection (optional column; MATLAB sim only)
if isfield(scenario, 'useRoutingTopology') if isfield(scenario, 'useRoutingTopology')
USE_ROUTING_TOPOLOGY = logical(scenario.useRoutingTopology); USE_ROUTING_TOPOLOGY = logical(scenario.useRoutingTopology);
else else
USE_ROUTING_TOPOLOGY = false; USE_ROUTING_TOPOLOGY = false;
end end
if isfield(scenario, 'routingFlowThreshold')
ROUTING_FLOW_THRESHOLD = scenario.routingFlowThreshold;
else
ROUTING_FLOW_THRESHOLD = 0.02;
end
% ---- Build domain -------------------------------------------------------- % ---- Build domain --------------------------------------------------------
dom = rectangularPrism; dom = rectangularPrism;
@@ -208,6 +203,6 @@ obj = obj.initialize(dom, agentList, BARRIER_GAIN, BARRIER_EXPONENT, ...
MIN_ALT, TIMESTEP, MAX_ITER, obstacleList, false, false, ... MIN_ALT, TIMESTEP, MAX_ITER, obstacleList, false, false, ...
USE_DOUBLE_INTEGRATOR, DAMPING_COEFF, USE_FIXED_TOPOLOGY, false, ... USE_DOUBLE_INTEGRATOR, DAMPING_COEFF, USE_FIXED_TOPOLOGY, false, ...
USE_SINR_COMMS, SINR_THRESHOLD, PATH_LOSS_EXPONENT, AMBIENT_TEMP, CENTER_FREQ, BANDWIDTH, ... USE_SINR_COMMS, SINR_THRESHOLD, PATH_LOSS_EXPONENT, AMBIENT_TEMP, CENTER_FREQ, BANDWIDTH, ...
USE_ROUTING_TOPOLOGY, ROUTING_FLOW_THRESHOLD); USE_ROUTING_TOPOLOGY);
end end
+1 -6
View File
@@ -116,17 +116,12 @@ if isfield(inits, 'useRoutingTopology')
else else
useRoutingTopology = false; useRoutingTopology = false;
end end
if isfield(inits, 'routingFlowThreshold')
routingFlowThreshold = inits.routingFlowThreshold;
else
routingFlowThreshold = 0.02;
end
% ---- Initialize simulation (plots and video disabled) ------------------------ % ---- Initialize simulation (plots and video disabled) ------------------------
obj = obj.initialize(dom, agentList, inits.barrierGain, inits.barrierExponent, ... obj = obj.initialize(dom, agentList, inits.barrierGain, inits.barrierExponent, ...
inits.minAlt, inits.timestep, inits.maxIter, obstacleList, ... inits.minAlt, inits.timestep, inits.maxIter, obstacleList, ...
false, false, useDoubleIntegrator, dampingCoeff, useFixedTopology, false, ... false, false, useDoubleIntegrator, dampingCoeff, useFixedTopology, false, ...
useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, ... useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, ...
useRoutingTopology, routingFlowThreshold); useRoutingTopology);
end end
+372
View File
@@ -0,0 +1,372 @@
function obj = lesserSinkNeighbor(obj)
% LESSERSINKNEIGHBOR Capacity-Aware Lesser Sink Neighbor algorithm (CALSN).
%
% Implements the algorithm specified in new/new_routing_algorithm.pdf as the
% selectable alternative to lesserNeighbor. UAVs 1 and N are base stations;
% every other UAV generates r = 1 unit of data per step, and all data must
% reach a base station. The algorithm outputs both a sparse maintained
% topology and a routing table.
%
% 1. Base-station potentials phi via distributed Bellman-Ford over the
% current communication neighborhoods (obj.adjacency), with link cost
% c_ij = 1/(C_ij + eps), where C_ij = bandwidth * log2(1 + SINR_ij) is
% the Shannon capacity of direction i -> j.
% 2. Lesser sink neighbors L_i replace LNA's "j < i" rule:
% L_i = { j in N_i : phi_j < phi_i, or phi_j == phi_i and j < i }.
% The equal-potential lexicographic tie-break is a documented deviation
% from the spec's strict inequality: at an exact potential watershed
% (e.g. a symmetric chain) the strict rule would split A_control into
% one tree per base station; the tie-break keeps the airborne graph
% connected, which the mission requires.
% 3. LNA-style sparsity: one parent per connected component of the
% L_i-induced subgraph (only links with positive capacity in at least
% one direction count as edges), chosen by
% p = argmin_{j in C} ( phi_j + 1/(C_ij + eps) ).
% A_control(i,p) = A_control(p,i) = 1 (diagonal = I).
% BASIN EXTENSION (documented deviation): with two potential minima
% (the two bases), one-parent-per-component alone yields a spanning
% forest with up to two disjoint base-rooted trees whenever a UAV's
% lesser sink neighbors collapse into a single component (dense usable
% graphs). To restore LNA's global-connectivity property, each UAV
% selects one representative per component AND per base-station BASIN
% (the base a neighbor's potential descent terminates at). Interior
% UAVs are unaffected (their whole neighborhood shares one basin);
% watershed UAVs whose neighborhoods span both basins gain a second
% parent, which is exactly the bridge between the two trees. A final
% stitch guard covers degenerate geometries: if A_control still has
% multiple components, the lowest-cost feasible inter-component link
% is added (maintained for connectivity only; carries no flow).
% 4. Routing, children first (decreasing phi; equal phi processed in
% decreasing index order so tie-break children precede their parents):
% aggregate demand D_i = r + inflows, split capacity-proportionally
% over parents; saturated links are capped at capacity and the residual
% redistributed over unsaturated parents; any demand beyond the total
% parent capacity is reported as unrouted and the UAV marked infeasible.
%
% Outputs (set on obj):
% constraintAdjacencyMatrix A_control: undirected maintained topology
% routingFlows F: flow on each directed link (units of r)
% routingFractions R = F/D: share of the sender's demand, 0..1
% routingAdjacencyMatrix F > 0 (directed, child -> parent)
% routingPotentials phi (base-station potential per UAV)
% routingUnrouted unrouted demand per UAV (zeros when feasible)
arguments (Input)
obj (1, 1) {mustBeA(obj, "miSim")};
end
arguments (Output)
obj (1, 1) {mustBeA(obj, "miSim")};
end
nAgents = size(obj.agents, 1);
r = 1.0; % data generated per non-base UAV per step (abstract unit)
EPS_CAP = 1e-12; % epsilon in c = 1/(C + eps); avoids division by zero
TOL = 1e-12; % residual-demand tolerance for the saturation loop
isBase = false(nAgents, 1);
isBase(1) = true;
isBase(nAgents) = true; % base stations are UAVs 1 and N
positions = zeros(nAgents, 3);
for kk = 1:nAgents
positions(kk, :) = obj.agents{kk}.pos;
end
% Directed Shannon link capacities over the current communication
% neighborhoods: C(i, j) = capacity of sending from i to j (receiver j)
C = zeros(nAgents);
for ii = 1:nAgents
for jj = 1:nAgents
if jj ~= ii && obj.adjacency(ii, jj)
C(ii, jj) = obj.bandwidth * log2(1 + obj.sinrLink(positions, jj, ii));
end
end
end
% ---- Base-station potentials (Bellman-Ford, at most N-1 sweeps) -----
phi = inf(nAgents, 1);
phi(isBase) = 0;
for sweep = 1:max(nAgents - 1, 1)
changed = false;
for ii = 1:nAgents
if isBase(ii)
continue;
end
newPhi = inf;
for jj = 1:nAgents
if jj == ii || ~obj.adjacency(ii, jj)
continue;
end
newPhi = min(newPhi, phi(jj) + 1 / (C(ii, jj) + EPS_CAP));
end
if newPhi ~= phi(ii)
phi(ii) = newPhi;
changed = true;
end
end
if ~changed
break;
end
end
% ---- Base-station basins (for the basin extension) -------------------
% Each finite-potential UAV descends its argmin neighbor chain; phi is
% strictly decreasing along it, so the chain terminates at a base. The
% basin label records which base that is.
descent = zeros(nAgents, 1);
for ii = 1:nAgents
if isBase(ii) || isinf(phi(ii))
continue;
end
bestMetric = inf;
for jj = 1:nAgents
if jj == ii || ~obj.adjacency(ii, jj)
continue;
end
metric = phi(jj) + 1 / (C(ii, jj) + EPS_CAP);
if metric < bestMetric
bestMetric = metric;
descent(ii) = jj;
end
end
end
basin = zeros(nAgents, 1);
basin(1) = 1;
basin(nAgents) = nAgents;
for ii = 1:nAgents
if isBase(ii) || isinf(phi(ii))
continue;
end
kk = ii;
while ~isBase(kk)
kk = descent(kk);
end
basin(ii) = kk;
end
% ---- Parent selection (one per lesser-sink component and basin) ------
Acontrol = logical(eye(nAgents));
isParent = false(nAgents); % isParent(i, p): p is a selected parent of i
for ii = 1:nAgents
if isBase(ii) || isinf(phi(ii))
% Base stations select no parents; an infinite potential means
% no current local route toward a base station
continue;
end
% Lesser sink neighbors (with the equal-potential tie-break)
lsnBuf = zeros(1, nAgents);
lsnCount = 0;
for jj = 1:nAgents
if jj == ii || ~obj.adjacency(ii, jj)
continue;
end
if phi(jj) < phi(ii) || (phi(jj) == phi(ii) && jj < ii)
lsnCount = lsnCount + 1;
lsnBuf(lsnCount) = jj;
end
end
if lsnCount == 0
continue;
end
lsn = lsnBuf(1:lsnCount);
% Subgraph induced by the lesser sink neighbors in the usable
% communication graph: only links with positive capacity in at
% least one direction count as edges (a zero-capacity link cannot
% carry data, so it must not merge two components)
subgraphAdjacency = false(lsnCount);
for aa = 1:lsnCount
for bb = (aa + 1):lsnCount
ja = lsn(aa);
jb = lsn(bb);
if obj.adjacency(ja, jb) && (C(ja, jb) > 0 || C(jb, ja) > 0)
subgraphAdjacency(aa, bb) = true;
subgraphAdjacency(bb, aa) = true;
end
end
end
% One parent per connected component and per base-station basin
% present in it (basin extension), selected by capacity and
% potential: p = argmin( phi_j + 1/(C_ij + eps) )
visited = false(1, lsnCount);
for aa = 1:lsnCount
if visited(aa)
continue;
end
component = bfs(subgraphAdjacency, aa);
visited(component) = true;
for bb = [1, nAgents] % the two possible basin labels
bestMetric = inf;
bestParent = 0;
for cc = 1:numel(component)
jj = lsn(component(cc));
if basin(jj) ~= bb
continue;
end
metric = phi(jj) + 1 / (C(ii, jj) + EPS_CAP);
if metric < bestMetric
bestMetric = metric;
bestParent = jj;
end
end
if bestParent > 0
isParent(ii, bestParent) = true;
Acontrol(ii, bestParent) = true;
Acontrol(bestParent, ii) = true;
end
end
end
end
% ---- Connectivity stitch guard ---------------------------------------
% The basin extension bridges the two base-rooted trees through
% watershed UAVs whenever each base borders a routing basin. Degenerate
% geometries (e.g. a base whose only feasible neighbor is the other
% base) can still leave A_control split; add the lowest-cost feasible
% link between components until one remains. Stitched links are
% maintained for connectivity only and carry no routed flow. If the
% feasibility graph itself is disconnected no stitch exists; validate()
% reports that case.
while true
comp = zeros(1, nAgents);
nComp = 0;
for ii = 1:nAgents
if comp(ii) == 0
nComp = nComp + 1;
comp(bfs(Acontrol, ii)) = nComp;
end
end
if nComp <= 1
break;
end
bestMetric = inf;
bestU = 0;
bestV = 0;
for uu = 1:nAgents
for vv = (uu + 1):nAgents
if comp(uu) ~= comp(vv) && obj.adjacency(uu, vv)
% Bidirectional link cost (finite even for infinite
% potentials, so isolated islands remain stitchable)
metric = 1 / (C(uu, vv) + EPS_CAP) + 1 / (C(vv, uu) + EPS_CAP);
if metric < bestMetric
bestMetric = metric;
bestU = uu;
bestV = vv;
end
end
end
end
if bestU == 0
break; % feasibility graph disconnected: nothing to stitch with
end
Acontrol(bestU, bestV) = true;
Acontrol(bestV, bestU) = true;
end
% ---- Routing (children first: decreasing potential order) -----------
% Equal potentials are processed in decreasing index order: with the
% lexicographic tie-break, flow within an equal-potential group goes
% from higher to lower index, so higher indices must aggregate first.
F = zeros(nAgents);
R = zeros(nAgents);
unrouted = zeros(nAgents, 1);
nonBase = find(~isBase);
[~, ord] = sortrows([phi(nonBase), nonBase], [-1, -2]);
order = nonBase(ord);
for oo = 1:numel(order)
ii = order(oo);
% Aggregated outgoing demand: own data plus everything received
D = r + sum(F(:, ii));
parents = find(isParent(ii, :));
if D == 0 || isempty(parents)
continue;
end
% Capacity-proportional split, saturating full links first and
% redistributing the remainder across parents with remaining
% capacity. Each pass either routes all residual demand or
% saturates at least one parent, so the loop terminates.
capP = C(ii, parents);
flows = zeros(1, numel(parents));
active = true(1, numel(parents));
residual = D;
while residual > TOL && any(active)
totalCap = sum(capP(active));
if totalCap <= 0
break;
end
alloc = zeros(1, numel(parents));
alloc(active) = residual .* capP(active) ./ totalCap;
flows = flows + alloc;
saturated = active & (flows > capP);
if any(saturated)
residual = sum(flows(saturated) - capP(saturated));
flows(saturated) = capP(saturated);
active(saturated) = false;
else
residual = 0;
end
end
if residual > TOL
% Demand exceeds the total parent capacity: route as much as
% possible and report the remainder as unrouted (infeasible)
unrouted(ii) = residual;
warning("miSim:routingInfeasible", ...
"UAV %d demand %.3g exceeds total parent capacity; %.3g units unrouted", ...
ii, D, residual);
end
F(ii, parents) = flows;
R(ii, parents) = flows ./ D;
end
% ---- Outputs ---------------------------------------------------------
obj.constraintAdjacencyMatrix = Acontrol;
obj.routingFlows = F;
obj.routingFractions = R;
obj.routingAdjacencyMatrix = F > 0;
obj.routingPotentials = phi;
obj.routingUnrouted = unrouted;
end
function cComp = bfs(subgraphAdjacency, startIdx)
% Breadth-first search returning the connected component of startIdx.
% Pre-allocated queue and component buffer with head/tail pointers to
% avoid element deletion and dynamic array growth (same pattern as the
% local bfs in lesserNeighbor.m).
n = size(subgraphAdjacency, 1);
visited = false(1, n);
queue = zeros(1, n);
cCompBuf = zeros(1, n);
qHead = 1;
qTail = 2;
queue(1) = startIdx;
cCompBuf(1) = startIdx;
cSize = 1;
visited(startIdx) = true;
while qHead < qTail
current = queue(qHead);
qHead = qHead + 1;
neighbors = find(subgraphAdjacency(current, :));
for kk = 1:numel(neighbors)
neighbor = neighbors(kk);
if ~visited(neighbor)
visited(neighbor) = true;
cCompBuf(cSize + 1) = neighbor;
cSize = cSize + 1;
queue(qTail) = neighbor;
qTail = qTail + 1;
end
end
end
cComp = sort(cCompBuf(1:cSize));
end
+12 -8
View File
@@ -31,13 +31,17 @@ classdef miSim
centerFreq = 2.4e9; % carrier frequency (Hz); sets path-loss offset K = (4*pi*f_c/c)^2 centerFreq = 2.4e9; % carrier frequency (Hz); sets path-loss offset K = (4*pi*f_c/c)^2
bandwidth = 20e6; % channel bandwidth (Hz) for thermal noise N = k_B*T*B bandwidth = 20e6; % channel bandwidth (Hz) for thermal noise N = k_B*T*B
% Topology-selection algorithm (requires useSinrComms; MATLAB sim only). % Topology-selection algorithm (requires useSinrComms; MATLAB sim only).
% false -> lesser-neighbor; true -> min-max-workload LP routing, which % false -> lesser-neighbor; true -> Capacity-Aware Lesser Sink Neighbor
% picks the links that route all sensing data to the network endpoints % (CALSN, see new/new_routing_algorithm.pdf): base-station potentials
% (agents 1 and the largest odd index) with minimal peak node workload. % replace the index-based "lesser" rule, and the maintained topology
% comes with a routing table (flows F, fractions R) that sends every
% UAV's data toward base stations 1 and N.
useRoutingTopology = false; useRoutingTopology = false;
routingFlowThreshold = 0.02; % prune links carrying less flow than this (units of one node's production) routingAdjacencyMatrix = false(0, 0); % directed flow links (i, j) = child i sends to parent j (F > 0)
routingAdjacencyMatrix = false(0, 0); % directed adjacency chosen by the routing LP: (i, j) = link i -> j routingFlows = zeros(0, 0); % F: flow on each directed link (units of r = 1 per UAV per step)
routingFlows = zeros(0, 0); % data flow carried by each directed link (units/timestep) routingFractions = zeros(0, 0); % R = F/D: share of the sender's aggregated demand per link (0..1)
routingPotentials = zeros(0, 1); % phi: Bellman-Ford base-station potential per UAV
routingUnrouted = zeros(0, 1); % unrouted demand per UAV (zeros when routing is feasible)
artifactName = ""; artifactName = "";
f; % main plotting tiled layout figure f; % main plotting tiled layout figure
fPerf; % performance plot figure fPerf; % performance plot figure
@@ -87,13 +91,13 @@ classdef miSim
obj.obstacles = {rectangularPrism}; obj.obstacles = {rectangularPrism};
obj.agents = {agent}; obj.agents = {agent};
end end
[obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, minAlt, timestep, maxIter, obstacles, makePlots, makeVideo, useDoubleIntegrator, dampingCoeff, useFixedTopology, optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology, routingFlowThreshold); [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, minAlt, timestep, maxIter, obstacles, makePlots, makeVideo, useDoubleIntegrator, dampingCoeff, useFixedTopology, optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology);
[obj] = initializeFromCsv(obj, csvPath); [obj] = initializeFromCsv(obj, csvPath);
[obj] = initializeFromInits(obj, initsPath); [obj] = initializeFromInits(obj, initsPath);
[obj] = plotFromSimHist(obj, initsPath, histPath); [obj] = plotFromSimHist(obj, initsPath, histPath);
[obj] = run(obj); [obj] = run(obj);
[obj] = lesserNeighbor(obj); [obj] = lesserNeighbor(obj);
[obj] = routeTopology(obj); [obj] = lesserSinkNeighbor(obj);
[obj] = constrainMotion(obj); [obj] = constrainMotion(obj);
[sinr, grad] = sinrLink(obj, positions, rxIdx, txIdx); [sinr, grad] = sinrLink(obj, positions, rxIdx, txIdx);
[obj] = partition(obj); [obj] = partition(obj);
+12 -14
View File
@@ -6,11 +6,10 @@ function obj = plotGraph(obj)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
end end
% The maintained (low-level connectivity) links are always drawn as a % The maintained links are always drawn as a dotted undirected graph.
% dotted undirected graph: basic traffic must be routable between any % In routing (CALSN) mode the flow-carrying links are overlaid as SOLID
% two drones at all times. In routing mode the bulk-data links selected % DIRECTED edges (arrow = child -> parent, toward a base station) on
% by the LP are overlaid as SOLID DIRECTED edges (arrow = transmitter -> % the same node layout.
% receiver) on the same node layout.
G = graph(obj.constraintAdjacencyMatrix, "omitselfloops"); G = graph(obj.constraintAdjacencyMatrix, "omitselfloops");
% Resolve the target axes for the network graph tile % Resolve the target axes for the network graph tile
@@ -24,7 +23,7 @@ function obj = plotGraph(obj)
hold(ax, "on"); hold(ax, "on");
o = plot(ax, G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2); o = plot(ax, G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2);
if obj.useRoutingTopology if obj.useRoutingTopology
% Bulk overlay: reuse the base layout's node coordinates and hide % Flow overlay: reuse the base layout's node coordinates and hide
% the overlay's own nodes/labels so only its edges show. Sizeable % the overlay's own nodes/labels so only its edges show. Sizeable
% arrowheads so the flow direction reads at tile size. % arrowheads so the flow direction reads at tile size.
D = digraph(obj.routingAdjacencyMatrix); D = digraph(obj.routingAdjacencyMatrix);
@@ -51,11 +50,11 @@ function obj = plotGraph(obj)
positions(kk, :) = obj.agents{kk}.pos; positions(kk, :) = obj.agents{kk}.pos;
end end
% Labels are bare numbers (no units) so they stay compact; GraphPlot % Labels are bare numbers (no units) so they stay compact; GraphPlot
% renders them horizontally, left-to-right. Dotted (low-level) edges % renders them horizontally, left-to-right. Dotted (maintained) edges
% are labelled with the binding (worse) directional SINR — a link is % are labelled with the binding (worse) directional SINR — a link is
% feasible only if BOTH directions clear the threshold. Links that % feasible only if BOTH directions clear the threshold. Links that
% also carry a bulk overlay leave the label to the solid edge, which % also carry flow leave the label to the solid directed edge, which
% shows the SINR of the transmission direction. % shows the flow value (units of r) sent child -> parent.
edgeLabels = strings(size(endNodes, 1), 1); edgeLabels = strings(size(endNodes, 1), 1);
for ee = 1:size(endNodes, 1) for ee = 1:size(endNodes, 1)
lo = endNodes(ee, 1); lo = endNodes(ee, 1);
@@ -70,13 +69,12 @@ function obj = plotGraph(obj)
end end
o(1).EdgeLabel = edgeLabels; o(1).EdgeLabel = edgeLabels;
if obj.useRoutingTopology if obj.useRoutingTopology
endNodesD = D.Edges.EndNodes; % each row [tx rx] endNodesD = D.Edges.EndNodes; % each row [child parent]
bulkLabels = strings(size(endNodesD, 1), 1); flowLabels = strings(size(endNodesD, 1), 1);
for ee = 1:size(endNodesD, 1) for ee = 1:size(endNodesD, 1)
sinrTx = obj.sinrLink(positions, endNodesD(ee, 2), endNodesD(ee, 1)); % rx, tx flowLabels(ee) = sprintf("%.2f", obj.routingFlows(endNodesD(ee, 1), endNodesD(ee, 2)));
bulkLabels(ee) = sprintf("%.1f", 10 * log10(sinrTx));
end end
o(2).EdgeLabel = bulkLabels; o(2).EdgeLabel = flowLabels;
end end
% Threshold goes in the existing single-line title (not a subtitle) so % Threshold goes in the existing single-line title (not a subtitle) so
% the small tile is not compressed further, and the hover toolbar is % the small tile is not compressed further, and the hover toolbar is
-146
View File
@@ -1,146 +0,0 @@
function obj = routeTopology(obj)
% ROUTETOPOLOGY Select the network topology by solving a min-max-workload
% routing LP (alternative to the lesser-neighbor algorithm).
%
% Every agent produces one unit of sensing data per timestep, all of which
% must reach one of the network endpoints (agents 1 and the largest odd
% index), which output data off-network for free (bottomless sinks). Data
% may split across paths, but relayed data accumulates at intermediate
% nodes, so each node experiences a workload: the share of a timestep it
% spends transmitting and receiving, with per-link effort flow/capacity and
% Shannon capacity c = log2(1 + SINR). The LP minimizes the maximum
% workload over all nodes, since the busiest node bounds system capacity.
%
% Candidate links are the threshold-feasible pairs in obj.adjacency (both
% directional SINRs clear sinrThreshold, per updateAdjacency). After the
% first solve, links carrying negligible flow (< routingFlowThreshold) are
% disallowed and the LP is re-solved so they cannot reappear: feasible but
% impractical links add system complexity for very little gain.
%
% The routed links are a HIGH-QUALITY OVERLAY on top of the low-level
% connectivity topology, not a replacement for it: basic command/telemetry
% traffic must always be routable between any two drones, so the caller
% runs lesserNeighbor first and this method ORs the bulk links into the
% maintained set. Both tiers are held above threshold by the CBF.
%
% Outputs (set on obj):
% routingAdjacencyMatrix directed adjacency, (i, j) = bulk link i -> j
% routingFlows flow carried by each directed link
% constraintAdjacencyMatrix existing (lesser-neighbor) links OR the
% symmetrized routed links (the CBF maintains
% both directions of every link)
%
% Uses linprog (Optimization Toolbox): MATLAB simulation only, no codegen.
arguments (Input)
obj (1, 1) {mustBeA(obj, "miSim")};
end
arguments (Output)
obj (1, 1) {mustBeA(obj, "miSim")};
end
nAgents = size(obj.agents, 1);
% Network endpoints: agent 1 and the largest ODD index (nAgents when
% odd, nAgents-1 when even); with one or two agents only agent 1.
endpoints = unique([1, 2 * ceil(nAgents / 2) - 1]);
isEndpoint = false(1, nAgents);
isEndpoint(endpoints) = true;
% Trivial network: every node is an endpoint, no bulk routing needed
% (the lesser-neighbor constraint topology is left untouched)
if all(isEndpoint)
obj.routingAdjacencyMatrix = false(nAgents);
obj.routingFlows = zeros(nAgents);
return;
end
positions = zeros(nAgents, 3);
for kk = 1:nAgents
positions(kk, :) = obj.agents{kk}.pos;
end
% Candidate directed links: threshold-feasible pairs, excluding
% endpoint-sourced links (endpoints only sink; allowing them to
% transmit only enables useless cycles). Capacity is the Shannon
% spectral efficiency of the transmission direction.
src = zeros(nAgents^2, 1);
dst = zeros(nAgents^2, 1);
cap = zeros(nAgents^2, 1);
nE = 0;
for ii = 1:nAgents
if isEndpoint(ii)
continue;
end
for jj = 1:nAgents
if jj == ii || ~obj.adjacency(ii, jj)
continue;
end
nE = nE + 1;
src(nE) = ii;
dst(nE) = jj;
cap(nE) = log2(1 + obj.sinrLink(positions, jj, ii)); % tx ii -> rx jj
end
end
src = src(1:nE);
dst = dst(1:nE);
cap = cap(1:nE);
% --- LP over x = [f_1 .. f_nE, W] ------------------------------------
% min W + eps*sum(f) (eps breaks ties toward sparse, cycle-free flow)
% s.t. flow conservation (per non-endpoint i): out(i) - in(i) = 1
% workload (per node i): sum(f_out/c) + sum(f_in/c) - W <= 0
% f >= 0
relays = find(~isEndpoint);
nRelays = numel(relays);
fObj = [1e-6 * ones(nE, 1); 1];
Aeq = zeros(nRelays, nE + 1);
for rr = 1:nRelays
Aeq(rr, 1:nE) = double(src == relays(rr))' - double(dst == relays(rr))';
end
beq = ones(nRelays, 1);
Aineq = zeros(nAgents, nE + 1);
for ii = 1:nAgents
Aineq(ii, 1:nE) = (double(src == ii)' + double(dst == ii)') ./ cap';
Aineq(ii, nE + 1) = -1;
end
bineq = zeros(nAgents, 1);
lb = zeros(nE + 1, 1);
options = optimoptions("linprog", "Display", "off");
[x, ~, exitflag] = linprog(fObj, Aineq, bineq, Aeq, beq, lb, [], options);
if exitflag <= 0
error("Routing LP infeasible: feasible links cannot route all data to an endpoint");
end
% Prune trivial links and re-solve with them disallowed, so negligible
% flows cannot reappear in the final solution
ub = inf(nE + 1, 1);
ub(x(1:nE) < obj.routingFlowThreshold) = 0;
[x, ~, exitflag] = linprog(fObj, Aineq, bineq, Aeq, beq, lb, ub, options);
if exitflag <= 0
error("Routing LP infeasible after pruning: routingFlowThreshold eliminated links needed to route all data to an endpoint");
end
% Extract the directed topology from the pruned solution
flows = x(1:nE);
obj.routingFlows = zeros(nAgents);
obj.routingAdjacencyMatrix = false(nAgents);
for ee = 1:nE
if flows(ee) > 1e-9
obj.routingFlows(src(ee), dst(ee)) = flows(ee);
obj.routingAdjacencyMatrix(src(ee), dst(ee)) = true;
end
end
% OR the bulk links into the low-level (lesser-neighbor) constraint
% topology already on obj: the CBF maintains both tiers, keeping the
% network connected for basic traffic while holding the routed links.
% Both directions of every link are maintained (feasibility requires
% both to clear the threshold anyway).
obj.constraintAdjacencyMatrix = obj.constraintAdjacencyMatrix | obj.routingAdjacencyMatrix | obj.routingAdjacencyMatrix';
end
+5 -4
View File
@@ -42,12 +42,13 @@ function [obj] = run(obj)
% plotting purposes, the real partitioning is done by the agents) % plotting purposes, the real partitioning is done by the agents)
[obj.partitioning, obj.agents] = obj.agents{1}.partition(obj.agents, obj.domain.objective); [obj.partitioning, obj.agents] = obj.agents{1}.partition(obj.agents, obj.domain.objective);
% Determine desired communications links: lesser-neighbor gives the % Determine desired communications links: the Capacity-Aware Lesser
% low-level connectivity topology; routing mode overlays bulk links % Sink Neighbor algorithm replaces lesser-neighbor when selected
if ~obj.useFixedTopology if ~obj.useFixedTopology
obj = obj.lesserNeighbor();
if coder.target('MATLAB') && obj.useRoutingTopology if coder.target('MATLAB') && obj.useRoutingTopology
obj = obj.routeTopology(); obj = obj.lesserSinkNeighbor();
else
obj = obj.lesserNeighbor();
end end
end end
+6
View File
@@ -19,6 +19,12 @@ function validate(obj)
error("Network is not connected"); error("Network is not connected");
end end
% In routing (CALSN) mode the MAINTAINED topology itself must form a
% single connected component (basin extension + stitch guard invariant)
if obj.useRoutingTopology && max(conncomp(graph(obj.constraintAdjacencyMatrix))) ~= 1
error("Maintained routing topology (A_control) is not connected");
end
if any(obj.adjacency - obj.constraintAdjacencyMatrix < 0, "all") if any(obj.adjacency - obj.constraintAdjacencyMatrix < 0, "all")
error("Eliminated network connections that were necessary"); error("Eliminated network connections that were necessary");
end end
+1 -1
View File
@@ -62,7 +62,7 @@ function writeInits(obj)
"useDoubleIntegrator", obj.useDoubleIntegrator, "dampingCoeff", obj.dampingCoeff, "useFixedTopology", obj.useFixedTopology, ... "useDoubleIntegrator", obj.useDoubleIntegrator, "dampingCoeff", obj.dampingCoeff, "useFixedTopology", obj.useFixedTopology, ...
"useSinrComms", obj.useSinrComms, "sinrThreshold", obj.sinrThreshold, "pathLossExponent", obj.pathLossExponent, "ambientTemp", obj.ambientTemp, ... "useSinrComms", obj.useSinrComms, "sinrThreshold", obj.sinrThreshold, "pathLossExponent", obj.pathLossExponent, "ambientTemp", obj.ambientTemp, ...
"centerFreq", obj.centerFreq, "bandwidth", obj.bandwidth, "txPower", txPower, ... % SINR comms model parameters "centerFreq", obj.centerFreq, "bandwidth", obj.bandwidth, "txPower", txPower, ... % SINR comms model parameters
"useRoutingTopology", obj.useRoutingTopology, "routingFlowThreshold", obj.routingFlowThreshold, ... % routing topology parameters "useRoutingTopology", obj.useRoutingTopology, ... % topology-selection algorithm (CALSN when true)
"tilt", tilt, "azimuth", azimuth, ... % joint sensor parameters "tilt", tilt, "azimuth", azimuth, ... % joint sensor parameters
"alphaDist", alphaDist, "betaDist", betaDist, "alphaTilt", alphaTilt, "betaTilt", betaTilt, ... % sigmoid sensor parameters "alphaDist", alphaDist, "betaDist", betaDist, "alphaTilt", alphaTilt, "betaTilt", betaTilt, ... % sigmoid sensor parameters
"lossExponent", lossExponent, "P_TX", P_TX, "BW", BW, "f_c", f_c, "G_RX_dBi", G_RX_dBi, "beamwidthExponent", beamwidthExponent, ... % RF sensor parameters "lossExponent", lossExponent, "P_TX", P_TX, "BW", BW, "f_c", f_c, "G_RX_dBi", G_RX_dBi, "beamwidthExponent", beamwidthExponent, ... % RF sensor parameters
+223 -153
View File
@@ -32,8 +32,8 @@ classdef test_miSim < matlab.unittest.TestCase
% Agents % Agents
initialStepSize = 0.2; % gradient ascent step size at the first iteration. Decreases linearly to 0 based on maxIter. initialStepSize = 0.2; % gradient ascent step size at the first iteration. Decreases linearly to 0 based on maxIter.
initialMaxAngleStepSize = 0.1; % angular step size (degrees) for tilt/azimuth gradient ascent per timestep. initialMaxAngleStepSize = 0.1; % angular step size (degrees) for tilt/azimuth gradient ascent per timestep.
minAgents = 3; % Minimum number of agents to be randomly generated minAgents = 7; % Minimum number of agents to be randomly generated
maxAgents = 4; % Maximum number of agents to be randomly generated maxAgents = 7; % Maximum number of agents to be randomly generated
useDoubleIntegrator = false; useDoubleIntegrator = false;
dampingCoeff = 2; dampingCoeff = 2;
agents = cell(0, 1); agents = cell(0, 1);
@@ -105,7 +105,7 @@ classdef test_miSim < matlab.unittest.TestCase
methods (Test) methods (Test)
% Test methods % Test methods
function miSim_initialization(tc) function miSim_initialization(tc)
% Test flag: set true to exercise the min-max-workload LP routing % Test flag: set true to exercise the SINR-aware routing
% overlay on this random scenario. Routing requires SINR comms, so % overlay on this random scenario. Routing requires SINR comms, so
% the flag switches the whole SINR stack on (agents keep their % the flag switches the whole SINR stack on (agents keep their
% default 0.1 W txPower); the permissive -30 dB threshold keeps % default 0.1 W txPower); the permissive -30 dB threshold keeps
@@ -248,7 +248,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize the simulation % Initialize the simulation
if useRoutingTopology if useRoutingTopology
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, true, -30.0, 2.0, 290, 2.4e9, 20e6, true, 0.02); tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, true, -30.0, 2.0, 290, 2.4e9, 20e6, true);
else else
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing); tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing);
end end
@@ -402,7 +402,7 @@ classdef test_miSim < matlab.unittest.TestCase
tc.testClass = tc.testClass.run(); tc.testClass = tc.testClass.run();
end end
function miSim_run(tc) function miSim_run(tc)
% Test flag: set true to exercise the min-max-workload LP routing % Test flag: set true to exercise the min-max-workload routing
% overlay on this random scenario. Routing requires SINR comms, so % overlay on this random scenario. Routing requires SINR comms, so
% the flag switches the whole SINR stack on (agents keep their % the flag switches the whole SINR stack on (agents keep their
% default 0.1 W txPower); the permissive -30 dB threshold keeps % default 0.1 W txPower); the permissive -30 dB threshold keeps
@@ -546,7 +546,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize the simulation % Initialize the simulation
if useRoutingTopology if useRoutingTopology
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, true, -30.0, 2.0, 290, 2.4e9, 20e6, true, 0.02); tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, true, -30.0, 2.0, 290, 2.4e9, 20e6, true);
else else
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing); tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing);
end end
@@ -981,129 +981,52 @@ classdef test_miSim < matlab.unittest.TestCase
% Run the simulation % Run the simulation
tc.testClass = tc.testClass.run(); tc.testClass = tc.testClass.run();
end end
function test_routing_topology_3_agents(tc) function test_routing_topology_convergence_demo(tc)
% Three near-collinear agents with the routing-LP topology % Demonstration of the Capacity-Aware Lesser Sink Neighbor
% selector. Endpoints are agents 1 and 3 (largest odd index); % (CALSN) topology + routing algorithm: four agents with
% agent 2 is the only relay, so only its unit of data transits % sigmoid sensors in a cube domain converge on a single
% the network. Agent 2 sits slightly closer to agent 1, giving % sensing objective while the communication links evolve.
% the 2->1 link strictly higher capacity than 2->3. The min-max %
% optimum is then unique: node 2's transmit workload % Agents start scattered in a quadrilateral near one corner;
% f21/c21 + f23/c23 is always the system max (endpoint receive % the objective sits in the opposite corner. Base stations are
% time is a subset of it), so ALL flow goes over the better % agents 1 and 4. As the fleet migrates and contracts around
% link. No split occurs because the receive side never binds % the objective, pairwise SINRs — and with them the Shannon
% with a single relay. % capacities, base-station potentials, parent choices, and
tc.minDimension = 10; % domain size % capacity-proportional flow splits — change every step, so
% the network-graph tile shows the maintained (dotted) and
% flow-carrying (solid, arrowed, flow-labelled) links
% re-routing dynamically. validate() enforces the connected-
% topology invariant on every step of the run.
tc.minDimension = 10; % cube domain edge length
tc.domain = tc.domain.initialize([zeros(1, 3);tc.minDimension* ones(1, 3)], REGION_TYPE.DOMAIN, "Domain"); tc.domain = tc.domain.initialize([zeros(1, 3);tc.minDimension* ones(1, 3)], REGION_TYPE.DOMAIN, "Domain");
% make basic sensing objective % Single sensing objective in the far corner
tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([2, 8; 8, 8]), tc.domain, tc.discretizationStep, tc.protectedRange); tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([7, 7]), tc.domain, tc.discretizationStep, tc.protectedRange);
% Initialize agent collision geometry. Asymmetric spacing: agent 2 % Four agents scattered (non-collinear) near the (low-x, low-y)
% is 1.8 m from agent 1 and 2.2 m from agent 3 (both above the 1 m % corner, co-altitude at z = 2. Base 1 trails the fleet and
% path-loss floor). The 1-3 link (4 m, with agent 2 interfering % base 4 leads it, with the relays staggered between: relay 3
% close-in) stays below the threshold in both directions. % starts too far from base 1 for a direct link to be worth
tc.agents = {agent; agent; agent;}; % using, so its initial basin-1 route is MULTI-HOP via relay 2
tc.collisionRanges = .25 * ones(size(tc.agents)); % and re-parents to the base directly as the fleet contracts.
dLeft = [1.8, 0, 0];
dRight = [2.2, 0, 0];
geometry1 = spherical;
geometry2 = geometry1;
geometry3 = geometry1;
geometry1 = geometry1.initialize(tc.domain.center - dLeft, tc.collisionRanges(1), REGION_TYPE.COLLISION);
geometry2 = geometry2.initialize(tc.domain.center, tc.collisionRanges(2), REGION_TYPE.COLLISION);
geometry3 = geometry3.initialize(tc.domain.center + dRight, tc.collisionRanges(3), REGION_TYPE.COLLISION);
% Initialize agent sensor model
tc.sensor = sigmoidSensor;
tc.sensor = tc.sensor.initialize(tc.minDimension / 2, 3, 15, 3);
% Initialize obstacles
tc.obstacles = {};
% Initialize agents
tc.maxIter = 50;
tc.commsRanges = 4 * ones(size(tc.agents)); % unused in SINR mode
tc.agents{1} = tc.agents{1}.initialize(tc.domain.center - dLeft, geometry1, tc.sensor, tc.commsRanges(1), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
tc.agents{2} = tc.agents{2}.initialize(tc.domain.center, geometry2, tc.sensor, tc.commsRanges(2), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
tc.agents{3} = tc.agents{3}.initialize(tc.domain.center + dRight, geometry3, tc.sensor, tc.commsRanges(3), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
% SINR communications model parameters (same threshold logic as
% the lesser-neighbor 3-agent test: a couple dB above the
% rejected end-to-end link, below the chain-link binding)
useSinrComms = true;
txPower = [0.1, 0.1, 0.1]; % homogeneous transmit power (W)
pathLossExponent = 2.0;
ambientTemp = 290; % K
centerFreq = 2.4e9; % Hz
bandwidth = 20e6; % Hz
endBindingDb = 10 * log10(0.5^pathLossExponent); % ~ -6.02 dB
sinrThreshold = endBindingDb + 2.0; % ~ -4.02 dB
useRoutingTopology = true;
routingFlowThreshold = 0.02;
tc.agents{1}.txPower = txPower(1);
tc.agents{2}.txPower = txPower(2);
tc.agents{3}.txPower = txPower(3);
% Initialize the simulation with the routing-LP topology selector
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology, routingFlowThreshold);
% Feasible (threshold) links form the 1-2-3 line; the routing LP
% then puts all of relay 2's data on the higher-capacity 2->1
% link and drops 2->3 entirely
tc.assertEqual(tc.testClass.routingAdjacencyMatrix, logical( ...
[ 0, 0, 0; ...
1, 0, 0; ...
0, 0, 0;]));
tc.assertEqual(tc.testClass.routingFlows(2, 1), 1.0, "AbsTol", 1e-6);
% Endpoints never transmit; total flow equals the relay's unit
tc.assertEqual(sum(tc.testClass.routingFlows, "all"), 1.0, "AbsTol", 1e-6);
% The CBF maintains the low-level (lesser-neighbor) connectivity
% line PLUS the routed bulk link (2->1, already part of the line):
% the network must stay connected for basic traffic even though
% bulk data only flows over 2->1
tc.assertEqual(tc.testClass.constraintAdjacencyMatrix, logical( ...
[ 1, 1, 0; ...
1, 1, 1; ...
0, 1, 1;]));
% Run the simulation
tc.testClass = tc.testClass.run();
end
function test_routing_topology_4_agents(tc)
% Four collinear agents, 2 m spacing. Endpoints are agents 1 and
% 3; relays are agents 2 and 4. Threshold-feasible links form the
% chain 1-2-3-4 (longer links are interference-limited below the
% threshold). Relay 4's only outlet is endpoint 3; relay 2 can
% reach both endpoints but 2->1 has strictly higher capacity than
% 2->3 (endpoint 3 is also loaded by relay 4's traffic), so the
% unique optimum is 2->1 and 4->3.
tc.minDimension = 10; % domain size
tc.domain = tc.domain.initialize([zeros(1, 3);tc.minDimension* ones(1, 3)], REGION_TYPE.DOMAIN, "Domain");
% make basic sensing objective
tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([2, 8; 8, 8]), tc.domain, tc.discretizationStep, tc.protectedRange);
% Initialize agent collision geometry: collinear at x = 2, 4, 6, 8
tc.agents = {agent; agent; agent; agent;}; tc.agents = {agent; agent; agent; agent;};
tc.collisionRanges = .25 * ones(size(tc.agents)); tc.collisionRanges = .25 * ones(size(tc.agents));
d = [2.0, 0, 0]; positions = [1.0, 2.0, 2; ...
positions = [tc.domain.center - 1.5 * d; ... 3.0, 3.5, 2; ...
tc.domain.center - 0.5 * d; ... 5.5, 3.0, 2; ...
tc.domain.center + 0.5 * d; ... 5.0, 5.5, 2;];
tc.domain.center + 1.5 * d;];
% Initialize agent sensor model % Initialize agent sensor model (sigmoid)
tc.sensor = sigmoidSensor; tc.sensor = sigmoidSensor;
tc.sensor = tc.sensor.initialize(tc.minDimension / 2, 3, 15, 3); tc.sensor = tc.sensor.initialize(tc.minDimension / 2, 3, 15, 3);
% Initialize obstacles % Initialize obstacles
tc.obstacles = {}; tc.obstacles = {};
% Initialize agents % Initialize agents: 50 steps is long enough to watch the
tc.maxIter = 50; % fleet converge on the objective and settle (the gradient
% step decays linearly to zero over maxIter)
tc.maxIter = 80;
tc.commsRanges = 4 * ones(size(tc.agents)); % unused in SINR mode tc.commsRanges = 4 * ones(size(tc.agents)); % unused in SINR mode
for aa = 1:4 for aa = 1:4
geometry = spherical; geometry = spherical;
@@ -1112,48 +1035,195 @@ classdef test_miSim < matlab.unittest.TestCase
tc.agents{aa}.txPower = 0.1; tc.agents{aa}.txPower = 0.1;
end end
% SINR communications model parameters % SINR communications model. The threshold must sit >= 3 dB
useSinrComms = true; % below the worst link CALSN may select, because the CBF holds
pathLossExponent = 2.0; % every maintained link at threshold + 3 dB in BOTH directions:
ambientTemp = 290; % K % a link selected inside that 3 dB band is born with a violated
centerFreq = 2.4e9; % Hz % barrier and can deadlock the QP (agents hold position, the
bandwidth = 20e6; % Hz % geometry never changes, and the same link is re-selected
endBindingDb = 10 * log10(0.5^pathLossExponent); % ~ -6.02 dB % forever). The staggered start's weakest candidate link (the
sinrThreshold = endBindingDb + 2.0; % ~ -4.02 dB % long 1-4 base-to-base diagonal) sits at ~ -9 dB and the
useRoutingTopology = true; % weakest link CALSN actually selects at ~ -4 dB, so -14 dB
routingFlowThreshold = 0.02; % leaves the -11 dB CBF floor a comfortable margin.
useSinrComms = true;
pathLossExponent = 2.0;
ambientTemp = 290; % K
centerFreq = 2.4e9; % Hz
bandwidth = 20e6; % Hz
sinrThreshold = -14.0; % dB
useRoutingTopology = true;
% Initialize the simulation with the routing-LP topology selector % Initialize the simulation with the CALSN topology selector
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology, routingFlowThreshold); tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology);
% Unique optimum: 2->1 and 4->3 % Initial routing invariants: bases at zero potential, relays
expectedDirected = false(4); % finite; maintained topology one connected component; both
expectedDirected(2, 1) = true; % generated units delivered to the bases with nothing unrouted
expectedDirected(4, 3) = true; phi = tc.testClass.routingPotentials;
tc.assertEqual(tc.testClass.routingAdjacencyMatrix, expectedDirected); tc.assertEqual(phi([1, 4]), [0; 0]);
tc.assertTrue(all(isfinite(phi([2, 3]))) && all(phi([2, 3]) > 0));
% The CBF maintains the full lesser-neighbor chain (low-level tc.assertEqual(max(conncomp(graph(tc.testClass.constraintAdjacencyMatrix))), 1);
% connectivity) with the bulk links overlaid on it
tc.assertEqual(tc.testClass.constraintAdjacencyMatrix, logical( ...
[ 1, 1, 0, 0; ...
1, 1, 1, 0; ...
0, 1, 1, 1; ...
0, 0, 1, 1;]));
% Flow conservation at every relay: out - in = 1 unit produced
F = tc.testClass.routingFlows; F = tc.testClass.routingFlows;
endpoints = [1, 3]; for aa = [2, 3]
for aa = 1:4 tc.assertEqual(sum(F(aa, :)) - sum(F(:, aa)), 1.0, "AbsTol", 1e-9);
if any(aa == endpoints) end
continue; bases = [1, 4];
tc.assertEqual(sum(F(:, bases), "all") - sum(F(bases, :), "all"), 2.0, "AbsTol", 1e-9);
tc.assertTrue(all(tc.testClass.routingUnrouted == 0));
% Run the simulation (per-step CALSN re-solve; validate()
% checks feasibility- and maintained-graph connectivity at
% every iteration)
tc.testClass = tc.testClass.run();
% Convergence: total sensing performance improved over the run
tc.assertGreaterThan(tc.testClass.performance(end), tc.testClass.performance(2));
% Dynamic evolution: the maintained topology changed at least
% once relative to the initial configuration
hist = tc.testClass.constraintAdjacencyHist;
changed = false;
for kk = 2:size(hist, 3)
if any(hist(:, :, kk) ~= hist(:, :, 1), "all")
changed = true;
break;
end end
tc.assertEqual(sum(F(aa, :)) - sum(F(:, aa)), 1.0, "AbsTol", 1e-6); end
tc.assertTrue(changed, "Maintained topology never changed during the run");
% Final routing state is still a valid delivery tree: one
% connected component, conservation at the generating UAVs,
% both units delivered, nothing unrouted
tc.assertEqual(max(conncomp(graph(tc.testClass.constraintAdjacencyMatrix))), 1);
F = tc.testClass.routingFlows;
for aa = [2, 3]
tc.assertEqual(sum(F(aa, :)) - sum(F(:, aa)), 1.0, "AbsTol", 1e-9);
end
tc.assertEqual(sum(F(:, bases), "all") - sum(F(bases, :), "all"), 2.0, "AbsTol", 1e-9);
tc.assertTrue(all(tc.testClass.routingUnrouted == 0));
end
function test_routing_topology_convergence_demo_rfSensor(tc)
% Demonstration of the Capacity-Aware Lesser Sink Neighbor
% (CALSN) topology + routing algorithm: four agents with
% sigmoid sensors in a cube domain converge on a single
% sensing objective while the communication links evolve.
%
% Agents start scattered in a quadrilateral near one corner;
% the objective sits in the opposite corner. Base stations are
% agents 1 and 4. As the fleet migrates and contracts around
% the objective, pairwise SINRs — and with them the Shannon
% capacities, base-station potentials, parent choices, and
% capacity-proportional flow splits — change every step, so
% the network-graph tile shows the maintained (dotted) and
% flow-carrying (solid, arrowed, flow-labelled) links
% re-routing dynamically. validate() enforces the connected-
% topology invariant on every step of the run.
tc.minDimension = 10; % cube domain edge length
tc.domain = tc.domain.initialize([zeros(1, 3);tc.minDimension* ones(1, 3)], REGION_TYPE.DOMAIN, "Domain");
% Single sensing objective in the far corner
tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([7, 7]), tc.domain, tc.discretizationStep, tc.protectedRange);
% Four agents scattered (non-collinear) near the (low-x, low-y)
% corner, co-altitude at z = 2. Base 1 trails the fleet and
% base 4 leads it, with the relays staggered between: relay 3
% starts too far from base 1 for a direct link to be worth
% using, so its initial basin-1 route is MULTI-HOP via relay 2
% and re-parents to the base directly as the fleet contracts.
tc.agents = {agent; agent; agent; agent;};
tc.collisionRanges = .25 * ones(size(tc.agents));
positions = [1.0, 2.0, 2; ...
3.0, 3.5, 2; ...
5.5, 3.0, 2; ...
5.0, 5.5, 2;];
% Initialize agent sensor model (sigmoid)
tc.sensor = rfSensor;
tilt = 0; azimuth = 0;
tc.sensor = tc.sensor.initialize(tc.P_TX * 1 + rand * 4, tc.T_0, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent + randi(100), tilt, azimuth, tc.lossExponent);
% Initialize obstacles
tc.obstacles = {};
% Initialize agents: 50 steps is long enough to watch the
% fleet converge on the objective and settle (the gradient
% step decays linearly to zero over maxIter)
tc.maxIter = 80;
tc.commsRanges = 4 * ones(size(tc.agents)); % unused in SINR mode
for aa = 1:4
geometry = spherical;
geometry = geometry.initialize(positions(aa, :), tc.collisionRanges(aa), REGION_TYPE.COLLISION);
tc.agents{aa} = tc.agents{aa}.initialize(positions(aa, :), geometry, tc.sensor, tc.commsRanges(aa), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
tc.agents{aa}.txPower = 0.1;
end end
% Every surviving link carries at least the pruning threshold, % SINR communications model. The threshold must sit >= 3 dB
% and total delivered data equals the number of relays % below the worst link CALSN may select, because the CBF holds
tc.assertTrue(all(F(F > 0) >= tc.testClass.routingFlowThreshold)); % every maintained link at threshold + 3 dB in BOTH directions:
tc.assertEqual(sum(F(:, endpoints), "all") - sum(F(endpoints, :), "all"), 2.0, "AbsTol", 1e-6); % a link selected inside that 3 dB band is born with a violated
% barrier and can deadlock the QP (agents hold position, the
% geometry never changes, and the same link is re-selected
% forever). The staggered start's weakest candidate link (the
% long 1-4 base-to-base diagonal) sits at ~ -9 dB and the
% weakest link CALSN actually selects at ~ -4 dB, so -14 dB
% leaves the -11 dB CBF floor a comfortable margin.
useSinrComms = true;
pathLossExponent = 2.0;
ambientTemp = 290; % K
centerFreq = 2.4e9; % Hz
bandwidth = 20e6; % Hz
sinrThreshold = -14.0; % dB
useRoutingTopology = true;
% Initialize the simulation with the CALSN topology selector
tc.optimizeSensorPointing = true;
tc.testClass = tc.testClass.initialize(tc.domain, tc.agents, tc.barrierGain, tc.barrierExponent, tc.minAlt, tc.timestep, tc.maxIter, tc.obstacles, tc.makePlots, tc.makeVideo, tc.useDoubleIntegrator, tc.dampingCoeff, tc.useFixedTopology, tc.optimizeSensorPointing, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, useRoutingTopology);
% Initial routing invariants: bases at zero potential, relays
% finite; maintained topology one connected component; both
% generated units delivered to the bases with nothing unrouted
phi = tc.testClass.routingPotentials;
tc.assertEqual(phi([1, 4]), [0; 0]);
tc.assertTrue(all(isfinite(phi([2, 3]))) && all(phi([2, 3]) > 0));
tc.assertEqual(max(conncomp(graph(tc.testClass.constraintAdjacencyMatrix))), 1);
F = tc.testClass.routingFlows;
for aa = [2, 3]
tc.assertEqual(sum(F(aa, :)) - sum(F(:, aa)), 1.0, "AbsTol", 1e-9);
end
bases = [1, 4];
tc.assertEqual(sum(F(:, bases), "all") - sum(F(bases, :), "all"), 2.0, "AbsTol", 1e-9);
tc.assertTrue(all(tc.testClass.routingUnrouted == 0));
% Run the simulation (per-step CALSN re-solve; validate()
% checks feasibility- and maintained-graph connectivity at
% every iteration)
tc.testClass = tc.testClass.run();
% Convergence: total sensing performance improved over the run
tc.assertGreaterThan(tc.testClass.performance(end), tc.testClass.performance(2));
% Dynamic evolution: the maintained topology changed at least
% once relative to the initial configuration
hist = tc.testClass.constraintAdjacencyHist;
changed = false;
for kk = 2:size(hist, 3)
if any(hist(:, :, kk) ~= hist(:, :, 1), "all")
changed = true;
break;
end
end
tc.assertTrue(changed, "Maintained topology never changed during the run");
% Final routing state is still a valid delivery tree: one
% connected component, conservation at the generating UAVs,
% both units delivered, nothing unrouted
tc.assertEqual(max(conncomp(graph(tc.testClass.constraintAdjacencyMatrix))), 1);
F = tc.testClass.routingFlows;
for aa = [2, 3]
tc.assertEqual(sum(F(aa, :)) - sum(F(:, aa)), 1.0, "AbsTol", 1e-9);
end
tc.assertEqual(sum(F(:, bases), "all") - sum(F(bases, :), "all"), 2.0, "AbsTol", 1e-9);
tc.assertTrue(all(tc.testClass.routingUnrouted == 0));
end end
function test_communications_constraint_fixed_radius(tc) function test_communications_constraint_fixed_radius(tc)
% No obstacles % No obstacles