Skip to content

Commit 163d9a1

Browse files
tmadlenerZehvogel
andauthored
Add utilities to facilitate the handling of the different I/O formats and related conversions (#234)
* Introduce a helper to facilitate I/O and related conversions * Add docstrings * Rework the logic of attaching converters to be a bit simpler * Detach from upstream k4FWCore functionality * Switch event_display example to use new functionality * Make output commands an optional argument * Remove unnecessary member * Add documentation * Fix formatting issues * Add deprecation warning for old module * Fix missing comma in documentation * Fix minor typos in docstrings Co-authored-by: Leonhard Reichenbach <Zehvogel@users.noreply.github.com> --------- Co-authored-by: Leonhard Reichenbach <Zehvogel@users.noreply.github.com>
1 parent 1026dd7 commit 163d9a1

8 files changed

Lines changed: 503 additions & 17 deletions

File tree

doc/MarlinWrapperIntroduction.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,71 @@ unconditionally. Hence, if it's missing the conversion will fail. Make sure to
227227
read it in, or create it on the fly.
228228
```
229229

230+
## Tools for facilitating interoperability
231+
Mixed reconstruction and analysis chains (i.e. chains that have wrapped Marlin
232+
processors and native Gaudi algorithms) come with some challenges, e.g. the need
233+
to handle LCIO and EDM4hep inputs and outputs transparently and making sure that
234+
all the necessary conversions are done at the appropriate places. To facilitate
235+
this k4MarlinWrapper provides some python tools to automate parts of this.
236+
237+
Most importantly the `IOHandlerHelper` class can be used to create and insert
238+
the correct readers and writers as well as injecting converters at the necessary
239+
places. We recommend using it if you need to support different input and output
240+
formats. The basic setup looks like this:
241+
242+
```python
243+
from Configurables import EventDataSvc
244+
from k4FWCore import IOSvc
245+
from k4MarlinWrapper.io_helpers import IOHandlerHelper
246+
247+
alg_list = []
248+
evt_svc = EventDataSvc("EventDataSvc")
249+
svc_list = [evt_svc]
250+
io_svc = IOSvc()
251+
252+
io_handler = IOHandlerHelper(alg_list, io_svc)
253+
# Create an appropriate reader for the input files
254+
io_handler.add_reader(input_files)
255+
```
256+
257+
This will either add an EDM4hep reader to the `IOSvc` (at the very beginning of
258+
the reconstruction chain) or add an `LcioEvent` algorithm at the current place
259+
in the `alg_list`. You can now simply add all the algorithms
260+
(`MarlinProcessorWrapper` or native Gaudi algorithms) as usual to the
261+
`alg_list`.
262+
263+
For adding EDm4hep output simply do
264+
```python
265+
io_handler.add_edm4hep_writer("output.edm4hep.root")
266+
```
267+
268+
which will create an output file `output.edm4hep.root` and use the `["keep *"]`
269+
as `IOSvc.outputCommands`. The latter can be changed by passing a second
270+
argument.
271+
272+
For adding LCIO output you need to
273+
```python
274+
lcio_writer = io_handler.add_lcio_writer("LCIOWriter")
275+
lcio_writer.Parameters = {
276+
"LCIOOutputFile": ["output.slcio"],
277+
"LCIOWriteMode": ["WRITE_NEW"]
278+
}
279+
```
280+
281+
Note that in this case the `lcio_writer` is not yet fully configured but simply
282+
added to the chain of algorithms to execute.
283+
284+
Finally, to insert all the necessary converters simply call
285+
`finalize_converters` before handing off the algorithm list to the
286+
`ApplicationMgr`, i.e.
287+
288+
```python
289+
io_handler.finalize_converters()
290+
291+
from k4FWCore import ApplicationMgr
292+
ApplicationMgr(TopAlg=alg_list, EvtSel="NONE", ExtSvc=[evt_svc])
293+
```
294+
230295
## Potential pitfalls when using other Gaudi Algorithms
231296

232297
Although mixing wrapped Marlin Processors with other Gaudi Algorithms is working

k4MarlinWrapper/examples/event_display.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
#
2020

2121
from Gaudi.Configuration import INFO
22-
from Configurables import MarlinProcessorWrapper, k4DataSvc, GeoSvc
22+
from Configurables import MarlinProcessorWrapper, EventDataSvc, GeoSvc
23+
from k4FWCore import ApplicationMgr, IOSvc
2324
from k4FWCore.parseArgs import parser
24-
from k4MarlinWrapper.inputReader import create_reader, attach_edm4hep2lcio_conversion
25+
from k4MarlinWrapper.io_helpers import IOHandlerHelper
2526

2627

2728
parser.add_argument(
@@ -42,25 +43,18 @@
4243
reco_args = parser.parse_known_args()[0]
4344

4445
algList = []
45-
svcList = []
46-
47-
evtsvc = k4DataSvc("EventDataSvc")
48-
svcList.append(evtsvc)
46+
svcList = [EventDataSvc("EventDataSvc")]
4947

48+
iosvc = IOSvc()
5049

5150
geoSvc = GeoSvc("GeoSvc")
5251
geoSvc.detectors = [reco_args.compactFile]
5352
geoSvc.OutputLevel = INFO
5453
geoSvc.EnableGeant4Geo = False
5554
svcList.append(geoSvc)
5655

57-
58-
if reco_args.inputFiles:
59-
read = create_reader(reco_args.inputFiles, evtsvc)
60-
read.OutputLevel = INFO
61-
algList.append(read)
62-
else:
63-
read = None
56+
io_handler = IOHandlerHelper(algList, iosvc)
57+
io_handler.add_reader(reco_args.inputFiles)
6458

6559
MyCEDViewer = MarlinProcessorWrapper("MyCEDViewer")
6660
MyCEDViewer.OutputLevel = INFO
@@ -527,8 +521,6 @@
527521
algList.append(MyCEDViewer)
528522

529523
# We need to convert the inputs in case we have EDM4hep input
530-
attach_edm4hep2lcio_conversion(algList, read)
531-
532-
from Configurables import ApplicationMgr
524+
io_handler.finalize_converters()
533525

534526
ApplicationMgr(TopAlg=algList, EvtSel="NONE", EvtMax=10, ExtSvc=svcList, OutputLevel=INFO)

k4MarlinWrapper/python/k4MarlinWrapper/inputReader.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,21 @@
1818
#
1919

2020
import sys
21-
from Configurables import LcioEvent, PodioInput, MarlinProcessorWrapper, EDM4hep2LcioTool
21+
import warnings
22+
23+
warnings.warn(
24+
"The 'k4MarlinWrapper.inputReader' module is deprecated and will be removed in a future version. "
25+
"Please use the utilities in `k4MarlinWrapper.io_helpers` instead.",
26+
category=DeprecationWarning,
27+
stacklevel=2,
28+
)
29+
30+
from Configurables import (
31+
LcioEvent,
32+
PodioInput,
33+
MarlinProcessorWrapper,
34+
EDM4hep2LcioTool,
35+
)
2236

2337

2438
def create_reader(input_files, evtSvc):
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2019-2024 Key4hep-Project.
4+
#
5+
# This file is part of Key4hep.
6+
# See https://key4hep.github.io/key4hep-doc/ for further info.
7+
#
8+
# Licensed under the Apache License, Version 2.0 (the "License");
9+
# you may not use this file except in compliance with the License.
10+
# You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
#
20+
21+
import sys
22+
import logging
23+
24+
from Configurables import (
25+
LcioEvent,
26+
MarlinProcessorWrapper,
27+
EDM4hep2LcioTool,
28+
Lcio2EDM4hepTool,
29+
)
30+
31+
logger = logging.getLogger()
32+
33+
34+
def _is_wrapped_proc_without_conv(alg, from_edm, to_edm):
35+
"""Check if this algorithm has a configured (i.e. named) converter attached
36+
for the direction described by from_edm and to_edm"""
37+
if isinstance(alg, MarlinProcessorWrapper):
38+
if not getattr(alg, f"{from_edm}2{to_edm}Tool").getName():
39+
return True
40+
41+
return False
42+
43+
44+
class IOHandlerHelper:
45+
"""Helper class to facilitate the transparent handling of LCIO or EDM4hep
46+
inputs and outputs.
47+
48+
This class allows to
49+
- add the correct reader as determined from the input file name(s)
50+
- add (multiple) LCIO writers
51+
- add an EDM4hep writer
52+
- make sure that the necessary converters are introduced at the right places
53+
"""
54+
55+
def __init__(self, alg_list, io_svc):
56+
"""Create a IOHandlerHelper
57+
58+
Args:
59+
alg_list (list): The algorithm list which is being populated
60+
io_svc (IOSvc): The IOSvc that is being used for this run
61+
"""
62+
self._alg_list = alg_list
63+
self._io_svc = io_svc
64+
self._lcio_input = False
65+
self._edm4hep_output = False
66+
67+
def add_reader(self, input_files):
68+
"""Add a reader that is equipped to read the passed files
69+
70+
If the input is LCIO the necessary algorithm will be configured and
71+
added to the list of algorithms at the current spot. If the input is
72+
EDM4hep the file names will be passed to the IOSvc.Input
73+
74+
Args:
75+
input_files (list): The input files that should be read
76+
"""
77+
if input_files[0].endswith(".slcio"):
78+
if any(not f.endswith(".slcio") for f in input_files):
79+
logger.error("All input files need to have the same format (LCIO)")
80+
sys.exit(1)
81+
82+
read = LcioEvent()
83+
read.Files = input_files
84+
self._alg_list.append(read)
85+
self._lcio_input = True
86+
else:
87+
if any(not f.endswith(".root") for f in input_files):
88+
logger.error("All input files need to have the same format (EDM4hep)")
89+
sys.exit(1)
90+
91+
self._io_svc.Input = input_files
92+
93+
def add_lcio_writer(self, alg_name):
94+
"""Add a writer for LCIO output at the current spot in the algorithm list
95+
96+
Note:
97+
This doesn't configure anything yet, that is still left to do outside
98+
99+
Args:
100+
alg_name (str): The name this writer should have
101+
102+
Returns:
103+
MarlinProcessorWrapper: The wrapped processor that has just been
104+
inserted into the algorithm list and that now needs to be
105+
further configured
106+
"""
107+
writer = MarlinProcessorWrapper(alg_name, ProcessorType="LCIOOutputProcessor")
108+
self._alg_list.append(writer)
109+
return writer
110+
111+
def add_edm4hep_writer(self, output_file, output_cmds=["keep *"]):
112+
"""Add an EDM4hep writer at the very end of the algorithm execution
113+
114+
This will pass the output file name as well as the output commands to
115+
the IOSvc.Ouptut and IOSvc.outputCommands respectively
116+
117+
Args:
118+
output_file (str): The name of the output file
119+
120+
output_cmds (list, optional): The list of output commands that
121+
should be applied. Defaults to ["keep *"]
122+
"""
123+
self._io_svc.Output = output_file
124+
self._io_svc.outputCommands = output_cmds
125+
self._edm4hep_output = True
126+
127+
def finalize_converters(self):
128+
"""Attach the appropriate converters in all places they are necessary
129+
130+
Go through the algorithm list and determine where appropriate converters
131+
need to be inserted such that the algorithms or wrapped processors always
132+
see a consistent picture of the event in both formats. Basically what
133+
this does is to go through the complete list of algorithms and introduce
134+
an LCIO to EDM4hep converter at the start of every run of
135+
MarlinProcessorWrappers and in the other direction at the end of every
136+
such run
137+
138+
Note:
139+
Call this just before you pass the algorithm list to the ApplicationMgr
140+
141+
Note:
142+
This will not change existing converters on wrapped processors
143+
"""
144+
for alg, next_alg in zip(self._alg_list, self._alg_list[1:]):
145+
if not isinstance(next_alg, MarlinProcessorWrapper) and _is_wrapped_proc_without_conv(
146+
alg, "Lcio", "EDM4hep"
147+
):
148+
# We change from a run of wrapped processors to algorithms and
149+
# we don't have a converter yet
150+
output_conv = Lcio2EDM4hepTool(f"{alg.getName()}_OutputConverter")
151+
output_conv.convertAll = True
152+
output_conv.collNameMapping = {"MCParticle": "MCParticles"}
153+
alg.Lcio2EDM4hepTool = output_conv
154+
logger.info(
155+
f"Added an output converter (LCIO to EDM4hep) to the {alg.getName()} algorithm"
156+
)
157+
158+
if not isinstance(
159+
alg, (MarlinProcessorWrapper, LcioEvent)
160+
) and _is_wrapped_proc_without_conv(next_alg, "EDM4hep", "Lcio"):
161+
# We change from a run of algorithms to wrapped processors and
162+
# we do not have a converter yet
163+
input_conv = EDM4hep2LcioTool(f"{next_alg.getName()}_InputConverter")
164+
input_conv.convertAll = True
165+
input_conv.collNameMapping = {"MCParticles": "MCParticle"}
166+
next_alg.EDM4hep2LcioTool = input_conv
167+
logger.info(
168+
f"Added an input converter (EDM4hep to LCIO) to the {next_alg.getName()} algorithm"
169+
)
170+
171+
if not self._lcio_input:
172+
# We need to convert the input to LCIO from EDM4hep. We attach this
173+
# to the first wrapped processor that does NOT have another converter
174+
# configured
175+
for alg in self._alg_list:
176+
if _is_wrapped_proc_without_conv(alg, "EDM4hep", "Lcio"):
177+
input_conv = EDM4hep2LcioTool("InputConversion")
178+
input_conv.convertAll = True
179+
input_conv.collNameMapping = {"MCParticles": "MCParticle"}
180+
alg.EDM4hep2LcioTool = input_conv
181+
logger.info(
182+
f"Added an input converter (EDM4hep to LCIO) to the {alg.getName()} algorithm"
183+
)
184+
break
185+
186+
if self._edm4hep_output:
187+
# We need to convert to EDM4hep. We attach the converter to the last
188+
# wrapped processor that does not have another converter attached
189+
for alg in reversed(self._alg_list):
190+
if _is_wrapped_proc_without_conv(alg, "Lcio", "EDM4hep"):
191+
output_conv = Lcio2EDM4hepTool("OutputConverter")
192+
output_conv.convertAll = True
193+
output_conv.collNameMapping = {"MCParticle": "MCParticles"}
194+
alg.Lcio2EDM4hepTool = output_conv
195+
logger.info(
196+
f"Added an output converter (LCIO to EDM4hep) to the {alg.getName()} algorithm"
197+
)
198+
break

test/CMakeLists.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ find_package(Marlin REQUIRED)
4343
add_library(MarlinTestProcessors SHARED
4444
src/TrivialMCTruthLinkerProcessor.cc
4545
src/MarlinMCRecoLinkChecker.cc
46+
src/PseudoRecoProcessor.cc
4647
)
4748
target_link_libraries(MarlinTestProcessors PUBLIC ${Marlin_LIBRARIES})
4849
target_include_directories(MarlinTestProcessors PUBLIC ${Marlin_INCLUDE_DIRS})
@@ -139,6 +140,42 @@ ExternalData_Add_Test( marlinwrapper_tests
139140
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_link_conversion_edm4hep.py --use-gaudi-algorithm --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/ttbar_20240223_edm4hep.root}
140141
)
141142

143+
# ---- IO Helpers tests
144+
ExternalData_Add_Test( marlinwrapper_tests
145+
NAME io_helpers_lcio_marlin_only
146+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/muons.slcio} --output-type lcio --outputbase lcio_marlin_only
147+
)
148+
149+
ExternalData_Add_Test( marlinwrapper_tests
150+
NAME io_helpers_edm4hep_marlin_only
151+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/ttbar_20240223_edm4hep.root} --output-type edm4hep --outputbase edm4hep_marlin_only
152+
)
153+
154+
ExternalData_Add_Test( marlinwrapper_tests
155+
NAME io_helpers_lcio_in_edm4hep_out_marlin_only
156+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/muons.slcio} --output-type edm4hep --outputbase lcio_in_edm4hep_out_marlin_only
157+
)
158+
159+
ExternalData_Add_Test( marlinwrapper_tests
160+
NAME io_helpers_edm4hep_in_lcio_out_marlin_only
161+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/ttbar_20240223_edm4hep.root} --output-type lcio --outputbase edm4hep_in_lcio_out_marlin_only
162+
)
163+
164+
ExternalData_Add_Test( marlinwrapper_tests
165+
NAME io_helpers_lcio_marlin_gaudi
166+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/muons.slcio} --with-gaudi-algorithm --output-type lcio --outputbase lcio_marlin_gaudi
167+
)
168+
169+
ExternalData_Add_Test( marlinwrapper_tests
170+
NAME io_helpers_edm4hep_marlin_gaudi
171+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/ttbar_20240223_edm4hep.root} --with-gaudi-algorithm --output-type edm4hep --outputbase edm4hep_marlin_gaudi
172+
)
173+
174+
ExternalData_Add_Test( marlinwrapper_tests
175+
NAME io_helpers_edm4hep_both_out_marlin_gaudi
176+
COMMAND ${K4RUN} ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/test_io_utilities.py --inputfile DATA{${PROJECT_SOURCE_DIR}/test/input_files/ttbar_20240223_edm4hep.root} --with-gaudi-algorithm --output-type both --outputbase edm4hep_both_out_marlin_gaudi
177+
)
178+
142179
add_test( event_header_conversion bash -c "k4run ${CMAKE_CURRENT_SOURCE_DIR}/gaudi_opts/createEventHeader.py && anajob test.slcio | grep 'EVENT: 42'" )
143180

144181
ExternalData_Add_Target(marlinwrapper_tests)

0 commit comments

Comments
 (0)