Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-09 08:30:26

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/TrackingGeometryMaterialJsonConverter.hpp"
0010 
0011 #include "Acts/Definitions/Units.hpp"
0012 #include "Acts/Material/BinnedSurfaceMaterial.hpp"
0013 #include "Acts/Material/GridSurfaceMaterial.hpp"
0014 #include "Acts/Material/HomogeneousSurfaceMaterial.hpp"
0015 #include "Acts/Material/MergedMaterialMarker.hpp"
0016 #include "Acts/Material/ProtoSurfaceMaterial.hpp"
0017 #include "Acts/Utilities/AxisSpec.hpp"
0018 #include "Acts/Utilities/BinningData.hpp"
0019 #include "ActsPlugins/Json/detail/JsonIo.hpp"
0020 
0021 #include <algorithm>
0022 #include <array>
0023 #include <bit>
0024 #include <cmath>
0025 #include <cstdint>
0026 #include <limits>
0027 #include <stdexcept>
0028 #include <string_view>
0029 #include <type_traits>
0030 
0031 namespace {
0032 using namespace Acts;
0033 using Converter = TrackingGeometryMaterialJsonConverter;
0034 using EncodeContext = Converter::EncodeContext;
0035 using DecodeContext = Converter::DecodeContext;
0036 constexpr double lengthUnit = UnitConstants::mm;
0037 constexpr double densityUnit =
0038     UnitConstants::mol / (lengthUnit * lengthUnit * lengthUnit);
0039 constexpr double energyUnit = UnitConstants::GeV;
0040 
0041 void check(bool condition, const std::string& message) {
0042   if (!condition) {
0043     throw std::invalid_argument(message);
0044   }
0045 }
0046 
0047 double unit(
0048     const nlohmann::json& units, const char* dimension,
0049     std::initializer_list<std::pair<std::string_view, double>> supported) {
0050   const auto name = units.at(dimension).get<std::string>();
0051   for (const auto& [symbol, factor] : supported) {
0052     if (name == symbol) {
0053       return factor;
0054     }
0055   }
0056   throw std::invalid_argument("unsupported " + std::string(dimension) +
0057                               " unit '" + name + "'");
0058 }
0059 
0060 void array(const nlohmann::json& j, std::size_t min,
0061            std::size_t max = std::numeric_limits<std::size_t>::max()) {
0062   check(j.is_array() && j.size() >= min && j.size() <= max,
0063         "invalid array size");
0064 }
0065 
0066 float finiteFloat(double v) {
0067   check(std::isfinite(v) && std::abs(v) <= std::numeric_limits<float>::max(),
0068         "value outside finite float range");
0069   auto f = static_cast<float>(v);
0070   check(v == 0 || f != 0, "value underflows float range");
0071   return f;
0072 }
0073 
0074 float quantize(float value, unsigned int fractionBits) {
0075   // Preserve signed zero, subnormals and infinity. Rounding subnormals by
0076   // clearing fraction bits would not obey the relative error bound.
0077   if (fractionBits == 23 || !std::isnormal(value)) {
0078     return value;
0079   }
0080   const unsigned int drop = 23 - fractionBits;
0081   const auto bits = std::bit_cast<std::uint32_t>(value);
0082   // Round to nearest, ties to even, including carries into the exponent.
0083   const auto rounded =
0084       (bits + ((1u << (drop - 1)) - 1) + ((bits >> drop) & 1u)) &
0085       ~((1u << drop) - 1);
0086   const auto result = std::bit_cast<float>(rounded);
0087   return std::isfinite(result) ? result : value;
0088 }
0089 
0090 void quantizeSlab(nlohmann::json& slab, unsigned int fractionBits) {
0091   slab.at("thickness") =
0092       quantize(slab.at("thickness").get<float>(), fractionBits);
0093   auto& material = slab.at("material");
0094   if (material.at("kind") == "material") {
0095     for (const auto* field :
0096          {"radiation_length", "interaction_length", "relative_atomic_mass",
0097           "atomic_number", "molar_density", "molar_electron_density",
0098           "mean_excitation_energy"}) {
0099       auto& value = material.at(field);
0100       if (value.is_number()) {
0101         value = quantize(value.get<float>(), fractionBits);
0102       }
0103     }
0104   }
0105 }
0106 
0107 void quantizeDocument(nlohmann::json& document, unsigned int fractionBits) {
0108   auto slabs = [fractionBits](nlohmann::json& values) {
0109     for (auto& slab : values) {
0110       quantizeSlab(slab, fractionBits);
0111     }
0112   };
0113   for (auto& entry : document.at("surfaces")) {
0114     auto& material = entry.at("material");
0115     if (material.is_null()) {
0116       continue;
0117     }
0118     const auto& kind = material.at("kind");
0119     if (kind == "homogeneous") {
0120       quantizeSlab(material.at("slab"), fractionBits);
0121     } else if (kind == "binned") {
0122       slabs(material.at("values"));
0123     } else if (kind == "grid") {
0124       auto& storage = material.at("storage");
0125       if (storage.at("kind") == "direct") {
0126         slabs(storage.at("values"));
0127       } else if (storage.at("kind") == "indexed") {
0128         slabs(storage.at("slabs"));
0129       }
0130     }
0131   }
0132   if (document.contains("slab_stores")) {
0133     for (auto& store : document.at("slab_stores")) {
0134       slabs(store);
0135     }
0136   }
0137 }
0138 
0139 std::size_t index(
0140     const nlohmann::json& j,
0141     std::size_t maximum = std::numeric_limits<std::size_t>::max()) {
0142   check(j.is_number_integer(), "expected integer");
0143   check(j.is_number_unsigned() || j.get<std::int64_t>() >= 0,
0144         "expected nonnegative integer");
0145   auto v = j.get<std::uint64_t>();
0146   check(v <= maximum, "integer out of range");
0147   return static_cast<std::size_t>(v);
0148 }
0149 
0150 std::size_t product(std::size_t a, std::size_t b) {
0151   check(b == 0 || a <= std::numeric_limits<std::size_t>::max() / b,
0152         "bin count overflow");
0153   return a * b;
0154 }
0155 
0156 nlohmann::json encodeId(GeometryIdentifier id) {
0157   nlohmann::json j = nlohmann::json::object();
0158   for (auto [name, value] :
0159        std::initializer_list<std::pair<const char*, std::uint64_t>>{
0160            {"volume", id.volume()},
0161            {"portal", id.boundary()},
0162            {"layer", id.layer()},
0163            {"passive", id.passive()},
0164            {"sensitive", id.sensitive()},
0165            {"extra", id.extra()}}) {
0166     if (value != 0) {
0167       j[name] = value;
0168     }
0169   }
0170   return j;
0171 }
0172 
0173 GeometryIdentifier decodeId(const nlohmann::json& j) {
0174   auto field = [&](const char* key, std::uint64_t max) {
0175     return j.contains(key) ? index(j.at(key), max) : 0;
0176   };
0177   return GeometryIdentifier()
0178       .withVolume(field("volume", GeometryIdentifier::getMaxVolume()))
0179       .withBoundary(field("portal", GeometryIdentifier::getMaxBoundary()))
0180       .withLayer(field("layer", GeometryIdentifier::getMaxLayer()))
0181       .withPassive(field("passive", GeometryIdentifier::getMaxApproach()))
0182       .withSensitive(field("sensitive", GeometryIdentifier::getMaxSensitive()))
0183       .withExtra(field("extra", GeometryIdentifier::getMaxExtra()));
0184 }
0185 
0186 const std::array<std::string, 9> directions{"x",    "y",     "z",   "r",  "phi",
0187                                             "rphi", "theta", "eta", "mag"};
0188 
0189 AxisDirection direction(const nlohmann::json& j) {
0190   const auto it = std::ranges::find(directions, j.get<std::string>());
0191   check(it != directions.end(), "unsupported axis direction");
0192   return static_cast<AxisDirection>(it - directions.begin());
0193 }
0194 
0195 std::string direction(AxisDirection d) {
0196   return directions.at(static_cast<std::size_t>(d));
0197 }
0198 
0199 double axisUnit(std::optional<AxisDirection> d, double length = lengthUnit,
0200                 double angle = UnitConstants::rad) {
0201   using enum AxisDirection;
0202   if (d == AxisPhi || d == AxisTheta) {
0203     return angle;
0204   }
0205   return d == AxisEta ? 1. : length;
0206 }
0207 
0208 AxisBoundaryType boundary(const nlohmann::json& j) {
0209   using enum AxisBoundaryType;
0210   auto s = j.get<std::string>();
0211   if (s == "open") {
0212     return Open;
0213   }
0214   if (s == "bound") {
0215     return Bound;
0216   }
0217   if (s == "closed") {
0218     return Closed;
0219   }
0220   throw std::invalid_argument("unsupported axis boundary '" + s + "'");
0221 }
0222 
0223 std::string boundary(AxisBoundaryType b) {
0224   using enum AxisBoundaryType;
0225   switch (b) {
0226     case Open:
0227       return "open";
0228     case Bound:
0229       return "bound";
0230     case Closed:
0231       return "closed";
0232   }
0233   throw std::invalid_argument("invalid boundary");
0234 }
0235 
0236 Transform3 decodeTransform(const nlohmann::json& j,
0237                            const DecodeContext& context) {
0238   array(j.at("rotation"), 3, 3);
0239   array(j.at("translation"), 3, 3);
0240   Transform3 t = Transform3::Identity();
0241   for (int r = 0; r < 3; ++r) {
0242     array(j.at("rotation").at(r), 3, 3);
0243     for (int c = 0; c < 3; ++c) {
0244       t.linear()(r, c) = j.at("rotation").at(r).at(c).get<double>();
0245     }
0246     t.translation()[r] =
0247         j.at("translation").at(r).get<double>() * context.lengthUnit();
0248   }
0249   return t;
0250 }
0251 
0252 nlohmann::json encodeTransform(const Transform3& t) {
0253   nlohmann::json j{{"rotation", nlohmann::json::array()},
0254                    {"translation", nlohmann::json::array()}};
0255   for (int r = 0; r < 3; ++r) {
0256     nlohmann::json row = nlohmann::json::array();
0257     for (int c = 0; c < 3; ++c) {
0258       row.emplace_back(t.linear()(r, c));
0259     }
0260     j["rotation"].emplace_back(row);
0261     j["translation"].emplace_back(t.translation()[r] / lengthUnit);
0262   }
0263   return j;
0264 }
0265 
0266 float decodeLength(const nlohmann::json& j, const DecodeContext& context) {
0267   if (j.is_string() && j == "infinity") {
0268     return std::numeric_limits<float>::infinity();
0269   }
0270   auto v = j.get<double>();
0271   return finiteFloat(v * context.lengthUnit());
0272 }
0273 
0274 nlohmann::json encodeLength(float v) {
0275   if (v == std::numeric_limits<float>::infinity()) {
0276     return "infinity";
0277   }
0278   return v / lengthUnit;
0279 }
0280 
0281 Material decodeMaterial(const nlohmann::json& j, const DecodeContext& context) {
0282   auto kind = j.at("kind").get<std::string>();
0283   if (kind == "vacuum") {
0284     return Material::Vacuum();
0285   }
0286   check(kind == "material", "unsupported composition kind");
0287   auto ar = finiteFloat(j.at("relative_atomic_mass").get<double>());
0288   return Material::fromMolarDensity(
0289       decodeLength(j.at("radiation_length"), context),
0290       decodeLength(j.at("interaction_length"), context), ar,
0291       finiteFloat(j.at("atomic_number").get<double>()),
0292       finiteFloat(j.at("molar_density").get<double>() * context.densityUnit()),
0293       finiteFloat(j.at("molar_electron_density").get<double>() *
0294                   context.densityUnit()),
0295       finiteFloat(j.at("mean_excitation_energy").get<double>() *
0296                   context.energyUnit()));
0297 }
0298 
0299 nlohmann::json encodeMaterial(const Material& m) {
0300   if (m.isVacuum()) {
0301     return nlohmann::json{{"kind", "vacuum"}};
0302   }
0303   nlohmann::json j{
0304       {"kind", "material"},
0305       {"radiation_length", encodeLength(m.X0())},
0306       {"interaction_length", encodeLength(m.L0())},
0307       {"relative_atomic_mass", m.Ar()},
0308       {"atomic_number", m.Z()},
0309       {"molar_density", m.molarDensity() / densityUnit},
0310       {"molar_electron_density", m.molarElectronDensity() / densityUnit},
0311       {"mean_excitation_energy", m.meanExcitationEnergy() / energyUnit}};
0312   return j;
0313 }
0314 
0315 MaterialSlab decodeSlab(const nlohmann::json& j, const DecodeContext& context) {
0316   return MaterialSlab(
0317       decodeMaterial(j.at("material"), context),
0318       finiteFloat(j.at("thickness").get<double>() * context.lengthUnit()));
0319 }
0320 
0321 nlohmann::json encodeSlab(const MaterialSlab& slab) {
0322   return {{"material", encodeMaterial(slab.material())},
0323           {"thickness", slab.thickness() / lengthUnit}};
0324 }
0325 
0326 std::vector<MaterialSlab> decodeSlabs(const nlohmann::json& j,
0327                                       const DecodeContext& context) {
0328   array(j, 1);
0329   std::vector<MaterialSlab> result;
0330   result.reserve(j.size());
0331   for (const auto& v : j) {
0332     result.emplace_back(decodeSlab(v, context));
0333   }
0334   return result;
0335 }
0336 
0337 nlohmann::json encodeSlabs(const std::vector<MaterialSlab>& slabs) {
0338   check(!slabs.empty(), "empty slab store");
0339   nlohmann::json result = nlohmann::json::array();
0340   for (const auto& s : slabs) {
0341     result.emplace_back(encodeSlab(s));
0342   }
0343   return result;
0344 }
0345 
0346 constexpr std::array<std::string_view, 4> mappingNames{"pre", "default", "post",
0347                                                        "sensor"};
0348 
0349 std::pair<double, MappingType> settings(const nlohmann::json& j) {
0350   const auto name = j.at("mapping_type").get<std::string>();
0351   const auto it = std::ranges::find(mappingNames, name);
0352   check(it != mappingNames.end(), "unsupported mapping type");
0353   return {j.at("split_factor").get<double>(),
0354           static_cast<MappingType>(it - mappingNames.begin() - 1)};
0355 }
0356 
0357 nlohmann::json settings(const ISurfaceMaterial& m) {
0358   return {
0359       {"mapping_type", mappingNames.at(static_cast<int>(m.mappingType()) + 1)},
0360       {"split_factor",
0361        m.factor(Direction::Backward(), MaterialUpdateMode::PreUpdate)}};
0362 }
0363 
0364 AxisSpec decodeAxis(const nlohmann::json& j, bool deferred,
0365                     const DecodeContext& context) {
0366   const auto kind = j.at("kind").get<std::string>();
0367   std::optional<AxisDirection> d;
0368   std::optional<AxisBoundaryType> b;
0369   if (j.contains("direction")) {
0370     d = direction(j.at("direction"));
0371   }
0372   if (j.contains("boundary")) {
0373     b = boundary(j.at("boundary"));
0374   }
0375   check(deferred || b.has_value(), "resolved axis requires boundary");
0376   if (kind == "equidistant") {
0377     const auto n =
0378         index(j.at("bins"), std::numeric_limits<std::size_t>::max() - 2);
0379     std::optional<double> min;
0380     std::optional<double> max;
0381     if (j.contains("range")) {
0382       array(j.at("range"), 2, 2);
0383       min = j.at("range").at(0).get<double>() *
0384             axisUnit(d, context.lengthUnit(), context.angleUnit());
0385       max = j.at("range").at(1).get<double>() *
0386             axisUnit(d, context.lengthUnit(), context.angleUnit());
0387     }
0388     check(deferred || min.has_value(), "resolved axis requires range");
0389     return AxisSpec::Equidistant(n, min, max, b, d);
0390   }
0391   check(kind == "variable" || (deferred && kind == "deferred-variable"),
0392         "unsupported axis kind");
0393   const std::string field = kind == "variable" ? "edges" : "normalized_edges";
0394   array(j.at(field), 2);
0395   std::vector<double> edges;
0396   for (const auto& e : j.at(field)) {
0397     edges.emplace_back(e.get<double>() *
0398                        (kind == "variable" ? axisUnit(d, context.lengthUnit(),
0399                                                       context.angleUnit())
0400                                            : 1.));
0401   }
0402   return kind == "variable" ? AxisSpec::Variable(edges, b, d)
0403                             : AxisSpec::DeferredVariable(edges, b, d);
0404 }
0405 
0406 nlohmann::json encodeAxis(const AxisSpec& axis) {
0407   nlohmann::json j;
0408   if (axis.isEquidistant()) {
0409     const auto& a = axis.asEquidistant();
0410     check(a.min.has_value() == a.max.has_value(),
0411           "partially specified range is not representable");
0412     j = {{"kind", "equidistant"}, {"bins", a.nBins}};
0413     if (a.min.has_value()) {
0414       j["range"] = {*a.min / axisUnit(axis.direction()),
0415                     *a.max / axisUnit(axis.direction())};
0416     }
0417   } else if (axis.isDeferredVariable()) {
0418     j = {{"kind", "deferred-variable"},
0419          {"normalized_edges", axis.asDeferredVariable().normalizedEdges}};
0420   } else {
0421     auto edges = axis.asVariable().edges;
0422     for (auto& v : edges) {
0423       v /= axisUnit(axis.direction());
0424     }
0425     j = {{"kind", "variable"}, {"edges", edges}};
0426   }
0427   if (const auto d = axis.direction(); d) {
0428     j["direction"] = direction(*d);
0429   }
0430   if (const auto b = axis.boundaryType(); b) {
0431     j["boundary"] = boundary(*b);
0432   }
0433   return j;
0434 }
0435 
0436 BinningData decodeBinAxis(const nlohmann::json& j, const DecodeContext& context,
0437                           unsigned int depth = 0) {
0438   using enum AxisDirection;
0439   check(depth < 32, "binning refinement nesting exceeds 32");
0440   if (j.at("kind") != "subdivided") {
0441     auto spec = decodeAxis(j, false, context);
0442     check(spec.direction().has_value(), "BinUtility axis requires direction");
0443     check(spec.boundaryType() != AxisBoundaryType::Open,
0444           "BinUtility has no guard cells");
0445     // Theta and magnitude have historical global-projection semantics that do
0446     // not match the physical coordinate names in the new format.
0447     check(spec.direction() != AxisTheta && spec.direction() != AxisMag,
0448           "legacy theta/mag BinUtility projection is not supported by this "
0449           "format");
0450     auto axis = spec.buildAxis();
0451     std::optional<float> previous;
0452     for (double e : axis->getBinEdges()) {
0453       const float edge = finiteFloat(e);
0454       check(!previous || edge > *previous,
0455             "BinUtility edges collapse at float precision");
0456       previous = edge;
0457     }
0458     return BinningData(*axis);
0459   }
0460   check(j.at("base").at("kind") != "subdivided",
0461         "refinement base must be resolved");
0462   auto base = decodeBinAxis(j.at("base"), context, depth + 1);
0463   auto sub = decodeBinAxis(j.at("subdivision"), context, depth + 1);
0464   check(base.binvalue == sub.binvalue && base.option == sub.option,
0465         "refinement axis mismatch");
0466   auto mode = j.at("mode").get<std::string>();
0467   check(mode == "replace" || mode == "repeat", "invalid refinement mode");
0468   const auto& edges = base.boundaries();
0469   if (mode == "replace") {
0470     bool match = false;
0471     for (std::size_t i = 1; i < edges.size(); ++i) {
0472       match |= sub.min == edges[i - 1] && sub.max == edges[i];
0473     }
0474     check(match, "replacement must match one base interval");
0475     check(base.bins() <= std::numeric_limits<std::size_t>::max() - sub.bins(),
0476           "refinement count overflow");
0477   } else {
0478     check(base.type == equidistant && sub.min == base.min &&
0479               sub.max == edges.at(1),
0480           "repeat needs an equidistant base and first-interval subdivision");
0481     product(base.bins(), sub.bins());
0482   }
0483   auto child = std::make_unique<const BinningData>(sub);
0484   if (base.type == equidistant) {
0485     return BinningData(base.option, base.binvalue, base.bins(), base.min,
0486                        base.max, std::move(child), mode == "replace");
0487   }
0488   return BinningData(base.option, base.binvalue, edges, std::move(child));
0489 }
0490 
0491 nlohmann::json encodeBinAxis(const BinningData& b) {
0492   BinningData base(b);
0493   base.subBinningData.reset();
0494   const auto& rawEdges = base.boundaries();
0495   nlohmann::json j{{"boundary", b.option == closed ? "closed" : "bound"},
0496                    {"direction", direction(b.binvalue)}};
0497   if (b.type == equidistant) {
0498     j["kind"] = "equidistant";
0499     j["bins"] = rawEdges.size() - 1;
0500     j["range"] = {b.min / axisUnit(b.binvalue), b.max / axisUnit(b.binvalue)};
0501   } else {
0502     j["kind"] = "variable";
0503     j["edges"] = nlohmann::json::array();
0504     for (float e : rawEdges) {
0505       j["edges"].emplace_back(e / axisUnit(b.binvalue));
0506     }
0507   }
0508   if (b.subBinningData) {
0509     j = {{"kind", "subdivided"},
0510          {"base", j},
0511          {"mode", b.subBinningAdditive ? "replace" : "repeat"},
0512          {"subdivision", encodeBinAxis(*b.subBinningData)}};
0513   }
0514   const auto decoded = decodeBinAxis(j, DecodeContext{});
0515   check(decoded.bins() == b.bins(), "refinement bin count cannot be preserved");
0516   return j;
0517 }
0518 
0519 BinUtility decodeBinning(const nlohmann::json& j, std::size_t min,
0520                          std::size_t max, const DecodeContext& context) {
0521   array(j.at("axes"), min, max);
0522   BinUtility b(j.contains("transform")
0523                    ? decodeTransform(j.at("transform"), context)
0524                    : Transform3::Identity());
0525   for (const auto& a : j.at("axes")) {
0526     b += BinUtility(decodeBinAxis(a, context));
0527   }
0528   return b;
0529 }
0530 
0531 nlohmann::json encodeBinning(const BinUtility& b) {
0532   nlohmann::json j{{"axes", nlohmann::json::array()}};
0533   for (const auto& a : b.binningData()) {
0534     j["axes"].emplace_back(encodeBinAxis(a));
0535   }
0536   if (!b.transform().isApprox(Transform3::Identity())) {
0537     j["transform"] = encodeTransform(b.transform());
0538   }
0539   return j;
0540 }
0541 
0542 std::optional<std::string> materialKey(const nlohmann::json& j) {
0543   return j.contains("material_key")
0544              ? std::optional(j.at("material_key").get<std::string>())
0545              : std::nullopt;
0546 }
0547 
0548 void checkKey(const ISurfaceMaterial& material, const std::string& key) {
0549   auto checkProto = [&](const auto* p) {
0550     if (p != nullptr && p->materialKey()) {
0551       check(*p->materialKey() == key,
0552             "proto material key disagrees with assignment");
0553     }
0554   };
0555   checkProto(dynamic_cast<const ProtoSurfaceMaterial*>(&material));
0556   checkProto(dynamic_cast<const ProtoGridSurfaceMaterial*>(&material));
0557 }
0558 
0559 nlohmann::json encodeHomogeneousSurface(const HomogeneousSurfaceMaterial& m,
0560                                         EncodeContext& /*context*/) {
0561   return {{"kind", "homogeneous"},
0562           {"settings", settings(m)},
0563           {"slab", encodeSlab(m.materialSlab())}};
0564 }
0565 
0566 std::unique_ptr<const ISurfaceMaterial> decodeHomogeneousSurface(
0567     const nlohmann::json& j, const DecodeContext& context) {
0568   auto [split, mapping] = settings(j.at("settings"));
0569   return std::make_unique<HomogeneousSurfaceMaterial>(
0570       decodeSlab(j.at("slab"), context), split, mapping);
0571 }
0572 
0573 nlohmann::json encodeBinned(const BinnedSurfaceMaterial& m,
0574                             EncodeContext& /*context*/) {
0575   nlohmann::json values = nlohmann::json::array();
0576   for (const auto& row : m.fullMaterial()) {
0577     for (const auto& s : row) {
0578       values.emplace_back(encodeSlab(s));
0579     }
0580   }
0581   return {{"kind", "binned"},
0582           {"settings", settings(m)},
0583           {"binning", encodeBinning(m.binUtility())},
0584           {"values", values}};
0585 }
0586 
0587 std::unique_ptr<const ISurfaceMaterial> decodeBinned(
0588     const nlohmann::json& j, const DecodeContext& context) {
0589   auto [split, mapping] = settings(j.at("settings"));
0590   auto bins = decodeBinning(j.at("binning"), 1, 2, context);
0591   auto n0 = bins.binningData()[0].bins();
0592   auto n1 = bins.dimensions() == 2 ? bins.binningData()[1].bins() : 1;
0593   auto count = product(n0, n1);
0594   array(j.at("values"), count, count);
0595   MaterialSlabMatrix matrix(n1);
0596   for (std::size_t i1 = 0; i1 < n1; ++i1) {
0597     for (std::size_t i0 = 0; i0 < n0; ++i0) {
0598       matrix[i1].emplace_back(
0599           decodeSlab(j.at("values").at(i0 + n0 * i1), context));
0600     }
0601   }
0602   return std::make_unique<BinnedSurfaceMaterial>(bins, std::move(matrix), split,
0603                                                  mapping);
0604 }
0605 
0606 nlohmann::json encodeProtoSurface(const ProtoSurfaceMaterial& m,
0607                                   EncodeContext& /*context*/) {
0608   nlohmann::json j{{"kind", "proto"},
0609                    {"settings", settings(m)},
0610                    {"binning", encodeBinning(m.binning())}};
0611   if (const auto& key = m.materialKey(); key) {
0612     j["material_key"] = *key;
0613   }
0614   return j;
0615 }
0616 
0617 std::unique_ptr<const ISurfaceMaterial> decodeProtoSurface(
0618     const nlohmann::json& j, const DecodeContext& context) {
0619   auto [split, mapping] = settings(j.at("settings"));
0620   check(split == 1, "proto surface split factor must be one");
0621   return std::make_unique<ProtoSurfaceMaterial>(
0622       decodeBinning(j.at("binning"), 0, 2, context), mapping, materialKey(j));
0623 }
0624 
0625 nlohmann::json encodeProtoGrid(const ProtoGridSurfaceMaterial& m,
0626                                EncodeContext& /*context*/) {
0627   nlohmann::json j{{"kind", "proto-grid"},
0628                    {"settings", settings(m)},
0629                    {"axes", nlohmann::json::array()}};
0630   for (const auto& a : m.binning().axisSpecs()) {
0631     j["axes"].emplace_back(encodeAxis(a));
0632   }
0633   if (const auto& key = m.materialKey(); key) {
0634     j["material_key"] = *key;
0635   }
0636   return j;
0637 }
0638 
0639 std::unique_ptr<const ISurfaceMaterial> decodeProtoGrid(
0640     const nlohmann::json& j, const DecodeContext& context) {
0641   array(j.at("axes"), 2, 2);
0642   auto [split, mapping] = settings(j.at("settings"));
0643   check(split == 1, "proto grid split factor must be one");
0644   MultiAxisSpec2D axes(
0645       std::array<AxisSpec, 2>{decodeAxis(j.at("axes").at(0), true, context),
0646                               decodeAxis(j.at("axes").at(1), true, context)});
0647   return std::make_unique<ProtoGridSurfaceMaterial>(axes, mapping,
0648                                                     materialKey(j));
0649 }
0650 
0651 nlohmann::json encodeMarker(const MergedMaterialMarker& m,
0652                             EncodeContext& /*context*/) {
0653   nlohmann::json origins = nlohmann::json::array();
0654   for (const auto& origin : m.origins()) {
0655     nlohmann::json j{{"geometry_id", encodeId(origin.geometryId)}};
0656     if (origin.materialKey) {
0657       check(!origin.materialKey->empty(), "empty origin key");
0658       j["material_key"] = *origin.materialKey;
0659     }
0660     origins.emplace_back(j);
0661   }
0662   return {{"kind", "merged-material-marker"}, {"origins", origins}};
0663 }
0664 
0665 std::unique_ptr<const ISurfaceMaterial> decodeMarker(
0666     const nlohmann::json& j, const DecodeContext& /*context*/) {
0667   array(j.at("origins"), 0);
0668   std::vector<MergedMaterialMarker::Origin> origins;
0669   for (const auto& origin : j.at("origins")) {
0670     origins.emplace_back(decodeId(origin.at("geometry_id")),
0671                          materialKey(origin));
0672   }
0673   return std::make_unique<MergedMaterialMarker>(origins);
0674 }
0675 
0676 template <typename Storage>
0677 nlohmann::json encodeGridStorage(const Storage& s, const GridSurfaceMaterial& m,
0678                                  EncodeContext& context) {
0679   nlohmann::json storage;
0680   const auto n = m.multiAxis().getNBins();
0681   if constexpr (std::is_same_v<Storage, GridSurfaceMaterial::Direct>) {
0682     storage = {{"kind", "direct"}, {"values", nlohmann::json::array()}};
0683   } else if constexpr (std::is_same_v<Storage, GridSurfaceMaterial::Indexed>) {
0684     storage = {{"kind", "indexed"},
0685                {"slabs", encodeSlabs(s.material)},
0686                {"indices", nlohmann::json::array()}};
0687   } else {
0688     storage = {{"kind", "globally-indexed"},
0689                {"store", context.storeId(s.material)},
0690                {"indices", nlohmann::json::array()}};
0691   }
0692   for (std::size_t i1 = 0; i1 < n[1] + 2; ++i1) {
0693     for (std::size_t i0 = 0; i0 < n[0] + 2; ++i0) {
0694       auto bin = m.multiAxis().getGlobalBinFromLocalBins({i0, i1});
0695       if constexpr (std::is_same_v<Storage, GridSurfaceMaterial::Direct>) {
0696         storage["values"].emplace_back(encodeSlab(s.at(bin)));
0697       } else {
0698         const auto size = [&] {
0699           if constexpr (std::is_same_v<Storage, GridSurfaceMaterial::Indexed>) {
0700             return s.material.size();
0701           } else {
0702             return s.material->size();
0703           }
0704         }();
0705         check(s.indices.at(bin) < size, "grid slab index out of range");
0706         storage["indices"].emplace_back(s.indices.at(bin));
0707       }
0708     }
0709   }
0710   return storage;
0711 }
0712 
0713 nlohmann::json encodeGrid(const GridSurfaceMaterial& m,
0714                           EncodeContext& context) {
0715   nlohmann::json axes = nlohmann::json::array();
0716   for (const auto& a : m.binning().axisSpecs()) {
0717     axes.emplace_back(encodeAxis(a));
0718   }
0719   const auto storage = std::visit(
0720       [&](const auto& value) { return encodeGridStorage(value, m, context); },
0721       m.storage());
0722   return {{"kind", "grid"},
0723           {"settings", settings(m)},
0724           {"axes", axes},
0725           {"storage", storage}};
0726 }
0727 
0728 std::unique_ptr<const ISurfaceMaterial> decodeGrid(
0729     const nlohmann::json& j, const DecodeContext& context) {
0730   array(j.at("axes"), 2, 2);
0731   auto [split, mapping] = settings(j.at("settings"));
0732   MultiAxisSpec2D spec(
0733       std::array<AxisSpec, 2>{decodeAxis(j.at("axes").at(0), false, context),
0734                               decodeAxis(j.at("axes").at(1), false, context)});
0735   auto axes = spec.buildMultiAxis();
0736   const auto n = axes->getNBins();
0737   auto count = product(n[0] + 2, n[1] + 2);
0738   const auto& storage = j.at("storage");
0739   GridSurfaceMaterial::Storage result;
0740   if (const auto kind = storage.at("kind").get<std::string>();
0741       kind == "direct") {
0742     array(storage.at("values"), count, count);
0743     GridSurfaceMaterial::Direct slabs(count);
0744     for (std::size_t i1 = 0; i1 < n[1] + 2; ++i1) {
0745       for (std::size_t i0 = 0; i0 < n[0] + 2; ++i0) {
0746         slabs[axes->getGlobalBinFromLocalBins({i0, i1})] =
0747             decodeSlab(storage.at("values").at(i0 + (n[0] + 2) * i1), context);
0748       }
0749     }
0750     result = std::move(slabs);
0751   } else {
0752     Converter::SlabStore shared;
0753     std::vector<MaterialSlab> local;
0754     if (kind == "indexed") {
0755       local = decodeSlabs(storage.at("slabs"), context);
0756     } else {
0757       check(kind == "globally-indexed", "unknown grid storage kind");
0758       const auto name = storage.at("store").get<std::string>();
0759       shared = context.store(name);
0760     }
0761     const auto size = shared ? shared->size() : local.size();
0762     check(size != 0, "empty slab store");
0763     array(storage.at("indices"), count, count);
0764     std::vector<std::size_t> indices(count);
0765     for (std::size_t i1 = 0; i1 < n[1] + 2; ++i1) {
0766       for (std::size_t i0 = 0; i0 < n[0] + 2; ++i0) {
0767         indices[axes->getGlobalBinFromLocalBins({i0, i1})] =
0768             index(storage.at("indices").at(i0 + (n[0] + 2) * i1), size - 1);
0769       }
0770     }
0771     if (shared) {
0772       result = GridSurfaceMaterial::GloballyIndexed{std::move(indices),
0773                                                     std::move(shared)};
0774     } else {
0775       result =
0776           GridSurfaceMaterial::Indexed{std::move(indices), std::move(local)};
0777     }
0778   }
0779   return std::make_unique<GridSurfaceMaterial>(
0780       std::move(spec), std::move(result), split, mapping);
0781 }
0782 
0783 }  // namespace
0784 
0785 namespace Acts {
0786 
0787 TrackingGeometryMaterialJsonConverter::DecodeContext::DecodeContext()
0788     : m_units{::lengthUnit, UnitConstants::rad, ::energyUnit,
0789               UnitConstants::mol} {}
0790 
0791 double TrackingGeometryMaterialJsonConverter::EncodeContext::lengthUnit()
0792     const {
0793   return ::lengthUnit;
0794 }
0795 
0796 double TrackingGeometryMaterialJsonConverter::EncodeContext::angleUnit() const {
0797   return UnitConstants::rad;
0798 }
0799 
0800 double TrackingGeometryMaterialJsonConverter::EncodeContext::energyUnit()
0801     const {
0802   return ::energyUnit;
0803 }
0804 
0805 double
0806 TrackingGeometryMaterialJsonConverter::EncodeContext::materialAmountUnit()
0807     const {
0808   return UnitConstants::mol;
0809 }
0810 
0811 double TrackingGeometryMaterialJsonConverter::EncodeContext::densityUnit()
0812     const {
0813   return materialAmountUnit() / (lengthUnit() * lengthUnit() * lengthUnit());
0814 }
0815 
0816 double TrackingGeometryMaterialJsonConverter::DecodeContext::lengthUnit()
0817     const {
0818   return m_units[0];
0819 }
0820 
0821 double TrackingGeometryMaterialJsonConverter::DecodeContext::angleUnit() const {
0822   return m_units[1];
0823 }
0824 
0825 double TrackingGeometryMaterialJsonConverter::DecodeContext::energyUnit()
0826     const {
0827   return m_units[2];
0828 }
0829 
0830 double
0831 TrackingGeometryMaterialJsonConverter::DecodeContext::materialAmountUnit()
0832     const {
0833   return m_units[3];
0834 }
0835 
0836 double TrackingGeometryMaterialJsonConverter::DecodeContext::densityUnit()
0837     const {
0838   return materialAmountUnit() / (lengthUnit() * lengthUnit() * lengthUnit());
0839 }
0840 
0841 std::string TrackingGeometryMaterialJsonConverter::EncodeContext::storeId(
0842     const SlabStore& store) {
0843   check(store != nullptr && !store->empty(), "null or empty shared slab store");
0844   for (const auto& [id, existing] : m_stores) {
0845     if (existing.get() == store.get()) {
0846       return id;
0847     }
0848   }
0849   std::string id = "store-" + std::to_string(m_stores.size());
0850   while (m_stores.contains(id)) {
0851     id += "-";
0852   }
0853   m_stores.try_emplace(id, store);
0854   return id;
0855 }
0856 
0857 TrackingGeometryMaterialJsonConverter::SlabStore
0858 TrackingGeometryMaterialJsonConverter::DecodeContext::store(
0859     const std::string& name) const {
0860   const auto found = m_stores.find(name);
0861   check(found != m_stores.end(), "unresolved slab store '" + name + "'");
0862   return found->second;
0863 }
0864 
0865 TrackingGeometryMaterialJsonConverter::Config
0866 TrackingGeometryMaterialJsonConverter::Config::defaultConfig() {
0867   Config c;
0868   c.encodeSurface.registerFunction(encodeHomogeneousSurface)
0869       .registerFunction(encodeBinned)
0870       .registerFunction(encodeGrid)
0871       .registerFunction(encodeProtoSurface)
0872       .registerFunction(encodeProtoGrid)
0873       .registerFunction(encodeMarker);
0874   c.decodeSurface.registerKind("homogeneous", decodeHomogeneousSurface)
0875       .registerKind("binned", decodeBinned)
0876       .registerKind("grid", decodeGrid)
0877       .registerKind("proto", decodeProtoSurface)
0878       .registerKind("proto-grid", decodeProtoGrid)
0879       .registerKind("merged-material-marker", decodeMarker);
0880   return c;
0881 }
0882 
0883 TrackingGeometryMaterialJsonConverter::TrackingGeometryMaterialJsonConverter(
0884     Config config)
0885     : m_config(std::move(config)) {}
0886 
0887 nlohmann::json TrackingGeometryMaterialJsonConverter::toJson(
0888     const TrackingGeometryMaterial& material, const Options& options) const {
0889   check(options.materialFractionBits <= 23,
0890         "material fraction bits must be between 0 and 23");
0891   check(material.volumeMaterials.empty(),
0892         "material document version 1 supports surface material only; "
0893         "volume assignments cannot be serialized");
0894   nlohmann::json j{{"$schema", "urn:acts:material-map:1"},
0895                    {"header",
0896                     {{"format", "acts-material-map"},
0897                      {"version", 1},
0898                      {"units",
0899                       {{"length", "mm"},
0900                        {"angle", "rad"},
0901                        {"energy", "GeV"},
0902                        {"material_amount", "mol"}}}}},
0903                    {"surfaces", nlohmann::json::array()}};
0904   if (material.description()) {
0905     j["header"]["description"] = *material.description();
0906   }
0907   EncodeContext context;
0908   for (const auto& [id, payload] : material.surfaceMaterials) {
0909     nlohmann::json value = payload ? m_config.encodeSurface(*payload, context)
0910                                    : nlohmann::json(nullptr);
0911     j["surfaces"].emplace_back(nlohmann::json{
0912         {"target", {{"kind", "geometry-id"}, {"geometry_id", encodeId(id)}}},
0913         {"material", value}});
0914   }
0915   for (const auto& [key, assignment] : material.keyedSurfaces) {
0916     check(!key.empty() && assignment.material != nullptr,
0917           "keyed assignment needs nonempty key and material");
0918     checkKey(*assignment.material, key);
0919     auto value = m_config.encodeSurface(*assignment.material, context);
0920     j["surfaces"].emplace_back(nlohmann::json{
0921         {"target",
0922          {{"kind", "stable-key"},
0923           {"key", key},
0924           {"recorded_geometry_id", encodeId(assignment.geometryId)}}},
0925         {"material", value}});
0926   }
0927   if (!context.m_stores.empty()) {
0928     j["slab_stores"] = nlohmann::json::object();
0929     for (const auto& [id, store] : context.m_stores) {
0930       check(store != nullptr, "null slab store");
0931       j["slab_stores"][id] = encodeSlabs(*store);
0932     }
0933   }
0934   if (options.materialFractionBits < 23) {
0935     quantizeDocument(j, options.materialFractionBits);
0936   }
0937   return j;
0938 }
0939 
0940 TrackingGeometryMaterial TrackingGeometryMaterialJsonConverter::fromJson(
0941     const nlohmann::json& encoded) const {
0942   check(!encoded.contains("volumes"), "version 1 does not support volumes");
0943   if (encoded.contains("Surfaces") || encoded.contains("Volumes") ||
0944       encoded.contains("acts-geometry-hierarchy-map")) {
0945     throw std::invalid_argument(
0946         "Legacy material format is not supported by "
0947         "TrackingGeometryMaterialJsonConverter. "
0948         "Convert the file with ActsMaterialMapMigrate <input> <output> first.");
0949   }
0950   check(
0951       encoded.contains("header"),
0952       "Material document is missing the required header (format and version)");
0953   const auto& header = encoded.at("header");
0954   check(header.at("format") == "acts-material-map",
0955         "unsupported material format");
0956   check(index(header.at("version")) == 1,
0957         "unsupported material document version");
0958   TrackingGeometryMaterial result;
0959   if (header.contains("description")) {
0960     result.setDescription(header.at("description").get<std::string>());
0961   }
0962   DecodeContext context;
0963   const auto& units = header.at("units");
0964   using namespace UnitConstants;
0965   context.m_units = {
0966       unit(units, "length",
0967            {{"nm", nm}, {"um", um}, {"mm", mm}, {"cm", cm}, {"m", m}}),
0968       unit(units, "angle", {{"rad", rad}, {"mrad", mrad}, {"deg", degree}}),
0969       unit(
0970           units, "energy",
0971           {{"eV", eV}, {"keV", keV}, {"MeV", MeV}, {"GeV", GeV}, {"TeV", TeV}}),
0972       unit(units, "material_amount",
0973            {{"mol", mol}, {"mmol", 1e-3 * mol}, {"kmol", 1e3 * mol}})};
0974   if (encoded.contains("slab_stores")) {
0975     for (const auto& [name, slabs] :
0976          encoded.at("slab_stores").get_ref<const nlohmann::json::object_t&>()) {
0977       check(!name.empty(), "empty slab store name");
0978       context.m_stores.try_emplace(name,
0979                                    std::make_shared<std::vector<MaterialSlab>>(
0980                                        decodeSlabs(slabs, context)));
0981     }
0982   }
0983   array(encoded.at("surfaces"), 0);
0984   for (const auto& entry : encoded.at("surfaces")) {
0985     const auto& target = entry.at("target");
0986     const auto kind = target.at("kind").get<std::string>();
0987     std::shared_ptr<const ISurfaceMaterial> payload;
0988     if (!entry.at("material").is_null()) {
0989       payload = m_config.decodeSurface(entry.at("material"), context);
0990       check(payload != nullptr,
0991             "decoder returned null for a non-null surface payload");
0992     }
0993     if (kind == "geometry-id") {
0994       check(result.surfaceMaterials
0995                 .try_emplace(decodeId(target.at("geometry_id")),
0996                              std::move(payload))
0997                 .second,
0998             "duplicate surface geometry ID");
0999     } else {
1000       check(kind == "stable-key", "unsupported surface target kind");
1001       const auto key = target.at("key").get<std::string>();
1002       check(payload != nullptr, "keyed material must not be null");
1003       checkKey(*payload, key);
1004       check(result.keyedSurfaces
1005                 .try_emplace(key, decodeId(target.at("recorded_geometry_id")),
1006                              std::move(payload))
1007                 .second,
1008             "duplicate stable key");
1009     }
1010   }
1011   return result;
1012 }
1013 
1014 void TrackingGeometryMaterialJsonConverter::toFile(
1015     const TrackingGeometryMaterial& material, const std::filesystem::path& path,
1016     const Options& options) const {
1017   detail::writeJsonFile(path, toJson(material, options), options.indentation,
1018                         options.compressionLevel);
1019 }
1020 
1021 TrackingGeometryMaterial TrackingGeometryMaterialJsonConverter::fromFile(
1022     const std::filesystem::path& path) const {
1023   return fromJson(detail::readJsonFile(path));
1024 }
1025 }  // namespace Acts