Esempio n. 1
0
def test_fully_cross_block_grid_variables():
    assert FullyCrossBlock([color, text, con_factor], [color, text],
                           []).grid_variables() == 24

    # Should include grid variables, as well as additional variables for complex windows.
    assert FullyCrossBlock([color, text, color_repeats_factor], [color, text],
                           []).grid_variables() == 16
Esempio n. 2
0
def test_fully_cross_block_variables_per_trial():
    assert FullyCrossBlock([color, text], [], []).variables_per_trial() == 4
    assert FullyCrossBlock([color, text, con_factor], [],
                           []).variables_per_trial() == 6

    # Should exclude Transition and Windows from variables per trial count, as they don't always
    # have a representation in the first few trials. (Depending on the window width)
    assert FullyCrossBlock([color, text, color_repeats_factor], [color, text],
                           []).variables_per_trial() == 4
Esempio n. 3
0
def fully_cross_block(design: List[Factor],
                      crossing: List[Factor],
                      constraints: List[Constraint],
                      require_complete_crossing=True,
                      cnf_fn=to_cnf_tseitin) -> Block:
    """Returns a fully crossed :class:`.Block` meant to be used in experiment
    synthesis. This is the preferred mechanism for describing an experiment.

    :param design:
        A :class:`list` of all the :class:`Factors <.Factor>` in the design.
        When a sequence of trials is generated, each trial will have one level
        from each factor in ``design``.

    :param crossing:
        A :class:`list` of :class:`Factors <.Factor>` used to produce
        crossings. The number of trials in each run of the experiment is
        determined as the product of the number of levels of factors in
        ``crossing``.

        If ``require_complete_crossing`` is ``False``, the ``constraints`` can
        reduce the total number of trials.

        Different trial sequences of the experiment will have different
        combinations of levels in different orders. The factors in ``crossing``
        supply an implicit constraint that every combination of levels in the
        cross should appear once. Derived factors impose additional
        constraints: only combinations of levels that are consistent with
        derivations can appear as a trial. Additional constraints can be
        manually imposed via the ``constraints`` parameter.

    :param constraints:
        A :class:`list` of :class:`Constraints <.Constraint>` that restrict the
        generated trials.

    :param require_complete_crossing:
        Whether every combination in ``crossing`` must appear in a block of
        trials. ``True`` by default. A ``False`` value is appropriate if
        combinations are excluded through an :class:`.Exclude`
        :class:`.Constraint`.

    :param cnf_fn:
        A CNF conversion function. Default is :func:`.to_cnf_tseitin`.
    """
    all_constraints = cast(List[Constraint],
                           [FullyCross(), Consistency()]) + constraints
    all_constraints = __desugar_constraints(
        all_constraints)  #expand the constraints into a form we can process.
    block = FullyCrossBlock(design, [crossing], all_constraints,
                            require_complete_crossing, cnf_fn)
    block.constraints += DerivationProcessor.generate_derivations(block)
    if not constraints and not list(filter(
            lambda f: f.is_derived(), crossing)) and not list(
                filter(lambda f: f.has_complex_window, design)):
        block.complex_factors_or_constraints = False
    return block
Esempio n. 4
0
def test_fully_cross_block_variables_for_factor():
    assert FullyCrossBlock([color, text], [[color, text]],
                           []).variables_for_factor(color) == 8
    assert FullyCrossBlock([color, text], [[color, text]],
                           []).variables_for_factor(text) == 8

    assert FullyCrossBlock([color, text, color_repeats_factor],
                           [[color, text]],
                           []).variables_for_factor(color_repeats_factor) == 6
    assert FullyCrossBlock([color, text, color_repeats_factor],
                           [[color, text]],
                           []).variables_for_factor(color_repeats_factor) == 6

    assert FullyCrossBlock([color3_repeats_factor, color3, text],
                           [[color3, text]],
                           []).variables_for_factor(color3) == 18

    assert FullyCrossBlock([color3_repeats_factor, color3, text],
                           [[color3, text]],
                           []).variables_for_factor(text) == 12

    assert FullyCrossBlock([color3_repeats_factor, color3, text],
                           [[color3, text]],
                           []).variables_for_factor(color3_repeats_factor) == 8

    assert FullyCrossBlock([color, text, congruent_bookend], [[color, text]],
                           []).variables_for_factor(congruent_bookend) == 4
Esempio n. 5
0
def test_fully_cross_block_crossing_size_with_overlapping_exclude():
    # How about with two overlapping exclude constraints? Initial crossing size
    # should be 3 x 3 = 9.
    # Excluding congruent pairs will reduce that to 9 - 3 = 6
    # Then excluding red and green on top of that should make it 5.
    color = Factor("color", ["red", "blue", "green"])
    text = Factor("text", ["red", "blue", "green"])

    congruent_factor = Factor("congruent?", [
        DerivedLevel("congruent", WithinTrial(op.eq, [color, text])),
        DerivedLevel("incongruent", WithinTrial(op.ne, [color, text])),
    ])

    def illegal(color, text):
        return (color == "red" and text == "green") or color == text

    def legal(color, text):
        return not illegal(color, text)

    legal_factor = Factor("legal", [
        DerivedLevel("yes", WithinTrial(legal, [color, text])),
        DerivedLevel("no", WithinTrial(illegal, [color, text]))
    ])

    assert FullyCrossBlock(
        [color, text, congruent_factor, legal_factor],
        [[color, text]],
        [
            Exclude(congruent_factor,
                    get_level_from_name(congruent_factor,
                                        "congruent")),  # Excludes 3
            Exclude(legal_factor, get_level_from_name(legal_factor, "no"))
        ],  # Exludes 4, but 3 were already excluded
        require_complete_crossing=False).crossing_size() == 5
Esempio n. 6
0
def test_fully_cross_block_validate():
    # Should not allow DerivedLevels in the crossing.
    # I think it makes sense to prohibit this, but I could be wrong. At the very least,
    # this will leave a reminder that, if it does make sense, there is more work in the
    # codebase to allow it correctly. The FullyCross constraint won't handle it right now.
    with pytest.raises(ValueError):
        FullyCrossBlock([color, text, con_factor], [color, text, con_factor],
                        [])
Esempio n. 7
0
def fully_cross_block(design: List[Factor],
                      crossing: List[Factor],
                      constraints: List[Constraint],
                      cnf_fn=to_cnf_tseitin) -> Block:
    all_constraints = cast(List[Constraint],
                           [FullyCross, Consistency]) + constraints
    block = FullyCrossBlock(design, crossing, all_constraints, cnf_fn)
    block.constraints += DerivationProcessor.generate_derivations(block)
    return block
Esempio n. 8
0
    def apply(block: FullyCrossBlock, backend_request: BackendRequest) -> None:
        fresh = backend_request.fresh

        # Step 1: Get a list of the trials that are involved in the crossing.
        crossing_size = max(block.min_trials, block.crossing_size())
        crossing_trials = list(filter(lambda t: all(map(lambda f: f.applies_to_trial(t),
                                                        block.crossing[0])),
                                      range(1, block.trials_per_sample() + 1)))
        crossing_trials = crossing_trials[:crossing_size]

        # Step 2: For each trial, cross all levels of all factors in the crossing.
        crossing_factors = list(map(lambda t: (list(product(*[block.factor_variables_for_trial(f, t)
                                                              for f in block.crossing[0]]))),
                                    crossing_trials))

        # Step 3: For each trial, cross all levels of all design-only factors in the crossing.
        design_factors = cast(List[List[List[int]]], [])
        design_factors = list(map(lambda _: [], crossing_trials))
        for f in list(filter(lambda f: f not in block.crossing[0] and not f.has_complex_window, block.design)):
            for i, t in enumerate(crossing_trials):
                design_factors[i].append(block.factor_variables_for_trial(f, t))
        design_combinations = cast(List[List[Tuple[int, ...]]], [])
        design_combinations = list(map(lambda l: list(product(*l)), design_factors))

        # Step 4: For each trial, combine each of the crossing factors with all of the design-only factors.
        crossings = cast(List[List[List[Tuple[int, ...]]]], [])
        for i, t in enumerate(crossing_trials):
            crossings.append(list(map(lambda c: [c] + design_combinations[i] ,crossing_factors[i])))

        # Step 5: Remove crossings that are not possible.
        # From here on ignore all values other than the first in every list.
        crossings = block.filter_excluded_derived_levels(crossings)

        # Step 6: Allocate additional variables to represent each crossing.
        num_state_vars = list(map(lambda c: len(c), crossings))
        state_vars = list(range(fresh, fresh + sum(num_state_vars)))
        fresh += sum(num_state_vars)

        # Step 7: Associate each state variable with its crossing.
        flattened_crossings = list(chain.from_iterable(crossings))
        iffs = list(map(lambda n: Iff(state_vars[n], And([*flattened_crossings[n][0]])), range(len(state_vars))))

        # Step 8: Constrain each crossing to occur in only one trial.
        states = list(chunk(state_vars, block.crossing_size()))
        transposed = cast(List[List[int]], list(map(list, zip(*states))))

        # We Use n < 2 rather than n = 1 here because they may exclude some levels from the crossing.
        # This ensures that there won't be duplicates, while still allowing some to be missing.
        # backend_request.ll_requests += list(map(lambda l: LowLevelRequest("LT", 2, l), transposed))
        backend_request.ll_requests += list(map(lambda l: LowLevelRequest("GT", 0, l), transposed))

        (cnf, new_fresh) = block.cnf_fn(And(iffs), fresh)

        backend_request.cnfs.append(cnf)
        backend_request.fresh = new_fresh
Esempio n. 9
0
def test_fully_cross_block_should_copy_input_lists():
    # FullyCrossBlock should copy the input lists, so as not to break if the
    # user modifies the original list.
    design = [color, text, con_factor]
    crossing = [color, text]
    constraints = [Exclude(con_factor, get_level_from_name(con_factor, "con"))]

    block = FullyCrossBlock(design, [crossing], constraints)

    design.clear()
    assert len(block.design) == 3

    crossing.clear()
    assert len(block.crossing[0]) == 2

    constraints.clear()
    assert len(block.constraints) == 1
Esempio n. 10
0
def test_fully_cross_block_trials_per_sample():
    text_single = Factor("text", ["red"])

    assert FullyCrossBlock([], [color, color], []).trials_per_sample() == 4
    assert FullyCrossBlock([], [color, color, color],
                           []).trials_per_sample() == 8
    assert FullyCrossBlock([], [size, text_single],
                           []).trials_per_sample() == 3
    assert FullyCrossBlock([], [size, color], []).trials_per_sample() == 6
    assert FullyCrossBlock([], [text_single], []).trials_per_sample() == 1

    assert FullyCrossBlock([color, text, color_repeats_factor], [color, text],
                           []).trials_per_sample() == 4
Esempio n. 11
0
def test_fully_cross_block_crossing_size_with_exclude():
    # No congruent excludes 2 trials, 4 - 2 = 2
    assert FullyCrossBlock(
        [color, text, con_factor], [[color, text]],
        [Exclude(con_factor, con_level)],
        require_complete_crossing=False).crossing_size() == 2