atlas-ftag-tools 0.2.6__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atlas-ftag-tools
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: ATLAS Flavour Tagging Tools
5
5
  Author: Sam Van Stroud, Philipp Gadow
6
6
  License: MIT
@@ -1,14 +1,14 @@
1
- ftag/__init__.py,sha256=edvBW3ySbO10OzSeqTQd9HPuXqiosdWibAhi3ZJFT4g,680
1
+ ftag/__init__.py,sha256=k5qBmtC7Ieh0trgm2Ba9Qj_6A2wQSpmAmXo2iIOAaI0,737
2
2
  ftag/cli_utils.py,sha256=w3TtQmUHSyAKChS3ewvOtcSDAUJAZGIIomaNi8f446U,298
3
3
  ftag/cuts.py,sha256=9_ooLZHaO3SnIQBNxwbaPZn-qptGdKnB27FdKQGTiTY,2933
4
- ftag/flavour.py,sha256=MUij7lknepnkidErDsyNsJDbAPotAEwS-uMaTSYCet4,3800
4
+ ftag/flavours.py,sha256=ShH4M2UjQZpZ_NlCctTm2q1tJbzYxjmGteioQ2GcqEU,114
5
5
  ftag/flavours.yaml,sha256=E_vpn38qJ3-Tygg2aHlH4wkn_rR1On_lMeaG8OemHCQ,8285
6
6
  ftag/git_check.py,sha256=Y-XqM80CVXZ5ZKrDdZcYOJt3X64uU6W3OP6Z0D7AZU0,1663
7
- ftag/labeller.py,sha256=iyDjRkOaly48jTaaFimA5eZ5YySp1lrLVEt3GyDZW-8,2553
7
+ ftag/labeller.py,sha256=IXUgU9UBir39PxVWRKs5r5fqI66Tv0x7nJD3-RYpbrg,2780
8
+ ftag/labels.py,sha256=C7IylPTnc32dFXq8C2Ks2wuljYK3WaY2EsPLGrhtXy8,3932
8
9
  ftag/mock.py,sha256=Eyj3tkkaSSnqvS3G6NS7fq8sB__Nx8YE9-OM2_lpdoQ,4992
9
10
  ftag/region.py,sha256=ANv0dGI2W6NJqD9fp7EfqAUReH4FOjc1gwl_Qn8llcM,360
10
- ftag/sample.py,sha256=TFXMhDkbPmjkms9-b-bINJ32T3bO86JcU70C0nY7wa8,2500
11
- ftag/test_cli_utils.py,sha256=xa08vf6SEOow58SSFagYdAselb-dkNOVvWsWheMnW-g,1001
11
+ ftag/sample.py,sha256=3N0FrRcu9l1sX8ohuGOHuMYGD0See6gMO4--7NzR2tE,2538
12
12
  ftag/track_selector.py,sha256=fJNk_kIBQriBqV4CPT_3ReJbOUnavDDzO-u3EQlRuyk,2654
13
13
  ftag/transform.py,sha256=uEGGJSnqoKOzLYQv650XdK_kDNw4Aw-5dc60z9Dp_y0,3963
14
14
  ftag/vds.py,sha256=nRViQZQIORB95nC7NZsW3KsSoGkLzEdOsuCViH5h8-U,3296
@@ -19,10 +19,10 @@ ftag/hdf5/h5split.py,sha256=4Wy6Xc3J58MdD9aBaSZHf5ZcVFnJSkWsm42R5Pgo-R4,2448
19
19
  ftag/hdf5/h5utils.py,sha256=-4zKTMtNCrDZr_9Ww7uzfsB7M7muBKpmm_1IkKJnHOI,3222
20
20
  ftag/hdf5/h5writer.py,sha256=9FkClV__UbBqmFsq_h2jwiZnbWVm8QFRL_4mDZZBbTs,5316
21
21
  ftag/wps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- ftag/wps/discriminant.py,sha256=rTASnt9s6xEHK5hLKHgvc9c7TiyyX6Upbp4LeTh6TB0,4270
23
- ftag/wps/working_points.py,sha256=fPfSZoTp-2qxL3YB7HdPxIB-7aSR_HozEtWWIA0BUtg,9749
24
- atlas_ftag_tools-0.2.6.dist-info/METADATA,sha256=gmEDL518a_xrRBsPsRYCFt-C06BD3K9bH6HCStV6mAU,5169
25
- atlas_ftag_tools-0.2.6.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
26
- atlas_ftag_tools-0.2.6.dist-info/entry_points.txt,sha256=LfVLsZHQolqbPnwPgtmc5IQTh527BKkN2v-IpXWTNHw,137
27
- atlas_ftag_tools-0.2.6.dist-info/top_level.txt,sha256=qiYQuKcAvMim-31FwkT3MTQu7WQm0s58tPAia5KKWqs,5
28
- atlas_ftag_tools-0.2.6.dist-info/RECORD,,
22
+ ftag/wps/discriminant.py,sha256=VJdZlJJUwaTeyxmIDEk23rQSAuvWs6wDA3XRjDI6-_c,4277
23
+ ftag/wps/working_points.py,sha256=cvStSpP8Cbb_FWM8v59tFsscUvdeqi831tLn5BiHUEg,9741
24
+ atlas_ftag_tools-0.2.7.dist-info/METADATA,sha256=oo5m85dK467AWuK-L8xaIbLDVmRUO3r7vA_J1vgR5b8,5169
25
+ atlas_ftag_tools-0.2.7.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
26
+ atlas_ftag_tools-0.2.7.dist-info/entry_points.txt,sha256=LfVLsZHQolqbPnwPgtmc5IQTh527BKkN2v-IpXWTNHw,137
27
+ atlas_ftag_tools-0.2.7.dist-info/top_level.txt,sha256=qiYQuKcAvMim-31FwkT3MTQu7WQm0s58tPAia5KKWqs,5
28
+ atlas_ftag_tools-0.2.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
ftag/__init__.py CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "v0.2.6"
6
-
5
+ __version__ = "v0.2.7"
7
6
 
8
7
  from ftag import hdf5
9
8
  from ftag.cuts import Cuts
10
- from ftag.flavour import Flavour, Flavours
9
+ from ftag.flavours import Flavours
11
10
  from ftag.labeller import Labeller
11
+ from ftag.labels import Label, LabelContainer
12
12
  from ftag.mock import get_mock_file
13
13
  from ftag.sample import Sample
14
14
  from ftag.transform import Transform
@@ -17,8 +17,9 @@ from ftag.wps.working_points import get_working_points
17
17
 
18
18
  __all__ = [
19
19
  "Cuts",
20
- "Flavour",
21
20
  "Flavours",
21
+ "Label",
22
+ "LabelContainer",
22
23
  "Labeller",
23
24
  "Sample",
24
25
  "Transform",
ftag/flavours.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from ftag.labels import LabelContainer
4
+
5
+ Flavours = LabelContainer.from_yaml()
ftag/labeller.py CHANGED
@@ -4,8 +4,9 @@ from dataclasses import dataclass
4
4
 
5
5
  import numpy as np
6
6
 
7
- from ftag.flavour import Flavour, FlavourContainer, Flavours
7
+ from ftag import Flavours
8
8
  from ftag.hdf5 import join_structured_arrays, structured_from_dict
9
+ from ftag.labels import Label, LabelContainer
9
10
 
10
11
 
11
12
  @dataclass
@@ -13,28 +14,36 @@ class Labeller:
13
14
  """
14
15
  Defines a labelling scheme.
15
16
 
16
- Labels are [0, ..., n] and are assigned using pre-defined selections.
17
+ Classes are assigned integer labels in [0, ..., n] based on pre-defined selections.
17
18
 
18
19
  Parameters
19
20
  ----------
20
- labels : FlavourContainer | list[str | Flavour]
21
+ labels : LabelContainer | list[str | Label]
21
22
  The labels to be use.
22
23
  require_labels : bool
23
24
  Whether to require that all objects are labelled.
24
25
  """
25
26
 
26
- labels: FlavourContainer | list[str | Flavour]
27
+ labels: LabelContainer | list[str | Label]
27
28
  require_labels: bool = True
28
29
 
29
- @property
30
- def variables(self):
31
- return sum((label.cuts.variables for label in self.labels), [])
32
-
33
30
  def __post_init__(self) -> None:
34
- if isinstance(self.labels, FlavourContainer):
31
+ if isinstance(self.labels, LabelContainer):
35
32
  self.labels = list(self.labels)
36
33
  self.labels = sorted([Flavours[label] for label in self.labels])
37
34
 
35
+ @property
36
+ def variables(self) -> list[str]:
37
+ """
38
+ Returns the variables used for labelling.
39
+
40
+ Returns
41
+ -------
42
+ list[str]
43
+ The variables used for labelling.
44
+ """
45
+ return sum((label.cuts.variables for label in self.labels), []) # type: ignore[union-attr]
46
+
38
47
  def get_labels(self, array: np.ndarray) -> np.ndarray:
39
48
  """
40
49
  Returns the labels for the given array.
ftag/labels.py ADDED
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ from ftag.cuts import Cuts
10
+
11
+
12
+ def remove_suffix(string: str, suffix: str) -> str:
13
+ if string.endswith(suffix):
14
+ return string[: -len(suffix)]
15
+ return string
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Label:
20
+ name: str
21
+ label: str
22
+ cuts: Cuts
23
+ colour: str
24
+ category: str
25
+ _px: str | None = None
26
+
27
+ @property
28
+ def px(self) -> str:
29
+ return self._px or f"p{remove_suffix(self.name, 'jets')}"
30
+
31
+ @property
32
+ def eff_str(self) -> str:
33
+ return self.label.replace("jets", "jet") + " efficiency"
34
+
35
+ @property
36
+ def rej_str(self) -> str:
37
+ return self.label.replace("jets", "jet") + " rejection"
38
+
39
+ @property
40
+ def frac_str(self) -> str:
41
+ return "f" + remove_suffix(self.name, "jets")
42
+
43
+ def __str__(self) -> str:
44
+ return self.name
45
+
46
+ def __lt__(self, other) -> bool:
47
+ return self.name < other.name
48
+
49
+
50
+ @dataclass
51
+ class LabelContainer:
52
+ labels: dict[str, Label]
53
+
54
+ def __iter__(self) -> Iterator:
55
+ yield from self.labels.values()
56
+
57
+ def __getitem__(self, key) -> Label:
58
+ if isinstance(key, Label):
59
+ key = key.name
60
+ try:
61
+ return self.labels[key]
62
+ except KeyError as e:
63
+ raise KeyError(f"Label '{key}' not found") from e
64
+
65
+ def __getattr__(self, name) -> Label:
66
+ return self[name]
67
+
68
+ def __contains__(self, label: str | Label) -> bool:
69
+ if isinstance(label, Label):
70
+ label = label.name
71
+ return label in self.labels
72
+
73
+ def __eq__(self, other) -> bool:
74
+ if isinstance(other, LabelContainer):
75
+ return self.labels == other.labels
76
+ if isinstance(other, list) and all(isinstance(f, str) for f in other):
77
+ return {f.name for f in self} == set(other)
78
+ return False
79
+
80
+ def __repr__(self) -> str:
81
+ return f"{self.__class__.__name__}({', '.join([f.name for f in self])})"
82
+
83
+ @property
84
+ def categories(self) -> list[str]:
85
+ return list(dict.fromkeys(f.category for f in self))
86
+
87
+ def by_category(self, category: str) -> LabelContainer:
88
+ f = LabelContainer({k: v for k, v in self.labels.items() if v.category == category})
89
+ if not f.labels:
90
+ raise KeyError(f"No labels with category '{category}' found")
91
+ return f
92
+
93
+ def from_cuts(self, cuts: list | Cuts) -> Label:
94
+ if isinstance(cuts, list):
95
+ cuts = Cuts.from_list(cuts)
96
+ for label in self:
97
+ if label.cuts == cuts:
98
+ return label
99
+ raise KeyError(f"Label with {cuts} not found")
100
+
101
+ @classmethod
102
+ def from_yaml(cls, yaml_path: Path | None = None) -> LabelContainer:
103
+ if yaml_path is None:
104
+ yaml_path = Path(__file__).parent / "flavours.yaml"
105
+ with open(yaml_path) as f:
106
+ config = yaml.safe_load(f)
107
+
108
+ # sanity checks
109
+ cuts = [Cuts.from_list(f["cuts"]) for f in config]
110
+ if duplicates := [c for c in cuts if cuts.count(c) > 1]:
111
+ raise ValueError(f"Duplicate label definitions detected: {duplicates}")
112
+ names = [f["name"] for f in config]
113
+ if duplicates := [n for n in names if names.count(n) > 1]:
114
+ raise ValueError(f"Duplicate label names detected: {duplicates}")
115
+
116
+ labels = {f["name"]: Label(cuts=Cuts.from_list(f.pop("cuts")), **f) for f in config}
117
+ return cls(labels)
118
+
119
+ @classmethod
120
+ def from_list(cls, labels: list[Label]) -> LabelContainer:
121
+ return cls({f.name: f for f in labels})
122
+
123
+ def backgrounds(self, label: Label, only_signals: bool = True) -> LabelContainer:
124
+ bkg = [f for f in self if f.category == label.category and f != label]
125
+ if not only_signals:
126
+ bkg = [f for f in bkg if f.name not in {"ujets", "qcd"}]
127
+ return LabelContainer.from_list(bkg)
ftag/sample.py CHANGED
@@ -4,7 +4,7 @@ import glob
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
 
7
- from ftag.flavour import remove_suffix
7
+ from ftag.labels import remove_suffix
8
8
  from ftag.vds import create_virtual_file
9
9
 
10
10
 
@@ -13,6 +13,7 @@ class Sample:
13
13
  pattern: Path | str | tuple[Path | str, ...]
14
14
  ntuple_dir: Path | str | None = None
15
15
  name: str | None = None
16
+ weights: list[float] | None = None
16
17
 
17
18
  def __post_init__(self) -> None:
18
19
  if not self.pattern:
ftag/wps/discriminant.py CHANGED
@@ -4,13 +4,14 @@ from typing import Callable
4
4
 
5
5
  import numpy as np
6
6
 
7
- from ftag.flavour import Flavour, Flavours, remove_suffix
7
+ from ftag import Flavours
8
+ from ftag.labels import Label, remove_suffix
8
9
 
9
10
 
10
11
  def discriminant(
11
12
  jets: np.ndarray,
12
13
  tagger: str,
13
- signal: Flavour,
14
+ signal: Label,
14
15
  fxs: dict[str, float],
15
16
  epsilon: float = 1e-10,
16
17
  ) -> np.ndarray:
@@ -87,7 +88,7 @@ def hcc_discriminant(jets, tagger, ftop=0.25, fhbb=0.3, epsilon=1e-10):
87
88
 
88
89
 
89
90
  def get_discriminant(
90
- jets: np.ndarray, tagger: str, signal: Flavour | str, epsilon: float = 1e-10, **fxs
91
+ jets: np.ndarray, tagger: str, signal: Label | str, epsilon: float = 1e-10, **fxs
91
92
  ):
92
93
  """Calculate the b-tag or c-tag discriminant for a given tagger.
93
94
 
@@ -97,7 +98,7 @@ def get_discriminant(
97
98
  Structured array of jets containing tagger outputs
98
99
  tagger : str
99
100
  Name of the tagger
100
- signal : Flavour
101
+ signal : Label
101
102
  Signal flavour (bjets/cjets or hbb/hcc)
102
103
  epsilon : float, optional
103
104
  Small number to avoid division by zero, by default 1e-10
@@ -8,9 +8,9 @@ from pathlib import Path
8
8
  import numpy as np
9
9
  import yaml
10
10
 
11
+ from ftag import Flavours
11
12
  from ftag.cli_utils import HelpFormatter
12
13
  from ftag.cuts import Cuts
13
- from ftag.flavour import Flavours
14
14
  from ftag.hdf5 import H5Reader
15
15
  from ftag.wps.discriminant import get_discriminant
16
16
 
ftag/flavour.py DELETED
@@ -1,127 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterator
4
- from dataclasses import dataclass
5
- from pathlib import Path
6
-
7
- import yaml
8
-
9
- from ftag.cuts import Cuts
10
-
11
-
12
- def remove_suffix(string: str, suffix: str) -> str:
13
- if string.endswith(suffix):
14
- return string[: -len(suffix)]
15
- return string
16
-
17
-
18
- @dataclass(frozen=True)
19
- class Flavour:
20
- name: str
21
- label: str
22
- cuts: Cuts
23
- colour: str
24
- category: str
25
- _px: str | None = None
26
-
27
- @property
28
- def px(self) -> str:
29
- return self._px or f"p{remove_suffix(self.name, 'jets')}"
30
-
31
- @property
32
- def eff_str(self) -> str:
33
- return self.label.replace("jets", "jet") + " efficiency"
34
-
35
- @property
36
- def rej_str(self) -> str:
37
- return self.label.replace("jets", "jet") + " rejection"
38
-
39
- @property
40
- def frac_str(self) -> str:
41
- return "f" + remove_suffix(self.name, "jets")
42
-
43
- def __str__(self) -> str:
44
- return self.name
45
-
46
- def __lt__(self, other) -> bool:
47
- return self.name < other.name
48
-
49
-
50
- @dataclass
51
- class FlavourContainer:
52
- flavours: dict[str, Flavour]
53
-
54
- def __iter__(self) -> Iterator:
55
- yield from self.flavours.values()
56
-
57
- def __getitem__(self, key) -> Flavour:
58
- if isinstance(key, Flavour):
59
- key = key.name
60
- try:
61
- return self.flavours[key]
62
- except KeyError as e:
63
- raise KeyError(f"Flavour '{key}' not found") from e
64
-
65
- def __getattr__(self, name) -> Flavour:
66
- return self[name]
67
-
68
- def __contains__(self, flavour: str | Flavour) -> bool:
69
- if isinstance(flavour, Flavour):
70
- flavour = flavour.name
71
- return flavour in self.flavours
72
-
73
- def __eq__(self, other) -> bool:
74
- if isinstance(other, FlavourContainer):
75
- return self.flavours == other.flavours
76
- if isinstance(other, list) and all(isinstance(f, str) for f in other):
77
- return {f.name for f in self} == set(other)
78
- return False
79
-
80
- def __repr__(self) -> str:
81
- return f"{self.__class__.__name__}({', '.join([f.name for f in self])})"
82
-
83
- @property
84
- def categories(self) -> list[str]:
85
- return list(dict.fromkeys(f.category for f in self))
86
-
87
- def by_category(self, category: str) -> FlavourContainer:
88
- f = FlavourContainer({k: v for k, v in self.flavours.items() if v.category == category})
89
- if not f.flavours:
90
- raise KeyError(f"No flavours with category '{category}' found")
91
- return f
92
-
93
- def from_cuts(self, cuts: list | Cuts) -> Flavour:
94
- if isinstance(cuts, list):
95
- cuts = Cuts.from_list(cuts)
96
- for flavour in self:
97
- if flavour.cuts == cuts:
98
- return flavour
99
- raise KeyError(f"Flavour with {cuts} not found")
100
-
101
- @classmethod
102
- def from_yaml(cls, yaml_path: Path | None = None) -> FlavourContainer:
103
- if yaml_path is None:
104
- yaml_path = Path(__file__).parent / "flavours.yaml"
105
-
106
- with open(yaml_path) as f:
107
- flavours_yaml = yaml.safe_load(f)
108
-
109
- flavours_dict = {
110
- f["name"]: Flavour(cuts=Cuts.from_list(f.pop("cuts")), **f) for f in flavours_yaml
111
- }
112
- assert len(flavours_dict) == len(flavours_yaml), "Duplicate flavour names detected"
113
-
114
- return cls(flavours_dict)
115
-
116
- @classmethod
117
- def from_list(cls, flavours: list[Flavour]) -> FlavourContainer:
118
- return cls({f.name: f for f in flavours})
119
-
120
- def backgrounds(self, flavour: Flavour, keep_possible_signals: bool = True) -> FlavourContainer:
121
- bkg = [f for f in self if f.category == flavour.category and f != flavour]
122
- if not keep_possible_signals:
123
- bkg = [f for f in bkg if f.name not in {"ujets", "qcd"}]
124
- return FlavourContainer.from_list(bkg)
125
-
126
-
127
- Flavours = FlavourContainer.from_yaml()
ftag/test_cli_utils.py DELETED
@@ -1,34 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from tempfile import NamedTemporaryFile, TemporaryDirectory
5
-
6
- import pytest
7
-
8
- from ftag.cli_utils import valid_path
9
-
10
-
11
- def test_valid_path_existing_file():
12
- # Test when the input path is an existing file
13
- # get a temp directory
14
- with TemporaryDirectory() as tmpdir, NamedTemporaryFile(dir=tmpdir) as f:
15
- input_path = f.name
16
- expected_output = Path(f.name)
17
- result = valid_path(input_path)
18
- assert result == expected_output
19
-
20
-
21
- def test_valid_path_non_existing_file():
22
- # Test when the input path is a non-existing file
23
- input_path = "non_existing_file.txt"
24
- with pytest.raises(FileNotFoundError) as e:
25
- valid_path(input_path)
26
- assert str(e.value) == input_path
27
-
28
-
29
- def test_valid_path_directory():
30
- # Test when the input path is a directory
31
- input_path = "directory/"
32
- with pytest.raises(FileNotFoundError) as e:
33
- valid_path(input_path)
34
- assert str(e.value) == input_path