Repository: bids-standard/pybids

Issue:
Scale transformation shouldn't return NAs in event of constant input
If constant input is passed to the `Scale` transformation, a column of N/A values is returned. This should probably just fail with an exception (or at minimum, return a column of `0`).

Discussion:

Patch:
diff --git a/bids/analysis/transformations/compute.py b/bids/analysis/transformations/compute.py
index 80f6232f..4c06a354 100644
--- a/bids/analysis/transformations/compute.py
+++ b/bids/analysis/transformations/compute.py
@@ -138,9 +138,19 @@ class Scale(Transformation):
         None, no replacement is performed. If 'before', missing values are
         replaced with 0's before scaling. If 'after', missing values are
         replaced with 0 after scaling.
+
+    Notes
+    -----
+    If a constant column is passed in, and replace_na is None or 'before', an
+    exception will be raised.
     """
 
     def _transform(self, data, demean=True, rescale=True, replace_na=None):
+        if data.nunique() == 1 and replace_na in {None, 'before'}:
+            val = data.unique()[0]
+            raise ValueError("Cannot scale a column with constant value ({})! "
+                             "If you want a constant column of 0's returned, "
+                             "set replace_na to 'after'.".format(val))
         if replace_na == 'before':
             data = data.fillna(0.)
         if demean:
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index 35cb0f61..7ac52f6b 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -897,17 +897,27 @@ class BIDSLayout(object):
         -------
         list of :obj:`bids.layout.BIDSFile` or str
             A list of BIDSFiles (default) or strings (see return_type).
+
+        Notes
+        -----
+        * In pybids 0.9.0, the 'extensions' argument has been removed in
+          favor of the 'extension' entity.
         """
+        if 'extensions' in filters:
+            filters['extension'] = filters.pop('extensions')
+            warnings.warn("In pybids 0.9.0, the 'extensions' filter was "
+                          "deprecated in favor of 'extension'. The former will"
+                          " stop working in 0.11.0.", DeprecationWarning)
 
         layouts = self._get_layouts_in_scope(scope)
 
         entities = self.get_entities()
 
-        # Strip leading periods if extensions were passed
+        # For consistency with past versions where "extensions" was a
+        # hard-coded argument, allow leading periods
         if 'extension' in filters:
             exts = listify(filters['extension'])
-            filters['extension'] = [x.lstrip('.') if isinstance(x, str) else x
-                                    for x in exts]
+            filters['extension'] = [x.lstrip('.') for x in exts]
 
         if drop_invalid_filters:
             invalid_filters = set(filters.keys()) - set(entities.keys())


Test patch:
diff --git a/bids/analysis/tests/test_transformations.py b/bids/analysis/tests/test_transformations.py
index cc3126dd..63eecaa3 100644
--- a/bids/analysis/tests/test_transformations.py
+++ b/bids/analysis/tests/test_transformations.py
@@ -128,7 +128,7 @@ def test_sum(collection):
                       output='sum', weights=[1, 1, 1])
 
 
-def test_scale(collection):
+def test_scale(collection, sparse_run_variable_with_missing_values):
     transform.Scale(collection, variables=['RT', 'parametric gain'],
                     output=['RT_Z', 'gain_Z'], groupby=['run', 'subject'])
     groupby = collection['RT'].get_grouper(['run', 'subject'])
@@ -137,6 +137,16 @@ def test_scale(collection):
         groupby).apply(lambda x: (x - x.mean()) / x.std())
     assert np.allclose(z1, z2)
 
+    # Test constant input
+    coll = sparse_run_variable_with_missing_values
+    coll['var'].values.fillna(1)
+    with pytest.raises(ValueError, match="Cannot scale.*1.0"):
+        transform.Scale(coll, 'var').values
+    with pytest.raises(ValueError, match="Cannot scale.*1.0"):
+        transform.Scale(coll, 'var', replace_na='before').values
+    transform.Scale(coll, 'var', replace_na='after', output='zero')
+    assert coll['zero'].values.unique() == 0
+
 
 def test_demean(collection):
     transform.Demean(collection, variables=['RT'], output=['RT_dm'])
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index 229634d9..52647802 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -280,8 +280,7 @@ def test_get_val_none(layout_7t_trt, acq):
 
 def test_get_val_enum_any(layout_7t_trt):
     t1w_files = layout_7t_trt.get(
-        subject='01', ses='1', suffix='T1w', acquisition=Query.ANY,
-        extension=Query.ANY)
+        subject='01', ses='1', suffix='T1w', acquisition=Query.ANY)
     assert not t1w_files
     bold_files = layout_7t_trt.get(subject='01', ses='1', run=1, suffix='bold',
                                    acquisition=Query.ANY)