-
Notifications
You must be signed in to change notification settings - Fork 1
/
riemann_surface.py
3798 lines (3164 loc) · 154 KB
/
riemann_surface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
r"""
Riemann matrices and endomorphism rings of algebraic Riemann surfaces
This module provides a class, :class:`RiemannSurface`, to model the
Riemann surface determined by a plane algebraic curve over a subfield
of the complex numbers.
A homology basis is derived from the edges of a Voronoi cell decomposition based
on the branch locus. The pull-back of these edges to the Riemann surface
provides a graph on it that contains a homology basis.
The class provides methods for computing the Riemann period matrix of the
surface numerically, using a certified homotopy continuation method due to
[Kr2016]_.
The class also provides facilities for computing the endomorphism ring of the
period lattice numerically, by determining integer (near) solutions to the
relevant approximate linear equations.
One can also calculate the Abel-Jacobi map on the Riemann surface, and there
is basic functionality to interface with divisors of curves to facilitate this.
AUTHORS:
- Alexandre Zotine, Nils Bruin (2017-06-10): initial version
- Nils Bruin, Jeroen Sijsling (2018-01-05): algebraization, isomorphisms
- Linden Disney-Hogg, Nils Bruin (2021-06-23): efficient integration
EXAMPLES:
We compute the Riemann matrix of a genus 3 curve::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<x,y> = QQ[]
sage: f = x^4-x^3*y+2*x^3+2*x^2*y+2*x^2-2*x*y^2+4*x*y-y^3+3*y^2+2*y+1
sage: S = RiemannSurface(f, prec=100)
sage: M = S.riemann_matrix()
We test the usual properties, i.e., that the period matrix is symmetric and that
the imaginary part is positive definite::
sage: all(abs(a) < 1e-20 for a in (M-M.T).list())
True
sage: iM = Matrix(RDF,3,3,[a.imag_part() for a in M.list()])
sage: iM.is_positive_definite()
True
We compute the endomorphism ring and check it has `\ZZ`-rank 6::
sage: A = S.endomorphism_basis(80,8)
sage: len(A) == 6
True
In fact it is an order in a number field::
sage: T.<t> = QQ[]
sage: K.<a> = NumberField(t^6 - t^5 + 2*t^4 + 8*t^3 - t^2 - 5*t + 7)
sage: all(len(a.minpoly().roots(K)) == a.minpoly().degree() for a in A)
True
We can look at an extended example of the Abel-Jacobi functionality. We will
demonstrate a particular half-canonical divisor on Klein's Curve, known in
the literature.::
sage: f = x^3*y + y^3 + x
sage: S = RiemannSurface(f, integration_method='rigorous')
sage: BL = S.places_at_branch_locus(); BL
[Place (x, y, y^2),
Place (x^7 + 27/4, y + 4/9*x^5, y^2 + 4/3*x^3),
Place (x^7 + 27/4, y - 2/9*x^5, y^2 + 1/3*x^3)]
We can read off out the output of ``places_at_branch_locus`` to choose our
divisor, and we can calculate the canonical divisor using curve functionality::
sage: P0 = 1*BL[0]
sage: from sage.schemes.curves.constructor import Curve
sage: C = Curve(f)
sage: F = C.function_field()
sage: K = (F(x).differential()).divisor() - F(f.derivative(y)).divisor()
sage: Pinf, Pinf_prime = C.places_at_infinity()
sage: if K-3*Pinf-1*Pinf_prime: Pinf, Pinf_prime = (Pinf_prime, Pinf);
sage: D = P0 + 2*Pinf - Pinf_prime
Note we could check using exact techniques that `2D=K`::
sage: Z = K - 2*D
sage: (Z.degree()==0, len(Z.basis_differential_space())==S.genus, len(Z.basis_function_space())==1)
(True, True, True)
We can also check this using our Abel-Jacobi functions::
sage: avoid = C.places_at_infinity()
sage: Zeq, _ = S.strong_approximation(Z, avoid)
sage: Zlist = S.divisor_to_divisor_list(Zeq)
sage: AJ = S.abel_jacobi(Zlist) # long time (1 second)
sage: S.reduce_over_period_lattice(AJ).norm() < 1e-10 # long time
True
REFERENCES:
The initial version of this code was developed alongside [BSZ2019]_.
"""
# ****************************************************************************
# Copyright (C) 2017 Alexandre Zotine, Nils Bruin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# https://www.gnu.org/licenses/
# ****************************************************************************
from scipy.spatial import Voronoi
from sage.arith.functions import lcm
from sage.arith.misc import GCD, algdep
from sage.ext.fast_callable import fast_callable
from sage.functions.log import lambert_w
from sage.graphs.graph import Graph
from sage.groups.matrix_gps.finitely_generated import MatrixGroup
from sage.groups.perm_gps.permgroup_named import SymmetricGroup
from sage.matrix.constructor import Matrix
from sage.matrix.special import block_matrix
from sage.misc.cachefunc import cached_method
from sage.misc.flatten import flatten
from sage.misc.functional import numerical_approx
from sage.misc.misc_c import prod
from sage.modules.free_module import VectorSpace
from sage.modules.free_module_integer import IntegerLattice
from sage.numerical.gauss_legendre import integrate_vector, integrate_vector_N
from sage.rings.complex_mpfr import ComplexField, CDF
from sage.rings.function_field.constructor import FunctionField
from sage.rings.infinity import Infinity
from sage.rings.integer_ring import ZZ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from sage.rings.qqbar import number_field_elements_from_algebraics
from sage.rings.rational_field import QQ
from sage.rings.real_mpfr import RealField
from sage.schemes.curves.constructor import Curve
import sage.libs.mpmath.all as mpall
def voronoi_ghost(cpoints, n=6, CC=CDF):
r"""
Convert a set of complex points to a list of real tuples `(x,y)`, and
appends n points in a big circle around them.
The effect is that, with n >= 3, a Voronoi decomposition will have only
finite cells around the original points. Furthermore, because the extra
points are placed on a circle centered on the average of the given points,
with a radius 3/2 times the largest distance between the center and the
given points, these finite cells form a simply connected region.
INPUT:
- ``cpoints`` -- a list of complex numbers
OUTPUT:
A list of real tuples `(x,y)` consisting of the original points and a set of
points which surround them.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import voronoi_ghost
sage: L = [1 + 1*I, 1 - 1*I, -1 + 1*I, -1 - 1*I]
sage: voronoi_ghost(L) # abs tol 1e-6
[(1.0, 1.0),
(1.0, -1.0),
(-1.0, 1.0),
(-1.0, -1.0),
(2.121320343559643, 0.0),
(1.0606601717798216, 1.8371173070873836),
(-1.060660171779821, 1.8371173070873839),
(-2.121320343559643, 2.59786816870648e-16),
(-1.0606601717798223, -1.8371173070873832),
(1.06066017177982, -1.8371173070873845)]
"""
cpoints = [CC(c) for c in cpoints]
average = sum(cpoints) / len(cpoints)
if len(cpoints) == 1:
radius = 1
else:
radius = 3 * max(abs(c - average) for c in cpoints) / 2
z = CC.zeta(n)
extra_points = [average + radius * z**i for i in range(n)]
return [tuple(c) for c in cpoints + extra_points]
def bisect(L, t):
r"""
Find position in a sorted list using bisection.
Given a list `L = [(t_0,...),(t_1,...),...(t_n,...)]` with increasing `t_i`,
find the index i such that `t_i <= t < t_{i+1}` using bisection. The rest of
the tuple is available for whatever use required.
INPUT:
- ``L`` -- A list of tuples such that the first term of each tuple is a real
number between 0 and 1. These real numbers must be increasing.
- ``t`` -- A real number between `t_0` and `t_n`.
OUTPUT:
An integer i, giving the position in L where t would be in
EXAMPLES:
Form a list of the desired form, and pick a real number between 0 and 1::
sage: from sage.schemes.riemann_surfaces.riemann_surface import bisect
sage: L = [(0.0, 'a'), (0.3, 'b'), (0.7, 'c'), (0.8, 'd'), (0.9, 'e'), (1.0, 'f')]
sage: t = 0.5
sage: bisect(L,t)
1
Another example which demonstrates that if t is equal to one of the t_i, it
returns that index::
sage: L = [(0.0, 'a'), (0.1, 'b'), (0.45, 'c'), (0.5, 'd'), (0.65, 'e'), (1.0, 'f')]
sage: t = 0.5
sage: bisect(L,t)
3
"""
# Defining starting indices for the loop.
min = 0
max = len(L) - 1
# If the input t is not between 0 and 1, raise an error.
if t < L[min][0] or t > L[max][0]:
raise ValueError("value for t out of range")
# Main loop.
while (min < max-1):
# Bisect.
mid = (max+min)//2
# If it's equal, return the index we bisected to.
if t == L[mid][0]:
return mid
# If it's smaller, then we're on the left side.
elif t < L[mid][0]:
max = mid
# Otherwise we're on the right side.
else:
min = mid
# Once the loop terminates, we return what the indices converged to.
return min
def numerical_inverse(C):
"""
Compute numerical inverse of a matrix via LU decomposition
INPUT:
- ``C`` -- A real or complex invertible square matrix
EXAMPLES::
sage: C = matrix(CC,3,3,[-4.5606e-31 + 1.2326e-31*I,
....: -0.21313 + 0.24166*I,
....: -3.4513e-31 + 0.16111*I,
....: -1.0175 + 9.8608e-32*I,
....: 0.30912 + 0.19962*I,
....: -4.9304e-32 + 0.39923*I,
....: 0.96793 - 3.4513e-31*I,
....: -0.091587 + 0.19276*I,
....: 3.9443e-31 + 0.38552*I])
sage: from sage.schemes.riemann_surfaces.riemann_surface import numerical_inverse
sage: max(abs(c) for c in (C^(-1)*C-C^0).list()) < 1e-10
False
sage: max(abs(c) for c in (numerical_inverse(C)*C-C^0).list()) < 1e-10
True
"""
R = C.parent()
prec = R.base_ring().prec()
mpall.mp.prec = prec
with mpall.workprec(prec):
Cmp = mpall.matrix([mpall.sage_to_mpmath(list(c), prec) for c in C])
PLU = mpall.lu(Cmp)
P, L, U = [R([mpall.mpmath_to_sage(c, prec) for c in M]) for M in PLU]
return U.inverse() * L.inverse() * P
class ConvergenceError(ValueError):
r"""
Error object suitable for raising and catching when Newton iteration fails.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import ConvergenceError
sage: raise ConvergenceError("test")
Traceback (most recent call last):
...
ConvergenceError: test
sage: isinstance(ConvergenceError(),ValueError)
True
"""
pass
def differential_basis_baker(f):
r"""
Compute a differential bases for a curve that is nonsingular outside (1:0:0),(0:1:0),(0:0:1)
Baker's theorem tells us that if a curve has its singularities at the coordinate vertices and meets
some further easily tested genericity criteria,
then we can read off a basis for the regular differentials from the interior of the
Newton polygon spanned by the monomials. While this theorem only applies to special plane curves
it is worth implementing because the analysis is relatively cheap and it applies to a lot of
commonly encountered curves (e.g., curves given by a hyperelliptic model). Other advantages include
that we can do the computation over any exact base ring (the alternative Singular based method for
computing the adjoint ideal requires the rationals), and that we can avoid being affected by subtle bugs
in the Singular code.
``None`` is returned when ``f`` does not describe a curve of the relevant type. If ``f`` is of the relevant
type, but is of genus `0` then ``[]`` is returned (which are both False values, but they are not equal).
INPUT:
- `f` -- a bivariate polynomial
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import differential_basis_baker
sage: R.<x,y> = QQ[]
sage: f = x^3+y^3+x^5*y^5
sage: differential_basis_baker(f)
[y^2, x*y, x*y^2, x^2, x^2*y, x^2*y^2, x^2*y^3, x^3*y^2, x^3*y^3]
sage: f = y^2-(x-3)^2*x
sage: differential_basis_baker(f) is None
True
sage: differential_basis_baker(x^2+y^2-1)
[]
TESTS::
sage: from sage.schemes.riemann_surfaces.riemann_surface import differential_basis_baker
sage: R.<x,y> = QQ[]
sage: f = y^12 - x*(x - 1)^7
sage: differential_basis_baker(f) is None
True
"""
k = f.base_ring()
R = PolynomialRing(k, 3, "x,y,z")
x, y, z = R.gens()
F = f(x / z, y / z).numerator()
W = [F] + [F.derivative(v) for v in R.gens()]
# we check that the singularities lie at (1:0:0),(0:1:0),(0:0:1)
# by checking that the eliminations of x, y, z result in
# (principal) ideals generated by a monomial. This is a sufficient
# condition, but not completely necessary.
# It's cheap to check, though.
for c in R.gens():
B = GCD([W[i].resultant(W[j], c) for i in range(4) for j in range(i)])
if len(B.monomials()) > 1:
return None
from sage.geometry.polyhedron.constructor import Polyhedron
D = {(k[0], k[1]): v for k, v in f.dict().items()}
P = Polyhedron(D)
kT = k['t']
# here we check the additional genericity conditions: that the polynomials
# along the edges of the Newton polygon are square-free.
for e in P.bounded_edges():
h = kT([D.get(tuple(c), 0) for c in Polyhedron(e).integral_points()])
if not h.is_squarefree():
return None
x, y = f.parent().gens()
return [x**(a[0] - 1) * y**(a[1] - 1) for a in P.integral_points()
if P.interior_contains(a)]
def find_closest_element(item, List):
r"""
Return the index of the closest element of a list.
Given ``List`` and ``item``, return the index of the element ``l`` of ``List``
which minimises ``(item-l).abs()``. If there are multiple such elements, the
first is returned.
INPUT:
- ``item`` -- value to minimise the distance to over the list.
- ``List`` -- list. List to look for closest element in.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import find_closest_element
sage: i = 5
sage: l = list(range(10))
sage: i == find_closest_element(i, l)
True
Note that this method does no checks on the input, but will fail for inputs
where the absolute value or subtraction do not make sense.
"""
dists = [(item-l).abs() for l in List]
return dists.index(min(dists))
def reparameterise_differential_minpoly(minpoly, z0):
r"""
Rewrites a minimal polynomial to write is around `z_0`.
Given a minimal polynomial `m(z,g)`, where `g` corresponds to a differential
on the surface (that is, it is represented as a rational function, and
implicitly carries a factor `dz`), we rewrite the minpoly in terms of
variables `\bar{z}, \bar{g}` s.t now `\bar{z}=0 \Leftrightarrow z=z_0`.
INPUT:
- ``minpoly`` -- a polynomial in two variables, where the first variables
corresponds to the base coordinate on the Riemann surface.
- ``z0`` -- complex number of infinity. The point about which to
reparameterise.
OUTPUT:
A polynomial in two variables giving the reparameterise minimal polynomial.
EXAMPLES:
On the curve given by `w^2-z^3+1=0`, we have differential
`\frac{dz}{2w} = \frac{dz}{2\sqrt{z^3-1}}`
with minimal polynomial `g^2(z^3-1)-1/4=0`. We can make the substitution
`\bar{z}=z^{-1}` to parameterise the differential about `z=\Infty` as
`\frac{-\bar{z}^{-2} d\bar{z}}{2\sqrt{\bar{z}^{-3}-1}} = \frac{-d\bar{z}}{2\sqrt{\bar{z}(1-\bar{z}^3)}}`.
Hence the transformed differential should have minimal polynomial
`\bar{g}^2\bar{z}(1-\bar{z}^3)-1/4=0`, and we can check this::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface, reparameterise_differential_minpoly
sage: R.<z,w> = QQ[]
sage: S = RiemannSurface(w^2-z^3+1)
sage: minpoly = S._cohomology_basis_bounding_data[1][0][2]
sage: z0 = Infinity
sage: reparameterise_differential_minpoly(minpoly, z0)
-zbar^4*gbar^2 + zbar*gbar^2 - 1/4
We can further check that reparameterising about `0` is the identity
operation::
sage: reparameterise_differential_minpoly(minpoly, 0)(*minpoly.parent().gens())==minpoly
True
.. NOTE::
As part of the routine, when reparameterising about infinity, a
rational function is reduced and then the numerator is taken. Over
an inexact ring this is numerically unstable, and so it is advisable
to only reparameterise about infinity over an exact ring.
"""
P = minpoly.parent()
F = PolynomialRing(P.base_ring(), [str(v)+"bar" for v in P.gens()])
try:
Inf = bool(z0==z0.parent()(Infinity))
except TypeError:
Inf = False
if Inf:
F = F.fraction_field()
mt = F(minpoly(F.gen(0)**(-1),-F.gen(0)**(+2)*F.gen(1)))
mt.reduce()
mt = mt.numerator()
else:
mt = minpoly(F.gen(0)+z0,F.gen(1))
return mt
class RiemannSurface(object):
r"""
Construct a Riemann Surface. This is specified by the zeroes of a bivariate
polynomial with rational coefficients `f(z,w) = 0`.
INPUT:
- ``f`` -- a bivariate polynomial with rational coefficients. The surface is
interpreted as the covering space of the coordinate plane in the first
variable.
- ``prec`` -- the desired precision of computations on the surface in bits
(default: 53)
- ``certification`` -- a boolean (default: True) value indicating whether
homotopy continuation is certified or not. Uncertified homotopy
continuation can be faster.
- ``differentials`` -- (default: None). If specified, provides a list of
polynomials `h` such that `h/(df/dw) dz` is a regular differential on the
Riemann surface. This is taken as a basis of the regular differentials, so
the genus is assumed to be equal to the length of this list. The results
from the homology basis computation are checked against this value.
Providing this parameter makes the computation independent from Singular.
For a nonsingular plane curve of degree `d`, an appropriate set is given
by the monomials of degree up to `d-3`.
- ``integration_method`` -- (default: ``'rigorous'``). String specifying the
integration method to use when calculating the integrals of differentials.
The options are ``'heuristic'`` and ``'rigorous'``, the latter of
which is often the most efficient.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 - z^3 + 1
sage: RiemannSurface(f)
Riemann surface defined by polynomial f = -z^3 + w^2 + 1 = 0, with 53 bits of precision
Another Riemann surface with 100 bits of precision::
sage: S = RiemannSurface(f, prec=100); S
Riemann surface defined by polynomial f = -z^3 + w^2 + 1 = 0, with 100 bits of precision
sage: S.riemann_matrix()^6 #abs tol 0.00000001
[1.0000000000000000000000000000 - 1.1832913578315177081175928479e-30*I]
We can also work with Riemann surfaces that are defined over fields with a
complex embedding, but since the current interface for computing genus and
regular differentials in Singular presently does not support extensions of
QQ, we need to specify a description of the differentials ourselves. We give
an example of a CM elliptic curve::
sage: Qt.<t> = QQ[]
sage: K.<a> = NumberField(t^2-t+3,embedding=CC(0.5+1.6*I))
sage: R.<x,y> = K[]
sage: f = y^2+y-(x^3+(1-a)*x^2-(2+a)*x-2)
sage: S = RiemannSurface(f, prec=100, differentials=[1])
sage: A = S.endomorphism_basis()
sage: len(A)
2
sage: all(len(T.minpoly().roots(K)) > 0 for T in A)
True
The ``'heuristic'`` integration method uses the method ``integrate_vector``
defined in ``sage.numerical.gauss_legendre`` to compute integrals of differentials.
As mentioned there, this works by iteratively doubling the number of nodes
used in the quadrature, and uses a heuristic based on the rate at which the
result is seemingly converging to estimate the error. The ``'rigorous'``
method uses results from [Neu2018]_, and bounds the algebraic integrands on
circular domains using Cauchy's form of the remainder in Taylor approximation
coupled to Fujiwara's bound on polynomial roots (see Bruin-DisneyHogg-Gao,
in preparation). Note this method of bounding on circular domains is also
implemented in :meth:`_compute_delta`. The net result of this bounding is
that one can know (an upper bound on) the number of nodes required to achieve
a certain error. This means that for any given integral, assuming that the
same number of nodes is required by both methods in order to achieve the
desired error (not necessarily true in practice), approximately half
the number of integrand evaluations are required. When the required number
of nodes is high, e.g. when the precision required is high, this can make
the ``'rigorous'`` method much faster. However, the ``'rigorous'`` method does
not benefit as much from the caching of the ``nodes`` method over multiple
integrals. The result of this is that, for calls of :meth:`matrix_of_integral_values`
if the computation is 'fast', the heuristic method may outperform the
rigorous method, but for slower computations the rigorous method can be much
faster::
sage: f = z*w^3+z^3+w
sage: p = 53
sage: Sh = RiemannSurface(f, prec=p, integration_method='heuristic')
sage: Sr = RiemannSurface(f, prec=p, integration_method='rigorous')
sage: from sage.numerical.gauss_legendre import nodes
sage: import time
sage: nodes.cache.clear()
sage: ct = time.time()
sage: Rh = Sh.riemann_matrix()
sage: ct1 = time.time()-ct
sage: nodes.cache.clear()
sage: ct = time.time()
sage: Rr = Sr.riemann_matrix()
sage: ct2 = time.time()-ct
sage: ct2/ct1 # random
1.2429363969691192
Note that for the above curve, the branch points are evenly distributed, and
hence the implicit assumptions in the heuristic method are more sensible,
meaning that a higher precision is required to see the heuristic method
being significantly slower than the rigorous method. For a worse conditioned
curve, this effect is more pronounced::
sage: q = 1/10
sage: f = y^2-(x^2-2*x+1+q^2)*(x^2+2*x+1+q^2)
sage: p = 500
sage: Sh = RiemannSurface(f, prec=p, integration_method='heuristic')
sage: Sr = RiemannSurface(f, prec=p, integration_method='rigorous')
sage: nodes.cache.clear()
sage: ct = time.time()
sage: Rh = Sh.riemann_matrix() # long time (8 seconds)
sage: ct1 = time.time()-ct
sage: nodes.cache.clear()
sage: ct = time.time()
sage: Rr = Sr.riemann_matrix() # long time (1 seconds)
sage: ct2 = time.time()-ct
sage: ct2/ct1 # random
0.19453288941244148
This disparity in timings can get increasingly worse, and testing has shown
that even for random quadrics the heuristic method can be as bad as 30 times
slower.
TESTS:
This elliptic curve has a relatively poorly conditioned set of branch
points, so it challenges the path choice a bit. The code just verifies that
the period is quadratic, because the curve has CM, but really the test is
that the computation completes at all.::
sage: prec = 50
sage: Qx.<t> = QQ[]
sage: CC = ComplexField(prec)
sage: g = t^2-t-1
sage: phiCC = g.roots(CC)[1][0]
sage: K.<phi> = NumberField(g, embedding=phiCC)
sage: R.<X,Y> = K[]
sage: f = Y^2+X*Y+phi*Y-(X^3-X^2-2*phi*X+phi)
sage: S = RiemannSurface(f,prec=prec, differentials=[1])
sage: tau = S.riemann_matrix()[0, 0]
sage: tau.algdep(6).degree() == 2
True
"""
def __init__(self, f, prec=53, certification=True, differentials=None, integration_method="rigorous"):
r"""
TESTS::
sage: R.<z,w> = QQ[]
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: S = RiemannSurface(w^2 - z^3 + 1)
sage: TestSuite(S).run() #not tested; Unclear what pickling strategy is best.
"""
# Initializations.
self._prec = prec
self._certification = certification
if not (integration_method=="heuristic" or integration_method=="rigorous"):
raise ValueError("Invalid integration method")
self._integration_method = integration_method
self._R = f.parent()
if len(self._R.gens()) != 2:
raise ValueError('only bivariate polynomials supported.')
if f.degree() <= 1:
raise ValueError('equation must be of degree at least 2.')
z, w = self._R.gen(0), self._R.gen(1)
self._CC = ComplexField(self._prec)
self._RR = RealField(self._prec)
self._CCz = PolynomialRing(self._CC, [self._R.gen(0)])
self._CCw = PolynomialRing(self._CC, [self._R.gen(1)])
self._RRz = PolynomialRing(self._RR, [self._R.gen(0)])
self.f = f
if differentials is not None:
self._differentials = [self._R(a) for a in differentials]
self.genus = len(self._differentials)
else:
B = differential_basis_baker(f)
if B is not None:
self._differentials = B
self.genus = len(B)
else:
self._differentials = None
self.genus = self._R.ideal(self.f).genus()
if self.genus < 0:
raise ValueError("Singular reports negative genus. Specify differentials manually.")
self.degree = self.f.degree(w)
self._dfdw = self.f.derivative(w)
self._dfdz = self.f.derivative(z)
self._discriminant = self.f.resultant(self._dfdw, w)
# Coefficients of the polynomial for use in homotopy continuation.
self._a0 = self._CCz(self.f.coefficient({w: self.degree})(self._CCz.gen(), 0))
self._a0roots = self._a0.roots(multiplicities=False)
self._aks = [self._CCz(self.f.coefficient({w: self.degree - k - 1})
(self._CCz.gen(), 0)) for k in range(self.degree)]
# Compute the branch locus. Takes the square-free part of the discriminant
# because of numerical issues.
self.branch_locus = []
existing_factors = [x[0] for x in self._discriminant.factor()]
for fac in existing_factors:
self.branch_locus += self._CCz(fac(self._CCz.gen(), 0)).roots(multiplicities=False)
self._f_branch_locus = self.branch_locus
self._cohomology_basis_bounding_data = self._bounding_data(self.cohomology_basis(),
exact=True)
RBzg, bounding_data_list = self._cohomology_basis_bounding_data
minpoly_list = [bd[2] for bd in bounding_data_list]
# We now want to calculate the additional branchpoints associated to
# the differentials.
discriminants = [RBzg(self._discriminant(*RBzg.gens()))]
for minpoly in minpoly_list:
F = RBzg(minpoly)
dF = F.derivative(RBzg.gen(1))
discriminants += [F.resultant(dF, RBzg.gen(1))]
combined_discriminant = lcm(discriminants)(*self._R.gens())
self._differentials_branch_locus = []
for x in combined_discriminant.factor():
if not x[0] in existing_factors:
self._differentials_branch_locus += self._CCz(x[0](self._CCz.gen(),
0)).roots(multiplicities=False)
# We add these branchpoints to the existing.
#self.branch_locus = self.branch_locus+self._differentials_branch_locus
# We now want to also check whether Infinity is a branch point of any
# of the differentials.
# This will be useful when calculating the Abel-Jacobi map.
minpoly_list = [reparameterise_differential_minpoly(mp, Infinity)
for mp in minpoly_list]
discriminants = []
for minpoly in minpoly_list:
F = RBzg(minpoly)
dF = F.derivative(RBzg.gen(1))
discriminants += [F.resultant(dF, RBzg.gen(1))]
discriminant_about_infinity = RBzg(lcm(discriminants))
if discriminant_about_infinity(0,0)==0:
self._differentials_branch_locus.append(self._CC(Infinity))
# Voronoi diagram and the important points associated with it
self.voronoi_diagram = Voronoi(voronoi_ghost(self.branch_locus,
CC=self._CC))
self._vertices = [self._CC(x0, y0)
for x0, y0 in self.voronoi_diagram.vertices]
self._wvalues = [self.w_values(z0) for z0 in self._vertices]
# We arbitrarily, but sensibly, set the basepoint to be the rightmost vertex
self._basepoint = (self._vertices.index(sorted(self._vertices,
key=lambda z:z.real())[-1]), 0)
self._Sn = SymmetricGroup(range(self.degree))
self._L = {}
self._integral_dict = {}
self._fastcall_f = fast_callable(f, domain=self._CC)
self._fastcall_dfdw = fast_callable(self._dfdw, domain=self._CC)
self._fastcall_dfdz = fast_callable(self._dfdz, domain=self._CC)
self._fastcall_cohomology_basis = [fast_callable(h, domain = self._CC)
for h in self.cohomology_basis()]
def __repr__(self):
r"""
Return a string representation of the Riemann surface class.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 - z^4 + 1
sage: RiemannSurface(f)
Riemann surface defined by polynomial f = -z^4 + w^2 + 1 = 0, with 53 bits of precision
"""
s = 'Riemann surface defined by polynomial f = %s = 0, with %s bits of precision' % (self.f, self._prec)
return s
def w_values(self, z0):
r"""
Return the points lying on the surface above ``z0``.
INPUT:
- ``z0`` -- (complex) a point in the complex z-plane.
OUTPUT:
A set of complex numbers corresponding to solutions of `f(z_0,w) = 0`.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 - z^4 + 1
sage: S = RiemannSurface(f)
Find the w-values above the origin, i.e. the solutions of `w^2 + 1 = 0`::
sage: S.w_values(0) # abs tol 1e-14
[-1.00000000000000*I, 1.00000000000000*I]
Note that typically the method returns a list of length ``self.degree``,
but that at ramification points, this may no longer be true::
sage: S.w_values(1) # abs tol 1e-14
[0.000000000000000]
"""
return self.f(z0,self._CCw.gen(0)).roots(multiplicities=False)
@cached_method
def downstairs_edges(self):
r"""
Compute the edgeset of the Voronoi diagram.
OUTPUT:
A list of integer tuples corresponding to edges between vertices in the
Voronoi diagram.
EXAMPLES:
Form a Riemann surface, one with a particularly simple branch locus::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 + z^3 - z^2
sage: S = RiemannSurface(f)
Compute the edges::
sage: S.downstairs_edges()
[(0, 1), (0, 5), (1, 4), (2, 3), (2, 4), (3, 5), (4, 5)]
This now gives an edgeset which one could use to form a graph.
.. NOTE::
The numbering of the vertices is given by the Voronoi package.
"""
# Because of how we constructed the Voronoi diagram, the first n points
# correspond to the branch locus points.
# The regions of these points are all of the edges which don't go off
# to infinity, which are exactly the ones we want.
n = len(self.branch_locus)
desired_edges = [self.voronoi_diagram.regions[self.voronoi_diagram.point_region[i]] for i in range(n)]
# First construct the edges as a set because the regions will overlap
# and we don't want to have two of the same edge.
edges1 = set()
for c in desired_edges:
for j in range(len(c)-1):
edges1.add(frozenset((c[j],c[j+1])))
edges1.add(frozenset((c[0],c[-1])))
# Then make it into a list and sort it.
# The sorting is important - it will make computing the monodromy group
# MUCH easier.
# We orient all the edges so that we go from lower to higher
# numbered vertex for the continuation.
edges = [(i0,i1) if (i0 < i1) else (i1,i0) for (i0,i1) in edges1]
edges.sort()
return edges
def downstairs_graph(self):
r"""
Return the Voronoi decomposition as a planar graph.
The result of this routine can be useful to interpret the labelling of
the vertices. See also :meth:`upstairs_graph`.
OUTPUT:
The Voronoi decomposition as a graph, with appropriate planar embedding.
EXAMPLES::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 - z^4 + 1
sage: S = RiemannSurface(f)
sage: S.downstairs_graph()
Graph on 11 vertices
"""
G = Graph(self.downstairs_edges())
G.set_pos(dict(enumerate(list(v) for v in self._vertices)))
return G
@cached_method
def upstairs_graph(self):
r"""
Return the graph of the upstairs edges.
This method can be useful for generating paths in the surface between points labelled
by upstairs vertices, and verifying that a homology basis is likely computed correctly.
See also :meth:`downstairs_graph`.
OUTPUT:
The homotopy-continued Voronoi decomposition as a graph, with appropriate 3D embedding.
EXAMPLES::
sage: R.<z,w> = QQ[]
sage: S = Curve(w^2-z^4+1).riemann_surface()
sage: G = S.upstairs_graph(); G
Graph on 22 vertices
sage: G.genus()
1
sage: G.is_connected()
True
"""
G = Graph(self.upstairs_edges())
G.set_pos({(i,j): [self._vertices[i].real(), self._vertices[i].imag(),
self.w_values(self._vertices[i])[j].imag()]
for i in range(len(self._vertices)) for j in range(self.degree)}, dim=3)
return G
def _compute_delta(self, z1, epsilon, wvalues=None):
r"""
Compute a delta for homotopy continuation when moving along a path.
INPUT:
- ``z1`` -- a complex number in the z-plane
- ``epsilon`` -- a real number, which is the minimum distance between
the w-values above ``z1``
- ``wvalues`` -- a list (default: ``None``). If specified, saves
recomputation.
OUTPUT:
A real number, which is a step size for moving along a path.
EXAMPLES:
Form a Riemann Surface::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = w^2 - z^4 + 1
sage: S = RiemannSurface(f)
Pick a point which lies on the Voronoi diagram, and compute an
appropriate epsilon::
sage: z1 = S._vertices[0]
sage: currw = S.w_values(z1)
sage: n = len(currw)
sage: epsilon = min([abs(currw[i] - currw[n-j-1]) for i in range(n) for j in range(n-i-1)])/3
sage: S._compute_delta(z1, epsilon) # abs tol 1e-8
0.152628501142363
If the Riemann surface does not have certified homotopy continuation,
then the delta will just be the minimum distance away from a branch
point::
sage: T = RiemannSurface(f, certification=False)
sage: z1 = T._vertices[0]
sage: currw = T.w_values(z1)
sage: n = len(currw)
sage: epsilon = min([abs(currw[i] - currw[n-j-1]) for i in range(n) for j in range(n-i-1)])/3
sage: T._compute_delta(z1, epsilon) # abs tol 1e-8
0.381881307912987
"""
if self._certification:
if wvalues is None:
wvalues = self.w_values(z1)
# For computation of rho. Need the branch locus + roots of a0.
badpoints = self._f_branch_locus + self._a0roots
rho = min(abs(z1 - z) for z in badpoints) / 2
Y = max(abs(self._fastcall_dfdz(z1, wi)/self._fastcall_dfdw(z1, wi))
for wi in wvalues)
# compute M
upperbounds = [sum(ak[k] * (abs(z1) + rho)**k
for k in range(ak.degree()))
for ak in self._aks]
upperbounds.reverse()
# If a0 is a constant polynomial, it is obviously bounded below.
if not self._a0roots:
lowerbound = self._CC(self._a0) / 2
else:
lowerbound = self._a0[self._a0.degree()]*prod(abs((zk - z1) - rho) for zk in self._a0roots) / 2
M = 2 * max((upperbounds[k]/lowerbound).abs().nth_root(k+1)
for k in range(self.degree-1))
return rho*(((rho*Y - epsilon)**2 + 4*epsilon*M).sqrt() - (rho*Y + epsilon))/(2*M - 2*rho*Y)
else:
# Instead, we just compute the minimum distance between branch
# points and the point in question.
return min(abs(b - z1) for b in self._f_branch_locus) / 2
def homotopy_continuation(self, edge):
r"""
Perform homotopy continuation along an edge of the Voronoi diagram using
Newton iteration.
INPUT:
- ``edge`` -- a tuple ``(z_start, z_end)`` indicating the straight line
over which to perform the homotopy continutation.
OUTPUT:
A list containing the initialised continuation data. Each entry in the
list contains: the `t` values that entry corresponds to, a list of
complex numbers corresponding to the points which are reached when
continued along the edge when traversing along the direction of the
edge, and a value ``epsilon`` giving the minimumdistance between the
fibre values divided by 3. The ordering of these points indicates how
they have been permuted due to the weaving of the curve.
EXAMPLES:
We check that continued values along an edge correspond (up to the
appropriate permutation) to what is stored. Note that the permutation
was originally computed from this data::
sage: from sage.schemes.riemann_surfaces.riemann_surface import RiemannSurface
sage: R.<z,w> = QQ[]
sage: f = z^3*w + w^3 + z
sage: S = RiemannSurface(f)
sage: edge1 = sorted(S.edge_permutations())[0]
sage: sigma = S.edge_permutations()[edge1]
sage: edge = [S._vertices[i] for i in edge1]
sage: continued_values = S.homotopy_continuation(edge)[-1][1]
sage: stored_values = S.w_values(S._vertices[edge1[1]])
sage: all(abs(continued_values[i]-stored_values[sigma(i)]) < 1e-8 for i in range(3))
True
"""
z_start, z_end = edge
z_start = self._CC(z_start)
z_end = self._CC(z_end)
ZERO = self._RR.zero()
ONE = self._RR.one()