{ "cells": [ { "cell_type": "markdown", "id": "8136f6c4", "metadata": {}, "source": [ "# Link Adaptation with CSI Feedback, HARQ, and OLLA Along a UE Trajectory\n", "\n", "This notebook demonstrates several **NeoRadium** features in a compact end-to-end link-adaptation example. It uses a DeepMIMO scenario and shows how channel conditions along a UE trajectory affect beam selection, CQI, BLER, and throughput.\n", "\n", "The workflow starts by configuring the bandwidth part, antenna panels, trajectory-based channel, and CSI-RS resources. The receiver processes CSI-RS measurements and feeds back beam-sweeping, beam-probing, and RI/PMI/CQI reports. The transmitter then uses this feedback to select the precoder, number of layers, modulation, and code rate for PDSCH transmission.\n", "\n", "The example also includes incremental-redundancy **HARQ** and **OLLA**. HARQ handles retransmissions after failed transport blocks, while OLLA adjusts the reported CQI through a link-adaptation offset based on decoding outcomes. Together, these mechanisms illustrate how feedback and error statistics can be used to adapt the downlink transmission as the UE moves.\n", "\n", "For clarity, the simulation is intentionally simplified. It uses a fixed thermal-noise model and disables per-location channel-gain normalization so that large-scale channel variations along the trajectory remain visible. The final animation summarizes the UE movement and plots the adjusted CQI, BLER, and throughput over the trajectory. This notebook is intended as an API-focused demonstration rather than a standards-complete model of 5G NR scheduling, beam management, or CSI reporting." ] }, { "cell_type": "code", "execution_count": 1, "id": "0d0aaa42", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import time\n", "import matplotlib\n", "from IPython.display import HTML, Markdown, display\n", "\n", "from neoradium import DeepMimoData, TrjChannel, BandwidthPart, AntennaPanel, PDSCH, random\n", "from neoradium import CsiRs, CsiRsSet, CsiRsConfig, CsiReport, CsiReportMan, OLLA\n", "from neoradium.utils import toDb, toLinear" ] }, { "cell_type": "code", "execution_count": 2, "id": "05aeedf9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "DeepMimoData Properties:\n", " Scenario: asu_campus_3p5\n", " Version: 4.0.0a3\n", " UE Grid: rx_grid\n", " Grid Size: 411 x 321\n", " Base Station: BS (at [166. 104. 22.])\n", " Total Grid Points: 131,931\n", " UE Spacing: [1. 1.]\n", " UE bounds (xyMin, xyMax) [-225.55 -160.17], [184.45 159.83]\n", " UE Height: 1.50\n", " Carrier Frequency: 3.5 GHz\n", " Num. paths (Min, Avg, Max): 0, 6.21, 10\n", " Num. total blockage: 46,774\n", " LOS percentage: 19.71%\n", "\n" ] } ], "source": [ "# Replace this with the folder on your computer where you store DeepMIMO scenarios\n", "dataFolder = \"/data/RayTracing/DeepMIMO/Scenarios/V4/\"\n", "DeepMimoData.setScenariosPath(dataFolder)\n", "\n", "# Create a DeepMimoData object\n", "dmData = DeepMimoData(\"asu_campus_3p5\")\n", "dmData.print()" ] }, { "cell_type": "code", "execution_count": 3, "id": "332d1852-cbae-4144-9fc0-96dea39e79ec", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Trajectory Properties:\n", " start (x,y,z): (-49.55, -83.17, 1.50)\n", " No. of points: 12,378\n", " curIdx: 0 (0.00%)\n", " curSpeed: [ 0. 14.93 0. ]\n", " Total distance: 185.23 meters\n", " Total time: 12.377 seconds\n", " Average Speed: 14.965 m/s\n", " Carrier Frequency: 3.5 GHz\n", " Paths (Min, Avg, Max): 4, 9.04, 10\n", " Totally blocked: 0\n", " LOS percentage: 0.00%\n", "\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAqQAAAG/CAYAAACOv8VsAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAWzpJREFUeJzt3QeYU2XaxvFn6L0piHQQQWk2ENC1IGBjERWl2rGsXcGGFRtgw4JiZQXssmLDLkXFtoAVpYlKk6ZI75Dvut8vJ5sMyUxmJpOT8v9dV2Dm5ExyTpKZ3HnelhMIBAIGAAAA+KSEX3cMAAAACIEUAAAAviKQAgAAwFcEUgAAAPiKQAoAAABfEUgBAADgKwIpAAAAfEUgBQAAgK9K+Xv3AFLVtm3b7Nlnn7UffvjBNm7caLfccovts88+fh8WkuC+++6zTZs22W233cbjDSApclipCUiMSZMm2XPPPee+vuOOO6xBgwa77fPwww/bt99+a1WrVnVfp7ITTjjBfvnlF7vqqqusYsWK1q1bN6tZs2bM877hhhtsv/32y/M2f/31V3vrrbfc7WqRuKZNm1qPHj2sSZMmUfdXGH7vvfds0aJF7jFr2bKl279SpUpR93/nnXds4sSJ9vjjj9uuXbvsvPPOc9vPOeccO/rooyP2/fnnn+3ee++1iy66yDp27Oi2eT/Tvn17u/jii/M8l61bt7pzmT59uq1Zs8Y9NkceeaQde+yxlpOTs9v+GzZssJdfftnmzZtnmzdvduH++OOPz/cxKy4PPfSQ/fnnn3bXXXftdl2XLl3cOc2YMcOXYwOQfWiyBxJk9uzZNnbsWHdRZTFaILnpppvc9a+88kpKP+6LFy+2999/34XMSy+91AW6aGE0/LyXL18e8/YU9HRbzZs3tylTplizZs1s//33t08//dRt03Xax6OweuGFF9ohhxxi3333ndtHgXTYsGFWt25dF1KjeeSRR2zp0qWh+/SeD53Dzp07I/b9448/3HULFiyIOE5t++STT/J8fKZNm2b77ruvDR482CpUqGBt27a17du32xlnnOG+/u233yL2/+ijj6xhw4b24IMPWpUqVdy5//e//7VWrVq5n/HDxx9/bG+88YYv9w0AudFkDySYqm0KNbfeemtEpezVV191oUWBRYEvlakiKXvttVdCbu/666+3Bx54wF544QXr27dvaPtll11mL730kvXr188FRjUVy4QJE+zpp592Fcxrr702tP+gQYPsxhtvDB1fuHXr1tnUqVNt1KhREdtVqVY1dPTo0S7kFtWPP/5oxx13nB1xxBEu0JUrVy7i+I466ijr1KmTffPNN1ajRg13XmeddZbVrl3bZs6cGdr/kksucUH59ttvL/IxAUC6I5ACCaZqopqBVf1TOPGoatq9e3dXKc0dSLXvv//9b/e1QqyapNu0aWO9e/d2FTWPKqsffvihC1eqNL755ptuu5rTu3btGnff0PHjx9vXX39tO3bssBYtWrgqXbVq1UJNuaqOel//5z//caFO3RAKQ03UI0aMcKEzPIx6tE3N7NrnggsucNVTNYPLiSeeGLFvqVKlXEhdvXr1brejqqnOR49xOD0u8+fPtyFDhlj//v1d94OiuPrqq10Fd9y4cRFh1AvwCtJquh86dKjdf//97rlW9fi0007bbf/DDz/cNePH26dTH3L0Gvjss8+scuXK1qdPHzvwwAMj9n3++edd9VNKlCjhntcOHTrYKaecYqVLl3bbb775Zvv+++/da1GvVylZsqR7XeX2+uuvu24ZqgTr9aiqdTg95grmes7U11jPn57TWBV1AIiGJnsgwQ4++GAXJseMGRPapj6TauY999xzo/7M3nvv7fo46qLKm4KNwswBBxxgf//9d2g/vemr+vrYY4+55mn1vVQIUMXummuuyffY1GewXbt2LlRVr17dGjVqZE888YRrElfTuLRu3ToUcvS1jklV3cJS4FJTuBd8otF12sfryqDmbVGzdjSqPOam/pwKXrVq1Yoa6JYtW+aqtEWxcuVKF84UlKPdj+j50/Oiyq/33CoIqjoa3i0hr3PJ7YMPPrC3337b9WudPHmye75++uknFw4VjMOpK4H3WjrssMOsbNmydvnll7vXiHf/eg3sscceLpx7+4Z/ePLoNaWg770+9PiGd2dQ9Ve3q9eTbktdEH7//XcXtGfNmhXHIwoAQRrUBKDoRo4cGdCv1PTp0wMjRowIVKpUKbBhwwZ33Y033hjYe++9Azt27Agcd9xxgb322ivf21u7dm2gZs2ageuvvz60bdCgQYGcnJzApZdeGrHvvffe6+576tSped7mGWecEShTpkxg/vz5oW3r168PNG3aNLD//vsHdu7c6bZNmTLF3d7bb78d93nrZ6I57bTT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6d+/v6vcHn/88damTRs744wzLJEY1AQAAFAEGzdutF9//dUaNWoU1/6qOj7++ON27rnnRoRRqVWrlqtCPvPMM7Z582a372OPPeaa5nOHUS845m7eV8X2yCOPdM3/48ePjwijRaGA27lzZ3v66act0aiQAoWskmZL1S6QT6UyWyrDABBu8ODBdv/999v27dtdNbJr16526623xvUgaW7Ov/76y9q1axf1+rZt27omf92u+pVq30MOOSSu21YwVmVT/UZjNa2rST53hVTf33DDDfnevo5ZXQqyOpBqnVuVrF955RXX+VYvhNxl92il8iuuuMKOOeaY0Pf6tDFmzBhXKlefC5Wg1fcjkySj32OmB5Fo4StbQmg02XzuAJCbBjEpOKp/p4Lj0KFDXdP4BRdckO+DpcqneIORctMgIm8/b1lVb1t+VC3VZcmSJS43RRscpQyVO3xqic94J7wvji4VaRNI9QlES0+p34JK4/Pnz99tH70o3nzzTbvnnnsiytpNmzaN2O/888+3Dz74wH260Si4k046yY06u/DCC5NyLgAAIL2F9yFVv0plkKuvvtrOPvvsfNduV/jTqlWqlEbz+++/h4Jj9erV3b7etvw0bNjQjZJXoa1fv35uoFWpUpFxL54+pLEsXbrUateubVkbSJXwp02b5kaKXXXVVe7rWNRvokOHDlGv+/77791Is6lTp9pRRx3ltmnUmT4p6EWUqH4WAAAgeyg8qmCmQtdee+2V574KmR07drQXX3zRhdjcFCL3228/a9Kkifv+sMMOc9VX7Ru+/GosGsj08ccfuz6fGp2vn80dSgtD3Qjeeecd1zc1awOpQqPCaDzUZ0LBUh2FzznnnIgK6Xvvvef6Yyi0ho9o0yg1dQL2QmomYLoiILvQrSJvOUmesB7ZQ9XR//znP26kfn5hNDyrdOrUyW677TbX99RrWtfI+vfff98FP899991nRx99tGvZvf3220PFMzXnjxo1yi677LLd1opXdwJ1TVTfVuWcV199Nepo/HitWLHCLr74YnefOo6sDaTxUgg99NBDXTlc4bN169ZuElmvj6jm+FKwDf+E4U31oOuiBVJ9ItDFs27dOksXhFJkAqZ+QqJWNCKUItGDmhRG582bZ5UqVXKhL9Z+4R544AE7/PDDXU7R4KOnnnrKdTVUs7zyyeuvv+66AXhUTVXFU3OQ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azObG3ZdaTPTaHoNYPL6jP4ZucyoQyiNSU3y3377rRt07VFuUDdBtb6qMupRiywyPJD26NHDrr76atdfQ6PmNd+oSuQHHnigtWzZMu59gGxFFRDIQMo/mtkw1vr0CqHbo6zoJITSmM4880wXOjUtpfqVaqae66+/PqJ19cQTT7SGDRu66SXbt2/vmu81hSXSPJCqf8bixYvdpxA9+X369AmVxfWiqFmzpv373/+2c8891958803XFK/m+ffeey90G/HsAyQLARBAsduZRxjNC6E0TypmHX300bZjxw776aefXD/Sm2++2YYOHequVwCdP3++vf766/bpp5+6/dWcr4u+RqScgEqEaeL99993ATK30047zUqV+l+2VlhVR2OF1COOOMLKl999Zt949olFfUjVF3Xt2rVWpUqVyCvDRtcB+SGQAkiEQHF2b+kRVimVF+Novs8VLaK9b6opWwvdNG7cOKJJ21Z/Y/b+IZZUx880q3FwXLsuX77c9t57b1fIUsXT89hjj7kme51jhQoVov6sFvcZNGiQffHFF9axY0fLdFtiPcfpXiENf+Lzoipo9+7di7wPMhMhEAAKgEppXDSDj6qlmzZtihlI+/bt6wLpkiVLeAmmcyBF+iIEAkAaI5TmSa2uTzzxhJufVKPv5brrrrMOHTrYscce68KqBlNrGkmFVW1HJAJpBiMEAgAShlAadVCTqqJ///23W4780UcfDV1/+umn23333efGrGhZcnVb0ADqt956i6kjoyCQJhghEACQsZIRSrWUp1ZPSuZKTbrPONWqVcsNsA6fxknbck/n1K5dO3v11Vfd13/99Zeb65wpn2IjkAIAgNQJpVrCU0t5puha9lp63JsQP14sFZo/AikAAChaKD3SzD6MMp9pYSkgFiAkIv0RSAEASHN+dRe78U+z/pXNOm0zW3nT/7anzXySSBlavRYAAKDAhv5t1m6x2UqWZ0cREUgBAEChbaIcigQgkAIAAMBXBFIAAAD4ikAKAAAAXxFIAQAA4CsCKQAAgM8WLlxo559/vm3evNmyEfOQAgCAFLPIzJK0UpNp2dD4JuF/4403bOLEiXnuc8899xRqZaZVq1bZ6NGj7f7777fy5ctbIvz66682dOhQe+yxx6xs2bKWygikAAAgxcJoczNL0lr2Vs7M5sYVSuvXr28dOnQIfX/dddfZP/7xDzvppJNC2wob/Bo1amRPP/20VahQwRJl5cqVLuQ+9NBDBFIAAID4/ZnEMGrB+/ozrkB6yCGHuItnyJAhdvDBB7umdpk7d65dddVVNmzYMHvmmWdcM/w111zjKqbXXnut26d06dLWpEkT69+/v9WpUyd0Wxs3brSvvvrKbfcEAgF788037ZNPPrFy5crZ0Ucfbccdd1zEMWmft99+O7RP7969rU2bNq7iquOQyy67zEqVKmUdO3a0AQMG2NatW+3555+3b7/91qpWrWqnnXaaHXTQQaHb1Hncd999EecxcOBAe/jhh+3ss8+2Qw89NLTv2rVr3TkqnO+7776FfA7oQwoAAJAQy5YtcxXJww47zFavXu3Ca+XKlV11UpVVXVq1auWCYIsWLWzBggW7Ndlv3bo1FDQVFG+55RarXbu2ValSxS644AIX/Dy7du2yU0891W1XZbVixYoucH755ZfuPlu2bOn2a9eunbvvffbZx3bu3GnHHHOMC5wNGjRwx6mAOWHChDzPQ8H1zz//tBEjRkSc84svvmhvvfWWq/AWBU32AAAACXTrrbfamWeeGbHNq6LKpZdean369LEHHnjARo0aFfU2FPRmzJhhP//8swuaovCpIHvxxRdb48aNbdy4cfb++++7ffS9qBL7999/uwCrrgSqcqqqWalSJXf9mDFj7IcffrDffvvN9txT/WfNVXCvvPJK6969u6vgxjoPnYP20e1Xr17dbfv3v//t9gn/ucIgkAIAACTQCSecsNu2xYsX28svv2yLFi1yI+lVHV2xYkXM25g4caILeYMGDXLVUu9SsmRJFygVQN99913r0qVLKIyKfqZWrVoxb3fq1KnuZ7wwKmeccYbdfffdbhBU8+bNY56Hfk7dDF544QXXDWDWrFkuNI8dO9aKikCaaENy/Q8AALJKtWrVIr6fOXOmHXHEEXbKKae45m9VL9UUrnAay59//uma6tu2bRuxvX379q7ZX1SpbNiwYYEHOtWtWzdimxdOFZDDA2nu88jJybHzzjvPVUUVSNWsr+NR1baoCKQAskOyPyTyoRRA0LPPPusGI6my6FFlMa9AWrduXZszZ05EU39u9erVi+iHmpsCZLSZAtRcH06DlkR9SvNz7rnnusFc06dPd+dz1113WSIwMT4AAEAxUlP7+vXrQ9///vvvrvk+L2eccYZ9/fXXNn78+N2a8r3J8/v27etG10+aNCl0/ZIlS1yQlRo1arj/VY31aKDU5MmT7fvvvw8dm/qyqnIbT7VVQVlN+WeddZabGUB9YROBCimQioakbuUtkMf97/5ZPIvpcaJKCsDMLrnkEjdf6eGHH2577723C5Ga+ikvXbp0cSPhNWDo0UcfdT+nvqNNmza1Y4891u2j/2+//Xbr1q2bHXnkkW5C/V9++SU0Yl6j6lu3bm0nn3yym9ZJo+Y1Cv9f//qX60LQtWtXVx1V/9b33nsvakU1GlVtdZsKpep+kAgEUsDHAJeqQS6eY0YcCKVAIewZnKw+mRPj/2+AT0EoMIb3udTXmtxeA4/CafqlefPmuSC6Y8cON3XSmjVrIprOA4GA+z/8ZzW/pwLptGnT3HRNd9xxhzVr1izitjUtlEbRf/HFFy4cKph6I+pLlCjhqqyqiKp/qBeCFXAVSr15SDt16uSmp8rvPDzenKXqT5ooOQHvEUDc1q1b555ATQab+5NBnp8ueJPPKIkMbQUKpENSN3CmWrBOGfzuI8sEbgvk+765ZcsWF8g0QlwTuqfD0qHF6a233nJN8GoGT3WaDuq1115zo+zzyj15P8eRqJACGVYhTOdjz1hUSoECapASITFZhg4dak888YQbMJTKVOF98sknXZeAV199Ne4m/ngQSJFVMiGsZcI5ZCVCKYAYDjzwQDeXp5YGTWV77bWX63eq1aJ0zIlEIEVGI7whpRBKAURx4oknpsXjst9++7lLcWDaJwBIJircALAbKqRAkjHwBwCASARSIEkIogAAREeTPQAAAHxFIAUAAICvCKRACggU4AIAyDx///23/ec//7Ht27dbNqIPKTJKOkzzRKgEgLxpRadNmzYl5WGqUKGCW0UqHj/99JPNnj073ymcdJsFtWDBAjv99NNdMK1WrVrcP7d06VKbM2eOlSlTxg444ICIFSRXr17tlg095ZRTYi4DWhCJvr1wBFKknXQIndEQRAEgvjCqtda15nsylCpVyi677LK4QumPP/7oqpied9991xo1amQtWrQIbTvqqKMKFUhr1KhhPXv2dMEyHlr5XevRP/fcc9a+fXu3br2Caa9evezBBx90+8ybN8+F3PXr14fWty+KRN9eOAIpUkq6hk0gW34/s2K2CP4O+UqV0WSFUdF96T7jCaR9+vRxF0+9evVcABwy5P9fNKtWrXLLaypYfvfdd7Zw4UI74ogjXEB955133D6lS5e2Jk2aWKtWrSKW3qxevbq7bV0fj3HjxrmL7qd58+Zu29atW2306NHu640bN9qUKVPc12+88YZbS75hw4bufvM7lmjncfDBB0e9vXbt2lkiEEiRVAROIL3wOwvET036qiD26NHDfv31V9t3333dyka1atWyl19+ORQaZ86caU2bNnUVVq/SuKCATfaq1ioQemFUypYta5dccon7WlXMjz76yH392muvuSb2I4880vbZZ598jyXaeeyxxx5Rb49AipTEmxeQ/vg9RpGrybdl92PYuHFjV0UMF97Uv2XLFte0r6b1W265pVD3ceihh9oDDzxgd955p51xxhnuPsPVrl3bhg4dah07dnTN+uFN7PEeS+7ziHV7iUCFFAXCGxWQufj9zlJ0UUi4q666KmZVc9GiRbZ582ZX3fzqq68KfR+9evVyA5pGjBhht956q6vCdu7c2a677jo78MAD8/35eI4l1nkUBwIpIvCGBGQXfueBxKtTp07E93/99Zcdd9xxtmTJEmvTpo0bCT937lzXD7Morr76anfRYKMvvvjCnnjiCVc5nT59uhtxH01BjiX3eRQnAmmW4c0HAH8HgOIVPkBIVMXUtsWLF4cGLV177bWhQUJF1axZM3fp3bu3C5GvvvpqzEBakGPJfR7FiUCaYXijAcDfBiC1LFu2zAVGLwBq8vu33347z36YgUDADR466KCD3ECk3FThrFu3bkRo1M/s3LkzNBepd/savOR9XZhj8US7vUTJqECqB+iiiy7abfv5559v//jHPyK2TZgwwSZNmuRK1BpJ1qFDhyQeKQAAyBbdu3d3UzppxP3ee+9tY8eOteXLl7vR7bHs3LnT5ZORI0e6eVJzU1h96qmn7NRTT3Wj4NetW2djxoxx01edddZZbh9N6aQR+7fddpsbuKQ5UwtzLJ5ot8co+yiU8vXA3nzzzRGfJjTSLNwVV1xhL730kl155ZVuegXNEaYnsX///pbqqIAC4G8GkBq6desWMSl+zZo13dydmqQ+nFY2UoDUiHVN73Tuuee6Kqb6euaWE6x46jZ0W7GCojKMVoUaP368K7BprlPdrkbce3OqapumalI20n4qzikD5Xcssc4j2u0lKpDmBFTfzRAbNmywypUr25dffhmz4vnzzz+7CWDfe+8916lX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f1jlypUtJyfH10+cCsWLFy+2KlWqWCbh3NITz1t64nlLT+n0vKkyqjBap04dK1GC3oLYHRXSQtAvU7169SxV6A9Rqv8xKizOLT3xvKUnnrf0lC7PG5VR5IWPKQAAAPAVgRQAAAC+IpCmsbJly9ptt93m/s80nFt64nlLTzxv6SmTnzdkHwY1AQAAwFdUSAEAAOArAikAAAB8RSAFAACAr5iHNA1oQuE33njDXn/9dTchf9OmTe3yyy+3li1bRuz3999/27Bhw2z69OlWvXp1O//88+3EE08s8D7J9tdff9mYMWPcOR522GF2zz33RFy/cOFC6927924/N3LkSGvXrl3o+7Vr19rw4cPtq6++cvPdnXvuudajRw/zkx7vsWPH2oQJE+yQQw6xBx98cLd9NFn0vffea59//rlbbOGss86ynj17Fngfvz3wwAM2fvz4iG2NGjWyl19+OWLbtGnT7NFHH7Xly5dbq1atbPDgwVa3bl1LVVu2bLERI0bYpEmTrFy5cu61qMc/3Tz55JP27LPPRmzbY4897J133onYNmPGDHvooYfcZOv77bef3XDDDda4cWNLJZs3b7ZXXnnFXnjhBatUqZL725jbjh073N+Id99910qWLOn+Flx00UURk7LHs0+ybd261f0ePffcc1aqVKndnh/p2rVraMVAz7/+9S8755xzQt/v3LnTRo0aZRMnTnTf//Of/7RLLrnEnSeQigikaeCaa66x33//3U4++WS3ysVrr71mBx98sH366afWvn17t8/27dvtmGOOsQoVKrg3kJ9//tn9cX3xxRft9NNPj3ufZNObXocOHdybvIL33Llzo775fP311/bWW29ZzZo1Q9ubN28e8cf32GOPdV/fdNNNNn/+fDvttNPcG/AZZ5xhfli5cqUddNBB7jj0xjJ79uzd9tE56wPBxo0b3WhZhe9+/frZY4895j4sxLtPKvjtt9/caN/wDxTly5eP2Oezzz6zzp07u9e0Qp2C6eGHH27ff/99yk6a3adPH/fc3X333e4DxqWXXuo+GOp3KJ3od23btm0upHjKlCkTsc+3335rRxxxhAtl/fv3t9GjR1vHjh3d87PXXntZqmjdurX78Kq/B1988UXUfXQOH374od13333uvAcOHGgLFiyw+++/v0D7JJv+th944IHu8daxRaOCwvXXX2+dOnUKbcu9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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "txBearingAngle = -135 # Point the TX panel toward the center of the map\n", "\n", "bwp = BandwidthPart(numRbs=12, spacing=15) # Create a bandwidth part\n", "\n", "trjPoints = np.array([[44, 77], [32, 52], [31, 32], [38, 20], [49, 18], \n", " [62, 17], [85, 16], [103, 16], [128, 17], [144, 32]])\n", "# trjPoints = np.array([[44, 77], [32, 52], [31, 32], [38, 20], [49, 18]])\n", "\n", "trjPoints = np.array([[-50, -83], [-47, -51], [-54, -29], [-70, -16], [-88, -14],\n", " [-104, -18], [-114, -29], [-116, -42], [-114, -63], [-115, -81]])\n", "trajectory = dmData.trajectoryFromPoints(trjPoints, bwp, speedMps=15)\n", "\n", "trajectory.print() # Print the trajectory information\n", "ax = dmData.drawMap(\"LOS-NLOS\", trajectory) # Draw the map with the trajectory\n", "dmData.drawBsPanel(ax, txBearingAngle) # Draw the base station antenna panel" ] }, { "cell_type": "code", "execution_count": 4, "id": "159d5799-a75c-437c-aa93-af11967e1e5c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Simulating end-to-end communication for 12378 slots on the trajectory ...\n", " Slot TX Blk RX Blk BLER Beam Ang OLLA Ofst CQI(Rep) RI Mod. Coderate Trj. Time Exe. Time\n", "------ ------ ------ ------ -------- --------- -------- --- ------ -------- --------- ---------\n", "12,378 12,364 11,187 9.717 -15.5 1.63 6(6) 1 16QAM 616/1024 12.38 3190.75 " ] } ], "source": [ "random.setSeed(123) # Make results reproducible\n", "numSlots = trajectory.numPoints # Total number of slots = number of points on the trajectory\n", "prgSize = 0 # Set to 0 for wideband, 2 or 4 for subband precoding\n", "\n", "# Note:\n", "# Since our goal is to show that different UE locations along a DeepMIMO trajectory experience different \n", "# BLER/throughput, we should not normalize the channel gain independently at each location. Per-location\n", "# gain normalization would remove exactly the large-scale variation we want to observe. So, we set \n", "# the 'normalizeGains' to 'False' in our channel model below and use a fixed noise power.\n", "\n", "# Calculating the noise power:\n", "k = 1.380649e-23 # Boltzmann constant (joules per kelvin)\n", "tempK = 290.0 # Temperature in kelvin\n", "nf = 8 # Receiver noise figure (dB)\n", "noiseVarFreq = k*tempK*bwp.spacing*1000*toLinear(nf)\n", "\n", "# Creating a trajectory-based channel model:\n", "channel = TrjChannel(bwp, trajectory, \n", " normalizeGains = False,\n", " txAntenna = AntennaPanel([1,4], polarization='x'), # 8 TX antennas\n", " txOrientation = [txBearingAngle,0,0], # BS antenna orientation\n", " rxAntenna = AntennaPanel([1,1], polarization='x', # 2 RX antennas\n", " beamWidth=[65,360])) # Omnidirectional\n", "\n", "# Create a typical CSI-RS configuration\n", "csiRsConfig = CsiRsConfig.beamformingConfig(bwp, channel.txAntenna.numPorts, sweepsPerSlot=4)\n", "# csiRsConfig.print() # Uncomment to print CSI-RS configuration details\n", "\n", "# Get the CSI resource sets for beam sweeping, beam probing, and RI/PMI/CQI feedback\n", "sweepSet, probeSet, pmiSet = csiRsConfig.csiRsSetList \n", "\n", "# Create CSI reports and a CsiReportMan object for the CSI-RS configuration\n", "csiReportMan = CsiReportMan.beamformingReports(csiRsConfig, channel.txAntenna, \n", " prgSize=prgSize, allowedRanks=[1,2], cqiTable=2)\n", "# csiReportMan.print() # Uncomment to print CSI report configuration details\n", "\n", "# Get the CSI report objects for beam sweeping, beam probing, and RI/PMI/CQI feedback\n", "sweepRep, probeRep, pmiRep = csiReportMan.csiReports\n", "\n", "# Initialize beam sweeping/probing parameters\n", "numPhi = len(sweepSet) # Number of horizontal beams\n", "numTheta = 1 # Number of vertical beams (restrict sweeping to azimuth)\n", "sweepWs, sweepBeams = channel.txAntenna.getSweepingBeams(numTheta, numPhi)\n", "probeWs, probeBeams = None, None # Probing weight vectors and beam angles\n", "sweepCri, probeCri = None, None # Most recent CRI for sweeping and probing \n", "probePhiLocal, probePhiGlobal = None, None # Best probing beam azimuth angle\n", "rawCqi = None # Most recent CQI feedback (before OLLA adjustment)\n", "\n", "pdsch = None # PDSCH is created when a CQI report is available\n", "harq = None # HARQ is created only once when the first CQI is received\n", "olla = None # OLLA is created only once when the first CQI is received\n", "precoder = None # The precoder based on the most recently received PMI\n", "pmiW = None # The steering vector used by the most recent RI/PMI/CQI CSI-RS resources\n", "pdschW = None # The steering vector used by PDSCH (consistent with precoder from PMI)\n", "\n", "# Historical values:\n", "beamAngles = [] # Azimuth beam angles (probePhiGlobal) for each slot\n", "cqiRep = [] # Reported CQI values\n", "cqiAdj = [] # Adjusted CQI values\n", "ris = [] # RI (number of layers)\n", "blockErrorFlags = [] # 1 for each transport block error and 0 otherwise\n", "rxBits = [] # Number of received bits for each transmission\n", "minMse, maxMse = 100, 0\n", "\n", "t0 = time.monotonic()\n", "print(f\"\\nSimulating end-to-end communication for {numSlots} slots on the trajectory ...\")\n", "print(\" Slot TX Blk RX Blk BLER Beam Ang OLLA Ofst CQI(Rep) RI Mod. Coderate Trj. Time Exe. Time\")\n", "print(\"------ ------ ------ ------ -------- --------- -------- --- ------ -------- --------- ---------\")\n", "for slotNo in range(numSlots):\n", " # Process CSI feedback\n", " csiReportInfo = csiReportMan.getFeedback() # Get all available CSI reports from CsiReport objects\n", " for reportId, csiFeedback in csiReportInfo.items(): # Process the CSI feedback for each report\n", " if reportId == sweepRep.reportId: # Beam-sweeping report\n", " sweepCri = csiFeedback.cri.cri # CSI-RS resource ID of the best beam\n", " probeSet.trigger() # Trigger the probing CSI-RS resource set\n", " probeRep.trigger() # Trigger the probing report\n", " \n", " elif reportId == probeRep.reportId: # Beam-probing report\n", " probeCri = csiFeedback.cri.cri # CSI-RS resource ID of the best beam\n", " beamIdx = probeCri - len(sweepSet) - 1 # Index of the best beam\n", " pmiSet.active=True # Activate RI/PMI/CQI CSI-RS resources\n", " pmiRep.active=True # Activate RI/PMI/CQI measurements\n", " \n", " # Update the best probing beam azimuth angle in local and global coordinates\n", " probePhiLocal = probeBeams[1][beamIdx]\n", " _, probePhiGlobal = AntennaPanel.local2Global( probeBeams[0][beamIdx],\n", " probePhiLocal,\n", " channel.txOrientation)\n", " \n", " elif reportId == pmiRep.reportId: # RI/PMI/CQI report\n", " precoder = csiFeedback.pmi.wbW if csiFeedback.pmi.sbWs is None else csiFeedback.pmi.sbWs\n", " pdschW = pmiW\n", " numLayers = csiFeedback.ri.ri # Set number of layers based on RI feedback\n", " if \"cqi\" in pmiRep.quantity.lower(): rawCqi = csiFeedback.cqi.cqi\n", " else:\n", " assert False, f\"Unknown report: {reportId}\"\n", "\n", " # Create a transmitted resource grid\n", " txGrid = bwp.createGrid(channel.txAntenna.numEl)\n", "\n", " # If we have a CQI report, transmit PDSCH data\n", " if rawCqi is not None:\n", " cqi = rawCqi if olla is None else olla.adjustCqi(rawCqi) # Adjust the raw CQI received in the report\n", " modulation, coderateX1024 = pmiRep.getModRate(cqi) # Get modulation and code rate based on CQI\n", " coderate = coderateX1024/1024\n", " pdsch = PDSCH(bwp, numLayers=numLayers, csiRsConfig=csiRsConfig, modulation=modulation, prgSize=prgSize)\n", " pdsch.setDMRS(additionalPos=2) # DMRS configuration\n", " if harq is None:\n", " # Create HARQ and OLLA objects the first time\n", " harq = pdsch.getHarq(coderates = coderate, # Code rate used by the LDPC codec\n", " harqType=\"IR\", # Use Incremental Redundancy\n", " numProc=16) # Number of HARQ processes\n", " olla = OLLA(harq, pmiRep.cqiTable, fixedOffset=-2) # Create the link adaptation object\n", " else:\n", " # We already have a HARQ and OLLA. Update HARQ's LDPC codec if needed.\n", " ldpcModulation = harq.ldpcCodec.modulations[0]\n", " ldpcCoderate = harq.ldpcCodec.coderates[0]\n", " ldpcNumLayers = harq.ldpcCodec.cwCodecs[0].numLayers\n", " if (numLayers != ldpcNumLayers) or (modulation != ldpcModulation) or (coderate != ldpcCoderate):\n", " harq.setLdpc( pdsch.getLdpcCodec(coderates=coderate) ) # Update HARQ's LDPC codec\n", "\n", " pdsch.initGrid() # Create and initialize PDSCH's internal grid\n", " numBits = pdsch.getBitCapacity()[0] # Number of bits available in the resource grid\n", " tbs = harq.ldpcCodec.txBlockSizes[0] # Transport block size (for the first codeword)\n", " if harq.needNewData[0]: txBlock = random.bits(tbs) # Create random bits for new transmissions\n", " else: txBlock = None # Set to None indicating a retransmission\n", "\n", " rateMatchedCBs = harq.encode(txBlock, numBits) # LDPC encode the transport block \n", " pdsch.setPdschData(rateMatchedCBs) # Map/modulate the data to the resource grid\n", " pdsch.precodeTo(txGrid, precoder, pdschW) # Precode PDSCH data into the txGrid\n", "\n", " # Process and add CSI-RS to the txGrid\n", " csiRsResources = csiRsConfig.getResources()\n", " for csiSetId, setResources in csiRsResources.items():\n", " if csiSetId == sweepSet.rsId: # CSI-RS for beam sweeping\n", " for resourceId, (lIdx, kIdx, sweepReValues) in setResources.items():\n", " b = resourceId-1 # Beam index\n", " w = sweepWs[:,b:b+1] # nt x 1\n", " # sweepReValues is a 1 x numCsiRsRE matrix. nt x 1 * 1 x numCsiRsRE = nt x numCsiRsRE\n", " txGrid[:,lIdx, kIdx] = (w * sweepReValues, \"CSIRS_NZP\", resourceId)\n", " \n", " elif csiSetId == probeSet.rsId: # CSI-RS for beam probing\n", " b = sweepCri-1 # Index of best sweeping beam\n", " theta0, phi0 = sweepBeams[0][b], sweepBeams[1][b] # Best sweeping beam angles\n", " probeWs, probeBeams = channel.txAntenna.getProbingBeams(theta0, phi0, len(probeSet), polStrategy='equal')\n", " for resourceId, (lIdx, kIdx, probeReValues) in setResources.items():\n", " b = resourceId - len(sweepSet) - 1 # Beam index\n", " w = probeWs[:,b:b+1] # nt x 1\n", " # probeReValues is a 1 x numCsiRsRE matrix. nt x 1 * 1 x numCsiRsRE = nt x numCsiRsRE\n", " txGrid[:,lIdx, kIdx] = (w * probeReValues, \"CSIRS_NZP\", resourceId)\n", "\n", " elif csiSetId == pmiSet.rsId: # CSI-RS for RI/PMI/CQI\n", " # Use the beamforming vector corresponding to the best probed beam. This will also be\n", " # saved to pdschW when the report for this CSI-RS is received. pdschW is then combined with\n", " # the PMI procoder to precode PDSCH.\n", " wIdx = probeCri - len(sweepSet) - 1 # Index of the best probing beam from CRI\n", " pmiW = probeWs[:,wIdx:wIdx+1].copy() # shape: nt x 1\n", " for resourceId, (lIdx, kIdx, pmiReValues) in setResources.items():\n", " txGrid[:,lIdx, kIdx] = (pmiW * pmiReValues, \"CSIRS_NZP\", resourceId) \n", "\n", " channelMatrix = channel.getChannelMatrix() # Get channel matrix from the channel model\n", " rxGrid = txGrid.applyChannel(channelMatrix) # Apply the channel in the frequency domain\n", " noisyRxGrid = rxGrid.addNoise(noiseVar=noiseVarFreq) # Add noise\n", "\n", " # Receiver side:\n", " if pdsch is not None:\n", " effChannelMatrix = channel.getEffChannel(channelMatrix, precoder, pdschW) # Perfect channel knowledge\n", " eqGrid, llrScales = pdsch.equalize(noisyRxGrid, effChannelMatrix) # Equalize received PDSCH data\n", " llrs = pdsch.getLLRs(eqGrid, llrScales) # Demodulate and get LLRs\n", " decodedTxBlocks, crcMatches = harq.decode(llrs) # Use HARQ to decode the LLRs\n", "\n", " beamAngles += [ probePhiGlobal ] # Record the best probe beam azimuth\n", " cqiRep += [ rawCqi ] # Record the reported CQI\n", " cqiAdj += [ cqi ] # Record the adjusted CQI\n", " ris += [ numLayers ] # Record number of layers\n", " rxBits += [ tbs if crcMatches[0][0] else 0 ] # Record successfully received bits\n", " blockErrorFlags += [ 0 if crcMatches[0][0] else 1 ] # Record the block-error flag\n", " else: # No PDSCH yet -> use zeros\n", " beamAngles += [ 0 ]\n", " cqiRep += [ 0 ]\n", " cqiAdj += [ 0 ]\n", " ris += [0]\n", " rxBits += [ 0 ]\n", " blockErrorFlags += [ 0 ]\n", " \n", " csiReportMan.processRxGrid(noisyRxGrid, csiRsResources) # Process the CSI-RS resources in the 'noisyRxGrid'\n", " dt = time.monotonic()-t0 # Total time spent so far\n", "\n", " if pdsch is None:\n", " print(f\"\\r{slotNo+1:^6d} {0:^6d} {0:^6d} {'N/A':^6s} \"\n", " f\"{'N/A':^8s} {\"N/A\":^9s} {'N/A':^8s} {'N/A'} {'N/A':^6s} {'N/A':^8s} \" \n", " f\"{channel.trajectory.cur.time:^9.2f} {dt:^9.2f}\", end='')\n", " else:\n", " # Note: Using bler1st, which is the first-transmission BLER\n", " print(f\"\\r{slotNo+1:^6,d} {harq.totalTxBlocks:^6,d} {harq.totalRxBlocks:^6,d} {harq.bler1st:^6.3f} \"\n", " f\"{probePhiLocal:^8.1f} {olla.cqiOffset[0]:^9.2f} {f'{cqi}({rawCqi})':^8s} {numLayers:^3d} \"\n", " f\"{modulation:^6s} {f'{coderateX1024}/1024':^8s} {channel.trajectory.cur.time:^9.2f} {dt:^9.2f}\",\n", " end='')\n", " \n", " channel.goNext() # Move to the next slot and trajectory point\n", " if harq is not None: harq.goNext() # Go to the next HARQ process\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "f0f15e05-a7ed-4dfa-9e60-b468cf565c2e", "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "![demo](AnimateLA.gif)" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "prevCqiRep = None\n", "prevCqiAdj = None\n", "prevRi = None\n", "prevBler = None\n", "prevBps = None\n", "arrow = None\n", "\n", "# Since we are averaging the angles, we need to make sure they are continuous:\n", "# Local angle range: -90 to 90\n", "# Global angle range: 135 .. -45\n", "# Adjusted global angle range: 135 .. 315 (Add 360 to the negative values)\n", "beamAngles = np.array(beamAngles)\n", "beamAngles[beamAngles<0] += 360 # Range: 135 .. 315\n", "\n", "# Callback used to initialize and update the scenario map and the graphs below it\n", "def handleGraph(request, ax, trajectory, points=None):\n", " global arrow, prevCqiRep, prevCqiAdj, prevRi, prevBler, prevBps\n", " if request==\"Config\":\n", " # CQI \n", " ax[0].set_xlim(0,trajectory.numPoints)\n", " ax[0].set_ylim(1,16)\n", " ax[0].set_title(\"CQI\")\n", " ax[0].set_xlabel(\"Trajectory points\")\n", " ax[0].grid()\n", " \n", " # RI \n", " ax[1].set_xlim(0,trajectory.numPoints)\n", " ax[1].set_ylim(0.8,2.2)\n", " ax[1].set_title(\"RI (Number of layers)\")\n", " ax[1].set_xlabel(\"Trajectory points\")\n", " ax[1].grid()\n", "\n", " # Use the following to show BLER in the 3rd graph\n", " # ax[2].set_xlim(0,trajectory.numPoints)\n", " # ax[2].set_ylim(0,22)\n", " # ax[2].set_title(\"BLER (%)\")\n", " # ax[2].set_xlabel(\"Trajectory points\")\n", " # ax[2].grid()\n", "\n", " # Use the following to show throughput in the 3rd graph\n", " ax[2].set_xlim(0,trajectory.numPoints)\n", " ax[2].set_ylim(0.1e7,2.5e7)\n", " ax[2].set_title(\"Throughput (bps)\")\n", " ax[2].set_xlabel(\"Trajectory points\")\n", " ax[2].grid()\n", "\n", " elif request==\"ConfigMap\":\n", " ax.set_title(\"CSI Feedback and OLLA Link Adaptation along a UE Trajectory\")\n", " dmData.drawBsPanel(ax, txBearingAngle) # Draw TX antenna panel\n", "\n", " # Create the arrow patch\n", " arrow = dmData.drawBeamArrow(ax, txBearingAngle, color=\"cyan\", length=50)\n", " arrow.set_animated(True)\n", "\n", " elif request==\"Draw\":\n", " p0, p1 = points\n", " # cqi = np.round(np.mean(cqiRep[p0: p1]))\n", " cqi = np.mean(cqiRep[p0: p1])\n", " if prevCqiRep is None: prevCqiRep = cqi\n", " ax[0].plot([p0,p1], [prevCqiRep, cqi], 'red', markersize=1, label=\"Reported\")\n", " prevCqiRep = cqi\n", " \n", " # cqi = np.round(np.mean(cqiAdj[p0: p1]))\n", " cqi = np.mean(cqiAdj[p0: p1])\n", " if prevCqiAdj is None: prevCqiAdj = cqi\n", " ax[0].plot([p0,p1], [prevCqiAdj, cqi], 'orange', markersize=1, label=\"Adjusted\")\n", " if p0==0: ax[0].legend(fontsize=8) # First time: add the legend\n", " prevCqiAdj = cqi\n", "\n", " ri = np.mean(ris[p0:p1])\n", " if prevRi is None: prevRi = ri\n", " ax[1].plot([p0,p1], [prevRi, ri], 'blue', markersize=1)\n", " prevRi = ri\n", "\n", " # Use the following to show BLER in the 3rd graph\n", " # bler = np.mean(blockErrorFlags[p0: p1])*100\n", " # if prevBler is None: prevBler = bler\n", " # ax[2].plot([p0,p1], [prevBler, bler], 'green', markersize=1)\n", " # prevBler = bler\n", "\n", " # Use the following to show throughput in the 3rd graph\n", " bps = np.sum(rxBits[p0:p1])/((p1-p0)/bwp.avgSlotDuration)\n", " if prevBps is None: prevBps = bps\n", " ax[2].plot([p0,p1], [prevBps, bps], 'green', markersize=1)\n", " prevBps = bps\n", "\n", " elif request==\"DrawOnMap\":\n", " p0, p1 = points\n", " if p0>0:\n", " meanPhi = np.mean(beamAngles[p0:p1])\n", " # Now that we have computed the mean angle, we can remove the 360 to get \n", " # the angle back in the correct range\n", " if meanPhi>180: meanPhi -= 360 \n", " # Update the arrow direction\n", " dmData.drawBeamArrow(ax, meanPhi, color=\"cyan\", length=50, arrow=arrow)\n", " return (arrow,)\n", "\n", "# Create the animation and display it below\n", "anim = dmData.animateTrajectory(trajectory, numGraphs=3, pointsPerFrame=200, \n", " graphCallback=handleGraph, fileName='AnimateLA.gif',\n", " lastFrameDur=3000) # Freeze on last frame for 3 seconds\n", "display(Markdown(\"![demo](AnimateLA.gif)\"))\n", "\n", "# Alternatively, the following code provides better control over running \n", "# the animation. Note that for this method to work, you should not pass a \n", "# 'fileName' to the 'animateTrajectory' function.\n", "# # Increase the animation memory limit for HTML-based animation display\n", "# matplotlib.rcParams['animation.embed_limit'] = 100000000\n", "# anim = dmData.animateTrajectory(trajectory, numGraphs=3, pointsPerFrame=100, \n", "# graphCallback=handleGraph)\n", "# HTML(anim.to_jshtml())" ] }, { "cell_type": "code", "execution_count": null, "id": "773e0a2f-0bc5-4c99-b89f-d33dd6d6cd49", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.10" } }, "nbformat": 4, "nbformat_minor": 5 }