1
2 """
3 Application class that implements pyFoamTimelinePlot.py
4 """
5
6 import sys
7 from os import path
8 from optparse import OptionGroup
9
10 from .PyFoamApplication import PyFoamApplication
11 from PyFoam.RunDictionary.TimelineDirectory import TimelineDirectory
12 from PyFoam.Basics.SpreadsheetData import WrongDataSize
13 from PyFoam.ThirdParty.six import print_
14
15 from .PlotHelpers import cleanFilename
16
19 description="""\
20 Searches a directory for timelines that were generated by some
21 functionObject and generates the commands to gnuplot it. As an option
22 the data can be written to a CSV-file.
23 """
24
25 PyFoamApplication.__init__(self,
26 args=args,
27 description=description,
28 usage="%prog [options] <casedir>",
29 nr=1,
30 changeVersion=False,
31 interspersed=True)
32
34 data=OptionGroup(self.parser,
35 "Data",
36 "Select the data to plot")
37 self.parser.add_option_group(data)
38
39 data.add_option("--fields",
40 action="append",
41 default=None,
42 dest="fields",
43 help="The fields for which timelines should be plotted. All if unset")
44 data.add_option("--positions",
45 action="append",
46 default=None,
47 dest="positions",
48 help="The positions for which timelines should be plotted. Either strings or integers (then the corresponding column number will be used). All if unset")
49 data.add_option("--write-time",
50 default=None,
51 dest="writeTime",
52 help="If more than one time-subdirectory is stored select which one is used")
53 data.add_option("--directory-name",
54 action="store",
55 default="probes",
56 dest="dirName",
57 help="Alternate name for the directory with the samples (Default: %default)")
58 data.add_option("--reference-directory",
59 action="store",
60 default=None,
61 dest="reference",
62 help="A reference directory. If fitting timeline data is found there it is plotted alongside the regular data")
63 data.add_option("--reference-case",
64 action="store",
65 default=None,
66 dest="referenceCase",
67 help="A reference case where a directory with the same name is looked for. Mutual exclusive with --reference-directory")
68
69 time=OptionGroup(self.parser,
70 "Time",
71 "Select the times to plot")
72 self.parser.add_option_group(time)
73
74 time.add_option("--time",
75 action="append",
76 type="float",
77 default=None,
78 dest="time",
79 help="The times that are plotted (can be used more than once). Has to be specified for bars")
80 time.add_option("--min-time",
81 action="store",
82 type="float",
83 default=None,
84 dest="minTime",
85 help="The smallest time that should be used for lines")
86 time.add_option("--max-time",
87 action="store",
88 type="float",
89 default=None,
90 dest="maxTime",
91 help="The biggest time that should be used for lines")
92 time.add_option("--reference-time",
93 action="store_true",
94 default=False,
95 dest="referenceTime",
96 help="Use the time of the reference data for scaling instead of the regular data")
97
98
99 plot=OptionGroup(self.parser,
100 "Plot",
101 "How data should be plotted")
102 self.parser.add_option_group(plot)
103
104 plot.add_option("--basic-mode",
105 type="choice",
106 dest="basicMode",
107 default=None,
108 choices=["bars","lines"],
109 help="Whether 'bars' of the values at selected times or 'lines' over the whole timelines should be plotted")
110 vModes=["mag","x","y","z"]
111 plot.add_option("--vector-mode",
112 type="choice",
113 dest="vectorMode",
114 default="mag",
115 choices=vModes,
116 help="How vectors should be plotted. By magnitude or as a component. Possible values are "+str(vModes)+" Default: %default")
117 plot.add_option("--collect-lines-by",
118 type="choice",
119 dest="collectLines",
120 default="fields",
121 choices=["fields","positions"],
122 help="Collect lines for lineplotting either by 'fields' or 'positions'. Default: %default")
123
124 output=OptionGroup(self.parser,
125 "Output",
126 "Where data should be plotted to")
127 self.parser.add_option_group(output)
128
129 output.add_option("--gnuplot-file",
130 action="store",
131 dest="gnuplotFile",
132 default=None,
133 help="Write the necessary gnuplot commands to this file. Else they are written to the standard output")
134 output.add_option("--picture-destination",
135 action="store",
136 dest="pictureDest",
137 default=None,
138 help="Directory the pictures should be stored to")
139 output.add_option("--name-prefix",
140 action="store",
141 dest="namePrefix",
142 default=None,
143 help="Prefix to the picture-name")
144 output.add_option("--clean-filename",
145 action="store_true",
146 dest="cleanFilename",
147 default=False,
148 help="Clean filenames so that they can be used in HTML or Latex-documents")
149 output.add_option("--csv-file",
150 action="store",
151 dest="csvFile",
152 default=None,
153 help="Write the data to a CSV-file instead of the gnuplot-commands")
154 output.add_option("--excel-file",
155 action="store",
156 dest="excelFile",
157 default=None,
158 help="Write the data to a Excel-file instead of the gnuplot-commands")
159 output.add_option("--pandas-data",
160 action="store_true",
161 dest="pandasData",
162 default=False,
163 help="Pass the raw data in pandas-format")
164 output.add_option("--numpy-data",
165 action="store_true",
166 dest="numpyData",
167 default=False,
168 help="Pass the raw data in numpy-format")
169 output.add_option("--reference-prefix",
170 action="store",
171 dest="refprefix",
172 default="Reference",
173 help="Prefix that gets added to the reference lines. Default: %default")
174
175 data.add_option("--info",
176 action="store_true",
177 dest="info",
178 default=False,
179 help="Print info about the sampled data and exit")
180 output.add_option("--resample",
181 action="store_true",
182 dest="resample",
183 default=False,
184 help="Resample the reference value to the current x-axis (for CSV and Excel-output)")
185 output.add_option("--extend-data",
186 action="store_true",
187 dest="extendData",
188 default=False,
189 help="Extend the data range if it differs (for CSV and Excel-files)")
190 output.add_option("--silent",
191 action="store_true",
192 dest="silent",
193 default=False,
194 help="Don't write to screen (with the silent and the compare-options)")
195
196 numerics=OptionGroup(self.parser,
197 "Quantify",
198 "Metrics of the data and numerical comparisons")
199 self.parser.add_option_group(numerics)
200 numerics.add_option("--compare",
201 action="store_true",
202 dest="compare",
203 default=None,
204 help="Compare all data sets that are also in the reference data")
205 numerics.add_option("--metrics",
206 action="store_true",
207 dest="metrics",
208 default=None,
209 help="Print the metrics of the data sets")
210 numerics.add_option("--use-reference-for-comparison",
211 action="store_false",
212 dest="compareOnOriginal",
213 default=True,
214 help="Use the reference-data as the basis for the numerical comparison. Otherwise the original data will be used")
215
226
228
229 if self.opts.dirName[-1]==path.sep:
230 self.opts.dirName=self.opts.dirName[:-1]
231
232 usedDirName=self.opts.dirName.replace("/","_")
233
234 timelines=TimelineDirectory(self.parser.getArgs()[0],
235 dirName=self.opts.dirName,
236 writeTime=self.opts.writeTime)
237 reference=None
238 if self.opts.reference and self.opts.referenceCase:
239 self.error("Options --reference-directory and --reference-case are mutual exclusive")
240 if (self.opts.csvFile or self.opts.excelFile or self.opts.pandasData or self.opts.numpyData) and (self.opts.compare or self.opts.metrics):
241 self.error("Options --csv-file/excel-file/--pandas-data/--numpy-data and --compare/--metrics are mutual exclusive")
242
243 if self.opts.reference:
244 reference=TimelineDirectory(self.parser.getArgs()[0],
245 dirName=self.opts.reference,
246 writeTime=self.opts.writeTime)
247 elif self.opts.referenceCase:
248 reference=TimelineDirectory(self.opts.referenceCase,
249 dirName=self.opts.dirName,
250 writeTime=self.opts.writeTime)
251
252 if self.opts.info:
253 self.setData({'writeTimes' : timelines.writeTimes,
254 'usedTimes' : timelines.usedTime,
255 'fields' : timelines.values,
256 'positions' : timelines.positions(),
257 'timeRange' : timelines.timeRange()})
258
259 if not self.opts.silent:
260 print_("Write Times : ",timelines.writeTimes)
261 print_("Used Time : ",timelines.usedTime)
262 print_("Fields : ",timelines.values,end="")
263 if len(timelines.vectors)>0:
264 if not self.opts.silent:
265 print_(" Vectors: ",timelines.vectors)
266 self.setData({'vectors':timelines.vectors})
267 else:
268 if not self.opts.silent:
269 print_()
270 if not self.opts.silent:
271 print_("Positions : ",timelines.positions())
272 print_("Time range : ",timelines.timeRange())
273
274 if reference:
275 refData={'writeTimes' : reference.writeTimes,
276 'fields' : reference.values,
277 'positions' : reference.positions(),
278 'timeRange' : reference.timeRange()}
279
280 if not self.opts.silent:
281 print_("\nReference Data")
282 print_("Write Times : ",reference.writeTimes)
283 print_("Fields : ",reference.values,end="")
284 if len(reference.vectors)>0:
285 if not self.opts.silent:
286 print_(" Vectors: ",reference.vectors)
287 refData["vectors"]=reference.vectors
288 else:
289 if not self.opts.silent:
290 print_()
291 if not self.opts.silent:
292 print_("Positions : ",reference.positions())
293 print_("Time range : ",reference.timeRange())
294 self.setData({"reference":refData})
295
296 return 0
297
298 if self.opts.fields==None:
299 self.opts.fields=timelines.values
300 else:
301 for v in self.opts.fields:
302 if v not in timelines.values:
303 self.error("The requested value",v,"not in possible values",timelines.values)
304 if self.opts.positions==None:
305 self.opts.positions=timelines.positions()
306 else:
307 pos=self.opts.positions
308 self.opts.positions=[]
309 for p in pos:
310 try:
311 p=int(p)
312 if p<0 or p>=len(timelines.positions()):
313 self.error("Time index",p,"out of range for positons",timelines.positions())
314 else:
315 self.opts.positions.append(timelines.positions()[p])
316 except ValueError:
317 if p not in timelines.positions():
318 self.error("Position",p,"not in",timelines.positions())
319 else:
320 self.opts.positions.append(p)
321
322 if len(self.opts.positions)==0:
323 self.error("No valid positions")
324
325 result="set term png nocrop enhanced \n"
326
327 if self.opts.basicMode==None:
328 self.error("No mode selected. Do so with '--basic-mode'")
329 elif self.opts.basicMode=='bars':
330 if self.opts.time==None:
331 self.error("No times specified for bar-plots")
332 self.opts.time.sort()
333 if self.opts.referenceTime and reference!=None:
334 minTime,maxTime=reference.timeRange()
335 else:
336 minTime,maxTime=timelines.timeRange()
337 usedTimes=[]
338 hasMin=False
339 for t in self.opts.time:
340 if t<minTime:
341 if not hasMin:
342 usedTimes.append(minTime)
343 hasMin=True
344 elif t>maxTime:
345 usedTimes.append(maxTime)
346 break
347 else:
348 usedTimes.append(t)
349 data=timelines.getData(usedTimes,
350 value=self.opts.fields,
351 position=self.opts.positions,
352 vectorMode=self.opts.vectorMode)
353
354 result+="set style data histogram\n"
355 result+="set style histogram cluster gap 1\n"
356 result+="set style fill solid border -1\n"
357 result+="set boxwidth 0.9\n"
358 result+="set xtics border in scale 1,0.5 nomirror rotate by 90 offset character 0, 0, 0\n"
359
360 result+="set xtics ("
361 for i,p in enumerate(self.opts.positions):
362 if i>0:
363 result+=" , "
364 result+='"%s" %d' % (p,i)
365 result+=")\n"
366 for tm in usedTimes:
367 if abs(float(tm))>1e20:
368 continue
369 result+=self.setFile("%s_writeTime_%s_Time_%s.png" % (usedDirName,timelines.usedTime,tm))
370 result+='set title "Directory: %s WriteTime: %s Time: %s"\n' % (self.opts.dirName.replace("_","\\\\_"),timelines.usedTime,tm)
371 result+= "plot "
372 first=True
373 for val in self.opts.fields:
374 if first:
375 first=False
376 else:
377 result+=", "
378 result+='"-" title "%s" ' % val.replace("_","\\\\_")
379 result+="\n"
380 for v,t,vals in data:
381 if t==tm:
382 for v in vals:
383 result+="%g\n" % v
384 result+="e\n"
385 elif self.opts.basicMode=='lines':
386
387 oPlots=timelines.getDataLocation(value=self.opts.fields,
388 position=self.opts.positions,
389 vectorMode=self.opts.vectorMode)
390
391 plots=oPlots[:]
392 rPlots=None
393
394 if reference:
395 rPlots=reference.getDataLocation(value=self.opts.fields,
396 position=self.opts.positions,
397 vectorMode=self.opts.vectorMode)
398 for gp,pos,val,comp,tv in rPlots:
399 plots.append((gp,
400 pos,
401 self.opts.refprefix+" "+val,
402 comp,
403 tv))
404 if self.opts.referenceTime and reference!=None:
405 minTime,maxTime=reference.timeRange()
406 else:
407 minTime,maxTime=timelines.timeRange()
408 if self.opts.minTime:
409 minTime=self.opts.minTime
410 if self.opts.maxTime:
411 maxTime=self.opts.maxTime
412 result+= "set xrange [%g:%g]\n" % (minTime,maxTime)
413 if self.opts.collectLines=="fields":
414 for val in self.opts.fields:
415 vname=val
416 if val in timelines.vectors:
417 vname+="_"+self.opts.vectorMode
418 result+=self.setFile("%s_writeTime_%s_Value_%s.png" % (usedDirName,timelines.usedTime,vname))
419 result+='set title "Directory: %s WriteTime: %s Value: %s"\n' % (self.opts.dirName.replace("_","\\\\_"),timelines.usedTime,vname.replace("_","\\\\\\_"))
420 result+= "plot "
421 first=True
422 for f,v,p,i,tl in plots:
423 if v==val:
424 if first:
425 first=False
426 else:
427 result+=" , "
428 if type(i)==int:
429 result+= ' "%s" using 1:%d title "%s" with lines ' % (f,i+2,p.replace("_","\\\\_"))
430 else:
431 result+= ' "%s" using 1:%s title "%s" with lines ' % (f,i,p.replace("_","\\\\_"))
432
433 result+="\n"
434 elif self.opts.collectLines=="positions":
435 for pos in self.opts.positions:
436 result+=self.setFile("%s_writeTime_%s_Position_%s.png" % (usedDirName,timelines.usedTime,pos))
437 result+='set title "Directory: %s WriteTime: %s Position: %s"\n' % (self.opts.dirName.replace("_","\\\\_"),timelines.usedTime,pos.replace("_","\\\\_"))
438 result+= "plot "
439 first=True
440 for f,v,p,i,tl in plots:
441 if p==pos:
442 if first:
443 first=False
444 else:
445 result+=" , "
446 if type(i)==int:
447 result+= ' "%s" using 1:%d title "%s" with lines ' % (f,i+2,v.replace("_","\\\\_"))
448 else:
449 result+= ' "%s" using 1:%s title "%s" with lines ' % (f,i,v.replace("_","\\\\_"))
450 result+="\n"
451
452 else:
453 self.error("Unimplemented collection of lines:",self.opts.collectLines)
454 else:
455 self.error("Not implemented basicMode",self.opts.basicMode)
456
457 if self.opts.csvFile or self.opts.excelFile or self.opts.pandasData or self.opts.numpyData:
458 if self.opts.basicMode!='lines':
459 self.error("CSV and Excel-files currently only supported for lines-mode")
460 spread=plots[0][-1]()
461 for line in plots[1:]:
462 if line[3]==0:
463 sp=line[-1]()
464 try:
465 spread+=sp
466 except WrongDataSize:
467 if self.opts.resample:
468 for n in sp.names()[1:]:
469 data=spread.resample(sp,
470 n,
471 extendData=self.opts.extendData)
472 try:
473 spread.append(n,data)
474 except ValueError:
475 spread.append(self.opts.refprefix+" "+n,data)
476 else:
477 self.warning("Try the --resample-option")
478 raise
479
480 if self.opts.csvFile:
481 spread.writeCSV(self.opts.csvFile)
482 if self.opts.excelFile:
483 spread.getData().to_excel(self.opts.excelFile)
484 if self.opts.pandasData:
485 self.setData({"series":spread.getSeries(),
486 "dataFrame":spread.getData()})
487 if self.opts.numpyData:
488 self.setData({"data":spread.data.copy()})
489
490 elif self.opts.compare or self.opts.metrics:
491 statData={}
492 if self.opts.compare:
493 statData["compare"]={}
494 if self.opts.metrics:
495 statData["metrics"]={}
496 for p in self.opts.positions:
497 if self.opts.compare:
498 statData["compare"][p]={}
499 if self.opts.metrics:
500 statData["metrics"][p]={}
501
502 if self.opts.basicMode!='lines':
503 self.error("Compare currently only supported for lines-mode")
504
505 if self.opts.compare:
506 if rPlots==None:
507 self.error("No reference data specified. Can't compare")
508 elif len(rPlots)!=len(oPlots):
509 self.error("Number of original data sets",len(oPlots),
510 "is not equal to the reference data sets",
511 len(rPlots))
512
513 for i,p in enumerate(oPlots):
514 pth,val,loc,ind,tl=p
515 if self.opts.compare:
516 rpth,rval,rloc,rind,rtl=rPlots[i]
517 if val!=rval or loc!=rloc or ind!=rind:
518 self.error("Original data",p,"and reference",rPlots[i],
519 "do not match")
520 data=tl()
521 try:
522 dataIndex=1+ind
523 if self.opts.metrics:
524 if not self.opts.silent:
525 print_("Metrics for",val,"on",loc,"index",ind,"(Path:",pth,")")
526 result=data.metrics(data.names()[dataIndex],
527 minTime=self.opts.minTime,
528 maxTime=self.opts.maxTime)
529 statData["metrics"][loc][val]=result
530 if not self.opts.silent:
531 print_(" Min :",result["min"])
532 print_(" Max :",result["max"])
533 print_(" Average :",result["average"])
534 print_(" Weighted average :",result["wAverage"])
535 if not self.opts.compare:
536 print_("Data size:",data.size())
537 print_(" Time Range :",result["tMin"],result["tMax"])
538 if self.opts.compare:
539 if not self.opts.silent:
540 print_("Comparing",val,"on",loc,"index",ind,"(path:",pth,")",end="")
541 ref=rtl()
542 if self.opts.compareOnOriginal:
543 if not self.opts.silent:
544 print_("on original data points")
545 result=data.compare(ref,
546 data.names()[dataIndex],
547 minTime=self.opts.minTime,
548 maxTime=self.opts.maxTime)
549 else:
550 if not self.opts.silent:
551 print_("on reference data points")
552 result=ref.compare(data,
553 data.names()[dataIndex],
554 minTime=self.opts.minTime,
555 maxTime=self.opts.maxTime)
556
557 statData["compare"][loc][val]=result
558 if not self.opts.silent:
559 print_(" Max difference :",result["max"])
560 print_(" Average difference :",result["average"])
561 print_(" Weighted average :",result["wAverage"])
562 print_("Data size:",data.size(),"Reference:",ref.size())
563 if not self.opts.metrics:
564 print_(" Time Range :",result["tMin"],result["tMax"])
565 if not self.opts.silent:
566 print_()
567 except TypeError:
568 if self.opts.vectorMode=="mag":
569 self.error("Vector-mode 'mag' not supported for --compare and --metrics")
570 else:
571 raise
572
573 self.setData(statData)
574 else:
575 dest=sys.stdout
576 if self.opts.gnuplotFile:
577 dest=open(self.opts.gnuplotFile,"w")
578
579 dest.write(result)
580
581
582