65
65
frequencies
66
66
"""
67
67
from collections import deque
68
- from itertools import accumulate , repeat
68
+ from itertools import accumulate , repeat , zip_longest
69
69
from typing import Any , Callable , Dict , Optional , Sequence , Tuple , Union
70
70
from warnings import warn
71
71
@@ -93,39 +93,49 @@ def _propagate_eigenvectors(propagators, eigvecs):
93
93
return propagators .transpose (0 , 2 , 1 ).conj () @ eigvecs
94
94
95
95
96
- def _transform_noise_operators ( n_coeffs , n_opers , eigvecs ):
97
- r"""
98
- Transform noise operators into the eigenspaces spanned by eigvecs.
96
+ def _transform_hamiltonian ( eigvecs , opers , coeffs = None ):
97
+ r"""Transform a Hamiltonian into the eigenspaces spanned by eigvecs.
98
+
99
99
I.e., the following transformation is performed:
100
100
101
101
.. math::
102
102
103
- B_\alpha\rightarrow s_\alpha^{(g)}V^{(g)}B_\alpha V^{(g)\dagger}
103
+ s_\alpha^{(g)} B_\alpha\rightarrow
104
+ s_\alpha^{(g)} V^{(g)}B_\alpha V^{(g)\dagger}
105
+
106
+ where :math:`s_\alpha^{(g)}` are coefficients of the operator
107
+ :math:`B_\alpha`.
104
108
105
109
"""
106
- assert len (n_opers ) == len (n_coeffs )
107
- n_opers_transformed = np .empty ((len (n_opers ), * eigvecs .shape ), dtype = complex )
108
- for j , (n_coeff , n_oper ) in enumerate (zip (n_coeffs , n_opers )):
109
- n_opers_transformed [j ] = n_oper @ eigvecs
110
- n_opers_transformed [j ] = eigvecs .conj ().transpose (0 , 2 , 1 ) @ n_opers_transformed [j ]
111
- n_opers_transformed [j ] *= n_coeff [:, None , None ]
110
+ if coeffs is None :
111
+ coeffs = []
112
+ else :
113
+ assert len (opers ) == len (coeffs )
112
114
113
- return n_opers_transformed
115
+ opers_transformed = np .empty ((len (opers ), * eigvecs .shape ), dtype = complex )
116
+ for j , (coeff , oper ) in enumerate (zip_longest (coeffs , opers , fillvalue = None )):
117
+ opers_transformed [j ] = _transform_by_unitary (eigvecs , oper , out = opers_transformed [j ])
118
+ if coeff is not None :
119
+ opers_transformed [j ] *= coeff [:, None , None ]
114
120
121
+ return opers_transformed
115
122
116
- def _transform_basis ( basis , eigvecs_propagated , out = None ):
117
- r"""
118
- Transform the basis into the eigenspace spanned by V propagated by Q
123
+
124
+ def _transform_by_unitary ( unitary , oper , out = None ):
125
+ r""" Transform the operators by a unitary. Uses broadcasting.
119
126
120
127
I.e., the following transformation is performed:
121
128
122
129
.. math::
123
130
124
- C_k\rightarrow Q_{g-1}V^{(g)\dagger} C_k V^{(g)}Q_{g-1} ^\dagger.
131
+ C_k\rightarrow U C_k U ^\dagger.
125
132
126
133
"""
127
- out = np .matmul (basis , eigvecs_propagated , out = out )
128
- out = np .matmul (eigvecs_propagated .conj ().T , out , out = out )
134
+ if out is None :
135
+ out = np .empty (oper .shape , dtype = oper .dtype )
136
+
137
+ out = np .matmul (oper , unitary , out = out )
138
+ out = np .matmul (unitary .conj ().swapaxes (- 1 , - 2 ), out , out = out )
129
139
return out
130
140
131
141
@@ -147,7 +157,7 @@ def _first_order_integral(E: ndarray, eigvals: ndarray, dt: float,
147
157
int_buf .imag = np .add .outer (E , dE , out = int_buf .imag )
148
158
149
159
# Catch zero-division
150
- mask = (int_buf .imag != 0 )
160
+ mask = (np . abs ( int_buf .imag ) > 1e-7 )
151
161
exp_buf = util .cexp (int_buf .imag * dt , out = exp_buf , where = mask )
152
162
exp_buf = np .subtract (exp_buf , 1 , out = exp_buf , where = mask )
153
163
int_buf = np .divide (exp_buf , int_buf , out = int_buf , where = mask )
@@ -463,7 +473,8 @@ def calculate_noise_operators_from_scratch(
463
473
n_coeffs : Sequence [Coefficients ],
464
474
dt : Coefficients ,
465
475
t : Optional [Coefficients ] = None ,
466
- show_progressbar : bool = False
476
+ show_progressbar : bool = False ,
477
+ cache_intermediates : bool = False
467
478
) -> ndarray :
468
479
r"""
469
480
Calculate the noise operators in interaction picture from scratch.
@@ -563,30 +574,44 @@ def calculate_noise_operators_from_scratch(
563
574
n_coeffs = np .asarray (n_coeffs )
564
575
565
576
# Precompute noise opers transformed to eigenbasis of each pulse
566
- # segment and Q ^\dagger @ V
577
+ # segment and V ^\dagger @ Q
567
578
eigvecs_propagated = _propagate_eigenvectors (eigvecs , propagators [:- 1 ])
568
- n_opers_transformed = _transform_noise_operators ( n_coeffs , n_opers , eigvecs )
579
+ n_opers_transformed = _transform_hamiltonian ( eigvecs , n_opers , n_coeffs )
569
580
570
581
# Allocate memory
571
582
exp_buf , int_buf = np .empty ((2 , len (omega ), d , d ), dtype = complex )
572
- intermediate = np .empty ((len (omega ), len (n_opers ), d , d ), dtype = complex )
573
583
noise_operators = np .zeros ((len (omega ), len (n_opers ), d , d ), dtype = complex )
574
584
585
+ if cache_intermediates :
586
+ sum_cache = np .empty ((len (dt ), len (omega ), len (n_opers ), d , d ), dtype = complex )
587
+ else :
588
+ sum_buf = np .empty ((len (omega ), len (n_opers ), d , d ), dtype = complex )
589
+
575
590
# Set up reusable expressions
576
591
expr_1 = oe .contract_expression ('akl,okl->oakl' ,
577
592
n_opers_transformed [:, 0 ].shape , int_buf .shape )
578
593
expr_2 = oe .contract_expression ('ji,...jk,kl' ,
579
- eigvecs_propagated [0 ].shape , intermediate . shape ,
594
+ eigvecs_propagated [0 ].shape , ( len ( omega ), len ( n_opers ), d , d ) ,
580
595
eigvecs_propagated [0 ].shape , optimize = [(0 , 1 ), (0 , 1 )])
581
596
582
597
for g in util .progressbar_range (len (dt ), show_progressbar = show_progressbar ,
583
598
desc = 'Calculating noise operators' ):
599
+ if cache_intermediates :
600
+ # Assign references to the locations in the cache for the quantities
601
+ # that should be stored
602
+ sum_buf = sum_cache [g ]
603
+
584
604
int_buf = _first_order_integral (omega , eigvals [g ], dt [g ], exp_buf , int_buf )
585
- intermediate = expr_1 (n_opers_transformed [:, g ],
586
- util . cexp ( omega [:, None , None ] * t [ g ]) * int_buf , out = intermediate )
605
+ sum_buf = expr_1 (n_opers_transformed [:, g ], util . cexp ( omega * t [ g ])[:, None , None ] * int_buf ,
606
+ out = sum_buf )
587
607
588
- noise_operators += expr_2 (eigvecs_propagated [g ].conj (), intermediate ,
589
- eigvecs_propagated [g ])
608
+ noise_operators += expr_2 (eigvecs_propagated [g ].conj (), sum_buf , eigvecs_propagated [g ],
609
+ out = sum_buf )
610
+
611
+ if cache_intermediates :
612
+ intermediates = dict (n_opers_transformed = n_opers_transformed ,
613
+ noise_operators_step = sum_cache )
614
+ return noise_operators , intermediates
590
615
591
616
return noise_operators
592
617
@@ -715,7 +740,7 @@ def calculate_control_matrix_from_scratch(
715
740
cache_intermediates: bool, optional
716
741
Keep and return intermediate terms
717
742
:math:`\mathcal{G}^{(g)}(\omega)` of the sum so that
718
- :math:`\mathcal{R }(\omega)=\sum_g\mathcal{G}^{(g)}(\omega)`.
743
+ :math:`\mathcal{B }(\omega)=\sum_g\mathcal{G}^{(g)}(\omega)`.
719
744
Otherwise the sum is performed in-place.
720
745
out: ndarray, optional
721
746
A location into which the result is stored. See
@@ -771,51 +796,58 @@ def calculate_control_matrix_from_scratch(
771
796
# Precompute noise opers transformed to eigenbasis of each pulse segment
772
797
# and Q^\dagger @ V
773
798
eigvecs_propagated = _propagate_eigenvectors (propagators [:- 1 ], eigvecs )
774
- n_opers_transformed = _transform_noise_operators ( n_coeffs , n_opers , eigvecs )
799
+ n_opers_transformed = _transform_hamiltonian ( eigvecs , n_opers , n_coeffs )
775
800
776
801
# Allocate result and buffers for intermediate arrays
777
- exp_buf , int_buf = np .empty ((2 , len (omega ), d , d ), dtype = complex )
778
-
802
+ exp_buf = np .empty ((len (omega ), d , d ), dtype = complex )
779
803
if out is None :
780
- control_matrix = np .zeros ((len (n_opers ), len (basis ), len (omega )), dtype = complex )
781
- else :
782
- control_matrix = out
804
+ out = np .zeros ((len (n_opers ), len (basis ), len (omega )), dtype = complex )
783
805
784
806
if cache_intermediates :
785
807
basis_transformed_cache = np .empty ((len (dt ), * basis .shape ), dtype = complex )
808
+ phase_factors_cache = np .empty ((len (dt ), len (omega )), dtype = complex )
809
+ int_cache = np .empty ((len (dt ), len (omega ), d , d ), dtype = complex )
786
810
sum_cache = np .empty ((len (dt ), len (n_opers ), len (basis ), len (omega )), dtype = complex )
787
811
else :
788
812
basis_transformed = np .empty (basis .shape , dtype = complex )
813
+ phase_factors = np .empty (len (omega ), dtype = complex )
814
+ int_buf = np .empty ((len (omega ), d , d ), dtype = complex )
789
815
sum_buf = np .empty ((len (n_opers ), len (basis ), len (omega )), dtype = complex )
790
816
791
817
# Optimize the contraction path dynamically since it differs for different
792
818
# values of d
793
819
expr = oe .contract_expression ('o,jmn,omn,knm->jko' ,
794
820
omega .shape , n_opers_transformed [:, 0 ].shape ,
795
- int_buf .shape , basis .shape , optimize = True )
821
+ exp_buf .shape , basis .shape , optimize = True )
796
822
for g in util .progressbar_range (len (dt ), show_progressbar = show_progressbar ,
797
823
desc = 'Calculating control matrix' ):
798
824
799
825
if cache_intermediates :
800
826
# Assign references to the locations in the cache for the quantities
801
827
# that should be stored
802
828
basis_transformed = basis_transformed_cache [g ]
829
+ phase_factors = phase_factors_cache [g ]
830
+ int_buf = int_cache [g ]
803
831
sum_buf = sum_cache [g ]
804
832
805
- basis_transformed = _transform_basis (basis , eigvecs_propagated [g ], out = basis_transformed )
833
+ basis_transformed = _transform_by_unitary (eigvecs_propagated [g ], basis ,
834
+ out = basis_transformed )
835
+ phase_factors = util .cexp (omega * t [g ], out = phase_factors )
806
836
int_buf = _first_order_integral (omega , eigvals [g ], dt [g ], exp_buf , int_buf )
807
- sum_buf = expr (util . cexp ( omega * t [ g ]) , n_opers_transformed [:, g ], int_buf ,
837
+ sum_buf = expr (phase_factors , n_opers_transformed [:, g ], int_buf ,
808
838
basis_transformed , out = sum_buf )
809
839
810
- control_matrix += sum_buf
840
+ out += sum_buf
811
841
812
842
if cache_intermediates :
813
843
intermediates = dict (n_opers_transformed = n_opers_transformed ,
814
844
basis_transformed = basis_transformed_cache ,
845
+ phase_factors = phase_factors_cache ,
846
+ first_order_integral = int_cache ,
815
847
control_matrix_step = sum_cache )
816
- return control_matrix , intermediates
848
+ return out , intermediates
817
849
818
- return control_matrix
850
+ return out
819
851
820
852
821
853
def calculate_control_matrix_periodic (phases : ndarray , control_matrix : ndarray ,
@@ -1180,7 +1212,7 @@ def calculate_decay_amplitudes(
1180
1212
"""
1181
1213
# TODO: Replace infidelity() by this?
1182
1214
# Noise operator indices
1183
- idx = util .get_indices_from_identifiers (pulse , n_oper_identifiers , 'noise' )
1215
+ idx = util .get_indices_from_identifiers (pulse . n_oper_identifiers , n_oper_identifiers )
1184
1216
if which == 'total' :
1185
1217
# Faster to use filter function instead of control matrix
1186
1218
if pulse .is_cached ('filter_function_gen' ):
@@ -1297,7 +1329,7 @@ def calculate_frequency_shifts(
1297
1329
pulse_sequence.concatenate: Concatenate ``PulseSequence`` objects.
1298
1330
calculate_pulse_correlation_filter_function
1299
1331
"""
1300
- idx = util .get_indices_from_identifiers (pulse , n_oper_identifiers , 'noise' )
1332
+ idx = util .get_indices_from_identifiers (pulse . n_oper_identifiers , n_oper_identifiers )
1301
1333
filter_function_2 = pulse .get_filter_function (omega , order = 2 ,
1302
1334
show_progressbar = show_progressbar )
1303
1335
integrand = _get_integrand (spectrum , omega , idx , which_pulse = 'total' , which_FF = 'generalized' ,
@@ -1478,7 +1510,7 @@ def calculate_second_order_filter_function(
1478
1510
t = np .concatenate (([0 ], np .asarray (dt ).cumsum ()))
1479
1511
# Cheap to precompute as these don't use a lot of memory
1480
1512
eigvecs_propagated = _propagate_eigenvectors (propagators [:- 1 ], eigvecs )
1481
- n_opers_transformed = _transform_noise_operators ( n_coeffs , n_opers , eigvecs )
1513
+ n_opers_transformed = _transform_hamiltonian ( eigvecs , n_opers , n_coeffs )
1482
1514
# These are populated anew during every iteration, so there is no need
1483
1515
# to keep every time step
1484
1516
basis_transformed = np .empty (basis .shape , dtype = complex )
@@ -1494,8 +1526,8 @@ def calculate_second_order_filter_function(
1494
1526
for g in util .progressbar_range (len (dt ), show_progressbar = show_progressbar ,
1495
1527
desc = 'Calculating second order FF' ):
1496
1528
if not intermediates :
1497
- basis_transformed = _transform_basis ( basis , eigvecs_propagated [g ],
1498
- out = basis_transformed )
1529
+ basis_transformed = _transform_by_unitary ( eigvecs_propagated [g ], basis ,
1530
+ out = basis_transformed )
1499
1531
# Need to compute G^(g) since no cache given. First initialize
1500
1532
# buffer to zero. There is a probably lots of overhead computing
1501
1533
# this individually for every time step.
@@ -1930,7 +1962,7 @@ def infidelity(pulse: 'PulseSequence', spectrum: Union[Coefficients, Callable],
1930
1962
plotting.plot_infidelity_convergence: Convenience function to plot results.
1931
1963
"""
1932
1964
# Noise operator indices
1933
- idx = util .get_indices_from_identifiers (pulse , n_oper_identifiers , 'noise' )
1965
+ idx = util .get_indices_from_identifiers (pulse . n_oper_identifiers , n_oper_identifiers )
1934
1966
1935
1967
if test_convergence :
1936
1968
if not callable (spectrum ):
0 commit comments