Getting started

Installation

pyplotit is a pure python package, so the latest version can be installed with

pip install git+https://gitlab.cern.ch/cp3-cms/pyplotit.git

or, for an editable install when frequent updates and/or testing of changes is expected, with

git clone https://gitlab.cern.ch/cp3-cms/pyplotit.git
pip install -e ./pyplotit

Example: loading histograms from a plotIt configuration

If you do not have a plotIt configuration and the corresponding ROOT files around, you can use the following commands to generate an example; they are also used here for the rest of the example

!wget -q https://gitlab.cern.ch/cp3-cms/pyplotit/-/raw/master/tests/data/ex1_syst.yml
!wget -q https://raw.githubusercontent.com/cp3-llbb/plotIt/master/test/generate_files.C
!mkdir -p files
!root -l -b -q generate_files.C
Hide code cell output
Processing generate_files.C...

We can load the configuration file ex1_syst.yml in pyplotit as follows:

import plotit
config, samples, plots, systematics, legend = plotit.loadFromYAML("ex1_syst.yml")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 1
----> 1 import plotit
      2 config, samples, plots, systematics, legend = plotit.loadFromYAML("ex1_syst.yml")

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/site-packages/plotit/__init__.py:1
----> 1 from .plotit import loadFromYAML
      2 from .version import version as __version__
      4 __all__ = ("__version__", "loadFromYAML")

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/site-packages/plotit/plotit.py:33
     29 import numpy as np
     31 from uhi.typing.plottable import PlottableAxisGeneric, PlottableHistogram, PlottableTraits
---> 33 from . import config
     34 from . import histo_utils as h1u
     35 from .logging import logger

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/site-packages/plotit/config.py:285
    272         return cfg
    274     # def __post_init__(self):
    275     #     if self.x_axis_range is not None:
    276     #        try:
   (...)
    281     #            raise ValueError("Could not parse x-axis-range {0}: {1}".format(self.x_axis_range, e))
    282     #        self.x_axis_range = lims
--> 285 @dataclass
    286 class Legend(BaseConfigObject):
    287     position: Position = Position(x1=0.6, y1=0.6, x2=0.9, y2=0.9)
    288     columns: int = 1

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/dataclasses.py:1230, in dataclass(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot)
   1227     return wrap
   1229 # We're called as @dataclass without parens.
-> 1230 return wrap(cls)

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/dataclasses.py:1220, in dataclass.<locals>.wrap(cls)
   1219 def wrap(cls):
-> 1220     return _process_class(cls, init, repr, eq, order, unsafe_hash,
   1221                           frozen, match_args, kw_only, slots,
   1222                           weakref_slot)

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/dataclasses.py:958, in _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot)
    955         kw_only = True
    956     else:
    957         # Otherwise it's a field of some type.
--> 958         cls_fields.append(_get_field(cls, name, type, kw_only))
    960 for f in cls_fields:
    961     fields[f.name] = f

File ~/checkouts/readthedocs.org/user_builds/pyplotit/conda/latest/lib/python3.11/dataclasses.py:815, in _get_field(cls, a_name, a_type, default_kw_only)
    811 # For real fields, disallow mutable defaults.  Use unhashable as a proxy
    812 # indicator for mutability.  Read the __hash__ attribute from the class,
    813 # not the instance.
    814 if f._field_type is _FIELD and f.default.__class__.__hash__ is None:
--> 815     raise ValueError(f'mutable default {type(f.default)} for field '
    816                      f'{f.name} is not allowed: use default_factory')
    818 return f

ValueError: mutable default <class 'plotit.config.Position'> for field position is not allowed: use default_factory

Most of the returned objects are either (lists of) simple objects that represent a part of the configuration, e.g. a single plot. The classes are implemented as data classes. The list returned in samples is based on the entries in the files block of the configuration file, but using the grouping specified by their group attributes and the list of groups, such that each entry corresponds to a visible contribution in the plots.

Since the File and Group classes also contain functionality for the efficient loading and summing of the histograms, the pure configuration part is kept in a separate class (also a data class), under the cfg attribute. For groups the list of grouped files can be found under files.

[smp.cfg for smp in samples]

Typical plots contain an observed histogram and expectation stack. Since the former may be the sum of multiple datasets, it is also handled as a stack:

p = plots[0]
from plotit.plotit import Stack
expStack = Stack([smp.getHist(p) for smp in samples if smp.cfg.type == "MC"])
obsStack = Stack([smp.getHist(p) for smp in samples if smp.cfg.type == "DATA"])

The above works because both the File and Group class have a getHist method, which loads a single histogram from a file, or triggers the loading of multiple histograms and adds them up, respectively.

getHist returns a small object similar to a smart pointer: for a single file it holds the pointer to the (Py)ROOT histogram, for a group of stack it lazily constructs the sum histogram, or adds up the contents and squared weights arrays, depending on which method is called (more details will be added once the interfaces are more stable). These smart pointer or histogram handle classes also implement the uhi PlottableHistogram protocol, so they can directly be used with e.g. mplhep:

from matplotlib import pyplot as plt
fig, ax = plt.subplots()
ax.set_xlim(*p.x_axis_range)
import mplhep
mplhep.histplot(obsStack, histtype="errorbar", color="k")
mplhep.histplot(expStack.entries, stack=True, histtype="fill", color=[e.style.fill_color for e in expStack.entries])
ax.set_xlabel(p.x_axis, loc="right")
ax.set_ylabel(p.y_axis, loc="top")
mplhep.cms.label(data=True, label="Internal", lumi=config.getLumi())