Generic progress report system with optional stdout and wm.progress
authorLukas Tönne <lukas.toenne@gmail.com>
Mon, 15 Dec 2014 13:28:17 +0000 (14:28 +0100)
committerLukas Tönne <lukas.toenne@gmail.com>
Mon, 15 Dec 2014 13:28:17 +0000 (14:28 +0100)
output.

This works as a context manager: For any step in the process a loop can
be wrapped in the ProgressContext context manager. These classes are
generated by make_progress_reporter, so the main output options can be
defined on a high level, while actual number of steps is defined only
when actually running the job and the total amount of items is known.

object_physics_meadow/hierarchical_dart_throw.py
object_physics_meadow/meadow.py
object_physics_meadow/patch.py
object_physics_meadow/ui.py
object_physics_meadow/util.py

index 77b84f1..78f8ca7 100644 (file)
@@ -51,6 +51,10 @@ class GridLevel():
         self.cells.append(cell)
         return cell
 
+    @staticmethod
+    def num_cells_in_range(imin, imax, jmin, jmax, kmin, kmax):
+        return (imax - imin) * (jmax - jmin) * (kmax - kmin)
+
     def set_active_cells(self, imin, imax, jmin, jmax, kmin, kmax):
         tot = (imax - imin) * (jmax - jmin) * (kmax - kmin)
         self.cells = [None] * tot
@@ -58,6 +62,7 @@ class GridLevel():
         for k in range(kmin, kmax):
             for j in range(jmin, jmax):
                 for i in range(imin, imax):
+                    progress_add(1)
                     self.cells[c] = GridCell(i, j, k)
                     c += 1
 
@@ -119,7 +124,17 @@ class PointCell():
         self.points = []
 
 class PointGrid():
-    def __init__(self, radius, b0, gridmin, gridmax):
+    @staticmethod
+    def num_cells(radius, gridmin, gridmax):
+        size = radius
+        amin = ifloor(gridmin[0] / size) - 1
+        bmin = ifloor(gridmin[1] / size) - 1
+        na = ifloor(gridmax[0] / size) + 2 - amin
+        nb = ifloor(gridmax[1] / size) + 2 - bmin
+        
+        return na * nb
+
+    def __init__(self, radius, gridmin, gridmax):
         width = gridmax[0] - gridmin[0]
         height = gridmax[1] - gridmin[1]
         size = radius
@@ -132,8 +147,13 @@ class PointGrid():
         self.na = ifloor(gridmax[0] / size) + 2 - self.amin
         self.nb = ifloor(gridmax[1] / size) + 2 - self.bmin
         
+        # modified range generator for progress reports
+        def range_progress(tot):
+            for i in range(tot):
+                progress_add(1)
+                yield i
         # note: row-major, so we can address it with cells[i][j]
-        self.cells = tuple(tuple(PointCell() for j in range(self.nb)) for i in range(self.na))
+        self.cells = tuple(tuple(PointCell() for j in range_progress(self.nb)) for i in range(self.na))
 
     def grid_from_loc(self, point):
         s = self.invsize
@@ -216,7 +236,8 @@ def split_cell(radius2, b0, pgrid, child_level, cell, x0, x1, y0, y1, z0, z1):
             if not is_covered(radius2, b0, pgrid, child_level, ci, cj, cx0, cx1, cy0, cy1):
                 child_cell = child_level.activate(ci, cj, ck)
 
-def hierarchical_dart_throw_gen(radius, max_levels, xmin, xmax, ymin, ymax):
+def hierarchical_dart_throw_gen(radius, max_levels, xmin, xmax, ymin, ymax,
+                                progress_reporter=DummyProgressContext):
     radius2 = radius * radius
     gridmin = (xmin, ymin)
     gridmax = (xmax, ymax)
@@ -232,28 +253,33 @@ def hierarchical_dart_throw_gen(radius, max_levels, xmin, xmax, ymin, ymax):
         levels = [base_level] + [GridLevel(i, base_level.size / (2**i), radius) for i in range(1, max_levels)]
         epsilon = levels[-1].weight * 0.5
         
-        base_level.set_active_cells(imin, imax, jmin, jmax, 0, 1)
+        with progress_reporter("Activate Cells", 0, GridLevel.num_cells_in_range(imin, imax, jmin, jmax, 0, 1)):
+            base_level.set_active_cells(imin, imax, jmin, jmax, 0, 1)
         
-        pgrid = PointGrid(radius, b0, gridmin, gridmax)
+        with progress_reporter("Init Spatial Grid", 0, PointGrid.num_cells(radius, gridmin, gridmax)):
+            pgrid = PointGrid(radius, gridmin, gridmax)
         
-        for i in range(num):
-            if not any(level.cells for level in levels):
-                break
-            
-            level, cell = pop_cell(levels)
-            if level:
-                x0, x1, y0, y1, z0, z1 = level.cell_corners(cell)
+        with progress_reporter("Generate Samples", 0, num):
+            for i in range(num):
+                progress_add(1)
                 
-                # test coverage
-                if not is_covered(radius2, b0, pgrid, level, cell.i, cell.j, x0, x1, y0, y1):
-                    point = level.sample(x0, x1, y0, y1, z0, z1)
-                    if test_disk(radius2, pgrid, point, level, cell.i, cell.j):
-                        yield point
-                        pgrid.insert(point)
-                    else:
-                        if level.index < max_levels - 1:
-                            split_cell(radius2, b0, pgrid, levels[level.index+1], cell, x0, x1, y0, y1, z0, z1)
-            else:
-                break
-    
+                if not any(level.cells for level in levels):
+                    break
+                
+                level, cell = pop_cell(levels)
+                if level:
+                    x0, x1, y0, y1, z0, z1 = level.cell_corners(cell)
+                    
+                    # test coverage
+                    if not is_covered(radius2, b0, pgrid, level, cell.i, cell.j, x0, x1, y0, y1):
+                        point = level.sample(x0, x1, y0, y1, z0, z1)
+                        if test_disk(radius2, pgrid, point, level, cell.i, cell.j):
+                            yield point
+                            pgrid.insert(point)
+                        else:
+                            if level.index < max_levels - 1:
+                                split_cell(radius2, b0, pgrid, levels[level.index+1], cell, x0, x1, y0, y1, z0, z1)
+                else:
+                    break
+
     return gen
index e5013d7..6642ea9 100644 (file)
@@ -26,6 +26,7 @@ from object_physics_meadow import patch, blob
 from object_physics_meadow.duplimesh import project_on_ground
 #from object_physics_meadow import dupliparticle
 #from object_physics_meadow.pointcache import cache_filename
+from object_physics_meadow.util import *
 
 from object_physics_meadow.best_candidate import best_candidate_gen
 from object_physics_meadow.hierarchical_dart_throw import hierarchical_dart_throw_gen
@@ -44,7 +45,7 @@ def make_samples(context, gridob, groundob):
     
     # get a sample generator implementation
     #gen = best_candidate_gen(groundob.meadow.patch_radius, xmin, xmax, ymin, ymax)
-    gen = hierarchical_dart_throw_gen(groundob.meadow.patch_radius, groundob.meadow.sampling_levels, xmin, xmax, ymin, ymax)
+    gen = hierarchical_dart_throw_gen(groundob.meadow.patch_radius, groundob.meadow.sampling_levels, xmin, xmax, ymin, ymax, progress_reporter=make_progress_reporter(True, True))
     
     mat = groundob.matrix_world
     loc2D = [(mat * Vector(p[0:3] + (1.0,)))[0:2] for p in gen(groundob.meadow.seed, groundob.meadow.max_patches)]
@@ -59,17 +60,17 @@ def make_blobs(context, gridob, groundob):
     if use_profiling:
         prof = cProfile.Profile()
         prof.enable()
-        samples2D = make_samples(context, gridob, groundob)
-        blob.make_blobs(context, gridob, groundob, samples2D, groundob.meadow.patch_radius)
+    
+    samples2D = make_samples(context, gridob, groundob)
+    blob.make_blobs(context, gridob, groundob, samples2D, groundob.meadow.patch_radius)
+
+    if use_profiling:
         prof.disable()
 
         s = io.StringIO()
         ps = pstats.Stats(prof, stream=s).sort_stats('tottime')
         ps.print_stats()
         print(s.getvalue())
-    else:
-        samples2D = make_samples(context, gridob, groundob)
-        blob.make_blobs(context, gridob, groundob, samples2D, groundob.meadow.patch_radius)
 
 ### Patch copies for simulation ###
 def make_patches(context, gridob, groundob):
index eae3b71..888c70d 100644 (file)
@@ -236,52 +236,53 @@ def bake_psys(context, ob, psys):
     # restore
     ob.particle_systems.active = curpsys
 
-def bake_all(context):
+def count_bakeable(context):
+    num = 0
+    for ob in patch_objects(context):
+        for psys in ob.particle_systems:
+            num += 1
+    return num
+
+def bake_all(context, progress_reporter):
     settings = _settings.get(context)
     wm = context.window_manager
     
     total_time = 0.0
     avg_time = 0.0
     
-    # XXX Note: wm.progress updates are disabled for now, because the bake
-    # operator overrides this with it's own progress numbers ...
-    
     total = count_bakeable(context)
-    #wm.progress_begin(0, total)
-    
-    num = 0
-    for ob in patch_objects(context):
-        for psys in ob.particle_systems:
-            sys.stdout.write("Baking blob {}/{} ... ".format(str(num).rjust(5), str(total).ljust(5)))
-            sys.stdout.flush()
-            
-            start_time = time.time()
-            
-            bake_psys(context, ob, psys)
-            
-            duration = time.time() - start_time
-            total_time += duration
-            avg_time = total_time / float(num + 1)
-            
-            #wm.progress_update(num)
-            time_string = lambda x: time.strftime("%H:%M:%S", time.gmtime(x)) + ".%02d" % (int(x * 100.0) % 100)
-            durstr = time_string(duration)
-            avgstr = time_string(avg_time) if avg_time > 0.0 else "--:--:--"
-            etastr = time_string(avg_time * (total - num)) if avg_time > 0.0 else "--:--:--"
-            sys.stdout.write("{} (avg. {}, ETA {})\n".format(durstr, avgstr, etastr))
-            sys.stdout.flush()
-            num += 1
     
-    #wm.progress_end()
-
-def count_bakeable(context):
-    num = 0
-    for ob in patch_objects(context):
-        for psys in ob.particle_systems:
-            num += 1
-    return num
+    with progress_reporter("Bake Blob", 0, total):
+        """
+        num = 0
+        """
+        for ob in patch_objects(context):
+            for psys in ob.particle_systems:
+                """
+                sys.stdout.write("Baking blob {}/{} ... ".format(str(num).rjust(5), str(total).ljust(5)))
+                sys.stdout.flush()
+                
+                start_time = time.time()
+                """
+                
+                progress_add(1)
+                bake_psys(context, ob, psys)
+                
+                """
+                duration = time.time() - start_time
+                total_time += duration
+                avg_time = total_time / float(num + 1)
+                
+                time_string = lambda x: time.strftime("%H:%M:%S", time.gmtime(x)) + ".%02d" % (int(x * 100.0) % 100)
+                durstr = time_string(duration)
+                avgstr = time_string(avg_time) if avg_time > 0.0 else "--:--:--"
+                etastr = time_string(avg_time * (total - num)) if avg_time > 0.0 else "--:--:--"
+                sys.stdout.write("{} (avg. {}, ETA {})\n".format(durstr, avgstr, etastr))
+                sys.stdout.flush()
+                num += 1
+                """
 
-def patch_objects_rebake(context):
+def patch_objects_rebake(context, progress_reporter=DummyProgressContext):
     settings = _settings.get(context)
     wm = context.window_manager
     
@@ -293,6 +294,6 @@ def patch_objects_rebake(context):
         # XXX have to set this because bake operator only bakes up to the last frame ...
         scene.frame_current = scene.frame_end
         
-        bake_all(context)
+        bake_all(context, progress_reporter)
         
         scene.frame_set(curframe)
index 24c4761..abe9fe9 100644 (file)
@@ -189,7 +189,9 @@ class RebakeMeadowOperator(MeadowOperatorBase, Operator):
     
     def execute(self, context):
         with ObjectSelection():
-            patch.patch_objects_rebake(context)
+            # XXX Note: wm.progress updates are disabled for now, because the bake
+            # operator overrides this with it's own progress numbers ...
+            patch.patch_objects_rebake(context, progress_reporter=make_progress_reporter(show_progress_bar=False, show_stdout=True))
         return {'FINISHED'}
 
 
index 1832394..b709b65 100644 (file)
@@ -18,7 +18,8 @@
 
 # <pep8 compliant>
 
-import bpy, time
+import bpy, time, sys
+from math import *
 
 def ifloor(x):
     return int(x) if x >= 0.0 else int(x) - 1
@@ -93,3 +94,94 @@ def set_object_parent(ob, parent):
     mat = ob.matrix_world
     ob.parent = parent
     ob.matrix_world = mat
+
+#-----------------------------------------------------------------------
+
+_progress_context = None
+
+def make_progress_reporter(show_progress_bar=True, show_stdout=False):
+
+    # internal class returned by the function, bound to output args
+    class ProgressContext():
+        def __init__(self, name, pmin, pmax):
+            self.name = name
+            self.pmin = pmin
+            self.pmax = pmax
+            self.tot = pmax - pmin
+            self.norm = 1.0 / float(self.tot) if self.tot > 0 else 0.0
+
+            self.pcur = pmin
+            self.perc_show = -2.0 # last displayed percentage, init to make sure we show the first time
+
+        def __enter__(self):
+            global _progress_context
+
+            assert(_progress_context is None)
+            _progress_context = self
+            
+            if show_progress_bar:
+                wm = bpy.context.window_manager
+                # always use 0..100 percentage on the progress counter,
+                # it does not display large numbers well
+                wm.progress_begin(0, 100)
+
+        def __exit__(self, exc_type, exc_value, traceback):
+            global _progress_context
+
+            if show_progress_bar:
+                wm = bpy.context.window_manager
+                wm.progress_end()
+
+            if show_stdout:
+                # clean newline
+                sys.stdout.write("\n")
+                sys.stdout.flush()
+
+            assert(_progress_context is self)
+            _progress_context = None
+
+        def set_progress(self, value, message):
+            self.pcur = value
+            done = value - self.pmin
+            perc = 100.0 * done * self.norm
+
+            # only write to progress indicator or stdout if the percentage actually changed
+            # avoids overhead for very frequent updates
+            if perc > self.perc_show + 1.0:
+                self.perc_show = perc
+                perc = min(max(int(perc), 0), 100)
+
+                if show_progress_bar:
+                    wm = bpy.context.window_manager
+                    wm.progress_update(perc)
+
+                if show_stdout:
+                    sys.stdout.write("\r>> {}: {}/{} [{}{}] {}".format(self.name, str(done).rjust(len(str(self.tot))), str(self.tot), '.' * perc, ' ' * (100 - perc), message))
+                    sys.stdout.flush()
+
+    return ProgressContext
+
+# dummy context manager class to avoid clumsy conditionals when passing None
+class DummyProgressContext():
+    def __init__(self, name, pmin, pmax):
+        pass
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        pass
+
+def progress_set(value, message=""):
+    global _progress_context
+    
+    if not _progress_context:
+        return
+    _progress_context.set_progress(value, message)
+
+def progress_add(value, message=""):
+    global _progress_context
+
+    if not _progress_context:
+        return
+    _progress_context.set_progress(_progress_context.pcur + value, message)