def filter_by_smiles(
    data_set, smiles_to_include, smiles_to_exclude, allow_partial_inclusion=False
):
    """Filters the data set so that it only contains either a specific set
    of smiles, or does not contain any of a set of specifically exluded smiles.

    Parameters
    ----------
    data_set: pandas.DataFrame
        The data set to filter
    smiles_to_include: list of str, optional
        The smiles patterns to retain. This option is mutually
        exclusive with `smiles_to_exclude`
    smiles_to_exclude: list of str, optional
        The smiles patterns to exclude. This option is mutually
        exclusive with `smiles_to_include`
    allow_partial_inclusion: bool
        If False, all the components in a substance must appear in
        the `smiles_to_include` list, otherwise, only some must appear.
        This option only applies when `smiles_to_include` is set.

    Returns
    -------
    pandas.DataFrame
        The filtered data set.
    """

    if (smiles_to_include is None and smiles_to_exclude is None) or (
        smiles_to_include is not None and smiles_to_exclude is not None
    ):

        raise ValueError(
            "Either a list of smiles to include, or a list of smiles to exclude must be provided, but not both."
        )

    if smiles_to_include is not None:
        smiles_to_exclude = []
    elif smiles_to_exclude is not None:
        smiles_to_include = []

    def filter_function(data_row):

        n_components = data_row["N Components"]

        component_smiles = [
            data_row[f"Component {index + 1}"] for index in range(n_components)
        ]

        if any(x in smiles_to_exclude for x in component_smiles):
            return False
        elif len(smiles_to_exclude) > 0:
            return True

        if not allow_partial_inclusion and not all(
            x in smiles_to_include for x in component_smiles
        ):
            return False

        if allow_partial_inclusion and not any(
            x in smiles_to_include for x in component_smiles
        ):
            return False

        return True

    return data_set[data_set.apply(filter_function, axis=1)]