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