Skip to content

refactor(json): Dispatch surface material (de)serialization via TypeDispatcher - #6023

Merged
paulgessinger merged 5 commits into
acts-project:mainfrom
paulgessinger:feat/material-json-typedispatcher
Sep 17, 2026
Merged

paulgessinger merged 5 commits into
acts-project:mainfrom
paulgessinger:feat/material-json-typedispatcher

Conversation

@paulgessinger

@paulgessinger paulgessinger commented Sep 3, 2026

Copy link
Copy Markdown
Member

Note

The grid dispatch is written against the GridSurfaceMaterial from #5871
(now merged): one encoder on the concrete class branching over the storage
variant, no bound/global-to-grid-local delegates, 2D only,
accessor.type: "direct". The store context below is also what lets
globally indexed grids be read back at all -- #5871's reader throws on
them, because the shared material vector has no source at single-surface
scope.

Surface material JSON conversion was a pair of free to_json/from_json
functions on surfaceMaterialPointer: a long dynamic_cast chain on write,
and a decoder that guessed the type from whichever keys happened to be
present. It could not be extended from outside the plugin, and the two sides
had drifted apart -- a GloballyIndexedSurfaceMaterial was read back as a
locally indexed one with an empty store.

This replaces them with SurfaceMaterialJsonConverter, built like
SurfaceJsonConverter:

  • write: a TypeDispatcher with one encoder per material type. The whole
    grid family is registered against the two abstract IGridSurfaceMaterial
    bases, so the concrete grid shapes stay out of the translation unit.
  • read: a JsonKindDispatcher keyed on the type tag. The tag is now
    authoritative -- a missing or unknown one throws instead of falling through
    to a guess.

Both directions take an optional context. Grids that index into a shared slab
store register it once per document and reference it by id, so the sharing
survives a round trip instead of becoming one copy per surface. Without a
context the store is inlined and the payload stays self-contained, which is
what standalone surface entries need.

The old free functions stay, marked [[deprecated]]. The on-disk payloads are
unchanged; Tests/Data/material-map.json still round-trips byte for byte.

Also adds a page documenting the material map file format. Its example is
generated by a unit test that fails if the file stops matching what the
converters write.

@github-actions github-actions Bot added this to the next milestone Sep 3, 2026
@paulgessinger
paulgessinger marked this pull request as ready for review September 3, 2026 13:28
@github-actions github-actions Bot added Component - Core Affects the Core module Component - Plugins Affects one or more Plugins Public API API breaking labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Public API surface diff

+12 added, 0 breaking.

➕ Added public API

New types / aliases / enums / variables / concepts (2)
  • type Acts::SurfaceMaterialJsonConverter
  • type Acts::SurfaceMaterialJsonConverter::Config
New call signatures (incl. defaulted-arg overloads) (8)
  • Acts::SurfaceMaterialJsonConverter::Config::defaultConfig()
  • Acts::SurfaceMaterialJsonConverter::SurfaceMaterialJsonConverter()
  • Acts::SurfaceMaterialJsonConverter::fromJson(const nlohmann::json &)
  • Acts::SurfaceMaterialJsonConverter::fromJson(const nlohmann::json &, const Config &)
  • Acts::SurfaceMaterialJsonConverter::fromJson(const nlohmann::json &, const Config &, const DecodeContext *)
  • Acts::SurfaceMaterialJsonConverter::toJson(const ISurfaceMaterial &)
  • Acts::SurfaceMaterialJsonConverter::toJson(const ISurfaceMaterial &, const Config &)
  • Acts::SurfaceMaterialJsonConverter::toJson(const ISurfaceMaterial &, const Config &, EncodeContext *)
New public data members (2)
  • Acts::SurfaceMaterialJsonConverter::Config::decoder
  • Acts::SurfaceMaterialJsonConverter::Config::encoder

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📊: Physics performance monitoring for 5c63c90

Full contents

physmon summary

@paulgessinger
paulgessinger marked this pull request as draft September 4, 2026 12:31
@paulgessinger
paulgessinger marked this pull request as ready for review September 8, 2026 09:51
@github-actions github-actions Bot added the Component - Documentation Affects the documentation label Sep 8, 2026
@paulgessinger
paulgessinger force-pushed the feat/material-json-typedispatcher branch from b422291 to 34135ab Compare September 8, 2026 16:27
@paulgessinger
paulgessinger marked this pull request as draft September 8, 2026 16:27
@github-actions github-actions Bot added Component - Detray Affects the Detray project API breaking labels Sep 8, 2026
paulgessinger and others added 4 commits September 11, 2026 15:00
…ispatcher

The `ISurfaceMaterial` <-> JSON conversion in `Plugins/Json` was a hand
written `dynamic_cast` chain on the write side and a shape-guessing reader
on the read side. This replaces both with the dispatcher pattern already
used by `SurfaceJsonConverter`: a new `Acts::SurfaceMaterialJsonConverter`
holds a `TypeDispatcher` encoder and a `JsonKindDispatcher` decoder in a
`Config`, with `Config::defaultConfig()` registering everything.

The payload of every type that already round-tripped is unchanged, so
existing material maps (e.g. `Tests/Data/material-map.json`) keep loading.

What this buys:

- The reader dispatches strictly on the type tag instead of inferring the
  type from the payload shape. A missing or unknown tag is an error. As a
  consequence a "binned" payload with a single bin now decodes as
  `BinnedSurfaceMaterial` rather than `HomogeneousSurfaceMaterial`.
- `ProtoGridSurfaceMaterial` (the `MultiAxisSpec2D` proto material) is
  covered under the new "proto-grid" tag; it used to serialize to nothing.
- Globally indexed grids read back with their slab store populated. The
  free-function reader refuses them outright, because the shared vector has
  no source at single-surface scope; the encoder here inlines it under
  `storage_vector`, and the next commit adds the document-wide store table
  that lets the sharing itself survive.
- `mappingType` round-trips for every type that can carry one, and the
  `mapMaterial == false` short circuit is applied once in `fromJson` before
  dispatch. Grid payloads gain the `mapMaterial` flag, which the
  free-function writer never emitted.

The whole grid material family needs exactly one encoder and one decoder:
`GridSurfaceMaterial` is a single concrete class whose storage backend is a
runtime `std::variant`, so the encoder branches with `std::visit` and never
names a concrete axis type.

The reader hands out a `std::unique_ptr<const ISurfaceMaterial>` so
ownership is explicit. `MaterialMapJsonConverter` correspondingly stores
`std::shared_ptr<const ISurfaceMaterial>` in its geometry hierarchy map
instead of an owning raw pointer.

The free `to_json`/`from_json` overloads for `surfaceMaterialPointer` stay
as deprecated forwarders; all in-tree callers use the new class directly.
Volume material conversion is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSH9Jg53ECS7DmzTYJdsZs
…dispatch

Globally indexed grid material indexes into a slab store that several grids
of a material map can share. The JSON payload had no way to express that
sharing: the previous commit makes each entry carry its own copy of the
store, which is correct but turns one shared vector into one vector per
surface on the way back.

Introduce an encode/decode context that a whole document is written and
read with. On encoding, the grid encoder registers the store it encounters
with the context, which keys stores on `shared_ptr` identity (never on
content, so two stores holding equal slabs stay distinct) and hands back an
id assigned in encounter order. The entry then only writes
`"store": <id>`, and the map-level converter can emit the stores once as a
top-level table. On decoding, the table is read first into the context and
`"store": <id>` resolves to the shared allocation, so the grids that shared
a store before the round trip share it again after.

A default constructed context has no store table. The grid encoder then
inlines the store as `storage_vector`, exactly as the locally indexed
backend does, which keeps a surface serialized on its own -- through
`SurfaceJsonConverter`, `JsonSurfacesWriter` or the tracking geometry
converter -- self-contained. A `"store"` reference read without a table, or
one past the end of the table, is an error rather than a silently empty
store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSH9Jg53ECS7DmzTYJdsZs
The store context work exposed more than callers need. Pull it back in.

The two context classes move to `detail/MaterialJsonContext.hpp` in
`Acts::detail`, following the plugin's existing detail convention. The public
header forward declares them and keeps the `EncodeContext`/`DecodeContext`
aliases, which is all the dispatcher signatures need; only the converter
sources and the unit tests see the definitions.

`toJson` and `fromJson` collapse from two overloads each to one, with the
context as an optional trailing pointer. A null pointer means what the
context-less overload used to mean: the encoder inlines the slab store, and
the decoder rejects a payload that references one. The duplicate cached
`SurfaceMaterialJsonConverter::defaultConfig()` is gone; `Config::defaultConfig()`
does the caching, as in `SurfaceJsonConverter`.

The free `to_json`/`from_json` on `std::shared_ptr<const ISurfaceMaterial>`
are removed. Their two callers -- the surface-and-material tuple encoder and
the material map converter's surface hierarchy -- now go through the class
API. The map converter encodes each entry itself and hands the hierarchy
container ready json, which is also what will let it thread a document-wide
context through the entries later.

With this the branch touches no Core header at all; it is confined to
`Plugins/Json`, its tests and the docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSH9Jg53ECS7DmzTYJdsZs
Addresses the two SonarCloud code smells on the PR: push_back of a
brace-initialised pair becomes emplace_back, and map insert of a pair
becomes try_emplace. The volume-side insert is changed too so the two
loops read the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVsEtWnNPh2DejfRkESkTJ
@paulgessinger
paulgessinger force-pushed the feat/material-json-typedispatcher branch from 34135ab to d86fbb4 Compare September 11, 2026 13:02
@github-actions github-actions Bot removed Component - Core Affects the Core module Component - Detray Affects the Detray project labels Sep 11, 2026
The JSON plugin's most user-facing artifact is the material map file, and
nothing described it. This adds a page that walks the document layout, every
surface material payload the dispatcher can write, and the volume material
payloads of the older converter.

No JSON on the page is typed out by hand, so none of it can drift.
`MaterialJsonDocumentation` builds the example, encodes it through
`MaterialMapJsonConverter`, checks that the result reads back, and compares it
against `docs/examples/material_map_example.json`; a format change that is not
reflected in the docs fails the test, and running it with
`ACTS_UPDATE_DOC_EXAMPLES=1` refreshes the file.

The example is kept minimal, because it is included verbatim and every line of
it is a line someone reads. It carries the three surface payloads that differ
in shape -- a plain slab, a binned matrix, a grid indexing into a slab store --
plus one volume entry, with the smallest bin counts the format allows and a
single material throughout. The types it leaves out differ only in keys, which
the tables on the page list; that the encoder covers all of them is already
asserted by SurfaceMaterialJsonConverterTests.

The dump uses four spaces so the repository's json formatting hook leaves it
alone, and .gitignore gets an exception for it next to the codegen manifest,
since `*.json` is ignored wholesale.

The test gets the example's path as a command line argument rather than a
compile definition, so it does not have to be rebuilt when it moves and the
path stays visible in the ctest invocation. `add_unittest` now forwards any
argument after the source file to the executable, behind the `--` separator
that Boost.Test wants for tokens that are not its own options; they arrive as
`master_test_suite().argv`, so this stays an ordinary Boost unit test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSH9Jg53ECS7DmzTYJdsZs
@paulgessinger
paulgessinger force-pushed the feat/material-json-typedispatcher branch from d86fbb4 to 5c63c90 Compare September 11, 2026 13:17
@paulgessinger
paulgessinger marked this pull request as ready for review September 11, 2026 15:00
@sonarqubecloud

Copy link
Copy Markdown

@noemina noemina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good to me. I have one comment on the description.

Comment thread docs/pages/material_map_json_format.md
@paulgessinger
paulgessinger added this pull request to the merge queue Sep 17, 2026
Merged via the queue into acts-project:main with commit d467975 Sep 17, 2026
39 checks passed
@paulgessinger
paulgessinger deleted the feat/material-json-typedispatcher branch September 18, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component - Documentation Affects the documentation Component - Plugins Affects one or more Plugins Public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants