#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# ade:
# Asynchronous Differential Evolution.
#
# Copyright (C) 2018-19 by Edwin A. Suominen,
# http://edsuom.com/ade
#
# See edsuom.com for API documentation as well as information about
# Ed's background and other projects, software and otherwise.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an "AS
# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""
An L{Individual} class for parameter combinations to be evaluated.
You won't need to construct individuals directly. Just let
L{Population} set up a population full of them, and have
L{DifferentialEvolution} create challengers as it does its thing.
"""
import random, pickle, time, struct
import numpy as np
from twisted.internet import defer, task
from .util import sub, msg
class Individual(object):
"""
I act like a sequence of parameter values, but with other stuff
like an SSE value, too.
Construct me with a L{Population} object. You can set my values
with a 1-D Numpy array of initial I{values}.
You can iterate my values in sequence. You can access them
(read/write) as items. You can even replace the whole 1-D Numpy
array of them at once with another array of like dimensions,
although the safest way to do that is supplying a list or 1-D
array to L{update}.
@ivar values: A 1-D Numpy array of parameter values.
@ivar p: The L{Population} I am part of.
@ivar dt: The time difference between start and end of my last
evaluation.
@keyword values: Set to a sequence of initial parameter values if
you're not going to set them with a call to L{update}.
"""
__slots__ = ['values', '_SSE', 'p', 'dt']
def __init__(self, p, values=None):
"""Individual(p, values=None)"""
self.p = p
if values is None:
self.values = np.empty(p.Nd)
else: self.update(values)
self.SSE = None
self.dt = None
def __getstate__(self):
"""
For pickling. Does not include the L{Population} object I{p}.
"""
state = {}
for name in {'values', '_SSE', 'dt'}:
state[name] = getattr(self, name)
return state
def __setstate__(self, state):
"""
For unpickling. You have to set the I{p} attribute of the
unpickled version of me to a L{Population} object.
"""
for name in state:
setattr(self, name, state[name])
def __repr__(self):
"""
Informative string representation, with SSE and pretty-printed values.
"""
prelude = "" if self.SSE is None else sub(
"SSE={:.4f}", float(self.SSE))
return sub("<{}>", self.p.pm.prettyValues(self.values, prelude))
@property
def params(self):
"""
Property: A dict of my parameter values, keyed by name.
"""
pd = {}
for k, value in enumerate(self):
name = self.p.pm.names[k]
pd[name] = value
return pd
@property
def SSE(self):
"""
Property: My SSE value, which must at least behave like a
float. Infinite if no SSE computed yet, a fatal error occurred
during evaluation, or was set to C{None}.
"""
if self._SSE is None or self._SSE < 0:
return float('+inf')
return self._SSE #float(self._SSE)
@SSE.setter
def SSE(self, x):
"""
Property setter: Sets my SSE value.
"""
self._SSE = x
def spawn(self, values):
"""
Returns another instance of my class with the same population and
with the supplied I{values}.
"""
return Individual(self.p, values)
def copy(self):
"""
Returns a complete copy of me, another instance of my class with
the same population, values, and SSE.
"""
i = Individual(self.p, list(self.values))
i.SSE = self.SSE
return i
def __float__(self):
"""
I can be evaluated as a float using my SSE. A negative SSE
translates to C{inf} because it indicates a fatal error.
"""
return float('+inf' if self.SSE < 0 else self.SSE)
def __getitem__(self, k):
"""
Sequence-like read access to values when I{k} is an integer,
dict-like access otherwise.
"""
if not isinstance(k, int):
k = self.p.pm.names.index(k)
return self.values[k]
def __setitem__(self, k, value):
"""
Sequence-like write access to values.
"""
self.values[k] = value
def __iter__(self):
"""
Sequence-like iteration over values.
"""
for value in self.values:
yield value
def __bool__(self):
"""
I am C{True} if there were no fatal errors during my last
evaluation, which would be indicated by an evaluation SSE
result of less than zero.
I will evaluate as C{True} even if my SSE is C{None},
infinite, or C{NaN}, so long as it is not negative.
"""
if self._SSE is None: return True
SSE = float(self._SSE)
if np.isnan(SSE): return True
return SSE >= 0
def __hash__(self):
return hash(
struct.pack('<f', self.SSE) + np.array(self.values).tobytes())
def __eq__(self, other):
"""
I am equal to another C{Individual} if we have the same SSE and
values.
"""
return (self.SSE == other.SSE) and self.equals(other)
def equals(self, other):
"""
Returns C{True} if my values equal the I{other} individual's
values, or the other values themselves if supplied as a
sequence or 1-D array.
"""
if hasattr(other, 'values'):
other = other.values
if not hasattr(other, 'shape'):
other = np.array(other)
return np.all(self.values == other)
def __lt__(self, other):
"""
My SSE less than other C{Individual} or float?
"""
return float(self) < float(other)
def __gt__(self, other):
"""
My SSE greater than other C{Individual} or float?
"""
return float(self) > float(other)
def __sub__(self, other):
"""
Subtract the other C{Individual}'s values from mine and return a
new C{Individual} whose values are the differences.
"""
return self.spawn(self.values - other.values)
def __add__(self, other):
"""
Add the other C{Individual}'s values to mine and return a new
C{Individual} whose values are the sums.
"""
return self.spawn(self.values + other.values)
def __mul__(self, F):
"""
Scales each of my values by the constant I{F}.
"""
return self.spawn(self.values * F)
def blacklist(self):
"""
Sets my SSE to the worst possible value and forces my population
to update its sorting to account for my degraded status.
"""
self.SSE = float('+inf')
del self.p.KS
def update(self, values):
"""
Updates my I{values} as an array form of the supplied list or
tuple.
Raises an exception if there's a different number of values
than my values length I{Nd}.
"""
if len(values) != self.p.Nd:
raise ValueError(sub(
"Expected {:d} values, not {:d}", self.p.Nd, len(values)))
if isinstance(values, (list, tuple)):
values = np.array(values)
self.values = values
def evaluate(self, xSSE=None):
"""
Computes the sum of squared errors (SSE) from my evaluation
function using my current I{values}.
Stores the result in my I{SSE} attribute and returns a
reference to me for convenience.
If the SSE value is less than zero, or results in a Twisted
failure, I{ade} will abort operations. Use this feature to
provide your evaluator with a simple way to stop everything if
something goes terribly wrong.
Updates my I{dt} attribute with the elapsed time for this
evaluation.
Returns a C{Deferred} that fires with a reference to my
instance when the evaluation is done, pass or fail.
"""
def done(SSE):
self.dt = time.time() - t0
self.p.counter += 1
self.SSE = SSE
return self
def failed(failureObj):
info = failureObj.getTraceback()
msg(0, "FATAL ERROR in evaluation:\n{}\n{}\n", '-'*40, info)
self.SSE = -1
return self
t0 = time.time()
if xSSE is None:
d = self.p.evalFunc(self.values)
else: d = self.p.evalFunc(self.values, xSSE=xSSE)
d.addCallbacks(done, failed)
return d
def save(self, filePath):
"""
Saves my parameters to I{filePath} as a pickled dict.
"""
with open(filePath, 'wb') as fh:
pickle.dump(self.params, fh)