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