##// END OF EJS Templates
Added wait=True to a couple more clear_output calls.
Jonathan Frederic -
Show More
@@ -1,706 +1,706 b''
1 """AsyncResult objects for the client
1 """AsyncResult objects for the client
2
2
3 Authors:
3 Authors:
4
4
5 * MinRK
5 * MinRK
6 """
6 """
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2010-2011 The IPython Development Team
8 # Copyright (C) 2010-2011 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 from __future__ import print_function
18 from __future__ import print_function
19
19
20 import sys
20 import sys
21 import time
21 import time
22 from datetime import datetime
22 from datetime import datetime
23
23
24 from zmq import MessageTracker
24 from zmq import MessageTracker
25
25
26 from IPython.core.display import clear_output, display, display_pretty
26 from IPython.core.display import clear_output, display, display_pretty
27 from IPython.external.decorator import decorator
27 from IPython.external.decorator import decorator
28 from IPython.parallel import error
28 from IPython.parallel import error
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Functions
31 # Functions
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34 def _raw_text(s):
34 def _raw_text(s):
35 display_pretty(s, raw=True)
35 display_pretty(s, raw=True)
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Classes
38 # Classes
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40
40
41 # global empty tracker that's always done:
41 # global empty tracker that's always done:
42 finished_tracker = MessageTracker()
42 finished_tracker = MessageTracker()
43
43
44 @decorator
44 @decorator
45 def check_ready(f, self, *args, **kwargs):
45 def check_ready(f, self, *args, **kwargs):
46 """Call spin() to sync state prior to calling the method."""
46 """Call spin() to sync state prior to calling the method."""
47 self.wait(0)
47 self.wait(0)
48 if not self._ready:
48 if not self._ready:
49 raise error.TimeoutError("result not ready")
49 raise error.TimeoutError("result not ready")
50 return f(self, *args, **kwargs)
50 return f(self, *args, **kwargs)
51
51
52 class AsyncResult(object):
52 class AsyncResult(object):
53 """Class for representing results of non-blocking calls.
53 """Class for representing results of non-blocking calls.
54
54
55 Provides the same interface as :py:class:`multiprocessing.pool.AsyncResult`.
55 Provides the same interface as :py:class:`multiprocessing.pool.AsyncResult`.
56 """
56 """
57
57
58 msg_ids = None
58 msg_ids = None
59 _targets = None
59 _targets = None
60 _tracker = None
60 _tracker = None
61 _single_result = False
61 _single_result = False
62
62
63 def __init__(self, client, msg_ids, fname='unknown', targets=None, tracker=None):
63 def __init__(self, client, msg_ids, fname='unknown', targets=None, tracker=None):
64 if isinstance(msg_ids, basestring):
64 if isinstance(msg_ids, basestring):
65 # always a list
65 # always a list
66 msg_ids = [msg_ids]
66 msg_ids = [msg_ids]
67 self._single_result = True
67 self._single_result = True
68 else:
68 else:
69 self._single_result = False
69 self._single_result = False
70 if tracker is None:
70 if tracker is None:
71 # default to always done
71 # default to always done
72 tracker = finished_tracker
72 tracker = finished_tracker
73 self._client = client
73 self._client = client
74 self.msg_ids = msg_ids
74 self.msg_ids = msg_ids
75 self._fname=fname
75 self._fname=fname
76 self._targets = targets
76 self._targets = targets
77 self._tracker = tracker
77 self._tracker = tracker
78
78
79 self._ready = False
79 self._ready = False
80 self._outputs_ready = False
80 self._outputs_ready = False
81 self._success = None
81 self._success = None
82 self._metadata = [ self._client.metadata.get(id) for id in self.msg_ids ]
82 self._metadata = [ self._client.metadata.get(id) for id in self.msg_ids ]
83
83
84 def __repr__(self):
84 def __repr__(self):
85 if self._ready:
85 if self._ready:
86 return "<%s: finished>"%(self.__class__.__name__)
86 return "<%s: finished>"%(self.__class__.__name__)
87 else:
87 else:
88 return "<%s: %s>"%(self.__class__.__name__,self._fname)
88 return "<%s: %s>"%(self.__class__.__name__,self._fname)
89
89
90
90
91 def _reconstruct_result(self, res):
91 def _reconstruct_result(self, res):
92 """Reconstruct our result from actual result list (always a list)
92 """Reconstruct our result from actual result list (always a list)
93
93
94 Override me in subclasses for turning a list of results
94 Override me in subclasses for turning a list of results
95 into the expected form.
95 into the expected form.
96 """
96 """
97 if self._single_result:
97 if self._single_result:
98 return res[0]
98 return res[0]
99 else:
99 else:
100 return res
100 return res
101
101
102 def get(self, timeout=-1):
102 def get(self, timeout=-1):
103 """Return the result when it arrives.
103 """Return the result when it arrives.
104
104
105 If `timeout` is not ``None`` and the result does not arrive within
105 If `timeout` is not ``None`` and the result does not arrive within
106 `timeout` seconds then ``TimeoutError`` is raised. If the
106 `timeout` seconds then ``TimeoutError`` is raised. If the
107 remote call raised an exception then that exception will be reraised
107 remote call raised an exception then that exception will be reraised
108 by get() inside a `RemoteError`.
108 by get() inside a `RemoteError`.
109 """
109 """
110 if not self.ready():
110 if not self.ready():
111 self.wait(timeout)
111 self.wait(timeout)
112
112
113 if self._ready:
113 if self._ready:
114 if self._success:
114 if self._success:
115 return self._result
115 return self._result
116 else:
116 else:
117 raise self._exception
117 raise self._exception
118 else:
118 else:
119 raise error.TimeoutError("Result not ready.")
119 raise error.TimeoutError("Result not ready.")
120
120
121 def _check_ready(self):
121 def _check_ready(self):
122 if not self.ready():
122 if not self.ready():
123 raise error.TimeoutError("Result not ready.")
123 raise error.TimeoutError("Result not ready.")
124
124
125 def ready(self):
125 def ready(self):
126 """Return whether the call has completed."""
126 """Return whether the call has completed."""
127 if not self._ready:
127 if not self._ready:
128 self.wait(0)
128 self.wait(0)
129 elif not self._outputs_ready:
129 elif not self._outputs_ready:
130 self._wait_for_outputs(0)
130 self._wait_for_outputs(0)
131
131
132 return self._ready
132 return self._ready
133
133
134 def wait(self, timeout=-1):
134 def wait(self, timeout=-1):
135 """Wait until the result is available or until `timeout` seconds pass.
135 """Wait until the result is available or until `timeout` seconds pass.
136
136
137 This method always returns None.
137 This method always returns None.
138 """
138 """
139 if self._ready:
139 if self._ready:
140 self._wait_for_outputs(timeout)
140 self._wait_for_outputs(timeout)
141 return
141 return
142 self._ready = self._client.wait(self.msg_ids, timeout)
142 self._ready = self._client.wait(self.msg_ids, timeout)
143 if self._ready:
143 if self._ready:
144 try:
144 try:
145 results = map(self._client.results.get, self.msg_ids)
145 results = map(self._client.results.get, self.msg_ids)
146 self._result = results
146 self._result = results
147 if self._single_result:
147 if self._single_result:
148 r = results[0]
148 r = results[0]
149 if isinstance(r, Exception):
149 if isinstance(r, Exception):
150 raise r
150 raise r
151 else:
151 else:
152 results = error.collect_exceptions(results, self._fname)
152 results = error.collect_exceptions(results, self._fname)
153 self._result = self._reconstruct_result(results)
153 self._result = self._reconstruct_result(results)
154 except Exception as e:
154 except Exception as e:
155 self._exception = e
155 self._exception = e
156 self._success = False
156 self._success = False
157 else:
157 else:
158 self._success = True
158 self._success = True
159 finally:
159 finally:
160 if timeout is None or timeout < 0:
160 if timeout is None or timeout < 0:
161 # cutoff infinite wait at 10s
161 # cutoff infinite wait at 10s
162 timeout = 10
162 timeout = 10
163 self._wait_for_outputs(timeout)
163 self._wait_for_outputs(timeout)
164
164
165
165
166 def successful(self):
166 def successful(self):
167 """Return whether the call completed without raising an exception.
167 """Return whether the call completed without raising an exception.
168
168
169 Will raise ``AssertionError`` if the result is not ready.
169 Will raise ``AssertionError`` if the result is not ready.
170 """
170 """
171 assert self.ready()
171 assert self.ready()
172 return self._success
172 return self._success
173
173
174 #----------------------------------------------------------------
174 #----------------------------------------------------------------
175 # Extra methods not in mp.pool.AsyncResult
175 # Extra methods not in mp.pool.AsyncResult
176 #----------------------------------------------------------------
176 #----------------------------------------------------------------
177
177
178 def get_dict(self, timeout=-1):
178 def get_dict(self, timeout=-1):
179 """Get the results as a dict, keyed by engine_id.
179 """Get the results as a dict, keyed by engine_id.
180
180
181 timeout behavior is described in `get()`.
181 timeout behavior is described in `get()`.
182 """
182 """
183
183
184 results = self.get(timeout)
184 results = self.get(timeout)
185 if self._single_result:
185 if self._single_result:
186 results = [results]
186 results = [results]
187 engine_ids = [ md['engine_id'] for md in self._metadata ]
187 engine_ids = [ md['engine_id'] for md in self._metadata ]
188
188
189
189
190 rdict = {}
190 rdict = {}
191 for engine_id, result in zip(engine_ids, results):
191 for engine_id, result in zip(engine_ids, results):
192 if engine_id in rdict:
192 if engine_id in rdict:
193 raise ValueError("Cannot build dict, %i jobs ran on engine #%i" % (
193 raise ValueError("Cannot build dict, %i jobs ran on engine #%i" % (
194 engine_ids.count(engine_id), engine_id)
194 engine_ids.count(engine_id), engine_id)
195 )
195 )
196 else:
196 else:
197 rdict[engine_id] = result
197 rdict[engine_id] = result
198
198
199 return rdict
199 return rdict
200
200
201 @property
201 @property
202 def result(self):
202 def result(self):
203 """result property wrapper for `get(timeout=-1)`."""
203 """result property wrapper for `get(timeout=-1)`."""
204 return self.get()
204 return self.get()
205
205
206 # abbreviated alias:
206 # abbreviated alias:
207 r = result
207 r = result
208
208
209 @property
209 @property
210 def metadata(self):
210 def metadata(self):
211 """property for accessing execution metadata."""
211 """property for accessing execution metadata."""
212 if self._single_result:
212 if self._single_result:
213 return self._metadata[0]
213 return self._metadata[0]
214 else:
214 else:
215 return self._metadata
215 return self._metadata
216
216
217 @property
217 @property
218 def result_dict(self):
218 def result_dict(self):
219 """result property as a dict."""
219 """result property as a dict."""
220 return self.get_dict()
220 return self.get_dict()
221
221
222 def __dict__(self):
222 def __dict__(self):
223 return self.get_dict(0)
223 return self.get_dict(0)
224
224
225 def abort(self):
225 def abort(self):
226 """abort my tasks."""
226 """abort my tasks."""
227 assert not self.ready(), "Can't abort, I am already done!"
227 assert not self.ready(), "Can't abort, I am already done!"
228 return self._client.abort(self.msg_ids, targets=self._targets, block=True)
228 return self._client.abort(self.msg_ids, targets=self._targets, block=True)
229
229
230 @property
230 @property
231 def sent(self):
231 def sent(self):
232 """check whether my messages have been sent."""
232 """check whether my messages have been sent."""
233 return self._tracker.done
233 return self._tracker.done
234
234
235 def wait_for_send(self, timeout=-1):
235 def wait_for_send(self, timeout=-1):
236 """wait for pyzmq send to complete.
236 """wait for pyzmq send to complete.
237
237
238 This is necessary when sending arrays that you intend to edit in-place.
238 This is necessary when sending arrays that you intend to edit in-place.
239 `timeout` is in seconds, and will raise TimeoutError if it is reached
239 `timeout` is in seconds, and will raise TimeoutError if it is reached
240 before the send completes.
240 before the send completes.
241 """
241 """
242 return self._tracker.wait(timeout)
242 return self._tracker.wait(timeout)
243
243
244 #-------------------------------------
244 #-------------------------------------
245 # dict-access
245 # dict-access
246 #-------------------------------------
246 #-------------------------------------
247
247
248 def __getitem__(self, key):
248 def __getitem__(self, key):
249 """getitem returns result value(s) if keyed by int/slice, or metadata if key is str.
249 """getitem returns result value(s) if keyed by int/slice, or metadata if key is str.
250 """
250 """
251 if isinstance(key, int):
251 if isinstance(key, int):
252 self._check_ready()
252 self._check_ready()
253 return error.collect_exceptions([self._result[key]], self._fname)[0]
253 return error.collect_exceptions([self._result[key]], self._fname)[0]
254 elif isinstance(key, slice):
254 elif isinstance(key, slice):
255 self._check_ready()
255 self._check_ready()
256 return error.collect_exceptions(self._result[key], self._fname)
256 return error.collect_exceptions(self._result[key], self._fname)
257 elif isinstance(key, basestring):
257 elif isinstance(key, basestring):
258 # metadata proxy *does not* require that results are done
258 # metadata proxy *does not* require that results are done
259 self.wait(0)
259 self.wait(0)
260 values = [ md[key] for md in self._metadata ]
260 values = [ md[key] for md in self._metadata ]
261 if self._single_result:
261 if self._single_result:
262 return values[0]
262 return values[0]
263 else:
263 else:
264 return values
264 return values
265 else:
265 else:
266 raise TypeError("Invalid key type %r, must be 'int','slice', or 'str'"%type(key))
266 raise TypeError("Invalid key type %r, must be 'int','slice', or 'str'"%type(key))
267
267
268 def __getattr__(self, key):
268 def __getattr__(self, key):
269 """getattr maps to getitem for convenient attr access to metadata."""
269 """getattr maps to getitem for convenient attr access to metadata."""
270 try:
270 try:
271 return self.__getitem__(key)
271 return self.__getitem__(key)
272 except (error.TimeoutError, KeyError):
272 except (error.TimeoutError, KeyError):
273 raise AttributeError("%r object has no attribute %r"%(
273 raise AttributeError("%r object has no attribute %r"%(
274 self.__class__.__name__, key))
274 self.__class__.__name__, key))
275
275
276 # asynchronous iterator:
276 # asynchronous iterator:
277 def __iter__(self):
277 def __iter__(self):
278 if self._single_result:
278 if self._single_result:
279 raise TypeError("AsyncResults with a single result are not iterable.")
279 raise TypeError("AsyncResults with a single result are not iterable.")
280 try:
280 try:
281 rlist = self.get(0)
281 rlist = self.get(0)
282 except error.TimeoutError:
282 except error.TimeoutError:
283 # wait for each result individually
283 # wait for each result individually
284 for msg_id in self.msg_ids:
284 for msg_id in self.msg_ids:
285 ar = AsyncResult(self._client, msg_id, self._fname)
285 ar = AsyncResult(self._client, msg_id, self._fname)
286 yield ar.get()
286 yield ar.get()
287 else:
287 else:
288 # already done
288 # already done
289 for r in rlist:
289 for r in rlist:
290 yield r
290 yield r
291
291
292 def __len__(self):
292 def __len__(self):
293 return len(self.msg_ids)
293 return len(self.msg_ids)
294
294
295 #-------------------------------------
295 #-------------------------------------
296 # Sugar methods and attributes
296 # Sugar methods and attributes
297 #-------------------------------------
297 #-------------------------------------
298
298
299 def timedelta(self, start, end, start_key=min, end_key=max):
299 def timedelta(self, start, end, start_key=min, end_key=max):
300 """compute the difference between two sets of timestamps
300 """compute the difference between two sets of timestamps
301
301
302 The default behavior is to use the earliest of the first
302 The default behavior is to use the earliest of the first
303 and the latest of the second list, but this can be changed
303 and the latest of the second list, but this can be changed
304 by passing a different
304 by passing a different
305
305
306 Parameters
306 Parameters
307 ----------
307 ----------
308
308
309 start : one or more datetime objects (e.g. ar.submitted)
309 start : one or more datetime objects (e.g. ar.submitted)
310 end : one or more datetime objects (e.g. ar.received)
310 end : one or more datetime objects (e.g. ar.received)
311 start_key : callable
311 start_key : callable
312 Function to call on `start` to extract the relevant
312 Function to call on `start` to extract the relevant
313 entry [defalt: min]
313 entry [defalt: min]
314 end_key : callable
314 end_key : callable
315 Function to call on `end` to extract the relevant
315 Function to call on `end` to extract the relevant
316 entry [default: max]
316 entry [default: max]
317
317
318 Returns
318 Returns
319 -------
319 -------
320
320
321 dt : float
321 dt : float
322 The time elapsed (in seconds) between the two selected timestamps.
322 The time elapsed (in seconds) between the two selected timestamps.
323 """
323 """
324 if not isinstance(start, datetime):
324 if not isinstance(start, datetime):
325 # handle single_result AsyncResults, where ar.stamp is single object,
325 # handle single_result AsyncResults, where ar.stamp is single object,
326 # not a list
326 # not a list
327 start = start_key(start)
327 start = start_key(start)
328 if not isinstance(end, datetime):
328 if not isinstance(end, datetime):
329 # handle single_result AsyncResults, where ar.stamp is single object,
329 # handle single_result AsyncResults, where ar.stamp is single object,
330 # not a list
330 # not a list
331 end = end_key(end)
331 end = end_key(end)
332 return (end - start).total_seconds()
332 return (end - start).total_seconds()
333
333
334 @property
334 @property
335 def progress(self):
335 def progress(self):
336 """the number of tasks which have been completed at this point.
336 """the number of tasks which have been completed at this point.
337
337
338 Fractional progress would be given by 1.0 * ar.progress / len(ar)
338 Fractional progress would be given by 1.0 * ar.progress / len(ar)
339 """
339 """
340 self.wait(0)
340 self.wait(0)
341 return len(self) - len(set(self.msg_ids).intersection(self._client.outstanding))
341 return len(self) - len(set(self.msg_ids).intersection(self._client.outstanding))
342
342
343 @property
343 @property
344 def elapsed(self):
344 def elapsed(self):
345 """elapsed time since initial submission"""
345 """elapsed time since initial submission"""
346 if self.ready():
346 if self.ready():
347 return self.wall_time
347 return self.wall_time
348
348
349 now = submitted = datetime.now()
349 now = submitted = datetime.now()
350 for msg_id in self.msg_ids:
350 for msg_id in self.msg_ids:
351 if msg_id in self._client.metadata:
351 if msg_id in self._client.metadata:
352 stamp = self._client.metadata[msg_id]['submitted']
352 stamp = self._client.metadata[msg_id]['submitted']
353 if stamp and stamp < submitted:
353 if stamp and stamp < submitted:
354 submitted = stamp
354 submitted = stamp
355 return (now-submitted).total_seconds()
355 return (now-submitted).total_seconds()
356
356
357 @property
357 @property
358 @check_ready
358 @check_ready
359 def serial_time(self):
359 def serial_time(self):
360 """serial computation time of a parallel calculation
360 """serial computation time of a parallel calculation
361
361
362 Computed as the sum of (completed-started) of each task
362 Computed as the sum of (completed-started) of each task
363 """
363 """
364 t = 0
364 t = 0
365 for md in self._metadata:
365 for md in self._metadata:
366 t += (md['completed'] - md['started']).total_seconds()
366 t += (md['completed'] - md['started']).total_seconds()
367 return t
367 return t
368
368
369 @property
369 @property
370 @check_ready
370 @check_ready
371 def wall_time(self):
371 def wall_time(self):
372 """actual computation time of a parallel calculation
372 """actual computation time of a parallel calculation
373
373
374 Computed as the time between the latest `received` stamp
374 Computed as the time between the latest `received` stamp
375 and the earliest `submitted`.
375 and the earliest `submitted`.
376
376
377 Only reliable if Client was spinning/waiting when the task finished, because
377 Only reliable if Client was spinning/waiting when the task finished, because
378 the `received` timestamp is created when a result is pulled off of the zmq queue,
378 the `received` timestamp is created when a result is pulled off of the zmq queue,
379 which happens as a result of `client.spin()`.
379 which happens as a result of `client.spin()`.
380
380
381 For similar comparison of other timestamp pairs, check out AsyncResult.timedelta.
381 For similar comparison of other timestamp pairs, check out AsyncResult.timedelta.
382
382
383 """
383 """
384 return self.timedelta(self.submitted, self.received)
384 return self.timedelta(self.submitted, self.received)
385
385
386 def wait_interactive(self, interval=1., timeout=-1):
386 def wait_interactive(self, interval=1., timeout=-1):
387 """interactive wait, printing progress at regular intervals"""
387 """interactive wait, printing progress at regular intervals"""
388 if timeout is None:
388 if timeout is None:
389 timeout = -1
389 timeout = -1
390 N = len(self)
390 N = len(self)
391 tic = time.time()
391 tic = time.time()
392 while not self.ready() and (timeout < 0 or time.time() - tic <= timeout):
392 while not self.ready() and (timeout < 0 or time.time() - tic <= timeout):
393 self.wait(interval)
393 self.wait(interval)
394 clear_output()
394 clear_output(wait=True)
395 print("%4i/%i tasks finished after %4i s" % (self.progress, N, self.elapsed), end="")
395 print("%4i/%i tasks finished after %4i s" % (self.progress, N, self.elapsed), end="")
396 sys.stdout.flush()
396 sys.stdout.flush()
397 print()
397 print()
398 print("done")
398 print("done")
399
399
400 def _republish_displaypub(self, content, eid):
400 def _republish_displaypub(self, content, eid):
401 """republish individual displaypub content dicts"""
401 """republish individual displaypub content dicts"""
402 try:
402 try:
403 ip = get_ipython()
403 ip = get_ipython()
404 except NameError:
404 except NameError:
405 # displaypub is meaningless outside IPython
405 # displaypub is meaningless outside IPython
406 return
406 return
407 md = content['metadata'] or {}
407 md = content['metadata'] or {}
408 md['engine'] = eid
408 md['engine'] = eid
409 ip.display_pub.publish(content['source'], content['data'], md)
409 ip.display_pub.publish(content['source'], content['data'], md)
410
410
411 def _display_stream(self, text, prefix='', file=None):
411 def _display_stream(self, text, prefix='', file=None):
412 if not text:
412 if not text:
413 # nothing to display
413 # nothing to display
414 return
414 return
415 if file is None:
415 if file is None:
416 file = sys.stdout
416 file = sys.stdout
417 end = '' if text.endswith('\n') else '\n'
417 end = '' if text.endswith('\n') else '\n'
418
418
419 multiline = text.count('\n') > int(text.endswith('\n'))
419 multiline = text.count('\n') > int(text.endswith('\n'))
420 if prefix and multiline and not text.startswith('\n'):
420 if prefix and multiline and not text.startswith('\n'):
421 prefix = prefix + '\n'
421 prefix = prefix + '\n'
422 print("%s%s" % (prefix, text), file=file, end=end)
422 print("%s%s" % (prefix, text), file=file, end=end)
423
423
424
424
425 def _display_single_result(self):
425 def _display_single_result(self):
426 self._display_stream(self.stdout)
426 self._display_stream(self.stdout)
427 self._display_stream(self.stderr, file=sys.stderr)
427 self._display_stream(self.stderr, file=sys.stderr)
428
428
429 try:
429 try:
430 get_ipython()
430 get_ipython()
431 except NameError:
431 except NameError:
432 # displaypub is meaningless outside IPython
432 # displaypub is meaningless outside IPython
433 return
433 return
434
434
435 for output in self.outputs:
435 for output in self.outputs:
436 self._republish_displaypub(output, self.engine_id)
436 self._republish_displaypub(output, self.engine_id)
437
437
438 if self.pyout is not None:
438 if self.pyout is not None:
439 display(self.get())
439 display(self.get())
440
440
441 def _wait_for_outputs(self, timeout=-1):
441 def _wait_for_outputs(self, timeout=-1):
442 """wait for the 'status=idle' message that indicates we have all outputs
442 """wait for the 'status=idle' message that indicates we have all outputs
443 """
443 """
444 if self._outputs_ready or not self._success:
444 if self._outputs_ready or not self._success:
445 # don't wait on errors
445 # don't wait on errors
446 return
446 return
447
447
448 # cast None to -1 for infinite timeout
448 # cast None to -1 for infinite timeout
449 if timeout is None:
449 if timeout is None:
450 timeout = -1
450 timeout = -1
451
451
452 tic = time.time()
452 tic = time.time()
453 while True:
453 while True:
454 self._client._flush_iopub(self._client._iopub_socket)
454 self._client._flush_iopub(self._client._iopub_socket)
455 self._outputs_ready = all(md['outputs_ready']
455 self._outputs_ready = all(md['outputs_ready']
456 for md in self._metadata)
456 for md in self._metadata)
457 if self._outputs_ready or \
457 if self._outputs_ready or \
458 (timeout >= 0 and time.time() > tic + timeout):
458 (timeout >= 0 and time.time() > tic + timeout):
459 break
459 break
460 time.sleep(0.01)
460 time.sleep(0.01)
461
461
462 @check_ready
462 @check_ready
463 def display_outputs(self, groupby="type"):
463 def display_outputs(self, groupby="type"):
464 """republish the outputs of the computation
464 """republish the outputs of the computation
465
465
466 Parameters
466 Parameters
467 ----------
467 ----------
468
468
469 groupby : str [default: type]
469 groupby : str [default: type]
470 if 'type':
470 if 'type':
471 Group outputs by type (show all stdout, then all stderr, etc.):
471 Group outputs by type (show all stdout, then all stderr, etc.):
472
472
473 [stdout:1] foo
473 [stdout:1] foo
474 [stdout:2] foo
474 [stdout:2] foo
475 [stderr:1] bar
475 [stderr:1] bar
476 [stderr:2] bar
476 [stderr:2] bar
477 if 'engine':
477 if 'engine':
478 Display outputs for each engine before moving on to the next:
478 Display outputs for each engine before moving on to the next:
479
479
480 [stdout:1] foo
480 [stdout:1] foo
481 [stderr:1] bar
481 [stderr:1] bar
482 [stdout:2] foo
482 [stdout:2] foo
483 [stderr:2] bar
483 [stderr:2] bar
484
484
485 if 'order':
485 if 'order':
486 Like 'type', but further collate individual displaypub
486 Like 'type', but further collate individual displaypub
487 outputs. This is meant for cases of each command producing
487 outputs. This is meant for cases of each command producing
488 several plots, and you would like to see all of the first
488 several plots, and you would like to see all of the first
489 plots together, then all of the second plots, and so on.
489 plots together, then all of the second plots, and so on.
490 """
490 """
491 if self._single_result:
491 if self._single_result:
492 self._display_single_result()
492 self._display_single_result()
493 return
493 return
494
494
495 stdouts = self.stdout
495 stdouts = self.stdout
496 stderrs = self.stderr
496 stderrs = self.stderr
497 pyouts = self.pyout
497 pyouts = self.pyout
498 output_lists = self.outputs
498 output_lists = self.outputs
499 results = self.get()
499 results = self.get()
500
500
501 targets = self.engine_id
501 targets = self.engine_id
502
502
503 if groupby == "engine":
503 if groupby == "engine":
504 for eid,stdout,stderr,outputs,r,pyout in zip(
504 for eid,stdout,stderr,outputs,r,pyout in zip(
505 targets, stdouts, stderrs, output_lists, results, pyouts
505 targets, stdouts, stderrs, output_lists, results, pyouts
506 ):
506 ):
507 self._display_stream(stdout, '[stdout:%i] ' % eid)
507 self._display_stream(stdout, '[stdout:%i] ' % eid)
508 self._display_stream(stderr, '[stderr:%i] ' % eid, file=sys.stderr)
508 self._display_stream(stderr, '[stderr:%i] ' % eid, file=sys.stderr)
509
509
510 try:
510 try:
511 get_ipython()
511 get_ipython()
512 except NameError:
512 except NameError:
513 # displaypub is meaningless outside IPython
513 # displaypub is meaningless outside IPython
514 return
514 return
515
515
516 if outputs or pyout is not None:
516 if outputs or pyout is not None:
517 _raw_text('[output:%i]' % eid)
517 _raw_text('[output:%i]' % eid)
518
518
519 for output in outputs:
519 for output in outputs:
520 self._republish_displaypub(output, eid)
520 self._republish_displaypub(output, eid)
521
521
522 if pyout is not None:
522 if pyout is not None:
523 display(r)
523 display(r)
524
524
525 elif groupby in ('type', 'order'):
525 elif groupby in ('type', 'order'):
526 # republish stdout:
526 # republish stdout:
527 for eid,stdout in zip(targets, stdouts):
527 for eid,stdout in zip(targets, stdouts):
528 self._display_stream(stdout, '[stdout:%i] ' % eid)
528 self._display_stream(stdout, '[stdout:%i] ' % eid)
529
529
530 # republish stderr:
530 # republish stderr:
531 for eid,stderr in zip(targets, stderrs):
531 for eid,stderr in zip(targets, stderrs):
532 self._display_stream(stderr, '[stderr:%i] ' % eid, file=sys.stderr)
532 self._display_stream(stderr, '[stderr:%i] ' % eid, file=sys.stderr)
533
533
534 try:
534 try:
535 get_ipython()
535 get_ipython()
536 except NameError:
536 except NameError:
537 # displaypub is meaningless outside IPython
537 # displaypub is meaningless outside IPython
538 return
538 return
539
539
540 if groupby == 'order':
540 if groupby == 'order':
541 output_dict = dict((eid, outputs) for eid,outputs in zip(targets, output_lists))
541 output_dict = dict((eid, outputs) for eid,outputs in zip(targets, output_lists))
542 N = max(len(outputs) for outputs in output_lists)
542 N = max(len(outputs) for outputs in output_lists)
543 for i in range(N):
543 for i in range(N):
544 for eid in targets:
544 for eid in targets:
545 outputs = output_dict[eid]
545 outputs = output_dict[eid]
546 if len(outputs) >= N:
546 if len(outputs) >= N:
547 _raw_text('[output:%i]' % eid)
547 _raw_text('[output:%i]' % eid)
548 self._republish_displaypub(outputs[i], eid)
548 self._republish_displaypub(outputs[i], eid)
549 else:
549 else:
550 # republish displaypub output
550 # republish displaypub output
551 for eid,outputs in zip(targets, output_lists):
551 for eid,outputs in zip(targets, output_lists):
552 if outputs:
552 if outputs:
553 _raw_text('[output:%i]' % eid)
553 _raw_text('[output:%i]' % eid)
554 for output in outputs:
554 for output in outputs:
555 self._republish_displaypub(output, eid)
555 self._republish_displaypub(output, eid)
556
556
557 # finally, add pyout:
557 # finally, add pyout:
558 for eid,r,pyout in zip(targets, results, pyouts):
558 for eid,r,pyout in zip(targets, results, pyouts):
559 if pyout is not None:
559 if pyout is not None:
560 display(r)
560 display(r)
561
561
562 else:
562 else:
563 raise ValueError("groupby must be one of 'type', 'engine', 'collate', not %r" % groupby)
563 raise ValueError("groupby must be one of 'type', 'engine', 'collate', not %r" % groupby)
564
564
565
565
566
566
567
567
568 class AsyncMapResult(AsyncResult):
568 class AsyncMapResult(AsyncResult):
569 """Class for representing results of non-blocking gathers.
569 """Class for representing results of non-blocking gathers.
570
570
571 This will properly reconstruct the gather.
571 This will properly reconstruct the gather.
572
572
573 This class is iterable at any time, and will wait on results as they come.
573 This class is iterable at any time, and will wait on results as they come.
574
574
575 If ordered=False, then the first results to arrive will come first, otherwise
575 If ordered=False, then the first results to arrive will come first, otherwise
576 results will be yielded in the order they were submitted.
576 results will be yielded in the order they were submitted.
577
577
578 """
578 """
579
579
580 def __init__(self, client, msg_ids, mapObject, fname='', ordered=True):
580 def __init__(self, client, msg_ids, mapObject, fname='', ordered=True):
581 AsyncResult.__init__(self, client, msg_ids, fname=fname)
581 AsyncResult.__init__(self, client, msg_ids, fname=fname)
582 self._mapObject = mapObject
582 self._mapObject = mapObject
583 self._single_result = False
583 self._single_result = False
584 self.ordered = ordered
584 self.ordered = ordered
585
585
586 def _reconstruct_result(self, res):
586 def _reconstruct_result(self, res):
587 """Perform the gather on the actual results."""
587 """Perform the gather on the actual results."""
588 return self._mapObject.joinPartitions(res)
588 return self._mapObject.joinPartitions(res)
589
589
590 # asynchronous iterator:
590 # asynchronous iterator:
591 def __iter__(self):
591 def __iter__(self):
592 it = self._ordered_iter if self.ordered else self._unordered_iter
592 it = self._ordered_iter if self.ordered else self._unordered_iter
593 for r in it():
593 for r in it():
594 yield r
594 yield r
595
595
596 # asynchronous ordered iterator:
596 # asynchronous ordered iterator:
597 def _ordered_iter(self):
597 def _ordered_iter(self):
598 """iterator for results *as they arrive*, preserving submission order."""
598 """iterator for results *as they arrive*, preserving submission order."""
599 try:
599 try:
600 rlist = self.get(0)
600 rlist = self.get(0)
601 except error.TimeoutError:
601 except error.TimeoutError:
602 # wait for each result individually
602 # wait for each result individually
603 for msg_id in self.msg_ids:
603 for msg_id in self.msg_ids:
604 ar = AsyncResult(self._client, msg_id, self._fname)
604 ar = AsyncResult(self._client, msg_id, self._fname)
605 rlist = ar.get()
605 rlist = ar.get()
606 try:
606 try:
607 for r in rlist:
607 for r in rlist:
608 yield r
608 yield r
609 except TypeError:
609 except TypeError:
610 # flattened, not a list
610 # flattened, not a list
611 # this could get broken by flattened data that returns iterables
611 # this could get broken by flattened data that returns iterables
612 # but most calls to map do not expose the `flatten` argument
612 # but most calls to map do not expose the `flatten` argument
613 yield rlist
613 yield rlist
614 else:
614 else:
615 # already done
615 # already done
616 for r in rlist:
616 for r in rlist:
617 yield r
617 yield r
618
618
619 # asynchronous unordered iterator:
619 # asynchronous unordered iterator:
620 def _unordered_iter(self):
620 def _unordered_iter(self):
621 """iterator for results *as they arrive*, on FCFS basis, ignoring submission order."""
621 """iterator for results *as they arrive*, on FCFS basis, ignoring submission order."""
622 try:
622 try:
623 rlist = self.get(0)
623 rlist = self.get(0)
624 except error.TimeoutError:
624 except error.TimeoutError:
625 pending = set(self.msg_ids)
625 pending = set(self.msg_ids)
626 while pending:
626 while pending:
627 try:
627 try:
628 self._client.wait(pending, 1e-3)
628 self._client.wait(pending, 1e-3)
629 except error.TimeoutError:
629 except error.TimeoutError:
630 # ignore timeout error, because that only means
630 # ignore timeout error, because that only means
631 # *some* jobs are outstanding
631 # *some* jobs are outstanding
632 pass
632 pass
633 # update ready set with those no longer outstanding:
633 # update ready set with those no longer outstanding:
634 ready = pending.difference(self._client.outstanding)
634 ready = pending.difference(self._client.outstanding)
635 # update pending to exclude those that are finished
635 # update pending to exclude those that are finished
636 pending = pending.difference(ready)
636 pending = pending.difference(ready)
637 while ready:
637 while ready:
638 msg_id = ready.pop()
638 msg_id = ready.pop()
639 ar = AsyncResult(self._client, msg_id, self._fname)
639 ar = AsyncResult(self._client, msg_id, self._fname)
640 rlist = ar.get()
640 rlist = ar.get()
641 try:
641 try:
642 for r in rlist:
642 for r in rlist:
643 yield r
643 yield r
644 except TypeError:
644 except TypeError:
645 # flattened, not a list
645 # flattened, not a list
646 # this could get broken by flattened data that returns iterables
646 # this could get broken by flattened data that returns iterables
647 # but most calls to map do not expose the `flatten` argument
647 # but most calls to map do not expose the `flatten` argument
648 yield rlist
648 yield rlist
649 else:
649 else:
650 # already done
650 # already done
651 for r in rlist:
651 for r in rlist:
652 yield r
652 yield r
653
653
654
654
655 class AsyncHubResult(AsyncResult):
655 class AsyncHubResult(AsyncResult):
656 """Class to wrap pending results that must be requested from the Hub.
656 """Class to wrap pending results that must be requested from the Hub.
657
657
658 Note that waiting/polling on these objects requires polling the Hubover the network,
658 Note that waiting/polling on these objects requires polling the Hubover the network,
659 so use `AsyncHubResult.wait()` sparingly.
659 so use `AsyncHubResult.wait()` sparingly.
660 """
660 """
661
661
662 def _wait_for_outputs(self, timeout=-1):
662 def _wait_for_outputs(self, timeout=-1):
663 """no-op, because HubResults are never incomplete"""
663 """no-op, because HubResults are never incomplete"""
664 self._outputs_ready = True
664 self._outputs_ready = True
665
665
666 def wait(self, timeout=-1):
666 def wait(self, timeout=-1):
667 """wait for result to complete."""
667 """wait for result to complete."""
668 start = time.time()
668 start = time.time()
669 if self._ready:
669 if self._ready:
670 return
670 return
671 local_ids = filter(lambda msg_id: msg_id in self._client.outstanding, self.msg_ids)
671 local_ids = filter(lambda msg_id: msg_id in self._client.outstanding, self.msg_ids)
672 local_ready = self._client.wait(local_ids, timeout)
672 local_ready = self._client.wait(local_ids, timeout)
673 if local_ready:
673 if local_ready:
674 remote_ids = filter(lambda msg_id: msg_id not in self._client.results, self.msg_ids)
674 remote_ids = filter(lambda msg_id: msg_id not in self._client.results, self.msg_ids)
675 if not remote_ids:
675 if not remote_ids:
676 self._ready = True
676 self._ready = True
677 else:
677 else:
678 rdict = self._client.result_status(remote_ids, status_only=False)
678 rdict = self._client.result_status(remote_ids, status_only=False)
679 pending = rdict['pending']
679 pending = rdict['pending']
680 while pending and (timeout < 0 or time.time() < start+timeout):
680 while pending and (timeout < 0 or time.time() < start+timeout):
681 rdict = self._client.result_status(remote_ids, status_only=False)
681 rdict = self._client.result_status(remote_ids, status_only=False)
682 pending = rdict['pending']
682 pending = rdict['pending']
683 if pending:
683 if pending:
684 time.sleep(0.1)
684 time.sleep(0.1)
685 if not pending:
685 if not pending:
686 self._ready = True
686 self._ready = True
687 if self._ready:
687 if self._ready:
688 try:
688 try:
689 results = map(self._client.results.get, self.msg_ids)
689 results = map(self._client.results.get, self.msg_ids)
690 self._result = results
690 self._result = results
691 if self._single_result:
691 if self._single_result:
692 r = results[0]
692 r = results[0]
693 if isinstance(r, Exception):
693 if isinstance(r, Exception):
694 raise r
694 raise r
695 else:
695 else:
696 results = error.collect_exceptions(results, self._fname)
696 results = error.collect_exceptions(results, self._fname)
697 self._result = self._reconstruct_result(results)
697 self._result = self._reconstruct_result(results)
698 except Exception as e:
698 except Exception as e:
699 self._exception = e
699 self._exception = e
700 self._success = False
700 self._success = False
701 else:
701 else:
702 self._success = True
702 self._success = True
703 finally:
703 finally:
704 self._metadata = map(self._client.metadata.get, self.msg_ids)
704 self._metadata = map(self._client.metadata.get, self.msg_ids)
705
705
706 __all__ = ['AsyncResult', 'AsyncMapResult', 'AsyncHubResult']
706 __all__ = ['AsyncResult', 'AsyncMapResult', 'AsyncHubResult']
@@ -1,412 +1,412 b''
1 {
1 {
2 "metadata": {
2 "metadata": {
3 "name": ""
3 "name": ""
4 },
4 },
5 "nbformat": 3,
5 "nbformat": 3,
6 "nbformat_minor": 0,
6 "nbformat_minor": 0,
7 "worksheets": [
7 "worksheets": [
8 {
8 {
9 "cells": [
9 "cells": [
10 {
10 {
11 "cell_type": "heading",
11 "cell_type": "heading",
12 "level": 1,
12 "level": 1,
13 "metadata": {},
13 "metadata": {},
14 "source": [
14 "source": [
15 "Interactive visualization of MPI simulaitons"
15 "Interactive visualization of MPI simulaitons"
16 ]
16 ]
17 },
17 },
18 {
18 {
19 "cell_type": "markdown",
19 "cell_type": "markdown",
20 "metadata": {},
20 "metadata": {},
21 "source": [
21 "source": [
22 "In this example, which builds on our previous one of interactive MPI monitoring, we now demonstrate how to use the IPython data publication APIs."
22 "In this example, which builds on our previous one of interactive MPI monitoring, we now demonstrate how to use the IPython data publication APIs."
23 ]
23 ]
24 },
24 },
25 {
25 {
26 "cell_type": "heading",
26 "cell_type": "heading",
27 "level": 2,
27 "level": 2,
28 "metadata": {},
28 "metadata": {},
29 "source": [
29 "source": [
30 "Load IPython support for working with MPI tasks"
30 "Load IPython support for working with MPI tasks"
31 ]
31 ]
32 },
32 },
33 {
33 {
34 "cell_type": "markdown",
34 "cell_type": "markdown",
35 "metadata": {},
35 "metadata": {},
36 "source": [
36 "source": [
37 "If you have not done so yet, use [the cluster tab in the Dashboard](/#tab2) to start your `mpi` cluster, it should be OK to leave the number of engines field empty (IPython will auto-detect the number of cores on your machine), unless you want to limit the run to use less cores than available in total. Once your MPI cluster is running, you can proceed with the rest of the code.\n",
37 "If you have not done so yet, use [the cluster tab in the Dashboard](/#tab2) to start your `mpi` cluster, it should be OK to leave the number of engines field empty (IPython will auto-detect the number of cores on your machine), unless you want to limit the run to use less cores than available in total. Once your MPI cluster is running, you can proceed with the rest of the code.\n",
38 "\n",
38 "\n",
39 "We begin by creating a cluster client that gives us a local handle on the engines running in the (possibly remote) MPI cluster. From the client we make a `view` object, which we set to use blocking mode by default as it is more convenient for interactive control. Since the real computation will be done over MPI without IPython intervention, setting the default behavior to be blocking will have no significant performance impact.\n",
39 "We begin by creating a cluster client that gives us a local handle on the engines running in the (possibly remote) MPI cluster. From the client we make a `view` object, which we set to use blocking mode by default as it is more convenient for interactive control. Since the real computation will be done over MPI without IPython intervention, setting the default behavior to be blocking will have no significant performance impact.\n",
40 "\n",
40 "\n",
41 "**Note:** if on first try the following cell gives you an error message, wait a few seconds and run it again. It's possible that the system is simply initializing all your MPI engines, which may take a bit of time to be completely ready if you hadn't used any MPI libraries recently and the disk cache is cold."
41 "**Note:** if on first try the following cell gives you an error message, wait a few seconds and run it again. It's possible that the system is simply initializing all your MPI engines, which may take a bit of time to be completely ready if you hadn't used any MPI libraries recently and the disk cache is cold."
42 ]
42 ]
43 },
43 },
44 {
44 {
45 "cell_type": "code",
45 "cell_type": "code",
46 "collapsed": false,
46 "collapsed": false,
47 "input": [
47 "input": [
48 "from IPython.parallel import Client, error\n",
48 "from IPython.parallel import Client, error\n",
49 "cluster = Client(profile=\"mpi\")\n",
49 "cluster = Client(profile=\"mpi\")\n",
50 "view = cluster[:]\n",
50 "view = cluster[:]\n",
51 "view.block = True"
51 "view.block = True"
52 ],
52 ],
53 "language": "python",
53 "language": "python",
54 "metadata": {},
54 "metadata": {},
55 "outputs": [],
55 "outputs": [],
56 "prompt_number": 1
56 "prompt_number": 1
57 },
57 },
58 {
58 {
59 "cell_type": "markdown",
59 "cell_type": "markdown",
60 "metadata": {},
60 "metadata": {},
61 "source": [
61 "source": [
62 "Let's also load the plotting and numerical libraries so we have them ready for visualization later on."
62 "Let's also load the plotting and numerical libraries so we have them ready for visualization later on."
63 ]
63 ]
64 },
64 },
65 {
65 {
66 "cell_type": "code",
66 "cell_type": "code",
67 "collapsed": false,
67 "collapsed": false,
68 "input": [
68 "input": [
69 "%matplotlib inline\n",
69 "%matplotlib inline\n",
70 "import numpy as np\n",
70 "import numpy as np\n",
71 "import matplotlib.pyplot as plt"
71 "import matplotlib.pyplot as plt"
72 ],
72 ],
73 "language": "python",
73 "language": "python",
74 "metadata": {},
74 "metadata": {},
75 "outputs": [],
75 "outputs": [],
76 "prompt_number": 2
76 "prompt_number": 2
77 },
77 },
78 {
78 {
79 "cell_type": "markdown",
79 "cell_type": "markdown",
80 "metadata": {},
80 "metadata": {},
81 "source": [
81 "source": [
82 "Now, we load the MPI libraries into the engine namespaces, and do a simple printing of their MPI rank information to verify that all nodes are operational and they match our cluster's real capacity. \n",
82 "Now, we load the MPI libraries into the engine namespaces, and do a simple printing of their MPI rank information to verify that all nodes are operational and they match our cluster's real capacity. \n",
83 "\n",
83 "\n",
84 "Here, we are making use of IPython's special `%%px` cell magic, which marks the entire cell for parallel execution. This means that the code below will not run in this notebook's kernel, but instead will be sent to *all* engines for execution there. In this way, IPython makes it very natural to control your entire cluster from within the notebook environment:"
84 "Here, we are making use of IPython's special `%%px` cell magic, which marks the entire cell for parallel execution. This means that the code below will not run in this notebook's kernel, but instead will be sent to *all* engines for execution there. In this way, IPython makes it very natural to control your entire cluster from within the notebook environment:"
85 ]
85 ]
86 },
86 },
87 {
87 {
88 "cell_type": "code",
88 "cell_type": "code",
89 "collapsed": false,
89 "collapsed": false,
90 "input": [
90 "input": [
91 "%%px\n",
91 "%%px\n",
92 "# MPI initialization, library imports and sanity checks on all engines\n",
92 "# MPI initialization, library imports and sanity checks on all engines\n",
93 "from mpi4py import MPI\n",
93 "from mpi4py import MPI\n",
94 "# Load data publication API so engines can send data to notebook client\n",
94 "# Load data publication API so engines can send data to notebook client\n",
95 "from IPython.kernel.zmq.datapub import publish_data\n",
95 "from IPython.kernel.zmq.datapub import publish_data\n",
96 "import numpy as np\n",
96 "import numpy as np\n",
97 "import time\n",
97 "import time\n",
98 "\n",
98 "\n",
99 "mpi = MPI.COMM_WORLD\n",
99 "mpi = MPI.COMM_WORLD\n",
100 "bcast = mpi.bcast\n",
100 "bcast = mpi.bcast\n",
101 "barrier = mpi.barrier\n",
101 "barrier = mpi.barrier\n",
102 "rank = mpi.rank\n",
102 "rank = mpi.rank\n",
103 "print \"MPI rank: %i/%i\" % (mpi.rank,mpi.size)"
103 "print \"MPI rank: %i/%i\" % (mpi.rank,mpi.size)"
104 ],
104 ],
105 "language": "python",
105 "language": "python",
106 "metadata": {},
106 "metadata": {},
107 "outputs": [
107 "outputs": [
108 {
108 {
109 "output_type": "stream",
109 "output_type": "stream",
110 "stream": "stdout",
110 "stream": "stdout",
111 "text": [
111 "text": [
112 "[stdout:0] MPI rank: 2/4\n",
112 "[stdout:0] MPI rank: 2/4\n",
113 "[stdout:1] MPI rank: 0/4\n",
113 "[stdout:1] MPI rank: 0/4\n",
114 "[stdout:2] MPI rank: 3/4\n",
114 "[stdout:2] MPI rank: 3/4\n",
115 "[stdout:3] MPI rank: 1/4\n"
115 "[stdout:3] MPI rank: 1/4\n"
116 ]
116 ]
117 }
117 }
118 ],
118 ],
119 "prompt_number": 3
119 "prompt_number": 3
120 },
120 },
121 {
121 {
122 "cell_type": "markdown",
122 "cell_type": "markdown",
123 "metadata": {},
123 "metadata": {},
124 "source": [
124 "source": [
125 "We write a utility that reorders a list according to the mpi ranks of the engines, since all gather operations will return data in engine id order, not in MPI rank order. We'll need this later on when we want to reassemble in IPython data structures coming from all the engines: IPython will collect the data ordered by engine ID, but our code creates data structures based on MPI rank, so we need to map from one indexing scheme to the other. This simple function does the job:"
125 "We write a utility that reorders a list according to the mpi ranks of the engines, since all gather operations will return data in engine id order, not in MPI rank order. We'll need this later on when we want to reassemble in IPython data structures coming from all the engines: IPython will collect the data ordered by engine ID, but our code creates data structures based on MPI rank, so we need to map from one indexing scheme to the other. This simple function does the job:"
126 ]
126 ]
127 },
127 },
128 {
128 {
129 "cell_type": "code",
129 "cell_type": "code",
130 "collapsed": false,
130 "collapsed": false,
131 "input": [
131 "input": [
132 "ranks = view['rank']\n",
132 "ranks = view['rank']\n",
133 "engine_mpi = np.argsort(ranks)\n",
133 "engine_mpi = np.argsort(ranks)\n",
134 "\n",
134 "\n",
135 "def mpi_order(seq):\n",
135 "def mpi_order(seq):\n",
136 " \"\"\"Return elements of a sequence ordered by MPI rank.\n",
136 " \"\"\"Return elements of a sequence ordered by MPI rank.\n",
137 "\n",
137 "\n",
138 " The input sequence is assumed to be ordered by engine ID.\"\"\"\n",
138 " The input sequence is assumed to be ordered by engine ID.\"\"\"\n",
139 " return [seq[x] for x in engine_mpi]"
139 " return [seq[x] for x in engine_mpi]"
140 ],
140 ],
141 "language": "python",
141 "language": "python",
142 "metadata": {},
142 "metadata": {},
143 "outputs": [],
143 "outputs": [],
144 "prompt_number": 4
144 "prompt_number": 4
145 },
145 },
146 {
146 {
147 "cell_type": "heading",
147 "cell_type": "heading",
148 "level": 2,
148 "level": 2,
149 "metadata": {},
149 "metadata": {},
150 "source": [
150 "source": [
151 "MPI simulation example"
151 "MPI simulation example"
152 ]
152 ]
153 },
153 },
154 {
154 {
155 "cell_type": "markdown",
155 "cell_type": "markdown",
156 "metadata": {},
156 "metadata": {},
157 "source": [
157 "source": [
158 "This is our 'simulation', a toy example that computes $\\sin(f(x^2+y^2))$ for a slowly increasing frequency $f$ over a gradually refined mesh. In a real-world example, there typically is a 'simulate' method that, afer setting up initial parameters, runs the entire computation. But having this simple example will be sufficient to see something that changes visually as the computation evolves and that is quick enough for us to test.\n",
158 "This is our 'simulation', a toy example that computes $\\sin(f(x^2+y^2))$ for a slowly increasing frequency $f$ over a gradually refined mesh. In a real-world example, there typically is a 'simulate' method that, afer setting up initial parameters, runs the entire computation. But having this simple example will be sufficient to see something that changes visually as the computation evolves and that is quick enough for us to test.\n",
159 "\n",
159 "\n",
160 "And while simple, this example has a realistic decomposition of the spatial domain in one array per MPI node that requires care in reordering the data for visualization, as would be needed in a real-world application (unless your code accumulates data in the rank 0 node that you can grab directly)."
160 "And while simple, this example has a realistic decomposition of the spatial domain in one array per MPI node that requires care in reordering the data for visualization, as would be needed in a real-world application (unless your code accumulates data in the rank 0 node that you can grab directly)."
161 ]
161 ]
162 },
162 },
163 {
163 {
164 "cell_type": "code",
164 "cell_type": "code",
165 "collapsed": false,
165 "collapsed": false,
166 "input": [
166 "input": [
167 "%%px\n",
167 "%%px\n",
168 "\n",
168 "\n",
169 "# Global flag in the namespace\n",
169 "# Global flag in the namespace\n",
170 "stop = False\n",
170 "stop = False\n",
171 "\n",
171 "\n",
172 "def simulation(nsteps=100, delay=0.1):\n",
172 "def simulation(nsteps=100, delay=0.1):\n",
173 " \"\"\"Toy simulation code, computes sin(f*(x**2+y**2)) for a slowly increasing f\n",
173 " \"\"\"Toy simulation code, computes sin(f*(x**2+y**2)) for a slowly increasing f\n",
174 " over an increasingly fine mesh.\n",
174 " over an increasingly fine mesh.\n",
175 "\n",
175 "\n",
176 " The purpose of this code is simply to illustrate the basic features of a typical\n",
176 " The purpose of this code is simply to illustrate the basic features of a typical\n",
177 " MPI code: spatial domain decomposition, a solution which is evolving in some \n",
177 " MPI code: spatial domain decomposition, a solution which is evolving in some \n",
178 " sense, and local per-node computation. In this case the nodes only communicate when \n",
178 " sense, and local per-node computation. In this case the nodes only communicate when \n",
179 " gathering results for publication.\"\"\"\n",
179 " gathering results for publication.\"\"\"\n",
180 " # Problem geometry\n",
180 " # Problem geometry\n",
181 " xmin, xmax = 0, np.pi\n",
181 " xmin, xmax = 0, np.pi\n",
182 " ymin, ymax = 0, 2*np.pi\n",
182 " ymin, ymax = 0, 2*np.pi\n",
183 " dy = (ymax-ymin)/mpi.size\n",
183 " dy = (ymax-ymin)/mpi.size\n",
184 "\n",
184 "\n",
185 " freqs = np.linspace(0.6, 1, nsteps)\n",
185 " freqs = np.linspace(0.6, 1, nsteps)\n",
186 " for j in range(nsteps):\n",
186 " for j in range(nsteps):\n",
187 " nx, ny = 2+j/4, 2+j/2/mpi.size\n",
187 " nx, ny = 2+j/4, 2+j/2/mpi.size\n",
188 " nyt = mpi.size*ny\n",
188 " nyt = mpi.size*ny\n",
189 " Xax = np.linspace(xmin, xmax, nx)\n",
189 " Xax = np.linspace(xmin, xmax, nx)\n",
190 " Yax = np.linspace(ymin+rank*dy, ymin+(rank+1)*dy, ny, endpoint=rank==mpi.size)\n",
190 " Yax = np.linspace(ymin+rank*dy, ymin+(rank+1)*dy, ny, endpoint=rank==mpi.size)\n",
191 " X, Y = np.meshgrid(Xax, Yax)\n",
191 " X, Y = np.meshgrid(Xax, Yax)\n",
192 " f = freqs[j]\n",
192 " f = freqs[j]\n",
193 " Z = np.cos(f*(X**2 + Y**2))\n",
193 " Z = np.cos(f*(X**2 + Y**2))\n",
194 " \n",
194 " \n",
195 " # We are now going to publish data to the clients. We take advantage of fast\n",
195 " # We are now going to publish data to the clients. We take advantage of fast\n",
196 " # MPI communications and gather the Z mesh at the rank 0 node in the Zcat variable:\n",
196 " # MPI communications and gather the Z mesh at the rank 0 node in the Zcat variable:\n",
197 " Zcat = mpi.gather(Z, root=0)\n",
197 " Zcat = mpi.gather(Z, root=0)\n",
198 " if mpi.rank == 0:\n",
198 " if mpi.rank == 0:\n",
199 " # Then we use numpy's concatenation to construct a single numpy array with the\n",
199 " # Then we use numpy's concatenation to construct a single numpy array with the\n",
200 " # full mesh that can be sent to the client for visualization:\n",
200 " # full mesh that can be sent to the client for visualization:\n",
201 " Zcat = np.concatenate(Zcat)\n",
201 " Zcat = np.concatenate(Zcat)\n",
202 " # We now can send a dict with the variables we want the client to have access to:\n",
202 " # We now can send a dict with the variables we want the client to have access to:\n",
203 " publish_data(dict(Z=Zcat, nx=nx, nyt=nyt, j=j, nsteps=nsteps))\n",
203 " publish_data(dict(Z=Zcat, nx=nx, nyt=nyt, j=j, nsteps=nsteps))\n",
204 " \n",
204 " \n",
205 " # We add a small delay to simulate that a real-world computation\n",
205 " # We add a small delay to simulate that a real-world computation\n",
206 " # would take much longer, and we ensure all nodes are synchronized\n",
206 " # would take much longer, and we ensure all nodes are synchronized\n",
207 " time.sleep(delay)\n",
207 " time.sleep(delay)\n",
208 " # The stop flag can be set remotely via IPython, allowing the simulation to be\n",
208 " # The stop flag can be set remotely via IPython, allowing the simulation to be\n",
209 " # cleanly stopped from the outside\n",
209 " # cleanly stopped from the outside\n",
210 " if stop:\n",
210 " if stop:\n",
211 " break"
211 " break"
212 ],
212 ],
213 "language": "python",
213 "language": "python",
214 "metadata": {},
214 "metadata": {},
215 "outputs": [],
215 "outputs": [],
216 "prompt_number": 5
216 "prompt_number": 5
217 },
217 },
218 {
218 {
219 "cell_type": "heading",
219 "cell_type": "heading",
220 "level": 2,
220 "level": 2,
221 "metadata": {},
221 "metadata": {},
222 "source": [
222 "source": [
223 "IPython tools to interactively monitor and plot the MPI results"
223 "IPython tools to interactively monitor and plot the MPI results"
224 ]
224 ]
225 },
225 },
226 {
226 {
227 "cell_type": "markdown",
227 "cell_type": "markdown",
228 "metadata": {},
228 "metadata": {},
229 "source": [
229 "source": [
230 "We now define a local (to this notebook) plotting function that fetches data from the engines' global namespace. Once it has retrieved the current state of the relevant variables, it produces and returns a figure:"
230 "We now define a local (to this notebook) plotting function that fetches data from the engines' global namespace. Once it has retrieved the current state of the relevant variables, it produces and returns a figure:"
231 ]
231 ]
232 },
232 },
233 {
233 {
234 "cell_type": "code",
234 "cell_type": "code",
235 "collapsed": false,
235 "collapsed": false,
236 "input": [
236 "input": [
237 "from IPython.display import display, clear_output\n",
237 "from IPython.display import display, clear_output\n",
238 "\n",
238 "\n",
239 "def plot_current_results(ar, in_place=True):\n",
239 "def plot_current_results(ar, in_place=True):\n",
240 " \"\"\"Makes a blocking call to retrieve remote data and displays the solution mesh\n",
240 " \"\"\"Makes a blocking call to retrieve remote data and displays the solution mesh\n",
241 " as a contour plot.\n",
241 " as a contour plot.\n",
242 " \n",
242 " \n",
243 " Parameters\n",
243 " Parameters\n",
244 " ----------\n",
244 " ----------\n",
245 " ar : async result object\n",
245 " ar : async result object\n",
246 "\n",
246 "\n",
247 " in_place : bool\n",
247 " in_place : bool\n",
248 " By default it calls clear_output so that new plots replace old ones. Set\n",
248 " By default it calls clear_output so that new plots replace old ones. Set\n",
249 " to False to allow keeping of all previous outputs.\n",
249 " to False to allow keeping of all previous outputs.\n",
250 " \"\"\"\n",
250 " \"\"\"\n",
251 " # Read data from MPI rank 0 engine\n",
251 " # Read data from MPI rank 0 engine\n",
252 " data = ar.data[engine_mpi[0]]\n",
252 " data = ar.data[engine_mpi[0]]\n",
253 " \n",
253 " \n",
254 " try:\n",
254 " try:\n",
255 " nx, nyt, j, nsteps = [data[k] for k in ['nx', 'nyt', 'j', 'nsteps']]\n",
255 " nx, nyt, j, nsteps = [data[k] for k in ['nx', 'nyt', 'j', 'nsteps']]\n",
256 " Z = data['Z']\n",
256 " Z = data['Z']\n",
257 " except KeyError:\n",
257 " except KeyError:\n",
258 " # This can happen if we read from the engines so quickly that the data \n",
258 " # This can happen if we read from the engines so quickly that the data \n",
259 " # hasn't arrived yet.\n",
259 " # hasn't arrived yet.\n",
260 " fig, ax = plt.subplots()\n",
260 " fig, ax = plt.subplots()\n",
261 " ax.plot([])\n",
261 " ax.plot([])\n",
262 " ax.set_title(\"No data yet\")\n",
262 " ax.set_title(\"No data yet\")\n",
263 " display(fig)\n",
263 " display(fig)\n",
264 " return fig\n",
264 " return fig\n",
265 " else:\n",
265 " else:\n",
266 " \n",
266 " \n",
267 " fig, ax = plt.subplots()\n",
267 " fig, ax = plt.subplots()\n",
268 " ax.contourf(Z)\n",
268 " ax.contourf(Z)\n",
269 " ax.set_title('Mesh: %i x %i, step %i/%i' % (nx, nyt, j+1, nsteps))\n",
269 " ax.set_title('Mesh: %i x %i, step %i/%i' % (nx, nyt, j+1, nsteps))\n",
270 " plt.axis('off')\n",
270 " plt.axis('off')\n",
271 " # We clear the notebook output before plotting this if in-place \n",
271 " # We clear the notebook output before plotting this if in-place \n",
272 " # plot updating is requested\n",
272 " # plot updating is requested\n",
273 " if in_place:\n",
273 " if in_place:\n",
274 " clear_output()\n",
274 " clear_output(wait=True)\n",
275 " display(fig)\n",
275 " display(fig)\n",
276 " \n",
276 " \n",
277 " return fig"
277 " return fig"
278 ],
278 ],
279 "language": "python",
279 "language": "python",
280 "metadata": {},
280 "metadata": {},
281 "outputs": [],
281 "outputs": [],
282 "prompt_number": 6
282 "prompt_number": 6
283 },
283 },
284 {
284 {
285 "cell_type": "markdown",
285 "cell_type": "markdown",
286 "metadata": {},
286 "metadata": {},
287 "source": [
287 "source": [
288 "Finally, this is a convenience wrapper around the plotting code so that we can interrupt monitoring at any point, and that will provide basic timing information:"
288 "Finally, this is a convenience wrapper around the plotting code so that we can interrupt monitoring at any point, and that will provide basic timing information:"
289 ]
289 ]
290 },
290 },
291 {
291 {
292 "cell_type": "code",
292 "cell_type": "code",
293 "collapsed": false,
293 "collapsed": false,
294 "input": [
294 "input": [
295 "def monitor_simulation(ar, refresh=5.0, plots_in_place=True):\n",
295 "def monitor_simulation(ar, refresh=5.0, plots_in_place=True):\n",
296 " \"\"\"Monitor the simulation progress and call plotting routine.\n",
296 " \"\"\"Monitor the simulation progress and call plotting routine.\n",
297 "\n",
297 "\n",
298 " Supress KeyboardInterrupt exception if interrupted, ensure that the last \n",
298 " Supress KeyboardInterrupt exception if interrupted, ensure that the last \n",
299 " figure is always displayed and provide basic timing and simulation status.\n",
299 " figure is always displayed and provide basic timing and simulation status.\n",
300 "\n",
300 "\n",
301 " Parameters\n",
301 " Parameters\n",
302 " ----------\n",
302 " ----------\n",
303 " ar : async result object\n",
303 " ar : async result object\n",
304 "\n",
304 "\n",
305 " refresh : float\n",
305 " refresh : float\n",
306 " Refresh interval between calls to retrieve and plot data. The default\n",
306 " Refresh interval between calls to retrieve and plot data. The default\n",
307 " is 5s, adjust depending on the desired refresh rate, but be aware that \n",
307 " is 5s, adjust depending on the desired refresh rate, but be aware that \n",
308 " very short intervals will start having a significant impact.\n",
308 " very short intervals will start having a significant impact.\n",
309 "\n",
309 "\n",
310 " plots_in_place : bool\n",
310 " plots_in_place : bool\n",
311 " If true, every new figure replaces the last one, producing a (slow)\n",
311 " If true, every new figure replaces the last one, producing a (slow)\n",
312 " animation effect in the notebook. If false, all frames are plotted\n",
312 " animation effect in the notebook. If false, all frames are plotted\n",
313 " in sequence and appended in the output area.\n",
313 " in sequence and appended in the output area.\n",
314 " \"\"\"\n",
314 " \"\"\"\n",
315 " import datetime as dt, time\n",
315 " import datetime as dt, time\n",
316 " \n",
316 " \n",
317 " if ar.ready():\n",
317 " if ar.ready():\n",
318 " plot_current_results(ar, in_place=plots_in_place)\n",
318 " plot_current_results(ar, in_place=plots_in_place)\n",
319 " plt.close('all')\n",
319 " plt.close('all')\n",
320 " print 'Simulation has already finished, no monitoring to do.'\n",
320 " print 'Simulation has already finished, no monitoring to do.'\n",
321 " return\n",
321 " return\n",
322 " \n",
322 " \n",
323 " t0 = dt.datetime.now()\n",
323 " t0 = dt.datetime.now()\n",
324 " fig = None\n",
324 " fig = None\n",
325 " try:\n",
325 " try:\n",
326 " while not ar.ready():\n",
326 " while not ar.ready():\n",
327 " fig = plot_current_results(ar, in_place=plots_in_place)\n",
327 " fig = plot_current_results(ar, in_place=plots_in_place)\n",
328 " plt.close('all') # prevent re-plot of old figures\n",
328 " plt.close('all') # prevent re-plot of old figures\n",
329 " time.sleep(refresh)\n",
329 " time.sleep(refresh)\n",
330 " except (KeyboardInterrupt, error.TimeoutError):\n",
330 " except (KeyboardInterrupt, error.TimeoutError):\n",
331 " msg = 'Monitoring interrupted, simulation is ongoing!'\n",
331 " msg = 'Monitoring interrupted, simulation is ongoing!'\n",
332 " else:\n",
332 " else:\n",
333 " msg = 'Simulation completed!'\n",
333 " msg = 'Simulation completed!'\n",
334 " tmon = dt.datetime.now() - t0\n",
334 " tmon = dt.datetime.now() - t0\n",
335 " if plots_in_place and fig is not None:\n",
335 " if plots_in_place and fig is not None:\n",
336 " clear_output()\n",
336 " clear_output(wait=True)\n",
337 " plt.close('all')\n",
337 " plt.close('all')\n",
338 " display(fig)\n",
338 " display(fig)\n",
339 " print msg\n",
339 " print msg\n",
340 " print 'Monitored for: %s.' % tmon"
340 " print 'Monitored for: %s.' % tmon"
341 ],
341 ],
342 "language": "python",
342 "language": "python",
343 "metadata": {},
343 "metadata": {},
344 "outputs": [],
344 "outputs": [],
345 "prompt_number": 7
345 "prompt_number": 7
346 },
346 },
347 {
347 {
348 "cell_type": "heading",
348 "cell_type": "heading",
349 "level": 2,
349 "level": 2,
350 "metadata": {},
350 "metadata": {},
351 "source": [
351 "source": [
352 "Interactive monitoring in the client of the published data"
352 "Interactive monitoring in the client of the published data"
353 ]
353 ]
354 },
354 },
355 {
355 {
356 "cell_type": "markdown",
356 "cell_type": "markdown",
357 "metadata": {},
357 "metadata": {},
358 "source": [
358 "source": [
359 "Now, we can monitor the published data. We submit the simulation for execution as an asynchronous task, and then monitor this task at any frequency we desire."
359 "Now, we can monitor the published data. We submit the simulation for execution as an asynchronous task, and then monitor this task at any frequency we desire."
360 ]
360 ]
361 },
361 },
362 {
362 {
363 "cell_type": "code",
363 "cell_type": "code",
364 "collapsed": false,
364 "collapsed": false,
365 "input": [
365 "input": [
366 "# Create the local client that controls our IPython cluster with MPI support\n",
366 "# Create the local client that controls our IPython cluster with MPI support\n",
367 "from IPython.parallel import Client\n",
367 "from IPython.parallel import Client\n",
368 "cluster = Client(profile=\"mpi\")\n",
368 "cluster = Client(profile=\"mpi\")\n",
369 "# We make a view that encompasses all the engines\n",
369 "# We make a view that encompasses all the engines\n",
370 "view = cluster[:]\n",
370 "view = cluster[:]\n",
371 "# And now we call on all available nodes our simulation routine,\n",
371 "# And now we call on all available nodes our simulation routine,\n",
372 "# as an asynchronous task\n",
372 "# as an asynchronous task\n",
373 "ar = view.apply_async(lambda : simulation(nsteps=10, delay=0.1))"
373 "ar = view.apply_async(lambda : simulation(nsteps=10, delay=0.1))"
374 ],
374 ],
375 "language": "python",
375 "language": "python",
376 "metadata": {},
376 "metadata": {},
377 "outputs": [],
377 "outputs": [],
378 "prompt_number": 8
378 "prompt_number": 8
379 },
379 },
380 {
380 {
381 "cell_type": "code",
381 "cell_type": "code",
382 "collapsed": false,
382 "collapsed": false,
383 "input": [
383 "input": [
384 "monitor_simulation(ar, refresh=1)"
384 "monitor_simulation(ar, refresh=1)"
385 ],
385 ],
386 "language": "python",
386 "language": "python",
387 "metadata": {},
387 "metadata": {},
388 "outputs": [
388 "outputs": [
389 {
389 {
390 "metadata": {},
390 "metadata": {},
391 "output_type": "display_data",
391 "output_type": "display_data",
392 "png": "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\n17/+Na6++uqYO3dudOvWLebNmxe33XZb3HvvvW0u//nPfz4OO+ywmDBhQqxcuTLWrl0bN9xwQ9x/\n//1x0UUXtS43ffr0GDZsWJtjNDc3x4EHHhjTp0+PDRs2xAsvvBBPP/107LPPPq3L9O/fP5588slY\ns2ZNrF+/Ps4666zo06dPnHzyyfHCCy/Ev//977jpppti9OjR8fLLL7eu9/7778cJJ5wQzz33XPzn\nP/+Jxx9/PMaOHRuTJ0+OPffcM+l7sqU33ngjFi9eHKeccso2l2vvaNdll10WkydPjqeffjo2bNgQ\nDz74YFx11VXR2NjYqfkA2yacoAsVCoU2jzp9/etfj1122aXd5Q4++OB44okn4u67744999wzRowY\nEXvssUfcddddEfHBEZMzzzwzTjzxxOjdu3dcc801cd9990W/fv1ax2tvPh/6xz/+0XprflsWL14c\nL7/8cgwYMGCra4Hmzp271fLr16+Pr33ta3H55Ze3PpdoyJAhcd1118U555wTr7/+epvbmT9/fgwe\nPDgOP/zwGDx4cNx3333x+OOPR0NDQ+syjz32WLt3n+28885xww03xK233hq77757fPGLX4wxY8bE\n5MmTW5e59NJL4/jjj49Pf/rT8eabb0ZdXV0sWrQoBg0aFCNHjoz+/fvH/Pnz45FHHom999679Xs1\nYcKEGDduXJx22mnRp0+fOO+88+KCCy6Ia6+9tt3v25a2/FnMmTMnxo8fv92jQ239txMRcdppp8UV\nV1zRGn/Tpk2LefPmxaGHHpo8JyBdIevoRQ4AJdTS0hJ9+/aNhQsXxoEHHthl27300kujubk5Lrvs\nslzHXbt2bWzatCn69OmT67hAcexc6gkAdMRf/vKXqK+v79JoKqb6+vpSTwHoAKfqgIpyyCGHbHbd\nEUBXEk4AiXbkkQNAdXCNEwBAotyucXqqg/8SGzk+ry0DABXtou0vkrvhnTtulNsRp46Ek2gCAHLX\nkQCrlHASTQBAyd3eufzp0ovDRRMAUMm65DlOggkAqAZFP+IkmgCAalHUcBJNAEA1KVo4iSYAoNoU\nJZxEEwBQjXK9OFwwAQDVLLcjTqIJAKh2PuQXACCRcAIASCScAAASCScAgETCCQAgkXACAEgknAAA\nEgknAIBEwgkAIJFwAgBIJJwAABIJJwCARMIJACCRcAIASCScAAASCScAgETCCQAgkXACAEgknAAA\n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\n",
392 "png": "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\n17/+Na6++uqYO3dudOvWLebNmxe33XZb3HvvvW0u//nPfz4OO+ywmDBhQqxcuTLWrl0bN9xwQ9x/\n//1x0UUXtS43ffr0GDZsWJtjNDc3x4EHHhjTp0+PDRs2xAsvvBBPP/107LPPPq3L9O/fP5588slY\ns2ZNrF+/Ps4666zo06dPnHzyyfHCCy/Ev//977jpppti9OjR8fLLL7eu9/7778cJJ5wQzz33XPzn\nP/+Jxx9/PMaOHRuTJ0+OPffcM+l7sqU33ngjFi9eHKeccso2l2vvaNdll10WkydPjqeffjo2bNgQ\nDz74YFx11VXR2NjYqfkA2yacoAsVCoU2jzp9/etfj1122aXd5Q4++OB44okn4u67744999wzRowY\nEXvssUfcddddEfHBEZMzzzwzTjzxxOjdu3dcc801cd9990W/fv1ax2tvPh/6xz/+0XprflsWL14c\nL7/8cgwYMGCra4Hmzp271fLr16+Pr33ta3H55Ze3PpdoyJAhcd1118U555wTr7/+epvbmT9/fgwe\nPDgOP/zwGDx4cNx3333x+OOPR0NDQ+syjz32WLt3n+28885xww03xK233hq77757fPGLX4wxY8bE\n5MmTW5e59NJL4/jjj49Pf/rT8eabb0ZdXV0sWrQoBg0aFCNHjoz+/fvH/Pnz45FHHom999679Xs1\nYcKEGDduXJx22mnRp0+fOO+88+KCCy6Ia6+9tt3v25a2/FnMmTMnxo8fv92jQ239txMRcdppp8UV\nV1zRGn/Tpk2LefPmxaGHHpo8JyBdIevoRQ4AJdTS0hJ9+/aNhQsXxoEHHthl27300kujubk5Lrvs\nslzHXbt2bWzatCn69OmT67hAcexc6gkAdMRf/vKXqK+v79JoKqb6+vpSTwHoAKfqgIpyyCGHbHbd\nEUBXEk4AiXbkkQNAdXCNEwBAotyucXqqg/8SGzk+ry0DABXtou0vkrvhnTtulNsRp46Ek2gCAHLX\nkQCrlHASTQBAyd3eufzp0ovDRRMAUMm65DlOggkAqAZFP+IkmgCAalHUcBJNAEA1KVo4iSYAoNoU\nJZxEEwBQjXK9OFwwAQDVLLcjTqIJAKh2PuQXACCRcAIASCScAAASCScAgETCCQAgkXACAEgknAAA\nEgknAIBEwgkAIJFwAgBIJJwAABIJJwCARMIJACCRcAIASCScAAASCScAgETCCQAgkXACAEgknAAA\n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\n",
393 "text": [
393 "text": [
394 "<matplotlib.figure.Figure at 0x10b00b350>"
394 "<matplotlib.figure.Figure at 0x10b00b350>"
395 ]
395 ]
396 },
396 },
397 {
397 {
398 "output_type": "stream",
398 "output_type": "stream",
399 "stream": "stdout",
399 "stream": "stdout",
400 "text": [
400 "text": [
401 "Simulation completed!\n",
401 "Simulation completed!\n",
402 "Monitored for: 0:00:01.229672.\n"
402 "Monitored for: 0:00:01.229672.\n"
403 ]
403 ]
404 }
404 }
405 ],
405 ],
406 "prompt_number": 9
406 "prompt_number": 9
407 }
407 }
408 ],
408 ],
409 "metadata": {}
409 "metadata": {}
410 }
410 }
411 ]
411 ]
412 } No newline at end of file
412 }
@@ -1,543 +1,543 b''
1 {
1 {
2 "metadata": {
2 "metadata": {
3 "name": ""
3 "name": ""
4 },
4 },
5 "nbformat": 3,
5 "nbformat": 3,
6 "nbformat_minor": 0,
6 "nbformat_minor": 0,
7 "worksheets": [
7 "worksheets": [
8 {
8 {
9 "cells": [
9 "cells": [
10 {
10 {
11 "cell_type": "heading",
11 "cell_type": "heading",
12 "level": 1,
12 "level": 1,
13 "metadata": {
13 "metadata": {
14 "slideshow": {
14 "slideshow": {
15 "slide_start": false
15 "slide_start": false
16 }
16 }
17 },
17 },
18 "source": [
18 "source": [
19 "Interactive monitoring of a parallel MPI simulation with the IPython Notebook"
19 "Interactive monitoring of a parallel MPI simulation with the IPython Notebook"
20 ]
20 ]
21 },
21 },
22 {
22 {
23 "cell_type": "code",
23 "cell_type": "code",
24 "collapsed": false,
24 "collapsed": false,
25 "input": [
25 "input": [
26 "%matplotlib inline\n",
26 "%matplotlib inline\n",
27 "import numpy as np\n",
27 "import numpy as np\n",
28 "import matplotlib.pyplot as plt\n",
28 "import matplotlib.pyplot as plt\n",
29 "\n",
29 "\n",
30 "from IPython.display import display\n",
30 "from IPython.display import display\n",
31 "from IPython.parallel import Client, error\n",
31 "from IPython.parallel import Client, error\n",
32 "\n",
32 "\n",
33 "cluster = Client(profile=\"mpi\")\n",
33 "cluster = Client(profile=\"mpi\")\n",
34 "view = cluster[:]\n",
34 "view = cluster[:]\n",
35 "view.block = True"
35 "view.block = True"
36 ],
36 ],
37 "language": "python",
37 "language": "python",
38 "metadata": {
38 "metadata": {
39 "slideshow": {
39 "slideshow": {
40 "slide_start": false
40 "slide_start": false
41 }
41 }
42 },
42 },
43 "outputs": [],
43 "outputs": [],
44 "prompt_number": 1
44 "prompt_number": 1
45 },
45 },
46 {
46 {
47 "cell_type": "code",
47 "cell_type": "code",
48 "collapsed": false,
48 "collapsed": false,
49 "input": [
49 "input": [
50 "cluster.ids"
50 "cluster.ids"
51 ],
51 ],
52 "language": "python",
52 "language": "python",
53 "metadata": {},
53 "metadata": {},
54 "outputs": [
54 "outputs": [
55 {
55 {
56 "metadata": {},
56 "metadata": {},
57 "output_type": "pyout",
57 "output_type": "pyout",
58 "prompt_number": 2,
58 "prompt_number": 2,
59 "text": [
59 "text": [
60 "[0, 1, 2, 3]"
60 "[0, 1, 2, 3]"
61 ]
61 ]
62 }
62 }
63 ],
63 ],
64 "prompt_number": 2
64 "prompt_number": 2
65 },
65 },
66 {
66 {
67 "cell_type": "markdown",
67 "cell_type": "markdown",
68 "metadata": {
68 "metadata": {
69 "slideshow": {
69 "slideshow": {
70 "slide_start": false
70 "slide_start": false
71 }
71 }
72 },
72 },
73 "source": [
73 "source": [
74 "Now, we load the MPI libraries into the engine namespaces, and do a simple printing of their MPI rank information to verify that all nodes are operational and they match our cluster's real capacity. \n",
74 "Now, we load the MPI libraries into the engine namespaces, and do a simple printing of their MPI rank information to verify that all nodes are operational and they match our cluster's real capacity. \n",
75 "\n",
75 "\n",
76 "Here, we are making use of IPython's special `%%px` cell magic, which marks the entire cell for parallel execution. This means that the code below will not run in this notebook's kernel, but instead will be sent to *all* engines for execution there. In this way, IPython makes it very natural to control your entire cluster from within the notebook environment:"
76 "Here, we are making use of IPython's special `%%px` cell magic, which marks the entire cell for parallel execution. This means that the code below will not run in this notebook's kernel, but instead will be sent to *all* engines for execution there. In this way, IPython makes it very natural to control your entire cluster from within the notebook environment:"
77 ]
77 ]
78 },
78 },
79 {
79 {
80 "cell_type": "code",
80 "cell_type": "code",
81 "collapsed": false,
81 "collapsed": false,
82 "input": [
82 "input": [
83 "%%px\n",
83 "%%px\n",
84 "# MPI initialization, library imports and sanity checks on all engines\n",
84 "# MPI initialization, library imports and sanity checks on all engines\n",
85 "from mpi4py import MPI\n",
85 "from mpi4py import MPI\n",
86 "import numpy as np\n",
86 "import numpy as np\n",
87 "import time\n",
87 "import time\n",
88 "\n",
88 "\n",
89 "mpi = MPI.COMM_WORLD\n",
89 "mpi = MPI.COMM_WORLD\n",
90 "bcast = mpi.bcast\n",
90 "bcast = mpi.bcast\n",
91 "barrier = mpi.barrier\n",
91 "barrier = mpi.barrier\n",
92 "rank = mpi.rank\n",
92 "rank = mpi.rank\n",
93 "print \"MPI rank: %i/%i\" % (mpi.rank,mpi.size)"
93 "print \"MPI rank: %i/%i\" % (mpi.rank,mpi.size)"
94 ],
94 ],
95 "language": "python",
95 "language": "python",
96 "metadata": {
96 "metadata": {
97 "slideshow": {
97 "slideshow": {
98 "slide_start": false
98 "slide_start": false
99 }
99 }
100 },
100 },
101 "outputs": [
101 "outputs": [
102 {
102 {
103 "output_type": "stream",
103 "output_type": "stream",
104 "stream": "stdout",
104 "stream": "stdout",
105 "text": [
105 "text": [
106 "[stdout:0] MPI rank: 3/4\n",
106 "[stdout:0] MPI rank: 3/4\n",
107 "[stdout:1] MPI rank: 2/4\n",
107 "[stdout:1] MPI rank: 2/4\n",
108 "[stdout:2] MPI rank: 0/4\n",
108 "[stdout:2] MPI rank: 0/4\n",
109 "[stdout:3] MPI rank: 1/4\n"
109 "[stdout:3] MPI rank: 1/4\n"
110 ]
110 ]
111 }
111 }
112 ],
112 ],
113 "prompt_number": 3
113 "prompt_number": 3
114 },
114 },
115 {
115 {
116 "cell_type": "markdown",
116 "cell_type": "markdown",
117 "metadata": {
117 "metadata": {
118 "slideshow": {
118 "slideshow": {
119 "slide_start": false
119 "slide_start": false
120 }
120 }
121 },
121 },
122 "source": [
122 "source": [
123 "We write a utility that reorders a list according to the mpi ranks of the engines, since all gather operations will return data in engine id order, not in MPI rank order. We'll need this later on when we want to reassemble in IPython data structures coming from all the engines: IPython will collect the data ordered by engine ID, but our code creates data structures based on MPI rank, so we need to map from one indexing scheme to the other. This simple function does the job:"
123 "We write a utility that reorders a list according to the mpi ranks of the engines, since all gather operations will return data in engine id order, not in MPI rank order. We'll need this later on when we want to reassemble in IPython data structures coming from all the engines: IPython will collect the data ordered by engine ID, but our code creates data structures based on MPI rank, so we need to map from one indexing scheme to the other. This simple function does the job:"
124 ]
124 ]
125 },
125 },
126 {
126 {
127 "cell_type": "code",
127 "cell_type": "code",
128 "collapsed": false,
128 "collapsed": false,
129 "input": [
129 "input": [
130 "ranks = view['rank']\n",
130 "ranks = view['rank']\n",
131 "rank_indices = np.argsort(ranks)\n",
131 "rank_indices = np.argsort(ranks)\n",
132 "\n",
132 "\n",
133 "def mpi_order(seq):\n",
133 "def mpi_order(seq):\n",
134 " \"\"\"Return elements of a sequence ordered by MPI rank.\n",
134 " \"\"\"Return elements of a sequence ordered by MPI rank.\n",
135 "\n",
135 "\n",
136 " The input sequence is assumed to be ordered by engine ID.\"\"\"\n",
136 " The input sequence is assumed to be ordered by engine ID.\"\"\"\n",
137 " return [seq[x] for x in rank_indices]"
137 " return [seq[x] for x in rank_indices]"
138 ],
138 ],
139 "language": "python",
139 "language": "python",
140 "metadata": {
140 "metadata": {
141 "slideshow": {
141 "slideshow": {
142 "slide_start": false
142 "slide_start": false
143 }
143 }
144 },
144 },
145 "outputs": [],
145 "outputs": [],
146 "prompt_number": 4
146 "prompt_number": 4
147 },
147 },
148 {
148 {
149 "cell_type": "heading",
149 "cell_type": "heading",
150 "level": 2,
150 "level": 2,
151 "metadata": {
151 "metadata": {
152 "slideshow": {
152 "slideshow": {
153 "slide_start": false
153 "slide_start": false
154 }
154 }
155 },
155 },
156 "source": [
156 "source": [
157 "MPI simulation example"
157 "MPI simulation example"
158 ]
158 ]
159 },
159 },
160 {
160 {
161 "cell_type": "markdown",
161 "cell_type": "markdown",
162 "metadata": {
162 "metadata": {
163 "slideshow": {
163 "slideshow": {
164 "slide_start": false
164 "slide_start": false
165 }
165 }
166 },
166 },
167 "source": [
167 "source": [
168 "This is our 'simulation', a toy example that computes $\\sin(f(x^2+y^2))$ for a slowly increasing frequency $f$ over a gradually refined mesh. In a real-world example, there typically is a 'simulate' method that, afer setting up initial parameters, runs the entire computation. But having this simple example will be sufficient to see something that changes visually as the computation evolves and that is quick enough for us to test.\n",
168 "This is our 'simulation', a toy example that computes $\\sin(f(x^2+y^2))$ for a slowly increasing frequency $f$ over a gradually refined mesh. In a real-world example, there typically is a 'simulate' method that, afer setting up initial parameters, runs the entire computation. But having this simple example will be sufficient to see something that changes visually as the computation evolves and that is quick enough for us to test.\n",
169 "\n",
169 "\n",
170 "And while simple, this example has a realistic decomposition of the spatial domain in one array per MPI node that requires care in reordering the data for visualization, as would be needed in a real-world application (unless your code accumulates data in the rank 0 node that you can grab directly)."
170 "And while simple, this example has a realistic decomposition of the spatial domain in one array per MPI node that requires care in reordering the data for visualization, as would be needed in a real-world application (unless your code accumulates data in the rank 0 node that you can grab directly)."
171 ]
171 ]
172 },
172 },
173 {
173 {
174 "cell_type": "code",
174 "cell_type": "code",
175 "collapsed": false,
175 "collapsed": false,
176 "input": [
176 "input": [
177 "%%px\n",
177 "%%px\n",
178 "\n",
178 "\n",
179 "stop = False\n",
179 "stop = False\n",
180 "nsteps = 100\n",
180 "nsteps = 100\n",
181 "delay = 0.1\n",
181 "delay = 0.1\n",
182 "\n",
182 "\n",
183 "xmin, xmax = 0, np.pi\n",
183 "xmin, xmax = 0, np.pi\n",
184 "ymin, ymax = 0, 2*np.pi\n",
184 "ymin, ymax = 0, 2*np.pi\n",
185 "dy = (ymax-ymin)/mpi.size\n",
185 "dy = (ymax-ymin)/mpi.size\n",
186 "\n",
186 "\n",
187 "def simulation():\n",
187 "def simulation():\n",
188 " \"\"\"Toy simulation code, computes sin(f*(x**2+y**2)) for a slowly increasing f\n",
188 " \"\"\"Toy simulation code, computes sin(f*(x**2+y**2)) for a slowly increasing f\n",
189 " over an increasingly fine mesh.\n",
189 " over an increasingly fine mesh.\n",
190 "\n",
190 "\n",
191 " The purpose of this code is simply to illustrate the basic features of a typical\n",
191 " The purpose of this code is simply to illustrate the basic features of a typical\n",
192 " MPI code: spatial domain decomposition, a solution which is evolving in some \n",
192 " MPI code: spatial domain decomposition, a solution which is evolving in some \n",
193 " sense, and local per-node computation. In this case the nodes don't really\n",
193 " sense, and local per-node computation. In this case the nodes don't really\n",
194 " communicate at all.\n",
194 " communicate at all.\n",
195 " \"\"\"\n",
195 " \"\"\"\n",
196 " # By making these few variables global, we allow the IPython client to access them\n",
196 " # By making these few variables global, we allow the IPython client to access them\n",
197 " # remotely for interactive introspection\n",
197 " # remotely for interactive introspection\n",
198 " global j, Z, nx, nyt\n",
198 " global j, Z, nx, nyt\n",
199 " freqs = np.linspace(0.6, 1, nsteps)\n",
199 " freqs = np.linspace(0.6, 1, nsteps)\n",
200 " for j in range(nsteps):\n",
200 " for j in range(nsteps):\n",
201 " nx, ny = 2+j/4, 2+j/2/mpi.size\n",
201 " nx, ny = 2+j/4, 2+j/2/mpi.size\n",
202 " nyt = mpi.size*ny\n",
202 " nyt = mpi.size*ny\n",
203 " Xax = np.linspace(xmin, xmax, nx)\n",
203 " Xax = np.linspace(xmin, xmax, nx)\n",
204 " Yax = np.linspace(ymin+rank*dy, ymin+(rank+1)*dy, ny, endpoint=rank==mpi.size)\n",
204 " Yax = np.linspace(ymin+rank*dy, ymin+(rank+1)*dy, ny, endpoint=rank==mpi.size)\n",
205 " X, Y = np.meshgrid(Xax, Yax)\n",
205 " X, Y = np.meshgrid(Xax, Yax)\n",
206 " f = freqs[j]\n",
206 " f = freqs[j]\n",
207 " Z = np.cos(f*(X**2 + Y**2))\n",
207 " Z = np.cos(f*(X**2 + Y**2))\n",
208 " # We add a small delay to simulate that a real-world computation\n",
208 " # We add a small delay to simulate that a real-world computation\n",
209 " # would take much longer, and we ensure all nodes are synchronized\n",
209 " # would take much longer, and we ensure all nodes are synchronized\n",
210 " time.sleep(delay)\n",
210 " time.sleep(delay)\n",
211 " # The stop flag can be set remotely via IPython, allowing the simulation to be\n",
211 " # The stop flag can be set remotely via IPython, allowing the simulation to be\n",
212 " # cleanly stopped from the outside\n",
212 " # cleanly stopped from the outside\n",
213 " if stop:\n",
213 " if stop:\n",
214 " break"
214 " break"
215 ],
215 ],
216 "language": "python",
216 "language": "python",
217 "metadata": {
217 "metadata": {
218 "slideshow": {
218 "slideshow": {
219 "slide_start": false
219 "slide_start": false
220 }
220 }
221 },
221 },
222 "outputs": [],
222 "outputs": [],
223 "prompt_number": 5
223 "prompt_number": 5
224 },
224 },
225 {
225 {
226 "cell_type": "heading",
226 "cell_type": "heading",
227 "level": 2,
227 "level": 2,
228 "metadata": {
228 "metadata": {
229 "slideshow": {
229 "slideshow": {
230 "slide_start": false
230 "slide_start": false
231 }
231 }
232 },
232 },
233 "source": [
233 "source": [
234 "IPython tools to interactively monitor and plot the MPI results"
234 "IPython tools to interactively monitor and plot the MPI results"
235 ]
235 ]
236 },
236 },
237 {
237 {
238 "cell_type": "markdown",
238 "cell_type": "markdown",
239 "metadata": {
239 "metadata": {
240 "slideshow": {
240 "slideshow": {
241 "slide_start": false
241 "slide_start": false
242 }
242 }
243 },
243 },
244 "source": [
244 "source": [
245 "We now define a local (to this notebook) plotting function that fetches data from the engines' global namespace. Once it has retrieved the current state of the relevant variables, it produces and returns a figure:"
245 "We now define a local (to this notebook) plotting function that fetches data from the engines' global namespace. Once it has retrieved the current state of the relevant variables, it produces and returns a figure:"
246 ]
246 ]
247 },
247 },
248 {
248 {
249 "cell_type": "code",
249 "cell_type": "code",
250 "collapsed": false,
250 "collapsed": false,
251 "input": [
251 "input": [
252 "from IPython.display import clear_output\n",
252 "from IPython.display import clear_output\n",
253 "\n",
253 "\n",
254 "def plot_current_results(in_place=True):\n",
254 "def plot_current_results(in_place=True):\n",
255 " \"\"\"Makes a blocking call to retrieve remote data and displays the solution mesh\n",
255 " \"\"\"Makes a blocking call to retrieve remote data and displays the solution mesh\n",
256 " as a contour plot.\n",
256 " as a contour plot.\n",
257 " \n",
257 " \n",
258 " Parameters\n",
258 " Parameters\n",
259 " ----------\n",
259 " ----------\n",
260 " in_place : bool\n",
260 " in_place : bool\n",
261 " By default it calls clear_output so that new plots replace old ones. Set\n",
261 " By default it calls clear_output so that new plots replace old ones. Set\n",
262 " to False to allow keeping of all previous outputs.\n",
262 " to False to allow keeping of all previous outputs.\n",
263 " \"\"\"\n",
263 " \"\"\"\n",
264 " \n",
264 " \n",
265 " # We make a blocking call to load the remote data from the simulation into simple named \n",
265 " # We make a blocking call to load the remote data from the simulation into simple named \n",
266 " # variables we can read from the engine namespaces\n",
266 " # variables we can read from the engine namespaces\n",
267 " #view.apply_sync(load_simulation_globals)\n",
267 " #view.apply_sync(load_simulation_globals)\n",
268 " # And now we can use the view to read these variables from all the engines. Then we\n",
268 " # And now we can use the view to read these variables from all the engines. Then we\n",
269 " # concatenate all of them into single arrays for local plotting\n",
269 " # concatenate all of them into single arrays for local plotting\n",
270 " try:\n",
270 " try:\n",
271 " Z = np.concatenate(mpi_order(view['Z']))\n",
271 " Z = np.concatenate(mpi_order(view['Z']))\n",
272 " except ValueError:\n",
272 " except ValueError:\n",
273 " print \"dimension mismatch in Z, not plotting\"\n",
273 " print \"dimension mismatch in Z, not plotting\"\n",
274 " ax = plt.gca()\n",
274 " ax = plt.gca()\n",
275 " return ax.figure\n",
275 " return ax.figure\n",
276 " \n",
276 " \n",
277 " nx, nyt, j, nsteps = view.pull(['nx', 'nyt', 'j', 'nsteps'], targets=0)\n",
277 " nx, nyt, j, nsteps = view.pull(['nx', 'nyt', 'j', 'nsteps'], targets=0)\n",
278 " fig, ax = plt.subplots()\n",
278 " fig, ax = plt.subplots()\n",
279 " ax.contourf(Z)\n",
279 " ax.contourf(Z)\n",
280 " ax.set_title('Mesh: %i x %i, step %i/%i' % (nx, nyt, j+1, nsteps))\n",
280 " ax.set_title('Mesh: %i x %i, step %i/%i' % (nx, nyt, j+1, nsteps))\n",
281 " plt.axis('off')\n",
281 " plt.axis('off')\n",
282 " # We clear the notebook output before plotting this if in-place plot updating is requested\n",
282 " # We clear the notebook output before plotting this if in-place plot updating is requested\n",
283 " if in_place:\n",
283 " if in_place:\n",
284 " clear_output()\n",
284 " clear_output(wait=True)\n",
285 " display(fig)\n",
285 " display(fig)\n",
286 " return fig"
286 " return fig"
287 ],
287 ],
288 "language": "python",
288 "language": "python",
289 "metadata": {
289 "metadata": {
290 "slideshow": {
290 "slideshow": {
291 "slide_start": false
291 "slide_start": false
292 }
292 }
293 },
293 },
294 "outputs": [],
294 "outputs": [],
295 "prompt_number": 6
295 "prompt_number": 6
296 },
296 },
297 {
297 {
298 "cell_type": "markdown",
298 "cell_type": "markdown",
299 "metadata": {
299 "metadata": {
300 "slideshow": {
300 "slideshow": {
301 "slide_start": false
301 "slide_start": false
302 }
302 }
303 },
303 },
304 "source": [
304 "source": [
305 "It will also be useful to be able to check whether the simulation is still alive or not. Below we will wrap the main simulation function into a thread to allow IPython to pull data from the engines, and we will call this object `simulation_thread`. So to check whether the code is still running, all we have to do is call the `is_alive` method on all of our engines and see whether any of them returns True:"
305 "It will also be useful to be able to check whether the simulation is still alive or not. Below we will wrap the main simulation function into a thread to allow IPython to pull data from the engines, and we will call this object `simulation_thread`. So to check whether the code is still running, all we have to do is call the `is_alive` method on all of our engines and see whether any of them returns True:"
306 ]
306 ]
307 },
307 },
308 {
308 {
309 "cell_type": "code",
309 "cell_type": "code",
310 "collapsed": false,
310 "collapsed": false,
311 "input": [
311 "input": [
312 "def simulation_alive():\n",
312 "def simulation_alive():\n",
313 " \"\"\"Return True if the simulation thread is still running on any engine.\n",
313 " \"\"\"Return True if the simulation thread is still running on any engine.\n",
314 " \"\"\"\n",
314 " \"\"\"\n",
315 " return any(view.apply_sync(lambda : simulation_thread.is_alive()))"
315 " return any(view.apply_sync(lambda : simulation_thread.is_alive()))"
316 ],
316 ],
317 "language": "python",
317 "language": "python",
318 "metadata": {
318 "metadata": {
319 "slideshow": {
319 "slideshow": {
320 "slide_start": false
320 "slide_start": false
321 }
321 }
322 },
322 },
323 "outputs": [],
323 "outputs": [],
324 "prompt_number": 7
324 "prompt_number": 7
325 },
325 },
326 {
326 {
327 "cell_type": "markdown",
327 "cell_type": "markdown",
328 "metadata": {
328 "metadata": {
329 "slideshow": {
329 "slideshow": {
330 "slide_start": false
330 "slide_start": false
331 }
331 }
332 },
332 },
333 "source": [
333 "source": [
334 "Finally, this is a convenience wrapper around the plotting code so that we can interrupt monitoring at any point, and that will provide basic timing information:"
334 "Finally, this is a convenience wrapper around the plotting code so that we can interrupt monitoring at any point, and that will provide basic timing information:"
335 ]
335 ]
336 },
336 },
337 {
337 {
338 "cell_type": "code",
338 "cell_type": "code",
339 "collapsed": false,
339 "collapsed": false,
340 "input": [
340 "input": [
341 "def monitor_simulation(refresh=5.0, plots_in_place=True):\n",
341 "def monitor_simulation(refresh=5.0, plots_in_place=True):\n",
342 " \"\"\"Monitor the simulation progress and call plotting routine.\n",
342 " \"\"\"Monitor the simulation progress and call plotting routine.\n",
343 "\n",
343 "\n",
344 " Supress KeyboardInterrupt exception if interrupted, ensure that the last \n",
344 " Supress KeyboardInterrupt exception if interrupted, ensure that the last \n",
345 " figure is always displayed and provide basic timing and simulation status.\n",
345 " figure is always displayed and provide basic timing and simulation status.\n",
346 "\n",
346 "\n",
347 " Parameters\n",
347 " Parameters\n",
348 " ----------\n",
348 " ----------\n",
349 " refresh : float\n",
349 " refresh : float\n",
350 " Refresh interval between calls to retrieve and plot data. The default\n",
350 " Refresh interval between calls to retrieve and plot data. The default\n",
351 " is 5s, adjust depending on the desired refresh rate, but be aware that \n",
351 " is 5s, adjust depending on the desired refresh rate, but be aware that \n",
352 " very short intervals will start having a significant impact.\n",
352 " very short intervals will start having a significant impact.\n",
353 "\n",
353 "\n",
354 " plots_in_place : bool\n",
354 " plots_in_place : bool\n",
355 " If true, every new figure replaces the last one, producing a (slow)\n",
355 " If true, every new figure replaces the last one, producing a (slow)\n",
356 " animation effect in the notebook. If false, all frames are plotted\n",
356 " animation effect in the notebook. If false, all frames are plotted\n",
357 " in sequence and appended in the output area.\n",
357 " in sequence and appended in the output area.\n",
358 " \"\"\"\n",
358 " \"\"\"\n",
359 " import datetime as dt, time\n",
359 " import datetime as dt, time\n",
360 " \n",
360 " \n",
361 " if not simulation_alive():\n",
361 " if not simulation_alive():\n",
362 " plot_current_results(in_place=plots_in_place)\n",
362 " plot_current_results(in_place=plots_in_place)\n",
363 " plt.close('all')\n",
363 " plt.close('all')\n",
364 " print 'Simulation has already finished, no monitoring to do.'\n",
364 " print 'Simulation has already finished, no monitoring to do.'\n",
365 " return\n",
365 " return\n",
366 " \n",
366 " \n",
367 " t0 = dt.datetime.now()\n",
367 " t0 = dt.datetime.now()\n",
368 " fig = None\n",
368 " fig = None\n",
369 " try:\n",
369 " try:\n",
370 " while simulation_alive():\n",
370 " while simulation_alive():\n",
371 " fig = plot_current_results(in_place=plots_in_place)\n",
371 " fig = plot_current_results(in_place=plots_in_place)\n",
372 " plt.close('all') # prevent re-plot of old figures\n",
372 " plt.close('all') # prevent re-plot of old figures\n",
373 " time.sleep(refresh) # so we don't hammer the server too fast\n",
373 " time.sleep(refresh) # so we don't hammer the server too fast\n",
374 " except (KeyboardInterrupt, error.TimeoutError):\n",
374 " except (KeyboardInterrupt, error.TimeoutError):\n",
375 " msg = 'Monitoring interrupted, simulation is ongoing!'\n",
375 " msg = 'Monitoring interrupted, simulation is ongoing!'\n",
376 " else:\n",
376 " else:\n",
377 " msg = 'Simulation completed!'\n",
377 " msg = 'Simulation completed!'\n",
378 " tmon = dt.datetime.now() - t0\n",
378 " tmon = dt.datetime.now() - t0\n",
379 " if plots_in_place and fig is not None:\n",
379 " if plots_in_place and fig is not None:\n",
380 " clear_output()\n",
380 " clear_output(wait=True)\n",
381 " plt.close('all')\n",
381 " plt.close('all')\n",
382 " display(fig)\n",
382 " display(fig)\n",
383 " print msg\n",
383 " print msg\n",
384 " print 'Monitored for: %s.' % tmon"
384 " print 'Monitored for: %s.' % tmon"
385 ],
385 ],
386 "language": "python",
386 "language": "python",
387 "metadata": {
387 "metadata": {
388 "slideshow": {
388 "slideshow": {
389 "slide_start": false
389 "slide_start": false
390 }
390 }
391 },
391 },
392 "outputs": [],
392 "outputs": [],
393 "prompt_number": 8
393 "prompt_number": 8
394 },
394 },
395 {
395 {
396 "cell_type": "heading",
396 "cell_type": "heading",
397 "level": 2,
397 "level": 2,
398 "metadata": {
398 "metadata": {
399 "slideshow": {
399 "slideshow": {
400 "slide_start": false
400 "slide_start": false
401 }
401 }
402 },
402 },
403 "source": [
403 "source": [
404 "Making a simulation object that can be monitored interactively"
404 "Making a simulation object that can be monitored interactively"
405 ]
405 ]
406 },
406 },
407 {
407 {
408 "cell_type": "code",
408 "cell_type": "code",
409 "collapsed": false,
409 "collapsed": false,
410 "input": [
410 "input": [
411 "%%px\n",
411 "%%px\n",
412 "from threading import Thread\n",
412 "from threading import Thread\n",
413 "stop = False\n",
413 "stop = False\n",
414 "nsteps = 100\n",
414 "nsteps = 100\n",
415 "delay=0.5\n",
415 "delay=0.5\n",
416 "# Create a thread wrapper for the simulation. The target must be an argument-less\n",
416 "# Create a thread wrapper for the simulation. The target must be an argument-less\n",
417 "# function so we wrap the call to 'simulation' in a simple lambda:\n",
417 "# function so we wrap the call to 'simulation' in a simple lambda:\n",
418 "simulation_thread = Thread(target = lambda : simulation())\n",
418 "simulation_thread = Thread(target = lambda : simulation())\n",
419 "# Now we actually start the simulation\n",
419 "# Now we actually start the simulation\n",
420 "simulation_thread.start()"
420 "simulation_thread.start()"
421 ],
421 ],
422 "language": "python",
422 "language": "python",
423 "metadata": {
423 "metadata": {
424 "slideshow": {
424 "slideshow": {
425 "slide_start": false
425 "slide_start": false
426 }
426 }
427 },
427 },
428 "outputs": [],
428 "outputs": [],
429 "prompt_number": 9
429 "prompt_number": 9
430 },
430 },
431 {
431 {
432 "cell_type": "code",
432 "cell_type": "code",
433 "collapsed": false,
433 "collapsed": false,
434 "input": [
434 "input": [
435 "monitor_simulation(refresh=1);"
435 "monitor_simulation(refresh=1);"
436 ],
436 ],
437 "language": "python",
437 "language": "python",
438 "metadata": {
438 "metadata": {
439 "slideshow": {
439 "slideshow": {
440 "slide_start": false
440 "slide_start": false
441 }
441 }
442 },
442 },
443 "outputs": [
443 "outputs": [
444 {
444 {
445 "metadata": {},
445 "metadata": {},
446 "output_type": "display_data",
446 "output_type": "display_data",
447 "png": "iVBORw0KGgoAAAANSUhEUgAAAlMAAAGKCAYAAAAomMSSAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztvXvwnkV5//9+UkNOQAgQKAxnQjgIhFA5lNgQUvyGgA0e\nIAK1lFP40VoYEAsjh2GnxVq0RenUEUJQUQtKZ9IhQGxEAgQDgi0QkEkrIVBAwikRwYSEQPb3R/g8\n+Tyf53Tvfe/huvZ+v2Yc5Xn23t1g9vq8Ptde927DWmtBCCGEEEJKMSz1BAghhBBCNEOZIoQQQgip\nAGWKEEIIIaQClClCCCGEkApQpgghhBBCKkCZIoQQQgipAGWKEGW88MIL2HvvvVNPgwhk06ZNqadA\nSC2hTBFSAWMMhg0bhm9+85td28yfPx/Dhg3D2WefHXFmxXj22Wdx8sknY9y4cdhll11w1lln4bXX\nXuvY9oknnsCIESPwq1/9yvs8pk2bhmHDhrX85w/+4A86tn3llVewzTbb4Cc/+Yn3ecRm3rx52G+/\n/TBixAhMnDgRt956a8v3GzduxD/+4z9izz33xMiRIzFp0iTcc889Xfu77rrr8NWvfrXlsx/96EcY\nOXIkFi9e3Nb+nXfewbnnnovx48dj3LhxOOWUU/Dqq6+2tFm1ahU++9nPYty4cRg/fjzOO+88rF27\ntsKfmpD8oEwRUpFRo0bhlltu6fr9vHnzMHr0aDQajYiz6s/bb7+N448/HrvtthtWrFiBJ554Au+/\n/z4+9alPdWx/0UUX4eyzz8bBBx/sfS6NRgPz5s3Dpk2bmv/54IMPOra97LLLMGXKFMycOdP7PGJy\n3XXX4brrrsPNN9+MNWvWYO7cubj22mvxr//6r802F110EebPn4/58+dj9erV+Pu//3ucc845uPvu\nu9v6s9billtuwcknn9z87Otf/zr+5m/+BiNHjuw4h1NPPRW///3v8fTTT2PlypXYddddccIJJzT/\n3b/33nuYMWMGdt99dzz//PN46qmn8Pbbb2P27Nme/20QohxLCCmNMcZ++tOftmPHjrWPPPJI2/cv\nvfSS3WqrreyZZ55pzzrrLC9jPv/883avvfaq3M+9995rp06d2vLZ+vXr7YgRI+yyZctaPr/tttvs\ntttua19//fXK43Zi2rRp9pZbbunbbunSpXb48OH2mWee8T6H+++/306bNs17v51477337NZbb22X\nLFnS8vmDDz5ot9tuO/v+++/b3/zmN3bYsGH2xRdfbGlz66232oMOOqitz5/+9Kd2ypQpzX/+/ve/\nb3fZZRf71FNP2b322sved999Le0feughu/POO9v169e3fH7wwQfb22+/3Vpr7Q9+8AN72GGHtXy/\nbt06O378+I5/3wmpK8xMEVKRMWPG4IwzzsC8efPavvvOd76D448/HnvssUeCmfXm+OOPx4MPPtjy\n2YgRIzBq1KiW2pu1a9fisssuwxVXXIHx48cHm4/tc7PVpk2bcNFFF+Hcc8/FQQcdFGweMXjjjTew\ndu1aTJ48ueXzww8/HL/73e/w6quv4oUXXsB2222H3Xffva3N//7v/7b1OXfuXJx//vnNf549ezYe\nfvhhHHLIIR3nsGjRIhx//PEYMWJEy+czZ85sbqEuWrQIJ510Usv3o0aNwrRp07LYZiXEF5QpQjww\nZ84c3HHHHS21JNZafO9732v5ATeYX/7yl5gyZQpGjRqF3XbbDddee23L1tY999yDQw89FKNHj8ah\nhx6K++67r+X5JUuW4GMf+xi23nprfOxjH8PDDz/c8v2RRx6JL37xi05/joHtowMPPLD52Ve/+lV8\n5CMfwcUXX1yoj9NPPx3Tpk1rytH69etxwAEH4Oabb+75nDEGo0ePxvjx43H55Zfj/fffb/n+O9/5\nDp599ln83d/9ncsfqYVHH30Uf/zHf4wxY8Zg4sSJ+NGPfgQA2GuvvTB9+nQ8+OCDGDZsGPbZZ5/m\nM88++yxmzpyJMWPGYKeddsIll1yCdevWNb+fNm0a7r77blx++eXYbbfdMHr0aEydOhWPPvpo13ns\nuOOOGD16NJ588smWz5944gkAwLbbbos99tgDb731Fl5++eW2NmPHjm357PXXX8eSJUvwuc99rvnZ\niBEjsNdee3Wdw8qVKzFp0qS2zydNmoTnnnuuZ5vDDjus2YYQQpkipBLWWjQaDUyePBkTJ07E7bff\n3vzu3nvvxYYNG/DJT36yLevyi1/8AqeddhquvPJKrF69GkuXLsXy5cvxhS98AQCwZs0anHrqqfin\nf/onvPXWWzj33HNbZOTNN9/EVVddhVtuuQWrVq3CKaecgtmzZ+O9995rttl///2dMmIvvfQS5syZ\ngy9/+cvNbMVrr72Gf/7nf8aIESMwYcIE7L777vjSl76Ed999t2s/c+fOxUsvvYTrr78eAHDNNddg\nv/32w5w5c7o+c/rpp+Oee+7Bm2++ifnz5+POO+/EBRdc0Px+w4YNuPrqq7H99tvj6KOPxi677IJz\nzz0Xa9asKfzns9Zi1qxZOP/887FmzRp85StfwY033ohNmzbhhRdewP33349jjz0WmzZtwsqVKwFs\nfnPy+OOPx5lnnolXX30VTz/9NBqNBj7zmc80+200GrjwwguxYcMGPPTQQ3jllVdw3nnnYebMmbjz\nzjs7zmWrrbbCZZddhjlz5uDhhx/GunXr8Mgjj2DOnDk4+uijsc0222C33XbDX/7lX2L27Nl4+umn\nsXbtWixatAiXXXYZZsyY0dLfd7/7XXzuc59ryzL14re//S223Xbbts+322675r/Xbm3Gjh3r9O+e\nkOxJucdIiHauueYa+/nPf95aa+2NN95ojzrqqOZ3p556qr3iiiustdZeeeWVLTVTRx99tL3//vtb\n+tqwYYPdeuut7erVq+2yZcvsmDFj7FtvvdU25vPPP28bjYZdvnx5y+e77rpr6VqiN9980x544IH2\nz/7sz1o+v/LKK+3w4cOtMcY+/vjjduHChfbwww+3J510Us/+Hn30UbvNNtvYW265xf7hH/6hfe21\n15zm86tf/coOHz7cPvfcc9Zaa2+++WbbaDTsRRddZB977DG7ePFi+6d/+qf2sMMOsxs3bizU51tv\nvWWHDRtmn3322Y7fd6qZOu200+x3v/vdtrb77bef/e///m9rrbXHHnusPf3009va3HTTTXbixIld\n57Np0yZ7/fXX2913392OGDHC7r///rbRaNj58+c322zYsMFeccUVdqeddrKjRo2yEyZMsMOGDbOP\nP/54Sz/77befffrpp7uO1almaubMmfamm25qa3vPPffYAw880Fpr7YEHHmgXLVrU1uZb3/qWnTlz\nZtfxCKkbzEwR4onTTz8dzzzzDJ555hm8+eabuOuuu3Deeee1tdu4cSMeffRRTJ8+veUogJEjR2Ld\nunVYvnw5DjnkEEyfPh0TJkzA2WefjR/+8IfYsGFDs48dd9wRBxxwQEu/e++9N1avXu0877Vr1+Kk\nk07CjjvuiDvuuKPlu0WLFuGSSy7BNddcg8mTJ2PmzJn4z//8Tzz44IN45JFHuvZ55JFH4gtf+ALO\nO+88XHfdddhpp52c5vTRj34U++yzDx577LHmPE499VTccMMNOOKII3Dcccfh7rvvxm9/+1v8+7//\ne6E+x44diwsuuACHH344TjvtNMydOxdvv/12z2ceeughnHPOOW3HNjz33HNYvnw5gM2ZqdNPP73t\n2U996lN49tln8cYbb3Tsu9Fo4JJLLsGLL76I9evXY+rUqTjmmGPw6U9/utlmq622wle+8hW89tpr\nWLduHfbZZx+cdtppLbVWixcvxo477uj8luW4cePwu9/9ru3zt956CzvssEPfNttvv73TeITkDGWK\nEE9su+22mD17Nm6++WZ8//vfx8c//vHm4ZpDj0VoNBp4+umnW44CGDgOYMqUKWg0GliwYAHuuusu\n7LvvvviHf/gHTJ06tVlHNGbMmLbxhw8f3reIeygbN27EKaecgk2bNmHhwoVtr9C//fbb+PjHP97y\n2fjx43HAAQf0rJl59913sWDBAhxyyCH4t3/7N6c5DbDVVls1z5rqNI+RI0fiiCOOwIoVKwr3+a1v\nfQs///nPccQRR2DevHmYPHlyR1kYYNiwYbjrrrs6/v/053/+5812rv/eh/LUU0/he9/7Hv7lX/6l\na5u7774bS5cuxde//vWWz+fOndtzC7Ub++67L5YtW9b2+ZNPPol99923b5sJEyY4j0lIrlCmCKnA\nUEmaM2cOfvCDH2DevHldf8ANHz4cxxxzDH74wx+2fP7BBx/gmWeeaf7z+++/j6OPPhpXXXUVli1b\nhuXLl+Opp57yNndrLc466yz85je/waJFi7D11lu3tdl///1b5gQA69atw8qVK7Hnnnt27fvSSy/F\nRz/6UTz00EP49a9/ja997Wtd2z733HP49re/3fLZypUrsWLFChx11FFd57Fp0yY888wzPYush/L+\n++/j0EMPxaWXXorHHnsMw4cPbxb2NxqNthPEjz322Lb/nwC0CIa1Fj/+8Y/b2tx5552YOHFioTcg\nL774Ypx11lk4/PDDO36/ceNGfOlLX8IVV1yBXXfdtfn5G2+8gfvvvx+nnXZa3zGGcsIJJ+BnP/sZ\n1q9f3/JnWbhwYfMMrxNOOKHtTKt169bhgQceUH/OFyFeSbrJSIhyBtdMDXDwwQfb8ePH2/fee6/5\n2dCaqV/+8pd2zJgx9pvf/KZdvXq1ff755+3s2bPtjBkzrLXWPvDAA3a//fazTz75pH333XftHXfc\nYUePHm1fe+21rudMTZs2zT7wwAPNf/785z9vv/GNb3Sd+5e//GU7YcIEu2rVqq5tHn/8cbv99tvb\nH//4x/btt9+2K1assCeffLL9xCc+0fWZBQsW2F122cWuXr3aWmvtz3/+cztq1Cj72GOPdWz/i1/8\nwm611Vb2G9/4hn3nnXfs448/bidPnmwvv/zyZpuXX37Z7rDDDvbb3/62XbNmjX355Zft+eefbw8+\n+GC7YcOGZrtTTz3V/tVf/VXHcX7961/b3XbbzT7wwAN2/fr19r777rPbbLONffLJJ6211v7P//yP\n3Xnnne3rr79u/+///s9aa+2LL75ox44da6+44gr76quv2lWrVtkLL7zQHnTQQfaDDz6w1m6umdpz\nzz3txRdfbFeuXGnXrFljb731Vrv99tvbO++8s+u/pwHmz59vt9tuO/vGG290bXP99dfbffbZp+XP\naq21X/va1+xf//Vf9x2jU82UtZvrpmbPnm1XrVplV69ebS+88EI7efLk5p9t48aN9tBDD7UXXXSR\nXbNmjX3llVfsKaecYj/5yU/2HZOQOkGZIqQCxhj7F3/xFy2f3XDDDfbSSy9t+eyqq66yZ599dstn\n//Vf/2X/5E/+xI4cOdLuvPPO9sILL7TvvPNO8/trr73W7rHHHnbkyJH2j/7oj+y9995rrd1cgL73\n3nu3zWXatGn2wQcfbP7zEUccYS+55JKuc582bZodNmyYbTQabf+59dZbm+0efvhhe8wxx9iRI0fa\nnXbayV5yySUt8xzMK6+8YnfaaSd7zz33tHx+9dVX2wkTJtjf//73HZ+799577ZFHHmlHjRpl99xz\nT3vDDTe0tVm+fLmdMWOGHT16tB03bpw9++yzWwrbN23aZHfccUf7s5/9rOufee7cuXbixIl25MiR\n9qCDDmoeTjnAOeecY0eMGGEPP/zw5mcrVqywJ554oh0zZozdfvvt7ZlnnmlfffXVln+Pd911l/3i\nF79od955Zzty5Eg7ZcqUQodabtiwwU6YMKGn9L755pt23Lhx9j/+4z/avtt///2bMtiLbjL1zjvv\n2HPPPdfusMMOduzYsfaUU05pe1lg1apV9rOf/awdO3as3WGHHex5551n165d23dMQupEw9qKm/2E\nECKAxx9/HJ/4xCfwxhtvYNiweBUMxx13HK6++mpMnz492pgDrFixgrVLhAiANVOEkCy47777MGvW\nrKgilRqKFCEy+EjqCRBCiA/+9m//NvUUCCE1pT6/whFCCCGEBIA1U4QQQgghFYiyzbd0yFk8pH5M\n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\n",
447 "png": "iVBORw0KGgoAAAANSUhEUgAAAlMAAAGKCAYAAAAomMSSAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztvXvwnkV5//9+UkNOQAgQKAxnQjgIhFA5lNgQUvyGgA0e\nIAK1lFP40VoYEAsjh2GnxVq0RenUEUJQUQtKZ9IhQGxEAgQDgi0QkEkrIVBAwikRwYSEQPb3R/g8\n+Tyf53Tvfe/huvZ+v2Yc5Xn23t1g9vq8Ptde927DWmtBCCGEEEJKMSz1BAghhBBCNEOZIoQQQgip\nAGWKEEIIIaQClClCCCGEkApQpgghhBBCKkCZIoQQQgipAGWKEGW88MIL2HvvvVNPgwhk06ZNqadA\nSC2hTBFSAWMMhg0bhm9+85td28yfPx/Dhg3D2WefHXFmxXj22Wdx8sknY9y4cdhll11w1lln4bXX\nXuvY9oknnsCIESPwq1/9yvs8pk2bhmHDhrX85w/+4A86tn3llVewzTbb4Cc/+Yn3ecRm3rx52G+/\n/TBixAhMnDgRt956a8v3GzduxD/+4z9izz33xMiRIzFp0iTcc889Xfu77rrr8NWvfrXlsx/96EcY\nOXIkFi9e3Nb+nXfewbnnnovx48dj3LhxOOWUU/Dqq6+2tFm1ahU++9nPYty4cRg/fjzOO+88rF27\ntsKfmpD8oEwRUpFRo0bhlltu6fr9vHnzMHr0aDQajYiz6s/bb7+N448/HrvtthtWrFiBJ554Au+/\n/z4+9alPdWx/0UUX4eyzz8bBBx/sfS6NRgPz5s3Dpk2bmv/54IMPOra97LLLMGXKFMycOdP7PGJy\n3XXX4brrrsPNN9+MNWvWYO7cubj22mvxr//6r802F110EebPn4/58+dj9erV+Pu//3ucc845uPvu\nu9v6s9billtuwcknn9z87Otf/zr+5m/+BiNHjuw4h1NPPRW///3v8fTTT2PlypXYddddccIJJzT/\n3b/33nuYMWMGdt99dzz//PN46qmn8Pbbb2P27Nme/20QohxLCCmNMcZ++tOftmPHjrWPPPJI2/cv\nvfSS3WqrreyZZ55pzzrrLC9jPv/883avvfaq3M+9995rp06d2vLZ+vXr7YgRI+yyZctaPr/tttvs\ntttua19//fXK43Zi2rRp9pZbbunbbunSpXb48OH2mWee8T6H+++/306bNs17v51477337NZbb22X\nLFnS8vmDDz5ot9tuO/v+++/b3/zmN3bYsGH2xRdfbGlz66232oMOOqitz5/+9Kd2ypQpzX/+/ve/\nb3fZZRf71FNP2b322sved999Le0feughu/POO9v169e3fH7wwQfb22+/3Vpr7Q9+8AN72GGHtXy/\nbt06O378+I5/3wmpK8xMEVKRMWPG4IwzzsC8efPavvvOd76D448/HnvssUeCmfXm+OOPx4MPPtjy\n2YgRIzBq1KiW2pu1a9fisssuwxVXXIHx48cHm4/tc7PVpk2bcNFFF+Hcc8/FQQcdFGweMXjjjTew\ndu1aTJ48ueXzww8/HL/73e/w6quv4oUXXsB2222H3Xffva3N//7v/7b1OXfuXJx//vnNf549ezYe\nfvhhHHLIIR3nsGjRIhx//PEYMWJEy+czZ85sbqEuWrQIJ510Usv3o0aNwrRp07LYZiXEF5QpQjww\nZ84c3HHHHS21JNZafO9732v5ATeYX/7yl5gyZQpGjRqF3XbbDddee23L1tY999yDQw89FKNHj8ah\nhx6K++67r+X5JUuW4GMf+xi23nprfOxjH8PDDz/c8v2RRx6JL37xi05/joHtowMPPLD52Ve/+lV8\n5CMfwcUXX1yoj9NPPx3Tpk1rytH69etxwAEH4Oabb+75nDEGo0ePxvjx43H55Zfj/fffb/n+O9/5\nDp599ln83d/9ncsfqYVHH30Uf/zHf4wxY8Zg4sSJ+NGPfgQA2GuvvTB9+nQ8+OCDGDZsGPbZZ5/m\nM88++yxmzpyJMWPGYKeddsIll1yCdevWNb+fNm0a7r77blx++eXYbbfdMHr0aEydOhWPPvpo13ns\nuOOOGD16NJ588smWz5944gkAwLbbbos99tgDb731Fl5++eW2NmPHjm357PXXX8eSJUvwuc99rvnZ\niBEjsNdee3Wdw8qVKzFp0qS2zydNmoTnnnuuZ5vDDjus2YYQQpkipBLWWjQaDUyePBkTJ07E7bff\n3vzu3nvvxYYNG/DJT36yLevyi1/8AqeddhquvPJKrF69GkuXLsXy5cvxhS98AQCwZs0anHrqqfin\nf/onvPXWWzj33HNbZOTNN9/EVVddhVtuuQWrVq3CKaecgtmzZ+O9995rttl///2dMmIvvfQS5syZ\ngy9/+cvNbMVrr72Gf/7nf8aIESMwYcIE7L777vjSl76Ed999t2s/c+fOxUsvvYTrr78eAHDNNddg\nv/32w5w5c7o+c/rpp+Oee+7Bm2++ifnz5+POO+/EBRdc0Px+w4YNuPrqq7H99tvj6KOPxi677IJz\nzz0Xa9asKfzns9Zi1qxZOP/887FmzRp85StfwY033ohNmzbhhRdewP33349jjz0WmzZtwsqVKwFs\nfnPy+OOPx5lnnolXX30VTz/9NBqNBj7zmc80+200GrjwwguxYcMGPPTQQ3jllVdw3nnnYebMmbjz\nzjs7zmWrrbbCZZddhjlz5uDhhx/GunXr8Mgjj2DOnDk4+uijsc0222C33XbDX/7lX2L27Nl4+umn\nsXbtWixatAiXXXYZZsyY0dLfd7/7XXzuc59ryzL14re//S223Xbbts+322675r/Xbm3Gjh3r9O+e\nkOxJucdIiHauueYa+/nPf95aa+2NN95ojzrqqOZ3p556qr3iiiustdZeeeWVLTVTRx99tL3//vtb\n+tqwYYPdeuut7erVq+2yZcvsmDFj7FtvvdU25vPPP28bjYZdvnx5y+e77rpr6VqiN9980x544IH2\nz/7sz1o+v/LKK+3w4cOtMcY+/vjjduHChfbwww+3J510Us/+Hn30UbvNNtvYW265xf7hH/6hfe21\n15zm86tf/coOHz7cPvfcc9Zaa2+++WbbaDTsRRddZB977DG7ePFi+6d/+qf2sMMOsxs3bizU51tv\nvWWHDRtmn3322Y7fd6qZOu200+x3v/vdtrb77bef/e///m9rrbXHHnusPf3009va3HTTTXbixIld\n57Np0yZ7/fXX2913392OGDHC7r///rbRaNj58+c322zYsMFeccUVdqeddrKjRo2yEyZMsMOGDbOP\nP/54Sz/77befffrpp7uO1almaubMmfamm25qa3vPPffYAw880Fpr7YEHHmgXLVrU1uZb3/qWnTlz\nZtfxCKkbzEwR4onTTz8dzzzzDJ555hm8+eabuOuuu3Deeee1tdu4cSMeffRRTJ8+veUogJEjR2Ld\nunVYvnw5DjnkEEyfPh0TJkzA2WefjR/+8IfYsGFDs48dd9wRBxxwQEu/e++9N1avXu0877Vr1+Kk\nk07CjjvuiDvuuKPlu0WLFuGSSy7BNddcg8mTJ2PmzJn4z//8Tzz44IN45JFHuvZ55JFH4gtf+ALO\nO+88XHfdddhpp52c5vTRj34U++yzDx577LHmPE499VTccMMNOOKII3Dcccfh7rvvxm9/+1v8+7//\ne6E+x44diwsuuACHH344TjvtNMydOxdvv/12z2ceeughnHPOOW3HNjz33HNYvnw5gM2ZqdNPP73t\n2U996lN49tln8cYbb3Tsu9Fo4JJLLsGLL76I9evXY+rUqTjmmGPw6U9/utlmq622wle+8hW89tpr\nWLduHfbZZx+cdtppLbVWixcvxo477uj8luW4cePwu9/9ru3zt956CzvssEPfNttvv73TeITkDGWK\nEE9su+22mD17Nm6++WZ8//vfx8c//vHm4ZpDj0VoNBp4+umnW44CGDgOYMqUKWg0GliwYAHuuusu\n7LvvvviHf/gHTJ06tVlHNGbMmLbxhw8f3reIeygbN27EKaecgk2bNmHhwoVtr9C//fbb+PjHP97y\n2fjx43HAAQf0rJl59913sWDBAhxyyCH4t3/7N6c5DbDVVls1z5rqNI+RI0fiiCOOwIoVKwr3+a1v\nfQs///nPccQRR2DevHmYPHlyR1kYYNiwYbjrrrs6/v/053/+5812rv/eh/LUU0/he9/7Hv7lX/6l\na5u7774bS5cuxde//vWWz+fOndtzC7Ub++67L5YtW9b2+ZNPPol99923b5sJEyY4j0lIrlCmCKnA\nUEmaM2cOfvCDH2DevHldf8ANHz4cxxxzDH74wx+2fP7BBx/gmWeeaf7z+++/j6OPPhpXXXUVli1b\nhuXLl+Opp57yNndrLc466yz85je/waJFi7D11lu3tdl///1b5gQA69atw8qVK7Hnnnt27fvSSy/F\nRz/6UTz00EP49a9/ja997Wtd2z733HP49re/3fLZypUrsWLFChx11FFd57Fp0yY888wzPYush/L+\n++/j0EMPxaWXXorHHnsMw4cPbxb2NxqNthPEjz322Lb/nwC0CIa1Fj/+8Y/b2tx5552YOHFioTcg\nL774Ypx11lk4/PDDO36/ceNGfOlLX8IVV1yBXXfdtfn5G2+8gfvvvx+nnXZa3zGGcsIJJ+BnP/sZ\n1q9f3/JnWbhwYfMMrxNOOKHtTKt169bhgQceUH/OFyFeSbrJSIhyBtdMDXDwwQfb8ePH2/fee6/5\n2dCaqV/+8pd2zJgx9pvf/KZdvXq1ff755+3s2bPtjBkzrLXWPvDAA3a//fazTz75pH333XftHXfc\nYUePHm1fe+21rudMTZs2zT7wwAPNf/785z9vv/GNb3Sd+5e//GU7YcIEu2rVqq5tHn/8cbv99tvb\nH//4x/btt9+2K1assCeffLL9xCc+0fWZBQsW2F122cWuXr3aWmvtz3/+cztq1Cj72GOPdWz/i1/8\nwm611Vb2G9/4hn3nnXfs448/bidPnmwvv/zyZpuXX37Z7rDDDvbb3/62XbNmjX355Zft+eefbw8+\n+GC7YcOGZrtTTz3V/tVf/VXHcX7961/b3XbbzT7wwAN2/fr19r777rPbbLONffLJJ6211v7P//yP\n3Xnnne3rr79u/+///s9aa+2LL75ox44da6+44gr76quv2lWrVtkLL7zQHnTQQfaDDz6w1m6umdpz\nzz3txRdfbFeuXGnXrFljb731Vrv99tvbO++8s+u/pwHmz59vt9tuO/vGG290bXP99dfbffbZp+XP\naq21X/va1+xf//Vf9x2jU82UtZvrpmbPnm1XrVplV69ebS+88EI7efLk5p9t48aN9tBDD7UXXXSR\nXbNmjX3llVfsKaecYj/5yU/2HZOQOkGZIqQCxhj7F3/xFy2f3XDDDfbSSy9t+eyqq66yZ599dstn\n//Vf/2X/5E/+xI4cOdLuvPPO9sILL7TvvPNO8/trr73W7rHHHnbkyJH2j/7oj+y9995rrd1cgL73\n3nu3zWXatGn2wQcfbP7zEUccYS+55JKuc582bZodNmyYbTQabf+59dZbm+0efvhhe8wxx9iRI0fa\nnXbayV5yySUt8xzMK6+8YnfaaSd7zz33tHx+9dVX2wkTJtjf//73HZ+799577ZFHHmlHjRpl99xz\nT3vDDTe0tVm+fLmdMWOGHT16tB03bpw9++yzWwrbN23aZHfccUf7s5/9rOufee7cuXbixIl25MiR\n9qCDDmoeTjnAOeecY0eMGGEPP/zw5mcrVqywJ554oh0zZozdfvvt7ZlnnmlfffXVln+Pd911l/3i\nF79od955Zzty5Eg7ZcqUQodabtiwwU6YMKGn9L755pt23Lhx9j/+4z/avtt///2bMtiLbjL1zjvv\n2HPPPdfusMMOduzYsfaUU05pe1lg1apV9rOf/awdO3as3WGHHex5551n165d23dMQupEw9qKm/2E\nECKAxx9/HJ/4xCfwxhtvYNiweBUMxx13HK6++mpMnz492pgDrFixgrVLhAiANVOEkCy47777MGvW\nrKgilRqKFCEy+EjqCRBCiA/+9m//NvUUCCE1pT6/whFCCCGEBIA1U4QQQgghFYiyzbd0yFk8pH5M\n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\n",
448 "text": [
448 "text": [
449 "<matplotlib.figure.Figure at 0x1141a1450>"
449 "<matplotlib.figure.Figure at 0x1141a1450>"
450 ]
450 ]
451 },
451 },
452 {
452 {
453 "output_type": "stream",
453 "output_type": "stream",
454 "stream": "stdout",
454 "stream": "stdout",
455 "text": [
455 "text": [
456 "Simulation completed!\n",
456 "Simulation completed!\n",
457 "Monitored for: 0:00:50.653178.\n"
457 "Monitored for: 0:00:50.653178.\n"
458 ]
458 ]
459 }
459 }
460 ],
460 ],
461 "prompt_number": 10
461 "prompt_number": 10
462 },
462 },
463 {
463 {
464 "cell_type": "markdown",
464 "cell_type": "markdown",
465 "metadata": {
465 "metadata": {
466 "slideshow": {
466 "slideshow": {
467 "slide_start": false
467 "slide_start": false
468 }
468 }
469 },
469 },
470 "source": [
470 "source": [
471 "If you execute the following cell before the MPI code is finished running, it will stop the simulation at that point, which you can verify by calling the monitoring again:"
471 "If you execute the following cell before the MPI code is finished running, it will stop the simulation at that point, which you can verify by calling the monitoring again:"
472 ]
472 ]
473 },
473 },
474 {
474 {
475 "cell_type": "code",
475 "cell_type": "code",
476 "collapsed": false,
476 "collapsed": false,
477 "input": [
477 "input": [
478 "view['stop'] = True"
478 "view['stop'] = True"
479 ],
479 ],
480 "language": "python",
480 "language": "python",
481 "metadata": {
481 "metadata": {
482 "slideshow": {
482 "slideshow": {
483 "slide_start": false
483 "slide_start": false
484 }
484 }
485 },
485 },
486 "outputs": [],
486 "outputs": [],
487 "prompt_number": 11
487 "prompt_number": 11
488 },
488 },
489 {
489 {
490 "cell_type": "code",
490 "cell_type": "code",
491 "collapsed": false,
491 "collapsed": false,
492 "input": [
492 "input": [
493 "%%px --target 0\n",
493 "%%px --target 0\n",
494 "from IPython.parallel import bind_kernel; bind_kernel()\n",
494 "from IPython.parallel import bind_kernel; bind_kernel()\n",
495 "%connect_info"
495 "%connect_info"
496 ],
496 ],
497 "language": "python",
497 "language": "python",
498 "metadata": {},
498 "metadata": {},
499 "outputs": [
499 "outputs": [
500 {
500 {
501 "output_type": "stream",
501 "output_type": "stream",
502 "stream": "stdout",
502 "stream": "stdout",
503 "text": [
503 "text": [
504 "{\n",
504 "{\n",
505 " \"stdin_port\": 65310, \n",
505 " \"stdin_port\": 65310, \n",
506 " \"ip\": \"127.0.0.1\", \n",
506 " \"ip\": \"127.0.0.1\", \n",
507 " \"control_port\": 58188, \n",
507 " \"control_port\": 58188, \n",
508 " \"hb_port\": 58187, \n",
508 " \"hb_port\": 58187, \n",
509 " \"key\": \"e4f5cda8-faa8-48d3-a62c-dbde67db9827\", \n",
509 " \"key\": \"e4f5cda8-faa8-48d3-a62c-dbde67db9827\", \n",
510 " \"shell_port\": 65083, \n",
510 " \"shell_port\": 65083, \n",
511 " \"transport\": \"tcp\", \n",
511 " \"transport\": \"tcp\", \n",
512 " \"iopub_port\": 54934\n",
512 " \"iopub_port\": 54934\n",
513 "}\n",
513 "}\n",
514 "\n",
514 "\n",
515 "Paste the above JSON into a file, and connect with:\n",
515 "Paste the above JSON into a file, and connect with:\n",
516 " $> ipython <app> --existing <file>\n",
516 " $> ipython <app> --existing <file>\n",
517 "or, if you are local, you can connect with just:\n",
517 "or, if you are local, you can connect with just:\n",
518 " $> ipython <app> --existing kernel-64604.json \n",
518 " $> ipython <app> --existing kernel-64604.json \n",
519 "or even just:\n",
519 "or even just:\n",
520 " $> ipython <app> --existing \n",
520 " $> ipython <app> --existing \n",
521 "if this is the most recent IPython session you have started.\n"
521 "if this is the most recent IPython session you have started.\n"
522 ]
522 ]
523 }
523 }
524 ],
524 ],
525 "prompt_number": 12
525 "prompt_number": 12
526 },
526 },
527 {
527 {
528 "cell_type": "code",
528 "cell_type": "code",
529 "collapsed": false,
529 "collapsed": false,
530 "input": [
530 "input": [
531 "%%px --target 0\n",
531 "%%px --target 0\n",
532 "%qtconsole"
532 "%qtconsole"
533 ],
533 ],
534 "language": "python",
534 "language": "python",
535 "metadata": {},
535 "metadata": {},
536 "outputs": [],
536 "outputs": [],
537 "prompt_number": 13
537 "prompt_number": 13
538 }
538 }
539 ],
539 ],
540 "metadata": {}
540 "metadata": {}
541 }
541 }
542 ]
542 ]
543 } No newline at end of file
543 }
General Comments 0
You need to be logged in to leave comments. Login now