[yt-svn] commit/yt: xarthisius: Merged in ngoldbaum/yt (pull request #2266)

commits-noreply at bitbucket.org commits-noreply at bitbucket.org
Sat Jul 16 09:25:27 PDT 2016


1 new commit in yt:

https://bitbucket.org/yt_analysis/yt/commits/e7aabcd61165/
Changeset:   e7aabcd61165
Branch:      yt
User:        xarthisius
Date:        2016-07-16 16:25:00+00:00
Summary:     Merged in ngoldbaum/yt (pull request #2266)

Volume Source refactor
Affected #:  12 files

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 doc/source/cookbook/amrkdtree_downsampling.py
--- a/doc/source/cookbook/amrkdtree_downsampling.py
+++ b/doc/source/cookbook/amrkdtree_downsampling.py
@@ -38,7 +38,7 @@
 # again.
 
 render_source.set_volume(kd_low_res)
-render_source.set_fields('density')
+render_source.set_field('density')
 sc.render()
 sc.save("v1.png", sigma_clip=6.0)
 

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 doc/source/visualizing/Volume_Rendering_Tutorial.ipynb
--- a/doc/source/visualizing/Volume_Rendering_Tutorial.ipynb
+++ b/doc/source/visualizing/Volume_Rendering_Tutorial.ipynb
@@ -257,10 +257,6 @@
     "sc.camera.set_position(ds.domain_left_edge)\n",
     "sc.camera.switch_orientation()\n",
     "\n",
-    "# reset the transfer function to the default\n",
-    "render_source = sc.get_source(0)\n",
-    "render_source.build_default_transfer_function()\n",
-    "\n",
     "# add an opaque source to the scene\n",
     "sc.annotate_axes()\n",
     "\n",

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 doc/source/visualizing/volume_rendering.rst
--- a/doc/source/visualizing/volume_rendering.rst
+++ b/doc/source/visualizing/volume_rendering.rst
@@ -13,6 +13,11 @@
 hardware. Optimized versions implemented with OpenGL and utilizing graphics
 processors are being actively developed.
 
+.. note::
+
+   There is a Jupyter notebook containing a volume rendering tutorial available
+   at :ref:`volume-rendering-tutorial`.
+
 Volume Rendering Introduction
 -----------------------------
 
@@ -166,8 +171,70 @@
 Because good transfer functions can be difficult to generate, the
 :class:`~yt.visualization.volume_rendering.transfer_function_helper.TransferFunctionHelper`
 exists in order to help create and modify transfer functions with smart
-defaults for your datasets.  To see a full example on how to use this
-interface, follow the annotated :ref:`transfer-function-helper-tutorial`.
+defaults for your datasets.
+
+To ease constructing transfer functions, each ``VolumeSource`` instance has a
+``TransferFunctionHelper`` instance associated with it. This is the easiest way
+to construct and customize a ``ColorTransferFunction`` for a volume rendering.
+
+In the following example, we make use of the ``TransferFunctionHelper``
+associated with a scene's ``VolumeSource`` to create an appealing transfer
+function between a physically motivated range of densities in a cosmological
+simulation:
+
+.. python-script::
+
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   # Get a reference to the VolumeSource associated with this scene
+   # It is the first source associated with the scene, so we can refer to it
+   # using index 0.
+   source = sc[0]
+
+   # Set the bounds of the transfer function
+   source.tfh.set_bounds((3e-31, 5e-27))
+
+   # set that the transfer function should be evaluated in log space
+   source.tfh.set_log(True)
+
+   # Make underdense regions appear opaque
+   source.tfh.grey_opacity = True
+
+   # Plot the transfer function, along with the CDF of the density field to
+   # see how the transfer function corresponds to structure in the CDF
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   # save the image, flooring especially bright pixels for better contrast
+   sc.save('rendering.png', sigma_clip=6.0)
+
+For fun, let's make the same volume_rendering, but this time setting
+``grey_opacity=False``, which will make overdense regions stand out more:
+
+.. python-script::
+
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   source = sc[0]
+
+   # Set transfer function properties
+   source.tfh.set_bounds((3e-31, 5e-27))
+   source.tfh.set_log(True)
+   source.tfh.grey_opacity = False
+
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   sc.save('rendering.png', sigma_clip=4.0)
+
+To see a full example on how to use the ``TransferFunctionHelper`` interface,
+follow the annotated :ref:`transfer-function-helper-tutorial`.
 
 Color Transfer Functions
 ++++++++++++++++++++++++
@@ -177,26 +244,168 @@
 and opacities in the rendered rays.  One can add discrete features to the
 transfer function, which will render isocontours in the field data and
 works well for visualizing nested structures in a simulation.  Alternatively,
-one can add continuous features to the transfer function, which tends to
-produce better results for most datasets.
+one can also add continuous features to the transfer function.
 
-In order to modify a
+See :ref:`cookbook-custom-transfer-function` for an annotated, runnable tutorial
+explaining usage of the ColorTransferFunction.
+
+There are several methods to create a
 :class:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction`
-use
-:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.add_layers`,
-which will add evenly spaced isocontours along the transfer
-function; use
-:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.sample_colormap`,
-which will sample a colormap at a given value;
-use
-:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.add_gaussian`,
-which will allow you to specify the colors directly on the transfer function,
-and use
-:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.map_to_colormap`,
-where you can map a segment of the transfer function space to an entire
-colormap at a single alpha value.
+for a volume rendering. We will describe the low-level interface for
+constructing color transfer functions here, and provide examples for each
+option.
 
-See :ref:`cookbook-custom-transfer-function` for an example usage.
+add_layers
+""""""""""
+
+The easiest way to create a ColorTransferFunction is to use the
+:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.add_layers` function,
+which will add evenly spaced isocontours along the transfer function, sampling a
+colormap to determine the colors of the layers.
+
+.. python-script::
+
+   import numpy as np
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   source = sc[0]
+
+   source.set_field('density')
+   source.set_log(True)
+
+   bounds = (3e-31, 5e-27)
+
+   # Since this rendering is done in log space, the transfer function needs
+   # to be specified in log space.
+   tf = yt.ColorTransferFunction(np.log10(bounds))
+
+   tf.add_layers(5, colormap='arbre')
+
+   source.tfh.tf = tf
+   source.tfh.bounds = bounds
+
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   sc.save('rendering.png', sigma_clip=6)
+
+sample_colormap
+"""""""""""""""
+
+To add a single gaussian layer with a color determined by a colormap value, use
+:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.sample_colormap`.
+
+.. python-script::
+
+   import numpy as np
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   source = sc[0]
+
+   source.set_field('density')
+   source.set_log(True)
+
+   bounds = (3e-31, 5e-27)
+
+   # Since this rendering is done in log space, the transfer function needs
+   # to be specified in log space.
+   tf = yt.ColorTransferFunction(np.log10(bounds))
+
+   tf.sample_colormap(np.log10(1e-30), w=.01, colormap='arbre')
+
+   source.tfh.tf = tf
+   source.tfh.bounds = bounds
+
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   sc.save('rendering.png', sigma_clip=6)
+   
+
+add_gaussian
+""""""""""""
+
+If you would like to add a gaussian with a customized color or no color, use
+:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.add_gaussian`.
+
+.. python-script::
+
+   import numpy as np
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   source = sc[0]
+
+   source.set_field('density')
+   source.set_log(True)
+
+   bounds = (3e-31, 5e-27)
+
+   # Since this rendering is done in log space, the transfer function needs
+   # to be specified in log space.
+   tf = yt.ColorTransferFunction(np.log10(bounds))
+
+   tf.add_gaussian(np.log10(1e-29), width=.005, height=[0.753, 1.0, 0.933, 1.0])
+
+   source.tfh.tf = tf
+   source.tfh.bounds = bounds
+
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   sc.save('rendering.png', sigma_clip=6)
+
+
+map_to_colormap
+"""""""""""""""
+
+Finally, to map a colormap directly to a range in densities use
+:meth:`~yt.visualization.volume_rendering.transfer_functions.ColorTransferFunction.map_to_colormap`. This
+makes it possible to map a segment of the transfer function space to a colormap
+at a single alpha value. Where the above options produced layered volume
+renderings, this allows all of the density values in a dataset to contribute to
+the volume rendering.
+
+.. python-script::
+
+   import numpy as np
+   import yt
+
+   ds = yt.load('Enzo_64/DD0043/data0043')
+
+   sc = yt.create_scene(ds, lens_type='perspective')
+
+   source = sc[0]
+
+   source.set_field('density')
+   source.set_log(True)
+
+   bounds = (3e-31, 5e-27)
+
+   # Since this rendering is done in log space, the transfer function needs
+   # to be specified in log space.
+   tf = yt.ColorTransferFunction(np.log10(bounds))
+
+   def linramp(vals, minval, maxval):
+       return (vals - vals.min())/(vals.max() - vals.min())
+
+   tf.map_to_colormap(np.log10(3e-31), np.log10(5e-27), colormap='arbre', 
+                      scale_func=linramp)
+
+   source.tfh.tf = tf
+   source.tfh.bounds = bounds
+
+   source.tfh.plot('transfer_function.png', profile_field='density')
+
+   sc.save('rendering.png', sigma_clip=6)   
 
 Projection Transfer Function
 ++++++++++++++++++++++++++++
@@ -351,8 +560,8 @@
              information can be hard.  We've provided information about best
              practices and tried to make the interface easy to develop nice
              visualizations, but getting them *just right* is often
-             time-consuming.  It's usually best to start out simple with the
-             built-in helper interface, and expand on that as you need.
+             time-consuming.  It's usually best to start out simple and expand 
+             and tweak as needed.
 
 The scene interface provides a modular interface for creating renderings
 of arbitrary data sources. As such, manual composition of a scene can require
@@ -412,6 +621,12 @@
   # sc is an instance of a Scene object, which allows you to further refine
   # your renderings and later save them.
 
+  # Let's zoom in and take a closer look
+  sc.camera.width = (300, 'kpc')
+  sc.camera.switch_orientation()
+
+  # Save the zoomed in rendering
+  sc.save('zoomed_rendering.png')
 
 Alternatively, if you don't want to immediately generate an image of your
 volume rendering, and you just want access to the default scene object,
@@ -423,12 +638,39 @@
 
 .. python-script::
 
-  import yt
-  ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030")
-  sc = yt.create_scene(ds, 'density')
+    import numpy as np
+    import yt
+  
 
+    ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030")
+    sc = yt.create_scene(ds, 'density')
 
-For a more in-depth tutorial on how to create a Scene and modify its contents,
+    source = sc[0]
+
+    source.transfer_function = yt.ColorTransferFunction(
+        np.log10((1e-30, 1e-23)), grey_opacity=True)
+
+    def linramp(vals, minval, maxval):
+        return (vals - vals.min())/(vals.max() - vals.min())
+
+    source.transfer_function.map_to_colormap(
+        np.log10(1e-25), np.log10(8e-24), colormap='arbre', scale_func=linramp)
+
+    # For this low resolution dataset it's very important to use interpolated
+    # vertex centered data to avoid artifacts. For high resolution data this
+    # setting may cause a substantial slowdown for marginal visual improvement.
+    source.set_use_ghost_zones(True)
+
+    cam = sc.camera
+
+    cam.width = 15*yt.units.kpc
+    cam.focus = ds.domain_center
+    cam.normal_vector = [-0.3, -0.3, 1]
+    cam.switch_orientation()
+
+    sc.save('rendering.png')
+
+For an in-depth tutorial on how to create a Scene and modify its contents,
 see this annotated :ref:`volume-rendering-tutorial`.
 
 
@@ -574,14 +816,14 @@
 ^^^^^^^
 
 There are currently two models for opacity when rendering a volume, which are
-controlled in the ColorTransferFunction with the keyword
-grey_opacity=False(default)/True. The first (default) will act such for each of
-the r,g,b channels, each channel is only opaque to itself.  This means that if
-a ray that has some amount of red then encounters material that emits blue, the
-red will still exist and in the end that pixel will be a combination of blue
-and red.  However, if the ColorTransferFunction is set up with
-grey_opacity=True, then blue will be opaque to red, and only the blue emission
-will remain.
+controlled in the ``ColorTransferFunction`` with the keyword
+``grey_opacity=False`` or ``True`` (the default). The first will act such for
+each of the red, green, and blue channels, each channel is only opaque to
+itself.  This means that if a ray that has some amount of red then encounters
+material that emits blue, the red will still exist and in the end that pixel
+will be a combination of blue and red.  However, if the ColorTransferFunction is
+set up with grey_opacity=True, then blue will be opaque to red, and only the
+blue emission will remain.
 
 For an in-depth example, please see the cookbook example on opaque renders here:
 :ref:`cookbook-opaque_rendering`.

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/utilities/amr_kdtree/amr_kdtree.py
--- a/yt/utilities/amr_kdtree/amr_kdtree.py
+++ b/yt/utilities/amr_kdtree/amr_kdtree.py
@@ -17,7 +17,9 @@
 import operator
 import numpy as np
 
-from yt.funcs import mylog
+from yt.funcs import \
+    iterable, \
+    mylog
 from yt.utilities.on_demand_imports import _h5py as h5py
 from yt.utilities.amr_kdtree.amr_kdtools import \
     receive_and_reduce, \
@@ -174,14 +176,17 @@
         regenerate_data = self.fields is None or \
                           len(self.fields) != len(new_fields) or \
                           self.fields != new_fields or force
+        if not iterable(log_fields):
+            log_fields = [log_fields]
+        new_log_fields = list(log_fields)
         self.tree.trunk.set_dirty(regenerate_data)
         self.fields = new_fields
 
         if self.log_fields is not None and not regenerate_data:
-            flip_log = list(map(operator.ne, self.log_fields, log_fields))
+            flip_log = list(map(operator.ne, self.log_fields, new_log_fields))
         else:
-            flip_log = [False] * len(log_fields)
-        self.log_fields = log_fields
+            flip_log = [False] * len(new_log_fields)
+        self.log_fields = new_log_fields
 
         self.no_ghost = no_ghost
         del self.bricks, self.brick_dimensions
@@ -189,7 +194,7 @@
         bricks = []
 
         for b in self.traverse():
-            list(map(_apply_log, b.my_data, flip_log, log_fields))
+            list(map(_apply_log, b.my_data, flip_log, self.log_fields))
             bricks.append(b)
         self.bricks = np.array(bricks)
         self.brick_dimensions = np.array(self.brick_dimensions)

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/plot_window.py
--- a/yt/visualization/plot_window.py
+++ b/yt/visualization/plot_window.py
@@ -1533,6 +1533,8 @@
         self.fields = fields
         self.interpolated = interpolated
         self.resolution = resolution
+        if weight is not None:
+            weight = self.dd._determine_fields(weight)[0]
         self.weight_field = weight
         self.volume = volume
         self.no_ghost = no_ghost

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/off_axis_projection.py
--- a/yt/visualization/volume_rendering/off_axis_projection.py
+++ b/yt/visualization/volume_rendering/off_axis_projection.py
@@ -127,10 +127,8 @@
     funits = data_source.ds._get_field_info(item).units
 
     vol = VolumeSource(data_source, item)
-    ptf = ProjectionTransferFunction()
-    vol.set_transfer_function(ptf)
     if weight is None:
-        vol.set_fields([item])
+        vol.set_field(item)
     else:
         # This is a temporary field, which we will remove at the end.
         weightfield = ("index", "temp_weightfield")
@@ -147,7 +145,10 @@
         deps, _ = data_source.ds.field_info.check_derived_fields([weightfield])
         data_source.ds.field_dependencies.update(deps)
         fields = [weightfield, weight]
-        vol.set_fields(fields)
+        vol.set_field(weightfield)
+        vol.set_weight_field(weight)
+    ptf = ProjectionTransferFunction()
+    vol.set_transfer_function(ptf)
     camera = sc.add_camera(data_source)
     camera.set_width(width)
     if not iterable(resolution):
@@ -176,15 +177,11 @@
     vol.set_sampler(camera, interpolated=False)
     assert (vol.sampler is not None)
 
+    fields = [vol.field]
+    if vol.weight_field is not None:
+        fields.append(vol.weight_field)
+
     mylog.debug("Casting rays")
-    double_check = False
-    if double_check:
-        for brick in vol.volume.bricks:
-            for data in brick.my_data:
-                if np.any(np.isnan(data)):
-                    raise RuntimeError
-
-    fields = vol.field
 
     for i, (grid, mask) in enumerate(data_source.blocks):
         data = [(grid[f] * mask).astype("float64") for f in fields]

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/render_source.py
--- a/yt/visualization/volume_rendering/render_source.py
+++ b/yt/visualization/volume_rendering/render_source.py
@@ -12,6 +12,7 @@
 # -----------------------------------------------------------------------------
 
 import numpy as np
+from functools import wraps
 from yt.config import \
     ytcfg
 from yt.funcs import mylog, ensure_numpy_array
@@ -44,6 +45,37 @@
     ytcfg["yt", "ray_tracing_engine"] = "yt"
 
 
+def invalidate_volume(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        ret = f(*args, **kwargs)
+        obj = args[0]
+        if isinstance(obj._transfer_function, ProjectionTransferFunction):
+            obj.sampler_type = 'projection'
+            obj._log_field = False
+            obj._use_ghost_zones = False
+        del obj.volume
+        obj._volume_valid = False
+        return ret
+    return wrapper
+
+def validate_volume(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        obj = args[0]
+        fields = [obj.field]
+        log_fields = [obj.log_field]
+        if obj.weight_field is not None:
+            fields.append(obj.weight_field)
+            log_fields.append(obj.log_field)
+        if obj._volume_valid is False:
+            obj.volume.set_fields(fields, log_fields,
+                                  no_ghost=(not obj.use_ghost_zones))
+        obj._volume_valid = True
+        return f(*args, **kwargs)
+    return wrapper
+
+
 class RenderSource(ParallelAnalysisInterface):
 
     """Base Class for Render Sources.
@@ -94,9 +126,6 @@
         data object or dataset.
     fields : string
         The name of the field(s) to be rendered.
-    auto: bool, optional
-        If True, will build a default AMRKDTree and transfer function based
-        on the data.
 
     Examples
     --------
@@ -128,47 +157,157 @@
     _image = None
     data_source = None
 
-    def __init__(self, data_source, field, auto=True):
+    def __init__(self, data_source, field):
         r"""Initialize a new volumetric source for rendering."""
         super(VolumeSource, self).__init__()
         self.data_source = data_source_or_all(data_source)
         field = self.data_source._determine_fields(field)[0]
-        self.field = field
-        self.volume = None
         self.current_image = None
         self.check_nans = False
         self.num_threads = 0
         self.num_samples = 10
         self.sampler_type = 'volume-render'
 
-        # Error checking
-        assert(self.field is not None)
-        assert(self.data_source is not None)
+        self._volume_valid = False
 
-        # In the future these will merge
+        # these are caches for properties, defined below
+        self._volume = None
+        self._transfer_function = None
+        self._field = field
+        self._log_field = self.data_source.ds.field_info[field].take_log
+        self._use_ghost_zones = False
+        self._weight_field = None
+
+        self.tfh = TransferFunctionHelper(self.data_source.pf)
+        self.tfh.set_field(self.field)
+
+    @property
+    def transfer_function(self):
+        """The transfer function associated with this VolumeSource"""
+        if self._transfer_function is not None:
+            return self._transfer_function
+
+        if self.tfh.tf is not None:
+            self._transfer_function = self.tfh.tf
+            return self._transfer_function
+
+        mylog.info("Creating transfer function")
+        self.tfh.set_field(self.field)
+        self.tfh.set_log(self.log_field)
+        self.tfh.build_transfer_function()
+        self.tfh.setup_default()
+        self._transfer_function = self.tfh.tf
+
+        return self._transfer_function
+
+    @transfer_function.setter
+    def transfer_function(self, value):
+        self.tfh.tf = None
+        valid_types = (TransferFunction, ColorTransferFunction,
+                       ProjectionTransferFunction, type(None))
+        if not isinstance(value, valid_types):
+            raise RuntimeError("transfer_function not a valid type, "
+                               "received object of type %s" % type(value))
+        if isinstance(value, ProjectionTransferFunction):
+            self.sampler_type = 'projection'
+            if self._volume is not None:
+                fields = [self.field]
+                if self.weight_field is not None:
+                    fields.append(self.weight_field)
+                self._volume_valid = False
+        self._transfer_function = value
+
+    @property
+    def volume(self):
+        """The abstract volume associated with this VolumeSource
+
+        This object does the heavy lifting to access data in an efficient manner
+        using a KDTree
+        """
+        if self._volume is None:
+            mylog.info("Creating volume")
+            volume = AMRKDTree(self.data_source.ds, data_source=self.data_source)
+            self._volume = volume
+
+        return self._volume
+
+    @volume.setter
+    def volume(self, value):
+        assert(isinstance(value, AMRKDTree))
+        del self._volume
+        self._field = value.fields
+        self._log_field = value.log_fields
+        self._volume = value
+        self._volume_valid is True
+
+    @volume.deleter
+    def volume(self):
+        del self._volume
+        self._volume = None
+
+    @property
+    def field(self):
+        """The field to be rendered"""
+        return self._field
+
+    @field.setter
+    @invalidate_volume
+    def field(self, value):
+        field = self.data_source._determine_fields(value)
+        if len(field) > 1:
+            raise RuntimeError(
+                "VolumeSource.field can only be a single field but received "
+                "multiple fields: %s") % field
+        field = field[0]
+        if self._field != field:
+            log_field = self.data_source.ds.field_info[field].take_log
+            self.tfh.bounds = None
+        else:
+            log_field = self._log_field
+        self._log_field = log_field
+        self._field = value
         self.transfer_function = None
-        self.tfh = None
-        if auto:
-            self.build_defaults()
+        self.tfh.set_field(value)
+        self.tfh.set_log(log_field)
 
-    def build_defaults(self):
-        """Sets a default volume and transfer function"""
-        mylog.info("Creating default volume")
-        self.build_default_volume()
-        mylog.info("Creating default transfer function")
-        self.build_default_transfer_function()
+    @property
+    def log_field(self):
+        """Whether or not the field rendering is computed in log space"""
+        return self._log_field
+
+    @log_field.setter
+    @invalidate_volume
+    def log_field(self, value):
+        self.transfer_function = None
+        self.tfh.set_log(value)
+        self._log_field = value
+
+    @property
+    def use_ghost_zones(self):
+        """Whether or not ghost zones are used to estimate vertex-centered data
+        values at grid boundaries"""
+        return self._use_ghost_zones
+
+    @use_ghost_zones.setter
+    @invalidate_volume
+    def use_ghost_zones(self, value):
+        self._use_ghost_zones = value
+
+    @property
+    def weight_field(self):
+        """The weight field for the rendering
+
+        Currently this is only used for off-axis projections.
+        """
+        return self._weight_field
+
+    @weight_field.setter
+    @invalidate_volume
+    def weight_field(self, value):
+        self._weight_field = value
 
     def set_transfer_function(self, transfer_function):
         """Set transfer function for this source"""
-        if not isinstance(transfer_function,
-                          (TransferFunction, ColorTransferFunction,
-                           ProjectionTransferFunction)):
-            raise RuntimeError("transfer_function not of correct type")
-        if isinstance(transfer_function, ProjectionTransferFunction):
-            self.sampler_type = 'projection'
-            self.volume.set_fields([self.field], log_fields=[False], 
-                                   no_ghost=True, force=True)
-
         self.transfer_function = transfer_function
         return self
 
@@ -177,53 +316,71 @@
         if self.data_source is None:
             raise RuntimeError("Data source not initialized")
 
-        if self.volume is None:
-            raise RuntimeError("Volume not initialized")
-
-        if self.transfer_function is None:
-            raise RuntimeError("Transfer Function not Supplied")
-
-    def build_default_transfer_function(self):
-        """Sets up a transfer function"""
-        self.tfh = \
-            TransferFunctionHelper(self.data_source.pf)
-        self.tfh.set_field(self.field)
-        self.tfh.build_transfer_function()
-        self.tfh.setup_default()
-        self.transfer_function = self.tfh.tf
-
-    def build_default_volume(self):
-        """Sets up an AMRKDTree based on the VolumeSource's field"""
-        self.volume = AMRKDTree(self.data_source.pf,
-                                data_source=self.data_source)
-        log_fields = [self.data_source.pf.field_info[self.field].take_log]
-        mylog.debug('Log Fields:' + str(log_fields))
-        self.volume.set_fields([self.field], log_fields, True)
-
     def set_volume(self, volume):
         """Associates an AMRKDTree with the VolumeSource"""
-        assert(isinstance(volume, AMRKDTree))
-        del self.volume
         self.volume = volume
+        return self
 
-    def set_fields(self, fields, no_ghost=True):
-        """Set the source's fields to render
+    def set_field(self, field):
+        """Set the source's field to render
 
         Parameters
         ----------
 
-        fields: field name or list of field names
-            The field or fields to render
-        no_ghost: boolean
-            If False, the AMRKDTree estimates vertex centered data using ghost
+        field: field name
+            The field to render
+        """
+        self.field = field
+        return self
+
+    def set_log(self, log_field):
+        """Set whether the rendering of the source's field is done in log space
+
+        Generally volume renderings of data whose values span a large dynamic
+        range should be done on log space and volume renderings of data with
+        small dynamic range should be done in linear space.
+
+        Parameters
+        ----------
+
+        log_field: boolean
+            If True, the volume rendering will be done in log space, and if False
+            will be done in linear space.
+        """
+        self.log_field = log_field
+        return self
+
+    def set_weight_field(self, weight_field):
+        """Set the source's weight field
+
+        .. note::
+
+          This is currently only used for renderings using the
+          ProjectionTransferFunction
+
+        Parameters
+        ----------
+
+        weight_field: field name
+            The weight field to use in the rendering
+        """
+        self.weight_field = weight_field
+        return self
+
+    def set_use_ghost_zones(self, use_ghost_zones):
+        """Set whether or not interpolation at grid edges uses ghost zones
+
+        Parameters
+        ----------
+
+        use_ghost_zones: boolean
+            If True, the AMRKDTree estimates vertex centered data using ghost
             zones, which can eliminate seams in the resulting volume rendering.
-            Defaults to True for performance reasons.
+            Defaults to False for performance reasons.
+
         """
-        fields = self.data_source._determine_fields(fields)
-        log_fields = [self.data_source.ds.field_info[f].take_log
-                      for f in fields]
-        self.volume.set_fields(fields, log_fields, no_ghost)
-        self.field = fields
+        self.use_ghost_zones = use_ghost_zones
+        return self
 
     def set_sampler(self, camera, interpolated=True):
         """Sets a volume render sampler
@@ -233,10 +390,10 @@
         sampler types are supported.
 
         The 'interpolated' argument is only meaningful for projections. If True,
-        the data is first interpolated to the cell vertices, and then tri-linearly
-        interpolated to the ray sampling positions. If False, then the cell-centered
-        data is simply accumulated along the ray. Interpolation is always performed
-        for volume renderings.
+        the data is first interpolated to the cell vertices, and then
+        tri-linearly interpolated to the ray sampling positions. If False, then
+        the cell-centered data is simply accumulated along the
+        ray. Interpolation is always performed for volume renderings.
 
         """
         if self.sampler_type == 'volume-render':
@@ -250,6 +407,7 @@
         self.sampler = sampler
         assert(self.sampler is not None)
 
+    @validate_volume
     def render(self, camera, zbuffer=None):
         """Renders an image using the provided camera
 
@@ -308,7 +466,8 @@
             Whether or not this is being called from a higher level in the VR
             interface. Used to set the correct orientation.
         """
-        image = self.volume.reduce_tree_images(image, camera.lens.viewpoint)
+        if self._volume is not None:
+            image = self.volume.reduce_tree_images(image, camera.lens.viewpoint)
         image.shape = camera.resolution[0], camera.resolution[1], 4
         # If the call is from VR, the image is rotated by 180 to get correct
         # up direction
@@ -320,7 +479,7 @@
 
     def __repr__(self):
         disp = "<Volume Source>:%s " % str(self.data_source)
-        disp += "transfer_function:%s" % str(self.transfer_function)
+        disp += "transfer_function:%s" % str(self._transfer_function)
         return disp
 
 

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/scene.py
--- a/yt/visualization/volume_rendering/scene.py
+++ b/yt/visualization/volume_rendering/scene.py
@@ -292,8 +292,7 @@
             suffix = '.png'
             fname = '%s%s' % (fname, suffix)
 
-        if self._last_render is None:
-            self.render()
+        self.render()
 
         mylog.info("Saving render %s", fname)
         self._last_render.write_png(fname, sigma_clip=sigma_clip)
@@ -380,8 +379,7 @@
             suffix = '.png'
             fname = '%s%s' % (fname, suffix)
 
-        if self._last_render is None:
-            self.render()
+        self.render()
 
         # which transfer function?
         rs = rensources[0]

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/tests/test_varia.py
--- a/yt/visualization/volume_rendering/tests/test_varia.py
+++ b/yt/visualization/volume_rendering/tests/test_varia.py
@@ -18,6 +18,8 @@
 import yt
 from yt.testing import \
     fake_random_ds
+from yt.visualization.volume_rendering.render_source import VolumeSource
+from yt.visualization.volume_rendering.scene import Scene
 from unittest import TestCase
 
 def setup():
@@ -62,7 +64,8 @@
         im, sc = yt.volume_render(self.ds)
 
         volume_source = sc.get_source(0)
-        volume_source.set_fields([('gas', 'velocity_x'), ('gas', 'density')])
+        volume_source.set_field(('gas', 'velocity_x'))
+        volume_source.set_weight_field(('gas', 'density'))
         sc.render()
 
     def test_rotation_volume_rendering(self):
@@ -76,3 +79,70 @@
 
     def test_simple_volume_rendering(self):
         im, sc = yt.volume_render(self.ds, sigma_clip=4.0)
+
+    def test_lazy_volume_source_construction(self):
+        sc = Scene()
+        source = VolumeSource(self.ds.all_data(), 'density')
+
+        assert source._volume is None
+        assert source._transfer_function is None
+
+        source.tfh.bounds = (0.1, 1)
+
+        source.set_log(False)
+
+        assert source.log_field is False
+        assert source.transfer_function.x_bounds == [0.1, 1]
+        assert source._volume is None
+
+        source.set_log(True)
+
+        assert source.log_field is True
+        assert source.transfer_function.x_bounds == [-1, 0]
+        assert source._volume is None
+
+        source.transfer_function = None
+        source.tfh.bounds = None
+
+        ad = self.ds.all_data()
+
+        assert source.transfer_function.x_bounds == \
+            list(np.log10(ad.quantities.extrema('density')))
+        assert source.tfh.log == source.log_field
+
+        source.set_field('velocity_x')
+        source.set_log(False)
+
+        assert source.transfer_function.x_bounds == \
+            list(ad.quantities.extrema('velocity_x'))
+        assert source._volume is None
+
+        source.set_field('density')
+
+        assert source.volume is not None
+        assert source.volume._initialized is False
+        assert source.volume.fields is None
+
+        del source.volume
+        assert source._volume is None
+
+        sc.add_source(source)
+
+        sc.add_camera()
+
+        sc.render()
+
+        assert source.volume is not None
+        assert source.volume._initialized is True
+        assert source.volume.fields == [('gas', 'density')]
+        assert source.volume.log_fields == [True]
+
+        source.set_field('velocity_x')
+        source.set_log(False)
+
+        sc.render()
+
+        assert source.volume is not None
+        assert source.volume._initialized is True
+        assert source.volume.fields == [('gas', 'velocity_x')]
+        assert source.volume.log_fields == [False]

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/transfer_function_helper.py
--- a/yt/visualization/volume_rendering/transfer_function_helper.py
+++ b/yt/visualization/volume_rendering/transfer_function_helper.py
@@ -83,8 +83,9 @@
         field: string
             The field to be rendered.
         """
+        if field != self.field:
+            self.log = self.ds._get_field_info(field).take_log
         self.field = field
-        self.log = self.ds._get_field_info(self.field).take_log
 
     def set_log(self, log):
         """
@@ -98,8 +99,6 @@
             Sets whether the transfer function should use log or linear space.
         """
         self.log = log
-        self.ds.index
-        self.ds._get_field_info(self.field).take_log = log
 
     def build_transfer_function(self):
         """
@@ -118,7 +117,7 @@
         """
         if self.bounds is None:
             mylog.info('Calculating data bounds. This may take a while.' +
-                       '  Set the .bounds to avoid this.')
+                       '  Set the TranferFunctionHelper.bounds to avoid this.')
             self.set_bounds()
 
         if self.log:
@@ -132,11 +131,13 @@
         return self.tf
 
     def setup_default(self):
-        """docstring for setup_default"""
-        if self.log:
-            mi, ma = np.log10(self.bounds[0]), np.log10(self.bounds[1])
-        else:
-            mi, ma = self.bounds
+        """Setup a default colormap
+
+        Creates a ColorTransferFunction including 10 gaussian layers whose
+        colors smaple the 'spectral' colormap. Also attempts to scale the
+        transfer function to produce a natural contrast ratio.
+
+        """
         self.tf.add_layers(10, colormap='spectral')
         factor = self.tf.funcs[-1].y.size / self.tf.funcs[-1].y.sum()
         self.tf.funcs[-1].y *= 2*factor
@@ -162,6 +163,7 @@
         from matplotlib.figure import Figure
         if self.tf is None:
             self.build_transfer_function()
+            self.setup_default()
         tf = self.tf
         if self.log:
             xfunc = np.logspace

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/transfer_functions.py
--- a/yt/visualization/volume_rendering/transfer_functions.py
+++ b/yt/visualization/volume_rendering/transfer_functions.py
@@ -708,7 +708,7 @@
         --------
 
         >>> def linramp(vals, minval, maxval):
-        ...     return (vals - vals.min())/(vals.(max) - vals.min())
+        ...     return (vals - vals.min())/(vals.max() - vals.min())
         >>> tf = ColorTransferFunction( (-10.0, -5.0) )
         >>> tf.map_to_colormap(-8.0, -6.0, scale=10.0, colormap='arbre')
         >>> tf.map_to_colormap(-6.0, -5.0, scale=10.0, colormap='arbre',

diff -r b58837b96e9d93e200ec564f11db913ec4c515e0 -r e7aabcd611651728be2dca5e817883f99c75a4a5 yt/visualization/volume_rendering/utils.py
--- a/yt/visualization/volume_rendering/utils.py
+++ b/yt/visualization/volume_rendering/utils.py
@@ -1,4 +1,6 @@
 import numpy as np
+from yt.data_objects.data_containers import \
+    YTSelectionContainer3D
 from yt.data_objects.static_output import Dataset
 from yt.utilities.lib import bounding_volume_hierarchy
 from yt.utilities.lib.grid_traversal import \
@@ -13,6 +15,11 @@
 def data_source_or_all(data_source):
     if isinstance(data_source, Dataset):
         data_source = data_source.all_data()
+    if not isinstance(data_source, (YTSelectionContainer3D, type(None))):
+        raise RuntimeError(
+            "The data_source is not a valid 3D data container.\n"
+            "Expected an ojbect of type YTSelectionContainer3D but received "
+            "an object of type %s." % type(data_source))
     return data_source

Repository URL: https://bitbucket.org/yt_analysis/yt/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.



More information about the yt-svn mailing list