Skip to content

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:

  1. Parsing raw display source with parse_display_source()
  2. Evaluating symbolic Primitive objects
  3. ComponentDisplay — single-component geometry
  4. InstrumentDisplay — full instrument in the global frame
  5. Rendering with matplotlib
  6. New primitives (CircleNormal, Disc, Annulus, Polygon, Polyhedron)
  7. Rendering with Plotly (solid surfaces)

1. Imports

# Enable interactive matplotlib backend in Jupyter
%matplotlib widget
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
fig, ax = plot_geometry({'circle r=0.1': polylines})
ax.set_title('Single circle, xy plane, r=0.1')
Text(0.5, 0.92, 'Single circle, xy plane, r=0.1')
Figure

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']
Figure

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)')
Figure

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')
Figure

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])
2026-03-12 22:21:06.674 | DEBUG    | mccode_antlr.reader.reader:add_component:243 - Component cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Arm.comp
2026-03-12 22:21:06.678 | DEBUG    | mccode_antlr.reader.reader:add_component:243 - Component cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Slit.comp


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)')
Figure

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()))
2026-03-12 22:21:06.865 | DEBUG    | mccode_antlr.reader.reader:add_component:243 - Component cache hit: /home/g/.cache/mccodeantlr/mcstas/v3.5.31/mcstas-comps/optics/Arm.comp


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)')
Figure

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)'>
Figure
# --- 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.