Repository: wright-group/WrightTools

Issue:
max and min methods



Discussion:
in the interest of quacking like an ndarray I vote we remove the @property decorators from max and min

as long as we're at it, mag and null should also lose @property for the sake of consistency

yes, this will change the API, which is regrettable

@ksunden your thoughts?
I think this is a reasonable concession.

Is this not a revert to previous behavior (well, similar to zmin/zmax, etc.) Or were those always @property?
actually they were always simple attributes

Resolving patch:
diff --git a/WrightTools/artists.py b/WrightTools/artists.py
--- a/WrightTools/artists.py
+++ b/WrightTools/artists.py
@@ -201,10 +201,10 @@ def plot_data(self, data, channel=0, interpolate=False, coloring=None,
         xaxis = data.axes[0]
         # get min
         if min is None:
-            min = channel.min
+            min = channel.min()
         # get max
         if max is None:
-            max = channel.max
+            max = channel.max()
         # 1D --------------------------------------------------------------------------------------
         if data.dimensionality == 1:
             # get list of all datas
@@ -1703,7 +1703,7 @@ def plot(self, channel=0, local=False, autosave=False, output_folder=None,
             if local:
                 pass
             else:
-                plt.ylim(channels[channel_index].min, channels[channel_index].max)
+                plt.ylim(channels[channel_index].min(), channels[channel_index].max())
             # label axes
             plt.xlabel(axes[0].get_label(), fontsize=18)
             plt.ylabel(channels[channel_index].name, fontsize=18)
@@ -1950,24 +1950,22 @@ def plot(self, channel=0,
             if normalize_slices == 'both':
                 pass
             elif normalize_slices == 'horizontal':
-                nmin = channel.null
+                nmin = channel.null()
                 # normalize all x traces to a common value
                 maxes = zi.max(axis=1)
                 numerator = (zi - nmin)
                 denominator = (maxes - nmin)
                 for j in range(zi.shape[0]):
                     zi[j] = numerator[j] / denominator[j]
-                channel.max = zi.max()
-                channel.min = zi.min()
-                channel.null = 0
+                channel._null = 0
             elif normalize_slices == 'vertical':
-                nmin = channel.null
+                nmin = channel.null()
                 maxes = zi.max(axis=0)
                 numerator = (zi - nmin)
                 denominator = (maxes - nmin)
                 for j in range(zi.shape[1]):
                     zi[:, j] = numerator[:, j] / denominator[j]
-                channel.null = 0
+                channel._null = 0
             # create figure -----------------------------------------------------------------------
             if fig and autosave:
                 plt.close(fig)
@@ -1999,27 +1997,27 @@ def plot(self, channel=0,
             if channel.signed:
                 if local:
                     print('signed local')
-                    limit = max(abs(channel.null - np.nanmin(zi)),
-                                abs(channel.null - np.nanmax(zi)))
+                    limit = max(abs(channel.null() - np.nanmin(zi)),
+                                abs(channel.null() - np.nanmax(zi)))
                 else:
                     if dynamic_range:
-                        limit = min(abs(channel.null - channel.min),
-                                    abs(channel.null - channel.max))
+                        limit = min(abs(channel.null() - channel.min()),
+                                    abs(channel.null() - channel.max()))
                     else:
-                        limit = channel.mag
+                        limit = channel.mag()
                 if np.isnan(limit):
                     limit = 1.
                 if limit is np.ma.masked:
                     limit = 1.
-                levels = np.linspace(-limit + channel.null, limit + channel.null, 200)
+                levels = np.linspace(-limit + channel.null(), limit + channel.null(), 200)
             else:
                 if local:
-                    levels = np.linspace(channel.null, np.nanmax(zi), 200)
+                    levels = np.linspace(channel.null(), np.nanmax(zi), 200)
                 else:
-                    if channel.max < channel.null:
-                        levels = np.linspace(channel.min, channel.null, 200)
+                    if channel.max() < channel.null():
+                        levels = np.linspace(channel.min(), channel.null(), 200)
                     else:
-                        levels = np.linspace(channel.null, channel.max, 200)
+                        levels = np.linspace(channel.null(), channel.max(), 200)
             # main plot ---------------------------------------------------------------------------
             # get colormap
             if cmap == 'automatic':
@@ -2095,7 +2093,7 @@ def plot(self, channel=0,
                     # force top and bottom contour to be just outside of data range
                     # add two contours
                     contours_levels = np.linspace(
-                        channel.null - 1e-10, np.nanmax(zi) + 1e-10, contours + 2)
+                        channel.null() - 1e-10, np.nanmax(zi) + 1e-10, contours + 2)
                 else:
                     contours_levels = contours
                 if contour_thickness is None:
@@ -2127,7 +2125,7 @@ def plot(self, channel=0,
                     axCorrx.set_ylim([0, 1.1])
                 # bin
                 if xbin:
-                    x_ax_int = np.nansum(zi, axis=0) - channel.null * len(self.yaxis.points)
+                    x_ax_int = np.nansum(zi, axis=0) - channel.null() * len(self.yaxis.points)
                     x_ax_int[x_ax_int == 0] = np.nan
                     # normalize (min is a pixel)
                     xmax = max(np.abs(x_ax_int))
@@ -2166,7 +2164,7 @@ def plot(self, channel=0,
                     axCorry.set_xlim([0, 1.1])
                 # bin
                 if ybin:
-                    y_ax_int = np.nansum(zi, axis=1) - channel.null * len(self.xaxis.points)
+                    y_ax_int = np.nansum(zi, axis=1) - channel.null() * len(self.xaxis.points)
                     y_ax_int[y_ax_int == 0] = np.nan
                     # normalize (min is a pixel)
                     ymax = max(np.abs(y_ax_int))
@@ -2439,22 +2437,6 @@ def plot(self, channel_index=0,
             subplot_main = plt.subplot(gs[0])
             subplot_main.patch.set_facecolor(facecolor)
             # levels ------------------------------------------------------------------------------
-            """
-            if channel.signed:
-
-                if dynamic_range:
-                    limit = min(abs(channel.null - channel.min), abs(channel.null - channel.max))
-                else:
-                    limit = max(abs(channel.null - channel.min), abs(channel.null - channel.max))
-                levels = np.linspace(-limit + channel.null, limit + channel.null, 200)
-
-            else:
-
-                if local:
-                    levels = np.linspace(channel.null, zi.max(), 200)
-                else:
-                    levels = np.linspace(channel.null, channel.max, 200)
-            """
             levels = np.linspace(0, 1, 200)
             # main plot ---------------------------------------------------------------------------
             # get colormap
@@ -2509,7 +2491,7 @@ def plot(self, channel_index=0,
                         # force top and bottom contour to be just outside of data range
                         # add two contours
                         contours_levels = np.linspace(
-                            channel.null - 1e-10, np.nanmax(zi) + 1e-10, contours + 2)
+                            channel.null() - 1e-10, np.nanmax(zi) + 1e-10, contours + 2)
                     else:
                         contours_levels = contours
                     plt.contour(xaxis.points, yaxis.points, zi,
@@ -2731,11 +2713,11 @@ def plot(self, channel=0, output_path=None, w1w2=True, w1_wigner=True,
                     for data_index in range(len(self.datas)):
                         data = self.chopped_datas[data_index][slice_index]
                         if self.data_signed:
-                            global_limits = [self.datas[data_index].channels[channel_index].min,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].min(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         else:
-                            global_limits = [self.datas[data_index].channels[channel_index].null,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].null(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         axs, spss = self._fill_row(
                             data, channel_index, gs, data_index, global_limits)
                         if not data_index == len(self.datas) - 1:
@@ -2757,11 +2739,11 @@ def plot(self, channel=0, output_path=None, w1w2=True, w1_wigner=True,
                     for data_index in range(len(self.datas)):
                         data = self.chopped_datas[data_index][slice_index]
                         if self.data_signed:
-                            global_limits = [self.datas[data_index].channels[channel_index].min,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].min(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         else:
-                            global_limits = [self.datas[data_index].channels[channel_index].null,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].null(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         axs, spss = self._fill_row(
                             data, channel_index, gs, data_index, global_limits)
                         if not data_index == len(self.datas) - 1:
@@ -2788,11 +2770,11 @@ def plot(self, channel=0, output_path=None, w1w2=True, w1_wigner=True,
                     for data_index in range(len(self.datas)):
                         data = self.chopped_datas[data_index][slice_index]
                         if self.data_signed:
-                            global_limits = [self.datas[data_index].channels[channel_index].min,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].min(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         else:
-                            global_limits = [self.datas[data_index].channels[channel_index].null,
-                                             self.datas[data_index].channels[channel_index].max]
+                            global_limits = [self.datas[data_index].channels[channel_index].null(),
+                                             self.datas[data_index].channels[channel_index].max()]
                         axs, spss = self._fill_row(
                             data, channel_index, gs, data_index, global_limits)
                         if not data_index == len(self.datas) - 1:
diff --git a/WrightTools/data.py b/WrightTools/data.py
--- a/WrightTools/data.py
+++ b/WrightTools/data.py
@@ -245,7 +245,7 @@ def __init__(self, values, units=None, file_idx=None, null=None, signed=None, na
         if values is not None:
             self.give_values(np.asarray(values), null, signed)
         else:
-            self.null = null
+            self._null = null
             self.signed = signed
 
     def __repr__(self):
@@ -277,9 +277,9 @@ def clip(self, min=None, max=None, replace='nan'):
         """
         # decide what min and max will actually be
         if max is None:
-            max = self.max
+            max = self.max()
         if min is None:
-            min = self.min
+            min = self.min()
         # replace values
         if replace == 'val':
             self.values.clip(min, max, out=self.values)
@@ -306,25 +306,25 @@ def give_values(self, values, null=None, signed=None):
         self.values = values
         # null
         if null is not None:
-            self.null = null
-        elif hasattr(self, 'null'):
-            if self.null:
+            self._null = null
+        elif hasattr(self, '_null'):
+            if self._null:
                 pass
             else:
-                self.null = 0.
+                self._null = 0.
         else:
-            self.null = 0.
+            self._null = 0.
         # signed
         if signed is not None:
             self.signed = signed
         elif hasattr(self, 'signed'):
             if self.signed is None:
-                if self.min < self.null:
+                if self.min() < self.null():
                     self.signed = True
                 else:
                     self.signed = False
         else:
-            if self.min < self.null:
+            if self.min() < self.null():
                 self.signed = True
             else:
                 self.signed = False
@@ -335,9 +335,9 @@ def info(self):
         info = collections.OrderedDict()
         info['name'] = self.name
         info['id'] = id(self)
-        info['min'] = self.min
-        info['max'] = self.max
-        info['null'] = self.null
+        info['min'] = self.min()
+        info['max'] = self.max()
+        info['null'] = self.null()
         info['signed'] = self.signed
         return info
 
@@ -345,17 +345,14 @@ def invert(self):
         """Invert channel values."""
         self.values = - self.values
 
-    @property
     def mag(self):
         """Channel magnitude (maximum deviation from null)."""
-        return max((self.max - self.null, self.null - self.min))
+        return max((self.max() - self.null(), self.null() - self.min()))
 
-    @property
     def max(self):
         """Maximum, ignorning nans."""
         return np.nanmax(self.values)
 
-    @property
     def min(self):
         """Minimum, ignoring nans."""
         return np.nanmin(self.values)
@@ -369,8 +366,8 @@ def normalize(self, axis=None):
             else:  # presumably a simple number
                 axis = int(axis)
         # subtract off null
-        self.values -= self.null
-        self.null = 0.
+        self.values -= self.null()
+        self._null = 0.
         # create dummy array
         dummy = self.values.copy()
         dummy[np.isnan(dummy)] = 0  # nans are propagated in np.amax
@@ -381,6 +378,10 @@ def normalize(self, axis=None):
         # finish
         self._update()
 
+    def null(self):
+        """Null value."""
+        return self._null
+
     def trim(self, neighborhood, method='ztest', factor=3, replace='nan',
              verbose=True):
         """Remove outliers from the dataset.
@@ -487,7 +488,7 @@ def znull(self):
         """Channel null."""
         message = "use null, not znull"
         warnings.warn(message, wt_exceptions.VisibleDeprecationWarning)
-        return self.null
+        return self.null()
 
 
 class Data:
@@ -1151,7 +1152,7 @@ def level(self, channel, axis, npts, verbose=True):
         values = values.transpose(transpose_order)
         # return
         channel.values = values
-        channel.null = 0.
+        channel._null = 0.
         # print
         if verbose:
             axis = self.axes[axis_index]
@@ -1561,8 +1562,7 @@ def save(self, filepath=None, verbose=True):
         return filepath
 
     def scale(self, channel=0, kind='amplitude', verbose=True):
-        """
-        Scale a channel.
+        """Scale a channel.
 
         Parameters
         ----------
@@ -2490,8 +2490,6 @@ def from_KENT(filepaths, null=None, name=None, ignore=['wm'], use_norm=False,
         #data_norm = data.channels[0].values*data.axes[0].points*data.axes[1].points/(OPA1*OPA2)
         data_norm = data.channels[0].values / (OPA1 * OPA2)  # I think this is correct.
         data.channels[0].values = data_norm
-        data.channels[0].max = data_norm.max()
-        data.channels[0].min = data_norm.min()
     # return --------------------------------------------------------------------------------------
     if verbose:
         print('data object succesfully created')
@@ -3029,7 +3027,7 @@ def join(datas, method='first', verbose=True, **kwargs):
             percent_nan = np.around(100. * (np.isnan(channel.values).sum() /
                                             float(channel.values.size)), decimals=2)
             print('    {0} : {1} to {2} ({3}% NaN)'.format(
-                channel.name, channel.min, channel.max, percent_nan))
+                channel.name, channel.min(), channel.max(), percent_nan))
     return out
 
 
diff --git a/WrightTools/fit.py b/WrightTools/fit.py
--- a/WrightTools/fit.py
+++ b/WrightTools/fit.py
@@ -740,8 +740,7 @@ def run(self, channel=0, verbose=True):
             # give the data all at once
             channel = self.outs.channels[i]
             values = channel.values
-            channel.null = 0
-        self.outs._update()
+            channel._null = 0
         return self.outs
 
 


Test patch: