-
Notifications
You must be signed in to change notification settings - Fork 10
/
bootstrap.py
2057 lines (1848 loc) · 96.7 KB
/
bootstrap.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
#!/usr/bin/env python
"""
PyCFTBoot is an interface for the numerical bootstrap in arbitrary dimension,
a field that was initiated in 2008 by Rattazzi, Rychkov, Tonni and Vichi in
arXiv:0807.0004. Starting from the analytic structure of conformal blocks, the
code formulates semidefinite programs without any proprietary software. The
actual optimization step must be performed by David Simmons-Duffin's program
SDPB available at https://github.com/davidsd/sdpb.
PyCFTBoot may be used to find bounds on OPE coefficients and allowed regions in
the space of scaling dimensions for various CFT operators. All operators used in
the explicit correlators must be scalars, but they may have different scaling
dimensions and transform in arbitrary representations of a global symmetry.
"""
from __future__ import print_function
import xml.dom.minidom
import multiprocessing
import subprocess
import itertools
import zipfile
import json
import time
import re
import os
# Regular sympy is slow but we only use it for quick access to Gegenbauer polynomials
# Even this could be removed since our conformal block code is needlessly general
from symengine.lib.symengine_wrapper import *
import sympy
if have_mpfr == False:
print("Symengine must be compiled with MPFR support")
quit(1)
# Relocate some self-contained classes to separate files
# Importing them would not make sense because they refer back to things in this file
exec(open("common.py").read())
exec(open("compat_autoboot.py").read())
exec(open("compat_juliboots.py").read())
exec(open("compat_scalar_blocks.py").read())
exec(open("blocks1.py").read())
exec(open("blocks2.py").read())
# MPFR has no trouble calling gamma_inc quickly when the first argument is zero
# In case we need to go back to using non-zero values, the following might be faster
"""
import mpmath
mpmath.mp.dps = dec_prec
def uppergamma(x, a):
return RealMPFR(str(mpmath.gammainc(mpmath.mpf(str(x)), a = mpmath.mpf(str(a)))), prec)
"""
class PolynomialVector:
"""
The main class for vectors on which the functionals being found by SDPB may act.
Attributes
----------
vector: A list of the components, expected to be polynomials in `delta`. The
number of components is dictated by the number of derivatives kept in
the search space.
label: A two element list where the first element is the spin and the second
is a user-defined label for the representation of some global symmetry
(or 0 if none have been set yet).
poles: A list of roots of the common denominator shared by all entries in
`vector`. This allows one to go back to the original rational functions
instead of the more convenient polynomials.
"""
def __init__(self, derivatives, spin_irrep, poles):
if type(spin_irrep) == type(1):
spin_irrep = [spin_irrep, 0]
self.vector = derivatives
self.label = spin_irrep
self.poles = poles
class ConformalBlockTable:
"""
A class which calculates tables of conformal block derivatives when initialized.
This uses recursion relations on the diagonal found by Hogervorst, Osborn and
Rychkov in arXiv:1305.1321.
Parameters
----------
dim: The spatial dimension. If even dimensions are of interest, floating
point numbers with small fractional parts are recommended.
k_max: Number controlling the accuracy of the rational approximation.
Specifically, it is the maximum power of the crossing symmetric value
of the radial co-ordinate as described in arXiv:1406.4858.
l_max: The maximum spin to include in the table.
m_max: Number controlling how many `a` derivatives to include where the
standard co-ordinates are expressed as `(a + sqrt(b)) / 2` and
`(a - sqrt(b)) / 2`. As explained in arXiv:1412.4127, a value of 0
does not necessarily eliminate all `a` derivatives.
n_max: The number of `b` derivatives to include where the standard
co-ordinates are expressed as `(a + sqrt(b)) / 2` and
`(a - sqrt(b)) / 2`.
delta_12: [Optional] The difference between the external scaling dimensions of
operator 1 and operator 2. Defaults to 0.
delta_34: [Optional] The difference between the external scaling dimensions of
operator 3 and operator 4. Defaults to 0.
odd_spins: [Optional] Whether to include 0, 1, 2, 3, ..., `l_max` instead of
just 0, 2, 4, ..., `l_max`. Defaults to `False`.
name: [Optional] The name of a file containing conformal blocks that have
already been calculated. If this is specified and refers to a file
produced by PyCFTBoot, all other parameters passed to the class are
overwritten by the ones in the table. If it refers to one produced
by JuliBoots, all parameters passed to the class except delta_12 and
delta_34 are overwritten. If it refers to a directory, the contents
will be treated as scalar_blocks output files. Parameters will be
only be overwritten in this case if they can easily be parsed from
the filenames.
Attributes
----------
table: A list of `PolynomialVector`s. A block's position in the table is
equal to its spin if `odd_spins` is True. Otherwise it is equal to
half of the spin.
m_order: A list with the same number of components as the `PolynomialVector`s
in `table`. Any `i`-th entry in a `PolynomialVector` is a particular
derivative of a conformal block, but to remember which one, just look
at the `i`-th entry of `m_order` which is the number of `a`
derivatives.
n_order: A list with the same number of components as the `PolynomialVector`s
in `table`. Any `i`-th entry in a `PolynomialVector` is a particular
derivative of a conformal block, but to remember which one, just look
at the `i`-th entry of `n_order` which is the number of `b`
derivatives.
"""
def __init__(self, dim, k_max, l_max, m_max, n_max, delta_12 = 0, delta_34 = 0, odd_spins = False, name = None):
self.dim = dim
self.k_max = k_max
self.l_max = l_max
self.m_max = m_max
self.n_max = n_max
self.delta_12 = delta_12
self.delta_34 = delta_34
self.odd_spins = odd_spins
if name != None:
try:
if os.path.isdir(name):
scalar_blocks_read(self, name)
else:
dump_file = open(name, 'r')
token = next(dump_file)[:4]
dump_file.close()
if token == "self":
dump_file = open(name, 'r')
command = dump_file.read()
exec(command)
dump_file.close()
else:
juliboots_read(self, name)
return
except:
print("Table " + name + " not present, generating another", flush = True)
if type(dim) == type(1) and dim % 2 == 0:
small_table = ConformalBlockTableSeed2(dim, k_max, l_max, min(m_max + 2 * n_max, 3), delta_12, delta_34, odd_spins)
else:
small_table = ConformalBlockTableSeed(dim, k_max, l_max, min(m_max + 2 * n_max, 3), 0, delta_12, delta_34, odd_spins)
self.m_order = small_table.m_order
self.n_order = small_table.n_order
self.table = small_table.table
a = Symbol('a')
nu = RealMPFR(str(dim - 2), prec) / 2
c_2 = (ell * (ell + 2 * nu) + delta * (delta - 2 * nu - 2)) / 2
c_4 = ell * (ell + 2 * nu) * (delta - 1) * (delta - 2 * nu - 1)
polys = [0, 0, 0, 0, 0]
poly_derivs = [[], [], [], [], []]
delta_prod = -delta_12 * delta_34 / two
delta_sum = -(delta_12 - delta_34) / two
# Polynomial 0 goes with the lowest order derivative on the right hand side
# Polynomial 3 goes with the highest order derivative on the right hand side
# Polynomial 4 goes with the derivative for which we are solving
polys[0] += (a ** 0) * (16 * c_2 * (2 * nu + 1) - 8 * c_4)
polys[0] += (a ** 1) * (4 * (c_4 + 2 * (2 * nu + 1) * (c_2 * delta_sum - c_2 + nu * delta_prod)))
polys[0] += (a ** 2) * (2 * (delta_sum - nu) * (c_2 * (2 * delta_sum - 1) + delta_prod * (6 * nu - 1)))
polys[0] += (a ** 3) * (2 * delta_prod * (delta_sum - nu) * (delta_sum - nu + 1))
polys[1] += (a ** 1) * (-16 * c_2 * (2 * nu + 1))
polys[1] += (a ** 2) * (4 * delta_prod - 24 * nu * delta_prod + 8 * nu * (2 * nu - 1) * (2 * delta_sum + 1) + 4 * c_2 * (1 - 4 * delta_sum + 6 * nu))
polys[1] += (a ** 3) * (2 * c_2 * (4 * delta_sum - 2 * nu + 1) + 4 * (2 * nu - 1) * (2 * delta_sum + 1) * (delta_sum - nu + 1) + 2 * delta_prod * (10 * nu - 5 - 4 * delta_sum))
polys[1] += (a ** 4) * ((delta_sum - nu + 1) * (4 * delta_prod + (2 * delta_sum + 1) * (delta_sum - nu + 2)))
polys[2] += (a ** 2) * (16 * c_2 + 16 * nu - 32 * nu * nu)
polys[2] += (a ** 3) * (8 * delta_prod - 8 * (3 * delta_sum - nu + 3) * (2 * nu - 1) - 16 * c_2 - 8 * nu + 16 * nu * nu)
polys[2] += (a ** 4) * (4 * (c_2 - delta_prod + (3 * delta_sum - nu + 3) * (2 * nu - 1)) - 4 * delta_prod - 2 * (delta_sum - nu + 2) * (5 * delta_sum - nu + 5))
polys[2] += (a ** 5) * (2 * delta_prod + (delta_sum - nu + 2) * (5 * delta_sum - nu + 5))
polys[3] += (a ** 3) * (32 * nu - 16)
polys[3] += (a ** 4) * (16 - 32 * nu + 4 * (4 * delta_sum - 2 * nu + 7))
polys[3] += (a ** 5) * (4 * (2 * nu - 1) - 4 * (4 * delta_sum - 2 * nu + 7))
polys[3] += (a ** 6) * (4 * delta_sum - 2 * nu + 7)
polys[4] += (a ** 7) - 6 * (a ** 6) + 12 * (a ** 5) - 8 * (a ** 4)
# Store all possible derivatives of these polynomials
for i in range(0, 5):
for j in range(0, i + 4):
poly_derivs[i].append(polys[i].subs(a, 1))
polys[i] = polys[i].diff(a)
for m in range(self.m_order[-1] + 1, m_max + 2 * n_max + 1):
for l in range(0, len(small_table.table)):
new_deriv = 0
for i in range(m - 1, max(m - 8, -1), -1):
coeff = 0
index = max(m - i - 4, 0)
prefactor = one
for k in range(0, index):
prefactor *= (m - 4 - k)
prefactor /= k + 1
k = max(4 + i - m, 0)
while k <= 4 and index <= (m - 4):
coeff += prefactor * poly_derivs[k][index]
prefactor *= (m - 4 - index)
prefactor /= index + 1
index += 1
k += 1
if type(coeff) != type(1):
coeff = coeff.subs(ell, small_table.table[l].label[0])
new_deriv -= coeff * self.table[l].vector[i]
new_deriv = new_deriv / poly_derivs[4][0]
self.table[l].vector.append(new_deriv.expand())
self.m_order.append(m)
self.n_order.append(0)
# This is just an alternative to storing derivatives as a doubly-indexed list
index = m_max + 2 * n_max + 1
index_map = [range(0, m_max + 2 * n_max + 1)]
for n in range(1, n_max + 1):
index_map.append([])
for m in range(0, 2 * (n_max - n) + m_max + 1):
index_map[n].append(index)
coeff1 = m * (-1) * (2 - 4 * n - 4 * nu)
coeff2 = m * (m - 1) * (2 - 4 * n - 4 * nu)
coeff3 = m * (m - 1) * (m - 2) * (2 - 4 * n - 4 * nu)
coeff4 = 1
coeff5 = (-6 + m + 4 * n - 2 * nu - 2 * delta_sum)
coeff6 = (-1) * (4 * c_2 + m * m + 8 * m * n - 5 * m + 4 * n * n - 2 * n - 2 - 4 * nu * (1 - m - n) + 4 * delta_sum * (m + 2 * n - 2) + 2 * delta_prod)
coeff7 = m * (-1) * (m * m + 12 * m * n - 13 * m + 12 * n * n - 34 * n + 22 - 2 * nu * (2 * n - m - 1) + 2 * delta_sum * (m + 4 * n - 5) + 2 * delta_prod)
coeff8 = (1 - n)
coeff9 = (1 - n) * (-6 + 3 * m + 4 * n - 2 * nu + 2 * delta_sum)
for l in range(0, len(small_table.table)):
new_deriv = 0
if m > 0:
new_deriv += coeff1 * self.table[l].vector[index_map[n][m - 1]]
if m > 1:
new_deriv += coeff2 * self.table[l].vector[index_map[n][m - 2]]
if m > 2:
new_deriv += coeff3 * self.table[l].vector[index_map[n][m - 3]]
new_deriv += coeff4 * self.table[l].vector[index_map[n - 1][m + 2]]
new_deriv += coeff5 * self.table[l].vector[index_map[n - 1][m + 1]]
new_deriv += coeff6.subs(ell, small_table.table[l].label[0]) * self.table[l].vector[index_map[n - 1][m]]
new_deriv += coeff7 * self.table[l].vector[index_map[n - 1][m - 1]]
if n > 1:
new_deriv += coeff8 * self.table[l].vector[index_map[n - 2][m + 2]]
new_deriv += coeff9 * self.table[l].vector[index_map[n - 2][m + 1]]
new_deriv = new_deriv / (2 - 4 * n - 4 * nu)
self.table[l].vector.append(new_deriv.expand())
self.m_order.append(m)
self.n_order.append(n)
index += 1
def dump(self, name, form = None):
"""
Saves a table of conformal block derivatives to a file. Unless overridden,
the file is valid Python code which manually populates the entries of
`table` when executed.
Parameters
----------
name: The path to use for output.
form: [Optional] A string indicating that the file should be saved in
another program's format if it is equal to "scalar_blocks" or
"juliboots". Any other value will be ignored. Defaults to `None`.
"""
if form == "juliboots":
juliboots_write(self, name)
elif form == "scalar_blocks":
scalar_blocks_write(self, name)
else:
dump_table_contents(self, name)
class ConvolvedBlockTable:
"""
A class which produces the functions that need to be linearly dependent in a
crossing symmetric CFT. If a `ConformalBlockTable` does not need to be changed
after a change to the external dimensions, a `ConvolvedBlockTable` does not
either. This is because external dimensions only appear symbolically through a
symbol called `delta_ext`.
Parameters
----------
block_table: A `ConformalBlockTable` from which to produce the convolved blocks.
odd_spins: [Optional] A parameter telling the class to keep odd spins which is
only used if `odd_spins` is True for `block_table`. Defaults to
`True`.
symmetric: [Optional] Whether to add blocks in two different channels instead
of subtract them. Defaults to `False`.
content: [Optional] A list of ordered triples that are used to produce
user-defined linear combinations of convolved conformal blocks
instead of just individual convolved conformal blocks where all the
coefficients are 1. Elements of a triple are taken to be the
coefficient, the dimension shift and the spin shift respectively.
It should always make sense to include a triple whose second and
third entries are 0 and 0 since this corresponds to a convolved
conformal block with scaling dimension `delta` and spin `ell`.
However, if other blocks in the multiplet have `delta + 1` and
`ell - 1` relative to this, another triple should be included whose
second and third entries are 1 and -1. The coefficient (first
entry) may be a polynomial in `delta` with coefficients depending
on `ell`.
Attributes
----------
dim: The spatial dimension, inherited from `block_table`.
k_max: Numer controlling the accuracy of the rational approximation,
inherited from `block_table`.
l_max: The highest spin kept in the convolved block table. This is at most
the `l_max` of `block_table`.
m_max: Number controlling how many `a` derivatives there are where the
standard co-ordinates are expressed as `(a + sqrt(b)) / 2` and
`(a - sqrt(b)) / 2`. This is at most the `m_max` of `block_table`.
n_max: The number of `b` derivatives there are where the standard
co-ordinates are expressed as `(a + sqrt(b)) / 2` and
`(a - sqrt(b)) / 2`. This is at most the `n_max` of `block_table`.
delta_12: The difference between the external scaling dimensions of operator
1 and operator 2, inherited from `block_table`.
delta_32: The difference between the external scaling dimensions of operator
3 and operator 4, inherited from `block_table`.
table: A list of `PolynomialVector`s. A block's position in the table is
equal to its spin if `odd_spins` is `True`. Otherwise it is equal
to half of the spin.
m_order: A list stating how many `a` derivatives are being described by the
corresponding entry in a `PolynomialVector` in `table`. Different
from the `m_order` of `block_table` because some derivatives vanish
by symmetry.
n_order: A list stating how many `b` derivatives are being described by the
corresponding entry in a `PolynomialVector` in `table`.
"""
def __init__(self, block_table, odd_spins = True, symmetric = False, spins = [], content = [[1, 0, 0]]):
# Copying everything but the unconvolved table is fine from a memory standpoint
self.dim = block_table.dim
self.k_max = block_table.k_max
self.l_max = block_table.l_max
self.m_max = block_table.m_max
self.n_max = block_table.n_max
self.delta_12 = block_table.delta_12
self.delta_34 = block_table.delta_34
self.m_order = []
self.n_order = []
self.table = []
max_spin_shift = 0
for trip in content:
max_spin_shift = max(max_spin_shift, trip[2])
self.l_max -= max_spin_shift
# We can restrict to even spin when the provided table has odd spin but not vice-versa
if odd_spins == False and block_table.odd_spins == True:
self.odd_spins = False
else:
self.odd_spins = block_table.odd_spins
if block_table.odd_spins == True:
step = 1
else:
step = 2
if len(spins) > 0:
spin_list = spins
elif self.odd_spins:
spin_list = range(0, self.l_max + 1, 1)
else:
spin_list = range(0, self.l_max + 1, 2)
symbol_array = []
for n in range(0, block_table.n_max + 1):
symbol_list = []
for m in range(0, 2 * (block_table.n_max - n) + block_table.m_max + 1):
symbol_list.append(Symbol('g_' + n.__str__() + '_' + m.__str__()))
symbol_array.append(symbol_list)
derivatives = []
for n in range(0, block_table.n_max + 1):
for m in range(0, 2 * (block_table.n_max - n) + block_table.m_max + 1):
# Skip the ones that will vanish
if (symmetric == False and m % 2 == 0) or (symmetric == True and m % 2 == 1):
continue
self.m_order.append(m)
self.n_order.append(n)
expression = 0
old_coeff = RealMPFR("0.25", prec) ** delta_ext
for j in range(0, n + 1):
coeff = old_coeff
for i in range(0, m + 1):
expression += coeff * symbol_array[n - j][m - i]
coeff *= (i + 2 * j - 2 * delta_ext) * (m - i) / (i + 1)
old_coeff *= (j - delta_ext) * (n - j) / (j + 1)
deriv = expression / RealMPFR(str(factorial(m) * factorial(n)), prec)
derivatives.append(deriv)
combined_block_table = []
for spin in spin_list:
vector = []
l = spin // step
# Different blocks in the linear combination may be divided by different poles
all_poles = []
pole_dict = {}
for trip in content:
del_shift = trip[1]
ell_shift = trip[2] // step
if l + ell_shift >= 0:
gathered_poles = gather(block_table.table[l + ell_shift].poles)
for p in gathered_poles.keys():
ind = get_index_approx(pole_dict.keys(), p - del_shift)
if ind == -1:
pole_dict[p - del_shift] = gathered_poles[p]
else:
pole_dict_index = index_iter(pole_dict.keys(), ind)
num = pole_dict[pole_dict_index]
pole_dict[pole_dict_index] = max(num, gathered_poles[p])
for p in pole_dict.keys():
all_poles += [p] * pole_dict[p]
for i in range(0, len(block_table.table[l].vector)):
entry = 0
for trip in content:
if "subs" in dir(trip[0]):
coeff = trip[0].subs(ell, spin)
else:
coeff = trip[0]
del_shift = trip[1]
ell_shift = trip[2] // step
coeff *= r_cross ** del_shift
if l + ell_shift >= 0:
coeff *= omit_all(all_poles, block_table.table[l + ell_shift].poles, delta, del_shift)
entry += coeff * block_table.table[l + ell_shift].vector[i].subs(delta, delta + del_shift)
vector.append(entry.expand())
combined_block_table.append(PolynomialVector(vector, [spin, 0], all_poles))
for l in range(0, len(combined_block_table)):
new_derivs = []
for i in range(0, len(derivatives)):
deriv = derivatives[i]
for j in range(len(combined_block_table[l].vector) - 1, 0, -1):
deriv = deriv.subs(symbol_array[block_table.n_order[j]][block_table.m_order[j]], combined_block_table[l].vector[j])
new_derivs.append(2 * deriv.subs(symbol_array[0][0], combined_block_table[l].vector[0]))
self.table.append(PolynomialVector(new_derivs, combined_block_table[l].label, combined_block_table[l].poles))
class SDP:
"""
A class where convolved conformal blocks are augmented by crossing equations
which allow numerical bounds to be derived. All calls to `SDPB` happen through
this class.
Parameters
----------
dim_list: A list of all scaling dimensions that appear in the external
operators of the four-point functions being considered. If
there is only one, this may be a float instead of a list.
conv_table_list: A list of all types of convolved conformal block tables that
appear in the crossing equations. If there is only one type,
this may be a `ConvolvedBlockTable` instance instead of a list.
vector_types: [Optional] A list of triples, one for each type of operator in
the sum rule. The third element of each triple is the arbitrary
label for that representation (something used to label
`PolynomialVector`s that are generated). The second element is
an even integer for even spin operators and an odd integer for
odd spin operators. The first element is everything else.
Specifically, it is a list of matrices of ordered quadruples
where a matrix is a list of lists. If the sum rule involves no
matrices, it may simply be a list of ordered quadruples. In a
quadruple, the first entry is a numerical coefficient and the
second entry is an index stating which element of
`conv_table_list` that coefficient should multiply. The third
and fourth entries (which may be omitted if `dim_list` has only
one entry) specify the external dimensions that should replace
`delta_ext` in a `ConvolvedConformalBlockTable` as positions in
`dim_list`. They are the "inner two" dimensions `j` and `k` if
convolved conformal blocks are given `i`, `j`, `k`, `l` labels
as in arXiv:1406.4858. The first triple must describe the even
spin singlet channel (where the identity can be found). After
this, the order of the triples is not important.
prototype: [Optional] A previous instance of `SDP` which may speed up the
allocation of this one. The idea is that if a bound on any
operator does not need to change from one table to the next,
the bilinear basis corresponding to it (which requires a
Cholesky decomposition and a matrix inversion to calculate)
might simply be copied.
Attributes
----------
dim: The spatial dimension, inherited from `conv_block_table_list`.
k_max: The corresponding attribute from `conv_block_table_list`.
l_max: The corresponding attribute from `conv_block_table_list`.
m_max: The corresponding attribute from `conv_block_table_list`.
n_max: The corresponding attribute from `conv_block_table_list`.
odd_spins: Whether any element of `conv_block_table_list` has odd spins.
table: A list of matrices of `PolynomialVector`s where the number of
rows and columns is determined from `vector_types`. They are
ordered first by the type of representation and then by spin.
Each `PolynomialVector` may be longer than a `PolynomialVector`
from a single entry of `conv_block_table_list`. They represent
the concatenation of several such `PolynomialVectors`, one for
each row of a vectorial sum rule.
m_order: Analogous to `m_order` in `ConformalBlockTable` or
`ConvolvedBlockTable`, this keeps track of the number of `a`
derivatives in these longer `PolynomialVector`s.
m_order: Analogous to `n_order` in `ConformalBlockTable` or
`ConvolvedBlockTable`, this keeps track of the number of `b`
derivatives in these longer `PolynomialVector`s.
options: A list of strings where each string is a command line option
that will be passed when `SDPB` is run from this `SDP`. This
list should be touched with `set_option` and not directly.
points: In addition to `PolynomialVector`s whose entries allow `delta`
to take any positive value, the user may also include in the
sum rule `PolynomialVector`s whose entries are pure numbers.
In other words, she may evaluate some of them once and for all
at particular values of `delta` to force certain operators to
appear in the spectrum. This list should be touched with
`add_point` and not directly.
unit: A list which gives the `PolynomialVector` corresponding to the
identity. This is obtained simply by plugging `delta = 0` into
the zero spin singlet channel. If such a channel involves
matrices, the sum of all elements is taken since the conformal
blocks are normalized under the convention that all OPE
coefficients involving the identity are 1. It should not be
necessary to change this.
irrep_set: A list of ordered pairs, one for each type of operator in
`vector_types`. The second element of each is a label for the
representation. The first is a modified version of the first
matrix. The ordered quadruples do not correspond to the
prefactors and list positions anymore but to the four external
operator dimensions that couple to the block in this position.
It should not be necessary to change this.
basis: A list of matrices which has as many matrices as `table`.
Each triangular matrix stores a set of orthogonal polynomials
in the monomial basis. It should not be necessary to change
this.
"""
def __init__(self, dim_list, conv_table_list, vector_types = [[[[[[1, 0, 0, 0]]]], 0, 0]], prototype = None):
# If a user is looking at single correlators, we will not punish
# her for only passing one dimension
if type(dim_list) != type([]):
dim_list = [dim_list]
if type(conv_table_list) != type([]):
conv_table_list = [conv_table_list]
# Same story here
self.dim = 0
self.k_max = 0
self.l_max = 0
self.m_max = 0
self.n_max = 0
self.odd_spins = False
# Just in case these are different
for tab in conv_table_list:
self.dim = max(self.dim, tab.dim)
self.k_max = max(self.k_max, tab.k_max)
self.l_max = max(self.l_max, tab.l_max)
self.m_max = max(self.m_max, tab.m_max)
self.n_max = max(self.n_max, tab.n_max)
self.points = []
self.m_order = []
self.n_order = []
self.table = []
self.unit = []
self.irrep_set = []
# Turn any "raw elements" from the vectorial sum rule into 1x1 matrices
for i in range(0, len(vector_types)):
for j in range(0, len(vector_types[i][0])):
if type(vector_types[i][0][j][0]) != type([]):
vector_types[i][0][j] = [[vector_types[i][0][j]]]
# Again, fill in arguments that need not be specified for single correlators
for i in range(0, len(vector_types)):
for j in range(0, len(vector_types[i][0])):
for k in range(0, len(vector_types[i][0][j])):
for l in range(0, len(vector_types[i][0][j][k])):
if len(vector_types[i][0][j][k][l]) == 2:
vector_types[i][0][j][k][l].append(0)
vector_types[i][0][j][k][l].append(0)
# We must assume the 0th element put in vector_types corresponds to the singlet channel
# This is because we must harvest the identity from it
for matrix in vector_types[0][0]:
chosen_tab = conv_table_list[matrix[0][0][1]]
for i in range(0, len(chosen_tab.table[0].vector)):
unit = 0
m = chosen_tab.m_order[i]
n = chosen_tab.n_order[i]
for r in range(0, len(matrix)):
for s in range(0, len(matrix[r])):
quad = matrix[r][s]
param = RealMPFR("0.5", prec) * (dim_list[quad[2]] + dim_list[quad[3]])
#tab = conv_table_list[quad[1]]
#factor = self.shifted_prefactor(tab.table[0].poles, r_cross, 0, 0)
#unit += factor * quad[0] * tab.table[0].vector[i].subs(delta, 0).subs(delta_ext, (dim_list[quad[2]] + dim_list[quad[3]]) / 2.0)
unit += 2 * quad[0] * (RealMPFR("0.25", prec) ** param) * rf(-param, n) * rf(2 * n - 2 * param, m) / (factorial(m) * factorial(n))
self.m_order.append(m)
self.n_order.append(n)
self.unit.append(unit)
# Looping over types and spins gives "0 - S", "0 - T", "1 - A" and so on
for vec in vector_types:
# Instead of specifying even or odd spins, the user can specify a list of spins
if type(vec[1]) == type([]):
spin_list = vec[1]
elif (vec[1] % 2) == 1:
self.odd_spins = True
spin_list = range(1, self.l_max, 2)
else:
spin_list = range(0, self.l_max, 2)
for l in spin_list:
size = len(vec[0][0])
outer_list = []
for r in range(0, size):
inner_list = []
for s in range(0, size):
derivatives = []
large_poles = []
for matrix in vec[0]:
quad = matrix[r][s]
tab = conv_table_list[quad[1]]
if tab.odd_spins:
index = l
else:
index = l // 2
if quad[0] != 0:
large_poles = tab.table[index].poles
for i in range(0, len(tab.table[index].vector)):
derivatives.append(quad[0] * tab.table[index].vector[i].subs(delta_ext, (dim_list[quad[2]] + dim_list[quad[3]]) / 2.0))
inner_list.append(PolynomialVector(derivatives, [l, vec[2]], large_poles))
outer_list.append(inner_list)
self.table.append(outer_list)
# We are done with vector_types now so we can change it
for vec in vector_types:
matrix = deepcopy(vec[0][0])
for r in range(0, len(matrix)):
for s in range(0, len(matrix)):
quad = matrix[r][s]
dim2 = dim_list[quad[2]]
dim3 = dim_list[quad[3]]
dim1 = dim2 + conv_table_list[quad[1]].delta_12
dim4 = dim3 - conv_table_list[quad[1]].delta_34
matrix[r][s] = [dim1, dim2, dim3, dim4]
self.irrep_set.append([matrix, vec[2]])
self.bounds = [0.0] * len(self.table)
self.options = []
if prototype == None:
self.basis = [0] * len(self.table)
self.set_bound(reset_basis = True)
else:
self.basis = []
for mat in prototype.basis:
self.basis.append(mat)
self.set_bound(reset_basis = False)
def add_point(self, spin_irrep = -1, dimension = -1, extra = []):
"""
Tells the `SDP` that a particular fixed operator should be included in the
sum rule. If called with one argument, all points with that label will be
removed. If called with no arguments, all points with any label will be
removed.
Parameters
----------
spin_irrep: [Optional] An ordered pair used to label the `PolynomialVector`
for the operator. The first entry is the spin, the second is the
label which must be found in `vector_types` or 0 if not present.
Defaults to -1 which means all operators.
dimension: [Optional] The scaling dimension of the operator being added.
Defaults to -1 which means the point should be removed.
extra: [Optional] A list of quintuples specifying information about
other operators that should be packaged with this operator. The
first two elements of a quintuple are the `spin_irrep` and
`dimension` except for the operator which is not being added
separately because its presence is tied to this one. The next
two elements of a quintuple are ordered pairs giving positions
in the crossing equation matrices. The operator described by the
first two quintuple elements should have its contribution in the
position given by the first ordered pair added to that of the
operator described by `spin_irrep` and `dimension` in the
position given by the second ordered pair. The final element of
the quintuple is a coefficient that should multiply whatever is
added. The purpose of this is to enforce OPE coefficient
relations as in arXiv:1603.04436.
"""
if spin_irrep == -1:
self.points = []
return
if type(spin_irrep) == type(1):
spin_irrep = [spin_irrep, 0]
if dimension != -1:
self.points.append((spin_irrep, dimension, extra))
else:
for p in self.points:
if p[0] == spin_irrep:
self.points.remove(p)
def get_bound(self, gapped_spin_irrep):
"""
Returns the minimum scaling dimension of a given operator in this `SDP`.
This will return the unitarity bound until the user starts calling
`set_bound`.
Parameters
----------
gapped_spin_irrep: An ordered pair used to label the `PolynomialVector`
whose bound should be read. The first entry is the spin
and the second is the label found in `vector_types` or
0 if not present.
"""
if type(gapped_spin_irrep) == type(1):
gapped_spin_irrep = [gapped_spin_irrep, 0]
for l in range(0, len(self.table)):
if self.table[l][0][0].label == gapped_spin_irrep:
return self.bounds[l]
def set_bound(self, gapped_spin_irrep = -1, delta_min = -1, reset_basis = True):
"""
Sets the minimum scaling dimension of a given operator in the sum rule. If
called with one argument, the operator with that label will be assigned the
unitarity bound. If called with no arguments, all operators will be assigned
the unitarity bound.
Parameters
----------
gapped_spin_irrep: [Optional] An ordered pair used to label the
`PolynomialVector` whose bound should be set. The first
entry is the spin and the second is the label found in
`vector_types` or 0 if not present. Defaults to -1 which
means all operators.
delta_min: [Optional] The minimum scaling dimension to set. Also
accepts oo to indicate that a continuum should not be
included. Defaults to -1 which means unitarity.
reset_basis: [Optional] An internal parameter which may be used to
prevent the orthogonal polynomials which improve the
numerical stability of `SDPB` from being recalculated.
Defaults to `True`.
"""
if gapped_spin_irrep == -1:
for l in range(0, len(self.table)):
spin = self.table[l][0][0].label[0]
self.bounds[l] = unitarity_bound(self.dim, spin)
if reset_basis:
self.set_basis(l)
else:
if type(gapped_spin_irrep) == type(1):
gapped_spin_irrep = [gapped_spin_irrep, 0]
l = self.get_table_index(gapped_spin_irrep)
spin = gapped_spin_irrep[0]
if delta_min == -1:
self.bounds[l] = unitarity_bound(self.dim, spin)
else:
self.bounds[l] = delta_min
if reset_basis and delta_min != oo:
self.set_basis(l)
def get_option(self, key):
"""
Returns the string representation of a value that `SDPB` will use, whether
or not it has been explicitly set.
Parameters
----------
key: The name of the `SDPB` parameter without any "--" at the beginning or
"=" at the end.
"""
if key in sdpb_options:
ret = sdpb_defaults[sdpb_options.index(key)]
opt_string = "--" + key + "="
for i in range(0, len(self.options)):
if self.options[i][:len(opt_string)] == opt_string:
ret = self.options[i][len(opt_string):]
break
return ret
def set_option(self, key = None, value = None):
"""
Sets the value of a switch that should be passed to `SDPB` on the command
line. `SDPB` options that do not take a parameter are handled by other
methods so it should not be necessary to pass them.
Parameters
----------
key: [Optional] The name of the `SDPB` parameter being set without any
"--" at the beginning or "=" at the end. Defaults to `None` which
means all parameters will be reset to their default values.
value: [Optional] The string or numerical value that should accompany `key`.
Defaults to `None` which means that the parameter for `key` will be
reset to its default value.
"""
if key == None:
self.options = []
elif key in sdpb_options:
found = False
opt_string = "--" + key + "="
for i in range(0, len(self.options)):
if self.options[i][:len(opt_string)] == opt_string:
found = True
break
if found == True and value == None:
self.options = self.options[:i] + self.options[i + 1:]
elif found == True and value != None:
self.options[i] = opt_string + str(value)
elif found == False and value != None:
self.options.append(opt_string + str(value))
else:
print("Unknown option", flush = True)
def get_table_index(self, spin_irrep):
"""
Searches for the label of a `PolynomialVector` and returns its position in
`table` or -1 if not found.
Parameters
----------
spin_irrep: An ordered pair of the type passed to `set_bound`. Used to
label the spin and representation being searched.
"""
if type(spin_irrep) == type(1):
spin_irrep = [spin_irrep, 0]
for l in range(0, len(self.table)):
if self.table[l][0][0].label == spin_irrep:
return l
return -1
def set_basis(self, index):
"""
Calculates a basis of polynomials that are orthogonal with respect to the
positive measure prefactor that turns a `PolynomialVector` into a rational
approximation to a conformal block. It should not be necessary to explicitly
call this.
Parameters
----------
index: The position of the matrix in `table` whose basis needs updating.
"""
poles = self.table[index][0][0].poles
delta_min = self.bounds[index]
delta_min = float(delta_min)
delta_min = RealMPFR(str(delta_min), prec)
bands = []
matrix = []
degree = 0
size = len(self.table[index])
for r in range(0, size):
for s in range(0, size):
polynomial_vector = self.table[index][r][s].vector
for n in range(0, len(polynomial_vector)):
expression = polynomial_vector[n].expand()
degree = max(degree, len(coefficients(expression)) - 1)
# Separate the poles and associate each with an uppergamma function
# This avoids computing these same functions for each d in the loop below
gathered_poles = gather(poles)
poles = []
orders = []
gammas = []
for p in gathered_poles:
if p < delta_min:
poles.append(p - delta_min)
orders.append(gathered_poles[p])
gammas.append(uppergamma(zero, (p - delta_min) * log(r_cross)))
for d in range(0, 2 * (degree // 2) + 1):
result = (r_cross ** delta_min) * self.integral(d, poles, orders, gammas)
bands.append(result)
for r in range(0, (degree // 2) + 1):
new_entries = []
for s in range(0, (degree // 2) + 1):
new_entries.append(bands[r + s])
matrix.append(new_entries)
matrix = DenseMatrix(matrix)
matrix = matrix.cholesky()
matrix = matrix.inv()
self.basis[index] = matrix
def reshuffle_with_normalization(self, vector, norm):
"""
Converts between the Mathematica definition and the bootstrap definition of
an SDP. As explained in arXiv:1502.02033, it is natural to normalize the
functionals being found by demanding that they give 1 when acting on a
particular `PolynomialVector`. `SDPB` on the other hand works with
functionals that have a fixed leading component. This is an equivalent
problem after a trivial reshuffling.
Parameters
----------
vector: The `vector` part of the `PolynomialVector` needing to be shuffled.
norm: The `vector` part of the `PolynomialVector` constrained to have
unit action under the functional before the reshuffling.
"""
norm_hack = []
for el in norm:
norm_hack.append(float(el))
max_index = norm_hack.index(max(norm_hack, key = abs))
const = vector[max_index] / norm[max_index]
ret = []
for i in range(0, len(norm)):
ret.append(vector[i] - const * norm[i])
ret = [const] + ret[:max_index] + ret[max_index + 1:]
return ret
def short_string(self, num):
"""
Returns the string representation of a number except with an attempt to trim
superfluous zeros if the number is too small.
Parameters
----------
num: The number.
"""
if abs(num) < tiny:
return "0"
else:
return str(num)
def make_laguerre_points(self, degree):
"""
Returns a list of convenient sample points for the XML files of `SDPB`.
Parameters
----------
degree: The maximum degree of all polynomials in a `PolynomialVector`.
"""
ret = []
for d in range(0, degree + 1):
point = -(pi.n(prec) ** 2) * ((4 * d - 1) ** 2) / (64 * (degree + 1) * log(r_cross))
ret.append(point)
return ret
def shifted_prefactor(self, poles, base, x, shift):
"""
Returns the positive measure prefactor that turns a `PolynomialVector` into
a rational approximation to a conformal block. Evaluating this at a sample
point produces a sample scaling needed by the XML files of `SDPB`.
Parameters
----------
poles: The roots of the prefactor's denominator, often from the `poles`
attribute of a `PolynomialVector`.
base: The base of the exponential in the numerator, often the crossing
symmetric value of the radial co-ordinate.
x: The argument of the function, often `delta`.
shift: An amount by which to shift `x`. This should match one of the minimal
values assigned by `set_bound`.
"""
product = 1
for p in poles:
product *= x - (p - shift)
return (base ** (x + shift)) / product
def basic_integral(self, pos, pole, order, gamma_val):
"""
Returns the inner product of two monic monomials with respect to a more
basic positive measure prefactor which has just a single pole.