1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 """Module for interfacing with Grace (also known as Xmgrace, Xmgr, and ace)."""
26
27
28 from math import ceil, sqrt
29 from os import chmod
30 from os.path import expanduser
31 from stat import S_IRWXU, S_IRGRP, S_IROTH
32
33
34 from lib.errors import RelaxError
35 from lib.io import get_file_path, open_write_file
36
37
38
39
40 GRACE2IMAGES = """\
41 #!/usr/bin/env python3
42
43 ###############################################################################
44 # #
45 # Copyright (C) 2013 Troels E. Linnet #
46 # Copyright (C) 2013,2017 Edward d'Auvergne #
47 # #
48 # This file is part of the program relax (http://www.nmr-relax.com). #
49 # #
50 # This program is free software: you can redistribute it and/or modify #
51 # it under the terms of the GNU General Public License as published by #
52 # the Free Software Foundation, either version 3 of the License, or #
53 # (at your option) any later version. #
54 # #
55 # This program is distributed in the hope that it will be useful, #
56 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
57 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
58 # GNU General Public License for more details. #
59 # #
60 # You should have received a copy of the GNU General Public License #
61 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
62 # #
63 ###############################################################################
64
65 # Script docstring.
66 \"\"\"Scripted conversion of Grace *.agr graphs into vector or bitmap graphics files.
67
68 This script is used to batch convert the Grace *.agr files into graphics bitmap files using the Grace program itself. Therefore you will need to install grace on your system using one of the programs:
69 Xmgrace - http://plasma-gate.weizmann.ac.il/Grace/,
70 qtgrace - http://sourceforge.net/projects/qtgrace/,
71 gracegtk - http://sourceforge.net/projects/gracegtk/.
72 \"\"\"
73
74 # Python module imports.
75 from argparse import Action, ArgumentParser
76 import sys
77 from os import getcwd, listdir, path
78 import shlex, subprocess
79
80
81 # Define a callback class for handling the multiple input of PNG, EPS, SVG, etc.
82 class SplitFormats(Action):
83 def __call__(self, parser, namespace, values, option_string=None):
84 setattr(namespace, self.dest, values.split(','))
85
86
87 # Add script argument parsing.
88 parser = ArgumentParser(description="Scripted conversion of Grace *.agr graphs into vector or bitmap graphics files.")
89
90 # Add the script arguments.
91 parser.add_argument('types', action=SplitFormats, nargs='?', default='EPS', help="The different image types to convert to. E.g. execute script with: python %s PNG,EPS,SVG,JPG" % (sys.argv[0]))
92 parser.add_argument('-g', action='store_true', dest='relax_gui', default=False, help="Allow the script to be run through the relax GUI via the 'User-functions -> script' submenu, by only allowing for PNG conversions.")
93
94 # Parse the arguments.
95 args = parser.parse_args()
96
97 # If we run through the GUI we cannot pass input arguments so we fall back to EPS-only conversion.
98 if args.relax_gui:
99 args.types = ['EPS']
100
101 # For PNG conversion, several parameters can be passed to xmgrace. These can be altered later and the script rerun.
102 # The option for transparency is good for poster or insertion in color backgrounds. This ability depends on the Grace compilation.
103 if "PNG" in args.types:
104 pngpar = "png.par"
105 if not path.isfile(pngpar):
106 wpngpar = open(pngpar, "w")
107 wpngpar.write("DEVICE \\\"PNG\\\" FONT ANTIALIASING on\\n")
108 wpngpar.write("DEVICE \\\"PNG\\\" OP \\\"transparent:off\\\"\\n")
109 wpngpar.write("DEVICE \\\"PNG\\\" OP \\\"compression:9\\\"\\n")
110 wpngpar.close()
111
112 # Convert the different possible graphics type options into Grace format.
113 types = []
114 text = []
115 ext = []
116 param = []
117 for type in args.types:
118 # PNG bitmap graphics.
119 if type in ["PNG", ".PNG", "png", ".png"]:
120 types.append("PNG")
121 text.append("portable network graphics (PNG)")
122 ext.append("png")
123 param.append(pngpar)
124
125 # Encapsulated postscript vector graphics.
126 elif type in ["EPS", ".EPS", "eps", ".eps"]:
127 types.append("EPS")
128 text.append("encapsulated postscript (EPS)")
129 ext.append("eps")
130 param.append(None)
131
132 # JPG bitmap graphics.
133 elif type in ["JPG", ".JPG", "jpg", ".jpg", "JPEG", ".JPEG", "jpeg", ".jpeg"]:
134 types.append("JPEG")
135 text.append("JPEG")
136 ext.append("jpg")
137 param.append(None)
138
139 # Scalable vector graphics.
140 elif type in ["SVG", ".SVG", "svg", ".svg"]:
141 types.append("SVG")
142 text.append("scalable vector graphics (SVG)")
143 ext.append("svg")
144 param.append(None)
145
146 # Unknown graphic.
147 else:
148 print("Unknown graphic type '%s', skipping the format." % type)
149 continue
150
151 # Loop over all files in the current directory.
152 for filename in listdir(getcwd()):
153 # Skip non-Grace files.
154 if not filename.endswith(".agr"):
155 continue
156
157 # Get the filename without extension.
158 basename = path.splitext(filename)[0]
159
160 # Loop over each output format.
161 for i in range(len(types)):
162 im_args = r"xmgrace -hdevice %s -hardcopy" % types[i]
163 if param[i]:
164 im_args += r" -param %s" % param[i]
165 im_args += r" -printfile %s.%s %s" % (basename, ext[i], filename)
166
167 # Split the arguments the right way to call xmgrace.
168 im_args = shlex.split(im_args)
169
170 # Generate the graphic.
171 print("Converting '%s' into a %s graphic." % (filename, text[i]))
172 return_code = subprocess.call(im_args)
173 """
174
175
177 """Create the grace2images.py executable script.
178
179 @keyword dir: The directory to place the script into.
180 @type dir: str
181 """
182
183
184 dir = expanduser(dir)
185
186
187 print("\nCreating the Python \"grace to PNG/EPS/SVG...\" conversion script.")
188 file_name = "grace2images.py"
189 file_path = get_file_path(file_name=file_name, dir=dir)
190 file = open_write_file(file_name=file_name, dir=dir, force=True)
191
192
193 script_grace2images(file=file)
194
195
196 file.close()
197 chmod(file_path, S_IRWXU|S_IRGRP|S_IROTH)
198
199
201 """Write a python "grace to PNG/EPS/SVG..." conversion script..
202
203 The makes a conversion script to image types as PNG/EPS/SVG. The conversion is looping over a directory list of *.agr files, and making function calls to xmgrace. Successful conversion of images depends on the compilation of xmgrace. The input is a list of image types which is wanted, f.ex: PNG EPS SVG. PNG is default.
204
205 @keyword file: The file object to write the data to.
206 @type file: file object
207 """
208
209
210 file.write(GRACE2IMAGES)
211
212
213 -def write_xy_data(data, file=None, graph_type=None, norm_type='first', norm=None, autoscale=True):
214 """Write the data into the Grace xy-scatter plot.
215
216 The numerical data should be supplied as a 4 dimensional list or array object. The first dimension corresponds to the graphs, Gx. The second corresponds the sets of each graph, Sx. The third corresponds to the data series (i.e. each data point). The forth is a list of the information about each point, it is a list where the first element is the x value, the second is the y value, the third is the optional dx or dy error (either dx or dy dependent upon the graph_type arg), and the forth is the optional dy error when graph_type is xydxdy (the third position is then dx).
217
218
219 @param data: The 4D structure of numerical data to graph (see docstring).
220 @type data: list of lists of lists of float
221 @keyword file: The file object to write the data to.
222 @type file: file object
223 @keyword graph_type: The graph type which can be one of xy, xydy, xydx, or xydxdy.
224 @type graph_type: str
225 @keyword norm_type: The point to normalise to 1. This can be 'first' or 'last'.
226 @type norm_type: str
227 @keyword norm: The normalisation flag which if set to True will cause all graphs to be normalised to 1. The first dimension is the graph.
228 @type norm: None or list of bool
229 @keyword autoscale: A flag which if True will cause the world view of each graph to be autoscaled (by placing the Grace command "@autoscale" at the end of the file). If you have supplied a world view for the header or the tick spacing, this argument should be set to False to prevent that world view from being overwritten.
230 @type autoscale: bool
231 """
232
233
234 graph_num = len(data)
235
236
237 if not norm:
238 norm = []
239 for gi in range(graph_num):
240 norm.append(False)
241
242
243 comment_col = 2
244 if graph_type in ['xydx', 'xydy']:
245 comment_col = 3
246 elif graph_type == 'xydxdy':
247 comment_col = 4
248
249
250 for gi in range(graph_num):
251
252 for si in range(len(data[gi])):
253
254 file.write("@target G%s.S%s\n" % (gi, si))
255 file.write("@type %s\n" % graph_type)
256
257
258 if len(data[gi][si]) == 0:
259 file.write("&\n")
260 continue
261
262
263 norm_fact = 1.0
264 if norm[gi]:
265 if norm_type == 'first':
266 norm_fact = data[gi][si][0][1]
267 elif norm_type == 'last':
268 norm_fact = data[gi][si][-1][1]
269 else:
270 raise RelaxError("The normalisation type '%s' must be one of ['first', 'last']." % norm_fact)
271
272
273 for point in data[gi][si]:
274
275 if point[0] == None or point[1] == None:
276 continue
277
278
279 file.write("%30.15f %30.15f" % (point[0], point[1]/norm_fact))
280
281
282 if graph_type in ['xydx', 'xydy', 'xydxdy']:
283
284 if len(point) < 3:
285 error = None
286 else:
287 error = point[2]
288
289 if error == None:
290 error = 0.0
291
292
293 file.write(" %30.15f" % (error/norm_fact))
294
295
296 if graph_type == 'xydxdy':
297
298 error = point[3]
299 if error == None:
300 error = 0.0
301
302
303 file.write(" %30.15f" % (error/norm_fact))
304
305
306 try:
307 file.write(" \"%s\"" % point[comment_col])
308 except IndexError:
309 pass
310
311
312 file.write("\n")
313
314
315 file.write("&\n")
316
317
318 if autoscale:
319 for i in range(graph_num):
320 file.write("@with g%i\n" % i)
321 file.write("@autoscale\n")
322
323
324 if len(data) > 1:
325 row_num = int(round(sqrt(len(data))))
326 col_num = int(ceil(sqrt(len(data))))
327 file.write("@arrange(%i, %i, .1, .1, .1, OFF, OFF, OFF)\n" % (row_num, col_num))
328
329
331 """Write the grace header for xy-scatter plots.
332
333 Many of these keyword arguments should be supplied in a [X, Y] list format, where the first element corresponds to the X data, and the second the Y data. Defaults will be used for any non-supplied args (or lists with elements set to None).
334
335
336 @keyword file: The file object to write the data to.
337 @type file: file object
338 @keyword paper_size: The paper size, i.e. 'A4'. If set to None, this will default to letter size.
339 @type paper_size: str
340 @keyword title: The title of the graph.
341 @type title: None or str
342 @keyword subtitle: The sub-title of the graph.
343 @type subtitle: None or str
344 @keyword world: The Grace plot default zoom. This consists of a list of the X-axis minimum, Y-axis minimum, X-axis maximum, and Y-axis maximum values. Each graph should supply its own world view.
345 @type world: Nor or list of list of numbers
346 @keyword view: List of 4 coordinates defining the graph view port.
347 @type view: None or list of float
348 @keyword graph_num: The total number of graphs.
349 @type graph_num: int
350 @keyword sets: The number of data sets in each graph.
351 @type sets: list of int
352 @keyword set_names: The names associated with each graph data set Gx.Sy. For example this can be a list of spin identification strings. The first dimension is the graph, the second is the set.
353 @type set_names: None or list of list of str
354 @keyword set_colours: The colours for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
355 @type set_colours: None or list of list of int
356 @keyword x_axis_type_zero: The flags specifying if the X-axis should be placed at zero.
357 @type x_axis_type_zero: None or list of lists of bool
358 @keyword y_axis_type_zero: The flags specifying if the Y-axis should be placed at zero.
359 @type y_axis_type_zero: None or list of lists of bool
360 @keyword symbols: The symbol style for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
361 @type symbols: None or list of list of int
362 @keyword symbol_sizes: The symbol size for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
363 @type symbol_sizes: None or list of list of int
364 @keyword symbol_fill: The symbol file style for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
365 @type symbol_fill: None or list of list of int
366 @keyword linestyle: The line style for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
367 @type linestyle: None or list of list of int
368 @keyword linetype: The line type for each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
369 @type linetype: None or list of list of int
370 @keyword linewidth: The line width for all elements of each graph data set Gx.Sy. The first dimension is the graph, the second is the set.
371 @type linewidth: None or list of float
372 @keyword data_type: The axis data category (in the [X, Y] list format).
373 @type data_type: None or list of list of str
374 @keyword seq_type: The sequence data type (in the [X, Y] list format). This is for molecular sequence specific data and can be one of 'res', 'spin', or 'mixed'.
375 @type seq_type: None or list of list of str
376 @keyword tick_major_spacing: The spacing between major ticks. This is in the [X, Y] list format whereby the first dimension corresponds to the graph number.
377 @type tick_major_spacing: None or list of list of numbers
378 @keyword tick_minor_count: The number of minor ticks between the major ticks. This is in the [X, Y] list format whereby the first dimension corresponds to the graph number.
379 @type tick_minor_count: None or list of list of int
380 @keyword axis_labels: The labels for the axes (in the [X, Y] list format). The first dimension is the graph.
381 @type axis_labels: None or list of list of str
382 @keyword legend: If True, the legend will be visible. The first dimension is the graph.
383 @type legend: list of bool
384 @keyword legend_pos: The position of the legend, e.g. [0.3, 0.8]. The first dimension is the graph.
385 @type legend_pos: None or list of list of float
386 @keyword legend_box_fill_pattern: The legend box fill. If set to 0, it will become transparent.
387 @type legend_box_fill_pattern: int
388 @keyword legend_char_size: The size of the legend box text.
389 @type legend_char_size: float
390 @keyword norm: The normalisation flag which if set to True will cause all graphs to be normalised to 1. The first dimension is the graph.
391 @type norm: list of bool
392 """
393
394
395 if sets == None:
396 sets = []
397 for gi in range(graph_num):
398 sets.append(1)
399 if x_axis_type_zero == None:
400 x_axis_type_zero = []
401 for gi in range(graph_num):
402 x_axis_type_zero.append(False)
403 if y_axis_type_zero == None:
404 y_axis_type_zero = []
405 for gi in range(graph_num):
406 y_axis_type_zero.append(False)
407 if linewidth == None:
408 linewidth = []
409 for gi in range(graph_num):
410 linewidth.append(0.5)
411 if norm == None:
412 norm = []
413 for gi in range(graph_num):
414 norm.append(False)
415 if legend == None:
416 legend = []
417 for gi in range(graph_num):
418 legend.append(True)
419 if not legend_box_fill_pattern:
420 legend_box_fill_pattern = []
421 for gi in range(graph_num):
422 legend_box_fill_pattern.append(1)
423 if not legend_char_size:
424 legend_char_size = []
425 for gi in range(graph_num):
426 legend_char_size.append(1.0)
427
428
429 if not data_type:
430 data_type = [None, None]
431 if not seq_type:
432 seq_type = [None, None]
433 if not axis_labels:
434 axis_labels = []
435 for gi in range(graph_num):
436 axis_labels.append([None, None])
437
438
439 file.write("@version 50121\n")
440
441
442 if paper_size == 'A4':
443 file.write("@page size 842, 595\n")
444
445
446 for gi in range(graph_num):
447
448 file.write("@with g%i\n" % gi)
449
450
451 if world:
452 file.write("@ world %s, %s, %s, %s\n" % (world[gi][0], world[gi][1], world[gi][2], world[gi][3]))
453
454
455 if not view:
456 view = [0.15, 0.15, 1.28, 0.85]
457 file.write("@ view %s, %s, %s, %s\n" % (view[0], view[1], view[2], view[3]))
458
459
460 if title:
461 file.write("@ title \"%s\"\n" % title)
462 if subtitle:
463 file.write("@ subtitle \"%s\"\n" % subtitle)
464
465
466 if x_axis_type_zero[gi]:
467 file.write("@ xaxis type zero true\n")
468 if y_axis_type_zero[gi]:
469 file.write("@ yaxis type zero true\n")
470
471
472 axes = ['x', 'y']
473 for i in range(2):
474
475 if data_type[i] == 'spin':
476
477 if seq_type[i] == 'res':
478
479 if not axis_labels[gi][i]:
480 axis_labels[gi][i] = "Residue number"
481
482
483 if seq_type[i] == 'spin':
484
485 if not axis_labels[gi][i]:
486 axis_labels[gi][i] = "Spin number"
487
488
489 if seq_type[i] == 'mixed':
490
491 if not axis_labels[gi][i]:
492 axis_labels[gi][i] = "Spin ID string"
493
494
495 if axis_labels[gi][i]:
496 file.write("@ %saxis label \"%s\"\n" % (axes[i], axis_labels[gi][i]))
497 file.write("@ %saxis label char size 1.00\n" % axes[i])
498 if tick_major_spacing != None:
499 file.write("@ %saxis tick major %s\n" % (axes[i], tick_major_spacing[gi][i]))
500 file.write("@ %saxis tick major size 0.50\n" % axes[i])
501 file.write("@ %saxis tick major linewidth %s\n" % (axes[i], linewidth[gi]))
502 if tick_minor_count != None:
503 file.write("@ %saxis tick minor ticks %s\n" % (axes[i], tick_minor_count[gi][i]))
504 file.write("@ %saxis tick minor linewidth %s\n" % (axes[i], linewidth[gi]))
505 file.write("@ %saxis tick minor size 0.25\n" % axes[i])
506 file.write("@ %saxis ticklabel char size 0.70\n" % axes[i])
507
508
509 if legend != None and legend[gi]:
510 file.write("@ legend on\n")
511 else:
512 file.write("@ legend off\n")
513 if legend_pos != None:
514 file.write("@ legend %s, %s\n" % (legend_pos[gi][0], legend_pos[gi][1]))
515 file.write("@ legend box fill pattern %s\n" % legend_box_fill_pattern[gi])
516 file.write("@ legend char size %s\n" % legend_char_size[gi])
517
518
519 file.write("@ frame linewidth %s\n" % linewidth[gi])
520
521
522 for i in range(sets[gi]):
523
524 if symbols:
525 file.write("@ s%i symbol %i\n" % (i, symbols[gi][i]))
526 else:
527
528 num = i % 10 + 1
529
530
531 file.write("@ s%i symbol %i\n" % (i, num))
532
533
534 if symbol_sizes:
535 file.write("@ s%i symbol size %s\n" % (i, symbol_sizes[gi][i]))
536 else:
537 file.write("@ s%i symbol size 0.45\n" % i)
538
539
540 if symbol_fill:
541 file.write("@ s%i symbol fill pattern %i\n" % (i, symbol_fill[gi][i]))
542
543
544 file.write("@ s%i symbol linewidth %s\n" % (i, linewidth[gi]))
545
546
547 if set_colours:
548 file.write("@ s%i symbol color %s\n" % (i, set_colours[gi][i]))
549 file.write("@ s%i symbol fill color %s\n" % (i, set_colours[gi][i]))
550
551
552 file.write("@ s%i errorbar size 0.5\n" % i)
553 file.write("@ s%i errorbar linewidth %s\n" % (i, linewidth[gi]))
554 file.write("@ s%i errorbar riser linewidth %s\n" % (i, linewidth[gi]))
555
556
557 if linestyle:
558 file.write("@ s%i line linestyle %s\n" % (i, linestyle[gi][i]))
559
560
561 if linetype:
562 file.write("@ s%i line type %s\n" % (i, linetype[gi][i]))
563
564
565 if set_colours:
566 file.write("@ s%i line color %s\n" % (i, set_colours[gi][i]))
567 file.write("@ s%i fill color %s\n" % (i, set_colours[gi][i]))
568 file.write("@ s%i avalue color %s\n" % (i, set_colours[gi][i]))
569 file.write("@ s%i errorbar color %s\n" % (i, set_colours[gi][i]))
570
571
572 if set_names and len(set_names) and len(set_names[gi]) and set_names[gi][i]:
573 file.write("@ s%i legend \"%s\"\n" % (i, set_names[gi][i]))
574