From d7c115c7b4bbd8089a460535ee005a9f105788d6 Mon Sep 17 00:00:00 2001 From: Maxim Kochurov Date: Thu, 23 Apr 2020 20:14:38 +0300 Subject: [PATCH] K-Stereographic (revised) (#126) * broadcastable implemnentation for *_k * projection and lambda x * default argument * default argument is zero * inner * norm * add more refactoring * tests for negative kappa * typo * fix tests * remove nans from k=0 * add tests for positive curvature, warn about numerics * implement poincare ball via subclassing * docs * change to softplus * refactor tests * fix typo * docs * plots * pytorch version * readme * read the docs update * weighted midpoint * typo * fix docs for weighted midpoint * scriptable funcitons * zero derivative for kappa * name typo * docs * script midpoint * update docs build * fix typo * proper shit * abs via sign * fix tests * add zero * refactor midpoint * add some test for weighted midpoint * test midpoint * fix doc * fix test & numerics * antipode * jit midpoint * remove todo comment * clam min in another place * test kappa -1,...,-1e-15,0,-1e-15,...,1 * skip seed * check numerics * check grads are not nan * fix docs, by @rrkarim * another maxnorm * clamp in projection * add more assertions * black * add nan check * add debug assertions for random travis fails * this is stupid * fix infs * fix infs * maxnorm 1e15 * atol * atol * add emacs to gitignore * add broadcasting for weighted midpoint * list to tuple in make tuple * add scaled distance to hyperplane * scaling distance to hyperplane * revert scaling * ill conditioned midpoint fix * fix typo * agree smth * check numerics for at least kappa=1 Co-authored-by: Maxim Kochurov Co-authored-by: Andreas Bloch --- .gitignore | 1 + README.rst | 5 +- docs/extended.rst | 2 +- docs/extended/poincare.rst | 116 - docs/extended/stereographic.rst | 147 ++ docs/manifolds.rst | 2 +- docs/plots/extended/poincare/distance.py | 27 - .../plots/extended/poincare/distance2plane.py | 35 - .../poincare/gyrovector_parallel_transport.py | 52 - .../poincare/hyperboloid_projection.png | Bin 8922 -> 0 bytes docs/plots/extended/poincare/klein_tiling.png | Bin 48738 -> 0 bytes .../extended/poincare/parallel_transport.py | 40 - .../extended/poincare/poincare_lines.gif | Bin 48849 -> 0 bytes docs/plots/extended/stereographic/distance.py | 137 ++ .../extended/stereographic/distance2plane.py | 139 ++ .../grid-of-geodesics-K--1.0.png | Bin 0 -> 41467 bytes .../stereographic/grid-of-geodesics-K-1.0.png | Bin 0 -> 49133 bytes .../gyrovector_parallel_transport.py | 182 ++ .../stereographic/hyperboloid-sproj.png | Bin 0 -> 64339 bytes .../{poincare => stereographic}/mobius_add.py | 13 +- .../mobius_matvec.py | 24 +- .../mobius_sigmoid_apply.py | 12 +- .../stereographic/parallel_transport.py | 170 ++ .../extended/stereographic/sphere-sproj.png | Bin 0 -> 34348 bytes geoopt/__init__.py | 4 + geoopt/manifolds/__init__.py | 11 +- geoopt/manifolds/poincare/math.py | 1359 ----------- geoopt/manifolds/stereographic/__init__.py | 10 + .../__init__.py => stereographic/manifold.py} | 329 ++- geoopt/manifolds/stereographic/math.py | 2004 +++++++++++++++++ geoopt/utils.py | 91 +- scripts/install_nix_win.sh | 2 +- setup.py | 2 +- tests/test_adam.py | 7 +- tests/test_gyrovector_math.py | 685 ++++++ tests/test_manifold_basic.py | 40 + tests/test_poincare_math.py | 371 --- tests/test_scaling.py | 4 +- 38 files changed, 3930 insertions(+), 2093 deletions(-) delete mode 100644 docs/extended/poincare.rst create mode 100644 docs/extended/stereographic.rst delete mode 100644 docs/plots/extended/poincare/distance.py delete mode 100644 docs/plots/extended/poincare/distance2plane.py delete mode 100644 docs/plots/extended/poincare/gyrovector_parallel_transport.py delete mode 100644 docs/plots/extended/poincare/hyperboloid_projection.png delete mode 100644 docs/plots/extended/poincare/klein_tiling.png delete mode 100644 docs/plots/extended/poincare/parallel_transport.py delete mode 100644 docs/plots/extended/poincare/poincare_lines.gif create mode 100644 docs/plots/extended/stereographic/distance.py create mode 100644 docs/plots/extended/stereographic/distance2plane.py create mode 100644 docs/plots/extended/stereographic/grid-of-geodesics-K--1.0.png create mode 100644 docs/plots/extended/stereographic/grid-of-geodesics-K-1.0.png create mode 100644 docs/plots/extended/stereographic/gyrovector_parallel_transport.py create mode 100644 docs/plots/extended/stereographic/hyperboloid-sproj.png rename docs/plots/extended/{poincare => stereographic}/mobius_add.py (73%) rename docs/plots/extended/{poincare => stereographic}/mobius_matvec.py (59%) rename docs/plots/extended/{poincare => stereographic}/mobius_sigmoid_apply.py (72%) create mode 100644 docs/plots/extended/stereographic/parallel_transport.py create mode 100644 docs/plots/extended/stereographic/sphere-sproj.png delete mode 100644 geoopt/manifolds/poincare/math.py create mode 100644 geoopt/manifolds/stereographic/__init__.py rename geoopt/manifolds/{poincare/__init__.py => stereographic/manifold.py} (50%) create mode 100644 geoopt/manifolds/stereographic/math.py create mode 100644 tests/test_gyrovector_math.py delete mode 100644 tests/test_poincare_math.py diff --git a/.gitignore b/.gitignore index 7f7c55aa..a4623166 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,4 @@ cfg/ data/ notebooks/ testing-report.html +*.el diff --git a/README.rst b/README.rst index f63e7106..fceb9887 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,9 @@ Now, pypi is behind master as we actively develop and implement new features. PyTorch Support ~~~~~~~~~~~~~~~ -Geoopt supports 2 latest stable versions of pytorch upstream or the latest major release. +Geoopt officially supports 2 latest stable versions of pytorch upstream or the latest major release. We also test against the nightly build, but do not be 100% sure about compatibility. +As for older pytorch versions, you may use it on your own risk. What is done so far ------------------- @@ -76,6 +77,8 @@ Manifolds ``A in R^{n x p} : A^t A=I``, ``n >= p`` - ``geoopt.Sphere`` - Sphere manifold ``||x||=1`` - ``geoopt.BirkhoffPolytope`` - manifold of Doubly Stochastic matrices +- ``geoopt.Stereographic`` - Constant curvature stereographic projection model +- ``geoopt.SphereProjection`` - Sphere stereographic projection model - ``geoopt.PoincareBall`` - Poincare ball model (`wiki `_) - ``geoopt.ProductManifold`` - Product manifold constructor - ``geoopt.Scaled`` - Scaled version of the manifold. Similar to `Learning Mixed-Curvature Representations in Product Spaces `_ if combined with ``ProductManifold`` diff --git a/docs/extended.rst b/docs/extended.rst index e2d1b35d..c545dad6 100644 --- a/docs/extended.rst +++ b/docs/extended.rst @@ -4,4 +4,4 @@ Extended Guide .. toctree:: :maxdepth: 1 - extended/poincare + extended/stereographic diff --git a/docs/extended/poincare.rst b/docs/extended/poincare.rst deleted file mode 100644 index dbba6761..00000000 --- a/docs/extended/poincare.rst +++ /dev/null @@ -1,116 +0,0 @@ -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 diff --git a/docs/extended/stereographic.rst b/docs/extended/stereographic.rst new file mode 100644 index 00000000..c25c048c --- /dev/null +++ b/docs/extended/stereographic.rst @@ -0,0 +1,147 @@ +:math:`\kappa`-Stereographic Projection model +============================================= + +Stereographic projection models comes to bind constant curvature spaces. Such as spheres, +hyperboloids and regular Euclidean manifold. Let's look at what does this mean. As we mentioned +constant curvature, let's name this constant :math:`\kappa`. + +Hyperbolic spaces +----------------- + +Hyperbolic space is a constant negative curvature (:math:`\kappa < 0`) Riemannian manifold. +(A very simple example of Riemannian manifold with constant, but positive curvature is sphere, we'll be back to it later) + +An (N+1)-dimensional hyperboloid spans the manifold that can be embedded into N-dimensional space via projections. + +.. figure:: ../plots/extended/stereographic/hyperboloid-sproj.png + :width: 300 + +Originally, the distance between points on the hyperboloid is defined as + +.. math:: + + d(x, y) = \operatorname{arccosh}(x, y) + +Not to work with this manifold, it is convenient to project the hyperboloid onto a plane. We can do it in many ways +recovering embedded manifolds with different properties (usually numerical). To connect constant curvature +manifolds we better use Poincare ball model (aka stereographic projection model). + +Poincare Model +~~~~~~~~~~~~~~ + +.. figure:: ../plots/extended/stereographic/grid-of-geodesics-K--1.0.png + :width: 300 + + Grid of Geodesics for :math:`\kappa=-1`, credits to `Andreas Bloch`_ + +First of all we note, that Poincare ball is embedded in a Sphere of radius :math:`r=1/\sqrt{\kappa}`, +where c is negative curvature. We also note, as :math:`\kappa` goes to :math:`0`, we recover infinite radius ball. +We should expect this limiting behaviour recovers Euclidean geometry. + +Spherical Spaces +---------------- +Another case of constant curvature manifolds is sphere. Unlike Hyperboloid this manifold is compact and has +positive :math:`\kappa`. But still we can embed a sphere onto a plane ignoring one of the poles. + +.. figure:: ../plots/extended/stereographic/sphere-sproj.png + :width: 300 + +Once we project sphere on the plane we have the following geodesics + +.. figure:: ../plots/extended/stereographic/grid-of-geodesics-K-1.0.png + :width: 300 + + Grid of Geodesics for :math:`\kappa=1`, credits to `Andreas Bloch`_ + + +Again, similarly to Poincare ball case, we have Euclidean geometry limiting :math:`\kappa` to :math:`0`. + +Universal Curvature Manifold +---------------------------- +To connect Euclidean space with its embedded manifold we need to get :math:`g^\kappa_x`. +It is done via `conformal factor` :math:`\lambda^\kappa_x`. Note, that the metric tensor is conformal, +which means all angles between tangent vectors are remained the same compared to what we +calculate ignoring manifold structure. + +The functions for the mathematics in gyrovector spaces are taken from the +following resources: + + [1] Ganea, Octavian, Gary Bécigneul, and Thomas Hofmann. "Hyperbolic + neural networks." Advances in neural information processing systems. + 2018. + [2] Bachmann, Gregor, Gary Bécigneul, and Octavian-Eugen Ganea. "Constant + Curvature Graph Convolutional Networks." arXiv preprint + arXiv:1911.05076 (2019). + [3] Skopek, Ondrej, Octavian-Eugen Ganea, and Gary Bécigneul. + "Mixed-curvature Variational Autoencoders." arXiv preprint + arXiv:1911.08411 (2019). + [4] Ungar, Abraham A. Analytic hyperbolic geometry: Mathematical + foundations and applications. World Scientific, 2005. + [5] Albert, Ungar Abraham. Barycentric calculus in Euclidean and + hyperbolic geometry: A comparative introduction. World Scientific, + 2010. + +.. autofunction:: geoopt.manifolds.stereographic.math.lambda_x + + +:math:`\lambda^\kappa_x` connects Euclidean inner product with Riemannian one + +.. autofunction:: geoopt.manifolds.stereographic.math.inner +.. autofunction:: geoopt.manifolds.stereographic.math.norm +.. autofunction:: geoopt.manifolds.stereographic.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.stereographic.math.mobius_add +.. autofunction:: geoopt.manifolds.stereographic.math.gyration + +Using this math, it is possible to define another useful operations + +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_sub +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_scalar_mul +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_pointwise_mul +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_matvec +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_fn_apply +.. autofunction:: geoopt.manifolds.stereographic.math.mobius_fn_apply_chain + +Manifold +-------- +Now we are ready to proceed with studying distances, geodesics, exponential maps and more + +.. autofunction:: geoopt.manifolds.stereographic.math.dist +.. autofunction:: geoopt.manifolds.stereographic.math.dist2plane +.. autofunction:: geoopt.manifolds.stereographic.math.parallel_transport +.. autofunction:: geoopt.manifolds.stereographic.math.geodesic +.. autofunction:: geoopt.manifolds.stereographic.math.geodesic_unit +.. autofunction:: geoopt.manifolds.stereographic.math.expmap +.. autofunction:: geoopt.manifolds.stereographic.math.expmap0 +.. autofunction:: geoopt.manifolds.stereographic.math.logmap +.. autofunction:: geoopt.manifolds.stereographic.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.stereographic.math.project + + +.. _Andreas Bloch: https://andbloch.github.io/K-Stereographic-Model + diff --git a/docs/manifolds.rst b/docs/manifolds.rst index d8ea25ae..7d1ce303 100644 --- a/docs/manifolds.rst +++ b/docs/manifolds.rst @@ -7,6 +7,6 @@ Manifolds All manifolds share same API. Some manifols may have several implementations of retraction operation, every implementation has a corresponding class. .. automodule:: geoopt.manifolds - :members: Euclidean, Stiefel, CanonicalStiefel, EuclideanStiefel, EuclideanStiefelExact, Sphere, SphereExact, PoincareBall, PoincareBallExact, Scaled, ProductManifold + :members: Euclidean, Stiefel, CanonicalStiefel, EuclideanStiefel, EuclideanStiefelExact, Sphere, SphereExact, Stereographic, StereographicExact, PoincareBall, PoincareBallExact, SphereProjection, SphereProjectionExact, Scaled, ProductManifold diff --git a/docs/plots/extended/poincare/distance.py b/docs/plots/extended/poincare/distance.py deleted file mode 100644 index 33408101..00000000 --- a/docs/plots/extended/poincare/distance.py +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 66cbc8fd..00000000 --- a/docs/plots/extended/poincare/distance2plane.py +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 894f6c94..00000000 --- a/docs/plots/extended/poincare/gyrovector_parallel_transport.py +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index df4843c5a839fbebdd763665d1d7713217743594..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/docs/plots/extended/poincare/parallel_transport.py b/docs/plots/extended/poincare/parallel_transport.py deleted file mode 100644 index be624c52..00000000 --- a/docs/plots/extended/poincare/parallel_transport.py +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index da427047779ffabd2126c5bd33faf1f0299308a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48849 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

IZUTor&p+JMbh%^`=-~ zUx4-G%my-TPJ~Q@D{Nd^bw0_2Z?o){2poABDA0zydK6r7XgCzZ`HiCfJ0Fb&7+RxL zyVYUohAsO?Y7zUGk5m!wRtlQ?FSqF&Bl21?Mx9NhTWEJMTc`9!7tvfioUK-hkBwO7|;6#&$vxk62j7TilnD4UYr9N z6KAgwj}L9R^uFBoK!}`CD{T5FA6qx3y5&2A(3<=-vSpmh%6Q{O0G=2K z#<8g)qkLL&crLi%QR5u1Qhp=FTc~N#1k)HA9JZaKiXL^|vZ8g0&t-glPg3u`#>((d z>|W4HSn4QT*-&!eh)!*(fn=Ly!%B?CAtHhIp!KO$-m`4Wig zDBXkR+HOBIGg*uiSFK!wd4Vv>hf9?MYQW_3(i}rN1WCR zgOM-*cOC0t2fj%`mi99Mk=Zqo#*TTIc6EVqf4WdJZ?R1q(X809{NtcDarPmVQLe|` z08WexyVG#paIJw1~K_#KQ(}T%-$S6U&ao@a+_}ZT_QAfg%F%!@W zANukjNfEG-XT#X2KL$4BwQ9?$<=(2)nl`sm)-)g88suHY~9heL~viv9%tr7Ovx->6}3@_0JrC?-`m; zIW@CiqX}eNx98DI05=2&;kU%hL*UC_2P->WPO$l2Yo^B_u}MuA*}w-MjG z?H(k9+z&tmbzb>4cqAcB_&|>IVfHRd2L(-$iaTMsMy=8ls2u(h%z`czdS83_?ZV;x zU{4#jhhA_e`X%hNzw7tb+yFnHI!FZI(abI@`bz$=(A^J|-#pP`8mj5Wt}sybH1!Q? zs3>-Tz^kb~Isp!?g`76`cRqs*ri;8}5Fa_BKq>5vmgwEu!j4b>$ zG2qzIvg@Vvt5AI2s7{>OHoy5jx-=S3FNX6&>vsg zkOr~~)C{r%XMRBYH1P+YT#`bYqNNaq8j=f?lXHfqYs;eQ^jAnB&TPn}Hqx$#e?E`2 zLJmd%&T-Dvq)wO%Zv>V~Up*_uvmrjT()1Z|pqr+6#oBxv;}g#VW+gL8ui^fX_Mzw72}y5*XN6MoJMe z@AM03W&ZPm6Y+!F0|v2$j>etT>^@~bwQ_kCIdatVi9Ond8rI+EcYt^Hq9+DC&yVI; z*d@U02L<8WoBO`&7i)_GKtBzQ<8 zX$oh|T?%TF8oHnelb#3;&yIG+X5e3-AqSo-g;)=<_Atseq99gkhzm;ir>SNx8jv zge~d9x0uP>l4vICMkUH%>k(9Nlh*d-n%k+%?)5NS1`$s-R+FSX>tu%xy$5-Mjy28e z^z0C%J7{*lw|XT+W_H5-E>#e1#Jh>*iE5DpO1SNPj0<1fc(y!R2C#g5C? zJmGs3^PNzw)u-F;oP;0oPS^C{#V#-tfvwjK*{ad(>Nd4v#{s@WyywhUA-b>cb+n0dCp3YJym~dInT|{%SxzZz3|SN_+EqJ3 zET$GdK4|IK(nJ`hoq#6NAQVYAT+qkt~!x~+P5^NY)D zu(&)C5DEKl3^ofMUB%gI!OtEM5O3Y6;|YFUtyZmVv9hV4O<~=i>03y9jt9`1zKIu>g6Y|w>wL70*m`f9XdVlcrE*cfw%yd)BD11 z6OKuzM#BIRi7EIP)!2R2t%Ol;J66x9&Z1D*S^zBD8 zC)C-05`BUfmhDi&_R0mUVQk?N_cm1a+IgVNGP?irjXum*$jftdb}nV4Pt%1Zj+K{g zR9<~5%9?&zmY!7_M=K5I2U?K)@HoZ^Z1~l_vh`N?#VWBsjF=~Fz26_3w+HQfM3~rz zi0rY`VneK&)_v7)LE8kUe?i?}R^hl2NWJ8NXo^zRl|4KM3i4aQlOlZP*R9&kacpVB zIS5!`0-_E0*ay01;I+;z7xY|KPs@=I9q^i^@>l?D%WCP}3!#wS7^Gz%>OodTVKHe! zfA*z4%Iut-lY6A_!>OR;U`?xQ$J_kulcW|}DCQN{a&`QfJ!SVE`504wn%iMk2c=a! z@}l@TfOl`fO$y7GSe6FH(pP;cUU`7{NE-^ZDYa60kNOtGJB4aYMa!S;^<7@A%}@AV zpP*;HuBt$Cb$cuI(1*LqN~L>sbUy|`dlud^wT}0pXIR5InNMWh@jHs**!bqmQ=H?o z?=KXcy`Y|3hmhw+C(P858k`BWKW+$KT)6V!%DDlTtezxh zGA{G*eh@`wnWG}EMq|K5$v^5CHBIBJ8L>mpoA-a}YIVxrJ~p_aOmCLM{lf&&^RD+e zW&qge32$Y}D}5gKwoC>=T)5I>8F%wq(3}0>5%02)-x3!*#)`SQpN%k7R-)Muu9q%D z3cVb^1b3$Ts()hbnr3&=Ppou!cw-5f)YFJ2L0g5N$=UFoZfNU0)I#0|nG~u&Lff&R zN)88~SO_%qFvN1JN_^nT_WmOT^4@rm^XJ`TpW;sDQz$C&Mr!%Z0Hj-|RxXp`74v0Z zAVuW=Y>gclOFeu{4o78c7%zOeEl2tOQ}q*jYUW`VUN?g`Kbh88wy_T$L24edz-Mn2 z==HrLN`C0k@W#w&7p8gLs)_YCF8XL!hkQ49D$%4+?+)ifeWu)vxlku5q-u-EODbA* z{dryNPs%;9KU)~8Id^OU{)5tZM87T?yt0|>8kyz0sPn4iC)w;Xsv}10?}Wx#qLpn6 zL1ePaV%`iZpJJDY#4=#a$W>WwzvtSnu1`zuRz)+g{e^-bq^zrR|$M~TQT_0WR4z0u#%W7{Qr@cv+; zS>@*tH1>V=w*L5ty}bBlTA4O`Yo#1|Dg(8=BJ+NMiz@A( zU;oNzwBL7~mKJkkb(%7gL3Qg1^Ye%BS(^^YjCdbFBgLDXmDj}a?n@R^hfjS@DgdC& zPVv_X(?2oACA)R&n*`C);2rIPIQcdy5BLKqQhA;CZ{sN%5Q;nfLmqXCfVE>U&}@2r z-IIz~8?c+Cik1Y=%_?=^i)$`=7wZ4jsb!eoDCzw|+HHo;EQ+TT9~eiiU~T9Jl^Hx; zM0uC48?N}~bpPi+Ra)NTPTkmKjdJAg&EW6b5n!Smas8SXot=~mBF(IUAC4T zcV+(N5nTnc^{egXB`%xZxphW%xLuWR-%ylxQp2qV!&~aLTOYh#s&5=GXWCr!qRdIj z!aFlg9`d2&?v2Is$4RfXpWm9zZEHdoSv?uFS|6|yEWE@RntTUev)^bs>8$-H_ym;q zDc`ixY4?NuStqtQZhF${ZQBaR`Ftd$x=NRmcY8Zg5q)#7 z@1;3XG&k&3(6OCfjp(Dz=i5%4kOt~L#*WIv#y4Xhxe1I}5u6B3oImAd(85CGz&_2# zFH1?&L<(yE#V5tPQO!HnAhnx0;{v$h&#}Lr^61zEY-7GBSgT}uV14fy9PI$8{@Hvp z={>rKZaLxiH>3K!>F>-R`NVu=X+vP3k-8X@Sti~@a6TiCJnxc$$J0>10M`_lpvEeh4WaQpDBwe2=J| zm0r#5Pd2!b4i*-?rjj?TN}zY=Sgs|<`jh?B+h0WHa;%At1eWuKfC-jBtjCNI{(Kmr zsISN?vZ1Upv9+l)@%z~)!c*H~X3KNJIr+Do=BEO3SffDe##LDA_$`GlV<2M{=Y#P- z6}GHo!l4%L5nN{LCGx*37L+ccn7aQ4ipfbuQirQpnX{Hh8tCO+=RmIT-{_NL(op-X zemYF&bevezLsz|K>B;hZ+C6r2K54KoV3v4C2YL&-uGWE*no{)Y#nU3}(c0!8f1IN_ zR!1X?j77lP<<61UB4Vf{W#A+=&)kochwW>=NBJa&>kO%oK}s^bjZ_;{LN;1zM3}bN zBxh!U(5SFET2e(gFdZY8zH5IO+F-fHXaaI89W6^-Cnv2~9#xO1z5r zr`vuUBee5g?lKTO0262SuOn*MWHPoLk5N%BsW3aNCf@y_>)ys)CMnl zw|Q2e^T|UbMg4-jRKIhG^y3H}CzY4F(Xwj66#^qceE?uZ5;|~&OGb@Ar>~lXnw7X5R^zoaLnqPnWnCJeBngHo7SuK=7dvJ7j{orEnWClpf~$WA7^vjs$hNZ~!P)w&F+S z!B$e%6PFUZ@_BfcW|*|%q?LvHfe#jZwE*{z7A^l@x2(rQG_cv-HP(|f>nMHd-Ke@^ z_k221r9acAUC4%R{Fk&+mnzUGq{G~#LNk~a``4jBK;u`rCo%6T!!%vTF9_nwN$vNN zwD0Wy{wh%Waozpo3M1#3&8xU$&t3fe<;x?}cNd+(EJl6JZh80+qL!FvgI?;k$OEX) z_y{*8189@NsoE_-Asc~CI-;~~>e;uE1V&pgH}m6@5mr=)W`HHslxc~#qi-2NE08Dg zV_@Jw7N?UKdy)&HHVA{OoR(PJ7P9sg<&U{X1(y1voI;)LYS?~VMliq;U|IJ5TE^G* z`!1^3IVuS9q5Q9Nvw)kZ+1S(9_ats0Sl#Ad!;OEB+Yr5uUe~tFH4~3itDVx zj^B2gEBI`+%bxHPWMwe7b9sNl&67hD>HUizrP!d+yOH@JR{pVUnkgGmDi8L3iNH1E zo2>FTT@3p0<=qb|c)D|?^O{BJmxQ)j@OASJk^>d$ur#QVOZ6O(GGLxfP_x{Hg$hXz z2>ao!*-w^u$LN`_zZVCt*@GRo>w_b|ZCbx#5A0G-6R%+euPCjNI}TW{iT#Nk8GF{QA>6^r!PbvPJ>Awm zdl0{iY0s`A20Q%OFfUssiUJX?1R7iJ3&Gtgv zBa=g66z*c#?9caU$U zx`4sLo|9jb0y1@KsIK*`_!Q*;cD}4Ok$)E2*14lV-HS1}rg;a_1JdEn6vfu=?6#G9 zD=QeQ&)tF$ACNMH;_w?w8DfVHIXIk7uB!g~T>z!e_NPioD9EY5dgSj;y#(Sa2_WY@ zbXaO-q&Ulan75r9Vpo4?JcYA^sxd}*;Nj3?i*-onRhcLdG-)k{!JK*pte*<7 z=i#9X;0n4D_B|v}CL(somw^T!2ypInf8BW##+dwLJPhZlxrQTHFi|cizKH9rAxQ!< z9`5S3klXeAi$z-M~GtR;+CB}6-mtfXrCP=x~;F-vHvcHp8=Pkc*| z0eNpzMv}K~Ttt8V5vaKX=SBB-NQ=qJ0U0R$t@ko3e_>s&QP%MZQDT6W6RdU~P)Gut{KYg0#DJ!{3S+2eL^fH~aH`OKZ<#MgIQLMD znr{v;hPDDq+c2;-Yn`$1+1hc?*GuV^-w&;8nD{E#v-4c-HXl`C7sO>TC_=`NN!WlC zGoUmx4NrT1N#yN-_v+xcUxK=K`J0UL;pbq`F}kHh8ZWtrfX(eRMs9pKw3M^Q`<#$; zhP;Xj!c!q04^p#bgAckIV=uLz>TaCsBi`r24RV%_7;)NAR6bn7FYBqM&>mp_uFNh7 z^ymY&of&H`g=mk1v;oE4@la&A9P6N$z)8b!6n04y0df&&V#uXYvZyfMCus}fip;nz zf0oIyM2PEjdond(4W*PMe_aB8xW9o|*%OWDYpB;K#8a^r+v*}0 zJ?DbAQ$=n~bAd2vj(+^6+#?Q4RqY-Z+QAhAMzxk9mlex8UM5pRHKQh~6t7l{JzdLq zsTK4bbUCen9}JI!^~lCnfC7*l1!BL_$p&JrOb=`tM359gj>cKWHxTn{7ENYK`uTAw zFb{{o&mTjOQcmsj8@^X<=DX(XAKlyF0Q%HiW9DXKs+R>AvP`{{;Lg1gU-p~j%7Nryv9FY&{Z1NbfzWC?ZXmc- z(g3oizB^71&{bM589B<=s{EV0f>O@R3N~!8@6rj1j6e6OvUPuOjsvJZR_tvfL>2NI z!God!jJGB6X(()Kg}h5EsM_QPWO4m^S-|MW5KMzz*3t^rb#`5N3J7<<_GxxA#cl?>cpI}~z!jV><7;F3tMD|Q|?VX`;zg*MzBr~Kd(x!o&p8<2i@ zYH%4-v0M!Ek}=S3vsGS%SAL}qEd(gg8q16QJWd~MLHHPp^la=aBS3f?NVI3rh9tX{ z=gN-yHyfNPBUZJ}yM21(@QJl!RsYx~e>IjM0d{>!Kinmx%sV!Gk=Ot?AuxsMgx<^X z;86@bY%V8DC4U_hZSRAx%pXTc6wA1;S)hQnqftkvy1dgw8z@2Zdu!eJpRKe`vmV4F z`|Zg$!P^`SO{XD_Hr)ja_i$C4{TEhacnU~|{DS+9;mCVQ{>x0|Q`=hJJX*Sp@QU%* zEIj^)v6r+cEOV@(OzplpH~VR6aCuhLYEZI4Yxb%R(0_=aB~zzh3mxq~wEMI_&=0bn zw_R@xwNMG(9>vd~=-b6zXT-G)w?5;I+V!7d4M^(jBoV3V)vvG{2pbAD%Y3NKK#P`V z3l?1u`>fS!yQV>|ab=1h&%fp`wqcT8i37kxFhK%=nxVX6;4V8-M3)s<=?&1~dwYA9 zsVR{r{Q4SBB_a*&mjp%qwolm0CsGHN^CELqpTH~@wm`T|@bz1D`+cwSFOKs1guuy9 zI(;#aVFi%@bZ0xHleth=iNXm4H76->g>-<{0B3g@XdPNg1HJ{x#ADsD=oX09fnk4y z!162^+8{UG4FxO8EM57;_ar|X$fr~J20zxVE|eQx@4hEBNNxJt|G@lJe98kYDtC74 z`GI>G>yiB+JOI{KG00`lXNtJd$^mc7O~LHJXD%CC zIWSU93)IJXFJFY!k*3V9esc8bJP?ChzNW_9<7qjiYp7C79PJf%lm&JV&D9-~b99F6 zhHk7W9`1x!{P27cKoUVu98Ru!dy4e!_#dIZ&xX~Y4cYIp9uofqqn{;#-$9U`qb`Q_ zhvIF`gGwHcIAv)Nlnl+R*+0jf*5(BS+!8!98CtObZoJ!3!~+xh6tP>6g2wny(_nrQ zJG|*=VXa_OL8xYz|JE0ko=S*LPRC}mE{8PdM~Vynnw{3pgUk2ASLPJ0Kldq|I2Bh2 zGEfb~?&Q)p@kvckFR6eVplwPDuw3c~S;hro66vKYZaCBLeq3hg$YaEuScC2$mxiWq zFYZOv1Y@mXF3gmZN$z|^U-JGk-zKPQjF3n|TspqrNNDs;sn{0=*6HADKQ9lLGRoPf zgSz~Y4)osVr9Vk&0CA_wON5y5#CJZ()eoowh+?56EqSxapQTTtzY1#2h~ ze1jXtZ_bawirtwC`e|~>*mQ5?`5`r-=v|VATL}eIe9f2he)WeV02i^d`2x#Y|1xTW z@4a?m->o+0&vS1Y!lHf&B`#2pIgl}tj-<6fh4h57d!TDT^;CayDkCh;kHkbS~nN!0fTo`Njd>dlad7(Cut4)OY_)4^`D6*CYtj(rK;4~ z=jv~Ex@YcAqBdRy3Q~q$b_33PLb|UbJ+>uCVG1V%E~&L^{b(dEZBUoq~S!v~+Cj-uLCon!-w|Dphrh zdlgd5*P=(B7#lJHY)OH5KzOENpEV&o1M1a+cdG-=ON|B;o~{h2{Z(oZ1=0W0`oCiW z=Q$7&7@gsKR2#}O32HihOyF>-AYJk6ho3Tho-z00@z#WQGl?66a>uVQAYLo@@sBv2 z9ZEH+%^U~3!<%TP{Gle~-TG@_90I)k0piv#i(~tEZjyrJTwOflx%cc2aV>PfeOtVb z!V08M8B`0J4g@v<)Eaums5NtZ!-1%mWW~Lc7eCg~N#_|a4u{7jWR4#=WHlGLvHkrX ziogobztdlk8U(<#d-bqV;mpbM@R8lo0BaXj?ilLDdhBzafA#8M2NOk`l#)i#`tAi8 zhK*eKlZ$1To#PAUER3Ei30mLCS*6j&Y7E^Ae_MU#9!*?c?TENqm$u=kebSwJ_rvKM zizjU~lZbU&H=Q8ugK1|b!EmZte$Mdb!_0(&jOW4n&TQ^kck@!~R&4exht3eB z-U&Vl=kAW@4nMHVae8r;zJ@DvIkv*QDdm}0^&e2;zunWHF+O8_J0kb6Db&a-TMD@+ zk+6Ayexi2vtelruu1+9Fe2>|iEr(`8AL`vWwrl(8mC5l5Q-<@lR7X&b<)%9-$Tm-AhASmmUu)Zt{4T_NElp z9+e>4Ri53A!Fjz|t+-pa@OU+_c)2?Us$rTiu-q$V1d4!DL49I~;SEs==mdWSa}Di~ zk1|euFJ0?dXZtIp6Rb9v+a(a*nDy;lry!xW`ACx-&Zl8ef8Y`q_&nPz z6mqkjCeVy&AB3Carz^T3`a3?(Jc#pJtTlIqUJ4@Vk4Sy$<3#f?qxHw@)?Rr-+;`t0 zJ-cf}oy;UYG@H!o6T`i@NUb&TpS^JEStD5;B|6#G!4|1=D`U<6hlc(2vy?FwrNl;I z13BIUODl=vtj@RN{7|h{XdA*|KGM2V7^3NCGrvekAF17CLFS|o4+oCll%Mb@{2bvX zLft$F{HIqcs12_fyz?+O9CSJw+mwg-=ZvBjb~F&839iu5t^Rasx*(Z|aEr2j;1cmD znZK!_rYQ-;im$ta&Ku|gH@;Gk1PI6XstHZh_OSL|;*H30qx{LPt8eZc!BYxcqi1K3VXBOtBP0f@`#bsaC!Ox>B-V)PUa`SiUq3}tr8D=}wksnf5G#-T6&xL3UrH+p| zVBD*~ahvn5TE&z$MV}rD&Y^`N zSh}!HyH`5uuzOTJNBAmgnH{>JKzL1uIPCJB1D8prXffv*ngtxeVD=umO|pm?!G<_K z^jn)L;%y_afZ!W&Ew<>b$+CFJCO%Kyp@5gxMq_N8BsG8sl1lHmZ{GA9Kr>s1!Cn1O zVF>!6lbSlD?yVYD6hZE4STi9`OLVM|3)z;|N-VG{G3|yTSwx^qrzX!zG^m!g_HdJo zcE^F6Qx%@dXsixl_Wf4%A5(cqrsPE`#j1^iDg~PT((_8Cm*I+2_g!J71@4YMcUv}C zcJAsO%4<<6gCkH&#dZWLdk?EC?R*-R0tg|Ffo9iyP*0nrs}1p>L)+*0n#sa8+mwpX zNh7N*UNO?+2^4|oKDXVT@(vv)n#OSRni0o3S_|^^PjW!HN`N0>wEHggH-z~x6!3S7(lc^+|Qh3O=&mgT%IoR*_VX{&R z*2AIH#-(-_*Yx=R45U3&3mCQ~jVDEs$~EZNjo9jMhrEwb#kl72^X>Exv5Ba-yE!F} zrN?MUA+bEFU7c5;41lkF9S7+^}9smm!_IbiqM=eE3=9X8O^=8jBUccx& zC>neekfiOSq>Q=50xP1TLZ$+YpD$w+mpA>Z=46on-Jd`KYGQ#J&5il>EuYazRZN%l zef~ah53LwEP4fzkZ&h+wJ6Q2(q~V~$`9BL7dQb+iRC%dmrO^D4C~53=x8}OvH0QnF zkvt`KOgui#115M*;djnJJVMy?=AnW1;ax$BoB?B(<*jrrfI9!VjlNL)DyA8*~L?B9gFYA#9*B>iuD_ zuH#eVyIy^Sv?^3VCfAz5iXa4EEsTiXe$jaEp|6XG$AG zc507>tkpV_pj99qRM*z_nc6&5jpw#QnytLbnA@M|Q87wa54WlQ=19YeQ@jh*QjFFLm7W5U6owPlfv&r!ENBW1$@I z#in}jR+s)%Y3=@(yQT0S%jLAUt2f8WdtNOOn5{TG>13e-$^Xq$A%=ApX$&`t+!aG(y*3tfk-K+CW+(A-m zt9BF}uS+^UCskJs*<{3umDk}yE+}nYhs)EOocF8*!w)VIUiykY5o>N3IRwP87P6aGtk1~x5C*=63-m$Ca`f%Q!Z5C7!&mG(9|YS zxAuDepZl8Rxj}SsGWbSy{BEJyWe2h&391CB<3`nc6}tP^l1JzSp#mS=3UK4X){Rw( z<`cS;S{%`dc+wVWk5S){kuu1QF>boU+^B2Pj9O-vxR77SP+RrIlT-i49x~oPsG;}| ziUR5?S}C9&YkVvGeO3@h`j5y*P0rAqcs$*3U1h z`|^vG7PGqrkiUFADjhWqfq&)CCI^kCFZ|iRq@N-Em5`hRp`nELHYw;B0?dxUwQ8-i z?qBq2XCpq;xo{oaxViR`3xpAW^UuBUC1=eRi9yF$EtDEm_MlOSucj@;AYC!oxS(_1 z)nhG=9%$S(?D8kI-1ZdY zCCI>Legs2<^wOfkK)7_G7EZV?80d9F3}2Mmw9iAcIpmM@#gZwz4dH@YpIls~#JvL8 zvpUbsW}Z7wVMo3Y?%Xr0JQptl`VnZW7*s2KHy!52$OLb^?c+YN2>~N-ipv zuGULbg&Kb?_iWu{tb7d zh(EwF$Sdu?j5qdfPd++ZrGS+R{ndk7y+kcXx>|{dd_G`}dnrmSq84%%{%_DCGZ`$$ z2gHDkQ*Aw+A8v&EaPx=14CIx|x53`@_)X5^Gq`~PC7LUJI3=699|)H_wv{2(cFq$r zh=E%d^b<0HgiF37tF7GyrqGc3;J-kD~P-YE875DI) zJ(UMrQtBqK-7}L1Jd%5euXVFt#k5}fFJFMDab|A?=}<$gVlOc60ppVc)IhluMj1Fs zU1!ynxq-@n=(Z}9^8{zI*6Jqx;vGRuIzH7`KV6!TmYiVcqv?uYbFIh!Juy1;i*C1z zTXR_Y$>ePbuIZs)bnfile`1=-Z5<_VLXAut%EXRLnx+lf#E*r=1Cgmf+*en!9EelW z)Bsz3J6+>CKJJ46()7Bq#r&MwhsL%WE1o^f+R8}u2sFQe3fOYTa^alI9LdC=50r8J z{{juJ1m@$=C}XVk|1k@V%lzSE?SNri-;$ofrXXNT%0k z)3I`zzMQ8Q+=>BR3MiDKmEx~9Xg!u7^A7}4WL_)Qo?q7vu5&C*1nG4?u{_HU?jZ#i z@w59Fgs=?rns?vlTCdlbBTAqqokS?%yFDJL{&x9>Rnp-%4bLU1 zyYnu;)bVMbw@QPs62C8Msn{ZR`*|EAg`Sb~OeBOPzM4IKaBvPis+It1d*x|H=>Hj3FMc#Wa18QlTv z5jx{VTKSiHE$;Vl8Q{>nZ0+eh*xV)Sk3hW1wNs3Vkjq{nNbYFTJx4hRe`1UUGldt+X^`cJCiN z;1SD8iv_IP9y9*Ml^~AZ5PRbIH`lovS6Zc*-<2FQ$e)k!=cy4=JL!KGkyWtTR zWcB%)g`9N3w{30wudW|>Z84Cvs{Nj}F4U6RA+{^rZ85R~{%?$z`<{SJ*%uC8w}9BYO{k1T zdUn?-Ouf0^C>bhrU!%5Fr4iuYbG4YKEjU0V;fPkF!UZ*0e>L_VtY7rN)cd!zd3158 z$P_AGarxi-++se^%K3I0gCL9mu;TM-dC1vz^JojLV(Nda$A_Qo9ow8-6?LT~9At#2 zZ~GOFS{JNu?u@we$cQ_rG0|3kAQ?QnP_}W@P=BK4jEAAoicf-gB*x2vimv$dJp9S| zJ6&RI=aqjd2guAp5VgzVw-^`O`Yo2P$Kj$PiaKu6zLf++-X6CIx!Z}cmhR^R?@!v5 zE$Wuo(I5Vxe_Ev%&2%&lc}ybR#`X4;6;2Ofac*N?}Jm5v=6)wWhOm;(arMvMku6>r0Z06 z(8JAgT|NRW^BEI;r>1Kk#;#9=b+-KTV*cht-d$kc9m^Mva_P&KHjfSMVBNv&k90!1 zw0~9WlnP}quo+}p1Y)Lre&GdX9sos*nQN~!C!p;q0T3kd5ELf=^W%@jStri~_Y)UM zW(Ic9VBnh_rYWqF8_Q)u|KRH~xrnxUzjMR?rrXtYy*%6qoUnX5Q$?=nV{2dQV7&75 z9Junn)wE}a04cr2*i0{G4XYkJnu3Oq4G_`RIxVfa^y*H=A=5->!bSfBK$f22tzfFKZ?u z=?2X(nEoM()H>IS4fY#tr#B$aSg;NQIW%V;Nsy>Gza3)4B=v7-0)AtvG3l$j+#NH| zd06qezqha7P9%AT5mz*#KN<$AVdB@E1|uiHCbMMdBF)-nTN{A?^fvErl=T7=W-?eh z$G2sNZM_2o()Zil5IUT?a}a!Q+_aA_WdhCp>%8{nGO+Y%d~Pw)>GWh1_=P6DLd#MR zCWW-aVp=@ zO7*q>Jup(ReVE@ykhkx87H)XBxj4_*ivC8&>qdznH(BnsRix4pJ@*%XIs$D*cREU; z?tO0#DCLXsV&N3}thvrt>gcTHwVU>~3H^HxYH6Zb#QD$>U86IrwgtkuA;j}dETEHP z$II$Z(UphWH;_+oQ?;*Lk(pGTs*Qeo_Wj)T;rhpsP?YlqLY z&^onVfTyumLJL$4m4ja_ckV@m!9Bn^X63fun@vlpkik4PGn@m32T;X=aMTy1!_AvFxXOGuruF*2l; z@C=YcLdI=5`|T3)FYtuXP4%30sAS5!rK&$p_bw zz-E<%C9AC`N}J3|6aB8Xo;gw39F5b)wu;=@Ev!v|2hm>oiDVA5_7QWX zhfGTWc^e2$ES?Y2LI^}smVcXVfQxH|8fJ#mPb#SwgrP}^#=B{wHs@eJ>pZgy8nyIA zpeoyqEk7!>w8k&XHfp$caI5`R49&_B{s%|aSia#roQMdx)o$%5Vv+7_QObBWX2~}~ z#_Xm!U$$>#A-E&0FOrB6SF>N`oG7&02QUszsb2lYI76ulLwaw$`P5~4!C6qZHZL5hWjeVz zCcsEIg>wC7>ZFJJ=T=D&R+*ckvh;+eu&ADEyw40#5SuskGN~^kIIQs|cHM0uX=vKY zNXa)%usDuveL6)U&$gvJMD?()y8cZ0uC^n>Vl%kBn#i=;_aiYmbx6%)f6$`;yiSf! zsloKhJXAZu;Z^C>9E*hVzb$*6xMxM}_zdg6%sA z?e?QDAotGv)9Adutz`I#cdJ>I32QO%DLTgX81<+%2)Spx1TXolk-n0|%at*1I_4hq zZ5{NyEj9|2DQO31!Fu~8;@1hp)$)6f5GJ`4qd*bBeT8>WZ}EC_=zB3^NO20ge|+Qv zd+1=?3LWLY?LMSkbT-nu+JGud0QJcoi^Q+==`7%T=UID){4t{l#a*=_xK>_0DDlt=7Pa@?qigdCPXJ}KsBHY6*5BTqKm`9glpjyF0CWQ>w4gnPPs4sTMwiXYgREgb*5wQ zeDuyB!pYeZNYv#pvPh8<@LQoW&(O*($$SqSAv3< ztR9>cr(~Iu0+1&f?-tns)CUw0<2f44p4AGcFY$bQr2s<;T`fb(GmLtTSjGEk$G7 zY7|I!j^jg^OqvOgW`njqs?Pnr0E_va+GX@G@)w_jYI7y8-SB)|9r)Z-ocphNHFrnQ z{=u8IasaPsa4cy9w&TnVV%J|O%y^Qo5lo?D5@-y#hXP6$CMRf|A67NzL&kzOI_$BV zBG_dN9rjuzoZVIFp55EedFGm&f6OjGDM_eN3~Q{KBJ~rlKoV6JWR ziUOuZ|7Fkumme&ZU$^!NE13YAt1nxS5eGraarCZOg_!?XG z0nJlMUPm8RTj}qvTUEGOEy+d>UiZh5I_j+lUZXQHL?L1QU1J*`P0h9E-|(6lht0YY zcR>no9aW@gQogH`haP{@-~=>Okvu%VMY*%^(3e6+M1{Cd~Uo;C!@GF zcLJmlLt}gysjUc?bJco38iEP?9^zJj_cy3~RTen@%Pe?RXPe`Wsh!Hdkg>Y$XB#9n^_^kTiTRC0#loBk*Xt*3&Y4i5M4+os{J9F3mS^e+e$->YVs zA+|0u1q#x@SWMN!-=*bQZ=86xzmwnU^5ACtLL9pOhTpVEaOuNcoilsmZ_TYW;UC7F zcd6Vu-R3&x_T%TR_p<`j!Q~#m{ezH;(qRp{6y~}x%F_2q?sNGQXk<&N!{d`*+w;mz z?+2Ky7asBao>A(9I?@QmwrX!Ok-6NeHYLjqm8);Mz{k_2|9u02@4oc$NX3`|zU3|w zf(BsgV(nqUvp$9*s;|~go=Nds8$@eh$#<@&@`BuQOH1*utq~a8Y&>Hj>lxP|=&EK> z$f1o!vbFzZrsjO2u$S5%pDSy4VDwoK{q(e^l8dR716fkY5(DB|qVYzFX35#9oXuxM zAT=&sU-gs3)z#vG2PA3PZ-Z4JXzOXqC!j#dQC)i}()kK%@fXw<{VI?eK+7-dqfWj>ZAk_Xwa|@PXHN{N`#(K1bEY6z&l853NTx;2T~h)%pBxP z(E2Xfd(xp4m$LgeBpof@ts_uhrQ9L!b`=y3sUO{)Wz~vs>|`t}(@K1EUh>qcdVzzZ zS*0QnfeYRbP~%G>dz>h_uV6|Stc(lE0H_s6fYt!yxvhmCQ-IaPSW_$QOGjLuaN!@z zO-2tv{Z77Dw3>n%G4U1%AMH7O|2#k^H8%PwE~Jh6@W}=)mf!So*K*a zZbXiilt{UW--)OOCrzyoc#KT1QW4yr&c~%_^wJTr^2Te7p zCYFf*GJprlR@W^M5qq{;CUDeH>F!kNfj3r`D*)tt8c^>_LIK4QM4{W}q8kL%_@rdr z!7QeVUaV!$D&VOAWDFLP9RfF%lsnhPYlq?C3YcDrdIDRpYL584V!OgczgWpKCl$Vv zkqM##qP1nOvgV{_{pAk9x6uIG1x(B1j17bXA5CF&qPkL!%I1QrIp}?67%ISrxu@{Y z2%wT7nv>nNG^usu;72C8jKP`O+}sTO??3wcPN# z2uVr;Is<}AX(^p0>M8|vUmlt%MLy&~yovbL$NnmY^$Y-+D+44`j$eR8Zs|%2cL$F6 z9(PIBxK(aytvK#hitZQUoN1;~JzPJ{_L6>(ib({xoFRfLOEpxq>s5Db?OvRL zq#hIkY7&dyfi=!Z^RrBo6BPBYV~m=`$&XSFtg}ENCm;}g{0av}>H)-BSQKzY3GBKj zs(nKUVU}VV(uMfO*Feeesm|p=!w?yuE!s>yMkFo|z&XoX2+vF(2u#tOj1L_SR8rK- zsS~EPJaDU%6S0W@Nyt>d--wVg?B7O7)svO{KY|ZKab%AqgO=;kUkh72^jWI4n&*uMu&$h>Tn%nR@UfF9+0jJ-d&z(k5 zSlaGa67W%pXd3s?V5AlaRCiX>HUeHPUJap&+h!t=G13E{Pa~#jPy@+n=cM>RpXj#` zO28hwFu=vnnkV2Mr~U+*T^S>L3pWeM+F#D<$s~q;KVsv?)UJ)Fi&R;$9#rgEi(n_+ zH{$vj$%$<_hx|!JO%$t`-F_NdF;*z^F4RSfPy21}nktFWYuk;_>G(`5^F9?;l_uqx zl0XxWC0PO3bCjMizB5w_>HMUd}li7^r>Jy>U_wp3g(uV=WH9f?E^2RwTPhJ9h1tgY)kICJ*Pdv2pdF zf+hJS#xcN%ft0#sQoDw)8Ji{esl8;(74( zOW5yH^-6ZfrygJer_(W66)u0@d?_P;RZ9UQoyDUZiE>~Ac zf@*Ja>I7mfx&JDG1}TuBw3;Hi}ITgNYdO^0vXz}&Fujt>ycDD`i z4`+ZC=QL+U&>%UgKmnDC)_dov#Yk`DD)MJiH74Fcqik?DOcA*6TFZ@TiB}WaxV5-I z+ZSV}^KWD>H=Q=0a=5VQ<~h`n@8@V`iM~R+dxeW0P@)Ia)TA6W8CPwRVzibtS$Nrx zDIU=$!A{@Ni#LR{DR3)a?8c;!ep@(|0ybp>ttHwfLd^LJ&1vOEkb+}J$ZUfg$7M~V zABSf3>axESe+OtRD=-TjwF^>YDHk?63}MQ_PTVxhWFUbUWI&Tcx5yuSiyw0aJ1bU0 z=wj|T1ldGHol~j4Y}V&Vdvk{fM_!!YVs!-9wzGD)T96voNP}6J4whafZ7v|`WppCsqkD;xyIOS11hKb4r21vx!NF=cNRK~ z#;3Vx5-6kGhN=yRL5mH@@5d&nwV;rdnT#70$eWh^e(IqT7Om*^wnsK%wq_@~SD}WdLeHa17*xg(L&U_lF=_YmrwJ zLDPV@)3nE_`_leP+q)-wZ(jG?`T^9W9HnVjdIsRWL{u*bG1LbddrR+l_n-iFvYjOu z^%_&J6uaAUFt!+X@9pBcPeaQp=liGDiUKnyRXZa_G|U0RdGA_xJSypZWF)>vmqH@A zum%olfUCYle6Y2}33-I=pqXtxb;hu^y49?1e~~8m6*|po=53T&ou3%3HwxJBANNp$Wy97x4tK&Is;K)A zWG)wCIx}zla{hpMXaZMtr$m3v)hr4bez6o&TVOTIg$%qv^>Wk-*voA_ZMF}FhyK_bxXpR9kp8$Q!NXNVEK#J6P-4V}K@TlwIkO&=+@{#Ba- z2k#Nmx&69RO|vvulN^8bo)nbol;w-GM#Di{A4|3-EL;E_{FUeWhsT zY3N?I4k-)}`f}NDcFMjAI<>O!wLm*`S)X=|v|Jib{lei;z!5(7^3{fPHM228_btX$bioKvFTZrbAtNCpACO9cN#qM@f>_ECdeLDS$R}BgtlY)3!slvm^IA z;+`yzCaxO2T=vzivuD998V7hK=;PkkbFsn1!aZ%?>mjeDcV$99Bl`x?Jdmt;)G$xu zB1w~%!q>f&|0&W@`08BBtWNE{*$s>OTk=cEzwKltFKm)!?nT$ulk=DZc32TZ-!I$S@y~E%IpV}SY)8f5Ytiv~{JHM5B%1Hq7eRK&Y1vB? zgW8)Jv^#5y9H-WFEUKXv8dvr6DYm0EK9s>Lm_~64Q7Nu4I9>*RLlfM{kT(-!af>dM z#J!~c19M3Wt`+S_J}PD|=3;qNcHvG{nsD$xTVQ+<#WiPl;hNIKkmkE5A}1Ij7KYDO z4RQl*%zDcW&i)LPoj+R>mkqPQXF`u-jF@NOC4AbM!9dviLI0}7POw9Vs58|xZ!)P2 z&IS)8}fLbRFj}E*_5cvl9I;qzjk5ftq`H=N(AwztE z>I{z~@yDoPkcqk3Rh-9%P5P~5<+!P{ZVp1%F|`TN5@~~Mg9I<-%Z%xSg1ohl*L@D0 zktkGVV?)@0FO|*L-4y{0w8F=7KJekXbm1((;`V@dHpo!kxu{DR^e&AB5_B)FFG9`N zRJvH^mg|{|*i21dAvLEhMTqsSA4ql{-|VC3AQW;A4EfjR-LoN$&991T887NR6zz{H zH{4=12XwIA*~)K{#u-nt_AipL2r1k(Y%#3TkRoV1=7K*u1qjgLo*G zA~G&~PoUS*S=gsq-;2iV5x7yPaEVxI%m0^jDSSkVGA*#V3e#~|u0rT(dWc-s6v;9& ziT2+Uewn0afRQA$qr;1}dGCb=tfQo^pI__d2g|g~$!xv$Yyb0LgY{+6uO&}hM*THQ zm~4tNRRaq)%G2FDKjbk$4>B}g-chPXHLD{WB8udOrmMw#wpf%gWw^v`=bTy62W(z1 z1o~#{+&A*IjlLpMj@!r2iAoIu<-L6n4Y~!5Z<0-_S&UTE)7v0>V3>+AM@T^h>>&_Y zQEKj8qQz7NFl7q(pVnY}vWRhryr9a)l5Cd?=v~K5r9#dy%QlHioc=;A<_vAW5d$y= z34Lr1Zosk{U*tTz#KllrvA3zFIHP0?nV9OR!GIkjC~>|KQ^scXAhQ~J?*$f6cR>a+ z?>EJ`84?lAScvg^8qQ5NsGi*kHjm1wN;FJK56nb(e03Bd0TY#P3L0VxB1Ykt7PvQ( zxCTGdM&T7^S`i!^==I||^;Dkbs#K^$zBF`9AZO=3?SUJg<>UV(oq|WOM>O0v3y=R! zrq2%eTF9g&#_fOzH3$aYLVw2OFh zH1wOK`OWz%PggVkj>|LqVHGAD*YIm>i4cu1YG|iz(V+ET6@Xi{LMz84OniJFiLS=K zuCf-`^;m<%Rn5}gn$V%dxn%89nh!xp5vVUl@JxERcnA;8J(2D5aSq4mlmu=Bf;TQz zTAK;q&s3^9a2bHw;16eUHWIu9%VXw7Jv7;qnp9v0hG`WM(J3nU_V(h4b-TV*TmR=+ z&2b1=MyowAZmLzu0c(~uhC#Cd$O7tU)Ro;1sO}QvTeObj`!gN&LBt(Z>S$Nul;?1u zZ0|@5!f{&$7S_*is8qHXoIZRXpU>57MYaXWiIq(Zh~D%lOiNj*;T$B!=na-rxj88J zuiO@tvkX$GD2pq(64={lzCp;AUkEU(S_&J7Esw?@RDg9Yei(JpCM^8gv6An~RS+Za zNzbWu7fIhmP@IPdnl9s`&V_KWroOc)VxC$Ic7%!FGkj1wY$zoT{pU(6372WbN(dGTmSuhLI)e($V$A}DDcUGQ@?uVoTEB?5h*+^@Z=PQ-5E5lQyve!W`kHz9jWBDW za(z1q@iGw7va4~hCASfzA75v-B;xNUh#oiCvSF3%vi*GmwZMumV}Ls^M3(h9P1;8p zj$UrtNPDt~O9fifhVA4$-OR67q$LSon=rkA?@+j28V}_=$tzQb3|5z}^k%6<4Qts| zQndu2xB5(!V#p!VMXGv?GM1U9ce{64L;MdYb&dWpsrBB$)eD4wPlOZc6J^>wJW=z> zfmt85_+CYy0SM{8uLMzfX+x%t0^W1g2UAM(BlXG!cAFu_%8b_)e|Z@5Ea^)F;2wJH zZ;gWAs)gokdaf-cdA%o0Z+{B1*P$z`@pK7Hf1RTnG4vJdXeF#bc=lKt@LhcYX@1>n zKlKmkKwVFm@x^CSZper3vi6zYDRA%ECyxhW}~4Rv>Jhh?y@75z(^C z#l!0Vb9|p+KXfpBj*r?;^tnDjQa6C}AsB4NEsNlyZu zaj-t9wmNE4>AbZ_+d+_7R0p(|+Vnh{M2PIf&tKju3+Zj%be;7o9`4y`GL(T~MI`@= zNh-4nL1?5Fw6;21dHBc1*sGtO3zRAu*&m+qJfcsmos%(^v`nE{$MfFb^$Q?Otfl40 z?);$ro{M)JJh=CC&F3qy1IICZxa?KCc4B?EFCnnhHh-tt0FmFeB)|Xhjqg!S+hc zbd{<_w8@r26Cz3;4wUMs?kx?$&*}1*`x0qM|93AZ-s zc$C%X;Oh(1Lquo@tsV=sito>BZ}G#UizV>yBYNT2nr)CKbsPU<$z}F)T>>khFZyUsD}QUd z(Mt(ewa^az-5?CA2pRS(=%ff}S5Hddd7`o?_61`ro;tPSY@>ljFLxBl_n`5Oi6c35 z{-X_&M|=d`iudsxqKGCqBmyx<5Sc_ltvP};A}cx!7EP_~ZtqHo4(My7BI^WBU(Gd@ z{7A%x8Y_q>2{hnCNk>=$*`Q#&iHh*_lDS<2Rs;+KO_d;r5C=CJ*twC16IHAR5kbKP z6ICmR+=_*9EZ)z%!fuifLmRT1EHU;J?m4zWD7wxUXos=_%!f+fg1Vh+Z;=LJbje-I zy;V34IDU3WGEmlE;_DIL|7eyB<2Y4O%EO7^W>5u&rtPL|VMt*!*H3L{!&qz(uR3Tw zO`!Fw6t5VTp6x4a(z-4!Phm6aqg90G)k^B69Y60rmO>x)x5df(>jNul!|?f?kPENs z*x^XcPB2@#u}2u#hlK!KEb?bt|A_7s{VMf9fJ8+|hfWAvZk$KKJgX8iD6Bm+fpMze z$R|M*wlo~usV)V-qd6YpXnpNRXVJjhK2_-qKznMh=hIh(e}`0(Pk9vJ@)a5X1_Xmi zF&*{*ETxozTB=fH6MQjH=H(M}7g2UGP9CQ-5lU(4>|=cfX|Xjrce`Ri%KuO{q}J z8nvLq974j>f_NUf?bhxg%$q@mktI7SM^or9N7@*)0O$rNQyx2t5&3<^L4s*uH|)kL z2|P6fg4l-eB6>S;k7H=y7@{kkB*ZC#V(L|IZy|ZCxAkd8l^SW2ros6_!rG^FB)CmwXjl%+`c<$Zk&#1BMN0HHkX0(NH>a*SZB*DLWu8DB|1(~4 z(7fRgR>gdVXVh98kpQ5(!Yucm{KzYL-~})YfuwS8yp92wQoQqIAWs`e+D%V4HiVj4 zY8)L}AEUSQrMAfkQTsHi3JXUXp0Qo2Ns-Nt0uBoAA^e_Q!*|}l3V_u*pO4cXMCx-k-D!FLfEnQ&@{m~%_J3W4BMQb2pG=wED3E~`F13X>UgR$UL#gac z*?6P~=DP%pJSW%~U)prHlA#3oDZzNgg-nuAPZaO;Nw5kd z!tkqf|AuIQBXM5)jxe11uwi!cd zG(^3hiMtEY?0l298*GMtFx-4LFoWD~>sdepnSOipkyifCH;1gXGsiLLRNb@&$;^*; z5mPCkjS0@nS5w_|(>FGuO0`Rr$hN#tzt_XUxDj615Oj67r`Z^_^u(_6oSHWGZcKY4 zuJME`YA-&6$u%_VxdAsKNr$|;_memBTCkH{wulL5`w+3q0sFTX-!U3>LM;3qHM{RIU=Sx-rp#v~?-HcmOAiO$u4>v>paO*ML^T(YZ z%dwLGkqF3BVRRY4MGfSj1Yru36&X{MXPX@XuMfx|XyAwI?7Bz4oo%?sA^|Bjmp`m^ zRff==^>x=re-#AmzZ@=Qf+6R%orR41h}YkZfV15BIs_Xlz!j*{%Y+=2zAXi_*pWR0 zP<T?xFDJU2}ovHN-{}-9P^xq%n^H7RIf@{YuWQxCkTk)qV z*INh`PfdCT_}@OEku47?<40bF8RiJ%3iRg(bTdUz{YIejEF!a!3$@yHy-}kt;QQU_ zrq6*$YvR|SvAhk&d12N(#wvIB&gYgvOhL#=T)Mn})8%7{(t%obEv^rsPkh{kD;eI7 z2o6b`m%y_^O8!HEI)a(WZo99E9YT3|%-`MlOz=c{!-uY**=wpOhqm83#eCZRh+DaY>)9ad2*#UiXkQIUiYRt<|Vi9*~f zO1I-2ZsJaU`~AEB_Skmqy567H`}rcTE!jBn4Cg9SO8mX^?^NE|yRpQ4zZe3Mtl*7xWb|mY}cFPFX z_r6F)T-QIv?ekjUEdJqP^slk&F~?GU(E4AukTKqJs^Rm6+I-&N=VJ=wtiQoF*5_== zh`~$?)O@VSaf&E=2Wi+Pp}IgRQ+31<7o-}>$wX`wt(|>(j~H7Je;skl&yv#o^Ww47 zcdX(q$Ozc#CoOSi7n_^~@1Vy~!FH$3T&IIejL9i<<}dmM$$t3Et0anP;W9FUZ)BAr zHES%ck+XJxKE0rT^K{A79`@JRfxX6o|!JmHGfC1#L+&fKetp4r$g ze-J!;Uc67fO!aM$P!0n?>7Wq%kQU@c*p|X7b~{2%U7Q*BH4$_aS^wbC^}mH^O3Y}V~O_~oAt#0B_;mV z8Q-7nw{N<#sPbN*?(OPOZ_CD3>AEv9^q^E>J?@(Bd8bFV9kT!8hhV*UdBeT)lG~>T@Cv)bdE3ReT*vf(TOKg&`dSce*nDM; z>*k`Qvu*Tmg2$XG?Kpyvuizm3zE8&$@BDiVUP2RgtNUlerdeqc7wlNyKJXEy9YSnV z7EuC{+2N~5T_MW-E??u`=y&_sWGr#C&Y?eDQ`M6rW*m6P5XQJqxQJ;V(aBU_$IjQ` zWx-En#SkfN8#ytW6^jb~uKlv8ME|spU^72o(#Y+Svw7LLv9CC)Yqq|)7H2Kvt)Z0Y z>z04aY+BCqSJpJDR%2kt1z}h<+io@SnM?R>{m4^fOVEyo|D!H$A3K_kX1f;W$4bJR z^Fw~VaLt$7-*&}b-hUHlk^4Uf$cx3d>!SC)8H7cT_D;n%sUK6}HPb2UPYmYUC9k`4 zrJB3^7PpKXvWFHKern${dwFW$$~QHE{?Ve@yJ3^^bI*K)Gt@@^8=$@1Y*1OU#K~H| z`CXWS<@*APk1bII=1;jl#O)2qV8hq1};+LGQPE2$nu6E;(NrotY5%?`{KoFn5Sz_u^bMC#Pjv^>m`E=e(|z znuD$JzIhKfUtCW1;}0lGn1@lb{~#$UyW>9^T>9^dx0~>;LoEW<8Nas+Ye3b6q5f4d zqQc(R+@^%I@ zxp7L{J9MZB6~uxOvh}wH{BxcxLuY)f5PiC zx3iDmQz|!8-o98?$X{F3Q_Q3rN02NIHeM@bWQdD$t!2Bpy($E)^6*KJQ^t%ml@Ihm z9DCnv_u9_E&HgWwu^vJY;}pG~2E&c}ER}zDcg0g*=QB~4Uu`OhQZ2WT7MXjfVF~I` zccF2KUJsh&qhX(b38H`gTRN|~F)D>pP_AAAi8=Iej>u3|o+Mbm(*e6ck=7NOdlK&R zUvW^Qq03CPrKbM!EqwDMsvG`H-ZbOne0^GU{z#wA;7C0rK;Ks{I!38Eyc^dC*0fO?;vH!ZgGbx*8Ue~si{TKEZlouFYB9<(f zGCz0pU_bA5dOEb-4ryy3qXOFP2pOXd)Ivk3Oe-z`jYqnfYIv!`AT*;3ZsSM8ED+B^ zJE?4#WrqvLk(y2l6ZRhF7zNR8ur#p~Mh#3VWSDFr_^T%n#%k3C<#bBeUVoo9!Aahb zgYJlyyRQGR*eXR{5yi@tQ)`EoUh5;=2cwzsnQLNtbM_{>Nbwv#2bF=iV8anYz0u3; z5;yOaHVRE0bxj9}%wn7FM>e@2_m(Ob>xAY~;-?_QW<`9!=JEX~GRGPndAArNMhf8- zK?-Bn+O~fkrnTk}_XM~6K*tR{aAPLHzO*&{v@EcvILcIlWLArlC(=}@2ke#pl21l( zOzXMj9YPoR3Sm1>9@wo2D|*pOLdF#^gmJ}YWT=@jl87VCnBZ#?UUq{||bZZBbtH<-SWa&Gu4I zE}`7+N_3sT31YtKX~Sw6L!)~X9tk%VnTF^7Nh1kkIY5r<1`>k>O`UIIcUy59zb*$C z8diXZzVPN6dN6jsmOJ4UaIu*bcZ#_I>G-d=iHzBwz72Hw939n@rdVB+& z{1h+~dF-WAS?X=*uU>?=hO$v(A3S~fRhY6;YNJ1SPu6?eM+83qA7d@lWQyaspic27 zS2b@}wJbV7bcyc_*c3*klJZwo&G0;*jeLLN{@7j~>IbK8N~U|5)JVahB9m3YMgCdo zhZ=qXO#WK_bA_ZVdKzkMo9*#m(;@L-kMGY$H_yk~p;zW}@V_4--H@Q1?y|f#biZ4b^HEV~mD)aG)xXRb zNzwQ~c2iaYWt1n!|KcHxMO3rEJD(l5_wMr0N4)g5{V)ZOist=HLMU9FVCSB`j?Vjp5Nlzp%N=#hSf zTc~;L1WNMMM`$XD%$RnY_hrG&qx%Z?E{)A=_n3*;{O@wAFOP@!e|z>dprB>#fA_|B zJ7@8i)Z*qbL0A4<%!c6ou$3x%6HfEI^WXo4b*@^oC}?x>%WrM@MXb;uKV2GDBf{Sp zkz*2DLmfLly5w}iP`Bq00^-%F`x_In4_0FrVD0u(_s;Je)%NNU=O6sL4mihyX* ztO;Lq2%=|~eK@d)rN+&w?jF#u=5~c{VDtW*0EQdIq7tcDHybO2`p%Tm^u4ZK(1847 z$3Yp>Shl9z`=btU2Es|&RT=WSV_A^h7l!AI48DpMTTa=^w$A zsXHV4c7L#?B*N|;A=`8xS=9c5bW-CJNQAT%+s<&&gXPsc-nUV}&q}Rv;2Cn+@+i$w&@W|@1WHjV&Pk?Yo`pBo}yFbaRvsX9M>qC3~Vjtk8M^i`B(_;@WLifUa z2`==%mwXfBEs$PAM;56#ey5pSrOXvm)B%yZ$YuVA0Kb6TEG6_Au9UK3JuWy>9wSm7PN(4v3W}kWM+16b>H$PMw zRZ5Q;<3j?}Z>S%qnBc#D2GuQ91lAtw7RLuXnByRny&Q|R(%B=_ z17QU(RIjp@afKf0pC=0D$)T5AG(K`8F$ZM{#tp`@Iu^`SOmOivn(_s<@5|<2FE+oz z!{|;-$U_NV`?UN?^tw9CtkvQj;9?p<5$w;YS-CVg>C%4+~ArTBa)+l5~|h6X!Q{v z2;2iQlO(o4`}I%W|%|Pdo;J8?oSij`c3Gh3Z)RwKrw!EG`H8_x;t z_!3K@1Y~pH3JL}ncT$~lobI6QT$!4uI7-Zai@i(>EM>8)z$9g3yST5P_%Coa#V@6>PprjQ8 zcrm78LhNT7-!vw=?O9fH_}?4h#2uD%|NN2fB7 zvgW$zXfc>MwN})(>W&7lS$*$<^x27wol4@)Ed^eu*Lhn*+Nv}IjPZqvB)S5c?^7En z=QzE-H78aQ1ck5I?YuxPvkbfsOiPXr8tXOt=0M=i(@@jE<~twm7R0Ev1CZKjsDl>f zCz1sj+%tHy+JK7%i0-r#TG$htn%-Hm*NjQTH?d+?=3)T54z=YCorHiK`-{iW`Og>f zmN~5q)es+)%vtXa%A*<&T6N0vS>eKb&LuaEe5}{43r9>49;iIs>;wHV218>b2lNes)Eqcs>SUHwef!$gi@6jCUW(JqfuVuD1FTnrzL%lCG^>` z(E4waK@%E`Z#7kKxh zPw3^#8YG)L&!+EDjs_1|WBxW8)yZ`<-fK_22IOj!?LE%RSQf2Pqm;}TAic~zIZv@@ zbv6HITSbN@Znt>gGZ*&ziS5_P{qVdZL{h4I>|Yt8J|{Yckl%h>Ei)rkZr4Iz{v}$6 z&wSFq=vf2wH? z_!y6kFiRHxWPRRf*sByVtqvAEeP=kifcuj}(w6+W97-C3nhr@^R2(u3Y60AjOxPr} z!YpOM?E%ivg`;`bAVikr&J_uBwAv$(>P3^*kE$#k_%FKIm;zRFum>f&GX)!1kN z4#V;;Zu=GdjDy5$2vg^lVJB2#1P@Z{cP#;Jyp zR7(Q22k-LYRSA?QhnO_~10>iw>$zX*HH$83@Hs~VSuzj)(Q0D-4!Q5g@rZ-mX!?}* zWBLsbZ(N2Is7`-v`uL0A%zhlKK7Gl6+&T7^f)>~(8<+0d4CO7BsbID*{)O*21;i-K|Z2Ab!k^j3?(0vjJsFR*VIGPq5zMS=e*nuGIqRNYy614g{FPsq{g~5rMG}+-H^&uO?z%YMt$RR$)-R9xhDfIC zwu>hYr6#G}GQAcrF4Pn~#e8hD%Xiq-XmGM&#`uqk4&r?kb!3|wmZfI5 zVgkNZy|D=IzzV7kUka@2Z9k17wiSmnjb7;cSweW*igJHQtq}j;()sDKIh|Jy0~^Bb z<5fE%)qQ=UmHfTQ*n!0-|K`%`aA$j)uAU6^O1e9Ik6r$%Z%$VfN(E(wpf@<)srdxp zBYC7WWU$d0)+viWC3)ZDpF^*{Ov!jZrg39COjkxSS8L{ImR(p{Oi& z|8H6L%US8pMOV2>ctB>*QmyoV9ZOL{$+lI1rw!!hr-D5^pmt5sJRZG!xrg)XTN?=% z?p>l^exr35Hrf71(^3##x2eW$0$8BxIm_L`WxgPJEj@rjiTzVqv-jrY%XX_L2PEApScjmpLq9s4?>x%FlJit@9+ zd981YCSW%gek#~^&@58OJ#)SY>}K12OZ#{u`{CeM!8p_lfNoo(S&k9giamWnbAr`O z{H&b&f+@RasExP`bsPeZq|*}M|V#IZH`m3G&o=uQ`VGf7A1 zWvfLm(kII)GQuF^7e)Bl)*@zv^`vvV$-4Um)4VZ?tf0hW=O1jOq)BIG0^K8c>)^6c z29YP|f^yx66=*s2^0tZ6*N2v!TjSHPe|#B}m@RzzEbDR4JLueq=UxSGGFl)OX_Q`* zVh@u;Hmw5Dp;75wc> zw_bEF>xXq_lD-8_T3ouHXtjRb{cFjiPvzTX?K9TNrxKvTsEF)kO(f^?v zys^ztf(jW?nrI@hY>Q+*Ig{sj4jI2o$w2e0))*QK!O({TAl;Q%+ z+$S9odzYSx+dshH*xOVStyI{TfvH`HYqBmV#r$IXKb#=9Sr9=zwk9z|e+61roiKmr zsPY5E*B~>k$6V``cqc5E;Fu0HjFc;9sG-!XY7g|WU|>oF?ivU>Sxya5uLb>WG&Rr5 z3~5NhO5Ns4bkLP~Q%Y)lQkjPU2vHG}mQzFlr#e)pR*WhoWx*v;EU(pw{jWAaWK?v*w>dw_0)Kids9k z<@&d1E0f7@w9&GBko!rS_G1jlgzPVGTmdvbOc51`21@&>2f&VJGHftKm zeeB%<3a+l#X&Ma0oc>NaIv_^#$)TEAv-Zj~Y=&^EhP~$;lrIk@ zTDC(hMhZ=KvAIm4_~`#Ml!868bMT~5b?8j@7ioX4u2864j0yQMFR!c5YP9RtoW_T| z^{l>U8SFw@NdM7h?~A5HLz4Ezt4~Oje?c>qY22=!vwfAPOc(uk+>$)EkxW2m z-q)aC;)nSeX|u2B3Wbc8sp6;njAoh`C#&6hWBF|Fk&j6$mp8XB{`b8T)356UTU1|< zap_gTH%HI|)PT31L!J*xU4-XK<)`;Yj~n3T2DX_#vRH+F01N&rnQSW|c${yB zRMKcZ+)0MZETGoPlrnsX=|=F?4s!9pt%PZDvAZvk;F$&)(U2p8?B{B__r*bK%Y%0%*v$`!-vY z>_fa>e4GoHyvGn~huw@7y4Ktj#@pG?mZgHhC#8;8{L zfT_N)JGS&IYtd)%Ct9QMfo0o3AMxh&X zhIVy%mnzjTU@uv6N8&Z2k|J?FdR)XE>pcn?|J7P@b^_OZHrW&!+o%SdKq$&mc|TDO zSz+N@e}NJyNnPBhV$Jz^2fiLbdi8$I!aq;fSsEjycXrmT5b?YUiz4i(Ylb8vGRqM) zf(k1kn8f)!;D;95hA;_Ed+%8_u%%cMxq6K#)M}Dk9B52ZuAFMt95bvj)P4~v!H(-p7} z9~TE?Tfxnf6-BlRaq$+PS1un_uqg9hf~Bt@H~t^CQ+BI)R$Yne<;xQ0eOC%n*gJN8 zo~`NIqmTdG&wv@5KP`E7guKpf&uK{ioD@5>(*Ss5_ml0UD{VFzqDJ_YWm<(1<)CE} zos8V{JYy|!Ms)YvN*L07+@TO-I1`mEkdC4{u>UB=7#`(XKzsS9DDOmI5a)JiHy3_y z-XrTaEo`@0xFv<0c6!;e*^00MJJeH1tvenaRsXaXBiFS$eHRxP*S+nfESXrJz4Ptm zoBN`*y@WzRAV**CnP&l5?=hm)qj4p)l@a)loBTx3mJG-l5zMmSIA_Y~jB#e9Ss$2z zlPKBbE?HE8+}%n1^GiB>lzr>1w7jDvfr*&8?Iz}I;|aB+XdAVh9sRE23x*^f^SJ`p7R9=~ zX48F7f9gs{G9(7zs{BvP;kL0vAUFbiuVOKH+XiH z2A@p-{pIH16~(X;0$ZoDr8H9Ce!P5V@z3ePW`+3vEiR3&|ClunV0vw48Bmm4kN4!m z%8qD?_V&AG2%6BKcH!D7>rXO^zps>2HuMMhzoLz=bGPB|G>rMS#xXP0q?mn2X6~Y| z3KOekP^-4l>>k3$b?&L4e>TikBQX~mncI4@)h7+mV-$h0U?!N;yu)kI5pmlp6BcFU z{5r%?T(5PvzW!OZDS$SA2QxqI!+Z&te&}UfW`YQmBlA>hDw_fcr{~*yf2oR^$+ViF zIQ~z9ms6R+YSNA>u+h%s%VuOS_$Gh{WT@V05OxD&W=76XtEXqawlJ^>524go)a9@; zbFU~+BN;50@A7Cc~b&(VHoB(tnMZ#UVbh0Ep}QEc)VsPC7v#Wud^ zok*tkBh!eeDa0=4qYlw~Z5lbmsDbVLe%5K311S^`i7BU%)(Z-dN=VQ_5v-D%f2&)K7gEBef+nRi25Qv@`aWb6*vrOS%1NZAso9-lT~#)nz%Rw-Q*58+v5h#d3E!7u*wxi z?9(GAzfm<-;0ikdBc;v`s5{&&wk{@?ztJ+F@m)}AhCbNHt-jSWTIHZ`8dR!Yudm<1 zN1=Q;L^YzsfHcaHdHxykF*2Aa60XZ9e_1Myqv#9^FRl?eU`kXU_;9?^83IT zXxOUGs2iRp?RRkQ5;l&Rb_IsYrp(QZ3?eRaem`c`?#DFpnTPCOC?MDJi7|+DhkrA; zVH~W7cp-Y+;z<=n9h=}@W~qb*OhBB9ik@c(yjWcO);^L*V)B-4+oTa1aT-yL10|~&#`C1V z>;}@{BCux`8fQd=m8!U}pUlR$WgPiug|7tbn`Ww1qd#-7*gIY#4mp**ao#3)fC_H$ zW3pLh)Gd7%!Krd8Y1>j~LyvYOdTS8edX&v1MZe6JnrkBCWsT%6uJvtWiHanZD}?*6 zo5~O`zhnGRk;LZ&k`luBL2q;kBd1rRLBI^+WA`4)c&k=3*t6BJZ^!nGnME;8UhADx z#Se%o_$X)+c)y$qFv$DTTh1@yE@yfQ?rv#V;t<(7oAXN=0V!(o)W>d)+}P@LW=Xsn zMD$w20s_N`lRHfFjoqqUW3+B{pFZtvyPZf^Pwu?~oL@xy-=)Mh9AVb`zl&Vp`;S1z z3HMIdcny?r-_`pxCYi#ezx@a)@Z#uni@D3P{5ZQRm#41nLjF@hp>HtlU|TK76C<44 ztKTv-oWJAsh$~I5X;Sk8OMp?m*_BYVx%$w^S4PpqFIslA3D5LI(I{18#hdG;5Mb^C zA1_KnD7^O_QQlWZT)Ad>bd{4i%$#kqFryvyaW;-T`?&aj(_8&rZP#2RzSf%+6(Yej zi}fI4MsEfJw#J*hlY#aciN&Ha=r%Hc`qoEP0C%*Wc!hO1#nj$SawAxsUePyitv&h- z-I?``(GR*g5beLmS8&rO0-q92?lW&zk=eBsvSBNOm^bl#uK{XJlOUx6V7gcX~Vq801rWm{pfpnyD9qus~5yA~(yr2|%J&3!fBK@ylb9eN+JNzox$fYZ>D6cD?)pX{l z%CjK0_4K1GK(KU=TX=*AZt@kQy%^&==8$b+_HaxbRzE)mX(<)&LNyR!=beyCjq7mM zzV+J?X%(Bt&49;M54F-6pL2+$m17@+nyxtM?SjB`m>_U&=PyXt0gvy|kZ!R|x5@as zSYeI#9jjm93pWGD&YpYLR#_G~p&l41#0&3$Z}lm?XNlnWVP4<3VqhUn>8=!8j!Hf= zB>0maXc1%=?a$tRSfVQ+xPT3d|C?fC8VJfACn@53`)zRL1JQoD$_w$EQh`fbC<-{reIp zo(~g;&IpZoyUMgzN63VPAW9t;od(}?`cEGxdRf_TA?va;13io`E5`!6FYM}gec-4& zs)Ln$*rRU37?(MdJI<3GI^3uNQpV_7_GQ0G1i01n5P}o~EfK6tSie%!!Cw1%R^aIs zN%O5oWg+bA4g?XU`%6-2@(??{T|kMx^y|=#s6|!NE$&Yuh~VTon&|imE9bzgX?4%$ z035gM|L&OS5eeJCgwl~l2vt00(ZwM;*$eC>*r=!XUd3C{YvXQm$Q@$*`(%8kU^l+j zZm)%-qg>1)xnPaQ-1rqyRK1SIt18~D+NNs)!qLDr!1eJxV+V4=N%H8$Y>}6 zRH$%1rGR<-H^jVk=JBTwX4^`>)h)8%BdH=WK56SxQ|W>35WCR{jZ2b;5wdNDL`QFQ zPw3rtQ;>h&-^90`3PP{yKy;L#zx@+K)(~|13Ma%!W`bR>o;%Gl>{5KLITeVYJLqI0yMo(`T{3_jSQh6tnWQBd27TqTPN44;PP&%uxkBH%`W!#P9OHha}p$ zyyD?gA%6+39~|RQGGd6QrIsCRNu%Ug(Xyhki}lMJJKptLf%S{S-v!6(2LcNIo?BtA zmLZj!e!Q%87qPiPebxnNria?CLe}eC=3yjdqf|OWPjf{&-=IHZoD=XQgJZLZ7aU60 zC8Li`tOm^MKGGPe6tLcO(DVjB)y`ka^;nwq=l87cCf4SZfib=g>M`J|AG3(z5Kr#k zaA_tmWvuQ>^nAhIM{O{hl(7NMe0o0{aW5RZWBAu9+x#)3^DH)Lge*sJEp`atnfhxd zKtvRXw)y*wPea3M2&nmO$!}Nn$l#yt`s}&lP$({b2wGIG>au|m(OM~~{&vNm_mM3$ z-kMv*M01x8RL6-$icx6@FaGn1#foWB4Vl$b(YoS>;_ZOuF1GJIChS8BcJVr#N&dZ7NQql-y!UQ455`{wAjpsY*HR!Qnp#~1I~ zOJ^y<=t&9$$;|k-w0A9M=av{|(8k%g1d@hG${ZEl@t(MmZ*u_hJ;*J1Wxl*xw@chp zM6iz1tAE|CTPORzXD#I%?czH#N8xcwG0FCPaI`1~4Z|Gv@KJOfI2v>9%D?YMj+gct zo2O_3!*$En>hvOX+@%#Zi#DrtNh|$gY+T}slXpm)V#Y-?Pp#~6=CuxNl)`Xu6{ih? zrZ_zA`MSv3KS@v(eyG0jDa=wATf#=Bh2BEayTKxlajVc!>nXW?nWev8jd70d>cM;E z&csJzG7~yVC8+OHuPWFns0VXy@O$(NllAe6Al~%T1%2an@_zRSr}Z9W{7 z`VB-S4D%nFw~!+|KA*ns2D3LW7?TkwQN)4)T*RK&_g#}4$)8g3Noq(2v29V9`%WrQ z0a^=ZH4q!C?{9BifFDf6z)6U)@O!2E-CAHaOZJ`3vs8gQ?uC7KkzkT5b=VmdDb-a`N15Xek|fa ze%`0wKUPohZh5!q^*5h&=-cni8MZ3l$1stz0efwi2@ln#nj>^nu69QS)XXNO)O6+T zj|OkST3qhAvbI8=5qMUa?7iVmYWezAC=yPGwrSj47M&oa7z^wj0za31e@wKSiY<`x z#K7w`0k`vwH9JB;wQjkt6RB`YFE^%uHfa^?)hw_j7GjLA<3Y2{W_6jyR_y=X7hZsZmK!+&W8BTH))fvBxq*TjE84}8# z%$sB7{5$a$aV{-vn0=t#+;CBq`6UIM*r<}52bL!P5zH2#V;)vHSGBFp7QbBFNU`0% z8);@Tf?rQ=>fK+hS~KEv+f@tc#LgFTN2y|TAO=6%1y3c{!y4HgSfpN`1#-esqk@W! zDh|-(hW6945mpd5)z3KPFIazP&#$l9+RU47B6i_G^JKdRKUy~t4$WI5i?d! zRc_1@API;P+YGjQj!J^e=|chLV=P9E#3;mKhb zGM2iNmA+Qf(EF@=98sM0&10~+b{3MMeq?q>+W5v;7Ma2U5Xo|MZD7&s_8vl3`=_H) z1$WXV0T$B9M36tjlPKfFukVR=(*l@Dh^#y~A4(N!xq0oJE)T6S(y{n24b2XCGB4mueR(|CZjI7MH|S=?F0c3uqbwZOMxs@AVX&K z#`+#J&p`wH>8$@C6n5n`14jn<0}tV_J>{5rxy6r_l!X6&P2WbPAS^}&(C{3<(lbQs zDiVxY-H|{M*63hmJ7wiF2M$WkN{;#LgjD|M)wgye8!9nxv0`MUJ=*I)BFz2LYcs;r zvgg@MqJ=!I8@{>NB~@G&yj^ZttNh@72>+UE`ODA1_qR6!bX$eJyBhS^5anl(8MvJ8 zsFL6$aZ*gyWA}|OpCxPc3LD-9$B&@wQhp>|?a?;J_sYIN-whJjbKzx5$#Qb$1U0|i zrTOUQ2&ZOt0Z~4-|F*$62L*}71?OYMpvweLF^Dv_xuvYcD`p*6& zZH`TY2^Y(u55;c|KteY@amNK1I>p;_3oX%SsP)%iO4D(&=k2u+=PT(I9#$&UfLIel zo~7mlcA(-_Tsp9suvrVmG^uU;$|c3-E-*8r3bn0i@t3Nyn>JsCMa(W*ftkl$>tmWw z60Nd-X_kuoh+0ew(7ig`ymebf<;}rV-B)*GEzTaLH(&IYY)r(+yU0DT8C}hqZZY&{ zUvVSRh$bALphvT6jl1rcg?u|ShYbs^_)c1JWL)D^admW|(PUL2NkT;qr=DKWv=VM6 zyN;wbLOxh{dPM zFXGW$MM|^RWIuH`h+8Ksw)(amf9JvP?_CkvIjtjJ>xEomc@TooZN!N|2{zuPr->xp zYtE94f~d?N=yLJxe&iu$aFz0-+QVZyT`~lv5}>qa6>o%Byp4z zD7qfi1mZ@(EyyuY$c8gnP~Qt?(jKoiUAAqS5zc~K2%sBO8)7FBWmVC1shy)LYFJEi z%!hDTFv|li|+W7?yr)_@v$5y@jZIUtwNFCPTtaUUs;t@06<-py_H%8;EOASyas- zf7%Z#grv8KXLmO#DoJF zxq4KK9+|Ggn%ZRp=ji>;M#}}L5v)C^%$FKqIP(8UoyPH7R(B8NvrJ&Z-mO@UCVXCn zVT$YEw z(JvK2__0c455`0QH6EY_lFM^`99D37<6>N?j@iOW?GhdOBgTJrHeKbsvsPs~@uT*A z$xed^n^Zz*sa>R9?wkx_k(=?QzVNYPf}ene?jfYIpt5yP6ES>&9brU3EHpAh?U0H! zP=65n?k78wd?Z)xGyq}JAJ0h#y)TK(z4a1rpY(SL7^D5>!zWLQQGq(9SIxE>wW-mH zdz^^2hLUnYWNrI#p}=;kz3M|yUjdc^F27*z*unLLnvIXxg*jC_=y+y8he z=?<+$V2K(ddb8j+6lN7BRgP{blUNv{YF*>D6Azcea!%~YW2mJ1S$s`wr-ivuFNAeaO-P_~&cCC0yiVt58z2EVcTS8Za+vY$+DDM%STj!uS{ z5)a92{NVpAC#SD14s~5ZH|+ld#>=W6!A5v7xRy>?J!KU6B_Uhe4^(d(uee^ay4j z+S>rzh2{;4@U|RzpI3ui8OX~05jXuK6NTTs>x3t zpU7u9>Bo|DVRMX9yMeV#;Ou7y(5Rx)3 zd5{WlQZ~FHm^=*GJPaXal7ZjFOXfHEq_#t|o>A(;5XZLIPU}%7x_V>}!BwjoBZ#}nNa?sqXi{UQV(T;KUHz5hodBYEAoh|T zVSv~o){MvG@t{5Z(~`_*MxN*zVY`4(x@0WpsF@NFh}Q* zGualw!`A64@ATMt9c%u?`9uVcwgLZ%MeB&bX|*m21)Z@QSE-2G^qd`5^j=LD(<*V zqO`G?@(f##hi$t@@A67lt$vlI5Mg)TaY&Cv`LQ=QdSv-25W_5AsR+Ntuuu!ZtJQ!i zzNxGPXE~3oR{KO&XBO)Bj_BΞ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^ diff --git a/docs/plots/extended/stereographic/distance.py b/docs/plots/extended/stereographic/distance.py new file mode 100644 index 00000000..f1e20bf7 --- /dev/null +++ b/docs/plots/extended/stereographic/distance.py @@ -0,0 +1,137 @@ +from geoopt import Stereographic +import torch +import numpy as np +import matplotlib.pyplot as plt +import math +import seaborn as sns +from matplotlib import rcParams +import shutil +if shutil.which("latex") is not None: + rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" + rcParams["text.usetex"] = True +sns.set_style("white") + + +def add_geodesic_grid(ax: plt.Axes, manifold: Stereographic, line_width=0.1): + + # define geodesic grid parameters + N_EVALS_PER_GEODESIC = 10000 + STYLE = "--" + COLOR = "gray" + LINE_WIDTH = line_width + + # get manifold properties + K = manifold.k.item() + R = manifold.radius.item() + + # get maximal numerical distance to origin on manifold + if K < 0: + # create point on R + r = torch.tensor((R, 0.0), dtype=manifold.dtype) + # project point on R into valid range (epsilon border) + r = manifold.projx(r) + # determine distance from origin + max_dist_0 = manifold.dist0(r).item() + else: + max_dist_0 = math.pi * R + # adjust line interval for spherical geometry + circumference = 2*math.pi*R + + # determine reasonable number of geodesics + # choose the grid interval size always as if we'd be in spherical + # geometry, such that the grid interpolates smoothly and evenly + # divides the sphere circumference + n_geodesics_per_circumference = 4 * 6 # multiple of 4! + n_geodesics_per_quadrant = n_geodesics_per_circumference // 2 + grid_interval_size = circumference / n_geodesics_per_circumference + if K < 0: + n_geodesics_per_quadrant = int(max_dist_0 / grid_interval_size) + + # create time evaluation array for geodesics + if K < 0: + min_t = -1.2*max_dist_0 + else: + min_t = -circumference/2.0 + t = torch.linspace(min_t, -min_t, N_EVALS_PER_GEODESIC)[:, None] + + # define a function to plot the geodesics + def plot_geodesic(gv): + ax.plot(*gv.t().numpy(), STYLE, color=COLOR, linewidth=LINE_WIDTH) + + # define geodesic directions + u_x = torch.tensor((0.0, 1.0)) + u_y = torch.tensor((1.0, 0.0)) + + # add origin x/y-crosshair + o = torch.tensor((0.0, 0.0)) + if K < 0: + x_geodesic = manifold.geodesic_unit(t, o, u_x) + y_geodesic = manifold.geodesic_unit(t, o, u_y) + plot_geodesic(x_geodesic) + plot_geodesic(y_geodesic) + else: + # add the crosshair manually for the sproj of sphere + # because the lines tend to get thicker if plotted + # as done for K<0 + ax.axvline(0, linestyle=STYLE, color=COLOR, linewidth=LINE_WIDTH) + ax.axhline(0, linestyle=STYLE, color=COLOR, linewidth=LINE_WIDTH) + + # add geodesics per quadrant + for i in range(1, n_geodesics_per_quadrant): + i = torch.as_tensor(float(i)) + # determine start of geodesic on x/y-crosshair + x = manifold.geodesic_unit(i*grid_interval_size, o, u_y) + y = manifold.geodesic_unit(i*grid_interval_size, o, u_x) + + # compute point on geodesics + x_geodesic = manifold.geodesic_unit(t, x, u_x) + y_geodesic = manifold.geodesic_unit(t, y, u_y) + + # plot geodesics + plot_geodesic(x_geodesic) + plot_geodesic(y_geodesic) + if K < 0: + plot_geodesic(-x_geodesic) + plot_geodesic(-y_geodesic) + + +lim = 1.1 +coords = np.linspace(-lim, lim, 100) +x = torch.tensor([-0.75, 0]) +xx, yy = np.meshgrid(coords, coords) +dist2 = xx ** 2 + yy ** 2 +mask = dist2 <= 1 +grid = np.stack([xx, yy], axis=-1) +fig, ax = plt.subplots(1, 2, figsize=(9, 4)) + +manifold = Stereographic(-1) + +dists = manifold.dist(torch.from_numpy(grid).float(), x) +dists[(~mask).nonzero()] = np.nan +circle = plt.Circle((0, 0), 1, fill=False, color="b") + + +ax[0].add_artist(circle) +ax[0].set_xlim(-lim, lim) +ax[0].set_ylim(-lim, lim) +ax[0].set_aspect("equal") +ax[0].contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +add_geodesic_grid(ax[0], manifold, 0.5) +ax[0].set_title(r"$\kappa = -1$") + +manifold = Stereographic(1) + +dists = manifold.dist(torch.from_numpy(grid).float(), x) + +ax[1].set_xlim(-lim, lim) +ax[1].set_ylim(-lim, lim) +ax[1].set_aspect("equal") +ax[1].contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +add_geodesic_grid(ax[1], manifold, 0.5) +ax[1].set_title(r"$\kappa = 1$") +fig.suptitle("log distance to ($-$0.75, 0)") +plt.show() diff --git a/docs/plots/extended/stereographic/distance2plane.py b/docs/plots/extended/stereographic/distance2plane.py new file mode 100644 index 00000000..2b9b9e46 --- /dev/null +++ b/docs/plots/extended/stereographic/distance2plane.py @@ -0,0 +1,139 @@ +from geoopt import Stereographic +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +import math +from matplotlib import rcParams +import shutil +if shutil.which("latex") is not None: + rcParams["text.latex.preamble"] = r"\usepackage{amsmath}" + rcParams["text.usetex"] = True + +sns.set_style("white") + + +def add_geodesic_grid(ax: plt.Axes, manifold: Stereographic, line_width=0.1): + + # define geodesic grid parameters + N_EVALS_PER_GEODESIC = 10000 + STYLE = "--" + COLOR = "gray" + LINE_WIDTH = line_width + + # get manifold properties + K = manifold.k.item() + R = manifold.radius.item() + + # get maximal numerical distance to origin on manifold + if K < 0: + # create point on R + r = torch.tensor((R, 0.0), dtype=manifold.dtype) + # project point on R into valid range (epsilon border) + r = manifold.projx(r) + # determine distance from origin + max_dist_0 = manifold.dist0(r).item() + else: + max_dist_0 = math.pi * R + # adjust line interval for spherical geometry + circumference = 2*math.pi*R + + # determine reasonable number of geodesics + # choose the grid interval size always as if we'd be in spherical + # geometry, such that the grid interpolates smoothly and evenly + # divides the sphere circumference + n_geodesics_per_circumference = 4 * 6 # multiple of 4! + n_geodesics_per_quadrant = n_geodesics_per_circumference // 2 + grid_interval_size = circumference / n_geodesics_per_circumference + if K < 0: + n_geodesics_per_quadrant = int(max_dist_0 / grid_interval_size) + + # create time evaluation array for geodesics + if K < 0: + min_t = -1.2*max_dist_0 + else: + min_t = -circumference/2.0 + t = torch.linspace(min_t, -min_t, N_EVALS_PER_GEODESIC)[:, None] + + # define a function to plot the geodesics + def plot_geodesic(gv): + ax.plot(*gv.t().numpy(), STYLE, color=COLOR, linewidth=LINE_WIDTH) + + # define geodesic directions + u_x = torch.tensor((0.0, 1.0)) + u_y = torch.tensor((1.0, 0.0)) + + # add origin x/y-crosshair + o = torch.tensor((0.0, 0.0)) + if K < 0: + x_geodesic = manifold.geodesic_unit(t, o, u_x) + y_geodesic = manifold.geodesic_unit(t, o, u_y) + plot_geodesic(x_geodesic) + plot_geodesic(y_geodesic) + else: + # add the crosshair manually for the sproj of sphere + # because the lines tend to get thicker if plotted + # as done for K<0 + ax.axvline(0, linestyle=STYLE, color=COLOR, linewidth=LINE_WIDTH) + ax.axhline(0, linestyle=STYLE, color=COLOR, linewidth=LINE_WIDTH) + + # add geodesics per quadrant + for i in range(1, n_geodesics_per_quadrant): + i = torch.as_tensor(float(i)) + # determine start of geodesic on x/y-crosshair + x = manifold.geodesic_unit(i*grid_interval_size, o, u_y) + y = manifold.geodesic_unit(i*grid_interval_size, o, u_x) + + # compute point on geodesics + x_geodesic = manifold.geodesic_unit(t, x, u_x) + y_geodesic = manifold.geodesic_unit(t, y, u_y) + + # plot geodesics + plot_geodesic(x_geodesic) + plot_geodesic(y_geodesic) + if K < 0: + plot_geodesic(-x_geodesic) + plot_geodesic(-y_geodesic) + + +lim = 1.1 +coords = np.linspace(-lim, lim, 100) +x = torch.tensor([-0.75, 0]) +v = torch.tensor([0.1 / 3, 0.0]) +xx, yy = np.meshgrid(coords, coords) +dist2 = xx ** 2 + yy ** 2 +mask = dist2 <= 1 +grid = np.stack([xx, yy], axis=-1) +fig, ax = plt.subplots(1, 2, figsize=(9, 4)) + +manifold = Stereographic(-1) + +dists = manifold.dist2plane(torch.from_numpy(grid).float(), x, v) +dists[(~mask).nonzero()] = np.nan +circle = plt.Circle((0, 0), 1, fill=False, color="b") + + +ax[0].add_artist(circle) +ax[0].set_xlim(-lim, lim) +ax[0].set_ylim(-lim, lim) +ax[0].set_aspect("equal") +ax[0].contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +add_geodesic_grid(ax[0], manifold, 0.5) +ax[0].set_title(r"$\kappa = -1$") + +manifold = Stereographic(1) + +dists = manifold.dist2plane(torch.from_numpy(grid).float(), x, v) + +ax[1].set_xlim(-lim, lim) +ax[1].set_ylim(-lim, lim) +ax[1].set_aspect("equal") +ax[1].contourf( + grid[..., 0], grid[..., 1], dists.log().numpy(), levels=100, cmap="inferno" +) +add_geodesic_grid(ax[1], manifold, 0.5) +ax[1].set_title(r"$\kappa = 1$") +fig.suptitle(r"log distance to $\tilde{H}_{a, p}$") +plt.show() diff --git a/docs/plots/extended/stereographic/grid-of-geodesics-K--1.0.png b/docs/plots/extended/stereographic/grid-of-geodesics-K--1.0.png new file mode 100644 index 0000000000000000000000000000000000000000..b568eff2ff613e2984311eadc48910706ebe68be GIT binary patch literal 41467 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 literal 0 HcmV?d00001 diff --git a/docs/plots/extended/stereographic/grid-of-geodesics-K-1.0.png b/docs/plots/extended/stereographic/grid-of-geodesics-K-1.0.png new file mode 100644 index 0000000000000000000000000000000000000000..7db5e0a51f69169fd77270846e0f55eb3f9615c0 GIT binary patch literal 49133 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≶3 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 XD+A&Ek0poU51`D9tPHE6@BM!OS-Ob& literal 0 HcmV?d00001 diff --git a/geoopt/__init__.py b/geoopt/__init__.py index f0810fb3..dbaf63cc 100644 --- a/geoopt/__init__.py +++ b/geoopt/__init__.py @@ -18,6 +18,10 @@ SphereExact, PoincareBall, PoincareBallExact, + Stereographic, + StereographicExact, + SphereProjection, + SphereProjectionExact, ProductManifold, Scaled, BirkhoffPolytope, diff --git a/geoopt/manifolds/__init__.py b/geoopt/manifolds/__init__.py index 01635fc9..dcdd3e72 100644 --- a/geoopt/manifolds/__init__.py +++ b/geoopt/manifolds/__init__.py @@ -2,9 +2,16 @@ from .euclidean import Euclidean from .stiefel import Stiefel, EuclideanStiefel, CanonicalStiefel, EuclideanStiefelExact from .sphere import Sphere, SphereExact -from .poincare import PoincareBall, PoincareBallExact from .birkhoff_polytope import BirkhoffPolytope +from .stereographic import ( + PoincareBall, + PoincareBallExact, + Stereographic, + StereographicExact, + SphereProjection, + SphereProjectionExact, +) from .product import ProductManifold -from . import poincare +from . import stereographic from . import scaled from .scaled import Scaled diff --git a/geoopt/manifolds/poincare/math.py b/geoopt/manifolds/poincare/math.py deleted file mode 100644 index d4e9bfcf..00000000 --- a/geoopt/manifolds/poincare/math.py +++ /dev/null @@ -1,1359 +0,0 @@ -""" -Poincare manifold utility functions. - -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 - - -MIN_NORM = 1e-15 -BALL_EPS = {torch.float32: 4e-3, torch.float64: 1e-5} - - -def tanh(x): - return x.clamp(-15, 15).tanh() - - -class Artanh(torch.autograd.Function): - @staticmethod - def forward(ctx, x): - x = x.clamp(-1 + 1e-5, 1 - 1e-5) - 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_(MIN_NORM).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, eps=None): - 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 - eps : float - stability parameter, uses default for dtype if not provided - - Returns - ------- - tensor - projected vector on the manifold - """ - return _project(x, c, dim, eps) - - -def _project(x, c, dim: int = -1, eps: float = None): - norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(MIN_NORM) - if eps is None: - eps = BALL_EPS[x.dtype] - maxnorm = (1 - eps) / (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)).clamp_min(MIN_NORM) - - -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""" - Compute 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): - 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 - # minimize denom (omit c to simplify th notation) - # 1) - # {d(denom)/d(x) = 2 y + 2x * = 0 - # {d(denom)/d(y) = 2 x + 2y * = 0 - # 2) - # {y + x * = 0 - # {x + y * = 0 - # 3) - # {- y/ = x - # {- x/ = y - # 4) - # minimum = 1 - 2 / + / = 0 - return num / denom.clamp_min(MIN_NORM) - - -def mobius_sub(x, y, *, c=1.0, dim=-1): - r""" - Compute Mobius substraction. - - Mobius substraction 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""" - Compute 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): - 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.clamp_min(MIN_NORM) - - -def mobius_cosub(x, y, *, c=1.0, dim=-1): - r""" - Compute Mobius cosubstraction operation. - - Mobius cosubstraction is defined as follows: - - .. 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""" - Compute 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_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(MIN_NORM) - 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""" - Compute geodesic 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""" - Compute geodesic 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 geodesic(t, x, y, *, c=1.0, dim=-1): - r""" - Compute geodesic at the time point :math:`t`. - - 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""" - Compute exponential map on the Poincare ball. - - 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): - sqrt_c = c ** 0.5 - u_norm = u.norm(dim=dim, p=2, keepdim=True).clamp_min(MIN_NORM) - 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""" - Compute 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): - sqrt_c = c ** 0.5 - u_norm = u.norm(dim=dim, p=2, keepdim=True).clamp_min(MIN_NORM) - 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""" - Compute unit speed geodesic at time :math:`t` 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).clamp_min(MIN_NORM) - 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""" - Compute 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).clamp_min(MIN_NORM) - 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""" - Compute 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_norm = y.norm(dim=dim, p=2, keepdim=True).clamp_min(MIN_NORM) - return y / y_norm / sqrt_c * artanh(sqrt_c * y_norm) - - -def mobius_matvec(m, x, *, c=1.0, dim=-1): - r""" - Compute a generalization for matrix-vector multiplication to hyperbolic space. - - Mobius matrix vector operation is defined as follows: - - .. 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_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(MIN_NORM) - 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).clamp_min(MIN_NORM) - 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""" - Compute a generalization for point-wise multiplication to hyperbolic space. - - Mobius pointwise multiplication is defined as follows - - .. 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_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(MIN_NORM) - sqrt_c = c ** 0.5 - wx = w * x - wx_norm = wx.norm(dim=dim, keepdim=True, p=2).clamp_min(MIN_NORM) - 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""" - Compute a generalization for sequential function application 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""" - Compute a generalization for function application 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""" - Wrap a function so that is works in hyperbolic space. - - Parameters - ---------- - fn : callable - function in Euclidean space, only its first argument is treated as hyperbolic - - Returns - ------- - callable - function working in hyperbolic space - - Notes - ----- - New function will accept additional argument ``c``. - """ - - @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""" - Compute geodesic distance from :math:`x` to a hyperbolic hyperplane in Poincare ball. - - The distance is computed to a plane 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).clamp_min(MIN_NORM) - 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).clamp_min(MIN_NORM) - num = 2 * sqrt_c * sc_diff_a - denom = (1 - c * diff_norm2) * a_norm - return arsinh(num / denom.clamp_min(MIN_NORM)) / sqrt_c - - -def gyration(a, b, u, *, c=1.0, dim=-1): - r""" - Apply gyration :math:`\operatorname{gyr}[u, v]w`. - - Guration 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.clamp_min(MIN_NORM) - - -def parallel_transport(x, y, v, *, c=1.0, dim=-1): - r""" - Perform parallel transport on the Poincare ball. - - 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""" - Perform parallel transport from zero point. - - 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)).clamp_min(MIN_NORM) - - -def parallel_transport0back(x, v, *, c=1.0, dim: int = -1): - r""" - Perform parallel transport to the zero point. - - Special case parallel transport with last point at zero that - can be computed more efficiently and numerically stable - - Parameters - ---------- - x : 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_transport0back(x, v, c=c, dim=dim) - - -def _parallel_transport0back(x, v, c, dim: int = -1): - return v / (1 - c * x.pow(2).sum(dim=dim, keepdim=True)).clamp_min(MIN_NORM) - - -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/stereographic/__init__.py b/geoopt/manifolds/stereographic/__init__.py new file mode 100644 index 00000000..ae787788 --- /dev/null +++ b/geoopt/manifolds/stereographic/__init__.py @@ -0,0 +1,10 @@ +from .manifold import ( + Stereographic, + StereographicExact, + PoincareBall, + PoincareBallExact, + SphereProjection, + SphereProjectionExact, +) + +from . import math diff --git a/geoopt/manifolds/poincare/__init__.py b/geoopt/manifolds/stereographic/manifold.py similarity index 50% rename from geoopt/manifolds/poincare/__init__.py rename to geoopt/manifolds/stereographic/manifold.py index 666e852f..83745671 100644 --- a/geoopt/manifolds/poincare/__init__.py +++ b/geoopt/manifolds/stereographic/manifold.py @@ -1,19 +1,84 @@ import torch.nn -from typing import Tuple, Optional +from typing import Tuple, Optional, List from . import math import geoopt from ...utils import size2shape, broadcast_shapes from ..base import Manifold, ScalingInfo -__all__ = ["PoincareBall", "PoincareBallExact"] +__all__ = [ + "Stereographic", + "StereographicExact", + "PoincareBall", + "PoincareBallExact", + "SphereProjection", + "SphereProjectionExact", +] + +_stereographic_doc = r""" + :math:`\kappa`-Stereographic model. + + Parameters + ---------- + k : float|tensor + sectional curvature :math:`\kappa` of the manifold + - k<0: Poincaré ball (stereographic projection of hyperboloid) + - k>0: Stereographic projection of sphere + - k=0: Euclidean geometry + + Notes + ----- + It is extremely recommended to work with this manifold in double precision. + + Documentation & Illustration + ---------------------------- + http://andbloch.github.io/K-Stereographic-Model/ or :doc:`/extended/stereographic` +""" + +_references = """References + ---------- + The functions for the mathematics in gyrovector spaces are taken from the + following resources: + + [1] Ganea, Octavian, Gary Bécigneul, and Thomas Hofmann. "Hyperbolic + neural networks." Advances in neural information processing systems. + 2018. + [2] Bachmann, Gregor, Gary Bécigneul, and Octavian-Eugen Ganea. "Constant + Curvature Graph Convolutional Networks." arXiv preprint + arXiv:1911.05076 (2019). + [3] Skopek, Ondrej, Octavian-Eugen Ganea, and Gary Bécigneul. + "Mixed-curvature Variational Autoencoders." arXiv preprint + arXiv:1911.08411 (2019). + [4] Ungar, Abraham A. Analytic hyperbolic geometry: Mathematical + foundations and applications. World Scientific, 2005. + [5] Albert, Ungar Abraham. Barycentric calculus in Euclidean and + hyperbolic geometry: A comparative introduction. World Scientific, + 2010. +""" _poincare_ball_doc = r""" - Poincare ball model, see more in :doc:`/extended/poincare`. + Poincare ball model. + + See more in :doc:`/extended/stereographic` Parameters ---------- c : float|tensor - ball negative curvature + ball's negative curvature. The parametrization is constrained to have positive c + + Notes + ----- + It is extremely recommended to work with this manifold in double precision +""" + +_sphere_projection_doc = r""" + Stereographic Projection Spherical model. + + See more in :doc:`/extended/stereographic` + + Parameters + ---------- + k : float|tensor + sphere's positive curvature. The parametrization is constrained to have positive k Notes ----- @@ -22,29 +87,42 @@ # noinspection PyMethodOverriding -class PoincareBall(Manifold): +class Stereographic(Manifold): __doc__ = r"""{} + {} + See Also -------- + :class:`StereographicExact` + :class:`PoincareBall` :class:`PoincareBallExact` + :class:`SphereProjection` + :class:`SphereProjectionExact` """.format( - _poincare_ball_doc + _stereographic_doc, _references, ) ndim = 1 reversible = False - name = "Poincare ball" + name = property(lambda self: self.__class__.__name__) __scaling__ = Manifold.__scaling__.copy() - def __init__(self, c=1.0): + @property + def radius(self): + return self.k.abs().sqrt().reciprocal() + + def __init__(self, k=0.0, learnable=False): super().__init__() - self.register_buffer("c", torch.as_tensor(c, dtype=torch.get_default_dtype())) + k = torch.as_tensor(k) + if not torch.is_floating_point(k): + k = k.to(torch.get_default_dtype()) + self.k = torch.nn.Parameter(k, requires_grad=learnable) def _check_point_on_manifold( self, x: torch.Tensor, *, atol=1e-5, rtol=1e-5, dim=-1 ) -> Tuple[bool, Optional[str]]: - px = math.project(x, c=self.c, dim=dim) + px = math.project(x, k=self.k, dim=dim) 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]" @@ -60,23 +138,23 @@ def _check_vector_on_tangent( def dist( self, x: torch.Tensor, y: torch.Tensor, *, keepdim=False, dim=-1 ) -> torch.Tensor: - return math.dist(x, y, c=self.c, keepdim=keepdim, dim=dim) + return math.dist(x, y, k=self.k, keepdim=keepdim, dim=dim) def dist2( self, x: torch.Tensor, y: torch.Tensor, *, keepdim=False, dim=-1 ) -> torch.Tensor: - return math.dist(x, y, c=self.c, keepdim=keepdim, dim=dim) ** 2 + return math.dist(x, y, k=self.k, keepdim=keepdim, dim=dim) ** 2 def egrad2rgrad(self, x: torch.Tensor, u: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.egrad2rgrad(x, u, c=self.c, dim=dim) + return math.egrad2rgrad(x, u, k=self.k, dim=dim) def retr(self, x: torch.Tensor, u: torch.Tensor, *, dim=-1) -> torch.Tensor: # always assume u is scaled properly approx = x + u - return math.project(approx, c=self.c, dim=dim) + return math.project(approx, k=self.k, dim=dim) def projx(self, x: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.project(x, c=self.c, dim=dim) + return math.project(x, k=self.k, dim=dim) def proju(self, x: torch.Tensor, u: torch.Tensor, *, dim=-1) -> torch.Tensor: target_shape = broadcast_shapes(x.shape, u.shape) @@ -89,31 +167,31 @@ def inner( v: torch.Tensor = None, *, keepdim=False, - dim=-1 + dim=-1, ) -> torch.Tensor: if v is None: v = u - return math.inner(x, u, v, c=self.c, keepdim=keepdim, dim=dim) + return math.inner(x, u, v, k=self.k, keepdim=keepdim, dim=dim) def norm( self, x: torch.Tensor, u: torch.Tensor, *, keepdim=False, dim=-1 ) -> torch.Tensor: - return math.norm(x, u, c=self.c, keepdim=keepdim, dim=dim) + return math.norm(x, u, k=self.k, keepdim=keepdim, dim=dim) def expmap( self, x: torch.Tensor, u: torch.Tensor, *, project=True, dim=-1 ) -> torch.Tensor: - res = math.expmap(x, u, c=self.c, dim=dim) + res = math.expmap(x, u, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def logmap(self, x: torch.Tensor, y: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.logmap(x, y, c=self.c, dim=dim) + return math.logmap(x, y, k=self.k, dim=dim) def transp(self, x: torch.Tensor, y: torch.Tensor, v: torch.Tensor, *, dim=-1): - return math.parallel_transport(x, y, v, c=self.c, dim=dim) + return math.parallel_transport(x, y, v, k=self.k, dim=dim) def transp_follow_retr( self, x: torch.Tensor, u: torch.Tensor, v: torch.Tensor, *, dim=-1 @@ -144,110 +222,113 @@ def retr_transp( def mobius_add( self, x: torch.Tensor, y: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_add(x, y, c=self.c, dim=dim) + res = math.mobius_add(x, y, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_sub( self, x: torch.Tensor, y: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_sub(x, y, c=self.c, dim=dim) + res = math.mobius_sub(x, y, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_coadd( self, x: torch.Tensor, y: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_coadd(x, y, c=self.c, dim=dim) + res = math.mobius_coadd(x, y, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_cosub( self, x: torch.Tensor, y: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_cosub(x, y, c=self.c, dim=dim) + res = math.mobius_cosub(x, y, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_scalar_mul( self, r: torch.Tensor, x: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_scalar_mul(r, x, c=self.c, dim=dim) + res = math.mobius_scalar_mul(r, x, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_pointwise_mul( self, w: torch.Tensor, x: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_pointwise_mul(w, x, c=self.c, dim=dim) + res = math.mobius_pointwise_mul(w, x, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def mobius_matvec( self, m: torch.Tensor, x: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.mobius_matvec(m, x, c=self.c, dim=dim) + res = math.mobius_matvec(m, x, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def geodesic( self, t: torch.Tensor, x: torch.Tensor, y: torch.Tensor, *, dim=-1 ) -> torch.Tensor: - return math.geodesic(t, x, y, c=self.c, dim=dim) + return math.geodesic(t, x, y, k=self.k, dim=dim) @__scaling__(ScalingInfo(t=-1)) def geodesic_unit( self, t: torch.Tensor, x: torch.Tensor, u: torch.Tensor, *, dim=-1, project=True ) -> torch.Tensor: - res = math.geodesic_unit(t, x, u, c=self.c, dim=dim) + res = math.geodesic_unit(t, x, u, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res def lambda_x(self, x: torch.Tensor, *, dim=-1, keepdim=False) -> torch.Tensor: - return math.lambda_x(x, c=self.c, dim=dim, keepdim=keepdim) + return math.lambda_x(x, k=self.k, dim=dim, keepdim=keepdim) @__scaling__(ScalingInfo(1)) def dist0(self, x: torch.Tensor, *, dim=-1, keepdim=False) -> torch.Tensor: - return math.dist0(x, c=self.c, dim=dim, keepdim=keepdim) + return math.dist0(x, k=self.k, dim=dim, keepdim=keepdim) @__scaling__(ScalingInfo(u=-1)) def expmap0(self, u: torch.Tensor, *, dim=-1, project=True) -> torch.Tensor: - res = math.expmap0(u, c=self.c, dim=dim) + res = math.expmap0(u, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res @__scaling__(ScalingInfo(1)) def logmap0(self, x: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.logmap0(x, c=self.c, dim=dim) + return math.logmap0(x, k=self.k, dim=dim) def transp0(self, y: torch.Tensor, u: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.parallel_transport0(y, u, c=self.c, dim=dim) + return math.parallel_transport0(y, u, k=self.k, dim=dim) def transp0back(self, y: torch.Tensor, u: torch.Tensor, *, dim=-1) -> torch.Tensor: - return math.parallel_transport0back(y, u, c=self.c, dim=dim) + return math.parallel_transport0back(y, u, k=self.k, dim=dim) def gyration( self, x: torch.Tensor, y: torch.Tensor, z: torch.Tensor, *, dim=-1 ) -> torch.Tensor: - return math.gyration(x, y, z, c=self.c, dim=dim) + return math.gyration(x, y, z, k=self.k, dim=dim) + + def antipode(self, x: torch.Tensor, *, dim=-1) -> torch.Tensor: + return math.antipode(x, k=self.k, dim=dim) @__scaling__(ScalingInfo(1)) def dist2plane( @@ -258,10 +339,11 @@ def dist2plane( *, dim=-1, keepdim=False, - signed=False + signed=False, + scaled=False, ) -> torch.Tensor: return math.dist2plane( - x, p, a, dim=dim, c=self.c, keepdim=keepdim, signed=signed + x, p, a, dim=dim, k=self.k, keepdim=keepdim, signed=signed, scaled=scaled ) # this does not yet work with scaling @@ -269,9 +351,9 @@ def dist2plane( def mobius_fn_apply( self, fn: callable, x: torch.Tensor, *args, dim=-1, project=True, **kwargs ) -> torch.Tensor: - res = math.mobius_fn_apply(fn, x, *args, c=self.c, dim=dim, **kwargs) + res = math.mobius_fn_apply(fn, x, *args, k=self.k, dim=dim, **kwargs) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res @@ -280,9 +362,9 @@ def mobius_fn_apply( def mobius_fn_apply_chain( self, x: torch.Tensor, *fns: callable, project=True, dim=-1 ) -> torch.Tensor: - res = math.mobius_fn_apply_chain(x, *fns, c=self.c, dim=dim) + res = math.mobius_fn_apply_chain(x, *fns, k=self.k, dim=dim) if project: - return math.project(res, c=self.c, dim=dim) + return math.project(res, k=self.k, dim=dim) else: return res @@ -317,16 +399,16 @@ def random_normal( """ size = size2shape(*size) self._assert_check_shape(size, "x") - if device is not None and device != self.c.device: + if device is not None and device != self.k.device: raise ValueError( "`device` does not match the manifold `device`, set the `device` argument to None" ) - if dtype is not None and dtype != self.c.dtype: + if dtype is not None and dtype != self.k.dtype: raise ValueError( "`dtype` does not match the manifold `dtype`, set the `dtype` argument to None" ) tens = ( - torch.randn(size, device=self.c.device, dtype=self.c.dtype) + torch.randn(size, device=self.k.device, dtype=self.k.dtype) * std / size[-1] ** 0.5 + mean @@ -361,23 +443,148 @@ def origin( torch.zeros(*size, dtype=dtype, device=device), manifold=self ) + def weighted_midpoint( + self, + xs: torch.Tensor, + weights: Optional[torch.Tensor] = None, + *, + reducedim: Optional[List[int]] = None, + dim: int = -1, + keepdim: bool = False, + lincomb: bool = False, + project=True, + ): + mid = math.weighted_midpoint( + xs=xs, + weights=weights, + k=self.k, + reducedim=reducedim, + dim=dim, + keepdim=keepdim, + lincomb=lincomb, + ) + if project: + return math.project(mid, k=self.k, dim=dim) + else: + return mid + + def sproj(self, x: torch.Tensor, *, dim: int = -1): + return math.sproj(x, k=self.k, dim=dim) + + def inv_sproj(self, x: torch.Tensor, *, dim: int = -1): + return math.inv_sproj(x, k=self.k, dim=dim) -class PoincareBallExact(PoincareBall): + +class StereographicExact(Stereographic): __doc__ = r"""{} The implementation of retraction is an exact exponential map, this retraction will be used in optimization. See Also -------- + :class:`Stereographic` :class:`PoincareBall` + :class:`PoincareBallExact` + :class:`SphereProjection` + :class:`SphereProjectionExact` """.format( - _poincare_ball_doc + _stereographic_doc ) reversible = True - retr_transp = PoincareBall.expmap_transp - transp_follow_retr = PoincareBall.transp_follow_expmap - retr = PoincareBall.expmap + retr_transp = Stereographic.expmap_transp + transp_follow_retr = Stereographic.transp_follow_expmap + retr = Stereographic.expmap def extra_repr(self): return "exact" + + +class PoincareBall(Stereographic): + __doc__ = r"""{} + + See Also + -------- + :class:`Stereographic` + :class:`StereographicExact` + :class:`PoincareBallExact` + :class:`SphereProjection` + :class:`SphereProjectionExact` + """.format( + _poincare_ball_doc + ) + + @property + def k(self): + return -self.c + + @property + def c(self): + return torch.nn.functional.softplus(self.isp_c) + + def __init__(self, c=1.0, learnable=False): + super().__init__(k=c, learnable=learnable) + k = self._parameters.pop("k") + with torch.no_grad(): + self.isp_c = k.exp_().sub_(1).log_() + + +class PoincareBallExact(PoincareBall, StereographicExact): + __doc__ = r"""{} + + The implementation of retraction is an exact exponential map, this retraction will be used in optimization. + + See Also + -------- + :class:`Stereographic` + :class:`StereographicExact` + :class:`PoincareBall` + :class:`SphereProjection` + :class:`SphereProjectionExact` + """.format( + _poincare_ball_doc + ) + + +class SphereProjection(Stereographic): + __doc__ = r"""{} + + See Also + -------- + :class:`Stereographic` + :class:`StereographicExact` + :class:`PoincareBall` + :class:`PoincareBallExact` + :class:`SphereProjectionExact` + :class:`Sphere` + """.format( + _sphere_projection_doc + ) + + @property + def k(self): + return torch.nn.functional.softplus(self.isp_k) + + def __init__(self, k=1.0, learnable=False): + super().__init__(k=k, learnable=learnable) + k = self._parameters.pop("k") + with torch.no_grad(): + self.isp_k = k.exp_().sub_(1).log_() + + +class SphereProjectionExact(SphereProjection, StereographicExact): + __doc__ = r"""{} + + The implementation of retraction is an exact exponential map, this retraction will be used in optimization. + + See Also + -------- + :class:`Stereographic` + :class:`StereographicExact` + :class:`PoincareBall` + :class:`PoincareBallExact` + :class:`SphereProjectionExact` + :class:`Sphere` + """.format( + _sphere_projection_doc + ) diff --git a/geoopt/manifolds/stereographic/math.py b/geoopt/manifolds/stereographic/math.py new file mode 100644 index 00000000..3203f1cb --- /dev/null +++ b/geoopt/manifolds/stereographic/math.py @@ -0,0 +1,2004 @@ +r""" +:math:`\kappa`-Stereographic math module. + +The functions for the mathematics in gyrovector spaces are taken from the +following resources: + + [1] Ganea, Octavian, Gary Bécigneul, and Thomas Hofmann. "Hyperbolic + neural networks." Advances in neural information processing systems. + 2018. + [2] Bachmann, Gregor, Gary Bécigneul, and Octavian-Eugen Ganea. "Constant + Curvature Graph Convolutional Networks." arXiv preprint + arXiv:1911.05076 (2019). + [3] Skopek, Ondrej, Octavian-Eugen Ganea, and Gary Bécigneul. + "Mixed-curvature Variational Autoencoders." arXiv preprint + arXiv:1911.08411 (2019). + [4] Ungar, Abraham A. Analytic hyperbolic geometry: Mathematical + foundations and applications. World Scientific, 2005. + [5] Albert, Ungar Abraham. Barycentric calculus in Euclidean and + hyperbolic geometry: A comparative introduction. World Scientific, + 2010. +""" + +import functools +import torch.jit +from typing import List, Optional +from ...utils import list_range, drop_dims, sign, clamp_abs, sabs + + +@torch.jit.script +def tanh(x): + return x.clamp(-15, 15).tanh() + + +@torch.jit.script +def artanh(x: torch.Tensor): + x = x.clamp(-1 + 1e-7, 1 - 1e-7) + return (torch.log(1 + x).sub(torch.log(1 - x))).mul(0.5) + + +@torch.jit.script +def arsinh(x: torch.Tensor): + return (x + torch.sqrt(1 + x.pow(2))).clamp_min(1e-15).log().to(x.dtype) + + +@torch.jit.script +def abs_zero_grad(x): + # this op has derivative equal to 1 at zero + return x * sign(x) + + +@torch.jit.script +def tan_k_zero_taylor(x: torch.Tensor, k: torch.Tensor, order: int = -1): + if order == 0: + return x + k = abs_zero_grad(k) + if order == -1 or order == 5: + return ( + x + + 1 / 3 * k * x ** 3 + + 2 / 15 * k ** 2 * x ** 5 + + 17 / 315 * k ** 3 * x ** 7 + + 62 / 2835 * k ** 4 * x ** 9 + + 1382 / 155925 * k ** 5 * x ** 11 + # + o(k**6) + ) + elif order == 1: + return x + 1 / 3 * k * x ** 3 + elif order == 2: + return x + 1 / 3 * k * x ** 3 + 2 / 15 * k ** 2 * x ** 5 + elif order == 3: + return ( + x + + 1 / 3 * k * x ** 3 + + 2 / 15 * k ** 2 * x ** 5 + + 17 / 315 * k ** 3 * x ** 7 + ) + elif order == 4: + return ( + x + + 1 / 3 * k * x ** 3 + + 2 / 15 * k ** 2 * x ** 5 + + 17 / 315 * k ** 3 * x ** 7 + + 62 / 2835 * k ** 4 * x ** 9 + ) + else: + raise RuntimeError("order not in [-1, 5]") + + +@torch.jit.script +def artan_k_zero_taylor(x: torch.Tensor, k: torch.Tensor, order: int = -1): + if order == 0: + return x + k = abs_zero_grad(k) + if order == -1 or order == 5: + return ( + x + - 1 / 3 * k * x ** 3 + + 1 / 5 * k ** 2 * x ** 5 + - 1 / 7 * k ** 3 * x ** 7 + + 1 / 9 * k ** 4 * x ** 9 + - 1 / 11 * k ** 5 * x ** 11 + # + o(k**6) + ) + elif order == 1: + return x - 1 / 3 * k * x ** 3 + elif order == 2: + return x - 1 / 3 * k * x ** 3 + 1 / 5 * k ** 2 * x ** 5 + elif order == 3: + return ( + x - 1 / 3 * k * x ** 3 + 1 / 5 * k ** 2 * x ** 5 - 1 / 7 * k ** 3 * x ** 7 + ) + elif order == 4: + return ( + x + - 1 / 3 * k * x ** 3 + + 1 / 5 * k ** 2 * x ** 5 + - 1 / 7 * k ** 3 * x ** 7 + + 1 / 9 * k ** 4 * x ** 9 + ) + else: + raise RuntimeError("order not in [-1, 5]") + + +@torch.jit.script +def arsin_k_zero_taylor(x: torch.Tensor, k: torch.Tensor, order: int = -1): + if order == 0: + return x + k = abs_zero_grad(k) + if order == -1 or order == 5: + return ( + x + + k * x ** 3 / 6 + + 3 / 40 * k ** 2 * x ** 5 + + 5 / 112 * k ** 3 * x ** 7 + + 35 / 1152 * k ** 4 * x ** 9 + + 63 / 2816 * k ** 5 * x ** 11 + # + o(k**6) + ) + elif order == 1: + return x + k * x ** 3 / 6 + elif order == 2: + return x + k * x ** 3 / 6 + 3 / 40 * k ** 2 * x ** 5 + elif order == 3: + return x + k * x ** 3 / 6 + 3 / 40 * k ** 2 * x ** 5 + 5 / 112 * k ** 3 * x ** 7 + elif order == 4: + return ( + x + + k * x ** 3 / 6 + + 3 / 40 * k ** 2 * x ** 5 + + 5 / 112 * k ** 3 * x ** 7 + + 35 / 1152 * k ** 4 * x ** 9 + ) + else: + raise RuntimeError("order not in [-1, 5]") + + +@torch.jit.script +def sin_k_zero_taylor(x: torch.Tensor, k: torch.Tensor, order: int = -1): + if order == 0: + return x + k = abs_zero_grad(k) + if order == -1 or order == 5: + return ( + x + - k * x ** 3 / 6 + + k ** 2 * x ** 5 / 120 + - k ** 3 * x ** 7 / 5040 + + k ** 4 * x ** 9 / 362880 + - k ** 5 * x ** 11 / 39916800 + # + o(k**6) + ) + elif order == 1: + return x - k * x ** 3 / 6 + elif order == 2: + return x - k * x ** 3 / 6 + k ** 2 * x ** 5 / 120 + elif order == 3: + return x - k * x ** 3 / 6 + k ** 2 * x ** 5 / 120 - k ** 3 * x ** 7 / 5040 + elif order == 4: + return ( + x + - k * x ** 3 / 6 + + k ** 2 * x ** 5 / 120 + - k ** 3 * x ** 7 / 5040 + + k ** 4 * x ** 9 / 362880 + ) + else: + raise RuntimeError("order not in [-1, 5]") + + +@torch.jit.script +def tan_k(x: torch.Tensor, k: torch.Tensor): + k_sign = k.sign() + zero = torch.zeros((), device=k.device, dtype=k.dtype) + k_zero = k.isclose(zero) + # shrink sign + k_sign = torch.masked_fill(k_sign, k_zero, zero.to(k_sign.dtype)) + if torch.all(k_zero): + return tan_k_zero_taylor(x, k, order=1) + k_sqrt = sabs(k).sqrt() + scaled_x = x * k_sqrt + + if torch.all(k_sign.lt(0)): + return k_sqrt.reciprocal() * tanh(scaled_x) + elif torch.all(k_sign.gt(0)): + return k_sqrt.reciprocal() * scaled_x.clamp_max(1e38).tan() + else: + tan_k_nonzero = ( + torch.where(k_sign.gt(0), scaled_x.clamp_max(1e38).tan(), tanh(scaled_x)) + * k_sqrt.reciprocal() + ) + return torch.where(k_zero, tan_k_zero_taylor(x, k, order=1), tan_k_nonzero) + + +@torch.jit.script +def artan_k(x: torch.Tensor, k: torch.Tensor): + k_sign = k.sign() + zero = torch.zeros((), device=k.device, dtype=k.dtype) + k_zero = k.isclose(zero) + # shrink sign + k_sign = torch.masked_fill(k_sign, k_zero, zero.to(k_sign.dtype)) + if torch.all(k_zero): + return artan_k_zero_taylor(x, k, order=1) + k_sqrt = sabs(k).sqrt() + scaled_x = x * k_sqrt + + if torch.all(k_sign.lt(0)): + return k_sqrt.reciprocal() * artanh(scaled_x) + elif torch.all(k_sign.gt(0)): + return k_sqrt.reciprocal() * scaled_x.atan() + else: + artan_k_nonzero = ( + torch.where(k_sign.gt(0), scaled_x.atan(), artanh(scaled_x)) + * k_sqrt.reciprocal() + ) + return torch.where(k_zero, artan_k_zero_taylor(x, k, order=1), artan_k_nonzero) + + +@torch.jit.script +def arsin_k(x: torch.Tensor, k: torch.Tensor): + k_sign = k.sign() + zero = torch.zeros((), device=k.device, dtype=k.dtype) + k_zero = k.isclose(zero) + # shrink sign + k_sign = torch.masked_fill(k_sign, k_zero, zero.to(k_sign.dtype)) + if torch.all(k_zero): + return arsin_k_zero_taylor(x, k) + k_sqrt = sabs(k).sqrt() + scaled_x = x * k_sqrt + + if torch.all(k_sign.lt(0)): + return k_sqrt.reciprocal() * arsinh(scaled_x) + elif torch.all(k_sign.gt(0)): + return k_sqrt.reciprocal() * scaled_x.asin() + else: + arsin_k_nonzero = ( + torch.where( + k_sign.gt(0), + scaled_x.clamp(-1 + 1e-7, 1 - 1e-7).asin(), + arsinh(scaled_x), + ) + * k_sqrt.reciprocal() + ) + return torch.where(k_zero, arsin_k_zero_taylor(x, k, order=1), arsin_k_nonzero) + + +@torch.jit.script +def sin_k(x: torch.Tensor, k: torch.Tensor): + k_sign = k.sign() + zero = torch.zeros((), device=k.device, dtype=k.dtype) + k_zero = k.isclose(zero) + # shrink sign + k_sign = torch.masked_fill(k_sign, k_zero, zero.to(k_sign.dtype)) + if torch.all(k_zero): + return sin_k_zero_taylor(x, k) + k_sqrt = sabs(k).sqrt() + scaled_x = x * k_sqrt + + if torch.all(k_sign.lt(0)): + return k_sqrt.reciprocal() * torch.sinh(scaled_x) + elif torch.all(k_sign.gt(0)): + return k_sqrt.reciprocal() * scaled_x.sin() + else: + sin_k_nonzero = ( + torch.where(k_sign.gt(0), scaled_x.sin(), torch.sinh(scaled_x)) + * k_sqrt.reciprocal() + ) + return torch.where(k_zero, sin_k_zero_taylor(x, k, order=1), sin_k_nonzero) + + +def project(x: torch.Tensor, *, k: torch.Tensor, dim=-1, eps=-1): + r""" + Safe projection on the manifold for numerical stability. + + Parameters + ---------- + x : tensor + point on the Poincare ball + k : tensor + sectional curvature of manifold + dim : int + reduction dimension to compute norm + eps : float + stability parameter, uses default for dtype if not provided + + Returns + ------- + tensor + projected vector on the manifold + """ + return _project(x, k, dim, eps) + + +@torch.jit.script +def _project(x, k, dim: int = -1, eps: float = -1.0): + if eps < 0: + if x.dtype == torch.float32: + eps = 4e-3 + else: + eps = 1e-5 + maxnorm = (1 - eps) / (sabs(k) ** 0.5) + maxnorm = torch.where(k.lt(0), maxnorm, k.new_full((), 1e15)) + norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + cond = norm > maxnorm + projected = x / norm * maxnorm + return torch.where(cond, projected, x) + + +def lambda_x(x: torch.Tensor, *, k: torch.Tensor, keepdim=False, dim=-1): + r""" + Compute the conformal factor :math:`\lambda^\kappa_x` for a point on the ball. + + .. math:: + \lambda^\kappa_x = \frac{1}{1 + \kappa \|x\|_2^2} + + Parameters + ---------- + x : tensor + point on the Poincare ball + k : tensor + sectional curvature of manifold + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + conformal factor + """ + return _lambda_x(x, k, keepdim=keepdim, dim=dim) + + +@torch.jit.script +def _lambda_x(x: torch.Tensor, k: torch.Tensor, keepdim: bool = False, dim: int = -1): + return 2 / (1 + k * x.pow(2).sum(dim=dim, keepdim=keepdim)).clamp_min(1e-15) + + +def inner( + x: torch.Tensor, u: torch.Tensor, v: torch.Tensor, *, k, 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^\kappa_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 + k : tensor + sectional curvature of manifold + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + inner product + """ + return _inner(x, u, v, k, keepdim=keepdim, dim=dim) + + +@torch.jit.script +def _inner( + x: torch.Tensor, + u: torch.Tensor, + v: torch.Tensor, + k: torch.Tensor, + keepdim: bool = False, + dim: int = -1, +): + return _lambda_x(x, k, keepdim=True, dim=dim) ** 2 * (u * v).sum( + dim=dim, keepdim=keepdim + ) + + +def norm(x: torch.Tensor, u: torch.Tensor, *, k: torch.Tensor, 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^\kappa_x \|u\|_2 + + Parameters + ---------- + x : tensor + point on the Poincare ball + u : tensor + tangent vector to :math:`x` on Poincare ball + k : tensor + sectional curvature of manifold + keepdim : bool + retain the last dim? (default: false) + dim : int + reduction dimension + + Returns + ------- + tensor + norm of vector + """ + return _norm(x, u, k, keepdim=keepdim, dim=dim) + + +@torch.jit.script +def _norm( + x: torch.Tensor, + u: torch.Tensor, + k: torch.Tensor, + keepdim: bool = False, + dim: int = -1, +): + return _lambda_x(x, k, keepdim=keepdim, dim=dim) * u.norm( + dim=dim, keepdim=keepdim, p=2 + ) + + +def mobius_add(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the Möbius gyrovector addition. + + .. math:: + + x \oplus_\kappa y = + \frac{ + (1 - 2 \kappa \langle x, y\rangle - \kappa \|y\|^2_2) x + + (1 + \kappa \|x\|_2^2) y + }{ + 1 - 2 \kappa \langle x, y\rangle + \kappa^2 \|x\|^2_2 \|y\|^2_2 + } + + .. plot:: plots/extended/stereographic/mobius_add.py + + In general this operation is not commutative: + + .. math:: + + x \oplus_\kappa y \ne y \oplus_\kappa x + + But in some cases this property holds: + + * zero vector case + + .. math:: + + \mathbf{0} \oplus_\kappa x = x \oplus_\kappa \mathbf{0} + + * zero 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_\kappa (x \oplus_\kappa y) = y + + Parameters + ---------- + x : tensor + point on the manifold + y : tensor + point on the manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of the Möbius addition + """ + return _mobius_add(x, y, k, dim=dim) + + +@torch.jit.script +def _mobius_add(x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1): + 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 * k * xy - k * y2) * x + (1 + k * x2) * y + denom = 1 - 2 * k * xy + k ** 2 * x2 * y2 + # minimize denom (omit K to simplify th notation) + # 1) + # {d(denom)/d(x) = 2 y + 2x * = 0 + # {d(denom)/d(y) = 2 x + 2y * = 0 + # 2) + # {y + x * = 0 + # {x + y * = 0 + # 3) + # {- y/ = x + # {- x/ = y + # 4) + # minimum = 1 - 2 / + / = 0 + return num / denom.clamp_min(1e-15) + + +def mobius_sub(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the Möbius gyrovector subtraction. + + The Möbius subtraction can be represented via the Möbius addition as + follows: + + .. math:: + + x \ominus_\kappa y = x \oplus_\kappa (-y) + + Parameters + ---------- + x : tensor + point on manifold + y : tensor + point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of the Möbius subtraction + """ + return _mobius_sub(x, y, k, dim=dim) + + +def _mobius_sub(x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1): + return _mobius_add(x, -y, k, dim=dim) + + +def gyration( + a: torch.Tensor, b: torch.Tensor, u: torch.Tensor, *, k: torch.Tensor, dim=-1 +): + r""" + Compute the gyration of :math:`u` by :math:`[a,b]`. + + The gyration is a special operation of gyrovector spaces. The gyrovector + space addition operation :math:`\oplus_\kappa` is not associative (as + mentioned in :func:`mobius_add`), but it is gyroassociative, which means + + .. math:: + + u \oplus_\kappa (v \oplus_\kappa w) + = + (u\oplus_\kappa v) \oplus_\kappa \operatorname{gyr}[u, v]w, + + where + + .. math:: + + \operatorname{gyr}[u, v]w + = + \ominus (u \oplus_\kappa v) \oplus (u \oplus_\kappa (v \oplus_\kappa w)) + + We can simplify this equation using the explicit formula for the Möbius + addition [1]. Recall, + + .. math:: + + A = - \kappa^2 \langle u, w\rangle \langle v, v\rangle + - \kappa \langle v, w\rangle + + 2 \kappa^2 \langle u, v\rangle \langle v, w\rangle\\ + B = - \kappa^2 \langle v, w\rangle \langle u, u\rangle + + \kappa \langle u, w\rangle\\ + D = 1 - 2 \kappa \langle u, v\rangle + + \kappa^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 manifold + b : tensor + second point on manifold + u : tensor + vector field for operation + k : tensor + sectional curvature of manifold + 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, k, dim=dim) + + +@torch.jit.script +def _gyration( + u: torch.Tensor, v: torch.Tensor, w: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + # non-simplified + # mupv = -_mobius_add(u, v, K) + # vpw = _mobius_add(u, w, K) + # upvpw = _mobius_add(u, vpw, K) + # return _mobius_add(mupv, upvpw, K) + # 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) + K2 = k ** 2 + a = -K2 * uw * v2 - k * vw + 2 * K2 * uv * vw + b = -K2 * vw * u2 + k * uw + d = 1 - 2 * k * uv + K2 * u2 * v2 + return w + 2 * (a * u + b * v) / d.clamp_min(1e-15) + + +def mobius_coadd(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the Möbius gyrovector coaddition. + + The addition operation :math:`\oplus_\kappa` is neither associative, nor + commutative. In contrast, the coaddition :math:`\boxplus_\kappa` (or + cooperation) is an associative operation that is defined as follows. + + .. math:: + + a \boxplus_\kappa b + = + b \boxplus_\kappa a + = + a\operatorname{gyr}[a, -b]b\\ + = \frac{ + (1 + \kappa \|y\|^2_2) x + (1 + \kappa \|x\|_2^2) y + }{ + 1 + \kappa^2 \|x\|^2_2 \|y\|^2_2 + }, + + where :math:`\operatorname{gyr}[a, b]v = \ominus_\kappa (a \oplus_\kappa b) + \oplus_\kappa (a \oplus_\kappa (b \oplus_\kappa v))` + + The following right cancellation property holds + + .. math:: + + (a \boxplus_\kappa b) \ominus_\kappa b = a\\ + (a \oplus_\kappa b) \boxminus_\kappa b = a + + Parameters + ---------- + x : tensor + point on manifold + y : tensor + point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of the Möbius coaddition + + """ + return _mobius_coadd(x, y, k, dim=dim) + + +# TODO: check numerical stability with Gregor's paper!!! +@torch.jit.script +def _mobius_coadd(x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1): + # x2 = x.pow(2).sum(dim=dim, keepdim=True) + # y2 = y.pow(2).sum(dim=dim, keepdim=True) + # num = (1 + K * y2) * x + (1 + K * x2) * y + # denom = 1 - K ** 2 * x2 * y2 + # avoid division by zero in this way + # return num / denom.clamp_min(1e-15) + # + return _mobius_add(x, _gyration(x, -y, y, k=k, dim=dim), k, dim=dim) + + +def mobius_cosub(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the Möbius gyrovector cosubtraction. + + The Möbius cosubtraction is defined as follows: + + .. math:: + + a \boxminus_\kappa b = a \boxplus_\kappa -b + + Parameters + ---------- + x : tensor + point on manifold + y : tensor + point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of the Möbius cosubtraction + + """ + return _mobius_cosub(x, y, k, dim=dim) + + +@torch.jit.script +def _mobius_cosub(x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1): + return _mobius_coadd(x, -y, k, dim=dim) + + +# TODO: can we make this operation somehow safer by breaking up the +# TODO: scalar multiplication for K>0 when the argument to the +# TODO: tan function gets close to pi/2+k*pi for k in Z? +# TODO: one could use the scalar associative law +# TODO: s_1 (X) s_2 (X) x = (s_1*s_2) (X) x +# TODO: to implement a more stable Möbius scalar mult +def mobius_scalar_mul(r: torch.Tensor, x: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the Möbius scalar multiplication. + + .. math:: + + r \otimes_\kappa x + = + \tan_\kappa(r\tan_\kappa^{-1}(\|x\|_2))\frac{x}{\|x\|_2} + + This operation has properties similar to the Euclidean scalar multiplication + + * `n-addition` property + + .. math:: + + r \otimes_\kappa x = x \oplus_\kappa \dots \oplus_\kappa x + + * Distributive property + + .. math:: + + (r_1 + r_2) \otimes_\kappa x + = + r_1 \otimes_\kappa x \oplus r_2 \otimes_\kappa x + + * Scalar associativity + + .. math:: + + (r_1 r_2) \otimes_\kappa x = r_1 \otimes_\kappa (r_2 \otimes_\kappa x) + + * Monodistributivity + + .. math:: + + r \otimes_\kappa (r_1 \otimes x \oplus r_2 \otimes x) = + r \otimes_\kappa (r_1 \otimes x) \oplus r \otimes (r_2 \otimes x) + + * Scaling property + + .. math:: + + |r| \otimes_\kappa x / \|r \otimes_\kappa x\|_2 = x/\|x\|_2 + + Parameters + ---------- + r : tensor + scalar for multiplication + x : tensor + point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the result of the Möbius scalar multiplication + """ + return _mobius_scalar_mul(r, x, k, dim=dim) + + +@torch.jit.script +def _mobius_scalar_mul( + r: torch.Tensor, x: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + x_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + res_c = tan_k(r * artan_k(x_norm, k), k) * (x / x_norm) + return res_c + + +def dist(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, keepdim=False, dim=-1): + r""" + Compute the geodesic distance between :math:`x` and :math:`y` on the manifold. + + .. math:: + + d_\kappa(x, y) = 2\tan_\kappa^{-1}(\|(-x)\oplus_\kappa y\|_2) + + .. plot:: plots/extended/stereographic/distance.py + + Parameters + ---------- + x : tensor + point on manifold + y : tensor + point on manifold + k : tensor + sectional curvature of manifold + 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, k, keepdim=keepdim, dim=dim) + + +@torch.jit.script +def _dist( + x: torch.Tensor, + y: torch.Tensor, + k: torch.Tensor, + keepdim: bool = False, + dim: int = -1, +): + return 2.0 * artan_k( + _mobius_add(-x, y, k, dim=dim).norm(dim=dim, p=2, keepdim=keepdim), k + ) + + +def dist0(x: torch.Tensor, *, k: torch.Tensor, keepdim=False, dim=-1): + r""" + Compute geodesic distance to the manifold's origin. + + Parameters + ---------- + x : tensor + point on manifold + k : tensor + sectional curvature of manifold + 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, k, keepdim=keepdim, dim=dim) + + +@torch.jit.script +def _dist0(x: torch.Tensor, k: torch.Tensor, keepdim: bool = False, dim: int = -1): + return 2.0 * artan_k(x.norm(dim=dim, p=2, keepdim=keepdim), k) + + +def geodesic( + t: torch.Tensor, x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1 +): + r""" + Compute the point on the path connecting :math:`x` and :math:`y` at time :math:`x`. + + The path can also be treated as an extension of the line segment to an + unbounded geodesic that goes through :math:`x` and :math:`y`. The equation + of the geodesic is given as: + + .. math:: + + \gamma_{x\to y}(t) + = + x \oplus_\kappa t \otimes_\kappa ((-x) \oplus_\kappa y) + + The properties of the geodesic are the following: + + .. math:: + + \gamma_{x\to y}(0) = x\\ + \gamma_{x\to y}(1) = y\\ + \dot\gamma_{x\to y}(t) = v + + Furthermore, the geodesic also satisfies the property of local distance + minimization: + + .. math:: + + d_\kappa(\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, we can always compute the constant speed :math:`v` from the points + that the particular path connects: + + .. math:: + + v = d_\kappa(\gamma_{x\to y}(0), \gamma_{x\to y}(1)) = d_\kappa(x, y) + + + Parameters + ---------- + t : tensor + travelling time + x : tensor + starting point on manifold + y : tensor + target point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + point on the geodesic going through x and y + """ + return _geodesic(t, x, y, k, dim=dim) + + +@torch.jit.script +def _geodesic( + t: torch.Tensor, x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + # this is not very numerically stable + v = _mobius_add(-x, y, k, dim=dim) + tv = _mobius_scalar_mul(t, v, k, dim=dim) + gamma_t = _mobius_add(x, tv, k, dim=dim) + return gamma_t + + +def expmap(x: torch.Tensor, u: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the exponential map of :math:`u` at :math:`x`. + + The expmap is tightly related with :func:`geodesic`. Intuitively, the + expmap represents a smooth travel along a geodesic from the starting point + :math:`x`, into the initial direction :math:`u` at speed :math:`\|u\|_x` for + the duration of one time unit. In formulas one can express this as the + travel along the 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 the differential + equation solution, that is local. For the universal manifold the solution + is well defined globally and we have. + + .. math:: + + \operatorname{exp}^\kappa_x(u) = \gamma_{x, u}(1) = \\ + x\oplus_\kappa \tan_\kappa(\|u\|_x/2) \frac{u}{\|u\|_2} + + Parameters + ---------- + x : tensor + starting point on manifold + u : tensor + speed vector in tangent space at x + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + :math:`\gamma_{x, u}(1)` end point + """ + return _expmap(x, u, k, dim=dim) + + +@torch.jit.script +def _expmap(x: torch.Tensor, u: torch.Tensor, k: torch.Tensor, dim: int = -1): + u_norm = u.norm(dim=dim, p=2, keepdim=True).clamp_min(1e-15) + lam = _lambda_x(x, k, dim=dim, keepdim=True) + second_term = tan_k((lam / 2.0) * u_norm, k) * (u / u_norm) + y = _mobius_add(x, second_term, k, dim=dim) + return y + + +def expmap0(u: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the exponential map of :math:`u` at the origin :math:`0`. + + .. math:: + + \operatorname{exp}^\kappa_0(u) + = + \tan_\kappa(\|u\|_2/2) \frac{u}{\|u\|_2} + + Parameters + ---------- + u : tensor + speed vector on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + :math:`\gamma_{0, u}(1)` end point + """ + return _expmap0(u, k, dim=dim) + + +@torch.jit.script +def _expmap0(u: torch.Tensor, k: torch.Tensor, dim: int = -1): + u_norm = u.norm(dim=dim, p=2, keepdim=True).clamp_min(1e-15) + gamma_1 = tan_k(u_norm, k) * (u / u_norm) + return gamma_1 + + +def geodesic_unit( + t: torch.Tensor, x: torch.Tensor, u: torch.Tensor, *, k: torch.Tensor, dim=-1 +): + r""" + Compute the point on the unit speed geodesic. + + The point on the unit speed geodesic at time :math:`t`, starting + from :math:`x` with initial direction :math:`u/\|u\|_x` is computed + as follows: + + .. math:: + + \gamma_{x,u}(t) = x\oplus_\kappa \tan_\kappa(t/2) \frac{u}{\|u\|_2} + + Parameters + ---------- + t : tensor + travelling time + x : tensor + initial point on manifold + u : tensor + initial direction in tangent space at x + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + the point on the unit speed geodesic + """ + return _geodesic_unit(t, x, u, k, dim=dim) + + +@torch.jit.script +def _geodesic_unit( + t: torch.Tensor, x: torch.Tensor, u: torch.Tensor, k: torch.Tensor, dim: int = -1, +): + u_norm = u.norm(dim=dim, p=2, keepdim=True).clamp_min(1e-15) + second_term = tan_k(t / 2.0, k) * (u / u_norm) + gamma_1 = _mobius_add(x, second_term, k, dim=dim) + return gamma_1 + + +def logmap(x: torch.Tensor, y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the logarithmic map of :math:`y` at :math:`x`. + + .. math:: + + \operatorname{log}^\kappa_x(y) = \frac{2}{\lambda_x^\kappa} + \tan_\kappa^{-1}(\|(-x)\oplus_\kappa y\|_2) + * \frac{(-x)\oplus_\kappa y}{\|(-x)\oplus_\kappa y\|_2} + + The result of the logmap is a vector :math:`u` in the tangent space of + :math:`x` such that + + .. math:: + + y = \operatorname{exp}^\kappa_x(\operatorname{log}^\kappa_x(y)) + + + Parameters + ---------- + x : tensor + starting point on manifold + y : tensor + target point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + tangent vector :math:`u\in T_x M` that transports :math:`x` to :math:`y` + """ + return _logmap(x, y, k, dim=dim) + + +@torch.jit.script +def _logmap(x: torch.Tensor, y: torch.Tensor, k: torch.Tensor, dim: int = -1): + sub = _mobius_add(-x, y, k, dim=dim) + sub_norm = sub.norm(dim=dim, p=2, keepdim=True).clamp_min(1e-15) + lam = _lambda_x(x, k, keepdim=True, dim=dim) + return 2.0 * artan_k(sub_norm, k) * (sub / (lam * sub_norm)) + + +def logmap0(y: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the logarithmic map of :math:`y` at the origin :math:`0`. + + .. math:: + + \operatorname{log}^\kappa_0(y) + = + \tan_\kappa^{-1}(\|y\|_2) \frac{y}{\|y\|_2} + + The result of the logmap at the origin is a vector :math:`u` in the tangent + space of the origin :math:`0` such that + + .. math:: + + y = \operatorname{exp}^\kappa_0(\operatorname{log}^\kappa_0(y)) + + Parameters + ---------- + y : tensor + target point on manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + tangent vector :math:`u\in T_0 M` that transports :math:`0` to :math:`y` + """ + return _logmap0(y, k, dim=dim) + + +@torch.jit.script +def _logmap0(y: torch.Tensor, k, dim: int = -1): + y_norm = y.norm(dim=dim, p=2, keepdim=True).clamp_min(1e-15) + return (y / y_norm) * artan_k(y_norm, k) + + +def mobius_matvec(m: torch.Tensor, x: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the generalization of matrix-vector multiplication in gyrovector spaces. + + The Möbius matrix vector operation is defined as follows: + + .. math:: + + M \otimes_\kappa x = \tan_\kappa\left( + \frac{\|Mx\|_2}{\|x\|_2}\tan_\kappa^{-1}(\|x\|_2) + \right)\frac{Mx}{\|Mx\|_2} + + .. plot:: plots/extended/stereographic/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 manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Möbius matvec result + """ + return _mobius_matvec(m, x, k, dim=dim) + + +@torch.jit.script +def _mobius_matvec(m: torch.Tensor, x: torch.Tensor, k: torch.Tensor, dim: int = -1): + if m.dim() > 2 and dim != -1: + raise RuntimeError( + "broadcasted Möbius matvec is supported for the last dim only" + ) + x_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + if dim != -1 or m.dim() == 2: + mx = torch.tensordot(x, m, [dim], [1]) + else: + mx = torch.matmul(m, x.unsqueeze(-1)).squeeze(-1) + mx_norm = mx.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + res_c = tan_k(mx_norm / x_norm * artan_k(x_norm, k), k) * (mx / mx_norm) + 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 + + +# TODO: check if this extends to gyrovector spaces for positive curvature +# TODO: add plot +def mobius_pointwise_mul(w: torch.Tensor, x: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the generalization for point-wise multiplication in gyrovector spaces. + + The Möbius pointwise multiplication is defined as follows + + .. math:: + + \operatorname{diag}(w) \otimes_\kappa x = \tan_\kappa\left( + \frac{\|\operatorname{diag}(w)x\|_2}{x}\tanh^{-1}(\|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 manifold + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Möbius point-wise mul result + """ + return _mobius_pointwise_mul(w, x, k, dim=dim) + + +@torch.jit.script +def _mobius_pointwise_mul( + w: torch.Tensor, x: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + x_norm = x.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + wx = w * x + wx_norm = wx.norm(dim=dim, keepdim=True, p=2).clamp_min(1e-15) + res_c = tan_k(wx_norm / x_norm * artan_k(x_norm, k), k) * (wx / wx_norm) + zero = torch.zeros((), dtype=res_c.dtype, device=res_c.device) + cond = wx.isclose(zero).prod(dim=dim, keepdim=True, dtype=torch.uint8) + res = torch.where(cond, zero, res_c) + return res + + +def mobius_fn_apply_chain(x: torch.Tensor, *fns: callable, k: torch.Tensor, dim=-1): + r""" + Compute the generalization of sequential function application in gyrovector spaces. + + First, a gyrovector is mapped to the tangent space (first-order approx.) via + :math:`\operatorname{log}^\kappa_0` and then the sequence of functions is + applied to the vector in the tangent space. The resulting tangent vector is + then mapped back with :math:`\operatorname{exp}^\kappa_0`. + + .. math:: + + f^{\otimes_\kappa}(x) + = + \operatorname{exp}^\kappa_0(f(\operatorname{log}^\kappa_0(y))) + + The definition of mobius function application allows chaining as + + .. math:: + + y = \operatorname{exp}^\kappa_0(\operatorname{log}^\kappa_0(y)) + + Resulting in + + .. math:: + + (f \circ g)^{\otimes_\kappa}(x) + = + \operatorname{exp}^\kappa_0( + (f \circ g) (\operatorname{log}^\kappa_0(y)) + ) + + Parameters + ---------- + x : tensor + point on manifold + fns : callable[] + functions to apply + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Apply chain result + """ + if not fns: + return x + else: + ex = _logmap0(x, k, dim=dim) + for fn in fns: + ex = fn(ex) + y = _expmap0(ex, k, dim=dim) + return y + + +def mobius_fn_apply( + fn: callable, x: torch.Tensor, *args, k: torch.Tensor, dim=-1, **kwargs +): + r""" + Compute the generalization of function application in gyrovector spaces. + + First, a gyrovector is mapped to the tangent space (first-order approx.) via + :math:`\operatorname{log}^\kappa_0` and then the function is applied + to the vector in the tangent space. The resulting tangent vector is then + mapped back with :math:`\operatorname{exp}^\kappa_0`. + + .. math:: + + f^{\otimes_\kappa}(x) + = + \operatorname{exp}^\kappa_0(f(\operatorname{log}^\kappa_0(y))) + + .. plot:: plots/extended/stereographic/mobius_sigmoid_apply.py + + Parameters + ---------- + x : tensor + point on manifold + fn : callable + function to apply + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Result of function in hyperbolic space + """ + ex = _logmap0(x, k, dim=dim) + ex = fn(ex, *args, **kwargs) + y = _expmap0(ex, k, dim=dim) + return y + + +def mobiusify(fn: callable): + r""" + Wrap a function such that is works in gyrovector spaces. + + Parameters + ---------- + fn : callable + function in Euclidean space + + Returns + ------- + callable + function working in gyrovector spaces + + Notes + ----- + New function will accept additional argument ``k`` and ``dim``. + """ + + @functools.wraps(fn) + def mobius_fn(x, *args, k, dim=-1, **kwargs): + ex = _logmap0(x, k, dim=dim) + ex = fn(ex, *args, **kwargs) + y = _expmap0(ex, k, dim=dim) + return y + + return mobius_fn + + +def dist2plane( + x: torch.Tensor, + p: torch.Tensor, + a: torch.Tensor, + *, + k: torch.Tensor, + keepdim=False, + signed=False, + scaled=False, + dim=-1, +): + r""" + Geodesic distance from :math:`x` to a hyperplane :math:`H_{a, b}`. + + The hyperplane is such that its set of points is orthogonal to :math:`a` and + contains :math:`p`. + + .. plot:: plots/extended/stereographic/distance2plane.py + + To form an intuition what is a hyperplane in gyrovector spaces, let's first + consider an 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 gyrovector space + we replace :math:`+` with :math:`\oplus_\kappa`. + + Next, we should figure out what is :math:`\{a\}^\perp` in the gyrovector + space. + + 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 \mathcal{M}_\kappa^n` and + :math:`a\in T_p\mathcal{M}_\kappa^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\mathcal{M}_\kappa^n\backslash \{\mathbf{0}\}` + with :math:`p \in \mathcal{M}_\kappa^n`. We then define + + .. math:: + + \{a\}_p^\perp := \left\{ + z\in T_p\mathcal{M}_\kappa^n \;:\; \langle z, a\rangle_p = 0 + \right\} + + Recalling that a tangent vector :math:`z` for point :math:`p` yields + :math:`x = \operatorname{exp}^\kappa_p(z)` we rewrite the above equation as + + .. math:: + \{a\}_p^\perp := \left\{ + x\in \mathcal{M}_\kappa^n \;:\; \langle + \operatorname{log}_p^\kappa(x), a\rangle_p = 0 + \right\} + + This formulation is something more pleasant to work with. + Putting all together + + .. math:: + + \tilde{H}_{a, p}^\kappa = p + \{a\}^\perp_p\\ + = \left\{ + x \in \mathcal{M}_\kappa^n\;:\;\langle + \operatorname{log}^\kappa_p(x), + a\rangle_p = 0 + \right\} \\ + = \left\{ + x \in \mathcal{M}_\kappa^n\;:\;\langle -p \oplus_\kappa x, a\rangle + = 0 + \right\} + + To compute the distance :math:`d_\kappa(x, \tilde{H}_{a, p}^\kappa)` we find + + .. math:: + + d_\kappa(x, \tilde{H}_{a, p}^\kappa) + = + \inf_{w\in \tilde{H}_{a, p}^\kappa} d_\kappa(x, w)\\ + = + \sin^{-1}_\kappa\left\{ + \frac{ + 2 |\langle(-p)\oplus_\kappa x, a\rangle| + }{ + (1+\kappa\|(-p)\oplus_\kappa \|x\|^2_2)\|a\|_2 + } + \right\} + + Parameters + ---------- + x : tensor + point on manifold to compute distance for + a : tensor + hyperplane normal vector in tangent space of :math:`p` + p : tensor + point on manifold lying on the hyperplane + k : tensor + sectional curvature of manifold + keepdim : bool + retain the last dim? (default: false) + signed : bool + return signed distance + scaled : bool + scale distance by tangent norm + dim : int + reduction dimension for operations + + Returns + ------- + tensor + distance to the hyperplane + """ + return _dist2plane( + x, a, p, k, keepdim=keepdim, signed=signed, dim=dim, scaled=scaled + ) + + +@torch.jit.script +def _dist2plane( + x: torch.Tensor, + a: torch.Tensor, + p: torch.Tensor, + k: torch.Tensor, + keepdim: bool = False, + signed: bool = False, + scaled: bool = False, + dim: int = -1, +): + diff = _mobius_add(-p, x, k, dim=dim) + diff_norm2 = diff.pow(2).sum(dim=dim, keepdim=keepdim).clamp_min(1e-15) + 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.0 * sc_diff_a + denom = clamp_abs((1 + k * diff_norm2) * a_norm) + distance = arsin_k(num / denom, k) + if scaled: + distance = distance * a_norm + return distance + + +def parallel_transport( + x: torch.Tensor, y: torch.Tensor, v: torch.Tensor, *, k: torch.Tensor, dim=-1 +): + r""" + Compute the parallel transport of :math:`v` from :math:`x` to :math:`y`. + + The parallel transport is essential for adaptive algorithms on Riemannian + manifolds. For gyrovector spaces the parallel transport is expressed through + the gyration. + + .. plot:: plots/extended/stereographic/gyrovector_parallel_transport.py + + To recover parallel transport we first need to study isomorphisms 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 \mathcal{M}_\kappa^n` and + :math:`\operatorname{gyr}[a, b]c = \ominus (a \oplus_\kappa b) + \oplus_\kappa (a \oplus_\kappa (b \oplus_\kappa c))` + + But we want to obtain parallel transport for vectors, not for gyrovectors. + The blessing is the isomorphism mentioned above. This mapping is given by + + .. math:: + + U^\kappa_p \: : \: T_p\mathcal{M}_\kappa^n \to \mathbb{G} + = + v \mapsto \lambda^\kappa_p v + + + Finally, having the points :math:`x,\:y \in \mathcal{M}_\kappa^n` and a + tangent vector :math:`u\in T_x\mathcal{M}_\kappa^n` we obtain + + .. math:: + + P^\kappa_{x\to y}(v) + = + (U^\kappa_y)^{-1}\left(\operatorname{gyr}[y, -x] U^\kappa_x(v)\right)\\ + = + \operatorname{gyr}[y, -x] v \lambda^\kappa_x / \lambda^\kappa_y + + .. plot:: plots/extended/stereographic/parallel_transport.py + + + Parameters + ---------- + x : tensor + starting point + y : tensor + end point + v : tensor + tangent vector at x to be transported to y + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + transported vector + """ + return _parallel_transport(x, y, v, k, dim=dim) + + +@torch.jit.script +def _parallel_transport( + x: torch.Tensor, y: torch.Tensor, u: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + return ( + _gyration(y, -x, u, k, dim=dim) + * _lambda_x(x, k, keepdim=True, dim=dim) + / _lambda_x(y, k, keepdim=True, dim=dim) + ) + + +def parallel_transport0(y: torch.Tensor, v: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Compute the parallel transport of :math:`v` from the origin :math:`0` to :math:`y`. + + This is just a special case of the parallel transport with the starting + point at the origin that can be computed more efficiently and more + numerically stable. + + Parameters + ---------- + y : tensor + target point + v : tensor + vector to be transported from the origin to y + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + """ + return _parallel_transport0(y, v, k, dim=dim) + + +@torch.jit.script +def _parallel_transport0( + y: torch.Tensor, v: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + return v * (1 + k * y.pow(2).sum(dim=dim, keepdim=True)).clamp_min(1e-15) + + +def parallel_transport0back( + x: torch.Tensor, v: torch.Tensor, *, k: torch.Tensor, dim: int = -1 +): + r""" + Perform parallel transport to the zero point. + + Special case parallel transport with last point at zero that + can be computed more efficiently and numerically stable + + Parameters + ---------- + x : tensor + target point + v : tensor + vector to be transported + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + """ + return _parallel_transport0back(x, v, k=k, dim=dim) + + +@torch.jit.script +def _parallel_transport0back( + x: torch.Tensor, v: torch.Tensor, k: torch.Tensor, dim: int = -1 +): + return v / (1 + k * x.pow(2).sum(dim=dim, keepdim=True)).clamp_min(1e-15) + + +def egrad2rgrad(x: torch.Tensor, grad: torch.Tensor, *, k: torch.Tensor, dim=-1): + r""" + Convert the Euclidean gradient to the Riemannian gradient. + + .. math:: + + \nabla_x = \nabla^E_x / (\lambda_x^\kappa)^2 + + Parameters + ---------- + x : tensor + point on the manifold + grad : tensor + Euclidean gradient for :math:`x` + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + Riemannian gradient :math:`u\in T_x\mathcal{M}_\kappa^n` + """ + return _egrad2rgrad(x, grad, k, dim=dim) + + +@torch.jit.script +def _egrad2rgrad(x: torch.Tensor, grad: torch.Tensor, k: torch.Tensor, dim: int = -1): + return grad / _lambda_x(x, k, keepdim=True, dim=dim) ** 2 + + +def sproj(x: torch.Tensor, *, k: torch.Tensor, dim: int = -1): + """ + Stereographic Projection from hyperboloid or sphere. + + Parameters + ---------- + x : tensor + point to be projected + k : tensor + constant sectional curvature + dim : int + dimension to operate on + + Returns + ------- + tensor + the result of the projection + """ + return _sproj(x, k, dim=dim) + + +@torch.jit.script +def _sproj(x: torch.Tensor, k: torch.Tensor, dim: int = -1): + inv_r = torch.sqrt(sabs(k)) + factor = 1.0 / (1.0 + inv_r * x.narrow(dim, -1, 1)) + proj = factor * x.narrow(dim, 0, x.size(dim) - 1) + return proj + + +def inv_sproj(x: torch.Tensor, *, k: torch.Tensor, dim: int = -1): + """ + Inverse of Stereographic Projection to hyperboloid or sphere. + + Parameters + ---------- + x : tensor + point to be projected + k : tensor + constant sectional curvature + dim : int + dimension to operate on + + Returns + ------- + tensor + the result of the projection + """ + return _inv_sproj(x, k, dim=dim) + + +@torch.jit.script +def _inv_sproj(x: torch.Tensor, k: torch.Tensor, dim: int = -1): + inv_r = torch.sqrt(sabs(k)) + lam_x = _lambda_x(x, k, keepdim=True, dim=dim) + A = lam_x * x + B = 1.0 / inv_r * (lam_x - 1.0) + proj = torch.cat((A, B), dim=dim) + return proj + + +def antipode(x: torch.Tensor, *, k: torch.Tensor, dim: int = -1): + r""" + Compute the antipode of a point :math:`x_1,...,x_n` for :math:`\kappa > 0`. + + Let :math:`x` be a point on some sphere. Then :math:`-x` is its antipode. + Since we're dealing with stereographic projections, for :math:`sproj(x)` we + get the antipode :math:`sproj(-x)`. Which is given as follows: + + .. math:: + + \text{antipode}(x) + = + \frac{1+\kappa\|x\|^2_2}{2\kappa\|x\|^2_2}{}(-x) + + Parameters + ---------- + x : tensor + points :math:`x_1,...,x_n` on manifold to compute antipode for + k : tensor + sectional curvature of manifold + dim : int + reduction dimension for operations + + Returns + ------- + tensor + antipode + """ + return _antipode(x, k, dim=dim) + + +@torch.jit.script +def _antipode(x: torch.Tensor, k: torch.Tensor, dim: int = -1): + # NOTE: implementation that uses stereographic projections seems to be less accurate + # sproj(-inv_sproj(x)) + if torch.all(k.le(0)): + return -x + v = x / x.norm(p=2, dim=dim, keepdim=True).clamp_min(1e-15) + R = sabs(k).sqrt().reciprocal() + pi = 3.141592653589793 + + a = _geodesic_unit(pi * R, x, v, k, dim=dim) + return torch.where(k.gt(0), a, -x) + + +def weighted_midpoint( + xs: torch.Tensor, + weights: Optional[torch.Tensor] = None, + *, + k: torch.Tensor, + reducedim: Optional[List[int]] = None, + dim: int = -1, + keepdim: bool = False, + lincomb: bool = False, +): + r""" + Compute weighted Möbius gyromidpoint. + + The weighted Möbius gyromidpoint of a set of points + :math:`x_1,...,x_n` according to weights + :math:`\alpha_1,...,\alpha_n` is computed as follows: + + The weighted Möbius gyromidpoint is computed as follows + + .. math:: + + m_{\kappa}(x_1,\ldots,x_n,\alpha_1,\ldots,\alpha_n) + = + \frac{1}{2} + \otimes_\kappa + \left( + \sum_{i=1}^n + \frac{ + \alpha_i\lambda_{x_i}^\kappa + }{ + \sum_{j=1}^n\alpha_j(\lambda_{x_j}^\kappa-1) + } + x_i + \right) + + where the weights :math:`\alpha_1,...,\alpha_n` do not necessarily need + to sum to 1 (only their relative weight matters). Note that this formula + also requires to choose between the midpoint and its antipode for + :math:`\kappa > 0`. + + Parameters + ---------- + xs : tensor + points on poincare ball + weights : tensor + weights for averaging (make sure they broadcast correctly and manifold dimension is skipped) + reducedim : int|list|tuple + reduce dimension + dim : int + dimension to calculate conformal and Lorenz factors + k : tensor + constant sectional curvature + keepdim : bool + retain the last dim? (default: false) + lincomb : bool + linear combination implementation + + Returns + ------- + tensor + Einstein midpoint in poincare coordinates + """ + return _weighted_midpoint( + xs=xs, + k=k, + weights=weights, + reducedim=reducedim, + dim=dim, + keepdim=keepdim, + lincomb=lincomb, + ) + + +@torch.jit.script +def _weighted_midpoint( + xs: torch.Tensor, + k: torch.Tensor, + weights: Optional[torch.Tensor] = None, + reducedim: Optional[List[int]] = None, + dim: int = -1, + keepdim: bool = False, + lincomb: bool = False, +): + if reducedim is None: + reducedim = list_range(xs.dim()) + reducedim.pop(dim) + gamma = _lambda_x(xs, k=k, dim=dim, keepdim=True) + if weights is None: + weights = torch.tensor(1.0, dtype=xs.dtype, device=xs.device) + else: + weights = weights.unsqueeze(dim) + denominator = ((gamma - 1) * weights).sum(reducedim, keepdim=True) + zero = torch.tensor(0.0, dtype=xs.dtype, device=xs.device) + one = torch.tensor(1.0, dtype=xs.dtype, device=xs.device) + ill_conditioned = torch.isclose(denominator, zero, atol=1e-7) + if lincomb: + nominator = (gamma * weights * xs).sum(reducedim, keepdim=True) + two_mean = nominator / torch.where(ill_conditioned, one, denominator) + elif ill_conditioned.any(): + weights_denom = torch.where(ill_conditioned, weights + 1e-7, weights) + nominator = (gamma * weights * xs).sum(reducedim, keepdim=True) + denominator = ((gamma - 1) * weights_denom).sum(reducedim, keepdim=True) + two_mean = nominator / denominator + else: + nominator = (gamma * weights * xs).sum(reducedim, keepdim=True) + two_mean = nominator / clamp_abs(denominator) + a_mean = _mobius_scalar_mul( + torch.tensor(0.5, dtype=xs.dtype, device=xs.device), two_mean, k=k, dim=dim + ) + if torch.any(k.gt(0)): + # check antipode + b_mean = _antipode(a_mean, k, dim=dim) + a_dist = _dist(a_mean, xs, k=k, keepdim=True, dim=dim).sum( + reducedim, keepdim=True + ) + b_dist = _dist(b_mean, xs, k=k, keepdim=True, dim=dim).sum( + reducedim, keepdim=True + ) + better = k.gt(0) & (b_dist < a_dist) + a_mean = torch.where(better, b_mean, a_mean) + if lincomb: + if weights.numel() == 1: + alpha = weights.clone() + for d in reducedim: + alpha *= xs.size(d) + else: + weights, _ = torch.broadcast_tensors(weights, gamma) + alpha = weights.sum(reducedim, keepdim=True) + alpha = torch.where(ill_conditioned, one, alpha) + a_mean = _mobius_scalar_mul(alpha, a_mean, k=k, dim=dim) + if not keepdim: + a_mean = drop_dims(a_mean, reducedim) + return a_mean diff --git a/geoopt/utils.py b/geoopt/utils.py index a6c50761..7b08a3dc 100644 --- a/geoopt/utils.py +++ b/geoopt/utils.py @@ -1,6 +1,8 @@ import itertools -from typing import Tuple, Any, Union -import torch +from typing import Tuple, Any, Union, List +import torch.jit +import functools +import operator import geoopt __all__ = [ @@ -11,6 +13,14 @@ "broadcast_shapes", "ismanifold", "canonical_manifold", + "list_range", + "idx2sign", + "drop_dims", + "canonical_dims", + "sign", + "prod", + "clamp_abs", + "sabs", ] @@ -49,13 +59,88 @@ def strip_tuple(tup: Tuple) -> Union[Tuple, Any]: return tup -def make_tuple(obj: Union[Tuple, Any]) -> Tuple: +def make_tuple(obj: Union[Tuple, List, Any]) -> Tuple: + if isinstance(obj, list): + obj = tuple(obj) if not isinstance(obj, tuple): return (obj,) else: return obj +def prod(items): + return functools.reduce(operator.mul, items, 1) + + +@torch.jit.script +def sign(x): + return torch.sign(x.sign() + 0.5) + + +@torch.jit.script +def sabs(x, eps: float = 1e-15): + return x.abs().add_(eps) + + +@torch.jit.script +def clamp_abs(x, eps: float = 1e-15): + s = sign(x) + return s * sabs(x, eps=eps) + + +@torch.jit.script +def idx2sign(idx: int, dim: int, neg: bool = True): + """ + Unify idx to be negative or positive, that helps in cases of broadcasting. + + Parameters + ---------- + idx : int + current index + dim : int + maximum dimension + neg : bool + indicate we need negative index + + Returns + ------- + int + """ + if neg: + if idx < 0: + return idx + else: + return (idx + 1) % -(dim + 1) + else: + return idx % dim + + +@torch.jit.script +def drop_dims(tensor: torch.Tensor, dims: List[int]): + # Workaround to drop several dims in :func:`torch.squeeze`. + seen: int = 0 + for d in dims: + tensor = tensor.squeeze(d - seen) + seen += 1 + return tensor + + +@torch.jit.script +def list_range(end: int): + res: List[int] = [] + for d in range(end): + res.append(d) + return res + + +@torch.jit.script +def canonical_dims(dims: List[int], maxdim: int): + result: List[int] = [] + for idx in dims: + result.append(idx2sign(idx, maxdim, neg=False)) + return result + + def size2shape(*size: Union[Tuple[int], int]) -> Tuple[int]: return make_tuple(strip_tuple(size)) diff --git a/scripts/install_nix_win.sh b/scripts/install_nix_win.sh index d5e71656..1e09c973 100644 --- a/scripts/install_nix_win.sh +++ b/scripts/install_nix_win.sh @@ -5,7 +5,7 @@ if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then elif [[ "$TRAVIS_OS_NAME" == "windows" ]]; then echo "folder $MINICONDA_SUB_PATH does not exist" echo "installing miniconda for windows"; - choco install miniconda3 --params="'/JustMe /AddToPath:1 /D:$MINICONDA_PATH_WIN'"; + choco install miniconda3 --params="'/JustMe /AddToPath:1 /D:$MINICONDA_PATH_WIN'" -y; fi; # end installing miniconda diff --git a/setup.py b/setup.py index e43f33e4..4f1b18d7 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def get_version(*path): maintainer_email="maxim.v.kochurov@gmail.com", long_description=LONG_DESCRIPTION, packages=find_packages(), - install_requires=["torch>=1.2.0", "numpy"], + install_requires=["torch", "numpy"], version=get_version(PROJECT_ROOT, "geoopt", "__init__.py"), url="https://github.com/geoopt/geoopt", python_requires=">=3.6.0", diff --git a/tests/test_adam.py b/tests/test_adam.py index a78c12c2..260d19c4 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -35,14 +35,15 @@ def closure(): def test_adam_poincare(): torch.manual_seed(44) + manifold = geoopt.PoincareBall() 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()) + start = manifold.expmap0(start) + start = geoopt.ManifoldParameter(start, manifold=manifold) def closure(): optim.zero_grad() - loss = geoopt.manifolds.poincare.math.dist(start, ideal) ** 2 + loss = manifold.dist(start, ideal) ** 2 loss.backward() return loss.item() diff --git a/tests/test_gyrovector_math.py b/tests/test_gyrovector_math.py new file mode 100644 index 00000000..24d3b9b1 --- /dev/null +++ b/tests/test_gyrovector_math.py @@ -0,0 +1,685 @@ +""" +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 +import warnings +import itertools +from geoopt.manifolds import stereographic + + +@pytest.fixture("function", autouse=True, params=range(30, 40)) +def seed(request): + seed = request.param + torch.manual_seed(seed) + random.seed(seed) + np.random.seed(seed) + return seed + + +@pytest.fixture( + "function", params=[torch.float64, torch.float32], ids=["float64", "float32"] +) +def dtype(request): + return request.param + + +def tolerant_allclose_check(a, b, strict=True, **tolerance): + if strict: + np.testing.assert_allclose(a.detach(), b.detach(), **tolerance) + else: + try: + np.testing.assert_allclose(a.detach(), b.detach(), **tolerance) + except AssertionError as e: + assert not torch.isnan(a).any(), "Found nans" + assert not torch.isnan(b).any(), "Found nans" + warnings.warn("Unstable numerics: " + " | ".join(str(e).splitlines()[3:6])) + + +@pytest.fixture(params=[True, False], ids=["negative", "positive"]) +def negative(request): + return request.param + + +@pytest.fixture() +def strict(seed, dtype, negative): + return seed in {30, 31} and dtype == torch.float64 or negative + + +# c = -k +@pytest.fixture +def c(seed, dtype, negative): + # test broadcasted and non broadcasted versions + if seed == 30: # strict seed + c = torch.tensor(0.0).to(dtype) + elif seed == 31: # strict seed too + c = torch.tensor(1.0).to(dtype) + elif seed == 39: + c = 10 ** torch.arange(-15, 1, dtype=dtype)[:, None] + 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) + if not negative: + c = -c + return c.requires_grad_(True) + + +@pytest.fixture +def k(c): + return -c + + +@pytest.fixture +def manifold(k): + return stereographic.Stereographic(k=k, learnable=True) + + +@pytest.fixture +def B(c): + if c.dim() > 1: + return c.shape[0] + else: + return 100 + + +@pytest.fixture +def a(seed, c, manifold, B, dtype): + r = manifold.radius + a = torch.empty(B, 10, dtype=dtype).normal_(-1, 1) + a /= a.norm(dim=-1, keepdim=True) + a *= torch.where(torch.isfinite(r), r, torch.ones((), dtype=dtype)).clamp_max_(100) + a *= torch.rand_like(a) + return manifold.projx(a).detach().requires_grad_(True) + + +@pytest.fixture +def b(seed, c, manifold, B, dtype): + r = manifold.radius + a = torch.empty(B, 10, dtype=dtype).normal_(-1, 1) + a /= a.norm(dim=-1, keepdim=True) + a *= torch.where(torch.isfinite(r), r, torch.ones((), dtype=dtype)).clamp_max_(100) + a *= torch.rand_like(a) + return manifold.projx(a).detach().requires_grad_(True) + + +@pytest.fixture +def logunif_input(dtype): + inp = 10 ** torch.arange(-15, 1, dtype=dtype) + inp = torch.cat([-inp.flip(0), torch.zeros([1], dtype=dtype), inp]) + return inp.requires_grad_(True) + + +def test_tanh_grad(logunif_input): + stereographic.math.tanh(logunif_input).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + + +def test_artanh_grad(logunif_input): + stereographic.math.artanh(logunif_input).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + + +def test_arsinh_grad(logunif_input): + stereographic.math.arsinh(logunif_input).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + + +def test_tan_k_grad(logunif_input): + k = logunif_input.detach().clone().requires_grad_() + stereographic.math.tan_k(logunif_input[None], k[:, None]).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + assert torch.isfinite(k.grad).all() + + +def test_artan_k_grad(logunif_input): + k = logunif_input.detach().clone().requires_grad_() + stereographic.math.artan_k(logunif_input[None], k[:, None]).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + assert torch.isfinite(k.grad).all() + + +def test_arsin_k_grad(logunif_input): + k = logunif_input.detach().clone().requires_grad_() + stereographic.math.arsin_k(logunif_input[None], k[:, None]).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + assert torch.isfinite(k.grad).all() + + +def test_sin_k_grad(logunif_input): + k = logunif_input.detach().clone().requires_grad_() + stereographic.math.sin_k(logunif_input[None], k[:, None]).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + assert torch.isfinite(k.grad).all() + + +def test_project_k_grad(logunif_input): + vec = logunif_input[:, None] * torch.ones(logunif_input.shape[0], 10) + k = logunif_input.detach().clone().requires_grad_() + stereographic.math.project(vec, k=k[:, None]).sum().backward() + assert torch.isfinite(logunif_input.grad).all() + assert torch.isfinite(k.grad).all() + + +def test_mobius_addition_left_cancelation(a, b, manifold, dtype): + res = manifold.mobius_add(-a, manifold.mobius_add(a, b)) + tolerance = {torch.float32: dict(atol=5e-5, rtol=5e-4), torch.float64: dict()} + np.testing.assert_allclose(res.detach(), b.detach(), **tolerance[dtype]) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobius_addition_zero_a(b, manifold): + a = torch.zeros_like(b) + res = manifold.mobius_add(a, b) + np.testing.assert_allclose(res.detach(), b.detach()) + res.sum().backward() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobius_addition_zero_b(a, c, manifold): + b = torch.zeros_like(a) + res = manifold.mobius_add(a, b) + np.testing.assert_allclose(res.detach(), a.detach()) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobius_addition_negative_cancellation(a, manifold, dtype): + res = manifold.mobius_add(a, -a) + tolerance = { + torch.float32: dict(atol=1e-4, rtol=1e-6), + torch.float64: dict(atol=1e-6), + } + np.testing.assert_allclose(res.detach(), torch.zeros_like(res), **tolerance[dtype]) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobius_negative_addition(a, b, manifold, dtype): + res = manifold.mobius_add(-b, -a) + res1 = -manifold.mobius_add(b, a) + tolerance = { + torch.float32: dict(atol=1e-7, rtol=1e-6), + torch.float64: dict(atol=1e-10), + } + + np.testing.assert_allclose(res.detach(), res1.detach(), **tolerance[dtype]) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +@pytest.mark.parametrize("n", list(range(5))) +def test_n_additions_via_scalar_multiplication(n, a, dtype, negative, manifold, strict): + n = torch.as_tensor(n, dtype=a.dtype).requires_grad_() + y = torch.zeros_like(a) + for _ in range(int(n.item())): + y = manifold.mobius_add(a, y) + ny = manifold.mobius_scalar_mul(n, a) + if negative: + tolerance = { + torch.float32: dict(atol=4e-5, rtol=1e-3), + torch.float64: dict(atol=1e-5, rtol=1e-3), + } + else: + tolerance = { + torch.float32: dict(atol=2e-6, rtol=1e-3), + torch.float64: dict(atol=1e-5, rtol=1e-3), + } + tolerant_allclose_check(y, ny, strict=strict, **tolerance[dtype]) + ny.sum().backward() + assert torch.isfinite(n.grad).all() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +@pytest.fixture +def r1(seed, dtype, B): + if seed % 3 == 0: + return ( + torch.tensor(random.uniform(-1, 1), dtype=dtype) + .detach() + .requires_grad_(True) + ) + else: + return (torch.rand(B, 1, dtype=dtype) * 2 - 1).detach().requires_grad_(True) + + +@pytest.fixture +def r2(seed, dtype, B): + if seed % 3 == 1: + return ( + torch.tensor(random.uniform(-1, 1), dtype=dtype) + .detach() + .requires_grad_(True) + ) + else: + return (torch.rand(B, 1, dtype=dtype) * 2 - 1).detach().requires_grad_(True) + + +def test_scalar_multiplication_distributive(a, r1, r2, manifold, dtype): + res = manifold.mobius_scalar_mul(r1 + r2, a) + res1 = manifold.mobius_add( + manifold.mobius_scalar_mul(r1, a), manifold.mobius_scalar_mul(r2, a), + ) + res2 = manifold.mobius_add( + manifold.mobius_scalar_mul(r1, a), manifold.mobius_scalar_mul(r2, a), + ) + tolerance = { + torch.float32: dict(atol=5e-6, rtol=1e-4), + torch.float64: dict(atol=1e-7, rtol=1e-4), + } + np.testing.assert_allclose(res1.detach(), res.detach(), **tolerance[dtype]) + np.testing.assert_allclose(res2.detach(), res.detach(), **tolerance[dtype]) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(r1.grad).all() + assert torch.isfinite(r2.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_scalar_multiplication_associative(a, r1, r2, manifold, dtype): + res = manifold.mobius_scalar_mul(r1 * r2, a) + res1 = manifold.mobius_scalar_mul(r1, manifold.mobius_scalar_mul(r2, a)) + res2 = manifold.mobius_scalar_mul(r2, manifold.mobius_scalar_mul(r1, a)) + tolerance = { + torch.float32: dict(atol=1e-5, rtol=1e-5), + torch.float64: dict(atol=1e-7, rtol=1e-7), + } + np.testing.assert_allclose(res1.detach(), res.detach(), **tolerance[dtype]) + np.testing.assert_allclose(res2.detach(), res.detach(), **tolerance[dtype]) + res.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(r1.grad).all() + assert torch.isfinite(r2.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_scaling_property(a, r1, manifold, dtype): + x1 = a / a.norm(dim=-1, keepdim=True) + ra = manifold.mobius_scalar_mul(r1, a) + x2 = manifold.mobius_scalar_mul(abs(r1), a) / 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.detach(), x2.detach(), **tolerance[dtype]) + x2.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(r1.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_geodesic_borders(a, b, manifold, dtype): + geo0 = manifold.geodesic(torch.tensor(0.0, dtype=dtype), a, b) + geo1 = manifold.geodesic(torch.tensor(1.0, dtype=dtype), a, b) + tolerance = { + torch.float32: dict(rtol=1e-5, atol=5e-5), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose(geo0.detach(), a.detach(), **tolerance[dtype]) + np.testing.assert_allclose(geo1.detach(), b.detach(), **tolerance[dtype]) + (geo0 + geo1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_geodesic_segment_length_property(a, b, manifold, dtype): + extra_dims = len(a.shape) + segments = 12 + t = torch.linspace(0, 1, segments + 1, dtype=dtype).view( + (segments + 1,) + (1,) * extra_dims + ) + gamma_ab_t = manifold.geodesic(t, a, b) + gamma_ab_t0 = gamma_ab_t[:-1] + gamma_ab_t1 = gamma_ab_t[1:] + dist_ab_t0mt1 = manifold.dist(gamma_ab_t0, gamma_ab_t1, keepdim=True) + speed = manifold.dist(a, b, keepdim=True).unsqueeze(0).expand_as(dist_ab_t0mt1) + # we have exactly 12 line segments + tolerance = { + torch.float32: dict(rtol=1e-5, atol=5e-3), + torch.float64: dict(rtol=1e-5, atol=5e-3), + } + length = speed / segments + np.testing.assert_allclose( + dist_ab_t0mt1.detach(), length.detach(), **tolerance[dtype] + ) + (length + dist_ab_t0mt1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_geodesic_segement_unit_property(a, b, manifold, dtype): + extra_dims = len(a.shape) + segments = 12 + t = torch.linspace(0, 1, segments + 1, dtype=dtype).view( + (segments + 1,) + (1,) * extra_dims + ) + gamma_ab_t = manifold.geodesic_unit(t, a, b) + gamma_ab_t0 = gamma_ab_t[:1] + gamma_ab_t1 = gamma_ab_t + dist_ab_t0mt1 = manifold.dist(gamma_ab_t0, gamma_ab_t1, keepdim=True) + true_distance_travelled = t.expand_as(dist_ab_t0mt1) + # we have exactly 12 line segments + tolerance = { + torch.float32: dict(atol=2e-4, rtol=5e-5), + torch.float64: dict(atol=1e-10), + } + np.testing.assert_allclose( + dist_ab_t0mt1.detach(), true_distance_travelled.detach(), **tolerance[dtype] + ) + (true_distance_travelled + dist_ab_t0mt1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_expmap_logmap(a, b, manifold, dtype): + # this test appears to be numerical unstable once a and b may appear on the opposite sides + bh = manifold.expmap(x=a, u=manifold.logmap(a, b)) + tolerance = {torch.float32: dict(rtol=1e-5, atol=5e-5), torch.float64: dict()} + np.testing.assert_allclose(bh.detach(), b.detach(), **tolerance[dtype]) + bh.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_expmap0_logmap0(a, manifold, dtype): + # this test appears to be numerical unstable once a and b may appear on the opposite sides + v = manifold.logmap0(a) + norm = manifold.norm(torch.zeros_like(v), v, keepdim=True) + dist = manifold.dist0(a, keepdim=True) + bh = manifold.expmap0(v) + tolerance = {torch.float32: dict(atol=1e-5, rtol=1e-5), torch.float64: dict()} + np.testing.assert_allclose(bh.detach(), a.detach(), **tolerance[dtype]) + np.testing.assert_allclose(norm.detach(), dist.detach(), **tolerance[dtype]) + (bh.sum() + dist.sum()).backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_matvec_zeros(a, manifold): + mat = a.new_zeros((3, a.shape[-1])) + z = manifold.mobius_matvec(mat, a) + np.testing.assert_allclose(z.detach(), 0.0) + z.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_matvec_via_equiv_fn_apply(a, negative, manifold, strict, dtype): + mat = a.new(3, a.shape[-1]).normal_() + y = manifold.mobius_fn_apply(lambda x: x @ mat.transpose(-1, -2), a) + y1 = manifold.mobius_matvec(mat, a) + tolerance = {torch.float32: dict(atol=1e-5), torch.float64: dict()} + + tolerant_allclose_check(y, y1, strict=strict, **tolerance[dtype]) + y.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobiusify(a, c, negative, strict, dtype): + mat = a.new(3, a.shape[-1]).normal_() + + @stereographic.math.mobiusify + def matvec(x): + return x @ mat.transpose(-1, -2) + + y = matvec(a, k=-c) + y1 = stereographic.math.mobius_matvec(mat, a, k=-c) + tolerance = {torch.float32: dict(atol=1e-5), torch.float64: dict()} + + tolerant_allclose_check(y, y1, strict=strict, **tolerance[dtype]) + y.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(c.grad).all() + + +def test_matvec_chain_via_equiv_fn_apply(a, negative, manifold, dtype): + mat1 = a.new(a.shape[-1], a.shape[-1]).normal_() + mat2 = a.new(a.shape[-1], a.shape[-1]).normal_() + y = manifold.mobius_fn_apply_chain( + a, lambda x: x @ mat1.transpose(-1, -2), lambda x: x @ mat2.transpose(-1, -2), + ) + y1 = manifold.mobius_matvec(mat1, a) + y1 = manifold.mobius_matvec(mat2, y1) + tolerance = {torch.float32: dict(atol=1e-5, rtol=1e-5), torch.float64: dict()} + + tolerant_allclose_check(y, y1, strict=negative, **tolerance[dtype]) + y.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_transp0_preserves_inner_products(a, manifold): + # 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 = manifold.transp0(a, v_0) + u_a = manifold.transp0(a, u_0) + # compute norms + vu_0 = manifold.inner(zero, v_0, u_0, keepdim=True) + vu_a = manifold.inner(a, v_a, u_a, keepdim=True) + np.testing.assert_allclose(vu_a.detach(), vu_0.detach(), atol=1e-6, rtol=1e-6) + (vu_0 + vu_a).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_transp0_is_same_as_usual(a, manifold): + # pointing to the center + v_0 = torch.rand_like(a) + 1e-5 + zero = torch.zeros_like(a) + v_a = manifold.transp0(a, v_0) + v_a1 = manifold.transp(zero, a, v_0) + # compute norms + np.testing.assert_allclose(v_a.detach(), v_a1.detach(), atol=1e-6, rtol=1e-6) + (v_a + v_a1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_transp_a_b(a, b, manifold): + # pointing to the center + v_0 = torch.rand_like(a) + u_0 = torch.rand_like(a) + v_1 = manifold.transp(a, b, v_0) + u_1 = manifold.transp(a, b, u_0) + # compute norms + vu_1 = manifold.inner(b, v_1, u_1, keepdim=True) + vu_0 = manifold.inner(a, v_0, u_0, keepdim=True) + np.testing.assert_allclose(vu_0.detach(), vu_1.detach(), atol=1e-6, rtol=1e-6) + (vu_0 + vu_1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_add_infinity_and_beyond(a, b, c, negative, manifold, dtype): + _a = a + if torch.isclose(c, c.new_zeros(())).any(): + pytest.skip("zero not checked") + infty = b * 10000000 + for i in range(100): + z = manifold.expmap(a, infty, project=False) + z = manifold.projx(z) + assert not torch.isnan(z).any(), ("Found nans", i, z) + assert torch.isfinite(z).all(), ("Found Infs", i, z) + z = manifold.mobius_scalar_mul( + torch.tensor(1000.0, dtype=z.dtype), z, project=False + ) + z = manifold.projx(z) + assert not torch.isnan(z).any(), ("Found nans", i, z) + assert torch.isfinite(z).all(), ("Found Infs", i, z) + + infty = manifold.transp(a, z, infty) + assert torch.isfinite(infty).all(), (i, infty) + a = z + z = manifold.expmap(a, -infty) + # 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), + } + if negative: + np.testing.assert_allclose(z.detach(), -a.detach(), **tolerance[dtype]) + else: + assert not torch.isnan(z).any(), "Found nans" + assert not torch.isnan(a).any(), "Found nans" + + +def test_mobius_coadd(a, b, negative, manifold, strict): + # (a \boxplus_c b) \ominus_c b = a + ah = manifold.mobius_sub(manifold.mobius_coadd(a, b), b) + tolerant_allclose_check(a, ah, strict=strict, atol=5e-5) + ah.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_mobius_cosub(a, b, negative, manifold, strict): + # (a \oplus_c b) \boxminus b = a + ah = manifold.mobius_cosub(manifold.mobius_add(a, b), b) + tolerant_allclose_check(a, ah, strict=strict, atol=1e-5) + ah.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(b.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_distance2plane(a, manifold): + v = torch.rand_like(a).requires_grad_() + vr = v / manifold.norm(a, v, keepdim=True) + z = manifold.expmap(a, vr) + dist1 = manifold.dist(a, z) + dist = manifold.dist2plane(z, a, vr) + + np.testing.assert_allclose(dist.detach(), dist1.detach(), atol=2e-4, rtol=1e-4) + (dist + dist1).sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(v.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_sproj(manifold, a): + ma = manifold.sproj(manifold.inv_sproj(a)) + np.testing.assert_allclose(ma.detach(), a.detach(), atol=1e-5) + ma.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +def test_antipode(manifold, negative, a, dtype, seed): + if seed == 39: + pytest.skip("This is amazingly unstable when tested against extreme values") + ma = manifold.antipode(a) + if manifold.k.le(0).all(): + np.testing.assert_allclose(ma.detach(), -a.detach()) + else: + s = manifold.inv_sproj(a) + ms = manifold.inv_sproj(ma) + tolerance = {torch.float32: dict(atol=1e-5), torch.float64: dict(atol=1e-6)} + np.testing.assert_allclose(ms.detach(), -s.detach(), **tolerance[dtype]) + ma.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +@pytest.mark.parametrize("_k,lincomb", itertools.product([-1, 0, 1], [True, False])) +def test_weighted_midpoint(_k, lincomb): + manifold = stereographic.Stereographic(_k, learnable=True) + a = manifold.random(2, 3, 10).requires_grad_(True) + mid = manifold.weighted_midpoint(a, lincomb=lincomb) + assert torch.isfinite(mid).all() + assert mid.shape == (a.shape[-1],) + mid.sum().backward() + assert torch.isfinite(a.grad).all() + assert not torch.isclose(manifold.k.grad, manifold.k.new_zeros(())) + + +@pytest.mark.parametrize("_k,lincomb", itertools.product([-1, 0, 1], [True, False])) +def test_weighted_midpoint_reduce_dim(_k, lincomb): + manifold = stereographic.Stereographic(_k, learnable=True) + a = manifold.random(2, 3, 10).requires_grad_(True) + mid = manifold.weighted_midpoint(a, reducedim=[0], lincomb=lincomb) + assert mid.shape == a.shape[-2:] + assert torch.isfinite(mid).all() + mid.sum().backward() + assert torch.isfinite(a.grad).all() + assert not torch.isclose(manifold.k.grad, manifold.k.new_zeros(())) + + +@pytest.mark.parametrize("_k,lincomb", itertools.product([-1, 0, 1], [True, False])) +def test_weighted_midpoint_weighted(_k, lincomb): + manifold = stereographic.Stereographic(_k, learnable=True) + a = manifold.random(2, 3, 10).requires_grad_(True) + mid = manifold.weighted_midpoint( + a, reducedim=[0], lincomb=lincomb, weights=torch.rand_like(a[..., 0]) + ) + assert mid.shape == a.shape[-2:] + assert torch.isfinite(mid).all() + mid.sum().backward() + assert torch.isfinite(a.grad).all() + assert not torch.isclose(manifold.k.grad, manifold.k.new_zeros(())) + + +@pytest.mark.parametrize("_k,lincomb", itertools.product([-1, 0, 1], [True, False])) +def test_weighted_midpoint_zero(_k, lincomb): + manifold = stereographic.Stereographic(_k, learnable=True) + a = manifold.random(2, 3, 10).requires_grad_(True) + mid = manifold.weighted_midpoint( + a, reducedim=[0], lincomb=lincomb, weights=torch.zeros_like(a[..., 0]) + ) + assert mid.shape == a.shape[-2:] + assert torch.allclose(mid, torch.zeros_like(mid)) + mid.sum().backward() + assert torch.isfinite(a.grad).all() + assert torch.isfinite(manifold.k.grad).all() + + +@pytest.mark.parametrize("lincomb", [True, False]) +def test_weighted_midpoint_euclidean(lincomb): + manifold = stereographic.Stereographic(0) + a = manifold.random(2, 3, 10).requires_grad_(True) + mid = manifold.weighted_midpoint(a, reducedim=[0], lincomb=lincomb) + assert mid.shape == a.shape[-2:] + if lincomb: + assert torch.allclose(mid, a.sum(0)) + else: + assert torch.allclose(mid, a.mean(0)) + + +@pytest.mark.parametrize("_k,lincomb", itertools.product([-1, 0, 1], [True, False])) +def test_weighted_midpoint_weighted_zero_sum(_k, lincomb): + manifold = stereographic.Stereographic(_k, learnable=True) + a = manifold.expmap0(torch.eye(3, 10)).detach().requires_grad_(True) + weights = torch.rand_like(a[..., 0]) + weights = weights - weights.sum() / weights.numel() + mid = manifold.weighted_midpoint(a, lincomb=lincomb, weights=weights) + if _k == 0 and lincomb: + np.testing.assert_allclose( + mid.detach(), + torch.cat([weights, torch.zeros(a.size(-1) - a.size(0))]), + atol=1e-6, + ) + assert mid.shape == a.shape[-1:] + assert torch.isfinite(mid).all() + mid.sum().backward() + assert torch.isfinite(a.grad).all() diff --git a/tests/test_manifold_basic.py b/tests/test_manifold_basic.py index 2a9de763..96031d12 100644 --- a/tests/test_manifold_basic.py +++ b/tests/test_manifold_basic.py @@ -22,6 +22,8 @@ def use_floatX(request): manifold_shapes = { geoopt.manifolds.PoincareBall: (3,), + geoopt.manifolds.Stereographic: (3,), + geoopt.manifolds.SphereProjection: (3,), geoopt.manifolds.EuclideanStiefel: (10, 5), geoopt.manifolds.BirkhoffPolytope: (4, 4), geoopt.manifolds.CanonicalStiefel: (10, 5), @@ -165,6 +167,42 @@ def poincare_case(): yield case +def stereographic_case(): + torch.manual_seed(42) + shape = manifold_shapes[geoopt.manifolds.Stereographic] + ex = torch.randn(*shape, dtype=torch.float64) / 3 + ev = torch.randn(*shape, dtype=torch.float64) / 3 + x = ex # default curvature = 0 + ex = x.clone() + v = ev.clone() + manifold = geoopt.Stereographic().to(dtype=torch.float64) + x = geoopt.ManifoldTensor(x, manifold=manifold) + case = UnaryCase(shape, x, ex, v, ev, manifold) + yield case + manifold = geoopt.StereographicExact().to(dtype=torch.float64) + x = geoopt.ManifoldTensor(x, manifold=manifold) + case = UnaryCase(shape, x, ex, v, ev, manifold) + yield case + + +def sphere_projection_case(): + torch.manual_seed(42) + shape = manifold_shapes[geoopt.manifolds.SphereProjection] + ex = torch.randn(*shape, dtype=torch.float64) / 3 + ev = torch.randn(*shape, dtype=torch.float64) / 3 + x = ex # default curvature = 0 + ex = x.clone() + v = ev.clone() + manifold = geoopt.manifolds.SphereProjection().to(dtype=torch.float64) + x = geoopt.ManifoldTensor(x, manifold=manifold) + case = UnaryCase(shape, x, ex, v, ev, manifold) + yield case + manifold = geoopt.manifolds.SphereProjectionExact().to(dtype=torch.float64) + x = geoopt.ManifoldTensor(x, manifold=manifold) + case = UnaryCase(shape, x, ex, v, ev, manifold) + yield case + + def sphere_subspace_case(): torch.manual_seed(42) shape = manifold_shapes[geoopt.manifolds.Sphere] @@ -295,7 +333,9 @@ def scaled(request): sphere_subspace_case(), euclidean_stiefel_case(), canonical_stiefel_case(), + stereographic_case(), poincare_case(), + sphere_projection_case(), product_case(), birkhoff_case(), ), diff --git a/tests/test_poincare_math.py b/tests/test_poincare_math.py deleted file mode 100644 index ef09596b..00000000 --- a/tests/test_poincare_math.py +++ /dev/null @@ -1,371 +0,0 @@ -""" -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) - np.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 - 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_scaling.py b/tests/test_scaling.py index e91f8e73..cd8f8ab4 100644 --- a/tests/test_scaling.py +++ b/tests/test_scaling.py @@ -50,7 +50,7 @@ def test_rescaling_methods_accessible(): rsball = geoopt.Scaled(sball, 0.5) v0 = torch.arange(10).float() / 10 v1 = -torch.arange(10).float() / 10 - rsball.geodesic(0.5, v0, v1) + rsball.geodesic(torch.tensor(0.5), v0, v1) def test_scaling_getattr(): @@ -58,7 +58,7 @@ def test_scaling_getattr(): sball = geoopt.Scaled(ball, 2) pa, pb = sball.random(2, 10) # this one is representative and not present in __scaling__ - sball.geodesic(0.5, pa, pb) + sball.geodesic(torch.tensor(0.5), pa, pb) def test_scaling_not_implemented():