diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index f5633dd72e1..d5a64a9a9e9 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -635,4 +635,4 @@ jobs: path: "*.whl" - name: Test - run: CI/dependencies/run.sh .env python3 Python/Examples/scripts/hello_world_kalman.py + run: CI/dependencies/run.sh .env python3 Examples/Scripts/Python/track_finding_python_only.py diff --git a/Examples/Scripts/Python/track_finding_python_only.py b/Examples/Scripts/Python/track_finding_python_only.py new file mode 100644 index 00000000000..79c7805b65d --- /dev/null +++ b/Examples/Scripts/Python/track_finding_python_only.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# This file is part of the ACTS project. +# +# Copyright (C) 2016 CERN for the benefit of the ACTS project +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import os +from pathlib import Path + +os.environ["ACTS_SEQUENCER_DISABLE_FPEMON"] = "1" + +import acts +import acts.examples +from acts import UnitConstants as u + + +def runTrackFindingPythonOnly( + trackingGeometry, + field, + digiConfigFile, + geoSelectionConfigFile, + outputDir, + decorators=[], + s=None, +): + from acts.examples.simulation import ( + addParticleGun, + MomentumConfig, + EtaConfig, + PhiConfig, + ParticleConfig, + addFatras, + addDigitization, + ) + + s = s or acts.examples.Sequencer(events=1, numThreads=1, logLevel=acts.logging.INFO) + outputDir = Path(outputDir) + rnd = acts.examples.RandomNumbers(seed=42) + + for d in decorators: + s.addContextDecorator(d) + + addParticleGun( + s, + MomentumConfig(1.0 * u.GeV, 10.0 * u.GeV, transverse=True), + EtaConfig(-2.0, 2.0, uniform=True), + PhiConfig(0.0, 360.0 * u.degree), + ParticleConfig(1, acts.PdgParticle.eMuon, randomizeCharge=True), + rnd=rnd, + ) + + addFatras( + s, + trackingGeometry, + field, + rnd=rnd, + ) + + addDigitization( + s, + trackingGeometry, + field, + digiConfigFile=digiConfigFile, + rnd=rnd, + ) + + s.addAlgorithm( + acts.examples.SpacePointMaker( + level=acts.logging.INFO, + trackingGeometry=trackingGeometry, + inputMeasurements="measurements", + outputSpacePoints="spacepoints", + geometrySelection=acts.examples.json.readJsonGeometryList( + str(geoSelectionConfigFile) + ), + ) + ) + + class PythonTrackFinder(acts.examples.IAlgorithm): + def __init__(self, name, level): + acts.examples.IAlgorithm.__init__(self, name, level) + + self.spacepoints = acts.examples.ReadDataHandle( + self, acts.SpacePointContainer2, "Spacepoints" + ) + self.spacepoints.initialize("spacepoints") + + self.prototracks = acts.examples.WriteDataHandle( + self, acts.examples.ProtoTrackContainer, "Prototracks" + ) + self.prototracks.initialize("prototracks") + + def execute(self, context): + spacepoints = self.spacepoints(context.eventStore) + + track = acts.examples.ProtoTrack() + for sp in sorted(spacepoints, key=lambda sp: sp.r): + for sl in sp.sourceLinks: + isl = acts.examples.IndexSourceLink.FromSourceLink(sl) + track.append(isl.index()) + + prototracks = acts.examples.ProtoTrackContainer() + prototracks.append(track) + + self.prototracks(context, prototracks) + return acts.examples.ProcessCode.SUCCESS + + s.addAlgorithm(PythonTrackFinder("PythonTrackFinder", acts.logging.INFO)) + + class PythonTrackFitter(acts.examples.IAlgorithm): + def __init__(self, name, level): + acts.examples.IAlgorithm.__init__(self, name, level) + + self.prototracks = acts.examples.ReadDataHandle( + self, acts.examples.ProtoTrackContainer, "Prototracks" + ) + self.prototracks.initialize("prototracks") + + self.tracks = acts.examples.WriteDataHandle( + self, acts.examples.ConstTrackContainer, "Tracks" + ) + self.tracks.initialize("fitted_tracks") + + def execute(self, context): + prototracks = self.prototracks(context.eventStore) + + container = acts.examples.TrackContainer() + for prototrack in prototracks: + track = container.makeTrack() + track.parameters = acts.BoundVector(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) + track.nMeasurements = len(prototrack) + + self.tracks(context, container.makeConst()) + return acts.examples.ProcessCode.SUCCESS + + s.addAlgorithm(PythonTrackFitter("PythonTrackFitter", acts.logging.INFO)) + + return s + + +if __name__ == "__main__": + srcdir = Path(__file__).resolve().parent.parent.parent.parent + + detector = acts.examples.GenericDetector(acts.examples.GenericDetector.Config()) + trackingGeometry = detector.trackingGeometry() + decorators = detector.contextDecorators() + + field = acts.ConstantBField(acts.Vector3(0.0, 0.0, 2.0 * u.T)) + + digiConfigFile = srcdir / "Examples/Configs/generic-digi-smearing-config.json" + geoSelectionConfigFile = srcdir / "Examples/Configs/generic-seeding-config.json" + + outputDir = Path.cwd() / "output_track_finding_python_only" + outputDir.mkdir(exist_ok=True) + + runTrackFindingPythonOnly( + trackingGeometry=trackingGeometry, + field=field, + digiConfigFile=digiConfigFile, + geoSelectionConfigFile=geoSelectionConfigFile, + outputDir=outputDir, + decorators=decorators, + ).run() diff --git a/Python/Core/src/EventData.cpp b/Python/Core/src/EventData.cpp index 18d94267249..dc873ca35ea 100644 --- a/Python/Core/src/EventData.cpp +++ b/Python/Core/src/EventData.cpp @@ -222,7 +222,14 @@ void addEventData(py::module_& m) { static_cast(&ConstSpacePointProxy2::varianceZ)) .def_property_readonly( "varianceR", - static_cast(&ConstSpacePointProxy2::varianceR)); + static_cast(&ConstSpacePointProxy2::varianceR)) + .def_property_readonly( + "sourceLinks", py::cpp_function( + [](const ConstSpacePointProxy2& self) { + auto sls = self.sourceLinks(); + return py::make_iterator(sls.begin(), sls.end()); + }, + py::keep_alive<0, 1>())); // SeedContainer2 auto seedContainer2 = diff --git a/Python/Examples/scripts/hello_world_kalman.py b/Python/Examples/scripts/hello_world_kalman.py deleted file mode 100755 index d08a67abb0b..00000000000 --- a/Python/Examples/scripts/hello_world_kalman.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 - -from pathlib import Path -import json - -import acts -import acts.examples - -from acts.examples.simulation import ( - addParticleGun, - ParticleConfig, - EtaConfig, - PhiConfig, - MomentumConfig, - addFatras, - addDigitization, - ParticleSelectorConfig, - addDigiParticleSelection, -) - -from acts.examples.reconstruction import ( - addSeeding, - SeedingAlgorithm, - addKalmanTracks, - addTrackWriters, -) - -u = acts.UnitConstants - - -if "__main__" == __name__: - # GenericDetector - detector = acts.examples.GenericDetector() - trackingGeometry = detector.trackingGeometry() - - # Create minimal digitization config with only volume id 0 (broadcasts to all surfaces) - digiConfig = { - "acts-geometry-hierarchy-map": { - "format-version": 0, - "value-identifier": "digitization-configuration", - }, - "entries": [ - { - "volume": 0, - "value": { - "smearing": [ - {"index": 0, "mean": 0.0, "stddev": 0.01, "type": "Gauss"}, - {"index": 1, "mean": 0.0, "stddev": 0.01, "type": "Gauss"}, - ] - }, - } - ], - } - - # Save config to file - digiConfigFile = Path.cwd() / "digi-config-minimal.json" - with open(digiConfigFile, "w") as f: - json.dump(digiConfig, f, indent=4) - logger = acts.getDefaultLogger("Truth tracking example", acts.logging.INFO) - logger.info(f"Saved minimal digitization config to {digiConfigFile}") - - field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T)) - - s = acts.examples.Sequencer(events=10, numThreads=-1, logLevel=acts.logging.INFO) - - rnd = acts.examples.RandomNumbers(seed=42) - - addParticleGun( - s, - ParticleConfig(num=1, pdg=acts.PdgParticle.eMuon, randomizeCharge=True), - EtaConfig(-3.0, 3.0, uniform=True), - MomentumConfig(1.0 * u.GeV, 100.0 * u.GeV, transverse=True), - PhiConfig(0.0, 360.0 * u.degree), - vtxGen=acts.examples.GaussianVertexGenerator( - mean=acts.Vector4(0, 0, 0, 0), - stddev=acts.Vector4(0, 0, 0, 0), - ), - multiplicity=1, - rnd=rnd, - ) - - addFatras( - s, - trackingGeometry, - field, - rnd=rnd, - enableInteractions=True, - ) - - addDigitization( - s, - trackingGeometry, - field, - digiConfigFile=digiConfigFile, - rnd=rnd, - ) - - addDigiParticleSelection( - s, - ParticleSelectorConfig( - pt=(0.9 * u.GeV, None), - measurements=(7, None), - removeNeutral=True, - removeSecondaries=True, - ), - ) - - addSeeding( - s, - trackingGeometry, - field, - rnd=rnd, - inputParticles="particles_generated", - seedingAlgorithm=SeedingAlgorithm.TruthSmeared, - ) - - addKalmanTracks( - s, - trackingGeometry, - field, - ) - - addTrackWriters( - s, - name="tracks", - outputDirCsv="csv", - ) - - s.run() diff --git a/Python/Examples/src/EventData.cpp b/Python/Examples/src/EventData.cpp index 83932226d3f..ca60348d71e 100644 --- a/Python/Examples/src/EventData.cpp +++ b/Python/Examples/src/EventData.cpp @@ -7,11 +7,11 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. #include "Acts/EventData/SpacePointContainer2.hpp" +#include "ActsExamples/EventData/IndexSourceLink.hpp" #include "ActsExamples/EventData/ProtoTrack.hpp" #include "ActsExamples/EventData/Track.hpp" #include "ActsPython/Utilities/WhiteBoardRegistry.hpp" -#include #include #include #include @@ -244,6 +244,72 @@ void addEventData(py::module& mex) { WhiteBoardRegistry::registerClass(protoTrackContainer); mex.attr("kTrackIndexInvalid") = Acts::kTrackIndexInvalid; + + py::class_(mex, "IndexSourceLink") + .def("FromSourceLink", + [](Acts::SourceLink const& sl) { return sl.get(); }) + .def("index", &IndexSourceLink::index) + .def("geometryId", &IndexSourceLink::geometryId); + + py::class_(mex, "TrackProxy") + .def_property( + "referenceSurface", + [](const TrackProxy& self) -> const Acts::Surface& { + return self.referenceSurface(); + }, + [](TrackProxy& self, std::shared_ptr srf) { + self.setReferenceSurface(std::move(srf)); + }) + .def_property( + "parameters", + [](const TrackProxy& self) { + return Acts::BoundVector{self.parameters()}; + }, + [](TrackProxy& self, const Acts::BoundVector& v) { + self.parameters() = v; + }) + .def_property( + "covariance", + [](const TrackProxy& self) { + return Acts::BoundMatrix{self.covariance()}; + }, + [](TrackProxy& self, const Acts::BoundMatrix& m) { + self.covariance() = m; + }) + .def_property( + "particleHypothesis", + [](const TrackProxy& self) { return self.particleHypothesis(); }, + [](TrackProxy& self, const Acts::ParticleHypothesis& hyp) { + self.setParticleHypothesis(hyp); + }) + .def_property( + "nMeasurements", + [](const TrackProxy& self) -> std::uint32_t { + return self.nMeasurements(); + }, + [](TrackProxy& self, std::uint32_t n) { self.nMeasurements() = n; }) + .def_property( + "nHoles", + [](const TrackProxy& self) -> std::uint32_t { return self.nHoles(); }, + [](TrackProxy& self, std::uint32_t n) { self.nHoles() = n; }) + .def_property( + "chi2", [](const TrackProxy& self) -> float { return self.chi2(); }, + [](TrackProxy& self, float v) { self.chi2() = v; }); + + py::class_(mex, "TrackContainer") + .def(py::init([]() { + return TrackContainer{std::make_shared(), + std::make_shared()}; + })) + .def("__len__", &TrackContainer::size) + .def("makeTrack", &TrackContainer::makeTrack, py::keep_alive<0, 1>()) + .def("makeConst", [](TrackContainer& self) { + return ConstTrackContainer{ + std::make_shared( + std::move(self.container())), + std::make_shared( + std::move(self.trackStateContainer()))}; + }); } } // namespace ActsPython diff --git a/Python/Examples/tests/test_truth_tracking.py b/Python/Examples/tests/test_truth_tracking.py index a4f15410ce9..c6058f75cf9 100644 --- a/Python/Examples/tests/test_truth_tracking.py +++ b/Python/Examples/tests/test_truth_tracking.py @@ -232,12 +232,15 @@ def execute(self, context): for track in tracks: params = track.parameters - assert params.shape == (6,) + assert isinstance(params, acts.BoundVector) cov = track.covariance - assert cov.shape == (6, 6) + assert isinstance(cov, acts.BoundMatrix) # parameters from per-proxy accessor must match the # bulk numpy array at the same index - assert np.allclose(params, tracks.parameters[track.index]) + assert np.allclose( + [params[i] for i in range(6)], + tracks.parameters[track.index], + ) n_meas_from_summary = track.nMeasurements n_meas_counted = 0 @@ -263,15 +266,15 @@ def execute(self, context): if state.hasPredicted: pred = state.predicted - assert pred.shape == (6,) + assert isinstance(pred, acts.BoundVector) if state.hasFiltered: filt = state.filtered - assert filt.shape == (6,) + assert isinstance(filt, acts.BoundVector) if state.hasSmoothed: smth = state.smoothed - assert smth.shape == (6,) + assert isinstance(smth, acts.BoundVector) assert n_meas_counted == n_meas_from_summary assert n_holes_counted == track.nHoles @@ -290,7 +293,7 @@ def execute(self, context): ] assert len(fwd_predicted) == len(rev_predicted) for fwd, rev in zip(fwd_predicted, reversed(rev_predicted)): - assert np.allclose(fwd, rev) + assert all(fwd[i] == pytest.approx(rev[i]) for i in range(6)) return acts.examples.ProcessCode.SUCCESS