Skip to content

Commit c6c8c8b

Browse files
authored
Merge pull request #2081 from Lomholy/main
Add in support for splitting a scan into many cpu processes
2 parents 3e8d3d2 + fadd76b commit c6c8c8b

File tree

2 files changed

+125
-6
lines changed

2 files changed

+125
-6
lines changed

tools/Python/mcrun/mcrun.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
from optparse import OptionParser, OptionGroup, OptionValueError
1212
from decimal import Decimal, InvalidOperation
1313
from datetime import datetime
14-
14+
import multiprocessing
1515
from mccode import McStas, Process
16-
from optimisation import Scanner, LinearInterval, MultiInterval, Optimizer
16+
from optimisation import Scanner, Scanner_split, LinearInterval, MultiInterval, Optimizer
1717

1818
# import config
1919
import sys
@@ -99,6 +99,11 @@ def add_mcrun_options(parser):
9999
action='store_true',
100100
help='Run a multi-dimensional scan')
101101

102+
add("--scan_split",
103+
type=int,
104+
metavar="scan_split",
105+
help='Scan by parallelising steps as individual cpu threads. Initialise by number of wanted threads (e.g. your number of cores).')
106+
102107
add('--autoplot',
103108
action='store_true',
104109
help='Open plotter on generated dataset')
@@ -264,11 +269,11 @@ def add_mcstas_options(parser):
264269
add('-t', '--trace',
265270
metavar='trace', type=int, default=0,
266271
help='Enable trace of %ss through instrument' % (mccode_config.configuration["PARTICLE"]))
267-
272+
268273
add('--no-trace',
269274
action='store_true', metavar='notrace', default=None,
270275
help='Disable trace of %ss in instrument (combine with -c)' % (mccode_config.configuration["PARTICLE"]))
271-
276+
272277
add('-y', '--yes',
273278
action='store_true', default=False,
274279
help='Assume any default parameter value in instrument')
@@ -574,13 +579,28 @@ def main():
574579
elif options.numpoints is not None:
575580
interval_points = LinearInterval.from_range(options.numpoints, intervals)
576581

582+
583+
# Check that mpi and scan split are not both used. Default to mpi if they are
584+
if options.scan_split is not None and options.mpi is not None:
585+
options.scan_split = None
586+
577587
# Parameters for linear scanning present
578-
if interval_points:
588+
if interval_points and (options.scan_split is None):
579589
scanner = Scanner(mcstas, intervals)
580590
scanner.set_points(interval_points)
581591
if (not options.dir == ''):
582592
mkdir(options.dir)
583593
scanner.run() # in optimisation.py
594+
595+
elif options.scan_split is not None:
596+
if options.scan_split == 0:
597+
options.scan_split = multiprocessing.cpu_count()-1
598+
split_scanner = Scanner_split(mcstas, intervals, options.scan_split)
599+
split_scanner.set_points(interval_points)
600+
if (not options.dir == ''):
601+
mkdir(options.dir)
602+
split_scanner.run() # in optimisation.py
603+
584604
elif options.optimize:
585605
optimizer = Optimizer(mcstas, intervals)
586606
if (not options.dir == ''):

tools/Python/mcrun/optimisation.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from datetime import datetime
44
from decimal import Decimal
55
from os.path import join
6+
from multiprocessing import Pool
7+
import copy
68

79
try:
810
from scipy.optimize import minimize
@@ -218,9 +220,39 @@ def from_range(N, intervals):
218220
class InvalidInterval(McRunException):
219221
pass
220222

223+
def _simulate_point(args):
224+
i, point, intervals, mcstas_config, mcstas_dir = args
225+
226+
from shutil import copyfile
227+
from os.path import join
228+
229+
# Make a new instance of McStas and configure it
230+
mcstas = copy.deepcopy(mcstas_config) # You need to define a way to deepcopy or clone your mcstas object
231+
par_values = []
232+
233+
# Ensure we get a mccode.sim pr. thread subdir (e.g. for monitoring seed value
234+
mcstas.simfile = join(mcstas_dir, 'mccode.sim')
235+
236+
# Shift thread seed to avoid duplicate simulations / biasing
237+
mcstas.options.seed = (i*1024)+mcstas.options.seed
238+
239+
for key in intervals:
240+
mcstas.set_parameter(key, point[key])
241+
par_values.append(point[key])
242+
243+
current_dir = f'{mcstas_dir}/{i}'
244+
mcstas.run(pipe=False, extra_opts={'dir': current_dir})
245+
detectors = mcsimdetectors(current_dir)
246+
247+
result = {
248+
'index': i,
249+
'params': par_values,
250+
'detectors': detectors
251+
}
252+
return result
221253

222254
class Scanner:
223-
""" Perform a series of simulation steps along a given set of points """
255+
""" Perform a series of simulation steps along a given set of points"""
224256
def __init__(self, mcstas, intervals):
225257
self.mcstas = mcstas
226258
self.intervals = intervals
@@ -244,6 +276,7 @@ def run(self):
244276
mcstas_dir = self.mcstas.options.dir
245277
if mcstas_dir == '':
246278
mcstas_dir = '.'
279+
247280

248281
with open(self.outfile, 'w') as outfile:
249282
for i, point in enumerate(self.points):
@@ -278,6 +311,72 @@ def run(self):
278311
outfile.flush()
279312

280313

314+
class Scanner_split:
315+
""" Perform a series of simulation steps along a given set of points,
316+
Where each simulation is controlled by its own thread. """
317+
def __init__(self, mcstas, intervals, nb_cpu):
318+
self.mcstas = mcstas
319+
self.intervals = intervals
320+
self.points = None
321+
self.nb_cpu = nb_cpu
322+
self.outfile = mcstas.options.optimise_file
323+
self.simfile = join(mcstas.options.dir, 'mccode.sim')
324+
325+
def set_points(self, points):
326+
self.points = points
327+
328+
def set_outfile(self, path):
329+
self.outfile = path
330+
331+
def run(self):
332+
LOG.info('Running Scanner split, result file is "%s"' % self.outfile)
333+
334+
if len(self.intervals) == 0:
335+
raise InvalidInterval('No interval range specified')
336+
337+
mcstas_dir = self.mcstas.options.dir or '.'
338+
339+
if self.mcstas.options.seed is None:
340+
dt=datetime.now()
341+
LOG.info('No incoming seed from cmdline, setting to current Unix epoch (%d)!' % dt.timestamp())
342+
self.mcstas.options.seed=dt.timestamp()
343+
344+
# Prepare data to pass into processes
345+
args_list = [
346+
(i, point, self.intervals, self.mcstas, mcstas_dir)
347+
for i, point in enumerate(self.points)
348+
]
349+
350+
with Pool(processes=self.nb_cpu) as pool:
351+
results = pool.map(_simulate_point, args_list)
352+
353+
# Sort results to preserve order
354+
results.sort(key=lambda r: r['index'])
355+
356+
with open(self.outfile, 'w') as outfile:
357+
wrote_headers = False
358+
for result in results:
359+
if result['detectors'] is None:
360+
continue
361+
362+
if not wrote_headers:
363+
names = [d.name for d in result['detectors']]
364+
outfile.write(build_header(self.mcstas.options, self.intervals.keys(), self.intervals, names))
365+
with open(self.simfile, 'w') as simfile:
366+
simfile.write(build_mccodesim_header(
367+
self.mcstas.options,
368+
self.intervals,
369+
names,
370+
version=self.mcstas.version
371+
))
372+
wrote_headers = True
373+
374+
values = ['%s %s' % (d.intensity, d.error) for d in result['detectors']]
375+
line = '%s %s\n' % (' '.join(map(str, result['params'])), ' '.join(values))
376+
outfile.write(line)
377+
outfile.flush()
378+
379+
281380
class Optimizer:
282381
""" Optimize monitors by varying the parameters within interval """
283382

0 commit comments

Comments
 (0)