class UCSolutionEnumerator(): def __init__(self, block: FullyCrossBlock) -> None: self._block = block self._partitions = DesignPartitions(block) self._crossing_instances = self.__generate_crossing_instances() self._source_combinations = self.__generate_source_combinations() self._segment_lengths = cast( List[int], []) # Will be populated by solution counting # Maintains a lookup for valid source combinations for a permutation. # Example [[2, 3], ...] means that for permutation 0, indices 2 and 3 in the source combinations # list are allowed. self._valid_source_combinations_indices = cast( List[List[int]], []) # Will be populated by solution counting # Needs to be called last. self._solution_count = self.__count_solutions() def solution_count(self): return self._solution_count def generate_random_sample(self, sample_array: List[int]) -> Tuple[int, dict]: # Select a random number from the range of solutions. sequence_number = random.randrange(0, self._solution_count) while sequence_number in sample_array: sequence_number = random.randrange(0, self._solution_count) return (sequence_number, self.generate_sample(sequence_number)) 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 generate_solution_variables(self) -> List[int]: sequence_number = random.randrange(0, self._solution_count) trial_values = self.generate_trail_values(sequence_number) solution = cast(List[int], []) # Convert to variable encoding for SAT checking for trial_number, trial_value in enumerate(trial_values): for factor, level in trial_value.items(): solution.append( self._block.get_variable(trial_number + 1, (factor, level))) solution.sort() return solution 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 """ Generates all the crossings, indexed by factor name for easy lookup later. [ {'factor': 'level', 'factor': 'level', ...}, ... ] """ def __generate_crossing_instances(self) -> List[dict]: crossing = self._partitions.get_crossed_factors() level_lists = [list(f.levels) for f in crossing] return [{crossing[i]: level for i, level in enumerate(levels)} for levels in product(*level_lists)] def __generate_source_combinations(self) -> List[dict]: ubs = self._partitions.get_uncrossed_basic_source_factors() level_lists = [list(f.levels) for f in ubs] return [{ubs[i]: level for i, level in enumerate(levels)} for levels in product(*level_lists)] 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 test_get_crossed_factors(): partitions = DesignPartitions(block) assert partitions.get_crossed_factors() == crossing