Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
915d72a
ci: fail the pass gate on skipped/cancelled jobs and gate release on …
Jul 7, 2026
fc9216b
fix(jetmet): repair raw pt/mass fallbacks in CorrectedJetsFactory init
Jul 7, 2026
527c378
fix(jetmet): handle empty partitions in rand_gauss
Jul 7, 2026
bcae11c
fix(jetmet): stop build() mutating the input array's layout parameters
Jul 7, 2026
0e1d923
fix(jetmet): repair broken format strings in error messages
Jul 7, 2026
f8d76e5
fix(nanoevents): register subtraction for Candidate so muon1 - muon2 …
Jul 7, 2026
e990af2
fix(nanoevents): negative scalar multiply of PtEtaPhiMLorentzVector n…
Jul 7, 2026
397ca22
fix(taskvine): drop leftover unconditional raise in _handle_early_ter…
Jul 7, 2026
904966a
fix(taskvine): honor concurrent_reads in accumulate task
Jul 7, 2026
5b3e2d4
fix(analysis_tools): WeightStatistics + and += return proper objects
Jul 7, 2026
d8cf7c4
fix(analysis_tools): fill option-type weights in delayed multivariation
Jul 7, 2026
303444d
fix(analysis_tools): suffix-match Up/Down modifiers and accept multiv…
Jul 7, 2026
a7f6a24
fix(analysis_tools): PackedSelection.require returns an independent m…
Jul 7, 2026
724ac76
fix(lookup_tools): use numpy.int64 for EA multi-binned counts
Jul 7, 2026
a59c9e9
fix(lookup_tools): wire up json_lookup for pileup-JSON path
Jul 7, 2026
9310f35
fix(nanoevents): correct jet/pf cross-references in PFNano associations
Jul 7, 2026
9070295
fix(processor): decompress processor payload only when it is compress…
Jul 7, 2026
e5baa6f
fix(processor): pop timeout from uproot_options to avoid duplicate up…
Jul 7, 2026
edce9dd
fix(processor): retry failed tasks regardless of skipbadfiles
Jul 7, 2026
f3205d5
fix(processor): support all documented FuturesExecutor mergepool forms
Jul 7, 2026
4aeb876
fix(processor): surface recoverable exceptions in the default run path
Jul 7, 2026
0709764
fix(processor): limit preprocessing to maxchunks files per dataset
Jul 7, 2026
7afcd5b
fix(processor): recompress running accumulator only when it changes
Jul 7, 2026
e8c7f31
fix(processor): share and clean up auto-created dask client
Jul 7, 2026
83d675e
fix(nanoevents): resolve edm4hep OneToManyRelations to correct types
Jul 8, 2026
2bf8a5e
fix(nanoevents): use mixins_dictionary in FCCSchema._unknown_collections
Jul 8, 2026
3bfbbcf
fix(nanoevents): raise on unknown FCC.get_schema version
Jul 8, 2026
c8e4a46
fix(nanoevents): preserve factory mode across pickling
Jul 8, 2026
76d1bc7
fix(nanoevents): honor buffer_cache in from_preloaded
Jul 8, 2026
b16f92d
fix(nanoevents): don't treat zero-valued index ranges as empty
Jul 8, 2026
946a67c
fix(lumi_tools): consistent uint32 cast and delayed index across call…
Jul 8, 2026
5fa3407
fix(dataset_tools): correct rucio mode="first" partial skip and site …
Jul 8, 2026
a079d8f
fix(nanoevents): round-trip non-contiguous arrays through buffer cache
Jul 8, 2026
a39157c
fix(nanoevents): don't require numcodecs for Codec-protocol codecs
Jul 8, 2026
a03c1ca
perf(nanoevents): decode cached buffers once per hit
Jul 8, 2026
b6dd0c6
fix(processor): use modern register_plugin API for ColumnCache worker…
Jul 8, 2026
901335c
fix(dataset_tools): use ast.literal_eval instead of eval on report args
Jul 8, 2026
979c6b6
chore(ci): remove plaintext codecov token from codecov.yml
Jul 8, 2026
83bfe5c
perf: drop unused numba import from util.py
Jul 8, 2026
846cc5f
fix(ml_tools): check WRITEABLE flag on kwargs in tf_wrapper.numpy_call
Jul 8, 2026
efdfd34
fix(ml_tools): convert triton retry backoff from ms to seconds
Jul 8, 2026
85c818c
perf(ml_tools): concatenate triton batch outputs once instead of per …
Jul 8, 2026
78766b6
chore: remove dead parsl chunking code and unused tailtimeout parameter
Jul 8, 2026
a04d222
chore: remove dead nanoevents transforms
Jul 8, 2026
22841cd
chore: remove dead analysis_tools code
Jul 8, 2026
57aa6a1
chore: remove dead lookup_tools code
Jul 8, 2026
7d64085
chore: remove dead util helpers
Jul 8, 2026
25f4ca0
perf(nanoevents): cache parsed edm4hep yaml across schema builds
Jul 8, 2026
c6bc406
perf(btag_tools): vectorize dense lookup bin mapping
Jul 8, 2026
fb95a20
Merge branch 'fix/1578-jetmet-factory' into integration/1578-wave-fixes
Jul 8, 2026
d52ed2c
Merge branch 'fix/1578-vector-arithmetic' into integration/1578-wave-…
Jul 8, 2026
1335154
Merge branch 'fix/1578-taskvine' into integration/1578-wave-fixes
Jul 8, 2026
5874715
Merge branch 'fix/1578-analysis-tools' into integration/1578-wave-fixes
Jul 8, 2026
82e8106
Merge branch 'fix/1578-lookup-tools' into integration/1578-wave-fixes
Jul 8, 2026
90ae76d
Merge branch 'fix/1578-pfnano' into integration/1578-wave-fixes
Jul 8, 2026
42be9bd
Merge branch 'fix/1578-executor' into integration/1578-wave-fixes
Jul 8, 2026
666b5f5
Merge branch 'fix/1578-fcc-edm4hep' into integration/1578-wave-fixes
Jul 8, 2026
d2384a2
Merge branch 'fix/1578-rucio-lumi' into integration/1578-wave-fixes
Jul 8, 2026
48b5656
Merge branch 'fix/1578-nanoevents-mapping' into integration/1578-wave…
Jul 8, 2026
452d97d
Merge branch 'fix/1578-misc-low' into integration/1578-wave-fixes
Jul 8, 2026
0d1a620
Merge branch 'fix/1578-ml-tools' into integration/1578-wave-fixes
Jul 8, 2026
7f5f848
Merge branch 'fix/1578-dead-code' into integration/1578-wave-fixes
Jul 8, 2026
fb19eba
Merge branch 'fix/1578-perf' into integration/1578-wave-fixes
Jul 8, 2026
23d3a13
fix: keep externally-used API — restore dense_evaluated_lookup and ne…
Jul 10, 2026
57c5fbf
Merge branch 'fix/1578-dead-code' into integration/1578-wave-fixes
Jul 10, 2026
c7c8a2f
docs(nanoevents): describe candidate subtraction/negative-scaling tes…
Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ jobs:
release:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: [test, test-no-dask, test-vine]
needs: [test, test-dask-client, test-no-dask, test-vine]
permissions:
id-token: write
attestations: write
Expand Down Expand Up @@ -278,7 +278,10 @@ jobs:
password: ${{ secrets.PYPI_TOKEN }}

pass:
if: always()
needs: [test, test-dask-client, test-no-dask, test-vine]
runs-on: ubuntu-latest
steps:
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
run: exit 1
- run: echo "All jobs passed"
1 change: 0 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
comment: off

codecov:
token: 045255bb-e0d8-4c5d-b413-9c33128a03a4
notify:
require_ci_to_pass: yes

Expand Down
71 changes: 23 additions & 48 deletions src/coffea/analysis_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,6 @@ def _get_hist_class(delayed_mode):
return hist.Hist


def _generate_slices(array_length, max_elements=128):
"""Generate slices to split an array into chunks of at most `max_elements` elements

Parameters
----------
array_length : int
The length of the array to split
max_elements : int, optional
The maximum number of elements in each chunk. Default is 128.

Returns
-------
slices : list of slice objects
A list of slice objects to iterate over and split the array into chunks with at most `max_elements` elements per slice
"""
slices = []
for start in range(0, array_length, max_elements):
end = min(start + max_elements, array_length)
slices.append(slice(start, end))
return slices


def boolean_masks_to_categorical_integers(
masks,
insert_unmasked_as_zeros=False,
Expand Down Expand Up @@ -187,10 +165,12 @@ def add(self, other):

def __add__(self, other):
temp = WeightStatistics(self.sumw, self.sumw2, self.minw, self.maxw, self.n)
return temp.add(other)
temp.add(other)
return temp

def __iadd__(self, other):
return self.add(other)
self.add(other)
return self


class Weights:
Expand Down Expand Up @@ -394,7 +374,7 @@ def __add_multivariation_delayed(
"""Add a new weight with multiple variations in delayed mode"""
dask_awkward = _import_dask_awkward()

if isinstance(weight, awkward.types.OptionType):
if isinstance(dask_awkward.type(weight), awkward.types.OptionType):
# TODO what to do with option-type? is it representative of unknown weight
# and we default to one or is it an invalid weight and we should never use this
# event in the first place (0) ?
Expand Down Expand Up @@ -567,8 +547,8 @@ def weight(self, modifier=None):
"""
if modifier is None:
return self._weight
elif "Down" in modifier and modifier not in self._modifiers:
return self._weight / self._modifiers[modifier.replace("Down", "Up")]
elif modifier.endswith("Down") and modifier not in self._modifiers:
return self._weight / self._modifiers[modifier[:-4] + "Up"]
return self._weight * self._modifiers[modifier]

def partial_weight(self, include=[], exclude=[], modifier=None):
Expand Down Expand Up @@ -628,12 +608,17 @@ def _partial_weight(self, include, exclude, modifier=None):

if modifier is None:
return w
elif modifier.replace("Down", "").replace("Up", "") not in names:
base = (
modifier[:-4]
if modifier.endswith("Down")
else modifier[:-2] if modifier.endswith("Up") else modifier
)
if not any(base == n or base.startswith(n + "_") for n in names):
raise ValueError(
f"Modifier {modifier} is not in the list of included weights"
)
elif "Down" in modifier and modifier not in self._modifiers:
return w / self._modifiers[modifier.replace("Down", "Up")]
if modifier.endswith("Down") and modifier not in self._modifiers:
return w / self._modifiers[modifier[:-4] + "Up"]
return w * self._modifiers[modifier]

@property
Expand All @@ -642,7 +627,8 @@ def variations(self):
keys = set(self._modifiers.keys())
# add any missing 'Down' variation
for k in self._modifiers.keys():
keys.add(k.replace("Up", "Down"))
if k.endswith("Up"):
keys.add(k[:-2] + "Down")
return keys


Expand Down Expand Up @@ -1188,10 +1174,6 @@ def yieldhist(self, weighted=None, scale=None, categorical=None):
if do_weighted:
axes.append(hist.storage.Weight())
if not self._delayed_mode and not do_categorical:
if categorical is not None:
raise NotImplementedError(
"yieldhist is not implemented for non-delayed mode (v1) with categorical"
)
h = hist.Hist(*axes)
weighttofill = self._wgtev if do_weighted else self._nev
if do_scaled:
Expand All @@ -1200,10 +1182,6 @@ def yieldhist(self, weighted=None, scale=None, categorical=None):
elif self._delayed_mode and not do_categorical:
dask_awkward = _import_dask_awkward()

if categorical is not None:
raise NotImplementedError(
"yieldhist is not implemented for non-delayed mode (v1) with categorical"
)
h = Hist(*axes)

for i, mask in enumerate(self._masks, 1):
Expand Down Expand Up @@ -1772,10 +1750,6 @@ def yieldhist(self, weighted=None, scale=None, categorical=None):
if do_weighted:
axes.append(hist.storage.Weight())
if not self._delayed_mode and not do_categorical:
if categorical is not None:
raise NotImplementedError(
"yieldhist is not implemented for non-delayed mode (v1) with categorical"
)
honecut = hist.Hist(*axes)
hcutflow = honecut.copy()
hcutflow.axes.name = ("cutflow",)
Expand All @@ -1789,10 +1763,6 @@ def yieldhist(self, weighted=None, scale=None, categorical=None):
elif self._delayed_mode and not do_categorical:
dask_awkward = _import_dask_awkward()

if categorical is not None:
raise NotImplementedError(
"yieldhist is not implemented for non-delayed mode (v1) with categorical"
)
honecut = Hist(*axes)
hcutflow = honecut.copy()
hcutflow.axes.name = ("cutflow",)
Expand Down Expand Up @@ -2291,7 +2261,6 @@ def add_multiple(self, selections, fill_value=False):
for name, selection in selections.items():
self.add(name, selection, fill_value)

@lru_cache
def require(self, **names):
"""Return a mask vector corresponding to specific requirements

Expand All @@ -2318,6 +2287,12 @@ def require(self, **names):
returns a boolean array where an entry is True if the corresponding entries
``cut1 == True``, ``cut2 == False``, and ``cut3`` arbitrary.
"""
# copy so a caller mutating the returned mask cannot corrupt the shared cache
result = self._require(**names)
return result.copy() if isinstance(result, numpy.ndarray) else result

@lru_cache
def _require(self, **names):
for cut, v in names.items():
if not isinstance(cut, str) or cut not in self._names:
raise ValueError(
Expand Down
59 changes: 34 additions & 25 deletions src/coffea/btag_tools/btagscalefactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,31 +147,40 @@ def __init__(self, filename, workingpoint, methods="comb,comb,incl", keep_df=Fal
)
mapping = numpy.full(bin_low_edges[0].shape, -1)

def findbin(flavor, eta, pt, discr):
btvflavor = self._flavor2btvflavor[flavor]
for i, (fbin, ebin, pbin, dbin) in enumerate(allbins):
if (
btvflavor == fbin
and ebin[0] <= eta < ebin[1]
and pbin[0] <= pt < pbin[1]
and dbin[0] <= discr < dbin[1]
):
return i
if eta < 0:
# maybe in this region we have only abseta
for i, (fbin, ebin, pbin, dbin) in enumerate(allbins):
if (
btvflavor == fbin
and -ebin[1] <= eta < -ebin[0]
and pbin[0] <= pt < pbin[1]
and dbin[0] <= discr < dbin[1]
):
return i
return -1

for idx, _ in numpy.ndenumerate(mapping):
flavor, eta, pt, discr = (x[idx] for x in bin_low_edges)
mapping[idx] = findbin(flavor, eta, pt, discr)
fbins = numpy.array([b[0] for b in allbins])
eta_lo = numpy.array([b[1][0] for b in allbins])
eta_hi = numpy.array([b[1][1] for b in allbins])
pt_lo = numpy.array([b[2][0] for b in allbins])
pt_hi = numpy.array([b[2][1] for b in allbins])
discr_lo = numpy.array([b[3][0] for b in allbins])
discr_hi = numpy.array([b[3][1] for b in allbins])

eta_cell = bin_low_edges[1].reshape(-1, 1)
pt_cell = bin_low_edges[2].reshape(-1, 1)
discr_cell = bin_low_edges[3].reshape(-1, 1)
btvflavor_cell = numpy.empty(bin_low_edges[0].size, dtype=fbins.dtype)
for flav, btv in self._flavor2btvflavor.items():
btvflavor_cell[bin_low_edges[0].reshape(-1) == flav] = btv
btvflavor_cell = btvflavor_cell.reshape(-1, 1)

base_match = (
(btvflavor_cell == fbins)
& (pt_lo <= pt_cell)
& (pt_cell < pt_hi)
& (discr_lo <= discr_cell)
& (discr_cell < discr_hi)
)
match = base_match & (eta_lo <= eta_cell) & (eta_cell < eta_hi)
found = match.any(axis=1)
result = numpy.where(found, match.argmax(axis=1), -1)

# maybe in this region we have only abseta
abseta_match = base_match & (-eta_hi <= eta_cell) & (eta_cell < -eta_lo)
fallback = (~found) & (eta_cell[:, 0] < 0)
fallback &= abseta_match.any(axis=1)
result[fallback] = abseta_match.argmax(axis=1)[fallback]

mapping = result.reshape(mapping.shape)

if self.workingpoint == BTagScaleFactor.RESHAPE:
self._corrections[syst] = dense_mapped_lookup(
Expand Down
3 changes: 2 additions & 1 deletion src/coffea/dataset_tools/manipulations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import ast
import copy
import sys
from collections.abc import Callable
Expand Down Expand Up @@ -321,7 +322,7 @@ def get_failed_steps_for_dataset(
)

for failure in failures:
args_as_types = tuple(eval(arg) for arg in failure.args)
args_as_types = tuple(ast.literal_eval(arg) for arg in failure.args)

fname, object_path, start, stop, is_step = args_as_types

Expand Down
7 changes: 4 additions & 3 deletions src/coffea/dataset_tools/rucio_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,9 @@ def get_dataset_files_replicas(
outfiles.append(outfile)
outsites.append(outsite)
elif mode == "first":
outfiles.append(outfile[0])
outsites.append(outsite[0])
if outfile:
outfiles.append(outfile[0])
outsites.append(outsite[0])
else:
raise NotImplementedError(f"Mode {mode} not yet implemented!")

Expand All @@ -287,7 +288,7 @@ def get_dataset_files_replicas(
sites_counts[site] += 1
elif mode == "first":
for site_by_file in outsites:
sites_counts[site] += 1
sites_counts[site_by_file] += 1

return outfiles, outsites, sites_counts

Expand Down
17 changes: 10 additions & 7 deletions src/coffea/jetmet_tools/CorrectedJetsFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ def __call__(self, array, like_what):


def rand_gauss(item):
np_item = awkward.typetracer.length_one_if_typetracer(item).to_numpy()
seeds = (
awkward.typetracer.length_one_if_typetracer(item).to_numpy()[[0, -1]].view("i4")
np_item[[0, -1]].view("i4")
if len(np_item)
else numpy.zeros(2, dtype=numpy.int32)
)
randomstate = numpy.random.Generator(numpy.random.PCG64(seeds))

Expand Down Expand Up @@ -148,20 +151,20 @@ def __init__(self, name_map, jec_stack):
# from PhysicsTools/PatUtils/interface/SmearedJetProducerT.h#L283
self.forceStochastic = False

if "ptRaw" not in name_map or name_map["ptRaw"] is None:
self.treat_pt_as_raw = "ptRaw" not in name_map or name_map["ptRaw"] is None
if self.treat_pt_as_raw:
warnings.warn(
"There is no name mapping for ptRaw,"
" CorrectedJets will assume that <object>.pt is raw pt!"
)
name_map["ptRaw"] = name_map["JetPt"] + "_raw"
self.treat_pt_as_raw = "ptRaw" not in name_map

if "massRaw" not in name_map or name_map["massRaw"] is None:
warnings.warn(
"There is no name mapping for massRaw,"
" CorrectedJets will assume that <object>.mass is raw pt!"
" CorrectedJets will assume that <object>.mass is raw mass!"
)
name_map["ptRaw"] = name_map["JetMass"] + "_raw"
name_map["massRaw"] = name_map["JetMass"] + "_raw"

total_signature = set()
for part in _stack_parts:
Expand Down Expand Up @@ -226,7 +229,7 @@ def build(self, injets):
fields = awkward.fields(jets)
if len(fields) == 0:
raise Exception(
"Empty record, please pass a jet object with at least {self.real_sig} defined!"
f"Empty record, please pass a jet object with at least {self.real_sig} defined!"
)
out = awkward.flatten(jets)
wrap = partial(
Expand Down Expand Up @@ -484,7 +487,7 @@ def build_variant(unc, template, jetpt, jetpt_orig, jetmass, jetmass_orig):
label=f"{name}",
)

out_parms = out.layout.parameters
out_parms = dict(out.layout.parameters)
out_parms["corrected"] = True
out = awkward.zip(
out_dict, depth_limit=1, parameters=out_parms, behavior=out.behavior
Expand Down
3 changes: 1 addition & 2 deletions src/coffea/jetmet_tools/FactorizedJetCorrector.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ def _checkConsistency(against, tocheck):
else:
if against != tocheck:
raise Exception(
"Corrector for {} is mixed"
"with correctors for {}!".format(tocheck, against)
f"Corrector for {tocheck} is mixed with correctors for {against}!"
)
return tocheck

Expand Down
2 changes: 2 additions & 0 deletions src/coffea/lookup_tools/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from coffea.lookup_tools.jec_uncertainty_lookup import jec_uncertainty_lookup
from coffea.lookup_tools.jersf_lookup import jersf_lookup
from coffea.lookup_tools.jme_standard_function import jme_standard_function
from coffea.lookup_tools.json_lookup import json_lookup
from coffea.lookup_tools.rochester_lookup import rochester_lookup

lookup_types = {
Expand All @@ -14,6 +15,7 @@
"jec_uncertainty_lookup": jec_uncertainty_lookup,
"rochester_lookup": rochester_lookup,
"correctionlib_wrapper": correctionlib_wrapper,
"json_lookup": json_lookup,
}


Expand Down
2 changes: 0 additions & 2 deletions src/coffea/lookup_tools/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ def add_weight_sets(self, weightsdescs):
else:
weights, thetype = self.extract_from_file(thefile, name)
self.add_weight_set(local_name, thetype, weights)
if thetype == "json_lookup":
self._names[local_name] = 0

def import_file(self, thefile):
"""
Expand Down
Loading
Loading