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
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
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
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
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
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], [])
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
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
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
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
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