##// END OF EJS Templates
Add SVG output.
Stefan van der Walt -
Show More
@@ -1,303 +1,350 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 ===========
3 ===========
4 octavemagic
4 octavemagic
5 ===========
5 ===========
6
6
7 Magics for interacting with Octave via oct2py.
7 Magics for interacting with Octave via oct2py.
8
8
9 Usage
9 Usage
10 =====
10 =====
11
11
12 ``%octave``
12 ``%octave``
13
13
14 {OCTAVE_DOC}
14 {OCTAVE_DOC}
15
15
16 ``%octave_push``
16 ``%octave_push``
17
17
18 {OCTAVE_PUSH_DOC}
18 {OCTAVE_PUSH_DOC}
19
19
20 ``%octave_pull``
20 ``%octave_pull``
21
21
22 {OCTAVE_PULL_DOC}
22 {OCTAVE_PULL_DOC}
23
23
24 """
24 """
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Copyright (C) 2012 The IPython Development Team
27 # Copyright (C) 2012 The IPython Development Team
28 #
28 #
29 # Distributed under the terms of the BSD License. The full license is in
29 # Distributed under the terms of the BSD License. The full license is in
30 # the file COPYING, distributed as part of this software.
30 # the file COPYING, distributed as part of this software.
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 import tempfile
33 import tempfile
34 from glob import glob
34 from glob import glob
35 from shutil import rmtree
35 from shutil import rmtree
36
36
37 import numpy as np
37 import numpy as np
38 import oct2py
38 import oct2py
39 from xml.dom import minidom
39
40
40 from IPython.core.displaypub import publish_display_data
41 from IPython.core.displaypub import publish_display_data
41 from IPython.core.magic import (Magics, magics_class, line_magic,
42 from IPython.core.magic import (Magics, magics_class, line_magic,
42 line_cell_magic)
43 line_cell_magic)
43 from IPython.testing.skipdoctest import skip_doctest
44 from IPython.testing.skipdoctest import skip_doctest
44 from IPython.core.magic_arguments import (
45 from IPython.core.magic_arguments import (
45 argument, magic_arguments, parse_argstring
46 argument, magic_arguments, parse_argstring
46 )
47 )
47 from IPython.utils.py3compat import unicode_to_str
48 from IPython.utils.py3compat import unicode_to_str
48
49
49
50 class OctaveMagicError(oct2py.Oct2PyError):
50 class OctaveMagicError(oct2py.Oct2PyError):
51 pass
51 pass
52
52
53 _mimetypes = {'png' : 'image/png',
54 'svg' : 'image/svg+xml',
55 'jpg' : 'image/jpeg',
56 'jpeg': 'image/jpeg'}
53
57
54 @magics_class
58 @magics_class
55 class OctaveMagics(Magics):
59 class OctaveMagics(Magics):
56 """A set of magics useful for interactive work with Octave via oct2py.
60 """A set of magics useful for interactive work with Octave via oct2py.
57 """
61 """
58 def __init__(self, shell):
62 def __init__(self, shell):
59 """
63 """
60 Parameters
64 Parameters
61 ----------
65 ----------
62
63 shell : IPython shell
66 shell : IPython shell
64
67
65 """
68 """
66 super(OctaveMagics, self).__init__(shell)
69 super(OctaveMagics, self).__init__(shell)
67 self.oct = oct2py.Oct2Py()
70 self._oct = oct2py.Oct2Py()
71 self._plot_format = 'png'
72
73
74 def _fix_gnuplot_svg_size(self, image, size=None):
75 """
76 GnuPlot SVGs do not have height/width attributes. Set
77 these to be the same as the viewBox, so that the browser
78 scales the image correctly.
79
80 Parameters
81 ----------
82 image : str
83 SVG data.
84 size : tuple of int
85 Image width, height.
86
87 """
88 (svg,) = minidom.parseString(image).getElementsByTagName('svg')
89 viewbox = svg.getAttribute('viewBox').split(' ')
90
91 if size is not None:
92 width, height = size
93 else:
94 width, height = viewbox[2:]
95
96 svg.setAttribute('width', '%dpx' % width)
97 svg.setAttribute('height', '%dpx' % height)
98 return svg.toxml()
68
99
69
100
70 @skip_doctest
101 @skip_doctest
71 @line_magic
102 @line_magic
72 def octave_push(self, line):
103 def octave_push(self, line):
73 '''
104 '''
74 Line-level magic that pushes a variable to Octave.
105 Line-level magic that pushes a variable to Octave.
75
106
76 `line` should be made up of whitespace separated variable names in the
107 `line` should be made up of whitespace separated variable names in the
77 IPython namespace::
108 IPython namespace::
78
109
79 In [7]: import numpy as np
110 In [7]: import numpy as np
80
111
81 In [8]: X = np.arange(5)
112 In [8]: X = np.arange(5)
82
113
83 In [9]: X.mean()
114 In [9]: X.mean()
84 Out[9]: 2.0
115 Out[9]: 2.0
85
116
86 In [10]: %octave_push X
117 In [10]: %octave_push X
87
118
88 In [11]: %octave mean(X)
119 In [11]: %octave mean(X)
89 Out[11]: 2.0
120 Out[11]: 2.0
90
121
91 '''
122 '''
92 inputs = line.split(' ')
123 inputs = line.split(' ')
93 for input in inputs:
124 for input in inputs:
94 input = unicode_to_str(input)
125 input = unicode_to_str(input)
95 self.oct.put(input, self.shell.user_ns[input])
126 self._oct.put(input, self.shell.user_ns[input])
96
127
97
128
98 @skip_doctest
129 @skip_doctest
99 @line_magic
130 @line_magic
100 def octave_pull(self, line):
131 def octave_pull(self, line):
101 '''
132 '''
102 Line-level magic that pulls a variable from Octave.
133 Line-level magic that pulls a variable from Octave.
103
134
104 In [18]: _ = %octave x = [1 2; 3 4]; y = 'hello'
135 In [18]: _ = %octave x = [1 2; 3 4]; y = 'hello'
105
136
106 In [19]: %octave_pull x y
137 In [19]: %octave_pull x y
107
138
108 In [20]: x
139 In [20]: x
109 Out[20]:
140 Out[20]:
110 array([[ 1., 2.],
141 array([[ 1., 2.],
111 [ 3., 4.]])
142 [ 3., 4.]])
112
143
113 In [21]: y
144 In [21]: y
114 Out[21]: 'hello'
145 Out[21]: 'hello'
115
146
116 '''
147 '''
117 outputs = line.split(' ')
148 outputs = line.split(' ')
118 for output in outputs:
149 for output in outputs:
119 output = unicode_to_str(output)
150 output = unicode_to_str(output)
120 self.shell.push({output: self.oct.get(output)})
151 self.shell.push({output: self._oct.get(output)})
121
152
122
153
123 @skip_doctest
154 @skip_doctest
124 @magic_arguments()
155 @magic_arguments()
125 @argument(
156 @argument(
126 '-i', '--input', action='append',
157 '-i', '--input', action='append',
127 help='Names of input variables to be pushed to Octave. Multiple names '
158 help='Names of input variables to be pushed to Octave. Multiple names '
128 'can be passed, separated by commas with no whitespace.'
159 'can be passed, separated by commas with no whitespace.'
129 )
160 )
130 @argument(
161 @argument(
131 '-o', '--output', action='append',
162 '-o', '--output', action='append',
132 help='Names of variables to be pulled from Octave after executing cell '
163 help='Names of variables to be pulled from Octave after executing cell '
133 'body. Multiple names can be passed, separated by commas with no '
164 'body. Multiple names can be passed, separated by commas with no '
134 'whitespace.'
165 'whitespace.'
135 )
166 )
136 @argument(
167 @argument(
137 '-s', '--size', action='append',
168 '-s', '--size', action='append',
138 help='Pixel size of plots. Default is "-s 400,250".'
169 help='Pixel size of plots, "width,height". Default is "-s 400,250".'
170 )
171 @argument(
172 '-f', '--format', action='append',
173 help='Plot format (png, svg or jpg).'
139 )
174 )
175
140 @argument(
176 @argument(
141 'code',
177 'code',
142 nargs='*',
178 nargs='*',
143 )
179 )
144 @line_cell_magic
180 @line_cell_magic
145 def octave(self, line, cell=None):
181 def octave(self, line, cell=None):
146 '''
182 '''
147 Execute code in Octave, and pull some of the results back into the
183 Execute code in Octave, and pull some of the results back into the
148 Python namespace.
184 Python namespace.
149
185
150 In [9]: %octave X = [1 2; 3 4]; mean(X)
186 In [9]: %octave X = [1 2; 3 4]; mean(X)
151 Out[9]: array([[ 2., 3.]])
187 Out[9]: array([[ 2., 3.]])
152
188
153 As a cell, this will run a block of Octave code, without returning any
189 As a cell, this will run a block of Octave code, without returning any
154 value::
190 value::
155
191
156 In [10]: %%octave
192 In [10]: %%octave
157 ....: p = [-2, -1, 0, 1, 2]
193 ....: p = [-2, -1, 0, 1, 2]
158 ....: polyout(p, 'x')
194 ....: polyout(p, 'x')
159
195
160 -2*x^4 - 1*x^3 + 0*x^2 + 1*x^1 + 2
196 -2*x^4 - 1*x^3 + 0*x^2 + 1*x^1 + 2
161
197
162 In the notebook, plots are published as the output of the cell, e.g.
198 In the notebook, plots are published as the output of the cell, e.g.
163
199
164 %octave plot([1 2 3], [4 5 6])
200 %octave plot([1 2 3], [4 5 6])
165
201
166 will create a line plot.
202 will create a line plot.
167
203
168 Objects can be passed back and forth between Octave and IPython via the
204 Objects can be passed back and forth between Octave and IPython via the
169 -i and -o flags in line::
205 -i and -o flags in line::
170
206
171 In [14]: Z = np.array([1, 4, 5, 10])
207 In [14]: Z = np.array([1, 4, 5, 10])
172
208
173 In [15]: %octave -i Z mean(Z)
209 In [15]: %octave -i Z mean(Z)
174 Out[15]: array([ 5.])
210 Out[15]: array([ 5.])
175
211
176
212
177 In [16]: %octave -o W W = Z * mean(Z)
213 In [16]: %octave -o W W = Z * mean(Z)
178 Out[16]: array([ 5., 20., 25., 50.])
214 Out[16]: array([ 5., 20., 25., 50.])
179
215
180 In [17]: W
216 In [17]: W
181 Out[17]: array([ 5., 20., 25., 50.])
217 Out[17]: array([ 5., 20., 25., 50.])
182
218
219 The size and format of output plots can be specified::
220
221 In [18]: %%octave -s 600,800 -f svg
222 ...: plot([1, 2, 3]);
223
183 '''
224 '''
184 args = parse_argstring(self.octave, line)
225 args = parse_argstring(self.octave, line)
185
226
186 # arguments 'code' in line are prepended to the cell lines
227 # arguments 'code' in line are prepended to the cell lines
187 if not cell:
228 if cell is None:
188 code = ''
229 code = ''
189 return_output = True
230 return_output = True
190 line_mode = True
231 line_mode = True
191 else:
232 else:
192 code = cell
233 code = cell
193 return_output = False
234 return_output = False
194 line_mode = False
235 line_mode = False
195
236
196 code = ' '.join(args.code) + code
237 code = ' '.join(args.code) + code
197
238
198 if args.input:
239 if args.input:
199 for input in ','.join(args.input).split(','):
240 for input in ','.join(args.input).split(','):
200 input = unicode_to_str(input)
241 input = unicode_to_str(input)
201 self.oct.put(input, self.shell.user_ns[input])
242 self._oct.put(input, self.shell.user_ns[input])
202
243
203 # generate plots in a temporary directory
244 # generate plots in a temporary directory
204 plot_dir = tempfile.mkdtemp()
245 plot_dir = tempfile.mkdtemp()
205 if args.size is not None:
246 if args.size is not None:
206 size = args.size[0]
247 size = args.size[0]
207 else:
248 else:
208 size = '400,240'
249 size = '400,240'
209
250
251 if args.format is not None:
252 plot_format = args.format[0]
253 else:
254 plot_format = 'png'
255
210 pre_call = '''
256 pre_call = '''
211 global __ipy_figures = [];
257 global __ipy_figures = [];
212 page_screen_output(0);
258 page_screen_output(0);
213
259
214 function fig_create(src, event)
260 function fig_create(src, event)
215 global __ipy_figures;
261 global __ipy_figures;
216 __ipy_figures(gcf()) = src;
262 __ipy_figures(gcf()) = src;
217 set(src, "visible", "off");
263 set(src, "visible", "off");
218 end
264 end
219
265
220 set(0, 'DefaultFigureCreateFcn', @fig_create);
266 set(0, 'DefaultFigureCreateFcn', @fig_create);
221
267
222 close all;
268 close all;
223 clear ans;
269 clear ans;
224 '''
270 '''
225
271
226 post_call = '''
272 post_call = '''
227
273
228 # Save output of the last execution
274 # Save output of the last execution
229 if exist("ans") == 1
275 if exist("ans") == 1
230 _ = ans;
276 _ = ans;
231 else
277 else
232 _ = nan;
278 _ = nan;
233 end
279 end
234
280
235 for f = __ipy_figures
281 for f = __ipy_figures
236 outfile = sprintf('%(plot_dir)s/__ipy_oct_fig_%%03d.png', f);
282 outfile = sprintf('%(plot_dir)s/__ipy_oct_fig_%%03d.png', f);
237 try
283 try
238 print(f, outfile, '-dpng', '-tight', '-S%(size)s');
284 print(f, outfile, '-d%(plot_format)s', '-tight', '-S%(size)s');
239 end
285 end
240 end
286 end
241
287
242 ''' % {'plot_dir': plot_dir, 'size': size}
288 ''' % {'plot_dir': plot_dir, 'size': size,
289 'plot_format': plot_format}
243
290
244 code = ' '.join((pre_call, code, post_call))
291 code = ' '.join((pre_call, code, post_call))
245 try:
292 try:
246 text_output = self.oct.run(code, verbose=False)
293 text_output = self._oct.run(code, verbose=False)
247 except (oct2py.Oct2PyError) as exception:
294 except (oct2py.Oct2PyError) as exception:
248 raise OctaveMagicError('Octave could not complete execution. '
295 raise OctaveMagicError('Octave could not complete execution. '
249 'Traceback (currently broken in oct2py): %s'
296 'Traceback (currently broken in oct2py): %s'
250 % exception.message)
297 % exception.message)
251
298
252 key = 'OctaveMagic.Octave'
299 key = 'OctaveMagic.Octave'
253 display_data = []
300 display_data = []
254
301
255 # Publish text output
302 # Publish text output
256 if text_output:
303 if text_output:
257 display_data.append((key, {'text/plain': text_output}))
304 display_data.append((key, {'text/plain': text_output}))
258
305
259 # Publish images
306 # Publish images
260 fmt = 'png'
261 mimetypes = {'png' : 'image/png',
262 'svg' : 'image/svg+xml'}
263 mime = mimetypes[fmt]
264
265 images = [open(imgfile, 'rb').read() for imgfile in \
307 images = [open(imgfile, 'rb').read() for imgfile in \
266 glob("%s/*.png" % plot_dir)]
308 glob("%s/*" % plot_dir)]
267 rmtree(plot_dir)
309 rmtree(plot_dir)
268
310
311 plot_mime_type = _mimetypes.get(plot_format, 'image/png')
312 width, height = [int(s) for s in size.split(',')]
269 for image in images:
313 for image in images:
270 display_data.append((key, {mime: image}))
314 if plot_format == 'svg':
315 image = self._fix_gnuplot_svg_size(image, size=(width, height))
316 display_data.append((key, {plot_mime_type: image}))
271
317
272 if args.output:
318 if args.output:
273 for output in ','.join(args.output).split(','):
319 for output in ','.join(args.output).split(','):
274 output = unicode_to_str(output)
320 output = unicode_to_str(output)
275 self.shell.push({output: self.oct.get(output)})
321 self.shell.push({output: self._oct.get(output)})
276
322
277 for tag, data in display_data:
323 for tag, data in display_data:
278 publish_display_data(tag, data)
324 publish_display_data(tag, data)
279
325
280 if return_output:
326 if return_output:
281 ans = self.oct.get('_')
327 ans = self._oct.get('_')
282
328
283 # Unfortunately, Octave doesn't have a "None" object,
329 # Unfortunately, Octave doesn't have a "None" object,
284 # so we can't return any NaN outputs
330 # so we can't return any NaN outputs
285 if np.isscalar(ans) and np.isnan(ans):
331 if np.isscalar(ans) and np.isnan(ans):
286 ans = None
332 ans = None
287
333
288 return ans
334 return ans
289
335
336
290 __doc__ = __doc__.format(
337 __doc__ = __doc__.format(
291 OCTAVE_DOC = ' '*8 + OctaveMagics.octave.__doc__,
338 OCTAVE_DOC = ' '*8 + OctaveMagics.octave.__doc__,
292 OCTAVE_PUSH_DOC = ' '*8 + OctaveMagics.octave_push.__doc__,
339 OCTAVE_PUSH_DOC = ' '*8 + OctaveMagics.octave_push.__doc__,
293 OCTAVE_PULL_DOC = ' '*8 + OctaveMagics.octave_pull.__doc__
340 OCTAVE_PULL_DOC = ' '*8 + OctaveMagics.octave_pull.__doc__
294 )
341 )
295
342
296
343
297 _loaded = False
344 _loaded = False
298 def load_ipython_extension(ip):
345 def load_ipython_extension(ip):
299 """Load the extension in IPython."""
346 """Load the extension in IPython."""
300 global _loaded
347 global _loaded
301 if not _loaded:
348 if not _loaded:
302 ip.register_magics(OctaveMagics)
349 ip.register_magics(OctaveMagics)
303 _loaded = True
350 _loaded = True
General Comments 0
You need to be logged in to leave comments. Login now