-
Notifications
You must be signed in to change notification settings - Fork 32
Add Gaudi Functional C++ Class Generator #372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This script generates Gaudi Functional C++ classes with appropriate structure and boilerplate code based on user-defined specifications.
Updated the Gaudi Functional C++ Class Generator to support k4FWCore framework, improved input/output specifications, and added new functionalities for property parsing and class generation.
Added command_line parameter to generate_class function and updated its usage in main.
|
@BrieucF - please, check. Thanks! |
|
Without any tests, inevitably this will not work in the future if something changes and we won't be able to know. |
Thanks for looking into it. A very good point! Shall I add the generation and compilation for it to CI tests? Thanks |
|
@tmadlener weren't you also working on something like this? :) |
|
Yes, sort of. I have a semi-working thing for a static website. But that also doesn't have the tests it should have. In the end, I don't care too much which version lands as long as one does. |
| class_keyword = "struct" if not use_class else "class" | ||
| public_keyword = "" if not use_class else "public:\n" | ||
|
|
||
| code = f"""// Generated by Gaudi Functional C++ Class Generator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use textwrap.dedent
https://docs.python.org/3/library/textwrap.html
| help='Namespace for the class') | ||
| parser.add_argument('-f', '--output-file', | ||
| help='Output file name (default: <ClassName>.cpp)') | ||
| parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the gaudi framework option in k4fwcore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the gaudi framework option in k4fwcore?
@andresailer - thanks for reviewing the PR! I thought since the README mentions the Gaudi tutorial it might be useful :-)
tmadlener
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this. I think the general idea of the script goes into the exact right direction, i.e. we want something that generates boilerplate for something that the user wants. I didn't look in extreme detail, but I see a few general improvements (also partially highlighted in the inline comments):
- Currently the user needs to know which functional type they want. But this is entirely specified by the number of inputs and outputs, so I would just determine that from there.
- Generally the implementation could probably be improved by sprinkling a few classes into the whole thing for holding intermediate information instead of passing around tuples of strings of various lengths. This would probably also make it easier to simply do some of the parsing / processing up-front and then pass the information around instead of re-parsing it several times.
- The current implementation does not handle the possibility of variable length inputs / outputs (I think). This is only possible in
k4FWCoreFunctional algorithms though.
I think this is borderline complex enough to warrant the use of a template engine to handle all the string formatting. It introduces some overhead, and would require writing some templates, but it would simplify the python script parts by quite potentially.
As already mentioned we definitely need tests that ensure that the outputs compile. That would probably be the first thing I do, because once that is in place refactoring and extending the implementation can be done with some guard rails.
| return help_text | ||
|
|
||
|
|
||
| def parse_data_spec(spec: str) -> Tuple[str, str]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called all over the place to do the same things again and again at the top of several functions. Why not call this once at the entry point (generate_class) and pass the outputs of this to all the functions that need it?
| if len(out_types) == 0: | ||
| return "void()" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case can never happen as this is already ruled out in the parsing of the arguments. Also, I think even if this were to happen, it doesn't compile because a Producer needs at least one output.
| if functional_type == 'consumer': | ||
| in_sig = ', '.join([f"const {t}&" for t in in_types]) | ||
| return f"void({in_sig})" | ||
| elif functional_type == 'producer': | ||
| if len(out_types) == 0: | ||
| return "void()" | ||
| elif len(out_types) == 1: | ||
| return f"{out_types[0]}()" | ||
| else: | ||
| return f"std::tuple<{', '.join(out_types)}>()" | ||
| elif functional_type == 'transformer': | ||
| in_sig = ', '.join([f"const {t}&" for t in in_types]) if in_types else "" | ||
| if len(out_types) == 1: | ||
| return f"{out_types[0]}({in_sig})" | ||
| else: | ||
| out_sig = ', '.join(out_types) | ||
| return f"std::tuple<{out_sig}>({in_sig})" | ||
| elif functional_type == 'filter': | ||
| in_sig = ', '.join([f"const {t}&" for t in in_types]) | ||
| return f"bool({in_sig})" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified. We don't need to differentiate many things based on which functional type we have, as there are a lot of commonalities. The main differentiation points are the number of inputs and the number of outputs (as also visible from the repetition in the code).
| # Remove Collection suffix and namespace | ||
| clean_name = typ.split('::')[-1].replace('Collection', '') | ||
| loc = clean_name | ||
| lines.append(f'KeyValues("{loc}", {{"{loc}"}})') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think from the current implementation there is no way we will ever need the KeyValues, but we always want the KeyValue. Unless the current version already handles inputs/outputs of type std::vector<const XYZCollection*> in which case there should be a differentiation here and only those get a KeyValues (of the appropriate length), single inputs should get a KeyValue.
| if functional_type == 'consumer': | ||
| params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) | ||
| return f"void operator()({params}) const override" | ||
| elif functional_type == 'producer': | ||
| if len(out_types) == 0: | ||
| return "void operator()() const override" | ||
| elif len(out_types) == 1: | ||
| return f"{out_types[0]} operator()() const override" | ||
| else: | ||
| return f"std::tuple<{', '.join(out_types)}> operator()() const override" | ||
| elif functional_type == 'transformer': | ||
| params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) | ||
| if len(out_types) == 1: | ||
| return f"{out_types[0]} operator()({params}) const override" | ||
| else: | ||
| return f"std::tuple<{', '.join(out_types)}> operator()({params}) const override" | ||
| elif functional_type == 'filter': | ||
| params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) | ||
| return f"bool operator()({params}) const override" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A similar argument as above applies here. Many things are common and are specialized on the number of inputs / outputs rather than on the type of the algorithm.
| if 'edm4hep::' in typ: | ||
| # Extract all collection types (handle nested templates) | ||
| # Match patterns like edm4hep::MCParticleCollection | ||
| pattern = r'edm4hep::(\w+Collection)' | ||
| matches = re.findall(pattern, typ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this differentiation is necessary at all. Algorithms will always take Collections as inputs/outputs. (I think trying to do anything different will not even compile).
| elif functional_type == 'transformer': | ||
| in_sig = ', '.join([f"const {t}&" for t in in_types]) if in_types else "" | ||
| if len(out_types) == 1: | ||
| return f"{out_types[0]}({in_sig})" | ||
| else: | ||
| out_sig = ', '.join(out_types) | ||
| return f"std::tuple<{out_sig}>({in_sig})" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this compiles for multiple outputs. It's either a Transformer (single output) or a MultiTransformer multiple outputs so depending on that we also need to generate a different class here.
BEGINRELEASENOTES
ENDRELEASENOTES
Gaudi Functional C++ Class Generator
This script generates Gaudi Functional C++ classes with appropriate structure and boilerplate code based on user-defined specifications. It supports both Gaudi::Functional and k4FWCore frameworks.
Features
Consumer,Producer,Transformer,FilterPredicatepodio::UserDataCollection<float>structby default, Gaudi usesclassby default-hto see all functional types and examplesInput Format
For inputs/outputs:
TypeName:LocationNamepodio::UserDataCollection<float>)For properties:
Type:Name:DefaultValue:DescriptionExamples
k4FWCore Producer (reproduces ExampleFunctionalProducerMultiple)
k4FWCore Consumer
python gaudi_gen.py MyConsumer consumer \ -i 'edm4hep::MCParticleCollection:MCParticles'k4FWCore Transformer
Gaudi Framework Examples
Command-Line Options