From 9d5ff257327979af132fb66f38dd206f3cc9ab17 Mon Sep 17 00:00:00 2001 From: Maxim Kochurov Date: Mon, 1 Apr 2019 00:48:44 +0300 Subject: [PATCH] Poincare ball model (#45) * add base * add mobius add|sub * fix * missing formulas * remove unused import * add scalar mul, test props * unnessesary cons in project * no cover script functions * add distance * fix typo in comment * add geodesics * add expmap * add functions * add singlt apply * black * fix typos in docs * fix typos in docs * add parallel transport * add dist to a plane and parallel transport. Parallel transport is numerically unstable * fix math bugs * add cool plots * fix small things * add egrad2rgrad * add reference * docs * fix typos * finish Poincare ball implementation * fix small typo * add to inifinite and beyond test * add signed distance * infinity and beyond test * black * docfix * fix docs * fix doc * fix docs typos * add import * add dist0 * optim fails * fix numerics, do not repare broken test * black * some refactoring * fix typo * p.data -> p in optim * update docs a bit * split pr * remove torch script (it gave minor improvemets), delay to https://github.com/pytorch/pytorch/issues/14455 resolution * fix coadd impl * coma typo in docs * nan police float32 * nan police! arcsinh * typo * nan police scripted!\nwratpping artanh in a script function results in umstable behavior * tests * fix typo * another test for parallel transport 0 * random doc fix to make typechecker happy * manifold->module migration fix * black * fix test for poincare (autocast double) * add float32 tests * fix typo * rename project->clip tangent * docs * fix side effect in tests * infinity anb beyond test was failing in torch==1.0.1 but not in torch_nightly, acceptable tolerance differs * add dim argument for poincare math * batched matvec * typo in dist formula * fix tracing issues and grad numerics for Arsinh,Artanh * _max_norm, specify device + dtype * clamp before save to backward in artanh * inplace ops in function impl * black * fix typo * fix spelling * some fixes to docs * euclidean -> Euclidean * black * math font for number * random travis fail? * pytorch future reminder --- docs/conf.py | 88 +- docs/devguide.rst | 6 +- docs/extended.rst | 7 + docs/extended/poincare.rst | 117 ++ docs/index.rst | 1 + docs/manifolds.rst | 8 +- docs/plots/extended/poincare/distance.py | 27 + .../plots/extended/poincare/distance2plane.py | 35 + .../poincare/gyrovector_parallel_transport.py | 52 + .../poincare/hyperboloid_projection.png | Bin 0 -> 8922 bytes docs/plots/extended/poincare/klein_tiling.png | Bin 0 -> 48738 bytes docs/plots/extended/poincare/mobius_add.py | 29 + docs/plots/extended/poincare/mobius_matvec.py | 30 + .../extended/poincare/mobius_sigmoid_apply.py | 27 + .../extended/poincare/parallel_transport.py | 40 + .../extended/poincare/poincare_lines.gif | Bin 0 -> 48849 bytes geoopt/__init__.py | 1 + geoopt/linalg/_expm.py | 3 +- geoopt/linalg/batch_linalg.py | 2 +- geoopt/manifolds/__init__.py | 2 + geoopt/manifolds/base.py | 18 +- geoopt/manifolds/euclidean.py | 4 +- geoopt/manifolds/poincare/__init__.py | 102 ++ geoopt/manifolds/poincare/math.py | 1347 +++++++++++++++++ geoopt/manifolds/sphere.py | 12 +- geoopt/manifolds/stiefel.py | 11 +- geoopt/optim/radam.py | 16 +- requirements-dev.txt | 1 + setup.py | 4 +- tests/test_adam.py | 21 + tests/test_manifold.py | 62 +- tests/test_poincare_math.py | 371 +++++ tests/test_rhmc.py | 11 +- tests/test_utils.py | 11 + 34 files changed, 2358 insertions(+), 108 deletions(-) create mode 100644 docs/extended.rst create mode 100644 docs/extended/poincare.rst create mode 100644 docs/plots/extended/poincare/distance.py create mode 100644 docs/plots/extended/poincare/distance2plane.py create mode 100644 docs/plots/extended/poincare/gyrovector_parallel_transport.py create mode 100644 docs/plots/extended/poincare/hyperboloid_projection.png create mode 100644 docs/plots/extended/poincare/klein_tiling.png create mode 100644 docs/plots/extended/poincare/mobius_add.py create mode 100644 docs/plots/extended/poincare/mobius_matvec.py create mode 100644 docs/plots/extended/poincare/mobius_sigmoid_apply.py create mode 100644 docs/plots/extended/poincare/parallel_transport.py create mode 100644 docs/plots/extended/poincare/poincare_lines.gif create mode 100644 geoopt/manifolds/poincare/__init__.py create mode 100644 geoopt/manifolds/poincare/math.py create mode 100644 tests/test_poincare_math.py diff --git a/docs/conf.py b/docs/conf.py index e1209002..cb2e0ae0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,34 +36,35 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', + "matplotlib.sphinxext.plot_directive", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'geoopt' -copyright = u'2018, Max Kochurov' -author = u'Max Kochurov' +project = u"geoopt" +copyright = u"2018, Max Kochurov" +author = u"Max Kochurov" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -93,7 +94,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -115,7 +116,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -132,7 +133,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -166,7 +167,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -246,34 +247,30 @@ # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'geooptdoc' +htmlhelp_basename = "geooptdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'geoopt.tex', u'geoopt Documentation', - u'Max Kochurov', 'manual'), + (master_doc, "geoopt.tex", u"geoopt Documentation", u"Max Kochurov", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -313,10 +310,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'geoopt', u'geoopt Documentation', - [author], 1) -] +man_pages = [(master_doc, "geoopt", u"geoopt Documentation", [author], 1)] # If true, show URL addresses after external links. # @@ -329,9 +323,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'geoopt', u'geoopt Documentation', - author, 'geoopt', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "geoopt", + u"geoopt Documentation", + author, + "geoopt", + "One line description of project.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. @@ -352,7 +352,7 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'python': ('https://docs.python.org/', None), - 'torch': ('https://pytorch.org/docs/master/', None), + "numpy": ("http://docs.scipy.org/doc/numpy/", None), + "python": ("https://docs.python.org/", None), + "torch": ("https://pytorch.org/docs/master/", None), } diff --git a/docs/devguide.rst b/docs/devguide.rst index 040469bb..b3674acd 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -1,5 +1,5 @@ -Extending ``geoopt`` -==================== +Developer Guide +=============== Base Manifold ------------- @@ -9,7 +9,7 @@ The common base class for all manifolds is :class:`geoopt.manifolds.base.Manifol .. autoclass:: geoopt.manifolds.base.Manifold :private-members: :members: - + :noindex: Metaclass --------- diff --git a/docs/extended.rst b/docs/extended.rst new file mode 100644 index 00000000..e2d1b35d --- /dev/null +++ b/docs/extended.rst @@ -0,0 +1,7 @@ +Extended Guide +============== + +.. toctree:: + :maxdepth: 1 + + extended/poincare diff --git a/docs/extended/poincare.rst b/docs/extended/poincare.rst new file mode 100644 index 00000000..c411803b --- /dev/null +++ b/docs/extended/poincare.rst @@ -0,0 +1,117 @@ +Poincare Ball model +=================== + +Poincare ball model is a compact representation of hyperbolic space. +To have a nice introduction into this model we should start from +simple concepts, putting them all together to build a more complete picture. + +Hyperbolic spaces +----------------- + +Hyperbolic space is a constant negative curvature Riemannian manifold. +A very simple example of Riemannian manifold with constant, but positive curvature is sphere. + +An (N+1)-dimensional hyperboloid spans the manifold that can be embedded into N-dimensional space via projections. + +.. figure:: ../plots/extended/poincare/hyperboloid_projection.png + :width: 300 + + img source `Wikipedia, Hyperboloid Model `_ + +Originally, the distance between points on the hyperboloid is defined as + +.. math:: + + d(x, y) = \operatorname{arccosh}(x, y) + +It is difficult to work in (N+1)-dimensional space and there is a range of useful embeddings +exist in literature + +Klein Model +~~~~~~~~~~~ + +.. figure:: ../plots/extended/poincare/klein_tiling.png + :width: 300 + + img source `Wikipedia, Klein Model `_ + + +Poincare Model +~~~~~~~~~~~~~~ + +.. figure:: ../plots/extended/poincare/poincare_lines.gif + :width: 300 + + img source `Bulatov, Poincare Model `_ + +Here we go. + +First of all we note, that Poincare ball is embedded in a Sphere of radius :math:`r=1/\sqrt{c}`, +where c is negative curvature. We also note, as :math:`c` goes to :math:`0`, we recover infinite radius ball. +We should expect this limiting behaviour recovers Euclidean geometry. + +To connect Euclidean space with its embedded manifold we need to get :math:`g_x`. +It is done via `conformal factor` :math:`\lambda^c_x`. + + +.. autofunction:: geoopt.manifolds.poincare.math.lambda_x + + +:math:`\lambda^c_x` connects Euclidean inner product with Riemannian one + +.. autofunction:: geoopt.manifolds.poincare.math.inner +.. autofunction:: geoopt.manifolds.poincare.math.norm +.. autofunction:: geoopt.manifolds.poincare.math.egrad2rgrad + +Math +---- +The good thing about Poincare ball is that it forms a Gyrogroup. Minimal definition of a Gyrogroup +assumes a binary operation :math:`*` defined that satisfies a set of properties. + +Left identity + For every element :math:`a\in G` there exist :math:`e\in G` such that :math:`e * a = a`. +Left Inverse + For every element :math:`a\in G` there exist :math:`b\in G` such that :math:`b * a = e` +Gyroassociativity + For any :math:`a,b,c\in G` there exist :math:`gyr[a, b]c\in G` such that :math:`a * (b * c)=(a * b) * gyr[a, b]c` +Gyroautomorphism + :math:`gyr[a, b]` is a magma automorphism in G +Left loop + :math:`gyr[a, b] = gyr[a * b, b]` + +As mentioned above, hyperbolic space forms a Gyrogroup equipped with + +.. autofunction:: geoopt.manifolds.poincare.math.mobius_add +.. autofunction:: geoopt.manifolds.poincare.math.gyration + +Using this math, it is possible to define another useful operations + +.. autofunction:: geoopt.manifolds.poincare.math.mobius_sub +.. autofunction:: geoopt.manifolds.poincare.math.mobius_scalar_mul +.. autofunction:: geoopt.manifolds.poincare.math.mobius_pointwise_mul +.. autofunction:: geoopt.manifolds.poincare.math.mobius_matvec +.. autofunction:: geoopt.manifolds.poincare.math.mobius_fn_apply +.. autofunction:: geoopt.manifolds.poincare.math.mobius_fn_apply_chain + +Manifold +-------- +Now we are ready to proceed with studying distances, geodesics, exponential maps and more + +.. autofunction:: geoopt.manifolds.poincare.math.dist +.. autofunction:: geoopt.manifolds.poincare.math.dist2plane +.. autofunction:: geoopt.manifolds.poincare.math.parallel_transport +.. autofunction:: geoopt.manifolds.poincare.math.geodesic +.. autofunction:: geoopt.manifolds.poincare.math.geodesic_unit +.. autofunction:: geoopt.manifolds.poincare.math.expmap +.. autofunction:: geoopt.manifolds.poincare.math.expmap0 +.. autofunction:: geoopt.manifolds.poincare.math.logmap +.. autofunction:: geoopt.manifolds.poincare.math.logmap0 + + +Stability +--------- +Numerical stability is a pain in this model. It is strongly recommended to work in ``float64``, +so expect adventures in ``float32`` (but this is not certain). + +.. autofunction:: geoopt.manifolds.poincare.math.project +.. autofunction:: geoopt.manifolds.poincare.math.clip_tangent diff --git a/docs/index.rst b/docs/index.rst index 9f62ee12..9ee6098a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ API optimizers tensors samplers + extended devguide Indices and tables diff --git a/docs/manifolds.rst b/docs/manifolds.rst index 97b65a0e..6dd3f579 100644 --- a/docs/manifolds.rst +++ b/docs/manifolds.rst @@ -4,13 +4,11 @@ Manifolds .. currentmodule:: geoopt.manifolds -All manifolds share same API. In order not to duplicate the same information, the complete public API is provided only for :class:`geoopt.manifolds.Euclidean` in the end of this file. +All manifolds share same API. In order not to duplicate the same information, the complete public API is provided only for :class:`geoopt.manifolds.Manifold` in the end of this file. .. automodule:: geoopt.manifolds - :members: Stiefel, Sphere, SphereSubspaceComplementIntersection, SphereSubspaceIntersection + :members: Euclidean, Stiefel, Sphere, SphereSubspaceComplementIntersection, SphereSubspaceIntersection, PoincareBall - -.. autoclass:: geoopt.manifolds.Euclidean +.. autoclass:: geoopt.manifolds.base.Manifold :members: - :inherited-members: diff --git a/docs/plots/extended/poincare/distance.py b/docs/plots/extended/poincare/distance.py new file mode 100644 index 00000000..33408101 --- /dev/null +++ b/docs/plots/extended/poincare/distance.py @@ -0,0 +1,27 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns + +sns.set_style("white") +radius = 1 +coords = np.linspace(-radius, radius, 100) +x = torch.tensor([-0.75, 0]) +xx, yy = np.meshgrid(coords, coords) +dist2 = xx ** 2 + yy ** 2 +mask = dist2 <= radius ** 2 +grid = np.stack([xx, yy], axis=-1) +dists = pmath.dist(torch.from_numpy(grid).float(), x) +dists[(~mask).nonzero()] = np.nan +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +plt.colorbar() +plt.title("log distance to ($-$0.75, 0)") +plt.show() diff --git a/docs/plots/extended/poincare/distance2plane.py b/docs/plots/extended/poincare/distance2plane.py new file mode 100644 index 00000000..66cbc8fd --- /dev/null +++ b/docs/plots/extended/poincare/distance2plane.py @@ -0,0 +1,35 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib import rcParams + +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True + +sns.set_style("white") +radius = 1 +coords = np.linspace(-radius, radius, 100) +x = torch.tensor([-0.75, 0]) +v = torch.tensor([0.1 / 3, -1 / 3]) +xx, yy = np.meshgrid(coords, coords) +dist2 = xx ** 2 + yy ** 2 +mask = dist2 <= radius ** 2 +grid = np.stack([xx, yy], axis=-1) +dists = pmath.dist2plane(torch.from_numpy(grid).float(), x, v) +dists[(~mask).nonzero()] = np.nan +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) + +plt.gca().set_aspect("equal") +plt.contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +plt.colorbar() +plt.scatter(*x, color="g") +plt.arrow(*x, *v, color="g", width=0.01) +plt.title(r"log distance to $\tilde{H}_{a, p}$") +plt.show() diff --git a/docs/plots/extended/poincare/gyrovector_parallel_transport.py b/docs/plots/extended/poincare/gyrovector_parallel_transport.py new file mode 100644 index 00000000..894f6c94 --- /dev/null +++ b/docs/plots/extended/poincare/gyrovector_parallel_transport.py @@ -0,0 +1,52 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib import rcParams + +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True + +sns.set_style("white") + +x = torch.tensor((-0.25, -0.75)) +xv1 = torch.tensor((np.sin(np.pi / 3), np.cos(np.pi / 3))) / 5 +xv2 = torch.tensor((np.sin(-np.pi / 3), np.cos(np.pi / 3))) / 5 +t = torch.linspace(0, 1, 10)[:, None] + +y = torch.tensor((0.65, -0.55)) +xy = pmath.logmap(x, y) +path = pmath.geodesic(t, x, y) +yv1 = pmath.parallel_transport(x, y, xv1) +yv2 = pmath.parallel_transport(x, y, xv2) + +xgv1 = pmath.geodesic_unit(t, x, xv1) +xgv2 = pmath.geodesic_unit(t, x, xv2) + +ygv1 = pmath.geodesic_unit(t, y, yv1) +ygv2 = pmath.geodesic_unit(t, y, yv2) + + +def plot_gv(gv, **kwargs): + plt.plot(*gv.t().numpy(), **kwargs) + plt.arrow(*gv[-2], *(gv[-1] - gv[-2]), width=0.01, **kwargs) + + +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.annotate("x", x - 0.09, fontsize=15) +plt.annotate("y", y - 0.09, fontsize=15) +plt.annotate(r"$\vec{v}$", x + torch.tensor([0.3, 0.5]), fontsize=15) +plot_gv(xgv1, color="r") +plot_gv(xgv2, color="b") +plt.arrow(*x, *xy, width=0.01, color="g") +plot_gv(ygv1, color="r") +plot_gv(ygv2, color="b") + +plt.plot(*path.t().numpy(), color="g") +plt.title(r"gyrovector parallel transport $P_{x\to y}$") +plt.show() diff --git a/docs/plots/extended/poincare/hyperboloid_projection.png b/docs/plots/extended/poincare/hyperboloid_projection.png new file mode 100644 index 0000000000000000000000000000000000000000..df4843c5a839fbebdd763665d1d7713217743594 GIT binary patch literal 8922 zcmaiacQjmI)V40VVD!`Js}1L28kM2Nf!eHQw3dKJi$V@7z2Y1 z(H9(hc@23CjD{qlJ4;;jJ;6J$krxIA>G%H%W(GGY1Gj|=PNBn+}nPk6@+2_qaOBzGLd1kk?5WRp&p zYNLB8S3-vkvEao0c&2^0ZU@6iu_M+F|4PMzRBTJj`HH2KEhvW@ts4d=+t752{%ybJ3rphtkz0`-EI@?%6?eLFS z<*dMl7uxq1K6ejhg;Dxw_i82d4ikQ}dAxAWP*)+R6fTpzc`UUm1J%PcK-Z&DppcH* zH4(rQt_c#PK>pBCGQN>2Za&upB@z@O>jmV8$$`VWk~7_ zrhcIeQu4euYWY~NsAG3zxI@2?9vN8B0*vy%^TAnp1|Y|+WDV~1UpS^qT25r?EWy%7 z8lwiz55dsz8G!Brbiie2%!wefc%-Aigz?$TlUksB#goCB4l$f0F$ z)z%f4PDVsj9~A}^7h)jSZh>k?p6V67KVNq-<<`dBp}s(@`4X8GlJIg0JVAqPA%D~K z6&Dvzo?iFlDvwT!=Ha_~nQpL8RrJ}8@AO2$(2-1JG8CVR6Yu4xv-#gAl*?yOX}>!t zm*KW)xK&{h*u3s{P~|y5naG~t*V?7gFRw*oLQW-PLlG}3$I(=^=3t424SgTT{^**o zvY-U&n9%T}i^Nna74kD%nv;FQHv<~`)Kzt+p19JrUM~O)Ya9zXYxx{CL+|g{AcD{R zW9_mp!mADK%(-vl5wDQ-?^B$dKOOQKR@4O2_}wj;SdN?PgtPR<;ovSG{8FGVPB1e(L4=>}v%rj;1r!dMY&Ha;gm3v=^2q@&JoY%g# zG>i;P?rGmfi`a%^IiB$q(_;ru11Wvm*N+2VSW378aD(_nPl&5#SQ9X9A-DX>`cy`_ zABK*UV`o>Wa3i+yxPhk>*G2)gD)a-tV1sgxBEy2dNoZV`+8JKd+~jMbP|OH`Nm|~*6ErZujp#E^vFCv#3gczz z0E04+(npZL|9>-OSb}cW=RWhfr|^sA55jiCCG(*kC(hGld4y;<96aS?I=u)Rxk5=q zy$v^-=olw&FWO9?WQGKi>T>0{!%&j+4=jv%%XqEW?YD78fvp8@iJ*=Qs-4p4!J^*( z4!+WVf=LMOHF47^rkV{{{*09`s0KuvJV$Liws0(f-QV0y2W%d9WTxi6sF9<$i`R$= zrbB#&(<2*WhnKVey7Xe2n71Hbxw>y02X;i~vZ`V-{L^f*ZgoAk=ia2#Ln#G_3MV-& zCb{W5C1U0bxywcy>gixI4AQR%0k3=m9avkpsq+ObZA%2#*if@c`$l4A3p3e&q1;e2 zw@|gXQ19-dlHAvTWPur&O4m$bGU-VI$hzhRj9>t7?#_}bV{zjI9G+dx3E#6mZr^FH z%3L(j53)rH#J_%9W~mZ$0J2UsP0p`Dlo`W!_b)K&W!ub-U+`ZW@PCzMUI4rU4}QQB zB2+zz-?&f+SVGcQ*5GiCC^og4qwdUIa?B;lV~38if61)B)hv8DO1@^1->{kpw!UbF zAQT0jXkFq{rPKu~k3GtEnDvaU`D!RLJM+nk-)~2;iRMRQ=}Wlpjq*1&>B++ey<9@( z>QAl8UV^0uC4p9r8^5b8na%k?AzOQc`Py;=y3-Ty^gT`nn)wg{7Rx;MO)I68;_XKD z`ZN_S0rFhe--1OmuUFC%uw=&nSzOIsQE_Rm)8R3O3uhXE!V$r4TdpaJC6o5T68#me zeGifX<-KGD>#(!>U}VbXhi8z~u(Q2j%&IMWqg9rYhFO)$Ejl-gkwySmxg|rwR%GYk z{01ew(w$BAl7$c_@-;ho@SV8Q#tsM}%2I7hHU!%0d=<-Ak6(EW{fesi{Qz&A8WNrR z)~vd8gSP}9@U!jMIQD6=Rh0-|k142zS$ko*8_+OjmD+A)(^4W8sV0?ezvTrFm-Of* zHQd<7NZC3gj@*tyh-Nj#%n~cZ@M0-l`U)eSXnv06c?FH;A#CPG7~VUS*z3Gn;?3z; z)NNvK+6Gjov&w4~_Jj-*_t`H&@NMbpbR+u+I7pacr9;;=Lw8s*8d%3U0#D;NH09{4 z+)9ksiFT_92meIsT&TxS>Ui#MUN%9J1qo-?G-toBexGZNKWR%qNeyhAVB|MUNPw+Z z5+Den?^CUuFa$x1v;>3Ivi4B&(di3GNUIx>mRna(kkl>sd*l>l7J&n$Mz}v)=+WR& zTf^pd%Mhd2sd)=@c&j?KHQjNgWnphXZ>Xo^P0#0GiTkm;Z-Vn3w%6G&yqzm|hGRW) zDy2MJg_$a@9gU375TmGV9y0<-enYy;qekjCQlTpz+~5WCCCvFEg}IN;NC4NHQyVe@12YGxEY zEJQ@(id?+ACnMidUwgjdWNjdEw+a*{j(ZG$na?eGb{E;=_WThikySxK02-0e{<;W* zS-i&GontFVi|7KUCzs$}-4Gov3s$bO_3^e^**WuCul_FL)@1zBzsUU|wC?o8(NHhS zDMe;G$2(~BD8)Z1LAD$~1g*KGehcgEt_;0*2@y0oZnh};U7eEv=zRJD0`Bcqus<;D zni@09|0y);K;%dZQEA?Gvjr?)ox_vNFDTEKn7QJV{i~N+`~x+{1;5u)H1a~-heQn% z<>PJ)^{i;NPKXDx73oE5O6gNR=*S0@a;;#LbA5&_+cEd#&0Vy{TQ-(5Eq{328wui1 zyKWxp*36~sxG*sI99a_*S|O2V|9O%PDc}r1c|D4w}`M&$A@-o!B z?QTT)Ufg?X@?%s-_ETcA4Sy7p_aAX2gED<4sz_2kaKf@rsb=U%Ogt{(+;6skBFTfH zN~@#mX}+aR2Y8^>s`5T-N#6pW@_b>TPObN{DR~CD%9{9xzk>YzZ;nwA_ zwltIEt9LJ6e#p`SBc3rSANoadLacWyM<1Q31H=}Gy64fTPm71F`I21(>ebu^GDbqOf->IJxfY^JG5wGS|E{4!ip^zIk-pxE zTXL^tUYQ1ZSM@WNNMD_}a+{1Jtn5R9*y(Uj*MTTP}uD43Uz=q zFiVNvF@as|=$pSzGjr0MqA#CV)DZE$#aWTw9`GpG*R>>tZ*oo&A!gNwzymi{m0Jy$ z-UopYHo09I%J=9U6f?EetWDSaYMiO&_5LmdpAkdO_Nr83ioQMk5TST$RWx|qQJk~O zx=c&t$(~;_ifo?Yf9~`KtMd5I4~=t+-RubE#E=kw|V23)6?`bdS@yNsV(_{3F^XSdxelf-f_0oi2wS$8q0A3PlJ% z+Rs&5L{CIpeu&dB&#`o7`o(C93o*MW4z?CTFKFM&-2d!V_3euF+naMZS>m#%-u-j- zs)g{F43AwV7G->oDChL8gn)|?AyFABmkNv2fSZg!lG2N6HaWRR<>&Lp zcSdUc2WM0i=}qP}&q1+Swa?Vmg-h1(AjJzY2lo$YOUtx+Za;`l-U>=s_b>SmQsXJn zcT_4jNAP;B8ZlFtom6KSC%62f55n%$tBe#-bgj|q`B`a>jKcIA#2ZqgXQ)y*2PL7W z;vf1;mjk4B+^=Cn)4i`b&PArk4E)oPC zQWK90zXZmh-&QOSwvQgN{P_b!MgmET^hqGrnM8vM+*nxmqUGiLvc?B-Z2s z2d2PnwcDsMZfsN~Ab?2TZb%-C_AN4=Bzey0h;oNAt277A{3vtaWKoSb_M<|iVK;6= zJC)x3odsEski`gJbmr~;CH^RjD@FST_WjsICc+Z~byEA8k;LjESC=`GZqQ+hNdR$b z&i8MTt3$?RQdNn574J(mTYFf-MEdsG77psDjKJOK{)h0vjZ|+vj?CU_iNjonJ4#UG zb#+fJXBmH7NX&sqQm+t%{!y9xDWOSFr#Li)^f_bG!-4Hh)!y0E$k{Qezv2>LU_{mG zR#nupM;^FI{=H{`edg<@yts$TZ?B2cKn|JIJ1y(GKN_;%k$1m$P=Ql=j3?>A|^UGXBqZP{dk{8`YnnDq1sPe#gwq+ zKY}kld+eK&W!ue93y62@)Ve4aGyNyMj&S=42;&7=XNQt>ar?yY6EsFLETby1j`p1p zf@!j6T}>H=y0taKJnU|-0$+=*^Sy8^W|AGe8*#iza=fX#9U5uU+Fb8H%{|ESGjsk| zgxOyS8hX_T$CKjW)0tQi^pDmaMIwr9T^Tux`7WhKryU;3GI{0r^pIP>wLq3toz{~>*7w`N93>1Z&gKUoO2&U{sA z>z@!(czb~-nkou|TR3aQiOFac^^mGb^3V6o_UV6r%ROK$kZ=^|BX#7Q^wM?iua4+p zD`^Vp#~fzL?TWP)7Tz>Yg4Vm35u8d~m(IngKo^bq_M+$ZL2Me!%VxqM4Uyw@&^el? z4P)1|c|4sO1u?uZ1 z=-ZjAup6*g(A)DOoay(njBai}W*oskR4maOH;t!Yc6?)y;w0h*0L9tuHQ`3-XHt!t zi`x9=&HCa!!x=KoQF@o#w<_DQcG`(QYE`f)N8bFj@_V9{@ho6cH6iv~wzb*gk5?%Moyqf-JBLij{N@?p$t#_)svXa^-O?mv{fPQz zN_kG`RUdiqC*%tVKlnS@2RvH>2q7vwIuKc(EYpnO1FLJ@R z6L|4V4WB?SI$xJiuHs}PA>}66Q+YbLOyh>|`RsKG-e)PAdz20b5es39ijb5oWig5! zme5YFg!Z?4opN%9id1(+R%;bzHOfgs$sI9QK|M z2HE=;AV1NQPUUxYJG{HNgZW_3-fYsUpQkG2Pj?cq@S+RUo68({ao)Hze*(E*5~nQH zT3Xoj1PEEzV_40RE%<`Js{d%O3**Q)!Q&`t9kvpjR{bD#h}3bu~j_k z-Furs&B9a%<3Hd<(gQL%T3lwP;=`@o^^k*P68TTLW?Xr3c{pJiVafW=m#KRiv))5xdn zmMV<}7qH#7E9aDEgX+B2S-WD-SYN4958I3M*xOXxpw`>E%S7xrkRlL}O=20EgqW*N zo2L&aluj3xB=;Zp0FHihH>7mrvo%5kYs! zd;apg0J_-rV}y#%?AKtVESanTPE9nu4asTaHadKdy6;ZZ59*?lWNyH}3PKL;_eQ*e$`qu z(8=fI`y!)n(J_?TiA%1;nd^&45{Wm%ssd42J)MXsyFeqPm~j*-8PpJYK};4x<|wTQJaa7hYU?cO&BWxs0rACvXa7@wE$Gr`PT>3qX2S2A^D)BG}({^R({wTWH! za!<%T;`iR~OZtL;Z5y7~D>VbMrgi+OE=Nk|A)w{U`?FADcXM5P7Jh3&@}1AGjLs?i z1ydvSSoVx3<*(e`*9vftr4yWGXyAfh3)-vnf@u+nGM`$czeo{EDf;bjTNIDD+iE~C z8JsjV6B7wU!rtYMnu8uM>PkFvdNW){F*|!=r(MU_0TnWF_EiZ8xcs(1nrTXrRVy-`;rV}k*fEIs0zUn>8w7?E!^ImT4NsSzmQ;Cg^>bx;>p65 zIIF_-vQm>S!k@e7yQt87b12q4*l5gI=S%IE;ZzI_%usqH+qLMFkhJy z{9$<;dKgQ;h_hd?wqQ|bUYw)YETpyi%_tvbM+Co7Ffr1`y&A}Psa(l2OgC&Po8NFp zGJHKXk3mef%pw;jbltMlH83Vhx-`EMlQr_#9TaQBm(~mZ{L-T?m;$#t<0{C|>hG~e zGT5iZDROARfA!NSa%7bV(b4acpxy3L%Bzli_=5BZmj*zZrIYhJP233Dp;Iqwi9wB+zb+aKh)@gk7)0^8~B(A?2s$b zd-yLK!;kQtmRH2QK)XXZq&SJG|5?sgybig|%ZkG+`;-U>zG6fVii2}RuaqV4bV{ef zXr_fL~rQnp=`S5IcJb08d8(-x16qnTs1d&J$g|0cREYz zs(D4UN`d=x|8`N_f1X7gFwUMG(sQt@5AcDC?@UgG%)S9dBnBHx+`OgsyRbznsxhQC zYF;ft;=(|F{5ZO#Iz)O@BI=)O>313X<kXTMA(YKR6UyT5W9BW&`&cDP!~xQ zDO+&mUCNKAWuB@)$0X=2WTEzTvz^OG%}Kl)atc20Z|G)($+igd6}}E@(tj=qSsN3i zBaJ_vs8~nN%oexi+v!v&8g46~ZEADEC=X#og%p(s_B!=y!(D!G+cs~G-m2K{LUD~w z7jw*RY4~4uj$x)X2FT;^f!K%FW-6k1T|c{SusrS-u80(}MC+t~q}NW3iL>Q-W+W%? zHrVI<(OheDNs;aMEgAdib!kOh@%DRMdRH(E6c{sg{1^B>o9Q*;7j}T8_(&K`cuW}f zYPZ_%jno)9TNjJ;D~yw{=y8Q3VD^SjdoPB6oH}p6v~b98ET#b^Sz*gh&_}0ma1~W} zgwkUhWFY!m3#+Rxa=1xhjJ*1N3LDqmJ(>Gb23zukZO)*to={0R+w!+0kygcYj~SM4$Ub^;JIBsFsX1o+`; z`|E1)sN&TMKy8obiS83rNgx@22b%D)#Nq8A5P1Q=WWlFiJKTnB{!ed|Lr$kg%PcZ7A zO|AOp)m$!tPwMz++VVo$i@U`{9(u@xi0wQz0~6enO3!2AgtN$MdH9|s;Fx!$Mx(@U zGQ7Dlr6mymj)3p(;#+f?Qq=5Md9}em@_D|7Ub$RfN{NEGXw`ciXTMV1Sqw_g@^u$y zauoPWnR2QoSPG^Jwsbqp7~21n_;~gl-=tnJ1tI$KvNtepvhJC+`lY}>$&{+ zyW$3Tkv^CV6}W!bcwbymc>-qI7KqL>|4XChWaKfIM%(8{p>qLh5|dB~RdEe9J{k6m0`st0rMq!iWzV2BuLr2(@_5Fy0?GS}bc9 za+AiWC7Zr6n$rFUQ8``d!mW?q#Y^Am)>lPAdpM4_ftq(RwjvUBT(&}9kZ>%ecDW{a z^t6=>oST9{uWT%UZL!SU0RZ5H$b}}u-+^NCN_&@vud9PS78B6nCjrYx=&d*ehp zsL!?t4|M1AoW^QmBURN^|3S^~t{#44=e%x-$v-bq{LAQo8BG4qIT>_)kWl>VLuX zD^EFx6{-BqgB;+XkN#X$+P^-qD&o_3OZHF-AXoKdfXJaC#pApbNJr!6^!;V$cfmAK zF=xVtoYQS!2W-ou2SsMhEc{gJGHaAqA>WZ_{L)C`nP04X6#UWqh^gcr^7jjR*)NRj zrUEaL*2nI;$ajKjX44403o%j-d)0nCf2BjC{`*LdZL#jiK?3lEQ?3NNsQwe-89K%u z{`NJ>Jl)TGHYJz$@l0-f9SZ9ee!6p992Of8#GC`9aAe1egA|6M(LTzolfuGu!oUoI zgS;pE(zWi3c~2b;o|@@}*P=-`?QoAePK6zJS`l~npyd*W6OO`P2*0CyJX~iLa3vw= zC$Qb5n|A%&sKQe^fgLIB9|zYBS4JmxMM{eAe+C8ad$w6DT=;VWqL)UWO${l=^^WO; xdr(-T_WAxA>B2kG>XzCB}fhpLyUwVjdXXXbn_dZ z@B4ZGm}_Rvb@n-Xuf6t)d)+JgovIuT7C9CQ3JQ*b{M+{^C}^q3PZ|h-yfXYP$p-la zw0WcQ1_cF{h<$H{j{J}5BCqF;f`Z%q?}Pf44VMCWk;X$t*F)ojwTHKvn-$8t501X( zz8+TYwEuo7IGNd4y>t7}x;6V81%(zx;q4nuAJl_3l?1BBKVAkw-(&^EP*Al;$Rk5U z>#+m6u3w!A@;`6h#`Zz^u8xDYEt~H4(VRD3`BiY}hRDpx?;(aTe74XHjZ!0c(9QDG z!!qgPGgJ@+1pp2~0n(yCOF;oHOOKK!?aG^i!&1o0J8xDF&#Hd%KP^vp6m2$gEx#UF zIhr+i-M`o-a5+--Ay=|}r0xxDt>8 zX{#|&@k{9mwYH;M?>0>sZ<&5N_o8Ut8#*qjn zn-`~m2=8l^OgK0M@fP?FM#zY*Lx%!A2L90>N)2Q^eteTQM6| z8M~pMX^ux6rb}#vq5yLUnU}|NN8-`{NhTGTwMX!Kb0W>KRr3AAZQwB$!(37QHr4Hj z&-6B;3o`MeJ}KcI4*J%|F=I5r$^Z^B zA<28EJncl0$t&(1_C|h|gpU0|4FekTY2<8~0cf~k>fd;|fV8GDA^in>Rhl2DiY8~1 zVh(L;1k$A7IIg@dueK}BY7tJ72#cf9df}5@G*r+8V@OQJ0HFgo1d@eNzoNV=t#fM( znDsa?Y-3D&s0^f9nmvREDF%BAG^I553@Zf%)|x*vGDsqiM6Rz9lxO~7HJ=Yio1}#J zt*tI-)7eKVE>~bvkRTpw@CR4y^*miTfN5Nv=6FjE|NQ=P`q`7tKDZwJw>bzo=2b|H z%GoOCf1`uvZaE{{gJ^cb)oEQ5>=nLa`q|%|#da5t#M5xRMU(X6TOxoAj3g+|98(58 z-r;}LU-#`52PwZnAr_U%a3KJYU;^>x1Df4SJ@cuu(3gk1s>Ad`8}Z_osf-ybqv*u0 z=&F;}6EAP<4os@%I2rKky$7Nn!66}FNY8r=qyTXMk7(0sTve*6Lc(jYpD9S54sVmY zZyNs|JxQLg9(gq0mVHp859HORd`IK#o3kuD-c~?y3)63o$RB#9ss?gYMIna!qJkxW zm6*U0!v|D3;kD(V*5NM_>JD0zAN8H5OC%&QTeJPu*d1(#`}9KQj;^L1o|qVk4wF5& zNdNmarz{3U6>5!)llcMJ>2jpEWFvcfk3OSE6ftBiqE9 z0odoJ>yD9P>{-YCS@L0eT1k(RmePdj{%k?AzU>5JJOIm3YXIG19!J=(2ukyzl;SAm z=>s~menb~K(CcML%p%8nH#U?eSVL3ye(NDjSxRIgznHh@^^BQuu*lss7DLQ_<&*v4 z5#sRyO^5M5k8$rav;JXpems3XKA?;M(o>nCw-ySKM+65DFYTWi3+N^A@2-8iP$2q} zUwh=L^~4_l`ynDA5_YL66PpzuiX0V-j}&LhyG|%V4VchBxl1R05xlo}J(Q$C(L3?G zH6PAq<7vtym#MN2_}e9kJyc(oMV$DqZs<>_8R$7uF{!dpilZRd0I1JotqJaRZEZ&- z#bcZ&#|P)3BDKFqveIQbARzcWh1ZqixTE^|I2T8Hn3a-~wi*Lf^MKr!0SnYh2nx~~ zh;kEKe}Iovp((6BZ^$`so=?_)SFL_P{>5uN#fp=kpD~Y=ykP(3@`t>}pgW!6n7W7i zzaI{NqXTKVS&&aEjP7y*1u(5p!&}_-6P(1W7oEfp_D`{_g~>ngAG_uc8GJe8dFkii z^plo#sKoV@n$wkog_bC8=Fiw_`+iYyy8vYhysfAV@d* z1%0IN@a&eO*P!j6vK+o*3uo_+ zr-!8UHrV|^P{j;*{#ARrO8(pS)Db+3D?H!qoQ?}fM_Tmsyd4Z^5b*qWnR2PkiK`zm zbFV=qqfj;1UGA(x9n*qR+?uv1z(#NC(n(_rK#F_X56uf)x_i3Y#ORy#DvGrN5g}(m z08tIu@3tce zp-YD)%k10X22Q&JX<8K&;>@isBM?8silncK{7g9i?=Q5(5sq7NIHhK{;gEb)RoT|p z`z1Jh`%^Ngs74h>$8jGcmL=O6Y2C2_VB5DS#Pc9_Yq}(Q+NkAVlSeNTp`@Mf+hvz5 zDda~8ZBca69u3mLo}@=$-AsdGT?Z!?NJ#q>Illc#8cV++c#(H~3n{})Qj}G1#ymWe zryC9Q?`3Lg8m^W3g(r9i24*_vt5dBW!%QpF%8wFy4S%^+RvJkBqPoT zj|1XjQ>;Mp(YIPH%7#I-(eJ%iw+Y{AI3EsFh29XlBEt+#2E^VE5TOS6`VB%nz>~>@ z_SwTachiMMzyO~xQa=Mdn6xK^j@n2H33@hDbbuDNk*Xs=%_I=yl z3mXujA^-s1?!ZAvyT5?&Hd!;ASSl zI{lj!UDN2{$q%~+Y+9<-%zs8Hp;;%B?1|Rn^3B*#1LW2;x{SR5(4jIJFF#rT9Jtdt zY&ug`^FGXWAGX`hA@ys@xTt1|Z{89~nPq;OOcwYdPb%|jWblM4izfS~tRX#M7X#{! zH2Us>3sA`CkR2W2q{o}#a?%G95Al4D9ir`9B30J+oPz`|7ab5`1yFB&K(tX51E|U1 zkA_}zZurf_0i&@9WJMl@*xynL8^{SN3Bnm>74NbPaM|x%%bVY;HdJ0cp;aaMKDqG1 zLp3n7rKH;zXM@v{D(%(EvS%>Ha}Iw}frr;jNT<$*3wby9lYXMYJu z<$osu2Z!c~+;(a@5R8&%*41@8;jB7G-Y1R3HF-^sKag_xr}F*(PmoLv*_$>E$x(a! zMtlyt)DFa@OumfH2W2hq9qM5WNxh5u&c^Mkn~is})tfI~P;6MRzy4ST$4RtC+iHz~ zKv0M&P*ypBLG&jNU1%n2Y(0`eLFCpKD(B#f?60_fYqy7n^K>rhZ$&Gq#nrbDQ`1{m z)hAB153hewG1`s6`-gt()s+K~(H$!EdX~~-jRPpiK>Jk3q&~p)fhv*2&Rb6PJVj5t zH;ix3{+vJlvJfAs)rJBIMsL#Dq|D3PnfYCoqxfFg&aUxVtehfuc;I^)$_GLho5;IU z==#bP*W~T#kEl^t*iVwD!a>)n+w^QcmOwYqf{Inb5HyfKOBP%4P%;rVmhS?+3u@Va z39dUhV=y-pNAB{4o8sF|;DWHL;L@(n!~2R_=i<4C@xLh+@xuoO5~IE}0R0lViy9{PsA#Mz-+V#1t(e!~aDYt%UOXI^KRmMJ9~Q;`jq@2l<>710^It>Q{9U zN|R3ZajYwKMYo3N7zSK1wDzQM!5>|=dNMzI{%1)#_NT9sSuP2)sp)z(x`l+cZ`_%d ze`noW&%3YdHp%2^tA0?K`{v$)crLqWqs~l{KKy2sL8?YdSEqe}159jU)JKKaE#kn?5AQ zh$XX|fFNlE4+sx9FBxByr2KFl$e)D!m>^y&3KY5^;?s8HSRwa+D&h%H2g0-H1HBLU z^tF9mV6@1ogeovH(5>Wj^k3QRetJdGW_zy{Y( zw)~fE1F#+{c6KaG!-dh3%?jziNAuezn~4npkJsL=A{z5Vu{s4Qvdt-K{ikq~ z!x95=;Ql92&hrnKhqKKO5kI3rV^#F8{_+(AjxM=sXC%h9fh{L5<+6{aBXg~ zy$7D2B^rK)Gi4;deib{?acVZp=TWC7hs-jJ`dx%n5#rV^-~0a3j^P+#&>a8b$gsP} ze!V^cf0@w0lFBZm!DuqBRf>L^D#1L-DjcI~W24?=D)4@p=L%2TcGecdE-My=)vGdK z)hRq=yy_@2=Y5dmVsC->Lk>pOq;FVzw9@04?RXc7alLAPT*O?cyJ#k~vrP0;sm!X} z-k!F;;Y%9sl1%S%96Dr#8k()J4|ai9-%o|!S*M4qH##thkhMivV1j-A9py%L0>d_G~EKyaq!@Yh+b zjo-%og5&vOT+$GMww)FKEL%&_Ae(jsUr|bwg?o-r)keqOj3g<|FQ=bf% z6;7lQ7@42bsI9<*0&gCV&F!K*YE`{!-5b}0=lGbP3=f2lXAP6h#jv9;C4e6A5-rK8 zsR-5NDJSDoR1tI6!kC=}m>as39vgLg>SK_^o4aQB=;`k)D#F3-iQCh4llVha^ko%)QK5`qxOWEFD2( z>V9h2sCf(u>y)O5%PWQzYD{^DTb-st>A$-Ks|)#CDlHU`CK<2OyZt_3;6Sh^TB*#%5Lh0ODF<#xpEHzt5zejDCA zOP1F8HB5X^g5?YF{W3DR4{BQ2oNhkn&vcI8-1&C!Yq>oL()a#y3Msz|D@*U-bNqv% zaB)$=gu5!LVjGFXi)t1g9%sHi*x7bin(gN63yZAaFO{yi!~aPm0FPh zD#i1dW?m%vqUlIjPR2pIa_+G8804i1oOgy&97bl{EI7;7i;=`q$L!&ef_1-!RMwyS zjV$cQs=GN%Mdaa^@-08!(#E8i+`p$ae9nMWbWm<-FgU3r^!oHdreR<`;ih0*QfIf8 zZpU%M=~*sUbG!`Qk!J|E;IEq5c&18rW?4+sp#!~5CHN(eyz+GU*|PWKgLJx>H>8hT zdBK_Uo4NNuQ?pM;aQF%hW~SHC0r#l-qeNM}1s7-Fu*Zn0ZF-KwYjL9bh*x;snF^j@?FD<+! zySEQkeilpq**$13khp^fHxx~Z>_s=}n_jJdqdwv@Fbm%@@{Z1Oa&g>vtnqYqAR6Ym zkx#2TK@Ef1&AEGN9_0AaP}YluKAfMjssatnR#J{m-`xFmnj3}%7m$d@$6b#z4@Oy| zs|H!q4EMNz(irpL;q1>AMcDoKxeUB3=$f#{YC3`g_$UAg912J{!&3hcV^ab>v)1ZP z)LMjkQQ==4pyEF{p3J%$$4j60he1ww%E?%VYT4R7KaNH&W*CBLVON~ zj=hw>U}Cs3D-(!8qw^MS`Bc(xJssHE(Um;R+};09jlAkuaB^*YWsYfuBw`awxYBUh zra1*S-|EH#nyXzok4)klEMhllfSMhO8s=e~NdlM-lEqh*Sj8u@a-7E^U>=x5a$Z?Z zf{`A<6+Lhv9~osHED;zruhqI=!_RVW?m)2bV zp`uZAcr^7v?28t^AwO+Mj1vlR@)LMet{M~5e*u-MHEZbB(ec^6@R6sBMLJ9PHp20; zTYmGrQ2fGhOy=7_wOp zGqvq%tF-nH0Btqfj=&;^7@HzTuaAr{W)GMy^N;Y{VR7ObKs+9qM@d#iF{H=&>k;+O zAK{Y^O0NyGjh&m}b8c5_wr7beZt6{pd;#8yce@LO zI-8+UHH2T6i7NBWzsrwPs`g)=x2v_al1|PRZ#C3L{q>j<(FzD6JAKtP(r*ntDa}X! zEW{9=CG*W-4Z#b~nnS05@>CP?VHPdSn#w#6p2Cj)7zw`SH0ua>YL@rbT23Eu8a{U1 z@RQAr&tr49P*_pI&`L3pB?k9Z(aicRmYJroO6QAq=zBq%Bvc3XI5=tF$s%{?SG>9; zev+gZ^RZ`{xU;33((xnz>0@TW-#ft?S^ z!`Z!kAzy$5zs7bp2UFjTQ}f`n-683hgE#bMo;shS!#I?kZ@g*zNXUY1Ie1$0N22wb zJY`K8AK7}`Qn=rK$-hyd;Lg>Xc&%$VN5mhyJzr<@~`GeHG2i$`H_eLu<=i4841 zyt1dV9-iXB|E=T~bQgQ9VxOHfc~D@kGhn;#lkjxD{^7k=!}Js#@NyA@od0wR7y2T$ zA6HyV?=z%FX0@)oBVLCo#m+GFv*fu?fa3c_n&*{oLvsDK`>p477q-^gakXv!9G0$C z{`hqvWiyt-N~#iAxke{TiJDp}Yc&euot#8M6Q1f5M)G zXT1$HbOlLMw20anfzuhWn4OW{mi&5_C853)U~Gi7G#tl&I_UV-4_hs--z4y089W~3hNg56 zt!=-snX{oAt^AX^oC!A%^4~c!dx>P-sLXcK)qITkDfL+C%Pq%55A8jwfhnhxkIY{#DLG;En4b5Y|9|4t?`u>0q7K)RH{KmCrOxau_28&#@}sw z1D&zfOJ5Sv4@VjP)DOZlP7q=LckWNls@w%5uca4?L`xSK$Gg+z@Mq?wXFp-Gdpr7jCm{FI)OxCK_?ZV>j@T;#J?IWDQTPTNa8^)D+w?R>IiMHk{j;uKyWul;1|4%GqQ1>b^z1O>{Y-81(nSdtpT)EcMd z%SdugE#Zz~kZPnvz*Lf?pRvONGL3nO*`L+ftS%p+$! z&2*M>_{}$HY=9ZnyWxc=ZX@kzHsXYM^>Wp~xvwF&`7Bm%*Y|R;NYwEar7|*X7eXZ| zP7TicMD=sobs=)CQTOERPt-1Dd)!CdbYSH7pe8gVB-nt7`VyVRdEs=U61i6A_ao8V z>M%?pgN4sVkVnNBnjiCW&Q9WwCsEVsa9!?!#Wr5D;dyjW+z;J1uU+=g3FJWWpcxjW zMoF97WMAM%fn64^aL0n%^d5FruM}m0zZz2=9ac>$Z>;+loPXSHh$h=D944k`?=}cR z)ji>YTKSo&A@#(o4f{$}ym)~6u4DfpgZvS3xgH4{p)zzPvqjmwTdmNsil0HIgw6Ie zTjbQ2T+xqwm7iGhccc;4fiXHqo)|wRKM&u^!(&w+g+DRXU@AH+OCgcpKF;Ts%1$g4sLCW%o(-@bi;q<6RT=ejR@Or(UR+^6JAKlZF zijEk{n+~5^{TmW>V}}p@rdm5`G=MF{?v2sPt3Bzvpc$@-+W3cmGE>#b!lRPykpK$K zx-@oLnSccASDceq^qbod0TY15%sQ@r+5(>V)Sgjw_F5A6 z;F8jKfj*N_p#}ok=EFOw#2eRgHy zq`dC;ly)#sg)ILiAN^Vs0G+UV#furstumA!0Dj7*G;u_NKFr#tnu|&#iL0zvJu5I5g;I;g8;tyvUGa*7CHy{U*?lYSDk1j) z30G1AI221FhO%^b(4gDER~5#tqP_0BFKuE(wZSzbWj*can&l6a;uqX4pWPe6z7yKS zsmad_inCb|v>d1+8Q4W7GmGep^jT4B? z<-%$QvZlfsQ^c7d={J{JU9&sbUdE{<&4zK-ipVk0{lvyIq-c6+G;NY7C%k{OH|s4% zVxvM4FcHC0bmDYrUlR-y&StK$nxa)+`hir zWUqP^jZF;KuPi9joVsx>#fw`gwcidASjI3n+miq)S42qA@nHl10E>-@Y@$8}kIG*k zIh}u0?pm$1LLvYIv(1O|hWlrZ`^{8>XtXZLke}&B+#AIj9#IH+K&_5XXK?f85xu3H zwr*y#I?Smlp;{z1-1AP}y!QoaqHyC&momTy&m~+&)G8^{OA*O46FjOz?g7_kMJs_g zG~#)%v)XuVxx%c9CCykxArt*pCi5qtae)n&PgpxV82l ztjVhvTi&IYlAm0i{I_SZ2s10&H)vEi@cz+I+SDFUOWJx(gY%QLCbzv+k;X24^E|61 zKSLxj#wn(;`R@+am#@2A(_5+=Dzuz3-YKnP=b;cbz!xjFnwjhr1R$243Ap?68KDWI zs8e>kXhKZ3P~ceo_PN6_;E1HTDLnj8HZvLv)aNH`z-x1eUds&x5aR3RohH&XosErz z?h^)5Z+NfIv7>61&rTQ$y&|}DjHaIFauk3co;&cJk;nO+5$<*4Udvfw&0n$_Rvuk1viCEps><2yqF&Jm6Z$hN9=~aYg z;}HP1HL}oPLE+DTp3%)$l4Xao2n)b)6MJatIa(9xisxPoT-YX|6|N6+<-vPN;YNAZY9 z7~%zLW6k}A=Tx#6=s?=^fJGy=mPW=ro(6A0AJ_Cd(v>+H3r$9zqgw04zAMKKwwjK` zlYGGIgjzh0gPrFb;64Vhrss*^IB1c!niXm#=!L@o`;{1V{KGE$HK%6z#R8Sir;ZH| zDlc5=fRn`?G12iidH7IUEjbDb;SfF03={X>LaC}OnnL;Cb$r7`oaYiqcqmY+>So6D zp-)kXyCT)fp$x?Q10b;sk66 z)%#TaIJ;{ix-X2Zl+ffv7vL=o(WE|5-SNEE0W&iuaUP#9{thd#cFp z(o7+yo*`hvb{iifkfFt7=9wwP$=x=|gyOogBb{5Cjt-CoRRiM~0I%b~I|0uH;`PlH zxLm>=H6D9@sl$49$Z?-*54hT0EQnfO7NAA1_}`|ugc*RFWN2&q_~45f8N=YTRD>dh zgp}7Wk+(e=0~yKHe!6=RB6IH3Lc^8LZ@K(#NjyVjk>$B2t?fn;QlTy|Xi&zqh!~ob zU3S@Rpjlq=1)abv&%K7HqV6>g!|#cGd{evfR1JF8mn&19ifn7Addeq>MOF)BT1b$t zf#f%qR{$m(HcLxNemwEkrE*`8W&@+=$&h=p5@>?%o%W$s!JbbXF=*yx^-PR>R9I5I zMo5V+TQi8kaiRj3m#596z_|GrZabs(HVN>SklC31!N^J$0 z85%S6ZK738s`H1)M6B#mUyKUZoa~u72>=aKto$ab>lw!I1lab?e+zL^r<-)jH!pkH z=EO~BHmUs#v&@~?u+FOr!59fy`3(6=*ckIkNoN-Yss}2sqt8kbkCmVEr59~wSBHgZ zB)v4tY+>F%af}wsCS3>XMxwCUrfU+&69d>PKv!|E$UluYfo53+G#r7%262_0kgwG> zWDD<0P5I};*#~y|2L(^sEP_Xl{p5o|l$e?`NT85x@ZKr(@@olFxSC;6v^&tBmN z2v(UM+_2-lM`WgKM@(iUJd#~$C3tx6E(t?{k^t=|JB|&oGus0z>9|Ru7us+6>BA3Z zYTouFtUeu9;qk)Qqt#4|38{B7#$w-Y4;q;N$gdw%Ko+K`K-D!~$U5{Ep{}*e&RNC| z>Rx!c=+xQjXe}1vNh_x_z1-o_6Vy0kBu`>NTL)nWA5~4xpdf0*1 z=^)WcXI0a5;!h+soXX*jBldDVmxT`!shZ6l&-2Hp^-s*dr<5W_2Ed_@tq~#UfcO+? z{<<7@8LB*dKrx8Dmi~U73#9mg&JsRJ&Dub*N6O+*>QixA8y%*#>aDbJili_ZWG8kJ za-`BR{Bpwv+<|QyF|N$y3)%2>-DKq71sOhGC^qv^V6!%8*l)aUDQx1pF!_p3EBj++ zH$2jgb~FN!B*SC*hf^pW2snPK^GTKZnFQDH*a;Tv7vOB(tn{Uvr^Dw(XKi?9^QoG5 z2zC_K?D+zCc=MsB)BWY{E@y!}-Zm$a<1uQcbrku<=liSC+oV0TZ6Bgr*Jg)s$0)-* z^GxaCFV*Qm*ZT})pPPu!H#m^Da|d`n_cm>eBdoFlIIuqpFOMLX?r9hby;DEX-S5@Q zEw_JhXL&uAUYn8y8UFPL7pe}tEl(KOiEDD*Wul@Tg(AoYR()Rmdr3~8AP1ACqjc+g z`1|f&R!P;}>4(40`7#(Ji7ILFOZ}J(B1y7Hh8g}&gFY%Q>K$-O7k4aKox&&%H0w3_ zF~+G!@F@)Fty6P39jnb_N(wk5tZq;)zu^mxj5aj~U~@equHh8F51f;=iivRoXa}8O zv0gi7)CjN_^kd{SyUymk7%rj`f#fkkhX)w=_%n^IMATj*c~#X2H=&$FPUh*O%FU7C zv$qY-n?epHGbEbJPRO95BEn9OxEFogoPOE*>BYlrb0oqKNFvlct%QXiPycVMDC6R% z3RHT_vS~8b29ZBk9nQ1_$hWufS9}ee9rqmd%qLApLmmqp%mpKpZ2TAAReU(b^H~VM z0G&|k!^e%3C$BTZX&f42gVb$!IC1PGV|h8$G(z+8ZegLscSL**7f3>f_}L&&k6C~L zMNg4lYx-iq38C(tVosqEdnwM8EymUXZFV`K?>4$*#rdJ|1)?rtbyLqW#~SR04Sw zQc!Owov@i8Zzi8WAni#w;^0}=!K37FDhoRBzX=zry>DhiJrxp^zg=uBnSpl)?Hw~X zw!B{qIkz8#os4(4LQ#OYz&~XNql>m(_tmEVD^WYj_IIi`wz4i<~=}0?4OvXsjJEn zGnQvO_VEfbP5}U4r$VaJ@8<`fq)jydWtE`soZyN}#s*2;gI8_=~qg2B0kbG!GfWmcwClVhWqp@k(TN6O+FcQKre#%QCS_ zq&lUH*Pu=3;>%D@u;Wi@fDV7Z_OS>bx~m;7YR|VQ+h?7E!zxHP5zAJZb?Y9O6xnRK zizLKu;P=f2pR$i(0}?@GEHW>h7i5G>=e@fQ#%YK%d-Q@?TPBL%XaCu*F_vDA47W@0Lf^EqRyWcJ`z4xZo z8~mlyRqbtdWsgt@DPZ?vrYGWWGKyIM?g@ebs;3i*$JvNG+ z)*{KXhhK1C({g_dsf52d%giL9-FhD?y$%{|qWmK9>(1r@dMw7LFYT3^!ZKdmlS2@XZbH;Q?Q)BrxVUJ1=qWf} zX*v=OT>Y{zQy75pScUrkqb|=8nwr>;q`fGn<6d`@zY8F0A7?0l6_m_?OrO@P-3-dM zbuIna%&iPH=E}|&U5)wpkhB&`(v_Z&A?Gfe+HgYW7=+)z8~QcZ1D?&2nIwfe#X`RX zC39I}p@I>!NFO{@P!?FoS%ZnnE)sDIWao<7(aZAEjg*fnd8;Rxx{aT29&BE4@vUcL zZ|l42Q)_g}3XFmSDL7(43qzT%!3-#mM3wW~A|v;ro4n{jcof!GIDg7PVoZFY)_|=s zCI#{A*f05e{x=9RRHM(o?s@lco8n$!G|Gax;grLwmbJKveM=WvB{MW}3$OK>0H04k z>(r$UvmQs$RsJJiKW79l!Xy2H_KM=#|K zr(rqn3?)q5_?M6&pVRWWy>wszl}9nxkfU=uUN>_TLYM=9CxxE818=iTQ5GUJ8P1tM?`&hPChhDDhbAVJxi<3IWJEu3q znaKpzwUloBvUBsy*@*wbuep#8QPz7 zBXv_k>!0BwD<_7X{6i&Sc3Q-Sk+x=uz*_=Os4*Q9-)2 z2rn}_Z1&dlRs^F0 zX|uV`TA*q^&7U7X34jJ`C^z^eozsp!rarZ|-QV6GN;+6>@W!-h9xx`%dM?4rxulEH zkj#H!sGjQ4(D>WyK+tnTT&SQ5_UF7>g!;(l+Lc80^TC==F+;v!2iljLobszFc-9QS zNP;}SsI@28$9T`MSvpf_NdHfX;cK{welx}+WkmTt?IE%{etqwXtxz2s6Zq8AR>if8 zC$an^CoiqoXWrLJ^(T;t1AOw@75njE7wxVq$Cqi38M-&{ZH$u?q~~ccG5iXtjWRut zr>CpRg)MU;aEgi#Ve#?WAAicbV*X=hLJ|?Omps_W%;5`1RFsYD&q8RDR;Rgr)CjTT zH}lO}U`Jaay-cV5qB|;d7)b4Wo84v<)DkM~;rj~M!~w0{Gt zc-#33OZBYnH>*nCOgM^J20i3WhGLX^WFeU{7AFpptK;%J24t*~?c0tLh#kEQvL}~$ zya4BaRE*Y#gElozI3NilmZoFGprsvK-abW6NgF zZ7@k@O0iR>0P2mpm@@}zd3k!j2#r5Xb!mjIB)f z_d>48GQqOaNntVJ<}k;mnWxK57(D-y-6ZKzULNx` zSBzk?R>5loJyIzy3f7S1u z8R^s%Ef^-J!$IFp$lI7^^nYy<)xR3_QZc9O@wrR13BO(7*QjY{`R!1A`o-AcAGqd?k6hw`jo7^jnx@)F2BHVRLPkaQKL@*LiNk|%6Ln(ta5bxyhUdMC^PD2QCjEA{j=>OVJ?XD){V@C@Yo`GyI$x|a_e zMr72j7_*>1%qd@jg$PP?+vL{kJYFKE#&9l9qC*8l?@;x#x7nqp@B(1HFr`x6E4|Pi zUhI_oT=h!2e;qn2B&gvB9EQI{-6bRx-f7SoIDA9*(k7@;DxDdSio;qMQ1=q4o}T%Q zp0mIRxmaSfZVh(fB)=lL3P*9MR)4`yTr(0Vc(l}oGOZ3v&1LkFx4%9ip#X1>p42(7 zL1On7f!UkfvykW3QT2ihEQeprziMqQBtNlcSyA^sf)p$=uR2GPWWOni`02o1QIQiu z0eE9-3Y#CRDh7*BL(HN=G-AB&F~wYuDwx7a)L}Mw=ANY^Il?#$B}z+D2np05(?JP2 zKFl}Sfnk9JLF2IU>|&oX3U5VN%d9>|m}ko`lQaD{J;sVN`oz_RRH}K5$6UIGV@Ir`g+19;03f z`MVzF5yd_#FGweOWLkQDFRMV|$Ajmr+SJ|c0fEAM6D(NFOeR!-Pb>*2?g0gONElp1 z^TQnK(Io>ymAMShXcG@8<`iOzkS_n#+ll7CU<~0s)#;50v#ME=6dc z$(_~d3}(xjE!o`MjZ3{UZVei@v|$-&Lr034sw2AEB0hCwO=R+UHW#?hlS>PL#q(;+ z)3(%hT>Ljv2Dvgfl*|2cwT(DjeMK&Azj3nVq)Gb-(#8)6E1*}2**WmmZbBi_9ySoUYN!Y-S6)_y~kr)W~r>4OofhR z2G?iQqfN}0pp$T5hd@}ORoYrq9z52lG7v&_s zOAiO{z-IObLXxDYCZcnH7kmY#mkYC%d-fU~n#<+(O_#(cLO~^u?m5{P(M(7~EdxLo zb@J$~+#x8Y<;I7uZN7U*(^ks@rSB)wDi@nS061o(A65;eQ2@l4y2w|U3#GXMe|)I* zGnOJlcA(W#P7r$YjBR6CzlS}B>7r5@q1YUb4HuK+qZa>x@->3qwKclDI9PDs_UJ8q z18M?7AO8h*Iq;wEE3O>Z3(lU{kGaBW*si!(8uXAXy+jvP5+oj0k2#|^Jxr)^w=`GE zBr*WB6kagG=18#Hj?~ZDbVnaA(bWmvRi2+M;NT()2!j{_cD3}pIm-h!pHnXd z{^?~0gFyfHqUC4JLH;)v7xTdY$%?jY=^hdL-7HL32N(DIH^7Z|`FOwvM(*9=lw7W- zl|dRAHBh-6qEG)_1taMtO|NR+{rQ^%*Vl4@%Eq{8TY3OHLjSjjvvWPWnptn(syI4Z zmSHC1q<%$|cKW~D&D|BXb+cjQ2!8Ba&dPG#E>#B!ENE zMpcAtZ=Jho%20pH_4qT`A*=NltA+#hjr2w=RIKag>+JBS^rwQkGrVJUYGiBQEttEW zBraGF>Peib!ISku_zTu8|E+Kpf-q9|Wk~qfxau=aE+vg9?~5I75HtW_G2Qt#Gt-WE zpSqi~vWdVsXB-N74VKOY&Z1%#6waSzIb~P9R zugq|GC;>=kp?hk>?e*CrU#@(iHe^B5*y+1v1=I64eGmS@ap!Izaf(`hEnfAF_UTb~wbU_^Vc>6gF z*=2?8u$tIm9Q<@`9sgaOI5SKEcVahzM}waBS*dCE;%UVA&n@z-1~}-Kqf& zV`+I;TD@edE(KBI9hWZj>VY(0BJ{yuB9Tm~+>hcK7?VH9q`^(nE6zyD)bVK12POdJ zQZ|Gy-WMP~9ZE^#M?d3B=^)?J&t(2|e|z~z;j;Y%_s7UJSQ~Z%1fefe4~8TFbzf1L z@lBEe#PN=K@OjyQr=V+xZ;L#K*6Md|fBkd(EBy7;VMl~8JIwhE8PB_q(R=bdJA}4I zrneT2k}T6;ZYhLhYkX5U1(-}K?@2%D#jf9gGPC$g#pdFt{v*7j)^L^Z;$NVKcx$~de=Equ+=(#Ye?l&a-EPlzd2Df#uF0ccPu zZ)8=RE0-z9T}1NRba1%=c6<#9JHM~!z3YJwF&hn26OpXDU1)RbVaG4+4ALnR)$KT206;=M!6;K^xbo|B<_k8w_taNdYtnq!>~Y|{^L|f61rf2 z>(Pidjc(hAj=bz}bEuA?%Gqtfr48FVbu^2oWGrczIVBi(#RhPIfZrWsS*kIiETC4U zK`tDtoQShJGoMspQ?=6rUxwVQqVVphSri6NrRv(Kgm*zq?BT_lG+ zh02$|lU)gU6~=HZnP)sYelnpyr&P^r3Xk-69z{+0`m=E54ervMMY^^A>W18M3e%l2o7jQq1u!g6pwGuHnOMhcl6U=K(h{ zn;Fx>X9GgJjA%m@zGD|sZ7SO3>@_!@B>`k@ERCM~@{2$Oz{cy7E>h2U*iwO@PxMsZQc~mZ;P)eYF;N}Qf7S+%B08u=D+;_0x`-@v- z)TfRrxk3DV`E6Ilr*c~FR$_o<3hzuz2+{qk%u8)M#u+%!9&>h1#+mqkTmaK0@7hI4 zO>Rh!F?Ck$0dMW~iwV88Lo53P3t{2}Wm_adJxE|W(DF4SRC+ZfP{f-STyeuHCCJTN zsP9)c^HlKJ}`7B3Rn^zh4Mmz1-kQ1!j8QO2Wk#DQZT#-TR~b{iQs$q zOc@-_zidY7iIc`fn<;4beO*^k?wI-_dHlOQ5-!3;Mo7d0qwLdHko*J1a+glN3VRA> zpHzuIWx(y}?P-NcH=3rSr{#m~gJ^R0VU_b=}*hB z*+Vp|KW~HLiILX9W-sPJNf)b&!wVR#mP1b{6*jPh8Cwq#wF>+rcjt2Df&y3sO%NA| zcL+5g1I?SX2cQ#E{WfEl*{D;M89fM+M~`}08Nw1Uvm zY>@+WK8GCNB71Eg4_>tpEdE@2$^*rfQ;*|4oa8sAl+bV~B_U5rMu-1L)K!N?*?etS z7g<1IX%Wz+8x^E$k(BNRk(O==kzFL05JgH-KtLKv30Z|D1nCq(LP`*olIEMm_ji5& zd0jp{`^?OlbI#o7KKJ~Xp3rJr9Mi^e!Zm#P?m6eva;mM%R_LW1eSfLCO@gr7HXZVD zgQ(m!qx*H^M%H)xq6bV7(5y#(CKqcz73U{8L5O;9<#IQI{JM}0OD+ofQuYPWeXQE5 zbu|q63Wtv*>yt38$E_8@0C38BtWJ=L$6J&(~e|9o?`t4dK*pGkDE_(snaII??LCvSipnoVX}JvjfcC8*fM zg~DdUS;Q=h8^xWH`|JXO;G;RvY>JlLN5Lh^v1_nqbq_^NX@&(e=u$k+*GkXx_PwM2bp?RYC&pBXQCm7$Zhdgb* z%%koj_7`Y=q(Y15)fK_Yczud)bk1qJ| zhdM+NKwnZ>$GTwX7nxxOZ{~}i6+5{75wDNIxXrKfuWih6=_BXjkiKFP>l#u}#P37` zzD*WnWTia9k>&diPwtQ+?(p$`P>b%RQ(ydP2!C8{qBPmHFc+zCkEotj%qJ~;k6Du6 zaIedVerRCIWns2YS4$6ocvDX7a7`xjBXw%-(%IGG+@a9uwMU65Kd7qTV`LU-!yk^j zsW#hxizK)|yav;0$8QK@RxHE<^w;TWAX?$W4|_SwD{W5XMEngF?5%{Htsnv={#I85 z$O&O(YN%HCu|C1;#xuja<$iE3tL`9p z!@Ie#YJHAM0}&(z^dCW2p+Q{hTTJ;X12{#V!z4bGsyW|$aA6H_Zq7s#*pOouj@r(017Ukg&|`G zt~&=+QzEY8)iu&XVLD?sCn^g(fSpc({b<%Zt!5BLB#cyZ`am zhHNYcFJ~3AR7nulzyC~To%efstVF*;7Gw6!U{DQ*sr=~)Sj!ckC5_-@3VZCx%fHee z=4HPiAoCyYnD@7Y2xNPV>sAjvr5?TfK1IYo3I93g)0hrjmJC|n5)~%$twACq$}9|? zCHNV*qLedBn=jtpux6d@SITua?4u1<^7vpv$ytaPj3?l0V07l>%!-wmzph(;1utVqk z+4G>x8N`(3v7CfPwVzw&lHpFqna)#d#v$f$CvPyxPpS8{q*KRm&)6>>UgK;6>7E0L zPP>Et6I)9T=ia>>*(`e#Zp5ub+lJ+`4d7Y+P?pI zgV`myh#7AKV9XsQ(MUKdYG*1QYQWezMPN9-Y4@N5PDg!);u^EP>~1(XBzgqMETM$q^|K0A=cma6BcbCu$6tpi&P11*R1N<25DYbN`3V54AEQ_0P(eQ)>&(#+W+()KU}8$dR{oOnNov@+$nr=khAn#JLv{i3+)4n9u2j zj@RizwZHTM&>75^0L(L`E*JB;8TxX3emivdhC%hASJ=w6uS9AfJ-TgqZ95Fo+w!j6 z2d`ydqyx;Z1YT^Cgnr`@8p8d%N(a}J4xu0|td#A@RUfnMK%9TQ>m8JWTaqI6@{Joa zhGl>e-O%TY$%HoeusQl4zMw?75Q?Fo$N)0*XOb!|S1W?{b`N9pd;2p|o@!+ZEuZ0hm<|^Xzm> z-V6!bJ?m+mGq+VY*Z_5E*?VN_yt_{fc5RBtjk~l8kBxB?By_b$%`t9jA`YYy|H(O-ERj=D}0p|2&Qc zgeeg4`i)YNIUk#kUWHBb?rlQ-Sgw`tehA7;O=*IP4lFLuPn2_%$y*0PHm=~i>V-+Z z8dak$$Mp!p8~%IEIP@y>`}n^4LiC9}3=|T78u2$6mOb7xy5auWxIuWmTyUUzy#DiY zJ^E?6m_rnH13D?`FfiwP+Q27~{Nlr3Vhh%$a+_(K=Z;~% zGl_kIj&+u&=z!!C+d7+!?*z(cTB0#boFxDEA`B+(9JThfeA}x@ysY zn9F`X8)Wq5(xD!da`Kh}`u; zh*PtC)u|@o`w9x1=l;uD98?c})65|m&~JwUdYM9GdhvyBK8!zSY3Tx!p+p}UKGpKS z@+nt@FvQ6$B(FJz%y47oK}ko?r?N0gHd_*%PTK=vDX2dJD_f(GLZ5K!p51cERXEXE|Ry!_E*Ab&k5`XG2*0wk|8OQfplgv{= z5!gtD%Yc%Q7`g`cHrx=%U*jR$d`m-?dkcE*8%4RWYwrRH`)D7_zgsRb`L<)CBxS4y zsNrhZdx~GWfj|!dx)8ecF#9-Lleu%BANu8uJP%G|@g1)}LVtV=xqqR^ivL$^gmBf( zw5t$!qdMwj1#oQ55sBMFll-(tfXzX-@PYVzk%22Fg++rd3OY~QXfKE0!b(KkOwc_`BlT^{yakgZf^o9PtfqiaTYRWtQceqW^bA3Fq?~40W0Z`(? zhYM?S2vZrTzG1Mx#vM~Im*Xya6wQSt!LKNxc7-s~+d!R77vid>%YQqC(&?|~Hb^2< z1uaV_GqoqIwDbqj_C^G5tt>UEA2v=ajdc}4`4&c4&-n?^h5em(`=}4Z*Iz?hjjG@8 zp5EH8IoE5$#}hzU0QAu-w}eu#GPF|T#$Z2KK*iaYKhe+Lu@-}D=r-H8=(d39e`p{0 z;J^%L^|F}!^;z7Q0@hjy@>t&Z_Vkz00{4c_un@4hX$bv!_1CZrDu{c;R;82u9NE7l zdqCCxe0*~(=>DQ(1rLVzwCLW27M<$T73ZN;^W=m#);HECUoW{y6JFR|@8{?@M=hl?JNl zYD&!$51)9)rC?xLi&zYU;myHO_SY@Um-%l;4ELLK=5I}#d`2FK~CT^ff z*A}F(CQk^6|6C2U;KA&?33_D{8h9`mMZ{M`;phO)(fFTZhj}t_&3X1-MU&A=bQOuy4r$xVu~AyZ$s_J5R=(&%`(^0#;97uGJF z$3C!nj*!eRAT7$$p3?};1}W%Rg+Ir1|J&=r6Qxw*rOT&(zWgZ&-ROe>qJuK3Cb%Jv zzRSGTp#Kthc11GE*Nc<^{1-m$NDITJD~e0zeA^aJp@Y#-iznsN^BS#4F;+5f-57Ss zT~Is_9HIuNuHrF>(4qS<{9ga`Q&s->1^|{{h)*Bg2qJf3C(|-3PF0t+a`gH_o z2qXlmhPJ(A8i%!iy=zpRN-mH>@bSMzE>l5`JZjCn`c$<|s`syOg^lIJ+ynjJ;|3_dPv4ed$dDkK`EQBfs~V{7R$p=@xui({)tHX4H|9cZ&7LQ%$q9(~P zAS^hYMPmG(kbSDP?jF)LL)^?Hps-t1sok;c+{1!20>K}Hm--Z8eQ5= z`KwFMC;8;fF3h(y-JQBTm5midf;|?`^}%n%WX}Mp?)YtaG$wiEC2v_6Binz$b_RxE zq3hlM9nQ%VT|BAik($;^{<7^jTQZysc7+}L#N^4{4;;WX@Dpl<*1Ta@Fli2oE;flt z6b@E>nO+-5uw(`rfbs7NN3ul>b@TH~ha;DJt{GRS#(9?r7Y!`o&)A-jbe9K>IpTOX z37m`_>W#0)&$v;&+DuZz4UX7VyVU$u0<;k1XSjn@ul+M2u*L9FknjuC9HnncwRu^* z;0=1!nx@wOa~=TSlzw(Y9~zzX6Bhm0j|wyjfWTx+d(HfIT!+96fftwfrTOb$R01JGnfB1&;bH=eZi##(MJ- z?HmHC_X}Luf;!|3tJio^p_)3q)X{>;OZl8g{2)SLxlD(i*-j}7&jU(ql69+zozY}~ z75v)*CEM^@nv@^E_xAC;fbo0^>y{V%JJJ9?>z1FuNHPs^ka7 zlHm`b@_YD0C<-SH6wO6Ejt%J_#3*inh)FrVl*eKjIA=B(j41{*hSn1({< z56VcQ8i_#RnS1oVLvOAyVYsW~TwP&OQp3vpQ4%J;6K|U0kPH^(i?t^eVUUPy+C|xgd}u(}j#W1MMUHL~2m}=7{C#^5jJ5_?M)_55Ec3 zt4^&Klq$9qL|CJN`Y8(;KKLQ$NI&=!0Dco;{Y}&baIQ?3^NP{$ri$R8+Qx&x1^IK&b-!J8-rImazd{eje!)C>8AX zeg8(+`fuvJD2(Prm#2ykYM2-z0S3aPwu4_2>v*JDvB|bSVJHqg(GLH>M(DD#rp>#zT2cEV&_a~=2SGf%SPAvE3<%!f=jI|6vU@M;266ER z;hj4;J?;~-2nKU_)w^ReMT{nXjiunooSi;z;*+3ERxTVp*h{rsB6!W&|7<`PBEicg znb>I#?{{pf7OFf%HYYgiLQ2nk80 z&f#KwPGKDtnx;l5&fFf2DQ9fAG(EXVd?^a0o$G$md^Kb~;S_95wK|2Hu3?fw=C*_`)*PWhL~4ZYkxM7f;G&roZMx_u{6; z7&)Kd7X?iT6(`FZoxmpwaMP z2r)mPiYR9UwN+00DQ#T;`-{IrZ$X3w*Km4Bar{$Fl#~7g_e6e!W+&A@xi-z^2u^1$ z>#@2oOia{xXFZ9+MmlzEvE-CY?mYSW*4g8JO&_&#xr+5cU6a+PXc!nXNXCvbX{gp9 zAF?BIgSHHC{(}64PmrMy$KL!lE_@CKUK9e`sM`Jo&!>J1CZsgd^b6JGeOx_9v>*OI zr{O01-HC&A37aObS^@9?m2CnVf&W=AGt8C4H%~g%AXvRngc-QWb$V*dc+m&HTWH^y zqR+An8TEaZ__x4sf{rRSB*c_6#au)fiDR|$u#fJ4012*`zD>P}7oJ8TInhMq&xr1* za`92l0nN<4rvovVgKa571;|!z<6`y{lyP*nc5?xc$2?E}UZgnx zAZsm%a5ks#p+&3YdvN^V-`A1fRdjc%@)>ehO-saYY|tKBA^f_oVchqNX^V91jAI1V z8V?|1dP$-f&wrze3C+_F@$x2>J2FsF#9;^;3O^iGS*OInsv^16=qm7^nihfj(~fHl zKB-~Stq#dVa{rz0<3Imv@C)CmtKQJy_%affo(Qb2x!|1k+YMahZ(c&9*x&+7QJ!X# zk5;ML&*^#AFhUouFpPUD_gEqMH!5BYicfzYgi3TTRC96$>|+G%rX;;75_hJ~)5rIgezY&2#q({Z09@ zGjY*88!3eiX-h%gLwWqlp6{zrlH19An(=3hAHvSAgSRN(RZXH^eb*A)FA`|x31R{Y zYXvo9g4@(YOf69_=BDdZfA=_W4yHmZ{lVj-;HN+_58b4qXX!%Qe%oE`UA%6!WUH4( zV*?lA51)Y+Y7zYQWr7&?!*1!}v=Z7M9_?S3Zk4L{IlIMM?~|w@c7w1c_K{Tq9&g=^OcE+3@CuZ-)FDH~tL0l0&2I zB|LweW~PJsFBlgos7Ub?oXxNhS-FXHe5#o&&!u)HCrk+<=QzY$JqnA29Hgu75 z>6a(3Y#Nl1b8vAs>_uPYNdNQKz+n3-k)h1Ki*AI3C*3HCbIz zlojz-M5m%F#mwh^7iGZ7-t~7?#Wp=~ka-}jGSrCs&mD*oVzBbPM~wMnv5Huda9+9v z95=&$IDy(9ynrkIW566pP*^-O9N6}@D}Ff-y!Xc6N1H-qKkiL=D*wMB`a`!k^dArS z&n}y1_fF>+xIRwY2?rFsy-=s>U-xmocvKE7EfrL8WhOc3;Sra^N(#_;UmuwZA|Qk= z5Ugr|QX2RoB`9Xs=?|7wY=Ys%G9a(65zb%{?oF>8K-Kx5)eDkH)2LEJ-D_&N9|d|A z)d=0b(ERf47c*0`4L?10{p< zSJ)iD&HmfGh9uD)8+f`&wR70*itD^|FG(W+X1RPaZ)Y`NzCv?^e{LKxs87C|=@P#I z)dfi%(*WVsSy5KxYmxuKq5*f1x9vksr$RfqqRZleg#sejvXmn+UtZS$8PLPW`w4vR zbyH{WQf8B6XVb+Sv~v`+<_~Skh`rx3-Fw$5fxjB5+A2wt|Wq`7bF<>^G_UniQ;Ctcd0I0 z@Y{RM%B_=MU$?nvkJfrYqPSh8aN)mX?A$~gp>cdli-9V1 zu})Ev?4ZHb+6Tf4FB**27oTcM@$NS@a9!uOKI15Ye*o|u_$W=1D%t^#%PU*;57s#2 z?iHKpD|viMhw8ik&z!BJP~0CGl;-1PA8H*&V18~~9s%)xK1YU*+^toT9hf4374=;{ zyvg#=_5RQ!m5#aG;z0;IKOo^J8zAX~v_O=~&!y+=PI-<&*{MKy)3$^QY>>_%e%eBI zt(VxT1?zc^b}$f9hW4(u*+V4M$E#($T0mcf{Lxu~P2E_V>Xof#ugho@K{7SI-gh1_ zSgNu&%x~l^r~&!Yr*iI|a95kJrZVER!RA! zqM`8B_A>PNju|6P_E|-iN*>6|=nz#c?xwQXW~D17CImAafUxu59_yrn@4k&eO>qs| zDluPYjNsi9S2jy%&zoryG21ROpR3VO408bFe!Pb;L18%$#|B0tS=9HtifatYC=y&i zM}jN&eX&atmKVN)Qy^k63foV?`Sf`O#Ws13qV|~6Vi{1b)F6JV@sGa_b}SKuthZ64 z#gAmPE`{M^yZbv{y0Lxu4rhBqd6L_OZv|B(00O8|wpQ*L3!CR;>L;Jl!HFKW6FZJH zbaRWUP$GJPx+%Ew0i==WUWONV^*p48hj$dU=p?@(K_blr7|7OyqUhU zY{pk~KwgXS(SZM`-ZQ!Q_HaaEZ+ffy~pT^56$#$^Tn$0_Zg=&mRk~C?mOG z;!3HjH-D8kVl-6^@3TCd>D8JfiTWx2?l~8Mwj;GH5N5@Xd=AFXU#0PS|M|9kl>0mo zs8Sd$*PsqEaNK*^ue9h3z5Qz>Frghk)uQayJ|oH!zJf}G?ln}8$RC!np7QVQn_HhU zb-BzhSaq4tm~E^SGLmxscVsl-^fWFrNw8=AHr@_Uvb7iN+{Kwn`h^Vr!Kr-?%j7zE zt$aMPig?B9uP`Jx8#u3Wy{c1AR|kOE0sp}m|2DoAN!`Uz`Ut!G3=t_obmpaBIue5G z1jFp#4qr8>4uR3<+amCD^qFLoiZ;WxjOzUd3wEgTf{_}|h-Ha!jnnbu?z;*lgsyU3 zp-CTk2c0_iJg`=K=%I8E@3z~t?!^dKR(i*kwKtfP?RE2y7LR`qWmn|Rgi_Z4cJM!M zkA6dmfxw_uW$Dh~(!4>-XAh4QT^V0}}Y^=BU5qx&p0@ zb$Ve>FwQ}N?~nv$bBQa_=vwaf*)#0{O9DM~}LHcoAx9uW$T=g$@^5uBLC zey8y_TSa3;!I|}4$<%`>%zh#Fhi0vhD}vR1r>M_RB~aJE@}u98htynI@$TE2_xm&E zz7_MOVW6!)LaNJLlY~r&m_Foua3FdzrApDGiVMw%av}Uljtj=k9UpgVOwxaCzDUpZ zTlFWIQ4;fu^SjlbvEIal=PZGb2K2t0W7!sEYPlHc=&ONxmJ_`GV~>~?z0E{1`;&n^ zDOt9^l5}AI&UAp?l6NyZI?l}9x(h=1L}juL)5`y?m)v%UzL6`?yRteP)HnZq3<4c? z!8f--V+M_-W(j`is&(cVUJ zUC9d1-pWM7?WWjoo6kRNz5DXb=$NSTcIsS3jxe1+LDav>b`=$2!n@?RQ>=89N)KZ@ zCUg%3LxA>nlh0<&6TXF)q1->XP1z~(45x}f;r=NvCq3h6+Kzli4R7dLOk3kEE?(5+Iwq3!!) zOC^(0BTjwlR;oceb#^G|Kc0gN_1uelQf={h>fIwz!VYHrdev){nIWrAIrB5)E56>M zH9Cg{F{bC!17Jys>&?v1vJ`Ba$f1IU9rjUNS`MJ+l*Hr$;p?^Uu5)r88K4iSBqJyR z>|kpSNahTmeGPxg%F2)rnaaRxA)jlnW&u?A?V+ddM}ywtI%m`V`eJGQyIl8CBl>uAm;LS0EICe&(K$#^``%R?z|O$S+> zFclJcmeZy1bMiTZ)~W5t`mfl}na|xDU{20cYVKb7|BAEHEZA4u;1oInRh<6HTUFD4 z@Kn<$2eER)g2wZO)D9RZVTRgse#^qc*RW&>VtwO4e7F*B62Lk>V@VD zHYuEU1;$V7`%#D9yA09$rIpRLzxDs+LTa=Uq32=%>sbb{w1!RD;e6kD?^=J zq+8p=l|@Db8Cr6yefYf4S{K5^5zCoU0QrHX`c^x!snnSp2s%xp4ps#g<6%nVzs>4 zFW4Nn+^jV)4rQO(OqLj$S?Ip4-Vg=t?o~it0(6o`kRU-t!?15jE_i6QtjSg#cNdT% zj(@!>Uu7*6Bx#f(yX8xQA;-JSaZap!mH%U|c)@$!%YS{j0UKiA`ZqbdBrEoZGFP5I z=gH(p<=d*GL+}?`fs{-W;(pe!#%Ses2f8oQW@} zwT?X>Z9C{tiYl_UB0i)aNWaRn{k-DP4+>l55ub)wGrPI8#@6hx$$9+n&O6P2K9e(J;TA-~DsX+CADqb7? zk8;Bc6?X_5&B}T;vE%j@I&&$8%P3)X|5P1gAj3`BDlqp4G<)M>2zi*j4s|y-go3P22ls);CGwH`LO3w1`yiPC-FnB2g-`ziv$ zops1v_HbF~wGcrg3z{+3b+*Il0 z+BBI5zT@sUqe^No9j2^3KY@2B{QVRceSR7RU+%c1*tJB?z|sjto7pJ*RQ;10!fljU zmTyBn1jMCOAl*}JNx2bh0(kr$P*rj7ogIF5*%#~?^wYshb8TjJluc+5Cbr#4tXEW# zOB)5J6pCE{K)^zv;*#{L3r2v^Ty`BeYA!6bfUe8Co_klegEqUkbsJN^HE!=Jx;U=f zrj!Dg;B-SA<5OSRjzXZ1W}dTg4%~TMU~?g#zncK&(F;R3p5KrIpi_dbCBTf*&OzOf zIoU%`EPd03`vvVK^Dw*=gg!B(EsqaVzU0lD5&t+dO#tIoCd ztY=)_BACPb`Qg8;D1P3=myVJ8kQ0uLfs{o5EyqemEBU283;MGpx!m$H<(j2 zZ8yY|xh6nwQ2bdRL5`d?T?X zP?P#n2mgjfUfw)wAokIV6%C~0t5BUZJm!maKG3Btb>=e~{l2vZs=t@Gu(HC^MB>&d zs7kBZ$~KwZ+Wwl!UD>PaPac12+zaS^KbBDURz`lsD*2}kK5@m$K@+QJKB_@q{)k_> z&IKR!Gbp=q2(tL9pp1)cddYD0P{>Hnl(p9`S0^arB448T^~N7urF)e#MbU~Ll&gXyMORbB4@ zAl zwUmEDfxuz+EsHGs&_{^@_8`3&&IwowOTild>1XQmrofk&iRny#2k^s6kpyMkYP6VwBMnujl& zr}Scz=4VCtDl#`>_P23-=!taZ%K@95VB5j4f*Qd;VOPvw7@!$1k-(g+B8^q&u6Tg& zrvp{FwI-1|5Zpko=WlQk=5@2*QO1wyj!s`e4s0()ohdO=v^t@%FQ5f2K*s!UO=do` z&P^h`D9PGx!DownSgn!E+XUIrvNtwtm~9EEKUNV@FCHBmJBB#&%EkH>p43=y#ftv0?4{oha!>!U_5SWupZ%$)HUp6 zI(DWgFUig_#{GP!Wn|{3j^4;E)8YG?x7TfYj;Tg1peB(%OJpAL)W7LpF!`^kWZGBX z4x}?*#{AJO5tZDZ@~I%7zo|q{c(*Bd$rn$R)yYDIE*Oa=$b-8V2F<*qPyAy`$7&`! zeg)Jm+t=%OUINwVW1{CK2j0EE(rwcLX%lpj*t}9Y8iApwNOmd@=qPMB$ZWH5dtG9> zA6PUw)r?$>UtjrJx~n)Dg{_B*Kdsl4L6f2VpDP%rSkz{`F2{^r2{U^@uYFT@rXi`x zk{&W12~YguQ^`^z%mYnfM{tc!vDDeMK#70{Gb1aC@(5YS=I+@amS8(k0xLW1dLz+f{?+jZW$0BB$4qJKJG8~q3%R5ltw%p9Z z0S?03h+MJ)w425qYfcDrKTIs*xM8M?Bs^f+v0CT@UTGvI%S(GL@d$d$9q(Mr_(PU< ziYP#OIG57?Ft%7(r2ZhW+Vm93BU(3KrWK(>?@)_ zR0%X1==z($h_8>c&5=Pq`L!Mk(mlIZ5gVZ_4lllw+kSN@>CW=uiS3VAqf$k7Wl5RX z$CZ1J>hesAO>}xxRShNj5I~ zBq24tY5wAKZ#5<> zUN3DA-OJOWrswysXG53*w2(%n!7^eIM6ef>li$0$quS@64>*@*GaCU#@;sjPzx+mp z^lqo-TYL?9f@ba;+dI8NgpY)ktX1U?Gw7p=-vpUVGPM1mBToXphYYQo5&Fxq`uWqH z&7}8I>XOut6F&KVm6!wmU8s#X`hVUPlRk{=SAE! ztu7IVU1+8p{G3o%W&R_ls}#5Wm4@y-o3P6|o$cO` z9ax}YqGs*{cVz9^L+(cPSc2L5-5@uveycP2;vq-1o&^vFl!YEv=Kd`|GS8WN+$Xeb zTGn^t5jE9b)RQC&uT8Dz#g<<)n-KOso|Tqwl`@GTClqr}$Zmt991q0(*`cH6%C~jz z{>%E+f?GRA$?r4rT-LI4Y}FsnlR?X<6PdBCWm1^XU0C9uApYvw8)%X5`dX)vZ%m4& zl;(NXr^aSCPFG%#g&=N*qx3~rGgy8HYn61#58O>B0~gG$RwX3pUkZft#V`V4jl8=| zS}J;{$fF5W^CJnfv35h30XO=ZUzbXaEWIpZ^m8XUFMwEF2<6WdVguxd1iu#(QYrlG zjSq+b5%~fM!jbr6rPOXvMyRE1`nb54kbL>fPj3;K%A`e_{A(R*yefcI$e}cUEs&Y` z>Vud=j}Eb~D3H3@#d=m6Ua*j=i1FXAqn7uDr#-`3(*{3cYL0_ja3(VJUd>@f&tPx8rJta0l zHrmTJpzsNYFh3R=<=L@gIK`bU8g7{W;bEJQpUT>87`YbU-gQeFjqQhK26&&%q_O{v=rb1D^^TmjE=^wI6ZnQP%e6b!3Xr#|GcN6uDiN1{1C5M_ z#1Q6im75VQb%qxozYIiBxA7$!tuKmE#__QI*y16Y?U%_!>sH&5HzQVDYHc?V)=>DX4JHFdZ`&00UB43w+Kx~ zHw|6yub^FHq#Sc5C)nn0r`7qLd?0rUWIEbAn5dQ0TD+QCvj%Sj{Sq1~NHKTOoXoF0 z6`B2bG7*Nv*Th9gsp?nH^iEq2eU$#q!SNcCzqevh#&E+!_rZ75)3zMU=1pL?1DM0uzsAT;xl$%CP|o;bY+{=i6w-DF}vSAIP0>HVu2tTkT{1ZZX8 zZXTpjQ*iR-d$u&wGvI1uOdZMhvs&TaYzHlzgY%=F@MoGM(TpxPr1MJeAVSYdgk~a@ zzrJsq&YsJgy*U$p35|xn+!XE)@U&CSueHO}y4$&i5!TG#0mTbPtWDNc;VMd#Z z7WzXE%~Le7t%uq(LCSj#l@n+FX|><@(?6GXVPh2i(4m^>KE~Mumj!!pP4`J6AGJ%oSJHJ3oApAqTvK%9XIg_Q(S!vQT0i6*WF3YCxy{X^ItQWV^=_U-bFOo391a;7bl#$`?a)=E5zXe zGmdl5v_FIW@O~PRAM3pU=L7jNg6dQL@uf0?Rz_|{uRoKv@1pB$*?{aO0nlpy-H?xI z&Lry)o0Yrm0Qx8(kb%_iOM;X?f3Nkf_cmbZ%{|KHVrk8Y?itMqKh6&RcJ-WcP)3<( z+25KiWz^g6qYaQ9a2BGlkLw;OzO(yBS*p9mq3tCyD+h^_yq6y>61RhEC-v;iVGML< zVdi|u;u(ug67h3km3+vC$cPJ0{@i`d5YK17sXo^u3%X9k5I)6|QBQ3nmfL3z=6}1d z*`1R^?qDBG9mn_X5`&T@ps$|tl1tNHzS=z~tS>KCXL}}J)+iZ;@zWSU^8MMtnVt^k z|Cp_tQ<#ayS1?RC5y1)X`;YDJN#cu`oE*P(ZfMf%-sTnEb`0}f{v^8oR+pNVu)pdVW3z{|x}ZatHyDUv#SQHqp_^Zp_5QBmC%nwhVEzVeF1 zcU4zuqud4`CjhD@5mar@p2abUj&IW_kZIl7^2Z{7Xw#L=@kH|Pc!gO2**YF& z*Vp`-(qOWZN5~^5Jo2>r1rCIXbL9GW#`ei%W0R%r3w=f%0Bt^#6n_q@x{1zg)je#S z_{api4CIBruZJ&;U7fi+d)D{C;4DKM*F-#XK%!N|cAv|O&n>ALu#AzuH!}F3Ev@W2 zo}=$rUUYGoOCqgl&`rJib_0qz9G-n#M~Y}4Gvqx7ElImigT-}y)%m2Ufq2;SJ9XpN zn`sW9<7z;*%@!#LsQHPyIS5u8`E4pjg!lgW-J0LWAm;-5C1!WjmF8 zTA(EX94~FOZ!>PlCWvU3UU^s<67VIi_CbnUWJ|qqD(UyY&Zl&C!?uewnU03l={X6l z$;rLL>8$zUcw`h=|K7a$8?}vlh5Lw%S8_EVTq4R9O3K!2UP~@MG3i+@<2AAvIJWpu z{yZ6?HZdsRverMtsn-0#5*WOrIpd>3wwt`ko6&w}S>#vA6|mxH|8=ubRjI}DH(CeN zmJPY8?pwQg>UNLM%(Dl94@DteAj(>7eoubZ`gK;fT|(F3@=DhN3v~5|#n*ranNR~J zxvP3h>;cE0p$?jrX;Z1vBAc2n^q7pCmOP-@0gaiaX=?$eABIuPbtwarXcRM~DePL) z3lr3~n#p~kKU}1@^HU|>Z_H^B&4e{bGUIQZBoLAFaQL~RB|SRPk_m`r@i0Y0hZ(<+ zTDq)ST~vHu{|UcQZubCO;98gD*XLVA-d~G-Z-S_6g7B11=MPBUlMiutqWNTDvmWXj zf13kQqj7>Tnk)OwZ}c%yqu^nCZPFU ze?)lBIJjGYNUN>sSI}GKxfD6RViOYam1p}&p8|yTjAU2wy;E~%9=}|@m+I3agSS~O zP`o9$(8F(~Om}wa?Ip()`rlO2u*6D9`_%R4R7$$+N*FFDac4|fjy}%DbaVP|BK8U2 z^G1hC1#%4(_5y73g6`*Ix@+mA)PhakSGh9eHgF0}Ci6#sb4=S(L=(b0gs#P@2o>ST zC{z}9BrQHdVYO_7f@9NDEib>R=LkO@ZL8L+4O#@cEvJn(U^39yJ~Jo7-kYA=FJ7}ecNl0la*eS zFM|gI$nov(oX^=c+rs6f&-s05N5Hwp4|bbZ)`yS=w)e8vyDS2!flN3ZKayM(G+0c^ z#>tk%*0q^K?{B|Z6z;rtB(b*s*6=|(*o`_+)vckA@`?-5FzOIo!#D%Wt-=JqmEWc= z>}UQmB4QrL`AsHX8jPkrmts@?){pzG^p)C+dV?PX@(ym~3Y^h9e&R_a@GIfS5jxzX zxVmnw>Xi^tw;!=Ze$`ZQ;;!=HI|hStPs|TIZU{c5Wq!$g<34CX3AjnfBQ&<&_rxp4 zMeUN)K*H9Amgc3#Hl+tq)-qMGW548*?xaMUCJPnO)+i+E5CVLrWaHVXgPbzkab>t( ztT4wRslvD@Yi`!vsLPw?2t%$=S)i=cv5K}V3$XXv(%oTr2OwlStB2h<4fQnO?xj(v z7c?bmIB{O>L?cB^u@Gc9z+gjW%Wd#`=nuL82l~>Xu{7LW>Zjiw+n4*ydUR*1G+h|N z0h!t?ChJYYgF6JvKZlW%KT5Mm*%hpxqU63wx%yQ0YrG|XB)Wx)?eed_D)2e^v!^>b zol32O9^G{39gvZcLS8(Ry-(G@_amQDfR8%SA>X~ti?q-%S*5-zY@yn6!zyi>BWdYV z_K(az{>w{Fj^t2;CmEqziMwuVqu!S(9CdEp(Vujy*M_PYZfcqnN$(M&zfXEBU5~}b z@08MnH4wfGYXBzXVhTYMk|Yi@he5uRCsG)y()?Q%aJkOkD>7ouYgEG7J4Drl$1BGd zzhG~L@WpRTXt%BUWol=r3zDnN?&$gsX{CUe)j(ZkyX>!i6ARnV)#4tAZeB$ zWO_wYk!&I|5Ef9Oz@TYXY&bQc3z?;QpE+EQ?(vzd8!~JeqT2S;YK>HF+B(za&uj-1 z!Q_)f;Mb%{{D(XEU|Zdb+suRmscl%lmw~$^Njq?@eF2kbq>>7Y*W*-esQ&#jEI`OF z)`)H$COLi@d1;%zY)vL{{$|n?sZnCqG6T!)!1&ePd2s^|)-7=FtAUX4bH58i93^0& z@=B?596^%6Jf*+f2V>#}WoDgy9&EeU1GYzL+%u&F!dt(T0>G?jfgceKh+DpES^q+X z&x#3giPYNf#UY!@=9WFE(!6gfr1NBAnapBv@tG^ z3*Cluz}-5rpP=h)-X-eKiEP-Y081Iv;6SI0FY`uU%+HWu*EG(?nxFhombA+N*wK=j z1vdRtUF|^I?xqrh`7$WSbmBm#;PMpNgWFP(uJ&;9c8m5IKLb_w%DmpU+LrlaDA0rs zfm975LfbGPGN1RDh<1-Qz=i(8b3NCLNRR(Wz~o08LZzvVri7$Viv6lb>Oll3U9#U#w~QPDYr} z1XYhHJakkDgfr`e!Kkzd178G6f`pqgoVY#;sK-vZ$ZYZy3nauNRXj3lno<*=E~H>R zYJWY@JHKIVr>19U5Hn@~b-YJ6CmLXu?^0s=dSQx5G|c3rD=ikyh3^EvAFR|W^X*!m z%jvg)YzoCOfB*W^QmwD%;!u3&f2sMxdU#Tg_#h{XrGv;!?gdT$Grg4XuA%wA#Y5nh z#Z~szq}LfF zrSDoDknn3M#aJLwr8O#9`M~`kH9!WY$jP~^bf%%w%%(S{AFYvf08jL;7)~V?>XyFQ zx^q4AT!}`K_9VDx-8j<0r8=RhDCKyC>Qo~ELWJO*5``@Q+1joJ$;6zG0!7{Fhsu6<^UtvR$jm6IbP)` z-1iQ%a1l3V0Uaq7)bss0ssoqV<6==Xkhj^+^oJ3+v6;D4ub2f9Xx~w6BHY_3U3#owbutVtL>qKJkyQG$ju9i;FE7GNR)wf)soyrM`p&z`P3RW!=U0)2aUB zc+W4RS~b4D_q5!>-oT|>2PVma&760gT0^u`oD2FbdaLe-nC&FK*KfWtdMpf8l!#M) zMY9po$W^5482$cW{vO9}u_me-AGMBk`%9{ljT<#dcqjlk@yA|jFz4O%OdaxX=VqVi z<~}}N9DJ`wQ!0)UhO|7A%hZk(BRkr+?e;@>E-wiU0LCIL21R#KmpvX4ff`!!wcW1j zNiwImS_MO8Q?cnGmmg4uW0WAw*tuw7;-_K6-_p$St+C83zG>$1QC3OAV(5L36s@>KuBsUZu-od-vI(gd zf6f@Wh~=gRh>}KzRsrNcK1moRW7fJCVtT&XPLiU-mOM0>fQMcHNHX&-(?2Mi1q+QD z7O*tRTyMa8h_awTm<>`ID0e8z9UR;5k(Mi}r+a_6A? zfDHm35s#cB;&-2d(^>pPaK3!xh5dGnf)&aR3dGzUhd7ub0!cQMKBZ&i(0{9&esa{x z`Du^@HM6E3S|(>==j7~I7T5)(JXb91dHdYdS$1ve1}Qz*hF zE7ijGzYo`M)K|RuGrMz9qkAo?jA?MVpBi(Gu+$Z($Kz&<=t%jIEpVQGynQW?9E3#& zl}_!z6`@d)DQkBs^)iW=w9W3x#=tf>)G@8RJ;Ph#Bb49L)P0-Ck>n$e4t7FS7qxc6 ze+%^Lf0KskX~1XXea8^!W8Rnfn2lEKn~pZYwP|&}MR<(PFRvgr5?vs^)GEbYlphii zvU_{25(ZGbbFmQkrw-?&Ka>?Ye8O`#4LlEEo*$J-f3dC-w^Qq_RdaMuVszM0Sa4j; z$7Zjz8#S&ty@Wg~mP;D=slkvpMFkAR^I%ea%qCf1tx-hxK0@8ant*fWhUgVrjX3A@ zM0lfS8}V4t<3OqnDDfOx@UnL`P_CN6e_&7(Q?pw1YZoO(RLhALJoGzSGY+p z-7{tDH{hJ4H!qjyG@H5aUu*m4?tv0W2NpO9n)sUzea8T#lkZ?WH@_(s_`_!0xGqT> zHu>0-a`swq5nC3SE)8vIpP0F~%gsvsy*0ePToo*-_0pTcHtH9GdS5UACo=((xw@f? zH&G!#qbZq7-WSM~-9vG%tehLAcro0gydf-Rw^%%CHlCe2v4bKcF3*j1S8FCIHN zV|K*=R-WVE!-i6A^lHJ#gy4W>$%@4aeaBmisoW;OFk#bA_sl16yU;Q zIVcAigKZ=WA1B?#s0H4(B0<_=Y;q#@H~X7qMs9jtc~`jE{hD+>n?_X+BK(&lj#a(G zK%)8utVDbL#VsF@+F_t(@1Yw5ULgLA%R{P@GxIwr zV-3q{T9M{GAQJJu70qQek9Ef9C_!Hm0v{ibLz=P|7}HGcMck<3+W=b)SFAkzH{ zpzHrdq?6EfMdzXD;DG_Kq|rMDw|(lg=%#OEFnoUb3?(sUT4#XwOnIyI$)lf!VQTzJ zXQk&9iC->N~lQv06wX(>-*1 z`0M#mEK=@O>=Rmwdwf$^;QCJCT-l>6soq?$Sq8Ys|NT1-pz)o7iv5aW!H`FUYcoDK zrk_O&M~EO4`1W^H`0MiX$Z6%)MS5yVpxwSM{YKIJ_vze(;+1_Ft?~@PotZ5Wzxt*F=MUTnBpKSV*FE+S?S$umWt4 zSAi%epIpba*jtl>lL#8DDQ5b4`tNZmcx$|a#})ZiVbtCd;rEIV?S4lp@;u;7tb#3; zHZ8HbyVyJrq$Se1Ubxr#H~F$5XS+o^OAD3!!Xs0@O$!J*35!heOp4xP!oQ;W#N{eaPxoK{ewfDHsyjM*$eJ0~+4hc|} z@fVJ`O$89MOzP8F`|Q)v#;WdjrOt<`zMo$h91`oM?Alo9xf30tkam`pqz51<{@5W6 zI)-9zJ&@DoJ_Fh^u$r1OrUjoqf; z8Dje7gvgqLI80S|=vo$iPyrA^OX&*!=h4G$YqA|gz{9KcX1pM(9DE*~Dxz$Lr*|A( zUD?c4V;R9A3WYLC5FjZ4a&GiEad;(v@@5hgl&l3@6zRdD1FJJp=B`uyYj|G1RJIxP zih=vG)Ap6ajJnudVy`ZdUD)!kKP1J(3AZ`=;mm zU>e@({rC>Slv-`HKyK7M)GOol3-ZFyc43S?Q%9`s(HHAOUcu-8z8cN_PB$z>+}c-< z!=DL752pN>42z0fEMEhvMgM~h7u`m^5Fy7DYt(u2(r9LRuChotl+rh zY=_p9&z5`Recob9a+s)s(&KIi65igHrJ@FkK^76X!i8DJhWMH4eWrREHjL;JSR11C zN;USSH0$o{i&uM>DDqqOxoGF&m1yGjlwUC`gaY~q{>O>3L^g94b0cRe1S4F!i+TC7 zgo5gPh69;4X5(H<26r_${_(Y<@Q2!k49VYR*gKPw(0E5@{-AomR|UZiHmUPkR?5j< z7yX&)af_1aplVt4(S%#*Qn!|HR&Uoe?yZ}I4+z^5`%?r){qAUb@Dw_OfDm!xg6TfZ zqg#E9k^OWwe5Rsx_KGL$<1W8Tbync+55Zq7X{5^6hHci)U-ck_ld_NP3dpbIA1ab~6>k;gauAI2t7 z%M<(ebl1fkBIF2Ef(AqEL?BDlIzf^N-D#JO?BK9BRwh_Dt0IA{*Vou&M7dw=@&g%I z1zNtm<-G_@I&q{g^A(MEcgBtjz@6b24^6^ug5}vPO5_R;VIVjRi*$i>BE?MKdl_oa`zbz{?Kq3x0fy;at z0>l+#0~Okjd}tTKNojRen_yNGR0BKEgm&W=3eA(RyemHc{e`+373fL>J&5NU(drSJ za*H#Cj6rdTP)1k(OHnkk^0)lbg`N^1yp`0Df*no0BH9d^`H}(>~O#( z=aWrq_LWh{!NF`#U3ZBDM>8V=)!Zw#9!g-8Me$%CROAP->WmVM-ggf3$Zw`}cenwo05OH zqElvi1{9EA=S7hV!B%z*?93WN4T;9>Ecdlo*&SLFf7WeBxj>@DHq_;LKM%vpazl>> zTe9g|7QLA7<))>0%4_|*YuwzFxmQ&M%o=~uGBg@XGlB*17MVt~#`g-8Ms$_TYx(P{ zeE+B#pcw$u$>HFg!x_4GsiRW$!0ez++AES+X>0TpK0v=BHOU`fk z7(!iDo>2f1(+7;@AwT$~ym;*4obiVhWTmnn zexrmldg$MdrwT#AW<=d+=O!Tx#=X6eo3Hom3)jD@|0G|pzEqk>$Q3q-j)etSR#Qvl z1!08gakLjPX@67=S^j{(yJb_Hf*0V)FQ_SQOaUsGd%)Z(Ybx?xuy(Sd$j%_Qlj@fQ z5~6m<$K352%I7VfY{y{UY#2tV7G$@cJQpj=T8*B{7Zp@K4C|@_6O?T5uP<9A{ zkqQdO_h8}ct^r-0E@TzqPvTP3wfV`cL2gTJJIgd^tutg5Ze5SMfbVd62SZTc^Jn zNw1nWg2C@-?O8tTncBvaDa8oI!&z>x^i0nJ>Xj8n$`s{%V1!aKp=6(_7b7qLM=1%i zo$ngQ-BsBGljeoCr&{!~mPC>BaN(mTNNE-pb~fb(r7u$z9@}YtQxx_q#xEOBJ3#lM zoAG2Bz#H7p|J>b(t?gD)D4)5?E#mcR4VOBxflhyXX7$Aj@+?^iF?AT>O8P1M+#GRq z(wp{>sLC4YB3aXi{e_#lD(sPDIfzz`*j?RQTk@4eRaS{8Tv zU3#gPhWCw*uTrHnv;K|$5~yGEZjT3}16R2+WtEZ+8ddT)C%4g!pFPNz6t~GI3?63M zyqPs{JH1u9{p{F5`yK;A&FBgusNeoUWL)=x$XGXm+NV=+SDPzDMx#uX$daZ(i&zM| zwSn4EO1YXp=LMittD`8Y=61EqCOX7b0(aH9C$5($%Y z{hm8qHWlFL)o;7pkD=QO(jGAQd z4eNb-f>e9_{?dCy0q^-il%|jK<~#3<#Xzdadg?toG7bbSFT&fmMg_0U;fP39O%$^$dlJ|(%(vXkk3OE=*(&H$Un8HGZ!u}1$%h1Y=t3%)oZp(c zYCr)MN}|B7kqlwhA*YvpCf`maJ;rjnwUMtN4Ae8&qBu zV~^G@GWN{=lmVVGYgJwK7fS8at|LaW>_Ve^ct${>Sn0fOdx+I%Te*p)3Oq;!mAPZR z{ocD@b7zmmm*A}pO?4?hLd)V#_@LJ5QAF82+)ymT>dKVN2!tj`Y9Z-X*)v~1I6H?g z*inao=jdal&Bq;R&f6+E4x#x`s)>B372I%+WN-8R#xDsh$d(8=Yvj)j##Rif@Ey0+U4{;5*Tx~r|D zSNQSA9(PYP_-i^eByU=+db+Z$PtBFfJ;{}p+s>$`&5OGO*EN>YL&&ca@Z&q;$$z+C zel~Dx&wH_+;2Gf8mggk_0ON?*#+|z;fkrlQlQ!LHF4btr@Q~5zp~uJT>#L&_<1cka zchK&CCf%8OYd=gHzp=g_k=1S;hlqlwVRgYv3#ISmxx+-yKNoW%L!#`Kux+;6TbK+?L){f@8 zJ{2d`xgCdyo#3|}naF*{thU*DOO2AA^R|YKcQQGY~ioBQCB!EI3Mqi_opCMly%0vX;A8uep$8+ z2wfP13D#@Zd`mwd4;t+$@S8qB7qDgUfSy@E31Agk^=}`P_7`n_VC4ASe*V&GW=c;K z-LRAQs;Q_Pu(>{i_on( zUJf|=l+vwQ1$-Rc&P_Qf3lA6Z*B5+$sQhNC~dCC+R@P zJqTYypKf%uuk!@y!V@hp;Dv!5AgsL$u@gy5`ax4KJ37PvY22bzcD%0yO#jBiMrU7G zjWCC{h)U;qiZ-sc&pOhv9{+Ohzjb$}}#8d=d z%{la#2;1k~5fuFm|JI;|w%`!YuOzVgM??;VzRJmOVD7Q%Zn*nwe^WGg@YMw}eOK*b zHzL;;Pu18UBdtd{)5euv{|uLC~Ikn#n?!RU07~`sX1uWp+a%6eCM!8;^FXv zBi$y@7Yo36UBBn~L8?iAjI@~Qfv$jfSM7y~eWt8(x}=H;UEc7L>hiU!@mhLA&gJmb zqjsWkp+lXxu0p+iufl7x^_4vTr`sza68PC^NFNKP?xKH*jzs7Ru?jt(8-J6Ies?Bt z7g?ApD_N=*CE+Cao3VfQTsQP|Dp@#Jy;dQ`LgbjP*K{lnsvY$4!)rJ^j<^7 zMKYWsQj4aF07Qry-6qD`lB}J5z5F9gHtzPE1uPWA4b@kthY{+LGlKlFJ>U=fyS(u4 zY{VEX12|ssU4^fUbDAS<|E0|ZPh2S##a4)scQHe_sH=8jWW0}Pj9vRP3n&?*+g`5zYR3HM`HlU(EYAXDy}r63~6 zFInSpmXCY&Ns!m1#9L}$3djNr7z(ZZ_+9A9VXLSj-mE1LGYP1w_gT6oCXcn7VPi?14=zbHiJVOoX#1e zr}vtXS@VJ{{&yuBU5}cPHcWl|%Z^;!gKhK9ZTBTj^*rdefB>a&y^!*a?TTpXhfM%q z`-)x70XVHxRB8;=o3I+)^RiP}vBZ>id#k<}D;4rr+v5?ux=q%TN5RN6VTO{nC*#SH zF;JV2pe#IZFv*(v%0{3`hVz~gqFkFj0LC_On`}8^$I1OWAslXZ-!G|GW!f}c{S~L- zDsy=^O*YywTDABUL0Tx2YXITPAPvM0SDPiF-)L#bkL|T-^>AHPMj4Tv+=c<^lsaAy z4c3k;zRYv6KB};y`MdYLn?sO&m4wA~ zC*8+SrHsHU6?@?L!C*EyK3EIgg~yHcF?vM=aQ>wB)--2^%RQm#H}g?2w$4a}5I_$= zeRA&8w}FbRWr>zwY?({;5>SO0MX`LV4bt$Ya%rYJcVHis)IcuPif2?pDFHrRFn|6Y3)%MSykB+n z3rssl*r)1P%1h+L=?&Q82_CsZ{!{pkjqk9f%+0UI(WfULaM@I)M z2ZH9l<+E1;rbq(Waiw(L*?*V0==6~BVo|jjSegcHf1!i6j6Zrtz;uueRXsgFrHwc2 zEA206JfG<+HH-(8Kn}p%VsKE(Zj`#bmQD~)9@)&*OB)__G^5sKe|h1Np`SOz=d)}d z#CQ&gLFPk)cH1FiU!4|?E=iA=4Zct2OjMYNfOTULx^eRXnWQ5|5I?`0oN@oKt9`A#hDBcDc1>tm5J1{- zsXkhyy2V8(PmM5~UvO{l&@O22T6AglR`Fv_h<^vl_7s4H<)^*~OzpCg-NrL4D>pZU z^6bP>6Uaq)8~>4L=w~ zX`cU^W7K+~jFy(h#3L2l5etPHj!Sva8J+1TNI*@BNI?ExQ267mH zl>o>TyG2R>SIs#FvOoNmvSL(#s$WY_8_#5$&k%RAI=&YTCj;A~DHGKd6+5a$NGr}F zDX+ekm(DWHhRLDQAAJ9d1nZpgeIRi0A%%zUNo`!&ZCtGEgqauFoRBLCP^4V7--aD) zmy2c&hd6+JxHh%j-y%v<Cn2WdM8Bw zRlsK4-hIg*0T*rzd@0+DbMo=f-yR$0(83DHOdD$s4VVNAmpF1D76GNlwD)#nLyYQn z8LWl=EE6vY*5m_NuMHoqIwiU&Mvzwv=eKWuorkVfY+9Gty;RT99o=dH(-cQoEwiBc z8XU=xCIs}u%RFnhh9w2}jV9@kFaB>yi$em`P2h9&=cPD6dFYB9=`M%BU9k7s#+Uu* z{x9AWA-;EyvW$rh8z>n!Kd$R4-!GQp^}#zd3x&_1jB=&vPKshM-T-j(3;IP@u}1}{ zZz(%E8m;@GddYo1?;a}V6GT@c_ojOz9jdrvN(LfukPrA|*ZY+T%v|FwZV~Gd2-hv% zs=T|b^w#OVaRKbE1dSNE1hVL?W6N~KUOR~Oz5i)-ruO$KBzik_;D&&D6(V!<(v*JZ zRs#tx#*3no_^wNOW=E8+tUqQ?TvuBjm6ZYptPO%#UuaxF;=YlnuQvmb$cu`$!_& zijlwc=Jkp9LD{{16X*u1wGtM~KQeE}h|>c4vr=Qo=<3HC*3^Ps=h?;$b!Z7MD>v!c z36gPgqgXZve(*!Aj*{87rV(MIEm>~2A~)M%r7yQ$kC=&zAP6^H?Cy@uj!mVdqw|-h z&cp=74sC9##yM$*UY=YWfdb7Szy7<;Ct}cJ^DBsuH_zh34H)BB6*UdeT7(GEzGQh# zu`zqYN_4$AD~&7;XZffY70bMJYn%BdS|}EX-Y!fdrB}dp#ZOn;!Q|+=$BmZs(i-A_ zw3FugdlJ%{(Q@a^73Wbg_ZXL;|^a93HcFh zgJ8d>o2#RXjK^sw{#AmrAY0tJ_NXyTX{- zQZ`jVy`+CY(qlbyyf(KW@aZM$aQHUZhfR-l`(92^bJJM{E8_(3)*lOm8^eb!{7R7A zB4GP-Yx8_j-nT#3)Sf%k(`g_v{-2$ivTs5Z`7O+6`PM7`0{=6&)td;pSzn_AH#Vs z9e(!1Zwxb*qeq9$thHmfni!H@=iQuU3>Pn>IbvVVXX~C5nXVnY^YRPA$@RZNZ#GN= z)^B8L@LycOb(;TYyD5r%( zs%K-LwE~^i`$EqjodVK+fepUXvVW&bm~?f3LJ;6!d`;EQz9S>>zxDI?_N7lDqd$ji zl{7kv%70S1zKGe1f~B((F7LI6p{%j=h=6**@`S!uU!+RvH?!cY zTW$LVW3!*`7A82vSO4#@ufK!ECK!pr^Zy>DGB!vlG2-N-tnA2YGN*-NQRm9$D;X%` zv)OCEUsxp@sZYJl4(3#I*k`jO9zlu@>`Dc0jKOF$O$<4hx6b_;+moL2OXlQFxmaGo zt?P*f&~IiH5b`ZC?4@dd@|gIBNo&`IaueOm7AyRKStl|p7>Ml2X|Pi{@DL8nk5Jem z%NNwfC-~cC$Xxp)wQtx#$#4IpC=-c_WR0%H??=aCTvzkCe0k(X9)f@|ftRPCG6jJf zP_@}?8@GQ72$kB^bjE2scGVtmS{rgMh(o*X+37@NnVl^zrvy0*;A<9W5;1DJvJXxb zK=P&l>}XnUDsrGb$PKhKh<^EEl)t@ABF&LL=SxaN&9=T!EKH#CVU;gSs`xj{>!k^@ zl@Om8K}`WCGfoshyO{LOe_QOD1oKO8TDR)3^926MT@fG6 z;UMQ09OSC(`Vk_Hi+1}`+s|1HK_eMvoB&uQ9RV`sdzr56%DqNnZNzxK#WuP$3;Bm8 z!-H}Km9z6C>1pFZ${DIALkp@_hrkW{*8YGua-o0osPAkWx0t@&K6i@906{60f`r%x z3WB|5QOX>iQY*?+hKHYmuRraeNy}VSYZ%I|cXu0U4D1P#E;X7@nwQGKyZlk5-uM+8 zZp_|7stosyiwfQ-up}(NlIW=eRWVwSlUI9ohmYU=qh#&6TPB5{x7^nIF>6j2r9a@+ zgvlEN@BnIZBMtr1U)+o31G!`B@$e$@+xa%X;}5TNp;c7CvBfWlnS`V673jc`fYH~O zqJo(4T*G@k)L_?iRE079hh*r8gf7}Fvlxfq{V9iQJ)e8R=Ix=U36@c-PACZk@J&Ae)A{k^|kLYW&EYG>&NEPyJ~S${j<6bPutbB*=a>O zY<>j15>Q?)F7ixN1S~k3v=sU?DIl}>ZUyghzbvy-!%IhaNHWpYV`uI+vE$ppws2ui z!R`6OwKO(FL-3ZPwEgurT2$}LQbQ!h@q+-*P}%=Kk?3+=%S$Jn=!c3}`+ZA3dlBXJ z7lz3CZ#6Yrh(V->$)d$7A3lB4+8yP(SHBw(>vM;UNdksf;bFo8 z?!c)Ul$@U!!QhX@nmh!H8IW=z@Q6}$=8yIz8}toies#u~Wi*IV5Qz@YdC%+IW>%0@ z`R7Wb^8EFBviV_CK$j4(lDiT3$>PRQ0rj;`GwyYI&b0%FH|utoZQ&k4p88xiHl@3 zihV3#{(MfV6R7~L^AHHER;hdWz}c{_WgY(hr@M<_vfFc)dH*Qyj)Y~_#Cnoq$&~wv z&3stGn4~fYzhEbU(#=F!#VcuOYRsRz@&SOEgr0FN=MPbq*dEFoS8B{p@MVSjw(k3T zos%-}MO!wWqQxIK4n7BoTiQ_Sa#Vfv_he@w-8?!;%PYnazD@&=%LED=0CsmO16Y16 zI+KzZRI)4QvZJ706Iy?BFMLGLLz>>Mxp_Yw{4_YqTTdo4+{9w~!2n2bvz>SdG$0wO z?bvBNR104xb7<&%hP$RCA}*o;?`*(z+Ju2XyCJCiM5){Ey%@9ywr0e%G(@04YoyzW z)Sl0#iW?TfoM&Y5bIP5f88vD&TtE8pG}Y#12FwrO0Desr5}7+?m2#JW?k!iIaKTkp zNCS>vU&wy$6*14+9+hUD^<18t4t|t?e*H#>aNCzX`{IP{-GgDKr)M!%5AT8~S~LKA z#{kW9SVZ z*{<3ZMm{ixq@JuT|0VNTd9y(El*GUO#JZ*NpIK(m93Z6=VX^?Mo)OehkmNZ%d+h6) za$>ED+qxd5*YX#x@1C>97)|cue&5NCg4|`Uc;^Wv~#uLI2Q7P zx4k}l`Jp}YgH(b-y*4cyoU%=E!hE9USz8_U8hATjz5*m5{7`_kRyaQ8v(&7<(}#0# zuJhd;bYa@=Z&7iuXuHDgbLGb&$d$Gcw2;OZ=?_U=Gf^r3pD=KU5l=PD@y5(gE&KQ{Aln-B{e2iJ{@Vv`rj_?7)MXMFdgdqmH`@hTk-y1y z-L)-H5C6+LjtxsJKJ7%Ezgey_wAv&>ziaH+Pdv-zz*t)r$FnV7E_Wvw80dUmt??hJ z1rM6QoX3BiPr}n2ng7-0eO?z2_Q0oq#_Jz}I^HuK`dNQ<<2%@IN9R!OU6j%P|F0YT ZD9V>tll!`XB<%(8Co8EWQ6X*|^gk2#LK6T0 literal 0 HcmV?d00001 diff --git a/docs/plots/extended/poincare/mobius_add.py b/docs/plots/extended/poincare/mobius_add.py new file mode 100644 index 00000000..159656a8 --- /dev/null +++ b/docs/plots/extended/poincare/mobius_add.py @@ -0,0 +1,29 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib import rcParams + +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True + +sns.set_style("white") + +x = torch.tensor((-0.25, -0.75)) / 2 +y = torch.tensor((0.65, -0.55)) / 2 +x_plus_y = pmath.mobius_add(x, y) + + +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.annotate("x", x - 0.09, fontsize=15) +plt.annotate("y", y - 0.09, fontsize=15) +plt.annotate(r"$x\oplus y$", x_plus_y - torch.tensor([0.1, 0.15]), fontsize=15) +plt.arrow(0, 0, *x, width=0.01, color="r") +plt.arrow(0, 0, *y, width=0.01, color="g") +plt.arrow(0, 0, *x_plus_y, width=0.01, color="b") +plt.title(r"Addition $x\oplus y$") +plt.show() diff --git a/docs/plots/extended/poincare/mobius_matvec.py b/docs/plots/extended/poincare/mobius_matvec.py new file mode 100644 index 00000000..df02968d --- /dev/null +++ b/docs/plots/extended/poincare/mobius_matvec.py @@ -0,0 +1,30 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib import rcParams + +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True +sns.set_style("white") +x = torch.tensor((-0.25, -0.75)) / 3 +M = torch.tensor([[-1, -1.5], [0.2, 0.5]]) +M_x = pmath.mobius_matvec(M, x) + + +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.annotate("x", x - 0.09, fontsize=15) +plt.annotate( + r"$M=\begin{bmatrix}-1 &-1.5\\.2 &.5\end{bmatrix}$", + x + torch.tensor([-0.5, 0.5]), + fontsize=15, +) +plt.annotate(r"$M^\otimes x$", M_x - torch.tensor([0.1, 0.15]), fontsize=15) +plt.arrow(0, 0, *x, width=0.01, color="r") +plt.arrow(0, 0, *M_x, width=0.01, color="b") +plt.title(r"Matrix multiplication $M\otimes x$") +plt.show() diff --git a/docs/plots/extended/poincare/mobius_sigmoid_apply.py b/docs/plots/extended/poincare/mobius_sigmoid_apply.py new file mode 100644 index 00000000..deeb7f7e --- /dev/null +++ b/docs/plots/extended/poincare/mobius_sigmoid_apply.py @@ -0,0 +1,27 @@ +import geoopt.manifolds.poincare.math as pmath +from matplotlib import rcParams +import torch +import matplotlib.pyplot as plt +import seaborn as sns + +sns.set_style("white") +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True +x = torch.tensor((-0.25, -0.75)) / 3 +f_x = pmath.mobius_fn_apply(torch.sigmoid, x) + + +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.annotate("x", x - 0.09, fontsize=15) +plt.annotate( + r"$\sigma(x)=\frac{1}{1+e^{-x}}$", x + torch.tensor([-0.7, 0.5]), fontsize=15 +) +plt.annotate(r"$\sigma^\otimes(x)$", f_x - torch.tensor([0.1, 0.15]), fontsize=15) +plt.arrow(0, 0, *x, width=0.01, color="r") +plt.arrow(0, 0, *f_x, width=0.01, color="b") +plt.title(r"Mobius function (sigmoid) apply $\sigma^\otimes(x)$") +plt.show() diff --git a/docs/plots/extended/poincare/parallel_transport.py b/docs/plots/extended/poincare/parallel_transport.py new file mode 100644 index 00000000..be624c52 --- /dev/null +++ b/docs/plots/extended/poincare/parallel_transport.py @@ -0,0 +1,40 @@ +import geoopt.manifolds.poincare.math as pmath +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib import rcParams + +rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" +rcParams["text.usetex"] = True + + +sns.set_style("white") + +x = torch.tensor((-0.25, -0.75)) +v1 = torch.tensor((np.sin(np.pi / 3), np.cos(np.pi / 3))) / 5 +v2 = torch.tensor((np.sin(-np.pi / 3), np.cos(np.pi / 3))) / 5 +y = torch.tensor((0.65, -0.55)) +t = torch.linspace(0, 1) +xy = pmath.logmap(x, y) +path = pmath.geodesic(t[:, None], x, y) +yv1 = pmath.parallel_transport(x, y, v1) +yv2 = pmath.parallel_transport(x, y, v2) + + +circle = plt.Circle((0, 0), 1, fill=False, color="b") +plt.gca().add_artist(circle) +plt.xlim(-1.1, 1.1) +plt.ylim(-1.1, 1.1) +plt.gca().set_aspect("equal") +plt.annotate("x", x - 0.07, fontsize=15) +plt.annotate("y", y - 0.07, fontsize=15) +plt.annotate(r"$\vec{v}$", x + torch.tensor([0.3, 0.5]), fontsize=15) +plt.arrow(*x, *v1, width=0.01, color="r") +plt.arrow(*x, *xy, width=0.01, color="g") +plt.arrow(*x, *v2, width=0.01, color="b") +plt.arrow(*y, *yv1, width=0.01, color="r") +plt.arrow(*y, *yv2, width=0.01, color="b") +plt.plot(*path.t().numpy(), color="g") +plt.title(r"parallel transport $P^c_{x\to y}$") +plt.show() diff --git a/docs/plots/extended/poincare/poincare_lines.gif b/docs/plots/extended/poincare/poincare_lines.gif new file mode 100644 index 0000000000000000000000000000000000000000..da427047779ffabd2126c5bd33faf1f0299308a6 GIT binary patch literal 48849 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

IZUTor&p+JMbh%^`=-~ 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Ξ2T4mp0u*Uus`f%bi_Px+Nvi9ebPIx&7 z_usVEUY~gY+1Fzb-M_6841$FArDl6XID;7VRuJU@w8l zK@btl9U9kR58oxAB9WG_iCxBn=2&zah%FQ>;N)VX$-s&noIknc4~SZP=VGCVkPqU1 zyCVyY-aPs7+>jN^_Q5ZKG@+6lvTAr6fof_;q>XWG6ENH3Fb!ft8#ttmM}=>}1yg|W zl)^&6HZ`m04~T`kpni1r{$+=dZV(ftI%zW4+!_}0ZYzGKMfKl~;gh?c*-D5PhC|1& zW{Oz!RAp=v=-4I}A6bl?1l2ZGm@!u*?;a-CXr_sAF1PUooz7FLq2_C&1NY*-t8K5n dB1RPw|7%QW3`_bLn>6Rt+H{{i#NZQuX^ literal 0 HcmV?d00001 diff --git a/geoopt/__init__.py b/geoopt/__init__.py index 43f95f74..fe0609f3 100644 --- a/geoopt/__init__.py +++ b/geoopt/__init__.py @@ -11,6 +11,7 @@ Sphere, SphereSubspaceIntersection, SphereSubspaceComplementIntersection, + PoincareBall, ) __version__ = "0.0.1" diff --git a/geoopt/linalg/_expm.py b/geoopt/linalg/_expm.py index 1727a33d..1ffb88a7 100644 --- a/geoopt/linalg/_expm.py +++ b/geoopt/linalg/_expm.py @@ -3,7 +3,7 @@ maintainer: ferrine """ -import torch +import torch.jit @torch.jit.script @@ -73,6 +73,7 @@ def expm_one(A): # pragma: no cover U, V = torch_pade13(Ascaled) P = U + V Q = -U + V + # TODO: torch.gesv -> torch.solve after pytorch release R, _ = torch.gesv(P, Q) # solve P = Q*R expmA = matrix_2_power(R, n_squarings) return expmA diff --git a/geoopt/linalg/batch_linalg.py b/geoopt/linalg/batch_linalg.py index f4d8842e..2a7aabe7 100644 --- a/geoopt/linalg/batch_linalg.py +++ b/geoopt/linalg/batch_linalg.py @@ -1,4 +1,4 @@ -import torch +import torch.jit from . import _expm __all__ = ["svd", "qr", "sym", "extract_diag", "matrix_rank", "expm", "block_matrix"] diff --git a/geoopt/manifolds/__init__.py b/geoopt/manifolds/__init__.py index a54c88b6..99fe4840 100644 --- a/geoopt/manifolds/__init__.py +++ b/geoopt/manifolds/__init__.py @@ -6,3 +6,5 @@ SphereSubspaceComplementIntersection, SphereSubspaceIntersection, ) +from .poincare import PoincareBall +from . import poincare diff --git a/geoopt/manifolds/base.py b/geoopt/manifolds/base.py index 3440cb86..ee1949e7 100644 --- a/geoopt/manifolds/base.py +++ b/geoopt/manifolds/base.py @@ -210,7 +210,7 @@ def set_default_order(self, order): Returns ------- - self + Manifold returns same instance """ if order is None: @@ -245,7 +245,7 @@ def reset_default_order(self): Returns ------- - self + Manifold returns same instance """ return self.set_default_order(None) @@ -471,7 +471,7 @@ def assert_check_vector_on_tangent(self, x, u, atol=1e-5, rtol=1e-5): ) ) - def dist(self, x, y): + def dist(self, x, y, keepdim=False): """ Compute distance between 2 points on the manifold that is the shortest path along geodesics @@ -481,13 +481,15 @@ def dist(self, x, y): point on the manifold y : tensor point on the manifold + keepdim : bool + keep the last dim? Returns ------- scalar distance between two points """ - return self._dist(x, y) + return self._dist(x, y, keepdim=keepdim) def retr(self, x, u, t=1.0, order=None): """ @@ -638,7 +640,7 @@ def transp(self, x, v, *more, u=None, t=1.0, y=None, order=None): else: raise TypeError("transp() requires either y or u") - def inner(self, x, u, v=None): + def inner(self, x, u, v=None, keepdim=False): """ Inner product for tangent vectors at point :math:`x` @@ -650,6 +652,8 @@ def inner(self, x, u, v=None): tangent vector at point :math:`x` v : tensor (optional) tangent vector at point :math:`x` + keepdim : bool + keep the last dim? Returns ------- @@ -658,7 +662,7 @@ def inner(self, x, u, v=None): """ if v is None and self._inner_autofill: v = u - return self._inner(x, u, v) + return self._inner(x, u, v, keepdim=keepdim) # dev: autofill None parameter or propagate None? _inner_autofill = True @@ -907,7 +911,7 @@ def _retr(self, x, u, t): _dist = not_implemented @abc.abstractmethod - def _inner(self, x, u, v): + def _inner(self, x, u, v, keepdim): """ Developer Guide diff --git a/geoopt/manifolds/euclidean.py b/geoopt/manifolds/euclidean.py index e4f5576d..60bf4f3c 100644 --- a/geoopt/manifolds/euclidean.py +++ b/geoopt/manifolds/euclidean.py @@ -24,7 +24,7 @@ def _check_vector_on_tangent(self, x, u, atol=1e-5, rtol=1e-5): def _retr(self, x, u, t): return x + t * u - def _inner(self, x, u, v): + def _inner(self, x, u, v, keepdim): return u * v def _proju(self, x, u): @@ -50,5 +50,5 @@ def _transp2y(self, x, v, *more, y): def _logmap(self, x, y): return y - x - def _dist(self, x, y): + def _dist(self, x, y, keepdim): return (x - y).abs() diff --git a/geoopt/manifolds/poincare/__init__.py b/geoopt/manifolds/poincare/__init__.py new file mode 100644 index 00000000..1d8d723f --- /dev/null +++ b/geoopt/manifolds/poincare/__init__.py @@ -0,0 +1,102 @@ +import torch +from . import math +from ..base import Manifold + +__all__ = ["PoincareBall"] + + +class PoincareBall(Manifold): + """ + Poincare ball model, see more in :doc:`/extended/poincare` + + Parameters + ---------- + c : float|tensor + ball negative curvature + + Notes + ----- + It is extremely recommended to work with this manifold in double precision + """ + + ndim = 1 + reversible = False + _default_order = 1 + name = "Poincare ball" + + def __init__(self, c=1.0): + super().__init__() + self.register_buffer("c", torch.as_tensor(c)) + + def _check_shape(self, x, name): + ok = x.dim() > 0 + if not ok: + reason = "'{}' on poincare ball requires more that zero dim".format(name) + else: + reason = None + return ok, reason + + def _check_point_on_manifold(self, x, atol=1e-5, rtol=1e-5): + px = math.project(x, c=self.c) + ok = torch.allclose(x, px, atol=atol, rtol=rtol) + if not ok: + reason = "'x' norm lies out of the bounds [-1/sqrt(c)+eps, 1/sqrt(c)-eps]" + else: + reason = None + return ok, reason + + def _check_vector_on_tangent(self, x, u, atol=1e-5, rtol=1e-5): + return True, None + + def _dist(self, x, y, keepdim): + return math.dist(x, y, c=self.c, keepdim=keepdim) + + def _egrad2rgrad(self, x, u): + return math.egrad2rgrad(x, u, c=self.c) + + def _retr(self, x, u, t): + # always assume u is scaled properly + approx = x + u * t + return math.project(approx, c=self.c) + + _retr_transp_default_preference = "2y" + + def _projx(self, x): + return math.project(x, c=self.c) + + def _proju(self, x, u): + return math.clip_tangent(x, u, c=self.c) + + def _inner(self, x, u, v, keepdim): + return math.inner(x, u, v, c=self.c, keepdim=keepdim) + + def _expmap(self, x, u, t): + return math.project(math.expmap(x, u * t, c=self.c), c=self.c) + + def _logmap(self, x, y): + return math.logmap(x, y, c=self.c) + + def _transp2y(self, x, v, *more, y): + if not more: + return math.parallel_transport(x, y, v, c=self.c) + else: + n = len(more) + 1 + vecs = torch.stack((v,) + more, dim=0) + transp = math.parallel_transport(x, y, vecs, c=self.c) + return tuple(transp[i] for i in range(n)) + + def _transp_follow(self, x, v, *more, u, t): + y = self._retr(x, u, t) + return self._transp2y(x, v, *more, y=y) + + def _expmap_transp(self, x, v, *more, u, t): + y = self._expmap(x, u, t) + vs = self._transp2y(x, v, *more, y=y) + if more: + return (y,) + vs + else: + return y, vs + + def _transp_follow_expmap(self, x, v, *more, u, t): + y = self._expmap(x, u, t) + return self._transp2y(x, v, *more, y=y) diff --git a/geoopt/manifolds/poincare/math.py b/geoopt/manifolds/poincare/math.py new file mode 100644 index 00000000..bd8f9469 --- /dev/null +++ b/geoopt/manifolds/poincare/math.py @@ -0,0 +1,1347 @@ +""" +Functions for math on Poincare ball model. Most of this is taken from +a well written paper by Octavian-Eugen Ganea (2018) [1]_ + + +.. [1] Octavian-Eugen Ganea et al., Hyperbolic Neural Networks, NIPS 2018 +""" + +import functools +import torch.jit + + +def tanh(x): + return x.clamp(-15, 15).tanh() + + +class Artanh(torch.autograd.Function): + @staticmethod + def forward(ctx, x): + x = x.clamp(-1 + 1e-15, 1 - 1e-15) + ctx.save_for_backward(x) + dtype = x.dtype + x = x.double() + res = (torch.log_(1 + x).sub_(torch.log_(1 - x))).mul_(0.5) + return res.to(dtype) + + @staticmethod + def backward(ctx, grad_output): + input, = ctx.saved_tensors + return grad_output / (1 - input ** 2) + + +class Arsinh(torch.autograd.Function): + @staticmethod + def forward(ctx, x): + ctx.save_for_backward(x) + z = x.double() + return (z + torch.sqrt_(1 + z.pow(2))).clamp_min_(1e-15).log_().to(x.dtype) + + @staticmethod + def backward(ctx, grad_output): + input, = ctx.saved_tensors + return grad_output / (1 + input ** 2) ** 0.5 + + +def artanh(x): + return Artanh.apply(x) + + +def arsinh(x): + return Arsinh.apply(x) + + +def project(x, *, c=1.0, dim=-1): + r""" + Safe projection on the manifold for numerical stability. + + Parameters + ---------- + x : tensor + point on the Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension to compute norm + + Returns + ------- + tensor + projected vector on the manifold + """ + return _project(x, c, dim) + + +@torch.jit.script +def _max_norm(x): + if x.dtype == torch.float32: + maxnorm = torch.full((), 1 - 3e-3, dtype=x.dtype, device=x.device) + else: + maxnorm = torch.full((), 1 - 1e-5, dtype=x.dtype, device=x.device) + return maxnorm + + +def _project(x, c, dim: int = -1): + norm = x.norm(dim=dim, keepdim=True, p=2) + maxnorm = _max_norm(x) / (c ** 0.5) + cond = norm > maxnorm + projected = x / norm * maxnorm + return torch.where(cond, projected, x) + + +def lambda_x(x, *, c=1.0, keepdim=False, dim=-1): + r""" + Compute the conformal factor :math:`\lambda^c_x` for a point on the ball + + .. math:: + + \lambda^c_x = \frac{1}{1 - c \|x\|_2^2} + + Parameters + ---------- + x : tensor + point on the Poincare ball + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + conformal factor + """ + return _lambda_x(x, c, keepdim=keepdim, dim=dim) + + +def _lambda_x(x, c, keepdim: bool = False, dim: int = -1): + return 2 / (1 - c * x.pow(2).sum(dim=dim, keepdim=keepdim)) + + +def inner(x, u, v, *, c=1.0, keepdim=False, dim=-1): + r""" + Compute inner product for two vectors on the tangent space w.r.t Riemannian metric on the Poincare ball + + .. math:: + + \langle u, v\rangle_x = (\lambda^c_x)^2 \langle u, v \rangle + + Parameters + ---------- + x : tensor + point on the Poincare ball + u : tensor + tangent vector to :math:`x` on Poincare ball + v : tensor + tangent vector to :math:`x` on Poincare ball + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + inner product + """ + return _inner(x, u, v, c, keepdim=keepdim, dim=dim) + + +def _inner(x, u, v, c, keepdim: bool = False, dim: int = -1): + return _lambda_x(x, c, keepdim=True, dim=dim) ** 2 * (u * v).sum( + dim=dim, keepdim=keepdim + ) + + +def norm(x, u, *, c=1.0, keepdim=False, dim=-1): + r""" + Compute vector norm on the tangent space w.r.t Riemannian metric on the Poincare ball + + .. math:: + + \|u\|_x = \lambda^c_x \|u\|_2 + + Parameters + ---------- + x : tensor + point on the Poincare ball + u : tensor + tangent vector to :math:`x` on Poincare ball + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + norm of vector + """ + return _norm(x, u, c, keepdim=keepdim, dim=dim) + + +def _norm(x, u, c, keepdim: bool = False, dim: int = -1): + return _lambda_x(x, c, keepdim=keepdim, dim=dim) * u.norm( + dim=dim, keepdim=keepdim, p=2 + ) + + +def mobius_add(x, y, *, c=1.0, dim=-1): + r""" + Mobius addition is a special operation in a hyperbolic space. + + .. math:: + + x \oplus_c y = \frac{ + (1 + 2 c \langle x, y\rangle + c \|y\|^2_2) x + (1 - c \|x\|_2^2) y + }{ + 1 + 2 c \langle x, y\rangle + c^2 \|x\|^2_2 \|y\|^2_2 + } + + .. plot:: plots/extended/poincare/mobius_add.py + + In general this operation is not commutative: + + .. math:: + + x \oplus_c y \ne y \oplus_c x + + But in some cases this property holds: + + * zero vector case + + .. math:: + + \mathbf{0} \oplus_c x = x \oplus_c \mathbf{0} + + * zero negative curvature case that is same as Euclidean addition + + .. math:: + + x \oplus_0 y = y \oplus_0 x + + Another useful property is so called left-cancellation law: + + .. math:: + + (-x) \oplus_c (x \oplus_c y) = y + + Parameters + ---------- + x : tensor + point on the Poincare ball + y : tensor + point on the Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of mobius addition + """ + return _mobius_add(x, y, c, dim=dim) + + +def _mobius_add(x, y, c, dim=-1): + y = y + 1e-15 + x2 = x.pow(2).sum(dim=dim, keepdim=True) + y2 = y.pow(2).sum(dim=dim, keepdim=True) + xy = (x * y).sum(dim=dim, keepdim=True) + num = (1 + 2 * c * xy + c * y2) * x + (1 - c * x2) * y + denom = 1 + 2 * c * xy + c ** 2 * x2 * y2 + # avoid division by zero in this way + return num / (denom + 1e-15) + + +def mobius_sub(x, y, *, c=1.0, dim=-1): + r""" + Mobius substraction that can be represented via Mobius addition as follows: + + .. math:: + + x \ominus_c y = x \oplus_c (-y) + + Parameters + ---------- + x : tensor + point on Poincare ball + y : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of mobius substraction + """ + return _mobius_sub(x, y, c, dim=dim) + + +def _mobius_sub(x, y, c, dim: int = -1): + return _mobius_add(x, -y, c, dim=dim) + + +def mobius_coadd(x, y, *, c=1.0, dim=-1): + r""" + Mobius coaddition operation + + Addition operation :math:`\oplus_c` is neither associative, nor commutative. Coaddition, or cooperation in + Gyrogroup is an associative operation that is defined as follows. + + .. math:: + + a \boxplus_c b = b \boxplus_c a = a\operatorname{gyr}[a, -b]b\\ + = \frac{ + (1 + c \|y\|^2_2) x + (1 - c \|x\|_2^2) y + }{ + 1 + c^2 \|x\|^2_2 \|y\|^2_2 + }, + + where :math:`\operatorname{gyr}[a, b]c = \ominus_c (a \oplus b) \oplus_c (a \oplus_c (b \oplus_c c))` + + The following right cancellation property holds + + .. math:: + + (a \boxplus_c b) \ominus_c b = a\\ + (a \oplus_c b) \boxminus_c b = a + + Parameters + ---------- + x : tensor + point on Poincare ball + y : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of mobius coaddition + + """ + return _mobius_coadd(x, y, c, dim=dim) + + +def _mobius_coadd(x, y, c, dim: int = -1): + y = y + 1e-15 + x2 = x.pow(2).sum(dim=dim, keepdim=True) + y2 = y.pow(2).sum(dim=dim, keepdim=True) + num = (1 - c * y2) * x + (1 - c * x2) * y + denom = 1 - c ** 2 * x2 * y2 + # avoid division by zero in this way + return num / (denom + 1e-15) + + +def mobius_cosub(x, y, *, c=1.0, dim=-1): + """ + Mobius cosubstraction operation + + .. math:: + + a \boxminus_c b = a \boxplus_c -b + + Parameters + ---------- + x : tensor + point on Poincare ball + y : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of mobius coaddition + + """ + return _mobius_cosub(x, y, c, dim=dim) + + +def _mobius_cosub(x, y, c, dim: int = -1): + return _mobius_coadd(x, -y, c, dim=dim) + + +def mobius_scalar_mul(r, x, *, c=1.0, dim=-1): + r""" + Left scalar multiplication on the Poincare ball + + .. math:: + + r \otimes_c x = (1/\sqrt{c}) \tanh(r\tanh^{-1}(\sqrt{c}\|x\|_2))\frac{x}{\|x\|_2} + + This operation has properties similar to Euclidean + + * `n-addition` property + + .. math:: + + r \otimes_c x = x \oplus_c \dots \oplus_c x + + * Distributive property + + .. math:: + + (r_1 + r_2) \otimes_c x = r_1 \otimes_c x \oplus r_2 \otimes_c x + + * Scalar associativity + + .. math:: + + (r_1 r_2) \otimes_c x = r_1 \otimes_c (r_2 \otimes_c x) + + * Scaling property + + .. math:: + + |r| \otimes_c x / \|r \otimes_c x\|_2 = x/\|x\|_2 + + Parameters + ---------- + r : float|tensor + scalar for multiplication + x : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of mobius scalar multiplication + """ + return _mobius_scalar_mul(r, x, c, dim=dim) + + +def _mobius_scalar_mul(r, x, c, dim: int = -1): + x = x + 1e-15 + x_norm = x.norm(dim=dim, keepdim=True, p=2) + sqrt_c = c ** 0.5 + res_c = tanh(r * artanh(sqrt_c * x_norm)) * x / (x_norm * sqrt_c) + return res_c + + +def dist(x, y, *, c=1.0, keepdim=False, dim=-1): + r""" + Distance on the Poincare ball + + .. math:: + + d_c(x, y) = \frac{2}{\sqrt{c}}\tanh^{-1}(\sqrt{c}\|(-x)\oplus_c y\|_2) + + .. plot:: plots/extended/poincare/distance.py + + Parameters + ---------- + x : tensor + point on Poincare ball + y : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + geodesic distance between :math:`x` and :math:`y` + """ + return _dist(x, y, c, keepdim=keepdim, dim=dim) + + +def _dist(x, y, c, keepdim: bool = False, dim: int = -1): + sqrt_c = c ** 0.5 + dist_c = artanh( + sqrt_c * _mobius_add(-x, y, c, dim=dim).norm(dim=dim, p=2, keepdim=keepdim) + ) + return dist_c * 2 / sqrt_c + + +def dist0(x, *, c=1.0, keepdim=False, dim=-1): + r""" + Distance on the Poincare ball to zero + + Parameters + ---------- + x : tensor + point on Poincare ball + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension for operations + + Returns + ------- + tensor + geodesic distance between :math:`x` and :math:`0` + """ + return _dist0(x, c, keepdim=keepdim, dim=dim) + + +def _dist0(x, c, keepdim: bool = False, dim: int = -1): + sqrt_c = c ** 0.5 + dist_c = artanh(sqrt_c * x.norm(dim=dim, p=2, keepdim=keepdim)) + return dist_c * 2 / sqrt_c + + +def clip_tangent(x, u, *, c=1.0, dim=-1): + r""" + Project tangent vector to reasonable values that do not exceed + maximum allowed (vector norm allowing to travel to the opposite pole) + + .. math:: + + \operatorname{maxnorm}_x = d_{c}(\operatorname{proj}(-\infty), \operatorname{proj}(\infty)) / \lambda_x^c + + Parameters + ---------- + x : tensor + point on Poincare ball + u : tensor + tangent vector + c : float|tensor + ball negative curvature + dim : int + reduction dimension to compute norm + + Returns + ------- + tensor + same tangent vector with reasonable values + """ + return _clip_tangent(x, u, c, dim=dim) + + +def _clip_tangent(x, u, c, dim: int = -1): + # get the almost infinite vecotor estimate + # this is the norm of travel vector to the opposite pole + s = x.size(dim) + p = torch.ones((s,), dtype=x.dtype, device=x.device) + p = p / s ** 0.5 / (c ** 0.5) + p = _project(p, c, dim=dim) + # normalize its length based on x + maxnorm = _dist(p, -p, c, keepdim=True, dim=dim) / _lambda_x( + x, c, keepdim=True, dim=dim + ) + norm = u.norm(dim=dim, keepdim=True, p=2) + cond = norm > maxnorm + projected = u / norm * maxnorm + return torch.where(cond, projected, u) + + +def geodesic(t, x, y, *, c=1.0, dim=-1): + r""" + Geodesic (the shortest) path connecting :math:`x` and :math:`y`. + The path can be treated as and extension of a line segment between + points but in a Riemannian manifold. In Poincare ball model, the path + is expressed using Mobius addition and scalar multiplication: + + .. math:: + + \gamma_{x\to y}(t) = x \oplus_c r \otimes_c ((-x) \oplus_c y) + + The required properties of this path are the following: + + .. math:: + + \gamma_{x\to y}(0) = x\\ + \gamma_{x\to y}(1) = y\\ + \dot\gamma_{x\to y}(t) = v + + Moreover, as geodesic path is not only the shortest path connecting points and Poincare ball. + This definition also requires local distance minimization and thus another property appears: + + .. math:: + + d_c(\gamma_{x\to y}(t_1), \gamma_{x\to y}(t_2)) = v|t_1-t_2| + + "Natural parametrization" of the curve ensures unit speed geodesics which yields the above formula with :math:`v=1`. + However, for Poincare ball we can always compute the constant speed :math:`v` from the points + that particular path connects: + + .. math:: + + v = d_c(\gamma_{x\to y}(0), \gamma_{x\to y}(1)) = d_c(x, y) + + + Parameters + ---------- + t : float|tensor + travelling time + x : tensor + starting point on Poincare ball + y : tensor + target point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + point on the Poincare ball + """ + return _geodesic(t, x, y, c, dim=dim) + + +def _geodesic(t, x, y, c, dim: int = -1): + # this is not very numerically unstable + v = _mobius_add(-x, y, c, dim=dim) + tv = _mobius_scalar_mul(t, v, c, dim=dim) + gamma_t = _mobius_add(x, tv, c, dim=dim) + return gamma_t + + +def expmap(x, u, *, c=1.0, dim=-1): + r""" + Exponential map for Poincare ball model. This is tightly related with :func:`geodesic`. + Intuitively Exponential map is a smooth constant travelling from starting point :math:`x` with speed :math:`u`. + + A bit more formally this is travelling along curve :math:`\gamma_{x, u}(t)` such that + + .. math:: + + \gamma_{x, u}(0) = x\\ + \dot\gamma_{x, u}(0) = u\\ + \|\dot\gamma_{x, u}(t)\|_{\gamma_{x, u}(t)} = \|u\|_x + + The existence of this curve relies on uniqueness of differential equation solution, that is local. + For the Poincare ball model the solution is well defined globally and we have. + + .. math:: + + \operatorname{Exp}^c_x(u) = \gamma_{x, u}(1) = \\ + x\oplus_c \tanh(\sqrt{c}/2 \|u\|_x) \frac{u}{\sqrt{c}\|u\|_2} + + Parameters + ---------- + x : tensor + starting point on Poincare ball + u : tensor + speed vector on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + :math:`\gamma_{x, u}(1)` end point + """ + return _expmap(x, u, c, dim=dim) + + +def _expmap(x, u, c, dim: int = -1): + u += 1e-15 + sqrt_c = c ** 0.5 + u_norm = u.norm(dim=dim, p=2, keepdim=True) + second_term = ( + tanh(sqrt_c / 2 * _lambda_x(x, c, keepdim=True, dim=dim) * u_norm) + * u + / (sqrt_c * u_norm) + ) + gamma_1 = _mobius_add(x, second_term, c, dim=dim) + return gamma_1 + + +def expmap0(u, *, c=1.0, dim=-1): + r""" + Exponential map for Poincare ball model from :math:`0`. + + .. math:: + + \operatorname{Exp}^c_0(u) = \tanh(\sqrt{c}/2 \|u\|_2) \frac{u}{\sqrt{c}\|u\|_2} + + Parameters + ---------- + u : tensor + speed vector on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + :math:`\gamma_{0, u}(1)` end point + """ + return _expmap0(u, c, dim=dim) + + +def _expmap0(u, c, dim: int = -1): + u = u + 1e-15 + sqrt_c = c ** 0.5 + u_norm = u.norm(dim=dim, p=2, keepdim=True) + gamma_1 = tanh(sqrt_c * u_norm) * u / (sqrt_c * u_norm) + return gamma_1 + + +def geodesic_unit(t, x, u, *, c=1.0, dim=-1): + r""" + Unit speed geodesic starting from :math:`x` with direction :math:`u/\|u\|_x` + + .. math:: + + \gamma_{x,u}(t) = x\oplus_c \tanh(t\sqrt{c}/2) \frac{u}{\sqrt{c}\|u\|_2} + + Parameters + ---------- + t : tensor + travelling time + x : tensor + initial point + u : tensor + direction + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the point on geodesic line + """ + return _geodesic_unit(t, x, u, c, dim=dim) + + +def _geodesic_unit(t, x, u, c, dim: int = -1): + sqrt_c = c ** 0.5 + u_norm = u.norm(dim=dim, p=2, keepdim=True) + second_term = tanh(sqrt_c / 2 * t) * u / (sqrt_c * u_norm) + gamma_1 = _mobius_add(x, second_term, c, dim=dim) + return gamma_1 + + +def logmap(x, y, *, c=1.0, dim=-1): + r""" + Logarithmic map for two points :math:`x` and :math:`y` on the manifold. + + .. math:: + + \operatorname{Log}^c_x(y) = \frac{2}{\sqrt{c}\lambda_x^c} \tanh^{-1}( + \sqrt{c} \|(-x)\oplus_c y\|_2 + ) * \frac{(-x)\oplus_c y}{\|(-x)\oplus_c y\|_2} + + The result of Logarithmic map is a vector such that + + .. math:: + + y = \operatorname{Exp}^c_x(\operatorname{Log}^c_x(y)) + + + Parameters + ---------- + x : tensor + starting point on Poincare ball + y : tensor + target point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + tangent vector that transports :math:`x` to :math:`y` + """ + return _logmap(x, y, c, dim=dim) + + +def _logmap(x, y, c, dim: int = -1): + sub = _mobius_add(-x, y, c, dim=dim) + sub_norm = sub.norm(dim=dim, p=2, keepdim=True) + lam = _lambda_x(x, c, keepdim=True, dim=dim) + sqrt_c = c ** 0.5 + return 2 / sqrt_c / lam * artanh(sqrt_c * sub_norm) * sub / sub_norm + + +def logmap0(y, *, c=1.0, dim=-1): + r""" + Logarithmic map for :math:`y` from :math:`0` on the manifold. + + + .. math:: + + \operatorname{Log}^c_0(y) = \tanh^{-1}(\sqrt{c}\|y\|_2) \frac{y}{\|y\|_2} + + The result is such that + + .. math:: + + y = \operatorname{Exp}^c_0(\operatorname{Log}^c_0(y)) + + Parameters + ---------- + y : tensor + target point on Poincare ball + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + tangent vector that transports :math:`0` to :math:`y` + """ + return _logmap0(y, c, dim=dim) + + +def _logmap0(y, c, dim: int = -1): + sqrt_c = c ** 0.5 + y = y + 1e-15 + y_norm = y.norm(dim=dim, p=2, keepdim=True) + return y / y_norm / sqrt_c * artanh(sqrt_c * y_norm) + + +def mobius_matvec(m, x, *, c=1.0, dim=-1): + r""" + Generalization for matrix-vector multiplication to hyperbolic space defined as + + .. math:: + + M \otimes_c x = (1/\sqrt{c}) \tanh\left( + \frac{\|Mx\|_2}{\|x\|_2}\tanh^{-1}(\sqrt{c}\|x\|_2) + \right)\frac{Mx}{\|Mx\|_2} + + .. plot:: plots/extended/poincare/mobius_matvec.py + + Parameters + ---------- + m : tensor + matrix for multiplication. + Batched matmul is performed if ``m.dim() > 2``, but only last dim reduction is supported + x : tensor + point on Poincare ball + c : float|tensor + negative ball curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Mobius matvec result + """ + return _mobius_matvec(m, x, c, dim=dim) + + +def _mobius_matvec(m, x, c, dim: int = -1): + if m.dim() > 2 and dim != -1: + raise RuntimeError( + "broadcasted Mobius matvec is supported for the last dim only" + ) + x = x + 1e-15 + x_norm = x.norm(dim=dim, keepdim=True, p=2) + sqrt_c = c ** 0.5 + if dim != -1 or m.dim() == 2: + mx = torch.tensordot(x, m, dims=([dim], [1])) + else: + mx = torch.matmul(m, x.unsqueeze(-1)).squeeze(-1) + mx_norm = mx.norm(dim=dim, keepdim=True, p=2) + res_c = tanh(mx_norm / x_norm * artanh(sqrt_c * x_norm)) * mx / (mx_norm * sqrt_c) + cond = (mx == 0).prod(dim=dim, keepdim=True, dtype=torch.uint8) + res_0 = torch.zeros(1, dtype=res_c.dtype, device=res_c.device) + res = torch.where(cond, res_0, res_c) + return res + + +def mobius_pointwise_mul(w, x, *, c=1.0, dim=-1): + r""" + Generalization for point-wise multiplication to hyperbolic space defined as + + .. math:: + + \operatorname{diag}(w) \otimes_c x = (1/\sqrt{c}) \tanh\left( + \frac{\|\operatorname{diag}(w)x\|_2}{x}\tanh^{-1}(\sqrt{c}\|x\|_2) + \right)\frac{\|\operatorname{diag}(w)x\|_2}{\|x\|_2} + + + Parameters + ---------- + w : tensor + weights for multiplication (should be broadcastable to x) + x : tensor + point on Poincare ball + c : float|tensor + negative ball curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Mobius point-wise mul result + """ + return _mobius_pointwise_mul(w, x, c, dim=dim) + + +def _mobius_pointwise_mul(w, x, c, dim: int = -1): + x = x + 1e-15 + x_norm = x.norm(dim=dim, keepdim=True, p=2) + sqrt_c = c ** 0.5 + wx = w * x + wx_norm = wx.norm(dim=dim, keepdim=True, p=2) + res_c = tanh(wx_norm / x_norm * artanh(sqrt_c * x_norm)) * wx / (wx_norm * sqrt_c) + cond = (wx == 0).prod(dim=dim, keepdim=True, dtype=torch.uint8) + res_0 = torch.zeros(1, dtype=res_c.dtype, device=res_c.device) + res = torch.where(cond, res_0, res_c) + return res + + +def mobius_fn_apply_chain(x, *fns, c=1.0, dim=-1): + r""" + Generalization for functions in hyperbolic space. + First, hyperbolic vector is mapped to a Euclidean space via + :math:`\operatorname{Log}^c_0` and nonlinear function is applied in this tangent space. + The resulting vector is then mapped back with :math:`\operatorname{Exp}^c_0` + + .. math:: + + f^{\otimes_c}(x) = \operatorname{Exp}^c_0(f(\operatorname{Log}^c_0(y))) + + The definition of mobius function application allows chaining as + + .. math:: + + y = \operatorname{Exp}^c_0(\operatorname{Log}^c_0(y)) + + Resulting in + + .. math:: + + (f \circ g)^{\otimes_c}(x) = \operatorname{Exp}^c_0((f \circ g) (\operatorname{Log}^c_0(y))) + + Parameters + ---------- + x : tensor + point on Poincare ball + fns : callable[] + functions to apply + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Apply chain result + """ + if not fns: + return x + else: + ex = _logmap0(x, c, dim=dim) + for fn in fns: + ex = fn(ex) + y = _expmap0(ex, c, dim=dim) + return y + + +def mobius_fn_apply(fn, x, *args, c=1.0, dim=-1, **kwargs): + r""" + Generalization for functions in hyperbolic space. + First, hyperbolic vector is mapped to a Euclidean space via + :math:`\operatorname{Log}^c_0` and nonlinear function is applied in this tangent space. + The resulting vector is then mapped back with :math:`\operatorname{Exp}^c_0` + + .. math:: + + f^{\otimes_c}(x) = \operatorname{Exp}^c_0(f(\operatorname{Log}^c_0(y))) + + .. plot:: plots/extended/poincare/mobius_sigmoid_apply.py + + Parameters + ---------- + x : tensor + point on Poincare ball + fn : callable + function to apply + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Result of function in hyperbolic space + """ + ex = _logmap0(x, c, dim=dim) + ex = fn(ex, *args, **kwargs) + y = _expmap0(ex, c, dim=dim) + return y + + +def mobiusify(fn): + r""" + Wraps a function so that is works in hyperbolic space. New function will accept additional argument ``c`` + + Parameters + ---------- + fn : callable + function in Euclidean space, only its first argument is treated as hyperbolic + + Returns + ------- + callable + function working in hyperbolic space + """ + + @functools.wraps(fn) + def mobius_fn(x, *args, c=1.0, dim=-1, **kwargs): + ex = _logmap0(x, c, dim=dim) + ex = fn(ex, *args, **kwargs) + y = _expmap0(ex, c, dim=dim) + return y + + return mobius_fn + + +def dist2plane(x, p, a, *, c=1.0, keepdim=False, signed=False, dim=-1): + r""" + Distance from :math:`x` to a hyperbolic hyperplane in Poincare ball + that is orthogonal to :math:`a` and contains :math:`p`. + + .. plot:: plots/extended/poincare/distance2plane.py + + To form an intuition what is a hyperbolic hyperplane, let's first consider Euclidean hyperplane + + .. math:: + + H_{a, b} = \left\{ + x \in \mathbb{R}^n\;:\;\langle x, a\rangle - b = 0 + \right\}, + + where :math:`a\in \mathbb{R}^n\backslash \{\mathbf{0}\}` and :math:`b\in \mathbb{R}^n`. + + This formulation of a hyperplane is hard to generalize, + therefore we can rewrite :math:`\langle x, a\rangle - b` + utilizing orthogonal completion. + Setting any :math:`p` s.t. :math:`b=\langle a, p\rangle` we have + + .. math:: + + H_{a, b} = \left\{ + x \in \mathbb{R}^n\;:\;\langle x, a\rangle - b = 0 + \right\}\\ + =H_{a, \langle a, p\rangle} = \tilde{H}_{a, p}\\ + = \left\{ + x \in \mathbb{R}^n\;:\;\langle x, a\rangle - \langle a, p\rangle = 0 + \right\}\\ + =\left\{ + x \in \mathbb{R}^n\;:\;\langle -p + x, a\rangle = 0 + \right\}\\ + = p + \{a\}^\perp + + Naturally we have a set :math:`\{a\}^\perp` with applied :math:`+` operator to each element. + Generalizing a notion of summation to the hyperbolic space we replace :math:`+` with :math:`\oplus_c`. + + Next, we should figure out what is :math:`\{a\}^\perp` in the Poincare ball. + + First thing that we should acknowledge is that notion of orthogonality is defined for vectors in tangent spaces. + Let's consider now :math:`p\in \mathbb{D}_c^n` and :math:`a\in T_p\mathbb{D}_c^n\backslash \{\mathbf{0}\}`. + + Slightly deviating from traditional notation let's write :math:`\{a\}_p^\perp` + highlighting the tight relationship of :math:`a\in T_p\mathbb{D}_c^n\backslash \{\mathbf{0}\}` + with :math:`p \in \mathbb{D}_c^n`. We then define + + .. math:: + + \{a\}_p^\perp := \left\{ + z\in T_p\mathbb{D}_c^n \;:\; \langle z, a\rangle_p = 0 + \right\} + + Recalling that a tangent vector :math:`z` for point :math:`p` yields :math:`x = \operatorname{Exp}^c_p(z)` + we rewrite the above equation as + + .. math:: + \{a\}_p^\perp := \left\{ + x\in \mathbb{D}_c^n \;:\; \langle \operatorname{Log}_p^c(x), a\rangle_p = 0 + \right\} + + This formulation is something more pleasant to work with. + Putting all together + + .. math:: + + \tilde{H}_{a, p}^c = p + \{a\}^\perp_p\\ + = \left\{ + x \in \mathbb{D}_c^n\;:\;\langle\operatorname{Log}^c_p(x), a\rangle_p = 0 + \right\} \\ + = \left\{ + x \in \mathbb{D}_c^n\;:\;\langle -p \oplus_c x, a\rangle = 0 + \right\} + + To compute the distance :math:`d_c(x, \tilde{H}_{a, p}^c)` we find + + .. math:: + + d_c(x, \tilde{H}_{a, p}^c) = \inf_{w\in \tilde{H}_{a, p}^c} d_c(x, w)\\ + = \frac{1}{\sqrt{c}} \sinh^{-1}\left\{ + \frac{ + 2\sqrt{c} |\langle(-p)\oplus_c x, a\rangle| + }{ + (1-c\|(-p)\oplus_c x\|^2_2)\|a\|_2 + } + \right\} + + Parameters + ---------- + x : tensor + point on Poincare ball + a : tensor + vector on tangent space of :math:`p` + p : tensor + point on Poincare ball lying on the hyperplane + c : float|tensor + ball negative curvature + keepdim : bool + retain the last dim? (default: false) + signed : bool + return signed distance + dim : int + reduction dimension for operations + + Returns + ------- + tensor + distance to the hyperplane + """ + return _dist2plane(x, a, p, c, keepdim=keepdim, signed=signed, dim=dim) + + +def _dist2plane(x, a, p, c, keepdim: bool = False, signed: bool = False, dim: int = -1): + sqrt_c = c ** 0.5 + diff = _mobius_add(-p, x, c, dim=dim) + diff_norm2 = diff.pow(2).sum(dim=dim, keepdim=keepdim) + sc_diff_a = (diff * a).sum(dim=dim, keepdim=keepdim) + if not signed: + sc_diff_a = sc_diff_a.abs() + a_norm = a.norm(dim=dim, keepdim=keepdim, p=2) + num = 2 * sqrt_c * sc_diff_a + denom = (1 - c * diff_norm2) * a_norm + return arsinh(num / (denom + 1e-15)) / sqrt_c + + +def gyration(a, b, u, *, c=1.0, dim=-1): + r""" + Gyration is a special operation in hyperbolic geometry. + Addition operation :math:`\oplus_c` is not associative (as mentioned in :func:`mobius_add`), + but gyroassociative which means + + .. math:: + + u \oplus_c (v \oplus_c w) = (u\oplus_c v) \oplus_c \operatorname{gyr}[u, v]w, + + where + + .. math:: + + \operatorname{gyr}[u, v]w = \ominus (u \oplus_c v) \oplus (u \oplus_c (v \oplus_c w)) + + We can simplify this equation using explicit formula for Mobius addition [1]. Recall + + .. math:: + + A = - c^2 \langle u, w\rangle \langle v, v\rangle + c \langle v, w\rangle + + 2 c^2 \langle u, v\rangle \langle v, w\rangle\\ + B = - c^2 \langle v, w\rangle \langle u, u\rangle - c \langle u, w\rangle\\ + D = 1 + 2 c \langle u, v\rangle + c^2 \langle u, u\rangle \langle v, v\rangle\\ + + \operatorname{gyr}[u, v]w = w + 2 \frac{A u + B v}{D} + + Parameters + ---------- + a : tensor + first point on Poincare ball + b : tensor + second point on Poincare ball + u : tensor + vector field for operation + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of automorphism + + References + ---------- + [1] A. A. Ungar (2009), A Gyrovector Space Approach to Hyperbolic Geometry + """ + return _gyration(a, b, u, c, dim=dim) + + +def _gyration(u, v, w, c, dim: int = -1): + # non-simplified + # mupv = -_mobius_add(u, v, c) + # vpw = _mobius_add(u, w, c) + # upvpw = _mobius_add(u, vpw, c) + # return _mobius_add(mupv, upvpw, c) + # simplified + u2 = u.pow(2).sum(dim=dim, keepdim=True) + v2 = v.pow(2).sum(dim=dim, keepdim=True) + uv = (u * v).sum(dim=dim, keepdim=True) + uw = (u * w).sum(dim=dim, keepdim=True) + vw = (v * w).sum(dim=dim, keepdim=True) + c2 = c ** 2 + a = -c2 * uw * v2 + c * vw + 2 * c2 * uv * vw + b = -c2 * vw * u2 - c * uw + d = 1 + 2 * c * uv + c2 * u2 * v2 + return w + 2 * (a * u + b * v) / (d + 1e-15) + + +def parallel_transport(x, y, v, *, c=1.0, dim=-1): + r""" + Parallel transport is essential for adaptive algorithms in Riemannian manifolds. + For Hyperbolic spaces parallel transport is expressed via gyration. + + .. plot:: plots/extended/poincare/gyrovector_parallel_transport.py + + To recover parallel transport we first need to study isomorphism between gyrovectors and vectors. + The reason is that originally, parallel transport is well defined for gyrovectors as + + .. math:: + + P_{x\to y}(z) = \operatorname{gyr}[y, -x]z, + + where :math:`x,\:y,\:z \in \mathbb{D}_c^n` and + :math:`\operatorname{gyr}[a, b]c = \ominus (a \oplus_c b) \oplus_c (a \oplus_c (b \oplus_c c))` + + But we want to obtain parallel transport for vectors, not for gyrovectors. + The blessing is isomorphism mentioned above. This mapping is given by + + .. math:: + + U^c_p \: : \: T_p\mathbb{D}_c^n \to \mathbb{G} = v \mapsto \lambda^c_p v + + + Finally, having points :math:`x,\:y \in \mathbb{D}_c^n` and a tangent vector :math:`u\in T_x\mathbb{D}_c^n` we obtain + + .. math:: + + P^c_{x\to y}(v) = (U^c_y)^{-1}\left(\operatorname{gyr}[y, -x] U^c_x(v)\right)\\ + = \operatorname{gyr}[y, -x] v \lambda^c_x / \lambda^c_y + + .. plot:: plots/extended/poincare/parallel_transport.py + + + Parameters + ---------- + x : tensor + starting point + y : tensor + end point + v : tensor + tangent vector to be transported + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + transported vector + """ + return _parallel_transport(x, y, v, c, dim=dim) + + +def _parallel_transport(x, y, u, c, dim: int = -1): + return ( + _gyration(y, -x, u, c, dim=dim) + * _lambda_x(x, c, keepdim=True, dim=dim) + / _lambda_x(y, c, keepdim=True, dim=dim) + ) + + +def parallel_transport0(y, v, *, c=1.0, dim=-1): + r""" + Special case parallel transport with starting point at zero that + can be computed more efficiently and numerically stable + + Parameters + ---------- + y : tensor + target point + v : tensor + vector to be transported + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + """ + return _parallel_transport0(y, v, c, dim=dim) + + +def _parallel_transport0(y, v, c, dim: int = -1): + return v * (1 - c * y.pow(2).sum(dim=dim, keepdim=True)) + + +def egrad2rgrad(x, grad, *, c=1.0, dim=-1): + r""" + Translate Euclidean gradient to Riemannian gradient on tangent space of :math:`x` + + .. math:: + + \nabla_x = \nabla^E_x / (\lambda_x^c)^2 + + Parameters + ---------- + x : tensor + point on the Poincare ball + grad : tensor + Euclidean gradient for :math:`x` + c : float|tensor + ball negative curvature + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Riemannian gradient :math:`u\in T_x\mathbb{D}_c^n` + """ + return _egrad2rgrad(x, grad, c, dim=dim) + + +def _egrad2rgrad(x, grad, c, dim: int = -1): + return grad / _lambda_x(x, c, keepdim=True, dim=dim) ** 2 diff --git a/geoopt/manifolds/sphere.py b/geoopt/manifolds/sphere.py index 5c3741d1..2795d37e 100644 --- a/geoopt/manifolds/sphere.py +++ b/geoopt/manifolds/sphere.py @@ -38,14 +38,14 @@ def _check_point_on_manifold(self, x, atol=1e-5, rtol=1e-5): return True, None def _check_vector_on_tangent(self, x, u, atol=1e-5, rtol=1e-5): - inner = self._inner(None, x, u) + inner = self._inner(None, x, u, keepdim=True) ok = torch.allclose(inner, inner.new((1,)).fill_(0), atol=atol, rtol=rtol) if not ok: return False, "` != 0` with atol={}, rtol={}".format(atol, rtol) return True, None - def _inner(self, x, u, v): - return (u * v).sum(-1) + def _inner(self, x, u, v, keepdim): + return (u * v).sum(-1, keepdim=keepdim) def _projx(self, x): return x / x.norm(dim=-1, keepdim=True) @@ -88,13 +88,13 @@ def _expmap_transp(self, x, v, *more, u, t): def _logmap(self, x, y): u = self._proju(x, y - x) - dist = self._dist(x, y).unsqueeze(-1) + dist = self._dist(x, y, keepdim=True) # If the two points are "far apart", correct the norm. cond = dist.gt(1e-6) return torch.where(cond, u * dist / u.norm(dim=-1, keepdim=True), u) - def _dist(self, x, y): - inner = self._inner(None, x, y).clamp(-1, 1) + def _dist(self, x, y, keepdim): + inner = self._inner(None, x, y, keepdim=keepdim).clamp(-1, 1) return torch.acos(inner) diff --git a/geoopt/manifolds/stiefel.py b/geoopt/manifolds/stiefel.py index 6bfa5c64..a6100ba6 100644 --- a/geoopt/manifolds/stiefel.py +++ b/geoopt/manifolds/stiefel.py @@ -89,7 +89,7 @@ class CanonicalStiefel(Stiefel): name = "Stiefel(canonical)" reversible = True - def _inner(self, x, u, v): + def _inner(self, x, u, v, keepdim): # _x = tr(u^T(I-1/2xx^T)v) # = tr(u^T(v-1/2xx^Tv)) # = tr(u^Tv-1/2u^Txx^Tv) @@ -102,7 +102,9 @@ def _inner(self, x, u, v): v = u else: xtv = x.transpose(-1, -2) @ v - return (u * v).sum([-1, -2]) - 0.5 * (xtv * xtu).sum([-1, -2]) + return (u * v).sum([-1, -2], keepdim=keepdim) - 0.5 * (xtv * xtu).sum( + [-1, -2], keepdim=keepdim + ) # we do faster on inner without autofill _inner_autofill = False @@ -112,6 +114,7 @@ def _transp_follow_one(self, x, v, *, u, t): rhs = v + t / 2 * a @ v lhs = -t / 2 * a lhs[..., torch.arange(a.shape[-2]), torch.arange(x.shape[-2])] += 1 + # TODO: torch.gesv -> torch.solve after pytorch release qv, _ = torch.gesv(rhs, lhs) return qv @@ -182,8 +185,8 @@ def _retr_transp(self, x, v, *more, u, t): else: return y, vs - def _inner(self, x, u, v): - return (u * v).sum([-1, -2]) + def _inner(self, x, u, v, keepdim): + return (u * v).sum([-1, -2], keepdim=keepdim) def _retr(self, x, u, t): q, r = linalg.batch_linalg.qr(x + u * t) diff --git a/geoopt/optim/radam.py b/geoopt/optim/radam.py index b669d6ad..b6a6d98a 100644 --- a/geoopt/optim/radam.py +++ b/geoopt/optim/radam.py @@ -83,17 +83,10 @@ def step(self, closure=None): # Exponential moving average of gradient values state["exp_avg"] = torch.zeros_like(p) # Exponential moving average of squared gradient values - inner_prod_shape = p.shape - if manifold.ndim > 0: - inner_prod_shape = inner_prod_shape[: -manifold.ndim] - state["exp_avg_sq"] = torch.zeros( - inner_prod_shape, dtype=p.dtype, device=p.device - ) + state["exp_avg_sq"] = torch.zeros_like(p) if amsgrad: # Maintains max of all exp. moving avg. of sq. grad. values - state["max_exp_avg_sq"] = torch.zeros( - inner_prod_shape, dtype=p.dtype, device=p.device - ) + state["max_exp_avg_sq"] = torch.zeros_like(p) # this is assumed to be already transported if "traced_step" not in state: @@ -174,7 +167,9 @@ def perform_step( grad.add_(weight_decay, point) grad = manifold.egrad2rgrad(point, grad) exp_avg.mul_(betas[0]).add_(1 - betas[0], grad) - exp_avg_sq.mul_(betas[1]).add_(1 - betas[1], manifold.inner(point, grad)) + exp_avg_sq.mul_(betas[1]).add_( + 1 - betas[1], manifold.inner(point, grad, keepdim=True) + ) if amsgrad: # Maintains the maximum of all 2nd moment running avg. till now torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq) @@ -182,7 +177,6 @@ def perform_step( denom = max_exp_avg_sq.sqrt().add_(eps) else: denom = exp_avg_sq.sqrt().add_(eps) - denom = manifold.broadcast_scalar(denom) step.add_(1) bias_correction1 = 1 - betas[0] ** step.type_as(betas) bias_correction2 = 1 - betas[1] ** step.type_as(betas) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5d33904c..ee440468 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,3 +7,4 @@ pymanopt twine wheel sphinx +seaborn diff --git a/setup.py b/setup.py index fe8f2fc0..f1a55f78 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ DESCRIPTION = """Unofficial implementation for “Riemannian Adaptive Optimization Methods” ICLR2019 and more""" PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__)) -with open(os.path.join(PROJECT_ROOT, 'README.rst'), encoding='utf-8') as buff: +with open(os.path.join(PROJECT_ROOT, "README.rst"), encoding="utf-8") as buff: LONG_DESCRIPTION = buff.read() @@ -44,5 +44,5 @@ def get_version(*path): url="https://github.com/geoopt/geoopt", python_requires=">=3.6.0", license="Apache License, Version 2.0", - classifiers=classifiers + classifiers=classifiers, ) diff --git a/tests/test_adam.py b/tests/test_adam.py index 318b2879..96a3159e 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -30,3 +30,24 @@ def closure(): np.testing.assert_allclose(X.data, Xstar, atol=1e-5, rtol=1e-5) optim.load_state_dict(optim.state_dict()) optim.step(closure) + + +def test_adam_poincare(): + torch.manual_seed(44) + ideal = torch.tensor([0.5, 0.5]) + start = torch.randn(2) / 2 + start = geoopt.manifolds.poincare.math.expmap0(start, c=1.0) + start = geoopt.ManifoldParameter(start, manifold=geoopt.PoincareBall()) + + def closure(): + optim.zero_grad() + loss = geoopt.manifolds.poincare.math.dist(start, ideal) ** 2 + loss.backward() + return loss.item() + + optim = geoopt.optim.RiemannianAdam([start], lr=1e-2) + + for _ in range(2000): + optim.step(closure) + + np.testing.assert_allclose(start.data, ideal, atol=1e-5, rtol=1e-5) diff --git a/tests/test_manifold.py b/tests/test_manifold.py index a10c8d06..4d3b6ec8 100644 --- a/tests/test_manifold.py +++ b/tests/test_manifold.py @@ -26,6 +26,7 @@ def t(request): # match implementation of pymanopt for stiefel functools.partial(geoopt.manifolds.Stiefel, canonical=False), functools.partial(geoopt.manifolds.Stiefel, canonical=True), + geoopt.manifolds.PoincareBall, geoopt.manifolds.Euclidean, geoopt.manifolds.Sphere, functools.partial( @@ -41,7 +42,7 @@ def t(request): def manifold(request, retraction_order): man = request.param() try: - return man.set_default_order(retraction_order) + return man.set_default_order(retraction_order).double() except ValueError: pytest.skip("not supported retraction order for {}".format(man)) @@ -63,6 +64,7 @@ def manifold(request, retraction_order): # shapes to verify unary element implementation shapes = { + geoopt.manifolds.PoincareBall: (3,), geoopt.manifolds.EuclideanStiefel: (10, 5), geoopt.manifolds.CanonicalStiefel: (10, 5), geoopt.manifolds.Euclidean: (1,), @@ -79,11 +81,17 @@ def manifold(request, retraction_order): @pytest.fixture() def unary_case(manifold): shape = shapes[type(manifold)] - manopt_manifold = mannopt[type(manifold)](*shape) np.random.seed(42) - rand = manopt_manifold.rand().astype("float64") - x = geoopt.ManifoldTensor(torch.from_numpy(rand), manifold=manifold) torch.manual_seed(43) + if type(manifold) in mannopt: + manopt_manifold = mannopt[type(manifold)](*shape) + rand = manopt_manifold.rand().astype("float64") + x = geoopt.ManifoldTensor(torch.from_numpy(rand), manifold=manifold) + else: + manopt_manifold = None + x = geoopt.ManifoldTensor( + torch.randn(shape, dtype=torch.float64) * 0.1, manifold=manifold + ) ex = geoopt.ManifoldTensor(torch.randn_like(x), manifold=manifold) v = x.proju(torch.randn_like(x)) ev = torch.randn_like(x) @@ -105,6 +113,8 @@ def test_projection_via_assert(unary_case): def test_vector_projection(unary_case): if isinstance(unary_case.manifold, geoopt.manifolds.CanonicalStiefel): pytest.skip("pymanopt uses euclidean Stiefel") + elif unary_case.manopt_manifold is None: + pytest.skip("pymanopt does not have {}".format(unary_case.manifold)) x = unary_case.x ev = unary_case.ev @@ -124,6 +134,8 @@ def test_vector_projection_via_assert(unary_case): def test_retraction(unary_case, retraction_order, t): + if unary_case.manopt_manifold is None: + pytest.skip("pymanopt does not have {}".format(unary_case.manifold)) if isinstance(unary_case.manifold, geoopt.manifolds.CanonicalStiefel): pytest.skip("pymanopt uses euclidean Stiefel") x = unary_case.x @@ -139,6 +151,8 @@ def test_retraction(unary_case, retraction_order, t): def test_transport(unary_case, t): + if unary_case.manopt_manifold is None: + pytest.skip("pymanopt does not have {}".format(unary_case.manifold)) if isinstance(unary_case.manifold, geoopt.manifolds.CanonicalStiefel): pytest.skip("pymanopt uses euclidean Stiefel") x = unary_case.x @@ -261,24 +275,28 @@ def test_broadcast_retr_transp_many(unary_case, t): def test_reversibility(unary_case, t): - torch.manual_seed(43) - X = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) - U = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) - X = unary_case.manifold.projx(X) - U = unary_case.manifold.proju(X, U) - Z, Q = unary_case.manifold.retr_transp(X, U, u=U, t=t) - X1, U1 = unary_case.manifold.retr_transp(Z, Q, u=Q, t=-t) if unary_case.manifold.reversible: + torch.manual_seed(43) + X = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) + U = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) + X = unary_case.manifold.projx(X) + U = unary_case.manifold.proju(X, U) + Z, Q = unary_case.manifold.retr_transp(X, U, u=U, t=t) + X1, U1 = unary_case.manifold.retr_transp(Z, Q, u=Q, t=-t) + np.testing.assert_allclose(X1, X, atol=1e-5) np.testing.assert_allclose(U1, U, atol=1e-5) else: - assert not np.allclose(X1, X, atol=1e-5) - assert not np.allclose(U1, U, atol=1e-5) + pytest.skip("The manifold {} is not supposed to be checked") def test_dist(unary_case): if type(unary_case.manifold)._dist is geoopt.manifolds.base.not_implemented: - pytest.skip("logmap is not implemented for {}".format(unary_case.manifold)) + pytest.skip("dist is not implemented for {}".format(unary_case.manifold)) + if unary_case.manopt_manifold is None: + pytest.skip( + "dist is not implemented for pymanopt {}".format(unary_case.manifold) + ) torch.manual_seed(43) x = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) y = torch.randn(*unary_case.shape, dtype=unary_case.x.dtype) @@ -295,12 +313,16 @@ def test_logmap(unary_case, t): x = unary_case.x v = unary_case.v - y = unary_case.manopt_manifold.exp(x.numpy(), v.numpy() * t) - vman = unary_case.manopt_manifold.log(x.numpy(), y) - vhat = unary_case.manifold.logmap(x, torch.as_tensor(y)) - np.testing.assert_allclose(vhat, vman) + if unary_case.manopt_manifold is not None: + y = unary_case.manopt_manifold.exp(x.numpy(), v.numpy() * t) + vman = unary_case.manopt_manifold.log(x.numpy(), y) + vhat = unary_case.manifold.logmap(x, torch.as_tensor(y)) + np.testing.assert_allclose(vhat, vman, atol=1e-7) + else: + y = unary_case.manifold.expmap(x, v) + vhat = unary_case.manifold.logmap(x, torch.as_tensor(y)) ey = unary_case.manifold.expmap(x, vhat) - np.testing.assert_allclose(y, ey) + np.testing.assert_allclose(y, ey, atol=1e-7) def test_logmap_many(unary_case, t): @@ -317,4 +339,4 @@ def test_logmap_many(unary_case, t): Uh = unary_case.manifold.logmap(X, Y) Yh = unary_case.manifold.expmap(X, Uh) - np.testing.assert_allclose(Yh, Y) + np.testing.assert_allclose(Yh, Y, atol=1e-7) diff --git a/tests/test_poincare_math.py b/tests/test_poincare_math.py new file mode 100644 index 00000000..31780120 --- /dev/null +++ b/tests/test_poincare_math.py @@ -0,0 +1,371 @@ +""" +Tests ideas are taken mostly from https://github.com/dalab/hyperbolic_nn/blob/master/util.py with some changes +""" +import torch +import random +import numpy as np +import pytest +from geoopt.manifolds import poincare + + +@pytest.fixture("function", autouse=True, params=range(30, 40)) +def seed(request): + seed = request.param + torch.manual_seed(seed) + random.seed(seed) + return seed + + +@pytest.fixture("function", params=[torch.float64, torch.float32]) +def dtype(request): + return request.param + + +@pytest.fixture +def c(seed, dtype): + # test broadcasted and non broadcasted versions + if seed == 30: + c = torch.tensor(0.0).to(dtype) + elif seed == 35: + c = torch.zeros(100, 1, dtype=dtype) + elif seed > 35: + c = torch.rand(100, 1, dtype=dtype) + else: + c = torch.tensor(random.random()).to(dtype) + return c + 1e-10 + + +@pytest.fixture +def a(seed, c): + if seed in {30, 35}: + a = torch.randn(100, 10, dtype=c.dtype) + elif seed > 35: + # do not check numerically unstable regions + # I've manually observed small differences there + a = torch.empty(100, 10, dtype=c.dtype).normal_(-1, 1) + a /= a.norm(dim=-1, keepdim=True) * 1.3 + a *= (torch.rand_like(c) * c) ** 0.5 + else: + a = torch.empty(100, 10, dtype=c.dtype).normal_(-1, 1) + a /= a.norm(dim=-1, keepdim=True) * 1.3 + a *= random.uniform(0, c) ** 0.5 + return poincare.math.project(a, c=c) + + +@pytest.fixture +def b(seed, c): + if seed in {30, 35}: + b = torch.randn(100, 10, dtype=c.dtype) + elif seed > 35: + b = torch.empty(100, 10, dtype=c.dtype).normal_(-1, 1) + b /= b.norm(dim=-1, keepdim=True) * 1.3 + b *= (torch.rand_like(c) * c) ** 0.5 + else: + b = torch.empty(100, 10, dtype=c.dtype).normal_(-1, 1) + b /= b.norm(dim=-1, keepdim=True) * 1.3 + b *= random.uniform(0, c) ** 0.5 + return poincare.math.project(b, c=c) + + +def test_mobius_addition_left_cancelation(a, b, c): + res = poincare.math.mobius_add(-a, poincare.math.mobius_add(a, b, c=c), c=c) + tolerance = {torch.float32: dict(atol=1e-6, rtol=1e-6), torch.float64: dict()} + np.testing.assert_allclose(res, b, **tolerance[c.dtype]) + + +def test_mobius_addition_zero_a(b, c): + a = torch.zeros(100, 10, dtype=c.dtype) + res = poincare.math.mobius_add(a, b, c=c) + np.testing.assert_allclose(res, b) + + +def test_mobius_addition_zero_b(a, c): + b = torch.zeros(100, 10, dtype=c.dtype) + res = poincare.math.mobius_add(a, b, c=c) + np.testing.assert_allclose(res, a) + + +def test_mobius_addition_negative_cancellation(a, c): + res = poincare.math.mobius_add(a, -a, c=c) + tolerance = { + torch.float32: dict(atol=1e-7, rtol=1e-6), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(res, torch.zeros_like(res), **tolerance[c.dtype]) + + +def test_mobius_negative_addition(a, b, c): + res = poincare.math.mobius_add(-b, -a, c=c) + res1 = -poincare.math.mobius_add(b, a, c=c) + tolerance = { + torch.float32: dict(atol=1e-7, rtol=1e-6), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(res, res1, **tolerance[c.dtype]) + + +@pytest.mark.parametrize("n", list(range(5))) +def test_n_additions_via_scalar_multiplication(n, a, c): + y = torch.zeros_like(a) + for _ in range(n): + y = poincare.math.mobius_add(a, y, c=c) + ny = poincare.math.mobius_scalar_mul(n, a, c=c) + tolerance = { + torch.float32: dict(atol=1e-7, rtol=1e-6), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(y, ny, **tolerance[c.dtype]) + + +@pytest.fixture +def r1(seed, dtype): + if seed % 3 == 0: + return random.uniform(-1, 1) + else: + return torch.rand(100, 1, dtype=dtype) * 2 - 1 + + +@pytest.fixture +def r2(seed, dtype): + if seed % 3 == 1: + return random.uniform(-1, 1) + else: + return torch.rand(100, 1, dtype=dtype) * 2 - 1 + + +def test_scalar_multiplication_distributive(a, c, r1, r2): + res = poincare.math.mobius_scalar_mul(r1 + r2, a, c=c) + res1 = poincare.math.mobius_add( + poincare.math.mobius_scalar_mul(r1, a, c=c), + poincare.math.mobius_scalar_mul(r2, a, c=c), + c=c, + ) + res2 = poincare.math.mobius_add( + poincare.math.mobius_scalar_mul(r1, a, c=c), + poincare.math.mobius_scalar_mul(r2, a, c=c), + c=c, + ) + tolerance = { + torch.float32: dict(atol=1e-6, rtol=1e-7), + torch.float64: dict(atol=1e-7, rtol=1e-10), + } + np.testing.assert_allclose(res1, res, **tolerance[c.dtype]) + np.testing.assert_allclose(res2, res, **tolerance[c.dtype]) + + +def test_scalar_multiplication_associative(a, c, r1, r2): + res = poincare.math.mobius_scalar_mul(r1 * r2, a, c=c) + res1 = poincare.math.mobius_scalar_mul( + r1, poincare.math.mobius_scalar_mul(r2, a, c=c), c=c + ) + res2 = poincare.math.mobius_scalar_mul( + r2, poincare.math.mobius_scalar_mul(r1, a, c=c), c=c + ) + tolerance = { + torch.float32: dict(atol=1e-7, rtol=1e-6), # worked with rtol=1e-7 locally + torch.float64: dict(atol=1e-7, rtol=1e-10), + } + np.testing.assert_allclose(res1, res, **tolerance[c.dtype]) + np.testing.assert_allclose(res2, res, **tolerance[c.dtype]) + + +def test_scaling_property(a, c, r1): + x1 = a / a.norm(dim=-1, keepdim=True) + ra = poincare.math.mobius_scalar_mul(r1, a, c=c) + x2 = poincare.math.mobius_scalar_mul(abs(r1), a, c=c) / ra.norm( + dim=-1, keepdim=True + ) + tolerance = { + torch.float32: dict(rtol=1e-5, atol=1e-6), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(x1, x2, **tolerance[c.dtype]) + + +def test_geodesic_borders(a, b, c): + geo0 = poincare.math.geodesic(0.0, a, b, c=c) + geo1 = poincare.math.geodesic(1.0, a, b, c=c) + tolerance = { + torch.float32: dict(rtol=1e-5, atol=1e-6), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(geo0, a, **tolerance[c.dtype]) + np.testing.assert_allclose(geo1, b, **tolerance[c.dtype]) + + +def test_geodesic_segment_length_property(a, b, c): + extra_dims = len(a.shape) + segments = 12 + t = torch.linspace(0, 1, segments + 1, dtype=c.dtype).view( + (segments + 1,) + (1,) * extra_dims + ) + gamma_ab_t = poincare.math.geodesic(t, a, b, c=c) + gamma_ab_t0 = gamma_ab_t[:-1] + gamma_ab_t1 = gamma_ab_t[1:] + dist_ab_t0mt1 = poincare.math.dist(gamma_ab_t0, gamma_ab_t1, c=c, keepdim=True) + speed = ( + poincare.math.dist(a, b, c=c, keepdim=True) + .unsqueeze(0) + .expand_as(dist_ab_t0mt1) + ) + # we have exactly 12 line segments + tolerance = {torch.float32: dict(rtol=1e-5), torch.float64: dict(atol=1e-10)} + np.testing.assert_allclose(dist_ab_t0mt1, speed / segments, **tolerance[c.dtype]) + + +def test_geodesic_segement_unit_property(a, b, c): + extra_dims = len(a.shape) + segments = 12 + t = torch.linspace(0, 1, segments + 1, dtype=c.dtype).view( + (segments + 1,) + (1,) * extra_dims + ) + gamma_ab_t = poincare.math.geodesic_unit(t, a, b, c=c) + gamma_ab_t0 = gamma_ab_t[:1] + gamma_ab_t1 = gamma_ab_t + dist_ab_t0mt1 = poincare.math.dist(gamma_ab_t0, gamma_ab_t1, c=c, keepdim=True) + true_distance_travelled = t.expand_as(dist_ab_t0mt1) + # we have exactly 12 line segments + tolerance = { + torch.float32: dict(atol=1e-6, rtol=1e-5), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose( + dist_ab_t0mt1, true_distance_travelled, **tolerance[c.dtype] + ) + + +def test_expmap_logmap(a, b, c): + # this test appears to be numerical unstable once a and b may appear on the opposite sides + bh = poincare.math.expmap(x=a, u=poincare.math.logmap(a, b, c=c), c=c) + tolerance = {torch.float32: dict(rtol=1e-5, atol=1e-6), torch.float64: dict()} + np.testing.assert_allclose(bh, b, **tolerance[c.dtype]) + + +def test_expmap0_logmap0(a, c): + # this test appears to be numerical unstable once a and b may appear on the opposite sides + v = poincare.math.logmap0(a, c=c) + norm = poincare.math.norm(torch.zeros_like(v), v, c=c, keepdim=True) + dist = poincare.math.dist0(a, c=c, keepdim=True) + bh = poincare.math.expmap0(v, c=c) + tolerance = {torch.float32: dict(rtol=1e-6), torch.float64: dict()} + np.testing.assert_allclose(bh, a, **tolerance[c.dtype]) + np.testing.assert_allclose(norm, dist, **tolerance[c.dtype]) + + +def test_matvec_zeros(a, c): + mat = a.new_zeros(3, a.shape[-1]) + z = poincare.math.mobius_matvec(mat, a, c=c) + np.testing.assert_allclose(z, 0.0) + + +def test_matvec_via_equiv_fn_apply(a, c): + mat = a.new(3, a.shape[-1]).normal_() + y = poincare.math.mobius_fn_apply(lambda x: x @ mat.transpose(-1, -2), a, c=c) + y1 = poincare.math.mobius_matvec(mat, a, c=c) + tolerance = {torch.float32: dict(atol=1e-5), torch.float64: dict()} + np.testing.assert_allclose(y, y1, **tolerance[c.dtype]) + + +def test_mobiusify(a, c): + mat = a.new(3, a.shape[-1]).normal_() + + @poincare.math.mobiusify + def matvec(x): + return x @ mat.transpose(-1, -2) + + y = matvec(a, c=c) + y1 = poincare.math.mobius_matvec(mat, a, c=c) + tolerance = {torch.float32: dict(atol=1e-5), torch.float64: dict()} + np.testing.assert_allclose(y, y1, **tolerance[c.dtype]) + + +def test_matvec_chain_via_equiv_fn_apply(a, c): + mat1 = a.new(a.shape[-1], a.shape[-1]).normal_() + mat2 = a.new(a.shape[-1], a.shape[-1]).normal_() + y = poincare.math.mobius_fn_apply_chain( + a, + lambda x: x @ mat1.transpose(-1, -2), + lambda x: x @ mat2.transpose(-1, -2), + c=c, + ) + y1 = poincare.math.mobius_matvec(mat1, a, c=c) + y1 = poincare.math.mobius_matvec(mat2, y1, c=c) + np.testing.assert_allclose(y, y1, atol=1e-5) + + +def test_parallel_transport0_preserves_inner_products(a, c): + # pointing to the center + v_0 = torch.rand_like(a) + 1e-5 + u_0 = torch.rand_like(a) + 1e-5 + zero = torch.zeros_like(a) + v_a = poincare.math.parallel_transport0(a, v_0, c=c) + u_a = poincare.math.parallel_transport0(a, u_0, c=c) + # compute norms + vu_0 = poincare.math.inner(zero, v_0, u_0, c=c, keepdim=True) + vu_a = poincare.math.inner(a, v_a, u_a, c=c, keepdim=True) + np.testing.assert_allclose(vu_a, vu_0, atol=1e-6, rtol=1e-6) + + +def test_parallel_transport0_is_same_as_usual(a, c): + # pointing to the center + v_0 = torch.rand_like(a) + 1e-5 + zero = torch.zeros_like(a) + v_a = poincare.math.parallel_transport0(a, v_0, c=c) + v_a1 = poincare.math.parallel_transport(zero, a, v_0, c=c) + # compute norms + np.testing.assert_allclose(v_a, v_a1, atol=1e-6, rtol=1e-6) + + +def test_parallel_transport_a_b(a, b, c): + # pointing to the center + v_0 = torch.rand_like(a) + u_0 = torch.rand_like(a) + v_1 = poincare.math.parallel_transport(a, b, v_0, c=c) + u_1 = poincare.math.parallel_transport(a, b, u_0, c=c) + # compute norms + vu_1 = poincare.math.inner(b, v_1, u_1, c=c, keepdim=True) + vu_0 = poincare.math.inner(a, v_0, u_0, c=c, keepdim=True) + np.testing.assert_allclose(vu_0, vu_1, atol=1e-6, rtol=1e-6) + + +def test_add_infinity_and_beyond(a, b, c): + infty = b * 10000000 + infty = poincare.math.clip_tangent(a, infty, c=c) + for i in range(100): + z = poincare.math.expmap(a, infty, c=c) + z = poincare.math.project(z, c=c) + z = poincare.math.mobius_scalar_mul(1000.0, z, c=c) + z = poincare.math.project(z, c=c) + infty = poincare.math.parallel_transport(a, z, infty, c=c) + assert np.isfinite(z).all(), (i, z) + assert np.isfinite(infty).all(), (i, infty) + a = z + z = poincare.math.expmap(a, -infty, c=c) + # they just need to be very far, exact answer is not supposed + tolerance = { + torch.float32: dict(rtol=3e-1, atol=2e-1), + torch.float64: dict(rtol=1e-1, atol=1e-3), + } + np.testing.assert_allclose(z, -a, **tolerance[c.dtype]) + + +def test_mobius_coadd(a, b, c): + # (a \boxplus_c b) \ominus_c b = a + ah = poincare.math.mobius_sub(poincare.math.mobius_coadd(a, b, c=c), b, c=c) + np.testing.assert_allclose(ah, a, atol=1e-5) + + +def test_mobius_cosub(a, b, c): + # (a \oplus_c b) \boxminus b = a + ah = poincare.math.mobius_cosub(poincare.math.mobius_add(a, b, c=c), b, c=c) + np.testing.assert_allclose(ah, a, atol=1e-5) + + +def test_distance2plane(a, c): + v = torch.rand_like(a) + vr = v / poincare.math.norm(a, v, c=c, keepdim=True) + z = poincare.math.expmap(a, vr, c=c) + dist1 = poincare.math.dist(a, z, c=c) + dist = poincare.math.dist2plane(z, a, vr, c=c) + + np.testing.assert_allclose(dist, dist1, atol=1e-5, rtol=1e-5) diff --git a/tests/test_rhmc.py b/tests/test_rhmc.py index 3fb8bdd1..2ce01c8f 100644 --- a/tests/test_rhmc.py +++ b/tests/test_rhmc.py @@ -6,6 +6,15 @@ import geoopt.samplers.rhmc +@pytest.fixture(autouse=True) +def withdtype(): + torch.set_default_dtype(torch.float64) + try: + yield + finally: + torch.set_default_dtype(torch.float32) + + @pytest.mark.parametrize( "params", [ @@ -20,8 +29,6 @@ ], ) def test_leapfrog_reversibility(params): - torch.set_default_dtype(torch.float64) - class NormalDist(torch.nn.Module): def __init__(self, mu, sigma): super().__init__() diff --git a/tests/test_utils.py b/tests/test_utils.py index e335ad82..da936667 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -106,3 +106,14 @@ def test_manifold_is_submodule(): container = torch.nn.ModuleDict({"sphere": sub_sphere}) container.to(torch.float64) assert sub_sphere._projector.dtype == torch.float64 + + +def test_manifold_is_submodule_poincare(): + print(torch.get_default_dtype()) + c = torch.tensor(1.0) + ball = geoopt.manifolds.PoincareBall(c) + assert ball.c.dtype == torch.float32 + ball.to(torch.float64) + container = torch.nn.ModuleDict({"ball": ball}) + container.to(torch.float64) + assert ball.c.dtype == torch.float64