• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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