/
obs.py
380 lines (296 loc) · 14.9 KB
/
obs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""# Observables
Implementations of come collider physics observables. Some observables
require the [FastJet](http://fastjet.fr/) Python interface to be importable;
if it's not, no warnings or errors will be issued, the observables will simply
not be included in this module."""
from __future__ import absolute_import, division, print_function
from abc import abstractmethod
import numpy as np
from numpy.core.multiarray import c_einsum
from energyflow.base import SingleEnergyCorrelatorBase
from energyflow.utils import import_fastjet, transfer
from energyflow.utils.fastjet_utils import *
from energyflow.utils.particle_utils import *
fj = import_fastjet()
__all__ = ['D2', 'C2', 'C3', 'image_activity']
###############################################################################
# D2
###############################################################################
class D2(SingleEnergyCorrelatorBase):
"""Ratio of EFPs (specifically, energy correlation functions) designed to
tag two prong signals. In graphs, the formula is:
<img src="https://github.com/pkomiske/EnergyFlow/raw/images/D2.png"
class="obs_center" width="20%"/>
For additional information, see the [original paper](https://arxiv.org/
abs/1409.6298).
"""
# line and triangle EFPs
graphs = [[(0,1)], [(0,1),(1,2),(2,0)]]
# D2(measure='hadr', beta=2, strassen=False, reg=0., kappa=1, normed=True,
# coords=None, check_input=True)
def __init__(self, measure='hadr', beta=2, strassen=False, reg=0., **kwargs):
r"""Since a `D2` defines and holds a `Measure` instance, all `Measure`
keywords are accepted.
**Arguments**
- **measure** : {`'hadr'`, `'hadrdot'`, `'hadrefm'`, `'ee'`, `'eeefm'`}
- The choice of measure. See [Measures](../measures) for additional
info.
- **beta** : _float_
- The parameter $\beta$ appearing in the measure. Must be greater
than zero.
- **strassen** : _bool_
- Whether to use matrix multiplication to speed up the evaluation.
Not recommended when $\beta=2$ since EFMs are faster.
- **reg** : _float_
- A regularizing value to be added to the denominator in the event
that it is zero. Should typically be something less than 1e-30.
- **kappa** : {_float_, `'pf'`}
- If a number, the energy weighting parameter $\kappa$. If `'pf'`,
use $\kappa=v-1$ where $v$ is the valency of the vertex.
- **normed** : _bool_
- Controls normalization of the energies in the measure.
- **coords** : {`'ptyphim'`, `'epxpypz'`, `None`}
- Controls which coordinates are assumed for the input. See
[Measures](../measures) for additional info.
- **check_input** : _bool_
- Whether to check the type of the input each time or assume the
first input type.
"""
# initialize base class
super(D2, self).__init__(self.graphs, measure, beta, strassen, kwargs)
self.reg = reg
def _strassen_compute(self, event, zs, thetas):
# evaluate the measure and get the zs and thetas
zs, thetas = super(D2, self)._strassen_compute(event, zs, thetas)
zthetas = thetas * zs
zthetas2 = np.dot(zthetas, zthetas)
dot = 1. if self.normed else np.sum(zs)
line = np.sum(zs[:,np.newaxis] * zthetas)
triangle = np.sum(zthetas2 * zthetas.T)
return triangle * dot**3/(line**3 + self.reg)
def _efp_compute(self, event, zs, thetas, nhats):
# get EFPset results
results = super(D2, self)._efp_compute(event, zs, thetas, nhats)
line, triangle = results[:2]
dot = 1. if self.normed else results[-1]
# implement D2 formula
return triangle * dot**3/(line**3 + self.reg)
###############################################################################
# C2
###############################################################################
class C2(SingleEnergyCorrelatorBase):
"""Ratio of Energy Correlation Functions designed to tag two prong signals.
In graphs, the formula is:
<img src="https://github.com/pkomiske/EnergyFlow/raw/images/C2.png"
class="obs_center" width="20%"/>
For additional information, see the [original paper](https://arxiv.org/
abs/1305.0007).
"""
# line and triangle EFPs
graphs = [[(0,1)], [(0,1),(1,2),(2,0)]]
# C2(measure='hadr', beta=2, strassen=False, reg=0., kappa=1, normed=True,
# coords=None, check_input=True)
def __init__(self, measure='hadr', beta=2, strassen=False, reg=0., **kwargs):
r"""Since a `C2` defines and holds a `Measure` instance, all `Measure`
keywords are accepted.
**Arguments**
- **measure** : {`'hadr'`, `'hadrdot'`, `'hadrefm'`, `'ee'`, `'eeefm'`}
- The choice of measure. See [Measures](../measures) for additional
info.
- **beta** : _float_
- The parameter $\beta$ appearing in the measure. Must be greater
than zero.
- **strassen** : _bool_
- Whether to use matrix multiplication to speed up the evaluation.
Not recommended when $\beta=2$ since EFMs are faster.
- **reg** : _float_
- A regularizing value to be added to the denominator in the event
that it is zero. Should typically be something less than 1e-30.
- **kappa** : {_float_, `'pf'`}
- If a number, the energy weighting parameter $\kappa$. If `'pf'`,
use $\kappa=v-1$ where $v$ is the valency of the vertex.
- **normed** : _bool_
- Controls normalization of the energies in the measure.
- **coords** : {`'ptyphim'`, `'epxpypz'`, `None`}
- Controls which coordinates are assumed for the input. See
[Measures](../measures) for additional info.
- **check_input** : _bool_
- Whether to check the type of the input each time or assume the
first input type.
"""
# initialize base class
super(C2, self).__init__(self.graphs, measure, beta, strassen, kwargs)
self.reg = reg
def _strassen_compute(self, event, zs, thetas):
# evaluate the measure and get the zs and thetas
zs, thetas = super(C2, self)._strassen_compute(event, zs, thetas)
zthetas = thetas * zs
zthetas2 = np.dot(zthetas, zthetas)
dot = 1. if self.normed else np.sum(zs)
line = np.sum(zs[:,np.newaxis] * zthetas)
triangle = np.sum(zthetas2 * zthetas.T)
return triangle * dot/(line**2 + self.reg)
def _efp_compute(self, event, zs, thetas, nhats):
# get EFPset results
results = super(C2, self)._efp_compute(event, zs, thetas, nhats)
line, triangle = results[:2]
dot = 1. if self.normed else results[-1]
# implement D2 formula
return triangle * dot/(line**2 + self.reg)
###############################################################################
# C3
###############################################################################
class C3(SingleEnergyCorrelatorBase):
"""Ratio of Energy Correlation Functions designed to tag three prong
signals. In graphs, the formula is:
<img src="https://github.com/pkomiske/EnergyFlow/raw/images/C3.png"
class="obs_center" width="30%"/>
For additional information, see the [original paper](https://arxiv.org/
abs/1305.0007).
"""
# line, triangle, and kite EFPs
graphs = [[(0,1)], [(0,1),(1,2),(2,0)], [(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)]]
# C3(measure='hadr', beta=2, reg=0., kappa=1, normed=True,
# coords=None, check_input=True)
def __init__(self, measure='hadr', beta=2, reg=0., **kwargs):
r"""Since a `D2` defines and holds a `Measure` instance, all `Measure`
keywords are accepted.
**Arguments**
- **measure** : {`'hadr'`, `'hadrdot'`, `'hadrefm'`, `'ee'`, `'eeefm'`}
- The choice of measure. See [Measures](../measures) for additional
info.
- **beta** : _float_
- The parameter $\beta$ appearing in the measure. Must be greater
than zero.
- **reg** : _float_
- A regularizing value to be added to the denominator in the event
that it is zero. Should typically be something less than 1e-30.
- **kappa** : {_float_, `'pf'`}
- If a number, the energy weighting parameter $\kappa$. If `'pf'`,
use $\kappa=v-1$ where $v$ is the valency of the vertex.
- **normed** : _bool_
- Controls normalization of the energies in the measure.
- **coords** : {`'ptyphim'`, `'epxpypz'`, `None`}
- Controls which coordinates are assumed for the input. See
[Measures](../measures) for additional info.
- **check_input** : _bool_
- Whether to check the type of the input each time or assume the
first input type.
"""
# initialize base class
super(C3, self).__init__(self.graphs, measure, beta, False, kwargs)
self.reg = reg
def _strassen_compute(self, *args, **kwargs):
raise NotImplementedError('no strassen implementation for C3')
def _efp_compute(self, event, zs, thetas, nhats):
# get EFPset results
results = super(C3, self)._efp_compute(event, zs, thetas, nhats)
# implement D2 formula
return results[2]*results[0]/(results[1]**2 + self.reg)
###############################################################################
# Image Activity (a.k.a. N95)
###############################################################################
def image_activity(ptyphis, f=0.95, R=1.0, npix=33, center=None, axis=None):
"""Image activity, also known as $N_f$, is the minimum number of pixels
in an image that contain a fraction $f$ of the total pT.
**Arguments**
- **ptyphis** : _2d numpy.ndarray_
- Array of particles in hadronic coordinates; the mass is optional
since it is not used in the computation of this observable.
- **f** : _float_
- The fraction $f$ of total pT that is to be contained by the pixels.
- **R** : _float_
- Half of the length of one side of the square space to tile with
pixels when forming the image. For a conical jet, this should typically
be the jet radius.
- **npix** : _int_
- The number of pixels along one dimension of the image, such that the
image has shape `(npix,npix)`.
- **center** : _str_ or `None`
- If not `None`, the centering scheme to use to center the particles
prior to calculating the image activity. See the option of the same
name for [`center_ptyphims`](/docs/utils/#center_ptyphims).
- **axis** : _numpy.ndarray_ or `None`
- If not `None`, the `[y,phi]` values to use for centering. If `None`,
the center of the image will be at `(0,0)`.
**Returns**
- _int_
- The image activity defined for the specified image paramters.
"""
# make bins
bins = np.linspace(-R, R, npix + 1)
# center if requested
if center is not None:
ptyphis = center_ptyphims(ptyphis, center=center, copy=True)
# handle passing an axis
if axis is not None:
bins = (axis[0] + bins, axis[1] + bins)
# make 2d image of pt
ptyphis = np.atleast_2d(ptyphis)
pixels = np.histogram2d(ptyphis[:,1], ptyphis[:,2], weights=ptyphis[:,0], bins=bins)[0].flatten()
# calcualte image activity
nf = np.argmax(np.cumsum(np.sort(pixels/(pixels.sum() + 10**-30))[::-1]) >= f) + 1
return nf
###############################################################################
# Observables relying on FastJet
###############################################################################
if fj:
__all__ += ['zg', 'zg_from_pj']
def zg(ptyphims, zcut=0.1, beta=0, R=1.0, algorithm='ca'):
r"""Groomed momentum fraction of a jet, as calculated on an array of
particles in hadronic coordinates. First, the particles are converted
to FastJet PseudoJets and clustered according to the specified
algorithm. Second, the jet is groomed according to the specified
SoftDrop parameters and the momentum fraction of the surviving pair of
Pseudojets is computed. See the [SoftDrop paper](https://arxiv.org/abs/
1402.2657) for a complete description of SoftDrop.
**Arguments**
- **ptyphims** : _numpy.ndarray_
- An array of particles in hadronic coordinates that will be
clustered into a single jet and groomed.
- **zcut** : _float_
- The $z_{\rm cut}$ parameter of SoftDrop. Should be between `0`
and `1`.
- **beta** : _int_ or _float_
- The $\beta$ parameter of SoftDrop.
- **R** : _float_
- The jet radius to use for the grooming. Only relevant if `beta!=0`.
- **algorithm** : {'kt', 'ca', 'antikt'}
- The jet algorithm to use when clustering the particles. Same as
the argument of the same name of [`cluster`](/docs/utils/#cluster).
**Returns**
- _float_
- The groomed momentum fraction of the given jet."""
return zg_from_pj(cluster(pjs_from_ptyphims(ptyphims), algorithm=algorithm)[0],
zcut=zcut, beta=beta, R=R)
def zg_from_pj(pseudojet, zcut=0.1, beta=0, R=1.0):
r"""Groomed momentum fraction $z_g$, as calculated on an ungroomed (but
already clustered) FastJet PseudoJet object. First, the jet is groomed
according to the specified SoftDrop parameters and then the momentum
fraction of the surviving pair of Pseudojets is computed. See the
[SoftDrop paper](https://arxiv.org/abs/1402.2657) for a complete
description of SoftDrop. This version of $z_g$ is provided in addition
to the above function so that a jet does not need to be reclustered if
multiple grooming parameters are to be used.
**Arguments**
- **pseudojet** : _fastjet.PseudoJet_
- A FastJet PseudoJet that has been obtained from a suitable
clustering (typically Cambridge/Aachen for SoftDrop).
- **zcut** : _float_
- The $z_{\rm cut}$ parameter of SoftDrop. Should be between `0`
and `1`.
- **beta** : _int_ or _float_
- The $\beta$ parameter of SoftDrop.
- **R** : _float_
- The jet radius to use for the grooming. Only relevant if `beta!=0`.
**Returns**
- _float_
- The groomed momentum fraction of the given jet.
"""
sd_jet = softdrop(pseudojet, zcut=zcut, beta=beta, R=R)
parent1, parent2 = fj.PseudoJet(), fj.PseudoJet()
if not sd_jet.has_parents(parent1, parent2):
return 0.
pt1, pt2 = parent1.pt(), parent2.pt()
ptsum = pt1 + pt2
return 0. if ptsum == 0. else min(pt1, pt2)/ptsum