1#!/usr/bin/env python3 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import time 18from enum import Enum 19 20import numpy as np 21from acts.controllers import cellular_simulator 22 23 24class BaseSimulation(object): 25 """ Base class for cellular connectivity simulations. 26 27 Classes that inherit from this base class implement different simulation 28 setups. The base class contains methods that are common to all simulation 29 configurations. 30 31 """ 32 33 NUM_UL_CAL_READS = 3 34 NUM_DL_CAL_READS = 5 35 MAX_BTS_INPUT_POWER = 30 36 MAX_PHONE_OUTPUT_POWER = 23 37 UL_MIN_POWER = -60.0 38 39 # Keys to obtain settings from the test_config dictionary. 40 KEY_CALIBRATION = "calibration" 41 KEY_ATTACH_RETRIES = "attach_retries" 42 KEY_ATTACH_TIMEOUT = "attach_timeout" 43 44 # Filepath to the config files stored in the Anritsu callbox. Needs to be 45 # formatted to replace {} with either A or B depending on the model. 46 CALLBOX_PATH_FORMAT_STR = 'C:\\Users\\MD8475{}\\Documents\\DAN_configs\\' 47 48 # Time in seconds to wait for the phone to settle 49 # after attaching to the base station. 50 SETTLING_TIME = 10 51 52 # Default time in seconds to wait for the phone to attach to the basestation 53 # after toggling airplane mode. This setting can be changed with the 54 # KEY_ATTACH_TIMEOUT keyword in the test configuration file. 55 DEFAULT_ATTACH_TIMEOUT = 120 56 57 # The default number of attach retries. This setting can be changed with 58 # the KEY_ATTACH_RETRIES keyword in the test configuration file. 59 DEFAULT_ATTACH_RETRIES = 3 60 61 # These two dictionaries allow to map from a string to a signal level and 62 # have to be overriden by the simulations inheriting from this class. 63 UPLINK_SIGNAL_LEVEL_DICTIONARY = {} 64 DOWNLINK_SIGNAL_LEVEL_DICTIONARY = {} 65 66 # Units for downlink signal level. This variable has to be overriden by 67 # the simulations inheriting from this class. 68 DOWNLINK_SIGNAL_LEVEL_UNITS = None 69 70 class BtsConfig: 71 """ Base station configuration class. This class is only a container for 72 base station parameters and should not interact with the instrument 73 controller. 74 75 Atributes: 76 output_power: a float indicating the required signal level at the 77 instrument's output. 78 input_level: a float indicating the required signal level at the 79 instrument's input. 80 """ 81 def __init__(self): 82 """ Initialize the base station config by setting all its 83 parameters to None. """ 84 self.output_power = None 85 self.input_power = None 86 self.band = None 87 88 def incorporate(self, new_config): 89 """ Incorporates a different configuration by replacing the current 90 values with the new ones for all the parameters different to None. 91 """ 92 for attr, value in vars(new_config).items(): 93 if value: 94 setattr(self, attr, value) 95 96 def __init__(self, simulator, log, dut, test_config, calibration_table): 97 """ Initializes the Simulation object. 98 99 Keeps a reference to the callbox, log and dut handlers and 100 initializes the class attributes. 101 102 Args: 103 simulator: a cellular simulator controller 104 log: a logger handle 105 dut: a device handler implementing BaseCellularDut 106 test_config: test configuration obtained from the config file 107 calibration_table: a dictionary containing path losses for 108 different bands. 109 """ 110 111 self.simulator = simulator 112 self.log = log 113 self.dut = dut 114 self.calibration_table = calibration_table 115 116 # Turn calibration on or off depending on the test config value. If the 117 # key is not present, set to False by default 118 if self.KEY_CALIBRATION not in test_config: 119 self.log.warning('The {} key is not set in the testbed ' 120 'parameters. Setting to off by default. To ' 121 'turn calibration on, include the key with ' 122 'a true/false value.'.format( 123 self.KEY_CALIBRATION)) 124 125 self.calibration_required = test_config.get(self.KEY_CALIBRATION, 126 False) 127 128 # Obtain the allowed number of retries from the test configs 129 if self.KEY_ATTACH_RETRIES not in test_config: 130 self.log.warning('The {} key is not set in the testbed ' 131 'parameters. Setting to {} by default.'.format( 132 self.KEY_ATTACH_RETRIES, 133 self.DEFAULT_ATTACH_RETRIES)) 134 135 self.attach_retries = test_config.get(self.KEY_ATTACH_RETRIES, 136 self.DEFAULT_ATTACH_RETRIES) 137 138 # Obtain the attach timeout from the test configs 139 if self.KEY_ATTACH_TIMEOUT not in test_config: 140 self.log.warning('The {} key is not set in the testbed ' 141 'parameters. Setting to {} by default.'.format( 142 self.KEY_ATTACH_TIMEOUT, 143 self.DEFAULT_ATTACH_TIMEOUT)) 144 145 self.attach_timeout = test_config.get(self.KEY_ATTACH_TIMEOUT, 146 self.DEFAULT_ATTACH_TIMEOUT) 147 148 # Configuration object for the primary base station 149 self.primary_config = self.BtsConfig() 150 151 # Store the current calibrated band 152 self.current_calibrated_band = None 153 154 # Path loss measured during calibration 155 self.dl_path_loss = None 156 self.ul_path_loss = None 157 158 # Target signal levels obtained during configuration 159 self.sim_dl_power = None 160 self.sim_ul_power = None 161 162 # Stores RRC status change timer 163 self.rrc_sc_timer = None 164 165 # Set to default APN 166 log.info("Configuring APN.") 167 self.dut.set_apn('test', 'test') 168 169 # Enable roaming on the phone 170 self.dut.toggle_data_roaming(True) 171 172 # Make sure airplane mode is on so the phone won't attach right away 173 self.dut.toggle_airplane_mode(True) 174 175 # Wait for airplane mode setting to propagate 176 time.sleep(2) 177 178 # Prepare the simulator for this simulation setup 179 self.setup_simulator() 180 181 def setup_simulator(self): 182 """ Do initial configuration in the simulator. """ 183 raise NotImplementedError() 184 185 def attach(self): 186 """ Attach the phone to the basestation. 187 188 Sets a good signal level, toggles airplane mode 189 and waits for the phone to attach. 190 191 Returns: 192 True if the phone was able to attach, False if not. 193 """ 194 195 # Turn on airplane mode 196 self.dut.toggle_airplane_mode(True) 197 198 # Wait for airplane mode setting to propagate 199 time.sleep(2) 200 201 # Provide a good signal power for the phone to attach easily 202 new_config = self.BtsConfig() 203 new_config.input_power = -10 204 new_config.output_power = -30 205 self.simulator.configure_bts(new_config) 206 self.primary_config.incorporate(new_config) 207 208 # Try to attach the phone. 209 for i in range(self.attach_retries): 210 211 try: 212 213 # Turn off airplane mode 214 self.dut.toggle_airplane_mode(False) 215 216 # Wait for the phone to attach. 217 self.simulator.wait_until_attached(timeout=self.attach_timeout) 218 219 except cellular_simulator.CellularSimulatorError: 220 221 # The phone failed to attach 222 self.log.info( 223 "UE failed to attach on attempt number {}.".format(i + 1)) 224 225 # Turn airplane mode on to prepare the phone for a retry. 226 self.dut.toggle_airplane_mode(True) 227 228 # Wait for APM to propagate 229 time.sleep(3) 230 231 # Retry 232 if i < self.attach_retries - 1: 233 # Retry 234 continue 235 else: 236 # No more retries left. Return False. 237 return False 238 239 else: 240 # The phone attached successfully. 241 time.sleep(self.SETTLING_TIME) 242 self.log.info("UE attached to the callbox.") 243 break 244 245 return True 246 247 def detach(self): 248 """ Detach the phone from the basestation. 249 250 Turns airplane mode and resets basestation. 251 """ 252 253 # Set the DUT to airplane mode so it doesn't see the 254 # cellular network going off 255 self.dut.toggle_airplane_mode(True) 256 257 # Wait for APM to propagate 258 time.sleep(2) 259 260 # Power off basestation 261 self.simulator.detach() 262 263 def stop(self): 264 """ Detach phone from the basestation by stopping the simulation. 265 266 Stop the simulation and turn airplane mode on. """ 267 268 # Set the DUT to airplane mode so it doesn't see the 269 # cellular network going off 270 self.dut.toggle_airplane_mode(True) 271 272 # Wait for APM to propagate 273 time.sleep(2) 274 275 # Stop the simulation 276 self.simulator.stop() 277 278 def start(self): 279 """ Start the simulation by attaching the phone and setting the 280 required DL and UL power. 281 282 Note that this refers to starting the simulated testing environment 283 and not to starting the signaling on the cellular instruments, 284 which might have been done earlier depending on the cellular 285 instrument controller implementation. """ 286 287 if not self.attach(): 288 raise RuntimeError('Could not attach to base station.') 289 290 # Starts IP traffic while changing this setting to force the UE to be 291 # in Communication state, as UL power cannot be set in Idle state 292 self.start_traffic_for_calibration() 293 294 # Wait until it goes to communication state 295 self.simulator.wait_until_communication_state() 296 297 # Set uplink power to a minimum before going to the actual desired 298 # value. This avoid inconsistencies produced by the hysteresis in the 299 # PA switching points. 300 self.log.info('Setting UL power to -30 dBm before going to the ' 301 'requested value to avoid incosistencies caused by ' 302 'hysteresis.') 303 self.set_uplink_tx_power(-30) 304 305 # Set signal levels obtained from the test parameters 306 self.set_downlink_rx_power(self.sim_dl_power) 307 self.set_uplink_tx_power(self.sim_ul_power) 308 309 # Verify signal level 310 try: 311 rx_power, tx_power = self.dut.get_rx_tx_power_levels() 312 313 if not tx_power or not rx_power[0]: 314 raise RuntimeError('The method return invalid Tx/Rx values.') 315 316 self.log.info('Signal level reported by the DUT in dBm: Tx = {}, ' 317 'Rx = {}.'.format(tx_power, rx_power)) 318 319 if abs(self.sim_ul_power - tx_power) > 1: 320 self.log.warning('Tx power at the UE is off by more than 1 dB') 321 322 except RuntimeError as e: 323 self.log.error('Could not verify Rx / Tx levels: %s.' % e) 324 325 # Stop IP traffic after setting the UL power level 326 self.stop_traffic_for_calibration() 327 328 def parse_parameters(self, parameters): 329 """ Configures simulation using a list of parameters. 330 331 Consumes parameters from a list. 332 Children classes need to call this method first. 333 334 Args: 335 parameters: list of parameters 336 """ 337 338 raise NotImplementedError() 339 340 def consume_parameter(self, parameters, parameter_name, num_values=0): 341 """ Parses a parameter from a list. 342 343 Allows to parse the parameter list. Will delete parameters from the 344 list after consuming them to ensure that they are not used twice. 345 346 Args: 347 parameters: list of parameters 348 parameter_name: keyword to look up in the list 349 num_values: number of arguments following the 350 parameter name in the list 351 Returns: 352 A list containing the parameter name and the following num_values 353 arguments 354 """ 355 356 try: 357 i = parameters.index(parameter_name) 358 except ValueError: 359 # parameter_name is not set 360 return [] 361 362 return_list = [] 363 364 try: 365 for j in range(num_values + 1): 366 return_list.append(parameters.pop(i)) 367 except IndexError: 368 raise ValueError( 369 "Parameter {} has to be followed by {} values.".format( 370 parameter_name, num_values)) 371 372 return return_list 373 374 def set_uplink_tx_power(self, signal_level): 375 """ Configure the uplink tx power level 376 377 Args: 378 signal_level: calibrated tx power in dBm 379 """ 380 new_config = self.BtsConfig() 381 new_config.input_power = self.calibrated_uplink_tx_power( 382 self.primary_config, signal_level) 383 self.simulator.configure_bts(new_config) 384 self.primary_config.incorporate(new_config) 385 386 def set_downlink_rx_power(self, signal_level): 387 """ Configure the downlink rx power level 388 389 Args: 390 signal_level: calibrated rx power in dBm 391 """ 392 new_config = self.BtsConfig() 393 new_config.output_power = self.calibrated_downlink_rx_power( 394 self.primary_config, signal_level) 395 self.simulator.configure_bts(new_config) 396 self.primary_config.incorporate(new_config) 397 398 def get_uplink_power_from_parameters(self, parameters): 399 """ Reads uplink power from a list of parameters. """ 400 401 values = self.consume_parameter(parameters, self.PARAM_UL_PW, 1) 402 403 if values: 404 if values[1] in self.UPLINK_SIGNAL_LEVEL_DICTIONARY: 405 return self.UPLINK_SIGNAL_LEVEL_DICTIONARY[values[1]] 406 else: 407 try: 408 if values[1][0] == 'n': 409 # Treat the 'n' character as a negative sign 410 return -int(values[1][1:]) 411 else: 412 return int(values[1]) 413 except ValueError: 414 pass 415 416 # If the method got to this point it is because PARAM_UL_PW was not 417 # included in the test parameters or the provided value was invalid. 418 raise ValueError( 419 "The test name needs to include parameter {} followed by the " 420 "desired uplink power expressed by an integer number in dBm " 421 "or by one the following values: {}. To indicate negative " 422 "values, use the letter n instead of - sign.".format( 423 self.PARAM_UL_PW, 424 list(self.UPLINK_SIGNAL_LEVEL_DICTIONARY.keys()))) 425 426 def get_downlink_power_from_parameters(self, parameters): 427 """ Reads downlink power from a list of parameters. """ 428 429 values = self.consume_parameter(parameters, self.PARAM_DL_PW, 1) 430 431 if values: 432 if values[1] not in self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY: 433 raise ValueError("Invalid signal level value {}.".format( 434 values[1])) 435 else: 436 return self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY[values[1]] 437 else: 438 # Use default value 439 power = self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY['excellent'] 440 self.log.info("No DL signal level value was indicated in the test " 441 "parameters. Using default value of {} {}.".format( 442 power, self.DOWNLINK_SIGNAL_LEVEL_UNITS)) 443 return power 444 445 def calibrated_downlink_rx_power(self, bts_config, signal_level): 446 """ Calculates the power level at the instrument's output in order to 447 obtain the required rx power level at the DUT's input. 448 449 If calibration values are not available, returns the uncalibrated signal 450 level. 451 452 Args: 453 bts_config: the current configuration at the base station. derived 454 classes implementations can use this object to indicate power as 455 spectral power density or in other units. 456 signal_level: desired downlink received power, can be either a 457 key value pair, an int or a float 458 """ 459 460 # Obtain power value if the provided signal_level is a key value pair 461 if isinstance(signal_level, Enum): 462 power = signal_level.value 463 else: 464 power = signal_level 465 466 # Try to use measured path loss value. If this was not set, it will 467 # throw an TypeError exception 468 try: 469 calibrated_power = round(power + self.dl_path_loss) 470 if calibrated_power > self.simulator.MAX_DL_POWER: 471 self.log.warning( 472 "Cannot achieve phone DL Rx power of {} dBm. Requested TX " 473 "power of {} dBm exceeds callbox limit!".format( 474 power, calibrated_power)) 475 calibrated_power = self.simulator.MAX_DL_POWER 476 self.log.warning( 477 "Setting callbox Tx power to max possible ({} dBm)".format( 478 calibrated_power)) 479 480 self.log.info( 481 "Requested phone DL Rx power of {} dBm, setting callbox Tx " 482 "power at {} dBm".format(power, calibrated_power)) 483 time.sleep(2) 484 # Power has to be a natural number so calibration wont be exact. 485 # Inform the actual received power after rounding. 486 self.log.info( 487 "Phone downlink received power is {0:.2f} dBm".format( 488 calibrated_power - self.dl_path_loss)) 489 return calibrated_power 490 except TypeError: 491 self.log.info("Phone downlink received power set to {} (link is " 492 "uncalibrated).".format(round(power))) 493 return round(power) 494 495 def calibrated_uplink_tx_power(self, bts_config, signal_level): 496 """ Calculates the power level at the instrument's input in order to 497 obtain the required tx power level at the DUT's output. 498 499 If calibration values are not available, returns the uncalibrated signal 500 level. 501 502 Args: 503 bts_config: the current configuration at the base station. derived 504 classes implementations can use this object to indicate power as 505 spectral power density or in other units. 506 signal_level: desired uplink transmitted power, can be either a 507 key value pair, an int or a float 508 """ 509 510 # Obtain power value if the provided signal_level is a key value pair 511 if isinstance(signal_level, Enum): 512 power = signal_level.value 513 else: 514 power = signal_level 515 516 # Try to use measured path loss value. If this was not set, it will 517 # throw an TypeError exception 518 try: 519 calibrated_power = round(power - self.ul_path_loss) 520 if calibrated_power < self.UL_MIN_POWER: 521 self.log.warning( 522 "Cannot achieve phone UL Tx power of {} dBm. Requested UL " 523 "power of {} dBm exceeds callbox limit!".format( 524 power, calibrated_power)) 525 calibrated_power = self.UL_MIN_POWER 526 self.log.warning( 527 "Setting UL Tx power to min possible ({} dBm)".format( 528 calibrated_power)) 529 530 self.log.info( 531 "Requested phone UL Tx power of {} dBm, setting callbox Rx " 532 "power at {} dBm".format(power, calibrated_power)) 533 time.sleep(2) 534 # Power has to be a natural number so calibration wont be exact. 535 # Inform the actual transmitted power after rounding. 536 self.log.info( 537 "Phone uplink transmitted power is {0:.2f} dBm".format( 538 calibrated_power + self.ul_path_loss)) 539 return calibrated_power 540 except TypeError: 541 self.log.info("Phone uplink transmitted power set to {} (link is " 542 "uncalibrated).".format(round(power))) 543 return round(power) 544 545 def calibrate(self, band): 546 """ Calculates UL and DL path loss if it wasn't done before. 547 548 The should be already set to the required band before calling this 549 method. 550 551 Args: 552 band: the band that is currently being calibrated. 553 """ 554 555 if self.dl_path_loss and self.ul_path_loss: 556 self.log.info("Measurements are already calibrated.") 557 558 # Attach the phone to the base station 559 if not self.attach(): 560 self.log.info( 561 "Skipping calibration because the phone failed to attach.") 562 return 563 564 # If downlink or uplink were not yet calibrated, do it now 565 if not self.dl_path_loss: 566 self.dl_path_loss = self.downlink_calibration() 567 if not self.ul_path_loss: 568 self.ul_path_loss = self.uplink_calibration() 569 570 # Detach after calibrating 571 self.detach() 572 time.sleep(2) 573 574 def start_traffic_for_calibration(self): 575 """ 576 Starts UDP IP traffic before running calibration. Uses APN_1 577 configured in the phone. 578 """ 579 self.simulator.start_data_traffic() 580 581 def stop_traffic_for_calibration(self): 582 """ 583 Stops IP traffic after calibration. 584 """ 585 self.simulator.stop_data_traffic() 586 587 def downlink_calibration(self, rat=None, power_units_conversion_func=None): 588 """ Computes downlink path loss and returns the calibration value 589 590 The DUT needs to be attached to the base station before calling this 591 method. 592 593 Args: 594 rat: desired RAT to calibrate (matching the label reported by 595 the phone) 596 power_units_conversion_func: a function to convert the units 597 reported by the phone to dBm. needs to take two arguments: the 598 reported signal level and bts. use None if no conversion is 599 needed. 600 Returns: 601 Dowlink calibration value and measured DL power. 602 """ 603 604 # Check if this parameter was set. Child classes may need to override 605 # this class passing the necessary parameters. 606 if not rat: 607 raise ValueError( 608 "The parameter 'rat' has to indicate the RAT being used as " 609 "reported by the phone.") 610 611 # Save initial output level to restore it after calibration 612 restoration_config = self.BtsConfig() 613 restoration_config.output_power = self.primary_config.output_power 614 615 # Set BTS to a good output level to minimize measurement error 616 new_config = self.BtsConfig() 617 new_config.output_power = self.simulator.MAX_DL_POWER - 5 618 self.simulator.configure_bts(new_config) 619 620 # Starting IP traffic 621 self.start_traffic_for_calibration() 622 623 down_power_measured = [] 624 for i in range(0, self.NUM_DL_CAL_READS): 625 # For some reason, the RSRP gets updated on Screen ON event 626 signal_strength = self.dut.get_telephony_signal_strength() 627 down_power_measured.append(signal_strength[rat]) 628 time.sleep(5) 629 630 # Stop IP traffic 631 self.stop_traffic_for_calibration() 632 633 # Reset bts to original settings 634 self.simulator.configure_bts(restoration_config) 635 time.sleep(2) 636 637 # Calculate the mean of the measurements 638 reported_asu_power = np.nanmean(down_power_measured) 639 640 # Convert from RSRP to signal power 641 if power_units_conversion_func: 642 avg_down_power = power_units_conversion_func( 643 reported_asu_power, self.primary_config) 644 else: 645 avg_down_power = reported_asu_power 646 647 # Calculate Path Loss 648 dl_target_power = self.simulator.MAX_DL_POWER - 5 649 down_call_path_loss = dl_target_power - avg_down_power 650 651 # Validate the result 652 if not 0 < down_call_path_loss < 100: 653 raise RuntimeError( 654 "Downlink calibration failed. The calculated path loss value " 655 "was {} dBm.".format(down_call_path_loss)) 656 657 self.log.info( 658 "Measured downlink path loss: {} dB".format(down_call_path_loss)) 659 660 return down_call_path_loss 661 662 def uplink_calibration(self): 663 """ Computes uplink path loss and returns the calibration value 664 665 The DUT needs to be attached to the base station before calling this 666 method. 667 668 Returns: 669 Uplink calibration value and measured UL power 670 """ 671 672 # Save initial input level to restore it after calibration 673 restoration_config = self.BtsConfig() 674 restoration_config.input_power = self.primary_config.input_power 675 676 # Set BTS1 to maximum input allowed in order to perform 677 # uplink calibration 678 target_power = self.MAX_PHONE_OUTPUT_POWER 679 new_config = self.BtsConfig() 680 new_config.input_power = self.MAX_BTS_INPUT_POWER 681 self.simulator.configure_bts(new_config) 682 683 # Start IP traffic 684 self.start_traffic_for_calibration() 685 686 up_power_per_chain = [] 687 # Get the number of chains 688 cmd = 'MONITOR? UL_PUSCH' 689 uplink_meas_power = self.anritsu.send_query(cmd) 690 str_power_chain = uplink_meas_power.split(',') 691 num_chains = len(str_power_chain) 692 for ichain in range(0, num_chains): 693 up_power_per_chain.append([]) 694 695 for i in range(0, self.NUM_UL_CAL_READS): 696 uplink_meas_power = self.anritsu.send_query(cmd) 697 str_power_chain = uplink_meas_power.split(',') 698 699 for ichain in range(0, num_chains): 700 if (str_power_chain[ichain] == 'DEACTIVE'): 701 up_power_per_chain[ichain].append(float('nan')) 702 else: 703 up_power_per_chain[ichain].append( 704 float(str_power_chain[ichain])) 705 706 time.sleep(3) 707 708 # Stop IP traffic 709 self.stop_traffic_for_calibration() 710 711 # Reset bts to original settings 712 self.simulator.configure_bts(restoration_config) 713 time.sleep(2) 714 715 # Phone only supports 1x1 Uplink so always chain 0 716 avg_up_power = np.nanmean(up_power_per_chain[0]) 717 if np.isnan(avg_up_power): 718 raise RuntimeError( 719 "Calibration failed because the callbox reported the chain to " 720 "be deactive.") 721 722 up_call_path_loss = target_power - avg_up_power 723 724 # Validate the result 725 if not 0 < up_call_path_loss < 100: 726 raise RuntimeError( 727 "Uplink calibration failed. The calculated path loss value " 728 "was {} dBm.".format(up_call_path_loss)) 729 730 self.log.info( 731 "Measured uplink path loss: {} dB".format(up_call_path_loss)) 732 733 return up_call_path_loss 734 735 def load_pathloss_if_required(self): 736 """ If calibration is required, try to obtain the pathloss values from 737 the calibration table and measure them if they are not available. """ 738 # Invalidate the previous values 739 self.dl_path_loss = None 740 self.ul_path_loss = None 741 742 # Load the new ones 743 if self.calibration_required: 744 745 band = self.primary_config.band 746 747 # Try loading the path loss values from the calibration table. If 748 # they are not available, use the automated calibration procedure. 749 try: 750 self.dl_path_loss = self.calibration_table[band]["dl"] 751 self.ul_path_loss = self.calibration_table[band]["ul"] 752 except KeyError: 753 self.calibrate(band) 754 755 # Complete the calibration table with the new values to be used in 756 # the next tests. 757 if band not in self.calibration_table: 758 self.calibration_table[band] = {} 759 760 if "dl" not in self.calibration_table[band] and self.dl_path_loss: 761 self.calibration_table[band]["dl"] = self.dl_path_loss 762 763 if "ul" not in self.calibration_table[band] and self.ul_path_loss: 764 self.calibration_table[band]["ul"] = self.ul_path_loss 765 766 def maximum_downlink_throughput(self): 767 """ Calculates maximum achievable downlink throughput in the current 768 simulation state. 769 770 Because thoughput is dependent on the RAT, this method needs to be 771 implemented by children classes. 772 773 Returns: 774 Maximum throughput in mbps 775 """ 776 raise NotImplementedError() 777 778 def maximum_uplink_throughput(self): 779 """ Calculates maximum achievable downlink throughput in the current 780 simulation state. 781 782 Because thoughput is dependent on the RAT, this method needs to be 783 implemented by children classes. 784 785 Returns: 786 Maximum throughput in mbps 787 """ 788 raise NotImplementedError() 789