forked from google/TensorNetwork
/
charge.py
811 lines (710 loc) · 28 KB
/
charge.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
# Copyright 2019 The TensorNetwork Authors
#
# 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
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import numpy as np
# pylint: disable=line-too-long
from typing import List, Optional, Type, Any, Union, Callable
#TODO (mganahl): switch from column to row order for unique labels
#TODO (mganahl): implement more efficient unique function
#TODO (mganahl): clean up implementation of identity charges
#TODO (mganahl): for rank-3 tensors with small bond dimensions, finding
# blocks brute force is much faster. Implement this.
class BaseCharge:
"""
Base class for charges of BlockSparseTensor. All user defined charges
should be derived from this class.
Attributes:
* unique_charges: np.ndarray of shape `(m,n)` with `m`
the number of charge types, and `n` the number of unique charges.
* charge_labels: np.ndarray of dtype np.int16. Used for identifying
charges with integer labels. `unique_charges[:, charge_labels]
is the np.ndarray of actual charges.
* charge_types: A list of `type` objects. Stored the different charge types,
on for each row in `unique_charges`.
"""
class Iterator:
def __init__(self, unique: np.ndarray, labels: np.ndarray):
self.n = 0
self.unique = unique
self.labels = labels
def __next__(self):
if self.n < len(self.labels):
out = self.unique[:, self.labels[self.n]]
self.n += 1
return out
raise StopIteration
def __init__(self,
charges: Union[List, np.ndarray],
charge_labels: Optional[np.ndarray] = None,
charge_types: Optional[List[Type["BaseCharge"]]] = None,
charge_dtype: Optional[Type[np.number]] = np.int16) -> None:
charges = np.asarray(charges)
if charges.ndim == 1:
charges = charges[None, :]
if (charge_types is not None) and (len(charge_types) != charges.shape[0]):
raise ValueError(
"`len(charge_types) = {}` does not match `charges.shape[0]={}`"
.format(len(charge_types), charges.shape[0]))
if charges.shape[0] <= 3:
label_dtype = np.int16
else:
label_dtype = np.int32
if charge_types is None:
charge_types = [type(self)] * charges.shape[0]
self.charge_types = charge_types
if charge_labels is None:
if charges.shape[1] > 0:
self.unique_charges, self.charge_labels = np.unique(
charges.astype(charge_dtype), return_inverse=True, axis=1)
self.charge_labels = self.charge_labels.astype(label_dtype)
else:
self.unique_charges = np.empty((charges.shape[0], 0),
dtype=charge_dtype)
self.charge_labels = np.empty(0, dtype=label_dtype)
else:
self.charge_labels = np.asarray(charge_labels, dtype=label_dtype)
self.unique_charges = charges.astype(charge_dtype)
self.charge_labels = charge_labels.astype(label_dtype)
@staticmethod
def fuse(charge1, charge2):
raise NotImplementedError("`fuse` has to be implemented in derived classes")
@staticmethod
def dual_charges(charges):
raise NotImplementedError(
"`dual_charges` has to be implemented in derived classes")
@staticmethod
def identity_charge():
raise NotImplementedError(
"`identity_charge` has to be implemented in derived classes")
@classmethod
def random(cls, dimension: int, minval: int, maxval: int):
raise NotImplementedError(
"`random` has to be implemented in derived classes")
@property
def dim(self):
return len(self.charge_labels)
@property
def num_symmetries(self) -> int:
"""
Return the number of different charges in `ChargeCollection`.
"""
return self.unique_charges.shape[0]
@property
def num_unique(self) -> int:
"""
Return the number of different charges in `ChargeCollection`.
"""
return self.unique_charges.shape[1]
def copy(self):
"""
Return a copy of `BaseCharge`.
"""
obj = self.__new__(type(self))
obj.__init__(
charges=self.unique_charges.copy(),
charge_labels=self.charge_labels.copy(),
charge_types=self.charge_types,
charge_dtype=self.dtype)
return obj
@property
def charges(self):
"""
Return the actual charges of `BaseCharge` as np.ndarray.
"""
return self.unique_charges[:, self.charge_labels]
@property
def dtype(self):
return self.unique_charges.dtype
@property
def label_dtype(self):
return self.charge_labels.dtype
@property
def degeneracies(self):
exp1 = self.charge_labels[:, None]
exp2 = np.arange(
self.unique_charges.shape[1], dtype=self.label_dtype)[None, :]
return np.sum(exp1 == exp2, axis=0)
def __repr__(self):
return 'BaseCharge object:' + '\n charge types: ' + self.names + '\n charges:' + str(
self.charges).replace('\n', '\n\t ') + '\n'
def __iter__(self):
return self.Iterator(self.unique_charges, self.charge_labels)
def __len__(self):
return len(self.charge_labels)
def __eq__(self, target_charges: Union[np.ndarray,
"BaseCharge"]) -> np.ndarray:
#FIXME (mganahl): calling np.unique can cause significant overhead in some cases
#fix code in block_tensor.py to work on np.ndarray instead
if isinstance(target_charges, type(self)):
if len(target_charges) == 0:
raise ValueError('input to __eq__ cannot be an empty charge')
targets = np.unique(
target_charges.unique_charges[:, target_charges.charge_labels],
axis=1)
else:
if target_charges.ndim == 1:
target_charges = target_charges[None, :]
if target_charges.shape[1] == 0:
raise ValueError('input to __eq__ cannot be an empty np.ndarray')
if target_charges.shape[0] != self.num_symmetries:
raise ValueError(
'shape of `target_charges = {}` is incompatible with `self.num_symmetries = {}'
.format(target_charges.shape, self.num_symmetries))
targets = np.unique(target_charges, axis=1)
#pylint: disable=no-member
inds = np.nonzero(
np.logical_and.reduce(
self.unique_charges[:, :, None] == targets[:, None, :], axis=0))[0]
return self.charge_labels[:, None] == inds[None, :]
@property
def identity_charges(self) -> "BaseCharge":
"""
Returns the identity charge.
Returns:
BaseCharge: The identity charge.
"""
unique_charges = np.asarray(
[ct.identity_charge() for ct in self.charge_types],
dtype=self.dtype)[:, None]
charge_labels = np.zeros(1, dtype=self.label_dtype)
obj = self.__new__(type(self))
obj.__init__(unique_charges, charge_labels, self.charge_types)
return obj
def __add__(self, other: "BaseCharge") -> "BaseCharge":
"""
Fuse `self` with `other`.
Args:
other: A `BaseCharge` object.
Returns:
BaseCharge: The result of fusing `self` with `other`.
"""
# fuse the unique charges from each index, then compute new unique charges
comb_charges = fuse_ndarray_charges(self.unique_charges,
other.unique_charges, self.charge_types)
#pylint: disable=unsubscriptable-object
if (comb_charges.shape[1] == 0) or (len(self.charge_labels) == 0) or (len(
other.charge_labels) == 0):
obj = self.__new__(type(self))
obj.__init__(
np.empty((self.num_symmetries, 0), dtype=self.dtype),
np.empty(0, dtype=self.label_dtype), self.charge_types)
return obj
unique_charges, charge_labels = np.unique(
comb_charges, return_inverse=True, axis=1)
charge_labels = charge_labels.reshape(self.unique_charges.shape[1],
other.unique_charges.shape[1]).astype(
self.label_dtype)
# find new labels using broadcasting
left_labels = self.charge_labels[:, None] + np.zeros([1, len(other)],
dtype=self.label_dtype)
right_labels = other.charge_labels[None, :] + np.zeros(
[len(self), 1], dtype=self.label_dtype)
charge_labels = charge_labels[np.ravel(left_labels), np.ravel(right_labels)]
obj = self.__new__(type(self))
obj.__init__(unique_charges, charge_labels, self.charge_types)
return obj
def dual(self, take_dual: Optional[bool] = False) -> "BaseCharge":
"""
Return the charges of `BaseCharge`, possibly conjugated.
Args:
take_dual: If `True` return the dual charges. If `False` return
regular charges.
Returns:
BaseCharge
"""
if take_dual:
unique_dual_charges = np.stack([
self.charge_types[n].dual_charges(self.unique_charges[n, :])
for n in range(len(self.charge_types))
],
axis=0)
obj = self.__new__(type(self))
obj.__init__(unique_dual_charges, self.charge_labels, self.charge_types)
return obj
return self
def __matmul__(self, other):
#some checks
if len(self) != len(other):
raise ValueError(
'__matmul__ requires charges to have the same number of elements')
charges = np.concatenate([self.charges, other.charges], axis=0)
charge_types = self.charge_types + other.charge_types
return BaseCharge(
charges=charges, charge_labels=None, charge_types=charge_types)
def __mul__(self, number: bool) -> "BaseCharge":
if not isinstance(number, (bool, np.bool_)):
raise ValueError(
"can only multiply by `True` or `False`, found {}".format(number))
return self.dual(number)
def intersect(self, other, assume_unique=False, return_indices=False) -> Any:
"""
Compute the intersection of `self` with `other`. See also np.intersect1d.
Args:
other: A BaseCharge object.
assume_unique: If `True` assume that elements are unique.
return_indices: If `True`, return index-labels.
Returns:
If `return_indices=True`:
BaseCharge
np.ndarray: The indices of the first occurrences of the common values in `self`.
np.ndarray: The indices of the first occurrences of the common values in `other`.
If `return_indices=False`:
BaseCharge
"""
if isinstance(other, type(self)):
out = intersect(
self.charges,
other.charges,
assume_unique=assume_unique,
axis=1,
return_indices=return_indices)
else:
if other.ndim == 1:
other = other[None, :]
out = intersect(
self.charges,
np.asarray(other),
axis=1,
assume_unique=assume_unique,
return_indices=return_indices)
obj = self.__new__(type(self))
if return_indices:
obj.__init__(
charges=out[0],
charge_labels=np.arange(out[0].shape[1], dtype=np.int16),
charge_types=self.charge_types,
)
return obj, out[1], out[2]
obj.__init__(
charges=out,
charge_labels=np.arange(out.shape[1], dtype=np.int16),
charge_types=self.charge_types,
)
return obj
def sort_unique_charges(self) -> "BaseCharge":
"""
Sort the `unique_charges` of BaseCharge` according to standard order
used by numpy.
Returns:
BaseCharge
"""
unique_charges, inverse = np.unique(
self.unique_charges, return_inverse=True, axis=1)
charge_labels = inverse[self.charge_labels]
obj = self.__new__(type(self))
obj.__init__(
charges=unique_charges,
charge_labels=charge_labels,
charge_types=self.charge_types)
return obj
def unique(self,
return_index: bool = False,
return_inverse: bool = False,
return_counts: bool = False,
sort: bool = True) -> Any:
"""
Compute the unique charges in `BaseCharge`.
See np.unique for a more detailed explanation. This function
does the same but instead of a np.ndarray, it returns the unique
elements in a `BaseCharge` object.
Args:
return_index: If `True`, also return the indices of `self.charges` (along the specified axis,
if provided, or in the flattened array) that result in the unique array.
return_inverse: If `True`, also return the indices of the unique array (for the specified
axis, if provided) that can be used to reconstruct `self.charges`.
return_counts: If `True`, also return the number of times each unique item appears
in `self.charges`.
sort: If `True`, the returned `BaseCharge` object has sorted `unique_charges`.
If `False`, `unique_`charges` are in general not sorted.
Returns:
BaseCharge: The sorted unique values.
np.ndarray: The indices of the first occurrences of the unique values in the
original array. Only provided if `return_index` is True.
np.ndarray: The indices to reconstruct the original array from the
unique array. Only provided if `return_inverse` is True.
np.ndarray: The number of times each of the unique values comes up in the
original array. Only provided if `return_counts` is True.
"""
if sort:
#make sure that unique_charges are sorted
tmp_charge = self.sort_unique_charges()
else:
tmp_charge = self
obj = tmp_charge.__new__(type(tmp_charge))
tmp = np.unique(
tmp_charge.charge_labels,
return_index=return_index,
return_inverse=return_inverse,
return_counts=return_counts)
if return_index or return_inverse or return_counts:
if tmp[0].ndim == 0: #only a single entry
index = np.asarray([tmp[0]])
unique_charges = tmp_charge.unique_charges[:, index]
else:
unique_charges = tmp_charge.unique_charges[:, tmp[0]]
else:
if tmp.ndim == 0:
tmp = np.asarray([tmp])
unique_charges = tmp_charge.unique_charges[:, tmp]
obj.__init__(
charges=unique_charges,
charge_labels=np.arange(
unique_charges.shape[1], dtype=self.label_dtype),
charge_types=self.charge_types)
out = [obj]
if return_index or return_inverse or return_counts:
for n in range(1, len(tmp)):
out.append(tmp[n])
#for a single return value we don't want to return a list or tuple
if len(out) == 1:
return out[0]
return tuple(out)
def reduce(self,
target_charges: np.ndarray,
return_locations: bool = False,
strides: Optional[int] = 1) -> Any:
"""
Reduce the dimension of a
charge to keep only the charge values that intersect target_charges
Args:
target_charges: array of unique charges to keep.
return_locations: If `True`, also return the locations of
target values within `BaseCharge`.
strides: An optional stride value.
Returns:
BaseCharge: charge of reduced dimension.
np.ndarray: If `return_locations = True`; the index locations
of target values.
"""
if isinstance(target_charges, (np.integer, int)):
target_charges = np.asarray([target_charges], dtype=self.dtype)
if target_charges.ndim == 1:
target_charges = target_charges[None, :]
target_charges = np.asarray(target_charges, dtype=self.dtype)
# find intersection of index charges and target charges
reduced_charges, label_to_unique, _ = intersect(
self.unique_charges, target_charges, axis=1, return_indices=True)
num_unique = len(label_to_unique)
# construct the map to the reduced charges
map_to_reduced = np.full(self.dim, fill_value=-1, dtype=self.label_dtype)
map_to_reduced[label_to_unique] = np.arange(
num_unique, dtype=self.label_dtype)
# construct the map to the reduced charges
reduced_ind_labels = map_to_reduced[self.charge_labels]
reduced_locs = reduced_ind_labels >= 0
new_ind_labels = reduced_ind_labels[reduced_locs].astype(self.label_dtype)
obj = self.__new__(type(self))
obj.__init__(reduced_charges, new_ind_labels, self.charge_types)
if return_locations:
return obj, strides * np.flatnonzero(reduced_locs).astype(np.uint32)
return obj
def __getitem__(self, n: Union[List[int], np.ndarray, int]) -> "BaseCharge":
"""
Return the charge-element at position `n`, wrapped into a `BaseCharge`
object.
Args:
n: An integer or `np.ndarray`.
Returns:
BaseCharge: The charges at `n`.
"""
if isinstance(n, (np.integer, int)):
n = np.asarray([n])
n = np.asarray(n)
obj = self.__new__(type(self))
labels = self.charge_labels[n]
unique_labels, new_labels = np.unique(labels, return_inverse=True)
if unique_labels.ndim == 0:
unique_labels = np.asarray(unique_labels)
unique_charges = self.unique_charges[:, unique_labels]
obj.__init__(unique_charges, new_labels, self.charge_types)
return obj
def isin(self, target_charges: Union[np.ndarray, "BaseCharge"]) -> np.ndarray:
"""
See also np.isin.
Returns an np.ndarray of `dtype=bool`, with `True` at all linear positions
where `self` is in `target_charges`, and `False` everywhere else.
Args:
target_charges: A `BaseCharge` object.
Returns:
np.ndarray: An array of boolean values.
"""
if isinstance(target_charges, type(self)):
if not np.all([
a == b for a, b in zip(self.charge_types, target_charges.charge_types)
]):
raise TypeError(
"isin only callable for equal charge types, found {} and {}".format(
self.charge_types, target_charges.charge_types))
targets = target_charges.unique_charges
else:
if target_charges.ndim == 1:
if target_charges.shape[0] == 0:
raise ValueError("input to `isin` cannot be an empty np.ndarray")
targets = np.unique(target_charges, axis=0)[None, :]
elif target_charges.ndim == 2:
if target_charges.shape[1] == 0:
raise ValueError("input to `isin` cannot be an empty np.ndarray")
targets = np.unique(target_charges, axis=1)
else:
raise ValueError("targets.ndim has to be 1 or 2, found {}".format(
target_charges.ndim))
if targets.shape[0] != self.num_symmetries:
raise ValueError(
"target_charges.shape[0]={} is different from self.num_symmetries = {}"
.format(targets.shape[0], self.num_symmetries))
tmp = self.unique_charges[:, :, None] == targets[:, None, :]
#pylint: disable=no-member
inds = np.nonzero(
np.logical_or.reduce(np.logical_and.reduce(tmp, axis=0), axis=1))[0]
return np.isin(self.charge_labels, inds)
@property
def names(self):
return repr([ct.__new__(ct).__class__.__name__ for ct in self.charge_types])
class U1Charge(BaseCharge):
"""Charge Class for the U1 symmetry group."""
@staticmethod
def fuse(charge1, charge2) -> np.ndarray:
return np.add.outer(charge1, charge2).ravel()
@staticmethod
def dual_charges(charges) -> np.ndarray:
return charges * charges.dtype.type(-1)
@staticmethod
def identity_charge() -> np.ndarray:
return np.int16(0)
@classmethod
def random(cls, dimension: int, minval: int, maxval: int) -> BaseCharge:
charges = np.random.randint(minval, maxval + 1, dimension, dtype=np.int16)
return cls(charges=charges)
class Z2Charge(BaseCharge):
"""Charge Class for the Z2 symmetry group."""
def __init__(self,
charges: Union[List, np.ndarray],
charge_labels: Optional[np.ndarray] = None,
charge_types: Optional[List[Type["BaseCharge"]]] = None,
charge_dtype: Optional[Type[np.number]] = np.int16) -> None:
#do some checks before calling the base class constructor
unique = np.unique(np.ravel(charges))
if not np.all(np.isin(unique, [0, 1])):
raise ValueError("Z2 charges can only be 0 or 1, found {}".format(unique))
super().__init__(
charges,
charge_labels,
charge_types=[type(self)],
charge_dtype=charge_dtype)
@staticmethod
def fuse(charge1, charge2) -> np.ndarray:
#pylint: disable=no-member
return np.bitwise_xor.outer(charge1, charge2).ravel()
@staticmethod
def dual_charges(charges) -> np.ndarray:
return charges
@staticmethod
def identity_charge() -> np.ndarray:
return np.int16(0)
@classmethod
def random(cls,
dimension: int,
minval: int = 0,
maxval: int = 1) -> BaseCharge:
if minval != 0 or maxval != 1:
raise ValueError("Z2 charges can only take values 0 or 1")
charges = np.random.randint(0, 2, dimension, dtype=np.int16)
return cls(charges=charges)
def ZNCharge(n: int) -> Callable:
"""Contstructor for charge classes of the ZN symmetry groups.
Args:
n: The module of the symmetry group.
Returns:
A charge class of your given ZN symmetry group.
"""
if n < 2:
raise ValueError(f"n must be >= 2, found {n}")
class ModularCharge(BaseCharge):
def __init__(self,
charges: Union[List, np.ndarray],
charge_labels: Optional[np.ndarray] = None,
charge_types: Optional[List[Type["BaseCharge"]]] = None,
charge_dtype: Optional[Type[np.number]] = np.int16) -> None:
unique = np.unique(np.ravel(charges))
if not np.all(np.isin(unique, list(range(n)))):
raise ValueError(f"Z{n} charges must be in range({n}), found: {unique}")
super().__init__(
charges,
charge_labels,
charge_types=[type(self)],
charge_dtype=charge_dtype)
@staticmethod
def fuse(charge1, charge2) -> np.ndarray:
#pylint: disable=no-member
return np.outer(charge1, charge2).ravel() % n
@staticmethod
def dual_charges(charges) -> np.ndarray:
return (n - charges) % n
@staticmethod
def identity_charge() -> np.ndarray:
return np.int16(0)
@classmethod
def random(cls,
dimension: int,
minval: int = 0,
maxval: int = n) -> BaseCharge:
if maxval >= n:
raise ValueError(f"maxval must be less than n={n}, got {maxval}")
if minval < 0:
raise ValueError(f"minval must be greater than 0, found {minval}")
# No need for the mod due to the checks above.
charges = np.random.randint(minval, maxval + 1, dimension, dtype=np.int16)
return cls(charges=charges)
return ModularCharge
def fuse_ndarray_charges(charges_A: np.ndarray, charges_B: np.ndarray,
charge_types: List[Type[BaseCharge]]) -> np.ndarray:
"""
Fuse the quantum numbers of two indices under their kronecker addition.
Args:
charges_A (np.ndarray): n-by-D1 dimensional array integers encoding charges,
with n the number of symmetries and D1 the index dimension.
charges__B (np.ndarray): n-by-D2 dimensional array of charges.
charge_types: A list of types of the charges.
Returns:
np.ndarray: n-by-(D1 * D2) dimensional array of the fused charges.
"""
comb_charges = [0] * len(charge_types)
for n, ct in enumerate(charge_types):
comb_charges[n] = ct.fuse(charges_A[n, :], charges_B[n, :])
return np.concatenate(
comb_charges, axis=0).reshape(
len(charge_types), charges_A.shape[1] * charges_B.shape[1])
def intersect(A: np.ndarray,
B: np.ndarray,
axis=0,
assume_unique=False,
return_indices=False) -> Any:
"""
Extends numpy's intersect1d to find the row or column-wise intersection of
two 2d arrays. Takes identical input to numpy intersect1d.
Args:
A, B (np.ndarray): arrays of matching widths and datatypes
Returns:
ndarray: sorted 1D array of common rows/cols between the input arrays
ndarray: the indices of the first occurrences of the common values in A.
Only provided if return_indices is True.
ndarray: the indices of the first occurrences of the common values in B.
Only provided if return_indices is True.
"""
#see https://stackoverflow.com/questions/8317022/get-intersecting-rows-across-two-2d-numpy-arrays
#pylint: disable=no-else-return
if A.ndim != B.ndim:
raise ValueError("array ndims must match to intersect")
if A.ndim == 1:
return np.intersect1d(
A, B, assume_unique=assume_unique, return_indices=return_indices)
elif A.ndim == 2:
if axis == 0:
ncols = A.shape[1]
if A.shape[1] != B.shape[1]:
raise ValueError("array widths must match to intersect")
dtype = {
'names': ['f{}'.format(i) for i in range(ncols)],
'formats': ncols * [A.dtype]
}
if return_indices:
C, A_locs, B_locs = np.intersect1d(
A.view(dtype),
B.view(dtype),
assume_unique=assume_unique,
return_indices=return_indices)
return C.view(A.dtype).reshape(-1, ncols), A_locs, B_locs
C = np.intersect1d(
A.view(dtype), B.view(dtype), assume_unique=assume_unique)
return C.view(A.dtype).reshape(-1, ncols)
elif axis == 1:
out = intersect(
A.T.copy(),
B.T.copy(),
axis=0,
assume_unique=assume_unique,
return_indices=return_indices)
if return_indices:
return out[0].T, out[1], out[2]
return out.T
raise NotImplementedError(
"intersection can only be performed on first or second axis")
raise NotImplementedError("intersect is only implemented for 1d or 2d arrays")
def fuse_charges(charges: List[BaseCharge], flows: List[bool]) -> BaseCharge:
"""
Fuse all `charges` into a new charge.
Charges are fused from "right to left",
in accordance with row-major order.
Args:
charges: A list of charges to be fused.
flows: A list of flows, one for each element in `charges`.
Returns:
BaseCharge: The result of fusing `charges`.
"""
if len(charges) != len(flows):
raise ValueError(
"`charges` and `flows` are of unequal lengths {} != {}".format(
len(charges), len(flows)))
fused_charges = charges[0] * flows[0]
for n in range(1, len(charges)):
fused_charges = fused_charges + charges[n] * flows[n]
return fused_charges
def fuse_degeneracies(degen1: Union[List, np.ndarray],
degen2: Union[List, np.ndarray]) -> np.ndarray:
"""
Fuse degeneracies `degen1` and `degen2` of two leg-charges
by simple kronecker product. `degen1` and `degen2` typically belong to two
consecutive legs of `BlockSparseTensor`.
Given `degen1 = [1, 2, 3]` and `degen2 = [10, 100]`, this returns
`[10, 100, 20, 200, 30, 300]`.
When using row-major ordering of indices in `BlockSparseTensor`,
the position of `degen1` should be "to the left" of the position of `degen2`.
Args:
degen1: Iterable of integers
degen2: Iterable of integers
Returns:
np.ndarray: The result of fusing `dege1` with `degen2`.
"""
return np.reshape(
np.array(degen1)[:, None] * np.array(degen2)[None, :],
len(degen1) * len(degen2))
def fuse_ndarrays(arrays: List[Union[List, np.ndarray]]) -> np.ndarray:
"""
Fuse all `arrays` by simple kronecker addition.
Arrays are fused from "right to left",
Args:
arrays: A list of arrays to be fused.
Returns:
np.ndarray: The result of fusing `arrays`.
"""
if len(arrays) == 1:
return np.array(arrays[0])
fused_arrays = np.asarray(arrays[0])
for n in range(1, len(arrays)):
fused_arrays = np.ravel(np.add.outer(fused_arrays, arrays[n]))
return fused_arrays
def charge_equal(c1: BaseCharge, c2: BaseCharge) -> bool:
"""
Compare two BaseCharges `c1` and `c2`.
Return `True` if they are equal, else `False`.
"""
if c1.dim != c2.dim:
return False
if not np.all(c1.unique_charges == c2.unique_charges):
return False
if not np.all(c1.charge_labels == c2.charge_labels):
return False
return True