/
model.py
2259 lines (1864 loc) · 100 KB
/
model.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 Model class and supporting functionality.
"""
#***************************************************************************************************
# 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 bisect as _bisect
import copy as _copy
import itertools as _itertools
import uuid as _uuid
import warnings as _warnings
import collections as _collections
import numpy as _np
from pygsti.baseobjs import statespace as _statespace
from pygsti.baseobjs.nicelyserializable import NicelySerializable as _NicelySerializable
from pygsti.models.layerrules import LayerRules as _LayerRules
from pygsti.models.modelparaminterposer import LinearInterposer as _LinearInterposer
from pygsti.evotypes import Evotype as _Evotype
from pygsti.forwardsims import forwardsim as _fwdsim
from pygsti.modelmembers import modelmember as _gm
from pygsti.modelmembers import operations as _op
from pygsti.baseobjs.basis import Basis as _Basis, TensorProdBasis as _TensorProdBasis
from pygsti.baseobjs.label import Label as _Label
from pygsti.baseobjs.resourceallocation import ResourceAllocation as _ResourceAllocation
from pygsti.tools import slicetools as _slct
from pygsti.tools import matrixtools as _mt
MEMLIMIT_FOR_NONGAUGE_PARAMS = None
class Model(_NicelySerializable):
"""
A predictive model for a Quantum Information Processor (QIP).
The main function of a `Model` object is to compute the outcome
probabilities of :class:`Circuit` objects based on the action of the
model's ideal operations plus (potentially) noise which makes the
outcome probabilities deviate from the perfect ones.
Parameters
----------
state_space : StateSpace
The state space of this model.
"""
def __init__(self, state_space):
super().__init__()
self._state_space = _statespace.StateSpace.cast(state_space)
self._num_modeltest_params = None
self._hyperparams = {}
self._paramvec = _np.zeros(0, 'd')
self._paramlbls = _np.empty(0, dtype=object)
self._param_bounds = None
self.uuid = _uuid.uuid4() # a Model's uuid is like a persistent id(), useful for hashing
def _to_nice_serialization(self):
state = super()._to_nice_serialization()
state.update({'state_space': self.state_space.to_nice_serialization(),
'parameter_labels': list(self._paramlbls) if len(self._paramlbls) > 0 else None,
'parameter_bounds': (self._encodemx(self._param_bounds)
if (self._param_bounds is not None) else None)})
return state
@property
def state_space(self):
"""
State space labels
Returns
-------
StateSpaceLabels
"""
return self._state_space
@property
def hyperparams(self):
"""
Dictionary of hyperparameters associated with this model
Returns
-------
dict
"""
return self._hyperparams # Note: no need to set this param - just set/update values
@property
def num_params(self):
"""
The number of free parameters when vectorizing this model.
Returns
-------
int
the number of model parameters.
"""
return len(self._paramvec)
@property
def num_modeltest_params(self):
"""
The parameter count to use when testing this model against data.
Often times, this is the same as :meth:`num_params`, but there are times
when it can convenient or necessary to use a parameter count different than
the actual number of parameters in this model.
Returns
-------
int
the number of model parameters.
"""
if not hasattr(self, '_num_modeltest_params'): # for backward compatibility
self._num_modeltest_params = None
if self._num_modeltest_params is not None:
return self._num_modeltest_params
elif 'num_nongauge_params' in dir(self): # better than hasattr, which *runs* the @property method
if MEMLIMIT_FOR_NONGAUGE_PARAMS is not None:
if hasattr(self, 'num_elements'):
memForNumGaugeParams = self.num_elements * (self.num_params + self.state_space.dim**2) \
* _np.dtype('d').itemsize # see Model._buildup_dpg (this is mem for dPG)
else:
return self.num_params
if memForNumGaugeParams > MEMLIMIT_FOR_NONGAUGE_PARAMS:
_warnings.warn(("Model.num_modeltest_params did not compute number of *non-gauge* parameters - "
"using total (make MEMLIMIT_FOR_NONGAUGE_PARAMS larger if you really want "
"the count of nongauge params"))
return self.num_params
try:
return self.num_nongauge_params # len(x0)
except: # numpy can throw a LinAlgError or sparse cases can throw a NotImplementedError
_warnings.warn(("Model.num_modeltest_params could not obtain number of *non-gauge* parameters"
" - using total instead"))
return self.num_params
else:
return self.num_params
@num_modeltest_params.setter
def num_modeltest_params(self, count):
self._num_modeltest_params = count
@property
def parameter_bounds(self):
""" Upper and lower bounds on the values of each parameter, utilized by optimization routines """
return self._param_bounds
def set_parameter_bounds(self, index, lower_bound=-_np.inf, upper_bound=_np.inf):
"""
Set the bounds for a single model parameter.
These limit the values the parameter can have during an optimization of the model.
Parameters
----------
index : int
The index of the paramter whose bounds should be set.
lower_bound, upper_bound : float, optional
The lower and upper bounds for the parameter. Can be set to the special
`numpy.inf` (or `-numpy.inf`) values to effectively have no bound.
Returns
-------
None
"""
if lower_bound == -_np.inf and upper_bound == _np.inf:
return # do nothing
if self._param_bounds is None:
self._param_bounds = _default_param_bounds(self.num_params)
self._param_bounds[index, :] = (lower_bound, upper_bound)
@property
def parameter_labels(self):
"""
A list of labels, usually of the form `(op_label, string_description)` describing this model's parameters.
"""
return self._paramlbls
@property
def parameter_labels_pretty(self):
"""
The list of parameter labels but formatted in a nice way.
In particular, tuples where the first element is an op label are made into
a single string beginning with the string representation of the operation.
"""
ret = []
for lbl in self.parameter_labels:
if isinstance(lbl, (tuple, list)):
ret.append(": ".join([str(x) for x in lbl]))
else:
ret.append(lbl)
return ret
def set_parameter_label(self, index, label):
"""
Set the label of a single model parameter.
Parameters
----------
index : int
The index of the paramter whose label should be set.
label : object
An object that serves to label this parameter. Often a string.
Returns
-------
None
"""
self._paramlbls[index] = label
def to_vector(self):
"""
Returns the model vectorized according to the optional parameters.
Returns
-------
numpy array
The vectorized model parameters.
"""
return self._paramvec
def from_vector(self, v, close=False):
"""
Sets this Model's operations based on parameter values `v`.
Parameters
----------
v : numpy.ndarray
A vector of parameters, with length equal to `self.num_params`.
close : bool, optional
Set to `True` if `v` is close to the current parameter vector.
This can make some operations more efficient.
Returns
-------
None
"""
assert(len(v) == self.num_params)
self._paramvec = v.copy()
def probabilities(self, circuit, clip_to=None):
"""
Construct a dictionary containing the outcome probabilities of `circuit`.
Parameters
----------
circuit : Circuit or tuple of operation labels
The sequence of operation labels specifying the circuit.
clip_to : 2-tuple, optional
(min,max) to clip probabilities to if not None.
Returns
-------
probs : dictionary
A dictionary such that
probs[SL] = pr(SL,circuit,clip_to)
for each spam label (string) SL.
"""
raise NotImplementedError("Derived classes should implement this!")
def bulk_probabilities(self, circuits, clip_to=None, comm=None, mem_limit=None, smartc=None):
"""
Construct a dictionary containing the probabilities for an entire list of circuits.
Parameters
----------
circuits : (list of Circuits) or CircuitOutcomeProbabilityArrayLayout
When a list, each element specifies a circuit to compute outcome probabilities for.
A :class:`CircuitOutcomeProbabilityArrayLayout` specifies the circuits along with
an internal memory layout that reduces the time required by this function and can
restrict the computed probabilities to those corresponding to only certain outcomes.
clip_to : 2-tuple, optional
(min,max) to clip return value if not None.
comm : mpi4py.MPI.Comm, optional
When not None, an MPI communicator for distributing the computation
across multiple processors. Distribution is performed over
subtrees of evalTree (if it is split).
mem_limit : int, optional
A rough memory limit in bytes which is used to determine processor
allocation.
smartc : SmartCache, optional
A cache object to cache & use previously cached values inside this
function.
Returns
-------
probs : dictionary
A dictionary such that `probs[opstr]` is an ordered dictionary of
`(outcome, p)` tuples, where `outcome` is a tuple of labels
and `p` is the corresponding probability.
"""
raise NotImplementedError("Derived classes should implement this!")
def _init_copy(self, copy_into, memo):
"""
Copies any "tricky" member of this model into `copy_into`, before
deep copying everything else within a .copy() operation.
"""
copy_into.uuid = _uuid.uuid4() # new uuid for a copy (don't duplicate!)
def _post_copy(self, copy_into, memo):
"""
Called after all other copying is done, to perform "linking" between
the new model (`copy_into`) and its members.
"""
pass
def copy(self):
"""
Copy this model.
Returns
-------
Model
a (deep) copy of this model.
"""
#Avoid having to reconstruct everything via __init__;
# essentially deepcopy this object, but give the
# class opportunity to initialize tricky members instead
# of letting deepcopy do it.
newModel = type(self).__new__(self.__class__) # empty object
memo = {} # so that copying preserves linked object references
#first call _init_copy to initialize any tricky members
# (like those that contain references to self or other members)
self._init_copy(newModel, memo)
for attr, val in self.__dict__.items():
if not hasattr(newModel, attr):
assert(attr != "uuid"), "Should not be copying UUID!"
setattr(newModel, attr, _copy.deepcopy(val, memo))
self._post_copy(newModel, memo)
return newModel
def __str__(self):
pass
def __hash__(self):
if self.uuid is not None:
return hash(self.uuid)
else:
raise TypeError('Use digest hash')
def circuit_outcomes(self, circuit):
"""
Get all the possible outcome labels produced by simulating this circuit.
Parameters
----------
circuit : Circuit
Circuit to get outcomes of.
Returns
-------
tuple
"""
return () # default = no outcomes
def compute_num_outcomes(self, circuit):
"""
The number of outcomes of `circuit`, given by it's existing or implied POVM label.
Parameters
----------
circuit : Circuit
The circuit to simplify
Returns
-------
int
"""
return len(self.circuit_outcomes(circuit))
def complete_circuit(self, circuit):
"""
Adds any implied preparation or measurement layers to `circuit`
Parameters
----------
circuit : Circuit
Circuit to act on.
Returns
-------
Circuit
Possibly the same object as `circuit`, if no additions are needed.
"""
return circuit
class OpModel(Model):
"""
A Model that contains operators (i.e. "members"), having a container structure.
These operators are independently (sort of) parameterized and can be thought
to have dense representations (even if they're not actually stored that way).
This gives rise to the model having `basis` and `evotype` members.
Secondly, attached to an `OpModel` is the idea of "circuit simplification"
whereby the operators (preps, operations, povms, instruments) within
a circuit get simplified to things corresponding to a single outcome
probability, i.e. pseudo-circuits containing just preps, operations,
and POMV effects.
Thirdly, an `OpModel` is assumed to use a *layer-by-layer* evolution, and,
because of circuit simplification process, the calculaton of circuit
outcome probabilities has been pushed to a :class:`ForwardSimulator`
object which just deals with the forward simulation of simplified circuits.
Furthermore, instead of relying on a static set of operations a forward
simulator queries a :class:`LayerLizard` for layer operations, making it
possible to build up layer operations in an on-demand fashion from pieces
within the model.
Parameters
----------
state_space : StateSpace
The state space for this model.
basis : Basis
The basis used for the state space by dense operator representations.
evotype : Evotype or str, optional
The evolution type of this model, describing how states are
represented. The special value `"default"` is equivalent
to specifying the value of `pygsti.evotypes.Evotype.default_evotype`.
layer_rules : LayerRules
The "layer rules" used for constructing operators for circuit
layers. This functionality is essential to using this model to
simulate ciruits, and is typically supplied by derived classes.
simulator : ForwardSimulator or {"auto", "matrix", "map"}
The forward simulator (or typ) that this model should use. `"auto"`
tries to determine the best type automatically.
"""
#Whether to perform extra parameter-vector integrity checks
_pcheck = False
#Experimental: whether to call .from_vector on operation *cache* elements as part of model.from_vector call
_call_fromvector_on_cache = True
def __init__(self, state_space, basis, evotype, layer_rules, simulator="auto"):
"""
Creates a new OpModel. Rarely used except from derived classes `__init__` functions.
"""
self._evotype = _Evotype.cast(evotype)
self._set_state_space(state_space, basis)
#sets self._state_space, self._basis
super(OpModel, self).__init__(self.state_space) # do this as soon as possible
self._layer_rules = layer_rules if (layer_rules is not None) else _LayerRules()
self._opcaches = {} # dicts of non-primitive operations (organized by derived class)
self._need_to_rebuild = True # whether we call _rebuild_paramvec() in to_vector() or num_params()
self.dirty = False # indicates when objects and _paramvec may be out of sync
self.sim = simulator # property setter does nontrivial initialization (do this *last*)
self._param_interposer = None
self._reinit_opcaches()
self.fogi_store = None
def __setstate__(self, state_dict):
self.__dict__.update(state_dict)
self._sim.model = self # ensure the simulator's `model` is set to self (usually == None in serialization)
##########################################
## Get/Set methods
##########################################
@property
def sim(self):
""" Forward simulator for this model """
self._clean_paramvec() # clear opcache and rebuild paramvec when needed
if hasattr(self._sim, 'model'):
assert(self._sim.model is self), "Simulator out of sync with model!!"
return self._sim
@sim.setter
def sim(self, simulator):
try: # don't fail if state space doesn't have an integral # of qubits
nqubits = self.state_space.num_qubits
except:
nqubits = None
# TODO: This should probably also take evotype (e.g. 'chp' should probably use a CHPForwardSim, etc)
self._sim = _fwdsim.ForwardSimulator.cast(simulator, nqubits)
self._sim.model = self # ensure the simulator's `model` is set to this object
@property
def evotype(self):
"""
Evolution type
Returns
-------
str
"""
return self._evotype
@property
def basis(self):
"""
The basis used to represent dense (super)operators of this model
Returns
-------
Basis
"""
return self._basis
@basis.setter
def basis(self, basis):
"""
The basis used to represent dense (super)operators of this model
"""
if isinstance(basis, _Basis):
assert(basis.is_compatible_with_state_space(self.state_space)), "Basis is incompabtible with state space!"
self._basis = basis
else: # create a basis with the proper structure & dimension
self._basis = _Basis.cast(basis, self.state_space)
def _set_state_space(self, lbls, basis="pp"):
"""
Sets labels for the components of the Hilbert space upon which the gates of this Model act.
Parameters
----------
lbls : list or tuple or StateSpaceLabels object
A list of state-space labels (can be strings or integers), e.g.
`['Q0','Q1']` or a :class:`StateSpaceLabels` object.
basis : Basis or str
A :class:`Basis` object or a basis name (like `"pp"`), specifying
the basis used to interpret the operators in this Model. If a
`Basis` object, then its dimensions must match those of `lbls`.
Returns
-------
None
"""
if isinstance(lbls, _statespace.StateSpace):
self._state_space = lbls
else:
#Maybe change to a different default?
self._state_space = _statespace.ExplicitStateSpace(lbls)
self.basis = basis # invokes basis setter to set self._basis
#Operator dimension of this Model
#self._dim = self.state_space.dim
#e.g. 4 for 1Q (densitymx) or 2 for 1Q (statevec)
#TODO - deprecate this?
@property
def dim(self):
"""
The dimension of the model.
This equals d when the gate (or, more generally, circuit-layer) matrices
would have shape d x d and spam vectors would have shape d x 1 (if they
were computed).
Returns
-------
int
model dimension
"""
return self._state_space.dim
####################################################
## Parameter vector maintenance
####################################################
@property
def num_params(self):
"""
The number of free parameters when vectorizing this model.
Returns
-------
int
the number of model parameters.
"""
self._clean_paramvec()
return len(self._paramvec)
def _iter_parameterized_objs(self):
raise NotImplementedError("Derived Model classes should implement _iter_parameterized_objs")
#return # default is to have no parameterized objects
#TODO: Make this work with param interposers.
def _check_paramvec(self, debug=False):
if debug: print("---- Model._check_paramvec ----")
TOL = 1e-8
for lbl, obj in self._iter_parameterized_objs():
if debug: print(lbl, ":", obj.num_params, obj.gpindices)
w = obj.to_vector()
msg = "None" if (obj.parent is None) else id(obj.parent)
assert(obj.parent is self), "%s's parent is not set correctly (%s)!" % (lbl, msg)
if obj.gpindices is not None and len(w) > 0:
if _np.linalg.norm(self._paramvec[obj.gpindices] - w) > TOL:
if debug: print(lbl, ".to_vector() = ", w, " but Model's paramvec = ",
self._paramvec[obj.gpindices])
raise ValueError("%s is out of sync with paramvec!!!" % lbl)
if not self.dirty and obj.dirty:
raise ValueError("%s is dirty but Model.dirty=False!!" % lbl)
def _clean_paramvec(self):
""" Updates _paramvec corresponding to any "dirty" elements, which may
have been modified without out knowing, leaving _paramvec out of
sync with the element's internal data. It *may* be necessary
to resolve conflicts where multiple dirty elements want different
values for a single parameter. This method is used as a safety net
that tries to insure _paramvec & Model elements are consistent
before their use."""
#Note on dirty flag processing and the "dirty_value" flag of members:
# A model member's "dirty" flag is set to True when the member's
# value (local parameter vector) may be different from its parent
# model's parameter vector. Usually, when `from_vector` is called on
# a member, this should set the dirty flag (since it sets the local
# parameter vector). The exception is when this function is being
# called within the parent's `from_vector` method, in which case the
# flag should be reset to `False`, even if it was True before.
# Whether this operation should refrain from setting it's dirty
# flag as a result of this call. `False` is the safe option, as
# this call potentially changes this operation's parameters.
#print("Cleaning Paramvec (dirty=%s, rebuild=%s)" % (self.dirty, self._need_to_rebuild))
#import inspect, pprint
#pprint.pprint([(x.filename,x.lineno,x.function) for x in inspect.stack()[0:7]])
if self._need_to_rebuild:
self._rebuild_paramvec()
self._need_to_rebuild = False
self._reinit_opcaches() # changes to parameter vector structure invalidate cached ops
if self.dirty: # if any member object is dirty (ModelMember.dirty setter should set this value)
TOL = 1e-8
ops_paramvec = self._model_paramvec_to_ops_paramvec(self._paramvec)
#Note: lbl args used *just* for potential debugging - could strip out once
# we're confident this code always works.
def clean_single_obj(obj, lbl): # sync an object's to_vector result w/_paramvec
if obj.dirty:
w = obj.to_vector()
chk_norm = _np.linalg.norm(ops_paramvec[obj.gpindices] - w)
#print(lbl, " is dirty! vec = ", w, " chk_norm = ",chk_norm)
if (not _np.isfinite(chk_norm)) or chk_norm > TOL:
ops_paramvec[obj.gpindices] = w
obj.dirty = False
def clean_obj(obj, lbl): # recursive so works with objects that have sub-members
for i, subm in enumerate(obj.submembers()):
clean_obj(subm, _Label(lbl.name + ":%d" % i, lbl.sslbls))
clean_single_obj(obj, lbl)
for lbl, obj in self._iter_parameterized_objs():
clean_obj(obj, lbl)
#re-update everything to ensure consistency ~ self.from_vector(self._paramvec)
#print("DEBUG: non-trivially CLEANED paramvec due to dirty elements")
for _, obj in self._iter_parameterized_objs():
obj.from_vector(ops_paramvec[obj.gpindices], dirty_value=False)
#object is known to be consistent with _paramvec
# Call from_vector on elements of the cache
if self._call_fromvector_on_cache:
for opcache in self._opcaches.values():
for obj in opcache.values():
obj.from_vector(ops_paramvec[obj.gpindices], dirty_value=False)
self.dirty = False
self._paramvec[:] = self._ops_paramvec_to_model_paramvec(ops_paramvec)
#self._reinit_opcaches() # this shouldn't be necessary
if OpModel._pcheck: self._check_paramvec()
def _mark_for_rebuild(self, modified_obj=None):
#re-initialze any members that also depend on the updated parameters
self._need_to_rebuild = True
# Specifically, we need to re-allocate indices for every object that
# contains a reference to the modified one. Previously all modelmembers
# of a model needed to have their self.parent *always* point to the
# parent model, and so we would clear the members' .gpindices to indicate
# allocation was needed. Now, that constraint has been loosened, and
# we instead set a member's .parent=None to indicate it needs reallocation
# (this allows the .gpindices to be used, e.g., for parameter counting
# within the object). Because _rebuild_paramvec determines whether an
# object needs allocation by calling its .gpindices_are_allocated method,
# which checks submembers too, there's no longer any need to clear (set
# to None) the .gpindinces any objects here.
#OLD
#for _, o in self._iter_parameterized_objs():
# if o._obj_refcount(modified_obj) > 0:
# o.clear_gpindices() # ~ o.gpindices = None but works w/submembers
# # (so params for this obj will be rebuilt)
self.dirty = True
#since it's likely we'll set at least one of our object's .dirty flags
# to True (and said object may have parent=None and so won't
# auto-propagate up to set this model's dirty flag (self.dirty)
def _print_gpindices(self, max_depth=100):
print("PRINTING MODEL GPINDICES!!!")
for lbl, obj in self._iter_parameterized_objs():
obj._print_gpindices("", str(lbl), max_depth=max_depth)
def print_parameters_by_op(self, max_depth=0):
plbls = {i: lbl for i, lbl in enumerate(self.parameter_labels)}
print("*** MODEL PARAMETERS (%d total) ***" % self.num_params)
for lbl, obj in self._iter_parameterized_objs():
obj._print_gpindices("", lbl, plbls, max_depth)
def collect_parameters(self, params_to_collect, new_param_label=None):
"""
Updates this model's parameters so that previously independent parameters are tied together.
The model's parameterization is modified so that all of the parameters
given by `params_to_collect` are replaced by a single parameter. The label
of this single parameter may be given if desired.
Note that after this function is called the model's parameter vector (i.e. the
result of `to_vector()`) should be assumed to have a new format unrelated to the
parameter vector before their adjustment. For example, you should not assume that
un-modified parameters will retain their old indices.
Parameters
----------
params_to_collect : iterable
A list or tuple of parameter labels describing the parameters to collect.
These should be a subset of the elements of `self.parameter_labels` or
of `self.parameter_labels_pretty`, or integer indices into the model's parameter
vector. If empty, no parameter adjustment is performed.
new_param_label : object, optional
The label for the new common parameter. If `None`, then the parameter label
of the first collected parameter is used.
Returns
-------
None
"""
if all([isinstance(p, int) for p in params_to_collect]):
indices = list(params_to_collect) # all parameters are given as indices
else:
plbl_dict = {plbl: i for i, plbl in enumerate(self.parameter_labels)}
try:
indices = [plbl_dict[plbl] for plbl in params_to_collect]
except KeyError:
plbl_dict_pretty = {plbl: i for i, plbl in enumerate(self.parameter_labels_pretty)}
indices = [plbl_dict_pretty[plbl] for plbl in params_to_collect]
indices.sort()
if len(indices) == 0:
return # nothing to do
# go through all gpindices and reset so that all occurrences of elements in
# indices[1:] are updated to be indices[0]
memo = set() # keep track of which object's gpindices have been set
for _, obj in self._iter_parameterized_objs():
assert(obj.gpindices is not None and obj.parent is self), \
"Model's parameter vector still needs to be built!"
new_gpindices = None
if isinstance(obj.gpindices, slice):
if indices[0] >= obj.gpindices.stop or indices[-1] < obj.gpindices.start:
continue # short circuit so we don't have to check condition in line below
if any([obj.gpindices.start <= i < obj.gpindices.stop for i in indices[1:]]):
new_gpindices = obj.gpindices_as_array()
else:
if indices[0] > max(obj.gpindices) or indices[-1] < min(obj.gpindices):
continue # short circuit
new_gpindices = obj.gpindices.copy()
if new_gpindices is not None:
for k in indices[1:]:
new_gpindices[new_gpindices == k] = indices[0]
obj.set_gpindices(new_gpindices, self, memo)
#Rename the collected parameter if desired.
if new_param_label is not None:
self._paramlbls[indices[0]] = new_param_label
# now all gpindices are updated, so just rebuild paramvec to remove unused indices.
self._rebuild_paramvec()
def uncollect_parameters(self, param_to_uncollect):
"""
Updates this model's parameters so that a common paramter becomes independent parameters.
The model's parameterization is modified so that each usage of the given parameter
in the model's parameterized operations is promoted to being a new independent
parameter. The labels of the new parameters are set by the operations.
Note that after this function is called the model's parameter vector (i.e. the
result of `to_vector()`) should be assumed to have a new format unrelated to the
parameter vector before their adjustment. For example, you should not assume that
un-modified parameters will retain their old indices.
Parameters
----------
param_to_uncollect : int or object
A parameter label specifying the parameter to "uncollect". This should be an
element of `self.parameter_labels` or `self.parameter_labels_pretty`, or it may be
an integer index into the model's parameter vector.
Returns
-------
None
"""
if isinstance(param_to_uncollect, int):
index = param_to_uncollect
else:
plbl_dict = {plbl: i for i, plbl in enumerate(self.parameter_labels)}
try:
index = plbl_dict[param_to_uncollect]
except KeyError:
plbl_dict_pretty = {plbl: i for i, plbl in enumerate(self.parameter_labels_pretty)}
index = plbl_dict_pretty[param_to_uncollect]
# go through all gpindices and reset so that each occurrence of `index` after the
# first gets a new index => independent parameter.
next_new_index = self.num_params; first_occurrence = True
memo = set() # keep track of which object's gpindices have been set
for lbl, obj in self._iter_parameterized_objs():
assert(obj.gpindices is not None and obj.parent is self), \
"Model's parameter vector still needs to be built!"
if id(obj) in memo:
continue # don't add any new indices when set_gpindices doesn't actually do anything
new_gpindices = None
if isinstance(obj.gpindices, slice):
if obj.gpindices.start <= index < obj.gpindices.stop:
if first_occurrence: # just update/reset parameter label
self._paramlbls[index] = (lbl, obj.parameter_labels[index - obj.gpindices.start])
first_occurrence = False
else: # index as a new parameter
new_gpindices = obj.gpindices_as_array()
new_gpindices[index - obj.gpindices.start] = next_new_index
next_new_index += 1
else:
if min(obj.gpindices) <= index <= max(obj.gpindices):
new_gpindices = obj.gpindices.copy()
for i in range(len(new_gpindices)):
if new_gpindices[i] == index:
if first_occurrence: # just update/reset parameter label
self._paramlbls[index] = (lbl, obj.parameter_labels[i])
first_occurrence = False
else: # index as a new parameter
new_gpindices[i] = next_new_index
next_new_index += 1
if new_gpindices is not None:
obj.set_gpindices(new_gpindices, self, memo)
# now all gpindices are updated, so just rebuild paramvec create new parameters.
self._rebuild_paramvec()
def _rebuild_paramvec(self):
""" Resizes self._paramvec and updates gpindices & parent members as needed,
and will initialize new elements of _paramvec, but does NOT change
existing elements of _paramvec (use _update_paramvec for this)"""
w = self._model_paramvec_to_ops_paramvec(self._paramvec)
Np = len(w) # NOT self.num_params since the latter calls us!
wl = self._paramlbls
wb = self._param_bounds if (self._param_bounds is not None) else _default_param_bounds(Np)
#NOTE: interposer doesn't quite work with parameter bounds yet, as we need to convert "model"
# bounds to "ops" bounds like we do the parameter vector. Need something like:
#wb = self._model_parambouds_to_ops_parambounds(self._param_bounds) \
# if (self._param_bounds is not None) else _default_param_bounds(Np)
debug = False
if debug: print("DEBUG: rebuilding model %s..." % str(id(self)))
# Step 1: add parameters that don't exist yet
# Note that iteration order (that of _iter_parameterized_objs) determines
# parameter index ordering, so "normally" an object that occurs before
# another in the iteration order will have gpindices which are lower - and
# when new indices are allocated we try to maintain this normal order by
# inserting them at an appropriate place in the parameter vector.
#Get a record up-front, before any allocations are made, of which objects will need to be reallocated.
is_allocated = {lbl: obj.gpindices_are_allocated(self) for lbl, obj in self._iter_parameterized_objs()}
max_index_processed_so_far = -1
for lbl, obj in self._iter_parameterized_objs():
completely_allocated = is_allocated[lbl]
if debug: print("Processing: ", lbl, " gpindices=", obj.gpindices, " allocated = ", completely_allocated)
if not completely_allocated: # obj.gpindices_are_allocated(self):
# We need to [re-]allocate obj's indices to this model
num_new_params, max_existing_index = obj.preallocate_gpindices(self) # any new indices need allocation?
max_index_processed_so_far = max(max_index_processed_so_far, max_existing_index)
insertion_point = max_index_processed_so_far + 1
if num_new_params > 0:
# If so, before allocating anything, make the necessary space in the parameter arrays:
for _, o in self._iter_parameterized_objs():
o.shift_gpindices(insertion_point, num_new_params, self)
w = _np.insert(w, insertion_point, _np.empty(num_new_params, 'd'))
wl = _np.insert(wl, insertion_point, _np.empty(num_new_params, dtype=object))
wb = _np.insert(wb, insertion_point, _default_param_bounds(num_new_params), axis=0)
# Now allocate (actually updates obj's gpindices). May be necessary even if
# num_new_params == 0 (e.g. a composed op needs to have it's gpindices updated bc
# a sub-member's # of params was updated, but this submember was already allocated
# on an earlier iteration - this is why we compute is_allocated as outset).
num_added_params = obj.allocate_gpindices(insertion_point, self)
assert(num_added_params == num_new_params), \
"Inconsistency between preallocate_gpindices and allocate_gpindices!"
if debug:
print("DEBUG: allocated %d new params starting at %d, resulting gpindices = %s"
% (num_new_params, insertion_point, str(obj.gpindices)))
newly_added_indices = slice(insertion_point, insertion_point + num_added_params) \
if num_added_params > 0 else None # for updating parameter labels below
else:
inds = obj.gpindices_as_array()
M = max(inds) if len(inds) > 0 else -1; L = len(w)
if debug: print("DEBUG: %s: existing indices = " % (str(lbl)), obj.gpindices, " M=", M, " L=", L)
if M >= L:
#Some indices specified by obj are absent, and must be created.
#set newly_added_indices to the indices for *obj* that have just been added
added_indices = slice(len(w), len(w) + (M + 1 - L))
if isinstance(obj.gpindices, slice):
newly_added_indices = _slct.intersect(added_indices, obj.gpindices)
else:
newly_added_indices = inds[_np.logical_and(inds >= added_indices.start,
inds < added_indices.stop)]
w = _np.concatenate((w, _np.empty(M + 1 - L, 'd')), axis=0) # [v.resize(M+1) doesn't work]
wl = _np.concatenate((wl, _np.empty(M + 1 - L, dtype=object)), axis=0)
wb = _np.concatenate((wb, _np.empty((M + 1 - L, 2), 'd')), axis=0)
if debug: print("DEBUG: --> added %d new params" % (M + 1 - L))
else:
newly_added_indices = None
#if M >= 0: # M == -1 signifies this object has no parameters, so we'll just leave `off` alone
# off = M + 1
#Update max_index_processed_so_far
max_gpindex = (obj.gpindices.stop - 1) if isinstance(obj.gpindices, slice) else max(obj.gpindices)
max_index_processed_so_far = max(max_index_processed_so_far, max_gpindex)
# Update parameter values / labels / bounds.
# - updating the labels is not strictly necessary since we usually *clean* the param vector next
# - we *always* sync object parameter bounds in case any have changed (when bounds on members are
# set/modified, model should be marked for rebuilding)
# - we only update *new* parameter labels, since we want any modified labels in the model to "stick"
w[obj.gpindices] = obj.to_vector()
wb[obj.gpindices, :] = obj.parameter_bounds if (obj.parameter_bounds is not None) \
else _default_param_bounds(obj.num_params)
if newly_added_indices is not None:
obj_paramlbls = _np.empty(obj.num_params, dtype=object)
obj_paramlbls[:] = [(lbl, obj_plbl) for obj_plbl in obj.parameter_labels]
wl[newly_added_indices] = obj_paramlbls[_gm._decompose_gpindices(obj.gpindices, newly_added_indices)]
#Step 2: remove any unused indices from paramvec and shift accordingly
used_gpindices = set()
for lbl, obj in self._iter_parameterized_objs():
#print("Removal: ",lbl,str(type(obj)),(id(obj.parent) if obj.parent is not None else None),obj.gpindices)
assert(obj.parent is self and obj.gpindices is not None)
used_gpindices.update(obj.gpindices_as_array())
#OLD: from when this was step 1:
#if obj.gpindices is not None:
# if obj.parent is self: # then obj.gpindices lays claim to our parameters
# used_gpindices.update(obj.gpindices_as_array())
# else:
# # ok for objects to have parent=None (before their params are allocated), in which case non-None
# # gpindices can enable the object to function without a parent model, but not parent=other_model,
# # as this indicates the objects parameters are allocated to another model (and should have been
# # cleared in the OrderedMemberDict.__setitem__ method used to add the model member.
# assert(obj.parent is None), \
# "Member's parent (%s) is not set correctly! (must be this model or None)" % repr(obj.parent)
#else: