-
Notifications
You must be signed in to change notification settings - Fork 67
PoC for consistent collection filtering #970
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
Draft
paulgessinger
wants to merge
14
commits into
AIDASoft:master
Choose a base branch
from
paulgessinger:feat/filtering
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ee81457
poc single collection
paulgessinger 7d4990f
poc one to many
paulgessinger 4918502
poc multi
paulgessinger 6011228
poc links
paulgessinger d34e079
poc framefilter
paulgessinger 2361a4c
copy vector members
paulgessinger be3f04b
centralize filter hook registry
paulgessinger 6851b09
support for interfaces
paulgessinger 3902d08
subset handling
paulgessinger 7c70af1
doc
paulgessinger f3bf1fa
drop helper
paulgessinger 9b5e605
span over vector
paulgessinger ffefa07
drop omits collection
paulgessinger 2179abe
drop unneeded unit test
paulgessinger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| # Filtering Frames | ||
|
|
||
| `podio::FrameFilter` produces a new `Frame` from an existing one by selecting a | ||
| subset of the objects in its collections and **rewiring all relations** so that | ||
| the result is internally consistent. It is the right tool when you want to drop | ||
| some objects (e.g. low-energy hits, or MC particles outside a region of | ||
| interest) while keeping the surviving relation graph valid. | ||
|
|
||
| Filtering is inherently a *Frame-level* operation: because relations cross | ||
| collections, dropping an object in one collection can leave dangling references | ||
| in another. `FrameFilter` therefore rebuilds every collection of the input | ||
| `Frame` and returns a new, fully-rewired `Frame`. | ||
|
|
||
| ## Basic usage | ||
|
|
||
| ```cpp | ||
| #include "podio/FrameFilter.h" | ||
|
|
||
| podio::Frame out = podio::FrameFilter{inFrame} | ||
| .keep("MCParticles", [](const ExampleMC& p) { return p.energy() > 1.0; }) | ||
| .keepReferenced("hits") | ||
| .cascade("ExampleCluster.Hits") | ||
| .run(); | ||
| ``` | ||
|
|
||
| - `keep(name, predicate)` — keep only the objects of the named collection for | ||
| which `predicate` returns `true`. The collection type is deduced from the | ||
| predicate's argument type. | ||
| - `keepReferenced(name)` — keep an object of the named collection only if some | ||
| surviving object still references it (an *orphan sweep*). | ||
| - `drop(name)` — remove the named collection entirely; it is omitted from the | ||
| output Frame. Unlike `keep`, this needs only the name, not the collection's | ||
| type. (To keep an empty collection instead, use an always-false `keep` | ||
| predicate.) | ||
| - `cascade("Type.relation")` — mark a relation as mandatory (see below). | ||
| - `run()` — execute and return the new `Frame`. | ||
|
|
||
| Collections of the input `Frame` that are not mentioned are kept in full, but | ||
| are still rebuilt so their relations into filtered collections are corrected. | ||
| Frame parameters are copied over verbatim. | ||
|
|
||
| ## The two axes | ||
|
|
||
| The behaviour is controlled by two orthogonal axes. | ||
|
|
||
| ### Retention mode (per collection) | ||
|
|
||
| How an object earns its place in the output: | ||
|
|
||
| - **keep-all** (default) — every object survives. | ||
| - **predicate** (`keep`) — an object survives iff the predicate holds. | ||
| - **drop-all** (`drop`) — no object survives *and* the collection is omitted from | ||
| the output Frame. Type-free, since it never inspects an object. (An always-false | ||
| `keep` predicate is the variant that leaves an empty collection in place.) | ||
| - **keep-if-referenced** (`keepReferenced`) — an object survives iff a surviving | ||
| object references it. This is an orphan sweep, computed as a mark-and-sweep | ||
| reachability from the surviving *roots* (the predicate/keep-all survivors). | ||
|
|
||
| Orphan sweep is shared-aware: if two collections reference the same `hit` and | ||
| only one of the referrers survives, the `hit` is kept. That is precisely why | ||
| `keepReferenced` is preferred over a "containment cascade" (deleting an object | ||
| deletes everything it points at) — the latter would wrongly drop objects that | ||
| are still shared with a survivor. | ||
|
|
||
| ### Edge policy (per relation) | ||
|
|
||
| What happens to a single relation whose target was removed: | ||
|
|
||
| - **leave-unconnected** (default) — drop the dangling edge. A | ||
| `OneToManyRelation` simply omits the removed target; a `OneToOneRelation` is | ||
| left unset. | ||
| - **cascade** (`cascade("Type.relation")`) — treat the relation as mandatory: | ||
| if the target is removed, remove the object that holds the relation as well | ||
| (*integrity cascade*). This propagates transitively across collections. | ||
|
|
||
| A relation is named `<value-type-name>.<relation-name>`, e.g. | ||
| `ExampleCluster.Hits` or `ExampleMC.daughters`. For links the value type name is | ||
| the templated `podio::Link<From, To>` name, so a link endpoint is cascaded as | ||
| e.g. `cascade(std::string(MyLinkCollection::valueTypeName) + ".to")`. | ||
|
|
||
| ## How `run()` works | ||
|
|
||
| The orchestration is type-agnostic and proceeds in phases: | ||
|
|
||
| 1. **Seed** — apply the predicates to mark the initially-removed objects. | ||
| 2. **Cascade closure** — for every relation flagged `cascade`, if its target is | ||
| removed, remove the holder; iterate to a fixpoint (cycle-safe). | ||
| 3. **Mark** — forward reachability from the surviving roots, marking the objects | ||
| that keep-referenced collections should retain. | ||
| 4. **Sweep** — remove unmarked objects from keep-referenced collections. | ||
| 5. **Subset fix-up** — a subset collection only references objects owned | ||
| elsewhere, so drop any entry whose target was removed. | ||
| 6. **Clone & remap** — clone the surviving objects (data members only) into new | ||
| collections and record an `ObjectID → new location` map. | ||
| 7. **Rewire** — walk every relation of every survivor and re-point it through | ||
| the remap; targets that were removed are dropped (or already cascaded away). | ||
| 8. **Assemble** — put the rebuilt collections into a new `Frame` and copy the | ||
| parameters. Collections marked `drop` are skipped here, so they do not appear | ||
| in the output `Frame` at all. | ||
|
|
||
| ## Design: a generic core plus generated hooks | ||
|
|
||
| The orchestration above is entirely type-erased — `FrameFilter` only ever sees | ||
| `podio::CollectionBase`. The parts that genuinely need concrete type knowledge | ||
| are generated once per datatype and registered, mirroring the | ||
| `CollectionBufferFactory` pattern: | ||
|
|
||
| - `podio::detail::FilterHooks` (in `podio/detail/FilterHooks.h`) is the | ||
| per-datatype interface with three operations: | ||
| - `edges` — report an object's outgoing relation targets as `ObjectID`s | ||
| (used by the cascade and mark phases). | ||
| - `cloneSurvivors` — clone the surviving objects into a fresh collection, | ||
| copying data members and vector members but no relations. | ||
| - `rewire` — re-point the cloned survivors' relations through the remap. | ||
| - `podio::detail::FilterHookRegistry` is the singleton (one per process, living | ||
| in the core library) that maps a datatype name to its hooks. Generated code | ||
| registers into it at library-load time. | ||
| - `podio::detail::resolveTarget<CollT>` looks a remapped target up and returns | ||
| the concrete handle to hand to a setter/adder. | ||
|
|
||
| For each datatype and link the generator emits a `<Type>FilterHooks.cc` that | ||
| implements and registers these hooks. The implementation is uniform: each | ||
| relation carries a *resolver expression*, so the generated code does not branch | ||
| on whether a relation is concrete or an interface. | ||
|
|
||
| ### Interface relations | ||
|
|
||
| A relation to an interface type is polymorphic — its target may live in any of | ||
| the interface's member collections. Because the remap is keyed by `ObjectID` | ||
| (type-agnostic), the target can always be located; recovering the concrete type | ||
| to build the interface handle is done by a generated resolver, one per | ||
| interface: | ||
|
|
||
| ```cpp | ||
| std::optional<EnergyInterface> | ||
| EnergyInterface::resolveFilteredTarget(const podio::detail::FilterRemap& remap, podio::ObjectID id); | ||
| ``` | ||
|
|
||
| It dispatches on the target collection's `getValueTypeName()` over the | ||
| interface's member types and constructs the interface from the concrete handle. | ||
| It is declared on the interface (in the generated interface header) and defined | ||
| once in a generated `<Interface>FilterResolver.cc`, so every relation that uses | ||
| the interface shares a single resolver. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Interface relations are resolved by a linear match over the interface's member | ||
| types (one comparison per candidate concrete type). | ||
| - Subset *link* collections are not handled; link collections are assumed to own | ||
| their endpoints. | ||
| - A subset collection's synthetic membership edge has no relation name, so it | ||
| cannot be targeted by `cascade(...)`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 confused me for a bit. There are two ways we can end up with empty collections after the filter has run. Either we have filtered out all elements of a collection, or we started the filtering with an empty collection. Not sure if this needs to be clarified in the end, I just wanted to not I stumbled over this while reading.