File indexing completed on 2026-10-09 08:30:26
0001
0002
0003
0004
0005
0006
0007
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
0076
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
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
0446
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& ) {
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& ) {
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& ) {
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& ) {
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& ) {
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& ) {
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 }
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 }