Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions .github/scripts/sync_cad_tutorial.py
Original file line number Diff line number Diff line change
@@ -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 <AliceO2 checkout> [--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())
49 changes: 49 additions & 0 deletions .github/workflows/cad-tutorial-sync.yml
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions docs/cadtutorial/README.md
Original file line number Diff line number Diff line change
@@ -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.*
70 changes: 70 additions & 0 deletions docs/cadtutorial/checks.md
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 48 additions & 0 deletions docs/cadtutorial/field-and-cuts.md
Original file line number Diff line number Diff line change
@@ -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.
```
Loading
Loading