Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-05-01 07:33:37

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include "ActsPlugins/Json/TrackingGeometryJsonConverter.hpp"
0010 
0011 #include "Acts/Geometry/CompositePortalLink.hpp"
0012 #include "Acts/Geometry/ConeVolumeBounds.hpp"
0013 #include "Acts/Geometry/CuboidVolumeBounds.hpp"
0014 #include "Acts/Geometry/CutoutCylinderVolumeBounds.hpp"
0015 #include "Acts/Geometry/CylinderVolumeBounds.hpp"
0016 #include "Acts/Geometry/DiamondVolumeBounds.hpp"
0017 #include "Acts/Geometry/GenericCuboidVolumeBounds.hpp"
0018 #include "Acts/Geometry/GeometryContext.hpp"
0019 #include "Acts/Geometry/GeometryIdentifier.hpp"
0020 #include "Acts/Geometry/GridPortalLink.hpp"
0021 #include "Acts/Geometry/Portal.hpp"
0022 #include "Acts/Geometry/PortalLinkBase.hpp"
0023 #include "Acts/Geometry/TrackingGeometry.hpp"
0024 #include "Acts/Geometry/TrackingVolume.hpp"
0025 #include "Acts/Geometry/TrapezoidVolumeBounds.hpp"
0026 #include "Acts/Geometry/TrivialPortalLink.hpp"
0027 #include "Acts/Geometry/VolumeBounds.hpp"
0028 #include "Acts/Navigation/INavigationPolicy.hpp"
0029 #include "Acts/Navigation/MultiLayerNavigationPolicy.hpp"
0030 #include "Acts/Navigation/MultiNavigationPolicy.hpp"
0031 #include "Acts/Navigation/SurfaceArrayNavigationPolicy.hpp"
0032 #include "Acts/Navigation/TryAllNavigationPolicy.hpp"
0033 #include "Acts/Surfaces/RegularSurface.hpp"
0034 #include "Acts/Surfaces/SurfacePlacementBase.hpp"
0035 #include "Acts/Utilities/AnyGridView.hpp"
0036 #include "Acts/Utilities/Enumerate.hpp"
0037 #include "Acts/Utilities/IAxis.hpp"
0038 #include "Acts/Utilities/Logger.hpp"
0039 #include "ActsPlugins/Json/AlgebraJsonConverter.hpp"
0040 #include "ActsPlugins/Json/GeometryIdentifierJsonConverter.hpp"
0041 #include "ActsPlugins/Json/GridJsonConverter.hpp"
0042 #include "ActsPlugins/Json/SurfaceJsonConverter.hpp"
0043 #include "ActsPlugins/Json/UtilitiesJsonConverter.hpp"
0044 
0045 #include <stdexcept>
0046 #include <unordered_set>
0047 
0048 template <typename object_t, const char* kContext>
0049 struct Acts::TrackingGeometryJsonConverter::PointerToIdLookup {
0050   // Insert a new object to ID mapping.
0051   //
0052   // @param object is the source object pointer key
0053   // @param objectId is the serialized ID to assign
0054   //
0055   // @return true if insertion happened, false if the object was already
0056   //         present
0057   bool emplace(const object_t& object, std::size_t objectId) {
0058     return m_objectIds.emplace(&object, objectId).second;
0059   }
0060 
0061   // Resolve a serialized object ID from an object reference.
0062   //
0063   // @param object is the source object key
0064   //
0065   // @return associated serialized object ID
0066   //
0067   // @throw std::invalid_argument if the object is not in the lookup
0068   std::size_t at(const object_t& object) const {
0069     auto it = m_objectIds.find(&object);
0070     if (it == m_objectIds.end()) {
0071       throw std::invalid_argument("Pointer-to-ID lookup failed for " +
0072                                   std::string{kContext} +
0073                                   ": object is outside serialized hierarchy");
0074     }
0075     return it->second;
0076   }
0077 
0078  private:
0079   // Container mapping object pointers to their respective id
0080   std::unordered_map<const object_t*, std::size_t> m_objectIds;
0081 };
0082 
0083 template <typename object_t, typename pointer_t, const char* kContext>
0084 struct Acts::TrackingGeometryJsonConverter::IdToPointerLikeLookup {
0085   /// Insert a new serialized ID to object mapping.
0086   ///
0087   /// @param objectId is the serialized ID key
0088   /// @param object is the target pointer-like object
0089   ///
0090   /// @return true if insertion happened, false if the ID was already present
0091   bool emplace(std::size_t objectId, pointer_t object) {
0092     return m_objects.emplace(objectId, std::move(object)).second;
0093   }
0094 
0095   /// Try to find a mapped pointer-like object by serialized ID.
0096   ///
0097   /// @param objectId is the serialized ID key
0098   ///
0099   /// @return mapped pointer-like object, or null-equivalent if not found
0100   pointer_t find(std::size_t objectId) const {
0101     auto it = m_objects.find(objectId);
0102     return it == m_objects.end() ? pointer_t{} : it->second;
0103   }
0104 
0105   /// Resolve a mapped pointer-like object by serialized ID.
0106   ///
0107   /// @param objectId is the serialized ID key
0108   ///
0109   /// @return mapped pointer-like object reference
0110   ///
0111   /// @throw std::invalid_argument if the ID is not mapped
0112   const pointer_t& at(std::size_t objectId) const {
0113     auto it = m_objects.find(objectId);
0114     if (it == m_objects.end()) {
0115       throw std::invalid_argument("ID-to-pointer lookup failed for " +
0116                                   std::string{kContext} +
0117                                   ": unknown serialized object ID");
0118     }
0119     return it->second;
0120   }
0121 
0122  private:
0123   /// Container mapping object ids to their respective pointers
0124   std::unordered_map<std::size_t, pointer_t> m_objects;
0125 };
0126 
0127 namespace {
0128 
0129 // Internal notes on the conversion flow used in this translation unit:
0130 // - Encoder side:
0131 //   - normalize enum-like values (axis/shape metadata) to stable JSON strings
0132 //   - encode bounds and portal-link polymorphic payloads using kind tags
0133 //   - emit volumes in deterministic depth-first order and connect them by IDs
0134 //   - emit unique portals in a top-level table and refer to them from volumes
0135 // - Decoder side:
0136 //   - read and validate schema metadata
0137 //   - construct all volumes first, then attach children and portals
0138 //   - rebuild polymorphic portal links from kind-tagged payloads
0139 
0140 constexpr const char* kHeaderKey = "acts-geometry-volumes";
0141 constexpr const char* kVersionKey = "format-version";
0142 constexpr const char* kScopeKey = "scope";
0143 constexpr const char* kScopeValue = "volumes-bounds-portals";
0144 constexpr int kFormatVersion = 1;
0145 
0146 constexpr const char* kPortalsKey = "portals";
0147 constexpr const char* kSurfacesKey = "surfaces";
0148 constexpr const char* kVolumesKey = "volumes";
0149 
0150 constexpr const char* kRootVolumeIdKey = "root_volume_id";
0151 constexpr const char* kVolumeIdKey = "volume_id";
0152 constexpr const char* kPortalIdKey = "portal_id";
0153 constexpr const char* kSurfaceIdKey = "surface_id";
0154 
0155 constexpr const char* kNameKey = "name";
0156 constexpr const char* kGeometryIdKey = "geometry_id";
0157 constexpr const char* kTransformKey = "transform";
0158 constexpr const char* kBoundsKey = "bounds";
0159 constexpr const char* kChildrenKey = "children";
0160 
0161 constexpr const char* kPortalIdsKey = "portal_ids";
0162 constexpr const char* kAlongNormalKey = "along_normal";
0163 constexpr const char* kOppositeNormalKey = "opposite_normal";
0164 
0165 constexpr const char* kNavigationPolicyKey = "navigation_policy";
0166 
0167 constexpr const char* kKindKey = "kind";
0168 constexpr const char* kValuesKey = "values";
0169 constexpr const char* kTargetVolumeIdKey = "target_volume_id";
0170 constexpr const char* kDirectionKey = "direction";
0171 constexpr const char* kAxesKey = "axes";
0172 constexpr const char* kBinsKey = "bins";
0173 constexpr const char* kLocalKey = "local";
0174 constexpr const char* kArtifactLinksKey = "artifact_links";
0175 
0176 // -------------------------------------------------------------------
0177 // Kind getters
0178 
0179 template <typename bounds_t>
0180 std::string getPortalLinkKind() {
0181   if (std::is_same_v<bounds_t, Acts::TrivialPortalLink>) {
0182     return "Trivial";
0183   } else if (std::is_same_v<bounds_t, Acts::CompositePortalLink>) {
0184     return "Composite";
0185   } else if (std::is_same_v<bounds_t, Acts::GridPortalLink>) {
0186     return "Grid";
0187   } else {
0188     throw std::invalid_argument("Unknown portal link kind");
0189   }
0190 }
0191 
0192 template <typename bounds_t>
0193 std::string getVolumeBoundsKind() {
0194   if (std::is_same_v<bounds_t, Acts::ConeVolumeBounds>) {
0195     return "Cone";
0196   } else if (std::is_same_v<bounds_t, Acts::CuboidVolumeBounds>) {
0197     return "Cuboid";
0198   } else if (std::is_same_v<bounds_t, Acts::CutoutCylinderVolumeBounds>) {
0199     return "CutoutCylinder";
0200   } else if (std::is_same_v<bounds_t, Acts::CylinderVolumeBounds>) {
0201     return "Cylinder";
0202   } else if (std::is_same_v<bounds_t, Acts::DiamondVolumeBounds>) {
0203     return "Diamond";
0204   } else if (std::is_same_v<bounds_t, Acts::GenericCuboidVolumeBounds>) {
0205     return "GenericCuboid";
0206   } else if (std::is_same_v<bounds_t, Acts::TrapezoidVolumeBounds>) {
0207     return "Trapezoid";
0208   } else {
0209     throw std::invalid_argument("Unknown volume bounds kind");
0210   }
0211 }
0212 
0213 template <typename bounds_t>
0214 std::string getNavigationPolicyKind() {
0215   if (std::is_same_v<bounds_t, Acts::TryAllNavigationPolicy>) {
0216     return "TryAll";
0217   } else if (std::is_same_v<bounds_t, Acts::SurfaceArrayNavigationPolicy>) {
0218     return "SurfaceArray";
0219   } else if (std::is_same_v<bounds_t, Acts::MultiNavigationPolicy>) {
0220     return "MultiNavigation";
0221   } else if (std::is_same_v<bounds_t,
0222                             Acts::Experimental::MultiLayerNavigationPolicy>) {
0223     return "MultiLayerNavigation";
0224   } else {
0225     throw std::invalid_argument("Unknown portal link kind");
0226   }
0227 }
0228 
0229 // -------------------------------------------------------------------
0230 // Volume bounds encoder/decoder
0231 
0232 template <typename bounds_t>
0233 std::unique_ptr<Acts::VolumeBounds> decodeVolumeBoundsT(
0234     const nlohmann::json& jBounds) {
0235   constexpr std::size_t kValues = bounds_t::BoundValues::eSize;
0236   const auto values = jBounds.at(kValuesKey).get<std::vector<double>>();
0237   if (values.size() != kValues) {
0238     throw std::invalid_argument("Invalid number of values for volume bounds");
0239   }
0240   std::array<double, kValues> boundValues{};
0241   std::copy_n(values.begin(), kValues, boundValues.begin());
0242   return std::make_unique<bounds_t>(boundValues);
0243 }
0244 
0245 template <typename bounds_t>
0246 nlohmann::json encodeVolumeBoundsT(const bounds_t& bounds) {
0247   nlohmann::json jBounds;
0248   jBounds["kind"] = getVolumeBoundsKind<bounds_t>();
0249   jBounds[kValuesKey] = bounds.values();
0250   return jBounds;
0251 }
0252 
0253 // -------------------------------------------------------------------
0254 // Navigation policy encoder/decoder
0255 
0256 std::unique_ptr<Acts::INavigationPolicy> decodeMultiNavigationPolicy(
0257     const nlohmann::json& encoded, const Acts::GeometryContext& gctx,
0258     const Acts::TrackingGeometryJsonConverter& converter,
0259     const Acts::TrackingVolume& volume, const Acts::Logger& logger) {
0260   std::vector<std::unique_ptr<Acts::INavigationPolicy>> children;
0261   for (const auto& child : encoded.at(kChildrenKey)) {
0262     children.push_back(converter.navigationPolicyFromJson(
0263         gctx, child[kNavigationPolicyKey], volume, logger));
0264   }
0265   return std::make_unique<Acts::MultiNavigationPolicy>(std::move(children));
0266 }
0267 
0268 std::unique_ptr<Acts::INavigationPolicy> decodeTryAllNavigationPolicy(
0269     const nlohmann::json& encoded, const Acts::GeometryContext& gctx,
0270     const Acts::TrackingGeometryJsonConverter& /*converter*/,
0271     const Acts::TrackingVolume& volume, const Acts::Logger& logger) {
0272   Acts::TryAllNavigationPolicy::Config cfg;
0273   cfg.passives = encoded.at("passives").get<bool>();
0274   cfg.sensitives = encoded.at("sensitives").get<bool>();
0275   cfg.portals = encoded.at("portals").get<bool>();
0276 
0277   return std::make_unique<Acts::TryAllNavigationPolicy>(gctx, volume, logger,
0278                                                         cfg);
0279 }
0280 
0281 std::unique_ptr<Acts::INavigationPolicy> decodeSurfaceArrayNavigationPolicy(
0282     const nlohmann::json& encoded, const Acts::GeometryContext& gctx,
0283     const Acts::TrackingGeometryJsonConverter& /*converter*/,
0284     const Acts::TrackingVolume& volume, const Acts::Logger& logger) {
0285   Acts::SurfaceArrayNavigationPolicy::Config cfg;
0286   cfg.layerType = encoded.at("layerType")
0287                       .get<Acts::SurfaceArrayNavigationPolicy::LayerType>();
0288   cfg.bins = {encoded.at("bins0").get<std::size_t>(),
0289               encoded.at("bins1").get<std::size_t>()};
0290 
0291   return std::make_unique<Acts::SurfaceArrayNavigationPolicy>(gctx, volume,
0292                                                               logger, cfg);
0293 }
0294 
0295 nlohmann::json encodeMultiNavigationPolicy(
0296     const Acts::MultiNavigationPolicy& policy,
0297     const Acts::TrackingGeometryJsonConverter& converter) {
0298   nlohmann::json jPolicy;
0299   jPolicy[kKindKey] = getNavigationPolicyKind<Acts::MultiNavigationPolicy>();
0300   for (const auto& pol : policy.policies()) {
0301     nlohmann::json jPol;
0302     jPol["navigation_policy"] = converter.navigationPolicyToJson(*pol);
0303     jPolicy[kChildrenKey].push_back(jPol);
0304   }
0305   return jPolicy;
0306 }
0307 
0308 nlohmann::json encodeTryAllNavigationPolicy(
0309     const Acts::TryAllNavigationPolicy& policy,
0310     const Acts::TrackingGeometryJsonConverter& /*converter*/) {
0311   const auto& cfg = policy.config();
0312 
0313   nlohmann::json jPolicy;
0314   jPolicy[kKindKey] = getNavigationPolicyKind<Acts::TryAllNavigationPolicy>();
0315   jPolicy["portals"] = cfg.portals;
0316   jPolicy["sensitives"] = cfg.sensitives;
0317   jPolicy["passives"] = cfg.passives;
0318   return jPolicy;
0319 }
0320 
0321 nlohmann::json encodeSurfaceArrayNavigationPolicy(
0322     const Acts::SurfaceArrayNavigationPolicy& policy,
0323     const Acts::TrackingGeometryJsonConverter& /*converter*/) {
0324   const auto& cfg = policy.config();
0325 
0326   nlohmann::json jPolicy;
0327   jPolicy[kKindKey] =
0328       getNavigationPolicyKind<Acts::SurfaceArrayNavigationPolicy>();
0329   jPolicy["layerType"] = cfg.layerType;
0330   jPolicy["bins0"] = cfg.bins.first;
0331   jPolicy["bins1"] = cfg.bins.second;
0332   return jPolicy;
0333 }
0334 
0335 nlohmann::json encodeMultiLayerNavigationPolicy(
0336     const Acts::Experimental::MultiLayerNavigationPolicy& policy,
0337     const Acts::TrackingGeometryJsonConverter& /*converter*/) {
0338   nlohmann::json jPolicy;
0339   jPolicy[kKindKey] =
0340       getNavigationPolicyKind<Acts::Experimental::MultiLayerNavigationPolicy>();
0341 
0342   const auto& grid = policy.indexedGrid();
0343   nlohmann::json jAxes;
0344   for (const auto* axis : grid.grid.axes()) {
0345     jAxes.push_back(Acts::AxisJsonConverter::toJson(*axis));
0346   }
0347   jPolicy["axes"] = jAxes;
0348 
0349   const auto& casts = grid.casts;
0350   jPolicy["casts"] =
0351       std::vector<Acts::AxisDirection>(casts.begin(), casts.end());
0352   jPolicy["binExpansion"] = policy.config().binExpansion;
0353 
0354   return jPolicy;
0355 }
0356 
0357 std::unique_ptr<Acts::INavigationPolicy> decodeMultiLayerNavigationPolicy(
0358     const nlohmann::json& encoded, const Acts::GeometryContext& gctx,
0359     const Acts::TrackingGeometryJsonConverter& /*converter*/,
0360     const Acts::TrackingVolume& volume, const Acts::Logger& logger) {
0361   const auto& jAxes = encoded.at("axes");
0362   std::array<double, 2> range0 = jAxes.at(0).at("range");
0363   std::size_t bins0 = jAxes.at(0).at("bins");
0364   std::array<double, 2> range1 = jAxes.at(1).at("range");
0365   std::size_t bins1 = jAxes.at(1).at("bins");
0366 
0367   Acts::Axis<Acts::AxisType::Equidistant, Acts::AxisBoundaryType::Bound> axis0(
0368       range0[0], range0[1], bins0);
0369   Acts::Axis<Acts::AxisType::Equidistant, Acts::AxisBoundaryType::Bound> axis1(
0370       range1[0], range1[1], bins1);
0371   Acts::Experimental::MultiLayerNavigationPolicy::GridType grid(
0372       std::move(axis0), std::move(axis1));
0373 
0374   std::vector<Acts::AxisDirection> castsVec =
0375       encoded.at("casts").get<std::vector<Acts::AxisDirection>>();
0376   std::array<Acts::AxisDirection, 2> casts = {castsVec.at(0), castsVec.at(1)};
0377 
0378   Acts::Experimental::MultiLayerNavigationPolicy::IndexedUpdatorType
0379       indexedGrid(std::move(grid), casts);
0380 
0381   Acts::Experimental::MultiLayerNavigationPolicy::Config config;
0382   config.binExpansion =
0383       encoded.at("binExpansion").get<std::vector<std::size_t>>();
0384 
0385   return std::make_unique<Acts::Experimental::MultiLayerNavigationPolicy>(
0386       gctx, volume, logger, config, std::move(indexedGrid));
0387 }
0388 
0389 // -------------------------------------------------------------------
0390 // Portal link encoder/decoder
0391 
0392 std::shared_ptr<Acts::RegularSurface> regularSurfaceFromJson(
0393     const nlohmann::json& jSurface) {
0394   auto surface = Acts::SurfaceJsonConverter::fromJson(jSurface);
0395   auto regular = std::dynamic_pointer_cast<Acts::RegularSurface>(surface);
0396   if (regular == nullptr) {
0397     throw std::invalid_argument("Portal link surface is not a RegularSurface");
0398   }
0399   return regular;
0400 }
0401 
0402 std::unique_ptr<Acts::GridPortalLink> makeGridPortalLink(
0403     const std::shared_ptr<Acts::RegularSurface>& surface,
0404     Acts::AxisDirection direction, const Acts::IAxis& axis0,
0405     const Acts::IAxis* axis1) {
0406   std::unique_ptr<Acts::GridPortalLink> grid;
0407 
0408   if (axis1 == nullptr) {
0409     axis0.visit([&](const auto& a0) {
0410       using axis_t = std::remove_cvref_t<decltype(a0)>;
0411       axis_t axisCopy = a0;
0412       grid =
0413           Acts::GridPortalLink::make(surface, direction, std::move(axisCopy));
0414     });
0415   } else {
0416     axis0.visit([&](const auto& a0) {
0417       using axis0_t = std::remove_cvref_t<decltype(a0)>;
0418       axis0_t axis0Copy = a0;
0419       axis1->visit([&](const auto& a1) {
0420         using axis1_t = std::remove_cvref_t<decltype(a1)>;
0421         axis1_t axis1Copy = a1;
0422         grid = Acts::GridPortalLink::make(surface, std::move(axis0Copy),
0423                                           std::move(axis1Copy));
0424       });
0425     });
0426   }
0427 
0428   if (grid == nullptr) {
0429     throw std::invalid_argument("Could not construct GridPortalLink from axes");
0430   }
0431 
0432   if (grid->direction() != direction) {
0433     throw std::invalid_argument(
0434         "Decoded grid direction does not match payload");
0435   }
0436 
0437   return grid;
0438 }
0439 
0440 nlohmann::json encodeTrivialPortalLink(
0441     const Acts::TrivialPortalLink& link, const Acts::GeometryContext& /*gctx*/,
0442     const Acts::TrackingGeometryJsonConverter& /*converter*/,
0443     const Acts::TrackingGeometryJsonConverter::SurfaceIdLookup& surfaceIds,
0444     const Acts::TrackingGeometryJsonConverter::VolumeIdLookup& volumeIds) {
0445   nlohmann::json jLink;
0446   jLink[kKindKey] = getPortalLinkKind<Acts::TrivialPortalLink>();
0447   jLink[kSurfaceIdKey] = surfaceIds.at(link.surface());
0448   jLink[kTargetVolumeIdKey] = volumeIds.at(link.volume());
0449   return jLink;
0450 }
0451 
0452 nlohmann::json encodeCompositePortalLink(
0453     const Acts::CompositePortalLink& link, const Acts::GeometryContext& gctx,
0454     const Acts::TrackingGeometryJsonConverter& converter,
0455     const Acts::TrackingGeometryJsonConverter::SurfaceIdLookup& surfaceIds,
0456     const Acts::TrackingGeometryJsonConverter::VolumeIdLookup& volumeIds) {
0457   nlohmann::json jLink;
0458   jLink[kKindKey] = getPortalLinkKind<Acts::CompositePortalLink>();
0459   jLink[kSurfaceIdKey] = surfaceIds.at(link.surface());
0460   jLink[kDirectionKey] = link.direction();
0461   jLink[kChildrenKey] = nlohmann::json::array();
0462 
0463   for (const auto& child : link.links()) {
0464     jLink[kChildrenKey].push_back(
0465         converter.portalLinkToJson(gctx, child, surfaceIds, volumeIds));
0466   }
0467   return jLink;
0468 }
0469 
0470 nlohmann::json encodeGridPortalLink(
0471     const Acts::GridPortalLink& link, const Acts::GeometryContext& gctx,
0472     const Acts::TrackingGeometryJsonConverter& converter,
0473     const Acts::TrackingGeometryJsonConverter::SurfaceIdLookup& surfaceIds,
0474     const Acts::TrackingGeometryJsonConverter::VolumeIdLookup& volumeIds) {
0475   nlohmann::json jLink;
0476   jLink[kKindKey] = getPortalLinkKind<Acts::GridPortalLink>();
0477   jLink[kDirectionKey] = link.direction();
0478   jLink[kSurfaceIdKey] = surfaceIds.at(link.surface());
0479   jLink[kAxesKey] = nlohmann::json::array();
0480 
0481   for (const auto* axis : link.grid().axes()) {
0482     jLink[kAxesKey].push_back(Acts::AxisJsonConverter::toJson(*axis));
0483   }
0484 
0485   Acts::AnyGridConstView<const Acts::TrackingVolume*> view(link.grid());
0486   const auto nBins = view.numLocalBins();
0487   const auto dim = view.dimensions();
0488 
0489   jLink[kBinsKey] = nlohmann::json::array();
0490   if (dim == 1u) {
0491     for (std::size_t i0 = 0u; i0 <= nBins.at(0) + 1u; ++i0) {
0492       nlohmann::json jBin;
0493       jBin[kLocalKey] = std::vector<std::size_t>{i0};
0494       if (const auto* target = view.atLocalBins({i0}); target == nullptr) {
0495         jBin[kTargetVolumeIdKey] = nullptr;
0496       } else {
0497         jBin[kTargetVolumeIdKey] = volumeIds.at(*target);
0498       }
0499       jLink[kBinsKey].push_back(std::move(jBin));
0500     }
0501   } else if (dim == 2u) {
0502     for (std::size_t i0 = 0u; i0 <= nBins.at(0) + 1u; ++i0) {
0503       for (std::size_t i1 = 0u; i1 <= nBins.at(1) + 1u; ++i1) {
0504         nlohmann::json jBin;
0505         jBin[kLocalKey] = std::vector<std::size_t>{i0, i1};
0506         if (const auto* target = view.atLocalBins({i0, i1});
0507             target == nullptr) {
0508           jBin[kTargetVolumeIdKey] = nullptr;
0509         } else {
0510           jBin[kTargetVolumeIdKey] = volumeIds.at(*target);
0511         }
0512         jLink[kBinsKey].push_back(std::move(jBin));
0513       }
0514     }
0515   } else {
0516     throw std::invalid_argument("Unsupported GridPortalLink dimensionality");
0517   }
0518 
0519   jLink[kArtifactLinksKey] = nlohmann::json::array();
0520   for (const auto& artifact : link.artifactPortalLinks()) {
0521     jLink[kArtifactLinksKey].push_back(
0522         converter.portalLinkToJson(gctx, artifact, surfaceIds, volumeIds));
0523   }
0524 
0525   return jLink;
0526 }
0527 
0528 std::unique_ptr<Acts::PortalLinkBase> decodeTrivialPortalLink(
0529     const nlohmann::json& encoded,
0530     const Acts::TrackingGeometryJsonConverter& /*converter*/,
0531     const Acts::TrackingGeometryJsonConverter::SurfacePointerLookup& surfaces,
0532     const Acts::TrackingGeometryJsonConverter::VolumePointerLookup& volumes) {
0533   const auto linkSurfaceId = encoded.at(kSurfaceIdKey).get<std::size_t>();
0534   const auto targetVolumeId = encoded.at(kTargetVolumeIdKey).get<std::size_t>();
0535   return std::make_unique<Acts::TrivialPortalLink>(surfaces.at(linkSurfaceId),
0536                                                    *volumes.at(targetVolumeId));
0537 }
0538 
0539 std::unique_ptr<Acts::PortalLinkBase> decodeCompositePortalLink(
0540     const nlohmann::json& encoded,
0541     const Acts::TrackingGeometryJsonConverter& converter,
0542     const Acts::TrackingGeometryJsonConverter::SurfacePointerLookup& surfaces,
0543     const Acts::TrackingGeometryJsonConverter::VolumePointerLookup& volumes) {
0544   const auto direction = encoded.at(kDirectionKey).get<Acts::AxisDirection>();
0545   std::vector<std::unique_ptr<Acts::PortalLinkBase>> children;
0546   for (const auto& child : encoded.at(kChildrenKey)) {
0547     children.push_back(converter.portalLinkFromJson(child, surfaces, volumes));
0548   }
0549   return std::make_unique<Acts::CompositePortalLink>(std::move(children),
0550                                                      direction);
0551 }
0552 
0553 std::unique_ptr<Acts::PortalLinkBase> decodeGridPortalLink(
0554     const nlohmann::json& encoded,
0555     const Acts::TrackingGeometryJsonConverter& converter,
0556     const Acts::TrackingGeometryJsonConverter::SurfacePointerLookup& surfaces,
0557     const Acts::TrackingGeometryJsonConverter::VolumePointerLookup& volumes) {
0558   auto linkSurfaceId = encoded.at(kSurfaceIdKey).get<std::size_t>();
0559   const auto direction = encoded.at(kDirectionKey).get<Acts::AxisDirection>();
0560 
0561   std::vector<std::unique_ptr<Acts::IAxis>> axes;
0562   for (const auto& jAxis : encoded.at(kAxesKey)) {
0563     axes.push_back(Acts::AxisJsonConverter::fromJson(jAxis));
0564   }
0565   if (axes.empty() || axes.size() > 2u) {
0566     throw std::invalid_argument("GridPortalLink requires 1 or 2 axes");
0567   }
0568 
0569   auto grid =
0570       makeGridPortalLink(surfaces.at(linkSurfaceId), direction, *axes.at(0),
0571                          axes.size() == 2u ? axes.at(1).get() : nullptr);
0572 
0573   Acts::AnyGridView<const Acts::TrackingVolume*> view(grid->grid());
0574   const auto dim = view.dimensions();
0575   for (const auto& jBin : encoded.at(kBinsKey)) {
0576     auto local = jBin.at(kLocalKey).get<std::vector<std::size_t>>();
0577     if (local.size() != dim) {
0578       throw std::invalid_argument("Grid bin dimensionality mismatch");
0579     }
0580     Acts::IGrid::AnyIndexType localIndices(local.begin(), local.end());
0581 
0582     const Acts::TrackingVolume* target = nullptr;
0583     if (!jBin.at(kTargetVolumeIdKey).is_null()) {
0584       const auto targetVolumeId =
0585           jBin.at(kTargetVolumeIdKey).get<std::size_t>();
0586       target = volumes.at(targetVolumeId);
0587     }
0588     view.atLocalBins(localIndices) = target;
0589   }
0590 
0591   std::vector<Acts::TrivialPortalLink> artifacts;
0592   if (encoded.contains(kArtifactLinksKey)) {
0593     for (const auto& jArtifact : encoded.at(kArtifactLinksKey)) {
0594       auto decodedArtifact =
0595           converter.portalLinkFromJson(jArtifact, surfaces, volumes);
0596       auto* trivial =
0597           dynamic_cast<Acts::TrivialPortalLink*>(decodedArtifact.get());
0598       if (trivial == nullptr) {
0599         throw std::invalid_argument(
0600             "GridPortalLink artifact link is not trivial");
0601       }
0602       artifacts.push_back(std::move(*trivial));
0603     }
0604   }
0605   grid->setArtifactPortalLinks(std::move(artifacts));
0606 
0607   return grid;
0608 }
0609 
0610 // -------------------------------------------------------------------
0611 // Records for temporary storage
0612 
0613 struct SurfaceRecord {
0614   std::size_t surfaceId = 0u;
0615   nlohmann::json payload;
0616 };
0617 
0618 struct PortalRecord {
0619   std::size_t portalId = 0u;
0620   nlohmann::json payload;
0621 };
0622 
0623 struct VolumeRecord {
0624   std::size_t volumeId = 0u;
0625   std::string name;
0626   Acts::GeometryIdentifier::Value geometryId = 0u;
0627   Acts::Transform3 transform{Acts::Transform3::Identity()};
0628   nlohmann::json bounds;
0629   std::vector<std::size_t> children;
0630   std::vector<std::size_t> portalIds;
0631   std::vector<std::size_t> surfaceIds;
0632   nlohmann::json navigationPolicy;
0633 };
0634 
0635 // -------------------------------------------------------------------
0636 // Utilities
0637 
0638 void verifySchemaHeader(const nlohmann::json& encoded) {
0639   if (!encoded.contains(kHeaderKey)) {
0640     throw std::invalid_argument("Missing geometry JSON header");
0641   }
0642   const auto& header = encoded.at(kHeaderKey);
0643   if (header.at(kVersionKey).get<int>() != kFormatVersion) {
0644     throw std::invalid_argument("Unsupported geometry JSON format version");
0645   }
0646   if (header.at(kScopeKey).get<std::string>() != kScopeValue) {
0647     throw std::invalid_argument("Unexpected geometry JSON scope");
0648   }
0649 }
0650 
0651 void ensureIdentifiers(Acts::TrackingVolume& volume,
0652                        Acts::GeometryIdentifier::Value& nextVolumeId) {
0653   Acts::GeometryIdentifier volumeId = volume.geometryId();
0654   if (volumeId == Acts::GeometryIdentifier{}) {
0655     volumeId = Acts::GeometryIdentifier{}.withVolume(nextVolumeId++);
0656     volume.assignGeometryId(volumeId);
0657   }
0658 
0659   for (const auto [ib, boundary] : Acts::enumerate(volume.boundarySurfaces())) {
0660     // Gen1 api, ignore Sonar complaints
0661     auto& mutableBoundarySurface =
0662         const_cast<Acts::RegularSurface&>(boundary->surfaceRepresentation());
0663     if (mutableBoundarySurface.geometryId() == Acts::GeometryIdentifier{}) {
0664       mutableBoundarySurface.assignGeometryId(
0665           Acts::GeometryIdentifier(volumeId).withBoundary(ib + 1u));
0666     }
0667   }
0668 
0669   std::size_t portalIndex = 0u;
0670   for (auto& portal : volume.portals()) {
0671     if (auto& mutablePortalSurface = portal.surface();
0672         mutablePortalSurface.geometryId() == Acts::GeometryIdentifier{}) {
0673       mutablePortalSurface.assignGeometryId(
0674           Acts::GeometryIdentifier(volumeId).withExtra(portalIndex + 1u));
0675     }
0676     ++portalIndex;
0677   }
0678 
0679   for (auto& child : volume.volumes()) {
0680     ensureIdentifiers(child, nextVolumeId);
0681   }
0682 }
0683 
0684 void collectGeometry(
0685     const Acts::TrackingVolume& volume,
0686     std::vector<const Acts::Surface*>& orderedSurfaces,
0687     std::vector<const Acts::Portal*>& orderedPortals,
0688     std::vector<const Acts::TrackingVolume*>& orderedVolumes,
0689     Acts::TrackingGeometryJsonConverter::SurfaceIdLookup& surfaceIds,
0690     Acts::TrackingGeometryJsonConverter::PortalIdLookup& portalIds,
0691     Acts::TrackingGeometryJsonConverter::VolumeIdLookup& volumeIds) {
0692   auto insertSurface = [&](const auto& surf) {
0693     if (surfaceIds.emplace(surf, orderedSurfaces.size())) {
0694       orderedSurfaces.push_back(&surf);
0695     }
0696   };
0697   auto insertPortal = [&](const auto& port) {
0698     if (portalIds.emplace(port, orderedPortals.size())) {
0699       orderedPortals.push_back(&port);
0700     }
0701   };
0702   auto insertVolume = [&](const auto& vol) {
0703     if (volumeIds.emplace(vol, orderedVolumes.size())) {
0704       orderedVolumes.push_back(&vol);
0705     }
0706   };
0707 
0708   auto insertPortalLink = [&](const auto& link, auto&& self) {
0709     insertSurface(link->surface());
0710 
0711     if (const auto* trivial =
0712             dynamic_cast<const Acts::TrivialPortalLink*>(link);
0713         trivial != nullptr) {
0714       return;
0715     }
0716 
0717     if (const auto* composite =
0718             dynamic_cast<const Acts::CompositePortalLink*>(link);
0719         composite != nullptr) {
0720       const auto& children = composite->links();
0721       for (const auto& child : children) {
0722         self(&child, self);
0723       }
0724       return;
0725     }
0726 
0727     const auto* grid = dynamic_cast<const Acts::GridPortalLink*>(link);
0728     if (grid != nullptr) {
0729       const auto& children = grid->artifactPortalLinks();
0730       for (const auto& child : children) {
0731         self(&child, self);
0732       }
0733       return;
0734     }
0735   };
0736 
0737   insertVolume(volume);
0738 
0739   for (const auto& portal : volume.portals()) {
0740     insertPortal(portal);
0741 
0742     insertSurface(portal.surface());
0743 
0744     if (const auto* along = portal.getLink(Acts::Direction::AlongNormal());
0745         along != nullptr) {
0746       insertPortalLink(along, insertPortalLink);
0747     }
0748 
0749     if (const auto* opposite =
0750             portal.getLink(Acts::Direction::OppositeNormal());
0751         opposite != nullptr) {
0752       insertPortalLink(opposite, insertPortalLink);
0753     }
0754   }
0755   for (const auto& surf : volume.surfaces()) {
0756     insertSurface(surf);
0757   }
0758 
0759   for (const auto& child : volume.volumes()) {
0760     collectGeometry(child, orderedSurfaces, orderedPortals, orderedVolumes,
0761                     surfaceIds, portalIds, volumeIds);
0762   }
0763 }
0764 
0765 }  // namespace
0766 
0767 Acts::TrackingGeometryJsonConverter::Config
0768 Acts::TrackingGeometryJsonConverter::Config::defaultConfig() {
0769   Config cfg;
0770 
0771   cfg.encodeVolumeBounds.registerFunction(encodeVolumeBoundsT<ConeVolumeBounds>)
0772       .registerFunction(encodeVolumeBoundsT<CuboidVolumeBounds>)
0773       .registerFunction(encodeVolumeBoundsT<CutoutCylinderVolumeBounds>)
0774       .registerFunction(encodeVolumeBoundsT<CylinderVolumeBounds>)
0775       .registerFunction(encodeVolumeBoundsT<DiamondVolumeBounds>)
0776       .registerFunction(encodeVolumeBoundsT<GenericCuboidVolumeBounds>)
0777       .registerFunction(encodeVolumeBoundsT<TrapezoidVolumeBounds>);
0778 
0779   cfg.encodeNavigationPolicy.registerFunction(encodeTryAllNavigationPolicy)
0780       .registerFunction(encodeSurfaceArrayNavigationPolicy)
0781       .registerFunction(encodeMultiNavigationPolicy)
0782       .registerFunction(encodeMultiLayerNavigationPolicy);
0783 
0784   cfg.encodePortalLink.registerFunction(encodeTrivialPortalLink)
0785       .registerFunction(encodeCompositePortalLink)
0786       .registerFunction(encodeGridPortalLink);
0787 
0788   cfg.decodePortalLink
0789       .registerKind(getPortalLinkKind<TrivialPortalLink>(),
0790                     decodeTrivialPortalLink)
0791       .registerKind(getPortalLinkKind<CompositePortalLink>(),
0792                     decodeCompositePortalLink)
0793       .registerKind(getPortalLinkKind<GridPortalLink>(), decodeGridPortalLink);
0794 
0795   cfg.decodeVolumeBounds
0796       .registerKind(getVolumeBoundsKind<ConeVolumeBounds>(),
0797                     decodeVolumeBoundsT<ConeVolumeBounds>)
0798       .registerKind(getVolumeBoundsKind<CuboidVolumeBounds>(),
0799                     decodeVolumeBoundsT<CuboidVolumeBounds>)
0800       .registerKind(getVolumeBoundsKind<CutoutCylinderVolumeBounds>(),
0801                     decodeVolumeBoundsT<CutoutCylinderVolumeBounds>)
0802       .registerKind(getVolumeBoundsKind<CylinderVolumeBounds>(),
0803                     decodeVolumeBoundsT<CylinderVolumeBounds>)
0804       .registerKind(getVolumeBoundsKind<DiamondVolumeBounds>(),
0805                     decodeVolumeBoundsT<DiamondVolumeBounds>)
0806       .registerKind(getVolumeBoundsKind<GenericCuboidVolumeBounds>(),
0807                     decodeVolumeBoundsT<GenericCuboidVolumeBounds>)
0808       .registerKind(getVolumeBoundsKind<TrapezoidVolumeBounds>(),
0809                     decodeVolumeBoundsT<TrapezoidVolumeBounds>);
0810 
0811   cfg.decodeNavigationPolicy
0812       .registerKind(getNavigationPolicyKind<TryAllNavigationPolicy>(),
0813                     decodeTryAllNavigationPolicy)
0814       .registerKind(getNavigationPolicyKind<SurfaceArrayNavigationPolicy>(),
0815                     decodeSurfaceArrayNavigationPolicy)
0816       .registerKind(getNavigationPolicyKind<MultiNavigationPolicy>(),
0817                     decodeMultiNavigationPolicy)
0818       .registerKind(
0819           getNavigationPolicyKind<Experimental::MultiLayerNavigationPolicy>(),
0820           decodeMultiLayerNavigationPolicy);
0821 
0822   return cfg;
0823 }
0824 
0825 Acts::TrackingGeometryJsonConverter::TrackingGeometryJsonConverter(
0826     Config config, std::unique_ptr<const Acts::Logger> logger)
0827     : m_cfg(std::move(config)), m_logger(std::move(logger)) {}
0828 
0829 nlohmann::json Acts::TrackingGeometryJsonConverter::toJson(
0830     const GeometryContext& gctx, const TrackingGeometry& geometry,
0831     const Options& options) const {
0832   if (geometry.geometryVersion() != TrackingGeometry::GeometryVersion::Gen3) {
0833     throw std::invalid_argument(
0834         "Tracking geometry serialization is only implemented for Gen3 "
0835         "geometries");
0836   }
0837   ACTS_DEBUG("Serializing TrackingGeometry to JSON");
0838   return trackingVolumeToJson(gctx, *geometry.highestTrackingVolume(), options);
0839 }
0840 
0841 nlohmann::json Acts::TrackingGeometryJsonConverter::portalLinkToJson(
0842     const GeometryContext& gctx, const PortalLinkBase& link,
0843     const SurfaceIdLookup& surfaceIds, const VolumeIdLookup& volumeIds) const {
0844   return m_cfg.encodePortalLink(link, gctx, *this, surfaceIds, volumeIds);
0845 }
0846 
0847 std::unique_ptr<Acts::PortalLinkBase>
0848 Acts::TrackingGeometryJsonConverter::portalLinkFromJson(
0849     const nlohmann::json& encoded, const SurfacePointerLookup& surfaces,
0850     const VolumePointerLookup& volumes) const {
0851   return m_cfg.decodePortalLink(encoded, *this, surfaces, volumes);
0852 }
0853 
0854 nlohmann::json Acts::TrackingGeometryJsonConverter::navigationPolicyToJson(
0855     const Acts::INavigationPolicy& policy) const {
0856   return m_cfg.encodeNavigationPolicy(policy, *this);
0857 }
0858 
0859 std::unique_ptr<Acts::INavigationPolicy>
0860 Acts::TrackingGeometryJsonConverter::navigationPolicyFromJson(
0861     const Acts::GeometryContext& gctx, const nlohmann::json& encoded,
0862     const Acts::TrackingVolume& volume, const Acts::Logger& logger) const {
0863   return m_cfg.decodeNavigationPolicy(encoded, gctx, *this, volume, logger);
0864 }
0865 
0866 nlohmann::json Acts::TrackingGeometryJsonConverter::trackingVolumeToJson(
0867     const GeometryContext& gctx, const TrackingVolume& world,
0868     const Options& /*options*/) const {
0869   nlohmann::json encoded;
0870   encoded[kHeaderKey] = nlohmann::json::object();
0871   encoded[kHeaderKey][kVersionKey] = kFormatVersion;
0872   encoded[kHeaderKey][kScopeKey] = kScopeValue;
0873 
0874   // Collect object
0875   std::vector<const Surface*> orderedSurfaces;
0876   std::vector<const Portal*> orderedPortals;
0877   std::vector<const TrackingVolume*> orderedVolumes;
0878 
0879   SurfaceIdLookup surfaceIds;
0880   PortalIdLookup portalIds;
0881   VolumeIdLookup volumeIds;
0882   collectGeometry(world, orderedSurfaces, orderedPortals, orderedVolumes,
0883                   surfaceIds, portalIds, volumeIds);
0884 
0885   ACTS_DEBUG("Encoding " << orderedVolumes.size() << " volumes, "
0886                          << orderedPortals.size() << " portals, "
0887                          << orderedSurfaces.size()
0888                          << " surfaces from root volume '" << world.volumeName()
0889                          << "'");
0890 
0891   encoded[kSurfacesKey] = nlohmann::json::array();
0892   encoded[kPortalsKey] = nlohmann::json::array();
0893   encoded[kVolumesKey] = nlohmann::json::array();
0894 
0895   encoded[kRootVolumeIdKey] = volumeIds.at(world);
0896 
0897   // Encode surfaces
0898   for (const auto* surf : orderedSurfaces) {
0899     nlohmann::json jSurface = SurfaceJsonConverter::toJson(gctx, *surf);
0900     jSurface[kSurfaceIdKey] = surfaceIds.at(*surf);
0901     encoded[kSurfacesKey].push_back(std::move(jSurface));
0902   }
0903 
0904   // Encode portals
0905   for (const auto* portal : orderedPortals) {
0906     nlohmann::json jPortal;
0907     jPortal[kPortalIdKey] = portalIds.at(*portal);
0908 
0909     if (const auto* along = portal->getLink(Direction::AlongNormal());
0910         along != nullptr) {
0911       jPortal[kAlongNormalKey] =
0912           portalLinkToJson(gctx, *along, surfaceIds, volumeIds);
0913     } else {
0914       jPortal[kAlongNormalKey] = nullptr;
0915     }
0916 
0917     if (const auto* opposite = portal->getLink(Direction::OppositeNormal());
0918         opposite != nullptr) {
0919       jPortal[kOppositeNormalKey] =
0920           portalLinkToJson(gctx, *opposite, surfaceIds, volumeIds);
0921     } else {
0922       jPortal[kOppositeNormalKey] = nullptr;
0923     }
0924 
0925     jPortal[kSurfaceIdKey] = surfaceIds.at(portal->surface());
0926     encoded[kPortalsKey].push_back(std::move(jPortal));
0927   }
0928 
0929   // Encode volumes
0930   for (const auto* volume : orderedVolumes) {
0931     nlohmann::json jVolume;
0932     const auto volumeId = volumeIds.at(*volume);
0933     ACTS_VERBOSE("Encoding volume '" << volume->volumeName()
0934                                      << "' (id=" << volumeId << ")");
0935     jVolume[kVolumeIdKey] = volumeId;
0936     jVolume[kNameKey] = volume->volumeName();
0937     jVolume[kGeometryIdKey] = nlohmann::json(volume->geometryId());
0938     jVolume[kTransformKey] =
0939         Transform3JsonConverter::toJson(volume->localToGlobalTransform(gctx));
0940     jVolume[kBoundsKey] = m_cfg.encodeVolumeBounds(volume->volumeBounds());
0941 
0942     jVolume[kNavigationPolicyKey] =
0943         navigationPolicyToJson(*volume->navigationPolicy());
0944 
0945     jVolume[kChildrenKey] = nlohmann::json::array();
0946     for (const auto& child : volume->volumes()) {
0947       jVolume[kChildrenKey].push_back(volumeIds.at(child));
0948     }
0949 
0950     jVolume[kPortalIdsKey] = nlohmann::json::array();
0951     for (const auto& portal : volume->portals()) {
0952       jVolume[kPortalIdsKey].push_back(portalIds.at(portal));
0953     }
0954 
0955     jVolume[kSurfaceIdKey] = nlohmann::json::array();
0956     for (const auto& surface : volume->surfaces()) {
0957       jVolume[kSurfaceIdKey].push_back(surfaceIds.at(surface));
0958     }
0959 
0960     encoded[kVolumesKey].push_back(std::move(jVolume));
0961   }
0962 
0963   return encoded;
0964 }
0965 
0966 std::shared_ptr<Acts::TrackingVolume>
0967 Acts::TrackingGeometryJsonConverter::trackingVolumeFromJson(
0968     const GeometryContext& gctx, const nlohmann::json& encoded,
0969     const Options& /*options*/) const {
0970   verifySchemaHeader(encoded);
0971 
0972   if (!encoded.contains(kVolumesKey) || !encoded.contains(kRootVolumeIdKey) ||
0973       !encoded.contains(kPortalsKey)) {
0974     throw std::invalid_argument(
0975         "Missing volume payload in tracking geometry JSON");
0976   }
0977 
0978   // Collect surface data
0979   std::unordered_map<std::size_t, SurfaceRecord> surfaceRecords;
0980   for (const auto& jSurface : encoded.at(kSurfacesKey)) {
0981     SurfaceRecord record;
0982     record.surfaceId = jSurface.at(kSurfaceIdKey).get<std::size_t>();
0983     record.payload = jSurface;
0984     const auto [id, inserted] =
0985         surfaceRecords.try_emplace(record.surfaceId, std::move(record));
0986     if (!inserted) {
0987       throw std::invalid_argument("Duplicate serialized surface ID");
0988     }
0989   }
0990 
0991   // Collect portal data
0992   std::unordered_map<std::size_t, PortalRecord> portalRecords;
0993   for (const auto& jPortal : encoded.at(kPortalsKey)) {
0994     PortalRecord record;
0995     record.portalId = jPortal.at(kPortalIdKey).get<std::size_t>();
0996     record.payload = jPortal;
0997     const auto [id, inserted] =
0998         portalRecords.try_emplace(record.portalId, std::move(record));
0999     if (!inserted) {
1000       throw std::invalid_argument("Duplicate serialized portal ID");
1001     }
1002   }
1003 
1004   // Collect volume data
1005   std::unordered_map<std::size_t, VolumeRecord> volumeRecords;
1006   for (const auto& jVolume : encoded.at(kVolumesKey)) {
1007     VolumeRecord record;
1008     record.volumeId = jVolume.at(kVolumeIdKey).get<std::size_t>();
1009     record.name = jVolume.at(kNameKey).get<std::string>();
1010     record.transform =
1011         Transform3JsonConverter::fromJson(jVolume.at(kTransformKey));
1012     record.bounds = jVolume.at(kBoundsKey);
1013     record.children = jVolume.value(kChildrenKey, std::vector<std::size_t>{});
1014     record.portalIds = jVolume.value(kPortalIdsKey, std::vector<std::size_t>{});
1015     record.surfaceIds =
1016         jVolume.value(kSurfaceIdKey, std::vector<std::size_t>{});
1017     record.navigationPolicy = jVolume.at(kNavigationPolicyKey);
1018 
1019     if (!jVolume["geometry_id"].is_null()) {
1020       GeometryIdentifier geoID =
1021           jVolume["geometry_id"].get<GeometryIdentifier>();
1022       record.geometryId = geoID.value();
1023     } else {
1024       record.geometryId = 0;
1025     }
1026 
1027     const auto [id, inserted] =
1028         volumeRecords.try_emplace(record.volumeId, std::move(record));
1029     if (!inserted) {
1030       throw std::invalid_argument("Duplicate serialized volume ID");
1031     }
1032   }
1033 
1034   const std::size_t rootVolumeId =
1035       encoded.at(kRootVolumeIdKey).get<std::size_t>();
1036 
1037   if (!volumeRecords.contains(rootVolumeId)) {
1038     throw std::invalid_argument("Serialized root volume ID does not exist");
1039   }
1040 
1041   ACTS_DEBUG("Decoding " << volumeRecords.size() << " volumes, "
1042                          << portalRecords.size() << " portals, "
1043                          << surfaceRecords.size()
1044                          << " surfaces from root volume '"
1045                          << volumeRecords.at(rootVolumeId).name << "'");
1046 
1047   // Collect surface pointers
1048   SurfacePointerLookup surfacePointers;
1049 
1050   // ---------------------------------------------------
1051   for (const auto& [surfaceId, record] : surfaceRecords) {
1052     auto surface = regularSurfaceFromJson(record.payload);
1053     surfacePointers.emplace(surfaceId, surface);
1054   }
1055 
1056   // Collect volume pointers
1057   std::unordered_map<std::size_t, std::unique_ptr<TrackingVolume>>
1058       volumeStorage;
1059   VolumePointerLookup volumePointers;
1060 
1061   for (const auto& [volumeId, record] : volumeRecords) {
1062     auto volumeBounds = m_cfg.decodeVolumeBounds(record.bounds);
1063     auto volume = std::make_unique<TrackingVolume>(
1064         record.transform, std::move(volumeBounds), record.name);
1065 
1066     GeometryIdentifier geometryId(record.geometryId);
1067     if (geometryId == GeometryIdentifier{}) {
1068       geometryId = GeometryIdentifier{}.withVolume(volumeId + 1u);
1069     }
1070     volume->assignGeometryId(geometryId);
1071     volumePointers.emplace(volumeId, volume.get());
1072     volumeStorage.emplace(volumeId, std::move(volume));
1073   }
1074 
1075   std::unordered_set<std::size_t> visiting;
1076   std::unordered_set<std::size_t> built;
1077 
1078   // Assemble the volume hierarchy
1079   auto attachChildren = [&](std::size_t volumeId, auto&& self) {
1080     if (built.contains(volumeId)) {
1081       return;
1082     }
1083     ACTS_VERBOSE("Assembling volume '" << volumeRecords.at(volumeId).name
1084                                        << "' (id=" << volumeId << ")");
1085     if (!visiting.insert(volumeId).second) {
1086       throw std::invalid_argument(
1087           "Cycle detected in serialized volume hierarchy");
1088     }
1089 
1090     const auto& parent = volumeStorage.at(volumeId);
1091     if (parent == nullptr) {
1092       throw std::invalid_argument("Volume was already moved unexpectedly");
1093     }
1094 
1095     for (std::size_t childId : volumeRecords.at(volumeId).children) {
1096       self(childId, self);
1097       auto& child = volumeStorage.at(childId);
1098       if (child == nullptr) {
1099         throw std::invalid_argument(
1100             "Serialized child volume has already been attached");
1101       }
1102       parent->addVolume(std::move(child));
1103     }
1104 
1105     visiting.erase(volumeId);
1106     built.insert(volumeId);
1107   };
1108 
1109   attachChildren(rootVolumeId, attachChildren);
1110   ACTS_DEBUG("Volume hierarchy assembled (" << built.size() << " volumes)");
1111   if (built.size() != volumeRecords.size()) {
1112     ACTS_WARNING(volumeRecords.size() - built.size()
1113                  << " volume(s) in the JSON are not reachable from the root "
1114                     "and were not assembled into the hierarchy");
1115   }
1116 
1117   auto decodePortal = [&](const nlohmann::json& jPortal) {
1118     std::unique_ptr<PortalLinkBase> along = nullptr;
1119     std::unique_ptr<PortalLinkBase> opposite = nullptr;
1120 
1121     if (!jPortal.at(kAlongNormalKey).is_null()) {
1122       along = portalLinkFromJson(jPortal.at(kAlongNormalKey), surfacePointers,
1123                                  volumePointers);
1124     }
1125     if (!jPortal.at(kOppositeNormalKey).is_null()) {
1126       opposite = portalLinkFromJson(jPortal.at(kOppositeNormalKey),
1127                                     surfacePointers, volumePointers);
1128     }
1129     if (along == nullptr && opposite == nullptr) {
1130       throw std::invalid_argument("Portal has no links");
1131     }
1132 
1133     auto portal =
1134         std::make_shared<Portal>(gctx, std::move(along), std::move(opposite));
1135     portal->surface().assignGeometryId(
1136         surfacePointers.at(jPortal.at(kSurfaceIdKey))->geometryId());
1137     return portal;
1138   };
1139 
1140   PortalPointerLookup portalPointers;
1141   for (const auto& [portalId, record] : portalRecords) {
1142     const auto inserted =
1143         portalPointers.emplace(portalId, decodePortal(record.payload));
1144     if (!inserted) {
1145       throw std::invalid_argument("Portal pointer reconstruction failed");
1146     }
1147   }
1148   ACTS_DEBUG("Decoded " << portalRecords.size() << " portals");
1149 
1150   for (const auto& [volumeId, record] : volumeRecords) {
1151     auto* volume = volumePointers.find(volumeId);
1152     if (volume == nullptr) {
1153       throw std::invalid_argument("Volume pointer reconstruction failed");
1154     }
1155 
1156     ACTS_VERBOSE("Attaching " << record.portalIds.size() << " portals and "
1157                               << record.surfaceIds.size()
1158                               << " surfaces to volume '" << record.name << "'");
1159 
1160     for (const std::size_t portalId : record.portalIds) {
1161       volume->addPortal(portalPointers.at(portalId));
1162     }
1163     for (const std::size_t surfaceId : record.surfaceIds) {
1164       volume->addSurface(surfacePointers.at(surfaceId));
1165     }
1166 
1167     volume->setNavigationPolicy(navigationPolicyFromJson(
1168         gctx, record.navigationPolicy, *volume, logger()));
1169   }
1170 
1171   auto root = std::move(volumeStorage.at(rootVolumeId));
1172   if (root == nullptr) {
1173     throw std::invalid_argument("Root volume reconstruction failed");
1174   }
1175 
1176   return std::shared_ptr<TrackingVolume>(std::move(root));
1177 }
1178 
1179 std::shared_ptr<Acts::TrackingGeometry>
1180 Acts::TrackingGeometryJsonConverter::fromJson(const GeometryContext& gctx,
1181                                               const nlohmann::json& encoded,
1182                                               const Options& options) const {
1183   ACTS_DEBUG("Reconstructing TrackingGeometry from JSON");
1184   auto world = trackingVolumeFromJson(gctx, encoded, options);
1185 
1186   GeometryIdentifier::Value nextVolumeId = 1u;
1187   ensureIdentifiers(*world, nextVolumeId);
1188 
1189   ACTS_DEBUG("TrackingGeometry reconstruction complete, root volume '"
1190              << world->volumeName() << "'");
1191   return std::make_shared<TrackingGeometry>(
1192       world, nullptr, GeometryIdentifierHook{}, getDummyLogger(), false);
1193 }