1#!/usr/bin/env python3
2#
3#   Copyright 2021 - 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 acts.controllers.cellular_lib.BaseCellConfig as base_cell
18import acts.controllers.cellular_lib.LteSimulation as lte_sim
19import math
20
21
22class LteCellConfig(base_cell.BaseCellConfig):
23    """ Extension of the BaseBtsConfig to implement parameters that are
24         exclusive to LTE.
25
26    Attributes:
27        band: an integer indicating the required band number.
28        dlul_config: an integer indicating the TDD config number.
29        ssf_config: an integer indicating the Special Sub-Frame config.
30        bandwidth: a float indicating the required channel bandwidth.
31        mimo_mode: an instance of LteSimulation.MimoMode indicating the
32            required MIMO mode for the downlink signal.
33        transmission_mode: an instance of LteSimulation.TransmissionMode
34            indicating the required TM.
35        scheduling_mode: an instance of LteSimulation.SchedulingMode
36            indicating whether to use Static or Dynamic scheduling.
37        dl_rbs: an integer indicating the number of downlink RBs
38        ul_rbs: an integer indicating the number of uplink RBs
39        dl_mcs: an integer indicating the MCS for the downlink signal
40        ul_mcs: an integer indicating the MCS for the uplink signal
41        dl_256_qam_enabled: a boolean indicating if 256 QAM is enabled
42        ul_64_qam_enabled: a boolean indicating if 256 QAM is enabled
43        mac_padding: a boolean indicating whether RBs should be allocated
44            when there is no user data in static scheduling
45        dl_channel: an integer indicating the downlink channel number
46        cfi: an integer indicating the Control Format Indicator
47        paging_cycle: an integer indicating the paging cycle duration in
48            milliseconds
49        phich: a string indicating the PHICH group size parameter
50        drx_connected_mode: a boolean indicating whether cDRX mode is
51            on or off
52        drx_on_duration_timer: number of PDCCH subframes representing
53            DRX on duration
54        drx_inactivity_timer: number of PDCCH subframes to wait before
55            entering DRX mode
56        drx_retransmission_timer: number of consecutive PDCCH subframes
57            to wait for retransmission
58        drx_long_cycle: number of subframes representing one long DRX cycle.
59            One cycle consists of DRX sleep + DRX on duration
60        drx_long_cycle_offset: number representing offset in range
61            0 to drx_long_cycle - 1
62    """
63    PARAM_FRAME_CONFIG = "tddconfig"
64    PARAM_BW = "bw"
65    PARAM_SCHEDULING = "scheduling"
66    PARAM_SCHEDULING_STATIC = "static"
67    PARAM_SCHEDULING_DYNAMIC = "dynamic"
68    PARAM_PATTERN = "pattern"
69    PARAM_TM = "tm"
70    PARAM_BAND = "band"
71    PARAM_MIMO = "mimo"
72    PARAM_DL_MCS = "dlmcs"
73    PARAM_UL_MCS = "ulmcs"
74    PARAM_SSF = "ssf"
75    PARAM_CFI = "cfi"
76    PARAM_PAGING = "paging"
77    PARAM_PHICH = "phich"
78    PARAM_DRX = "drx"
79    PARAM_PADDING = "mac_padding"
80    PARAM_DL_256_QAM_ENABLED = "256_qam_dl_enabled"
81    PARAM_UL_64_QAM_ENABLED = "64_qam_ul_enabled"
82    PARAM_DL_EARFCN = "dl_earfcn"
83    PARAM_TA = "tracking_area"
84    PARAM_DISABLE_ALL_UL_SUBFRAMES = "disable_all_ul_subframes"
85
86    def __init__(self, log):
87        """ Initialize the base station config by setting all its
88        parameters to None.
89        Args:
90            log: logger object.
91        """
92        super().__init__(log)
93        self.band = None
94        self.dlul_config = None
95        self.ssf_config = None
96        self.bandwidth = None
97        self.mimo_mode = None
98        self.transmission_mode = None
99        self.scheduling_mode = None
100        self.dl_rbs = None
101        self.ul_rbs = None
102        self.dl_mcs = None
103        self.ul_mcs = None
104        self.dl_256_qam_enabled = None
105        self.ul_64_qam_enabled = None
106        self.mac_padding = None
107        self.dl_channel = None
108        self.cfi = None
109        self.paging_cycle = None
110        self.phich = None
111        self.drx_connected_mode = None
112        self.drx_on_duration_timer = None
113        self.drx_inactivity_timer = None
114        self.drx_retransmission_timer = None
115        self.drx_long_cycle = None
116        self.drx_long_cycle_offset = None
117        self.tracking_area = None
118        self.disable_all_ul_subframes = None
119
120    def __str__(self):
121        return str(vars(self))
122
123    def configure(self, parameters):
124        """ Configures an LTE cell using a dictionary of parameters.
125
126        Args:
127            parameters: a configuration dictionary
128        """
129        # Setup band
130        if self.PARAM_BAND not in parameters:
131            raise ValueError(
132                "The configuration dictionary must include a key '{}' with "
133                "the required band number.".format(self.PARAM_BAND))
134
135        self.band = parameters[self.PARAM_BAND]
136
137        if self.PARAM_DL_EARFCN not in parameters:
138            band = int(self.band)
139            channel = int(lte_sim.LteSimulation.LOWEST_DL_CN_DICTIONARY[band] +
140                          lte_sim.LteSimulation.LOWEST_DL_CN_DICTIONARY[band +
141                                                                        1]) / 2
142            self.log.warning(
143                "Key '{}' was not set. Using center band channel {} by default."
144                .format(self.PARAM_DL_EARFCN, channel))
145            self.dl_channel = channel
146        else:
147            self.dl_channel = parameters[self.PARAM_DL_EARFCN]
148
149        # Set TDD-only configs
150        if self.get_duplex_mode() == lte_sim.DuplexMode.TDD:
151
152            # Sub-frame DL/UL config
153            if self.PARAM_FRAME_CONFIG not in parameters:
154                raise ValueError("When a TDD band is selected the frame "
155                                 "structure has to be indicated with the '{}' "
156                                 "key with a value from 0 to 6.".format(
157                                     self.PARAM_FRAME_CONFIG))
158
159            self.dlul_config = int(parameters[self.PARAM_FRAME_CONFIG])
160
161            # Special Sub-Frame configuration
162            if self.PARAM_SSF not in parameters:
163                self.log.warning(
164                    'The {} parameter was not provided. Setting '
165                    'Special Sub-Frame config to 6 by default.'.format(
166                        self.PARAM_SSF))
167                self.ssf_config = 6
168            else:
169                self.ssf_config = int(parameters[self.PARAM_SSF])
170
171        # Setup bandwidth
172        if self.PARAM_BW not in parameters:
173            raise ValueError(
174                "The config dictionary must include parameter {} with an "
175                "int value (to indicate 1.4 MHz use 14).".format(
176                    self.PARAM_BW))
177
178        bw = float(parameters[self.PARAM_BW])
179
180        if abs(bw - 14) < 0.00000000001:
181            bw = 1.4
182
183        self.bandwidth = bw
184
185        # Setup mimo mode
186        if self.PARAM_MIMO not in parameters:
187            raise ValueError(
188                "The config dictionary must include parameter '{}' with the "
189                "mimo mode.".format(self.PARAM_MIMO))
190
191        for mimo_mode in lte_sim.MimoMode:
192            if parameters[self.PARAM_MIMO] == mimo_mode.value:
193                self.mimo_mode = mimo_mode
194                break
195        else:
196            raise ValueError("The value of {} must be one of the following:"
197                             "1x1, 2x2 or 4x4.".format(self.PARAM_MIMO))
198
199        # Setup transmission mode
200        if self.PARAM_TM not in parameters:
201            raise ValueError(
202                "The config dictionary must include key {} with an "
203                "int value from 1 to 4 indicating transmission mode.".format(
204                    self.PARAM_TM))
205
206        for tm in lte_sim.TransmissionMode:
207            if parameters[self.PARAM_TM] == tm.value[2:]:
208                self.transmission_mode = tm
209                break
210        else:
211            raise ValueError(
212                "The {} key must have one of the following values:"
213                "1, 2, 3, 4, 7, 8 or 9.".format(self.PARAM_TM))
214
215        # Setup scheduling mode
216        if self.PARAM_SCHEDULING not in parameters:
217            self.scheduling_mode = lte_sim.SchedulingMode.STATIC
218            self.log.warning(
219                "The test config does not include the '{}' key. Setting to "
220                "static by default.".format(self.PARAM_SCHEDULING))
221        elif parameters[
222                self.PARAM_SCHEDULING] == self.PARAM_SCHEDULING_DYNAMIC:
223            self.scheduling_mode = lte_sim.SchedulingMode.DYNAMIC
224        elif parameters[self.PARAM_SCHEDULING] == self.PARAM_SCHEDULING_STATIC:
225            self.scheduling_mode = lte_sim.SchedulingMode.STATIC
226        else:
227            raise ValueError("Key '{}' must have a value of "
228                             "'dynamic' or 'static'.".format(
229                                 self.PARAM_SCHEDULING))
230
231        if self.scheduling_mode == lte_sim.SchedulingMode.STATIC:
232
233            if self.PARAM_PADDING not in parameters:
234                self.log.warning(
235                    "The '{}' parameter was not set. Enabling MAC padding by "
236                    "default.".format(self.PARAM_PADDING))
237                self.mac_padding = True
238            else:
239                self.mac_padding = parameters[self.PARAM_PADDING]
240
241            if self.PARAM_PATTERN not in parameters:
242                self.log.warning(
243                    "The '{}' parameter was not set, using 100% RBs for both "
244                    "DL and UL. To set the percentages of total RBs include "
245                    "the '{}' key with a list of two ints indicating downlink "
246                    "and uplink percentages.".format(self.PARAM_PATTERN,
247                                                     self.PARAM_PATTERN))
248                dl_pattern = 100
249                ul_pattern = 100
250            else:
251                dl_pattern = int(parameters[self.PARAM_PATTERN][0])
252                ul_pattern = int(parameters[self.PARAM_PATTERN][1])
253
254            if not (0 <= dl_pattern <= 100 and 0 <= ul_pattern <= 100):
255                raise ValueError(
256                    "The scheduling pattern parameters need to be two "
257                    "positive numbers between 0 and 100.")
258
259            self.dl_rbs, self.ul_rbs = (self.allocation_percentages_to_rbs(
260                dl_pattern, ul_pattern))
261
262            # Check if 256 QAM is enabled for DL MCS
263            if self.PARAM_DL_256_QAM_ENABLED not in parameters:
264                self.log.warning("The key '{}' is not set in the test config. "
265                                 "Setting to false by default.".format(
266                                     self.PARAM_DL_256_QAM_ENABLED))
267
268            self.dl_256_qam_enabled = parameters.get(
269                self.PARAM_DL_256_QAM_ENABLED, False
270            )
271
272            self.disable_all_ul_subframes = parameters.get(
273                self.PARAM_DISABLE_ALL_UL_SUBFRAMES, False
274            )
275
276            # Look for a DL MCS configuration in the test parameters. If it is
277            # not present, use a default value.
278            if self.PARAM_DL_MCS in parameters:
279                self.dl_mcs = int(parameters[self.PARAM_DL_MCS])
280            else:
281                self.log.warning(
282                    'The test config does not include the {} key. Setting '
283                    'to the max value by default'.format(self.PARAM_DL_MCS))
284                if self.dl_256_qam_enabled and self.bandwidth == 1.4:
285                    self.dl_mcs = 26
286                elif (not self.dl_256_qam_enabled and self.mac_padding
287                      and self.bandwidth != 1.4):
288                    self.dl_mcs = 28
289                else:
290                    self.dl_mcs = 27
291
292            # Check if 64 QAM is enabled for UL MCS
293            if self.PARAM_UL_64_QAM_ENABLED not in parameters:
294                self.log.warning("The key '{}' is not set in the config file. "
295                                 "Setting to false by default.".format(
296                                     self.PARAM_UL_64_QAM_ENABLED))
297
298            self.ul_64_qam_enabled = parameters.get(
299                self.PARAM_UL_64_QAM_ENABLED, False)
300
301            # Look for an UL MCS configuration in the test parameters. If it is
302            # not present, use a default value.
303            if self.PARAM_UL_MCS in parameters:
304                self.ul_mcs = int(parameters[self.PARAM_UL_MCS])
305            else:
306                self.log.warning(
307                    'The test config does not include the {} key. Setting '
308                    'to the max value by default'.format(self.PARAM_UL_MCS))
309                if self.ul_64_qam_enabled:
310                    self.ul_mcs = 28
311                else:
312                    self.ul_mcs = 23
313
314        # Configure the simulation for DRX mode
315        if self.PARAM_DRX in parameters and len(
316                parameters[self.PARAM_DRX]) == 5:
317            self.drx_connected_mode = True
318            self.drx_on_duration_timer = parameters[self.PARAM_DRX][0]
319            self.drx_inactivity_timer = parameters[self.PARAM_DRX][1]
320            self.drx_retransmission_timer = parameters[self.PARAM_DRX][2]
321            self.drx_long_cycle = parameters[self.PARAM_DRX][3]
322            try:
323                long_cycle = int(parameters[self.PARAM_DRX][3])
324                long_cycle_offset = int(parameters[self.PARAM_DRX][4])
325                if long_cycle_offset in range(0, long_cycle):
326                    self.drx_long_cycle_offset = long_cycle_offset
327                else:
328                    self.log.error(
329                        ("The cDRX long cycle offset must be in the "
330                         "range 0 to (long cycle  - 1). Setting "
331                         "long cycle offset to 0"))
332                    self.drx_long_cycle_offset = 0
333
334            except ValueError:
335                self.log.error(("cDRX long cycle and long cycle offset "
336                                "must be integers. Disabling cDRX mode."))
337                self.drx_connected_mode = False
338        else:
339            self.log.warning(
340                ("DRX mode was not configured properly. "
341                 "Please provide a list with the following values: "
342                 "1) DRX on duration timer "
343                 "2) Inactivity timer "
344                 "3) Retransmission timer "
345                 "4) Long DRX cycle duration "
346                 "5) Long DRX cycle offset "
347                 "Example: [2, 6, 16, 20, 0]."))
348
349        # Channel Control Indicator
350        if self.PARAM_CFI not in parameters:
351            self.log.warning('The {} parameter was not provided. Setting '
352                             'CFI to BESTEFFORT.'.format(self.PARAM_CFI))
353            self.cfi = 'BESTEFFORT'
354        else:
355            self.cfi = parameters[self.PARAM_CFI]
356
357        # PHICH group size
358        if self.PARAM_PHICH not in parameters:
359            self.log.warning('The {} parameter was not provided. Setting '
360                             'PHICH group size to 1 by default.'.format(
361                                 self.PARAM_PHICH))
362            self.phich = '1'
363        else:
364            if parameters[self.PARAM_PHICH] == '16':
365                self.phich = '1/6'
366            elif parameters[self.PARAM_PHICH] == '12':
367                self.phich = '1/2'
368            elif parameters[self.PARAM_PHICH] in ['1/6', '1/2', '1', '2']:
369                self.phich = parameters[self.PARAM_PHICH]
370            else:
371                raise ValueError('The {} parameter can only be followed by 1,'
372                                 '2, 1/2 (or 12) and 1/6 (or 16).'.format(
373                                     self.PARAM_PHICH))
374
375        # Paging cycle duration
376        if self.PARAM_PAGING not in parameters:
377            self.log.warning('The {} parameter was not provided. Setting '
378                             'paging cycle duration to 1280 ms by '
379                             'default.'.format(self.PARAM_PAGING))
380            self.paging_cycle = 1280
381        else:
382            try:
383                self.paging_cycle = int(parameters[self.PARAM_PAGING])
384            except ValueError:
385                raise ValueError(
386                    'The {} key has to be followed by the paging cycle '
387                    'duration in milliseconds.'.format(self.PARAM_PAGING))
388
389        if self.PARAM_TA in parameters:
390            self.tracking_area = int(parameters[self.PARAM_TA])
391
392    def get_duplex_mode(self):
393        """ Determines if the cell uses FDD or TDD duplex mode
394
395        Returns:
396          an variable of class DuplexMode indicating if band is FDD or TDD
397        """
398        if 33 <= int(self.band) <= 46:
399            return lte_sim.DuplexMode.TDD
400        else:
401            return lte_sim.DuplexMode.FDD
402
403    def allocation_percentages_to_rbs(self, dl, ul):
404        """ Converts usage percentages to number of DL/UL RBs
405
406        Because not any number of DL/UL RBs can be obtained for a certain
407        bandwidth, this function calculates the number of RBs that most
408        closely matches the desired DL/UL percentages.
409
410        Args:
411            dl: desired percentage of downlink RBs
412            ul: desired percentage of uplink RBs
413        Returns:
414            a tuple indicating the number of downlink and uplink RBs
415        """
416
417        # Validate the arguments
418        if (not 0 <= dl <= 100) or (not 0 <= ul <= 100):
419            raise ValueError("The percentage of DL and UL RBs have to be two "
420                             "positive between 0 and 100.")
421
422        # Get min and max values from tables
423        max_rbs = lte_sim.TOTAL_RBS_DICTIONARY[self.bandwidth]
424        min_dl_rbs = lte_sim.MIN_DL_RBS_DICTIONARY[self.bandwidth]
425        min_ul_rbs = lte_sim.MIN_UL_RBS_DICTIONARY[self.bandwidth]
426
427        def percentage_to_amount(min_val, max_val, percentage):
428            """ Returns the integer between min_val and max_val that is closest
429            to percentage/100*max_val
430            """
431
432            # Calculate the value that corresponds to the required percentage.
433            closest_int = round(max_val * percentage / 100)
434            # Cannot be less than min_val
435            closest_int = max(closest_int, min_val)
436            # RBs cannot be more than max_rbs
437            closest_int = min(closest_int, max_val)
438
439            return closest_int
440
441        # Calculate the number of DL RBs
442
443        # Get the number of DL RBs that corresponds to
444        #  the required percentage.
445        desired_dl_rbs = percentage_to_amount(min_val=min_dl_rbs,
446                                              max_val=max_rbs,
447                                              percentage=dl)
448
449        if self.transmission_mode == lte_sim.TransmissionMode.TM3 or \
450                self.transmission_mode == lte_sim.TransmissionMode.TM4:
451
452            # For TM3 and TM4 the number of DL RBs needs to be max_rbs or a
453            # multiple of the RBG size
454
455            if desired_dl_rbs == max_rbs:
456                dl_rbs = max_rbs
457            else:
458                dl_rbs = (math.ceil(
459                    desired_dl_rbs / lte_sim.RBG_DICTIONARY[self.bandwidth]) *
460                          lte_sim.RBG_DICTIONARY[self.bandwidth])
461
462        else:
463            # The other TMs allow any number of RBs between 1 and max_rbs
464            dl_rbs = desired_dl_rbs
465
466        # Calculate the number of UL RBs
467
468        # Get the number of UL RBs that corresponds
469        # to the required percentage
470        desired_ul_rbs = percentage_to_amount(min_val=min_ul_rbs,
471                                              max_val=max_rbs,
472                                              percentage=ul)
473
474        # Create a list of all possible UL RBs assignment
475        # The standard allows any number that can be written as
476        # 2**a * 3**b * 5**c for any combination of a, b and c.
477
478        def pow_range(max_value, base):
479            """ Returns a range of all possible powers of base under
480              the given max_value.
481          """
482            return range(int(math.ceil(math.log(max_value, base))))
483
484        possible_ul_rbs = [
485            2 ** a * 3 ** b * 5 ** c for a in pow_range(max_rbs, 2)
486            for b in pow_range(max_rbs, 3)
487            for c in pow_range(max_rbs, 5)
488            if 2 ** a * 3 ** b * 5 ** c <= max_rbs]  # yapf: disable
489
490        # Find the value in the list that is closest to desired_ul_rbs
491        differences = [abs(rbs - desired_ul_rbs) for rbs in possible_ul_rbs]
492        ul_rbs = possible_ul_rbs[differences.index(min(differences))]
493
494        # Report what are the obtained RB percentages
495        self.log.info("Requested a {}% / {}% RB allocation. Closest possible "
496                      "percentages are {}% / {}%.".format(
497                          dl, ul, round(100 * dl_rbs / max_rbs),
498                          round(100 * ul_rbs / max_rbs)))
499
500        return dl_rbs, ul_rbs
501