Coverage for python/lsst/sims/maf/slicers/movieSlicer.py : 14%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from builtins import str
2# cumulative one dimensional movie slicer
3import os
4import warnings
5import subprocess
6from subprocess import CalledProcessError
7import numpy as np
8from functools import wraps
10from lsst.sims.maf.utils import percentileClipping, optimalBins
11from lsst.sims.maf.stackers import ColInfo
12from .baseSlicer import BaseSlicer
14__all__ = ['MovieSlicer']
16class MovieSlicer(BaseSlicer):
17 def __init__(self, sliceColName=None, sliceColUnits=None,
18 bins=None, binMin=None, binMax=None, binsize=None,
19 verbose=True, badval=0, cumulative=True, forceNoFfmpeg=False):
20 """
21 The 'movieSlicer' acts similarly to the OneDSlicer (slices on one data column).
22 However, the data slices from the movieSlicer are intended to be fed to another slicer, which then
23 (together with a set of Metrics) calculates metric values + plots at each slice created by the movieSlicer.
24 The job of the movieSlicer is to track those slices and put them together into a movie.
26 'sliceColName' is the name of the data column to use for slicing.
27 'sliceColUnits' lets the user set the units (for plotting purposes) of the slice column.
28 'bins' can be a numpy array with the binpoints for sliceCol or a single integer value
29 (if a single value, this will be used as the number of bins, together with data min/max or binMin/Max),
30 as in numpy's histogram function.
31 If 'binsize' is used, this will override the bins value and will be used together with the data min/max
32 or binMin/Max to set the binpoint values.
34 Bins work like numpy histogram bins: the last 'bin' value is end value of last bin;
35 all bins except for last bin are half-open ([a, b>), the last one is ([a, b]).
37 The movieSlicer stitches individual frames together into a movie using ffmpeg. Thus, on
38 instantiation it checks that ffmpeg is available and will raise and exception if not.
39 This behavior can be overriden using forceNoFfmpeg = True (in order to create a movie later perhaps).
40 """
41 # Check for ffmpeg.
42 if not forceNoFfmpeg:
43 try:
44 fnull = open(os.devnull, 'w')
45 subprocess.check_call(['which', 'ffmpeg'], stdout=fnull)
46 except CalledProcessError:
47 raise Exception('Could not find ffmpeg on the system, so will not be able to create movie.'
48 ' Use forceNoFfmpeg=True to override this error and create individual images.')
49 super().__init__(verbose=verbose, badval=badval)
50 self.sliceColName = sliceColName
51 self.columnsNeeded = [sliceColName]
52 self.bins = bins
53 self.binMin = binMin
54 self.binMax = binMax
55 self.binsize = binsize
56 self.cumulative = cumulative
57 if sliceColUnits is None:
58 co = ColInfo()
59 self.sliceColUnits = co.getUnits(self.sliceColName)
60 self.slicer_init = {'sliceColName':self.sliceColName, 'sliceColUnits':sliceColUnits,
61 'badval':badval}
63 def setupSlicer(self, simData, maps=None):
64 """
65 Set up bins in slicer.
66 """
67 if self.sliceColName is None:
68 raise Exception('sliceColName was not defined when slicer instantiated.')
69 sliceCol = simData[self.sliceColName]
70 # Set bin min/max values.
71 if self.binMin is None:
72 self.binMin = sliceCol.min()
73 if self.binMax is None:
74 self.binMax = sliceCol.max()
75 # Give warning if binMin = binMax, and do something at least slightly reasonable.
76 if self.binMin == self.binMax:
77 warnings.warn('binMin = binMax (maybe your data is single-valued?). '
78 'Increasing binMax by 1 (or 2*binsize, if binsize set).')
79 if self.binsize is not None:
80 self.binMax = self.binMax + 2 * self.binsize
81 else:
82 self.binMax = self.binMax + 1
83 # Set bins.
84 # Using binsize.
85 if self.binsize is not None:
86 if self.bins is not None:
87 warnings.warn('Both binsize and bins have been set; Using binsize %f only.' %(self.binsize))
88 self.bins = np.arange(self.binMin, self.binMax+self.binsize/2.0, float(self.binsize), 'float')
89 # Using bins value.
90 else:
91 # Bins was a sequence (np array or list)
92 if hasattr(self.bins, '__iter__'):
93 self.bins = np.sort(self.bins)
94 # Or bins was a single value.
95 else:
96 if self.bins is None:
97 self.bins = optimalBins(sliceCol, self.binMin, self.binMax)
98 nbins = int(self.bins)
99 self.binsize = (self.binMax - self.binMin) / float(nbins)
100 self.bins = np.arange(self.binMin, self.binMax+self.binsize/2.0, self.binsize, 'float')
101 # Set nbins to be one less than # of bins because last binvalue is RH edge only
102 self.nslice = len(self.bins) - 1
103 # Set slicePoint metadata.
104 self.slicePoints['sid'] = np.arange(self.nslice)
105 self.slicePoints['bins'] = self.bins
106 # Add metadata from maps.
107 self._runMaps(maps)
108 # Set up data slicing.
109 self.simIdxs = np.argsort(simData[self.sliceColName])
110 simFieldsSorted = np.sort(simData[self.sliceColName])
111 # "left" values are location where simdata == bin value
112 self.left = np.searchsorted(simFieldsSorted, self.bins[:-1], 'left')
113 self.left = np.concatenate((self.left, np.array([len(self.simIdxs),])))
114 # Set up _sliceSimData method for this class.
115 if self.cumulative:
116 @wraps(self._sliceSimData)
117 def _sliceSimData(islice):
118 """
119 Slice simData on oneD sliceCol, to return relevant indexes for slicepoint.
120 """
121 #this is the important part. The ids here define the pieces of data that get
122 #passed on to subsequent slicers
123 #cumulative version of 1D slicing
124 idxs = self.simIdxs[0:self.left[islice+1]]
125 return {'idxs':idxs,
126 'slicePoint':{'sid':islice, 'binLeft':self.bins[0], 'binRight':self.bins[islice+1]}}
127 setattr(self, '_sliceSimData', _sliceSimData)
128 else:
129 @wraps(self._sliceSimData)
130 def _sliceSimData(islice):
131 """
132 Slice simData on oneD sliceCol, to return relevant indexes for slicepoint.
133 """
134 idxs = self.simIdxs[self.left[islice]:self.left[islice+1]]
135 return {'idxs':idxs,
136 'slicePoint':{'sid':islice, 'binLeft':self.bins[islice], 'binRight':self.bins[islice+1]}}
137 setattr(self, '_sliceSimData', _sliceSimData)
139 def __eq__(self, otherSlicer):
140 """
141 Evaluate if slicers are equivalent.
142 """
143 if isinstance(otherSlicer, MovieSlicer):
144 return np.all(otherSlicer.slicePoints['bins'] == self.slicePoints['bins'])
145 else:
146 return False
148 def makeMovie(self, outfileroot, sliceformat, plotType, figformat, outDir='Output', ips=10.0, fps=10.0):
149 """
150 Takes in metric and slicer metadata and calls ffmpeg to stitch together output files.
151 """
152 if not os.path.isdir(outDir):
153 raise Exception('Cannot find output directory %s with movie input files.' %(outDir))
154 #make video
155 # ffmpeg -r 30 -i [image names - FilterColors_SkyMap_%03d.png] -r 30
156 # -pix_fmt yuv420p -crf 18 -preset slower outfile
157 callList = ['ffmpeg', '-r', str(ips), '-i',
158 os.path.join(outDir,'%s_%s_%s.%s'%(outfileroot, sliceformat, plotType, figformat)),
159 '-r', str(fps), '-pix_fmt', 'yuv420p', '-crf', '18', '-preset', 'slower',
160 os.path.join(outDir,'%s_%s_%s_%s.mp4' %(outfileroot, plotType, str(ips), str(fps)))]
161 print('Attempting to call ffmpeg with:')
162 print(' '.join(callList))
163 p = subprocess.check_call(callList)
164 #make thumbnail gif
165 callList = ['ffmpeg','-i',os.path.join(outDir,'%s_%s_%s_%s.mp4' %(outfileroot, plotType, str(ips), str(fps))),
166 '-vf', 'scale=%s:%s' %(str(320),str(-1)), '-t', str(10), '-r', str(10),
167 os.path.join(outDir,'%s_%s_%s_%s.gif' %(outfileroot, plotType, str(ips), str(fps)))]
168 print('converting to animated gif with:')
169 print(' '.join(callList))
170 p2 = subprocess.check_call(callList)