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