Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
generic_plotter.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file generic_plotter.py
1 #!/usr/bin/env python3
2 from pathlib import Path
3 from typing import Optional, Dict, List
4 import re
5 import enum
6 import sys
7 
8 import uproot
9 import typer
10 import hist
11 import pydantic
12 import yaml
13 import pandas
14 import matplotlib.pyplot
15 import awkward
16 
17 
18 class Model(pydantic.BaseModel):
19  class Config:
20  extra = "forbid"
21 
22 
24  nbins: int = 100
25  min: Optional[float] = None
26  max: Optional[float] = None
27  label: Optional[str] = None
28 
29 
31  expression: str
32  name: str
33 
34 
35 class Config(Model):
36  histograms: Dict[str, HistConfig] = pydantic.Field(default_factory=dict)
37  extra_histograms: List[Extra] = pydantic.Field(default_factory=list)
38  exclude: List[str] = pydantic.Field(default_factory=list)
39 
40 
41 class Mode(str, enum.Enum):
42  recreate = "recreate"
43  update = "update"
44 
45 
46 def main(
47  infile: Path = typer.Argument(
48  ..., exists=True, dir_okay=False, help="The input ROOT file"
49  ),
50  treename: str = typer.Argument(..., help="The tree to look up branched from"),
51  outpath: Path = typer.Argument(
52  "outfile", dir_okay=False, help="The output ROOT file"
53  ),
54  config_file: Optional[Path] = typer.Option(
55  None,
56  "--config",
57  "-c",
58  exists=True,
59  dir_okay=False,
60  help="A config file following the input spec. By default, all branches will be plotted.",
61  ),
62  mode: Mode = typer.Option(Mode.recreate, help="Mode to open ROOT file in"),
63  plots: Optional[Path] = typer.Option(
64  None,
65  "--plots",
66  "-p",
67  file_okay=False,
68  help="If set, output plots individually to this directory",
69  ),
70  plot_format: str = typer.Option(
71  "pdf", "--plot-format", "-f", help="Format to write plots in if --plots is set"
72  ),
73  silent: bool = typer.Option(
74  False, "--silent", "-s", help="Do not print any output"
75  ),
76  dump_yml: bool = typer.Option(False, help="Print axis ranges as yml"),
77 ):
78  """
79  Script to plot all branches in a TTree from a ROOT file, with optional configurable binning and ranges.
80  Also allows setting extra expressions to be plotted as well.
81  """
82 
83  rf = uproot.open(infile)
84  tree = rf[treename]
85 
86  outfile = getattr(uproot, mode.value)(outpath)
87 
88  if config_file is None:
89  config = Config()
90  else:
91  with config_file.open() as fh:
92  config = Config.parse_obj(yaml.safe_load(fh))
93 
94  histograms = {}
95 
96  if not silent:
97  print(config.extra_histograms, file=sys.stderr)
98 
99  for df in tree.iterate(library="ak", how=dict):
100  for col in df.keys():
101  if any([re.match(ex, col) for ex in config.exclude]):
102  continue
103  h = histograms.get(col)
104  values = awkward.flatten(df[col], axis=None)
105 
106  if h is None:
107  # try to find config
108  found = None
109  for ex, data in config.histograms.items():
110  if re.match(ex, col):
111  found = data.copy()
112  print(
113  "Found HistConfig",
114  ex,
115  "for",
116  col,
117  ":",
118  found,
119  file=sys.stderr,
120  )
121 
122  if found is None:
123  found = HistConfig()
124 
125  if found.min is None:
126  found.min = awkward.min(values)
127 
128  if found.max is None:
129  found.max = awkward.max(values)
130 
131  if found.min == found.max:
132  found.min -= 1
133  found.max += 1
134 
135  h = hist.Hist(
136  hist.axis.Regular(
137  found.nbins, found.min, found.max, name=found.label or col
138  )
139  )
140 
141  histograms[col] = h
142  h.fill(values)
143 
144  for extra in config.extra_histograms:
145  h = histograms.get(extra.name)
146  # calc = pandas.eval(extra.expression, target=df)
147  calc = eval(extra.expression)
148  values = awkward.flatten(calc, axis=None)
149  if h is None:
150  if extra.min is None:
151  extra.min = awkward.min(values)
152  if extra.max is None:
153  extra.max = awkward.max(values)
154 
155  if extra.min == extra.max:
156  extra.min -= 1
157  extra.max += 1
158 
159  h = hist.Hist(
160  hist.axis.Regular(
161  extra.nbins,
162  extra.min,
163  extra.max,
164  name=extra.label or extra.name,
165  )
166  )
167 
168  histograms[extra.name] = h
169  h.fill(values)
170 
171  if plots is not None:
172  plots.mkdir(parents=True, exist_ok=True)
173 
174  for k, h in histograms.items():
175  if not silent:
176  if dump_yml:
177  ax = h.axes[0]
178  s = """
179 {k}:
180  nbins: {b}
181  min: {min}
182  max: {max}
183  """.format(
184  k=k, b=len(ax.edges) - 1, min=ax.edges[0], max=ax.edges[-1]
185  )
186  print(s)
187  else:
188  print(k, h.axes[0])
189  outfile[k] = h
190 
191  if plots is not None:
192  fig, ax = matplotlib.pyplot.subplots()
193 
194  h.plot(ax=ax, flow=None)
195 
196  fig.tight_layout()
197  fig.savefig(str(plots / f"{k}.{plot_format}"))
198  matplotlib.pyplot.close()
199 
200 
201 if __name__ == "__main__":
202  typer.run(main)