Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
409dfe3
Implement timeframe splitting support
11kumaoka Aug 4, 2026
1d2f7fc
Fix splitting plugin compiler warnings
11kumaoka Aug 4, 2026
9a01c5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 4, 2026
b73d352
fix: iwyu [skip ci]
github-actions[bot] Aug 4, 2026
1437f6f
fix copilot review about optimize association loop
11kumaoka Aug 4, 2026
ccbd1a8
fix: cache timeframe association indices per parent
11kumaoka Aug 4, 2026
a711130
refactor: separate timeframe splitting and time alignment implementat…
11kumaoka Aug 10, 2026
7311db9
ci: add timeframe splitting runtime test
11kumaoka Aug 9, 2026
c828d72
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
49fee5e
fix: preserve timeframe splitting levels for silicon trackers
11kumaoka Aug 10, 2026
0e839da
Add timeframe splitting framework to EICrecon (fix: iwyu) (#2849)
epic-capybara Aug 10, 2026
af6d50f
Potential fix for pull request finding
11kumaoka Aug 10, 2026
c90ea2c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
4520646
Merge remote-tracking branch 'upstream/main' into timeframe_splitting…
11kumaoka Aug 11, 2026
2a3564a
Merge remote-tracking branch 'upstream/main' into timeframe_splitting…
11kumaoka Aug 11, 2026
6fcd19e
refactor: apply algorithm/factory way for hit time alignment module a…
11kumaoka Aug 11, 2026
d2ce953
Merge remote-tracking branch 'upstream/main' into timeframe_splitting…
11kumaoka Aug 11, 2026
610d2fa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 11, 2026
c1f5fc7
fix: skip splitting plugin on JANA2 < 2026
wdconinc Aug 12, 2026
a5fd275
fix(ci): change minq2 value 1000 => 100
wdconinc Aug 12, 2026
4e373f9
Add timeframe splitting framework to EICrecon (fix: iwyu) (#2864)
epic-capybara Aug 12, 2026
8c5ca6e
fix(ci): switch to 18x275 with full geometry (incl. calorimetry)
wdconinc Aug 12, 2026
e2695c7
fix: update timeframe splitting CI and PODIO handling
11kumaoka Aug 12, 2026
ebe13a5
Merge remote-tracking branch 'upstream/main' into timeframe_splitting…
11kumaoka Aug 12, 2026
72a8245
refactor: move hit time alignment to event building
11kumaoka Aug 13, 2026
a8ecb29
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2026
ca90e3e
Merge remote-tracking branch 'upstream/main' into timeframe_splitting…
11kumaoka Aug 13, 2026
04428f0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 13, 2026
67292c3
Clarify timeframe scan loop termination
11kumaoka Aug 13, 2026
9bdd943
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 13, 2026
9d38152
Add timeframe splitting framework to EICrecon (fix: iwyu) (#2868)
epic-capybara Aug 13, 2026
9427b86
Refactor based on PR comments
11kumaoka Aug 14, 2026
47582e6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 14, 2026
16ca933
Fix signedness of grid multiplicity thresholds
11kumaoka Aug 14, 2026
34e9d69
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 14, 2026
d7c313d
Add timeframe splitting framework to EICrecon (fix: iwyu) (#2873)
epic-capybara Aug 15, 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
88 changes: 88 additions & 0 deletions .github/workflows/linux-eic-shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,94 @@ jobs:
if-no-files-found: error
include-hidden-files: true

eicrecon-timeframe-splitting:
runs-on: ubuntu-24.04
needs:
- build
- npsim-dis
- npsim-minbias

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused?

strategy:
matrix:
include:
- CXX: clang++
beam: 18x275
minq2: 100
detector_config: craterlake_18x275
sanitizer: ASAN

- CXX: clang++
beam: 18x275
minq2: 100
detector_config: craterlake_18x275

sanitizer: TSAN
nthreads: 4

steps:
- name: Checkout .github and scripts
uses: actions/checkout@v7
with:
sparse-checkout: |
.github
src/scripts

- name: Download install directory
uses: actions/download-artifact@v8
with:
name: install-${{ matrix.CXX }}-eic-shell-Release-${{ env.platform }}-${{ env.release }}-${{ matrix.sanitizer }}

- name: Unarchive install directory
run: tar -xaf install.tar.zst

- name: Download simulation input
uses: actions/download-artifact@v8
with:
name: sim_dis_${{ matrix.beam }}_minQ2=${{ matrix.minq2 }}_${{ matrix.detector_config }}.edm4hep.rnt.root

- name: Setup cvmfs
uses: cvmfs-contrib/github-action-cvmfs@v5

- name: Run EICrecon with timeframe splitting
uses: eic/run-cvmfs-osg-eic-shell@main
with:
organization: "${{ env.organization }}"
platform-release: "${{ env.platform }}:${{ env.release }}"
setup: "/opt/detector/epic-${{ env.detector-version }}/bin/thisepic.sh"
run: |
echo "::add-matcher::${{github.workspace}}/.github/ubsan.json"
echo "::add-matcher::${{github.workspace}}/.github/eicrecon.json"

export DETECTOR_CONFIG=${DETECTOR}_${{ matrix.detector_config }}
export LD_LIBRARY_PATH=$PWD/install/lib:$LD_LIBRARY_PATH
export JANA_PLUGIN_PATH=$PWD/install/lib/EICrecon/plugins:/usr/local/plugins

prmon --json-summary timeframe_splitter_${{matrix.sanitizer}}.prmon.json -- \
$PWD/install/bin/eicrecon \
${{env.JANA_OPTIONS}} \
${{matrix.nthreads > 1 && format('-Pnthreads={0}', matrix.nthreads) || ''}} \
-Ppodio:output_file=timeframe_splitter_${{matrix.sanitizer}}.edm4eic.root \
sim_dis_${{matrix.beam}}_minQ2=${{matrix.minq2}}_${{ matrix.detector_config }}.edm4hep.rnt.root \
-Psplit_timeframes=true

- name: Verify output file
run: |
test -s timeframe_splitter_${{matrix.sanitizer}}.edm4eic.root

- name: Upload output
uses: actions/upload-artifact@v7
with:
name: timeframe_splitter_${{matrix.sanitizer}}.edm4eic.root
path: timeframe_splitter_${{matrix.sanitizer}}.edm4eic.root
if-no-files-found: error

- name: Upload prmon report
uses: actions/upload-artifact@v7
with:
name: timeframe_splitter_${{matrix.sanitizer}}.prmon.json
path: timeframe_splitter_${{matrix.sanitizer}}.prmon.json
if-no-files-found: error


compare-single-multi-threaded:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comparison between single and multi-threaded we should also compare the single and multi-threaded timeframe splitter execution branches.

And we should compare timeframe splitting analysis with the regular physics event reconstruction on the same physics simulation input (single-threaded for now).

runs-on: ubuntu-24.04
needs:
Expand Down
1 change: 1 addition & 0 deletions src/algorithms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ add_subdirectory(digi)
add_subdirectory(reco)
add_subdirectory(fardetectors)
add_subdirectory(onnx)
add_subdirectory(event_building)
8 changes: 8 additions & 0 deletions src/algorithms/event_building/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
set(PLUGIN_NAME "algorithms_event_building")

# Function creates ${PLUGIN_NAME}_plugin and ${PLUGIN_NAME}_library targets
# Setting default includes, libraries and installation paths
plugin_headers_only(${PLUGIN_NAME})

# Find dependencies
plugin_add_algorithms(${PLUGIN_NAME})
64 changes: 64 additions & 0 deletions src/algorithms/event_building/HitTimeAlignment.h
Comment thread
ruse-traveler marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (C) 2026 Takuya Kumaoka

#pragma once

#include <algorithm>
#include <cmath>
#include <string_view>
#include <vector>

#include <algorithms/algorithm.h>

#include "algorithms/interfaces/WithPodConfig.h"
#include "algorithms/event_building/HitTimeAlignmentConfig.h"

namespace eicrecon {

template <typename HitT>
using HitTimeAlignmentAlgorithm =
algorithms::Algorithm<algorithms::Input<const typename HitT::collection_type>,
algorithms::Output<typename HitT::collection_type>>;

template <typename HitT>
class HitTimeAlignment : public HitTimeAlignmentAlgorithm<HitT>,
public WithPodConfig<HitTimeAlignmentConfig> {
public:
using AlgorithmT = HitTimeAlignmentAlgorithm<HitT>;

explicit HitTimeAlignment(std::string_view name)
: AlgorithmT{name,
{"inputHits"},
{"outputHits"},
"Correct reconstructed hit times for propagation and sort by time."} {}

void init() final {}

void process(const typename AlgorithmT::Input& input,
const typename AlgorithmT::Output& output) const final {
const auto [hits_in] = input;
auto [hits_out] = output;

std::vector<typename HitT::mutable_type> sorted_hits;
sorted_hits.reserve(hits_in->size());
for (const auto& hit : *hits_in) {
auto copied_hit = hit.clone();
const auto position = hit.getPosition();
const auto radius = std::sqrt(position[0] * position[0] + position[1] * position[1] +
position[2] * position[2]);
const auto average_time_of_flight = radius * this->m_cfg.reference_inverse_velocity;
copied_hit.setTime(hit.getTime() - average_time_of_flight);
sorted_hits.push_back(copied_hit);
}

std::stable_sort(sorted_hits.begin(), sorted_hits.end(), [](const auto& lhs, const auto& rhs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in case someone has the same thought, the size of a hit here is typically < 64 bytes so a hit fits in an L1 cache line and so there is no benefit to sort an iota to avoid the move costs. Microbenchmark could be useful but not necessary since likely not a major impact on performance here at this point.

return lhs.getTime() < rhs.getTime();
});

for (const auto& hit : sorted_hits) {
hits_out->push_back(hit);
}
}
};

} // namespace eicrecon
13 changes: 13 additions & 0 deletions src/algorithms/event_building/HitTimeAlignmentConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (C) 2026 Takuya Kumaoka

#pragma once

namespace eicrecon {

struct HitTimeAlignmentConfig {
double reference_inverse_velocity =
0.0034; //< ns/mm estimated by MC average time of flight / distance from IP to calorimeter
};

} // namespace eicrecon
9 changes: 7 additions & 2 deletions src/detectors/B0ECAL/B0ECAL.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Copyright (C) 2022 - 2025 Whitney Armstrong, Sylvester Joosten, Chao Peng, David Lawrence, Wouter Deconinck, Kolja Kauder, Nathan Brei, Dmitry Kalinkin, Derek Anderson, Michael Pitt

#include <Evaluator/DD4hepUnits.h>
#include <JANA/JApplication.h>
#include <JANA/JApplicationFwd.h>
#include <JANA/Utils/JEventLevel.h>
#include <JANA/Utils/JTypeInfo.h>
#include <cmath>
#include <string>
Expand All @@ -23,6 +25,9 @@ void InitPlugin(JApplication* app) {
using namespace eicrecon;

InitJANAPlugin(app);
const bool split_timeframes =
app->RegisterParameter<bool>("split_timeframes", false, "Enable timeframe splitting");
const auto hit_level = split_timeframes ? JEventLevel::Timeslice : JEventLevel::PhysicsEvent;

app->Add(new JOmniFactoryGeneratorT<CalorimeterHitDigi_factory>(
"B0ECalRawHits", {"EventHeader", "B0ECalHits"},
Expand All @@ -40,7 +45,7 @@ void InitPlugin(JApplication* app) {
.corrMeanScale = "1.0",
.readout = "B0ECalHits",
},
app));
app, hit_level));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand semantically why the hit_level is attached to the factory and not implemented as a property of the collection. If I understand correctly, this PR does not change the behavior of these factories and it only changes at which level the collections are searched. The way this is now implemented, it adds a lot of overhead (every plugin gets a parameter, so there will be 10s of parameters to specify when you want to change this). Since you already modify the podio services, what about injecting the JEventSource into either the TimeSlice level or the PhysicsEvent level, and pass the level at which collections are stored via the collection tags we use? In the current model (and for clarity's sake hopefully never) you don't need to allow for the same collection names at different levels.

@11kumaoka 11kumaoka Aug 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. The main reason I attached hit_level to the factories is that time splitting needs to be optional. Whether time splitting is enabled or disabled, the factories other than the time-splitting component itself should behave in the same way, without needing to be aware of whether their input collections have gone through time splitting.

The time-splitting mechanism is intended to intercept the normal data flow, so I have also tried to keep the collection names unchanged between the normal and time-splitting workflows.

If we instead use different collection names for the time-splitting workflow, we would effectively need a separate version of almost every detector plugin under src/detectors/ with nearly identical configuration except for the collection names. We used this approach previously, but it made maintenance difficult because changes to the original detector plugins also had to be manually propagated to the time-splitting versions. With Nathan’s recent updates, it became possible to avoid this duplication and use the current approach.

My intention with hit_level was therefore to allow the same factories and collection names to be used in both workflows, while changing only the level from which the collections are retrieved.

Sorry, I may not be able to explain the JANA2-level architecture well enough by myself. I am currently visiting JLab and working with Nathan, so if you have some time, I would appreciate the opportunity to discuss this with you and Nathan together.

In parallel, we will continue to optimize and improve this factory. However, we would also like to get this PR merged before the next week's streaming reconstruction workshop, if possible.

Screenshot 2026-08-11 at 0 10 48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you misunderstood my suggestion. I'm not suggesting you change the names of collections, but use the level at which a collection is defined as something programmatically attached to the collection name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand semantically why the hit_level is attached to the factory and not implemented as a property of the collection.

Inside JANA2, factories are always attached to an EventLevel. It's just implicit when all of your factories are PhysicsEvent-level.

If I understand correctly, this PR does not change the behavior of these factories and it only changes at which level the collections are searched.

That may be so in this case, but JANA2 emphatically does not make that assumption globally. Collections at different levels are not substitutable because they have different semantics from the physicist's perspective, and factories are lazily-evaluated collections. If a collection is missing, JANA2 won't "search upwards" among its parents, and analogously, it won't execute a factory based off of collection name alone.

Take a look at how JANA2 models factories and collections internally:

JEvent ~= (event_nr, event_level, {collection_name : JDatabundle}, {parent_level : JEvent} )
JDatabundle ~= (collection_name, optional collection, optional factory)

The way this is now implemented, it adds a lot of overhead (every plugin gets a parameter, so there will be 10s of parameters to specify when you want to change this).

There is exactly one parameter, split_timeframes. It isn't prefixed with the plugin name. Any plugins that provide factories that need to run upstream of the timeframe splitter should check this parameter. If true, then, for each factory that runs on timeframes instead of physics events (mainly digitization), the user should set the JEventLevel=Timeframe. I don't think this is a huge burden.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is indeed exactly one parameter, it should be defined once, in one place.

app->Add(new JOmniFactoryGeneratorT<CalorimeterHitReco_factory>(
"B0ECalRecHits", {"B0ECalRawHits"}, {"B0ECalRecHits"},
{
Expand All @@ -55,7 +60,7 @@ void InitPlugin(JApplication* app) {
.readout = "B0ECalHits",
.sectorField = "sector",
},
app));
app, hit_level));
app->Add(new JOmniFactoryGeneratorT<CalorimeterTruthClustering_factory>(
"B0ECalTruthProtoClusters", {"B0ECalRecHits", "B0ECalHits"}, {"B0ECalTruthProtoClusters"},
app));
Expand Down
9 changes: 7 additions & 2 deletions src/detectors/B0TRK/B0TRK.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//

#include <Evaluator/DD4hepUnits.h>
#include <JANA/JApplication.h>
#include <JANA/JApplicationFwd.h>
#include <JANA/Utils/JEventLevel.h>
#include <JANA/Utils/JTypeInfo.h>
#include <string>
#include <vector>
Expand All @@ -18,6 +20,9 @@ void InitPlugin(JApplication* app) {
InitJANAPlugin(app);

using namespace eicrecon;
const bool split_timeframes =
app->RegisterParameter<bool>("split_timeframes", false, "Enable timeframe splitting");
const auto hit_level = split_timeframes ? JEventLevel::Timeslice : JEventLevel::PhysicsEvent;

// Digitization
app->Add(new JOmniFactoryGeneratorT<SiliconTrackerDigi_factory>(
Expand All @@ -27,14 +32,14 @@ void InitPlugin(JApplication* app) {
.threshold = 10.0 * dd4hep::keV,
.timeResolution = 8,
},
app));
app, hit_level));

// Convert raw digitized hits into hits with geometry info (ready for tracking)
app->Add(new JOmniFactoryGeneratorT<TrackerHitReconstruction_factory>(
"B0TrackerRecHits", {"B0TrackerRawHits"}, {"B0TrackerRecHits"},
{
.timeResolution = 8,
},
app));
app, hit_level));
}
} // extern "C"
14 changes: 9 additions & 5 deletions src/detectors/BEMC/BEMC.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <Evaluator/DD4hepUnits.h>
#include <JANA/JApplication.h>
#include <JANA/JApplicationFwd.h>
#include <JANA/Utils/JEventLevel.h>
#include <JANA/Utils/JTypeInfo.h>
#include <edm4eic/unit_system.h>
#include <edm4hep/SimCalorimeterHit.h>
Expand Down Expand Up @@ -43,6 +44,9 @@ extern "C" {
void InitPlugin(JApplication* app) {

using namespace eicrecon;
const bool split_timeframes =
app->RegisterParameter<bool>("split_timeframes", false, "Enable timeframe splitting");
const auto hit_level = split_timeframes ? JEventLevel::Timeslice : JEventLevel::PhysicsEvent;

InitJANAPlugin(app);

Expand Down Expand Up @@ -250,7 +254,7 @@ void InitPlugin(JApplication* app) {
.readout = "EcalBarrelScFiHits",
.fields = {"fiber", "z"},
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));
app->Add(new JOmniFactoryGeneratorT<CalorimeterHitReco_factory>(
"EcalBarrelScFiRecHits", {"EcalBarrelScFiRawHits"}, {"EcalBarrelScFiRecHits"},
Expand All @@ -272,7 +276,7 @@ void InitPlugin(JApplication* app) {
.maskPos = "xy",
.maskPosFields = {"fiber", "z"},
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));
app->Add(new JOmniFactoryGeneratorT<CalorimeterIslandCluster_factory>(
"EcalBarrelScFiProtoClusters", {"EcalBarrelScFiRecHits"}, {"EcalBarrelScFiProtoClusters"},
Expand Down Expand Up @@ -374,7 +378,7 @@ void InitPlugin(JApplication* app) {
.readout = "EcalBarrelImagingHits",
.timeWindow = EcalBarrelImaging_timeWindow,
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));
app->Add(new JOmniFactoryGeneratorT<CalorimeterHitDigi_factory>(
"EcalBarrelImagingRawHits", {"EventHeader", "EcalBarrelImagingProcessedHits"},
Expand All @@ -391,7 +395,7 @@ void InitPlugin(JApplication* app) {
.corrMeanScale = "1.0",
.readout = "EcalBarrelImagingHits",
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));
app->Add(new JOmniFactoryGeneratorT<CalorimeterHitReco_factory>(
"EcalBarrelImagingRecHits", {"EcalBarrelImagingRawHits"}, {"EcalBarrelImagingRecHits"},
Expand All @@ -408,7 +412,7 @@ void InitPlugin(JApplication* app) {
.layerField = "layer",
.sectorField = "sector",
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));
app->Add(new JOmniFactoryGeneratorT<ImagingTopoCluster_factory>(
"EcalBarrelImagingProtoClusters", {"EcalBarrelImagingRecHits"},
Expand Down
9 changes: 7 additions & 2 deletions src/detectors/BHCAL/BHCAL.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Copyright (C) 2022 - 2024 David Lawrence, Derek Anderson, Wouter Deconinck

#include <Evaluator/DD4hepUnits.h>
#include <JANA/JApplication.h>
#include <JANA/JApplicationFwd.h>
#include <JANA/Utils/JEventLevel.h>
#include <JANA/Utils/JTypeInfo.h>
#include <string>
#include <variant>
Expand All @@ -27,6 +29,9 @@ void InitPlugin(JApplication* app) {
using namespace eicrecon;

InitJANAPlugin(app);
const bool split_timeframes =
app->RegisterParameter<bool>("split_timeframes", false, "Enable timeframe splitting");
const auto hit_level = split_timeframes ? JEventLevel::Timeslice : JEventLevel::PhysicsEvent;

// Make sure digi and reco use the same value
decltype(CalorimeterHitDigiConfig::capADC) HcalBarrel_capADC = 65536; //65536, 16bit ADC
Expand Down Expand Up @@ -64,7 +69,7 @@ void InitPlugin(JApplication* app) {
.corrMeanScale = "1.0",
.readout = "HcalBarrelHits",
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));

app->Add(new JOmniFactoryGeneratorT<CalorimeterHitReco_factory>(
Expand All @@ -82,7 +87,7 @@ void InitPlugin(JApplication* app) {
.layerField = "",
.sectorField = "",
},
app // TODO: Remove me once fixed
app, hit_level // TODO: Remove me once fixed
));

// --------------------------------------------------------------------
Expand Down
Loading
Loading