wcpy util gdml-to-wires -d <detector> -o <out.json[.gz|.bz2]> <input.gdml>
converts a LArSoft detector GDML file into a Wire-Cell Toolkit (WCT) "wires
schema" JSON file.
This document is the durable guide for extending the command to new geometries. It is written for both human and LLM maintainers. If you are an agent adding support for a new detector/geometry, read all of it, do the work, then update it per the checklist at the end.
The command must produce a wires file whose wire coordinates, wire idents, channel numbers and segments match LArSoft exactly, so the geometry can be used interchangeably with LArSoft-produced data.
Two facts shape the whole design:
-
Wire endpoints come straight from the GDML wire tube solids. LArSoft's standard wire builder (
geo::WireReadoutGeomBuilderStandard) makes onegeo::WireGeopervolTPCWire*physvol; its endpoints are the tube end-caps (center ± halfLengthalong the local tube axis) pushed through the placement transform. The command'sextract_wiresalready reproduces these byte-for-byte (verified to 0.000 mm on geometries whose wires live in the GDML). You almost never need to recompute wire positions. -
The hard part is the numbering, i.e. the per-plane wire ident ordering and the global channel assignment. These are not in the GDML — they are defined by whichever LArSoft wire-readout class and TPC/wire sorter the geometry is configured to use. Extending the command to a new geometry is therefore mostly: identify the LArSoft classes, port their ordering and channel arithmetic, and validate against a LArSoft dump.
A corollary: the WCT wires files shipped in wire-cell-data are only valid
references for the exact geometry revision they were generated from. Wire
counts and coordinates drift between revisions, so never assume a similarly
named *.json.bz2 matches your GDML — confirm by wire count, or regenerate a
dump (below).
-
Inspect the GDML naming. Find the logical-volume name patterns for the wire, plane, face (TPC), and detector (cryostat) roles. These become the
role_patternsregexes of a new entry inBUILTIN_CONFIGS(wirecell/util/wires/gdml.py).re.fullmatchis used, so patterns must match the entire LV name. "Refactored" DUNE GDMLs reuse a singlevolTPC/volTPCPlaneULV placed many times; older ones number every LV. -
Identify the authoritative LArSoft wire-readout class for the geometry (see "Which LArSoft class?" below). This dictates channel numbering and whether induction strips are shared/wrapped.
-
Identify the TPC sorter and wire sorter the geometry uses. The TPC sorter sets the CRU/TPC iteration order (hence channel blocks); the wire sorter sets the per-plane wire ident. Port both exactly (they are small, self-contained comparators).
-
Decide the anode/face model the WCT file should use (one-face-per-CRU vs open-book two-face anodes, segments present or all zero). This follows from the LArSoft class, and you can confirm it by inspecting a matching reference wires file's structure (faces-per-anode, segment histogram, channel count).
-
Implement a
convert-mode branch that applies the ported ordering and channel arithmetic and emits the WCTschema.Store. Keep wire endpoints fromextract_wires; only assign ident/channel/segment and group into anodes/faces/planes. -
Generate a LArSoft dump for the exact geometry (driver below) and validate wire-for-wire: same channel per (tpc,plane,wire), same endpoints. Also cross-check your ported wire sorter by re-sorting a genuine reference wires file's wires and confirming you recover its stored idents.
-
Add an integration test (see
wirecell/util/test/test_gdml_integration_*) and update this document's table.
The wire-readout class is chosen by dunecore's DUNEWireReadout service
(dunecore/Geometry/DUNEWireReadout_service.cc). Two mechanisms:
-
An explicit
WireReadoutClassfhicl parameter, if present, wins. -
Otherwise the class is derived from the geometry's detector Name (
geo::GeometryCore::DetectorName()), by substring match. Current mapping of the substrings that matter for DUNE:detector Name contains wire-readout class dunevd10ktgeo::WireReadoutCRUGeomdune10kt/lbne10ktAPA channel map ( useApaMap)dunevdcb1geo::ColdBoxWireReadoutGeomdunecrpcbgeo::CRPWireReadoutGeomprotodunevdgeo::CRPWireReadoutGeomprotodunev7/protodunev8geo::ProtoDUNEWireReadoutGeom{v7,v8}protodune(other)geo::ProtoDUNEWireReadoutGeomdunedphase*/*dphasegeo::WireReadoutCRMGeom
The TPC sorter is set separately by the geometry service's
SortingParameters: { tool_type: ... } (e.g. GeoObjectSorterCRU,
GeoObjectSorterCRU60D, GeoObjectSorterAPA). Gotcha: some geometry fcl
blocks inherit a base block and do not re-set SortingParameters; confirm the
effective value rather than assuming. The wire sorter is set inside the
wire-readout class constructor (e.g. WireReadoutCRUGeom installs
WireReadoutSorterCRU; CRPWireReadoutGeom installs its own
CRPWireReadoutSorter).
Always confirm the class/sorter against the actual fcl configuration used to produce your reference — the name heuristic can be overridden, and it has changed across dunecore versions.
Run inside a LArSoft job configured with the target geometry (the same services
your production uses). This prints one FLAG line per wire giving the channel,
the (cryostat, tpc, plane, wire) indices, and both endpoints. Treat its
output as ground truth.
#include "larcore/Geometry/Geometry.h"
// ... inside an art module/analyzer ...
art::ServiceHandle<geo::Geometry> geom;
double xyz[3]; // wire start endpoint
double abc[3]; // wire end endpoint
int chan;
int cryo = geom->Ncryostats();
for (int c = 0; c < cryo; ++c) {
int tpc = geom->NTPC(c);
for (int t = 0; t < tpc; ++t) {
int Nplanes = geom->Nplanes(t, c);
for (int p = 0; p < Nplanes; ++p) {
int Nwires = geom->Nwires(p, t, c);
for (int w = 0; w < Nwires; ++w) {
geom->WireEndPoints(c, t, p, w, xyz, abc);
chan = geom->PlaneWireToChannel(p, w, t, c);
std::cout << "FLAG " << chan << " "
<< c << " " << t << " " << p << " " << w << " "
<< xyz[0] << " " << xyz[1] << " " << xyz[2] << " "
<< abc[0] << " " << abc[1] << " " << abc[2] << std::endl;
}
}
}
}Validation recipe: parse the FLAG lines, group by channel, and compare to the
command's output — every wire's channel and endpoints must match (endpoints up
to a tolerance of ~0.01 mm; note LArSoft may apply a PlaneGeo::shouldFlipWire
flip that swaps which endpoint is "start" vs "end" without moving the wire).
Units: LArSoft WireEndPoints are in cm; WCT wires schema is in mm.
connectivity_mode is the command's internal algorithm selector (in
BUILTIN_CONFIGS), chosen to reproduce the LArSoft class in the last column.
Config (-d) |
Detector | GDML naming | faces/anode, segments | LArSoft wire-readout class | TPC sorter / wire sorter | Channel scheme |
|---|---|---|---|---|---|---|
protodunevd_v4 |
ProtoDUNE-VD | volTPC\d+, volTPCPlane[UVZ]\d+, volTPCWire[UVZ]\d+ |
2 (open-book) | CRPWireReadoutGeom |
GeoObjectSorterCRU60D / CRPWireReadoutSorter |
shared induction across faces |
protodunevd_v5 |
ProtoDUNE-VD | v5 LV suffixes (volTPCWireU0_0, volTPCWireZ0) |
2 (open-book) | CRPWireReadoutGeom |
GeoObjectSorterCRU60D / CRPWireReadoutSorter |
shared induction across faces |
protodunehd_v8 |
ProtoDUNE-HD | volTPCWire(…)(Inner|Outer) etc. |
2, wrapped segments 0/1/2 | ProtoDUNE wire readout (verify by Name) | APA-family sorter | wrapped-wire clusters at Z boundaries |
fdvd |
DUNE FD VD (dunevd10kt, workspace e.g. 1x8x14) |
refactored: single volTPC, volTPCPlane[UVZ], volTPCWireZ + numbered U/V |
1 face/CRU, all segment 0 | WireReadoutCRUGeom |
GeoObjectSorterCRU (Z↑ then Y↑) / WireReadoutSorterCRU |
hierarchical, no sharing: running counter over CRU→plane(U,V,Z)→wire |
fdvd(DUNE FD vertical drift). The authoritative class fordunevd10ktisWireReadoutCRUGeom— hierarchical, no induction sharing, one single-face anode per CRU, every wire segment 0, channels a dense running counter so a workspace subset is numbered0..N-1with no gaps. CRUs sort by center Z ascending then Y ascending (GeoObjectSorterCRU); within a plane, wires sort byWireReadoutSorterCRU::compareWires(both induction views primary center-Z ascending with a Y tie-break; collection wires, which run along Y, sort by center-Z ascending). Planes mapU→0, V→1, Z→2.- Important distinction: the full FD-VD detector wires file
(
dunevd10kt_3view_30deg_v*_refactored.json.bz2, hundreds of CRUs) is a different convention — open-book two-face anodes with segments 0/1 and shared induction, i.e. aCRPWireReadoutGeom-style map — whereas the workspace subsets (…1x8x14…,…1x8x6…) use theWireReadoutCRUGeomhierarchical map above. Decide which one your task targets and confirm with a dump; do not mix them.
- Important distinction: the full FD-VD detector wires file
(
- ProtoDUNE-VD (
protodunevd_*). Open-book: two faces per anode share induction channels where strips meet across the face boundary;vdconnectivity_mode matches induction endpoints within a tolerance. - ProtoDUNE-HD (
protodunehd_v8). Wrapped induction wires: a strip is several geometric wires (segments 0/1/2) across Inner/Outer TPCs, connected at Z boundaries;hdmode detects the boundary clusters and does the topological segment/channel assignment. The exact LArSoft class should be re-verified via the Name-based dispatch for the specific geometry. - DUNE FD HD (
dune10kt, future work). Dispatches to the APA channel map (useApaMap), not any CRU/CRP class — expect an APA-style sorter and numbering, closer to the ProtoDUNE-HD wrapped model than to VD.
- Wire counts per plane match a LArSoft dump of the same revision.
- Every wire's channel matches the dump (
PlaneWireToChannel). - Endpoints match the dump within ~0.01 mm (mind cm→mm and the flip).
- Ported wire sorter recovers stored idents when re-sorting a genuine reference wires file.
- Structural invariants hold (faces/anode, segment histogram, channel count and density) and are asserted in an integration test.
When you extend gdml-to-wires to a new geometry, you MUST also update this
document, in the same change:
- Add a row to the geometry summary table with the new
-dconfig name, detector, GDML naming, anode/segment model, the LArSoft wire-readout class, the TPC and wire sorters, and the channel scheme. - Add a per-geometry note capturing anything non-obvious: naming quirks, open-book vs single-face, wrapped segments, workspace-vs-full differences, tolerances, and any place the Name-based class dispatch is overridden or surprising.
- If you discovered a new reusable fact (e.g. a new sorter, a new channel arithmetic, a dispatch change in a newer dunecore), add it to the relevant general section ("Which LArSoft class?", "What the command has to reproduce") so the next agent benefits.
- Keep it area-agnostic. Do NOT put absolute paths, machine names, build
directories, or anything specific to a particular working checkout in this
file. Refer to packages and source files by their in-repo relative names
(e.g.
dunecore/Geometry/DUNEWireReadout_service.cc,wirecell/util/wires/gdml.py), not their location on disk. - Record how you validated (which dump, which reference) in the integration test and, if useful, as a one-line note in the table — but keep the concrete artifact out of the repo.
- Prefer reuse. New geometries often share a LArSoft class with an existing one; if so, reuse the ported sorter/numbering and only add a config + notes, rather than duplicating logic.
If a geometry's convention cannot be validated against any available reference of the matching revision, say so explicitly in the note and mark the config as provisional until a LArSoft dump confirms it.