├── .gitignore ├── LICENSE.txt ├── README.md ├── checkpoints └── .gitkeep ├── code ├── gen_dataset.py ├── neural_materials.py ├── trainable_antenna_pattern.py ├── trainable_materials.py ├── trainable_scattering_pattern.py └── utils.py ├── data ├── coordinates.csv ├── reftx-offsets-dichasus-dc01.json ├── spec.json ├── synthetic_positions.npy ├── tfrecords │ └── .gitkeep └── traced_paths │ └── .gitkeep ├── notebooks ├── CDFs.ipynb ├── CIRs.ipynb ├── Heat_Maps.ipynb ├── ITU_Materials.ipynb ├── Learned_Materials.ipynb ├── Neural_Materials.ipynb └── Synthetic_Data.ipynb ├── results ├── measurements │ ├── cir-1.pdf │ ├── cir-2.pdf │ ├── cir-3.pdf │ ├── cir-4.pdf │ ├── ds.pdf │ ├── ds_rae.pdf │ ├── path_loss_rx1.pdf │ ├── path_loss_rx2.pdf │ ├── power.pdf │ └── power_ale.pdf └── synthetic │ ├── antenna_pattern_h.pdf │ ├── antenna_pattern_v.pdf │ ├── conductivity.pdf │ ├── relative_permittivity.pdf │ ├── scattering_coefficient.pdf │ ├── scattering_pattern.pdf │ └── xpd_coefficient.pdf └── scenes └── inue_simple ├── inue_simple.blend ├── inue_simple.xml └── meshes ├── ceiling.ply ├── floor.ply └── walls.ply /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | NVIDIA License 2 | 3 | 1. Definitions 4 | 5 | “Licensor” means any person or entity that distributes its Work. 6 | “Work” means (a) the original work of authorship made available under this license, which may include software, documentation, or other files, and (b) any additions to or derivative works thereof that are made available under this license. 7 | The terms “reproduce,” “reproduction,” “derivative works,” and “distribution” have the meaning as provided under U.S. copyright law; provided, however, that for the purposes of this license, derivative works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work. 8 | Works are “made available” under this license by including in or with the Work either (a) a copyright notice referencing the applicability of this license to the Work, or (b) a copy of this license. 9 | 10 | 2. License Grant 11 | 12 | 2.1 Copyright Grant. Subject to the terms and conditions of this license, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to use, reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form. 13 | 14 | 3. Limitations 15 | 16 | 3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this license, (b) you include a complete copy of this license with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work. 17 | 18 | 3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (“Your Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this license (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself. 19 | 20 | 3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use non-commercially. Notwithstanding the foregoing, NVIDIA Corporation and its affiliates may use the Work and any derivative works commercially. As used herein, “non-commercially” means for research or evaluation purposes only. 21 | 22 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this license from such Licensor (including the grant in Section 2.1) will terminate immediately. 23 | 24 | 3.5 Trademarks. This license does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this license. 25 | 26 | 3.6 Termination. If you violate any term of this license, then your rights under this license (including the grant in Section 2.1) will terminate immediately. 27 | 28 | 4. Disclaimer of Warranty. 29 | 30 | THE WORK IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 31 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. 32 | 33 | 5. Limitation of Liability. 34 | 35 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Radio Environments by Differentiable Ray Tracing 2 | 3 | This repository contains the source code to reproduce the results from the paper [Learning Radio Environments by Differentiable Ray Tracing [A]](https://arxiv.org/abs/2311.18558) 4 | using the [Sionna™ link-level simulator [B]](https://nvlabs.github.io/sionna/) and its differentiable ray tracing extension [Sionna RT [C]](https://arxiv.org/abs/2303.11103). 5 | 6 | ## Abstract 7 | Ray tracing (RT) is instrumental in 6G research in order to generate spatially-consistent and environment-specific channel impulse responses(CIRs). While acquiring accurate scene geometries is now relatively straightforward, determining material characteristics requires precise calibration using channel measurements. We therefore introduce a novel gradient-based calibration method, complemented by differentiable parametrizations of material properties, scattering and antenna patterns. Our method seamlessly integrates with differentiable ray tracers that enable the computation of derivatives of CIRs with respect to these parameters. Essentially, we approach field computation as a large computational graph wherein parameters are trainable akin to weights of a neural network (NN). We have validated our method using both synthetic data and real-world indoor channel measurements, employing a distributed multiple-input multiple-output (MIMO) channel sounder. 8 | 9 | ## Setup 10 | Running this code requires [Sionna 0.18](https://nvlabs.github.io/sionna/) or later. 11 | To run the notebooks on your machine, you also need [Jupyter](https://jupyter.org). 12 | We recommend Ubuntu 22.04, Python 3.10, and TensorFlow 2.15. 13 | 14 | ## How to use this repository 15 | 16 | The notebook [Synthetic_Data.ipynb](notebooks/Synthetic_Data.ipynb) reproduces all the results from Secion IV-B. 17 | You don't need to download or generate any additional file to run this notebook. 18 | 19 | To reproduce the results from Section IV-C, you first need to download the "dichasus-dc01.tfrecords" file from the [DICHASUS website](https://dichasus.inue.uni-stuttgart.de/datasets/data/dichasus-dcxx/) to the folder `data/tfrecords` within the cloned repository. More information about the DICHASUS channel sounder can be found in [[D]](https://arxiv.org/abs/2206.15302). 20 | 21 | Then, you need to create a dataset of traced paths using the script [gen_dataset.py](code/gen_dataset.py). 22 | For this purpose, ensure that you are in the `code/` folder, and run: 23 | 24 | ```bash 25 | python gen_dataset.py -traced_paths_dataset dichasus-dc01 -traced_paths_dataset_size 10000 26 | ``` 27 | This script stores the generated dataset in the `data/traced_paths/` folder. 28 | Generating the dataset of traced paths can take a while. 29 | 30 | Once the dataset is generated, you can train the models considered in Section IV-C by running the corresponding notebooks ([ITU Materials](notebooks/ITU_Materials.ipynb), [Learned Materials](notebooks/Learned_Materials.ipynb) and [Neural Materials](notebooks/Neural_Materials.ipynb)). 31 | The weights of the trained models are saved by the notebooks in the `checkpoints/` folder. 32 | Note that for "ITU Materials", training consists in fitting the power scaling factor. 33 | 34 | Once the trainings are done, the figures from Section IV-C can be reproduced by the notebooks [CDFs.ipynb](notebooks/CDFs.ipynb), [Heat_Maps.ipynb](notebooks/Heat_Maps.ipynb) and [CIRs](notebooks/CIRs.ipynb). 35 | 36 | ## Structure of this repository 37 | . 38 | ├── LICENSE.txt # License file 39 | ├── README.md # Readme 40 | ├── checkpoints # Folder to store weights after training 41 | ├── code 42 | │ ├── gen_dataset.py # Script to generate a dataset of traced paths 43 | │ ├── neural_materials.py # NeuralMaterials Class 44 | │ ├── trainable_antenna_pattern.py # TrainableAntennaPattern Class 45 | │ ├── trainable_materials.py # TrainableMaterials Class 46 | │ ├── trainable_scattering_pattern.py # TrainableScatteringPatterm Class 47 | │ └── utils.py # Utility functions 48 | ├── data 49 | │ ├── coordinates.csv # Coordinates of receivers and other points of interest 50 | │ ├── reftx-offsets-dichasus-dc01.json # Phase and time offsets 51 | │ ├── spec.json # Specification of the "dichasus-dcxx" dataset 52 | │ ├── synthetic_positions.npy # Positions used for the results with synthetic data 53 | │ ├── tfrecords # Folder to store DICHASUS tfrecords files 54 | │ └── traced_paths # Folder to store datasets of traced paths from `gen_dataset.py` 55 | ├── notebooks 56 | │ ├── CDFs.ipynb # CDFs from Section IV-C 57 | │ ├── CIRs.ipynb # CIRs from Section IV-C 58 | │ ├── Heat_Maps.ipynb # Heatmaps from Section IV-C 59 | │ ├── ITU_Materials.ipynb # Fits the power scaling for the "ITU Materials" baseline from Section IV-C 60 | │ ├── Neural_Materials.ipynb # Trains the "Neural Materials" model from Section IV-C 61 | │ ├── Synthetic_Data.ipynb # Results from Section IV-B 62 | │ └── Learned_Materials.ipynb # Trains the "Learned Materials" model from Section IV-C 63 | ├── results # Precomputed results and figures. Might be overwritten when notebooks are executed. 64 | │ ├── measurements # Results for measured data 65 | │ └── synthetic # Results for synthetic data 66 | └── scenes 67 | └── inue_simple # Simple 3D model of the INUE at Stuttgart University 68 | ├── inue_simple.blend # Blender scene file 69 | ├── inue_simple.xml # Mitsuba scene file 70 | └── meshes # Meshes used in the Mitsuba scene 71 | ├── ceiling.ply 72 | ├── floor.ply 73 | └── walls.ply 74 | 75 | ## References 76 | 77 | [A] J. Hoydis, F. Ait Aoudia, S. Cammerer, F. Euchner, M. Nimier-David, S. ten Brink, A. Keller, ["Learning Radio Environments by Differentiable Ray Tracing"](https://arxiv.org/abs/2311.18558), Mar. 2023. 78 | 79 | [B] J. Hoydis, S. Cammerer, F. Ait Aoudia, A. Vem, N. Binder, G. Marcus, A. Keller, ["Sionna: An Open-Source Library for Next-Generation Physical Layer Research"](https://arxiv.org/abs/2203.11854), Mar. 2022. 80 | 81 | [C] J. Hoydis, F. Ait Aoudia, S. Cammerer, M. Nimier-David, N. Binder, G. Marcus, A. Keller, ["Sionna RT: Differentiable Ray Tracing for Radio Propagation Modeling"](https://arxiv.org/abs/2303.11103), Mar. 2023. 82 | 83 | [D] F. Euchner, M. Gauger, S. Doerner, S. ten Brink, ["A Distributed Massive MIMO Channel Sounder for "Big CSI Data"-driven Machine Learning"](https://arxiv.org/abs/2206.15302), 84 | in Proc. Int. ITG Works. Smart Antennas (WSA), Nov. 2021. 85 | 86 | ## License and Citation 87 | 88 | Copyright © 2023, NVIDIA Corporation. All rights reserved. 89 | 90 | This work is made available under the [NVIDIA License](LICENSE.txt). 91 | 92 | If you use this software, please cite it as: 93 | ```bibtex 94 | @article{sionna-rt-calibration, 95 | title = {{Learning Radio Environments by Differentiable Ray Tracing}}, 96 | author = {Hoydis, Jakob and {Ait Aoudia}, Fayçal and Cammerer, Sebastian and Euchner, Florian and Nimier-David, Merlin and ten Brink, Stephan and Keller, Alexander}, 97 | year = {2023}, 98 | month = DEC, 99 | journal = {arXiv preprint}, 100 | online = {https://arxiv.org/abs/2311.18558} 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /checkpoints/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/checkpoints/.gitkeep -------------------------------------------------------------------------------- /code/gen_dataset.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | """ 13 | Script to generate a dataset of traced paths 14 | """ 15 | 16 | ########################################### 17 | # Parse arguments 18 | ########################################### 19 | 20 | import argparse 21 | 22 | ## Define the expected arguments and their default value 23 | parser = argparse.ArgumentParser(description='Generate dataset of traced paths', 24 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 25 | parser.add_argument('-gpu_num', type=int, help='Index of the GPU to use', 26 | default=0) 27 | # 28 | parser.add_argument('-seed', type=int, help='Tensorflow seed', 29 | default=1) 30 | # 31 | parser.add_argument('-meas_ds', type=str, help='Name of the dataset of measurements', 32 | default="dichasus-dc01") 33 | # 34 | parser.add_argument('-scene_name', type=str, help='Sionna scene to use for ray tracing', 35 | default="inue_simple") 36 | # 37 | parser.add_argument('-num_samples', type=int, help='Number of samples used for tracing', 38 | default=int(4e6)) 39 | # 40 | parser.add_argument('-max_depth', type=int, help='Maximum depth used for tracing', 41 | default=5) 42 | # 43 | parser.add_argument('-los', help='Enables LoS when tracing', default=True, action='store_true') 44 | parser.add_argument('-no-los', action='store_false', dest='los') 45 | # 46 | parser.add_argument('-reflection', help='Enables reflection when tracing', default=True, action='store_true') 47 | parser.add_argument('-no-reflection', action='store_false', dest='reflection') 48 | # 49 | parser.add_argument('-diffraction', help='Enables diffraction when tracing', default=True, action='store_true') 50 | parser.add_argument('-no-diffraction', action='store_false', dest='diffraction') 51 | # 52 | parser.add_argument('-edge_diffraction', help='Enables edge diffraction when tracing', default=False, action='store_true') 53 | parser.add_argument('-no-edge_diffraction', action='store_false', dest='edge_diffraction') 54 | # 55 | parser.add_argument('-scattering', help='Enables scattering when tracing', default=True, action='store_true') 56 | parser.add_argument('-no-scattering', action='store_false', dest='scattering') 57 | # 58 | parser.add_argument('-scat_keep_prob', type=float, help='Probability to keep a scattered paths when tracing', 59 | default=0.001) 60 | parser.add_argument('-traced_paths_dataset', type=str, help='(Required) Filename of the dataset of traced paths to create', 61 | required=True) 62 | parser.add_argument('-traced_paths_dataset_size', type=int, help='(Required) Size of the dataset of traced paths', 63 | required=True) 64 | parser.add_argument('-delete_raw_dataset', help='Deletes the raw (unpost-processed) dataset', default=True, action='store_true') 65 | parser.add_argument('-keep_raw_dataset', action='store_false', dest='delete_raw_dataset') 66 | 67 | ## Parse arguments 68 | args = parser.parse_args() 69 | # GPU index to use 70 | gpu_num = args.gpu_num 71 | # Tensorflow seed 72 | seed = args.seed 73 | # Name of the dataset of measurments 74 | meas_ds = args.meas_ds 75 | # Sionna scene to use for ray tracing 76 | scene_name = args.scene_name 77 | # Number of samples used for tracing 78 | num_samples = args.num_samples 79 | # Maximum depth used for tracing 80 | max_depth = args.max_depth 81 | # Enables LoS when tracing 82 | los = args.los 83 | # Enables reflection when tracing 84 | reflection = args.reflection 85 | # Enables diffraction when tracing 86 | diffraction = args.diffraction 87 | # Enables edge diffraction when tracing 88 | edge_diffraction = args.edge_diffraction 89 | # Enables scattering when tracing 90 | scattering = args.scattering 91 | # Probability to keep a scattered paths when tracing 92 | scat_keep_prob = args.scat_keep_prob 93 | # Filename of the dataset of traced paths to create 94 | traced_paths_dataset = args.traced_paths_dataset 95 | # Size of the dataset of traced paths 96 | # Set to -1 to match the datset of measurements 97 | traced_paths_dataset_size = args.traced_paths_dataset_size 98 | # Delete the raw dataset once post-processed? 99 | delete_raw_dataset = args.delete_raw_dataset 100 | # Folder where to save the dataset 101 | traced_paths_dataset_folder = '../data/traced_paths' 102 | 103 | 104 | ########################################### 105 | # Imports 106 | ########################################### 107 | 108 | import os 109 | 110 | os.environ["CUDA_VISIBLE_DEVICES"] = f"{gpu_num}" 111 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 112 | import tensorflow as tf 113 | gpus = tf.config.list_physical_devices('GPU') 114 | if gpus: 115 | try: 116 | tf.config.experimental.set_memory_growth(gpus[0], True) 117 | except RuntimeError as e: 118 | print(e) 119 | tf.get_logger().setLevel('ERROR') 120 | 121 | # Set the seed 122 | tf.random.set_seed(seed) 123 | 124 | import sys 125 | sys.path.append('../../..') 126 | sys.path.append('../code') 127 | 128 | # import sionna 129 | from utils import * 130 | import json 131 | 132 | 133 | ########################################### 134 | # Setup the scene 135 | ########################################### 136 | 137 | # Load the scene 138 | scene = init_scene(scene_name, use_tx_array=True) 139 | 140 | # Place the transmitters 141 | place_transmitter_arrays(scene, [1,2]) 142 | 143 | # Instantiate the receivers 144 | instantiate_receivers(scene, 1) 145 | 146 | 147 | ######################################### 148 | # Generate a dataset of traced paths 149 | ######################################### 150 | 151 | # Load the dataset of measurements. 152 | dataset = load_dataset(meas_ds, calibrate=True, y_filter=[-15, 25]) 153 | dataset = dataset.batch(1).repeat() 154 | 155 | # Build an iterator on the dataset 156 | data_iter = iter(dataset) 157 | 158 | traced_paths_raw_dataset_datafile = os.path.join(traced_paths_dataset_folder, traced_paths_dataset + '_raw.tfrecords') 159 | 160 | # Iterate through all the positions in the dataset 161 | # Length of the line printed to show progress 162 | line_length = 50 163 | # and trace the paths 164 | # File writer to save the dataset 165 | file_writer = tf.io.TFRecordWriter(traced_paths_raw_dataset_datafile) 166 | # Keep track of the max_num_paths 167 | max_num_paths_spec = -1 168 | max_num_paths_diff = -1 169 | max_num_paths_scat = -1 170 | for it_num in range(traced_paths_dataset_size): 171 | 172 | # Retrieve the next item 173 | # `None` is returned if the iterator is exhausted 174 | next_item = next(data_iter, None) 175 | # Stop if exhausted 176 | if next_item is None: 177 | break 178 | 179 | # Retrieve the position 180 | h_meas_raw, rx_pos = next_item 181 | 182 | # Place the receiver 183 | set_receiver_positions(scene, rx_pos) 184 | 185 | # Trace the paths 186 | traced_paths = scene.trace_paths(num_samples=num_samples, 187 | max_depth=max_depth, 188 | los=los, 189 | reflection=reflection, 190 | diffraction=diffraction, 191 | edge_diffraction=edge_diffraction, 192 | scattering=scattering, 193 | scat_keep_prob=scat_keep_prob, 194 | check_scene=False) 195 | 196 | # Update max_num_paths 197 | num_paths_spec = traced_paths[0].objects.shape[-1] 198 | num_paths_diff = traced_paths[1].objects.shape[-1] 199 | num_paths_scat = traced_paths[2].objects.shape[-1] 200 | 201 | if num_paths_spec > max_num_paths_spec: 202 | max_num_paths_spec = num_paths_spec 203 | if num_paths_diff > max_num_paths_diff: 204 | max_num_paths_diff = num_paths_diff 205 | if num_paths_scat > max_num_paths_scat: 206 | max_num_paths_scat = num_paths_scat 207 | 208 | # Reshape the channel measurement 209 | h_meas = reshape_h_meas(h_meas_raw) 210 | 211 | # Serialize the traced paths 212 | record_bytes = serialize_traced_paths(rx_pos[0], h_meas, traced_paths, True) 213 | 214 | # Save the traced paths 215 | file_writer.write(record_bytes) 216 | 217 | # Print progress 218 | progress_message = f"\rProgress: {it_num+1}/{traced_paths_dataset_size}" 219 | progress_message = progress_message.ljust(line_length) 220 | print(progress_message, end="") 221 | 222 | file_writer.close() 223 | print("") 224 | print("Raw dataset generated.") 225 | print(f"Maximum number of paths:\n\tLoS + Specular: {max_num_paths_spec}\n\tDiffracted: {max_num_paths_diff}\n\tScattered: {max_num_paths_scat}") 226 | 227 | 228 | ######################################### 229 | # Post-process the generated raw dataset 230 | ######################################### 231 | 232 | print("Post-processing the raw dataset...") 233 | 234 | raw_dataset = tf.data.TFRecordDataset([traced_paths_raw_dataset_datafile]) 235 | raw_dataset = raw_dataset.map(deserialize_paths_as_tensor_dicts) 236 | raw_dataset_iter = iter(raw_dataset) 237 | 238 | # Iterate through all the dataset and tile the paths to the same ``max_num_paths`` 239 | # File writer to save the dataset 240 | traced_paths_dataset_datafile = os.path.join(traced_paths_dataset_folder, traced_paths_dataset + '.tfrecords') 241 | file_writer = tf.io.TFRecordWriter(traced_paths_dataset_datafile) 242 | for it_num in range(traced_paths_dataset_size): 243 | 244 | # Retrieve the next item 245 | # `None` is returned if the iterator is exhausted 246 | next_item = next(raw_dataset_iter, None) 247 | # Stop if exhausted 248 | if next_item is None: 249 | break 250 | 251 | # Retreive the receiver position separately 252 | rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:] 253 | 254 | # Build traced paths 255 | traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths) 256 | 257 | # Tile 258 | traced_paths = pad_traced_paths(traced_paths, max_num_paths_spec, max_num_paths_diff, max_num_paths_scat) 259 | 260 | # Serialize tiled traced paths 261 | record_bytes = serialize_traced_paths(rx_pos, h_meas, traced_paths, False) 262 | 263 | # Save the tiled traced paths 264 | file_writer.write(record_bytes) 265 | 266 | # Print progress 267 | progress_message = f"\rProgress: {it_num+1}/{traced_paths_dataset_size}" 268 | progress_message = progress_message.ljust(line_length) 269 | print(progress_message, end="") 270 | 271 | file_writer.close() 272 | print("") 273 | 274 | 275 | ######################################### 276 | # Removing the raw dataset 277 | ######################################### 278 | 279 | # Removes the raw (unpost-processed) dataset if requested 280 | if delete_raw_dataset: 281 | os.remove(traced_paths_raw_dataset_datafile) 282 | 283 | 284 | ####################################### 285 | # Save the dataset properties 286 | ####################################### 287 | 288 | # Filename for storing the dataset parameters 289 | params_filename = os.path.join(traced_paths_dataset_folder, traced_paths_dataset + '.json') 290 | 291 | # Retrieve the input parameters as a dict 292 | params = vars(args) 293 | 294 | # Add the maximum number of paths 295 | params['max_num_paths_spec'] = max_num_paths_spec 296 | params['max_num_paths_diff'] = max_num_paths_diff 297 | params['max_num_paths_scat'] = max_num_paths_scat 298 | 299 | # Dump the dataset parameters in a JSON file 300 | with open(params_filename, "w") as outfile: 301 | json.dump(params, outfile) 302 | -------------------------------------------------------------------------------- /code/neural_materials.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | import tensorflow as tf 13 | from sionna import PI, DIELECTRIC_PERMITTIVITY_VACUUM 14 | from sionna.utils import expand_to_rank, flatten_last_dims 15 | import pickle 16 | 17 | from tensorflow.keras.layers import Layer, Dense 18 | 19 | 20 | class NeuralMaterials(Layer): 21 | """Neural Network-based trainable materials 22 | 23 | This layer implements an MLP that computes the material properties 24 | from positions in the scene. 25 | 26 | Parameters 27 | ---------- 28 | scene : Scene 29 | Instance of the loaded scene 30 | 31 | pos_encoding_size : int 32 | Size of the positional encoding. 33 | Defaults to 10. 34 | 35 | learn_scattering : bool 36 | If set to `False`, then zero-valued tensors are returned for the 37 | scattering and XPD coefficients. 38 | Defaults to `True`. 39 | 40 | Input 41 | ----- 42 | objects [batch_dims], tf.int 43 | Integers uniquely identifying the intersected objects 44 | Not used. 45 | 46 | points : [batch_dims, 3], tf.float 47 | Positions of the intersection points 48 | 49 | Output 50 | ------ 51 | complex_relative_permittivity : [num_objects], tf.float32 52 | Complex-valued relative permittivity 53 | 54 | scattering_coefficient : [num_objects], tf.float32 55 | Scattering coefficient. 56 | Returns a zero-valued tensor if `learn_scattering` is set to `False`. 57 | 58 | xpd_coefficient : [num_objects], tf.float32 59 | XPD coefficient. 60 | Returns a zero-valued tensor if `learn_scattering` is set to `False`. 61 | """ 62 | 63 | def __init__(self, scene, pos_encoding_size=10, learn_scattering=True): 64 | super(NeuralMaterials, self).__init__() 65 | 66 | self.frequency = scene.frequency 67 | self.num_hidden_layers = 4 68 | self.num_units_per_hidden_layer = 128 69 | self.pos_encoding_size = pos_encoding_size 70 | self.learn_scattering = learn_scattering 71 | 72 | # For scaling to the bounding box 73 | x_min = scene.mi_scene.bbox().min.x 74 | y_min = scene.mi_scene.bbox().min.y 75 | z_min = scene.mi_scene.bbox().min.z 76 | 77 | x_max = scene.mi_scene.bbox().max.x 78 | y_max = scene.mi_scene.bbox().max.y 79 | z_max = scene.mi_scene.bbox().max.z 80 | 81 | self.center = tf.constant([0.5*(x_max + x_min), 82 | 0.5*(y_max + y_min), 83 | 0.5*(z_max + z_min)], tf.float32) 84 | 85 | scale = tf.constant([0.5*(x_max - x_min), 86 | 0.5*(y_max - y_min), 87 | 0.5*(z_max - z_min)], tf.float32) 88 | self.scale = tf.reduce_max(scale) 89 | 90 | def to_unit_bbox(self, x): 91 | """ 92 | Center and scales `x` to the unit cube according to the scene 93 | bounding box 94 | """ 95 | 96 | center_ = expand_to_rank(self.center, tf.rank(x), 0) 97 | scale_ = self.scale 98 | x_u = (x-center_)/scale_ 99 | return x_u 100 | 101 | def pos_enc(self, x): 102 | """ 103 | Positional encoding from: 104 | https://dl.acm.org/doi/pdf/10.1145/3503250 105 | """ 106 | # x : [..., 3, 1], tf.float 107 | x = tf.expand_dims(x, axis=-1) 108 | 109 | # Tile x to fit the size of the positional encoding 110 | 111 | # Exponents 112 | # [pos_encoding_size] 113 | indices = tf.range(self.pos_encoding_size, dtype=tf.float32) 114 | # [..., 1, pos_encoding_size] 115 | indices = expand_to_rank(indices, tf.rank(x), 0) 116 | 117 | # Compute positional encoding 118 | # [..., 3, pos_encoding_size] 119 | enc = tf.pow(2., indices)*PI*x 120 | enc_cos = tf.math.cos(enc) 121 | enc_sin = tf.math.sin(enc) 122 | 123 | # [..., 3, 2*pos_encoding_size] 124 | enc = tf.concat([enc_cos, enc_sin], axis=-1) 125 | 126 | # Flatten feature dim 127 | # [..., 3*2*pos_encoding_size] 128 | enc = flatten_last_dims(enc, 2) 129 | 130 | return enc 131 | 132 | def complex_relative_permittivity(self, eta_prime, sigma): 133 | r""" 134 | Computes the complex relative permittivity from the relative permittivity 135 | and the conductivity 136 | """ 137 | 138 | epsilon_0 = DIELECTRIC_PERMITTIVITY_VACUUM 139 | frequency = self.frequency 140 | omega = tf.cast(2.*PI*frequency, tf.float32) 141 | 142 | return tf.complex(eta_prime, 143 | -tf.math.divide_no_nan(sigma, epsilon_0*omega)) 144 | 145 | def build(self, input_shape): 146 | 147 | # Build the neural network 148 | layers = [] 149 | 150 | for _ in range(self.num_hidden_layers): 151 | layers.append(Dense(self.num_units_per_hidden_layer, 'relu')) 152 | # Output layer has no activation and two outputs: 153 | # - Real relative permittivity 154 | # - Conductivity 155 | # - Scattering coefficient 156 | # - XPD 157 | layers.append(Dense(4, None)) 158 | 159 | self.layers = layers 160 | 161 | def get_mat_props(self, pos): 162 | r""" 163 | Computes the material properties from the position 164 | """ 165 | 166 | # pos : [..., 3], tf.float 167 | 168 | # Fit to unit cube 169 | pos = self.to_unit_bbox(pos) 170 | 171 | # Encode position 172 | # [..., 3*2*pos_encoding_size] 173 | enc_pos = self.pos_enc(pos) 174 | 175 | # MLP 176 | mat_prop = enc_pos 177 | for layer in self.layers: 178 | mat_prop = layer(mat_prop) 179 | # mat_prop : [..., 4] 180 | 181 | # [...] 182 | eta_prime = mat_prop[...,0] # Real relative_permittivity 183 | sigma = mat_prop[...,1] # Conductivity 184 | s = mat_prop[...,2] # Scattering coefficient 185 | xpd = mat_prop[...,3] # XPD 186 | 187 | return eta_prime, sigma, s, xpd 188 | 189 | def call(self, objects, points): 190 | 191 | # objects : [...], int 192 | # points : [..., 3], float 193 | 194 | # [...] 195 | eta_prime, sigma, s, xpd = self.get_mat_props(points) 196 | 197 | eta_prime = tf.exp(tf.clip_by_value(eta_prime, tf.math.log(1e-3), tf.math.log(200.))) + 1. 198 | sigma = tf.exp(tf.clip_by_value(sigma, tf.math.log(1e-3), tf.math.log(1e6))) 199 | s = tf.clip_by_value(tf.math.sigmoid(s), 1e-3, 1-1e-3) 200 | xpd = tf.clip_by_value(tf.math.sigmoid(xpd), 1e-3, 1-1e-3) 201 | 202 | # [...] 203 | eta = self.complex_relative_permittivity(eta_prime, sigma) 204 | 205 | if not self.learn_scattering: 206 | s = tf.zeros_like(s) 207 | xpd = tf.zeros_like(xpd) 208 | 209 | return eta, s, xpd 210 | -------------------------------------------------------------------------------- /code/trainable_antenna_pattern.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | import tensorflow as tf 13 | from tensorflow.keras.layers import Layer 14 | from sionna.constants import PI 15 | from sionna.rt import polarization_model_1, polarization_model_2 16 | from sionna.rt.utils import r_hat, normalize 17 | 18 | class TrainableAntennaPattern(Layer): 19 | """Trainable antenna pattern 20 | 21 | This antenna pattern consists of a mixture of spherical Gaussians 22 | with trainable mean directions and concentration parameters. 23 | 24 | Parameters 25 | ---------- 26 | num_mixtures : int 27 | Number of mixtures 28 | 29 | slant_angle : float 30 | Slant angle of the polarization model. 31 | Defaults to 0. 32 | 33 | polarization_model : 1 | 2 34 | The polarization model from 3GPP TR 38.901 to be used. 35 | Defaults to 2. 36 | 37 | dtype : tf.complex64 | tf.complex128 38 | Datatype to be used. 39 | Defaults to tf.complex64 40 | 41 | Input 42 | ----- 43 | theta : [array_like], tf.float 44 | Zenith angle 45 | 46 | phi = : [array_like], tf.float 47 | Azimuth angle 48 | 49 | Output 50 | ------ 51 | c_theta : [array_like], tf.complex 52 | Zenith pattern 53 | 54 | c_phi : [array_like], tf.complex 55 | Azimuth pattern 56 | """ 57 | def __init__(self, 58 | num_mixtures, 59 | slant_angle=0.0, 60 | polarization_model=2, 61 | dtype=tf.complex64): 62 | super(TrainableAntennaPattern, self).__init__() 63 | 64 | self._num_mixtures = num_mixtures 65 | self._polarization_model = polarization_model 66 | self._dtype = dtype 67 | self._rdtype = dtype.real_dtype 68 | self._slant_angle = tf.cast(slant_angle, self._rdtype) 69 | 70 | def build(self, input_shape): 71 | self._mu = tf.Variable(tf.initializers.GlorotUniform()(shape=(1, self._num_mixtures, 3)), dtype=self._rdtype) 72 | self._lambdas = tf.Variable(tf.initializers.GlorotUniform()(shape=(1, self._num_mixtures)), dtype=self._rdtype) 73 | self._weights = tf.Variable(tf.initializers.GlorotUniform()(shape=(1, self._num_mixtures)), dtype=self._rdtype) 74 | self._e_rad = tf.Variable((1), dtype=self._rdtype) 75 | 76 | def call(self, theta, phi): 77 | # Compute direction vectors 78 | theta = tf.cast(theta, self._rdtype) 79 | phi = tf.cast(phi, self._rdtype) 80 | v = tf.expand_dims(r_hat(theta, phi), -2) 81 | 82 | # Compute mean vectors and lambdas 83 | mu, _ = normalize(self._mu) 84 | lambdas = tf.abs(self._lambdas) 85 | 86 | # Compute scaling factor 87 | a = lambdas/tf.cast(2*PI, self._rdtype)/(1-tf.exp(-tf.cast(2, self._rdtype)*lambdas)) 88 | 89 | # Compute PDFs 90 | gains = tf.cast(4*PI, self._rdtype) * a * tf.exp(lambdas*(tf.reduce_sum(v*mu, axis=-1) - tf.cast(1, self._rdtype))) 91 | 92 | # Compute weighted sum 93 | weights = tf.nn.softmax(self._weights) 94 | gain = tf.reduce_sum(gains*weights, axis=-1) 95 | 96 | # Add radiation efficieny 97 | gain *= self._e_rad 98 | 99 | # Compute antenna pattern from gain 100 | c = tf.complex(tf.sqrt(gain), tf.zeros_like(gain)) 101 | if self._polarization_model==1: 102 | return polarization_model_1(c, theta, phi, self._slant_angle) 103 | else: 104 | return polarization_model_2(c, self._slant_angle) 105 | 106 | -------------------------------------------------------------------------------- /code/trainable_materials.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | import tensorflow as tf 13 | from tensorflow.keras.layers import Layer, Dense, Embedding 14 | from sionna import PI, DIELECTRIC_PERMITTIVITY_VACUUM 15 | 16 | class TrainableMaterials(Layer): 17 | """Trainable materials 18 | 19 | This layer computes overparametrized trainable radio material properties. 20 | 21 | Parameters 22 | ---------- 23 | scene : Scene 24 | Instance of the loaded scene 25 | 26 | num_objects : int 27 | Number of objects in the scene 28 | 29 | embedding_size : int 30 | Size of the embeddings used to represent scalar parameters 31 | 32 | learn_scattering : bool 33 | If set to `False`, then zero-valued tensors are returned for the 34 | scattering and XPD coefficients. 35 | Defaults to `True`. 36 | 37 | Input 38 | ----- 39 | None 40 | 41 | Output 42 | ------ 43 | relative_permittivity : [num_objects], tf.float32 44 | Relative permittivity 45 | 46 | conductivity : [num_objects], tf.float32 47 | Conductivity 48 | 49 | scattering_coefficient : [num_objects], tf.float32 50 | Scattering coefficient. 51 | Returns a zero-valued tensor if `learn_scattering` is set to `False`. 52 | 53 | xpd_coefficient : [num_objects], tf.float32 54 | XPD coefficient. 55 | Returns a zero-valued tensor if `learn_scattering` is set to `False`. 56 | """ 57 | def __init__(self, scene, num_objects, embedding_size=10, learn_scattering=True): 58 | super(TrainableMaterials, self).__init__() 59 | self.frequency = scene.frequency 60 | self.num_objects = num_objects 61 | self.embedding_size = embedding_size 62 | self.learn_scattering = learn_scattering 63 | self.start_id = tf.reduce_min([obj.object_id for obj in scene.objects.values()]) 64 | 65 | def complex_relative_permittivity(self, eta_prime, sigma): 66 | r""" 67 | Computes the complex relative permittivity 68 | """ 69 | epsilon_0 = DIELECTRIC_PERMITTIVITY_VACUUM 70 | frequency = self.frequency 71 | omega = tf.cast(2.*PI*frequency, tf.float32) 72 | 73 | return tf.complex(eta_prime, 74 | -tf.math.divide_no_nan(sigma, epsilon_0*omega)) 75 | 76 | def build(self, input_shape): 77 | self._v = tf.Variable(tf.initializers.GlorotUniform()(shape=(1, self.embedding_size)), name="mat-nn-v") 78 | self._w = tf.Variable(tf.initializers.GlorotUniform()(shape=(self.embedding_size, 4*self.num_objects)), name="mat-nn-w") 79 | 80 | 81 | def get_params(self): 82 | # Compute parameters in logarithmic domain 83 | epsilon_r, sigma, scattering_coefficient, xpd_coefficient = tf.split(tf.squeeze(tf.matmul(self._v, self._w)), 4, axis=0) 84 | 85 | epsilon_r = tf.squeeze(epsilon_r) 86 | sigma = tf.squeeze(sigma) 87 | scattering_coefficient = tf.squeeze(scattering_coefficient) 88 | xpd_coefficient = tf.squeeze(xpd_coefficient) 89 | 90 | # Clip to svae values for gradient computation and map to linear domain 91 | epsilon_r = tf.exp(tf.clip_by_value(epsilon_r, tf.math.log(1e-3), tf.math.log(200.))) + 1. 92 | sigma = tf.exp(tf.clip_by_value(sigma, tf.math.log(1e-3), tf.math.log(1e6))) 93 | scattering_coefficient = tf.clip_by_value(tf.math.sigmoid(scattering_coefficient), 1e-3, 1-1e-3) 94 | xpd_coefficient = tf.clip_by_value(tf.math.sigmoid(xpd_coefficient), 1e-3, 1-1e-3) 95 | 96 | if not self.learn_scattering: 97 | scattering_coefficient = tf.zeros_like(scattering_coefficient) 98 | xpd_coefficient = tf.zeros_like(xpd_coefficient) 99 | 100 | return epsilon_r, sigma, scattering_coefficient, xpd_coefficient 101 | 102 | def call(self, object_id, points): 103 | 104 | # Ensure that object_id starts at 0 and eliminate negative values 105 | object_id -= self.start_id 106 | object_id = tf.maximum(object_id, 0) 107 | 108 | epsilon_r, sigma, scattering_coefficient, xpd_coefficient = self.get_params() 109 | 110 | # Compute complex relative permittivity 111 | eta = self.complex_relative_permittivity(epsilon_r, sigma) 112 | 113 | # Gather parameters corresponding to object_ids 114 | eta = tf.gather(eta, object_id) 115 | scattering_coefficient = tf.gather(scattering_coefficient, object_id) 116 | xpd_coefficient = tf.gather(xpd_coefficient, object_id) 117 | 118 | if not self.learn_scattering: 119 | scattering_coefficient = tf.zeros_like(scattering_coefficient) 120 | xpd_coefficient = tf.zeros_like(xpd_coefficient) 121 | 122 | return eta, scattering_coefficient, xpd_coefficient 123 | -------------------------------------------------------------------------------- /code/trainable_scattering_pattern.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | import tensorflow as tf 13 | from tensorflow.keras.layers import Layer 14 | from sionna.constants import PI 15 | from sionna.utils import expand_to_rank 16 | from sionna.rt.utils import rot_mat_from_unit_vecs, dot, normalize 17 | 18 | class TrainableScatteringPattern(Layer): 19 | """Trainable scattering pattern 20 | 21 | This scattering pattern consists of a mixture of a diffuse, directive, 22 | and backscattering componen. The last two have trainable concentration 23 | parameters. 24 | 25 | Parameters 26 | ---------- 27 | dtype : tf.complex64 | tf.complex128 28 | Datatype to be used. 29 | Defaults to tf.complex64 30 | 31 | Input 32 | ----- 33 | k_i : [batch_dims, 3], tf.float 34 | Incoming directions 35 | 36 | k_s : [batch_dims, 3], tf.float 37 | Outgoing directions 38 | 39 | n : [batch_dims, 3], tf.float 40 | Surface normals 41 | 42 | Output 43 | ------ 44 | f_s : [batch_dims], tf.float 45 | Scattering pattern 46 | """ 47 | def __init__(self, dtype=tf.complex64): 48 | super(TrainableScatteringPattern, self).__init__() 49 | self._dtype = dtype 50 | self._rdtype = dtype.real_dtype 51 | 52 | def build(self, input_shape): 53 | self._weights = tf.Variable([[1/3, 1/3, 1/3]], dtype=self._rdtype) 54 | self._lambda_r = tf.Variable([1], dtype=self._rdtype) 55 | self._lambda_i = tf.Variable([1], dtype=self._rdtype) 56 | 57 | @staticmethod 58 | def a_u(lambda_): 59 | return 2*PI/lambda_*(1-tf.exp(-lambda_)) 60 | 61 | @staticmethod 62 | def a_b(lambda_): 63 | return 2*PI/lambda_*tf.exp(-2*lambda_)*(tf.exp(lambda_)-1) 64 | 65 | @staticmethod 66 | def t(lambda_): 67 | return tf.sqrt(lambda_+1e-12) * (1.6988*lambda_**2 + 10.8438*lambda_) / (lambda_**2 + 6.2201*lambda_ + 10.2415) 68 | 69 | @staticmethod 70 | def a_h(lambda_, beta): 71 | a = tf.exp(TrainableScatteringPattern.t(lambda_)) 72 | b = tf.exp(TrainableScatteringPattern.t(lambda_)*tf.cos(beta)) 73 | s = (a*b-1) / ((a-1)*(b+1)) 74 | return TrainableScatteringPattern.a_u(lambda_)*s + TrainableScatteringPattern.a_b(lambda_)*(1-s) 75 | 76 | def call(self, object_id, points, k_i, k_s, n_hat): 77 | # Compute rotation matrix to bring all vectors to LCS 78 | z_hat = tf.constant([0, 0, 1], tf.float32) 79 | z_hat = tf.broadcast_to(z_hat, n_hat.shape) 80 | rot = rot_mat_from_unit_vecs(n_hat, z_hat) 81 | 82 | # Represent vectors in LCS 83 | n_hat = z_hat 84 | k_i = tf.linalg.matvec(rot, k_i) 85 | k_s = tf.linalg.matvec(rot, k_s) 86 | 87 | # Compute specular reflection 88 | k_r = k_i - 2 * dot(n_hat, k_i, keepdim=True) * n_hat 89 | 90 | # Lambertian pattern 91 | cos_theta_s = dot(k_s, n_hat, clip=True) 92 | pattern_l = cos_theta_s/tf.cast(PI, k_i.dtype) 93 | pattern_l = tf.where(pattern_l<0., tf.cast(0, self._rdtype), pattern_l) 94 | 95 | # Lobe in specular direction 96 | lambda_r = tf.squeeze(tf.clip_by_value(tf.abs(self._lambda_r), 1e-3, 30)) 97 | mu_r = k_r 98 | z = tf.acos(tf.clip_by_value(mu_r[...,2], -1 , 1)) 99 | q = tf.acos(tf.clip_by_value(mu_r[...,2], -1+1e-6, 1-1e-6)) 100 | diff = tf.stop_gradient(z-q) 101 | beta_r = q + diff 102 | dot_mu_r_k_s = dot(mu_r, k_s, clip=True) 103 | pattern_r = tf.exp(lambda_r*(dot_mu_r_k_s-1)) / self.a_h(lambda_r, beta_r) 104 | 105 | # Compute lobe in incoming direction 106 | lambda_i = tf.squeeze(tf.clip_by_value(tf.abs(self._lambda_i), 1e-3, 30)) 107 | mu_i = -k_i 108 | z = tf.acos(tf.clip_by_value(mu_i[...,2], -1 , 1)) 109 | q = tf.acos(tf.clip_by_value(mu_i[...,2], -1+1e-6, 1-1e-6)) 110 | diff = tf.stop_gradient(z-q) 111 | beta_i = q + diff 112 | dot_mu_i_k_s = tf.clip_by_value(dot(mu_i, k_s), -1, 1) 113 | pattern_i = tf.exp(lambda_i*(dot_mu_i_k_s-1)) / self.a_h(lambda_i, beta_i) 114 | 115 | # Compute weighted sum 116 | patterns = tf.stack([pattern_l, pattern_r, pattern_i], axis=0) 117 | weights, _ = normalize(tf.abs(self._weights)) 118 | weights = expand_to_rank(weights[0]**2, tf.rank(patterns), axis=-1) 119 | pattern = tf.reduce_sum(patterns*weights, axis=0) 120 | 121 | return pattern 122 | -------------------------------------------------------------------------------- /code/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 | # SPDX-License-Identifier: LicenseRef-NvidiaProprietary 3 | # 4 | # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual 5 | # property and proprietary rights in and to this material, related 6 | # documentation and any modifications thereto. Any use, reproduction, 7 | # disclosure or distribution of this material and related documentation 8 | # without an express license agreement from NVIDIA CORPORATION or 9 | # its affiliates is strictly prohibited. 10 | 11 | 12 | import csv 13 | import matplotlib.pyplot as plt 14 | import tensorflow as tf 15 | import numpy as np 16 | import pickle 17 | import json 18 | import sys 19 | from sionna.channel import subcarrier_frequencies, cir_to_ofdm_channel 20 | from sionna.rt import normalize, rotation_matrix, theta_phi_from_unit_vec, \ 21 | PlanarArray, Transmitter, Receiver, load_scene, RadioMaterial, \ 22 | Paths, tr38901_pattern, r_hat 23 | from sionna.constants import PI 24 | from sionna.rt.solver_paths import PathsTmpData 25 | from sionna.utils import expand_to_rank, log10 26 | 27 | def change_coordinate_system(pos, center=None, rot_mat=None): 28 | """Transforms coordinates by applying an affine transformation 29 | 30 | Input 31 | ------ 32 | pos : [batch_size, 3], float 33 | Coordinates 34 | 35 | center : [3], float or `None` 36 | Offset vector. 37 | Set to `None` for no offset. 38 | Defaults to `None`. 39 | 40 | rot_mat : [3,3], float or None 41 | Rotation matrix. 42 | Set to `None` to not apply a rotation. 43 | Defaults to `None`. 44 | 45 | Output 46 | ------- 47 | pos : [batch_size, 3] 48 | Rotated and centered coordinates 49 | """ 50 | if center is not None: 51 | pos -= center 52 | if rot_mat is not None: 53 | pos = tf.squeeze(tf.matmul(rot_mat, tf.reshape(pos, [3,1]))) 54 | return pos 55 | 56 | def get_coordinate_system(): 57 | """Get points of interest as well as coordinate transformation parameters 58 | 59 | Output 60 | ------- 61 | center : [3] 62 | Offset vector 63 | 64 | rot_mat : [3,3] 65 | Rotation matrix 66 | 67 | poi : dict 68 | Dictionary with points of interest and their coordinates 69 | """ 70 | 71 | # Read and parse coordinates of points of interest (POIs) 72 | data_dict = {} 73 | with open('../data/coordinates.csv', mode ='r') as file: 74 | csv_file = csv.DictReader(file) 75 | for row in csv_file: 76 | data_dict[row['Name']] = {k: v for k, v in row.items() if k != 'Name'} 77 | 78 | poi = {} 79 | for val in data_dict: 80 | pos = data_dict[val] 81 | if not pos["South"]=="noLoS": 82 | poi[val] = tf.constant([float(pos["West"]), 83 | float(pos["South"]), 84 | float(pos["Height"])], tf.float32) 85 | 86 | # Add antenna array position 87 | poi["array_1"] = tf.constant([7.480775, -20.9824, 1.39335], tf.float32) 88 | poi["array_2"] = tf.constant([-6.390425, 24.440075, 1.4197], tf.float32) 89 | 90 | # Center coordinate system 91 | center = poi["AU"] # This is our new origin 92 | for p in poi: 93 | poi[p] = change_coordinate_system(poi[p], center) 94 | 95 | # Compute rotation matrix such that NWU has coordinates [0, 1, ~0] 96 | nwu_hat, _ = normalize(poi["NWU"]) 97 | theta, phi = theta_phi_from_unit_vec(nwu_hat) 98 | rot_mat = tf.squeeze(rotation_matrix(tf.constant([[-phi.numpy()+PI/2, 0, 0]], tf.float32))) 99 | 100 | # Rotate all POIs to match the new coordinate system 101 | for p in poi: 102 | poi[p] = change_coordinate_system(poi[p], rot_mat=rot_mat) 103 | 104 | return center, rot_mat, poi 105 | 106 | def load_dataset(name, calibrate=True, y_filter=None): 107 | """Loads the dataset and applies optionally calibration as well as offset 108 | corrections 109 | 110 | Input 111 | ------ 112 | name : str 113 | Name of the dataset, e.g. "dichasus-dc01" 114 | 115 | calibrate : bool 116 | Determines if the dataset is calibrated. Note that the 117 | corresponding calibration file must be located in the folder 118 | "data/". 119 | 120 | y_filter : [2] 121 | The minimum and maximum y-coordinates of the measurement positions 122 | to be considered. 123 | 124 | Output 125 | ------- 126 | dataset : A cached TFRecordDataset 127 | The data returns channel frequency responses of shape [batch_size, 64, 1024] 128 | as well as positions of shape [batch_size, 3]. 129 | """ 130 | 131 | raw_dataset = tf.data.TFRecordDataset(["../data/tfrecords/" + name + ".tfrecords"]) 132 | 133 | center, rot_mat, _ = get_coordinate_system() 134 | 135 | feature_description = { 136 | "cfo": tf.io.FixedLenFeature([], tf.string, default_value = ''), 137 | "csi": tf.io.FixedLenFeature([], tf.string, default_value = ''), 138 | "gt-interp-age-tachy": tf.io.FixedLenFeature([], tf.float32, default_value = 0), 139 | "pos-tachy": tf.io.FixedLenFeature([], tf.string, default_value = ''), 140 | "snr": tf.io.FixedLenFeature([], tf.string, default_value = ''), 141 | "time": tf.io.FixedLenFeature([], tf.float32, default_value = 0), 142 | } 143 | 144 | def record_parse_function(proto): 145 | record = tf.io.parse_single_example(proto, feature_description) 146 | csi = tf.ensure_shape(tf.io.parse_tensor(record["csi"], out_type = tf.float32), (64, 1024, 2)) 147 | csi = tf.signal.fftshift(csi, axes=1) 148 | csi = tf.complex(csi[...,0], csi[...,1]) 149 | pos = tf.ensure_shape(tf.io.parse_tensor(record["pos-tachy"], out_type = tf.float64), (3)) 150 | pos = tf.cast(pos, tf.float32) 151 | pos = change_coordinate_system(pos, center=center, rot_mat=rot_mat) 152 | return csi, pos 153 | 154 | def apply_calibration(csi, pos): 155 | """Apply STO and CPO calibration""" 156 | sto_offset = tf.tensordot(tf.constant(offsets["sto"]), 2 * np.pi * tf.range(tf.shape(csi)[1], dtype = np.float32) / tf.cast(tf.shape(csi)[1], np.float32), axes = 0) 157 | cpo_offset = tf.tensordot(tf.constant(offsets["cpo"]), tf.ones(tf.shape(csi)[1], dtype = np.float32), axes = 0) 158 | csi = tf.multiply(csi, tf.exp(tf.complex(0.0, sto_offset + cpo_offset))) 159 | return csi, pos 160 | 161 | dataset = raw_dataset.map(record_parse_function, num_parallel_calls = tf.data.experimental.AUTOTUNE) 162 | 163 | if calibrate: 164 | with open(f"../data/reftx-offsets-{name}.json", "r") as offsetfile: 165 | offsets = json.load(offsetfile) 166 | dataset = dataset.map(apply_calibration) 167 | 168 | if y_filter is not None: 169 | def position_filter(csi, pos): 170 | "Limit y-range to certain range to avoid position too close to the receiver" 171 | return pos[1] > y_filter[0] and pos[1] shifted_left, tensor > shifted_right) 197 | peaks = tf.cast(tf.math.logical_and(peaks, tensor > max_peak*0.1), tf.int16) 198 | 199 | # Get indices of peaks 200 | peak_indices = tf.argmax(peaks, output_type=tf.int32, axis=-1) 201 | return peak_indices 202 | 203 | def freq2time(h, l_min=-8, l_max=80, peak_aligned=False, sampling_rate=50e6): 204 | """Transforms channel frequency response to time response 205 | 206 | Input 207 | ------ 208 | h : [batch_size, num_subcarriers] 209 | Channel frequency response 210 | 211 | l_min : int 212 | Minimum tap index tp keep 213 | 214 | l_max : int 215 | Maximum tap index to keep 216 | 217 | peak_aligned: bool 218 | If `True`, the channel impulse response will be shifted in time such that 219 | the first significant peak appears at tap index 0. 220 | 221 | sampling_rate : float 222 | Sampling rate in Hz. 223 | Defaults to 50MHz. 224 | 225 | Output 226 | ------- 227 | h_t : [batch_size, l_max-l_min+1] 228 | Channel impulse response 229 | 230 | tap_delays : [l_max-l_min+1] or [1, l_max-l_min+1] 231 | Tap delays in (s) 232 | """ 233 | # h_t = tf.signal.fftshift(tf.signal.ifft(h), axes=-1) 234 | h_t = tf.signal.fftshift(tf.signal.ifft(tf.signal.ifftshift(h, axes=-1)), axes=-1) 235 | start = 512 + l_min 236 | stop = 512 + l_max 237 | h_t = h_t[...,start:stop] 238 | 239 | tap_ind = tf.range(l_min, l_max, dtype=tf.float32) 240 | 241 | if peak_aligned: 242 | peak_ind = tf.expand_dims(detect_first_peak_2d_tensor(tf.abs(h_t)), -1) 243 | tap_ind = tf.expand_dims(tap_ind, 0) 244 | tap_ind -= tf.cast(l_min, tf.float32) + tf.cast(peak_ind, tf.float32) 245 | 246 | # Compute tap delays in ns 247 | tap_delays = tap_ind / sampling_rate 248 | 249 | return h_t, tap_delays 250 | 251 | def rms_delay_spread(h, tap_delays): 252 | """Computes RMS Delay spread 253 | 254 | Input 255 | ------ 256 | h : [batch_size, num_taps] 257 | Channel impulse response 258 | 259 | tap_delays : [num_taps] or [1 or batch_size, num_taps] 260 | Tap delays 261 | 262 | Output 263 | ------- 264 | tau_rms : [batch_size] 265 | Delay spread 266 | """ 267 | a = tf.abs(h)**2 268 | tap_delays = expand_to_rank(tap_delays, tf.rank(a), axis=0) 269 | total_power = tf.reduce_sum(a, axis=-1, keepdims=True) 270 | time_weighted_power = tf.reduce_sum(tap_delays*a, axis=-1, keepdims=True) 271 | tau_bar = time_weighted_power / total_power 272 | squared_delays = (tap_delays-tau_bar)**2 273 | tau_rms = tf.sqrt( tf.reduce_sum(squared_delays*a, axis=-1) / total_power[...,0] ) 274 | return tau_rms 275 | 276 | def init_scene(name, use_tx_array=True, tx_pattern="tr38901", rx_pattern="dipole"): 277 | """Loads a scene by name and configures transmit and receive arrays 278 | 279 | Input 280 | ------ 281 | name : str 282 | Must correspond to the name of a valid scene in the `scenes` folder. 283 | 284 | use_tx_array : bool 285 | If set to `True`, then the transmitter array is configured to be 286 | a 4x8 array (as the one used for the measurements). 287 | Otherwise, a single-antenna array is set. 288 | Defaults to `True`. 289 | 290 | tx_pattern : str 291 | The antenna pattern to use at the transmitter. 292 | Defaults to "tr38901". 293 | 294 | rx_pattern : str 295 | The antenna pattern to use at the receiver. 296 | Defaults to "dipole". 297 | 298 | Output 299 | ------- 300 | scene : sionna.rt.Scene 301 | Loaded scene 302 | """ 303 | scene = load_scene(f"../scenes/{name}/{name}.xml") 304 | scene.frequency = 3.438e9 305 | scene.synthetic_array=False 306 | 307 | # Configure array 308 | if use_tx_array: 309 | num_rows = 4 310 | num_cols = 8 311 | else: 312 | num_rows = 1 313 | num_cols = 1 314 | 315 | scene.tx_array = PlanarArray(num_rows=num_rows, 316 | num_cols=num_cols, 317 | vertical_spacing=0.5, 318 | horizontal_spacing=0.5, 319 | pattern=tx_pattern, 320 | polarization="V") 321 | 322 | # This is the antenna used by the measurement robot 323 | scene.rx_array = PlanarArray(num_rows=1, 324 | num_cols=1, 325 | vertical_spacing=0.5, 326 | horizontal_spacing=0.5, 327 | pattern=rx_pattern, 328 | polarization="V") 329 | 330 | return scene 331 | 332 | def select_transmitter(scene, tx_ind, ant_ind): 333 | 334 | tx_ind = int(tx_ind) 335 | ant_ind = int(ant_ind) 336 | 337 | # Get points of interest 338 | _, _, poi = get_coordinate_system() 339 | 340 | # Get center of antenna array 341 | center = poi[f"array_{tx_ind}"] 342 | 343 | # Get offset to center for the desired antenna 344 | array = PlanarArray(num_rows=4, 345 | num_cols=8, 346 | vertical_spacing=0.5, 347 | horizontal_spacing=0.5, 348 | polarization="V", 349 | pattern="iso") 350 | 351 | # Compute final position 352 | position = center + array.positions[ant_ind] 353 | 354 | # Select correct orientation 355 | if tx_ind==1: 356 | orientation = [-PI/2, 0, 0] 357 | elif tx_ind==2: 358 | orientation = [PI/2, 0, 0] 359 | 360 | # Place transmitter 361 | tx = scene.get("tx") 362 | if tx is None: 363 | scene.add(Transmitter(name="tx", 364 | position=position, 365 | orientation=orientation)) 366 | else: 367 | tx.position = position 368 | tx.orientation = orientation 369 | 370 | def place_transmitter_arrays(scene, tx_indices): 371 | r""" 372 | Places transmitters arrays 373 | 374 | Input 375 | ------- 376 | scene : sionna.rt.Scene 377 | Scene in which to place the transmitters 378 | 379 | tx_indices : `list` of `int` 380 | List of transmitters to place in `scene` 381 | """ 382 | 383 | if isinstance(tx_indices, int): 384 | tx_indices = [tx_indices] 385 | 386 | # Get points of interest 387 | _, _, poi = get_coordinate_system() 388 | 389 | for tx_ind in tx_indices: 390 | 391 | # Get center of antenna array 392 | position = poi[f"array_{tx_ind}"] 393 | 394 | # Select correct orientation 395 | if tx_ind==1: 396 | orientation = [-PI/2, 0, 0] 397 | elif tx_ind==2: 398 | orientation = [PI/2, 0, 0] 399 | 400 | # Place transmitters 401 | scene.add(Transmitter(name=f"tx-{tx_ind}", 402 | position=position, 403 | orientation=orientation)) 404 | 405 | def total_power(h): 406 | """Computes the total power over the subcarriers 407 | 408 | Input 409 | ------ 410 | h : [batch_size, num_subcarriers], tf.complex 411 | Channel frequency response 412 | 413 | Output 414 | ------- 415 | : [batch_size], tf.float 416 | Total energy of each channel response 417 | """ 418 | return tf.reduce_sum(tf.abs(h)**2, axis=-1) 419 | 420 | def delay_spread_loss(h_rt, h_meas, return_ds=False): 421 | """Computes the RMS delay spread for all examples and the loss based on 422 | the symmetric mean absolute percentage error (SMAPE) 423 | 424 | Input 425 | ------ 426 | h_rt : [batch_size, num_subcarriers], tf.complex 427 | Synthetic channel frequency response 428 | 429 | h_meas : [batch_size, num_subcarriers], tf.complex 430 | Ground truth channel frequency response 431 | 432 | return_ds : bool 433 | Return the delay spreads 434 | 435 | Output 436 | ------- 437 | : tf.float 438 | Delay spread loss 439 | 440 | : tf.float 441 | Average delay spread of the synthetic channels. 442 | Only returned if ``return_ds`` is `True`. 443 | 444 | : tf.float 445 | Average delay spread of the measured channels. 446 | Only returned if ``return_ds`` is `True`. 447 | """ 448 | h, t = freq2time(h_rt) 449 | ds_rt = rms_delay_spread(h, t*1e9) 450 | 451 | h, t = freq2time(h_meas) 452 | ds_meas = rms_delay_spread(h, t*1e9) 453 | 454 | loss = tf.reduce_mean(tf.math.divide_no_nan(tf.abs(ds_rt-ds_meas), 455 | tf.stop_gradient(ds_meas + ds_rt))) 456 | 457 | if return_ds: 458 | return loss, tf.reduce_mean(ds_rt), tf.reduce_mean(ds_meas) 459 | 460 | return loss 461 | 462 | 463 | def power_loss(h_rt, h_meas, return_pow=False): 464 | """Computes the total power for all examples and the loss based on 465 | the symmetric mean absolute percentage error (SMAPE) 466 | 467 | Input 468 | ------ 469 | h_rt : [batch_size, num_subcarriers], tf.complex 470 | Synthetic channel frequency response 471 | 472 | h_meas : [batch_size, num_subcarriers], tf.complex 473 | Ground truth channel frequency response 474 | 475 | return_po : bool 476 | Return the powers 477 | 478 | Output 479 | ------- 480 | : tf.float 481 | Power loss 482 | 483 | : tf.float 484 | Average power of the synthetic channels. 485 | Only returned if ``return_ds`` is `True`. 486 | 487 | : tf.float 488 | Average power of the measured channels. 489 | Only returned if ``return_ds`` is `True`. 490 | """ 491 | pow_rt = total_power(h_rt) 492 | pow_meas = total_power(h_meas) 493 | 494 | loss = tf.reduce_mean(tf.math.divide_no_nan(tf.abs(pow_rt-pow_meas), 495 | tf.stop_gradient(pow_meas + pow_rt))) 496 | 497 | if return_pow: 498 | return loss, tf.reduce_mean(pow_rt), tf.reduce_mean(pow_meas) 499 | 500 | return loss 501 | 502 | def mse_power_scaling_factor(h_rt, h_meas): 503 | """Scaling factor alpha that minimizes: 504 | sum_i (pow_rt_i - alpha*pow_meas_i)^2 505 | 506 | Input 507 | ------ 508 | h_rt : [batch_size, num_subcarriers], tf.complex 509 | Synthetic channel frequency response 510 | 511 | h_meas : [batch_size, num_subcarriers], tf.complex 512 | Ground truth channel frequency response 513 | 514 | Output 515 | ------- 516 | : [batch_size], tf.float 517 | Scaling factor 518 | """ 519 | pow_rt = tf.reduce_sum(tf.abs(h_rt)**2, axis=-1) 520 | pow_meas = tf.reduce_sum(tf.abs(h_meas)**2, axis=-1) 521 | scaling_factor = tf.reduce_sum(pow_rt*pow_meas) / tf.reduce_sum(pow_meas**2) 522 | return scaling_factor 523 | 524 | def instantiate_receivers(scene, num_rx): 525 | """ 526 | Removes all receivers from ``scene`` and instantiates ``num_rx`` receivers 527 | 528 | Input 529 | ------ 530 | scene: sionna.rt.Scene 531 | Scene 532 | 533 | num_rx : int 534 | Number of receivers to instantiate 535 | """ 536 | 537 | rx_names = scene.receivers.keys() 538 | for rx in rx_names: 539 | scene.remove(rx) 540 | 541 | for i in range(num_rx): 542 | name = f'rx-{i}' 543 | if scene.get(name) is None: 544 | scene.add(Receiver(name=f"rx-{i}", position=(0.,0.,0.))) 545 | 546 | def set_receiver_positions(scene, rx_pos): 547 | """ 548 | Sets positions of the receivers in ``scene`` 549 | 550 | Input 551 | ------ 552 | scene: sionna.rt.Scene 553 | Scene 554 | 555 | rx_pos : [num_rx, 3], tf.float 556 | Positions of the receivers 557 | """ 558 | 559 | num_rx = rx_pos.shape[0] 560 | for i in range(num_rx): 561 | name = f'rx-{i}' 562 | scene.receivers[name].position = rx_pos[i] 563 | 564 | def serialize_traced_paths(rx_pos, h_meas, traced_paths, squeeze_target_dim): 565 | """ 566 | Serializes the traced paths 567 | 568 | Input 569 | ------ 570 | rx_pos : [3], tf.float 571 | Position of the receiver 572 | 573 | h_meas : [1, num_tx*num_tx_ant=64, num_subcarriers=1024], tf.float 574 | Tensor of measurements 575 | 576 | traced_paths : list 577 | The traced paths as generated by `Scene.trace_paths()` 578 | 579 | squeeze_target_dim : bool 580 | If set to `True`, the target dimension is squeezed. 581 | This helps with creating batches. 582 | 583 | Output 584 | ------ 585 | : str 586 | Serialized traced paths 587 | """ 588 | 589 | # Map Paths objects to a single dictionary of tensor 590 | dict_list_ = [x.to_dict() for x in traced_paths] 591 | 592 | # Target axis index 593 | target_axis = {'a' : 0, 594 | 'tau' : 0, 595 | 'mask' : 0, 596 | 'objects' : 1, 597 | 'phi_r' : 0, 598 | 'phi_t' : 0, 599 | 'theta_r' : 0, 600 | 'theta_t' : 0, 601 | 'vertices' : 1, 602 | 'targets' : 0, 603 | # Paths TMP data 604 | 'normals' : 1, 605 | 'k_i' : 1, 606 | 'k_r' : 1, 607 | 'total_distance' : 0, 608 | 'mat_t' : 0, 609 | 'k_tx' : 0, 610 | 'k_rx' : 0, 611 | 'scat_last_objects' : 0, 612 | 'scat_last_vertices' : 0, 613 | 'scat_last_k_i' : 0, 614 | 'scat_k_s' : 0, 615 | 'scat_last_normals' : 0, 616 | 'scat_src_2_last_int_dist' : 0, 617 | 'scat_2_target_dist' : 0} 618 | 619 | # Remove useless tensors and drop the target axis 620 | dict_list = [] 621 | for d_ in dict_list_: 622 | d = {} 623 | for k in d_.keys(): 624 | # Drop useless tensors 625 | if not k.startswith('scat_prefix_'): 626 | d.update({k : d_[k]}) 627 | # Squeezes target dimension if requested 628 | if squeeze_target_dim and (k in target_axis): 629 | d[k] = tf.squeeze(d[k], axis=target_axis[k]) 630 | dict_list.append(d) 631 | 632 | # Add a prefix to indicate to which object each tensor belongs to 633 | all_tensors = {} 634 | all_tensors.update({'spec-' + k : dict_list[0][k] for k in dict_list[0]}) 635 | all_tensors.update({'diff-' + k : dict_list[1][k] for k in dict_list[1]}) 636 | all_tensors.update({'scat-' + k : dict_list[2][k] for k in dict_list[2]}) 637 | all_tensors.update({'tmp-spec-' + k : dict_list[3][k] for k in dict_list[3]}) 638 | all_tensors.update({'tmp-diff-' + k : dict_list[4][k] for k in dict_list[4]}) 639 | all_tensors.update({'tmp-scat-' + k : dict_list[5][k] for k in dict_list[5]}) 640 | 641 | # Add the receiver position 642 | all_tensors.update({'rx_pos' : rx_pos}) 643 | 644 | # Add the channel measurement 645 | all_tensors.update({'h_meas' : h_meas}) 646 | 647 | # Serialize the tensors to a string of bytes 648 | for k,v in all_tensors.items(): 649 | all_tensors[k] = tf.train.Feature(bytes_list=tf.train.BytesList(value=[tf.io.serialize_tensor(v).numpy()])) 650 | 651 | ex = tf.train.Example(features=tf.train.Features(feature=all_tensors)) 652 | record_bytes = ex.SerializeToString() 653 | return record_bytes 654 | 655 | def reshape_h_meas(h_meas_raw): 656 | """ 657 | Reshapes ``h_meas_raw`` to separate the two transmitters 658 | 659 | Input 660 | ------ 661 | h_meas_raw : [1, num_tx*num_tx_ant=64, num_subcarriers=1024], tf.float 662 | Tensor of measurements 663 | 664 | Output 665 | ------ 666 | : [num_tx=2, num_tx_ant=32, num_subcarriers=1024], tf.float 667 | Reshaped tensor of measurements 668 | """ 669 | 670 | # Indices of the antennas for each transmitter 671 | tx_1_ind = tf.constant([58, 57, 63, 34, 672 | 12, 7, 13, 9, 673 | 24, 45, 55, 36, 674 | 9, 18, 37, 32, 675 | 16, 42, 46, 22, 676 | 2, 52, 50, 62, 677 | 33, 28, 43, 39, 678 | 38, 21, 51, 10], tf.int32) 679 | 680 | tx_2_ind = tf.constant([60, 40, 44, 3, 681 | 54, 8, 53, 11, 682 | 6, 0, 61, 47, 683 | 17, 27, 59, 30, 684 | 49, 41, 48, 14, 685 | 20, 25, 35, 4, 686 | 5, 23, 1, 15, 687 | 19, 56, 26, 31], tf.int32) 688 | 689 | # Extract the antennas for each transmitter 690 | # [num_tx = 1, num_tx_and = 32, nu_subcarriers = 1024] 691 | h_meas_tx_1 = tf.gather(h_meas_raw, tx_1_ind, axis=1) 692 | h_meas_tx_2 = tf.gather(h_meas_raw, tx_2_ind, axis=1) 693 | 694 | # Concatenate into one tensor 695 | # [num_tx = 2, num_tx_and = 32, nu_subcarriers = 1024] 696 | h_meas = tf.concat([h_meas_tx_1, h_meas_tx_2], axis=0) 697 | 698 | return h_meas 699 | 700 | def deserialize_paths_as_tensor_dicts(serialized_item): 701 | """ 702 | Deserializes examples of a dataset of traced paths 703 | 704 | Input 705 | ----- 706 | serialized_item : str 707 | A stream of bytes 708 | 709 | Output 710 | ------- 711 | rx_pos : [3], tf.float 712 | Position of the receiver 713 | 714 | h_meas : [num_tx*num_tx_ant=64, num_subcarriers=1024], tf.complex 715 | Measured CSI 716 | 717 | spec_data : dict 718 | Dictionary of LoS and specular traced paths 719 | 720 | diff_data : dict 721 | Dictionary of diffracted traced paths 722 | 723 | scat_data : dict 724 | Dictionary of scattered traced paths 725 | 726 | tmp_spec_data : dict 727 | Dictionary of additional LoS and specular traced paths data 728 | 729 | tmp_diff_data : dict 730 | Dictionary of additional diffracted traced paths data 731 | 732 | tmp_scat_data : dict 733 | Dictionary of additional scattered traced paths data 734 | """ 735 | 736 | # Fields names and types 737 | paths_fields_dtypes = {'a' : tf.complex64, 738 | 'mask' : tf.bool, 739 | 'normalize_delays' : tf.bool, 740 | 'objects' : tf.int32, 741 | 'phi_r' : tf.float32, 742 | 'phi_t' : tf.float32, 743 | 'reverse_direction' : tf.bool, 744 | 'sources' : tf.float32, 745 | 'targets' : tf.float32, 746 | 'tau' : tf.float32, 747 | 'theta_r' : tf.float32, 748 | 'theta_t' : tf.float32, 749 | 'types' : tf.int32, 750 | 'vertices' : tf.float32} 751 | tmp_paths_fields_dtypes = {'k_i' : tf.float32, 752 | 'k_r' : tf.float32, 753 | 'k_rx' : tf.float32, 754 | 'k_tx' : tf.float32, 755 | 'normals' : tf.float32, 756 | 'scat_2_target_dist' : tf.float32, 757 | 'scat_k_s' : tf.float32, 758 | 'scat_last_k_i' : tf.float32, 759 | 'scat_last_normals' : tf.float32, 760 | 'scat_last_objects' : tf.int32, 761 | 'scat_last_vertices' : tf.float32, 762 | 'scat_src_2_last_int_dist' : tf.float32, 763 | 'sources' :tf.float32, 764 | 'targets' : tf.float32, 765 | 'total_distance' : tf.float32, 766 | 'num_samples' : tf.int32, 767 | 'scat_keep_prob' : tf.float32} 768 | members_dtypes = {} 769 | members_dtypes.update({'spec-' + k : paths_fields_dtypes[k] for k in paths_fields_dtypes}) 770 | members_dtypes.update({'diff-' + k : paths_fields_dtypes[k] for k in paths_fields_dtypes}) 771 | members_dtypes.update({'scat-' + k : paths_fields_dtypes[k] for k in paths_fields_dtypes}) 772 | members_dtypes.update({'tmp-spec-' + k : tmp_paths_fields_dtypes[k] for k in tmp_paths_fields_dtypes}) 773 | members_dtypes.update({'tmp-diff-' + k : tmp_paths_fields_dtypes[k] for k in tmp_paths_fields_dtypes}) 774 | members_dtypes.update({'tmp-scat-' + k : tmp_paths_fields_dtypes[k] for k in tmp_paths_fields_dtypes}) 775 | members_dtypes.update({'tmp-scat-' + k : tmp_paths_fields_dtypes[k] for k in tmp_paths_fields_dtypes}) 776 | 777 | # Add the receiver position 778 | members_dtypes.update({'rx_pos' : tf.float32}) 779 | 780 | # Add channel measurement 781 | members_dtypes.update({'h_meas' : tf.complex64}) 782 | 783 | # Build dict of tensors 784 | # Deserializes the byte stream corresponding to each tensor 785 | features = {k : tf.io.FixedLenFeature([], tf.string, default_value = '') for k in members_dtypes} 786 | record = tf.io.parse_single_example(serialized_item, features) 787 | members_data = {k : tf.io.parse_tensor(record[k], out_type = members_dtypes[k]) for k in members_dtypes} 788 | 789 | # Builds the paths objects 790 | spec_data = {k[len('spec-'):] : members_data[k] for k in members_data if k.startswith('spec-')} 791 | diff_data = {k[len('diff-'):] : members_data[k] for k in members_data if k.startswith('diff-')} 792 | scat_data = {k[len('scat-'):] : members_data[k] for k in members_data if k.startswith('scat-')} 793 | tmp_spec_data = {k[len('tmp-spec-'):] : members_data[k] for k in members_data if k.startswith('tmp-spec-')} 794 | tmp_diff_data = {k[len('tmp-diff-'):] : members_data[k] for k in members_data if k.startswith('tmp-diff-')} 795 | tmp_scat_data = {k[len('tmp-scat-'):] : members_data[k] for k in members_data if k.startswith('tmp-scat-')} 796 | 797 | # Retrieve receiver position 798 | rx_pos = members_data['rx_pos'] 799 | 800 | # Retrieve channel measurement 801 | h_meas = members_data['h_meas'] 802 | 803 | return rx_pos, h_meas, spec_data, diff_data, scat_data, tmp_spec_data, tmp_diff_data, tmp_scat_data 804 | 805 | def tensor_dicts_to_traced_paths(scene, tensor_dicts): 806 | """ 807 | Creates Sionna `Paths` and `PathsTmpData` objects from dictionaries 808 | 809 | Input 810 | ------ 811 | scene : Sionna.rt.Scene 812 | Scene 813 | 814 | tensor_dicts : `list` of `dict` 815 | List of dictionaries, as retrieved when iterating over a dataset 816 | of traced paths and using ``deserialize_paths_as_tensor_dicts()`` 817 | to retrieve the data. 818 | 819 | Output 820 | ------- 821 | spec_paths : Paths 822 | Specular paths 823 | 824 | diff_paths : Paths 825 | Diffracted paths 826 | 827 | scat_paths : Paths 828 | Scattered paths 829 | 830 | spec_paths_tmp : PathsTmpData 831 | Additional data required to compute the EM fields of the specular 832 | paths 833 | 834 | diff_paths_tmp : PathsTmpData 835 | Additional data required to compute the EM fields of the diffracted 836 | paths 837 | 838 | scat_paths_tmp : PathsTmpData 839 | Additional data required to compute the EM fields of the scattered 840 | paths 841 | """ 842 | 843 | sources = tensor_dicts[0]['sources'] 844 | targets = tensor_dicts[0]['targets'] 845 | 846 | spec_paths = Paths(sources, targets, scene) 847 | spec_paths.from_dict(tensor_dicts[0]) 848 | 849 | diff_paths = Paths(sources, targets, scene) 850 | diff_paths.from_dict(tensor_dicts[1]) 851 | 852 | scat_paths = Paths(sources, targets, scene) 853 | scat_paths.from_dict(tensor_dicts[2]) 854 | 855 | spec_tmp_paths = PathsTmpData(sources, targets, scene.dtype) 856 | spec_tmp_paths.from_dict(tensor_dicts[3]) 857 | 858 | diff_tmp_paths = PathsTmpData(sources, targets, scene.dtype) 859 | diff_tmp_paths.from_dict(tensor_dicts[4]) 860 | 861 | scat_tmp_paths = PathsTmpData(sources, targets, scene.dtype) 862 | scat_tmp_paths.from_dict(tensor_dicts[5]) 863 | 864 | return (spec_paths, diff_paths, scat_paths, spec_tmp_paths, diff_tmp_paths, scat_tmp_paths) 865 | 866 | def pad_traced_paths(traced_paths, max_num_paths_spec, max_num_paths_diff, max_num_paths_scat): 867 | """ 868 | Pads traced paths such that they all have the same maximum number of paths 869 | 870 | The paths added for padding are masked. 871 | 872 | Input 873 | ------ 874 | traced_paths : `list` 875 | List of `Paths` and `PathsTmpData` objects, as returned by 876 | ``tensor_dicts_to_traced_paths()`` 877 | 878 | max_num_paths_spec : int 879 | Maximum number of LoS and specular paths over the dataset. 880 | Padding will be added to reach that number. 881 | 882 | max_num_paths_diff : int 883 | Maximum number of diffracted paths over the dataset. 884 | Padding will be added to reach that number. 885 | 886 | max_num_paths_scat : int 887 | Maximum number of scattered paths over the dataset. 888 | Padding will be added to reach that number. 889 | 890 | Output 891 | ------- 892 | spec_paths : Paths 893 | Specular paths 894 | 895 | diff_paths : Paths 896 | Diffracted paths 897 | 898 | scat_paths : Paths 899 | Scattered paths 900 | 901 | spec_paths_tmp : PathsTmpData 902 | Additional data required to compute the EM fields of the specular 903 | paths 904 | 905 | diff_paths_tmp : PathsTmpData 906 | Additional data required to compute the EM fields of the diffracted 907 | paths 908 | 909 | scat_paths_tmp : PathsTmpData 910 | Additional data required to compute the EM fields of the scattered 911 | paths 912 | """ 913 | 914 | ### Function that pads a `Paths` object 915 | def _pad_paths(paths, max_num_paths): 916 | axis_to_pad = {'mask' : -1, 917 | 'objects' : -1, 918 | 'phi_r' : -1, 919 | 'phi_t' : -1, 920 | 'tau' : -1, 921 | 'theta_r' : -1, 922 | 'theta_t' : -1, 923 | 'vertices' : -2} 924 | 925 | paths_dicts = paths.to_dict() 926 | for k,a in axis_to_pad.items(): 927 | t = paths_dicts[k] 928 | 929 | r = tf.rank(t).numpy() 930 | dim = r + a 931 | 932 | num_paths = t.shape[dim] 933 | if num_paths == 0: 934 | continue 935 | 936 | padding_size = max_num_paths - num_paths 937 | assert padding_size >= 0 938 | 939 | paddings = [[0,0]]*r 940 | paddings[dim] = [0,padding_size] 941 | 942 | if k == 'mask': 943 | t = tf.pad(t, paddings, constant_values=False) # Mask the paths added for padding 944 | elif k == 'tau': 945 | t = tf.pad(t, paddings, constant_values=-1.) 946 | elif k == 'objects': 947 | t = tf.pad(t, paddings, constant_values=-1) 948 | else: 949 | t = tf.pad(t, paddings, constant_values=0) 950 | paths_dicts[k] = t 951 | paths.from_dict(paths_dicts) 952 | 953 | return paths 954 | 955 | ### Function that pads a `PathsTmpData` object 956 | def _pad_tmp_paths(tmp_paths, max_num_paths): 957 | axis_to_pad = {'k_i' : -2, 958 | 'k_r' : -2, 959 | 'k_rx' : -2, 960 | 'k_tx' : -2, 961 | 'normals' : -2, 962 | 'scat_2_target_dist' : -1, 963 | 'scat_k_s' : -2, 964 | 'scat_last_k_i' : -2, 965 | 'scat_last_normals' : -2, 966 | 'scat_last_objects' : -1, 967 | 'scat_last_vertices' : -2, 968 | 'scat_src_2_last_int_dist' : -1, 969 | 'total_distance' : -1} 970 | 971 | paths_dicts = tmp_paths.to_dict() 972 | for k,a in axis_to_pad.items(): 973 | t = paths_dicts[k] 974 | 975 | r = tf.rank(t).numpy() 976 | dim = r + a 977 | 978 | num_paths = t.shape[dim] 979 | if num_paths == 0: 980 | continue 981 | 982 | padding_size = max_num_paths - num_paths 983 | assert padding_size >= 0 984 | 985 | paddings = [[0,0]]*r 986 | paddings[dim] = [0,padding_size] 987 | 988 | t = tf.pad(t, paddings, constant_values=0) 989 | paths_dicts[k] = t 990 | tmp_paths.from_dict(paths_dicts) 991 | 992 | return tmp_paths 993 | 994 | # Tiling the paths 995 | spec_paths = _pad_paths(traced_paths[0], max_num_paths_spec) 996 | diff_paths = _pad_paths(traced_paths[1], max_num_paths_diff) 997 | scat_paths = _pad_paths(traced_paths[2], max_num_paths_scat) 998 | 999 | # Tiling the additional data paths 1000 | tmp_spec_paths = _pad_tmp_paths(traced_paths[3], max_num_paths_spec) 1001 | tmp_diff_paths = _pad_tmp_paths(traced_paths[4], max_num_paths_diff) 1002 | tmp_scat_paths = _pad_tmp_paths(traced_paths[5], max_num_paths_scat) 1003 | 1004 | return spec_paths, diff_paths, scat_paths, tmp_spec_paths, tmp_diff_paths, tmp_scat_paths 1005 | 1006 | def batchify(traced_paths_dicts): 1007 | """ 1008 | Batchifies traced paths dictionaries 1009 | 1010 | This utility enables sampling batches of receivers positions from the dataset of traced paths. 1011 | It arranges the receivers as targets, by concatenating and reshaping the tensors accordingly. 1012 | 1013 | Input 1014 | ------ 1015 | tensor_dicts : `list` of `dict` 1016 | List of dictionaries, as retrieved when iterating over a dataset 1017 | of traced paths and using ``deserialize_paths_as_tensor_dicts()`` 1018 | to retrieve the data. 1019 | 1020 | Output 1021 | ------- 1022 | : `list` of `dict` 1023 | List of dictionaries 1024 | """ 1025 | 1026 | # Target axis index 1027 | axis_to_swap = {'objects' : 1, 1028 | 'vertices' : 1, 1029 | 'k_i' : 1, 1030 | 'k_r' : 1, 1031 | 'normals' : 1} 1032 | 1033 | for d in traced_paths_dicts: 1034 | # Swap axis 0 and targets if required 1035 | # This is done because when batching a TF dataset, the batch dimension 1036 | # is always axis 0, which might not be the target axis. 1037 | for k in d.keys(): 1038 | if k not in axis_to_swap: 1039 | continue 1040 | v = d[k] 1041 | a = axis_to_swap[k] 1042 | perm = tf.range(tf.rank(v)) 1043 | perm = tf.tensor_scatter_nd_update(perm, [[0]], [a]) 1044 | perm = tf.tensor_scatter_nd_update(perm, [[a]], [0]) 1045 | d[k] = tf.transpose(v, perm) 1046 | 1047 | # Drop the batch dimension for sources, as these are the same 1048 | # for all examples in the batch 1049 | if 'sources' in d: 1050 | d['sources'] = d['sources'][0] 1051 | 1052 | # Drop the batch dim for types 1053 | if 'types' in d: 1054 | d['types'] = d['types'][0] 1055 | 1056 | # De-batchify num_samples and scat_keep_prop 1057 | traced_paths_dicts[-1]['num_samples'] = traced_paths_dicts[-1]['num_samples'][0] 1058 | traced_paths_dicts[-1]['scat_keep_prob'] = traced_paths_dicts[-1]['scat_keep_prob'][0] 1059 | traced_paths_dicts[-2]['num_samples'] = traced_paths_dicts[-2]['num_samples'][0] 1060 | traced_paths_dicts[-2]['scat_keep_prob'] = traced_paths_dicts[-2]['scat_keep_prob'][0] 1061 | traced_paths_dicts[-3]['num_samples'] = traced_paths_dicts[-3]['num_samples'][0] 1062 | traced_paths_dicts[-3]['scat_keep_prob'] = traced_paths_dicts[-3]['scat_keep_prob'][0] 1063 | 1064 | return traced_paths_dicts 1065 | 1066 | def split_dataset(dataset, dataset_size, training_set_size, validation_set_size, 1067 | test_set_size, shuffle_seed=42): 1068 | r""" 1069 | Splits the dataset by taking the first ``training_set_size`` elements to form 1070 | the training set, and the last ``validation_set_size`` and ``test_set_size`` 1071 | elements to form the validation and test sets, respectively. 1072 | 1073 | The dataset is first shuffled using ``shuffle_seed`` as seed for the random 1074 | number generator. Multiple calls to this function using the same seed leads 1075 | to the same subsets to be created. 1076 | 1077 | Input 1078 | ------ 1079 | dataset : tf.data.Dataset 1080 | Dataset to split 1081 | 1082 | dataset_size : int 1083 | Size of the dataset to split 1084 | 1085 | training_set_size : int 1086 | Size of the training subset to create 1087 | 1088 | validation_set_size : int 1089 | Size of the validation subset to create 1090 | 1091 | test_set_size : int 1092 | Size of the test subset to create 1093 | 1094 | shuffle_seed : int 1095 | Seed for shuffling the dataset before splitting. 1096 | Defaults to 42. 1097 | 1098 | Output 1099 | ------- 1100 | training_set : tf.data.Dataset 1101 | Training subset 1102 | 1103 | validation_set : tf.data.Dataset 1104 | Validation subset 1105 | 1106 | test_set : tf.data.Dataset 1107 | Test subset 1108 | """ 1109 | 1110 | # Not reshuffle after each iteration to ensure that multiple calls to this 1111 | # function leads to the same subsets to be created, assuming that the same 1112 | # seed is used. 1113 | tf.random.set_seed(42) 1114 | shuffled_dataset = dataset.shuffle(256, seed=shuffle_seed, 1115 | reshuffle_each_iteration=False) 1116 | 1117 | training_set = shuffled_dataset.take(training_set_size) 1118 | test_validation_set = shuffled_dataset.skip(dataset_size-validation_set_size-test_set_size) 1119 | validation_set = test_validation_set.take(validation_set_size) 1120 | test_set = test_validation_set.skip(validation_set_size) 1121 | 1122 | return training_set, validation_set, test_set 1123 | 1124 | def ale(p, p_ref): 1125 | """ 1126 | Computes the absolute logarithmic error (ALE) 1127 | 1128 | Input 1129 | ------ 1130 | p : [..., num_antenna] 1131 | Batch of predictions 1132 | 1133 | p_ref : [..., num_antennas] 1134 | Batch of reference values 1135 | 1136 | Output 1137 | ------- 1138 | : [...] 1139 | Absolute logarithmic error (ALE) 1140 | """ 1141 | 1142 | # Average over antennas 1143 | # [...] 1144 | p_avg = tf.reduce_mean(p, axis=-1) 1145 | p_ref_avg = tf.reduce_mean(p_ref, axis=-1) 1146 | 1147 | # Linear to dB scale 1148 | # [...] 1149 | p_avg_db = 10.*log10(p_avg) 1150 | p_ref_avg_db = 10.*log10(p_ref_avg) 1151 | 1152 | # ALE for each example and antenna 1153 | # [...] 1154 | ale_ = tf.abs(p_avg_db - p_ref_avg_db) 1155 | 1156 | return ale_ 1157 | 1158 | def relative_abs_error(p, p_ref): 1159 | """ 1160 | Computes the relative absolute error 1161 | 1162 | Input 1163 | ------ 1164 | p : [..., num_antenna] 1165 | Batch of predictions 1166 | 1167 | p_ref : [..., num_antennas] 1168 | Batch of reference values 1169 | 1170 | Output 1171 | ------- 1172 | : [...] 1173 | Absolute relative error 1174 | """ 1175 | 1176 | # Average over antennas 1177 | # [...] 1178 | p_avg = tf.reduce_mean(p, axis=-1) 1179 | p_ref_avg = tf.reduce_mean(p_ref, axis=-1) 1180 | 1181 | # Absolute error for each example and antenna 1182 | # [...] 1183 | ae_ = tf.abs(p_avg - p_ref_avg)/p_ref_avg 1184 | 1185 | return ae_ 1186 | 1187 | def ds_ray_trace(scene, scaling_factor, params, test_set, batch_size, 1188 | test_set_size, num_subcarriers, bandwidth, scattering): 1189 | """ 1190 | Computes the synthetic channel frequency response over the dataset 1191 | ``test_set``. 1192 | 1193 | The measured CIRs are scaled by ``scaling_factor``. 1194 | 1195 | Input 1196 | ------ 1197 | scaling_factor : float 1198 | Scaling factor by which to scale the measurements 1199 | 1200 | params : `dict` 1201 | Dataset parameters 1202 | 1203 | test_set : tf.data.Dataset 1204 | Dataset 1205 | 1206 | batch_size : int 1207 | Batch size to use for tracing 1208 | 1209 | test_set_size : int 1210 | Size of the dataset ``test_set`` 1211 | 1212 | num_subcarriers : int 1213 | Number of subcarriers 1214 | 1215 | bandwidth : float 1216 | Bandwidth [Hz] 1217 | 1218 | scattering : bool 1219 | Enable/Disable scattering 1220 | 1221 | Output 1222 | ------- 1223 | rx_pos : [test_set_size, 3], tf.float 1224 | Receivers positions 1225 | 1226 | h_rt : [num_samples, num_tx, num_antenna, num_subcarriers], tf.complex 1227 | Synthetic channel impulse responses generated by the ray tracer 1228 | 1229 | h_meas : [num_samples, num_tx, num_antenna, num_subcarriers], tf.complex 1230 | Measured channel impulse responses 1231 | """ 1232 | 1233 | frequencies = subcarrier_frequencies(num_subcarriers, bandwidth/num_subcarriers) 1234 | 1235 | ########################################### 1236 | # Function that runs a single evaluation 1237 | # step 1238 | ########################################## 1239 | @tf.function 1240 | def evaluation_step(rx_pos, h_meas, traced_paths): 1241 | 1242 | # Placer receiver 1243 | set_receiver_positions(scene, rx_pos) 1244 | 1245 | # Build traced paths 1246 | traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths) 1247 | 1248 | paths = scene.compute_fields(*traced_paths, 1249 | scat_random_phases=False, 1250 | check_scene=False) 1251 | 1252 | a, tau = paths.cir(scattering=scattering) 1253 | 1254 | # Compute channel frequency response 1255 | h_rt = cir_to_ofdm_channel(frequencies, a, tau) 1256 | 1257 | # Remove useless dimensions 1258 | h_rt = tf.squeeze(h_rt, axis=[0,2,5]) 1259 | 1260 | # Normalize h to make sure that power is independent of the number of subacrriers 1261 | h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.) 1262 | 1263 | # Scale measurements 1264 | h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.) 1265 | 1266 | return h_rt, h_meas 1267 | 1268 | ########################################## 1269 | # Compute frequency domain CIRs over 1270 | # the test set 1271 | ########################################## 1272 | 1273 | h_rt = [] 1274 | h_meas = [] 1275 | rx_pos = [] 1276 | 1277 | # Number of iterations 1278 | num_test_iter = test_set_size // batch_size 1279 | test_set_ter = iter(test_set.batch(batch_size)) 1280 | for next_item in range(num_test_iter): 1281 | # Next set of traced paths 1282 | next_item = next(test_set_ter, None) 1283 | 1284 | # Retreive the receiver position separately 1285 | rx_pos_, h_meas_, traced_paths = next_item[0], next_item[1], next_item[2:] 1286 | # Skip iteration if does not match the batch size 1287 | if rx_pos_.shape[0] != batch_size: 1288 | continue 1289 | 1290 | # Batchify 1291 | traced_paths = batchify(traced_paths) 1292 | 1293 | # Evaluate 1294 | eval_quantities = evaluation_step(rx_pos_, h_meas_, traced_paths) 1295 | h_rt_, h_meas_ = eval_quantities 1296 | h_rt.append(h_rt_) 1297 | h_meas.append(h_meas_) 1298 | rx_pos.append(rx_pos_) 1299 | rx_pos = tf.concat(rx_pos, axis=0) 1300 | h_rt = tf.concat(h_rt, axis=0) 1301 | h_meas = tf.concat(h_meas, axis=0) 1302 | 1303 | return rx_pos, h_rt, h_meas 1304 | 1305 | def cir2freq(a, tau): 1306 | """Converts complex baseband channel impulse response to frequency response 1307 | 1308 | Input 1309 | ----- 1310 | a : [batch_size, num_rx, num_rx_ant, num_tx, num_tx_ant, max_num_paths, num_time_steps], tf.complex 1311 | Path coefficients 1312 | 1313 | tau : [batch_size, num_rx, num_rx_ant, num_tx, num_tx_ant, max_num_paths] , tf.real 1314 | Path delays 1315 | 1316 | Output 1317 | ------ 1318 | h : [batch size, num_rx, num_rx_ant, num_tx, num_tx_ant, num_time_steps, 1024], tf.complex 1319 | Channel frequency response assuming a 50MHz 1024-subcarrier OFDM system 1320 | """ 1321 | # Compute channel frequency response 1322 | num_subcarriers = 1024 1323 | bandwidth = 50e6 1324 | frequencies = subcarrier_frequencies(num_subcarriers, bandwidth/num_subcarriers) 1325 | h_rt = cir_to_ofdm_channel(frequencies, a, tau) 1326 | 1327 | # Normalize h to make sure that power is independent of the number of subacrriers 1328 | h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.) 1329 | 1330 | return h_rt 1331 | 1332 | def save_model(layer, filename, scaling_factor): 1333 | weights = layer.get_weights() 1334 | weights.append(scaling_factor) 1335 | with open(filename, 'wb') as f: 1336 | pickle.dump(weights, f) 1337 | 1338 | def load_model(layer, filename): 1339 | with open(filename, 'rb') as f: 1340 | weights = pickle.load(f) 1341 | scaling_factor = weights[-1] 1342 | weights = weights[:-1] 1343 | layer.set_weights(weights) 1344 | return scaling_factor 1345 | 1346 | def plot_results_synthetic_data(train_dict, ground_truth, scene, save_figs=False): 1347 | """Utility function to plot results for the calibration against a synthetic dataset""" 1348 | fig_folder = "../results/synthetic/" 1349 | 1350 | ### 1351 | ### Learned Materials 1352 | ### 1353 | iterations = np.array(train_dict["iterations"]) 1354 | for param in ground_truth["floor"]: 1355 | plt.rcParams['font.size'] = 16 1356 | plt.rcParams['font.family'] = 'serif' 1357 | # activate to reproduce the exact figures from the paper 1358 | # with latex backend 1359 | plt.rcParams['text.usetex'] = False 1360 | plt.figure(figsize=(7,4)) 1361 | legend = [] 1362 | start_id = np.min([obj.object_id for obj in scene.objects.values()]) 1363 | for obj in scene.objects.values(): 1364 | i = obj.object_id-start_id 1365 | name = obj.name 1366 | plt.plot(iterations, train_dict[name][param], "-", c=f"C{i}") 1367 | for obj in scene.objects.values(): 1368 | i = obj.object_id-start_id 1369 | name = obj.name 1370 | plt.plot(iterations, np.ones_like(iterations)*ground_truth[name][param], "--", c=f"C{i}") 1371 | legend.append(name.capitalize()) 1372 | plt.legend(legend, loc="upper right"); 1373 | plt.xlabel("Iteration") 1374 | 1375 | if param=="relative_permittivity": 1376 | ylabel = r"Relative permittivity $\varepsilon_r$" 1377 | elif param=="conductivity": 1378 | ylabel=r"Conductivity $\sigma$" 1379 | elif param=="scattering_coefficient": 1380 | ylabel=r"Scattering coefficient $S$" 1381 | elif param=="xpd_coefficient": 1382 | ylabel=r"XPD coefficient $K_x$" 1383 | plt.ylabel(ylabel, fontsize=14); 1384 | plt.xlim([0, 3000]) 1385 | plt.tight_layout() 1386 | if save_figs: 1387 | plt.savefig(fig_folder + f"{param}.pdf") 1388 | 1389 | ### 1390 | ### Antenna pattern 1391 | ### 1392 | # Vertical cut 1393 | def comp_v(pattern): 1394 | theta = np.linspace(0.0, PI, 1000) 1395 | c_theta, c_phi = pattern(theta, np.zeros_like(theta)) 1396 | g = np.abs(c_theta)**2 + np.abs(c_phi)**2 1397 | g = np.where(g==0, 1e-12, g) 1398 | g_db = 10*np.log10(g) 1399 | g_db_max = np.max(g_db) 1400 | g_db_min = np.min(g_db) 1401 | if g_db_min==g_db_max: 1402 | g_db_min = -30 1403 | else: 1404 | g_db_min = np.maximum(-60., g_db_min) 1405 | return theta, g_db, g_db_min, g_db_max 1406 | 1407 | # Horizontal cut 1408 | def comp_h(pattern): 1409 | phi = np.linspace(-PI, PI, 1000) 1410 | c_theta, c_phi = pattern(PI/2*tf.ones_like(phi) , 1411 | tf.constant(phi, tf.float32)) 1412 | c_theta = c_theta.numpy() 1413 | c_phi = c_phi.numpy() 1414 | g = np.abs(c_theta)**2 + np.abs(c_phi)**2 1415 | g = np.where(g==0, 1e-12, g) 1416 | g_db = 10*np.log10(g) 1417 | g_db_max = np.max(g_db) 1418 | g_db_min = np.min(g_db) 1419 | if g_db_min==g_db_max: 1420 | g_db_min = -30 1421 | else: 1422 | g_db_min = np.maximum(-60., g_db_min) 1423 | return phi, g_db, g_db_min, g_db_max 1424 | 1425 | fig_v = plt.figure(figsize=(4,4)) 1426 | theta, g_db, g_db_min, g_db_max = comp_v(tr38901_pattern) 1427 | plt.polar(theta, g_db, "--") 1428 | theta, g_db, g_db_min, g_db_max = comp_v(scene.tx_array.antenna.patterns[0]) 1429 | plt.polar(theta, g_db) 1430 | fig_v.axes[0].set_rmin(g_db_min) 1431 | fig_v.axes[0].set_rmax(g_db_max+3) 1432 | fig_v.axes[0].set_theta_zero_location("N") 1433 | fig_v.axes[0].set_theta_direction(-1) 1434 | fig_v.axes[0].set_yticklabels([]) 1435 | plt.legend(["IEEE 38.901", "Learned"], loc='upper left') 1436 | plt.tight_layout() 1437 | if save_figs: 1438 | plt.savefig(fig_folder + "antenna_pattern_v.pdf") 1439 | 1440 | fig_h = plt.figure(figsize=(4,4)) 1441 | phi, g_db, g_db_min, g_db_max = comp_h(tr38901_pattern) 1442 | plt.polar(phi, g_db, "--") 1443 | phi, g_db, g_db_min, g_db_max = comp_h(scene.tx_array.antenna.patterns[0]) 1444 | plt.polar(phi, g_db) 1445 | fig_h.axes[0].set_rmin(g_db_min) 1446 | fig_h.axes[0].set_rmax(g_db_max+3) 1447 | fig_h.axes[0].set_theta_zero_location("E") 1448 | fig_h.axes[0].set_yticklabels([]) 1449 | #plt.legend(["IEEE 38.901", "Learned"], loc='upper left') 1450 | plt.tight_layout() 1451 | if save_figs: 1452 | plt.savefig(fig_folder + "antenna_pattern_h.pdf") 1453 | 1454 | ### 1455 | ### Scattering pattern 1456 | ### 1457 | theta_i = PI/3 1458 | k_i = -r_hat(theta_i, PI) 1459 | n_hat = r_hat(0., 0.) 1460 | 1461 | learned_pattern = scene.scattering_pattern_callable 1462 | theta_s = tf.cast(tf.linspace(0.0, PI/2, 100), dtype=tf.float32) 1463 | phi_s = tf.broadcast_to(0., theta_s.shape) 1464 | 1465 | k_s = r_hat(theta_s, phi_s) 1466 | k_i = tf.broadcast_to(k_i, k_s.shape) 1467 | n_hat = tf.broadcast_to(n_hat, k_s.shape) 1468 | 1469 | fig_cut = plt.figure() 1470 | plt.polar(theta_s, ground_truth["target_pattern"](k_i, k_s, n_hat), color='C0', linestyle="dashed") 1471 | plt.polar(theta_s, learned_pattern(None, None, k_i, k_s, n_hat), color='C1') 1472 | 1473 | plt.polar(2*PI-theta_s, ground_truth["target_pattern"](k_i, r_hat(theta_s, phi_s-PI), n_hat), color='C0', linestyle="dashed") 1474 | plt.polar(2*PI-theta_s, learned_pattern(None, None, k_i, r_hat(theta_s, phi_s-PI), n_hat), color='C1') 1475 | 1476 | plt.legend(["Backscattering Model", "Learned"], loc="best") 1477 | 1478 | ax = fig_cut.axes[0] 1479 | xticks = [0, 30/180*np.pi, 60/180*np.pi, 90/180*np.pi, -30/180*np.pi, -60/180*np.pi, -90/180*np.pi] 1480 | ax.set_xticks(xticks) 1481 | ax.text(-theta_i-10*PI/180, ax.get_yticks()[-1]*2/3, r"$\hat{\mathbf{k}}_\mathrm{i}$", horizontalalignment='center') 1482 | ax.text(theta_i+10*PI/180, ax.get_yticks()[-1]*2/3, r"$\hat{\mathbf{k}}_\mathrm{r}$", horizontalalignment='center') 1483 | plt.quiver([0], [0], [np.sin(theta_i)], [np.cos(theta_i)], scale=1., color="grey",) 1484 | plt.quiver([0], [0], [-np.sin(theta_i)], [np.cos(theta_i)], scale=1., color="grey",) 1485 | ax.set_theta_zero_location("N") 1486 | ax.set_theta_direction(-1) 1487 | ax.set_thetamin(-90) 1488 | ax.set_thetamax(90) 1489 | plt.tight_layout() 1490 | labels = [e.get_text() for e in ax.get_xticklabels()] 1491 | labels[-2] = r"$\theta_i$" 1492 | labels[2] = r"$\theta_r$" 1493 | ax.set_xticklabels(labels); 1494 | ax.set_yticklabels([]); 1495 | 1496 | if save_figs: 1497 | plt.savefig(fig_folder + "scattering_pattern.pdf") 1498 | -------------------------------------------------------------------------------- /data/coordinates.csv: -------------------------------------------------------------------------------- 1 | Name,South,West,Height 2 | TSU,3.9165,9.8176,-0.1120 3 | TSO,3.9195,9.8189,2.8502 4 | TNU,1.7911,10.4856,-0.1093 5 | TNO,noLoS,noLoS,noLoS 6 | CU,2.5766,5.2816,-0.1131 7 | CO,2.5770,5.2383,2.8445 8 | BU,6.9001,3.8987,-0.1094 9 | BO,noLoS,noLoS,noLoS 10 | AU,5.8567,0.4693,-0.1120 11 | AO,noLoS,noLoS,noLoS 12 | DU,-0.7558,2.3487,-0.1110 13 | DO,noLoS,noLoS,noLoS 14 | NOO,-23.1860,6.9211,2.8337 15 | NOU,-23.1565,6.9172,-0.1214 16 | NWU,-22.4925,9.0996,-0.1106 17 | NWO,noLoS,noLoS,noLoS 18 | SOO,26.1379,-8.1092,2.8357 19 | SOU,26.1325,-8.0808,-0.1260 20 | SWU,26.7883,-5.9074,-0.1238 21 | SWO,noLoS,noLoS,noLoS 22 | P1RO,-4.8401,1.3456,1.8851 23 | P1LO,-5.9752,1.6936,1.8844 24 | P1LU,-5.9768,1.6995,0.9231 25 | P1RU,-4.8345,1.3538,0.9232 26 | P2LU,-2.5311,0.6503,0.9208 27 | P2LO,-2.5350,0.6459,1.8823 28 | P2RO,-1.3959,0.3039,1.8833 29 | P2RU,-1.3868,0.2967,0.9197 30 | ARO,6.6234,3.0305,2.4402 31 | ALO,5.9123,0.7195,2.4391 32 | ALU,5.9103,0.7289,0.6128 33 | ARU,6.6122,3.0218,0.6083 34 | ENU,7.0706,27.7969,-0.1205 35 | ESU,9.2243,27.1504,-0.1149 36 | ESO,noLoS,noLoS,noLoS 37 | ENO,noLoS,noLoS,noLoS 38 | ISO,12.3296,37.3360,2.8534 39 | ISU,12.3296,37.3413,-0.1208 40 | INU,10.1697,38.0061,-0.1199 41 | INO,noLoS,noLoS,noLoS 42 | Ant-N-LO,-20.9116,7.6933,1.6197 43 | Ant-N-RO,-21.0472,7.2643,1.6168 44 | Ant-N-RU,-21.0568,7.2694,1.1678 45 | Ant-N-LU,-20.9140,7.6961,1.1691 46 | Ant-S-LO,24.3864,-6.6091,1.6453 47 | Ant-S-RO,24.4924,-6.1708,1.6447 48 | Ant-S-RU,24.4922,-6.1717,1.1936 49 | Ant-S-LU,24.3893,-6.6101,1.1952 50 | -------------------------------------------------------------------------------- /data/reftx-offsets-dichasus-dc01.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpo": [ 3 | -0.0, 4 | 0.3331394233091999, 5 | -2.4454171588797595, 6 | -2.0224946979237988, 7 | 0.14171067172057322, 8 | 0.7094055284946256, 9 | -0.1791256810448573, 10 | 1.3619416550867731, 11 | -0.7076597428125532, 12 | 0.11417145697771933, 13 | 2.293734547274741, 14 | -1.4670644022413715, 15 | 0.2388545843948204, 16 | 0.10569186846470123, 17 | -0.4561771223543705, 18 | 0.7121822415283641, 19 | -1.3795209697780249, 20 | -0.12547499947164664, 21 | 2.3017855804762277, 22 | 0.9772580818408783, 23 | 1.0824429217576814, 24 | -2.187491502896716, 25 | -0.9277654503738869, 26 | 1.136357646496524, 27 | -0.3279503614146123, 28 | 0.20893990736467488, 29 | 0.7886912114241672, 30 | -0.3412669633969655, 31 | -2.201327328149536, 32 | 2.7326731906862185, 33 | -0.518812029665646, 34 | 0.6849034659095132, 35 | -0.6292338357307788, 36 | -2.8615385699707656, 37 | 0.7918894918921295, 38 | 0.6388813349477308, 39 | -0.8692835916282857, 40 | -0.9938333491887602, 41 | -3.0189467411190325, 42 | -1.3784978061844375, 43 | -1.877050681975935, 44 | 0.4069985674916668, 45 | -0.6408495719249361, 46 | -2.071594253726909, 47 | -1.7259063610786693, 48 | 0.42622983933144903, 49 | -1.345568017639514, 50 | -0.43549924294754855, 51 | 0.0615750921313629, 52 | 0.5019389139445477, 53 | -1.7511503427243706, 54 | -2.7735290681682327, 55 | -1.9584994185891746, 56 | -0.9047090863700054, 57 | -1.057894300803271, 58 | -0.6135103938282511, 59 | 0.42410111858310134, 60 | 1.2461011789182668, 61 | 1.58100496139218, 62 | -0.13912545278485589, 63 | -1.5630415771050437, 64 | -0.2920542433669189, 65 | -1.3835789830574856, 66 | 0.6742349239798474 67 | ], 68 | "sto": [ 69 | -0.0, 70 | -0.28523391645920254, 71 | 0.1440650053300625, 72 | -1.248190995761548, 73 | -0.2054600044345894, 74 | 0.2306875304247321, 75 | 0.17683044015667582, 76 | 1.9438232740145347, 77 | -0.43062740466385174, 78 | 1.5413219137913334, 79 | -2.4802804310132207, 80 | -0.9499756606790216, 81 | -3.5302550836012725, 82 | 1.1085108429799377, 83 | -0.8726905802308101, 84 | -0.16738946572251073, 85 | 0.4334445126529146, 86 | -0.05742268545468362, 87 | 0.5128780932032863, 88 | 1.1766405915253781, 89 | -0.0789271834049503, 90 | -1.8053944985720292, 91 | 0.6239790669585272, 92 | 0.35836835432261005, 93 | -0.4869398497375366, 94 | -0.3464748478239491, 95 | 0.6863367257181966, 96 | -0.24373645758684492, 97 | -0.2669764468026261, 98 | -0.06993971066100464, 99 | -0.5321204360036886, 100 | -0.33843707131598016, 101 | 1.1605625946145521, 102 | -0.4271203624183642, 103 | 1.2516026277673022, 104 | -0.1219238415447161, 105 | 0.9587057272984795, 106 | 0.7769653594707054, 107 | -1.7828929550411614, 108 | -0.7557092572378641, 109 | -1.9632352084988451, 110 | -0.09022047729122969, 111 | 0.8824128827737194, 112 | -0.5836754833010636, 113 | -1.810422416176927, 114 | 1.1212559213503455, 115 | 0.7324674862241918, 116 | -0.4428963098512558, 117 | -0.6632641753790769, 118 | -0.2497190438576614, 119 | 0.08756775501627961, 120 | -1.9094978381641292, 121 | -0.020184588875934984, 122 | -0.5507590090426809, 123 | -0.37664236655088545, 124 | 0.8077796003953458, 125 | 0.10301255479602145, 126 | 0.17489349236470758, 127 | -0.8298708738302564, 128 | -0.4294345787589101, 129 | -2.196240001973107, 130 | -0.37814953377998156, 131 | -0.058046610854393166, 132 | 0.2292313050486255 133 | ] 134 | } -------------------------------------------------------------------------------- /data/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier" : "dichasus-dcxx", 3 | "name" : "Institute hallway with known 3D model, distributed antennas", 4 | "shortDescription" : "Two separate antenna arrays with 64 antennas in total observe a transmitter in a hallway with known 3D model, ideal for raytracing experiments.", 5 | "bandwidth" : 50e6, 6 | "frequency" : 3438000000, 7 | "outdoor" : false, 8 | "year" : 2023, 9 | "warnings" : [ 10 | "This dataset is currently still being processed.", 11 | "A 3D model of the hallway will be made available alongside the dataset." 12 | ], 13 | "antennas" : [ 14 | { 15 | "name" : "North wing antenna array", 16 | "type" : "array", 17 | "spacingX" : 0.04364, 18 | "spacingY" : 0.04364, 19 | "assignments" : [ 20 | [58, 12, 24, 29, 16, 2, 33, 38], 21 | [57, 7, 45, 18, 42, 52, 28, 21], 22 | [63, 13, 55, 37, 46, 50, 43, 51], 23 | [34, 9, 36, 32, 22, 62, 39, 10] 24 | ], 25 | "location" : [7.480775, -20.9824, 1.39335], 26 | "direction" : [-0.30928047, 0.9508464, -0.01538569] 27 | }, 28 | { 29 | "name" : "South wing antenna array", 30 | "type" : "array", 31 | "spacingX" : 0.04364, 32 | "spacingY" : 0.04364, 33 | "assignments" : [ 34 | [60, 54, 6, 17, 49, 20, 5, 19], 35 | [40, 8, 0, 27, 41, 25, 23, 56], 36 | [44, 53, 61, 59, 48, 35, 1, 26], 37 | [3, 11, 47, 30, 14, 4, 15, 31] 38 | ], 39 | "location" : [-6.390425, 24.440075, 1.4197], 40 | "direction" : [0.23178338, -0.97276149, -0.00339935] 41 | } 42 | ], 43 | "format" : { 44 | "csi" : { 45 | "description" : "Channel coefficients for all antennas, over all subcarriers, real and imaginary parts", 46 | "type" : "tensor" 47 | }, 48 | "snr" : { 49 | "description" : "Signal-to-Noise ratio estimates for all antennas", 50 | "type" : "tensor" 51 | }, 52 | "pos-tachy" : { 53 | "description" : "Position of transmitter determined by a tachymeter pointed at a prism mounted on top of the antenna, in meters (X / Y / Z coordinates)", 54 | "type" : "tensor64" 55 | }, 56 | "gt-interp-age-tachy" : { 57 | "description" : "Time in seconds to closest known tachymeter position. Indicates quality of linear interpolation.", 58 | "type" : "float32" 59 | }, 60 | "time" : { 61 | "description" : "Timestamp since start of measurement campaign, in seconds", 62 | "type" : "float32" 63 | }, 64 | "cfo" : { 65 | "description" : "Measured carrier frequency offset between MOBTX and each receive antenna.", 66 | "type" : "tensor" 67 | } 68 | }, 69 | "tfrecords" : [ 70 | { 71 | "filename" : "tfrecords/dichasus-dc40.tfrecords", 72 | "description" : "Robot follows a pseudorandom trajectory in the central \"lobby\" area of the hallway, partly LoS and partly NLoS. No obstacles in the hallway. Gain of all receivers is 66dB." 73 | }, 74 | { 75 | "filename" : "tfrecords/dichasus-dc41.tfrecords", 76 | "description" : "Robot follows the \"T-Rex\" trajectory in the central \"lobby\" area of the hallway, partly LoS and partly NLoS. No obstacles in the hallway. Gain of all receivers is 66dB." 77 | } 78 | ], 79 | "photos" : [ 80 | { 81 | "filename" : "photos/northwing-array-hallway-obstacle.jpg", 82 | "description" : "Close-up photo of MOBTX robot in front of west wing." 83 | }, 84 | { 85 | "filename" : "photos/empty-hallway-from-northwing.jpg", 86 | "description" : "Empty hallway with tachymeter, seen from the north wing antenna array. Of course, no people were present in the hallway during measurement." 87 | }, 88 | { 89 | "filename" : "photos/empty-hallway-from-southwing.jpg", 90 | "description" : "Empty hallway with tachymeter, seen from the perspective of the south wing antenna array." 91 | }, 92 | { 93 | "filename" : "photos/northwing-array.jpg", 94 | "description" : "Frontal view of north wing antenna array." 95 | }, 96 | { 97 | "filename" : "photos/northwing-array-hallway-obstacle.jpg", 98 | "description" : "Hallway with obstacle in north-west alignment seen from behind the north wing antenna array." 99 | }, 100 | { 101 | "filename" : "photos/obstacle-towards-northwest.jpg", 102 | "description" : "Obstacle in north-west alignment configuration." 103 | }, 104 | { 105 | "filename" : "photos/obstacle-towards-southwest.jpg", 106 | "description" : "Obstacle in south-west alignment configuration." 107 | }, 108 | { 109 | "filename" : "photos/robot-lobby.jpg", 110 | "description" : "MOBTX robot in west wing, with parts of the lobby area on the left." 111 | }, 112 | { 113 | "filename" : "photos/southwing-array.jpg", 114 | "description" : "Frontal view of south wing antenna array." 115 | }, 116 | { 117 | "filename" : "photos/lobby.jpg", 118 | "description" : "Empty \"lobby\" area." 119 | } 120 | ], 121 | "thumbnail" : "photos/thumbnail.jpg" 122 | } 123 | -------------------------------------------------------------------------------- /data/synthetic_positions.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/data/synthetic_positions.npy -------------------------------------------------------------------------------- /data/tfrecords/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/data/tfrecords/.gitkeep -------------------------------------------------------------------------------- /data/traced_paths/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/data/traced_paths/.gitkeep -------------------------------------------------------------------------------- /notebooks/ITU_Materials.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "a6de4075", 6 | "metadata": {}, 7 | "source": [ 8 | "# Experiments with Measured Data - ITU Materials\n", 9 | "\n", 10 | "This notebooks fits the power scaling for the \"ITU Materials\" baseline of Section IV-C of the paper [\"Learning Radio Environments by\n", 11 | "Differentiable Ray Tracing\"](https://arxiv.org/abs/2311.18558) by J. Hoydis, F. Ait Aoudia, S. Cammerer, F. Euchner, M. Nimier-David, S. ten Brink, and A. Keller, Dec. 2023.\n", 12 | "\n", 13 | "The code is made available under the [NVIDIA License](https://github.com/NVlabs/diff-rt-calibration/blob/main/LICENSE.txt).\n", 14 | "\n", 15 | "To run this notebook, you need first to:\n", 16 | "\n", 17 | "- Download the \"dichasus-dc01.tfrecords\" file from the [DICHASUS website](https://dichasus.inue.uni-stuttgart.de/datasets/data/dichasus-dcxx/) to the folder `data/tfrecords` within the cloned repository. More information about the DICHASUS channel sounder can be found [here](https://arxiv.org/abs/2206.15302).\n", 18 | "\n", 19 | "- Create a dataset of traced paths using the script [gen_dataset.py](../code/gen_dataset.py). For this purpose, ensure that you are in the `code/` folder, and run:\n", 20 | "```bash\n", 21 | "python gen_dataset.py -traced_paths_dataset dichasus-dc01 -traced_paths_dataset_size 10000\n", 22 | "```\n", 23 | "This script stores the generated dataset in the `data/traced_paths/` folder.\n", 24 | "Generating the dataset of traced paths can take a while." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "6fe098cc", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import os\n", 35 | "gpu_num = 0 # Use \"\" to use the CPU\n", 36 | "os.environ[\"CUDA_VISIBLE_DEVICES\"] = f\"{gpu_num}\"\n", 37 | "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n", 38 | "import tensorflow as tf\n", 39 | "gpus = tf.config.list_physical_devices('GPU')\n", 40 | "if gpus:\n", 41 | " try:\n", 42 | " tf.config.experimental.set_memory_growth(gpus[0], True)\n", 43 | " except RuntimeError as e:\n", 44 | " print(e)\n", 45 | "tf.get_logger().setLevel('ERROR')\n", 46 | "\n", 47 | "%matplotlib widget\n", 48 | "import matplotlib.pyplot as plt\n", 49 | "import numpy as np\n", 50 | "import sys\n", 51 | "\n", 52 | "sys.path.append('../code')\n", 53 | "\n", 54 | "import sionna\n", 55 | "from utils import *\n", 56 | "import datetime # For logging\n", 57 | "import pickle\n" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "97e27d51", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "training_name = \"itu_materials\"\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "c89ed399", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# Dataset\n", 78 | "dataset_name = '../data/traced_paths/dichasus-dc01'\n", 79 | "dataset_filename = os.path.join(dataset_name + '.tfrecords')\n", 80 | "params_filename = os.path.join(dataset_name + '.json')\n", 81 | "\n", 82 | "# Configure training parameters and step\n", 83 | "batch_size = 8\n", 84 | "num_iterations = 10000\n", 85 | "delta = 0.999 # Parameter for exponential moving average\n", 86 | "\n", 87 | "# Size of validation set size\n", 88 | "# The validation set is used for early stopping, to ensure\n", 89 | "# training does not overfit.\n", 90 | "validation_set_size = 100\n", 91 | "# We don't use the test set here, but need is size for splitting\n", 92 | "test_set_size = 4900\n", 93 | "\n", 94 | "# Sizes of the training set to evaluate\n", 95 | "training_set_size = 5000\n" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": null, 101 | "id": "c8285052", 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "with open(params_filename, 'r') as openfile:\n", 106 | " params = json.load(openfile)\n", 107 | "\n", 108 | "# Scene\n", 109 | "scene_name = params['scene_name']\n", 110 | "# Size of the dataset\n", 111 | "dataset_size = params['traced_paths_dataset_size']\n", 112 | "\n", 113 | "num_subcarriers = 1024\n", 114 | "bandwidth = 50e6\n", 115 | "frequencies = subcarrier_frequencies(num_subcarriers, bandwidth/num_subcarriers)\n" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "id": "e5fd3faa", 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "# Load the TF records as a dataset\n", 126 | "dataset = tf.data.TFRecordDataset([dataset_filename]).map(deserialize_paths_as_tensor_dicts)\n", 127 | "\n", 128 | "# Split the dataset\n", 129 | "# We don't use the test set\n", 130 | "training_set, validation_set, _ = split_dataset(dataset, dataset_size, training_set_size, validation_set_size, test_set_size)\n" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "5656b95b", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "def train():\n", 141 | "\n", 142 | " # Training set\n", 143 | " training_set_iter = iter(training_set.shuffle(256, seed=42).batch(batch_size).repeat(-1))\n", 144 | "\n", 145 | " # Validation set\n", 146 | " validation_set_iter = iter(validation_set.batch(batch_size).repeat(-1))\n", 147 | " num_validation_iter = validation_set_size // batch_size\n", 148 | "\n", 149 | " # Load the scene\n", 150 | " scene = init_scene(scene_name, use_tx_array=True)\n", 151 | "\n", 152 | " # Place the transmitters\n", 153 | " place_transmitter_arrays(scene, [1,2])\n", 154 | "\n", 155 | " # Instantitate receivers\n", 156 | " instantiate_receivers(scene, batch_size)\n", 157 | "\n", 158 | " scaling_factor = tf.Variable(6e-9, dtype=tf.float32, trainable=False)\n", 159 | "\n", 160 | " # Setting up tensorboard\n", 161 | " current_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n", 162 | " train_log_dir = os.path.join('../tb_logs/', training_name, current_time)\n", 163 | " train_summary_writer = tf.summary.create_file_writer(train_log_dir)\n", 164 | " # Checkpoint\n", 165 | " weights_filename = os.path.join('../checkpoints/', training_name)\n", 166 | "\n", 167 | " @tf.function\n", 168 | " def training_step(rx_pos, h_meas, traced_paths):\n", 169 | "\n", 170 | " # Placer receiver\n", 171 | " set_receiver_positions(scene, rx_pos)\n", 172 | "\n", 173 | " # Build traced paths\n", 174 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 175 | "\n", 176 | "\n", 177 | " # Compute paths fields\n", 178 | " paths = scene.compute_fields(*traced_paths,\n", 179 | " scat_random_phases=False,\n", 180 | " check_scene=False)\n", 181 | "\n", 182 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 183 | "\n", 184 | " # Compute channel frequency response\n", 185 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 186 | "\n", 187 | " # Remove useless dimensions\n", 188 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 189 | "\n", 190 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 191 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 192 | "\n", 193 | " # Compute scaling factor\n", 194 | " scaling_factor.assign(delta*scaling_factor + (1-delta)*mse_power_scaling_factor(h_rt, h_meas))\n", 195 | "\n", 196 | " # Scale measurements\n", 197 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 198 | "\n", 199 | " # Compute losses\n", 200 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 201 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 202 | " # Compute the average of a power and delay spread loss\n", 203 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 204 | " loss_pow = power_loss(h_rt, h_meas)\n", 205 | " loss_ds_pow = loss_ds + loss_pow\n", 206 | "\n", 207 | " return loss_ds_pow, loss_ds, loss_pow, scaling_factor\n", 208 | "\n", 209 | " @tf.function\n", 210 | " def evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor):\n", 211 | "\n", 212 | " # Placer receiver\n", 213 | " set_receiver_positions(scene, rx_pos)\n", 214 | "\n", 215 | " # Build traced paths\n", 216 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 217 | "\n", 218 | " paths = scene.compute_fields(*traced_paths,\n", 219 | " scat_random_phases=False,\n", 220 | " check_scene=False)\n", 221 | "\n", 222 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 223 | "\n", 224 | " # Compute channel frequency response\n", 225 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 226 | "\n", 227 | " # Remove useless dimensions\n", 228 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 229 | "\n", 230 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 231 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 232 | "\n", 233 | " # Scale measurements\n", 234 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 235 | "\n", 236 | " # Compute losses\n", 237 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 238 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 239 | " # Compute the average of a power and delay spread loss\n", 240 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 241 | " loss_pow = power_loss(h_rt, h_meas)\n", 242 | "\n", 243 | " return loss_ds, loss_pow\n", 244 | "\n", 245 | " for step in range(num_iterations):\n", 246 | "\n", 247 | " # Next set of traced paths\n", 248 | " next_item = next(training_set_iter, None)\n", 249 | "\n", 250 | " # Retreive the receiver position separately\n", 251 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 252 | " # Skip iteration if does not match the batch size\n", 253 | " if rx_pos.shape[0] != batch_size:\n", 254 | " continue\n", 255 | "\n", 256 | " # Batchify\n", 257 | " traced_paths = batchify(traced_paths)\n", 258 | "\n", 259 | " # Train\n", 260 | " tr_quantities = training_step(rx_pos, h_meas, traced_paths)\n", 261 | " loss_ds_pow, loss_ds, loss_pow, scaling_factor = tr_quantities\n", 262 | "\n", 263 | " # Logging\n", 264 | " if (step % 100) == 0:\n", 265 | " with train_summary_writer.as_default():\n", 266 | " # Log in TB\n", 267 | " tf.summary.scalar('loss_ds_pow_training', loss_ds_pow.numpy(), step=step)\n", 268 | " tf.summary.scalar('loss_ds_training', loss_ds.numpy(), step=step)\n", 269 | " tf.summary.scalar('loss_pow_training', loss_pow.numpy(), step=step)\n", 270 | " tf.summary.scalar('scaling_factor', scaling_factor.numpy(), step=step)\n", 271 | " # Save model\n", 272 | " with open(weights_filename, 'wb') as f:\n", 273 | " pickle.dump(scaling_factor, f)\n", 274 | "\n", 275 | " # Evaluate periodically on the evaluation set\n", 276 | " if ((step+1) % 1000) == 0:\n", 277 | " eval_loss_ds = 0.0\n", 278 | " eval_loss_pow = 0.0\n", 279 | " for _ in range(num_validation_iter):\n", 280 | " # Next set of traced paths\n", 281 | " next_item = next(validation_set_iter, None)\n", 282 | "\n", 283 | " # Retreive the receiver position separately\n", 284 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 285 | " # Skip iteration if does not match the batch size\n", 286 | " if rx_pos.shape[0] != batch_size:\n", 287 | " continue\n", 288 | "\n", 289 | " # Batchify\n", 290 | " traced_paths = batchify(traced_paths)\n", 291 | "\n", 292 | " # Train\n", 293 | " eval_quantities = evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor)\n", 294 | " loss_ds, loss_pow = eval_quantities\n", 295 | " eval_loss_ds += loss_ds\n", 296 | " eval_loss_pow += loss_pow\n", 297 | " eval_loss_ds /= float(num_validation_iter)\n", 298 | " eval_loss_pow /= float(num_validation_iter)\n", 299 | " # Log in TB\n", 300 | " with train_summary_writer.as_default():\n", 301 | " tf.summary.scalar('loss_ds_evaluation', eval_loss_ds, step=step)\n", 302 | " tf.summary.scalar('loss_pow_evaluation', eval_loss_pow, step=step)\n", 303 | "\n", 304 | " # Save model\n", 305 | " with open(weights_filename, 'wb') as f:\n", 306 | " pickle.dump(scaling_factor, f)\n" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "id": "36cf9d0a", 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "train()\n" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "id": "e92ac65f", 323 | "metadata": {}, 324 | "outputs": [], 325 | "source": [] 326 | } 327 | ], 328 | "metadata": { 329 | "kernelspec": { 330 | "display_name": "Python 3 (ipykernel)", 331 | "language": "python", 332 | "name": "python3" 333 | }, 334 | "language_info": { 335 | "codemirror_mode": { 336 | "name": "ipython", 337 | "version": 3 338 | }, 339 | "file_extension": ".py", 340 | "mimetype": "text/x-python", 341 | "name": "python", 342 | "nbconvert_exporter": "python", 343 | "pygments_lexer": "ipython3", 344 | "version": "3.10.12" 345 | } 346 | }, 347 | "nbformat": 4, 348 | "nbformat_minor": 5 349 | } 350 | -------------------------------------------------------------------------------- /notebooks/Learned_Materials.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "acdc34f1", 6 | "metadata": {}, 7 | "source": [ 8 | "# Experiments with Measured Data - Learned Materials\n", 9 | "\n", 10 | "This notebooks trains the \"Learned Materials\" of Section IV-C of the paper [\"Learning Radio Environments by\n", 11 | "Differentiable Ray Tracing\"](https://arxiv.org/abs/2311.18558) by J. Hoydis, F. Ait Aoudia, S. Cammerer, F. Euchner, M. Nimier-David, S. ten Brink, and A. Keller, Dec. 2023.\n", 12 | "\n", 13 | "The code is made available under the [NVIDIA License](https://github.com/NVlabs/diff-rt-calibration/blob/main/LICENSE.txt).\n", 14 | "\n", 15 | "To run this notebook, you need first to:\n", 16 | "\n", 17 | "- Download the \"dichasus-dc01.tfrecords\" file from the [DICHASUS website](https://dichasus.inue.uni-stuttgart.de/datasets/data/dichasus-dcxx/) to the folder `data/tfrecords` within the cloned repository. More information about the DICHASUS channel sounder can be found [here](https://arxiv.org/abs/2206.15302).\n", 18 | "\n", 19 | "- Create a dataset of traced paths using the script [gen_dataset.py](../code/gen_dataset.py). For this purpose, ensure that you are in the `code/` folder, and run:\n", 20 | "```bash\n", 21 | "python gen_dataset.py -traced_paths_dataset dichasus-dc01 -traced_paths_dataset_size 10000\n", 22 | "```\n", 23 | "This script stores the generated dataset in the `data/traced_paths/` folder.\n", 24 | "Generating the dataset of traced paths can take a while." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "c77eb2ba", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import os\n", 35 | "gpu_num = 0 # Use \"\" to use the CPU\n", 36 | "os.environ[\"CUDA_VISIBLE_DEVICES\"] = f\"{gpu_num}\"\n", 37 | "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n", 38 | "import tensorflow as tf\n", 39 | "gpus = tf.config.list_physical_devices('GPU')\n", 40 | "if gpus:\n", 41 | " try:\n", 42 | " tf.config.experimental.set_memory_growth(gpus[0], True)\n", 43 | " except RuntimeError as e:\n", 44 | " print(e)\n", 45 | "tf.get_logger().setLevel('ERROR')\n", 46 | "\n", 47 | "%matplotlib widget\n", 48 | "import matplotlib.pyplot as plt\n", 49 | "import numpy as np\n", 50 | "import sys\n", 51 | "\n", 52 | "sys.path.append('../code')\n", 53 | "\n", 54 | "import sionna\n", 55 | "from utils import *\n", 56 | "import datetime # For logging\n", 57 | "from trainable_materials import TrainableMaterials\n" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "675c16a5", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "training_name = \"learned_materials\"\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "b0292ca4", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# Dataset\n", 78 | "dataset_name = '../data/traced_paths/dichasus-dc01'\n", 79 | "dataset_filename = os.path.join(dataset_name + '.tfrecords')\n", 80 | "params_filename = os.path.join(dataset_name + '.json')\n", 81 | "\n", 82 | "# Configure training parameters and step\n", 83 | "batch_size = 8\n", 84 | "learning_rate = 1e-3\n", 85 | "num_iterations = 10000\n", 86 | "delta = 0.999 # Parameter for exponential moving average\n", 87 | "\n", 88 | "# Size of validation set size\n", 89 | "# The validation set is used for early stopping, to ensure\n", 90 | "# training does not overfit.\n", 91 | "validation_set_size = 100\n", 92 | "# We don't use the test set here, but need is size for splitting\n", 93 | "test_set_size = 4900\n", 94 | "\n", 95 | "# Sizes of the training set to evaluate\n", 96 | "training_set_size = 5000\n" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "c0b5925f", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "with open(params_filename, 'r') as openfile:\n", 107 | " params = json.load(openfile)\n", 108 | "\n", 109 | "# Scene\n", 110 | "scene_name = params['scene_name']\n", 111 | "# Size of the dataset\n", 112 | "dataset_size = params['traced_paths_dataset_size']\n", 113 | "\n", 114 | "num_subcarriers = 1024\n", 115 | "bandwidth = 50e6\n", 116 | "frequencies = subcarrier_frequencies(num_subcarriers, bandwidth/num_subcarriers)\n" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "id": "a6f109ba", 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "# Load the TF records as a dataset\n", 127 | "dataset = tf.data.TFRecordDataset([dataset_filename]).map(deserialize_paths_as_tensor_dicts)\n", 128 | "\n", 129 | "# Split the dataset\n", 130 | "# We don't use the test set\n", 131 | "training_set, validation_set, _ = split_dataset(dataset, dataset_size, training_set_size, validation_set_size, test_set_size)\n" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "id": "e9dcbb4b", 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "def train():\n", 142 | "\n", 143 | " # Training set\n", 144 | " training_set_iter = iter(training_set.shuffle(256, seed=42).batch(batch_size).repeat(-1))\n", 145 | "\n", 146 | " # Validation set\n", 147 | " validation_set_iter = iter(validation_set.batch(batch_size).repeat(-1))\n", 148 | " num_validation_iter = validation_set_size // batch_size\n", 149 | "\n", 150 | " # Load the scene\n", 151 | " scene = init_scene(scene_name, use_tx_array=True)\n", 152 | " scene.radio_material_callable = TrainableMaterials(scene, num_objects=len(scene.objects), embedding_size=30, learn_scattering=False)\n", 153 | "\n", 154 | " # Place the transmitters\n", 155 | " place_transmitter_arrays(scene, [1,2])\n", 156 | "\n", 157 | " # Instantitate receivers\n", 158 | " instantiate_receivers(scene, batch_size)\n", 159 | "\n", 160 | " optimizer = tf.keras.optimizers.Adam(learning_rate)\n", 161 | "\n", 162 | " scaling_factor = tf.Variable(6e-9, dtype=tf.float32, trainable=False)\n", 163 | "\n", 164 | " # Setting up tensorboard\n", 165 | " current_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n", 166 | " train_log_dir = os.path.join('../tb_logs/', training_name, current_time)\n", 167 | " train_summary_writer = tf.summary.create_file_writer(train_log_dir)\n", 168 | " # Checkpoint\n", 169 | " weights_filename = os.path.join('../checkpoints/', training_name)\n", 170 | "\n", 171 | " @tf.function\n", 172 | " def training_step(rx_pos, h_meas, traced_paths):\n", 173 | "\n", 174 | " # Placer receiver\n", 175 | " set_receiver_positions(scene, rx_pos)\n", 176 | "\n", 177 | " # Build traced paths\n", 178 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 179 | "\n", 180 | " with tf.GradientTape() as tape:\n", 181 | "\n", 182 | " # Compute paths fields\n", 183 | " paths = scene.compute_fields(*traced_paths,\n", 184 | " scat_random_phases=False,\n", 185 | " check_scene=False)\n", 186 | "\n", 187 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 188 | "\n", 189 | " # Compute channel frequency response\n", 190 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 191 | "\n", 192 | " # Remove useless dimensions\n", 193 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 194 | "\n", 195 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 196 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 197 | "\n", 198 | " # Compute scaling factor\n", 199 | " scaling_factor.assign(delta*scaling_factor + (1-delta)*mse_power_scaling_factor(h_rt, h_meas))\n", 200 | "\n", 201 | " # Scale measurements\n", 202 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 203 | "\n", 204 | " # Compute losses\n", 205 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 206 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 207 | " # Compute the average of a power and delay spread loss\n", 208 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 209 | " loss_pow = power_loss(h_rt, h_meas)\n", 210 | " loss_ds_pow = loss_ds + loss_pow\n", 211 | "\n", 212 | " # Use loss_ds_pow for training\n", 213 | " grads = tape.gradient(loss_ds_pow, tape.watched_variables(), unconnected_gradients=tf.UnconnectedGradients.ZERO)\n", 214 | " optimizer.apply_gradients(zip(grads, tape.watched_variables()))\n", 215 | "\n", 216 | " return loss_ds_pow, loss_ds, loss_pow, scaling_factor\n", 217 | "\n", 218 | " @tf.function\n", 219 | " def evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor):\n", 220 | "\n", 221 | " # Placer receiver\n", 222 | " set_receiver_positions(scene, rx_pos)\n", 223 | "\n", 224 | " # Build traced paths\n", 225 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 226 | "\n", 227 | " paths = scene.compute_fields(*traced_paths,\n", 228 | " scat_random_phases=False,\n", 229 | " check_scene=False)\n", 230 | "\n", 231 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 232 | "\n", 233 | " # Compute channel frequency response\n", 234 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 235 | "\n", 236 | " # Remove useless dimensions\n", 237 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 238 | "\n", 239 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 240 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 241 | "\n", 242 | " # Scale measurements\n", 243 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 244 | "\n", 245 | " # Compute losses\n", 246 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 247 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 248 | " # Compute the average of a power and delay spread loss\n", 249 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 250 | " loss_pow = power_loss(h_rt, h_meas)\n", 251 | "\n", 252 | " return loss_ds, loss_pow\n", 253 | "\n", 254 | " for step in range(num_iterations):\n", 255 | "\n", 256 | " # Next set of traced paths\n", 257 | " next_item = next(training_set_iter, None)\n", 258 | "\n", 259 | " # Retreive the receiver position separately\n", 260 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 261 | " # Skip iteration if does not match the batch size\n", 262 | " if rx_pos.shape[0] != batch_size:\n", 263 | " continue\n", 264 | "\n", 265 | " # Batchify\n", 266 | " traced_paths = batchify(traced_paths)\n", 267 | "\n", 268 | " # Train\n", 269 | " tr_quantities = training_step(rx_pos, h_meas, traced_paths)\n", 270 | " loss_ds_pow, loss_ds, loss_pow, scaling_factor = tr_quantities\n", 271 | "\n", 272 | " # Logging\n", 273 | " if (step % 100) == 0:\n", 274 | " with train_summary_writer.as_default():\n", 275 | " # Log in TB\n", 276 | " tf.summary.scalar('loss_ds_pow_training', loss_ds_pow.numpy(), step=step)\n", 277 | " tf.summary.scalar('loss_ds_training', loss_ds.numpy(), step=step)\n", 278 | " tf.summary.scalar('loss_pow_training', loss_pow.numpy(), step=step)\n", 279 | " tf.summary.scalar('scaling_factor', scaling_factor.numpy(), step=step)\n", 280 | " # Save model\n", 281 | " save_model(scene.radio_material_callable, weights_filename, scaling_factor=scaling_factor.numpy())\n", 282 | "\n", 283 | " # Evaluate periodically on the evaluation set\n", 284 | " if ((step+1) % 1000) == 0:\n", 285 | " eval_loss_ds = 0.0\n", 286 | " eval_loss_pow = 0.0\n", 287 | " for _ in range(num_validation_iter):\n", 288 | " # Next set of traced paths\n", 289 | " next_item = next(validation_set_iter, None)\n", 290 | "\n", 291 | " # Retreive the receiver position separately\n", 292 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 293 | " # Skip iteration if does not match the batch size\n", 294 | " if rx_pos.shape[0] != batch_size:\n", 295 | " continue\n", 296 | "\n", 297 | " # Batchify\n", 298 | " traced_paths = batchify(traced_paths)\n", 299 | "\n", 300 | " # Train\n", 301 | " eval_quantities = evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor)\n", 302 | " loss_ds, loss_pow = eval_quantities\n", 303 | " eval_loss_ds += loss_ds\n", 304 | " eval_loss_pow += loss_pow\n", 305 | " eval_loss_ds /= float(num_validation_iter)\n", 306 | " eval_loss_pow /= float(num_validation_iter)\n", 307 | " # Log in TB\n", 308 | " with train_summary_writer.as_default():\n", 309 | " tf.summary.scalar('loss_ds_evaluation', eval_loss_ds, step=step)\n", 310 | " tf.summary.scalar('loss_pow_evaluation', eval_loss_pow, step=step)\n", 311 | "\n", 312 | " # Save model\n", 313 | " save_model(scene.radio_material_callable, weights_filename, scaling_factor=scaling_factor.numpy())\n" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": null, 319 | "id": "08e7a8c3", 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "train()\n" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "id": "3d29b733", 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [] 333 | } 334 | ], 335 | "metadata": { 336 | "kernelspec": { 337 | "display_name": "Python 3 (ipykernel)", 338 | "language": "python", 339 | "name": "python3" 340 | }, 341 | "language_info": { 342 | "codemirror_mode": { 343 | "name": "ipython", 344 | "version": 3 345 | }, 346 | "file_extension": ".py", 347 | "mimetype": "text/x-python", 348 | "name": "python", 349 | "nbconvert_exporter": "python", 350 | "pygments_lexer": "ipython3", 351 | "version": "3.10.12" 352 | } 353 | }, 354 | "nbformat": 4, 355 | "nbformat_minor": 5 356 | } 357 | -------------------------------------------------------------------------------- /notebooks/Neural_Materials.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b5193df6", 6 | "metadata": {}, 7 | "source": [ 8 | "# Experiments with Measured Data - Neural Materials\n", 9 | "\n", 10 | "This notebooks trains the \"Neural Materials\" of Section IV-C of the paper [\"Learning Radio Environments by\n", 11 | "Differentiable Ray Tracing\"](https://arxiv.org/abs/2311.18558) by J. Hoydis, F. Ait Aoudia, S. Cammerer, F. Euchner, M. Nimier-David, S. ten Brink, and A. Keller, Dec. 2023.\n", 12 | "\n", 13 | "The code is made available under the [NVIDIA License](https://github.com/NVlabs/diff-rt-calibration/blob/main/LICENSE.txt).\n", 14 | "\n", 15 | "To run this notebook, you need first to:\n", 16 | "\n", 17 | "- Download the \"dichasus-dc01.tfrecords\" file from the [DICHASUS website](https://dichasus.inue.uni-stuttgart.de/datasets/data/dichasus-dcxx/) to the folder `data/tfrecords` within the cloned repository. More information about the DICHASUS channel sounder can be found [here](https://arxiv.org/abs/2206.15302).\n", 18 | "\n", 19 | "- Create a dataset of traced paths using the script [gen_dataset.py](../code/gen_dataset.py). For this purpose, ensure that you are in the `code/` folder, and run:\n", 20 | "```bash\n", 21 | "python gen_dataset.py -traced_paths_dataset dichasus-dc01 -traced_paths_dataset_size 10000\n", 22 | "```\n", 23 | "This script stores the generated dataset in the `data/traced_paths/` folder.\n", 24 | "Generating the dataset of traced paths can take a while." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "437a0d0e", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import os\n", 35 | "gpu_num = 0 # Use \"\" to use the CPU\n", 36 | "os.environ[\"CUDA_VISIBLE_DEVICES\"] = f\"{gpu_num}\"\n", 37 | "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n", 38 | "import tensorflow as tf\n", 39 | "gpus = tf.config.list_physical_devices('GPU')\n", 40 | "if gpus:\n", 41 | " try:\n", 42 | " tf.config.experimental.set_memory_growth(gpus[0], True)\n", 43 | " except RuntimeError as e:\n", 44 | " print(e)\n", 45 | "tf.get_logger().setLevel('ERROR')\n", 46 | "\n", 47 | "%matplotlib widget\n", 48 | "import matplotlib.pyplot as plt\n", 49 | "import numpy as np\n", 50 | "import sys\n", 51 | "\n", 52 | "sys.path.append('../code')\n", 53 | "\n", 54 | "import sionna\n", 55 | "from utils import *\n", 56 | "from neural_materials import NeuralMaterials\n", 57 | "import datetime # For logging\n" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "3ca92e86", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "training_name = \"neural_materials\"\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "275b330f", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# Dataset\n", 78 | "dataset_name = '../data/traced_paths/dichasus-dc01'\n", 79 | "dataset_filename = os.path.join(dataset_name + '.tfrecords')\n", 80 | "params_filename = os.path.join(dataset_name + '.json')\n", 81 | "\n", 82 | "# Configure training parameters and step\n", 83 | "batch_size = 8\n", 84 | "learning_rate = 1e-3\n", 85 | "num_iterations = 10000\n", 86 | "delta = 0.999 # Parameter for exponential moving average\n", 87 | "\n", 88 | "# Size of validation set size\n", 89 | "# The validation set is used for early stopping, to ensure\n", 90 | "# training does not overfit.\n", 91 | "validation_set_size = 100\n", 92 | "# We don't use the test set here, but need is size for splitting\n", 93 | "test_set_size = 4900\n", 94 | "\n", 95 | "# Sizes of the positional encoding to evaluate\n", 96 | "position_encoding_size = 10\n", 97 | "# Sizes of the training set to evaluate\n", 98 | "training_set_size = 5000\n" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "id": "496ce973", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "with open(params_filename, 'r') as openfile:\n", 109 | " params = json.load(openfile)\n", 110 | "\n", 111 | "# Scene\n", 112 | "scene_name = params['scene_name']\n", 113 | "# Size of the dataset\n", 114 | "dataset_size = params['traced_paths_dataset_size']\n", 115 | "\n", 116 | "num_subcarriers = 1024\n", 117 | "bandwidth = 50e6\n", 118 | "frequencies = subcarrier_frequencies(num_subcarriers, bandwidth/num_subcarriers)\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "2ec1f791", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "# Load the TF records as a dataset\n", 129 | "dataset = tf.data.TFRecordDataset([dataset_filename]).map(deserialize_paths_as_tensor_dicts)\n", 130 | "\n", 131 | "# Split the dataset\n", 132 | "# We don't use the test set\n", 133 | "training_set, validation_set, _ = split_dataset(dataset, dataset_size, training_set_size, validation_set_size, test_set_size)\n" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "id": "f0a66f57", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "def train():\n", 144 | "\n", 145 | " # Training set\n", 146 | " training_set_iter = iter(training_set.shuffle(256, seed=42).batch(batch_size).repeat(-1))\n", 147 | "\n", 148 | " # Validation set\n", 149 | " validation_set_iter = iter(validation_set.batch(batch_size).repeat(-1))\n", 150 | " num_validation_iter = validation_set_size // batch_size\n", 151 | "\n", 152 | " # Load the scene\n", 153 | " scene = init_scene(scene_name, use_tx_array=True)\n", 154 | "\n", 155 | " # Place the transmitters\n", 156 | " place_transmitter_arrays(scene, [1,2])\n", 157 | "\n", 158 | " # Instantitate receivers\n", 159 | " instantiate_receivers(scene, batch_size)\n", 160 | "\n", 161 | " optimizer = tf.keras.optimizers.Adam(learning_rate)\n", 162 | "\n", 163 | " scaling_factor = tf.Variable(6e-9, dtype=tf.float32, trainable=False)\n", 164 | "\n", 165 | " # Setting up tensorboard\n", 166 | " current_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n", 167 | " train_log_dir = os.path.join('../tb_logs/', training_name, current_time)\n", 168 | " train_summary_writer = tf.summary.create_file_writer(train_log_dir)\n", 169 | " # Checkpoint\n", 170 | " weights_filename = os.path.join('../checkpoints/', training_name)\n", 171 | "\n", 172 | " scene.radio_material_callable = NeuralMaterials(scene, pos_encoding_size=position_encoding_size, learn_scattering=False)\n", 173 | "\n", 174 | " @tf.function\n", 175 | " def training_step(rx_pos, h_meas, traced_paths):\n", 176 | "\n", 177 | " # Placer receiver\n", 178 | " set_receiver_positions(scene, rx_pos)\n", 179 | "\n", 180 | " # Build traced paths\n", 181 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 182 | "\n", 183 | " with tf.GradientTape() as tape:\n", 184 | " # Compute paths fields\n", 185 | " paths = scene.compute_fields(*traced_paths,\n", 186 | " scat_random_phases=False,\n", 187 | " check_scene=False)\n", 188 | "\n", 189 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 190 | "\n", 191 | " # Compute channel frequency response\n", 192 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 193 | "\n", 194 | " # Remove useless dimensions\n", 195 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 196 | "\n", 197 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 198 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 199 | "\n", 200 | " # Compute scaling factor\n", 201 | " scaling_factor.assign(delta*scaling_factor + (1-delta)*mse_power_scaling_factor(h_rt, h_meas))\n", 202 | "\n", 203 | " # Scale measurements\n", 204 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 205 | "\n", 206 | " # Compute losses\n", 207 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 208 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 209 | " # Compute the average of a power and delay spread loss\n", 210 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 211 | " loss_pow = power_loss(h_rt, h_meas)\n", 212 | " loss_ds_pow = loss_ds + loss_pow\n", 213 | "\n", 214 | " # Use loss_ds_pow for training\n", 215 | " grads = tape.gradient(loss_ds_pow, tape.watched_variables(), unconnected_gradients=tf.UnconnectedGradients.ZERO)\n", 216 | " optimizer.apply_gradients(zip(grads, tape.watched_variables()))\n", 217 | "\n", 218 | " return loss_ds_pow, loss_ds, loss_pow, scaling_factor\n", 219 | "\n", 220 | " @tf.function\n", 221 | " def evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor):\n", 222 | "\n", 223 | " # Placer receiver\n", 224 | " set_receiver_positions(scene, rx_pos)\n", 225 | "\n", 226 | " # Build traced paths\n", 227 | " traced_paths = tensor_dicts_to_traced_paths(scene, traced_paths)\n", 228 | "\n", 229 | " paths = scene.compute_fields(*traced_paths,\n", 230 | " scat_random_phases=False,\n", 231 | " check_scene=False)\n", 232 | "\n", 233 | " a, tau = paths.cir(scattering=False) # Disable scattering\n", 234 | "\n", 235 | " # Compute channel frequency response\n", 236 | " h_rt = cir_to_ofdm_channel(frequencies, a, tau)\n", 237 | "\n", 238 | " # Remove useless dimensions\n", 239 | " h_rt = tf.squeeze(h_rt, axis=[0,2,5])\n", 240 | "\n", 241 | " # Normalize h to make sure that power is independent of the number of subacrriers\n", 242 | " h_rt /= tf.complex(tf.sqrt(tf.cast(num_subcarriers, tf.float32)), 0.)\n", 243 | "\n", 244 | " # Scale measurements\n", 245 | " h_meas *= tf.complex(tf.sqrt(scaling_factor), 0.)\n", 246 | "\n", 247 | " # Compute losses\n", 248 | " h_rt = sionna.utils.flatten_dims(h_rt, 3, 0)\n", 249 | " h_meas = sionna.utils.flatten_dims(h_meas, 3, 0)\n", 250 | " # Compute the average of a power and delay spread loss\n", 251 | " loss_ds = delay_spread_loss(h_rt, h_meas)\n", 252 | " loss_pow = power_loss(h_rt, h_meas)\n", 253 | "\n", 254 | " return loss_ds, loss_pow\n", 255 | "\n", 256 | " for step in range(num_iterations):\n", 257 | "\n", 258 | " # Next set of traced paths\n", 259 | " next_item = next(training_set_iter, None)\n", 260 | "\n", 261 | " # Retreive the receiver position separately\n", 262 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 263 | " # Skip iteration if does not match the batch size\n", 264 | " if rx_pos.shape[0] != batch_size:\n", 265 | " continue\n", 266 | "\n", 267 | " # Batchify\n", 268 | " traced_paths = batchify(traced_paths)\n", 269 | "\n", 270 | " # Train\n", 271 | " tr_quantities = training_step(rx_pos, h_meas, traced_paths)\n", 272 | " loss_ds_pow, loss_ds, loss_pow, scaling_factor = tr_quantities\n", 273 | "\n", 274 | " # Logging\n", 275 | " if (step % 100) == 0:\n", 276 | " with train_summary_writer.as_default():\n", 277 | " # Log in TB\n", 278 | " tf.summary.scalar('loss_ds_pow_training', loss_ds_pow.numpy(), step=step)\n", 279 | " tf.summary.scalar('loss_ds_training', loss_ds.numpy(), step=step)\n", 280 | " tf.summary.scalar('loss_pow_training', loss_pow.numpy(), step=step)\n", 281 | " tf.summary.scalar('scaling_factor', scaling_factor.numpy(), step=step)\n", 282 | " # Save model\n", 283 | " save_model(scene.radio_material_callable, weights_filename, scaling_factor=scaling_factor.numpy())\n", 284 | "\n", 285 | " # Evaluate periodically on the evaluation set\n", 286 | " if ((step+1) % 1000) == 0:\n", 287 | " eval_loss_ds = 0.0\n", 288 | " eval_loss_pow = 0.0\n", 289 | " for _ in range(num_validation_iter):\n", 290 | " # Next set of traced paths\n", 291 | " next_item = next(validation_set_iter, None)\n", 292 | "\n", 293 | " # Retreive the receiver position separately\n", 294 | " rx_pos, h_meas, traced_paths = next_item[0], next_item[1], next_item[2:]\n", 295 | " # Skip iteration if does not match the batch size\n", 296 | " if rx_pos.shape[0] != batch_size:\n", 297 | " continue\n", 298 | "\n", 299 | " # Batchify\n", 300 | " traced_paths = batchify(traced_paths)\n", 301 | "\n", 302 | " # Train\n", 303 | " eval_quantities = evaluation_step(rx_pos, h_meas, traced_paths, scaling_factor)\n", 304 | " loss_ds, loss_pow = eval_quantities\n", 305 | " eval_loss_ds += loss_ds\n", 306 | " eval_loss_pow += loss_pow\n", 307 | " eval_loss_ds /= float(num_validation_iter)\n", 308 | " eval_loss_pow /= float(num_validation_iter)\n", 309 | " # Log in TB\n", 310 | " with train_summary_writer.as_default():\n", 311 | " tf.summary.scalar('loss_ds_evaluation', eval_loss_ds, step=step)\n", 312 | " tf.summary.scalar('loss_pow_evaluation', eval_loss_pow, step=step)\n", 313 | "\n", 314 | "\n", 315 | " # Save model\n", 316 | " save_model(scene.radio_material_callable, weights_filename, scaling_factor=scaling_factor.numpy())\n" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "id": "6f025d97", 323 | "metadata": {}, 324 | "outputs": [], 325 | "source": [ 326 | "train()\n" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": null, 332 | "id": "f18032cd", 333 | "metadata": {}, 334 | "outputs": [], 335 | "source": [] 336 | } 337 | ], 338 | "metadata": { 339 | "kernelspec": { 340 | "display_name": "Python 3 (ipykernel)", 341 | "language": "python", 342 | "name": "python3" 343 | }, 344 | "language_info": { 345 | "codemirror_mode": { 346 | "name": "ipython", 347 | "version": 3 348 | }, 349 | "file_extension": ".py", 350 | "mimetype": "text/x-python", 351 | "name": "python", 352 | "nbconvert_exporter": "python", 353 | "pygments_lexer": "ipython3", 354 | "version": "3.10.12" 355 | } 356 | }, 357 | "nbformat": 4, 358 | "nbformat_minor": 5 359 | } 360 | -------------------------------------------------------------------------------- /results/measurements/cir-1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/cir-1.pdf -------------------------------------------------------------------------------- /results/measurements/cir-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/cir-2.pdf -------------------------------------------------------------------------------- /results/measurements/cir-3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/cir-3.pdf -------------------------------------------------------------------------------- /results/measurements/cir-4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/cir-4.pdf -------------------------------------------------------------------------------- /results/measurements/ds.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/ds.pdf -------------------------------------------------------------------------------- /results/measurements/ds_rae.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/ds_rae.pdf -------------------------------------------------------------------------------- /results/measurements/path_loss_rx1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/path_loss_rx1.pdf -------------------------------------------------------------------------------- /results/measurements/path_loss_rx2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/path_loss_rx2.pdf -------------------------------------------------------------------------------- /results/measurements/power.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/power.pdf -------------------------------------------------------------------------------- /results/measurements/power_ale.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/measurements/power_ale.pdf -------------------------------------------------------------------------------- /results/synthetic/antenna_pattern_h.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/antenna_pattern_h.pdf -------------------------------------------------------------------------------- /results/synthetic/antenna_pattern_v.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/antenna_pattern_v.pdf -------------------------------------------------------------------------------- /results/synthetic/conductivity.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/conductivity.pdf -------------------------------------------------------------------------------- /results/synthetic/relative_permittivity.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/relative_permittivity.pdf -------------------------------------------------------------------------------- /results/synthetic/scattering_coefficient.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/scattering_coefficient.pdf -------------------------------------------------------------------------------- /results/synthetic/scattering_pattern.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/scattering_pattern.pdf -------------------------------------------------------------------------------- /results/synthetic/xpd_coefficient.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/results/synthetic/xpd_coefficient.pdf -------------------------------------------------------------------------------- /scenes/inue_simple/inue_simple.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/scenes/inue_simple/inue_simple.blend -------------------------------------------------------------------------------- /scenes/inue_simple/inue_simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /scenes/inue_simple/meshes/ceiling.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/scenes/inue_simple/meshes/ceiling.ply -------------------------------------------------------------------------------- /scenes/inue_simple/meshes/floor.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/scenes/inue_simple/meshes/floor.ply -------------------------------------------------------------------------------- /scenes/inue_simple/meshes/walls.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/diff-rt-calibration/cc70d813a5ac8cd976c0b43ca811712dd5a1629a/scenes/inue_simple/meshes/walls.ply --------------------------------------------------------------------------------