Source code for qiskit_metal.toolbox_metal.parsing

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# pylint: disable-msg=broad-except
# pylint: disable-msg=relative-beyond-top-level
# pylint: disable-msg=import-error
# pylint: disable-msg=line-too-long
"""Parsing module Qiskit Metal.

The main function in this module is `parse_value`, and it explains what
and how it is handled. Some basic arithmetic can be handled as well,
such as `'-2 * 1e5 nm'` will yield float(-0.2) when the default units are set to `mm`.

Example parsing values test:
----------------------------
    .. code-block:: python

        from qiskit_metal.toolbox_metal.parsing import *

        def test(val, _vars):
            res = parse_value(val, _vars)
            print( f'{type(val).__name__:<6} |{val:>12} >> {str(res):<20} | {type(res).__name__:<6}')

        def test2(val, _vars):
            res = parse_value(val, _vars)
            print( f'{type(val).__name__:<6} |{str(val):>38} >> {str(res):<47} | {type(res).__name__:<6}')

        vars_ = Dict({'x':5.0, 'y':'5um', 'cpw_width':'10um'})

        print('------------------------------------------------')
        print('String: Basics')
        test(1, vars_)
        test(1., vars_)
        test('1', vars_)
        test('1.', vars_)
        test('+1.', vars_)
        test('-1.', vars_)
        test('1.0', vars_)
        test('1mm', vars_)
        test(' 1  mm ', vars_)
        test('100mm', vars_)
        test('1.mm', vars_)
        test('1.0mm', vars_)
        test('1um', vars_)
        test('+1um', vars_)
        test('-1um', vars_)
        test('-0.1um', vars_)
        test('.1um', vars_)
        test('  0.1  m', vars_)
        test('-1E6 nm', vars_)
        test('-1e6 nm', vars_)
        test('.1e6 nm', vars_)
        test(' - .1e6nm ', vars_)
        test(' - .1e6 nm ', vars_)
        test(' - 1e6 nm ', vars_)
        test('- 1e6 nm ', vars_)
        test(' - 1. ', vars_)
        test(' + 1. ', vars_)
        test('1 .', vars_)

        print('------------------------------------------------')
        print('String: Arithmetic')
        test('2*1', vars_)
        test('2*10mm', vars_)
        test('-2 * 1e5 nm', vars_)

        print('------------------------------------------------')
        print('String: Variable')
        test('x', vars_)
        test('y', vars_)
        test('z', vars_)
        test('x1', vars_)
        test('2*y', vars_)

        print('------------------------------------------------')
        print('String: convert list and dict')
        test2(' [1,2,3.,4., "5um", " -0.1e6 nm"  ] ', vars_)
        test2(' {3:2, 4: " -0.1e6 nm"  } ', vars_)

        print('')
        print('------------------------------------------------')
        print('Dict: convert list and dict')
        my_dict = Dict(
            string1 = '1m',
            string2 = '1mm',
            string3 = '1um',
            string4 = '1nm',
            variable1 = 'cpw_width',
            list1 = "['1m', '5um', 'cpw_width', -1, False, 'a string']",
            dict1 = "{'key1':'4e-6mm', '2mm':'100um'}"
        )
        #test2(my_dict, vars_)
        display(parse_value(my_dict, vars_))


Returns:
------------------

    .. code-block:: python

        ------------------------------------------------
        String: Basics
        int    |           1 >> 1                    | int
        float  |         1.0 >> 1.0                  | float
        str    |           1 >> 1.0                  | float
        str    |          1. >> 1.0                  | float
        str    |         +1. >> 1.0                  | float
        str    |         -1. >> -1.0                 | float
        str    |         1.0 >> 1.0                  | float
        str    |         1mm >> 1                    | int
        str    |      1  mm  >> 1                    | int
        str    |       100mm >> 100                  | int
        str    |        1.mm >> 1.0                  | float
        str    |       1.0mm >> 1.0                  | float
        str    |         1um >> 0.001                | float
        str    |        +1um >> 0.001                | float
        str    |        -1um >> -0.001               | float
        str    |      -0.1um >> -0.0001              | float
        str    |        .1um >> 0.0001               | float
        str    |      0.1  m >> 100.0                | float
        str    |     -1E6 nm >> -1.0000000000000002  | float
        str    |     -1e6 nm >> -1.0000000000000002  | float
        str    |     .1e6 nm >> 0.10000000000000002  | float
        str    |   - .1e6nm  >> -0.10000000000000002 | float
        str    |  - .1e6 nm  >> -0.10000000000000002 | float
        str    |   - 1e6 nm  >>  - 1e6 nm            | str
        str    |   - 1e6 nm  >> - 1e6 nm             | str
        str    |       - 1.  >>  - 1.                | str
        str    |       + 1.  >>  + 1.                | str
        str    |         1 . >> 1 .                  | str
        ------------------------------------------------
        String: Arithmetic
        str    |         2*1 >> 2*1                  | str
        str    |      2*10mm >> 20                   | int
        str    | -2 * 1e5 nm >> -0.20000000000000004 | float
        ------------------------------------------------
        String: Variable
        str    |           x >> 5.0                  | float
        str    |           y >> 0.005                | float
        str    |           z >> z                    | str
        str    |          x1 >> x1                   | str
        str    |         2*y >> 2*y                  | str
        ------------------------------------------------
        String: convert list and dict
        str    |   [1,2,3.,4., "5um", " -0.1e6 nm"  ]  >> [1, 2, 3.0, 4.0, 0.005, -0.10000000000000002]   | list
        str    |             {3:2, 4: " -0.1e6 nm"  }  >> {3: 2, 4: -0.10000000000000002}                 | Dict


        ------------------------------------------------
        Dict: convert list and dict

        {'string1': 1000.0,
        'string2': 1,
        'string3': 0.001,
        'string4': 1.0000000000000002e-06,
        'variable1': 0.01,
        'list1': [1000.0, 0.005, 0.01, -1, False, 'a string'],
        'dict1': {'key1': 4e-06, '2mm': 0.1}}
"""

from collections.abc import Iterable
from collections.abc import Mapping
from numbers import Number
from typing import Union

import ast
import numpy as np
import pint
from pint import UnitRegistry

from .. import Dict, config, logger

__all__ = [
    'parse_value',  # Main function
    'is_variable_name',  # extra helpers
    'is_numeric_possible',
    'is_for_ast_eval',
    'is_true',
    'parse_options'
]

#########################################################################
# Constants

# Values that can represent True bool
TRUE_STR = [
    'true', 'True', 'TRUE', True, '1', 't', 'y', 'Y', 'YES', 'yes', 'yeah', 1,
    1.0
]
FALSE_STR = [
    'false', 'False', 'FALSE', False, '0', 'f', 'n', 'N', 'NO', 'no', 'na', 0,
    0.0
]


[docs] def is_true(value: Union[str, int, bool, float]) -> bool: """Check if a value is true or not. Args: value (str): Value to check Returns: bool: Is the string a true """ return value in TRUE_STR # membership test operator
# The unit registry stores the definitions and relationships between units. UREG = pint.UnitRegistry() ######################################################################### # Basic string to number units = config.DefaultMetalOptions.default_generic.units def _parse_string_to_float(expr: str): """Extract the value of a string. If the passed value is not convertable, the input value `expr` will just ne returned. Note that you can also pass in some arithmetic: `UREG.Quantity('2*130um').to('mm').magnitude` >> 0.26 Original code: pyEPR.hfss - see file. Args: expr (str): String expression such as '1nm'. Internal: to_units (str): Units to convert the value to, such as 'mm'. Hardcoded to config.DEFAULT.units Returns: float: Converted value, such as float(1e-6) Raises: Exception: Errors in parsing """ try: return UREG.Quantity(expr).to(units).magnitude except Exception: # DimensionalityError, UndefinedUnitError, TypeError try: return float(expr) except Exception: return expr ######################################################################### # UNIT and Conversion related
[docs] def is_variable_name(test_str: str): """Is the test string a valid name for a variable or not? Args: test_str (str): Test string Returns: bool: Is str a variable name """ return test_str.isidentifier()
[docs] def is_for_ast_eval(test_str: str): """Is the test string a valid list of dict string, such as "[1, 2]", that can be evaluated by ast eval. Args: test_str (str): Test string Returns: bool: Is test_str a valid list of dict strings """ return ('[' in test_str and ']' in test_str) or \ ('{' in test_str and '}' in test_str)
[docs] def is_numeric_possible(test_str: str): """Is the test string a valid possible numerical with /or w/o units. Args: test_str (str): Test string Returns: bool: Is the test string a valid possible numerical """ return test_str[0].isdigit() or test_str[0] in ['+', '-', '.']
# look into pyparsing # pylint: disable-msg=too-many-branches # pylint: disable-msg=too-many-return-statements
[docs] def parse_value(value: str, variable_dict: dict): """Parse a string, mappable (dict, Dict), iterable (list, tuple) to account for units conversion, some basic arithmetic, and design variables. This is the main parsing function of Qiskit Metal. Handled Inputs: Strings of numbers, numbers with units; e.g., '1', '1nm', '1 um' Converts to int or float. Some basic arithmetic is possible, see below. Strings of variables 'variable1'. Variable interpretation will use string method isidentifier 'variable1'.isidentifier() Strings of Dictionaries: Returns ordered `Dict` with same key-value mappings, where the values have been subjected to parse_value. Strings of Iterables(list, tuple, ...): Returns same kind and calls itself `parse_value` on each elemnt. Numbers: Returns the number as is. Int to int, etc. Arithmetic: Some basic arithmetic can be handled as well, such as `'-2 * 1e5 nm'` will yield float(-0.2) when the default units are set to `mm`. Default units: User units can be set in the design. The design will set config.DEFAULT.units Examples: See the docstring for this module. >> ?qiskit_metal.toolbox_metal.parsing Args: value (str): String to parse variable_dict (dict): dict pointer of variables Return: str, float, list, tuple, or ast eval: Parsed value """ if isinstance(value, str): # remove trailing and leading white spaces in the name val = str(value).strip() if val: if is_variable_name(val): # we have a string that could be interpreted as a variable # check if there is such a variable name, else return as string # logger.warning(f'Missing variable {opts[name]} from variable list.\n') if val in variable_dict: # Parse the returned value return parse_value(variable_dict[val], variable_dict) # Assume it is a string and just return it # CAUTION: This could cause issues for the user, if they meant to pass a variable # but mistyped it or didn't define it. But they might also want to pass a string # that is variable name compatible, such as pec. # This is basically about type checking, which we can get back to later. return val if is_for_ast_eval(val): # If it is a list or dict, this will do a literal eval, so string have # to be in "" else [5um , 4um ] wont work, but ["5um", "0.4 um"] will evaluated = ast.literal_eval(val) if isinstance(evaluated, list): # check if list, parse each element of the list return [ parse_value(element, variable_dict) for element in evaluated ] if isinstance(evaluated, dict): return Dict({ key: parse_value(element, variable_dict) for key, element in evaluated.items() }) logger.error( f'Unknown error in `is_for_ast_eval`\nval={val}\nevaluated={evaluated}' ) return evaluated if is_numeric_possible(val): return _parse_string_to_float(value) elif isinstance(value, Mapping): # If the value is a dictionary (dict,Dict,...), # then parse that dictionary. return Dict return Dict( map( lambda item: # item = [key, value] [item[0], parse_value(item[1], variable_dict)], value.items())) elif isinstance(value, Iterable): # list, tuple, ... Return the same type return { np.ndarray: np.array }.get(type(value), type(value))([parse_value(val, variable_dict) for val in value]) elif isinstance(value, Number): # If it is an int it will return an int, not a float, etc. return value # else no parsing needed, it is not data that we can handle return value
[docs] def parse_options(params: dict, parse_names: str, variable_dict=None): """ Calls parse_value to extract from a dictionary a small subset of values. You can specify parse_names = 'x,y,z,cpw_width'. Args: params (dict): Dictionary of params parse_names (str): Name to parse variable_dict (dict): Dictionary of variables. Defaults to None. """ # Prep args if not variable_dict: # If None, create an empty dict variable_dict = {} res = [] for name in parse_names.split(','): name = name.strip( ) # remove trailing and leading white spaces in the name # is the name in the options at all? if not name in params: logger.warning( f'Missing key {name} from params {params}. Skipping ...\n') continue # option_dict[name] should be a string res += [parse_value(params[name], variable_dict)] return res
############################################################################## # From pyepr, being used by renderer using comm port. # """The methods in this section were copied from Ansys renderer which used comm-ports.""" # UNITS # LENGTH_UNIT --- HFSS UNITS # #Assumed default input units for ansys hfss LENGTH_UNIT = 'meter' # LENGTH_UNIT_ASSUMED --- USER UNITS # if a user inputs a blank number with no units in `parse_fix`, # we can assume the following using LENGTH_UNIT_ASSUMED = 'mm' try: u_reg = UnitRegistry() Q = u_reg.Quantity except (ImportError, ModuleNotFoundError): pass # raise NameError ("Pint module not installed. Please install.") def extract_value_unit(expr, units): """ :type expr: str :type units: str :return: float """ # pylint: disable=broad-except try: return Q(expr).to(units).magnitude except Exception: try: return float(expr) except Exception: return expr def fix_units(x, unit_assumed=None): ''' Convert all numbers to string and append the assumed units if needed. For an iterable, returns a list ''' unit_assumed = LENGTH_UNIT_ASSUMED if unit_assumed is None else unit_assumed if isinstance(x, str): # Check if there are already units defined, assume of form 2.46mm or 2.0 or 4. if x[-1].isdigit() or x[-1] == '.': # number return x + unit_assumed else: # units are already applied return x elif isinstance(x, Number): return fix_units(str(x) + unit_assumed, unit_assumed=unit_assumed) elif isinstance(x, Iterable): # hasattr(x, '__iter__'): return [fix_units(y, unit_assumed=unit_assumed) for y in x] else: return x def parse_entry(entry, convert_to_unit=LENGTH_UNIT): ''' Should take a list of tuple of list... of int, float or str... For iterables, returns lists ''' if not isinstance(entry, list) and not isinstance(entry, tuple): return extract_value_unit(entry, convert_to_unit) else: entries = entry _entry = [] for entry in entries: _entry.append(parse_entry(entry, convert_to_unit=convert_to_unit)) return _entry def parse_units(x): ''' Convert number, string, and lists/arrays/tuples to numbers scaled in HFSS units. Converts to LENGTH_UNIT = meters [HFSS UNITS] Assumes input units LENGTH_UNIT_ASSUMED = mm [USER UNITS] [USER UNITS] ----> [HFSS UNITS] ''' return parse_entry(fix_units(x))