Created
December 5, 2025 04:15
-
-
Save odashi/077234b7adb8fc30340fd59397074e13 to your computer and use it in GitHub Desktop.
Config generator for fractional factorial design
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2025 Yusuke Oda. | |
| # License: MIT | |
| # | |
| # Usage: | |
| # | |
| # $ python generate_fracfact_config.py --num-total-factors 8 --resolution 4 --print-binary | |
| # INFO:root:Determined number of base factors: 4 | |
| # 00000000 | |
| # 10001110 | |
| # 01001101 | |
| # 11000011 | |
| # 00101011 | |
| # 10100101 | |
| # 01100110 | |
| # 11101000 | |
| # 00010111 | |
| # 10011001 | |
| # 01011010 | |
| # 11010100 | |
| # 00111100 | |
| # 10110010 | |
| # 01110001 | |
| # 11111111 | |
| import argparse | |
| import heapq | |
| import logging | |
| def parse_args() -> argparse.Namespace: | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--num-total-factors", type=int, required=True, help="Number of total factors") | |
| p.add_argument("--resolution", type=int, required=True, help="Resolution of experiment") | |
| p.add_argument("--experiment-id", type=int, default=None, help="Experiment ID of fractal factorization") | |
| p.add_argument("--num-base-factors", type=int, default=None, help="Number of independent factors. If not set, it will be automatically determined.") | |
| p.add_argument("--print-binary", action="store_true", help="Print factors in binary format") | |
| args = p.parse_args() | |
| if args.num_base_factors is not None and args.num_base_factors < 1: | |
| raise ValueError("Number of base factors must be at least 1.") | |
| if args.resolution < 3: | |
| raise ValueError("Resolution must be at least 3.") | |
| if args.num_base_factors is not None and args.num_total_factors >= 2 ** args.num_base_factors: | |
| raise ValueError(f"Too many total factors for the given number of base factors: {args.num_total_factors} >= 2^{args.num_base_factors}") | |
| if ( | |
| args.experiment_id is not None | |
| and args.num_base_factors is not None | |
| and args.experiment_id >= 2 ** args.num_base_factors | |
| ): | |
| raise ValueError(f"Experiment ID out of range for the given number of base factors: {args.experiment_id} >= 2^{args.num_base_factors}") | |
| return args | |
| def generate_factors(num_base_factors: int, num_total_factors: int, resolution: int): | |
| factors = [] | |
| distances = [99999999] * (2 ** num_base_factors) | |
| distances[0] = -1 # No factor experiment: not used | |
| max_distance = resolution - 2 | |
| for f in range(num_base_factors): | |
| factor = 1 << f | |
| factors.append(factor) | |
| distances[factor] = 0 | |
| logging.debug(f"New factor: {factor:0{num_base_factors}b}") | |
| if len(factors) >= num_total_factors: | |
| return factors | |
| queue = [(0, a) for a in factors] | |
| while queue: | |
| cur_dist, cur_factor = heapq.heappop(queue) | |
| if cur_dist > distances[cur_factor]: | |
| continue | |
| if cur_dist >= max_distance: | |
| factors.append(cur_factor) | |
| cur_dist = distances[cur_factor] = 0 | |
| logging.debug(f"New factor: {cur_factor:0{num_base_factors}b}") | |
| if len(factors) >= num_total_factors: | |
| return factors | |
| for factor in factors: | |
| new_factor = cur_factor ^ factor | |
| new_dist = cur_dist + 1 | |
| if new_dist < distances[new_factor]: | |
| distances[new_factor] = new_dist | |
| heapq.heappush(queue, (new_dist, new_factor)) | |
| raise ValueError("Could not generate enough factors within the given resolution.") | |
| def main(): | |
| args = parse_args() | |
| if args.num_base_factors is not None: | |
| num_base_factors = args.num_base_factors | |
| factors = generate_factors(num_base_factors, args.num_total_factors, args.resolution) | |
| else: | |
| # Automatically determine the number of base factors | |
| for num_base_factors in range(1, 21): | |
| try: | |
| factors = generate_factors(num_base_factors, args.num_total_factors, args.resolution) | |
| logging.info(f"Determined number of base factors: {num_base_factors}") | |
| break | |
| except ValueError: | |
| logging.debug(f"Failed to generate factors with {num_base_factors} base factors.") | |
| for i, f in enumerate(factors): | |
| logging.debug(f"Factor {i}: {f:0{num_base_factors}b}") | |
| if args.experiment_id is not None: | |
| exp_conf = [(f & args.experiment_id).bit_count() % 2 for f in factors] | |
| if args.print_binary: | |
| print("".join(str(x) for x in exp_conf)) | |
| else: | |
| print(" ".join(str(i) for i, x in enumerate(exp_conf) if x == 1)) | |
| else: | |
| for exp_id in range(2 ** num_base_factors): | |
| exp_conf = [(f & exp_id).bit_count() % 2 for f in factors] | |
| if args.print_binary: | |
| print("".join(str(x) for x in exp_conf)) | |
| else: | |
| print(" ".join(str(i) for i, x in enumerate(exp_conf) if x == 1)) | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.INFO) | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment