McCode Display Geometry — Interactive Notebook¶
This notebook demonstrates mccode_antlr.display, which parses a
component's MCDISPLAY section into symbolic Python geometry backed by
Expr, evaluates it with concrete parameter values, and renders it in 3-D —
without compiling the instrument.
Topics:
- Parsing raw display source with
parse_display_source() - Evaluating symbolic
Primitiveobjects ComponentDisplay— single-component geometryInstrumentDisplay— full instrument in the global frame- Rendering with matplotlib
- New primitives (CircleNormal, Disc, Annulus, Polygon, Polyhedron)
- Rendering with Plotly (solid surfaces)
1. Imports¶
import numpy as np
import matplotlib
# matplotlib.use('Agg') # change to 'TkAgg' or 'Qt5Agg' for interactive windows
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # registers the '3d' projection
from mccode_antlr.display import (
parse_display_source,
ComponentDisplay,
InstrumentDisplay,
Circle, CircleNormal, Line, DashedLine, Multiline, Box, Cone, Sphere,
Cylinder, Disc, Annulus, Polygon, Polyhedron, ConditionalBlock, LoopBlock,
)
from mccode_antlr.display.render.matplotlib import plot_geometry
try:
from mccode_antlr.display.render.plotly import show_geometry as plotly_show
_has_plotly = True
except ImportError:
_has_plotly = False
from mccode_antlr.common.expression import Expr
print("All imports OK")
All imports OK
2. parse_display_source — parsing raw MCDISPLAY C text¶
Every numeric argument is stored as an Expr and stays symbolic until
.to_polylines(params) is called.
# Circle with a symbolic radius
prims = parse_display_source('circle("xy", 0, 0, 0, r);')
c = prims[0]
print("Type :", type(c).__name__)
print("Plane :", c.plane)
print("Radius :", c.r, " (symbolic —", c.r.is_id, ")")
Type : Circle
Plane : xy
Radius : r (symbolic — True )
# Evaluate at r = 0.1
polylines = c.to_polylines({'r': 0.1})
pts = polylines[0]
print("Shape :", pts.shape) # (25, 3) — 24 segments, closed
print("All z = 0? :", np.allclose(pts[:, 2], 0))
print("All |xy| = 0.1?:", np.allclose(np.hypot(pts[:, 0], pts[:, 1]), 0.1))
Shape : (25, 3)
All z = 0? : True
All |xy| = 0.1?: True
Text(0.5, 0.92, 'Single circle, xy plane, r=0.1')
3. Local variables and C-style casts¶
Real display sections often declare temporary variables and use C casts like
(double)ymax. The visitor resolves both automatically.
src = '''
double h2 = height / 2.0;
line(0, -(double)h2, 0, 0, (double)h2, 0);
circle("xz", 0, 0, 0, radius);
'''
prims = parse_display_source(src)
print("Parsed:", [type(p).__name__ for p in prims])
params = {'height': 0.4, 'radius': 0.15}
all_pls = [pl for p in prims for pl in p.to_polylines(params)]
fig, ax = plot_geometry({'component': all_pls})
ax.set_title('Vertical bar + horizontal ring')
plt.tight_layout(); plt.savefig('/tmp/bar_ring.png', dpi=100); plt.show()
Parsed: ['Line', 'Circle']
4. Conditional blocks — if (cond) { ... }¶
if-guarded geometry becomes a ConditionalBlock. You decide at render
time which branch to show.
src = '''
if (use_circle) {
circle("xy", 0, 0, 0, xw/2);
}
line(-xw/2, 0, 0, xw/2, 0, 0); /* always drawn */
'''
prims = parse_display_source(src)
print("Types:", [(type(p).__name__, getattr(p, 'condition', '—')) for p in prims])
params = {'xw': 0.10, 'use_circle': 1} # enable the circle
all_pls = []
for p in prims:
if isinstance(p, ConditionalBlock):
val = p.condition.evaluate(params).value
if val:
all_pls += [pl for sub in p.body for pl in sub.to_polylines(params)]
else:
all_pls += p.to_polylines(params)
fig, ax = plot_geometry({'slit': all_pls})
ax.set_title('Line + conditional circle (use_circle=1)')
Types: [('ConditionalBlock', scalar 0 Symbol('use_circle')), ('Line', None)]
Text(0.5, 0.92, 'Line + conditional circle (use_circle=1)')
5. ComponentDisplay — geometry for a single component¶
ComponentDisplay wraps any Comp object and lazily parses its MCDISPLAY
section.
from mccode_antlr.comp import Comp
from mccode_antlr.common import RawC
display_src = '''
/* Arm: 3 axis lines + 3 cones */
line(0,0,0, 0.2,0,0);
line(0,0,0, 0,0.2,0);
line(0,0,0, 0,0,0.2);
cone(0.2,0,0, 0.01,0.02, 1,0,0);
cone(0,0.2,0, 0.01,0.02, 0,1,0);
cone(0,0,0.2, 0.01,0.02, 0,0,1);
'''
comp = Comp(name='Arm', display=(RawC(filename=None, line=0, source=display_src),))
cd = ComponentDisplay(comp)
print("Primitives:", [type(p).__name__ for p in cd.primitives])
polylines = cd.to_polylines()
fig, ax = plot_geometry({'Arm': polylines})
ax.set_title('Arm component — axis triad')
Primitives: ['Line', 'Line', 'Line', 'Cone', 'Cone', 'Cone']
Text(0.5, 0.92, 'Arm component — axis triad')
6. Loading a real McStas component¶
We build a small instrument file in memory and load the Slit component's
display geometry.
from mccode_antlr.loader import load_mcstas_instr
import tempfile, os
instr_src = '''
DEFINE INSTRUMENT slit_demo(xw=0.05, yh=0.1)
TRACE
COMPONENT origin = Arm()
AT (0,0,0) ABSOLUTE
COMPONENT slit = Slit(xmin=-xw/2, xmax=xw/2, ymin=-yh/2, ymax=yh/2)
AT (0,0,5) RELATIVE origin
END
'''
with tempfile.NamedTemporaryFile(mode='w', suffix='.instr', delete=False) as f:
f.write(instr_src); fname = f.name
instr = load_mcstas_instr(fname); os.unlink(fname)
slit_type = next(c for c in instr.components if c.name == 'slit').type
cd = ComponentDisplay(slit_type)
print("Slit primitives:", [type(p).__name__ for p in cd.primitives])
[32m2026-03-12 22:21:06.674[0m | [34m[1mDEBUG [0m | [36mmccode_antlr.reader.reader[0m:[36madd_component[0m:[36m243[0m - [34m[1mComponent cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Arm.comp[0m
[32m2026-03-12 22:21:06.678[0m | [34m[1mDEBUG [0m | [36mmccode_antlr.reader.reader[0m:[36madd_component[0m:[36m243[0m - [34m[1mComponent cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Slit.comp[0m
Slit primitives: ['ConditionalBlock']
# The Slit display is wrapped in if (is_unset(radius)) { ... } else { circle }
# We render the true branch (rectangular aperture) with concrete parameters.
params = {'xmin': -0.025, 'xmax': 0.025, 'ymin': -0.05, 'ymax': 0.05}
all_pls = []
for p in cd.primitives:
if isinstance(p, ConditionalBlock):
for sub in p.body:
try:
all_pls += sub.to_polylines(params)
except Exception as e:
print("skip:", e)
else:
try:
all_pls += p.to_polylines(params)
except Exception as e:
print("skip:", e)
print(f"Polylines rendered: {len(all_pls)}")
fig, ax = plot_geometry({'Slit': all_pls})
ax.set_title('Slit component (xw=5 cm, yh=10 cm)')
Polylines rendered: 4
Text(0.5, 0.92, 'Slit component (xw=5 cm, yh=10 cm)')
7. InstrumentDisplay — full instrument in the global frame¶
InstrumentDisplay.to_polylines() applies each component's AT/ROTATED
transform so everything ends up in the same coordinate system.
instr_src = '''
DEFINE INSTRUMENT demo(xw=0.05, yh=0.1)
TRACE
COMPONENT origin = Arm()
AT (0,0,0) ABSOLUTE
COMPONENT guide_in = Arm()
AT (0,0,2) RELATIVE origin
COMPONENT sample = Arm()
AT (0,0,5) RELATIVE origin
ROTATED (0, 45, 0) RELATIVE origin
COMPONENT detector = Arm()
AT (0,0,8) RELATIVE origin
END
'''
with tempfile.NamedTemporaryFile(mode='w', suffix='.instr', delete=False) as f:
f.write(instr_src); fname = f.name
instr = load_mcstas_instr(fname); os.unlink(fname)
disp = InstrumentDisplay(instr)
polylines = disp.to_polylines({}) # dict[name -> list[ndarray]]
print("Components with geometry:", list(polylines.keys()))
[32m2026-03-12 22:21:06.865[0m | [34m[1mDEBUG [0m | [36mmccode_antlr.reader.reader[0m:[36madd_component[0m:[36m243[0m - [34m[1mComponent cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Arm.comp[0m
Components with geometry: ['origin', 'guide_in', 'sample', 'detector']
fig, ax = plot_geometry(polylines)
ax.set_title('4-component instrument in the global frame\n(sample rotated 45° around y)')
Text(0.5, 0.92, '4-component instrument in the global frame\n(sample rotated 45° around y)')
8. Manual geometry construction¶
You can also build Primitive objects directly — useful for testing or
for quickly sketching geometry in a notebook.
box = Box(Expr.integer(0), Expr.integer(0), Expr.integer(0),
Expr.float(0.1), Expr.float(0.1), Expr.float(0.05))
circ = Circle('xy', Expr.integer(0), Expr.integer(0), Expr.float(0.025),
Expr.float(0.04))
sph = Sphere(Expr.integer(0), Expr.integer(0), Expr.float(0.025), Expr.float(0.04))
cyl = Cylinder(Expr.integer(0), Expr.integer(0), Expr.float(-0.025),
Expr.float(0.04), Expr.float(0.05),
Expr.integer(0), Expr.integer(1), Expr.integer(0))
all_pls = box.to_polylines() + circ.to_polylines() + sph.to_polylines() + cyl.to_polylines()
fig, ax = plot_geometry({'detector': all_pls})
ax.set_title('Manual geometry: box + circle + sphere + cylinder');
ax
<Axes3D: title={'center': 'Manual geometry: box + circle + sphere + cylinder'}, xlabel='x (m)', ylabel='z (m)', zlabel='y (m)'>
# --- Plotly renderer: interactive 3-D with optional solid surfaces ---
# Requires: pip install plotly
try:
from IPython.display import HTML, display as ipy_display
from mccode_antlr.display.render.plotly import show_geometry as plotly_show
_fig = plotly_show(polylines, disp, use_mesh=True)
print(f'{len(_fig.data)} traces')
ipy_display(HTML(_fig.to_html(include_plotlyjs=True, full_html=False)))
except ImportError:
print('Install plotly: pip install plotly')
Summary¶
| Task | API |
|---|---|
| Parse MCDISPLAY source | parse_display_source(raw_c, local_vars={}) |
| Single-component geometry | ComponentDisplay(comp).to_polylines(params) |
| Full instrument geometry | InstrumentDisplay(instr).to_polylines(params) |
| Matplotlib render | plot_geometry(dict[name → list[ndarray]]) |
| WebGL render (optional) | from mccode_antlr.display.render.threejs import show_geometry |
All numeric arguments are Expr objects — symbolic until evaluated — so
you can call .to_polylines() many times with different parameter sets
without re-parsing.