Commit 9947fa6d authored by Gordian Edenhofer's avatar Gordian Edenhofer
Browse files

correlated_fields.py: Move ZM to finalize

The zero-mode must not be known for constructing amplitudes. Only when
finalizing the model, the zero-mode needs to be specified. Honor this by
pulling all parameters relating to the zero-mode to the `finalize`
method.

Move `moment_slice_to_average` outside of `CorrelatedFieldMaker` as it
does not depend on nor require the inherent structure of the class.
parent b28fa6f8
......@@ -87,17 +87,21 @@
"main_sample = None\n",
"def init_model(m_pars, fl_pars):\n",
" global main_sample\n",
" cf = ift.CorrelatedFieldMaker.make(**m_pars)\n",
" offset_mean = m_pars[\"offset_mean\"]\n",
" offset_std = m_pars[\"offset_std\"]\n",
" cf = ift.CorrelatedFieldMaker.make(m_pars[\"prefix\"])\n",
" cf.add_fluctuations(**fl_pars)\n",
" field = cf.finalize(prior_info=0)\n",
" field = cf.finalize(offset_mean, offset_std, prior_info=0)\n",
" main_sample = ift.from_random(field.domain)\n",
" print(\"model domain keys:\", field.domain.keys())\n",
" \n",
"# Helper: field and spectrum from parameter dictionaries + plotting\n",
"def eval_model(m_pars, fl_pars, title=None, samples=None):\n",
" cf = ift.CorrelatedFieldMaker.make(**m_pars)\n",
" offset_mean = m_pars[\"offset_mean\"]\n",
" offset_std = m_pars[\"offset_std\"]\n",
" cf = ift.CorrelatedFieldMaker.make(m_pars[\"prefix\"])\n",
" cf.add_fluctuations(**fl_pars)\n",
" field = cf.finalize(prior_info=0)\n",
" field = cf.finalize(offset_mean, offset_std, prior_info=0)\n",
" spectrum = cf.amplitude\n",
" if samples is None:\n",
" samples = [main_sample]\n",
......@@ -462,18 +466,6 @@
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.3"
}
},
"nbformat": 4,
......
%% Cell type:markdown id: tags:
# Notebook showcasing the NIFTy 6 Correlated Field model
**Skip to `Parameter Showcases` for the meat/veggies ;)**
The field model roughly works like this:
`f = HT( A * zero_mode * xi ) + offset`
`A` is a spectral power field which is constructed from power spectra that hold on subdomains of the target domain.
It is scaled by a zero mode operator and then pointwise multiplied by a gaussian excitation field, yielding
a representation of the field in harmonic space.
It is then transformed into the target real space and a offset added.
The power spectra `A` is constructed of are in turn constructed as the sum of a power law component
and an integrated Wiener process whose amplitude and roughness can be set.
## Setup code
%% Cell type:code id: tags:
``` python
import nifty7 as ift
import matplotlib.pyplot as plt
import numpy as np
ift.random.push_sseq_from_seed(43)
n_pix = 256
x_space = ift.RGSpace(n_pix)
```
%% Cell type:code id: tags:
``` python
# Plotting routine
def plot(fields, spectra, title=None):
# Plotting preparation is normally handled by nifty7.Plot
# It is done manually here to be able to tweak details
# Fields are assumed to have identical domains
fig = plt.figure(tight_layout=True, figsize=(12, 3.5))
if title is not None:
fig.suptitle(title, fontsize=14)
# Field
ax1 = fig.add_subplot(1, 2, 1)
ax1.axhline(y=0., color='k', linestyle='--', alpha=0.25)
for field in fields:
dom = field.domain[0]
xcoord = np.arange(dom.shape[0]) * dom.distances[0]
ax1.plot(xcoord, field.val)
ax1.set_xlim(xcoord[0], xcoord[-1])
ax1.set_ylim(-5., 5.)
ax1.set_xlabel('x')
ax1.set_ylabel('f(x)')
ax1.set_title('Field realizations')
# Spectrum
ax2 = fig.add_subplot(1, 2, 2)
for spectrum in spectra:
xcoord = spectrum.domain[0].k_lengths
ycoord = spectrum.val_rw()
ycoord[0] = ycoord[1]
ax2.plot(xcoord, ycoord)
ax2.set_ylim(1e-6, 10.)
ax2.set_xscale('log')
ax2.set_yscale('log')
ax2.set_xlabel('k')
ax2.set_ylabel('p(k)')
ax2.set_title('Power Spectrum')
fig.align_labels()
plt.show()
# Helper: draw main sample
main_sample = None
def init_model(m_pars, fl_pars):
global main_sample
cf = ift.CorrelatedFieldMaker.make(**m_pars)
offset_mean = m_pars["offset_mean"]
offset_std = m_pars["offset_std"]
cf = ift.CorrelatedFieldMaker.make(m_pars["prefix"])
cf.add_fluctuations(**fl_pars)
field = cf.finalize(prior_info=0)
field = cf.finalize(offset_mean, offset_std, prior_info=0)
main_sample = ift.from_random(field.domain)
print("model domain keys:", field.domain.keys())
# Helper: field and spectrum from parameter dictionaries + plotting
def eval_model(m_pars, fl_pars, title=None, samples=None):
cf = ift.CorrelatedFieldMaker.make(**m_pars)
offset_mean = m_pars["offset_mean"]
offset_std = m_pars["offset_std"]
cf = ift.CorrelatedFieldMaker.make(m_pars["prefix"])
cf.add_fluctuations(**fl_pars)
field = cf.finalize(prior_info=0)
field = cf.finalize(offset_mean, offset_std, prior_info=0)
spectrum = cf.amplitude
if samples is None:
samples = [main_sample]
field_realizations = [field(s) for s in samples]
spectrum_realizations = [spectrum.force(s) for s in samples]
plot(field_realizations, spectrum_realizations, title)
def gen_samples(key_to_vary):
if key_to_vary is None:
return [main_sample]
dct = main_sample.to_dict()
subdom_to_vary = dct.pop(key_to_vary).domain
samples = []
for i in range(8):
d = dct.copy()
d[key_to_vary] = ift.from_random(subdom_to_vary)
samples.append(ift.MultiField.from_dict(d))
return samples
def vary_parameter(parameter_key, values, samples_vary_in=None):
s = gen_samples(samples_vary_in)
for v in values:
if parameter_key in cf_make_pars.keys():
m_pars = {**cf_make_pars, parameter_key: v}
eval_model(m_pars, cf_x_fluct_pars, f"{parameter_key} = {v}", s)
else:
fl_pars = {**cf_x_fluct_pars, parameter_key: v}
eval_model(cf_make_pars, fl_pars, f"{parameter_key} = {v}", s)
```
%% Cell type:markdown id: tags:
## Before the Action: The Moment-Matched Log-Normal Distribution
Many properties of the correlated field are modelled as being lognormally distributed.
The distribution models are parametrized via their means and standard-deviations (first and second position in tuple).
To get a feeling of how the ratio of the `mean` and `stddev` parameters influences the distribution shape,
here are a few example histograms: (observe the x-axis!)
%% Cell type:code id: tags:
``` python
fig = plt.figure(figsize=(13, 3.5))
mean = 1.0
sigmas = [1.0, 0.5, 0.1]
for i in range(3):
op = ift.LognormalTransform(mean=mean, sigma=sigmas[i],
key='foo', N_copies=0)
op_samples = np.array(
[op(s).val for s in [ift.from_random(op.domain) for i in range(10000)]])
ax = fig.add_subplot(1, 3, i + 1)
ax.hist(op_samples, bins=50)
ax.set_title(f"mean = {mean}, sigma = {sigmas[i]}")
ax.set_xlabel('x')
del op_samples
plt.show()
```
%% Cell type:markdown id: tags:
## The Neutral Field
To demonstrate the effect of all parameters, first a 'neutral' set of parameters
is defined which then are varied one by one, showing the effect of the variation
on the generated field realizations and the underlying power spectrum from which
they were drawn.
As a neutral field, a model with a white power spectrum and vanishing spectral power was chosen.
%% Cell type:code id: tags:
``` python
# Neutral model parameters yielding a quasi-constant field
cf_make_pars = {
'offset_mean': 0.,
'offset_std': (1e-3, 1e-16),
'prefix': ''
}
cf_x_fluct_pars = {
'target_subdomain': x_space,
'fluctuations': (1e-3, 1e-16),
'flexibility': (1e-3, 1e-16),
'asperity': (1e-3, 1e-16),
'loglogavgslope': (0., 1e-16)
}
init_model(cf_make_pars, cf_x_fluct_pars)
```
%% Cell type:code id: tags:
``` python
# Show neutral field
eval_model(cf_make_pars, cf_x_fluct_pars, "Neutral Field")
```
%% Cell type:markdown id: tags:
# Parameter Showcases
## The `fluctuations` parameters of `add_fluctuations()`
determine the **amplitude of variations along the field dimension**
for which `add_fluctuations` is called.
`fluctuations[0]` set the _average_ amplitude of the fields fluctuations along the given dimension,\
`fluctuations[1]` sets the width and shape of the amplitude distribution.
The amplitude is modelled as being log-normally distributed,
see `The Moment-Matched Log-Normal Distribution` above for details.
#### `fluctuations` mean:
%% Cell type:code id: tags:
``` python
vary_parameter('fluctuations', [(0.05, 1e-16), (0.5, 1e-16), (2., 1e-16)], samples_vary_in='xi')
```
%% Cell type:markdown id: tags:
#### `fluctuations` std:
%% Cell type:code id: tags:
``` python
vary_parameter('fluctuations', [(1., 0.01), (1., 0.1), (1., 1.)], samples_vary_in='fluctuations')
cf_x_fluct_pars['fluctuations'] = (1., 1e-16)
```
%% Cell type:markdown id: tags:
## The `loglogavgslope` parameters of `add_fluctuations()`
determine **the slope of the loglog-linear (power law) component of the power spectrum**.
The slope is modelled to be normally distributed.
#### `loglogavgslope` mean:
%% Cell type:code id: tags:
``` python
vary_parameter('loglogavgslope', [(-6., 1e-16), (-2., 1e-16), (2., 1e-16)], samples_vary_in='xi')
```
%% Cell type:markdown id: tags:
#### `loglogavgslope` std:
%% Cell type:code id: tags:
``` python
vary_parameter('loglogavgslope', [(-2., 0.02), (-2., 0.2), (-2., 2.0)], samples_vary_in='loglogavgslope')
cf_x_fluct_pars['loglogavgslope'] = (-2., 1e-16)
```
%% Cell type:markdown id: tags:
## The `flexibility` parameters of `add_fluctuations()`
determine **the amplitude of the integrated Wiener process component of the power spectrum**
(how strong the power spectrum varies besides the power-law).
`flexibility[0]` sets the _average_ amplitude of the i.g.p. component,\
`flexibility[1]` sets how much the amplitude can vary.\
These two parameters feed into a moment-matched log-normal distribution model,
see above for a demo of its behavior.
#### `flexibility` mean:
%% Cell type:code id: tags:
``` python
vary_parameter('flexibility', [(0.4, 1e-16), (4.0, 1e-16), (12.0, 1e-16)], samples_vary_in='spectrum')
```
%% Cell type:markdown id: tags:
#### `flexibility` std:
%% Cell type:code id: tags:
``` python
vary_parameter('flexibility', [(4., 0.02), (4., 0.2), (4., 2.)], samples_vary_in='flexibility')
cf_x_fluct_pars['flexibility'] = (4., 1e-16)
```
%% Cell type:markdown id: tags:
## The `asperity` parameters of `add_fluctuations()`
`asperity` determines **how rough the integrated Wiener process component of the power spectrum is**.
`asperity[0]` sets the average roughness, `asperity[1]` sets how much the roughness can vary.\
These two parameters feed into a moment-matched log-normal distribution model,
see above for a demo of its behavior.
#### `asperity` mean:
%% Cell type:code id: tags:
``` python
vary_parameter('asperity', [(0.001, 1e-16), (1.0, 1e-16), (5., 1e-16)], samples_vary_in='spectrum')
```
%% Cell type:markdown id: tags:
#### `asperity` std:
%% Cell type:code id: tags:
``` python
vary_parameter('asperity', [(1., 0.01), (1., 0.1), (1., 1.)], samples_vary_in='asperity')
cf_x_fluct_pars['asperity'] = (1., 1e-16)
```
%% Cell type:markdown id: tags:
## The `offset_mean` parameter of `CorrelatedFieldMaker.make()`
The `offset_mean` parameter defines a global additive offset on the field realizations.
If the field is used for a lognormal model `f = field.exp()`, this acts as a global signal magnitude offset.
%% Cell type:code id: tags:
``` python
# Reset model to neutral
cf_x_fluct_pars['fluctuations'] = (1e-3, 1e-16)
cf_x_fluct_pars['flexibility'] = (1e-3, 1e-16)
cf_x_fluct_pars['asperity'] = (1e-3, 1e-16)
cf_x_fluct_pars['loglogavgslope'] = (1e-3, 1e-16)
```
%% Cell type:code id: tags:
``` python
vary_parameter('offset_mean', [3., 0., -2.])
```
%% Cell type:markdown id: tags:
## The `offset_std` parameters of `CorrelatedFieldMaker.make()`
Variation of the global offset of the field are modelled as being log-normally distributed.
See `The Moment-Matched Log-Normal Distribution` above for details.
The `offset_std[0]` parameter sets how much NIFTy will vary the offset *on average*.\
The `offset_std[1]` parameters defines the with and shape of the offset variation distribution.
#### `offset_std` mean:
%% Cell type:code id: tags:
``` python
vary_parameter('offset_std', [(1e-16, 1e-16), (0.5, 1e-16), (2., 1e-16)], samples_vary_in='xi')
```
%% Cell type:markdown id: tags:
#### `offset_std` std:
%% Cell type:code id: tags:
``` python
vary_parameter('offset_std', [(1., 0.01), (1., 0.1), (1., 1.)], samples_vary_in='zeromode')
```
......
......@@ -73,11 +73,11 @@ def main():
sp2 = ift.RGSpace(npix2)
# Set up signal model
cfmaker = ift.CorrelatedFieldMaker.make(0., (1e-2, 1e-6), '')
cfmaker = ift.CorrelatedFieldMaker.make('')
cfmaker.add_fluctuations(sp1, (0.1, 1e-2), (2, .2), (.01, .5), (-4, 2.), 'amp1')
cfmaker.add_fluctuations(sp2, (0.1, 1e-2), (2, .2), (.01, .5),
(-3, 1), 'amp2')
correlated_field = cfmaker.finalize()
correlated_field = cfmaker.finalize(0., (1e-2, 1e-6))
pspec1 = cfmaker.normalized_amplitudes[0]**2
pspec2 = cfmaker.normalized_amplitudes[1]**2
......
......@@ -88,6 +88,7 @@ from .library.adjust_variances import (make_adjust_variances_hamiltonian,
do_adjust_variances)
from .library.nft import Gridder, FinuFFT
from .library.correlated_fields import CorrelatedFieldMaker
from .library.correlated_fields import moment_slice_to_average
from .library.correlated_fields_simple import SimpleCorrelatedField
from . import extra
......
......@@ -85,6 +85,21 @@ def _total_fluctuation_realized(samples):
return np.sqrt(res if np.isscalar(res) else res.val)
def moment_slice_to_average(amp, zm, fluctuations_slice_mean, nsamples=1000):
fluctuations_slice_mean = float(fluctuations_slice_mean)
if not fluctuations_slice_mean > 0:
msg = "fluctuations_slice_mean must be greater zero; got {!r}"
raise ValueError(msg.format(fluctuations_slice_mean))
from ..sugar import from_random
scm = 1.
for a in amp:
op = a.fluctuation_amplitude*zm.ptw("reciprocal")
res = np.array([op(from_random(op.domain, 'normal')).val
for _ in range(nsamples)])
scm *= res**2 + 1.
return fluctuations_slice_mean/np.mean(np.sqrt(scm))
class _SlopeRemover(EndomorphicOperator):
def __init__(self, domain, space=0):
self._domain = makeDomain(domain)
......@@ -413,19 +428,17 @@ class CorrelatedFieldMaker:
See the methods :func:`make`, :func:`add_fluctuations`
and :func:`finalize` for further usage information."""
def __init__(self, offset_mean, offset_fluctuations_op, prefix, total_N):
if not isinstance(offset_fluctuations_op, Operator):
raise TypeError("offset_fluctuations_op needs to be an operator")
def __init__(self, prefix, total_N):
self._azm = None
self._offset_mean = None
self._a = []
self._target_subdomains = []
self._offset_mean = offset_mean
self._azm = offset_fluctuations_op
self._prefix = prefix
self._total_N = total_N
@staticmethod
def make(offset_mean, offset_std, prefix, total_N=0, dofdex=None):
def make(prefix, total_N=0):
"""Returns a CorrelatedFieldMaker object.
Parameters
......@@ -452,17 +465,7 @@ class CorrelatedFieldMaker:
*If not specified*, use the same zero mode model for all
constructed field models.
"""
if dofdex is None:
dofdex = np.full(total_N, 0)
elif len(dofdex) != total_N:
raise ValueError("length of dofdex needs to match total_N")
N = max(dofdex) + 1 if total_N > 0 else 0
if len(offset_std) != 2:
raise TypeError
zm = LognormalTransform(*offset_std, prefix + 'zeromode', N)
if total_N > 0:
zm = _Distributor(dofdex, zm.target, UnstructuredDomain(total_N)) @ zm
return CorrelatedFieldMaker(offset_mean, zm, prefix, total_N)
return CorrelatedFieldMaker(prefix, total_N)
def add_fluctuations(self,
target_subdomain,
......@@ -664,7 +667,7 @@ class CorrelatedFieldMaker:
self._a.append(amp)
self._target_subdomains.append(target_subdomain)
def finalize(self, prior_info=100):
def finalize(self, offset_mean, offset_std, dofdex=None, prior_info=100):
"""Finishes model construction process and returns the constructed
operator.
......@@ -674,6 +677,20 @@ class CorrelatedFieldMaker:
How many prior samples to draw for property verification statistics
If zero, skips calculating and displaying statistics.
"""
if dofdex is None:
dofdex = np.full(self._total_N, 0)
elif len(dofdex) != self._total_N:
raise ValueError("length of dofdex needs to match total_N")
N = max(dofdex) + 1 if self._total_N > 0 else 0
# TODO: Allow for `offset_std` being an arbitrary operator
if len(offset_std) != 2:
raise TypeError
zm = LognormalTransform(*offset_std, self._prefix + 'zeromode', N)
if self._total_N > 0:
zm = _Distributor(dofdex, zm.target, UnstructuredDomain(self._total_N)) @ zm
self._azm = zm
self._offset_mean = offset_mean
n_amplitudes = len(self._a)
if self._total_N > 0:
hspace = makeDomain(
......@@ -757,22 +774,9 @@ class CorrelatedFieldMaker:
for m, s in zip(mean.flatten(), stddev.flatten()):
logger.info('{}: {:.02E} ± {:.02E}'.format(kk, m, s))
def moment_slice_to_average(self, fluctuations_slice_mean, nsamples=1000):
fluctuations_slice_mean = float(fluctuations_slice_mean)
if not fluctuations_slice_mean > 0:
msg = "fluctuations_slice_mean must be greater zero; got {!r}"
raise ValueError(msg.format(fluctuations_slice_mean))
from ..sugar import from_random
scm = 1.
for a in self._a:
op = a.fluctuation_amplitude*self._azm.ptw("reciprocal")
res = np.array([op(from_random(op.domain, 'normal')).val
for _ in range(nsamples)])
scm *= res**2 + 1.
return fluctuations_slice_mean/np.mean(np.sqrt(scm))
@property
def normalized_amplitudes(self):
# TODO: rename
"""Returns the amplitude operators used in the model"""
return self._a
......
......@@ -71,7 +71,7 @@ def test_init(total_N, asperity, flexibility, ind, matern):
pytest.skip()
cfg = 1, 1
for dofdex in ([None], [None, [0]], [None, [0, 0], [0, 1], [1, 1]])[total_N]:
cfm = ift.CorrelatedFieldMaker.make(0, cfg, '', total_N, dofdex)
cfm = ift.CorrelatedFieldMaker.make('', total_N)
cfm.add_fluctuations(ift.RGSpace(4), cfg, flexibility, asperity, (-2, 0.1))
if matern:
if total_N == 0:
......@@ -81,7 +81,7 @@ def test_init(total_N, asperity, flexibility, ind, matern):
cfm.add_fluctuations_matern(ift.RGSpace(4), *(3*[cfg]))
else:
cfm.add_fluctuations(ift.RGSpace(4), *(4*[cfg]), index=ind)
cfm.finalize(0)
cfm.finalize(0, cfg, dofdex=dofdex, prior_info=0)
@pmp('sspace', spaces)
......@@ -94,13 +94,12 @@ def testAmplitudesInvariants(sspace, N):
astds = 0.2, 1.2
offset_std_mean = 1.3
fa = ift.CorrelatedFieldMaker.make(1.2, (offset_std_mean, 1e-2), '', N,
dofdex1)
fa = ift.CorrelatedFieldMaker.make('', N)
fa.add_fluctuations(sspace, (astds[0], 1e-2), (1.1, 2.), (2.1, .5), (-2, 1.),
'spatial', dofdex=dofdex2)
fa.add_fluctuations(fsspace, (astds[1], 1e-2), (3.1, 1.), (.5, .1), (-4, 1.),
'freq', dofdex=dofdex3)
op = fa.finalize()
op = fa.finalize(1.2, (offset_std_mean, 1e-2), dofdex=dofdex1, prior_info=0)
samples = [ift.from_random(op.domain) for _ in range(100)]
tot_flm, _ = _stats(fa.total_fluctuation, samples)
......@@ -126,14 +125,18 @@ def testAmplitudesInvariants(sspace, N):
assert_allclose(slice_fluct_std0, sl_fluct_space, rtol=0.5)
assert_allclose(slice_fluct_std1, sl_fluct_freq, rtol=0.5)
fa = ift.CorrelatedFieldMaker.make(0., (offset_std_mean, .1), '', N, dofdex1)
fa = ift.CorrelatedFieldMaker.make('', N)
fa.add_fluctuations(fsspace, (astds[1], 1.), (3.1, 1.), (.5, .1), (-4, 1.), 'freq',
dofdex=dofdex3)
m = 3.
x = fa.moment_slice_to_average(m)
n_zm = max(dofdex1) + 1 if N > 0 else 0
zm = ift.LognormalTransform(*(offset_std_mean, .1), 'zeromode', n_zm)
if N > 0:
zm = ift.library.correlated_fields._Distributor(dofdex1, zm.target, ift.UnstructuredDomain(N)) @ zm
x = ift.moment_slice_to_average(fa.normalized_amplitudes, zm, m)
fa.add_fluctuations(sspace, (x, 1.5), (1.1, 2.), (2.1, .5), (-2, 1.), 'spatial', 0,
dofdex=dofdex2)
op = fa.finalize()
op = fa.finalize(0., (offset_std_mean, .1), dofdex=dofdex1, prior_info=0)
em, estd = _stats(fa.slice_fluctuation(0), samples)
assert_allclose(m, em, rtol=0.5)
......@@ -180,7 +183,7 @@ def test_complicated_vs_simple(seed, domain, without):
asperity,
loglogavgslope
)
cfm = ift.CorrelatedFieldMaker.make(offset_mean, offset_std, prefix)
cfm = ift.CorrelatedFieldMaker.make(prefix)
if asperity is not None and flexibility is None:
with pytest.raises(ValueError):
scf = ift.SimpleCorrelatedField(*scf_args, prefix=prefix,
......@@ -194,7 +197,7 @@ def test_complicated_vs_simple(seed, domain, without):
cfm.add_fluctuations(*add_fluct_args, prefix='',
harmonic_partner=hspace)
inp = ift.from_random(scf.domain)
op1 = cfm.finalize()
op1 = cfm.finalize(offset_mean, offset_std, prior_info=0)
assert scf.domain is op1.domain
ift.extra.assert_allclose(scf(inp), op1(inp))
ift.extra.check_operator(scf, inp, ntries=10)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment