1#!/usr/bin/env python3 2# 3# Copyright 2023 - 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"""Provides utilities for generating CMW500 CA scenarios based on band/mimo combinations. 17 18NOTE: Currently does not support: 19 1. Scenarios requiring multiple CMW500s 20 2. SUA coprocessing 21 3. Fading scenarios 22""" 23 24from collections import defaultdict 25from collections import namedtuple 26 27# Maps CMW scenario names to antenna (MIMO) configuration. 28SCENARIO_NAME_MAPPING = { 29 # 1CC 30 "SCEL": (1, ), 31 "TRO": (2, ), 32 "AD": (4, ), 33 # 2CC 34 "CATR": (1, 1), 35 "CAFR": (2, 2), 36 "BF": (4, 2), 37 "BFSM4": (2, 4), 38 "BH": (4, 4), 39 # 3CC 40 "CC": (1, 1, 1), 41 "CF": (2, 2, 2), 42 "CCMP": (2, 1, 1), 43 "CCMS1": (1, 2, 1), 44 # 4CC 45 "DD": (1, 1, 1, 1), 46 "DH": (2, 2, 2, 2), 47 # 5CC - 8CC require multiple CMWs 48} 49 50# Maps antenna combinations to CMW scenario name. 51SCENARIO_ANTENNA_MAPPING = {v: k for k, v in SCENARIO_NAME_MAPPING.items()} 52 53 54def get_scenario(bands, antennas): 55 """Gets a compatible scenario for the given band/antenna combination. 56 57 Args: 58 bands: a list defining the bands to use for each CC. 59 antennas: a list of integers defining the number of antennas to use for each CC. 60 61 Returns: 62 CMW500Scenario: the generated scenario. 63 64 Raises: 65 ValueError: if there is no scenario available for the given antenna/band combination. 66 """ 67 antennas = tuple(antennas) 68 if not antennas in SCENARIO_ANTENNA_MAPPING: 69 raise ValueError( 70 "No CMW scenario matches antenna combination: {}".format(antennas)) 71 72 generator = CMW500ScenarioGenerator() 73 port_configs = [generator.get_next(b, a) for b, a in zip(bands, antennas)] 74 75 scenario_name = SCENARIO_ANTENNA_MAPPING[antennas] 76 return CMW500Scenario(scenario_name, port_configs) 77 78 79def get_antennas(name): 80 """Gets the antenna combination mimo corresponding to a scenario name. 81 82 Args: 83 name: a string defining the scenario name. 84 85 Returns: 86 antennas: a list of integers defining the number of antennas for each CC. 87 88 Raises: 89 ValueError: if the scenario name is unknown. 90 """ 91 if not name in SCENARIO_NAME_MAPPING: 92 raise ValueError("Unknown scenario name: {}".format(name)) 93 94 return list(SCENARIO_NAME_MAPPING[name]) 95 96 97class CMW500Scenario(object): 98 """A routed scenario in a CMW500.""" 99 100 def __init__(self, name, configs): 101 """Initialize a CMW500 scenario from a name and PortConfiguration list. 102 103 Args: 104 name: a string defining the CMW500 scenario name. 105 configs: list(list(PortConfigurations)) defining the configurations for each CC. 106 """ 107 self.name = name 108 self.configs = configs 109 110 @property 111 def routing(self): 112 """Gets the CMW routing text for the scenario. 113 114 Returns: 115 routing: a string defining the routing text for the CMW scenario command. 116 """ 117 routing = [] 118 i = 1 119 for carrier_config in self.configs: 120 routing.append("SUA{}".format(i)) 121 # Assume PCC antenna & uplink are always on port 1 122 if i == 1: 123 routing.append("RF1C") 124 routing.append("RX1") 125 for config in carrier_config: 126 routing.append("RF{}C".format(config.port_id)) 127 routing.append("TX{}".format(config.converter_id)) 128 i += 1 129 return ",".join(routing) 130 131 132# A port/converter combination for a single callbox port in a component carrier. 133PortConfiguration = namedtuple("PortConfiguration", "port_id converter_id") 134 135 136class CMW500ScenarioGenerator(object): 137 """Class that is responsible for generating port/converter configurations for cmw500. 138 139 Generator prioritizes using the fewest total number of antenna ports it can. 140 141 Generation rules: 142 - There are 4 'converters' 143 - Each converter can be used up to a maximum of twice (if both CCs use the same band) 144 - Each converter has 2 potental antenna ports that it can be used with 145 """ 146 147 # Maps converters to possible antenna ports. 148 PORT_MAPPING = {1: (1, 2), 2: (3, 4), 3: (1, 2), 4: (3, 4)} 149 150 # Maps antennas to possible converters. 151 CONVERTER_MAPPING = {1: (1, 3), 2: (1, 3), 3: (2, 4), 4: (2, 4)} 152 153 def __init__(self): 154 self.used_once = defaultdict(list) 155 self.free_converters = set([1, 2, 3, 4]) 156 157 def get_next(self, band, antenna_count): 158 """Generates a routing configuration for the next component carrier in the sequence. 159 160 Returns: 161 configs a list of PortConfigurations defining the configuration to use for each port 162 163 Raises: 164 ValueError: if the generator fails to find a compatible scenario routing. 165 """ 166 if antenna_count < 1: 167 raise ValueError("antenna_count must be greater than 0") 168 169 configs = [] 170 free_ports = [1, 3, 2, 4] 171 converters_temp = [] 172 # First, try to reuse previously used converters where we can. 173 for converter in self.used_once[band]: 174 port = next( 175 (a for a in self.PORT_MAPPING[converter] if a in free_ports), 176 None, 177 ) 178 if port is None: 179 # No port available to be used with this converter, save for later. 180 converters_temp.append(converter) 181 continue 182 183 free_ports.remove(port) 184 configs.append(PortConfiguration(port, converter)) 185 if len(configs) == antenna_count: 186 break 187 188 self.used_once[band] = converters_temp 189 if len(configs) == antenna_count: 190 return configs 191 192 # Try to use unused converters. 193 for port in free_ports: 194 converter = next( 195 (c for c in self.CONVERTER_MAPPING[port] 196 if c in self.free_converters), 197 None, 198 ) 199 if converter is None: 200 continue 201 202 # Save converter to reuse later. 203 self.free_converters.remove(converter) 204 self.used_once[band].append(converter) 205 configs.append(PortConfiguration(port, converter)) 206 if len(configs) == antenna_count: 207 break 208 209 if len(configs) != antenna_count: 210 raise ValueError( 211 "Unable to generate CMW500 scenario for requested combination") 212 213 return configs 214