From 3e046af78d41201947d9f13335e346f3c0b4253e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 25 Oct 2020 18:42:36 -0500 Subject: [PATCH 001/154] elementary tagging --- meshmode/discretization/__init__.py | 6 ++++++ meshmode/discretization/connection/direct.py | 8 ++++---- meshmode/dof_array.py | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index d1ec5a6b5..18a7460cf 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -25,6 +25,7 @@ from pytools import memoize_in, memoize_method from pytools.obj_array import make_obj_array from meshmode.array_context import ArrayContext, make_loopy_program +import loopy as lp # underscored because it shouldn't be imported from here. from meshmode.dof_array import DOFArray as _DOFArray @@ -345,6 +346,11 @@ def prg(): result[iel, idof] = \ sum(j, resampling_mat[idof, j] * nodes[iel, j]) """, + kernel_data=[ + lp.GlobalArg("result", None, shape=lp.auto, tags="dof_array"), + lp.GlobalArg("nodes", None, shape=lp.auto, tags=""), + ... + ], name="nodes") return make_obj_array([ diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 55820fe03..bf463d157 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -269,10 +269,10 @@ def mat_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto), + offset=lp.auto, tags="dof_array"), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto), + offset=lp.auto, tags="dof_array"), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), "...", @@ -293,10 +293,10 @@ def pick_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto), + offset=lp.auto, tags="dof_array"), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto), + offset=lp.auto, tags="dof_array"), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), lp.ValueArg("n_from_nodes", np.int32), diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index c1bde0163..bebca55a3 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -24,6 +24,7 @@ import numpy as np from typing import Optional, Iterable, Any from functools import partial +import loopy as lp from pytools import single_valued, memoize_in from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args @@ -230,6 +231,10 @@ def prg(): "{[iel,idof]: 0<=iel Date: Tue, 10 Nov 2020 02:41:10 -0600 Subject: [PATCH 002/154] use tag rather than string --- meshmode/discretization/__init__.py | 3 ++- meshmode/discretization/connection/direct.py | 9 +++++---- meshmode/dof_array.py | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 18a7460cf..4a7769113 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -25,6 +25,7 @@ from pytools import memoize_in, memoize_method from pytools.obj_array import make_obj_array from meshmode.array_context import ArrayContext, make_loopy_program +from meshmode.dof_array import DOFTag import loopy as lp # underscored because it shouldn't be imported from here. @@ -347,7 +348,7 @@ def prg(): sum(j, resampling_mat[idof, j] * nodes[iel, j]) """, kernel_data=[ - lp.GlobalArg("result", None, shape=lp.auto, tags="dof_array"), + lp.GlobalArg("result", None, shape=lp.auto, tags=DOFTag()), lp.GlobalArg("nodes", None, shape=lp.auto, tags=""), ... ], diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index bf463d157..cbdfe286c 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -26,6 +26,7 @@ import loopy as lp from pytools import memoize_in, keyed_memoize_method from meshmode.array_context import ArrayContext, make_loopy_program +from meshmode.dof_array import DOFTag # {{{ interpolation batch @@ -269,10 +270,10 @@ def mat_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags="dof_array"), + offset=lp.auto, tags=DOFTag()), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags="dof_array"), + offset=lp.auto, tags=DOFTag()), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), "...", @@ -293,10 +294,10 @@ def pick_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags="dof_array"), + offset=lp.auto, tags=DOFTag()), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags="dof_array"), + offset=lp.auto, tags=DOFTag()), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), lp.ValueArg("n_from_nodes", np.int32), diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index bebca55a3..62ac251ba 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -28,10 +28,10 @@ from pytools import single_valued, memoize_in from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args +from pytools.tag import Tag from meshmode.array_context import ArrayContext, make_loopy_program - __doc__ = """ .. autoclass:: DOFArray .. autofunction:: thaw @@ -41,6 +41,8 @@ .. autofunction:: flat_norm """ +class DOFTag(Tag): + pass # {{{ DOFArray @@ -232,7 +234,7 @@ def prg(): """result[grp_start + iel*ndofs_per_element + idof] \ = grp_ary[iel, idof]""", kernel_data=[ - lp.GlobalArg("grp_ary", None, shape=lp.auto, tags="dof_array"), + lp.GlobalArg("grp_ary", None, shape=lp.auto, tags=DOFTag()), ... ], name="flatten") From a7581e399a5341af6e471de9107ae0d08ffc3074 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 10 Nov 2020 03:42:05 -0600 Subject: [PATCH 003/154] add blank lines --- meshmode/dof_array.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 62ac251ba..5d4554b42 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -41,9 +41,11 @@ .. autofunction:: flat_norm """ -class DOFTag(Tag): + +class DOFTag(Tag):i pass + # {{{ DOFArray class DOFArray(np.ndarray): From 0815303748e03815b3ddc29d04435174232b4663 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 10 Nov 2020 03:44:13 -0600 Subject: [PATCH 004/154] remove typo --- meshmode/dof_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 5d4554b42..45100af7f 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -42,7 +42,7 @@ """ -class DOFTag(Tag):i +class DOFTag(Tag): pass From 4dd15cdf296f5fbba45e9067dadbd5fc2ff7a8e6 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 00:48:42 -0600 Subject: [PATCH 005/154] setup.py --- meshmode/discretization/__init__.py | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 4a7769113..23fb1c0ee 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -349,7 +349,6 @@ def prg(): """, kernel_data=[ lp.GlobalArg("result", None, shape=lp.auto, tags=DOFTag()), - lp.GlobalArg("nodes", None, shape=lp.auto, tags=""), ... ], name="nodes") diff --git a/setup.py b/setup.py index 4628efa51..e696e8111 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def main(): "numpy", "modepy", "gmsh_interop", - "pytools>=2020.4.1", + "pytools>=2020.4.2", "pytest>=2.3", "loo.py>=2014.1", "recursivenodes", From ac2cd1b4f59e4362c39efe135d80477e10623a77 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:01:13 -0600 Subject: [PATCH 006/154] add documentation --- meshmode/dof_array.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 8956d3284..484ee356a 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -37,6 +37,7 @@ from meshmode.array_context import ArrayContext, make_loopy_program __doc__ = """ +.. autoclass:: DOFTag .. autoclass:: DOFArray .. autofunction:: obj_or_dof_array_vectorize @@ -51,12 +52,19 @@ .. autofunction:: flat_norm """ +# { {{ DOFTag class DOFTag(Tag): + """A tag to mark arrays of DOFs in Loopy kernels. Applications + could use this to decide how to change the memory layout of + these arrays. + """ pass + # }}} + -# {{{ DOFArray +# { {{ DOFArray class DOFArray: """This array type holds degree-of-freedom arrays for use with From 6f0a97a5b612509b551ae01a95fe314ab20745e9 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:08:49 -0600 Subject: [PATCH 007/154] update required Loopy version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4e242a094..73b334e91 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def main(): # 2019.1 is required for the Firedrake CIs, which use an very specific # version of Loopy. - "loopy>=2019.1", + "loopy>=2020.2", "recursivenodes", "dataclasses; python_version<='3.6'", From 3f727b500d50ea492ff14cfcbbc15d3f0ca7afbd Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:11:56 -0600 Subject: [PATCH 008/154] remove spaces --- meshmode/dof_array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 484ee356a..58f8018af 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -52,7 +52,7 @@ .. autofunction:: flat_norm """ -# { {{ DOFTag +# {{{ DOFTag class DOFTag(Tag): """A tag to mark arrays of DOFs in Loopy kernels. Applications @@ -64,7 +64,7 @@ class DOFTag(Tag): # }}} -# { {{ DOFArray +# {{{ DOFArray class DOFArray: """This array type holds degree-of-freedom arrays for use with From ee54dea0ff0f356562d82befbdda051235335868 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:15:01 -0600 Subject: [PATCH 009/154] only import auto and GlobalArg --- meshmode/discretization/__init__.py | 4 ++-- meshmode/dof_array.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 59822159d..5274230c8 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -26,7 +26,7 @@ from pytools.obj_array import make_obj_array from meshmode.array_context import ArrayContext, make_loopy_program from meshmode.dof_array import DOFTag -import loopy as lp +from loopy import GlobalArg, auto # underscored because it shouldn't be imported from here. from meshmode.dof_array import DOFArray as _DOFArray @@ -348,7 +348,7 @@ def prg(): sum(j, resampling_mat[idof, j] * nodes[iel, j]) """, kernel_data=[ - lp.GlobalArg("result", None, shape=lp.auto, tags=DOFTag()), + GlobalArg("result", None, shape=auto, tags=DOFTag()), ... ], name="nodes") diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 58f8018af..acf2902a9 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -24,7 +24,7 @@ import numpy as np from typing import Optional, Iterable, Any, Tuple, Union from functools import partial -import loopy as lp +from loopy import GlobalArg, auto from pytools import single_valued, memoize_in from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args @@ -334,7 +334,7 @@ def prg(): """result[grp_start + iel*ndofs_per_element + idof] \ = grp_ary[iel, idof]""", kernel_data=[ - lp.GlobalArg("grp_ary", None, shape=lp.auto, tags=DOFTag()), + GlobalArg("grp_ary", None, shape=auto, tags=DOFTag()), ... ], name="flatten") From 7828ec1191a0c542ad9febdb2b413cd5f7d53afc Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:18:41 -0600 Subject: [PATCH 010/154] remove redundant import --- meshmode/dof_array.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index acf2902a9..47cfc2e86 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -256,7 +256,6 @@ def obj_or_dof_array_vectorize_n_args(f, *args): if isinstance(arg, DOFArray)] if not dofarray_arg_indices: - from pytools.obj_array import obj_array_vectorize_n_args return obj_array_vectorize_n_args(f, *args) leading_da_index = dofarray_arg_indices[0] From 759d34d3c61ca7b84375e31e9edb5b6ca441660e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:23:05 -0600 Subject: [PATCH 011/154] blank line --- meshmode/dof_array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 47cfc2e86..da4ac1452 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -52,6 +52,7 @@ .. autofunction:: flat_norm """ + # {{{ DOFTag class DOFTag(Tag): @@ -140,7 +141,7 @@ def from_list(cls, actx: Optional[ArrayContext], res_list) -> "DOFArray": return cls(actx, tuple(res_list)) - # }}} + # }}} # {{{ sequence protocol From 7048d2adad6d9ac31b19619b572f05da93e40b35 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 16 Nov 2020 01:26:06 -0600 Subject: [PATCH 012/154] remove white space --- meshmode/dof_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index da4ac1452..47b33e2ab 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -141,7 +141,7 @@ def from_list(cls, actx: Optional[ArrayContext], res_list) -> "DOFArray": return cls(actx, tuple(res_list)) - # }}} + # }}} # {{{ sequence protocol From 4feed7757be083ae7831576072c12bc01fa34472 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 22:30:48 -0600 Subject: [PATCH 013/154] requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7027fed52..6a0d66fdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/inducer/pytools.git#egg=pytools +git+https://github.com/nchristensen/pytools.git@dof_tagging#egg=pytools == 2020.4.4 git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy @@ -11,7 +11,7 @@ git+https://github.com/inducer/islpy.git#egg=islpy git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git#egg=loopy +git+https://github.com/inducer/loopy.git@revert-172-revert-156-master#egg=loopy == 2020.2.2 # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree From bafd4e7fcf41bddf6561b51fab4293ea8a6876a2 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 22:31:41 -0600 Subject: [PATCH 014/154] update required version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 73b334e91..63218f5e8 100644 --- a/setup.py +++ b/setup.py @@ -44,12 +44,12 @@ def main(): "numpy", "modepy>=2020.2", "gmsh_interop", - "pytools>=2020.4.2", + "pytools>=2020.4.4", "pytest>=2.3", # 2019.1 is required for the Firedrake CIs, which use an very specific # version of Loopy. - "loopy>=2020.2", + "loopy>=2020.2.2", "recursivenodes", "dataclasses; python_version<='3.6'", From ddbdf711f3b499cd8665d5f792d384f827832dbe Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 22:42:41 -0600 Subject: [PATCH 015/154] redundant import --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6a0d66fdd..135ed5aad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/nchristensen/pytools.git@dof_tagging#egg=pytools == 2020.4.4 +git+https://github.com/nchristensen/pytools.git#egg=pytools == 2020.4.4 git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy From 8d2d563af5760ca55064e4fb9e0edd1f3f6b9a63 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 22:46:44 -0600 Subject: [PATCH 016/154] remove redundant import --- meshmode/dof_array.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 5873cb917..375986775 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -266,7 +266,6 @@ def obj_or_dof_array_vectorize_n_args(f, *args): if not dofarray_arg_indices: if any(isinstance(arg, np.ndarray) and arg.dtype.char == "O" for i, arg in enumerate(args)): - from pytools.obj_array import obj_array_vectorize_n_args return obj_array_vectorize_n_args( partial(obj_or_dof_array_vectorize_n_args, f), *args) else: From cf1ac1316c37b7324f6e394752db8893efadd546 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 23:05:23 -0600 Subject: [PATCH 017/154] requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 135ed5aad..3dab5b423 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/nchristensen/pytools.git#egg=pytools == 2020.4.4 +git+https://github.com/nchristensen/pytools.git@master#egg=pytools == 2020.4.4 git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy From 33ec3a7bb53ed874d995e0dcf8eb70af8bf0ee8e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 23:22:55 -0600 Subject: [PATCH 018/154] update version number --- meshmode/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/version.py b/meshmode/version.py index 82ea6fb12..31b60a7be 100644 --- a/meshmode/version.py +++ b/meshmode/version.py @@ -1,2 +1,2 @@ -VERSION = (2020, 2) +VERSION = (2020, 2, 1) VERSION_TEXT = ".".join(str(i) for i in VERSION) From 0f67d1a6ee3471107b83bcc7f0da08b578a1250a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 23:30:58 -0600 Subject: [PATCH 019/154] requirements.txt pytential version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3dab5b423..b199d5331 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ git+https://github.com/inducer/loopy.git@revert-172-revert-156-master#egg=loopy # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree git+https://github.com/inducer/sumpy.git#egg=sumpy -git+https://github.com/inducer/pytential.git#pytential +git+https://github.com/nchristensen/pytential.git#pytential==2020.2.1 # requires pymetis for tests for partition_mesh git+https://github.com/inducer/pymetis.git#egg=pymetis From 7cb9124ba26fcaec85045616a6777991e386c35a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 30 Nov 2020 23:54:14 -0600 Subject: [PATCH 020/154] not the problem --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b199d5331..3dab5b423 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ git+https://github.com/inducer/loopy.git@revert-172-revert-156-master#egg=loopy # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree git+https://github.com/inducer/sumpy.git#egg=sumpy -git+https://github.com/nchristensen/pytential.git#pytential==2020.2.1 +git+https://github.com/inducer/pytential.git#pytential # requires pymetis for tests for partition_mesh git+https://github.com/inducer/pymetis.git#egg=pymetis From 847eb1ea6a8f1949ed27495fbd1dc87a7db31ce1 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 1 Dec 2020 00:00:03 -0600 Subject: [PATCH 021/154] missing egg? --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3dab5b423..0160701e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ git+https://github.com/inducer/loopy.git@revert-172-revert-156-master#egg=loopy # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree git+https://github.com/inducer/sumpy.git#egg=sumpy -git+https://github.com/inducer/pytential.git#pytential +git+https://github.com/inducer/pytential.git#egg=pytential # requires pymetis for tests for partition_mesh git+https://github.com/inducer/pymetis.git#egg=pymetis From a895671d5150320da9b9ec3bc16b6e89e95d9482 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 1 Dec 2020 00:35:47 -0600 Subject: [PATCH 022/154] point firedrake to fork --- .ci/install-for-firedrake.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install-for-firedrake.sh b/.ci/install-for-firedrake.sh index 3be82c657..39f9017c5 100644 --- a/.ci/install-for-firedrake.sh +++ b/.ci/install-for-firedrake.sh @@ -19,5 +19,5 @@ pip install dataclasses pip install pytest pip install -r /tmp/myreq.txt -pip install --force-reinstall git+https://github.com/inducer/loopy.git@firedrake-usable_for_potentials +pip install --force-reinstall git+https://github.com/nchristensen/loopy.git@test-firedrake-usable pip install . From d1700e99751aeb77b70a9a13e1914b7eb3cbc500 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 1 Dec 2020 00:56:42 -0600 Subject: [PATCH 023/154] Trigger CI From 5d8a14cc8794e1660bf8b2ab8ad25b4f12781932 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 1 Dec 2020 01:18:12 -0600 Subject: [PATCH 024/154] back down pytools and loopy versions for firedrake --- .ci/install-for-firedrake.sh | 2 +- setup.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.ci/install-for-firedrake.sh b/.ci/install-for-firedrake.sh index 39f9017c5..3be82c657 100644 --- a/.ci/install-for-firedrake.sh +++ b/.ci/install-for-firedrake.sh @@ -19,5 +19,5 @@ pip install dataclasses pip install pytest pip install -r /tmp/myreq.txt -pip install --force-reinstall git+https://github.com/nchristensen/loopy.git@test-firedrake-usable +pip install --force-reinstall git+https://github.com/inducer/loopy.git@firedrake-usable_for_potentials pip install . diff --git a/setup.py b/setup.py index 63218f5e8..4e73db06f 100644 --- a/setup.py +++ b/setup.py @@ -44,12 +44,14 @@ def main(): "numpy", "modepy>=2020.2", "gmsh_interop", - "pytools>=2020.4.4", + "pytools>=2020.4.1", + #"pytools>=2020.4.4", "pytest>=2.3", # 2019.1 is required for the Firedrake CIs, which use an very specific # version of Loopy. - "loopy>=2020.2.2", + loopy>=2019.1 + #"loopy>=2020.2.2", "recursivenodes", "dataclasses; python_version<='3.6'", From 09c8fe190dd6ea4623c3ffe3465cfb6712cdbd50 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 1 Dec 2020 01:19:58 -0600 Subject: [PATCH 025/154] fix setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4e73db06f..c45823810 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def main(): # 2019.1 is required for the Firedrake CIs, which use an very specific # version of Loopy. - loopy>=2019.1 + "loopy>=2019.1", #"loopy>=2020.2.2", "recursivenodes", From 1d50cf50d32009358c97f39f4b6d97a10e6c1b9f Mon Sep 17 00:00:00 2001 From: nchristensen <11543181+nchristensen@users.noreply.github.com> Date: Mon, 7 Dec 2020 06:49:11 +0000 Subject: [PATCH 026/154] Update meshmode/dof_array.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- meshmode/dof_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index d139480e0..29a4b29b1 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -56,7 +56,7 @@ # {{{ DOFTag class DOFTag(Tag): - """A tag to mark arrays of DOFs in Loopy kernels. Applications + """A tag to mark arrays of DOFs in :mod:`loopy` kernels. Applications could use this to decide how to change the memory layout of these arrays. """ From 866b52b4648196ac3cadb741039fceba5970ddaa Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Dec 2020 00:55:53 -0600 Subject: [PATCH 027/154] DOFTag -> IsDOFArray --- meshmode/discretization/__init__.py | 4 ++-- meshmode/discretization/connection/direct.py | 10 +++++----- meshmode/dof_array.py | 8 ++++---- setup.py | 7 +++---- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 5274230c8..79a3d8e79 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -25,7 +25,7 @@ from pytools import memoize_in, memoize_method from pytools.obj_array import make_obj_array from meshmode.array_context import ArrayContext, make_loopy_program -from meshmode.dof_array import DOFTag +from meshmode.dof_array import IsDOFArray from loopy import GlobalArg, auto # underscored because it shouldn't be imported from here. @@ -348,7 +348,7 @@ def prg(): sum(j, resampling_mat[idof, j] * nodes[iel, j]) """, kernel_data=[ - GlobalArg("result", None, shape=auto, tags=DOFTag()), + GlobalArg("result", None, shape=auto, tags=IsDOFArray()), ... ], name="nodes") diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index dc44a8e50..c17398082 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -27,7 +27,7 @@ from pytools import memoize_in, keyed_memoize_method from pytools.obj_array import obj_array_vectorized_n_args from meshmode.array_context import ArrayContext, make_loopy_program -from meshmode.dof_array import DOFTag +from meshmode.dof_array import IsDOFArray # {{{ interpolation batch @@ -275,10 +275,10 @@ def mat_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags=DOFTag()), + offset=lp.auto, tags=IsDOFArray()), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags=DOFTag()), + offset=lp.auto, tags=IsDOFArray()), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), "...", @@ -299,10 +299,10 @@ def pick_knl(): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags=DOFTag()), + offset=lp.auto, tags=IsDOFArray()), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags=DOFTag()), + offset=lp.auto, tags=IsDOFArray()), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), lp.ValueArg("n_from_nodes", np.int32), diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index d139480e0..ce200e839 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -37,7 +37,7 @@ from meshmode.array_context import ArrayContext, make_loopy_program __doc__ = """ -.. autoclass:: DOFTag +.. autoclass:: IsDOFArray .. autoclass:: DOFArray .. autofunction:: obj_or_dof_array_vectorize @@ -53,9 +53,9 @@ """ -# {{{ DOFTag +# {{{ IsDOFArray -class DOFTag(Tag): +class IsDOFArray(Tag): """A tag to mark arrays of DOFs in Loopy kernels. Applications could use this to decide how to change the memory layout of these arrays. @@ -360,7 +360,7 @@ def prg(): """result[grp_start + iel*ndofs_per_element + idof] \ = grp_ary[iel, idof]""", kernel_data=[ - GlobalArg("grp_ary", None, shape=auto, tags=DOFTag()), + GlobalArg("grp_ary", None, shape=auto, tags=IsDOFArray()), ... ], name="flatten") diff --git a/setup.py b/setup.py index c45823810..93b42ea48 100644 --- a/setup.py +++ b/setup.py @@ -44,14 +44,13 @@ def main(): "numpy", "modepy>=2020.2", "gmsh_interop", - "pytools>=2020.4.1", - #"pytools>=2020.4.4", + "pytools>=2020.4.4", "pytest>=2.3", # 2019.1 is required for the Firedrake CIs, which use an very specific # version of Loopy. - "loopy>=2019.1", - #"loopy>=2020.2.2", + #"loopy>=2019.1", + "loopy>=2020.2.2", "recursivenodes", "dataclasses; python_version<='3.6'", From cc3eb6590bd70dd04a4addc0cc447bcb9f051ff2 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Dec 2020 04:16:53 -0600 Subject: [PATCH 028/154] use tag in transform code --- examples/simple-dg.py | 13 ++++++++++++- meshmode/array_context.py | 27 ++++++++++++++++++++++++++- meshmode/discretization/__init__.py | 6 ++++++ meshmode/dof_array.py | 18 ++---------------- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/examples/simple-dg.py b/examples/simple-dg.py index 324f2a925..bd966bdd6 100644 --- a/examples/simple-dg.py +++ b/examples/simple-dg.py @@ -31,7 +31,9 @@ obj_array_vectorize) from meshmode.mesh import BTAG_ALL, BTAG_NONE # noqa from meshmode.dof_array import freeze, thaw -from meshmode.array_context import PyOpenCLArrayContext, make_loopy_program +from meshmode.array_context import (PyOpenCLArrayContext, make_loopy_program, + IsDOFArray) +from loopy import GlobalArg, auto # Features lost vs. https://github.com/inducer/grudge: @@ -247,6 +249,11 @@ def knl(): 0<=idof Date: Mon, 7 Dec 2020 04:20:49 -0600 Subject: [PATCH 029/154] flake8 --- meshmode/dof_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 447b49a32..0148bb913 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -50,8 +50,8 @@ .. autofunction:: unflatten """ +# {{{ DOFArray - # {{{ DOFArray class DOFArray: """This array type holds degree-of-freedom arrays for use with From b7b1662e301feb30277fb5d0909a0f288db42d6d Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Dec 2020 04:24:55 -0600 Subject: [PATCH 030/154] more flake8 --- meshmode/array_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 94422ae7c..2d1bfddef 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -541,14 +541,14 @@ def transform_loopy_program(self, program): inner_iname = None - hasDOFResult = False + has_dof_result = False for arg in program.args: - if arg.name == "result" and isinstance(getattr(arg, 'tags', None), + if arg.name == "result" and isinstance(getattr(arg, "tags", None), IsDOFArray): - hasDOFResult = True + has_dof_result = True break - if hasDOFResult: + if has_dof_result: outer_iname = "iel" inner_iname = "idof" elif "iel" not in all_inames and "i0" in all_inames: From 610bc1fffc6d7a37965e2ccf5a7e655fed1ef30c Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Fri, 11 Dec 2020 06:22:14 -0600 Subject: [PATCH 031/154] Add exception handling to transform function --- meshmode/array_context.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 2d1bfddef..e8b51ccc5 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -541,26 +541,24 @@ def transform_loopy_program(self, program): inner_iname = None - has_dof_result = False + has_dof_array = False for arg in program.args: - if arg.name == "result" and isinstance(getattr(arg, "tags", None), - IsDOFArray): - has_dof_result = True + if isinstance(getattr(arg, "tags", None), IsDOFArray): + has_dof_array = True break - if has_dof_result: + if has_dof_array: outer_iname = "iel" inner_iname = "idof" - elif "iel" not in all_inames and "i0" in all_inames: + # Needed for act_special_exp + elif "i0" in all_inames: outer_iname = "i0" - if "i1" in all_inames: inner_iname = "i1" + elif len(all_inames) == 1: + outer_iname = all_inames[0] else: - outer_iname = "iel" - - if "idof" in all_inames: - inner_iname = "idof" + raise ValueError("outer_iname not defined in {}.".format(program.name)) if inner_iname is not None: program = lp.split_iname(program, inner_iname, 16, inner_tag="l.0") From 82bd6c338a70f3e0573abe7ac2b6d7d62c085b8f Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 14 Dec 2020 22:39:35 -0600 Subject: [PATCH 032/154] bump required pytools version --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0160701e4..7da82f1ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/nchristensen/pytools.git@master#egg=pytools == 2020.4.4 +git+https://github.com/nchristensen/pytools.git@master#egg=pytools == 2020.4.5 git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy diff --git a/setup.py b/setup.py index 93b42ea48..511f3cc8f 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def main(): "numpy", "modepy>=2020.2", "gmsh_interop", - "pytools>=2020.4.4", + "pytools>=2020.4.5", "pytest>=2.3", # 2019.1 is required for the Firedrake CIs, which use an very specific From 1cf6d72bdbb25f2d0f381821ae13c5a282cfff04 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 15 Dec 2020 02:43:34 -0600 Subject: [PATCH 033/154] trigger GitHub actions From d72a3883a895436f4d3c383293e4c9c43ad9c0d3 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 15 Dec 2020 03:37:30 -0600 Subject: [PATCH 034/154] add tag to oversample_mat kernel --- meshmode/discretization/connection/direct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index c17398082..dc1c07ac5 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -386,7 +386,7 @@ def knl(): [ lp.GlobalArg("result", None, shape="nnodes_tgt, nnodes_src", - offset=lp.auto), + offset=lp.auto, tags=IsDOFArray()), lp.ValueArg("itgt_base,isrc_base", np.int32), lp.ValueArg("nnodes_tgt,nnodes_src", np.int32), "...", From fb57d6d18f8d1771500d51c0e93b704f3ffe0463 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 15 Dec 2020 03:58:49 -0600 Subject: [PATCH 035/154] tag IsDOFArray in unflatten --- meshmode/dof_array.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 0148bb913..b6d8b2f00 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -376,6 +376,10 @@ def prg(): return make_loopy_program( "{[iel,idof]: 0<=iel Date: Tue, 15 Dec 2020 04:47:20 -0600 Subject: [PATCH 036/154] add IsDOFTag --- meshmode/array_context.py | 2 ++ meshmode/discretization/__init__.py | 6 ++++-- meshmode/discretization/connection/projection.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index e8b51ccc5..71b1e9e1b 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -550,6 +550,8 @@ def transform_loopy_program(self, program): if has_dof_array: outer_iname = "iel" inner_iname = "idof" + elif program.name == "conn_projection_knl": + outer_iname = "iel" # Needed for act_special_exp elif "i0" in all_inames: outer_iname = "i0" diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 37c695a5a..5a34c661e 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -288,7 +288,6 @@ def prg(): "result[iel,idof] = sum(j, diff_mat[idof, j] * vec[iel, j])", kernel_data=[ GlobalArg("result", None, shape=auto, tags=IsDOFArray()), - GlobalArg("vec", None, shape=auto, tags=IsDOFArray()), ... ], name="diff") @@ -321,6 +320,10 @@ def prg(): return make_loopy_program( "{[iel,idof]: 0<=iel Date: Tue, 15 Dec 2020 04:57:54 -0600 Subject: [PATCH 037/154] fix import --- meshmode/discretization/connection/projection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshmode/discretization/connection/projection.py b/meshmode/discretization/connection/projection.py index bc166a3c2..f25a04078 100644 --- a/meshmode/discretization/connection/projection.py +++ b/meshmode/discretization/connection/projection.py @@ -24,11 +24,10 @@ from pytools import keyed_memoize_method, memoize_in from pytools.obj_array import obj_array_vectorized_n_args -from pytools.array_context import IsDOFArray import loopy as lp -from meshmode.array_context import make_loopy_program +from meshmode.array_context import make_loopy_program, IsDOFArray from meshmode.dof_array import DOFArray from meshmode.discretization.connection.direct import ( DiscretizationConnection, From dd6a8bc3a0d3843adde6fddfbe7650ffbe839616 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 15 Dec 2020 05:12:34 -0600 Subject: [PATCH 038/154] specify array shape --- meshmode/discretization/connection/projection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/discretization/connection/projection.py b/meshmode/discretization/connection/projection.py index f25a04078..7a5cd1721 100644 --- a/meshmode/discretization/connection/projection.py +++ b/meshmode/discretization/connection/projection.py @@ -173,7 +173,7 @@ def keval(): sum(ibasis, vdm[idof, ibasis] * coefficients[iel, ibasis]) """, [ - lp.GlobalArg("result", None, tags=IsDOFArray()), + lp.GlobalArg("result", None, shape=lp.auto, tags=IsDOFArray()), lp.GlobalArg("coefficients", None, shape=("nelements", "n_to_nodes")), "..." From 1849c35a2d2fadf04da05ed71937e839930e9d87 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Fri, 1 Jan 2021 17:24:18 -0600 Subject: [PATCH 039/154] handle math operations first --- meshmode/array_context.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 71b1e9e1b..6d125d88e 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -547,18 +547,21 @@ def transform_loopy_program(self, program): has_dof_array = True break - if has_dof_array: + if program.name == "conn_projection_knl": outer_iname = "iel" - inner_iname = "idof" - elif program.name == "conn_projection_knl": - outer_iname = "iel" - # Needed for act_special_exp + # Needed for act_special_exp etc. elif "i0" in all_inames: outer_iname = "i0" if "i1" in all_inames: inner_iname = "i1" elif len(all_inames) == 1: outer_iname = all_inames[0] + elif has_dof_array: + # Ce n'est pas tout à fait correct. Whether or not an array is a dof + # array has no bearing on the names of the loop variables. The "special" + # math operations, for instance, name the variables i0, i1. + outer_iname = "iel" + inner_iname = "idof" else: raise ValueError("outer_iname not defined in {}.".format(program.name)) From 7cad4575a6e3f89cd4a4e3c7c3baea60d5d57356 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Fri, 8 Jan 2021 03:28:32 -0600 Subject: [PATCH 040/154] parameterize test generation by array context --- meshmode/array_context.py | 80 +++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 6d125d88e..be8020727 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -34,6 +34,7 @@ .. autoclass:: ArrayContext .. autoclass:: PyOpenCLArrayContext .. autofunction:: pytest_generate_tests_for_pyopencl_array_context +.. autofunction:: generate_pytest_generate_tests """ @@ -574,34 +575,15 @@ def transform_loopy_program(self, program): # {{{ pytest integration -def pytest_generate_tests_for_pyopencl_array_context(metafunc): - """Parametrize tests for pytest to use a :mod:`pyopencl` array context. - - Performs device enumeration analogously to - :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. - - Using the line: - - .. code-block:: python - - from meshmode.array_context import pytest_generate_tests_for_pyopencl \ - as pytest_generate_tests - - in your pytest test scripts allows you to use the arguments ctx_factory, - device, or platform in your test functions, and they will automatically be - run for each OpenCL device/platform in the system, as appropriate. - - It also allows you to specify the ``PYOPENCL_TEST`` environment variable - for device selection. - """ +def _pytest_generate_tests_for_pyopencl_array_context(array_context_type, metafunc): import pyopencl as cl from pyopencl.tools import _ContextFactory class ArrayContextFactory(_ContextFactory): def __call__(self): ctx = super().__call__() - return PyOpenCLArrayContext(cl.CommandQueue(ctx)) + return array_context_type(cl.CommandQueue(ctx)) def __str__(self): return ("" % @@ -631,6 +613,62 @@ def __str__(self): metafunc.parametrize(arg_names, arg_values, ids=ids) + +def generate_pytest_generate_tests(array_context_type): + """Generate a function to parametrize tests for pytest to use + a :mod:`pyopencl` array context of the specified subtype. + + The returned function performs device enumeration analogously to + :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. + + Using the line: + + .. code-block:: python + + from meshmode.array_context import generate_pytest_generate_tests + pytest_generate_tests = + generate_pytest_generate_tests() + + + in your pytest test scripts allows you to use the arguments ctx_factory, + device, or platform in your test functions, and they will automatically be + run for each OpenCL device/platform in the system, as appropriate. + + It also allows you to specify the ``PYOPENCL_TEST`` environment variable + for device selection. + """ + + from functools import partial + + return lambda metafunc: partial( + _pytest_generate_tests_for_pyopencl_array_context, + array_context_type)(metafunc) + + +def pytest_generate_tests_for_pyopencl_array_context(metafunc): + """Parametrize tests for pytest to use a :mod:`pyopencl` array context. + + Performs device enumeration analogously to + :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. + + Using the line: + + .. code-block:: python + + from meshmode.array_context import pytest_generate_tests_for_pyopencl \ + as pytest_generate_tests + + in your pytest test scripts allows you to use the arguments ctx_factory, + device, or platform in your test functions, and they will automatically be + run for each OpenCL device/platform in the system, as appropriate. + + It also allows you to specify the ``PYOPENCL_TEST`` environment variable + for device selection. + """ + + generate_pytest_generate_tests(PyOpenCLArrayContext)(metafunc) + + # }}} From bc87918f7d978de8971dab93b63c1bf3133eff77 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 11 Jan 2021 03:55:41 -0600 Subject: [PATCH 041/154] flake8 fix --- meshmode/dof_array.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 763a0fdb8..152287df4 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -31,14 +31,9 @@ import threading from contextlib import contextmanager - from pytools import single_valued, memoize_in from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args -from numbers import Number -import operator as op -import decorator - from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray __doc__ = """ From 3f6bc987b2a2275f0850e138409343fefc2211ce Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 11 Jan 2021 04:03:09 -0600 Subject: [PATCH 042/154] update requirements --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7da82f1ef..933e124e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/nchristensen/pytools.git@master#egg=pytools == 2020.4.5 +git+https://github.com/inducer/pytools.git@master#egg=pytools == 2021.1 git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy @@ -11,7 +11,7 @@ git+https://github.com/inducer/islpy.git#egg=islpy git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git@revert-172-revert-156-master#egg=loopy == 2020.2.2 +git+https://github.com/inducer/loopy.git@#egg=loopy == 2020.2.2 # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree From a00bf08569908b37c24ae1da22aed68cd886b6aa Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sat, 23 Jan 2021 03:19:57 -0600 Subject: [PATCH 043/154] add coord_dtype parameter to generate_regular_rect_mesh --- meshmode/mesh/generation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index 27ea045ac..a5f962aaa 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -943,6 +943,7 @@ def generate_box_mesh(axis_coords, order=1, coord_dtype=np.float64, @deprecate_keyword("group_factory", "group_cls") def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), n=(5, 5), order=1, + coord_dtype=np.float64, boundary_tag_to_face=None, group_cls=None, mesh_type=None, @@ -964,7 +965,7 @@ def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), n=(5, 5), order=1, axis_coords = [np.linspace(a_i, b_i, n_i) for a_i, b_i, n_i in zip(a, b, n)] - return generate_box_mesh(axis_coords, order=order, + return generate_box_mesh(axis_coords, order=order, coord_dtype=np.float64, boundary_tag_to_face=boundary_tag_to_face, group_cls=group_cls, mesh_type=mesh_type) From 0ba40f80e4eeb019d493f6f3a27e93d9d0ae3fe8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 02:40:33 -0600 Subject: [PATCH 044/154] add documentation for coord_dtype --- meshmode/mesh/generation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index a5f962aaa..474163ae2 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -958,6 +958,7 @@ def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), n=(5, 5), order=1, See :func:`generate_box_mesh`. :param group_cls: see :func:`generate_box_mesh`. :param mesh_type: see :func:`generate_box_mesh`. + :param coord_dtype: see :func: `generate_box_mesh`. """ if min(n) < 2: raise ValueError("need at least two points in each direction") From 34c75ec35e61e1810360fda507f99615be65edc6 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 02:42:10 -0600 Subject: [PATCH 045/154] Allow for specifying shape in scalar loopy function generator --- meshmode/array_context.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index be8020727..bf3b5d25d 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -151,7 +151,8 @@ def loopy_implemented_elwise_func(*args): # FIXME: Maybe involve loopy type inference? result = actx.empty(args[0].shape, args[0].dtype) prg = actx._get_scalar_func_loopy_program( - c_name, nargs=len(args), naxes=len(args[0].shape)) + c_name, nargs=len(args), naxes=len(args[0].shape), + shape=args[0].shape) actx.call_loopy(prg, out=result, **{"inp%d" % i: arg for i, arg in enumerate(args)}) return result @@ -268,7 +269,7 @@ def call_loopy(self, program, **kwargs): raise NotImplementedError @memoize_method - def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): + def _get_scalar_func_loopy_program(self, c_name, nargs, naxes, shape=None): from pymbolic import var var_names = ["i%d" % i for i in range(naxes)] @@ -282,7 +283,7 @@ def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): domain_bset, = domain.get_basic_sets() - return make_loopy_program( + prog = make_loopy_program( [domain_bset], [ lp.Assignment( @@ -292,6 +293,16 @@ def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): ], name="actx_special_%s" % c_name) + if shape is not None: + prog = lp.fix_parameters(prog, + **{"n%d" % i: val for i, val in enumerate(shape)}) + + for arg in prog.args: + if isinstance(arg, lp.ArrayArg): + arg.tags = IsDOFArray() + + return prog + def freeze(self, array): """Return a version of the context-defined array *array* that is 'frozen', i.e. suitable for long-term storage and reuse. Frozen arrays @@ -519,7 +530,7 @@ def call_loopy(self, program, **kwargs): if len(wait_event_queue) > self._wait_event_queue_length: wait_event_queue.pop(0).wait() - return result + return evt, result def freeze(self, array): array.finish() @@ -548,15 +559,15 @@ def transform_loopy_program(self, program): has_dof_array = True break - if program.name == "conn_projection_knl": + if len(all_inames) == 1: + outer_iname = all_inames[0] + elif program.name == "conn_projection_knl": outer_iname = "iel" # Needed for act_special_exp etc. elif "i0" in all_inames: outer_iname = "i0" if "i1" in all_inames: inner_iname = "i1" - elif len(all_inames) == 1: - outer_iname = all_inames[0] elif has_dof_array: # Ce n'est pas tout à fait correct. Whether or not an array is a dof # array has no bearing on the names of the loop variables. The "special" From c4ff43f92abf3eccd66b4c107d521f92af281b7a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 02:48:58 -0600 Subject: [PATCH 046/154] pass coord_dtype to box generator --- meshmode/mesh/generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index f4163db60..df80a969d 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -966,7 +966,7 @@ def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), n=(5, 5), order=1, axis_coords = [np.linspace(a_i, b_i, n_i) for a_i, b_i, n_i in zip(a, b, n)] - return generate_box_mesh(axis_coords, order=order, coord_dtype=np.float64, + return generate_box_mesh(axis_coords, order=order, coord_dtype=coord_dtype, boundary_tag_to_face=boundary_tag_to_face, group_cls=group_cls, mesh_type=mesh_type) From 9028a79bc1c7db9a0a62bb9809d78260642ba43d Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 03:05:29 -0600 Subject: [PATCH 047/154] Index into tuple returned from call_loopy --- meshmode/discretization/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 21efc98ea..77c4b8d20 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -306,7 +306,7 @@ def get_mat(grp): return _DOFArray(actx, tuple( actx.call_loopy( prg(), diff_mat=actx.from_numpy(get_mat(grp)), vec=vec[grp.index] - )["result"] + )[1]["result"] for grp in self.groups)) @memoize_method @@ -369,7 +369,7 @@ def prg(): resampling_mat=actx.from_numpy( grp.from_mesh_interp_matrix()), nodes=actx.from_numpy(grp.mesh_el_group.nodes[iaxis]) - )["result"]) + )[1]["result"]) for grp in self.groups)) for iaxis in range(self.ambient_dim)]) From 192998cbfe3d17e1b5cf1b014e4d3a1c0e47dd59 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 03:27:27 -0600 Subject: [PATCH 048/154] Index into return tuple --- meshmode/discretization/__init__.py | 2 +- meshmode/dof_array.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 77c4b8d20..e7ec2ce80 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -332,7 +332,7 @@ def prg(): prg(), weights=actx.from_numpy(grp.weights), nelements=grp.nelements, - )["result"]) + )[1]["result"]) for grp in self.groups)) @memoize_method diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 413447345..5bc4dc68c 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -526,7 +526,7 @@ def prg(): grp_start=grp_start, ary=ary, nelements=nel, ndofs_per_element=ndof, - )["result"] + )[1]["result"] for grp_start, (nel, ndof) in zip(group_starts, group_shapes))) From 5c2a52ac50873770d3def956111776bd79e9fb33 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 11:34:53 -0600 Subject: [PATCH 049/154] Specify not the shape in elwise kernel for now --- meshmode/array_context.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index ef99757cd..d6053bcf5 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -151,8 +151,11 @@ def loopy_implemented_elwise_func(*args): # FIXME: Maybe involve loopy type inference? result = actx.empty(args[0].shape, args[0].dtype) prg = actx._get_scalar_func_loopy_program( - c_name, nargs=len(args), naxes=len(args[0].shape), - shape=args[0].shape) + c_name, nargs=len(args), naxes=len(args[0].shape)) + # FIXME: Specifying shape breaks some tests + #prg = actx._get_scalar_func_loopy_program( + # c_name, nargs=len(args), naxes=len(args[0].shape), + # shape=args[0].shape) actx.call_loopy(prg, out=result, **{"inp%d" % i: arg for i, arg in enumerate(args)}) return result From a5fac0888b62290df2f3b53edced86d86bb560cc Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 11:45:26 -0600 Subject: [PATCH 050/154] Trigger CI From 2c9161ec8f2794e885405f7b6490500ba182591f Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Feb 2021 12:57:13 -0600 Subject: [PATCH 051/154] update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 933e124e5..956a510e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/inducer/pytools.git@master#egg=pytools == 2021.1 +git+https://github.com/inducer/pytools.git#egg git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy @@ -11,7 +11,7 @@ git+https://github.com/inducer/islpy.git#egg=islpy git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git@#egg=loopy == 2020.2.2 +git+https://github.com/inducer/loopy.git#egg=loopy # more pytential dependencies git+https://github.com/inducer/boxtree.git#egg=boxtree From 1d0f360e282086f47b248ae333f71b8cf6a5c85b Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 22 Mar 2021 12:01:42 -0500 Subject: [PATCH 052/154] Add missing underscore to _array_context --- meshmode/array_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index d6053bcf5..a2e364222 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -75,7 +75,7 @@ def __init__(self, array_context): self.linalg = self._get_fake_numpy_linalg_namespace() def _get_fake_numpy_linalg_namespace(self): - return _BaseFakeNumpyLinalgNamespace(self.array_context) + return _BaseFakeNumpyLinalgNamespace(self._array_context) _numpy_math_functions = frozenset({ # https://numpy.org/doc/stable/reference/routines.math.html From 0a5cbd470263ab84e01a5b589e7c5b4cb96f3793 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Wed, 24 Mar 2021 17:55:18 -0500 Subject: [PATCH 053/154] remove shape argument from special function generator --- meshmode/array_context.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index a2e364222..9adf1d590 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -152,10 +152,6 @@ def loopy_implemented_elwise_func(*args): result = actx.empty(args[0].shape, args[0].dtype) prg = actx._get_scalar_func_loopy_program( c_name, nargs=len(args), naxes=len(args[0].shape)) - # FIXME: Specifying shape breaks some tests - #prg = actx._get_scalar_func_loopy_program( - # c_name, nargs=len(args), naxes=len(args[0].shape), - # shape=args[0].shape) actx.call_loopy(prg, out=result, **{"inp%d" % i: arg for i, arg in enumerate(args)}) return result @@ -272,7 +268,7 @@ def call_loopy(self, program, **kwargs): raise NotImplementedError @memoize_method - def _get_scalar_func_loopy_program(self, c_name, nargs, naxes, shape=None): + def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): from pymbolic import var var_names = ["i%d" % i for i in range(naxes)] @@ -296,10 +292,6 @@ def _get_scalar_func_loopy_program(self, c_name, nargs, naxes, shape=None): ], name="actx_special_%s" % c_name) - if shape is not None: - prog = lp.fix_parameters(prog, - **{"n%d" % i: val for i, val in enumerate(shape)}) - for arg in prog.args: if isinstance(arg, lp.ArrayArg): arg.tags = IsDOFArray() From c193ece1cb7b41dd64451901231e042a8119d0cf Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 6 Apr 2021 00:44:32 -0500 Subject: [PATCH 054/154] move ParameterValue to meshmode --- meshmode/array_context.py | 9 +++++++-- meshmode/discretization/connection/direct.py | 15 +++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 9adf1d590..241ca3ff2 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -26,7 +26,7 @@ import loopy as lp from loopy.version import MOST_RECENT_LANGUAGE_VERSION from pytools import memoize_method -from pytools.tag import Tag +from pytools.tag import Tag, UniqueTag __doc__ = """ .. autofunction:: make_loopy_program @@ -55,7 +55,7 @@ def make_loopy_program(domains, statements, kernel_data=["..."], lang_version=MOST_RECENT_LANGUAGE_VERSION) -# {{{ IsDOFArray +# {{{ Tags class IsDOFArray(Tag): """A tag to mark arrays of DOFs in :mod:`loopy` kernels. Applications @@ -64,6 +64,11 @@ class IsDOFArray(Tag): """ pass +class ParameterValue(UniqueTag): + + def __init__(self, value): + self.value = value + # }}} diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index dc1c07ac5..ad8d2731b 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -26,8 +26,7 @@ import loopy as lp from pytools import memoize_in, keyed_memoize_method from pytools.obj_array import obj_array_vectorized_n_args -from meshmode.array_context import ArrayContext, make_loopy_program -from meshmode.dof_array import IsDOFArray +from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue # {{{ interpolation batch @@ -263,7 +262,7 @@ def __call__(self, ary): actx = ary.array_context @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl")) - def mat_knl(): + def mat_knl(n_to_nodes, n_from_nodes): knl = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 3 May 2021 18:04:59 -0500 Subject: [PATCH 055/154] add ParameterValues to nodes --- meshmode/array_context.py | 4 ++++ meshmode/discretization/__init__.py | 36 +++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 241ca3ff2..f173750a7 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -300,6 +300,10 @@ def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): for arg in prog.args: if isinstance(arg, lp.ArrayArg): arg.tags = IsDOFArray() + if arg.name == "out": + arg.is_output_only = True + + return prog diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index e7ec2ce80..eb578ba52 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -24,9 +24,8 @@ from pytools import memoize_in, memoize_method from pytools.obj_array import make_obj_array -from meshmode.array_context import ArrayContext, make_loopy_program -from meshmode.dof_array import IsDOFArray -from loopy import GlobalArg, auto +from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue +from loopy import GlobalArg, ValueArg, auto # underscored because it shouldn't be imported from here. from meshmode.dof_array import DOFArray as _DOFArray @@ -345,7 +344,7 @@ def nodes(self): actx = self._setup_actx @memoize_in(actx, (Discretization, "nodes_prg")) - def prg(): + def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): return make_loopy_program( """{[iel,idof,j]: 0<=iel Date: Mon, 3 May 2021 19:44:38 -0500 Subject: [PATCH 056/154] fix parameters with tag values --- meshmode/array_context.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 241ca3ff2..285ff888c 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -543,8 +543,12 @@ def transform_loopy_program(self, program): "Did you use meshmode.array_context.make_loopy_program " "to create this program?") - # FIXME: This could be much smarter. import loopy as lp + for arg in program.args: + if isinstance(arg.tags, ParameterValue): + program = lp.fix_parameters(program, **{arg.name: arg.tags.value}) + + # FIXME: This could be much smarter. # accommodate loopy with and without kernel callables try: all_inames = program.all_inames() From 8e5c5d222aa6e36891912703f305a064504a5fbd Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 4 May 2021 00:55:19 -0500 Subject: [PATCH 057/154] taggable numpy array, undo shape specification for nodes --- meshmode/discretization/__init__.py | 29 +++++++++++++++++++---------- meshmode/taggable_numpy_array.py | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 meshmode/taggable_numpy_array.py diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index eb578ba52..d1790e4d6 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -344,8 +344,9 @@ def nodes(self): actx = self._setup_actx @memoize_in(actx, (Discretization, "nodes_prg")) - def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): - return make_loopy_program( + def prg(): + #def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): + result = make_loopy_program( """{[iel,idof,j]: 0<=iel Date: Sat, 22 May 2021 11:31:29 -0500 Subject: [PATCH 058/154] push memory usage --- meshmode/discretization/connection/direct.py | 3 +- meshmode/discretization/connection/modal.py | 372 +++++++++++++++++++ meshmode/dof_array.py | 67 ++++ meshmode/mesh/__init__.py | 2 +- 4 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 meshmode/discretization/connection/modal.py diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index ad8d2731b..17e79efc0 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -279,7 +279,8 @@ def mat_knl(n_to_nodes, n_from_nodes): shape="nelements_vec, n_from_nodes", offset=lp.auto, tags=IsDOFArray()), lp.ValueArg("n_to_nodes", tags=ParameterValue(n_to_nodes)), - lp.ValueArg("n_from_nodes", tags=ParameterValue(n_from_nodes)), + # Specifying this breaks order 4 for some reason + #lp.ValueArg("n_from_nodes", tags=ParameterValue(n_from_nodes)), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), "...", diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py new file mode 100644 index 000000000..799b0706f --- /dev/null +++ b/meshmode/discretization/connection/modal.py @@ -0,0 +1,372 @@ +__copyright__ = """ +Copyright (C) 2021 University of Illinois Board of Trustees +""" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +import numpy as np +import numpy.linalg as la +import modepy as mp + +from meshmode.array_context import make_loopy_program +from meshmode.dof_array import DOFArray +from meshmode.discretization import InterpolatoryElementGroupBase +from meshmode.discretization.poly_element import QuadratureSimplexElementGroup +from meshmode.discretization.connection.direct import DiscretizationConnection + +from pytools import memoize_in, keyed_memoize_in +from pytools.obj_array import obj_array_vectorized_n_args + + +class NodalToModalDiscretizationConnection(DiscretizationConnection): + r"""A concrete subclass of :class:`DiscretizationConnection`, which + maps nodal data to its modal representation. For interpolatory + (unisolvent) element groups, the mapping from nodal to modal + representations is performed via: + + .. math:: + + y = V^{-1} [\text{nodal basis coefficients}] + + where :math:`V_{i,j} = \phi_j(x_i)` is the generalized + Vandermonde matrix, :math:`\phi_j` is a nodal basis, + and :math:`x_i` are nodal points on the reference + element defining the nodal discretization. + + For non-interpolatory element groups (for example, + :class:`~meshmode.discretization.poly_element.QuadratureSimplexElementGroup`), + modal coefficients are computed using the underlying quadrature rule + :math:`(w_q, x_q)`, and an orthonormal basis :math:`\psi_i` + spanning the modal discretization space. The modal coefficients + are then obtained via: + + .. math:: + + y = V^T W [\text{nodal basis coefficients}] + + where :math:`V_{i, j} = \psi_j(x_i)` is the Vandermonde matrix + constructed from the orthonormal basis evaluated at the quadrature + nodes :math:`x_i`, and :math:`W = \text{Diag}(w_q)` is a diagonal + matrix containing the quadrature weights :math:`w_q`. + + .. note:: + + This connection requires that both nodal and modal discretizations + are defined on the *same* mesh. + + .. attribute:: from_discr + + An instance of :class:`meshmode.discretization.Discretization` containing + :class:`~meshmode.discretization.NodalElementGroupBase` element groups + + .. attribute:: to_discr + + An instance of :class:`meshmode.discretization.Discretization` containing + :class:`~meshmode.discretization.ModalElementGroupBase` element groups + + .. attribute:: groups + + A list of :class:`DiscretizationConnectionElementGroup` + instances, with a one-to-one correspondence to the groups in + :attr:`to_discr`. + + .. automethod:: __init__ + .. automethod:: __call__ + + """ + + def __init__(self, from_discr, to_discr, allow_approximate_quad=False): + """ + :arg from_discr: a :class:`meshmode.discretization.Discretization` + containing :class:`~meshmode.discretization.NodalElementGroupBase` + element groups. + :arg to_discr: a :class:`meshmode.discretization.Discretization` + containing :class:`~meshmode.discretization.ModalElementGroupBase` + element groups. + :arg allow_approximate_quad: an optional :class:`bool` flag indicating + whether to proceed with numerically approximating (via quadrature) + modal coefficients, even when the underlying quadrature method + is not exact. The default value is *False*. + """ + + if not from_discr.is_nodal: + raise ValueError("`from_discr` must be defined on nodal " + "element groups to use this connection.") + + if not to_discr.is_modal: + raise ValueError("`to_discr` must be defined on modal " + "element groups to use this connection.") + + if to_discr.mesh is not from_discr.mesh: + raise ValueError("Both `from_discr` and `to_discr` must be on " + "the same mesh.") + + super().__init__( + from_discr=from_discr, + to_discr=to_discr, + is_surjective=True) + + self._allow_approximate_quad = allow_approximate_quad + + def _project_via_quadrature(self, actx, ary, grp, mgrp): + # Handle the case with non-interpolatory element groups or + # quadrature-based element groups + @memoize_in(actx, (NodalToModalDiscretizationConnection, + "apply_quadrature_proj_knl")) + def quad_proj_keval(): + return make_loopy_program([ + "{[iel]: 0 <= iel < nelements}", + "{[idof]: 0 <= idof < n_to_dofs}", + "{[ibasis]: 0 <= ibasis < n_from_dofs}" + ], + """ + result[iel, idof] = sum(ibasis, + vtw[idof, ibasis] + * nodal_coeffs[iel, ibasis]) + """, + name="apply_quadrature_proj_knl") + + @keyed_memoize_in(actx, (NodalToModalDiscretizationConnection, + "quadrature_matrix"), + lambda grp, mgrp: ( + grp.discretization_key(), + mgrp.discretization_key(), + )) + def quadrature_matrix(grp, mgrp): + vdm = mp.vandermonde(mgrp.basis_obj().functions, + grp.unit_nodes) + w_diag = np.diag(grp.weights) + vtw = np.dot(vdm.T, w_diag) + return actx.from_numpy(vtw) + + output = actx.call_loopy(quad_proj_keval(), + nodal_coeffs=ary[grp.index], + vtw=quadrature_matrix(grp, mgrp)) + + return output + + def _compute_coeffs_via_inv_vandermonde(self, actx, ary, grp): + + # Simple mat-mul kernel to apply the inverse of the + # Vandermonde matrix + @memoize_in(actx, (NodalToModalDiscretizationConnection, + "apply_inv_vandermonde_knl")) + def vinv_keval(): + return make_loopy_program([ + "{[iel]: 0 <= iel < nelements}", + "{[idof]: 0 <= idof < n_from_dofs}", + "{[jdof]: 0 <= jdof < n_from_dofs}" + ], + """ + result[iel, idof] = sum(jdof, + vdm_inv[idof, jdof] + * nodal_coeffs[iel, jdof]) + """, + name="apply_inv_vandermonde_knl") + + @keyed_memoize_in(actx, (NodalToModalDiscretizationConnection, + "vandermonde_inverse"), + lambda grp: grp.discretization_key()) + def vandermonde_inverse(grp): + vdm = mp.vandermonde(grp.basis_obj().functions, + grp.unit_nodes) + vdm_inv = la.inv(vdm) + return actx.from_numpy(vdm_inv) + + output = actx.call_loopy(vinv_keval(), + vdm_inv=vandermonde_inverse(grp), + nodal_coeffs=ary[grp.index]) + + return output + + @obj_array_vectorized_n_args + def __call__(self, ary): + """Computes modal coefficients data from a functions + nodal coefficients. + + :arg ary: a :class:`meshmode.dof_array.DOFArray` containing + nodal coefficient data. + """ + + if not isinstance(ary, DOFArray): + raise TypeError("Non-array passed to discretization connection") + + if ary.shape != (len(self.from_discr.groups),): + raise ValueError("Invalid shape of incoming nodal data") + + actx = ary.array_context + result_data = [] + + for igrp, grp in enumerate(self.from_discr.groups): + + mgrp = self.to_discr.groups[igrp] + + # For element groups without an interpolatory nodal basis, + # we use an orthonormal basis and a quadrature rule + # to compute the modal coefficients. + if isinstance(grp, QuadratureSimplexElementGroup): + + if ( + grp._quadrature_rule().exact_to < 2*mgrp.order + and not self._allow_approximate_quad + ): + raise ValueError("Quadrature rule is not exact, please " + "set `allow_approximate_quad=True`") + + output = self._project_via_quadrature( + actx, ary, grp, mgrp) + + # Handle all other interpolatory element groups by + # inverting the Vandermonde matrix to compute the + # modal coefficients + elif isinstance(grp, InterpolatoryElementGroupBase): + output = self._compute_coeffs_via_inv_vandermonde( + actx, ary, grp) + else: + raise NotImplementedError( + "Don't know how to project from group types " + "%s to %s" % (grp.__class__.__name__, + mgrp.__class__.__name__) + ) + + result_data.append(output["result"]) + + return DOFArray(actx, data=tuple(result_data)) + + +class ModalToNodalDiscretizationConnection(DiscretizationConnection): + r"""A concrete subclass of :class:`DiscretizationConnection`, which + maps modal data back to its nodal representation. This is computed + via: + + .. math:: + + y = V [\text{modal basis coefficients}] + + where :math:`V_{i,j} = \phi_j(x_i)` is the generalized + Vandermonde matrix, :math:`\phi_j` is an orthonormal (modal) + basis, and :math:`x_i` are nodal points on the reference + element defining the nodal discretization. + + .. note:: + + This connection requires that both nodal and modal discretizations + are defined on the *same* mesh. + + .. attribute:: from_discr + + An instance of :class:`meshmode.discretization.Discretization` containing + :class:`~meshmode.discretization.ModalElementGroupBase` element groups + + .. attribute:: to_discr + + An instance of :class:`meshmode.discretization.Discretization` containing + :class:`~meshmode.discretization.NodalElementGroupBase` element groups + + .. attribute:: groups + + A list of :class:`DiscretizationConnectionElementGroup` + instances, with a one-to-one correspondence to the groups in + :attr:`to_discr`. + + .. automethod:: __init__ + .. automethod:: __call__ + + """ + + def __init__(self, from_discr, to_discr): + """ + :arg from_discr: a :class:`meshmode.discretization.Discretization` + containing :class:`~meshmode.discretization.ModalElementGroupBase` + element groups. + :arg to_discr: a :class:`meshmode.discretization.Discretization` + containing :class:`~meshmode.discretization.NodalElementGroupBase` + element groups. + """ + + if not from_discr.is_modal: + raise ValueError("`from_discr` must be defined on modal " + "element groups to use this connection.") + + if not to_discr.is_nodal: + raise ValueError("`to_discr` must be defined on nodal " + "element groups to use this connection.") + + if to_discr.mesh is not from_discr.mesh: + raise ValueError("Both `from_discr` and `to_discr` must be on " + "the same mesh.") + + super().__init__( + from_discr=from_discr, + to_discr=to_discr, + is_surjective=True) + + @obj_array_vectorized_n_args + def __call__(self, ary): + """Computes nodal coefficients from modal data. + + :arg ary: a :class:`meshmode.dof_array.DOFArray` containing + modal coefficient data. + """ + + if not isinstance(ary, DOFArray): + raise TypeError("Non-array passed to discretization connection") + + if ary.shape != (len(self.from_discr.groups),): + raise ValueError("Invalid shape of incoming modal data") + + actx = ary.array_context + + # Evaluates the action of the Vandermonde matrix on the + # vector of modal coefficeints to obtain nodal values + @memoize_in(actx, (ModalToNodalDiscretizationConnection, + "evaluation_knl")) + def keval(): + return make_loopy_program([ + "{[iel]: 0 <= iel < nelements}", + "{[idof]: 0 <= idof < n_to_dofs}", + "{[ibasis]: 0 <= ibasis < n_from_dofs}" + ], + """ + result[iel, idof] = sum(ibasis, + vdm[idof, ibasis] + * coefficients[iel, ibasis]) + """, + name="modal_to_nodal_evaluation_knl") + + @keyed_memoize_in(actx, (ModalToNodalDiscretizationConnection, "matrix"), + lambda to_grp, from_grp: ( + to_grp.discretization_key(), + from_grp.discretization_key(), + )) + def matrix(to_grp, from_grp): + vdm = mp.vandermonde(from_grp.basis_obj().functions, + to_grp.unit_nodes) + return actx.from_numpy(vdm) + + result_data = tuple( + actx.call_loopy(keval(), + vdm=matrix(grp, self.from_discr.groups[igrp]), + coefficients=ary[grp.index])["result"] + for igrp, grp in enumerate(self.to_discr.groups) + ) + return DOFArray(actx, data=result_data) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 5bc4dc68c..3861137e0 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -55,6 +55,14 @@ # {{{ DOFArray +from dataclasses import dataclass + + +@dataclass(frozen=True, repr=True) +class MyFrameSummary: + filename: str + lineno: int + func_name: str class DOFArray: r"""This array type holds degree-of-freedom arrays for use with @@ -131,6 +139,11 @@ class DOFArray: to :func:`array_context_for_pickling`. """ + total_alloc = 0 + alloc_frames = {} + cum_alloc = 0 + n_cum_alloc = 0 + def __init__(self, actx: Optional[ArrayContext], data: Tuple[Any]): if not (actx is None or isinstance(actx, ArrayContext)): raise TypeError("actx must be of type ArrayContext") @@ -138,9 +151,63 @@ def __init__(self, actx: Optional[ArrayContext], data: Tuple[Any]): if not isinstance(data, tuple): raise TypeError("'data' argument must be a tuple") + alloc_size = sum(d.size for d in data) + self.array_context = actx self._data = data + DOFArray.total_alloc += alloc_size + DOFArray.cum_alloc += alloc_size + from traceback import extract_stack + s = tuple(MyFrameSummary(filename=fs.filename, lineno=fs.lineno, + func_name=fs.name) + for fs in extract_stack()) + DOFArray.alloc_frames[s] = DOFArray.alloc_frames.get(s, 0) + 1 + self._alloc_frame = s + nallocs = sum(DOFArray.alloc_frames.values()) + DOFArray.n_cum_alloc += 1 + + #if True: + # print(f"ALLOC {alloc_size} -> {DOFArray.total_alloc} ({nallocs})") + + #if nallocs >= 62: + # self.print_allocation_summary() + # 1/0 + + @classmethod + def print_allocation_summary(cls): + nallocs = sum(DOFArray.alloc_frames.values()) + print(f"{nallocs} allocations total") + for stack, count in cls.alloc_frames.items(): + if count: + print(78*"-") + print(f"{count} allocations for stack:") + for f in stack: + print(f) + + """ + def __del__(self): + + active_bytes = self.array_context.allocator.active_bytes/1e9 + print(f"ACTIVE BYTES BEFORE DELETE: {active_bytes}") + #for entry in self._data: + # pass + #entry.base_data.release() + #active_bytes = self.array_context.allocator.active_bytes/1e9 + #print(f"ACTIVE BYTES AFTER DELETE: {active_bytes}") + + + alloc_size = sum(d.size for d in self._data) + DOFArray.total_alloc -= alloc_size + s = self._alloc_frame + DOFArray.alloc_frames[s] = DOFArray.alloc_frames.get(s, 0) - 1 + if True: + print(f"FREE {alloc_size} -> {DOFArray.total_alloc} " + f"({sum(DOFArray.alloc_frames.values())})") + """ + + + # Tell numpy that we would like to do our own array math, thank you very much. # (numpy arrays have priority 0.) __array_priority__ = 10 diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py index a29b1e588..433f5e2ee 100644 --- a/meshmode/mesh/__init__.py +++ b/meshmode/mesh/__init__.py @@ -210,7 +210,7 @@ def __init__(self, order, vertex_indices, nodes, Do not supply *element_nr_base* and *node_nr_base*, they will be automatically assigned. """ - + super().__init__( order=order, vertex_indices=vertex_indices, From 8143728e33ef926a4b99be0dab2c38280c577ad5 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 23 May 2021 22:15:15 -0500 Subject: [PATCH 059/154] remove coord_dtype, add return statement --- meshmode/discretization/__init__.py | 1 + meshmode/mesh/generation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 587dd0b43..82f7c9681 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -593,6 +593,7 @@ def prg(): ... ], name="nodes") + return result def resample_mesh_nodes(grp, iaxis): # TODO: would be nice to have the mesh use an array context already diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index 3e041b9c8..00d686e1e 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -1076,7 +1076,7 @@ def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), *, nelements_per_axis=None, axis_coords = [np.linspace(a_i, b_i, npoints_i) for a_i, b_i, npoints_i in zip(a, b, npoints_per_axis)] - return generate_box_mesh(axis_coords, order=order, coord_dtype=coord_dtype, + return generate_box_mesh(axis_coords, order=order, boundary_tag_to_face=boundary_tag_to_face, group_cls=group_cls, mesh_type=mesh_type) From 16fd54cba5deb2db18609400d17f80910872562a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 May 2021 01:58:12 -0500 Subject: [PATCH 060/154] fix return indexing --- meshmode/array_context.py | 2 +- meshmode/discretization/__init__.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index a7768c562..59c58a4d3 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -708,6 +708,7 @@ def thaw(self, array): @memoize_method def transform_loopy_program(self, t_unit): + print(t_unit.name) # accommodate loopy with and without kernel callables default_entrypoint = _loopy_get_default_entrypoint(t_unit) @@ -718,7 +719,6 @@ def transform_loopy_program(self, t_unit): "Did you use meshmode.array_context.make_loopy_program " "to create this kernel?") -# Need to fix this since "program" is no longer a variable for arg in t_unit.args: if isinstance(arg.tags, ParameterValue): t_unit = lp.fix_parameters(t_unit, **{arg.name: arg.tags.value}) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 82f7c9681..f7e6d810d 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -611,7 +611,8 @@ def resample_mesh_nodes(grp, iaxis): prg(), resampling_mat=actx.from_numpy(grp.from_mesh_interp_matrix()), nodes=nodes, - )["result"] + )[1]["result"] + result = make_obj_array([ _DOFArray(None, tuple([ @@ -655,6 +656,10 @@ def prg(): return make_loopy_program( "{[iel,idof,j]: 0 <= iel < nelements and 0 <= idof, j < nunit_dofs}", "result[iel,idof] = sum(j, diff_mat[idof, j] * vec[iel, j])", + kernel_data=[ + GlobalArg("vec", None, shape=auto, tags=IsDOFArray()), + ... + ], name="diff") @keyed_memoize_in(actx, @@ -677,7 +682,7 @@ def get_mat(grp, gref_axes): return _DOFArray(actx, tuple( actx.call_loopy( prg(), diff_mat=get_mat(grp, ref_axes), vec=vec[grp.index] - )["result"] + )[1]["result"] for grp in discr.groups)) # }}} From 9b3d2658c52f9818e122f2a04e4bbf4bd31241ad Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 May 2021 02:39:36 -0500 Subject: [PATCH 061/154] More support for coord_dtype --- meshmode/mesh/generation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index 00d686e1e..84c723f49 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -1025,6 +1025,7 @@ def generate_box_mesh(axis_coords, order=1, coord_dtype=np.float64, def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), *, nelements_per_axis=None, npoints_per_axis=None, order=1, + coord_dtype=np.float32, boundary_tag_to_face=None, group_cls=None, mesh_type=None, @@ -1076,7 +1077,7 @@ def generate_regular_rect_mesh(a=(0, 0), b=(1, 1), *, nelements_per_axis=None, axis_coords = [np.linspace(a_i, b_i, npoints_i) for a_i, b_i, npoints_i in zip(a, b, npoints_per_axis)] - return generate_box_mesh(axis_coords, order=order, + return generate_box_mesh(axis_coords, order=order, coord_dtype=coord_dtype, boundary_tag_to_face=boundary_tag_to_face, group_cls=group_cls, mesh_type=mesh_type) From 423df31cbde7c6b35a04ae7f939d06ac90e0f6a8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 May 2021 23:20:25 -0500 Subject: [PATCH 062/154] Add IsOpArry tag --- meshmode/array_context.py | 3 +++ meshmode/discretization/connection/direct.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 59c58a4d3..a9a12ea91 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -74,6 +74,9 @@ class IsDOFArray(Tag): """ pass +class IsOpArray(Tag): + pass + class ParameterValue(UniqueTag): def __init__(self, value): diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 03a88f016..e1c1c3a8c 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -26,7 +26,7 @@ import loopy as lp from pytools import memoize_in, keyed_memoize_method from pytools.obj_array import obj_array_vectorized_n_args -from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue +from meshmode.array_context import ArrayContext, make_loopy_program, IsOpArray, IsDOFArray, ParameterValue # {{{ interpolation batch @@ -293,6 +293,9 @@ def mat_knl(n_to_nodes, n_from_nodes): lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", offset=lp.auto, tags=IsDOFArray()), + lp.GlobalArg("resample_mat", None, + shape="n_to_nodes, n_from_nodes", + offset=lp.auto, tags=IsOpArray()), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", offset=lp.auto, tags=IsDOFArray()), @@ -309,7 +312,7 @@ def mat_knl(n_to_nodes, n_from_nodes): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_picking_knl")) - def pick_knl(): + def pick_knl(n_to_nodes): knl = make_loopy_program( """{[iel, idof]: 0<=iel Date: Wed, 9 Jun 2021 01:48:01 -0500 Subject: [PATCH 063/154] pass shape and dtype into special function generator --- meshmode/array_context.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index a9a12ea91..4ee335a8d 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -180,7 +180,7 @@ def loopy_implemented_elwise_func(*args): # FIXME: Maybe involve loopy type inference? result = actx.empty(args[0].shape, args[0].dtype) prg = actx._get_scalar_func_loopy_program( - c_name, nargs=len(args), naxes=len(args[0].shape)) + c_name, nargs=len(args), naxes=len(args[0].shape), shape=args[0].shape, dtype=args[0].dtype) actx.call_loopy(prg, out=result, **{"inp%d" % i: arg for i, arg in enumerate(args)}) return result @@ -350,7 +350,7 @@ def call_loopy(self, program, **kwargs): """ @memoize_method - def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): + def _get_scalar_func_loopy_program(self, c_name, nargs, naxes, shape=None, dtype=None): from pymbolic import var var_names = ["i%d" % i for i in range(naxes)] @@ -377,10 +377,14 @@ def _get_scalar_func_loopy_program(self, c_name, nargs, naxes): for arg in prog.args: if isinstance(arg, lp.ArrayArg): arg.tags = IsDOFArray() - if arg.name == "out": - arg.is_output_only = True - - + if shape is not None: + arg.shape = shape + if dtype is not None: + arg.dtype = dtype + if arg.name == "out": + arg.is_output_only = True + if isinstance(arg, lp.ValueArg) and shape is not None: + arg.tags = ParameterValue(shape[int(arg.name[1])]) return prog From 5e4332a0dc2eed5034a3720d7173d7c970b2fee2 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Wed, 9 Jun 2021 22:56:32 -0500 Subject: [PATCH 064/154] Pass sizes to nodes function --- meshmode/discretization/__init__.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index f7e6d810d..47914627b 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -571,8 +571,8 @@ def nodes(self, cached=True): raise ElementGroupTypeError("Element groups must be nodal.") @memoize_in(actx, (Discretization, "nodes_prg")) - def prg(): - #def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): + #def prg(): + def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): result = make_loopy_program( """{[iel,idof,j]: 0<=iel Date: Thu, 10 Jun 2021 23:50:57 -0500 Subject: [PATCH 065/154] Fix special function datatype specification --- meshmode/array_context.py | 4 +++- meshmode/discretization/__init__.py | 5 ++--- meshmode/dof_array.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 4ee335a8d..75a0ee2a6 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -179,8 +179,10 @@ def loopy_implemented_elwise_func(*args): actx = self._array_context # FIXME: Maybe involve loopy type inference? result = actx.empty(args[0].shape, args[0].dtype) + from loopy.types import to_loopy_type + dtype = to_loopy_type(args[0].dtype) prg = actx._get_scalar_func_loopy_program( - c_name, nargs=len(args), naxes=len(args[0].shape), shape=args[0].shape, dtype=args[0].dtype) + c_name, nargs=len(args), naxes=len(args[0].shape), shape=args[0].shape, dtype=dtype) actx.call_loopy(prg, out=result, **{"inp%d" % i: arg for i, arg in enumerate(args)}) return result diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 47914627b..96496ac5a 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -29,7 +29,7 @@ from abc import ABCMeta, abstractproperty, abstractmethod from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array -from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue +from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue, IsOpArray from loopy import GlobalArg, ValueArg, auto from warnings import warn @@ -585,8 +585,7 @@ def prg(nelements, ndiscr_nodes, nmesh_nodes, fp_format): kernel_data=[ GlobalArg("result", fp_format, shape=(nelements, ndiscr_nodes), tags=IsDOFArray()), GlobalArg("nodes", fp_format, shape=auto, tags=None), - GlobalArg("resampling_mat", fp_format, shape=(ndiscr_nodes, nmesh_nodes)), - # Many errors when these are specified + GlobalArg("resampling_mat", fp_format, shape=(ndiscr_nodes, nmesh_nodes), tags=IsOpArray()), ValueArg("nelements", tags=ParameterValue(nelements)), ValueArg("ndiscr_nodes", tags=ParameterValue(ndiscr_nodes)), ValueArg("nmesh_nodes", tags=ParameterValue(nmesh_nodes)), diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 557e1216d..7fe575da3 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -567,13 +567,15 @@ def flatten(ary: Union[DOFArray, np.ndarray]) -> Any: actx = ary.array_context @memoize_in(actx, (flatten, "flatten_prg")) - def prg(): + def prg(nelements, ndofs, dtype): return make_loopy_program( "{[iel,idof]: 0<=iel Date: Fri, 11 Jun 2021 11:15:35 -0500 Subject: [PATCH 066/154] add missing imports --- meshmode/dof_array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 7fe575da3..f5c126737 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -23,7 +23,7 @@ import operator as op import numpy as np from typing import Optional, Iterable, Any, Tuple, Union -from loopy import GlobalArg, auto +from loopy import GlobalArg, ValueArg, auto from functools import partial, update_wrapper from numbers import Number import threading @@ -32,7 +32,7 @@ from pytools import single_valued, memoize_in from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args -from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray +from meshmode.array_context import ArrayContext, make_loopy_program, IsDOFArray, ParameterValue __doc__ = """ .. autoclass:: DOFArray From 66174b180fbbbafa048b93ff23e17b8c5d2ffa2d Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Thu, 15 Jul 2021 21:10:32 -0500 Subject: [PATCH 067/154] enable resample_by_mat autotuning --- meshmode/discretization/connection/direct.py | 48 +++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index fc884eb95..a19ee5592 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -484,7 +484,7 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) - def mat_knl(n_to_nodes, n_from_nodes): + def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el_ind, n_to_el_ind, fp_format, index_dtype): knl = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 23 Aug 2021 14:49:39 -0500 Subject: [PATCH 068/154] Indirection permutation code --- meshmode/discretization/connection/direct.py | 97 ++++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index a19ee5592..fc0acf54d 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -178,6 +178,24 @@ def __init__(self, batches): # }}} +# Creates the inverse mapping of an indirection array +def Pinv(p): + p2 = np.empty_like(p) + p2[p] = np.arange(0,len(p), dtype=np.int32) + return p2 + +# Combine two indirection arrays p_l and p_r into a new +# indirection array p_c so that +# if B[p_l[i]] = A[p_r[i]] then B[i] = A[p_c[i]] +# or, alternatively, B[p_c[i]] = A[i] if lhs=True. +# Alternatively, flipping the order of the input arguments +# will do the same thing. +def combine_indirection_arrays(p_l, p_r, lhs=False): + if lhs: + return p_l[Pinv(p_r)] + else: + return p_r[Pinv(p_l)] + # {{{ connection classes @@ -527,6 +545,7 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_picking_knl_inplace")) def pick_knl(n_to_nodes): + knl = make_loopy_program( """{[iel, idof]: 0<=iel just use offset + # lhs is some zero based indexing + + to_element_indices = batch.to_element_indices.get(queue=actx.queue) + from_element_indices = batch.from_element_indices.get(queue=actx.queue) + #print(to_element_indices) + #print(from_element_indices) + #print(to_element_indices.shape) + #print(from_element_indices.shape) + #offset = to_element_indices[0] + #compare_to = np.arange(offset, len(to_element_indices) + offset, dtype=np.int64) + indirection = "both" + #compare = "rhs" if np.allclose(compare_to, to_element_indices) else "both" + #if compare == "both": + # print(np.array(sorted(to_element_indices))) + # print(np.array(sorted(from_element_indices))) + # exit() + + + if True:#indirection == "both": + actx.call_loopy(pick_knl(n_to_nodes), + pick_list=point_pick_indices, + result=result[i_tgrp], + ary=ary[batch.from_group_index], + from_element_indices=batch.from_element_indices, + to_element_indices=batch.to_element_indices) + elif indirection == "rhs": + # This also doesn't work due to the offset on the receiving side + #from pyopencl.array import to_device + #indirection_array = to_device(actx.queue, indirection_array) + #print(indirection_array.get()) + + actx.call_loopy(pick_knl_rhs(n_to_nodes, offset), + pick_list=point_pick_indices, + result=result[i_tgrp], + ary=ary[batch.from_group_index], + from_element_indices=from_element_indices) + + else: + raise ValueError("value must be 'both', 'rhs'") return result From 63dbbea3f2be04fec92e63b85c784dbf08380f94 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 24 Aug 2021 23:09:50 -0500 Subject: [PATCH 069/154] wrap tags in list --- meshmode/array_context.py | 2 +- meshmode/discretization/connection/direct.py | 32 +++++++++---------- .../discretization/connection/projection.py | 2 +- meshmode/dof_array.py | 4 +-- meshmode/taggable_numpy_array.py | 26 --------------- 5 files changed, 20 insertions(+), 46 deletions(-) delete mode 100644 meshmode/taggable_numpy_array.py diff --git a/meshmode/array_context.py b/meshmode/array_context.py index ab96b334a..80aa7938a 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -30,7 +30,7 @@ from arraycontext import ( # noqa: F401 ArrayContext, CommonSubexpressionTag, FirstAxisIsElementsTag, - ParameterValue, IsDOFArray, + ParameterValue, IsDOFArray, IsOpArray, ArrayContainer, is_array_container, is_array_container_type, serialize_container, deserialize_container, diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 74aa5baf6..a83a1442d 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -495,27 +495,27 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el lp.GlobalArg("result", fp_format, #shape="nelements_result, n_to_nodes", shape=(nelements_result, n_to_nodes), - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("resample_mat", fp_format, #shape="n_to_nodes, n_from_nodes", shape=(n_to_nodes, n_from_nodes), - offset=lp.auto, tags=IsOpArray()), + offset=lp.auto, tags=[IsOpArray()]), lp.GlobalArg("ary", fp_format, #shape="nelements_vec, n_from_nodes", shape=(nelements_vec, n_from_nodes), - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("from_element_indices", index_dtype, shape=(n_from_el_ind,), offset=lp.auto), lp.GlobalArg("to_element_indices", index_dtype, shape=(n_to_el_ind,), offset=lp.auto), - lp.ValueArg("n_to_nodes", tags=ParameterValue(n_to_nodes)), + lp.ValueArg("n_to_nodes", tags=[ParameterValue(n_to_nodes)]), # Specifying this breaks order 4 for some reason - lp.ValueArg("n_from_nodes", tags=ParameterValue(n_from_nodes)), - lp.ValueArg("nelements", np.int32, tags=ParameterValue(n_from_nodes)), # I'm guessing - lp.ValueArg("nelements_result", np.int32, tags=ParameterValue(nelements_result)), - lp.ValueArg("nelements_vec", np.int32, tags=ParameterValue(nelements_vec)), + lp.ValueArg("n_from_nodes", tags=[ParameterValue(n_from_nodes)]), + lp.ValueArg("nelements", np.int32, tags=[ParameterValue(n_from_nodes)]), # I'm guessing + lp.ValueArg("nelements_result", np.int32, tags=[ParameterValue(nelements_result)]), + lp.ValueArg("nelements_vec", np.int32, tags=[ParameterValue(nelements_vec)]), "...", ], name="resample_by_mat_inplace") @@ -536,14 +536,14 @@ def pick_knl(n_to_nodes): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), lp.ValueArg("n_from_nodes", np.int32), - lp.ValueArg("n_to_nodes", np.int32, tags=ParameterValue(n_to_nodes)), + lp.ValueArg("n_to_nodes", np.int32, tags=[ParameterValue(n_to_nodes)]), "...", ], name="resample_by_picking_inplace") @@ -565,15 +565,15 @@ def pick_knl_rhs(n_to_nodes, offset): [ lp.GlobalArg("result", None, shape="nelements_result, n_to_nodes", - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.ValueArg("nelements_result", np.int32), lp.ValueArg("nelements_vec", np.int32), lp.ValueArg("n_from_nodes", np.int32), - lp.ValueArg("n_to_nodes", np.int32, tags=ParameterValue(n_to_nodes)), - lp.ValueArg("offset", tags=ParameterValue(offset)), + lp.ValueArg("n_to_nodes", np.int32, tags=[ParameterValue(n_to_nodes)]), + lp.ValueArg("offset", tags=[ParameterValue(offset)]), "..." ], name="resample_by_picking_inplace_rhs") @@ -717,7 +717,7 @@ def knl(): [ lp.GlobalArg("result", None, shape="nnodes_tgt, nnodes_src", - offset=lp.auto, tags=IsDOFArray()), + offset=lp.auto, tags=[IsDOFArray()]), lp.ValueArg("itgt_base, isrc_base", np.int32), lp.ValueArg("nnodes_tgt, nnodes_src", np.int32), "...", diff --git a/meshmode/discretization/connection/projection.py b/meshmode/discretization/connection/projection.py index ed01b015a..2728dadb9 100644 --- a/meshmode/discretization/connection/projection.py +++ b/meshmode/discretization/connection/projection.py @@ -159,7 +159,7 @@ def kproj(): shape=("n_from_elements", "n_from_nodes")), lp.GlobalArg("result", None, shape=("n_to_elements", "n_to_nodes"), - is_input=False, tags=IsDOFArray()), + is_input=False, tags=[IsDOFArray()]), lp.GlobalArg("basis_tabulation", None, shape=("n_to_nodes", "n_to_nodes")), lp.GlobalArg("weights", None, diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 080a72fc7..d6c0a6969 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -492,7 +492,7 @@ def prg(): lp.GlobalArg("result", None, shape="nelements * ndofs_per_element"), lp.GlobalArg("grp_ary", None, - shape=("nelements", "ndofs_per_element"), tags=IsDOFArray()), + shape=("nelements", "ndofs_per_element"), tags=[IsDOFArray()]), lp.ValueArg("nelements", np.int32), lp.ValueArg("ndofs_per_element", np.int32), "..." @@ -562,7 +562,7 @@ def prg(): "{[iel,idof]: 0<=iel Date: Mon, 20 Sep 2021 14:59:39 -0500 Subject: [PATCH 070/154] wrap tags in list --- meshmode/discretization/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 0d78de278..b9e5baf59 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -539,7 +539,7 @@ def prg(): "{[iel,idof]: 0<=iel Date: Mon, 20 Sep 2021 17:44:04 -0500 Subject: [PATCH 071/154] Kernel tags, variable name updates --- meshmode/discretization/__init__.py | 28 +++++++++++++++----- meshmode/discretization/connection/direct.py | 4 +-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index b9e5baf59..a9d6352b1 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -671,12 +671,16 @@ def num_reference_derivative( f"got {ref_axes} for dimension {discr.dim}") @memoize_in(actx, (num_reference_derivative, "reference_derivative_prg")) - def prg(): + def prg(nelments, nunit_dofs, fp_format): t_unit = make_loopy_program( "{[iel,idof,j]: 0 <= iel < nelements and 0 <= idof, j < nunit_dofs}", "result[iel,idof] = sum(j, diff_mat[idof, j] * vec[iel, j])", kernel_data=[ - GlobalArg("vec", None, shape=auto, tags=[IsDOFArray()]), + GlobalArg("vec", fp_format, shape=(nelements,nunit_dofs), tags=[IsDOFArray()]), + GlobalArg("result", fp_format, shape=(nelements,nunit_dofs), tags=[IsDOFArray()]), + GlobalArg("diff_mat", fp_format, shape=(nunit_dofs,nunit_dofs), tags=[IsOpArray()]), + ValueArg("nelements", tags=[ParameterValue(nelements)]), + ValueArg("nunit_dofs", tags=[ParameterValue(nunit_dofs)]), ... ], name="diff") @@ -702,11 +706,21 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) - return _DOFArray(actx, tuple( - actx.call_loopy( - prg(), diff_mat=get_mat(grp, ref_axes), vec=vec[grp.index] - )[1]["result"] - for grp in discr.groups)) + results = [] + for grp in discr.groups: + nelements, nunit_dofs = vec[grp.index].shape + fp_format = vec[grp.index].dtype + output = actx.call_loopy(prg(nelements, nunit_dofs, fp_format), + diff_mat=get_mat(grp, ref_axes), vec=vec[grp.index]) + results.append(output[1]["result"]) + + return _DOFArray(actx, tuple(results)) + + #return _DOFArray(actx, tuple( + # actx.call_loopy( + # prg(), diff_mat=get_mat(grp, ref_axes), vec=vec[grp.index] + # )[1]["result"] + # for grp in discr.groups)) # }}} diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 965fbc470..f032b1ce4 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -546,7 +546,7 @@ def pick_knl(n_to_nodes): ], name="resample_by_picking_inplace") - return knl + return t_unit @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_picking_knl_inplace_rhs")) @@ -554,7 +554,7 @@ def pick_knl_rhs(n_to_nodes, offset): from pymbolic import parse oset = parse(str(offset)) - knl = make_loopy_program( + t_unit = make_loopy_program( """{[iel, idof]: 0<=iel Date: Tue, 5 Oct 2021 01:27:06 -0500 Subject: [PATCH 072/154] remove commented code --- meshmode/discretization/connection/direct.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index f032b1ce4..15dbf74a7 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -491,15 +491,12 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el * ary[from_element_indices[iel], j])", [ lp.GlobalArg("result", fp_format, - #shape="nelements_result, n_to_nodes", shape=(nelements_result, n_to_nodes), offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("resample_mat", fp_format, - #shape="n_to_nodes, n_from_nodes", shape=(n_to_nodes, n_from_nodes), offset=lp.auto, tags=[IsOpArray()]), lp.GlobalArg("ary", fp_format, - #shape="nelements_vec, n_from_nodes", shape=(nelements_vec, n_from_nodes), offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("from_element_indices", index_dtype, From 4cdff69db6c3645ec7c03af60722617a19171bfc Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 11 Oct 2021 14:53:51 -0500 Subject: [PATCH 073/154] Event now in returned dictionary --- meshmode/discretization/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index a9d6352b1..2669f6320 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -629,7 +629,7 @@ def resample_mesh_nodes(grp, iaxis): prog, resampling_mat=actx.from_numpy(grp.from_mesh_interp_matrix()), nodes=nodes, - )[1]["result"] + )["result"] return result @@ -712,7 +712,7 @@ def get_mat(grp, gref_axes): fp_format = vec[grp.index].dtype output = actx.call_loopy(prg(nelements, nunit_dofs, fp_format), diff_mat=get_mat(grp, ref_axes), vec=vec[grp.index]) - results.append(output[1]["result"]) + results.append(output["result"]) return _DOFArray(actx, tuple(results)) From d2217826b63bc7d851de5001989a2c3d01c14687 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sat, 13 Nov 2021 02:21:52 -0600 Subject: [PATCH 074/154] Add kernel data tags --- meshmode/array_context.py | 2 +- meshmode/discretization/__init__.py | 52 ++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 4be119d4a..e13d3e61d 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -31,7 +31,7 @@ from arraycontext import ( # noqa: F401 ArrayContext, CommonSubexpressionTag, FirstAxisIsElementsTag, - ParameterValue, IsDOFArray, IsOpArray, + ParameterValue, IsDOFArray, IsOpArray, KernelDataTag, ArrayContainer, is_array_container, is_array_container_type, serialize_container, deserialize_container, diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index a87883b66..6c94f8f0b 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -29,7 +29,7 @@ from abc import ABCMeta, abstractproperty, abstractmethod from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array -from meshmode.array_context import IsDOFArray, ParameterValue, IsOpArray +from meshmode.array_context import IsDOFArray, ParameterValue, IsOpArray, KernelDataTag from loopy import GlobalArg, ValueArg, auto from arraycontext import ArrayContext, make_loopy_program @@ -591,10 +591,27 @@ def resample_mesh_nodes(grp, iaxis): and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): return nodes - return actx.einsum("ij,ej->ei", - actx.from_numpy(grp.from_mesh_interp_matrix()), + mat = actx.from_numpy(grp.from_mesh_interp_matrix()) + fp_format = nodes.dtype + Ne, Nj = nodes.shape + Ni, Nj = mat.shape + + kernel_data = [ + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto), # In default data layout apparently + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + ... + ] + + kd_tag = KernelDataTag(kernel_data) + + return actx.einsum("ij,ej->ei", + mat, nodes, - tagged=(FirstAxisIsElementsTag(),)) + tagged=(FirstAxisIsElementsTag(),kd_tag,)) result = make_obj_array([ _DOFArray(None, tuple([ @@ -650,6 +667,33 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) + data = [] + for grp in discr.groups: + + mat = get_mat(grp, ref_axes) + fp_format = vec[grp.index].dtype + Ne, Nj = vec[grp.index].shape + Ni, Nj = mat.shape + + kernel_data = [ + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, tags=[IsDOFArray()]), + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + ... + ] + + kd_tag = KernelDataTag(kernel_data) + + data.append(actx.einsum("ij,ej->ei", + mat, + vec[grp.index], + tagged=(FirstAxisIsElementsTag(),kd_tag,))) + + return _DOFArray(actx, tuple(data)) + return _DOFArray(actx, tuple( actx.einsum("ij,ej->ei", get_mat(grp, ref_axes), From 54f99662179ab179d8a1c369a622c6da3b3bfab5 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sat, 13 Nov 2021 15:27:39 -0600 Subject: [PATCH 075/154] Define resample_mat object --- meshmode/discretization/connection/direct.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 90cdf9d97..80a4c38e1 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -447,6 +447,7 @@ def batch_pick_knl(): point_pick_indices = self._resample_point_pick_indices( actx, i_tgrp, i_batch) + resample_mat = self._resample_matrix(actx, i_tgrp, i_batch) n_to_nodes, n_from_nodes = resample_mat.shape if point_pick_indices is None: From c379d426367821f821390caab3869df14a3ab240 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 16 Nov 2021 15:44:29 -0600 Subject: [PATCH 076/154] Specify parameters to vandermonde applicator --- meshmode/discretization/connection/modal.py | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index a56f0a0de..8a4ea4b63 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -27,8 +27,8 @@ import numpy.linalg as la import modepy as mp -from arraycontext import ( - NotAnArrayContainerError, serialize_container, deserialize_container) +from arraycontext import (KernelDataTag, IsDOFArray, IsOpArray, ParameterValue, + NotAnArrayContainerError, serialize_container, deserialize_container,) from meshmode.transform_metadata import FirstAxisIsElementsTag from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup @@ -162,10 +162,27 @@ def vandermonde_inverse(grp): vdm_inv = la.inv(vdm) return actx.from_numpy(vdm_inv) + fp_format = ary[grp.index].dtype + vi_mat = vandermonde_inverse(grp) + Ne, Nj = ary[grp.index].shape + Ni, Nj = vi_mat.shape + + import loopy as lp + kernel_data = [ + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, tags=[IsDOFArray()]), + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + ... + ] + kd_tag = KernelDataTag(kernel_data) + return actx.einsum("ij,ej->ei", - vandermonde_inverse(grp), + vi_mat, ary[grp.index], - tagged=(FirstAxisIsElementsTag(),)) + tagged=(FirstAxisIsElementsTag(),kd_tag,)) def __call__(self, ary): """Computes modal coefficients data from a functions From 4b192c273193505132196b68be17d1cee3d60203 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Wed, 1 Dec 2021 15:05:23 -0600 Subject: [PATCH 077/154] Disable sqrt for now, raises error --- meshmode/discretization/connection/direct.py | 3 ++- meshmode/dof_array.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 80a4c38e1..1cec4c55f 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -561,7 +561,8 @@ def pick_knl(n_to_nodes): "...", ], name="resample_by_picking_inplace") - + + return t_unit @memoize_in(actx, diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 54bf98d5b..18dd131a6 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -526,7 +526,7 @@ def _flatten(grp_ary): return actx.call_loopy( prg(), grp_ary=grp_ary - )[1]["result"] + )["result"] if len(ary) == 1: # can avoid a copy if reshape succeeds @@ -779,9 +779,10 @@ def _reduce_norm(actx, arys, ord): anp = actx.np # NOTE: these are ordered by an expected usage frequency - if ord == 2: - return anp.sqrt(sum(subary*subary for subary in arys)) - elif ord == np.inf: + #if ord == 2: + #return anp.sqrt(sum(subary*subary for subary in arys)) + # return sum(subary*subary for subary in arys) + if ord == np.inf: return reduce(anp.maximum, arys) elif ord == -np.inf: return reduce(anp.minimum, arys) From bbb3f6d8d467625c846b9219be6dbd101f7712d8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 12:32:02 -0600 Subject: [PATCH 078/154] Reorganize tags --- meshmode/array_context.py | 4 ++-- meshmode/transform_metadata.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index e13d3e61d..7c32f79b6 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -31,7 +31,7 @@ from arraycontext import ( # noqa: F401 ArrayContext, CommonSubexpressionTag, FirstAxisIsElementsTag, - ParameterValue, IsDOFArray, IsOpArray, KernelDataTag, + ParameterValue, IsDOFArray,# IsOpArray, KernelDataTag, ArrayContainer, is_array_container, is_array_container_type, serialize_container, deserialize_container, @@ -51,7 +51,7 @@ # point. #pytest_generate_tests_for_pyopencl_array_context ) - +from meshmode.transform_metadata import IsOpArray, KernelDataTag # {{{ Tags diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 981685c60..1e87fc52a 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -57,3 +57,19 @@ class ConcurrentDOFInameTag(Tag): computations for all DOFs within each element may be performed concurrently. """ + + +class IsOpArray(Tag): + """A tag that is applicable to arrays indicating the array is an + operator (as opposed, for instance, to a DOF array).""" + pass + + +class KernelDataTag(Tag): + """A tag that applies to :class:`loopy.LoopKernel`. Kernel data provided + with this tag can be later applied to the kernel. This is used, for + instance, to specify kernel data in einsum kernels.""" + + def __init__(self, kernel_data): + self.kernel_data = kernel_data + From 8b0c6b4480faaceefe4069de7bb373f5e88ef53e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:13:39 -0600 Subject: [PATCH 079/154] add egg=pytools --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea595889a..9f1ebca1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -git+https://github.com/inducer/pytools.git#egg +git+https://github.com/inducer/pytools.git#egg=pytools git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy From 6209f7d8f7a164ebd262185b04608450379be04f Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:16:05 -0600 Subject: [PATCH 080/154] remove whitespace --- meshmode/transform_metadata.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 1e87fc52a..491e741cb 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -72,4 +72,3 @@ class KernelDataTag(Tag): def __init__(self, kernel_data): self.kernel_data = kernel_data - From 7f30a608e4125a0803dd2328c90d869d8dbbd270 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:23:23 -0600 Subject: [PATCH 081/154] remove unneeded file --- test/test_modal.py.copy | 317 ---------------------------------------- 1 file changed, 317 deletions(-) delete mode 100644 test/test_modal.py.copy diff --git a/test/test_modal.py.copy b/test/test_modal.py.copy deleted file mode 100644 index aeb1a56ae..000000000 --- a/test/test_modal.py.copy +++ /dev/null @@ -1,317 +0,0 @@ -__copyright__ = "Copyright (C) 2021 University of Illinois Board of Trustees" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - - -from functools import partial -import numpy as np - -import meshmode # noqa: F401 -from meshmode.array_context import ( # noqa - pytest_generate_tests_for_pyopencl_array_context - as pytest_generate_tests - ) -from meshmode.dof_array import DOFArray -from meshmode.mesh import ( - SimplexElementGroup, - TensorProductElementGroup - ) - -from meshmode.discretization.poly_element import ( - # Simplex group factories - ModalSimplexGroupFactory, - InterpolatoryQuadratureSimplexGroupFactory, - PolynomialWarpAndBlendGroupFactory, - PolynomialRecursiveNodesGroupFactory, - PolynomialEquidistantSimplexGroupFactory, - # Tensor product group factories - ModalTensorProductGroupFactory, - LegendreGaussLobattoTensorProductGroupFactory, - # Quadrature-based (non-interpolatory) group factories - QuadratureSimplexGroupFactory - ) - -from meshmode.discretization import Discretization -from meshmode.discretization.connection.modal import ( - NodalToModalDiscretizationConnection, - ModalToNodalDiscretizationConnection - ) - -from meshmode.dof_array import thaw - -import meshmode.mesh.generation as mgen -import pytest - - -@pytest.mark.parametrize("nodal_group_factory", [ - InterpolatoryQuadratureSimplexGroupFactory, - PolynomialWarpAndBlendGroupFactory, - partial(PolynomialRecursiveNodesGroupFactory, family="lgl"), - PolynomialEquidistantSimplexGroupFactory, - LegendreGaussLobattoTensorProductGroupFactory, - ]) -def test_inverse_modal_connections(actx_factory, nodal_group_factory): - - if nodal_group_factory is LegendreGaussLobattoTensorProductGroupFactory: - group_cls = TensorProductElementGroup - modal_group_factory = ModalTensorProductGroupFactory - else: - group_cls = SimplexElementGroup - modal_group_factory = ModalSimplexGroupFactory - - actx = actx_factory() - order = 4 - - def f(x): - return 2*actx.np.sin(20*x) + 0.5*actx.np.cos(10*x) - - # Make a regular rectangle mesh - mesh = mgen.generate_regular_rect_mesh( - a=(0, 0)*2, b=(5, 3), n=(10, 6,), order=order, group_cls=group_cls) - - # Make discretizations - nodal_disc = Discretization(actx, mesh, nodal_group_factory(order)) - modal_disc = Discretization(actx, mesh, modal_group_factory(order)) - - # Make connections - nodal_to_modal_conn = NodalToModalDiscretizationConnection( - nodal_disc, modal_disc - ) - modal_to_nodal_conn = ModalToNodalDiscretizationConnection( - modal_disc, nodal_disc - ) - - x_nodal = thaw(actx, nodal_disc.nodes()[0]) - nodal_f = f(x_nodal) - - # Map nodal coefficients of f to modal coefficients - modal_f = nodal_to_modal_conn(nodal_f) - # Now map the modal coefficients back to nodal - nodal_f_2 = modal_to_nodal_conn(modal_f) - - # This error should be small since we composed a map with - # its inverse - err = actx.np.linalg.norm(nodal_f - nodal_f_2) - - assert err <= 1e-13 - - -@pytest.mark.parametrize("quad_group_factory", [ - QuadratureSimplexGroupFactory - ]) -def test_modal_coefficients_by_projection(actx_factory, quad_group_factory): - group_cls = SimplexElementGroup - modal_group_factory = ModalSimplexGroupFactory - actx = actx_factory() - order = 10 - m_order = 5 - - # Make a regular rectangle mesh - mesh = mgen.generate_regular_rect_mesh( - a=(0, 0)*2, b=(5, 3), n=(10, 6,), order=order, group_cls=group_cls) - - # Make discretizations - nodal_disc = Discretization(actx, mesh, quad_group_factory(order)) - modal_disc = Discretization(actx, mesh, modal_group_factory(m_order)) - - # Make connections one using quadrature projection - nodal_to_modal_conn_quad = NodalToModalDiscretizationConnection( - nodal_disc, modal_disc, allow_approximate_quad=True - ) - - def f(x): - return 2*actx.np.sin(5*x) - - x_nodal = thaw(actx, nodal_disc.nodes()[0]) - nodal_f = f(x_nodal) - - # Compute modal coefficients we expect to get - import modepy as mp - - grp, = nodal_disc.groups - shape = mp.Simplex(grp.dim) - space = mp.space_for_shape(shape, order=m_order) - basis = mp.orthonormal_basis_for_space(space, shape) - quad = grp._quadrature_rule() - - nodal_f_data = actx.to_numpy(nodal_f[0]) - vdm = mp.vandermonde(basis.functions, quad.nodes) - w_diag = np.diag(quad.weights) - - modal_data = [] - for _, nodal_data in enumerate(nodal_f_data): - # Compute modal data in each element: V.T * W * nodal_data - elem_modal_f = np.dot(vdm.T, np.dot(w_diag, nodal_data)) - modal_data.append(elem_modal_f) - - modal_data = actx.from_numpy(np.asarray(modal_data)) - modal_f_expected = DOFArray(actx, data=(modal_data,)) - - # Map nodal coefficients using the quadrature-based projection - modal_f_computed = nodal_to_modal_conn_quad(nodal_f) - - err = actx.np.linalg.norm(modal_f_expected - modal_f_computed) - - assert err <= 1e-13 - - -@pytest.mark.parametrize("quad_group_factory", [ - QuadratureSimplexGroupFactory - ]) -def test_quadrature_based_modal_connection_reverse(actx_factory, quad_group_factory): - - group_cls = SimplexElementGroup - modal_group_factory = ModalSimplexGroupFactory - actx = actx_factory() - order = 10 - m_order = 5 - - # Make a regular rectangle mesh - mesh = mgen.generate_regular_rect_mesh( - a=(0, 0)*2, b=(5, 3), n=(10, 6,), order=order, group_cls=group_cls) - - # Make discretizations - nodal_disc = Discretization(actx, mesh, quad_group_factory(order)) - modal_disc = Discretization(actx, mesh, modal_group_factory(m_order)) - - # Make connections one using quadrature projection - nodal_to_modal_conn_quad = NodalToModalDiscretizationConnection( - nodal_disc, modal_disc - ) - - # And the reverse connection - modal_to_nodal_conn = ModalToNodalDiscretizationConnection( - modal_disc, nodal_disc - ) - - def f(x): - return 1 + 2*x + 3*x**2 - - x_nodal = thaw(actx, nodal_disc.nodes()[0]) - nodal_f = f(x_nodal) - - # Map nodal coefficients using the quadrature-based projection - modal_f_quad = nodal_to_modal_conn_quad(nodal_f) - - # Back to nodal - nodal_f_computed = modal_to_nodal_conn(modal_f_quad) - - err = actx.np.linalg.norm(nodal_f - nodal_f_computed) - - assert err <= 1e-11 - - -@pytest.mark.parametrize("nodal_group_factory", [ - InterpolatoryQuadratureSimplexGroupFactory, - PolynomialWarpAndBlendGroupFactory, - LegendreGaussLobattoTensorProductGroupFactory, - ]) -@pytest.mark.parametrize(("dim", "mesh_pars"), [ - (2, [10, 20, 30]), - (3, [10, 20, 30]), - ]) -def test_modal_truncation(actx_factory, nodal_group_factory, - dim, mesh_pars): - - if nodal_group_factory is LegendreGaussLobattoTensorProductGroupFactory: - group_cls = TensorProductElementGroup - modal_group_factory = ModalTensorProductGroupFactory - else: - group_cls = SimplexElementGroup - modal_group_factory = ModalSimplexGroupFactory - - actx = actx_factory() - order = 5 - truncated_order = 3 - - from pytools.convergence import EOCRecorder - eoc_rec = EOCRecorder() - - def f(x): - return actx.np.sin(2*x) - - for mesh_par in mesh_pars: - - # Make the mesh - mesh = mgen.generate_warped_rect_mesh(dim, order=order, n=mesh_par, - group_cls=group_cls) - h = 1/mesh_par - - # Make discretizations - nodal_disc = Discretization(actx, mesh, nodal_group_factory(order)) - modal_disc = Discretization(actx, mesh, modal_group_factory(order)) - - # Make connections (nodal -> modal) - nodal_to_modal_conn = NodalToModalDiscretizationConnection( - nodal_disc, modal_disc - ) - - # And the reverse connection (modal -> nodal) - modal_to_nodal_conn = ModalToNodalDiscretizationConnection( - modal_disc, nodal_disc - ) - - x_nodal = thaw(actx, nodal_disc.nodes()[0]) - nodal_f = f(x_nodal) - - # Map to modal - modal_f = nodal_to_modal_conn(nodal_f) - - # Now we compute the basis function indices corresonding - # to modes > truncated_order - mgrp, = modal_disc.groups - mgrp_mode_ids = mgrp.basis_obj().mode_ids - truncation_matrix = np.identity(len(mgrp_mode_ids)) - for mode_idx, mode_id in enumerate(mgrp_mode_ids): - if sum(mode_id) > truncated_order: - truncation_matrix[mode_idx, mode_idx] = 0 - - # Zero out the modal coefficients corresponding to - # the targeted modes. - modal_f_data = actx.to_numpy(modal_f[0]) - num_elem, _ = modal_f_data.shape - for el_idx in range(num_elem): - modal_f_data[el_idx] = np.dot(truncation_matrix, - modal_f_data[el_idx]) - - modal_f_data = actx.from_numpy(modal_f_data) - modal_f_truncated = DOFArray(actx, data=(modal_f_data,)) - - # Now map truncated modal coefficients back to nodal - nodal_f_truncated = modal_to_nodal_conn(modal_f_truncated) - - err = actx.np.linalg.norm(nodal_f - nodal_f_truncated) - eoc_rec.add_data_point(h, err) - threshold_lower = 0.8*truncated_order - threshold_upper = 1.2*truncated_order - - assert threshold_upper >= eoc_rec.order_estimate() >= threshold_lower - - -if __name__ == "__main__": - import sys - if len(sys.argv) > 1: - exec(sys.argv[1]) - else: - from pytest import main - main([__file__]) - -# vim: fdm=marker From 3d53647011aee6d6f6ed3f83e4179dd90ec09f5c Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:25:20 -0600 Subject: [PATCH 082/154] Remove more redundant files --- doc/distributed.rst.copy | 4 - examples/moving-geometry.py.copy | 273 ------------------------------- 2 files changed, 277 deletions(-) delete mode 100644 doc/distributed.rst.copy delete mode 100644 examples/moving-geometry.py.copy diff --git a/doc/distributed.rst.copy b/doc/distributed.rst.copy deleted file mode 100644 index 657344c98..000000000 --- a/doc/distributed.rst.copy +++ /dev/null @@ -1,4 +0,0 @@ -Distributed memory -================== - -.. automodule:: meshmode.distributed diff --git a/examples/moving-geometry.py.copy b/examples/moving-geometry.py.copy deleted file mode 100644 index ed8260f91..000000000 --- a/examples/moving-geometry.py.copy +++ /dev/null @@ -1,273 +0,0 @@ -__copyright__ = "Copyright (C) 2021 Alexandru Fikl" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -import numpy as np -import pyopencl as cl - -from meshmode.dof_array import thaw -from meshmode.array_context import PyOpenCLArrayContext - -from pytools import memoize_in, keyed_memoize_in -from pytools.obj_array import make_obj_array - -import logging -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -def plot_solution(actx, vis, filename, discr, t, x): - names_and_fields = [] - - try: - from pytential import bind, sym - kappa = bind(discr, sym.mean_curvature(discr.ambient_dim))(actx) - names_and_fields.append(("kappa", kappa)) - except ImportError: - pass - - vis.write_vtk_file(filename, names_and_fields, overwrite=True) - - -def reconstruct_discr_from_nodes(actx, discr, x): - @memoize_in(actx, (reconstruct_discr_from_nodes, "resample_by_mat_prg")) - def resample_by_mat_prg(): - from meshmode.array_context import make_loopy_program - return make_loopy_program( - """ - {[iel, idof, j]: - 0 <= iel < nelements - and 0 <= idof < nmesh_nodes - and 0 <= j < ndiscr_nodes} - """, - """ - result[iel, idof] = sum(j, resampling_mat[idof, j] * nodes[iel, j]) - """, - name="resample_by_mat_prg") - - @keyed_memoize_in(actx, - (reconstruct_discr_from_nodes, "to_mesh_interp_matrix"), - lambda grp: grp.discretization_key()) - def to_mesh_interp_matrix(grp) -> np.ndarray: - import modepy as mp - mat = mp.resampling_matrix( - grp.basis_obj().functions, - grp.mesh_el_group.unit_nodes, - grp.unit_nodes) - - return actx.freeze(actx.from_numpy(mat)) - - def resample_nodes_to_mesh(grp, igrp, iaxis): - discr_nodes = x[iaxis][igrp] - - grp_unit_nodes = grp.unit_nodes.reshape(-1) - meg_unit_nodes = grp.mesh_el_group.unit_nodes.reshape(-1) - - tol = 10 * np.finfo(grp_unit_nodes.dtype).eps - if (grp_unit_nodes.shape == meg_unit_nodes.shape - and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): - return discr_nodes - - return actx.call_loopy( - resample_by_mat_prg(), - nodes=discr_nodes, - resampling_mat=to_mesh_interp_matrix(grp), - )["result"] - - megs = [] - for igrp, grp in enumerate(discr.groups): - nodes = np.stack([ - actx.to_numpy(resample_nodes_to_mesh(grp, igrp, iaxis)) - for iaxis in range(discr.ambient_dim) - ]) - - meg = grp.mesh_el_group.copy( - vertex_indices=None, - nodes=nodes, - ) - megs.append(meg) - - mesh = discr.mesh.copy(groups=megs, vertices=None) - return discr.copy(actx, mesh=mesh) - - -def advance(actx, dt, t, x, fn): - # NOTE: everybody's favorite three stage SSP RK3 method - k1 = x + dt * fn(t, x) - k2 = 3.0 / 4.0 * x + 1.0 / 4.0 * (k1 + dt * fn(t + dt, k1)) - return 1.0 / 3.0 * x + 2.0 / 3.0 * (k2 + dt * fn(t + 0.5 * dt, k2)) - - -def run(actx, *, - ambient_dim: int = 3, - resolution: int = None, - target_order: int = 4, - tmax: float = 1.0, - timestep: float = 1.0e-2, - group_factory_name: str = "warp_and_blend", - visualize: bool = True): - if ambient_dim not in (2, 3): - raise ValueError(f"unsupported dimension: {ambient_dim}") - - mesh_order = target_order - radius = 1.0 - - # {{{ geometry - - # {{{ element groups - - import modepy as mp - import meshmode.discretization.poly_element as poly - - # NOTE: picking the same unit nodes for the mesh and the discr saves - # a bit of work when reconstructing after a time step - - if group_factory_name == "warp_and_blend": - group_factory_cls = poly.PolynomialWarpAndBlendGroupFactory - - unit_nodes = mp.warp_and_blend_nodes(ambient_dim - 1, mesh_order) - elif group_factory_name == "quadrature": - group_factory_cls = poly.InterpolatoryQuadratureSimplexGroupFactory - - if ambient_dim == 2: - unit_nodes = mp.LegendreGaussQuadrature( - mesh_order, force_dim_axis=True).nodes - else: - unit_nodes = mp.VioreanuRokhlinSimplexQuadrature(mesh_order, 2).nodes - else: - raise ValueError(f"unknown group factory: '{group_factory_name}'") - - # }}} - - # {{{ discretization - - import meshmode.mesh.generation as gen - if ambient_dim == 2: - nelements = 8192 if resolution is None else resolution - mesh = gen.make_curve_mesh( - lambda t: radius * gen.ellipse(1.0, t), - np.linspace(0.0, 1.0, nelements + 1), - order=mesh_order, - unit_nodes=unit_nodes) - else: - nrounds = 4 if resolution is None else resolution - mesh = gen.generate_icosphere(radius, - uniform_refinement_rounds=nrounds, - order=mesh_order, - unit_nodes=unit_nodes) - - from meshmode.discretization import Discretization - discr0 = Discretization(actx, mesh, group_factory_cls(target_order)) - - logger.info("ndofs: %d", discr0.ndofs) - logger.info("nelements: %d", discr0.mesh.nelements) - - # }}} - - if visualize: - from meshmode.discretization.visualization import make_visualizer - vis = make_visualizer(actx, discr0, - vis_order=target_order, - # NOTE: setting this to True will add some unnecessary - # resampling in Discretization.nodes for the vis_discr underneath - force_equidistant=False) - - # }}} - - # {{{ ode - - def velocity_field(nodes, alpha=1.0): - return make_obj_array([ - alpha * nodes[0], -alpha * nodes[1], 0.0 * nodes[0] - ][:ambient_dim]) - - def source(t, x): - discr = reconstruct_discr_from_nodes(actx, discr0, x) - u = velocity_field(thaw(actx, discr.nodes())) - - # {{{ - - # NOTE: these are just here because this was at some point used to - # profile some more operators (turned out well!) - - from meshmode.discretization import num_reference_derivative - x = thaw(actx, discr.nodes()[0]) - gradx = sum( - num_reference_derivative(discr, (i,), x) - for i in range(discr.dim)) - intx = sum(actx.np.sum(xi * wi) for xi, wi in zip(x, discr.quad_weights())) - - assert gradx is not None - assert intx is not None - - # }}} - - return u - - # }}} - - # {{{ evolve - - maxiter = int(tmax // timestep) + 1 - dt = tmax / maxiter + 1.0e-15 - - x = thaw(actx, discr0.nodes()) - t = 0.0 - - if visualize: - filename = f"moving-geometry-{0:09d}.vtu" - plot_solution(actx, vis, filename, discr0, t, x) - - for n in range(1, maxiter + 1): - x = advance(actx, dt, t, x, source) - t += dt - - if visualize: - discr = reconstruct_discr_from_nodes(actx, discr0, x) - vis = make_visualizer(actx, discr, vis_order=target_order) - # vis = vis.copy_with_same_connectivity(actx, discr) - - filename = f"moving-geometry-{n:09d}.vtu" - plot_solution(actx, vis, filename, discr, t, x) - - logger.info("[%05d/%05d] t = %.5e/%.5e dt = %.5e", - n, maxiter, t, tmax, dt) - - # }}} - - -if __name__ == "__main__": - cl_ctx = cl.create_some_context() - queue = cl.CommandQueue(cl_ctx) - actx = PyOpenCLArrayContext(queue) - - from pytools import ProcessTimer - for _ in range(1): - with ProcessTimer() as p: - run(actx, - ambient_dim=3, - group_factory_name="warp_and_blend", - tmax=1.0, - timestep=1.0e-2, - visualize=False) - - logger.info("elapsed: %.3fs wall %.2fx cpu", - p.wall_elapsed, p.process_elapsed / p.wall_elapsed) From 1a7bdc22b7fe9a6f5f8bc8ebc0087d70518f92bd Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:37:06 -0600 Subject: [PATCH 083/154] Remove unneeded IsDOFArray import --- examples/simple-dg.py | 1 - meshmode/transform_metadata.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/simple-dg.py b/examples/simple-dg.py index 18480252d..556db0794 100644 --- a/examples/simple-dg.py +++ b/examples/simple-dg.py @@ -32,7 +32,6 @@ from meshmode.mesh import BTAG_ALL, BTAG_NONE # noqa from loopy import GlobalArg, auto -from meshmode.array_context import IsDOFArray from meshmode.dof_array import DOFArray, flat_norm from meshmode.array_context import (PyOpenCLArrayContext, PytatoPyOpenCLArrayContext) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 491e741cb..a6e038217 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -29,7 +29,7 @@ """ from pytools.tag import Tag - +from arraycontext import IsDOFArray, ParameterValue class FirstAxisIsElementsTag(Tag): """A tag that is applicable to array outputs indicating that the first From ee83e3c626d56643595ad008f6c94bb17113b0b4 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:38:26 -0600 Subject: [PATCH 084/154] Remove more unused imports --- examples/simple-dg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple-dg.py b/examples/simple-dg.py index 556db0794..8a41712c9 100644 --- a/examples/simple-dg.py +++ b/examples/simple-dg.py @@ -31,7 +31,6 @@ from pytools.obj_array import flat_obj_array, make_obj_array from meshmode.mesh import BTAG_ALL, BTAG_NONE # noqa -from loopy import GlobalArg, auto from meshmode.dof_array import DOFArray, flat_norm from meshmode.array_context import (PyOpenCLArrayContext, PytatoPyOpenCLArrayContext) @@ -296,6 +295,7 @@ def face_mass(self, vec): actx = vec.array_context dtype = vec.entry_dtype + all_faces_conn = self.get_connection("vol", "all_faces") all_faces_discr = all_faces_conn.to_discr vol_discr = all_faces_conn.from_discr From 755f06a0ed5818d2d7647a40c68042e8f2700d12 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 13:47:55 -0600 Subject: [PATCH 085/154] remove redundant import --- meshmode/discretization/connection/projection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/discretization/connection/projection.py b/meshmode/discretization/connection/projection.py index 7f0a03816..4efa8e959 100644 --- a/meshmode/discretization/connection/projection.py +++ b/meshmode/discretization/connection/projection.py @@ -27,7 +27,6 @@ import loopy as lp from meshmode.array_context import IsDOFArray -from meshmode.dof_array import DOFArray # Is this still needed? from arraycontext import ( NotAnArrayContainerError, make_loopy_program, serialize_container, deserialize_container) From ed3db7c8c05d855b86b8a042b4be608e79816dfb Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 11 Jan 2022 14:07:03 -0600 Subject: [PATCH 086/154] flake8 fixes --- meshmode/discretization/connection/modal.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index 8a4ea4b63..8806dbd78 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -27,8 +27,10 @@ import numpy.linalg as la import modepy as mp -from arraycontext import (KernelDataTag, IsDOFArray, IsOpArray, ParameterValue, - NotAnArrayContainerError, serialize_container, deserialize_container,) +from meshmode.array_context import (KernelDataTag, IsDOFArray, + IsOpArray, ParameterValue) +from arraycontext import (NotAnArrayContainerError, + serialize_container, deserialize_container,) from meshmode.transform_metadata import FirstAxisIsElementsTag from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup @@ -169,9 +171,12 @@ def vandermonde_inverse(grp): import loopy as lp kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, + tags=[IsDOFArray()]), + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + tags=[IsDOFArray()], is_output=True), lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), @@ -182,7 +187,7 @@ def vandermonde_inverse(grp): return actx.einsum("ij,ej->ei", vi_mat, ary[grp.index], - tagged=(FirstAxisIsElementsTag(),kd_tag,)) + tagged=(FirstAxisIsElementsTag(), kd_tag,)) def __call__(self, ary): """Computes modal coefficients data from a functions From e173643a126b5aa615c88922f3e1c53f0b6c58e1 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 16 Jan 2022 00:57:05 -0600 Subject: [PATCH 087/154] Move more tags from arraycontext --- meshmode/array_context.py | 4 ++-- meshmode/transform_metadata.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index aca89a9db..9c2192e83 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -31,7 +31,7 @@ from arraycontext import ( # noqa: F401 ArrayContext, CommonSubexpressionTag, FirstAxisIsElementsTag, - ParameterValue, IsDOFArray,# IsOpArray, KernelDataTag, + #ParameterValue, IsDOFArray, ArrayContainer, is_array_container, is_array_container_type, serialize_container, deserialize_container, @@ -51,7 +51,7 @@ # point. #pytest_generate_tests_for_pyopencl_array_context ) -from meshmode.transform_metadata import IsOpArray, KernelDataTag +from meshmode.transform_metadata import IsOpArray, KernelDataTag, ParameterValue, IsDOFArray # {{{ Tags diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index a6e038217..113f2b237 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -28,8 +28,8 @@ THE SOFTWARE. """ -from pytools.tag import Tag -from arraycontext import IsDOFArray, ParameterValue +from pytools.tag import Tag, UniqueTag +#from arraycontext import IsDOFArray, ParameterValue class FirstAxisIsElementsTag(Tag): """A tag that is applicable to array outputs indicating that the first @@ -59,6 +59,28 @@ class ConcurrentDOFInameTag(Tag): """ +class ParameterValue(UniqueTag): + """A tag that applies to :class:`loopy.ValueArg`. Instances of this tag + are initialized with the value of the parameter and this value may be + later retrieved + for use with `loopy.fix_parameter` in `transform_loopy_program`. This allows + the fixing of parameters whose values cannot be set during the creation of the + program/kernel. This is useful for fixing the loop parameters of einsum kernels + for instance. + """ + + def __init__(self, value): + self.value = value + + +class IsDOFArray(Tag): + """A tag that is applicable to :class:`loopy.ArrayArg` indicating the content of the + array comprises element DOFs. + """ + pass + + + class IsOpArray(Tag): """A tag that is applicable to arrays indicating the array is an operator (as opposed, for instance, to a DOF array).""" From 0f3e4826a1ffec4803e2f7382971009512237ac8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 11:40:45 -0600 Subject: [PATCH 088/154] remove white space --- meshmode/mesh/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py index eb3560537..ca80edf83 100644 --- a/meshmode/mesh/__init__.py +++ b/meshmode/mesh/__init__.py @@ -215,7 +215,7 @@ def __init__(self, order, vertex_indices, nodes, Do not supply *element_nr_base* and *node_nr_base*, they will be automatically assigned. """ - + super().__init__( order=order, vertex_indices=vertex_indices, From b6853f1f05bf0612f61f6412da25bc6f897ce5d7 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 11:44:13 -0600 Subject: [PATCH 089/154] flake8 errors in transform_metadata.py --- meshmode/transform_metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 113f2b237..a4f08ef92 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -29,7 +29,7 @@ """ from pytools.tag import Tag, UniqueTag -#from arraycontext import IsDOFArray, ParameterValue + class FirstAxisIsElementsTag(Tag): """A tag that is applicable to array outputs indicating that the first @@ -80,7 +80,6 @@ class IsDOFArray(Tag): pass - class IsOpArray(Tag): """A tag that is applicable to arrays indicating the array is an operator (as opposed, for instance, to a DOF array).""" From d7ad0af16ab89ea670bad233ff094996959ac8ef Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 11:51:46 -0600 Subject: [PATCH 090/154] Simplify projection.py imports --- meshmode/discretization/connection/projection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/meshmode/discretization/connection/projection.py b/meshmode/discretization/connection/projection.py index 4efa8e959..d027f42cd 100644 --- a/meshmode/discretization/connection/projection.py +++ b/meshmode/discretization/connection/projection.py @@ -26,11 +26,10 @@ import loopy as lp -from meshmode.array_context import IsDOFArray from arraycontext import ( NotAnArrayContainerError, make_loopy_program, serialize_container, deserialize_container) -from meshmode.transform_metadata import FirstAxisIsElementsTag +from meshmode.transform_metadata import FirstAxisIsElementsTag, IsDOFArray from meshmode.discretization.connection.direct import ( DiscretizationConnection, DirectDiscretizationConnection) @@ -168,7 +167,6 @@ def kproj(): * basis_tabulation[ibasis, i_quad] \ * weights[i_quad]) {dep=barrier} """, - # Are any more of these DOF arrays? [ lp.GlobalArg("ary", None, shape=("n_from_elements", "n_from_nodes")), From 0ceff6131c8ba15aefc4f4369d93fd3c340c0452 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 11:56:33 -0600 Subject: [PATCH 091/154] simplify modal.py imports --- meshmode/discretization/connection/modal.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index 8806dbd78..052c937ce 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -27,11 +27,10 @@ import numpy.linalg as la import modepy as mp -from meshmode.array_context import (KernelDataTag, IsDOFArray, - IsOpArray, ParameterValue) from arraycontext import (NotAnArrayContainerError, serialize_container, deserialize_container,) -from meshmode.transform_metadata import FirstAxisIsElementsTag +from meshmode.transform_metadata import (FirstAxisIsElementsTag, + KernelDataTag, IsDOFArray, IsOpArray, ParameterValue) from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup from meshmode.discretization.connection.direct import DiscretizationConnection From 40b6f4de2fc7c71feed70f7d5d991e3582698bd6 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 12:10:05 -0600 Subject: [PATCH 092/154] dof_array.py flake8 fixes --- meshmode/dof_array.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 18dd131a6..9bc936587 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -22,17 +22,13 @@ import threading import operator as op -from loopy import GlobalArg, ValueArg, auto +from loopy import GlobalArg, auto from warnings import warn from numbers import Number from contextlib import contextmanager from functools import partial, update_wrapper -from typing import Any, Callable, Iterable, Optional, Tuple, Union +from typing import Any, Callable, Iterable, Optional, Tuple -# Is this one still needed? -from pytools.obj_array import obj_array_vectorize, obj_array_vectorize_n_args - -from meshmode.array_context import IsDOFArray, ParameterValue import numpy as np import loopy as lp @@ -40,7 +36,8 @@ from pytools import single_valued, memoize_in from meshmode.transform_metadata import ( - ConcurrentElementInameTag, ConcurrentDOFInameTag) + ConcurrentElementInameTag, ConcurrentDOFInameTag, + IsDOFArray) from arraycontext import ( ArrayContext, NotAnArrayContainerError, make_loopy_program, with_container_arithmetic, @@ -77,6 +74,7 @@ class MyFrameSummary: lineno: int func_name: str + @with_container_arithmetic( bcast_obj_array=True, bcast_numpy_array=True, @@ -208,8 +206,6 @@ def __del__(self): f"({sum(DOFArray.alloc_frames.values())})") """ - - # Tell numpy that we would like to do our own array math, thank you very much. # (numpy arrays have priority 0.) __array_priority__ = 10 @@ -503,9 +499,9 @@ def prg(): """, [ lp.GlobalArg("result", None, - shape="nelements * ndofs_per_element"), + shape="nelements * ndofs_per_element"), lp.GlobalArg("grp_ary", None, - shape=("nelements", "ndofs_per_element"), tags=[IsDOFArray()]), + shape=("nelements", "ndofs_per_element"), tags=[IsDOFArray()]), lp.ValueArg("nelements", np.int32), lp.ValueArg("ndofs_per_element", np.int32), "..." From 5024a47839ba1b14e083949ba5a25ffa1014b994 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 12:19:14 -0600 Subject: [PATCH 093/154] Remove indexing into returned value --- meshmode/dof_array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 9bc936587..bdb9b07a2 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -591,7 +591,7 @@ def prg(): grp_start=grp_start, ary=ary, nelements=nel, ndofs_per_element=ndof, - )[1]["result"] + )["result"] for grp_start, (nel, ndof) in zip(group_starts, group_shapes))) From 195c2ba398ec01b2804dcb97b75c7499cf671715 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 13:51:49 -0600 Subject: [PATCH 094/154] Uncomment sqrt call --- meshmode/dof_array.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index bdb9b07a2..74f399a7f 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -775,10 +775,10 @@ def _reduce_norm(actx, arys, ord): anp = actx.np # NOTE: these are ordered by an expected usage frequency - #if ord == 2: - #return anp.sqrt(sum(subary*subary for subary in arys)) - # return sum(subary*subary for subary in arys) - if ord == np.inf: + if ord == 2: + # Check with force_device_scalars + return anp.sqrt(sum(subary*subary for subary in arys)) + elif ord == np.inf: return reduce(anp.maximum, arys) elif ord == -np.inf: return reduce(anp.minimum, arys) From ad5479e8eb6c4bad0bd645793a8c8234266fb2bc Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 13:57:30 -0600 Subject: [PATCH 095/154] Remove allocation tracking code --- meshmode/dof_array.py | 66 ------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index 74f399a7f..f6e2b5da4 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -65,15 +65,6 @@ # {{{ DOFArray -from dataclasses import dataclass - - -@dataclass(frozen=True, repr=True) -class MyFrameSummary: - filename: str - lineno: int - func_name: str - @with_container_arithmetic( bcast_obj_array=True, @@ -138,11 +129,6 @@ class DOFArray: the array context given to :func:`array_context_for_pickling`. """ - total_alloc = 0 - alloc_frames = {} - cum_alloc = 0 - n_cum_alloc = 0 - def __init__(self, actx: Optional[ArrayContext], data: Tuple[Any]): if __debug__: if not (actx is None or isinstance(actx, ArrayContext)): @@ -151,61 +137,9 @@ def __init__(self, actx: Optional[ArrayContext], data: Tuple[Any]): if not isinstance(data, tuple): raise TypeError("'data' argument must be a tuple") - alloc_size = sum(d.size for d in data) - self._array_context = actx self._data = data - DOFArray.total_alloc += alloc_size - DOFArray.cum_alloc += alloc_size - from traceback import extract_stack - s = tuple(MyFrameSummary(filename=fs.filename, lineno=fs.lineno, - func_name=fs.name) - for fs in extract_stack()) - DOFArray.alloc_frames[s] = DOFArray.alloc_frames.get(s, 0) + 1 - self._alloc_frame = s - nallocs = sum(DOFArray.alloc_frames.values()) - DOFArray.n_cum_alloc += 1 - - #if True: - # print(f"ALLOC {alloc_size} -> {DOFArray.total_alloc} ({nallocs})") - - #if nallocs >= 62: - # self.print_allocation_summary() - # 1/0 - - @classmethod - def print_allocation_summary(cls): - nallocs = sum(DOFArray.alloc_frames.values()) - print(f"{nallocs} allocations total") - for stack, count in cls.alloc_frames.items(): - if count: - print(78*"-") - print(f"{count} allocations for stack:") - for f in stack: - print(f) - - """ - def __del__(self): - - active_bytes = self.array_context.allocator.active_bytes/1e9 - print(f"ACTIVE BYTES BEFORE DELETE: {active_bytes}") - #for entry in self._data: - # pass - #entry.base_data.release() - #active_bytes = self.array_context.allocator.active_bytes/1e9 - #print(f"ACTIVE BYTES AFTER DELETE: {active_bytes}") - - - alloc_size = sum(d.size for d in self._data) - DOFArray.total_alloc -= alloc_size - s = self._alloc_frame - DOFArray.alloc_frames[s] = DOFArray.alloc_frames.get(s, 0) - 1 - if True: - print(f"FREE {alloc_size} -> {DOFArray.total_alloc} " - f"({sum(DOFArray.alloc_frames.values())})") - """ - # Tell numpy that we would like to do our own array math, thank you very much. # (numpy arrays have priority 0.) __array_priority__ = 10 From 50ed17fb73a0f6f9f64fd9395b7472a07f5d4a5a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 14:00:30 -0600 Subject: [PATCH 096/154] Simplify imports --- meshmode/dof_array.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index f6e2b5da4..d4b504892 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -22,7 +22,6 @@ import threading import operator as op -from loopy import GlobalArg, auto from warnings import warn from numbers import Number from contextlib import contextmanager @@ -511,7 +510,7 @@ def prg(): "{[iel,idof]: 0<=iel Date: Tue, 18 Jan 2022 14:07:06 -0600 Subject: [PATCH 097/154] Simpify discretization __init__.py imports --- meshmode/discretization/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 6c94f8f0b..e6e3c8f78 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -29,13 +29,12 @@ from abc import ABCMeta, abstractproperty, abstractmethod from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array -from meshmode.array_context import IsDOFArray, ParameterValue, IsOpArray, KernelDataTag -from loopy import GlobalArg, ValueArg, auto from arraycontext import ArrayContext, make_loopy_program import loopy as lp from meshmode.transform_metadata import ( - ConcurrentElementInameTag, ConcurrentDOFInameTag, FirstAxisIsElementsTag) + ConcurrentElementInameTag, ConcurrentDOFInameTag, FirstAxisIsElementsTag, + IsDOFArray, ParameterValue, IsOpArray, KernelDataTag) from warnings import warn @@ -540,7 +539,7 @@ def prg(): "{[iel,idof]: 0<=iel Date: Tue, 18 Jan 2022 14:08:35 -0600 Subject: [PATCH 098/154] Remove now unnecessary indexing --- meshmode/discretization/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index e6e3c8f78..5d7fbe2b5 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -553,7 +553,7 @@ def prg(): prg(), weights=actx.from_numpy(grp.quadrature_rule().weights), nelements=grp.nelements, - )[1]["result"]) + )["result"]) for grp in self.groups)) def nodes(self, cached=True): From 12415b3753274ff79ea3bde31a24acb5c780859c Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 14:15:18 -0600 Subject: [PATCH 099/154] flake8 fixes --- meshmode/discretization/__init__.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 5d7fbe2b5..912ce126a 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -596,9 +596,12 @@ def resample_mesh_nodes(grp, iaxis): Ni, Nj = mat.shape kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto), # In default data layout apparently - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), + offset=lp.auto), # In default data layout apparently + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + tags=[IsDOFArray()], is_output=True), lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), @@ -607,10 +610,10 @@ def resample_mesh_nodes(grp, iaxis): kd_tag = KernelDataTag(kernel_data) - return actx.einsum("ij,ej->ei", + return actx.einsum("ij,ej->ei", mat, nodes, - tagged=(FirstAxisIsElementsTag(),kd_tag,)) + tagged=(FirstAxisIsElementsTag(), kd_tag,)) result = make_obj_array([ _DOFArray(None, tuple([ @@ -675,9 +678,12 @@ def get_mat(grp, gref_axes): Ni, Nj = mat.shape kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), + lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, + tags=[IsDOFArray()]), + lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + tags=[IsOpArray()]), + lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + tags=[IsDOFArray()], is_output=True), lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), @@ -689,7 +695,7 @@ def get_mat(grp, gref_axes): data.append(actx.einsum("ij,ej->ei", mat, vec[grp.index], - tagged=(FirstAxisIsElementsTag(),kd_tag,))) + tagged=(FirstAxisIsElementsTag(), kd_tag,))) return _DOFArray(actx, tuple(data)) From 940c8b8e1de88092be70da3735d14d79d6449dd8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 18 Jan 2022 14:22:46 -0600 Subject: [PATCH 100/154] Simplify direct.py imports --- meshmode/discretization/connection/direct.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index c9e9ed11b..666eed132 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -26,10 +26,9 @@ import loopy as lp from meshmode.transform_metadata import ( - ConcurrentElementInameTag, ConcurrentDOFInameTag) + ConcurrentElementInameTag, ConcurrentDOFInameTag, + IsOpArray, IsDOFArray, ParameterValue) from pytools import memoize_in, keyed_memoize_method -from pytools.obj_array import obj_array_vectorized_n_args -from meshmode.array_context import IsOpArray, IsDOFArray, ParameterValue from arraycontext import ( ArrayContext, NotAnArrayContainerError, serialize_container, deserialize_container, make_loopy_program) From 7a78af9075d00dcd99ae35de49a5259f8aa7488b Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 16:13:18 -0600 Subject: [PATCH 101/154] Delete commented code --- meshmode/discretization/connection/direct.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 666eed132..cecfdf52d 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -450,16 +450,6 @@ def batch_pick_knl(): n_to_nodes, n_from_nodes = resample_mat.shape if point_pick_indices is None: - #resample_mat = self._resample_matrix(actx, i_tgrp, i_batch) - #batch_result = actx.call_loopy( - # batch_mat_knl(n_to_nodes, n_from_nodes), - # resample_mat=resample_mat, - # ary=ary[batch.from_group_index], - # from_element_indices=batch._global_from_element_indices( - # actx, self.to_discr.groups[i_tgrp]), - # n_to_nodes=self.to_discr.groups[i_tgrp].nunit_dofs - #)["result"] - from_element = actx.thaw(batch._global_from_element_indices( actx, self.to_discr.groups[i_tgrp])) From 8ed6673c2d926aadc911391a2e3b4ffc14e28002 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 16:15:44 -0600 Subject: [PATCH 102/154] Remove unused permutation code --- meshmode/discretization/connection/direct.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index cecfdf52d..2a90e1fc9 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -151,24 +151,6 @@ def __init__(self, batches): # }}} -# Creates the inverse mapping of an indirection array -def Pinv(p): - p2 = np.empty_like(p) - p2[p] = np.arange(0,len(p), dtype=np.int32) - return p2 - -# Combine two indirection arrays p_l and p_r into a new -# indirection array p_c so that -# if B[p_l[i]] = A[p_r[i]] then B[i] = A[p_c[i]] -# or, alternatively, B[p_c[i]] = A[i] if lhs=True. -# Alternatively, flipping the order of the input arguments -# will do the same thing. -def combine_indirection_arrays(p_l, p_r, lhs=False): - if lhs: - return p_l[Pinv(p_r)] - else: - return p_r[Pinv(p_l)] - # {{{ connection classes From 572f63936df1f36c985a82131124ffb11b2b16ec Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 16:30:35 -0600 Subject: [PATCH 103/154] Remove more commented code --- meshmode/discretization/connection/direct.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 2a90e1fc9..05e18831f 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -534,9 +534,8 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el shape=(n_to_el_ind,), offset=lp.auto), lp.ValueArg("n_to_nodes", tags=[ParameterValue(n_to_nodes)]), - # Specifying this breaks order 4 for some reason lp.ValueArg("n_from_nodes", tags=[ParameterValue(n_from_nodes)]), - lp.ValueArg("nelements", np.int32, tags=[ParameterValue(n_from_nodes)]), # I'm guessing + lp.ValueArg("nelements", np.int32, tags=[ParameterValue(n_from_nodes)]), lp.ValueArg("nelements_result", np.int32, tags=[ParameterValue(nelements_result)]), lp.ValueArg("nelements_vec", np.int32, tags=[ParameterValue(nelements_vec)]), "...", @@ -629,13 +628,6 @@ def pick_knl_rhs(n_to_nodes, offset): index_dtype = batch.from_element_indices.dtype fp_format = resample_mat.dtype - #print(resample_mat.shape) - #print(result[i_tgrp].shape) - #print(ary[batch.from_group_index].shape) - #print(batch.from_element_indices.shape) - #print(batch.to_element_indices.shape) - #exit() - actx.call_loopy(mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el_ind, n_to_el_ind, fp_format, index_dtype), resample_mat=resample_mat, From 3e7419f3655db004ae3f074bc50b503478fe0671 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 18:31:09 -0600 Subject: [PATCH 104/154] Remove more unused code --- meshmode/discretization/connection/direct.py | 48 +++----------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 05e18831f..93d8607e4 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -638,48 +638,12 @@ def pick_knl_rhs(n_to_nodes, offset): else: nelements, n_to_nodes = result[i_tgrp].shape - - # Cases: - # lhs is some constant offset into result array -> just use offset - # lhs is some zero based indexing - - to_element_indices = batch.to_element_indices.get(queue=actx.queue) - from_element_indices = batch.from_element_indices.get(queue=actx.queue) - #print(to_element_indices) - #print(from_element_indices) - #print(to_element_indices.shape) - #print(from_element_indices.shape) - #offset = to_element_indices[0] - #compare_to = np.arange(offset, len(to_element_indices) + offset, dtype=np.int64) - indirection = "both" - #compare = "rhs" if np.allclose(compare_to, to_element_indices) else "both" - #if compare == "both": - # print(np.array(sorted(to_element_indices))) - # print(np.array(sorted(from_element_indices))) - # exit() - - - if True:#indirection == "both": - actx.call_loopy(pick_knl(n_to_nodes), - pick_list=point_pick_indices, - result=result[i_tgrp], - ary=ary[batch.from_group_index], - from_element_indices=batch.from_element_indices, - to_element_indices=batch.to_element_indices) - elif indirection == "rhs": - # This also doesn't work due to the offset on the receiving side - #from pyopencl.array import to_device - #indirection_array = to_device(actx.queue, indirection_array) - #print(indirection_array.get()) - - actx.call_loopy(pick_knl_rhs(n_to_nodes, offset), - pick_list=point_pick_indices, - result=result[i_tgrp], - ary=ary[batch.from_group_index], - from_element_indices=from_element_indices) - - else: - raise ValueError("value must be 'both', 'rhs'") + actx.call_loopy(pick_knl(n_to_nodes), + pick_list=point_pick_indices, + result=result[i_tgrp], + ary=ary[batch.from_group_index], + from_element_indices=batch.from_element_indices, + to_element_indices=batch.to_element_indices) return result From 04735e5867186c1b8c552584af5ac354a580d938 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 18:52:44 -0600 Subject: [PATCH 105/154] Remove unused pick_knl_rhs --- meshmode/discretization/connection/direct.py | 32 +------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 93d8607e4..cbc03ce3a 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -570,41 +570,11 @@ def pick_knl(n_to_nodes): ], name="resample_by_picking_inplace") - - return t_unit - - @memoize_in(actx, - (DirectDiscretizationConnection, "resample_by_picking_knl_inplace_rhs")) - def pick_knl_rhs(n_to_nodes, offset): - from pymbolic import parse - oset = parse(str(offset)) - - t_unit = make_loopy_program( - """{[iel, idof]: - 0<=iel Date: Mon, 24 Jan 2022 20:03:12 -0600 Subject: [PATCH 106/154] remove untested changes to _apply_without_inplace_updates --- meshmode/discretization/connection/direct.py | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index cbc03ce3a..004435f56 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -362,7 +362,7 @@ def _apply_without_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl")) - def batch_mat_knl(n_to_nodes, n_from_nodes): + def batch_mat_knl(): t_unit = make_loopy_program( [ "{[iel]: 0 <= iel < nelements}", @@ -428,8 +428,6 @@ def batch_pick_knl(): point_pick_indices = self._resample_point_pick_indices( actx, i_tgrp, i_batch) - resample_mat = self._resample_matrix(actx, i_tgrp, i_batch) - n_to_nodes, n_from_nodes = resample_mat.shape if point_pick_indices is None: from_element = actx.thaw(batch._global_from_element_indices( @@ -442,14 +440,13 @@ def batch_pick_knl(): actx.np.not_equal(from_element .reshape(-1, 1), -1), - #This will probably need kernel data tags actx.einsum("ij,ej->ei", mat, grp_ary[from_element]), 0) else: batch_result = actx.call_loopy( - batch_mat_knl(n_to_nodes, n_from_nodes), + batch_mat_knl(), resample_mat=mat, ary=grp_ary, from_element_indices=from_element, @@ -509,6 +506,7 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el_ind, n_to_el_ind, fp_format, index_dtype): + # Need to find a test code that uses this. t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 24 Jan 2022 20:22:47 -0600 Subject: [PATCH 107/154] Remove comment --- meshmode/discretization/connection/direct.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 004435f56..eae924483 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -506,7 +506,6 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el_ind, n_to_el_ind, fp_format, index_dtype): - # Need to find a test code that uses this. t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 24 Jan 2022 20:28:46 -0600 Subject: [PATCH 108/154] flake8 fixes --- meshmode/discretization/connection/direct.py | 38 ++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index eae924483..06f506510 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -505,7 +505,8 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) - def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, n_from_el_ind, n_to_el_ind, fp_format, index_dtype): + def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, + n_from_el_ind, n_to_el_ind, fp_format, index_dtype): t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 24 Jan 2022 20:43:52 -0600 Subject: [PATCH 109/154] Use upstream arraycontext.py --- meshmode/array_context.py | 124 -------------------------------------- 1 file changed, 124 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 9c2192e83..12b6e3f48 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -25,74 +25,6 @@ THE SOFTWARE. """ -from functools import partial -from pytools.tag import Tag, UniqueTag - -from arraycontext import ( # noqa: F401 - ArrayContext, - CommonSubexpressionTag, FirstAxisIsElementsTag, - #ParameterValue, IsDOFArray, - ArrayContainer, - is_array_container, is_array_container_type, - serialize_container, deserialize_container, - get_container_context, get_container_context_recursively, - with_container_arithmetic, - dataclass_array_container, - map_array_container, multimap_array_container, - rec_map_array_container, rec_multimap_array_container, - mapped_over_array_containers, - multimapped_over_array_containers, - thaw as _thaw, freeze, - - PyOpenCLArrayContext, - - make_loopy_program, - # Use the version defined in this file for now. This will need to be moved to arraycontext at some - # point. - #pytest_generate_tests_for_pyopencl_array_context - ) -from meshmode.transform_metadata import IsOpArray, KernelDataTag, ParameterValue, IsDOFArray -# {{{ Tags - - -# {{{ pytest integration - - -def _pytest_generate_tests_for_pyopencl_array_context(array_context_type, metafunc): - import pyopencl as cl - from pyopencl.tools import _ContextFactory - - class ArrayContextFactory(_ContextFactory): - def __call__(self): - ctx = super().__call__() - return array_context_type(cl.CommandQueue(ctx)) - - def __str__(self): - return ("" % - (self.device.name.strip(), - self.device.platform.name.strip())) - - import pyopencl.tools as cl_tools - arg_names = cl_tools.get_pyopencl_fixture_arg_names( - metafunc, extra_arg_names=["actx_factory"]) - - if not arg_names: - return - - arg_values, ids = cl_tools.get_pyopencl_fixture_arg_values() - if "actx_factory" in arg_names: - if "ctx_factory" in arg_names or "ctx_getter" in arg_names: - raise RuntimeError("Cannot use both an 'actx_factory' and a " - "'ctx_factory' / 'ctx_getter' as arguments.") - - for arg_dict in arg_values: - arg_dict["actx_factory"] = ArrayContextFactory(arg_dict["device"]) - - arg_values = [ - tuple(arg_dict[name] for name in arg_names) - for arg_dict in arg_values - ] - import sys from warnings import warn from arraycontext import PyOpenCLArrayContext as PyOpenCLArrayContextBase @@ -102,62 +34,6 @@ def __str__(self): _PytestPytatoPyOpenCLArrayContextFactory, register_pytest_array_context_factory) -def generate_pytest_generate_tests(array_context_type): - """Generate a function to parametrize tests for pytest to use - a :mod:`pyopencl` array context of the specified subtype. - - The returned function performs device enumeration analogously to - :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. - - Using the line: - - .. code-block:: python - - from meshmode.array_context import generate_pytest_generate_tests - pytest_generate_tests = - generate_pytest_generate_tests() - - - in your pytest test scripts allows you to use the arguments ctx_factory, - device, or platform in your test functions, and they will automatically be - run for each OpenCL device/platform in the system, as appropriate. - - It also allows you to specify the ``PYOPENCL_TEST`` environment variable - for device selection. - """ - - from functools import partial - - return lambda metafunc: partial( - _pytest_generate_tests_for_pyopencl_array_context, - array_context_type)(metafunc) - - -def pytest_generate_tests_for_pyopencl_array_context(metafunc): - """Parametrize tests for pytest to use a :mod:`pyopencl` array context. - - Performs device enumeration analogously to - :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. - - Using the line: - - .. code-block:: python - - from meshmode.array_context import pytest_generate_tests_for_pyopencl \ - as pytest_generate_tests - - in your pytest test scripts allows you to use the arguments ctx_factory, - device, or platform in your test functions, and they will automatically be - run for each OpenCL device/platform in the system, as appropriate. - - It also allows you to specify the ``PYOPENCL_TEST`` environment variable - for device selection. - """ - - generate_pytest_generate_tests(PyOpenCLArrayContext)(metafunc) - - -# }}} def thaw(actx, ary): warn("meshmode.array_context.thaw is deprecated. Use arraycontext.thaw instead. " From f207d960d7ce00dddd762e553f9605aa6f2eb882 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 20:58:28 -0600 Subject: [PATCH 110/154] flake8 fixes --- meshmode/discretization/__init__.py | 32 ++++++++++----------- meshmode/discretization/connection/modal.py | 16 +++++------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 912ce126a..7fbb37b9e 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -592,19 +592,19 @@ def resample_mesh_nodes(grp, iaxis): mat = actx.from_numpy(grp.from_mesh_interp_matrix()) fp_format = nodes.dtype - Ne, Nj = nodes.shape - Ni, Nj = mat.shape + ne, nj = nodes.shape + ni, nj = mat.shape kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), + lp.GlobalArg("arg1", fp_format, shape=(ne, nj), offset=lp.auto), # In default data layout apparently - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + lp.ValueArg("Ni", tags=[ParameterValue(ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(ne)]), ... ] @@ -674,19 +674,19 @@ def get_mat(grp, gref_axes): mat = get_mat(grp, ref_axes) fp_format = vec[grp.index].dtype - Ne, Nj = vec[grp.index].shape - Ni, Nj = mat.shape + ne, nj = vec[grp.index].shape + ni, nj = mat.shape kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, + lp.GlobalArg("arg1", fp_format, shape=(ne, nj), offset=lp.auto, tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + lp.ValueArg("Ni", tags=[ParameterValue(ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(ne)]), ... ] diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index 052c937ce..fafd629e4 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -165,20 +165,20 @@ def vandermonde_inverse(grp): fp_format = ary[grp.index].dtype vi_mat = vandermonde_inverse(grp) - Ne, Nj = ary[grp.index].shape - Ni, Nj = vi_mat.shape + ne, nj = ary[grp.index].shape + ni, nj = vi_mat.shape import loopy as lp kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(Ne, Nj), offset=lp.auto, + lp.GlobalArg("arg1", fp_format, shape=(ne, nj), offset=lp.auto, tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(Ni, Nj), offset=lp.auto, + lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(Ne, Ni), offset=lp.auto, + lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(Ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(Nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(Ne)]), + lp.ValueArg("Ni", tags=[ParameterValue(ni)]), + lp.ValueArg("Nj", tags=[ParameterValue(nj)]), + lp.ValueArg("Ne", tags=[ParameterValue(ne)]), ... ] kd_tag = KernelDataTag(kernel_data) From e2c4390af4266add1308872aa0ce74dbcbdce1fd Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 21:07:26 -0600 Subject: [PATCH 111/154] specify ValueArg datatypes --- meshmode/discretization/connection/direct.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 06f506510..303cb66ba 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -531,13 +531,13 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, lp.GlobalArg("to_element_indices", index_dtype, shape=("n_to_el_ind",), offset=lp.auto), - lp.ValueArg("n_to_nodes", + lp.ValueArg("n_to_nodes", np.int32, tags=[ParameterValue(n_to_nodes)]), - lp.ValueArg("n_from_nodes", + lp.ValueArg("n_from_nodes", np.int32, tags=[ParameterValue(n_from_nodes)]), - lp.ValueArg("n_to_el_ind", + lp.ValueArg("n_to_el_ind", np.int32, tags=[ParameterValue(n_to_el_ind)]), - lp.ValueArg("n_from_el_ind", + lp.ValueArg("n_from_el_ind", np.int32, tags=[ParameterValue(n_from_el_ind)]), lp.ValueArg("nelements", np.int32, tags=[ParameterValue(n_from_nodes)]), From fe04d4d93a3aa0e599223ff336994dc0df47c3db Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 22:39:31 -0600 Subject: [PATCH 112/154] Fixes to pass tests --- meshmode/discretization/connection/direct.py | 25 +++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 303cb66ba..087f59a9a 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -506,7 +506,7 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, - n_from_el_ind, n_to_el_ind, fp_format, index_dtype): + fp_format, index_dtype): t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 24 Jan 2022 22:42:13 -0600 Subject: [PATCH 113/154] Delete unneeded print statement --- meshmode/discretization/connection/direct.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 087f59a9a..a15d2d902 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -591,11 +591,6 @@ def pick_knl(n_to_nodes): index_dtype = batch.from_element_indices.dtype fp_format = resample_mat.dtype - print(mat_knl(nelements_vec, nelements_result, - n_to_nodes, n_from_nodes, - fp_format, index_dtype)) - #exit() - actx.call_loopy(mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, fp_format, index_dtype), From 243aeb9ea50b687a6b681ce473f3db3230d5e913 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 22:43:43 -0600 Subject: [PATCH 114/154] Delete redundant return --- meshmode/discretization/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 7fbb37b9e..5040752bf 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -699,13 +699,6 @@ def get_mat(grp, gref_axes): return _DOFArray(actx, tuple(data)) - return _DOFArray(actx, tuple( - actx.einsum("ij,ej->ei", - get_mat(grp, ref_axes), - vec[grp.index], - tagged=(FirstAxisIsElementsTag(),)) - for grp in discr.groups)) - # }}} # vim: fdm=marker From 28b8c55b04c62b20b94abb285696c71719160924 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 24 Jan 2022 23:09:39 -0600 Subject: [PATCH 115/154] Pass data types of all arrays into kernel --- meshmode/discretization/connection/direct.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index a15d2d902..83308b004 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -506,7 +506,7 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, - fp_format, index_dtype): + result_dtype, rmat_dtype, ary_dtype, index_dtype): t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Sun, 30 Jan 2022 17:09:36 -0600 Subject: [PATCH 116/154] Remove comment --- meshmode/dof_array.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/dof_array.py b/meshmode/dof_array.py index d4b504892..cd501839a 100644 --- a/meshmode/dof_array.py +++ b/meshmode/dof_array.py @@ -709,7 +709,6 @@ def _reduce_norm(actx, arys, ord): # NOTE: these are ordered by an expected usage frequency if ord == 2: - # Check with force_device_scalars return anp.sqrt(sum(subary*subary for subary in arys)) elif ord == np.inf: return reduce(anp.maximum, arys) From 25a6670536f8d5a0619506f643e817b2e0c48a8f Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 01:56:54 -0600 Subject: [PATCH 117/154] Only pass tags to einsum --- meshmode/discretization/__init__.py | 51 ++++----------------- meshmode/discretization/connection/modal.py | 26 ++--------- meshmode/transform_metadata.py | 12 ++--- requirements.txt | 1 + setup.py | 1 + 5 files changed, 22 insertions(+), 69 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index ac7a6acba..28cba6775 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -30,11 +30,12 @@ from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array from arraycontext import ArrayContext, make_loopy_program +from frozendict import frozendict import loopy as lp from meshmode.transform_metadata import ( ConcurrentElementInameTag, ConcurrentDOFInameTag, FirstAxisIsElementsTag, - IsDOFArray, ParameterValue, IsOpArray, KernelDataTag) + IsDOFArray, IsOpArray, EinsumArgsTags) from warnings import warn @@ -606,30 +607,13 @@ def resample_mesh_nodes(grp, iaxis): and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): return nodes - mat = actx.from_numpy(grp.from_mesh_interp_matrix()) - fp_format = nodes.dtype - ne, nj = nodes.shape - ni, nj = mat.shape - - kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(ne, nj), - offset=lp.auto), # In default data layout apparently - lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, - tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, - tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(ne)]), - ... - ] - - kd_tag = KernelDataTag(kernel_data) + kd_tag = EinsumArgsTags(frozendict({"out": [IsDOFArray()], + "arg0": [IsOpArray()]})) return actx.einsum("ij,ej->ei", - mat, + actx.from_numpy(grp.from_mesh_interp_matrix()), nodes, - tagged=(FirstAxisIsElementsTag(), kd_tag,)) + tagged=(FirstAxisIsElementsTag(), kd_tag)) result = make_obj_array([ _DOFArray(None, tuple([ @@ -688,28 +672,11 @@ def get_mat(grp, gref_axes): data = [] for grp in discr.groups: - mat = get_mat(grp, ref_axes) - fp_format = vec[grp.index].dtype - ne, nj = vec[grp.index].shape - ni, nj = mat.shape - - kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(ne, nj), offset=lp.auto, - tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, - tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, - tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(ne)]), - ... - ] - - kd_tag = KernelDataTag(kernel_data) + kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], + "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) data.append(actx.einsum("ij,ej->ei", - mat, + get_mat(grp, ref_axes), vec[grp.index], tagged=(FirstAxisIsElementsTag(), kd_tag,))) diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index fafd629e4..1c78e37f9 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -30,10 +30,11 @@ from arraycontext import (NotAnArrayContainerError, serialize_container, deserialize_container,) from meshmode.transform_metadata import (FirstAxisIsElementsTag, - KernelDataTag, IsDOFArray, IsOpArray, ParameterValue) + IsDOFArray, IsOpArray, EinsumArgsTags) from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup from meshmode.discretization.connection.direct import DiscretizationConnection +from frozendict import frozendict from pytools import keyed_memoize_in @@ -163,28 +164,11 @@ def vandermonde_inverse(grp): vdm_inv = la.inv(vdm) return actx.from_numpy(vdm_inv) - fp_format = ary[grp.index].dtype - vi_mat = vandermonde_inverse(grp) - ne, nj = ary[grp.index].shape - ni, nj = vi_mat.shape - - import loopy as lp - kernel_data = [ - lp.GlobalArg("arg1", fp_format, shape=(ne, nj), offset=lp.auto, - tags=[IsDOFArray()]), - lp.GlobalArg("arg0", fp_format, shape=(ni, nj), offset=lp.auto, - tags=[IsOpArray()]), - lp.GlobalArg("out", fp_format, shape=(ne, ni), offset=lp.auto, - tags=[IsDOFArray()], is_output=True), - lp.ValueArg("Ni", tags=[ParameterValue(ni)]), - lp.ValueArg("Nj", tags=[ParameterValue(nj)]), - lp.ValueArg("Ne", tags=[ParameterValue(ne)]), - ... - ] - kd_tag = KernelDataTag(kernel_data) + kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], + "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) return actx.einsum("ij,ej->ei", - vi_mat, + vandermonde_inverse(grp), ary[grp.index], tagged=(FirstAxisIsElementsTag(), kd_tag,)) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index a4f08ef92..155406caf 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -86,10 +86,10 @@ class IsOpArray(Tag): pass -class KernelDataTag(Tag): - """A tag that applies to :class:`loopy.LoopKernel`. Kernel data provided - with this tag can be later applied to the kernel. This is used, for - instance, to specify kernel data in einsum kernels.""" +class EinsumArgsTags(Tag): + """A tag containing a frozendict of lists of tags indexed by argument name.""" - def __init__(self, kernel_data): - self.kernel_data = kernel_data + def __init__(self, tags_list): + from frozendict import frozendict + assert isinstance(tags_list, frozendict) + self.tags_dict = tags_list diff --git a/requirements.txt b/requirements.txt index 9f1ebca1c..a5f603e7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ numpy recursivenodes +frozendict git+https://github.com/inducer/pytools.git#egg=pytools git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile diff --git a/setup.py b/setup.py index 41483390f..081762e2c 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ def main(): "arraycontext", + "frozendict", "recursivenodes", "dataclasses; python_version<='3.6'", ], From 6a613b9f2fbeb4f016b4f3bd6c2402664a302655 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 02:45:49 -0600 Subject: [PATCH 118/154] Use iterator instead of appending to list --- meshmode/discretization/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 28cba6775..07b66b210 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -670,15 +670,14 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) data = [] - for grp in discr.groups: + kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], + "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) - kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], - "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) - - data.append(actx.einsum("ij,ej->ei", + data = tuple((actx.einsum("ij,ej->ei", get_mat(grp, ref_axes), vec[grp.index], - tagged=(FirstAxisIsElementsTag(), kd_tag,))) + tagged=(FirstAxisIsElementsTag(), kd_tag,)) + for grp in discr.groups)) return _DOFArray(actx, tuple(data)) From f150cf06fa6facf59b9a666da772adcdddf953d8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 04:51:55 -0600 Subject: [PATCH 119/154] remove redundant tuple conversion --- meshmode/discretization/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 07b66b210..650fe6bdb 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -677,9 +677,9 @@ def get_mat(grp, gref_axes): get_mat(grp, ref_axes), vec[grp.index], tagged=(FirstAxisIsElementsTag(), kd_tag,)) - for grp in discr.groups)) + for grp in discr.groups)) - return _DOFArray(actx, tuple(data)) + return _DOFArray(actx, data) # }}} From ef8c10df722e379c7fc573a41536e4754ef0cd06 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 04:53:23 -0600 Subject: [PATCH 120/154] Remove redundant list declaration --- meshmode/discretization/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 650fe6bdb..3091aca0e 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -669,7 +669,6 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) - data = [] kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) From aa2d762de6300718fb53a5beb19c5bfe0da8ff24 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 13:35:49 -0600 Subject: [PATCH 121/154] Improve documentation, remove unused argument --- meshmode/discretization/connection/direct.py | 5 ++--- meshmode/transform_metadata.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 83308b004..ef47657bf 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -506,7 +506,7 @@ def _apply_with_inplace_updates(self, ary): @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_mat_knl_inplace")) def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, - result_dtype, rmat_dtype, ary_dtype, index_dtype): + result_dtype, rmat_dtype, ary_dtype): t_unit = make_loopy_program( """{[iel, idof, j]: 0<=iel Date: Mon, 31 Jan 2022 13:45:52 -0600 Subject: [PATCH 122/154] use type hint instead of assertion in transform_metadata --- meshmode/transform_metadata.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index f79151007..52a152876 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -29,6 +29,7 @@ """ from pytools.tag import Tag, UniqueTag +from frozendict import frozendict class FirstAxisIsElementsTag(Tag): @@ -87,7 +88,5 @@ class IsOpArray(Tag): class EinsumArgsTags(Tag): """A tag containing a frozendict of lists of tags indexed by argument name.""" - def __init__(self, tags_list): - from frozendict import frozendict - assert isinstance(tags_list, frozendict) - self.tags_dict = tags_list + def __init__(self, tags_dict: frozendict): + self.tags_dict = tags_dict From bdebcdbd557c9bf4c1e2e306562925be5e25047a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 31 Jan 2022 13:48:47 -0600 Subject: [PATCH 123/154] add autoclass comments --- meshmode/transform_metadata.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 52a152876..60f13aabe 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -2,6 +2,10 @@ .. autoclass:: FirstAxisIsElementsTag .. autoclass:: ConcurrentElementInameTag .. autoclass:: ConcurrentDOFInameTag +.. autoclass:: ParameterValue +.. autoclass:: IsDOFArray +.. autoclass:: IsOpArray +.. autoclass:: EinsumArgsTags """ __copyright__ = """ From 5a2eb9fcffb361bc8908e6f621b42695ad26ab75 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Feb 2022 00:41:18 -0600 Subject: [PATCH 124/154] Use immutables.Map instead of frozendict --- meshmode/array_context_old.py | 453 ++++++++++++++++++++ meshmode/discretization/__init__.py | 6 +- meshmode/discretization/connection/modal.py | 4 +- meshmode/transform_metadata.py | 8 +- requirements.txt | 2 +- setup.py | 2 +- 6 files changed, 464 insertions(+), 11 deletions(-) create mode 100644 meshmode/array_context_old.py diff --git a/meshmode/array_context_old.py b/meshmode/array_context_old.py new file mode 100644 index 000000000..9c2192e83 --- /dev/null +++ b/meshmode/array_context_old.py @@ -0,0 +1,453 @@ +""" +.. autoclass:: PyOpenCLArrayContext +.. autoclass:: PytatoPyOpenCLArrayContext +""" + +__copyright__ = "Copyright (C) 2020 Andreas Kloeckner" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from functools import partial +from pytools.tag import Tag, UniqueTag + +from arraycontext import ( # noqa: F401 + ArrayContext, + CommonSubexpressionTag, FirstAxisIsElementsTag, + #ParameterValue, IsDOFArray, + ArrayContainer, + is_array_container, is_array_container_type, + serialize_container, deserialize_container, + get_container_context, get_container_context_recursively, + with_container_arithmetic, + dataclass_array_container, + map_array_container, multimap_array_container, + rec_map_array_container, rec_multimap_array_container, + mapped_over_array_containers, + multimapped_over_array_containers, + thaw as _thaw, freeze, + + PyOpenCLArrayContext, + + make_loopy_program, + # Use the version defined in this file for now. This will need to be moved to arraycontext at some + # point. + #pytest_generate_tests_for_pyopencl_array_context + ) +from meshmode.transform_metadata import IsOpArray, KernelDataTag, ParameterValue, IsDOFArray +# {{{ Tags + + +# {{{ pytest integration + + +def _pytest_generate_tests_for_pyopencl_array_context(array_context_type, metafunc): + import pyopencl as cl + from pyopencl.tools import _ContextFactory + + class ArrayContextFactory(_ContextFactory): + def __call__(self): + ctx = super().__call__() + return array_context_type(cl.CommandQueue(ctx)) + + def __str__(self): + return ("" % + (self.device.name.strip(), + self.device.platform.name.strip())) + + import pyopencl.tools as cl_tools + arg_names = cl_tools.get_pyopencl_fixture_arg_names( + metafunc, extra_arg_names=["actx_factory"]) + + if not arg_names: + return + + arg_values, ids = cl_tools.get_pyopencl_fixture_arg_values() + if "actx_factory" in arg_names: + if "ctx_factory" in arg_names or "ctx_getter" in arg_names: + raise RuntimeError("Cannot use both an 'actx_factory' and a " + "'ctx_factory' / 'ctx_getter' as arguments.") + + for arg_dict in arg_values: + arg_dict["actx_factory"] = ArrayContextFactory(arg_dict["device"]) + + arg_values = [ + tuple(arg_dict[name] for name in arg_names) + for arg_dict in arg_values + ] + +import sys +from warnings import warn +from arraycontext import PyOpenCLArrayContext as PyOpenCLArrayContextBase +from arraycontext import PytatoPyOpenCLArrayContext as PytatoPyOpenCLArrayContextBase +from arraycontext.pytest import ( + _PytestPyOpenCLArrayContextFactoryWithClass, + _PytestPytatoPyOpenCLArrayContextFactory, + register_pytest_array_context_factory) + +def generate_pytest_generate_tests(array_context_type): + """Generate a function to parametrize tests for pytest to use + a :mod:`pyopencl` array context of the specified subtype. + + The returned function performs device enumeration analogously to + :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. + + Using the line: + + .. code-block:: python + + from meshmode.array_context import generate_pytest_generate_tests + pytest_generate_tests = + generate_pytest_generate_tests() + + + in your pytest test scripts allows you to use the arguments ctx_factory, + device, or platform in your test functions, and they will automatically be + run for each OpenCL device/platform in the system, as appropriate. + + It also allows you to specify the ``PYOPENCL_TEST`` environment variable + for device selection. + """ + + from functools import partial + + return lambda metafunc: partial( + _pytest_generate_tests_for_pyopencl_array_context, + array_context_type)(metafunc) + + +def pytest_generate_tests_for_pyopencl_array_context(metafunc): + """Parametrize tests for pytest to use a :mod:`pyopencl` array context. + + Performs device enumeration analogously to + :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. + + Using the line: + + .. code-block:: python + + from meshmode.array_context import pytest_generate_tests_for_pyopencl \ + as pytest_generate_tests + + in your pytest test scripts allows you to use the arguments ctx_factory, + device, or platform in your test functions, and they will automatically be + run for each OpenCL device/platform in the system, as appropriate. + + It also allows you to specify the ``PYOPENCL_TEST`` environment variable + for device selection. + """ + + generate_pytest_generate_tests(PyOpenCLArrayContext)(metafunc) + + +# }}} + +def thaw(actx, ary): + warn("meshmode.array_context.thaw is deprecated. Use arraycontext.thaw instead. " + "WARNING: The argument order is reversed between these two functions. " + "meshmode.array_context.thaw will continue to work until 2022.", + DeprecationWarning, stacklevel=2) + + from arraycontext import thaw as _thaw + # /!\ arg order flipped + return _thaw(ary, actx) + + +# {{{ kernel transform function + +def _transform_loopy_inner(t_unit): + import loopy as lp + from meshmode.transform_metadata import FirstAxisIsElementsTag + from arraycontext.transform_metadata import ElementwiseMapKernelTag + + from pymbolic.primitives import Subscript, Variable + + default_ep = t_unit.default_entrypoint + + # FIXME: Firedrake branch lacks kernel tags + kernel_tags = getattr(default_ep, "tags", ()) + + # {{{ FirstAxisIsElementsTag on kernel (compatibility) + + if any(isinstance(tag, FirstAxisIsElementsTag) for tag in kernel_tags): + if (len(default_ep.instructions) != 1 + or not isinstance( + default_ep.instructions[0], lp.Assignment)): + raise ValueError("FirstAxisIsElementsTag may only be applied to " + "a kernel if the kernel contains a single assignment.") + + stmt, = default_ep.instructions + + if not isinstance(stmt.assignee, Subscript): + raise ValueError("single assignment in FirstAxisIsElementsTag kernel " + "must be a subscript") + + output_name = stmt.assignee.aggregate.name + new_args = [ + arg.tagged(FirstAxisIsElementsTag()) + if arg.name == output_name else arg + for arg in default_ep.args] + default_ep = default_ep.copy(args=new_args) + t_unit = t_unit.with_kernel(default_ep) + + # }}} + + # {{{ ElementwiseMapKernelTag on kernel + + if any(isinstance(tag, ElementwiseMapKernelTag) for tag in kernel_tags): + el_inames = [] + dof_inames = [] + for stmt in default_ep.instructions: + if isinstance(stmt, lp.MultiAssignmentBase): + for assignee in stmt.assignees: + if isinstance(assignee, Variable): + # some scalar assignee kernel => no concurrency in the + # workload => skip + continue + if not isinstance(assignee, Subscript): + raise ValueError("assignees in " + "ElementwiseMapKernelTag-tagged kernels must be " + "subscripts") + + for i, subscript in enumerate(assignee.index_tuple[:2]): + if (not isinstance(subscript, Variable) + or subscript.name not in default_ep.all_inames()): + raise ValueError("subscripts in " + "ElementwiseMapKernelTag-tagged kernels must be " + "inames") + + if i == 0: + el_inames.append(subscript.name) + elif i == 1: + dof_inames.append(subscript.name) + + return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) + + # }}} + + # {{{ FirstAxisIsElementsTag on output variable + + first_axis_el_args = [arg.name for arg in default_ep.args + if any(isinstance(tag, FirstAxisIsElementsTag) for tag in arg.tags)] + + if first_axis_el_args: + el_inames = [] + dof_inames = [] + + for stmt in default_ep.instructions: + if isinstance(stmt, lp.MultiAssignmentBase): + for assignee in stmt.assignees: + if not isinstance(assignee, Subscript): + raise ValueError("assignees in " + "FirstAxisIsElementsTag-tagged kernels must be " + "subscripts") + + if assignee.aggregate.name not in first_axis_el_args: + continue + + subscripts = assignee.index_tuple[:2] + + for i, subscript in enumerate(subscripts): + if (not isinstance(subscript, Variable) + or subscript.name not in default_ep.all_inames()): + raise ValueError("subscripts in " + "FirstAxisIsElementsTag-tagged kernels must be " + "inames") + + if i == 0: + el_inames.append(subscript.name) + elif i == 1: + dof_inames.append(subscript.name) + return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) + + # }}} + + # {{{ element/dof iname tag + + from meshmode.transform_metadata import \ + ConcurrentElementInameTag, ConcurrentDOFInameTag + el_inames = [iname.name + for iname in default_ep.inames.values() + if ConcurrentElementInameTag() in iname.tags] + dof_inames = [iname.name + for iname in default_ep.inames.values() + if ConcurrentDOFInameTag() in iname.tags] + + if el_inames: + return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) + + # }}} + + # *shrug* no idea how to transform this thing. + return None + + +def _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames): + import loopy as lp + + if set(el_inames) & set(dof_inames): + raise ValueError("Some inames are marked as both 'element' and 'dof' " + "inames. These must be disjoint.") + + # Sorting ensures the same order of transformations is used every + # time; avoids accidentally generating cache misses or kernel + # hash conflicts. + + for dof_iname in sorted(dof_inames): + t_unit = lp.split_iname(t_unit, dof_iname, 32, inner_tag="l.0") + for el_iname in sorted(el_inames): + t_unit = lp.tag_inames(t_unit, {el_iname: "g.0"}) + return t_unit + +# }}} + + +# {{{ pyopencl array context subclass + +class PyOpenCLArrayContext(PyOpenCLArrayContextBase): + """Extends :class:`arraycontext.PyOpenCLArrayContext` with knowledge about + program transformation for finite element programs. + + See :mod:`meshmode.transform_metadata` for relevant metadata. + """ + + def transform_loopy_program(self, t_unit): + default_ep = t_unit.default_entrypoint + options = default_ep.options + if not (options.return_dict and options.no_numpy): + raise ValueError("Loopy kernel passed to call_loopy must " + "have return_dict and no_numpy options set. " + "Did you use arraycontext.make_loopy_program " + "to create this kernel?") + + transformed_t_unit = _transform_loopy_inner(t_unit) + + if transformed_t_unit is not None: + return transformed_t_unit + + warn("meshmode.array_context.PyOpenCLArrayContext." + "transform_loopy_program fell back on " + "arraycontext.PyOpenCLArrayContext to find a transform for " + f"'{default_ep.name}'. " + "Please update your program to use metadata from " + "meshmode.transform_metadata. " + "This code path will stop working in 2022.", + DeprecationWarning, stacklevel=3) + + return super().transform_loopy_program(t_unit) + +# }}} + + +# {{{ pytato pyopencl array context subclass + +class PytatoPyOpenCLArrayContext(PytatoPyOpenCLArrayContextBase): + def transform_loopy_program(self, t_unit): + # FIXME: Do not parallelize for now. + return t_unit + +# }}} + + +# {{{ pytest actx factory + +class PytestPyOpenCLArrayContextFactory( + _PytestPyOpenCLArrayContextFactoryWithClass): + actx_class = PyOpenCLArrayContext + + +# deprecated +class PytestPyOpenCLArrayContextFactoryWithHostScalars( + _PytestPyOpenCLArrayContextFactoryWithClass): + actx_class = PyOpenCLArrayContext + force_device_scalars = False + + +class PytestPytatoPyOpenCLArrayContextFactory( + _PytestPytatoPyOpenCLArrayContextFactory): + + @property + def actx_class(self): + return PytatoPyOpenCLArrayContext + + +register_pytest_array_context_factory("meshmode.pyopencl", + PytestPyOpenCLArrayContextFactory) +register_pytest_array_context_factory("meshmode.pyopencl-deprecated", + PytestPyOpenCLArrayContextFactoryWithHostScalars) +register_pytest_array_context_factory("meshmode.pytato_cl", + PytestPytatoPyOpenCLArrayContextFactory) + +# }}} + + +# {{{ handle move deprecation + +_actx_names = ( + "ArrayContext", + + "CommonSubexpressionTag", + "FirstAxisIsElementsTag", + + "ArrayContainer", + "is_array_container", "is_array_container_type", + "serialize_container", "deserialize_container", + "get_container_context", "get_container_context_recursively", + "with_container_arithmetic", + "dataclass_array_container", + + "map_array_container", "multimap_array_container", + "rec_map_array_container", "rec_multimap_array_container", + "mapped_over_array_containers", + "multimapped_over_array_containers", + "freeze", + + "make_loopy_program", + + "pytest_generate_tests_for_pyopencl_array_context" + ) + + +if sys.version_info >= (3, 7): + def __getattr__(name): + if name not in _actx_names: + raise AttributeError(name) + + import arraycontext + result = getattr(arraycontext, name) + + warn(f"meshmode.array_context.{name} is deprecated. " + f"Use arraycontext.{name} instead. " + f"meshmode.array_context.{name} will continue to work until 2022.", + DeprecationWarning, stacklevel=2) + + return result +else: + def _import_names(): + import arraycontext + for name in _actx_names: + globals()[name] = getattr(arraycontext, name) + + _import_names() + +# }}} + + +# vim: foldmethod=marker diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 3091aca0e..09177c49d 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -30,7 +30,7 @@ from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array from arraycontext import ArrayContext, make_loopy_program -from frozendict import frozendict +from immutables import Map import loopy as lp from meshmode.transform_metadata import ( @@ -607,7 +607,7 @@ def resample_mesh_nodes(grp, iaxis): and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): return nodes - kd_tag = EinsumArgsTags(frozendict({"out": [IsDOFArray()], + kd_tag = EinsumArgsTags(Map({"out": [IsDOFArray()], "arg0": [IsOpArray()]})) return actx.einsum("ij,ej->ei", @@ -669,7 +669,7 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) - kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], + kd_tag = EinsumArgsTags(Map({"arg0": [IsOpArray()], "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) data = tuple((actx.einsum("ij,ej->ei", diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index 1c78e37f9..bbd700a6e 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -34,7 +34,7 @@ from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup from meshmode.discretization.connection.direct import DiscretizationConnection -from frozendict import frozendict +from immutables import Map from pytools import keyed_memoize_in @@ -164,7 +164,7 @@ def vandermonde_inverse(grp): vdm_inv = la.inv(vdm) return actx.from_numpy(vdm_inv) - kd_tag = EinsumArgsTags(frozendict({"arg0": [IsOpArray()], + kd_tag = EinsumArgsTags(Map({"arg0": [IsOpArray()], "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) return actx.einsum("ij,ej->ei", diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index 60f13aabe..a8f0e6187 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -33,7 +33,7 @@ """ from pytools.tag import Tag, UniqueTag -from frozendict import frozendict +from immutables import Map class FirstAxisIsElementsTag(Tag): @@ -90,7 +90,7 @@ class IsOpArray(Tag): class EinsumArgsTags(Tag): - """A tag containing a frozendict of lists of tags indexed by argument name.""" + """A tag containing a FrozenMap of lists of tags indexed by argument name.""" - def __init__(self, tags_dict: frozendict): - self.tags_dict = tags_dict + def __init__(self, tags_map: Map): + self.tags_map = tags_map diff --git a/requirements.txt b/requirements.txt index a5f603e7a..24a2f5bbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy recursivenodes -frozendict +immutables git+https://github.com/inducer/pytools.git#egg=pytools git+https://github.com/inducer/gmsh_interop.git#egg=gmsh_interop git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile diff --git a/setup.py b/setup.py index 081762e2c..e20641863 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def main(): "arraycontext", - "frozendict", + "immutables", "recursivenodes", "dataclasses; python_version<='3.6'", ], From 368ed946db608d5c0cee885ab67a25c9e8177191 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Feb 2022 01:31:21 -0600 Subject: [PATCH 125/154] Fix doc error --- meshmode/transform_metadata.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index a8f0e6187..8a72fbc20 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -92,5 +92,6 @@ class IsOpArray(Tag): class EinsumArgsTags(Tag): """A tag containing a FrozenMap of lists of tags indexed by argument name.""" - def __init__(self, tags_map: Map): + def __init__(self, tags_map): + assert isinstance(tags_map, Map) self.tags_map = tags_map From 9c02e7aa6afd9374385d3307390298a4868ffed1 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Feb 2022 02:19:39 -0600 Subject: [PATCH 126/154] Delete unneeded file --- meshmode/array_context_old.py | 453 ---------------------------------- 1 file changed, 453 deletions(-) delete mode 100644 meshmode/array_context_old.py diff --git a/meshmode/array_context_old.py b/meshmode/array_context_old.py deleted file mode 100644 index 9c2192e83..000000000 --- a/meshmode/array_context_old.py +++ /dev/null @@ -1,453 +0,0 @@ -""" -.. autoclass:: PyOpenCLArrayContext -.. autoclass:: PytatoPyOpenCLArrayContext -""" - -__copyright__ = "Copyright (C) 2020 Andreas Kloeckner" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -from functools import partial -from pytools.tag import Tag, UniqueTag - -from arraycontext import ( # noqa: F401 - ArrayContext, - CommonSubexpressionTag, FirstAxisIsElementsTag, - #ParameterValue, IsDOFArray, - ArrayContainer, - is_array_container, is_array_container_type, - serialize_container, deserialize_container, - get_container_context, get_container_context_recursively, - with_container_arithmetic, - dataclass_array_container, - map_array_container, multimap_array_container, - rec_map_array_container, rec_multimap_array_container, - mapped_over_array_containers, - multimapped_over_array_containers, - thaw as _thaw, freeze, - - PyOpenCLArrayContext, - - make_loopy_program, - # Use the version defined in this file for now. This will need to be moved to arraycontext at some - # point. - #pytest_generate_tests_for_pyopencl_array_context - ) -from meshmode.transform_metadata import IsOpArray, KernelDataTag, ParameterValue, IsDOFArray -# {{{ Tags - - -# {{{ pytest integration - - -def _pytest_generate_tests_for_pyopencl_array_context(array_context_type, metafunc): - import pyopencl as cl - from pyopencl.tools import _ContextFactory - - class ArrayContextFactory(_ContextFactory): - def __call__(self): - ctx = super().__call__() - return array_context_type(cl.CommandQueue(ctx)) - - def __str__(self): - return ("" % - (self.device.name.strip(), - self.device.platform.name.strip())) - - import pyopencl.tools as cl_tools - arg_names = cl_tools.get_pyopencl_fixture_arg_names( - metafunc, extra_arg_names=["actx_factory"]) - - if not arg_names: - return - - arg_values, ids = cl_tools.get_pyopencl_fixture_arg_values() - if "actx_factory" in arg_names: - if "ctx_factory" in arg_names or "ctx_getter" in arg_names: - raise RuntimeError("Cannot use both an 'actx_factory' and a " - "'ctx_factory' / 'ctx_getter' as arguments.") - - for arg_dict in arg_values: - arg_dict["actx_factory"] = ArrayContextFactory(arg_dict["device"]) - - arg_values = [ - tuple(arg_dict[name] for name in arg_names) - for arg_dict in arg_values - ] - -import sys -from warnings import warn -from arraycontext import PyOpenCLArrayContext as PyOpenCLArrayContextBase -from arraycontext import PytatoPyOpenCLArrayContext as PytatoPyOpenCLArrayContextBase -from arraycontext.pytest import ( - _PytestPyOpenCLArrayContextFactoryWithClass, - _PytestPytatoPyOpenCLArrayContextFactory, - register_pytest_array_context_factory) - -def generate_pytest_generate_tests(array_context_type): - """Generate a function to parametrize tests for pytest to use - a :mod:`pyopencl` array context of the specified subtype. - - The returned function performs device enumeration analogously to - :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. - - Using the line: - - .. code-block:: python - - from meshmode.array_context import generate_pytest_generate_tests - pytest_generate_tests = - generate_pytest_generate_tests() - - - in your pytest test scripts allows you to use the arguments ctx_factory, - device, or platform in your test functions, and they will automatically be - run for each OpenCL device/platform in the system, as appropriate. - - It also allows you to specify the ``PYOPENCL_TEST`` environment variable - for device selection. - """ - - from functools import partial - - return lambda metafunc: partial( - _pytest_generate_tests_for_pyopencl_array_context, - array_context_type)(metafunc) - - -def pytest_generate_tests_for_pyopencl_array_context(metafunc): - """Parametrize tests for pytest to use a :mod:`pyopencl` array context. - - Performs device enumeration analogously to - :func:`pyopencl.tools.pytest_generate_tests_for_pyopencl`. - - Using the line: - - .. code-block:: python - - from meshmode.array_context import pytest_generate_tests_for_pyopencl \ - as pytest_generate_tests - - in your pytest test scripts allows you to use the arguments ctx_factory, - device, or platform in your test functions, and they will automatically be - run for each OpenCL device/platform in the system, as appropriate. - - It also allows you to specify the ``PYOPENCL_TEST`` environment variable - for device selection. - """ - - generate_pytest_generate_tests(PyOpenCLArrayContext)(metafunc) - - -# }}} - -def thaw(actx, ary): - warn("meshmode.array_context.thaw is deprecated. Use arraycontext.thaw instead. " - "WARNING: The argument order is reversed between these two functions. " - "meshmode.array_context.thaw will continue to work until 2022.", - DeprecationWarning, stacklevel=2) - - from arraycontext import thaw as _thaw - # /!\ arg order flipped - return _thaw(ary, actx) - - -# {{{ kernel transform function - -def _transform_loopy_inner(t_unit): - import loopy as lp - from meshmode.transform_metadata import FirstAxisIsElementsTag - from arraycontext.transform_metadata import ElementwiseMapKernelTag - - from pymbolic.primitives import Subscript, Variable - - default_ep = t_unit.default_entrypoint - - # FIXME: Firedrake branch lacks kernel tags - kernel_tags = getattr(default_ep, "tags", ()) - - # {{{ FirstAxisIsElementsTag on kernel (compatibility) - - if any(isinstance(tag, FirstAxisIsElementsTag) for tag in kernel_tags): - if (len(default_ep.instructions) != 1 - or not isinstance( - default_ep.instructions[0], lp.Assignment)): - raise ValueError("FirstAxisIsElementsTag may only be applied to " - "a kernel if the kernel contains a single assignment.") - - stmt, = default_ep.instructions - - if not isinstance(stmt.assignee, Subscript): - raise ValueError("single assignment in FirstAxisIsElementsTag kernel " - "must be a subscript") - - output_name = stmt.assignee.aggregate.name - new_args = [ - arg.tagged(FirstAxisIsElementsTag()) - if arg.name == output_name else arg - for arg in default_ep.args] - default_ep = default_ep.copy(args=new_args) - t_unit = t_unit.with_kernel(default_ep) - - # }}} - - # {{{ ElementwiseMapKernelTag on kernel - - if any(isinstance(tag, ElementwiseMapKernelTag) for tag in kernel_tags): - el_inames = [] - dof_inames = [] - for stmt in default_ep.instructions: - if isinstance(stmt, lp.MultiAssignmentBase): - for assignee in stmt.assignees: - if isinstance(assignee, Variable): - # some scalar assignee kernel => no concurrency in the - # workload => skip - continue - if not isinstance(assignee, Subscript): - raise ValueError("assignees in " - "ElementwiseMapKernelTag-tagged kernels must be " - "subscripts") - - for i, subscript in enumerate(assignee.index_tuple[:2]): - if (not isinstance(subscript, Variable) - or subscript.name not in default_ep.all_inames()): - raise ValueError("subscripts in " - "ElementwiseMapKernelTag-tagged kernels must be " - "inames") - - if i == 0: - el_inames.append(subscript.name) - elif i == 1: - dof_inames.append(subscript.name) - - return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) - - # }}} - - # {{{ FirstAxisIsElementsTag on output variable - - first_axis_el_args = [arg.name for arg in default_ep.args - if any(isinstance(tag, FirstAxisIsElementsTag) for tag in arg.tags)] - - if first_axis_el_args: - el_inames = [] - dof_inames = [] - - for stmt in default_ep.instructions: - if isinstance(stmt, lp.MultiAssignmentBase): - for assignee in stmt.assignees: - if not isinstance(assignee, Subscript): - raise ValueError("assignees in " - "FirstAxisIsElementsTag-tagged kernels must be " - "subscripts") - - if assignee.aggregate.name not in first_axis_el_args: - continue - - subscripts = assignee.index_tuple[:2] - - for i, subscript in enumerate(subscripts): - if (not isinstance(subscript, Variable) - or subscript.name not in default_ep.all_inames()): - raise ValueError("subscripts in " - "FirstAxisIsElementsTag-tagged kernels must be " - "inames") - - if i == 0: - el_inames.append(subscript.name) - elif i == 1: - dof_inames.append(subscript.name) - return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) - - # }}} - - # {{{ element/dof iname tag - - from meshmode.transform_metadata import \ - ConcurrentElementInameTag, ConcurrentDOFInameTag - el_inames = [iname.name - for iname in default_ep.inames.values() - if ConcurrentElementInameTag() in iname.tags] - dof_inames = [iname.name - for iname in default_ep.inames.values() - if ConcurrentDOFInameTag() in iname.tags] - - if el_inames: - return _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames) - - # }}} - - # *shrug* no idea how to transform this thing. - return None - - -def _transform_with_element_and_dof_inames(t_unit, el_inames, dof_inames): - import loopy as lp - - if set(el_inames) & set(dof_inames): - raise ValueError("Some inames are marked as both 'element' and 'dof' " - "inames. These must be disjoint.") - - # Sorting ensures the same order of transformations is used every - # time; avoids accidentally generating cache misses or kernel - # hash conflicts. - - for dof_iname in sorted(dof_inames): - t_unit = lp.split_iname(t_unit, dof_iname, 32, inner_tag="l.0") - for el_iname in sorted(el_inames): - t_unit = lp.tag_inames(t_unit, {el_iname: "g.0"}) - return t_unit - -# }}} - - -# {{{ pyopencl array context subclass - -class PyOpenCLArrayContext(PyOpenCLArrayContextBase): - """Extends :class:`arraycontext.PyOpenCLArrayContext` with knowledge about - program transformation for finite element programs. - - See :mod:`meshmode.transform_metadata` for relevant metadata. - """ - - def transform_loopy_program(self, t_unit): - default_ep = t_unit.default_entrypoint - options = default_ep.options - if not (options.return_dict and options.no_numpy): - raise ValueError("Loopy kernel passed to call_loopy must " - "have return_dict and no_numpy options set. " - "Did you use arraycontext.make_loopy_program " - "to create this kernel?") - - transformed_t_unit = _transform_loopy_inner(t_unit) - - if transformed_t_unit is not None: - return transformed_t_unit - - warn("meshmode.array_context.PyOpenCLArrayContext." - "transform_loopy_program fell back on " - "arraycontext.PyOpenCLArrayContext to find a transform for " - f"'{default_ep.name}'. " - "Please update your program to use metadata from " - "meshmode.transform_metadata. " - "This code path will stop working in 2022.", - DeprecationWarning, stacklevel=3) - - return super().transform_loopy_program(t_unit) - -# }}} - - -# {{{ pytato pyopencl array context subclass - -class PytatoPyOpenCLArrayContext(PytatoPyOpenCLArrayContextBase): - def transform_loopy_program(self, t_unit): - # FIXME: Do not parallelize for now. - return t_unit - -# }}} - - -# {{{ pytest actx factory - -class PytestPyOpenCLArrayContextFactory( - _PytestPyOpenCLArrayContextFactoryWithClass): - actx_class = PyOpenCLArrayContext - - -# deprecated -class PytestPyOpenCLArrayContextFactoryWithHostScalars( - _PytestPyOpenCLArrayContextFactoryWithClass): - actx_class = PyOpenCLArrayContext - force_device_scalars = False - - -class PytestPytatoPyOpenCLArrayContextFactory( - _PytestPytatoPyOpenCLArrayContextFactory): - - @property - def actx_class(self): - return PytatoPyOpenCLArrayContext - - -register_pytest_array_context_factory("meshmode.pyopencl", - PytestPyOpenCLArrayContextFactory) -register_pytest_array_context_factory("meshmode.pyopencl-deprecated", - PytestPyOpenCLArrayContextFactoryWithHostScalars) -register_pytest_array_context_factory("meshmode.pytato_cl", - PytestPytatoPyOpenCLArrayContextFactory) - -# }}} - - -# {{{ handle move deprecation - -_actx_names = ( - "ArrayContext", - - "CommonSubexpressionTag", - "FirstAxisIsElementsTag", - - "ArrayContainer", - "is_array_container", "is_array_container_type", - "serialize_container", "deserialize_container", - "get_container_context", "get_container_context_recursively", - "with_container_arithmetic", - "dataclass_array_container", - - "map_array_container", "multimap_array_container", - "rec_map_array_container", "rec_multimap_array_container", - "mapped_over_array_containers", - "multimapped_over_array_containers", - "freeze", - - "make_loopy_program", - - "pytest_generate_tests_for_pyopencl_array_context" - ) - - -if sys.version_info >= (3, 7): - def __getattr__(name): - if name not in _actx_names: - raise AttributeError(name) - - import arraycontext - result = getattr(arraycontext, name) - - warn(f"meshmode.array_context.{name} is deprecated. " - f"Use arraycontext.{name} instead. " - f"meshmode.array_context.{name} will continue to work until 2022.", - DeprecationWarning, stacklevel=2) - - return result -else: - def _import_names(): - import arraycontext - for name in _actx_names: - globals()[name] = getattr(arraycontext, name) - - _import_names() - -# }}} - - -# vim: foldmethod=marker From 8e85fefb19d87f82d0add0795a6e4b0bc71c9955 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 7 Feb 2022 18:07:48 -0500 Subject: [PATCH 127/154] Update documentation --- meshmode/transform_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index a8f0e6187..1b182cda6 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -90,7 +90,7 @@ class IsOpArray(Tag): class EinsumArgsTags(Tag): - """A tag containing a FrozenMap of lists of tags indexed by argument name.""" + """A tag containing an `immutables.Map` of tuples of tags indexed by argument name.""" def __init__(self, tags_map: Map): self.tags_map = tags_map From da69c01b6ba8f7cdc7c24e57bc7c93316ef387d5 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Wed, 9 Feb 2022 14:39:00 -0600 Subject: [PATCH 128/154] Do type conversion inside constructor --- meshmode/discretization/__init__.py | 9 ++++----- meshmode/discretization/connection/modal.py | 5 ++--- meshmode/transform_metadata.py | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 09177c49d..109f969ba 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -30,7 +30,6 @@ from pytools import memoize_in, memoize_method, keyed_memoize_in from pytools.obj_array import make_obj_array from arraycontext import ArrayContext, make_loopy_program -from immutables import Map import loopy as lp from meshmode.transform_metadata import ( @@ -607,8 +606,8 @@ def resample_mesh_nodes(grp, iaxis): and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): return nodes - kd_tag = EinsumArgsTags(Map({"out": [IsDOFArray()], - "arg0": [IsOpArray()]})) + kd_tag = EinsumArgsTags({"out": [IsDOFArray()], + "arg0": [IsOpArray()]}) return actx.einsum("ij,ej->ei", actx.from_numpy(grp.from_mesh_interp_matrix()), @@ -669,8 +668,8 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) - kd_tag = EinsumArgsTags(Map({"arg0": [IsOpArray()], - "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) + kd_tag = EinsumArgsTags({"arg0": [IsOpArray()], + "arg1": [IsDOFArray()], "out": [IsDOFArray()]}) data = tuple((actx.einsum("ij,ej->ei", get_mat(grp, ref_axes), diff --git a/meshmode/discretization/connection/modal.py b/meshmode/discretization/connection/modal.py index bbd700a6e..87d5e9290 100644 --- a/meshmode/discretization/connection/modal.py +++ b/meshmode/discretization/connection/modal.py @@ -34,7 +34,6 @@ from meshmode.discretization import InterpolatoryElementGroupBase from meshmode.discretization.poly_element import QuadratureSimplexElementGroup from meshmode.discretization.connection.direct import DiscretizationConnection -from immutables import Map from pytools import keyed_memoize_in @@ -164,8 +163,8 @@ def vandermonde_inverse(grp): vdm_inv = la.inv(vdm) return actx.from_numpy(vdm_inv) - kd_tag = EinsumArgsTags(Map({"arg0": [IsOpArray()], - "arg1": [IsDOFArray()], "out": [IsDOFArray()]})) + kd_tag = EinsumArgsTags({"arg0": [IsOpArray()], + "arg1": [IsDOFArray()], "out": [IsDOFArray()]}) return actx.einsum("ij,ej->ei", vandermonde_inverse(grp), diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index bf01e23f0..ce83fa39b 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -93,5 +93,4 @@ class EinsumArgsTags(Tag): """A tag containing an `immutables.Map` of tuples of tags indexed by argument name.""" def __init__(self, tags_map): - assert isinstance(tags_map, Map) - self.tags_map = tags_map + self.tags_map = Map(tags_map) From a1be7d06efee6d035351c0a6f40e5c4ab5c511c8 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 13 Feb 2022 19:11:55 -0600 Subject: [PATCH 129/154] Flake8 fix --- meshmode/transform_metadata.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index ce83fa39b..b070ae02c 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -90,7 +90,9 @@ class IsOpArray(Tag): class EinsumArgsTags(Tag): - """A tag containing an `immutables.Map` of tuples of tags indexed by argument name.""" + """A tag containing an `immutables.Map` of tuples of tags indexed by + argument name. + """ def __init__(self, tags_map): self.tags_map = Map(tags_map) From fa3e20a8dd22af7cdefd3d25e619b1583538ce77 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 15 Feb 2022 01:37:22 -0600 Subject: [PATCH 130/154] Make tags classes dataclasses --- meshmode/discretization/__init__.py | 10 ++--- meshmode/discretization/connection/direct.py | 42 +++++++++++++++----- meshmode/discretization/connection/modal.py | 4 +- meshmode/transform_metadata.py | 12 +++--- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 109f969ba..017c76567 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -606,13 +606,13 @@ def resample_mesh_nodes(grp, iaxis): and np.linalg.norm(grp_unit_nodes - meg_unit_nodes) < tol): return nodes - kd_tag = EinsumArgsTags({"out": [IsDOFArray()], - "arg0": [IsOpArray()]}) + kd_tag = EinsumArgsTags({"out": (IsDOFArray(),), + "arg0": (IsOpArray(),)}) return actx.einsum("ij,ej->ei", actx.from_numpy(grp.from_mesh_interp_matrix()), nodes, - tagged=(FirstAxisIsElementsTag(), kd_tag)) + tagged=(FirstAxisIsElementsTag(), kd_tag,)) result = make_obj_array([ _DOFArray(None, tuple([ @@ -668,8 +668,8 @@ def get_mat(grp, gref_axes): return actx.from_numpy(mat) - kd_tag = EinsumArgsTags({"arg0": [IsOpArray()], - "arg1": [IsDOFArray()], "out": [IsDOFArray()]}) + kd_tag = EinsumArgsTags({"arg0": (IsOpArray(),), + "arg1": (IsDOFArray(),), "out": (IsDOFArray(),)}) data = tuple((actx.einsum("ij,ej->ei", get_mat(grp, ref_axes), diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index ef47657bf..27f191298 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -543,7 +543,9 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_picking_knl_inplace")) - def pick_knl(n_to_nodes): + def pick_knl(nelements, nelements_result, n_to_nodes, nelements_vec, + n_from_nodes, result_dtype, ary_dtype, from_dtype, + to_dtype, pick_list_dtype): t_unit = make_loopy_program( """{[iel, idof]: 0<=ielei", vandermonde_inverse(grp), diff --git a/meshmode/transform_metadata.py b/meshmode/transform_metadata.py index b070ae02c..b01ad74ca 100644 --- a/meshmode/transform_metadata.py +++ b/meshmode/transform_metadata.py @@ -32,8 +32,9 @@ THE SOFTWARE. """ -from pytools.tag import Tag, UniqueTag +from pytools.tag import Tag, UniqueTag, tag_dataclass from immutables import Map +from typing import Any class FirstAxisIsElementsTag(Tag): @@ -64,6 +65,7 @@ class ConcurrentDOFInameTag(Tag): """ +@tag_dataclass class ParameterValue(UniqueTag): """A tag that applies to :class:`loopy.ValueArg`. Instances of this tag are initialized with the value of the parameter and this value may be @@ -71,9 +73,7 @@ class ParameterValue(UniqueTag): calls to `loopy.fix_parameter` to `transform_loopy_program` so that all kernel transformations may occur there. """ - - def __init__(self, value): - self.value = value + value: Any class IsDOFArray(Tag): @@ -89,10 +89,12 @@ class IsOpArray(Tag): pass +@tag_dataclass class EinsumArgsTags(Tag): """A tag containing an `immutables.Map` of tuples of tags indexed by argument name. """ + tags_map: Map def __init__(self, tags_map): - self.tags_map = Map(tags_map) + object.__setattr__(self, "tags_map", Map(tags_map)) From 322cd4a3169561f05e15affd2da67d0c90829e5a Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Mon, 14 Mar 2022 19:18:07 -0400 Subject: [PATCH 131/154] kernel_data for kernels --- meshmode/discretization/connection/direct.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index ef47657bf..8fb4bba2c 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -377,7 +377,7 @@ def batch_mat_knl(): end result[iel, idof] = rowres if from_element_indices[iel] != -1 else 0 """, - [ + kernel_data=[ lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", offset=lp.auto), @@ -403,7 +403,7 @@ def batch_pick_knl(): ary[from_element_indices[iel], pick_list[idof]] if from_element_indices[iel] != -1 else 0) """, - [ + kernel_data=[ lp.GlobalArg("ary", None, shape="nelements_vec, n_from_nodes", offset=lp.auto), @@ -515,7 +515,7 @@ def mat_knl(nelements_vec, nelements_result, n_to_nodes, n_from_nodes, "result[to_element_indices[iel], idof] \ = sum(j, resample_mat[idof, j] \ * ary[from_element_indices[iel], j])", - [ + kernel_data=[ lp.GlobalArg("result", result_dtype, shape="nelements_result, n_to_nodes", offset=lp.auto, tags=[IsDOFArray()]), @@ -550,7 +550,7 @@ def pick_knl(n_to_nodes): 0<=idof Date: Mon, 14 Mar 2022 19:21:20 -0400 Subject: [PATCH 132/154] extraneous bracket --- meshmode/discretization/connection/direct.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 3fe4aef99..5cc26f339 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -553,7 +553,6 @@ def pick_knl(nelements, nelements_result, n_to_nodes, nelements_vec, "result[to_element_indices[iel], idof] \ = ary[from_element_indices[iel], pick_list[idof]]", kernel_data=[ - [ lp.GlobalArg("result", result_dtype, shape="nelements_result, n_to_nodes", offset=lp.auto, tags=[IsDOFArray()]), From 404f709c08c0df904340762faaf6d18239ce5d7b Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 25 Mar 2022 10:20:40 -0500 Subject: [PATCH 133/154] test_element_orientation_via_single_elements (from @lukeolson) --- meshmode/mesh/generation.py | 6 +++- test/test_mesh.py | 70 +++++++++++++++++++++++++++++++++++-- test/testmesh.msh | 15 ++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 test/testmesh.msh diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py index 35accf884..6a4867f60 100644 --- a/meshmode/mesh/generation.py +++ b/meshmode/mesh/generation.py @@ -570,7 +570,7 @@ def generate_sphere(r: float, order: int, *, # }}} -# {{ generate_surface_of_revolution +# {{{ generate_surface_of_revolution def generate_surface_of_revolution( get_radius: Callable[[np.ndarray, np.ndarray], np.ndarray], @@ -754,6 +754,8 @@ def idx(i, j): # }}} +# {{{ generate_torus + def generate_torus( r_major: float, r_minor: float, n_major: int = 20, n_minor: int = 10, order: int = 1, @@ -807,6 +809,8 @@ def generate_torus( return mesh +# }}} + # {{{ get_urchin diff --git a/test/test_mesh.py b/test/test_mesh.py index 5fbb7c571..cbcde0316 100644 --- a/test/test_mesh.py +++ b/test/test_mesh.py @@ -1,4 +1,7 @@ -__copyright__ = "Copyright (C) 2020 Andreas Kloeckner" +__copyright__ = """ +Copyright (C) 2020 Andreas Kloeckner +Copyright (C) 2021 University of Illinois Board of Trustees +""" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -42,6 +45,7 @@ LegendreGaussLobattoTensorProductGroupFactory, ) import meshmode.mesh.generation as mgen +import meshmode.mesh.io as mio from meshmode.mesh.tools import AffineMap import modepy as mp @@ -456,7 +460,7 @@ def test_merge_and_map(actx_factory, group_cls, visualize=False): # {{{ element orientation -def test_element_orientation(): +def test_element_orientation_via_flipping(): from meshmode.mesh.io import generate_gmsh, FileSource mesh_order = 3 @@ -485,6 +489,68 @@ def test_element_orientation(): assert ((mesh_orient < 0) == (flippy > 0)).all() + +@pytest.mark.parametrize("order", [1, 2, 3]) +def test_element_orientation_via_single_elements(order): + from meshmode.mesh.processing import find_volume_mesh_element_group_orientation + + def check(vertices, element_indices, tol=1e-14): + grp = mgen.make_group_from_vertices(vertices, element_indices, order) + orient = find_volume_mesh_element_group_orientation(vertices, grp) + return ( + np.where(orient > tol)[0], + np.where(orient < tol)[0], + np.where(np.abs(orient) <= tol)[0]) + + # References: + # https://github.com/inducer/meshmode/pull/314 + # https://github.com/lukeolson/mesh_orientation/blob/460bb2b634e2abb6aa32c3d02e2c732969bf08bf/check.py + # https://math.stackexchange.com/questions/4209203/signed-volume-for-tetrahedra-n-simplices + + # 3D (pos) + vertices = np.array([[1, 0, 0], + [0, 1, 0], + [0, 0, 0], + [0, 0, 1]]) + elements = np.array([[0, 1, 2, 3]]) + el_ind_pos, el_ind_neg, el_ind_zero = check(vertices.T, elements) + assert len(el_ind_pos) == 1 + assert len(el_ind_neg) == 0 + assert len(el_ind_zero) == 0 + + # (neg) + elements = np.array([[1, 0, 2, 3]]) + el_ind_pos, el_ind_neg, el_ind_zero = check(vertices.T, elements) + assert len(el_ind_pos) == 0 + assert len(el_ind_neg) == 1 + assert len(el_ind_zero) == 0 + + # 2D + # CCW (positive) + vertices = np.array([[1, 0], + [0, 1], + [0, 0]]) + elements = np.array([[0, 1, 2]]) + el_ind_pos, el_ind_neg, el_ind_zero = check(vertices.T, elements) + assert len(el_ind_pos) == 1 + assert len(el_ind_neg) == 0 + assert len(el_ind_zero) == 0 + + # CW (negative) + elements = np.array([[0, 2, 1]]) + el_ind_pos, el_ind_neg, el_ind_zero = check(vertices.T, elements) + assert len(el_ind_pos) == 0 + assert len(el_ind_neg) == 1 + assert len(el_ind_zero) == 0 + + mesh = mio.read_gmsh("testmesh.msh", force_ambient_dim=2, + mesh_construction_kwargs=dict(skip_tests=True)) + mgrp, = mesh.groups + el_ind_pos, el_ind_neg, el_ind_zero = check(mesh.vertices, mgrp.vertex_indices) + assert len(el_ind_pos) == 1 + assert len(el_ind_neg) == 1 + assert len(el_ind_zero) == 0 + # }}} diff --git a/test/testmesh.msh b/test/testmesh.msh new file mode 100644 index 000000000..07de5a57b --- /dev/null +++ b/test/testmesh.msh @@ -0,0 +1,15 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +4 +1 0 0 0 +2 1 0 0 +3 0 1 0 +4 1 1 0 +$EndNodes +$Elements +2 +1 2 2 3 1 1 2 3 +2 2 2 3 1 2 3 4 +$EndElements From a2d1cfbbdeab7e8a0905407acc771a95ac128d15 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Wed, 30 Mar 2022 17:33:15 -0400 Subject: [PATCH 134/154] Revert "add types and some docs to meshmode.mesh.visualization" This reverts commit 48abe4bedf5e54e57fca392756423b6f45abff9a. --- meshmode/mesh/visualization.py | 74 +++++++++------------------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/meshmode/mesh/visualization.py b/meshmode/mesh/visualization.py index 8a2854fb2..05b65b8bb 100644 --- a/meshmode/mesh/visualization.py +++ b/meshmode/mesh/visualization.py @@ -20,14 +20,8 @@ THE SOFTWARE. """ -from typing import Any, Dict, Optional - import numpy as np -from arraycontext import ArrayContext -from meshmode.mesh import Mesh - - __doc__ = """ .. autofunction:: draw_2d_mesh .. autofunction:: draw_curve @@ -42,20 +36,9 @@ # {{{ draw_2d_mesh -def draw_2d_mesh( - mesh: Mesh, *, - draw_vertex_numbers: bool = True, - draw_element_numbers: bool = True, - draw_nodal_adjacency: bool = False, - draw_face_numbers: bool = False, - set_bounding_box: bool = False, **kwargs: Any) -> None: - """Draw the mesh and its connectivity using ``matplotlib``. - - :arg set_bounding_box: if *True*, the plot limits are set to the mesh - bounding box. This can help if some of the actors are not visible. - :arg kwargs: additional arguments passed to ``PathPatch`` when drawing - the mesh group elements. - """ +def draw_2d_mesh(mesh, draw_vertex_numbers=True, draw_element_numbers=True, + draw_nodal_adjacency=False, draw_face_numbers=False, + set_bounding_box=False, **kwargs): assert mesh.ambient_dim == 2 import matplotlib.pyplot as pt @@ -96,13 +79,13 @@ def draw_2d_mesh( pt.text(centroid[0], centroid[1], el_label, fontsize=17, ha="center", va="center", - bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) + bbox=dict(facecolor="white", alpha=0.5, lw=0)) if draw_vertex_numbers: for ivert, vert in enumerate(mesh.vertices.T): pt.text(vert[0], vert[1], str(ivert), fontsize=15, ha="center", va="center", color="blue", - bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) + bbox=dict(facecolor="white", alpha=0.5, lw=0)) if draw_nodal_adjacency: def global_iel_to_group_and_iel(global_iel): @@ -154,7 +137,7 @@ def global_iel_to_group_and_iel(global_iel): pt.text(face_center[0], face_center[1], str(iface), fontsize=12, ha="center", va="center", color="purple", - bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) + bbox=dict(facecolor="white", alpha=0.5, lw=0)) if set_bounding_box: from meshmode.mesh.processing import find_bounding_box @@ -167,28 +150,13 @@ def global_iel_to_group_and_iel(global_iel): # {{{ draw_curve -def draw_curve( - mesh: Mesh, *, - el_bdry_style: str = "o", - el_bdry_kwargs: Optional[Dict[str, Any]] = None, - node_style: str = "x-", - node_kwargs: Optional[Dict[str, Any]] = None) -> None: - """Draw a curve mesh. - - :arg el_bdry_kwargs: passed to ``plot`` when drawing elements. - :arg node_kwargs: passed to ``plot`` when drawing group nodes. - """ - - if not (mesh.ambient_dim == 2 and mesh.dim == 1): - raise ValueError( - f"cannot draw a mesh of ambient dimension {mesh.ambient_dim} " - f"and dimension {mesh.dim}") - +def draw_curve(mesh, + el_bdry_style="o", el_bdry_kwargs=None, + node_style="x-", node_kwargs=None): import matplotlib.pyplot as plt if el_bdry_kwargs is None: el_bdry_kwargs = {} - if node_kwargs is None: node_kwargs = {} @@ -205,10 +173,9 @@ def draw_curve( # {{{ write_vtk_file -def write_vertex_vtk_file( - mesh: Mesh, file_name: str, *, - compressor: Optional[str] = None, - overwrite: bool = False) -> None: +def write_vertex_vtk_file(mesh, file_name, + compressor=None, + overwrite=False): from pyvisfile.vtk import ( UnstructuredGrid, DataArray, AppendedDataXMLGenerator, @@ -297,10 +264,10 @@ def write_vertex_vtk_file( # {{{ mesh_to_tikz -def mesh_to_tikz(mesh: Mesh) -> str: +def mesh_to_tikz(mesh): lines = [] - lines.append(r"\def\nelements{%s}" % mesh.nelements) + lines.append(r"\def\nelements{%s}" % (sum(grp.nelements for grp in mesh.groups))) lines.append(r"\def\nvertices{%s}" % mesh.nvertices) lines.append("") @@ -337,10 +304,9 @@ def mesh_to_tikz(mesh: Mesh) -> str: # {{{ visualize_mesh -def vtk_visualize_mesh( - actx: ArrayContext, mesh: Mesh, filename: str, *, - vtk_high_order: bool = True, - overwrite: bool = False) -> None: +def vtk_visualize_mesh(actx, mesh, filename, + vtk_high_order=True, + overwrite=False): order = vis_order = max(mgrp.order for mgrp in mesh.groups) if not vtk_high_order: vis_order = None @@ -364,7 +330,7 @@ def vtk_visualize_mesh( # {{{ write_stl_file -def write_stl_file(mesh: Mesh, stl_name: str, *, overwrite: bool = False) -> None: +def write_stl_file(mesh, stl_name, overwrite=False): """Writes a `STL `__ file from a triangular mesh in 3D. Requires the `numpy-stl `__ package. @@ -408,11 +374,11 @@ def write_stl_file(mesh: Mesh, stl_name: str, *, overwrite: bool = False) -> Non # {{{ visualize_mesh_vertex_resampling_error def visualize_mesh_vertex_resampling_error( - actx: ArrayContext, mesh: Mesh, filename: str, *, - overwrite: bool = False) -> None: + actx, mesh, filename, overwrite=False): # {{{ comput resampling errors from meshmode.mesh import _mesh_group_node_vertex_error + from meshmode.dof_array import DOFArray error = DOFArray(actx, tuple([ actx.from_numpy( From 1d64425ea372e2e7f9145d56d8956e843d76bd07 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 00:26:28 -0400 Subject: [PATCH 135/154] remove binary file --- meshmode/core | Bin 9371648 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 meshmode/core diff --git a/meshmode/core b/meshmode/core deleted file mode 100644 index c7cd11bb80d3e8cd2611deeed649ff6eba0369f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9371648 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 `x8orob)f1#Hkd1)oPy+=$)qc0$FvA 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

l!ymDPA9_wz|ZPrEzh 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%0AUvpg;oNPZN3) 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

$3&IGu}t2xLgW`)U1xjfd!`61}Fn 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%0AUvAVUJ)PXl@q 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 z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009F3SpuiO_kd44`{kQkTe9EIAFt9IrZr*D zffxLyTH8uRcKu*e{VK| z#Ktf8qy6s2YfK6cx^;5Bzti8}^ZP5-@U&ZQhT{yYu?+m zxBK}``P|6!k7(K-`pCR3wv*EvpV}O^qi+0Jy*~R~m&QT%PrFyF*{LXohkJ_d{$!85 z-+BAFpBqzubNhLH4s-oE?`e*AxBt3roAI~Rxom)+2~T#~}2 z%NDF&x_nu2NZxP?bG^N<+Tml>`I5J@{x6?bZ)(k}oBX-&Y1*5S!rcBJJN|RK_oe!| z8=a3VtfLp*+=RIw-Tt}!RX56kkZ;yIhcJFU)Z{5Jp>-FcQ zdh3b@>hwLIo>}MIkhYu0cPduQP5HNvY04iosc9##@20%S>-cyDuja({~Ck<$;R{m34NKQ+0G>fhj~c#ihHBp;!i^WPhe+WMGk%WIDN z{mxgOwa-JpjqB*^QaEEtlkV>IQP5<*ueoZ@ z{d2#0>9to~HK%(!e|9>L=iINUrK>svmx<$cxs ze|bjzc|89oDSW62ySuir-F#2V`9taZP)`5JVfuXSeDtiQYD!n%O}1Fl=kcY_f9fxO zdD?|HY_U#)!2giIhHIPWfE(VkxPE^5mXn%}Z2DMpevO2qo>h$QYUe{uxl#AoqgRXn zKPJ%m_?Fk@|HpR!Pu|_FcHi>(!k^qgcZK!Md0ccl&6*SxIqWVkP2fKuaL7RCkb(B$ zrjxIncSd=dgjHo>z|>=*%2w&m3Ru7umN&nZJoLGpaIC`HG%H z2FlBOqu`}uW)4idc&j-B(-#a(J9J=Z=0MprUJOOi2czCQe^1-9qs-)XZ2xP+!;`Cr zcTx9sRohqg9x^a=#h60|rd__(%z^2bj-5Gh(8Ya646JYU+%#_Bpva2NMv*AmZ0?Bh z2SxSy(bhx%G(7yA>PGId`gz#Eik>y&2Bys%)zib9ef*oK`{+0Rw0+zqHuH_BSKK~4 z{Hh2Vzazr$q7lC;dv^T%I?64Ha`nUO&HH)y$bKF+YCqStde;7){hS>y5~qH1c=%IW z?q}>(Ivz*FetxEFKWB|Ej){I;5_Q+zIXt|w`tX0Oer)zWEWCSFQK7G>U9Z9mGK74hnLUfL65`r_W|pdLGJpp2Y2FOC>rOpnzYqwFr<8y-G19f!-S zvX}P8akzNQ5d#}qJ*SMT2U=8$^X=&IRh==hqcgrgJbYsEN_FB~x_Hd-0~=dC*Nm=n z*!VK)929lV{paD~p@=tQ?70Kgu&c(^NwIE4tXq`&kj|faT+Q0@{5f%8e$VVUnuq?* z=@a_ZN6oru>y3904<8@#YMC5AABnt8k(b|1Rr%F+zY_WPMgDWEjK@0Hj*DwT*ASdJ ze(sj%UTUk17hAuMwoXrNU0GcNuIP<@xV*Z)T)I_^p^L|!+q5?7oIkQ|?eKgNsQbGp z+UndhJbYKxR$696_|nL~GxGOHPLI8fqXuU5Tsp4yiK95OsiVf%`@A9A`1}ushu^WK z_2#j@`P?~TU`?xM-eaCRv&J72{h2VaD5l*zJbd2cwYB-YIl5}=#K&x_J`Ia%|4xtg zX8&;WSbW_5>m7(Qu)2PZ-oZo0SI2Z+v~kjX|2g~j(`f6KAN}X-U)8sR2jcksczF2L zTV7{2Uq5G$x_&;cSACg@)+_`F5FkL{|6YN$Pky9+{x>&;>vn6>)h$J}oR%g;#7Aen zEKKRRjj5OWwrRrnsIHf(&2+b7hm^kUFNf;bl$36#aK{vGOzWqnbbee>zqF6M2e)qe z^YCwnmsdsBZquame5Lt2Ht95vr|4K=S+Tg8 zUpC{@2i55r%bR%^orzDc(?eh0yAC&AvsWFi+0djrzu2`-&wZ$=x9%UB_8v^-X6%&a za~#@)n;ve;ZFo+T9!g>7d(C`@6<=uTZ%pm4cu(rjE=@nyPJ4De|D$tl(~sh;H1F&? z&3un_UrPO&*X+-xvvSy!o15mT4Q*`7ZJN>aWBOa0avO8_>Zbkaw>Irge|zKAxo69H z8S`J?jLYird$)0wuEs?*=6~?Xm!Djviw({5*J+2Zsgnn#bTRmnI(@^BAE?jsH9O`r zrJ85@*lpJp{)k`y(fWVHCKMr13*_tM>~z7)VVQ<=^?6X86Ztxvue;UrlIoJ4>(5Ug z3^~l74^gCE^7S^S4~j^P^X0kzv~+#W;VVzi^a(H;Gudekuov;5ntlF=Zs{d7) z-2W=CTB_G_)n`MlpFbaRxTY!;OZWaaxka%sB30jV{Uwu{H96eSBg^`!9RS_-n{X!1&=W4aCTC!u4DvFg68C}1AKvS(u zAJA*_j-?9u{vn5d*;Rk-c1^(?=KiMYBmK|$mv+@(m+I&6#dC7aCd}K-xtDj<&-X_; z%=ZboeE0remFn-43QSwnL~_{Mwg0)_c^tnb)z8JAdS+8UhsWdJE+SQte7}|N2XCtmOf22yb2^8gjxyDd&GqyB;>J|J+dJ3G;Xg#( z>JpLb=lhmTseV2&s`IB>a=qN{D!6t4b^M#P&ARIM596n+i}lKp`gLmaukP_Py6>Ay#q;L0{YOS;M*Ok1)m222009C7 z{s#r7$Gf)oSKE(wOg%3fH_(oE!__mKx$&Uo^RceJyw4u!oD{1L8{e~_df8ar>#m6P z`^7u7?^ks(RT;m4a7F!#gF_VT=d``+KAbHA6Q`uTZ(-d=w1e@%j`-ZfPn z%Fg+{|C3UC`Mv+{O}GEseviEOZ=F$Z^U-vCHhySvonF7X`MXG$eze)|{NDfil)fp2 zUr1s1Jb*`1{>Ev|d+_aUYTDT?g+tG7-lxw>=~Gge=LcMn(wC<2>J-j+a`RsQrj*X} z1MW!aJWt@kbDQ_{V{dM@yIl%rr1=2{r1Z%AgftJ}%#?pY3NKAzo)2(+O5c>iFQo8} z6y|vXkEC>-FR)!Y-@4}o@GZr_-VMMpu45A8zl; z?|%2+ozDHq+t2AgwEb^)Z6|O4dtLeYI&e={em?*IwJSeg2OjFm@9uYZI`=>KJBN?w z_wQZp=l+iwyXA4n{U6trpZmXcSAKWDa=HquCHv-ZsK41>Ik5@fG1!Esru@rNxG{x! zeL1cvH$R0pq;UVPb{EI_%l-ae6YjZq$-FaH zE;w&-vFFldOIOcZuyW;s7Z-b;8HpTU^x|a;&Re>$*z>IAtM^=S?&@OCMGIChDE2&K z)vCx_cK*CYi_bX!taFzxJLf;HQhm-hvsoC?5e*a-5=&~L*B6{S1tMddk&XH-stU5PX{)KxxaPg_?6Q+?4E}* zBh}C0smJ7+O_;ZvbGzq}jnGaDV)Rh9z;$dMLunBAlFdrS*~%iq=J))|3f-?X__c*d_9- zXE>W47#`kKT@(g7vj)neCXK(ay{351j?dhE-(8+mRjukD7C+bgY{ z@Y3o!o7*`0%HAu+T)x%CV|&N+G)3#aekF>$C~j6~L^x9R?9FZcG_qeG>z_8dtvRpj zy>iSITU|c(;=To=+A5>ZS3NpBTsahTtA7g~eV-fMj%3yE>hA+z{W$A~Vtq9~@GY@^ zhwgp*B%Cnuw3|H~yJ+jT2ebw%0|OpG29Bqsts|dGEzzdR~$XR{g2I z54_>uhKK9hf$TJEbARuP^)HL{^?hJoKXdc?c9<-Boch(KEd&SPp|!mHv5Gx+gto%J26lr1TS-W{c@5Jt3tZtRC*g zGO-DY%d0OFvE=vtmpyH$PCwrJ{ec@B42_9{GE1ey^XOb00LP*a&*&Pe&0XR&#Lf$!7_hdv%TE(imr6gb4I-? z&jYw79k1eji|hRS{y+cw|1}$$_vmL$O#eSd`a1yA(|nR&Z}?&J@BcS8>#v^MZ2$Tc z-ju@WpKIE=Bc(4(|DVXvCz|}R$2ax1OW|%QT$8plE2U3K;h8DCAcZ@p|Ig;?l)gOm zYyOv;es$9J?nvwJO=0n(=hovTdksxH@#GT^A3A2~vhy!`%Fuy(?!V`jPgD<%g3XX> zMo2zi22%t1e97ZIx0}PK*7cthznl8sP8C0#2Usqc!e)C{&CTU|I?alFzN}3rT26OW zFRJ%-slRz>)a@RV6}>aQ5joQiikvZH zwpzMubKZ z&W$~8T@@W0+a9~%D^>K7*mhsvH>>C~?g`jCs~u^-@_9W)r+@D&qqY9q?u}pjR?(vW ztoQ?r{rgq%b}_cSwA$F}mqq^mRkZ!)U3-dY6#bWfKH`h2_?TjRyZ?pNKQ*54m9wJH^Xq8q z`dGiX>R&YqKJ=7my4`>F>XBy79o5WvBh8%khN_u$F?9Nu*3B&ZY&5g1{#%IcMNxZt z|LONdk%{%wxrtSC?fy$1jWR2$>`woac8eaosEU{U=N%C7l~vnA{STiM@l{oPm;Nsw z7|pM)%1kQ;s;oyNI;lDsF*ep7*;9<4QYFSu8-M82;>tYXBd4{B@rS=XEGE4CrxBf1 z=U&u~=n>UODh8^t^QF%pxM~f4xO$X1*#1@3=AAL5dj>!7gR0HXKA{zD zu6je96@xFnGlu%$#bMVsc;avznZdpG3F*PFht#;iy#99!|NYB*iaRIn{YYHT`tSc* z)ZMok?f)L9+{6Q77poEenTQ`)#XJ4G9?&W#9#qB4{yp!G_|vQSQ2%lN9%Y_U#i#ZE zpMQw>GpqRY{)@gI@q?@QLH)0IT9kiQ6`#?+-De|zMirmkf6DrZ&#dBe`*(g*lsTk| zpW1)x7b1Sx#IZ#&zkl-CJ;lUX+eCax|K4AW{Ml7MSM(qEaMV4rim&c})J+k; zsQ>@O$elQ+y8Nx_Uv+tuId)FOU)le{!y^B<9ip9!`?ra+d*bm=iTGvx|5nvK?bax> zwtr~w5_{A^qFy{fMEv*>i+o2v?~Z^y=_R4WIf zkHx1VQunD-}<`hjGcUGRp9!%z@Als57q^q zR~7hBRbad655;ibsb_p&)zzx`$tS+0Rcy0M9lhjLJ;gTr)Kxwi*Z*w}tfO!FWT-u} zif-SWIy)Zxo$xQ;7Tdb7w|vXaJ;jfD%QHV!6hH1QU;Em4V&7Y?je+{p-g3j%Me(n_ z@{3ouiidj3Grm?7 z5BHXXUyT#EuiQ4y&i=mg#3(tguUs1+7UTQMIY;#r=k}H3Kh`QX_Lb8*J;m4h%AcOv zD!$%V-u}yY_1srp8YANyedR@m#osCJE3cYW6nFHM)1TB+e6z3o;$^Mk&c5MLJ$XRG*jUwQOktN2b|`TaPv?&>QaJv?5{^p$rU)Kh$~ue>t;&HwlN%1d6`DmL|% z=N#5k{B!gyu1TA$7VqK_q@L-e%e>QWJX~1l{;M9D(>$qSHydx2l~pF zK0C1b%Kc)d#LxQ5+dddredWS9t$)#1-XB|dsIUB4Tu2{|?oMhIkMx!6?!2t0_;p|TmP=a2Z=%i5wTj>NmEVm2`fRwb z{Bi7G(JoI~5+54va_ry66|r63eqvk_+vTUuiECoJ{F^y(QEZnd@7EJUsJ#2vF>c%C z(}SnoF5h`}T*=zyXJdSfYnQj)7atex@^#0=wXt39*b^skyIlMARx!{nUw>(w7VUEC zm*V=^E>DQjwROAv+YlUVm*2fGhGcZ&f*A7c^4Pdx*tT6hIj-;9wae%2+bXtim;ZQp zt0>#$kAi21cDYCNWyf||?$Iiyw9D;cFidTi?YP`Np*Lzm zEgE*)HF7cu%ocyBrSk z>Fu&VE&}^Tn{is~*Dn9R1x2xcyPOyY{D5|OS`ZxAF0XrIT&3IP{g)NR)1&&1Me&Sw z*@?b9vt7RC{P;Vt?Q-_=_&9EtADGcoJiA?9IMh=-r(M3}C9PscyL{WZJ;ls+x%&F} z2g(5ww+n~1%b)DqQydnDaqpryyj`9hXTYp>`K9SS#S!817p-D;yZmk3?;P1K-xGJ0 zN5vim`O)q2$XItwyZqw9qL|Yzm&Rw?vF-8=$F_>&+U2HmC~h5JF8yVz`0V)d<2S@*e^U8_U$=_uCY7JK zrd9myr1B>x7sWd!m6P7vD&9G%e0SXaylYbVg*WyT*H0?H9G}hao>YGSUt7fulgihA zx>dYqQaLu}_`i2jdFb9f#rr0e-&)!#-XE>Z?kPSnsXTc@QG9Sx`OHYAe@@+9D{%%tFhJSArH%7fU(jS>r?z~g0_~@kaAGa-vnczD zi(;oImG6p^~R&dQ$n}nMLu5N#*RTdx}p^Dlh(vp5jxJ%4_1P@##tB)$!-c zKQpQP(J66H-YI|n#JF$ilrKCnuE?Eo;$5xcvTe#wuPKUGZ&MzAYf-#roAP}zykEOb z`L;1darrjo>M1?N72A|MZ)g=OwlC+z9=&M$^1is8Sh;=q*B2MXs_o0&;{vgI`|{>E zQ_tVN{Qj6$al!WG`k%Fm3%4&HJ-Jm}w0*fvT&Pc-QvUOu@z)ZkloZfza=$p5Q_ANpilaHDyz#uA;~uQwYmXmUwM06VYd6b z?FQn?H2dPuw?7w|d0DVx>9RAI@42uzeO$47@%&v^y`b22)iZ{cF1ujCxl0!ftz59| zoS_JJT~%i={-e{O)eBZNYZv9URnDqdcmA@GtcB+->dINRe9_{r ztQCt_KCQ}bnp_z5SC4G5-pY}Ob=9a!pSyI`>XD7@)$B^$lS7xEf9|4XPhLH=aOL6! zs}~O~S-xs@Rk&ETbkVMHP>Q-#-SDo9hUyJR`0Qs5Eo*jQ*G119T6NCS6@hbBBUPb{ zw7Pu7;$=h4=7!E(dhX((YVEG)<#tEPEnKqv!ey~?X!Y`;^DD(gLub`NF5UE_uD$9R zyDp0CXlT`fGZ)XhaOKkI_q3?7{LHBLl%X>h#D=O8^=9h+|55c;ELgdE>4I~I>IR$b zjr4ETi#i>GbAm^oHoc3cub%^VOv1sYa z#S2$2U-{x%FpR3cA6lKR`ZhWs@&);*`^NYDNk2!JP%(+J% z*2GSjdF;_Se(Z6F9iHPS&OPq9W9A)p>StvL7mvz9K4Tg558&pbWq#WlXT=zGJ%7gb^W z-wf*_JR|EV=)?|k*r zAK#9vWkM4aw`|iy;xSdd{Cck@%zn<^@BP=lp-FF**I(PD<3VmyKCQ3rpQ`1A)ZhBEJ<{3t?sq)Ss+as)Hg7M#_rEPc<@f%1yZOEUA#ZKk`|o=1 zKmG5U?WNr-Zur`&+GAY`bAI>x|Mls(6<0U6!ukFGz3*t+dnAQp-`S+MOW|%QJRpU$ zQg})V&rIP3DZDg=SEum$6yB7=FQo8}6yBS{M^ZTUU1|F%+%1I%q;OUWPf6jKDZC(s zm!|OQ6keagn^O3N6yA}-dsFyG3ddfbwx7b?Qg}cLXQl9z6rP#F3sQJ#3a?J#^(nk5 zg-|KHK zHWF*fFQ1Xi-yRPnt7X@GqBhBq-%bCX@zz}aAJTOtpXc>|mKUW*(jzsS$RD1saO8~Y zMRc{l?bbVf5ibs3v!E#UjkhG-yV4Bk*xgophj{uI^A&dgWCUaSj``~d+I^pjSCBC! z;EKPGr(@I4jz;r*g%3R=9`2v{uE^P9zQWVuS^Ah}zQUJ=Bbw(c{LAa2_~MU7PBUL& z!`2aP<}2L3AsT7sD?D{-+rd^e&EmHgg*$RQ-#0W$_|3BJmF6YwoT^|p+6GW`qIRU7(xyb(xK zBs#~R7x6SjVlbvid?pHPvr9Z(?w?;r?~NzS{fn#qRRgcHA>KpA6p0^>G}BCxXqrh= zB%+zK#>S`HHmAjC?O$3qvv0gG=wJ4Qs+ma#{3>3f^q>CxC^EU~PcgY_uHAWhycOtQ zQDt{J9}buP7gh1HbMik#yqO}=x%=*jH&Y}!FWWPkUtN`%R!oSOcf~moO$W4hBd%&B*;ZsgcHw{GOPhoeAIH&Vswp43w$ejeGqZyi%jk@!Tc*mf{3aogP; znK2;xBHq&(voiLj_nRA{dNVoavoYW&9Jelt^nN+c*opOYpGlX+FuHSK@AqsKMEkxY z#%eRgp!40RIM7Tn=)C`p5pSj#bozf8@n(uaXWzeyVctwJ=$!qf$Zw_?bdHJ(-as?O zpmTQBW;4a0^SQtnXr>r+9=bZ>%@l*q+Sf$9nPSlS^{i;KnPSlS+OmirwjsJUzw>WD zkNB)FM0`o7e|f~4DF&Szv! zxUWPz7k9oNXUD+tcSZcNPQ3Fh22Q&*bk=sB9jE@l^M4rm*L1GjC*r67JmTv*%Ytp- z1yvtz=o}ySLIcedgU*j4XJB6SK;)Lrl(`XarWkbo?x={L@o21#DF!RQ72~vTMjV^% z5B^N#?@_M#NOiP65v#|xtNi*fSG}nZbCvs%NR-cv-i&SET}59Tof+Hy*NE0-tJw*4 z*=>q}i(VNA@YU7yhq3MIU=G~%thji*rpg$&_+Mk)YwPHT<3!%NKJh2SX!c0zcSH|N6=qG5G6ODU&XV z_d9DQf2Nv|(D~HM;^pkEuZU>Txp8Tnke{uxdpc_`iTuyaitLM;o%m{G*E?~3G1%Gm zj$cM{oAwQ@V%t0G=(U3p{Z?PxnQpTwuCCjDd;97pbwV|}qROmmPAVo;^Czk`b-J&Z za89J3oYTF8mug1!MsiHex z^0_#tcIr7R;>}!$fv+DOFPH1N5Ci|z?kTpf=Ryp;Ffg{S=RyoTa8-1po(s|YuWyVu z(1%^j*G2>7)I|Zf!+Uyu5LK?3x_z9DJG{52+S_$gpZCK^ zysvKHol|d%XRJHCzvsHa7_3h?bMc~LhxI*gip2V<`^6c#!-wlSo2G7xQ(%X`>#43# z_dRikTO)B}-E^n*#GlOUDR%fsohn;Te8EFe>!Wq8q1F?x`~TT{6X>Xl?O*tGpQQVA zPN&le89Jmx8VDF<3Uin=0TP-yToF+SNf^Q)Lxg~!Akjc1K(2wS2#!G!0jG$9fC6C< zoW%)2P@@6{aR`W9R3zVTSM3TR_rm?J_3m5meczX}R`+l3UAuPe8cv;|>Qv#{5WU6f zfx!ezql+Jd+bR-Hv^09J0Ngf_sU%CIOM$StU06xRoxz|kD3t-*k$KNl+-&J_cYx1% zhxm^>ShTpO7NcIp1T4=*>mi(D=G5Mpdu0Zp?yY8P)RS(mz-VxQxdeARq2^C`8f{^2kWAHjm4{~gWeM`{fGng7+4Vs+- zP7lg`+M>0$1WkVlsak{bQD5zK^F7p_#e8KU-CFq2mfWTTC#lcegwruGXzmB7K}%4- zwy0ujkpFRj9&{YnXw9ejHocBwX|8{Wa*6AgT~j2T^}Eni%#|OI;r`9Zu!*V%c7*r1jvN7A@J#--r`4X!#=|#rT5&YtZ%m0Rcg8mm@_Z$c+ls zd_fCwMbVNQ?-~Y9L;0*}g-{LUm51&W>RRLY^F+We-{N8L@fQ7CR8rhq8)|%iY6RSg%cjksv(RRcvdtKU z;3kVtpx5y%vOi9SCt!||r_fHpn)himHP8Km3Gj|B0kc>zNF}{7xFY*R#IaheEm0#) z6Fi-bKgq^xixAeuusX>`!ay)df-$@!F>lAyRWNSvvMMm$1mp2;S_ZZ5g7Inb6tx-r zBXhplA40E(Q3l#(>_Yw64C*iVCOKwEE|krCG3P-ux1ugB<~{qUYC7~pv%^#j+-;j9 zFo7e`+&%&`+RS(HZ#R$O-!Ol|zr)g1esAN~_$xK4cwX!s)BiD_>kQK-+G+B+D@4u1`Wm?rAO{VDGAynQLE;ogXw zGOwTHZg>Bs(CJ5XxVyLkrU3P)WIdYR=Tig>xCDFE&9=LJ+QbONJyn928`aG=5g$o> zzTh_!kB&<_l`HtCh^N;fr?Le92=Vlq=v2DkR}fFHg-#_4ehKk0#3u^=KH_mb(oXpV zKO@Aa<)978!(ZI<4k+C0FAkgp^mo1BFFKJI?Oz&rJ@ovG%JYIVsw;BZgv2cr5i<7X zTxLSzG{kR5@rpZLFU+*Kg!um5mvnzom4A2Tjeg61|3{bdJTQ31B@H^s;MMHq_xuLw zYO~t<3wjANqWk*JSYw<#`JVe+eVSLQ%AG29Dlors$75q@)rfpp`0HG%m7fi@5GPt zc>*VN;x3!9vHsG{Q=$7Wddw84dMfwH4kNLaDE{BDgH-abuG28AhioG8M zDrPZ3vG?%6qF*YVA{)BDc>dwalA6?t$5)O_8I_Weu{Wh)pJ&k661qdEDkOU(6kic&Nfl7^asydzZo%#ucSJPbm5mzNZv@ zR@qxbNPWvc_D_HJo?iz0w2=GPk-?>bhr#%lIxqMay`5+Ji$1KpMdYa^{qZdKlCX&2@f8CW5Tb>seh6isfX$IK%=gT9P}c%awgeWI_R*!I3gar2-}6M zam088SN?(lVU)~Um9t5?c>cl4sggJ>^H;Dhfs08ruT!_H+})BgT;*^?zoNh3dj}TJ-#V~()_Q+&YAJHF_;k#`rQ_H8m(BwB zb>#+lIS~0qqx=f`1B-qiSacSFi>ZF!K;XjiSs%hgd2JIgRR&Y}r+Wx9#XW!2Rn}Ki zu8UIN5@{6erzFxYCvi8qL}fDOinRPo=jT^GDHD64-_leAT}jA7+}NOIa*aMqlW?M} zKoVr_0$$pZw-o|Y7Y|x=J}-69(l&z@{gl^p(Aerhi>mMGG^pqtY%Z>(#W)5Q{Y?5L zAp7A&$ny+Z+A5y3Lk1RA3~Ygp=Yl@4>xFqAz_C@^aKC|Cj%XoL`>L`(V&UBF>tA#< z&!FU6P_o7I%PLDLqs8+}D<39eY3eF8(Gj9TQ%0v`X6!{aJk%p5au!Y{p?={{ql8 zoknGl4;`b4p6Mv4aLU!vzy}>Z0NgqnvT&aeb@wl-4p~Ua4=kQ~_;e$x_5Eb5V6PFf zu(X!WgP^ygvx3W=FxgtG{2ERnf1fF#i=1BCz4CyR??5j#4oO2DbLd3K3o_(s(nPEPZGN>OODZHS{SVKM*_nZunm63~})9B67 zL!uLw^YF$_nUsmCG8|lJ+D@K1eVR6N%B&ev+g{de zD>b!T%xcS=Icv<+NqICW2)UN?eP&I|9)n3~m~=K}diFS9|C@#k_Fc9qRqysSnLK^W zq;XBUHJjC{$;`a*xr&~in>T6tw3!H)*{aF-X|su#FKp9a=1k188lNc^yrlIpNmhH# z%)fd`8kY}*)08>n*C%tKX3)&c#Z*HirDe~UFk7oNrAd~7ocW{Ol#cmAnOY`K1QOhk z^kMym49=8k&@`pX`LD^fD1@3qy6ld2+22em>r7<0Rw(x2yj(vb9Mjn{xrD@=pLA&B z@jx4f`7Kj`9nmbPT{l8qKM#BnNGFsPjq4mR7#NNgaR7*SIodNg)yDzfL`ROt>Hj#- zrvh_`Dxmit*JHvQBf7?@!v|s%ev{!}2?^EQnpCXEG)7L>@j;xA-y!G>174xi|MFTv zYY}8azHo1OS%>G{#6v{=z!}(}-nPaD&*8D&D$^ppAMq=I9XuWVtKNR8*9t*YUR_aM zK6D2)_G+!$q;I=CFDL;jeW2e0R|7nsUDm(6W>nA1+6b>d>Pz7N1tpMmW~Lb5^)XV@ zrX!Qi2^@-_REM8ahhNOROGeXHFz;ku?F)-XPH`-xCqW$az(>c{Sf4<8z@tO;G4udP zM>&Vn10NkKJV3Hq)i*L9YF=Ts}=^nX0!0=yWzH=Bu&r3czTokhl7hlitQURZTjwo{I{n39SXca}#^=F{4 zT9C((b^Q@aMQ@9tUA>f~v{b-vK=?TTe%guFW8hDK7(vosLh1oAe<$hn$O5k#uphqj zssR(=X<)+wDA-%0K=r5LTwqKoMR?4Q+axiDZd`{i@DAJCy@5vHXmBsm=|FyHj-%+l z?VW!CX*k?{L3bhA?Kl8i_9UV`j+aKkc2}Z(j**Qa??!ZjBOPz>?cIq^bhP~xIz5O^ za;!(wwD%-B*>Re5l8N>^-tPuFh3It0I*bh1dl5at@z@aP_xS{_WjWT|3LnyrfS%|` z#n8OnPk!b)-VcVnAJKV^xo<+h|8CH89P2SsW9P4&$Kh?OoxgJ4xC-=;uVJUa(GG9D z?L%pP+G59iihJvOh+E?5j;ZbTk$*tG$}xW*FtK*+t5I2kbDR*deZDu z1MiTRkifMHuU8#Q1!-H2sRRwlQ)goYjm0lSvyULHTUWu$7?QXGNvjcQxkivQj->h# zG-)$=3#58KG<0*wb3}fHc5G&UPGmc@W2gOFkj4vxFfbC_?hC|o%*Xf0!5xT13tNG4 z@Qy^&M2gL1b^$&z3+_au@!Fl3Na2bcfhOZfL_N7YR_b{fBhnaSlfW_Ya$VTLDCz|4fN$;>CHkl;ze z%z~~Ur#(q#jM5gEND(}$BYMaNX%o&bXF(4J3OJcXfiwEA|JBao;Li0iMS`)w9 zP_l9&g~JmD{XEj)i4=W6iiU!Y=>3^iEksPD2u%p-Jr^Ni9d3pw^)$*N?8s1j`I{!B z!_mS+`chXUTU(Jbf{7FlqQ?pG4}l~!F=W8b8Y6>5_JonaLMn_5`2Y&i2vNclF_9wW z1@v~e;EW7Oe-HuTOS6#0IO~s~ZDN{Hcri&D3yG-d(0J+^TrJNc<@TagCSg23w2L6i z5HGa5Aa8Go@xs(YX4;rWIaDX`JNS%NU7S)(Lo#7uT1GA7xG8;h>^9~DAIR*-oib!~pghv*mH#sAO^dIpj3HxlvnQp^v zq0ONG2si#r+hcV43nBeSG{c`U`Vev!`SVoJj)|i%pk5{Y>l%yCwa=KibF?}h!F_q; z*|WH-3~+RL7COJJh1}5WiHK!0=s#ix{){Y~*xty$-U00}=UBAJYGEw$a9c#em=;-! z{N`>j77JZ)0wQfV^CPX6T(TEgbry^+LOhy%zA#Sz5g+2ujDG+b{Xz)oKjJtE?>!9R zFG5KF5xONwGAip7UtRT-5IZcqXCPTzrLHdYXC`c;UhzA5o#S-y|AipN^89|;SvP_T; z{71Yl$VNo&7i0$_KN6&$$Yc1^_fJJpHKH;!9!2d0M8;9!I{uD^9NGALXi)nthR&V1 z9l%gv75|b*$&~af>1W;#M`HM z^u@2>#EW{8G+gB)Fkz!qoUifMVMfOq>TFz_NnxE(*h3003WesR@DeG6FMI&fFbj$G z9-Gk-_M&rAAA*awCoLfefv$LbK2H-p67VnaiBDUb(1T-S&xbK3Vy%^OZbTNI z1M`e^BYHk}WS0;y&sxJ^z!RCe3G**DSjqWSk+;Rez(y;TPHE&m*M8h*4B zb)+V8v~^-&chxuyJIBx}9oCpna&Q@l`KX%iA16GDDM>MV7GNGm%)n@?)-EO~g=QD+ zz$K?ijOPfZRCq`ArHLHg7Yb-1hxh(cG&$WnY93AG@cQouSiO%wgZT=&cga+m$l?9y zC{5(>-qedGa(E}lp%q%Zd-?#Z-drRSrh6wMl_nPNo9k&Jhj-tbG?Bx5^n04f;r;4G zD<*PyZ-0v>a>V3#aO)ql4n3+h=Ec@nqa~)>O;*hplZI<(qIYvED}ueBP<>jx8&gmK zx_2JEaQ0~pZ|+E4#f@M*o%I7$3{LoIiHTZ@+y9s*e%wpPT!52C%)lGa0=z@hpz4l! zcMW34So2VpF&5m%*)-exp;#PB}t1Ngkd0wGL}wWD1_Rg3LX z4lynqv9UK05un9x|DJ+y_BNUUIVNvV#Weh`drehM)M@FQn4P#gw8R|6_1qfsL={z% zhIU}Ye7FoJW6b_}7C6@eE+ofH+>4ndTB8B^$lxsf6PRnXaXL(g1-5}9o6!;f=A=%T zA~Jn3;$mh-HYAaqaJk3KjGxGz$O%s%^2>qL;tZ|PMKo^A%;*ZWxNduK|L%D;@NJ6I zbs_>b3pFirhYQmkwgh618_uCU#JnaLcjSd-P}?fhJdy3P!E6&DzR1VcfY}~Mr&@y6 zs0tPCc|C9{rL*@&0#Thri7DHscqJssxgD%lH}ZNF2{Ng)x`$ zPR99+Dn9Pk#qs9*GCrPH#Icom+?k8x81s1bD2__z)eZVN=JCuej!VqrRfjl&K9Kgs zdkRP*nHNt+;NzLcbGA4VnQzK`Pv-GBBaQ*g<5@u*BbawFKc4vz=4UYPWJteRFu3|^j z`JmEM;EcUWw^{D%R%v#n={3@_Ij45-y;1| z;R@CGeg%*F=j&J6QF^66`2<-}K2D$-|3BoHc26?q^1A=Feo`LE*qpI5V;bXd#`JSZ z!r$4<7c#D7T*tVDaX;fB#*>WYH)abvw*E4GAM?$b@61>=rKQlz-5}G=VR>SFS0OKC zKAHJ-%x_`LFK;LG4l(b}l=jXuZyUh*XRMeY!#gv7Fje|FocSEa#K)yxA@eI4*D-Ek zRP~~;lfio0cu3E>(t7^?wi3AgzvK}0r_=}5 z^dpL3jIWc9>d%$92?*-LRkQ>q6uP;CSMgN;s(89SVyY=#h{_|0J7k44wn~T6SMjBk zUQ^=jDSj&c1-?KitmGRCa)YjgD!%H^G44Vm^)>M;_yVp_U3Z`&4s|_MJY6>F8lcke z)l!BiyoVzw|COG?RH)N6Me*voqOgP$Qe~|CXR$U61ld;c(_70Bh3Yz{(pT|S{zr5C zUNV}N+))w=)peBOQ=Gc-CqhP9RM%~FAE8j)Z$U*I$`8el1yMVGITa`lg`#q#Nc^k& zMTOHKy&^u|)YDPUsQi`E%l<3*od~EM{~*UxcmV?pwGWl9l3~2HI{Z*7nO@*t~4I3NqF)COJg8WzU=k1aq3NO}4unw!Ot`mRGJ{ezOS)B@01rF(Kx1e03kX>OZ8f;_pUWDnFHe z=?^||7((RuH334ZX+A~;AAq2?UlX72k4k@%8u&I<9NZ1ooIk1^%|K}F^h{xAP2zZHItpxW`ZDxXxYl2F3wOYUFWe+2?+$1gqO6RM?W1Zd?FYW{sSlq609 z$QPCWtg}85kabo7hD0T*f1e^4PXp_yh!7P&v5tP-_FLQc+Nvd-{vWl4p#NQ3byVuv z`Vy!wf%+1tFM)qk0ySU6@wbJw*R9UQw`Fq>Cmr8zl>)or+pMj?MR*BP0ek^S9q<=G z%>B_i;Tx-TU?DIU_&JcSSBdzRZ7Yxt^?vVa>sD7`9q9_X`S}m)RRG4T%{@f0t;-`1*#0lO#M+`0`(Ee_shCC-Ar}Bi-85(KkS%;)mDav+D5E>+lPhSFfYjFt6qh zWbuO<57TRNcpOE|A1H0@6AoN${=j+;zuNqPs!*9PHGg1n1IerRs_UaAuf`z`bGVv6 zpvI9*SqwGb;j8zSTJmRC%pXYN{HfzLodxS#P5 z<7(E+VtcmcoIYb|woIop^J$F38FLta4Up*-GQW~htwXSd`TdNC7*8^uXS7`>(_a%N z?YT#C`Ycal9L|`Z@|kF-s-7y_$oRiADtTyIDOdbWslI`erp=nu!q=rur#6@0s`r3` z96ORNSDnXd-jzC!`E*d`YOs}v-vs|Met&N0t<{NHqbUiQ-hx3&e+HjmN_`&p8;Y+J zU8C`0_E()ULWTO7KKNPkZ-s5O{=nLgu>QdJ7-RVe9|hHQZM=vGbM}A_ae8@r(7)dL zb_@1o*3ZtMJ(=|YjIaEl>+||quqU&A;04-~S>L>$_GH#83u#Yg{rQ9V9$eQS#22Jh zx?YM^0M6+8%NSQXtLshiCGxMj-YJ9jWY$;XbFJTWec0Q`yj>6a2R;C|>#pTkH{Pzt z;X5bOuCLmuVNYhg;WpZnS^wvDcy8Cn;Zv}W?Rs&H6?-!4qqkeIC$rx62HKNZpV!!m zJ(=~Jd*St&UElP*hIIn;k8){GW_{i#c-Lmvk1e7-ne_t;H0;T&zm6GbU)uE>E?TfB zvz~@=`mgPJ`EU3D&aOY%i}qyJn{}i;nf0#VzO(C5DDLBSeI52;Jz>{dwY6eTX1#a0 zhCP|}HHcn`JYZ+GQ+E9%K4ksDu0MymJ8jqhDnen|^@%;K*ppem3wi(9u1|%^FLph6 ziWPe@>v`yX&)D@gc0t@pU+wyQG_`Yfeed1alGLs@`WW*A?D}mBt=N-U@3Fv& zJ(>0UPST#tdMUnA`@^nxM5h0=>p#9^!Jf?eV7&GE%dX#w4|Ol0u!hl|%z6$~G(!&^ zjE}Yq{q7I&WtpLSzCv{y`cM7wg_)rj-cPGJ=%aq8@A!4wMofz^^vHDVKVazp?2D>0 z^rQGxHOSD{mte{PIK0Yp8v5?n@D-(@+wrZJ%h1R4#BijcXW+G_+t3H2>kc*an@8bm zIzunIKcmdg3v(Lqnf{W*ZH6=i@6uL$AO` ziZO8F4=mJb=tEo5p3M4AKfWU~^pAGn+bl!>_zrxXW$52_z;}Lz{yEN!CWh{K+KN4y z^^>cu*ppdbjW5ia8G2eS?a8cno=JN$>yM&ACm4FKF;?u!to#0hfF){M8~S(4XisMSk7*X{$*kWw!HPYZ^$u_v?M>`uIMM6xJ}PKG`M+ctML z^u9l9*ppfRsUPjhtnbOUVozrMwqz^zWY!N|uVGJS{q$hklUbiKiuPpIcedBCC$m1| zc?j|VuqU&=7+(&i82YlWE!dM;e{=-x$*gb0q8xn;y%D}POf~d3QGaQMKCsGy zJ(=~Y<+LZW9==e+p3HhFzW(cvYTTeC$ruLHxzG} z`nTxDc9{BKXl6T2{dU~t>@xLkyDiw0S%3N^+LKw2MUS$_)O{EcEe+9EJwbai>$g6D ztrJ7^+rOkenf0fGXisL{H52cXL-bGX)vzbC{$QL1dot_mZp7ECA^I#gvm@}>}d zA9}i1LiDE=Sg}vBJrWIiaRYr~ zHtor*4@HC}4fLbyE!dM;KY6_cdot_SFTtje4fN+Rfj2iwe;F$S+!3W$-fF>~%zA5_ z=rg1AKTy1RQTnCvSVl5RKh>4?WY*ikr8}ea(`~HSlUYBqkM?BN-^-vqne}EZE!dM; z{|QZaWQ@LHC0=>Q=$l{BuqU%VXdUgztnd90YhcCb>(QxZ#purs!4BIo`j*F#u^9b* zJ2DodPd|X#jL`?9xs}K09nsu&$LKS00_};>m*9*`j)O70XphrhL&w=GPR~Zu?;WQv z>VT80seTk6a<*uyzl{pGzN!8sge{xuPY=V@qN!ehtek18A4og}Jk zVozrMp`XN_%mD%R&%Q&ET8x1|p`)_et8b?M%HGTdu-Nulv6M_sJg#ER51~>6tbtFV zSOaffj1N;2zJ$aU5IAXCo)*{}BQDnK=@J(>a0=90wX#Fq2>ccW7kCye$J+Wh1*Ttz zm@9-{?++2>AA%f*=>XPM6eaMbN1(7;kSVc{J`zIGg?~Y}l%#gUFcy<-i|t)F=O8+8 zFp|_`8WTAY&IWkSQd+mb%Yf(rlBUAFfQFGE9h-?i2af_`IuQBS56IAM!np#JknOM_ zQ{kLFxgRO~4KCY<+(2Y+cx2C>Ok^QEGVHTJ2L06s-La!x7u=$l-B68=4n%6^UrVs$ zN=Kp%bn{`r!1vI`9G!>^IyxQo7ZkY}v^fa9M39Gas+oE4Iw+coCBQ8DmX>EBxnV9n z2HHzLMMXY(pWO0 znH#SIJx(NqON?XmZ%}aU!w1HWEJ1qE%{a!Bf7BovKaGU3OH3>tIi#5>waiSdF*BhD z6kOj;hM7sijHNxuX&1>%&`Nxwja_1vLnVa#(L%_aVgC6FbaF}THiOVrI_@CaV>Z4A z^bE4?Ge@Gp9W#lJH!UXUJkm+f+$359GL$L~bvXfxaF{W~1Y?&N+QPsUo(54WEw13w zaj#{j3F)B?$Vy*oRGOQb(Me_GI#d)cqo)6PNP@9TObnj$q>(`)d&07^4VXj)f%RUW)+F7(aRTd{D{=WDGq~?AS znIoa_D@1c|1WzG?w`*=XCB9RkhDP2?rDTNKpgXA~`hTVwU z3)cxEgTBPD;o*>W`A)>yo`_dH4BdW6=lhU?VU{4vAzg@eo1ZtwiZ4k-d(82; zsS4>zw9mW;y?01Aq7%%@OOSUbI?+sg4|ETrlgu<+8A5szooo)S2Axc_-yDQfAtZ(9 zbo2N3LH8nhg!$3{==6CQu4S1o_lFN@kAt3QKAjI8Klzz!{tX*1g!Cgi&up<1`u&H2 zo?`~$E-7RX)mXmy`Wv7Jp9X!OY2r>GWXRL7Q(z9p=^HZi70`>#))e>FOvEiQHynU` zqRFJfMLYU~8+}8zT92>s~ z%@GPPj0fPfqcO;^HFzEii=jX_)kI$q!Y9Oi6PLsWPj(@#9ycQ|rIgh$8c!~;TF26Y zE_9$n*4CoE8EvYoM_9o0a=HI*?d9?m>!~i^vVasqHF&AhXPLQ20NaEPniqE{kmN*6X|x&tU(dR!ex@!MBdXVe6r{4|;gC5I#IWKKz}^pg5qHD`8n+A6H_b zEya~oU{7~dC0qX)N}Pd`?JbJAziI95^7sW!`CE)Q z#}Q{6#kpL@{u-Z#!KYj6_~bV;TnT+^rB9awP5TtKMs&u0-fU|bm1&_4M=|qau>|^=_GRO`GaZbxUa_BmR^w)zxXB$!|^z ziMsj`anrkF=ZhL2{M0T`{sjRGYSk45*B|vI zP+tP|B~V`i^(9bW0{`bqApcW29+k{EEBde)pZd&{c$D!g#_t$^VEmc!9Aj=98Set~ z-!n!ul=2bHB);mA_$a40kMS?o4{*wGlhMr>!5G8Xm~nO^sXvP`p5@muUeDN?F_EzY zV`s*$j5jj+8UMz3E92E_Y(f zZ;Z)|e#W7Uw=s@koXmI!qpFWeZdccHdwMfK=BE?$Js5j2_GKKzcoXBTjG2t%8E(6i`icJ5Xm>=@GK60fy*I1U52+&>GhZVBIOUqpQYm8B*Qx}pV&|GU74@)OTL$~ z$9zBL)0t0aUSs|i6~B*+KbrZ(!IHn7`I1!0_luMHFK7Md5+t9+d>-rP4wCvW*Qtj$ z>hM>q7bPFjRNB{uOM7|TKB`7Y{t@O6-YogMna{sf^7k_DWr3O0*BX)r}*|9o;*gTm!$NWPh~!9stg~* zeEB5F->kwXN`5r+WuqlubADw@elmyWW=eh*^NAdOFY{GdGJGlXrOdBlJ~>B*mol%7 zm%M8KRcwC)ho>|DD)a7fQeTzB)o^SFS-+c`OH}tEO8)@M-(ghcufjiMdC3f!-e=60 z&g1m?{zJ=`{CnKq(>eSo>mOVw!z&bjujDHgKVS0d`cuK__2u(Dm-Urj3YCAQb^Oca z^oJ??1=7C8{#G$>viThMfY^mhmVBXDqIrAkC$nZCr&u9PMR^df5{2=pL z%oj1A$ox^}D;7)rzcXLPysD3UE}sexSLJt-`Fz$t&3roZRm}UC&sP2Y{W87ZI6RB_ zKbdzkZ{h1x1?MlD!&Uu{tHV#K!{^oERXl}lxV=8b>1<>yW8B7gkWs}`xSQoAD`kET zFrWUYCAu0;XdY%EByzh{%Ph*m_N&WBJ;}6($!M`B8QiA z`pR#G`}z6F_omc;SMlp5e~|g~y^{Zo`LYis-;dAVk`0poLg_y%`EQsnXZ;(v{!5>b z;U_sfajoQ!GGEI2KPvty8D7PF)f&kUQTGS?B!7X!lR3O$d)Yr^y)MHqad_?)$$L1w z{58p|a#8pk+b{W2hR)fx(c)S z`jhp&44=S!*=fnE`YJssc~!n;rzHQ1h5N@7k{`zLmHq#O`M=6|Wq(RcyvRJ`e~Q0} z@0ZJ~WV~(6=l>%4{}g{W>nHzH>c7o=?jMr>lzEN)+^Bdqd|MrUWF5Y|PI)|CC;UJ8 zr|yrd`2K!aXIb8*e@Xvdwo5*j?}ya$ll!;~AI17vizL5@`>QNIKW<`w%0H9gnXI4w z56O>XzU(K-tNXd!>5`ws;Z=P9TsJ>7Oh!jT8UVwg`1m?}TjqHhy4IC>j3SGp3S*FT zVE9)YMi~w0U^fHtkKy8ZO$CK1DB{3yoj4jYdN|z6yb2FszG|p^Z=~k)SB{W8CJuCS&F7zp*^6|j`TUrCCk{2A|E_##PtE7AP!+>^|7}p~{#Eh%f6D&BorgrZ zsP?VoYJPu(WsDO2TP!V>^{$pvLlb zEKg^CKl6tePcoiov>lN4A{m=Ac4kat9L|{jC8y7Pp_1ocFZ^7`e94XSJloHFg;j>1 zWIq2JX~*^!r_b1&v8uC-pT_)f#vI0c_M?#bl}diLh45nw^NA;6_FFwSNyWX$?mu6vkuT-w{h^8Jj57*8^uXSBV;=`$vOFZs^Q=YK8v z;mqeS&SorRT*;W6EX!>R^ZOYOF`i^R&uDv>(`RhX*qJeFyUf>c=5rXUj>veW6%tpn zd>!K!#{Eh^L$>3S%qO3e@zi>ek?%>~{S)IEiD@jaIwkoW=F2N3pS(b}tE@}1-q&&X z7RLRIYTe0`%%5kpy)W%VGB#%{Nx0$MCCv2CQsvL`9LCvgUQA;y!8=NWAua{d|1xZmi^ zd>UgZmqQNovl$B+S2C_++`_2V={&@I`e~WodFE{gIeo_FjGY6_FFwSNyWIWhQ zmh(F1w=nK!Jj8gC@jRpLBTk>OIb&zWG{&-)(tZx}vl$B+S2C_++`_n@@et!l#`BCm zwj24e^rJarXGS-lU&EQtVVuoa$heYm9pe_ptnZ}VL(C`hd32t6+b5iV#^#Kj8Oyjo z8P0qTWBEyWU!X2-9oo#Cjt`86SR2|^G;}O zW=@=$HzO~5jMiq_^t|zHCQO^vX3VTfQ^vKPG)@!T#O#?9wKn7CPMbM*s>Hk*GGzAn z88atMpLPY4iLe>tr({zErgEp`X>BG=n}mOmZ6-_ykvD!0{&OIO()4lJdD&W<@e?z1 zW@JwtpE+?Hj3`b<&CH%LBYUolp=gRlX~BT-D|_msvG8Sj9wh^bFgj-DOyOUq$n%s* z({5Kq^uL63e@6FI^6nAq;P8EqLYiY&`%w3XO4bAf-4m+#_{u{Z3eWKkqKc>P*A(9f z+O^jgtz-p-7g$m0tN1b%Ifsn$pwh448)AiSzJVkk$u$*U-5=6?JGvKC@vGv&;83XU zW2N-+{kr0lK-6Ae^e!@pL*b8H9?E}}j>1&v)?Qy!-KQ!n6V_xIlb_0e7XQooqMi;i zM4`GbR_UwwD*rTZlEQki!3)`vs6oCjCmkxvy74DMMp;zP0qVI#p}K#Eia3-XiXRK2 zc6_&(poixd#w_)K!x7wda9W-Cxdl=|q0;AQHU6u7+*v1nKF3$M2p>4rKI+<=UnjnL z4pXR}N0g1a`KRP4|0)$9KgTH?(pkln=)02DmV&PxKlh}xr0}A8pyGIS(_dXT{+}G5 z(IX~3UQSe%o02>Yf}T-z7O3Y=_dF@7tFL&4`1b0G^+DBhspo$f|8>Nt{HyeR{9Nnf z`1zsi7=!OhR;>F)ZCAxt&rhYrEa8e#&%MfT)ei9O>J{miKG0O8Q2Kz3@2``dim&ja zI`Op<8DA@r@ypeT#dcIYh4}94iuB7@%J}6gWqhTl+^EY}Ab@028~s1Wf9oposjO=s zC&1K|QMihqSX&u%-S$_z?rN)E;PlVc7J~kFY1L7wXX{I#z69z^puPm^OQ604>Pw)$ z1nNtmz69z^puPnDKUf0!k-v-a(qzV`2TDHLBk@({k4H&<2lElkA7Fk5$Nx`7bpB>( z$J;{UmcL2-K0)Fi9KP>*$=9`~o?jR73kAhLSciY24*xcCsXgmR`I~UdYz@_0jT$jZyB<@l|0!ddG)>nYl4VFy^mArsrdmF z5i(rO4^Zi<`2pd9(w>?hp!|@r{`>z)gRg8q<#osX>#X~yJSd&-6J+8~V+5-4|CMfO z@8Am^#5lGZ$1ndT;&m$LLmYmR@jPQpPZ=*VRAO^RH4d-F+lR9}hjBJzA>&HMS^cFQ zHGZzQl>8wMFRPa0VQReE7RKo_HfQY2n8sN0g&c3nVLq#)v{T6ZO2&1JTNrbX@i-Rq zYW%pA$Nkj!@Py#+M7cF@ApPmgn8sN8EvL`?Y(_QCyOQ~JjA~!PM2>fe<>}0yXWkah z`Dbj-*qL#3=?UT2aOQIuXEPQuCU@a+Oy;*RHr0L+@gumNeO!)te||0GYMd}KLi*91 zu`^>D<8a0b9*3RH{3x#PmCTpUkmt2(uXXX*0GUnz_sf4{tgDys`mt=1sqx!45m;Y&eP4;Dmy3~k3x(@Wg z&yxQTd{^r+TCqHa(@~*mIqzs%-K>un5n;{A4I!Kn^N9$juK@zYBONm)qP%#7y5o=%# zA~N)LD7PvFX*B}JwZ=MBMjOzrhf(07X0!&D(EI9L#QujMCyO<(C`v%{hiDBfLH^}M zv`2*brGG%Tl%#=%&A14JgRjFuJJA9A;E3K3eW4c60IBOS4-vT(z6W@yGY$9$oDPW2 zBXX!%4r>WWJMHEX5CrD}VxFa-4^XUuhsjUlH!Rn4*w$_@D!?)44y$JCAZpw3$`H_< z&y$MdbQjQFh;}<3#*#j^B%(cz6(caH)|F_VqX9g#bt5{#vG5Y)-HA?g+>OQ2Y(0oh za+IP)+j^&SJF|TME(Xj^*!w?nU$n$JhbT>GK6#%W`z>2OrWZKu>fW zz6Uyf@-x>l2#Yk@`VpPym{Sh@{%?VvlW;yR zA%W|aaMa>bK`t19rOO(Ud(O!S%ET{3vkw!|bogK&M-e*pL~Dqlz<>%g-k{_ih!Ai; znsm^RO+;3s#Rg@6MP%_GSoq3*3ZyZn57tO?wtE-x9J?c}nzI9unxh?-@pN`1+JJZK z$ZYe!;6W!MjRLF(nGhV=4~@go6Eza-A@?*#8Jco%v^Z@XkA8(UvV_})qx3LnFUg&b zpt+!Zr0;edMk@+#G8J^VV-H$#a8sfq9eylS6&(K&lJa0dO=tFGO6W~2F6JCdMl?q@ z)<|=XBRT*}Z8}F6K_M&~iw`=p1lbNZpw98+A9WfpKZC>!=j4}b%uK0eW@?R@2?xo{ z%cuqCBw=P(JCM^#$c(Xp)<|=XYKAVc0r{gfAajOeNf~r1FvGl`COd_&pDBb@}zNuo|5MUy}W7C#Q_iN;V{3QchJo{W&NZ?Wp4EA<%4 zBCPjZkZD3X8g1Ftw-k!94c?`UT&D&6hQ7t+PlhBk(KX5_JonaLMn_5*#!k@ zgec)jTk0NnJdS?pT^CjoOJ9V5aC-(CUYzwy&^Ck4c30$FWIA4uXHJ7`Pqpgmz8z#2 zK^8#Q-JM9QJ7DJ9$l&P-ShcE4W8A-+LWL8aF2u%{~DTu;DeVY~@Wu zpz$_($NcbJ?^C2FhFi7p-OE90jtU3pJtVd|8ext__+F}tIkI$UtuawLv$Rlm#34JL z3BsO0qlh>x$Obz>9*H7pGj!V#UtC8I4bDbzorEH0h~{hoo8$>o>{$#DvF_5Tj(ogmUlHE=fwVH5Q=0y&L*@^;eNi3~)R zipqGsp;e1qQwm+zZKSYHD3p`Ji$Y-sDZE4q0pDW)J*wAZNIBs1{UFmmfw8C;y;x<{ z;(5yY3GNy^9nRwKY3uv+?B%&wY|%Vxt&}^%)8!PHXRME+BHW$TnfM%yAX+l>9NC*N)t=$=Enin*uCXg5m=9nK5Eh0TVfx6(yFz$#-74r zdF^%YyLiO4c;^J+j>fy&XVu)^J-w}(&)Z`Xy1CfT<8VE)#4d_K-B@FX-heFYu|_Vy zr^W1Sg?k%W44w5EsDAI_N2nO`?!}Fex8O#c*51u<((uN0M+=C3su$w9y~`g*Z0}qc zN%m&^VbStqd!w+C%F}O)R8FKI&4Acpzk@Skr$+;Pv4^p~wwBzeT->Hwv_|op!E08n zk)<;c0a~N5Z-F?;wYW9sELzkpI#o=JR*1zxrH>Q!-Q?rJN3aUCw+j-sdY}J+D(TX2 z#Pfc3A36~43p25lviAwNknHtsg>zb5F={VrmVOT+$62$iT2xrz5zsc{Yy6u}V&;F` zjUh}P}4jg*0O@#P7V^F|R z+XLyeOVHweDF^p@AbkPi+tdZQi`o(R0i`3X?G$k|&q6Hw8?{UN=GkyFm~vr&)7dT4 zp={J2^(9bW0`(62$W^ z9*!lewXegc)ZvFR?_&Lt%sZKXtC=K%nXeiuiD>3WO!Wy0uT90l(Y1!mXK^^*Azu+r z??LIPx>x2yjoW7}l04qbh$CyclNen zi&;OMgYj}n9BSM?`EhAq$+I4nyvkPv7c5>Ch(o2P#_g*(<5wHE|6{eZr^fAT{A7Fo zZSWjn|9{UGrr$8f2rLbfsM33>qa068Wc^hf{ub*y45^>~cCv~h(JJNTtf$rmc$N8y zT>tJ@#|nKl{@?Y1Z^d}7`tIL*{7b<%Unl)ltMC7N{UzhYoRIp39KMpVVc=K7 zPDMzzz^6u}h;$ouKPLE-tXJMdjwc^EE$u}bh9r?dC?CNU#OP(OXKk2jAwX!{22RR$nsv> zlSMwGrptW+KmSPD-Ou4Sy)XHXPe}Vmu9M|r+aTp{e<-mz^Xj|sOCL(RpZ_lX$>H$i zqgf(JuawJnAHH0IZ|ljWSl z{A|WT#+8i2-jng?z9#+t+9%^3;_wh|cjuYk^sdy8d`_a8f6$rvG)A8x^V5dQ$!(Q< z;%-?E*&Ss5tIl>6{S9{$qE4>$M|#wk)Ybp$@n5njUgA%9>xZLa0Lsy2U(@0le82(Rv1k zXzkbENMuWVMrUvJ1V{{LEx_oweg{4rv$sy8z)Lhjzg&=``@-l75#^~$NdFWJ0=9NJl&Y@?jx8Zw0xE%l-17M}#Z)VbdvlsgOEKq5Bk(7!0OS z68*D&(0fq?&VLMnuLv?Z0a11fG7)O_eMA}thRQViDEwh+!Mi9K2MP3(2cdCDB=sb$ z8Ha^i8ylf5rqaBOpwF;no;xUL8)!2OBMU)JdR48N_c<^^>>di*(1PY*=baL>&0SUm z?A?#Q#4_k;=0bG!W~YxRee))mFnfLv+HE#I2Kt6$pgm?xW6;U3gZ7%*BcM}YK=YZm z;r_zxO`gS@-7bJmC7TK6J7m8<29B_E(Bn@+e=x@0v2#!&95vIsgHASEV<^KM`5D@%74r{}L1$@fE(AM6 zA`?`cAwjY@e~-tWKqig!p*ZgzfpGwLI~uw-Lz7Vc9f;J-7uG=Dk!ad=jBMz8Fnr*~ z-ia{%=0c39hekdJ+UztFBSIc3Jk1=o5d%rl)baw%-hV>gkmQCr={L|`k~__HsQXYK z$=zm7U&xzef(|#IT8+3(iH4aM?YUjA-U}D17%gq64s% zrF-;gD7bG%6Le<@a%C*|@#G&0*go%N6pG=V+yFO#(#(`vW~SDdna~~z?o|t5W|A<| z110UAc9zTpJ&YloME9usp%O~|XrW}zFjrvyf;*SQZZrHo{NF*e$85R?^bE4?GY6us z+%t)eH@8qW@<<0G>Lhv?q-YH2FbWo9U@V4OMo22%Yabaow6My}KJ4{%=`=6?#3ki@V7p}4D+Mh1!O2_u7rR2UgTjZqqD3{~Mt zTTTD|S)`TzJ&YJe7s&0jkz$x(+yH3M4}amJ%Jp^4!Fw<)tLZ<7zcR&b7gSDX0kr*A1+(mO~&DM8gOnA498G1eFJtVf8B^W7>+)KrZ z6Of9_w6TF%$R3%slO*EYCOAxE_=XtCxBEzVW+k?XYbk{EZ|_9HW?uM{EQIuL?@z*^ zPb2!xLP-DiO#B5UeT|$&{roIwv*~Dza#czHmSdCJs57a!$G4i<_kcdz3Uq*Z_lwZ^ z?JLlR=9nyu(ZBt9GPa=teOJ_9v>TxrjV*Jds)e!0)ol?;vj}UEOY*^3EL89Tk@@KV zqO6vUWG^xz0O{x=#G^T06UOP^?tsCF85oCkgo82+`nNYC;lNW6wh%)4x6^Ki5gF(c z9kiDr+5-vwB)kh_$&QWK9F~>w9q<{Mi61Khvn)8K_{_Xb*lIq^RLXa`! z)8m3nByznVQ;B>62*+!|3C28}`O1VqJA;hN_#s26g&yL3EP25WPoqPok5}Z%HSaXup|{Za+GO=yY@V0O<50dW6|F9dw^o zc&yGccTIy2Y3)HzG?)Do`hM~=*DS$Ue{?^h^URI!Ltk8<=a_G7^zdh=^^xY46=g~1McYjD31op}%Ft>%6VdPirGKjr2koEFhz#zB9d=Ap2j3;EF@9i=cAj5Cvj8{WYODRm>PNjWEVkhLL>8b7vx3E zUWsijbc45RT5KC4qm%Zam|I3{z`P{GxDGio3~D~2^P~b4xgx0#OvFnlK*JbJbO9lA>YAjCNI5;21RpMm? zH`E;KNXz;prZL2jBLgF;U zKSGMdojwNK>~C=i@%_6m>HeZB|L)2g{g(ayk1pkTU@)5vI?3SG?B(|?f|@p~t-qjW z7ou(^f1LiMJtu>!?1>yy`ti44n6qT;4T;Mq$9~-llYOLm#;F`=P{hjj@m{FlLcjt$ znk=>v-aBw<;Kwd&H3d;b?q%E

z;$H;+-uaYaH!Xb`=pZkkSC~$%2pGS+n-bs(bm0KYaQAehX%GjGybQr~z zhcf}iwJ2EBHQh1q!o`M*>tRGVy{li*#mWp}E#RK=6kHszZFg~>_=$*3Q+^P;uZ(R_ z?8}9HE5&}GPI2tQNuX6WB2Q44)&Ik+{re36*Q9XCU+|;hFW6`7rR?EoHnW>g{(MuW zXOF{-H+d-eP2;DHn>1~L@1}lz2lN{_(3dyempft7IA60VvxK73YnBT`y|TyoauGN? zd&(?UnR(}=ys;BAb2BGTACs9oeP*6mT1+fDrcqjfMue~<_1aFGK5e{ofd8f3H+ab4 zRA26tSraBr!@G_cy?Zk1;5z6$|7UeI^>P=1c#_dhUaYQu448nDe*@S7cno+0@FK7u z5chl9Fkla0CU6vRGH@Pn7Vt4(KJX1-A@CS*Iq)LzF`yT{(X+rFz)iqWz#YJOz`el7 zfNule0DcNQ2K)ke5qJXV4ZwSAU=QG*z)`@5aK_R#wiLJ)xEZ()SPnc6d>0siHg^aZ z5Bvt$2lyj!9PppOg+MDB+ge~Sa39bEJPvFM3_y#=wyD+eK=uCGa@XH9%N(~Q#%VW7 zP(JY}Q!jr0KdP&73CF{h8#_1l4>9jF^0gZqcE7$ud=nB={66^QLJ$3a%|UTT;nxfG z(Tk*Ff63m#w)7yRkYZB!Jp9HX9KHPI@H7O2rSQk_8*+{CHxSA{zDD>lgwMG~ z_(g<|y+*hf?R@Yx!h0aRE5fhl|0smVT_b!R!UL~i|1pGDVSwXm_TNDGA%tJ8evTo0 z_cg*VBK-NQgp2l%^YmeaKT^AXdN0!p0&bU$7;-OueW2&KMm}gh_&L}SJzULsaJilr zBK*`<{2=>l5&r2l!uKJ3*EPbABYfjE!UNE+Jamolc!cL)BfJm7Z@)(PID`+sM)*R6 zr(7d^Ey7z}BYYpiqplHt9O2fhgj3blAN3_rUjqN#5~y-_6XRE9jJVW@qcP4!Iuzfs z4&SW~-=BGxjHV4^-pPDvh$MoUPj4UzHLsv7)+Ypbe<}`jkJ6C&tsGAMEFCJGlhLXy zG9PMQ!3bUQxRnw|u0!(T8U;ytfaLKIFOKPKNL-^J@vt0SvOIjZT`{j9i^b}`%+7qx zyn+yEU&$xBB(MBF%=+{IM2AWbw}sP*_j5tw_D}Gt9@KgO6~WRTx=RsW<0nP_-*8km zkm=Q!4!@FD^8m`Zo)SBb6RK+bUybjd3M! zVe`=gh}!`ivLscz3KKHC)#7%g+69YipcSav-&g7?(rSZ?!pj)RwqSN!2RStUYdJ|FPpy$D#psi6+T7MFndcu1|S};zjwWwU<667R0PSE<-y-|o_R1+C^btR4M*|VO)d)Dw9@d`Jv-F=AQTpn!I0y_|? zIUl{(q6Ky&nm${_2U(i_@ddcriAZw`1}YM4kyLZezu$sSbUlBE~^n;c|w?wy3Va*)k=U<&B?#YoDd1sj3cjVYn) zZE#5rvN_W*4im_OY%U{kbTSk|78XF5H5{=*j-mYojwk;p{^abM_;Pej#^=#B#GjB) z@gE<9_>)BZAbgn>IE}1o=A2lJt_67Y$lE?-`6wb9a9Z z4bAzk74#kwTb(UWU?7>t$wbDRLM$>q3(xc+hX#QSnTo0kIV{L`wtzg6LDFW0`1@ii zNX?*cx=1KuhG^o0Yzh%P23^SE`HI&uc8~FDcgUwJA#%<}C5IfPbdfVs3Jpaw4mnPN zp%d|x8ghb2{iRIAvA+S*e6SscG(y`o!)4Vu1r0p3gFI1w#khHB=i}s&b1iZo+J$Jh za~{s(&?KTg&R(~`Y*(Uv&L!cHcOyE%`Q`7BcPBd0x#C^WJ%~|;5k0~=us?MA?1pPu&OIscA?*O@iO#W~LB~&i<~sNO0lFX2 zdCqn_px=KB=sC^|ynhUpLmJMVFN2mt8qQbHctVHJcfSSB2hcHu4yBK77dxv_B%!zN zM%)tTfgO;Ktb}}(v%?#ZkNOpKsq+Zj3e6xN);ouym4}Z07W!q*jRQbuVk|@3>U{8e z#LXgq%AJK@K|bbp=+SGELMN^wH}7K#DuHFWZX`bc4& zP*_C@FA9ZWr0^0c1R0|+E*xhqfC-y0A3Ab2-V?=BN})q5LADoBR-;71y9n|YT;;;M z6B(BLAPTf)gVr6;!5MD|8`K~~%cUX_ev;+QU>fX0k=Tsyh)hI@*o>b+3SAog6uJ$x z(2XsuT0{|zf7%TCWInVPI;@B#BnbT!*V%}rf;@|^J5qMMbh$`FaJXjgPFfv%V8}X# zB>MBu<6*Pcw{#|++Y2&n3ZxMWqWHt)DF!sZ`5Eo-X={EkauhkV5TEd_wTfj6BDHV9 zJY!vi=%D!C->L(-${2_%N9gPPBNEeBic;OeI-7uVH=)K1?Q?cm=DUK-onN zxD%Og@G~yp`g7Ufhu%+obT?du??zLJ8u2iAv%^@cRvJCI6nCgjZ+i?$-98%0VRZsr zU8AQEZ8%?Fi}9+dlohx0)m5OUb;14A|HIyyz(-M}f4^rY$ApB*aAZ2%BVvpS5)dOQ z>Hr}G1dNIi6(vZBK@mm>Au8%97*PS^l?6o|6=S@kVn9I&iV;_ltVaaTtVhK4iV9g& zblzWe{c4!BIS`Nk?z`QePx7tjsi&T*e!9B4r>l?TgzxH+o_!qB-h^N92z@ud(5EJx zh}G?Okw~W{oQDy&+Z>S|n()~LNY523;7brU40S6!80T482{s$_^F%s3VI*cxyA_GL zCM7Js6*lul8*&r6E`h#S=nE1=%(`7HR@R?z{e4JZBJ{-xl`G-rQqi73Lgl+iFA()E zPI&wpl)X%(OA{VA5cC%Mfd`O=w(#sRR_y#u9 zL^_yoXFurkM7knjDLO^>=^|a3kkbYA=8N>&go`^OeV#~HC)nOW*#eQ?oG|ixq-Th9 zZ9)eGwEOuYy)B_^JM=25CkZRLeZdQ1xC_3|I4Y14akf|q>E&0=Vy(H{Gi2{0OV8lF$@RifjF+$$ZO)zNF!E}U8R zI(!j^yIv>Z%;$h!qi`q3pk7a7*6u}-fr}ZL5|)>^un-e>wUZru=hR> zm2`IW{v5URwe^1YR`HmA@1;0G^>y_A6bG=uw%)^+i^uePKlGG%OuyF+9{iZ-HO+xj z-(JJSJx{&54YT99_^ud3(|Vtx+mYRShzA3vz4uu3N}NV_nFV;YKA)b68yQj@B1%19 z#gNL7q&p5m|LJuXX7_Eq@-M;Je6L=(BO$6+!I3y^>SaF$8>IJ3*ixu#`U-gJwE+%& zy}tY&PsR71=S7CC_bD$JHKwBq$qH z@!Y=A$F)9Bqc`BW{r5yYWrMMbcy7N$)MI#i&M2dKHYK5<8)Vx&w~U8SZ8%^(8)ZFO zpDEY^`t#8laB$%3=u1QOP0>l%%{*Jnkntj0ZM3b%5TZO^h`_zXRd_|1ZH|tZR{ho7 zrHJA|b2)E$xW=i*Y4{!yzC<)*`rN~(iR@HNQ67|ch2v1Ho+E0-m=IZ>HKVk5{?xhN38#%4C#~AI z^Nt@rddx_Zop3Z`t{2~Z@=o<~W>l<}ckax2b1s}>tRb5^rBK$?N_)iGOeyqEn?0*= z`mDma$24#EgxQ4!V!be_JI@G#sU4@!nT_08^DaDh`W)}5s$wc`Dcy%o1drTbtMR;euzbI2yDwuU(Az8^;m)>j`4$)W&`n z3Jnpu@O)nLdF^wMkK^9neAB%4m^Sj)qu7u(^4~*#pEmOMAiqN^`TdaB6YIST>va{< z;r5SN>R1xBFxpO7jcV8R(oWPX`uh^}UvW7pYtC%2JbfqX|B z`J0jdHTrsM{dWiQx3`h+#2C4$m3*;Dti_6e6#**(e{=+DGtV^UwM$8HiXfIlF&>FU z$wN%?NRxajW&C_Jma{42_qwqZ^F$9nR*faWE7?s#s~jK|t~shGLka?a{OdbJ*ByY z%zvk#?fmpR+RncT*VS6~-`{B8M%*$l^WVXxAh8Tm9aW0q|FS*xS1J2&1{1?TyEbf% z%D;{~jM^{VUTLUg77GJ&{3_mRC^GWIxRHTBTVN7mrS6}|!6R=8uj2xLz-!Z{8-^%u zbN2rcu*V+qParCG({q3`cHn8aTxg3K`UUp#yB5Gz$^j^W=f__!!iC@a{tiuQDm3bT ziD>+evK=}Q8pqNpxZc?Ra$)X_z6bR=YH(58HXvPO-h@d4+f9aD{{}d@*+`yoDzwW* z5y!FrMskIb?E3+dw@nq=uDDWa3kt2cU*a7QjXMF2ixp|d4XD!IW3EWvjmA2X#o9aW zM=KrOazru~&2w}|$Iu*`(HKXMt3~oFIEWg9W@wH#Fy=&WGn#lf6fwTrh4vP-Hs+*K zk?e=Y#7tc;k{&cBHufbXo!KYgVzP74B7{FK_#PBTh@=*G`)PP|dWcAiJ1C}rj-Svx z=aC}m{2p)UPmS|nf*VgO;-Yk1@>CSk;{3;9p1qrhNK~BTA}G2Gy)$mj8A$gO`h>U% zN5bAK^zOKw$3oxtB&55>^}7W614Y^s_s(7L)9*S|m5ixO=hOp5MT^fsH=8D$XmK+! zYwOGt=_oDU={#oy3=(Sbn5;9`NY2L;k8`?cpXdu)F&_gr=i`B2x|tc_=EAU>^R5+c zk~W~pGYmIZJq95mU{=5gjC`XI_q`5>75|4BnPH-c|LCay5^#k0-r*$~7Anatk zv3iD+35M2ia#FE43K~vC%5Y-5`1?Dk9UC#j(IHzrB<;8X7hB_dihahpdI)yHgbg)N z$5pY9Wx@TkRr{kw0=@=Y^yE9_NO@hu-&&4ediG#Lb#EP4|8Pyyi zda4#T1p8EC`kP2P<91XbeUwPMZ(v-@xV=kxq?^eIDs! zMLI3+BjIzHNDqyB*$eyQMA{b@dK!A4NN2?@u0VRYNN2|_#x!B#2$7x?xBN+@Gp>li zlwn*C+-Q=Rc{S1nacQ_@lb9vi>5rTBFO(f4(uHw@E{1+=Inu>(U2y|S;&`#~0&yo_ z37ZLGmguUuhtr`y>3aBC7bO+Q+9aMU9*f?D zG3B4o#fmt)#M*t2gt1963sW}@oA9oWp>dDHNaYTS;_fZC;ose&$Gf))gL1=Qf-rd0 zFxV;#9uo$gPWl}G`q+n~su-tm?>+{DN#}k>@vO?-t2r2u_SsJWp=eikm?U+b-yM zE0BNf>(InF^We-q9#d{zt`iFP)wu7c%k@U`EX?|K?Jip3`T*-Mp1a1~58^Hn_dU3X z)wPGvI5yPd&WRDXqF*~o`XHHkD6~m0Uu?%~***8z4|QS;eXIxWqqx^dYo6ns5LVg6 zzT@;v9{}M#`$QX@dVa=X(sRFE+_r!)b?tbu8+Vdb(dd^1g_B?KssrUHMojgz2#mKZH`N#($JZZSb-4E}~ zg|ym`aQ8#fL2&+*QOaveE*u6QRrb&DTLE`J#Qy|otx+-Ves~54HP6$=O5yH@?nk5C zGlmx*X7o3r2}zx!)3Nq3&SCi9WpDqzXYNvos(A+Wj$KA>*L`2`RMnBw->hBw4q6xq8}CB0`H^Di)^*g zwi-i-@}z!-oR_!?3xwHbBggHzq7R(CYzSRFH{i@5>Fj&au-SWI zYB9zsA|2xtafoq>_V;LvPP(hZd$?ff5&x2c9YpvvPxRLi?(QJEsMFKuEYy8Z2jR}` zc?6?u(kde-*>hAM2<3+0^$cAC;eJC%_3SvG&3erwpOgbKmW7P-O_qJwp?av&ImDo}K{+ zYYhh#o*1z;pEhzTJ%hTTZO<6OTF=whA!nT-RC|_ofVcHV5I1|~+=HBFja8`iWc`es z=M1xLp3&IPlQtMay=VLrFsn9%9iD0U?U1z55E?yGvA-ofZ?s^y=ekS?n>vWixhGkS zCrLFO#K^1Jl6T+)GHJ7s<+LSd%tGs4Hu5kAtPuU{6(bL0K!4H2Up1tjw&aU8LE2(e zh7sZT8{y@(4x%GrMA*|0d0Pz$BSNWI$y&n_BZ6lhq<_dwXiHv|i*m0U(g@rubq%C9 z4a+RlJ_6EPMt#|~?KZ3N~@KRt){tTBmlHM^w=eH$) z+85HhhGnrWd0j6^?-@P=w&aPpZ6N7=que4}@_f9_DCq-3DzPR1JPwv08s!$-l2e|8 zRBz;!+LD)7Li(p6-DFEXLu}rU4a?=W6}eQD&awIyfbu#xnYAywOwcMgK|jUjEeC3ij^ zQllZ&+LC8Ynld#nZ%X0pLNU{d4XhdRoO!dtvUrqu%BHqwu0Yr*qDKlt=Tp%9M3+GcPq7y`F}$#X)X-QZ`S z7ss^s=gK+SHwsY>@^P1hLP7A>1)7WlB4(5X6zyNsii*gS3sW(s#^4D*MLK=3r2j7x&uTwt= zg(gBC{t4!q!ExX!FzHjY3;Z1139f0tTr&>)&x6CjE#PEu8@K>`eQ(y*I4d#LG zgNwi&;41KYa1;0o_zC#HSC|9FS*BRJN@%UkD?1Ai6U3&ydFA?WIi8x745)p7OfBRyG7>9W@@4x{g9uG{H>Asd*^32h?;~Q zUPPj8fL&9h9p`(C*kWN<2)m`&N7}N(F+y7jyR)#*oG;=(p7D#Ak9Is16^=tHOkRck ztwo{GBV=?A=(SKXAwf`Vpi+1#bzuPYlg;rx9Y{}N~vpw7n zv3?U_-v|3_BI?j`*ly!FV|ybSF#Ls@SB-crfL+YOQ0RDJr?&5CHSg^hY=g0=e--R{ zE`qq*Pa++*x6f>5 zlM0&!&xJw{McRn@{xQ-_^rLaGacpRPZgVE`zlM+aNFNdRQ7l3J_Ez#mTpvdMCgev) z#!KB#;Fs+%)*p6vqaAr-l0mJ1WW2^Z!|NveMj@_mZ3%_&BVez+++M4cp|I-@yN1_r zpAOPm0k>;-d)coQolEpB@l4|-TSK8W7^_;U-+trXwgzAuy)6`~7k#{?wjR@bU$2Dy zQ}4GN!+vm4i~Jz+Uq(9YPp-?<$aUEbyWt;%LgMtdxgB5V0JZSvMZ3O&-D_>wiEW<+ zyPH33xm_Z^0Qr}DyU7z|;NL-T$uUE0L+Z$tL zBl3qK|CGr5OJrokwsT`Vn1y`&`q*1OJl4%sAvCu0FxY+zTjN|nZM(3oW4qdNUbrfN z?NcA&xQ=wVUD$u-)1w#aC9oOvaVR9N!bRHHuaQP#{VHMe8EjhHhiZ}kdK>wT$bYtt zd^e8E6>a3FA^+w!^2O)lE^i~>kNg>J7~? zgIme>qDpZ-(i!=!jdNmr83LOXu<41u6y8pe;J zUyNa5QTaFw8B60uay`_%d}@)D#V#h6>P1q<@MSDJ=1ZB))V^iH7#CrYOgo$k$i;Vi zvw3;(rc1G?^M8tXL#JF)Zvxb9CDr(J5qW$1jVU!EcDvzV{SmC~+&dLQX! zxoVbE_SvdD=l#=Yr{@1Bu|HI=nr0MH^Zzxs$+(2y13CGWD!|HmXYOT6{+6OUq`utY$9XsWc_3cnNDVsIb=RrM3#_cWI0(y){u2%1KC8z z+{OCI6f&L6By-4ovWP4p%gA!FbVT$0MD^ppjCg|4TC50I5wId)MZk)H6@mZG2;fGR zH)8eMa7pORSbcORE-c3CtMctyU97%xtfsvktDlKGD7MAwS8T9r+hg@o+?DiBtUeYu zqR=*D~ zHTW3C@$=%7SbcnhUHddvf4$JIHN@)v_i(8wR$q+bJ7V>Zaofb_v3loSn6HY}zr^pV zFJkq#alhr4v3kzccI~TJ{k3mx+SjrAik+JF4QgL!*BWE>**D`NSFC<=wN3jjR_{}9 z)4oTw_}R8IRv&pC9yW^A9cWThtp4IZ?ApI$_54@u+U{7rKW1@%K&$aKh96_~v!1tW zKgH@jaPQEbSpEDwP5U`kf5KOH?G1RW==g3 z@9Ic!>a$+Q4O&k9f;;gV9j88e6mF(*>L1mMt3CS9xbvcuQ$G~p>Fm^>?v5L1oO(2V zrgnAe#W*QTa_aGYagT{p|2zRtE~7YR;FF#DKup7SbLvaQ3m2UFbljZO!>O-0+OG9< z>J^y%>*dr>I2kWYcj`B7$0bRp9(xBKy>{wX;pW5xoO%arvA#|{|699upi>WhZrA!b z_2-YpgU?R=$jk6zFQ?ugF+13)e}P%!RHuI6<96*3r~cjjxaZ!f&;7=(9p=>E{m`!U zcj^lg?AidQe#%O`2iU29yTYaobn08H#9fB^f*0-D5l;P`MdG@Zz6g;x645QSYw1pX z31%pca_X~jF?1-xicNep>chQ#$2j$82Wr}}PQ4J5SHlp`^Dr&x)Cb|Vq~o3X7&Pew zr|#K}`^}uXeU4ol?$o{A?b-;ZzUe-@HWC5ZY1cBGdOtj?I?AaZgYB2;)F16{*G4<_ z+n&eERh;^J=viZ&`cJ3Zw6RY8u8o>@qEi>&A{yt^d)SJRPr}g01pQygne`rmc(DkHd7?fLH!j?tsORC9zluaX=_$-H zChDKwhbhWL{i*?W?V&{dhC)nWBO_6Z)3|<|sGo(-UX`dnhKklC>em2k6ZM!8HtlJ+ z#^!h?QSX0)rmaiVFTTO9txwb^U`Ky8Q4eCS>A6JxqvPz_hD5y*ogvDt|AMzPM7#Cv z=$J8Xy&7F|aTooP3o+r^MW1$pO}nm(?)=cEUEf8&@)Vo4q>H|01t#vg=<9Hi#INgf zhTF7@bp6SNHf@fsAA7c4o2%;|du>{wu2)@zS8nTi{4|?Zr0d<#r1`pj3C5*jUH|r7 zn>MK*hW7<%+a0flLZ}h1=TY;cqvXZjNw{WTqiJ4biT7TA1iKPpr>?WxXYtCpoTu0f zn+(k1%*NIY+o-uA&gG>dOI-iI9&7k~jk>xqj^7uU9aEm~avFG7A1 zY{PY4w)a}~WaPh!{MN43FF^j9Hu6^@|DHDT*CT&vEBPY6?;*b!`M9@fZ~Nq25U;4q z*oivB4v>iLqwufQ2x}CPw!e%{e2Ee{~+|CKdO|)${>^8PzmkK*CE^yYs zZbYQN@SNxDuum`Y$HDf!cG~BM%_r^DYixhmeGa=5BJ1V;d_HRxcGa-caQ{kMc4A-N z0XrRbA{~wkd$$)VaX&~GCuLdamW{SwB|#^(+`_IuxT%zqP~@|J+h7Z z)+2vV8}*6!)Whaz*!YkRuQ%hU-g++VoEW4|ZpY3GyENF%Xs6DJu(=2}ZEX`#=K|O* zZpTi`=8FE6sZICuC5><*2z!>v!#rmGR9JWmXvW?V=OnYy*Qj3i<*ZI&-;7VH=Fl&az$F3 z_s_ao`o~WNW2v}8%BtN#HcXs$ibd6@=KWJ~*+eX@&HI;LCjF^-e{p{BXDqpg%Jo*h zy@yEI?^xMvzCV@v+QVdi@Dh0+fRB1rKFD%`gK5{))wqv9&Hpd%C-oZhRXO$Czj^*& z-4EjbSM&M*lAC0F%E)q3-49ShxsGfgo5+}(WqA*oLZ*|MWDc267Lg@n8CgzNku_u; z*+4dtF}JXOGKEYhGszq>pDZFv$TG5=tRid3I!7eMa1`QboCjtRox9CNk!4te;FF z)5%OShs-C7$P%)QEGMhT8nTXTAe+dT<*c7fA=AlBGKb73i^vkPj4UUs$QrVaY#^J+ zm=&y_Od-?BOfrYeCyU4uvWzSztH>I%j%*;C$e7z$Kbb{PF9gM zWF6T+Hjyzr9(u?WGCUqJ5#V@r0c3TojI@b~lH+8lCh4YpFnKgNjy#9Fh+IT2Cm$g< zl5dbZ$US6Iw5;zC@_2GbjMO`0B_~s#O8FAXernL%dLkDn|hr!xNv z@^-S4jEtF;{_lxEOMU*or}b7#tq52Vup(eZz>0ts0V@Jl1gr>H5wId)MZk)H6#**( zRs^gFSP`%yur~s}2i6<&o*JpHf2MIFSjoqjUmnU+%>{++tDl<=N-i z4Oz{X`(N#A$gRzn7c;-r`SRg%J=A=8^;jvZ`SOYjq^#!4gC|K@&6nT9_O?1-?*6dZ zeEIS&Msr(zPJZz<(!ZK7Z@fy%YQB6qC;D5PFAwtm0yST*wnHPIlOG$9{?vSVcwS#N z>ks=s_^IcN^{&?C`YXpBDSOG~qQ!7iD4Eaq@1P(3jDH&ItLFOyv{TRhPo^ATKX0sSKL4L{yWHOSWD!|HmXYOT_`U$j zbz}qCM8>R?em!IgnNDVsIb=RrM3#_cWI0(y){u2%1KC8z+`;nBslbTX67 zA@j*1vV<%n%gHLThO8qS$R;u-DC>XiOu1jAP);W^$s97DEFw$DGP0bkB5TMxvVm+O zWA5hqlPP36nMvl5`D77ULY9%`WEELM){zZl6B%<4>nBslbTX67A@j*1vV<%n%gHLT zhO8qS$R;x8Ue-^hkm+P5nM3B2MPvzCMymHzR8g)W>&OPOiHupr`pFbBoy;V2$b7Ph zEFsItaz9+Swq&54P+A;Q_lLy6f&L6 zBy-4ovWP4p%gA!FimV~)$Of{BjJconlPP36nMvl5`D77ULY9%`WEELM){zZl6B$#% z`pFbBoy;V2$e=nd;(&*H`$FoxM5M){sPyW%xF2ls%^&JKLLJA|`GWF)&dG8d`(Ui( z22$l)v?5?dz>0ts0V@Jl1gr>{BandifN0Bb)u>}85(70yG2U|R%AN=fKGKD&ERI_X z@tTa}iO6-F@&uCI@{x>v5C)DN$Ko?S-4_b^86n>-K3CKuC`^yV4Ylr`gOGIkTsAGx zX+tMmii$h@R-4vo<6AJ+;u`1TjT??NiQ#95q13$iTiFTE&)#Wz&VTe0$HFN4CE^Xf zX|Sn=&Bd@eN1S*~Z{eNbDdMf!;uYIlUfT+j{`lL8GWXtv_d$y?>iq$c&xp_4+Z1u# ztzTz6&lwDb1|uD|KV900{0!uOj{G5!`2{jxJX@NF{GXAJ_gm~OFQ2C$p=$CXYYFTM z?hb{fMB2&qJyoo)@Kp($dtqZ-B~kB4kT&A0%f`FJ#WN(^VD|y+#5>c&Z4N)Dy>|sg zJfd(-Z^FIuTLod;$oGS%?|q-RSl=PApLHMJXN|OWzIso?Wup2TQTIgTN0)~}OC#%! zh_B;Pu}$Q*2*5V~{!nO_xIn44MP%KHd+QdyD`9`wqoEM`(q8)r-}XI*Yh+rB6#**( zRs^gFSP`%yU`4=+fE9sXN1$|(oF~mD#c8Ej&cJ!5Sd=`?B+oI)S5a2?0W6`c?gQ|h zF8!(d017%azYm~zX504x1eo9IeEIVzIgn;5DRY~(A0cCUIk|?^?B02(&y$%8K?cmBEEAYme%eAaQlpG z4VTtuiOO$i&zAoF^!osg{HHO0pZhnt{<2k?_DGqOtI46vSN8!tK)sLUU)(O+ ztKO@x?gLmwyKT#*ztuNNyDa*9i1|TM)qflNtGXZHVdh6KljVIZukH)jP5-&d5A)r1 ztdIKLWGeHAvHd>k?BmE+km+P5nM3B2MPvzCMwXLRWDQwIHjqtZ%*CvqOd-?B zOfrYeCyU4uvWzSztH>I%j%*;C$e2r5Kbb{PF9gMWF6T+Hjy!x zvVJm!OeZtR95SCQB1_0JvYf0UYsfmXfovi_`GoZ^kW3-d$xJeb%qNS;60(deC#%RB zvW{#Zo5+~USU;IUrjwau4w+9DktJjqSx#1wHDn#xKsJ#vm$QB{g-jjI%K2mwSwfbP;$6f&L6By-4ovWP4p%gA!FimV~)$Of{BjJb;S zlPP36nMvl5`D77ULY9%`WEELM){zZl6B)CR^^+-NI+;o4koja0SwfbP<>XKvKg9Wg zn1fg632HuGQR&t3>G!ZDU+Va-j;HE4uIf?8Wp&&>rBud4ABVw~c?`z0f}3lsD1*Nx;^xS-;G-$?pJa)*(;S|q8r=Dk~BGruov;@z&gXP`_tZXi_nG6P-$?{eQ@ zAK^+~<{0JjE{z)DN?j72;Yurs8SWaoFm}8v+rF@aYiOn`ZMZ9Sgv$#{lo;W14v(J< z{qPp_8PJbxL4R2*dSTy^f8)K5t8sC!;03KQr#=3m=lUi)oiSe57H6#(=Ro)Z|J_+fs{+Meg)i@mq0oQ&HWb%Ri`{tNJlcAOSZ~~Ku+W~PddR^}e+e z`IC@uya(SjezyO#bwItl%24Cx&ZPfXE~6KU{o+fMZA96Y##|BC1P{iCZK2Rz&Buq9VsWA?*RiIx=8HZ$ z3BDG+^Pk^G#TXTZ$+E0>TkeBinSN4zkVP)AC$71cb~%cioPlqF)oNjbbYa8Pw3lh-rdQHaVRntHAZ9iGnSznrGKSQ ztCq5AxA>@{SXgbizCE00U*1RN|4doU_v7n$#-iHylWATa*V&DE`dXH6ZQj1(IoX~+ zd)_{c+l|c*Bk<{=Mm*ii4>RbWF6r)gv>{hMGSpxoPRiP=M@ajdrJZ)wK$-vDAqKN| zO8em7rCsjVQcnAU`O9Q|r8^Fiejb&6a-WfQzR!*{^!^Ug&i?@Q{bc?1J7m1P4-b)c z9;x>~A??#{W&2l<2T8qlzw}do9_#x>w#WCJ)cd2PUG2juhTp(LQufi0_jtKJX?Mu- zfdi!+%;Wkmm-g9T$@Qw%at%K!&;fJ+vH0GV`m~MB^Oc>t4?xS7{aooSS`n}!U`4=+ zfE58N0#*d92v`xYB494I?I;;2 zl={~E<-T)_yiU4ZQhQsn`d!H&^|f`(r=x1hK{A(AcDXx;$#$8_ncMNuB57yVwI-5b{Veoji&hN*+yK%!wvn@K~c?<<2BeA)A_Sr!1})i?<3d z`$_dj+@7RH2LCDbKC-@E$^p_#R)5Iyq(*u_pq~B6+>rh0|H4Oj9*+HD0(n7!><@~n zznbe+^8VI;I31{Zek&)kz5B~qSIYJLt>w+_{H^7Gul>A(W#BeEx69asX1RdlrrJJ= zt!>Bs-F|A`HkRX)IWB7Z{#o@K_fx=m4*S8GQIWIlO5Ig6Y_UQ7na#pF%o?c_b= zL*!%RljIt59l3#gfqaR4h1^QMLB37COMXm#Nq$Ev|GO#wOxn5M#*tmf9%Mgq0GUP} zL1vLBlV_6V?b@EKwzsm&<~W*5`pE!UN(RYFvYLF4bnce>Ybu#d?q+^5<*rTAt{*vs z%pfO{d1N8Eh`fbdMXn|{kwMl|O}lM zBefqH2eOuS?J=L*^O*H%#wkDn<@3pzCRuI2S=3KreQF%}v-|0v9p^uLKkmwY(2pEK zW{|3%G+tH{;lCQ|KZ`#XNC`)Cf~3E}H>sJ8F#^{?);DPjLn{i!|vQT40) znw&h}Zm(XIuhv`1?a|y`RbFv_w~snMyS1J8{aN*^`4^&86V zR6wqy{$=uQ@>5c6m+wuo(kuR(`9G1V&&czAwLbgXe%$i?pRsI^>r-!9FV#4)9mT#7 z+$8M+q{>(K>wS2>Jgx*5HUGV@#)(guA2j8we)1*rt4;ap_vUxZuQ%nZcBt*8WW~2< z%65H7{+sM{q0}Eh9!-uT^U2G|8_0Xf-K%B!D#}~P56Le`+bn6Yj97rBV=8%Qtb>!{j@rv8A4aCqehlSFlyfQj$ztk*$<5R^lF4gjJz3;4wA)I4M1DhRh0?w^Ig}hr zP9v`%?;xKbUm)KmKO=u26Xr?(2a`vUn@Jexe9EF`Zcmy&mrRpcw=$K)Qe^L$zVq2$qICOMHb z$H9_YjzAvcRY1-p7m*i{MdYRA734y4F}akyiToQ`N3JCACM(EC$S24(XAy5GXUGTFY;Yxb{wKP{K-JE4_ImzvslH)T90(PDe=XTEaxJZ14d zBB9$#S)EtDX|n&EGM>vY?7yb0&Qr7Am+e*le=iTYOZpo{o!)^Yr0e`M-_YZ^JmYwAnYngy?^T5x%2asHJ$@k`Z==;XHT0wQxzyx`g!Nh zoH6Z!=@+xeV(RDkr|oruRDk-q)8`b;n6@`S#Y*3Nbu~Zr&744}jFZoa1juT#maOa` z^UdXyT-!EpCQk65)e~9!G9w$^)Y>rKEO@lr_r!6Qt~>z4EK*oh$Qw7qJ}aC9}y&m9GktjhD-E z*_TN+w!$FwrsA3}Aj`SQT-w!MA@u>ut(DiVk#^c*$!yC0>*!bIn=7VVy-eEGl4&=x zT&ZLu^{o`wvhS33^(!TVWHs4H`tFeVWqqOsp^<Zv#Te=JWP9|AskKZVMV z!XnZem>(R~a{hMam!8;i{uj!BcFXx4r_1{5r>QJSn=F3?^Bd1JmzSUWyMy`eo0{jV`d2eQcSZAj zRe#{q=6V(Xzlz>@@_3(niDW&QyHLu(Vo5Kl+T{&MeSp-ilyWucyNcz>+?~8nfcKNA z`%BdI%|Dx}-R%*F!z+YM2DjUJykEt<{mp*F&LaNA@l_pT%{KcYtNK;FsvYM3Y4)Y+ zRqZh6wsDbj;_UVAEK^}KfV$?}X?J)iAgE#88nY3liG zbv@k}RG?q`ynJp{J%3&KZz-#Jh2@?58RMRs$1fOipdqX0uQ%oj?|2)PMEH4Vb)H>& zyX=4JJiUMwxAy#Xag^K-)x1#PI$6JZ{(3i07+ZV(y8cBODfRrdm+jiu=dZJkmw;%k zzBm2Mxw1X#`RjEmkgVsALj88R-OT=c>bd4!@;T(d(R|LC&nZ{GDfOj|lC_8Md1pTN z9K89cb;o1R@^+W{>S(_1tt? zrnE~-rC&1p1SzZaP|u0`21tD++aJha{r5>f^``Z0WILZyM`m zJ;A4>-@td0+7FWI`EK<*yJ}~JI6=pP&yX2QHCainB{!3`~b^3_~_?I~%O zOS|HKN_jc$3)qh82c*7~?Wv`_nEn?3%zQTao5``He*C%`xq z)6YuUrJXd@82_>hG8OAoJDsQ}0WN3d;OkmTP3a1>BB7`u8y| zo4LM0Ziiy7hibRr6&Q4VU|%7a7BL=w>Ah*vExos7ozVCBn2V16EN#mCnC-7_IN|TN z75?q%e^nf@-nHVDl~u0YyT1D1?lZfdIr@efeLi{W;rG^!uRF9MdGcF#e7x(fSBBqy zSn9uTd~8Hjk88?m7d{nwAb;iV@g0V|`RS)x+1IPOHhuechi502?LNI``m(8OZoPer zwmfst&pVgwzDSMVre=jju{vVpZgn{IDi1u8xTU4ih8-CR+pmxPwAb8nzw@zxdLM{7 z-Y7rncq`w4Bed#>l}Z!J0g67-B-hfhTDK6)M4QF>iZu}gG+PyMb^c4jJjRbJJnes7uWl{|pw z#pZJAcbKv>Q`xKXsy_Am%xtgZvslGX1Zb)WsOWZu`Wem}V%H*kR1QrX!tp+p>_>|Iz7PJ?{vTAw zg;wr=FuDEzMy!6e?fCtFv!bo6NPFwQ*!##&trY#!)B$)l2Jwh-Jnap+oy_{`iv8Sv zFYITzKW(l59LMc#UpL>Q)n4slJQIui?db-qpTEIi<+F>VoH5H_?#_J47o}bOi3Nt- z+P~7*q@N(mH_~6}C|S<^g!Grz`8>l#sZ+{Wku~F_A9dX@6x}bmwLs()w13|2f02z<}0S% zFY~MCOXf0P(ff_mE0#t}KZ=b#q^uZZdBsY~ioxfkKgC++E7nt1Y^1EHJ0l=+IK%vTIjR;;9~SWQ{6ma<~tQt3Z!i)1776}3mW z{-oPPFJ;9v%8EY9ilx`cdK7)**-uFC=~7kqq8NuULJv%vW@elColLe<>>lSzfV{vSKx5 z#q6_XdBu9>D>hPA)E?vdlfFDzUeQavVj5*dpDIUYo0v;kG5bLoH^l(+6&w4?`W1uB zSFEJ0SWQ{6ma<|!WyMCyidrStpLCn(rL34nSsN|y%8FjfiZkcWv!{0G*=1fvc0V_MzkBg{196>L zE$X}jA3l{Tvi1^r9kG6yq_TG(Cy)O@GIR0Zoxi}JqOvnn)g!Jyh^4rJ+fj<)^U=~B z&F`oAU!Oly*IOB;U!ml$`u)J`m}2_{@Mr#=;BMvjLp`r^{;J{aDfs(g(ObkDz zrvdz#*U#J9`jt2D$L+0FIG^hm-oA{lYDZ&|oGkIdN7j?R zm!&>HW_RsxT<7+_BK5hXpR6YX^9CDsjg-?~mG)^{%sQ+BDp5;ovkaF(7B(tg4 zzLawH9?4oVNGiKZ%B?i7UA1?N^zXg0mG+O8`K4poo|7coYk%#DwEwf)zlH64Thjfe zq&fcHm#Js`{UN!&D*kCtG5<-)v~Oj;pYLN1l8x&9%q*|sU(fYy{EgegyPxf^T`$|& zUi&q!cjNQYzWQmF>nvGM{rL;X{on17a=)l$zPbN5PLg)5wg0#F|KHpGdbaz&t^KP1r}-KGna$&$%kd{rDD{n- zB&(-MhU3ruw|0)y*T+h>*8jb<|GoX+-2Upns{L!Yp1*VdHMhUpLuiY%yiDb9Kh^nXXTSpug-t|s_|2` z-@jSb-&*^FFUb7tms)OrEzfV-ZT}xUeyaAjcmAo4U%&VK?T;f0v8{!Cv%Mu-5wId)MZk)H6#**(Rs^gFSP}TYih#OD;E%4qEx+o11ogi0 zV$SoL+u&c;{5zfbDIXj2qut0?50Y~5%dWC|fE58N0{>eP@M>B`j!$(fR> z-s+d+op_Z`OGcUOqG_0XmMr%rWqs0DxguG#O`iRAr{01Uf&Zlle4X8K%vm?xH2&-C z0kiJJe1O~=H0|r`rCBZM|LE9=`3hBMr0>>rE$Ji6{blLg8~?JZtcvzO0yb@6-t?k@ zGiU#`hpTXd)%%LV3M*knz>0ts0V@Jl1gr>H5wId)MZk)H6@mZI2-L6IxBCoX?D4fLjQQgW$w$a7CLh_OI zl<$zduvBsYx#dpUQGVnu+Sg0J9p9FGVZFCXKC(to&2I;7R~h#Y zE4hVjk=TOn4^y>K?;lpj|AnLd*&oEQ%-q)CO@Hilk?&iF87oUF6XHDG59|^emwr47(d1(zBMP7{`k8E|F+@z zm@|c9AM7{5-ucYF*^95??S%bM*hgNo6Z(V!nwDnLi}v(`K6M}ci|^J>g#9Ggt8dRH zLstOZMCdY*#yWW~!FmtJ$7n^pi;%Y%d6Cbi2|p{L4@CG8x=QF4m~@+mu2L01@| z^P=pR(D|Wz1Zi!!%e~M(#^trI?BH?_HyR`SyARj2J+L`g*kBRu>jz!(x=`qGp|IcL za*ua8g?|BV5TNT<#HQzza36Zh1}ky#?hf)`vppi}GAV+a88qdo~p6q?8t|2v`xYB49

lW?GgB#<{W$rIq<^hs+D1v=yJR2fub%WCOZfouQ)%b^S~9ptvc8+l z4|JDw_mIpTDp^Z9tFJWXt16F?dTlxDy-_lo@l^9lM~M>?EJ_|u*(KFl4&?;OjWcAT zBjt%xWXW!nvmQ9WNcEsB&Tqt`p6i~({GL*6e2ZBm)%!97onF!vW&(+i@YPwWwyDyUdmEJ#7 z%Ids+F&n0yFI4sI3CZ^FI72%6nX-y&Z~9mEKQaFh>VKhJdzMsS>dT0SYB#=TZ^)g` zlkNF|@?e(#qfz;w+^)a!drpec-?RntJ2bVgN~-Uy`}#`RT`27uKN}?Nmku;odzh5{ zbJGnu?Owj;`bDWv>o4i;ceJ!$F3Wjmv)=Qh|G;ed{{09kA9Jw5MwZuD zF86C`Ur)Johy0#Tb*ijSyI$6p%kRzvd&>Gs4oWrRl*aX|{7S~7{yb@)dlCK6zlxjIL;9^`{q<~T zF24&?eYDKi7^mEkY{wE=uaE5vrn4QrWIJjbxPD9Jda3?YA1&JxI7-?#c9Z^US-+3{ zGMC#&_1ge%h$<#`-yz={Tz#wLC)6u@EhzQ=8)f}ry*a^gSB+!U4@|ET?~|!skz@3W z>J_I+dB#BoSxqR9e#H2s#wbN|6>Z6C{LcL}$*51swfD+6wN=*2vm#(cz>0ts0V@Jl z1gr>H5wId)MZk)H6#**(Rs^gFSP`%yU`4=+fE58N0#*d92v`xYB490ts0V@Jl1gr>H5wId)MZk)H6#**(Rs^gFSP`%yU`4=+fE58N0#*d9 z2v`xYB49<%}-O_NUHZXAIJ4nRG0g< z(C$rAeKX@D%Ab?!V&J!we;`$y%~Wx&aLG81CsmxY8Gn06Q=BPxB2}CRQXWjII8UHF zkyLS>M)^Ea#rYD-mys&YS5sb0syN?9`3_RW`2ot0k}A$mQr_&A>!sfFy)046YnZRz z_x%dxe~{{Z)*n&cL8_19?x7qa)pm3-zMV<69d*jxRX#V=0Ln*@U8o;Lc?_xE<9!DRPR6kl=4@kdhhwQ^s|Ii??(?( z-of~*_oClN{exsLw(}LrYe@AT@oLKMPI9~Tq5UT6)2R1SznS_;)E_|oR_fm%`%?c2 z<p?(_W)5xExFQ8n^b{gN_LEbFt3&{g%zl8El zWIyWfr~Dv!5cO*)uP4<9hBs4wm8ALa#(MS(Y9JQO;tMt((y?MJSKdQeZnCw-1l%1l| zU&8@djc+$nR^wy+v9g337l*Lpt8pN0$N`3d+RxRvt;U0)%vbxn%2(^>?kvv_)OQe4 zyGdCcNBje%td1X>dr4W1_usI+YMfW&mWua|@%I|%8EQMH(yq5gteK{ztXQg zO3JF;8V7uJ98&eE?;upB$atymAgJ|FYINfAu{CcSrf1 zfj}I;Z;&eODnBCMmo$IhKz-LBaD)7wfHqQo??ZjhAbXkAH};h8(O2I!@Y*niw-y95qyg4&F>CW zzA0sm{(K|&y^{N-KJ7EP-hP$;2;<7{AgJ#O1o@qSdLP^ElLJCcR`TsmA+( zln0SVkdwN|0ts0V@Jl z1gr?`M+B0WIPlT;lBnUX)P>QbU8V7k(Y6l8hv0{+55Z@OYjMNy*Yg@p3k=>B3W2EYl2;7JA#0QHH*!~~H`{rGXy-D=2$y$h z)CgDV67jkFl9=JHp$lVAa#cF*r*?1+9qvj)2w(zJtl9{dbF|@5`0Kh@)3P#lg^m^N zMkf7@3j50p`y1n`?Bej+TXcA$a47uc!Qar)yFvrSMa;ea_O=g=Vf)6o0xm~F%l?eG z3V*BNFL}(a(4$C4_}kmQNPk%0i&|`+Twk%BqOL{!;IF&7$O@g%_k=!h;;zuM5qeSo z5a?4U>jaELXN8 zwqplZGE~^$qm1n{1SYFtzisBO&>^gUiLftmWV*Zyqei=eZbu?2^g@S%*sw<2vQX|z zlzaKYU7@F2Yj;+}cBt;+SZZsj*;se@8hV|kWz60cS|P5lXNKE+vdeE@9Bwl9Bw?Ee z+qDnw3ayHRuy_43(U}*8ZAVDkQrHe#y(@HMbK5M~o)fmE-}SJqe0Eo8KHNt59p@@_ z*r$e#hsS3ldk1Wzp4%1raRP+4``~z2AkOhbY>V4WZUnd(ioapkqyB`Z&|neIX)gB? z`xvnuFup8|vVR~OfcTGy&sLja)C)DQVoZQ}G0Lv)*c7Vfw&2*35gwZyQ@Ii4-Z?xT z8(C~qDH`L*diY37YzlQ0KCt-lR|{Q#cT;#Q_|=#-)|F<@GL3j6;xnKb8R37Np+S!d z*El9V-&86i9`(({U&a#jN7UJe>%#tsbrJnB%|0%&HyU+$GgU-RjL(V8&5X~D6f@$_ ziWJe4MSoh2`WAO;3I)aZWb~(z9b?8Ubu5Wm812|@?~o8H`@9%K>tVa6Yg6b0(YBMs z80xqMfrUYq%lX0>q0q#>;am#8o~F>5T!&wcp^;-sz0*Fn#qKdCUW^+P;cK(r6uL2D z+z|Si&}VmV3VmqOFM>X~S5xSUh~tE?UkQD{+Z4hogqJ*|RztrV`dV@GMB9Bi!{v88 z5!GT_GvgiOTOVjeJw2CcTJ8}|q2m(vsh*1*E!HzS-u`5Z8OHc0>RE((hMv$A`nOEr4C^*-as1N)pFt`?+P_ z3cZ4~T7-T*^tn@-Lgh*c3cqhczZ?4VW&LUPDR4L{-o6o#M?L9x5A1wXn?f#O$3>Ly zT8iy)Zd1r7`l)DF1qx=w+n)g<+dm9;*?CQ&`BBo&ag5lrM#MW-3mU_N(VjfmRTMOZ zP8a7p@aGsW@<+!zenuR*rlQ^@u&ci?Z0B>FM!P0qH&zTc#(CLl*d@hP$G?kx_3k;gz$ehB_{qWrePrch_Ge%OB^%3~9;1lITcu#gqsAEy0pME@yj z3jHkV#kjN3E;jCRc|L~iRwz@(rb~m#WZ0J$H-%1%mGv|q7wc{I4^4v`%&g^qBLe>O zRvGh_X{6c(SN4_bujF#dE{0_tWicch%f}KODNedX45FH*?r&JU=Lkb??f!;R=C^u( z!&P#=RNdcDe6N(%eF4dX}T2LD9a9(8|%T2EQ(kNdxz+s*7TFyvq(o{f_pGUy&Gna2AZf=9@Fbzeg5 zS}B|FJ8-`t%LUj!gPrN;UCH`BlI~p(8ue&zNjdl5QqFx_%8i4i|N0N5oL0wjQ)$op z0JJx$|6Z04yf0<7ermqo8Ib)&>8o#@wC?yT@H?4$MU}7S{p)S#N)_$Yc2jn93#a<$ z<`rrq8>D8@ICbZulxNO3_d;#%%-Qp&7EYZtSR3j)XwKAPuRxx}G>LQlIG03`6Y(du zz4BYIPkyJ&m^Hn4>a@c4{f{>J@7o~PsLgh_>VEK$)Pv^C^&cYJeuByOnfv1VS50p2 zQ;b59@%Z2fxo5Q6UW7T)<+;VOZQ}Ud%ugRV613`P>f9O49OV@o+sEu@`T_0vnKpZF zWP|AE7*l&f2esqpSIzN?P9#>(XqVYvztnd8{i<2gAG)F8Z?vg>eXnT8pXlHF+fVkz zck>vcvCaF*NK<PRj2$E|7Y`1y_fd*|SG{Qas~ZTIhcX0_w*cl7Uf_Qm&a?BA2`-iP+?NB_QZ zpZxBB|DLeDop$eQ|L!5r)P8sW?$w^3Uv;FG`}YCu`1!5<`@nYm{i<1Q_wVP2x8v`3 z^zR?{#rJRQ-~G?shxYDA|2}!2{O*7Me(mvg+TB9`7Uzfq1|NRJpu^|RHaOs@!;eTi zym)B(l=LBo=S@F%-g#Q(zlKi1A+FU${Cu(FChh78@_f#?#t)tNy=T)pX;}&Kmz3Q5 zG~;$|VPF4=bZK1MNAf`0dugwJPl@Xo!cQ*ssj`?Ru2%?sa>wTW2U71e>5rs7&7>FS zRl<+gq(7Sap{Di^r`~7MXDNG=p9$0lP5M))uQ&NQjrt0c{Tb9(n)H*YUu)8fjV9Ks z+N2l1DujNsNuN)>-=v>TeSt|ogLWfYKtEewE>93@Iu}NP-y*WNtQ@`9~e=YU3rtP?ddbi0Qm)MN>Z!_87Lj7iw{!Z#O zlOORMg4m8`Kex5w=ML(3oBTgYeWOW#5A~_0dhe&+Ttu@)NqrTdt-#~pm+Zhl)Ot2h0So$e7*}s51;eWYF{}S~c7;i4Zy#&tP0XEq8($n)Ya zxr<%f3BO`o*Dx(+#0Bl2D%$iyjp1V$Y@*@AxE7~;oFaXQIwm834}6GYOn6>ytZa|C zzP|wZPLvNs=8u!}c9{DRHN}g#uYz4J?8KNA_Omxmw3O>r3)@FvduXIB+MNq;qD_s+ zUxR$(I-qLvse9Y(?rPT_L;j%1^3!EoywIf~zq*xtQADiQMA&SBjp(c4_KsZQz%1}Q z=}Gpd1+bex=f{vkY&+GK$ZhwnY?l|=6|kK!_s7sFNQZ5u&#}Tv*t`mx1+Wpv%CHUR zK`()kXj>y}g0MNi1snThS)Z^=PO@ts!LDy)+tjvsac?|)uzRo2w60kT9rnkhjqsHR zo9pKN7+M|aYpfBEZ&W;l-4fV&=KmNvSnR?o9)_LcXd4Yhe^?E>q>FzHoe=47g4%B! z4rMER*26aSk{?40kq-M*>v5rUD*A=fW7ihK?yyKZ_IKeY75U4Mzv%z5cPH?9&3XU- zkCx~{8$}gG6wz8LwOm?jx`D zh%HkTu}subs;#s-$p4)4d4JpH$R%g)+vC3dfA=eo2k(5o=X*Zqd!E<1wk*xY+hyBs z%7#AK{=6^zc{v-;o3vqjk-t6d{B_N@Z@j(ToDe=ZPPxDRc7J>DJiLCr?l|MEPhhan zpSQ-J7d%g`pSSU~Q@Ua9r~cftE?Cz*bz`rM=C(g0c1Yyt{t4t~q!;vA%D+ zJZt6;rTPCI=+9Z{&&l>bUq8pL#bH6C;5p**qsr0v>^b6Z-{O!m1+r#^7z8IMt1evE{I>u3`sbI> zwZ#0}UFE}sKQaM?^n96`QBi@yT5F7yS6Lea1AW;=g#xz)+U?#fX&^;-@eWE z{?9;Mw66KoV6Hw={oKbNFZai{aDyZQ|HA@lKfD)yf2rZ$mSc7NkNT|$$AkA%!N(V> z7hCE5TlLGTe^UL5>XiL$|KRm#@X=NEt5$mHs)N^p!AC#U;cGX)9;AAo;C}9Zj8wg^ z>Pf1@#{qx)?^W-o?N_VLP+h6IpXvuxAE5d%)#a*RRDG@L*HvGq`UBP1tNupy4XVFa z{b$wf4=|4k)m>GGuYLXX@2oofnMc1qO!ZBgKSK4*s!vk=7uBb#o}>C~)wihrz3N+4 zU!(dq)pJx=s(x7Y?W$i>eTV9gRo|!j8`Y~-dk5P2ey6&t>L%5@s9vLbZ`I+?ocQN| zr0Var{dm>xdOTpX^$)%d5Pa;aI(!}G*IiXl47Yk-ch!?r_f{Re{|`QnQa!~=?=aOW z*GKzv?6|>qAA+SVSDm7I^+wwd^T&SJXnRnPeg50|LJcGDN45L+Y;IQ_zCY2NIPu^Y=HUMvLUS;Gq3Y?{`4!d6f*Z#F_*C`hs=d{= z{}-#z%)x9-;x$0r6zg9g{^*5^b)&46~AEWwP)%mJds-CU=SE-(*?WG|-83{h*HbuIk0A-&4Ir^-|TBY5(`#eAOSQ z_SV|*KUAHexi3cAc3;{2k5%ue`bhtMMDQ_Q`+uVCm8w5gU8nkAs=e>6U!&>_ z)t{-(R=rI1!8)G4t?JFx_Aj)(QuUXrYgB*b`f2~=swb=dTJJ_SMRDY{F_1`vsrRqG@t5nZdy;}8J)!(Vk_`&vXQhlY?w?=iIwy#xPuKIh`b*k5? z_I|W}Kd8=7{iEtU)jz4ORNbt4vFdKReeckEI;wtBb@)v*|Mp*^dJ}E;ezN1YbJvIJ z_Nr@CZ|deZ+x*Q`_g0;vx=He;G0tKM?C_1{8uowjeO+Vkvr zri<#{s=KP*Q~Rf?9r{7Fy+ZYNsxQ?3JyiGJ#MZaH z>d~rqP+h8eN7XlK|DLLAw0$Sl4XSrmJy-L0QQf1R9WPDwFx9)NE>+!2^~>6SH`Sl2 z-d*(%s`pU6?bmjDq^qt}zdcp=*7n}24_Dns^$Du=;}Kqs_TO9E*Q(Z!J$S>~+xhIP z?c-JVRXsud_ETN1?fP*CuTJ#=+Fqvl2dch9^+Bo^sZOZ&Hnrm)tU5z=f7Nel{s7hA zs6Iq>=WopGP}P@d|HD+zSADqZwW^O$y@&SCRDG!GfvQKV9;CYWX13nJs>iDyqI$OK zp{f_F&QiTX^--#Or&zyXswe1p!&Mh)`_ZavR3D@I63ri>y2Iwy?^xB@s*h8Blja|< z`cc){s_)VK6I3^-9;rH`gY`dA^<}CTcDBc#H`VWP)gP&TLiN|G7pZQy!fwAORqw3& zDbYi)z7G2tmD_HUZMI~)oC4Vz0av0rn*uubSJ3a?W+Hv`YzSitFBUgpXxhR z&s4v^s(w-1?@_%}_1&shslHcrr*G~2?^9i=esfhXQGLJa)J}Fj52)_1dYsvlI{ zOa14ouGIDgs%um~q`E=%!>SM0{*S0WS@ol;r>m}3eU0kJRNt<8q3VZKKd$-})laDY zm+D2Te^mXX>TWB|>nYV8I-AGSsxwqSqdHIZrok8P{Eu?go2l-tekrPJwEZ%@aT%=b zm#fZGeTC`^R9~rjh4#Nnb=nrTzN=LaQ(dGtPDQHEQ9WDrG}Vh$7pq>YdWPzob^H?5 zJ+`#tovZpGZJ((+Q`<-D2jBmu?Wd?N()LqTSE@cu_2-&@y6QS@KSOngE_VDgRcETs zRh_3gPxWQ0pH#h2^;4>ssD4^?hpyKD8P%Jwvg@Hn^{%R)Rh>}%oazkipSg=&52IBN zR9&umkm|*%2dnl{?fAWSv;8wv_fb7c$JtH^+au7q52Zl->UwL>XoYJt6ru0 zRn@CiSE~Pasz23sTiu5L*NodX{CV&^rKhYcwDC-3LxI($$~0x(SR41hi52##9Hae| z4QE+hqwS@t)0BDn+OG7Jm3cPakZW10OjBlRd(D})Jx}Q=E6=d;Ol8CA>Z43k)|{sO zm7cQlRPC>9I7Ry_)0Ez58?RC3DJxIb{>p|N?XOHz)|_PHc}h=NIZFF08&1^z$~0xo zNE@%oR3FuzvhoD=Q8r|&k1|bJbG-IfddkYs0u`zafa(0o~QJbm4|76Wy7J`Uzw(?IYj#_J!Rzp?XPU; zul<#2%9?|N|FPXTd%X64_$B-pe1v@+)qV@W^Y-|i zwue87;(7bJI?d*Pbe^`CT0OvR_iW(Cv)%sTmzBLcRO{u0*E~_%HSqZQ`Sz9Eq3iqE zg!8pM%?{^vn56ynh2Ps;t=Hc<>-V=kwsPAutwHVj;~B`@^|cP5a9>w`?O^k9Ust~S z5%X-Xc?CKj@p>H(wC%yywS$lQHQ#;RdA3`RZT5~XT<@h33wQie9=Gl8`1z{c@%L2i zjz3uSR_Zof^{%QC3e&5_58>f1IZ7)*2l@>V5)mq>mU9AP)pn6x$ zzth#4Ki}1n`TaEiNp0_^`G0e@=D(x5o92J+YRzBcYR%vD3G)npe#JkhtzE78X|C4% zeN>14c7s3v5Lavda93;o$*RS*KD0b9_d^?U_1Tg7yhwdXq`qFY`#!}js@?Y?D!ba1 zu%(Xt>SgvqcYD?IJ02c>rk8*2%W~ch+P+ZRgP*|&K6JNo4y-0yiV{K#r| zz1N-iPWXKpcfBt@Xh~SR@0)aO2-_z8Jx{M@oAC9Nc)jdPZNB?HiN5T({`Wkycd+mG zy76Tr-VKlE);(U|n04Rhao<02{|@!SMRp|jdz$V%-0x{N9AN$2?`hub4rI&v)&Bp{ z+^@oQS2o%C7x!8g)+h9C2s7hcd%VB+2b=egPPTp7BpcuK7OQ{y%fG_;>95$la@E7u zSe<)`rKkO_Dq0@SKen5Vmwso*f2)TbH&w^Y)BKrNoRnFhew$FRl=AYZ$>Z>oe?7f4nCr!s&(rn}RZ)5pvPs^u%vi|M{ zcx_u7pE=7sURr0{i&XD)n{EI5#hUlmPr`LA*19q#+kSJq+3~OMV2)`iHlDV2qv<=l#LAHr`wFuGq`AZ=v;_ajtD&qU$N|emQEs>ofIi+wU5!e_LG- z_v~!@_t*Shx~}@`dUU_9J9oLwJ9&!byE@Os-`M#asdc>E-HubP+iPjxkL|jA+SWDd zGTVRX6_$72Z7!>IT^+6K?}!7e&t-jVznT|yUOMkb-_SaAeSf^U&HuK8We=_Yl5EwF z+4WSX_0HAxvHC{qGvi&$y>;DZYX8&Ux9etUuMflXxIx!LW1`@J`xa=kuvN zPWY#dk5ixfbe-gqE>(joA<&Ilf)|K1-`*i%s zoVCsdIrY8H`knW<%^$A&Tm2k6uOYgBJwDU6&nU3@SN~-7AKtUao%eRM`YYY8!_Tw2 zcB0K+r2AE-uA{r;G(gwaw|YEG)%Iii*nY2XXMO&z+dWO^yTh%vzR7cJUgde_{o-ny z|G4h2eYD>zoo)UCJ)Tbfv+eiN_l==T=N)XnO}g6nj*nU0^C`<=T34Rj*699mlWvFo zbh{1H`gW7sJ-XdS=(=&g*IubU&-bwY2Ugnsy;?2}y1x$5aYrl9(f#X%t870{I-Db{SyR*WFoXd2bJkrtTuc@>??(xWFcDlVz^1jOn zUysZ$J~+(!+wAp5qq0evQfcF_KKEI;&UDoYWtOu3Wjnt@)jvG?emK8Ob%nA@S*@&9 z)+-y8P0Cvyu*dW6x7#|>l?i2*vhXH*+}mSEb1BhynX*DzrL0!gD(jVv$}=kMxG8to zar5+dC(>0Xlv&CgWudY}nc18dI#j4`9%bvSR$Z&CS2ik}l$R{~I^4heojQMILiy)i z?0j=n7b;7XWy%U=m9kn{tE^Y<*TJsG9sAn#GkI6*-~F#TzA~YF>`S{Ya#R;8Qxf+0 zFm{RUU!n0TWwo+aS+8tVuG!5V|5NU=<91i3D-+6^-uCxMa#SC`$gaCrSJ?W?|C}EB zrQWn(m{port*liZbA&xFE-16xr%B@}Rd)Qx*4TKu>Vz^&>HdC6q3ROl$P(MH|4w#2 zRcZVWC)jx9+jjfbYrIj}q)fS6=dVmxt{h|cw=C6rd}QnIS#8HD(Ri7%LRqD(R@N%( zm5s_KWy(Ev{2Kkem2}k!WtK8WS*ZMEYkR#n;1)Z-3XNAOtCh9NdgW~kZ2s#9*!iX0 ztMgZ;D-+5rWsb5?S)wdcRwzq*+22#CdC2~LORdK1m5s`xX4{@}pN_9gS0 zjmpY?y~F+~^K^V=x-y~6QsyWNm8p8XC{tabtWs7hYnAoNkJ{Po)1*4(K^>KwqK#fuPL>o!Yu zjgx|4w^cO;?>zW+`)&g~|cf>-<$$D65p! z%35W;vVXbVo(-LB-6;>tUzx5Px25&ZQa$i_^KgF`t3>1TAF#)4DryzUv@{*UVRS0#l-0^wWxcXd*`!QaXvgoaOjjn9yY;d4=BO@ImMF`V70SBJ z?E0xzU8}5DHY%HxDUa*;%5-Hy`Qrd6y%35W;a^!P%y*H^YT9_9;4t8H;$4yrz zlv&CgWufx^C#{eByLgrJ4hZ{HX?wM@R#~rXR5mG7o|L~bU71j3DX&;z_p^r7y~E?& zc8__LX?umTN_otA_IOpR`VKvBHL7k>raWcG-=mj3F0R_t`X)48caTEak2T+5AG)CCW0T`@4^0{$clr zYK;)#ENU-&)o6%0^|Aa*I_qulox+zVgE~yI=Gkuh*X%Kjlc9U!uB9 zS)r^_W`1Dr!)jH#zjt}T@wR`H#w*Xa`)T(Vb$n$)nWfB8ezw4_zrW~xK$*rXlvT=V zWv#Mad3b-jUYb<@^8%fJt&XouD6^C~%0gv{vP@ZXq#plOS1W6k^~y$Nld{{UdjI#5 zj;~B8vy?f?LS>1vOj)6MM8C!GDlgcEL~}j zk7cS)c-CI8RH?33=Kal%SFgHJ+3#~(U&_lmzA{~zP-ZD}l!eL?W$L`W!{?(4)$`x7 z`&+f@T4mkkcKbA{Zc?VaqT?&ml?i2*GDlgcEK%ltY+pyNP+g_0R@N%(m4n*b{k=(b z%HMQ+WxDd>d3K!_-EWU4IT|liey7LJGSwBzDrL3uhY|L?=Kk*QBb(^@*Y=cGb$n&I zGNH^;<|qr5CCV~og|bRnt=uugj#sa`QQ4$i*kIQ|!+mx;rvKg6bL}!)cb4iLWudY} zS#*NE-mFkvrL0!gD(jWykCI=TN<8=Rr9e3aybN9a2^S{P(l!eL?<-P;#_N-7{ zrTpYIJFnf#b^U9+QQ4$Sc}@PxbY(*MVVU)xc!)U`YP>{Qd4IZH|EjB$)yi7sKW?-0 zYE<2zzvrA%XUDI1uYXi0lxMtQ>(5bLs7&i%j}v98E0k4A_jizMRqv&*7dEPHQvRUV z?|Z&tUg@vfdJ;8caT zEai$#?Qx}0b&0Y}S)r^_Rx3AuMAyISMrD&SmWr?y( z*?p}&j#a77)W3&btGZsApGB|5$`q0Ca|C<~P($}(kzvP${LV|F`SpzpKQ zYkc7I_PX-l2ionK@{atK>B@vMOPQlARF){qlvDn0w?~!gYGtjmUfHN@Qr_~it+V^P zwvNoH`udmZEM<B@xizEV5i9My%&8Bg2oQl|QmUG08VrMg;KtL)I;uCoXA{o2xw z_I=fq1|47N>GgC%b(S(mS*R>g7SFQlr9yR;vRYZItXDQFo0KUZ>G;ZYWkQ*y%$RN0 z^Q;T({#2szGG&FbO4;ymUii9Ug}yGg&nmtC(e`KVG}n}mb$n&7*UUAcI!oEm&Thv- z)tf(W_v13v70N2*;&s-)R&~9yQQ4$S`9#N8rYjT5EM<0+Z}-32dF+&{uRm$~eY)PtROdZm_op*@+x>9G zq`kxaclwK6@AaDZslFfHq&nqaI=(XdJbRy(P+k6p-EKLm3zY+E?fzM&xi}QY*aQWQyO)A<#K(!BcVD=S-P@Mcs?~B+55c`jh87al$YuIZ`G=6mG#O-?jz1MCWmGE?6VuUGv<7keCNQtfSJ zw^R3JwvKdVLYbw^QPyv6-#3|mPTz1HWg4$gRw=8MwaR*Bqq0u#n-5C0^=voQ&NKaU zTSr2frCg<-qbgKgqAXKZD65n=>icK4s_T`F$|hxBJF2Pn(e;tgc$RX=t#&`Id%(WWaB@P|LA6~!HLu#^OqJ$WE3+rs zbzQHzQQ4$SS+4U}rYpUk_INvfmE8_G8lU>S-5&0JZ<)p`loND2_0iY=8pnfTE|zWD-+5rWsb5?nenVWzvTa5>w8mQuWcA-9@TE%@9pudUUj3gNtyDE zj;~ztne|Jk&Qj(mXFO()!zHTAloiSefMu|9qG!1GD|sryt$MfVb?{8#+Myv z&o>pS*Oc4sTdlgGtIe-h-KZRqVvjpt>*rj*8f1OaSK2xf$}Hu89rgN8b&0Y}S)r`C z({As~OgnC^#_RNTnjiH2>L!h+tdhSnU77iwd1a}-KwpO`R9&JhQ&uRel-0^wWxcYm zUdJ}6PFZco&)nT!52ndCq48e&d6gX1g~}3Tnev@4?e?isJxY(8sd_$qQr}-~)b=K2 z%6E4Bq58Rq5A=PugvOUv+x1a5)I19{{%)?_US+B)lvT=VD@zzNY&cTSvMwt(#qM zS*mlCg~}4;{5AHvq(XI-vRZlh7k0bUs~&WwdETv`14>z|^H-)T6Ur=Qjf!60=Hm~(@w_l&X+V;EdZ#i$Td#v8!_Lk#rKbPm- z-1_#b|I+%n+i&9xT!N3l$v$gt4t|@=eZKMB=;zVf=b@MFxNY4&rT6L=4gJ6D?0$zZ z+B>KR-C~c+Im*e(izx7+e^Wt8g?tbNd?RkrBJ_^5H5$*faDm$K=H>hXp^P>ADs}J7WUT;OW zmn9jG&U263?r}1BJ_zRY-^U&&g5O6Eo=Jc5N8PqyyZd-4+vd$2Y59@rb;@l{u<=T5 z@2fgXnXf!g`DbN~<}XnFs&bifQ*H01JXCqIGB}^$H~dc+ZO1=NIaOJvoUNRzd`elT z{6x7%xy32gFHO0>@+jqL%3|eZ%3G8xa%}(9lPwo%JVW(6s+*KspK9~^DUVa0tt?aC zsT{37GnKQIHQN4xa-Fj4Y1ZG}o^PIM^)lr>V_R-_^IU#aYTJKQZgY{ZDJpOGlpV#k;Iq%V!^RA0I@0tVbanHRfPj=q_+uqT2 zeK9ZI?f;|ZwRL`*%B!t;-D1vjuZP>}w`CZ8*w>@LdJAFYszkOq_Z-32m|Ncny z_Hy;=n77;eG3UAKx2^m4kuh(#p)t=R?)@P<=5a^IoEP`;Y<$dq=W3q&_m|x56wM1Y zuPx^*WA=;o|4-|<*Lhvc_nZIe@k?Xg&xXaE7x($(teE|-j5#mv-YH})8N9&`V0QnK4~>zI!#6Jz?#j5+T{okv^idq@4+n%7b1(boO;vzYyg zAGOy>Z5?;M`n7f38})UgwyuNPGwir+&D$jA?Y8tO>(|!$UWqv`?&HT5G1qs*K6ah9 z^*A~+=Dg!|+_qk4WW<~|DCWGsydUp%^)Bky)^*i8=DZ#Ei}!lkKn77;WG3Ol|bA5wi&O0&YyxfQ5-M**C?Dt!LAM%Ho z=Mne)z}@QC*7fpG%;To-AMg46RrPD@b{i|^V)LBj=8=wV;*;U%zl@|oHst^ac_y)Z$Zp{wK3;?7IWS^G1s^4Uh%H`pJE<& z&zSuVjXCeAnDb84zsKIzbvhyDypcL?Ti5fMG1pfTbKco8=lwC}yz&9@Zr{1;*Vg09 z<1y#G5OaNR$L#k>%z59&oVPCK`Z~w_IiKz^=j{@6UT(~JXUCj3UGv(yo=aoSyE5jy zA9a6g>$t!6*Tw!CbA1amudVC;Z<^QEVk2!Br%z1IYu6Rkze&sRe-4S!%f|&ER-Z$RkeV>@~4%580zAkV^ z%zjVBJda5+`=!L3cYMrwburhsJm$Phb=hzdmNaPh-y8A|u}8^WibqcU;VQ zyTv?iR?K;4#+=tL=DZo2*Vf~GSmPI8iJI4z^O-UGjg2{PTFiMDXM(BIK>uZy0E%$xS3&2xWm&&`Ye z-pxJV+Pu%}E#15yBmJJ!-_Lp|GVfoJc_ZJqKA*nZvfl^#zNtHppY*!!l1M*y{I>G- zXY6(7GUYmDvfqEGud(@SU$k7Uy3?~(@1{IVd6Ke6t!+O?_2tStl}{*NSN7NZFI8{; zob5kaW;cSXR1zDJzI6A>V>M8D7}}h@2T28O?i#-9_91O_msi!^ajsW(d%s3Kdj$B{%-k| zvPk2dl(Uu5=euFA+kQpL*~%K_GG%MW4(=;yZ`%IDlts$f${J;|`&IT5+kd9AQu*22 zHeRE8nKJbq8}F|itt?gEqpVY|RYuR-t;hX-+MHtRyU$X~LzKgnCo2n;GnAJquT|cr zd_=ie8QgBc8fLl!EARWjj`y7M9pzWbwaU&P+Pui_Uhl2!=QFdFU#42!QFSmsxORfv z#9u)0dGHa`!Nv1`)mELaK#@SixbYq8XX~*AJ<6F*Z{r&AZ>i9T`r@?8L9P7(e1SC6Z0zGx%K`hM;j!muj||L_BdSq7f1R>cTd*8 zLiv6$tcbOA>;v1zL(sF%nd$PP#ean^q+|7KV^GkvOzyIGN=O3LH zt(VJZhWfv!>nA#IN^tx7|2>oV&(=RyeWxlA^?L`0*h}-G$B))WYTmc0v!M2 zutQt#XInS?f1s|jPJ7w>bk#MF+w+3jt>58nt^d=*;`RSj>zJebo7O)i$>Zk)9q@xe z@%qmjX!ANL7pZ@{RjseTSZ8wDka+#~9&GbYSO2-{e`%7(pYih9V!wF(7yQevquu5I z7xkZ=r2jbeKQw0lcAr`Qf$CrPj@9QTx&8~~zy1c-Cruxvi@0s-M;K*VRe-kB!-XaMzZ%{|>4@ zQ2&KVe8$M(U^>bVQ zYg+$A_5W*<>*q|rf1Ho)j%|7SH|lyAss7ua()#1u8R~!0aV_^xcFc2hoD-D&)PI52 zAAS9Nx_pxLbK{#I-}3kixb-H> zc|X|g`-SGuQJv8B6W#BW=m9n1tp@88o!_MKzNzNbOOJ0KC%HbS`3;gBqDAZB87=)Y z_O^PFj#r!H{xw?7=OkHgi-Y(@{_)oLLal$E_WOI1{yA|L@An-WIsTpMU#9WqSM2>u zbUh~o1O71vMfQ)*bM?wE>~m)Jc|_|ow7uJp=2fQt(RL?B2VBEQ4(rzP`rJkJ z2=%Yf*J+~dMrvT26I$*cX}`Xlo^A6^Rj&Kc)_>1Vt-pRgLH$pP*?)r8dy%f6FVz3@ zB-j7Y^(*`3PaaYqH|uKoyruEePH6q{Kigk_a0*AC7|-WVBW>Op%DdG6xpP~;{)_zH zLH~EP9=E<^d2X|LFV_45)lEr!j+cM5-*l}jI_~N%wzl)QPV<_RT%X5<{gWKTHS=?_ z`njPYsw=hrj(fCz{rp<}w@vc=GTmQLu=;*&c(`?s)VznZ{;%G(+v5mbKW?7OW80<) zr=~=XzsEM_zq7`-k&o-^@_7G~;1HkbdCrX|%UODUIYa%w*M7Ss@t>Nc(JkBB`l5Ng z`d*}bs@Z(flk^{v#OL_~tba6LQ@=jlZTq`g|JF&qJ~qtn9~|P744a?ibk+|iR@i!% z=<(+j^ffBC|IzCINX-5>EVurP)c-#9uS(MYKYjoB zudl6t^!?-RN&Jt}`n&au=l{oVt^apg{}oz)B1!)#{w){Wz5`?SU$WBrciYF-GfVvo z2DbkAf1LVXe^R{l7oBMH_EGn*W*V0ZIC2`P~Aa zXJhtv2Y*oWAKS!^H%R^6c9&T}^Nl|G>wGsFYW34*^*>(o4@mO(JT%hZHT&RwyB%G< zKGiS!`u`ZMKl=V_qITTkz<9UsTD@;iY-jUd*E&v2;xi<&{)Nfbz5WF3lT+3IGJSpN zv?TpcjqI1KER7uZQT4w=3UqD^}nooQWBqm5&vX9{UbgHYCWfF{KzDaKbbN6pA&g}`kj1wsQ)4Q_aUO~j*Jc@ zIf2;`zp1J>ZD+2>CRzWXe*fSQ_eIv1xA^ z^u4$B*S85B@ZrpO{U6qOzM=KMt@W2CS^q)m|4rojNOt>X=z4iy^LJLgD9QCRDp{k* z_;r!T*T3odc}DBMBFXv>l>Y(S$Mcz}_3oMRmN&f@XKlhY){ZATg^A1(c*ZOZw z(tm{S5Znak#$11u)?1+YH>kcu{Ufa2r077B`uAF=+kCJ3-=gumldOL~T|aI=mn$N- zryIXk?=L=5|8vy;WL-be=ig)8j&0;Sk;kVWbp4#B{)@E!w)($xO1#^7uI6Q_|KcP* zM{C{t?GUf;N4hAjY( zxAL|Aoz(w=?OXr)#)RkrRX@6;tuLB$)%PZ4r}j3lD9QCRJktNHh=bc49nY7~*E+A) zlia`dN#gTl#KRrOW&fVG?)NnBq*Lti-)(m}%zdJ~Me|*{&92@{Uw11~{|7aHUXtg> zz5VqEr%s_MtU!eXgm$!a>_EG;!PK(z+?Npn$-AJY6TR=+-) zf4l1MlJwtG{V$B!|7-P|toch-Zyvc#e?DgaKAV}x0?pq}_0~!HALRr1hwwJv z+4hTiMC-BY_nrEWRNW&<|J}9zYtM++|De-t-TEYP_?)eiFTXclP@Shj=RH zymu`qGwG1lXkT}ZaMp@&QSldB=_IR{tm$*4jh#q+TG$ zKWp88RGpjT`rJYNFNpd2J_^0-={?jx*Gs*gUsDE8y zyzA%H0-Kkv9Hjp1l3br-eBa;@7o8oi|LE~HuYF(3e(HZglJ_s$`OSm=k0g0r9=UnF zEt>3o#!l+LtNLG*#HYLZcbOD#{T(LSyf1aWKkDnNS0w4bt@=mT8LjV%9RDrV*~;gW zJU{+g{jW^ozm3-aNlgDU)|mex@?S2WTa)xZ(T50b-){PTUDPF7&s%H#PgZ~T^OGHu zeE+(e*1tgOi1v%tgY^FB65YR^*M9jcT7P`oDpi3Z56{#Mt z-`9(@^Nw{5l&-H!x7pRn`e!D2eKJH19-GqA*Y%BNlWvFQT7RMHswCIXmXQ&Y`roCx{QTD6Uw8KVZ+wU=)ZeA+@9Nz<+wsrWylqwIB>DPGhmD*2&8EhD zoQQsZ?Um83KR%r07O31t&wno6W>;@B&elIm&%bACJ&z~x-#p^u`b9H+kCwOpTKQBf zUrKWSO^Mn6-bnuf)kn+!?@89*DdK;;*5lHhhpWHQ^^&Q14XOtv`MWTiMf$sbozh!+ zxbZTr_fxHZlIqu!tbfxa{awFkHtBx&lir`!sQ<^Ow|@QPM;oY~nq>W*x3KdXuI=Bd zziSg+ZhL9j`u#Pdgp($EmG*NBf?mep|~YSM{hQzaKYH4Gz}) zXuoKEzxwT=`8TWnD9QSR-=)+kL|e6}4=ZYUerNU8?a;>K$&oF(c;2V#)AgK|{SJxL z{ndA{a^Mhq|C_BGtGrM-UwM%_PyMJHdrpS0l^|?JV9%294?^}0x z#LjCMW$<@7H~v_?$hN0EX}PO1@NzaA&v(Cnyyz#J|Az7_W&36u-$8kh@)%{la+dN> z$~%>hDqmCfZ?`i1JBg!}rOM^n-p;e_-IcwSM<_=sCn+yg-m0ut)+v`M+izlhcTpa! z9ImX?I>xEKMCpEad1Es^|Gz8X?zjJU1vqmA5cstO-0PVCZoiCn_8obD`>gDdmZWaPa8BQT$)?AOSk^ifwtYPKf1qn`Y-Rl;?$P= zN7wKAlK+bW!JD!F=12BRE$_EQi(A(J&Gzh`L?CZ#`$|~!7HsHd<4Jb=*>H6KpH29! z4*W_BuiMuiC+iMr`FQ!OpSIRpcYMpo#b5n6G(-Ra1Q0*~0R#|0;Qt2#4Ig{=A911Q zW%jY0-`(;*U3%#n+ppnp%j~@^YkK@={nEDBBwSDFNZW74k(PP;TGs6luU}1?^+_9N z^UFtRzd@Eo`&l;h)OeB;&O6BZ)SPegGA3A7=2)fYMC+2vhpCy)ZHvIr(2)O z$2ESpW$6u;D=xCkn`&8ix@G3kmh%s`Oxr`_|Iqj%jo)ioUSa87Vp(*KWy6`4*&{4# z23Th7sqs3EKc(@xmX)(DQ_Cz%r(3Sbv&=irvhGkz?<%>!q48%dGas;=f3szIH{}dV zZ;WM8wq<3d^-2AcWzjY^-teqt_Jfu+b1XBiu&g}SGPS@mdR?TITffqJ+rHv?%e)1a zb+=k(US&CdmSwb$t23^%e&tIvuhz2YVatX}%j`dCyW25IX5L`^D&MvFsf#U3AGKU@ zN4!3+eRhTQo4?fdOMAt#{IUP6kL#B=+xpdfVEbph`d{ng`W4-5{puS2dwpC#5&;Ad zKmY**5I_Kd-;x0D|9|;)KJWj{hxh;XzL59-c0KU^|F`rg&RYIo5%{nEc#Yd{g?>Ki zx98zj_}g0hl6QvKCE;{P{nWpneET4@hI7yJJW@y7-QV^g}M9@oYG z#q?m?r~ZF4{cSr0+Xi+?y|~@zaBJ_N_P=0ufxqo>Z7XXxuuG4N+YjlI-R`1JU3&Pl z2X^UjRA(>f6Z{SP)K$&RJ8SO6?M`3cXPCcj8-H6XeahDN8PwU^#^1NtpL>r#_xPaC z>0MG!^Sh+^pI+sUH~8Z>1>@m5gJm4mDO|(g&R$P{c)ma9^wrJH`>&sKTBj}@Ms@aA z=>_Lg=WoBj-+puOgGzpn_2-k(?wIxK7~0v}$KU*eKX*k_bMquWy`#INUe#_$mmX#Q zxu;#cX}gIwdr+4S!#ew$1NVLWp7XzMZr;NGJorfS|I75pdaiA5{#`I|OqaBFWBlbD z-FZj|;|~SrJJz2w)1T8Pn4^zi`~rWx+#et02K~(R|GU8-U+j;67>tJw?T+@l4(&X` z|NoHA!~Fjb>+G|17~VM;&kDy!bk6ZV9~i7PxUH7>J;r|D+&m-bF{Dezh5knWw7vd* zxjN|YZ(A8`3;WON(qn9ww4?0y4jZ4>rN^-wj|2!o-;DPD?X<4B`PyLLAzd=t4e_V@ z8-w#1;crX*cXM->V4FSyx3T_s2Y-AsH%KCY00IagfB*sr{GSy_{b<`o!Ot(vUv62~ zV40x|UYZ9VZ|W0Qzpqc+Yt5Ca-D`8NtL^9BBQ4&^>g_GOx_#2Z&vsNDyiN^1!uLpi zHfVeBo+J2h+c&k+OMS!E`Q1;=Q-h(!Z(8kMhx_}d+ga%?e&71L@rkR8U?eG5$*|2u&UZMJpcWpiX+m3L% z>#ybiYOzAMTeM-`&b`CGgIZLucbKIe2ZUMq$^n+=C&FAY$+oAi+9Rwp&$IEgnU)z7 zEmLP%)>S3K+o|pr+pl5!{BS>a{L&p-)-`_~9nNpq)y7jM92U;6+2ky@OPHxUYQIXG zUvzD{`r7eYO#Uwv?)LdlcU?Ad|J6VF_g8oOx%)#mztJY%_Fi5_TG-P5?|H%X6O3nT z++9b(d*OEeowP<5boid%A6w|(0fPC%G~e|f>0fZc{0!X@-TnMP^~rR1P`?g768?X| z{L;wyaE;eR#&iAg;J9fzuDiacgyY+;e?0KZ@Z-<^|AYRiTWLUXyPuo*?}pvui`!b` ze-g}3)BM}C;Z==i2Y~*Ed%Swb|2&vKU*qm^Dfn7WFkZfOFxz~CT;cy0&ey~bgF*k} z2mildJXH^5;d_36EZx5V!ufh!bdRGQ{Syz?zeM*VcYAFUj&JR~&@1S=;iFeLzP(o} zH#dJje>~`)r}YfdE(0`PBZtd19(?^Mn4h8f!SxY*oDhzudhYStMm_Ij&7Uua$F(6( zUj@oe)N17X}nJBaqE9R?9<(gK90Sr`Kh|SuGInR{c+Fh=FM~u)EfUh?9;>R z5LwT6;rRC6Wg2()uXg_Ru!*;wm$qM;I#~4Z$AjA?Q%@M-*UJ1&dxYcuy|8PG>0!~{ z@3c5vSIcef{Z3!CI^Nz(_4`FX+IyRNi`^6AMjPAvk3QvY;Vs5fyxHyoY%#vM_lA4o zZ87eD&Copow;1o}x#zQuhW+Q}PF{w4;Mr(Hd;eK{vAg587*FQ2rI*&Kn}08iJT3+8 z+6VWGR>yySzp(jRd2i`D*=RX_pPyeRHh*g`dzX!8+UM>DqZ*Tv)(Oqc6p#Rz6B;zOf<7q8sT5)!g@vpXlFDy7{+zUgYuW%Ovyrx%rXD+vX(md-(UOpnr|76Zd@H z+aGUjod^2kt+@>j=SR=V^UezUw=%AEMjxO55YG44vz6`Ro);>T^tnZSqL0J#{Q0e| z^C``bKHmOAu>a4m^Hu(J*xGgF6;3NC z$ema)D|d3iWY14;QvUSf+-U{VXG|*grWfZIPZ*PX){H`L%=m&a6LW*v-q?a^1>+`6 zFD{stTRb^;%%rJP3Z{Fxxnrm1j+->~to%v2V~eLwo1U9LW2QG|>g1wH1;qtp`z8hq z=->LJ+`HdyZOrJH`eOj{7=M$zBPIcpxH|)Hjvwz5}kpqt%nmhEkA-TE1nkN;G znc^RHTuE5uj~zR$U-((LcL=k7gBPAnSOy0TKM9T)eiAM({H$Qww5ii_C+AO?5|aAd z{_s)5-Lb|`ojP&Dyx_CxQ*+1XPZ>L@ARG=39DWiWV#q1S4Lo-E;BX*3Bmc9bjyoxL z=rEngupuM;p;5;U{`o15@p6wEar~fxBXW<=$~tlAsN7Kl2aOo&pS6FeaE;Su_(uwG z{0{lki>6JOQe2psdDMvEg9hjB-*^AM2ZdKq%aPpNqT=z>3i8K>7uFd6vtobU!$%#P zyW!IDR})+~qw>!R&&lmDuAq2&QNfrAg%ig3SKqWTMYE!<{cF;{5XX%7FUI@{lQwKU zFj#Z$nEc`~<8#BMyG_|6hvy6&HPjoEKWWm`F>ch>?#qtx4_RC=vpBk8(uA}8e!&GF z+&b=r59sS3CwJWBsZ(n#~n6U)$|GDrug@ZR+nX0u-i+^ z^7jcYA2+(b*Lv8gQ^xqD{aUsc#JH6$N+k!*O+Q`!N-*EC{X66TXulz|9 z&WoJRhMVUA&+F@-SaJSYeil!&9Pd7xGPSs%@3<*5`kpmo!lbeLP8jR;4TlB}8oqCF z{x~n(Ha>s)c(3o+SyO_0SD3}qY)eT&D-XF0A%Fk^2q1s}0tg_000IagfB*srAb4Rt)9*_xI^x0&OB;XSZchaD ztGifUkT>UoVCyEDvd27|-*ZzNnB8snaDH8flfyn8F3$;bQ>AwMS-Abq8k`w!bH};p zoR#6BX7;r4I`yehy8daqj}7XZvchwGgEh^1Rc_G5N_DQfBF zZx%U(^T<#ix7n@Jt=si?+g-Z-UES<|xX**^aHIJ<*NwORC!a4{l^;%D@h3ZP=e6Is z{a63w-_A8k*Yn|a2Zt`v^L5+Zy3*8duNgMKy(auIGe5i@qUYoGU!OPHde+xTJ}KSB z^O~F6CE5DFIQ(#Kc%EsiEL+@o!LknZ|C@65nDNslO!xNkj+;2OsMyQREu1;MIKMb? zpcWq9<^k2ucGrXrF=^_U{7J)q#&v<({~pQQyKk+I)$x!tJSKQ4dv|7-7C z0OPEx{J-=8rBEBNKzS$w6fBPg?Owy!?gqOBf zb|cDKSjC_&6xT<5tQ33&QK>Gj0as;l@gG^m0xNU{p$O9dIrp41^L^jUBxxz`y50{m z=ey^gd+xoz@7{CIeSBXplPApmZv209k^d*l_7B{@%wc)K0gf2Z55DtW)i_^*#Hzus7xWc*LvaQl$#rCt)=bbA-oPZ(lXm_F}YabNq> zFMLJPWwU!Ackdr2ZmoOX_w)DPe(SAuqV7#oDZiA96-s$l{)nE}o6bio&o+k_ci0MSNT1PW-b4djgL+BAJ)(JHN8Uc-fMnEH= z5zq+yg(6_vKa*N4bp0SVw_B1ZI(ske!&#Vz5pdr_{_^A$Q33zuV(%b^F=<+{I`e0D=;I`A7=h48~;7b|LBMRUVQ>Y)$h9?1bp~6!WEi!v7reKWuA9_c8xg8~;1ZxA&VL zG2h;PA7}oQP0xQa-`;+oXTJS8+xeHUoE=Z$+v_WQJD$R~>o3ouU--xL(Q}k`A$nft zbYj1SFL)m=T|7kJ5wy3LmCU!dmou0z{Xz0OkNNg?2K%>3NL#AG7hFWWK#W?PvZCHu)EsZy%rDV7`5PIxbG_$38x-V!nNR zI*a-C@##Y5+xvGh^X=o4oB8&BznS^={%z;~`f4h-y*&$Gp2OkNr&q7f=&$~s`SyPE zI`i%A{g~58kG&r5ECrJ9Rmyo^3G?SdjoE4aK1mknG+QH}5zq)|1T+E~0gZr0KqH_L z&9cMbpbc(6t4U%^SUVBG<$_?F|60dHFB9hF8QSUd}|3xb2^zVs^`00G}wNr&-rIRb@-_bI2Wi1`)GWeV8-wWFRGr~dA$ol)9 zrL@mr)_WxPGZ#BlGAEy^u^+CP6#Q7Zlp_-E?uq2b2lERlq5v51&mh^^oXRi?hT}sd zQxJ~~4@L1_-SiGeP)|}&ZYF-N&P&X2{=ToRqUZKU#$W6n+e1mV<5lbPnc#He_fh&7 z(_E%brf#NFOsAR7F!eF*Vmiforc zF~EN~>}~M3?^o3R_dP>&^)Zht{iu8)_HUNYElo+jT+XhC$(|UKFDZQswtokyoYSvZ z``2nL5L`E3)J8Y9-`P3rpNP(7|N8$SRc|HF{XLB@$_|rG9apAO_|B(ac(=z3eUVT^xTJ)c?5fA@Q-9UORr@fW)%_K+3qka!O7 z;dHyNrSvH7)hutJ|BHwEZaQ$)^9$%`aiR+|o{aKEw7e zi~nb9mty}We?sl~B&fhs;C#S8m0{ok;~k%-_P^sEqK|xyDAsc!_Akcg38y4qF6ZiT zYFEbieTQfN{>1k0b*udo|L@@}{-5t{bNhd2Kj*T4<3FH!i`^0bFZSrUs{Dw@o4Izp zzD^|{WyjC{?}SbYew>Mv!)~}z{$FN$ma=z4q4;2A#N;}X=3F}eIK%nv+f4PIUCj8g zolDw3F3+gMb6C>t^-}shOg&8dnT|5u&-5VEWp$Kp2Zwiy9GB-H)5n=|w)2vGtj8|+ zIP*Q6?=lXrWxASa2~$a5!txT%M*CT=o#|SptC>o^l70vCC48iG^eE{$()^Ehx^~9j z&Fyref!b>}e=lZhmtyaHKPCHdJQVN~@Q2@3#JO+DX{2v2)3s+%7@`(r@4EQB@1*3* z<(wR)_H4LsJS=pdXlVk-I0 zN2f)lu!CtXQzuhN?i6`pWT9Ow=VR(%n#(kqFQ*@8ein5LX2*__o&{(4 z77O|qe>b;N=liL>9+zc(uglgh#ol%OjO-nni$6hUItwUvy=CW;zGZ8Oj$cR=s|Eg@ zbUO@YyiVeC>=TkNmve6p+s)~~Vc9#+v1I2akI8KBp3P$KrsuGC7eLA9^Y0FpP`#zX z(*KQrJUaHybMM&-zrAeeaBbCmjSGIvlyZQ*vly(83oZR-b;Zt@OFKf%<=)W@`o=?v3ZrU#hDn2s|wgmS(IxO~Y9 z&T}(mOE2Qn%y<3+l{eQW=dgt(orC2ioQ?We&Mo>S9aG6y(#IrT7)MCEj*^}u%>Jk+ zbMtd>b32__b)IUk%az4S&(3FSmtyZG9w&Q;v)~tDPIU+FvWcm^Cf)IBXM0{y-eLsUPK&wx|ra{2_!w(JB#5;**gnc zvN2@eRmv}REGfYI6ItVB{f_0t{u#D;uB1=uJ<@cnSM;Tocb<6hMDabM((UJe$5+ZFd^0_1@lxZsZB$PwuL0}2=4+Jq zQ)Hh)zNWMYgk*05<*$Riq{Z-ue7vTHNpH@|Ps3*V_&LWH{_%Z>UfZR1ZSMWp$It%w zF>aNWiZ=}3p?7^}_|R)VDZuka9?+vUYETheml82d6d5g9?}ryf3?ncyTI?$HyNqyg2_1h9ujiZo zKzv&$5;2U?(BObS)*n_mSbpsLQm^Nh@OwQLF=O&u{C(=7^!yf7&fSNZ-{)EWz!Bp4 zO-`S!T_}(;{5;C(%ymXFX)j5;$7!Et`InfQR9bK`@l`upo=5F$b?cF}YY(Se$n!Jj zeFo=yxVYpUVY=86*6(u8PwFSnL!N`VP8iq1^1ZfnB9|@Q4J?2AVWbnCNbftsbdTE7 z%~!u?Sbldpo~k&9lof1A4$h^B6z*dep@?7{E5R-k9U$-a$J0kC%I{T;`W8j3f*L^#Jn z6fer<90I-*jhDQqcOnJTeg%)T2+hz4XaqC@8Uc-fMnEI*7lJ@GeUaf3$XW0cg z{8e^b#qUh68|9K;S+dBjB$wSu{@U#6G;_#y9`(Q%NsiSg$qr6G%5*i;5~d!edztQI z`Z&{eru|Hliuq<)7QS6h!n?D`@5#cKblK=Ko;PHR&&EH<`R!-A+NP((7S5(;KK|OH zq-VZ-{#@~E880`tQ^_|u4->{bboLjmYQBf_MVw>cJP_w@kMq3pAkz|_M?#eG4*Z$t zl6hHDjLRwWoAE5`wn_Q7@WyT6~C(j2Xgb z@=HY1GR*BxV`XBjj?BI9Cusynu@kYAZ2KuTdnf5;_`8U)-vty)_Io5=^htOx%S(8g z(@j55>8F^wnRYRaF+IT4c!A_|nT|7mg6S;lJ-}4*TPU4jJ(H3jC!Do~4Gue)<}!6M zojJ_>om?LW(}kAz&C0p|%=x|9^3Ipu$N0P1;>?b@RT5bbb*v$OQo>WVcA3=A{Sf7_ z#mV<0rcZDy5%&tBH!UZc1KcxJ4E&m#KqkR{1rw@1M)mE|)AHN4T@d z+0z}Z@R8={^^E=(!RU0w98qx*EpY& zy*2a_{i zEO@Wsc-h~Tu*}=nbG$t)11$2~3h=~J2U7UW3siYGs{CA&UV)6UOPo}Wsi+E)F zvkietwO!v1)+ab*(}jGqzO?UYO2L`>Y<`1XzQ86gczl5C`&mxEo2l%p&R4#y`FxA} z4^vzAM!{e#Q4kyoj1|N}1+hRZT(Cah3IDkY`O7J`1X91-Y|qET`p;(hdCR-Nd{gCv zFCp{>x`)ES0$09k9TqZU3Ar5h^6h87%vTp)zPa;FeP71%*=ZYUI;auQ2xtT}0vZ90 zfJQ(gpb^jrXaqC@8Uc-fMnEH=5zq)|1T+E~0gZr0KqH_L&=rT^DguX(^6e)8k%ReTPMylRVY z{8mmz`9&<>+S_G`Pv!KNarb8Nx&Iw6e=QT9|5xz&%Xo@^y``Kzy@x7)mFuN9pr21& zNz~CrboO??rGMrIh%a_vXg~G;gNNv^?kvrdXe(b2FZj58|T=5Tk;LyihYf_ynH! z-7<{zIMA6HANSH;rO1vCGR<~K$|Lp(^e#fZC7gbi^`o6K>r%?$htNw=AE$FN75u*P z->LBf+ux3R+b!db=$l#ArucV0XO$QJB)2n>m*-}d$2j57u)NS?IHCJ0!pG=M;iuxh z#O31WWk`Fl<~xpRuM>dgtpcb`P#vh>R&j$Dsi zF0X-|4X;V6&$RG~Yg~9I;Va`0&yE>QhH z_MQ~JDC%Q5PG-n+V6$=v`IzU5zkTNoB7^k2FRcH%IeZG2XOi_fL)LtzB%f*O|56^~ zJ_`4*Fl$TQOB*+r2YEr(H+u=mE>9&%yTkr9?x*rN7Q4mO^poBdR{vARnOU~KlE27{-IDtMayyMj{i~^+u7w0n_?wpg&2)nGz5h{a z|Dw;<&m_Av+I2eVAKgsjg?JqgkGLH72Md`-5 z|4KKOZ04hgM=|_$aeEc}Yn@z85(+ho{(Jh$!m9_9xesa5DgZ zcK<};r5ux756MU3<@pK!6pj$;VS6I+?hRBgEC*hPCN89QHvak7-hk&I@!4oAr~eF7 zk%O?SXq~f2$|H7ml5rP%CH7eI7yDDj&->6vs607P9&8uF&qg$yDaut$}9fiEVmbPdGWalKO5c8>0V?iauUu`(lquBr+oiHWUl@Ce*`Ep z{!X({(;5novz;-ITPP>v)sG%|F0K|C5~Ifxn}1NTsKiO)*?v|3EzMkH_PZPP|~LuaEUj-`%SG zR-r#Er~E}u%E#FnzGS@#(fThxubzj8`C^~Cj-`H#>9K8!-8q&OIYc2-d0rytX8xPC zPm;|HmrwGM`O`#IzY>|fm;9?`oM3u+x4IjO(tJ?#2tC01Cf`QS`_tUOCXE*q9->F^ zpT3WtSNHqMl?o^0zh))L=dL6@#sefb{SrT4TUhi=^pijG>^{O@$|cWL^4-Dt;PZSb zXc!-qcEt2cOb50E`-j4D<3gi89PA&A1mcbgWF$fZ@z8)#5Ddl(`Uj(-0ue0e4i6M` z2jcxkWUw!cLxd5BBJ-^jM^lY}MnEH=5zq)|1T+E~0gZr0KqH_L&E8%Yd{@kDSt8$hwo%wQ5NjcQ7Vfg-#qwZ$!B{^dmMdUKQ zp5>%|vky>u%-;ubZ#yw_{gTkIo@J{@&dFgZzk~H-KB#XAhb6!BIE>_N@VA!n{Pfv` z|13tpbpnyJ`WjR{!;vkX%Omk!&ryQ;@Q5{7pTBWx^IdNx9)2i4KF{~U-!}NOUz7ce zhwV8^eI_e=RFY)9puA5_Jb2mkE_yC|T!k;a0S|4uo2Z0+!e=>=|2v;kwX4`_Y7gUU zh)$kn4bPlz4NtJXYw9RHqK$uD@1pWac>GifW4p#bd;Iiqq!*&}r4i5wXaqC@e`W;E zhWzSn{!K0#Yq-uLLdF~Jm$9GV1=W*j{Qu;{Jdh9p| z-(KEjjECgEn%g1zV0g*%H>Yz_6#a7jgmvrBf3|li1WvKFXVJHq1>H==4~=nH@{{(9 zx?CKGMEvn={#$GVJ!gq`b6Uw)^fTK?;?0alzenvrrNctl>L)lP(?9;Rz4+|kX{+x9 z3&?!sdJcP7Zi>TF9_bdStB~W6%vWD2-+@c1y-qZfKbQ1RS=TJ{otF|mSaDj z8^%3gZx+%%%Q?Y(?enV7uK&7u{@P!3uJY$EJBR4mKR)hHs7l)Nd0!y;yIeM9pSYa#jWZSfjx~gr z*sY1bDSXM4g<)_z5xr|69_KZCUg;*W-xKZs15qi5?>?&EF|17FrT&sfGR%7193kbD za*KXRC+$np=W+zj;_=qa;XVII_zSMWx3@1zFY(g8u-{^R+kbUX;p^aWPx9q#(#Tg9 zU80_&U2aD3<8sYzBs?!1CV9J}=`i)cp!M?@9CP!iR6 zfy#K0TS4vP1g6Qt%6P;LSNy6)EWqt!JSl&eH`4yau8JKMHIv+KC2TL>AJ)_OX_rHJ zbDx+fqUVbICHNzk)Q`i`?j?FY`irM8RQ7c8--*ijDCyGlJ;dvEB4DEjU%Wu+6CA$# zIN4RH%X^E-zm;Uz|MMgORPwX8->|Zu3{Z%iiAK^=?O*!=+?Yd*^C;6tMm{|T2D^IHLbv-8MD*F9m;FZ+!Xx2F6%oLih^TOV<9 z1qHXY9L~1>#L;S=B59E~-};ET94IgONWS84J6^+B!x+tBe8=1F!{;FDo8veMf@`Xt z{Bj;1jJq#iuj=0qu1*}%_gqF{r3`;ok>cGeUr(~|W|Hv{Dv@?M;oIqS9hF1c#UzJU zr3BrnTPOwxu-^EWHjm`=!uN1k<^iH>=8N=PXMEQDV^>hv$M}f+t5;M1kgzd9ca!5* zD)EA=G45aiqU|3g>bQ;h5+AX+(wcg zZ{>8iTjvL{HsXsN7}`(uJ4nsrP~A1w=YN24k#ugBmwZs3YC^vvFVxHB7yZxsR-BIO z%d=U^RdKDXKjgr3#{aG>FS_;Cx=Gd}c_D6i^>64?ROVpFmwU4vlJbjvdfxZD3$Pp> zPCvo=@%dr<5_zQ@exzLq9p!Xxrh@lZ{@ZF#cih`951?zYak=Ub25$tonLpYuJ&+i&4$_k>_cYs` z7cW!g5&pYRqIC`Fqq3fSKkFBqllb%eXH_%lAGwg~*TwlA;QGlru$05#{D0Lz>mO_5 zjD~ucF_+{_h086VuCe@VYAHhl<{{Jnl)n)PL;{$gWB`Wc^=o7kmWA zaV}Rs(|>qz{ij7IQo*n2EW&T!;D%F>FV9!ndrcPnmT@_kFum&1c)K}0-kb50{Ts{{S8j{;V88~$>-m`^$=!fxYrZ@Gn{Ukspz-2AME#- zPM+UrE~Rh3;o^Tnz7$vR+s*hrv61~HUU!-43Zl5N`@WDy?>W0}#;B2UHvnLornOf{PLS8D&_KgxPD zron4EK>dE2`@QrFiZ#Wv-0oTz*#j1Eo*Yqkm~Q#?Ni&tiQ|yM6zvh=@-$g%`61&Rj zcM-?ur}EhCyQDXGJV0sHPAFM2zo{!jiqpk%OGF>bHD5*g$*4RbLRR=yrl2v zdUY{vV%o@bil5(sF2q^@-61Ze+eP^P(v9Yu} z*jP|>NpvY48(_R5>o?Y1U0ZN9wNltudpEa3E~h*fiJxTt$vxKgXDdcp@YvtKpm3M^ zhpMUlNxSz2Dc|Eczt7ynDf&*_!q54v-SqHejczF0rJkS7=-f36GbweK6$ucxJ)kO2AS%c94Qmst?* zPvm+EF4*5OeID~AJi+<-Bwud7;|&3_gKQ`k@j}F<<;=I`C-!3ehNy~>x?^%D{2k~c zoE=PajDnHCP{EMF`4vQm2l_(!@o~0i6HEn5X}@ECL-~L5A72PyMi=1^Tq1wjGpDF= z>&j2|D*pJnJ&MZn``N1$e-dzW40!6Czg>R%ZW@q{(dUlzDZxojf9lR>RC?dXNRO1?2v9oFBk6@YIR4np&#ClRvEEnP zE+Icd3TmU0zg@5JpHtp8*jFxp%f!=4zr>5W3GUZDKl-hTmws{O<!{!(5yk0YYr?te-? zs2{J~Z(7U0pVLz;{Ib8?;g7Xg+T(%aX*?^j`3+LuB(BnaS+1X{kLhlvA}>_RRlJDC zKjG(o$k>B;#W>yQ9Z#zMAo4R@uPKgi{q0jKUeZr&yjCEfdha5A7A6G59(?25q+jAE z?-pXf zPP-rLe}KahZ`U))@hg7dRw7CLS@QFRsr*wnlf1MCb2-EmB9?NG?Mm*kTHFOl$k)IN^QCqG|& z>q+z+*$qStsW{h%pis_1-lzxEe^f5F~x|_p4X8I&k*9S@dpKc-gA51^^w-mmE>Gzqw#B}V# z6u;{uME`~9e==SDQQ{XeZD4xWZN%Tl^xv2k{yp)7OmAoU9j0qPM)8}NjxxQQ>Cc&- zyqn~Tn07IZG5tEz*OSsF2^f9Jq-9d7GrXOH>57VzR{n5us?iWm#euBbh zGTp%RJxm9fKF{>@J4t@xE~0lb{Swn3GyMhAMV}(MR;K-*q3{Qo{yWnX{*m|_n6@+h z3e%r5J;?O1N2c%i3h_f0YL@Oi%qfrE@ZkFuj@S<4m7ty5buozlG@sncl|qM@;uKeb+ZhzJ+NY)2Ep( z{uafrXWGv6YNj7%`YER0VEQc6yl+$b%b3KO+A5Oe>hSGJS#R>r5~BSCX%0`aY(= zX8K2_Z~rmLo%blwQl_;`{Y-CS`c2I07#PsZcBl$X}*E0Pv)Bk3A`cJt&Og&8fO#7I=m+5UxpJ2N3XOw;|(_W@O zew_HfWorC8g;y}GWZKDejOpY5L2|2}B%06ka;C#fZ(({5(}$Tp&-AokQu<1!-Ar#} z`dg;Q{XddBhw1)bQTXjo5j~&jCZ?Ze`ZUvmr%BGkG|comrp3Rd_(rDVOmAa)&odPN z7}Hs%$2?2?HB4KWZe{varr&1zQ>OmsDBUegKlwWf|B$KiJnLl|WO@zLPceOo>B%pU z{H07=nf^0V@ypJZ8^`79y*^ogJoMVGQ!$F?;e5CV*7I4OR&;`;w9guk?;y)(;O4--)0M|N-z(}X?*|c2;p-&hhxYM$(D!;dOV^c*f@UCd7ZLF&F`x+Z6TPm8H${?=JS6kuP z;7U(dTVLUAsjTwj_d)#S<&71d^+ja`@O_nn;80+!AQmbJ215m|b@|SGr@9RUhJyX; ziweR6!N_nAM1xo)+#LtyRT@hDGJGtG;D)M#Gc zz_6-!AZ}DNmUmFap+04pb6zwS4)zz6PY$_WVEOR(+0Gh0BWIX5EH3h(T2Gh90@;?xCnlovsZ*)BWI3SrJICLw;yf z=_%ZBVSX^$le`a&hLkqNAB-mNy90^TeImv0iA0k8L|{niae>Z6_x99%U+R7+25F(# zDAN<_PTmKGwj}Qpfs}k?dw-H2*&Yof1KY!~)cqhLMM_giJCi(9{@vRPlk7yd>E6OO z1x8Z}10(bhilOY?QOI7Q4)q`iklhMo%yb^tYF!K|p@^1^7ltLg?`EAiu z=|)XLqrp_|M@_i}!o3gy4+~pZAO@{X?uV1Ny^%mL8Hg6qLr3{&IMq_4v2`h#SW14d z530jDM+Q^;Z2>4K^S5zBLY>=$&3(J7y{ZiCSiGQrFd8Zt*b?j?3dakg;el{)aG)o! zPMI2Yn{F$?Y@qe1@Rrxt_^WH%(Z+7B0h>`>Ta&>;pj8Zp3kqHN&XRmreqjM7E+_>% zO7;$oMnP?%UwHG-W=(?22In`%zdh*c(=cHW&#E^cA?!7TXdVo`$AoW%zg{ zUfIgZrn)+>zpX)@pfV~15anr5g&Vg4mx z{SZ=n?M#D8ax7p}crb_st@eK*8WuR3w&^XQpnR~8Em^(F=f zVK9W2;wpqkn;}Q*Xk!!33bCg4oYzZnYG=BKpnj}Gi*l;WsN<;$Pap77D&HjkeX!z>}dxj&S=0JBO z};xyw~cLb z1q)lpnxoZ;&ZbR0zNSr8oo$^PgPx7sYr~_Zn6|~UX`r#KxX{~HKi1K{6?D2TfppRC zFv{1Cbg$d!^cJePkzh^BNOz5Eq_d(FMx91icTHh6J#M8~>K{h_9kBJY;k;kpV{ z_IP(;_15Nfo3?hg*J52q>&8a61*4_gS}R?b^frxl*ACP>-y|Nk{A=T&cS}cMOKHdz z4|hh3FyFAZuyk8bMJd&3-NuNwa1`o)#YlHs=~k#uq%#_ccedA!bQG43bvBh^`jNWk zI;h8-b+^dD^XRUPj*fJ+HTr_l#z+wHZ|;i+J)1^?)mM~;);aNcZa2is_183e{b0uY zfoPAvuXnW687Ry1X1A?(=F$ zfMqLj6crX;QsQtGmgE<@O2O0>=ewLPM`5ASUf$B|@itVHd(ke}m)BL5MF#uA1K?D` z2RSRe4NbrZ#pA)qU>sh*>UW$MhyU5Zoy+ZnlFmD*nEcR%4J30UV05e!rm=&@1O(73 zVlwb)MsOZs)9`538HKX*)T}5L%7f7;FEJDvNDQhb2a`YloRgitAdbc`$xC&k3@m7D z*&c)PMpT(9pm){698nb|<%gBU0t@Fci#>3~TISb4>ICUF2;c;^>xH3zCMO;iqN`V_tz;B*216el)4a zVx>843veFiu$Su((_YeZ@GC2)Cgs>v%$~1-9&=e}z~9o8-_l&2S7I`@HC6S}Bw%U` z*!t-amw}^9NH%peHCNS%di8|qiEgZ|Z)vWD88sp~U*iD?Ep8p1$zYdjW!W*QB}`=RBfZsx$aWI-6$ z!FoznBQ8{w74*fR5P?W6JP;~?ujd95uhD#TZ zZz)*QI@USRIMQ7>y4~BhsXthVvTwp;0&gN1?R0h*md1l)u5bsG!MnY7)Vrl>%)6z0 zxZBg(Po6<1cw3_l9c`|N^701eOsDG8T~pfMSp^;mlo`|1R~&WkXQ^>P1(b?u40dH|Tcz_9@W9{3khU=O;d)o_x#hqkG5&a%7+7#Kge2L9l1Pr#_GZSr}`JHW$%=>UEaaDxjM6uTGQ;{ox(!Gl=V9Rz2dXA8>E`8CHCBPufXNJe8asqbqBwWXgXOlqP$FKy-@z6( zm@OW;ab&YhJ2DTZYMHEKr!_3!&Vp_%-wZX^dQ{I4rTjsZE>CFXKj|3JD*5w!G z!P0~uM$Gkoe_v@RP*hwP1Wy?kbmy-N=8iz(b5*OfuJvizW-YcP#CCA_iIe`y;bjP= zq2-dpOO|JHUKW@#3eQr3lk&dQ0u!uL5oTuZVSzdYZu`i;@Z_fc2Fv-d3?fNQex13Y zaG#X^LDEJ=lc%b(wz(`64FuyxjjzTJi(+LUG*S?Q&Qz>yoWH!XwYf&+WY~!~sD+5B52%iyN2t z%Q~Z5!(NY?=&H#!EI1@Wm3@WY_QtW!wqj>D4%_Vsm?(D+t6?x)Iui6;F+z(6rQ2Zg z9mk=?TL_C3p4K=lcEn(jVmmEhL?GVf>}cOK(AnMyyhfO~SG&3;eLGC-+xv>@!!Bnq z3X2#uCE17HC@zR7eMJqG9f=;#rbsuq1(_%DRcVv>o|@_{0VhoIJ&l7{kGcw8%vhjn zqUxJoUWJ|6ZJ7fr`vOip(6%Ys(Y9$U&{j8sLuTC=EM9CWSFS|oKx@3aDzj_kRl_tb zc4V%9?fmuU#T@c?Q=Y%fD0sy%eXyzbHsOw*b*Qn<$z_@iCKY<`0IWY&4HTSLpC1gs z_|2>3GFwVp1T-y}!}cVOQX`gW4^5I}mrVv|(;!0Lfu{i$M%2U{42Rh}kSReU>}=xB zX6JL{qNz6EhVv38z}GM$f0+2d=k$dacFcN8Y7I@>oo0~NTQTBX)d z!TdUV+UwLT57v*Jf%eXbGF=0$+hInNnq?_7zcqm-HCR|50~w{4h8tHRSQFWz^d8xe z1nbAe5m{I6C>-tYjs{`fxE$7vOGjWRL^HgN*WMG|?Hl3at$ia66-6{V%)X{h>(p(o zZGpD>LA3^t<~*EbO}%N}S>fnm?lD;Mw_&!eyX{TxG0Cy2Z*20E z#bH@HZ?Ix;U?3DsG>4<1!Qn)i%L%;d!J%z|p&nR*MW;*pl&R;nmN(Xx!%33V*_Gs~ zzB>TMJ^~+@Nem9Tll@Uet4XRQ-tbpIhb_yL1qzp`&S+fGoSal*Z^g+f4T?RX_}0W= zEUza3he%)@AJ3K0AOdCfR<%}n%W$R&^TLX1zvX-hk9Y={kWGkNhB;$~A(XR?4GnHF z+AC}PP2iZqo`b)(vaHa#Ze6-nx18RYXXKfGjN~Id7Sj*YlFV*HhVwsa^^Vq|U_>pW zy=6LuT2pBmwV7hRM$l!)hB*C>2yh)u3gs3}T@2QMy9QjiW7S%Hsh*P#i7#koP)2DERvt+#B=*}VP+o*md0o6t)xV;p@g{^oM1adX*d za>2o5Sy~&s<*@RKtK`b@D6gw@-~(`gl`&ESXjMHvoJx2)Y3J2w*@k>n(+m$6Qn%uX zH`oG(6`BH?@x==ipJu0VXfLi}+`@nt4$~*BdE2{3!`<6qGaxw;#|HhT@;EY0bbQTP z+zMEju=lUDl^C!}3!0z9K9BXxNSdbL;2e|vg`|$blGKo`F!QF1hDZI7vq}r+0_Q1X za!pRwhvrRz>lb(;2Ik=N$_8V@IMLxXGf9#%@#ZB3FP=9mi7ac25V6n|L}lw!GY8HG7Yt47e#kZVekuyjNF<4A zRwFF4moGf1n`KuF_-Tt?z_TrEWtwBDv2M;8ojtEiXU`xn%SyDl%vnyH&6z}w3fR<| z&`Hht?21w#qusWinS#2^EiNk=83a?$7l(Jdnq(Ui)03Rmrg;+!SxJE$GB^=t=P=7| zuuSH0!bp3L=Dr7NE$zlw=pICJL(=M}WZ7&Ik?VaUq!MOOKq$BGaphQ?Ql{Zc7kDcg2!oCI~%XMTEFyyW+NNf} zeycc~9nBtauorYHG&Yv#hq)3q4K>p^oYV5~48dZm13}_5C5DDWCYLJvv3;Y#2YcjR z@F-yuw~-H;+OaerVU-m*bp)az78OB676KGT$h~6A(4I#W`)Vnkhe3m6f`gAFSX8y0uKf)Z8xt?_e}lZuVzi zw#4%bbj$?~+gSaMREe>JsTP!4>9KC8rpjnOism1LM+pbhp@^9lF`?(!CLO6h32~;0 zJsgVyOXE=|o=m<@%A0}H&~Pvl-!n-z_V&3qH_(2wM~+P$C$E*|&E;s!noKk5beCYE zj}cptdcmXF(^yqrNwuj!4~D0Hmct-ft@T2dU-1+Ojj*`g3@?h5v9pO!aE?Zvv2v+K zmSHs!8~|BQRJXLEsBUphQQfGOXsEqN@`kq4xWq^I8j7Jzgo@Q2t$IR)W zB)g4dWf?b9Pne|PAJO)!Ce!38hpv}JHr<6&ZGwzed@Nq3CR)~APM4F{MB3`(9c{$} zuzQBPY;d5cGp2UZwc%}tbF3Mcy5Wd(E$y^g%O3VtEclps zBGBI73&*5c-zGSQzVLEb4qv4XNgYo9!=3GLY&(hdVd1*1r>WGk+e^<&>2L+t)s6%U z`}>1YI3jO57T#O~ZxFWksdBP`c z&&MMNIH$0XO86Qsw3~)aml~K z1&7DlHo4%4|JK%;2)#nk*4f|ffkSE(zdcY>I@VL&7^6dTF4#dHg+pMYaAY60T=>{J z?6N07N3q(Cj>szl>HuEa@q0MpzNNbmw=Uqd0`&^QCOjsucA;h0TkZTOci`2w2A>va z?d@2%(PfQyS)Li~Q1kA6Ybmsc#&z)O!jLUZlJ9LRjRxq|1!%Llb>R$Kw|~@uw*4S26mbPHC@BBwxueok+-j@?a~RTG(2>E$FEmfrEX~ zj`r4gPnOpuG9CEiHiA2sWy)`kM}~)@1?o2+cO7XdpI;j-Yp6HoTg-r_YuhxP|3sS1 zL+NDNyvLx#z2-womIG`_UTXQmx_F@ynw`UoW-#?5bxz9+#3q6*F(^SHI}Aq$V>k@Y zPC+z~_8ug#3WD)yWF#~c4-ckC^V{vomq0VAFw3Am8X@Ohh0CDIVjwaVH{}mz@$u$K zlp)X5Fr6uF`U+hp6_!`=Y5o!G84RW=OgG8AZ^Qtu;@TK)s=9s+oEpQ=Q8)F@y9TXJ$53tsoeMh0`$QZL?bI{AFop>*Phjg`TY| ztjEJ3=7SzvgN3d{Hy$84!h;@3%h}{&T_xc4!;6mLd8}ued`1g83g-vsdol6|4{(HtdhMYcUI2w-|MTM_Wf-YL`AT@vw5ezey*qWF)iRpm zD_rW8>ExF;=v*sriksF7{pH?TSetGtTRSiuiCmC!sQp(o;Fm0hqJaoa@93o_Ye$l` z)<#&(QU~^Ht83vDz*;yoc!5JL4B8Iq!hATnI3Xw8lJkNE7+Or6{Y87rtkvxAWZCpE zEd*3U75pBUTSM{5xuBUo);eun!>k$(^VNFG)W5wKkHO2{&`HCQ?EaMT5t)a1e6t_;TO8sSZ z(Yj$Z1%y*)G|jNT&D%2nLD<7Q2+Z%zsDr=o-VIDqs-1zVs?yLHOh>KLj_nY)1>6ca zsN4yMpVhld$*C&5!OijfmUDYOzR^5d--Oer&ffYha>!avz2TeM_jQ5&jFr;+PI7Dm-k|1qKG5yjj)%AFn&}XD`}Pie z6Mai(Q%UM@GAunDVLics6uUwd8S-$}qpN#3%L+I(95K{^diBy-USaY2;9z8M$bfVD zUg!|$WhWP!c|jjmqRRMH$>gHv91E9Zcgic_L>t(OY;S@Fdf>cOek2@E*m{k6chF)M z(F=lCSJ1DVyk>`dkqP$%edUeKwd(bG_|j02wiW!A!&-;6zmQ>?LQKsCZ%HjRHaZiR@i?$)0&d7o7Qf*} z=OolqZYt|d4EVvv^TX?4xH|)L1^!w9pHzp_iSV52H{%0=r(xQ$UC?<5+W1y$9O-YH z;(5e8<>mHJeQB1hlyQHc!z}Y} zs@o4p82E)BFC1X2!!H9=G*s5Y8_aIy`-d)8?=W9n+tiR(Qc_%+=PJIq!sjY1E;J35 z@fxVrh{vHD5m5 zk)|enQfbj(?KakvAE}vDwj^n}*`|}0beSiUR*|`;k}1hKCz2VrR83Vu=QuTqG)bo_ zLla1q-(1s2Do}FrsDjq1qe;S4g4CSTI&Gw8ieF+7ThIMaHQ(gPUiHB|I^A1c3ue65 zUk*0MX^<~Gryrcb#fF2`73Gb*r;a5Xj0XclmNmo-gVmq){HXc3-Qk~5O}Y^q7Op4j z*-?6+?4hI`J%lNT?XadCl(Zg{by|CRnx47Ojm}*LIr(WT$UHe(&y3EULs~zcN4NRB z=-iq!oEA0rPt#l4bXGJ=q&fj=ic75~nNDC9kd*3+*`8+*Pl?V`WJsI-y11pOnKCsg zOXHdtWC)J1KU=&gwYx2ELwFl%l+P;X|0_4bu@rg90q@HzD;x161j7U2(G9UcqQ9&F zK8*@U!Xac=(K=^QLH98GJ-F>BRti7E=~{0tw8Zc{0)6pD_)KaojONNdS+yD|YhwCf z0UONJLnr^xS6;ChToFEpWjcsUPdVHTAK3)=M~){Y5BnrPM+Ki(NgnsJ$YA0bPnx4b{Yo{;4zk(5zXip!$LjQQRTM0?Tq-Q z?Z_^vF`R{#1lpM8 zK>dP>1JB2_SZ4Y}iP@<)Hw)W)9GHn^QX`)@Pzy}tfHb8J7L)UlbR2mLMLv3jFZ<&s z7huFc@|W!`a9PJc)2sFtDbws44QcA*#VM)u4oquHDPeM>nyQ-d$cMkW#q0GqwfKDS z;T3!vn7x$+z6w#}-`Lo&xr$bP(N)34B+Cwj3l4X|%aZVQEcgzqtf95)OU5m-gT|p;jk7QaI3~|y4v)X)~Vft%E1vUp0>}<5bp32t3?-o zqK2`z9TvO-zaiT^cv#=vl^0#-8ouG$I(GZ?use} zWZ2YcN%blnSUK^SN>m3(MYegDDyi6?LS#zdgW1-o(hjpsMqPMWK$04*MQBy4C1!WTs0>z2_TQVxk!3xbIneTP^7 zvqc&Kjetf#BcKt`2xtT}0vZ90fJQ(gpb^jrXaqC@8Uc-fMnEH=5zq)|1T+E~0gZr0 zKqH_L&9BZMb%X9?LNR7giOEeJlpx!n9^R7>9qTYiCi((&~_L zm&l{8_q9BEweiCXkMq#+t!yn4q`KT7d~Il?CqC%! z3ndcaXlSsvCom>FWXOFGK4iUBq|%r}p}6os5qwU{VgJ{3}1-_q_c%tyQy zW&96IiTKlhcYXW$6{{eOxU^#gT=*y!!SBn%Us#NBcp(mWy^Tmxmb(vgQ|-&a&+`v& z92&&Pehy3Dd>k7pT->dVy&TVQ8bcHfNYb^M>FbABANSKgahTN^?_@a%AK>^+EYHzz z8NvEUAESSj724I}rK+B)itrCu*43>Ewk(Dy>Zy+A()^l^*_fLDRcBS4FQ%Ja+F-e}~E=H{Gn z!m-OHfL{c%_{{NPe-EkWZMpAX)U@K5iVrQmaYDmXMTC(|;GjpqUtuBB0 zp`4XBe*lt|-#EG?C$ap>y&!t1{Hk|9v@++_-ig|s-}OBBl*6;DVy*YMr)qb_mW@8? zDg9abF3;-ta?b_7_pJQlqn?}@2%qWM6?c^H%B?JYV)$-v&d)tR+3#6gv(~eCo5NFf z=m&RPI&zBZF;AZH!#gg$@jE%h+V+Ac=Vw>e?krzcyVK>G##~>VcqRk|=5~u9#f{e+SjQdIrmuNSnf2UKb{a5UFYjOU#-T$uPp`Z= z2V-^(&w6&X?0;qi0wy$jCvGhJ-F+(Q(x-qzw!io-v=(R`o|To~0Z=`=+>XRz&(33^ z37l1F7#LOF><#|5{KnT;HhFfwxUE!~w#uEqf~@ZZ9Y2}9_eN;^D{uM?6t*__lIOvv zmw0mCsN8jI`ODMrT*iTY<8{!wPTGsrsoeQn&(2@2T$*|ywL8Ba4a-J< zT3h<)t_Emi-V1)njcixLnciLXj$Nmgm(C1-3fouqc2%46D5CA%dAw^{e0f$n)SVl=R@PQ(yj0&hW*aUCTT>kN52I0NK23*UHi#4*$kA{oJ?r ze^7nIwy>+{CYIl_{tK4;BdFk(mpYcGKSzNQsvumX{ z=RvO7nv6Z>ht!TdJKz4y{qV$Hk3IJs6k?C`mm3eQ+*W;K*)lAi=a!{^`e*f!U;fRZ zLpxuE)-|IZ9z3#-z#~hXw(Exd@JKv2znnPVv$J}c=V3K`xuzf5(>1)JW$$kw61vaM z-@N;wy~c56#&bV~s0qu}viBO)u=6)Bu6+0)w4cQCo!`0ssY5Hl13MM(EC94YGdir#dkqq1Oh`C_$UMhAaJR%bn)V=0LC5>Yk#Hga;tM;UN8kK1qguC=`nX zf}v<=0L0*5R8Tii(Kma+gHwJu3}NaLzegyT1a*n~0nJEVOPHd6^D_7-l3x5Ip+1(g zr)Rc-elF^e^x{Vem2zNyFk~m^3#;KCuXn&7>X!84mkAX=3}x{`9p>vi2tb=jmS6lV zp^}f3U-A)J2lW;!^3Pm3QFDyUG2!6=Js~rBY6N>&xz7cA!Kgvsb@msM^;1opi zpXLW66g!allIJEod_IVYq|Y{w6M1|3_t?_=mXf5<)i%JT{GzX)(e^L?MI>|e>0V5FEU7f z%;mV#%!GSMXJhN2MnEH=5zq)|1T+E~0gZr0KqH_L& Date: Sun, 24 Apr 2022 00:53:13 -0400 Subject: [PATCH 136/154] Remove test code --- meshmode/mesh/__init__.py | 1 - meshmode/mesh/processing.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py index 382b655db..85a284940 100644 --- a/meshmode/mesh/__init__.py +++ b/meshmode/mesh/__init__.py @@ -1034,7 +1034,6 @@ def __init__(self, vertices, groups, *, skip_tests=False, if self.dim == self.ambient_dim and not skip_element_orientation_test: # only for volume meshes, for now if not test_volume_mesh_element_orientations(self): - #print("NEGATIVE MESH") raise ValueError("negatively oriented elements found") def get_copy_kwargs(self, **kwargs): diff --git a/meshmode/mesh/processing.py b/meshmode/mesh/processing.py index 6339cca40..3d533f1a3 100644 --- a/meshmode/mesh/processing.py +++ b/meshmode/mesh/processing.py @@ -596,9 +596,6 @@ def test_volume_mesh_element_orientations(mesh): area_elements = find_volume_mesh_element_orientations( mesh, tolerate_unimplemented_checks=True) - #for entry in area_elements: - # print(entry) - valid = ~np.isnan(area_elements) return (area_elements[valid] > 0).all() From 13cfd7a9528470a3ab62aeabc04c3cda7e9670ea Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 01:11:08 -0400 Subject: [PATCH 137/154] visualization.py from main --- meshmode/mesh/visualization.py | 74 +++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/meshmode/mesh/visualization.py b/meshmode/mesh/visualization.py index 05b65b8bb..8a2854fb2 100644 --- a/meshmode/mesh/visualization.py +++ b/meshmode/mesh/visualization.py @@ -20,8 +20,14 @@ THE SOFTWARE. """ +from typing import Any, Dict, Optional + import numpy as np +from arraycontext import ArrayContext +from meshmode.mesh import Mesh + + __doc__ = """ .. autofunction:: draw_2d_mesh .. autofunction:: draw_curve @@ -36,9 +42,20 @@ # {{{ draw_2d_mesh -def draw_2d_mesh(mesh, draw_vertex_numbers=True, draw_element_numbers=True, - draw_nodal_adjacency=False, draw_face_numbers=False, - set_bounding_box=False, **kwargs): +def draw_2d_mesh( + mesh: Mesh, *, + draw_vertex_numbers: bool = True, + draw_element_numbers: bool = True, + draw_nodal_adjacency: bool = False, + draw_face_numbers: bool = False, + set_bounding_box: bool = False, **kwargs: Any) -> None: + """Draw the mesh and its connectivity using ``matplotlib``. + + :arg set_bounding_box: if *True*, the plot limits are set to the mesh + bounding box. This can help if some of the actors are not visible. + :arg kwargs: additional arguments passed to ``PathPatch`` when drawing + the mesh group elements. + """ assert mesh.ambient_dim == 2 import matplotlib.pyplot as pt @@ -79,13 +96,13 @@ def draw_2d_mesh(mesh, draw_vertex_numbers=True, draw_element_numbers=True, pt.text(centroid[0], centroid[1], el_label, fontsize=17, ha="center", va="center", - bbox=dict(facecolor="white", alpha=0.5, lw=0)) + bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) if draw_vertex_numbers: for ivert, vert in enumerate(mesh.vertices.T): pt.text(vert[0], vert[1], str(ivert), fontsize=15, ha="center", va="center", color="blue", - bbox=dict(facecolor="white", alpha=0.5, lw=0)) + bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) if draw_nodal_adjacency: def global_iel_to_group_and_iel(global_iel): @@ -137,7 +154,7 @@ def global_iel_to_group_and_iel(global_iel): pt.text(face_center[0], face_center[1], str(iface), fontsize=12, ha="center", va="center", color="purple", - bbox=dict(facecolor="white", alpha=0.5, lw=0)) + bbox={"facecolor": "white", "alpha": 0.5, "lw": 0}) if set_bounding_box: from meshmode.mesh.processing import find_bounding_box @@ -150,13 +167,28 @@ def global_iel_to_group_and_iel(global_iel): # {{{ draw_curve -def draw_curve(mesh, - el_bdry_style="o", el_bdry_kwargs=None, - node_style="x-", node_kwargs=None): +def draw_curve( + mesh: Mesh, *, + el_bdry_style: str = "o", + el_bdry_kwargs: Optional[Dict[str, Any]] = None, + node_style: str = "x-", + node_kwargs: Optional[Dict[str, Any]] = None) -> None: + """Draw a curve mesh. + + :arg el_bdry_kwargs: passed to ``plot`` when drawing elements. + :arg node_kwargs: passed to ``plot`` when drawing group nodes. + """ + + if not (mesh.ambient_dim == 2 and mesh.dim == 1): + raise ValueError( + f"cannot draw a mesh of ambient dimension {mesh.ambient_dim} " + f"and dimension {mesh.dim}") + import matplotlib.pyplot as plt if el_bdry_kwargs is None: el_bdry_kwargs = {} + if node_kwargs is None: node_kwargs = {} @@ -173,9 +205,10 @@ def draw_curve(mesh, # {{{ write_vtk_file -def write_vertex_vtk_file(mesh, file_name, - compressor=None, - overwrite=False): +def write_vertex_vtk_file( + mesh: Mesh, file_name: str, *, + compressor: Optional[str] = None, + overwrite: bool = False) -> None: from pyvisfile.vtk import ( UnstructuredGrid, DataArray, AppendedDataXMLGenerator, @@ -264,10 +297,10 @@ def write_vertex_vtk_file(mesh, file_name, # {{{ mesh_to_tikz -def mesh_to_tikz(mesh): +def mesh_to_tikz(mesh: Mesh) -> str: lines = [] - lines.append(r"\def\nelements{%s}" % (sum(grp.nelements for grp in mesh.groups))) + lines.append(r"\def\nelements{%s}" % mesh.nelements) lines.append(r"\def\nvertices{%s}" % mesh.nvertices) lines.append("") @@ -304,9 +337,10 @@ def mesh_to_tikz(mesh): # {{{ visualize_mesh -def vtk_visualize_mesh(actx, mesh, filename, - vtk_high_order=True, - overwrite=False): +def vtk_visualize_mesh( + actx: ArrayContext, mesh: Mesh, filename: str, *, + vtk_high_order: bool = True, + overwrite: bool = False) -> None: order = vis_order = max(mgrp.order for mgrp in mesh.groups) if not vtk_high_order: vis_order = None @@ -330,7 +364,7 @@ def vtk_visualize_mesh(actx, mesh, filename, # {{{ write_stl_file -def write_stl_file(mesh, stl_name, overwrite=False): +def write_stl_file(mesh: Mesh, stl_name: str, *, overwrite: bool = False) -> None: """Writes a `STL `__ file from a triangular mesh in 3D. Requires the `numpy-stl `__ package. @@ -374,11 +408,11 @@ def write_stl_file(mesh, stl_name, overwrite=False): # {{{ visualize_mesh_vertex_resampling_error def visualize_mesh_vertex_resampling_error( - actx, mesh, filename, overwrite=False): + actx: ArrayContext, mesh: Mesh, filename: str, *, + overwrite: bool = False) -> None: # {{{ comput resampling errors from meshmode.mesh import _mesh_group_node_vertex_error - from meshmode.dof_array import DOFArray error = DOFArray(actx, tuple([ actx.from_numpy( From b1d9bad106fdec4d7e43f1e1142093e3b59dbb78 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 01:18:02 -0400 Subject: [PATCH 138/154] opposite_face.py from main --- .../discretization/connection/opposite_face.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/meshmode/discretization/connection/opposite_face.py b/meshmode/discretization/connection/opposite_face.py index 05a1f0a3c..4ca9e6bac 100644 --- a/meshmode/discretization/connection/opposite_face.py +++ b/meshmode/discretization/connection/opposite_face.py @@ -143,16 +143,8 @@ def _find_src_unit_nodes_via_gauss_newton( _, nelements, ntgt_unit_nodes = tgt_bdry_nodes.shape initial_guess = np.mean(src_mesh_grp.vertex_unit_coordinates(), axis=0) - #print("IS NAN?:", np.isnan(initial_guess).any()) src_unit_nodes = np.empty((dim, nelements, ntgt_unit_nodes)) src_unit_nodes[:] = initial_guess.reshape(-1, 1, 1) - #print("IS NAN2?:", np.isnan(src_unit_nodes).any()) - #for i, entry in enumerate(initial_guess.flatten()): - # print(i,entry) - # if np.isnan(entry): - # exit() - - import modepy as mp src_grp_basis_fcts = src_grp.basis_obj().functions @@ -172,17 +164,9 @@ def apply_map(unit_nodes): .reshape(nelements, ntgt_unit_nodes)) intp_coeffs = np.einsum("fj,jet->fet", inv_t_vdm, basis_at_unit_nodes) - #intp_coeffs_sum = np.sum(intp_coeffs)#np.sum(np.sum(intp_coeffs, axis=0) - 1) - #print(tol, intp_coeffs_sum) - #for i, entry in enumerate(unit_nodes.flatten()): - # print(i,entry) - # if np.isnan(entry): - # exit() # If we're interpolating 1, we had better get 1 back. one_deviation = np.abs(np.sum(intp_coeffs, axis=0) - 1) - print("Max:",np.max(one_deviation)) - assert (one_deviation < tol).all(), np.max(one_deviation) mapped = np.einsum("fet,aef->aet", intp_coeffs, src_bdry_nodes) @@ -251,8 +235,6 @@ def get_map_jacobian(unit_nodes): niter = 0 while True: - - print("IS NAN?", np.isnan(src_unit_nodes).any()) resid = apply_map(src_unit_nodes) - tgt_bdry_nodes df = get_map_jacobian(src_unit_nodes) From ba3f5a925aa0faa6564c29cdc3663715c9aa46dc Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 02:02:51 -0400 Subject: [PATCH 139/154] flake8 fixes --- meshmode/discretization/connection/direct.py | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index daf6322a7..91bc02a44 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -30,7 +30,7 @@ import loopy as lp from meshmode.transform_metadata import ( ConcurrentElementInameTag, ConcurrentDOFInameTag, - IsOpArray, IsDOFArray, ParameterValue) + IsDOFArray, ParameterValue) from pytools import memoize_in, keyed_memoize_method from arraycontext import ( ArrayContext, NotAnArrayContainerError, @@ -727,25 +727,27 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, fgpd.from_el_present nelements = fgpd.from_element_indices.shape[0] - nelements_src, nunit_dofs_src = ary[fgpd.from_group_index].shape + nelements_src, nunit_dofs_src = \ + ary[fgpd.from_group_index].shape nelements_tgt = fgpd.dof_pick_lists.shape[0] nunit_dofs_tgt = self.to_discr.groups[i_tgrp].nunit_dofs ary_dtype = ary[fgpd.from_group_index].dtype - result_dtype = ary_dtype # Assume they are the same + result_dtype = ary_dtype # Assume they are the same from_el_ind_dtype = fgpd.from_element_indices.dtype pick_lists_dtype = fgpd.dof_pick_lists.dtype pick_list_ind_dtype = fgpd.dof_pick_list_index.dtype - #print(fgpd.dof_pick_lists.shape) - #print(fgpd.dof_pick_list_index.shape) - #print(fgpd.from_element_indices.shape) - group_array_contributions.append( actx.call_loopy( #group_pick_knl(fgpd.is_surjective), - group_pick_knl(nelements, nelements_src, nunit_dofs_src, - nelements_tgt, nunit_dofs_tgt, result_dtype, ary_dtype, - from_el_ind_dtype, pick_lists_dtype, pick_list_ind_dtype, + group_pick_knl(nelements, nelements_src, + nunit_dofs_src, + nelements_tgt, + nunit_dofs_tgt, + result_dtype, + ary_dtype, + from_el_ind_dtype, pick_lists_dtype, + pick_list_ind_dtype, fgpd.is_surjective), dof_pick_lists=fgpd.dof_pick_lists, dof_pick_list_index=fgpd.dof_pick_list_index, From aac45560f3b79b5b80b3fa6e1b273490b913c729 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 02:05:08 -0400 Subject: [PATCH 140/154] remove comments and print statements --- meshmode/discretization/connection/direct.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 91bc02a44..fa0b5f33f 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -681,8 +681,6 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, ], name="resample_by_picking_group", ) - #print(t_unit.default_entrypoint) - #exit() return lp.tag_inames(t_unit, { "iel": ConcurrentElementInameTag(), "idof": ConcurrentDOFInameTag()}) From bf862247052e03400dea3d8ae36c48df1024f28e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 02:06:32 -0400 Subject: [PATCH 141/154] remove debugging print statement --- meshmode/discretization/connection/direct.py | 1 - 1 file changed, 1 deletion(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index fa0b5f33f..df33f7fd5 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -634,7 +634,6 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, nelements_tgt, nunit_dofs_tgt, result_dtype, ary_dtype, from_el_ind_dtype, pick_lists_dtype, pick_list_ind_dtype, is_surjective: bool): - print("CALLING GROUP PICK KNL") if is_surjective: if_present = "" else: From 9caa6756cdc2260f7038735f151ba6ddfb6db8ad Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 18:14:36 -0400 Subject: [PATCH 142/154] Point requirements.txt toward specific loopy branch --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b379f17f..a1e7eaefb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ git+https://github.com/inducer/pytato.git#egg=pytato git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git#egg=loopy +git+https://github.com/inducer/loopy.git#egg=loopy@more-0-strides-fixing # depends on loopy, so should come after it. git+https://github.com/inducer/arraycontext.git#egg=arraycontext From dcbc29fe26c9e062086a62f481e7cb4ffb7c4ff6 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 24 Apr 2022 18:18:54 -0400 Subject: [PATCH 143/154] fix requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1e7eaefb..cca114df1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ git+https://github.com/inducer/pytato.git#egg=pytato git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git#egg=loopy@more-0-strides-fixing +git+https://github.com/inducer/loopy.git@more-0-strides-fixing#egg=loopy # depends on loopy, so should come after it. git+https://github.com/inducer/arraycontext.git#egg=arraycontext From 71f701a0c1a909c57cc5b267526854e20f92e899 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Sun, 22 May 2022 14:02:22 -0500 Subject: [PATCH 144/154] update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cca114df1..3b379f17f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ git+https://github.com/inducer/pytato.git#egg=pytato git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git@more-0-strides-fixing#egg=loopy +git+https://github.com/inducer/loopy.git#egg=loopy # depends on loopy, so should come after it. git+https://github.com/inducer/arraycontext.git#egg=arraycontext From 0ba5fd5baa8937371d6c8d9eee93fb87ca57ecbe Mon Sep 17 00:00:00 2001 From: Kaushik Kulkarni Date: Sat, 12 Mar 2022 13:00:21 -0600 Subject: [PATCH 145/154] CHERRY-PICK: SingleGridPytatoArrayContext Co-authored-by: Matthew Smith --- examples/simple-dg.py | 30 ++-- meshmode/array_context.py | 283 ++++++++++++++++++++++++++++++++++++++ meshmode/pytato_utils.py | 62 +++++++++ requirements.txt | 2 +- 4 files changed, 359 insertions(+), 18 deletions(-) create mode 100644 meshmode/pytato_utils.py diff --git a/examples/simple-dg.py b/examples/simple-dg.py index 964e0a12d..84bea31a4 100644 --- a/examples/simple-dg.py +++ b/examples/simple-dg.py @@ -33,7 +33,7 @@ from meshmode.mesh import BTAG_ALL, BTAG_NONE # noqa from meshmode.dof_array import DOFArray, flat_norm from meshmode.array_context import (PyOpenCLArrayContext, - PytatoPyOpenCLArrayContext) + SingleGridWorkBalancingPytatoArrayContext as PytatoPyOpenCLArrayContext) from arraycontext import ( ArrayContainer, map_array_container, @@ -455,11 +455,10 @@ def main(lazy=False): cl_ctx = cl.create_some_context() queue = cl.CommandQueue(cl_ctx) - actx_outer = PyOpenCLArrayContext(queue, force_device_scalars=True) if lazy: - actx_rhs = PytatoPyOpenCLArrayContext(queue) + actx = PytatoPyOpenCLArrayContext(queue) else: - actx_rhs = actx_outer + actx = PyOpenCLArrayContext(queue, force_device_scalars=True) nel_1d = 16 from meshmode.mesh.generation import generate_regular_rect_mesh @@ -475,37 +474,34 @@ def main(lazy=False): logger.info("%d elements", mesh.nelements) - discr = DGDiscretization(actx_outer, mesh, order=order) + discr = DGDiscretization(actx, mesh, order=order) fields = WaveState( - u=bump(actx_outer, discr), - v=make_obj_array([discr.zeros(actx_outer) for i in range(discr.dim)]), + u=bump(actx, discr), + v=make_obj_array([discr.zeros(actx) for i in range(discr.dim)]), ) from meshmode.discretization.visualization import make_visualizer - vis = make_visualizer(actx_outer, discr.volume_discr) + vis = make_visualizer(actx, discr.volume_discr) def rhs(t, q): - return wave_operator(actx_rhs, discr, c=1, q=q) + return wave_operator(actx, discr, c=1, q=q) - compiled_rhs = actx_rhs.compile(rhs) - - def rhs_wrapper(t, q): - r = compiled_rhs(t, actx_rhs.thaw(actx_outer.freeze(q))) - return actx_outer.thaw(actx_rhs.freeze(r)) + compiled_rhs = actx.compile(rhs) t = np.float64(0) t_final = 3 istep = 0 while t < t_final: - fields = rk4_step(fields, t, dt, rhs_wrapper) + fields = actx.thaw(actx.freeze(fields,)) + fields = rk4_step(fields, t, dt, compiled_rhs) if istep % 10 == 0: # FIXME: Maybe an integral function to go with the # DOFArray would be nice? assert len(fields.u) == 1 logger.info("[%05d] t %.5e / %.5e norm %.5e", - istep, t, t_final, actx_outer.to_numpy(flat_norm(fields.u, 2))) + istep, t, t_final, actx.to_numpy(flat_norm(fields.u, 2))) vis.write_vtk_file("fld-wave-min-%04d.vtu" % istep, [ ("q", fields), ]) @@ -513,7 +509,7 @@ def rhs_wrapper(t, q): t += dt istep += 1 - assert flat_norm(fields.u, 2) < 100 + assert actx.to_numpy(flat_norm(fields.u, 2)) < 100 if __name__ == "__main__": diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 259ceabc3..071071dc4 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -26,6 +26,8 @@ """ import sys +import logging + from warnings import warn from arraycontext import PyOpenCLArrayContext as PyOpenCLArrayContextBase from arraycontext import PytatoPyOpenCLArrayContext as PytatoPyOpenCLArrayContextBase @@ -33,6 +35,9 @@ _PytestPyOpenCLArrayContextFactoryWithClass, _PytestPytatoPyOpenCLArrayContextFactory, register_pytest_array_context_factory) +from loopy.translation_unit import for_each_kernel + +logger = logging.getLogger(__name__) def thaw(actx, ary): @@ -345,4 +350,282 @@ def _import_names(): # }}} +@for_each_kernel +def _single_grid_work_group_transform(kernel, cl_device): + import loopy as lp + from meshmode.transform_metadata import (ConcurrentElementInameTag, + ConcurrentDOFInameTag) + + splayed_inames = set() + ngroups = cl_device.max_compute_units * 4 # '4' to overfill the device + l_one_size = 4 + l_zero_size = 16 + + for insn in kernel.instructions: + if insn.within_inames in splayed_inames: + continue + + if isinstance(insn, lp.CallInstruction): + # must be a callable kernel, don't touch. + pass + elif isinstance(insn, lp.Assignment): + bigger_loop = None + smaller_loop = None + + if len(insn.within_inames) == 0: + continue + + if len(insn.within_inames) == 1: + iname, = insn.within_inames + + kernel = lp.split_iname(kernel, iname, + ngroups * l_zero_size * l_one_size) + kernel = lp.split_iname(kernel, f"{iname}_inner", + l_zero_size, inner_tag="l.0") + kernel = lp.split_iname(kernel, f"{iname}_inner_outer", + l_one_size, inner_tag="l.1", + outer_tag="g.0") + + splayed_inames.add(insn.within_inames) + continue + + for iname in insn.within_inames: + if kernel.iname_tags_of_type(iname, + ConcurrentElementInameTag): + assert bigger_loop is None + bigger_loop = iname + elif kernel.iname_tags_of_type(iname, + ConcurrentDOFInameTag): + assert smaller_loop is None + smaller_loop = iname + else: + pass + + if bigger_loop or smaller_loop: + assert (bigger_loop is not None + and smaller_loop is not None) + else: + sorted_inames = sorted(tuple(insn.within_inames), + key=kernel.get_constant_iname_length) + smaller_loop = sorted_inames[0] + bigger_loop = sorted_inames[-1] + + kernel = lp.split_iname(kernel, f"{bigger_loop}", + l_one_size * ngroups) + kernel = lp.split_iname(kernel, f"{bigger_loop}_inner", + l_one_size, inner_tag="l.1", outer_tag="g.0") + kernel = lp.split_iname(kernel, smaller_loop, + l_zero_size, inner_tag="l.0") + splayed_inames.add(insn.within_inames) + elif isinstance(insn, lp.BarrierInstruction): + pass + else: + raise NotImplementedError(type(insn)) + + return kernel + + +def _alias_global_temporaries(t_unit): + """ + Returns a copy of *t_unit* with temporaries of that have disjoint live + intervals using the same :attr:`loopy.TemporaryVariable.base_storage`. + """ + from loopy.kernel.data import AddressSpace + from loopy.kernel import KernelState + from loopy.schedule import (RunInstruction, EnterLoop, LeaveLoop, + CallKernel, ReturnFromKernel, Barrier) + from loopy.schedule.tools import get_return_from_kernel_mapping + from pytools import UniqueNameGenerator + from collections import defaultdict + + kernel = t_unit.default_entrypoint + assert kernel.state == KernelState.LINEARIZED + temp_vars = frozenset(tv.name + for tv in kernel.temporary_variables.values() + if tv.address_space == AddressSpace.GLOBAL) + temp_to_live_interval_start = {} + temp_to_live_interval_end = {} + return_from_kernel_idxs = get_return_from_kernel_mapping(kernel) + + for sched_idx, sched_item in enumerate(kernel.linearization): + if isinstance(sched_item, RunInstruction): + for var in (kernel.id_to_insn[sched_item.insn_id].dependency_names() + & temp_vars): + if var not in temp_to_live_interval_start: + assert var not in temp_to_live_interval_end + temp_to_live_interval_start[var] = sched_idx + assert var in temp_to_live_interval_start + temp_to_live_interval_end[var] = return_from_kernel_idxs[sched_idx] + elif isinstance(sched_item, (EnterLoop, LeaveLoop, CallKernel, + ReturnFromKernel, Barrier)): + # no variables are accessed within these schedule items => do + # nothing. + pass + else: + raise NotImplementedError(type(sched_item)) + + vng = UniqueNameGenerator() + # a mapping from shape to the available base storages from temp variables + # that were dead. + shape_to_available_base_storage = defaultdict(set) + + sched_idx_to_just_live_temp_vars = [set() for _ in kernel.linearization] + sched_idx_to_just_dead_temp_vars = [set() for _ in kernel.linearization] + + for tv, just_alive_idx in temp_to_live_interval_start.items(): + sched_idx_to_just_live_temp_vars[just_alive_idx].add(tv) + + for tv, just_dead_idx in temp_to_live_interval_end.items(): + sched_idx_to_just_dead_temp_vars[just_dead_idx].add(tv) + + new_tvs = {} + + for sched_idx, _ in enumerate(kernel.linearization): + just_dead_temps = sched_idx_to_just_dead_temp_vars[sched_idx] + to_be_allocated_temps = sched_idx_to_just_live_temp_vars[sched_idx] + for tv_name in sorted(just_dead_temps): + tv = new_tvs[tv_name] + assert tv.base_storage is not None + assert tv.base_storage not in shape_to_available_base_storage[tv.nbytes] + shape_to_available_base_storage[tv.nbytes].add(tv.base_storage) + + for tv_name in sorted(to_be_allocated_temps): + assert len(to_be_allocated_temps) <= 1 + tv = kernel.temporary_variables[tv_name] + assert tv.name not in new_tvs + assert tv.base_storage is None + if shape_to_available_base_storage[tv.nbytes]: + base_storage = sorted(shape_to_available_base_storage[tv.nbytes])[0] + shape_to_available_base_storage[tv.nbytes].remove(base_storage) + else: + base_storage = vng("_msh_actx_tmp_base") + + new_tvs[tv.name] = tv.copy(base_storage=base_storage) + + for name, tv in kernel.temporary_variables.items(): + if tv.address_space != AddressSpace.GLOBAL: + new_tvs[name] = tv + else: + assert name in new_tvs + + kernel = kernel.copy(temporary_variables=new_tvs) + + return t_unit.with_kernel(kernel) + + +def _can_be_eagerly_computed(ary) -> bool: + from pytato.transform import InputGatherer + from pytato.array import Placeholder + return all(not isinstance(inp, Placeholder) + for inp in InputGatherer()(ary)) + + +def deduplicate_data_wrappers(dag): + import pytato as pt + data_wrapper_cache = {} + data_wrappers_encountered = 0 + + def cached_data_wrapper_if_present(ary): + nonlocal data_wrappers_encountered + + if isinstance(ary, pt.DataWrapper): + + data_wrappers_encountered += 1 + cache_key = (ary.data.base_data.int_ptr, ary.data.offset, + ary.shape, ary.data.strides) + try: + result = data_wrapper_cache[cache_key] + except KeyError: + result = ary + data_wrapper_cache[cache_key] = result + + return result + else: + return ary + + dag = pt.transform.map_and_copy(dag, cached_data_wrapper_if_present) + + if data_wrappers_encountered: + logger.info("data wrapper de-duplication: " + "%d encountered, %d kept, %d eliminated", + data_wrappers_encountered, + len(data_wrapper_cache), + data_wrappers_encountered - len(data_wrapper_cache)) + + return dag + + +class SingleGridWorkBalancingPytatoArrayContext(PytatoPyOpenCLArrayContextBase): + """ + A :class:`PytatoPyOpenCLArrayContext` that parallelizes work in an OpenCL + kernel so that the work + """ + def transform_loopy_program(self, t_unit): + import loopy as lp + + t_unit = _single_grid_work_group_transform(t_unit, self.queue.device) + t_unit = lp.set_options(t_unit, "insert_gbarriers") + t_unit = lp.linearize(lp.preprocess_kernel(t_unit)) + t_unit = _alias_global_temporaries(t_unit) + + return t_unit + + def _get_fake_numpy_namespace(self): + from meshmode.pytato_utils import ( + EagerReduceComputingPytatoFakeNumpyNamespace) + return EagerReduceComputingPytatoFakeNumpyNamespace(self) + + def transform_dag(self, dag): + import pytato as pt + + # {{{ face_mass: materialize einsum args + + def materialize_face_mass_vec(expr): + if (isinstance(expr, pt.Einsum) + and pt.analysis.is_einsum_similar_to_subscript( + expr, "ifj,fej,fej->ei")): + mat, jac, vec = expr.args + return pt.einsum("ifj,fej,fej->ei", + mat, + jac, + vec.tagged(pt.tags.ImplStored())) + else: + return expr + + dag = pt.transform.map_and_copy(dag, materialize_face_mass_vec) + + # }}} + + # {{{ materialize all einsums + + def materialize_einsums(ary: pt.Array) -> pt.Array: + if isinstance(ary, pt.Einsum): + return ary.tagged(pt.tags.ImplStored()) + + return ary + + dag = pt.transform.map_and_copy(dag, materialize_einsums) + + # }}} + + dag = pt.transform.materialize_with_mpms(dag) + dag = deduplicate_data_wrappers(dag) + + # {{{ /!\ Remove tags from Loopy call results. + # See + + def untag_loopy_call_results(expr): + from pytato.loopy import LoopyCallResult + if isinstance(expr, LoopyCallResult): + return expr.copy(tags=frozenset(), + axes=(pt.Axis(frozenset()),)*expr.ndim) + else: + return expr + + dag = pt.transform.map_and_copy(dag, untag_loopy_call_results) + + # }}} + + return dag + # vim: foldmethod=marker diff --git a/meshmode/pytato_utils.py b/meshmode/pytato_utils.py new file mode 100644 index 000000000..b0724b1df --- /dev/null +++ b/meshmode/pytato_utils.py @@ -0,0 +1,62 @@ +from functools import partial, reduce +from arraycontext.impl.pytato.fake_numpy import PytatoFakeNumpyNamespace +from arraycontext import rec_map_reduce_array_container +import pyopencl.array as cl_array + + +def _can_be_eagerly_computed(ary) -> bool: + from pytato.transform import InputGatherer + from pytato.array import Placeholder + return all(not isinstance(inp, Placeholder) + for inp in InputGatherer()(ary)) + + +class EagerReduceComputingPytatoFakeNumpyNamespace(PytatoFakeNumpyNamespace): + """ + A Numpy-namespace that computes the reductions eagerly whenever possible. + """ + def sum(self, a, axis=None, dtype=None): + if (rec_map_reduce_array_container(all, + _can_be_eagerly_computed, a) + and axis is None): + + def _pt_sum(ary): + return cl_array.sum(self._array_context.freeze(ary), + dtype=dtype, + queue=self._array_context.queue) + + return self._array_context.thaw(rec_map_reduce_array_container(sum, + _pt_sum, + a)) + else: + return super().sum(a, axis=axis, dtype=dtype) + + def min(self, a, axis=None): + if (rec_map_reduce_array_container(all, + _can_be_eagerly_computed, a) + and axis is None): + queue = self._array_context.queue + frozen_result = rec_map_reduce_array_container( + partial(reduce, partial(cl_array.minimum, queue=queue)), + lambda ary: cl_array.min(self._array_context.freeze(ary), + queue=queue), + a) + return self._array_context.thaw(frozen_result) + else: + return super().min(a, axis=axis) + + def max(self, a, axis=None): + if (rec_map_reduce_array_container(all, + _can_be_eagerly_computed, a) + and axis is None): + queue = self._array_context.queue + frozen_result = rec_map_reduce_array_container( + partial(reduce, partial(cl_array.maximum, queue=queue)), + lambda ary: cl_array.max(self._array_context.freeze(ary), + queue=queue), + a) + return self._array_context.thaw(frozen_result) + else: + return super().max(a, axis=axis) + +# vim: fdm=marker diff --git a/requirements.txt b/requirements.txt index 7ad3c51b2..35a924099 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ git+https://github.com/inducer/pytato.git#egg=pytato git+https://github.com/inducer/pymbolic.git#egg=pymbolic # also depends on pymbolic, so should come after it -git+https://github.com/inducer/loopy.git#egg=loopy +git+https://github.com/kaushikcfd/loopy.git#egg=loopy # depends on loopy, so should come after it. git+https://github.com/inducer/arraycontext.git#egg=arraycontext From a7036d78480898bc5993b4656fc684e249cd6516 Mon Sep 17 00:00:00 2001 From: Kaushik Kulkarni Date: Sat, 12 Mar 2022 13:03:32 -0600 Subject: [PATCH 146/154] Adds more pytato utils: Axes types unification --- meshmode/pytato_utils.py | 575 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 574 insertions(+), 1 deletion(-) diff --git a/meshmode/pytato_utils.py b/meshmode/pytato_utils.py index b0724b1df..986c65fff 100644 --- a/meshmode/pytato_utils.py +++ b/meshmode/pytato_utils.py @@ -1,7 +1,23 @@ +import pyopencl.array as cl_array +import kanren +import pytato as pt +import unification +import logging + from functools import partial, reduce from arraycontext.impl.pytato.fake_numpy import PytatoFakeNumpyNamespace from arraycontext import rec_map_reduce_array_container -import pyopencl.array as cl_array +from meshmode.transform_metadata import DiscretizationEntityAxisTag +from pytato.loopy import LoopyCall +from pytato.array import EinsumElementwiseAxis, EinsumReductionAxis +from pytato.transform import ArrayOrNames +from arraycontext import ArrayContainer +from arraycontext.container.traversal import rec_map_array_container +from typing import Set, Mapping, Tuple, Union +logger = logging.getLogger(__name__) + + +MAX_UNIFY_RETRIES = 50 # used by unify_discretization_entity_tags def _can_be_eagerly_computed(ary) -> bool: @@ -59,4 +75,561 @@ def max(self, a, axis=None): else: return super().max(a, axis=axis) + +# {{{ solve for discretization metadata for arrays' axes + +class DiscretizationEntityConstraintCollector(pt.transform.Mapper): + """ + .. warning:: + + Instances of this mapper type store state that are only for visiting a + single DAG. Using a single instance for collecting the constraints on + multiple DAGs is undefined behavior. + """ + def __init__(self): + super().__init__() + self._visited_ids: Set[int] = set() + + # axis_to_var: mapping from (array, iaxis) to the kanren variable to be + # used for unification. + self.axis_to_tag_var: Mapping[Tuple[pt.Array, int], + unification.variable.Var] = {} + self.variables_to_solve: Set[unification.variable.Var] = set() + self.constraints = [] + + # type-ignore reason: CachedWalkMapper.rec's type does not match + # WalkMapper.rec's type + def rec(self, expr: ArrayOrNames) -> None: # type: ignore + if id(expr) in self._visited_ids: + return + + # type-ignore reason: super().rec expects either 'Array' or + # 'AbstractResultWithNamedArrays', passed 'ArrayOrNames' + super().rec(expr) # type: ignore + self._visited_ids.add(id(expr)) + + def get_kanren_var_for_axis_tag(self, + expr: pt.Array, + iaxis: int + ) -> unification.variable.Var: + key = (expr, iaxis) + + if key not in self.axis_to_tag_var: + self.axis_to_tag_var[key] = kanren.var() + + return self.axis_to_tag_var[key] + + def _record_all_axes_to_be_solved_if_impl_stored(self, expr): + if expr.tags_of_type(pt.tags.ImplStored): + for iaxis in range(expr.ndim): + self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(expr, + iaxis)) + + def _record_all_axes_to_be_solved(self, expr): + for iaxis in range(expr.ndim): + self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(expr, + iaxis)) + + def record_constraint(self, lhs, rhs): + self.constraints.append((lhs, rhs)) + + def record_eq_constraints_from_tags(self, expr: pt.Array) -> None: + for iaxis, axis in enumerate(expr.axes): + if axis.tags_of_type(DiscretizationEntityAxisTag): + discr_tag, = axis.tags_of_type(DiscretizationEntityAxisTag) + axis_var = self.get_kanren_var_for_axis_tag(expr, iaxis) + self.record_constraint(axis_var, discr_tag) + + def _map_input_base(self, expr: pt.InputArgumentBase + ) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + + for dim in expr.shape: + if isinstance(dim, pt.Array): + self.rec(dim) + + map_placeholder = _map_input_base + map_data_wrapper = _map_input_base + map_size_param = _map_input_base + + def map_index_lambda(self, expr: pt.IndexLambda) -> None: + from pytato.utils import are_shape_components_equal + from pytato.raising import index_lambda_to_high_level_op + from pytato.raising import (BinaryOp, FullOp, WhereOp, + BroadcastOp, C99CallOp, ReduceOp) + + # {{{ record constraints for expr and its subexprs. + + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + + for dim in expr.shape: + if isinstance(dim, pt.Array): + self.rec(dim) + + for bnd in expr.bindings.values(): + self.rec(bnd) + + # }}} + + hlo = index_lambda_to_high_level_op(expr) + + if isinstance(hlo, BinaryOp): + subexprs = (hlo.x1, hlo.x2) + elif isinstance(hlo, WhereOp): + subexprs = (hlo.condition, hlo.then, hlo.else_) + elif isinstance(hlo, FullOp): + # A full-op does not impose any constraints + subexprs = () + elif isinstance(hlo, BroadcastOp): + subexprs = (hlo.x,) + elif isinstance(hlo, C99CallOp): + subexprs = hlo.args + elif isinstance(hlo, ReduceOp): + # {{{ ReduceOp doesn't quite involve broadcasting + + i_out_axis = 0 + for i_in_axis in range(hlo.x.ndim): + if i_in_axis not in hlo.axes: + in_tag_var = self.get_kanren_var_for_axis_tag(hlo.x, + i_in_axis) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + i_out_axis) + self.record_constraint(in_tag_var, out_tag_var) + i_out_axis += 1 + + assert i_out_axis == expr.ndim + + # }}} + + for axis in hlo.axes: + self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(hlo.x, + axis)) + return + + else: + raise NotImplementedError(type(hlo)) + + for subexpr in subexprs: + if isinstance(subexpr, pt.Array): + for i_in_axis, i_out_axis in zip( + range(subexpr.ndim), + range(expr.ndim-subexpr.ndim, expr.ndim)): + in_dim = subexpr.shape[i_in_axis] + out_dim = expr.shape[i_out_axis] + if are_shape_components_equal(in_dim, out_dim): + in_tag_var = self.get_kanren_var_for_axis_tag(subexpr, + i_in_axis) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + i_out_axis) + + self.record_constraint(in_tag_var, out_tag_var) + else: + # broadcasted axes, cannot belong to the same + # discretization entity. + assert are_shape_components_equal(in_dim, 1) + + def map_stack(self, expr: pt.Stack) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + # TODO; I think the axis corresponding to 'axis' need not be solved. + for ary in expr.arrays: + self.rec(ary) + + for iaxis in range(expr.ndim): + for ary in expr.arrays: + if iaxis < expr.axis: + in_tag_var = self.get_kanren_var_for_axis_tag(ary, + iaxis) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + iaxis) + + self.record_constraint(in_tag_var, out_tag_var) + elif iaxis == expr.axis: + pass + elif iaxis > expr.axis: + in_tag_var = self.get_kanren_var_for_axis_tag(ary, + iaxis-1) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + iaxis) + + self.record_constraint(in_tag_var, out_tag_var) + else: + raise AssertionError + + def map_concatenate(self, expr: pt.Concatenate) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + # TODO; I think the axis corresponding to 'axis' need not be solved. + for ary in expr.arrays: + self.rec(ary) + + for ary in expr.arrays: + assert ary.ndim == expr.ndim + for iaxis in range(expr.ndim): + if iaxis != expr.axis: + # non-concatenated axes share the dimensions. + in_tag_var = self.get_kanren_var_for_axis_tag(ary, + iaxis) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + iaxis) + self.record_constraint(in_tag_var, out_tag_var) + + def map_axis_permutation(self, expr: pt.AxisPermutation + ) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + self.rec(expr.array) + + assert expr.ndim == expr.array.ndim + + for out_axis in range(expr.ndim): + in_axis = expr.axis_permutation[out_axis] + out_tag = self.get_kanren_var_for_axis_tag(expr, out_axis) + in_tag = self.get_kanren_var_for_axis_tag(expr, in_axis) + self.record_constraint(out_tag, in_tag) + + def map_basic_index(self, expr: pt.IndexBase) -> None: + from pytato.array import NormalizedSlice + from pytato.utils import are_shape_components_equal + + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + self.rec(expr.array) + + i_out_axis = 0 + + assert len(expr.indices) == expr.array.ndim + + for i_in_axis, idx in enumerate(expr.indices): + if isinstance(idx, int): + pass + else: + assert isinstance(idx, NormalizedSlice) + if (idx.step == 1 + and are_shape_components_equal(idx.start, 0) + and are_shape_components_equal(idx.stop, + expr.array.shape[i_in_axis])): + + i_in_axis_tag = self.get_kanren_var_for_axis_tag(expr.array, + i_in_axis) + i_out_axis_tag = self.get_kanren_var_for_axis_tag(expr, + i_out_axis) + self.record_constraint(i_in_axis_tag, i_out_axis_tag) + + i_out_axis += 1 + + def map_contiguous_advanced_index(self, + expr: pt.AdvancedIndexInContiguousAxes + ) -> None: + from pytato.array import NormalizedSlice + from pytato.utils import (partition, get_shape_after_broadcasting, + are_shapes_equal, are_shape_components_equal) + + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + self.rec(expr.array) + for idx in expr.indices: + if isinstance(idx, pt.Array): + self.rec(idx) + + i_adv_indices, i_basic_indices = partition( + lambda idx: isinstance(expr.indices[idx], NormalizedSlice), + range(len(expr.indices))) + npre_advanced_basic_indices = len([i_idx + for i_idx in i_basic_indices + if i_idx < i_adv_indices[0]]) + npost_advanced_basic_indices = len([i_idx + for i_idx in i_basic_indices + if i_idx > i_adv_indices[-1]]) + + indirection_arrays = [expr.indices[i_idx] for i_idx in i_adv_indices] + assert are_shapes_equal( + get_shape_after_broadcasting(indirection_arrays), + expr.shape[ + npre_advanced_basic_indices:expr.ndim-npost_advanced_basic_indices]) + + for subexpr in indirection_arrays: + if isinstance(subexpr, pt.Array): + for i_in_axis, i_out_axis in zip( + range(subexpr.ndim), + range(expr.ndim-subexpr.ndim+npre_advanced_basic_indices, + expr.ndim-npost_advanced_basic_indices)): + in_dim = subexpr.shape[i_in_axis] + out_dim = expr.shape[i_out_axis] + if are_shape_components_equal(in_dim, out_dim): + in_tag_var = self.get_kanren_var_for_axis_tag(subexpr, + i_in_axis) + out_tag_var = self.get_kanren_var_for_axis_tag(expr, + i_out_axis) + + self.record_constraint(in_tag_var, out_tag_var) + else: + # broadcasted axes, cannot belong to the same + # discretization entity. + assert are_shape_components_equal(in_dim, 1) + + def map_non_contiguous_advanced_index(self, + expr: pt.AdvancedIndexInNoncontiguousAxes + ) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + self.rec(expr.array) + for idx in expr.indices: + if isinstance(idx, pt.Array): + self.rec(idx) + + def map_reshape(self, expr: pt.Reshape) -> None: + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + self.rec(expr.array) + # we can add constraints to reshape that only include new axes in its + # reshape. + # Other reshapes do not 'conserve' the types in our type-system. + # Well *what if*. Let's just say this type inference fails for + # non-trivial 'reshapes'. So, what are the 'trivial' reshapes? + # trivial reshapes: + # (x1, x2, ... xn) -> ((1,)*, x1, (1,)*, x2, (1,)*, x3, (1,)*, ..., xn, 1*) + # given all(x1!=1, x2!=1, x3!=1, .. xn!= 1) + if ((1 not in (expr.array.shape)) # leads to ambiguous newaxis + and (set(expr.shape) <= (set(expr.array.shape) | {1}))): + i_in_axis = 0 + for i_out_axis, dim in enumerate(expr.shape): + if dim != 1: + assert dim == expr.array.shape[i_in_axis] + i_in_axis_tag = self.get_kanren_var_for_axis_tag(expr.array, + i_in_axis) + i_out_axis_tag = self.get_kanren_var_for_axis_tag(expr, + i_out_axis) + self.record_constraint(i_in_axis_tag, i_out_axis_tag) + i_in_axis += 1 + else: + # print(f"Skipping: {expr.array.shape} -> {expr.shape}") + # Wacky reshape => bail. + return + + def map_einsum(self, expr: pt.Einsum) -> None: + + self.record_eq_constraints_from_tags(expr) + self._record_all_axes_to_be_solved_if_impl_stored(expr) + + for arg in expr.args: + self.rec(arg) + + descr_to_tag = {} + for iaxis in range(expr.ndim): + descr_to_tag[EinsumElementwiseAxis(iaxis)] = ( + self.get_kanren_var_for_axis_tag(expr, iaxis)) + + for access_descrs, arg in zip(expr.access_descriptors, + expr.args): + # if an einsum is stored => every argument's axes must + # also be inferred, even those that are getting reduced. + for iarg_axis, descr in enumerate(access_descrs): + in_tag_var = self.get_kanren_var_for_axis_tag(arg, + iarg_axis) + + if descr in descr_to_tag: + self.record_constraint(descr_to_tag[descr], in_tag_var) + else: + descr_to_tag[descr] = in_tag_var + + if isinstance(descr, EinsumReductionAxis): + self.variables_to_solve.add(in_tag_var) + + def map_dict_of_named_arrays(self, expr: pt.DictOfNamedArrays + ) -> None: + for _, subexpr in sorted(expr._data.items()): + self.rec(subexpr) + self._record_all_axes_to_be_solved(subexpr) + + def map_loopy_call(self, expr: LoopyCall) -> None: + for _, subexpr in sorted(expr.bindings.items()): + if isinstance(subexpr, pt.Array): + if not isinstance(subexpr, pt.InputArgumentBase): + self._record_all_axes_to_be_solved(subexpr) + self.rec(subexpr) + + # there's really no good way to propagate the metadata in this case. + # One *could* raise the loopy kernel instruction expressions to + # high level ops, but that's really involved and probably not worth it. + + def map_named_array(self, expr: pt.NamedArray) -> None: + self.record_eq_constraints_from_tags(expr) + self.rec(expr._container) + + def map_distributed_send_ref_holder(self, + expr: pt.DistributedSendRefHolder + ) -> None: + self.record_eq_constraints_from_tags(expr) + self.rec(expr.passthrough_data) + for idim in range(expr.ndim): + assert (expr.passthrough_data.shape[idim] + == expr.shape[idim]) + self.record_constraint( + self.get_kanren_var_for_axis_tag(expr.passthrough_data, + idim), + self.get_kanren_var_for_axis_tag(expr, idim) + ) + + def map_distributed_recv(self, + expr: pt.DistributedRecv) -> None: + self.record_eq_constraints_from_tags(expr) + + +def unify_discretization_entity_tags(expr: Union[ArrayContainer, ArrayOrNames] + ) -> ArrayOrNames: + if not isinstance(expr, (pt.Array, pt.DictOfNamedArrays)): + return rec_map_array_container(unify_discretization_entity_tags, + expr) + + from collections import defaultdict + discr_unification_helper = DiscretizationEntityConstraintCollector() + discr_unification_helper(expr) + tag_var_to_axis = {} + variables_to_solve = [] + + for (axis, var) in discr_unification_helper.axis_to_tag_var.items(): + tag_var_to_axis[var] = axis + if var in discr_unification_helper.variables_to_solve: + variables_to_solve.append(var) + + lhs = [cnstrnt[0] for cnstrnt in discr_unification_helper.constraints] + rhs = [cnstrnt[1] for cnstrnt in discr_unification_helper.constraints] + assert len(lhs) == len(rhs) + solutions = {} + + for i_retry in range(MAX_UNIFY_RETRIES): + old_solutions = solutions.copy() + solutions = unification.unify(lhs, rhs, + {l_expr: r_expr + for l_expr, r_expr in solutions.items() + if isinstance(r_expr, + DiscretizationEntityAxisTag)}) + if solutions == old_solutions: + logger.info(f"Unification converged after {i_retry} iterations.") + break + else: + logger.warn(f"Could not converge after {MAX_UNIFY_RETRIES} iterations.") + + # Ideally it might be better to enable this, but that would be too + # restrictive as not all computation graphs result in DOFArray ouptuts + # if not (frozenset(variables_to_solve) <= frozenset(solutions)): + # raise RuntimeError("Unification failed.") + + # ary_to_axes_tags: mapping from array to a mapping from iaxis to the + # solved tag. + ary_to_axes_tags = defaultdict(dict) + for var in solutions: + ary, axis = tag_var_to_axis[var] + if isinstance(solutions[var], DiscretizationEntityAxisTag): + ary_to_axes_tags[ary][axis] = solutions[var] + if var in variables_to_solve and ( + not isinstance(solutions[var], DiscretizationEntityAxisTag)): + raise RuntimeError(f"Could not solve for {var}.") + + def attach_tags(expr: ArrayOrNames) -> ArrayOrNames: + if not isinstance(expr, pt.Array): + return expr + + for iaxis, solved_tag in ary_to_axes_tags[expr].items(): + if expr.axes[iaxis].tags_of_type(DiscretizationEntityAxisTag): + discr_tag, = (expr + .axes[iaxis] + .tags_of_type(DiscretizationEntityAxisTag)) + assert discr_tag == solved_tag + else: + if not isinstance(solved_tag, DiscretizationEntityAxisTag): + actual_tag = discr_unification_helper.axis_to_tag_var[(expr, + iaxis)] + assert actual_tag in discr_unification_helper.variables_to_solve + assert actual_tag in variables_to_solve + raise ValueError(f"In {expr!r}, axis={iaxis}'s type cannot be " + "inferred.") + expr = expr.with_tagged_axis(iaxis, solved_tag) + + if isinstance(expr, pt.Einsum): + redn_descr_to_entity_type = {} + for access_descrs, arg in zip(expr.access_descriptors, + expr.args): + for iaxis, access_descr in enumerate(access_descrs): + if isinstance(access_descr, EinsumReductionAxis): + redn_descr_to_entity_type[access_descr] = ( + ary_to_axes_tags[arg][iaxis]) + + if (frozenset(redn_descr_to_entity_type) + != frozenset(expr.redn_descr_to_redn_dim)): + raise ValueError + + for redn_descr, solved_tag in redn_descr_to_entity_type.items(): + if not isinstance(solved_tag, DiscretizationEntityAxisTag): + raise ValueError(f"In {expr!r}, redn_descr={redn_descr}'s" + " type cannot be inferred.") + expr = expr.with_tagged_redn_dim(redn_descr, solved_tag) + + if isinstance(expr, pt.IndexLambda): + from pytato.raising import (index_lambda_to_high_level_op, + ReduceOp) + + hlo = index_lambda_to_high_level_op(expr) + if isinstance(hlo, ReduceOp): + for iaxis in hlo.axes: + solved_tag = ary_to_axes_tags[hlo.x][iaxis] + if not isinstance(solved_tag, DiscretizationEntityAxisTag): + raise ValueError(f"In {expr!r}, redn_descr={iaxis}'s" + " type cannot be inferred.") + + expr = expr.with_tagged_redn_dim(iaxis, solved_tag) + + return expr + + return pt.transform.map_and_copy(expr, attach_tags) + +# }}} + + +class UnInferredStoredArrayCatcher(pt.transform.CachedWalkMapper): + """ + Raises a :class:`ValueError` if a stored array has axes without a + :class:`DiscretizationEntityAxisTag` tagged to it. + """ + def post_visit(self, expr: ArrayOrNames) -> None: + if (isinstance(expr, pt.Array) + and expr.tags_of_type(pt.tags.ImplStored)): + if any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 + for axis in expr.axes): + raise ValueError(f"{expr!r} doesn't have all its axes inferred.") + + if (isinstance(expr, pt.IndexLambda) + and any(len(redn_dim.tags_of_type(DiscretizationEntityAxisTag)) != 1 + for redn_dim in expr.reduction_dims.values())): + raise ValueError(f"{expr!r} doesn't have all its redn axes inferred.") + + if (isinstance(expr, pt.Einsum) + and any(len(redn_dim.tags_of_type(DiscretizationEntityAxisTag)) != 1 + for redn_dim in expr.redn_descr_to_redn_dim.values())): + raise ValueError(f"{expr!r} doesn't have all its redn axes inferred.") + + if isinstance(expr, pt.DictOfNamedArrays): + if any(any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 + for axis in subexpr.axes) + for subexpr in expr._data.values()): + raise ValueError(f"{expr!r} doesn't have all its axes inferred.") + + from pytato.loopy import LoopyCall + + if isinstance(expr, LoopyCall): + if any(any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 + for axis in subexpr.axes) + for subexpr in expr.bindings.values() + if (isinstance(subexpr, pt.Array) + and not isinstance(subexpr, pt.InputArgumentBase) + and subexpr.ndim != 0)): + raise ValueError(f"{expr!r} doesn't have all its axes inferred.") + + +def are_all_stored_arrays_inferred(expr: ArrayOrNames): + UnInferredStoredArrayCatcher()(expr) + # vim: fdm=marker From 9ebdf05bc28331a079cf0b95cc7d7885c16aeafb Mon Sep 17 00:00:00 2001 From: Kaushik Kulkarni Date: Sat, 12 Mar 2022 13:05:08 -0600 Subject: [PATCH 147/154] Implements FusionContractorArrayContext * Performs Loop Fusion * Performs Array contraction * Splits kernels at the granularity of fused einsums * Transforms those einsums using recorded values from github.com/kaushikcfd/feinsum Co-authored-by: Matthias Diener Co-authored-by: Matthew Smith --- meshmode/array_context.py | 1226 ++++++++++++++++++++++++++++++++++++- meshmode/pytato_utils.py | 555 +---------------- 2 files changed, 1243 insertions(+), 538 deletions(-) diff --git a/meshmode/array_context.py b/meshmode/array_context.py index 071071dc4..a1e9b0c8b 100644 --- a/meshmode/array_context.py +++ b/meshmode/array_context.py @@ -27,8 +27,10 @@ import sys import logging +import numpy as np from warnings import warn +from typing import Union, FrozenSet, Tuple, Any from arraycontext import PyOpenCLArrayContext as PyOpenCLArrayContextBase from arraycontext import PytatoPyOpenCLArrayContext as PytatoPyOpenCLArrayContextBase from arraycontext.pytest import ( @@ -37,6 +39,21 @@ register_pytest_array_context_factory) from loopy.translation_unit import for_each_kernel +from loopy.tools import memoize_on_disk +from pytools import ProcessLogger, memoize_on_first_arg +from pytools.tag import UniqueTag, tag_dataclass + +from meshmode.transform_metadata import (DiscretizationElementAxisTag, + DiscretizationDOFAxisTag, + DiscretizationFaceAxisTag, + DiscretizationDimAxisTag, + DiscretizationTopologicalDimAxisTag, + DiscretizationAmbientDimAxisTag, + DiscretizationFlattenedDOFAxisTag, + DiscretizationEntityAxisTag) +from dataclasses import dataclass + +from pyrsistent import pmap logger = logging.getLogger(__name__) @@ -506,7 +523,10 @@ def _alias_global_temporaries(t_unit): if tv.address_space != AddressSpace.GLOBAL: new_tvs[name] = tv else: - assert name in new_tvs + # FIXME: Need tighter assertion condition (this doesn't work when + # zero-size arrays are present) + # assert name in new_tvs + pass kernel = kernel.copy(temporary_variables=new_tvs) @@ -628,4 +648,1208 @@ def untag_loopy_call_results(expr): return dag + +def get_temps_not_to_contract(knl): + from functools import reduce + wmap = knl.writer_map() + rmap = knl.reader_map() + + temps_not_to_contract = set() + for tv in knl.temporary_variables: + if len(wmap.get(tv, set())) == 1: + writer_id, = wmap[tv] + writer_loop_nest = knl.id_to_insn[writer_id].within_inames + insns_in_writer_loop_nest = reduce(frozenset.union, + (knl.iname_to_insns()[iname] + for iname in writer_loop_nest), + frozenset()) + if ( + (not (rmap.get(tv, frozenset()) + <= insns_in_writer_loop_nest)) + or len(knl.id_to_insn[writer_id].reduction_inames()) != 0 + or any((len(knl.id_to_insn[reader_id].reduction_inames()) != 0) + for reader_id in rmap.get(tv, frozenset()))): + temps_not_to_contract.add(tv) + else: + temps_not_to_contract.add(tv) + return temps_not_to_contract + + # Better way to query it... + # import loopy as lp + # from kanren.constraints import neq as kanren_neq + # + # tempo = lp.relations.get_tempo(knl) + # producero = lp.relations.get_producero(knl) + # consumero = lp.relations.get_consumero(knl) + # withino = lp.relations.get_withino(knl) + # reduce_insno = lp.relations.get_reduce_insno(knl) + # + # # temp_k: temporary variable that cannot be contracted + # temp_k = kanren.var() + # producer_insn_k = kanren.var() + # producer_loops_k = kanren.var() + # consumer_insn_k = kanren.var() + # consumer_loops_k = kanren.var() + + # temps_not_to_contract = kanren.run(0, + # temp_k, + # tempo(temp_k), + # producero(producer_insn_k, + # temp_k), + # consumero(consumer_insn_k, + # temp_k), + # withino(producer_insn_k, + # producer_loops_k), + # withino(consumer_insn_k, + # consumer_loops_k), + # kanren.lany( + # kanren_neq( + # producer_loops_k, + # consumer_loops_k), + # reduce_insno(consumer_insn_k)), + # results_filter=frozenset) + # return temps_not_to_contract + + +def _is_iel_loop_part_of_global_dof_loops(iel: str, knl) -> bool: + insn, = knl.iname_to_insns()[iel] + return any(iname + for iname in knl.id_to_insn[insn].within_inames + if knl.iname_tags_of_type(iname, DiscretizationDOFAxisTag)) + + +def _discr_entity_sort_key(discr_tag: DiscretizationEntityAxisTag + ) -> Tuple[Any, ...]: + + return type(discr_tag).__name__ + + +# {{{ define FEMEinsumTag + +@dataclass(frozen=True) +class EinsumIndex: + discr_entity: DiscretizationEntityAxisTag + length: int + + @classmethod + def from_iname(cls, iname, kernel): + discr_entity, = kernel.filter_iname_tags_by_type( + iname, DiscretizationEntityAxisTag) + length = kernel.get_constant_iname_length(iname) + return cls(discr_entity, length) + + +@dataclass(frozen=True) +class FreeEinsumIndex(EinsumIndex): + pass + + +@dataclass(frozen=True) +class SummationEinsumIndex(EinsumIndex): + pass + + +@dataclass(frozen=True) +class FEMEinsumTag(UniqueTag): + indices: Tuple[Tuple[EinsumIndex, ...], ...] + + +class NotAnFEMEinsumError(ValueError): + """ + pass + """ + +# }}} + + +@memoize_on_first_arg +def _get_redn_iname_to_insns(kernel): + from immutables import Map + redn_iname_to_insns = {iname: set() + for iname in kernel.all_inames()} + + for insn in kernel.instructions: + for redn_iname in insn.reduction_inames(): + redn_iname_to_insns[redn_iname].add(insn.id) + + return Map({k: frozenset(v) + for k, v in redn_iname_to_insns.items()}) + + +def _do_inames_belong_to_different_einsum_types(iname1, iname2, kernel): + if kernel.iname_to_insns()[iname1]: + assert (len(kernel.iname_to_insns()[iname1]) + == len(kernel.iname_to_insns()[iname2]) + == 1) + insn1, = kernel.iname_to_insns()[iname1] + insn2, = kernel.iname_to_insns()[iname2] + else: + redn_iname_to_insns = _get_redn_iname_to_insns(kernel) + assert (len(redn_iname_to_insns[iname1]) + == len(redn_iname_to_insns[iname2]) + == 1) + insn1, = redn_iname_to_insns[iname1] + insn2, = redn_iname_to_insns[iname2] + + assert (len(redn_iname_to_insns[iname1]) + == len(redn_iname_to_insns[iname2]) + == 1) + + var1_name, = kernel.id_to_insn[insn1].assignee_var_names() + var2_name, = kernel.id_to_insn[insn2].assignee_var_names() + var1 = kernel.get_var_descriptor(var1_name) + var2 = kernel.get_var_descriptor(var2_name) + + ensm1, = var1.tags_of_type(FEMEinsumTag) + ensm2, = var2.tags_of_type(FEMEinsumTag) + + return ensm1 != ensm2 + + +def _fuse_loops_over_a_discr_entity(knl, + mesh_entity, + fused_loop_prefix, + should_fuse_redn_loops, + orig_knl): + import loopy as lp + import kanren + from functools import reduce, partial + taggedo = lp.relations.get_taggedo_of_type(orig_knl, mesh_entity) + + redn_loops = reduce(frozenset.union, + (insn.reduction_inames() + for insn in orig_knl.instructions), + frozenset()) + + non_redn_loops = reduce(frozenset.union, + (insn.within_inames + for insn in orig_knl.instructions), + frozenset()) + + # tag_k: tag of type 'mesh_entity' + tag_k = kanren.var() + tags = kanren.run(0, + tag_k, + taggedo(kanren.var(), tag_k), + results_filter=frozenset) + for itag, tag in enumerate( + sorted(tags, key=lambda x: _discr_entity_sort_key(x))): + # iname_k: iname tagged with 'tag' + iname_k = kanren.var() + inames = kanren.run(0, + iname_k, + taggedo(iname_k, tag), + results_filter=frozenset) + inames = frozenset(inames) + if should_fuse_redn_loops: + inames = inames & redn_loops + else: + inames = inames & non_redn_loops + + length_to_inames = {} + for iname in inames: + length = knl.get_constant_iname_length(iname) + length_to_inames.setdefault(length, set()).add(iname) + + for i, (_, inames_to_fuse) in enumerate( + sorted(length_to_inames.items())): + + knl = lp.rename_inames_in_batch( + knl, + lp.get_kennedy_unweighted_fusion_candidates( + knl, inames_to_fuse, + prefix=f"{fused_loop_prefix}_{itag}_{i}_", + force_infusible=partial( + _do_inames_belong_to_different_einsum_types, + kernel=orig_knl), + )) + knl = lp.tag_inames(knl, {f"{fused_loop_prefix}_{itag}_*": tag}) + + return knl + + +@memoize_on_disk +def fuse_same_discretization_entity_loops(knl): + # maintain an 'orig_knl' to keep the original iname and tags before + # transforming it. + orig_knl = knl + + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationFaceAxisTag, + "iface", + False, + orig_knl) + + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationElementAxisTag, + "iel", + False, + orig_knl) + + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationDOFAxisTag, + "idof", + False, + orig_knl) + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationDimAxisTag, + "idim", + False, + orig_knl) + + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationFaceAxisTag, + "iface", + True, + orig_knl) + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationDOFAxisTag, + "idof", + True, + orig_knl) + knl = _fuse_loops_over_a_discr_entity(knl, DiscretizationDimAxisTag, + "idim", + True, + orig_knl) + + return knl + + +@memoize_on_disk +def contract_arrays(knl, callables_table): + import loopy as lp + from loopy.transform.precompute import precompute_for_single_kernel + + temps_not_to_contract = get_temps_not_to_contract(knl) + all_temps = frozenset(knl.temporary_variables) + + logger.info("Array Contraction: Contracting " + f"{len(all_temps-frozenset(temps_not_to_contract))} temps") + + wmap = knl.writer_map() + + for temp in sorted(all_temps - frozenset(temps_not_to_contract)): + writer_id, = wmap[temp] + rmap = knl.reader_map() + ensm_tag, = knl.id_to_insn[writer_id].tags_of_type(EinsumTag) + + knl = lp.assignment_to_subst(knl, temp, + remove_newly_unused_inames=False) + if temp not in rmap: + # no one was reading 'temp' i.e. dead code got eliminated :) + assert f"{temp}_subst" not in knl.substitutions + continue + knl = precompute_for_single_kernel( + knl, callables_table, f"{temp}_subst", + sweep_inames=(), + temporary_address_space=lp.AddressSpace.PRIVATE, + compute_insn_id=f"_mm_contract_{temp}", + ) + + knl = lp.map_instructions(knl, + f"id:_mm_contract_{temp}", + lambda x: x.tagged(ensm_tag)) + + return lp.remove_unused_inames(knl) + + +def _get_group_size_for_dof_array_loop(nunit_dofs): + """ + Returns the OpenCL workgroup size for a loop iterating over the global DOFs + of a discretization with *nunit_dofs* per cell. + """ + if nunit_dofs == {6}: + return 16, 6 + elif nunit_dofs == {10}: + return 16, 10 + elif nunit_dofs == {20}: + return 16, 10 + elif nunit_dofs == {1}: + return 32, 1 + elif nunit_dofs == {2}: + return 32, 2 + elif nunit_dofs == {4}: + return 16, 4 + elif nunit_dofs == {3}: + return 32, 3 + elif nunit_dofs == {35}: + return 9, 7 + elif nunit_dofs == {15}: + return 8, 8 + else: + raise NotImplementedError(nunit_dofs) + + +def _get_iel_to_idofs(kernel): + iel_inames = {iname + for iname in kernel.all_inames() + if (kernel + .inames[iname] + .tags_of_type((DiscretizationElementAxisTag, + DiscretizationFlattenedDOFAxisTag))) + } + idof_inames = {iname + for iname in kernel.all_inames() + if (kernel + .inames[iname] + .tags_of_type(DiscretizationDOFAxisTag)) + } + iface_inames = {iname + for iname in kernel.all_inames() + if (kernel + .inames[iname] + .tags_of_type(DiscretizationFaceAxisTag)) + } + idim_inames = {iname + for iname in kernel.all_inames() + if (kernel + .inames[iname] + .tags_of_type(DiscretizationDimAxisTag)) + } + + iel_to_idofs = {iel: set() for iel in iel_inames} + + for insn in kernel.instructions: + if (len(insn.within_inames) == 1 + and (insn.within_inames) <= iel_inames): + iel, = insn.within_inames + if all(kernel.id_to_insn[el_insn].within_inames == insn.within_inames + for el_insn in kernel.iname_to_insns()[iel]): + # the iel here doesn't interfere with any idof i.e. we + # support parallelizing such loops. + pass + else: + raise NotImplementedError(f"The loop {insn.within_inames}" + " does not appear as a singly nested" + " loop.") + elif ((len(insn.within_inames) == 2) + and (len(insn.within_inames & iel_inames) == 1) + and (len(insn.within_inames & idof_inames) == 1)): + iel, = insn.within_inames & iel_inames + idof, = insn.within_inames & idof_inames + iel_to_idofs[iel].add(idof) + if all((iel in kernel.id_to_insn[dof_insn].within_inames) + for dof_insn in kernel.iname_to_insns()[idof]): + pass + else: + raise NotImplementedError("The loop " + f"'{insn.within_inames}' has the idof-loop" + " that's not nested within the iel-loop.") + elif ((len(insn.within_inames) > 2) + and (len(insn.within_inames & iel_inames) == 1) + and (len(insn.within_inames & idof_inames) == 1) + and (len(insn.within_inames & (idim_inames | iface_inames)) + == (len(insn.within_inames) - 2))): + iel, = insn.within_inames & iel_inames + idof, = insn.within_inames & idof_inames + iel_to_idofs[iel].add(idof) + if all((all({iel, idof} <= kernel.id_to_insn[non_iel_insn].within_inames + for non_iel_insn in kernel.iname_to_insns()[non_iel_iname])) + for non_iel_iname in insn.within_inames - {iel}): + iel_to_idofs[iel].add(idof) + else: + raise NotImplementedError("Could not fit into " + " loop nest pattern.") + else: + raise NotImplementedError(f"Cannot fit loop nest '{insn.within_inames}'" + " into known set of loop-nest patterns.") + + return pmap({iel: frozenset(idofs) + for iel, idofs in iel_to_idofs.items()}) + + +def _get_iel_loop_from_insn(insn, knl): + iel, = {iname + for iname in insn.within_inames + if knl.inames[iname].tags_of_type((DiscretizationElementAxisTag, + DiscretizationFlattenedDOFAxisTag))} + return iel + + +def _get_element_loop_topo_sorted_order(knl): + dag = {iel: set() + for iel in knl.all_inames() + if knl.inames[iel].tags_of_type(DiscretizationElementAxisTag)} + + for insn in knl.instructions: + succ_iel = _get_iel_loop_from_insn(insn, knl) + for dep_id in insn.depends_on: + pred_iel = _get_iel_loop_from_insn(knl.id_to_insn[dep_id], knl) + if pred_iel != succ_iel: + dag[pred_iel].add(succ_iel) + + from pytools.graph import compute_topological_order + return compute_topological_order(dag, key=lambda x: x) + + +@tag_dataclass +class EinsumTag(UniqueTag): + orig_loop_nest: FrozenSet[str] + + +def _prepare_kernel_for_parallelization(kernel): + discr_tag_to_prefix = {DiscretizationElementAxisTag: "iel", + DiscretizationDOFAxisTag: "idof", + DiscretizationDimAxisTag: "idim", + DiscretizationAmbientDimAxisTag: "idim", + DiscretizationTopologicalDimAxisTag: "idim", + DiscretizationFlattenedDOFAxisTag: "imsh_nodes", + DiscretizationFaceAxisTag: "iface"} + import loopy as lp + from loopy.match import ObjTagged + + # A mapping from inames that the instruction accesss to + # the instructions ids within that iname. + ensm_buckets = {} + vng = kernel.get_var_name_generator() + + for insn in kernel.instructions: + inames = insn.within_inames | insn.reduction_inames() + ensm_buckets.setdefault(tuple(sorted(inames)), set()).add(insn.id) + + # FIXME: Dependency violation is a big concern here + # Waiting on the loopy feature: https://github.com/inducer/loopy/issues/550 + + for ieinsm, (loop_nest, insns) in enumerate(sorted(ensm_buckets.items())): + new_insns = [insn.tagged(EinsumTag(frozenset(loop_nest))) + if insn.id in insns + else insn + for insn in kernel.instructions] + kernel = kernel.copy(instructions=new_insns) + + new_inames = [] + for iname in loop_nest: + discr_tag, = kernel.iname_tags_of_type(iname, + DiscretizationEntityAxisTag) + new_iname = vng(f"{discr_tag_to_prefix[type(discr_tag)]}_ensm{ieinsm}") + new_inames.append(new_iname) + + kernel = lp.duplicate_inames( + kernel, + loop_nest, + within=ObjTagged(EinsumTag(frozenset(loop_nest))), + new_inames=new_inames, + tags=kernel.iname_to_tags) + + return kernel + + +def _get_elementwise_einsum(t_unit, einsum_tag): + import loopy as lp + import feinsum as fnsm + from loopy.match import ObjTagged + from pymbolic.primitives import Variable, Subscript + + kernel = t_unit.default_entrypoint + + assert isinstance(einsum_tag, EinsumTag) + insn_match = ObjTagged(einsum_tag) + + global_vars = ({tv.name + for tv in kernel.temporary_variables.values() + if tv.address_space == lp.AddressSpace.GLOBAL} + | set(kernel.arg_dict.keys())) + insns = [insn + for insn in kernel.instructions + if insn_match(kernel, insn)] + idx_tuples = set() + + for insn in insns: + assert len(insn.assignees) == 1 + if isinstance(insn.assignee, Variable): + if insn.assignee.name in global_vars: + raise NotImplementedError(insn) + else: + assert (kernel.temporary_variables[insn.assignee.name].address_space + == lp.AddressSpace.PRIVATE) + elif isinstance(insn.assignee, Subscript): + assert insn.assignee_name in global_vars + idx_tuples.add(tuple(idx.name + for idx in insn.assignee.index_tuple)) + else: + raise NotImplementedError(insn) + + if len(idx_tuples) != 1: + raise NotImplementedError("Multiple einsums in the same loop nest =>" + " not allowed.") + idx_tuple, = idx_tuples + subscript = "{lhs}, {lhs}->{lhs}".format( + lhs="".join(chr(97+i) + for i in range(len(idx_tuple)))) + arg_shape = tuple(np.inf + if kernel.iname_tags_of_type(idx, DiscretizationElementAxisTag) + else kernel.get_constant_iname_length(idx) + for idx in idx_tuple) + return fnsm.einsum(subscript, + fnsm.array(arg_shape, "float64"), + fnsm.array(arg_shape, "float64")) + + +def _combine_einsum_domains(knl): + import islpy as isl + from functools import reduce + + new_domains = [] + einsum_tags = reduce( + frozenset.union, + (insn.tags_of_type(EinsumTag) + for insn in knl.instructions), + frozenset()) + + for tag in sorted(einsum_tags, + key=lambda x: sorted(x.orig_loop_nest)): + insns = [insn + for insn in knl.instructions + if tag in insn.tags] + inames = reduce(frozenset.union, + ((insn.within_inames | insn.reduction_inames()) + for insn in insns), + frozenset()) + domain = knl.get_inames_domain(frozenset(inames)) + new_domains.append(domain.project_out_except(sorted(inames), + [isl.dim_type.set])) + + return knl.copy(domains=new_domains) + + +def _rewrite_tvs_as_base_plus_offset(t_unit, device): + import loopy as lp + knl = t_unit.default_entrypoint + vng = knl.get_var_name_generator() + nbytes_to_base_storages = {} + for tv in knl.temporary_variables.values(): + if tv.address_space == lp.AddressSpace.GLOBAL: + nbytes_to_base_storages.setdefault(tv.nbytes, + set()).add(tv.base_storage) + + nbytes_to_new_storage_name = {nbytes: vng("_mm_base_storage") + for nbytes in sorted(nbytes_to_base_storages)} + + if any(nbytes > device.max_mem_alloc_size + for nbytes in nbytes_to_new_storage_name): + raise RuntimeError("Some of the variables " + "require more memory than the CL-device " + "allows.") + + old_storage_to_new_storage_plus_offset = {} + new_storage_to_alloc_nbytes = {} + for nbytes, old_storages in nbytes_to_base_storages.items(): + new_storage_name = nbytes_to_new_storage_name[nbytes] + offset = 0 + new_storage_to_alloc_nbytes[new_storage_name] = offset + for old_storage in sorted(old_storages): + assert (offset + nbytes) < device.max_mem_alloc_size + old_storage_to_new_storage_plus_offset[old_storage] = ( + (new_storage_name, offset)) + offset = offset + nbytes + new_storage_to_alloc_nbytes[new_storage_name] = offset + if (offset + nbytes) > device.max_mem_alloc_size: + new_storage_name = vng("_mm_base_storage") + offset = 0 + + del nbytes_to_new_storage_name + + new_tvs = {} + for name, tv in knl.temporary_variables.items(): + if tv.address_space == lp.AddressSpace.GLOBAL: + new_storage_name, offset_nbytes = ( + old_storage_to_new_storage_plus_offset[tv.base_storage]) + new_storage_size = ( + new_storage_to_alloc_nbytes[new_storage_name] + // tv.dtype.numpy_dtype.itemsize) + tv = tv.copy(base_storage=new_storage_name, + offset=offset_nbytes//tv.dtype.numpy_dtype.itemsize, + storage_shape=(new_storage_size,) + (1,)*(len(tv.shape)-1) + ) + + new_tvs[name] = tv + + knl = knl.copy(temporary_variables=new_tvs) + return t_unit.with_kernel(knl) + + +class FusionContractorArrayContext( + SingleGridWorkBalancingPytatoArrayContext): + + def transform_dag(self, dag): + import pytato as pt + + # {{{ Remove FEMEinsumTags that might have been propagated + + # TODO: Is this too hacky? + + def remove_fem_einsum_tags(expr): + if isinstance(expr, pt.Array): + try: + fem_ensm_tag = next(iter(expr.tags_of_type(FEMEinsumTag))) + except StopIteration: + return expr + else: + assert isinstance(expr, pt.InputArgumentBase) + return expr.without_tags(fem_ensm_tag) + else: + return expr + + dag = pt.transform.map_and_copy(dag, remove_fem_einsum_tags) + + # }}} + + # {{{ CSE + + with ProcessLogger(logger, "transform_dag.mpms_materialization"): + dag = pt.transform.materialize_with_mpms(dag) + + def mark_materialized_nodes_as_cse( + ary: Union[pt.Array, + pt.AbstractResultWithNamedArrays]) -> pt.Array: + if isinstance(ary, pt.AbstractResultWithNamedArrays): + return ary + + if ary.tags_of_type(pt.tags.ImplStored): + return ary.tagged(pt.tags.PrefixNamed("cse")) + else: + return ary + + with ProcessLogger(logger, "transform_dag.naming_cse"): + dag = pt.transform.map_and_copy(dag, mark_materialized_nodes_as_cse) + + # }}} + + # {{{ indirect addressing are non-negative + + indirection_maps = set() + + class _IndirectionMapRecorder(pt.transform.CachedWalkMapper): + def post_visit(self, expr): + if isinstance(expr, pt.IndexBase): + for idx in expr.indices: + if isinstance(idx, pt.Array): + indirection_maps.add(idx) + + _IndirectionMapRecorder()(dag) + + def tag_indices_as_non_negative(ary): + if ary in indirection_maps: + return ary.tagged(pt.tags.AssumeNonNegative()) + else: + return ary + + with ProcessLogger(logger, "transform_dag.tag_indices_as_non_negative"): + dag = pt.transform.map_and_copy(dag, tag_indices_as_non_negative) + + # }}} + + with ProcessLogger(logger, "transform_dag.deduplicate_data_wrappers"): + dag = pt.transform.deduplicate_data_wrappers(dag) + + # {{{ get rid of copies for different views of a cl-array + + def eliminate_reshapes_of_data_wrappers(ary): + if (isinstance(ary, pt.Reshape) + and isinstance(ary.array, pt.DataWrapper)): + return pt.make_data_wrapper(ary.array.data.reshape(ary.shape), + tags=ary.tags, + axes=ary.axes) + else: + return ary + + dag = pt.transform.map_and_copy(dag, + eliminate_reshapes_of_data_wrappers) + + # }}} + + # {{{ face_mass: materialize einsum args + + def materialize_face_mass_input_and_output(expr): + if (isinstance(expr, pt.Einsum) + and pt.analysis.is_einsum_similar_to_subscript( + expr, + "ifj,fej,fej->ei")): + mat, jac, vec = expr.args + return (pt.einsum("ifj,fej,fej->ei", + mat, + jac, + vec.tagged(pt.tags.ImplStored())) + .tagged((pt.tags.ImplStored(), + pt.tags.PrefixNamed("face_mass")))) + else: + return expr + + with ProcessLogger(logger, + "transform_dag.materialize_face_mass_ins_and_outs"): + dag = pt.transform.map_and_copy(dag, + materialize_face_mass_input_and_output) + + # }}} + + # {{{ materialize inverse mass inputs + + def materialize_inverse_mass_inputs(expr): + if (isinstance(expr, pt.Einsum) + and pt.analysis.is_einsum_similar_to_subscript( + expr, + "ei,ij,ej->ei")): + arg1, arg2, arg3 = expr.args + if not arg3.tags_of_type(pt.tags.PrefixNamed): + arg3 = arg3.tagged(pt.tags.PrefixNamed("mass_inv_inp")) + if not arg3.tags_of_type(pt.tags.ImplStored): + arg3 = arg3.tagged(pt.tags.ImplStored()) + + return pt.Einsum(expr.access_descriptors, + (arg1, arg2, arg3), + expr.axes, + expr.redn_axis_to_redn_descr, + expr.index_to_access_descr, + expr.tags) + else: + return expr + + dag = pt.transform.map_and_copy(dag, materialize_inverse_mass_inputs) + + # }}} + + # {{{ materialize all einsums + + def materialize_all_einsums_or_reduces(expr): + from pytato.raising import (index_lambda_to_high_level_op, + ReduceOp) + + if isinstance(expr, pt.Einsum): + return expr.tagged(pt.tags.ImplStored()) + elif (isinstance(expr, pt.IndexLambda) + and isinstance(index_lambda_to_high_level_op(expr), ReduceOp)): + return expr.tagged(pt.tags.ImplStored()) + else: + return expr + + with ProcessLogger(logger, + "transform_dag.materialize_all_einsums_or_reduces"): + dag = pt.transform.map_and_copy(dag, materialize_all_einsums_or_reduces) + + # }}} + + # {{{ infer axis types + + from meshmode.pytato_utils import unify_discretization_entity_tags + + with ProcessLogger(logger, "transform_dag.infer_axes_tags"): + dag = unify_discretization_entity_tags(dag) + + # }}} + + # {{{ /!\ Remove tags from Loopy call results. + # See + + def untag_loopy_call_results(expr): + from pytato.loopy import LoopyCallResult + if isinstance(expr, LoopyCallResult): + return expr.copy(tags=frozenset(), + axes=(pt.Axis(frozenset()),)*expr.ndim) + else: + return expr + + dag = pt.transform.map_and_copy(dag, untag_loopy_call_results) + + # }}} + + # {{{ remove broadcasts from einsums: help feinsum + + ensm_arg_rewrite_cache = {} + + def _get_rid_of_broadcasts_from_einsum(expr): + # Helpful for matching against the available expressions + # in feinsum. + + from pytato.utils import (are_shape_components_equal, + are_shapes_equal) + if isinstance(expr, pt.Einsum): + from pytato.array import EinsumElementwiseAxis + idx_to_len = expr._access_descr_to_axis_len() + new_access_descriptors = [] + new_args = [] + inp_gatherer = pt.transform.InputGatherer() + access_descr_to_axes = dict(expr.redn_axis_to_redn_descr) + for iax, axis in enumerate(expr.axes): + access_descr_to_axes[EinsumElementwiseAxis(iax)] = axis + + for access_descrs, arg in zip(expr.access_descriptors, + expr.args): + new_shape = [] + new_access_descrs = [] + new_axes = [] + for iaxis, (access_descr, axis_len) in enumerate( + zip(access_descrs, + arg.shape)): + if not are_shape_components_equal(axis_len, + idx_to_len[access_descr]): + assert are_shape_components_equal(axis_len, 1) + if any(isinstance(inp, pt.Placeholder) + for inp in inp_gatherer(arg)): + # do not get rid of broadcasts from parameteric + # data. + new_shape.append(axis_len) + new_access_descrs.append(access_descr) + new_axes.append(arg.axes[iaxis]) + else: + new_axes.append(arg.axes[iaxis]) + new_shape.append(axis_len) + new_access_descrs.append(access_descr) + + if not are_shapes_equal(new_shape, arg.shape): + assert len(new_axes) == len(new_shape) + arg_to_freeze = (arg.reshape(new_shape) + .copy(axes=tuple( + access_descr_to_axes[acc_descr] + for acc_descr in new_access_descrs))) + + try: + new_arg = ensm_arg_rewrite_cache[arg_to_freeze] + except KeyError: + new_arg = self.thaw(self.freeze(arg_to_freeze)) + ensm_arg_rewrite_cache[arg_to_freeze] = new_arg + + arg = new_arg + + assert arg.ndim == len(new_access_descrs) + new_args.append(arg) + new_access_descriptors.append(tuple(new_access_descrs)) + + return pt.Einsum(tuple(new_access_descriptors), + tuple(new_args), + tags=expr.tags, + axes=expr.axes, + redn_axis_to_redn_descr=(expr + .redn_axis_to_redn_descr), + index_to_access_descr=expr.index_to_access_descr) + else: + return expr + + dag = pt.transform.map_and_copy(dag, _get_rid_of_broadcasts_from_einsum) + + # }}} + + # {{{ remove any PartID tags + + from pytato.distributed import PartIDTag + + def remove_part_id_tags(expr): + if isinstance(expr, pt.Array) and expr.tags_of_type(PartIDTag): + tag, = expr.tags_of_type(PartIDTag) + return expr.without_tags(tag) + else: + return expr + + dag = pt.transform.map_and_copy(dag, remove_part_id_tags) + + # }}} + + # {{{ attach FEMEinsumTag tags + + dag_outputs = frozenset(dag._data.values()) + + def add_fem_einsum_tags(expr): + if isinstance(expr, pt.Einsum): + from pytato.array import (EinsumElementwiseAxis, + EinsumReductionAxis) + assert expr.tags_of_type(pt.tags.ImplStored) + ensm_indices = [] + for arg, access_descrs in zip(expr.args, + expr.access_descriptors): + arg_indices = [] + for iaxis, access_descr in enumerate(access_descrs): + try: + discr_tag = next( + iter(arg + .axes[iaxis] + .tags_of_type(DiscretizationEntityAxisTag))) + except StopIteration: + raise NotAnFEMEinsumError(expr) + else: + if isinstance(access_descr, EinsumElementwiseAxis): + arg_indices.append(FreeEinsumIndex(discr_tag, + arg.shape[iaxis])) + elif isinstance(access_descr, EinsumReductionAxis): + arg_indices.append(SummationEinsumIndex( + discr_tag, + arg.shape[iaxis])) + else: + raise NotImplementedError(access_descr) + ensm_indices.append(tuple(arg_indices)) + + return expr.tagged(FEMEinsumTag(tuple(ensm_indices))) + elif (isinstance(expr, pt.Array) + and (expr.tags_of_type(pt.tags.ImplStored) + or expr in dag_outputs)): + if (isinstance(expr, pt.IndexLambda) + and expr.var_to_reduction_descr): + raise NotImplementedError("pure reductions not implemented") + else: + discr_tags = [] + for axis in expr.axes: + try: + discr_tag = next( + iter(axis.tags_of_type(DiscretizationEntityAxisTag))) + except StopIteration: + raise NotAnFEMEinsumError(expr) + else: + discr_tags.append(discr_tag) + + fem_ensm_tag = FEMEinsumTag( + (tuple(FreeEinsumIndex(discr_tag, dim) + for dim, discr_tag in zip(expr.shape, + discr_tags)),) * 2 + ) + + return expr.tagged(fem_ensm_tag) + + else: + return expr + + try: + dag = pt.transform.map_and_copy(dag, add_fem_einsum_tags) + except NotAnFEMEinsumError: + pass + + # }}} + + # {{{ untag outputs tagged from being tagged ImplStored + + def _untag_impl_stored(expr): + if isinstance(expr, pt.InputArgumentBase): + return expr + else: + return expr.without_tags(pt.tags.ImplStored(), + verify_existence=False) + + dag = pt.make_dict_of_named_arrays({ + name: _untag_impl_stored(named_ary.expr) + for name, named_ary in dag.items()}) + + # }}} + + return dag + + def transform_loopy_program(self, t_unit): + import loopy as lp + from functools import reduce + from arraycontext.impl.pytato.compile import FromArrayContextCompile + + original_t_unit = t_unit + + # from loopy.transform.instruction import simplify_indices + # t_unit = simplify_indices(t_unit) + + knl = t_unit.default_entrypoint + + logger.info(f"Transforming kernel with {len(knl.instructions)} statements.") + + # {{{ fallback: if the inames are not inferred which mesh entity they + # iterate over. + + for iname in knl.all_inames(): + if not knl.iname_tags_of_type(iname, DiscretizationEntityAxisTag): + warn("Falling back to a slower transformation strategy as some" + " loops are uninferred which mesh entity they belong to.", + stacklevel=2) + + return super().transform_loopy_program(original_t_unit) + + for insn in knl.instructions: + for assignee in insn.assignee_var_names(): + var = knl.get_var_descriptor(assignee) + if not var.tags_of_type(FEMEinsumTag): + warn("Falling back to a slower transformation strategy as some" + " instructions couldn't be inferred as einsums", + stacklevel=2) + + return super().transform_loopy_program(original_t_unit) + + # }}} + + # {{{ hardcode offset to 0 (sorry humanity) + + knl = knl.copy(args=[arg.copy(offset=0) + for arg in knl.args]) + + # }}} + + # {{{ loop fusion + + with ProcessLogger(logger, "Loop Fusion"): + knl = fuse_same_discretization_entity_loops(knl) + + # }}} + + # {{{ align kernels for fused einsums + + knl = _prepare_kernel_for_parallelization(knl) + knl = _combine_einsum_domains(knl) + + # }}} + + # {{{ array contraction + + with ProcessLogger(logger, "Array Contraction"): + knl = contract_arrays(knl, t_unit.callables_table) + + # }}} + + # {{{ Stats Collection (Disabled) + + if 0: + with ProcessLogger(logger, "Counting Kernel Ops"): + from loopy.kernel.array import ArrayBase + from pytools import product + knl = knl.copy( + silenced_warnings=(knl.silenced_warnings + + ["insn_count_subgroups_upper_bound", + "summing_if_branches_ops"])) + + t_unit = t_unit.with_kernel(knl) + + op_map = lp.get_op_map(t_unit, subgroup_size=32) + + c64_ops = {op_type: (op_map.filter_by(dtype=[np.complex64], + name=op_type, + kernel_name=knl.name) + .eval_and_sum({})) + for op_type in ["add", "mul", "div"]} + c128_ops = {op_type: (op_map.filter_by(dtype=[np.complex128], + name=op_type, + kernel_name=knl.name) + .eval_and_sum({})) + for op_type in ["add", "mul", "div"]} + f32_ops = ((op_map.filter_by(dtype=[np.float32], + kernel_name=knl.name) + .eval_and_sum({})) + + (2 * c64_ops["add"] + + 6 * c64_ops["mul"] + + (6 + 3 + 2) * c64_ops["div"])) + f64_ops = ((op_map.filter_by(dtype=[np.float64], + kernel_name="_pt_kernel") + .eval_and_sum({})) + + (2 * c128_ops["add"] + + 6 * c128_ops["mul"] + + (6 + 3 + 2) * c128_ops["div"])) + + # {{{ footprint gathering + + nfootprint_bytes = 0 + + for ary in knl.args: + if (isinstance(ary, ArrayBase) + and ary.address_space == lp.AddressSpace.GLOBAL): + nfootprint_bytes += (product(ary.shape) + * ary.dtype.itemsize) + + for ary in knl.temporary_variables.values(): + if ary.address_space == lp.AddressSpace.GLOBAL: + # global temps would be written once and read once + nfootprint_bytes += (2 * product(ary.shape) + * ary.dtype.itemsize) + + # }}} + + if f32_ops: + logger.info(f"Single-prec. GFlOps: {f32_ops * 1e-9}") + if f64_ops: + logger.info(f"Double-prec. GFlOps: {f64_ops * 1e-9}") + logger.info(f"Footprint GBs: {nfootprint_bytes * 1e-9}") + + # }}} + + # {{{ check whether we can parallelize the kernel + + try: + iel_to_idofs = _get_iel_to_idofs(knl) + except NotImplementedError as err: + if knl.tags_of_type(FromArrayContextCompile): + raise err + else: + warn("FusionContractorArrayContext.transform_loopy_program not" + " broad enough (yet). Falling back to a possibly slower" + " transformation strategy.") + return super().transform_loopy_program(original_t_unit) + + # }}} + + # {{{ insert barriers between consecutive iel-loops + + toposorted_iels = _get_element_loop_topo_sorted_order(knl) + + for iel_pred, iel_succ in zip(toposorted_iels[:-1], + toposorted_iels[1:]): + knl = lp.add_barrier(knl, + insn_before=f"iname:{iel_pred}", + insn_after=f"iname:{iel_succ}") + + # }}} + + # {{{ Parallelization strategy: Use feinsum + + t_unit = t_unit.with_kernel(knl) + del knl + + if False and t_unit.default_entrypoint.tags_of_type(FromArrayContextCompile): + # FIXME: Enable this branch, WIP for now and hence disabled it. + from loopy.match import ObjTagged + import feinsum as fnsm + from meshmode.feinsum_transformations import FEINSUM_TO_TRANSFORMS + + assert all(insn.tags_of_type(EinsumTag) + for insn in t_unit.default_entrypoint.instructions + if isinstance(insn, lp.MultiAssignmentBase) + ) + + einsum_tags = reduce( + frozenset.union, + (insn.tags_of_type(EinsumTag) + for insn in t_unit.default_entrypoint.instructions), + frozenset()) + for ensm_tag in sorted(einsum_tags, + key=lambda x: sorted(x.orig_loop_nest)): + if reduce(frozenset.union, + (insn.reduction_inames() + for insn in (t_unit.default_entrypoint.instructions) + if ensm_tag in insn.tags), + frozenset()): + fused_einsum = fnsm.match_einsum(t_unit, ObjTagged(ensm_tag)) + else: + # elementwise loop + fused_einsum = _get_elementwise_einsum(t_unit, ensm_tag) + + try: + fnsm_transform = FEINSUM_TO_TRANSFORMS[ + fnsm.normalize_einsum(fused_einsum)] + except KeyError: + fnsm.query(fused_einsum, + self.queue.context, + err_if_no_results=True) + 1/0 + + t_unit = fnsm_transform(t_unit, + insn_match=ObjTagged(ensm_tag)) + else: + knl = t_unit.default_entrypoint + for iel, idofs in sorted(iel_to_idofs.items()): + if idofs: + nunit_dofs = {knl.get_constant_iname_length(idof) + for idof in idofs} + idof, = idofs + + l_one_size, l_zero_size = _get_group_size_for_dof_array_loop( + nunit_dofs) + + knl = lp.split_iname(knl, iel, l_one_size, + inner_tag="l.1", outer_tag="g.0") + knl = lp.split_iname(knl, idof, l_zero_size, + inner_tag="l.0", outer_tag="unr") + else: + knl = lp.split_iname(knl, iel, 32, + outer_tag="g.0", inner_tag="l.0") + + t_unit = t_unit.with_kernel(knl) + + # }}} + + t_unit = lp.linearize(lp.preprocess_kernel(t_unit)) + t_unit = _alias_global_temporaries(t_unit) + t_unit = _rewrite_tvs_as_base_plus_offset(t_unit, self.queue.device) + + return t_unit + # vim: foldmethod=marker diff --git a/meshmode/pytato_utils.py b/meshmode/pytato_utils.py index 986c65fff..046981678 100644 --- a/meshmode/pytato_utils.py +++ b/meshmode/pytato_utils.py @@ -1,19 +1,17 @@ import pyopencl.array as cl_array -import kanren import pytato as pt -import unification import logging from functools import partial, reduce from arraycontext.impl.pytato.fake_numpy import PytatoFakeNumpyNamespace from arraycontext import rec_map_reduce_array_container from meshmode.transform_metadata import DiscretizationEntityAxisTag -from pytato.loopy import LoopyCall -from pytato.array import EinsumElementwiseAxis, EinsumReductionAxis from pytato.transform import ArrayOrNames +from pytato.transform.metadata import ( + AxesTagsEquationCollector as BaseAxesTagsEquationCollector) from arraycontext import ArrayContainer from arraycontext.container.traversal import rec_map_array_container -from typing import Set, Mapping, Tuple, Union +from typing import Union logger = logging.getLogger(__name__) @@ -78,404 +76,28 @@ def max(self, a, axis=None): # {{{ solve for discretization metadata for arrays' axes -class DiscretizationEntityConstraintCollector(pt.transform.Mapper): - """ - .. warning:: - - Instances of this mapper type store state that are only for visiting a - single DAG. Using a single instance for collecting the constraints on - multiple DAGs is undefined behavior. - """ - def __init__(self): - super().__init__() - self._visited_ids: Set[int] = set() - - # axis_to_var: mapping from (array, iaxis) to the kanren variable to be - # used for unification. - self.axis_to_tag_var: Mapping[Tuple[pt.Array, int], - unification.variable.Var] = {} - self.variables_to_solve: Set[unification.variable.Var] = set() - self.constraints = [] - - # type-ignore reason: CachedWalkMapper.rec's type does not match - # WalkMapper.rec's type - def rec(self, expr: ArrayOrNames) -> None: # type: ignore - if id(expr) in self._visited_ids: - return - - # type-ignore reason: super().rec expects either 'Array' or - # 'AbstractResultWithNamedArrays', passed 'ArrayOrNames' - super().rec(expr) # type: ignore - self._visited_ids.add(id(expr)) - - def get_kanren_var_for_axis_tag(self, - expr: pt.Array, - iaxis: int - ) -> unification.variable.Var: - key = (expr, iaxis) - - if key not in self.axis_to_tag_var: - self.axis_to_tag_var[key] = kanren.var() - - return self.axis_to_tag_var[key] - - def _record_all_axes_to_be_solved_if_impl_stored(self, expr): - if expr.tags_of_type(pt.tags.ImplStored): - for iaxis in range(expr.ndim): - self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(expr, - iaxis)) - - def _record_all_axes_to_be_solved(self, expr): - for iaxis in range(expr.ndim): - self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(expr, - iaxis)) - - def record_constraint(self, lhs, rhs): - self.constraints.append((lhs, rhs)) - - def record_eq_constraints_from_tags(self, expr: pt.Array) -> None: - for iaxis, axis in enumerate(expr.axes): - if axis.tags_of_type(DiscretizationEntityAxisTag): - discr_tag, = axis.tags_of_type(DiscretizationEntityAxisTag) - axis_var = self.get_kanren_var_for_axis_tag(expr, iaxis) - self.record_constraint(axis_var, discr_tag) - - def _map_input_base(self, expr: pt.InputArgumentBase - ) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - - for dim in expr.shape: - if isinstance(dim, pt.Array): - self.rec(dim) - - map_placeholder = _map_input_base - map_data_wrapper = _map_input_base - map_size_param = _map_input_base - - def map_index_lambda(self, expr: pt.IndexLambda) -> None: - from pytato.utils import are_shape_components_equal - from pytato.raising import index_lambda_to_high_level_op - from pytato.raising import (BinaryOp, FullOp, WhereOp, - BroadcastOp, C99CallOp, ReduceOp) - - # {{{ record constraints for expr and its subexprs. - - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - - for dim in expr.shape: - if isinstance(dim, pt.Array): - self.rec(dim) - - for bnd in expr.bindings.values(): - self.rec(bnd) - - # }}} - - hlo = index_lambda_to_high_level_op(expr) - - if isinstance(hlo, BinaryOp): - subexprs = (hlo.x1, hlo.x2) - elif isinstance(hlo, WhereOp): - subexprs = (hlo.condition, hlo.then, hlo.else_) - elif isinstance(hlo, FullOp): - # A full-op does not impose any constraints - subexprs = () - elif isinstance(hlo, BroadcastOp): - subexprs = (hlo.x,) - elif isinstance(hlo, C99CallOp): - subexprs = hlo.args - elif isinstance(hlo, ReduceOp): - # {{{ ReduceOp doesn't quite involve broadcasting - - i_out_axis = 0 - for i_in_axis in range(hlo.x.ndim): - if i_in_axis not in hlo.axes: - in_tag_var = self.get_kanren_var_for_axis_tag(hlo.x, - i_in_axis) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - i_out_axis) - self.record_constraint(in_tag_var, out_tag_var) - i_out_axis += 1 - - assert i_out_axis == expr.ndim - - # }}} - - for axis in hlo.axes: - self.variables_to_solve.add(self.get_kanren_var_for_axis_tag(hlo.x, - axis)) - return - - else: - raise NotImplementedError(type(hlo)) - - for subexpr in subexprs: - if isinstance(subexpr, pt.Array): - for i_in_axis, i_out_axis in zip( - range(subexpr.ndim), - range(expr.ndim-subexpr.ndim, expr.ndim)): - in_dim = subexpr.shape[i_in_axis] - out_dim = expr.shape[i_out_axis] - if are_shape_components_equal(in_dim, out_dim): - in_tag_var = self.get_kanren_var_for_axis_tag(subexpr, - i_in_axis) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - i_out_axis) - - self.record_constraint(in_tag_var, out_tag_var) - else: - # broadcasted axes, cannot belong to the same - # discretization entity. - assert are_shape_components_equal(in_dim, 1) - - def map_stack(self, expr: pt.Stack) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - # TODO; I think the axis corresponding to 'axis' need not be solved. - for ary in expr.arrays: - self.rec(ary) - - for iaxis in range(expr.ndim): - for ary in expr.arrays: - if iaxis < expr.axis: - in_tag_var = self.get_kanren_var_for_axis_tag(ary, - iaxis) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - iaxis) - - self.record_constraint(in_tag_var, out_tag_var) - elif iaxis == expr.axis: - pass - elif iaxis > expr.axis: - in_tag_var = self.get_kanren_var_for_axis_tag(ary, - iaxis-1) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - iaxis) - - self.record_constraint(in_tag_var, out_tag_var) - else: - raise AssertionError - - def map_concatenate(self, expr: pt.Concatenate) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - # TODO; I think the axis corresponding to 'axis' need not be solved. - for ary in expr.arrays: - self.rec(ary) - - for ary in expr.arrays: - assert ary.ndim == expr.ndim - for iaxis in range(expr.ndim): - if iaxis != expr.axis: - # non-concatenated axes share the dimensions. - in_tag_var = self.get_kanren_var_for_axis_tag(ary, - iaxis) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - iaxis) - self.record_constraint(in_tag_var, out_tag_var) - - def map_axis_permutation(self, expr: pt.AxisPermutation - ) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - self.rec(expr.array) - - assert expr.ndim == expr.array.ndim - - for out_axis in range(expr.ndim): - in_axis = expr.axis_permutation[out_axis] - out_tag = self.get_kanren_var_for_axis_tag(expr, out_axis) - in_tag = self.get_kanren_var_for_axis_tag(expr, in_axis) - self.record_constraint(out_tag, in_tag) - - def map_basic_index(self, expr: pt.IndexBase) -> None: - from pytato.array import NormalizedSlice - from pytato.utils import are_shape_components_equal - - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - self.rec(expr.array) - - i_out_axis = 0 - - assert len(expr.indices) == expr.array.ndim - - for i_in_axis, idx in enumerate(expr.indices): - if isinstance(idx, int): - pass - else: - assert isinstance(idx, NormalizedSlice) - if (idx.step == 1 - and are_shape_components_equal(idx.start, 0) - and are_shape_components_equal(idx.stop, - expr.array.shape[i_in_axis])): - - i_in_axis_tag = self.get_kanren_var_for_axis_tag(expr.array, - i_in_axis) - i_out_axis_tag = self.get_kanren_var_for_axis_tag(expr, - i_out_axis) - self.record_constraint(i_in_axis_tag, i_out_axis_tag) - - i_out_axis += 1 - - def map_contiguous_advanced_index(self, - expr: pt.AdvancedIndexInContiguousAxes - ) -> None: - from pytato.array import NormalizedSlice - from pytato.utils import (partition, get_shape_after_broadcasting, - are_shapes_equal, are_shape_components_equal) - - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - self.rec(expr.array) - for idx in expr.indices: - if isinstance(idx, pt.Array): - self.rec(idx) - - i_adv_indices, i_basic_indices = partition( - lambda idx: isinstance(expr.indices[idx], NormalizedSlice), - range(len(expr.indices))) - npre_advanced_basic_indices = len([i_idx - for i_idx in i_basic_indices - if i_idx < i_adv_indices[0]]) - npost_advanced_basic_indices = len([i_idx - for i_idx in i_basic_indices - if i_idx > i_adv_indices[-1]]) - - indirection_arrays = [expr.indices[i_idx] for i_idx in i_adv_indices] - assert are_shapes_equal( - get_shape_after_broadcasting(indirection_arrays), - expr.shape[ - npre_advanced_basic_indices:expr.ndim-npost_advanced_basic_indices]) - - for subexpr in indirection_arrays: - if isinstance(subexpr, pt.Array): - for i_in_axis, i_out_axis in zip( - range(subexpr.ndim), - range(expr.ndim-subexpr.ndim+npre_advanced_basic_indices, - expr.ndim-npost_advanced_basic_indices)): - in_dim = subexpr.shape[i_in_axis] - out_dim = expr.shape[i_out_axis] - if are_shape_components_equal(in_dim, out_dim): - in_tag_var = self.get_kanren_var_for_axis_tag(subexpr, - i_in_axis) - out_tag_var = self.get_kanren_var_for_axis_tag(expr, - i_out_axis) - - self.record_constraint(in_tag_var, out_tag_var) - else: - # broadcasted axes, cannot belong to the same - # discretization entity. - assert are_shape_components_equal(in_dim, 1) - - def map_non_contiguous_advanced_index(self, - expr: pt.AdvancedIndexInNoncontiguousAxes - ) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - self.rec(expr.array) - for idx in expr.indices: - if isinstance(idx, pt.Array): - self.rec(idx) - +class AxesTagsEquationCollector(BaseAxesTagsEquationCollector): def map_reshape(self, expr: pt.Reshape) -> None: - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - self.rec(expr.array) - # we can add constraints to reshape that only include new axes in its - # reshape. - # Other reshapes do not 'conserve' the types in our type-system. - # Well *what if*. Let's just say this type inference fails for - # non-trivial 'reshapes'. So, what are the 'trivial' reshapes? - # trivial reshapes: - # (x1, x2, ... xn) -> ((1,)*, x1, (1,)*, x2, (1,)*, x3, (1,)*, ..., xn, 1*) - # given all(x1!=1, x2!=1, x3!=1, .. xn!= 1) - if ((1 not in (expr.array.shape)) # leads to ambiguous newaxis + super().map_reshape(expr) + + if (expr.size > 0 + and (1 not in (expr.array.shape)) # leads to ambiguous newaxis and (set(expr.shape) <= (set(expr.array.shape) | {1}))): i_in_axis = 0 for i_out_axis, dim in enumerate(expr.shape): if dim != 1: assert dim == expr.array.shape[i_in_axis] - i_in_axis_tag = self.get_kanren_var_for_axis_tag(expr.array, - i_in_axis) - i_out_axis_tag = self.get_kanren_var_for_axis_tag(expr, - i_out_axis) - self.record_constraint(i_in_axis_tag, i_out_axis_tag) + self.record_equation( + self.get_var_for_axis(expr.array, + i_in_axis), + self.get_var_for_axis(expr, + i_out_axis) + ) i_in_axis += 1 else: # print(f"Skipping: {expr.array.shape} -> {expr.shape}") # Wacky reshape => bail. - return - - def map_einsum(self, expr: pt.Einsum) -> None: - - self.record_eq_constraints_from_tags(expr) - self._record_all_axes_to_be_solved_if_impl_stored(expr) - - for arg in expr.args: - self.rec(arg) - - descr_to_tag = {} - for iaxis in range(expr.ndim): - descr_to_tag[EinsumElementwiseAxis(iaxis)] = ( - self.get_kanren_var_for_axis_tag(expr, iaxis)) - - for access_descrs, arg in zip(expr.access_descriptors, - expr.args): - # if an einsum is stored => every argument's axes must - # also be inferred, even those that are getting reduced. - for iarg_axis, descr in enumerate(access_descrs): - in_tag_var = self.get_kanren_var_for_axis_tag(arg, - iarg_axis) - - if descr in descr_to_tag: - self.record_constraint(descr_to_tag[descr], in_tag_var) - else: - descr_to_tag[descr] = in_tag_var - - if isinstance(descr, EinsumReductionAxis): - self.variables_to_solve.add(in_tag_var) - - def map_dict_of_named_arrays(self, expr: pt.DictOfNamedArrays - ) -> None: - for _, subexpr in sorted(expr._data.items()): - self.rec(subexpr) - self._record_all_axes_to_be_solved(subexpr) - - def map_loopy_call(self, expr: LoopyCall) -> None: - for _, subexpr in sorted(expr.bindings.items()): - if isinstance(subexpr, pt.Array): - if not isinstance(subexpr, pt.InputArgumentBase): - self._record_all_axes_to_be_solved(subexpr) - self.rec(subexpr) - - # there's really no good way to propagate the metadata in this case. - # One *could* raise the loopy kernel instruction expressions to - # high level ops, but that's really involved and probably not worth it. - - def map_named_array(self, expr: pt.NamedArray) -> None: - self.record_eq_constraints_from_tags(expr) - self.rec(expr._container) - - def map_distributed_send_ref_holder(self, - expr: pt.DistributedSendRefHolder - ) -> None: - self.record_eq_constraints_from_tags(expr) - self.rec(expr.passthrough_data) - for idim in range(expr.ndim): - assert (expr.passthrough_data.shape[idim] - == expr.shape[idim]) - self.record_constraint( - self.get_kanren_var_for_axis_tag(expr.passthrough_data, - idim), - self.get_kanren_var_for_axis_tag(expr, idim) - ) - - def map_distributed_recv(self, - expr: pt.DistributedRecv) -> None: - self.record_eq_constraints_from_tags(expr) + pass def unify_discretization_entity_tags(expr: Union[ArrayContainer, ArrayOrNames] @@ -484,152 +106,11 @@ def unify_discretization_entity_tags(expr: Union[ArrayContainer, ArrayOrNames] return rec_map_array_container(unify_discretization_entity_tags, expr) - from collections import defaultdict - discr_unification_helper = DiscretizationEntityConstraintCollector() - discr_unification_helper(expr) - tag_var_to_axis = {} - variables_to_solve = [] - - for (axis, var) in discr_unification_helper.axis_to_tag_var.items(): - tag_var_to_axis[var] = axis - if var in discr_unification_helper.variables_to_solve: - variables_to_solve.append(var) - - lhs = [cnstrnt[0] for cnstrnt in discr_unification_helper.constraints] - rhs = [cnstrnt[1] for cnstrnt in discr_unification_helper.constraints] - assert len(lhs) == len(rhs) - solutions = {} - - for i_retry in range(MAX_UNIFY_RETRIES): - old_solutions = solutions.copy() - solutions = unification.unify(lhs, rhs, - {l_expr: r_expr - for l_expr, r_expr in solutions.items() - if isinstance(r_expr, - DiscretizationEntityAxisTag)}) - if solutions == old_solutions: - logger.info(f"Unification converged after {i_retry} iterations.") - break - else: - logger.warn(f"Could not converge after {MAX_UNIFY_RETRIES} iterations.") - - # Ideally it might be better to enable this, but that would be too - # restrictive as not all computation graphs result in DOFArray ouptuts - # if not (frozenset(variables_to_solve) <= frozenset(solutions)): - # raise RuntimeError("Unification failed.") - - # ary_to_axes_tags: mapping from array to a mapping from iaxis to the - # solved tag. - ary_to_axes_tags = defaultdict(dict) - for var in solutions: - ary, axis = tag_var_to_axis[var] - if isinstance(solutions[var], DiscretizationEntityAxisTag): - ary_to_axes_tags[ary][axis] = solutions[var] - if var in variables_to_solve and ( - not isinstance(solutions[var], DiscretizationEntityAxisTag)): - raise RuntimeError(f"Could not solve for {var}.") - - def attach_tags(expr: ArrayOrNames) -> ArrayOrNames: - if not isinstance(expr, pt.Array): - return expr - - for iaxis, solved_tag in ary_to_axes_tags[expr].items(): - if expr.axes[iaxis].tags_of_type(DiscretizationEntityAxisTag): - discr_tag, = (expr - .axes[iaxis] - .tags_of_type(DiscretizationEntityAxisTag)) - assert discr_tag == solved_tag - else: - if not isinstance(solved_tag, DiscretizationEntityAxisTag): - actual_tag = discr_unification_helper.axis_to_tag_var[(expr, - iaxis)] - assert actual_tag in discr_unification_helper.variables_to_solve - assert actual_tag in variables_to_solve - raise ValueError(f"In {expr!r}, axis={iaxis}'s type cannot be " - "inferred.") - expr = expr.with_tagged_axis(iaxis, solved_tag) - - if isinstance(expr, pt.Einsum): - redn_descr_to_entity_type = {} - for access_descrs, arg in zip(expr.access_descriptors, - expr.args): - for iaxis, access_descr in enumerate(access_descrs): - if isinstance(access_descr, EinsumReductionAxis): - redn_descr_to_entity_type[access_descr] = ( - ary_to_axes_tags[arg][iaxis]) - - if (frozenset(redn_descr_to_entity_type) - != frozenset(expr.redn_descr_to_redn_dim)): - raise ValueError - - for redn_descr, solved_tag in redn_descr_to_entity_type.items(): - if not isinstance(solved_tag, DiscretizationEntityAxisTag): - raise ValueError(f"In {expr!r}, redn_descr={redn_descr}'s" - " type cannot be inferred.") - expr = expr.with_tagged_redn_dim(redn_descr, solved_tag) - - if isinstance(expr, pt.IndexLambda): - from pytato.raising import (index_lambda_to_high_level_op, - ReduceOp) - - hlo = index_lambda_to_high_level_op(expr) - if isinstance(hlo, ReduceOp): - for iaxis in hlo.axes: - solved_tag = ary_to_axes_tags[hlo.x][iaxis] - if not isinstance(solved_tag, DiscretizationEntityAxisTag): - raise ValueError(f"In {expr!r}, redn_descr={iaxis}'s" - " type cannot be inferred.") - - expr = expr.with_tagged_redn_dim(iaxis, solved_tag) - - return expr - - return pt.transform.map_and_copy(expr, attach_tags) + return pt.unify_axes_tags(expr, + tag_t=DiscretizationEntityAxisTag, + equations_collector_t=AxesTagsEquationCollector) # }}} -class UnInferredStoredArrayCatcher(pt.transform.CachedWalkMapper): - """ - Raises a :class:`ValueError` if a stored array has axes without a - :class:`DiscretizationEntityAxisTag` tagged to it. - """ - def post_visit(self, expr: ArrayOrNames) -> None: - if (isinstance(expr, pt.Array) - and expr.tags_of_type(pt.tags.ImplStored)): - if any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 - for axis in expr.axes): - raise ValueError(f"{expr!r} doesn't have all its axes inferred.") - - if (isinstance(expr, pt.IndexLambda) - and any(len(redn_dim.tags_of_type(DiscretizationEntityAxisTag)) != 1 - for redn_dim in expr.reduction_dims.values())): - raise ValueError(f"{expr!r} doesn't have all its redn axes inferred.") - - if (isinstance(expr, pt.Einsum) - and any(len(redn_dim.tags_of_type(DiscretizationEntityAxisTag)) != 1 - for redn_dim in expr.redn_descr_to_redn_dim.values())): - raise ValueError(f"{expr!r} doesn't have all its redn axes inferred.") - - if isinstance(expr, pt.DictOfNamedArrays): - if any(any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 - for axis in subexpr.axes) - for subexpr in expr._data.values()): - raise ValueError(f"{expr!r} doesn't have all its axes inferred.") - - from pytato.loopy import LoopyCall - - if isinstance(expr, LoopyCall): - if any(any(len(axis.tags_of_type(DiscretizationEntityAxisTag)) != 1 - for axis in subexpr.axes) - for subexpr in expr.bindings.values() - if (isinstance(subexpr, pt.Array) - and not isinstance(subexpr, pt.InputArgumentBase) - and subexpr.ndim != 0)): - raise ValueError(f"{expr!r} doesn't have all its axes inferred.") - - -def are_all_stored_arrays_inferred(expr: ArrayOrNames): - UnInferredStoredArrayCatcher()(expr) - # vim: fdm=marker From 5d98d872503e31abcad5a2c56db1a51ebed6ae47 Mon Sep 17 00:00:00 2001 From: Kaushik Kulkarni Date: Sat, 12 Mar 2022 13:25:57 -0600 Subject: [PATCH 148/154] adds feinsum, kanren to deps --- requirements.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35a924099..e99abecc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ git+https://github.com/inducer/pyvisfile.git#egg=pyvisfile git+https://github.com/inducer/modepy.git#egg=modepy git+https://github.com/inducer/pyopencl.git#egg=pyopencl git+https://github.com/inducer/islpy.git#egg=islpy -git+https://github.com/inducer/pytato.git#egg=pytato +git+https://github.com/kaushikcfd/pytato.git#egg=pytato # required by pytential, which is in turn needed for some tests git+https://github.com/inducer/pymbolic.git#egg=pymbolic @@ -27,3 +27,8 @@ git+https://github.com/inducer/pymetis.git#egg=pymetis # for examples/tp-lagrange-stl.py numpy-stl + + +# for FusionContractorActx transforms +git+https://github.com/kaushikcfd/feinsum.git#egg=feinsum +git+https://github.com/pythological/kanren.git#egg=miniKanren From 130612213ca3b2df738e1d243498604fee0d1bad Mon Sep 17 00:00:00 2001 From: Matthew Smith Date: Wed, 16 Mar 2022 11:25:56 -0500 Subject: [PATCH 149/154] add mpi_distribute --- meshmode/distributed.py | 97 ++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/meshmode/distributed.py b/meshmode/distributed.py index d99e90dc8..532d52bb7 100644 --- a/meshmode/distributed.py +++ b/meshmode/distributed.py @@ -3,6 +3,7 @@ .. autoclass:: InterRankBoundaryInfo .. autoclass:: MPIBoundaryCommSetupHelper +.. autofunction:: mpi_distribute .. autofunction:: get_partition_by_pymetis .. autofunction:: membership_list_to_map .. autofunction:: get_connected_parts @@ -37,6 +38,7 @@ """ from dataclasses import dataclass +from contextlib import contextmanager import numpy as np from typing import List, Set, Union, Mapping, cast, Sequence, TYPE_CHECKING @@ -66,12 +68,69 @@ import logging logger = logging.getLogger(__name__) -TAG_BASE = 83411 -TAG_DISTRIBUTE_MESHES = TAG_BASE + 1 - # {{{ mesh distributor +@contextmanager +def _duplicate_mpi_comm(mpi_comm): + dup_comm = mpi_comm.Dup() + try: + yield dup_comm + finally: + dup_comm.Free() + + +def mpi_distribute(mpi_comm, source_data=None, source_rank=0): + """ + Distribute data to a set of processes. + + :arg mpi_comm: An ``MPI.Intracomm`` + :arg source_data: A :class:`dict` mapping destination ranks to data to be sent. + Only present on the source rank. + :arg source_rank: The rank from which the data is being sent. + :returns: The data local to the current process if there is any, otherwise + *None*. + """ + with _duplicate_mpi_comm(mpi_comm) as mpi_comm: + num_proc = mpi_comm.Get_size() + rank = mpi_comm.Get_rank() + + local_data = None + + if rank == source_rank: + if source_data is None: + raise TypeError("source rank has no data.") + + sending_to = np.full(num_proc, False) + for dest_rank in source_data.keys(): + sending_to[dest_rank] = True + + mpi_comm.scatter(sending_to, root=source_rank) + + reqs = [] + for dest_rank, data in source_data.items(): + if dest_rank == rank: + local_data = data + logger.info("rank %d: received data", rank) + else: + reqs.append(mpi_comm.isend(data, dest=dest_rank)) + + logger.info("rank %d: sent all data", rank) + + from mpi4py import MPI + MPI.Request.waitall(reqs) + + else: + receiving = mpi_comm.scatter(None, root=source_rank) + + if receiving: + local_data = mpi_comm.recv(source=source_rank) + logger.info("rank %d: received data", rank) + + return local_data + + +# TODO: Deprecate? class MPIMeshDistributor: """ .. automethod:: is_mananger_rank @@ -99,9 +158,7 @@ def send_mesh_parts(self, mesh, part_per_element, num_parts): Sends each part to a different rank. Returns one part that was not sent to any other rank. """ - mpi_comm = self.mpi_comm - rank = mpi_comm.Get_rank() - assert num_parts <= mpi_comm.Get_size() + assert num_parts <= self.mpi_comm.Get_size() assert self.is_mananger_rank() @@ -110,38 +167,16 @@ def send_mesh_parts(self, mesh, part_per_element, num_parts): from meshmode.mesh.processing import partition_mesh parts = partition_mesh(mesh, part_num_to_elements) - local_part = None - - reqs = [] - for r, part in parts.items(): - if r == self.manager_rank: - local_part = part - else: - reqs.append(mpi_comm.isend(part, dest=r, tag=TAG_DISTRIBUTE_MESHES)) - - logger.info("rank %d: sent all mesh parts", rank) - for req in reqs: - req.wait() - - return local_part + return mpi_distribute( + self.mpi_comm, source_data=parts, source_rank=self.manager_rank) def receive_mesh_part(self): """ Returns the mesh sent by the manager rank. """ - mpi_comm = self.mpi_comm - rank = mpi_comm.Get_rank() - assert not self.is_mananger_rank(), "Manager rank cannot receive mesh" - from mpi4py import MPI - status = MPI.Status() - result = self.mpi_comm.recv( - source=self.manager_rank, tag=TAG_DISTRIBUTE_MESHES, - status=status) - logger.info("rank %d: received local mesh (size = %d)", rank, status.count) - - return result + return mpi_distribute(self.mpi_comm, source_rank=self.manager_rank) # }}} From dc3051f71a726294b1ff49723323e0b15ec455a5 Mon Sep 17 00:00:00 2001 From: Matthew Smith Date: Wed, 29 Jun 2022 14:26:10 -0500 Subject: [PATCH 150/154] add type hints to mpi_distribute --- meshmode/distributed.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/meshmode/distributed.py b/meshmode/distributed.py index 532d52bb7..161dc78f4 100644 --- a/meshmode/distributed.py +++ b/meshmode/distributed.py @@ -40,7 +40,9 @@ from dataclasses import dataclass from contextlib import contextmanager import numpy as np -from typing import List, Set, Union, Mapping, cast, Sequence, TYPE_CHECKING +from typing import ( + Any, Optional, List, Set, Union, Mapping, cast, Sequence, TYPE_CHECKING +) from arraycontext import ArrayContext from meshmode.discretization.connection import ( @@ -80,7 +82,10 @@ def _duplicate_mpi_comm(mpi_comm): dup_comm.Free() -def mpi_distribute(mpi_comm, source_data=None, source_rank=0): +def mpi_distribute( + mpi_comm: "mpi4py.MPI.Intracomm", + source_data: Optional[Mapping[int, Any]] = None, + source_rank: int = 0) -> Optional[Any]: """ Distribute data to a set of processes. @@ -88,6 +93,7 @@ def mpi_distribute(mpi_comm, source_data=None, source_rank=0): :arg source_data: A :class:`dict` mapping destination ranks to data to be sent. Only present on the source rank. :arg source_rank: The rank from which the data is being sent. + :returns: The data local to the current process if there is any, otherwise *None*. """ @@ -101,7 +107,7 @@ def mpi_distribute(mpi_comm, source_data=None, source_rank=0): if source_data is None: raise TypeError("source rank has no data.") - sending_to = np.full(num_proc, False) + sending_to = [False] * num_proc for dest_rank in source_data.keys(): sending_to[dest_rank] = True @@ -121,7 +127,7 @@ def mpi_distribute(mpi_comm, source_data=None, source_rank=0): MPI.Request.waitall(reqs) else: - receiving = mpi_comm.scatter(None, root=source_rank) + receiving = mpi_comm.scatter([], root=source_rank) if receiving: local_data = mpi_comm.recv(source=source_rank) From 86128267e4461d01276419173c71ec98ff99ee8d Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Thu, 7 Jul 2022 19:31:46 -0700 Subject: [PATCH 151/154] Add single indirection resampling code --- meshmode/discretization/connection/direct.py | 296 +++++++++++++++++-- 1 file changed, 274 insertions(+), 22 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index c265d93c4..006f5dcee 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -33,7 +33,7 @@ ConcurrentElementInameTag, ConcurrentDOFInameTag, IsDOFArray, ParameterValue, DiscretizationElementAxisTag, DiscretizationDOFAxisTag) -from pytools import memoize_in, keyed_memoize_method +from pytools import memoize_in, keyed_memoize_method, keyed_memoize_in from arraycontext import ( ArrayContext, NotAnArrayContainerError, serialize_container, deserialize_container, make_loopy_program, @@ -682,7 +682,7 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, ary[ from_element_indices[iel], dof_pick_lists[dof_pick_list_index[iel], idof] - ] + ] \ { if_present }) """, [ @@ -694,7 +694,7 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, offset=lp.auto, tags=[IsDOFArray()]), lp.GlobalArg("dof_pick_lists", pick_lists_dtype, shape="nelements_tgt, nunit_dofs_tgt", - offset=lp.auto), + offset=0), lp.GlobalArg("from_element_indices", from_el_ind_dtype, shape="nelements,", offset=lp.auto), lp.GlobalArg("dof_pick_list_index", pick_list_ind_dtype, @@ -713,10 +713,146 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, ], name="resample_by_picking_group", ) + return lp.tag_inames(t_unit, { "iel": ConcurrentElementInameTag(), "idof": ConcurrentDOFInameTag()}) + + def calc_get_indices_key(dof_pick_lists, dof_pick_list_index, from_element_indices, + ary_shape, ary_order, from_el_present): + from pyopencl.array import Array + key = (dof_pick_lists.data.int_ptr, + dof_pick_list_index.data.int_ptr, + from_element_indices.data.int_ptr, + ary_shape, + ary_order, + from_el_present.data.int_ptr if isinstance(from_el_present, Array) else None,) + return key + + + # from_el_present should be set to None if the indexing is surjective + @keyed_memoize_in(actx, + (DirectDiscretizationConnection, "calc_indices_knl"), calc_get_indices_key) + def get_indices_loopy(dof_pick_lists, dof_pick_list_index, + from_element_indices, ary_shape, ary_order, + from_el_present): + + nelements = from_element_indices.shape[0] + nelements_tgt, nunit_dofs_tgt = dof_pick_lists.shape + + if ary_order == "F": + row_stride = 1 + col_stride = ary_shape[0] + else: + row_stride = ary_shape[1] + col_stride = 1 + + if from_el_present is None: + if_present = "" + else: + if_present = "if from_el_present[iel] else -1" + + + t_unit = make_loopy_program( + [ + "{[iel]: 0 <= iel < nelements}", + "{[idof]: 0 <= idof < nunit_dofs_tgt}" + ], + f""" + indices[iel, idof] = from_element_indices[iel]*{row_stride} \ + + dof_pick_lists[dof_pick_list_index[iel], idof]*{col_stride} \ + {if_present} + """, + [ + lp.GlobalArg("indices", np.int32, + shape="nelements, nunit_dofs_tgt", + offset=lp.auto, tags=[IsDOFArray()]), + lp.ValueArg("nunit_dofs_tgt", np.int32, + tags=[ParameterValue(nunit_dofs_tgt)]), + lp.ValueArg("nelements", np.int32, + tags=[ParameterValue(nelements)]), + lp.ValueArg("nelements_tgt", np.int32, + tags=[ParameterValue(nelements_tgt)]), + lp.GlobalArg("dof_pick_lists", dof_pick_lists.dtype, + shape="nelements_tgt, nunit_dofs_tgt", + offset=lp.auto), + lp.GlobalArg("from_element_indices", from_element_indices.dtype, + shape="nelements,", offset=lp.auto), + lp.GlobalArg("dof_pick_list_index", dof_pick_list_index.dtype, + shape="nelements,", offset=lp.auto), + "...", + ], + name="resample_by_picking_calc_indices_knl", + ) + + t_unit = lp.tag_inames(t_unit, { + "iel": ConcurrentElementInameTag(), + "idof": ConcurrentDOFInameTag()}) + + if from_el_present is None: + out = actx.call_loopy(t_unit, from_element_indices=from_element_indices, + dof_pick_lists=dof_pick_lists, dof_pick_list_index=dof_pick_list_index) + else: + out = actx.call_loopy(t_unit, from_element_indices=from_element_indices, + dof_pick_lists=dof_pick_lists, dof_pick_list_index=dof_pick_list_index, + from_el_present=from_el_present) + + + return out["indices"] + + + @memoize_in(actx, + (DirectDiscretizationConnection, "resample_by_picking_single_indirection_knl")) + def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, ary_dtype, is_surjective: bool): + + if is_surjective: + if_present = "" + else: + if_present = "if indices[iel,idof] >= 0 else 0" + + + t_unit = make_loopy_program( + [ + "{[iel]: 0 <= iel < nelements}", + "{[idof]: 0 <= idof < nunit_dofs_tgt}" + ], + f""" + result[iel, idof] = ary[indices[iel,idof]] \ + {if_present} + """, + [ + lp.GlobalArg("result", ary_dtype, + shape="nelements, nunit_dofs_tgt", + offset=lp.auto, tags=[IsDOFArray()]), + # Assuming np.int32 but could it be np.int64? + lp.GlobalArg("indices", np.int32, + shape="nelements, nunit_dofs_tgt", + offset=lp.auto, tags=[IsDOFArray()]), + lp.GlobalArg("ary", ary_dtype, offset=lp.auto), + # shape="nelements_src, nunit_dofs_src", + # offset=lp.auto)#, tags=[IsDOFArray()]), + lp.ValueArg("nunit_dofs_tgt", np.int32, + tags=[ParameterValue(nunit_dofs_tgt)]), + lp.ValueArg("nelements", np.int32, + tags=[ParameterValue(nelements)]), + "...", + ], + name="resample_by_picking_single_indirection", + ) + + t_unit = lp.tag_inames(t_unit, { + "iel": ConcurrentElementInameTag(), + "idof": ConcurrentDOFInameTag()}) + + return t_unit + #order = "F" if ary.flags.f_contiguous else "C" + #out = actx.call_loopy(t_unit, ary=ary.ravel(order=order), indices=indices) + # Raveling not needed? + #out = actx.call_loopy(t_unit, ary=ary, indices=indices) + #return = out["result"] + + # }}} group_arrays = [] @@ -775,25 +911,141 @@ def group_pick_knl(nelements, nelements_src, nunit_dofs_src, pick_lists_dtype = fgpd.dof_pick_lists.dtype pick_list_ind_dtype = fgpd.dof_pick_list_index.dtype - group_array_contributions.append( - actx.call_loopy( - #group_pick_knl(fgpd.is_surjective), - group_pick_knl(nelements, nelements_src, - nunit_dofs_src, - nelements_tgt, - nunit_dofs_tgt, - result_dtype, - ary_dtype, - from_el_ind_dtype, pick_lists_dtype, - pick_list_ind_dtype, - fgpd.is_surjective), - dof_pick_lists=fgpd.dof_pick_lists, - dof_pick_list_index=fgpd.dof_pick_list_index, - ary=ary[fgpd.from_group_index], - from_element_indices=fgpd.from_element_indices, - #nunit_dofs_tgt=( - # self.to_discr.groups[i_tgrp].nunit_dofs), - **group_knl_kwargs)["result"]) + if True:#fgpd.is_surjective: # Check if can handle non-surjective cases + + dof_pick_lists = fgpd.dof_pick_lists + dof_pick_list_index = fgpd.dof_pick_list_index + data_ary = ary[fgpd.from_group_index] + from_element_indices = fgpd.from_element_indices + + + # Can delete this once the rest is verified to work. + def get_indices_numpy(dof_pick_lists, dof_pick_list_index, ary, from_element_indices): + np_dof_pick_lists = dof_pick_lists.get(queue=actx.queue) + np_dof_pick_list_index = dof_pick_list_index.get(queue=actx.queue) + np_ary = ary.get(queue=actx.queue) + np_from_element_indices = from_element_indices.get(queue=actx.queue) + + print(np_dof_pick_lists) + print(np_dof_pick_list_index) + print(np_from_element_indices) + print(np_ary.flags.f_contiguous, np_ary.flags.c_contiguous) + + nelements = np_from_element_indices.shape[0] + ndofs = np_dof_pick_lists.shape[1] + + order = "F" if np_ary.flags.f_contiguous else "C" + result = np.zeros((nelements, ndofs), order=order) + result_ind = np.zeros((nelements,ndofs),order=order, dtype=np.int32) + + if ary.flags.f_contiguous: + row_stride = 1 + col_stride = ary.shape[0] + else: + row_stride = ary.shape[1] + col_stride = 1 + + for iel in range(nelements): + for idof in range(ndofs): + # Get the row and column indices + row = np_from_element_indices[iel] + col = np_dof_pick_lists[np_dof_pick_list_index[iel], idof] + # Calculate the index + + result_ind[iel,idof] = row*row_stride + col*col_stride + + result[iel,idof] = np_ary[row,col] + + ary_flat = np_ary.flatten(order=order) + result_ary_flat = ary_flat[result_ind] + result_ary_flat_index_flat = ary_flat[result_ind.flatten(order=order)] + result_from_flat = np.reshape(result_ary_flat, result.shape, order=order) + orig_result_flat = result.flatten(order=order) + + print(result_ary_flat.shape) + print(result_from_flat.shape) + print(result_ary_flat_index_flat.shape) + print(orig_result_flat.shape) + print(result.shape) + + #assert np.allclose(result_ary_flat, orig_result_flat) + #assert np.allclose(result_ary_flat, result) + assert np.allclose(result_ary_flat, result) + assert np.allclose(result_from_flat, result) + assert np.allclose(result_ary_flat_index_flat, orig_result_flat) + print("DONE!") + #print(result_ary_flat) + #print(result) + return result_ind, result + + #indices, np_result = get_indices_numpy(dof_pick_lists, dof_pick_list_index, data_ary, from_element_indices) + #lp_indices_host = lp_indices.get(queue=actx.queue) + #assert np.allclose(indices, lp_indices_host) + + + + order = "F" if data_ary.flags.f_contiguous else "C" + lp_indices = get_indices_loopy(dof_pick_lists, dof_pick_list_index, from_element_indices, + data_ary.shape, order, None if fgpd.is_surjective else fgpd.from_el_present) + + #print("IS SURJECTIVE:", fgpd.is_surjective) + cl_result = actx.call_loopy(group_pick_knl_single_indirection(lp_indices.shape[0], + lp_indices.shape[1], ary_dtype, fgpd.is_surjective), + ary=data_ary, indices=lp_indices) + + group_array_contributions.append(cl_result["result"]) + #np_cl_result = cl_result.get(queue=actx.queue) + #assert np.allclose(np_cl_result, np_result) + #print(np_cl_result) + #print(cl_result) + + #exit() + """ + old_result = actx.call_loopy( + #group_pick_knl(fgpd.is_surjective), + group_pick_knl(nelements, nelements_src, + nunit_dofs_src, + nelements_tgt, + nunit_dofs_tgt, + result_dtype, + ary_dtype, + from_el_ind_dtype, + pick_lists_dtype, + pick_list_ind_dtype, + fgpd.is_surjective), + dof_pick_lists=fgpd.dof_pick_lists, + dof_pick_list_index=fgpd.dof_pick_list_index, + ary=ary[fgpd.from_group_index], + from_element_indices=fgpd.from_element_indices, + #nunit_dofs_tgt=( + # self.to_discr.groups[i_tgrp].nunit_dofs), + **group_knl_kwargs)["result"] + assert np.allclose(old_result.get(queue=actx.queue), cl_result["result"].get(queue=actx.queue)) + """ + + + else: + #print("==============CALLING NON-SURJECTIVE================") + group_array_contributions.append( + actx.call_loopy( + #group_pick_knl(fgpd.is_surjective), + group_pick_knl(nelements, nelements_src, + nunit_dofs_src, + nelements_tgt, + nunit_dofs_tgt, + result_dtype, + ary_dtype, + from_el_ind_dtype, + pick_lists_dtype, + pick_list_ind_dtype, + fgpd.is_surjective), + dof_pick_lists=fgpd.dof_pick_lists, + dof_pick_list_index=fgpd.dof_pick_list_index, + ary=ary[fgpd.from_group_index], + from_element_indices=fgpd.from_element_indices, + #nunit_dofs_tgt=( + # self.to_discr.groups[i_tgrp].nunit_dofs), + **group_knl_kwargs)["result"]) group_array = sum(group_array_contributions) elif cgrp.batches: From 68816948e5de214edae918eac4fd99c868ebc9a1 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Fri, 8 Jul 2022 12:27:06 -0700 Subject: [PATCH 152/154] Delete numpy version of single indirection code --- meshmode/discretization/connection/direct.py | 68 +------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 006f5dcee..62e927809 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -911,79 +911,13 @@ def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, ary_dtype, is_s pick_lists_dtype = fgpd.dof_pick_lists.dtype pick_list_ind_dtype = fgpd.dof_pick_list_index.dtype - if True:#fgpd.is_surjective: # Check if can handle non-surjective cases + if False: dof_pick_lists = fgpd.dof_pick_lists dof_pick_list_index = fgpd.dof_pick_list_index data_ary = ary[fgpd.from_group_index] from_element_indices = fgpd.from_element_indices - - # Can delete this once the rest is verified to work. - def get_indices_numpy(dof_pick_lists, dof_pick_list_index, ary, from_element_indices): - np_dof_pick_lists = dof_pick_lists.get(queue=actx.queue) - np_dof_pick_list_index = dof_pick_list_index.get(queue=actx.queue) - np_ary = ary.get(queue=actx.queue) - np_from_element_indices = from_element_indices.get(queue=actx.queue) - - print(np_dof_pick_lists) - print(np_dof_pick_list_index) - print(np_from_element_indices) - print(np_ary.flags.f_contiguous, np_ary.flags.c_contiguous) - - nelements = np_from_element_indices.shape[0] - ndofs = np_dof_pick_lists.shape[1] - - order = "F" if np_ary.flags.f_contiguous else "C" - result = np.zeros((nelements, ndofs), order=order) - result_ind = np.zeros((nelements,ndofs),order=order, dtype=np.int32) - - if ary.flags.f_contiguous: - row_stride = 1 - col_stride = ary.shape[0] - else: - row_stride = ary.shape[1] - col_stride = 1 - - for iel in range(nelements): - for idof in range(ndofs): - # Get the row and column indices - row = np_from_element_indices[iel] - col = np_dof_pick_lists[np_dof_pick_list_index[iel], idof] - # Calculate the index - - result_ind[iel,idof] = row*row_stride + col*col_stride - - result[iel,idof] = np_ary[row,col] - - ary_flat = np_ary.flatten(order=order) - result_ary_flat = ary_flat[result_ind] - result_ary_flat_index_flat = ary_flat[result_ind.flatten(order=order)] - result_from_flat = np.reshape(result_ary_flat, result.shape, order=order) - orig_result_flat = result.flatten(order=order) - - print(result_ary_flat.shape) - print(result_from_flat.shape) - print(result_ary_flat_index_flat.shape) - print(orig_result_flat.shape) - print(result.shape) - - #assert np.allclose(result_ary_flat, orig_result_flat) - #assert np.allclose(result_ary_flat, result) - assert np.allclose(result_ary_flat, result) - assert np.allclose(result_from_flat, result) - assert np.allclose(result_ary_flat_index_flat, orig_result_flat) - print("DONE!") - #print(result_ary_flat) - #print(result) - return result_ind, result - - #indices, np_result = get_indices_numpy(dof_pick_lists, dof_pick_list_index, data_ary, from_element_indices) - #lp_indices_host = lp_indices.get(queue=actx.queue) - #assert np.allclose(indices, lp_indices_host) - - - order = "F" if data_ary.flags.f_contiguous else "C" lp_indices = get_indices_loopy(dof_pick_lists, dof_pick_list_index, from_element_indices, data_ary.shape, order, None if fgpd.is_surjective else fgpd.from_el_present) From 4f62ee2668cd3c1bf233a7b2cdf0da1db45b379e Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Fri, 8 Jul 2022 14:03:09 -0700 Subject: [PATCH 153/154] update requirements.txt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6648cbd30..8dfd4e72f 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def main(): "numpy", "modepy>=2020.2", "gmsh_interop", - "pytools>=2020.4.5", + "pytools>=2021.2.1", "pytest>=2.3", # 2019.1 is required for the Firedrake CIs, which use an very specific From a9db0c7be91c3a65ffaddddb03c88595dcbf1418 Mon Sep 17 00:00:00 2001 From: Nicholas Christensen Date: Tue, 12 Jul 2022 12:11:22 -0700 Subject: [PATCH 154/154] Fix incorrect variable names --- meshmode/discretization/connection/direct.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/meshmode/discretization/connection/direct.py b/meshmode/discretization/connection/direct.py index 99ef234ef..7d5a4889b 100644 --- a/meshmode/discretization/connection/direct.py +++ b/meshmode/discretization/connection/direct.py @@ -775,7 +775,7 @@ def get_indices_loopy(dof_pick_lists, dof_pick_list_indices, ], f""" indices[iel, idof] = from_element_indices[iel]*{row_stride} \ - + dof_pick_lists[dof_pick_list_index[iel], idof]*{col_stride} \ + + dof_pick_lists[dof_pick_list_indices[iel], idof]*{col_stride} \ {if_present} """, [ @@ -793,7 +793,7 @@ def get_indices_loopy(dof_pick_lists, dof_pick_list_indices, offset=lp.auto), lp.GlobalArg("from_element_indices", from_element_indices.dtype, shape="nelements,", offset=lp.auto), - lp.GlobalArg("dof_pick_list_index", dof_pick_list_indices.dtype, + lp.GlobalArg("dof_pick_list_indices", dof_pick_list_indices.dtype, shape="nelements,", offset=lp.auto), "...", ], @@ -818,7 +818,8 @@ def get_indices_loopy(dof_pick_lists, dof_pick_list_indices, @memoize_in(actx, (DirectDiscretizationConnection, "resample_by_picking_single_indirection_knl")) - def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, ary_dtype, is_surjective: bool): + def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, nelements_src, nunit_dofs_src, + ary_dtype, is_surjective: bool): if is_surjective: if_present = "" @@ -843,13 +844,16 @@ def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, ary_dtype, is_s lp.GlobalArg("indices", np.int32, shape="nelements, nunit_dofs_tgt", offset=lp.auto, tags=[IsDOFArray()]), - lp.GlobalArg("ary", ary_dtype, offset=lp.auto), + lp.GlobalArg("ary", ary_dtype, offset=lp.auto, + shape="ary_size"), # shape="nelements_src, nunit_dofs_src", # offset=lp.auto)#, tags=[IsDOFArray()]), lp.ValueArg("nunit_dofs_tgt", np.int32, tags=[ParameterValue(nunit_dofs_tgt)]), lp.ValueArg("nelements", np.int32, tags=[ParameterValue(nelements)]), + lp.ValueArg("ary_size", np.int32, + tags=[ParameterValue(nelements_src*nunit_dofs_src)]), "...", ], name="resample_by_picking_single_indirection", @@ -938,10 +942,11 @@ def group_pick_knl_single_indirection(nelements, nunit_dofs_tgt, ary_dtype, is_s lp_indices = get_indices_loopy(dof_pick_lists, dof_pick_list_indices, from_element_indices, data_ary.shape, order, None if fgpd.is_surjective else fgpd.from_el_present) - + cl_result = actx.call_loopy(group_pick_knl_single_indirection(lp_indices.shape[0], - lp_indices.shape[1], ary_dtype, fgpd.is_surjective), - ary=data_ary, indices=lp_indices) + lp_indices.shape[1], data_ary.shape[0], data_ary.shape[1], ary_dtype, + fgpd.is_surjective), + ary=data_ary.ravel(order=order), indices=lp_indices) group_array_contributions.append(cl_result["result"])