Repository: bids-standard/pybids
Pull request #279 (2018-11-02 23:41:57)

WIP: Rename transformations to match BIDS-StatsModel spec
Attempt to sync up pybids and BIDS StatsModel document, with a focus on transformations. 

Here
- [x] Rename transformations with incorrect names (convolve_hrf, delete)
- [x] Sort transformations alphabetically
- [ ] Organize transformations into modules according to spec (arithmetic, logical, munging, computation, other)
- [ ] Add skeletons for transformations yet to be implemented (maybe implement some...)
- [x] CamelCase to snake_case convertion when loading model
- [ ] Other mismatches between new models and existing implementation?
- [x] Update models in tests

In Google Docs
- [x] Clean up transformations: CamelCase for all inputs, use similar wording when possible (e.g. Outputs), sort alphabetically and more misc editing
- [x] Use `loopable` whenever possible (some were not written to take multiple inputs)
- [x] Add skeleton for all transformations / fill description of most transformations


Patch:
diff --git a/bids/__init__.py b/bids/__init__.py
index 0e6dcd3e..ff5d0248 100644
--- a/bids/__init__.py
+++ b/bids/__init__.py
@@ -8,6 +8,7 @@ from .variables.io import load_variables
 
 __all__ = [
     "analysis",
+    "grabbids",
     "layout",
     "reports",
     "BIDSLayout",
diff --git a/bids/analysis/analysis.py b/bids/analysis/analysis.py
index 112190a9..32d3ceb0 100644
--- a/bids/analysis/analysis.py
+++ b/bids/analysis/analysis.py
@@ -1,6 +1,6 @@
 import json
 from bids.layout import BIDSLayout
-from bids.utils import matches_entities
+from bids.utils import matches_entities, convert_JSON
 from bids.variables import BIDSVariableCollection, merge_collections
 from . import transformations as transform
 from collections import namedtuple, OrderedDict
@@ -27,38 +27,44 @@ class Analysis(object):
             layout = BIDSLayout(layout)
         self.layout = layout
 
-        if isinstance(model, str):
-            model = json.load(open(model))
-        self.model = model
-
-        self._load_blocks(model['blocks'])
+        self._load_model(model)
 
     def __iter__(self):
-        for b in self.blocks:
+        for b in self.steps:
             yield b
 
     def __getitem__(self, index):
         if isinstance(index, int):
-            return self.blocks[index]
-        name_matches = list(filter(lambda x: x.name == index, self.blocks))
+            return self.steps[index]
+        level = index.lower()
+        name_matches = list(filter(lambda x: x.name == level, self.steps))
         if not name_matches:
             raise KeyError('There is no block with the name "%s".' % index)
         return name_matches[0]
 
-    def _load_blocks(self, blocks):
-        self.blocks = []
-        for i, block_args in enumerate(blocks):
-            block = Block(self.layout, index=i, **block_args)
-            self.blocks.append(block)
 
-    def setup(self, blocks=None, agg_func='mean', **kwargs):
-        ''' Set up the sequence of blocks for analysis.
+    def _load_model(self, model):
+        if isinstance(model, str):
+            model = json.load(open(model))
+
+        # Convert JSON from CamelCase to snake_case keys
+        self.model = convert_JSON(model)
+
+        steps = self.model['steps']
+        self.steps = []
+        for i, step_args in enumerate(steps):
+            step_args['level'] = step_args['level'].lower()
+            step = Step(self.layout, index=i, **step_args)
+            self.steps.append(step)
+
+    def setup(self, steps=None, agg_func='mean', **kwargs):
+        ''' Set up the sequence of steps for analysis.
 
         Args:
-            blocks (list): Optional list of blocks to set up. Each element
-                must be either an int giving the index of the block in the
+            steps (list): Optional list of steps to set up. Each element
+                must be either an int giving the index of the step in the
                 JSON config block list, or a str giving the (unique) name of
-                the block, as specified in the JSON config. Blocks that do not
+                the step, as specified in the JSON config. Steps that do not
                 match either index or name will be skipped.
             agg_func (str or Callable): The aggregation function to use when
                 combining rows from the previous level of analysis. E.g.,
@@ -77,17 +83,17 @@ class Analysis(object):
         selectors = self.model.get('input', {})
         selectors.update(kwargs)
 
-        for i, b in enumerate(self.blocks):
+        for i, b in enumerate(self.steps):
 
-            # Skip any blocks whose names or indexes don't match block list
-            if blocks is not None and i not in blocks and b.name not in blocks:
+            # Skip any steps whose names or indexes don't match block list
+            if steps is not None and i not in steps and b.name not in steps:
                 continue
 
             b.setup(input_nodes, **selectors)
             input_nodes = b.output_nodes
 
 
-class Block(object):
+class Step(object):
 
     ''' Represents a single analysis block from a BIDS-Model specification.
 
@@ -96,7 +102,7 @@ class Block(object):
         level (str): The BIDS keyword to use as the grouping variable; must be
             one of ['run', 'session', 'subject', or 'dataset'].
         index (int): The numerical index of the current Block within the
-            sequence of blocks.
+            sequence of steps.
         name (str): Optional name to assign to the block. Must be specified
             in order to enable name-based indexing in the parent Analysis.
         transformations (list): List of BIDS-Model transformations to apply.
@@ -117,7 +123,7 @@ class Block(object):
                 auto_contrasts=False):
 
         self.layout = layout
-        self.level = level
+        self.level = level.lower()
         self.index = index
         self.name = name
         self.transformations = transformations or []
@@ -188,11 +194,7 @@ class Block(object):
 
         # Set up and validate variable lists
         model = self.model or {}
-        variables = model.get('variables', [])
-        hrf_variables = model.get('HRF_variables', [])
-        if not set(variables) >= set(hrf_variables):
-            raise ValueError("HRF_variables must be a subset ",
-                                "of variables in BIDS model.")
+        X = model.get('X', [])
 
         for grp in groups:
             # Split into separate lists of Collections and Nodes
@@ -206,8 +208,8 @@ class Block(object):
             coll = merge_collections(colls) if len(colls) > 1 else colls[0]
 
             coll = apply_transformations(coll, self.transformations)
-            if variables:
-                transform.select(coll, variables)
+            if X:
+                transform.Select(coll, X)
 
             node = AnalysisNode(self.level, coll, self.contrasts, input_nodes,
                                 self.auto_contrasts)
@@ -301,7 +303,7 @@ class AnalysisNode(object):
 
     def __init__(self, level, collection, contrasts, input_nodes=None,
                  auto_contrasts=None):
-        self.level = level
+        self.level = level.lower()
         self.collection = collection
         self._block_contrasts = contrasts
         self.input_nodes = input_nodes
@@ -476,6 +478,6 @@ def apply_transformations(collection, transformations, select=None):
             func(collection, cols, **kwargs)
 
     if select is not None:
-        transform.select(collection, select)
+        transform.Select(collection, select)
 
     return collection
diff --git a/bids/analysis/auto_model.py b/bids/analysis/auto_model.py
index fea97fc0..8fe50e68 100644
--- a/bids/analysis/auto_model.py
+++ b/bids/analysis/auto_model.py
@@ -5,14 +5,14 @@ import numpy as np
 
 
 def _make_passthrough_contrast(level, contrast_names):
-    block = OrderedDict(level=level, name=level,
-                        model={'variables': contrast_names})
+    block = OrderedDict(Level=level, Name=level,
+                        Model={'X': contrast_names})
     contrasts = []
     for cn in contrast_names:
-        cdict = OrderedDict(name=level + "_" + cn, condition_list=[cn],
-                            weights=[1], type='T')
+        cdict = OrderedDict(Name=level.lower() + "_" + cn, ConditionList=[cn],
+                            Weights=[1], Type='T')
         contrasts.append(cdict)
-    block["contrasts"] = contrasts
+    block["Contrasts"] = contrasts
     return block
 
 
@@ -41,16 +41,16 @@ def auto_model(layout, scan_length=None, one_vs_rest=False):
     for task_name in tasks:
         # Populate model meta-data
         model = OrderedDict()
-        model["name"] = "_".join([base_name, task_name])
-        model["description"] = ("Autogenerated model for the %s task from %s" %
+        model["Name"] = "_".join([base_name, task_name])
+        model["Description"] = ("Autogenerated model for the %s task from %s" %
                                 (task_name, base_name))
-        model["input"] = {"task": task_name}
-        blocks = []
+        model["Input"] = {"Task": task_name}
+        steps = []
 
         # Make run level block
-        transformations = OrderedDict(name='factor', input=['trial_type'])
-        run = OrderedDict(level='run', name='run',
-                          transformations=[transformations])
+        transformations = OrderedDict(Name='Factor', Input=['trial_type'])
+        run = OrderedDict(Level='Run', Name='Run',
+                          Transformations=[transformations])
 
         # Get trial types
         run_nodes = load_variables(layout, task=task_name, levels=['run'],
@@ -62,9 +62,12 @@ def auto_model(layout, scan_length=None, one_vs_rest=False):
         trial_types = np.unique(evs)
         trial_type_factors = ["trial_type." + tt for tt in trial_types]
 
-        run_model = OrderedDict(HRF_variables=trial_type_factors,
-                                variables=trial_type_factors)
-        run["model"] = run_model
+        # Add HRF
+        run['Transformations'].append(
+                OrderedDict(Name='Convolve', Input=trial_type_factors))
+
+        run_model = OrderedDict(X=trial_type_factors)
+        run["Model"] = run_model
 
         if one_vs_rest:
             # if there are multiple trial types, build contrasts
@@ -72,10 +75,10 @@ def auto_model(layout, scan_length=None, one_vs_rest=False):
             for i, tt in enumerate(trial_types):
                 cdict = OrderedDict()
                 if len(trial_types) > 1:
-                    cdict["name"] = "run_" + tt + "_vs_others"
+                    cdict["Name"] = "run_" + tt + "_vs_others"
                 else:
-                    cdict["name"] = "run_" + tt
-                cdict["condition_list"] = trial_type_factors
+                    cdict["Name"] = "run_" + tt
+                cdict["ConditionList"] = trial_type_factors
 
                 # Calculate weights for contrast
                 weights = np.ones(len(trial_types))
@@ -83,13 +86,13 @@ def auto_model(layout, scan_length=None, one_vs_rest=False):
                     weights[trial_types != tt] = -1.0 / (len(trial_types) - 1)
                 except ZeroDivisionError:
                     pass
-                cdict["weights"] = list(weights)
+                cdict["Weights"] = list(weights)
 
-                cdict["type"] = "T"
+                cdict["Type"] = "T"
                 contrasts.append(cdict)
 
-            run["contrasts"] = contrasts
-        blocks.append(run)
+            run["Contrasts"] = contrasts
+        steps.append(run)
 
         if one_vs_rest:
             # if there are multiple sessions, t-test run level contrasts at
@@ -97,23 +100,23 @@ def auto_model(layout, scan_length=None, one_vs_rest=False):
             sessions = layout.get_sessions()
             if len(sessions) > 1:
                 # get contrasts names from previous block
-                contrast_names = [cc["name"] for cc in blocks[-1]["contrasts"]]
-                blocks.append(_make_passthrough_contrast("session",
+                contrast_names = [cc["Name"] for cc in steps[-1]["Contrasts"]]
+                steps.append(_make_passthrough_contrast("Session",
                                                          contrast_names))
 
             subjects = layout.get_subjects()
             if len(subjects) > 1:
                 # get contrasts names from previous block
-                contrast_names = [cc["name"] for cc in blocks[-1]["contrasts"]]
-                blocks.append(_make_passthrough_contrast("subject",
+                contrast_names = [cc["Name"] for cc in steps[-1]["Contrasts"]]
+                steps.append(_make_passthrough_contrast("Subject",
                                                          contrast_names))
 
             # get contrasts names from previous block
-            contrast_names = [cc["name"] for cc in blocks[-1]["contrasts"]]
-            blocks.append(_make_passthrough_contrast("dataset",
+            contrast_names = [cc["Name"] for cc in steps[-1]["Contrasts"]]
+            steps.append(_make_passthrough_contrast("Dataset",
                                                      contrast_names))
 
-        model["blocks"] = blocks
+        model["Steps"] = steps
         task_models.append(model)
 
     return task_models
diff --git a/bids/analysis/transformations/__init__.py b/bids/analysis/transformations/__init__.py
index c3458a46..9d874f9a 100644
--- a/bids/analysis/transformations/__init__.py
+++ b/bids/analysis/transformations/__init__.py
@@ -1,27 +1,27 @@
-from .compute import (sum, product, scale, orthogonalize, threshold, and_, or_,
-                      not_, demean, convolve_HRF)
-from .munge import (split, rename, assign, copy, factor, filter, select,
-                    remove, replace, to_dense)
+from .compute import (Sum, Product, Scale, Orthogonalize, Threshold, And, Or,
+                      Not, Demean, Convolve)
+from .munge import (Split, Rename, Assign, Copy, Factor, Filter, Select,
+                    Delete, Replace, ToDense)
 
 __all__ = [
-    'and_',
-    'assign',
-    'convolve_HRF',
-    'copy',
-    'demean',
-    'factor',
-    'filter',
-    'not_',
-    'or_',
-    'orthogonalize',
-    'product',
-    'remove',
-    'rename',
-    'replace',
-    'scale',
-    'select',
-    'split',
-    'sum',
-    'threshold',
-    'to_dense'
+    'And',
+    'Assign',
+    'Convolve',
+    'Copy',
+    'Demean',
+    'Delete',
+    'Factor',
+    'Filter',
+    'Not',
+    'Or',
+    'Orthogonalize',
+    'Product',
+    'Rename',
+    'Replace',
+    'Scale',
+    'Select',
+    'Split',
+    'Sum',
+    'Threshold',
+    'ToDense'
 ]
diff --git a/bids/analysis/transformations/compute.py b/bids/analysis/transformations/compute.py
index 13857852..0b582250 100644
--- a/bids/analysis/transformations/compute.py
+++ b/bids/analysis/transformations/compute.py
@@ -10,7 +10,91 @@ from bids.analysis import hrf
 from bids.variables import SparseRunVariable
 
 
-class scale(Transformation):
+class Convolve(Transformation):
+    """Convolve the input variable with an HRF.
+
+    Args:
+        var (Variable): The variable to convolve.
+        model (str): The name of the HRF model to apply. Must be one of 'spm',
+            'glover', or 'fir'.
+        derivative (bool): Whether or not to include the temporal derivative.
+        dispersion (bool): Whether or not to include the dispersion derivative.
+        fir_delays (iterable): A list or iterable of delays to use if model is
+            'fir' (ignored otherwise). Spacing between delays must be fixed.
+
+    Note: Uses the HRF convolution functions implemented in nistats.
+    """
+
+    _input_type = 'variable'
+
+    def _transform(self, var, model='spm', derivative=False, dispersion=False,
+                   fir_delays=None):
+
+        model = model.lower()
+
+        if isinstance(var, SparseRunVariable):
+            sr = self.collection.sampling_rate
+            var = var.to_dense(sr)
+
+        df = var.to_df(entities=False)
+        onsets = df['onset'].values
+        vals = df[['onset', 'duration', 'amplitude']].values.T
+
+        if model in ['spm', 'glover']:
+            if derivative:
+                model += ' + derivative'
+            if dispersion:
+                model += ' + dispersion'
+        elif model != 'fir':
+            raise ValueError("Model must be one of 'spm', 'glover', or 'fir'.")
+
+        convolved = hrf.compute_regressor(vals, model, onsets,
+                                          fir_delays=fir_delays, min_onset=0)
+
+        return pd.DataFrame(convolved[0], index=df.index)
+
+
+class Demean(Transformation):
+
+    def _transform(self, data):
+        return data - data.mean()
+
+
+class Orthogonalize(Transformation):
+
+    _variables_used = ('variables', 'other')
+    _densify = ('variables', 'other')
+    _align = ('other')
+
+    def _transform(self, var, other):
+
+        other = listify(other)
+
+        # Set up X matrix and slice into it based on target variable indices
+        X = np.array([self._variables[c].values.values.squeeze()
+                      for c in other]).T
+        X = X[var.index, :]
+        assert len(X) == len(var)
+        y = var.values
+        _aX = np.c_[np.ones(len(y)), X]
+        coefs, resids, rank, s = np.linalg.lstsq(_aX, y)
+        result = pd.DataFrame(y - X.dot(coefs[1:]), index=var.index)
+        return result
+
+
+class Product(Transformation):
+
+    _loopable = False
+    _groupable = False
+    _align = True
+    _output_required = True
+
+    def _transform(self, data):
+        data = pd.concat(data, axis=1)
+        return data.product(1)
+
+
+class Scale(Transformation):
     ''' Scale a variable.
 
     Args:
@@ -36,13 +120,7 @@ class scale(Transformation):
         return data
 
 
-class demean(Transformation):
-
-    def _transform(self, data):
-        return data - data.mean()
-
-
-class sum(Transformation):
+class Sum(Transformation):
 
     _loopable = False
     _groupable = False
@@ -62,41 +140,8 @@ class sum(Transformation):
         return (data * weights).sum(axis=1)
 
 
-class product(Transformation):
 
-    _loopable = False
-    _groupable = False
-    _align = True
-    _output_required = True
-
-    def _transform(self, data):
-        data = pd.concat(data, axis=1)
-        return data.product(1)
-
-
-class orthogonalize(Transformation):
-
-    _variables_used = ('variables', 'other')
-    _densify = ('variables', 'other')
-    _align = ('other')
-
-    def _transform(self, var, other):
-
-        other = listify(other)
-
-        # Set up X matrix and slice into it based on target variable indices
-        X = np.array([self._variables[c].values.values.squeeze()
-                      for c in other]).T
-        X = X[var.index, :]
-        assert len(X) == len(var)
-        y = var.values
-        _aX = np.c_[np.ones(len(y)), X]
-        coefs, resids, rank, s = np.linalg.lstsq(_aX, y)
-        result = pd.DataFrame(y - X.dot(coefs[1:]), index=var.index)
-        return result
-
-
-class threshold(Transformation):
+class Threshold(Transformation):
     ''' Threshold and/or binarize a variable.
 
     Args:
@@ -131,11 +176,11 @@ class threshold(Transformation):
         return data
 
 
-class or_(Transformation):
-    ''' Logical OR (inclusive) on two or more variables.
+class And(Transformation):
+    ''' Logical AND on two or more variables.
 
     Args:
-        dfs (list of DFs): variables to enter into the disjunction.
+        dfs (list of DFs): variables to enter into the conjunction.
     '''
 
     _loopable = False
@@ -144,10 +189,10 @@ class or_(Transformation):
 
     def _transform(self, dfs):
         df = pd.concat(dfs, axis=1)
-        return df.any(axis=1).astype(int)
+        return df.all(axis=1).astype(int)
 
 
-class not_(Transformation):
+class Not(Transformation):
     ''' Logical negation of a variable.
 
     Args:
@@ -161,11 +206,11 @@ class not_(Transformation):
         return ~var.astype(bool)
 
 
-class and_(Transformation):
-    ''' Logical AND on two or more variables.
+class Or(Transformation):
+    ''' Logical OR (inclusive) on two or more variables.
 
     Args:
-        dfs (list of DFs): variables to enter into the conjunction.
+        dfs (list of DFs): variables to enter into the disjunction.
     '''
 
     _loopable = False
@@ -174,48 +219,4 @@ class and_(Transformation):
 
     def _transform(self, dfs):
         df = pd.concat(dfs, axis=1)
-        return df.all(axis=1).astype(int)
-
-
-class convolve_HRF(Transformation):
-    """Convolve the input variable with an HRF.
-
-    Args:
-        var (Variable): The variable to convolve.
-        model (str): The name of the HRF model to apply. Must be one of 'spm',
-            'glover', or 'fir'.
-        derivative (bool): Whether or not to include the temporal derivative.
-        dispersion (bool): Whether or not to include the dispersion derivative.
-        fir_delays (iterable): A list or iterable of delays to use if model is
-            'fir' (ignored otherwise). Spacing between delays must be fixed.
-
-    Note: Uses the HRF convolution functions implemented in nistats.
-    """
-
-    _input_type = 'variable'
-
-    def _transform(self, var, model='spm', derivative=False, dispersion=False,
-                   fir_delays=None):
-
-        model = model.lower()
-
-        if isinstance(var, SparseRunVariable):
-            sr = self.collection.sampling_rate
-            var = var.to_dense(sr)
-
-        df = var.to_df(entities=False)
-        onsets = df['onset'].values
-        vals = df[['onset', 'duration', 'amplitude']].values.T
-
-        if model in ['spm', 'glover']:
-            if derivative:
-                model += ' + derivative'
-            if dispersion:
-                model += ' + dispersion'
-        elif model != 'fir':
-            raise ValueError("Model must be one of 'spm', 'glover', or 'fir'.")
-
-        convolved = hrf.compute_regressor(vals, model, onsets,
-                                          fir_delays=fir_delays, min_onset=0)
-
-        return pd.DataFrame(convolved[0], index=df.index)
+        return df.any(axis=1).astype(int)
diff --git a/bids/analysis/transformations/munge.py b/bids/analysis/transformations/munge.py
index 002ba833..d217621d 100644
--- a/bids/analysis/transformations/munge.py
+++ b/bids/analysis/transformations/munge.py
@@ -11,108 +11,7 @@ from patsy import dmatrix
 import re
 from bids.variables import DenseRunVariable, SimpleVariable
 
-
-class copy(Transformation):
-    ''' Copy/clone a variable.
-
-    Args:
-        col (str): Name of variable to copy.
-    '''
-
-    _groupable = False
-    _output_required = True
-    _allow_categorical = ('variables',)
-
-    def _transform(self, col):
-        # We don't have to do anything else here b/c it's handled in base.
-        return col
-
-
-class rename(Transformation):
-    ''' Rename a variable.
-
-    Args:
-        var (str): Name of existing variable to rename.
-    '''
-    _groupable = False
-    _output_required = True
-    _input_type = 'variable'
-    _allow_categorical = ('variables',)
-
-    def _transform(self, var):
-        ''' Rename happens automatically in the base class, so all we need to
-        do is unset the original variable in the collection. '''
-        self.collection.variables.pop(var.name)
-        return var.values.values
-
-
-class split(Transformation):
-    ''' Split a single variable into N variables as defined by the levels of one or
-    more other variables.
-
-    Args:
-        by (str, list): Name(s) of variable(s) to split on.
-    '''
-
-    _variables_used = ('variables', 'by')
-    _groupable = False
-    _input_type = 'variable'
-    _return_type = 'variable'
-    _allow_categorical = ('by',)
-    _densify = ('variables', 'by')
-
-    def _transform(self, var, by, drop_orig=True):
-
-        if not isinstance(var, SimpleVariable):
-            self._densify_variables()
-
-        # Set up all the splitting variables as a DF. Note that variables in
-        # 'by' can be either regular variables, or entities in the index--so
-        # we need to check both places.
-        all_variables = self._variables
-        by_variables = [all_variables[v].values if v in all_variables
-                        else var.index[v].reset_index(drop=True)
-                        for v in listify(by)]
-        group_data = pd.concat(by_variables, axis=1)
-        group_data.columns = listify(by)
-
-        # For sparse data, we need to set up a 1D grouper
-        if isinstance(var, SimpleVariable):
-            # Create single grouping variable by combining all 'by' variables
-            if group_data.shape[1] == 1:
-                group_labels = group_data.iloc[:, 0].values
-            else:
-                group_rows = group_data.astype(str).values.tolist()
-                group_labels = ['_'.join(r) for r in group_rows]
-
-            result = var.split(group_labels)
-
-        # For dense data, use patsy to create design matrix, then multiply
-        # it by target variable
-        else:
-            group_data = group_data.astype(str)
-            formula = '0+' + '*'.join(listify(by))
-            dm = dmatrix(formula, data=group_data, return_type='dataframe')
-            result = var.split(dm)
-
-        if drop_orig:
-            self.collection.variables.pop(var.name)
-
-        return result
-
-
-class to_dense(Transformation):
-    ''' Convert variable to dense representation. '''
-
-    _groupable = False
-    _input_type = 'variable'
-    _return_type = 'variable'
-
-    def _transform(self, var, sampling_rate=10):
-        return var.to_dense(sampling_rate=sampling_rate)
-
-
-class assign(Transformation):
+class Assign(Transformation):
     ''' Assign one variable's amplitude, duration, or onset attribute to
     another. '''
 
@@ -166,7 +65,42 @@ class assign(Transformation):
         return target
 
 
-class factor(Transformation):
+class Copy(Transformation):
+    ''' Copy/clone a variable.
+
+    Args:
+        col (str): Name of variable to copy.
+    '''
+
+    _groupable = False
+    _output_required = True
+    _allow_categorical = ('variables',)
+
+    def _transform(self, col):
+        # We don't have to do anything else here b/c it's handled in base.
+        return col
+
+
+class Delete(Transformation):
+    ''' Delete variables from the namespace.
+
+    Args:
+        variables (list, str): Name(s) of variables to delete.
+    '''
+    _groupable = False
+    _loopable = False
+    _input_type = 'variable'
+    _return_type = 'none'
+    _allow_categorical = ('variables',)
+
+    def _transform(self, variables):
+        variables = set([v.name for v in variables])
+        self.collection.variables = {k: v for k, v in
+                                     self.collection.variables.items()
+                                     if k not in variables}
+
+
+class Factor(Transformation):
 
     _groupable = False
     _input_type = 'variable'
@@ -210,7 +144,7 @@ class factor(Transformation):
         return result
 
 
-class filter(Transformation):
+class Filter(Transformation):
 
     _groupable = False
     _input_type = 'variable'
@@ -245,43 +179,25 @@ class filter(Transformation):
         return var
 
 
-class select(Transformation):
-    ''' Select variables to retain.
-
-    Args:
-        variables (list, str): Name(s) of variables to retain. All variables
-            not in the list will be dropped from the collection.
-    '''
-    _groupable = False
-    _loopable = False
-    _input_type = 'variable'
-    _return_type = 'none'
-    _allow_categorical = ('variables',)
-
-    def _transform(self, variables):
-        self.collection.variables = {v.name: v for v in variables}
-
-
-class remove(Transformation):
-    ''' Remove variables from the namespace.
+class Rename(Transformation):
+    ''' Rename a variable.
 
     Args:
-        variables (list, str): Name(s) of variables to remove.
+        var (str): Name of existing variable to rename.
     '''
     _groupable = False
-    _loopable = False
+    _output_required = True
     _input_type = 'variable'
-    _return_type = 'none'
     _allow_categorical = ('variables',)
 
-    def _transform(self, variables):
-        variables = set([v.name for v in variables])
-        self.collection.variables = {k: v for k, v in
-                                     self.collection.variables.items()
-                                     if k not in variables}
+    def _transform(self, var):
+        ''' Rename happens automatically in the base class, so all we need to
+        do is unset the original variable in the collection. '''
+        self.collection.variables.pop(var.name)
+        return var.values.values
 
 
-class replace(Transformation):
+class Replace(Transformation):
     ''' Replace values in the values, onset, or duration attributes.
 
     '''
@@ -303,3 +219,86 @@ class replace(Transformation):
                              "'onset', or 'duration'.")
 
         return var
+
+
+class Select(Transformation):
+    ''' Select variables to retain.
+
+    Args:
+        variables (list, str): Name(s) of variables to retain. All variables
+            not in the list will be dropped from the collection.
+    '''
+    _groupable = False
+    _loopable = False
+    _input_type = 'variable'
+    _return_type = 'none'
+    _allow_categorical = ('variables',)
+
+    def _transform(self, variables):
+        self.collection.variables = {v.name: v for v in variables}
+
+
+class Split(Transformation):
+    ''' Split a single variable into N variables as defined by the levels of one or
+    more other variables.
+
+    Args:
+        by (str, list): Name(s) of variable(s) to split on.
+    '''
+
+    _variables_used = ('variables', 'by')
+    _groupable = False
+    _input_type = 'variable'
+    _return_type = 'variable'
+    _allow_categorical = ('by',)
+    _densify = ('variables', 'by')
+
+    def _transform(self, var, by, drop_orig=True):
+
+        if not isinstance(var, SimpleVariable):
+            self._densify_variables()
+
+        # Set up all the splitting variables as a DF. Note that variables in
+        # 'by' can be either regular variables, or entities in the index--so
+        # we need to check both places.
+        all_variables = self._variables
+        by_variables = [all_variables[v].values if v in all_variables
+                        else var.index[v].reset_index(drop=True)
+                        for v in listify(by)]
+        group_data = pd.concat(by_variables, axis=1)
+        group_data.columns = listify(by)
+
+        # For sparse data, we need to set up a 1D grouper
+        if isinstance(var, SimpleVariable):
+            # Create single grouping variable by combining all 'by' variables
+            if group_data.shape[1] == 1:
+                group_labels = group_data.iloc[:, 0].values
+            else:
+                group_rows = group_data.astype(str).values.tolist()
+                group_labels = ['_'.join(r) for r in group_rows]
+
+            result = var.split(group_labels)
+
+        # For dense data, use patsy to create design matrix, then multiply
+        # it by target variable
+        else:
+            group_data = group_data.astype(str)
+            formula = '0+' + '*'.join(listify(by))
+            dm = dmatrix(formula, data=group_data, return_type='dataframe')
+            result = var.split(dm)
+
+        if drop_orig:
+            self.collection.variables.pop(var.name)
+
+        return result
+
+
+class ToDense(Transformation):
+    ''' Convert variable to dense representation. '''
+
+    _groupable = False
+    _input_type = 'variable'
+    _return_type = 'variable'
+
+    def _transform(self, var, sampling_rate=10):
+        return var.to_dense(sampling_rate=sampling_rate)
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
index 3b0e2cd1..08fb693f 100644
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -9,7 +9,6 @@ from grabbit.utils import listify
 import nibabel as nb
 from collections import defaultdict
 from functools import reduce
-from itertools import chain
 import re
 
 
@@ -68,29 +67,32 @@ class BIDSLayout(Layout):
             If False, queries return relative paths, unless the root argument
             was left empty (in which case the root defaults to the file system
             root).
-        derivatives (bool, str, list): Specificies whether and/or which
-            derivatives to to index. If True, all pipelines found in the
-            derivatives/ subdirectory will be indexed. If a str or list, gives
-            the paths to one or more derivatives directories to index. If False
-            or None, the derivatives/ directory is ignored during indexing, and
-            derivatives will have to be added manually via add_derivatives().
-        config (str, list): Optional name(s) of configuration file(s) to use.
-            By default (None), uses 'bids'.
-        sources (BIDLayout, list): Optional BIDSLayout(s) from which the
-            current BIDSLayout is derived.
+        derivatives (bool, str, list): Specification of whether/how to index
+            derivatives directories. Can be one of the following types:
+            * None or False: No derivatives are indexed.
+            * True: If a derivatives/ directory exists below the root, it will
+                be indexed.
+            * string or iterable of strings: One or more directories to be
+                treated as derivatives and indexed. Note that if derivatives
+                are specified this way, a derivatives/ directory under {root}
+                must be explicitly included if it is to be indexed.
         kwargs: Optional keyword arguments to pass onto the Layout initializer
             in grabbit.
     """
 
     def __init__(self, root, validate=True, index_associated=True,
-                 include=None, absolute_paths=True, derivatives=False,
-                 config=None, sources=None, **kwargs):
+                 include=None, absolute_paths=True, derivatives=None,
+                 **kwargs):
 
         self.validator = BIDSValidator(index_associated=index_associated)
         self.validate = validate
         self.metadata_index = MetadataIndex(self)
-        self.derivatives = {}
-        self.sources = listify(sources)
+
+        # Determine which configs to load
+        conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                 'config', '%s.json')
+        bids_conf = conf_path % 'bids'
+        deriv_conf = [bids_conf, conf_path % 'derivatives']
 
         # Validate arguments
         if not isinstance(root, six.string_types):
@@ -106,6 +108,7 @@ class BIDSLayout(Layout):
             raise ValueError("'dataset_description.json' file is missing from "
                           "project root. Every valid BIDS dataset must have "
                           "this file.")
+            self.description = None
         else:
             self.description = json.load(open(target, 'r'))
             for k in ['Name', 'BIDSVersion']:
@@ -113,91 +116,32 @@ class BIDSLayout(Layout):
                     raise ValueError("Mandatory '%s' field missing from "
                                      "dataset_description.json." % k)
 
+        # Construct paths to pass to grabbit
+        paths = [(root, bids_conf)]
+
+        if derivatives:
+            if derivatives == True:
+                derivatives = os.path.join(root, 'derivatives')
+            derivatives = [os.path.normpath(os.path.join(root, der))
+                           for der in listify(derivatives)]
+            paths.append((derivatives, deriv_conf))
+
+        # We'll need this for file validation later
+        self.derivatives = derivatives
+
         # Determine which subdirectories to exclude from indexing
-        excludes = {"code", "stimuli", "sourcedata", "models", "derivatives"}
+        excludes = {"code", "stimuli", "sourcedata", "models"}
+        if not derivatives:
+            excludes.add("derivatives")
         if include is not None:
-            include = listify(include)
-            if "derivatives" in include:
-                raise ValueError("Do not pass 'derivatives' in the include "
-                                 "list. To index derivatives, either set "
-                                 "derivatives=True, or use add_derivatives().")
             excludes -= set([d.strip(os.path.sep) for d in include])
         self._exclude_dirs = list(excludes)
 
-        # Set up path and config for grabbit
-        if config is None:
-            config = 'bids'
-        config = listify(config)
-        conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                 'config', '%s.json')
-        bids_conf = [conf_path % c for c in config]
-        path = (root, bids_conf)
-
-        # Initialize grabbit Layout
-        super(BIDSLayout, self).__init__(path, root=self.root,
+        super(BIDSLayout, self).__init__(paths, root=self.root,
                                          dynamic_getters=True,
                                          absolute_paths=absolute_paths,
                                          **kwargs)
 
-        # Add derivatives if any are found
-        self.derivatives = {}
-        if derivatives:
-            if derivatives == True:
-                derivatives = os.path.join(root, 'derivatives')
-            self.add_derivatives(
-                derivatives, validate=validate,
-                index_associated=index_associated,include=include,
-                absolute_paths=absolute_paths, derivatives=None, config=None,
-                sources=self, **kwargs)
-
-    def add_derivatives(self, path, **kwargs):
-        ''' Add BIDS-Derivatives datasets to tracking.
-
-        Args:
-            path (str, list): One or more paths to BIDS-Derivatives datasets.
-                Each path can point to either a derivatives/ directory
-                containing one more more pipeline directories, or to a single
-                pipeline directory (e.g., derivatives/fmriprep).
-            kwargs (dict): Optional keyword arguments to pass on to
-                BIDSLayout() when initializing each of the derivative datasets.
-        '''
-        paths = listify(path)
-        deriv_dirs = []
-
-        # Collect all paths that contain a dataset_description.json
-        def check_for_description(dir):
-            dd = os.path.join(dir, 'dataset_description.json')
-            return os.path.exists(dd)
-
-        for p in paths:
-            p = os.path.abspath(p)
-            if check_for_description(p):
-                deriv_dirs.append(p)
-            else:
-                subdirs = [d for d in os.listdir(p)
-                           if os.path.isdir(os.path.join(p, d))]
-                for sd in subdirs:
-                    sd = os.path.join(p, sd)
-                    if check_for_description(sd):
-                        deriv_dirs.append(sd)
-
-        for deriv in deriv_dirs:
-            dd = os.path.join(deriv, 'dataset_description.json')
-            description = json.load(open(dd, 'r'))
-            pipeline_name = description.get('PipelineDescription.Name', None)
-            if pipeline_name is None:
-                raise ValueError("Every valid BIDS-derivatives dataset must "
-                                "have a PipelineDescription.Name field set "
-                                "inside dataset_description.json.")
-            if pipeline_name in self.derivatives:
-                raise ValueError("Pipeline name '%s' has already been added "
-                                 "to this BIDSLayout. Every added pipeline "
-                                 "must have a unique name!")
-            # Default config and sources values
-            kwargs['config'] = kwargs.get('config') or ['bids', 'derivatives']
-            kwargs['sources'] = kwargs.get('sources') or self
-            self.derivatives[pipeline_name] = BIDSLayout(deriv, **kwargs)
-
     def to_df(self, **kwargs):
         """
         Return information for all Files tracked in the Layout as a pandas
@@ -227,7 +171,7 @@ class BIDSLayout(Layout):
     def _validate_dir(self, d):
         # Callback from grabbit. Exclude special directories like derivatives/
         # and code/ from indexing unless they were explicitly included at
-        # initialization.
+        # initialization in either the include or paths arguments.
         no_root = os.path.relpath(d, self.root).split(os.path.sep)[0]
         if no_root in self._exclude_dirs:
             check_paths = set(self._paths_to_index) - {self.root}
@@ -241,13 +185,17 @@ class BIDSLayout(Layout):
         if not self.validate:
             return True
 
-        # For derivatives, we need to cheat a bit and construct a fake
-        # derivatives path--prepend 'derivatives' and the pipeline name
-        to_check = os.path.relpath(f, self.root)
-        if 'derivatives' in self.domains:
-            to_check = os.path.join(
-                'derivatives', self.description['PipelineDescription.Name'],
-                to_check)
+        if f.startswith(self.root):
+            to_check = os.path.relpath(f, self.root)
+        # For validation purposes, we need to treat all derivatives outside
+        # the project root as if they were inside it
+        elif self.derivatives:
+            for der in self.derivatives:
+                if f.startswith(der):
+                    to_check = f.replace(der, 'derivatives' + os.path.sep)
+                    break
+        else:
+            to_check = f
 
         sep = os.path.sep
         if to_check[:len(sep)] != sep:
@@ -260,13 +208,12 @@ class BIDSLayout(Layout):
         path = os.path.abspath(path)
 
         if not suffix:
-            f = self.get_file(path)
-            if 'suffix' not in f.entities:
+            if 'suffix' not in self.files[path].entities:
                 raise ValueError(
                     "File '%s' does not have a valid suffix, most "
                     "likely because it is not a valid BIDS file." % path
                 )
-            suffix = f.entities['suffix']
+            suffix = self.files[path].entities['suffix']
 
         tmp = self.get_nearest(path, extensions=extension, all_=True,
                                suffix=suffix, ignore_strict_entities=['suffix'],
@@ -278,8 +225,7 @@ class BIDSLayout(Layout):
             return None
 
     def get(self, return_type='tuple', target=None, extensions=None,
-            derivatives=True, regex_search=None, defined_fields=None,
-            domains=None, **kwargs):
+            domains=None, regex_search=None, defined_fields=None, **kwargs):
         """
         Retrieve files and/or metadata from the current Layout.
 
@@ -292,15 +238,12 @@ class BIDSLayout(Layout):
                 'id': returns a list of unique IDs. Must be used together with
                     a valid target.
                 'obj': returns a list of matching File objects.
-            target (str): Optional name of the target entity to get results for
-                (only used if return_type is 'dir' or 'id').
+            target (str): The name of the target entity to get results for
+                (if return_type is 'dir' or 'id').
             extensions (str, list): One or more file extensions to filter on.
                 Files with any other extensions will be excluded.
-            derivatives (bool, str, list): Whether/how to search associated
-                BIDS-Derivatives datasets. If True (default), all available
-                derivatives are searched. If a str or list, must be the name(s)
-                of the derivatives to search (as defined in the
-                PipelineDescription.Name field in dataset_description.json).
+            domains (list): Optional list of domain names to scan for files.
+                If None, all available domains are scanned.
             regex_search (bool or None): Whether to require exact matching
                 (False) or regex search (True) when comparing the query string
                 to each entity. If None (default), uses the value found in
@@ -309,8 +252,6 @@ class BIDSLayout(Layout):
                 that must be defined in JSON sidecars in order to consider the
                 file a match, but which don't need to match any particular
                 value.
-            domains (str, list): Domain(s) to search in. Valid values are
-                'bids' and 'derivatives'.
             kwargs (dict): Any optional key/values to filter the entities on.
                 Keys are entity names, values are regexes to filter on. For
                 example, passing filter={ 'subject': 'sub-[12]'} would return
@@ -320,17 +261,10 @@ class BIDSLayout(Layout):
             A named tuple (default) or a list (see return_type for details).
         """
 
-        if derivatives == True:
-            derivatives = list(self.derivatives.keys())
-
         # Separate entity kwargs from metadata kwargs
         ent_kwargs, md_kwargs = {}, {}
 
         all_ents = self.get_domain_entities()
-        if derivatives:
-            for deriv in derivatives:
-                deriv_ents = self.derivatives[deriv].get_domain_entities()
-                all_ents.update(deriv_ents)
 
         for k, v in kwargs.items():
             if k in all_ents:
@@ -338,41 +272,23 @@ class BIDSLayout(Layout):
             else:
                 md_kwargs[k] = v
 
-        all_results = []
-
         # Get entity-based search results using the superclass's get()
-        result = []
         result = super(
-            BIDSLayout, self).get(return_type, target, extensions, None,
+            BIDSLayout, self).get(return_type, target, extensions, domains,
                                   regex_search, **ent_kwargs)
 
-        # Search the metadata if needed
-        if return_type not in {'dir', 'id'}:
-
-            if md_kwargs:
-                if return_type.startswith('obj'):
-                    result = [f.path for f in result]
-
-                result = self.metadata_index.search(result, defined_fields,
-                                                    **md_kwargs)
+        if return_type in ['dir', 'id']:
+            return result
 
-                if return_type.startswith('obj'):
-                    result = [self.files[f] for f in result]
+        if return_type.startswith('obj'):
+            result = [f.path for f in result]
 
-        all_results.append(result)
+        if md_kwargs:
+            result = self.metadata_index.search(result, defined_fields,
+                                                **md_kwargs)
 
-        # Add results from derivatives
-        if derivatives:
-            for deriv in derivatives:
-                deriv = self.derivatives[deriv]
-                deriv_res = deriv.get(return_type, target, extensions, None,
-                                      regex_search, **ent_kwargs)
-                all_results.append(deriv_res)
-
-        # Flatten results
-        result = list(chain(*all_results))
-        if return_type in ['dir', 'id']:
-            result = list(set(result))
+        if return_type.startswith('obj'):
+            result = [self.files[f] for f in result]
 
         return result
 
@@ -402,8 +318,7 @@ class BIDSLayout(Layout):
         self.metadata_index.index_file(path)
 
         if include_entities:
-            f = self.get_file(os.path.abspath(path))
-            entities = f.entities
+            entities = self.files[os.path.abspath(path)].entities
             results = entities
         else:
             results = {}
@@ -523,24 +438,6 @@ class BIDSLayout(Layout):
         # Override grabbit's File with a BIDSFile.
         return BIDSFile(os.path.join(root, f), self)
 
-    def get_file(self, filename, derivatives=True):
-        ''' Returns the BIDSFile object with the specified path.
-
-        Args:
-            filename (str): The path of the file to retrieve.
-            derivatives (bool: If True, checks all associated derivative
-                datasets as well.
-
-        Returns: A BIDSFile.
-        '''
-        layouts = [self]
-        if derivatives:
-            layouts += self.derivatives.values()
-        for ly in layouts:
-            if filename in ly.files:
-                return ly.files[filename]
-        return None
-
 
 class MetadataIndex(object):
     """A simple dict-based index for key/value pairs in JSON metadata.
@@ -563,7 +460,7 @@ class MetadataIndex(object):
                 an entry already exists.
         """
         if isinstance(f, six.string_types):
-            f = self.layout.get_file(f)
+            f = self.layout.files[f]
 
         if f.path in self.file_index and not overwrite:
             return
diff --git a/bids/layout/validation.py b/bids/layout/validation.py
index 690e3702..7f10ac77 100644
--- a/bids/layout/validation.py
+++ b/bids/layout/validation.py
@@ -3,7 +3,6 @@
 import re
 import json
 from os.path import join, abspath, dirname
-import pandas as pd
 
 __all__ = ['BIDSValidator']
 
@@ -23,7 +22,7 @@ class BIDSValidator():
 
     Examples
     --------
-    >>> from bids.layout import BIDSValidator
+    >>> from bids.grabbids import BIDSValidator
     >>> validator = BIDSValidator()
     >>> filepaths = ["/sub-01/anat/sub-01_rec-CSD_T1w.nii.gz",
     >>> "/sub-01/anat/sub-01_acq-23_rec-CSD_T1w.exe", #wrong extension
@@ -36,7 +35,8 @@ class BIDSValidator():
     """
 
     def __init__(self, index_associated=True):
-        self.rule_dir = join(dirname(abspath(__file__)),'config', 'validator')
+        self.rule_dir = join(dirname(abspath(__file__)), 'config',
+                              'validator')
         self.index_associated = index_associated
 
     def is_bids(self, path):
@@ -52,7 +52,7 @@ class BIDSValidator():
 
         Examples
         --------
-        >>> from bids.layout import BIDSValidator
+        >>> from bids.grabbids import BIDSValidator
         >>> validator = BIDSValidator()
         >>> validator.is_bids("/sub-01/ses-test/anat/sub-01_ses-test_rec-CSD_run-23_T1w.nii.gz")
         True
@@ -181,144 +181,3 @@ class BIDSValidator():
             return ((match[1] == match[2][1:]) | (not match[1]))
         else:
             return False
-
-def validate_sequences(layout, config):
-    """Checks files in BIDS project match user defined expectations.
-
-    This method is a wrapper for the check_duplicate_files() and 
-    check_expected_files() methods. Use it to check whether there are
-    files with duplicate content within the BIDS data set and to check
-    the number of data set files against a user customized configuration
-    file. Returns a named tuple of three data frames: duplicates, summary, and problems.
-
-
-    Parameters
-    ----------
-        layout: BIDSLayout class
-            A BIDSLayout path of a data set.
-
-        config: string
-            Path to customized configuration file. Requires `runs` as an input.
-            See the sample config for an example (bids/layout/tests/data/sample_validation_config.json).
-
-
-    Examples
-    --------
-    >>> layout = bids.grabbids.BIDSLayout('/path_to/sample_project_root')
-    >>> dfs = validate_sequences(layout, 'pybids/bids/layout/tests/data/sample_validation_config.json')
-    >>> dfs.duplicates
-    # Put example output here
-    >>> df.summary
-    # Put example output here
-    >>> df.problems
-    # Put example output here
-    """
-    
-    duplicate_file_df = check_duplicate_files(layout)
-    summary_df, problem_df = check_expected_files(layout, config)
-    output = namedtuple('output', ['duplicates', 'summary', 'problems'])
-    return output(duplicate_file_df, summary_df, problem_df)
-    
-    
-def check_duplicate_files(layout):
-    """Checks images in BIDS project are not duplicated.
-
-    Check whether any files have duplicate content within the 
-    BIDS data set. Returns a data frame: duplicate_file_df.
-
-
-    Parameters
-    ----------
-        layout: BIDSLayout class
-            A BIDSLayout path of a data set.
-
-
-    Examples
-    --------
-    >>> layout = bids.grabbids.BIDSLayout('/path_to/sample_project_root')
-    >>> duplicate_file_df = check_duplicate_files(layout)
-    >>> duplicate_file_df
-    # Put example output here
-
-
-    Notes
-    ------
-    Returns a data frame in which the first column is the file
-    identifier and the second column is the path to the file.
-    Files with matching identifiers have the same content.
-    """
-    
-    def md5(fname):
-        hash_md5 = hashlib.md5()
-        with open(fname, "rb") as f:
-            for chunk in iter(lambda: f.read(4096), b""):
-                hash_md5.update(chunk)
-        return hash_md5.hexdigest()
-    hash_map = {}
-    all_niftis = layout.get(return_type="file", extensions='.nii.gz')
-    for nifti_file in all_niftis:
-        md5sum = md5(nifti_file)
-        if md5sum in hash_map:
-            hash_map[md5sum].append(nifti_file)
-        else:
-            hash_map[md5sum] = [nifti_file]
-    df = pd.DataFrame.from_dict(hash_map, orient='index') 
-    pruned_df = df.stack().reset_index().drop(columns='level_1')
-    out_df = pruned_df.rename(columns={'level_0': 'hash', 0: 'filename'})
-    return out_df
-    
-    
-def check_expected_files(layout, config):
-    """Checks files in BIDS project match user defined expectations.
-
-    This method checks the number of data set files against a user customized 
-    configuration file. Returns two data frames: summary_df, problem_df.
-
-
-    Parameters
-    ----------
-        layout: BIDSLayout class
-            A BIDSLayout path of a data set.
-
-        config: string
-            Path to customized configuration file.
-
-
-    Examples
-    --------
-    >>> layout = bids.grabbids.BIDSLayout('/path_to/sample_project_root')
-    >>> summary_df, problem_df = check_expected_files(layout, 'pybids/bids/layout/tests/data/sample_validation_config.json')
-    >>> summary_df
-    # Put example output here
-    >>> problem_df
-    # Put example output here
-
-
-    Notes
-    --------
-
-    `runs` is a mandatory field in the config file.
-    
-    The configuration file can take any keys that are valid arguments for
-    pybids `layout.get()` Values shoud match those in the BIDS file names. 
-    See the sample config for an example (bids/layout/tests/data/sample_validation_config.json).
-    The more specific keys are provided, the more informative the output will be.
-
-    """
-
-    dictlist = []
-    with open(config) as f:
-        json_data = json.load(f)
-        subjects = layout.get_subjects()
-    for sub in subjects: 
-        for scan_params_d in json_data['sequences']:
-            scan_params = scan_params_d.copy()
-            seq_params = {i: scan_params[i] for i in scan_params if i != 'runs'}
-            actual_runs = layout.get(return_type='obj', subject=sub, extensions='.nii.gz', **seq_params)
-            scan_params['subject'] = sub
-            scan_params['runs_found'] = len(actual_runs)
-            scan_params['problem'] = len(actual_runs) != scan_params['runs']
-            dictlist.append(scan_params)
-    summary_df = pd.DataFrame(dictlist)
-    problem_df = summary_df.loc[summary_df['problem'] == True]
-    return summary_df, problem_df
\ No newline at end of file
diff --git a/bids/utils.py b/bids/utils.py
index f240788f..126df722 100644
--- a/bids/utils.py
+++ b/bids/utils.py
@@ -1,3 +1,5 @@
+import re
+
 def listify(obj):
     ''' Wraps all non-list or tuple objects in a list; provides a simple way
     to accept flexible arguments. '''
@@ -19,3 +21,36 @@ def matches_entities(obj, entities, strict=False):
         elif current != target:
             return False
     return True
+
+def convert_JSON(j):
+    """ Recursively convert CamelCase keys to snake_case.
+    From: https://stackoverflow.com/questions/17156078/converting-identifier-naming-between-camelcase-and-underscores-during-json-seria
+    """
+
+    def camel_to_snake(s):
+        a = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
+        return a.sub(r'_\1', s).lower()
+
+    def convertArray(a):
+        newArr = []
+        for i in a:
+            if isinstance(i,list):
+                newArr.append(convertArray(i))
+            elif isinstance(i, dict):
+                newArr.append(convert_JSON(i))
+            else:
+                newArr.append(i)
+        return newArr
+
+    out = {}
+    for k, value in j.items():
+        newK = camel_to_snake(k)
+
+        if isinstance(value, dict):
+            out[newK] = convert_JSON(value)
+        elif isinstance(value, list):
+            out[newK] = convertArray(value)
+        else:
+            out[newK] = value
+
+    return out
diff --git a/bids/variables/entities.py b/bids/variables/entities.py
index c547e35d..a1060f6f 100644
--- a/bids/variables/entities.py
+++ b/bids/variables/entities.py
@@ -14,7 +14,7 @@ class Node(object):
     '''
 
     def __init__(self, level, entities):
-        self.level = level
+        self.level = level.lower()
         self.entities = entities
         self.variables = {}
 
diff --git a/bids/variables/io.py b/bids/variables/io.py
index 95037cee..39bbebfc 100644
--- a/bids/variables/io.py
+++ b/bids/variables/io.py
@@ -6,6 +6,7 @@ from bids.utils import listify
 from .entities import NodeIndex
 from .variables import SparseRunVariable, DenseRunVariable, SimpleVariable
 import warnings
+from bids.config import get_option
 
 
 BASE_ENTITIES = ['subject', 'session', 'task', 'run']
@@ -59,7 +60,7 @@ def load_variables(layout, types=None, levels=None, skip_empty=True,
                 'subject': ['sessions'],
                 'dataset': ['participants']
             }
-            [types.extend(lev_map[l]) for l in listify(levels)]
+            [types.extend(lev_map[l.lower()]) for l in listify(levels)]
         else:
             types = TYPES
 
@@ -126,17 +127,15 @@ def _load_time_variables(layout, dataset=None, columns=None, scan_length=None,
 
     selectors['datatype'] = 'func'
     selectors['suffix'] = 'bold'
-    images = layout.get(return_type='object', extensions='.nii.gz',
-                        derivatives=True, **selectors)
+    images = layout.get(return_type='file', extensions='.nii.gz', **selectors)
 
     if not images:
         raise ValueError("No functional images that match criteria found.")
 
     # Main loop over images
-    for img_obj in images:
+    for img_f in images:
 
-        entities = img_obj.entities
-        img_f = img_obj.path
+        entities = layout.files[img_f].entities
 
         # Run is not mandatory, but we need a default for proper indexing
         if 'run' in entities:
diff --git a/bids/variables/kollekshuns.py b/bids/variables/kollekshuns.py
index d9eeae8f..ab76c83d 100644
--- a/bids/variables/kollekshuns.py
+++ b/bids/variables/kollekshuns.py
@@ -34,8 +34,6 @@ class BIDSVariableCollection(object):
 
     def __init__(self, variables):
 
-        if not variables:
-            raise ValueError("No variables were provided")
         SOURCE_TO_LEVEL = {
             'events': 'run',
             'physio': 'run',
@@ -45,6 +43,7 @@ class BIDSVariableCollection(object):
             'sessions': 'subject',
             'participants': 'dataset'
         }
+
         var_levels = set([SOURCE_TO_LEVEL[v.source] if v.source in
                           SOURCE_TO_LEVEL else v.source for v in variables])
 
@@ -54,11 +53,6 @@ class BIDSVariableCollection(object):
                              "variables at more than one level of analysis. "
                              "Levels found in input variables: %s" %
                              var_levels)
-        elif not var_levels:
-            raise ValueError(
-                "None of the provided variables matched any of the known levels, which are: %s"
-                % (', '.join(sorted(SOURCE_TO_LEVEL.values())))
-            )
 
         self.level = list(var_levels)[0]
         variables = self.merge_variables(variables)
diff --git a/bids/version.py b/bids/version.py
index 18a4d13d..aa881794 100644
--- a/bids/version.py
+++ b/bids/version.py
@@ -45,7 +45,7 @@ AUTHOR = "PyBIDS developers"
 AUTHOR_EMAIL = "bids-discussion@googlegroups.com"
 PLATFORMS = "OS Independent"
 # No data for now
-REQUIRES = ["grabbit==0.2.5", "six", "num2words", "numpy", "scipy", "pandas",
+REQUIRES = ["grabbit==0.2.4", "six", "num2words", "numpy", "scipy", "pandas",
             "nibabel", "patsy"]
 EXTRAS_REQUIRE = {
    # Just to not break compatibility with externals requiring
@@ -65,7 +65,7 @@ def package_files(directory):
 
 extra_files = package_files('path_to/extra_files_dir')
 PACKAGE_DATA = {
-    'bids.layout': ['config/*.json', 'config/validator/*.json'],
+    'bids.layout': ['config/*.json'],
     'bids.reports': ['config/*.json'],
     'bids': package_files('bids/tests/data')
 }
diff --git a/doc/introduction.rst b/doc/introduction.rst
index 16b80640..57e34523 100644
--- a/doc/introduction.rst
+++ b/doc/introduction.rst
@@ -50,7 +50,7 @@ Finding help
 :Mailing lists and forums:
 
     * Don't hesitate to ask questions about pybids on `NeuroStars
-      <https://neurostars.org/tags/pybids>`_.
+      <https://neurostars.org/t/pybids/>`_.
 
     * You can find help with neuroimaging in Python (file I/O,
       neuroimaging-specific questions) via the nipy user group:


Test patch:
diff --git a/bids/analysis/tests/test_automodel.py b/bids/analysis/tests/test_automodel.py
index ec4198d0..f1df8c2b 100644
--- a/bids/analysis/tests/test_automodel.py
+++ b/bids/analysis/tests/test_automodel.py
@@ -25,27 +25,26 @@ def test_auto_model_analysis(model):
     analysis = Analysis(layout, model)
     analysis.setup(scan_length=480)
 
-    assert model['name'] == 'ds005_mixedgamblestask'
+    assert model['Name'] == 'ds005_mixedgamblestask'
 
     # run level
-    block = model['blocks'][0]
-    assert block['name'] == 'run'
-    assert block['level'] == 'run'
-    assert block['transformations'][0]['name'] == 'factor'
-    assert block['model']['HRF_variables'][0] == 'trial_type.parametric gain'
-    assert block['contrasts'][0]['name'] == 'run_parametric gain'
-    assert block['contrasts'][0]['weights'] == [1]
+    block = model['Steps'][0]
+    assert block['Name'] == 'Run'
+    assert block['Level'] == 'Run'
+    assert block['Transformations'][0]['Name'] == 'Factor'
+    assert block['Contrasts'][0]['Name'] == 'run_parametric gain'
+    assert block['Contrasts'][0]['Weights'] == [1]
 
     # subject level
-    block = model['blocks'][1]
-    assert block['name'] == 'subject'
-    assert block['level'] == 'subject'
-    assert block['model']['variables'][0] == 'run_parametric gain'
-    assert block['contrasts'][0]['name'] == 'subject_run_parametric gain'
+    block = model['Steps'][1]
+    assert block['Name'] == 'Subject'
+    assert block['Level'] == 'Subject'
+    assert block['Model']['X'][0] == 'run_parametric gain'
+    assert block['Contrasts'][0]['Name'] == 'subject_run_parametric gain'
 
     # dataset level
-    block = model['blocks'][2]
-    assert block['name'] == 'dataset'
-    assert block['level'] == 'dataset'
-    assert block['model']['variables'][0] == 'subject_run_parametric gain'
-    assert block['contrasts'][0]['name'] == 'dataset_subject_run_parametric gain'
+    block = model['Steps'][2]
+    assert block['Name'] == 'Dataset'
+    assert block['Level'] == 'Dataset'
+    assert block['Model']['X'][0] == 'subject_run_parametric gain'
+    assert block['Contrasts'][0]['Name'] == 'dataset_subject_run_parametric gain'
diff --git a/bids/analysis/tests/test_transformations.py b/bids/analysis/tests/test_transformations.py
index b5d73972..f5c7c49c 100644
--- a/bids/analysis/tests/test_transformations.py
+++ b/bids/analysis/tests/test_transformations.py
@@ -21,7 +21,7 @@ def collection():
 def test_rename(collection):
     dense_rt = collection.variables['RT'].to_dense(10)
     assert len(dense_rt.values) == 230400
-    transform.rename(collection, 'RT', output='reaction_time')
+    transform.Rename(collection, 'RT', output='reaction_time')
     assert 'reaction_time' in collection.variables
     assert 'RT' not in collection.variables
     col = collection.variables['reaction_time']
@@ -31,7 +31,7 @@ def test_rename(collection):
 
 def test_product(collection):
     c = collection
-    transform.product(collection, variables=['parametric gain', 'gain'],
+    transform.Product(collection, variables=['parametric gain', 'gain'],
                       output='prod')
     res = c['prod'].values
     assert (res == c['parametric gain'].values * c['gain'].values).all()
@@ -39,22 +39,22 @@ def test_product(collection):
 
 def test_sum(collection):
     c = collection
-    transform.sum(collection, variables=['parametric gain', 'gain'],
+    transform.Sum(collection, variables=['parametric gain', 'gain'],
                       output='sum')
     res = c['sum'].values
     target = c['parametric gain'].values + c['gain'].values
     assert np.array_equal(res, target)
-    transform.sum(collection, variables=['parametric gain', 'gain'],
+    transform.Sum(collection, variables=['parametric gain', 'gain'],
                       output='sum', weights=[2, 2])
     assert np.array_equal(c['sum'].values, target * 2)
     with pytest.raises(ValueError):
-        transform.sum(collection, variables=['parametric gain', 'gain'],
+        transform.Sum(collection, variables=['parametric gain', 'gain'],
                       output='sum', weights=[1, 1, 1])
-    
+
 
 
 def test_scale(collection):
-    transform.scale(collection, variables=['RT', 'parametric gain'],
+    transform.Scale(collection, variables=['RT', 'parametric gain'],
                     output=['RT_Z', 'gain_Z'], groupby=['run', 'subject'])
     groupby = collection['RT'].get_grouper(['run', 'subject'])
     z1 = collection['RT_Z'].values
@@ -64,7 +64,7 @@ def test_scale(collection):
 
 
 def test_demean(collection):
-    transform.demean(collection, variables=['RT'], output=['RT_dm'])
+    transform.Demean(collection, variables=['RT'], output=['RT_dm'])
     m1 = collection['RT_dm'].values
     m2 = collection['RT'].values
     m2 -= m2.values.mean()
@@ -72,14 +72,14 @@ def test_demean(collection):
 
 
 def test_orthogonalize_dense(collection):
-    transform.factor(collection, 'trial_type', sep=sep)
+    transform.Factor(collection, 'trial_type', sep=sep)
 
     # Store pre-orth variables needed for tests
     pg_pre = collection['trial_type/parametric gain'].to_dense(10)
     rt = collection['RT'].to_dense(10)
 
     # Orthogonalize and store result
-    transform.orthogonalize(collection, variables='trial_type/parametric gain',
+    transform.Orthogonalize(collection, variables='trial_type/parametric gain',
                             other='RT', dense=True, groupby=['run', 'subject'])
     pg_post = collection['trial_type/parametric gain']
 
@@ -100,7 +100,7 @@ def test_orthogonalize_dense(collection):
 def test_orthogonalize_sparse(collection):
     pg_pre = collection['parametric gain'].values
     rt = collection['RT'].values
-    transform.orthogonalize(collection, variables='parametric gain',
+    transform.Orthogonalize(collection, variables='parametric gain',
                             other='RT', groupby=['run', 'subject'])
     pg_post = collection['parametric gain'].values
     vals = np.c_[rt.values, pg_pre.values, pg_post.values]
@@ -121,7 +121,7 @@ def test_split(collection):
     rt_pre_onsets = collection['RT'].onset
 
     # Grouping SparseEventVariable by one column
-    transform.split(collection, ['RT'], ['respcat'])
+    transform.Split(collection, ['RT'], ['respcat'])
     assert 'RT.0' in collection.variables.keys() and \
            'RT.-1' in collection.variables.keys()
     rt_post_onsets = np.r_[collection['RT.0'].onset,
@@ -130,12 +130,12 @@ def test_split(collection):
     assert np.array_equal(rt_pre_onsets.sort(), rt_post_onsets.sort())
 
     # Grouping SparseEventVariable by multiple columns
-    transform.split(collection, variables=['RT_2'], by=['respcat', 'loss'])
+    transform.Split(collection, variables=['RT_2'], by=['respcat', 'loss'])
     assert 'RT_2.-1_13' in collection.variables.keys() and \
            'RT_2.1_13' in collection.variables.keys()
 
     # Grouping by DenseEventVariable
-    transform.split(collection, variables='RT_3', by='respcat',
+    transform.Split(collection, variables='RT_3', by='respcat',
                     drop_orig=False)
     targets = ['RT_3.respcat[-1]', 'RT_3.respcat[0]', 'RT_3.respcat[1]']
     assert not set(targets) - set(collection.variables.keys())
@@ -145,7 +145,7 @@ def test_split(collection):
 
     # Grouping by entities in the index
     collection['RT_4'] = orig.clone(name='RT_4')
-    transform.split(collection, variables=['RT_4'], by=['respcat', 'run'])
+    transform.Split(collection, variables=['RT_4'], by=['respcat', 'run'])
     assert 'RT_4.-1_3' in collection.variables.keys()
 
 
@@ -155,7 +155,7 @@ def test_resample_dense(collection):
     collection.resample(50, in_place=True)
     assert len(old_rt.values) * 5 == len(collection['RT'].values)
     # Should work after explicitly converting categoricals
-    transform.factor(collection, 'trial_type')
+    transform.Factor(collection, 'trial_type')
     collection.resample(5, force_dense=True, in_place=True)
     assert len(old_rt.values) == len(collection['parametric gain'].values) * 2
 
@@ -165,24 +165,24 @@ def test_threshold(collection):
     orig_vals = old_pg.values
 
     collection['pg'] = old_pg.clone()
-    transform.threshold(collection, 'pg', threshold=0.2, binarize=True)
+    transform.Threshold(collection, 'pg', threshold=0.2, binarize=True)
     assert collection.variables['pg'].values.sum() == (orig_vals >= 0.2).sum()
 
     collection['pg'] = old_pg.clone()
-    transform.threshold(collection, 'pg', threshold=0.2, binarize=False)
+    transform.Threshold(collection, 'pg', threshold=0.2, binarize=False)
     assert collection.variables['pg'].values.sum() != (orig_vals >= 0.2).sum()
     coll_sum = (collection.variables['pg'].values >= 0.2).sum()
     assert coll_sum == (orig_vals >= 0.2).sum()
 
     collection['pg'] = old_pg.clone()
-    transform.threshold(collection, 'pg', threshold=-0.1, binarize=True,
+    transform.Threshold(collection, 'pg', threshold=-0.1, binarize=True,
                         signed=False, above=False)
     n = np.logical_and(orig_vals <= 0.1, orig_vals >= -0.1).sum()
     assert collection.variables['pg'].values.sum() == n
 
 
 def test_assign(collection):
-    transform.assign(collection, 'parametric gain', target='RT',
+    transform.Assign(collection, 'parametric gain', target='RT',
                      target_attr='onset', output='test1')
     t1 = collection['test1']
     pg = collection['parametric gain']
@@ -191,7 +191,7 @@ def test_assign(collection):
     assert np.array_equal(t1.duration, rt.duration)
     assert np.array_equal(t1.values.values, rt.values.values)
 
-    transform.assign(collection, 'RT', target='parametric gain',
+    transform.Assign(collection, 'RT', target='parametric gain',
                      input_attr='onset', target_attr='amplitude',
                      output='test2')
     t2 = collection['test2']
@@ -201,7 +201,7 @@ def test_assign(collection):
 
 
 def test_copy(collection):
-    transform.copy(collection, 'RT', output='RT_copy')
+    transform.Copy(collection, 'RT', output='RT_copy')
     assert 'RT_copy' in collection.variables.keys()
     assert np.array_equal(collection['RT'].values.values,
                           collection['RT_copy'].values.values)
@@ -210,9 +210,9 @@ def test_copy(collection):
 def test_regex_variable_expansion(collection):
     # Should fail because two output values are required following expansion
     with pytest.raises(Exception):
-        transform.copy(collection, 'resp', regex_variables='variables')
+        transform.Copy(collection, 'resp', regex_variables='variables')
 
-    transform.copy(collection, 'resp', output_suffix='_copy',
+    transform.Copy(collection, 'resp', output_suffix='_copy',
                    regex_variables='variables')
     assert 'respnum_copy' in collection.variables.keys()
     assert 'respcat_copy' in collection.variables.keys()
@@ -226,7 +226,7 @@ def test_factor(collection):
     # Full-rank dummy-coding, only one unique value in variable
     trial_type = collection.variables['trial_type'].clone()
     coll = collection.clone()
-    transform.factor(coll, 'trial_type', sep='@')
+    transform.Factor(coll, 'trial_type', sep='@')
     assert 'trial_type@parametric gain' in coll.variables.keys()
     pg = coll.variables['trial_type@parametric gain']
     assert pg.values.unique() == [1]
@@ -234,7 +234,7 @@ def test_factor(collection):
 
     # Reduced-rank dummy-coding, only one unique value in variable
     coll = collection.clone()
-    transform.factor(coll, 'trial_type', constraint='mean_zero')
+    transform.Factor(coll, 'trial_type', constraint='mean_zero')
     assert 'trial_type.parametric gain' in coll.variables.keys()
     pg = coll.variables['trial_type.parametric gain']
     assert pg.values.unique() == [1]
@@ -242,7 +242,7 @@ def test_factor(collection):
 
     # full-rank dummy-coding, multiple values
     coll = collection.clone()
-    transform.factor(coll, 'respnum')
+    transform.Factor(coll, 'respnum')
     targets = set(['respnum.%d' % d for d in range(0, 5)])
     assert not targets - set(coll.variables.keys())
     assert all([set(coll.variables[t].values.unique()) == {0.0, 1.0}
@@ -252,7 +252,7 @@ def test_factor(collection):
 
     # reduced-rank dummy-coding, multiple values
     coll = collection.clone()
-    transform.factor(coll, 'respnum', constraint='drop_one')
+    transform.Factor(coll, 'respnum', constraint='drop_one')
     targets = set(['respnum.%d' % d for d in range(1, 5)])
     assert not targets - set(coll.variables.keys())
     assert 'respnum.0' not in coll.variables.keys()
@@ -263,7 +263,7 @@ def test_factor(collection):
 
     # Effect coding, multiple values
     coll = collection.clone()
-    transform.factor(coll, 'respnum', constraint='mean_zero')
+    transform.Factor(coll, 'respnum', constraint='mean_zero')
     targets = set(['respnum.%d' % d for d in range(1, 5)])
     assert not targets - set(coll.variables.keys())
     assert 'respnum.0' not in coll.variables.keys()
@@ -276,13 +276,13 @@ def test_factor(collection):
 def test_filter(collection):
     orig = collection['parametric gain'].clone()
     q = "parametric gain > 0"
-    transform.filter(collection, 'parametric gain', query=q)
+    transform.Filter(collection, 'parametric gain', query=q)
     assert len(orig.values) == 2 * len(collection['parametric gain'].values)
     assert np.all(collection['parametric gain'].values > 0)
 
     orig = collection['RT'].clone()
     q = 'parametric gain > 0.1'
-    transform.filter(collection, 'RT', query=q, by='parametric gain')
+    transform.Filter(collection, 'RT', query=q, by='parametric gain')
     assert len(orig.values) != len(collection['RT'].values)
     # There is some bizarro thing going on where, on travis, the result is
     # randomly either 1536 or 3909 when running on Python 3 (on linux or mac).
@@ -297,17 +297,17 @@ def test_replace(collection):
     orig = collection['parametric gain'].clone()
     # Values
     replace_dict = {0.0335: 2.0, -0.139: 2.0}
-    transform.replace(collection, 'parametric gain', replace_dict)
+    transform.Replace(collection, 'parametric gain', replace_dict)
     target = set(orig.values.unique()) - {0.0335, -0.139} | {2.0}
     assert set(collection['parametric gain'].values.unique()) == target
     # Durations
     replace_dict = {3: 2}
-    transform.replace(collection, 'parametric gain', replace_dict, 'duration')
+    transform.Replace(collection, 'parametric gain', replace_dict, 'duration')
     target = set(np.unique(orig.duration)) - {3} | {2.0}
     assert set(np.unique(collection['parametric gain'].duration)) == target
     # Onsets
     replace_dict = {4.: 3., 476.: 475.5}
-    transform.replace(collection, 'parametric gain', replace_dict, 'onset')
+    transform.Replace(collection, 'parametric gain', replace_dict, 'onset')
     target = set(np.unique(orig.onset)) - {4., 476.} | {3., 475.5}
     assert set(np.unique(collection['parametric gain'].onset)) == target
 
@@ -315,40 +315,40 @@ def test_replace(collection):
 def test_select(collection):
     coll = collection.clone()
     keep = ['RT', 'parametric gain', 'respcat']
-    transform.select(coll, keep)
+    transform.Select(coll, keep)
     assert set(coll.variables.keys()) == set(keep)
 
 
-def test_remove(collection):
+def test_delete(collection):
     coll = collection.clone()
     all_cols = set(coll.variables.keys())
     drop = ['RT', 'parametric gain', 'respcat']
-    transform.remove(coll, drop)
+    transform.Delete(coll, drop)
     assert all_cols - set(coll.variables.keys()) == set(drop)
 
 
 def test_and(collection):
     coll = collection.clone()
-    transform.factor(coll, 'respnum')
+    transform.Factor(coll, 'respnum')
     names = ['respnum.%d' % d for d in range(0, 5)]
-    transform.and_(coll, names, output='conjunction')
+    transform.And(coll, names, output='conjunction')
     assert not coll.variables['conjunction'].values.sum()
 
     coll['copy'] = coll.variables['respnum.0'].clone()
-    transform.and_(coll, ['respnum.0', 'copy'], output='conj')
+    transform.And(coll, ['respnum.0', 'copy'], output='conj')
     assert coll.variables['conj'].values.astype(float).equals(
         coll.variables['respnum.0'].values)
 
 
 def test_or(collection):
     coll = collection.clone()
-    transform.factor(coll, 'respnum')
+    transform.Factor(coll, 'respnum')
     names = ['respnum.%d' % d for d in range(0, 5)]
-    transform.or_(coll, names, output='disjunction')
+    transform.Or(coll, names, output='disjunction')
     assert (coll.variables['disjunction'].values == 1).all()
 
     coll['copy'] = coll.variables['respnum.0'].clone()
-    transform.or_(coll, ['respnum.0', 'copy'], output='or')
+    transform.Or(coll, ['respnum.0', 'copy'], output='or')
     assert coll.variables['or'].values.astype(float).equals(
         coll.variables['respnum.0'].values)
 
@@ -356,6 +356,6 @@ def test_or(collection):
 def test_not(collection):
     coll = collection.clone()
     pre_rt = coll.variables['RT'].values.values
-    transform.not_(coll, 'RT')
+    transform.Not(coll, 'RT')
     post_rt = coll.variables['RT'].values.values
     assert (post_rt == ~pre_rt.astype(bool)).all()
diff --git a/bids/layout/tests/data/sample_validation_config.json b/bids/layout/tests/data/sample_validation_config.json
deleted file mode 100644
index cc93342e..00000000
--- a/bids/layout/tests/data/sample_validation_config.json
+++ /dev/null
@@ -1,95 +0,0 @@
-{
-    "name": "user_sequences",
-    "sequences": [
-        {
-            "modality": "func",
-            "session": "wave1",
-            "type": "bold",
-            "task": "sst",
-            "runs": 2
-        },
-        {
-            "modality": "func",
-            "session": "wave1",
-            "type": "bold",
-            "task": "gng",
-            "runs": 2
-        },
-        {
-            "modality": "func",
-            "session": "wave1",
-            "type": "bold",
-            "task": "react",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave1",
-            "type": "phasediff",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave1",
-            "type": "magnitude1",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave1",
-            "type": "magnitude2",
-            "runs": 2
-        },
-        {
-            "modality": "anat",
-            "session": "wave1",
-            "type": "T1w",
-            "runs": 1
-        },
-        {
-            "modality": "func",
-            "session": "wave2",
-            "type": "bold",
-            "task": "sst",
-            "runs": 2
-        },
-        {
-            "modality": "func",
-            "session": "wave2",
-            "type": "bold", 
-            "task": "gng",
-            "runs": 2
-        },
-        {
-            "modality": "func",
-            "session": "wave2",
-            "type": "bold",
-            "task": "react",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave2",
-            "type": "phasediff",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave2",
-            "type": "magnitude1",
-            "runs": 2
-        },
-        {
-            "modality": "fmap",
-            "session": "wave2",
-            "type": "magnitude2",
-            "runs": 2
-        },
-        {
-            "modality": "anat",
-            "session": "wave2",
-            "type": "T1w",
-            "runs": 1
-        }
-    ]
-}
\ No newline at end of file
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index 3354f560..e652dc56 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -4,7 +4,7 @@ functionality should go in the grabbit package. """
 import pytest
 from bids.layout import BIDSLayout
 from bids.layout.layout import BIDSFile
-from os.path import join, abspath, sep, basename
+from os.path import join, abspath, sep
 from bids.tests import get_test_data_path
 
 
@@ -30,20 +30,14 @@ def layout_ds117():
 @pytest.fixture(scope='module')
 def layout_ds005_derivs():
     data_dir = join(get_test_data_path(), 'ds005')
-    layout = BIDSLayout(data_dir)
-    deriv_dir = join(data_dir, 'derivatives', 'events')
-    layout.add_derivatives(deriv_dir)
-    return layout
+    return BIDSLayout(data_dir, derivatives=True)
 
 
 @pytest.fixture(scope='module')
 def layout_ds005_multi_derivs():
     data_dir = join(get_test_data_path(), 'ds005')
-    layout = BIDSLayout(data_dir)
-    deriv_dir1 = join(get_test_data_path(), 'ds005_derivs')
-    deriv_dir2 = join(data_dir, 'derivatives', 'events')
-    layout.add_derivatives([deriv_dir1, deriv_dir2])
-    return layout
+    deriv_dir = join(get_test_data_path(), 'ds005_derivs')
+    return BIDSLayout(data_dir, derivatives=['derivatives', deriv_dir])
 
 
 @pytest.fixture(scope='module')
@@ -146,7 +140,7 @@ def test_get_bvals_bvecs(layout_ds005):
 def test_get_subjects(layout_7t_trt):
     result = layout_7t_trt.get_subjects()
     predicted = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10']
-    assert set(predicted) == set(result)
+    assert predicted == result
 
 
 def test_get_fieldmap(layout_7t_trt):
@@ -168,15 +162,13 @@ def test_get_fieldmap2(layout_7t_trt):
 
 
 def test_bids_json(layout_7t_trt):
-    res = layout_7t_trt.get(return_type='id', target='run')
-    assert set(res) == {1, 2}
-    res = layout_7t_trt.get(return_type='id', target='session')
-    assert set(res) == {'1', '2'}
+    assert layout_7t_trt.get(return_type='id', target='run') == [1, 2]
+    assert layout_7t_trt.get(return_type='id', target='session') == ['1', '2']
 
 
 def test_include(layout_ds005, layout_ds005_models):
     target= join(layout_ds005_models.root, 'models',
-                'ds-005_type-russ_sub-all_model.json')
+                'ds-005_type-test_model.json')
     assert target not in layout_ds005.files
     assert target in layout_ds005_models.files
     assert 'all' not in layout_ds005_models.get_subjects()
@@ -187,50 +179,32 @@ def test_include(layout_ds005, layout_ds005_models):
 def test_layout_with_derivs(layout_ds005_derivs):
     assert layout_ds005_derivs.root == join(get_test_data_path(), 'ds005')
     assert isinstance(layout_ds005_derivs.files, dict)
-    assert len(layout_ds005_derivs.derivatives) == 1
-    deriv = layout_ds005_derivs.derivatives['events']
-    assert deriv.files
-    assert len(deriv.files) == 2
-    event_file = "sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv"
-    deriv_files = [basename(f) for f in list(deriv.files.keys())]
-    assert event_file in deriv_files
-    assert 'derivatives.roi' in deriv.entities
-    assert 'bids.roi' not in deriv.entities
-    assert 'bids.subject' in deriv.entities
+    assert set(layout_ds005_derivs.domains.keys()) == {'bids', 'derivatives'}
+    assert layout_ds005_derivs.domains['bids'].files
+    assert layout_ds005_derivs.domains['derivatives'].files
+    assert 'derivatives.roi' in layout_ds005_derivs.entities
+    assert 'bids.roi' not in layout_ds005_derivs.entities
+    assert 'bids.subject' in layout_ds005_derivs.entities
 
 
 def test_layout_with_multi_derivs(layout_ds005_multi_derivs):
     assert layout_ds005_multi_derivs.root == join(get_test_data_path(), 'ds005')
     assert isinstance(layout_ds005_multi_derivs.files, dict)
-    assert len(layout_ds005_multi_derivs.derivatives) == 2
-    deriv = layout_ds005_multi_derivs.derivatives['events']
-    assert deriv.files
-    assert len(deriv.files) == 2
-    deriv = layout_ds005_multi_derivs.derivatives['dummy']
-    assert deriv.files
-    assert len(deriv.files) == 4
-    assert 'derivatives.roi' in deriv.entities
-    assert 'bids.roi' not in deriv.entities
-    assert 'bids.subject' in deriv.entities
+    assert set(layout_ds005_multi_derivs.domains.keys()) == {'bids', 'derivatives'}
+    assert layout_ds005_multi_derivs.domains['bids'].files
+    assert layout_ds005_multi_derivs.domains['derivatives'].files
+    assert 'derivatives.roi' in layout_ds005_multi_derivs.entities
+    assert 'bids.roi' not in layout_ds005_multi_derivs.entities
+    assert 'bids.subject' in layout_ds005_multi_derivs.entities
     preproc = layout_ds005_multi_derivs.get(desc='preproc')
     assert len(preproc) == 3
 
 
 def test_query_derivatives(layout_ds005_derivs):
-    result = layout_ds005_derivs.get(suffix='events', return_type='object')
-    result = [f.filename for f in result]
-    assert len(result) == 49
-    assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' in result
     result = layout_ds005_derivs.get(suffix='events', return_type='object',
-                                     derivatives=False)
-    assert len(result) == 48
-    result = [f.filename for f in result]
-    assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' not in result
-    result = layout_ds005_derivs.get(suffix='events', return_type='object',
-                                     desc='extra')
+                              domains='derivatives')
     assert len(result) == 1
-    result = [f.filename for f in result]
-    assert 'sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv' in result
+    assert result[0].filename == 'sub-01_task-mixedgamblestask_run-01_events.tsv'
 
 
 def test_get_bidsfile_image_prop():
diff --git a/bids/tests/data/ds005/derivatives/events/dataset_description.json b/bids/tests/data/ds005/derivatives/events/dataset_description.json
deleted file mode 100644
index 5ca5c105..00000000
--- a/bids/tests/data/ds005/derivatives/events/dataset_description.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-    "PipelineDescription.Name": "events",
-    "BIDSVersion": "1.0.0rc2",
-    "License": "This dataset is made available under the Public Domain Dedication and License \nv1.0, whose full text can be found at \nhttp://www.opendatacommons.org/licenses/pddl/1.0/. \nWe hope that all users will follow the ODC Attribution/Share-Alike \nCommunity Norms (http://www.opendatacommons.org/norms/odc-by-sa/); \nin particular, while not legally required, we hope that all users \nof the data will acknowledge the OpenfMRI project and NSF Grant \nOCI-1131441 (R. Poldrack, PI) in any publications.",
-    "Name": "Mixed-gambles task",
-    "ReferencesAndLinks": "Tom, S.M., Fox, C.R., Trepel, C., Poldrack, R.A. (2007). The neural basis of loss aversion in decision-making under risk. Science, 315(5811):515-8"
-}
\ No newline at end of file
diff --git a/bids/tests/data/ds005/derivatives/events/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv b/bids/tests/data/ds005/derivatives/events/func/sub-01_task-mixedgamblestask_run-01_events.tsv
similarity index 100%
rename from bids/tests/data/ds005/derivatives/events/sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-extra_events.tsv
rename to bids/tests/data/ds005/derivatives/events/func/sub-01_task-mixedgamblestask_run-01_events.tsv
diff --git a/bids/tests/data/ds005/models/ds-005_model.json b/bids/tests/data/ds005/models/ds-005_model.json
deleted file mode 100644
index cb0bb678..00000000
--- a/bids/tests/data/ds005/models/ds-005_model.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
-  "run_model": {
-    "Level": "Run",
-    "DependentVariable": "task-mixedgamblestask",
-    "Columns": {
-      "dummy": {
-        "Variable": "respcat",
-        "Level": -1,
-        "HRFmodelling": "Gamma"
-      },
-      "gain": {
-        "Variable": "respcat",
-        "Level": 0,
-        "ModulationVar": "RT",
-        "ModulationOrder": 1,
-        "HRFmodelling": "Gamma"
-      },
-      "loss": {
-        "Variable": "respcat",
-        "Level": 1,
-        "ModulationVar": "RT",
-        "HRFmodelling": "Gamma"
-      }
-    },
-    "Contrasts": {
-      "pos_vs_neg_contrast": {
-        "Columns": ["gain", "loss"],
-        "Weights": [1, -1]
-      }
-    }
-  },
-  "group_model": {
-    "Level": "Group",
-    "DependentVariable": "pos_vs_neg_contrast",
-    "Columns": {
-      "group_mean": {
-        "Variable": "1"
-      }
-    },
-    "Contrasts": {
-      "pos_vs_neg_group": {
-        "Columns": ["group_mean"],
-        "Weights": [1]
-      }
-    }
-  }
-}
diff --git a/bids/tests/data/ds005/models/ds-005_type-russ_sub-all_model.json b/bids/tests/data/ds005/models/ds-005_type-russ_sub-all_model.json
deleted file mode 100644
index 7c83310c..00000000
--- a/bids/tests/data/ds005/models/ds-005_type-russ_sub-all_model.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
-  "run_model": {
-    "Level": "Run",
-    "DependentVariable": "task-mixedgamblestask",
-    "HighPassFilterCutoff": 80,
-    "Columns": {
-      "task": {
-        "Variable": "trial_type",
-        "Level": "n/a",
-        "HRFModelling": "Gamma",
-        "duration": 1.3932149481304148
-      },
-      "param-gain": {
-        "Variable": "trial_type",
-        "Level": "n/a",
-        "ModulationVar": "gain",
-        "Demean": true,
-        "ModulationOrder": 1,
-        "HRFModelling": "Gamma+derivs",
-        "duration": 1.3932149481304148
-
-      },
-      "param-loss": {
-        "Variable": "trial_type",
-        "Level": "n/a",
-        "ModulationVar": "loss",
-        "Demean": true,
-        "HRFModelling": "Gamma+derivs",
-        "duration": 1.3932149481304148
-
-      },
-      "param-rt": {
-        "Variable": "trial_type",
-        "Level": "n/a",
-        "ModulationVar": "RT",
-        "Demean": true,
-        "HRFModelling": "Gamma+derivs",
-        "duration": 1.3932149481304148
-      },
-      "motion-param": {
-        "FileSelector": {
-          "pattern": "derivatives/mcflirt/par/_runcode_{0[run]:1d}/_subject_id_sub-{0[sub]}/sub-{0[sub]}_task-mixedgamblestask_run-{0[run]}_bold_mcf.nii.gz.par",
-          "EntitiesKeys": {"run":"run", "subject":"sub"}
-        },
-        "Regressors": ["all", "deriv1"]
-      }
-
-    },
-    "Contrasts": {
-      "task>Baseline": {
-        "Columns": ["task"],
-        "Statistic": "T",
-        "Weights": [1]
-      },
-      "param-gain": {
-        "Columns": ["param-gain"],
-        "Statistic": "T",
-        "Weights": [1]
-      },
-      "param-loss-neg": {
-        "Columns": ["param-loss"],
-        "Statistic": "T",
-        "Weights": [-1]
-      }
-    },
-    "Error": {
-      "SerialCorrelations": true
-    }
-  },
-  "group_model": {
-    "Level": "Group",
-    "DependentVariable": "pos_vs_neg_contrast",
-    "Columns": {
-      "group_mean": {
-        "Variable": "1"
-      }
-    },
-    "Contrasts": {
-      "pos_vs_neg_group": {
-        "Columns": ["group_mean"],
-        "Weights": [1]
-      }
-    }
-  }
-}
diff --git a/bids/tests/data/ds005/models/ds-005_type-test_model.json b/bids/tests/data/ds005/models/ds-005_type-test_model.json
index 46d5b866..d629ea53 100644
--- a/bids/tests/data/ds005/models/ds-005_type-test_model.json
+++ b/bids/tests/data/ds005/models/ds-005_type-test_model.json
@@ -1,33 +1,36 @@
 {
-  "name": "test_model",
-  "description": "simple test model",
-  "blocks": [
+  "Name": "test_model",
+  "Description": "simple test model",
+  "Steps": [
     {
-      "name": "run",
-      "level": "run",
-      "transformations": [
+      "Name": "run",
+      "Level": "Run",
+      "Model": {
+        "X": ["RT", "trial_type"]
+      },
+      "Transformations": [
         {
-          "name": "scale",
-          "input": "RT"
+          "Name": "Scale",
+          "Input": "RT"
         }
       ],
-      "auto_contrasts": ["RT"],
-      "contrasts": [
+      "AutoContrasts": ["RT"],
+      "Contrasts": [
         {
-          "name": "RT-trial_type",
-          "condition_list": [
+          "Name": "RT-trial_type",
+          "ConditionList": [
             "RT",
             "trial_type"
           ],
-          "weights": [1, -1]
+          "Weights": [1, -1]
         },
         {
-          "name": "crummy-F",
-          "condition_list": [
+          "Name": "crummy-F",
+          "ConditionList": [
             "RT",
             "trial_type"
           ],
-          "weights": [
+          "Weights": [
             [1, 0],
             [0, 1]
           ]
@@ -35,19 +38,19 @@
       ]
     },
     {
-      "name": "session",
-      "level": "session",
-      "auto_contrasts": true
+      "Name": "session",
+      "Level": "Session",
+      "AutoContrasts": true
     },
     {
-      "name": "participant",
-      "level": "subject",
-      "auto_contrasts": true
+      "Name": "participant",
+      "Level": "Subject",
+      "AutoContrasts": true
     },
     {
-      "name": "group",
-      "level": "dataset",
-      "auto_contrasts": true
+      "Name": "group",
+      "Level": "Dataset",
+      "AutoContrasts": true
     }
   ]
 }
diff --git a/bids/tests/data/ds005/models/ds-005_type-trial01_model.json b/bids/tests/data/ds005/models/ds-005_type-trial01_model.json
deleted file mode 100644
index 6258c0ec..00000000
--- a/bids/tests/data/ds005/models/ds-005_type-trial01_model.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
-  "run_model": {
-    "Level": "Run",
-    "DependentVariable": "task-mixedgamblestask",
-    "Columns": {
-      "dummy": {
-        "Variable": "respcat",
-        "Level": -1,
-        "HRFModelling": "Gamma"
-      },
-      "gain": {
-        "Variable": "respcat",
-        "Level": 0,
-        "ModulationVar": "RT",
-        "ModulationOrder": 1,
-        "HRFModelling": "Gamma"
-      },
-      "loss": {
-        "Variable": "respcat",
-        "Level": 1,
-        "ModulationVar": "RT",
-        "HRFModelling": "Gamma"
-      }
-    },
-    "Contrasts": {
-      "pos_vs_neg_contrast": {
-        "Columns": ["gain", "loss"],
-        "Weights": [1, -1]
-      }
-    }
-  },
-  "group_model": {
-    "Level": "Group",
-    "DependentVariable": "pos_vs_neg_contrast",
-    "Columns": {
-      "group_mean": {
-        "Variable": "1"
-      }
-    },
-    "Contrasts": {
-      "pos_vs_neg_group": {
-        "Columns": ["group_mean"],
-        "Weights": [1]
-      }
-    }
-  }
-}
diff --git a/bids/tests/data/ds005_derivs/dummy/dataset_description.json b/bids/tests/data/ds005_derivs/dummy/dataset_description.json
deleted file mode 100644
index 424359c0..00000000
--- a/bids/tests/data/ds005_derivs/dummy/dataset_description.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "PipelineDescription.Name": "dummy",
-    "BIDSVersion": "1.0.0rc2",
-    "Name": "Mixed-gambles task -- dummy derivative",
-    "ReferencesAndLinks": "Tom, S.M., Fox, C.R., Trepel, C., Poldrack, R.A. (2007). The neural basis of loss aversion in decision-making under risk. Science, 315(5811):515-8"
-}
\ No newline at end of file
diff --git a/bids/tests/data/synthetic/derivatives/fmriprep/dataset_description.json b/bids/tests/data/synthetic/derivatives/fmriprep/dataset_description.json
deleted file mode 100644
index 68df0ddb..00000000
--- a/bids/tests/data/synthetic/derivatives/fmriprep/dataset_description.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-	"PipelineDescription.Name": "fmriprep",
-	"Name": "synthetic",
-	"BIDSVersion": "1.0.2",
-	"License": "PD",
-	"Authors": ["Markiewicz, C. J."]
-}
diff --git a/bids/variables/tests/test_io.py b/bids/variables/tests/test_io.py
index bc21cc0c..05c8ea41 100644
--- a/bids/variables/tests/test_io.py
+++ b/bids/variables/tests/test_io.py
@@ -18,9 +18,10 @@ def layout1():
 @pytest.fixture(scope="module", params=["events", "preproc"])
 def synthetic(request):
     root = join(get_test_data_path(), 'synthetic')
+    deriv = join(root, 'derivatives')
     if request.param == 'preproc':
         layout = BIDSLayout(root, derivatives=True)
-        dataset = load_variables(layout, skip_empty=True, desc='preproc',
+        dataset = load_variables(layout, skip_empty=True, suffix='preproc',
                                  space='T1w')
     else:
         layout = BIDSLayout(root)