Skip to content

Instantly share code, notes, and snippets.

@derrickturk
Last active July 27, 2022 19:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derrickturk/3c97593399c008a95d76387af28b49a1 to your computer and use it in GitHub Desktop.
Save derrickturk/3c97593399c008a95d76387af28b49a1 to your computer and use it in GitHub Desktop.
fun with lark and PHDwin
%import common.SIGNED_NUMBER
%import common.CNAME
%import common.WS
%ignore WS
?start: expr
?expr: comp_expr
?comp_expr: addsub_expr [comp_op addsub_expr]
?addsub_expr: muldiv_expr (addsub_op muldiv_expr)*
?muldiv_expr: factor (muldiv_op factor)*
?factor: sign_op factor | atom_expr
// TODO: not equal?
!comp_op: "<" | ">" | "=" | "<=" | ">="
!addsub_op: "+" | "-"
!muldiv_op: "x" | "/"
!sign_op: "+" | "-"
?atom_expr: identifier "(" [args] ")" -> funcall
| atom
// TODO: I see some function calls(?) inside { } - what are these?
?atom: "(" expr ")"
| identifier
| number
?args: expr ("," expr)*
// TODO: we need to know the escaping rules here...
?identifier: (CNAME | "{" /[^}]+/ "}") -> var
?number: SIGNED_NUMBER -> const
import sys
import lark
from enum import Enum, auto
from collections import namedtuple
class UnaryOp(Enum):
Pos = auto()
Neg = auto()
class BinaryOp(Enum):
Add = auto()
Sub = auto()
Mul = auto()
Div = auto()
Lt = auto()
Gt = auto()
LtEq = auto()
GtEq = auto()
Eq = auto()
class Constant(namedtuple('Constant', ['val'])):
__slots__ = ()
def eval(self, var_dict):
return self.val
def pprint(self):
return f'{self.val}'
class Var(namedtuple('Var', ['name'])):
__slots__ = ()
def eval(self, var_dict):
return var_dict[self.name]
def pprint(self):
return f'{{{self.name}}}'
class BinaryOpApply(namedtuple('BinaryOpApply', ['op', 'lhs', 'rhs'])):
__slots__ = ()
def eval(self, var_dict):
lhs_val = self.lhs.eval(var_dict)
rhs_val = self.rhs.eval(var_dict)
if self.op == BinaryOp.Add:
return lhs_val + rhs_val
if self.op == BinaryOp.Sub:
return lhs_val - rhs_val
if self.op == BinaryOp.Mul:
return lhs_val * rhs_val
if self.op == BinaryOp.Div:
return lhs_val / rhs_val
if self.op == BinaryOp.Lt:
return lhs_val < rhs_val
if self.op == BinaryOp.Gt:
return lhs_val > rhs_val
if self.op == BinaryOp.LtEq:
return lhs_val <= rhs_val
if self.op == BinaryOp.GtEq:
return lhs_val >= rhs_val
if self.op == BinaryOp.Eq:
return lhs_val == rhs_val
raise ArgumentError('invalid binary operator')
def pprint(self):
lhs_pp = self.lhs.pprint()
rhs_pp = self.rhs.pprint()
if self.op == BinaryOp.Add:
return f'({lhs_pp} + {rhs_pp})'
if self.op == BinaryOp.Sub:
return f'({lhs_pp} - {rhs_pp})'
if self.op == BinaryOp.Mul:
return f'({lhs_pp} x {rhs_pp})'
if self.op == BinaryOp.Div:
return f'({lhs_pp} / {rhs_pp})'
if self.op == BinaryOp.Lt:
return f'({lhs_pp} < {rhs_pp})'
if self.op == BinaryOp.Gt:
return f'({lhs_pp} > {rhs_pp})'
if self.op == BinaryOp.LtEq:
return f'({lhs_pp} <= {rhs_pp})'
if self.op == BinaryOp.GtEq:
return f'({lhs_pp} >= {rhs_pp})'
if self.op == BinaryOp.Eq:
return f'({lhs_pp} = {rhs_pp})'
raise ArgumentError('invalid binary operator')
class UnaryOpApply(namedtuple('UnaryOpApply', ['op', 'expr'])):
__slots__ = ()
def eval(self, var_dict):
expr_val = self.expr.eval(var_dict)
if self.op == UnaryOp.Pos:
return expr_val
if self.op == UnaryOp.Neg:
return -expr_val
raise ArgumentError('invalid unary operator')
def pprint(self):
expr_pp = self.expr.pprint()
if self.op == UnaryOp.Pos:
return f'+{expr_pp}'
if self.op == UnaryOp.Neg:
return f'-{expr_pp}'
raise ArgumentError('invalid unary operator')
class FunCall(namedtuple('FunCall', ['fn', 'args'])):
__slots__ = ()
def eval(self, var_dict):
if self.fn == 'If':
# if has special rules - it only evaluates one or the
# other of its arguments!
cond, do_if, do_else = self.args
if cond.eval(var_dict):
return do_if.eval(var_dict)
return do_else.eval(var_dict)
if self.fn == 'Abs':
val, = self.args
return abs(val.eval(var_dict))
raise ArgumentError(f'function {self.fn} not yet implemented!')
def pprint(self):
args = ', '.join(a.pprint() for a in self.args)
return f'{self.fn}({args})'
class ASTBuilder(lark.Transformer):
def var(self, args):
return Var(name=args[0].value)
def const(self, args):
return Constant(float(args[0]))
def sign_op(self, args):
if args[0] == '-':
return UnaryOp.Neg
elif args[0] == '+':
return UnaryOp.Pos
else:
raise ArgumentError('invalid unary operator')
def addsub_op(self, args):
if args[0] == '+':
return BinaryOp.Add
elif args[0] == '-':
return BinaryOp.Sub
else:
raise ArgumentError('invalid add/sub operator')
def muldiv_op(self, args):
if args[0] == 'x':
return BinaryOp.Mul
elif args[0] == '/':
return BinaryOp.Div
else:
raise ArgumentError('invalid mul/div operator')
def comp_op(self, args):
if args[0] == '<':
return BinaryOp.Lt
elif args[0] == '>':
return BinaryOp.Gt
elif args[0] == '<=':
return BinaryOp.LtEq
elif args[0] == '>=':
return BinaryOp.GtEq
elif args[0] == '=':
return BinaryOp.Eq
else:
raise ArgumentError('invalid comparison operator')
def comp_expr(self, args):
lhs, op, rhs = args
# either just an expression
if op is None and rhs is None:
return lhs
# or a comparison between two expressions
return BinaryOpApply(op=op, lhs=lhs, rhs=rhs)
def addsub_expr(self, args):
# either just an expression
if len(args) == 1:
return args[0]
# or a sequence of left-associative operations
# (this looks a little crazy, but it's going to turn
# x + y + z into (x + y) + z and so on)
ex, *args = args
while len(args) > 0:
op, next_ex, *args = args
ex = BinaryOpApply(op, lhs=ex, rhs=next_ex)
return ex
def muldiv_expr(self, args):
# same logic as addsub_expr
if len(args) == 1:
return args[0]
ex, *args = args
while len(args) > 0:
op, next_ex, *args = args
ex = BinaryOpApply(op=op, lhs=ex, rhs=next_ex)
return ex
def funcall(self, args):
fn, fnargs = args
# we have to account for the way Lark handles [args], and ensure
# that we always end up with a list
if fnargs is None:
fnargs = []
elif not isinstance(fnargs, list):
fnargs = [fnargs]
# fn will be a Var, having been already transformed;
# however, functions can only have certain hard-coded names,
# so we just want the string
return FunCall(fn=fn.name, args=fnargs)
def args(self, args):
# function args should just be in-lined into the FunCall tuple
return args
def main(argv):
with open('formula.lark') as f:
p = lark.Lark(f, parser='lalr', transformer=ASTBuilder())
for l in sys.stdin:
tree = p.parse(l)
print(tree)
print(tree.eval({}))
print(tree.pprint())
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"attachments": {
"sphinx.png": {
"image/png": "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"
}
},
"cell_type": "markdown",
"id": "d0329b4e",
"metadata": {},
"source": [
"# Parsing & evaluating PHDwin formulas\n",
"\n",
"PHDwin implements a small formula language (similar to Excel, Spotfire, or a bunch of other tools) which can be used to perform custom calculations for economic arrays and product streams.\n",
"\n",
"While the PHDwin data format is tabular, this embedded \"little language\" doesn't lend itself to table-based (e.g. Pandas) handling - it's much more like a programming language, and we can make use of tools and ideas (lexers, parsers, interpreters, and compilers) commonly used in the implementation of programming languages to handle this formula language and other embedded \"little languages\" (such as ARIES economic or overlay lines, perhaps).\n",
"\n",
"There are many approaches to parsing, from hand-written [recursive descent](https://en.wikipedia.org/wiki/Recursive_descent_parser) to functional [parser combinators](https://usethe.computer/posts/18-irregular-expressions.html). For applications like this, though, we can save a lot of time by using a parser generator - a tool which can read a simple representation of a language's \"grammar\" and automatically produce or execute code to parse strings matching that language.\n",
"\n",
"We'll use a parser generator for Python, called [Lark](https://github.com/lark-parser/lark), to build a parser for (a subset of) the PHDwin formula language. Then, we'll implement a simple interpreter which can access per-well variable values and evaluate these formulas to produce new values.\n",
"\n",
"Lark requires us to write down the grammar of our language - the rules governing what \"atomic\" parts it consists of, and how they can be assembled into larger structures - in [Extended Backus-Naur form](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). Don't worry, it sounds more complicated than it is. Parts of the syntax should be familiar from regular expressions; the rest is mostly concerned with naming rules which build pieces of the grammar from other pieces.\n",
"\n",
"Lark parsers will read strings of text and either fail with a useful error message (if the string doesn't match the grammar) or produce an _abstract syntax tree_: a structured representation of the data, conforming to the rules of the grammar. If you remember diagramming sentences in school, it's the exact same idea:\n",
"![sphinx.png](attachment:sphinx.png)"
]
},
{
"attachments": {
"ratio.png": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABGMAAAD4CAIAAADHIbzAAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAADccSURBVHhe7d1Lbtw8s4DhrOVbhtfwI8vw0DMvI4DH3oJHZ+JZkKkH3kBgIJMAWUA2kCOxShRZvIhUX6yW3gcEkpYoiaTYVJXbly//AAAAAAAxMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTAgAAAACLTAkAAAAALDIlAAAAALDIlAAAAADAIlMCAAAAAItMCQAAAAAsMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTAgAAAACLTAkAAAAALDIlAAAAALDIlAAAAADAIlMCAAAAAItMCQAAAAAsMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTAgAAAACLTAkAAAAALDIlAAAAALDIlKB+/Xz47//+l5Rv33W/+vX37fn94auv8OPh6/vL97+/dbdnqv2fVNOdnlz068/kcPHnW35vz8n/e3/T1zGzV14+/pFXs9b+Ot/ffZPGkm2V56748JxUMCex5cfLr3JrG0fGcFfMtGRgLiQvG++XVE6KnVHX8fHxen93d/fFu7u7f3r9+NDdsY+noeLdU37nv/FMepIvw0lsrfHg+NiP16fh2nrEQC6tOz130S/3r/ry9X6sWWrDyczVclyzx/YOhiZnWjwO6VQh7lK4K39warFJbkgywlH68G3O3JrJ2Lry3oE0pdSW/N5x69gS3Ttb6n7PWK1p2KBtBhpNM7DzzF1vw9L9Vq5p0mXf467x0ZdW4WAAR0WmBNUQ1/7+/h7WCSPyMGqvVfsa5y3+opmgf5DJlNacPBPZuzOPpZYpNfZ3lkty8hnIv39vj1IhSeTWZkodI2P0ZkrhlshGM6UxOtMoaIjMHH2VD4umCC23LxNdxcHkWGHeEl56EMaIQygfnV/O7Ld9dqaUCVOj1uTCTH+6xVHKWdOk0XxEctncRX2lypV8ncwJfCvi48dDXO2kEU65+5n6DZWbG9YxA42lGdh75t63Yel+T4LR9od3jU/+TuXbAuC4yJSgJK4tflzgI/gf357/RHXkUxcfOk+B/rAlrvbnm8buQdQeBNO5SD3JlNaePI7s/77MWUQ5U2rsbyhOOYbUZTxDfkh9qlZMpUZpq0SxtW0jY8TNjpgLBUO6fL+kcmVGXYcGSOnX6t3XtzNh0RygpeGW7JtPNZ4iqjVGX37DdCZ77TlmDC5vYj53bCbeOxNztdRw/fnjgaHBY2vn9miQ6Ts2VdATfjzd+y5/vGrdpb4sNskyQ6Rt0gtri8xVtY7bEd+TyFzNNKi6Q64lVeadU/9LHesaq96G6W1pm4GGGV6j98xa31YfDhiPKLdC2VGdmO1d4yPbij0EAIdMCaoe12qI7D7QqNEEoPS5gX6QYiLvx/fCUSZTWnfyn5IX+aOmarK9kCm19jdmUw5JyTIpyu/nH2OTnn+6HpVzGNMqz27vHBmjN1NqvF9S+ZMzpWy8XKOh19OTO9CEZm5n7WRjhWm/xoZpdCfsbr3w9NLtvlwUZ66Wen2N90lzpwPiVyM5Yb7BbX1ZbFLMXjBtUrJFNrQ35cn9M59Bj9fttvd61rQbXR2rj1Vfw6bNhSu37C60pPfMsmFx3MtKY2i2d42PVD6hUQAOgUwJqhrXSmRf+/TDkWrFiHwkMf2UgfhAXP5jM5Mo8l5/8nD7/DlPnMbMlUeN/bXilKP8mZK0Z7y05DClDMe0apZrbcfIGN2ZUtP90jqFGXUd/bGQxFRjLDX/L6ARX/KFcTVeT68ml7bHR+LwUYO8KLi7XBRnrrYsPiA9vHhC/ZxkuSt9TbK3J3d0PIZyRNMV5pMF92j+b3KtccN0HbtTP9xquOpY1x1cHquehsmL6oWDk6Tczuy+3jNL/cJl2kQ9C5jt88vl8TlHswAcAJkSlMa+pkgoLElFIc4OuLh/oZqE9ZobhIF4JviOIu+TTi5Jy38/3CUkO6pkSq39tfQqYcmdJMxM5LqldCJqVSDe3j0yxopMyf+/fL+mCqb0j+oJysFeQXhAPpCavrko95Pr4xFT/ZZLR4FbHMX1N72PudqipL6En1PKKLvTsRrruLFq6EdXk5LhyY1XeEb5f+P5wwO1E/JDNeG55jONr/2lpwsFqt/pp9rGqqdhuRGxbE9C5eN7z9xSf0GpoWZ7ctXK+Ogr67R2AtgfMiWoWlxb/C4y46RkZqCZhr/Q+TKlaVdwhsVMabm/ls2Usq01J5eXhRzGjI8Xb/+cTGlQvV9a2ZaFdp5VKbwqyYZd6fGl36821vdxVnecai5+huiyxlxtSa41/gdSVNpYjVWd5a50NEmqRqfMtTA4o/x3uGH3wb0r5i9xU/TY+ezxbvdyvvJUO7aYLLaNVU/DWmZQfL5Y+fjOM9cu0qp0DrM9fimvSuPjd8cW+gXgcMiUoCSuzX+4IdH8cozbErK7OoVMyecz2ozuTKl+8rfHH0E+sJgp9cf0QcoRdyQg2UXQKt2SHfmkCyre3j0yxupMqXq/tHK2X1cioZANfaIAKdwpwWoYjcmWXOz04X+FdhSGzVVbosnognEU13T8CczVqnTAosrSdCtzPvcD++O+xb60NykaN5Ubr+CM+faWrpY0ZehDcO549/iquHM0jUDhYrPlseppWMsMyo3kpHx855mlVba+bFULJ8t0XJntSbWlG9dwaQAHR6YEVY1rJSDOB9OBhmoumPZhfRJ5DySmdxujyPssJw9UMqXW/lpxyiEdMSfR3mVKLtUpdaG7tfHIGCdkSoPS/dLKn5kp5WMh2aiCfRLb5ZSCKT2T7HYv5hitIQyTC05V5AB/gpZo9ATmahW5jsi2YaP/VEZ/Giff4IaxGDQ3KQjCZ7mjgzHU5vkfMKv/lrmFpkS73YugavbYhRMGpGZprBbOE+2un8mp3bTKDOw9c7a+bFTVc42ingXM9lI1Fe+WV4uXBnBwZEpQ9bhW9i59cDFF5IXPLv79DaJqJxd5TznMELu7Xw03N+ksJ/dqmVJzf2M25UjyEz1toaRNLXXBbu8cGeO0TKl4v6Typ2ZKGgzlo6E4EtSaBaXYKwi9xtNF1SRYLB2qqcW8O47iKnHqWZirFUkrbb3c1soZ2y7W2CSplo5M2qZwS2Y4811zFpoS7R5PE9bMHltqdKp+6Z6GTT0sVU9moJEZMq/zzNKu/Mlql5mVOm62d42PvGq5KQCOjEwJaimulQ8uhrjf/n2hIRAf/77QdKALu8e4uf1PHmUieA39xxI26TwnF9VMqb2/oTTlkAZPp5WMJZPPyNXTxKzUhXR718gYabM9c6FSe7L3Syp/bqY0hUNDQJT83ZcoTpLYLhNi6fFa7/U+/PGW8IOU8b/2cA0YM9eeflgmOMIEeW0B5GrmanlTpaQRflCyQzHsvg9HKRrBsqYmVWrpdbS5UYMG08upWdVWLTQl3D2eN6qYHlv9/KpvrDoa5kivhxO2zEDDHVtsSeeZtWNLb8OiUsfN9q7xkVeLlwZwcGRKUBLXJiUMoKfkYdqlIbgrQUD8+/t7eKq42vtbmAwUI++gPXGofZ6TjxYypUFjf2eZlEM/b3HZkUsnCplD/jvoSl3ItrZ9ZIwpyzJlbLO5UGVIZddQfAf9lrjkU7JLkohI3Dn6YqBxUi0qDAOqKT4MuT3jjkyANseNTnhlGzSaIC93pUExCOxkriZcY4dmyatw2AKyO7tzOjSzsyEebWjSNCym0iQZs+iqmREttSrblFmwO73v2ZEZ5U/XN1btDZt0zEBjaQb2njnsaeFtWFbquNneNT5hgwINUxXAkZApQTXGtb/+vDzK79qeKnx1n7rYD0PcBy9BsP7w9f3lexIiVyLvgUTwaWpxlpM3ZEqjxv6K7IczOrDvL9lcaCYfy8Sf/BRbVdreNjLGWTKlgblf2nFbrp8pjdxvYAhis+H/85fyJWQqR0hheC4nGl8PhpPIOdKAefYRHTIILz0zQd5SnHqibEgZpiWF6wfHuG7pxnkknPwgLVhs0mCMzsMf0U+Ufi+hE7a42qpsU2bz7sx9l52R+rV6xqq5YZHGGWi0zMDeM0v1uX70Nqwpddxs7xofeZUgUwIQIVMCAAAAAItMCQAAAAAsMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAABmyR+C+5/uAHAwZEoAAADK5Ei9Rc8CYBfIlAAAAJTJfC5U9GIAto1MCQAAYIFJda5T9NoAPgmZEgAAKCJq7+KH65pFrw3g3MiUgMMxj9ih6A4AiJm1Qoruw2nMqF6n6LUBtCFTAnbIPBoXix4GAAmzXJiilXAxZsCvU/TawOGRKQG3xzzSTi96XgAoMItGtmhVfAZzL65T9NrAfpEpAVtknkbnLXoNADm8Uxb5IaoUrYqNMbfpOkWvDdwgMiXgc5gHyRmLXgDAKrybuoTDVSpaFTfF3MTrFL02sBlkSsClmAfAGYteAMAF8HZbx4xbqWht7IW5v9cpem3g8siUgPXM2n3GohcA8El4P54iHL1S0ao4BnP3r1P02sAJyJSAIrPmnrfoNQ6PAcFm+cnJFD2FGcZs0ao4PDMxrlP02kAOmRIOzSyX5y16DZQxXNi4cIoyS09nxjNbtCpQZabNdYpeG0dCpoSdM8vcGYteAGsxnrgJTNQLMQObLVoVWMvMqOsUvTZ2gUwJN8+sUGcsegGc217HeWfdQchPV27xJYTDWypaFbgAM9muU/Ta2DYyJdwAs7icsegFcEV7vQW77BRC4S3mLl+IGeRs0arA1ZmpeKGiF8M2kCnh85k14rxFr4ENMLdmKLrj9u2mXzfd+CsI7zJjdWlmtLNFqwIbYyZqb9GzYAPIlHANZgk4Y9ELYPN2fON206999OLS/CgxVtdkhj1btCpwO8wc9kV3YwPIlHB+5g1/YtGT4pbt9Z7uqV+76ch1MFyfKBz8UtGqAHAaMiWcmXlctRQ9Enu043u9s67tpiNX40eMcftE5i5ki1YFgH5kSjg/85Qaiu7Awex4Guyva3vqy9X4QWPcPl14L0pFqwJAMzIlABex4xhlf/3aX4+uhnHbIH9TKkWrAkAVmRKAM9txRLLXru2vR4Dwc7tStCoAJMiUAJzTjkOQvXZtl50CUuFULxWtCgAOmRKA8zABx1B0xy4coWv6GjgAP+0rRasCODAyJQBnsO8IY8ddG+y4a0AL/xaoFK0K4GDIlACcaschxY67JnbcNaCXfztUilYFcAxkSgDW23cMse/eiX33DljNvzUqRasC2C8yJQAr7Tto2HHXvCP0EThd+E4pFa0KYF/IlAB0MyHCUHTHLuy4a8YR+gicl3/XVIpWBXD7yJQA9Nl3TLDv3hkH6SZwIf4dVClaFUt+/XwYhuvxT/Ty68/f+tr4883v/f5uxzwqP15+TWdLyrfv7mTmDF/fX77/dTsm6eFff3x7juuM/r49vz98nas9pKcauMs9ZA6PuYtmqrX31w/mrK2F7YNvLLbt2VXIHPv3xbVq7G99tNO9ruitvIQ1mdLilAp94p02clPz9/OP8Qz+nlVugDS4fHfnmpZWyN7Ft8dhl+vsrOOdNpe0Tm6EH8bZ9qcw9U/w8fF6f3d398W7u7t/ev340N2xj6eh4t1Tfue/8Ux6ki/DSWyt8eD42I/Xp+HaesRALq07PXfRL/ev+vL1fqxZagMqzKTSrXux794Zvd1cXHNC21n55ajp/A+P8QIo12oMCFYvqumT5ZQwy4xPexdaBr+JW/DdejsYltx0xZ3J2jsvvrHi3raFfTvsYOaKVkVBdmKHWyKXzJRciZavwuFh235/9+vMWMI38rBavmktx10uszzGXHw4lPjYQXt/46HraGH74BsNbdPAOz6zdlY21ke7fisv4QyZkiulW/6Zd9pIp6a0ITyqegOW7m5h6vhzlnYFJ+x9p5mS6V2+tD8Rl0WpjdBX+YejS1IK+/SpGTJpUZQohZcehI/VIcuKzk+mdDozi3Trjuy7d6nezi6uOYHtrPxTS8L6wVLs1+f4cpPlTGkq1UW18GRZufib8WnvQsvgt1hcqEO+cqbK9DSIHwcdC/s22YHNFa2KQGli59eZasSVvhdke1pfuLeGv8rwZhyvmy4U0RtWmje9a6Y3V/K1mD/f9L0cvIvjyxW4Dso5KzXr/Q23d7VQDpf67YNvlNqWfH6Q/9CiNNqyd/HqZ7Q6U6pNqdmn3mnDTE09Ve4JVLwBC3fXPZLtw0Y2usee3SWHz3lwV+/i7mTugmyJR/X3rz8vj67N7Q/FOn3SpV9UdB8zZR5q/tGYeW7KvvlU7guXYa0oUZrOZK89P2aDy5MpncjNzLno1r3Yd++yVvR3cc2ZbWjlH1bgh8efb7LcDfVNq+RaZuMsDghWL6pJj04Ks8zZurrgpYPc7uPp3q+6H6+SCRWX0zCtihOi/I6uhX37pltTK1r18LIT+/Fd3rPJJwadE1u259cru7hNwV75TecEIZ8uLEkjlX69xh9uL5eh8eHzT3fm4vq20N95e2cL9fDOwTdKbRtJe9zo6erXPNqyd/HqZ3R6ppSZUpNPvtNG2Gw5c/pgW74BvXd3mk9SObsrfNneu8W7kLmisun7evJM68g4NGF5etKcSDcLt7N2srHCtF8fp6Vnpt2tF55ekil1cTNzLrp1L/bdu5IV/V1ccyabWvnfvsfbzcKo12oLCFYvqrmezg/+3g6as3V1wcs1aaX6cuoXfZfnxGuw355sblzYb4u7QQtFqx5SfmL/0f/YgK1zYsv20js0Xtx6P1PSt3/t3STv8akLdi1NSf1x8ZS3f2lxWOjvtL27hf5w+U/j4Bultgkd5B+Z8ycH7vgzpc++04Zvtpw2W63lBlTubiFdcWNV2TU68Z3W+JnSRBpTGKhm8gTsyTfkMTc+5eb/BWRj+vmUChIluXT1eakn09aRKa0zTrmg6NYd2X0HS1Z0eXHNURtb+Q1zUf9S/mNP4s7s+7h6UU16elKYdUoXvKRJK+lnSuXVdF57gzV5/m+0NHcv7LdrvFNLRaseRmli+/9Hc7tzYusZTJlOKG/t7C6RP3z8EHjYGXzVo0je5roYxmtpRlhBLp1ZaZ16f6ft3S0MD9e+Nwy+UWrbRJe+9BmhV7RFRruwd6F3JznTzynlmvjpd9rQ9siXOQt12m5A8e5Ou6Y3QJQdVXYN1r3T4hIfLhUKczRuzErd6UZ4gDwS7dHT91fkfnh3PGKq33LpzBOYTKlLNLv2nibppmNY1+sbXfljtmZ4LV3/w2u1Z0rVRVXPbMvKMMuMW1cXPHOSNSRpcct1ZSkN1149Qn7ySDZFS3P3wr4P4y1bKlp11yoTe6Drj4+aLpopJe/H5PDxV7lMv21lXfxWDsBMfCgvC+tbvb/T9lMXmcbBN0pt86RC6SYGJRztdK8rC707yTkypWz7NnCnjajZ7/pt60bjDZBq+VkSTCDTr/Aos2vtO63WSKlQmqNu72mZUu8Dy9SXl+nxH0/Td6PHz9+xvn+EkildVjS1XNEde7Hv3i1a1/EbXfkjyQPetEH7WAgIVi+qcpWgnBRmmTZ3dcEzR60xZUqj8moar7267s/1o90HzZS88cYtFa26R/WJPb0LpsncObFle1pfBG/e+CqT6vul5S3s6kTxWzEAS9cZ2ZJtfL2/0/buFianbRp8ozpoI6mQnqd+YOmoyznxu+/yU2qwhTttTM3WNmeTpcYbUK3m25k2uGVXhauTeafl70J6CwJySH6gWskDyz7VpgehE+6Up2r4fJMtucei+wWx4874eTpXbXmgRhc0T9eW4w9rfHcERbfuyO47WLe674trziBddmTLJ678s1zalrQh7lpHplRbVEs9dbo7aM7W1QWv2qR27lf3jCttcTlNMpv4F/VEu7sX9h0b7s5i0ap7sTSxB/JGcBsvlSkN5Cr+5aj6fpF3XFTfcq31b/P4cob2MVNyq0S9v9P27hbmTrs8+EZ10EZSoXd1Kh11Oaf/nFJmSm3kThtBs/VBktZsvAH1atps9/O1po4+aDO7TnynZe6CXis71SRiKAxUM3nEmceabFTBPnm85ZSei3om2e1ezA/H7KVjcsGpihxAprRonGNB0a07svsOLlrd9xtd+Sf58+TaoL0YN8YBwepFtdRTp7uD5mxdXfCqTepTX47N2mtFu7sX9oMY7tRi0aq3rGFi+6936E9StE9s2Z7WF/HilllSFt4vUr/0xZq/wfvRsZcLyIVKpdSv5e2dLcyfdmnwjYVBmyr0rk6loy7n9EypOKVKJe18aVDs9s47bcTNzidLjTdgoZpOpvByk8quE99pyV1wFbKjoX0/fZ7JYy3/0IpzEa1ZUHqIBs/Q8XRRNXlelg79sLujxzGZUo6bfnPRrfuy+w4uOmUEbnTlF6X1MN+GQkCwelEt9VR1dtCcrasL3kKTepjV1ajvzS7NxerJwn5Aw11bLFr11jRN7IG+X8bSPrFle+kdahe35M2++H6R+sNJ+v/KS0je7JmlQBpgQtZBvb/h9q4WFvtbHXxjcdCkQu/qVDrqcs6RKdkptZU7bSTNziRLjTdgsZq2M9OY6aK5dnb1LumOHu6Hy7x0fo+nyqWIa8lTbkg6kj994XZMuYg83jJPNz1e673eh7/JQR+Kbt/4X3u4PjMz18782Y3c45hMaeYm3lx0647svoONThmBpTVnoyv/YDphZsUutSEbEKxeVItXmXR10JytqwveYpPKxj+nFCzU0SKeMmuvlezuWtgPzt/fbNFKt6N1Yk+7htI+sf0hcdEFLV3cpq816IJWaczkd/2PR4eLw/R+N+Xbd/eeLcSWEj3GjVzqb7y9o4WV/vqRrMTAYnHQpELv6uQbEBc7Mmd0nkwpmlKbudNGptlJstR4A6RabZa4Qcjf5squ7nda+S4U34qunCdNEvqcdO4cfTHwGdD8f0sOl53TIzLk9ow7Mk/H+dHp2D/lHl7PPI5zVxoc8Qls5wZp0q6dMgg3uvJPjbRFzl9qw8A/DnynVi+qlatM1gcxXV3wGppUEq75qpgnJWuvldndsbBDyF1Oi+6+Ee0TeyBvxvaJ7d8LcdFFKbO4+UPcFynqjfF+/X17fg/fvA9f31/8L23zCivJg/uai23GzK2x5qtC9f5mtre1sN7f0uAbi4MmFXpXJ9mblPK4nexMmZJv+vtL9ok4u+KdNrLNds8ndxL3nGu8AVKtOkveHn9kvrbqDLuqz9SOd1rpLozDq/2Ky9cf355zX1g9kfsNDMHzbPj//DXHMBfKkaxFnpRyovH1YDiJnKOQKDkf0SGD8NIzMqUsMz10677svoPtThyK21z59VvX0iLnL11LyCrql/rVi2r9Kt66IKarC15jk7Ly63RBf6Y0alzYkXIzR4tuAnCCNZkSgB0IH6h7fabuvoNdGAoAALqQKQGH4yPmfcfNR+hjO0YDAIBeZErAsYQR876D5iP0sR2jAQBALzIlLCPA2gcfK/uiO3AA3HQAAHqRKa13kMjDd3P3Pd238D5yK4+G+w4AwApkSiv5yGP3wUfY0913dq+4iQfHrQcAYAUypTV82HGQ4ONo/d0Tc++GojtwGNx9AADWIVPqE8YcRws7Dtvx2xXeMu7aYTEBTscAAsAxkSl18AHHYZ+aB+/+bQlvFvfryJgDJ/IDyBiGQ8FoADgCMqVWPCEEg7B94T3iNh0c0+AUfvR80R3HxoAAOA4ypSY8GELhaDAgW8PdQYiZsJofOgYwxcgAOAgypWU8ElKMyQaFN0WK7sBRMRlWC4eO0ctifAAcAZnSMh4GWQzLpvjbwU2Bx3xYwQ8aQ7eIsQKwe2RKwM0jXkGKKbFCOGiMWwtGDMC+kSkBN4wwBSXMii5+uBi0Xgxd3ff3cVi+/vytr72/L1/HEXt4/qsbSn79fMhXq53h7dHdjsc/4wt3Bv2/+PXnmzvWte395ZdujkjLi+XHy3ND1+TSYfn645tvcLrXlW/fdf+uvN5/qbl7+vj37+Ppbvjv/aseIi/n1zGzV19ahYPRjEwJuFXm6aJbl2SfTNmnqbzMPAXFn2+lve7A/OPfPXqXI4OBeUgPz/Lv5aNKV1x80g/xgen17O/b8/uDDyaG89fbsDVTs4lcl/mxYsRWYwDLfj//GIclXmSiTKZKa/73/qYbQm4dlqUsoFf063N2bY+KPcOoYf1c7lrmWq5U95Ip6SE++XE7Y/50ZEoXRqYE3J7kudIRmmSfTOFDK32axk/BSTFTqj3XV2dKrpQOLF5xbab0+/t7OEphvjTkbLl4ZWN8a4eim5ATDpQU3YF+DGORfsbiEwCbydRILjSW/AKoC3WwLpW2TKucLJjf/Nd9xs+XcplSKLdOOktdSw78/V2aFyy/TeOwLyYj8kqZ0iCqnNsh2zI5FU5DpgTcmPEZExTd2qz4wEt2yUtXck/oUqZUfa53Zkq+5pC6jOfMP1CXIglR6ni6fcqvHh7/RJebv19l88mSa/9Y9DVy/CgxVufCeBbJMuXSA11hmpYRSTy+Pf90h+cPiZOT4EJetMq53KY3OSmtn6Nq13IHulSNTKk5U7p/krzIb9aPk3Q7mdKFkSkBt2R8CAVFt/aoPPDMLn35LnlI8u0Q7umYPOEWnutx/lNja8pXLjOBQkskMSp13G7XvKv07R/RN5Zsk2u/Ft2EWDhEUnQHTsbAlkgW8fXHuOCYTKZIlqNxWZs+CNIdhux9eP5jPuFR8Sqnlc1XgupK66eodC05kM+URn2Z0vBSkiOXBs3/jWuTKV0ImdIhyGIUlvRHKvMrYD4aHrmjiiFv6adF05a4Ulr9MUsGbWUIUrndZpd/qXfNPNqzc2Ppub42Uyp/ptQaSRQ7Hm/Xr85mx0fJFRsDnc8wNs8VfY2YHx9G6UIY4QJdXtofeeEyKCtVManQ74Kb64fs6jc9oNt/9rK0fk6KXZMDkzLkaeW9G15dz6U7U/IfJN2NW3RTLlOyyJxOdtuZUvoeIwHIKjRPB2fVQElUmv8SfuZy08K38YHaLDNiunWV7C3IzoTwpR4VPsByc2Pxud6ZKcUl9/hsjyRM77x4u5vYC89p/RRrs5NWh4sINccPDkN0UYxzVn2NsswH6fYngowpV8ktX5nV7+/Lo+Y2D48N7Smtn16pa7I9KA9DnDb/iJTd68rCCrwHKzKlORXyyU9+d4xM6WR7y5SkhLFd/o19sAQgGYrMx999A+W2u5JGvdOX9v1SGPy0qFyr9TkBZxpqLbp1rexczb5lzEtNXfxbI50bDc/19ZlS9tnZE0mU5nm8/eYzJT9i+hoBPziMz3Uw4IasNo1PQFkDwyVLtmQPlzNrSQKYwuo3RALTh0tLTSqtn16pa/UDS0ftnsmIPLM9qfZ6fxekPrlMiczo7PaQKQXvQBKAvNxQzBFh/0BppJj/yRAXrZaGYuMDtTXj8AZFt56mcrvNrqSmfs1Sb18yN1qe625Le6YkNePrBlqu6JU6Hm9vyZSmpUBfbsvQFyn6GgEG5/r8mDPsA1lt7AI1Bx5jidaibEkXKDmD255dA0ur30i/wLSwLNfO4OS7tnRg6ajdW5spxciUrmJnmdKABCAjGYrTUkpZlMfxyYaMsjH/06IbH6hNGQc5KLr1ZJXbbXblaurDe9xo50bTc31VpjTQSRUf2HRFr9TxeLssAtUWBkHJ1mj3XdFNwGdjWnqy2uQfqVORtUhqlkq0jtnP0vVLS+EjuLT6ifpesVgn37W2Sx8wJCBTuiF8phTYbwIgzUtK9Ucqg2L6FYaw+Y6Xf1o0f61NBp2fyI7PWcMLuQXZ94XZla/pvwDpvqDgb33+zk7Fn2RtppTJTxqv6JU6brdr1FL4yOivpmfpebbAtXws+hrYBj8zDz4/ZbVpCBWyQchI1715JcyuSHZjvMq9Pf749uwjGV3TFpbl0vrplbpWP7B01O6RKd2QXf6cEgmAlTSv4UcqgxKNgwTK/gM3+9WsSeGnRTc+UFtgBke3no/cguxzy+wq1gy+AjrNjdbn+vpMSbf49rRHEqrUnXS7XGicuvGXRbb/95Rcs8eir4HN8JPzyPNTVpvlxCD7xVwVfe4ti1Wmpv+SlqsWr3KaR0Vl6SlcWj+9UtfqB8repDQ9I27a5TKlBKnTqXaWKZEA5LUsVfm9yXodR6uj4kqd+2lRuVauMkbD4IRFt55V5XabXYs1h6K3svO5npRMwuNqxs/L8F3ZfEWv1J3c9t/f37WDrjzINJby9f1tm4m9b6G+BjaGKSqrzdITcOl7gOVrVcNCJOtw6Qs3+pWdccG0q9zft+d3v6w9tKxppfXTK3WtfqB2wRYypfxLi0zpKvb3c0qz2t6DJQDnG6jc16KklNLC3Fe2NjtQnygazEtGEpXbbXbVp03wBml9rg//PSlTmpr03/tL+xW9UneK26NgYrxc+98e+RRTO8mUAAA4DzIlsfME4FwDJTVLpeX8Gx+oz+LHUIpuBZoxfwAAODsypdHuE4AzDVTjT4bUflp04wN1feO4xUV3AD2YPwAAnB2Z0mD/CcB5BipOL2Ph91/lPqCbPprTIbWl/G1Uu2bGQbcCnZhFAABcApnSIRKAcwxUx8+iVH5adOMDdU1mEHQr0I9ZBADAJZApkQDg2pJ5QoCLkzCRAAC4hNvOlICb44NaQlucxTanU+6XHI5/xWH+BmYlX6vKfPOz+b5oc8L67yF0X7rKfKGq8KsXp+K+R6D4Jba234Uohxe+SaH+LQzXs2Ic9KuBhV9IbfbqS1uyd7l1AnQNbNj4ls4CQAmZEnA95iGtW4ETbHM61cLTKDZdmSm5UvrQfvpO6SSsX5spLfx9La3l+CRh6dsZPtP6TCnbePnOi6FcPlMaSsvAho0nUwJwCjIl4BrM41m3AqfZ7KSS8NTkG7/+TH+5ew5PuzIlnxoNqct4nnzK4QP36vc/m0zAS7dPofbDY/yB2C/9y55RShYkCbmrbyNTCjWOQ9CvuLL+wQxX4kyprZvdmVLLwDZ2CgAWkSkBFzc94LXoVuBkm51UuUxJSGTsg9p1mdIUoCefGvkTPv90+VKmgmoNpjXvyrZwoJ9f+fp6+HvhqBvPlB5/Sl7k+zV1X7ZfPlNqHNjGTgHAIjIl4ILcQz0qugM4h83Oq3KmNCU5+rHSukyp/JmSJDZjyB4dnmoLpjWvq8XWcsXpUzJ/uPzHfnPXrWdKw8ugv/NdiBNXqXyRTKltYE3jvdJ2ACghUwIuZXycB0W3Amey5dlVy5Q0OJa0pytTiouJlZ0woarH623BtGtD7kKBqAvh4fL/6PAdZEr+XvxwvZPsKJcp2ZIfxu5Myf+/MrCm8V5pOwCUkCkBFzE+yIOiW4Hz2fLsqmZKYT6zNlPKht3mW/LkZf7kjcH0SZnSQJvtm7SLTGnqcjAy182UBvWBbewUACwiUwLObHx+B0W3Ame18TlWzZTC4LgrUwqTq0y+kV5UtmQzk7ZguiVTqgX0trU7yZQGb48//DdD5jOltm6uzJTqA9vYKQBYRKYEnNPwGA6LbgXObeNzrJYpSVSt6cdSoDxlKUGmNJAYOojUR7IxV3KpTlswLc0zF4q5GN1fIndabdi4cT+ZUuxymdKqgW3sFAAsIlMCzmN4AJuiO4AL2Pg0K2dK8UcBlZqSgUzBd5wp2fxkIEFwqbQHzXa7XCgfyg8ZQhCsO/nT6vcBPshv5CNTCjVOgK6BbewUACwiUwLOYHyiB0W37tzH092XL1/uX/Vl6vV+2J9x9/ShNcaT3I9nGbfeB5tjH6/3lb0HtP2Zlgt/f49/gMilSdGHPJqKPDz+fJs2/v71U/9UkT+DzZTsJSRjyeQzEhynHyu1B9NyobGFzX9PKT3t1M2xkClF2iZA18A2dmqX3LIbrrE5H6/Dwqsr7+huWGBfy4d8jEtwWp8lGYdApgScyj+npejW/VubKc1HyBkCuee7r1S50tFsf7JN2UWuJEnL9/cxfk3L1yADSTOl6fMElx25cLkQmue/g64nmP4dt/BBEyRXhkaG3anE4rJrPOQYmVJSit/E2DIBKg3wlyNTGixlSmPSIyuqE+Y/X+4yi2xYf8iWRvqKNRnHQKYErCePZ19061EsZkqWeYRrCnQvX8ocnsfjK/OEn3Opu9qXPA/lJqZcNlP6+uPbc/yxjPfrz8uj/NbpQs1MpuRD5PeXhZ8mko8dgrB70BtM//r79vwe5kgPX99fvidXrMfiMixkSqnFCdA1sI2d2qV6pjR9+WrIieKV1udD8Yqu9W314YDxCDIlHMHOM6Wuj4xdTFZcX6Kvq+z4G4HaPpQ3MXJ9ad6r4YkbFt16IJ2ZklSfp4k8gsPDky3+qX60uVV14CkHYEHtcawramnRTnbLBtZfHNuOM6UotRH6Kr9OTEFZbt/8hW1vh2tHOGKD2ofyZEo+Wj1wzNqXKckbbK6dOzqeSLW35JEde9YBqCk/jjWQqS2osujq0VL/WE92ILXbTEmDrPaPjKeoLLcuyL75VO6Tqr2tHtMA2CGb86dg1I6cKfk49fDRalemlMyR3KQJz6hP9dbzHwUTD0BF+XHc8qAO1+BDPdiBop1mSu4N3vMO19Xh6ckdaGIzt3Pny4WMWDEqtbvD1XRwnAXVx6lEq8ksqJGq0RTJTZrgjPLfIXG/nzL1+i9nOgrmHoCK8uO45UGdrMFNCzywZ7vMlDJR2QJJBMYVYf5fQDamn0/thkaltSVRx0AH1SyhLQvwrfMRqi+647jaH6S5t1Vu0gRnlEMSLVfbL6YfgLry47jlQR2s1flISqMFsfOnPuDsMlNqWQ4i4QH5xWH6DrSdflm7ZcSisDh6sWLEb00YoRKkTswsKAsevrPc0cFEkmPmL098vLr6u55mi5iBAOrKj+N8dBPRdVeqZOvLRnXo5RiHscdMqTl+U6a+rgPJ8U1/IfNGlZfWWTRMZsxajr9dPjwlSI01vtOkWjo95JkcHh5uycyp9IBDYRICWFR7HMsaWlpEP+xujYayJ6tdBtiV/WZK9h2sb3kR7kzDL9mSWwLcr9Aed+5sgWhZ86JxMjHyXtfMMDYlPE2YWVBQrqXvSf3YSJ/SfhpNL/2HSlJ7h9OsEfMQwKL641jW1aFCy69u8stuprrbcdzlGEdynJ9Tmt7wTrBvWjdSpTVghxFbdsRicRgrB+w7U/KBKeFpgZkFwj1vh8eqvtSJk0uUBsm7L5pEmffm3iZZD6YigEWZdXPk1+A5J3LsnwNJVtgwdjJ/b+XQCzKOY5eZUjWXiYP6cA1IFcK7fIR423RxLfSp8KG8fxkP6s3zIakvugOR7PvAZErjU7n6K/Wr39U6f4g77M49xA+D2QigxVKmNPpo+xPznlSf6w//51eR4jD2mSmN4Ze8pxc+MpYlxYR6Iz1e6w3BXrAq2O8S2olpeU2HbIpVg3HacaYUhqREpdgIJiQAANe310xpMCVLo/ED48xHxrUAXw6XnVMSEdpbojTo+FA+lymlMinoxvl4lKgU28GcBADgU+w4UxpVPzIOc6EcCf8l2o8+qt7ztwE1fii/v0wpDEaJR7EpTEsAAD7FzjMloIWPRIlHsUHMTAAAPgWZEo7Oh6EEo9ggZmYf9ztE9FPtXX/+DwC4AjIlHJePQYlEsVnMzy7hz6eqHf5M6abpLSh883Vxb9s3fgPAlZEp4aB8AEoYis1iivb6eLr3HyPprzolVboun6xmxt3/SGuUKS38MiGtBQCfgUwJRxQGoMSg2Cym6IlcZE6mdFXhx3pxQpTfMWVPTX+gAgCujEwJh+OjTwJQbBwT9RT6mRKJ0nVJQnT/JHmRz3I0IdLtyeZCMrSwGwAujUwJh0P0iZvARF1v/pyCNOnaNFMachu5Cy5Tnf877/Z1q4lQcBIAuD4yJQDYIjKl9aZMaUSQfV1hKqT3QX7ySDZFmZLbv3CDogMA4MrIlABgc3yaRKa01sf0gy6kSlcVZzb6qdF8E8iUANwUMiUA2BzSpLOQKJtU6ZqSzGb8C1fzHejOlORzKTIlAJ+DTAkANodM6Sz4POL6FsY82i0vqqmSJEokuwA+CZkSAGAfxj+nNP+9UonDibKvqydTmhKhUvWP+m4AuDgyJQDAPmhuFCJPurK+TMnnSvw9JQCbRKYEANiJj9ene/lNa4M7G3zjCnozpTAncvz9G3ELAXwuMiUAAHAe/ZnS6CPKcQdDmjt/HyUAfBYyJQAAAACwyJQAAAAAwCJTAgAAAACLTAkAAAAALDIlAAAAALDIlAAAAADAIlMCAAAAAItMCQAAAAAsMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTAgAAAACLTAnb8uvnw3//97/HP9HLrz9/62vjz7dwb3js9/f/Df8vlh8vv5Jrhdyuh+e/+jLkzpzfZZg2fH1/+V4+qnTFkzry9+35/eHrXP+h3gYYt3kHfz//GGp++64vQ2+P40l0l1z0Qm+u8ZA/33zLh2bLRgAAbgiZErbFhIzyMtwSuVSmJAHl//57f9MNAXfmNZmSK6UDi1dc25Hf39916FwJo+0hbM30C6nbvIN9mdJQLvfmSrcDAHBDyJSwLSZkDOKtXIRaDuZCvdvltK6UPiJoz5R8zSHwHc+Z/xJ+9Ypee0emWPbh8U90ufnL/CRLDW7zDnZnSvk2n/Tmmi40nXZsNpkSAODWkClhW0zgpS/fJQxNgr+LZEoaaD7/dBfNf0RQi4Y9W/PvyxjjZgLchSt6rR3RqD0bKw/004/0PDBu8w72ZUoXeXO5gcqnlAAA3A4yJWyLCbz8S/mP/QaeS2RKEqSOwW4UVnprM6XyJxJLV/TaOiKBcqbaTK7I1/iX3OYd7MyULvLmkgvZD8QAALgtZErYFhN4hS8z8dxJwVx+exgcSwUTGcfRc40E1lHJhbaLV/TaOuKC1IUsqBJMY3abd7A7U/L/P+ObazhcvkuQXyICALhdZErYFhN4mZcaufrvbjp7pmS+vUpexkFnGBbX2Tg7G/s2XNFr6wiZ0tnc5h1ckykNzvvmGv19eRxbMpSHx0LqCADAlpEpYVtM4JXEYRIFTgHcuTMlCRbTLf4SA7elPVOSmnGzAy1X9No60hJnRxEzSm7zDq7MlM775pr8/v5z+nApNyAAAGwZmRK2xQReuThMoj238cyZkp45U4KwNYieF8Q15eTmwKYrem0dkXi32kI3bkuxOG70Di5lStNRucZod8aN58mUHP2crdojAAC2h0wJ22ICr3wc5gMv96vGzpUpyctS8dXWZkqZ6Lbxil5rB+VCpY+M/gbRMKpu9A66Zuf2ymmn76/LN+ZMby6jsRoAAJtCpoRtMRFVMcDSUHIsZ8qUJPrMxKZSzcfH6zMlG8K2XtFr76BcaLg6f0/pFLd6B/Xd8fD4821qwO9f03fB+auUGnOON9fb449vz77lmto1vWsAANgOMiVsiwm8isHctGso58mUXHToTxWLvhtqCmFNyYTLaZwd/bh/8xW9ng7+/v4+bpzKg4bXrnx99wE0am73DsbHzmU4UGuUGzOQXWP91ZlSfN2xJHkjAAAbR6aEbTGBVyWYG0jSco5MaeknQ+Sr7C7KPClTmi763/tL+xW97g7+fXt+DyPsB35lc5ebvoO//rw8/nBNdeVr+CGPU7qoOPHN9S9q+dBsknMAwO0hUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTAgAAAACLTAkAAAAALDIlAAAAALDIlAAAAADAIlMCAAAAAItMCQAAAAAsMiUAAAAAsMiUAAAAAMAiUwIAAAAAi0wJAAAAACwyJQAAAACwyJQAAAAAwCJTwsF9PN19+fLl/lVf5ny8Pt2PlUZ3d/evH7rd+3i9v/MV7p/CCuGu/MEAAADYIjIlHNxipvR6L2lO4O4pzIXcCWL+dJmd0cFAhk6bwqzM7x23jnMrmXLk5wAArESmhIOTwLKeKc2fE31o3uTTHY1LfTA6VdATfjzd+zD141Xrkiqhzmc7mbniM/d4zk6J0nxshEkHAEA/MiUc3HKm9Brvk0h1OiB+NZIT5iNTV5ugFQvCbCeamtUdMrHshJ4S9Kg6AABoQKaEg7OB5aL4gPTw4gk1ZCVRwhKdQ0/un3kuTZ9XynY76XRidcxIAABQRaaEg+sNI5P606dKkv/IbpsNaYg7fhsfaRIWzZNMZo6bT/N/kzk4bpjmnN2p3w/aPsMBAIAiU8LBJVFnnQs7TSL06n8znrB50pwpjdK9QCyclDp35DcoyqYkGQoSJd0ZuYt+HSMAAGhEpoSD68mUNAiNKodJ0Cxzvo/xF4aP+0iVsCCelFPuY5IhP8fGl/OkmmrH+DQTAIB+ZEo4OBN1VkjNOM/RsDT4or1+s1M+HcqdArCSSTn+Va551sS7x1fFnaMpRW+Z5AAAYEamhINLA8s8SYBsvdzWyhlbL4ZjW5gn0W73IqiaPZaJBwDACmRKOLimGHKqlHwWJDuKnymNf04p2DVVTk4DhBYmZbR7nG9hzeyxspGZBwBAFzIlHFw2sHTfrnQ3bZQqCdmd3TkdmtlJtIpF2Uk5C3fbRClzLH/yGACAdciUcHDZoDTMlOQzogx/zBCJzr/97m44bA5I3a5pX7wLKGnPlJJESXdmFE8HAADyyJQAYFuaM6U0UcplSqToAACsQqYEAAAAABaZEgAAAABYZEoAAAAAYJEpAQAAAIBFpgQAAAAAFpkSAAAAAFhkSgAAAABgkSkBAAAAgEWmBAAAAAAWmRIAAAAAWGRKAAAAAGCRKQEAAACARaYEAAAAABaZEgAAAABYZEoAAAAAYJEpAQAAAIBFpgQAAAAAsX///h+Jfgh17BaEigAAAABJRU5ErkJggg=="
}
},
"cell_type": "markdown",
"id": "c60bb6d1",
"metadata": {},
"source": [
"Here's how an ARIES line might break down into a similar syntax tree (sorry, ARIES happens to be the example I had sitting around - but we'll be parsing PHDwin formulas shortly):\n",
"![ratio.png](attachment:ratio.png)\n",
"\n",
"In Python, each node of this tree might correspond to an object - e.g. (with appropriate enum and named tuple types defined):\n",
"```\n",
"RatioLine(\n",
" keyword=Ratio(Phase.Gas, Phase.Oil),\n",
" initial_ratio=1.283,\n",
" final_ratio=1.706,\n",
" units=RatioUnits(Unit.Mcf, Unit.Bbl),\n",
" second_point=RatioSecondPoint(\n",
" duration=3,\n",
" units=Unit.Month\n",
" ),\n",
" yaxis=YAxis.Log,\n",
" interpolate=Interpolate.Time\n",
")\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "fe13af5a",
"metadata": {},
"source": [
"## Lark example: address book\n",
"\n",
"Let's look at a simple grammar to get a feel for how Lark operates as well as the EBNF syntax. We'll parse a phone book whose lines look like:\n",
"\n",
"```\n",
"John:555-123-4567\n",
"Sally : 555 321 7654\n",
"```\n",
"\n",
"Each entry consists of a single name, a colon, and then a U.S. phone number with an area code. Components of the phone number can be separated by either spaces or hyphens.\n",
"\n",
"We can begin our grammar by importing some helpful built-ins from Lark, for whitespace, newlines, \"words\" (one or more letters), and digits. We're also going to tell Lark that we want to ignore whitespace in our entries.\n",
"```\n",
"%import common.WS_INLINE\n",
"%import common.NEWLINE\n",
"%import common.WORD\n",
"%import common.DIGIT\n",
"\n",
"%ignore WS_INLINE\n",
"```\n",
"\n",
"Every grammar needs a \"start\" rule, indicating the top-level structure of the thing we want to parse (in a programming language, this would typically be the rule corresponding to an entire source file). We'll parse a phone book of at least one entry, with entries separated by newlines. (The `*` zero-or-more operator is the same one familiar from regular expressions.) The newline after the final entry is optional.\n",
"```\n",
"start: phonebook\n",
"\n",
"?phonebook: entry (NEWLINE entry)* NEWLINE?\n",
"```\n",
"\n",
"A rule reads left-to-right: `thing: recipe` means \"when you see `recipe`, assemble a `thing` out of it\".\n",
"Lark uses prefixes on rules (like `?` and `!`) to control aspects of the generated syntax tree, which we won't go into here.\n",
"For details of the rule format, the [Lark cheatsheet](https://lark-parser.readthedocs.io/en/latest/_static/lark_cheatsheet.pdf) and [documentation](https://lark-parser.readthedocs.io/en/latest/) are helpful.\n",
"\n",
"We now need to write rules for how a phonebook entry is constructed from parts:\n",
"```\n",
"?entry: name \":\" area_code \"-\"? number_part1 \"-\"? number_part2\n",
"?name: WORD\n",
"?area_code: DIGIT~3\n",
"?number_part1: DIGIT~3\n",
"?number_part2: DIGIT~4\n",
"```\n",
"\n",
"That completes our grammar! For this small language, we'll load the grammar into Lark directly as a string, and apply the resulting parser to some example data."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "5a3411aa",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Tree(Token('RULE', 'start'), [Tree(Token('RULE', 'phonebook'), [Tree(Token('RULE', 'entry'), [Token('WORD', 'John'), Tree(Token('RULE', 'area_code'), [Token('DIGIT', '5'), Token('DIGIT', '5'), Token('DIGIT', '5')]), Tree(Token('RULE', 'number_part1'), [Token('DIGIT', '1'), Token('DIGIT', '2'), Token('DIGIT', '3')]), Tree(Token('RULE', 'number_part2'), [Token('DIGIT', '4'), Token('DIGIT', '2'), Token('DIGIT', '3'), Token('DIGIT', '4')])]), Token('NEWLINE', '\\n'), Tree(Token('RULE', 'entry'), [Token('WORD', 'Sally'), Tree(Token('RULE', 'area_code'), [Token('DIGIT', '5'), Token('DIGIT', '5'), Token('DIGIT', '5')]), Tree(Token('RULE', 'number_part1'), [Token('DIGIT', '1'), Token('DIGIT', '7'), Token('DIGIT', '7')]), Tree(Token('RULE', 'number_part2'), [Token('DIGIT', '2'), Token('DIGIT', '8'), Token('DIGIT', '3'), Token('DIGIT', '8')])]), Token('NEWLINE', '\\n'), Tree(Token('RULE', 'entry'), [Token('WORD', 'Bob'), Tree(Token('RULE', 'area_code'), [Token('DIGIT', '5'), Token('DIGIT', '5'), Token('DIGIT', '5')]), Tree(Token('RULE', 'number_part1'), [Token('DIGIT', '1'), Token('DIGIT', '1'), Token('DIGIT', '1')]), Tree(Token('RULE', 'number_part2'), [Token('DIGIT', '2'), Token('DIGIT', '2'), Token('DIGIT', '2'), Token('DIGIT', '2')])]), Token('NEWLINE', '\\n')])])\n"
]
}
],
"source": [
"import lark\n",
"\n",
"phonebook_parser = lark.Lark('''\n",
"%import common.WS_INLINE\n",
"%import common.NEWLINE\n",
"%import common.WORD\n",
"%import common.DIGIT\n",
"\n",
"%ignore WS_INLINE\n",
"\n",
"start: phonebook\n",
"\n",
"?phonebook: entry (NEWLINE entry)* NEWLINE?\n",
"\n",
"?entry: name \":\" area_code \"-\"? number_part1 \"-\"? number_part2\n",
"?name: WORD\n",
"?area_code: DIGIT~3\n",
"?number_part1: DIGIT~3\n",
"?number_part2: DIGIT~4\n",
"''')\n",
"\n",
"phonebook = '''John: 555-123-4234\n",
"Sally : 555 177 2838\n",
"Bob : 555 111-2222\n",
"'''\n",
"\n",
"phonebook_ast = phonebook_parser.parse(phonebook)\n",
"print(phonebook_ast)"
]
},
{
"cell_type": "markdown",
"id": "9a740c3f",
"metadata": {},
"source": [
"That's a little hard to read! Lark has successfully parsed our phonebook according to the grammar and produced a structured abstract syntax tree. Let's have Lark pretty-print it for us, using indentation to show the nested structure."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "04265578",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"start\n",
" phonebook\n",
" entry\n",
" John\n",
" area_code\n",
" 5\n",
" 5\n",
" 5\n",
" number_part1\n",
" 1\n",
" 2\n",
" 3\n",
" number_part2\n",
" 4\n",
" 2\n",
" 3\n",
" 4\n",
" \n",
"\n",
" entry\n",
" Sally\n",
" area_code\n",
" 5\n",
" 5\n",
" 5\n",
" number_part1\n",
" 1\n",
" 7\n",
" 7\n",
" number_part2\n",
" 2\n",
" 8\n",
" 3\n",
" 8\n",
" \n",
"\n",
" entry\n",
" Bob\n",
" area_code\n",
" 5\n",
" 5\n",
" 5\n",
" number_part1\n",
" 1\n",
" 1\n",
" 1\n",
" number_part2\n",
" 2\n",
" 2\n",
" 2\n",
" 2\n",
" \n",
"\n",
"\n"
]
}
],
"source": [
"print(phonebook_ast.pretty())"
]
},
{
"cell_type": "markdown",
"id": "16a3c73b",
"metadata": {},
"source": [
"## Parsing PHDwin formulas\n",
"\n",
"The grammar for PHDwin formulas is, obviously, a little more complex than our phonebook.\n",
"From sifting through an example model file's \"OTPF\" table, I found formulas which look like:\n",
"```\n",
"{Oil} / (AddMonthsToDate({CurrentDate},1) - {CurrentDate}) / 1000\n",
"```\n",
"Or:\n",
"```\n",
"If({MB} < {CumThreshold1},{CumTranche1},If({MB} < {CumThreshold2},{CumTranche2},{CumTranche3}))\n",
"```\n",
"Or:\n",
"```\n",
"If({StartHist} <= 0,If({MajSeg1} <= 0,{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3}))),If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3})))\n",
"```\n",
"\n",
"What I've inferred (and I may be wrong, but that's easy to fix) is that these formulas support: \n",
"- function calls `F(x, y, z)`\n",
"- reference to variables, which sometimes (?) need \"quotes\" `{Some Variable Name}`\n",
"- two-argument math operators `+`, `-`, `*`, `/` following PEMDAS order-of-operations\n",
"- one-argument negation or (no-op) \"postiviation\" like `-3` or `+5`\n",
"- boolean comparison operators `<`, `>`, `<=`, `>=`, `=`\n",
"\n",
"We'll write a grammar for these formulas. It's large enough we'll write it into a separate `formula.lark` file, shown below:"
]
},
{
"cell_type": "markdown",
"id": "d93ba9df",
"metadata": {},
"source": [
"```\n",
"%import common.SIGNED_NUMBER\n",
"%import common.CNAME\n",
"%import common.WS\n",
"\n",
"%ignore WS\n",
"\n",
"?start: expr\n",
"\n",
"?expr: comp_expr\n",
"?comp_expr: addsub_expr (comp_op addsub_expr)*\n",
"?addsub_expr: muldiv_expr (addsub_op muldiv_expr)*\n",
"?muldiv_expr: factor (muldiv_op factor)*\n",
"?factor: sign_op factor | atom_expr\n",
"\n",
"// TODO: not equal?\n",
"!comp_op: \"<\" | \">\" | \"=\" | \"<=\" | \">=\"\n",
"!addsub_op: \"+\" | \"-\"\n",
"!muldiv_op: \"x\" | \"/\"\n",
"!sign_op: \"+\" | \"-\"\n",
"\n",
"?atom_expr: identifier \"(\" [args] \")\" -> funcall\n",
" | atom\n",
"\n",
"// TODO: I see some function calls(?) inside { } - what are these?\n",
"?atom: \"(\" expr \")\"\n",
" | identifier\n",
" | number\n",
"\n",
"?args: expr (\",\" expr)*\n",
"\n",
"// TODO: we need to know the escaping rules here...\n",
"?identifier: (CNAME | \"{\" /[^}]+/ \"}\") -> var\n",
"\n",
"?number: SIGNED_NUMBER -> const\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "395259e1",
"metadata": {},
"source": [
"The trickiest part here is handling the combination of \"left recursion\" (e.g. an expression can be **\"some expression\"** `+` \"some expression\") and operator precedence (i.e. PEMDAS rules); the approach here (using nested expression rules like `comp_expr`, `addsub_expr`...) is fairly standard and well-supported by Lark.\n",
"\n",
"We can now use our parser to read formulas!"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "078ed143",
"metadata": {},
"outputs": [],
"source": [
"import lark\n",
"\n",
"with open('formula.lark') as f:\n",
" formula_parser = lark.Lark(f)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "fdf9ffe4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"comp_expr\n",
" muldiv_expr\n",
" var\tOil\n",
" muldiv_op\t/\n",
" comp_expr\n",
" addsub_expr\n",
" funcall\n",
" var\tAddMonthsToDate\n",
" args\n",
" comp_expr\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" comp_expr\n",
" const\t1\n",
" None\n",
" None\n",
" addsub_op\t-\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" muldiv_op\t/\n",
" const\t1000\n",
" None\n",
" None\n",
"\n"
]
}
],
"source": [
"example = '{Oil} / (AddMonthsToDate({CurrentDate},1) - {CurrentDate}) / 1000'\n",
"tree = formula_parser.parse(example)\n",
"print(tree.pretty())"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b64a584a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" var\tMB\n",
" comp_op\t<\n",
" var\tCumThreshold1\n",
" comp_expr\n",
" var\tCumTranche1\n",
" None\n",
" None\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" var\tMB\n",
" comp_op\t<\n",
" var\tCumThreshold2\n",
" comp_expr\n",
" var\tCumTranche2\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tCumTranche3\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
"\n"
]
}
],
"source": [
"example = 'If({MB} < {CumThreshold1},{CumTranche1},If({MB} < {CumThreshold2},{CumTranche2},{CumTranche3}))'\n",
"tree = formula_parser.parse(example)\n",
"print(tree.pretty())"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "57fa43b6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" var\tStartHist\n",
" comp_op\t<=\n",
" const\t0\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" var\tMajSeg1\n",
" comp_op\t<=\n",
" const\t0\n",
" comp_expr\n",
" var\tDateTranche1\n",
" None\n",
" None\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" funcall\n",
" var\tMonthsBetweenDates\n",
" args\n",
" comp_expr\n",
" var\tMajSeg1\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" comp_op\t<=\n",
" var\tMonthThreshold1\n",
" comp_expr\n",
" var\tDateTranche1\n",
" None\n",
" None\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" funcall\n",
" var\tMonthsBetweenDates\n",
" args\n",
" comp_expr\n",
" var\tMajSeg1\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" comp_op\t<=\n",
" var\tMonthThreshold2\n",
" comp_expr\n",
" var\tDateTranche2\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tDateTranche3\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" funcall\n",
" var\tMonthsBetweenDates\n",
" args\n",
" comp_expr\n",
" var\tStartHist\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" comp_op\t<=\n",
" var\tMonthThreshold1\n",
" comp_expr\n",
" var\tDateTranche1\n",
" None\n",
" None\n",
" comp_expr\n",
" funcall\n",
" var\tIf\n",
" args\n",
" comp_expr\n",
" funcall\n",
" var\tMonthsBetweenDates\n",
" args\n",
" comp_expr\n",
" var\tStartHist\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tCurrentDate\n",
" None\n",
" None\n",
" comp_op\t<=\n",
" var\tMonthThreshold2\n",
" comp_expr\n",
" var\tDateTranche2\n",
" None\n",
" None\n",
" comp_expr\n",
" var\tDateTranche3\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
" None\n",
"\n"
]
}
],
"source": [
"example = 'If({StartHist} <= 0,If({MajSeg1} <= 0,{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3}))),If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3})))'\n",
"tree = formula_parser.parse(example)\n",
"print(tree.pretty())"
]
},
{
"cell_type": "markdown",
"id": "df29b5fb",
"metadata": {},
"source": [
"Let's go back to the first (simple) example and take a look at the structure of the syntax tree, so that we can begin thinking about how to evaluate these formulas."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "0d85c3ba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Tree(Token('RULE', 'comp_expr'), [Tree(Token('RULE', 'muldiv_expr'), [Tree('var', [Token('__ANON_2', 'Oil')]), Tree(Token('RULE', 'muldiv_op'), [Token('SLASH', '/')]), Tree(Token('RULE', 'comp_expr'), [Tree(Token('RULE', 'addsub_expr'), [Tree('funcall', [Tree('var', [Token('CNAME', 'AddMonthsToDate')]), Tree(Token('RULE', 'args'), [Tree(Token('RULE', 'comp_expr'), [Tree('var', [Token('__ANON_2', 'CurrentDate')]), None, None]), Tree(Token('RULE', 'comp_expr'), [Tree('const', [Token('SIGNED_NUMBER', '1')]), None, None])])]), Tree(Token('RULE', 'addsub_op'), [Token('MINUS', '-')]), Tree('var', [Token('__ANON_2', 'CurrentDate')])]), None, None]), Tree(Token('RULE', 'muldiv_op'), [Token('SLASH', '/')]), Tree('const', [Token('SIGNED_NUMBER', '1000')])]), None, None])\n"
]
}
],
"source": [
"example = '{Oil} / (AddMonthsToDate({CurrentDate},1) - {CurrentDate}) / 1000'\n",
"tree = formula_parser.parse(example)\n",
"print(tree)"
]
},
{
"cell_type": "markdown",
"id": "1007c7a2",
"metadata": {},
"source": [
"The Lark AST is fine, but it'd be nicer to \"lock down\" the representation a little and make it more suitable for our particular task. There are many approaches we could take here, ranging from making use of the pattern matching features in newer versions of Python to a highly \"object-oriented\" approach. We'll split the interest here, defining `Enum` and `namedtuple` types to represent the nodes in our syntax tree.\n",
"\n",
"We can use the Lark \"transformers\" feature (an implementation of the [vistor pattern](https://en.wikipedia.org/wiki/Visitor_pattern)) to have Lark recursively turn its AST into our own representation.\n",
"\n",
"Let's begin by defining some types for a structured representation of PHDwin formulas. I'll make use of classes which subclass custom `namedtuple` classes. For now the only \"contents\" are `__slots__ = ()` (for performance), but later we'll add interesting methods on these to enable evaluation of formulas.\n",
"\n",
"I'll use custom `Enum` types to represent the different unary (one-argument) and binary (two-argument) operators available in formulas."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "01fce43e",
"metadata": {},
"outputs": [],
"source": [
"from enum import Enum, auto\n",
"from collections import namedtuple\n",
"\n",
"class UnaryOp(Enum):\n",
" Pos = auto()\n",
" Neg = auto()\n",
"\n",
"class BinaryOp(Enum):\n",
" Add = auto()\n",
" Sub = auto()\n",
" Mul = auto()\n",
" Div = auto()\n",
" Lt = auto()\n",
" Gt = auto()\n",
" LtEq = auto()\n",
" GtEq = auto()\n",
" Eq = auto()\n",
"\n",
"# constant numbers\n",
"class Constant(namedtuple('Constant', ['val'])):\n",
" __slots__ = ()\n",
"\n",
"# named variables\n",
"class Var(namedtuple('Var', ['name'])):\n",
" __slots__ = ()\n",
"\n",
"# binary operator applications, like \"2 + {X}\"\n",
"# lhs, rhs = \"left-hand side\", \"right-hand side\"\n",
"class BinaryOpApply(namedtuple('BinaryOpApply', ['op', 'lhs', 'rhs'])):\n",
" __slots__ = ()\n",
"\n",
"# unary operator applications, like \"-{Capex}\"\n",
"class UnaryOpApply(namedtuple('UnaryOpApply', ['op', 'expr'])):\n",
" __slots__ = ()\n",
"\n",
"# built-in function calls, like \"AddMonthsToDate({CurrentDate}, 1)\"\n",
"# fn = function name\n",
"# args = (possibly empty) list of arguments\n",
"class FunCall(namedtuple('FunCall', ['fn', 'args'])):\n",
" __slots__ = ()"
]
},
{
"cell_type": "markdown",
"id": "aa761081",
"metadata": {},
"source": [
"We can now create a custom `Transformer` class which will rewrite Lark ASTs into our own data types, recursively from the \"leaves\" up. Since our grammar is simple enough to be handled by the less-powerful but simpler LALR parser implementation provided by Lark, we can also ask Lark to perform this transformation on-the-fly rather than apply it at the end of parsing to a complete AST.\n",
"\n",
"We'll do this to produce a \"new and improved\" formula parser."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "1bb9d1c4",
"metadata": {},
"outputs": [],
"source": [
"class ASTBuilder(lark.Transformer):\n",
" def var(self, args):\n",
" return Var(name=args[0].value)\n",
"\n",
" def const(self, args):\n",
" return Constant(float(args[0]))\n",
"\n",
" def sign_op(self, args):\n",
" if args[0] == '-':\n",
" return UnaryOp.Neg\n",
" elif args[0] == '+':\n",
" return UnaryOp.Pos\n",
" else:\n",
" raise ArgumentError('invalid unary operator')\n",
"\n",
" def addsub_op(self, args):\n",
" if args[0] == '+':\n",
" return BinaryOp.Add\n",
" elif args[0] == '-':\n",
" return BinaryOp.Sub\n",
" else:\n",
" raise ArgumentError('invalid add/sub operator')\n",
"\n",
" def muldiv_op(self, args):\n",
" if args[0] == 'x':\n",
" return BinaryOp.Mul\n",
" elif args[0] == '/':\n",
" return BinaryOp.Div\n",
" else:\n",
" raise ArgumentError('invalid mul/div operator')\n",
"\n",
" def comp_op(self, args):\n",
" if args[0] == '<':\n",
" return BinaryOp.Lt\n",
" elif args[0] == '>':\n",
" return BinaryOp.Gt\n",
" elif args[0] == '<=':\n",
" return BinaryOp.LtEq\n",
" elif args[0] == '>=':\n",
" return BinaryOp.GtEq\n",
" elif args[0] == '=':\n",
" return BinaryOp.Eq\n",
" else:\n",
" raise ArgumentError('invalid comparison operator')\n",
"\n",
" def comp_expr(self, args):\n",
" lhs, op, rhs = args\n",
" # either just an expression\n",
" if op is None and rhs is None:\n",
" return lhs\n",
" # or a comparison between two expressions\n",
" return BinaryOpApply(op=op, lhs=lhs, rhs=rhs)\n",
"\n",
" def addsub_expr(self, args):\n",
" # either just an expression\n",
" if len(args) == 1:\n",
" return args[0]\n",
" # or a sequence of left-associative operations\n",
" # (this looks a little crazy, but it's going to turn\n",
" # x + y + z into (x + y) + z and so on)\n",
" ex, *args = args\n",
" while len(args) > 0:\n",
" op, next_ex, *args = args\n",
" ex = BinaryOpApply(op, lhs=ex, rhs=next_ex)\n",
" return ex\n",
"\n",
" def muldiv_expr(self, args):\n",
" # same logic as addsub_expr\n",
" if len(args) == 1:\n",
" return args[0]\n",
" ex, *args = args\n",
" while len(args) > 0:\n",
" op, next_ex, *args = args\n",
" ex = BinaryOpApply(op=op, lhs=ex, rhs=next_ex)\n",
" return ex\n",
"\n",
" def funcall(self, args):\n",
" fn, fnargs = args\n",
" # we have to account for the way Lark handles [args], and ensure \n",
" # that we always end up with a list\n",
" if fnargs is None:\n",
" fnargs = []\n",
" elif not isinstance(fnargs, list):\n",
" fnargs = [fnargs]\n",
" # fn will be a Var, having been already transformed;\n",
" # however, functions can only have certain hard-coded names,\n",
" # so we just want the string\n",
" return FunCall(fn=fn.name, args=fnargs)\n",
"\n",
" def args(self, args):\n",
" # function args should just be in-lined into the FunCall tuple\n",
" return args\n",
" \n",
"with open('formula.lark') as f:\n",
" formula_parser = lark.Lark(f, parser='lalr', transformer=ASTBuilder())"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "621d51b1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"BinaryOpApply(op=<BinaryOp.Div: 4>, lhs=BinaryOpApply(op=<BinaryOp.Div: 4>, lhs=Var(name='Oil'), rhs=BinaryOpApply(op=<BinaryOp.Sub: 2>, lhs=FunCall(fn='AddMonthsToDate', args=[Var(name='CurrentDate'), Constant(val=1.0)]), rhs=Var(name='CurrentDate'))), rhs=Constant(val=1000.0))\n"
]
}
],
"source": [
"example = '{Oil} / (AddMonthsToDate({CurrentDate},1) - {CurrentDate}) / 1000'\n",
"tree = formula_parser.parse(example)\n",
"print(tree)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "1a10e013",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.Lt: 5>, lhs=Var(name='MB'), rhs=Var(name='CumThreshold1')), Var(name='CumTranche1'), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.Lt: 5>, lhs=Var(name='MB'), rhs=Var(name='CumThreshold2')), Var(name='CumTranche2'), Var(name='CumTranche3')])])\n"
]
}
],
"source": [
"example = 'If({MB} < {CumThreshold1},{CumTranche1},If({MB} < {CumThreshold2},{CumTranche2},{CumTranche3}))'\n",
"tree = formula_parser.parse(example)\n",
"print(tree)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "ce7d306f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=Var(name='StartHist'), rhs=Constant(val=0.0)), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=Var(name='MajSeg1'), rhs=Constant(val=0.0)), Var(name='DateTranche1'), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=FunCall(fn='MonthsBetweenDates', args=[Var(name='MajSeg1'), Var(name='CurrentDate')]), rhs=Var(name='MonthThreshold1')), Var(name='DateTranche1'), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=FunCall(fn='MonthsBetweenDates', args=[Var(name='MajSeg1'), Var(name='CurrentDate')]), rhs=Var(name='MonthThreshold2')), Var(name='DateTranche2'), Var(name='DateTranche3')])])]), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=FunCall(fn='MonthsBetweenDates', args=[Var(name='StartHist'), Var(name='CurrentDate')]), rhs=Var(name='MonthThreshold1')), Var(name='DateTranche1'), FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.LtEq: 7>, lhs=FunCall(fn='MonthsBetweenDates', args=[Var(name='StartHist'), Var(name='CurrentDate')]), rhs=Var(name='MonthThreshold2')), Var(name='DateTranche2'), Var(name='DateTranche3')])])])\n"
]
}
],
"source": [
"example = 'If({StartHist} <= 0,If({MajSeg1} <= 0,{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({MajSeg1},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3}))),If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold1},{DateTranche1},If(MonthsBetweenDates({StartHist},{CurrentDate}) <= {MonthThreshold2},{DateTranche2},{DateTranche3})))'\n",
"tree = formula_parser.parse(example)\n",
"print(tree)"
]
},
{
"cell_type": "markdown",
"id": "82c84ea3",
"metadata": {},
"source": [
"While we no longer have the Lark pretty-printer, it's clear that we've now got a much better structured representation of the formulas! We can now go back and add logic to the named tuple classes to evaluate the formulas or pretty-print them, recursively. Our \"straw-man\" implementation of an evaluator will pass around a dictionary of variable values, representing a \"well record\" or what-have-you as appropriate to the actual semantics.\n",
"\n",
"We'll re-define our named tuple classes to include these new methods."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "f49f69fa",
"metadata": {},
"outputs": [],
"source": [
"class Constant(namedtuple('Constant', ['val'])):\n",
" __slots__ = ()\n",
"\n",
" def eval(self, var_dict):\n",
" return self.val\n",
"\n",
" def pprint(self):\n",
" return f'{self.val}'\n",
"\n",
"class Var(namedtuple('Var', ['name'])):\n",
" __slots__ = ()\n",
"\n",
" def eval(self, var_dict):\n",
" return var_dict[self.name]\n",
"\n",
" def pprint(self):\n",
" return f'{{{self.name}}}'\n",
"\n",
"class BinaryOpApply(namedtuple('BinaryOpApply', ['op', 'lhs', 'rhs'])):\n",
" __slots__ = ()\n",
"\n",
" def eval(self, var_dict):\n",
" lhs_val = self.lhs.eval(var_dict)\n",
" rhs_val = self.rhs.eval(var_dict)\n",
" if self.op == BinaryOp.Add:\n",
" return lhs_val + rhs_val\n",
" if self.op == BinaryOp.Sub:\n",
" return lhs_val - rhs_val\n",
" if self.op == BinaryOp.Mul:\n",
" return lhs_val * rhs_val\n",
" if self.op == BinaryOp.Div:\n",
" return lhs_val / rhs_val\n",
" if self.op == BinaryOp.Lt:\n",
" return lhs_val < rhs_val\n",
" if self.op == BinaryOp.Gt:\n",
" return lhs_val > rhs_val\n",
" if self.op == BinaryOp.LtEq:\n",
" return lhs_val <= rhs_val\n",
" if self.op == BinaryOp.GtEq:\n",
" return lhs_val >= rhs_val\n",
" if self.op == BinaryOp.Eq:\n",
" return lhs_val == rhs_val\n",
" raise ArgumentError('invalid binary operator')\n",
"\n",
" def pprint(self):\n",
" lhs_pp = self.lhs.pprint()\n",
" rhs_pp = self.rhs.pprint()\n",
" if self.op == BinaryOp.Add:\n",
" return f'({lhs_pp} + {rhs_pp})'\n",
" if self.op == BinaryOp.Sub:\n",
" return f'({lhs_pp} - {rhs_pp})'\n",
" if self.op == BinaryOp.Mul:\n",
" return f'({lhs_pp} x {rhs_pp})'\n",
" if self.op == BinaryOp.Div:\n",
" return f'({lhs_pp} / {rhs_pp})'\n",
" if self.op == BinaryOp.Lt:\n",
" return f'({lhs_pp} < {rhs_pp})'\n",
" if self.op == BinaryOp.Gt:\n",
" return f'({lhs_pp} > {rhs_pp})'\n",
" if self.op == BinaryOp.LtEq:\n",
" return f'({lhs_pp} <= {rhs_pp})'\n",
" if self.op == BinaryOp.GtEq:\n",
" return f'({lhs_pp} >= {rhs_pp})'\n",
" if self.op == BinaryOp.Eq:\n",
" return f'({lhs_pp} = {rhs_pp})'\n",
" raise ArgumentError('invalid binary operator')\n",
"\n",
"class UnaryOpApply(namedtuple('UnaryOpApply', ['op', 'expr'])):\n",
" __slots__ = ()\n",
"\n",
" def eval(self, var_dict):\n",
" expr_val = self.expr.eval(var_dict)\n",
" if self.op == UnaryOp.Pos:\n",
" return expr_val\n",
" if self.op == UnaryOp.Neg:\n",
" return -expr_val\n",
" raise ArgumentError('invalid unary operator')\n",
"\n",
" def pprint(self):\n",
" expr_pp = self.expr.pprint()\n",
" if self.op == UnaryOp.Pos:\n",
" return f'+{expr_pp}'\n",
" if self.op == UnaryOp.Neg:\n",
" return f'-{expr_pp}'\n",
" raise ArgumentError('invalid unary operator')\n",
"\n",
"class FunCall(namedtuple('FunCall', ['fn', 'args'])):\n",
" __slots__ = ()\n",
"\n",
" def eval(self, var_dict):\n",
" if self.fn == 'If':\n",
" # if has special rules - it only evaluates one or the\n",
" # other of its arguments!\n",
" cond, do_if, do_else = self.args\n",
" if cond.eval(var_dict):\n",
" return do_if.eval(var_dict)\n",
" return do_else.eval(var_dict)\n",
" if self.fn == 'Abs':\n",
" val, = self.args\n",
" return abs(val.eval(var_dict))\n",
" raise ArgumentError(f'function {self.fn} not yet implemented!')\n",
"\n",
" def pprint(self):\n",
" args = ', '.join(a.pprint() for a in self.args)\n",
" return f'{self.fn}({args})'"
]
},
{
"cell_type": "markdown",
"id": "d3d5e884",
"metadata": {},
"source": [
"That means we'll also need to re-create our transformer class and parser, to capture these updated definitions, but we won't make any changes to their source code:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "fcc6ceb5",
"metadata": {},
"outputs": [],
"source": [
"class ASTBuilder(lark.Transformer):\n",
" def var(self, args):\n",
" return Var(name=args[0].value)\n",
"\n",
" def const(self, args):\n",
" return Constant(float(args[0]))\n",
"\n",
" def sign_op(self, args):\n",
" if args[0] == '-':\n",
" return UnaryOp.Neg\n",
" elif args[0] == '+':\n",
" return UnaryOp.Pos\n",
" else:\n",
" raise ArgumentError('invalid unary operator')\n",
"\n",
" def addsub_op(self, args):\n",
" if args[0] == '+':\n",
" return BinaryOp.Add\n",
" elif args[0] == '-':\n",
" return BinaryOp.Sub\n",
" else:\n",
" raise ArgumentError('invalid add/sub operator')\n",
"\n",
" def muldiv_op(self, args):\n",
" if args[0] == 'x':\n",
" return BinaryOp.Mul\n",
" elif args[0] == '/':\n",
" return BinaryOp.Div\n",
" else:\n",
" raise ArgumentError('invalid mul/div operator')\n",
"\n",
" def comp_op(self, args):\n",
" if args[0] == '<':\n",
" return BinaryOp.Lt\n",
" elif args[0] == '>':\n",
" return BinaryOp.Gt\n",
" elif args[0] == '<=':\n",
" return BinaryOp.LtEq\n",
" elif args[0] == '>=':\n",
" return BinaryOp.GtEq\n",
" elif args[0] == '=':\n",
" return BinaryOp.Eq\n",
" else:\n",
" raise ArgumentError('invalid comparison operator')\n",
"\n",
" def comp_expr(self, args):\n",
" lhs, op, rhs = args\n",
" # either just an expression\n",
" if op is None and rhs is None:\n",
" return lhs\n",
" # or a comparison between two expressions\n",
" return BinaryOpApply(op=op, lhs=lhs, rhs=rhs)\n",
"\n",
" def addsub_expr(self, args):\n",
" # either just an expression\n",
" if len(args) == 1:\n",
" return args[0]\n",
" # or a sequence of left-associative operations\n",
" # (this looks a little crazy, but it's going to turn\n",
" # x + y + z into (x + y) + z and so on)\n",
" ex, *args = args\n",
" while len(args) > 0:\n",
" op, next_ex, *args = args\n",
" ex = BinaryOpApply(op, lhs=ex, rhs=next_ex)\n",
" return ex\n",
"\n",
" def muldiv_expr(self, args):\n",
" # same logic as addsub_expr\n",
" if len(args) == 1:\n",
" return args[0]\n",
" ex, *args = args\n",
" while len(args) > 0:\n",
" op, next_ex, *args = args\n",
" ex = BinaryOpApply(op=op, lhs=ex, rhs=next_ex)\n",
" return ex\n",
"\n",
" def funcall(self, args):\n",
" fn, fnargs = args\n",
" # we have to account for the way Lark handles [args], and ensure \n",
" # that we always end up with a list\n",
" if fnargs is None:\n",
" fnargs = []\n",
" elif not isinstance(fnargs, list):\n",
" fnargs = [fnargs]\n",
" # fn will be a Var, having been already transformed;\n",
" # however, functions can only have certain hard-coded names,\n",
" # so we just want the string\n",
" return FunCall(fn=fn.name, args=fnargs)\n",
"\n",
" def args(self, args):\n",
" # function args should just be in-lined into the FunCall tuple\n",
" return args\n",
" \n",
"with open('formula.lark') as f:\n",
" formula_parser = lark.Lark(f, parser='lalr', transformer=ASTBuilder())"
]
},
{
"cell_type": "markdown",
"id": "5658aa59",
"metadata": {},
"source": [
"We can now pretty-print and evaluate (with access to variable values from a dictionary) our formulas! Our pretty-printer adds redundant parentheses to clarify order of operations."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "92f6fd57",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"BinaryOpApply(op=<BinaryOp.Add: 1>, lhs=BinaryOpApply(op=<BinaryOp.Sub: 2>, lhs=BinaryOpApply(op=<BinaryOp.Add: 1>, lhs=Constant(val=1.0), rhs=BinaryOpApply(op=<BinaryOp.Mul: 3>, lhs=Constant(val=2.0), rhs=Constant(val=3.0))), rhs=BinaryOpApply(op=<BinaryOp.Div: 4>, lhs=Constant(val=7.0), rhs=Constant(val=5.0))), rhs=FunCall(fn='Abs', args=[BinaryOpApply(op=<BinaryOp.Sub: 2>, lhs=Constant(val=777.0), rhs=Constant(val=999.0))]))\n",
"\n",
"(((1.0 + (2.0 x 3.0)) - (7.0 / 5.0)) + Abs((777.0 - 999.0)))\n",
"\n",
"227.6\n"
]
}
],
"source": [
"some_math = formula_parser.parse('1 + 2 x 3 - 7 / 5 + Abs(777 - 999)')\n",
"print(some_math)\n",
"print()\n",
"print(some_math.pprint())\n",
"print()\n",
"print(some_math.eval({})) # empty variable dictionary"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "0e5557bb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"FunCall(fn='If', args=[BinaryOpApply(op=<BinaryOp.Gt: 6>, lhs=Var(name='X'), rhs=Constant(val=3.0)), BinaryOpApply(op=<BinaryOp.Mul: 3>, lhs=Var(name='X'), rhs=Constant(val=7.0)), BinaryOpApply(op=<BinaryOp.Sub: 2>, lhs=Var(name='X'), rhs=Constant(val=9.0))])\n",
"\n",
"If(({X} > 3.0), ({X} x 7.0), ({X} - 9.0))\n",
"\n",
"35.0\n",
"\n",
"-7.0\n"
]
}
],
"source": [
"use_a_variable = formula_parser.parse('If({X} > 3, {X} x 7, {X} - 9)')\n",
"print(use_a_variable)\n",
"print()\n",
"print(use_a_variable.pprint())\n",
"print()\n",
"print(use_a_variable.eval({'X': 5})) # provide a value for 'X'\n",
"print()\n",
"print(use_a_variable.eval({'X': 2})) # provide a value for 'X'"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "94819860",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"BinaryOpApply(op=<BinaryOp.Add: 1>, lhs=BinaryOpApply(op=<BinaryOp.Add: 1>, lhs=Var(name='Royalty'), rhs=Var(name='GovProfOil')), rhs=Var(name='IncomeTax'))\n",
"\n",
"(({Royalty} + {GovProfOil}) + {IncomeTax})\n",
"\n",
"2340000.0\n"
]
}
],
"source": [
"multiple_vars = formula_parser.parse('{Royalty} + {GovProfOil} + {IncomeTax}')\n",
"print(multiple_vars)\n",
"print()\n",
"print(multiple_vars.pprint())\n",
"print()\n",
"print(multiple_vars.eval({\n",
" 'Royalty': 1.2e6,\n",
" 'GovProfOil': 2.3e5,\n",
" 'IncomeTax': 9.1e5\n",
"}))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5f6eea00",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment