# %% IMPORTS
# Built-in imports
import logging as log
# Package imports
import astropy.units as u
# IMAGINE imports
from imagine.fields.grid import BaseGrid, UniformGrid
from imagine.fields.field import Field
from imagine.priors import Prior
from imagine.tools import BaseClass, unity_mapper
# All declaration
__all__ = ['FieldFactory']
# %% CLASS DEFINITIONS
[docs]class FieldFactory(BaseClass):
"""
The `FieldFactory` object stores all the required information for the
:py:obj:`Pipeline <imagine.pipelines.pipeline.Pipeline>` to generate
multiple realisations of a given :py:obj:`Field <imagine.fields.field.Field>`
while sampling the parameter space.
To produce a `FieldFactory` one needs to supply the chosen
:py:class:`Field <imagine.fields.field.Field>` subclass, a list of active
parameters, the default values for the parameters (at least for the
inactive ones), the a dictionary of :py:obj:`Prior <imagine.priors.prior.Prior>`
objects, containing (at least) all the active parameters, and (if the `Field`
is not a *dummy*) a :py:obj:`Grid <imagine.fields.grid.BaseGrid>` object,
with the grid where the instances should be evaluated.
Parameters
----------
field_class : imagine.fields.Field
The field class based on which one wants to create a field factory.
A Field *object* can also be supplied.
active_parameters : tuple
List of parameter to be varied/constrained
default_parameters : dict
Dictionary containing the default values of the inactive parameters
priors : dict
Dictionary containing parameter names as keys and
:py:obj:`Prior <imagine.priors.prior.Prior>` objects as values, for
all the active parameters.
grid : imagine.fields.grid.Grid
:py:obj:`Grid <imagine.fields.grid.Grid>` object that will be used
by the fields generated by this field factory
field_kwargs : dict
Initialization keyword arguments to be passed to the fields produced
by this factory.
boxsize : list/tuple of floats
The physical size of simulation box (i.e. edges of the box).
resolution : list/tuple of ints
The discretization size in corresponding dimension
"""
def __init__(self, field_class=None, active_parameters=(),
default_parameters={}, priors={}, grid=None, boxsize=None,
resolution=None, field_kwargs={}):
log.debug('@ field_factory::__init__')
if hasattr(self, 'FIELD_CLASS'):
assert field_class==None, 'For subclasses with an associated FIELD_CLASS attribute, no field_class argument should be supplied'
field_class = self.FIELD_CLASS
default_parameters = self.DEFAULT_PARAMETERS
priors = self.PRIORS
# Call super constructor
super().__init__()
# If an object is used, take its parameters as default parameters
# and save the class (this is very convenient sometimes)
if isinstance(field_class, Field):
parameters = field_class.parameters.copy()
# Uses the original object's field, if none is provided
if grid is None:
grid = field_class.grid
parameters.update(default_parameters)
default_parameters = parameters
field_class = type(field_class)
assert issubclass(field_class, Field), 'Need to supply IMAGINE Field class!'
self.parameter_ranges = {}
self._field_class = field_class
self.active_parameters = active_parameters
self.default_parameters = default_parameters
self.priors = priors
if self.field_type == 'dummy':
# In dummy fields, we do not use a grid
self._grid = None
self._boxsize = None
self._resolution = None
else:
# Uses user defined grid if `grid` is present
if grid is not None:
assert isinstance(grid, BaseGrid)
self._grid = grid
# Otherwise, assumes a regular Cartesian grid
# Which is generated when the property is first called
else:
self._grid = None
self._boxsize = boxsize
self._resolution = resolution
self.field_kwargs = field_kwargs
[docs] def __call__(self, *, variables={}, ensemble_size=None,
ensemble_seeds=None):
"""
Takes an active variable dictionary, an ensemble size and a random
seed value, translates the active variables to parameter values
(updating the default parameter dictionary accordingly) and send this
to an instance of the field class.
Parameters
----------
variables : dict
Dictionary of variables with name and value
ensemble_size : int
Number of instances in a field ensemble
ensemble_seeds
seeds for generating random numbers
in realising instances in field ensemble
if ensemble_seeds is None,
field_class initialization will take all seed as 0
Returns
-------
result_field : imagine.fields.field.Field
a Field object
"""
log.debug('@ field_factory::generate')
# map variable value to parameter value
# copy default parameters and update wrt argument
work_parameters = dict(self.default_parameters)
# update is safe
work_parameters.update(variables)
# generate fields
result_field = self.field_class(grid=self.grid,
parameters=work_parameters,
ensemble_size=ensemble_size,
ensemble_seeds=ensemble_seeds,
**self.field_kwargs)
log.debug('generated field with work-parameters %s' % work_parameters)
return result_field
@property
def field_class(self):
"""Python class whose instances are produced by the present factory"""
return self._field_class
@property
def field_name(self):
"""Name of the physical field"""
return self.field_class.NAME
@property
def name(self):
# For backwards-compatibility only
return self.field_name
@property
def field_type(self):
"""Type of physical field."""
return self.field_class.TYPE
@property
def field_units(self):
"""Units of physical field."""
return self.field_class.UNITS
@property
def grid(self):
"""
Instance of `imagine.fields.BaseGrid` containing a 3D grid where the
field is/was evaluated
"""
if self._grid is None:
if (self._boxsize is not None) and (self._resolution is not None):
self._grid = UniformGrid(box=self._boxsize,
resolution=self._resolution)
elif self.field_type != 'dummy':
raise ValueError('Non-dummy fields must be initialized with'
'either a valid Grid object or its properties'
'(box and resolution).')
return self._grid
@property
def resolution(self):
"""
How many bins on each direction of simulation box
"""
return self._resolution
@property
def default_parameters(self):
"""
Dictionary storing parameter name as entry, default parameter value
as content
"""
return self._default_parameters
@default_parameters.setter
def default_parameters(self, new_defaults):
assert isinstance(new_defaults, dict)
new_defaults = {key: u.Quantity(value)
for (key, value) in new_defaults.items()}
try:
self._default_parameters.update(new_defaults)
log.debug('update default parameters %s' % str(new_defaults))
except AttributeError:
self._default_parameters = new_defaults
log.debug('set default parameters %s' % str(new_defaults))
@property
def priors(self):
"""
A dictionary containing the priors associated with each parameter.
Each prior is represented by an instance of
:py:class:`imagine.priors.prior.Prior`.
To set new priors one can update the priors dictionary using
attribution (any missing values will be set to
:py:class:`imagine.priors.basic_priors.FlatPrior`).
"""
return self._priors
@priors.setter
def priors(self, new_prior_dict):
if not hasattr(self, '_priors'):
self._priors = {}
parameter_ranges = {}
# Uses previous information
prior_dict = self._priors.copy()
prior_dict.update(new_prior_dict)
for name in self.active_parameters:
assert (name in prior_dict), 'Missing Prior for '+name
for name in prior_dict:
prior = prior_dict[name]
assert isinstance(prior, Prior), 'Prior must be an instance of :py:class:`imagine.priors.prior.GeneralPrior`.'
self._priors[name] = prior
parameter_ranges[name] = prior.range
self.parameter_ranges = parameter_ranges
@property
def parameter_ranges(self):
"""
Dictionary storing varying range of all default parameters in
the form {'parameter-name': (min, max)}
"""
return self._parameter_ranges
@parameter_ranges.setter
def parameter_ranges(self, new_ranges):
assert isinstance(new_ranges, dict)
for k, v in new_ranges.items():
assert (len(v) == 2)
try:
self._parameter_ranges.update(new_ranges)
log.debug('update parameter ranges %s' % str(new_ranges))
except AttributeError:
self._parameter_ranges = new_ranges
log.debug('set parameter ranges %s' % str(new_ranges))
@staticmethod
def _interval(mean, sigma, n):
return mean-n*sigma, mean+n*sigma
@staticmethod
def _positive_interval(mean, sigma, n):
return max(0, mean-n*sigma), mean+n*sigma