File indexing completed on 2026-10-20 08:42:20
0001
0002
0003
0004
0005
0006
0007
0008
0009 #include <boost/test/unit_test.hpp>
0010
0011 #include "Acts/Definitions/Units.hpp"
0012 #include "Acts/Material/GridSurfaceMaterial.hpp"
0013 #include "Acts/Material/HomogeneousSurfaceMaterial.hpp"
0014 #include "Acts/Material/HomogeneousVolumeMaterial.hpp"
0015 #include "Acts/Material/ProtoSurfaceMaterial.hpp"
0016 #include "ActsPlugins/Json/TrackingGeometryMaterialJsonConverter.hpp"
0017 #include "ActsPlugins/Json/detail/JsonIo.hpp"
0018 #include "ActsTests/CommonHelpers/TemporaryDirectory.hpp"
0019
0020 #include <algorithm>
0021 #include <array>
0022 #include <bit>
0023 #include <cmath>
0024 #include <cstdint>
0025 #include <cstdlib>
0026 #include <filesystem>
0027 #include <fstream>
0028 #include <limits>
0029 #include <string>
0030
0031 using namespace Acts;
0032 using Converter = TrackingGeometryMaterialJsonConverter;
0033
0034 namespace ActsTests {
0035 namespace {
0036 std::filesystem::path examples() {
0037 const auto& master = boost::unit_test::framework::master_test_suite();
0038 BOOST_REQUIRE_EQUAL(master.argc, 2);
0039 return master.argv[1];
0040 }
0041 nlohmann::json fixture(const char* name) {
0042 return detail::readJsonFile(examples() / name);
0043 }
0044
0045 class CustomMaterial final : public ISurfaceMaterial {
0046 public:
0047 using ISurfaceMaterial::materialSlab;
0048 explicit CustomMaterial(double thickness)
0049 : m_slab(MaterialSlab::Vacuum(static_cast<float>(thickness))) {}
0050 CustomMaterial& scale(double factor) override {
0051 m_slab.scaleThickness(static_cast<float>(factor));
0052 return *this;
0053 }
0054 const MaterialSlab& materialSlab(
0055 const Vector2& ) const override {
0056 return m_slab;
0057 }
0058 std::vector<AxisDirection> localAxisDirections() const override { return {}; }
0059 std::ostream& toStream(std::ostream& out) const override {
0060 return out << "custom";
0061 }
0062
0063 private:
0064 MaterialSlab m_slab;
0065 };
0066 nlohmann::json encodeCustom(const CustomMaterial& m,
0067 Converter::EncodeContext& context) {
0068 return {{"kind", "application-custom"},
0069 {"thickness",
0070 m.materialSlab(Vector2::Zero()).thickness() / context.lengthUnit()}};
0071 }
0072 std::unique_ptr<const ISurfaceMaterial> decodeCustom(
0073 const nlohmann::json& j, const Converter::DecodeContext& context) {
0074 return std::make_unique<CustomMaterial>(j.at("thickness").get<double>() *
0075 context.lengthUnit());
0076 }
0077 }
0078
0079 BOOST_AUTO_TEST_SUITE(JsonSuite)
0080 BOOST_AUTO_TEST_CASE(MaterialDocumentExamples) {
0081 Converter converter;
0082 for (const auto* name : {"minimal.json", "surfaces.json", "templates.json"}) {
0083 BOOST_TEST_CONTEXT(name) {
0084 const auto decoded = converter.fromFile(examples() / name);
0085 const auto encoded = converter.toJson(decoded);
0086 BOOST_CHECK(encoded == converter.toJson(converter.fromJson(encoded)));
0087
0088 if (const char* output =
0089 std::getenv("ACTS_MATERIAL_DOCUMENT_OUTPUT_DIR")) {
0090 std::filesystem::create_directories(output);
0091 std::ofstream(std::filesystem::path(output) / name)
0092 << encoded.dump(4) << '\n';
0093 }
0094 }
0095 }
0096 const auto surfaces = converter.fromJson(fixture("surfaces.json"));
0097 BOOST_REQUIRE(surfaces.description());
0098 BOOST_CHECK_EQUAL(*surfaces.description(), fixture("surfaces.json")
0099 .at("header")
0100 .at("description")
0101 .get<std::string>());
0102 const auto* a = dynamic_cast<const GridSurfaceMaterial*>(
0103 surfaces.keyedSurfaces.at("barrel/support-a").material.get());
0104 const auto* b = dynamic_cast<const GridSurfaceMaterial*>(
0105 surfaces.keyedSurfaces.at("barrel/support-b").material.get());
0106 BOOST_REQUIRE(a != nullptr);
0107 BOOST_REQUIRE(b != nullptr);
0108 BOOST_CHECK(
0109 std::get<GridSurfaceMaterial::GloballyIndexed>(a->storage()).material ==
0110 std::get<GridSurfaceMaterial::GloballyIndexed>(b->storage()).material);
0111 }
0112
0113 BOOST_AUTO_TEST_CASE(MaterialDocumentUnits) {
0114 Converter converter;
0115 auto document = fixture("minimal.json");
0116 document["header"]["units"] = {{"length", "cm"},
0117 {"angle", "deg"},
0118 {"energy", "MeV"},
0119 {"material_amount", "mmol"}};
0120 const auto originalSlab = document["surfaces"][0]["material"]["slab"];
0121 const auto encoded = converter.toJson(converter.fromJson(document));
0122 BOOST_CHECK(encoded["header"]["units"] ==
0123 fixture("minimal.json")["header"]["units"]);
0124 const auto& slab = encoded["surfaces"][0]["material"]["slab"];
0125 BOOST_CHECK_CLOSE_FRACTION(slab["thickness"].get<double>(),
0126 originalSlab["thickness"].get<double>() * 10.,
0127 1e-6);
0128 for (const auto* field :
0129 {"radiation_length", "interaction_length", "molar_density",
0130 "molar_electron_density", "mean_excitation_energy"}) {
0131 const std::string name(field);
0132 const double factor = name.ends_with("length") ? 10.
0133 : name.ends_with("density") ? 1e-6
0134 : 1e-3;
0135 BOOST_CHECK_CLOSE_FRACTION(
0136 slab["material"][field].get<double>(),
0137 originalSlab["material"][field].get<double>() * factor, 1e-6);
0138 }
0139
0140
0141
0142 auto surfaces = fixture("surfaces.json");
0143 const auto baseline = converter.toJson(converter.fromJson(surfaces));
0144 surfaces["header"]["units"] = document["header"]["units"];
0145 const auto rescaled = converter.toJson(converter.fromJson(surfaces));
0146 auto checkSlabs = [&](auto&& self, const nlohmann::json& before,
0147 const nlohmann::json& after) -> void {
0148 if (before.is_object() && before.contains("thickness")) {
0149 BOOST_CHECK_CLOSE_FRACTION(after.at("thickness").get<double>(),
0150 before.at("thickness").get<double>() * 10.,
0151 1e-6);
0152 if (before.at("material").at("kind") == "material") {
0153 for (const auto* field : {"molar_density", "molar_electron_density"}) {
0154 BOOST_CHECK_CLOSE_FRACTION(
0155 after.at("material").at(field).template get<double>(),
0156 before.at("material").at(field).template get<double>() * 1e-6,
0157 1e-6);
0158 }
0159 }
0160 } else if (before.is_object()) {
0161 for (const auto& [key, value] : before.items()) {
0162 self(self, value, after.at(key));
0163 }
0164 } else if (before.is_array()) {
0165 for (std::size_t i = 0; i < before.size(); ++i) {
0166 self(self, before.at(i), after.at(i));
0167 }
0168 }
0169 };
0170 checkSlabs(checkSlabs, baseline, rescaled);
0171
0172 auto templates = fixture("templates.json");
0173 templates["header"]["units"] = document["header"]["units"];
0174 auto& protoAxes = templates["surfaces"][1]["material"]["axes"];
0175 protoAxes[0] = {{"kind", "equidistant"},
0176 {"bins", 2},
0177 {"direction", "phi"},
0178 {"range", {-180., 180.}}};
0179 auto& binning = templates["surfaces"][0]["material"]["binning"];
0180 binning["transform"] = {
0181 {"rotation", {{1., 0., 0.}, {0., 1., 0.}, {0., 0., 1.}}},
0182 {"translation", {1., 2., 3.}}};
0183 const auto output = converter.toJson(converter.fromJson(templates));
0184 const auto& axes = output["surfaces"].back()["material"]["axes"];
0185 BOOST_CHECK_CLOSE_FRACTION(axes[0]["range"][1].get<double>(),
0186 180. * UnitConstants::degree, 1e-12);
0187 BOOST_CHECK(axes[1] == protoAxes[1]);
0188 BOOST_CHECK(output["surfaces"][0]["material"]["binning"]["transform"]
0189 ["translation"] == nlohmann::json({10., 20., 30.}));
0190 for (const auto* direction : {"x", "rphi", "eta", "theta"}) {
0191 protoAxes[0]["direction"] = direction;
0192 const auto converted = converter.toJson(converter.fromJson(templates));
0193 const double factor = std::string(direction) == "eta" ? 1.
0194 : std::string(direction) == "theta"
0195 ? UnitConstants::degree
0196 : 10.;
0197 BOOST_CHECK_CLOSE_FRACTION(converted["surfaces"]
0198 .back()["material"]["axes"][0]["range"][1]
0199 .get<double>(),
0200 180. * factor, 1e-12);
0201 }
0202
0203 for (const auto* key : {"length", "angle", "energy", "material_amount"}) {
0204 auto invalid = document;
0205 invalid["header"]["units"][key] = "unknown";
0206 BOOST_CHECK_EXCEPTION(converter.fromJson(invalid), std::invalid_argument,
0207 [&](const auto& error) {
0208 return std::string(error.what()).find(key) !=
0209 std::string::npos;
0210 });
0211 invalid["header"]["units"].erase(key);
0212 BOOST_CHECK_THROW(converter.fromJson(invalid), nlohmann::json::exception);
0213 }
0214 document["header"]["units"]["length"] = "GeV";
0215 BOOST_CHECK_THROW(converter.fromJson(document), std::invalid_argument);
0216 document["header"].erase("units");
0217 BOOST_CHECK_THROW(converter.fromJson(document), nlohmann::json::exception);
0218 }
0219
0220 BOOST_AUTO_TEST_CASE(MaterialDocumentQuantization) {
0221 Converter converter;
0222 const auto id = GeometryIdentifier().withVolume(1);
0223 for (unsigned int bits : {0u, 12u, 16u, 20u, 23u}) {
0224 Converter::Options options;
0225 options.materialFractionBits = bits;
0226 const double bound = std::ldexp(1., -static_cast<int>(bits) - 1);
0227 for (float value :
0228 {0.f, -0.f, 7.23751f, std::nextafter(1.f, 2.f),
0229 1.f + std::ldexp(1.f, -17), 1.f + 3 * std::ldexp(1.f, -17),
0230 std::numeric_limits<float>::denorm_min(),
0231 std::numeric_limits<float>::min(),
0232 std::numeric_limits<float>::max()}) {
0233 const auto material = Material::fromMolarDensity(
0234 value, value, value > 0 ? value : 28.f, value, value, value, value);
0235 TrackingGeometryMaterial source;
0236 const double split = 0.123456789012345;
0237 source.surfaceMaterials[id] =
0238 std::make_shared<HomogeneousSurfaceMaterial>(
0239 MaterialSlab(material, value), split);
0240 const auto original = converter.toJson(source);
0241 const auto encoded = converter.toJson(source, options);
0242 if (bits == 23) {
0243 BOOST_CHECK(encoded == original);
0244 }
0245 for (const auto& parsed :
0246 {nlohmann::json::parse(encoded.dump()),
0247 nlohmann::json::from_cbor(nlohmann::json::to_cbor(encoded))}) {
0248 const auto recovered = converter.fromJson(parsed);
0249 BOOST_CHECK(converter.toJson(recovered, options) == encoded);
0250 const auto& surface = *recovered.surfaceMaterials.at(id);
0251 const float actual = surface.materialSlab(Vector2::Zero()).thickness();
0252 if (!std::isnormal(value) || bits == 23) {
0253 BOOST_CHECK_EQUAL(std::bit_cast<std::uint32_t>(actual),
0254 std::bit_cast<std::uint32_t>(value));
0255 } else {
0256 BOOST_CHECK_LE(std::abs((double(actual) - value) / value), bound);
0257 }
0258 if (bits == 16 && value == 1.f + std::ldexp(1.f, -17)) {
0259 BOOST_CHECK_EQUAL(actual, 1.f);
0260 }
0261 if (bits == 16 && value == 1.f + 3 * std::ldexp(1.f, -17)) {
0262 BOOST_CHECK_EQUAL(actual, 1.f + std::ldexp(1.f, -15));
0263 }
0264 BOOST_CHECK_EQUAL(surface.factor(Direction::Backward(),
0265 MaterialUpdateMode::PreUpdate),
0266 split);
0267 }
0268 for (const auto& change : nlohmann::json::diff(original, encoded)) {
0269 BOOST_CHECK_EQUAL(change.at("op").get<std::string>(), "replace");
0270 const auto path =
0271 nlohmann::json::json_pointer(change.at("path").get<std::string>());
0272 const double before = original.at(path).get<double>();
0273 const double after = encoded.at(path).get<double>();
0274 BOOST_REQUIRE_NE(before, 0.);
0275 BOOST_CHECK_LE(std::abs((after - before) / before), bound);
0276 }
0277 }
0278 }
0279 Converter::Options invalid;
0280 invalid.materialFractionBits = 24;
0281 BOOST_CHECK_THROW(converter.toJson({}, invalid), std::invalid_argument);
0282 }
0283
0284 BOOST_AUTO_TEST_CASE(MaterialDocumentQuantizationStorageAndFiles) {
0285 Converter converter;
0286 Converter::Options options;
0287 options.materialFractionBits = 16;
0288 TemporaryDirectory tmp;
0289 for (const auto* name : {"minimal.json", "surfaces.json", "templates.json"}) {
0290 const auto source = converter.fromJson(fixture(name));
0291 const auto original = converter.toJson(source);
0292 const auto encoded = converter.toJson(source, options);
0293 for (const auto& change : nlohmann::json::diff(original, encoded)) {
0294 BOOST_CHECK_EQUAL(change.at("op").get<std::string>(), "replace");
0295 const auto path = change.at("path").get<std::string>();
0296
0297
0298 const std::array fields{"/thickness",
0299 "/radiation_length",
0300 "/interaction_length",
0301 "/relative_atomic_mass",
0302 "/atomic_number",
0303 "/molar_density",
0304 "/molar_electron_density",
0305 "/mean_excitation_energy"};
0306 BOOST_CHECK(std::ranges::any_of(
0307 fields, [&](const auto* field) { return path.ends_with(field); }));
0308 }
0309 std::vector<std::string> extensions{".json", ".cbor"};
0310 if (detail::zstdSupported()) {
0311 extensions.insert(extensions.end(), {".json.zst", ".cbor.zst"});
0312 }
0313 for (const auto& extension : extensions) {
0314 const auto path = tmp.path() / (std::string(name) + extension);
0315 converter.toFile(source, path, options);
0316 BOOST_CHECK(converter.toJson(converter.fromFile(path)) == encoded);
0317 }
0318 }
0319 }
0320
0321 BOOST_AUTO_TEST_CASE(MaterialDocumentDescription) {
0322 Converter converter;
0323 TrackingGeometryMaterial material;
0324 BOOST_CHECK(!material.description());
0325 BOOST_CHECK(!converter.toJson(material).at("header").contains("description"));
0326 material.setDescription("Mapped detector material");
0327 auto copy = material;
0328 copy.setDescription("");
0329 BOOST_CHECK_EQUAL(*material.description(), "Mapped detector material");
0330 BOOST_REQUIRE(copy.description());
0331 BOOST_CHECK(copy.description()->empty());
0332 const auto encoded = converter.toJson(material);
0333 BOOST_CHECK_EQUAL(*converter.fromJson(encoded).description(),
0334 "Mapped detector material");
0335 BOOST_REQUIRE(converter.fromJson(converter.toJson(copy)).description());
0336 copy.setDescription(std::nullopt);
0337 BOOST_CHECK(!converter.toJson(copy).at("header").contains("description"));
0338 for (const nlohmann::json& invalid :
0339 {nlohmann::json(nullptr), nlohmann::json(42), nlohmann::json::object(),
0340 nlohmann::json::array()}) {
0341 auto malformed = encoded;
0342 malformed["header"]["description"] = invalid;
0343 BOOST_CHECK_THROW(converter.fromJson(malformed), nlohmann::json::exception);
0344 }
0345 }
0346
0347 BOOST_AUTO_TEST_CASE(MaterialDocumentPhysicalPropertiesAndStorage) {
0348 Converter converter;
0349 const auto silicon = Material::fromMolarDensity(90.f, 450.f, 28.f, 14.f,
0350 0.001f, 0.02f, 0.0003f);
0351 const MaterialSlab slab(silicon, 0.25f);
0352 TrackingGeometryMaterial source;
0353 const auto id = GeometryIdentifier()
0354 .withVolume(255)
0355 .withBoundary(255)
0356 .withLayer(4095)
0357 .withPassive(255)
0358 .withSensitive(1048575)
0359 .withExtra(255);
0360 source.surfaceMaterials[id] = std::make_shared<HomogeneousSurfaceMaterial>(
0361 slab, 0.25, MappingType::Sensor);
0362 source.setDescription("roundtrip");
0363 const auto encoded = converter.toJson(source);
0364 BOOST_CHECK_EQUAL(encoded.at("$schema").get<std::string>(),
0365 "urn:acts:material-map:1");
0366 const auto& encodedId =
0367 encoded.at("surfaces").at(0).at("target").at("geometry_id");
0368 BOOST_CHECK_EQUAL(encodedId.at("passive").get<unsigned int>(), 255u);
0369 BOOST_CHECK(!encodedId.contains("approach"));
0370 BOOST_CHECK_EQUAL(encodedId.at("portal").get<unsigned int>(), 255u);
0371 BOOST_CHECK(!encodedId.contains("boundary"));
0372 const auto recovered = converter.fromJson(encoded);
0373 BOOST_CHECK(source.description() == recovered.description());
0374 const auto& material = *recovered.surfaceMaterials.at(id);
0375 BOOST_CHECK(material.materialSlab(Vector2::Zero()) == slab);
0376 BOOST_CHECK_EQUAL(
0377 material.factor(Direction::Negative(), MaterialUpdateMode::PreUpdate),
0378 0.25);
0379 BOOST_CHECK(material.mappingType() == MappingType::Sensor);
0380
0381
0382
0383 MultiAxisSpec2D axes(std::array<AxisSpec, 2>{
0384 AxisSpec::Equidistant(2, 0., 2., AxisBoundaryType::Open,
0385 AxisDirection::AxisX),
0386 AxisSpec::Equidistant(3, 0., 3., AxisBoundaryType::Open,
0387 AxisDirection::AxisY)});
0388 auto multi = axes.buildMultiAxis();
0389 GridSurfaceMaterial::Direct values(20);
0390 for (std::size_t y = 0; y < 5; ++y) {
0391 for (std::size_t x = 0; x < 4; ++x) {
0392 values[multi->getGlobalBinFromLocalBins({x, y})] =
0393 MaterialSlab::Vacuum(static_cast<float>(x + 10 * y));
0394 }
0395 }
0396 source.surfaceMaterials.clear();
0397 source.surfaceMaterials[id] =
0398 std::make_shared<GridSurfaceMaterial>(axes, values);
0399 const auto j = converter.toJson(source);
0400 const auto& serialized = j["surfaces"][0]["material"]["storage"]["values"];
0401 BOOST_CHECK_EQUAL(serialized[1]["thickness"].get<double>(), 1.);
0402 BOOST_CHECK_EQUAL(serialized[4]["thickness"].get<double>(), 10.);
0403 const auto decoded = converter.fromJson(j);
0404 const auto& grid = dynamic_cast<const GridSurfaceMaterial&>(
0405 *decoded.surfaceMaterials.at(id));
0406 BOOST_CHECK(std::get<GridSurfaceMaterial::Direct>(grid.storage()) == values);
0407 BOOST_CHECK_EQUAL(grid.materialSlab(Vector2(-1., -1.)).thickness(), 0.);
0408 BOOST_CHECK_EQUAL(grid.materialSlab(Vector2(3., 4.)).thickness(), 43.);
0409 }
0410
0411 BOOST_AUTO_TEST_CASE(MaterialDocumentRejectsVolumes) {
0412 Converter converter;
0413 TrackingGeometryMaterial material;
0414 material.volumeMaterials.emplace(
0415 GeometryIdentifier().withVolume(1),
0416 std::make_shared<HomogeneousVolumeMaterial>(Material::Vacuum()));
0417 BOOST_CHECK_THROW(converter.toJson(material), std::invalid_argument);
0418
0419 material.volumeMaterials.begin()->second.reset();
0420 BOOST_CHECK_THROW(converter.toJson(material), std::invalid_argument);
0421 auto document = fixture("minimal.json");
0422 BOOST_CHECK(!document.contains("volumes"));
0423 document["volumes"] = nlohmann::json::array();
0424 BOOST_CHECK_THROW(converter.fromJson(document), std::invalid_argument);
0425 document["volumes"].push_back(
0426 {{"geometry_id", {{"volume", 1}}}, {"material", nullptr}});
0427 BOOST_CHECK_THROW(converter.fromJson(document), std::invalid_argument);
0428 const auto decoded = converter.fromJson(fixture("minimal.json"));
0429 BOOST_CHECK(decoded.volumeMaterials.empty());
0430 BOOST_CHECK(!converter.toJson(decoded).contains("volumes"));
0431 }
0432
0433 BOOST_AUTO_TEST_CASE(MaterialDocumentRejectsInvalidInputs) {
0434 Converter converter;
0435 const auto base = fixture("surfaces.json");
0436 auto bad = [&](auto mutation) {
0437 auto j = base;
0438 mutation(j);
0439 BOOST_CHECK_THROW(converter.fromJson(j), std::invalid_argument);
0440 };
0441 bad([](auto& j) { j["header"]["version"] = 2; });
0442 bad([](auto& j) { j["header"]["version"] = 1.5; });
0443 bad([](auto& j) { j["header"]["format"] = "legacy"; });
0444 bad([](auto& j) { j["surfaces"][0]["material"]["kind"] = "unknown"; });
0445 bad([](auto& j) {
0446 j["surfaces"][0]["target"]["geometry_id"]["volume"] = 256;
0447 });
0448 bad([](auto& j) {
0449 auto copy = j["surfaces"][0];
0450 copy["target"]["geometry_id"]["extra"] = 0;
0451 j["surfaces"].push_back(copy);
0452 });
0453 bad([](auto& j) {
0454 j["surfaces"][5]["material"]["storage"]["store"] = "missing";
0455 });
0456 bad([](auto& j) {
0457 j["surfaces"][4]["material"]["storage"]["indices"][4] = 2;
0458 });
0459 bad([](auto& j) {
0460 j["surfaces"][3]["material"]["storage"]["values"].erase(0);
0461 });
0462 bad([](auto& j) {
0463 j["surfaces"][3]["material"]["axes"][0]["range"] = {1., -1.};
0464 });
0465 bad([](auto& j) { j["surfaces"][5]["material"] = nullptr; });
0466 bad([](auto& j) {
0467 j["surfaces"][0]["material"]["slab"]["thickness"] =
0468 std::numeric_limits<double>::quiet_NaN();
0469 });
0470 auto proto = fixture("templates.json");
0471 proto["surfaces"][1]["material"]["material_key"] = "mismatch";
0472 BOOST_CHECK_THROW(converter.fromJson(proto), std::invalid_argument);
0473 }
0474
0475 BOOST_AUTO_TEST_CASE(MaterialDocumentBasicParsing) {
0476 Converter converter;
0477 auto document = fixture("minimal.json");
0478 document.erase("header");
0479 BOOST_CHECK_THROW(converter.fromJson(document), std::invalid_argument);
0480 const nlohmann::json legacy{
0481 {"Surfaces", {{"entries", nlohmann::json::array()}}}};
0482 BOOST_CHECK_EXCEPTION(
0483 converter.fromJson(legacy), std::invalid_argument, [](const auto& error) {
0484 return std::string(error.what()).find("ActsMaterialMapMigrate") !=
0485 std::string::npos;
0486 });
0487 document = fixture("minimal.json");
0488 document["header"] = nullptr;
0489 BOOST_CHECK_THROW(converter.fromJson(document), nlohmann::json::exception);
0490 document = fixture("minimal.json");
0491 document["extra"] = "ignored by codec; rejected by offline schema";
0492 document["surfaces"][0]["material"]["extra"] = true;
0493 BOOST_CHECK_NO_THROW(converter.fromJson(document));
0494 document["surfaces"][0]["material"].erase("slab");
0495 BOOST_CHECK_THROW(converter.fromJson(document), nlohmann::json::exception);
0496 document = fixture("minimal.json");
0497 document["surfaces"][0]["material"]["slab"]["thickness"] = "invalid";
0498 BOOST_CHECK_THROW(converter.fromJson(document), nlohmann::json::exception);
0499 }
0500
0501 BOOST_AUTO_TEST_CASE(MaterialDocumentExtensionDispatchAndFiles) {
0502 Converter::Config config = Converter::Config::defaultConfig();
0503 config.encodeSurface.registerFunction(encodeCustom);
0504 config.decodeSurface.registerKind("application-custom", decodeCustom);
0505 Converter converter(std::move(config));
0506 TrackingGeometryMaterial source;
0507 source.surfaceMaterials[GeometryIdentifier().withVolume(1)] =
0508 std::make_shared<CustomMaterial>(7.23751);
0509 const auto encoded = converter.toJson(source);
0510 Converter::Options quantized;
0511 quantized.materialFractionBits = 0;
0512 BOOST_CHECK(converter.toJson(source, quantized) == encoded);
0513 BOOST_CHECK_THROW(Converter().fromJson(encoded), std::invalid_argument);
0514 auto centimeters = encoded;
0515 centimeters["header"]["units"]["length"] = "cm";
0516 const auto scaled = converter.fromJson(centimeters);
0517 BOOST_CHECK_CLOSE_FRACTION(scaled.surfaceMaterials.begin()
0518 ->second->materialSlab(Vector2::Zero())
0519 .thickness(),
0520 source.surfaceMaterials.begin()
0521 ->second->materialSlab(Vector2::Zero())
0522 .thickness() *
0523 10.,
0524 1e-6);
0525 const auto decoded = converter.fromJson(encoded);
0526 BOOST_CHECK(dynamic_cast<const CustomMaterial*>(
0527 decoded.surfaceMaterials.begin()->second.get()) != nullptr);
0528 TemporaryDirectory tmp;
0529 std::vector<std::string> extensions{".json", ".cbor"};
0530 if (detail::zstdSupported()) {
0531 extensions.insert(extensions.end(), {".json.zst", ".cbor.zst"});
0532 }
0533 for (const auto& ext : extensions) {
0534 const auto path = tmp.path() / ("material" + ext);
0535 converter.toFile(source, path);
0536 const auto renamed = tmp.path() / "unknown-extension.bin";
0537 std::filesystem::rename(path, renamed);
0538 BOOST_CHECK(converter.toJson(converter.fromFile(renamed)) == encoded);
0539 std::filesystem::remove(renamed);
0540 }
0541 }
0542 BOOST_AUTO_TEST_SUITE_END()
0543 }