Skip to content

mctest: %Scan: prototype - #2772

Merged
willend merged 10 commits into
mccode-dev:mainfrom
willend:mctest-scan-prototype
Oct 9, 2026
Merged

willend merged 10 commits into
mccode-dev:mainfrom
willend:mctest-scan-prototype

Conversation

@willend

@willend willend commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Free-form text area

Please describe what your PR is adding in terms of features or bugfixes:

mctest: %Scan: tests, per-point scan seeds in mcrun, and HTML plotting of scan directories

Summary

Adds a scan test mode to mctest. An instrument header can now hold a %Scan: line, which gives an mcrun parameter scan and one expected detector value per scan point:

* %Scan: mcrun SE_example.instr dBz=-0.0001,0.0001 -N41 -n1e5 Detector: detector_I={
*   86.04,101.892,108.731,...
*   85.406 }

The scan arguments go to mcrun unchanged, so every mcrun scan syntax works (a,b -N, a:delta:b, -L, -M, -N=a,b,c, …) and mctest has no scan parsing of its own.

The HTML plotting frontends could not plot mcrun scan directories, which mctest scan results need. This PR fixes that too.

Changes

mctest

  • A %Scan: line becomes a test like %Example:: its own test number, output directory and JSON entry ("scan": true, with list-valued targetval/testval).
  • The target values are a {}-enclosed, comma-separated list and may span several header lines.
  • mcrun and *.instr tokens on %Example:/%Scan: lines are ignored.
  • A scan uses its own -n if given, otherwise at most 1e5 per point.
  • Scans run with --scan_split=auto unless MPI is used. The timeout scales with the number of points. mcdisplay runs at the first scan point.
  • Values are read from the scan's mccode.dat, which works for both the McCode and NeXus formats. A scan passes when every point is within 20 % of its target.
  • New --noscans option.

mcviewtest: one cell per scan (e.g. scan, 41/41 pts (41/41 pts OK)). Hovering shows each point's test value, target and percentage. The diff/coplot links now work for scan rows (see plotting below).

mcdoc: %Scan: lines appear like %Example: lines, labelled Scan: …, with the value list shortened to {N values}.

mcrun

  • --scan_split=auto is an alias for the existing (undocumented) 0: the number of cores minus one.
  • --scan_split is now overridden only by an explicit --mpi greater than 1, with a warning. Before, the default --mpi=1 silently disabled it.
  • Per-point scan seeds: point i uses base + i*1024 in both the serial and the --scan_split scanner. Before, only the split scanner did this.
    • The base is --seed if given, otherwise the current time, which is logged so the run can be reproduced.
    • --seeds scans use the scanned seeds unchanged.

HTML plotting of mcrun scan directories (mcplot-html, mccoplot-html, mcplotdiff-html)

Scan curves loaded from mccode.dat had an empty filepath, and all of them had the file name mccode.dat. The HTML frontends build output file names from those two fields, so:

  • mcplot-html wrote every curve to .html/_log.html in the current directory, and its parallel jobs raced on deleting that shared file (FileNotFoundError).
  • mccoplot-html wrote every pane to coplot_mccode.html, so all panes showed the last detector.
  • mcplotdiff-html crashed: scan curves have no values triplet, no (I, ERR) pair and no N column.

Fixes:

  • mcplotloader: each scan curve gets its own filepath, <scandir>/<yvar> (e.g. scan/detector_I), so mcplot-html writes scan/detector_I.html.
  • mcplot-html: the racy "delete if it exists" step before writing is gone. Opening the file for writing already truncates it.
  • mcplotdiffloader:
    • New monitor_basename(): the base name of filepath, falling back to filename. It is used to key and match monitors and to name the diff .dat files and their mccode.sim entries. Ordinary monitors keep their old names (e.g. PSD.dat).
    • The diff and the .dat writer now handle scan curves: the % difference comes from the summed curves when there is no values triplet, an <name>_ERR column is derived, and N is written as 0 when there is no N column.
  • mccoplot-html / mcplotdiff-html: page names, and the lookup of existing mcplot-html pages to link to, now use monitor_basename().

Instruments: SE_example gets a 41-point %Scan: test, with targets generated using -s 1000.

⚠️ Numerical behaviour change

Serial mcrun scans run with --seed now give different numbers than before. Each point gets its own seed instead of every point reusing the same one, which also removes the point-to-point correlation in the noise. Split scans are unchanged, and serial and split scans now give identical results for a given seed. Without --seed, scans were already non-reproducible, so nothing is lost there.

Testing (macOS arm64, clang, mccode-dev conda env, McStas only)

  • mctest --instr SE_example: 41/41 points pass (about 4 s with --scan_split). Also passes with --nexus.
  • With two target values deliberately corrupted, the test fails (39/41 points OK).
  • --noscans skips the scan.
  • mcviewtest renders one scan row with pass/fail colouring and per-point tooltips.
  • Serial and split mcrun scans with -s 1000 give identical results. A --seeds=7,9 -N3 scan matches single runs at those seeds.
  • The mcdoc header parser shows Scan: … {41 values}.
  • Plotting, on an SE_example scan (-N41):
    • mcplot-html, mccoplot-html and mcplotdiff-html each produce one page per scan curve.
    • The diff output directory reopens in mcplot-html.
    • Diff/coplot output names for a single step directory are unchanged.
    • The matplotlib mcplot and mccoplot still work.

Not tested / known gaps

  • Linux, Windows and macOS x86_64.
  • McXtrace. The Python tools are shared, but no McXtrace instrument has a %Scan: line yet and mxtest/mxrun/mxplot weren't run.
  • mctest with --mpi (scans then run serially through MPI).
  • The pyqtgraph frontend on scan directories. Nothing in it was changed.
  • The HTML pages in other browsers and at narrow widths.

Declaration of use of AI-tools

  • Please add a checkmark here if you used AI-tools during the work for this contribution
  • Furter, please describe how / where and for what the tools were used:

^ 🤖 Generated with Claude Code


Development OS / boundary conditions

Please describe what OS you developed and tested your additions on, and if any special dependencies are required:


PR Checklist for contributing to McStas/McXtrace

For a coherent and useful contribution to McStas/McXtrace, please fill in relevant parts of the checklist:

  • My contribution contains something else

    • Explanation is added in free form text above or below the checklist

willend and others added 2 commits October 7, 2026 20:37
…mcrun

%Scan: header lines describe a parameter scan test, using the mcrun scan
syntax unchanged (so every mcrun scan form works), followed by one target
detector value per scan point in a {}-enclosed list that may span lines:

  * %Scan: mcrun SE_example.instr dBz=-0.0001,0.0001 -N41 -n1e5 Detector: detector_I={
  *   86.04,101.892,...
  *   85.406 }

mctest:
- %Scan: lines become tests like %Example: (own testnb, dir, JSON with
  "scan": true and list-valued targetval/testval)
- "mcrun" and "*.instr" tokens on %Example:/%Scan: lines are ignored; a
  scan's own -n is used, else at most 1e5 per point
- scans run with --scan_split=auto unless MPI is used, timeout scales with
  the number of points, mcdisplay runs at the first scan point
- values are read from the scan's mccode.dat (McCode and NeXus formats);
  a scan passes when all points are within 20% of target
- new --noscans option to skip scans

mcviewtest: one cell per scan ("scan, 41/41 pts (41/41 pts OK)"), with a
per-point test/target/percent hover tooltip.

mcdoc: %Scan: lines are shown like %Example: lines, as "Scan: ...", with
the target value list shortened to "{N values}".

mcrun:
- --scan_split=auto (alias for the existing 0): number of cores minus one
- --scan_split is only overridden by an explicit multi-process --mpi
  (--mpi defaults to 1), with a warning when that happens
- each scan point i now uses seed base+i*1024 in both the serial and the
  --scan_split scanner (previously only the latter), so serial and split
  scans give identical, reproducible results for a given --seed; without
  --seed the base is taken from the current time and logged; --seeds
  scans use the scanned seeds unchanged

SE_example gets a 41-point %Scan: test (targets from -s 1000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dirs

Scan curves loaded from mccode.dat had an empty filepath and all shared
filename 'mccode.dat', while the html frontends derive output file names
from those fields:

- mcplot-html wrote every curve to '.html'/'_log.html' in the cwd, and
  its parallel jobs raced on exists()/remove() of that shared file
  (FileNotFoundError).
- mccoplot-html wrote every pane to 'coplot_mccode.html', so all panes
  showed the last detector.
- mcplotdiff-html crashed: scan curves have no 'values' triplet, no
  (I, ERR) yvar pair and no N column.

Fixes:
- mcplotloader: give each scan curve a unique filepath <scandir>/<yvar>
  (e.g. scan/detector_I), so mcplot-html writes scan/detector_I.html.
- mcplot-html: drop the racy exists()/remove() before writing; open('w')
  already truncates.
- mcplotdiffloader: add monitor_basename() (basename of filepath, else
  filename) and use it to key and match monitors and to name the diff
  .dat files and the mccode.sim entries. Ordinary monitors keep their old
  names (e.g. PSD.dat). Make the diff and the .dat writer handle scan
  curves: % diff from summed curves when 'values' is empty, a derived
  <name>_ERR yvar, and N=0 rows when there is no N column.
- mccoplot-html/mcplotdiff-html: use monitor_basename() for page names
  and for locating existing mcplot-html pages to link to.

Verified with an SE_example scan (-N41): mcplot-html, mccoplot-html and
mcplotdiff-html produce one page per scan curve; the diff output dir
reopens with mcplot-html; single step-dir diff/coplot output names are
unchanged; matplotlib mcplot/mccoplot still work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@willend

willend commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Now adding %Scan: lines to instruments with non-executed Example: (without %) scans lines.

willend and others added 8 commits October 8, 2026 15:01
Scan examples that were written as plain "Example:"/"mcrun ..."/"mxrun ..."
header lines are turned into %Scan: tests, with reference values generated
the way mctest runs them (--no-mpi -s 1000 --scan_split=auto). Ranges,
point counts and ncount are adjusted so every point has at most ~8%
statistical error, keeping the 20% per-point tolerance meaningful:

McStas:
- SE_example2: dBz scan, 41 pts (as SE_example)
- Tomography: omega=0,340 -N18; drops the stale offfile=bunny.off
  (the instrument has no such parameter, so every point failed)
- SESANS_Delft: By=0,0.0468 -N31 -n1e6
- Test_PowderN_Res: radius=0.001,0.021 -N21
- Test_Magnon_bcc_TAS: hw=0,1 -N21 -n1e6 on e_monitor (e_monitor1 was
  up to 100% noisy and zero for most hw)
- Test_Dispersion_relation_TAS: -M -N 5,5 (was 21,21 = 441 pts)
- Samples_Incoherent: SAMPLE=1,5 -N5 STOP=1 (prose examples kept)
- LLB_6T2: C60 200 rocking curve over the peak, phi=9.27,10.37 -N12
  -n1e7 (was -N31 -n1e8 with --mpi); moved out of %Parameters

McXtrace:
- SSRL_bl_11_2_white_src / _not_white_src: Etohit=6900,7500 -N31 -n1e6
  (was -N601)
- SOLEIL_DISCO: x_screw=20,103 -N21 scan=1 on the Wavelength monitor
- SOLEIL_ROCK: Cu edge E0=8.5,9.5 -N21 -n1e6 on fluo_monitor
- Czerny_Turner: x_screw=20,103 -N21 -n1e6 scan=1 on Wavelength

%Scan: lines are placed before any following descriptive text, so mcdoc
keeps that text in the description.

mctest: also ignore an "mxrun" token on %Example:/%Scan: lines.

Verified: mctest and mxtest --local on all 13 pass with every point at
100% (macOS arm64); together they add ~2.5 min on 12 cores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s_Incoherent

Reference values generated as mctest runs them (--no-mpi -s 1000
--scan_split=auto), ranges trimmed to the curve after trial scans so
every point has at most ~6% statistical error:

- TAS1_C1: monochromator rocking curve PHM=-38.077,-36.077 -N11
- TAS1_C1_Tilt: collimator tilt OMC1=-44.5,55.5 -N21
- TAS1_Diff_Slit: direct beam C1=30 TT=-0.6,0.6 -N13
- TAS1_Diff_Powder: powder line TT=32.92,34.32 -N15
- TAS1_Diff_Vana: flat incoherent background TT=31.52,35.52 -N21
- TAS1_Powder, TAS1_Vana: analyzer scan OMA=-19.45,-15.45 -N21; a note
  explains the transmission dip (with TTA=0 the detector looks straight
  through the analyzer)
  (all -n1e6)
- SOLEIL_ROCK: Mn/Cr edge scan E0=5.7,6.8 sample_file=MnCr -N21 -n1e6
  replaces its prose example line
- Samples_Incoherent: the two beamstop-removed scans SAMPLE=2,5 -N4
  STOP=0 (PSD_Sphere_4pi) and with DB=1 (Dirbeam)

Doc fix: OMC1 is in arcmin (the code divides it by 60), not deg, in the
six TAS1 instruments that have it.

Verified: mctest/mxtest --local pass with every point at 100% (macOS
arm64); together ~3.5 min, of which 74 s is the SOLEIL_ROCK Mn/Cr scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
%Scan: header lines are mctest test definitions like %Example: lines, so
edits to them must still list the .comp/.instr file. A %Scan: line runs
on to the } closing its target values, which may span several header
lines; the header's * prefixes inside the {} are ignored, so re-wrapping
the values is not a change. The stderr reason is now %Example/%Scan.

Checked on a0f97e1 and aa47fec: all 13 resp. 9 instruments with new
%Scan: lines are listed (previously skipped as header-only edits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both scans failed outside the reference setup (other seeds / MPI rank
layouts): the true seed-to-seed scatter is far larger than the reported
error bars, because few independent histories get past the monochromator
and the downstream SPLIT (sample / fluorescence) reuses them, so the
errors are underestimated. E.g. LLB_6T2 at -n1e7: points moved up to 50%
with +/-3% error bars; SOLEIL_ROCK Mn/Cr at -n1e6: 3 of 21 points >20%
off, worst 50%, with +/-4% error bars.

The header example lines are restored to their pre-%Scan text. LLB_6T2
keeps SPLIT 10 on Monoc, which halves the real scatter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

- tomo_recon.py: Python port (numpy + matplotlib) of the old Matlab
  tomo_recon.m. Filtered back-projection (ramp*Hann filter, as Matlab
  iradon's 'hann') of an mcrun omega scan, one sinogram per detector row
  from the I block of each step's x_y monitor file, using -log(I/I_max)
  as projection. Shows the 3 central slices; --save writes vol.npy.
  Usage: python tomo_recon.py <scan dir> [--save]
- Tomography.instr / README.md: point to tomo_recon.py instead of the
  Matlab function (which needed the imaging toolbox, PGPLOT output and
  is no longer in tools/matlab).
- %Scan: now a full rotation omega=0,350 -N36 -n1e6, with targets from
  that scan (error bars ~0.1%, no SPLIT in the instrument).

Verified: mctest passes the scan (36/36 points at 100%, ~48 s);
tomo_recon.py reconstructs an 80x80x40 volume from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Description says what the instrument does (OFF sample rotated by omega
  around y, transmitted beam on a 2D detector) and gives a working
  tomography command (omega=0,355 -N72 -n1e7 -d TomoScan), replacing the
  stale example with the non-existent offfile= parameter and the
  unshipped bunny.off.
- %Scan: moved after %Example so mcdoc keeps the description intact; now
  a full rotation in 5 deg steps, omega=0,355 -N72 -n1e6, targets
  generated as mctest runs it (-s 1000 --scan_split=auto).
- Defaults det_w=0.25, det_h=0.15, opts="x bins=128 y bins=64";
  %Example: target updated for the smaller detector (9.37708e-10).
- README.md regenerated with mcdoc's Markdown writer.

Verified: mctest passes the %Example (100%) and the scan (72/72 points
at 100%, ~82 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan failed on Windows CI (mxtest --mpi=auto, 29/31 points, worst
125%): the two SPLIT 10 Bragg crystals reuse each history up to 100x, so
at 1e6 the real seed-to-seed scatter was 8-12% per point against 3-5%
error bars. At 1e7 the typical scatter is ~2%. Targets are the mean of
seeds 1000/2000/3000 (all within 8% of it).

Verified: mxtest --seed 5000 (31/31, worst 92%, 50 s) and --mpi=auto
(31/31, worst 106%, 92 s) on macOS arm64. Expect ~15 min on Windows CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@willend

willend commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@mads-bertelsen, @g5t, @farhi, @Lomholy won't request a full review from either of you (I know you are all busy with other stuff), but wanted you to know this is incoming.

A quick thumbs up / down is sufficient for now - and we can of course re-model details later.

@willend

willend commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@Lomholy likes it. ;-) This makes me think "let's roll with it for a while". Merging.

@willend
willend merged commit 8c8e115 into mccode-dev:main Oct 9, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant