36 Commits
Author SHA1 Message Date
kdee 1e335d33be basin extension replaced 2026-10-11 16:39:46 -07:00
kdee a55798c599 centralized thesis figure plotting scripts 2026-10-08 23:58:09 -07:00
kdee dd5a0fd103 legend cleanup 2026-10-08 21:41:17 -07:00
kdee eed8b7d431 added thesis plot antenna gain patterns 2026-10-08 18:44:10 -07:00
kdee 3efec27b57 project fix 2026-10-07 23:22:41 -07:00
kdee 742b09a459 thesis figure plotting uniformity 2026-10-07 23:22:16 -07:00
kdee cfc0ed4748 improved sigmoid sensor parameter plotting for thesis figure 2026-10-07 22:26:39 -07:00
kdee acfb2719e2 new routing algorithm 2026-07-22 17:37:04 -07:00
kdee 37598479cc better routing solution 2026-07-12 22:50:14 -07:00
kdee 56cc4fced5 added routing logic and network graph improvements 2026-07-05 22:43:26 -07:00
kdee 1d414e2bd4 made SINR threshold check bidirectional 2026-07-05 20:54:59 -07:00
kdee f9aa9cc44d network plotting cleanup 2026-06-30 23:01:58 -07:00
kdee efa251d747 new SINR threshold comms CBF constraint 2026-06-26 22:13:51 -07:00
kdee b53f62391b edit param default 2026-06-05 20:16:46 -07:00
kdee ec0a223cf8 added simHist save to analysis runs 2026-06-02 21:37:45 -07:00
kdee 1479892014 stagger takeoff/landing for increased spacing 2026-06-02 20:43:56 -07:00
kdee 0adaf0b350 plotting hack 2026-06-02 10:13:16 -07:00
kdee f97ed8bc23 better AERPAW collision analysis 2026-06-02 10:13:16 -07:00
kdee 0f43c3fe44 more AERPAW analysis 2026-06-02 08:50:17 -07:00
kdee 8307ea25cc FSPL efficiency improvement 2026-05-29 20:00:33 -07:00
kdee 472cbdb85d test cleanup 2026-05-29 19:45:59 -07:00
kdee 2f9f13fdc8 rf sensor model cleanup 2026-05-29 19:38:51 -07:00
kdee 8e8bb39cd9 added support for 2 objectives in analysis script 2026-05-28 17:50:12 -07:00
kdee 283266c2a8 codegen fix 2026-05-25 15:48:51 -07:00
kdee e0b23bb94e columns scenario first 2026-05-25 15:48:35 -07:00
kdee 1c9c83f7e5 ran 3 new experiments on AERPAW simulation to test before submitting 2026-05-25 15:48:35 -07:00
kdee 3a74169a9c fixed multi-objective support, collected sim data on 3 new experiments 2026-05-25 15:48:11 -07:00
kdee a655f58e9d Merge pull request #18 from krdee1/revert-17-AERPAW-experiments
Revert "AERPAW experiment updates to be brought into main"
2026-05-25 15:27:44 -07:00
kdee 8373fa8740 Revert "AERPAW experiment updates to be brought into main" 2026-05-25 15:27:31 -07:00
kdee d1940fd83f Merge pull request #17 from krdee1/AERPAW-experiments
AERPAW experiment updates to be brought into main
2026-05-25 15:24:16 -07:00
kdee 5b9b196e60 Merge branch 'main' into AERPAW-experiments 2026-05-25 15:10:12 -07:00
kdee 85d2570c38 codegen fix 2026-05-25 14:56:14 -07:00
kdee 4eefe1b9ac added 2 objective support for AERPAW 2026-05-25 14:53:03 -07:00
kdee c9774622f2 columns scenario first 2026-05-25 14:09:14 -07:00
kdee a30dfe6be7 ran 3 new experiments on AERPAW simulation to test before submitting 2026-05-25 14:07:49 -07:00
kdee 9c65bf7880 fixed multi-objective support, collected sim data on 3 new experiments 2026-05-24 20:28:12 -07:00
49 changed files with 2323 additions and 614 deletions

No files matched your search

+1
View File
@@ -4,6 +4,7 @@
*.autosave *.autosave
*.slx.r* *.slx.r*
*.mdl.r* *.mdl.r*
*.bak
# Derived content-obscured files # Derived content-obscured files
*.p *.p
+3 -2
View File
@@ -19,7 +19,8 @@ classdef agent
fovGeometry; fovGeometry;
% Communication % Communication
commsGeometry; commsGeometry; % fixed-radius comms model (spherical, radius = comRange)
txPower = 0.1; % transmit power (W) for the SINR comms model (isotropic)
lesserNeighbors = zeros(1, 0); lesserNeighbors = zeros(1, 0);
% Performance % Performance
@@ -49,7 +50,7 @@ classdef agent
obj.fovGeometry = cone; obj.fovGeometry = cone;
obj.commsGeometry = spherical; obj.commsGeometry = spherical;
end end
[obj] = initialize(obj, pos, pan, tilt, collisionGeometry, sensorModel, guidanceModel, comRange, index, label); [obj] = initialize(obj, pos, collisionGeometry, sensorModel, comRange, maxIter, initialStepSize, initialMaxAngleStepSize, label, plotCommsGeometry, txPower);
[obj] = run(obj, domain, partitioning, t, index, useDoubleIntegrator, dampingCoeff, dt, optimizeSensorPointing, otherAgents); [obj] = run(obj, domain, partitioning, t, index, useDoubleIntegrator, dampingCoeff, dt, optimizeSensorPointing, otherAgents);
[partitioning, agents] = partition(obj, agents, objective) [partitioning, agents] = partition(obj, agents, objective)
[obj, f] = plot(obj, ind, f); [obj, f] = plot(obj, ind, f);
+3 -1
View File
@@ -1,4 +1,4 @@
function obj = initialize(obj, pos, collisionGeometry, sensorModel, comRange, maxIter, initialStepSize, initialMaxAngleStepSize, label, plotCommsGeometry) function obj = initialize(obj, pos, collisionGeometry, sensorModel, comRange, maxIter, initialStepSize, initialMaxAngleStepSize, label, plotCommsGeometry, txPower)
arguments (Input) arguments (Input)
obj (1, 1) {mustBeA(obj, "agent")}; obj (1, 1) {mustBeA(obj, "agent")};
pos (1, 3) double; pos (1, 3) double;
@@ -10,6 +10,7 @@ function obj = initialize(obj, pos, collisionGeometry, sensorModel, comRange, ma
initialMaxAngleStepSize (1, 1) double = 5.0; initialMaxAngleStepSize (1, 1) double = 5.0;
label (1, 1) string = ""; label (1, 1) string = "";
plotCommsGeometry (1, 1) logical = false; plotCommsGeometry (1, 1) logical = false;
txPower (1, 1) double = 0.1;
end end
arguments (Output) arguments (Output)
obj (1, 1) {mustBeA(obj, "agent")}; obj (1, 1) {mustBeA(obj, "agent")};
@@ -25,6 +26,7 @@ function obj = initialize(obj, pos, collisionGeometry, sensorModel, comRange, ma
obj.plotCommsGeometry = plotCommsGeometry; obj.plotCommsGeometry = plotCommsGeometry;
obj.initialStepSize = initialStepSize; obj.initialStepSize = initialStepSize;
obj.initialMaxAngleStepSize = initialMaxAngleStepSize; obj.initialMaxAngleStepSize = initialMaxAngleStepSize;
obj.txPower = txPower;
obj.stepDecayRate = obj.initialStepSize / maxIter; obj.stepDecayRate = obj.initialStepSize / maxIter;
obj.angleStepDecayRate = obj.initialMaxAngleStepSize / maxIter; obj.angleStepDecayRate = obj.initialMaxAngleStepSize / maxIter;
+82 -19
View File
@@ -141,33 +141,96 @@ function [obj] = constrainMotion(obj)
% Add communication network constraints % Add communication network constraints
hComms = NaN(nAgents, nAgents); hComms = NaN(nAgents, nAgents);
hComms(logical(eye(nAgents))) = 0; hComms(logical(eye(nAgents))) = 0;
for ii = 1:(nAgents - 1) if ~obj.useSinrComms
for jj = (ii + 1):nAgents % Fixed-radius comms: keep each maintained pair within the smaller of
if obj.constraintAdjacencyMatrix(ii, jj) % the two comms radii via the barrier h = r_comms^2 - dist^2.
paddingFactor = 0.9; % Barrier at 90% of actual range; real comms still work beyond this for ii = 1:(nAgents - 1)
r_comms = paddingFactor * min([obj.agents{ii}.commsGeometry.radius, obj.agents{jj}.commsGeometry.radius]); for jj = (ii + 1):nAgents
hComms(ii, jj) = r_comms^2 - norm(obj.agents{ii}.lastPos - obj.agents{jj}.lastPos)^2; if obj.constraintAdjacencyMatrix(ii, jj)
paddingFactor = 0.9; % Barrier at 90% of actual range; real comms still work beyond this
r_comms = paddingFactor * min([obj.agents{ii}.commsGeometry.radius, obj.agents{jj}.commsGeometry.radius]);
hComms(ii, jj) = r_comms^2 - norm(obj.agents{ii}.lastPos - obj.agents{jj}.lastPos)^2;
A(kk, (3 * ii - 2):(3 * ii)) = 2 * (obj.agents{ii}.lastPos - obj.agents{jj}.lastPos); A(kk, (3 * ii - 2):(3 * ii)) = 2 * (obj.agents{ii}.lastPos - obj.agents{jj}.lastPos);
A(kk, (3 * jj - 2):(3 * jj)) = -A(kk, (3 * ii - 2):(3 * ii)); A(kk, (3 * jj - 2):(3 * jj)) = -A(kk, (3 * ii - 2):(3 * ii));
% One-step forward invariance: b = h/dt ensures h cannot % One-step forward invariance: b = h/dt ensures h cannot
% go negative in a single timestep (linear approximation) % go negative in a single timestep (linear approximation)
v_max_ij = max(obj.agents{ii}.initialStepSize, obj.agents{jj}.initialStepSize) / obj.timestep; v_max_ij = max(obj.agents{ii}.initialStepSize, obj.agents{jj}.initialStepSize) / obj.timestep;
hMin = -4 * r_comms * v_max_ij * obj.timestep; hMin = -4 * r_comms * v_max_ij * obj.timestep;
if norm(A(kk, :)) < 1e-9 if norm(A(kk, :)) < 1e-9
b(kk) = 0; b(kk) = 0;
else else
b(kk) = max(hMin, hComms(ii, jj)) / obj.timestep; b(kk) = max(hMin, hComms(ii, jj)) / obj.timestep;
end
kk = kk + 1;
end end
end
end
else
% SINR comms: a link is feasible only if each agent receives the other
% above threshold, so every maintained pair is held with TWO barrier
% rows, h = SINR - gammaCbf, one per receiver direction. Each gradient
% couples the receiver, transmitter, and every interferer, so the QP
% rows are dense across all agent blocks.
% Maintain SINR a margin above the connectivity threshold so that
% discrete-time overshoot of the (convex) SINR does not transiently
% drop a maintained link below threshold. Mirrors the fixed-radius
% barrier's 0.9 padding: the CBF holds SINR >= gammaCbf > gammaLin,
% while connectivity (updateAdjacency) still uses gammaLin.
paddingDb = 3.0;
gammaCbf = 10^((obj.sinrThreshold + paddingDb) / 10);
lastPositions = zeros(nAgents, 3);
for ll = 1:nAgents
lastPositions(ll, :) = obj.agents{ll}.lastPos;
end
for ii = 1:(nAgents - 1)
for jj = (ii + 1):nAgents
if obj.constraintAdjacencyMatrix(ii, jj)
% Direction 1: lower-index agent (ii) receives higher (jj).
% Upper triangle of hComms stores this direction's barrier.
[sinr1, grad1] = obj.sinrLink(lastPositions, ii, jj);
hComms(ii, jj) = sinr1 - gammaCbf;
% CBF row: A = -grad(h), b = h/dt (one-step forward invariance)
for aa = 1:nAgents
A(kk, (3 * aa - 2):(3 * aa)) = -grad1(aa, :);
end
if norm(A(kk, :)) < 1e-9
b(kk) = 0;
else
b(kk) = hComms(ii, jj) / obj.timestep;
end
kk = kk + 1;
kk = kk + 1; % Direction 2: higher-index agent (jj) receives lower (ii).
% Lower triangle of hComms stores this direction's barrier.
[sinr2, grad2] = obj.sinrLink(lastPositions, jj, ii);
hComms(jj, ii) = sinr2 - gammaCbf;
for aa = 1:nAgents
A(kk, (3 * aa - 2):(3 * aa)) = -grad2(aa, :);
end
if norm(A(kk, :)) < 1e-9
b(kk) = 0;
else
b(kk) = hComms(jj, ii) / obj.timestep;
end
kk = kk + 1;
end
end end
end end
end end
if coder.target('MATLAB') if coder.target('MATLAB')
obj.barriers(idx:(idx + length(hComms(triu(true(size(hComms)), 1))) - 1), obj.timestepIndex) = hComms(triu(true(size(hComms)), 1)); if obj.useSinrComms
% Both directions are maintained: log the upper triangle
% (lower-index receiver) then the lower triangle (higher-index
% receiver), matching the doubled comms-barrier allocation.
commsLog = [hComms(triu(true(size(hComms)), 1)); hComms(tril(true(size(hComms)), -1))];
else
commsLog = hComms(triu(true(size(hComms)), 1));
end
obj.barriers(idx:(idx + numel(commsLog) - 1), obj.timestepIndex) = commsLog;
end end
% Double-integrator: transform QP from velocity to acceleration space. % Double-integrator: transform QP from velocity to acceleration space.
+77 -13
View File
@@ -1,4 +1,4 @@
function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, minAlt, timestep, maxIter, obstacles, makePlots, makeVideo, useDoubleIntegrator, dampingCoeff, useFixedTopology, optimizeSensorPointing) 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};
@@ -15,6 +15,13 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
dampingCoeff (1, 1) double = 2.0; dampingCoeff (1, 1) double = 2.0;
useFixedTopology (1, 1) logical = false; useFixedTopology (1, 1) logical = false;
optimizeSensorPointing (1, 1) logical = false; optimizeSensorPointing (1, 1) logical = false;
useSinrComms (1, 1) logical = false;
sinrThreshold (1, 1) double = 0;
pathLossExponent (1, 1) double = 2.0;
ambientTemp (1, 1) double = 290.0;
centerFreq (1, 1) double = 2.4e9;
bandwidth (1, 1) double = 20e6;
useRoutingTopology (1, 1) logical = false;
end end
arguments (Output) arguments (Output)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
@@ -72,16 +79,18 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
obj.constraintAdjacencyMatrix = logical(eye(size(agents, 1))); obj.constraintAdjacencyMatrix = logical(eye(size(agents, 1)));
% Set labels for agents and collision geometries in cases where they % Set labels for agents and collision geometries in cases where they
% were not provieded at the time of their initialization % were not provieded at the time of their initialization.
for ii = 1:size(obj.agents, 1) % Guarded by coder.target: label is a fixed-size "" in codegen, and
% Agent % sprintf returns variable-length — incompatible even in a dead branch.
if isempty(char(obj.agents{ii}.label)) % On the compiled path agents always receive explicit labels from the caller.
obj.agents{ii}.label = sprintf("Agent %d", int8(ii)); if coder.target('MATLAB')
end for ii = 1:size(obj.agents, 1)
if isempty(char(obj.agents{ii}.label))
% Collision geometry obj.agents{ii}.label = sprintf("Agent %d", int8(ii));
if isempty(char(obj.agents{ii}.collisionGeometry.label)) end
obj.agents{ii}.collisionGeometry.label = sprintf("Agent %d Collision Geometry", int8(ii)); if isempty(char(obj.agents{ii}.collisionGeometry.label))
obj.agents{ii}.collisionGeometry.label = sprintf("Agent %d Collision Geometry", int8(ii));
end
end end
end end
@@ -96,14 +105,60 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
obj.useFixedTopology = useFixedTopology; obj.useFixedTopology = useFixedTopology;
obj.optimizeSensorPointing = optimizeSensorPointing; obj.optimizeSensorPointing = optimizeSensorPointing;
% Compute adjacency matrix and network topology % Set communications model and SINR parameters
obj.useSinrComms = useSinrComms;
obj.sinrThreshold = sinrThreshold;
obj.pathLossExponent = pathLossExponent;
obj.ambientTemp = ambientTemp;
obj.centerFreq = centerFreq;
obj.bandwidth = bandwidth;
% 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;
% Compute adjacency matrix and network topology. The Capacity-Aware
% Lesser Sink Neighbor algorithm replaces lesser-neighbor when selected
% (MATLAB sim only; the compiled path never sets the flag).
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();
end end
% 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
% Set up times to iterate over % Set up times to iterate over
obj.times = linspace(0, obj.timestep * obj.maxIter, obj.maxIter+1)'; obj.times = linspace(0, obj.timestep * obj.maxIter, obj.maxIter+1)';
@@ -124,7 +179,16 @@ function [obj] = initialize(obj, domain, agents, barrierGain, barrierExponent, m
end end
nAOPairs = size(obj.agents, 1) * size(obj.obstacles, 1); % unique agent/obstacle pairs nAOPairs = size(obj.agents, 1) * size(obj.obstacles, 1); % unique agent/obstacle pairs
nADPairs = size(obj.agents, 1) * 6; % agents x (4 walls + 1 floor + 1 ceiling) nADPairs = size(obj.agents, 1) * 6; % agents x (4 walls + 1 floor + 1 ceiling)
nLNAPairs = sum(triu(obj.constraintAdjacencyMatrix, 1), "all"); % Comms barriers: size to the upper bound (full mesh) rather than the initial
% constraint-adjacency count. Connectivity is dynamic (lesser-neighbor and,
% especially, SINR) and can later maintain more pairs than at init; sizing to
% nAAPairs prevents the QP A/b matrices from overflowing mid-run. SINR mode
% enforces both directions per link, so it needs two barrier rows per pair.
if obj.useSinrComms
nLNAPairs = 2 * nAAPairs;
else
nLNAPairs = nAAPairs;
end
obj.numBarriers = nAAPairs + nAOPairs + nADPairs + nLNAPairs; obj.numBarriers = nAAPairs + nAOPairs + nADPairs + nLNAPairs;
if coder.target('MATLAB') if coder.target('MATLAB')
+51 -1
View File
@@ -107,6 +107,53 @@ else
USE_FIXED_TOPOLOGY = false; USE_FIXED_TOPOLOGY = false;
end end
% SINR communications model (optional columns — backward compatible with
% older CSVs that predate the SINR model; defaults keep the fixed-radius path)
if isfield(scenario, 'txPower')
TX_POWER_VEC = scenario.txPower; % 1×N (Watts)
assert(numel(TX_POWER_VEC) == numAgents, ...
"txPower has %d values but expected %d (one per UAV)", numel(TX_POWER_VEC), numAgents);
else
TX_POWER_VEC = 0.1 * ones(1, numAgents);
end
if isfield(scenario, 'useSinrComms')
USE_SINR_COMMS = logical(scenario.useSinrComms);
else
USE_SINR_COMMS = false;
end
if isfield(scenario, 'sinrThreshold')
SINR_THRESHOLD = scenario.sinrThreshold; % dB
else
SINR_THRESHOLD = 0;
end
if isfield(scenario, 'pathLossExponent')
PATH_LOSS_EXPONENT = scenario.pathLossExponent;
else
PATH_LOSS_EXPONENT = 2.0;
end
if isfield(scenario, 'ambientTemp')
AMBIENT_TEMP = scenario.ambientTemp; % Kelvin
else
AMBIENT_TEMP = 290.0;
end
if isfield(scenario, 'centerFreq')
CENTER_FREQ = scenario.centerFreq; % Hz
else
CENTER_FREQ = 2.4e9;
end
if isfield(scenario, 'bandwidth')
BANDWIDTH = scenario.bandwidth; % Hz
else
BANDWIDTH = 20e6;
end
% Routing topology selection (optional column; MATLAB sim only)
if isfield(scenario, 'useRoutingTopology')
USE_ROUTING_TOPOLOGY = logical(scenario.useRoutingTopology);
else
USE_ROUTING_TOPOLOGY = false;
end
% ---- Build domain -------------------------------------------------------- % ---- Build domain --------------------------------------------------------
dom = rectangularPrism; dom = rectangularPrism;
dom = dom.initialize([DOMAIN_MIN; DOMAIN_MAX], REGION_TYPE.DOMAIN, "Guidance Domain"); dom = dom.initialize([DOMAIN_MIN; DOMAIN_MAX], REGION_TYPE.DOMAIN, "Guidance Domain");
@@ -134,6 +181,7 @@ for ii = 1:numAgents
ag = agent; ag = agent;
ag = ag.initialize(pos, geom, sensor, COMMS_RANGE_VEC(ii), MAX_ITER, ... ag = ag.initialize(pos, geom, sensor, COMMS_RANGE_VEC(ii), MAX_ITER, ...
INITIAL_STEP_SIZE, sprintf("UAV %d", ii)); INITIAL_STEP_SIZE, sprintf("UAV %d", ii));
ag.txPower = TX_POWER_VEC(ii); % SINR comms model transmit power (W)
agentList{ii} = ag; agentList{ii} = ag;
end end
@@ -153,6 +201,8 @@ end
% ---- Initialise simulation (plots and video disabled) -------------------- % ---- Initialise simulation (plots and video disabled) --------------------
obj = obj.initialize(dom, agentList, BARRIER_GAIN, BARRIER_EXPONENT, ... 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); USE_DOUBLE_INTEGRATOR, DAMPING_COEFF, USE_FIXED_TOPOLOGY, false, ...
USE_SINR_COMMS, SINR_THRESHOLD, PATH_LOSS_EXPONENT, AMBIENT_TEMP, CENTER_FREQ, BANDWIDTH, ...
USE_ROUTING_TOPOLOGY);
end end
+41 -1
View File
@@ -47,6 +47,9 @@ for ii = 1:numAgents
ag = agent; ag = agent;
ag = ag.initialize(pos, geom, sensor, inits.comRange(ii), inits.maxIter, ... ag = ag.initialize(pos, geom, sensor, inits.comRange(ii), inits.maxIter, ...
inits.initialStepSize(ii), sprintf("UAV %d", ii)); inits.initialStepSize(ii), sprintf("UAV %d", ii));
if isfield(inits, 'txPower')
ag.txPower = inits.txPower(ii); % SINR comms model transmit power (W)
end
agentList{ii} = ag; agentList{ii} = ag;
end end
@@ -78,10 +81,47 @@ if isfield(inits, 'useFixedTopology')
else else
useFixedTopology = false; useFixedTopology = false;
end end
if isfield(inits, 'useSinrComms')
useSinrComms = logical(inits.useSinrComms);
else
useSinrComms = false;
end
if isfield(inits, 'sinrThreshold')
sinrThreshold = inits.sinrThreshold;
else
sinrThreshold = 0;
end
if isfield(inits, 'pathLossExponent')
pathLossExponent = inits.pathLossExponent;
else
pathLossExponent = 2.0;
end
if isfield(inits, 'ambientTemp')
ambientTemp = inits.ambientTemp;
else
ambientTemp = 290.0;
end
if isfield(inits, 'centerFreq')
centerFreq = inits.centerFreq;
else
centerFreq = 2.4e9;
end
if isfield(inits, 'bandwidth')
bandwidth = inits.bandwidth;
else
bandwidth = 20e6;
end
if isfield(inits, 'useRoutingTopology')
useRoutingTopology = logical(inits.useRoutingTopology);
else
useRoutingTopology = false;
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, useDoubleIntegrator, dampingCoeff, useFixedTopology, false, ...
useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth, ...
useRoutingTopology);
end end
+327
View File
@@ -0,0 +1,327 @@
function obj = lesserSinkNeighbor(obj)
% LESSERSINKNEIGHBOR Capacity-Aware Lesser Sink Neighbor algorithm (CALSN).
%
% Implements the algorithm given in the CALSN algorithm description document
% (ADD; kept outside the repository) 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 ADD's strict inequality. It makes (phi, index) a total order,
% so every feasible link between two non-base UAVs has exactly one
% deciding endpoint (the uphill one).
% 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).
% 4. Connectivity bridge (documented deviation; not in the ADD). Every UAV
% must stay connected to the whole fleet to exchange positions for
% guidance, so A_control must be one connected graph. LNA's grouping
% argument guarantees that whenever every feasible link has a deciding
% endpoint. The one exception is the base-to-base link: both bases have
% phi = 0 and select no parents, so nobody maintains it, yet it still
% merges groups when other UAVs form their components. Steps 1-3
% therefore leave A_control either connected or split into exactly two
% pieces, one containing each base. In the split case one bridge is
% added: of all feasible links between the two pieces (base-to-base
% included), the one whose weaker direction has the highest SINR, i.e.
% the most margin under the comms barrier. A warning is issued if even
% that link is inside the CBF maintenance band (sinrThreshold + 3 dB).
% The bridge is maintained for connectivity only and carries no flow,
% so when present it is the one maintained link outside the ADD's
% invariant A_control(i,j) = 1, i ~= j, iff F(i,j) > 0 or F(j,i) > 0.
% 5. 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 SINRs and Shannon link capacities over the current
% communication neighborhoods: S(i, j) and C(i, j) describe sending
% from i to j (receiver j)
S = zeros(nAgents);
C = zeros(nAgents);
for ii = 1:nAgents
for jj = 1:nAgents
if jj ~= ii && obj.adjacency(ii, jj)
S(ii, jj) = obj.sinrLink(positions, jj, ii);
C(ii, jj) = obj.bandwidth * log2(1 + S(ii, jj));
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
% ---- Parent selection (one per lesser-sink component) ---------------
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, 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;
bestMetric = inf;
bestParent = 0;
for cc = 1:numel(component)
jj = lsn(component(cc));
metric = phi(jj) + 1 / (C(ii, jj) + EPS_CAP);
if metric < bestMetric
bestMetric = metric;
bestParent = jj;
end
end
isParent(ii, bestParent) = true;
Acontrol(ii, bestParent) = true;
Acontrol(bestParent, ii) = true;
end
end
% ---- Connectivity bridge ---------------------------------------------
% The unmaintained base-to-base link is the only way steps 1-3 can
% leave A_control split, and then into exactly two pieces, one per
% base. Bridge them with the feasible cross link whose weaker direction
% has the highest SINR. If the pieces are not joined by any feasible
% link, the feasibility graph itself is disconnected; validate()
% reports that case.
pieceOfBase1 = false(1, nAgents);
pieceOfBase1(bfs(Acontrol, 1)) = true;
if ~pieceOfBase1(nAgents)
bestSinr = -inf;
bestU = 0;
bestV = 0;
for uu = find(pieceOfBase1)
for vv = find(~pieceOfBase1)
if obj.adjacency(uu, vv)
sinrBinding = min(S(uu, vv), S(vv, uu));
if sinrBinding > bestSinr
bestSinr = sinrBinding;
bestU = uu;
bestV = vv;
end
end
end
end
if bestU > 0
Acontrol(bestU, bestV) = true;
Acontrol(bestV, bestU) = true;
% CBF maintenance band: constrainMotion holds maintained links
% at sinrThreshold + 3 dB (paddingDb; keep in sync)
if bestSinr < 10^((obj.sinrThreshold + 3.0) / 10)
warning("miSim:bridgeInCbfBand", ...
"Connectivity bridge %d-%d has SINR %.1f dB, inside the CBF maintenance band (sinrThreshold %.1f dB + 3 dB)", ...
bestU, bestV, 10 * log10(bestSinr), obj.sinrThreshold);
end
end
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
+24 -1
View File
@@ -21,6 +21,27 @@ classdef miSim
dampingCoeff = 2.0; % velocity-proportional damping for double-integrator mode dampingCoeff = 2.0; % velocity-proportional damping for double-integrator mode
useFixedTopology = false; % false = lesser neighbor (dynamic), true = fixed initial topology useFixedTopology = false; % false = lesser neighbor (dynamic), true = fixed initial topology
optimizeSensorPointing = false; % false = fixed sensor tilt/azimuth, true = optimize tilt/azimuth via gradient ascent optimizeSensorPointing = false; % false = fixed sensor tilt/azimuth, true = optimize tilt/azimuth via gradient ascent
% Communications model selection and SINR-model parameters.
% useSinrComms = false -> fixed-radius comms (comRange / commsGeometry).
% useSinrComms = true -> SINR/path-loss comms (txPower + params below).
useSinrComms = false;
sinrThreshold = 0; % SINR threshold for connectivity & CBF maintenance (dB)
pathLossExponent = 2.0; % path-loss exponent n in S = P / (K * d^n)
ambientTemp = 290.0; % ambient temperature (K) for thermal noise N = k_B*T*B
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
% Topology-selection algorithm (requires useSinrComms; MATLAB sim only).
% false -> lesser-neighbor; true -> Capacity-Aware Lesser Sink Neighbor
% (CALSN, see the algorithm description document, ADD): base-station
% potentials 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;
routingAdjacencyMatrix = false(0, 0); % directed flow links (i, j) = child i sends to parent j (F > 0)
routingFlows = zeros(0, 0); % F: flow on each directed link (units of r = 1 per UAV per step)
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
@@ -70,13 +91,15 @@ 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); [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] = lesserSinkNeighbor(obj);
[obj] = constrainMotion(obj); [obj] = constrainMotion(obj);
[sinr, grad] = sinrLink(obj, positions, rxIdx, txIdx);
[obj] = partition(obj); [obj] = partition(obj);
[obj] = updateAdjacency(obj); [obj] = updateAdjacency(obj);
[obj] = plot(obj); [obj] = plot(obj);
+72 -13
View File
@@ -6,23 +6,82 @@ function obj = plotGraph(obj)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
end end
% Form graph from adjacency matrix % The maintained links are always drawn as a dotted undirected graph.
% In routing (CALSN) mode the flow-carrying links are overlaid as SOLID
% DIRECTED edges (arrow = child -> parent, toward a base station) on
% the same node layout.
G = graph(obj.constraintAdjacencyMatrix, "omitselfloops"); G = graph(obj.constraintAdjacencyMatrix, "omitselfloops");
% Plot graph object % Resolve the target axes for the network graph tile
if isnan(obj.networkGraphIndex) if isnan(obj.networkGraphIndex)
hold(obj.f.CurrentAxes, "on"); ax = obj.f.CurrentAxes;
o = plot(obj.f.CurrentAxes, G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2);
hold(obj.f.CurrentAxes, "off");
else else
hold(obj.f.Children(1).Children(obj.networkGraphIndex(1)), "on"); ax = obj.f.Children(1).Children(obj.networkGraphIndex(1));
o = plot(obj.f.Children(1).Children(obj.networkGraphIndex(1)), G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2); end
hold(obj.f.Children(1).Children(obj.networkGraphIndex(1)), "off");
if size(obj.networkGraphIndex, 2) > 1 % Plot graph object(s)
for ii = 2:size(ind, 2) hold(ax, "on");
o = [o; copyobj(o(1), obj.f.Children(1).Children(obj.networkGraphIndex(ii)))]; o = plot(ax, G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2);
end if obj.useRoutingTopology
% Flow overlay: reuse the base layout's node coordinates and hide
% the overlay's own nodes/labels so only its edges show. Sizeable
% arrowheads so the flow direction reads at tile size.
D = digraph(obj.routingAdjacencyMatrix);
o = [o; plot(ax, D, "XData", o(1).XData, "YData", o(1).YData, ...
"LineStyle", "-", "EdgeColor", "g", "LineWidth", 2, ...
"ArrowSize", 12, "Marker", "none", "NodeLabel", {})];
end
hold(ax, "off");
nBase = size(o, 1);
if ~isnan(obj.networkGraphIndex) && size(obj.networkGraphIndex, 2) > 1
for ii = 2:size(obj.networkGraphIndex, 2)
o = [o; copyobj(o(1:nBase), obj.f.Children(1).Children(obj.networkGraphIndex(ii)))];
end end
end end
% In SINR comms mode, label each edge with the connection's SINR and report
% the connectivity threshold in the tile title. The fixed-radius model is
% left unchanged (no edge labels, title stays as set in firstPlotSetup).
if obj.useSinrComms
endNodes = G.Edges.EndNodes;
nAgents = size(obj.agents, 1);
positions = zeros(nAgents, 3);
for kk = 1:nAgents
positions(kk, :) = obj.agents{kk}.pos;
end
% Labels are bare numbers (no units) so they stay compact; GraphPlot
% renders them horizontally, left-to-right. Dotted (maintained) edges
% are labelled with the binding (worse) directional SINR — a link is
% feasible only if BOTH directions clear the threshold. Links that
% also carry flow leave the label to the solid directed edge, which
% shows the flow value (units of r) sent child -> parent.
edgeLabels = strings(size(endNodes, 1), 1);
for ee = 1:size(endNodes, 1)
lo = endNodes(ee, 1);
hi = endNodes(ee, 2);
if obj.useRoutingTopology && (obj.routingAdjacencyMatrix(lo, hi) || obj.routingAdjacencyMatrix(hi, lo))
edgeLabels(ee) = ""; % solid overlay carries this link's label
else
sinrLoRx = obj.sinrLink(positions, lo, hi); % receiver lo (lower index)
sinrHiRx = obj.sinrLink(positions, hi, lo); % receiver hi (higher index)
edgeLabels(ee) = sprintf("%.1f", 10 * log10(min(sinrLoRx, sinrHiRx)));
end
end
o(1).EdgeLabel = edgeLabels;
if obj.useRoutingTopology
endNodesD = D.Edges.EndNodes; % each row [child parent]
flowLabels = strings(size(endNodesD, 1), 1);
for ee = 1:size(endNodesD, 1)
flowLabels(ee) = sprintf("%.2f", obj.routingFlows(endNodesD(ee, 1), endNodesD(ee, 2)));
end
o(2).EdgeLabel = flowLabels;
end
% Threshold goes in the existing single-line title (not a subtitle) so
% the small tile is not compressed further, and the hover toolbar is
% disabled so the R2026 "..." menu stops covering the readout.
title(ax, sprintf("Network Graph (\\geq %.1f dB)", obj.sinrThreshold));
ax.Toolbar.Visible = "off";
end
obj.graphPlot = o; obj.graphPlot = o;
end end
+7 -2
View File
@@ -42,9 +42,14 @@ 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 % Determine desired communications links: the Capacity-Aware Lesser
% Sink Neighbor algorithm replaces lesser-neighbor when selected
if ~obj.useFixedTopology if ~obj.useFixedTopology
obj = obj.lesserNeighbor(); if coder.target('MATLAB') && obj.useRoutingTopology
obj = obj.lesserSinkNeighbor();
else
obj = obj.lesserNeighbor();
end
end end
% Log constraint adjacency for this timestep % Log constraint adjacency for this timestep
+101
View File
@@ -0,0 +1,101 @@
function [sinr, grad] = sinrLink(obj, positions, rxIdx, txIdx)
% SINRLINK Linear SINR of the link txIdx -> rxIdx and its position gradient.
%
% Computes the signal-to-interference-plus-noise ratio seen at receiver rxIdx
% for the signal transmitted by txIdx, treating every other agent as an
% interferer at that receiver. Isotropic transmit/receive (no antenna gains).
%
% Returns the linear SINR and (optionally) its gradient with respect to every
% agent position, grad(a, :) = d(SINR)/d(p_a). The value is used by
% updateAdjacency (connectivity) and both the value and gradient by
% constrainMotion (the SINR CBF barrier).
%
% Model (linear power domain), receiver r = rxIdx, transmitter t = txIdx:
% S = P_t / (K * d_tr^n) % received signal at r from t (W)
% I = sum_{k != t,r} P_k / (K * d_kr^n) % interference at r from others (W)
% N = k_B * T * B % thermal noise (W)
% SINR = S / (N + I)
% with K = (4*pi*f_c/c)^2 and physical constants baked in below. The only
% tunables are per-agent txPower (P), pathLossExponent (n), ambientTemp (T),
% all read from obj; the SINR threshold is applied by the callers.
arguments (Input)
obj (1, 1) {mustBeA(obj, "miSim")};
positions (:, 3) double;
rxIdx (1, 1) double;
txIdx (1, 1) double;
end
arguments (Output)
sinr (1, 1) double;
grad (:, 3) double;
end
% Universal physical constants
k_B = 1.380649e-23; % Boltzmann constant (J/K)
c = 3e8; % speed of light (m/s)
% System parameters
f_c = obj.centerFreq; % carrier frequency (Hz)
B = obj.bandwidth; % channel bandwidth (Hz)
K = (4 * pi * f_c / c)^2; % free-space path-loss reference constant
n = obj.pathLossExponent;
T = obj.ambientTemp;
d0 = 1.0; % near-field distance floor (m)
nAgents = size(positions, 1);
grad = zeros(nAgents, 3);
r = rxIdx;
t = txIdx;
pr = positions(r, :);
% Thermal noise (always > 0, so the denominator never vanishes)
N = k_B * T * B;
% Signal term S (source = transmitter t)
P_t = obj.agents{t}.txPower;
dvec_t = positions(t, :) - pr;
d_t = max(norm(dvec_t), d0);
S = P_t / (K * d_t^n);
% Interference I = sum of received powers from every other agent at r
I = 0;
for k = 1:nAgents
if k == r || k == t
continue;
end
d_k = max(norm(positions(k, :) - pr), d0);
I = I + obj.agents{k}.txPower / (K * d_k^n);
end
D = N + I; % denominator: noise + interference
sinr = S / D;
% ---- Gradient (skip unless requested) -------------------------------
% For a received term P_x/(K*d_xr^n):
% d(term)/dp_x = -n*term/d_xr^2 * (p_x - p_r), d(term)/dp_r = -d(term)/dp_x
if nargout > 1
% Transmitter affects S only: dSINR/dp_t = (dS/dp_t)/D
dS_dpt = (-n * S / (d_t^2)) * dvec_t;
grad(t, :) = dS_dpt / D;
% Interferers affect I only; accumulate the receiver's I-contribution
dI_dpr = zeros(1, 3);
for k = 1:nAgents
if k == r || k == t
continue;
end
dvec_k = positions(k, :) - pr;
d_k = max(norm(dvec_k), d0);
term_k = obj.agents{k}.txPower / (K * d_k^n);
dterm_dpk = (-n * term_k / (d_k^2)) * dvec_k;
grad(k, :) = (-S / (D^2)) * dterm_dpk; % dSINR/dp_k
dI_dpr = dI_dpr - dterm_dpk; % d(term_k)/dp_r = -d(term_k)/dp_k
end
% Receiver affects both S and I
dS_dpr = -dS_dpt;
grad(r, :) = dS_dpr / D - (S / (D^2)) * dI_dpr;
end
end
+34 -9
View File
@@ -6,16 +6,41 @@ function obj = updateAdjacency(obj)
obj (1, 1) {mustBeA(obj, "miSim")}; obj (1, 1) {mustBeA(obj, "miSim")};
end end
% Initialize assuming only self-connections nAgents = size(obj.agents, 1);
A = true(size(obj.agents, 1));
% Check lower triangle off-diagonal connections % Initialize assuming all agents are connected
for ii = 2:size(A, 1) A = true(nAgents);
for jj = 1:(ii - 1)
% Check that agents are not out of range if ~obj.useSinrComms
if norm(obj.agents{ii}.pos - obj.agents{jj}.pos) > min([obj.agents{ii}.commsGeometry.radius, obj.agents{jj}.commsGeometry.radius]) % Fixed-radius model: pair connected iff separation is within the
A(ii, jj) = false; % comm range violation % smaller of the two comms radii.
continue; for ii = 2:nAgents
for jj = 1:(ii - 1)
if norm(obj.agents{ii}.pos - obj.agents{jj}.pos) > min([obj.agents{ii}.commsGeometry.radius, obj.agents{jj}.commsGeometry.radius])
A(ii, jj) = false; % comm range violation
continue;
end
end
end
else
% SINR model: a link between a pair is feasible only if BOTH directional
% SINRs clear the threshold, i.e. each agent receives the other's signal
% above gammaLin (with interference from all other agents at the
% receiver). Every ordered pair (permutation) is evaluated; a single
% failing direction eliminates the undirected link.
gammaLin = 10^(obj.sinrThreshold / 10); % dB threshold -> linear
positions = zeros(nAgents, 3);
for kk = 1:nAgents
positions(kk, :) = obj.agents{kk}.pos;
end
for ii = 2:nAgents
for jj = 1:(ii - 1)
sinrLoRx = obj.sinrLink(positions, jj, ii); % receiver jj (lower), transmitter ii
sinrHiRx = obj.sinrLink(positions, ii, jj); % receiver ii (higher), transmitter jj
if sinrLoRx < gammaLin || sinrHiRx < gammaLin
A(ii, jj) = false; % at least one direction below threshold
continue;
end
end end
end end
end end
+15
View File
@@ -5,11 +5,26 @@ function validate(obj)
arguments (Output) arguments (Output)
end end
%% Mode Validators
if obj.useRoutingTopology && ~obj.useSinrComms
error("Routing topology requires SINR comms mode (useSinrComms)");
end
if obj.useRoutingTopology && obj.useFixedTopology
error("useRoutingTopology and useFixedTopology are mutually exclusive topology selectors");
end
%% Communications Network Validators %% Communications Network Validators
if max(conncomp(graph(obj.adjacency))) ~= 1 if max(conncomp(graph(obj.adjacency))) ~= 1
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 (guaranteed by the connectivity bridge)
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
+4
View File
@@ -47,6 +47,7 @@ function writeInits(obj)
tilt = cellfun(@(x) x.sensorModel.tilt, obj.agents); tilt = cellfun(@(x) x.sensorModel.tilt, obj.agents);
azimuth = cellfun(@(x) x.sensorModel.azimuth, obj.agents); azimuth = cellfun(@(x) x.sensorModel.azimuth, obj.agents);
comRanges = cellfun(@(x) x.commsGeometry.radius, obj.agents); comRanges = cellfun(@(x) x.commsGeometry.radius, obj.agents);
txPower = cellfun(@(x) x.txPower, obj.agents);
initialStepSize = cellfun(@(x) x.initialStepSize, obj.agents); initialStepSize = cellfun(@(x) x.initialStepSize, obj.agents);
pos = cell2mat(cellfun(@(x) x.pos, obj.agents, 'UniformOutput', false)); pos = cell2mat(cellfun(@(x) x.pos, obj.agents, 'UniformOutput', false));
obsMinCorners = cell2mat(cellfun(@(x) x.minCorner, userObstacles, 'UniformOutput', false)); obsMinCorners = cell2mat(cellfun(@(x) x.minCorner, userObstacles, 'UniformOutput', false));
@@ -59,6 +60,9 @@ function writeInits(obj)
"barrierGain", obj.barrierGain, "barrierExponent", obj.barrierExponent, "numObstacles", numInputObs, ... "barrierGain", obj.barrierGain, "barrierExponent", obj.barrierExponent, "numObstacles", numInputObs, ...
"numAgents", size(obj.agents, 1), "collisionRadius", collisionRadii, "comRange", comRanges, ... "numAgents", size(obj.agents, 1), "collisionRadius", collisionRadii, "comRange", comRanges, ...
"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, ...
"centerFreq", obj.centerFreq, "bandwidth", obj.bandwidth, "txPower", txPower, ... % SINR comms model 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
+1 -2
View File
@@ -19,6 +19,5 @@ function value = RSS(obj, d, dx, dy, dz)
cos_theta = (st .* (dx .* sa + dy .* ca) - ct .* dz) ./ max(d, eps); cos_theta = (st .* (dx .* sa + dy .* ca) - ct .* dz) ./ max(d, eps);
cos_theta = max(-1, min(1, cos_theta)); cos_theta = max(-1, min(1, cos_theta));
theta = acosd(cos_theta); theta = acosd(cos_theta);
gain = 10 .* obj.beamwidthExponent .* log10((1 + cosd(theta)) ./ 2); value = obj.P_TX_dBm + obj.transmitterGain(theta) + obj.G_RX_dBi - obj.pathLoss(d);
value = obj.P_TX_dBm + gain + obj.G_RX_dBi - obj.pathLoss(d);
end end
+1 -5
View File
@@ -5,12 +5,8 @@ function value = halfAngle(obj)
arguments (Output) arguments (Output)
value (1, 1) double; value (1, 1) double;
end end
% Sweep angular offset from boresight by evaluating transmitterGain at
% (obj.tilt + dtheta, obj.azimuth). The cosine difference identity guarantees
% the resulting angular offset from boresight equals dtheta exactly,
% independent of the actual pointing direction.
dtheta = (0:0.1:179.9)'; dtheta = (0:0.1:179.9)';
gain = obj.transmitterGain(obj.tilt + dtheta, obj.azimuth * ones(size(dtheta))); gain = obj.transmitterGain(dtheta);
target = gain(1) - 3; target = gain(1) - 3;
idx = find(gain <= target, 1); idx = find(gain <= target, 1);
if isempty(idx) || idx == 1 if isempty(idx) || idx == 1
+4 -1
View File
@@ -1,7 +1,8 @@
function obj = initialize(obj, txPower, bandwidth, centerFreq, rxGain_dBi, beamwidthExponent, tilt, azimuth, lossExponent) function obj = initialize(obj, txPower, temperature, bandwidth, centerFreq, rxGain_dBi, beamwidthExponent, tilt, azimuth, lossExponent)
arguments (Input) arguments (Input)
obj (1, 1) {mustBeA(obj, "rfSensor")} obj (1, 1) {mustBeA(obj, "rfSensor")}
txPower (1, 1) double; txPower (1, 1) double;
temperature (1, 1) double;
bandwidth (1, 1) double; bandwidth (1, 1) double;
centerFreq (1, 1) double; centerFreq (1, 1) double;
rxGain_dBi (1, 1) double; rxGain_dBi (1, 1) double;
@@ -16,6 +17,7 @@ function obj = initialize(obj, txPower, bandwidth, centerFreq, rxGain_dBi, beamw
%% Provided values %% Provided values
obj.P_TX = txPower; % Transmit power (W) obj.P_TX = txPower; % Transmit power (W)
obj.T_0 = temperature; % Temperature (K)
obj.BW = bandwidth; % Bandwidth (Hz) obj.BW = bandwidth; % Bandwidth (Hz)
obj.f_c = centerFreq; % Center frequency (Hz) obj.f_c = centerFreq; % Center frequency (Hz)
obj.G_RX_dBi = rxGain_dBi; % Receiving Antenna Gain (dBi) obj.G_RX_dBi = rxGain_dBi; % Receiving Antenna Gain (dBi)
@@ -29,4 +31,5 @@ function obj = initialize(obj, txPower, bandwidth, centerFreq, rxGain_dBi, beamw
%% Computed values %% Computed values
obj.P_TX_dBm = 10*log10(obj.P_TX/1e-3); % Transmit power in dBm obj.P_TX_dBm = 10*log10(obj.P_TX/1e-3); % Transmit power in dBm
obj.N = obj.k_B * obj.T_0 * obj.BW; % Thermal noise obj.N = obj.k_B * obj.T_0 * obj.BW; % Thermal noise
obj.constantGainTerm_dB = 10 .* log10(2 * (obj.beamwidthExponent + 1)); % used in RSS calculation, only need to compute once
end end
+1 -1
View File
@@ -8,6 +8,6 @@ function L_FSPL_dB = pathLoss(obj, d)
end end
% Free Space Path Loss (dB); d clamped away from zero (log undefined at d=0) % Free Space Path Loss (dB); d clamped away from zero (log undefined at d=0)
L_FSPL_dB = obj.lossExponent * 10 * log10(max(d, eps)) + 20 * log10(obj.f_c) + 20 * log10((4*pi)/obj.c); L_FSPL_dB = obj.lossExponent * 10 * log10(max(d, eps)) + 20 * log10((4 * pi * obj.f_c) / obj.c);
end end
+5 -2
View File
@@ -23,8 +23,11 @@ function f = plotParameters(obj)
% For agent at altitude alt, ground target at tilt T has slant distance: % For agent at altitude alt, ground target at tilt T has slant distance:
D = alt ./ cosd(T); D = alt ./ cosd(T);
% Compute RSS for each (d, t, a) triple % Convert LOS (D, T, A) to displacement vectors for RSS
rss = obj.RSS(D(:), T(:), A(:)); dx = D .* sind(T) .* sind(A);
dy = D .* sind(T) .* cosd(A);
dz = -D .* cosd(T); % negative: ground targets are below agent
rss = obj.RSS(D(:), dx(:), dy(:), dz(:));
Fslice = reshape(rss, size(D)); Fslice = reshape(rss, size(D));
% Disc geometry: t=0 (nadir) -> center, t~90 (horizon) -> edge % Disc geometry: t=0 (nadir) -> center, t~90 (horizon) -> edge
+4 -6
View File
@@ -34,10 +34,6 @@ function f = plotPerformance(obj, altitude, otherSensorsPos, otherSensors)
SINR = reshape(SINR, size(targetPosX)); SINR = reshape(SINR, size(targetPosX));
SNR = reshape(SNR, size(targetPosX)); SNR = reshape(SNR, size(targetPosX));
% normalize in linear scale
SINR = 10.^(SINR/10); SINR = SINR ./ max(SINR(:)); SINR = 10 * log10(SINR);
SNR = 10.^(SNR/10); SNR = SNR ./ max(SNR(:)); SNR = 10 * log10(SNR);
% Collect sensor positions and boresight parameters for overlay % Collect sensor positions and boresight parameters for overlay
sensorXY = [0, 0; otherSensorsPos(:, 1:2)]; sensorXY = [0, 0; otherSensorsPos(:, 1:2)];
sensorTilts = [obj.tilt; cellfun(@(s) s.tilt, otherSensors)]; sensorTilts = [obj.tilt; cellfun(@(s) s.tilt, otherSensors)];
@@ -50,7 +46,8 @@ function f = plotPerformance(obj, altitude, otherSensorsPos, otherSensors)
nexttile; nexttile;
imagesc(distances, distances, SNR); imagesc(distances, distances, SNR);
axis("image"); set(gca, 'YDir', 'normal'); axis("image"); set(gca, 'YDir', 'normal');
colorbar; xlabel("X (m)"); ylabel("Y (m)"); cb = colorbar; cb.Label.String = "SNR (dB)";
xlabel("X (m)"); ylabel("Y (m)");
title("Linearly Normalized SNR (dB)"); title("Linearly Normalized SNR (dB)");
subtitle("No interfering sources"); subtitle("No interfering sources");
addSensorOverlay(gca, sensorXY(1, 1:2), sensorTilts(1, 1), sensorAzimuths(1, 1), tailScale); addSensorOverlay(gca, sensorXY(1, 1:2), sensorTilts(1, 1), sensorAzimuths(1, 1), tailScale);
@@ -58,7 +55,8 @@ function f = plotPerformance(obj, altitude, otherSensorsPos, otherSensors)
nexttile; nexttile;
imagesc(distances, distances, SINR); imagesc(distances, distances, SINR);
axis("image"); set(gca, 'YDir', 'normal'); axis("image"); set(gca, 'YDir', 'normal');
colorbar; xlabel("X (m)"); ylabel("Y (m)"); cb = colorbar; cb.Label.String = "SNR (dB)";
xlabel("X (m)"); ylabel("Y (m)");
title("Linearly Normalized SINR (dB)"); title("Linearly Normalized SINR (dB)");
subtitle(sprintf("%d interfering source(s)", size(otherSensorsPos, 1))); subtitle(sprintf("%d interfering source(s)", size(otherSensorsPos, 1)));
addSensorOverlay(gca, sensorXY, sensorTilts, sensorAzimuths, tailScale); addSensorOverlay(gca, sensorXY, sensorTilts, sensorAzimuths, tailScale);
+5 -4
View File
@@ -3,7 +3,7 @@ classdef rfSensor
% Physical parameters % Physical parameters
c = 3e8; % Speed of light (m/s) c = 3e8; % Speed of light (m/s)
k_B = 1.38e-23 % Boltzmann constant (W/Hz/K) for thermal noise model k_B = 1.38e-23 % Boltzmann constant (W/Hz/K) for thermal noise model
T_0 = 300; % Ambient temperature (Kelvin) for thermal noise model T_0 = NaN; % Ambient temperature (Kelvin) for thermal noise model
lossExponent = NaN; % Path loss exponent (2 for free space, up to 6 for the lossiest environments) lossExponent = NaN; % Path loss exponent (2 for free space, up to 6 for the lossiest environments)
% Sensor parameters % Sensor parameters
P_TX = NaN; % Transmit power (Watts) P_TX = NaN; % Transmit power (Watts)
@@ -14,6 +14,7 @@ classdef rfSensor
% Values computed at initialization % Values computed at initialization
P_TX_dBm = NaN; % Transmit power (dBm) P_TX_dBm = NaN; % Transmit power (dBm)
N = NaN; % Thermal noise N = NaN; % Thermal noise
constantGainTerm_dB = NaN; % used in RSS calculation, only need to compute once
% Cached state (per timestep) % Cached state (per timestep)
end end
properties (Access = public) properties (Access = public)
@@ -23,7 +24,7 @@ classdef rfSensor
end end
methods (Access = public) methods (Access = public)
[obj] = initialize(obj, txPower, bandwidth, centerFreq, rxGain, beamwidthExponent, tilt, azimuth); % initialize sensor, define parameters [obj] = initialize(obj, txPower, temperature, bandwidth, centerFreq, rxGain, beamwidthExponent, tilt, azimuth); % initialize sensor, define parameters
[SINR, SNR, obj, otherSensors] = sensorPerformance(obj, agentPos, targetPos, otherSensorsPos, otherSensors); % determine sensor performance for a given single sensor and target geometry [SINR, SNR, obj, otherSensors] = sensorPerformance(obj, agentPos, targetPos, otherSensorsPos, otherSensors); % determine sensor performance for a given single sensor and target geometry
[d, dx, dy, dz] = computePointToPoints(obj, agentPos, targetPos); [d, dx, dy, dz] = computePointToPoints(obj, agentPos, targetPos);
[value] = halfAngle(obj); % tilt angle (deg) at which sensor performance is halved [value] = halfAngle(obj); % tilt angle (deg) at which sensor performance is halved
@@ -34,7 +35,7 @@ classdef rfSensor
end end
methods (Access = private) methods (Access = private)
x = RSS(obj, d, dx, dy, dz); % Received signal strength (function of distance and tilt angle) x = RSS(obj, d, dx, dy, dz); % Received signal strength (function of distance and tilt angle)
G_TX_dB = transmitterGain(obj, t, a); % Antenna gain for a given TX/RX pair G_TX_dB = transmitterGain(obj, theta); % Antenna gain for a given angle from boresight (degrees)
L_FSPL_dB = pathLoss(obj, d); % Free space path loss for a given TX/RX pair L_FSPL_dB = pathLoss(obj, d); % Free space path loss for a given TX/RX pair
end end
end end
+6 -17
View File
@@ -1,23 +1,12 @@
function value = transmitterGain(obj, t, a) function value = transmitterGain(obj, theta)
arguments (Input) arguments (Input)
obj (1, 1) {mustBeA(obj, "rfSensor")}; obj (1, 1) {mustBeA(obj, "rfSensor")};
t (:, 1) double; % LOS tilt angle theta (:, 1) double; % angle from boresight (degrees)
a (:, 1) double; % LOS azimuth angle
end end
arguments (Output) arguments (Output)
value (:, 1) double value (:, 1) double
end end
if ~isequal(size(t), size(a)) % Cosine pattern: peak at boresight, -inf dB at 90°.
error("t and a must be the same size"); % Clamped so targets behind the antenna (theta > 90°) get -inf rather than NaN.
end value = obj.constantGainTerm_dB + 10 .* obj.beamwidthExponent .* log10(max(0, cosd(theta)));
% Angular offset from boresight via spherical law of cosines
% Convention: t=0° nadir, t=90° horizon; a=0° +y, a=90° +x
cos_theta = sind(obj.tilt) .* sind(t) .* cosd(a - obj.azimuth) + ...
cosd(obj.tilt) .* cosd(t);
cos_theta = max(-1, min(1, cos_theta)); % clamp for numerical safety
theta = acosd(cos_theta);
% Cardioid family: peak at boresight (theta=0), null opposite (theta=180°)
value = 10 .* obj.beamwidthExponent .* log10((1 + cosd(theta)) ./ 2);
end end
+8 -1
View File
@@ -36,7 +36,14 @@ function f = plot(obj, ind, f)
% Add to other perspectives % Add to other perspectives
if size(ind, 2) > 1 if size(ind, 2) > 1
for ii = 2:size(ind, 2) for ii = 2:size(ind, 2)
copyobj(o, f.Children(1).Children(ind(ii))); axCopy = f.Children(1).Children(ind(ii));
copyobj(o, axCopy);
% copyobj does not carry color scaling. The objective values are
% sum-normalized (tiny), so without matching CLim the copied surface
% is squished to one color once the destination axes' auto CLim is
% widened by later surfaces (e.g. agent geometries). Pin it to the
% source range so the heatmap renders as a visible gradient.
clim(axCopy, cRange);
end end
end end
end end
+33 -7
View File
@@ -1,22 +1,32 @@
function f = plotParameters(obj) function f = plotParameters(obj, f)
arguments (Input) arguments (Input)
obj (1, 1) {mustBeA(obj, "sigmoidSensor")}; obj (1, 1) {mustBeA(obj, "sigmoidSensor")};
f (1, 1) {mustBeA(f, "matlab.ui.Figure")} = figure;
end end
arguments (Output) arguments (Output)
f (1, 1) {mustBeA(f, "matlab.ui.Figure")}; f (1, 1) {mustBeA(f, "matlab.ui.Figure")};
end end
% Distance and tilt sample points % Distance and tilt sample points
d = 0:(obj.alphaDist / 100):(2*obj.alphaDist); d = 0:(obj.alphaDist / 200):(2*obj.alphaDist);
t = -90:1:90; t = -90:0.5:90;
% Sample membership functions % Sample membership functions
d_x = obj.distanceMembership(d); d_x = obj.distanceMembership(d);
t_x = obj.tiltMembership(t); t_x = obj.tiltMembership(t);
% pad ends of sampled results with zeros at +/- some big number
big = 1e6 * max(d);
d = [-big, d, big];
t = [-big, t, big];
d_x = [0, d_x', 0];
t_x = [0, t_x', 0];
% Plot resultant sigmoid curves % Plot resultant sigmoid curves
f = figure; hold(gca(f), "on");
tiledlayout(f, 2, 1, "TileSpacing", "tight", "Padding", "compact"); if isempty(gca(f).Children)
tiledlayout(f, 2, 1, "TileSpacing", "tight", "Padding", "compact");
end
% Distance % Distance
nexttile(1, [1, 1]); nexttile(1, [1, 1]);
@@ -24,19 +34,35 @@ function f = plotParameters(obj)
title("Distance Membership Sigmoid"); title("Distance Membership Sigmoid");
xlabel("Distance (m)"); xlabel("Distance (m)");
ylabel("Membership"); ylabel("Membership");
ld = legend();
hold("on"); hold("on");
plot(d, d_x, "LineWidth", 2); plot(d, d_x, "LineWidth", 2);
hold("off"); hold("off");
xData = cell2mat(arrayfun(@(x) x.XData(2:(end - 1)), f.Children.Children(end).Children, 'UniformOutput', false));
xlim([min(xData, [], "all"), max(xData, [], "all")]);
ylim([0, 1]); ylim([0, 1]);
% Tilt % Tilt
nexttile(2, [1, 1]); nexttile(2, [1, 1]);
grid("on"); grid("on");
title("Tilt Membership Sigmoid"); title("Tilt Membership Sigmoid");
xlabel("Tilt (deg)"); xlabel("Tilt ($^\circ$)");
ylabel("Membership"); ylabel("Membership");
lt = legend();
hold("on"); hold("on");
plot(t, t_x, "LineWidth", 2); plot(t, t_x, "LineWidth", 2);
hold("off"); hold("off");
xlim([-90, 90]);
ylim([0, 1]); ylim([0, 1]);
% add legends
nexttile(1, [1, 1]);
f.Children.Children(3).String{end} = sprintf("Sensor %d", length(f.Children.Children(end).Children));
f.Children.Children(1).String{end} = sprintf("Sensor %d", length(f.Children.Children(end).Children));
nexttile(2, [1, 1]);
f.Children.Children(3).String{end} = sprintf("Sensor %d", length(f.Children.Children(end).Children));
f.Children.Children(1).String{end} = sprintf("Sensor %d", length(f.Children.Children(end).Children));
hold(gca(f), "off");
end end
+1 -1
View File
@@ -16,7 +16,7 @@ classdef sigmoidSensor
[obj] = initialize(obj, alphaDist, betaDist, alphaTilt, betaTilt, tilt, azimuth); % initialize sensor, define parameters [obj] = initialize(obj, alphaDist, betaDist, alphaTilt, betaTilt, tilt, azimuth); % initialize sensor, define parameters
[value] = sensorPerformance(obj, agentPos, targetPos); % determine sensor performance for a given single sensor and target geometry [value] = sensorPerformance(obj, agentPos, targetPos); % determine sensor performance for a given single sensor and target geometry
[value] = halfAngle(obj); % tilt angle (deg) at which sensor performance is halved [value] = halfAngle(obj); % tilt angle (deg) at which sensor performance is halved
[f] = plotParameters(obj); % debug, plot sensor response as a function of distance and tilt angle [f] = plotParameters(obj, f); % debug, plot sensor response as a function of distance and tilt angle
end end
methods (Access = private) methods (Access = private)
x = distanceMembership(obj, d); % used in computing distance factor of sensor performance x = distanceMembership(obj, d); % used in computing distance factor of sensor performance
+1
View File
@@ -8,5 +8,6 @@ classdef MESSAGE_TYPE < uint8
GUIDANCE_TOGGLE (6) % Server->Client: toggle guidance mode on/off GUIDANCE_TOGGLE (6) % Server->Client: toggle guidance mode on/off
REQUEST_POSITION (7) % Server->Client: respond with current ENU position REQUEST_POSITION (7) % Server->Client: respond with current ENU position
POSITION (8) % Client->Server: current ENU position (3 doubles) POSITION (8) % Client->Server: current ENU position (3 doubles)
TAKEOFF (9) % Server->Client: take off to 25 m, ACK then READY on arrival
end end
end end
+31 -6
View File
@@ -44,6 +44,7 @@ class MessageType(IntEnum):
GUIDANCE_TOGGLE = 6 GUIDANCE_TOGGLE = 6
REQUEST_POSITION = 7 REQUEST_POSITION = 7
POSITION = 8 POSITION = 8
TAKEOFF = 9
AERPAW_DIR = Path('/root/miSim/aerpaw') AERPAW_DIR = Path('/root/miSim/aerpaw')
@@ -184,13 +185,11 @@ class UAVRunner(BasicRunner):
log_task = None log_task = None
nav_task = None nav_task = None
try: try:
# Takeoff to above AERPAW minimum altitude # Takeoff is now driven by a TAKEOFF command from the controller
print("[UAV] Taking off...") # (staggered in time between UAVs), not performed at startup. Start
await drone.takeoff(25) # GPS logging immediately so the ground/idle period is captured too.
print("[UAV] Takeoff complete, waiting for commands...")
# Start GPS logging in background
log_task = asyncio.create_task(_gps_log_loop(drone)) log_task = asyncio.create_task(_gps_log_loop(drone))
print("[UAV] Connected; waiting for commands...")
# Command loop - handle all messages from controller # Command loop - handle all messages from controller
waypoint_num = 0 waypoint_num = 0
@@ -213,6 +212,27 @@ class UAVRunner(BasicRunner):
await send_message_type(writer, MessageType.ACK) await send_message_type(writer, MessageType.ACK)
print("[UAV] Sent ACK (guidance mode exited, ready for sequential commands)") print("[UAV] Sent ACK (guidance mode exited, ready for sequential commands)")
elif msg_type == MessageType.TAKEOFF:
# aerpawlib only switches the vehicle to GUIDED inside
# _initialize_postarm(), which await_ready_to_move() runs only
# when the vehicle is DISARMED at the first movement command.
# The safety pilot arms all UAVs at experiment start, so a UAV
# whose takeoff is deliberately delayed (for time separation)
# is already armed by now and would skip that path -> no GUIDED
# switch -> simple_takeoff is ignored in ALT_HOLD and it never
# leaves the ground. Run the post-arm init explicitly to
# guarantee GUIDED mode (and home capture) before taking off;
# takeoff()'s own gate then sees the vehicle armed and does not
# re-initialize.
await send_message_type(writer, MessageType.ACK)
print("[UAV] Sent ACK (takeoff)")
print("[UAV] Initializing (GUIDED) and taking off to 25 m...")
await drone._initialize_postarm()
await drone.takeoff(25)
print("[UAV] Takeoff complete")
await send_message_type(writer, MessageType.READY)
print("[UAV] Sent READY")
elif msg_type == MessageType.REQUEST_POSITION: elif msg_type == MessageType.REQUEST_POSITION:
# Respond immediately with current ENU position relative to origin # Respond immediately with current ENU position relative to origin
pos = drone.position pos = drone.position
@@ -288,6 +308,11 @@ class UAVRunner(BasicRunner):
except (ValueError, asyncio.IncompleteReadError, ConnectionError) as e: except (ValueError, asyncio.IncompleteReadError, ConnectionError) as e:
print(f"[UAV] Error: {e}") print(f"[UAV] Error: {e}")
except Exception as e:
import traceback
print(f"[UAV] Unhandled exception: {type(e).__name__}: {e}")
traceback.print_exc()
finally: finally:
if nav_task is not None and not nav_task.done(): if nav_task is not None and not nav_task.done():
nav_task.cancel() nav_task.cancel()
+2 -2
View File
@@ -1,2 +1,2 @@
timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology, txPower, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth
1, 100, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 80.0", "0.25, 0.25", "8.0, 8.0", "0.1, 0.1", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "66.6, 66.6", "55, 35, 35, 55", 0.15, "15.0, 15.0, 50.0, 40.0, 15.0, 50.0", 1, "0.0, 35.0, 0.0", "50, 40.0, 60", 1, 2.0, 1 1, 80, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "60.0, 80.0, 45.0, 70.0", "70, 15, 15, 20, 20, 15, 15, 70", 0.15, "15.0, 15.0, 50.0, 40.0, 10.0, 45.0", 8, "0.0, 30.0, 0.0, 42.0, 30.0, 0.0, 84.0, 30.0, 0.0, 13.0, 60.0, 0.0, 55.0, 60.0, 0.0, 0.0, 90, 0.0, 42.0, 90.0, 0.0, 84.0, 90.0, 0.0", "16.0, 40.0, 100.0, 58.0, 40.0, 100.0, 100.0, 40.0, 100.0, 29.0, 70.0, 100.0, 71.0, 70.0, 100.0, 16.0, 100.0, 100.0, 58.0, 100.0, 100.0, 100.0, 100.0, 100.0", 0, 2.0, 1, "0.2, 0.1", 0, 5.0, 2.5, 290.0, 2.4e9, 20e6
1 timestep maxIter minAlt discretizationStep protectedRange initialStepSize barrierGain barrierExponent collisionRadius comRange alphaDist betaDist alphaTilt betaTilt domainMin domainMax objectivePos objectiveVar sensorPerformanceMinimum initialPositions numObstacles obstacleMin obstacleMax useDoubleIntegrator dampingCoeff useFixedTopology txPower useSinrComms sinrThreshold pathLossExponent ambientTemp centerFreq bandwidth
2 1 100 80 35.0 0.1 2.0 6 1 1 8.0, 8.0 35.0, 35.0 80.0, 80.0 80.0, 50.0 0.25, 0.25 0.25, 1.0 8.0, 8.0 8.0, 25.0 0.1, 0.1 0.1, 0.02 0.0, 0.0, 0.0 100.0, 100.0, 100.0 66.6, 66.6 60.0, 80.0, 45.0, 70.0 55, 35, 35, 55 70, 15, 15, 20, 20, 15, 15, 70 0.15 15.0, 15.0, 50.0, 40.0, 15.0, 50.0 15.0, 15.0, 50.0, 40.0, 10.0, 45.0 1 8 0.0, 35.0, 0.0 0.0, 30.0, 0.0, 42.0, 30.0, 0.0, 84.0, 30.0, 0.0, 13.0, 60.0, 0.0, 55.0, 60.0, 0.0, 0.0, 90, 0.0, 42.0, 90.0, 0.0, 84.0, 90.0, 0.0 50, 40.0, 60 16.0, 40.0, 100.0, 58.0, 40.0, 100.0, 100.0, 40.0, 100.0, 29.0, 70.0, 100.0, 71.0, 70.0, 100.0, 16.0, 100.0, 100.0, 58.0, 100.0, 100.0, 100.0, 100.0, 100.0 1 0 2.0 1 0.2, 0.1 0 5.0 2.5 290.0 2.4e9 20e6
+1 -1
View File
@@ -1,2 +1,2 @@
timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology
1, 50, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "60.0, 80.0, 45.0, 70.0", "70, 15, 15, 20, 20, 15, 15, 70", 0.15, "10.0, 10.0, 50.0, 40.0, 15.0, 45.0", 8, "0.0, 30.0, 0.0, 42.0, 30.0, 0.0, 84.0, 30.0, 0.0, 13.0, 60.0, 0.0, 55.0, 60.0, 0.0, 0.0, 90, 0.0, 42.0, 90.0, 0.0, 84.0, 90.0, 0.0", "16.0, 40.0, 100.0, 58.0, 40.0, 100.0, 100.0, 40.0, 100.0, 29.0, 70.0, 100.0, 71.0, 70.0, 100.0, 16.0, 100.0, 100.0, 58.0, 100.0, 100.0, 100.0, 100.0, 100.0", 0, 2.0, 1 1, 80, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "60.0, 80.0, 45.0, 70.0", "70, 15, 15, 20, 20, 15, 15, 70", 0.15, "15.0, 15.0, 50.0, 40.0, 10.0, 45.0", 8, "0.0, 30.0, 0.0, 42.0, 30.0, 0.0, 84.0, 30.0, 0.0, 13.0, 60.0, 0.0, 55.0, 60.0, 0.0, 0.0, 90, 0.0, 42.0, 90.0, 0.0, 84.0, 90.0, 0.0", "16.0, 40.0, 100.0, 58.0, 40.0, 100.0, 100.0, 40.0, 100.0, 29.0, 70.0, 100.0, 71.0, 70.0, 100.0, 16.0, 100.0, 100.0, 58.0, 100.0, 100.0, 100.0, 100.0, 100.0", 0, 2.0, 1
1 timestep maxIter minAlt discretizationStep protectedRange initialStepSize barrierGain barrierExponent collisionRadius comRange alphaDist betaDist alphaTilt betaTilt domainMin domainMax objectivePos objectiveVar sensorPerformanceMinimum initialPositions numObstacles obstacleMin obstacleMax useDoubleIntegrator dampingCoeff useFixedTopology
2 1 50 80 35.0 0.1 2.0 6 1 1 8.0, 8.0 35.0, 35.0 80.0, 50.0 0.25, 1.0 8.0, 25.0 0.1, 0.02 0.0, 0.0, 0.0 100.0, 100.0, 100.0 60.0, 80.0, 45.0, 70.0 70, 15, 15, 20, 20, 15, 15, 70 0.15 10.0, 10.0, 50.0, 40.0, 15.0, 45.0 15.0, 15.0, 50.0, 40.0, 10.0, 45.0 8 0.0, 30.0, 0.0, 42.0, 30.0, 0.0, 84.0, 30.0, 0.0, 13.0, 60.0, 0.0, 55.0, 60.0, 0.0, 0.0, 90, 0.0, 42.0, 90.0, 0.0, 84.0, 90.0, 0.0 16.0, 40.0, 100.0, 58.0, 40.0, 100.0, 100.0, 40.0, 100.0, 29.0, 70.0, 100.0, 71.0, 70.0, 100.0, 16.0, 100.0, 100.0, 58.0, 100.0, 100.0, 100.0, 100.0, 100.0 0 2.0 1
+1 -1
View File
@@ -1,2 +1,2 @@
timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology
1, 65, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "30.0, 80.0", "60, 20, 20, 30", 0.15, "65.0, 15.0, 65.0, 65.0, 15.0, 45.0", 3, "0.0, 25.0, 55.0, 40.0, 10.0, 0.0, 40.0, 45.0, 60.0", "100.0, 70.0, 60.0, 45.0, 80.0, 55.0, 100.0, 50.0, 100.0", 0, 2.0, 1 1, 125, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "30.0, 80.0", "60, 20, 20, 30", 0.15, "65.0, 15.0, 65.0, 65.0, 15.0, 45.0", 3, "0.0, 25.0, 55.0, 40.0, 10.0, 0.0, 40.0, 45.0, 60.0", "100.0, 70.0, 60.0, 45.0, 80.0, 55.0, 100.0, 50.0, 100.0", 0, 2.0, 1
1 timestep maxIter minAlt discretizationStep protectedRange initialStepSize barrierGain barrierExponent collisionRadius comRange alphaDist betaDist alphaTilt betaTilt domainMin domainMax objectivePos objectiveVar sensorPerformanceMinimum initialPositions numObstacles obstacleMin obstacleMax useDoubleIntegrator dampingCoeff useFixedTopology
2 1 65 125 35.0 0.1 2.0 6 1 1 8.0, 8.0 35.0, 35.0 80.0, 50.0 0.25, 1.0 8.0, 25.0 0.1, 0.02 0.0, 0.0, 0.0 100.0, 100.0, 100.0 30.0, 80.0 60, 20, 20, 30 0.15 65.0, 15.0, 65.0, 65.0, 15.0, 45.0 3 0.0, 25.0, 55.0, 40.0, 10.0, 0.0, 40.0, 45.0, 60.0 100.0, 70.0, 60.0, 45.0, 80.0, 55.0, 100.0, 50.0, 100.0 0 2.0 1
+1 -1
View File
@@ -1,2 +1,2 @@
timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology timestep, maxIter, minAlt, discretizationStep, protectedRange, initialStepSize, barrierGain, barrierExponent, collisionRadius, comRange, alphaDist, betaDist, alphaTilt, betaTilt, domainMin, domainMax, objectivePos, objectiveVar, sensorPerformanceMinimum, initialPositions, numObstacles, obstacleMin, obstacleMax, useDoubleIntegrator, dampingCoeff, useFixedTopology
1, 65, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "30.0, 80.0", "60, 20, 20, 30", 0.15, "90.0, 10.0, 50.0, 65.0, 15.0, 45.0", 4, "0.0, 30.0, 0.0, 70.0, 30.0, 0.0, 0.0, 60.0, 0.0, 55.0, 60.0, 0.0", "50.0, 40.0, 100.0, 100.0, 40.0, 100.0, 35.0, 70.0, 100.0, 100.0, 70.0, 100.0", 0, 2.0, 1 1, 125, 35.0, 0.1, 2.0, 6, 1, 1, "8.0, 8.0", "35.0, 35.0", "80.0, 50.0", "0.25, 1.0", "8.0, 25.0", "0.1, 0.02", "0.0, 0.0, 0.0", "100.0, 100.0, 100.0", "30.0, 80.0", "60, 20, 20, 30", 0.15, "90.0, 10.0, 50.0, 65.0, 15.0, 45.0", 4, "0.0, 30.0, 0.0, 70.0, 30.0, 0.0, 0.0, 60.0, 0.0, 55.0, 60.0, 0.0", "50.0, 40.0, 100.0, 100.0, 40.0, 100.0, 35.0, 70.0, 100.0, 100.0, 70.0, 100.0", 0, 2.0, 1
1 timestep maxIter minAlt discretizationStep protectedRange initialStepSize barrierGain barrierExponent collisionRadius comRange alphaDist betaDist alphaTilt betaTilt domainMin domainMax objectivePos objectiveVar sensorPerformanceMinimum initialPositions numObstacles obstacleMin obstacleMax useDoubleIntegrator dampingCoeff useFixedTopology
2 1 65 125 35.0 0.1 2.0 6 1 1 8.0, 8.0 35.0, 35.0 80.0, 50.0 0.25, 1.0 8.0, 25.0 0.1, 0.02 0.0, 0.0, 0.0 100.0, 100.0, 100.0 30.0, 80.0 60, 20, 20, 30 0.15 90.0, 10.0, 50.0, 65.0, 15.0, 45.0 4 0.0, 30.0, 0.0, 70.0, 30.0, 0.0, 0.0, 60.0, 0.0, 55.0, 60.0, 0.0 50.0, 40.0, 100.0, 100.0, 40.0, 100.0, 35.0, 70.0, 100.0, 100.0, 70.0, 100.0 0 2.0 1
+168 -382
View File
File diff suppressed because it is too large. Load diff
+203 -70
View File
@@ -7,58 +7,50 @@ coder.extrinsic('disp', 'readScenarioCsv');
% Maximum clients supported (one initial position per UAV) % Maximum clients supported (one initial position per UAV)
MAX_CLIENTS = 4; MAX_CLIENTS = 4;
% Two waypoints per UAV: altitude-staggered transit + final position % Three waypoints per UAV: stage (climb) → traverse (XY) → settle (descend)
MAX_TARGETS = MAX_CLIENTS * 2; MAX_TARGETS = MAX_CLIENTS * 3;
% Altitude-staggered flyout/flyback currently supports exactly 2 UAVs
if numClients ~= int32(2)
error('Altitude-staggered flyout/flyback requires exactly 2 UAVs');
end
% Minimum vertical separation maintained between UAVs during XY transit (metres)
TRANSIT_SEP = 30.0;
% Allocate targets array (MAX_TARGETS x 3) % Allocate targets array (MAX_TARGETS x 3)
targets = zeros(MAX_TARGETS, 3); targets = zeros(MAX_TARGETS, 3);
numWaypoints = int32(0); numWaypoints = int32(0); %#ok<NASGU>
totalLoaded = int32(0); % pre-declare type for coder.ceval %#ok<NASGU> totalLoaded = int32(0);
% Load initial UAV positions from scenario CSV % Experiment start positions from scenario CSV (N x 3)
scenarioPositions = zeros(MAX_CLIENTS, 3);
% Load experiment start positions from scenario CSV
if coder.target('MATLAB') if coder.target('MATLAB')
disp('Loading initial positions from scenario.csv (simulation)...'); disp('Loading initial positions from scenario.csv (simulation)...');
tmpSim = miSim; tmpSim = miSim;
sc = tmpSim.readScenarioCsv('aerpaw/config/scenario.csv'); sc = tmpSim.readScenarioCsv('aerpaw/config/scenario.csv');
flatPos = double(sc.initialPositions); % 1×(3*N) flat vector flatPos = double(sc.initialPositions); % 1×(3*N) flat vector
posMatrix = reshape(flatPos, 3, [])'; % N×3, same layout as initializeFromCsv posMatrix = reshape(flatPos, 3, [])'; % N×3
totalLoaded = int32(size(posMatrix, 1)); totalLoaded = int32(size(posMatrix, 1));
scenarioPositions(1:totalLoaded, :) = posMatrix;
% MATLAB path: send directly to scenario positions in one waypoint
targets(1:totalLoaded, :) = posMatrix; targets(1:totalLoaded, :) = posMatrix;
numWaypoints = int32(1); numWaypoints = int32(1);
disp(['Loaded ', num2str(double(totalLoaded)), ' initial positions']); disp(['Loaded ', num2str(double(totalLoaded)), ' initial positions']);
else else
coder.cinclude('controller_impl.h'); coder.cinclude('controller_impl.h');
filename = ['config/scenario.csv', char(0)]; filename = ['config/scenario.csv', char(0)];
% Load into targets temporarily; copy to scenarioPositions below
totalLoaded = coder.ceval('loadInitialPositions', coder.ref(filename), ... totalLoaded = coder.ceval('loadInitialPositions', coder.ref(filename), ...
coder.ref(targets), int32(MAX_TARGETS)); coder.ref(targets), int32(MAX_TARGETS));
numWaypoints = totalLoaded / int32(numClients); scenarioPositions(1:totalLoaded, :) = targets(1:totalLoaded, :);
end numWaypoints = int32(3);
% In the compiled path, inject altitude-staggered transit waypoints so UAVs
% are vertically separated while flying horizontally to their start positions.
% ArduPilot reaches target altitude before horizontal movement, so UAV i is at
% altitude (TRANSIT_ALT_BASE + (i-1)*TRANSIT_ALT_STEP) throughout its transit,
% preventing collisions regardless of horizontal path geometry.
% TRANSIT_ALT_STEP must exceed 2 * max(collisionRadius).
% Waypoint 1: each UAV flies to (finalX, finalY) at its unique transit altitude.
% Waypoint 2: each UAV adjusts to its actual target altitude.
% These constants are also used for the altitude-staggered return before RTL.
TRANSIT_ALT_BASE = 25.0; % must match drone.takeoff() altitude in uav_runner.py
TRANSIT_ALT_STEP = 25; % vertical separation per UAV (m); must exceed 2*collisionRadius
if ~coder.target('MATLAB')
for ii = double(totalLoaded):-1:1
transitRow = (ii - 1) * 2 + 1;
finalRow = (ii - 1) * 2 + 2;
finalPos = targets(ii, :);
transitAlt = TRANSIT_ALT_BASE + (ii - 1) * TRANSIT_ALT_STEP;
targets(finalRow, :) = finalPos;
targets(transitRow, :) = [finalPos(1), finalPos(2), transitAlt];
end
numWaypoints = int32(2);
end end
% Load guidance scenario from CSV (parameters for guidance_step) % Load guidance scenario from CSV (parameters for guidance_step)
NUM_SCENARIO_PARAMS = 55; NUM_SCENARIO_PARAMS = 65;
MAX_OBSTACLES_CTRL = int32(8); MAX_OBSTACLES_CTRL = int32(8);
scenarioParams = zeros(1, NUM_SCENARIO_PARAMS); scenarioParams = zeros(1, NUM_SCENARIO_PARAMS);
obstacleMin = zeros(MAX_OBSTACLES_CTRL, 3); obstacleMin = zeros(MAX_OBSTACLES_CTRL, 3);
@@ -92,43 +84,126 @@ for i = 1:numClients
end end
end end
% Waypoint loop: send each waypoint to all clients, wait for all to arrive % Query takeoff-pad GPS positions before sending any waypoints.
for w = 1:numWaypoints % These are also the flyback targets so each UAV lands where it took off.
% Send TARGET for waypoint w to each client initialPositions = zeros(MAX_CLIENTS, 3);
for i = 1:numClients if ~coder.target('MATLAB')
% Targets are grouped by client: client i's waypoints are at rows coder.ceval('sendRequestPositions', int32(numClients));
% (i-1)*numWaypoints+1 through i*numWaypoints coder.ceval('recvPositions', int32(numClients), coder.ref(initialPositions), int32(MAX_CLIENTS));
targetIdx = (i - 1) * numWaypoints + w; else
target = targets(targetIdx, :); % Simulation: treat scenario positions as the takeoff locations
initialPositions(1:totalLoaded, :) = scenarioPositions(1:totalLoaded, :);
end
if coder.target('MATLAB') % ---- Build altitude-staggered flyout waypoints --------------------------------
% Each UAV follows three phases:
% WP1: Climb to transit altitude at pad XY
% WP2: Traverse to final XY at transit altitude (>= TRANSIT_SEP vertical gap)
% WP3: Descend to final altitude at final XY
%
% Transit altitude assignment: the UAV with the highest final altitude keeps
% that altitude as its transit altitude; the other UAV climbs TRANSIT_SEP above
% it, guaranteeing >= 30 m vertical separation during the simultaneous XY
% traverse (WP2). Both transit altitudes are well above minAlt.
if ~coder.target('MATLAB')
if scenarioPositions(1, 3) >= scenarioPositions(2, 3)
higherIdx = int32(1);
lowerIdx = int32(2);
else
higherIdx = int32(2);
lowerIdx = int32(1);
end
transitAlt = zeros(1, MAX_CLIENTS);
transitAlt(lowerIdx) = scenarioPositions(lowerIdx, 3);
transitAlt(higherIdx) = scenarioPositions(lowerIdx, 3) + TRANSIT_SEP;
for iUAV = 1:numClients
base = double(iUAV - 1) * double(numWaypoints);
targets(base + 1, :) = [initialPositions(iUAV,1), initialPositions(iUAV,2), transitAlt(iUAV)];
targets(base + 2, :) = [scenarioPositions(iUAV,1), scenarioPositions(iUAV,2), transitAlt(iUAV)];
targets(base + 3, :) = scenarioPositions(iUAV, :);
end
end
% -------------------------------------------------------------------------------
% Waypoint loop
if coder.target('MATLAB')
% Simulation: simple lockstep (collisions are not modelled here).
for w = 1:numWaypoints
for i = 1:numClients
targetIdx = (i - 1) * numWaypoints + w;
target = targets(targetIdx, :);
disp(['Sending TARGET to client ', num2str(i), ' (waypoint ', num2str(w), '): ', ... disp(['Sending TARGET to client ', num2str(i), ' (waypoint ', num2str(w), '): ', ...
num2str(target(1)), ',', num2str(target(2)), ',', num2str(target(3))]); num2str(target(1)), ',', num2str(target(2)), ',', num2str(target(3))]);
else
coder.ceval('sendTarget', int32(i), coder.ref(target));
end end
end
% Wait for ACK from all clients
if coder.target('MATLAB')
disp('Waiting for ACK from all clients...'); disp('Waiting for ACK from all clients...');
else
coder.ceval('waitForAllMessageType', int32(numClients), ...
int32(MESSAGE_TYPE.ACK));
end
% Wait for READY from all clients (all arrived at waypoint w)
if coder.target('MATLAB')
disp(['All UAVs arrived at waypoint ', num2str(w)]); disp(['All UAVs arrived at waypoint ', num2str(w)]);
else end
coder.ceval('waitForAllMessageType', int32(numClients), ... else
int32(MESSAGE_TYPE.READY)); % ---- Staggered flyout -----------------------------------------------------
% Separate the two UAVs in TIME so they are never doing the same vertical
% climb at the same moment over their ~10 m-separated pads (which can trip
% the proximity guard on a spurious GPS sample). The UAV that transits
% highest leads by exactly one event.
%
% Events per UAV: 1 = TAKEOFF (25 m), 2 = WP1 (climb at pad), 3 = WP2
% (traverse), 4 = WP3 (descend). At each step the higher UAV does event
% `step` while the lower UAV does event `step-1`; both are dispatched, then
% we collect ACK and then READY (arrival) from each active UAV.
numEvents = double(numWaypoints) + 1; % 3 waypoints + 1 takeoff
for step = 1:(numEvents + 1)
hEvent = step;
lEvent = step - 1;
hActive = (hEvent >= 1) && (hEvent <= numEvents);
lActive = (lEvent >= 1) && (lEvent <= numEvents);
% --- Dispatch this step's event to each active UAV ---
if hActive
if hEvent == 1
coder.ceval('sendMessageType', higherIdx, int32(MESSAGE_TYPE.TAKEOFF));
else
hRow = (double(higherIdx) - 1) * double(numWaypoints) + (hEvent - 1);
hTarget = targets(hRow, :);
coder.ceval('sendTarget', higherIdx, coder.ref(hTarget));
end
end
if lActive
if lEvent == 1
coder.ceval('sendMessageType', lowerIdx, int32(MESSAGE_TYPE.TAKEOFF));
else
lRow = (double(lowerIdx) - 1) * double(numWaypoints) + (lEvent - 1);
lTarget = targets(lRow, :);
coder.ceval('sendTarget', lowerIdx, coder.ref(lTarget));
end
end
% --- Collect ACK (command received) from each active UAV ---
if hActive
coder.ceval('waitForClientMessageType', higherIdx, int32(MESSAGE_TYPE.ACK));
end
if lActive
coder.ceval('waitForClientMessageType', lowerIdx, int32(MESSAGE_TYPE.ACK));
end
% --- Collect READY (arrived at event) from each active UAV ---
if hActive
coder.ceval('waitForClientMessageType', higherIdx, int32(MESSAGE_TYPE.READY));
end
if lActive
coder.ceval('waitForClientMessageType', lowerIdx, int32(MESSAGE_TYPE.READY));
end
end end
end end
% ---- Phase 2: miSim guidance loop ---------------------------------------- % ---- Phase 2: miSim guidance loop ----------------------------------------
% Guidance parameters (adjust here and recompile as needed) % Number of guidance steps comes from maxIter in scenario.csv (scenarioParams(2))
MAX_GUIDANCE_STEPS = int32(100); % number of guidance iterations % so the controller and the agent step-decay schedule stay in sync.
if coder.target('MATLAB')
MAX_GUIDANCE_STEPS = int32(sc.maxIter);
else
MAX_GUIDANCE_STEPS = int32(scenarioParams(2));
end
% Enter guidance mode on all clients % Enter guidance mode on all clients
if ~coder.target('MATLAB') if ~coder.target('MATLAB')
@@ -141,8 +216,8 @@ if ~coder.target('MATLAB')
coder.ceval('sendRequestPositions', int32(numClients)); coder.ceval('sendRequestPositions', int32(numClients));
coder.ceval('recvPositions', int32(numClients), coder.ref(positions), int32(MAX_CLIENTS)); coder.ceval('recvPositions', int32(numClients), coder.ref(positions), int32(MAX_CLIENTS));
else else
% Simulation: seed positions from CSV waypoints so agents don't start at origin % Simulation: seed positions from scenario positions so agents don't start at origin
positions(1:totalLoaded, :) = targets(1:totalLoaded, :); positions(1:totalLoaded, :) = scenarioPositions(1:totalLoaded, :);
end end
guidance_step(positions(1:numClients, :), true, ... guidance_step(positions(1:numClients, :), true, ...
scenarioParams, obstacleMin, obstacleMax, numObstacles); scenarioParams, obstacleMin, obstacleMax, numObstacles);
@@ -197,20 +272,78 @@ if ~coder.target('MATLAB')
end end
% -------------------------------------------------------------------------- % --------------------------------------------------------------------------
% Altitude-staggered return: separate UAVs vertically before issuing RTL, % ---- Altitude-staggered flyback -------------------------------------------
% mirroring the initial positioning stagger so UAVs transit laterally at % Mirror of flyout: stage (climb) → traverse (XY) → settle (descend to pad).
% unique altitudes and cannot collide during the return flight. % Transit altitudes are based on post-guidance positions so the UAV that ended
% higher stays highest, guaranteeing >= TRANSIT_SEP vertical clearance during
% the simultaneous XY traverse.
NUM_RETURN_WP = int32(3);
returnTargets = zeros(MAX_TARGETS, 3);
if ~coder.target('MATLAB') if ~coder.target('MATLAB')
for i = 1:numClients if positions(1, 3) >= positions(2, 3)
transitAlt = TRANSIT_ALT_BASE + (double(i) - 1) * TRANSIT_ALT_STEP; higherRetIdx = int32(1);
target = [positions(i, 1), positions(i, 2), transitAlt]; lowerRetIdx = int32(2);
coder.ceval('sendTarget', int32(i), coder.ref(target)); else
higherRetIdx = int32(2);
lowerRetIdx = int32(1);
end
returnTransitAlt = zeros(1, MAX_CLIENTS);
returnTransitAlt(lowerRetIdx) = positions(lowerRetIdx, 3);
returnTransitAlt(higherRetIdx) = positions(lowerRetIdx, 3) + TRANSIT_SEP;
for iUAV = 1:numClients
retBase = double(iUAV - 1) * double(NUM_RETURN_WP);
returnTargets(retBase + 1, :) = [positions(iUAV,1), positions(iUAV,2), returnTransitAlt(iUAV)];
returnTargets(retBase + 2, :) = [initialPositions(iUAV,1), initialPositions(iUAV,2), returnTransitAlt(iUAV)];
% Final return waypoint: pad XY from the pre-takeoff GPS query, but a fixed
% 25 m up. That query ran while the UAV was on the ground, so its ENU
% up-component is ~0 (often slightly negative) — using it directly commands
% a negative altitude. RTL/LAND perform the actual touchdown from here.
returnTargets(retBase + 3, :) = [initialPositions(iUAV,1), initialPositions(iUAV,2), 25.0];
end
% Staggered flyback: same higher-leads-by-one pattern as the flyout, but
% over the 3 return waypoints only (the UAVs are already airborne, so there
% is no TAKEOFF event). The higher UAV climbs/traverses/descends one event
% ahead of the lower, so they never share the same descent over their pads.
numReturnEvents = double(NUM_RETURN_WP); % 3
for step = 1:(numReturnEvents + 1)
hEvent = step;
lEvent = step - 1;
hActive = (hEvent >= 1) && (hEvent <= numReturnEvents);
lActive = (lEvent >= 1) && (lEvent <= numReturnEvents);
if hActive
hRow = (double(higherRetIdx) - 1) * double(NUM_RETURN_WP) + hEvent;
hTarget = returnTargets(hRow, :);
coder.ceval('sendTarget', higherRetIdx, coder.ref(hTarget));
end
if lActive
lRow = (double(lowerRetIdx) - 1) * double(NUM_RETURN_WP) + lEvent;
lTarget = returnTargets(lRow, :);
coder.ceval('sendTarget', lowerRetIdx, coder.ref(lTarget));
end
if hActive
coder.ceval('waitForClientMessageType', higherRetIdx, int32(MESSAGE_TYPE.ACK));
end
if lActive
coder.ceval('waitForClientMessageType', lowerRetIdx, int32(MESSAGE_TYPE.ACK));
end
if hActive
coder.ceval('waitForClientMessageType', higherRetIdx, int32(MESSAGE_TYPE.READY));
end
if lActive
coder.ceval('waitForClientMessageType', lowerRetIdx, int32(MESSAGE_TYPE.READY));
end
end end
coder.ceval('waitForAllMessageType', int32(numClients), int32(MESSAGE_TYPE.ACK));
coder.ceval('waitForAllMessageType', int32(numClients), int32(MESSAGE_TYPE.READY));
else else
disp('Altitude-staggered return (simulation): UAVs commanded to transit altitudes.'); disp('Altitude-staggered return (simulation): UAVs commanded back to takeoff positions.');
end end
% --------------------------------------------------------------------------
% Send RTL command to all clients % Send RTL command to all clients
for i = 1:numClients for i = 1:numClients
+32 -9
View File
@@ -17,7 +17,8 @@ function nextPositions = guidance_step(currentPositions, isInit, ...
% Flat array of guidance parameters (compiled path). % Flat array of guidance parameters (compiled path).
% On MATLAB path this is ignored; parameters are loaded % On MATLAB path this is ignored; parameters are loaded
% from scenario.csv via initializeFromCsv instead. % from scenario.csv via initializeFromCsv instead.
% Index mapping (1-based): % Index mapping (1-based; authoritative map in
% aerpaw/impl/controller_impl.h):
% 1 timestep 9-12 collisionRadius[1:4] % 1 timestep 9-12 collisionRadius[1:4]
% 2 maxIter 13-16 comRange[1:4] % 2 maxIter 13-16 comRange[1:4]
% 3 minAlt 17-20 alphaDist[1:4] % 3 minAlt 17-20 alphaDist[1:4]
@@ -26,12 +27,20 @@ function nextPositions = guidance_step(currentPositions, isInit, ...
% 6 initialStepSize 29-32 betaTilt[1:4] % 6 initialStepSize 29-32 betaTilt[1:4]
% 7 barrierGain 33-35 domainMin % 7 barrierGain 33-35 domainMin
% 8 barrierExponent 36-38 domainMax % 8 barrierExponent 36-38 domainMax
% 39-40 objectivePos % 39 numObjectiveComponents
% 41-44 objectiveVar (2x2, col-major) % 40-43 objectivePos [x1,y1,x2,y2]
% 45 sensorPerformanceMinimum % 44-51 objectiveVar [per component]
% 46 useDoubleIntegrator % 52 sensorPerformanceMinimum
% 47 dampingCoeff % 53 useDoubleIntegrator
% 48 useFixedTopology % 54 dampingCoeff
% 55 useFixedTopology
% 56-59 txPower[1:4]
% 60 useSinrComms
% 61 sinrThreshold (dB)
% 62 pathLossExponent
% 63 ambientTemp (K)
% 64 centerFreq (Hz)
% 65 bandwidth (Hz)
% obstacleMin (MAX_OBSTACLES × 3) double column-major obstacle corners (compiled path) % obstacleMin (MAX_OBSTACLES × 3) double column-major obstacle corners (compiled path)
% obstacleMax (MAX_OBSTACLES × 3) double % obstacleMax (MAX_OBSTACLES × 3) double
% numObstacles (1,1) int32 actual obstacle count % numObstacles (1,1) int32 actual obstacle count
@@ -101,6 +110,13 @@ if isInit
USE_DOUBLE_INTEGRATOR = logical(scenarioParams(53)); USE_DOUBLE_INTEGRATOR = logical(scenarioParams(53));
DAMPING_COEFF = scenarioParams(54); DAMPING_COEFF = scenarioParams(54);
USE_FIXED_TOPOLOGY = logical(scenarioParams(55)); USE_FIXED_TOPOLOGY = logical(scenarioParams(55));
TX_POWER_VEC = scenarioParams(56:59); % per-UAV [1:MAX_CLIENTS] (W)
USE_SINR_COMMS = logical(scenarioParams(60));
SINR_THRESHOLD = scenarioParams(61); % dB
PATH_LOSS_EXPONENT = scenarioParams(62);
AMBIENT_TEMP = scenarioParams(63); % Kelvin
CENTER_FREQ = scenarioParams(64); % Hz
BANDWIDTH = scenarioParams(65); % Hz
% --- Build domain geometry --- % --- Build domain geometry ---
dom = rectangularPrism; dom = rectangularPrism;
@@ -141,6 +157,7 @@ if isInit
ag = agent; ag = agent;
ag = ag.initialize(pos, geom, sensor, COMMS_RANGE_VEC(ii), MAX_ITER, ... ag = ag.initialize(pos, geom, sensor, COMMS_RANGE_VEC(ii), MAX_ITER, ...
INITIAL_STEP_SIZE, sprintf("UAV %d", ii)); INITIAL_STEP_SIZE, sprintf("UAV %d", ii));
ag.txPower = TX_POWER_VEC(ii); % SINR comms model transmit power (W)
agentList{ii} = ag; agentList{ii} = ag;
end end
@@ -158,7 +175,8 @@ if isInit
sim = miSim; sim = miSim;
sim = sim.initialize(dom, agentList, BARRIER_GAIN, BARRIER_EXPONENT, ... sim = sim.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); USE_DOUBLE_INTEGRATOR, DAMPING_COEFF, USE_FIXED_TOPOLOGY, false, ...
USE_SINR_COMMS, SINR_THRESHOLD, PATH_LOSS_EXPONENT, AMBIENT_TEMP, CENTER_FREQ, BANDWIDTH);
end end
% On the init call return current positions unchanged % On the init call return current positions unchanged
@@ -187,8 +205,13 @@ else
% 2. Advance timestep counter % 2. Advance timestep counter
sim.timestepIndex = sim.timestepIndex + 1; sim.timestepIndex = sim.timestepIndex + 1;
% 3. Update communications topology (Lesser Neighbour Assignment) % 3. Update communications topology (Lesser Neighbour Assignment).
% Refresh the adjacency graph from current positions first so connectivity
% (fixed-radius or SINR) tracks the agents as they move; otherwise the graph
% would be frozen at init on the compiled path (the MATLAB sim refreshes it
% in @miSim/run.m). Skip when topology is fixed.
if ~sim.useFixedTopology if ~sim.useFixedTopology
sim = sim.updateAdjacency();
sim = sim.lesserNeighbor(); sim = sim.lesserNeighbor();
end end
+93 -2
View File
@@ -238,8 +238,8 @@ int loadScenario(const char* filename, double* params) {
strncpy(copy, line, sizeof(copy) - 1); strncpy(copy, line, sizeof(copy) - 1);
copy[sizeof(copy) - 1] = '\0'; copy[sizeof(copy) - 1] = '\0';
char* fields[32]; char* fields[40];
int nf = splitCSVRow(copy, fields, 32); int nf = splitCSVRow(copy, fields, 40);
if (nf < 26) { if (nf < 26) {
fprintf(stderr, "loadScenario: expected >=26 columns, got %d\n", nf); fprintf(stderr, "loadScenario: expected >=26 columns, got %d\n", nf);
return 0; return 0;
@@ -376,8 +376,71 @@ int loadScenario(const char* filename, double* params) {
params[54] = atof(trimField(tmp)); params[54] = atof(trimField(tmp));
} }
// ---- SINR communications model (columns 26–30; optional) ----
// Defaults mirror @miSim/initializeFromCsv.m so a CSV predating the SINR
// model still loads (useSinrComms stays 0 -> fixed-radius path is used).
// txPower[1:4] -> params[55:58]; useSinrComms -> 59;
// sinrThreshold (dB) -> 60; pathLossExponent -> 61; ambientTemp (K) -> 62;
// centerFreq (Hz) -> 63; bandwidth (Hz) -> 64.
params[55] = 0.1; params[56] = 0.1; params[57] = 0.1; params[58] = 0.1;
params[59] = 0.0; // useSinrComms
params[60] = 0.0; // sinrThreshold (dB)
params[61] = 2.0; // pathLossExponent
params[62] = 290.0; // ambientTemp (K)
params[63] = 2.4e9; // centerFreq (Hz)
params[64] = 20e6; // bandwidth (Hz)
// txPower: column 26 (per-UAV, up to MAX_CLIENTS_PER_PARAM values, zero-padded)
if (nf > 26) {
char tmp[256]; strncpy(tmp, fields[26], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
char* t = trimField(tmp);
double vals[4] = {0, 0, 0, 0};
int count = 0;
char* tok = strtok(t, ",");
while (tok && count < MAX_CLIENTS_PER_PARAM) {
vals[count++] = atof(tok);
tok = strtok(nullptr, ",");
}
if (count > 0) {
for (int k = 0; k < MAX_CLIENTS_PER_PARAM; k++) params[55 + k] = vals[k];
}
}
// useSinrComms: column 27
if (nf > 27) {
char tmp[64]; strncpy(tmp, fields[27], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[59] = atof(trimField(tmp));
}
// sinrThreshold: column 28 (dB)
if (nf > 28) {
char tmp[64]; strncpy(tmp, fields[28], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[60] = atof(trimField(tmp));
}
// pathLossExponent: column 29
if (nf > 29) {
char tmp[64]; strncpy(tmp, fields[29], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[61] = atof(trimField(tmp));
}
// ambientTemp: column 30 (K)
if (nf > 30) {
char tmp[64]; strncpy(tmp, fields[30], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[62] = atof(trimField(tmp));
}
// centerFreq: column 31 (Hz)
if (nf > 31) {
char tmp[64]; strncpy(tmp, fields[31], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[63] = atof(trimField(tmp));
}
// bandwidth: column 32 (Hz)
if (nf > 32) {
char tmp[64]; strncpy(tmp, fields[32], sizeof(tmp) - 1); tmp[sizeof(tmp)-1] = '\0';
params[64] = atof(trimField(tmp));
}
printf("Loaded scenario: domain [%g,%g,%g] to [%g,%g,%g], %d objective component(s)\n", printf("Loaded scenario: domain [%g,%g,%g] to [%g,%g,%g], %d objective component(s)\n",
params[32], params[33], params[34], params[35], params[36], params[37], (int)params[38]); params[32], params[33], params[34], params[35], params[36], params[37], (int)params[38]);
printf("Comms model: %s (SINR threshold %g dB, path-loss exp %g, T=%g K, f_c=%g Hz, BW=%g Hz)\n",
params[59] != 0.0 ? "SINR" : "fixed-radius", params[60], params[61], params[62], params[63], params[64]);
return 1; return 1;
} }
@@ -512,6 +575,7 @@ static const char* messageTypeName(uint8_t msgType) {
case 6: return "GUIDANCE_TOGGLE"; case 6: return "GUIDANCE_TOGGLE";
case 7: return "REQUEST_POSITION"; case 7: return "REQUEST_POSITION";
case 8: return "POSITION"; case 8: return "POSITION";
case 9: return "TAKEOFF";
default: return "UNKNOWN"; default: return "UNKNOWN";
} }
} }
@@ -614,6 +678,33 @@ int waitForAllMessageType(int numClients, int expectedType) {
return 1; return 1;
} }
// Wait for a specific message type from a SINGLE client (blocking).
// Used by the staggered flyout/flyback, where the controller waits on one UAV
// while the other is still flying its own (offset) event. Skips past any
// unexpected byte and returns 0 on disconnect/error, 1 on success.
int waitForClientMessageType(int clientId, int expectedType) {
if (clientId <= 0 || clientId > (int)clientSockets.size()) return 0;
int sock = clientSockets[clientId - 1];
uint8_t expected = (uint8_t)expectedType;
while (true) {
uint8_t msg;
int len = recv(sock, &msg, 1, MSG_WAITALL);
if (len <= 0) {
std::cerr << logPrefix() << "waitForClientMessageType: client " << clientId
<< " disconnected while waiting for " << messageTypeName(expected) << "\n";
return 0;
}
if (msg == expected) {
std::cout << logPrefix() << "Received " << messageTypeName(expected)
<< " from client " << clientId << "\n";
return 1;
}
std::cerr << logPrefix() << "Unexpected " << messageTypeName(msg) << " from client "
<< clientId << " (expected " << messageTypeName(expected) << ")\n";
}
}
// Broadcast GUIDANCE_TOGGLE to all clients // Broadcast GUIDANCE_TOGGLE to all clients
void sendGuidanceToggle(int numClients) { void sendGuidanceToggle(int numClients) {
for (int i = 1; i <= numClients; i++) { for (int i = 1; i <= numClients; i++) {
+11 -1
View File
@@ -34,7 +34,14 @@ int loadTargets(const char* filename, double* targets, int maxClients);
// 52 useDoubleIntegrator (0=single-integrator, 1=double-integrator) // 52 useDoubleIntegrator (0=single-integrator, 1=double-integrator)
// 53 dampingCoeff // 53 dampingCoeff
// 54 useFixedTopology (0=dynamic lesser-neighbor, 1=fixed) // 54 useFixedTopology (0=dynamic lesser-neighbor, 1=fixed)
#define NUM_SCENARIO_PARAMS 55 // 55-58 txPower[1:4] (per-UAV transmit power, Watts; SINR comms model)
// 59 useSinrComms (0=fixed-radius comms, 1=SINR/path-loss comms)
// 60 sinrThreshold (SINR connectivity/CBF threshold, dB)
// 61 pathLossExponent (path-loss exponent n)
// 62 ambientTemp (ambient temperature, K; thermal noise)
// 63 centerFreq (carrier frequency, Hz; path-loss offset)
// 64 bandwidth (channel bandwidth, Hz; thermal noise)
#define NUM_SCENARIO_PARAMS 65
#define MAX_CLIENTS_PER_PARAM 4 #define MAX_CLIENTS_PER_PARAM 4
// Maximum number of obstacles (upper bound for pre-allocated arrays). // Maximum number of obstacles (upper bound for pre-allocated arrays).
#define MAX_OBSTACLES 8 #define MAX_OBSTACLES 8
@@ -61,6 +68,9 @@ int loadObstacles(const char* filename,
int sendMessageType(int clientId, int msgType); int sendMessageType(int clientId, int msgType);
int sendTarget(int clientId, const double* coords); int sendTarget(int clientId, const double* coords);
int waitForAllMessageType(int numClients, int expectedType); int waitForAllMessageType(int numClients, int expectedType);
// Block until a SINGLE client sends expectedType (used by the staggered
// flyout/flyback to wait on one UAV while the other is mid-flight).
int waitForClientMessageType(int clientId, int expectedType);
// Guidance loop operations // Guidance loop operations
void setGuidanceStep(int step, int totalSteps); // call at the top of each guidance iteration void setGuidanceStep(int step, int totalSteps); // call at the top of each guidance iteration
+13 -2
View File
@@ -78,6 +78,11 @@ function [f, G] = plotGpsLogs(logDirs, seaToGroundLevel, plotWholeFlight)
d = d(~isnan(d)); d = d(~isnan(d));
fprintf("Minimum distance between agents %d and %d is %2.3f\n", ii, jj, min(d)); fprintf("Minimum distance between agents %d and %d is %2.3f\n", ii, jj, min(d));
figure;
plot(G{1}.Timestamp(1:length(d)), d);
xlabel("Time"); ylabel("Distance"); title("UAV0 and UAV1 distance"); grid("on");
fprintf("Average distance between agents %d and %d is %2.3f\n", ii, jj, mean(d));
fprintf("Median distance between agents %d and %d is %2.3f\n", ii, jj, median(d));
if min(d) < 6 if min(d) < 6
warning("Minimum distance between agents %d and %d of %2.3f is questionable for AERPAW", ii, jj, min(d)); warning("Minimum distance between agents %d and %d of %2.3f is questionable for AERPAW", ii, jj, min(d));
end end
@@ -86,6 +91,7 @@ function [f, G] = plotGpsLogs(logDirs, seaToGroundLevel, plotWholeFlight)
% Plot recorded trajectory over specified range of indices % Plot recorded trajectory over specified range of indices
geoplot3(gf, G{ii}.Latitude(startIdx:stopIdx), G{ii}.Longitude(startIdx:stopIdx), G{ii}.Altitude(startIdx:stopIdx) + seaToGroundLevel, c(mod(ii, length(c))), 'LineWidth', 2, "MarkerSize", 5); geoplot3(gf, G{ii}.Latitude(startIdx:stopIdx), G{ii}.Longitude(startIdx:stopIdx), G{ii}.Altitude(startIdx:stopIdx) + seaToGroundLevel, c(mod(ii, length(c))), 'LineWidth', 2, "MarkerSize", 5);
end end
fprintf("Flight duration is %s\n", string(G{1}.Timestamp(end) - G{1}.Timestamp(1), 'mm:ss'));
% Plot domain % Plot domain
altOffset = 1; % to avoid clipping into the ground when displayed altOffset = 1; % to avoid clipping into the ground when displayed
@@ -102,9 +108,14 @@ function [f, G] = plotGpsLogs(logDirs, seaToGroundLevel, plotWholeFlight)
geoplot3(gf, [domain(1, 1), domain(2, 1), domain(2, 1), domain(1, 1), domain(1, 1)], [domain(1, 2), domain(1, 2), domain(2, 2), domain(2, 2), domain(1, 2)], repmat(domain(1, 3) + altOffset + floorAlt, 1, 5), 'LineWidth', 3, 'Color', 'r'); geoplot3(gf, [domain(1, 1), domain(2, 1), domain(2, 1), domain(1, 1), domain(1, 1)], [domain(1, 2), domain(1, 2), domain(2, 2), domain(2, 2), domain(1, 2)], repmat(domain(1, 3) + altOffset + floorAlt, 1, 5), 'LineWidth', 3, 'Color', 'r');
% Plot objective % Plot objective
objectivePos = [params.objectivePos, 0]; if length(params.objectivePos) > 2
objectivePos = reshape(params.objectivePos, [2, length(params.objectivePos) / 2])';
end
objectivePos = [objectivePos, zeros(size(objectivePos, 1), 1)];
llaObj = enu2lla(objectivePos, lla0, "flat"); llaObj = enu2lla(objectivePos, lla0, "flat");
geoplot3(gf, [llaObj(1), llaObj(1)], [llaObj(2), llaObj(2)], [llaObj(3), domain(2, 3)], 'LineWidth', 3, "Color", 'y'); for ii = 1:size(llaObj, 1)
geoplot3(gf, [llaObj(ii, 1), llaObj(ii, 1)], [llaObj(ii, 2), llaObj(ii, 2)], [llaObj(ii, 3), domain(2, 3)], 'LineWidth', 3, "Color", 'y');
end
% Plot obstacles % Plot obstacles
for ii = 1:params.numObstacles for ii = 1:params.numObstacles
+30 -4
View File
@@ -1,5 +1,5 @@
%% Plot AERPAW logs (trajectory, radio) %% Plot AERPAW logs (trajectory, radio)
resultsPath = fullfile(matlab.project.rootProject().RootFolder, "sandbox", "two_around_wall"); % Define path to results copied from AERPAW platform resultsPath = fullfile(matlab.project.rootProject().RootFolder, "sandbox", "columns_simulated"); % Define path to results copied from AERPAW platform
% Check timeline in controller logs % Check timeline in controller logs
controller = controllerAnalysis(resultsPath); controller = controllerAnalysis(resultsPath);
@@ -25,7 +25,14 @@ makeVideo = true;
% Define scenario according to CSV specification % Define scenario according to CSV specification
domain = rectangularPrism; domain = rectangularPrism;
domain = domain.initialize([params.domainMin; params.domainMax], REGION_TYPE.DOMAIN, "Domain"); domain = domain.initialize([params.domainMin; params.domainMax], REGION_TYPE.DOMAIN, "Domain");
domain.objective = domain.objective.initialize(objectiveFunctionWrapper(params.objectivePos, reshape(params.objectiveVar, [1, 2 2])), domain, params.discretizationStep, params.protectedRange, params.sensorPerformanceMinimum); if length(params.objectiveVar) > 4 && length(params.objectivePos) > 2
objectiveSigma = permute(reshape(params.objectiveVar, [length(params.objectiveVar)/4 2 2]), [3 1 2]);
objectivePos = reshape(params.objectivePos, [length(params.objectivePos)/2, 2])';
else
objectiveSigma = reshape(params.objectiveVar, [1, 2, 2]);
objectivePos = params.objectivePos;
end
domain.objective = domain.objective.initialize(objectiveFunctionWrapper(objectivePos, objectiveSigma), domain, params.discretizationStep, params.protectedRange, params.sensorPerformanceMinimum);
agents = cell(size(params.initialPositions, 2) / 3, 1); agents = cell(size(params.initialPositions, 2) / 3, 1);
for ii = 1:size(agents, 1) for ii = 1:size(agents, 1)
@@ -38,6 +45,9 @@ for ii = 1:size(agents, 1)
collisionGeometry = collisionGeometry.initialize(params.initialPositions((((ii - 1) * 3) + 1):(ii * 3)), params.collisionRadius(ii), REGION_TYPE.COLLISION, sprintf("Agent %d collision geometry", ii)); collisionGeometry = collisionGeometry.initialize(params.initialPositions((((ii - 1) * 3) + 1):(ii * 3)), params.collisionRadius(ii), REGION_TYPE.COLLISION, sprintf("Agent %d collision geometry", ii));
agents{ii} = agents{ii}.initialize(params.initialPositions((((ii - 1) * 3) + 1):(ii * 3)), collisionGeometry, sensorModel, params.comRange(ii), params.maxIter, params.initialStepSize, 5.0, sprintf("Agent %d", ii), plotCommsGeometry); agents{ii} = agents{ii}.initialize(params.initialPositions((((ii - 1) * 3) + 1):(ii * 3)), collisionGeometry, sensorModel, params.comRange(ii), params.maxIter, params.initialStepSize, 5.0, sprintf("Agent %d", ii), plotCommsGeometry);
if isfield(params, 'txPower')
agents{ii}.txPower = params.txPower(ii); % SINR comms model transmit power (W)
end
end end
% Create obstacles % Create obstacles
@@ -47,9 +57,22 @@ for ii = 1:size(obstacles, 1)
obstacles{ii} = obstacles{ii}.initialize([params.obstacleMin((((ii - 1) * 3) + 1):(ii * 3)); params.obstacleMax((((ii - 1) * 3) + 1):(ii * 3))], "OBSTACLE", sprintf("Obstacle %d", ii)); obstacles{ii} = obstacles{ii}.initialize([params.obstacleMin((((ii - 1) * 3) + 1):(ii * 3)); params.obstacleMax((((ii - 1) * 3) + 1):(ii * 3))], "OBSTACLE", sprintf("Obstacle %d", ii));
end end
% Set up simulation % SINR communications model parameters (optional CSV columns; default to the
% fixed-radius path so existing scenarios are unaffected)
if isfield(params, 'useSinrComms'); useSinrComms = logical(params.useSinrComms); else; useSinrComms = false; end
if isfield(params, 'sinrThreshold'); sinrThreshold = params.sinrThreshold; else; sinrThreshold = 0; end
if isfield(params, 'pathLossExponent'); pathLossExponent = params.pathLossExponent; else; pathLossExponent = 2.0; end
if isfield(params, 'ambientTemp'); ambientTemp = params.ambientTemp; else; ambientTemp = 290.0; end
if isfield(params, 'centerFreq'); centerFreq = params.centerFreq; else; centerFreq = 2.4e9; end
if isfield(params, 'bandwidth'); bandwidth = params.bandwidth; else; bandwidth = 20e6; end
% Set up simulation. The four args after makeVideo (useDoubleIntegrator,
% dampingCoeff, useFixedTopology, optimizeSensorPointing) are kept at their
% prior effective defaults to preserve existing analysis behavior; only the
% SINR comms parameters are threaded through from the scenario.
sim = miSim; sim = miSim;
sim = sim.initialize(domain, agents, params.barrierGain, params.barrierExponent, params.minAlt, params.timestep, params.maxIter, obstacles, makePlots, makeVideo); sim = sim.initialize(domain, agents, params.barrierGain, params.barrierExponent, params.minAlt, params.timestep, params.maxIter, obstacles, makePlots, makeVideo, ...
false, 2.0, false, false, useSinrComms, sinrThreshold, pathLossExponent, ambientTemp, centerFreq, bandwidth);
% Save simulation parameters to output file % Save simulation parameters to output file
sim.writeInits(); sim.writeInits();
@@ -57,6 +80,9 @@ sim.writeInits();
% Run % Run
sim = sim.run(); sim = sim.run();
% Save results
sim = sim.teardown();
%% Plot AERPAW trajectory logs onto simulated result for comparison %% Plot AERPAW trajectory logs onto simulated result for comparison
% Duplicate plot to overlay with logged trajectories % Duplicate plot to overlay with logged trajectories
comparison = figure; comparison = figure;
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Info>
<Category UUID="FileClassCategory">
<Label UUID="design"/>
</Category>
</Info>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Info location="okabeIto.m" type="File"/>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Info>
<Category UUID="FileClassCategory">
<Label UUID="design"/>
</Category>
</Info>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Info location="exportThesisFig.m" type="File"/>
-1
View File
@@ -42,7 +42,6 @@ classdef parametricTestSuite < matlab.unittest.TestCase
objectivePos = params.objectivePos; objectivePos = params.objectivePos;
end end
tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper(objectivePos, objectiveSigma), tc.domain, params.discretizationStep, params.protectedRange, params.sensorPerformanceMinimum, objectivePos, objectiveSigma); tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper(objectivePos, objectiveSigma), tc.domain, params.discretizationStep, params.protectedRange, params.sensorPerformanceMinimum, objectivePos, objectiveSigma);
agents = cell(size(params.initialPositions, 2) / 3, 1); agents = cell(size(params.initialPositions, 2) / 3, 1);
for ii = 1:size(agents, 1) for ii = 1:size(agents, 1)
agents{ii} = agent; agents{ii} = agent;
+421 -10
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);
@@ -57,6 +57,7 @@ classdef test_miSim < matlab.unittest.TestCase
opticalPartitioningMin = 1e-6; opticalPartitioningMin = 1e-6;
% rfSensor % rfSensor
P_TX = 1e-3; % Transmit power (Watts) P_TX = 1e-3; % Transmit power (Watts)
T_0 = 300; % Temperature (K)
BW = 20e6; % Bandwidth (Hz) BW = 20e6; % Bandwidth (Hz)
f_c = 3e9; % Center frequency (Hz) f_c = 3e9; % Center frequency (Hz)
G_RX_dBi = 3; % Receiving Antenna Gain (dBi) G_RX_dBi = 3; % Receiving Antenna Gain (dBi)
@@ -104,6 +105,13 @@ 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 SINR-aware routing
% overlay on this random scenario. Routing requires SINR comms, so
% the flag switches the whole SINR stack on (agents keep their
% default 0.1 W txPower); the permissive -30 dB threshold keeps
% random geometries fully connected.
useRoutingTopology = true;
% randomly create obstacles % randomly create obstacles
nGeom = tc.minNumObstacles + randi(tc.maxNumObstacles - tc.minNumObstacles); nGeom = tc.minNumObstacles + randi(tc.maxNumObstacles - tc.minNumObstacles);
tc.obstacles = cell(nGeom, 1); tc.obstacles = cell(nGeom, 1);
@@ -239,7 +247,11 @@ classdef test_miSim < matlab.unittest.TestCase
end end
% Initialize the simulation % Initialize the simulation
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); 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);
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);
end
end end
function miSim_run_rf_sensor(tc) function miSim_run_rf_sensor(tc)
% randomly create obstacles % randomly create obstacles
@@ -323,7 +335,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize candidate agent sensor model % Initialize candidate agent sensor model
tc.sensor = rfSensor; tc.sensor = rfSensor;
tilt = 0; azimuth = 0; tilt = 0; azimuth = 0;
tc.sensor = tc.sensor.initialize(tc.P_TX * 1 + rand * 4, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent + randi(100), tilt, azimuth, tc.lossExponent); 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 candidate agent % Initialize candidate agent
newAgent = tc.agents{ii}.initialize(candidatePos, candidateGeometry, tc.sensor, tc.commsRanges(ii), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize); newAgent = tc.agents{ii}.initialize(candidatePos, candidateGeometry, tc.sensor, tc.commsRanges(ii), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
@@ -390,6 +402,13 @@ 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 routing
% overlay on this random scenario. Routing requires SINR comms, so
% the flag switches the whole SINR stack on (agents keep their
% default 0.1 W txPower); the permissive -30 dB threshold keeps
% random geometries fully connected.
useRoutingTopology = true;
% randomly create obstacles % randomly create obstacles
nGeom = tc.minNumObstacles + randi(tc.maxNumObstacles - tc.minNumObstacles); nGeom = tc.minNumObstacles + randi(tc.maxNumObstacles - tc.minNumObstacles);
tc.obstacles = cell(nGeom, 1); tc.obstacles = cell(nGeom, 1);
@@ -526,7 +545,11 @@ classdef test_miSim < matlab.unittest.TestCase
end end
% Initialize the simulation % Initialize the simulation
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); 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);
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);
end
% Write out initialization state % Write out initialization state
tc.testClass.writeInits(); tc.testClass.writeInits();
@@ -651,7 +674,7 @@ classdef test_miSim < matlab.unittest.TestCase
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 % make basic sensing objective
tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([7, 6]), tc.domain, tc.discretizationStep, tc.protectedRange, tc.sinrPartitioningMin, [7, 6]); tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([8, 3], reshape([15, 4; 4, 15], [1 2 2])), tc.domain, tc.discretizationStep, tc.protectedRange, tc.sinrPartitioningMin, [7, 6]);
% Initialize agent collision geometry % Initialize agent collision geometry
tc.agents = {agent}; tc.agents = {agent};
@@ -659,7 +682,7 @@ classdef test_miSim < matlab.unittest.TestCase
geometry1 = geometry1.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], tc.collisionRanges(1), REGION_TYPE.COLLISION); geometry1 = geometry1.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], tc.collisionRanges(1), REGION_TYPE.COLLISION);
tc.sensor = rfSensor; tc.sensor = rfSensor;
tc.sensor = tc.sensor.initialize(tc.P_TX, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent, 45, 45, tc.lossExponent); tc.sensor = tc.sensor.initialize(tc.P_TX, tc.T_0, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent, 45, 45, tc.lossExponent);
% Initialize agents % Initialize agents
tc.maxIter = 75; tc.maxIter = 75;
@@ -717,7 +740,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize agent sensor model % Initialize agent sensor model
tc.sensor = rfSensor; tc.sensor = rfSensor;
tc.sensor = tc.sensor.initialize(tc.P_TX, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent, 0, 0, tc.lossExponent); tc.sensor = tc.sensor.initialize(tc.P_TX, tc.T_0, tc.BW, tc.f_c, tc.G_RX_dBi, tc.beamwidthExponent, 0, 0, tc.lossExponent);
% Initialize agents % Initialize agents
tc.maxIter = 75; tc.maxIter = 75;
@@ -814,7 +837,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Run the simulation % Run the simulation
tc.testClass.run(); tc.testClass.run();
end end
function test_communications_constraint(tc) function test_communications_constraint_SINR_threshold(tc)
% No obstacles % No obstacles
% Fixed two agents initial conditions % Fixed two agents initial conditions
% Negligible collision geometries % Negligible collision geometries
@@ -842,6 +865,394 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize obstacles % Initialize obstacles
tc.obstacles = {}; tc.obstacles = {};
% Initialize agents
tc.maxIter = 50;
tc.commsRanges = 4 * ones(size(tc.agents)); % unused in SINR mode; kept for parity with the fixed-radius variant
tc.agents{1} = tc.agents{1}.initialize(tc.domain.center + d, geometry1, tc.sensor, tc.commsRanges(1), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
tc.agents{2} = tc.agents{2}.initialize(tc.domain.center - d, geometry2, tc.sensor, tc.commsRanges(2), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
% SINR communications model parameters. With isotropic links and only
% two agents (no interference) the link is noise-limited:
% SINR = Ptx / ((4*pi*fc/c)^2 * d^n * kB*T*B)
% Pick the threshold so the SINR connectivity boundary sits at an
% effective range of ~4 m, mirroring the commsRange used by the
% fixed-radius variant of this test (so the two agents still cannot
% reach their objectives without breaking connectivity).
useSinrComms = true;
txPower = [0.01, 0.01]; % transmit power per agent (W)
pathLossExponent = 2.0;
ambientTemp = 290; % K
centerFreq = 2.4e9; % Hz
bandwidth = 20e6; % Hz
effRange = 4.0; % desired effective comms range (m)
K_pl = (4 * pi * centerFreq / 3e8)^2; % free-space path-loss reference
noiseW = 1.380649e-23 * ambientTemp * bandwidth; % thermal noise (W)
sinrThreshold = 10 * log10(mean(txPower) / (K_pl * effRange^pathLossExponent * noiseW)); % dB
tc.agents{1}.txPower = txPower(1);
tc.agents{2}.txPower = txPower(2);
% Initialize the simulation in SINR comms mode
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);
% Run the simulation
tc.testClass = tc.testClass.run();
end
function test_communications_constraint_SINR_threshold_3_agents(tc)
% No obstacles
% Fixed three agents initial conditions
% Negligible collision geometries
% Non-standard domain with two objectives that will try to pull the
% agents apart
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
tc.agents = {agent; agent; agent;};
tc.collisionRanges = .25 * ones(size(tc.agents));
d = [2.0, 0, 0]; % 2 m collinear spacing (above the 1 m path-loss floor)
geometry1 = spherical;
geometry2 = geometry1;
geometry3 = geometry1;
geometry1 = geometry1.initialize(tc.domain.center - d, tc.collisionRanges(1), REGION_TYPE.COLLISION);
geometry2 = geometry2.initialize(tc.domain.center, tc.collisionRanges(2), REGION_TYPE.COLLISION);
geometry3 = geometry3.initialize(tc.domain.center + d, 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; kept for parity with the fixed-radius variant
tc.agents{1} = tc.agents{1}.initialize(tc.domain.center - d, 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 + d, geometry3, tc.sensor, tc.commsRanges(3), tc.maxIter, tc.initialStepSize, tc.initialMaxAngleStepSize);
% SINR communications model parameters. Three collinear, equally
% spaced agents with homogeneous transmit power form an
% INTERFERENCE-limited network (co-agent interference swamps thermal
% noise), unlike the two-agent noise-limited case. Two structural
% facts set the connectivity boundaries:
% * A chain link (1-2 or 2-3) is limited by the MIDDLE agent, which
% receives one end while the other end interferes at the SAME
% range: signal = interference -> 0 dB.
% * The end-to-end (1,3) link has its transmitter at 2x the range
% while the middle agent interferes at 1x: (1/2)^n -> -6.02 dB.
% Homogeneous power is required for the symmetry: unequal power would
% push one chain link's middle-receiver SINR below 0 dB and break it.
% Placing the threshold between the two boundaries (a couple dB above
% the -6 dB direct link, a few dB below the 0 dB chain links) makes
% exactly the two adjacent links {1-2, 2-3} feasible and rejects 1-3,
% giving the desired 1 -> 2 -> 3 line. The +3 dB CBF padding then
% still lands below the 0 dB chain binding, so the QP starts feasible.
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); % (1,3) link SINR: (1/2)^n = -6.02 dB
sinrThreshold = endBindingDb + 2.0; % ~ -4.02 dB: 2 dB above the rejected
% 1-3 link, ~4 dB below the 0 dB chain
tc.agents{1}.txPower = txPower(1);
tc.agents{2}.txPower = txPower(2);
tc.agents{3}.txPower = txPower(3);
% Initialize the simulation in SINR comms mode
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);
% The initial SINR topology must be exactly the 1 -> 2 -> 3 line:
% two links (1-2 and 2-3), with the direct 1-3 link rejected.
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_convergence_demo(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 route to base 1 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 = sigmoidSensor;
tc.sensor = tc.sensor.initialize(tc.minDimension / 2, 3, 15, 3);
% 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
% SINR communications model. The threshold must sit >= 3 dB
% below the worst link CALSN may select, because the CBF holds
% every maintained link at threshold + 3 dB in BOTH directions:
% 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.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
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 route to base 1 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
% SINR communications model. The threshold must sit >= 3 dB
% below the worst link CALSN may select, because the CBF holds
% every maintained link at threshold + 3 dB in BOTH directions:
% 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
function test_communications_constraint_fixed_radius(tc)
% No obstacles
% Fixed two agents initial conditions
% Negligible collision geometries
% Non-standard domain with two objectives that will try to pull the
% agents apart
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
tc.agents = {agent; agent;};
tc.collisionRanges = .25 * ones(size(tc.agents));
d = [1, 0, 0];
geometry1 = spherical;
geometry2 = geometry1;
geometry1 = geometry1.initialize(tc.domain.center + d, tc.collisionRanges(1), REGION_TYPE.COLLISION);
geometry2 = geometry2.initialize(tc.domain.center - d, tc.collisionRanges(2), 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 % Initialize agents
tc.maxIter = 50; tc.maxIter = 50;
tc.commsRanges = 4 * ones(size(tc.agents)); % defined such that they cannot reach their objective without breaking connectivity tc.commsRanges = 4 * ones(size(tc.agents)); % defined such that they cannot reach their objective without breaking connectivity
@@ -850,7 +1261,7 @@ classdef test_miSim < matlab.unittest.TestCase
% Initialize the simulation % Initialize the simulation
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);
% Run the simulation % Run the simulation
tc.testClass = tc.testClass.run(); tc.testClass = tc.testClass.run();
end end
+19 -13
View File
@@ -15,28 +15,30 @@ classdef test_rfSensor < matlab.unittest.TestCase
function plot_RSS(tc) function plot_RSS(tc)
% Plot sensor performance with no sources of interference % Plot sensor performance with no sources of interference
P_TX = 1e-3; % Transmit power (Watts) P_TX = 1e-3; % Transmit power (Watts)
T_0 = 300; % Temperature (K)
BW = 20e6; % Bandwidth (Hz) BW = 20e6; % Bandwidth (Hz)
f_c = 2e9; % Center frequency (Hz) f_c = 2e9; % Center frequency (Hz)
G_RX_dBi = 3; % Receiving Antenna Gain (dBi) G_RX_dBi = 3; % Receiving Antenna Gain (dBi)
beamwidthExponent = 6; beamwidthExponent = 6;
lossExponent = 2; lossExponent = 2;
tc.testClass = tc.testClass.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent); tc.testClass = tc.testClass.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent);
tc.testClass.plotParameters(); tc.testClass.plotParameters();
end end
function plot_SNR(tc) function plot_SNR(tc)
% Plot sensor performance with no sources of interference % Plot sensor performance with no sources of interference
P_TX = 1e-3; % Transmit power (Watts) P_TX = 1e-3; % Transmit power (Watts)
T_0 = 300; % Temperature (K)
BW = 20e6; % Bandwidth (Hz) BW = 20e6; % Bandwidth (Hz)
f_c = 2e9; % Center frequency (Hz) f_c = 2e9; % Center frequency (Hz)
G_RX_dBi = 3; % Receiving Antenna Gain (dBi) G_RX_dBi = 3; % Receiving Antenna Gain (dBi)
beamwidthExponent = 6; beamwidthExponent = 6;
lossExponent = 2; lossExponent = 2;
tc.testClass = tc.testClass.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 30, 135, lossExponent); tc.testClass = tc.testClass.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 30, 135, lossExponent);
altitude = 30; altitude = 20;
% Boresight azimuth=135° (between +X at 90° and -Y at 180°) → hotspot at +X,-Y. % Boresight azimuth=135° (between +X at 90° and -Y at 180°) → hotspot at +X,-Y.
% SNR at (5,-5) should be higher than at (5,+5). % SNR at (5,-5) should be higher than at (5,+5).
@@ -51,13 +53,14 @@ classdef test_rfSensor < matlab.unittest.TestCase
function plot_SINR_one_interferer(tc) function plot_SINR_one_interferer(tc)
% Plot sensor performance with no sources of interference % Plot sensor performance with no sources of interference
P_TX = 1e-3; % Transmit power (Watts) P_TX = 1e-3; % Transmit power (Watts)
T_0 = 300; % Temperature (K)
BW = 20e6; % Bandwidth (Hz) BW = 20e6; % Bandwidth (Hz)
f_c = 2e9; % Center frequency (Hz) f_c = 2e9; % Center frequency (Hz)
G_RX_dBi = 3; % Receiving Antenna Gain (dBi) G_RX_dBi = 3; % Receiving Antenna Gain (dBi)
beamwidthExponent = 6; beamwidthExponent = 6;
lossExponent = 2; lossExponent = 2;
tc.testClass = tc.testClass.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent); tc.testClass = tc.testClass.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent);
altitude = 30; altitude = 30;
otherSensorsPos = [6, -4, -1]; % relative to main sensor otherSensorsPos = [6, -4, -1]; % relative to main sensor
@@ -70,13 +73,14 @@ classdef test_rfSensor < matlab.unittest.TestCase
function plot_SINR_heterogenous_interferers(tc) function plot_SINR_heterogenous_interferers(tc)
% Plot sensor performance with no sources of interference % Plot sensor performance with no sources of interference
P_TX = 1e-3; % Transmit power (Watts) P_TX = 1e-3; % Transmit power (Watts)
T_0 = 300; % Temperature (K)
BW = 20e6; % Bandwidth (Hz) BW = 20e6; % Bandwidth (Hz)
f_c = 2e9; % Center frequency (Hz) f_c = 2e9; % Center frequency (Hz)
G_RX_dBi = 3; % Receiving Antenna Gain (dBi) G_RX_dBi = 3; % Receiving Antenna Gain (dBi)
beamwidthExponent = 6; beamwidthExponent = 6;
lossExponent = 2; lossExponent = 2;
tc.testClass = tc.testClass.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent); tc.testClass = tc.testClass.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent);
altitude = 30; altitude = 30;
otherSensorsPos = [6, -4, -1; -2, 6, 0]; % relative to main sensor otherSensorsPos = [6, -4, -1; -2, 6, 0]; % relative to main sensor
@@ -85,13 +89,14 @@ classdef test_rfSensor < matlab.unittest.TestCase
otherSensors{2} = rfSensor; otherSensors{2} = rfSensor;
% Must use same center frequency and bandwidth for interference sources % Must use same center frequency and bandwidth for interference sources
otherSensors{1} = otherSensors{1}.initialize(10 * P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent); otherSensors{1} = otherSensors{1}.initialize(10 * P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent);
otherSensors{2} = otherSensors{2}.initialize(10 * P_TX, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent); otherSensors{2} = otherSensors{2}.initialize(10 * P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent, 0, 0, lossExponent);
tc.testClass.plotPerformance(altitude, otherSensorsPos, otherSensors); tc.testClass.plotPerformance(altitude, otherSensorsPos, otherSensors);
end end
function plot_SINR_heterogenous_interferers_efficiently(tc) function plot_SINR_heterogenous_interferers_efficiently(tc)
P_TX = 1e-3; P_TX = 1e-3;
T_0 = 300;
BW = 20e6; BW = 20e6;
f_c = 2e9; f_c = 2e9;
G_RX_dBi = 3; G_RX_dBi = 3;
@@ -100,11 +105,11 @@ classdef test_rfSensor < matlab.unittest.TestCase
lossExponent = 2; lossExponent = 2;
sensor1 = rfSensor; sensor1 = rfSensor;
sensor1 = sensor1.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(1), 15, 45, lossExponent); sensor1 = sensor1.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(1), 15, 45, lossExponent);
sensor2 = rfSensor; sensor2 = rfSensor;
sensor2 = sensor2.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(2), 10, 150, lossExponent); sensor2 = sensor2.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(2), 10, 150, lossExponent);
sensor3 = rfSensor; sensor3 = rfSensor;
sensor3 = sensor3.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(3), 20, 200, lossExponent); sensor3 = sensor3.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(3), 20, 200, lossExponent);
pos1 = [0, 0, altitude]; pos1 = [0, 0, altitude];
pos2 = [6, -4, altitude - 1]; pos2 = [6, -4, altitude - 1];
@@ -141,6 +146,7 @@ classdef test_rfSensor < matlab.unittest.TestCase
end end
function plot_SINR_heterogenous_interferers_3d(tc) function plot_SINR_heterogenous_interferers_3d(tc)
P_TX = 1e-3; P_TX = 1e-3;
T_0 = 300;
BW = 20e6; BW = 20e6;
f_c = 2e9; f_c = 2e9;
G_RX_dBi = 3; G_RX_dBi = 3;
@@ -149,11 +155,11 @@ classdef test_rfSensor < matlab.unittest.TestCase
lossExponent = 2; lossExponent = 2;
sensor1 = rfSensor; sensor1 = rfSensor;
sensor1 = sensor1.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(1), 15, 45, lossExponent); sensor1 = sensor1.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(1), 15, 45, lossExponent);
sensor2 = rfSensor; sensor2 = rfSensor;
sensor2 = sensor2.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(2), 10, 150, lossExponent); sensor2 = sensor2.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(2), 10, 150, lossExponent);
sensor3 = rfSensor; sensor3 = rfSensor;
sensor3 = sensor3.initialize(P_TX, BW, f_c, G_RX_dBi, beamwidthExponent(3), 20, 200, lossExponent); sensor3 = sensor3.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(3), 20, 200, lossExponent);
pos1 = [0, 0, altitude]; pos1 = [0, 0, altitude];
pos2 = [6, -4, altitude - 5]; pos2 = [6, -4, altitude - 5];
+230
View File
@@ -0,0 +1,230 @@
classdef test_thesis_plots < matlab.unittest.TestCase
% test class that generates plots I use in my thesis
methods (Test)
function service_performance(tc)
% TX parameters
P_TX = 10e-3;
T_0 = 300;
BW = 20e6;
f_c = 2e9;
G_RX_dBi = 3;
beamwidthExponent = [16, 12, 32];
elevation = [30, 20, 45];
azimuth = [15, 325, 200];
lossExponent = 2;
% TX Positions
pos1 = [50, 50, 35];
pos2 = [70, 30, 30];
pos3 = [40, 75, 40];
% User Distribution Parameters
mu = [78, 66];
sig = reshape([2000, 1100; 1100, 2000], [1, 2, 2]);
sensor1 = rfSensor;
sensor1 = sensor1.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(1), elevation(1), azimuth(1), lossExponent);
sensor2 = rfSensor;
sensor2 = sensor2.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(2), elevation(2), azimuth(2), lossExponent);
sensor3 = rfSensor;
sensor3 = sensor3.initialize(P_TX, T_0, BW, f_c, G_RX_dBi, beamwidthExponent(3), elevation(3), azimuth(3), lossExponent);
% Compute sensor and system performance for a single user distribution
domain = rectangularPrism;
domain = domain.initialize([zeros(1, 3); 100 * ones(1, 3)], "DOMAIN", "Domain");
objectiveFunction = objectiveFunctionWrapper(mu, sig);
domain.objective = domain.objective.initialize(objectiveFunction, domain, 0.1, 0, 1e-6, mu, sig);
% Build target grid matching the domain objective grid and flatten user distribution
nX = size(domain.objective.values, 2);
nY = size(domain.objective.values, 1);
x_domain = linspace(0, 100, nX);
y_domain = linspace(0, 100, nY);
[Xd, Yd] = meshgrid(x_domain, y_domain);
targetPos_domain = [Xd(:), Yd(:), zeros(numel(Xd), 1)];
userDist = domain.objective.values(:) / sum(domain.objective.values, "all");
[SINR1, ~, sensor1, others] = sensor1.sensorPerformance(pos1, targetPos_domain, [pos2; pos3], {sensor2; sensor3});
sensor2 = others{1}; sensor3 = others{2};
[SINR2, ~, sensor2, others] = sensor2.sensorPerformance(pos2, targetPos_domain, [pos1; pos3], {sensor1; sensor3});
sensor1 = others{1}; sensor3 = others{2};
[SINR3, ~, sensor3, ~] = sensor3.sensorPerformance(pos3, targetPos_domain, [pos1; pos2], {sensor1; sensor2});
maxSINR_linear = max([10.^(SINR1/10), 10.^(SINR2/10), 10.^(SINR3/10)], [], 2);
% Plot SINR from each UAV's perspective with a shared color scale,
% one top-down map per UAV, in the style of the sigmoid sensor figure
sharedClim = [-80, max([SINR1; SINR2; SINR3])];
SINR = {SINR1, SINR2, SINR3};
sensors = {sensor1, sensor2, sensor3};
positions = [pos1; pos2; pos3];
for ii = 1:3
f = plotSinrMap(x_domain, y_domain, reshape(SINR{ii}, size(Xd)), ...
sensors, positions, ii, sharedClim);
if ii == 3 % only plot for #3 right now
f = exportThesisFig(f, "sinrexample" + ii, 0.95, Colormap="parula", Recolor=false, Resolution=300);
end
close(f);
end
end
function sigmoid_sensor_performance(tc)
s = sigmoidSensor;
s = s.initialize(65, 1, 50, 0.2, 30, 120);
p = 50 * ones(1, 3);
mu = [50, 50];
sig = 25 * reshape(eye(2), [1, 2, 2]);
domain = rectangularPrism;
domain = domain.initialize([zeros(1, 3); 100 * ones(1, 3)], "DOMAIN", "Domain");
objectiveFunction = objectiveFunctionWrapper(mu, sig);
domain.objective = domain.objective.initialize(objectiveFunction, domain, 0.1, 0, 1e-6, mu, sig);
S = s.sensorPerformance(p, [domain.objective.X(:), domain.objective.Y(:), zeros(size(domain.objective.X(:)))]);
S = reshape(S, size(domain.objective.X));
% meshgrid puts y down the rows, so plot against the grid vectors
% and put y = 0 at the bottom (imagesc defaults to row 1 at the top)
x = domain.objective.X(1, :);
y = domain.objective.Y(:, 1);
f = figure;
imagesc(x, y, S);
set(gca, "YDir", "normal");
axis("image");
hold("on");
% Boresight direction: the arrow runs from the UAV to where the
% boresight meets the ground, so its length grows with tilt
% (altitude * tan(tilt)). Azimuth 0 = +Y, 90 = +X, as in sensorPerformance.
reach = p(3) * tand(s.tilt);
hit = p(1:2) + reach * [sind(s.azimuth), cosd(s.azimuth)];
quiver(p(1), p(2), hit(1) - p(1), hit(2) - p(2), 0, "k", "MaxHeadSize", 0.4);
hUav = plot(p(1), p(2), "k+");
hHit = plot(hit(1), hit(2), "kx");
hold("off");
legend([hUav, hHit], "UAV position", "Boresight-ground intersection", "Location", "northwest");
xlabel("$x$ (m)");
ylabel("$y$ (m)");
cb = colorbar;
cb.Label.String = "Sensor performance $s_n$";
clim([0, 1]);
% Full text width so it's included at 1:1 like the other figures;
% 0.8 fits the square axes plus colorbar without side margins
f = exportThesisFig(f, "sigmoidsensorexample", 0.8, Colormap="parula", Recolor=false, Resolution=300);
close(f);
end
function antenna_patterns(tc)
f = figure;
xi = -90:0.1:90; % degrees
n = [1, 4, 16, 64, 256];
for ii = 1:length(n)
gain = 10 .* log10(2 * (n(ii) + 1)) + 10 .* n(ii) .* log10(max(0, cosd(xi)));
plot(xi, gain, "LineWidth", 2);
if ii == 1
hold("on");
end
end
hold("off");
grid("on");
ylim([-100, 35]);
xlim([-90, 90]);
xticks(-90:45:90);
% exportThesisFig switches all text to the LaTeX interpreter
title("Transmitting Antenna Gain Model");
ylabel("Transmitting Antenna Gain (dBi)");
xlabel("Elevation Angle from Antenna Boresight $\xi$ ($^\circ$)");
legend("$\delta = " + string(n) + "$");
exportThesisFig(f, "antennapatterns", 3/4);
close(f);
end
function sigmoid_sensor_membership_functions(tc)
s = [sigmoidSensor, sigmoidSensor];
alphaDist = [2, 8];
betaDist = [25, 1];
alphaTilt = [50, 15]; % degrees
betaTilt = [0.2, 3];
s(1) = s(1).initialize(alphaDist(1), betaDist(1), alphaTilt(1), betaTilt(1));
s(2) = s(2).initialize(alphaDist(2), betaDist(2), alphaTilt(2), betaTilt(2));
set(groot, 'defaultTextInterpreter', 'latex');
set(groot, 'defaultAxesTickLabelInterpreter', 'latex');
set(groot, 'defaultLegendInterpreter', 'latex');
set(groot, 'defaultColorbarTickLabelInterpreter', 'latex');
set(groot, 'defaultAxesFontSize', 10); % = \footnotesize in a 12pt doc
set(groot, 'defaultTextFontSize', 10);
% Plot
f = s(1).plotParameters;
f = s(2).plotParameters(f);
f = exportThesisFig(f, "sigmoidmodel", 3/4);
close(f);
end
end
end
function f = plotSinrMap(x, y, SINR, sensors, positions, tx, cLimits)
% Top-down SINR map for transmitting UAV tx, matching the sigmoid sensor
% figure. Every UAV gets a marker, an arrow to the X where its boresight
% meets the ground, and its -3 dB (half-power) beam footprint; the
% transmitting UAV is drawn solid, the interferers dashed.
f = figure;
imagesc(x, y, SINR);
set(gca, "YDir", "normal"); % y = 0 at the bottom
axis("image");
hold("on");
for ii = 1:numel(sensors)
[hit, ring] = beamFootprint(sensors{ii}, positions(ii, :));
pos = positions(ii, :);
if ii == tx
style = "-"; marker = "k+";
else
style = "--"; marker = "ko";
end
hRing(ii) = plot(ring(:, 1), ring(:, 2), "k" + style); %#ok<AGROW>
quiver(pos(1), pos(2), hit(1) - pos(1), hit(2) - pos(2), 0, "k", ...
"LineStyle", style, "MaxHeadSize", 0.4);
hHit = plot(hit(1), hit(2), "kx");
hUav(ii) = plot(pos(1), pos(2), marker, "MarkerFaceColor", "w"); %#ok<AGROW>
end
hold("off");
rx = setdiff(1:numel(sensors), tx);
legend([hUav(tx), hUav(rx(1)), hHit, hRing(tx), hRing(rx(1))], ...
"Transmitting UAV", "Interfering UAVs", "Boresight-ground intersection", ...
"Transmitter $-3$ dB contour", "Interferer $-3$ dB contour", ...
"Location", "southoutside", "NumColumns", 2);
xlim([min(x), max(x)]); % footprints can extend past the area
ylim([min(y), max(y)]);
xlabel("$x$ (m)");
ylabel("$y$ (m)");
cb = colorbar;
cb.Label.String = "SINR (dB)";
clim(cLimits);
end
function [hit, ring] = beamFootprint(sensor, pos)
% Where the boresight meets the ground (z = 0), and the ground curve where
% the gain is 3 dB below boresight: the intersection of the half-power cone
% with the ground, as in rfSensor/plot. Azimuth 0 = +Y, 90 = +X.
tlt = sensor.tilt;
az = sensor.azimuth;
hit = pos(1:2) + pos(3) * tand(tlt) * [sind(az), cosd(az)];
% Rotate nadir to the boresight
Ry = [cosd(tlt), 0, -sind(tlt); 0, 1, 0; sind(tlt), 0, cosd(tlt)];
Rz = [sind(az), -cosd(az), 0; cosd(az), sind(az), 0; 0, 0, 1];
R = Rz * Ry;
b = R * [0; 0; -1];
u = R * [1; 0; 0];
v = R * [0; 1; 0];
ha = sensor.halfAngle();
phi = linspace(0, 2*pi, 720)';
dirs = cosd(ha) .* b' + sind(ha) .* (cos(phi) .* u' + sin(phi) .* v');
t = -pos(3) ./ dirs(:, 3);
t(t <= 0) = NaN; % cone generators that never reach the ground
ring = pos(1:2) + t .* dirs(:, 1:2);
end
+128
View File
@@ -0,0 +1,128 @@
function f = exportThesisFig(f, name, aspect, opts)
% Restyle a figure for print and export it as a vector PDF sized for the
% thesis, so it can be included at 1:1 (\includegraphics with no width).
%
% exportThesisFig(f, "sigmoidmodel") % full width, 4:3
% exportThesisFig(f, "traj", 1, Width=230) % half-width subfigure
% exportThesisFig(f, "traj", 3/4, LineStyles=true) % distinguishable in grayscale
% exportThesisFig(f, "perf", 0.8, Width=330) % imagesc plot -> name.png
%
% Figures containing images (imagesc, image) are exported as a lossless
% 400 dpi PNG instead: MATLAB's vector PDF export draws them with a
% visible diagonal seam. The PNG records its dpi, so LaTeX still places it
% at Width points. The figure is modified in place.
arguments (Input)
f (1, 1) {mustBeA(f, "matlab.ui.Figure")};
name (1, 1) string;
aspect (1, 1) double {mustBePositive} = 3/4;
opts.Width (1, 1) double {mustBePositive} = 469; % points; \the\hsize
opts.FontSize (1, 1) double {mustBePositive} = 10; % \footnotesize in a 12pt doc
opts.LineWidth (1, 1) double {mustBePositive} = 2; % data lines, points
opts.AxesLineWidth (1, 1) double {mustBePositive} = 0.75;
opts.MarkerSize (1, 1) double {mustBePositive} = 4; % markers on lines
opts.PointMarkerSize (1, 1) double {mustBePositive} = 16; % marker-only series, e.g. plot(x, y, "k+")
opts.PointMarkerLineWidth (1, 1) double {mustBePositive} = 2.5;
opts.Palette (:, 3) double = okabeIto(); % colorblind- and print-safe
opts.Recolor (1, 1) logical = true; % also override explicitly set colors
opts.LineStyles (1, 1) logical = false; % cycle line styles per series
opts.Raster (1, 1) logical = ~isempty(findall(f, 'Type', 'image')); % PNG; also for very dense plots
% dpi, for Raster. MATLAB silently writes an all-black PNG when the image is
% too large (3909 px wide failed, 2750 px worked), so full width stays at 400
opts.Resolution (1, 1) double {mustBePositive} = 400;
opts.Colormap = []; % for images/surfaces, e.g. "parula"; [] keeps the figure's
opts.OutDir (1, 1) string = matlab.project.rootProject().RootFolder;
end
arguments (Output)
f (1, 1) {mustBeA(f, "matlab.ui.Figure")};
end
styles = ["-", "--", ":", "-."];
nColors = size(opts.Palette, 1);
% Text
set(findall(f, '-property', 'Interpreter'), 'Interpreter', 'latex');
set(findall(f, '-property', 'TickLabelInterpreter'), 'TickLabelInterpreter', 'latex');
set(findall(f, '-property', 'FontSize'), 'FontSize', opts.FontSize);
% Axes: frame width, and palette for any series that pick their color automatically
for ax = findall(f, 'Type', 'axes')'
ax.LineWidth = opts.AxesLineWidth;
ax.ColorOrder = opts.Palette;
if opts.LineStyles
ax.LineStyleOrder = styles;
end
if ~isempty(opts.Colormap)
colormap(ax, opts.Colormap);
end
end
% Colorbars (image and surface plots) get the same frame as the axes. Their
% Label isn't reached by findall above, so set it directly.
for cb = findall(f, 'Type', 'colorbar')'
cb.LineWidth = opts.AxesLineWidth;
cb.Label.Interpreter = 'latex';
cb.Label.FontSize = opts.FontSize;
end
% Data series. SeriesIndex is the series' position in its axes' color
% order, so it keeps the palette assignment consistent with the legend.
series = findall(f, '-property', 'SeriesIndex');
for s = series'
k = s.SeriesIndex;
if ~isnumeric(k) || k < 1
continue; % series excluded from the color order
end
c = opts.Palette(mod(k - 1, nColors) + 1, :);
% Line-type series have a single Color; bars/areas have Face/EdgeColor
% instead, and keep their thin default outline
isLine = isprop(s, 'Color');
isScatter = isa(s, 'matlab.graphics.chart.primitive.Scatter');
% Marker-only series (no connecting line) mark individual points; a
% thick data-line width would fill in markers like + and x
isPoint = isLine && isprop(s, 'Marker') && strcmp(s.LineStyle, 'none') ...
&& ~strcmp(s.Marker, 'none');
if isPoint
s.LineWidth = opts.PointMarkerLineWidth;
s.MarkerSize = opts.PointMarkerSize;
else
if isLine || isScatter
s.LineWidth = opts.LineWidth;
end
if isprop(s, 'MarkerSize')
s.MarkerSize = opts.MarkerSize;
end
end
if opts.LineStyles && isLine && ~strcmp(s.LineStyle, 'none')
s.LineStyle = styles(mod(k - 1, numel(styles)) + 1);
end
if opts.Recolor
if isLine
s.Color = c; % line, stair, errorbar, function plots
end
if isprop(s, 'FaceColor') && ~ischar(s.FaceColor)
s.FaceColor = c; % bar, area, histogram (leaves 'none'/'flat' alone)
end
if isScatter && size(s.CData, 1) <= 1
s.CData = c; % single-color scatter (leaves colormapped ones alone)
end
end
end
% Page size in points so the PDF is exactly Width wide, independent of
% the on-screen window (which the window manager may resize)
w = opts.Width;
h = aspect * w;
f.PaperUnits = 'points';
f.PaperPositionMode = 'manual';
f.PaperPosition = [0, 0, w, h];
f.PaperSize = [w, h];
if opts.Raster
% PNG rather than '-dpdf -image', which embeds a lossy JPEG
print(f, fullfile(opts.OutDir, name + ".png"), '-dpng', "-r" + opts.Resolution);
else
print(f, fullfile(opts.OutDir, name + ".pdf"), '-dpdf', '-vector');
end
end
+11
View File
@@ -0,0 +1,11 @@
function p = okabeIto()
% Okabe & Ito (2008) colorblind-safe palette, minus yellow (too light on
% white paper). Returns an N-by-3 RGB matrix, e.g. colororder(okabeIto()).
p = [ 0, 114, 178; % blue
213, 94, 0; % vermillion
0, 158, 115; % bluish green
204, 121, 167; % reddish purple
86, 180, 233; % sky blue
230, 159, 0; % orange
0, 0, 0] / 255;
end