Commit 03765dbf authored by Martin Reinecke's avatar Martin Reinecke

merge

parents b6a01964 e8618b90
Pipeline #75301 passed with stages
in 8 minutes and 27 seconds
......@@ -119,3 +119,8 @@ run_curve_fitting:
artifacts:
paths:
- '*.png'
run_visual_mgvi:
stage: demo_runs
script:
- python3 demos/mgvi_visualized.py
......@@ -47,9 +47,9 @@ import numpy as np
dom = ift.UnstructuredDomain(5)
dtype = [np.float64, np.complex128][1]
invcov = ift.ScalingOperator(dom, 3)
e = ift.GaussianEnergy(mean=ift.from_random('normal', dom, dtype=dtype),
e = ift.GaussianEnergy(mean=ift.from_random(dom, 'normal', dtype=dtype),
inverse_covariance=invcov)
pos = ift.from_random('normal', dom, dtype=np.complex128)
pos = ift.from_random(dom, 'normal', dtype=np.complex128)
lin = e(ift.Linearization.make_var(pos, want_metric=True))
met = lin.metric
print(met)
......@@ -71,6 +71,13 @@ the generation of reproducible random numbers in the presence of MPI parallelism
and leads to cleaner code overall. Please see the documentation of
`nifty6.random` for details.
Interface Change for from_random and OuterProduct
=================================================
The sugar.from_random, Field.from_random, MultiField.from_random now take domain
as the first argument and default to 'normal' for the second argument.
Likewise OuterProduct takes domain as the first argument and a field as the second.
Interface Change for non-linear Operators
=========================================
......
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2019 Max-Planck-Society
# Author: Martin Reinecke
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
from time import time
import matplotlib.pyplot as plt
......
......@@ -43,7 +43,7 @@ if __name__ == '__main__':
harmonic_space = position_space.get_default_codomain()
HT = ift.HarmonicTransformOperator(harmonic_space, position_space)
position = ift.from_random('normal', harmonic_space)
position = ift.from_random(harmonic_space, 'normal')
# Define power spectrum and amplitudes
def sqrtpspec(k):
......@@ -58,13 +58,13 @@ if __name__ == '__main__':
# Generate mock data
p = R(sky)
mock_position = ift.from_random('normal', harmonic_space)
mock_position = ift.from_random(harmonic_space, 'normal')
tmp = p(mock_position).val.astype(np.float64)
data = ift.random.current_rng().binomial(1, tmp)
data = ift.Field.from_raw(R.target, data)
# Compute likelihood and Hamiltonian
position = ift.from_random('normal', harmonic_space)
position = ift.from_random(harmonic_space, 'normal')
likelihood = ift.BernoulliEnergy(data) @ p
ic_newton = ift.DeltaEnergyController(
name='Newton', iteration_limit=100, tol_rel_deltaE=1e-8)
......
......@@ -40,7 +40,7 @@ def make_checkerboard_mask(position_space):
def make_random_mask():
# Random mask for spherical mode
mask = ift.from_random('pm1', position_space)
mask = ift.from_random(position_space, 'pm1')
mask = (mask + 1)/2
return mask.val
......
......@@ -90,7 +90,7 @@ if __name__ == '__main__':
# Generate mock data and define likelihood operator
d_space = R.target[0]
lamb = R(sky)
mock_position = ift.from_random('normal', domain)
mock_position = ift.from_random(domain, 'normal')
data = lamb(mock_position)
data = ift.random.current_rng().poisson(data.val.astype(np.float64))
data = ift.Field.from_raw(d_space, data)
......@@ -103,7 +103,7 @@ if __name__ == '__main__':
# Compute MAP solution by minimizing the information Hamiltonian
H = ift.StandardHamiltonian(likelihood)
initial_position = ift.from_random('normal', domain)
initial_position = ift.from_random(domain, 'normal')
H = ift.EnergyAdapter(initial_position, H, want_metric=True)
H, convergence = minimizer(H)
......
......@@ -98,7 +98,7 @@ if __name__ == '__main__':
N = ift.ScalingOperator(data_space, noise)
# Generate mock signal and data
mock_position = ift.from_random('normal', signal_response.domain)
mock_position = ift.from_random(signal_response.domain, 'normal')
data = signal_response(mock_position) + N.draw_sample_with_dtype(dtype=np.float64)
# Minimization parameters
......
......@@ -97,7 +97,7 @@ if __name__ == '__main__':
N = ift.ScalingOperator(data_space, noise)
# Generate mock signal and data
mock_position = ift.from_random('normal', signal_response.domain)
mock_position = ift.from_random(signal_response.domain, 'normal')
data = signal_response(mock_position) + N.draw_sample_with_dtype(dtype=np.float64)
plot = ift.Plot()
......@@ -114,7 +114,9 @@ if __name__ == '__main__':
ic_newton = ift.AbsDeltaEnergyController(name='Newton',
deltaE=0.01,
iteration_limit=35)
minimizer = ift.NewtonCG(ic_newton)
ic_sampling.enable_logging()
ic_newton.enable_logging()
minimizer = ift.NewtonCG(ic_newton, activate_logging=True)
## number of samples used to estimate the KL
N_samples = 20
......@@ -143,10 +145,15 @@ if __name__ == '__main__':
plot.add([A2.force(KL.position),
A2.force(mock_position)],
title="power2")
plot.output(nx=2,
plot.add((ic_newton.history, ic_sampling.history,
minimizer.inversion_history),
label=['KL', 'Sampling', 'Newton inversion'],
title='Cumulative energies', s=[None, None, 1],
alpha=[None, 0.2, None])
plot.output(nx=3,
ny=2,
ysize=10,
xsize=10,
xsize=15,
name=filename.format("loop_{:02d}".format(i)))
# Done, draw posterior samples
......
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2013-2020 Max-Planck-Society
# Authors: Reimar Leike, Philipp Arras
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
###############################################################################
# Metric Gaussian Variational Inference (MGVI)
#
# This script demonstrates how MGVI works for an inference problem with only
# two real quantities of interest. This enables us to plot the posterior
# probability density as two-dimensional plot. The posterior samples generated
# by MGVI are contrasted with the maximum-a-posterior (MAP) solution together
# with samples drawn with the Laplace method. This method uses the local
# curvature at the MAP solution as inverse covariance of a Gaussian probability
# density.
###############################################################################
import numpy as np
import pylab as plt
from matplotlib.colors import LogNorm
import nifty7 as ift
if __name__ == '__main__':
dom = ift.UnstructuredDomain(1)
scale = 10
a = ift.FieldAdapter(dom, 'a')
b = ift.FieldAdapter(dom, 'b')
lh = (a.adjoint @ a).scale(scale) + (b.adjoint @ b).scale(-1.35*2).exp()
lh = ift.VariableCovarianceGaussianEnergy(dom, 'a', 'b', np.float64) @ lh
icsamp = ift.AbsDeltaEnergyController(deltaE=0.1, iteration_limit=2)
ham = ift.StandardHamiltonian(lh, icsamp)
x_limits = [-8/scale, 8/scale]
x_limits_scaled = [-8, 8]
y_limits = [-4, 4]
x = np.linspace(*x_limits, num=401)
y = np.linspace(*y_limits, num=401)
xx, yy = np.meshgrid(x, y, indexing='ij')
def np_ham(x, y):
prior = x**2 + y**2
mean = x*scale
lcov = 1.35*2*y
lh = .5*(mean**2*np.exp(-lcov) + lcov)
return lh + prior
z = np.exp(-1*np_ham(xx, yy))
plt.plot(y, np.sum(z, axis=0))
plt.xlabel('y')
plt.ylabel('unnormalized pdf')
plt.title('Marginal density')
plt.pause(2.0)
plt.close()
plt.plot(x*scale, np.sum(z, axis=1))
plt.xlabel('x')
plt.ylabel('unnormalized pdf')
plt.title('Marginal density')
plt.pause(2.0)
plt.close()
pos = ift.from_random(ham.domain, 'normal')
MAP = ift.EnergyAdapter(pos, ham, want_metric=True)
minimizer = ift.NewtonCG(
ift.GradientNormController(iteration_limit=20, name='Mini'))
MAP, _ = minimizer(MAP)
map_xs, map_ys = [], []
for ii in range(10):
samp = (MAP.metric.draw_sample(from_inverse=True) + MAP.position).val
map_xs.append(samp['a'])
map_ys.append(samp['b'])
minimizer = ift.NewtonCG(
ift.GradientNormController(iteration_limit=2, name='Mini'))
pos = ift.from_random(ham.domain, 'normal')
plt.figure(figsize=[12, 8])
for ii in range(15):
if ii % 3 == 0:
mgkl = ift.MetricGaussianKL(pos, ham, 40)
plt.cla()
plt.imshow(z.T, origin='lower', norm=LogNorm(), vmin=1e-3,
vmax=np.max(z), cmap='gist_earth_r',
extent=x_limits_scaled + y_limits)
if ii == 0:
cbar = plt.colorbar()
cbar.ax.set_ylabel('pdf')
xs, ys = [], []
for samp in mgkl.samples:
samp = (samp + pos).val
xs.append(samp['a'])
ys.append(samp['b'])
plt.scatter(np.array(xs)*scale, np.array(ys), label='MGVI samples')
plt.scatter(pos.val['a']*scale, pos.val['b'], label='MGVI latent mean')
plt.scatter(np.array(map_xs)*scale, np.array(map_ys),
label='Laplace samples')
plt.scatter(MAP.position.val['a']*scale, MAP.position.val['b'],
label='Maximum a posterior solution')
plt.legend()
plt.draw()
plt.pause(1.0)
mgkl, _ = minimizer(mgkl)
pos = mgkl.position
ift.logger.info('Finished')
# Uncomment the following line in order to leave the plots open
# plt.show()
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2019 Max-Planck-Society
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
import numpy as np
import nifty7 as ift
......
rm -rf docs/build docs/source/mod
sphinx-apidoc -e -o docs/source/mod nifty7
EXCLUDE="nifty7/logger.py nifty7/git_version.py"
sphinx-apidoc -e -o docs/source/mod nifty7 ${EXCLUDE}
sphinx-build -b html docs/source/ docs/build/
......@@ -7,7 +7,7 @@ NIFTy-related publications
author={Arras, Philipp and Baltac, Mihai and Ensslin, Torsten A and Frank, Philipp and Hutschenreuter, Sebastian and Knollmueller, Jakob and Leike, Reimar and Newrzella, Max-Niklas and Platz, Lukas and Reinecke, Martin and others},
journal={Astrophysics Source Code Library},
year={2019}
}
}
@software{nifty,
author = {{Martin Reinecke, Theo Steininger, Marco Selig}},
......@@ -15,7 +15,7 @@ NIFTy-related publications
url = {https://gitlab.mpcdf.mpg.de/ift/NIFTy},
version = {nifty7},
date = {2018-04-05},
}
}
@article{2013A&A...554A..26S,
author = {{Selig}, M. and {Bell}, M.~R. and {Junklewitz}, H. and {Oppermann}, N. and {Reinecke}, M. and {Greiner}, M. and {Pachajoa}, C. and {En{\ss}lin}, T.~A.},
......@@ -33,7 +33,7 @@ NIFTy-related publications
doi = {10.1051/0004-6361/201321236},
adsurl = {http://cdsads.u-strasbg.fr/abs/2013A%26A...554A..26S},
adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}
}
@article{2017arXiv170801073S,
author = {{Steininger}, T. and {Dixit}, J. and {Frank}, P. and {Greiner}, M. and {Hutschenreuter}, S. and {Knollm{\"u}ller}, J. and {Leike}, R. and {Porqueres}, N. and {Pumpe}, D. and {Reinecke}, M. and {{\v S}raml}, M. and {Varady}, C. and {En{\ss}lin}, T.},
......@@ -47,4 +47,4 @@ NIFTy-related publications
month = aug,
adsurl = {http://cdsads.u-strasbg.fr/abs/2017arXiv170801073S},
adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}
}
......@@ -26,7 +26,7 @@ from .operators.diagonal_operator import DiagonalOperator
from .operators.distributors import DOFDistributor, PowerDistributor
from .operators.domain_tuple_field_inserter import DomainTupleFieldInserter
from .operators.einsum import LinearEinsum, MultiLinearEinsum
from .operators.contraction_operator import ContractionOperator
from .operators.contraction_operator import ContractionOperator, IntegrationOperator
from .operators.linear_interpolation import LinearInterpolator
from .operators.endomorphic_operator import EndomorphicOperator
from .operators.harmonic_operators import (
......@@ -44,7 +44,7 @@ from .operators.block_diagonal_operator import BlockDiagonalOperator
from .operators.outer_product_operator import OuterProduct
from .operators.simple_linear_operators import (
VdotOperator, ConjugationOperator, Realizer, FieldAdapter, ducktape,
GeometryRemover, NullOperator, PartialExtractor)
GeometryRemover, NullOperator, PartialExtractor, Imaginizer)
from .operators.matrix_product_operator import MatrixProductOperator
from .operators.value_inserter import ValueInserter
from .operators.energy_operators import (
......@@ -98,5 +98,7 @@ from .linearization import Linearization
from .operator_spectrum import operator_spectrum
from .operator_tree_optimiser import optimise_operator
# We deliberately don't set __all__ here, because we don't want people to do a
# "from nifty7 import *"; that would swamp the global namespace.
......@@ -42,8 +42,8 @@ def _adjoint_implementation(op, domain_dtype, target_dtype, atol, rtol,
needed_cap = op.TIMES | op.ADJOINT_TIMES
if (op.capability & needed_cap) != needed_cap:
return
f1 = from_random("normal", op.domain, dtype=domain_dtype)
f2 = from_random("normal", op.target, dtype=target_dtype)
f1 = from_random(op.domain, "normal", dtype=domain_dtype)
f2 = from_random(op.target, "normal", dtype=target_dtype)
res1 = f1.s_vdot(op.adjoint_times(f2))
res2 = op.times(f1).s_vdot(f2)
if only_r_linear:
......@@ -55,11 +55,11 @@ def _inverse_implementation(op, domain_dtype, target_dtype, atol, rtol):
needed_cap = op.TIMES | op.INVERSE_TIMES
if (op.capability & needed_cap) != needed_cap:
return
foo = from_random("normal", op.target, dtype=target_dtype)
foo = from_random(op.target, "normal", dtype=target_dtype)
res = op(op.inverse_times(foo))
assert_allclose(res, foo, atol=atol, rtol=rtol)
foo = from_random("normal", op.domain, dtype=domain_dtype)
foo = from_random(op.domain, "normal", dtype=domain_dtype)
res = op.inverse_times(op(foo))
assert_allclose(res, foo, atol=atol, rtol=rtol)
......@@ -75,8 +75,8 @@ def _check_linearity(op, domain_dtype, atol, rtol):
needed_cap = op.TIMES
if (op.capability & needed_cap) != needed_cap:
return
fld1 = from_random("normal", op.domain, dtype=domain_dtype)
fld2 = from_random("normal", op.domain, dtype=domain_dtype)
fld1 = from_random(op.domain, "normal", dtype=domain_dtype)
fld2 = from_random(op.domain, "normal", dtype=domain_dtype)
alpha = np.random.random() # FIXME: this can break badly with MPI!
val1 = op(alpha*fld1+fld2)
val2 = alpha*op(fld1)+op(fld2)
......@@ -88,7 +88,7 @@ def _actual_domain_check_linear(op, domain_dtype=None, inp=None):
if (op.capability & needed_cap) != needed_cap:
return
if domain_dtype is not None:
inp = from_random("normal", op.domain, dtype=domain_dtype)
inp = from_random(op.domain, "normal", dtype=domain_dtype)
elif inp is None:
raise ValueError('Need to specify either dtype or inp')
assert_(inp.domain is op.domain)
......@@ -219,7 +219,7 @@ def consistency_check(op, domain_dtype=np.float64, target_dtype=np.float64,
def _get_acceptable_location(op, loc, lin):
if not np.isfinite(lin.val.s_sum()):
raise ValueError('Initial value must be finite')
dir = from_random("normal", loc.domain)
dir = from_random(loc.domain, "normal")
dirder = lin.jac(dir)
if dirder.norm() == 0:
dir = dir * (lin.val.norm()*1e-5)
......
......@@ -124,7 +124,7 @@ class Field(Operator):
return Field(DomainTuple.make(new_domain), self._val)
@staticmethod
def from_random(random_type, domain, dtype=np.float64, **kwargs):
def from_random(domain, random_type='normal', dtype=np.float64, **kwargs):
"""Draws a random field with the given parameters.
Parameters
......@@ -283,7 +283,7 @@ class Field(Operator):
raise TypeError("The multiplier must be an instance of " +
"the Field class")
from .operators.outer_product_operator import OuterProduct
return OuterProduct(self, x.domain)(x)
return OuterProduct(x.domain, self)(x)
def vdot(self, x, spaces=None):
"""Computes the dot product of 'self' with x.
......@@ -709,6 +709,7 @@ class Field(Operator):
tmp = ptw_dict[op][1](self._val, *argstmp, **kwargstmp)
return (Field(self._domain, tmp[0]), Field(self._domain, tmp[1]))
for op in ["__add__", "__radd__",
"__sub__", "__rsub__",
"__mul__", "__rmul__",
......
......@@ -524,7 +524,7 @@ class CorrelatedFieldMaker:
for kk, op in lst:
sc = StatCalculator()
for _ in range(prior_info):
sc.add(op(from_random('normal', op.domain)))
sc.add(op(from_random(op.domain, 'normal')))
mean = sc.mean.val
stddev = sc.var.ptw("sqrt").val
for m, s in zip(mean.flatten(), stddev.flatten()):
......@@ -539,7 +539,7 @@ class CorrelatedFieldMaker:
scm = 1.
for a in self._a:
op = a.fluctuation_amplitude*self._azm.ptw("reciprocal")
res = np.array([op(from_random('normal', op.domain)).val
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))
......
......@@ -11,7 +11,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2013-2019 Max-Planck-Society
# Copyright(C) 2013-2020 Max-Planck-Society
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
......@@ -133,6 +133,10 @@ class Linearization(Operator):
def real(self):
return self.new(self._val.real, self._jac.real)
@property
def imag(self):
return self.new(self._val.imag, self._jac.imag)
def _myadd(self, other, neg):
if np.isscalar(other) or other.jac is None:
return self.new(self._val-other if neg else self._val+other,
......@@ -207,12 +211,13 @@ class Linearization(Operator):
return self.__mul__(other)
from .operators.outer_product_operator import OuterProduct
if other.jac is None:
return self.new(OuterProduct(self._val, other.domain)(other),
OuterProduct(self._jac(self._val), other.domain))
return self.new(OuterProduct(other.domain, self._val)(other),
OuterProduct(other.domain, self._jac(self._val)))
tmp_op = OuterProduct(other.target, self._val)
return self.new(
OuterProduct(self._val, other.target)(other._val),
OuterProduct(self._jac(self._val), other.target)._myadd(
OuterProduct(self._val, other.target)(other._jac), False))
tmp_op(other._val),
OuterProduct(other.target, self._jac(self._val))._myadd(
tmp_op(other._jac), False))
def vdot(self, other):
"""Computes the inner product of this Linearization with a Field or
......@@ -270,10 +275,8 @@ class Linearization(Operator):
Linearization
the (partial) integral
"""
from .operators.contraction_operator import ContractionOperator
return self.new(
self._val.integrate(spaces),
ContractionOperator(self._jac.target, spaces, 1)(self._jac))
from .operators.contraction_operator import IntegrationOperator
return IntegrationOperator(self._target, spaces)(self)
def ptw(self, op, *args, **kwargs):
t1, t2 = self._val.ptw_with_deriv(op, *args, **kwargs)
......
......@@ -166,7 +166,8 @@ class NewtonCG(DescentMinimizer):
"""
def __init__(self, controller, napprox=0, line_searcher=None, name=None,
nreset=20, max_cg_iterations=200, energy_reduction_factor=0.1):
nreset=20, max_cg_iterations=200, energy_reduction_factor=0.1,
activate_logging=False):
if line_searcher is None:
line_searcher = LineSearch(preferred_initial_step_size=1.)
super(NewtonCG, self).__init__(controller=controller,
......@@ -176,6 +177,8 @@ class NewtonCG(DescentMinimizer):
self._nreset = nreset
self._max_cg_iterations = max_cg_iterations
self._alpha = energy_reduction_factor
from .iteration_controllers import EnergyHistory
self._history = EnergyHistory() if activate_logging else None
def get_descent_direction(self, energy, old_value=None):
if old_value is None:
......@@ -184,14 +187,22 @@ class NewtonCG(DescentMinimizer):
ediff = self._alpha*(old_value-energy.value)
ic = AbsDeltaEnergyController(
ediff, iteration_limit=self._max_cg_iterations, name=self._name)
if self._history is not None:
ic.enable_logging()
e = QuadraticEnergy(0*energy.position, energy.metric, energy.gradient)
p = None
if self._napprox > 1:
met = energy.metric
p = makeOp(approximation2endo(met, self._napprox)).inverse
e, conv = ConjugateGradient(ic, nreset=self._nreset)(e, p)
if self._history is not None:
self._history += ic.history
return -e.position
@property
def inversion_history(self):
return self._history
class L_BFGS(DescentMinimizer):
def __init__(self, controller, line_searcher=LineSearch(),
......
......@@ -11,10 +11,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2013-2019 Max-Planck-Society
# Copyright(C) 2013-2020 Max-Planck-Society
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
import functools
from time import time
import numpy as np
from ..logger import logger
......@@ -37,10 +40,17 @@ class IterationController(metaclass=NiftyMeta):
class; the implementer has full flexibility to use whichever criteria are
appropriate for a particular problem - as long as they can be computed from
the information passed to the controller during the iteration process.
For analyzing minimization procedures IterationControllers can log energy
values together with the respective time stamps. In order to activate this
feature `activate_logging()` needs to be called.
"""
CONVERGED, CONTINUE, ERROR = list(range(3))
def __init__(self):
self._history = None
def start(self, energy):
"""Starts the iteration.
......@@ -69,6 +79,68 @@ class IterationController(metaclass=NiftyMeta):
"""
raise NotImplementedError
def enable_logging(self):
"""Enables the logging functionality. If the log has been populated
before, it stays as it is."""
if self._history is None:
self._history = EnergyHistory()
def disable_logging(self):
"""Disables the logging functionality. If the log has been populated
before, it is dropped."""
self._history = None
@property
def history(self):
return self._history
class EnergyHistory(object):
def __init__(self):