-
Notifications
You must be signed in to change notification settings - Fork 56
/
matrixforwardsim.py
2761 lines (2270 loc) · 138 KB
/
matrixforwardsim.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
""" Defines the MatrixForwardSimulator calculator class"""
#***************************************************************************************************
# Copyright 2015, 2019 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
# Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights
# in this software.
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory.
#***************************************************************************************************
import warnings as _warnings
import numpy as _np
import numpy.linalg as _nla
import time as _time
import itertools as _itertools
import collections as _collections
from ..tools import mpitools as _mpit
from ..tools import slicetools as _slct
from ..tools.matrixtools import _fas
from .profiler import DummyProfiler as _DummyProfiler
from .label import Label as _Label
from .matrixevaltree import MatrixEvalTree as _MatrixEvalTree
from .forwardsim import ForwardSimulator
_dummy_profiler = _DummyProfiler()
# Smallness tolerances, used internally for conditional scaling required
# to control bulk products, their gradients, and their Hessians.
PSMALL = 1e-100
DSMALL = 1e-100
HSMALL = 1e-100
class MatrixForwardSimulator(ForwardSimulator):
"""
Encapsulates a calculation tool used by model objects to perform product
and derivatives-of-product calculations.
This is contained in a class separate from Model to allow for additional
model classes (e.g. ones which use entirely different -- non-gate-local
-- parameterizations of operation matrices and SPAM vectors) access to these
fundamental operations.
"""
def __init__(self, dim, simplified_op_server, paramvec):
"""
Construct a new MatrixForwardSimulator object.
Parameters
----------
dim : int
The gate-dimension. All operation matrices should be dim x dim, and all
SPAM vectors should be dim x 1.
gates, preps, effects : OrderedDict
Ordered dictionaries of LinearOperator, SPAMVec, and SPAMVec objects,
respectively. Must be *ordered* dictionaries to specify a
well-defined column ordering when taking derivatives.
paramvec : ndarray
The parameter vector of the Model.
autogator : AutoGator
An auto-gator object that may be used to construct virtual gates
for use in computations.
"""
super(MatrixForwardSimulator, self).__init__(
dim, simplified_op_server, paramvec)
if self.evotype not in ("statevec", "densitymx"):
raise ValueError(("Evolution type %s is incompatbile with "
"matrix-based calculations" % self.evotype))
def copy(self):
""" Return a shallow copy of this MatrixForwardSimulator """
return MatrixForwardSimulator(self.dim, self.sos, self.paramvec)
def product(self, circuit, scale=False):
"""
Compute the product of a specified sequence of operation labels.
Note: LinearOperator matrices are multiplied in the reversed order of the tuple. That is,
the first element of circuit can be thought of as the first gate operation
performed, which is on the far right of the product of matrices.
Parameters
----------
circuit : Circuit or tuple of operation labels
The sequence of operation labels.
scale : bool, optional
When True, return a scaling factor (see below).
Returns
-------
product : numpy array
The product or scaled product of the operation matrices.
scale : float
Only returned when scale == True, in which case the
actual product == product * scale. The purpose of this
is to allow a trace or other linear operation to be done
prior to the scaling.
"""
if scale:
scaledGatesAndExps = {}
scale_exp = 0
G = _np.identity(self.dim)
for lOp in circuit:
if lOp not in scaledGatesAndExps:
opmx = self.sos.get_operation(lOp).todense()
ng = max(_nla.norm(opmx), 1.0)
scaledGatesAndExps[lOp] = (opmx / ng, _np.log(ng))
gate, ex = scaledGatesAndExps[lOp]
H = _np.dot(gate, G) # product of gates, starting with identity
scale_exp += ex # scale and keep track of exponent
if H.max() < PSMALL and H.min() > -PSMALL:
nG = max(_nla.norm(G), _np.exp(-scale_exp))
G = _np.dot(gate, G / nG); scale_exp += _np.log(nG) # LEXICOGRAPHICAL VS MATRIX ORDER
else: G = H
old_err = _np.seterr(over='ignore')
scale = _np.exp(scale_exp)
_np.seterr(**old_err)
return G, scale
else:
G = _np.identity(self.dim)
for lOp in circuit:
G = _np.dot(self.sos.get_operation(lOp).todense(), G) # LEXICOGRAPHICAL VS MATRIX ORDER
return G
def _process_wrt_filter(self, wrt_filter, obj):
""" Helper function for doperation and hoperation below: pulls out pieces of
a wrt_filter argument relevant for a single object (gate or spam vec) """
#Create per-gate with-respect-to parameter filters, used to
# select a subset of all the derivative columns, essentially taking
# a derivative of only a *subset* of all the gate's parameters
if isinstance(wrt_filter, slice):
wrt_filter = _slct.indices(wrt_filter)
if wrt_filter is not None:
obj_wrtFilter = [] # values = object-local param indices
relevant_gpindices = [] # indices into original wrt_filter'd indices
gpindices = obj.gpindices_as_array()
for ii, i in enumerate(wrt_filter):
if i in gpindices:
relevant_gpindices.append(ii)
obj_wrtFilter.append(list(gpindices).index(i))
relevant_gpindices = _np.array(relevant_gpindices, _np.int64)
if len(relevant_gpindices) == 1:
#Don't return a length-1 list, as this doesn't index numpy arrays
# like length>1 lists do... ugh.
relevant_gpindices = slice(relevant_gpindices[0],
relevant_gpindices[0] + 1)
elif len(relevant_gpindices) == 0:
#Don't return a length-0 list, as this doesn't index numpy arrays
# like length>1 lists do... ugh.
relevant_gpindices = slice(0, 0) # slice that results in a zero dimension
else:
obj_wrtFilter = None
relevant_gpindices = obj.gpindices
return obj_wrtFilter, relevant_gpindices
#Vectorizing Identities. (Vectorization)
# Note when vectorizing op uses numpy.flatten rows are kept contiguous, so the first identity below is valid.
# Below we use E(i,j) to denote the elementary matrix where all entries are zero except the (i,j) entry == 1
# if vec(.) concatenates rows (which numpy.flatten does)
# vec( A * E(0,1) * B ) = vec( mx w/ row_i = A[i,0] * B[row1] ) = A tensor B^T * vec( E(0,1) )
# In general: vec( A * X * B ) = A tensor B^T * vec( X )
# if vec(.) stacks columns
# vec( A * E(0,1) * B ) = vec( mx w/ col_i = A[col0] * B[0,1] ) = B^T tensor A * vec( E(0,1) )
# In general: vec( A * X * B ) = B^T tensor A * vec( X )
def doperation(self, op_label, flat=False, wrt_filter=None):
""" Return the derivative of a length-1 (single-gate) sequence """
dim = self.dim
gate = self.sos.get_operation(op_label)
op_wrtFilter, gpindices = self._process_wrt_filter(wrt_filter, gate)
# Allocate memory for the final result
num_deriv_cols = self.Np if (wrt_filter is None) else len(wrt_filter)
flattened_dprod = _np.zeros((dim**2, num_deriv_cols), 'd')
_fas(flattened_dprod, [None, gpindices],
gate.deriv_wrt_params(op_wrtFilter)) # (dim**2, n_params[op_label])
if _slct.length(gpindices) > 0: # works for arrays too
# Compute the derivative of the entire operation sequence with respect to the
# gate's parameters and fill appropriate columns of flattened_dprod.
#gate = self.sos.get_operation[op_label] UNNEEDED (I think)
_fas(flattened_dprod, [None, gpindices],
gate.deriv_wrt_params(op_wrtFilter)) # (dim**2, n_params in wrt_filter for op_label)
if flat:
return flattened_dprod
else:
# axes = (gate_ij, prod_row, prod_col)
return _np.swapaxes(flattened_dprod, 0, 1).reshape((num_deriv_cols, dim, dim))
def hoperation(self, op_label, flat=False, wrt_filter1=None, wrt_filter2=None):
""" Return the hessian of a length-1 (single-gate) sequence """
dim = self.dim
gate = self.sos.get_operation(op_label)
op_wrtFilter1, gpindices1 = self._process_wrt_filter(wrt_filter1, gate)
op_wrtFilter2, gpindices2 = self._process_wrt_filter(wrt_filter2, gate)
# Allocate memory for the final result
num_deriv_cols1 = self.Np if (wrt_filter1 is None) else len(wrt_filter1)
num_deriv_cols2 = self.Np if (wrt_filter2 is None) else len(wrt_filter2)
flattened_hprod = _np.zeros((dim**2, num_deriv_cols1, num_deriv_cols2), 'd')
if _slct.length(gpindices1) > 0 and _slct.length(gpindices2) > 0: # works for arrays too
# Compute the derivative of the entire operation sequence with respect to the
# gate's parameters and fill appropriate columns of flattened_dprod.
_fas(flattened_hprod, [None, gpindices1, gpindices2],
gate.hessian_wrt_params(op_wrtFilter1, op_wrtFilter2))
if flat:
return flattened_hprod
else:
return _np.transpose(flattened_hprod, (1, 2, 0)).reshape(
(num_deriv_cols1, num_deriv_cols2, dim, dim)) # axes = (gate_ij1, gateij2, prod_row, prod_col)
def dproduct(self, circuit, flat=False, wrt_filter=None):
"""
Compute the derivative of a specified sequence of operation labels.
Parameters
----------
circuit : Circuit or tuple of operation labels
The sequence of operation labels.
flat : bool, optional
Affects the shape of the returned derivative array (see below).
wrt_filter : list of ints, optional
If not None, a list of integers specifying which gate parameters
to include in the derivative. Each element is an index into an
array of gate parameters ordered by concatenating each gate's
parameters (in the order specified by the model). This argument
is used internally for distributing derivative calculations across
multiple processors.
Returns
-------
deriv : numpy array
* if flat == False, a M x G x G array, where:
- M == length of the vectorized model (number of model parameters)
- G == the linear dimension of a operation matrix (G x G operation matrices).
and deriv[i,j,k] holds the derivative of the (j,k)-th entry of the product
with respect to the i-th model parameter.
* if flat == True, a N x M array, where:
- N == the number of entries in a single flattened gate (ordering as numpy.flatten)
- M == length of the vectorized model (number of model parameters)
and deriv[i,j] holds the derivative of the i-th entry of the flattened
product with respect to the j-th model parameter.
"""
# LEXICOGRAPHICAL VS MATRIX ORDER
# we do matrix multiplication in this order (easier to think about)
revOpLabelList = tuple(reversed(tuple(circuit)))
N = len(revOpLabelList) # length of operation sequence
# prod = G1 * G2 * .... * GN , a matrix # noqa
# dprod/d(opLabel)_ij = sum_{L s.t. G(L) == oplabel} [ G1 ... G(L-1) dG(L)/dij G(L+1) ... GN ] , a matrix for each given (i,j) # noqa
# vec( dprod/d(opLabel)_ij ) = sum_{L s.t. G(L) == oplabel} [ (G1 ... G(L-1)) tensor (G(L+1) ... GN)^T vec( dG(L)/dij ) ] # noqa
# = [ sum_{L s.t. G(L) == oplabel} [ (G1 ... G(L-1)) tensor (G(L+1) ... GN)^T ]] * vec( dG(L)/dij) ) # noqa
# if dG(L)/dij = E(i,j) # noqa
# = vec(i,j)-col of [ sum_{L s.t. G(L) == oplabel} [ (G1 ... G(L-1)) tensor (G(L+1) ... GN)^T ]] # noqa
#
# So for each opLabel the matrix [ sum_{L s.t. GL == oplabel} [ (G1 ... G(L-1)) tensor (G(L+1) ... GN)^T ]] has
# columns which correspond to the vectorized derivatives of each of the product components (i.e. prod_kl) with
# respect to a given gateLabel_ij. This function returns a concatenated form of the above matrices, so that
# each column corresponds to a (opLabel,i,j) tuple and each row corresponds to an element of the product (els of
# prod.flatten()).
#
# Note: if gate G(L) is just a matrix of parameters, then dG(L)/dij = E(i,j), an elementary matrix
dim = self.dim
#Cache partial products (relatively little mem required)
leftProds = []
G = _np.identity(dim); leftProds.append(G)
for opLabel in revOpLabelList:
G = _np.dot(G, self.sos.get_operation(opLabel).todense())
leftProds.append(G)
rightProdsT = []
G = _np.identity(dim); rightProdsT.append(_np.transpose(G))
for opLabel in reversed(revOpLabelList):
G = _np.dot(self.sos.get_operation(opLabel).todense(), G)
rightProdsT.append(_np.transpose(G))
# Allocate memory for the final result
num_deriv_cols = self.Np if (wrt_filter is None) else len(wrt_filter)
flattened_dprod = _np.zeros((dim**2, num_deriv_cols), 'd')
# For each operation label, compute the derivative of the entire operation sequence
# with respect to only that gate's parameters and fill the appropriate
# columns of flattened_dprod.
uniqueOpLabels = sorted(list(set(revOpLabelList)))
for opLabel in uniqueOpLabels:
gate = self.sos.get_operation(opLabel)
op_wrtFilter, gpindices = self._process_wrt_filter(wrt_filter, gate)
dop_dopLabel = gate.deriv_wrt_params(op_wrtFilter)
for (i, gl) in enumerate(revOpLabelList):
if gl != opLabel: continue # loop over locations of opLabel
LRproduct = _np.kron(leftProds[i], rightProdsT[N - 1 - i]) # (dim**2, dim**2)
_fas(flattened_dprod, [None, gpindices],
_np.dot(LRproduct, dop_dopLabel), add=True) # (dim**2, n_params[opLabel])
if flat:
return flattened_dprod
else:
# axes = (gate_ij, prod_row, prod_col)
return _np.swapaxes(flattened_dprod, 0, 1).reshape((num_deriv_cols, dim, dim))
def hproduct(self, circuit, flat=False, wrt_filter1=None, wrt_filter2=None):
"""
Compute the hessian of a specified sequence of operation labels.
Parameters
----------
circuit : Circuit or tuple of operation labels
The sequence of operation labels.
flat : bool, optional
Affects the shape of the returned derivative array (see below).
wrt_filter1, wrt_filter2 : list of ints, optional
If not None, a list of integers specifying which gate parameters
to differentiate with respect to in the first (row) and second (col)
derivative operations, respectively. Each element is an index into an
array of gate parameters ordered by concatenating each gate's
parameters (in the order specified by the model). This argument
is used internally for distributing derivative calculations across
multiple processors.
Returns
-------
hessian : numpy array
* if flat == False, a M x M x G x G numpy array, where:
- M == length of the vectorized model (number of model parameters)
- G == the linear dimension of a operation matrix (G x G operation matrices).
and hessian[i,j,k,l] holds the derivative of the (k,l)-th entry of the product
with respect to the j-th then i-th model parameters.
* if flat == True, a N x M x M numpy array, where:
- N == the number of entries in a single flattened gate (ordered as numpy.flatten)
- M == length of the vectorized model (number of model parameters)
and hessian[i,j,k] holds the derivative of the i-th entry of the flattened
product with respect to the k-th then k-th model parameters.
"""
# LEXICOGRAPHICAL VS MATRIX ORDER
# we do matrix multiplication in this order (easier to think about)
revOpLabelList = tuple(reversed(tuple(circuit)))
# prod = G1 * G2 * .... * GN , a matrix # noqa
# dprod/d(opLabel)_ij = sum_{L s.t. GL == oplabel} [ G1 ... G(L-1) dG(L)/dij G(L+1) ... GN ] , a matrix for each given (i,j) # noqa
# d2prod/d(opLabel1)_kl*d(opLabel2)_ij = sum_{M s.t. GM == gatelabel1} sum_{L s.t. GL == gatelabel2, M < L} # noqa
# [ G1 ... G(M-1) dG(M)/dkl G(M+1) ... G(L-1) dG(L)/dij G(L+1) ... GN ] + {similar with L < M} # noqa
# + sum{M==L} [ G1 ... G(M-1) d2G(M)/(dkl*dij) G(M+1) ... GN ] # noqa
# a matrix for each given (i,j,k,l) # noqa
# vec( d2prod/d(opLabel1)_kl*d(opLabel2)_ij ) = sum{...} [ G1 ... G(M-1) dG(M)/dkl G(M+1) ... G(L-1) tensor (G(L+1) ... GN)^T vec( dG(L)/dij ) ] # noqa
# = sum{...} [ unvec( G1 ... G(M-1) tensor (G(M+1) ... G(L-1))^T vec( dG(M)/dkl ) ) # noqa
# tensor (G(L+1) ... GN)^T vec( dG(L)/dij ) ] # noqa
# + sum{ L < M} [ G1 ... G(L-1) tensor # noqa
# ( unvec( G(L+1) ... G(M-1) tensor (G(M+1) ... GN)^T vec( dG(M)/dkl ) ) )^T vec( dG(L)/dij ) ] # noqa
# + sum{ L == M} [ G1 ... G(M-1) tensor (G(M+1) ... GN)^T vec( d2G(M)/dkl*dji ) # noqa
#
# Note: ignoring L == M terms assumes that d^2 G/(dij)^2 == 0, which is true IF each operation matrix element
# is at most *linear* in each of the gate parameters. If this is not the case, need LinearOperator objects to
# have a 2nd-deriv method in addition of deriv_wrt_params
#
# Note: unvec( X ) can be done efficiently by actually computing X^T ( note (A tensor B)^T = A^T tensor B^T )
# and using numpy's reshape
dim = self.dim
uniqueOpLabels = sorted(list(set(revOpLabelList)))
used_operations = _collections.OrderedDict()
#Cache processed parameter filters for multiple uses below
gpindices1 = {}; gate_wrtFilters1 = {}
gpindices2 = {}; gate_wrtFilters2 = {}
for l in uniqueOpLabels:
used_operations[l] = self.sos.get_operation(l)
gate_wrtFilters1[l], gpindices1[l] = self._process_wrt_filter(wrt_filter1, used_operations[l])
gate_wrtFilters2[l], gpindices2[l] = self._process_wrt_filter(wrt_filter2, used_operations[l])
#Cache partial products (relatively little mem required)
prods = {}
ident = _np.identity(dim)
for (i, opLabel1) in enumerate(revOpLabelList): # loop over "starting" gate
prods[(i, i - 1)] = ident # product of no gates
G = ident
for (j, opLabel2) in enumerate(revOpLabelList[i:], start=i): # loop over "ending" gate (>= starting gate)
G = _np.dot(G, self.sos.get_operation(opLabel2).todense())
prods[(i, j)] = G
prods[(len(revOpLabelList), len(revOpLabelList) - 1)] = ident # product of no gates
#Also Cache gate jacobians (still relatively little mem required)
dop_dopLabel1 = {
opLabel: gate.deriv_wrt_params(gate_wrtFilters1[opLabel])
for opLabel, gate in used_operations.items()}
if wrt_filter1 == wrt_filter2:
dop_dopLabel2 = dop_dopLabel1
else:
dop_dopLabel2 = {
opLabel: gate.deriv_wrt_params(gate_wrtFilters2[opLabel])
for opLabel, gate in used_operations.items()}
#Finally, cache any nonzero gate hessians (memory?)
hop_dopLabels = {}
for opLabel, gate in used_operations.items():
if gate.has_nonzero_hessian():
hop_dopLabels[opLabel] = gate.hessian_wrt_params(
gate_wrtFilters1[opLabel], gate_wrtFilters2[opLabel])
# Allocate memory for the final result
num_deriv_cols1 = self.Np if (wrt_filter1 is None) else len(wrt_filter1)
num_deriv_cols2 = self.Np if (wrt_filter2 is None) else len(wrt_filter2)
flattened_d2prod = _np.zeros((dim**2, num_deriv_cols1, num_deriv_cols2), 'd')
# For each pair of gates in the string, compute the hessian of the entire
# operation sequence with respect to only those two gates' parameters and fill
# add the result to the appropriate block of flattened_d2prod.
#NOTE: if we needed to perform a hessian calculation (i.e. for l==m) then
# it could make sense to iterate through the self.operations.keys() as in
# dproduct(...) and find the labels in the string which match the current
# gate (so we only need to compute this gate hessian once). But since we're
# assuming that the gates are at most linear in their parameters, this
# isn't currently needed.
N = len(revOpLabelList)
for m, opLabel1 in enumerate(revOpLabelList):
inds1 = gpindices1[opLabel1]
nDerivCols1 = dop_dopLabel1[opLabel1].shape[1]
if nDerivCols1 == 0: continue
for l, opLabel2 in enumerate(revOpLabelList):
inds2 = gpindices1[opLabel2]
#nDerivCols2 = dop_dopLabel2[opLabel2].shape[1]
# FUTURE: we could add logic that accounts for the symmetry of the Hessian, so that
# if gl1 and gl2 are both in opsToVectorize1 and opsToVectorize2 we only compute d2(prod)/d(gl1)d(gl2)
# and not d2(prod)/d(gl2)d(gl1) ...
if m < l:
x0 = _np.kron(_np.transpose(prods[(0, m - 1)]), prods[(m + 1, l - 1)]) # (dim**2, dim**2)
x = _np.dot(_np.transpose(dop_dopLabel1[opLabel1]), x0); xv = x.view() # (nDerivCols1,dim**2)
xv.shape = (nDerivCols1, dim, dim) # (reshape without copying - throws error if copy is needed)
y = _np.dot(_np.kron(xv, _np.transpose(prods[(l + 1, N - 1)])), dop_dopLabel2[opLabel2])
# above: (nDerivCols1,dim**2,dim**2) * (dim**2,nDerivCols2) = (nDerivCols1,dim**2,nDerivCols2)
flattened_d2prod[:, inds1, inds2] += _np.swapaxes(y, 0, 1)
# above: dim = (dim2, nDerivCols1, nDerivCols2);
# swapaxes takes (kl,vec_prod_indx,ij) => (vec_prod_indx,kl,ij)
elif l < m:
x0 = _np.kron(_np.transpose(prods[(l + 1, m - 1)]), prods[(m + 1, N - 1)]) # (dim**2, dim**2)
x = _np.dot(_np.transpose(dop_dopLabel1[opLabel1]), x0); xv = x.view() # (nDerivCols1,dim**2)
xv.shape = (nDerivCols1, dim, dim) # (reshape without copying - throws error if copy is needed)
# transposes each of the now un-vectorized dim x dim mxs corresponding to a single kl
xv = _np.swapaxes(xv, 1, 2)
y = _np.dot(_np.kron(prods[(0, l - 1)], xv), dop_dopLabel2[opLabel2])
# above: (nDerivCols1,dim**2,dim**2) * (dim**2,nDerivCols2) = (nDerivCols1,dim**2,nDerivCols2)
flattened_d2prod[:, inds1, inds2] += _np.swapaxes(y, 0, 1)
# above: dim = (dim2, nDerivCols1, nDerivCols2);
# swapaxes takes (kl,vec_prod_indx,ij) => (vec_prod_indx,kl,ij)
else:
# l==m, which we *used* to assume gave no contribution since we assume all gate elements are at most
# linear in the parameters
assert(opLabel1 == opLabel2)
if opLabel1 in hop_dopLabels: # indicates a non-zero hessian
x0 = _np.kron(_np.transpose(prods[(0, m - 1)]), prods[(m + 1, N - 1)]) # (dim**2, dim**2)
# (nDerivCols1,nDerivCols2,dim**2)
x = _np.dot(_np.transpose(hop_dopLabels[opLabel1], axes=(1, 2, 0)), x0); xv = x.view()
xv = _np.transpose(xv, axes=(2, 0, 1)) # (dim2, nDerivCols1, nDerivCols2)
flattened_d2prod[:, inds1, inds2] += xv
if flat:
return flattened_d2prod # axes = (vectorized_op_el_index, model_parameter1, model_parameter2)
else:
vec_kl_size, vec_ij_size = flattened_d2prod.shape[1:3] # == num_deriv_cols1, num_deriv_cols2
return _np.rollaxis(flattened_d2prod, 0, 3).reshape((vec_kl_size, vec_ij_size, dim, dim))
# axes = (model_parameter1, model_parameter2, model_element_row, model_element_col)
def prs(self, rholabel, elabels, circuit, clip_to, use_scaling=False, time=None):
"""
Compute probabilities of a multiple "outcomes" (spam-tuples) for a single
operation sequence. The spam tuples may only vary in their effect-label (their
prep labels must be the same)
Parameters
----------
rholabel : Label
The state preparation label.
elabels : list
A list of :class:`Label` objects giving the *simplified* effect labels.
circuit : Circuit or tuple
A tuple-like object of *simplified* gates (e.g. may include
instrument elements like 'Imyinst_0')
clip_to : 2-tuple
(min,max) to clip returned probability to if not None.
Only relevant when pr_mx_to_fill is not None.
use_scaling : bool, optional
Whether to use a post-scaled product internally. If False, this
routine will run slightly faster, but with a chance that the
product will overflow and the subsequent trace operation will
yield nan as the returned probability.
time : float, optional
The *start* time at which `circuit` is evaluated.
Returns
-------
numpy.ndarray
An array of floating-point probabilities, corresponding to
the elements of `elabels`.
"""
assert(time is None), "MatrixForwardSimulator cannot be used to simulate time-dependent circuits"
rho, Es = self._rho_es_from_spam_tuples(rholabel, elabels)
#shapes: rho = (N,1), Es = (len(elabels),N)
if use_scaling:
old_err = _np.seterr(over='ignore')
G, scale = self.product(circuit, True)
if self.evotype == "statevec":
ps = _np.real(_np.abs(_np.dot(Es, _np.dot(G, rho)) * scale)**2)
else: # evotype == "densitymx"
# probability, with scaling applied (may generate overflow, but OK)
ps = _np.real(_np.dot(Es, _np.dot(G, rho)) * scale)
_np.seterr(**old_err)
else: # no scaling -- faster but susceptible to overflow
G = self.product(circuit, False)
if self.evotype == "statevec":
ps = _np.real(_np.abs(_np.dot(Es, _np.dot(G, rho)))**2)
else: # evotype == "densitymx"
ps = _np.real(_np.dot(Es, _np.dot(G, rho)))
ps = ps.flatten()
if _np.any(_np.isnan(ps)):
if len(circuit) < 10:
strToPrint = str(circuit)
else:
strToPrint = str(circuit[0:10]) + " ... (len %d)" % len(circuit)
_warnings.warn("pr(%s) == nan" % strToPrint)
#DEBUG: print "backtrace" of product leading up to nan
#G = _np.identity( self.dim ); total_exp = 0.0
#for i,lOp in enumerate(gateLabelList):
# G = _np.dot(G,self[lOp]) # product of gates, starting with G0
# nG = norm(G); G /= nG; total_exp += log(nG) # scale and keep track of exponent
#
# p = _mt.trace( _np.dot(self.SPAMs[spamLabel],G) ) * exp(total_exp) # probability
# print "%d: p = %g, norm %g, exp %g\n%s" % (i,p,norm(G),total_exp,str(G))
# if _np.isnan(p): raise ValueError("STOP")
if clip_to is not None:
ret = _np.clip(ps, clip_to[0], clip_to[1])
else:
ret = ps
#DEBUG CHECK
#check_ps = _np.array( [ self.pr( (rholabel,elabel), circuit, clip_to, scale) for elabel in elabels ])
#assert(_np.linalg.norm(ps-check_ps) < 1e-8)
return ret
def dpr(self, spam_tuple, circuit, return_pr, clip_to):
"""
Compute the derivative of a probability generated by a operation sequence and
spam tuple as a 1 x M numpy array, where M is the number of model
parameters.
Parameters
----------
spam_tuple : (rho_label, simplified_effect_label)
Specifies the prep and POVM effect used to compute the probability.
circuit : Circuit or tuple
A tuple-like object of *simplified* gates (e.g. may include
instrument elements like 'Imyinst_0')
return_pr : bool
when set to True, additionally return the probability itself.
clip_to : 2-tuple
(min,max) to clip returned probability to if not None.
Only relevant when pr_mx_to_fill is not None.
Returns
-------
derivative : numpy array
a 1 x M numpy array of derivatives of the probability w.r.t.
each model parameter (M is the length of the vectorized model).
probability : float
only returned if return_pr == True.
"""
if self.evotype == "statevec": raise NotImplementedError("Unitary evolution not fully supported yet!")
# To support unitary evolution we need to:
# - alter product, dproduct, etc. to allow for *complex* derivatives, since matrices can be complex
# - update probability-derivative computations: dpr/dx -> d|pr|^2/dx = d(pr*pr.C)/dx = dpr/dx*pr.C + pr*dpr/dx.C
# = 2 Re(dpr/dx*pr.C) , where dpr/dx is the usual density-matrix-mode probability
# (TODO in FUTURE)
# pr = Tr( |rho><E| * prod ) = sum E_k prod_kl rho_l
# dpr/d(op_label)_ij = sum E_k [dprod/d(op_label)_ij]_kl rho_l
# dpr/d(rho)_i = sum E_k prod_ki
# dpr/d(E)_i = sum prod_il rho_l
rholabel, elabel = spam_tuple # can't deal w/"custom" spam label...
rho, E = self._rho_e_from_spam_tuple(spam_tuple)
rhoVec = self.sos.get_prep(rholabel) # distinct from rho,E b/c rho,E are
EVec = self.sos.get_effect(elabel) # arrays, these are SPAMVecs
#Derivs wrt Gates
old_err = _np.seterr(over='ignore')
prod, scale = self.product(circuit, True)
dprod_dOps = self.dproduct(circuit)
dpr_dOps = _np.empty((1, self.Np))
for i in range(self.Np):
dpr_dOps[0, i] = float(_np.dot(E, _np.dot(dprod_dOps[i], rho)))
if return_pr:
p = _np.dot(E, _np.dot(prod, rho)) * scale # may generate overflow, but OK
if clip_to is not None: p = _np.clip(p, clip_to[0], clip_to[1])
#Derivs wrt SPAM
derivWrtAnyRhovec = scale * _np.dot(E, prod)
dpr_drhos = _np.zeros((1, self.Np))
_fas(dpr_drhos, [0, self.sos.get_prep(rholabel).gpindices],
_np.dot(derivWrtAnyRhovec, rhoVec.deriv_wrt_params())) # may overflow, but OK
dpr_dEs = _np.zeros((1, self.Np))
derivWrtAnyEvec = scale * _np.transpose(_np.dot(prod, rho)) # may overflow, but OK
# (** doesn't depend on eIndex **) -- TODO: should also conjugate() here if complex?
_fas(dpr_dEs, [0, EVec.gpindices],
_np.dot(derivWrtAnyEvec, EVec.deriv_wrt_params()))
_np.seterr(**old_err)
if return_pr:
return dpr_drhos + dpr_dEs + dpr_dOps, p
else: return dpr_drhos + dpr_dEs + dpr_dOps
def hpr(self, spam_tuple, circuit, return_pr, return_deriv, clip_to):
"""
Compute the Hessian of a probability generated by a operation sequence and
spam tuple as a 1 x M x M array, where M is the number of model
parameters.
Parameters
----------
spam_tuple : (rho_label, simplified_effect_label)
Specifies the prep and POVM effect used to compute the probability.
circuit : Circuit or tuple
A tuple-like object of *simplified* gates (e.g. may include
instrument elements like 'Imyinst_0')
return_pr : bool
when set to True, additionally return the probability itself.
return_deriv : bool
when set to True, additionally return the derivative of the
probability.
clip_to : 2-tuple
(min,max) to clip returned probability to if not None.
Only relevant when pr_mx_to_fill is not None.
Returns
-------
hessian : numpy array
a 1 x M x M array, where M is the number of model parameters.
hessian[0,j,k] is the derivative of the probability w.r.t. the
k-th then the j-th model parameter.
derivative : numpy array
only returned if return_deriv == True. A 1 x M numpy array of
derivatives of the probability w.r.t. each model parameter.
probability : float
only returned if return_pr == True.
"""
if self.evotype == "statevec": raise NotImplementedError("Unitary evolution not fully supported yet!")
# pr = Tr( |rho><E| * prod ) = sum E_k prod_kl rho_l
# d2pr/d(opLabel1)_mn d(opLabel2)_ij = sum E_k [dprod/d(opLabel1)_mn d(opLabel2)_ij]_kl rho_l
# d2pr/d(rho)_i d(op_label)_mn = sum E_k [dprod/d(op_label)_mn]_ki (and same for other diff order)
# d2pr/d(E)_i d(op_label)_mn = sum [dprod/d(op_label)_mn]_il rho_l (and same for other diff order)
# d2pr/d(E)_i d(rho)_j = prod_ij (and same for other diff order)
# d2pr/d(E)_i d(E)_j = 0
# d2pr/d(rho)_i d(rho)_j = 0
rholabel, elabel = spam_tuple
rho, E = self._rho_e_from_spam_tuple(spam_tuple)
rhoVec = self.sos.get_prep(rholabel) # distinct from rho,E b/c rho,E are
EVec = self.sos.get_effect(elabel) # arrays, these are SPAMVecs
d2prod_dGates = self.hproduct(circuit)
assert(d2prod_dGates.shape[0] == d2prod_dGates.shape[1])
d2pr_dOps2 = _np.empty((1, self.Np, self.Np))
for i in range(self.Np):
for j in range(self.Np):
d2pr_dOps2[0, i, j] = float(_np.dot(E, _np.dot(d2prod_dGates[i, j], rho)))
old_err = _np.seterr(over='ignore')
prod, scale = self.product(circuit, True)
if return_pr:
p = _np.dot(E, _np.dot(prod, rho)) * scale # may generate overflow, but OK
if clip_to is not None: p = _np.clip(p, clip_to[0], clip_to[1])
dprod_dOps = self.dproduct(circuit)
assert(dprod_dOps.shape[0] == self.Np)
if return_deriv: # same as in dpr(...)
dpr_dOps = _np.empty((1, self.Np))
for i in range(self.Np):
dpr_dOps[0, i] = float(_np.dot(E, _np.dot(dprod_dOps[i], rho)))
#Derivs wrt SPAM
if return_deriv: # same as in dpr(...)
dpr_drhos = _np.zeros((1, self.Np))
derivWrtAnyRhovec = scale * _np.dot(E, prod)
_fas(dpr_drhos, [0, self.sos.get_prep(rholabel).gpindices],
_np.dot(derivWrtAnyRhovec, rhoVec.deriv_wrt_params())) # may overflow, but OK
dpr_dEs = _np.zeros((1, self.Np))
derivWrtAnyEvec = scale * _np.transpose(_np.dot(prod, rho)) # may overflow, but OK
_fas(dpr_dEs, [0, EVec.gpindices],
_np.dot(derivWrtAnyEvec, EVec.deriv_wrt_params()))
dpr = dpr_drhos + dpr_dEs + dpr_dOps
d2pr_drhos = _np.zeros((1, self.Np, self.Np))
_fas(d2pr_drhos, [0, None, self.sos.get_prep(rholabel).gpindices],
_np.dot(_np.dot(E, dprod_dOps), rhoVec.deriv_wrt_params())[0]) # (= [0,:,:])
d2pr_dEs = _np.zeros((1, self.Np, self.Np))
derivWrtAnyEvec = _np.squeeze(_np.dot(dprod_dOps, rho), axis=(2,))
_fas(d2pr_dEs, [0, None, EVec.gpindices],
_np.dot(derivWrtAnyEvec, EVec.deriv_wrt_params()))
d2pr_dErhos = _np.zeros((1, self.Np, self.Np))
derivWrtAnyEvec = scale * _np.dot(prod, rhoVec.deriv_wrt_params()) # may generate overflow, but OK
_fas(d2pr_dErhos, [0, EVec.gpindices, self.sos.get_prep(rholabel).gpindices],
_np.dot(_np.transpose(EVec.deriv_wrt_params()), derivWrtAnyEvec))
#Note: these 2nd derivatives are non-zero when the spam vectors have
# a more than linear dependence on their parameters.
if self.sos.get_prep(rholabel).has_nonzero_hessian():
derivWrtAnyRhovec = scale * _np.dot(E, prod) # may overflow, but OK
d2pr_d2rhos = _np.zeros((1, self.Np, self.Np))
_fas(d2pr_d2rhos, [0, self.sos.get_prep(rholabel).gpindices, self.sos.get_prep(rholabel).gpindices],
_np.tensordot(derivWrtAnyRhovec, self.sos.get_prep(rholabel).hessian_wrt_params(), (1, 0)))
# _np.einsum('ij,jkl->ikl', derivWrtAnyRhovec, self.sos.get_prep(rholabel).hessian_wrt_params())
else:
d2pr_d2rhos = 0
if self.sos.get_effect(elabel).has_nonzero_hessian():
derivWrtAnyEvec = scale * _np.transpose(_np.dot(prod, rho)) # may overflow, but OK
d2pr_d2Es = _np.zeros((1, self.Np, self.Np))
_fas(d2pr_d2Es, [0, self.sos.get_effect(elabel).gpindices, self.sos.get_effect(elabel).gpindices],
_np.tensordot(derivWrtAnyEvec, self.sos.get_effect(elabel).hessian_wrt_params(), (1, 0)))
# _np.einsum('ij,jkl->ikl',derivWrtAnyEvec,self.sos.get_effect(elabel).hessian_wrt_params())
else:
d2pr_d2Es = 0
ret = d2pr_dErhos + _np.transpose(d2pr_dErhos, (0, 2, 1)) + \
d2pr_drhos + _np.transpose(d2pr_drhos, (0, 2, 1)) + \
d2pr_dEs + _np.transpose(d2pr_dEs, (0, 2, 1)) + \
d2pr_d2rhos + d2pr_d2Es + d2pr_dOps2
# Note: add transposes b/c spam terms only compute one triangle of hessian
# Note: d2pr_d2rhos and d2pr_d2Es terms are always zero
_np.seterr(**old_err)
if return_deriv:
if return_pr: return ret, dpr, p
else: return ret, dpr
else:
if return_pr: return ret, p
else: return ret
## BEGIN CACHE FUNCTIONS
def _compute_product_cache(self, eval_tree, comm=None):
"""
Computes a tree of products in a linear cache space. Will *not*
parallelize computation, even if given a split tree (since there's
no good way to reconstruct the parent tree's *non-final* elements from
those of the sub-trees). Note also that there would be no memory savings
from using a split tree. In short, parallelization should be done at a
higher level.
"""
dim = self.dim
#Note: previously, we tried to allow for parallelization of
# _compute_product_cache when the tree was split, but this is was
# incorrect (and luckily never used) - so it's been removed.
if comm is not None: # ignoring comm since can't do anything with it!
#_warnings.warn("More processors than can be used for product computation")
pass # this is a fairly common occurrence, and doesn't merit a warning
# ------------------------------------------------------------------
if eval_tree.is_split():
_warnings.warn("Ignoring tree splitting in product cache calc.")
cacheSize = len(eval_tree)
prodCache = _np.zeros((cacheSize, dim, dim))
scaleCache = _np.zeros(cacheSize, 'd')
#First element of cache are given by eval_tree's initial single- or zero-operation labels
for i, opLabel in zip(eval_tree.get_init_indices(), eval_tree.get_init_labels()):
if opLabel == "": # special case of empty label == no gate
prodCache[i] = _np.identity(dim)
# Note: scaleCache[i] = 0.0 from initialization
else:
gate = self.sos.get_operation(opLabel).todense()
nG = max(_nla.norm(gate), 1.0)
prodCache[i] = gate / nG
scaleCache[i] = _np.log(nG)
#evaluate operation sequences using tree (skip over the zero and single-gate-strings)
#cnt = 0
for i in eval_tree.get_evaluation_order():
# combine iLeft + iRight => i
# LEXICOGRAPHICAL VS MATRIX ORDER Note: we reverse iLeft <=> iRight from eval_tree because
# (iRight,iLeft,iFinal) = tup implies circuit[i] = circuit[iLeft] + circuit[iRight], but we want:
# since then matrixOf(circuit[i]) = matrixOf(circuit[iLeft]) * matrixOf(circuit[iRight])
(iRight, iLeft) = eval_tree[i]
L, R = prodCache[iLeft], prodCache[iRight]
prodCache[i] = _np.dot(L, R)
scaleCache[i] = scaleCache[iLeft] + scaleCache[iRight]
if prodCache[i].max() < PSMALL and prodCache[i].min() > -PSMALL:
nL, nR = max(_nla.norm(L), _np.exp(-scaleCache[iLeft]),
1e-300), max(_nla.norm(R), _np.exp(-scaleCache[iRight]), 1e-300)
sL, sR = L / nL, R / nR
prodCache[i] = _np.dot(sL, sR); scaleCache[i] += _np.log(nL) + _np.log(nR)
#print "bulk_product DEBUG: %d rescalings out of %d products" % (cnt, len(eval_tree))
nanOrInfCacheIndices = (~_np.isfinite(prodCache)).nonzero()[0] # may be duplicates (a list, not a set)
# since all scaled gates start with norm <= 1, products should all have norm <= 1
assert(len(nanOrInfCacheIndices) == 0)
return prodCache, scaleCache
def _compute_dproduct_cache(self, eval_tree, prod_cache, scale_cache,
comm=None, wrt_slice=None, profiler=None):
"""
Computes a tree of product derivatives in a linear cache space. Will
use derivative columns and then (and only when needed) a split tree
to parallelize computation, since there are no memory savings
from using a split tree.
"""
if profiler is None: profiler = _dummy_profiler
dim = self.dim
nDerivCols = self.Np if (wrt_slice is None) \
else _slct.length(wrt_slice)
deriv_shape = (nDerivCols, dim, dim)
cacheSize = len(eval_tree)
# ------------------------------------------------------------------
#print("MPI: _compute_dproduct_cache begin: %d deriv cols" % nDerivCols)
if comm is not None and comm.Get_size() > 1:
#print("MPI: _compute_dproduct_cache called w/comm size %d" % comm.Get_size())
# parallelize of deriv cols, then sub-trees (if available and necessary)
if comm.Get_size() > nDerivCols:
#If there are more processors than deriv cols, give a
# warning -- note that we *cannot* make use of a tree being
# split because there's no good way to reconstruct the
# *non-final* parent-tree elements from those of the sub-trees.
_warnings.warn("Increased speed could be obtained"
" by giving dproduct cache computation"
" *fewer* processors and *smaller* (sub-)tree"
" (e.g. by splitting tree beforehand), as there"
" are more cpus than derivative columns.")
# Use comm to distribute columns
allDerivColSlice = slice(0, nDerivCols) if (wrt_slice is None) else wrt_slice
_, myDerivColSlice, _, mySubComm = \
_mpit.distribute_slice(allDerivColSlice, comm)
#print("MPI: _compute_dproduct_cache over %d cols (%s) (rank %d computing %s)" \
# % (nDerivCols, str(allDerivColIndices), comm.Get_rank(), str(myDerivColIndices)))
if mySubComm is not None and mySubComm.Get_size() > 1:
_warnings.warn("Too many processors to make use of in "
" _compute_dproduct_cache.")
if mySubComm.Get_rank() > 0: myDerivColSlice = slice(0, 0)
#don't compute anything on "extra", i.e. rank != 0, cpus
my_results = self._compute_dproduct_cache(
eval_tree, prod_cache, scale_cache, None, myDerivColSlice, profiler)
# pass None as comm, *not* mySubComm, since we can't do any
# further parallelization
tm = _time.time()
all_results = comm.allgather(my_results)
profiler.add_time("MPI IPC", tm)
return _np.concatenate(all_results, axis=1) # TODO: remove this concat w/better gather?
# ------------------------------------------------------------------
tSerialStart = _time.time()
if eval_tree.is_split():
_warnings.warn("Ignoring tree splitting in dproduct cache calc.")
dProdCache = _np.zeros((cacheSize,) + deriv_shape)
# This iteration **must** match that in bulk_evaltree
# in order to associate the right single-gate-strings w/indices
wrtIndices = _slct.indices(wrt_slice) if (wrt_slice is not None) else None
for i, opLabel in zip(eval_tree.get_init_indices(), eval_tree.get_init_labels()):
if opLabel == "": # special case of empty label == no gate
dProdCache[i] = _np.zeros(deriv_shape)
else:
#doperation = self.dproduct( (opLabel,) , wrt_filter=wrtIndices)
doperation = self.doperation(opLabel, wrt_filter=wrtIndices)
dProdCache[i] = doperation / _np.exp(scale_cache[i])
#profiler.print_mem("DEBUGMEM: POINT1"); profiler.comm.barrier()
#evaluate operation sequences using tree (skip over the zero and single-gate-strings)
for i in eval_tree.get_evaluation_order():
tm = _time.time()
# combine iLeft + iRight => i
# LEXICOGRAPHICAL VS MATRIX ORDER Note: we reverse iLeft <=> iRight from eval_tree because
# (iRight,iLeft,iFinal) = tup implies circuit[i] = circuit[iLeft] + circuit[iRight], but we want:
# since then matrixOf(circuit[i]) = matrixOf(circuit[iLeft]) * matrixOf(circuit[iRight])
(iRight, iLeft) = eval_tree[i]
L, R = prod_cache[iLeft], prod_cache[iRight]
dL, dR = dProdCache[iLeft], dProdCache[iRight]
dProdCache[i] = _np.dot(dL, R) + \
_np.swapaxes(_np.dot(L, dR), 0, 1) # dot(dS, T) + dot(S, dT)
profiler.add_time("compute_dproduct_cache: dots", tm)
profiler.add_count("compute_dproduct_cache: dots")
scale = scale_cache[i] - (scale_cache[iLeft] + scale_cache[iRight])
if abs(scale) > 1e-8: # _np.isclose(scale,0) is SLOW!
dProdCache[i] /= _np.exp(scale)
if dProdCache[i].max() < DSMALL and dProdCache[i].min() > -DSMALL:
_warnings.warn("Scaled dProd small in order to keep prod managable.")
elif _np.count_nonzero(dProdCache[i]) and dProdCache[i].max() < DSMALL and dProdCache[i].min() > -DSMALL:
_warnings.warn("Would have scaled dProd but now will not alter scale_cache.")
#profiler.print_mem("DEBUGMEM: POINT2"); profiler.comm.barrier()
profiler.add_time("compute_dproduct_cache: serial", tSerialStart)
profiler.add_count("compute_dproduct_cache: num columns", nDerivCols)
return dProdCache
def _compute_hproduct_cache(self, eval_tree, prod_cache, d_prod_cache1,
d_prod_cache2, scale_cache, comm=None,
wrt_slice1=None, wrt_slice2=None):
"""
Computes a tree of product 2nd derivatives in a linear cache space. Will
use derivative rows and columns and then (as needed) a split tree
to parallelize computation, since there are no memory savings