Skip to content

Commit 3c9cc7e

Browse files
authored
Merge pull request #146 from QInfer/cgranade/unknown-T2
Add unknown T₂, Gaussian hyperparameterized models.
2 parents 64d9b2f + acbd0c9 commit 3c9cc7e

File tree

6 files changed

+267
-27
lines changed

6 files changed

+267
-27
lines changed

doc/source/apiref/derived_models.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a
55
letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View,
66
California, 94041, USA.
7-
7+
88
.. _derived_models:
9-
9+
1010
.. currentmodule:: qinfer
1111

1212
Derived Models
@@ -30,6 +30,12 @@ additional functionality or changing the behaviors of underlying models.
3030
.. autoclass:: BinomialModel
3131
:members:
3232

33+
:class:`GaussianHyperparameterizedModel` - Model over Gaussian outcomes conditioned on two-outcome experiments
34+
--------------------------------------------------------------------------------------------------------------
35+
36+
.. autoclass:: GaussianHyperparameterizedModel
37+
:members:
38+
3339
:class:`MultinomialModel` - Model over batches of D-outcome experiments
3440
----------------------------------------------------------------------
3541

@@ -41,13 +47,13 @@ additional functionality or changing the behaviors of underlying models.
4147

4248
.. autoclass:: MLEModel
4349
:members:
44-
50+
4551
:class:`RandomWalkModel` - Model for adding fixed random walk to parameters
4652
---------------------------------------------------------------------------
4753

4854
.. autoclass:: RandomWalkModel
4955
:members:
50-
56+
5157
:class:`GaussianRandomWalkModel` - Model for adding gaussian random walk to parameters
5258
--------------------------------------------------------------------------------------
5359

doc/source/apiref/test_models.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,28 @@ built on top of QInfer.
2828
.. autoclass:: SimplePrecessionModel
2929
:members:
3030

31+
:class:`UnknownT2Model` - Model of a single qubit undergoing Larmor precession with finite decoherence
32+
------------------------------------------------------------------------------------------------------
33+
34+
.. autoclass:: UnknownT2Model
35+
:members:
36+
3137
:class:`NoisyCoinModel` - Classical coin flip model corrupted by a noisy process
3238
--------------------------------------------------------------------------------
3339

3440
.. autoclass:: NoisyCoinModel
3541
:members:
36-
37-
:class:`NDieModel`
42+
43+
:class:`NDieModel`
3844
------------------
3945

4046
.. autoclass:: NDieModel
4147
:members:
42-
48+
4349
Custom Models
4450
-------------
4551

46-
Writing custom models is standard practice for QInfer users.
52+
Writing custom models is standard practice for QInfer users.
4753
See :ref:`CustomModels`.
4854

4955
.. currentmodule:: qinfer.tests.base_test

src/qinfer/derived_models.py

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'DerivedModel',
4747
'PoisonedModel',
4848
'BinomialModel',
49+
'GaussianHyperparameterizedModel',
4950
'MultinomialModel',
5051
'MLEModel',
5152
'RandomWalkModel',
@@ -59,14 +60,14 @@
5960
from past.builtins import basestring
6061

6162
import numpy as np
62-
from scipy.stats import binom, multivariate_normal
63+
from scipy.stats import binom, multivariate_normal, norm
6364
from itertools import combinations_with_replacement as tri_comb
6465

6566
from qinfer.utils import binomial_pdf, multinomial_pdf, sample_multinomial
6667
from qinfer.abstract_model import Model, DifferentiableModel
6768
from qinfer._lib import enum # <- TODO: replace with flufl.enum!
6869
from qinfer.utils import binom_est_error
69-
from qinfer.domains import IntegerDomain, MultinomialDomain
70+
from qinfer.domains import IntegerDomain, MultinomialDomain, RealDomain
7071

7172
## FUNCTIONS ###################################################################
7273

@@ -245,9 +246,9 @@ def __init__(self, underlying_model):
245246
else:
246247
self._expparams_scalar = False
247248
self._expparams_dtype = underlying_model.expparams_dtype + [('n_meas', 'uint')]
248-
249+
249250
## PROPERTIES ##
250-
251+
251252
@property
252253
def decorated_model(self):
253254
# Provided for backcompat only.
@@ -380,6 +381,131 @@ def fisher_information(self, modelparams, expparams):
380381
)
381382
return two_outcome_fi * expparams['n_meas']
382383

384+
class GaussianHyperparameterizedModel(DerivedModel):
385+
"""
386+
Model representing a two-outcome model viewed through samples
387+
from one of two distinct Gaussian distributions. This model adds four new
388+
model parameters to its underlying model, respectively representing the
389+
mean outcome conditioned on an underlying 0, the mean outcome conditioned
390+
on an underlying 1, and the variance of outcomes conditioned in each case.
391+
392+
:param qinfer.abstract_model.Model underlying_model: An instance of a two-
393+
outcome model to be viewed through Gaussian distributions.
394+
"""
395+
396+
def __init__(self, underlying_model):
397+
super(GaussianHyperparameterizedModel, self).__init__(underlying_model)
398+
399+
if not (underlying_model.is_n_outcomes_constant and underlying_model.n_outcomes(None) == 2):
400+
raise ValueError("Decorated model must be a two-outcome model.")
401+
402+
n_orig_mps = underlying_model.n_modelparams
403+
self._orig_mps_slice = np.s_[:n_orig_mps]
404+
self._mu_slice = np.s_[n_orig_mps:n_orig_mps + 2]
405+
self._sigma2_slice = np.s_[n_orig_mps + 2:n_orig_mps + 4]
406+
407+
## PROPERTIES ##
408+
409+
@property
410+
def decorated_model(self):
411+
# Provided for backcompat only.
412+
return self.underlying_model
413+
414+
@property
415+
def modelparam_names(self):
416+
return self.underlying_model.modelparam_names + [
417+
r'\mu_0', r'\mu_1',
418+
r'\sigma_0^2', r'\sigma_1^2'
419+
]
420+
421+
@property
422+
def n_modelparams(self):
423+
return len(self.modelparam_names)
424+
425+
## METHODS ##
426+
427+
def domain(self, expparams):
428+
return [RealDomain()] * len(expparams) if expparams is not None else RealDomain()
429+
430+
def are_expparam_dtypes_consistent(self, expparams):
431+
return True
432+
433+
def are_models_valid(self, modelparams):
434+
orig_mps = modelparams[:, self._orig_mps_slice]
435+
sigma2 = modelparams[:, self._sigma2_slice]
436+
437+
return np.all([
438+
self.underlying_model.are_models_valid(orig_mps),
439+
np.all(sigma2 > 0, axis=-1)
440+
], axis=0)
441+
442+
def underlying_likelihood(self, binary_outcomes, modelparams, expparams):
443+
"""
444+
Given outcomes hypothesized for the underlying model, returns the likelihood
445+
which which those outcomes occur.
446+
"""
447+
original_mps = modelparams[..., self._orig_mps_slice]
448+
return self.underlying_model.likelihood(binary_outcomes, original_mps, expparams)
449+
450+
def likelihood(self, outcomes, modelparams, expparams):
451+
# By calling the superclass implementation, we can consolidate
452+
# call counting there.
453+
super(GaussianHyperparameterizedModel, self).likelihood(outcomes, modelparams, expparams)
454+
455+
# We want these to broadcast to the shape
456+
# (idx_underlying_outcome, idx_outcome, idx_modelparam, idx_experiment).
457+
# Thus, we need shape
458+
# (idx_underlying_outcome, 1, idx_modelparam, 1).
459+
mu = (modelparams[:, self._mu_slice].T)[:, np.newaxis, :, np.newaxis]
460+
sigma = np.sqrt(
461+
(modelparams[:, self._sigma2_slice].T)[:, np.newaxis, :, np.newaxis]
462+
)
463+
464+
assert np.all(sigma > 0)
465+
466+
# Now we can rescale the outcomes to be random variates z drawn from N(0, 1).
467+
scaled_outcomes = (outcomes[np.newaxis,:,np.newaxis,np.newaxis] - mu) / sigma
468+
469+
# We can then compute the conditional likelihood Pr(z | underlying_outcome, model).
470+
conditional_L = norm(0, 1).pdf(scaled_outcomes)
471+
472+
# To find the marginalized likeihood, we now need the underlying likelihood
473+
# Pr(underlying_outcome | model), so that we can sum over the idx_u_o axis.
474+
# Note that we need to add a new axis to shift the underlying outcomes left
475+
# of the real-valued outcomes z.
476+
underlying_L = self.underlying_likelihood(
477+
np.array([0, 1], dtype='uint'),
478+
modelparams, expparams
479+
)[:, None, :, :]
480+
481+
# Now we marginalize and return.
482+
return (underlying_L * conditional_L).sum(axis=0)
483+
484+
def simulate_experiment(self, modelparams, expparams, repeat=1):
485+
super(GaussianHyperparameterizedModel, self).simulate_experiment(modelparams, expparams)
486+
487+
# Start by generating a bunch of (0, 1) normalized random variates
488+
# that we'll randomly rescale to the right location and shape.
489+
zs = np.random.randn(modelparams.shape[0], expparams.shape[0])
490+
491+
# Next, we sample a bunch of underlying outcomes to figure out
492+
# how to rescale everything.
493+
underlying_outcomes = self.underlying_model.simulate_experiment(
494+
modelparams[:, :-4], expparams, repeat=repeat
495+
)
496+
497+
# We can now rescale zs to obtain the actual outcomes.
498+
mu = (modelparams[:, self._mu_slice].T)[:, None, :, None]
499+
sigma = np.sqrt(
500+
(modelparams[:, self._sigma2_slice].T)[:, None, :, None]
501+
)
502+
outcomes = (
503+
np.where(underlying_outcomes, mu[1], mu[0]) +
504+
np.where(underlying_outcomes, sigma[1], sigma[0]) * zs
505+
)
506+
507+
return outcomes[0,0,0] if outcomes.size == 1 else outcomes
508+
383509
class MultinomialModel(DerivedModel):
384510
"""
385511
Model representing finite numbers of iid samples from another model with
@@ -680,7 +806,7 @@ def __init__(
680806
# therefore, we need to add modelparams
681807
self._has_fixed_covariance = False
682808
if self._diagonal:
683-
self._srw_names = ["\sigma_{{{}}}".format(name) for name in self._rw_names]
809+
self._srw_names = [r"\sigma_{{{}}}".format(name) for name in self._rw_names]
684810
self._srw_idxs = (underlying_model.n_modelparams + \
685811
np.arange(self._n_rw)).astype(np.int)
686812
else:
@@ -692,9 +818,9 @@ def __init__(
692818
for idx1, name1 in enumerate(self._rw_names):
693819
for name2 in self._rw_names[:idx1+1]:
694820
if name1 == name2:
695-
self._srw_names.append("\sigma_{{{}}}".format(name1))
821+
self._srw_names.append(r"\sigma_{{{}}}".format(name1))
696822
else:
697-
self._srw_names.append("\sigma_{{{},{}}}".format(name2,name1))
823+
self._srw_names.append(r"\sigma_{{{},{}}}".format(name2,name1))
698824
else:
699825
# In this case the covariance matrix is fixed and fully specified
700826
self._has_fixed_covariance = True

src/qinfer/test_models.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
__all__ = [
4444
'SimpleInversionModel',
4545
'SimplePrecessionModel',
46+
'UnknownT2Model',
4647
'CoinModel',
4748
'NoisyCoinModel',
4849
'NDieModel'
@@ -54,10 +55,10 @@
5455

5556
import numpy as np
5657

57-
from .utils import binomial_pdf
58-
58+
from .utils import binomial_pdf, decorate_init
5959
from .abstract_model import FiniteOutcomeModel, DifferentiableModel
60-
60+
from ._due import due, Doi
61+
6162
## CLASSES ####################################################################
6263

6364
class SimpleInversionModel(FiniteOutcomeModel, DifferentiableModel):
@@ -137,28 +138,28 @@ def likelihood(self, outcomes, modelparams, expparams):
137138
# will cause an error.
138139
pr0 = np.zeros((modelparams.shape[0], expparams.shape[0]))
139140
pr0[:, :] = np.cos(t * dw / 2) ** 2
140-
141+
141142
# Now we concatenate over outcomes.
142143
return FiniteOutcomeModel.pr0_to_likelihood_array(outcomes, pr0)
143144

144145
def score(self, outcomes, modelparams, expparams, return_L=False):
145146
if len(modelparams.shape) == 1:
146147
modelparams = modelparams[:, np.newaxis]
147-
148+
148149
t = expparams['t']
149150
dw = modelparams - expparams['w_']
150151

151152
outcomes = outcomes.reshape((outcomes.shape[0], 1, 1))
152153

153-
arg = dw * t / 2
154+
arg = dw * t / 2
154155
q = (
155156
np.power( t / np.tan(arg), outcomes) *
156157
np.power(-t * np.tan(arg), 1 - outcomes)
157158
)[np.newaxis, ...]
158159

159160
assert q.ndim == 4
160-
161-
161+
162+
162163
if return_L:
163164
return q, self.likelihood(outcomes, modelparams, expparams)
164165
else:
@@ -188,15 +189,21 @@ def likelihood(self, outcomes, modelparams, expparams):
188189
# Pass the expparams to the superclass as a record array.
189190
new_eps = np.empty(expparams.shape, dtype=super(SimplePrecessionModel, self).expparams_dtype)
190191
new_eps['w_'] = 0
191-
new_eps['t'] = expparams
192+
try:
193+
new_eps['t'] = expparams
194+
except ValueError:
195+
new_eps['t'] = expparams['t']
192196

193197
return super(SimplePrecessionModel, self).likelihood(outcomes, modelparams, new_eps)
194198

195199
def score(self, outcomes, modelparams, expparams, return_L=False):
196200
# Pass the expparams to the superclass as a record array.
197201
new_eps = np.empty(expparams.shape, dtype=super(SimplePrecessionModel, self).expparams_dtype)
198202
new_eps['w_'] = 0
199-
new_eps['t'] = expparams
203+
try:
204+
new_eps['t'] = expparams
205+
except ValueError:
206+
new_eps['t'] = expparams['t']
200207

201208
q = super(SimplePrecessionModel, self).score(outcomes, modelparams, new_eps, return_L=False)
202209

@@ -205,6 +212,53 @@ def score(self, outcomes, modelparams, expparams, return_L=False):
205212
else:
206213
return q
207214

215+
@decorate_init(
216+
due.dcite(
217+
Doi('10.1088/1367-2630/14/10/103013'),
218+
description='Robust online Hamiltonian learning',
219+
tags=['implementation']
220+
)
221+
)
222+
class UnknownT2Model(FiniteOutcomeModel):
223+
"""
224+
Describes the free evolution of a single qubit prepared in the
225+
:math:`\left|+\right\rangle` state under a Hamiltonian
226+
:math:`H = \omega \sigma_z / 2` with an unknown :math:`T_2` process,
227+
as explored in [GFWC12]_.
228+
229+
:modelparam omega: The precession frequency :math:`\omega`.
230+
:modelparam T2_inv: The decoherence strength :math:`T_2^{-1}`.
231+
:scalar-expparam float: The evolution time :math:`t`.
232+
"""
233+
234+
@property
235+
def n_modelparams(self): return 2
236+
237+
@property
238+
def modelparam_names(self): return [r'\omega', r'T_2^{-1}']
239+
240+
@property
241+
def expparams_dtype(self):
242+
return [('t', 'float')]
243+
244+
def n_outcomes(self, modelparams):
245+
return 2
246+
247+
def are_models_valid(self, modelparams):
248+
return np.all(modelparams >= 0, axis=1)
249+
250+
def likelihood(self, outcomes, modelparams, expparams):
251+
w, T2_inv = modelparams.T[:, :, None]
252+
t = expparams['t']
253+
254+
visibility = np.exp(-t * T2_inv)
255+
256+
pr0 = np.empty((w.shape[0], t.shape[0]))
257+
pr0[:, :] = visibility * np.cos(w * t / 2) ** 2 + (1 - visibility) / 2
258+
259+
return FiniteOutcomeModel.pr0_to_likelihood_array(outcomes, pr0)
260+
261+
208262
class CoinModel(FiniteOutcomeModel, DifferentiableModel):
209263
r"""
210264
Arguably the simplest possible model; the unknown model parameter

0 commit comments

Comments
 (0)