Takes an input experiment spec and creates/modifies/archives the existing
    experiments to match the spec.

    If there's an experiment in the spec that currently doesn't exist, it will
    be created along with the associated choices.

    If there's an experiment in the spec that currently exists, and the set of
    choices are different, that experiment's choices will be modified to match
    the spec.

    If there's an experiment not in the spec that currently exists, it will be
    archived.

    A spec looks like this:

    {
       "experiment 1": ["choice 1", "choice 2", "choice 3"],
       "experiment 2": ["choice 1", "choice 2"]
    }

def sync_from_spec(redis, schema):
    """
    Takes an input experiment spec and creates/modifies/archives the existing
    experiments to match the spec.

    If there's an experiment in the spec that currently doesn't exist, it will
    be created along with the associated choices.

    If there's an experiment in the spec that currently exists, and the set of
    choices are different, that experiment's choices will be modified to match
    the spec.

    If there's an experiment not in the spec that currently exists, it will be
    archived.

    A spec looks like this:

    {
       "experiment 1": ["choice 1", "choice 2", "choice 3"],
       "experiment 2": ["choice 1", "choice 2"]
    }
    """

    def get_experiments_dict(active=True):
        """Returns a dictionary of experiment names -> experiment objects"""
        return dict((experiment.name, experiment) for experiment in get_experiments(redis, active=active))

    # Get the current experiments
    active_experiments = get_experiments_dict()
    archived_experiments = get_experiments_dict(active=False)

    # Get the newly defined experiment names and the names of the experiments
    # already setup
    new_experiment_names = set(schema.keys())
    active_experiment_names = set(active_experiments.keys())

    # Find all the experiments that are in the schema and are defined among the
    # archived experiments, but not the active ones (we check against active
    # experiments to prevent the edge case where an experiment might be defined
    # doubly in both active and archived experiments)
    unarchivable_experiment_names = (new_experiment_names - active_experiment_names) & set(archived_experiments.keys())

    # De-archive the necessary experiments
    for unarchivable_experiment_name in unarchivable_experiment_names:
        print("- De-archiving %s" % unarchivable_experiment_name)

        # Because there is no function to de-archive an experiment, it must
        # be done manually
        pipe = redis.pipeline(transaction=True)
        pipe.sadd(ACTIVE_EXPERIMENTS_REDIS_KEY, unarchivable_experiment_name)
        pipe.srem(ARCHIVED_EXPERIMENTS_REDIS_KEY, unarchivable_experiment_name)
        pipe.execute()

    # Reload the active experiments if we de-archived any
    if unarchivable_experiment_names:
        active_experiments = get_experiments_dict()
        active_experiment_names = set(active_experiments.keys())

    # Create the new experiments
    for new_experiment_name in new_experiment_names - active_experiment_names:
        print("- Creating experiment %s" % new_experiment_name)

        experiment = add_experiment(redis, new_experiment_name)

        for choice in schema[new_experiment_name]:
            print("  - Adding choice %s" % choice)
            experiment.add_choice(choice)

    # Archive experiments not defined in the schema
    for archivable_experiment_name in active_experiment_names - new_experiment_names:
        print("- Archiving %s" % archivable_experiment_name)
        active_experiments[archivable_experiment_name].archive()

    # Update the choices for existing experiments that are also defined in the
    # schema
    for experiment_name in new_experiment_names & active_experiment_names:
        experiment = active_experiments[experiment_name]
        new_choice_names = set(schema[experiment_name])
        old_choice_names = set(experiment.choice_names)

        # Add choices in the schema that do not exist yet
        for new_choice_name in new_choice_names - old_choice_names:
            print("- Adding choice %s to existing experiment %s" % (new_choice_name, experiment_name))
            experiment.add_choice(new_choice_name)

        # Remove choices that aren't in the schema
        for removable_choice_name in old_choice_names - new_choice_names:
            print("- Removing choice %s from existing experiment %s" % (removable_choice_name, experiment_name))
            experiment.remove_choice(removable_choice_name)