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