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