# coding: utf-8
"""
Definition of a data-taking campaign and the connection of its information to an analysis within a
config.
"""
__all__ = ["Campaign", "Config"]
from order.unique import UniqueObject, UniqueObjectIndex, unique_tree
from order.mixins import CopyMixin, AuxDataMixin, TagMixin
from order.shift import Shift
from order.dataset import Dataset
from order.process import Process
from order.category import Channel, Category
from order.variable import Variable
from order.util import typed
[docs]@unique_tree(cls=Dataset, parents=False)
class Campaign(UniqueObject, CopyMixin, AuxDataMixin, TagMixin):
"""
Class that provides data that is subject to a campaign, i.e., a well-defined range of
data-taking, detector alignment, MC production settings, datasets, etc. Common, generic
information is available via dedicated attributes, specialized data can be stored as auxiliary
data.
**Arguments**
*ecm* is the center-of-mass energy, *bx* the bunch-crossing. *tags* are forwarded to the
:py:class:`~order.mixins.TagMixin`, *aux* to the :py:class:`~order.mixins.AuxDataMixin`, *name*
and *id* to the :py:class:`~order.unique.UniqueObject` constructor.
**Copy behavior**
``copy()``
All attributes are copied.
``copy_shallow()``
All attributs are copied except for contained :py:attr:`datasets` which are set to a default
value instead.
**Example**
.. code-block:: python
import order as od
c = od.Campaign(
name="2017B",
id=1,
ecm=13,
bx=25,
)
d = c.add_dataset("ttH", 1)
d in c.datasets
# -> True
d.campaign == c
# -> True
**Members**
.. py:attribute:: ecm
type: float
The center-of-mass energy in arbitrary units.
.. py:attribute:: bx
type: float
The bunch crossing in arbitrary units.
"""
cls_name_singular = "campaign"
cls_name_plural = "campaigns"
copy_specs = (
[
{
"attr": "_datasets",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Dataset)),
},
] +
UniqueObject.copy_specs +
AuxDataMixin.copy_specs +
TagMixin.copy_specs
)
def __init__(self, name, id, ecm=None, bx=None, datasets=None, tags=None, aux=None):
UniqueObject.__init__(self, name, id)
AuxDataMixin.__init__(self, aux=aux)
TagMixin.__init__(self, tags=tags)
# instance members
self._ecm = None
self._bx = None
# set initial values
if ecm is not None:
self.ecm = ecm
if bx is not None:
self.bx = bx
if datasets is not None:
self.extend_datasets(datasets)
@typed
def ecm(self, ecm):
# ecm parser
if not isinstance(ecm, (int, float)):
raise TypeError("invalid ecm type: {}".format(ecm))
return float(ecm)
@typed
def bx(self, bx):
# bx parser
if not isinstance(bx, (int, float)):
raise TypeError("invalid bx type: {}".format(bx))
return float(bx)
[docs] def add_dataset(self, *args, **kwargs):
"""
Adds a child dataset to the :py:attr:`datasets` index and returns it. See
:py:meth:`UniqueObjectIndex.add` for more info. Also sets the *campaign* of the added
dataset to *this* instance.
"""
dataset = self.datasets.add(*args, **kwargs)
# update the dataset's campaign
dataset.campaign = None
dataset._campaign = self
return dataset
[docs] def remove_dataset(self, *args, **kwargs):
"""
Removes a child dataset from the :py:attr:`datasets` index and returns the removed object.
See :py:meth:`UniqueObjectIndex.remove` for more info. Also resets the *campaign* of the
added dataset.
"""
dataset = self.datasets.remove(*args, **kwargs)
# reset the dataset's campaign
if dataset:
dataset._campaign = None
return dataset
[docs]@unique_tree(cls=Dataset, parents=False)
@unique_tree(cls=Process, parents=False, deep_children=True)
@unique_tree(cls=Channel, parents=False, deep_children=True)
@unique_tree(cls=Category, parents=False, deep_children=True)
@unique_tree(cls=Variable, parents=False)
@unique_tree(cls=Shift, parents=False)
class Config(UniqueObject, CopyMixin, AuxDataMixin, TagMixin):
"""
Class holding analysis information that is related to a :py:class:`Campaign` instance. Most of
the analysis configuration happens here.
Besides references to the :py:class:`~order.analysis.Analysis` and :py:class:`Campaign`
instances it belongs to, it stores analysis *datasets*, *processes*, *channels*, *categories*,
*variables*, and *shifts* in :py:class:`~order.unique.UniqueObjectIndex` instances.
**Arguments**
*datasets*, *processes*, *channels*, *categories*, *variables*, and *shifts* are initialized
from constructor arguments. *name* and *id* are forwarded to the
:py:class:`~order.unique.UniqueObject` constructor. *name* and *id* default to the values of the
*campaign* instance. Specialized data such as integrated luminosities, triggers, etc, can be
stored as auxiliary data *aux*, which are forwarded to the
:py:class:`~order.mixins.AuxDataMixin`. *tags* are forwarded to the
:py:class:`~order.mixins.TagMixin`.
**Copy behavior**
``copy()``
The :py:attr:`campaign` and :py:attr:`analysis` attributes are carried over as references, all
remaining attributes are copied. Note that the copied config is also registered in the analysis.
``copy_shallow()``
All attributs are copied except for the :py:attr:`analysis` and :py:attr:`campaign`, as well as
contained :py:attr:`datasets`, :py:attr:`processes`, :py:attr:`channels`, :py:attr:`variables`
and :py:attr:`shifts` which are set to default values instead.
**Example**
.. code-block:: python
import order as od
analysis = od.Analysis("ttH", 1)
campaign = od.Campaign("data_taking_2018", 1)
# add the campaign to the analysis, which returns a new config
cfg = analysis.add_config(campaign)
cfg.name, cfg.id
# -> "data_taking_2019", 1
# start configuration
cfg.add_dataset(campaign.get_dataset("ttH_bb"))
cfg.add_process("ttH_bb", 1, xsecs={13: 0.5071})
bb = cfg.add_channel("bb", 1)
bb.add_category("eq6j_eq4b")
cfg.add_variable("jet1_px", expression="jet1_pt * cos(jet1_phi)")
cfg.add_shift("pdf_up", type=Shift.SHAPE)
...
# at some point you might want to create a second config
# with other values for that campaign, e.g. for sub-measurements
cfg2 = analysis.add_config(campaign, name="sf_meausurement", id=2)
...
**Members**
.. py:attribute:: campaign
type: :py:class:`Campaign` (read-only)
The :py:class:`Campaign` instance this config belongs to.
.. py:attribute:: analysis
type: :py:class:`Analysis` (read-only)
The :py:class:`~order.analysis.Analysis` instance this config belongs to. When set, *this*
config is added to the index of configs of the analysis object.
"""
cls_name_singular = "config"
cls_name_plural = "configs"
copy_specs = (
[
{
"attr": "_campaign",
"ref": True,
"skip_shallow": True,
},
{
"attr": "_analysis",
"ref": True,
"skip_shallow": True,
},
{
"attr": "_datasets",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Dataset)),
},
{
"attr": "_processes",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Process)),
},
{
"attr": "_channels",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Channel)),
},
{
"attr": "_categories",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Category)),
},
{
"attr": "_variables",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Variable)),
},
{
"attr": "_shifts",
"skip_shallow": True,
"skip_value": CopyMixin.Deferred(lambda inst: UniqueObjectIndex(cls=Shift)),
},
] +
UniqueObject.copy_specs +
AuxDataMixin.copy_specs +
TagMixin.copy_specs
)
def __init__(
self,
campaign=None,
name=None,
id=None,
analysis=None,
datasets=None,
processes=None,
channels=None,
categories=None,
variables=None,
shifts=None,
tags=None,
aux=None,
):
# instance members
self._campaign = None
self._analysis = None
# if name or id are None, campaign must be set
# use the campaign setter for type validation first
self.campaign = campaign
if name is None:
if self.campaign is None:
raise ValueError("a name must be set when campaign is missing")
name = self.campaign.name
if id is None:
if self.campaign is None:
raise ValueError("an id must be set when campaign is missing")
id = self.campaign.id
UniqueObject.__init__(self, name=name, id=id)
AuxDataMixin.__init__(self, aux=aux)
TagMixin.__init__(self, tags=tags)
# set initial values
if analysis is not None:
self.analysis = analysis
if datasets is not None:
self.extend_datasets(datasets)
if processes is not None:
self.extend_processes(processes)
if channels is not None:
self.extend_channels(channels)
if categories is not None:
self.extend_categories(categories)
if variables is not None:
self.extend_variables(variables)
if shifts is not None:
self.extend_shifts(shifts)
[docs] def copy(self, *args, **kwargs):
inst = super(Config, self).copy(*args, **kwargs)
# register in the analysis
if inst.analysis:
inst.analysis.configs.add(inst)
return inst
@typed
def campaign(self, campaign):
# campaign parser
if not isinstance(campaign, Campaign) and campaign is not None:
raise TypeError("invalid campaign type: {}".format(campaign))
return campaign
@property
def analysis(self):
# analysis getter
return self._analysis
@analysis.setter
def analysis(self, analysis):
# analysis setter
if analysis is not None and not isinstance(analysis, Analysis):
raise TypeError("invalid analysis type: {}".format(analysis))
# remove this config from the current analysis' config index
if self._analysis:
self._analysis.configs.remove(self)
# add this config to the analysis' config index
if analysis:
analysis.configs.add(self)
self._analysis = analysis
# prevent circular imports
from order.analysis import Analysis