<commit_before>"""
This module contains the :class:`Experiment` class and associated helper functions.
Public objects are imported in ``__init__.py``.

"""
import os
import pickle
import inspect
import functools
from logging import getLogger
from importlib import import_module
from contextlib import contextmanager, ExitStack
from datetime import datetime
from collections import ChainMap

try:
    import yaml
except ImportError:
    yaml = None

from experimentator.section import ExperimentSection
from experimentator.design import DesignTree, Design
import experimentator.order as order

logger = getLogger(__name__)


def run_experiment_section(experiment, section_obj=None, demo=False, resume=False, parent_callbacks=True,
                           from_section=1, session_options='', **section_numbers):
    """Run an experiment section.

    Run an experiment from a file or an :class:`Experiment` instance, and save it.
    If an exception is encountered, the :class:`Experiment` will be backed up and saved.

    Parameters
    ----------
    experiment : str or :class:`Experiment`
        File location where an :class:`Experiment` instance is pickled,
        or an :class:`Experiment` instance.
    demo : bool, optional
        If True, data will not be saved and sections will not be marked as run.
    resume: bool, optional
        If True, the specified section will be resumed
        (started automatically where it left off).
    parent_callbacks : bool, optional
        If True (the default), all parent callbacks will be called.
    section_obj : :class:`ExperimentSection`, optional
        The section of the experiment to run.
        Alternatively, the section can be specified in the keyword arguments.
    from_section : int or list of int, optional
        Which section to start running from.
        If a list is passed, it specifies where to start running on multiple levels.
        See the example below.
    session_options : str, optional
        Pass an experiment-specific options string to be stored in :attr:`Experiment.session_data`
        under the key ``'options'``.
    **section_numbers
        Keyword arguments describing how to descend the experiment hierarchy to find the section to run.
        See the example below.

    Examples
    --------
    A simple example:
        >>> exp = Experiment.load('example.exp')
        >>> run_experiment_section(exp, exp.subsection(participant=1, session=2))

    Equivalently:

        >>> run_experiment_section(exp, participant=1, session=2)

    To demonstrate `from_section`,
    assuming the experiment hierarchy is ``('participant', 'session', 'block', 'trial')``,
    this would start from the second block:

        >>> run_experiment_section(exp, participant=1, session=2, from_section=2)

    To start from the fifth trial of the second block:

        >>> run_experiment_section(exp, participant=1, session=2, from_section=[2, 5])

    """
    if isinstance(experiment, Experiment):
        exp = experiment
    else:
        exp = Experiment.load(experiment)
    exp.session_data['options'] = session_options

    if not section_obj:
        section_obj = exp.subsection(**section_numbers)

    try:
        if resume:
            exp.resume_section(section_obj, demo=demo, parent_callbacks=parent_callbacks)
        else:
            exp.run_section(section_obj, demo=demo, parent_callbacks=parent_callbacks, from_section=from_section)

    except:
        logger.warning('Exception occurred saving.')
        # Backup experiment file.
        os.rename(exp.filename,
                  exp.filename + datetime.now().strftime('.%m-%d-%H-%M-backup'))
        raise

    finally:
        exp.save()


def export_experiment_data(exp_filename, data_filename, **kwargs):
    """ Export data.

    Reads a pickled :class:`Experiment` instance and saves its data in ``.csv`` format.

    Parameters
     ----------
    exp_filename : str
        The file location where an :class:`Experiment` instance is pickled.
    data_filename : str
        The file location where the data will be written.
    skip_columns : list of str, optional
        Data columns to skip.
    **kwargs
        Arbitrary keyword arguments passed through to :meth:`pandas.Dataframe.to_csv`.

    Notes
    -----
    This shortcut function is not recommended for experiments with compound data types,
    for example an experiment which stores a time series for every trial.
    In such cases it is recommended to write a custom script
    that parses :attr:`Experiment.dataframe` as desired
    (or use the `skip_columns` option to ignore the compound data).

    """
    Experiment.load(exp_filename).export_data(data_filename, **kwargs)


class Experiment(ExperimentSection):
    """
    An :class:`~experimentator.section.ExperimentSection` subclass
    that represents the largest 'section' of the experiment;
    that is, the entire experiment.
    Functionality added on top of :class:`~experimentator.section.ExperimentSection` includes
    various constructors,
    saving to disk, and
    management of callbacks.

    Parameters
    ----------
    tree : :class:`~experimentator.design.DesignTree`
        A :class:`~experimentator.design.DesignTree` instance defining the experiment hierarchy.
    filename : str, optional
        A file location where the :class:`Experiment` will be saved.

    Attributes
    ----------
    tree : :class:`~experimentator.design.DesignTree`
        The :class:`~experimentator.design.DesignTree` instance defining the experiment's hierarchy.
    filename : str
        The file location where the :class:`Experiment` will be pickled.
    run_callback : func
        The function to be run when the bottom of the tree is reached (i.e., the trial function).
    context_managers : dict
        A dictionary, mapping level names to :std:label:`context-managers`
        (e.g., generator functions decorated with :func:`contextlib.contextmanager`).
        Defines behavior to run before and/or after each section at the associated level.
    session_data : dict
        A dictionary where temporary data can be stored,
        persistent only within one session of the Python interpreter.
        This is a good place to store external resources that aren't :ref:`picklable <pickle-picklable>`;
        external resources, for example, can be loaded in a
        :ref:`context-manager <typecontextmanager>` callback and stored here.
        In addition, anything returned by the ``__exit__`` method of a context-manager callback
        will be stored here, with the callback's level name as the key.
        This dictionary is emptied before saving the :class:`Experiment` to disk.
    experiment_data : dict
        A dictionary where data can be stored that is persistent across Python sessions.
        Everything here must be :ref:`picklable <pickle-picklable>`.

    """
    def __init__(self, tree, filename=None):
        tree.add_base_level()
        super().__init__(tree, ChainMap())

        self.filename = filename

        self.context_managers = {level: _dummy_context for level in self.levels}
        self.context_managers['_base'] = _dummy_context
        self._context_info = {level: None for level in self.levels}

        self.run_callback = _dummy_callback
        self._callback_info = None

        self.session_data = {}
        self.experiment_data = {}

    @staticmethod
    def load(filename):
        """Load an experiment from disk.

        Parameters
        ----------
        filename : str
            Path to a file generated by :attr:`Experiment.save`.

        Returns
        -------
        :class:`Experiment`

        """
        with open(filename, 'rb') as f:
            self = pickle.load(f)
        self.filename = filename
        return self

    @classmethod
    def from_dict(cls, spec):
        """Experiment from dict.

        Constructs an :class:`Experiment` based on a dictionary specification.

        Parameters
        ----------
        spec : dict
            `spec` should have, at minimum, a key named ``'design'``.
            The value of this key specifies the :class:`~experimentator.design.DesignTree`.
            See :meth:`DesignTree.from_spec() <experimentator.design.DesignTree.from_spec>` for details.
            The value of the key ``'filename'`` or ``'file'``, if one exists,
            is saved in  :attr:`Experiment.filename`
            All other fields are saved in the :attr:`Experiment.experiment_data`.

        Returns
        -------
        :class:`Experiment`

        See Also
        --------
        Experiment.from_yaml_file
        DesignTree.from_spec
        Design.from_dict

        """
        tree = DesignTree.from_spec(spec.pop('design'))
        filename = spec.pop('filename', spec.pop('file', None))
        self = cls(tree, filename=filename)
        self.experiment_data = spec
        return self

    @classmethod
    def from_yaml_file(cls, filename):
        """Experiment from YAML file.

        Constructs an :class:`Experiment` based on specification in a YAML file.
        Requires module `PyYAML <http://pyyaml.org/wiki/PyYAML>`_.

        Parameters
        ----------
        filename : str
            YAML file location.
            The YAML should specify a dictionary matching the specification of :meth:`Experiment.from_dict`.

        Returns
        -------
        :class:`Experiment`

        See Also
        --------
        Experiment.from_dict
        DesignTree.from_spec
        Design.from_dict

        """
        if not yaml:
            raise ImportError('PyYAML is not installed')
        with open(filename, 'r') as f:
            spec = yaml.load(f)
        return cls.from_dict(spec)

    @classmethod
    def within_subjects(cls, ivs, n_participants, design_matrix=None, ordering=None, filename=None):
        """Construct a within-subjects experiment.

        This function creates a within-subjects :class:`Experiment`,
        in which all the IVs are at the trial level.

        Parameters
        ----------
        ivs : list or dict
            A list of the experiment's IVs, specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        n_participants : int
            Number of participants to initialize.
        design_matrix : array-like, optional
            Design matrix for the experiment. If not specified, IVs will be fully crossed.
            See documentation for :class:`~experimentator.design.Design` for more on design matrices.
        ordering : Ordering, optional
            An instance of the class :class:`~experimentator.order.Ordering` or one of its subclasses,
            specifying how the trials will be ordered.
            If not specified, :class:`~experimentator.order.Shuffle` will be used.
        filename : str, optional
            File location to save the experiment.

        Returns
        -------
        :class:`Experiment`

        """
        levels_and_designs = [('participant', [Design(ordering=order.Shuffle(n_participants))]),
                              ('trial', [Design(ivs=ivs, design_matrix=design_matrix, ordering=ordering)])]

        return cls(DesignTree(levels_and_designs), filename=filename)

    @classmethod
    def blocked(cls, trial_ivs, n_participants, design_matrices=None, orderings=None, block_ivs=None, filename=None):
        """Construct a blocked, within-subjects experiment.

        This function creates a blocked within-subjects :class:`Experiment`,
        in which all the IVs are at either the trial level or the block level.

        Parameters
        ----------
        trial_ivs : list or dict
            A list of the IVs to define at the trial level, specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        n_participants : int
            Number of participants to initialize.
            If a :class:`~experimentator.order.NonAtomicOrdering` is used,
            this is the number of participants per order.
        design_matrices : dict, optional
            Design matrices for the experiment.
            Keys are ``'trial'`` and ``'block'``; values are the respective design matrices (if any).
            If not specified, IVs will be fully crossed.
            See documentation for :class:`~experimentator.design.Design` for more on design matrices.
        orderings : dict, optional
            Dictionary with keys of ``'trial'`` and ``'block'``.
            Each value should be an instance of the class `Ordering` or one of its descendants,
            specifying how the trials will be ordered
            If not specified, :class:`~experimentator.order.Shuffle` will be used.
        block_ivs : list or dict, optional
            IVs to define at the block level.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        filename : str, optional
            File location to save the experiment.

        Notes
        -----
        For blocks to have any effect,
        you should either define at least one IV at the block level
        or use the ordering ``Ordering(n)`` to create ``n`` blocks for every participant.

        Returns
        -------
        :class:`Experiment`

        """
        if not design_matrices:
            design_matrices = {}
        if not orderings:
            orderings = {}

        levels_and_designs = [('participant', [Design(ordering=order.Shuffle(n_participants))]),
                              ('block', [Design(ivs=block_ivs,
                                                design_matrix=design_matrices.get('block'),
                                                ordering=orderings.get('block'))]),
                              ('trial', [Design(ivs=trial_ivs,
                                                design_matrix=design_matrices.get('trial'),
                                                ordering=orderings.get('trial'))])]

        return cls(DesignTree(levels_and_designs), filename=filename)

    @classmethod
    def basic(cls, levels, ivs_by_level, design_matrices_by_level=None, ordering_by_level=None, filename=None):
        """Construct a basic, homogeneously-organized experiment.

        This function builds a basic :class:`Experiment`,
        which is to say an experiment that has arbitrary levels
        but only one :class:`~experimentator.design.Design` at each level,
        and the same structure everywhere in the hierarchy.

        Parameters
        ----------
        levels : sequence of str
            Names of the levels of the experiment
        ivs_by_level : dict
            Dictionary specifying the IVs and their possible values at every level.
            The keys are be the level names,
            and the values are lists of the IVs at that level,
            specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        design_matrices_by_level : dict, optional
            Specify the design matrix for any levels.
            Keys are level names; values are design matrices.
            Any levels without a design matrix will be fully crossed.
            See :class:`~experimentator.design.Design` for more on design matrices.
        ordering_by_level : dict, optional
            Specify the ordering for each level.
            Keys are level names; values are instance objects from :mod:`experimentator.order`.
            For any levels without an order specified, :class:`~experimentator.order.Shuffle` will be used.
        filename : str, optional
            File location to save the experiment.

        Returns
        -------
        :class:`Experiment`

        """
        if not design_matrices_by_level:
            design_matrices_by_level = {}
        if not ordering_by_level:
            ordering_by_level = {}

        levels_and_designs = [(level, [Design(ivs=ivs_by_level.get(level),
                                       design_matrix=design_matrices_by_level.get(level),
                                       ordering=ordering_by_level.get(level))])
                              for level in levels]

        return cls(DesignTree(levels_and_designs), filename=filename)

    def __repr__(self):
        return "Experiment({}, filename='{}')".format(self.tree.__repr__(), self.filename)

    def save(self, filename=None):
        """Save experiment.

        Pickles the `Experiment` to the location in `Experiment.filename`.

        Parameters
        ----------
        filename : str, optional
            If specified, overrides `Experiment.filename`.

        """
        filename = filename or self.filename
        if filename:
            logger.debug('Saving Experiment instance to {}.'.format(filename))
            logger.debug('Saving Experiment instance to {}.'.format(filename))
            with open(filename, 'wb') as f:
                pickle.dump(self, f)

        else:
            logger.warning('Cannot save experiment: No filename provided.')

    def export_data(self, filename, skip_columns=None, **kwargs):
        """Export data.

        Exports `Experiment.data` in ``.csv`` format.

        Parameters
        ----------
        filename : str
            A file location where the data should be saved.
        skip_columns : list of str, optional
            Columns to skip.
        **kwargs
            Arbitrary keyword arguments passed through to `pandas.Dataframe.to_csv`.
<commit_msg>fix Experiment.save docstring<commit_after>"""
This module contains the :class:`Experiment` class and associated helper functions.
Public objects are imported in ``__init__.py``.

"""
import os
import pickle
import inspect
import functools
from logging import getLogger
from importlib import import_module
from contextlib import contextmanager, ExitStack
from datetime import datetime
from collections import ChainMap

try:
    import yaml
except ImportError:
    yaml = None

from experimentator.section import ExperimentSection
from experimentator.design import DesignTree, Design
import experimentator.order as order

logger = getLogger(__name__)


def run_experiment_section(experiment, section_obj=None, demo=False, resume=False, parent_callbacks=True,
                           from_section=1, session_options='', **section_numbers):
    """Run an experiment section.

    Run an experiment from a file or an :class:`Experiment` instance, and save it.
    If an exception is encountered, the :class:`Experiment` will be backed up and saved.

    Parameters
    ----------
    experiment : str or :class:`Experiment`
        File location where an :class:`Experiment` instance is pickled,
        or an :class:`Experiment` instance.
    demo : bool, optional
        If True, data will not be saved and sections will not be marked as run.
    resume: bool, optional
        If True, the specified section will be resumed
        (started automatically where it left off).
    parent_callbacks : bool, optional
        If True (the default), all parent callbacks will be called.
    section_obj : :class:`ExperimentSection`, optional
        The section of the experiment to run.
        Alternatively, the section can be specified in the keyword arguments.
    from_section : int or list of int, optional
        Which section to start running from.
        If a list is passed, it specifies where to start running on multiple levels.
        See the example below.
    session_options : str, optional
        Pass an experiment-specific options string to be stored in :attr:`Experiment.session_data`
        under the key ``'options'``.
    **section_numbers
        Keyword arguments describing how to descend the experiment hierarchy to find the section to run.
        See the example below.

    Examples
    --------
    A simple example:
        >>> exp = Experiment.load('example.exp')
        >>> run_experiment_section(exp, exp.subsection(participant=1, session=2))

    Equivalently:

        >>> run_experiment_section(exp, participant=1, session=2)

    To demonstrate `from_section`,
    assuming the experiment hierarchy is ``('participant', 'session', 'block', 'trial')``,
    this would start from the second block:

        >>> run_experiment_section(exp, participant=1, session=2, from_section=2)

    To start from the fifth trial of the second block:

        >>> run_experiment_section(exp, participant=1, session=2, from_section=[2, 5])

    """
    if isinstance(experiment, Experiment):
        exp = experiment
    else:
        exp = Experiment.load(experiment)
    exp.session_data['options'] = session_options

    if not section_obj:
        section_obj = exp.subsection(**section_numbers)

    try:
        if resume:
            exp.resume_section(section_obj, demo=demo, parent_callbacks=parent_callbacks)
        else:
            exp.run_section(section_obj, demo=demo, parent_callbacks=parent_callbacks, from_section=from_section)

    except:
        logger.warning('Exception occurred saving.')
        # Backup experiment file.
        os.rename(exp.filename,
                  exp.filename + datetime.now().strftime('.%m-%d-%H-%M-backup'))
        raise

    finally:
        exp.save()


def export_experiment_data(exp_filename, data_filename, **kwargs):
    """ Export data.

    Reads a pickled :class:`Experiment` instance and saves its data in ``.csv`` format.

    Parameters
     ----------
    exp_filename : str
        The file location where an :class:`Experiment` instance is pickled.
    data_filename : str
        The file location where the data will be written.
    skip_columns : list of str, optional
        Data columns to skip.
    **kwargs
        Arbitrary keyword arguments passed through to :meth:`pandas.Dataframe.to_csv`.

    Notes
    -----
    This shortcut function is not recommended for experiments with compound data types,
    for example an experiment which stores a time series for every trial.
    In such cases it is recommended to write a custom script
    that parses :attr:`Experiment.dataframe` as desired
    (or use the `skip_columns` option to ignore the compound data).

    """
    Experiment.load(exp_filename).export_data(data_filename, **kwargs)


class Experiment(ExperimentSection):
    """
    An :class:`~experimentator.section.ExperimentSection` subclass
    that represents the largest 'section' of the experiment;
    that is, the entire experiment.
    Functionality added on top of :class:`~experimentator.section.ExperimentSection` includes
    various constructors,
    saving to disk, and
    management of callbacks.

    Parameters
    ----------
    tree : :class:`~experimentator.design.DesignTree`
        A :class:`~experimentator.design.DesignTree` instance defining the experiment hierarchy.
    filename : str, optional
        A file location where the :class:`Experiment` will be saved.

    Attributes
    ----------
    tree : :class:`~experimentator.design.DesignTree`
        The :class:`~experimentator.design.DesignTree` instance defining the experiment's hierarchy.
    filename : str
        The file location where the :class:`Experiment` will be pickled.
    run_callback : func
        The function to be run when the bottom of the tree is reached (i.e., the trial function).
    context_managers : dict
        A dictionary, mapping level names to :std:label:`context-managers`
        (e.g., generator functions decorated with :func:`contextlib.contextmanager`).
        Defines behavior to run before and/or after each section at the associated level.
    session_data : dict
        A dictionary where temporary data can be stored,
        persistent only within one session of the Python interpreter.
        This is a good place to store external resources that aren't :ref:`picklable <pickle-picklable>`;
        external resources, for example, can be loaded in a
        :ref:`context-manager <typecontextmanager>` callback and stored here.
        In addition, anything returned by the ``__exit__`` method of a context-manager callback
        will be stored here, with the callback's level name as the key.
        This dictionary is emptied before saving the :class:`Experiment` to disk.
    experiment_data : dict
        A dictionary where data can be stored that is persistent across Python sessions.
        Everything here must be :ref:`picklable <pickle-picklable>`.

    """
    def __init__(self, tree, filename=None):
        tree.add_base_level()
        super().__init__(tree, ChainMap())

        self.filename = filename

        self.context_managers = {level: _dummy_context for level in self.levels}
        self.context_managers['_base'] = _dummy_context
        self._context_info = {level: None for level in self.levels}

        self.run_callback = _dummy_callback
        self._callback_info = None

        self.session_data = {}
        self.experiment_data = {}

    @staticmethod
    def load(filename):
        """Load an experiment from disk.

        Parameters
        ----------
        filename : str
            Path to a file generated by :attr:`Experiment.save`.

        Returns
        -------
        :class:`Experiment`

        """
        with open(filename, 'rb') as f:
            self = pickle.load(f)
        self.filename = filename
        return self

    @classmethod
    def from_dict(cls, spec):
        """Experiment from dict.

        Constructs an :class:`Experiment` based on a dictionary specification.

        Parameters
        ----------
        spec : dict
            `spec` should have, at minimum, a key named ``'design'``.
            The value of this key specifies the :class:`~experimentator.design.DesignTree`.
            See :meth:`DesignTree.from_spec() <experimentator.design.DesignTree.from_spec>` for details.
            The value of the key ``'filename'`` or ``'file'``, if one exists,
            is saved in  :attr:`Experiment.filename`
            All other fields are saved in the :attr:`Experiment.experiment_data`.

        Returns
        -------
        :class:`Experiment`

        See Also
        --------
        Experiment.from_yaml_file
        DesignTree.from_spec
        Design.from_dict

        """
        tree = DesignTree.from_spec(spec.pop('design'))
        filename = spec.pop('filename', spec.pop('file', None))
        self = cls(tree, filename=filename)
        self.experiment_data = spec
        return self

    @classmethod
    def from_yaml_file(cls, filename):
        """Experiment from YAML file.

        Constructs an :class:`Experiment` based on specification in a YAML file.
        Requires module `PyYAML <http://pyyaml.org/wiki/PyYAML>`_.

        Parameters
        ----------
        filename : str
            YAML file location.
            The YAML should specify a dictionary matching the specification of :meth:`Experiment.from_dict`.

        Returns
        -------
        :class:`Experiment`

        See Also
        --------
        Experiment.from_dict
        DesignTree.from_spec
        Design.from_dict

        """
        if not yaml:
            raise ImportError('PyYAML is not installed')
        with open(filename, 'r') as f:
            spec = yaml.load(f)
        return cls.from_dict(spec)

    @classmethod
    def within_subjects(cls, ivs, n_participants, design_matrix=None, ordering=None, filename=None):
        """Construct a within-subjects experiment.

        This function creates a within-subjects :class:`Experiment`,
        in which all the IVs are at the trial level.

        Parameters
        ----------
        ivs : list or dict
            A list of the experiment's IVs, specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        n_participants : int
            Number of participants to initialize.
        design_matrix : array-like, optional
            Design matrix for the experiment. If not specified, IVs will be fully crossed.
            See documentation for :class:`~experimentator.design.Design` for more on design matrices.
        ordering : Ordering, optional
            An instance of the class :class:`~experimentator.order.Ordering` or one of its subclasses,
            specifying how the trials will be ordered.
            If not specified, :class:`~experimentator.order.Shuffle` will be used.
        filename : str, optional
            File location to save the experiment.

        Returns
        -------
        :class:`Experiment`

        """
        levels_and_designs = [('participant', [Design(ordering=order.Shuffle(n_participants))]),
                              ('trial', [Design(ivs=ivs, design_matrix=design_matrix, ordering=ordering)])]

        return cls(DesignTree(levels_and_designs), filename=filename)

    @classmethod
    def blocked(cls, trial_ivs, n_participants, design_matrices=None, orderings=None, block_ivs=None, filename=None):
        """Construct a blocked, within-subjects experiment.

        This function creates a blocked within-subjects :class:`Experiment`,
        in which all the IVs are at either the trial level or the block level.

        Parameters
        ----------
        trial_ivs : list or dict
            A list of the IVs to define at the trial level, specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        n_participants : int
            Number of participants to initialize.
            If a :class:`~experimentator.order.NonAtomicOrdering` is used,
            this is the number of participants per order.
        design_matrices : dict, optional
            Design matrices for the experiment.
            Keys are ``'trial'`` and ``'block'``; values are the respective design matrices (if any).
            If not specified, IVs will be fully crossed.
            See documentation for :class:`~experimentator.design.Design` for more on design matrices.
        orderings : dict, optional
            Dictionary with keys of ``'trial'`` and ``'block'``.
            Each value should be an instance of the class `Ordering` or one of its descendants,
            specifying how the trials will be ordered
            If not specified, :class:`~experimentator.order.Shuffle` will be used.
        block_ivs : list or dict, optional
            IVs to define at the block level.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        filename : str, optional
            File location to save the experiment.

        Notes
        -----
        For blocks to have any effect,
        you should either define at least one IV at the block level
        or use the ordering ``Ordering(n)`` to create ``n`` blocks for every participant.

        Returns
        -------
        :class:`Experiment`

        """
        if not design_matrices:
            design_matrices = {}
        if not orderings:
            orderings = {}

        levels_and_designs = [('participant', [Design(ordering=order.Shuffle(n_participants))]),
                              ('block', [Design(ivs=block_ivs,
                                                design_matrix=design_matrices.get('block'),
                                                ordering=orderings.get('block'))]),
                              ('trial', [Design(ivs=trial_ivs,
                                                design_matrix=design_matrices.get('trial'),
                                                ordering=orderings.get('trial'))])]

        return cls(DesignTree(levels_and_designs), filename=filename)

    @classmethod
    def basic(cls, levels, ivs_by_level, design_matrices_by_level=None, ordering_by_level=None, filename=None):
        """Construct a basic, homogeneously-organized experiment.

        This function builds a basic :class:`Experiment`,
        which is to say an experiment that has arbitrary levels
        but only one :class:`~experimentator.design.Design` at each level,
        and the same structure everywhere in the hierarchy.

        Parameters
        ----------
        levels : sequence of str
            Names of the levels of the experiment
        ivs_by_level : dict
            Dictionary specifying the IVs and their possible values at every level.
            The keys are be the level names,
            and the values are lists of the IVs at that level,
            specified in the form of tuples
            with the first element being the IV name and the second element a list of its possible values.
            Alternatively, the IVs at each level can be specified in a dictionary.
            See documentation for :class:`~experimentator.design.Design` for more information on specifying IVs.
        design_matrices_by_level : dict, optional
            Specify the design matrix for any levels.
            Keys are level names; values are design matrices.
            Any levels without a design matrix will be fully crossed.
            See :class:`~experimentator.design.Design` for more on design matrices.
        ordering_by_level : dict, optional
            Specify the ordering for each level.
            Keys are level names; values are instance objects from :mod:`experimentator.order`.
            For any levels without an order specified, :class:`~experimentator.order.Shuffle` will be used.
        filename : str, optional
            File location to save the experiment.

        Returns
        -------
        :class:`Experiment`

        """
        if not design_matrices_by_level:
            design_matrices_by_level = {}
        if not ordering_by_level:
            ordering_by_level = {}

        levels_and_designs = [(level, [Design(ivs=ivs_by_level.get(level),
                                       design_matrix=design_matrices_by_level.get(level),
                                       ordering=ordering_by_level.get(level))])
                              for level in levels]

        return cls(DesignTree(levels_and_designs), filename=filename)

    def __repr__(self):
        return "Experiment({}, filename='{}')".format(self.tree.__repr__(), self.filename)

    def save(self, filename=None):
        """Save experiment.

        Pickles the :class:`Experiment` to the location in :attr:`Experiment.filename`.

        Parameters
        ----------
        filename : str, optional
            If specified, overrides :attr:`Experiment.filename`.

        """
        filename = filename or self.filename
        if filename:
            logger.debug('Saving Experiment instance to {}.'.format(filename))
            logger.debug('Saving Experiment instance to {}.'.format(filename))
            with open(filename, 'wb') as f:
                pickle.dump(self, f)

        else:
            logger.warning('Cannot save experiment: No filename provided.')

    def export_data(self, filename, skip_columns=None, **kwargs):
        """Export data.

        Exports `Experiment.data` in ``.csv`` format.

        Parameters
        ----------
        filename : str
            A file location where the data should be saved.
        skip_columns : list of str, optional
            Columns to skip.
        **kwargs
            Arbitrary keyword arguments passed through to `pandas.Dataframe.to_csv`.

{'fraction_non_alphanumeric': 0.085367520432988, 'fraction_numerical': 0.0006560424240767569, 'mean_word_length': 2.3434472674099798, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 22, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2946872', 'n_tokens_mistral': 9271, 'n_tokens_neox': 8961, 'n_words': 3338}