diff --git a/@miSim/plotGraph.m b/@miSim/plotGraph.m index 50eaf49..d303833 100644 --- a/@miSim/plotGraph.m +++ b/@miSim/plotGraph.m @@ -6,8 +6,7 @@ function obj = plotGraph(obj) obj (1, 1) {mustBeA(obj, "miSim")}; end - % Form graph from adjacency matrix - G = graph(obj.constraintAdjacencyMatrix, "omitselfloops"); + nAgents = size(obj.agents, 1); % Resolve the target axes for the network graph tile if isnan(obj.networkGraphIndex) @@ -16,7 +15,36 @@ function obj = plotGraph(obj) ax = obj.f.Children(1).Children(obj.networkGraphIndex(1)); end - % Plot graph object + % Build the graph to plot. The fixed-radius model is undirected. In SINR + % mode the link set (lesser-neighbor topology) is kept exactly as-is, but + % each connection is given an orientation: of the two directional SINRs on + % a link, the larger one wins, and the edge is directed from that + % direction's transmitter to its receiver (arrow = transmitter -> receiver). + if obj.useSinrComms + positions = zeros(nAgents, 3); + for kk = 1:nAgents + positions(kk, :) = obj.agents{kk}.pos; + end + D = false(nAgents); % directed adjacency: D(tx, rx) = true + for aa = 1:(nAgents - 1) + for bb = (aa + 1):nAgents + if obj.constraintAdjacencyMatrix(aa, bb) + sinrARx = obj.sinrLink(positions, aa, bb); % aa receives bb + sinrBRx = obj.sinrLink(positions, bb, aa); % bb receives aa + if sinrARx >= sinrBRx + D(bb, aa) = true; % better as bb -> aa (tx bb, rx aa) + else + D(aa, bb) = true; % better as aa -> bb (tx aa, rx bb) + end + end + end + end + G = digraph(D); + else + G = graph(obj.constraintAdjacencyMatrix, "omitselfloops"); + end + + % Plot graph object (digraph renders arrowheads automatically) hold(ax, "on"); o = plot(ax, G, "LineStyle", "--", "EdgeColor", "g", "NodeColor", "k", "LineWidth", 2); hold(ax, "off"); @@ -26,34 +54,33 @@ function obj = plotGraph(obj) 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). + % Mark ground-station-connected nodes with an "x" (others keep the default + % dot). Ground stations attach to the two "router" agents: agent 1 and the + % agent with the largest ODD index (that is nAgents when odd, else + % nAgents-1). With one or two agents only agent 1 qualifies. + routers = unique([1, 2 * ceil(nAgents / 2) - 1]); + for oo = 1:numel(o) + highlight(o(oo), routers, "Marker", "x", "MarkerSize", 8); + end + + % In SINR comms mode, label each directed edge with the better SINR that set + % its orientation and report the threshold in the tile title. The + % fixed-radius model is left unchanged (no edge labels, title from setup). if obj.useSinrComms - endNodes = G.Edges.EndNodes; % each row [lo hi]; graph stores lo < hi - nAgents = size(obj.agents, 1); - positions = zeros(nAgents, 3); - for kk = 1:nAgents - positions(kk, :) = obj.agents{kk}.pos; - end - % A link is feasible only if BOTH directions clear the threshold, so - % label each edge with the binding (worse) directional SINR: the min of - % the lower-index-receiver and higher-index-receiver links, with - % interference from all other agents at each receiver. Labels are bare - % numbers (no units) so they stay compact; GraphPlot renders them + endNodes = G.Edges.EndNodes; % each row [tx rx] for a directed edge + % Label with the SINR of the chosen (better) direction: the receiver is + % the edge target, the transmitter the edge source. Bare numbers (no + % units) keep the small tile uncluttered; GraphPlot draws labels % horizontally, left-to-right. edgeLabels = strings(size(endNodes, 1), 1); for ee = 1:size(endNodes, 1) - lo = endNodes(ee, 1); - hi = endNodes(ee, 2); - 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))); + sinrLin = obj.sinrLink(positions, endNodes(ee, 2), endNodes(ee, 1)); % rx, tx + edgeLabels(ee) = sprintf("%.1f", 10 * log10(sinrLin)); end o(1).EdgeLabel = edgeLabels; - % 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. + % Threshold in the existing single-line title (not a subtitle) so the + % small tile isn't compressed, and disable the hover toolbar so the + % R2026 "..." menu stops covering the readout. title(ax, sprintf("Network Graph (\\geq %.1f dB)", obj.sinrThreshold)); ax.Toolbar.Visible = "off"; end diff --git a/@sensingObjective/plot.m b/@sensingObjective/plot.m index baf31b3..624fe87 100644 --- a/@sensingObjective/plot.m +++ b/@sensingObjective/plot.m @@ -36,7 +36,14 @@ function f = plot(obj, ind, f) % Add to other perspectives if size(ind, 2) > 1 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 \ No newline at end of file diff --git a/test/test_miSim.m b/test/test_miSim.m index 05af713..0bde39e 100644 --- a/test/test_miSim.m +++ b/test/test_miSim.m @@ -877,6 +877,88 @@ classdef test_miSim < matlab.unittest.TestCase % 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_communications_constraint_fixed_radius(tc) % No obstacles % Fixed two agents initial conditions