def __count_exclusions(self, num): from sweetpea.constraints import Exclude excluded_crossings = set() excluded_external_names = set() # Get the exclude constraints. exclusions = list( filter(lambda c: isinstance(c, Exclude), self.constraints)) if not exclusions: return 0 # If there are any, generate the full crossing as a list of tuples. levels_lists = [list(f.levels) for f in self.crossing[0]] all_crossings = list(product(*levels_lists)) for constraint in exclusions: if constraint.factor.has_complex_window(): # If the excluded factor has a complex window, then we don't need # to reduce the sequence length. What if the transition being excluded # is in the crossing? If it is, then they shouldn't be excluding it. # We should give an error if we detect that. continue # Retrieve the derivation function that defines this exclusion. excluded_level = constraint.level if type(excluded_level) is SimpleLevel: for c in all_crossings: if excluded_level in c: excluded_crossings.add( get_internal_level_name(c[0]) + ", " + get_internal_level_name(c[1])) else: # For each crossing, ensure that atleast one combination is possible with the disgn-only factors keeping in mind the exclude contraints. for c in all_crossings: if all( list( map( lambda d: self.__excluded_derived( excluded_level, c + d), list( product(*[ list(f.levels) for f in filter( lambda f: f not in self. crossing[0], self.design) ]))))): excluded_crossings.add( get_internal_level_name(c[0]) + ", " + get_internal_level_name(c[1])) excluded_external_names.add( get_external_level_name(c[0]) + ", " + get_external_level_name(c[1])) if self.require_complete_crossing and len(excluded_crossings) != 0: er = "Complete crossing is not possible beacuse the following combinations have been excluded:" for names in excluded_external_names: er += "\n" + names self.errors.add(er) return len(excluded_crossings)
def generate_derivations(block: Block) -> List[Derivation]: derived_factors = list(filter(lambda f: f.is_derived(), block.design)) accum = [] for fact in derived_factors: according_level: Dict[Tuple[Any, ...], DerivedLevel] = {} # according_level = {} for level in fact.levels: level_index = block.first_variable_for_level(fact, level) x_product = level.get_dependent_cross_product() # filter to valid tuples, and get their idxs valid_tuples = [] for tup in x_product: args = DerivationProcessor.generate_argument_list( level, tup) fn_result = level.window.fn(*args) # Make sure the fn returned a boolean if not isinstance(fn_result, bool): raise ValueError( 'Derivation function did not return a boolean! factor={} level={} fn={} return={} args={} ' .format(fact.factor_name, get_external_level_name(level), level.window.fn, fn_result, args)) # If the result was true, add the tuple to the list if fn_result: valid_tuples.append(tup) if tup in according_level.keys(): raise ValueError( 'Factor={} matches both level={} and level={} with assignment={}' .format(fact.factor_name, according_level[tup], get_external_level_name(level), args)) else: according_level[tup] = get_external_level_name( level) if not valid_tuples: print( 'WARNING: There is no assignment that matches factor={} level={}' .format(fact.factor_name, get_external_level_name(level))) valid_idxs = [[ block.first_variable_for_level(pair[0], pair[1]) for pair in tup_list ] for tup_list in valid_tuples] shifted_idxs = DerivationProcessor.shift_window( valid_idxs, level.window, block.variables_per_trial()) accum.append(Derivation(level_index, shifted_idxs, fact)) return accum
def print_experiments(block: Block, experiments: List[dict]): """Displays the generated experiments in a human-friendly form. :param block: An experimental description as a :class:`.Block`. :param experiments: A list of experiments as :class:`dicts <dict>`. These are produced by calls to any of the synthesis functions (:func:`.synthesize_trials`, :func:`.synthesize_trials_non_uniform`, or :func:`.synthesize_trials_uniform`). """ nested_assignment_strs = [ list( map(lambda l: f.factor_name + " " + get_external_level_name(l), f.levels)) for f in block.design ] column_widths = list( map(lambda l: max(list(map(len, l))), nested_assignment_strs)) format_str = reduce(lambda a, b: a + '{{:<{}}} | '.format(b), column_widths, '')[:-3] + '\n' print('{} trial sequences found.'.format(len(experiments))) for idx, e in enumerate(experiments): print('Experiment {}:'.format(idx)) strs = [ list(map(lambda v: name + " " + v, values)) for (name, values) in e.items() ] transposed = list(map(list, zip(*strs))) print(reduce(lambda a, b: a + format_str.format(*b), transposed, ''))
def __count_solutions(self): self._segment_lengths = [] ############################################################## # Permutations of crossing instances n = self._block.crossing_size() n_factorial = factorial(n) self._segment_lengths.append(n_factorial) ############################################################## # Uncrossed Dependent Factors level_combinations = self.__generate_source_combinations() # Keep only allowed combos for each permutation for ci in self._crossing_instances: sc_indices = list(range(len(self._source_combinations))) for sc_idx, sc in enumerate(self._source_combinations): # Apply the derivation fn for each DF in the crossing for this instance and make sure it returns # true for this level combination. If it doesn't, then remove this combination. merged_levels = {**ci, **sc} for df in self._partitions.get_crossed_factors_derived(): w = merged_levels[df].window if not w.fn(*[get_external_level_name(merged_levels[f]) for f in w.args]): sc_indices.remove(sc_idx) self._segment_lengths.append(len(sc_indices)) self._valid_source_combinations_indices.append(sc_indices) ############################################################## # Uncrossed Independent Factors u_b_i_counts = [] u_b_i = self._partitions.get_uncrossed_basic_independent_factors() for f in u_b_i: self._segment_lengths.append(pow(len(f.levels), n)) return reduce(op.mul, self._segment_lengths, 1)
def extract_simplelevel(self, block: Block, level: DerivedLevel) -> List[Dict[Factor, SimpleLevel]]: excluded_levels = [] excluded = list(filter(lambda c: level.window.fn(*list(map(lambda f: get_external_level_name(f[1]), c))), level.get_dependent_cross_product())) for i in excluded: combos = cast(List[Dict[Factor, SimpleLevel]], [dict()]) for j in i: if type(j[1]) is DerivedLevel: result = self.extract_simplelevel(block, j[1]) newcombos = [] valid = True for r in result: for c in combos: for f in c: if f in r: if c[f] != r[f]: valid = False if valid: newcombos.append({**r, **c}) combos = newcombos else: for c in combos: if block.factor_in_crossing(j[0]) and block.require_complete_crossing: block.errors.add("WARNING: Some combinations have been excluded, this crossing may not be complete!") c[j[0]] = j[1] excluded_levels.extend(combos) return excluded_levels
def generate_argument_list(level: DerivedLevel, tup: Tuple) -> List: # User-supplied string level names are the arguments for the user-supplied derivation functions level_strings = list(map(lambda t: get_external_level_name(t[1]), tup)) # For windows with a width of 1, we just pass the arguments directly, rather than putting them in lists. if level.window.width == 1: return level_strings else: return list(chunk_list(level_strings, level.window.width))
def generate_sample(self, sequence_number: int) -> dict: trial_values = self.generate_trail_values(sequence_number) experiment = cast(dict, {}) for trial_number, trial_value in enumerate(trial_values): for factor, level in trial_value.items(): if factor.factor_name not in experiment: experiment[factor.factor_name] = [] experiment[factor.factor_name].append(get_external_level_name(level)) return experiment
def __assert_atmostkinarow_factor(k: int, f: factor, experiments: List[dict]) -> None: factor_name = f.factor_name for level in f.levels: level_name = get_external_level_name(level) sublist = list(repeat(level_name, k + 1)) for e in experiments: assert sublist not in [ e[factor_name][i:i + k + 1] for i in range(len(e[factor_name]) - (k + 1)) ]
def get_all_external_level_names( design: List[Factor]) -> List[Tuple[str, str]]: """Usage :: >>> color = Factor("color", ["red", "blue"]) >>> text = Factor("text", ["red", "blue"]) >>> get_all_internal_level_names([color, text]) [('color', 'red'), ('color', 'blue'), ('text', 'red'), ('text', 'blue')] """ return [(factor.factor_name, get_external_level_name(level)) for factor in design for level in factor.levels]
def __excluded_derived(self, excluded_level, c): """Given the complete crossing and an exclude constraint returns true if that combination results in the exclude level. """ ret = [] for f in filter(lambda f: f.is_derived(), excluded_level.window.args): ret.append(self.__excluded_derived(list(filter(lambda l: l.factor == f, c))[0], c)) # Invoking the fn this way is only ok because we only do this for WithinTrial windows. # With complex windows, it wouldn't work due to the list aspect for each argument. ret.append(excluded_level.window.fn(*list(map(lambda l: get_external_level_name(l), filter(lambda l: l.factor in excluded_level.window.args, c))))) return all(ret)
def generate_trail_values(self, sequence_number: int) -> List[dict]: # 1. Extract the component pieces (permutation, each combination setting, etc) # The 0th component is always the permutation index. # The 1st-nth components are always the source combination indices for each trial in the sequence # Any following components are the combination indices for independent basic factors. components = extract_components(self._segment_lengths, sequence_number) # 2. Generate the inversion sequence for the selected permutation number. # Use the inversion sequence to construct the permutation. l = self._block.crossing_size() inversion_sequence = compute_jth_inversion_sequence(l, components[0]) permutation_indices = construct_permutation(inversion_sequence) permutation = list(map(lambda i: self._crossing_instances[i], permutation_indices)) # 3. Generate the source combinations for the selected sequence. source_combinations = cast(List[dict], []) for i, p in enumerate(permutation_indices): component_for_p = components[p + 1] source_combination_index_for_component = self._valid_source_combinations_indices[p][component_for_p] source_combinations.append(self._source_combinations[source_combination_index_for_component]) # 4. Generate the combinations for independent basic factors independent_factor_combinations = cast(List[dict], [{}] *l) u_b_i = self._partitions.get_uncrossed_basic_independent_factors() if u_b_i: independent_combination_idx = components[l+1] for f in u_b_i: combo = compute_jth_combination(l, len(f.levels), independent_combination_idx) for i in range(l): if not independent_factor_combinations[i]: independent_factor_combinations[i] = {f : f.levels[combo[i]]} continue independent_factor_combinations[i][f] = f.levels[combo[i]] # 5. Merge the selected levels gathered so far to facilitate computing the uncrossed derived factor levels. trial_values = cast(List[dict], [{}] * l) for t in range(l): trial_values[t] = {**permutation[t], **source_combinations[t], **independent_factor_combinations[t]} # 6. Generate uncrossed derived level values u_d = self._partitions.get_uncrossed_derived_factors() for f in u_d: for t in range(l): # For each level in the factor, see if the derivation function is true. for level in f.levels: if level.window.fn(*[get_external_level_name(trial_values[t][f]) for f in level.window.args]): trial_values[t][f] = level return trial_values
def test_generate_crossing_instances(): enumerator = UCSolutionEnumerator(block) crossing_instances = enumerator._UCSolutionEnumerator__generate_crossing_instances() simplified_names = [] for d in crossing_instances: d_simple = {} for (f, l) in d.items(): d_simple[f.factor_name] = get_external_level_name(l) simplified_names.append(d_simple) assert simplified_names == [ {'color': 'red', 'text': 'red'}, {'color': 'red', 'text': 'blue'}, {'color': 'blue', 'text': 'red'}, {'color': 'blue', 'text': 'blue'} ]
def test_generate_source_combinations(): block = fully_cross_block(design, [congruency], []) enumerator = UCSolutionEnumerator(block) crossing_source_combos = enumerator._UCSolutionEnumerator__generate_source_combinations() simplified_names = [] for d in crossing_source_combos: d_simple = {} for (f, l) in d.items(): d_simple[f.factor_name] = get_external_level_name(l) simplified_names.append(d_simple) assert simplified_names == [ {'color': 'red', 'text': 'red'}, {'color': 'red', 'text': 'blue'}, {'color': 'blue', 'text': 'red'}, {'color': 'blue', 'text': 'blue'} ]
def tabulate_experiments(experiments: List[Dict], factors: Optional[List[Factor]] = None, trials: Optional[List[int]] = None): """Tabulates and prints the given experiments in a human-friendly form. Outputs a table that shows the absolute and relative frequencies of combinations of factor levels. :param experiments: A list of experiments as :class:`dicts <dict>`. These are produced by calls to any of the synthesis functions (:func:`.synthesize_trials`, :func:`.synthesize_trials_non_uniform`, or :func:`.synthesize_trials_uniform`). :param factors: An optional :class:`list` of :class:`Factors <.Factor>`... .. todo:: Finish specification of this parameter. :param trials: An optional :class:`list` of :class:`ints <int>`... .. todo:: Finish specification of this parameter. """ if factors is None: factors = [] for exp_idx, e in enumerate(experiments): tabulation: Dict[str, List[str]] = dict() frequency_list = list() proportion_list = list() levels: List[List[str]] = list() if trials is None: trials = list(range(0, len(e[list(e.keys())[0]]))) num_trials = len(trials) # initialize table for f in factors: tabulation[f.factor_name] = list() factor_levels: List[str] = list() for l in f.levels: factor_levels.append(l.external_name) levels.append(factor_levels) max_combinations = 0 # Each `element` is an n-tuple (s1, s2, ..., sn) where n is the number # of levels and each element is a level name. for element in itertools.product(*levels): max_combinations += 1 # add factor combination for idx, factor_name in enumerate(tabulation.keys()): tabulation[factor_name].append(element[idx]) # compute frequency frequency = 0 for trial in trials: valid_condition = True for idx, factor in enumerate(tabulation.keys()): if e[factor][trial] != element[idx]: valid_condition = False break if valid_condition: frequency += 1 proportion = frequency / num_trials frequency_list.append(str(frequency)) proportion_list.append(str(proportion * 100) + '%') tabulation["frequency"] = frequency_list tabulation["proportion"] = proportion_list frequency_factor = Factor("frequency", list(set(frequency_list))) proportion_factor = Factor("proportion", list(set(proportion_list))) design = list() for f in factors: design.append(f) design.append(frequency_factor) design.append(proportion_factor) # print tabulation nested_assignment_strs = [ list( map(lambda l: f.factor_name + " " + get_external_level_name(l), f.levels)) for f in design ] column_widths = list( map(lambda l: max(list(map(len, l))), nested_assignment_strs)) format_str = reduce(lambda a, b: a + '{{:<{}}} | '.format(b), column_widths, '')[:-3] + '\n' print('Experiment {}:'.format(exp_idx)) strs = [ list(map(lambda v: name + " " + v, values)) for (name, values) in tabulation.items() ] transposed = list(map(list, zip(*strs))) print(reduce(lambda a, b: a + format_str.format(*b), transposed, ''))
def get_all_external_level_names( design: List[Factor]) -> List[Tuple[str, str]]: return [(factor.factor_name, get_external_level_name(level)) for factor in design for level in factor.levels]