Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parser cleanup/organization, tuple structuring working (but still clunky) #31

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 0 additions & 83 deletions flopy4/lark/__init__.py

This file was deleted.

4 changes: 4 additions & 0 deletions flopy4/mf6/io/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = ["make_parser", "MF6Transformer"]

from flopy4.mf6.io.parser import make_parser
from flopy4.mf6.io.transformer import MF6Transformer
110 changes: 110 additions & 0 deletions flopy4/mf6/io/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from os import linesep
from typing import Iterable

from lark import Lark

MF6_GRAMMAR = r"""
// component
component: _NL* (block _NL+)* _NL*

// block
block: _paramblock | _listblock
_paramblock: _BEGIN paramblock _NL params _END paramblock
_listblock: _BEGIN listblock _NL list _END listblock
paramblock: PARAMBLOCK
listblock: LISTBLOCK [_blockindex]
_blockindex: INT
_BEGIN: "begin"i
_END: "end"i

// parameter
params.1: (param _NL)*
param: key | _pair
_pair: key value
key: PARAM
?value: array
| list
| path
| string
| scalar
?scalar: int
| float
| word

// string
word: WORD
?string: word+
NON_SEPARATOR_STRING: /[a-zA-z.;\\\/]+/

// number
int: INT
float: FLOAT

// file path
path: INOUT PATH
PATH: [_PATHSEP] (NON_SEPARATOR_STRING [_PATHSEP]) [NON_SEPARATOR_STRING]
_PATHSEP: "/"
INOUT: "filein"i|"fileout"i

// array
array: constantarray | internalarray | externalarray
constantarray: "CONSTANT" float
internalarray: "INTERNAL" [factor] [iprn] (float* [_NL])*
externalarray: "OPEN/CLOSE" PATH [factor] ["binary"] [iprn]
factor: "FACTOR" NUMBER
iprn: "IPRN" INT

// list adapted from https://github.com/lark-parser/lark/blob/master/examples/composition/csv.lark
// negative priority for records because the pattern is so indiscriminate
list.-1: record*
record.-1: _record+ _NL
_record: scalar

// newline
_NL: /(\r?\n[\t ]*)+/

%import common.SH_COMMENT -> COMMENT
%import common.SIGNED_NUMBER -> NUMBER
%import common.SIGNED_INT -> INT
%import common.SIGNED_FLOAT -> FLOAT
%import common.WORD
%import common.WS_INLINE

%ignore COMMENT
%ignore WS_INLINE
"""
"""
EBNF description for the MODFLOW 6 input language.
"""


def make_parser(
params: Iterable[str],
param_blocks: Iterable[str],
list_blocks: Iterable[str],
):
"""
Create a parser for the MODFLOW 6 input language with the given
parameter and block specification.

Notes
-----
We specify blocks containing parameters separately from blocks
that contain a list. These must be handled separately because
the pattern for list elements (records) casts a wider net than
the pattern for parameters, causing parameter blocks to parse
as lists otherwise.

"""
params = "|".join(['"' + n + '"i' for n in params])
param_blocks = "|".join(['"' + n + '"i' for n in param_blocks])
list_blocks = "|".join(['"' + n + '"i' for n in list_blocks])
grammar = linesep.join(
[
MF6_GRAMMAR,
f"PARAM: ({params})",
f"PARAMBLOCK: ({param_blocks})",
f"LISTBLOCK: ({list_blocks})",
]
)
return Lark(grammar, start="component")
91 changes: 91 additions & 0 deletions flopy4/mf6/io/transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from pathlib import Path

import numpy as np
from lark import Transformer


class MF6Transformer(Transformer):
"""
Transforms a parse tree for the MODFLOW 6 input language
into a nested dictionary AST suitable for structuring to
a strongly-typed input data model.

Notes
-----
Each function represents a node in the tree. Its argument
is a list of its children. Nodes are processed bottom-up,
so non-leaf functions can assume they will get a list of
primitives which are already in the right representation.

See https://lark-parser.readthedocs.io/en/stable/visitors.html#transformer
for more info.
"""

def key(self, k):
(k,) = k
return str(k).lower()

def word(self, w):
(w,) = w
return str(w)

def path(self, p):
_, p = p
return Path(p)

def string(self, s):
return " ".join(s)

def int(self, i):
(i,) = i
return int(i)

def float(self, f):
(f,) = f
return float(f)

def array(self, a):
(a,) = a
return a

def constantarray(self, a):
# TODO factor out `ConstantArray`
# array-like class from `MFArray`
# with deferred shape and use it
pass

def internalarray(self, a):
factor = a[0]
array = np.array(a[2:])
if factor is not None:
array *= factor
return array

def externalarray(self, a):
# TODO
pass

record = tuple
list = list

def param(self, p):
k = p[0]
v = True if len(p) == 1 else p[1]
return k, v

params = dict

def block(self, b):
return tuple(b[:2])

def paramblock(self, bn):
return str(bn[0]).lower()

def listblock(self, bn):
name = str(bn[0])
if len(bn) == 2:
index = int(bn[1])
name = f"{name} {index}"
return name.lower()

component = dict
Loading
Loading