diff --git a/.github/scripts/sync_cad_tutorial.py b/.github/scripts/sync_cad_tutorial.py new file mode 100755 index 0000000..4854af5 --- /dev/null +++ b/.github/scripts/sync_cad_tutorial.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +"""Generate the CAD simulation tutorial section from its sources in AliceO2. + +The tutorial is written and reviewed in AliceO2, under +`Detectors/CADSupport/doc/tutorial/docs`, so that it stays next to the code it documents. +This script converts those pages into what this Jekyll site expects and writes them to +`docs/cadtutorial`. Do not edit that directory by hand: the next run overwrites it. + +Two things are converted. + + * GitHub alerts (`> [!NOTE]`) become fenced `note` and `warning` blocks, which the theme + renders as callouts. The bold line under the marker stays as the callout's own title, + because the theme's title is fixed. + * Every page gets the `sort`/`title` front matter the navigation is built from. + +Usage: + sync_cad_tutorial.py --source [--out docs/cadtutorial] +""" + +import argparse +import os +import re +import shutil +import sys + +SECTION_SORT = 9 +SECTION_TITLE = "CAD simulation tutorial" + +# The reading order of the tutorial, and the title each page carries here. +PAGES = [ + ("index.md", "README.md", "CAD simulation tutorial"), + ("install.md", "install.md", "Install the software"), + ("first-conversion.md", "first-conversion.md", "Convert your first model"), + ("representation.md", "representation.md", "How a part is represented"), + ("partial.md", "partial.md", "Convert only part of a model"), + ("materials.md", "materials.md", "Give it materials"), + ("field-and-cuts.md", "field-and-cuts.md", "Field and cuts"), + ("geom-c.md", "geom-c.md", "The geom.C file"), + ("passive.md", "passive.md", "Add passive geometry"), + ("hits.md", "hits.md", "Make it produce hits"), + ("real-detector.md", "real-detector.md", "Grow it into a real detector"), + ("its-round-trip.md", "its-round-trip.md", "The ITS, out and back again"), + ("checks.md", "checks.md", "Check your geometry"), + ("limits.md", "limits.md", "Limits and pain points"), +] + +ALERT = {"NOTE": "note", "TIP": "tip", "IMPORTANT": "note", + "WARNING": "warning", "CAUTION": "danger"} + +SOURCE_URL = ("https://github.com/AliceO2Group/AliceO2/tree/dev/" + "Detectors/CADSupport/doc/tutorial") + +PROVENANCE = f""" + +--- + +*These pages are generated from the tutorial sources in AliceO2, +[Detectors/CADSupport/doc/tutorial]({SOURCE_URL}), which is where corrections belong.* +""" + + +def alerts_to_fences(text): + """`> [!WARNING]` blocks become ```warning fences, whose body the theme markdownifies.""" + lines, out, i = text.split("\n"), [], 0 + while i < len(lines): + m = re.match(r"^> \[!(\w+)\]\s*$", lines[i]) + if not m or m.group(1) not in ALERT: + out.append(lines[i]) + i += 1 + continue + kind = ALERT[m.group(1)] + i += 1 + body = [] + while i < len(lines) and lines[i].startswith(">"): + body.append(lines[i][2:] if lines[i].startswith("> ") else lines[i][1:]) + i += 1 + if any(b.startswith("```") for b in body): + raise SystemExit("a code fence inside an alert cannot be carried into a " + "fenced callout; rewrite the source page") + out.append("```" + kind) + out.extend(body) + out.append("```") + return "\n".join(out) + + +def relink(text, names): + """Links between tutorial pages: index.md is README.md here, the rest keep their names.""" + text = text.replace("(index.md)", "(README.md)") + return text + + +def front_matter(sort, title): + return f"---\nsort: {sort}\ntitle: {title}\n---\n\n" + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--source", required=True, + help="an AliceO2 checkout, or its Detectors/CADSupport/doc/tutorial directory") + p.add_argument("--out", default="docs/cadtutorial") + args = p.parse_args() + + src = args.source + if os.path.isdir(os.path.join(src, "Detectors")): + src = os.path.join(src, "Detectors/CADSupport/doc/tutorial") + docs = os.path.join(src, "docs") + if not os.path.isdir(docs): + raise SystemExit(f"no tutorial sources under {docs}") + + os.makedirs(args.out, exist_ok=True) + names = {a for a, _, _ in PAGES} + written = [] + for i, (source, target, title) in enumerate(PAGES): + path = os.path.join(docs, source) + if not os.path.exists(path): + raise SystemExit(f"missing tutorial page: {path}") + body = relink(alerts_to_fences(open(path).read()), names) + sort = SECTION_SORT if target == "README.md" else i + with open(os.path.join(args.out, target), "w") as fh: + fh.write(front_matter(sort, SECTION_TITLE if target == "README.md" else title)) + fh.write(body) + if target == "README.md": + fh.write(PROVENANCE) + written.append(target) + + images_in = os.path.join(docs, "images") + if os.path.isdir(images_in): + images_out = os.path.join(args.out, "images") + os.makedirs(images_out, exist_ok=True) + for f in sorted(os.listdir(images_in)): + shutil.copy2(os.path.join(images_in, f), os.path.join(images_out, f)) + written.append(os.path.join("images", f)) + + print(f"wrote {len(written)} file(s) to {args.out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/cad-tutorial-sync.yml b/.github/workflows/cad-tutorial-sync.yml new file mode 100644 index 0000000..61108d9 --- /dev/null +++ b/.github/workflows/cad-tutorial-sync.yml @@ -0,0 +1,49 @@ +--- +# Regenerate docs/cadtutorial from the tutorial sources in AliceO2. +# +# The tutorial is written and reviewed next to the code it documents, in +# AliceO2 under Detectors/CADSupport/doc/tutorial. This job converts those +# pages into the form this site expects, so the section here never drifts +# from the converter it describes. + +name: CAD tutorial + +on: + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Fetch the tutorial sources + working-directory: ${{ runner.temp }} + run: | + git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/AliceO2Group/AliceO2.git aliceo2 + cd aliceo2 + git sparse-checkout set Detectors/CADSupport/doc/tutorial + + - name: Regenerate the section + run: | + python3 .github/scripts/sync_cad_tutorial.py \ + --source "${{ runner.temp }}/aliceo2" + + - name: Commit if anything changed + run: | + if git diff --quiet -- docs/cadtutorial; then + echo "already up to date" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/cadtutorial + git commit -m "Regenerate the CAD simulation tutorial from AliceO2" + git push diff --git a/docs/cadtutorial/README.md b/docs/cadtutorial/README.md new file mode 100644 index 0000000..5f19016 --- /dev/null +++ b/docs/cadtutorial/README.md @@ -0,0 +1,70 @@ +--- +sort: 9 +title: CAD simulation tutorial +--- + +# Simulating ALICE geometries that come from CAD + +Detectors are designed in CAD, but Geant transports particles through ROOT's TGeo geometry. This +guide is about crossing that gap automatically — taking an engineering model as it comes out of the +design office and turning it into something particles can be simulated through, all the way to hits +you can plot. + +The usual way of crossing that gap is to read the drawings and write the geometry again by hand, in +C++, volume by volume. That works, and most of ALICE was built this way, but it is slow, it is easy +to get subtly wrong, and every time the engineers move a bracket the translation has to be redone. +For a detector that is still being designed — which is exactly the situation during an upgrade study +— the hand-written geometry is out of date almost as soon as it is written. + +So instead we convert the CAD file directly. You export the assembly as STEP, run one converter over +it, and you get a ROOT macro that builds the geometry. From there a small JSON file tells `o2-sim` to +load that macro and place it in the ALICE world. Nothing is recompiled at any point, so the loop from +a new CAD revision to a new simulation takes minutes rather than weeks. + +Getting the geometry in is only half of it, though. A shape that particles fly through is a passive +obstacle; to do physics you want it to *record* something. The second half of this guide is therefore +about the external-detector mechanism, which lets you declare parts of your imported geometry +sensitive and have them write hits — again with no detector class and no rebuild. That is usually +enough to answer the first questions an upgrade study asks: does this thing get hit, how often, and +where. + +## What you will be able to do by the end + +- Install the converter and check that it works. +- Convert a STEP assembly and look at the result. +- Understand and control how faithfully each part is represented. +- Attach materials, and know what the magnetic field and physics cuts will and will not do. +- Place the geometry inside ALICE as passive material. +- Make parts of it sensitive, run a simulation, and count hits. +- Take an existing ALICE detector out to CAD and back, and simulate the result. +- Know where the system's limits are, so you do not discover them in your results. + +We assume you can run `o2-sim`, and nothing more. No CAD experience is needed, and no knowledge of +OpenCascade, which does the heavy lifting underneath but never has to be addressed directly. + +## Where the code lives + +Everything in this guide is in `Detectors/CADSupport` in [AliceO2](https://github.com/AliceO2Group/AliceO2). +`README.md` there is the complete option reference, and `doc/reference/` documents the solids, their +file formats and the recognition pipeline. + +## Contents + +**Start** — [Install the software](install.md) · [Convert your first model](first-conversion.md) + +**Converting** — [How a part is represented](representation.md) · +[Convert only part of a model](partial.md) · [Give it materials](materials.md) · +[Field and cuts](field-and-cuts.md) · [The geom.C file](geom-c.md) + +**Simulating** — [Add passive geometry](passive.md) · [Make it produce hits](hits.md) · +[Grow it into a real detector](real-detector.md) + +**Worked example** — [The ITS, out and back again](its-round-trip.md) + +**Reference** — [Check your geometry](checks.md) · [Limits and pain points](limits.md) + + +--- + +*These pages are generated from the tutorial sources in AliceO2, +[Detectors/CADSupport/doc/tutorial](https://github.com/AliceO2Group/AliceO2/tree/dev/Detectors/CADSupport/doc/tutorial), which is where corrections belong.* diff --git a/docs/cadtutorial/checks.md b/docs/cadtutorial/checks.md new file mode 100644 index 0000000..7da723a --- /dev/null +++ b/docs/cadtutorial/checks.md @@ -0,0 +1,70 @@ +--- +sort: 12 +title: Check your geometry +--- + +# Check your geometry + +Before trusting any physics that came out of a conversion, it is worth spending a few minutes on four +checks. They are ordered cheapest first, and in practice the first two catch most problems. + +## 1 · Read the cascade table + +The converter already told you what it decided for every part, and wrote the same information to +`csg_report.json`. A part that declined CSG says which test it failed and by how much, which is often +enough to see that a model is nearly-but-not-quite a primitive. A large tessellated count on a model +you expected to be analytic is the signal to look at `--recognize-surfaces` and the surface report +below. + +## 2 · Look for overlaps + +Run `build_and_export("geom.root", true, true)` to get `CheckOverlaps`; zero illegal overlaps is what +you want to see. A non-zero count is worth taking seriously, but do not assume it is the conversion's +fault: engineering assemblies are drawn for manufacture, not for particle transport, and slightly +interpenetrating parts are common in perfectly good CAD models. + +## 3 · Confirm the exact solids really load + +Successfully extracting a solid's surfaces does not guarantee the result is a usable, watertight body. +This macro loads every `surfaces_*.bin` in a directory the same way the transport does, and reports +closure, orientation consistency and enclosed volume: + +```bash +# $O2_SRC is your AliceO2 source directory +root -l -b -q "$O2_SRC/Detectors/CADSupport/test/checkSurfaceSidecars.macro(\"cad_out/excavator\")" +``` + +```text +OK surfaces_Bucket_0_1_1_6.bin surfaces= 97 closed=1 orient=1 capacity=58.3121 +OK surfaces_Base_0_1_1_3.bin surfaces= 44 closed=1 orient=1 capacity=241.281 +... +SUMMARY cad_out/excavator + sidecars found : 13 + loaded : 13 + rejected by the reader : 0 + loaded but not IsClosed() : 0 + orientation inconsistent : 0 +``` + +`closed=1` means the solid is a watertight manifold, which is precisely what navigation requires. Any +non-zero number on the last three summary lines identifies a part that will not transport correctly. + +## 4 · Find out what the geometry really is + +A subtlety worth knowing: the surface type stored in a STEP file describes the *exporter*, not the +geometry. CAD kernels routinely write an exact cylinder as a rational B-spline, which is an exact +representation rather than an approximation — but dispatching on the stored type would throw that +exactness away. The converter therefore classifies faces by their actual shape, and its surface report +shows the effect: + +```bash +# a per-face classification, written alongside a normal conversion +--surface-report cad_out/mydet/surface_report.json +``` + +## Going further + +`Detectors/CADSupport/validation/` holds the tools the development of this system is validated with: +an acceptance gate that scores converted parts against the OpenCascade oracle, an overlap census, a +round-trip report, and the closure test that the [ITS example](its-round-trip.md) follows. They are +not installed — run them from the source tree. `README.md` lists them all. diff --git a/docs/cadtutorial/field-and-cuts.md b/docs/cadtutorial/field-and-cuts.md new file mode 100644 index 0000000..1897ed1 --- /dev/null +++ b/docs/cadtutorial/field-and-cuts.md @@ -0,0 +1,48 @@ +--- +sort: 6 +title: Field and cuts +--- + +# Field and cuts + +There is one place where the converter cannot give you everything, and it is worth being explicit +about rather than discovering later. A CAD file describes a *part*. It cannot describe how you want +that part simulated — how the magnetic field should be integrated through it, how long a step may be, +which secondaries are worth producing. Those are simulation choices, and no CAD format has anywhere +to record them. + +## Magnetic field + +For the field there is a clean answer. Pass `--in-field` when the module sits inside the magnet, and +the emitted macro will ask the **live** field for its integration method and maximum field strength +at the moment the geometry is built — which is exactly what a hand-written O2 detector does from its +own `createMaterials()`. Nothing is baked into the file: + +`geom.C · emitted` + +```cpp +int cad_ifield = 2; +float cad_fieldm = 10; +cadFieldTrackingParams(cad_ifield, cad_fieldm); // queries the loaded field +med_Stainless_Steel->SetParam(1, cad_ifield); // ifield, from the live field +med_Stainless_Steel->SetParam(2, cad_fieldm); // fieldm, from the live field +``` + +The `2,10` you see there is only a seed, used if no field happens to be loaded, and `--in-field 1,5.5` +overrides it. To confirm that the query really happened, check `fieldm` rather than `ifield`: +`ifield = 2` is also the seed value and therefore proves nothing, whereas a `fieldm` the seed could +not have produced — ALICE reports 15 — proves the live field answered. + +## Step control and physics cuts + +```warning +**These silently default to nothing** + +Without `--in-field`, a CAD-authored medium is built through ROOT's three-argument `TGeoMedium` +constructor, which **zeroes every parameter** — including `ifield`, meaning no field tracking at +all. Step control (`tmaxfd stemax deemax epsil stmin`) stays at the transport default in every +case, and special physics cuts are never applied, because there is no `simcuts.dat` for a module +with no detector directory to hold one. None of this is loud: the simulation runs and the numbers +look plausible. So set `--in-field` deliberately, and treat cuts as a known open item until your +study grows into a [real detector](real-detector.md), which is where they come back. +``` diff --git a/docs/cadtutorial/first-conversion.md b/docs/cadtutorial/first-conversion.md new file mode 100644 index 0000000..fefcd65 --- /dev/null +++ b/docs/cadtutorial/first-conversion.md @@ -0,0 +1,97 @@ +--- +sort: 2 +title: Convert your first model +--- + +# Convert your first model + +Rather than start on your own detector, it is worth converting something small and known-good first, +so that anything odd later is clearly your model and not your installation. A toy excavator arm is +committed to the repository for exactly this purpose: + +```text +$O2_ROOT/share/CADSupport/examples/ExcavatorArm.step # 13 leaf solids, ~500 kB +``` + +It converts in seconds and is varied enough to be interesting: the hydraulic rams and pivot pins are +plain cylinders, the boom and stick are machined bodies full of concave features, and the bucket has +a torus in it. Run the converter over it, asking for all three representations at once — we come back +to what those are in the next section: + +```bash +mkdir -p cad_out/excavator +o2-cad-to-tgeo \ + $O2_ROOT/share/CADSupport/examples/ExcavatorArm.step \ + --output-folder cad_out/excavator \ + -o geom.C \ + --step-unit auto \ + --csg auto --exact-surfaces auto --mesh --mesh-prec 0.05 +``` + +That takes about thirteen seconds. Along the way the converter prints three lines worth reading on +*every* run, because each one catches a different common mistake: + +```text +Detected STEP length unit: mm (scale to cm = 0.1) +Placement check: 13 leaf placement(s), all at distinct world transforms. +Emitting 13/13 logical volumes as exact O2BVHSurfaceSolid +``` + +The unit line bites hardest. TGeo works in centimetres and most CAD systems export millimetres, so a +silent unit error gives you a detector ten times too big and a simulation that still looks almost +plausible. `--step-unit auto` reads the declaration in the file; pass `--step-unit mm` explicitly when +the file declares something you do not believe. The placement line then tells you whether two leaves +landed on the same world transform, which almost always means a duplicated part in the CAD model +rather than a real coincidence. + +Finally the converter prints what it decided for each part, ending in a one-line summary: + +```text +=== REPRESENTATION CASCADE (per leaf solid) === + volume carried by evidence + BasePin csg TGeoTube(rmin=0, rmax=1, dz=5) [tier1-tube], dV_sym=0 cm^3 + Base surface declined CSG: 7 axis clusters: beyond the recogniser's scope ... + BoomCylinderOuter csg TGeoTube(0.6,1,7.991) u TGeoTube(0.7,1.5,1.5), dV_sym=0 cm^3 + ... + tiers: CSG 7, exact surfaces 6, tessellated 0 (of 13 leaf solids) +``` + +Seven parts came out as ordinary ROOT shapes, six as exact surface solids, and none had to fall back +to an approximate mesh. The `dV_sym=0` is the reassuring part: it is the symmetric-difference volume +between what was emitted and the original CAD solid, so zero means the conversion is exact rather +than merely close. + +## Look at what you made + +Numbers in a terminal are no substitute for seeing the thing. The macro can build the geometry and +write it out as an ordinary ROOT file: + +```bash +cd cad_out/excavator +root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root");' +``` + +![A shaded render of the converted excavator arm: bucket, stick, boom and hydraulic rams, seen from above and to the side.](images/excavator_render.png) + +*The converted model, drawn by casting one ray per pixel through the TGeo navigator — so this is the +geometry as the transport sees it, not a separate preview mesh.* + +The simplest interactive way to inspect the result is ROOT's own web display, which renders the +geometry with JSROOT in your browser and lets you rotate it, hide volumes and click through the tree: + +```bash +root --web geom.root +``` + +If you are on a remote machine where opening a browser is awkward, export the geometry as a JSROOT +document instead and open that file locally. It is a self-contained 32 kB for this model, and can be +dragged straight onto [root.cern/js](https://root.cern/js/): + +```bash +root -l -b -q -e 'TGeoManager::Import("geom.root");' \ + -e 'TBufferJSON::ExportToFile("excavator.json.gz", gGeoManager);' +``` + +Spend a minute here. Turning the model around is the fastest way to notice that a subassembly is +missing, that something sits at the wrong scale, or that the part you care about was quietly filtered +out. diff --git a/docs/cadtutorial/geom-c.md b/docs/cadtutorial/geom-c.md new file mode 100644 index 0000000..b3c4d95 --- /dev/null +++ b/docs/cadtutorial/geom-c.md @@ -0,0 +1,40 @@ +--- +sort: 7 +title: The geom.C file +--- + +# The geom.C file + +Everything the converter does ends up in one ROOT macro, and it is the artefact worth caring about. +It exports two functions: `get_builder_hook_unchecked()`, which is what `o2-sim` calls when it loads +your geometry, and `build_and_export()`, which you already used to look at the model on its own. + +Alongside it, the output folder holds the binary payloads the macro reads — `facets_*.bin` for meshed +parts, `surfaces_*.bin` for exact ones and `flatcsg_*.bin` for flat CSG solids — plus +`csg_report.json`, which records what each part became and why. + +```warning +**The macro and its binaries travel together** + +`geom.C` loads those `.bin` files **relative to its own location**. Move or copy the macro without +the rest of its folder and it will build an empty geometry without complaining. Always move the +directory. +``` + +`build_and_export()` runs `CheckOverlaps` only when asked, because on large models it is slow: + +```bash +root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root", true, true);' +``` + +```text +Info in : 14 nodes/ 14 volume UID's in geom +Info in : Checking overlaps for Assembly and daughters within 0.1 +Info in : Number of illegal overlaps/extrusions : 0 +``` + +Finally, a structural point that shapes how you organise your work: each converted directory holds +exactly one `geom.C`, and each `geom.C` describes one thing you hook into the simulation. If your +study involves three CAD subsystems, you run the converter three times into three folders. They +coexist without trouble, because the loader compiles each macro into its own namespace at run time, +so the identical function names inside them never collide. diff --git a/docs/cadtutorial/hits.md b/docs/cadtutorial/hits.md new file mode 100644 index 0000000..92a7864 --- /dev/null +++ b/docs/cadtutorial/hits.md @@ -0,0 +1,122 @@ +--- +sort: 9 +title: Make it produce hits +--- + +# Make it produce hits + +Passive geometry answers questions about material budget. To ask whether your detector is actually +hit, and how often, some of its volumes need to be sensitive. This is the fastest route from a CAD +file to plottable hits, and it still needs no detector class and no rebuild — we simply change the +array name to `externalDetectors` and say which volumes should record: + +`externalGeometry.json` + +```json +{ + "externalDetectors": [ + { + "name": "EXCV", + "title": "Excavator as a sensitive detector", + "macro": "cad_out/excavator/geom.C", + "anchor": "barrel", + "detID": "TST", + "sensitiveVolumes": ["Bucket"], + "placement": { "translation": [21.01, -13.22, -19.66] } + } + ] +} +``` + +## Choosing the sensitive volumes + +There are two ways of selecting them, and you may use either or both as long as at least one is +non-empty. `sensitiveVolumes` matches against TGeo volume names, and `sensitiveMedia` matches against +medium names — the latter being a convenient way to make every silicon part in an assembly sensitive +at once, however the parts happen to be named. + +```warning +**Both match substrings, not whole names** + +This catches people out. On the excavator model, `"sensitiveVolumes": ["Bucket"]` selects **five** +volumes rather than one — `Bucket`, `BucketLink1`, `BucketLink2`, `BucketCylinderInner` and +`BucketCylinderOuter`. The startup log prints every volume it registered, so read it and tighten +the string if that was not what you meant. +``` + +## Choosing a DetID + +The `detID` field ties your detector to an existing O2 detector identity, which is what determines +where the hits are filed. Pick a slot no active built-in detector is using: + +- `TST` is the general-purpose test slot, and the right default for a quick study. +- An upgrade study normally borrows the slot it stands in for — `TRK` for an ALICE 3 tracker, for + instance — because it is semantically honest and keeps downstream tooling happy. + +The hit branch keeps *your* module name rather than the borrowed one, so the configuration above +produces a branch called `EXCVHit`. + +## Running it + +```bash +o2-sim-serial -n 3 -g boxgen --seed 42 \ + --detectorList EXTCAD:detectorlist.json \ + --extGeomFile externalGeometry.json \ + --configKeyValues 'BoxGun.number=500;BoxGun.pdg=211;BoxGun.eta[0]=-1;BoxGun.eta[1]=1;BoxGun.prange[0]=2.0;BoxGun.prange[1]=5.0' +``` + +```text +External detector EXCV: 5 sensitive volume(s) selected +External detector EXCV: registered sensitive volume 'Bucket' (MC volID 8, sensor 0) +CREATING BRANCH EXCVHit +External detector EXCV EndOfEvent: 681 sensitive step(s) -> 94 hit(s) +External detector EXCV EndOfEvent: 402 sensitive step(s) -> 59 hit(s) +External detector EXCV EndOfEvent: 927 sensitive step(s) -> 124 hit(s) +``` + +The hits land in `o2sim.root`, one entry per event: + +```bash +root -l -b -q -e 'TFile f("o2sim.root"); TTree *t=(TTree*)f.Get("o2sim"); + t->Draw("EXCVHit@.size()");' +``` + +```note +**Zero hits is usually aim, not breakage** + +The most common first result is `0 sensitive step(s)`, and the instinct is to suspect the +conversion. Check where the particles are going first. The run above produces nothing at all at +the default multiplicity of 10, simply because the excavator is a 40 cm object sitting 40 cm +off-axis and is a small target. Raise the multiplicity or aim the gun. To rule out the geometry +independently, shoot a ray through it in ROOT with `gGeoManager->FindNextBoundaryAndStep()` and +print the volume names you cross — if they appear, navigation is fine and the problem is aim. +``` + +## Custom sensitive actions + +With no further configuration, every sensitive volume records a charged-track entrance and exit hit in +the generic `o2::ext::Hit` format: position in and out, momentum, energy loss, PDG code and track +length. That is enough for occupancy, acceptance and material studies, which covers most first +questions. + +When you need something else — a different hit definition, a cut applied at scoring time, extra +quantities — you can point at a macro returning an `o2::ext::ExternalDetector::SensitiveFcn`. It is +compiled at run time and can query `TVirtualMC::GetMC()` and call helpers such as `currentSensorID()`, +`currentTrackID()` and `addHit()`: + +`externalGeometry.json · fragment` + +```json +"sensitiveMedia": ["Silicon"], +"sensitiveMacro": "sensitive_action.macro", +"sensitiveFunction": "sensitiveAction()" +``` + +```note +**A worked example that needs no CAD file** + +`run/SimExamples/External_Sensitive_Detectors` defines two artificial detectors entirely from data +— one using the built-in action, one with a custom action compiled at run time — from hand-written +macros that mimic converter output. Running `./run.sh` in that directory shows both hit branches +appearing. +``` diff --git a/docs/cadtutorial/images/excavator_cascade.png b/docs/cadtutorial/images/excavator_cascade.png new file mode 100644 index 0000000..be16f31 Binary files /dev/null and b/docs/cadtutorial/images/excavator_cascade.png differ diff --git a/docs/cadtutorial/images/excavator_mesh_only.png b/docs/cadtutorial/images/excavator_mesh_only.png new file mode 100644 index 0000000..0207713 Binary files /dev/null and b/docs/cadtutorial/images/excavator_mesh_only.png differ diff --git a/docs/cadtutorial/images/excavator_render.png b/docs/cadtutorial/images/excavator_render.png new file mode 100644 index 0000000..55e6d4b Binary files /dev/null and b/docs/cadtutorial/images/excavator_render.png differ diff --git a/docs/cadtutorial/install.md b/docs/cadtutorial/install.md new file mode 100644 index 0000000..f963233 --- /dev/null +++ b/docs/cadtutorial/install.md @@ -0,0 +1,77 @@ +--- +sort: 1 +title: Install the software +--- + +# Install the software + +The converter is a Python script, but it leans on OpenCascade — the CAD kernel that reads STEP files +— through its Python bindings, `pythonOCC`. That is the one piece you have to provide yourself. + +```warning +**pythonOCC is not part of O2sim** + +It is a separate aliBuild package, and it is **not** pulled in when you build or load `O2sim`. +If you have never built it, that is genuinely step one — no amount of loading `O2sim` will +conjure it up. +``` + +So we build it first. This pulls in OpenCascade itself as a dependency, and takes a while the first +time: + +```bash +cd ~/alisw +aliBuild build pythonOCC --defaults o2 --no-system SWIG +``` + +The `--no-system SWIG` is worth keeping even when aliBuild tells you the system SWIG will do. The +recipe asks for SWIG 4.2.1 and several distributions ship 4.2.0, which is close enough to be picked +up and not close enough to build. Forcing aliBuild to build its own costs a few minutes once and +saves a confusing failure later. + +With that in place, everything happens in a single shell. We load `pythonOCC` together with `O2sim`, +because the converter needs ROOT as well as OpenCascade — and the same environment then runs `o2-sim` +afterwards, so there is no need to switch shells between converting and simulating: + +```bash +alienv enter O2sim/latest,pythonOCC/latest +``` + +Two quick checks confirm the environment is sound. The first proves the CAD bindings import at all; +the second runs the converter's own self-test, which builds its test cases in memory and needs no +input file: + +```bash +python3 -c "import OCC.Core.Bnd; print('OCC import OK')" +o2-cad-to-tgeo --self-test +``` + +```text +OCC import OK +... +20/20 in-field media checks passed +``` + +The two commands you will use throughout are `o2-cad-to-tgeo`, which takes STEP to TGeo, and +`o2-tgeo-to-cad`, which takes TGeo back to STEP. They are also installed under their older names, +`O2_CADtoTGeo.py` and `O2_TGeoToCAD.py`, which work identically. + +```note +**If the import fails with “No module named 'OCC'”** + +Some `pythonOCC` installations carry a modulefile that puts the `OCC` package directory itself on +`PYTHONPATH`, rather than the `site-packages` directory containing it — so Python looks inside the +package and never finds it. The cure is to drop the trailing `/OCC` from the +`prepend-path PYTHONPATH` line in `$PYTHONOCC_ROOT/etc/modulefiles/pythonOCC`. A recipe fix is on +its way to alidist. +``` + +## Outside the ALICE stack + +A conda environment with `pythonocc-core` also works. There, run the script from the source tree: + +```bash +conda create -n occ -c conda-forge python=3.10 pythonocc-core -y +conda activate occ +python3 $O2_SRC/Detectors/CADSupport/tools/O2_CADtoTGeo.py --help +``` diff --git a/docs/cadtutorial/its-round-trip.md b/docs/cadtutorial/its-round-trip.md new file mode 100644 index 0000000..8407ac8 --- /dev/null +++ b/docs/cadtutorial/its-round-trip.md @@ -0,0 +1,213 @@ +--- +sort: 11 +title: The ITS, out and back again +--- + +# The ITS, out and back again + +Everything so far started from a CAD file. This example starts from ALICE itself: we take the ITS as +O2 builds it, export it to STEP, convert it back, and simulate hits in the result. It is the most +realistic thing you can do with the tools, because the answer is known — the same detector, +transported by the same Geant, is sitting right next to it. + +It is also the standard way of testing the converter on a part you do not have a CAD file for. Any +O2 module works the same way. + +The four steps are: + +```mermaid +flowchart LR + A["o2-sim -m ITS
o2sim_geometry.root"] --> B["o2-tgeo-to-cad
ITS.step + media sidecar"] + B --> C["o2-cad-to-tgeo
conv/geom.C"] + C --> D["o2-sim
external detector → hits"] +``` + +## 1 · The source geometry + +`-n 0` builds the geometry, writes it and transports nothing: + +```bash +mkdir -p its_roundtrip && cd its_roundtrip +o2-sim-serial -n 0 -g boxgen -m ITS -o o2sim +``` + +That leaves `o2sim_geometry.root`, which is the input to the export. + +## 2 · TGeo to STEP + +```bash +o2-tgeo-to-cad o2sim_geometry.root ITS.step \ + --top barrel \ + --hollow-volume barrel --hollow-tag ITS \ + --media-json ITS_media.json \ + --report ITS_writer_report.json +``` + +```text +Step File Name : ITS.step(278254 ents) Write Done +261 solids, 84 volumes with daughters, 29 pure assemblies, 1996 components, 2 volumes declined +capacity check: max relative deviation 2.012e-02, median 3.365e-16 +report: ITS_writer_report.json (28.22 s, 16.17 MB) +media: ITS_media.json (33 media over 261 parts) +``` + +Three of those options deserve a word. + +`--top barrel` converts the subtree under `barrel`, which is where `o2-sim` hangs the ITS. Converting +from the world root instead would drag the experiment hall along with it. + +`--hollow-volume barrel` emits `barrel` as a pure assembly: its daughters keep their own transforms, +but the volume itself contributes no body. This matters because `o2-sim` always builds `cave`, +`barrel` and `caveRB24` itself, whatever module list it is given — shipping a second copy would put +two coincident air boxes in the world. `--hollow-tag ITS` then suffixes the hollowed name, so two +modules exported from the same world do not collide when they are placed together. + +`--media-json` is the sidecar that makes this a *round trip* rather than a one-way conversion. It +records every medium as O2 built it, so the back-conversion can rebuild them verbatim instead of +guessing materials from part names. + +The `capacity check` line is the writer's own verification: it compares the volume of each solid it +wrote against the volume ROOT reports for the original shape. A median deviation of 3.4e-16 is machine +precision. + +## 3 · STEP back to TGeo + +```bash +o2-cad-to-tgeo ITS.step -o geom.C --output-folder conv \ + --csg auto --exact-surfaces auto --mesh \ + --media-json ITS_media.json +``` + +This one takes about five minutes — the ITS is 261 solids, several of which are deep boolean +constructions. + +```text +Detected STEP length unit: mm (scale to cm = 0.1) +Placement check: 296716 leaf placement(s), all at distinct world transforms. + tessellation is EXACT (every face a planar polygon) for 142 of 261 part(s) -- 54.4 % + tiers: CSG 252, exact surfaces 9, tessellated 0 (of 261 leaf solids) +Media from sidecar: 261/261 volumes carry their source medium +Wrote ROOT macro: .../conv/geom.C +``` + +Two lines to read carefully. `tiers: CSG 252, exact surfaces 9, tessellated 0` says the whole ITS came +back exactly: 252 parts as ordinary ROOT shapes, nine as exact surface solids, and nothing at all fell +through to the approximate mesh. `Media from sidecar: 261/261` says every volume got its original +medium back rather than a placeholder. + +You can check the media independently: + +```bash +python3 $O2_SRC/Detectors/CADSupport/validation/closure/check_media.py \ + --original o2sim_geometry.root --macro conv/geom.C --rtol 1e-6 \ + --writer-report ITS_writer_report.json +``` + +```text +converted volumes with a medium: 261 + media identical to the source: 261 + left on the Default placeholder (transparent): 0 + disagreeing with the source: 0 +VERDICT: every volume carries its source medium +``` + +```note +**One shell or two** + +The converter and `o2-sim` share one `alienv enter O2sim/latest,pythonOCC/latest` shell. If your +`pythonOCC` modulefile still has the `PYTHONPATH` defect described in +[Install the software](install.md), that same path makes `o2-sim` segfault at startup — run the +converter in a shell of its own until the modulefile is fixed. +``` + +## 4 · Hits from the converted ITS + +Now hook it in. The sensitive volumes are the seven ITS sensor volumes, `ITSUSensor0` … `ITSUSensor6`, +which one substring selects. Because the geometry was converted from `barrel` with `barrel` hollowed, +it goes back into the real `barrel` with no placement at all — every part lands at exactly the +transform the source geometry gave it: + +`externalGeometry.json` + +```json +{ + "externalDetectors": [ + { + "name": "CITS", + "title": "CAD round-tripped ITS", + "macro": "conv/geom.C", + "anchor": "barrel", + "detID": "ITS", + "sensitiveVolumes": ["ITSUSensor"] + } + ] +} +``` + +`detectorlist.json` + +```json +{ "CADITS": ["CITS"] } +``` + +```bash +o2-sim-serial -n 3 -g boxgen --seed 42 \ + --detectorList CADITS:detectorlist.json \ + --extGeomFile externalGeometry.json \ + --configKeyValues 'SimCutParams.trackSeed=true;BoxGun.number=100;BoxGun.pdg=211;BoxGun.eta[0]=-1;BoxGun.eta[1]=1;BoxGun.prange[0]=2.0;BoxGun.prange[1]=5.0' +``` + +```text +External detector CITS: 7 sensitive volume(s) selected +External detector CITS: registered sensitive volume 'ITSUSensor0' (MC volID 13, sensor 0) +External detector CITS: registered sensitive volume 'ITSUSensor1' (MC volID 61, sensor 1) +... +External detector CITS: registered sensitive volume 'ITSUSensor6' (MC volID 264, sensor 6) +CREATING BRANCH CITSHit +External detector CITS EndOfEvent: 1825 sensitive step(s) -> 849 hit(s) +External detector CITS EndOfEvent: 1862 sensitive step(s) -> 887 hit(s) +External detector CITS EndOfEvent: 1754 sensitive step(s) -> 869 hit(s) +``` + +The ITS that came back from CAD is producing hits, on the `ITS` DetID slot, in a branch called +`CITSHit`. No detector class was written and nothing was recompiled. + +## Is it the same detector? + +The cheapest answer is the radius of the hits. Run the native ITS with the same gun and the same seed + +```bash +o2-sim-serial -n 3 -g boxgen --seed 42 -m ITS -o native \ + --configKeyValues 'SimCutParams.trackSeed=true;BoxGun.number=100;BoxGun.pdg=211;BoxGun.eta[0]=-1;BoxGun.eta[1]=1;BoxGun.prange[0]=2.0;BoxGun.prange[1]=5.0' +``` + +and histogram the hit radius on both sides: + +```cpp +sqrt(ITSHit.mPos.fCoordinates.fX**2 + ITSHit.mPos.fCoordinates.fY**2) // native, in native_HitsITS.root +sqrt(CITSHit.mPos.fCoordinates.fX**2 + CITSHit.mPos.fCoordinates.fY**2) // CAD, in o2sim.root +``` + +| r (cm) | 1.9 | 2.6 | 3.4 | 4.1 | 19.1 | 19.9 | 24.4 | 25.1 | 34.1 | 34.9 | 38.6 | 39.4 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| native | 14 | 158 | 171 | 185 | 69 | 201 | 265 | 73 | 273 | 101 | 58 | 299 | +| CAD | 74 | 270 | 346 | 362 | 166 | 207 | 348 | 53 | 197 | 187 | — | 395 | + +Every populated radius is populated on both sides, and no hit appears anywhere else: the three inner +barrel layers and the four outer ones are exactly where the native ITS puts them, to the bin. That is +the geometry check, and it passes. + +The *counts* are not the same, and should not be read as one. The two runs are not on identical +physics: a module loaded through the JSON mechanism has no detector directory and therefore no +`simcuts.dat`, so its production cuts are not the ones the ITS sets for itself, and it makes more +low-energy secondaries. Carrying the cuts across takes a cut dump from the baseline, a probe run to +learn the CAD run's own medium indices, and a remap by medium name — which is exactly what +`validation/closure/` does: + +```bash +$O2_SRC/Detectors/CADSupport/validation/closure/run_closure.sh +``` + +It runs PIPE, ITS, TPC and MAG through the same round trip, remaps the cuts, and then compares hits +and material budget between the two sides properly. Use it when you need a number; use the radius +histogram above when you need to know, in a minute, that your geometry arrived where it should. diff --git a/docs/cadtutorial/limits.md b/docs/cadtutorial/limits.md new file mode 100644 index 0000000..cbda32c --- /dev/null +++ b/docs/cadtutorial/limits.md @@ -0,0 +1,33 @@ +--- +sort: 13 +title: Limits and pain points +--- + +# Limits and pain points + +The honest list. These are the things known to catch people today, roughly in order of how often they +do it. None is a reason not to use the system, but all of them are cheaper to read about here than to +rediscover in a result. + +| What | Why it happens | What to do | +| --- | --- | --- | +| One `geom.C` per hooked thing | The macro exports a single builder hook, and that hook is what the JSON refers to. | Run the converter once per subsystem, into its own folder. They coexist happily in one JSON. | +| Media, cuts and field default to zero | A CAD file carries a material, never a medium, and the emitter uses a three-argument `TGeoMedium` which zeroes every parameter. | Pass `--in-field`. Accept transport defaults for step control, and treat production cuts as unset until you write a real detector. | +| The anchor volume must already exist | Placement is expressed inside the frame of an existing O2 volume. | Use `barrel` unless you have a reason not to, and remember it sits at cave `(0, -30, 0)`. | +| Free-form surfaces stay tessellated | Genuine B-spline *surfaces* are not supported by the exact tier at all. | Check the surface report. Recognition already recovers quadrics written as NURBS, which is the large majority of them. | +| Illegal overlaps in the CAD model | Engineering assemblies are not drawn as legal transport worlds, and parts routinely interpenetrate. | Read `CheckOverlaps`, then fix in CAD or clip the offending region. | +| Degenerate facets at coarse precision | `O2Tessellated` drops triangles that collapse to a line. | Treat it as a mesh-quality signal: lower `--mesh-prec`, or move the part onto an exact tier. | +| A surprisingly huge output directory | Meshing a metre-scale curved part at a fine chord tolerance. | Convert large models without `--mesh`, and never use the default `--mesh-prec` on something metre-sized. | +| `o2-sim` complains about a missing `externalModules` array | Cosmetic. The message is emitted even when your JSON correctly contains only `externalDetectors`. | Ignore it. | + +## One rule that is not a preference + +Run `--csg auto` conversions **strictly serially**. Parallel runs race each other and silently lose +shapes, which produces a geometry that looks complete and is not — the worst possible failure mode, +and the hardest to notice afterwards. + +--- + +Deeper material lives in `Detectors/CADSupport`: `README.md` for the complete option reference, +`doc/reference/` for the exact-surface solid, its file format and the CSG pipeline, and +`doc/known-issues.md` for open defects. diff --git a/docs/cadtutorial/materials.md b/docs/cadtutorial/materials.md new file mode 100644 index 0000000..7af9c41 --- /dev/null +++ b/docs/cadtutorial/materials.md @@ -0,0 +1,61 @@ +--- +sort: 5 +title: Give it materials +--- + +# Give it materials + +So far the geometry has shape but no substance. Without material information every volume is assigned +a dummy medium called `Default`, which is fine while you are checking that things are in the right +place and quite wrong the moment you want physics out of it. + +The normal route is the **bill of materials** that the CAD system can export alongside the geometry. +We hand that to the converter as a CSV and it matches each part's material name against a Geant4 NIST +database. The rows it looks for are mechanical part rows in this shape: + +`detector_bom.csv` + +```csv +Type,...,Part Number,Version,Name,Mass (kg),Material +CAD,Mechanical/Part,Base,AA.01,Base,,Stainless Steel +CAD,Mechanical/Part,BasePin,AA.01,BasePin,,Stainless Steel +``` + +Adding both files to the conversion is all that is required: + +```bash +o2-cad-to-tgeo my.step \ + --output-folder cad_out/mydet -o geom.C \ + --csg auto --exact-surfaces auto --mesh --mesh-prec 0.05 \ + --materials-csv detector_bom.csv \ + --bom-mass-unit kg \ + --g4-nist-json $O2_ROOT/share/CADSupport/tools/g4_nist_database/G4_NIST_DB.json +``` + +```text +Loaded Geant4 NIST DB with 309 materials from: .../G4_NIST_DB.json +Loaded 13 BOM entries from: detector_bom.csv +``` + +Matching uses a combined score of name similarity and density plausibility, which handles the fact +that engineers write “Stainless Steel” where Geant4 says `G4_STAINLESS-STEEL`. A confident match +becomes a real `TGeoMixture` carrying its element composition, radiation length and interaction +length. An ambiguous or missing one falls back to a simple material and leaves a comment in `geom.C` +naming the part — so unresolved materials stay visible and greppable rather than silently wrong. The +scoring thresholds are adjustable (`--mat-min-score`, `--mat-ambiguity-delta` and a few others), but +the defaults are usually right, and it is better to fix an ambiguous name in the BOM than to loosen +the matcher. + +One nice consequence of feeding in the BOM: where both a part mass and a CAD volume are available, +the converter derives an effective density from them. That is how a perforated bracket or a +partly-filled cable tray ends up with an honest average density instead of the density of solid +metal. + +```note +**If your model came from TGeo in the first place** + +Geometry exported out of ALICE with `o2-tgeo-to-cad` and coming back should use `--media-json` +instead. That rebuilds the original media verbatim, field by field, rather than guessing them from +names, and takes precedence over the BOM for every part it names. The +[ITS worked example](its-round-trip.md) does exactly this. +``` diff --git a/docs/cadtutorial/partial.md b/docs/cadtutorial/partial.md new file mode 100644 index 0000000..60d0184 --- /dev/null +++ b/docs/cadtutorial/partial.md @@ -0,0 +1,44 @@ +--- +sort: 4 +title: Convert only part of a model +--- + +# Convert only part of a model + +Real engineering assemblies contain far more than you want to simulate — the mounting frame, the +trolley it sits on, sometimes the building. Converting all of it wastes time and fills your geometry +with volumes no particle will ever reach, so the converter offers two independent ways of cutting a +model down. They combine freely. + +## Selecting by name + +The first is by name. `--include-name` and `--exclude-name` take regular expressions matched against +the part name stored in the CAD file, case-insensitively, and either may be repeated. Matching an +assembly takes its whole subtree along with it, which is usually what you want: + +```bash +--include-name 'Bucket' --exclude-name '^SOLID\b' +``` + +Add `--name-filter-case-sensitive` if you need the matching to respect case. + +## Selecting by region + +The second is geometric. `--clip-box` restricts the conversion to an axis-aligned box, given as +`xmin ymin zmin xmax ymax zmax` in the assembly's global frame. Note that these are **STEP file +units**, before the conversion to centimetres — so if your file is in millimetres, so is your clip +box: + +```bash +--clip-box -50 -50 -20 50 50 20 +``` + +Every solid is then classified against that box before any meshing happens. Solids fully outside are +dropped; solids fully inside are kept unchanged; and solids straddling the boundary are cut against +it with a boolean intersection, so only the part inside survives. Assemblies left with no surviving +children disappear from the output tree altogether. + +By default, subtrees that end up entirely inside the box keep their shared logical definitions, which +keeps the output compact when a part is repeated many times. If you need one distinct volume per +surviving occurrence instead — say because you want to name them individually later — pass +`--clip-deduplicate none`. diff --git a/docs/cadtutorial/passive.md b/docs/cadtutorial/passive.md new file mode 100644 index 0000000..42c67aa --- /dev/null +++ b/docs/cadtutorial/passive.md @@ -0,0 +1,63 @@ +--- +sort: 8 +title: Add passive geometry +--- + +# Add passive geometry + +With a macro in hand we can put the geometry into ALICE. The mechanism is deliberately data-driven: +two small JSON files, no code and no rebuild. We start with the simpler case — passive material such +as supports, cooling or cabling, which should scatter particles but does not record anything. That +goes into an `externalModules` array: + +`externalGeometry.json` + +```json +{ + "externalModules": [ + { + "name": "EXCV", + "title": "Excavator support structure from CAD", + "macro": "cad_out/excavator/geom.C", + "anchor": "barrel", + "placement": { + "translation": [21.01, -13.22, -19.66], + "rotation_deg": [0.0, 0.0, 0.0] + } + } + ] +} +``` + +| field | meaning | +| --- | --- | +| `name` | a short tag for the module. It must also appear in the module list below, or the module is silently skipped. | +| `macro` | the path to the `geom.C` you produced. | +| `anchor` | a volume that already exists in the ALICE geometry. `barrel` is the usual choice, and it sits at cave coordinates `(0, -30, 0)`. | +| `placement` | translation and rotation **within the anchor's frame**, in centimetres and degrees. | + +The second file is the module list, which is what actually switches the module on. The split exists +so that you can describe several modules in one geometry file and enable them individually: + +`detectorlist.json` + +```json +{ "EXTCAD": ["EXCV"] } +``` + +Then run the simulation, pointing at both: + +```bash +o2-sim-serial -n 1 -g boxgen \ + --detectorList EXTCAD:detectorlist.json \ + --extGeomFile externalGeometry.json +``` + +```text +Configured external module 'EXCV' from macro 'cad_out/excavator/geom.C' anchored to volume 'barrel' +Activating EXCV module +Setting special cuts for passive module EXCV +``` + +Those three lines mean your CAD geometry is in the simulation and particles are being transported +through it. You can list as many modules in the same array as you like. diff --git a/docs/cadtutorial/real-detector.md b/docs/cadtutorial/real-detector.md new file mode 100644 index 0000000..477f789 --- /dev/null +++ b/docs/cadtutorial/real-detector.md @@ -0,0 +1,41 @@ +--- +sort: 10 +title: Grow it into a real detector +--- + +# Grow it into a real detector + +```warning +**Not yet exercised end to end** + +Everything before this page has been run, with its output pasted from a real terminal. This route +follows from how `ExternalDetector` and the built-in detectors are written, but no detector has +yet been built this way. Treat it as a design rather than a recipe, and expect to debug it. +``` + +The external-detector route deliberately trades flexibility for speed: you get one generic hit type +and a borrowed `DetID`, and in exchange you get results the same afternoon. Once a study turns into a +real subdetector you will want your own hit class, your own digitisation and a `DetID` of your own — +and none of that requires giving up the CAD import. The generated geometry simply becomes one step +inside an ordinary O2 detector. + +Three changes to a normal detector implementation are involved: + +1. **Build the geometry from the macro instead of by hand.** Copy `geom.C` into your detector's + simulation directory and call its builder hook from `ConstructGeometry()`, in place of the + `new TGeoTube(...)` code you would otherwise write. Keep the `.bin` payloads beside it and install + them with the detector's data files, since the macro resolves them relative to itself. +2. **Register your own sensitive volumes.** Call `AddSensitiveVolume()` for the volumes the macro + created, using the names the converter derived from the CAD part names. Print them once from + `geom.root` and pin them down in code, because a rename in CAD would otherwise quietly unregister a + sensor. +3. **Write your own hits.** Implement `ProcessHits()` with your own hit class and your own `DetID`, + exactly as any hand-written detector does. Nothing about the geometry's CAD origin constrains this. + +Two things come back the moment you take this step, both of which the external-detector route cannot +offer: `initFieldTrackingParams()` called from your own `createMaterials()`, and +`SetSpecialPhysicsCuts()` reading a real `simcuts.dat` from your detector's data directory. That +closes the gap described under [Field and cuts](field-and-cuts.md). + +The payoff is that re-running the converter after a CAD change regenerates only the geometry. Your +detector code stays untouched, which is the whole point of importing rather than transcribing. diff --git a/docs/cadtutorial/representation.md b/docs/cadtutorial/representation.md new file mode 100644 index 0000000..79b9606 --- /dev/null +++ b/docs/cadtutorial/representation.md @@ -0,0 +1,83 @@ +--- +sort: 3 +title: How a part is represented +--- + +# How a part is represented + +You have just run a conversion where every part came out exact, which is a good outcome but not an +automatic one. It is worth understanding what the converter was choosing between, because on a real +detector those choices decide both how faithful your simulation is and how fast it runs. + +The difficulty is that CAD and TGeo describe solids in different languages. CAD describes a body by +its boundary surfaces — this face is a piece of a cylinder, trimmed by these curves. TGeo describes a +body by combining primitives — a tube minus a box, say. Neither language is a superset of the other, +so there is no single translation that always works. The converter therefore carries three different +answers and picks the best available one **for each leaf solid independently**. + +```mermaid +flowchart TD + A["my.step
CAD assembly"] --> B["o2-cad-to-tgeo
per leaf solid"] + B --> C["1 · CSG primitives
TGeoTube, booleans — exact"] + B --> D["2 · Exact surfaces
O2BVHSurfaceSolid — exact"] + B --> E["3 · Triangle mesh
O2Tessellated — fallback"] + C --> F["geom.C
+ binary payloads"] + D --> F + E --> F +``` + +The three are complementary rather than competing, and all of them end up in the same `geom.C`. +Nothing is ever lost along the way: a part that resists exact description still ships as a mesh, so a +conversion always produces a complete geometry. + +| Tier | What it is | Exact | Covers | Flag | +| --- | --- | --- | --- | --- | +| **CSG** | Native ROOT shapes — `TGeoTube`, `TGeoBBox`, `TGeoCone` and booleans of them | Yes | Mechanical parts that really are primitives. Fastest to navigate and smallest on disk, so it is tried first. | `--csg auto` | +| **Surfaces** | The part's real trimmed boundary faces carried into TGeo as `O2BVHSurfaceSolid`, with a bounding-volume hierarchy for ray queries | Yes | Anything whose faces are planes, cylinders, cones, spheres or tori, however complicatedly trimmed. | `--exact-surfaces auto` | +| **Mesh** | A triangle mesh as `O2Tessellated` | No | Everything else, as the fallback. Genuinely free-form surfaces end up here. | `--mesh` | + +The difference is easiest to see rather than describe. Below, the same model is converted twice: once +to triangles alone at a coarse tolerance, and once with the full cascade, coloured by which tier +carried each part. + +| Tessellated only | The cascade, by tier | +| --- | --- | +| ![The excavator arm converted to triangles only, showing faceted, polygonal silhouettes on the cylindrical rams.](images/excavator_mesh_only.png) | ![The same model with the full cascade: hydraulic rams and pins in green for CSG, machined bodies in blue for exact surfaces.](images/excavator_cascade.png) | + +On the left the cylinders have visibly polygonal silhouettes and flat shading bands — that is the +approximation you are accepting. On the right the rams and pivot pins were recognised as unions of +tubes and the machined bodies carried as their exact trimmed surfaces, so the curves are curves. Both +images are cast through the TGeo navigator with the same camera. + +In practice one asks for all three and lets the converter decide, which is what the `auto` values in +the earlier command did. Each of `--csg` and `--exact-surfaces` accepts three settings, and the third +is more useful than it looks: + +- `off` — never use this tier. This is the default for both, so a bare conversion gives you meshes + only, which is the left-hand picture above. +- `auto` — use it wherever it is accepted, and fall through quietly elsewhere. +- `required` — stop with a report if any part cannot be represented this way. Use it when you want to + *know* your geometry is exact rather than hope so. + +One thing to trust here: a part is only accepted as CSG when OpenCascade's symmetric-difference volume +against the original solid falls inside the model's own tolerance. The recogniser is never allowed to +be approximately right, which is why `dV_sym=0` keeps appearing in the evidence column. + +## Mesh precision, and one way to fill a disk + +When a part does fall through to the mesh tier, `--mesh-prec` sets both the linear deflection (in +model units) and the angular deflection (in radians) of the mesher: lower is finer and slower. For a +desk-scale part `0.05` is a reasonable default. For anything metre-scale you should be careful, +because the cost grows quickly with size — the default `0.1` applied to a two-metre sphere has +produced a **22.9 GB** output directory. The right move for large models is to leave `--mesh` off +entirely and let the two exact tiers carry them. + +```warning +**`--mesh-solid tgeo` does not navigate** + +The mesh tier defaults to `--mesh-solid o2`, which emits `o2::base::O2Tessellated` and needs the +O2 environment to load. The alternative, `--mesh-solid tgeo`, emits ROOT's own `TGeoTessellated`, +which implements none of `Contains`, `DistFromInside`, `DistFromOutside` or `Safety`. Every such +volume is then transported as its **filled bounding box**, silently and with no warning. Only +reach for it when the macro must load outside O2 and will never have a particle sent through it. +```