Main Content

5G Network Digital Twin in 3-D Urban Scenario

Since R2026a

This example shows how to model a digital twin of a 5G network in a specific geographical location. The example performs a network or system-level simulation with ray-tracing-based channels.

Introduction

This example implements a 5G network simulation in a realistic urban environment using ray tracing for channel modeling. In this example, you create a digital twin of a network in a specific location and then evaluate its performance in that environment.

This example follows these steps:

The steps in this example are 1. scenario configuration, 2. ray tracing analysis, 3. network simulation, and 4. results analysis.

The example models several base stations (gNBs) and user equipment (UE) units. You can configure the location of each gNB and UE. UEs remain static throughout the simulation. The ray tracing analysis results in propagation channel models for the network simulation.

After network simulation, the example calculates key performance indicators such as system throughput, spectral efficiency, and block error rate. It also provides UE-specific metrics including modulation and coding scheme (MCS) and signal to interference and noise ratio (SINR).

Specify System Parameters

Specify the main simulation parameters.

simulationTime = 0.025;          % Seconds
fc = 4.2e9;                      % Carrier frequency (Hz)
PtxgNBdBm = 33;                  % gNB transmit power in dBm
PtxUEdBm = 23;                   % UE transmit power in dBm
NRB = 51;                        % Number of resource blocks
SCS = 30;                        % Subcarrier spacing (kHz)
duplexingMode = "TDD";           % "FDD" or "TDD"
maxNumReflections = 2;           % Maximum number of ray tracing reflections
maxNumDiffractions = 1;          % Maximum number of ray tracing diffractions
gNBArrayRectangularDim = [2 2];  % gNB rectangular array dimensions (rows, columns). Valid values:
                                 % [1 1], [2 1], [2 2], [2 4], [4 4]
ueArrayRectangularDim = [2 1];   % UE rectangular array dimensions (rows, columns)
validategNBArraySize(gNBArrayRectangularDim);
BW = validateNumerology(SCS,NRB);% BW: bandwidth in Hz

Specify the name of an OpenStreetMap file [1] with 3-D buildings information, and specify the geographic coordinates (latitude and longitude) of the gNBs and UEs.

fileName = "chicago.osm";

The number of gNBs and UEs is specified by the size of the location arrays gNBLocations and ueLocations.

% gNB locations
gNBLocations = [41.876853,-87.626942;  % gNB1
                41.877706,-87.630108;  % gNB2
                41.875433,-87.630880]; % gNB3
gNBAzAngle = [180 -90 70];             % gNB array normal azimuth angles (deg) relative to east
gNBElAngle = [-6 -6 -6];               % gNB array normal elevation angles (deg), negative is downwards
gNBAntennaHeight = [5 5 25];           % gNB antenna height (m) above mounting surface (ground or building roof)
numgNBs = size(gNBLocations,1);
gNBLabels = "gNB"+string(1:numgNBs);

% UE locations
ueLocations = [41.877306,-87.627886;   % UE1
               41.877090,-87.628397;   % UE2
               41.877448,-87.629231;   % UE3
               41.876888,-87.630645;   % UE4
               41.876920,-87.629331;   % UE5
               41.875769,-87.628913;   % UE6
               41.876582,-87.630087];  % UE7
ueAntennaHeight = 1.5; % m
numUEs = size(ueLocations,1);
ueLabels = "UE"+string(1:numUEs);

% Random number generator default settings for result reproducibility
rng("default");

Display the gNB and UE locations.

plotNodeLocations(fileName,"gNB and UE Locations", ...
gNBLocations,ueLocations,gNBLabels,ueLabels);

Figure contains an axes object with type geoaxes. The geoaxes object contains 13 objects of type polygon, scatter, text.

Perform Ray Tracing Analysis

Perform ray tracing analysis, resulting in a set of rays the example uses for channel modeling during network simulation.

Create gNB Sites

Create the gNB sites for ray tracing analysis.

Precalculate the gNB transmit power in watts, the wavelength, and the reference ellipsoid for converting geographical coordinates (latitude and longitude) to cartesian coordinates used by the network simulation nodes.

% gNB transmit power in watts
PtxW = db2pow(PtxgNBdBm-30);

% Wavelength for array dimensioning
lambda  = freq2wavelen(fc);

% Reference ellipsoid object for lat/lon to cartesian conversion
wgs84 = wgs84Ellipsoid;

Create the gNB site objects for ray tracing analysis. Each gNB has its own array with the broadside pointing in the direction specified in gNBAzAngle and gNBElAngle. The gNBsTable table contains name, indexing, and cartesian coordinates information associated with each gNB. You can use this information later to create the nodes for network simulation.

% Create gNB site objects (txsite objects)
gNBsTable = table(Size=[numgNBs 4], ...
    VariableTypes=["string","int8","int8","double"], ...
    VariableNames=["gNBLabel","gnbArrayIdx","gnbID","xyz"]);
gNBsTable.xyz = zeros(numgNBs,3); % Make each xyz row a 3-element vector: [x y z]
gnbSites = txsite.empty();

% Calculate bandwidth and uplink and downlink carrier frequencies
[dlFc,ulFc] = fddCarrierFreq(duplexingMode,fc,BW);

for n = 1:numgNBs
    % Array object
    txArray = phased.NRRectangularPanelArray(Size=[gNBArrayRectangularDim 1 1], ...
                        Spacing=[0.5*lambda*[1 1] 1 1], ...
                        ElementSet={phased.NRAntennaElement(PolarizationAngle=45), ...
                                      phased.NRAntennaElement(PolarizationAngle=-45)});
                            
    % Create gNB txsites
    gnbSites(n)= txsite(Name=(gNBLabels(n)), ...
        Latitude=gNBLocations(n,1), ...
        Longitude=gNBLocations(n,2), ...
        TransmitterPower=PtxW, ...
        TransmitterFrequency=dlFc, ...
        Antenna=txArray, ...
        AntennaHeight=gNBAntennaHeight(n), ...
        AntennaAngle=[gNBAzAngle(n) gNBElAngle(n)]');

    % Equivalent cartesian coordinates: gNB of interest is origin
    [x,y,z] = latLonToCartesian(gNBLocations(n,:),gnbSites(n).elevation+gnbSites(n).AntennaHeight,gNBLocations(1,:),wgs84);
    
    gNBsTable(n,:) = {gNBLabels(n),n,0,[x,y,z]};
end

% Assume all gNBs have the same number of antennas
numgNBAntennas = txArray.getNumElements;

Create UE Sites

Create the UE site objects for ray tracing analysis. The uesTable table contains name, indexing, and cartesian coordinates information associated with each UE. You can use this information later to create the nodes for network simulation.

% Create UE dipole antenna elements
pol1AntElement = createUEAntennaElement(0,fc); % Antenna element with 1st polarization angle
pol2AntElement = createUEAntennaElement(90,fc); % Antenna element with 2nd polarization angle
% Table with UE information
uesTable = table(Size=[numUEs 5], ...
    VariableTypes=["string","int8","int8","double","double"], ...
    VariableNames=["ueLabel","ueArrayIdx","ueID","latLon","xyz"]);
uesTable.latLon = zeros(numUEs,2); % Make each latLon row a 2-element vector: [lat lon]
uesTable.xyz = zeros(numUEs,3);    % Make each xyz row a 3-element vector: [x y z]
% Generate UE sites
ueSites = rxsite.empty();
for n = 1:numUEs
    rxArray = phased.NRRectangularPanelArray(Size=[ueArrayRectangularDim 1 1], ...
                        Spacing=[0.5*lambda*[1 1] 1 1], ...
                        ElementSet={clone(pol1AntElement),clone(pol2AntElement)});

    % Create UE rxsites
    ueSites(n) = rxsite(Name=ueLabels(n), ...
        Latitude=ueLocations(n,1), ...
        Longitude=ueLocations(n,2), ...
        Antenna=rxArray, ...
        AntennaHeight=ueAntennaHeight);
    
    % Equivalent cartesian coordinates: gNB of interest is origin
    [x,y,z] = latLonToCartesian(ueLocations(n,:),ueSites(n).elevation+ueSites(n).AntennaHeight,gNBLocations(1,:),wgs84);
    uesTable(n,:) = {ueLabels(n),n,0,[ueLocations(n,:)],[x,y,z]};
end

% Assume all UEs have the same number of antennas
numUEAntennas = rxArray.getNumElements;

Open Site Viewer to view the map and buildings, and display the gNB and UE locations.

% Open Site viewer and display tx and rx sites
if exist('viewer','var') && isvalid(viewer) % viewer handle exists and viewer window is open
    clearMap(viewer);
else
    viewer = siteviewer(Basemap="openstreetmap",Buildings=fileName);
end

show(ueSites)
show(gnbSites)

Perform Ray Tracing Analysis

Perform ray tracing analysis between each gNB and all UEs. Create a ray tracing propagation model with the specified maximum number of reflections and diffractions. You can specify other parameters associated with ray tracing in this propagation model object.

% Ray tracing propagation model
pm = propagationModel("raytracing",MaxNumReflections=maxNumReflections,MaxNumDiffractions=maxNumDiffractions,MaxRelativePathLoss=Inf);

The gnbSites and ueSites are arrays of txsite and rxsite objects respectively. You can use them to perform the downlink ray tracing analysis. To perform the uplink ray tracing analysis, create txsite and rxsite objects for UEs and gNBs respectively. You can also use these objects to create the uplink ray tracing channel models.

% Create sites for UL modeling
[txueUL,rxgNBUL] = createULSites(gnbSites,ueSites,db2pow(PtxUEdBm-30),ulFc);

Perform the ray tracing analysis between each gNB and each UE in the uplink (UL) and downlink (DL) direction.

For TDD, the example assumes channel reciprocity. In that case, it skips the uplink ray tracing calculation. Instead, it uses the information in the downlink rays to calculate the uplink rays.

raysDL = cell(1,numgNBs);
raysUL = cell(1,numgNBs);
for n = 1:numgNBs
    % DL ray tracing analysis
    raysDL{n} = raytrace(gnbSites(n),ueSites,pm);
    if duplexingMode == "TDD"
        % Assume channel reciprocity, no need to ray trace in the uplink,
        raysUL{n} = dl2ulRays(raysDL{n});
    else % FDD
        % Uplink ray tracing analysis
        for m=1:numel(txueUL)
            ulrays = raytrace(txueUL(m),rxgNBUL(n),pm);
            raysUL{n}{m} = ulrays{1};
        end
    end
end

Simulate Network

Now set up and perform the network simulation using the results of the ray tracing analysis.

Initialize the network simulator.

networkSimulator = wirelessNetworkSimulator.init;

Create gNB Nodes

In the previous sections you used txsite and rxsite objects to perform ray tracing analysis. The network simulation uses nrGNB and nrUE objects to represent the different nodes in the simulation.

Create nrGNB nodes for network simulation. Specify the position of each node using the cartesian coordinates that correspond to the geographical coordinates (latitude and longitude) stored in gNBsTable.

gnbsxyz = gNBsTable.xyz;

gnbs = nrGNB(Position=gnbsxyz, ...
    NumTransmitAntennas=numgNBAntennas(1), ...
    NumReceiveAntennas=numgNBAntennas(1), ...
    CarrierFrequency=fc, ...
    ChannelBandwidth=BW, ...
    NumResourceBlocks=NRB, ...
    SubcarrierSpacing=SCS, ...
    DuplexMode=duplexingMode, ...
    TransmitPower=PtxgNBdBm, ...
    NoiseFigure=6, ...
    ReceiveGain=6, ...
    SRSPeriodicityUE=10, ...
    PHYModel="abstract-phy", ...
    Name=gNBsTable.gNBLabel');

% gNB node IDs to table
gNBsTable.gnbID = [gnbs.ID]';

Configure the scheduler. Specify the scheduling algorithm, the downlink channel state information measurement signal, and the link adaptation configuration parameters.

% Link adaptation structure
laConfig = struct(StepUp=0.9,StepDown=0.1,InitialOffset=1,ResetOffsetOnCSIReport=false);

% Scheduler configuration
configureScheduler(gnbs,Scheduler="RoundRobin",CSIMeasurementSignalDL="CSI-RS", ...
                   LinkAdaptationConfigDL=laConfig,LinkAdaptationConfigUL=laConfig);

Create UE Nodes

Create nrUE nodes per UE for network simulation. Specify the position of each node using the cartesian coordinates that correspond to the geographical coordinates (latitude and longitude) stored in uesTable.

% Create UE objects
uesxyz = uesTable.xyz;

ues = nrUE(Position=uesxyz, ...
    NumTransmitAntennas=numUEAntennas(1), ...
    NumReceiveAntennas=numUEAntennas(1), ...
    TransmitPower=PtxUEdBm, ...
    NoiseFigure=6, ...
    ReceiveGain=6, ...
    Name=uesTable.ueLabel');

% UE node IDs to table
uesTable.ueID = [ues.ID]';

Set Up Network Simulator

Add the nrGNB and nrUE nodes to the network simulator.

addNodes(networkSimulator,gnbs)
addNodes(networkSimulator,ues)

Connect UEs to gNBs

Connect the UE nodes to the gNB nodes and enable full-buffer traffic.

Generate a connection matrix where each row corresponds to a UE and each column corresponds to a gNB. An entry of 1 in the matrix indicates that the corresponding UE (row) is connected to the corresponding gNB (column). In this example, the system assigns each UE to the closest gNB.

[connectionMatrix,gNBsTable,uesTable] = generateConnectionMatrix(gnbSites,ueSites,gNBsTable,uesTable);

Connect gNBs to UEs using the connection matrix information. Specify full-buffer traffic for both the uplink and downlink direction.

for n=1:numgNBs
    gnbIdx = gNBsTable.gnbArrayIdx(n);
    targetUEsIdx = gNBsTable.AssociatedUEsIdx{n};    
    if ~isempty(targetUEsIdx)
        connectUE(gnbs(gnbIdx),ues(targetUEsIdx),FullBufferTraffic="on");
    end
    associatedUEsStr = mat2str([ues(cell2mat(gNBsTable.AssociatedUEsIdx(n))).Name]);
disp("UEs associated to gNB "+gnbs(n).ID+" ("""+gnbs(n).Name+"""): "+associatedUEsStr)
end
UEs associated to gNB 1 ("gNB1"): ["UE1" "UE2"]
UEs associated to gNB 2 ("gNB2"): ["UE3" "UE4" "UE5" "UE7"]
UEs associated to gNB 3 ("gNB3"): "UE6"

Create Ray Tracing Channel Models

Use the ray tracing rays to create a ray tracing channel for each gNB-to-UE link.

Calculate OFDM-related information. The example uses this information later to obtain the signal sampling rate and related 5G numerology information.

% Calculate OFDM information
waveformInfo = nrOFDMInfo(NRB,SCS);
sr = waveformInfo.SampleRate;

% Channel filtering flag based on PHY model used. Assume all gNBs use the
% same PHY model
channelFilteringFlag = (gnbs(1).PHYModel~="abstract-phy");

rtChannelRegistry = RayTracingChannelRegistry([gnbs.ID ues.ID],sr,channelFilteringFlag);

Create a ray tracing channel for each gNB-UE pair.

createRayTracingChannels(rtChannelRegistry,raysDL,raysUL, ...
                        gnbSites,ueSites,txueUL,rxgNBUL, ...
                        gnbs,ues);

Use a function handle to add the ray tracing channel model to the network simulator.

addChannelModel(networkSimulator,@rtChannelRegistry.applyChannel);

Run the Simulation

Create the metric visualizer and the simulation scheduling logger for the gNBs to store simulation metrics and run-time scheduling information.

[metricsVisualizer,simSchedulingLogger] = createVisualizerAndLogger(simulationTime,1:numgNBs,gnbs,ues);

Run the simulation for the specified simulation time.

run(networkSimulator,simulationTime)

Obtain the log tables for all the UEs.

logTables = cell(numgNBs,1);
for n = 1:numgNBs
    uesOfInterest = ues(ismember([ues.ID],[gnbs(n).UENodeIDs])); % UEs associated to this gNB
    logTables{n} = getSimulationLogsTables(simSchedulingLogger{n},gnbs(n).DuplexMode,[uesOfInterest.RNTI]);
end

Evaluate Simulation Results

You can inspect the simulation results using the generated log tables. This example shows how to obtain the downlink and uplink throughput, the average downlink and uplink SINR, and the MCS at the end of the simulation for all the gNBs and their associated UEs.

gNBsMetricsSummary = metricsSummary(gnbs,ues,metricsVisualizer,logTables,simulationTime);
display(gNBsMetricsSummary)
gNBsMetricsSummary=7×10 table
     gNB       UE      DL Thput (Mbps)    Avg DL SINR (dB)    DL MCS    Max DL NumLayers    UL Thput (Mbps)    Avg UL SINR (dB)    UL MCS    Max UL NumLayers
    ______    _____    _______________    ________________    ______    ________________    _______________    ________________    ______    ________________

    "gNB1"    "UE1"         38.862             22.541           25             2                47.444              24.933           24             3        
    "gNB1"    "UE2"         33.771             16.396           15             3                53.093              25.121           26             3        
    "gNB2"    "UE3"         6.7888             13.478           21             2                20.445              27.539           26             2        
    "gNB2"    "UE4"        0.04544             8.4092           22             2                13.318              16.502           18             4        
    "gNB2"    "UE5"         2.4666             10.828           23             2                15.656              17.763           20             4        
    "gNB2"    "UE7"         6.6589             15.415           20             2                23.525              21.474           21             4        
    "gNB3"    "UE6"          23.34             13.289           18             2                57.873              23.767           24             2        

Display downlink throughput per UE on the map.

figure;
[~,loc] = ismember(gNBsMetricsSummary.UE, uesTable.ueLabel);
plotNodeLocations(fileName,"Downlink Throughput (Mbps)",gNBLocations, ...
uesTable.latLon(loc,:),gNBLabels,gNBsMetricsSummary.UE,gNBsMetricsSummary.("DL Thput (Mbps)"));

Figure contains an axes object with type geoaxes. The geoaxes object contains 13 objects of type polygon, scatter, text.

You can also display the peak and achieved throughput, spectral efficiency, and block error rate.

for m=1:numgNBs
disp(gnbs(m).Name+"---------------------------"+newline)
    if ~isempty(metricsVisualizer{m})
displayPerformanceIndicators(metricsVisualizer{m});
    end
end
gNB1---------------------------
Peak UL throughput: 210.31 Mbps
Achieved cell UL throughput: 100.54 Mbps
Achieved UL throughput for each UE: [47.44        53.09]
Peak UL spectral efficiency: 10.52 bits/s/Hz
Achieved UL spectral efficiency for cell: 5.03 bits/s/Hz 
Block error rate for each UE in the UL direction: [0.222       0.111]

Peak DL throughput: 290.09 Mbps
Achieved cell DL throughput: 72.63 Mbps
Achieved DL throughput for each UE: [38.86        33.77]
Peak DL spectral efficiency: 14.50 bits/s/Hz
Achieved DL spectral efficiency for cell: 3.63 bits/s/Hz
Block error rate for each UE in the DL direction: [0.138        0.31]
gNB2---------------------------
Peak UL throughput: 210.31 Mbps
Achieved cell UL throughput: 72.94 Mbps
Achieved UL throughput for each UE: [20.44        13.32        15.66        23.53]
Peak UL spectral efficiency: 10.52 bits/s/Hz
Achieved UL spectral efficiency for cell: 3.65 bits/s/Hz 
Block error rate for each UE in the UL direction: [0       0.611       0.556       0.333]

Peak DL throughput: 290.09 Mbps
Achieved cell DL throughput: 15.96 Mbps
Achieved DL throughput for each UE: [6.79        0.05        2.47        6.66]
Peak DL spectral efficiency: 14.50 bits/s/Hz
Achieved DL spectral efficiency for cell: 0.80 bits/s/Hz
Block error rate for each UE in the DL direction: [0.621       0.897       0.793       0.586]
gNB3---------------------------
Peak UL throughput: 210.31 Mbps
Achieved cell UL throughput: 57.87 Mbps
Achieved UL throughput for each UE: [57.87]
Peak UL spectral efficiency: 10.52 bits/s/Hz
Achieved UL spectral efficiency for cell: 2.89 bits/s/Hz 
Block error rate for each UE in the UL direction: [0.222]

Peak DL throughput: 290.09 Mbps
Achieved cell DL throughput: 23.34 Mbps
Achieved DL throughput for each UE: [23.34]
Peak DL spectral efficiency: 14.50 bits/s/Hz
Achieved DL spectral efficiency for cell: 1.17 bits/s/Hz
Block error rate for each UE in the DL direction: [0.655]

Using the log tables generated, you can investigate how certain metrics evolve throughout the simulation. For example, you can inspect the MCS and SINR values in each slot for the UEs.

plotMCSandSINR(logTables,ues,gnbs,waveformInfo);

Figure contains 6 axes objects. Axes object 1 with title Downlink MCS, ylabel MCS contains 2 objects of type line. One or more of the lines displays its values using only markers Axes object 2 with title Downlink SINR, ylabel SINR (dB) contains 2 objects of type line. One or more of the lines displays its values using only markers Axes object 3 with title Downlink Num Layers, ylabel Num Layers contains 2 objects of type line. One or more of the lines displays its values using only markers Axes object 4 with title Uplink MCS, ylabel MCS contains 2 objects of type line. One or more of the lines displays its values using only markers Axes object 5 with title Uplink SINR, ylabel SINR (dB) contains 2 objects of type line. One or more of the lines displays its values using only markers Axes object 6 with title Uplink Num Layers, ylabel Num Layers contains 2 objects of type line. One or more of the lines displays its values using only markers These objects represent UE1, UE2.

Figure contains 6 axes objects. Axes object 1 with title Downlink MCS, ylabel MCS contains 4 objects of type line. One or more of the lines displays its values using only markers Axes object 2 with title Downlink SINR, ylabel SINR (dB) contains 4 objects of type line. One or more of the lines displays its values using only markers Axes object 3 with title Downlink Num Layers, ylabel Num Layers contains 4 objects of type line. One or more of the lines displays its values using only markers Axes object 4 with title Uplink MCS, ylabel MCS contains 4 objects of type line. One or more of the lines displays its values using only markers Axes object 5 with title Uplink SINR, ylabel SINR (dB) contains 4 objects of type line. One or more of the lines displays its values using only markers Axes object 6 with title Uplink Num Layers, ylabel Num Layers contains 4 objects of type line. One or more of the lines displays its values using only markers These objects represent UE3, UE4, UE5, UE7.

Figure contains 6 axes objects. Axes object 1 with title Downlink MCS, ylabel MCS contains a line object which displays its values using only markers. Axes object 2 with title Downlink SINR, ylabel SINR (dB) contains a line object which displays its values using only markers. Axes object 3 with title Downlink Num Layers, ylabel Num Layers contains a line object which displays its values using only markers. Axes object 4 with title Uplink MCS, ylabel MCS contains a line object which displays its values using only markers. Axes object 5 with title Uplink SINR, ylabel SINR (dB) contains a line object which displays its values using only markers. Axes object 6 with title Uplink Num Layers, ylabel Num Layers contains a line object which displays its values using only markers. This object represents UE6.

The simulation scenario is static. Therefore any variations in the results displayed are caused by the dynamic interference introduced by the scheduler, and the link adaptation algorithm. Depending on which UE is scheduled for downlink transmission, the interference level on other UEs can fluctuate. The downlink MCS decreases as the simulation progresses. This decrease is introduced by the link adaptation algorithm. This behavior happens because:

  • The initial channel state information (CSI) estimation uses reference signals without precoding (CSI-RS and SRS). The gNB chooses the channel rank and the MCS using the SINR measured during the CSI estimation.

  • Data packets use precoding. Therefore, the SINR experienced by UEs is different from the SINR observed during the CSI estimation.

  • This SINR mismatch can result in packet reception errors. The link adaptation algorithm adjusts the downlink MCS to compensate for the SINR mismatch, reduce the probability of packet reception failure, and increase the throughput.

References

[1] The OpenStreetMap file is downloaded from https://www.openstreetmap.org, which provides access to crowd-sourced map data all over the world. The data is licensed under the Open Data Commons Open Database License (ODbL), https://opendatacommons.org/licenses/odbl/.

Local Functions

function [x,y,z] = latLonToCartesian(latLon, totalAntennaHeight, latLonReference, refEllipsoid)
% Convert lat/lon and height position into cartesian representation (x,y,z)
% relative to a reference point. LATLON is a two-element vector with the
% latitude and longitude values to convert, and TOTALANTENNAHEIGHT is the
% total antenna height, including terrain, building, and antenna height.
% LATLONREF is a two element vector with the reference latitude and
% longitude. The conversion assumes a height of 0 meters for the reference
% point. REFELLIPSOID is the reference ellipsoid object

    [x,y,z] = geodetic2enu(latLon(1),latLon(2),totalAntennaHeight,latLonReference(1),latLonReference(2),0,refEllipsoid);
end

function validategNBArraySize(arraySize)
% Validate arraySize ([rows columns]) array size.
% Valid sizes:
%  [1 1]
%  [2 1]
%  [2 2]
%  [2 4]
%  [4 4]
        
    % Valid sizes
    validSizes = [1 1;
                  2 1;
                  2 2;
                  2 4;
                  4 4];
    
    % Check arraySize validity
    if ~ismember(arraySize(:).',validSizes,"rows")
        error("Invalid gNB array size ("+mat2str(arraySize)+"). Valid sizes are: "+mat2str(validSizes))
    end
end

function BW = validateNumerology(SCS,NRB)
% Validate subcarrier spacing and grid size values and return the bandwidth in Hz

    % Channel transmission bandwidth configuration table (SCS, BW and NRB)
    txBwConfig = nrDLCarrierConfig.FR1BandwidthTable;
    rowName = string(SCS+"kHz"); % SCS to string

    % Validate SCS
    if ~ismember(rowName, string(txBwConfig.Properties.RowNames))
        error("Invalid SCS ("+SCS+"). Value must be a member of: "+ ...
            strjoin(erase(string(txBwConfig.Properties.RowNames),"kHz")));
    end

    nrbRow = txBwConfig{rowName, :};             % Extract the row of NRB values
    validMask = ~isnan(nrbRow);                   % Valid entries are those that are not NaN
    matchMask = validMask & (nrbRow == NRB);      % Find exact NRB matches among valid entries
    if any(matchMask)
        % Column names where there is a match
        bwVars = string(txBwConfig.Properties.VariableNames(matchMask));
        % Convert "XXMHz" to numeric Hz        
        BW = str2double(erase(bwVars, "MHz"))*1e6; % Remove the "MHz" suffix and convert to double
    else
        disp("Valid channel transmission bandwidth configurations: SCS, bandwidth and NRBs.")
        disp(txBwConfig)
        [~,idx]=min(abs(nrbRow-NRB));
        error("Invalid combination of SCS ("+SCS+" kHz) and NRB ("+NRB+"). "+newline+ ...
                "Select a valid combination from the channel transmission bandwidth configuration table."+newline+ ...
                "The closest set of values in the table is SCS = "+SCS+"kHz, and NRB = "+nrbRow(idx));
    end
end

function [dlFc,ulFc] = fddCarrierFreq(duplexingMode,fc,channelBandwidth)
% Return the downlink and uplink channels center carrier frequency    

    if duplexingMode=="TDD"
        dlFc = fc;
        ulFc = fc;
    else
        % DL/UL band offset is  +/- guardBand*0.5 Hz of the carrier frequency and is
        % channelBandwidth Hz wide.
        guardBand = 140e6;
        dlFc = fc + guardBand/2 + channelBandwidth/2;
        ulFc = fc - guardBand/2 - channelBandwidth/2;
    end
end

function plotMCSandSINR(logTables,ues,gnbs,waveformInfo)
% Plot the MCS and SINR per slot for all gNBs and UEs simulated.
    for m=1:numel(logTables)
        if ~isempty(logTables{m})
            uesOfInterest = ues(ismember([ues.ID],[gnbs(m).UENodeIDs])); 
            ueName = [uesOfInterest.Name];
            figure; tl = tiledlayout(2,3);
            rntisLocal = unique(logTables{m}.grantLogsTable.RNTI); % UEs assigned to this gNB
            for n=1:numel(rntisLocal)
                [dlmcs,dlsinr,dlnumLayers,dlSlotNum] = getMCSSINR(logTables{m}.dlUELogs{n},waveformInfo.SlotsPerFrame);
                [ulmcs,ulsinr,ulnumLayers,ulSlotNum] = getMCSSINR(logTables{m}.ulUELogs{n},waveformInfo.SlotsPerFrame);
                nexttile(tl,1); plot(dlSlotNum,dlmcs,'.'); hold on; grid on; title("Downlink MCS"); ylabel("MCS")
                nexttile(tl,2); plot(dlSlotNum,dlsinr,'.'); hold on; grid on; title("Downlink SINR"); ylabel("SINR (dB)");
                nexttile(tl,3); plot(dlSlotNum,dlnumLayers,'.'); hold on; grid on; title("Downlink Num Layers"); ylabel("Num Layers");
                nexttile(tl,4); plot(ulSlotNum,ulmcs,'.'); hold on; grid on; title("Uplink MCS"); ylabel("MCS")
                nexttile(tl,5); plot(ulSlotNum,ulsinr,'.'); hold on; grid on; title("Uplink SINR"); ylabel("SINR (dB)")
                nexttile(tl,6); plot(ulSlotNum,ulnumLayers,'.'); hold on; grid on; title("Uplink Num Layers"); ylabel("Num Layers");
            end
            legendtext = ueName;
            legend(legendtext)
            xlabel(tl,"Slot number"); title(tl,gnbs(m).Name+". MCS and SINR ("+gnbs(1).DuplexMode+")") % assume all gNBs have the same duplexing mode
            leg = legend(legendtext,Orientation="vertical",NumColumns=2);leg.Layout.Tile = 'east';
        end
    end
end

function ueAntennaElement = createUEAntennaElement(polAngleDeg,fc)
% Create a custom antenna element using a short dipole element as a
% reference. POLANGLEDEG is the polarization angle in degrees relative to
% the z axes of the reference short dipole element. FC is the frequency
% used to calculate the antenna response.

    % Compute polarization direction vector
    polVector = [0; sind(polAngleDeg); cosd(polAngleDeg)]; % y–z plane rotation
    
    % Create reference short dipole antenna element
    shortDipole = phased.ShortDipoleAntennaElement(AxisDirection="Custom",CustomAxisDirection=polVector);
    
    % Define angles
    azAngles = -180:180;  % Azimuth angles
    elAngles = -90:90;    % Elevation angles

    % Extract H and V patterns in magnitude and phase
    patHmag = zeros(numel(elAngles),numel(azAngles));
    patVmag = patHmag;
    patHphase = patHmag;
    patVphase = patHmag;

    for m = 1:numel(elAngles)
        pat = step(shortDipole,fc,[azAngles;elAngles(m)*ones(1,numel(azAngles))]);

        patHmag(m,:) = mag2db(abs(pat.H)); % Store horizontal pattern magnitude
        patVmag(m,:) = mag2db(abs(pat.V)); % Store vertical pattern magnitude
        patHphase(m,:) = rad2deg(angle(pat.H));        % Store horizontal pattern phase
        patVphase(m,:) = rad2deg(angle(pat.V));        % Store vertical pattern phase
    end
    
    % Initialize the custom antenna element with polarization applied
    ueAntennaElement = phased.CustomAntennaElement(PatternCoordinateSystem='az-el', ...
            AzimuthAngles=azAngles,ElevationAngles=elAngles, ...
            SpecifyPolarizationPattern=true, ...
            HorizontalMagnitudePattern=patHmag,HorizontalPhasePattern=patHphase, ...
            VerticalMagnitudePattern=patVmag,VerticalPhasePattern=patVphase);
end

function [metricsVisualizer,simSchedulingLogger] = createVisualizerAndLogger(simulationTime,gNBsOfInterestID,gnbs,ues)
% Create metric visualizer and scheduling logger objects for the gNBs of
% interest
    numFrameSimulation = simulationTime/10e-3;
    metricsVisualizer = cell(numel(gNBsOfInterestID),1);
    simSchedulingLogger = cell(numel(gNBsOfInterestID),1);
    
    for n = 1:numel(gNBsOfInterestID)
        uesOfInterest = ues(ismember([ues.ID],[gnbs(gNBsOfInterestID(n)).UENodeIDs])); % UEs associated to this gNB
        if ~isempty(uesOfInterest)
            metricsVisualizer{n} = helperNRMetricsVisualizer(gnbs(gNBsOfInterestID(n)),uesOfInterest, ...
                PlotSchedulerMetrics=false,PlotPhyMetrics=false);
            % Create an object to log scheduler traces
            simSchedulingLogger{n} = helperNRSchedulingLogger(numFrameSimulation,gnbs(gNBsOfInterestID(n)),uesOfInterest);
        else
            metricsVisualizer{n} = [];
        end    
    end
end

function metricsSummary = metricsSummary(gnbs,ues,metricsVisualizer,logTables,simulationTime)
% Compute and summarize key performance metrics for each UE associated with
% gNBs of interest and aggregate results into a table. The summary includes
% throughput, average SINR, and MCS for both downlink and uplink per UE.
    metricsSummary = [];
    numgNBs = numel(gnbs);
    for m=1:numgNBs
        % UEs associated to this gNB
        uesAssociatedTogNB = ues(ismember([ues.ID],[gnbs(m).UENodeIDs])); 
        if ~isempty(uesAssociatedTogNB)
            rntis = zeros(numel(uesAssociatedTogNB),1);
            dlAvgSINR = zeros(numel(uesAssociatedTogNB),1);
            ulAvgSINR = zeros(numel(uesAssociatedTogNB),1);
            dlMCS = zeros(numel(uesAssociatedTogNB),1);
            ulMCS = zeros(numel(uesAssociatedTogNB),1);
            dlPeakNumLayers = zeros(numel(uesAssociatedTogNB),1);
            ulPeakNumLayers = zeros(numel(uesAssociatedTogNB),1);
            % Calculate average SINR and MCS
            slotOffset = 10; % start slot to obtain average metrics (avoid transient)
            % if simulation has less than 10 slots, use two thirds for
            % average metrics            
            for n=1:numel(uesAssociatedTogNB)
                if slotOffset > numel(logTables{m}.dlUELogs{n}.SINR) 
                    slotOffset = floor(numel(logTables{m}.dlUELogs{n}.SINR)/3);
                end
                rntis(n) = uesAssociatedTogNB(n).RNTI;
                dlAvgSINR(n) = mean(logTables{m}.dlUELogs{n}.SINR(slotOffset:end));
                ulAvgSINR(n) = mean(logTables{m}.ulUELogs{n}.SINR(slotOffset:end));
                dlMCS(n) = logTables{m}.dlUELogs{n}.("MCS Index")(end);
                ulMCS(n) = logTables{m}.ulUELogs{n}.("MCS Index")(end);
                dlPeakNumLayers(n) = max(logTables{m}.dlUELogs{n}.NumLayers);
                ulPeakNumLayers(n) = max(logTables{m}.ulUELogs{n}.NumLayers);
            end
            [~, rntiIdx] = sort(rntis);
            dlAvgSINR = dlAvgSINR(rntiIdx);
            ulAvgSINR = ulAvgSINR(rntiIdx);
            dlMCS = round(dlMCS(rntiIdx));
            ulMCS = round(ulMCS(rntiIdx));
            dlPeakNumLayers = dlPeakNumLayers(rntiIdx);
            ulPeakNumLayers = ulPeakNumLayers(rntiIdx);
    
            % Calculate throughput
            simMetrics = getMetrics(metricsVisualizer{m});
            dlTh = 1e-6*8*simMetrics.MACMetrics.("DL Throughput Bytes")/simulationTime;
            ulTh = 1e-6*8*simMetrics.MACMetrics.("UL Throughput Bytes")/simulationTime;
            ueName = [uesAssociatedTogNB.Name]';
            gNBName = repmat(gnbs(m).Name,size(ueName,1),1);
            metricsSummarygNB = table(gNBName,ueName,dlTh,dlAvgSINR,dlMCS,dlPeakNumLayers,ulTh,ulAvgSINR,ulMCS,ulPeakNumLayers, ...
                VariableNames=["gNB" "UE" "DL Thput (Mbps)" "Avg DL SINR (dB)" "DL MCS" "Max DL NumLayers" "UL Thput (Mbps)" "Avg UL SINR (dB)" "UL MCS" "Max UL NumLayers"]);
    
            metricsSummary = [metricsSummary; metricsSummarygNB];
        end
    end
end

function [connectionMatrix,gNBsTable,uesTable] = generateConnectionMatrix(gnbSites,ueSites,gNBsTable,uesTable)
% Generate a connection matrix between gNBs and UEs. connectionMatrix is a
% matrix of size numUEs by numgNBs, where numUEs is the number of UEs
% (rxsites) in the ueSites array, and numgNBs is the number of gNBs
% (txsites) in the gnbSites array. A 1 entry in the matrix indicates that
% the corresponding gNB (row number) is connected to the UE corresponding
% to the column number.
%
% A connection is established based on distance. UEs are connected to the
% closest gNB.

    % Distance between all gNBs and all UEs
    d = distance(gnbSites,ueSites);
    [~,idxgnb] = min(d,[],2);
    connectionMatrix = ((1:size(d,2)) == idxgnb);

    % Add connection information to the gNB and UE tables
    for n=1:size(connectionMatrix,2) % for all gNBs
        gnbcol = connectionMatrix(:,n);
        if any(gnbcol)
            rowLogicalIdx = gNBsTable.gnbArrayIdx==n;
            gNBsTable.AssociatedUEsIdx(rowLogicalIdx) = {find(gnbcol)'};
            gNBsTable.AssociatedUEs(rowLogicalIdx) = {uesTable.ueLabel(gnbcol)'};
        end
    end
    % Connection matrix to table
    for n=1:size(connectionMatrix,1) % for all UEs
        uerow = connectionMatrix(n,:);
        if any(uerow)
            logicalIdx = uesTable.ueArrayIdx==n;
            uesTable.AssociatedgNBsIdx(logicalIdx) = {find(uerow)'};
            uesTable.AssociatedgNBs(logicalIdx) = string(gnbSites(logical(uerow)).Name);
        end
    end

end

function [txUL,rxUL] = createULSites(tx,rx,PtxUEW,ulFc)
% Turn tx sites into rxsites. This is used to perform ray tracing in the
% uplink
    % Create rxsites for the gNBs
    ngNBs = numel(tx);
    rxUL = rxsite.empty();
    for n=ngNBs:-1:1
        rxArrayUL = tx(n).Antenna.clone();
        rxUL(n) = rxsite(Latitude=tx(n).Latitude,Longitude=tx(n).Longitude, ...
            Antenna=rxArrayUL,AntennaAngle=tx(n).AntennaAngle, ...
            AntennaHeight=tx(n).AntennaHeight);
    end
    
    % Create txsites for the UEs
    nUEs = numel(rx);    
    fc = ulFc;
    txUL = txsite.empty();
    for n=nUEs:-1:1
        txArrayUL = rx(n).Antenna.clone();
        txUL(n) = txsite(Latitude=rx(n).Latitude,Longitude=rx(n).Longitude, ...
            TransmitterPower=PtxUEW,TransmitterFrequency=fc, ...
            Antenna=txArrayUL,AntennaHeight=rx(n).AntennaHeight,AntennaAngle=rx(n).AntennaAngle);
    end
end

function createRayTracingChannels(rtChannelRegistry,raysDL,raysUL,txgnbDL,rxueDL,txueUL,rxgNBUL,gnbs,ues)
% Create a ray tracing channel for each gNB-UE pair. Skip channel creation
% if no rays exist between the gNB and the UE.
    numgNBs = numel(gnbs);
    numUEs = numel(ues);
    % Create uplink and downlink ray tracing channels
    for gNBIdx = 1:numgNBs
        % Create channels between all UEs and all gNBs
        for ueIdx=1:numUEs
            % Downlink channels. Create a ray tracing channel model per UE
            if ~isempty(raysDL{gNBIdx}{ueIdx})
                rtChannelRegistry.addRayTracingChannel(raysDL{gNBIdx}{ueIdx},txgnbDL(gNBIdx),rxueDL(ueIdx),gnbs(gNBIdx).ID,ues(ueIdx).ID);
            end
    
            % Uplink channels
            if ~isempty(raysUL{gNBIdx}{ueIdx})
                rtChannelRegistry.addRayTracingChannel(raysUL{gNBIdx}{ueIdx},txueUL(ueIdx),rxgNBUL(gNBIdx),ues(ueIdx).ID,gnbs(gNBIdx).ID);
            end
        end
    end
end

function [mcs,sinr,numLayers,slotNum] = getMCSSINR(ueLogs,slotsPerFrame)
% Extract the MCS, SINR and absolute slot number from the UE logs UELOGS
% given the number of slots per frame SLOTSPERFRAME.
    % Calculate absolute slot number
    fn = ueLogs.Frame;
    sn = ueLogs.Slot;    
    slotN = fn.*slotsPerFrame + sn;

    % Sort absolute slot number in ascending order
    [slotNum,sortIdx] = sort(slotN);

    % Extract and reorder MCS and SINR
    mcs = ueLogs.("MCS Index");
    sinr = ueLogs.SINR;
    numLayers = ueLogs.NumLayers;
    mcs = mcs(sortIdx);
    sinr = sinr(sortIdx);    
end


function raysUL = dl2ulRays(rays)
% Convert downlink rays to uplink rays when we assume channel reciprocity.
% This function swaps the transmitter and receiver locations, "flips" the
% interaction matrix, and transposes the polarization matrix. Other uplink
% ray properties remain the same. Constructing the uplink rays this way
% results on the swapping of the angles of arrival and departure.
    raysUL = rays; % Allocate
    for n = 1:numel(rays) % loop over all receivers for this transmitter
        for m = 1:numel(rays{n}) % loop over all rays for this tx/rx link
            raysUL{n}(m).TransmitterLocation = rays{n}(m).ReceiverLocation;
            raysUL{n}(m).ReceiverLocation = rays{n}(m).TransmitterLocation;
            raysUL{n}(m).PolarizationMatrix = rays{n}(m).PolarizationMatrix.';
            raysUL{n}(m).Interactions = flip(rays{n}(m).Interactions);
        end
    end
end

See Also

| | |