# -*- 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 qiskit_metal 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))