# Source code for stk.ea.fitness_normalizers.shift_up

```"""
Shift Up
========

"""

from functools import partial

import numpy as np

from .fitness_normalizer import FitnessNormalizer

[docs]class ShiftUp(FitnessNormalizer):
"""
Shifts negative fitness values to be positive.

Assume you have a vector-valued fitness value, where each number
represents a different property of the molecule, for example,
``[1, -10, 1]``

One way to convert the vector-valued fitness value into a
scalar fitness value is by summing the elements, and the result in
this case would be ``-8``. Clearly this doesn't work, because the
resulting fitness value is not a positive number. To fix this,
the ``-10`` should be shifted to a positive value.

:class:`.ShiftUp` finds the minimum value of each element in the
vector-valued fitness value across the entire population, and for
elements where this minimum value is less than ``0``, shifts up
the element value for every molecule in the population, so that the
minimum value in the entire population is ``1``.

For example, take a population with the vector-valued fitness
values

.. code-block:: python

[1, -5, 5]
[3, -10, 2]
[2, 20, 1]

After normalization the fitness values will be.

.. code-block:: python

[1, 6, 5]
[3, 1, 2]
[2, 31, 1]

:class:`.ShiftUp` also works when the fitness value is a
single value.

Examples
--------
*Ensuring Positive Fitness Values*

Here you final fitness value is calculated by taking a
:class:`.Sum` of the different components of the fitness value.
To ensure that the final sum is positive, each component must
also be positive.

.. testcode:: ensuring-positive-fitness-values

import stk
import numpy as np

building_block = stk.BuildingBlock(
smiles='BrCCBr',
functional_groups=[stk.BromoFactory()],
)

population = (
stk.MoleculeRecord(
topology_graph=stk.polymer.Linear(
building_blocks=(building_block, ),
repeating_unit='A',
num_repeating_units=2,
),
).with_fitness_value(
fitness_value=(1, -2, 3),
normalized=False,
),
stk.MoleculeRecord(
topology_graph=stk.polymer.Linear(
building_blocks=(building_block, ),
repeating_unit='A',
num_repeating_units=2,
),
).with_fitness_value(
fitness_value=(4, 5, -6),
normalized=False,
),
)

# Create the normalizer.
shifter = stk.ShiftUp()

normalized_population = tuple(shifter.normalize(population))
normalized_record1, normalized_record2 = normalized_population
assert np.all(np.equal(
normalized_record1.get_fitness_value(),
(1, 1, 10),
))
assert np.all(np.equal(
normalized_record2.get_fitness_value(),
(4, 8, 1),
))

*Selectively Normalizing Fitness Values*

Sometimes, you only want to normalize some members of a population,
for example if some do not have an assigned fitness value,
because the fitness calculation failed for whatever reason.
You can use the `filter` parameter to exclude records from the
normalization

.. testcode:: selectively-normalizing-fitness-values

import stk
import numpy as np

building_block = stk.BuildingBlock(
smiles='BrCCBr',
functional_groups=[stk.BromoFactory()],
)

population = (
stk.MoleculeRecord(
topology_graph=stk.polymer.Linear(
building_blocks=(building_block, ),
repeating_unit='A',
num_repeating_units=2,
),
).with_fitness_value(
fitness_value=(1, -2, 3),
normalized=False,
),
# This will have a fitness value of None.
stk.MoleculeRecord(
topology_graph=stk.polymer.Linear(
building_blocks=(building_block, ),
repeating_unit='A',
num_repeating_units=2,
),
),
)

normalizer = stk.ShiftUp(
# Only normalize values which are not None.
filter=lambda population, record:
record.get_fitness_value() is not None,
)
normalized_population = tuple(normalizer.normalize(population))
normalized_record1, normalized_record2 = normalized_population
assert np.all(np.equal(
normalized_record1.get_fitness_value(),
(1, 1, 3),
))
assert normalized_record2.get_fitness_value() is None

"""

[docs]    def __init__(self, filter=lambda population, record: True):
"""
Initialize a :class:`.ShiftUp` instance.

Parameters
----------
filter : :class:`callable`, optional
Takes two parameters, first is a :class:`tuple`
of :class:`.MoleculeRecord` instances,
and the second is a :class:`.MoleculeRecord`. The
:class:`callable` returns ``True`` or ``False``. Only
molecules which return ``True`` will have fitness values
normalized. By default, all molecules will have fitness
values normalized.
The instance passed to the `population` argument of
:meth:`.normalize` is passed as the first argument, while
the second argument will be passed every
:class:`.MoleculeRecord` in it, one at a time.

"""

self._filter = filter

[docs]    def normalize(self, population):
filtered = filter(
partial(self._filter, population),
population,
)
# Get all the fitness arrays in a matrix.
fmat = np.array([
record.get_fitness_value() for record in filtered
])

# Get the minimum value of each element across the population.
# keepdims ensures that np.min returns a 1-D array, because
# it will be True if fitness values are scalar and False if
# they are array-valued.
mins = np.min(fmat, axis=0, keepdims=len(fmat.shape) == 1)

# Convert all elements in mins which are not to be shifted to 0
# and make the shift equal to the minimum value + 1.
shift = np.zeros(len(mins))
for i, min_ in enumerate(mins):
if min_ <= 0:
shift[i] = 1 - min_

for record in population:
if self._filter(population, record):
yield record.with_fitness_value(
fitness_value=record.get_fitness_value() + shift,
)
else:
yield record
```