From 0e39e0037d72b4b17a68b2c44afef7222391d79f Mon Sep 17 00:00:00 2001 From: Kevin D Date: Sun, 3 May 2026 14:32:53 -0700 Subject: [PATCH] added sensor tilting and rf sensor sim test cases --- @agent/initialize.m | 2 +- @rfSensor/halfAngle.m | 23 +++++++++++ @rfSensor/rfSensor.m | 1 + @rfSensor/sensorPerformance.m | 4 +- @rfSensor/transmitterGain.m | 2 +- @sigmoidSensor/halfAngle.m | 9 +++++ @sigmoidSensor/initialize.m | 9 ++++- @sigmoidSensor/sensorPerformance.m | 16 +++++--- @sigmoidSensor/sigmoidSensor.m | 3 +- geometries/@cone/cone.m | 8 ++-- geometries/@cone/initialize.m | 28 +++++++------ geometries/@cone/plot.m | 13 +++++- test/test_miSim.m | 65 +++++++++++++++++++++++++++++- 13 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 @rfSensor/halfAngle.m create mode 100644 @sigmoidSensor/halfAngle.m diff --git a/@agent/initialize.m b/@agent/initialize.m index b74a926..47c3e98 100644 --- a/@agent/initialize.m +++ b/@agent/initialize.m @@ -35,5 +35,5 @@ function obj = initialize(obj, pos, collisionGeometry, sensorModel, comRange, ma % Initialize FOV cone obj.fovGeometry = cone; - obj.fovGeometry = obj.fovGeometry.initialize([obj.pos(1:3)], tand(obj.sensorModel.alphaTilt) * obj.pos(3), obj.pos(3), REGION_TYPE.FOV, sprintf("%s FOV", obj.label)); + obj.fovGeometry = obj.fovGeometry.initialize([obj.pos(1:3)], tand(obj.sensorModel.halfAngle()) * obj.pos(3), obj.pos(3), REGION_TYPE.FOV, sprintf("%s FOV", obj.label), obj.sensorModel.tilt, obj.sensorModel.azimuth); end diff --git a/@rfSensor/halfAngle.m b/@rfSensor/halfAngle.m new file mode 100644 index 0000000..53fe95d --- /dev/null +++ b/@rfSensor/halfAngle.m @@ -0,0 +1,23 @@ +function value = halfAngle(obj) + arguments (Input) + obj (1, 1) {mustBeA(obj, "rfSensor")}; + end + arguments (Output) + value (1, 1) double; + 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)'; + gain = obj.transmitterGain(obj.tilt + dtheta, obj.azimuth * ones(size(dtheta))); + target = gain(1) - 3; + idx = find(gain <= target, 1); + if isempty(idx) || idx == 1 + value = dtheta(end); + return; + end + % Linear interpolation between bracketing samples + value = dtheta(idx-1) + (target - gain(idx-1)) * ... + (dtheta(idx) - dtheta(idx-1)) / (gain(idx) - gain(idx-1)); +end diff --git a/@rfSensor/rfSensor.m b/@rfSensor/rfSensor.m index 9f1a271..07f7841 100644 --- a/@rfSensor/rfSensor.m +++ b/@rfSensor/rfSensor.m @@ -22,6 +22,7 @@ classdef rfSensor [obj] = initialize(obj, txPower, bandwidth, centerFreq, rxGain); % 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 [d, t, a] = computePointToPoints(obj, agentPos, targetPos); + [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] = plotPerformance(obj, altitude, otherSensorsPos, otherSensors); % debug, plot SNR or SINR ground heatmap for a given geometry obj = clearRssCache(obj); diff --git a/@rfSensor/sensorPerformance.m b/@rfSensor/sensorPerformance.m index 1842e21..65783f4 100644 --- a/@rfSensor/sensorPerformance.m +++ b/@rfSensor/sensorPerformance.m @@ -17,7 +17,7 @@ function [SINR, SNR, obj, otherSensors] = sensorPerformance(obj, agentPos, targe [d, t, a] = obj.computePointToPoints(agentPos, targetPos); if isempty(obj.rssCache) - obj.rssCache = 10 .^ (0.1 .* obj.RSS(d, t, a)); + obj.rssCache = 1e-3 .* 10 .^ (0.1 .* obj.RSS(d, t, a)); % dBm → W end S = obj.rssCache; @@ -25,7 +25,7 @@ function [SINR, SNR, obj, otherSensors] = sensorPerformance(obj, agentPos, targe for ii = 1:size(otherSensors, 1) if isempty(otherSensors{ii}.rssCache) [d_other, t_other, a_other] = otherSensors{ii}.computePointToPoints(otherSensorsPos(ii, 1:3), targetPos); - otherSensors{ii}.rssCache = 10 .^ (0.1 .* otherSensors{ii}.RSS(d_other, t_other, a_other)); + otherSensors{ii}.rssCache = 1e-3 .* 10 .^ (0.1 .* otherSensors{ii}.RSS(d_other, t_other, a_other)); % dBm → W end I = I + otherSensors{ii}.rssCache; end diff --git a/@rfSensor/transmitterGain.m b/@rfSensor/transmitterGain.m index c85941b..06e0d68 100644 --- a/@rfSensor/transmitterGain.m +++ b/@rfSensor/transmitterGain.m @@ -11,7 +11,7 @@ function value = transmitterGain(obj, t, a) error("t and a must be the same size"); end - n = 4; % beamwidth exponent (higher = narrower beam) + n = 6; % beamwidth exponent (higher = narrower beam) % Angular offset from boresight via spherical law of cosines % Convention: t=0° nadir, t=90° horizon; a=0° +y, a=90° +x diff --git a/@sigmoidSensor/halfAngle.m b/@sigmoidSensor/halfAngle.m new file mode 100644 index 0000000..8c8ac79 --- /dev/null +++ b/@sigmoidSensor/halfAngle.m @@ -0,0 +1,9 @@ +function value = halfAngle(obj) + arguments (Input) + obj (1, 1) {mustBeA(obj, "sigmoidSensor")}; + end + arguments (Output) + value (1, 1) double; + end + value = obj.alphaTilt; +end diff --git a/@sigmoidSensor/initialize.m b/@sigmoidSensor/initialize.m index 5c8249e..160f034 100644 --- a/@sigmoidSensor/initialize.m +++ b/@sigmoidSensor/initialize.m @@ -1,17 +1,24 @@ -function obj = initialize(obj, alphaDist, betaDist, alphaTilt, betaTilt) +function obj = initialize(obj, alphaDist, betaDist, alphaTilt, betaTilt, tilt, azimuth) arguments (Input) obj (1, 1) {mustBeA(obj, "sigmoidSensor")} alphaDist (1, 1) double; betaDist (1, 1) double; alphaTilt (1, 1) double; betaTilt (1, 1) double; + tilt (1, 1) double = 0; + azimuth (1, 1) double = 0; end arguments (Output) obj (1, 1) {mustBeA(obj, "sigmoidSensor")} end + % Sensor performance parameters obj.alphaDist = alphaDist; obj.betaDist = betaDist; obj.alphaTilt = alphaTilt; obj.betaTilt = betaTilt; + + % Sensor pointing parameters + obj.tilt = tilt; + obj.azimuth = azimuth; end \ No newline at end of file diff --git a/@sigmoidSensor/sensorPerformance.m b/@sigmoidSensor/sensorPerformance.m index 18f1c74..15521c7 100644 --- a/@sigmoidSensor/sensorPerformance.m +++ b/@sigmoidSensor/sensorPerformance.m @@ -8,16 +8,20 @@ function value = sensorPerformance(obj, agentPos, targetPos) value (:, 1) double; end - % compute direct distance and distance projected onto the ground - d = vecnorm(agentPos - targetPos, 2, 2); % distance from sensor to target - x = vecnorm(agentPos(1:2) - targetPos(:, 1:2), 2, 2); % distance from sensor nadir to target nadir (i.e. distance ignoring height difference) + % Unit vectors from agent to each target + diffs = targetPos - agentPos; + d = vecnorm(diffs, 2, 2); + dirs = diffs ./ d; - % compute tilt angle - tiltAngle = (180 - atan2d(x, targetPos(:, 3) - agentPos(3))); % degrees + % Boresight unit vector: tilt=0 → nadir [0,0,-1]; azimuth 0=+Y, 90=+X clockwise + boresight = [sind(obj.tilt)*sind(obj.azimuth), sind(obj.tilt)*cosd(obj.azimuth), -cosd(obj.tilt)]; + + % Angular offset from boresight to each target direction + angularOffset = acosd(dirs * boresight'); % Membership functions mu_d = obj.distanceMembership(d); - mu_t = obj.tiltMembership(tiltAngle); + mu_t = obj.tiltMembership(angularOffset); value = mu_d .* mu_t; % assume pan membership is always 1 end \ No newline at end of file diff --git a/@sigmoidSensor/sigmoidSensor.m b/@sigmoidSensor/sigmoidSensor.m index 81a23e3..8c7879b 100644 --- a/@sigmoidSensor/sigmoidSensor.m +++ b/@sigmoidSensor/sigmoidSensor.m @@ -12,8 +12,9 @@ classdef sigmoidSensor end methods (Access = public) - [obj] = initialize(obj, alphaDist, betaDist, alphaTilt, betaTilt); % 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] = 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 end methods (Access = private) diff --git a/geometries/@cone/cone.m b/geometries/@cone/cone.m index c73479b..3f05456 100644 --- a/geometries/@cone/cone.m +++ b/geometries/@cone/cone.m @@ -6,9 +6,11 @@ classdef cone label = ""; % Spatial - center = NaN; - radius = NaN; - height = NaN; + center = NaN; + radius = NaN; + height = NaN; + tilt = 0; % degrees, 0=nadir 90=horizon + azimuth = 0; % degrees, 0=+Y 90=+X clockwise % Plotting surface; diff --git a/geometries/@cone/initialize.m b/geometries/@cone/initialize.m index a811633..736f931 100644 --- a/geometries/@cone/initialize.m +++ b/geometries/@cone/initialize.m @@ -1,19 +1,23 @@ -function obj = initialize(obj, center, radius, height, tag, label) +function obj = initialize(obj, center, radius, height, tag, label, tilt, azimuth) arguments (Input) - obj (1, 1) {mustBeA(obj, "cone")}; - center (1, 3) double; - radius (1, 1) double; - height (1, 1) double; - tag (1, 1) REGION_TYPE = REGION_TYPE.INVALID; - label (1, 1) string = ""; + obj (1, 1) {mustBeA(obj, "cone")}; + center (1, 3) double; + radius (1, 1) double; + height (1, 1) double; + tag (1, 1) REGION_TYPE = REGION_TYPE.INVALID; + label (1, 1) string = ""; + tilt (1, 1) double = 0; + azimuth (1, 1) double = 0; end arguments (Output) obj (1, 1) {mustBeA(obj, "cone")}; end - obj.center = center; - obj.radius = radius; - obj.height = height; - obj.tag = tag; - obj.label = label; + obj.center = center; + obj.radius = radius; + obj.height = height; + obj.tag = tag; + obj.label = label; + obj.tilt = tilt; + obj.azimuth = azimuth; end \ No newline at end of file diff --git a/geometries/@cone/plot.m b/geometries/@cone/plot.m index 83098db..43b55a6 100644 --- a/geometries/@cone/plot.m +++ b/geometries/@cone/plot.m @@ -20,7 +20,18 @@ function [obj, f] = plot(obj, ind, f, maxAlt) % Scale to match height Z = Z * maxAlt; - + + % Rotate mesh around apex to match boresight tilt and azimuth. + % Apex sits at [0, 0, maxAlt] before center translation. + % Convention: tilt 0=nadir, 90=horizon; azimuth 0=+Y, 90=+X, clockwise. + Ry = [cosd(obj.tilt), 0, -sind(obj.tilt); 0, 1, 0; sind(obj.tilt), 0, cosd(obj.tilt)]; + Rz = [sind(obj.azimuth), -cosd(obj.azimuth), 0; cosd(obj.azimuth), sind(obj.azimuth), 0; 0, 0, 1]; + R = Rz * Ry; + pts = R * [X(:)'; Y(:)'; Z(:)' - maxAlt]; + X = reshape(pts(1, :), size(X)); + Y = reshape(pts(2, :), size(Y)); + Z = reshape(pts(3, :) + maxAlt, size(Z)); + % Move to center location X = X + obj.center(1); Y = Y + obj.center(2); diff --git a/test/test_miSim.m b/test/test_miSim.m index dbef0a4..22bd52b 100644 --- a/test/test_miSim.m +++ b/test/test_miSim.m @@ -456,7 +456,70 @@ classdef test_miSim < matlab.unittest.TestCase 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); % Run the simulation - tc.testClass = tc.testClass.run();end + tc.testClass = tc.testClass.run(); + end + function test_single_agent_gradient_ascent_tilted(tc) + % make basic domain + 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([7, 6]), tc.domain, tc.discretizationStep, tc.protectedRange, 1e-6, [7, 6]); + + % Initialize agent collision geometry + tc.agents = {agent}; + geometry1 = spherical; + geometry1 = geometry1.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], tc.collisionRanges(1), REGION_TYPE.COLLISION); + + % Initialize agent sensor model with fixed parameters + tc.sensor = tc.sensor.initialize(tc.minDimension / 2, 3, 20, 3, 25, 155); + + % Initialize agents + tc.maxIter = 75; + tc.agents{1} = tc.agents{1}.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], geometry1, tc.sensor, tc.commsRanges(1), tc.maxIter, tc.initialStepSize); + + % Initialize the simulation + tc.obstacles = cell(0, 1); + 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); + + % Run the simulation + tc.testClass = tc.testClass.run(); + end + function test_single_agent_gradient_ascent_tilted_RF_sensor(tc) + % make basic domain + 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 + minimumSINR = 50; % (dB) + tc.domain.objective = tc.domain.objective.initialize(objectiveFunctionWrapper([7, 6]), tc.domain, tc.discretizationStep, tc.protectedRange, minimumSINR, [7, 6]); + + % Initialize agent collision geometry + tc.agents = {agent}; + geometry1 = spherical; + geometry1 = geometry1.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], tc.collisionRanges(1), REGION_TYPE.COLLISION); + + % Initialize agent sensor model with fixed parameters + P_TX = 1e-3; % Transmit power (Watts) + BW = 20e6; % Bandwidth (Hz) + f_c = 2e9; % Center frequency (Hz) + G_RX_dBi = 3; % Receiving Antenna Gain (dBi) + + tc.sensor = rfSensor; + tc.sensor = tc.sensor.initialize(P_TX, BW, f_c, G_RX_dBi, 45, 45); + + % Initialize agents + tc.maxIter = 75; + tc.agents{1} = tc.agents{1}.initialize([tc.domain.center(1:2)-tc.domain.dimensions(1)/4, 3], geometry1, tc.sensor, tc.commsRanges(1), tc.maxIter, tc.initialStepSize); + + % Initialize the simulation + tc.obstacles = cell(0, 1); + tc.minAlt = 0.5; + 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); + + % Run the simulation + tc.testClass = tc.testClass.run(); + end function test_collision_avoidance(tc) % No obstacles % Fixed agent initial conditions