Example #1
0
def consistency_check(params, f_max_from_si):
    """ checks that the requested parameters are consistant (spacing vs nb channel,
        vs transponder mode...)
    """
    f_min = params['f_min']
    f_max = params['f_max']
    max_recommanded_nb_channels = automatic_nch(f_min, f_max,
                                                params['spacing'])
    if params['baud_rate'] is not None:
        #implicitely means that a mode is defined with min_spacing
        if params['min_spacing'] > params['spacing']:
            msg = f'Request {params["request_id"]} has spacing below transponder ' +\
                  f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\
                  f'{params["min_spacing"]*1e-9}GHz.\nComputation stopped'
            print(msg)
            LOGGER.critical(msg)
            raise ServiceError(msg)
        if f_max > f_max_from_si:
            msg = dedent(f'''
            Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz and requested spacing {params["spacing"]*1e-9}GHz 
            is not consistent with frequency range {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz.
            max recommanded nb of channels is {max_recommanded_nb_channels}
            Computation stopped.''')
            LOGGER.critical(msg)
            raise ServiceError(msg)
Example #2
0
def correct_route_list(network, pathreqlist):
    """ prepares the format of route list of nodes to be consistant
        remove wrong names, remove endpoints
        also correct source and destination
    """
    anytype = [n.uid for n in network.nodes()]
    # TODO there is a problem of identification of fibers in case of parallel fibers
    # between two adjacent roadms so fiber constraint is not supported
    transponders = [
        n.uid for n in network.nodes() if isinstance(n, Transceiver)
    ]
    for pathreq in pathreqlist:
        for i, n_id in enumerate(pathreq.nodes_list):
            # replace possibly wrong name with a formated roadm name
            # print(n_id)
            if n_id not in anytype:
                # find nodes name that include constraint among all possible names except
                # transponders (not yet supported as constraints).
                nodes_suggestion = [uid for uid in anytype \
                    if n_id.lower() in uid.lower() and uid not in transponders]
                if pathreq.loose_list[i] == 'LOOSE':
                    if len(nodes_suggestion) > 0:
                        new_n = nodes_suggestion[0]
                        print(f'invalid route node specified:\
                        \n\'{n_id}\', replaced with \'{new_n}\'')
                        pathreq.nodes_list[i] = new_n
                    else:
                        print(f'\x1b[1;33;40m'+f'invalid route node specified \'{n_id}\',' +\
                              f' could not use it as constraint, skipped!'+'\x1b[0m')
                        pathreq.nodes_list.remove(n_id)
                        pathreq.loose_list.pop(i)
                else:
                    msg = f'\x1b[1;33;40m'+f'could not find node: {n_id} in network topology.' +\
                          f' Strict constraint can not be applied.' + '\x1b[0m'
                    LOGGER.critical(msg)
                    raise ValueError(msg)
        if pathreq.source not in transponders:
            msg = f'\x1b[1;31;40m' + f'Request: {pathreq.request_id}: could not find' +\
                  f' transponder source: {pathreq.source}.'+'\x1b[0m'
            LOGGER.critical(msg)
            print(f'{msg}\nComputation stopped.')
            raise ServiceError(msg)

        if pathreq.destination not in transponders:
            msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find' +\
                  f' transponder destination: {pathreq.destination}.'+'\x1b[0m'
            LOGGER.critical(msg)
            print(f'{msg}\nComputation stopped.')
            raise ServiceError(msg)

        # TODO remove endpoints from this list in case they were added by the user
        # in the xls or json files
    return pathreqlist
Example #3
0
def select_candidate(candidates, policy):
    """ selects a candidate among all available spectrum
    """
    if policy == 'first_fit':
        if candidates:
            return candidates[0]
        else:
            return (None, None, None)
    else:
        raise ServiceError('Only first_fit spectrum assignment policy is implemented.')
Example #4
0
 def detailed_path_json(self):
     """ a function that builds path object for normal and blocking cases
     """
     index = 0
     pro_list = []
     for element in self.computed_path:
         temp = {
             'path-route-object': {
                 'index': index,
                 'num-unnum-hop': {
                     'node-id': element.uid,
                     'link-tp-id': element.uid,
                     # TODO change index in order to insert transponder attribute
                 }
             }
         }
         pro_list.append(temp)
         index += 1
         if self.path_request.M > 0:
             temp = {
                 'path-route-object': {
                     'index': index,
                     "label-hop": {
                         "N": self.path_request.N,
                         "M": self.path_request.M
                     },
                 }
             }
             pro_list.append(temp)
             index += 1
         elif self.path_request.M == 0 and hasattr(self.path_request,
                                                   'blocking_reason'):
             # if the path is blocked due to spectrum, no label object is created, but
             # the json response includes a detailed path for user infromation.
             pass
         else:
             raise ServiceError(
                 'request {self.path_id} should have positive path bandwidth value.'
             )
         if isinstance(element, Transceiver):
             temp = {
                 'path-route-object': {
                     'index': index,
                     'transponder': {
                         'transponder-type': self.path_request.tsp,
                         'transponder-mode': self.path_request.tsp_mode
                     }
                 }
             }
             pro_list.append(temp)
             index += 1
     return pro_list
Example #5
0
def _check_one_request(params, f_max_from_si):
    """Checks that the requested parameters are consistant (spacing vs nb channel vs transponder mode...)"""
    f_min = params['f_min']
    f_max = params['f_max']
    max_recommanded_nb_channels = automatic_nch(f_min, f_max,
                                                params['spacing'])
    if params['baud_rate'] is not None:
        # implicitly means that a mode is defined with min_spacing
        if params['min_spacing'] > params['spacing']:
            msg = f'Request {params["request_id"]} has spacing below transponder ' +\
                  f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\
                  f'{params["min_spacing"]*1e-9}GHz.\nComputation stopped'
            print(msg)
            _logger.critical(msg)
            raise ServiceError(msg)
        if f_max > f_max_from_si:
            msg = f'''Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz
            and requested spacing {params["spacing"]*1e-9}GHz is not consistent with frequency range
            {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz.
            max recommanded nb of channels is {max_recommanded_nb_channels}.'''
            _logger.critical(msg)
            raise ServiceError(msg)
    # Transponder mode already selected; will it fit to the requested bandwidth?
    if params['trx_mode'] is not None and params['effective_freq_slot'] is not None \
            and params['effective_freq_slot']['M'] is not None:
        _, requested_m = compute_spectrum_slot_vs_bandwidth(
            params['path_bandwidth'], params['spacing'], params['bit_rate'])
        # params['effective_freq_slot']['M'] value should be bigger than the computed requested_m (simple estimate)
        # TODO: elaborate a more accurate estimate with nb_wl * tx_osnr + possibly guardbands in case of
        # superchannel closed packing.

        if requested_m > params['effective_freq_slot']['M']:
            msg = f'requested M {params["effective_freq_slot"]["M"]} number of slots for request' +\
                  f'{params["request_id"]} should be greater than {requested_m} to support request' +\
                  f'{params["path_bandwidth"] * 1e-9} Gbit/s with {params["trx_type"]} {params["trx_mode"]}'
            _logger.critical(msg)
            raise ServiceError(msg)
Example #6
0
 def detailed_path_json(self, path):
     """ a function that builds path object for normal and blocking cases
     """
     index = 0
     pro_list = []
     for element in path:
         temp = {
             'path-route-object': {
                 'index': index,
                 'num-unnum-hop': {
                     'node-id': element.uid,
                     'link-tp-id': element.uid,
                     # TODO change index in order to insert transponder attribute
                 }
             }
         }
         node_type = element_to_node_type(element)
         if (node_type is not None):
             temp['path-route-object']['num-unnum-hop'][
                 'gnpy-node-type'] = node_type
         pro_list.append(temp)
         index += 1
         if self.path_request.M > 0:
             temp = {
                 'path-route-object': {
                     'index': index,
                     "label-hop": {
                         "N": self.path_request.N,
                         "M": self.path_request.M
                     },
                 }
             }
             pro_list.append(temp)
             index += 1
         elif self.path_request.M == 0 and hasattr(self.path_request,
                                                   'blocking_reason'):
             # if the path is blocked due to spectrum, no label object is created, but
             # the json response includes a detailed path for user infromation.
             pass
         else:
             raise ServiceError(
                 'request {self.path_id} should have positive path bandwidth value.'
             )
         if isinstance(element, Transceiver):
             temp = {
                 'path-route-object': {
                     'index': index,
                     'transponder': {
                         'transponder-type': self.path_request.tsp,
                         'transponder-mode': self.path_request.tsp_mode
                     }
                 }
             }
             pro_list.append(temp)
             index += 1
         if isinstance(element, Roadm):
             temp = {
                 'path-route-object': {
                     'index': index,
                     'target-channel-power': {
                         'value': element.effective_pch_out_db,
                     }
                 }
             }
             pro_list.append(temp)
             index += 1
         if isinstance(element, Edfa):
             temp = {
                 'path-route-object': {
                     'index': index,
                     'target-channel-power': {
                         'value': element.effective_pch_out_db,
                     },
                     'output-voa': {
                         'value': element.out_voa,
                     }
                 }
             }
             pro_list.append(temp)
             index += 1
     return pro_list
Example #7
0
    def __init__(self, Request, equipment, bidir):
        # request_id is str
        # excel has automatic number formatting that adds .0 on integer values
        # the next lines recover the pure int value, assuming this .0 is unwanted
        self.request_id = correct_xlrd_int_to_str_reading(Request.request_id)
        self.source = f'trx {Request.source}'
        self.destination = f'trx {Request.destination}'
        # TODO: the automatic naming generated by excel parser requires that source and dest name
        # be a string starting with 'trx' : this is manually added here.
        self.srctpid = f'trx {Request.source}'
        self.dsttpid = f'trx {Request.destination}'
        self.bidir = bidir
        # test that trx_type belongs to eqpt_config.json
        # if not replace it with a default
        try:
            if equipment['Transceiver'][Request.trx_type]:
                self.trx_type = correct_xlrd_int_to_str_reading(
                    Request.trx_type)
            if Request.mode is not None:
                Requestmode = correct_xlrd_int_to_str_reading(Request.mode)
                if [
                        mode for mode in equipment['Transceiver']
                    [Request.trx_type].mode if mode['format'] == Requestmode
                ]:
                    self.mode = Requestmode
                else:
                    msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.'
                    # print(msg)
                    logger.critical(msg)
                    raise ServiceError(msg)
            else:
                Requestmode = None
                self.mode = Request.mode
        except KeyError:
            msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.'
            # print(msg)
            logger.critical(msg)
            raise ServiceError(msg)
        # excel input are in GHz and dBm
        if Request.spacing is not None:
            self.spacing = Request.spacing * 1e9
        else:
            msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped'
            logger.critical(msg)
            raise ServiceError(msg)
        if Request.power is not None:
            self.power = db2lin(Request.power) * 1e-3
        else:
            self.power = None
        if Request.nb_channel is not None:
            self.nb_channel = int(Request.nb_channel)
        else:
            self.nb_channel = None

        value = correct_xlrd_int_to_str_reading(Request.disjoint_from)
        self.disjoint_from = [n for n in value.split(' | ') if value]
        self.nodes_list = []
        if Request.nodes_list:
            self.nodes_list = Request.nodes_list.split(' | ')
        self.loose = 'LOOSE'
        if Request.is_loose.lower() == 'no':
            self.loose = 'STRICT'
        self.path_bandwidth = None
        if Request.path_bandwidth is not None:
            self.path_bandwidth = Request.path_bandwidth * 1e9
        else:
            self.path_bandwidth = 0
Example #8
0
def correct_xls_route_list(network_filename, network, pathreqlist):
    """ prepares the format of route list of nodes to be consistant with nodes names:
        remove wrong names, find correct names for ila, roadm and fused if the entry was
        xls.
        if it was not xls, all names in list should be exact name in the network.
    """

    # first loads the base correspondance dict built with excel naming
    corresp_roadm, corresp_fused, corresp_ila = corresp_names(
        network_filename, network)
    # then correct dict names with names of the autodisign and find next_node name
    # according to xls naming
    corresp_ila, next_node = corresp_next_node(network, corresp_ila,
                                               corresp_roadm)
    # finally correct constraints based on these dict
    trxfibertype = [
        n.uid for n in network.nodes() if isinstance(n, (Transceiver, Fiber))
    ]
    roadmtype = [n.uid for n in network.nodes() if isinstance(n, Roadm)]
    edfatype = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
    # TODO there is a problem of identification of fibers in case of parallel
    # fibers between two adjacent roadms so fiber constraint is not supported
    transponders = [
        n.uid for n in network.nodes() if isinstance(n, Transceiver)
    ]
    for pathreq in pathreqlist:
        # first check that source and dest are transceivers
        if pathreq.source not in transponders:
            msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\
                f' transponder source : {pathreq.source}.{ansi_escapes.reset}'
            logger.critical(msg)
            raise ServiceError(msg)

        if pathreq.destination not in transponders:
            msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\
                f' transponder destination: {pathreq.destination}.{ansi_escapes.reset}'
            logger.critical(msg)
            raise ServiceError(msg)
        # silently pop source and dest nodes from the list if they were added by the user as first
        # and last elem in the constraints respectively. Other positions must lead to an error
        # caught later on
        if pathreq.nodes_list and pathreq.source == pathreq.nodes_list[0]:
            pathreq.loose_list.pop(0)
            pathreq.nodes_list.pop(0)
        if pathreq.nodes_list and pathreq.destination == pathreq.nodes_list[-1]:
            pathreq.loose_list.pop(-1)
            pathreq.nodes_list.pop(-1)
        # Then process user defined constraints with respect to automatic namings
        temp = deepcopy(pathreq)
        # This needs a temporary object since we may suppress/correct elements in the list
        # during the process
        for i, n_id in enumerate(temp.nodes_list):
            # n_id must not be a transceiver and must not be a fiber (non supported, user
            # can not enter fiber names in excel)
            if n_id not in trxfibertype:
                # check that n_id is in the node list, if not find a correspondance name
                if n_id in roadmtype + edfatype:
                    nodes_suggestion = [n_id]
                else:
                    # checks first roadm, fused, and ila in this order, because ila automatic name
                    # contain roadm names. If it is a fused node, next ila names might be correct
                    # suggestions, especially if following fibers were splitted and ila names
                    # created with the name of the fused node
                    if n_id in corresp_roadm.keys():
                        nodes_suggestion = corresp_roadm[n_id]
                    elif n_id in corresp_fused.keys():
                        nodes_suggestion = corresp_fused[n_id] + corresp_ila[
                            n_id]
                    elif n_id in corresp_ila.keys():
                        nodes_suggestion = corresp_ila[n_id]
                    else:
                        nodes_suggestion = []
                if nodes_suggestion:
                    try:
                        if len(nodes_suggestion) > 1:
                            # if there is more than one suggestion, we need to choose the direction
                            # we rely on the next node provided by the user for this purpose
                            new_n = next(
                                n for n in nodes_suggestion
                                if n in next_node.keys() and next_node[n] in
                                temp.nodes_list[i:] + [pathreq.destination]
                                and next_node[n] not in temp.nodes_list[:i])
                        else:
                            new_n = nodes_suggestion[0]
                        if new_n != n_id:
                            # warns the user when the correct name is used only in verbose mode,
                            # eg 'a' is a roadm and correct name is 'roadm a' or when there was
                            # too much ambiguity, 'b' is an ila, its name can be:
                            # Edfa0_fiber (a → b)-xx if next node is c or
                            # Edfa0_fiber (c → b)-xx if next node is a
                            msg = f'{ansi_escapes.yellow}Invalid route node specified:' +\
                                f'\n\t\'{n_id}\', replaced with \'{new_n}\'{ansi_escapes.reset}'
                            logger.info(msg)
                            pathreq.nodes_list[pathreq.nodes_list.index(
                                n_id)] = new_n
                    except StopIteration:
                        # shall not come in this case, unless requested direction does not exist
                        msg = f'{ansi_escapes.yellow}Invalid route specified {n_id}: could' +\
                            f' not decide on direction, skipped!.\nPlease add a valid' +\
                            f' direction in constraints (next neighbour node){ansi_escapes.reset}'
                        print(msg)
                        logger.info(msg)
                        pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
                        pathreq.nodes_list.remove(n_id)
                else:
                    if temp.loose_list[i] == 'LOOSE':
                        # if no matching can be found in the network just ignore this constraint
                        # if it is a loose constraint
                        # warns the user that this node is not part of the topology
                        msg = f'{ansi_escapes.yellow}Invalid node specified:\n\t\'{n_id}\'' +\
                            f', could not use it as constraint, skipped!{ansi_escapes.reset}'
                        print(msg)
                        logger.info(msg)
                        pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
                        pathreq.nodes_list.remove(n_id)
                    else:
                        msg = f'{ansi_escapes.red}Could not find node:\n\t\'{n_id}\' in network' +\
                            f' topology. Strict constraint can not be applied.{ansi_escapes.reset}'
                        logger.critical(msg)
                        raise ServiceError(msg)
            else:
                if temp.loose_list[i] == 'LOOSE':
                    print(
                        f'{ansi_escapes.yellow}Invalid route node specified:\n\t\'{n_id}\''
                        +
                        f' type is not supported as constraint with xls network input,'
                        + f' skipped!{ansi_escapes.reset}')
                    pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
                    pathreq.nodes_list.remove(n_id)
                else:
                    msg = f'{ansi_escapes.red}Invalid route node specified \n\t\'{n_id}\'' +\
                        f' type is not supported as constraint with xls network input,' +\
                        f', Strict constraint can not be applied.{ansi_escapes.reset}'
                    logger.critical(msg)
                    raise ServiceError(msg)
    return pathreqlist
Example #9
0
def compute_requests(network, data, equipment):
    """ Main program calling functions
    """
    # Build the network once using the default power defined in SI in eqpt config
    # TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
    # spacing, f_min and f_max
    p_db = equipment['SI']['default'].power_dbm

    p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\
        equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
    build_network(network, equipment, p_db, p_total_db)
    save_network(ARGS.network_filename, network)

    oms_list = build_oms_list(network, equipment)

    try:
        rqs = requests_from_json(data, equipment)
    except ServiceError as this_e:
        print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
        raise this_e
    # check that request ids are unique. Non unique ids, may
    # mess the computation: better to stop the computation
    all_ids = [r.request_id for r in rqs]
    if len(all_ids) != len(set(all_ids)):
        for item in list(set(all_ids)):
            all_ids.remove(item)
        msg = f'Requests id {all_ids} are not unique'
        LOGGER.critical(msg)
        raise ServiceError(msg)
    try:
        rqs = correct_route_list(network, rqs)
    except ServiceError as this_e:
        print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
        raise this_e
        #exit(1)
    # pths = compute_path(network, equipment, rqs)
    dsjn = disjunctions_from_json(data)

    print('\x1b[1;34;40m' + f'List of disjunctions' + '\x1b[0m')
    print(dsjn)
    # need to warn or correct in case of wrong disjunction form
    # disjunction must not be repeated with same or different ids
    dsjn = correct_disjn(dsjn)

    # Aggregate demands with same exact constraints
    print('\x1b[1;34;40m' + f'Aggregating similar requests' + '\x1b[0m')

    rqs, dsjn = requests_aggregation(rqs, dsjn)
    # TODO export novel set of aggregated demands in a json file

    print('\x1b[1;34;40m' + 'The following services have been requested:' +
          '\x1b[0m')
    print(rqs)

    print('\x1b[1;34;40m' + f'Computing all paths with constraints' +
          '\x1b[0m')
    try:
        pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
    except DisjunctionError as this_e:
        print(
            f'{ansi_escapes.red}Disjunction error:{ansi_escapes.reset} {this_e}'
        )
        raise this_e

    print('\x1b[1;34;40m' + f'Propagating on selected path' + '\x1b[0m')
    propagatedpths, reversed_pths, reversed_propagatedpths = \
        compute_path_with_disjunction(network, equipment, rqs, pths)
    # Note that deepcopy used in compute_path_with_disjunction returns
    # a list of nodes which are not belonging to network (they are copies of the node objects).
    # so there can not be propagation on these nodes.

    pth_assign_spectrum(pths, rqs, oms_list, reversed_pths)

    print('\x1b[1;34;40m' + f'Result summary' + '\x1b[0m')
    header = ['req id', '  demand', '  snr@bandwidth A-Z (Z-A)', '  [email protected] A-Z (Z-A)',\
              '  Receiver minOSNR', '  mode', '  Gbit/s', '  nb of tsp pairs',\
              'N,M or blocking reason']
    data = []
    data.append(header)
    for i, this_p in enumerate(propagatedpths):
        rev_pth = reversed_propagatedpths[i]
        if rev_pth and this_p:
            psnrb = f'{round(mean(this_p[-1].snr),2)} ({round(mean(rev_pth[-1].snr),2)})'
            psnr = f'{round(mean(this_p[-1].snr_01nm), 2)}' +\
                   f' ({round(mean(rev_pth[-1].snr_01nm),2)})'
        elif this_p:
            psnrb = f'{round(mean(this_p[-1].snr),2)}'
            psnr = f'{round(mean(this_p[-1].snr_01nm),2)}'

        try:
            if rqs[i].blocking_reason in BLOCKING_NOPATH:
                line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} :',\
                        f'-', f'-', f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',\
                        f'-', f'{rqs[i].blocking_reason}']
            else:
                line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,\
                        psnr, f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',\
                        f'-', f'{rqs[i].blocking_reason}']
        except AttributeError:
            line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,\
                    psnr, f'{rqs[i].OSNR}', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',\
                    f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }', f'({rqs[i].N},{rqs[i].M})']
        data.append(line)

    col_width = max(len(word) for row in data for word in row[2:])  # padding
    firstcol_width = max(len(row[0]) for row in data)  # padding
    secondcol_width = max(len(row[1]) for row in data)  # padding
    for row in data:
        firstcol = ''.join(row[0].ljust(firstcol_width))
        secondcol = ''.join(row[1].ljust(secondcol_width))
        remainingcols = ''.join(
            word.center(col_width, ' ') for word in row[2:])
        print(f'{firstcol} {secondcol} {remainingcols}')
    print('\x1b[1;33;40m'+f'Result summary shows mean SNR and OSNR (average over all channels)' +\
          '\x1b[0m')

    return propagatedpths, reversed_propagatedpths, rqs