diff --git a/Io/CMakeLists.txt b/Io/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..65a9411aeabe0fee673625f98d6a45cc3bffe4f5
--- /dev/null
+++ b/Io/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_subdirectory(Csv)
+add_subdirectory(HepMC3)
+add_subdirectory(Json)
+add_subdirectory(Obj)
+add_subdirectory(Performance)
+add_subdirectory(Root)
diff --git a/Io/Csv/CMakeLists.txt b/Io/Csv/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..783ccc90b37ee667b42c29e79c4d221984336583
--- /dev/null
+++ b/Io/Csv/CMakeLists.txt
@@ -0,0 +1,22 @@
+add_library(
+  ActsExamplesIoCsv SHARED
+  src/CsvOptionsReader.cpp
+  src/CsvOptionsWriter.cpp
+  src/CsvParticleReader.cpp
+  src/CsvParticleWriter.cpp
+  src/CsvPlanarClusterReader.cpp
+  src/CsvPlanarClusterWriter.cpp
+  src/CsvTrackingGeometryWriter.cpp)
+target_include_directories(
+  ActsExamplesIoCsv
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
+target_link_libraries(
+  ActsExamplesIoCsv
+  PRIVATE
+    ActsCore ActsDigitizationPlugin ActsIdentificationPlugin
+    ActsExamplesFramework
+    Threads::Threads Boost::program_options dfelibs)
+
+install(
+  TARGETS ActsExamplesIoCsv
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsReader.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsReader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3ccbb7a6ab308f22f2e4ba9b8e7ce4b820767077
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsReader.hpp
@@ -0,0 +1,29 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include "ACTFW/Io/Csv/CsvParticleReader.hpp"
+#include "ACTFW/Io/Csv/CsvPlanarClusterReader.hpp"
+#include "ACTFW/Utilities/OptionsFwd.hpp"
+
+namespace FW {
+namespace Options {
+
+// There are no additional CSV reader options apart from the
+// format-independent, generic input option.
+
+/// Read the CSV particle reader config.
+FW::CsvParticleReader::Config readCsvParticleReaderConfig(const Variables& vm);
+
+/// Read the CSV particle reader config.
+FW::CsvPlanarClusterReader::Config readCsvPlanarClusterReaderConfig(
+    const Variables& vm);
+
+}  // namespace Options
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsWriter.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1bd5fff34919e05e2efdf912b2ed8467c5bac31d
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvOptionsWriter.hpp
@@ -0,0 +1,34 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include "ACTFW/Io/Csv/CsvParticleWriter.hpp"
+#include "ACTFW/Io/Csv/CsvPlanarClusterWriter.hpp"
+#include "ACTFW/Io/Csv/CsvTrackingGeometryWriter.hpp"
+#include "ACTFW/Utilities/OptionsFwd.hpp"
+
+namespace FW {
+namespace Options {
+
+// Add common CSV writer options.
+void addCsvWriterOptions(Description& desc);
+
+/// Read the CSV particle writer options.
+FW::CsvParticleWriter::Config readCsvParticleWriterConfig(const Variables& vm);
+
+/// Read the CSV planar cluster writer options.
+FW::CsvPlanarClusterWriter::Config readCsvPlanarClusterWriterConfig(
+    const Variables& vm);
+
+/// Read the CSV tracking geometry writer config.
+FW::CsvTrackingGeometryWriter::Config readCsvTrackingGeometryWriterConfig(
+    const Variables& vm);
+
+}  // namespace Options
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvParticleReader.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvParticleReader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b77f4ac7df9aaf4d392f5c030388ae9451076987
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvParticleReader.hpp
@@ -0,0 +1,62 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Utilities/Logger.hpp>
+#include <memory>
+#include <string>
+
+#include "ACTFW/Framework/IReader.hpp"
+
+namespace FW {
+
+/// Read particles in the TrackML comma-separated-value format.
+///
+/// This reads one file per event in the configured input directory
+/// and filename. Files are assumed to be named using the following schema
+///
+///     event000000001-<stem>.csv
+///     event000000002-<stem>.csv
+///
+/// and each line in the file corresponds to one particle. The
+/// input filename can be configured and defaults to `particles.csv`.
+class CsvParticleReader final : public IReader {
+ public:
+  struct Config {
+    /// Where to read input files from.
+    std::string inputDir;
+    /// Input filename stem.
+    std::string inputStem = "particles";
+    /// Which particle collection to read into.
+    std::string outputParticles;
+  };
+
+  /// Construct the particle reader.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  CsvParticleReader(const Config& cfg, Acts::Logging::Level lvl);
+
+  std::string name() const final override;
+
+  /// Return the available events range.
+  std::pair<size_t, size_t> availableEvents() const final override;
+
+  /// Read out data from the input stream.
+  ProcessCode read(const FW::AlgorithmContext& ctx) final override;
+
+ private:
+  Config m_cfg;
+  std::pair<size_t, size_t> m_eventsRange;
+  std::unique_ptr<const Acts::Logger> m_logger;
+
+  const Acts::Logger& logger() const { return *m_logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvParticleWriter.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvParticleWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..67d2bc465a27779bb077de325b19e3cfb78dceb2
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvParticleWriter.hpp
@@ -0,0 +1,65 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+
+namespace FW {
+
+/// Write out particles in the TrackML comma-separated-value format.
+///
+/// This writer is restricted to outgoing particles, it is designed for
+/// generated particle information.
+///
+/// This writes one file per event into the configured output directory. By
+/// default it writes to the current working directory. Files are named
+/// using the following schema
+///
+///     event000000001-<stem>.csv
+///     event000000002-<stem>.csv
+///     ...
+///
+/// and each line in the file corresponds to one particle.
+class CsvParticleWriter final : public WriterT<SimParticleContainer> {
+ public:
+  struct Config {
+    /// Input particles collection to write.
+    std::string inputParticles;
+    /// Where to place output files.
+    std::string outputDir;
+    /// Output filename stem.
+    std::string outputStem = "particles";
+    /// Number of decimal digits for floating point precision in output.
+    size_t outputPrecision = std::numeric_limits<float>::max_digits10;
+  };
+
+  /// Construct the particle writer.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  CsvParticleWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+ protected:
+  /// Type-specific write implementation.
+  ///
+  /// @param[in] ctx is the algorithm context
+  /// @param[in] particles are the particle to be written
+  ProcessCode writeT(const FW::AlgorithmContext& ctx,
+                     const SimParticleContainer& particles) final override;
+
+ private:
+  Config m_cfg;  //!< Nested configuration struct
+};
+
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterReader.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterReader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ce5e8ee0ad5a49e999ef0cf17b881356b2942238
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterReader.hpp
@@ -0,0 +1,79 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+#pragma once
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+#include "ACTFW/Framework/IReader.hpp"
+#include "Acts/Geometry/GeometryID.hpp"
+#include "Acts/Geometry/TrackingGeometry.hpp"
+#include "Acts/Utilities/Logger.hpp"
+
+namespace Acts {
+class Surface;
+}
+
+namespace FW {
+
+/// Read in a planar cluster collection in comma-separated-value format.
+///
+/// This reads three files per event file in the configured input
+/// directory. By default it reads file in the current working directory.
+/// Files are assumed to be named using the following schema
+///
+///     event000000001-cells.csv
+///     event000000001-hits.csv
+///     event000000001-truth.csv
+///     event000000002-cells.csv
+///     event000000002-hits.csv
+///     event000000002-truth.csv
+///
+/// and each line in the file corresponds to one hit/cluster.
+class CsvPlanarClusterReader final : public IReader {
+ public:
+  struct Config {
+    /// Where to read input files from.
+    std::string inputDir;
+    /// Output cluster collection.
+    std::string outputClusters;
+    /// For each cluster/ hit index the original hit id stored on file.
+    std::string outputHitIds;
+    /// Output hit-particles mapping collection.
+    std::string outputHitParticlesMap;
+    /// Output simulated (truth) hits collection.
+    std::string outputSimulatedHits;
+    /// Tracking geometry required to access global-to-local transforms.
+    std::shared_ptr<const Acts::TrackingGeometry> trackingGeometry;
+  };
+
+  /// Construct the cluster reader.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  CsvPlanarClusterReader(const Config& cfg, Acts::Logging::Level lvl);
+
+  std::string name() const final override;
+
+  /// Return the available events range.
+  std::pair<size_t, size_t> availableEvents() const final override;
+
+  /// Read out data from the input stream.
+  ProcessCode read(const FW::AlgorithmContext& ctx) final override;
+
+ private:
+  Config m_cfg;
+  std::unordered_map<Acts::GeometryID, const Acts::Surface*> m_surfaces;
+  std::pair<size_t, size_t> m_eventsRange;
+  std::unique_ptr<const Acts::Logger> m_logger;
+
+  const Acts::Logger& logger() const { return *m_logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterWriter.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8ddd3b4737540e843549cf28c21fbb6af26b46fa
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvPlanarClusterWriter.hpp
@@ -0,0 +1,69 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <limits>
+#include <string>
+
+#include "ACTFW/EventData/GeometryContainers.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "Acts/Plugins/Digitization/PlanarModuleCluster.hpp"
+
+namespace FW {
+
+/// Write out a planar cluster collection in comma-separated-value format.
+///
+/// This writes multiples file per event containing information about the
+/// space points, local constituent cells, and hit-particle truth mapping
+/// into the configured output directory. By default it writes to the
+/// current working directory. Files are named using the following schema
+///
+///     event000000001-cells.csv
+///     event000000001-hits.csv
+///     event000000001-truth.csv
+///     event000000002-cells.csv
+///     event000000002-hits.csv
+///     event000000002-truth.csv
+///     ...
+///
+/// and each line in the file corresponds to one hit/cluster.
+class CsvPlanarClusterWriter final
+    : public WriterT<GeometryIdMultimap<Acts::PlanarModuleCluster>> {
+ public:
+  struct Config {
+    /// Which cluster collection to write.
+    std::string inputClusters;
+    /// Which simulated (truth) hits collection to use.
+    std::string inputSimulatedHits;
+    /// Where to place output files
+    std::string outputDir;
+    /// Number of decimal digits for floating point precision in output.
+    size_t outputPrecision = std::numeric_limits<float>::max_digits10;
+  };
+
+  /// Construct the cluster writer.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  CsvPlanarClusterWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+ protected:
+  /// Type-specific write implementation.
+  ///
+  /// @param[in] ctx is the algorithm context
+  /// @param[in] particles are the particle to be written
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const GeometryIdMultimap<Acts::PlanarModuleCluster>&
+                         clusters) final override;
+
+ private:
+  Config m_cfg;
+};
+
+}  // namespace FW
diff --git a/Io/Csv/include/ACTFW/Io/Csv/CsvTrackingGeometryWriter.hpp b/Io/Csv/include/ACTFW/Io/Csv/CsvTrackingGeometryWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..41ac854cf84eeefdc772481df95cc1e0ac905c73
--- /dev/null
+++ b/Io/Csv/include/ACTFW/Io/Csv/CsvTrackingGeometryWriter.hpp
@@ -0,0 +1,69 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Geometry/TrackingGeometry.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <limits>
+
+#include "ACTFW/Framework/IWriter.hpp"
+
+namespace Acts {
+class TrackingVolume;
+}
+
+namespace FW {
+
+/// Write out the geometry for all sensitive detector surfaces.
+///
+/// This writes a `detectors.csv` file at the end of the run using the
+/// default context to determine the geometry. If configured, it also writes
+/// an additional file for each event using the following schema
+///
+///     event000000001-detectors.csv
+///     event000000002-detectors.csv
+///     ...
+///
+/// that uses the per-event context to determine the geometry.
+class CsvTrackingGeometryWriter : public IWriter {
+ public:
+  struct Config {
+    /// The tracking geometry that should be written.
+    std::shared_ptr<const Acts::TrackingGeometry> trackingGeometry;
+    /// Where to place output files.
+    std::string outputDir;
+    /// Number of decimal digits for floating point precision in output.
+    std::size_t outputPrecision = std::numeric_limits<float>::max_digits10;
+    /// Whether to write the per-event file.
+    bool writePerEvent = false;
+  };
+
+  /// Construct the geometry writer.
+  ///
+  /// @param cfg is the configuration object
+  /// @param lvl is the logging level
+  CsvTrackingGeometryWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  std::string name() const final override;
+
+  /// Write geometry using the per-event context (optional).
+  ProcessCode write(const AlgorithmContext& context) final override;
+
+  /// Write geometry using the default context.
+  ProcessCode endRun() final override;
+
+ private:
+  Config m_cfg;
+  const Acts::TrackingVolume* m_world;
+  std::unique_ptr<const Acts::Logger> m_logger;
+
+  const Acts::Logger& logger() const { return *m_logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Csv/src/CsvOptionsReader.cpp b/Io/Csv/src/CsvOptionsReader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d38d000551c5e020442c39e86134ad8450d4c698
--- /dev/null
+++ b/Io/Csv/src/CsvOptionsReader.cpp
@@ -0,0 +1,29 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvOptionsReader.hpp"
+
+#include <boost/program_options.hpp>
+
+FW::CsvParticleReader::Config FW::Options::readCsvParticleReaderConfig(
+    const Variables& vm) {
+  FW::CsvParticleReader::Config cfg;
+  if (not vm["input-dir"].empty()) {
+    cfg.inputDir = vm["input-dir"].as<std::string>();
+  }
+  return cfg;
+}
+
+FW::CsvPlanarClusterReader::Config
+FW::Options::readCsvPlanarClusterReaderConfig(const Variables& vm) {
+  FW::CsvPlanarClusterReader::Config cfg;
+  if (not vm["input-dir"].empty()) {
+    cfg.inputDir = vm["input-dir"].as<std::string>();
+  }
+  return cfg;
+}
diff --git a/Io/Csv/src/CsvOptionsWriter.cpp b/Io/Csv/src/CsvOptionsWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..76148d8d79b20c67a1e04a64b99fc211e2b749ec
--- /dev/null
+++ b/Io/Csv/src/CsvOptionsWriter.cpp
@@ -0,0 +1,56 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvOptionsWriter.hpp"
+
+#include <boost/program_options.hpp>
+#include <dfe/dfe_io_dsv.hpp>
+#include <limits>
+
+void FW::Options::addCsvWriterOptions(FW::Options::Description& desc) {
+  using namespace boost::program_options;
+
+  desc.add_options()(
+      "csv-output-precision",
+      value<size_t>()->default_value(std::numeric_limits<float>::max_digits10),
+      "Floating number output precision.")(
+      "csv-tg-perevent", bool_switch(), "Write tracking geometry per event.");
+}
+
+FW::CsvParticleWriter::Config FW::Options::readCsvParticleWriterConfig(
+    const FW::Options::Variables& vm) {
+  FW::CsvParticleWriter::Config cfg;
+  if (not vm["output-dir"].empty()) {
+    cfg.outputDir = vm["output-dir"].as<std::string>();
+  }
+  cfg.outputPrecision = vm["csv-output-precision"].as<size_t>();
+  return cfg;
+}
+
+FW::CsvPlanarClusterWriter::Config
+FW::Options::readCsvPlanarClusterWriterConfig(
+    const FW::Options::Variables& vm) {
+  FW::CsvPlanarClusterWriter::Config cfg;
+  if (not vm["output-dir"].empty()) {
+    cfg.outputDir = vm["output-dir"].as<std::string>();
+  }
+  cfg.outputPrecision = vm["csv-output-precision"].as<size_t>();
+  return cfg;
+}
+
+FW::CsvTrackingGeometryWriter::Config
+FW::Options::readCsvTrackingGeometryWriterConfig(
+    const FW::Options::Variables& vm) {
+  FW::CsvTrackingGeometryWriter::Config cfg;
+  if (not vm["output-dir"].empty()) {
+    cfg.outputDir = vm["output-dir"].as<std::string>();
+  }
+  cfg.outputPrecision = vm["csv-output-precision"].as<size_t>();
+  cfg.writePerEvent = vm.count("csv-tg-perevent");
+  return cfg;
+}
diff --git a/Io/Csv/src/CsvParticleReader.cpp b/Io/Csv/src/CsvParticleReader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6de72b4cc5d64be549536695149201efcbb4973c
--- /dev/null
+++ b/Io/Csv/src/CsvParticleReader.cpp
@@ -0,0 +1,77 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvParticleReader.hpp"
+
+#include <Acts/Utilities/Units.hpp>
+#include <dfe/dfe_io_dsv.hpp>
+#include <fstream>
+#include <ios>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "TrackMlData.hpp"
+
+FW::CsvParticleReader::CsvParticleReader(
+    const FW::CsvParticleReader::Config& cfg, Acts::Logging::Level lvl)
+    : m_cfg(cfg),
+      m_eventsRange(
+          determineEventFilesRange(cfg.inputDir, cfg.inputStem + ".csv")),
+      m_logger(Acts::getDefaultLogger("CsvParticleReader", lvl)) {
+  if (m_cfg.inputStem.empty()) {
+    throw std::invalid_argument("Missing input filename stem");
+  }
+  if (m_cfg.outputParticles.empty()) {
+    throw std::invalid_argument("Missing output collection");
+  }
+}
+
+std::string FW::CsvParticleReader::CsvParticleReader::name() const {
+  return "CsvParticleReader";
+}
+
+std::pair<size_t, size_t> FW::CsvParticleReader::availableEvents() const {
+  return m_eventsRange;
+}
+
+FW::ProcessCode FW::CsvParticleReader::read(const FW::AlgorithmContext& ctx) {
+  SimParticleContainer::sequence_type unordered;
+
+  auto path = perEventFilepath(m_cfg.inputDir, m_cfg.inputStem + ".csv",
+                               ctx.eventNumber);
+  // vt and m are an optional columns
+  dfe::NamedTupleCsvReader<ParticleData> reader(path, {"vt", "m"});
+  ParticleData data;
+
+  while (reader.read(data)) {
+    ActsFatras::Particle particle(ActsFatras::Barcode(data.particle_id),
+                                  Acts::PdgParticle(data.particle_type),
+                                  data.q * Acts::UnitConstants::e,
+                                  data.m * Acts::UnitConstants::GeV);
+    particle.setProcess(static_cast<ActsFatras::ProcessType>(data.process));
+    particle.setPosition4(
+        data.vx * Acts::UnitConstants::mm, data.vy * Acts::UnitConstants::mm,
+        data.vz * Acts::UnitConstants::mm, data.vt * Acts::UnitConstants::ns);
+    // only used for direction; normalization/units do not matter
+    particle.setDirection(data.px, data.py, data.pz);
+    particle.setAbsMomentum(std::hypot(data.px, data.py, data.pz) *
+                            Acts::UnitConstants::GeV);
+    unordered.push_back(std::move(particle));
+  }
+
+  // write ordered particles container to the EventStore
+  SimParticleContainer particles;
+  particles.adopt_sequence(std::move(unordered));
+  ctx.eventStore.add(m_cfg.outputParticles, std::move(particles));
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Csv/src/CsvParticleWriter.cpp b/Io/Csv/src/CsvParticleWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..72580e57a1d9c4b2a3d4b64f1c07e4ac12a5e6bd
--- /dev/null
+++ b/Io/Csv/src/CsvParticleWriter.cpp
@@ -0,0 +1,55 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvParticleWriter.hpp"
+
+#include <Acts/Utilities/Units.hpp>
+#include <dfe/dfe_io_dsv.hpp>
+#include <map>
+#include <stdexcept>
+
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "TrackMlData.hpp"
+
+FW::CsvParticleWriter::CsvParticleWriter(
+    const FW::CsvParticleWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputParticles, "CsvParticleWriter", lvl), m_cfg(cfg) {
+  // inputParticles is already checked by base constructor
+  if (m_cfg.outputStem.empty()) {
+    throw std::invalid_argument("Missing ouput filename stem");
+  }
+}
+
+FW::ProcessCode FW::CsvParticleWriter::writeT(
+    const FW::AlgorithmContext& ctx, const SimParticleContainer& particles) {
+  auto pathParticles = perEventFilepath(
+      m_cfg.outputDir, m_cfg.outputStem + ".csv", ctx.eventNumber);
+  dfe::NamedTupleCsvWriter<ParticleData> writer(pathParticles,
+                                                m_cfg.outputPrecision);
+
+  ParticleData data;
+  for (const auto& particle : particles) {
+    data.particle_id = particle.particleId().value();
+    data.particle_type = particle.pdg();
+    data.process = static_cast<decltype(data.process)>(particle.process());
+    data.vx = particle.position().x() / Acts::UnitConstants::mm;
+    data.vy = particle.position().y() / Acts::UnitConstants::mm;
+    data.vz = particle.position().z() / Acts::UnitConstants::mm;
+    data.vt = particle.time() / Acts::UnitConstants::ns;
+    const auto p = particle.absMomentum() / Acts::UnitConstants::GeV;
+    data.px = p * particle.unitDirection().x();
+    data.py = p * particle.unitDirection().y();
+    data.pz = p * particle.unitDirection().z();
+    data.m = particle.mass() / Acts::UnitConstants::GeV;
+    data.q = particle.charge() / Acts::UnitConstants::e;
+    writer.append(data);
+  }
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Csv/src/CsvPlanarClusterReader.cpp b/Io/Csv/src/CsvPlanarClusterReader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a4729d9650ea82aae7e35d5ddbc38c32f657bd2a
--- /dev/null
+++ b/Io/Csv/src/CsvPlanarClusterReader.cpp
@@ -0,0 +1,287 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvPlanarClusterReader.hpp"
+
+#include <dfe/dfe_io_dsv.hpp>
+
+#include "ACTFW/EventData/GeometryContainers.hpp"
+#include "ACTFW/EventData/IndexContainers.hpp"
+#include "ACTFW/EventData/SimHit.hpp"
+#include "ACTFW/EventData/SimIdentifier.hpp"
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "ACTFW/Utilities/Range.hpp"
+#include "Acts/Plugins/Digitization/PlanarModuleCluster.hpp"
+#include "Acts/Plugins/Identification/IdentifiedDetectorElement.hpp"
+#include "Acts/Utilities/Units.hpp"
+#include "TrackMlData.hpp"
+
+FW::CsvPlanarClusterReader::CsvPlanarClusterReader(
+    const FW::CsvPlanarClusterReader::Config& cfg, Acts::Logging::Level lvl)
+    : m_cfg(cfg)
+      // TODO check that all files (hits,cells,truth) exists
+      ,
+      m_eventsRange(determineEventFilesRange(cfg.inputDir, "hits.csv")),
+      m_logger(Acts::getDefaultLogger("CsvPlanarClusterReader", lvl)) {
+  if (m_cfg.outputClusters.empty()) {
+    throw std::invalid_argument("Missing cluster output collection");
+  }
+  if (m_cfg.outputHitIds.empty()) {
+    throw std::invalid_argument("Missing hit id output collection");
+  }
+  if (m_cfg.outputHitParticlesMap.empty()) {
+    throw std::invalid_argument("Missing hit-particles map output collection");
+  }
+  if (m_cfg.outputSimulatedHits.empty()) {
+    throw std::invalid_argument("Missing simulated hits output collection");
+  }
+  if (not m_cfg.trackingGeometry) {
+    throw std::invalid_argument("Missing tracking geometry");
+  }
+  // fill the geo id to surface map once to speed up lookups later on
+  m_cfg.trackingGeometry->visitSurfaces([this](const Acts::Surface* surface) {
+    this->m_surfaces[surface->geoID()] = surface;
+  });
+}
+
+std::string FW::CsvPlanarClusterReader::CsvPlanarClusterReader::name() const {
+  return "CsvPlanarClusterReader";
+}
+
+std::pair<size_t, size_t> FW::CsvPlanarClusterReader::availableEvents() const {
+  return m_eventsRange;
+}
+
+namespace {
+struct CompareHitId {
+  // support transparent comparision between identifiers and full objects
+  using is_transparent = void;
+  template <typename T>
+  constexpr bool operator()(const T& left, const T& right) const {
+    return left.hit_id < right.hit_id;
+  }
+  template <typename T>
+  constexpr bool operator()(uint64_t left_id, const T& right) const {
+    return left_id < right.hit_id;
+  }
+  template <typename T>
+  constexpr bool operator()(const T& left, uint64_t right_id) const {
+    return left.hit_id < right_id;
+  }
+};
+
+/// Convert separate volume/layer/module id into a single geometry identifier.
+inline Acts::GeometryID extractGeometryId(const FW::HitData& data) {
+  // if available, use the encoded geometry directly
+  if (data.geometry_id != 0u) {
+    return data.geometry_id;
+  }
+  // otherwise, reconstruct it from the available components
+  Acts::GeometryID geoId;
+  geoId.setVolume(data.volume_id);
+  geoId.setLayer(data.layer_id);
+  geoId.setSensitive(data.module_id);
+  return geoId;
+}
+
+struct CompareGeometryId {
+  bool operator()(const FW::HitData& left, const FW::HitData& right) const {
+    auto leftId = extractGeometryId(left).value();
+    auto rightId = extractGeometryId(right).value();
+    return leftId < rightId;
+  }
+};
+
+template <typename Data>
+inline std::vector<Data> readEverything(
+    const std::string& inputDir, const std::string& filename,
+    const std::vector<std::string>& optionalColumns, size_t event) {
+  std::string path = FW::perEventFilepath(inputDir, filename, event);
+  dfe::NamedTupleCsvReader<Data> reader(path, optionalColumns);
+
+  std::vector<Data> everything;
+  Data one;
+  while (reader.read(one)) {
+    everything.push_back(one);
+  }
+
+  return everything;
+}
+
+std::vector<FW::HitData> readHitsByGeoId(const std::string& inputDir,
+                                         size_t event) {
+  // geometry_id and t are optional columns
+  auto hits = readEverything<FW::HitData>(inputDir, "hits.csv",
+                                          {"geometry_id", "t"}, event);
+  // sort same way they will be sorted in the output container
+  std::sort(hits.begin(), hits.end(), CompareGeometryId{});
+  return hits;
+}
+
+std::vector<FW::CellData> readCellsByHitId(const std::string& inputDir,
+                                           size_t event) {
+  // timestamp is an optional element
+  auto cells =
+      readEverything<FW::CellData>(inputDir, "cells.csv", {"timestamp"}, event);
+  // sort for fast hit id look up
+  std::sort(cells.begin(), cells.end(), CompareHitId{});
+  return cells;
+}
+
+std::vector<FW::TruthHitData> readTruthHitsByHitId(const std::string& inputDir,
+                                                   size_t event) {
+  // define all optional columns
+  std::vector<std::string> optionalColumns = {
+      "geometry_id", "tt",      "te",     "deltapx",
+      "deltapy",     "deltapz", "deltae", "index",
+  };
+  auto truths = readEverything<FW::TruthHitData>(inputDir, "truth.csv",
+                                                 optionalColumns, event);
+  // sort for fast hit id look up
+  std::sort(truths.begin(), truths.end(), CompareHitId{});
+  return truths;
+}
+
+}  // namespace
+
+FW::ProcessCode FW::CsvPlanarClusterReader::read(
+    const FW::AlgorithmContext& ctx) {
+  // hit_id in the files is not required to be neither continuous nor
+  // monotonic. internally, we want continous indices within [0,#hits)
+  // to simplify data handling. to be able to perform this mapping we first
+  // read all data into memory before converting to the internal event data
+  // types.
+  auto hits = readHitsByGeoId(m_cfg.inputDir, ctx.eventNumber);
+  auto cells = readCellsByHitId(m_cfg.inputDir, ctx.eventNumber);
+  auto truths = readTruthHitsByHitId(m_cfg.inputDir, ctx.eventNumber);
+
+  // prepare containers for the hit data using the framework event data types
+  GeometryIdMultimap<Acts::PlanarModuleCluster> clusters;
+  std::vector<uint64_t> hitIds;
+  IndexMultimap<ActsFatras::Barcode> hitParticlesMap;
+  SimHitContainer simHits;
+  clusters.reserve(hits.size());
+  hitIds.reserve(hits.size());
+  hitParticlesMap.reserve(truths.size());
+  simHits.reserve(truths.size());
+
+  for (const HitData& hit : hits) {
+    Acts::GeometryID geoId = extractGeometryId(hit);
+
+    // find associated truth/ simulation hits
+    std::vector<std::size_t> simHitIndices;
+    {
+      auto range = makeRange(std::equal_range(truths.begin(), truths.end(),
+                                              hit.hit_id, CompareHitId{}));
+      simHitIndices.reserve(range.size());
+      for (const auto& truth : range) {
+        const auto simGeometryId = Acts::GeometryID(truth.geometry_id);
+        // TODO validate geo id consistency
+        const auto simParticleId = ActsFatras::Barcode(truth.particle_id);
+        const auto simIndex = truth.index;
+        ActsFatras::Hit::Vector4 simPos4{
+            truth.tx * Acts::UnitConstants::mm,
+            truth.ty * Acts::UnitConstants::mm,
+            truth.tz * Acts::UnitConstants::mm,
+            truth.tt * Acts::UnitConstants::ns,
+        };
+        ActsFatras::Hit::Vector4 simMom4{
+            truth.tpx * Acts::UnitConstants::GeV,
+            truth.tpy * Acts::UnitConstants::GeV,
+            truth.tpz * Acts::UnitConstants::GeV,
+            truth.te * Acts::UnitConstants::GeV,
+        };
+        ActsFatras::Hit::Vector4 simDelta4{
+            truth.deltapx * Acts::UnitConstants::GeV,
+            truth.deltapy * Acts::UnitConstants::GeV,
+            truth.deltapz * Acts::UnitConstants::GeV,
+            truth.deltae * Acts::UnitConstants::GeV,
+        };
+
+        // the cluster stores indices to the underlying simulation hits. thus
+        // their position in the container must be stable. the preordering of
+        // hits by geometry id should ensure that new sim hits are always added
+        // at the end and previously created ones rest at their existing
+        // locations.
+        auto inserted = simHits.emplace_hint(simHits.end(), simGeometryId,
+                                             simParticleId, simPos4, simMom4,
+                                             simMom4 + simDelta4, simIndex);
+        if (std::next(inserted) != simHits.end()) {
+          ACTS_FATAL("Truth hit sorting broke for input hit id " << hit.hit_id);
+          return ProcessCode::ABORT;
+        }
+        simHitIndices.push_back(simHits.index_of(inserted));
+      }
+    }
+
+    // find matching pixel cell information
+    std::vector<Acts::DigitizationCell> digitizationCells;
+    {
+      auto range = makeRange(std::equal_range(cells.begin(), cells.end(),
+                                              hit.hit_id, CompareHitId{}));
+      for (const auto& c : range) {
+        digitizationCells.emplace_back(c.ch0, c.ch1, c.value);
+      }
+    }
+
+    // identify hit surface
+    auto it = m_surfaces.find(geoId);
+    if (it == m_surfaces.end() or not it->second) {
+      ACTS_FATAL("Could not retrieve the surface for hit " << hit);
+      return ProcessCode::ABORT;
+    }
+    const Acts::Surface& surface = *(it->second);
+
+    // transform global hit coordinates into local coordinates on the surface
+    Acts::Vector3D pos(hit.x * Acts::UnitConstants::mm,
+                       hit.y * Acts::UnitConstants::mm,
+                       hit.z * Acts::UnitConstants::mm);
+    double time = hit.t * Acts::UnitConstants::ns;
+    Acts::Vector3D mom(1, 1, 1);  // fake momentum
+    Acts::Vector2D local(0, 0);
+    surface.globalToLocal(ctx.geoContext, pos, mom, local);
+    // TODO what to use as cluster uncertainty?
+    Acts::ActsSymMatrixD<3> cov = Acts::ActsSymMatrixD<3>::Identity();
+    // create the planar cluster
+    Acts::PlanarModuleCluster cluster(
+        surface.getSharedPtr(),
+        Identifier(identifier_type(geoId.value()), std::move(simHitIndices)),
+        std::move(cov), local[0], local[1], time, std::move(digitizationCells));
+
+    // due to the previous sorting of the raw hit data by geometry id, new
+    // clusters should always end up at the end of the container. previous
+    // elements were not touched; cluster indices remain stable and can
+    // be used to identify the hit.
+    auto inserted =
+        clusters.emplace_hint(clusters.end(), geoId, std::move(cluster));
+    if (std::next(inserted) != clusters.end()) {
+      ACTS_FATAL("Something went horribly wrong with the hit sorting");
+      return ProcessCode::ABORT;
+    }
+    auto hitIndex = clusters.index_of(inserted);
+    auto truthRange = makeRange(std::equal_range(truths.begin(), truths.end(),
+                                                 hit.hit_id, CompareHitId{}));
+    for (const auto& truth : truthRange) {
+      hitParticlesMap.emplace_hint(hitParticlesMap.end(), hitIndex,
+                                   truth.particle_id);
+    }
+
+    // map internal hit/cluster index back to original, non-monotonic hit id
+    hitIds.push_back(hit.hit_id);
+  }
+
+  // write the data to the EventStore
+  ctx.eventStore.add(m_cfg.outputClusters, std::move(clusters));
+  ctx.eventStore.add(m_cfg.outputHitIds, std::move(hitIds));
+  ctx.eventStore.add(m_cfg.outputHitParticlesMap, std::move(hitParticlesMap));
+  ctx.eventStore.add(m_cfg.outputSimulatedHits, std::move(simHits));
+
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Csv/src/CsvPlanarClusterWriter.cpp b/Io/Csv/src/CsvPlanarClusterWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3d3cb041fe1be32dc72ab642c3d5271c1574f0b0
--- /dev/null
+++ b/Io/Csv/src/CsvPlanarClusterWriter.cpp
@@ -0,0 +1,135 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvPlanarClusterWriter.hpp"
+
+#include <dfe/dfe_io_dsv.hpp>
+#include <stdexcept>
+
+#include "ACTFW/EventData/SimHit.hpp"
+#include "ACTFW/EventData/SimIdentifier.hpp"
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/EventData/SimVertex.hpp"
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/Plugins/Digitization/PlanarModuleCluster.hpp"
+#include "Acts/Utilities/Units.hpp"
+#include "TrackMlData.hpp"
+
+FW::CsvPlanarClusterWriter::CsvPlanarClusterWriter(
+    const FW::CsvPlanarClusterWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputClusters, "CsvPlanarClusterWriter", lvl), m_cfg(cfg) {
+  // inputClusters is already checked by base constructor
+  if (m_cfg.inputSimulatedHits.empty()) {
+    throw std::invalid_argument("Missing simulated hits input collection");
+  }
+}
+
+FW::ProcessCode FW::CsvPlanarClusterWriter::writeT(
+    const AlgorithmContext& ctx,
+    const FW::GeometryIdMultimap<Acts::PlanarModuleCluster>& clusters) {
+  // retrieve simulated hits
+  const auto& simHits =
+      ctx.eventStore.get<SimHitContainer>(m_cfg.inputSimulatedHits);
+
+  // open per-event file for all components
+  std::string pathHits =
+      perEventFilepath(m_cfg.outputDir, "hits.csv", ctx.eventNumber);
+  std::string pathCells =
+      perEventFilepath(m_cfg.outputDir, "cells.csv", ctx.eventNumber);
+  std::string pathTruth =
+      perEventFilepath(m_cfg.outputDir, "truth.csv", ctx.eventNumber);
+
+  dfe::NamedTupleCsvWriter<HitData> writerHits(pathHits, m_cfg.outputPrecision);
+  dfe::NamedTupleCsvWriter<CellData> writerCells(pathCells,
+                                                 m_cfg.outputPrecision);
+  dfe::NamedTupleCsvWriter<TruthHitData> writerTruth(pathTruth,
+                                                     m_cfg.outputPrecision);
+
+  HitData hit;
+  CellData cell;
+  TruthHitData truth;
+  // will be reused as hit counter
+  hit.hit_id = 0;
+
+  for (const auto& entry : clusters) {
+    Acts::GeometryID geoId = entry.first;
+    const Acts::PlanarModuleCluster& cluster = entry.second;
+    // local cluster information
+    const auto& parameters = cluster.parameters();
+    Acts::Vector2D localPos(parameters[0], parameters[1]);
+    Acts::Vector3D globalFakeMom(1, 1, 1);
+    Acts::Vector3D globalPos(0, 0, 0);
+    // transform local into global position information
+    cluster.referenceSurface().localToGlobal(ctx.geoContext, localPos,
+                                             globalFakeMom, globalPos);
+
+    // encoded geometry identifier
+    hit.geometry_id = geoId.value();
+    // (partially) decoded geometry identifier
+    hit.volume_id = geoId.volume();
+    hit.layer_id = geoId.layer();
+    hit.module_id = geoId.sensitive();
+    // write global hit information
+    hit.x = globalPos.x() / Acts::UnitConstants::mm;
+    hit.y = globalPos.y() / Acts::UnitConstants::mm;
+    hit.z = globalPos.z() / Acts::UnitConstants::mm;
+    hit.t = parameters[2] / Acts::UnitConstants::ns;
+    writerHits.append(hit);
+
+    // write local cell information
+    cell.hit_id = hit.hit_id;
+    for (auto& c : cluster.digitizationCells()) {
+      cell.ch0 = c.channel0;
+      cell.ch1 = c.channel1;
+      // TODO store digitial timestamp once added to the cell definition
+      cell.timestamp = 0;
+      cell.value = c.data;
+      writerCells.append(cell);
+    }
+
+    // write hit-particle truth association
+    // each hit can have multiple particles, e.g. in a dense environment
+    truth.hit_id = hit.hit_id;
+    truth.geometry_id = hit.geometry_id;
+    for (auto idx : cluster.sourceLink().indices()) {
+      auto it = simHits.nth(idx);
+      if (it == simHits.end()) {
+        ACTS_FATAL("Simulation hit with index " << idx << " does not exist");
+        return ProcessCode::ABORT;
+      }
+
+      const auto& simHit = *it;
+      truth.particle_id = simHit.particleId().value();
+      // hit position
+      truth.tx = simHit.position().x() / Acts::UnitConstants::mm;
+      truth.ty = simHit.position().y() / Acts::UnitConstants::mm;
+      truth.tz = simHit.position().z() / Acts::UnitConstants::mm;
+      truth.tt = simHit.time() / Acts::UnitConstants::ns;
+      // particle four-momentum before interaction
+      truth.tpx = simHit.momentum4Before().x() / Acts::UnitConstants::GeV;
+      truth.tpy = simHit.momentum4Before().y() / Acts::UnitConstants::GeV;
+      truth.tpz = simHit.momentum4Before().z() / Acts::UnitConstants::GeV;
+      truth.te = simHit.momentum4Before().w() / Acts::UnitConstants::GeV;
+      // particle four-momentum change due to interaction
+      const auto delta4 = simHit.momentum4After() - simHit.momentum4Before();
+      truth.deltapx = delta4.x() / Acts::UnitConstants::GeV;
+      truth.deltapy = delta4.y() / Acts::UnitConstants::GeV;
+      truth.deltapz = delta4.z() / Acts::UnitConstants::GeV;
+      truth.deltae = delta4.w() / Acts::UnitConstants::GeV;
+      // TODO write hit index along the particle trajectory
+      truth.index = simHit.index();
+      writerTruth.append(truth);
+    }
+
+    // increase hit id for next iteration
+    hit.hit_id += 1;
+  }
+
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Csv/src/CsvTrackingGeometryWriter.cpp b/Io/Csv/src/CsvTrackingGeometryWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..208e9c0c67722c483a856f9fad88a9df4a46624a
--- /dev/null
+++ b/Io/Csv/src/CsvTrackingGeometryWriter.cpp
@@ -0,0 +1,168 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Csv/CsvTrackingGeometryWriter.hpp"
+
+#include <Acts/Geometry/TrackingVolume.hpp>
+#include <Acts/Plugins/Digitization/CartesianSegmentation.hpp>
+#include <Acts/Plugins/Digitization/DigitizationModule.hpp>
+#include <Acts/Plugins/Identification/IdentifiedDetectorElement.hpp>
+#include <Acts/Surfaces/Surface.hpp>
+#include <Acts/Utilities/Units.hpp>
+#include <dfe/dfe_io_dsv.hpp>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+
+#include "ACTFW/Utilities/Paths.hpp"
+#include "TrackMlData.hpp"
+
+using namespace FW;
+
+CsvTrackingGeometryWriter::CsvTrackingGeometryWriter(
+    const CsvTrackingGeometryWriter::Config& cfg, Acts::Logging::Level lvl)
+    : m_cfg(cfg),
+      m_world(nullptr),
+      m_logger(Acts::getDefaultLogger("CsvTrackingGeometryWriter", lvl))
+
+{
+  if (not m_cfg.trackingGeometry) {
+    throw std::invalid_argument("Missing tracking geometry");
+  }
+  m_world = m_cfg.trackingGeometry->highestTrackingVolume();
+  if (not m_world) {
+    throw std::invalid_argument("Could not identify the world volume");
+  }
+}
+
+std::string CsvTrackingGeometryWriter::name() const {
+  return "CsvTrackingGeometryWriter";
+}
+
+namespace {
+using SurfaceWriter = dfe::NamedTupleCsvWriter<SurfaceData>;
+
+/// Write a single surface.
+void writeSurface(SurfaceWriter& writer, const Acts::Surface& surface,
+                  const Acts::GeometryContext& geoCtx) {
+  SurfaceData data;
+
+  // encoded and partially decoded geometry identifier
+  data.geometry_id = surface.geoID().value();
+  data.volume_id = surface.geoID().volume();
+  data.layer_id = surface.geoID().layer();
+  data.module_id = surface.geoID().sensitive();
+  // center position
+  auto center = surface.center(geoCtx);
+  data.cx = center.x() / Acts::UnitConstants::mm;
+  data.cy = center.y() / Acts::UnitConstants::mm;
+  data.cz = center.z() / Acts::UnitConstants::mm;
+  // rotation matrix components are unit-less
+  auto transform = surface.transform(geoCtx);
+  data.rot_xu = transform(0, 0);
+  data.rot_xv = transform(0, 1);
+  data.rot_xw = transform(0, 2);
+  data.rot_yu = transform(1, 0);
+  data.rot_yv = transform(1, 1);
+  data.rot_yw = transform(1, 2);
+  data.rot_zu = transform(2, 0);
+  data.rot_zv = transform(2, 1);
+  data.rot_zw = transform(2, 2);
+
+  // module thickness
+  if (surface.associatedDetectorElement()) {
+    const auto* detElement =
+        dynamic_cast<const Acts::IdentifiedDetectorElement*>(
+            surface.associatedDetectorElement());
+    if (detElement) {
+      data.module_t = detElement->thickness() / Acts::UnitConstants::mm;
+    }
+  }
+
+  // bounds and pitch (if available)
+  const auto& bounds = surface.bounds();
+  const auto* planarBounds = dynamic_cast<const Acts::PlanarBounds*>(&bounds);
+  if (planarBounds) {
+    // extract limits from value store
+    auto boundValues = surface.bounds().values();
+    if (boundValues.size() == 2) {
+      data.module_minhu = boundValues[0] / Acts::UnitConstants::mm;
+      data.module_minhu = boundValues[0] / Acts::UnitConstants::mm;
+      data.module_minhu = boundValues[1] / Acts::UnitConstants::mm;
+    } else if (boundValues.size() == 3) {
+      data.module_minhu = boundValues[0] / Acts::UnitConstants::mm;
+      data.module_minhu = boundValues[0] / Acts::UnitConstants::mm;
+      data.module_minhu = boundValues[1] / Acts::UnitConstants::mm;
+    }
+    // get the pitch from the digitization module
+    const auto* detElement =
+        dynamic_cast<const Acts::IdentifiedDetectorElement*>(
+            surface.associatedDetectorElement());
+    if (detElement and detElement->digitizationModule()) {
+      auto dModule = detElement->digitizationModule();
+      // dynamic_cast to CartesianSegmentation
+      const auto* cSegmentation =
+          dynamic_cast<const Acts::CartesianSegmentation*>(
+              &(dModule->segmentation()));
+      if (cSegmentation) {
+        auto pitch = cSegmentation->pitch();
+        data.pitch_u = pitch.first / Acts::UnitConstants::mm;
+        data.pitch_u = pitch.second / Acts::UnitConstants::mm;
+      }
+    }
+  }
+
+  writer.append(data);
+}
+
+/// Write all child surfaces and descend into confined volumes.
+void writeVolume(SurfaceWriter& writer, const Acts::TrackingVolume& volume,
+                 const Acts::GeometryContext& geoCtx) {
+  // process all layers that are directly stored within this volume
+  if (volume.confinedLayers()) {
+    for (auto layer : volume.confinedLayers()->arrayObjects()) {
+      // we jump navigation layers
+      if (layer->layerType() == Acts::navigation) {
+        continue;
+      }
+      // check for sensitive surfaces
+      if (layer->surfaceArray()) {
+        for (auto surface : layer->surfaceArray()->surfaces()) {
+          if (surface) {
+            writeSurface(writer, *surface, geoCtx);
+          }
+        }
+      }
+    }
+  }
+  // step down into hierarchy to process all child volumnes
+  if (volume.confinedVolumes()) {
+    for (auto confined : volume.confinedVolumes()->arrayObjects()) {
+      writeVolume(writer, *confined.get(), geoCtx);
+    }
+  }
+}
+}  // namespace
+
+ProcessCode CsvTrackingGeometryWriter::write(const AlgorithmContext& ctx) {
+  if (not m_cfg.writePerEvent) {
+    return ProcessCode::SUCCESS;
+  }
+  SurfaceWriter writer(
+      perEventFilepath(m_cfg.outputDir, "detectors.csv", ctx.eventNumber),
+      m_cfg.outputPrecision);
+  writeVolume(writer, *m_world, ctx.geoContext);
+  return ProcessCode::SUCCESS;
+}
+
+ProcessCode CsvTrackingGeometryWriter::endRun() {
+  SurfaceWriter writer(joinPaths(m_cfg.outputDir, "detectors.csv"),
+                       m_cfg.outputPrecision);
+  writeVolume(writer, *m_world, Acts::GeometryContext());
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Csv/src/TrackMlData.hpp b/Io/Csv/src/TrackMlData.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..411e300de287d85185c5e9cd56a27216c315c1d4
--- /dev/null
+++ b/Io/Csv/src/TrackMlData.hpp
@@ -0,0 +1,130 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @brief Plain structs that each define one row in a TrackML csv file
+
+#pragma once
+
+#include <cstdint>
+#include <dfe/dfe_namedtuple.hpp>
+
+namespace FW {
+
+struct ParticleData {
+  /// Event-unique particle identifier a.k.a barcode.
+  uint64_t particle_id;
+  /// Particle type number a.k.a. PDG particle number.
+  int32_t particle_type;
+  /// Production process type. Not available in the TrackML datasets.
+  uint32_t process = 0u;
+  /// Production position components in mm.
+  float vx, vy, vz;
+  // Production time in ns. Not available in the TrackML datasets.
+  float vt = 0.0f;
+  /// Momentum components in GeV.
+  float px, py, pz;
+  /// Mass in GeV. Not available in the TrackML datasets
+  float m = 0.0f;
+  /// Charge in e.
+  float q;
+
+  DFE_NAMEDTUPLE(ParticleData, particle_id, particle_type, process, vx, vy, vz,
+                 vt, px, py, pz, m, q);
+};
+
+struct TruthHitData {
+  /// Event-unique hit identifier. As defined for the simulated hit below and
+  /// used to link back to it; same value can appear multiple times here due to
+  /// shared hits in dense environments.
+  uint64_t hit_id;
+  /// Hit surface identifier. Not available in the TrackML datasets.
+  uint64_t geometry_id = 0u;
+  /// Event-unique particle identifier of the generating particle.
+  uint64_t particle_id;
+  /// True global hit position components in mm.
+  float tx, ty, tz;
+  // True global hit time in ns. Not available in the TrackML datasets.
+  float tt = 0.0f;
+  /// True particle momentum in GeV before interaction.
+  float tpx, tpy, tpz;
+  /// True particle energy in GeV before interaction.
+  /// Not available in the TrackML datasets.
+  float te = 0.0f;
+  /// True four-momentum change in GeV due to interaction.
+  /// Not available in the TrackML datasets.
+  float deltapx = 0.0f;
+  float deltapy = 0.0f;
+  float deltapz = 0.0f;
+  float deltae = 0.0f;
+  // Hit index along the trajectory. Not available in the TrackML datasets.
+  int32_t index = -1;
+
+  DFE_NAMEDTUPLE(TruthHitData, hit_id, particle_id, geometry_id, tx, ty, tz, tt,
+                 tpx, tpy, tpz, te, deltapx, deltapy, deltapz, deltae, index);
+};
+
+struct HitData {
+  /// Event-unique hit identifier. Each value can appear at most once.
+  uint64_t hit_id;
+  /// Hit surface identifier. Not available in the TrackML datasets.
+  uint64_t geometry_id = 0u;
+  /// Partially decoded hit surface identifier components.
+  uint32_t volume_id, layer_id, module_id;
+  /// Global hit position components in mm.
+  float x, y, z;
+  /// Global hit time in ns. Not available in the TrackML datasets.
+  float t = 0.0f;
+
+  DFE_NAMEDTUPLE(HitData, hit_id, geometry_id, volume_id, layer_id, module_id,
+                 x, y, z, t);
+};
+
+struct CellData {
+  /// Event-unique hit identifier. As defined for the simulated hit above and
+  /// used to link back to it; same value can appear multiple times for clusters
+  /// with more than one active cell.
+  uint64_t hit_id;
+  /// Digital cell address/ channel identifier. These should have been named
+  /// channel{0,1} but we cannot change it now to avoid breaking backward
+  /// compatibility.
+  int32_t ch0, ch1;
+  /// Digital cell timestamp. Not available in the TrackML datasets.
+  int32_t timestamp = 0;
+  /// (Digital) measured cell value, e.g. amplitude or time-over-threshold.
+  int32_t value;
+
+  DFE_NAMEDTUPLE(CellData, hit_id, ch0, ch1, timestamp, value);
+};
+
+struct SurfaceData {
+  /// Surface identifier. Not available in the TrackML datasets.
+  uint64_t geometry_id;
+  /// Partially decoded surface identifier components.
+  uint32_t volume_id, layer_id, module_id;
+  /// Center position components in mm.
+  float cx, cy, cz;
+  /// Rotation matrix components.
+  float rot_xu, rot_xv, rot_xw;
+  float rot_yu, rot_yv, rot_yw;
+  float rot_zu, rot_zv, rot_zw;
+  /// Limits and pitches in mm. Not always available.
+  float module_t = -1;
+  float module_minhu = -1;
+  float module_maxhu = -1;
+  float module_hv = -1;
+  float pitch_u = -1;
+  float pitch_v = -1;
+
+  DFE_NAMEDTUPLE(SurfaceData, geometry_id, volume_id, layer_id, module_id, cx,
+                 cy, cz, rot_xu, rot_xv, rot_xw, rot_yu, rot_yv, rot_yw, rot_zu,
+                 rot_zv, rot_zw, module_t, module_minhu, module_maxhu,
+                 module_hv, pitch_u, pitch_v);
+};
+
+}  // namespace FW
diff --git a/Io/HepMC3/CMakeLists.txt b/Io/HepMC3/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e1e8534f345b9b463f760eed1da8d62dd8bb46bc
--- /dev/null
+++ b/Io/HepMC3/CMakeLists.txt
@@ -0,0 +1,18 @@
+find_package(HepMC3 REQUIRED)
+add_library(
+  ActsExamplesIoHepMC3 SHARED
+  src/HepMC3Event.cpp
+  src/HepMC3Particle.cpp
+  src/HepMC3Reader.cpp
+  src/HepMC3Vertex.cpp
+  src/HepMC3Writer.cpp)
+target_include_directories(
+  ActsExamplesIoHepMC3
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> ${HEPMC3_INCLUDE_DIR})
+target_link_libraries(
+  ActsExamplesIoHepMC3
+  PUBLIC ActsCore ActsExamplesFramework ${HEPMC3_LIBRARIES} HepPID)
+
+install(
+  TARGETS ActsExamplesIoHepMC3
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Event.hpp b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Event.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c093f4d16dcb9d3e703bbc3d6545e4f67827c7a3
--- /dev/null
+++ b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Event.hpp
@@ -0,0 +1,180 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <HepMC3/FourVector.h>
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/GenParticle.h>
+#include <HepMC3/GenVertex.h>
+#include <HepPID/ParticleIDMethods.hh>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/EventData/SimVertex.hpp"
+#include "Acts/Utilities/Units.hpp"
+
+namespace FW {
+
+/// Helper struct to convert HepMC3 event to the internal format.
+struct HepMC3Event {
+ public:
+  ///
+  /// Setter
+  ///
+
+  /// @brief Sets new units for momentums
+  /// @note The allowed units are MeV and Gev
+  /// @param event event in HepMC data type
+  /// @param momentumUnit new unit of momentum
+  void momentumUnit(std::shared_ptr<HepMC3::GenEvent> event,
+                    const double momentumUnit);
+
+  /// @brief Sets new units for lengths
+  /// @note The allowed units are mm and cm
+  /// @param event event in HepMC data type
+  /// @param lengthUnit new unit of length
+  void lengthUnit(std::shared_ptr<HepMC3::GenEvent> event,
+                  const double lengthUnit);
+
+  /// @brief Shifts the positioning of an event in space and time
+  /// @param event event in HepMC data type
+  /// @param deltaPos relative spatial shift that will be applied
+  /// @param deltaTime relative time shift that will be applied
+  void shiftPositionBy(std::shared_ptr<HepMC3::GenEvent> event,
+                       const Acts::Vector3D& deltaPos, const double deltaTime);
+
+  /// @brief Shifts the positioning of an event to a paint in space and time
+  /// @param event event in HepMC data type
+  /// @param pos new position of the event
+  /// @param time new time of the event
+  void shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                       const Acts::Vector3D& pos, const double time);
+
+  /// @brief Shifts the positioning of an event to a paint in space
+  /// @param event event in HepMC data type
+  /// @param pos new position of the event
+  void shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                       const Acts::Vector3D& pos);
+
+  /// @brief Shifts the positioning of an event to a paint in time
+  /// @param event event in HepMC data type
+  /// @param time new time of the event
+  void shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                       const double time);
+
+  ///
+  /// Adder
+  ///
+
+  /// @brief Adds a new particle
+  /// @param event event in HepMC data type
+  /// @param particle new particle that will be added
+  void addParticle(std::shared_ptr<HepMC3::GenEvent> event,
+                   std::shared_ptr<SimParticle> particle);
+
+  /// @brief Adds a new vertex
+  /// @param event event in HepMC data type
+  /// @param vertex new vertex that will be added
+  /// @note The statuses are not represented in Acts and therefore set to 0
+  void addVertex(std::shared_ptr<HepMC3::GenEvent> event,
+                 const std::shared_ptr<SimVertex> vertex);
+  ///
+  /// Remover
+  ///
+
+  /// @brief Removes a particle from the record
+  /// @param event event in HepMC data type
+  /// @param particle particle that will be removed
+  void removeParticle(std::shared_ptr<HepMC3::GenEvent> event,
+                      const std::shared_ptr<SimParticle>& particle);
+
+  /// @brief Removes a vertex from the record
+  /// @note The identification of the vertex is potentially unstable (c.f.
+  /// HepMC3Event::compareVertices())
+  /// @param event event in HepMC data type
+  /// @param vertex vertex that will be removed
+  void removeVertex(std::shared_ptr<HepMC3::GenEvent> event,
+                    const std::shared_ptr<SimVertex>& vertex);
+
+  ///
+  /// Getter
+  ///
+
+  /// @brief Getter of the unit of momentum used
+  /// @param event event in HepMC data type
+  /// @return unit of momentum
+  double momentumUnit(const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Getter of the unit of length used
+  /// @param event event in HepMC data type
+  /// @return unit of length
+  double lengthUnit(const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Getter of the position of the event
+  /// @param event event in HepMC data type
+  /// @return vector to the location of the event
+  Acts::Vector3D eventPos(const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Getter of the time of the event
+  /// @param event event in HepMC data type
+  /// @return time of the event
+  double eventTime(const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Get list of particles
+  /// @param event event in HepMC data type
+  /// @return List of particles
+  std::vector<std::unique_ptr<SimParticle>> particles(
+      const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Get list of vertices
+  /// @param event event in HepMC data type
+  /// @return List of vertices
+  std::vector<std::unique_ptr<SimVertex>> vertices(
+      const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Get beam particles
+  /// @param event event in HepMC data type
+  /// @return List of beam particles
+  std::vector<std::unique_ptr<SimParticle>> beams(
+      const std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Get final state particles
+  /// @param event event in HepMC data type
+  /// @return List of final state particles
+  std::vector<std::unique_ptr<SimParticle>> finalState(
+      const std::shared_ptr<HepMC3::GenEvent> event);
+
+ private:
+  /// @brief Converts an SimParticle into HepMC3::GenParticle
+  /// @note The conversion ignores HepMC status codes
+  /// @param actsParticle Acts particle that will be converted
+  /// @return converted particle
+  HepMC3::GenParticlePtr actsParticleToGen(
+      std::shared_ptr<SimParticle> actsParticle);
+
+  /// @brief Converts an Acts vertex to a HepMC3::GenVertexPtr
+  /// @note The conversion ignores HepMC status codes
+  /// @param actsVertex Acts vertex that will be converted
+  /// @return Converted Acts vertex to HepMC3::GenVertexPtr
+  HepMC3::GenVertexPtr createGenVertex(
+      const std::shared_ptr<SimVertex>& actsVertex);
+
+  /// @brief Compares an Acts vertex with a HepMC3::GenVertex
+  /// @note An Acts vertex does not store a barcode. Therefore the content of
+  /// both vertices is compared. The position, time and number of incoming and
+  /// outgoing particles will be compared. Since a second vertex could exist in
+  /// the record with identical informations (although unlikely), this
+  /// comparison could lead to false positive results. On the other hand, a
+  /// numerical deviation of the parameters could lead to a false negative.
+  /// @param actsVertex Acts vertex
+  /// @param genVertex HepMC3::GenVertex
+  /// @return boolean result if both vertices are identical
+  bool compareVertices(const std::shared_ptr<SimVertex>& actsVertex,
+                       const HepMC3::GenVertexPtr& genVertex);
+};
+}  // namespace FW
diff --git a/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Particle.hpp b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Particle.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8bfcdac829cefddf98187fad75e8529d37782327
--- /dev/null
+++ b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Particle.hpp
@@ -0,0 +1,95 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <HepMC3/FourVector.h>
+#include <HepMC3/GenParticle.h>
+#include <HepMC3/GenVertex.h>
+#include <HepPID/ParticleIDMethods.hh>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/EventData/SimVertex.hpp"
+
+namespace FW {
+
+/// Helper struct to convert HepMC3 particles to internal format.
+struct HepMC3Particle {
+ public:
+  /// @brief Returns the particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return corresponding Acts particle
+  std::unique_ptr<SimParticle> particle(
+      const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the id of the particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return id of the particle
+  int id(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the production vertex of the particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return production vertex of the particle
+  std::unique_ptr<SimVertex> productionVertex(
+      const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the end vertex of the particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return end vertex of the particle
+  std::unique_ptr<SimVertex> endVertex(
+      const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the PDG code of a particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return PDG code of the particle
+  int pdgID(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the momentum of a particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return momentum of the particle
+  Acts::Vector3D momentum(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the energy of a particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return energy of the particle
+  double energy(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the mass of a particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return mass of the particle
+  double mass(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Returns the charge of a particle translated into Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @return charge of the particle
+  double charge(const std::shared_ptr<HepMC3::GenParticle> particle);
+
+  /// @brief Sets the PDG code of a particle translated from Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @param pid PDG code that will be set
+  void pdgID(std::shared_ptr<HepMC3::GenParticle> particle, const int pid);
+
+  /// @brief Sets the momentum of a particle translated from Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @param mom momentum that will be set
+  void momentum(std::shared_ptr<HepMC3::GenParticle> particle,
+                const Acts::Vector3D& mom);
+
+  /// @brief Sets the energy of a particle translated from Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @param energy energy that will be set
+  void energy(std::shared_ptr<HepMC3::GenParticle> particle,
+              const double energy);
+
+  /// @brief Sets the mass of a particle translated from Acts
+  /// @param particle HepMC3::GenParticle particle
+  /// @param mass mass that will be set
+  void mass(std::shared_ptr<HepMC3::GenParticle> particle, const double mass);
+};
+
+}  // namespace FW
diff --git a/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Reader.hpp b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Reader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6c8b1c34d4ecb0ac50dca6040842d5391fc7a1cb
--- /dev/null
+++ b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Reader.hpp
@@ -0,0 +1,31 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/ReaderAscii.h>
+
+namespace FW {
+
+/// HepMC3 event reader.
+struct HepMC3ReaderAscii {
+ public:
+  /// @brief Reads an event from file
+  /// @param reader reader of run files
+  /// @param event storage of the read event
+  /// @return boolean indicator if the reading was successful
+  bool readEvent(HepMC3::ReaderAscii& reader,
+                 std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Reports the status of the reader
+  /// @param reader reader of run files
+  /// @return boolean status indicator
+  bool status(HepMC3::ReaderAscii& reader);
+};
+}  // namespace FW
diff --git a/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..dfcf70625804460edecaad764a9559988e6ad9e5
--- /dev/null
+++ b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp
@@ -0,0 +1,121 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <HepMC3/FourVector.h>
+#include <HepMC3/GenParticle.h>
+#include <HepMC3/GenVertex.h>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/EventData/SimVertex.hpp"
+
+namespace FW {
+
+/// Helper struct to convert HepMC3 vertex into the internal format.
+struct HepMC3Vertex {
+ public:
+  /// @brief Returns a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return corresponding Acts vertex
+  std::unique_ptr<SimVertex> processVertex(
+      const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns a boolean expression if a vertex is in an event translated
+  /// into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return boolean expression if the vertex is in an event
+  bool inEvent(const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return id of the vertex
+  int id(const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns the incoming particles of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return incoming particles of the vertex
+  std::vector<SimParticle> particlesIn(
+      const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns the outgoing particles of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return outgoing particles of the vertex
+  std::vector<SimParticle> particlesOut(
+      const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns the position of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return position of the vertex
+  Acts::Vector3D position(const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Returns the time of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @return time of the vertex
+  double time(const std::shared_ptr<HepMC3::GenVertex> vertex);
+
+  /// @brief Adds an incoming particle to a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param particle incoming particle that will be added
+  void addParticleIn(std::shared_ptr<HepMC3::GenVertex> vertex,
+                     std::shared_ptr<SimParticle> particle);
+
+  /// @brief Adds an outgoing particle to a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param particle outgoing particle that will be added
+  void addParticleOut(std::shared_ptr<HepMC3::GenVertex> vertex,
+                      std::shared_ptr<SimParticle> particle);
+
+  /// @brief Removes an incoming particle from a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param particle incoming particle that will be removed
+  void removeParticleIn(std::shared_ptr<HepMC3::GenVertex> vertex,
+                        std::shared_ptr<SimParticle> particle);
+
+  /// @brief Removes an outgoing particle from a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param particle outgoing particle that will be removed
+  void removeParticleOut(std::shared_ptr<HepMC3::GenVertex> vertex,
+                         std::shared_ptr<SimParticle> particle);
+
+  /// @brief Sets the position of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param pos new position of the vertex
+  void position(const std::shared_ptr<HepMC3::GenVertex> vertex,
+                Acts::Vector3D pos);
+
+  /// @brief Sets the time of a vertex translated into Acts
+  /// @param vertex vertex in HepMC data type
+  /// @param time new time of the vertex
+  void time(const std::shared_ptr<HepMC3::GenVertex> vertex, double time);
+
+ private:
+  /// @brief Converts HepMC3::GenParticle objects into Acts
+  /// @param genParticles list of HepMC3::GenParticle objects
+  /// @return converted list
+  std::vector<SimParticle> genParticlesToActs(
+      const std::vector<HepMC3::GenParticlePtr>& genParticles);
+
+  /// @brief Converts an SimParticle into HepMC3::GenParticle
+  /// @note The conversion ignores HepMC status codes
+  /// @param actsParticle Acts particle that will be converted
+  /// @return converted particle
+  HepMC3::GenParticlePtr actsParticleToGen(
+      std::shared_ptr<SimParticle> actsParticle);
+
+  /// @brief Finds a HepMC3::GenParticle from a list that matches an
+  /// SimParticle object
+  /// @param genParticles list of HepMC particles
+  /// @param actsParticle Acts particle
+  /// @return HepMC particle that matched with the Acts particle or nullptr if
+  /// no match was found
+  HepMC3::GenParticlePtr matchParticles(
+      const std::vector<HepMC3::GenParticlePtr>& genParticles,
+      std::shared_ptr<SimParticle> actsParticle);
+};
+}  // namespace FW
diff --git a/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Writer.hpp b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Writer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e14f860d6ca952041879b86185764a823a3fa84c
--- /dev/null
+++ b/Io/HepMC3/include/ACTFW/Plugins/HepMC3/HepMC3Writer.hpp
@@ -0,0 +1,33 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/WriterAscii.h>
+
+namespace FW {
+
+/// HepMC3 event writer.
+struct HepMC3WriterAscii {
+ public:
+  /// @brief Writes an event to file
+  /// @param writer writer of run files
+  /// @param event storage of the event
+  /// @return boolean indicator if the writing was successful
+  /// @note HepMC3 does not state a success or failure. The returned argument is
+  /// always true.
+  bool writeEvent(HepMC3::WriterAscii& writer,
+                  std::shared_ptr<HepMC3::GenEvent> event);
+
+  /// @brief Reports the status of the writer
+  /// @param writer writer of run files
+  /// @return boolean status indicator
+  bool status(HepMC3::WriterAscii& writer);
+};
+}  // namespace FW
diff --git a/Io/HepMC3/src/HepMC3Event.cpp b/Io/HepMC3/src/HepMC3Event.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..33f00a8f97a6b99900b4b8fb107a3ddaed644f75
--- /dev/null
+++ b/Io/HepMC3/src/HepMC3Event.cpp
@@ -0,0 +1,286 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Event.hpp"
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Particle.hpp"
+#include "ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp"
+
+///
+/// Setter
+///
+
+void FW::HepMC3Event::momentumUnit(std::shared_ptr<HepMC3::GenEvent> event,
+                                   const double momentumUnit) {
+  // Check, if the momentum unit fits Acts::units::_MeV or _GeV
+  HepMC3::Units::MomentumUnit mom;
+  if (momentumUnit == Acts::units::_MeV)
+    mom = HepMC3::Units::MomentumUnit::MEV;
+  else if (momentumUnit == Acts::units::_GeV)
+    mom = HepMC3::Units::MomentumUnit::GEV;
+  else {
+    // Report invalid momentum unit and set GeV
+    std::cout << "Invalid unit of momentum: " << momentumUnit << std::endl;
+    std::cout << "Momentum unit [GeV] will be used instead" << std::endl;
+    mom = HepMC3::Units::MomentumUnit::GEV;
+  }
+  // Set units
+  event->set_units(mom, event->length_unit());
+}
+
+void FW::HepMC3Event::lengthUnit(std::shared_ptr<HepMC3::GenEvent> event,
+                                 const double lengthUnit) {
+  // Check, if the length unit fits Acts::units::_mm or _cm
+  HepMC3::Units::LengthUnit len;
+  if (lengthUnit == Acts::units::_mm)
+    len = HepMC3::Units::LengthUnit::MM;
+  else if (lengthUnit == Acts::units::_cm)
+    len = HepMC3::Units::LengthUnit::CM;
+  else {
+    // Report invalid length unit and set mm
+    std::cout << "Invalid unit of length: " << lengthUnit << std::endl;
+    std::cout << "Length unit [mm] will be used instead" << std::endl;
+    len = HepMC3::Units::LengthUnit::MM;
+  }
+
+  // Set units
+  event->set_units(event->momentum_unit(), len);
+}
+
+void FW::HepMC3Event::shiftPositionBy(std::shared_ptr<HepMC3::GenEvent> event,
+                                      const Acts::Vector3D& deltaPos,
+                                      const double deltaTime) {
+  // Create HepMC3::FourVector from position and time for shift
+  const HepMC3::FourVector vec(deltaPos(0), deltaPos(1), deltaPos(2),
+                               deltaTime);
+  event->shift_position_by(vec);
+}
+
+void FW::HepMC3Event::shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                                      const Acts::Vector3D& pos,
+                                      const double time) {
+  // Create HepMC3::FourVector from position and time for the new position
+  const HepMC3::FourVector vec(pos(0), pos(1), pos(2), time);
+  event->shift_position_to(vec);
+}
+
+void FW::HepMC3Event::shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                                      const Acts::Vector3D& pos) {
+  // Create HepMC3::FourVector from position and time for the new position
+  const HepMC3::FourVector vec(pos(0), pos(1), pos(2), event->event_pos().t());
+  event->shift_position_to(vec);
+}
+
+void FW::HepMC3Event::shiftPositionTo(std::shared_ptr<HepMC3::GenEvent> event,
+                                      const double time) {
+  // Create HepMC3::FourVector from position and time for the new position
+  const HepMC3::FourVector vec(event->event_pos().x(), event->event_pos().y(),
+                               event->event_pos().z(), time);
+  event->shift_position_to(vec);
+}
+
+///
+/// Adder
+///
+
+HepMC3::GenParticlePtr FW::HepMC3Event::actsParticleToGen(
+    std::shared_ptr<SimParticle> actsParticle) {
+  // Extract momentum and energy from Acts particle for HepMC3::FourVector
+  const auto mom4 = actsParticle->momentum4();
+  const HepMC3::FourVector vec(mom4[0], mom4[1], mom4[2], mom4[3]);
+  // Create HepMC3::GenParticle
+  HepMC3::GenParticle genParticle(vec, actsParticle->pdg());
+  genParticle.set_generated_mass(actsParticle->mass());
+
+  return std::shared_ptr<HepMC3::GenParticle>(&genParticle);
+}
+
+void FW::HepMC3Event::addParticle(std::shared_ptr<HepMC3::GenEvent> event,
+                                  std::shared_ptr<SimParticle> particle) {
+  // Add new particle
+  event->add_particle(actsParticleToGen(particle));
+}
+
+HepMC3::GenVertexPtr FW::HepMC3Event::createGenVertex(
+    const std::shared_ptr<SimVertex>& actsVertex) {
+  const HepMC3::FourVector vec(
+      actsVertex->position4[0], actsVertex->position4[1],
+      actsVertex->position4[2], actsVertex->position4[3]);
+
+  // Create vertex
+  HepMC3::GenVertex genVertex(vec);
+
+  // Store incoming particles
+  for (auto& particle : actsVertex->incoming) {
+    HepMC3::GenParticlePtr genParticle =
+        actsParticleToGen(std::make_shared<SimParticle>(particle));
+    genVertex.add_particle_in(genParticle);
+  }
+  // Store outgoing particles
+  for (auto& particle : actsVertex->outgoing) {
+    HepMC3::GenParticlePtr genParticle =
+        actsParticleToGen(std::make_shared<SimParticle>(particle));
+    genVertex.add_particle_out(genParticle);
+  }
+  return std::shared_ptr<HepMC3::GenVertex>(&genVertex);
+}
+
+void FW::HepMC3Event::addVertex(std::shared_ptr<HepMC3::GenEvent> event,
+                                const std::shared_ptr<SimVertex> vertex) {
+  // Add new vertex
+  event->add_vertex(createGenVertex(vertex));
+}
+
+///
+/// Remover
+///
+
+void FW::HepMC3Event::removeParticle(
+    std::shared_ptr<HepMC3::GenEvent> event,
+    const std::shared_ptr<SimParticle>& particle) {
+  const std::vector<HepMC3::GenParticlePtr> genParticles = event->particles();
+  const auto id = particle->particleId();
+  // Search HepMC3::GenParticle with the same id as the Acts particle
+  for (auto& genParticle : genParticles) {
+    if (genParticle->id() == id) {
+      // Remove particle if found
+      event->remove_particle(genParticle);
+      break;
+    }
+  }
+}
+
+bool FW::HepMC3Event::compareVertices(
+    const std::shared_ptr<SimVertex>& actsVertex,
+    const HepMC3::GenVertexPtr& genVertex) {
+  // Compare position, time, number of incoming and outgoing particles between
+  // both vertices. Return false if one criterium does not match, else true.
+  HepMC3::FourVector genVec = genVertex->position();
+  if (actsVertex->position4[0] != genVec.x())
+    return false;
+  if (actsVertex->position4[1] != genVec.y())
+    return false;
+  if (actsVertex->position4[2] != genVec.z())
+    return false;
+  if (actsVertex->position4[3] != genVec.t())
+    return false;
+  if (actsVertex->incoming.size() != genVertex->particles_in().size())
+    return false;
+  if (actsVertex->outgoing.size() != genVertex->particles_out().size())
+    return false;
+  return true;
+}
+
+void FW::HepMC3Event::removeVertex(std::shared_ptr<HepMC3::GenEvent> event,
+                                   const std::shared_ptr<SimVertex>& vertex) {
+  const std::vector<HepMC3::GenVertexPtr> genVertices = event->vertices();
+  // Walk over every recorded vertex
+  for (auto& genVertex : genVertices)
+    if (compareVertices(vertex, genVertex)) {
+      // Remove vertex if it matches actsVertex
+      event->remove_vertex(genVertex);
+      break;
+    }
+}
+
+///
+/// Getter
+///
+
+double FW::HepMC3Event::momentumUnit(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  // HepMC allows only MEV and GEV. This allows an easy identification.
+  return (event->momentum_unit() == HepMC3::Units::MomentumUnit::MEV
+              ? Acts::units::_MeV
+              : Acts::units::_GeV);
+}
+
+double FW::HepMC3Event::lengthUnit(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  // HepMC allows only MM and CM. This allows an easy identification.
+  return (event->length_unit() == HepMC3::Units::LengthUnit::MM
+              ? Acts::units::_mm
+              : Acts::units::_cm);
+}
+
+Acts::Vector3D FW::HepMC3Event::eventPos(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  // Extract the position from HepMC3::FourVector
+  Acts::Vector3D vec;
+  vec(0) = event->event_pos().x();
+  vec(1) = event->event_pos().y();
+  vec(2) = event->event_pos().z();
+  return vec;
+}
+
+double FW::HepMC3Event::eventTime(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  // Extract the time from HepMC3::FourVector
+  return event->event_pos().t();
+}
+
+std::vector<std::unique_ptr<FW::SimParticle>> FW::HepMC3Event::particles(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  std::vector<std::unique_ptr<SimParticle>> actsParticles;
+  const std::vector<HepMC3::GenParticlePtr> genParticles = event->particles();
+
+  HepMC3Particle simPart;
+
+  // Translate all particles
+  for (auto& genParticle : genParticles)
+    actsParticles.push_back(std::move(
+        simPart.particle(std::make_shared<HepMC3::GenParticle>(*genParticle))));
+
+  return std::move(actsParticles);
+}
+
+std::vector<std::unique_ptr<FW::SimVertex>> FW::HepMC3Event::vertices(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  std::vector<std::unique_ptr<SimVertex>> actsVertices;
+  const std::vector<HepMC3::GenVertexPtr> genVertices = event->vertices();
+
+  HepMC3Vertex simVert;
+
+  // Translate all vertices
+  for (auto& genVertex : genVertices) {
+    actsVertices.push_back(std::move(simVert.processVertex(
+        std::make_shared<HepMC3::GenVertex>(*genVertex))));
+  }
+  return std::move(actsVertices);
+}
+
+std::vector<std::unique_ptr<FW::SimParticle>> FW::HepMC3Event::beams(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  std::vector<std::unique_ptr<SimParticle>> actsBeams;
+  const std::vector<HepMC3::GenParticlePtr> genBeams = event->beams();
+
+  HepMC3Particle simPart;
+
+  // Translate beam particles and store the result
+  for (auto& genBeam : genBeams)
+    actsBeams.push_back(std::move(
+        simPart.particle(std::make_shared<HepMC3::GenParticle>(*genBeam))));
+  return std::move(actsBeams);
+}
+
+std::vector<std::unique_ptr<FW::SimParticle>> FW::HepMC3Event::finalState(
+    const std::shared_ptr<HepMC3::GenEvent> event) {
+  std::vector<HepMC3::GenParticlePtr> particles = event->particles();
+  std::vector<std::unique_ptr<SimParticle>> fState;
+
+  HepMC3Particle simPart;
+
+  // Walk over every vertex
+  for (auto& particle : particles) {
+    // Collect particles without end vertex
+    if (!particle->end_vertex())
+      fState.push_back(
+          simPart.particle(std::make_shared<HepMC3::GenParticle>(*particle)));
+  }
+  return fState;
+}
diff --git a/Io/HepMC3/src/HepMC3Particle.cpp b/Io/HepMC3/src/HepMC3Particle.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ba3840ea9e1a2a64960a418c868bcf8135faecc
--- /dev/null
+++ b/Io/HepMC3/src/HepMC3Particle.cpp
@@ -0,0 +1,105 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Particle.hpp"
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp"
+
+std::unique_ptr<FW::SimParticle> FW::HepMC3Particle::particle(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  // TODO this is probably not quite right
+  ActsFatras::Barcode particleId;
+  particleId.setParticle(particle->id());
+  SimParticle fw(particleId, static_cast<Acts::PdgParticle>(particle->pid()),
+                 HepPID::charge(particle->pid()), particle->generated_mass());
+  fw.setDirection(particle->momentum().x(), particle->momentum().y(),
+                  particle->momentum().z());
+  fw.setAbsMomentum(particle->momentum().p3mod());
+  return std::make_unique<SimParticle>(std::move(fw));
+}
+
+int FW::HepMC3Particle::id(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  return particle->id();
+}
+
+std::unique_ptr<FW::SimVertex> FW::HepMC3Particle::productionVertex(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  HepMC3Vertex simVert;
+
+  // Return the vertex if it exists
+  if (particle->production_vertex())
+    return std::move(simVert.processVertex(
+        std::make_shared<HepMC3::GenVertex>(*particle->production_vertex())));
+  else
+    return nullptr;
+}
+
+std::unique_ptr<FW::SimVertex> FW::HepMC3Particle::endVertex(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  HepMC3Vertex simVert;
+
+  // Return the vertex if it exists
+  if (particle->end_vertex())
+    return std::move(simVert.processVertex(
+        std::make_shared<HepMC3::GenVertex>(*(particle->end_vertex()))));
+  else
+    return nullptr;
+}
+
+int FW::HepMC3Particle::pdgID(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  return particle->pid();
+}
+
+Acts::Vector3D FW::HepMC3Particle::momentum(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  Acts::Vector3D mom;
+  mom(0) = particle->momentum().x();
+  mom(1) = particle->momentum().y();
+  mom(2) = particle->momentum().z();
+  return mom;
+}
+
+double FW::HepMC3Particle::energy(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  return particle->momentum().e();
+}
+
+double FW::HepMC3Particle::mass(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  return particle->generated_mass();
+}
+
+double FW::HepMC3Particle::charge(
+    const std::shared_ptr<HepMC3::GenParticle> particle) {
+  return HepPID::charge(particle->pid());
+}
+
+void FW::HepMC3Particle::pdgID(std::shared_ptr<HepMC3::GenParticle> particle,
+                               const int pid) {
+  particle->set_pid(pid);
+}
+
+void FW::HepMC3Particle::momentum(std::shared_ptr<HepMC3::GenParticle> particle,
+                                  const Acts::Vector3D& mom) {
+  HepMC3::FourVector fVec(mom(0), mom(1), mom(2), particle->momentum().e());
+  particle->set_momentum(fVec);
+}
+
+void FW::HepMC3Particle::energy(std::shared_ptr<HepMC3::GenParticle> particle,
+                                const double energy) {
+  HepMC3::FourVector fVec(particle->momentum().x(), particle->momentum().y(),
+                          particle->momentum().z(), energy);
+  particle->set_momentum(fVec);
+}
+
+void FW::HepMC3Particle::mass(std::shared_ptr<HepMC3::GenParticle> particle,
+                              const double mass) {
+  particle->set_generated_mass(mass);
+}
diff --git a/Io/HepMC3/src/HepMC3Reader.cpp b/Io/HepMC3/src/HepMC3Reader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..60fcf8f07f7cec282ad71261ef65c4429ccaf0d5
--- /dev/null
+++ b/Io/HepMC3/src/HepMC3Reader.cpp
@@ -0,0 +1,19 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Reader.hpp"
+
+bool FW::HepMC3ReaderAscii::readEvent(HepMC3::ReaderAscii& reader,
+                                      std::shared_ptr<HepMC3::GenEvent> event) {
+  // Read event and store it
+  return reader.read_event(*event);
+}
+
+bool FW::HepMC3ReaderAscii::status(HepMC3::ReaderAscii& reader) {
+  return !reader.failed();
+}
diff --git a/Io/HepMC3/src/HepMC3Vertex.cpp b/Io/HepMC3/src/HepMC3Vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..98ee0496addf7be450ae9c4f53636f5a19d55b25
--- /dev/null
+++ b/Io/HepMC3/src/HepMC3Vertex.cpp
@@ -0,0 +1,132 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Vertex.hpp"
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Particle.hpp"
+
+std::vector<FW::SimParticle> FW::HepMC3Vertex::genParticlesToActs(
+    const std::vector<HepMC3::GenParticlePtr>& genParticles) {
+  HepMC3Particle simPart;
+
+  std::vector<SimParticle> actsParticles;
+  // Translate all particles
+  for (auto& genParticle : genParticles)
+    actsParticles.push_back(*(
+        simPart.particle(std::make_shared<HepMC3::GenParticle>(*genParticle))));
+  return actsParticles;
+}
+
+std::unique_ptr<FW::SimVertex> FW::HepMC3Vertex::processVertex(
+    const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  SimVertex vtx({vertex->position().x(), vertex->position().y(),
+                 vertex->position().z(), vertex->position().t()});
+  vtx.incoming = genParticlesToActs(vertex->particles_in());
+  vtx.outgoing = genParticlesToActs(vertex->particles_out());
+  // Create Acts vertex
+  return std::make_unique<SimVertex>(std::move(vtx));
+}
+
+bool FW::HepMC3Vertex::inEvent(
+    const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  return vertex->in_event();
+}
+
+int FW::HepMC3Vertex::id(const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  return vertex->id();
+}
+
+std::vector<FW::SimParticle> FW::HepMC3Vertex::particlesIn(
+    const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  return genParticlesToActs(vertex->particles_in());
+}
+
+std::vector<FW::SimParticle> FW::HepMC3Vertex::particlesOut(
+    const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  return genParticlesToActs(vertex->particles_out());
+}
+
+Acts::Vector3D FW::HepMC3Vertex::position(
+    const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  Acts::Vector3D vec;
+  vec(0) = vertex->position().x();
+  vec(1) = vertex->position().y();
+  vec(2) = vertex->position().z();
+  return vec;
+}
+
+double FW::HepMC3Vertex::time(const std::shared_ptr<HepMC3::GenVertex> vertex) {
+  return vertex->position().t();
+}
+
+HepMC3::GenParticlePtr FW::HepMC3Vertex::actsParticleToGen(
+    std::shared_ptr<SimParticle> actsParticle) {
+  // Extract momentum and energy from Acts particle for HepMC3::FourVector
+  const auto mom = actsParticle->momentum4();
+  const HepMC3::FourVector vec(mom[0], mom[1], mom[2], mom[3]);
+  // Create HepMC3::GenParticle
+  HepMC3::GenParticle genParticle(vec, actsParticle->pdg());
+  genParticle.set_generated_mass(actsParticle->mass());
+
+  return std::shared_ptr<HepMC3::GenParticle>(&genParticle);
+}
+
+void FW::HepMC3Vertex::addParticleIn(std::shared_ptr<HepMC3::GenVertex> vertex,
+                                     std::shared_ptr<SimParticle> particle) {
+  vertex->add_particle_in(actsParticleToGen(particle));
+}
+
+void FW::HepMC3Vertex::addParticleOut(std::shared_ptr<HepMC3::GenVertex> vertex,
+                                      std::shared_ptr<SimParticle> particle) {
+  vertex->add_particle_out(actsParticleToGen(particle));
+}
+
+HepMC3::GenParticlePtr FW::HepMC3Vertex::matchParticles(
+    const std::vector<HepMC3::GenParticlePtr>& genParticles,
+    std::shared_ptr<SimParticle> actsParticle) {
+  const auto id = actsParticle->particleId();
+  // Search HepMC3::GenParticle with the same id as the Acts particle
+  for (auto& genParticle : genParticles) {
+    if (genParticle->id() == id) {
+      // Return particle if found
+      return genParticle;
+    }
+  }
+  return nullptr;
+}
+
+void FW::HepMC3Vertex::removeParticleIn(
+    std::shared_ptr<HepMC3::GenVertex> vertex,
+    std::shared_ptr<SimParticle> particle) {
+  // Remove particle if it exists
+  if (HepMC3::GenParticlePtr genParticle =
+          matchParticles(vertex->particles_in(), particle))
+    vertex->remove_particle_in(genParticle);
+}
+
+void FW::HepMC3Vertex::removeParticleOut(
+    std::shared_ptr<HepMC3::GenVertex> vertex,
+    std::shared_ptr<SimParticle> particle) {
+  // Remove particle if it exists
+  if (HepMC3::GenParticlePtr genParticle =
+          matchParticles(vertex->particles_out(), particle))
+    vertex->remove_particle_out(genParticle);
+}
+
+void FW::HepMC3Vertex::position(const std::shared_ptr<HepMC3::GenVertex> vertex,
+                                Acts::Vector3D pos) {
+  HepMC3::FourVector fVec(pos(0), pos(1), pos(2), vertex->position().t());
+  vertex->set_position(fVec);
+}
+
+void FW::HepMC3Vertex::time(const std::shared_ptr<HepMC3::GenVertex> vertex,
+                            double time) {
+  HepMC3::FourVector fVec(vertex->position().x(), vertex->position().y(),
+                          vertex->position().z(), time);
+  vertex->set_position(fVec);
+}
diff --git a/Io/HepMC3/src/HepMC3Writer.cpp b/Io/HepMC3/src/HepMC3Writer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1cb6f7bf5c70fa5e519270df17cd4aa7e7162c68
--- /dev/null
+++ b/Io/HepMC3/src/HepMC3Writer.cpp
@@ -0,0 +1,20 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/HepMC3/HepMC3Writer.hpp"
+
+bool FW::HepMC3WriterAscii::writeEvent(
+    HepMC3::WriterAscii& writer, std::shared_ptr<HepMC3::GenEvent> event) {
+  // Write event from storage
+  writer.write_event(*event);
+  return true;
+}
+
+bool FW::HepMC3WriterAscii::status(HepMC3::WriterAscii& writer) {
+  return writer.failed();
+}
diff --git a/Io/Json/CMakeLists.txt b/Io/Json/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..88d07d106424435f016609e87d85f462d8794fdb
--- /dev/null
+++ b/Io/Json/CMakeLists.txt
@@ -0,0 +1,13 @@
+add_library(
+  ActsExamplesIoJson SHARED
+  src/JsonMaterialWriter.cpp)
+target_include_directories(
+  ActsExamplesIoJson
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
+target_link_libraries(
+  ActsExamplesIoJson
+  PUBLIC ActsCore ActsJsonPlugin ActsExamplesFramework)
+
+install(
+  TARGETS ActsExamplesIoJson
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/Json/include/ACTFW/Plugins/Json/JsonMaterialWriter.hpp b/Io/Json/include/ACTFW/Plugins/Json/JsonMaterialWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3022a4cd722befaf750c3ebc3c2fae964ed54cc4
--- /dev/null
+++ b/Io/Json/include/ACTFW/Plugins/Json/JsonMaterialWriter.hpp
@@ -0,0 +1,79 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+///////////////////////////////////////////////////////////////////
+// JsonMaterialWriter.h
+///////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <mutex>
+
+#include "ACTFW/Framework/ProcessCode.hpp"
+#include "Acts/Geometry/GeometryID.hpp"
+#include "Acts/Material/ISurfaceMaterial.hpp"
+#include "Acts/Material/IVolumeMaterial.hpp"
+#include "Acts/Plugins/Json/JsonGeometryConverter.hpp"
+#include "Acts/Utilities/Definitions.hpp"
+#include "Acts/Utilities/Logger.hpp"
+
+namespace Acts {
+
+class TrackingGeometry;
+
+using SurfaceMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const ISurfaceMaterial>>;
+
+using VolumeMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const IVolumeMaterial>>;
+
+using DetectorMaterialMaps = std::pair<SurfaceMaterialMap, VolumeMaterialMap>;
+}  // namespace Acts
+
+namespace FW {
+
+namespace Json {
+
+/// @class Json Material writer
+///
+/// @brief Writes out Detector material maps
+/// using the Json Geometry converter
+class JsonMaterialWriter {
+ public:
+  /// Constructor
+  ///
+  /// @param cfg The configuration struct of the converter
+  JsonMaterialWriter(const Acts::JsonGeometryConverter::Config& cfg,
+                     const std::string& fileName);
+
+  /// Virtual destructor
+  ~JsonMaterialWriter();
+
+  /// Write out the material map
+  ///
+  /// @param detMaterial is the SurfaceMaterial and VolumeMaterial maps
+  void write(const Acts::DetectorMaterialMaps& detMaterial);
+
+  /// Write out the material map from Geometry
+  ///
+  /// @param tGeometry is the TrackingGeometry
+  void write(const Acts::TrackingGeometry& tGeometry);
+
+ private:
+  /// The config class of the converter
+  Acts::JsonGeometryConverter::Config m_cfg;
+
+  /// The file name
+  std::string m_fileName;
+
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+};
+
+}  // namespace Json
+}  // namespace FW
diff --git a/Io/Json/include/ACTFW/Plugins/Json/JsonSpacePointWriter.hpp b/Io/Json/include/ACTFW/Plugins/Json/JsonSpacePointWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3aacbae9627c8f9d6f8f6d78000fd3f53fc277a2
--- /dev/null
+++ b/Io/Json/include/ACTFW/Plugins/Json/JsonSpacePointWriter.hpp
@@ -0,0 +1,113 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+/// @file
+/// @date 2016-05-23 Initial version
+/// @date 2017-08-07 Rewrite with new interfaces
+
+#pragma once
+
+#include <fstream>
+
+#include "ACTFW/EventData/DataContainers.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+
+namespace FW {
+namespace Json {
+
+/// Write out a space point collection in JSON format.
+///
+/// This writes one file per event into the configured output directory. By
+/// default it writes to the current working directory. Files are named
+/// using the following schema
+///
+///     event000000001-spacepoints.json
+///     event000000002-spacepoints.json
+///
+template <class T>
+class JsonSpacePointWriter : public WriterT<GeometryIdMultimap<T>> {
+ public:
+  struct Config {
+    std::string collection;      ///< which collection to write
+    std::string outputDir;       ///< where to place output files
+    size_t outputPrecision = 6;  ///< floating point precision
+  };
+
+  JsonSpacePointWriter(const Config& cfg,
+                       Acts::Logging::Level level = Acts::Logging::INFO);
+
+ protected:
+  FW::ProcessCode writeT(
+      const FW::AlgorithmContext& context,
+      const GeometryIdMultimap<T>& spacePoints) final override;
+
+ private:
+  // since class itself is templated, base class template must be fixed
+  using Base = WriterT<GeometryIdMultimap<T>>;
+
+  Config m_cfg;
+};
+
+}  // namespace Json
+}  // namespace FW
+
+template <class T>
+FW::Json::JsonSpacePointWriter<T>::JsonSpacePointWriter(
+    const FW::Json::JsonSpacePointWriter<T>::Config& cfg,
+    Acts::Logging::Level level)
+    : Base(cfg.collection, "JsonSpacePointWriter", level), m_cfg(cfg) {
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  }
+}
+
+template <class T>
+FW::ProcessCode FW::Json::JsonSpacePointWriter<T>::writeT(
+    const FW::AlgorithmContext& context,
+    const GeometryIdMultimap<T>& spacePoints) {
+  // open per-event file
+  std::string path = perEventFilepath(m_cfg.outputDir, "spacepoints.json",
+                                      context.eventNumber);
+  std::ofstream os(path, std::ofstream::out | std::ofstream::trunc);
+  if (!os) {
+    throw std::ios_base::failure("Could not open '" + path + "' to write");
+  }
+
+  os << std::setprecision(m_cfg.outputPrecision);
+  os << "{\n";
+
+  bool firstVolume = true;
+  for (auto& volumeData : spacePoints) {
+    geo_id_value volumeID = volumeData.first;
+
+    if (!firstVolume)
+      os << ",\n";
+    os << "  \"SpacePoints_" << volumeID << "\" : [\n";
+
+    bool firstPoint = true;
+    for (auto& layerData : volumeData.second) {
+      for (auto& moduleData : layerData.second) {
+        for (auto& data : moduleData.second) {
+          // set the comma correctly
+          if (!firstPoint)
+            os << ",\n";
+          // write the space point
+          os << "    [" << data.x() << ", " << data.y() << ", " << data.z()
+             << "]";
+          firstPoint = false;
+        }
+      }
+    }
+    os << "]";
+    firstVolume = false;
+  }
+  os << "\n}\n";
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Json/src/JsonMaterialWriter.cpp b/Io/Json/src/JsonMaterialWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e592e3b44f489d606acdccbb7270b7d964d2cf30
--- /dev/null
+++ b/Io/Json/src/JsonMaterialWriter.cpp
@@ -0,0 +1,48 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/Json/JsonMaterialWriter.hpp"
+
+#include <fstream>
+#include <ios>
+#include <iostream>
+#include <stdexcept>
+
+#include "Acts/Geometry/GeometryID.hpp"
+#include "Acts/Material/BinnedSurfaceMaterial.hpp"
+
+FW::Json::JsonMaterialWriter::JsonMaterialWriter(
+    const Acts::JsonGeometryConverter::Config& cfg, const std::string& fileName)
+    : m_cfg(cfg), m_fileName(fileName) {
+  // Validate the configuration
+  if (m_cfg.name.empty()) {
+    throw std::invalid_argument("Missing service name");
+  }
+}
+
+FW::Json::JsonMaterialWriter::~JsonMaterialWriter() {}
+
+void FW::Json::JsonMaterialWriter::write(
+    const Acts::DetectorMaterialMaps& detMaterial) {
+  // Evoke the converter
+  Acts::JsonGeometryConverter jmConverter(m_cfg);
+  auto jout = jmConverter.materialMapsToJson(detMaterial);
+  // And write the file
+  std::ofstream ofj(m_fileName);
+  ofj << std::setw(4) << jout << std::endl;
+}
+
+void FW::Json::JsonMaterialWriter::write(
+    const Acts::TrackingGeometry& tGeometry) {
+  // Evoke the converter
+  Acts::JsonGeometryConverter jmConverter(m_cfg);
+  auto jout = jmConverter.trackingGeometryToJson(tGeometry);
+  // And write the file
+  std::ofstream ofj(m_fileName);
+  ofj << std::setw(4) << jout << std::endl;
+}
diff --git a/Io/Obj/CMakeLists.txt b/Io/Obj/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5cad394e273f7efad2cfdaaa974c077d24ea0e52
--- /dev/null
+++ b/Io/Obj/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_library(
+  ActsExamplesIoObj SHARED
+  src/ObjHelper.cpp
+  src/ObjSurfaceWriter.cpp
+  src/ObjTrackingGeometryWriter.cpp)
+target_include_directories(
+  ActsExamplesIoObj
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
+target_link_libraries(
+  ActsExamplesIoObj
+  PUBLIC ActsCore ActsExamplesFramework Threads::Threads)
+
+install(
+  TARGETS ActsExamplesIoObj
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjHelper.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjHelper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d448a414eb8e201de0fe27f805ab7bda572059f3
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjHelper.hpp
@@ -0,0 +1,74 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <fstream>
+#include <vector>
+
+#include "Acts/Utilities/Definitions.hpp"
+
+namespace FW {
+
+namespace Obj {
+
+/// This is the counter struct for keeping track of the vertices
+struct VtnCounter {
+  unsigned int vcounter = 0;
+  unsigned int vtcounter = 0;
+  unsigned int ncounter = 0;
+};
+
+/// This will write a vertex to the fstream
+/// @param stream is the stream where to write to
+/// @param vertex is the vertex to be written out
+/// @param cvertex is the current vertex number
+void writeVTN(std::ofstream& stream, VtnCounter& vtnCounter, double scalor,
+              const Acts::Vector3D& vertex, const std::string& vtntype = "v",
+              bool point = false);
+
+/// construct vertical faces
+/// this takes a range and constructs faces
+void constructVerticalFaces(std::ofstream& stream, unsigned int start,
+                            const std::vector<unsigned int>& vsides);
+
+/// This will write a planar face
+/// - normal is given by cross product
+///
+/// @param stream is the stream where to write to
+/// @param face is the face to be written out
+/// @param cvertex is the current vertex number
+/// @param thickness is the (optional) thickness
+void writePlanarFace(std::ofstream& stream, VtnCounter& vtnCounter,
+                     double scalor, const std::vector<Acts::Vector3D>& vertices,
+                     double thickness = 0.,
+                     const std::vector<unsigned int>& vsides = {});
+
+/// This will write a cylindrical object
+///
+/// @param stream is the stream where to write to
+void writeTube(std::ofstream& stream, VtnCounter& vtnCounter, double scalor,
+               unsigned int nSegments, const Acts::Transform3D& transform,
+               double r, double hZ, double thickness = 0.);
+
+/// Helper method for bezier line interpolation
+///
+/// @param t is the step parameter along the line
+/// @param p0 anker point
+/// @param p1 is p0 + direction@p0
+/// @param p2 is p2 - direction@p2
+/// @param p3 is second anker poit
+///
+/// @return the bezier point
+Acts::Vector3D calculateBezierPoint(double t, const Acts::Vector3D& p0,
+                                    const Acts::Vector3D& p1,
+                                    const Acts::Vector3D& p2,
+                                    const Acts::Vector3D& p3);
+
+}  // namespace Obj
+}  // namespace FW
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjPropagationStepsWriter.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjPropagationStepsWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..40a7107139df456eb991f4ab92d5c4362432da2f
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjPropagationStepsWriter.hpp
@@ -0,0 +1,103 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <fstream>
+
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/Propagator/detail/SteppingLogger.hpp"
+
+namespace FW {
+
+namespace Obj {
+
+/// @class ObjPropagationStepsWriter
+///
+/// Write out the steps of test propgations for stepping validation
+/// Writes one file per event with form:
+///
+///     event000000001-propagation-steps.obj
+///     event000000002-propagation-steps.obj
+///
+/// One Thread per write call and hence thread safe
+template <typename step_t>
+class ObjPropagationStepsWriter
+    : public WriterT<std::vector<std::vector<step_t>>> {
+ public:
+  struct Config {
+    std::string collection;      ///< which collection to write
+    std::string outputDir;       ///< where to place output files
+    double outputScalor = 1.0;   ///< scale output values
+    size_t outputPrecision = 6;  ///< floating point precision
+  };
+
+  /// Constructor with arguments
+  ///
+  /// @param cfg configuration struct
+  /// @param level Output logging level
+  ObjPropagationStepsWriter(const Config& cfg,
+                            Acts::Logging::Level level = Acts::Logging::INFO)
+      : WriterT<std::vector<std::vector<step_t>>>(cfg.collection,
+                                                  "ObjSpacePointWriter", level),
+        m_cfg(cfg) {
+    if (m_cfg.collection.empty()) {
+      throw std::invalid_argument("Missing input collection");
+    }
+  }
+
+  /// Virtual destructor
+  ~ObjPropagationStepsWriter() override = default;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override { return FW::ProcessCode::SUCCESS; }
+
+ private:
+  Config m_cfg;  ///!< Internal configuration represenation
+
+ protected:
+  /// This implementation holds the actual writing method
+  /// and is called by the WriterT<>::write interface
+  ProcessCode writeT(
+      const AlgorithmContext& context,
+      const std::vector<std::vector<step_t>>& stepCollection) final override {
+    // open per-event file
+    std::string path = FW::perEventFilepath(
+        m_cfg.outputDir, "propagation-steps.obj", context.eventNumber);
+    std::ofstream os(path, std::ofstream::out | std::ofstream::trunc);
+    if (!os) {
+      throw std::ios_base::failure("Could not open '" + path + "' to write");
+    }
+
+    // Initialize the vertex counter
+    unsigned int vCounter = 0;
+
+    for (auto& steps : stepCollection) {
+      // At least three points to draw
+      if (steps.size() > 2) {
+        // We start from one
+        ++vCounter;
+        for (auto& step : steps) {
+          // Write the space point
+          os << "v " << m_cfg.outputScalor * step.position.x() << " "
+             << m_cfg.outputScalor * step.position.y() << " "
+             << m_cfg.outputScalor * step.position.z() << '\n';
+        }
+        // Write out the line - only if we have at least two points created
+        size_t vBreak = vCounter + steps.size() - 1;
+        for (; vCounter < vBreak; ++vCounter)
+          os << "l " << vCounter << " " << vCounter + 1 << '\n';
+      }
+    }
+    return FW::ProcessCode::SUCCESS;
+  }
+};
+
+}  // namespace Obj
+}  // namespace FW
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjSpacePointWriter.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjSpacePointWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f55113adea8b9d79376583921d1da08223a0e6e1
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjSpacePointWriter.hpp
@@ -0,0 +1,96 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <fstream>
+
+#include "ACTFW/EventData/GeometryContainers.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+
+namespace FW {
+namespace Obj {
+
+/// Write out a space point collection in OBJ format.
+///
+/// This writes one file per event into the configured output directory. By
+/// default it writes to the current working directory. Files are named
+/// using the following schema
+///
+///     event000000001-spacepoints.obj
+///     event000000002-spacepoints.obj
+///
+/// One write call per thread and hence thread safe.
+template <typename T>
+class ObjSpacePointWriter : public WriterT<GeometryIdMultimap<T>> {
+ public:
+  struct Config {
+    std::string collection;      ///< which collection to write
+    std::string outputDir;       ///< where to place output files
+    double outputScalor = 1.0;   ///< scale output values
+    size_t outputPrecision = 6;  ///< floating point precision
+  };
+
+  ObjSpacePointWriter(const Config& cfg,
+                      Acts::Logging::Level level = Acts::Logging::INFO);
+
+ protected:
+  ProcessCode writeT(const AlgorithmContext& context,
+                     const GeometryIdMultimap<T>& spacePoints);
+
+ private:
+  // since class iitself is templated, base class template must be fixed
+  using Base = WriterT<GeometryIdMultimap<T>>;
+
+  Config m_cfg;
+};
+
+}  // namespace Obj
+}  // namespace FW
+
+template <typename T>
+inline FW::Obj::ObjSpacePointWriter<T>::ObjSpacePointWriter(
+    const ObjSpacePointWriter<T>::Config& cfg, Acts::Logging::Level level)
+    : Base(cfg.collection, "ObjSpacePointWriter", level), m_cfg(cfg) {
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  }
+}
+
+template <typename T>
+inline FW::ProcessCode FW::Obj::ObjSpacePointWriter<T>::writeT(
+    const FW::AlgorithmContext& context,
+    const FW::GeometryIdMultimap<T>& spacePoints) {
+  // open per-event file
+  std::string path = FW::perEventFilepath(m_cfg.outputDir, "spacepoints.obj",
+                                          context.eventNumber);
+  std::ofstream os(path, std::ofstream::out | std::ofstream::trunc);
+  if (!os) {
+    throw std::ios_base::failure("Could not open '" + path + "' to write");
+  }
+
+  os << std::setprecision(m_cfg.outputPrecision);
+  // count the vertex
+  size_t vertex = 0;
+  // loop and fill the space point data
+  for (auto& volumeData : spacePoints) {
+    for (auto& layerData : volumeData.second) {
+      for (auto& moduleData : layerData.second) {
+        for (auto& data : moduleData.second) {
+          // write the space point
+          os << "v " << m_cfg.outputScalor * data.x() << " "
+             << m_cfg.outputScalor * data.y() << " "
+             << m_cfg.outputScalor * data.z() << '\n';
+          os << "p " << ++vertex << '\n';
+        }
+      }
+    }
+  }
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9415930a8d7d6f1656539679ed5937a20e820e8b
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp
@@ -0,0 +1,102 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Surfaces/Surface.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <fstream>
+#include <iostream>
+#include <mutex>
+
+#include "ACTFW/Framework/AlgorithmContext.hpp"
+#include "ACTFW/Framework/ProcessCode.hpp"
+#include "ACTFW/Plugins/Obj/ObjHelper.hpp"
+
+namespace FW {
+namespace Obj {
+
+/// @class ObjSurfaceWriter
+///
+/// An Obj writer for the geometry: surface section
+///
+class ObjSurfaceWriter {
+ public:
+  // @class Config
+  //
+  // The nested config class for the Surface writer
+  class Config {
+   public:
+    /// the default logger
+    std::shared_ptr<const Acts::Logger> logger;
+    /// the name of the algorithm
+    std::string name;
+    /// approximate cyinders by that
+    unsigned int outputPhiSegemnts = 72;
+    /// write thickness if available
+    double outputThickness = 2.;
+    /// write sensitive surfaces
+    bool outputSensitive = true;
+    /// write the layer surface out
+    bool outputLayerSurface = true;
+    /// output scalor
+    double outputScalor = 1.;
+    /// precision for out
+    unsigned int outputPrecision = 6;
+    /// file prefix to be written out
+    std::string filePrefix = "";
+    /// prefixes
+    /// @todo These aren't used anywhere, should they be dropped?
+    std::string planarPrefix = "";
+    std::string cylinderPrefix = "";
+    std::string diskPrefix = "";
+    /// the output stream
+    std::shared_ptr<std::ofstream> outputStream = nullptr;
+
+    Config(const std::string& lname = "ObjSurfaceWriter",
+           Acts::Logging::Level lvl = Acts::Logging::INFO)
+        : logger(Acts::getDefaultLogger(lname, lvl)), name(lname) {}
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg is the configuration class
+  ObjSurfaceWriter(const Config& cfg);
+
+  /// Framework name() method
+  std::string name() const;
+
+  /// The write interface
+  /// @param context the Algorithm/Event context of this call
+  /// @param surface to be written out
+  FW::ProcessCode write(const AlgorithmContext& context,
+                        const Acts::Surface& surface);
+
+  /// write a bit of string
+  /// @param is the string to be written
+  FW::ProcessCode write(const std::string& sinfo);
+
+ private:
+  Config m_cfg;                  ///< the config class
+  Obj::VtnCounter m_vtnCounter;  ///< vertex, texture, normal
+  std::mutex m_write_mutex;      ///< mutex to protect multi-threaded writes
+
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+};
+
+inline FW::ProcessCode ObjSurfaceWriter::write(const std::string& sinfo) {
+  // lock the mutex for writing
+  std::lock_guard<std::mutex> lock(m_write_mutex);
+  // and write
+  (*m_cfg.outputStream) << sinfo;
+  return FW::ProcessCode::SUCCESS;
+}
+
+}  // namespace Obj
+}  // namespace FW
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjTrackingGeometryWriter.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjTrackingGeometryWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f8f2374460cfc1b1ca6bc80d5a507863bb641b83
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjTrackingGeometryWriter.hpp
@@ -0,0 +1,84 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Utilities/Logger.hpp>
+#include <fstream>
+#include <iostream>
+#include <mutex>
+
+#include "ACTFW/Framework/ProcessCode.hpp"
+#include "ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp"
+
+namespace Acts {
+class TrackingVolume;
+class TrackingGeometry;
+}  // namespace Acts
+
+namespace FW {
+namespace Obj {
+
+/// @class ObjTrackingGeometryWriter
+///
+/// An Obj writer for the geometry: TrackingGeometry master
+/// It delegates the writing of surfaces to the surface writers
+class ObjTrackingGeometryWriter {
+ public:
+  // @class Config
+  //
+  // The nested config class
+  class Config {
+   public:
+    /// the default logger
+    std::shared_ptr<const Acts::Logger> logger;
+    /// the name of the writer
+    std::string name = "";
+    /// surfaceWriters
+    std::vector<std::shared_ptr<ObjSurfaceWriter>> surfaceWriters;
+    std::string filePrefix = "";
+    std::string sensitiveGroupPrefix = "";
+    std::string layerPrefix = "";
+
+    Config(const std::string& lname = "ObjTrackingGeometryWriter",
+           Acts::Logging::Level lvl = Acts::Logging::INFO)
+        : logger(Acts::getDefaultLogger(lname, lvl)),
+          name(lname),
+          surfaceWriters() {}
+  };
+
+  /// Constructor
+  /// @param cfg is the configuration class
+  ObjTrackingGeometryWriter(const Config& cfg);
+
+  /// Framework name() method
+  /// @return the name of the tool
+  std::string name() const;
+
+  /// The write interface
+  /// @param context the Algorithm/Event context of this call
+  /// @param tGeometry is the geometry to be written out
+  /// @return ProcessCode to indicate success/failure
+  FW::ProcessCode write(const AlgorithmContext& context,
+                        const Acts::TrackingGeometry& tGeometry);
+
+ private:
+  Config m_cfg;  ///< the config class
+
+  /// process this volume
+  /// @param context the Algorithm/Event context for this call
+  /// @param tVolume the volume to be processed
+  void write(const AlgorithmContext& context,
+             const Acts::TrackingVolume& tVolume);
+
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+};
+
+}  // namespace Obj
+}  // namespace FW
diff --git a/Io/Obj/include/ACTFW/Plugins/Obj/ObjWriterOptions.hpp b/Io/Obj/include/ACTFW/Plugins/Obj/ObjWriterOptions.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..41adb932cbcc632dd6a54e12e9fad0bff780e437
--- /dev/null
+++ b/Io/Obj/include/ACTFW/Plugins/Obj/ObjWriterOptions.hpp
@@ -0,0 +1,87 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <iostream>
+
+#include "ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp"
+#include "ACTFW/Plugins/Obj/ObjTrackingGeometryWriter.hpp"
+#include "ACTFW/Utilities/Options.hpp"
+#include "Acts/Utilities/Logger.hpp"
+
+namespace po = boost::program_options;
+
+namespace FW {
+
+namespace Options {
+
+/// Common obj writing options
+///
+/// @tparam aopt_t Type of the options object (from BOOST)
+///
+/// @param opt The options object, where string based options are attached
+template <typename aopt_t>
+void addObjWriterOptions(aopt_t& opt) {
+  opt.add_options()("obj-tg-fileheader",
+                    po::value<std::string>()->default_value(""),
+                    "The (optional) file header for the tracking geometry.")(
+      "obj-tg-sensitiveheader", po::value<std::string>()->default_value(""),
+      "The (optional) header in front of sensitive sensors.")(
+      "obj-tg-layerheader", po::value<std::string>()->default_value(""),
+      "The (optional) header in front of layer surfaces.")(
+      "obj-sf-fileheader", po::value<std::string>()->default_value(""),
+      "The (optional) file header for the surface writer.")(
+      "obj-sf-phisegments", po::value<int>()->default_value(72),
+      "Number of phi segments to approximate curves.")(
+      "obj-sf-outputPrecission", po::value<int>()->default_value(6),
+      "Floating number output precission.")(
+      "obj-sf-outputScalor", po::value<double>()->default_value(1.),
+      "Scale factor to be applied.")("obj-sf-outputThickness",
+                                     po::value<double>()->default_value(1.),
+                                     "The surface thickness.")(
+      "obj-sf-outputSensitive", po::value<bool>()->default_value(true),
+      "Write sensitive surfaces.")("obj-sf-outputLayers",
+                                   po::value<bool>()->default_value(true),
+                                   "Write layer surfaces.");
+}
+
+/// read the evgen options and return a Config file
+template <class AMAP>
+FW::Obj::ObjTrackingGeometryWriter::Config readObjTrackingGeometryWriterConfig(
+    const AMAP& vm, const std::string& name,
+    Acts::Logging::Level loglevel = Acts::Logging::INFO) {
+  FW::Obj::ObjTrackingGeometryWriter::Config objTgConfig(name, loglevel);
+  objTgConfig.filePrefix = vm["obj-tg-fileheader"].template as<std::string>();
+  objTgConfig.sensitiveGroupPrefix =
+      vm["obj-tg-sensitiveheader"].template as<std::string>();
+  objTgConfig.layerPrefix = vm["obj-tg-layerheader"].template as<std::string>();
+  return objTgConfig;
+}
+
+template <class AMAP>
+FW::Obj::ObjSurfaceWriter::Config readObjSurfaceWriterConfig(
+    const AMAP& vm, const std::string& name, Acts::Logging::Level loglevel) {
+  FW::Obj::ObjSurfaceWriter::Config objSfConfig(name,
+                                                loglevel = Acts::Logging::INFO);
+  objSfConfig.filePrefix = vm["obj-sf-fileheader"].template as<std::string>();
+  objSfConfig.outputPhiSegemnts = vm["obj-sf-phisegments"].template as<int>();
+  objSfConfig.outputPrecision =
+      vm["obj-sf-outputPrecission"].template as<int>();
+  objSfConfig.outputScalor = vm["obj-sf-outputScalor"].template as<double>();
+  objSfConfig.outputThickness =
+      vm["obj-sf-outputThickness"].template as<double>();
+  objSfConfig.outputSensitive =
+      vm["obj-sf-outputSensitive"].template as<bool>();
+  objSfConfig.outputLayerSurface =
+      vm["obj-sf-outputLayers"].template as<bool>();
+  return objSfConfig;
+}
+
+}  // namespace Options
+}  // namespace FW
diff --git a/Io/Obj/src/ObjHelper.cpp b/Io/Obj/src/ObjHelper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..294a6c2ee7c805c5b099960f9c02f643c7651d77
--- /dev/null
+++ b/Io/Obj/src/ObjHelper.cpp
@@ -0,0 +1,191 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/Obj/ObjHelper.hpp"
+
+#include <vector>
+
+void FW::Obj::writeVTN(std::ofstream& stream, VtnCounter& vtnCounter,
+                       double scalor, const Acts::Vector3D& vertex,
+                       const std::string& vtntype, bool point) {
+  // in case you make a point
+  unsigned int cp = 0;
+  // the counter
+  if (vtntype == "v") {
+    ++vtnCounter.vcounter;
+    cp = vtnCounter.vcounter;
+  } else if (vtntype == "t") {
+    ++vtnCounter.vtcounter;
+    cp = vtnCounter.vtcounter;
+  } else if (vtntype == "vn") {
+    ++vtnCounter.ncounter;
+    cp = vtnCounter.ncounter;
+  } else
+    return;
+
+  // write out the vertex, texture vertex, normal
+  stream << vtntype << " " << scalor * vertex.x() << " " << scalor * vertex.y()
+         << " " << scalor * vertex.z() << '\n';
+  // we create a point if needed
+  if (point)
+    stream << "p " << cp;
+}
+
+void FW::Obj::constructVerticalFaces(std::ofstream& stream, unsigned int start,
+                                     const std::vector<unsigned int>& vsides) {
+  // construct the vertical faces
+  size_t nsides = vsides.size();
+  unsigned int sstart = start;
+  for (auto vside : vsides) {
+    if (vside) {
+      // start streaming the side
+      // all but the last
+      if (start - sstart < nsides - 1) {
+        stream << "f " << start << " " << start + 1 << " ";
+        stream << start + nsides + 1 << " " << start + nsides;
+      } else {
+        stream << "f " << start << " " << sstart << " ";
+        stream << sstart + nsides << " " << start + nsides;
+      }
+    }
+    stream << '\n';
+    // increase
+    ++start;
+  }
+}
+
+void FW::Obj::writePlanarFace(std::ofstream& stream, VtnCounter& vtnCounter,
+                              double scalor,
+                              const std::vector<Acts::Vector3D>& vertices,
+                              double thickness,
+                              const std::vector<unsigned int>& vsides) {
+  // minimum 3 vertices needed
+  if (vertices.size() < 3)
+    return;
+  // the first vertex
+  unsigned int fvertex = vtnCounter.vcounter + 1;
+  // lets create the normal vector first
+  Acts::Vector3D sideOne = vertices[1] - vertices[0];
+  Acts::Vector3D sideTwo = vertices[2] - vertices[1];
+  Acts::Vector3D nvector(sideTwo.cross(sideOne).normalized());
+  // thickness or not thickness
+  std::vector<int> sides = {0};
+  if (thickness != 0.)
+    sides = {-1, 1};
+  // now write all the vertices - this works w/wo thickness
+  for (auto side : sides) {
+    // save the current vertex counter
+    unsigned int cvc = vtnCounter.vcounter;
+    // loop over the sides
+    for (auto v : vertices)
+      writeVTN(stream, vtnCounter, scalor,
+               v + (0.5 * side * thickness) * nvector, "v");
+
+    // now write the face
+    stream << "f ";
+    for (auto n = vertices.size(); 0 < n; --n)
+      stream << ++cvc << " ";
+    stream << '\n';
+  }
+  // now process the vertical sides
+  constructVerticalFaces(stream, fvertex, vsides);
+}
+
+void FW::Obj::writeTube(std::ofstream& stream, VtnCounter& vtnCounter,
+                        double scalor, unsigned int nSegments,
+                        const Acts::Transform3D& transform, double r, double hZ,
+                        double thickness) {
+  // flip along plus/minus and declare the faces
+  std::vector<int> flip = {-1, 1};
+  std::vector<int> vfaces = {1, 2, 4, 3};
+  // the number of phisteps
+  double phistep = 2 * M_PI / nSegments;
+  // make it twice if necessary
+  std::vector<double> roffsets = {0.};
+  if (thickness != 0.)
+    roffsets = {-0.5 * thickness, 0.5 * thickness};
+  // now loop over the thickness and make an outer and inner
+  unsigned int cvc = vtnCounter.vcounter;
+  size_t iside = 0;
+  for (auto t : roffsets) {
+    size_t iphi = 0;
+    // loop over phi steps
+    for (; iphi < nSegments; ++iphi) {
+      // currentPhi
+      double phi = -M_PI + iphi * phistep;
+      for (auto iflip : flip) {
+        // create the vertex
+        Acts::Vector3D point(transform * Acts::Vector3D((r + t) * cos(phi),
+                                                        (r + t) * sin(phi),
+                                                        iflip * hZ));
+        // write the normal vector
+        writeVTN(stream, vtnCounter, scalor, point, "v");
+      }
+    }
+    // now create the faces
+    iphi = 0;
+    // side offset for faces
+    unsigned int soff = 2 * iside * nSegments;
+    for (; iphi < nSegments - 1; ++iphi) {
+      // output to file
+      stream << "f ";
+      for (auto face : vfaces)
+        stream << soff + cvc + (2 * iphi) + face << " ";
+      stream << '\n';
+    }
+    // close the loop
+    stream << "f " << soff + cvc + (2 * iphi) + 1 << " "
+           << soff + cvc + (2 * iphi) + 2 << " " << soff + cvc + 2 << " "
+           << soff + cvc + 1 << '\n';
+    // new line at the end of the line
+    stream << '\n';
+    ++iside;
+  }
+
+  // construct the sides at the end when all vertices are done
+  // Acts::Vector3D nvectorSide = transform.rotation().col(2);
+  if (thickness != 0.) {
+    // loop over the two sides
+    for (iside = 0; iside < 2; ++iside) {
+      // rest iphi
+      size_t iphi = 0;
+      for (; iphi < nSegments - 1; ++iphi) {
+        stream << "f ";
+        unsigned int base = cvc + (2 * iphi) + 1;
+        stream << iside + base << " ";
+        stream << iside + base + 2 << " ";
+        stream << iside + base + (2 * nSegments) + 2 << " ";
+        stream << iside + base + (2 * nSegments) << '\n';
+      }
+      // close the loop
+      stream << "f ";
+      stream << iside + cvc + (2 * iphi) + 1 << " ";
+      stream << iside + cvc + 1 << " ";
+      stream << iside + cvc + 1 + (2 * nSegments) << " ";
+      stream << iside + cvc + (2 * iphi) + 1 + (2 * nSegments) << '\n';
+    }
+  }
+}
+
+// Bezier interpolation, see documentation
+Acts::Vector3D FW::Obj::calculateBezierPoint(double t, const Acts::Vector3D& p0,
+                                             const Acts::Vector3D& p1,
+                                             const Acts::Vector3D& p2,
+                                             const Acts::Vector3D& p3) {
+  double u = 1. - t;
+  double tt = t * t;
+  double uu = u * u;
+  double uuu = uu * u;
+  double ttt = tt * t;
+
+  Acts::Vector3D p = uuu * p0;  // first term
+  p += 3 * uu * t * p1;         // second term
+  p += 3 * u * tt * p2;         // third term
+  p += ttt * p3;                // fourth term
+  return p;
+}
diff --git a/Io/Obj/src/ObjSurfaceWriter.cpp b/Io/Obj/src/ObjSurfaceWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3efc747ac6d778deed1961b6d71af666d861bae7
--- /dev/null
+++ b/Io/Obj/src/ObjSurfaceWriter.cpp
@@ -0,0 +1,127 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/Obj/ObjSurfaceWriter.hpp"
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Geometry/Layer.hpp>
+#include <Acts/Surfaces/CylinderBounds.hpp>
+#include <Acts/Surfaces/PlanarBounds.hpp>
+#include <Acts/Surfaces/RadialBounds.hpp>
+#include <Acts/Surfaces/SurfaceBounds.hpp>
+#include <ios>
+#include <iostream>
+#include <stdexcept>
+
+FW::Obj::ObjSurfaceWriter::ObjSurfaceWriter(
+    const FW::Obj::ObjSurfaceWriter::Config& cfg)
+    : m_cfg(cfg) {
+  // Validate the configuration
+  if (!m_cfg.logger) {
+    throw std::invalid_argument("Missing logger");
+  } else if (m_cfg.name.empty()) {
+    throw std::invalid_argument("Missing algorithm name");
+  } else if (!m_cfg.outputStream) {
+    throw std::invalid_argument("Missing output stream");
+  }
+
+  // Write down the file prefix
+  (*(m_cfg.outputStream)) << m_cfg.filePrefix << '\n';
+}
+
+std::string FW::Obj::ObjSurfaceWriter::name() const {
+  return m_cfg.name;
+}
+
+FW::ProcessCode FW::Obj::ObjSurfaceWriter::write(
+    const AlgorithmContext& context, const Acts::Surface& surface) {
+  std::lock_guard<std::mutex> lock(m_write_mutex);
+
+  ACTS_DEBUG(">>Obj: Writer for Surface object called.");
+
+  auto scalor = m_cfg.outputScalor;
+  // let's get the bounds & the transform
+  const Acts::SurfaceBounds& surfaceBounds = surface.bounds();
+  auto sTransform = surface.transform(context.geoContext);
+
+  // dynamic_cast to PlanarBounds
+  const Acts::PlanarBounds* planarBounds =
+      dynamic_cast<const Acts::PlanarBounds*>(&surfaceBounds);
+  // only continue if the cast worked
+  if (planarBounds && m_cfg.outputSensitive) {
+    ACTS_VERBOSE(">>Obj: Writing out a PlaneSurface");
+    // set the precision - just to be sure
+    (*(m_cfg.outputStream)) << '\n';
+    (*(m_cfg.outputStream)) << std::setprecision(m_cfg.outputPrecision);
+    // get the vertices
+    auto planarVertices = planarBounds->vertices();
+    // loop over the vertices
+    std::vector<Acts::Vector3D> vertices;
+    vertices.reserve(planarVertices.size());
+    for (auto pv : planarVertices) {
+      // get the point in 3D
+      Acts::Vector3D v3D(sTransform * Acts::Vector3D(pv.x(), pv.y(), 0.));
+      vertices.push_back(v3D);
+    }
+    // get the thickness and vertical faces
+    double thickness = 0.;
+    std::vector<unsigned int> vfaces;
+    if (surface.associatedDetectorElement() and m_cfg.outputThickness != 0.) {
+      // get the thickness form the detector element
+      thickness = surface.associatedDetectorElement()->thickness();
+      vfaces = {1, 1, 1, 1};
+    }
+    // output to file
+    Obj::writePlanarFace(*(m_cfg.outputStream), m_vtnCounter, scalor, vertices,
+                         thickness, vfaces);
+    (*(m_cfg.outputStream)) << '\n';
+  }
+
+  // check if you have layer and check what your have
+  // dynamic cast to CylinderBounds work the same
+  const Acts::CylinderBounds* cylinderBounds =
+      dynamic_cast<const Acts::CylinderBounds*>(&surfaceBounds);
+  if (cylinderBounds && m_cfg.outputLayerSurface) {
+    ACTS_VERBOSE(">>Obj: Writing out a CylinderSurface with r = "
+                 << cylinderBounds->get(Acts::CylinderBounds::eR));
+    // name the object
+    auto layerID = surface.geoID().layer();
+    (*(m_cfg.outputStream))
+        << " o Cylinder_" << std::to_string(layerID) << '\n';
+    // output to the file
+    Obj::writeTube(*(m_cfg.outputStream), m_vtnCounter, scalor,
+                   m_cfg.outputPhiSegemnts, sTransform,
+                   cylinderBounds->get(Acts::CylinderBounds::eR),
+                   cylinderBounds->get(Acts::CylinderBounds::eHalfLengthZ),
+                   m_cfg.outputThickness);
+    (*(m_cfg.outputStream)) << '\n';
+  }
+
+  ////dynamic cast to RadialBounds or disc bounds work the same
+  const Acts::RadialBounds* radialBounds =
+      dynamic_cast<const Acts::RadialBounds*>(&surfaceBounds);
+  if (radialBounds && m_cfg.outputLayerSurface) {
+    ACTS_VERBOSE(">>Obj: Writing out a DiskSurface at z = "
+                 << sTransform.translation().z());
+    // name the object
+    auto layerID = surface.geoID().layer();
+    (*(m_cfg.outputStream)) << "o Disk_" << std::to_string(layerID) << '\n';
+    // we use the tube writer in the other direction
+    double rMin = radialBounds->rMin();
+    double rMax = radialBounds->rMax();
+    double thickness = rMax - rMin;
+    // output to the file
+    Obj::writeTube(*(m_cfg.outputStream), m_vtnCounter, scalor,
+                   m_cfg.outputPhiSegemnts, sTransform, 0.5 * (rMin + rMax),
+                   m_cfg.outputThickness, thickness);
+    (*(m_cfg.outputStream)) << '\n';
+  }
+
+  // return success
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Obj/src/ObjTrackingGeometryWriter.cpp b/Io/Obj/src/ObjTrackingGeometryWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acddd4e1aa1813f8d73a1e062e85e27b515cef0d
--- /dev/null
+++ b/Io/Obj/src/ObjTrackingGeometryWriter.cpp
@@ -0,0 +1,106 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Plugins/Obj/ObjTrackingGeometryWriter.hpp"
+
+#include <Acts/Geometry/Layer.hpp>
+#include <Acts/Geometry/TrackingGeometry.hpp>
+#include <Acts/Geometry/TrackingVolume.hpp>
+#include <Acts/Surfaces/Surface.hpp>
+#include <iostream>
+
+FW::Obj::ObjTrackingGeometryWriter::ObjTrackingGeometryWriter(
+    const FW::Obj::ObjTrackingGeometryWriter::Config& cfg)
+    : m_cfg(cfg) {}
+
+std::string FW::Obj::ObjTrackingGeometryWriter::name() const {
+  return m_cfg.name;
+}
+
+FW::ProcessCode FW::Obj::ObjTrackingGeometryWriter::write(
+    const AlgorithmContext& context, const Acts::TrackingGeometry& tGeometry) {
+  ACTS_DEBUG(">>Obj: Writer for TrackingGeometry object called.");
+  // get the world volume
+  auto world = tGeometry.highestTrackingVolume();
+  if (world)
+    write(context, *world);
+  // return the success code
+  return FW::ProcessCode::SUCCESS;
+}
+
+/// process this volume
+void FW::Obj::ObjTrackingGeometryWriter::write(
+    const AlgorithmContext& context, const Acts::TrackingVolume& tVolume) {
+  ACTS_DEBUG(">>Obj: Writer for TrackingVolume object called.");
+  // get the confined layers and process them
+  if (tVolume.confinedLayers()) {
+    ACTS_VERBOSE(">>Obj: Layers are present, process them.");
+    // loop over the layers
+    for (auto layer : tVolume.confinedLayers()->arrayObjects()) {
+      // we jump navigation layers
+      if (layer->layerType() == Acts::navigation)
+        continue;
+      // get the volume name
+      const std::string& volumeName = tVolume.volumeName();
+      // find the right surfacewriter
+      std::shared_ptr<ObjSurfaceWriter> surfaceWriter = nullptr;
+      for (auto writer : m_cfg.surfaceWriters) {
+        // get name and writer
+        auto writerName = writer->name();
+        // and break
+        ACTS_VERBOSE(">>Obj: The writer name is: " << writerName);
+        ACTS_VERBOSE(">>Obj: The volume name is: " << volumeName);
+        if (volumeName.find(writerName) != std::string::npos) {
+          // asign the writer
+          surfaceWriter = writer;
+          // break the loop
+          break;
+        }
+      }
+      // bail out if you have no surface writer
+      if (!surfaceWriter)
+        return;
+      // layer prefix
+      surfaceWriter->write(m_cfg.layerPrefix);
+      // try to write the material surface as well
+      if (layer->surfaceRepresentation().surfaceMaterial()) {
+        surfaceWriter->write(context, layer->surfaceRepresentation());
+      }
+      // the the approaching surfaces and check if they have material
+      if (layer->approachDescriptor()) {
+        // loop over the contained Surfaces
+        for (auto& cSurface : layer->approachDescriptor()->containedSurfaces())
+          if (cSurface->surfaceMaterial()) {
+            surfaceWriter->write(context, *cSurface);
+          }
+      }
+      // check for sensitive surfaces
+      if (layer->surfaceArray() && surfaceWriter) {
+        ACTS_VERBOSE(">>Obj: There are "
+                     << layer->surfaceArray()->surfaces().size()
+                     << " surfaces.");
+        // surfaces
+        // surfaceWriter->write(m_cfg.sensitiveGroupPrefix);
+        // loop over the surface
+        for (auto& surface : layer->surfaceArray()->surfaces()) {
+          if (surface && (surfaceWriter->write(context, *surface)) ==
+                             FW::ProcessCode::ABORT)
+            return;
+        }
+      }
+    }
+  }
+  // Recursive self call
+  // get the confined volumes and step down the hierarchy
+  if (tVolume.confinedVolumes()) {
+    // loop over the volumes and write what they have
+    for (auto volume : tVolume.confinedVolumes()->arrayObjects()) {
+      write(context, *volume.get());
+    }
+  }
+}
diff --git a/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.cpp b/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ec4169f6a3b4ba23a8ace247827e31d824ebcc1a
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.cpp
@@ -0,0 +1,210 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2020 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 http://mozilla.org/MPL/2.0/.
+
+#include <numeric>
+#include <stdexcept>
+
+#include <TFile.h>
+#include <TTree.h>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Io/Performance/CKFPerformanceWriter.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/EventData/MultiTrajectoryHelpers.hpp"
+#include "Acts/EventData/TrackParameters.hpp"
+
+FW::CKFPerformanceWriter::CKFPerformanceWriter(
+    FW::CKFPerformanceWriter::Config cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputTrajectories, "CKFPerformanceWriter", lvl),
+      m_cfg(std::move(cfg)),
+      m_effPlotTool(m_cfg.effPlotToolConfig, lvl),
+      m_fakeRatePlotTool(m_cfg.fakeRatePlotToolConfig, lvl),
+      m_duplicationPlotTool(m_cfg.duplicationPlotToolConfig, lvl),
+      m_trackSummaryPlotTool(m_cfg.trackSummaryPlotToolConfig, lvl) {
+  // Input track and truth collection name
+  if (m_cfg.inputTrajectories.empty()) {
+    throw std::invalid_argument("Missing input trajectories collection");
+  }
+  if (m_cfg.inputParticles.empty()) {
+    throw std::invalid_argument("Missing input particles collection");
+  }
+  if (m_cfg.outputFilename.empty()) {
+    throw std::invalid_argument("Missing output filename");
+  }
+
+  // the output file can not be given externally since TFile accesses to the
+  // same file from multiple threads are unsafe.
+  // must always be opened internally
+  auto path = joinPaths(m_cfg.outputDir, m_cfg.outputFilename);
+  m_outputFile = TFile::Open(path.c_str(), "RECREATE");
+  if (not m_outputFile) {
+    throw std::invalid_argument("Could not open '" + path + "'");
+  }
+
+  // initialize the plot tools
+  m_effPlotTool.book(m_effPlotCache);
+  m_fakeRatePlotTool.book(m_fakeRatePlotCache);
+  m_duplicationPlotTool.book(m_duplicationPlotCache);
+  m_trackSummaryPlotTool.book(m_trackSummaryPlotCache);
+}
+
+FW::CKFPerformanceWriter::~CKFPerformanceWriter() {
+  m_effPlotTool.clear(m_effPlotCache);
+  m_fakeRatePlotTool.clear(m_fakeRatePlotCache);
+  m_duplicationPlotTool.clear(m_duplicationPlotCache);
+  m_trackSummaryPlotTool.clear(m_trackSummaryPlotCache);
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::CKFPerformanceWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_effPlotTool.write(m_effPlotCache);
+    m_fakeRatePlotTool.write(m_fakeRatePlotCache);
+    m_duplicationPlotTool.write(m_duplicationPlotCache);
+    m_trackSummaryPlotTool.write(m_trackSummaryPlotCache);
+    ACTS_INFO("Wrote performance plots to '" << m_outputFile->GetPath() << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::CKFPerformanceWriter::writeT(
+    const AlgorithmContext& ctx, const TrajectoryContainer& trajectories) {
+  // The number of majority particle hits and fitted track parameters
+  using RecoTrackInfo = std::pair<size_t, Acts::BoundParameters>;
+
+  // Read truth particles from input collection
+  const auto& particles =
+      ctx.eventStore.get<SimParticleContainer>(m_cfg.inputParticles);
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Counter of truth-matched reco tracks
+  std::map<ActsFatras::Barcode, std::vector<RecoTrackInfo>> matched;
+  // Counter of truth-unmatched reco tracks
+  std::map<ActsFatras::Barcode, size_t> unmatched;
+
+  // Loop over all trajectories
+  for (const auto& traj : trajectories) {
+    // The trajectory entry indices and the multiTrajectory
+    const auto& [trackTips, mj] = traj.trajectory();
+    if (trackTips.empty()) {
+      ACTS_WARNING("Empty multiTrajectory.");
+      continue;
+    }
+
+    // Loop over all trajectories in a multiTrajectory
+    for (const size_t& trackTip : trackTips) {
+      // Collect the trajectory summary info
+      auto trajState =
+          Acts::MultiTrajectoryHelpers::trajectoryState(mj, trackTip);
+      // Reco track selection
+      //@TODO: add interface for applying others cuts on reco tracks:
+      // -> pT, d0, z0, detector-specific hits/holes number cut
+      if (trajState.nMeasurements < m_cfg.nMeasurementsMin) {
+        continue;
+      }
+      // Check if the reco track has fitted track parameters
+      if (not traj.hasTrackParameters(trackTip)) {
+        ACTS_WARNING(
+            "No fitted track parameters for trajectory with entry index = "
+            << trackTip);
+        continue;
+      }
+      const auto& fittedParameters = traj.trackParameters(trackTip);
+      // Fill the trajectory summary info
+      m_trackSummaryPlotTool.fill(m_trackSummaryPlotCache, fittedParameters,
+                                  trajState.nStates, trajState.nMeasurements,
+                                  trajState.nOutliers, trajState.nHoles);
+
+      // Get the majority truth particle to this track
+      std::vector<ParticleHitCount> particleHitCount =
+          traj.identifyMajorityParticle(trackTip);
+      if (particleHitCount.empty()) {
+        ACTS_WARNING(
+            "No truth particle associated with this trajectory with entry "
+            "index = "
+            << trackTip);
+        continue;
+      }
+      // Get the majority particleId and majority particle counts
+      // Note that the majority particle might be not in the truth seeds
+      // collection
+      ActsFatras::Barcode majorityParticleId =
+          particleHitCount.front().particleId;
+      size_t nMajorityHits = particleHitCount.front().hitCount;
+
+      // Check if the trajectory is matched with truth.
+      // If not, it will be classified as 'fake'
+      bool isFake = false;
+      if (nMajorityHits * 1. / trajState.nMeasurements >=
+          m_cfg.truthMatchProbMin) {
+        matched[majorityParticleId].push_back(
+            {nMajorityHits, fittedParameters});
+      } else {
+        isFake = true;
+        unmatched[majorityParticleId]++;
+      }
+      // Fill fake rate plots
+      m_fakeRatePlotTool.fill(m_fakeRatePlotCache, fittedParameters, isFake);
+    }  // end all trajectories in a multiTrajectory
+  }    // end all multiTrajectories
+
+  // Loop over all truth-matched reco tracks for duplication rate plots
+  for (auto& [particleId, matchedTracks] : matched) {
+    // Sort the reco tracks matched to this particle by the number of majority
+    // hits
+    std::sort(matchedTracks.begin(), matchedTracks.end(),
+              [](const RecoTrackInfo& lhs, const RecoTrackInfo& rhs) {
+                return lhs.first > rhs.first;
+              });
+    for (size_t itrack = 0; itrack < matchedTracks.size(); itrack++) {
+      const auto& [nMajorityHits, fittedParameters] = matchedTracks.at(itrack);
+      // The tracks with maximum number of majority hits is taken as the 'real'
+      // track; others are as 'duplicated'
+      bool isDuplicated = (itrack != 0);
+      // Fill the duplication rate
+      m_duplicationPlotTool.fill(m_duplicationPlotCache, fittedParameters,
+                                 isDuplicated);
+    }
+  }
+
+  // Loop over all truth particle seeds for efficiency plots and reco details.
+  // These are filled w.r.t. truth particle seed info
+  for (const auto& particle : particles) {
+    auto particleId = particle.particleId();
+    // Investigate the truth-matched tracks
+    size_t nMatchedTracks = 0;
+    bool isReconstructed = false;
+    auto imatched = matched.find(particleId);
+    if (imatched != matched.end()) {
+      nMatchedTracks = imatched->second.size();
+      isReconstructed = true;
+    }
+    // Fill efficiency plots
+    m_effPlotTool.fill(m_effPlotCache, particle, isReconstructed);
+    // Fill number of duplicated tracks for this particle
+    m_duplicationPlotTool.fill(m_duplicationPlotCache, particle,
+                               nMatchedTracks - 1);
+
+    // Investigate the fake (i.e. truth-unmatched) tracks
+    size_t nFakeTracks = 0;
+    auto ifake = unmatched.find(particleId);
+    if (ifake != unmatched.end()) {
+      nFakeTracks = ifake->second;
+    }
+    // Fill number of reconstructed/truth-matched/fake tracks for this particle
+    m_fakeRatePlotTool.fill(m_fakeRatePlotCache, particle, nMatchedTracks,
+                            nFakeTracks);
+  }  // end all truth particles
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.hpp b/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a0ddc45dae88d2b6ade51b946a0ea4d4d2a4e295
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/CKFPerformanceWriter.hpp
@@ -0,0 +1,89 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2020 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <mutex>
+
+#include "ACTFW/EventData/Track.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/Validation/DuplicationPlotTool.hpp"
+#include "ACTFW/Validation/EffPlotTool.hpp"
+#include "ACTFW/Validation/FakeRatePlotTool.hpp"
+#include "ACTFW/Validation/TrackSummaryPlotTool.hpp"
+#include "Acts/Utilities/Units.hpp"
+
+class TFile;
+class TTree;
+
+using namespace Acts::UnitLiterals;
+
+namespace FW {
+
+/// Write out the performance of CombinatorialKalmanFilter (CKF), e.g.
+/// track efficiency, fake rate etc.
+/// @TODO: add duplication plots
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class CKFPerformanceWriter final : public WriterT<TrajectoryContainer> {
+ public:
+  struct Config {
+    /// Input truth particles collection.
+    std::string inputParticles;
+    /// Input (found) trajectories collection.
+    std::string inputTrajectories;
+    /// Output directory.
+    std::string outputDir;
+    /// Output filename.
+    std::string outputFilename = "performance_ckf.root";
+    /// Plot tool configurations.
+    EffPlotTool::Config effPlotToolConfig;
+    FakeRatePlotTool::Config fakeRatePlotToolConfig;
+    DuplicationPlotTool::Config duplicationPlotToolConfig;
+    TrackSummaryPlotTool::Config trackSummaryPlotToolConfig;
+    /// Min reco-truth matching probability
+    double truthMatchProbMin = 0.5;
+    /// Min number of measurements
+    size_t nMeasurementsMin = 9;
+    /// Min transverse momentum
+    double ptMin = 1_GeV;
+  };
+
+  /// Construct from configuration and log level.
+  CKFPerformanceWriter(Config cfg, Acts::Logging::Level lvl);
+  ~CKFPerformanceWriter() override;
+
+  /// Finalize plots.
+  ProcessCode endRun() final override;
+
+ private:
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const TrajectoryContainer& trajectories) final override;
+
+  Config m_cfg;
+  /// Mutex used to protect multi-threaded writes.
+  std::mutex m_writeMutex;
+  TFile* m_outputFile{nullptr};
+  /// Plot tool for efficiency
+  EffPlotTool m_effPlotTool;
+  EffPlotTool::EffPlotCache m_effPlotCache;
+  /// Plot tool for fake rate
+  FakeRatePlotTool m_fakeRatePlotTool;
+  FakeRatePlotTool::FakeRatePlotCache m_fakeRatePlotCache{};
+  /// Plot tool for duplication rate
+  DuplicationPlotTool m_duplicationPlotTool;
+  DuplicationPlotTool::DuplicationPlotCache m_duplicationPlotCache{};
+  /// Plot tool for track hit info
+  TrackSummaryPlotTool m_trackSummaryPlotTool;
+  TrackSummaryPlotTool::TrackSummaryPlotCache m_trackSummaryPlotCache;
+};
+
+}  // namespace FW
diff --git a/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.cpp b/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..30d4c5ef5e62d56120804ceb5b90c53d1e26f0b8
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.cpp
@@ -0,0 +1,259 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Performance/TrackFinderPerformanceWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <algorithm>
+#include <cstdint>
+#include <mutex>
+#include <unordered_map>
+#include <vector>
+
+#include "ACTFW/EventData/IndexContainers.hpp"
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "ACTFW/Utilities/Range.hpp"
+#include "ACTFW/Validation/ProtoTrackClassification.hpp"
+#include "Acts/Utilities/Helpers.hpp"
+#include "Acts/Utilities/Units.hpp"
+#include "ActsFatras/EventData/Barcode.hpp"
+
+namespace {
+using SimParticleContainer = FW::SimParticleContainer;
+using HitParticlesMap = FW::IndexMultimap<ActsFatras::Barcode>;
+using ProtoTrackContainer = FW::ProtoTrackContainer;
+}  // namespace
+
+struct FW::TrackFinderPerformanceWriter::Impl {
+  Config cfg;
+  TFile* file = nullptr;
+
+  // per-track tree
+  TTree* trkTree = nullptr;
+  std::mutex trkMutex;
+  // track identification
+  ULong64_t trkEventId;
+  ULong64_t trkTrackId;
+  // track content
+  // number of hits on track
+  UShort_t trkNumHits;
+  // number of particles contained in the track
+  UShort_t trkNumParticles;
+  // track particle content; for each contributing particle, largest first
+  std::vector<ULong64_t> trkParticleId;
+  // total number of hits generated by this particle
+  std::vector<UShort_t> trkParticleNumHitsTotal;
+  // number of hits within this track
+  std::vector<UShort_t> trkParticleNumHitsOnTrack;
+
+  // per-particle tree
+  TTree* prtTree = nullptr;
+  std::mutex prtMutex;
+  // particle identification
+  ULong64_t prtEventId;
+  ULong64_t prtParticleId;
+  Int_t prtParticleType;
+  // particle kinematics
+  // vertex position in mm
+  float prtVx, prtVy, prtVz;
+  // vertex time in ns
+  float prtVt;
+  // particle momentum at production in GeV
+  float prtPx, prtPy, prtPz;
+  // particle mass in GeV
+  float prtM;
+  // particle charge in e
+  float prtQ;
+  // particle reconstruction
+  UShort_t prtNumHits;    // number of hits for this particle
+  UShort_t prtNumTracks;  // number of tracks this particle was reconstructed in
+  UShort_t prtNumTracksMajority;  // number of tracks reconstructed as majority
+  // extra logger reference for the logging macros
+  const Acts::Logger& _logger;
+
+  Impl(Config&& c, const Acts::Logger& l) : cfg(std::move(c)), _logger(l) {
+    if (cfg.inputParticles.empty()) {
+      throw std::invalid_argument("Missing particles input collection");
+    }
+    if (cfg.inputHitParticlesMap.empty()) {
+      throw std::invalid_argument("Missing hit-particles map input collection");
+    }
+    if (cfg.inputProtoTracks.empty()) {
+      throw std::invalid_argument("Missing proto tracks input collection");
+    }
+    if (cfg.outputFilename.empty()) {
+      throw std::invalid_argument("Missing output filename");
+    }
+
+    // the output file can not be given externally since TFile accesses to the
+    // same file from multiple threads are unsafe.
+    // must always be opened internally
+    auto path = joinPaths(cfg.outputDir, cfg.outputFilename);
+    file = TFile::Open(path.c_str(), "RECREATE");
+    if (not file) {
+      throw std::invalid_argument("Could not open '" + path + "'");
+    }
+
+    // construct trees
+    trkTree = new TTree("track_finder_tracks", "");
+    trkTree->SetDirectory(file);
+    trkTree->Branch("event_id", &trkEventId);
+    trkTree->Branch("track_id", &trkTrackId);
+    trkTree->Branch("size", &trkNumHits);
+    trkTree->Branch("nparticles", &trkNumParticles);
+    trkTree->Branch("particle_id", &trkParticleId);
+    trkTree->Branch("particle_nhits_total", &trkParticleNumHitsTotal);
+    trkTree->Branch("particle_nhits_on_track", &trkParticleNumHitsOnTrack);
+    prtTree = new TTree("track_finder_particles", "");
+    prtTree->SetDirectory(file);
+    prtTree->Branch("event_id", &prtEventId);
+    prtTree->Branch("particle_id", &prtParticleId);
+    prtTree->Branch("particle_type", &prtParticleType);
+    prtTree->Branch("vx", &prtVx);
+    prtTree->Branch("vy", &prtVy);
+    prtTree->Branch("vz", &prtVz);
+    prtTree->Branch("vt", &prtVt);
+    prtTree->Branch("px", &prtPx);
+    prtTree->Branch("py", &prtPy);
+    prtTree->Branch("pz", &prtPz);
+    prtTree->Branch("m", &prtM);
+    prtTree->Branch("q", &prtQ);
+    prtTree->Branch("nhits", &prtNumHits);
+    prtTree->Branch("ntracks", &prtNumTracks);
+    prtTree->Branch("ntracks_majority", &prtNumTracksMajority);
+  }
+
+  const Acts::Logger& logger() const { return _logger; }
+
+  void write(uint64_t eventId, const SimParticleContainer& particles,
+             const HitParticlesMap& hitParticlesMap,
+             const ProtoTrackContainer& tracks) {
+    // compute the inverse mapping on-the-fly
+    const auto& particleHitsMap = invertIndexMultimap(hitParticlesMap);
+    // How often a particle was reconstructed.
+    std::unordered_map<ActsFatras::Barcode, std::size_t> reconCount;
+    reconCount.reserve(particles.size());
+    // How often a particle was reconstructed as the majority particle.
+    std::unordered_map<ActsFatras::Barcode, std::size_t> majorityCount;
+    majorityCount.reserve(particles.size());
+    // For each particle within a track, how many hits did it contribute
+    std::vector<ParticleHitCount> particleHitCounts;
+
+    // write per-track performance measures
+    {
+      std::lock_guard<std::mutex> guardTrk(trkMutex);
+      for (size_t itrack = 0; itrack < tracks.size(); ++itrack) {
+        const auto& track = tracks[itrack];
+
+        identifyContributingParticles(hitParticlesMap, track,
+                                      particleHitCounts);
+        // extract per-particle reconstruction counts
+        // empty track hits counts could originate from a  buggy track finder
+        // that results in empty tracks or from purely noise track where no hits
+        // is from a particle.
+        if (not particleHitCounts.empty()) {
+          auto it = majorityCount
+                        .try_emplace(particleHitCounts.front().particleId, 0u)
+                        .first;
+          it->second += 1;
+        }
+        for (const auto& hc : particleHitCounts) {
+          auto it = reconCount.try_emplace(hc.particleId, 0u).first;
+          it->second += 1;
+        }
+
+        trkEventId = eventId;
+        trkTrackId = itrack;
+        trkNumHits = track.size();
+        trkNumParticles = particleHitCounts.size();
+        trkParticleId.clear();
+        trkParticleNumHitsTotal.clear();
+        trkParticleNumHitsOnTrack.clear();
+        for (const auto& phc : particleHitCounts) {
+          trkParticleId.push_back(phc.particleId.value());
+          // count total number of hits for this particle
+          auto trueParticleHits =
+              makeRange(particleHitsMap.equal_range(phc.particleId.value()));
+          trkParticleNumHitsTotal.push_back(trueParticleHits.size());
+          trkParticleNumHitsOnTrack.push_back(phc.hitCount);
+        }
+
+        trkTree->Fill();
+      }
+    }
+
+    // write per-particle performance measures
+    {
+      std::lock_guard<std::mutex> guardPrt(trkMutex);
+      for (const auto& particle : particles) {
+        // find all hits for this particle
+        auto hits =
+            makeRange(particleHitsMap.equal_range(particle.particleId()));
+
+        // identification
+        prtEventId = eventId;
+        prtParticleId = particle.particleId().value();
+        prtParticleType = particle.pdg();
+        // kinematics
+        prtVx = particle.position().x() / Acts::UnitConstants::mm;
+        prtVy = particle.position().y() / Acts::UnitConstants::mm;
+        prtVz = particle.position().z() / Acts::UnitConstants::mm;
+        prtVt = particle.time() / Acts::UnitConstants::ns;
+        const auto p = particle.absMomentum() / Acts::UnitConstants::GeV;
+        prtPx = p * particle.unitDirection().x();
+        prtPy = p * particle.unitDirection().y();
+        prtPz = p * particle.unitDirection().z();
+        prtM = particle.mass() / Acts::UnitConstants::GeV;
+        prtQ = particle.charge() / Acts::UnitConstants::e;
+        // reconstruction
+        prtNumHits = hits.size();
+        auto nt = reconCount.find(particle.particleId());
+        prtNumTracks = (nt != reconCount.end()) ? nt->second : 0u;
+        auto nm = majorityCount.find(particle.particleId());
+        prtNumTracksMajority = (nm != majorityCount.end()) ? nm->second : 0u;
+
+        prtTree->Fill();
+      }
+    }
+  }
+  /// Write everything to disk and close the file.
+  void close() {
+    if (not file) {
+      ACTS_ERROR("Output file is not available");
+      return;
+    }
+    file->Write();
+    file->Close();
+  }
+};
+
+FW::TrackFinderPerformanceWriter::TrackFinderPerformanceWriter(
+    FW::TrackFinderPerformanceWriter::Config cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputProtoTracks, "TrackFinderPerformanceWriter", lvl),
+      m_impl(std::make_unique<Impl>(std::move(cfg), logger())) {}
+
+FW::TrackFinderPerformanceWriter::~TrackFinderPerformanceWriter() {
+  // explicit destructor needed for pimpl idiom to work
+}
+
+FW::ProcessCode FW::TrackFinderPerformanceWriter::writeT(
+    const FW::AlgorithmContext& ctx, const FW::ProtoTrackContainer& tracks) {
+  const auto& particles =
+      ctx.eventStore.get<SimParticleContainer>(m_impl->cfg.inputParticles);
+  const auto& hitParticlesMap =
+      ctx.eventStore.get<HitParticlesMap>(m_impl->cfg.inputHitParticlesMap);
+  m_impl->write(ctx.eventNumber, particles, hitParticlesMap, tracks);
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::TrackFinderPerformanceWriter::endRun() {
+  m_impl->close();
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.hpp b/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5834cd8bede47125e49f96b3bc801a0f67eafa28
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/TrackFinderPerformanceWriter.hpp
@@ -0,0 +1,51 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "ACTFW/EventData/ProtoTrack.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+
+namespace FW {
+
+/// Write track finder performance measures.
+///
+/// Only considers the track finding itself, i.e. grouping of hits into tracks,
+/// and computes relevant per-track and per-particles statistics.
+class TrackFinderPerformanceWriter final : public WriterT<ProtoTrackContainer> {
+ public:
+  struct Config {
+    /// True set of input particles.
+    std::string inputParticles;
+    /// True hit-particles mapping.
+    std::string inputHitParticlesMap;
+    /// Reconstructed input proto tracks.
+    std::string inputProtoTracks;
+    /// Output directory.
+    std::string outputDir;
+    /// Output filename
+    std::string outputFilename = "performance_track_finder.root";
+  };
+
+  TrackFinderPerformanceWriter(Config cfg, Acts::Logging::Level lvl);
+  ~TrackFinderPerformanceWriter();
+
+  ProcessCode endRun() final override;
+
+ private:
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const ProtoTrackContainer& tracks) final override;
+
+  struct Impl;
+  std::unique_ptr<Impl> m_impl;
+};
+
+}  // namespace FW
diff --git a/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.cpp b/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..22aeb221d38c34bee8621e7b9e42c15004efd6e1
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.cpp
@@ -0,0 +1,162 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Performance/TrackFitterPerformanceWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <stdexcept>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/EventData/MultiTrajectoryHelpers.hpp"
+#include "Acts/Utilities/Helpers.hpp"
+
+using Acts::VectorHelpers::eta;
+
+FW::TrackFitterPerformanceWriter::TrackFitterPerformanceWriter(
+    FW::TrackFitterPerformanceWriter::Config cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputTrajectories, "TrackFitterPerformanceWriter", lvl),
+      m_cfg(std::move(cfg)),
+      m_resPlotTool(m_cfg.resPlotToolConfig, lvl),
+      m_effPlotTool(m_cfg.effPlotToolConfig, lvl),
+      m_trackSummaryPlotTool(m_cfg.trackSummaryPlotToolConfig, lvl)
+
+{
+  // Input track and truth collection name
+  if (m_cfg.inputTrajectories.empty()) {
+    throw std::invalid_argument("Missing input trajectories collection");
+  }
+  if (m_cfg.inputParticles.empty()) {
+    throw std::invalid_argument("Missing input particles collection");
+  }
+  if (m_cfg.outputFilename.empty()) {
+    throw std::invalid_argument("Missing output filename");
+  }
+
+  // the output file can not be given externally since TFile accesses to the
+  // same file from multiple threads are unsafe.
+  // must always be opened internally
+  auto path = joinPaths(m_cfg.outputDir, m_cfg.outputFilename);
+  m_outputFile = TFile::Open(path.c_str(), "RECREATE");
+  if (not m_outputFile) {
+    throw std::invalid_argument("Could not open '" + path + "'");
+  }
+
+  // initialize the residual and efficiency plots tool
+  m_resPlotTool.book(m_resPlotCache);
+  m_effPlotTool.book(m_effPlotCache);
+  m_trackSummaryPlotTool.book(m_trackSummaryPlotCache);
+}
+
+FW::TrackFitterPerformanceWriter::~TrackFitterPerformanceWriter() {
+  m_resPlotTool.clear(m_resPlotCache);
+  m_effPlotTool.clear(m_effPlotCache);
+  m_trackSummaryPlotTool.clear(m_trackSummaryPlotCache);
+
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::TrackFitterPerformanceWriter::endRun() {
+  // fill residual and pull details into additional hists
+  m_resPlotTool.refinement(m_resPlotCache);
+
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_resPlotTool.write(m_resPlotCache);
+    m_effPlotTool.write(m_effPlotCache);
+    m_trackSummaryPlotTool.write(m_trackSummaryPlotCache);
+
+    ACTS_INFO("Wrote performance plots to '" << m_outputFile->GetPath() << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::TrackFitterPerformanceWriter::writeT(
+    const AlgorithmContext& ctx, const TrajectoryContainer& trajectories) {
+  // Read truth particles from input collection
+  const auto& particles =
+      ctx.eventStore.get<SimParticleContainer>(m_cfg.inputParticles);
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Truth particles with corresponding reconstructed tracks
+  std::vector<ActsFatras::Barcode> reconParticleIds;
+  reconParticleIds.reserve(particles.size());
+
+  // Loop over all trajectories
+  for (const auto& traj : trajectories) {
+    // The trajectory entry indices and the multiTrajectory
+    const auto& [trackTips, mj] = traj.trajectory();
+    if (trackTips.empty()) {
+      ACTS_WARNING("Empty multiTrajectory.");
+      continue;
+    }
+
+    // Check the size of the trajectory entry indices. For track fitting, there
+    // should be at most one trajectory
+    if (trackTips.size() > 1) {
+      ACTS_ERROR("Track fitting should not result in multiple trajectories.");
+      return ProcessCode::ABORT;
+    }
+    // Get the entry index for the single trajectory
+    auto& trackTip = trackTips.front();
+
+    // Select reco track with fitted parameters
+    if (not traj.hasTrackParameters(trackTip)) {
+      ACTS_WARNING("No fitted track parameters.");
+      continue;
+    }
+    const auto& fittedParameters = traj.trackParameters(trackTip);
+
+    // Get the majority truth particle for this trajectory
+    const auto particleHitCount = traj.identifyMajorityParticle(trackTip);
+    if (particleHitCount.empty()) {
+      ACTS_WARNING("No truth particle associated with this trajectory.");
+      continue;
+    }
+    // Find the truth particle for the majority barcode
+    const auto ip = particles.find(particleHitCount.front().particleId);
+    if (ip == particles.end()) {
+      ACTS_WARNING("Majority particle not found in the particles collection.");
+      continue;
+    }
+
+    // Record this majority particle ID of this trajectory
+    reconParticleIds.push_back(ip->particleId());
+    // Fill the residual plots
+    m_resPlotTool.fill(m_resPlotCache, ctx.geoContext, *ip,
+                       traj.trackParameters(trackTip));
+    // Collect the trajectory summary info
+    auto trajState =
+        Acts::MultiTrajectoryHelpers::trajectoryState(mj, trackTip);
+    // Fill the trajectory summary info
+    m_trackSummaryPlotTool.fill(m_trackSummaryPlotCache, fittedParameters,
+                                trajState.nStates, trajState.nMeasurements,
+                                trajState.nOutliers, trajState.nHoles);
+  }
+
+  // Fill the efficiency, defined as the ratio between number of tracks with
+  // fitted parameter and total truth tracks (assumes one truth partilce has
+  // one truth track)
+  for (const auto& particle : particles) {
+    bool isReconstructed = false;
+    // Find if the particle has been reconstructed
+    auto it = std::find(reconParticleIds.begin(), reconParticleIds.end(),
+                        particle.particleId());
+    if (it != reconParticleIds.end()) {
+      isReconstructed = true;
+    }
+    m_effPlotTool.fill(m_effPlotCache, particle, isReconstructed);
+  }
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.hpp b/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fec88713d61f17b33cd3ef169110ffe6319ca569
--- /dev/null
+++ b/Io/Performance/ACTFW/Io/Performance/TrackFitterPerformanceWriter.hpp
@@ -0,0 +1,75 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <mutex>
+
+#include "ACTFW/EventData/Track.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/Validation/EffPlotTool.hpp"
+#include "ACTFW/Validation/ResPlotTool.hpp"
+#include "ACTFW/Validation/TrackSummaryPlotTool.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// Write out the residual and pull of track parameters and efficiency.
+///
+/// Efficiency here is the fraction of smoothed tracks compared to all tracks.
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class TrackFitterPerformanceWriter final : public WriterT<TrajectoryContainer> {
+ public:
+  struct Config {
+    /// Input truth particles collection.
+    std::string inputParticles;
+    /// Input (fitted) trajectories collection.
+    std::string inputTrajectories;
+    /// Output directory.
+    std::string outputDir;
+    /// Output filename.
+    std::string outputFilename = "performance_track_fitter.root";
+    /// Plot tool configurations.
+    ResPlotTool::Config resPlotToolConfig;
+    EffPlotTool::Config effPlotToolConfig;
+    TrackSummaryPlotTool::Config trackSummaryPlotToolConfig;
+  };
+
+  /// Construct from configuration and log level.
+  TrackFitterPerformanceWriter(Config cfg, Acts::Logging::Level lvl);
+  ~TrackFitterPerformanceWriter() override;
+
+  /// Finalize plots.
+  ProcessCode endRun() final override;
+
+ private:
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const TrajectoryContainer& trajectories) final override;
+
+  Config m_cfg;
+  /// Mutex used to protect multi-threaded writes.
+  std::mutex m_writeMutex;
+  TFile* m_outputFile{nullptr};
+  /// Plot tool for residuals and pulls.
+  ResPlotTool m_resPlotTool;
+  ResPlotTool::ResPlotCache m_resPlotCache;
+  /// Plot tool for efficiency
+  EffPlotTool m_effPlotTool;
+  EffPlotTool::EffPlotCache m_effPlotCache;
+  /// Plot tool for track hit info
+  TrackSummaryPlotTool m_trackSummaryPlotTool;
+  TrackSummaryPlotTool::TrackSummaryPlotCache m_trackSummaryPlotCache;
+};
+
+}  // namespace FW
diff --git a/Io/Performance/CMakeLists.txt b/Io/Performance/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6ee0f202917fe5c45447ec17eab3caed308cb687
--- /dev/null
+++ b/Io/Performance/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(
+  ActsExamplesIoPerformance SHARED
+  ACTFW/Io/Performance/TrackFinderPerformanceWriter.cpp
+  ACTFW/Io/Performance/TrackFitterPerformanceWriter.cpp
+  ACTFW/Io/Performance/CKFPerformanceWriter.cpp)
+target_include_directories(
+  ActsExamplesIoPerformance
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
+target_link_libraries(
+  ActsExamplesIoPerformance
+  PUBLIC ActsExamplesFramework
+  PRIVATE ActsCore ROOT::Core ROOT::Tree)
+
+install(
+  TARGETS ActsExamplesIoPerformance
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/Root/CMakeLists.txt b/Io/Root/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..771909b8b018a9dec6982353fdb86e98bdf2b5e1
--- /dev/null
+++ b/Io/Root/CMakeLists.txt
@@ -0,0 +1,28 @@
+add_library(
+  ActsExamplesIoRoot SHARED
+  src/RootMaterialDecorator.cpp
+  src/RootMaterialWriter.cpp
+  src/RootMaterialTrackReader.cpp
+  src/RootMaterialTrackWriter.cpp
+  src/RootPlanarClusterWriter.cpp
+  src/RootParticleWriter.cpp
+  src/RootPropagationStepsWriter.cpp
+  src/RootSimHitWriter.cpp
+  src/RootTrackParameterWriter.cpp
+  src/RootVertexAndTracksWriter.cpp
+  src/RootVertexAndTracksReader.cpp
+  src/RootTrajectoryWriter.cpp)
+target_include_directories(
+  ActsExamplesIoRoot
+  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
+target_link_libraries(
+  ActsExamplesIoRoot
+  PUBLIC
+    ActsCore ActsDigitizationPlugin ActsIdentificationPlugin
+    ActsExamplesFramework ActsExamplesPropagation ActsExamplesTruthTracking
+    Threads::Threads
+  PRIVATE ROOT::Core ROOT::Hist ROOT::Tree)
+
+install(
+  TARGETS ActsExamplesIoRoot
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/Io/Root/include/ACTFW/Io/Root/RootBFieldWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootBFieldWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8abdd9704782a4a7cfaf492145cf6248941e9d70
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootBFieldWriter.hpp
@@ -0,0 +1,318 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/MagneticField/ConstantBField.hpp>
+#include <Acts/MagneticField/InterpolatedBFieldMap.hpp>
+#include <Acts/Utilities/Helpers.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <Acts/Utilities/Units.hpp>
+#include <TFile.h>
+#include <TTree.h>
+#include <array>
+#include <boost/optional.hpp>
+#include <ios>
+#include <mutex>
+#include <sstream>
+#include <stdexcept>
+
+#include "ACTFW/Framework/IService.hpp"
+#include "ACTFW/Framework/ProcessCode.hpp"
+#include "ACTFW/Plugins/BField/ScalableBField.hpp"
+
+namespace FW {
+
+/// @class RootBFieldWriter
+///
+/// Writes out the Acts::InterpolatedbFieldMap. Currently implemented for 'rz'
+/// and 'xyz' field maps.
+template <typename bfield_t>
+class RootBFieldWriter {
+ public:
+  /// Describes the axes definition of the grid of the magnetic field map.
+  enum class GridType { rz = 0, xyz = 1 };
+
+  struct Config {
+    /// The name of the output tree
+    std::string treeName = "TTree";
+    /// The name of the output file
+    std::string fileName = "TFile.root";
+    /// the file access mode (recreate by default)
+    std::string fileMode = "recreate";
+    /// The magnetic field to be written out
+    std::shared_ptr<const bfield_t> bField = nullptr;
+    /// How the magnetic field map should be written out
+    GridType gridType = GridType::xyz;
+    /// [optional] Setting the range to be printed out in either r (for
+    /// cylinder coordinates) or x/y (in cartesian coordinates)
+    /// @note setting this parameter is optional, in case no boundaries are
+    /// handed over the full magnetic field map will be printed out
+    boost::optional<std::array<double, 2>> rBounds;
+    /// [optional] Setting the range in z to be printed out
+    /// @note setting this parameter is optional, in case no boundaries are
+    /// handed over the full magnetic field map will be printed out
+    boost::optional<std::array<double, 2>> zBounds;
+    /// Number of bins in r
+    /// @note setting this parameter is optional, in case no bin numbers are
+    /// handed over the full magnetic field map will be printed out
+    size_t rBins = 200;
+    /// Number of bins in z
+    // @note setting this parameter is optional, in case no bin numbers are
+    /// handed over the full magnetic field map will be printed out
+    size_t zBins = 300;
+    /// Number of bins in phi
+    // @note setting this parameter is optional, in case no bin numbers are
+    /// handed over the full magnetic field map will be printed out
+    size_t phiBins = 100;
+  };
+
+  /// Write down an interpolated magnetic field map
+  static void run(const Config& cfg,
+                  std::unique_ptr<const Acts::Logger> p_logger =
+                      Acts::getDefaultLogger("RootBFieldWriter",
+                                             Acts::Logging::INFO)) {
+    // Set up (local) logging
+    // @todo Remove dangerous using declaration once the logger macro
+    // tolerates it
+    using namespace Acts;
+    ACTS_LOCAL_LOGGER(std::move(p_logger))
+
+    // Check basic configuration
+    if (cfg.treeName.empty()) {
+      throw std::invalid_argument("Missing tree name");
+    } else if (cfg.fileName.empty()) {
+      throw std::invalid_argument("Missing file name");
+    } else if (!cfg.bField) {
+      throw std::invalid_argument("Missing interpolated magnetic field");
+    }
+
+    // Setup ROOT I/O
+    ACTS_INFO("Registering new ROOT output File : " << cfg.fileName);
+    TFile* outputFile = TFile::Open(cfg.fileName.c_str(), cfg.fileMode.c_str());
+    if (!outputFile) {
+      throw std::ios_base::failure("Could not open '" + cfg.fileName);
+    }
+    outputFile->cd();
+    TTree* outputTree = new TTree(cfg.treeName.c_str(), cfg.treeName.c_str());
+    if (!outputTree)
+      throw std::bad_alloc();
+
+    // The position values
+    double z;
+    outputTree->Branch("z", &z);
+
+    // The BField values
+    double Bz;
+    outputTree->Branch("Bz", &Bz);
+
+    // Get the underlying mapper of the InterpolatedBFieldMap
+    auto mapper = cfg.bField->getMapper();
+
+    // Access the minima and maxima of all axes
+    auto minima = mapper.getMin();
+    auto maxima = mapper.getMax();
+    auto nBins = mapper.getNBins();
+
+    if (cfg.gridType == GridType::xyz) {
+      ACTS_INFO("Map will be written out in cartesian coordinates (x,y,z).");
+
+      // Write out the interpolated magnetic field map
+      double stepX = 0., stepY = 0., stepZ = 0.;
+      double minX = 0., minY = 0., minZ = 0.;
+      double maxX = 0., maxY = 0., maxZ = 0.;
+      size_t nBinsX = 0, nBinsY = 0, nBinsZ = 0;
+
+      // The position values in xy
+      double x;
+      outputTree->Branch("x", &x);
+      double y;
+      outputTree->Branch("y", &y);
+      // The BField values in xy
+      double Bx;
+      outputTree->Branch("Bx", &Bx);
+      double By;
+      outputTree->Branch("By", &By);
+
+      // check if range is user defined
+      if (cfg.rBounds && cfg.zBounds) {
+        ACTS_INFO("User defined ranges handed over.");
+
+        // print out map in user defined range
+        minX = cfg.rBounds->at(0);
+        minY = cfg.rBounds->at(0);
+        minZ = cfg.zBounds->at(0);
+
+        maxX = cfg.rBounds->at(1);
+        maxY = cfg.rBounds->at(1);
+        maxZ = cfg.zBounds->at(1);
+
+        nBinsX = cfg.rBins;
+        nBinsY = cfg.rBins;
+        nBinsZ = cfg.zBins;
+
+      } else {
+        ACTS_INFO(
+            "No user defined ranges handed over - printing out whole map.");
+        // print out whole map
+        // check dimension of Bfieldmap
+        if (minima.size() == 3 && maxima.size() == 3) {
+          minX = minima.at(0);
+          minY = minima.at(1);
+          minZ = minima.at(2);
+
+          maxX = maxima.at(0);
+          maxY = maxima.at(1);
+          maxZ = maxima.at(2);
+
+          nBinsX = nBins.at(0);
+          nBinsY = nBins.at(1);
+          nBinsZ = nBins.at(2);
+
+        } else if (minima.size() == 2 && maxima.size() == 2) {
+          minX = -maxima.at(0);
+          minY = -maxima.at(0);
+          minZ = minima.at(1);
+
+          maxX = maxima.at(0);
+          maxY = maxima.at(0);
+          maxZ = maxima.at(1);
+
+          nBinsX = nBins.at(0);
+          nBinsY = nBins.at(0);
+          nBinsZ = nBins.at(1);
+        } else {
+          std::ostringstream errorMsg;
+          errorMsg
+              << "BField has wrong dimension. The dimension needs to be "
+                 "either 2 (r,z,Br,Bz) or 3(x,y,z,Bx,By,Bz) in order to be "
+                 "written out by this writer.";
+          throw std::invalid_argument(errorMsg.str());
+        }
+      }
+
+      stepX = fabs(minX - maxX) / nBinsX;
+      stepY = fabs(minY - maxY) / nBinsY;
+      stepZ = fabs(minZ - maxZ) / nBinsZ;
+
+      for (size_t i = 0; i <= nBinsX; i++) {
+        double raw_x = minX + i * stepX;
+        for (size_t j = 0; j <= nBinsY; j++) {
+          double raw_y = minY + j * stepY;
+          for (size_t k = 0; k <= nBinsZ; k++) {
+            double raw_z = minZ + k * stepZ;
+            Acts::Vector3D position(raw_x, raw_y, raw_z);
+            if (cfg.bField->isInside(position)) {
+              auto bField = cfg.bField->getField(position);
+
+              x = raw_x / Acts::units::_mm;
+              y = raw_y / Acts::units::_mm;
+              z = raw_z / Acts::units::_mm;
+              Bx = bField.x() / Acts::units::_T;
+              By = bField.y() / Acts::units::_T;
+              Bz = bField.z() / Acts::units::_T;
+              outputTree->Fill();
+            }
+          }  // for z
+        }    // for y
+      }      // for x
+    } else {
+      ACTS_INFO("Map will be written out in cylinder coordinates (r,z).");
+      // The position value in r
+      double r;
+      outputTree->Branch("r", &r);
+      // The BField value in r
+      double Br;
+      outputTree->Branch("Br", &Br);
+
+      double minR = 0, maxR = 0;
+      double minZ = 0, maxZ = 0;
+      size_t nBinsR = 0, nBinsZ = 0, nBinsPhi = 0;
+      double stepR = 0, stepZ = 0;
+
+      if (cfg.rBounds && cfg.zBounds) {
+        ACTS_INFO("User defined ranges handed over.");
+
+        minR = cfg.rBounds->at(0);
+        minZ = cfg.zBounds->at(0);
+
+        maxR = cfg.rBounds->at(1);
+        maxZ = cfg.zBounds->at(1);
+
+        nBinsR = cfg.rBins;
+        nBinsZ = cfg.zBins;
+        nBinsPhi = cfg.phiBins;
+      } else {
+        ACTS_INFO(
+            "No user defined ranges handed over - printing out whole map.");
+
+        if (minima.size() == 3 && maxima.size() == 3) {
+          minR = 0.;
+          minZ = minima.at(2);
+
+          maxR = maxima.at(0);
+          maxZ = maxima.at(2);
+
+          nBinsR = nBins.at(0);
+          nBinsZ = nBins.at(2);
+          nBinsPhi = 100.;
+
+        } else if (minima.size() == 2 || maxima.size() == 2) {
+          minR = minima.at(0);
+          minZ = minima.at(1);
+
+          maxR = maxima.at(0);
+          maxZ = maxima.at(1);
+
+          nBinsR = nBins.at(0);
+          nBinsZ = nBins.at(1);
+          nBinsPhi = 100.;
+
+        } else {
+          std::ostringstream errorMsg;
+          errorMsg
+              << "BField has wrong dimension. The dimension needs to be "
+                 "either 2 (r,z,Br,Bz) or 3(x,y,z,Bx,By,Bz) in order to be "
+                 "written out by this writer.";
+          throw std::invalid_argument(errorMsg.str());
+        }
+      }
+      double minPhi = -M_PI;
+      stepR = fabs(minR - maxR) / nBinsR;
+      stepZ = fabs(minZ - maxZ) / nBinsZ;
+      double stepPhi = (2 * M_PI) / nBinsPhi;
+
+      for (size_t i = 0; i < nBinsPhi; i++) {
+        double phi = minPhi + i * stepPhi;
+        for (size_t k = 0; k < nBinsZ; k++) {
+          double raw_z = minZ + k * stepZ;
+          for (size_t j = 0; j < nBinsR; j++) {
+            double raw_r = minR + j * stepR;
+            Acts::Vector3D position(raw_r * cos(phi), raw_r * sin(phi), raw_z);
+            if (cfg.bField->isInside(position)) {
+              auto bField = cfg.bField->getField(position);
+              z = raw_z / Acts::units::_mm;
+              r = raw_r / Acts::units::_mm;
+              Bz = bField.z() / Acts::units::_T;
+              Br = VectorHelpers::perp(bField) / Acts::units::_T;
+              outputTree->Fill();
+            }
+          }
+        }
+      }  // for
+    }
+
+    // Tear down ROOT I/O
+    ACTS_INFO("Closing and Writing ROOT output File : " << cfg.fileName);
+    outputFile->cd();
+    outputTree->Write();
+    outputFile->Close();
+  }
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootMaterialDecorator.hpp b/Io/Root/include/ACTFW/Io/Root/RootMaterialDecorator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d60417e9bc074ac1491e26621f8421a05cf312a5
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootMaterialDecorator.hpp
@@ -0,0 +1,151 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Geometry/TrackingVolume.hpp>
+#include <Acts/Material/IMaterialDecorator.hpp>
+#include <Acts/Material/ISurfaceMaterial.hpp>
+#include <Acts/Material/IVolumeMaterial.hpp>
+#include <Acts/Surfaces/Surface.hpp>
+#include <Acts/Utilities/Definitions.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <map>
+#include <mutex>
+
+#include "ACTFW/Framework/ProcessCode.hpp"
+
+class TFile;
+
+namespace Acts {
+using SurfaceMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const ISurfaceMaterial>>;
+using VolumeMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const IVolumeMaterial>>;
+}  // namespace Acts
+
+namespace FW {
+
+/// @class RootMaterialDecorator
+///
+/// @brief Read the collection of SurfaceMaterial & VolumeMaterial
+class RootMaterialDecorator : public Acts::IMaterialDecorator {
+ public:
+  /// @class Config
+  /// Configuration of the Reader
+  class Config {
+   public:
+    /// The name of the output tree
+    std::string folderNameBase = "Material";
+    /// The volume identification string
+    std::string voltag = "_vol";
+    /// The boundary identification string
+    std::string boutag = "_bou";
+    /// The layer identification string
+    std::string laytag = "_lay";
+    /// The approach identification string
+    std::string apptag = "_app";
+    /// The sensitive identification string
+    std::string sentag = "_sen";
+    /// The bin number tag
+    std::string ntag = "n";
+    /// The value tag -> binning values: binZ, binR, binPhi, etc.
+    std::string vtag = "v";
+    /// The option tag -> binning options: open, closed
+    std::string otag = "o";
+    /// The range min tag: min value
+    std::string mintag = "min";
+    /// The range max tag: max value
+    std::string maxtag = "max";
+    /// The thickness tag
+    std::string ttag = "t";
+    /// The x0 tag
+    std::string x0tag = "x0";
+    /// The l0 tag
+    std::string l0tag = "l0";
+    /// The A tag
+    std::string atag = "A";
+    /// The Z tag
+    std::string ztag = "Z";
+    /// The rho tag
+    std::string rhotag = "rho";
+    /// The name of the output file
+    std::string fileName = "material-maps.root";
+    /// The default logger
+    std::shared_ptr<const Acts::Logger> logger;
+    // The name of the writer
+    std::string name = "";
+
+    /// Constructor
+    ///
+    /// @param lname Name of the writer tool
+    /// @param lvl The output logging level
+    Config(const std::string& lname = "MaterialReader",
+           Acts::Logging::Level lvl = Acts::Logging::INFO)
+        : logger(Acts::getDefaultLogger(lname, lvl)), name(lname) {}
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg configuration struct for the reader
+  RootMaterialDecorator(const Config& cfg);
+
+  /// Destructor
+  ~RootMaterialDecorator();
+
+  /// Decorate a surface
+  ///
+  /// @param surface the non-cost surface that is decorated
+  void decorate(Acts::Surface& surface) const final {
+    // Null out the material for this surface
+    if (m_clearSurfaceMaterial) {
+      surface.assignSurfaceMaterial(nullptr);
+    }
+    // Try to find the surface in the map
+    auto sMaterial = m_surfaceMaterialMap.find(surface.geoID());
+    if (sMaterial != m_surfaceMaterialMap.end()) {
+      surface.assignSurfaceMaterial(sMaterial->second);
+    }
+  }
+
+  /// Decorate a TrackingVolume
+  ///
+  /// @param volume the non-cost volume that is decorated
+  void decorate(Acts::TrackingVolume& volume) const final {
+    // Null out the material for this volume
+    if (m_clearSurfaceMaterial) {
+      volume.assignVolumeMaterial(nullptr);
+    }
+    // Try to find the surface in the map
+    auto vMaterial = m_volumeMaterialMap.find(volume.geoID());
+    if (vMaterial != m_volumeMaterialMap.end()) {
+      volume.assignVolumeMaterial(vMaterial->second);
+    }
+  }
+
+ private:
+  /// The config class
+  Config m_cfg;
+
+  /// The input file
+  TFile* m_inputFile{nullptr};
+
+  /// Surface based material
+  Acts::SurfaceMaterialMap m_surfaceMaterialMap;
+
+  /// Volume based material
+  Acts::VolumeMaterialMap m_volumeMaterialMap;
+
+  bool m_clearSurfaceMaterial{true};
+
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackReader.hpp b/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackReader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..151552852a046c50f55071c53c447d71bf10993b
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackReader.hpp
@@ -0,0 +1,116 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Propagator/MaterialInteractor.hpp>
+#include <Acts/Utilities/Definitions.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <mutex>
+#include <vector>
+
+#include "ACTFW/Framework/IReader.hpp"
+#include "ACTFW/Framework/IService.hpp"
+#include "ACTFW/Framework/ProcessCode.hpp"
+
+class TChain;
+
+namespace FW {
+
+/// @class RootMaterialTrackReader
+///
+/// @brief Reads in MaterialTrack information from a root file
+/// and fills it into a format to be understood by the MaterialMapping
+/// algorithm
+class RootMaterialTrackReader : public IReader {
+ public:
+  /// @brief The nested configuration struct
+  struct Config {
+    std::string collection =
+        "material-tracks";                     ///< material collection to read
+    std::string filePath = "";                 ///< path of the output file
+    std::string treeName = "material-tracks";  ///< name of the output tree
+    std::vector<std::string> fileList;         ///< The name of the input file
+
+    unsigned int batchSize = 1;  ///!< The number of tracks per event
+
+    /// The default logger
+    std::shared_ptr<const Acts::Logger> logger;
+
+    /// The name of the service
+    std::string name;
+
+    /// Constructor
+    /// @param lname The name of the Material reader
+    /// @parqam lvl The log level for the logger
+    Config(const std::string& lname = "MaterialReader",
+           Acts::Logging::Level lvl = Acts::Logging::INFO)
+        : logger(Acts::getDefaultLogger(lname, lvl)), name(lname) {}
+  };
+
+  /// Constructor
+  /// @param cfg The Configuration struct
+  RootMaterialTrackReader(const Config& cfg);
+
+  /// Destructor
+  ~RootMaterialTrackReader();
+
+  /// Framework name() method
+  std::string name() const final override;
+
+  /// Return the available events range.
+  std::pair<size_t, size_t> availableEvents() const final override;
+
+  /// Read out data from the input stream
+  ///
+  /// @param context The algorithm context
+  ProcessCode read(const FW::AlgorithmContext& context) final override;
+
+ private:
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+
+  /// The config class
+  Config m_cfg;
+
+  /// mutex used to protect multi-threaded reads
+  std::mutex m_read_mutex;
+
+  /// The number of events
+  size_t m_events = 0;
+
+  /// The input tree name
+  TChain* m_inputChain = nullptr;
+
+  float m_v_x;    ///< start global x
+  float m_v_y;    ///< start global y
+  float m_v_z;    ///< start global z
+  float m_v_px;   ///< start global momentum x
+  float m_v_py;   ///< start global momentum y
+  float m_v_pz;   ///< start global momentum z
+  float m_v_phi;  ///< start phi direction
+  float m_v_eta;  ///< start eta direction
+  float m_tX0;    ///< thickness in X0/L0
+  float m_tL0;    ///< thickness in X0/L0
+
+  std::vector<float>* m_step_x = new std::vector<float>;   ///< step x position
+  std::vector<float>* m_step_y = new std::vector<float>;   ///< step y position
+  std::vector<float>* m_step_z = new std::vector<float>;   ///< step z position
+  std::vector<float>* m_step_dx = new std::vector<float>;  ///< step x direction
+  std::vector<float>* m_step_dy = new std::vector<float>;  ///< step y direction
+  std::vector<float>* m_step_dz = new std::vector<float>;  ///< step z direction
+  std::vector<float>* m_step_length = new std::vector<float>;  ///< step length
+  std::vector<float>* m_step_X0 = new std::vector<float>;  ///< step material x0
+  std::vector<float>* m_step_L0 = new std::vector<float>;  ///< step material l0
+  std::vector<float>* m_step_A = new std::vector<float>;   ///< step material A
+  std::vector<float>* m_step_Z = new std::vector<float>;   ///< step material Z
+  std::vector<float>* m_step_rho =
+      new std::vector<float>;  ///< step material rho
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..64c3d42e9aa4cc93c8198a204e35f6b0465986bf
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootMaterialTrackWriter.hpp
@@ -0,0 +1,139 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Propagator/MaterialInteractor.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <mutex>
+
+#include "ACTFW/Framework/IService.hpp"
+#include "ACTFW/Framework/ProcessCode.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+
+class TFile;
+class TTree;
+
+namespace Acts {
+// Using some short hands for Recorded Material
+using RecordedMaterial = MaterialInteractor::result_type;
+// And recorded material track
+// - this is start:  position, start momentum
+//   and the Recorded material
+using RecordedMaterialTrack =
+    std::pair<std::pair<Acts::Vector3D, Acts::Vector3D>, RecordedMaterial>;
+}  // namespace Acts
+
+namespace FW {
+
+/// @class RootMaterialTrackWriter
+///
+/// @brief Writes out MaterialTrack collections from a root file
+///
+/// This service is the root implementation of the IWriterT.
+/// It writes out a MaterialTrack which is usually generated from
+/// Geant4 material mapping
+class RootMaterialTrackWriter
+    : public WriterT<std::vector<Acts::RecordedMaterialTrack>> {
+ public:
+  struct Config {
+    std::string collection =
+        "material-tracks";                     ///< material collection to write
+    std::string filePath = "";                 ///< path of the output file
+    std::string fileMode = "RECREATE";         ///< file access mode
+    std::string treeName = "material-tracks";  ///< name of the output tree
+    TFile* rootFile = nullptr;                 ///< common root file
+
+    /// Re-calculate total values from individual steps (for cross-checks)
+    bool recalculateTotals = false;
+    /// Write aut pre and post step (for G4), otherwise central step position
+    bool prePostStep = false;
+    /// Write the surface to which the material step correpond
+    bool storesurface = false;
+  };
+
+  /// Constructor with
+  /// @param cfg configuration struct
+  /// @param output logging level
+  RootMaterialTrackWriter(const Config& cfg,
+                          Acts::Logging::Level level = Acts::Logging::INFO);
+
+  /// Virtual destructor
+  ~RootMaterialTrackWriter() override;
+
+  /// Framework intialize method
+  FW::ProcessCode endRun() final override;
+
+ protected:
+  // This implementation holds the actual writing method
+  /// and is called by the WriterT<>::write interface
+  ///
+  /// @param ctx The Algorithm context with per event information
+  /// @param clusters is the data to be written out
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const std::vector<Acts::RecordedMaterialTrack>&
+                         materialtracks) final override;
+
+ private:
+  /// The config class
+  Config m_cfg;
+  /// mutex used to protect multi-threaded writes
+  std::mutex m_writeMutex;
+  /// The output file name
+  TFile* m_outputFile;
+  /// The output tree name
+  TTree* m_outputTree;
+
+  float m_v_x;    ///< start global x
+  float m_v_y;    ///< start global y
+  float m_v_z;    ///< start global z
+  float m_v_px;   ///< start global momentum x
+  float m_v_py;   ///< start global momentum y
+  float m_v_pz;   ///< start global momentum z
+  float m_v_phi;  ///< start phi direction
+  float m_v_eta;  ///< start eta direction
+  float m_tX0;    ///< thickness in X0/L0
+  float m_tL0;    ///< thickness in X0/L0
+
+  std::vector<float> m_step_sx;      ///< step x (start) position (optional)
+  std::vector<float> m_step_sy;      ///< step y (start) position (optional)
+  std::vector<float> m_step_sz;      ///< step z (start) position (optional)
+  std::vector<float> m_step_x;       ///< step x position
+  std::vector<float> m_step_y;       ///< step y position
+  std::vector<float> m_step_z;       ///< step z position
+  std::vector<float> m_step_ex;      ///< step x (end) position (optional)
+  std::vector<float> m_step_ey;      ///< step y (end) position (optional)
+  std::vector<float> m_step_ez;      ///< step z (end) position (optional)
+  std::vector<float> m_step_dx;      ///< step x direction
+  std::vector<float> m_step_dy;      ///< step y direction
+  std::vector<float> m_step_dz;      ///< step z direction
+  std::vector<float> m_step_length;  ///< step length
+  std::vector<float> m_step_X0;      ///< step material x0
+  std::vector<float> m_step_L0;      ///< step material l0
+  std::vector<float> m_step_A;       ///< step material A
+  std::vector<float> m_step_Z;       ///< step material Z
+  std::vector<float> m_step_rho;     ///< step material rho
+
+  std::vector<std::uint64_t>
+      m_sur_id;  ///< ID of the suface associated with the step
+  std::vector<int32_t>
+      m_sur_type;              ///< Type of the suface associated with the step
+  std::vector<float> m_sur_x;  ///< x position of the center of the suface
+                               ///< associated with the step
+  std::vector<float> m_sur_y;  ///< y position of the center of the suface
+                               ///< associated with the step
+  std::vector<float> m_sur_z;  ///< z position of the center of the suface
+                               ///< associated with the step
+
+  std::vector<float>
+      m_sur_range_min;  ///< Min range of the suface associated with the step
+  std::vector<float>
+      m_sur_range_max;  ///< Max range of the suface associated with the step
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootMaterialWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootMaterialWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..043035f05f8f0f779c2f7efa4b714a0da2e53d37
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootMaterialWriter.hpp
@@ -0,0 +1,154 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2019 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 http://mozilla.org/MPL/2.0/.
+
+///////////////////////////////////////////////////////////////////
+// RootMaterialWriter.h
+///////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Geometry/TrackingGeometry.hpp>
+#include <Acts/Geometry/TrackingVolume.hpp>
+#include <Acts/Material/IMaterialDecorator.hpp>
+#include <Acts/Material/ISurfaceMaterial.hpp>
+#include <Acts/Material/IVolumeMaterial.hpp>
+#include <Acts/Surfaces/Surface.hpp>
+#include <Acts/Utilities/Definitions.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <map>
+#include <mutex>
+
+#include "ACTFW/Framework/ProcessCode.hpp"
+
+namespace Acts {
+using SurfaceMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const ISurfaceMaterial>>;
+using VolumeMaterialMap =
+    std::map<GeometryID, std::shared_ptr<const IVolumeMaterial>>;
+using DetectorMaterialMaps = std::pair<SurfaceMaterialMap, VolumeMaterialMap>;
+}  // namespace Acts
+
+namespace FW {
+
+/// @brief Material decorator from Root format
+///
+/// This reads in material maps for surfaces and volumes
+/// from a root file
+class RootMaterialWriter {
+ public:
+  /// @class Config
+  ///
+  /// Configuration of the Writer
+  struct Config {
+    /// Steering to handle sensitive data
+    bool processSensitives = true;
+
+    /// Steering to handle approach data
+    bool processApproaches = true;
+
+    /// Steering to handle representing data
+    bool processRepresenting = true;
+
+    /// Steering to handle boundary data
+    bool processBoundaries = true;
+
+    /// Steering to handle volume data
+    bool processVolumes = true;
+
+    /// The name of the output tree
+    std::string folderNameBase = "Material";
+    /// The volume identification string
+    std::string voltag = "_vol";
+    /// The boundary identification string
+    std::string boutag = "_bou";
+    /// The layer identification string
+    std::string laytag = "_lay";
+    /// The approach identification string
+    std::string apptag = "_app";
+    /// The sensitive identification string
+    std::string sentag = "_sen";
+    /// The bin number tag
+    std::string ntag = "n";
+    /// The value tag -> binning values: binZ, binR, binPhi, etc.
+    std::string vtag = "v";
+    /// The option tag -> binning options: open, closed
+    std::string otag = "o";
+    /// The range min tag: min value
+    std::string mintag = "min";
+    /// The range max tag: max value
+    std::string maxtag = "max";
+    /// The thickness tag
+    std::string ttag = "t";
+    /// The x0 tag
+    std::string x0tag = "x0";
+    /// The l0 tag
+    std::string l0tag = "l0";
+    /// The A tag
+    std::string atag = "A";
+    /// The Z tag
+    std::string ztag = "Z";
+    /// The rho tag
+    std::string rhotag = "rho";
+    /// The name of the output file
+    std::string fileName = "material-maps.root";
+    /// The default logger
+    std::shared_ptr<const Acts::Logger> logger;
+    // The name of the writer
+    std::string name = "";
+
+    /// Constructor
+    ///
+    /// @param lname Name of the writer tool
+    /// @param lvl The output logging level
+    Config(const std::string& lname = "RootMaterialWriter",
+           Acts::Logging::Level lvl = Acts::Logging::INFO)
+        : logger(Acts::getDefaultLogger(lname, lvl)), name(lname) {}
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg The configuration struct
+  RootMaterialWriter(const Config& cfg);
+
+  /// Virtual destructor
+  ~RootMaterialWriter() = default;
+
+  /// Write out the material map
+  ///
+  /// @param detMaterial is the SurfaceMaterial and VolumeMaterial maps
+  void write(const Acts::DetectorMaterialMaps& detMaterial);
+
+  /// Write out the material map from Geometry
+  ///
+  /// @param tGeometry is the TrackingGeometry
+  void write(const Acts::TrackingGeometry& tGeometry);
+
+ private:
+  /// Collect the material from the tracking geometry
+  ///
+  /// @param tVolume The TrackingVolume for the material to be collected
+  /// @param [in,out] detMatMap the map to be filled
+  void collectMaterial(const Acts::TrackingVolume& tVolume,
+                       Acts::DetectorMaterialMaps& detMatMap);
+
+  /// Collect the material from the tracking geometry
+  ///
+  /// @param tLayer The TrackingVolume for the material to be collected
+  /// @param [in,out] detMatMap the map to be filled
+  void collectMaterial(const Acts::Layer& tLayer,
+                       Acts::DetectorMaterialMaps& detMatMap);
+
+  /// The config class
+  Config m_cfg;
+
+  /// Private access to the logging instance
+  const Acts::Logger& logger() const { return *m_cfg.logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootParticleWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootParticleWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4e8077d1da5c28e6c0d7bbca9723653f16081678
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootParticleWriter.hpp
@@ -0,0 +1,102 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <cstdint>
+#include <mutex>
+#include <string>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// Write out particles as a flat TTree.
+///
+/// Each entry in the TTree corresponds to one particle for optimum writing
+/// speed. The event number is part of the written data.
+///
+/// Safe to use from multiple writer threads. To avoid thread-saftey issues,
+/// the writer must be the sole owner of the underlying file. Thus, the
+/// output file pointer can not be given from the outside.
+class RootParticleWriter final : public WriterT<SimParticleContainer> {
+ public:
+  struct Config {
+    /// Input particle collection to write.
+    std::string inputParticles;
+    /// Path to the output file.
+    std::string filePath;
+    /// Output file access mode.
+    std::string fileMode = "RECREATE";
+    /// Name of the tree within the output file.
+    std::string treeName = "particles";
+  };
+
+  /// Construct the particle writer.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  RootParticleWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Ensure underlying file is closed.
+  ~RootParticleWriter() final override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// Type-specific write implementation.
+  ///
+  /// @param[in] ctx is the algorithm context
+  /// @param[in] particles are the particle to be written
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const SimParticleContainer& particles) final override;
+
+ private:
+  Config m_cfg;
+  std::mutex m_writeMutex;
+  TFile* m_outputFile = nullptr;
+  TTree* m_outputTree = nullptr;
+  /// Event identifier.
+  uint32_t m_eventId;
+  /// Event-unique particle identifier a.k.a barcode.
+  uint64_t m_particleId;
+  /// Particle type a.k.a. PDG particle number
+  int32_t m_particleType;
+  /// Production process type, i.e. what generated the particle.
+  uint32_t m_process;
+  /// Production position components in mm.
+  float m_vx, m_vy, m_vz;
+  // Production time in ns.
+  float m_vt;
+  /// Momentum components in GeV.
+  float m_px, m_py, m_pz;
+  /// Mass in GeV.
+  float m_m;
+  /// Charge in e.
+  float m_q;
+  // Derived kinematic quantities
+  /// Direction pseudo-rapidity.
+  float m_eta;
+  /// Direction angle in the transverse plane.
+  float m_phi;
+  /// Transverse momentum in GeV.
+  float m_pt;
+  // Decoded particle identifier; see Barcode definition for details.
+  uint32_t m_vertexPrimary;
+  uint32_t m_vertexSecondary;
+  uint32_t m_particle;
+  uint32_t m_generation;
+  uint32_t m_subParticle;
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootPlanarClusterWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootPlanarClusterWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1dbdde6673632dbd085bc42961be7c2100e5cd84
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootPlanarClusterWriter.hpp
@@ -0,0 +1,102 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <mutex>
+
+#include "ACTFW/EventData/GeometryContainers.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "Acts/Plugins/Digitization/PlanarModuleCluster.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// @class RootPlanarClusterWriter
+///
+/// Write out a planar cluster collection into a root file
+/// to avoid immense long vectors, each cluster is one entry
+/// in the root file for optimised data writing speed
+/// The event number is part of the written data.
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class RootPlanarClusterWriter
+    : public WriterT<GeometryIdMultimap<Acts::PlanarModuleCluster>> {
+ public:
+  struct Config {
+    /// Which cluster collection to write.
+    std::string inputClusters;
+    /// Which simulated (truth) hits collection to use.
+    std::string inputSimulatedHits;
+    std::string filePath = "";          ///< path of the output file
+    std::string fileMode = "RECREATE";  ///< file access mode
+    std::string treeName = "clusters";  ///< name of the output tree
+    TFile* rootFile = nullptr;          ///< common root file
+  };
+
+  /// Constructor with
+  /// @param cfg configuration struct
+  /// @param output logging level
+  RootPlanarClusterWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Virtual destructor
+  ~RootPlanarClusterWriter() override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// This implementation holds the actual writing method
+  /// and is called by the WriterT<>::write interface
+  ///
+  /// @param ctx The Algorithm context with per event information
+  /// @param clusters is the data to be written out
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const GeometryIdMultimap<Acts::PlanarModuleCluster>&
+                         clusters) final override;
+
+ private:
+  Config m_cfg;                    ///< the configuration object
+  std::mutex m_writeMutex;         ///< protect multi-threaded writes
+  TFile* m_outputFile;             ///< the output file
+  TTree* m_outputTree;             ///< the output tree
+  int m_eventNr;                   ///< the event number of
+  int m_volumeID;                  ///< volume identifier
+  int m_layerID;                   ///< layer identifier
+  int m_surfaceID;                 ///< surface identifier
+  float m_x;                       ///< global x
+  float m_y;                       ///< global y
+  float m_z;                       ///< global z
+  float m_t;                       ///< global t
+  float m_lx;                      ///< local lx
+  float m_ly;                      ///< local ly
+  float m_cov_lx;                  ///< local covariance lx
+  float m_cov_ly;                  ///< local covariance ly
+  std::vector<int> m_cell_IDx;     ///< cell ID in lx
+  std::vector<int> m_cell_IDy;     ///< cell ID in ly
+  std::vector<float> m_cell_lx;    ///< local cell position x
+  std::vector<float> m_cell_ly;    ///< local cell position y
+  std::vector<float> m_cell_data;  ///< local cell position y
+
+  // (optional) the truth position
+  std::vector<float> m_t_gx;  ///< truth position global x
+  std::vector<float> m_t_gy;  ///< truth position global y
+  std::vector<float> m_t_gz;  ///< truth position global z
+  std::vector<float> m_t_gt;  ///< truth time t
+  std::vector<float> m_t_lx;  ///< truth position local x
+  std::vector<float> m_t_ly;  ///< truth position local y
+  std::vector<unsigned long>
+      m_t_barcode;  ///< associated truth particle barcode
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootPropagationStepsWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootPropagationStepsWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ccf3a6cf971a887a42c40b791f0c4fb3098c474
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootPropagationStepsWriter.hpp
@@ -0,0 +1,91 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <ACTFW/Framework/WriterT.hpp>
+#include <mutex>
+
+#include "Acts/Propagator/detail/SteppingLogger.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+using PropagationSteps = std::vector<Acts::detail::Step>;
+
+/// @class RootPropagationStepsWriter
+///
+/// Write out the steps of test propgations for stepping validation,
+/// each step sequence is one entry in the  in the root file for optimised
+/// data writing speed.
+/// The event number is part of the written data.
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class RootPropagationStepsWriter
+    : public WriterT<std::vector<PropagationSteps>> {
+ public:
+  struct Config {
+    std::string collection =
+        "propagation_steps";            ///< particle collection to write
+    std::string filePath = "";          ///< path of the output file
+    std::string fileMode = "RECREATE";  ///< file access mode
+    std::string treeName = "propagation_steps";  ///< name of the output tree
+    TFile* rootFile = nullptr;                   ///< common root file
+  };
+
+  /// Constructor with
+  /// @param cfg configuration struct
+  /// @param output logging level
+  RootPropagationStepsWriter(const Config& cfg,
+                             Acts::Logging::Level level = Acts::Logging::INFO);
+
+  /// Virtual destructor
+  ~RootPropagationStepsWriter() override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// This implementation holds the actual writing method
+  /// and is called by the WriterT<>::write interface
+  ///
+  /// @param context The Algorithm context with per event information
+  /// @param steps is the data to be written out
+  ProcessCode writeT(const AlgorithmContext& context,
+                     const std::vector<PropagationSteps>& steps) final override;
+
+ private:
+  Config m_cfg;                    ///< the configuration object
+  std::mutex m_writeMutex;         ///< protect multi-threaded writes
+  TFile* m_outputFile;             ///< the output file name
+  TTree* m_outputTree;             ///< the output tree
+  int m_eventNr;                   ///< the event number of
+  std::vector<int> m_volumeID;     ///< volume identifier
+  std::vector<int> m_boundaryID;   ///< boundary identifier
+  std::vector<int> m_layerID;      ///< layer identifier if
+  std::vector<int> m_approachID;   ///< surface identifier
+  std::vector<int> m_sensitiveID;  ///< surface identifier
+  std::vector<float> m_x;          ///< global x
+  std::vector<float> m_y;          ///< global y
+  std::vector<float> m_z;          ///< global z
+  std::vector<float> m_dx;         ///< global direction x
+  std::vector<float> m_dy;         ///< global direction y
+  std::vector<float> m_dz;         ///< global direction z
+  std::vector<int> m_step_type;    ///< step type
+  std::vector<float> m_step_acc;   ///< accuracy
+  std::vector<float> m_step_act;   ///< actor check
+  std::vector<float> m_step_abt;   ///< aborter
+  std::vector<float> m_step_usr;   ///< user
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootSimHitWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootSimHitWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b98cd385e59cdb388996ab2a99ff409cb02e09f6
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootSimHitWriter.hpp
@@ -0,0 +1,93 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <cstdint>
+#include <mutex>
+#include <string>
+
+#include "ACTFW/EventData/SimHit.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// Write out simulated hits as a flat TTree.
+///
+/// Each entry in the TTree corresponds to one hit for optimum writing
+/// speed. The event number is part of the written data.
+///
+/// Safe to use from multiple writer threads. To avoid thread-saftey issues,
+/// the writer must be the sole owner of the underlying file. Thus, the
+/// output file pointer can not be given from the outside.
+class RootSimHitWriter final : public WriterT<SimHitContainer> {
+ public:
+  struct Config {
+    /// Input particle collection to write.
+    std::string inputSimulatedHits;
+    /// Path to the output file.
+    std::string filePath;
+    /// Output file access mode.
+    std::string fileMode = "RECREATE";
+    /// Name of the tree within the output file.
+    std::string treeName = "hits";
+  };
+
+  /// Construct the particle writer.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  RootSimHitWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Ensure underlying file is closed.
+  ~RootSimHitWriter() final override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// Type-specific write implementation.
+  ///
+  /// @param[in] ctx is the algorithm context
+  /// @param[in] hits are the hits to be written
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const SimHitContainer& hits) final override;
+
+ private:
+  Config m_cfg;
+  std::mutex m_writeMutex;
+  TFile* m_outputFile = nullptr;
+  TTree* m_outputTree = nullptr;
+  /// Event identifier.
+  uint32_t m_eventId;
+  /// Hit surface identifier.
+  uint64_t m_geometryId;
+  /// Event-unique particle identifier a.k.a. barcode.
+  uint64_t m_particleId;
+  /// True global hit position components in mm.
+  float m_tx, m_ty, m_tz;
+  // True global hit time in ns.
+  float m_tt;
+  /// True particle four-momentum in GeV at hit position before interaction.
+  float m_tpx, m_tpy, m_tpz, m_te;
+  /// True change in particle four-momentum in GeV due to interactions.
+  float m_deltapx, m_deltapy, m_deltapz, m_deltae;
+  /// Hit index along the particle trajectory
+  int32_t m_index;
+  // Decoded hit surface identifier components.
+  uint32_t m_volumeId;
+  uint32_t m_boundaryId;
+  uint32_t m_layerId;
+  uint32_t m_approachId;
+  uint32_t m_sensitiveId;
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootTrackParameterWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootTrackParameterWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c2e6da18c88ce471ed3e505736b273fe81804ecf
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootTrackParameterWriter.hpp
@@ -0,0 +1,70 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/EventData/TrackParameters.hpp>
+#include <mutex>
+
+#include "ACTFW/Framework/WriterT.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+using BoundTrackParameters = Acts::BoundParameters;
+using TrackParameterWriter = WriterT<std::vector<BoundTrackParameters>>;
+
+/// Writes out SingleBoundTrackParamters into a TTree
+
+class RootTrackParameterWriter final : public TrackParameterWriter {
+ public:
+  struct Config {
+    std::string collection;             ///< parameter collection to write
+    std::string filePath;               ///< path of the output file
+    std::string fileMode = "RECREATE";  ///< file access mode
+    std::string treeName = "trackparameters";  ///< name of the output tree
+    TFile* rootFile = nullptr;                 ///< common root file
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg Configuration struct
+  /// @param level Message level declaration
+  RootTrackParameterWriter(const Config& cfg,
+                           Acts::Logging::Level level = Acts::Logging::INFO);
+
+  /// Virtual destructor
+  ~RootTrackParameterWriter() override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// @brief Write method called by the base class
+  /// @param [in] ctx is the algorithm context for event information
+  /// @param [in] trackParams are parameters to write
+  ProcessCode writeT(
+      const AlgorithmContext& ctx,
+      const std::vector<BoundTrackParameters>& trackParams) final override;
+
+ private:
+  Config m_cfg;             ///< The config class
+  std::mutex m_writeMutex;  ///< Mutex used to protect multi-threaded writes
+  TFile* m_outputFile{nullptr};  ///< The output file
+  TTree* m_outputTree{nullptr};  ///< The output tree
+  int m_eventNr{0};              ///< the event number of
+  float m_d0{0.};                ///< transversal IP d0
+  float m_z0{0.};                ///< longitudinal IP z0
+  float m_phi{0.};               ///< phi
+  float m_theta{0.};             ///< theta
+  float m_qp{0.};                ///< q/p
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootTrajectoryWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootTrajectoryWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..af5865adf8596dcfa544ebb64a1356e0ed9c3ee0
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootTrajectoryWriter.hpp
@@ -0,0 +1,249 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <mutex>
+#include <vector>
+
+#include "ACTFW/EventData/Track.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "Acts/Utilities/ParameterDefinitions.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// @class RootTrajectoryWriter
+///
+/// Write out a trajectory (i.e. a vector of
+/// trackState at the moment) into a TTree
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+///
+/// Each entry in the TTree corresponds to one trajectory for optimum
+/// writing speed. The event number is part of the written data.
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing
+/// file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class RootTrajectoryWriter final : public WriterT<TrajectoryContainer> {
+ public:
+  /// @brief The nested configuration struct
+  struct Config {
+    std::string inputParticles;     ///< input truth particles collection.
+    std::string inputTrajectories;  ///< input (fitted) trajectories collection
+    std::string outputDir;          ///< output directory
+    std::string outputFilename = "tracks.root";  ///< output filename
+    std::string outputTreename = "tracks";       ///< name of the output tree
+    std::string fileMode = "RECREATE";           ///< file access mode
+    TFile* rootFile = nullptr;                   ///< common root file
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg Configuration struct
+  /// @param level Message level declaration
+  RootTrajectoryWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Virtual destructor
+  ~RootTrajectoryWriter() final override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// @brief Write method called by the base class
+  /// @param [in] ctx is the algorithm context for event information
+  /// @param [in] trajectories are what to be written out
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const TrajectoryContainer& trajectories) final override;
+
+ private:
+  Config m_cfg;             ///< The config class
+  std::mutex m_writeMutex;  ///< Mutex used to protect multi-threaded writes
+  TFile* m_outputFile{nullptr};  ///< The output file
+  TTree* m_outputTree{nullptr};  ///< The output tree
+  int m_eventNr{0};              ///< the event number
+  int m_trajNr{0};               ///< the trajectory number
+
+  unsigned long m_t_barcode{0};  ///< Truth particle barcode
+  int m_t_charge{0};             ///< Truth particle charge
+  float m_t_time{0};             ///< Truth particle time
+  float m_t_vx{-99.};            ///< Truth particle vertex x
+  float m_t_vy{-99.};            ///< Truth particle vertex y
+  float m_t_vz{-99.};            ///< Truth particle vertex z
+  float m_t_px{-99.};            ///< Truth particle initial momentum px
+  float m_t_py{-99.};            ///< Truth particle initial momentum py
+  float m_t_pz{-99.};            ///< Truth particle initial momentum pz
+  float m_t_theta{-99.};         ///< Truth particle initial momentum theta
+  float m_t_phi{-99.};           ///< Truth particle initial momentum phi
+  float m_t_pT{-99.};            ///< Truth particle initial momentum pT
+  float m_t_eta{-99.};           ///< Truth particle initial momentum eta
+
+  std::vector<float> m_t_x;  ///< Global truth hit position x
+  std::vector<float> m_t_y;  ///< Global truth hit position y
+  std::vector<float> m_t_z;  ///< Global truth hit position z
+  std::vector<float> m_t_r;  ///< Global truth hit position r
+  std::vector<float>
+      m_t_dx;  ///< Truth particle direction x at global hit position
+  std::vector<float>
+      m_t_dy;  ///< Truth particle direction y at global hit position
+  std::vector<float>
+      m_t_dz;  ///< Truth particle direction z at global hit position
+
+  std::vector<float> m_t_eLOC0;   ///< truth parameter eLOC_0
+  std::vector<float> m_t_eLOC1;   ///< truth parameter eLOC_1
+  std::vector<float> m_t_ePHI;    ///< truth parameter ePHI
+  std::vector<float> m_t_eTHETA;  ///< truth parameter eTHETA
+  std::vector<float> m_t_eQOP;    ///< truth parameter eQOP
+  std::vector<float> m_t_eT;      ///< truth parameter eT
+
+  int m_nStates{0};                 ///< number of all states
+  int m_nMeasurements{0};           ///< number of states with measurements
+  std::vector<int> m_volumeID;      ///< volume identifier
+  std::vector<int> m_layerID;       ///< layer identifier
+  std::vector<int> m_moduleID;      ///< surface identifier
+  std::vector<float> m_lx_hit;      ///< uncalibrated measurement local x
+  std::vector<float> m_ly_hit;      ///< uncalibrated measurement local y
+  std::vector<float> m_x_hit;       ///< uncalibrated measurement global x
+  std::vector<float> m_y_hit;       ///< uncalibrated measurement global y
+  std::vector<float> m_z_hit;       ///< uncalibrated measurement global z
+  std::vector<float> m_res_x_hit;   ///< hit residual x
+  std::vector<float> m_res_y_hit;   ///< hit residual y
+  std::vector<float> m_err_x_hit;   ///< hit err x
+  std::vector<float> m_err_y_hit;   ///< hit err y
+  std::vector<float> m_pull_x_hit;  ///< hit pull x
+  std::vector<float> m_pull_y_hit;  ///< hit pull y
+  std::vector<int> m_dim_hit;       ///< dimension of measurement
+
+  bool m_hasFittedParams;        ///< if the track has fitted parameter
+  float m_eLOC0_fit{-99.};       ///< fitted parameter eLOC_0
+  float m_eLOC1_fit{-99.};       ///< fitted parameter eLOC_1
+  float m_ePHI_fit{-99.};        ///< fitted parameter ePHI
+  float m_eTHETA_fit{-99.};      ///< fitted parameter eTHETA
+  float m_eQOP_fit{-99.};        ///< fitted parameter eQOP
+  float m_eT_fit{-99.};          ///< fitted parameter eT
+  float m_err_eLOC0_fit{-99.};   ///< fitted parameter eLOC_-99.err
+  float m_err_eLOC1_fit{-99.};   ///< fitted parameter eLOC_1 err
+  float m_err_ePHI_fit{-99.};    ///< fitted parameter ePHI err
+  float m_err_eTHETA_fit{-99.};  ///< fitted parameter eTHETA err
+  float m_err_eQOP_fit{-99.};    ///< fitted parameter eQOP err
+  float m_err_eT_fit{-99.};      ///< fitted parameter eT err
+
+  int m_nPredicted{0};      ///< number of states with predicted parameter
+  std::vector<bool> m_prt;  ///< predicted status
+  std::vector<float> m_eLOC0_prt;       ///< predicted parameter eLOC0
+  std::vector<float> m_eLOC1_prt;       ///< predicted parameter eLOC1
+  std::vector<float> m_ePHI_prt;        ///< predicted parameter ePHI
+  std::vector<float> m_eTHETA_prt;      ///< predicted parameter eTHETA
+  std::vector<float> m_eQOP_prt;        ///< predicted parameter eQOP
+  std::vector<float> m_eT_prt;          ///< predicted parameter eT
+  std::vector<float> m_res_eLOC0_prt;   ///< predicted parameter eLOC0 residual
+  std::vector<float> m_res_eLOC1_prt;   ///< predicted parameter eLOC1 residual
+  std::vector<float> m_res_ePHI_prt;    ///< predicted parameter ePHI residual
+  std::vector<float> m_res_eTHETA_prt;  ///< predicted parameter eTHETA residual
+  std::vector<float> m_res_eQOP_prt;    ///< predicted parameter eQOP residual
+  std::vector<float> m_res_eT_prt;      ///< predicted parameter eT residual
+  std::vector<float> m_err_eLOC0_prt;   ///< predicted parameter eLOC0 error
+  std::vector<float> m_err_eLOC1_prt;   ///< predicted parameter eLOC1 error
+  std::vector<float> m_err_ePHI_prt;    ///< predicted parameter ePHI error
+  std::vector<float> m_err_eTHETA_prt;  ///< predicted parameter eTHETA error
+  std::vector<float> m_err_eQOP_prt;    ///< predicted parameter eQOP error
+  std::vector<float> m_err_eT_prt;      ///< predicted parameter eT error
+  std::vector<float> m_pull_eLOC0_prt;  ///< predicted parameter eLOC0 pull
+  std::vector<float> m_pull_eLOC1_prt;  ///< predicted parameter eLOC1 pull
+  std::vector<float> m_pull_ePHI_prt;   ///< predicted parameter ePHI pull
+  std::vector<float> m_pull_eTHETA_prt;  ///< predicted parameter eTHETA pull
+  std::vector<float> m_pull_eQOP_prt;    ///< predicted parameter eQOP pull
+  std::vector<float> m_pull_eT_prt;      ///< predicted parameter eT pull
+  std::vector<float> m_x_prt;            ///< predicted global x
+  std::vector<float> m_y_prt;            ///< predicted global y
+  std::vector<float> m_z_prt;            ///< predicted global z
+  std::vector<float> m_px_prt;           ///< predicted momentum px
+  std::vector<float> m_py_prt;           ///< predicted momentum py
+  std::vector<float> m_pz_prt;           ///< predicted momentum pz
+  std::vector<float> m_eta_prt;          ///< predicted momentum eta
+  std::vector<float> m_pT_prt;           ///< predicted momentum pT
+
+  int m_nFiltered{0};              ///< number of states with filtered parameter
+  std::vector<bool> m_flt;         ///< filtered status
+  std::vector<float> m_eLOC0_flt;  ///< filtered parameter eLOC0
+  std::vector<float> m_eLOC1_flt;  ///< filtered parameter eLOC1
+  std::vector<float> m_ePHI_flt;   ///< filtered parameter ePHI
+  std::vector<float> m_eTHETA_flt;       ///< filtered parameter eTHETA
+  std::vector<float> m_eQOP_flt;         ///< filtered parameter eQOP
+  std::vector<float> m_eT_flt;           ///< filtered parameter eT
+  std::vector<float> m_res_eLOC0_flt;    ///< filtered parameter eLOC0 residual
+  std::vector<float> m_res_eLOC1_flt;    ///< filtered parameter eLOC1 residual
+  std::vector<float> m_res_ePHI_flt;     ///< filtered parameter ePHI residual
+  std::vector<float> m_res_eTHETA_flt;   ///< filtered parameter eTHETA residual
+  std::vector<float> m_res_eQOP_flt;     ///< filtered parameter eQOP residual
+  std::vector<float> m_res_eT_flt;       ///< filtered parameter eT residual
+  std::vector<float> m_err_eLOC0_flt;    ///< filtered parameter eLOC0 error
+  std::vector<float> m_err_eLOC1_flt;    ///< filtered parameter eLOC1 error
+  std::vector<float> m_err_ePHI_flt;     ///< filtered parameter ePHI error
+  std::vector<float> m_err_eTHETA_flt;   ///< filtered parameter eTHETA error
+  std::vector<float> m_err_eQOP_flt;     ///< filtered parameter eQOP error
+  std::vector<float> m_err_eT_flt;       ///< filtered parameter eT error
+  std::vector<float> m_pull_eLOC0_flt;   ///< filtered parameter eLOC0 pull
+  std::vector<float> m_pull_eLOC1_flt;   ///< filtered parameter eLOC1 pull
+  std::vector<float> m_pull_ePHI_flt;    ///< filtered parameter ePHI pull
+  std::vector<float> m_pull_eTHETA_flt;  ///< filtered parameter eTHETA pull
+  std::vector<float> m_pull_eQOP_flt;    ///< filtered parameter eQOP pull
+  std::vector<float> m_pull_eT_flt;      ///< filtered parameter eT pull
+  std::vector<float> m_x_flt;            ///< filtered global x
+  std::vector<float> m_y_flt;            ///< filtered global y
+  std::vector<float> m_z_flt;            ///< filtered global z
+  std::vector<float> m_px_flt;           ///< filtered momentum px
+  std::vector<float> m_py_flt;           ///< filtered momentum py
+  std::vector<float> m_pz_flt;           ///< filtered momentum pz
+  std::vector<float> m_eta_flt;          ///< filtered momentum eta
+  std::vector<float> m_pT_flt;           ///< filtered momentum pT
+  std::vector<float> m_chi2;             ///< chisq from filtering
+
+  int m_nSmoothed{0};              ///< number of states with smoothed parameter
+  std::vector<bool> m_smt;         ///< smoothed status
+  std::vector<float> m_eLOC0_smt;  ///< smoothed parameter eLOC0
+  std::vector<float> m_eLOC1_smt;  ///< smoothed parameter eLOC1
+  std::vector<float> m_ePHI_smt;   ///< smoothed parameter ePHI
+  std::vector<float> m_eTHETA_smt;       ///< smoothed parameter eTHETA
+  std::vector<float> m_eQOP_smt;         ///< smoothed parameter eQOP
+  std::vector<float> m_eT_smt;           ///< smoothed parameter eT
+  std::vector<float> m_res_eLOC0_smt;    ///< smoothed parameter eLOC0 residual
+  std::vector<float> m_res_eLOC1_smt;    ///< smoothed parameter eLOC1 residual
+  std::vector<float> m_res_ePHI_smt;     ///< smoothed parameter ePHI residual
+  std::vector<float> m_res_eTHETA_smt;   ///< smoothed parameter eTHETA residual
+  std::vector<float> m_res_eQOP_smt;     ///< smoothed parameter eQOP residual
+  std::vector<float> m_res_eT_smt;       ///< smoothed parameter eT residual
+  std::vector<float> m_err_eLOC0_smt;    ///< smoothed parameter eLOC0 error
+  std::vector<float> m_err_eLOC1_smt;    ///< smoothed parameter eLOC1 error
+  std::vector<float> m_err_ePHI_smt;     ///< smoothed parameter ePHI error
+  std::vector<float> m_err_eTHETA_smt;   ///< smoothed parameter eTHETA error
+  std::vector<float> m_err_eQOP_smt;     ///< smoothed parameter eQOP error
+  std::vector<float> m_err_eT_smt;       ///< smoothed parameter eT error
+  std::vector<float> m_pull_eLOC0_smt;   ///< smoothed parameter eLOC0 pull
+  std::vector<float> m_pull_eLOC1_smt;   ///< smoothed parameter eLOC1 pull
+  std::vector<float> m_pull_ePHI_smt;    ///< smoothed parameter ePHI pull
+  std::vector<float> m_pull_eTHETA_smt;  ///< smoothed parameter eTHETA pull
+  std::vector<float> m_pull_eQOP_smt;    ///< smoothed parameter eQOP pull
+  std::vector<float> m_pull_eT_smt;      ///< smoothed parameter eT pull
+  std::vector<float> m_x_smt;            ///< smoothed global x
+  std::vector<float> m_y_smt;            ///< smoothed global y
+  std::vector<float> m_z_smt;            ///< smoothed global z
+  std::vector<float> m_px_smt;           ///< smoothed momentum px
+  std::vector<float> m_py_smt;           ///< smoothed momentum py
+  std::vector<float> m_pz_smt;           ///< smoothed momentum pz
+  std::vector<float> m_eta_smt;          ///< smoothed momentum eta
+  std::vector<float> m_pT_smt;           ///< smoothed momentum pT
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksReader.hpp b/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksReader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cff03c4445659f5364e6882429d82af3051536d
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksReader.hpp
@@ -0,0 +1,87 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <Acts/Propagator/MaterialInteractor.hpp>
+#include <Acts/Utilities/Definitions.hpp>
+#include <Acts/Utilities/Logger.hpp>
+#include <mutex>
+#include <vector>
+
+#include "ACTFW/Framework/IReader.hpp"
+#include "ACTFW/Framework/IService.hpp"
+#include "ACTFW/Framework/ProcessCode.hpp"
+
+class TChain;
+
+namespace FW {
+
+/// @class RootVertexAndTracksReader
+///
+/// @brief Reads in vertex and tracks information from a root file
+/// and fills it into a format to be understood by the vertexing algorithms
+class RootVertexAndTracksReader final : public IReader {
+ public:
+  /// @brief The nested configuration struct
+  struct Config {
+    std::string outputCollection = "vertexAndTracksCollection";
+    std::string treeName = "event";     ///< name of the output tree
+    std::vector<std::string> fileList;  ///< The name of the input file
+    unsigned int batchSize = 1;         ///!< Batch
+  };
+
+  /// Constructor
+  /// @param cfg The Configuration struct
+  /// @param lvl Message level declaration
+  RootVertexAndTracksReader(Config cfg, Acts::Logging::Level lvl);
+
+  /// Destructor
+  ~RootVertexAndTracksReader() final override;
+
+  /// Framework name() method
+  std::string name() const final override;
+
+  /// Return the available events range.
+  std::pair<size_t, size_t> availableEvents() const final override;
+
+  /// Read out data from the input stream
+  ///
+  /// @param context The algorithm context
+  ProcessCode read(const FW::AlgorithmContext& context) final override;
+
+ private:
+  /// The config class
+  Config m_cfg;
+  /// mutex used to protect multi-threaded reads
+  std::mutex m_read_mutex;
+  /// The number of events
+  size_t m_events = 0;
+  /// The input tree name
+  TChain* m_inputChain = nullptr;
+  int m_eventNr = 0;
+
+  std::vector<double>* m_ptrVx = new std::vector<double>;
+  std::vector<double>* m_ptrVy = new std::vector<double>;
+  std::vector<double>* m_ptrVz = new std::vector<double>;
+  std::vector<double>* m_ptrD0 = new std::vector<double>;
+  std::vector<double>* m_ptrZ0 = new std::vector<double>;
+  std::vector<double>* m_ptrPhi = new std::vector<double>;
+  std::vector<double>* m_ptrTheta = new std::vector<double>;
+  std::vector<double>* m_ptrQP = new std::vector<double>;
+  std::vector<double>* m_ptrTime = new std::vector<double>;
+  std::vector<int>* m_ptrVtxID = new std::vector<int>;
+  std::vector<std::vector<double>>* m_ptrTrkCov =
+      new std::vector<std::vector<double>>;
+
+  std::unique_ptr<const Acts::Logger> m_logger;
+
+  const Acts::Logger& logger() const { return *m_logger; }
+};
+
+}  // namespace FW
diff --git a/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksWriter.hpp b/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1fce8de69aec7a717b3ed81ccbd24a3daa2c87ae
--- /dev/null
+++ b/Io/Root/include/ACTFW/Io/Root/RootVertexAndTracksWriter.hpp
@@ -0,0 +1,183 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <mutex>
+
+#include "ACTFW/Framework/WriterT.hpp"
+#include "ACTFW/TruthTracking/VertexAndTracks.hpp"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// Write out vertices together with associated tracks into a TTree
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+///
+/// A common file can be provided for to the writer to attach his TTree,
+/// this is done by setting the Config::rootFile pointer to an existing file
+///
+/// Safe to use from multiple writer threads - uses a std::mutex lock.
+class RootVertexAndTracksWriter final
+    : public WriterT<std::vector<VertexAndTracks>> {
+ public:
+  /// @brief The nested configuration struct
+  struct Config {
+    std::string collection;             ///< particle collection to write
+    std::string filePath;               ///< path of the output file
+    std::string fileMode = "RECREATE";  ///< file access mode
+    std::string treeName = "event";     ///< name of the output tree
+    TFile* rootFile = nullptr;          ///< common root file
+  };
+
+  /// Constructor
+  ///
+  /// @param cfg Configuration struct
+  /// @param lvl Message level declaration
+  RootVertexAndTracksWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Virtual destructor
+  ~RootVertexAndTracksWriter() final override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// @brief Write method called by the base class
+  /// @param [in] context is the algorithm context for event information
+  /// @param [in] vertexAndTracksCollection is the VertexAndTracks collection
+  ProcessCode writeT(const AlgorithmContext& context,
+                     const std::vector<VertexAndTracks>&
+                         vertexAndTracksCollection) final override;
+
+ private:
+  Config m_cfg;             ///< The config class
+  std::mutex m_writeMutex;  ///< Mutex used to protect multi-threaded writes
+  TFile* m_outputFile{nullptr};  ///< The output file
+  TTree* m_outputTree{nullptr};  ///< The output tree
+  int m_eventNr{0};              ///< the event number of
+
+  /// The vertex positions
+  std::vector<double> m_vx;
+  std::vector<double> m_vy;
+  std::vector<double> m_vz;
+
+  /// The track parameter
+  std::vector<double> m_d0;
+  std::vector<double> m_z0;
+  std::vector<double> m_phi;
+  std::vector<double> m_theta;
+  std::vector<double> m_qp;
+  std::vector<double> m_time;
+  std::vector<int> m_vtxID;
+
+  /// The track covariance matrix
+  std::vector<double> m_cov11;
+  std::vector<double> m_cov12;
+  std::vector<double> m_cov13;
+  std::vector<double> m_cov14;
+  std::vector<double> m_cov15;
+  std::vector<double> m_cov16;
+
+  std::vector<double> m_cov21;
+  std::vector<double> m_cov22;
+  std::vector<double> m_cov23;
+  std::vector<double> m_cov24;
+  std::vector<double> m_cov25;
+  std::vector<double> m_cov26;
+
+  std::vector<double> m_cov31;
+  std::vector<double> m_cov32;
+  std::vector<double> m_cov33;
+  std::vector<double> m_cov34;
+  std::vector<double> m_cov35;
+  std::vector<double> m_cov36;
+
+  std::vector<double> m_cov41;
+  std::vector<double> m_cov42;
+  std::vector<double> m_cov43;
+  std::vector<double> m_cov44;
+  std::vector<double> m_cov45;
+  std::vector<double> m_cov46;
+
+  std::vector<double> m_cov51;
+  std::vector<double> m_cov52;
+  std::vector<double> m_cov53;
+  std::vector<double> m_cov54;
+  std::vector<double> m_cov55;
+  std::vector<double> m_cov56;
+
+  std::vector<double> m_cov61;
+  std::vector<double> m_cov62;
+  std::vector<double> m_cov63;
+  std::vector<double> m_cov64;
+  std::vector<double> m_cov65;
+  std::vector<double> m_cov66;
+
+  /// Pointers to the vectors
+  std::vector<double>* m_ptrVx = &m_vx;
+  std::vector<double>* m_ptrVy = &m_vy;
+  std::vector<double>* m_ptrVz = &m_vz;
+  std::vector<double>* m_ptrD0 = &m_d0;
+  std::vector<double>* m_ptrZ0 = &m_z0;
+  std::vector<double>* m_ptrPhi = &m_phi;
+  std::vector<double>* m_ptrTheta = &m_theta;
+  std::vector<double>* m_ptrQP = &m_qp;
+  std::vector<double>* m_ptrTime = &m_time;
+  std::vector<int>* m_ptrVtxID = &m_vtxID;
+
+  std::vector<double>* m_ptrCov11 = &m_cov11;
+  std::vector<double>* m_ptrCov12 = &m_cov12;
+  std::vector<double>* m_ptrCov13 = &m_cov13;
+  std::vector<double>* m_ptrCov14 = &m_cov14;
+  std::vector<double>* m_ptrCov15 = &m_cov15;
+  std::vector<double>* m_ptrCov16 = &m_cov16;
+
+  std::vector<double>* m_ptrCov21 = &m_cov21;
+  std::vector<double>* m_ptrCov22 = &m_cov22;
+  std::vector<double>* m_ptrCov23 = &m_cov23;
+  std::vector<double>* m_ptrCov24 = &m_cov24;
+  std::vector<double>* m_ptrCov25 = &m_cov25;
+  std::vector<double>* m_ptrCov26 = &m_cov26;
+
+  std::vector<double>* m_ptrCov31 = &m_cov31;
+  std::vector<double>* m_ptrCov32 = &m_cov32;
+  std::vector<double>* m_ptrCov33 = &m_cov33;
+  std::vector<double>* m_ptrCov34 = &m_cov34;
+  std::vector<double>* m_ptrCov35 = &m_cov35;
+  std::vector<double>* m_ptrCov36 = &m_cov36;
+
+  std::vector<double>* m_ptrCov41 = &m_cov41;
+  std::vector<double>* m_ptrCov42 = &m_cov42;
+  std::vector<double>* m_ptrCov43 = &m_cov43;
+  std::vector<double>* m_ptrCov44 = &m_cov44;
+  std::vector<double>* m_ptrCov45 = &m_cov45;
+  std::vector<double>* m_ptrCov46 = &m_cov46;
+
+  std::vector<double>* m_ptrCov51 = &m_cov51;
+  std::vector<double>* m_ptrCov52 = &m_cov52;
+  std::vector<double>* m_ptrCov53 = &m_cov53;
+  std::vector<double>* m_ptrCov54 = &m_cov54;
+  std::vector<double>* m_ptrCov55 = &m_cov55;
+  std::vector<double>* m_ptrCov56 = &m_cov56;
+
+  std::vector<double>* m_ptrCov61 = &m_cov61;
+  std::vector<double>* m_ptrCov62 = &m_cov62;
+  std::vector<double>* m_ptrCov63 = &m_cov63;
+  std::vector<double>* m_ptrCov64 = &m_cov64;
+  std::vector<double>* m_ptrCov65 = &m_cov65;
+  std::vector<double>* m_ptrCov66 = &m_cov66;
+
+  /// @brief Clears all vectors
+  void ClearAll();
+};
+
+}  // namespace FW
diff --git a/Io/Root/src/RootMaterialDecorator.cpp b/Io/Root/src/RootMaterialDecorator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..49362889fb8d8305132c6062421eb0735fa8d0ce
--- /dev/null
+++ b/Io/Root/src/RootMaterialDecorator.cpp
@@ -0,0 +1,194 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootMaterialDecorator.hpp"
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Material/BinnedSurfaceMaterial.hpp>
+#include <Acts/Material/HomogeneousSurfaceMaterial.hpp>
+#include <Acts/Utilities/BinUtility.hpp>
+#include <Acts/Utilities/BinningType.hpp>
+#include <TFile.h>
+#include <TH2F.h>
+#include <TIterator.h>
+#include <TKey.h>
+#include <TList.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/finder.hpp>
+#include <boost/algorithm/string/iter_find.hpp>
+#include <cstdio>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+FW::RootMaterialDecorator::RootMaterialDecorator(
+    const FW::RootMaterialDecorator::Config& cfg)
+    : m_cfg(cfg), m_inputFile(nullptr) {
+  // Validate the configuration
+  if (m_cfg.folderNameBase.empty()) {
+    throw std::invalid_argument("Missing ROOT folder name");
+  } else if (m_cfg.fileName.empty()) {
+    throw std::invalid_argument("Missing file name");
+  } else if (!m_cfg.logger) {
+    throw std::invalid_argument("Missing logger");
+  } else if (m_cfg.name.empty()) {
+    throw std::invalid_argument("Missing service name");
+  }
+
+  // Setup ROOT I/O
+  m_inputFile = TFile::Open(m_cfg.fileName.c_str());
+  if (!m_inputFile) {
+    throw std::ios_base::failure("Could not open '" + m_cfg.fileName);
+  }
+
+  // Get the list of keys from the file
+  TList* tlist = m_inputFile->GetListOfKeys();
+  auto tIter = tlist->MakeIterator();
+  tIter->Reset();
+
+  // Iterate over the keys in the file
+  while (TKey* key = (TKey*)(tIter->Next())) {
+    // The surface material to be read in for this
+    std::shared_ptr<const Acts::ISurfaceMaterial> sMaterial = nullptr;
+
+    // Remember the directory
+    std::string tdName(key->GetName());
+
+    ACTS_VERBOSE("Processing directory: " << tdName);
+
+    // volume
+    std::vector<std::string> splitNames;
+    iter_split(splitNames, tdName,
+               boost::algorithm::first_finder(m_cfg.voltag));
+    boost::split(splitNames, splitNames[1], boost::is_any_of("_"));
+    Acts::GeometryID::Value volID = std::stoi(splitNames[0]);
+    // boundary
+    iter_split(splitNames, tdName,
+               boost::algorithm::first_finder(m_cfg.boutag));
+    boost::split(splitNames, splitNames[1], boost::is_any_of("_"));
+    Acts::GeometryID::Value bouID = std::stoi(splitNames[0]);
+    // layer
+    iter_split(splitNames, tdName,
+               boost::algorithm::first_finder(m_cfg.laytag));
+    boost::split(splitNames, splitNames[1], boost::is_any_of("_"));
+    Acts::GeometryID::Value layID = std::stoi(splitNames[0]);
+    // approach
+    iter_split(splitNames, tdName,
+               boost::algorithm::first_finder(m_cfg.apptag));
+    boost::split(splitNames, splitNames[1], boost::is_any_of("_"));
+    Acts::GeometryID::Value appID = std::stoi(splitNames[0]);
+    // sensitive
+    iter_split(splitNames, tdName,
+               boost::algorithm::first_finder(m_cfg.sentag));
+    Acts::GeometryID::Value senID = std::stoi(splitNames[1]);
+
+    // Reconstruct the geometry ID
+    Acts::GeometryID geoID;
+    geoID.setVolume(volID);
+    geoID.setBoundary(bouID);
+    geoID.setLayer(layID);
+    geoID.setApproach(appID);
+    geoID.setSensitive(senID);
+    ACTS_VERBOSE("GeometryID re-constructed as " << geoID);
+
+    // Construct the names
+    std::string nName = tdName + "/" + m_cfg.ntag;
+    std::string vName = tdName + "/" + m_cfg.vtag;
+    std::string oName = tdName + "/" + m_cfg.otag;
+    std::string minName = tdName + "/" + m_cfg.mintag;
+    std::string maxName = tdName + "/" + m_cfg.maxtag;
+    std::string tName = tdName + "/" + m_cfg.ttag;
+    std::string x0Name = tdName + "/" + m_cfg.x0tag;
+    std::string l0Name = tdName + "/" + m_cfg.l0tag;
+    std::string aName = tdName + "/" + m_cfg.atag;
+    std::string zName = tdName + "/" + m_cfg.ztag;
+    std::string rhoName = tdName + "/" + m_cfg.rhotag;
+
+    // Get the histograms
+    TH1F* n = dynamic_cast<TH1F*>(m_inputFile->Get(nName.c_str()));
+    TH1F* v = dynamic_cast<TH1F*>(m_inputFile->Get(vName.c_str()));
+    TH1F* o = dynamic_cast<TH1F*>(m_inputFile->Get(oName.c_str()));
+    TH1F* min = dynamic_cast<TH1F*>(m_inputFile->Get(minName.c_str()));
+    TH1F* max = dynamic_cast<TH1F*>(m_inputFile->Get(maxName.c_str()));
+    TH2F* t = dynamic_cast<TH2F*>(m_inputFile->Get(tName.c_str()));
+    TH2F* x0 = dynamic_cast<TH2F*>(m_inputFile->Get(x0Name.c_str()));
+    TH2F* l0 = dynamic_cast<TH2F*>(m_inputFile->Get(l0Name.c_str()));
+    TH2F* A = dynamic_cast<TH2F*>(m_inputFile->Get(aName.c_str()));
+    TH2F* Z = dynamic_cast<TH2F*>(m_inputFile->Get(zName.c_str()));
+    TH2F* rho = dynamic_cast<TH2F*>(m_inputFile->Get(rhoName.c_str()));
+
+    // Only go on when you have all histograms
+    if (n and v and o and min and max and t and x0 and l0 and A and Z and rho) {
+      // Get the number of bins
+      int nbins0 = t->GetNbinsX();
+      int nbins1 = t->GetNbinsY();
+
+      // The material matrix
+      Acts::MaterialPropertiesMatrix materialMatrix(
+          nbins1,
+          Acts::MaterialPropertiesVector(nbins0, Acts::MaterialProperties()));
+
+      // We need binned material properties
+      if (nbins0 * nbins1 > 1) {
+        // Fill the matrix first
+        for (int ib0 = 1; ib0 <= nbins0; ++ib0) {
+          for (int ib1 = 1; ib1 <= nbins1; ++ib1) {
+            double dt = t->GetBinContent(ib0, ib1);
+            if (dt > 0.) {
+              double dx0 = x0->GetBinContent(ib0, ib1);
+              double dl0 = l0->GetBinContent(ib0, ib1);
+              double da = A->GetBinContent(ib0, ib1);
+              double dz = Z->GetBinContent(ib0, ib1);
+              double drho = rho->GetBinContent(ib0, ib1);
+              // Create material properties
+              materialMatrix[ib1 - 1][ib0 - 1] =
+                  Acts::MaterialProperties(dx0, dl0, da, dz, drho, dt);
+            }
+          }
+        }
+
+        // Now reconstruct the bin untilities
+        Acts::BinUtility bUtility;
+        for (int ib = 1; ib < n->GetNbinsX() + 1; ++ib) {
+          size_t nbins = size_t(n->GetBinContent(ib));
+          Acts::BinningValue val = Acts::BinningValue(v->GetBinContent(ib));
+          Acts::BinningOption opt = Acts::BinningOption(o->GetBinContent(ib));
+          float rmin = min->GetBinContent(ib);
+          float rmax = max->GetBinContent(ib);
+          bUtility += Acts::BinUtility(nbins, rmin, rmax, opt, val);
+        }
+        ACTS_VERBOSE("Created " << bUtility);
+
+        // Construct the binned material with the right bin utility
+        sMaterial = std::make_shared<const Acts::BinnedSurfaceMaterial>(
+            bUtility, std::move(materialMatrix));
+
+      } else {
+        // Only homogeneous material present
+        double dt = t->GetBinContent(1, 1);
+        double dx0 = x0->GetBinContent(1, 1);
+        double dl0 = l0->GetBinContent(1, 1);
+        double da = A->GetBinContent(1, 1);
+        double dz = Z->GetBinContent(1, 1);
+        double drho = rho->GetBinContent(1, 1);
+        // Create and set the homogenous surface material
+        sMaterial = std::make_shared<const Acts::HomogeneousSurfaceMaterial>(
+            Acts::MaterialProperties(dx0, dl0, da, dz, drho, dt));
+      }
+    }
+    ACTS_VERBOSE("Successfully read Material for : " << geoID);
+
+    // Insert into the new collection
+    m_surfaceMaterialMap.insert({geoID, std::move(sMaterial)});
+  }
+}
+
+FW::RootMaterialDecorator::~RootMaterialDecorator() {
+  m_inputFile->Close();
+}
diff --git a/Io/Root/src/RootMaterialTrackReader.cpp b/Io/Root/src/RootMaterialTrackReader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..03b3c400b6b6a9a696f82057ab83d4e91d6af455
--- /dev/null
+++ b/Io/Root/src/RootMaterialTrackReader.cpp
@@ -0,0 +1,134 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootMaterialTrackReader.hpp"
+
+#include <TChain.h>
+#include <TFile.h>
+#include <iostream>
+
+#include "ACTFW/Framework/WhiteBoard.hpp"
+
+FW::RootMaterialTrackReader::RootMaterialTrackReader(
+    const FW::RootMaterialTrackReader::Config& cfg)
+    : FW::IReader(), m_cfg(cfg), m_events(0), m_inputChain(nullptr) {
+  m_inputChain = new TChain(m_cfg.treeName.c_str());
+
+  // Set the branches
+  m_inputChain->SetBranchAddress("v_x", &m_v_x);
+  m_inputChain->SetBranchAddress("v_y", &m_v_y);
+  m_inputChain->SetBranchAddress("v_z", &m_v_z);
+  m_inputChain->SetBranchAddress("v_px", &m_v_px);
+  m_inputChain->SetBranchAddress("v_py", &m_v_py);
+  m_inputChain->SetBranchAddress("v_pz", &m_v_pz);
+  m_inputChain->SetBranchAddress("v_phi", &m_v_phi);
+  m_inputChain->SetBranchAddress("v_eta", &m_v_eta);
+  m_inputChain->SetBranchAddress("t_X0", &m_tX0);
+  m_inputChain->SetBranchAddress("t_L0", &m_tL0);
+  m_inputChain->SetBranchAddress("mat_x", &m_step_x);
+  m_inputChain->SetBranchAddress("mat_y", &m_step_y);
+  m_inputChain->SetBranchAddress("mat_z", &m_step_z);
+  m_inputChain->SetBranchAddress("mat_dx", &m_step_dx);
+  m_inputChain->SetBranchAddress("mat_dy", &m_step_dy);
+  m_inputChain->SetBranchAddress("mat_dz", &m_step_dz);
+  m_inputChain->SetBranchAddress("mat_step_length", &m_step_length);
+  m_inputChain->SetBranchAddress("mat_X0", &m_step_X0);
+  m_inputChain->SetBranchAddress("mat_L0", &m_step_L0);
+  m_inputChain->SetBranchAddress("mat_A", &m_step_A);
+  m_inputChain->SetBranchAddress("mat_Z", &m_step_Z);
+  m_inputChain->SetBranchAddress("mat_rho", &m_step_rho);
+
+  // loop over the input files
+  for (auto inputFile : m_cfg.fileList) {
+    // add file to the input chain
+    m_inputChain->Add(inputFile.c_str());
+    ACTS_DEBUG("Adding File " << inputFile << " to tree '" << m_cfg.treeName
+                              << "'.");
+  }
+
+  m_events = m_inputChain->GetEntries();
+  ACTS_DEBUG("The full chain has " << m_events << " entries.");
+}
+
+FW::RootMaterialTrackReader::~RootMaterialTrackReader() {
+  delete m_step_x;
+  delete m_step_y;
+  delete m_step_z;
+  delete m_step_length;
+  delete m_step_X0;
+  delete m_step_L0;
+  delete m_step_A;
+  delete m_step_Z;
+  delete m_step_rho;
+}
+
+std::string FW::RootMaterialTrackReader::name() const {
+  return m_cfg.name;
+}
+
+std::pair<size_t, size_t> FW::RootMaterialTrackReader::availableEvents() const {
+  return {0u, m_events};
+}
+
+FW::ProcessCode FW::RootMaterialTrackReader::read(
+    const FW::AlgorithmContext& context) {
+  ACTS_DEBUG("Trying to read recorded material from tracks.");
+  // read in the material track
+  if (m_inputChain && context.eventNumber < m_events) {
+    // lock the mutex
+    std::lock_guard<std::mutex> lock(m_read_mutex);
+    // now read
+
+    // The collection to be written
+    std::vector<Acts::RecordedMaterialTrack> mtrackCollection;
+
+    for (size_t ib = 0; ib < m_cfg.batchSize; ++ib) {
+      // Read the correct entry: batch size * event_number + ib
+      m_inputChain->GetEntry(m_cfg.batchSize * context.eventNumber + ib);
+      ACTS_VERBOSE("Reading entry: " << m_cfg.batchSize * context.eventNumber +
+                                            ib);
+
+      Acts::RecordedMaterialTrack rmTrack;
+      // Fill the position and momentum
+      rmTrack.first.first = Acts::Vector3D(m_v_x, m_v_y, m_v_z);
+      rmTrack.first.second = Acts::Vector3D(m_v_px, m_v_py, m_v_pz);
+
+      // Fill the individual steps
+      size_t msteps = m_step_length->size();
+      ACTS_VERBOSE("Reading " << msteps << " material steps.");
+      rmTrack.second.materialInteractions.reserve(msteps);
+      rmTrack.second.materialInX0 = 0.;
+      rmTrack.second.materialInL0 = 0.;
+
+      for (size_t is = 0; is < msteps; ++is) {
+        double mX0 = (*m_step_X0)[is];
+        double mL0 = (*m_step_L0)[is];
+        double s = (*m_step_length)[is];
+
+        rmTrack.second.materialInX0 += s / mX0;
+        rmTrack.second.materialInL0 += s / mL0;
+
+        /// Fill the position & the material
+        Acts::MaterialInteraction mInteraction;
+        mInteraction.position =
+            Acts::Vector3D((*m_step_x)[is], (*m_step_y)[is], (*m_step_z)[is]);
+        mInteraction.direction = Acts::Vector3D(
+            (*m_step_dx)[is], (*m_step_dy)[is], (*m_step_dz)[is]);
+        mInteraction.materialProperties = Acts::MaterialProperties(
+            mX0, mL0, (*m_step_A)[is], (*m_step_Z)[is], (*m_step_rho)[is], s);
+        rmTrack.second.materialInteractions.push_back(std::move(mInteraction));
+      }
+      mtrackCollection.push_back(std::move(rmTrack));
+    }
+
+    // Write to the collection to the EventStore
+    context.eventStore.add(m_cfg.collection, std::move(mtrackCollection));
+  }
+  // Return success flag
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootMaterialTrackWriter.cpp b/Io/Root/src/RootMaterialTrackWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4b78744461ed429a749ae14efd1e576235806cd4
--- /dev/null
+++ b/Io/Root/src/RootMaterialTrackWriter.cpp
@@ -0,0 +1,280 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootMaterialTrackWriter.hpp"
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Surfaces/CylinderBounds.hpp>
+#include <Acts/Surfaces/RadialBounds.hpp>
+#include <Acts/Utilities/Helpers.hpp>
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <iostream>
+#include <stdexcept>
+
+using Acts::VectorHelpers::eta;
+using Acts::VectorHelpers::perp;
+using Acts::VectorHelpers::phi;
+
+FW::RootMaterialTrackWriter::RootMaterialTrackWriter(
+    const FW::RootMaterialTrackWriter::Config& cfg, Acts::Logging::Level level)
+    : WriterT(cfg.collection, "RootMaterialTrackWriter", level),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // An input collection name and tree name must be specified
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  } else if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + m_cfg.filePath);
+    }
+  }
+  m_outputFile->cd();
+  m_outputTree =
+      new TTree(m_cfg.treeName.c_str(), "TTree from RootMaterialTrackWriter");
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+
+  // Set the branches
+  m_outputTree->Branch("v_x", &m_v_x);
+  m_outputTree->Branch("v_y", &m_v_y);
+  m_outputTree->Branch("v_z", &m_v_z);
+  m_outputTree->Branch("v_px", &m_v_px);
+  m_outputTree->Branch("v_py", &m_v_py);
+  m_outputTree->Branch("v_pz", &m_v_pz);
+  m_outputTree->Branch("v_phi", &m_v_phi);
+  m_outputTree->Branch("v_eta", &m_v_eta);
+  m_outputTree->Branch("t_X0", &m_tX0);
+  m_outputTree->Branch("t_L0", &m_tL0);
+  m_outputTree->Branch("mat_x", &m_step_x);
+  m_outputTree->Branch("mat_y", &m_step_y);
+  m_outputTree->Branch("mat_z", &m_step_z);
+  m_outputTree->Branch("mat_dx", &m_step_dx);
+  m_outputTree->Branch("mat_dy", &m_step_dy);
+  m_outputTree->Branch("mat_dz", &m_step_dz);
+  m_outputTree->Branch("mat_step_length", &m_step_length);
+  m_outputTree->Branch("mat_X0", &m_step_X0);
+  m_outputTree->Branch("mat_L0", &m_step_L0);
+  m_outputTree->Branch("mat_A", &m_step_A);
+  m_outputTree->Branch("mat_Z", &m_step_Z);
+  m_outputTree->Branch("mat_rho", &m_step_rho);
+
+  if (m_cfg.prePostStep) {
+    m_outputTree->Branch("mat_sx", &m_step_sx);
+    m_outputTree->Branch("mat_sy", &m_step_sy);
+    m_outputTree->Branch("mat_sz", &m_step_sz);
+    m_outputTree->Branch("mat_ex", &m_step_ex);
+    m_outputTree->Branch("mat_ey", &m_step_ey);
+    m_outputTree->Branch("mat_ez", &m_step_ez);
+  }
+  if (m_cfg.storesurface) {
+    m_outputTree->Branch("sur_id", &m_sur_id);
+    m_outputTree->Branch("sur_type", &m_sur_type);
+    m_outputTree->Branch("sur_x", &m_sur_x);
+    m_outputTree->Branch("sur_y", &m_sur_y);
+    m_outputTree->Branch("sur_z", &m_sur_z);
+    m_outputTree->Branch("sur_range_min", &m_sur_range_min);
+    m_outputTree->Branch("sur_range_max", &m_sur_range_max);
+  }
+}
+
+FW::RootMaterialTrackWriter::~RootMaterialTrackWriter() {
+  m_outputFile->Close();
+}
+
+FW::ProcessCode FW::RootMaterialTrackWriter::endRun() {
+  // write the tree and close the file
+  ACTS_INFO("Writing ROOT output File : " << m_cfg.filePath);
+  m_outputFile->cd();
+  m_outputTree->Write();
+  return FW::ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootMaterialTrackWriter::writeT(
+    const AlgorithmContext& ctx,
+    const std::vector<Acts::RecordedMaterialTrack>& materialTracks) {
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Loop over the material tracks and write them out
+  for (auto& mtrack : materialTracks) {
+    // Clearing the vector first
+    m_step_sx.clear();
+    m_step_sy.clear();
+    m_step_sz.clear();
+    m_step_x.clear();
+    m_step_y.clear();
+    m_step_z.clear();
+    m_step_ex.clear();
+    m_step_ey.clear();
+    m_step_ez.clear();
+    m_step_dx.clear();
+    m_step_dy.clear();
+    m_step_dz.clear();
+    m_step_length.clear();
+    m_step_X0.clear();
+    m_step_L0.clear();
+    m_step_A.clear();
+    m_step_Z.clear();
+    m_step_rho.clear();
+
+    m_sur_id.clear();
+    m_sur_type.clear();
+    m_sur_x.clear();
+    m_sur_y.clear();
+    m_sur_z.clear();
+    m_sur_range_min.clear();
+    m_sur_range_max.clear();
+
+    // Reserve the vector then
+    size_t mints = mtrack.second.materialInteractions.size();
+    m_step_sx.reserve(mints);
+    m_step_sy.reserve(mints);
+    m_step_sz.reserve(mints);
+    m_step_x.reserve(mints);
+    m_step_y.reserve(mints);
+    m_step_z.reserve(mints);
+    m_step_ex.reserve(mints);
+    m_step_ey.reserve(mints);
+    m_step_ez.reserve(mints);
+    m_step_dx.reserve(mints);
+    m_step_dy.reserve(mints);
+    m_step_dz.reserve(mints);
+    m_step_length.reserve(mints);
+    m_step_X0.reserve(mints);
+    m_step_L0.reserve(mints);
+    m_step_A.reserve(mints);
+    m_step_Z.reserve(mints);
+    m_step_rho.reserve(mints);
+
+    m_sur_id.reserve(mints);
+    m_sur_type.reserve(mints);
+    m_sur_x.reserve(mints);
+    m_sur_y.reserve(mints);
+    m_sur_z.reserve(mints);
+    m_sur_range_min.reserve(mints);
+    m_sur_range_max.reserve(mints);
+
+    // reset the global counter
+    if (m_cfg.recalculateTotals) {
+      m_tX0 = 0.;
+      m_tL0 = 0.;
+    } else {
+      m_tX0 = mtrack.second.materialInX0;
+      m_tL0 = mtrack.second.materialInL0;
+    }
+
+    // set the track information at vertex
+    m_v_x = mtrack.first.first.x();
+    m_v_y = mtrack.first.first.y();
+    m_v_z = mtrack.first.first.z();
+    m_v_px = mtrack.first.second.x();
+    m_v_py = mtrack.first.second.y();
+    m_v_pz = mtrack.first.second.z();
+    m_v_phi = phi(mtrack.first.second);
+    m_v_eta = eta(mtrack.first.second);
+
+    // an now loop over the material
+    for (auto& mint : mtrack.second.materialInteractions) {
+      // The material step position information
+      m_step_x.push_back(mint.position.x());
+      m_step_y.push_back(mint.position.y());
+      m_step_z.push_back(mint.position.z());
+      m_step_dx.push_back(mint.direction.x());
+      m_step_dy.push_back(mint.direction.y());
+      m_step_dz.push_back(mint.direction.z());
+
+      if (m_cfg.prePostStep) {
+        Acts::Vector3D prePos =
+            mint.position - 0.5 * mint.pathCorrection * mint.direction;
+        Acts::Vector3D posPos =
+            mint.position + 0.5 * mint.pathCorrection * mint.direction;
+        m_step_sx.push_back(prePos.x());
+        m_step_sy.push_back(prePos.y());
+        m_step_sz.push_back(prePos.z());
+        m_step_ex.push_back(posPos.x());
+        m_step_ey.push_back(posPos.y());
+        m_step_ez.push_back(posPos.z());
+      }
+
+      if (m_cfg.storesurface) {
+        const Acts::Surface* surface = mint.surface;
+        Acts::GeometryID layerID;
+        if (surface) {
+          Acts::Intersection intersection = surface->intersectionEstimate(
+              ctx.geoContext, mint.position, mint.direction, true);
+          layerID = surface->geoID();
+          m_sur_id.push_back(layerID.value());
+          m_sur_type.push_back(surface->type());
+          m_sur_x.push_back(intersection.position.x());
+          m_sur_y.push_back(intersection.position.y());
+          m_sur_z.push_back(intersection.position.z());
+
+          const Acts::SurfaceBounds& surfaceBounds = surface->bounds();
+          const Acts::RadialBounds* radialBounds =
+              dynamic_cast<const Acts::RadialBounds*>(&surfaceBounds);
+          const Acts::CylinderBounds* cylinderBounds =
+              dynamic_cast<const Acts::CylinderBounds*>(&surfaceBounds);
+
+          if (radialBounds) {
+            m_sur_range_min.push_back(radialBounds->rMin());
+            m_sur_range_max.push_back(radialBounds->rMax());
+          } else if (cylinderBounds) {
+            m_sur_range_min.push_back(
+                -cylinderBounds->get(Acts::CylinderBounds::eHalfLengthZ));
+            m_sur_range_max.push_back(
+                cylinderBounds->get(Acts::CylinderBounds::eHalfLengthZ));
+          } else {
+            m_sur_range_min.push_back(0);
+            m_sur_range_max.push_back(0);
+          }
+        } else {
+          layerID.setVolume(0);
+          layerID.setBoundary(0);
+          layerID.setLayer(0);
+          layerID.setApproach(0);
+          layerID.setSensitive(0);
+          m_sur_id.push_back(layerID.value());
+          m_sur_type.push_back(-1);
+
+          m_sur_x.push_back(0);
+          m_sur_y.push_back(0);
+          m_sur_z.push_back(0);
+          m_sur_range_min.push_back(0);
+          m_sur_range_max.push_back(0);
+        }
+      }
+
+      // the material information
+      const auto& mprops = mint.materialProperties;
+      m_step_length.push_back(mprops.thickness());
+      m_step_X0.push_back(mprops.material().X0());
+      m_step_L0.push_back(mprops.material().L0());
+      m_step_A.push_back(mprops.material().Ar());
+      m_step_Z.push_back(mprops.material().Z());
+      m_step_rho.push_back(mprops.material().massDensity());
+      // re-calculate if defined to do so
+      if (m_cfg.recalculateTotals) {
+        m_tX0 += mprops.thicknessInX0();
+        m_tL0 += mprops.thicknessInL0();
+      }
+    }
+    // write to
+    m_outputTree->Fill();
+  }
+
+  // return success
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootMaterialWriter.cpp b/Io/Root/src/RootMaterialWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9aca015c8e7cb5d0774ead5c0607133f43296dbd
--- /dev/null
+++ b/Io/Root/src/RootMaterialWriter.cpp
@@ -0,0 +1,235 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootMaterialWriter.hpp"
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Material/BinnedSurfaceMaterial.hpp>
+#include <TFile.h>
+#include <TH2F.h>
+#include <ios>
+#include <iostream>
+#include <stdexcept>
+
+FW::RootMaterialWriter::RootMaterialWriter(
+    const FW::RootMaterialWriter::Config& cfg)
+    : m_cfg(cfg) {
+  // Validate the configuration
+  if (m_cfg.folderNameBase.empty()) {
+    throw std::invalid_argument("Missing folder name base");
+  } else if (m_cfg.fileName.empty()) {
+    throw std::invalid_argument("Missing file name");
+  } else if (!m_cfg.logger) {
+    throw std::invalid_argument("Missing logger");
+  } else if (m_cfg.name.empty()) {
+    throw std::invalid_argument("Missing service name");
+  }
+}
+
+void FW::RootMaterialWriter::write(
+    const Acts::DetectorMaterialMaps& detMaterial) {
+  // Setup ROOT I/O
+  TFile* outputFile = TFile::Open(m_cfg.fileName.c_str(), "recreate");
+  if (!outputFile) {
+    throw std::ios_base::failure("Could not open '" + m_cfg.fileName);
+  }
+
+  // Change to the output file
+  outputFile->cd();
+
+  auto& surfaceMaps = detMaterial.first;
+  for (auto& [key, value] : surfaceMaps) {
+    // Get the Surface material
+    const Acts::ISurfaceMaterial* sMaterial = value.get();
+
+    // get the geometry ID
+    Acts::GeometryID geoID = key;
+    // decode the geometryID
+    const auto gvolID = geoID.volume();
+    const auto gbouID = geoID.boundary();
+    const auto glayID = geoID.layer();
+    const auto gappID = geoID.approach();
+    const auto gsenID = geoID.sensitive();
+    // create the directory
+    std::string tdName = m_cfg.folderNameBase.c_str();
+    tdName += m_cfg.voltag + std::to_string(gvolID);
+    tdName += m_cfg.boutag + std::to_string(gbouID);
+    tdName += m_cfg.laytag + std::to_string(glayID);
+    tdName += m_cfg.apptag + std::to_string(gappID);
+    tdName += m_cfg.sentag + std::to_string(gsenID);
+    // create a new directory
+    outputFile->mkdir(tdName.c_str());
+    outputFile->cd(tdName.c_str());
+
+    ACTS_VERBOSE("Writing out map at " << tdName);
+
+    size_t bins0 = 1, bins1 = 1;
+    // understand what sort of material you have in mind
+    const Acts::BinnedSurfaceMaterial* bsm =
+        dynamic_cast<const Acts::BinnedSurfaceMaterial*>(sMaterial);
+    if (bsm) {
+      // overwrite the bin numbers
+      bins0 = bsm->binUtility().bins(0);
+      bins1 = bsm->binUtility().bins(1);
+
+      // Get the binning data
+      auto& binningData = bsm->binUtility().binningData();
+      // 1-D or 2-D maps
+      size_t binningBins = binningData.size();
+
+      // The bin number information
+      TH1F* n = new TH1F(m_cfg.ntag.c_str(), "bins; bin", binningBins, -0.5,
+                         binningBins - 0.5);
+
+      // The binning value information
+      TH1F* v = new TH1F(m_cfg.vtag.c_str(), "binning values; bin", binningBins,
+                         -0.5, binningBins - 0.5);
+
+      // The binning option information
+      TH1F* o = new TH1F(m_cfg.otag.c_str(), "binning options; bin",
+                         binningBins, -0.5, binningBins - 0.5);
+
+      // The binning option information
+      TH1F* min = new TH1F(m_cfg.mintag.c_str(), "min; bin", binningBins, -0.5,
+                           binningBins - 0.5);
+
+      // The binning option information
+      TH1F* max = new TH1F(m_cfg.maxtag.c_str(), "max; bin", binningBins, -0.5,
+                           binningBins - 0.5);
+
+      // Now fill the histogram content
+      size_t b = 1;
+      for (auto bData : binningData) {
+        // Fill: nbins, value, option, min, max
+        n->SetBinContent(b, int(binningData[b - 1].bins()));
+        v->SetBinContent(b, int(binningData[b - 1].binvalue));
+        o->SetBinContent(b, int(binningData[b - 1].option));
+        min->SetBinContent(b, binningData[b - 1].min);
+        max->SetBinContent(b, binningData[b - 1].max);
+        ++b;
+      }
+      n->Write();
+      v->Write();
+      o->Write();
+      min->Write();
+      max->Write();
+    }
+
+    TH2F* t = new TH2F(m_cfg.ttag.c_str(), "thickness [mm] ;b0 ;b1", bins0,
+                       -0.5, bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+    TH2F* x0 = new TH2F(m_cfg.x0tag.c_str(), "X_{0} [mm] ;b0 ;b1", bins0, -0.5,
+                        bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+    TH2F* l0 = new TH2F(m_cfg.l0tag.c_str(), "#Lambda_{0} [mm] ;b0 ;b1", bins0,
+                        -0.5, bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+    TH2F* A = new TH2F(m_cfg.atag.c_str(), "X_{0} [mm] ;b0 ;b1", bins0, -0.5,
+                       bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+    TH2F* Z = new TH2F(m_cfg.ztag.c_str(), "#Lambda_{0} [mm] ;b0 ;b1", bins0,
+                       -0.5, bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+    TH2F* rho = new TH2F(m_cfg.rhotag.c_str(), "#rho [g/mm^3] ;b0 ;b1", bins0,
+                         -0.5, bins0 - 0.5, bins1, -0.5, bins1 - 0.5);
+
+    // loop over the material and fill
+    for (size_t b0 = 0; b0 < bins0; ++b0) {
+      for (size_t b1 = 0; b1 < bins1; ++b1) {
+        // get the material for the bin
+        auto& mat = sMaterial->materialProperties(b0, b1);
+        if (mat) {
+          t->SetBinContent(b0 + 1, b1 + 1, mat.thickness());
+          x0->SetBinContent(b0 + 1, b1 + 1, mat.material().X0());
+          l0->SetBinContent(b0 + 1, b1 + 1, mat.material().L0());
+          A->SetBinContent(b0 + 1, b1 + 1, mat.material().Ar());
+          Z->SetBinContent(b0 + 1, b1 + 1, mat.material().Z());
+          rho->SetBinContent(b0 + 1, b1 + 1, mat.material().massDensity());
+        }
+      }
+    }
+    t->Write();
+    x0->Write();
+    l0->Write();
+    A->Write();
+    Z->Write();
+    rho->Write();
+  }
+
+  outputFile->Close();
+}
+
+void FW::RootMaterialWriter::write(const Acts::TrackingGeometry& tGeometry) {
+  // Create a detector material map and loop recursively through it
+  Acts::DetectorMaterialMaps detMatMap;
+  auto hVolume = tGeometry.highestTrackingVolume();
+  if (hVolume != nullptr) {
+    collectMaterial(*hVolume, detMatMap);
+  }
+  // Write the resulting map to the file
+  write(detMatMap);
+}
+
+void FW::RootMaterialWriter::collectMaterial(
+    const Acts::TrackingVolume& tVolume,
+    Acts::DetectorMaterialMaps& detMatMap) {
+  // If the volume has volume material, write that
+  if (tVolume.volumeMaterialSharedPtr() != nullptr and m_cfg.processVolumes) {
+    detMatMap.second[tVolume.geoID()] = tVolume.volumeMaterialSharedPtr();
+  }
+
+  // If confined layers exist, loop over them and collect the layer material
+  if (tVolume.confinedLayers() != nullptr) {
+    for (auto& lay : tVolume.confinedLayers()->arrayObjects()) {
+      collectMaterial(*lay, detMatMap);
+    }
+  }
+
+  // If any of the boundary surfaces has material collect that
+  if (m_cfg.processBoundaries) {
+    for (auto& bou : tVolume.boundarySurfaces()) {
+      const auto& bSurface = bou->surfaceRepresentation();
+      if (bSurface.surfaceMaterialSharedPtr() != nullptr) {
+        detMatMap.first[bSurface.geoID()] = bSurface.surfaceMaterialSharedPtr();
+      }
+    }
+  }
+
+  // If the volume has sub volumes, step down
+  if (tVolume.confinedVolumes() != nullptr) {
+    for (auto& tvol : tVolume.confinedVolumes()->arrayObjects()) {
+      collectMaterial(*tvol, detMatMap);
+    }
+  }
+}
+
+void FW::RootMaterialWriter::collectMaterial(
+    const Acts::Layer& tLayer, Acts::DetectorMaterialMaps& detMatMap) {
+  // If the representing surface has material, collect it
+  const auto& rSurface = tLayer.surfaceRepresentation();
+  if (rSurface.surfaceMaterialSharedPtr() != nullptr and
+      m_cfg.processRepresenting) {
+    detMatMap.first[rSurface.geoID()] = rSurface.surfaceMaterialSharedPtr();
+  }
+
+  // Check the approach surfaces
+  if (tLayer.approachDescriptor() != nullptr and m_cfg.processApproaches) {
+    for (auto& aSurface : tLayer.approachDescriptor()->containedSurfaces()) {
+      if (aSurface->surfaceMaterialSharedPtr() != nullptr) {
+        detMatMap.first[aSurface->geoID()] =
+            aSurface->surfaceMaterialSharedPtr();
+      }
+    }
+  }
+
+  // Check the sensitive surfaces
+  if (tLayer.surfaceArray() != nullptr and m_cfg.processSensitives) {
+    // sensitive surface loop
+    for (auto& sSurface : tLayer.surfaceArray()->surfaces()) {
+      if (sSurface->surfaceMaterialSharedPtr() != nullptr) {
+        detMatMap.first[sSurface->geoID()] =
+            sSurface->surfaceMaterialSharedPtr();
+      }
+    }
+  }
+}
diff --git a/Io/Root/src/RootParticleWriter.cpp b/Io/Root/src/RootParticleWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c1b2b6ef05ce168be70117cbfbe57c69afa03f85
--- /dev/null
+++ b/Io/Root/src/RootParticleWriter.cpp
@@ -0,0 +1,123 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootParticleWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "Acts/Utilities/Helpers.hpp"
+#include "Acts/Utilities/Units.hpp"
+
+FW::RootParticleWriter::RootParticleWriter(
+    const FW::RootParticleWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputParticles, "RootParticleWriter", lvl), m_cfg(cfg) {
+  // inputParticles is already checked by base constructor
+  if (m_cfg.filePath.empty()) {
+    throw std::invalid_argument("Missing file path");
+  }
+  if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // open root file and create the tree
+  m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+  if (m_outputFile == nullptr) {
+    throw std::ios_base::failure("Could not open '" + m_cfg.filePath + "'");
+  }
+  m_outputFile->cd();
+  m_outputTree = new TTree(m_cfg.treeName.c_str(), m_cfg.treeName.c_str());
+  if (m_outputTree == nullptr) {
+    throw std::bad_alloc();
+  }
+
+  // setup the branches
+  m_outputTree->Branch("event_id", &m_eventId);
+  m_outputTree->Branch("particle_id", &m_particleId, "particle_id/l");
+  m_outputTree->Branch("particle_type", &m_particleType);
+  m_outputTree->Branch("process", &m_process);
+  m_outputTree->Branch("vx", &m_vx);
+  m_outputTree->Branch("vy", &m_vy);
+  m_outputTree->Branch("vz", &m_vz);
+  m_outputTree->Branch("vt", &m_vt);
+  m_outputTree->Branch("px", &m_px);
+  m_outputTree->Branch("py", &m_py);
+  m_outputTree->Branch("pz", &m_pz);
+  m_outputTree->Branch("m", &m_m);
+  m_outputTree->Branch("q", &m_q);
+  m_outputTree->Branch("eta", &m_eta);
+  m_outputTree->Branch("phi", &m_phi);
+  m_outputTree->Branch("pt", &m_pt);
+  m_outputTree->Branch("vertex_primary", &m_vertexPrimary);
+  m_outputTree->Branch("vertex_secondary", &m_vertexSecondary);
+  m_outputTree->Branch("particle", &m_particle);
+  m_outputTree->Branch("generation", &m_generation);
+  m_outputTree->Branch("sub_particle", &m_subParticle);
+}
+
+FW::RootParticleWriter::~RootParticleWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootParticleWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_INFO("Wrote particles to tree '" << m_cfg.treeName << "' in '"
+                                          << m_cfg.filePath << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootParticleWriter::writeT(
+    const AlgorithmContext& ctx, const SimParticleContainer& particles) {
+  if (not m_outputFile) {
+    ACTS_ERROR("Missing output file");
+    return ProcessCode::ABORT;
+  }
+
+  // ensure exclusive access to tree/file while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  m_eventId = ctx.eventNumber;
+  for (const auto& particle : particles) {
+    m_particleId = particle.particleId().value();
+    m_particleType = particle.pdg();
+    m_process = static_cast<decltype(m_process)>(particle.process());
+    // position
+    m_vx = particle.position4().x() / Acts::UnitConstants::mm;
+    m_vy = particle.position4().y() / Acts::UnitConstants::mm;
+    m_vz = particle.position4().z() / Acts::UnitConstants::mm;
+    m_vt = particle.position4().w() / Acts::UnitConstants::ns;
+    // momentum
+    const auto p = particle.absMomentum() / Acts::UnitConstants::GeV;
+    m_px = p * particle.unitDirection().x();
+    m_py = p * particle.unitDirection().y();
+    m_pz = p * particle.unitDirection().z();
+    // particle constants
+    m_m = particle.mass() / Acts::UnitConstants::GeV;
+    m_q = particle.charge() / Acts::UnitConstants::e;
+    // derived kinematic quantities
+    m_eta = Acts::VectorHelpers::eta(particle.unitDirection());
+    m_phi = Acts::VectorHelpers::phi(particle.unitDirection());
+    m_pt = p * Acts::VectorHelpers::perp(particle.unitDirection());
+    // decoded barcode components
+    m_vertexPrimary = particle.particleId().vertexPrimary();
+    m_vertexSecondary = particle.particleId().vertexSecondary();
+    m_particle = particle.particleId().particle();
+    m_generation = particle.particleId().generation();
+    m_subParticle = particle.particleId().subParticle();
+    m_outputTree->Fill();
+  }
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootPlanarClusterWriter.cpp b/Io/Root/src/RootPlanarClusterWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..af609d966ec44b53f62127d134c15b433b38ad21
--- /dev/null
+++ b/Io/Root/src/RootPlanarClusterWriter.cpp
@@ -0,0 +1,195 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootPlanarClusterWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "ACTFW/EventData/SimHit.hpp"
+#include "ACTFW/EventData/SimIdentifier.hpp"
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/Plugins/Digitization/DigitizationModule.hpp"
+#include "Acts/Plugins/Digitization/PlanarModuleCluster.hpp"
+#include "Acts/Plugins/Digitization/Segmentation.hpp"
+#include "Acts/Plugins/Identification/IdentifiedDetectorElement.hpp"
+#include "Acts/Utilities/Units.hpp"
+
+FW::RootPlanarClusterWriter::RootPlanarClusterWriter(
+    const FW::RootPlanarClusterWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputClusters, "RootPlanarClusterWriter", lvl),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // inputClusters is already checked by base constructor
+  if (m_cfg.inputSimulatedHits.empty()) {
+    throw std::invalid_argument("Missing simulated hits input collection");
+  }
+  if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + m_cfg.filePath);
+    }
+  }
+  m_outputFile->cd();
+  m_outputTree =
+      new TTree(m_cfg.treeName.c_str(), "TTree from RootPlanarClusterWriter");
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+
+  // Set the branches
+  m_outputTree->Branch("event_nr", &m_eventNr);
+  m_outputTree->Branch("volume_id", &m_volumeID);
+  m_outputTree->Branch("layer_id", &m_layerID);
+  m_outputTree->Branch("surface_id", &m_surfaceID);
+  m_outputTree->Branch("g_x", &m_x);
+  m_outputTree->Branch("g_y", &m_y);
+  m_outputTree->Branch("g_z", &m_z);
+  m_outputTree->Branch("g_t", &m_t);
+  m_outputTree->Branch("l_x", &m_lx);
+  m_outputTree->Branch("l_y", &m_ly);
+  m_outputTree->Branch("cov_l_x", &m_cov_lx);
+  m_outputTree->Branch("cov_l_y", &m_cov_ly);
+  m_outputTree->Branch("cell_ID_x", &m_cell_IDx);
+  m_outputTree->Branch("cell_ID_y", &m_cell_IDy);
+  m_outputTree->Branch("cell_l_x", &m_cell_lx);
+  m_outputTree->Branch("cell_l_y", &m_cell_ly);
+  m_outputTree->Branch("cell_data", &m_cell_data);
+  m_outputTree->Branch("truth_g_x", &m_t_gx);
+  m_outputTree->Branch("truth_g_y", &m_t_gy);
+  m_outputTree->Branch("truth_g_z", &m_t_gz);
+  m_outputTree->Branch("truth_g_t", &m_t_gt);
+  m_outputTree->Branch("truth_l_x", &m_t_lx);
+  m_outputTree->Branch("truth_l_y", &m_t_ly);
+  m_outputTree->Branch("truth_barcode", &m_t_barcode, "truth_barcode/l");
+}
+
+FW::RootPlanarClusterWriter::~RootPlanarClusterWriter() {
+  /// Close the file if it's yours
+  if (m_cfg.rootFile == nullptr) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootPlanarClusterWriter::endRun() {
+  // Write the tree
+  m_outputFile->cd();
+  m_outputTree->Write();
+  ACTS_INFO("Wrote particles to tree '" << m_cfg.treeName << "' in '"
+                                        << m_cfg.filePath << "'");
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootPlanarClusterWriter::writeT(
+    const AlgorithmContext& ctx,
+    const FW::GeometryIdMultimap<Acts::PlanarModuleCluster>& clusters) {
+  // retrieve simulated hits
+  const auto& simHits =
+      ctx.eventStore.get<SimHitContainer>(m_cfg.inputSimulatedHits);
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+  // Get the event number
+  m_eventNr = ctx.eventNumber;
+
+  // Loop over the planar clusters in this event
+  for (const auto& entry : clusters) {
+    Acts::GeometryID geoId = entry.first;
+    const Acts::PlanarModuleCluster& cluster = entry.second;
+    // local cluster information: position, @todo coveraiance
+    auto parameters = cluster.parameters();
+    Acts::Vector2D local(parameters[Acts::ParDef::eLOC_0],
+                         parameters[Acts::ParDef::eLOC_1]);
+
+    /// prepare for calculating the
+    Acts::Vector3D pos(0, 0, 0);
+    Acts::Vector3D mom(1, 1, 1);
+    // the cluster surface
+    const auto& clusterSurface = cluster.referenceSurface();
+    // transform local into global position information
+    clusterSurface.localToGlobal(ctx.geoContext, local, mom, pos);
+    // identification
+    m_volumeID = geoId.volume();
+    m_layerID = geoId.layer();
+    m_surfaceID = geoId.sensitive();
+    m_x = pos.x();
+    m_y = pos.y();
+    m_z = pos.z();
+    m_t = parameters[2] / Acts::UnitConstants::ns;
+    m_lx = local.x();
+    m_ly = local.y();
+    m_cov_lx = 0.;  // @todo fill in
+    m_cov_ly = 0.;  // @todo fill in
+    // get the cells and run through them
+    const auto& cells = cluster.digitizationCells();
+    auto detectorElement = dynamic_cast<const Acts::IdentifiedDetectorElement*>(
+        clusterSurface.associatedDetectorElement());
+    for (auto& cell : cells) {
+      // cell identification
+      m_cell_IDx.push_back(cell.channel0);
+      m_cell_IDy.push_back(cell.channel1);
+      m_cell_data.push_back(cell.data);
+      // for more we need the digitization module
+      if (detectorElement && detectorElement->digitizationModule()) {
+        auto digitationModule = detectorElement->digitizationModule();
+        const Acts::Segmentation& segmentation =
+            digitationModule->segmentation();
+        // get the cell positions
+        auto cellLocalPosition = segmentation.cellPosition(cell);
+        m_cell_lx.push_back(cellLocalPosition.x());
+        m_cell_ly.push_back(cellLocalPosition.y());
+      }
+    }
+    // write hit-particle truth association
+    // each hit can have multiple particles, e.g. in a dense environment
+    for (auto idx : cluster.sourceLink().indices()) {
+      auto it = simHits.nth(idx);
+      if (it == simHits.end()) {
+        ACTS_FATAL("Simulation hit with index " << idx << " does not exist");
+        return ProcessCode::ABORT;
+      }
+      const auto& simHit = *it;
+
+      // local position to be calculated
+      Acts::Vector2D lPosition;
+      clusterSurface.globalToLocal(ctx.geoContext, simHit.position(),
+                                   simHit.unitDirection(), lPosition);
+      // fill the variables
+      m_t_gx.push_back(simHit.position().x());
+      m_t_gy.push_back(simHit.position().y());
+      m_t_gz.push_back(simHit.position().z());
+      m_t_gt.push_back(simHit.time());
+      m_t_lx.push_back(lPosition.x());
+      m_t_ly.push_back(lPosition.y());
+      m_t_barcode.push_back(simHit.particleId().value());
+    }
+    // fill the tree
+    m_outputTree->Fill();
+    // now reset
+    m_cell_IDx.clear();
+    m_cell_IDy.clear();
+    m_cell_lx.clear();
+    m_cell_ly.clear();
+    m_cell_data.clear();
+    m_t_gx.clear();
+    m_t_gy.clear();
+    m_t_gz.clear();
+    m_t_gt.clear();
+    m_t_lx.clear();
+    m_t_ly.clear();
+    m_t_barcode.clear();
+  }
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootPropagationStepsWriter.cpp b/Io/Root/src/RootPropagationStepsWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7e7ba7cf7c6805a2be62de39f910dd21ab1862ac
--- /dev/null
+++ b/Io/Root/src/RootPropagationStepsWriter.cpp
@@ -0,0 +1,181 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootPropagationStepsWriter.hpp"
+
+#include <Acts/Geometry/GeometryID.hpp>
+#include <Acts/Geometry/TrackingVolume.hpp>
+#include <Acts/Propagator/ConstrainedStep.hpp>
+#include <Acts/Surfaces/Surface.hpp>
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+
+FW::RootPropagationStepsWriter::RootPropagationStepsWriter(
+    const FW::RootPropagationStepsWriter::Config& cfg,
+    Acts::Logging::Level level)
+    : WriterT(cfg.collection, "RootPropagationStepsWriter", level),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // An input collection name and tree name must be specified
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  } else if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + m_cfg.filePath);
+    }
+  }
+  m_outputFile->cd();
+
+  m_outputTree = new TTree(m_cfg.treeName.c_str(),
+                           "TTree from RootPropagationStepsWriter");
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+
+  // Set the branches
+  m_outputTree->Branch("event_nr", &m_eventNr);
+  m_outputTree->Branch("volume_id", &m_volumeID);
+  m_outputTree->Branch("boundary_id", &m_boundaryID);
+  m_outputTree->Branch("layer_id", &m_layerID);
+  m_outputTree->Branch("approach_id", &m_approachID);
+  m_outputTree->Branch("sensitive_id", &m_sensitiveID);
+  m_outputTree->Branch("g_x", &m_x);
+  m_outputTree->Branch("g_y", &m_y);
+  m_outputTree->Branch("g_z", &m_z);
+  m_outputTree->Branch("d_x", &m_dx);
+  m_outputTree->Branch("d_y", &m_dy);
+  m_outputTree->Branch("d_z", &m_dz);
+  m_outputTree->Branch("type", &m_step_type);
+  m_outputTree->Branch("step_acc", &m_step_acc);
+  m_outputTree->Branch("step_act", &m_step_act);
+  m_outputTree->Branch("step_abt", &m_step_abt);
+  m_outputTree->Branch("step_usr", &m_step_usr);
+}
+
+FW::RootPropagationStepsWriter::~RootPropagationStepsWriter() {
+  /// Close the file if it's yours
+  if (m_cfg.rootFile == nullptr) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootPropagationStepsWriter::endRun() {
+  // Write the tree
+  m_outputFile->cd();
+  m_outputTree->Write();
+  ACTS_VERBOSE("Wrote particles to tree '" << m_cfg.treeName << "' in '"
+                                           << m_cfg.filePath << "'");
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootPropagationStepsWriter::writeT(
+    const AlgorithmContext& context,
+    const std::vector<PropagationSteps>& stepCollection) {
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // we get the event number
+  m_eventNr = context.eventNumber;
+
+  // loop over the step vector of each test propagation in this
+  for (auto& steps : stepCollection) {
+    // clear the vectors for each collection
+    m_volumeID.clear();
+    m_boundaryID.clear();
+    m_layerID.clear();
+    m_approachID.clear();
+    m_sensitiveID.clear();
+    m_x.clear();
+    m_y.clear();
+    m_z.clear();
+    m_dx.clear();
+    m_dy.clear();
+    m_dz.clear();
+    m_step_type.clear();
+    m_step_acc.clear();
+    m_step_act.clear();
+    m_step_abt.clear();
+    m_step_usr.clear();
+
+    // loop over single steps
+    for (auto& step : steps) {
+      // the identification of the step
+      Acts::GeometryID::Value volumeID = 0;
+      Acts::GeometryID::Value boundaryID = 0;
+      Acts::GeometryID::Value layerID = 0;
+      Acts::GeometryID::Value approachID = 0;
+      Acts::GeometryID::Value sensitiveID = 0;
+      // get the identification from the surface first
+      if (step.surface) {
+        auto geoID = step.surface->geoID();
+        volumeID = geoID.volume();
+        boundaryID = geoID.boundary();
+        layerID = geoID.layer();
+        approachID = geoID.approach();
+        sensitiveID = geoID.sensitive();
+      }
+      // a current volume overwrites the surface tagged one
+      if (step.volume) {
+        volumeID = step.volume->geoID().volume();
+      }
+      // now fill
+      m_sensitiveID.push_back(sensitiveID);
+      m_approachID.push_back(approachID);
+      m_layerID.push_back(layerID);
+      m_boundaryID.push_back(boundaryID);
+      m_volumeID.push_back(volumeID);
+
+      // kinematic information
+      m_x.push_back(step.position.x());
+      m_y.push_back(step.position.y());
+      m_z.push_back(step.position.z());
+      auto direction = step.momentum.normalized();
+      m_dx.push_back(direction.x());
+      m_dy.push_back(direction.y());
+      m_dz.push_back(direction.z());
+
+      double accuracy = step.stepSize.value(Acts::ConstrainedStep::accuracy);
+      double actor = step.stepSize.value(Acts::ConstrainedStep::actor);
+      double aborter = step.stepSize.value(Acts::ConstrainedStep::aborter);
+      double user = step.stepSize.value(Acts::ConstrainedStep::user);
+      double act2 = actor * actor;
+      double acc2 = accuracy * accuracy;
+      double abo2 = aborter * aborter;
+      double usr2 = user * user;
+
+      // todo - fold with direction
+      if (act2 < acc2 && act2 < abo2 && act2 < usr2) {
+        m_step_type.push_back(0);
+      } else if (acc2 < abo2 && acc2 < usr2) {
+        m_step_type.push_back(1);
+      } else if (abo2 < usr2) {
+        m_step_type.push_back(2);
+      } else {
+        m_step_type.push_back(3);
+      }
+
+      // step size information
+      m_step_acc.push_back(accuracy);
+      m_step_act.push_back(actor);
+      m_step_abt.push_back(aborter);
+      m_step_usr.push_back(user);
+    }
+    m_outputTree->Fill();
+  }
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootSimHitWriter.cpp b/Io/Root/src/RootSimHitWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f00004321a69869542b56f9c874f02e44a809ab8
--- /dev/null
+++ b/Io/Root/src/RootSimHitWriter.cpp
@@ -0,0 +1,123 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootSimHitWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "Acts/Utilities/Units.hpp"
+
+FW::RootSimHitWriter::RootSimHitWriter(const FW::RootSimHitWriter::Config& cfg,
+                                       Acts::Logging::Level lvl)
+    : WriterT(cfg.inputSimulatedHits, "RootSimHitWriter", lvl), m_cfg(cfg) {
+  // inputParticles is already checked by base constructor
+  if (m_cfg.filePath.empty()) {
+    throw std::invalid_argument("Missing file path");
+  }
+  if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // open root file and create the tree
+  m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+  if (m_outputFile == nullptr) {
+    throw std::ios_base::failure("Could not open '" + m_cfg.filePath + "'");
+  }
+  m_outputFile->cd();
+  m_outputTree = new TTree(m_cfg.treeName.c_str(), m_cfg.treeName.c_str());
+  if (m_outputTree == nullptr) {
+    throw std::bad_alloc();
+  }
+
+  // setup the branches
+  m_outputTree->Branch("event_id", &m_eventId);
+  m_outputTree->Branch("geometry_id", &m_geometryId, "geometry_id/l");
+  m_outputTree->Branch("particle_id", &m_particleId, "particle_id/l");
+  m_outputTree->Branch("tx", &m_tx);
+  m_outputTree->Branch("ty", &m_ty);
+  m_outputTree->Branch("tz", &m_tz);
+  m_outputTree->Branch("tt", &m_tt);
+  m_outputTree->Branch("tpx", &m_tpx);
+  m_outputTree->Branch("tpy", &m_tpy);
+  m_outputTree->Branch("tpz", &m_tpz);
+  m_outputTree->Branch("te", &m_te);
+  m_outputTree->Branch("deltapx", &m_deltapx);
+  m_outputTree->Branch("deltapy", &m_deltapy);
+  m_outputTree->Branch("deltapz", &m_deltapz);
+  m_outputTree->Branch("deltae", &m_deltae);
+  m_outputTree->Branch("index", &m_index);
+  m_outputTree->Branch("volume_id", &m_volumeId);
+  m_outputTree->Branch("boundary_id", &m_boundaryId);
+  m_outputTree->Branch("layer_id", &m_layerId);
+  m_outputTree->Branch("approach_id", &m_approachId);
+  m_outputTree->Branch("sensitive_id", &m_sensitiveId);
+}
+
+FW::RootSimHitWriter::~RootSimHitWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootSimHitWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_VERBOSE("Wrote hits to tree '" << m_cfg.treeName << "' in '"
+                                        << m_cfg.filePath << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootSimHitWriter::writeT(const AlgorithmContext& ctx,
+                                             const FW::SimHitContainer& hits) {
+  if (not m_outputFile) {
+    ACTS_ERROR("Missing output file");
+    return ProcessCode::ABORT;
+  }
+
+  // ensure exclusive access to tree/file while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Get the event number
+  m_eventId = ctx.eventNumber;
+  for (const auto& hit : hits) {
+    m_particleId = hit.particleId().value();
+    m_geometryId = hit.geometryId().value();
+    // write hit position
+    m_tx = hit.position4().x() / Acts::UnitConstants::mm;
+    m_ty = hit.position4().y() / Acts::UnitConstants::mm;
+    m_tz = hit.position4().z() / Acts::UnitConstants::mm;
+    m_tt = hit.position4().w() / Acts::UnitConstants::ns;
+    // write four-momentum before interaction
+    m_tpx = hit.momentum4Before().x() / Acts::UnitConstants::GeV;
+    m_tpy = hit.momentum4Before().y() / Acts::UnitConstants::GeV;
+    m_tpz = hit.momentum4Before().z() / Acts::UnitConstants::GeV;
+    m_te = hit.momentum4Before().w() / Acts::UnitConstants::GeV;
+    // write four-momentum change due to interaction
+    const auto delta4 = hit.momentum4After() - hit.momentum4Before();
+    m_deltapx = delta4.x() / Acts::UnitConstants::GeV;
+    m_deltapy = delta4.y() / Acts::UnitConstants::GeV;
+    m_deltapz = delta4.z() / Acts::UnitConstants::GeV;
+    m_deltae = delta4.w() / Acts::UnitConstants::GeV;
+    // write hit index along trajectory
+    m_index = hit.index();
+    // decoded geometry for simplicity
+    m_volumeId = hit.geometryId().volume();
+    m_boundaryId = hit.geometryId().boundary();
+    m_layerId = hit.geometryId().layer();
+    m_approachId = hit.geometryId().approach();
+    m_sensitiveId = hit.geometryId().sensitive();
+    // Fill the tree
+    m_outputTree->Fill();
+  }
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootTrackParameterWriter.cpp b/Io/Root/src/RootTrackParameterWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e6b3a3e46d62fa5c9b56270367d9c5c8ec0e0a81
--- /dev/null
+++ b/Io/Root/src/RootTrackParameterWriter.cpp
@@ -0,0 +1,92 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootTrackParameterWriter.hpp"
+
+#include <Acts/Utilities/Helpers.hpp>
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <iostream>
+#include <stdexcept>
+
+FW::RootTrackParameterWriter::RootTrackParameterWriter(
+    const FW::RootTrackParameterWriter::Config& cfg, Acts::Logging::Level level)
+    : TrackParameterWriter(cfg.collection, "RootTrackParameterWriter", level),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // An input collection name and tree name must be specified
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  } else if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + m_cfg.filePath);
+    }
+  }
+  m_outputFile->cd();
+  m_outputTree = new TTree(m_cfg.treeName.c_str(), m_cfg.treeName.c_str());
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+  else {
+    // I/O parameters
+    m_outputTree->Branch("event_nr", &m_eventNr);
+    m_outputTree->Branch("d0", &m_d0);
+    m_outputTree->Branch("z0", &m_z0);
+    m_outputTree->Branch("phi", &m_phi);
+    m_outputTree->Branch("theta", &m_theta);
+    m_outputTree->Branch("qp", &m_qp);
+    // MORE HERE
+  }
+}
+
+FW::RootTrackParameterWriter::~RootTrackParameterWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootTrackParameterWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_INFO("Wrote trackparameters to tree '" << m_cfg.treeName << "' in '"
+                                                << m_cfg.filePath << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootTrackParameterWriter::writeT(
+    const FW::AlgorithmContext& ctx,
+    const std::vector<BoundTrackParameters>& trackParams) {
+  if (m_outputFile == nullptr)
+    return ProcessCode::SUCCESS;
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Get the event number
+  m_eventNr = ctx.eventNumber;
+
+  for (auto& params : trackParams) {
+    m_d0 = params.parameters()[0];
+    m_z0 = params.parameters()[1];
+    m_phi = params.parameters()[2];
+    m_theta = params.parameters()[3];
+    m_qp = params.parameters()[4];
+
+    m_outputTree->Fill();
+  }
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootTrajectoryWriter.cpp b/Io/Root/src/RootTrajectoryWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5be1c1246cf8171b93c0a347be43648565834af
--- /dev/null
+++ b/Io/Root/src/RootTrajectoryWriter.cpp
@@ -0,0 +1,924 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootTrajectoryWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "ACTFW/EventData/SimParticle.hpp"
+#include "ACTFW/Utilities/Paths.hpp"
+#include "Acts/EventData/Measurement.hpp"
+#include "Acts/EventData/MultiTrajectory.hpp"
+#include "Acts/EventData/MultiTrajectoryHelpers.hpp"
+#include "Acts/EventData/TrackParameters.hpp"
+#include "Acts/Utilities/Helpers.hpp"
+
+using Acts::VectorHelpers::eta;
+using Acts::VectorHelpers::perp;
+using Acts::VectorHelpers::phi;
+using Acts::VectorHelpers::theta;
+using Measurement = Acts::Measurement<FW::SimSourceLink, Acts::ParDef::eLOC_0,
+                                      Acts::ParDef::eLOC_1>;
+
+FW::RootTrajectoryWriter::RootTrajectoryWriter(
+    const FW::RootTrajectoryWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.inputTrajectories, "RootTrajectoryWriter", lvl),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // An input collection name and tree name must be specified
+  if (m_cfg.inputTrajectories.empty()) {
+    throw std::invalid_argument("Missing input trajectory collection");
+  } else if (m_cfg.inputParticles.empty()) {
+    throw std::invalid_argument("Missing input particle collection");
+  } else if (cfg.outputFilename.empty()) {
+    throw std::invalid_argument("Missing output filename");
+  } else if (m_cfg.outputTreename.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    auto path = joinPaths(m_cfg.outputDir, m_cfg.outputFilename);
+    m_outputFile = TFile::Open(path.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + path);
+    }
+  }
+  m_outputFile->cd();
+  m_outputTree =
+      new TTree(m_cfg.outputTreename.c_str(), m_cfg.outputTreename.c_str());
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+  else {
+    // I/O parameters
+    m_outputTree->Branch("event_nr", &m_eventNr);
+    m_outputTree->Branch("traj_nr", &m_trajNr);
+    m_outputTree->Branch("t_barcode", &m_t_barcode, "t_barcode/l");
+    m_outputTree->Branch("t_charge", &m_t_charge);
+    m_outputTree->Branch("t_time", &m_t_time);
+    m_outputTree->Branch("t_vx", &m_t_vx);
+    m_outputTree->Branch("t_vy", &m_t_vy);
+    m_outputTree->Branch("t_vz", &m_t_vz);
+    m_outputTree->Branch("t_px", &m_t_px);
+    m_outputTree->Branch("t_py", &m_t_py);
+    m_outputTree->Branch("t_pz", &m_t_pz);
+    m_outputTree->Branch("t_theta", &m_t_theta);
+    m_outputTree->Branch("t_phi", &m_t_phi);
+    m_outputTree->Branch("t_eta", &m_t_eta);
+    m_outputTree->Branch("t_pT", &m_t_pT);
+
+    m_outputTree->Branch("t_x", &m_t_x);
+    m_outputTree->Branch("t_y", &m_t_y);
+    m_outputTree->Branch("t_z", &m_t_z);
+    m_outputTree->Branch("t_r", &m_t_r);
+    m_outputTree->Branch("t_dx", &m_t_dx);
+    m_outputTree->Branch("t_dy", &m_t_dy);
+    m_outputTree->Branch("t_dz", &m_t_dz);
+    m_outputTree->Branch("t_eLOC0", &m_t_eLOC0);
+    m_outputTree->Branch("t_eLOC1", &m_t_eLOC1);
+    m_outputTree->Branch("t_ePHI", &m_t_ePHI);
+    m_outputTree->Branch("t_eTHETA", &m_t_eTHETA);
+    m_outputTree->Branch("t_eQOP", &m_t_eQOP);
+    m_outputTree->Branch("t_eT", &m_t_eT);
+
+    m_outputTree->Branch("nStates", &m_nStates);
+    m_outputTree->Branch("nMeasurements", &m_nMeasurements);
+    m_outputTree->Branch("volume_id", &m_volumeID);
+    m_outputTree->Branch("layer_id", &m_layerID);
+    m_outputTree->Branch("module_id", &m_moduleID);
+    m_outputTree->Branch("l_x_hit", &m_lx_hit);
+    m_outputTree->Branch("l_y_hit", &m_ly_hit);
+    m_outputTree->Branch("g_x_hit", &m_x_hit);
+    m_outputTree->Branch("g_y_hit", &m_y_hit);
+    m_outputTree->Branch("g_z_hit", &m_z_hit);
+    m_outputTree->Branch("res_x_hit", &m_res_x_hit);
+    m_outputTree->Branch("res_y_hit", &m_res_y_hit);
+    m_outputTree->Branch("err_x_hit", &m_err_x_hit);
+    m_outputTree->Branch("err_y_hit", &m_err_y_hit);
+    m_outputTree->Branch("pull_x_hit", &m_pull_x_hit);
+    m_outputTree->Branch("pull_y_hit", &m_pull_y_hit);
+    m_outputTree->Branch("dim_hit", &m_dim_hit);
+
+    m_outputTree->Branch("hasFittedParams", &m_hasFittedParams);
+    m_outputTree->Branch("eLOC0_fit", &m_eLOC0_fit);
+    m_outputTree->Branch("eLOC1_fit", &m_eLOC1_fit);
+    m_outputTree->Branch("ePHI_fit", &m_ePHI_fit);
+    m_outputTree->Branch("eTHETA_fit", &m_eTHETA_fit);
+    m_outputTree->Branch("eQOP_fit", &m_eQOP_fit);
+    m_outputTree->Branch("eT_fit", &m_eT_fit);
+    m_outputTree->Branch("err_eLOC0_fit", &m_err_eLOC0_fit);
+    m_outputTree->Branch("err_eLOC1_fit", &m_err_eLOC1_fit);
+    m_outputTree->Branch("err_ePHI_fit", &m_err_ePHI_fit);
+    m_outputTree->Branch("err_eTHETA_fit", &m_err_eTHETA_fit);
+    m_outputTree->Branch("err_eQOP_fit", &m_err_eQOP_fit);
+    m_outputTree->Branch("err_eT_fit", &m_err_eT_fit);
+
+    m_outputTree->Branch("nPredicted", &m_nPredicted);
+    m_outputTree->Branch("predicted", &m_prt);
+    m_outputTree->Branch("eLOC0_prt", &m_eLOC0_prt);
+    m_outputTree->Branch("eLOC1_prt", &m_eLOC1_prt);
+    m_outputTree->Branch("ePHI_prt", &m_ePHI_prt);
+    m_outputTree->Branch("eTHETA_prt", &m_eTHETA_prt);
+    m_outputTree->Branch("eQOP_prt", &m_eQOP_prt);
+    m_outputTree->Branch("eT_prt", &m_eT_prt);
+    m_outputTree->Branch("res_eLOC0_prt", &m_res_eLOC0_prt);
+    m_outputTree->Branch("res_eLOC1_prt", &m_res_eLOC1_prt);
+    m_outputTree->Branch("res_ePHI_prt", &m_res_ePHI_prt);
+    m_outputTree->Branch("res_eTHETA_prt", &m_res_eTHETA_prt);
+    m_outputTree->Branch("res_eQOP_prt", &m_res_eQOP_prt);
+    m_outputTree->Branch("res_eT_prt", &m_res_eT_prt);
+    m_outputTree->Branch("err_eLOC0_prt", &m_err_eLOC0_prt);
+    m_outputTree->Branch("err_eLOC1_prt", &m_err_eLOC1_prt);
+    m_outputTree->Branch("err_ePHI_prt", &m_err_ePHI_prt);
+    m_outputTree->Branch("err_eTHETA_prt", &m_err_eTHETA_prt);
+    m_outputTree->Branch("err_eQOP_prt", &m_err_eQOP_prt);
+    m_outputTree->Branch("err_eT_prt", &m_err_eT_prt);
+    m_outputTree->Branch("pull_eLOC0_prt", &m_pull_eLOC0_prt);
+    m_outputTree->Branch("pull_eLOC1_prt", &m_pull_eLOC1_prt);
+    m_outputTree->Branch("pull_ePHI_prt", &m_pull_ePHI_prt);
+    m_outputTree->Branch("pull_eTHETA_prt", &m_pull_eTHETA_prt);
+    m_outputTree->Branch("pull_eQOP_prt", &m_pull_eQOP_prt);
+    m_outputTree->Branch("pull_eT_prt", &m_pull_eT_prt);
+    m_outputTree->Branch("g_x_prt", &m_x_prt);
+    m_outputTree->Branch("g_y_prt", &m_y_prt);
+    m_outputTree->Branch("g_z_prt", &m_z_prt);
+    m_outputTree->Branch("px_prt", &m_px_prt);
+    m_outputTree->Branch("py_prt", &m_py_prt);
+    m_outputTree->Branch("pz_prt", &m_pz_prt);
+    m_outputTree->Branch("eta_prt", &m_eta_prt);
+    m_outputTree->Branch("pT_prt", &m_pT_prt);
+
+    m_outputTree->Branch("nFiltered", &m_nFiltered);
+    m_outputTree->Branch("filtered", &m_flt);
+    m_outputTree->Branch("eLOC0_flt", &m_eLOC0_flt);
+    m_outputTree->Branch("eLOC1_flt", &m_eLOC1_flt);
+    m_outputTree->Branch("ePHI_flt", &m_ePHI_flt);
+    m_outputTree->Branch("eTHETA_flt", &m_eTHETA_flt);
+    m_outputTree->Branch("eQOP_flt", &m_eQOP_flt);
+    m_outputTree->Branch("eT_flt", &m_eT_flt);
+    m_outputTree->Branch("res_eLOC0_flt", &m_res_eLOC0_flt);
+    m_outputTree->Branch("res_eLOC1_flt", &m_res_eLOC1_flt);
+    m_outputTree->Branch("res_ePHI_flt", &m_res_ePHI_flt);
+    m_outputTree->Branch("res_eTHETA_flt", &m_res_eTHETA_flt);
+    m_outputTree->Branch("res_eQOP_flt", &m_res_eQOP_flt);
+    m_outputTree->Branch("res_eT_flt", &m_res_eT_flt);
+    m_outputTree->Branch("err_eLOC0_flt", &m_err_eLOC0_flt);
+    m_outputTree->Branch("err_eLOC1_flt", &m_err_eLOC1_flt);
+    m_outputTree->Branch("err_ePHI_flt", &m_err_ePHI_flt);
+    m_outputTree->Branch("err_eTHETA_flt", &m_err_eTHETA_flt);
+    m_outputTree->Branch("err_eQOP_flt", &m_err_eQOP_flt);
+    m_outputTree->Branch("err_eT_flt", &m_err_eT_flt);
+    m_outputTree->Branch("pull_eLOC0_flt", &m_pull_eLOC0_flt);
+    m_outputTree->Branch("pull_eLOC1_flt", &m_pull_eLOC1_flt);
+    m_outputTree->Branch("pull_ePHI_flt", &m_pull_ePHI_flt);
+    m_outputTree->Branch("pull_eTHETA_flt", &m_pull_eTHETA_flt);
+    m_outputTree->Branch("pull_eQOP_flt", &m_pull_eQOP_flt);
+    m_outputTree->Branch("pull_eT_flt", &m_pull_eT_flt);
+    m_outputTree->Branch("g_x_flt", &m_x_flt);
+    m_outputTree->Branch("g_y_flt", &m_y_flt);
+    m_outputTree->Branch("g_z_flt", &m_z_flt);
+    m_outputTree->Branch("px_flt", &m_px_flt);
+    m_outputTree->Branch("py_flt", &m_py_flt);
+    m_outputTree->Branch("pz_flt", &m_pz_flt);
+    m_outputTree->Branch("eta_flt", &m_eta_flt);
+    m_outputTree->Branch("pT_flt", &m_pT_flt);
+    m_outputTree->Branch("chi2", &m_chi2);
+
+    m_outputTree->Branch("nSmoothed", &m_nSmoothed);
+    m_outputTree->Branch("smoothed", &m_smt);
+    m_outputTree->Branch("eLOC0_smt", &m_eLOC0_smt);
+    m_outputTree->Branch("eLOC1_smt", &m_eLOC1_smt);
+    m_outputTree->Branch("ePHI_smt", &m_ePHI_smt);
+    m_outputTree->Branch("eTHETA_smt", &m_eTHETA_smt);
+    m_outputTree->Branch("eQOP_smt", &m_eQOP_smt);
+    m_outputTree->Branch("eT_smt", &m_eT_smt);
+    m_outputTree->Branch("res_eLOC0_smt", &m_res_eLOC0_smt);
+    m_outputTree->Branch("res_eLOC1_smt", &m_res_eLOC1_smt);
+    m_outputTree->Branch("res_ePHI_smt", &m_res_ePHI_smt);
+    m_outputTree->Branch("res_eTHETA_smt", &m_res_eTHETA_smt);
+    m_outputTree->Branch("res_eQOP_smt", &m_res_eQOP_smt);
+    m_outputTree->Branch("res_eT_smt", &m_res_eT_smt);
+    m_outputTree->Branch("err_eLOC0_smt", &m_err_eLOC0_smt);
+    m_outputTree->Branch("err_eLOC1_smt", &m_err_eLOC1_smt);
+    m_outputTree->Branch("err_ePHI_smt", &m_err_ePHI_smt);
+    m_outputTree->Branch("err_eTHETA_smt", &m_err_eTHETA_smt);
+    m_outputTree->Branch("err_eQOP_smt", &m_err_eQOP_smt);
+    m_outputTree->Branch("err_eT_smt", &m_err_eT_smt);
+    m_outputTree->Branch("pull_eLOC0_smt", &m_pull_eLOC0_smt);
+    m_outputTree->Branch("pull_eLOC1_smt", &m_pull_eLOC1_smt);
+    m_outputTree->Branch("pull_ePHI_smt", &m_pull_ePHI_smt);
+    m_outputTree->Branch("pull_eTHETA_smt", &m_pull_eTHETA_smt);
+    m_outputTree->Branch("pull_eQOP_smt", &m_pull_eQOP_smt);
+    m_outputTree->Branch("pull_eT_smt", &m_pull_eT_smt);
+    m_outputTree->Branch("g_x_smt", &m_x_smt);
+    m_outputTree->Branch("g_y_smt", &m_y_smt);
+    m_outputTree->Branch("g_z_smt", &m_z_smt);
+    m_outputTree->Branch("px_smt", &m_px_smt);
+    m_outputTree->Branch("py_smt", &m_py_smt);
+    m_outputTree->Branch("pz_smt", &m_pz_smt);
+    m_outputTree->Branch("eta_smt", &m_eta_smt);
+    m_outputTree->Branch("pT_smt", &m_pT_smt);
+  }
+}
+
+FW::RootTrajectoryWriter::~RootTrajectoryWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootTrajectoryWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_INFO("Write trajectories to tree '"
+              << m_cfg.outputTreename << "' in '"
+              << joinPaths(m_cfg.outputDir, m_cfg.outputFilename) << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::RootTrajectoryWriter::writeT(
+    const AlgorithmContext& ctx, const TrajectoryContainer& trajectories) {
+  if (m_outputFile == nullptr)
+    return ProcessCode::SUCCESS;
+
+  auto& gctx = ctx.geoContext;
+
+  // read truth particles from input collection
+  const auto& particles =
+      ctx.eventStore.get<SimParticleContainer>(m_cfg.inputParticles);
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Get the event number
+  m_eventNr = ctx.eventNumber;
+
+  // Loop over the trajectories
+  int iTraj = 0;
+  for (const auto& traj : trajectories) {
+    m_trajNr = iTraj;
+
+    // The trajectory entry indices and the multiTrajectory
+    const auto& [trackTips, mj] = traj.trajectory();
+    if (trackTips.empty()) {
+      ACTS_WARNING("Empty multiTrajectory.");
+      continue;
+    }
+    // Check the size of the trajectory entry indices. For track fitting, there
+    // should be at most one trajectory
+    if (trackTips.size() > 1) {
+      ACTS_ERROR("Track fitting should not result in multiple trajectories.");
+      return ProcessCode::ABORT;
+    }
+    // Get the entry index for the single trajectory
+    auto& trackTip = trackTips.front();
+
+    // Collect the trajectory summary info
+    auto trajState =
+        Acts::MultiTrajectoryHelpers::trajectoryState(mj, trackTip);
+    m_nMeasurements = trajState.nMeasurements;
+    m_nStates = trajState.nStates;
+
+    // Get the majority truth particle to this track
+    const auto particleHitCount = traj.identifyMajorityParticle(trackTip);
+    if (not particleHitCount.empty()) {
+      // Get the barcode of the majority truth particle
+      m_t_barcode = particleHitCount.front().particleId.value();
+      // Find the truth particle via the barcode
+      auto ip = particles.find(m_t_barcode);
+      if (ip != particles.end()) {
+        const auto& particle = *ip;
+        ACTS_DEBUG("Find the truth particle with barcode = " << m_t_barcode);
+        // Get the truth particle info at vertex
+        const auto p = particle.absMomentum();
+        m_t_charge = particle.charge();
+        m_t_time = particle.time();
+        m_t_vx = particle.position().x();
+        m_t_vy = particle.position().y();
+        m_t_vz = particle.position().z();
+        m_t_px = p * particle.unitDirection().x();
+        m_t_py = p * particle.unitDirection().y();
+        m_t_pz = p * particle.unitDirection().z();
+        m_t_theta = theta(particle.unitDirection());
+        m_t_phi = phi(particle.unitDirection());
+        m_t_eta = eta(particle.unitDirection());
+        m_t_pT = p * perp(particle.unitDirection());
+      } else {
+        ACTS_WARNING("Truth particle with barcode = " << m_t_barcode
+                                                      << " not found!");
+      }
+    }
+
+    // Get the fitted track parameter
+    m_hasFittedParams = false;
+    if (traj.hasTrackParameters(trackTip)) {
+      m_hasFittedParams = true;
+      const auto& boundParam = traj.trackParameters(trackTip);
+      const auto& parameter = boundParam.parameters();
+      const auto& covariance = *boundParam.covariance();
+      m_eLOC0_fit = parameter[Acts::ParDef::eLOC_0];
+      m_eLOC1_fit = parameter[Acts::ParDef::eLOC_1];
+      m_ePHI_fit = parameter[Acts::ParDef::ePHI];
+      m_eTHETA_fit = parameter[Acts::ParDef::eTHETA];
+      m_eQOP_fit = parameter[Acts::ParDef::eQOP];
+      m_eT_fit = parameter[Acts::ParDef::eT];
+      m_err_eLOC0_fit =
+          sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0));
+      m_err_eLOC1_fit =
+          sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1));
+      m_err_ePHI_fit = sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI));
+      m_err_eTHETA_fit =
+          sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA));
+      m_err_eQOP_fit = sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP));
+      m_err_eT_fit = sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT));
+    }
+
+    // Get the trackStates on the trajectory
+    m_nPredicted = 0;
+    m_nFiltered = 0;
+    m_nSmoothed = 0;
+    mj.visitBackwards(trackTip, [&](const auto& state) {
+      // we only fill the track states with non-outlier measurement
+      auto typeFlags = state.typeFlags();
+      if (not typeFlags.test(Acts::TrackStateFlag::MeasurementFlag)) {
+        return true;
+      }
+
+      // get the geometry ID
+      auto geoID = state.referenceSurface().geoID();
+      m_volumeID.push_back(geoID.volume());
+      m_layerID.push_back(geoID.layer());
+      m_moduleID.push_back(geoID.sensitive());
+
+      auto meas = std::get<Measurement>(*state.uncalibrated());
+
+      // get local position
+      Acts::Vector2D local(meas.parameters()[Acts::ParDef::eLOC_0],
+                           meas.parameters()[Acts::ParDef::eLOC_1]);
+      // get global position
+      Acts::Vector3D global(0, 0, 0);
+      Acts::Vector3D mom(1, 1, 1);
+      meas.referenceSurface().localToGlobal(ctx.geoContext, local, mom, global);
+
+      // get measurement covariance
+      auto cov = meas.covariance();
+      // float resX = sqrt(cov(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0));
+      // float resY = sqrt(cov(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1));
+
+      // push the measurement info
+      m_lx_hit.push_back(local.x());
+      m_ly_hit.push_back(local.y());
+      m_x_hit.push_back(global.x());
+      m_y_hit.push_back(global.y());
+      m_z_hit.push_back(global.z());
+
+      // get the truth hit corresponding to this trackState
+      const auto& truthHit = state.uncalibrated().truthHit();
+      // get local truth position
+      Acts::Vector2D truthlocal;
+      meas.referenceSurface().globalToLocal(
+          gctx, truthHit.position(), truthHit.unitDirection(), truthlocal);
+
+      // push the truth hit info
+      m_t_x.push_back(truthHit.position().x());
+      m_t_y.push_back(truthHit.position().y());
+      m_t_z.push_back(truthHit.position().z());
+      m_t_r.push_back(perp(truthHit.position()));
+      m_t_dx.push_back(truthHit.unitDirection().x());
+      m_t_dy.push_back(truthHit.unitDirection().y());
+      m_t_dz.push_back(truthHit.unitDirection().z());
+
+      // get the truth track parameter at this track State
+      float truthLOC0 = 0, truthLOC1 = 0, truthPHI = 0, truthTHETA = 0,
+            truthQOP = 0, truthTIME = 0;
+      truthLOC0 = truthlocal.x();
+      truthLOC1 = truthlocal.y();
+      truthPHI = phi(truthHit.unitDirection());
+      truthTHETA = theta(truthHit.unitDirection());
+      truthQOP =
+          m_t_charge / truthHit.momentum4Before().template head<3>().norm();
+      truthTIME = truthHit.time();
+
+      // push the truth track parameter at this track State
+      m_t_eLOC0.push_back(truthLOC0);
+      m_t_eLOC1.push_back(truthLOC1);
+      m_t_ePHI.push_back(truthPHI);
+      m_t_eTHETA.push_back(truthTHETA);
+      m_t_eQOP.push_back(truthQOP);
+      m_t_eT.push_back(truthTIME);
+
+      // get the predicted parameter
+      bool predicted = false;
+      if (state.hasPredicted()) {
+        predicted = true;
+        m_nPredicted++;
+        Acts::BoundParameters parameter(
+            gctx, state.predictedCovariance(), state.predicted(),
+            state.referenceSurface().getSharedPtr());
+        auto covariance = state.predictedCovariance();
+        // local hit residual info
+        auto H = meas.projector();
+        auto resCov = cov + H * covariance * H.transpose();
+        auto residual = meas.residual(parameter);
+        m_res_x_hit.push_back(residual(Acts::ParDef::eLOC_0));
+        m_res_y_hit.push_back(residual(Acts::ParDef::eLOC_1));
+        m_err_x_hit.push_back(
+            sqrt(resCov(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_err_y_hit.push_back(
+            sqrt(resCov(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_pull_x_hit.push_back(
+            residual(Acts::ParDef::eLOC_0) /
+            sqrt(resCov(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_pull_y_hit.push_back(
+            residual(Acts::ParDef::eLOC_1) /
+            sqrt(resCov(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_dim_hit.push_back(state.calibratedSize());
+
+        // predicted parameter
+        m_eLOC0_prt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0]);
+        m_eLOC1_prt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1]);
+        m_ePHI_prt.push_back(parameter.parameters()[Acts::ParDef::ePHI]);
+        m_eTHETA_prt.push_back(parameter.parameters()[Acts::ParDef::eTHETA]);
+        m_eQOP_prt.push_back(parameter.parameters()[Acts::ParDef::eQOP]);
+        m_eT_prt.push_back(parameter.parameters()[Acts::ParDef::eT]);
+
+        // predicted residual
+        m_res_eLOC0_prt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0] -
+                                  truthLOC0);
+        m_res_eLOC1_prt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1] -
+                                  truthLOC1);
+        m_res_ePHI_prt.push_back(parameter.parameters()[Acts::ParDef::ePHI] -
+                                 truthPHI);
+        m_res_eTHETA_prt.push_back(
+            parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA);
+        m_res_eQOP_prt.push_back(parameter.parameters()[Acts::ParDef::eQOP] -
+                                 truthQOP);
+        m_res_eT_prt.push_back(parameter.parameters()[Acts::ParDef::eT] -
+                               truthTIME);
+
+        // predicted parameter error
+        m_err_eLOC0_prt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_err_eLOC1_prt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_err_ePHI_prt.push_back(
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_err_eTHETA_prt.push_back(
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_err_eQOP_prt.push_back(
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_err_eT_prt.push_back(
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // predicted parameter pull
+        m_pull_eLOC0_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_0] - truthLOC0) /
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_pull_eLOC1_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_1] - truthLOC1) /
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_pull_ePHI_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::ePHI] - truthPHI) /
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_pull_eTHETA_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA) /
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_pull_eQOP_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::eQOP] - truthQOP) /
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_pull_eT_prt.push_back(
+            (parameter.parameters()[Acts::ParDef::eT] - truthTIME) /
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // further predicted parameter info
+        m_x_prt.push_back(parameter.position().x());
+        m_y_prt.push_back(parameter.position().y());
+        m_z_prt.push_back(parameter.position().z());
+        m_px_prt.push_back(parameter.momentum().x());
+        m_py_prt.push_back(parameter.momentum().y());
+        m_pz_prt.push_back(parameter.momentum().z());
+        m_pT_prt.push_back(parameter.pT());
+        m_eta_prt.push_back(eta(parameter.position()));
+      } else {
+        // push default values if no predicted parameter
+        m_res_x_hit.push_back(-99.);
+        m_res_y_hit.push_back(-99.);
+        m_err_x_hit.push_back(-99.);
+        m_err_y_hit.push_back(-99.);
+        m_pull_x_hit.push_back(-99.);
+        m_pull_y_hit.push_back(-99.);
+        m_dim_hit.push_back(-99.);
+        m_eLOC0_prt.push_back(-99.);
+        m_eLOC1_prt.push_back(-99.);
+        m_ePHI_prt.push_back(-99.);
+        m_eTHETA_prt.push_back(-99.);
+        m_eQOP_prt.push_back(-99.);
+        m_eT_prt.push_back(-99.);
+        m_res_eLOC0_prt.push_back(-99.);
+        m_res_eLOC1_prt.push_back(-99.);
+        m_res_ePHI_prt.push_back(-99.);
+        m_res_eTHETA_prt.push_back(-99.);
+        m_res_eQOP_prt.push_back(-99.);
+        m_res_eT_prt.push_back(-99.);
+        m_err_eLOC0_prt.push_back(-99);
+        m_err_eLOC1_prt.push_back(-99);
+        m_err_ePHI_prt.push_back(-99);
+        m_err_eTHETA_prt.push_back(-99);
+        m_err_eQOP_prt.push_back(-99);
+        m_err_eT_prt.push_back(-99);
+        m_pull_eLOC0_prt.push_back(-99.);
+        m_pull_eLOC1_prt.push_back(-99.);
+        m_pull_ePHI_prt.push_back(-99.);
+        m_pull_eTHETA_prt.push_back(-99.);
+        m_pull_eQOP_prt.push_back(-99.);
+        m_pull_eT_prt.push_back(-99.);
+        m_x_prt.push_back(-99.);
+        m_y_prt.push_back(-99.);
+        m_z_prt.push_back(-99.);
+        m_px_prt.push_back(-99.);
+        m_py_prt.push_back(-99.);
+        m_pz_prt.push_back(-99.);
+        m_pT_prt.push_back(-99.);
+        m_eta_prt.push_back(-99.);
+      }
+
+      // get the filtered parameter
+      bool filtered = false;
+      if (state.hasFiltered()) {
+        filtered = true;
+        m_nFiltered++;
+        Acts::BoundParameters parameter(
+            gctx, state.filteredCovariance(), state.filtered(),
+            state.referenceSurface().getSharedPtr());
+        auto covariance = state.filteredCovariance();
+        // filtered parameter
+        m_eLOC0_flt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0]);
+        m_eLOC1_flt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1]);
+        m_ePHI_flt.push_back(parameter.parameters()[Acts::ParDef::ePHI]);
+        m_eTHETA_flt.push_back(parameter.parameters()[Acts::ParDef::eTHETA]);
+        m_eQOP_flt.push_back(parameter.parameters()[Acts::ParDef::eQOP]);
+        m_eT_flt.push_back(parameter.parameters()[Acts::ParDef::eT]);
+
+        // filtered residual
+        m_res_eLOC0_flt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0] -
+                                  truthLOC0);
+        m_res_eLOC1_flt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1] -
+                                  truthLOC1);
+        m_res_ePHI_flt.push_back(parameter.parameters()[Acts::ParDef::ePHI] -
+                                 truthPHI);
+        m_res_eTHETA_flt.push_back(
+            parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA);
+        m_res_eQOP_flt.push_back(parameter.parameters()[Acts::ParDef::eQOP] -
+                                 truthQOP);
+        m_res_eT_flt.push_back(parameter.parameters()[Acts::ParDef::eT] -
+                               truthTIME);
+
+        // filtered parameter error
+        m_err_eLOC0_flt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_err_eLOC1_flt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_err_ePHI_flt.push_back(
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_err_eTHETA_flt.push_back(
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_err_eQOP_flt.push_back(
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_err_eT_flt.push_back(
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // filtered parameter pull
+        m_pull_eLOC0_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_0] - truthLOC0) /
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_pull_eLOC1_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_1] - truthLOC1) /
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_pull_ePHI_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::ePHI] - truthPHI) /
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_pull_eTHETA_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA) /
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_pull_eQOP_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::eQOP] - truthQOP) /
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_pull_eT_flt.push_back(
+            (parameter.parameters()[Acts::ParDef::eT] - truthTIME) /
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // more filtered parameter info
+        m_x_flt.push_back(parameter.position().x());
+        m_y_flt.push_back(parameter.position().y());
+        m_z_flt.push_back(parameter.position().z());
+        m_px_flt.push_back(parameter.momentum().x());
+        m_py_flt.push_back(parameter.momentum().y());
+        m_pz_flt.push_back(parameter.momentum().z());
+        m_pT_flt.push_back(parameter.pT());
+        m_eta_flt.push_back(eta(parameter.position()));
+        m_chi2.push_back(state.chi2());
+      } else {
+        // push default values if no filtered parameter
+        m_eLOC0_flt.push_back(-99.);
+        m_eLOC1_flt.push_back(-99.);
+        m_ePHI_flt.push_back(-99.);
+        m_eTHETA_flt.push_back(-99.);
+        m_eQOP_flt.push_back(-99.);
+        m_eT_flt.push_back(-99.);
+        m_res_eLOC0_flt.push_back(-99.);
+        m_res_eLOC1_flt.push_back(-99.);
+        m_res_ePHI_flt.push_back(-99.);
+        m_res_eTHETA_flt.push_back(-99.);
+        m_res_eQOP_flt.push_back(-99.);
+        m_res_eT_flt.push_back(-99.);
+        m_err_eLOC0_flt.push_back(-99);
+        m_err_eLOC1_flt.push_back(-99);
+        m_err_ePHI_flt.push_back(-99);
+        m_err_eTHETA_flt.push_back(-99);
+        m_err_eQOP_flt.push_back(-99);
+        m_err_eT_flt.push_back(-99);
+        m_pull_eLOC0_flt.push_back(-99.);
+        m_pull_eLOC1_flt.push_back(-99.);
+        m_pull_ePHI_flt.push_back(-99.);
+        m_pull_eTHETA_flt.push_back(-99.);
+        m_pull_eQOP_flt.push_back(-99.);
+        m_pull_eT_flt.push_back(-99.);
+        m_x_flt.push_back(-99.);
+        m_y_flt.push_back(-99.);
+        m_z_flt.push_back(-99.);
+        m_py_flt.push_back(-99.);
+        m_pz_flt.push_back(-99.);
+        m_pT_flt.push_back(-99.);
+        m_eta_flt.push_back(-99.);
+        m_chi2.push_back(-99.0);
+      }
+
+      // get the smoothed parameter
+      bool smoothed = false;
+      if (state.hasSmoothed()) {
+        smoothed = true;
+        m_nSmoothed++;
+        Acts::BoundParameters parameter(
+            gctx, state.smoothedCovariance(), state.smoothed(),
+            state.referenceSurface().getSharedPtr());
+        auto covariance = state.smoothedCovariance();
+
+        // smoothed parameter
+        m_eLOC0_smt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0]);
+        m_eLOC1_smt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1]);
+        m_ePHI_smt.push_back(parameter.parameters()[Acts::ParDef::ePHI]);
+        m_eTHETA_smt.push_back(parameter.parameters()[Acts::ParDef::eTHETA]);
+        m_eQOP_smt.push_back(parameter.parameters()[Acts::ParDef::eQOP]);
+        m_eT_smt.push_back(parameter.parameters()[Acts::ParDef::eT]);
+
+        // smoothed residual
+        m_res_eLOC0_smt.push_back(parameter.parameters()[Acts::ParDef::eLOC_0] -
+                                  truthLOC0);
+        m_res_eLOC1_smt.push_back(parameter.parameters()[Acts::ParDef::eLOC_1] -
+                                  truthLOC1);
+        m_res_ePHI_smt.push_back(parameter.parameters()[Acts::ParDef::ePHI] -
+                                 truthPHI);
+        m_res_eTHETA_smt.push_back(
+            parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA);
+        m_res_eQOP_smt.push_back(parameter.parameters()[Acts::ParDef::eQOP] -
+                                 truthQOP);
+        m_res_eT_smt.push_back(parameter.parameters()[Acts::ParDef::eT] -
+                               truthTIME);
+
+        // smoothed parameter error
+        m_err_eLOC0_smt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_err_eLOC1_smt.push_back(
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_err_ePHI_smt.push_back(
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_err_eTHETA_smt.push_back(
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_err_eQOP_smt.push_back(
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_err_eT_smt.push_back(
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // smoothed parameter pull
+        m_pull_eLOC0_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_0] - truthLOC0) /
+            sqrt(covariance(Acts::ParDef::eLOC_0, Acts::ParDef::eLOC_0)));
+        m_pull_eLOC1_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::eLOC_1] - truthLOC1) /
+            sqrt(covariance(Acts::ParDef::eLOC_1, Acts::ParDef::eLOC_1)));
+        m_pull_ePHI_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::ePHI] - truthPHI) /
+            sqrt(covariance(Acts::ParDef::ePHI, Acts::ParDef::ePHI)));
+        m_pull_eTHETA_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::eTHETA] - truthTHETA) /
+            sqrt(covariance(Acts::ParDef::eTHETA, Acts::ParDef::eTHETA)));
+        m_pull_eQOP_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::eQOP] - truthQOP) /
+            sqrt(covariance(Acts::ParDef::eQOP, Acts::ParDef::eQOP)));
+        m_pull_eT_smt.push_back(
+            (parameter.parameters()[Acts::ParDef::eT] - truthTIME) /
+            sqrt(covariance(Acts::ParDef::eT, Acts::ParDef::eT)));
+
+        // further smoothed parameter info
+        m_x_smt.push_back(parameter.position().x());
+        m_y_smt.push_back(parameter.position().y());
+        m_z_smt.push_back(parameter.position().z());
+        m_px_smt.push_back(parameter.momentum().x());
+        m_py_smt.push_back(parameter.momentum().y());
+        m_pz_smt.push_back(parameter.momentum().z());
+        m_pT_smt.push_back(parameter.pT());
+        m_eta_smt.push_back(eta(parameter.position()));
+      } else {
+        // push default values if no smoothed parameter
+        m_eLOC0_smt.push_back(-99.);
+        m_eLOC1_smt.push_back(-99.);
+        m_ePHI_smt.push_back(-99.);
+        m_eTHETA_smt.push_back(-99.);
+        m_eQOP_smt.push_back(-99.);
+        m_eT_smt.push_back(-99.);
+        m_res_eLOC0_smt.push_back(-99.);
+        m_res_eLOC1_smt.push_back(-99.);
+        m_res_ePHI_smt.push_back(-99.);
+        m_res_eTHETA_smt.push_back(-99.);
+        m_res_eQOP_smt.push_back(-99.);
+        m_res_eT_smt.push_back(-99.);
+        m_err_eLOC0_smt.push_back(-99);
+        m_err_eLOC1_smt.push_back(-99);
+        m_err_ePHI_smt.push_back(-99);
+        m_err_eTHETA_smt.push_back(-99);
+        m_err_eQOP_smt.push_back(-99);
+        m_err_eT_smt.push_back(-99);
+        m_pull_eLOC0_smt.push_back(-99.);
+        m_pull_eLOC1_smt.push_back(-99.);
+        m_pull_ePHI_smt.push_back(-99.);
+        m_pull_eTHETA_smt.push_back(-99.);
+        m_pull_eQOP_smt.push_back(-99.);
+        m_pull_eT_smt.push_back(-99.);
+        m_x_smt.push_back(-99.);
+        m_y_smt.push_back(-99.);
+        m_z_smt.push_back(-99.);
+        m_px_smt.push_back(-99.);
+        m_py_smt.push_back(-99.);
+        m_pz_smt.push_back(-99.);
+        m_pT_smt.push_back(-99.);
+        m_eta_smt.push_back(-99.);
+      }
+
+      m_prt.push_back(predicted);
+      m_flt.push_back(filtered);
+      m_smt.push_back(smoothed);
+      return true;
+    });  // all states
+
+    // fill the variables for one track to tree
+    m_outputTree->Fill();
+
+    // now reset
+    m_t_x.clear();
+    m_t_y.clear();
+    m_t_z.clear();
+    m_t_r.clear();
+    m_t_dx.clear();
+    m_t_dy.clear();
+    m_t_dz.clear();
+    m_t_eLOC0.clear();
+    m_t_eLOC1.clear();
+    m_t_ePHI.clear();
+    m_t_eTHETA.clear();
+    m_t_eQOP.clear();
+    m_t_eT.clear();
+
+    m_volumeID.clear();
+    m_layerID.clear();
+    m_moduleID.clear();
+    m_lx_hit.clear();
+    m_ly_hit.clear();
+    m_x_hit.clear();
+    m_y_hit.clear();
+    m_z_hit.clear();
+    m_res_x_hit.clear();
+    m_res_y_hit.clear();
+    m_err_x_hit.clear();
+    m_err_y_hit.clear();
+    m_pull_x_hit.clear();
+    m_pull_y_hit.clear();
+    m_dim_hit.clear();
+
+    m_prt.clear();
+    m_eLOC0_prt.clear();
+    m_eLOC1_prt.clear();
+    m_ePHI_prt.clear();
+    m_eTHETA_prt.clear();
+    m_eQOP_prt.clear();
+    m_eT_prt.clear();
+    m_res_eLOC0_prt.clear();
+    m_res_eLOC1_prt.clear();
+    m_res_ePHI_prt.clear();
+    m_res_eTHETA_prt.clear();
+    m_res_eQOP_prt.clear();
+    m_res_eT_prt.clear();
+    m_err_eLOC0_prt.clear();
+    m_err_eLOC1_prt.clear();
+    m_err_ePHI_prt.clear();
+    m_err_eTHETA_prt.clear();
+    m_err_eQOP_prt.clear();
+    m_err_eT_prt.clear();
+    m_pull_eLOC0_prt.clear();
+    m_pull_eLOC1_prt.clear();
+    m_pull_ePHI_prt.clear();
+    m_pull_eTHETA_prt.clear();
+    m_pull_eQOP_prt.clear();
+    m_pull_eT_prt.clear();
+    m_x_prt.clear();
+    m_y_prt.clear();
+    m_z_prt.clear();
+    m_px_prt.clear();
+    m_py_prt.clear();
+    m_pz_prt.clear();
+    m_eta_prt.clear();
+    m_pT_prt.clear();
+
+    m_flt.clear();
+    m_eLOC0_flt.clear();
+    m_eLOC1_flt.clear();
+    m_ePHI_flt.clear();
+    m_eTHETA_flt.clear();
+    m_eQOP_flt.clear();
+    m_eT_flt.clear();
+    m_res_eLOC0_flt.clear();
+    m_res_eLOC1_flt.clear();
+    m_res_ePHI_flt.clear();
+    m_res_eTHETA_flt.clear();
+    m_res_eQOP_flt.clear();
+    m_res_eT_flt.clear();
+    m_err_eLOC0_flt.clear();
+    m_err_eLOC1_flt.clear();
+    m_err_ePHI_flt.clear();
+    m_err_eTHETA_flt.clear();
+    m_err_eQOP_flt.clear();
+    m_err_eT_flt.clear();
+    m_pull_eLOC0_flt.clear();
+    m_pull_eLOC1_flt.clear();
+    m_pull_ePHI_flt.clear();
+    m_pull_eTHETA_flt.clear();
+    m_pull_eQOP_flt.clear();
+    m_pull_eT_flt.clear();
+    m_x_flt.clear();
+    m_y_flt.clear();
+    m_z_flt.clear();
+    m_px_flt.clear();
+    m_py_flt.clear();
+    m_pz_flt.clear();
+    m_eta_flt.clear();
+    m_pT_flt.clear();
+    m_chi2.clear();
+
+    m_smt.clear();
+    m_eLOC0_smt.clear();
+    m_eLOC1_smt.clear();
+    m_ePHI_smt.clear();
+    m_eTHETA_smt.clear();
+    m_eQOP_smt.clear();
+    m_eT_smt.clear();
+    m_res_eLOC0_smt.clear();
+    m_res_eLOC1_smt.clear();
+    m_res_ePHI_smt.clear();
+    m_res_eTHETA_smt.clear();
+    m_res_eQOP_smt.clear();
+    m_res_eT_smt.clear();
+    m_err_eLOC0_smt.clear();
+    m_err_eLOC1_smt.clear();
+    m_err_ePHI_smt.clear();
+    m_err_eTHETA_smt.clear();
+    m_err_eQOP_smt.clear();
+    m_err_eT_smt.clear();
+    m_pull_eLOC0_smt.clear();
+    m_pull_eLOC1_smt.clear();
+    m_pull_ePHI_smt.clear();
+    m_pull_eTHETA_smt.clear();
+    m_pull_eQOP_smt.clear();
+    m_pull_eT_smt.clear();
+    m_x_smt.clear();
+    m_y_smt.clear();
+    m_z_smt.clear();
+    m_px_smt.clear();
+    m_py_smt.clear();
+    m_pz_smt.clear();
+    m_eta_smt.clear();
+    m_pT_smt.clear();
+
+    iTraj++;
+  }  // all trajectories
+
+  return ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootVertexAndTracksReader.cpp b/Io/Root/src/RootVertexAndTracksReader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..251221090005c7c976b99cc3c80fdf36da360e56
--- /dev/null
+++ b/Io/Root/src/RootVertexAndTracksReader.cpp
@@ -0,0 +1,140 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootVertexAndTracksReader.hpp"
+
+#include <Acts/Surfaces/PerigeeSurface.hpp>
+#include <TChain.h>
+#include <TFile.h>
+#include <iostream>
+
+#include "ACTFW/Framework/WhiteBoard.hpp"
+#include "ACTFW/TruthTracking/VertexAndTracks.hpp"
+
+FW::RootVertexAndTracksReader::RootVertexAndTracksReader(
+    FW::RootVertexAndTracksReader::Config cfg, Acts::Logging::Level lvl)
+    : m_cfg(std::move(cfg)),
+      m_events(0),
+      m_inputChain(nullptr),
+      m_logger(Acts::getDefaultLogger("RootVertexAndTracksReader", lvl)) {
+  m_inputChain = new TChain(m_cfg.treeName.c_str());
+
+  m_inputChain->SetBranchAddress("event_nr", &m_eventNr);
+  m_inputChain->SetBranchAddress("vx", &m_ptrVx);
+  m_inputChain->SetBranchAddress("vy", &m_ptrVy);
+  m_inputChain->SetBranchAddress("vz", &m_ptrVz);
+
+  m_inputChain->SetBranchAddress("d0", &m_ptrD0);
+  m_inputChain->SetBranchAddress("z0", &m_ptrZ0);
+  m_inputChain->SetBranchAddress("phi", &m_ptrPhi);
+  m_inputChain->SetBranchAddress("theta", &m_ptrTheta);
+  m_inputChain->SetBranchAddress("qp", &m_ptrQP);
+  m_inputChain->SetBranchAddress("time", &m_ptrTime);
+  m_inputChain->SetBranchAddress("vtxID", &m_ptrVtxID);
+  m_inputChain->SetBranchAddress("trkCov", &m_ptrTrkCov);
+
+  // loop over the input files
+  for (auto inputFile : m_cfg.fileList) {
+    // add file to the input chain
+    m_inputChain->Add(inputFile.c_str());
+    ACTS_DEBUG("Adding File " << inputFile << " to tree '" << m_cfg.treeName
+                              << "'.");
+  }
+
+  m_events = m_inputChain->GetEntries();
+  ACTS_DEBUG("The full chain has " << m_events << " entries.");
+}
+
+FW::RootVertexAndTracksReader::~RootVertexAndTracksReader() {
+  delete m_ptrVx;
+  delete m_ptrVy;
+  delete m_ptrVz;
+  delete m_ptrD0;
+  delete m_ptrZ0;
+  delete m_ptrPhi;
+  delete m_ptrTheta;
+  delete m_ptrQP;
+  delete m_ptrTime;
+  delete m_ptrVtxID;
+  delete m_ptrTrkCov;
+}
+
+std::string FW::RootVertexAndTracksReader::name() const {
+  return "RootVertexAndTracksReader";
+}
+
+std::pair<size_t, size_t> FW::RootVertexAndTracksReader::availableEvents()
+    const {
+  return {0u, m_events};
+}
+
+FW::ProcessCode FW::RootVertexAndTracksReader::read(
+    const FW::AlgorithmContext& context) {
+  ACTS_DEBUG("Trying to read vertex and tracks.");
+
+  if (m_inputChain && context.eventNumber < m_events) {
+    // Lock the mutex
+    std::lock_guard<std::mutex> lock(m_read_mutex);
+
+    // The collection to be written
+    std::vector<FW::VertexAndTracks> mCollection;
+
+    for (size_t ib = 0; ib < m_cfg.batchSize; ++ib) {
+      // Read the correct entry: batch size * event_number + ib
+      m_inputChain->GetEntry(m_cfg.batchSize * context.eventNumber + ib);
+      ACTS_VERBOSE("Reading entry: " << m_cfg.batchSize * context.eventNumber +
+                                            ib);
+
+      // Loop over all vertices
+      for (size_t idx = 0; idx < m_ptrVx->size(); ++idx) {
+        FW::VertexAndTracks vtxAndTracks;
+        vtxAndTracks.vertex.position4[0] = (*m_ptrVx)[idx];
+        vtxAndTracks.vertex.position4[1] = (*m_ptrVy)[idx];
+        vtxAndTracks.vertex.position4[2] = (*m_ptrVz)[idx];
+        vtxAndTracks.vertex.position4[3] = 0;
+
+        std::vector<Acts::BoundParameters> tracks;
+        // Loop over all tracks in current event
+        for (size_t trkId = 0; trkId < m_ptrD0->size(); ++trkId) {
+          // Take only tracks that belong to current vertex
+          if (static_cast<size_t>((*m_ptrVtxID)[trkId]) == idx) {
+            // Get track parameter
+            Acts::BoundVector newTrackParams;
+            newTrackParams << (*m_ptrD0)[trkId], (*m_ptrZ0)[trkId],
+                (*m_ptrPhi)[trkId], (*m_ptrTheta)[trkId], (*m_ptrQP)[trkId],
+                (*m_ptrTime)[trkId];
+
+            // Get track covariance vector
+            std::vector<double> trkCovVec = (*m_ptrTrkCov)[trkId];
+
+            // Construct track covariance
+            Acts::BoundSymMatrix covMat =
+                Eigen::Map<Acts::BoundSymMatrix>(trkCovVec.data());
+
+            // Create track parameters and add to track list
+            std::shared_ptr<Acts::PerigeeSurface> perigeeSurface =
+                Acts::Surface::makeShared<Acts::PerigeeSurface>(
+                    Acts::Vector3D(0., 0., 0.));
+            tracks.push_back(
+                Acts::BoundParameters(context.geoContext, std::move(covMat),
+                                      newTrackParams, perigeeSurface));
+          }
+        }  // End loop over all tracks
+        // Set tracks
+        vtxAndTracks.tracks = tracks;
+        // Add to collection
+        mCollection.push_back(std::move(vtxAndTracks));
+      }
+    }
+
+    // Write to the collection to the EventStore
+    context.eventStore.add(m_cfg.outputCollection, std::move(mCollection));
+  }
+  // Return success flag
+  return FW::ProcessCode::SUCCESS;
+}
diff --git a/Io/Root/src/RootVertexAndTracksWriter.cpp b/Io/Root/src/RootVertexAndTracksWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c35878443649a977d9519657be8260cae8ca1d2d
--- /dev/null
+++ b/Io/Root/src/RootVertexAndTracksWriter.cpp
@@ -0,0 +1,256 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2019 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 http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/RootVertexAndTracksWriter.hpp"
+
+#include <Acts/Utilities/Helpers.hpp>
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+using Acts::VectorHelpers::eta;
+using Acts::VectorHelpers::perp;
+using Acts::VectorHelpers::phi;
+
+FW::RootVertexAndTracksWriter::RootVertexAndTracksWriter(
+    const FW::RootVertexAndTracksWriter::Config& cfg, Acts::Logging::Level lvl)
+    : WriterT(cfg.collection, "RootVertexAndTracksWriter", lvl),
+      m_cfg(cfg),
+      m_outputFile(cfg.rootFile) {
+  // An input collection name and tree name must be specified
+  if (m_cfg.collection.empty()) {
+    throw std::invalid_argument("Missing input collection");
+  } else if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // Setup ROOT I/O
+  if (m_outputFile == nullptr) {
+    m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+    if (m_outputFile == nullptr) {
+      throw std::ios_base::failure("Could not open '" + m_cfg.filePath);
+    }
+  }
+  m_outputFile->cd();
+  m_outputTree = new TTree(m_cfg.treeName.c_str(), m_cfg.treeName.c_str());
+  if (m_outputTree == nullptr)
+    throw std::bad_alloc();
+  else {
+    // I/O parameters
+    m_outputTree->Branch("event_nr", &m_eventNr);
+    m_outputTree->Branch("vx", &m_ptrVx);
+    m_outputTree->Branch("vy", &m_ptrVy);
+    m_outputTree->Branch("vz", &m_ptrVz);
+
+    m_outputTree->Branch("d0", &m_ptrD0);
+    m_outputTree->Branch("z0", &m_ptrZ0);
+    m_outputTree->Branch("phi", &m_ptrPhi);
+    m_outputTree->Branch("theta", &m_ptrTheta);
+    m_outputTree->Branch("qp", &m_ptrQP);
+    m_outputTree->Branch("time", &m_ptrTime);
+    m_outputTree->Branch("vtxID", &m_ptrVtxID);
+
+    m_outputTree->Branch("trkCov11", &m_ptrCov11);
+    m_outputTree->Branch("trkCov12", &m_ptrCov12);
+    m_outputTree->Branch("trkCov13", &m_ptrCov13);
+    m_outputTree->Branch("trkCov14", &m_ptrCov14);
+    m_outputTree->Branch("trkCov15", &m_ptrCov15);
+    m_outputTree->Branch("trkCov16", &m_ptrCov16);
+
+    m_outputTree->Branch("trkCov21", &m_ptrCov21);
+    m_outputTree->Branch("trkCov22", &m_ptrCov22);
+    m_outputTree->Branch("trkCov23", &m_ptrCov23);
+    m_outputTree->Branch("trkCov24", &m_ptrCov24);
+    m_outputTree->Branch("trkCov25", &m_ptrCov25);
+    m_outputTree->Branch("trkCov26", &m_ptrCov26);
+
+    m_outputTree->Branch("trkCov31", &m_ptrCov31);
+    m_outputTree->Branch("trkCov32", &m_ptrCov32);
+    m_outputTree->Branch("trkCov33", &m_ptrCov33);
+    m_outputTree->Branch("trkCov34", &m_ptrCov34);
+    m_outputTree->Branch("trkCov35", &m_ptrCov35);
+    m_outputTree->Branch("trkCov36", &m_ptrCov36);
+
+    m_outputTree->Branch("trkCov41", &m_ptrCov41);
+    m_outputTree->Branch("trkCov42", &m_ptrCov42);
+    m_outputTree->Branch("trkCov43", &m_ptrCov43);
+    m_outputTree->Branch("trkCov44", &m_ptrCov44);
+    m_outputTree->Branch("trkCov45", &m_ptrCov45);
+    m_outputTree->Branch("trkCov46", &m_ptrCov46);
+
+    m_outputTree->Branch("trkCov51", &m_ptrCov51);
+    m_outputTree->Branch("trkCov52", &m_ptrCov52);
+    m_outputTree->Branch("trkCov53", &m_ptrCov53);
+    m_outputTree->Branch("trkCov54", &m_ptrCov54);
+    m_outputTree->Branch("trkCov55", &m_ptrCov55);
+    m_outputTree->Branch("trkCov56", &m_ptrCov56);
+
+    m_outputTree->Branch("trkCov61", &m_ptrCov61);
+    m_outputTree->Branch("trkCov62", &m_ptrCov62);
+    m_outputTree->Branch("trkCov63", &m_ptrCov63);
+    m_outputTree->Branch("trkCov64", &m_ptrCov64);
+    m_outputTree->Branch("trkCov65", &m_ptrCov65);
+    m_outputTree->Branch("trkCov66", &m_ptrCov66);
+  }
+}
+
+FW::RootVertexAndTracksWriter::~RootVertexAndTracksWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::RootVertexAndTracksWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_INFO("Wrote event to tree '" << m_cfg.treeName << "' in '"
+                                      << m_cfg.filePath << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+void FW::RootVertexAndTracksWriter::ClearAll() {
+  m_vx.clear();
+  m_vy.clear();
+  m_vz.clear();
+  m_d0.clear();
+  m_z0.clear();
+  m_phi.clear();
+  m_theta.clear();
+  m_qp.clear();
+  m_time.clear();
+  m_vtxID.clear();
+
+  m_cov11.clear();
+  m_cov12.clear();
+  m_cov13.clear();
+  m_cov14.clear();
+  m_cov15.clear();
+  m_cov16.clear();
+
+  m_cov21.clear();
+  m_cov22.clear();
+  m_cov23.clear();
+  m_cov24.clear();
+  m_cov25.clear();
+  m_cov26.clear();
+
+  m_cov31.clear();
+  m_cov32.clear();
+  m_cov33.clear();
+  m_cov34.clear();
+  m_cov35.clear();
+  m_cov36.clear();
+
+  m_cov41.clear();
+  m_cov42.clear();
+  m_cov43.clear();
+  m_cov44.clear();
+  m_cov45.clear();
+  m_cov46.clear();
+
+  m_cov51.clear();
+  m_cov52.clear();
+  m_cov53.clear();
+  m_cov54.clear();
+  m_cov55.clear();
+  m_cov56.clear();
+
+  m_cov61.clear();
+  m_cov62.clear();
+  m_cov63.clear();
+  m_cov64.clear();
+  m_cov65.clear();
+  m_cov66.clear();
+}
+
+FW::ProcessCode FW::RootVertexAndTracksWriter::writeT(
+    const AlgorithmContext& context,
+    const std::vector<VertexAndTracks>& vertexAndTracksCollection) {
+  if (m_outputFile == nullptr || vertexAndTracksCollection.empty()) {
+    return ProcessCode::SUCCESS;
+  }
+
+  // Exclusive access to the tree while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  ClearAll();
+
+  // Get the event number
+  m_eventNr = context.eventNumber;
+
+  for (auto& vertexAndTracks : vertexAndTracksCollection) {
+    // Collect the vertex information
+    m_vx.push_back(vertexAndTracks.vertex.position().x());
+    m_vy.push_back(vertexAndTracks.vertex.position().y());
+    m_vz.push_back(vertexAndTracks.vertex.position().z());
+
+    for (auto& track : vertexAndTracks.tracks) {
+      // Collect the track information
+      m_d0.push_back(track.parameters()[Acts::ParDef::eLOC_D0]);
+      m_z0.push_back(track.parameters()[Acts::ParDef::eLOC_Z0]);
+      m_phi.push_back(track.parameters()[Acts::ParDef::ePHI]);
+      m_theta.push_back(track.parameters()[Acts::ParDef::eTHETA]);
+      m_qp.push_back(track.parameters()[Acts::ParDef::eQOP]);
+      m_time.push_back(track.parameters()[Acts::ParDef::eT]);
+      // Current vertex index as vertex ID
+      m_vtxID.push_back(m_vx.size() - 1);
+
+      // Save track covariance
+      Acts::BoundSymMatrix cov = *track.covariance();
+
+      m_cov11.push_back(cov(0, 0));
+      m_cov12.push_back(cov(0, 1));
+      m_cov13.push_back(cov(0, 2));
+      m_cov14.push_back(cov(0, 3));
+      m_cov15.push_back(cov(0, 4));
+      m_cov16.push_back(cov(0, 5));
+
+      m_cov21.push_back(cov(1, 0));
+      m_cov22.push_back(cov(1, 1));
+      m_cov23.push_back(cov(1, 2));
+      m_cov24.push_back(cov(1, 3));
+      m_cov25.push_back(cov(1, 4));
+      m_cov26.push_back(cov(1, 5));
+
+      m_cov31.push_back(cov(2, 0));
+      m_cov32.push_back(cov(2, 1));
+      m_cov33.push_back(cov(2, 2));
+      m_cov34.push_back(cov(2, 3));
+      m_cov35.push_back(cov(2, 4));
+      m_cov36.push_back(cov(2, 5));
+
+      m_cov41.push_back(cov(3, 0));
+      m_cov42.push_back(cov(3, 1));
+      m_cov43.push_back(cov(3, 2));
+      m_cov44.push_back(cov(3, 3));
+      m_cov45.push_back(cov(3, 4));
+      m_cov46.push_back(cov(3, 5));
+
+      m_cov51.push_back(cov(4, 0));
+      m_cov52.push_back(cov(4, 1));
+      m_cov53.push_back(cov(4, 2));
+      m_cov54.push_back(cov(4, 3));
+      m_cov55.push_back(cov(4, 4));
+      m_cov56.push_back(cov(4, 5));
+
+      m_cov61.push_back(cov(5, 0));
+      m_cov62.push_back(cov(5, 1));
+      m_cov63.push_back(cov(5, 2));
+      m_cov64.push_back(cov(5, 3));
+      m_cov65.push_back(cov(5, 4));
+      m_cov66.push_back(cov(5, 5));
+    }
+  }
+
+  m_outputTree->Fill();
+
+  return ProcessCode::SUCCESS;
+}