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