"""Library query parser"""
import logging
import operator
import fnmatch
from ply import lex, yacc
from ..config import OpAmpLibrary
from ..format import Quantity
LOGGER = logging.getLogger(__name__)
LIBRARY = OpAmpLibrary()
[docs]class LibraryQueryParser:
"""Op-amp library query parser.
This implements a lexer to identify search terms for the Zero op-amp library, and returns
lambda functions that perform corresponding checks.
"""
# Parameter tokens.
parameters = {
"model": "MODEL",
"a0": "OPEN_LOOP_GAIN",
"gbw": "GAIN_BANDWIDTH",
"vnoise": "VNOISE",
"vcorner": "VNOISE_CORNER",
"inoise": "INOISE",
"icorner": "INOISE_CORNER",
"vmax": "V_MAX",
"imax": "I_MAX",
"sr": "SLEW_RATE"
}
# top level tokens
tokens = [
'ID', # Numeric or string values.
'PARAMETER',
'EQUAL',
'NOT_EQUAL',
'GREATER_THAN',
'GREATER_THAN_EQUAL',
'LESS_THAN',
'LESS_THAN_EQUAL',
'AND',
'OR',
'LPAREN',
'RPAREN'
]
# operator method map
_operators = {
"==": "eq",
"!=": "ne",
">": "gt",
">=": "ge",
"<": "lt",
"<=": "le",
"&": "and_",
"|": "or_"
}
# textual parameters (support wildcard comparisons)
_text_params = {
"model"
}
# ignore spaces and tabs
t_ignore = ' \t'
# simple tokens (don't need method)
t_EQUAL = r'=='
t_NOT_EQUAL = r'!='
t_GREATER_THAN = r'>'
t_GREATER_THAN_EQUAL = r'>='
t_LESS_THAN = r'<'
t_LESS_THAN_EQUAL = r'<='
t_AND = r'\&'
t_OR = r'\|'
t_LPAREN = r'\('
t_RPAREN = r'\)'
def __init__(self):
# Parsed search filters.
self._filters = None
# Order in which parameters have been queried.
self.parameter_query_order = []
# Create lexer and parser handlers. Set lex and yacc to not generate grammar files, for
# packaging simplicity, at the cost of a slight speed penalty.
self.lexer = lex.lex(module=self, optimize=False, debug=False)
self.parser = yacc.yacc(module=self, write_tables=False, debug=False)
[docs] def parse(self, text):
# clear existing filters
self._filters = None
self.parser.parse(text, lexer=self.lexer)
return self._filters
@classmethod
def _get_comparison_method(cls, comparison, parameter):
"""Get appropriate comparison method for the specified comparison and parameter."""
# check if this is a text comparison
if parameter in cls._text_params:
if comparison == getattr(operator, cls._operators["=="]):
# equal
comparison = cls._textual_equal
elif comparison == getattr(operator, cls._operators["!="]):
# not equal
comparison = cls._textual_not_equal
return comparison
@classmethod
def _textual_equal(cls, left, right):
# slightly abuse unix file path matching
return fnmatch.fnmatch(left.lower(), right.lower())
@classmethod
def _textual_not_equal(cls, left, right):
return not cls._textual_equal(left, right)
[docs] def t_newline(self, t):
r'\n+'
t.lexer.lineno += t.value.count("\n")
# Error handling.
[docs] def t_error(self, t):
# Anything that gets past the other filters.
raise ValueError(f"illegal character '{t.value[0]}' on line {t.lexer.lineno}")
[docs] def t_eof(self, t):
return None
[docs] def t_ID(self, t):
r'[a-zA-Z\?\*\d.-]+'
if t.value.lower() in self.parameters:
t.type = 'PARAMETER'
return t
[docs] def p_error(self, p):
lineno = self.lexer.lineno
if p:
if hasattr(p, 'value'):
# parser object
# check for unexpected new line or end of file
if p.type == "EOF":
message = "unexpected end of file"
# compensate for mistaken newline
lineno -= 1
elif p.value.startswith("\n"):
message = "unexpected end of line"
# compensate for mistaken newlines
lineno -= p.value.count("\n")
else:
message = f"'{p.value}'"
else:
# error message thrown by production
message = str(p)
# productions always end with newlines, so errors in productions are on previous
# lines
if lineno is not None:
lineno -= 1
else:
message = "unexpected end of file"
raise LibraryParserError(message, line=lineno)
[docs] def p_statement(self, t):
'statement : expression'
self._filters = t[1]
[docs] def p_binary_operator(self, t):
'''binary_operator : OR
| AND'''
t[0] = getattr(operator, self._operators[t[1]])
[docs] def p_comparison_operator(self, t):
'''comparison_operator : EQUAL
| NOT_EQUAL
| GREATER_THAN
| GREATER_THAN_EQUAL
| LESS_THAN
| LESS_THAN_EQUAL'''
t[0] = getattr(operator, self._operators[t[1]])
[docs] def p_value_with_unit(self, t):
'value_with_unit : ID ID'
# Matches a value with a unit.
t[0] = t[1] + t[2]
[docs] def p_comparison_expression(self, t):
'''expression : PARAMETER comparison_operator ID
| PARAMETER comparison_operator value_with_unit'''
# parse value
try:
value = Quantity(t[3])
except ValueError:
# assume string
value = t[3]
parameter = t[1]
comparison = t[2]
if parameter not in self.parameter_query_order:
# This parameter has not been seen yet.
self.parameter_query_order.append(parameter)
# change comparison method if necessary (e.g. text comparison)
comparison = self._get_comparison_method(comparison, parameter)
# create expression
t[0] = lambda opamps: set([opamp for opamp in opamps
if comparison(getattr(opamp, parameter), value)])
[docs] def p_expression_group(self, t):
'expression : LPAREN expression RPAREN'
t[0] = t[2]
[docs] def p_binary_expression(self, t):
'expression : expression binary_operator expression'
operation = t[2]
lhs = t[1]
rhs = t[3]
# combine
t[0] = lambda opamps: operation(lhs(opamps), rhs(opamps))
[docs]class LibraryParserError(ValueError):
"""Library parser error"""
def __init__(self, message, line=None, pos=None, **kwargs):
if line is not None:
line = int(line)
if pos is not None:
pos = int(pos)
# add line number and position
message = f"{message} (line {line}, position {pos})"
else:
# add line number
message = f"{message} (line {line})"
# prepend message
message = f"Syntax error: {message}"
super().__init__(message, **kwargs)
[docs]class LibraryQueryEngine:
"""Query engine for op-amp library"""
def __init__(self):
self._parser = LibraryQueryParser()
[docs] def query(self, text, sort_order=None):
"""Query the library.
Parameters
----------
text : :class:`str`
The query text.
sort_order : :class:`dict`, optional
The sort order map. If specified, the items in this dictionary are used to determine the
sort order for the returned results. The keys represent the parameter to filter, and
the values represent the order (standard or reverse). The sorting is applied in the
order that the parameter appears in the query text (left to right).
Returns
-------
:class:`list`
The matched op-amps.
"""
expression = self._parser.parse(text)
# Run query with op-amp set so we can use support for binary operators.
parts = list(expression(self.opamp_set))
if sort_order is not None:
# Sort the results in the order they were specified (left to right).
for parameter in reversed(self._parser.parameter_query_order):
reverse = sort_order[parameter]
parts = sorted(parts, key=lambda part: getattr(part, parameter), reverse=reverse)
return parts
@property
def opamp_set(self):
return set(LIBRARY.opamps)
@property
def parameters(self):
return self._parser.parameters.keys()