Skip to content

Commit

Permalink
Merge branch 'main' into eventHeader
Browse files Browse the repository at this point in the history
  • Loading branch information
BrieucF authored Aug 16, 2023
2 parents b5e4aef + 99d036b commit e03d061
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### File copied from https://github.com/key4hep/key4hep-dev-utils
### DO NOT EDIT, CHANGES WILL BE OVERWRITTEN

### C++ ###
# Prerequisites
*.d
Expand Down Expand Up @@ -238,6 +241,8 @@ podio_generated_files.cmake
/python/edm4hep/__version__.py
edm4hep/edm4hep/
edm4hep/src/
## k4FWCore
test/k4FWCoreTest/**/*.root
## k4MarlinWrapper
test/inputFiles/*.slcio
test/gaudi_opts/testConverterConstants.py
Expand Down
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# k4FWCore (key4hep FrameWork Core)

k4FWCore is a Gaudi package that provides the PodioDataService, that allows to
k4FWCore is a Gaudi package that provides the PodioDataService, which allows to
use podio-based event data models like EDM4hep in Gaudi workflows.

k4FWCore also provides the `k4run` script used to run Gaudi steering files.

## Components

### Basic I/O
Expand All @@ -13,12 +15,39 @@ Component wrapping the PodioDataService to handle PODIO types and collections.

#### PodioInput

Algorithm to read data from input file(s) on disk.
Algorithm to read data from one or multiple input file(s) on disk.

#### PodioOutput

Algorithm to write data to output file on disk.
Algorithm to write data to an output file on disk.

## k4run
```bash
$ k4run --help
usage: k4run [-h] [--dry-run] [-v] [-n NUM_EVENTS] [-l] [--gdb] [--ncpus NCPUS] [config_files ...]

Run job in the Key4HEP framework

positional arguments:
config_files Gaudi config (python) files describing the job

options:
-h, --help show this help message and exit
--dry-run Do not actually run the job, just parse the config files
-v, --verbose Run job with verbose output
-n NUM_EVENTS, --num-events NUM_EVENTS
Number of events to run
-l, --list Print all the configurable components available in the framework and exit
--gdb Attach gdb debugger
--ncpus NCPUS Start Gaudi in parallel mode using NCPUS processes. 0 => serial mode (default), -1 => use all CPUs
```
When supplied with a Gaudi steering file `k4run --help file.py` also shows the settable properties of the Gaudi algorithms used in the file. Additionally, it is possible to add further arguments and use them in the steering file by using the Python `argparse.ArgumentParser` shared by `k4run`.
```python
from k4FWCore.parseArgs import parser
parser.add_argument("-f", "--foo", type=int, help="hello world")
my_opts = parser.parse_known_args()
print(my_opts[0].foo)
```

## Dependencies

Expand All @@ -30,7 +59,7 @@ Algorithm to write data to output file on disk.

## Installation and downstream usage.

k4FWCore is a cmake project. After setting up the dependencies (use for example `source /cvmfs/sw.hsf.org/key4hep/setup.sh`)
k4FWCore is a CMake project. After setting up the dependencies (use for example `source /cvmfs/sw.hsf.org/key4hep/setup.sh`)


```
Expand Down
8 changes: 8 additions & 0 deletions k4FWCore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ find_package(podio 0.16.3 REQUIRED)
find_package(EDM4HEP)

gaudi_install(SCRIPTS)
gaudi_install(PYTHON)


gaudi_add_library(k4FWCore
Expand All @@ -32,3 +33,10 @@ install(TARGETS k4FWCore k4FWCorePlugins
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT shlib
COMPONENT dev)

# Copy python parsing file to genConfDir in Gaudi
add_custom_command(
TARGET k4FWCore POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${PROJECT_SOURCE_DIR}/k4FWCore/python/k4FWCore/parseArgs.py
${CMAKE_CURRENT_BINARY_DIR}/genConfDir/k4FWCore/parseArgs.py)

3 changes: 3 additions & 0 deletions k4FWCore/python/k4FWCore/parseArgs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import argparse

parser = argparse.ArgumentParser(description="Run job in the Key4HEP framework", add_help=False)
119 changes: 59 additions & 60 deletions k4FWCore/scripts/k4run
Original file line number Diff line number Diff line change
Expand Up @@ -22,65 +22,48 @@ FILTER_GAUDI_PROPS = [ "ContextService", "Cardinality", "Context", "CounterList"
seen_files = set()
option_db = {}


# There is no way of knowing if parse_known_args() or parse_args() was called
# so we'll track wether this is the first parsing or not
first_run = True
class LoadFromFile(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
global first_run
if not values:
if not first_run:
print('Error: missing gaudi options file.\n'
'Usage: k4run <options_file.py>, use --help to get a complete list of arguments')
sys.exit(1)
first_run = False
return
first_run = False

for wrapper in values:
if wrapper.name in seen_files:
return
seen_files.add(wrapper.name)
exec(open(wrapper.name).read(), globals())
# loop over all components that were configured in the options file
# use dict.fromkeys so that the confs are sorted (Python >= 3.7)
for conf in dict.fromkeys(ApplicationMgr.allConfigurables.values()):
# skip public tools and the applicationmgr itself
if "ToolSvc" in conf.name() or "ApplicationMgr" in conf.name():
continue
props = conf.getPropertiesWithDescription() #dict propertyname: (propertyvalue, propertydescription)
for prop in props:
# only add arguments for relevant properties
if prop in FILTER_GAUDI_PROPS or "Audit" in prop or hasattr(props[prop][0], '__slots__'):
def load_file(file):
exec(file.read())


def add_arguments(parser, app_mgr):
configurables = app_mgr.allConfigurables.values()
for conf in configurables:
# skip public tools and the applicationmgr itself
if "ToolSvc" in conf.name() or "ApplicationMgr" in conf.name():
continue
props = conf.getPropertiesWithDescription() #dict propertyname: (propertyvalue, propertydescription)
for prop in props:
# only add arguments for relevant properties
if prop in FILTER_GAUDI_PROPS or "Audit" in prop or hasattr(props[prop][0], '__slots__'):
continue
propvalue = props[prop][0]

# if it is set to "no value" it hasn't been touched in the options file
if propvalue == conf.propertyNoValue:
propvalue = conf.getDefaultProperty(prop) # thus get the default value
proptype = type(props[prop][0])
# if the property is a list of something, we need to set argparse nargs to '+'
propnargs = "?"
if proptype == list:
# tricky edgecase: if the default is an empty list there is no way to get the type
if len(propvalue) == 0:
# just skip for now
#print("Warning: argparse cannot deduce type for property %s of %s. Needs to be set in options file." % (prop, conf.name()))
continue
propvalue = props[prop][0]

# if it is set to "no value" it hasn't been touched in the options file
if propvalue == conf.propertyNoValue:
propvalue = conf.getDefaultProperty(prop) # thus get the default value
proptype = type(props[prop][0])
# if the property is a list of something, we need to set argparse nargs to '+'
propnargs = "?"
if proptype == list:
# tricky edgecase: if the default is an empty list there is no way to get the type
if len(propvalue) == 0:
# just skip for now
#print("Warning: argparse cannot deduce type for property %s of %s. Needs to be set in options file." % (prop, conf.name()))
continue
else:
# deduce type from first item of the list
proptype = type(propvalue[0])
propnargs = "+"

# add the argument twice, once as "--PodioOutput.filename"
# and once as "--filename.PodioOutput"
propName = conf.name() + '.' + prop
propNameReversed = prop + '.' + conf.name()
option_db[propName] = (conf, propName)
parser.add_argument( "--%s" % propName, "--%s" % propNameReversed, type=proptype, help=props[prop][1],
nargs=propnargs,
default=propvalue)
else:
# deduce type from first item of the list
proptype = type(propvalue[0])
propnargs = "+"

# add the argument twice, once as "--PodioOutput.filename"
# and once as "--filename.PodioOutput"
propName = conf.name() + '.' + prop
propNameReversed = prop + '.' + conf.name()
option_db[propName] = (conf, propName)
parser.add_argument( f"--{propName}", f"--{propNameReversed}", type=proptype, help=props[prop][1],
nargs=propnargs,
default=propvalue)


if __name__ == "__main__":
Expand All @@ -95,8 +78,8 @@ if __name__ == "__main__":
handler.setFormatter(formatter)
logger.addHandler(handler)

parser = argparse.ArgumentParser(description="Run job in the Key4HEP framework")
parser.add_argument("config_files", type=open, action=LoadFromFile, nargs="*",
from k4FWCore.parseArgs import parser
parser.add_argument("config_files", type=open, action="store", nargs="*",
help="Gaudi config (python) files describing the job")
parser.add_argument("--dry-run", action="store_true",
help="Do not actually run the job, just parse the config files")
Expand Down Expand Up @@ -139,8 +122,23 @@ if __name__ == "__main__":
print(" %s (from %s)" % (item, cfgDb[item]["lib"]))
sys.exit()

for file in opts[0].config_files:
load_file(file)

# ApplicationMgr is a singleton
from Configurables import ApplicationMgr
add_arguments(parser, ApplicationMgr())

# add help manually here, if it is added earlier the parser exits after the first parse_arguments call
parser.add_argument("-h", "--help", action="help", default=argparse.SUPPRESS, help="show this help message and exit")

opts = parser.parse_args()

if len(opts.config_files) == 0:
print('Error: missing gaudi options file.\n'
'Usage: k4run <options_file.py>, use --help to get a complete list of arguments')
sys.exit(1)

# print a doc line showing the configured algorithms
logger.info(' '.join(f'--> {alg.name()}' for alg in ApplicationMgr().TopAlg))

Expand All @@ -150,6 +148,7 @@ if __name__ == "__main__":
propTuple[0].setProp(propTuple[1].rsplit(".",1)[1], opts_dict[optionName])

if opts.verbose:
from Gaudi.Configuration import VERBOSE
ApplicationMgr().OutputLevel = VERBOSE
if opts.num_events is not None:
ApplicationMgr().EvtMax = opts.num_events
Expand Down
22 changes: 22 additions & 0 deletions test/k4FWCoreTest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,25 @@ add_test(NAME TestEventHeaderFiller
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMAND ${K4RUN} options/cfillEventHeader.py)
set_test_env(TestEventHeaderFiller)

add_test(NAME Testk4runCustomArguments
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMAND ${K4RUN} --foo=42 options/TestArgs.py)
set_test_env(Testk4runCustomArguments)
set_tests_properties(Testk4runCustomArguments
PROPERTIES PASS_REGULAR_EXPRESSION "The answer is 42")

add_test(NAME Testk4runVerboseOutput
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMAND ${K4RUN} --verbose options/TestArgs.py)
set_test_env(Testk4runVerboseOutput)
set_tests_properties(Testk4runVerboseOutput
PROPERTIES PASS_REGULAR_EXPRESSION " VERBOSE ")

add_test(NAME Testk4runHelpOnly
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMAND ${K4RUN} --help)
set_test_env(Testk4runHelpOnly)
set_tests_properties(Testk4runHelpOnly
PROPERTIES PASS_REGULAR_EXPRESSION "show this help message and exit")

5 changes: 5 additions & 0 deletions test/k4FWCoreTest/options/TestArgs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from k4FWCore.parseArgs import parser
parser.add_argument("-f", "--foo", type=int, help="hello world")
my_opts = parser.parse_known_args()

print(f"The answer is {my_opts[0].foo}")

0 comments on commit e03d061

Please sign in to comment.