YTEP-0015: Transfer Function Refactor¶
Created: August 13, 2013 Author: Sam Skillman
This YTEP proposes a fundamental change in the way transfer functions are constructed, modified, and implemented. The overall goal is to decrease the overhead and difficulty in constructing high-quality transfer functions and displaying their current state to the user.
Status should be one of the following:
- In Progress
YTEPs do not need to pass through every stage.
- Transfer functions are currently:
- Fragile – to log/linear, ranges, field swapping
- Complex – must have prior knowledge of data to construct valid TF
- Difficult to Design – User must guess where interesting features will be.
The aim of this refactoring is to alleviate these three problems. To do so, we will implement several helper functions that are automate many of the actions that are commonly used during the design process of the transfer function. Several operations may be costly, and thus will not be done automatically but rather upon request by the user.
This splits up the TransferFunction into two pieces – the TransferFunction and TransferFunctionData. The former encompasses all the user-facing API in terms of designing and modifying a transfer function, and the latter contains the data needed by the volume renderer.
Suggested TF Structure:
class TransferFunctionData(object): """ Contains the data used by the Camera to actually do the volume rendering. Not accessed by the user in most circumstances. This contains most of what the TransferFunction used to be. """ class TransferFunction(object): def __init__(self, data_source=None): self.data_source = data_source self.pf = self.data_source.pf self.rgb_field = None self.bounds = None self.alpha_field = None self._valid = False self.transfer_function_data = None def smart_build(self): """ Automatically set up best guess bounds, and run initial 1D profiling of given field. We could make this as automatic or not as we want. """ pass def set_field(self, field): """ Sets the rgb channel to be linked to a given field, invalidating the current profiles/ranges if different than current field. """ if field == self.rgb_field: return self._valid = False assert (field in self.pf.h.field_list) self.rgb_field = field def _get_field_bounds(self): return self.data_source.quantities['Extrema'](self.field) def set_bounds(self, bounds=None): if bounds is None: bounds = self._get_field_bounds() # Do error checking on log/linear state of rendering. self.bounds = bounds def _get_1D_field_histogram(self): """ Calculates 1D profile (in mass/volume/count) of current field to aid in placement of transfer function features. """ pass def plot/show/display(self): """ plots, shows, or displays current TF based on how the user is interacting with yt. This could save an image to tf_tmp.png, display in an interactive matplotlib backend, display in an IPython notebook, or directly interact with the user's visual cortex. """ # add in all the transfer function modifiers here (gaussians, layers, # ramps, map_to_colormap, etc.) def set_log(self, log=True): self.log = log def clear(self): """Clears out the channel values, but leaves the bounds intact""" pass def _get_tf_data(self): """ This is what the Cameras call to get the TF information. This does error checking to make sure the transfer function is valid.""" if not self._valid: # Rebuild TransferFunctionData pass return self.transfer_function_data
After this is implemented, the usage pattern I would see would be something like:
tf = TransferFunction(pf.h.all_data()) tf.set_field('Density') tf.smart_build() #<--- maybe another name like: auto_build or auto_awesome tf.display() #<--- Should we make this automatically display if possible? cam.set_transfer_function(tf) #<---- links a camera to this tf. # Alternatively we could have done tf = cam.transfer_function and modified # the camera's tf directly.' tf.set_log(True) # <--- invalidates the TF tf.do_whatever_modifications(...) cam.snapshot()
This change will break backwards compatibility with how old TransferFunctions were constructed.