Skip to content

Commit bb9451a

Browse files
sawenzelclaude
andcommitted
Add the CAD simulation tutorial as a generated section
This adds a new documentation section, CAD simulation tutorial, and the job that keeps it in step with its sources. - docs/cadtutorial holds the section. It is generated, not edited here. - The sources live in AliceO2, Detectors/CADSupport/doc/tutorial, next to the converter they document. - .github/scripts/sync_cad_tutorial.py converts them: GitHub alerts become the theme's note and warning fences, and each page gets its front matter. - .github/workflows/cad-tutorial-sync.yml runs it weekly and on demand, and commits when the result changes. AliceO2Group/AliceO2#15790 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gNpas1pZ9DNBhVesoM5QZ
1 parent 8b5d64d commit bb9451a

19 files changed

Lines changed: 1252 additions & 0 deletions
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
3+
"""Generate the CAD simulation tutorial section from its sources in AliceO2.
4+
5+
The tutorial is written and reviewed in AliceO2, under
6+
`Detectors/CADSupport/doc/tutorial/docs`, so that it stays next to the code it documents.
7+
This script converts those pages into what this Jekyll site expects and writes them to
8+
`docs/cadtutorial`. Do not edit that directory by hand: the next run overwrites it.
9+
10+
Two things are converted.
11+
12+
* GitHub alerts (`> [!NOTE]`) become fenced `note` and `warning` blocks, which the theme
13+
renders as callouts. The bold line under the marker stays as the callout's own title,
14+
because the theme's title is fixed.
15+
* Every page gets the `sort`/`title` front matter the navigation is built from.
16+
17+
Usage:
18+
sync_cad_tutorial.py --source <AliceO2 checkout> [--out docs/cadtutorial]
19+
"""
20+
21+
import argparse
22+
import os
23+
import re
24+
import shutil
25+
import sys
26+
27+
SECTION_SORT = 9
28+
SECTION_TITLE = "CAD simulation tutorial"
29+
30+
# The reading order of the tutorial, and the title each page carries here.
31+
PAGES = [
32+
("index.md", "README.md", "CAD simulation tutorial"),
33+
("install.md", "install.md", "Install the software"),
34+
("first-conversion.md", "first-conversion.md", "Convert your first model"),
35+
("representation.md", "representation.md", "How a part is represented"),
36+
("partial.md", "partial.md", "Convert only part of a model"),
37+
("materials.md", "materials.md", "Give it materials"),
38+
("field-and-cuts.md", "field-and-cuts.md", "Field and cuts"),
39+
("geom-c.md", "geom-c.md", "The geom.C file"),
40+
("passive.md", "passive.md", "Add passive geometry"),
41+
("hits.md", "hits.md", "Make it produce hits"),
42+
("real-detector.md", "real-detector.md", "Grow it into a real detector"),
43+
("its-round-trip.md", "its-round-trip.md", "The ITS, out and back again"),
44+
("checks.md", "checks.md", "Check your geometry"),
45+
("limits.md", "limits.md", "Limits and pain points"),
46+
]
47+
48+
ALERT = {"NOTE": "note", "TIP": "tip", "IMPORTANT": "note",
49+
"WARNING": "warning", "CAUTION": "danger"}
50+
51+
SOURCE_URL = ("https://github.com/AliceO2Group/AliceO2/tree/dev/"
52+
"Detectors/CADSupport/doc/tutorial")
53+
54+
PROVENANCE = f"""
55+
56+
---
57+
58+
*These pages are generated from the tutorial sources in AliceO2,
59+
[Detectors/CADSupport/doc/tutorial]({SOURCE_URL}), which is where corrections belong.*
60+
"""
61+
62+
63+
def alerts_to_fences(text):
64+
"""`> [!WARNING]` blocks become ```warning fences, whose body the theme markdownifies."""
65+
lines, out, i = text.split("\n"), [], 0
66+
while i < len(lines):
67+
m = re.match(r"^> \[!(\w+)\]\s*$", lines[i])
68+
if not m or m.group(1) not in ALERT:
69+
out.append(lines[i])
70+
i += 1
71+
continue
72+
kind = ALERT[m.group(1)]
73+
i += 1
74+
body = []
75+
while i < len(lines) and lines[i].startswith(">"):
76+
body.append(lines[i][2:] if lines[i].startswith("> ") else lines[i][1:])
77+
i += 1
78+
if any(b.startswith("```") for b in body):
79+
raise SystemExit("a code fence inside an alert cannot be carried into a "
80+
"fenced callout; rewrite the source page")
81+
out.append("```" + kind)
82+
out.extend(body)
83+
out.append("```")
84+
return "\n".join(out)
85+
86+
87+
def relink(text, names):
88+
"""Links between tutorial pages: index.md is README.md here, the rest keep their names."""
89+
text = text.replace("(index.md)", "(README.md)")
90+
return text
91+
92+
93+
def front_matter(sort, title):
94+
return f"---\nsort: {sort}\ntitle: {title}\n---\n\n"
95+
96+
97+
def main():
98+
p = argparse.ArgumentParser(description=__doc__,
99+
formatter_class=argparse.RawDescriptionHelpFormatter)
100+
p.add_argument("--source", required=True,
101+
help="an AliceO2 checkout, or its Detectors/CADSupport/doc/tutorial directory")
102+
p.add_argument("--out", default="docs/cadtutorial")
103+
args = p.parse_args()
104+
105+
src = args.source
106+
if os.path.isdir(os.path.join(src, "Detectors")):
107+
src = os.path.join(src, "Detectors/CADSupport/doc/tutorial")
108+
docs = os.path.join(src, "docs")
109+
if not os.path.isdir(docs):
110+
raise SystemExit(f"no tutorial sources under {docs}")
111+
112+
os.makedirs(args.out, exist_ok=True)
113+
names = {a for a, _, _ in PAGES}
114+
written = []
115+
for i, (source, target, title) in enumerate(PAGES):
116+
path = os.path.join(docs, source)
117+
if not os.path.exists(path):
118+
raise SystemExit(f"missing tutorial page: {path}")
119+
body = relink(alerts_to_fences(open(path).read()), names)
120+
sort = SECTION_SORT if target == "README.md" else i
121+
with open(os.path.join(args.out, target), "w") as fh:
122+
fh.write(front_matter(sort, SECTION_TITLE if target == "README.md" else title))
123+
fh.write(body)
124+
if target == "README.md":
125+
fh.write(PROVENANCE)
126+
written.append(target)
127+
128+
images_in = os.path.join(docs, "images")
129+
if os.path.isdir(images_in):
130+
images_out = os.path.join(args.out, "images")
131+
os.makedirs(images_out, exist_ok=True)
132+
for f in sorted(os.listdir(images_in)):
133+
shutil.copy2(os.path.join(images_in, f), os.path.join(images_out, f))
134+
written.append(os.path.join("images", f))
135+
136+
print(f"wrote {len(written)} file(s) to {args.out}")
137+
return 0
138+
139+
140+
if __name__ == "__main__":
141+
sys.exit(main())
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
# Regenerate docs/cadtutorial from the tutorial sources in AliceO2.
3+
#
4+
# The tutorial is written and reviewed next to the code it documents, in
5+
# AliceO2 under Detectors/CADSupport/doc/tutorial. This job converts those
6+
# pages into the form this site expects, so the section here never drifts
7+
# from the converter it describes.
8+
9+
name: CAD tutorial
10+
11+
on:
12+
schedule:
13+
- cron: "17 4 * * 1"
14+
workflow_dispatch:
15+
16+
permissions:
17+
contents: write
18+
19+
jobs:
20+
sync:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Fetch the tutorial sources
26+
working-directory: ${{ runner.temp }}
27+
run: |
28+
git clone --depth 1 --filter=blob:none --sparse \
29+
https://github.com/AliceO2Group/AliceO2.git aliceo2
30+
cd aliceo2
31+
git sparse-checkout set Detectors/CADSupport/doc/tutorial
32+
33+
- name: Regenerate the section
34+
run: |
35+
python3 .github/scripts/sync_cad_tutorial.py \
36+
--source "${{ runner.temp }}/aliceo2"
37+
38+
- name: Commit if anything changed
39+
run: |
40+
if git diff --quiet -- docs/cadtutorial; then
41+
echo "already up to date"
42+
exit 0
43+
fi
44+
git config user.name "github-actions[bot]"
45+
git config user.email \
46+
"41898282+github-actions[bot]@users.noreply.github.com"
47+
git add docs/cadtutorial
48+
git commit -m "Regenerate the CAD simulation tutorial from AliceO2"
49+
git push

‎docs/cadtutorial/README.md‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
sort: 9
3+
title: CAD simulation tutorial
4+
---
5+
6+
# Simulating ALICE geometries that come from CAD
7+
8+
Detectors are designed in CAD, but Geant transports particles through ROOT's TGeo geometry. This
9+
guide is about crossing that gap automatically — taking an engineering model as it comes out of the
10+
design office and turning it into something particles can be simulated through, all the way to hits
11+
you can plot.
12+
13+
The usual way of crossing that gap is to read the drawings and write the geometry again by hand, in
14+
C++, volume by volume. That works, and most of ALICE was built this way, but it is slow, it is easy
15+
to get subtly wrong, and every time the engineers move a bracket the translation has to be redone.
16+
For a detector that is still being designed — which is exactly the situation during an upgrade study
17+
— the hand-written geometry is out of date almost as soon as it is written.
18+
19+
So instead we convert the CAD file directly. You export the assembly as STEP, run one converter over
20+
it, and you get a ROOT macro that builds the geometry. From there a small JSON file tells `o2-sim` to
21+
load that macro and place it in the ALICE world. Nothing is recompiled at any point, so the loop from
22+
a new CAD revision to a new simulation takes minutes rather than weeks.
23+
24+
Getting the geometry in is only half of it, though. A shape that particles fly through is a passive
25+
obstacle; to do physics you want it to *record* something. The second half of this guide is therefore
26+
about the external-detector mechanism, which lets you declare parts of your imported geometry
27+
sensitive and have them write hits — again with no detector class and no rebuild. That is usually
28+
enough to answer the first questions an upgrade study asks: does this thing get hit, how often, and
29+
where.
30+
31+
## What you will be able to do by the end
32+
33+
- Install the converter and check that it works.
34+
- Convert a STEP assembly and look at the result.
35+
- Understand and control how faithfully each part is represented.
36+
- Attach materials, and know what the magnetic field and physics cuts will and will not do.
37+
- Place the geometry inside ALICE as passive material.
38+
- Make parts of it sensitive, run a simulation, and count hits.
39+
- Take an existing ALICE detector out to CAD and back, and simulate the result.
40+
- Know where the system's limits are, so you do not discover them in your results.
41+
42+
We assume you can run `o2-sim`, and nothing more. No CAD experience is needed, and no knowledge of
43+
OpenCascade, which does the heavy lifting underneath but never has to be addressed directly.
44+
45+
## Where the code lives
46+
47+
Everything in this guide is in `Detectors/CADSupport` in [AliceO2](https://github.com/AliceO2Group/AliceO2).
48+
`README.md` there is the complete option reference, and `doc/reference/` documents the solids, their
49+
file formats and the recognition pipeline.
50+
51+
## Contents
52+
53+
**Start** — [Install the software](install.md) · [Convert your first model](first-conversion.md)
54+
55+
**Converting** — [How a part is represented](representation.md) ·
56+
[Convert only part of a model](partial.md) · [Give it materials](materials.md) ·
57+
[Field and cuts](field-and-cuts.md) · [The geom.C file](geom-c.md)
58+
59+
**Simulating** — [Add passive geometry](passive.md) · [Make it produce hits](hits.md) ·
60+
[Grow it into a real detector](real-detector.md)
61+
62+
**Worked example** — [The ITS, out and back again](its-round-trip.md)
63+
64+
**Reference** — [Check your geometry](checks.md) · [Limits and pain points](limits.md)
65+
66+
67+
---
68+
69+
*These pages are generated from the tutorial sources in AliceO2,
70+
[Detectors/CADSupport/doc/tutorial](https://github.com/AliceO2Group/AliceO2/tree/dev/Detectors/CADSupport/doc/tutorial), which is where corrections belong.*

‎docs/cadtutorial/checks.md‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
sort: 12
3+
title: Check your geometry
4+
---
5+
6+
# Check your geometry
7+
8+
Before trusting any physics that came out of a conversion, it is worth spending a few minutes on four
9+
checks. They are ordered cheapest first, and in practice the first two catch most problems.
10+
11+
## 1 · Read the cascade table
12+
13+
The converter already told you what it decided for every part, and wrote the same information to
14+
`csg_report.json`. A part that declined CSG says which test it failed and by how much, which is often
15+
enough to see that a model is nearly-but-not-quite a primitive. A large tessellated count on a model
16+
you expected to be analytic is the signal to look at `--recognize-surfaces` and the surface report
17+
below.
18+
19+
## 2 · Look for overlaps
20+
21+
Run `build_and_export("geom.root", true, true)` to get `CheckOverlaps`; zero illegal overlaps is what
22+
you want to see. A non-zero count is worth taking seriously, but do not assume it is the conversion's
23+
fault: engineering assemblies are drawn for manufacture, not for particle transport, and slightly
24+
interpenetrating parts are common in perfectly good CAD models.
25+
26+
## 3 · Confirm the exact solids really load
27+
28+
Successfully extracting a solid's surfaces does not guarantee the result is a usable, watertight body.
29+
This macro loads every `surfaces_*.bin` in a directory the same way the transport does, and reports
30+
closure, orientation consistency and enclosed volume:
31+
32+
```bash
33+
# $O2_SRC is your AliceO2 source directory
34+
root -l -b -q "$O2_SRC/Detectors/CADSupport/test/checkSurfaceSidecars.macro(\"cad_out/excavator\")"
35+
```
36+
37+
```text
38+
OK surfaces_Bucket_0_1_1_6.bin surfaces= 97 closed=1 orient=1 capacity=58.3121
39+
OK surfaces_Base_0_1_1_3.bin surfaces= 44 closed=1 orient=1 capacity=241.281
40+
...
41+
SUMMARY cad_out/excavator
42+
sidecars found : 13
43+
loaded : 13
44+
rejected by the reader : 0
45+
loaded but not IsClosed() : 0
46+
orientation inconsistent : 0
47+
```
48+
49+
`closed=1` means the solid is a watertight manifold, which is precisely what navigation requires. Any
50+
non-zero number on the last three summary lines identifies a part that will not transport correctly.
51+
52+
## 4 · Find out what the geometry really is
53+
54+
A subtlety worth knowing: the surface type stored in a STEP file describes the *exporter*, not the
55+
geometry. CAD kernels routinely write an exact cylinder as a rational B-spline, which is an exact
56+
representation rather than an approximation — but dispatching on the stored type would throw that
57+
exactness away. The converter therefore classifies faces by their actual shape, and its surface report
58+
shows the effect:
59+
60+
```bash
61+
# a per-face classification, written alongside a normal conversion
62+
--surface-report cad_out/mydet/surface_report.json
63+
```
64+
65+
## Going further
66+
67+
`Detectors/CADSupport/validation/` holds the tools the development of this system is validated with:
68+
an acceptance gate that scores converted parts against the OpenCascade oracle, an overlap census, a
69+
round-trip report, and the closure test that the [ITS example](its-round-trip.md) follows. They are
70+
not installed — run them from the source tree. `README.md` lists them all.

‎docs/cadtutorial/field-and-cuts.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
sort: 6
3+
title: Field and cuts
4+
---
5+
6+
# Field and cuts
7+
8+
There is one place where the converter cannot give you everything, and it is worth being explicit
9+
about rather than discovering later. A CAD file describes a *part*. It cannot describe how you want
10+
that part simulated — how the magnetic field should be integrated through it, how long a step may be,
11+
which secondaries are worth producing. Those are simulation choices, and no CAD format has anywhere
12+
to record them.
13+
14+
## Magnetic field
15+
16+
For the field there is a clean answer. Pass `--in-field` when the module sits inside the magnet, and
17+
the emitted macro will ask the **live** field for its integration method and maximum field strength
18+
at the moment the geometry is built — which is exactly what a hand-written O2 detector does from its
19+
own `createMaterials()`. Nothing is baked into the file:
20+
21+
`geom.C · emitted`
22+
23+
```cpp
24+
int cad_ifield = 2;
25+
float cad_fieldm = 10;
26+
cadFieldTrackingParams(cad_ifield, cad_fieldm); // queries the loaded field
27+
med_Stainless_Steel->SetParam(1, cad_ifield); // ifield, from the live field
28+
med_Stainless_Steel->SetParam(2, cad_fieldm); // fieldm, from the live field
29+
```
30+
31+
The `2,10` you see there is only a seed, used if no field happens to be loaded, and `--in-field 1,5.5`
32+
overrides it. To confirm that the query really happened, check `fieldm` rather than `ifield`:
33+
`ifield = 2` is also the seed value and therefore proves nothing, whereas a `fieldm` the seed could
34+
not have produced — ALICE reports 15 — proves the live field answered.
35+
36+
## Step control and physics cuts
37+
38+
```warning
39+
**These silently default to nothing**
40+
41+
Without `--in-field`, a CAD-authored medium is built through ROOT's three-argument `TGeoMedium`
42+
constructor, which **zeroes every parameter** — including `ifield`, meaning no field tracking at
43+
all. Step control (`tmaxfd stemax deemax epsil stmin`) stays at the transport default in every
44+
case, and special physics cuts are never applied, because there is no `simcuts.dat` for a module
45+
with no detector directory to hold one. None of this is loud: the simulation runs and the numbers
46+
look plausible. So set `--in-field` deliberately, and treat cuts as a known open item until your
47+
study grows into a [real detector](real-detector.md), which is where they come back.
48+
```

0 commit comments

Comments
 (0)