diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bae7dcb6..4f60c011 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,21 +6,14 @@ jobs: main: strategy: matrix: - # Python 3.6 is not supported on Ubuntu 22.04 - os: ['ubuntu-22.04', 'ubuntu-20.04'] - python-version: ['3.6', '3.9', '3.12'] + # os: ['ubuntu-22.04'] + python-version: ['3.9', '3.12'] sample: [mapper, minsearch, exchange, exchange_mesh, pamc, bayes, transform] - exclude: - - os: 'ubuntu-22.04' - python-version: '3.6' - - os: 'ubuntu-20.04' - python-version: '3.9' - - os: 'ubuntu-20.04' - python-version: '3.12' fail-fast: false name: ${{ matrix.sample }} with Python ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} + runs-on: 'ubuntu-22.04' + # runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: diff --git a/README.md b/README.md index 70452287..55cce540 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ It also offers a driver script to solve the problem with predefined optimization ### Prerequists - Required - - python >= 3.6.8 + - python >= 3.9 - numpy >= 1.14 - tomli >= 1.2.0 - Optional diff --git a/doc/en/source/start.rst b/doc/en/source/start.rst index c45af938..bf7889a0 100644 --- a/doc/en/source/start.rst +++ b/doc/en/source/start.rst @@ -3,7 +3,7 @@ Installation of ODAT-SE Prerequisites ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- Python3 (>=3.6.8) +- Python3 (>=3.9) - The following Python packages are required. - tomli >= 1.2 @@ -13,7 +13,7 @@ Prerequisites - mpi4py (required for grid search) - scipy (required for Nelder-Mead method) - - physbo (>=0.3, required for Baysian optimization) + - physbo (>=2.0, required for Baysian optimization) How to download and install diff --git a/doc/ja/source/start.rst b/doc/ja/source/start.rst index a34859c0..867aa025 100644 --- a/doc/ja/source/start.rst +++ b/doc/ja/source/start.rst @@ -3,7 +3,7 @@ ODAT-SE のインストール 実行環境・必要なパッケージ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- python 3.6.8 以上 +- python 3.9 以上 - 必要なpythonパッケージ @@ -14,7 +14,7 @@ ODAT-SE のインストール - mpi4py (グリッド探索利用時) - scipy (Nelder-Mead法利用時) - - physbo (ベイズ最適化利用時, ver. 0.3以上) + - physbo (ベイズ最適化利用時, ver. 2.0以上) ダウンロード・インストール ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 2b838256..4d311299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.6.8" +python = ">=3.9" numpy = "^1.14" tomli = ">=1.2" scipy = {version = "^1", optional = true} diff --git a/src/odatse/_info.py b/src/odatse/_info.py index 5b337d00..4ba431e8 100644 --- a/src/odatse/_info.py +++ b/src/odatse/_info.py @@ -16,18 +16,43 @@ class Info: + """ + A class to represent the information structure for the data-analysis software. + """ + base: dict algorithm: dict solver: dict runner: dict def __init__(self, d: Optional[MutableMapping] = None): + """ + Initialize the Info object. + + Parameters + ---------- + d : MutableMapping (optional) + A dictionary to initialize the Info object. + """ if d is not None: self.from_dict(d) else: self._cleanup() def from_dict(self, d: MutableMapping) -> None: + """ + Initialize the Info object from a dictionary. + + Parameters + ---------- + d : MutableMapping + A dictionary containing the information to initialize the Info object. + + Raises + ------ + exception.InputError + If any required section is missing in the input dictionary. + """ for section in ["base", "algorithm", "solver"]: if section not in d: raise exception.InputError( @@ -48,6 +73,9 @@ def from_dict(self, d: MutableMapping) -> None: ) def _cleanup(self) -> None: + """ + Reset the Info object to its default state. + """ self.base = {} self.base["root_dir"] = Path(".").absolute() self.base["output_dir"] = self.base["root_dir"] @@ -57,6 +85,28 @@ def _cleanup(self) -> None: @classmethod def from_file(cls, file_name, fmt="", **kwargs): + """ + Create an Info object from a file. + + Parameters + ---------- + file_name : str + The name of the file to load the information from. + fmt : str + The format of the file (default is ""). + **kwargs + Additional keyword arguments. + + Returns + ------- + Info + An Info object initialized with the data from the file. + + Raises + ------ + ValueError + If the file format is unsupported. + """ if fmt == "toml" or fnmatch(file_name.lower(), "*.toml"): inp = {} if mpi.rank() == 0: diff --git a/src/odatse/_main.py b/src/odatse/_main.py index 93865f4b..f4efd9e6 100644 --- a/src/odatse/_main.py +++ b/src/odatse/_main.py @@ -14,6 +14,11 @@ def main(): + """ + Main function to run the data-analysis software for quantum beam diffraction experiments + on 2D material structures. It parses command-line arguments, loads the input file, + selects the appropriate algorithm and solver, and executes the analysis. + """ import argparse parser = argparse.ArgumentParser( diff --git a/src/odatse/_runner.py b/src/odatse/_runner.py index 23b2178f..1582e5ad 100644 --- a/src/odatse/_runner.py +++ b/src/odatse/_runner.py @@ -26,14 +26,16 @@ class Run(metaclass=ABCMeta): def __init__(self, nprocs=None, nthreads=None, comm=None): """ + Initialize the Run class. + Parameters ---------- nprocs : int - Number of process which one solver uses + Number of processes which one solver uses. nthreads : int - Number of threads which one solver process uses + Number of threads which one solver process uses. comm : MPI.Comm - MPI Communicator + MPI Communicator. """ self.nprocs = nprocs self.nthreads = nthreads @@ -41,6 +43,14 @@ def __init__(self, nprocs=None, nthreads=None, comm=None): @abstractmethod def submit(self, solver): + """ + Abstract method to submit a solver. + + Parameters + ---------- + solver : object + Solver object to be submitted. + """ pass @@ -54,10 +64,18 @@ def __init__(self, mapping = None, limitation = None) -> None: """ + Initialize the Runner class. Parameters ---------- - Solver: odatse.solver.SolverBase object + solver : odatse.solver.SolverBase + Solver object. + info : Optional[odatse.Info] + Information object. + mapping : object, optional + Mapping object. + limitation : object, optional + Limitation object. """ self.solver = solver self.solver_name = solver.name @@ -72,7 +90,7 @@ def __init__(self, else: # trivial mapping self.mapping = odatse.util.mapping.TrivialMapping() - + if limitation is not None: self.limitation = limitation elif "limitation" in info.runner: @@ -82,11 +100,38 @@ def __init__(self, self.limitation = odatse.util.limitation.Unlimited() def prepare(self, proc_dir: Path): + """ + Prepare the logger with the given process directory. + + Parameters + ---------- + proc_dir : Path + Path to the process directory. + """ self.logger.prepare(proc_dir) def submit( self, x: np.ndarray, args = (), nprocs: int = 1, nthreads: int = 1 ) -> float: + """ + Submit the solver with the given parameters. + + Parameters + ---------- + x : np.ndarray + Input array. + args : tuple, optional + Additional arguments. + nprocs : int, optional + Number of processes. + nthreads : int, optional + Number of threads. + + Returns + ------- + float + Result of the solver evaluation. + """ if self.limitation.judge(x): xp = self.mapping(x) result = self.solver.evaluate(xp, args) @@ -96,4 +141,7 @@ def submit( return result def post(self) -> None: + """ + Write the logger data. + """ self.logger.write() diff --git a/src/odatse/algorithm/_algorithm.py b/src/odatse/algorithm/_algorithm.py index 407fe181..28129c1e 100644 --- a/src/odatse/algorithm/_algorithm.py +++ b/src/odatse/algorithm/_algorithm.py @@ -31,11 +31,14 @@ from mpi4py import MPI class AlgorithmStatus(IntEnum): + """Enumeration for the status of the algorithm.""" INIT = 1 PREPARE = 2 RUN = 3 class AlgorithmBase(metaclass=ABCMeta): + """Base class for algorithms, providing common functionality and structure.""" + mpicomm: Optional["MPI.Comm"] mpisize: int mpirank: int @@ -60,6 +63,18 @@ def __init__( runner: Optional[odatse.Runner] = None, run_mode: str = "initial" ) -> None: + """ + Initialize the algorithm with the given information and runner. + + Parameters + ---------- + info : Info + Information object containing algorithm and base parameters. + runner : Runner (optional) + Optional runner object to execute the algorithm. + run_mode : str + Mode in which the algorithm should run. + """ self.mpicomm = mpi.comm() self.mpisize = mpi.size() self.mpirank = mpi.rank() @@ -109,6 +124,14 @@ def __init__( self.set_runner(runner) def __init_rng(self, info: odatse.Info) -> None: + """ + Initialize the random number generator. + + Parameters + ---------- + info : Info + Information object containing algorithm parameters. + """ seed = info.algorithm.get("seed", None) seed_delta = info.algorithm.get("seed_delta", 314159) @@ -118,9 +141,20 @@ def __init_rng(self, info: odatse.Info) -> None: self.rng = np.random.RandomState(seed + self.mpirank * seed_delta) def set_runner(self, runner: odatse.Runner) -> None: + """ + Set the runner for the algorithm. + + Parameters + ---------- + runner : Runner + Runner object to execute the algorithm. + """ self.runner = runner def prepare(self) -> None: + """ + Prepare the algorithm for execution. + """ if self.runner is None: msg = "Runner is not assigned" raise RuntimeError(msg) @@ -129,9 +163,13 @@ def prepare(self) -> None: @abstractmethod def _prepare(self) -> None: + """Abstract method to be implemented by subclasses for preparation steps.""" pass def run(self) -> None: + """ + Run the algorithm. + """ if self.status < AlgorithmStatus.PREPARE: msg = "algorithm has not prepared yet" raise RuntimeError(msg) @@ -145,9 +183,18 @@ def run(self) -> None: @abstractmethod def _run(self) -> None: + """Abstract method to be implemented by subclasses for running steps.""" pass def post(self) -> Dict: + """ + Perform post-processing after the algorithm has run. + + Returns + ------- + Dict + Dictionary containing post-processing results. + """ if self.status < AlgorithmStatus.RUN: msg = "algorithm has not run yet" raise RuntimeError(msg) @@ -159,9 +206,13 @@ def post(self) -> Dict: @abstractmethod def _post(self) -> Dict: + """Abstract method to be implemented by subclasses for post-processing steps.""" pass def main(self): + """ + Main method to execute the algorithm. + """ time_sta = time.perf_counter() self.prepare() time_end = time.perf_counter() @@ -187,6 +238,14 @@ def main(self): return result def write_timer(self, filename: Path): + """ + Write the timing information to a file. + + Parameters + ---------- + filename : Path + Path to the file where timing information will be written. + """ with open(filename, "w") as fw: fw.write("#in units of seconds\n") @@ -204,6 +263,18 @@ def output_file(type): output_file("post") def _save_data(self, data, filename="state.pickle", ngen=3) -> None: + """ + Save data to a file with versioning. + + Parameters + ---------- + data + Data to be saved. + filename + Name of the file to save the data. + ngen : int, default: 3 + Number of generations for versioning. + """ try: fn = Path(filename + ".tmp") with open(fn, "wb") as f: @@ -225,6 +296,19 @@ def _save_data(self, data, filename="state.pickle", ngen=3) -> None: print("save_state: write to {}".format(filename)) def _load_data(self, filename="state.pickle") -> Dict: + """ + Load data from a file. + + Parameters + ---------- + filename + Name of the file to load the data from. + + Returns + ------- + Dict + Dictionary containing the loaded data. + """ if Path(filename).exists(): try: fn = Path(filename) @@ -240,12 +324,23 @@ def _load_data(self, filename="state.pickle") -> Dict: return data def _show_parameters(self): + """ + Show the parameters of the algorithm. + """ if self.mpirank == 0: info = flatten_dict(self.info) for k, v in info.items(): print("{:16s}: {}".format(k, v)) def _check_parameters(self, param=None): + """ + Check the parameters of the algorithm against previous parameters. + + Parameters + ---------- + param (optional) + Previous parameters to check against. + """ info = flatten_dict(self.info) info_prev = flatten_dict(param) @@ -259,6 +354,23 @@ def _check_parameters(self, param=None): # utility def flatten_dict(d, parent_key="", separator="."): + """ + Flatten a nested dictionary. + + Parameters + ---------- + d + Dictionary to flatten. + parent_key : str, default : "" + Key for the parent dictionary. + separator : str, default : "." + Separator to use between keys. + + Returns + ------- + dict + Flattened dictionary. + """ items = [] if d: for key_, val in d.items(): diff --git a/src/odatse/algorithm/bayes.py b/src/odatse/algorithm/bayes.py index e7aec9c7..fa7e2305 100644 --- a/src/odatse/algorithm/bayes.py +++ b/src/odatse/algorithm/bayes.py @@ -19,30 +19,52 @@ import odatse.domain class Algorithm(odatse.algorithm.AlgorithmBase): - - # inputs - mesh_list: np.ndarray - label_list: List[str] - - # hyperparameters of Bayesian optimization - random_max_num_probes: int - bayes_max_num_probes: int - score: str - interval: int - num_rand_basis: int - - # results - xopt: np.ndarray - best_fx: List[float] - best_action: List[int] - fx_list: List[float] - param_list: List[np.ndarray] - - def __init__(self, info: odatse.Info, - runner: odatse.Runner = None, - domain = None, - run_mode: str = "initial" - ) -> None: + """ + A class to represent the Bayesian optimization algorithm. + + Attributes + ---------- + mesh_list : np.ndarray + The mesh grid list. + label_list : List[str] + The list of labels. + random_max_num_probes : int + The maximum number of random probes. + bayes_max_num_probes : int + The maximum number of Bayesian probes. + score : str + The scoring method. + interval : int + The interval for Bayesian optimization. + num_rand_basis : int + The number of random basis. + xopt : np.ndarray + The optimal solution. + best_fx : List[float] + The list of best function values. + best_action : List[int] + The list of best actions. + fx_list : List[float] + The list of function values. + param_list : List[np.ndarray] + The list of parameters. + """ + + def __init__(self, info: odatse.Info, runner: odatse.Runner = None, domain = None, run_mode: str = "initial") -> None: + """ + Constructs all the necessary attributes for the Algorithm object. + + Parameters + ---------- + info : odatse.Info + The information object. + runner : odatse.Runner, optional + The runner object (default is None). + domain : optional + The domain object (default is None). + run_mode : str, optional + The run mode (default is "initial"). + """ super().__init__(info=info, runner=runner, run_mode=run_mode) info_param = info.algorithm.get("param", {}) @@ -67,7 +89,6 @@ def __init__(self, info: odatse.Info, print(f"interval = {self.interval}") print(f"num_rand_basis = {self.num_rand_basis}") - #self.mesh_list, actions = self._meshgrid(info, split=False) if domain and isinstance(domain, odatse.domain.MeshGrid): self.domain = domain else: @@ -82,12 +103,14 @@ def __init__(self, info: odatse.Info, seed = info.algorithm["seed"] self.policy.set_seed(seed) - # store state self.file_history = "history.npz" self.file_training = "training.npz" self.file_predictor = "predictor.dump" def _initialize(self): + """ + Initializes the algorithm parameters and timers. + """ self.istep = 0 self.param_list = [] self.fx_list = [] @@ -97,13 +120,27 @@ def _initialize(self): self._show_parameters() def _run(self) -> None: + """ + Runs the Bayesian optimization process. + """ runner = self.runner mesh_list = self.mesh_list - # fx_list = [] - # param_list = [] class simulator: def __call__(self, action: np.ndarray) -> float: + """ + Simulates the function evaluation for a given action. + + Parameters + ---------- + action : np.ndarray + The action to be evaluated. + + Returns + ------- + float + The negative function value. + """ a = int(action[0]) args = (a, 0) x = mesh_list[a, 1:] @@ -137,9 +174,7 @@ def __call__(self, action: np.ndarray) -> float: time_end = time.perf_counter() self.timer["run"]["random_search"] = time_end - time_sta - # store initial state if self.checkpoint: - # print(">>> store initial state") self._save_state(self.checkpoint_file) else: if self.istep >= self.bayes_max_num_probes: @@ -149,7 +184,6 @@ def __call__(self, action: np.ndarray) -> float: next_checkpoint_time = time.time() + self.checkpoint_interval while self.istep < self.bayes_max_num_probes: - # print(">>> step {}".format(self.istep+1)) intv = 0 if self.istep % self.interval == 0 else -1 time_sta = time.perf_counter() @@ -168,8 +202,6 @@ def __call__(self, action: np.ndarray) -> float: if self.checkpoint: time_now = time.time() if self.istep >= next_checkpoint_step or time_now >= next_checkpoint_time: - # print(">>> checkpointing") - self.fx_list = fx_list self.param_list = param_list @@ -182,18 +214,19 @@ def __call__(self, action: np.ndarray) -> float: self.fx_list = fx_list self.param_list = param_list - # physbo.search.utility.show_search_results(self.policy.history, 20) - - # store final state for continuation if self.checkpoint: - # print(">>> store final state") self._save_state(self.checkpoint_file) def _prepare(self) -> None: - # do nothing + """ + Prepares the algorithm for execution. + """ pass def _post(self) -> None: + """ + Finalizes the algorithm execution and writes the results to a file. + """ label_list = self.label_list if self.mpirank == 0: with open("BayesData.txt", "w") as file_BD: @@ -221,14 +254,20 @@ def _post(self) -> None: return {"x": self.xopt, "fx": self.best_fx} def _save_state(self, filename): + """ + Saves the current state of the algorithm to a file. + + Parameters + ---------- + filename : str + The name of the file to save the state. + """ data = { - #-- _algorithm "mpisize": self.mpisize, "mpirank": self.mpirank, "rng": self.rng.get_state(), "timer": self.timer, "info": self.info, - #-- bayes "istep": self.istep, "param_list": self.param_list, "fx_list": self.fx_list, @@ -239,19 +278,28 @@ def _save_state(self, filename): } self._save_data(data, filename) - #-- bayes self.policy.save(file_history=Path(self.output_dir, self.file_history), file_training=Path(self.output_dir, self.file_training), file_predictor=Path(self.output_dir, self.file_predictor)) - def _load_state(self, filename, mode="resume", restore_rng=True): + """ + Loads the state of the algorithm from a file. + + Parameters + ---------- + filename : str + The name of the file to load the state from. + mode : str, optional + The mode to load the state (default is "resume"). + restore_rng : bool, optional + Whether to restore the random number generator state (default is True). + """ data = self._load_data(filename) if not data: print("ERROR: Load status file failed") sys.exit(1) - #-- _algorithm assert self.mpisize == data["mpisize"] assert self.mpirank == data["mpirank"] @@ -264,11 +312,10 @@ def _load_state(self, filename, mode="resume", restore_rng=True): info = data["info"] self._check_parameters(info) - #-- bayes self.istep = data["istep"] self.param_list = data["param_list"] self.fx_list = data["fx_list"] self.policy.load(file_history=Path(self.output_dir, self.file_history), file_training=Path(self.output_dir, self.file_training), - file_predictor=Path(self.output_dir, self.file_predictor)) + file_predictor=Path(self.output_dir, self.file_predictor)) \ No newline at end of file diff --git a/src/odatse/algorithm/exchange.py b/src/odatse/algorithm/exchange.py index effd54a3..c6d2c33a 100644 --- a/src/odatse/algorithm/exchange.py +++ b/src/odatse/algorithm/exchange.py @@ -73,6 +73,18 @@ def __init__(self, runner: odatse.Runner = None, run_mode: str = "initial" ) -> None: + """ + Initialize the Algorithm class. + + Parameters + ---------- + info : odatse.Info + Information object containing algorithm parameters. + runner : odatse.Runner, optional + Runner object for executing the algorithm. + run_mode : str, optional + Mode to run the algorithm in, by default "initial". + """ time_sta = time.perf_counter() info_exchange = info.algorithm["exchange"] @@ -89,12 +101,18 @@ def __init__(self, self.timer["init"]["total"] = time_end - time_sta def _print_info(self) -> None: + """ + Print information about the algorithm. + """ if self.mpirank == 0: pass if self.mpisize > 1: self.mpicomm.barrier() def _initialize(self) -> None: + """ + Initialize the algorithm parameters and state. + """ super()._initialize() self.Tindex = np.arange( @@ -109,6 +127,9 @@ def _initialize(self) -> None: self._show_parameters() def _run(self) -> None: + """ + Run the algorithm. + """ # print(">>> _run") if self.mode is None: @@ -188,13 +209,28 @@ def _run(self) -> None: self._save_state(self.checkpoint_file) def _exchange(self, direction: bool) -> None: - """try to exchange temperatures""" + """ + Try to exchange temperatures. + + Parameters + ---------- + direction : bool + Direction of the exchange. + """ if self.nwalkers == 1: self.__exchange_single_walker(direction) else: self.__exchange_multi_walker(direction) def __exchange_single_walker(self, direction: bool) -> None: + """ + Exchange temperatures for a single walker. + + Parameters + ---------- + direction : bool + Direction of the exchange. + """ if self.mpisize > 1: self.mpicomm.Barrier() if direction: @@ -248,6 +284,14 @@ def __exchange_single_walker(self, direction: bool) -> None: self.mpicomm.Bcast(self.T2rep, root=0) def __exchange_multi_walker(self, direction: bool) -> None: + """ + Exchange temperatures for multiple walkers. + + Parameters + ---------- + direction : bool + Direction of the exchange. + """ comm = self.mpicomm if self.mpisize > 1: fx_all = comm.allgather(self.fx) @@ -292,10 +336,16 @@ def __exchange_multi_walker(self, direction: bool) -> None: ] def _prepare(self) -> None: + """ + Prepare the algorithm for execution. + """ self.timer["run"]["submit"] = 0.0 self.timer["run"]["exchange"] = 0.0 def _post(self) -> None: + """ + Post-process the results of the algorithm. + """ Ts = self.betas if self.input_as_beta else 1.0 / self.betas if self.mpirank == 0: print(f"start separateT {self.mpirank}") @@ -352,6 +402,14 @@ def _post(self) -> None: } def _save_state(self, filename) -> None: + """ + Save the current state of the algorithm. + + Parameters + ---------- + filename : str + The name of the file to save the state to. + """ data = { #-- _algorithm "mpisize": self.mpisize, @@ -380,6 +438,18 @@ def _save_state(self, filename) -> None: self._save_data(data, filename) def _load_state(self, filename, mode="resume", restore_rng=True): + """ + Load the state of the algorithm from a file. + + Parameters + ---------- + filename : str + The name of the file to load the state from. + mode : str, optional + The mode to load the state in, by default "resume". + restore_rng : bool, optional + Whether to restore the random number generator state, by default True. + """ data = self._load_data(filename) if not data: print("ERROR: Load status file failed") @@ -418,5 +488,4 @@ def _load_state(self, filename, mode="resume", restore_rng=True): self.Tindex = data["Tindex"] self.rep2T = data["rep2T"] self.T2rep = data["T2rep"] - self.exchange_direction = data["exchange_direction"] - + self.exchange_direction = data["exchange_direction"] \ No newline at end of file diff --git a/src/odatse/algorithm/mapper_mpi.py b/src/odatse/algorithm/mapper_mpi.py index df0a1ea3..9c6e2f7a 100644 --- a/src/odatse/algorithm/mapper_mpi.py +++ b/src/odatse/algorithm/mapper_mpi.py @@ -18,6 +18,10 @@ import odatse.domain class Algorithm(odatse.algorithm.AlgorithmBase): + """ + Algorithm class for data analysis of quantum beam diffraction experiments. + Inherits from odatse.algorithm.AlgorithmBase. + """ mesh_list: List[Union[int, float]] def __init__(self, info: odatse.Info, @@ -25,6 +29,20 @@ def __init__(self, info: odatse.Info, domain = None, run_mode: str = "initial" ) -> None: + """ + Initialize the Algorithm instance. + + Parameters + ---------- + info : Info + Information object containing algorithm parameters. + runner : Runner + Optional runner object for submitting tasks. + domain : + Optional domain object, defaults to MeshGrid. + run_mode : str + Mode to run the algorithm, defaults to "initial". + """ super().__init__(info=info, runner=runner, run_mode=run_mode) if domain and isinstance(domain, odatse.domain.MeshGrid): @@ -39,21 +57,25 @@ def __init__(self, info: odatse.Info, self.local_colormap_file = Path(self.colormap_file).name + ".tmp" def _initialize(self) -> None: + """ + Initialize the algorithm parameters and timer. + """ self.fx_list = [] self.timer["run"]["submit"] = 0.0 self._show_parameters() def _run(self) -> None: + """ + Execute the main algorithm process. + """ # Make ColorMap if self.mode is None: raise RuntimeError("mode unset") if self.mode.startswith("init"): - # print(">>> initialize") self._initialize() elif self.mode.startswith("resume"): - # print(">>> resume") self._load_state(self.checkpoint_file) else: raise RuntimeError("unknown mode {}".format(self.mode)) @@ -66,8 +88,6 @@ def _run(self) -> None: iterations = len(self.mesh_list) istart = len(self.fx_list) - # print(">>> iterations={}, istart={}".format(iterations, istart)) - next_checkpoint_step = istart + self.checkpoint_steps next_checkpoint_time = time.time() + self.checkpoint_interval @@ -103,8 +123,6 @@ def _run(self) -> None: opt_id, opt_fx = self.fx_list[opt_index] opt_mesh = self.mesh_list[opt_index] - # assert opt_id == opt_mesh[0] - self.opt_fx = opt_fx self.opt_mesh = opt_mesh @@ -113,12 +131,14 @@ def _run(self) -> None: self._output_results() if Path(self.local_colormap_file).exists(): - # print(">>> remove local colormap file {}".format(self.local_colormap_file)) os.remove(Path(self.local_colormap_file)) print("complete main process : rank {:08d}/{:08d}".format(self.mpirank, self.mpisize)) def _output_results(self): + """ + Output the results to the colormap file. + """ print("Make ColorMap") time_sta = time.perf_counter() @@ -143,10 +163,20 @@ def _output_results(self): self.timer["run"]["file_CM"] = time_end - time_sta def _prepare(self) -> None: - # do nothing + """ + Prepare the algorithm (no operation). + """ pass def _post(self) -> Dict: + """ + Post-process the results and gather data from all MPI ranks. + + Returns + ------- + Dict + Dictionary of results. + """ if self.mpisize > 1: fx_lists = self.mpicomm.allgather(self.fx_list) results = [v for vs in fx_lists for v in vs] @@ -155,7 +185,6 @@ def _post(self) -> Dict: if self.mpirank == 0: with open(self.colormap_file, "w") as fp: - # fp.write("#" + " ".join(self.label_list) + " fval\n") for x, (idx, fx) in zip(self.domain.grid, results): assert x[0] == idx fp.write(" ".join( @@ -165,38 +194,48 @@ def _post(self) -> Dict: return {} def _save_state(self, filename) -> None: + """ + Save the current state of the algorithm to a file. + + Parameters + ---------- + filename + The name of the file to save the state to. + """ data = { - #-- _algorithm "mpisize": self.mpisize, "mpirank": self.mpirank, - #"rng": self.rng.get_state(), "timer": self.timer, "info": self.info, - #-- mapper "fx_list": self.fx_list, "mesh_size": len(self.mesh_list), } self._save_data(data, filename) def _load_state(self, filename, restore_rng=True): + """ + Load the state of the algorithm from a file. + + Parameters + ---------- + filename + The name of the file to load the state from. + restore_rng : bool + Whether to restore the random number generator state. + """ data = self._load_data(filename) if not data: print("ERROR: Load status file failed") sys.exit(1) - #-- _algorithm assert self.mpisize == data["mpisize"] assert self.mpirank == data["mpirank"] - # if restore_rng: - # self.rng = np.random.RandomState() - # self.rng.set_state(data["rng"]) self.timer = data["timer"] info = data["info"] self._check_parameters(info) - #-- mapper self.fx_list = data["fx_list"] assert len(self.mesh_list) == data["mesh_size"] diff --git a/src/odatse/algorithm/min_search.py b/src/odatse/algorithm/min_search.py index 7c7a65f5..42fbd068 100644 --- a/src/odatse/algorithm/min_search.py +++ b/src/odatse/algorithm/min_search.py @@ -18,6 +18,9 @@ class Algorithm(odatse.algorithm.AlgorithmBase): + """ + Algorithm class for performing minimization using the Nelder-Mead method. + """ # inputs label_list: np.ndarray @@ -46,6 +49,20 @@ def __init__(self, info: odatse.Info, domain = None, run_mode: str = "initial" ) -> None: + """ + Initialize the Algorithm class. + + Parameters + ---------- + info : Info + Information object containing algorithm settings. + runner : Runner + Runner object for submitting jobs. + domain : + Domain object defining the search space. + run_mode : str + Mode of running the algorithm. + """ super().__init__(info=info, runner=runner, run_mode=run_mode) if domain and isinstance(domain, odatse.domain.Region): @@ -72,6 +89,9 @@ def __init__(self, info: odatse.Info, self._show_parameters() def _run(self) -> None: + """ + Run the minimization algorithm. + """ run = self.runner min_list = self.min_list @@ -90,17 +110,38 @@ def _run(self) -> None: if scipy_version[0] >= 1 and scipy_version[1] >= 11: def _cb(intermediate_result): + """ + Callback function for scipy.optimize.minimize. + """ x = intermediate_result.x fun = intermediate_result.fun print("eval: x={}, fun={}".format(x, fun)) iter_history.append([*x, fun]) else: def _cb(x): + """ + Callback function for scipy.optimize.minimize. + """ fun = _f_calc(x, 1) print("eval: x={}, fun={}".format(x, fun)) iter_history.append([*x, fun]) def _f_calc(x_list: np.ndarray, iset) -> float: + """ + Calculate the objective function value. + + Parameters + ---------- + x_list : np.ndarray + List of variables. + iset : + Set index. + + Returns + ------- + float + Objective function value. + """ # check if within region -> boundary option in minimize # note: 'bounds' option supported in scipy >= 1.7.0 in_range = np.all((min_list < x_list) & (x_list < max_list)) @@ -159,6 +200,9 @@ def _f_calc(x_list: np.ndarray, iset) -> float: self.mpicomm.barrier() def _prepare(self): + """ + Prepare the initial simplex for the Nelder-Mead algorithm. + """ # make initial simplex # [ v0, v0+a_1*e_1, v0+a_2*e_2, ... v0+a_d*e_d ] # where a = ( a_1 a_2 a_3 ... a_d ) and e_k is a unit vector along k-axis @@ -167,6 +211,9 @@ def _prepare(self): self.initial_simplex_list = np.vstack((v, v + np.diag(a))) def _output_results(self): + """ + Output the results of the minimization to files. + """ label_list = self.label_list with open("SimplexData.txt", "w") as fp: @@ -187,6 +234,9 @@ def _output_results(self): fp.write(f"function_evaluations = {self.funcalls}\n") def _post(self): + """ + Post-process the results after minimization. + """ result = { "x": self.xopt, "fx": self.fopt, diff --git a/src/odatse/algorithm/montecarlo.py b/src/odatse/algorithm/montecarlo.py index 00bcd93d..a59a92cf 100644 --- a/src/odatse/algorithm/montecarlo.py +++ b/src/odatse/algorithm/montecarlo.py @@ -85,11 +85,26 @@ class AlgorithmBase(odatse.algorithm.AlgorithmBase): naccepted: int def __init__(self, info: odatse.Info, - runner: odatse.Runner = None, - domain = None, - nwalkers: int = 1, - run_mode: str = "initial" - ) -> None: + runner: odatse.Runner = None, + domain = None, + nwalkers: int = 1, + run_mode: str = "initial") -> None: + """ + Initialize the AlgorithmBase class. + + Parameters + ---------- + info : odatse.Info + Information object containing algorithm parameters. + runner : odatse.Runner, optional + Runner object for executing the algorithm (default is None). + domain : optional + Domain object defining the problem space (default is None). + nwalkers : int, optional + Number of walkers (default is 1). + run_mode : str, optional + Mode of the run, e.g., "initial" (default is "initial"). + """ time_sta = time.perf_counter() super().__init__(info=info, runner=runner, run_mode=run_mode) self.nwalkers = nwalkers @@ -138,6 +153,13 @@ def __init__(self, info: odatse.Info, self.input_as_beta = False def _initialize(self): + """ + Initialize the algorithm state. + + This method sets up the initial state of the algorithm, including the + positions and energies of the walkers, and resets the counters for + accepted and trial steps. + """ if self.iscontinuous: self.domain.initialize(rng=self.rng, limitation=self.runner.limitation, num_walkers=self.nwalkers) self.x = self.domain.initial_list @@ -154,6 +176,21 @@ def _initialize(self): self.ntrial = 0 def _setup_neighbour(self, info_param): + """ + Set up the neighbor list for the discrete problem. + + Parameters + ---------- + info_param : dict + Dictionary containing algorithm parameters, including the path to the neighbor list file. + + Raises + ------ + ValueError + If the neighbor list path is not specified in the parameters. + RuntimeError + If the transition graph made from the neighbor list is not connected or not bidirectional. + """ if "neighborlist_path" in info_param: nn_path = self.root_dir / Path(info_param["neighborlist_path"]).expanduser() self.neighbor_list = load_neighbor_list(nn_path, nnodes=self.nnodes) @@ -176,16 +213,22 @@ def _setup_neighbour(self, info_param): ) # otherwise find neighbourlist + def _evaluate(self, in_range: np.ndarray = None) -> np.ndarray: - """evaluate current "Energy"s + """ + Evaluate the current "Energy"s. - ``self.fx`` will be overwritten with the result + This method overwrites `self.fx` with the result. Parameters - ========== - run_info: dict - Parameter set. - Some parameters will be overwritten. + ---------- + in_range : np.ndarray, optional + Array indicating whether each walker is within the valid range (default is None). + + Returns + ------- + np.ndarray + Array of evaluated energies for the current configurations. """ # print(">>> _evaluate") for iwalker in range(self.nwalkers): @@ -202,17 +245,18 @@ def _evaluate(self, in_range: np.ndarray = None) -> np.ndarray: return self.fx def propose(self, current: np.ndarray) -> np.ndarray: - """propose next candidate + """ + Propose the next candidate positions for the walkers. Parameters - ========== - current: np.ndarray - current position + ---------- + current : np.ndarray + Current positions of the walkers. Returns - ======= - proposed: np.ndarray - proposal + ------- + proposed : np.ndarray + Proposed new positions for the walkers. """ if self.iscontinuous: dx = self.rng.normal(size=(self.nwalkers, self.dimension)) * self.xstep @@ -229,10 +273,11 @@ def local_update( file_result: TextIO, extra_info_to_write: Union[List, Tuple] = None, ): - """one step of Monte Carlo + """ + one step of Monte Carlo Parameters - ========== + ---------- beta: np.ndarray inverse temperature for each walker file_trial: TextIO @@ -305,6 +350,16 @@ def local_update( self._write_result(file_result, extra_info_to_write=extra_info_to_write) def _write_result_header(self, fp, extra_names=None) -> None: + """ + Write the header for the result file. + + Parameters + ---------- + fp : TextIO + File pointer to the result file. + extra_names : list of str, optional + Additional column names to include in the header. + """ if self.input_as_beta: fp.write("# step walker beta fx") else: @@ -317,6 +372,16 @@ def _write_result_header(self, fp, extra_names=None) -> None: fp.write("\n") def _write_result(self, fp, extra_info_to_write: Union[List, Tuple] = None) -> None: + """ + Write the result of the current step to the file. + + Parameters + ---------- + fp : TextIO + File pointer to the result file. + extra_info_to_write : Union[List, Tuple], optional + Additional information to write for each walker (default is None). + """ for iwalker in range(self.nwalkers): if isinstance(self.Tindex, int): beta = self.betas[self.Tindex] @@ -336,16 +401,31 @@ def _write_result(self, fp, extra_info_to_write: Union[List, Tuple] = None) -> N fp.write(f" {ex[iwalker]}") fp.write("\n") fp.flush() - def read_Ts(info: dict, numT: int = None) -> Tuple[bool, np.ndarray]: """ + Read temperature or inverse-temperature values from the provided info dictionary. + + Parameters + ---------- + info : dict + Dictionary containing temperature or inverse-temperature parameters. + numT : int, optional + Number of temperature or inverse-temperature values to generate (default is None). Returns ------- - as_beta: bool - true when using inverse-temperature - betas: np.ndarray - sequence of inverse-temperature + as_beta : bool + True if using inverse-temperature, False if using temperature. + betas : np.ndarray + Sequence of inverse-temperature values. + + Raises + ------ + ValueError + If numT is not specified, or if both Tmin/Tmax and bmin/bmax are defined, or if neither are defined, + or if bmin/bmax or Tmin/Tmax values are invalid. + RuntimeError + If the mode is unknown (neither set_T nor set_b). """ if numT is None: raise ValueError("read_Ts: numT is not specified") diff --git a/src/odatse/algorithm/pamc.py b/src/odatse/algorithm/pamc.py index e4e3628f..b09427e9 100644 --- a/src/odatse/algorithm/pamc.py +++ b/src/odatse/algorithm/pamc.py @@ -92,6 +92,18 @@ def __init__(self, runner: odatse.Runner = None, run_mode: str = "initial" ) -> None: + """ + Initialize the Algorithm class. + + Parameters + ---------- + info : odatse.Info + Information object containing algorithm parameters. + runner : odatse.Runner, optional + Runner object for executing the algorithm, by default None. + run_mode : str, optional + Mode in which to run the algorithm, by default "initial". + """ time_sta = time.perf_counter() info_pamc = info.algorithm["pamc"] @@ -139,6 +151,19 @@ def _initialize(self) -> None: self._show_parameters() def _find_scheduling(self, info_pamc) -> int: + """ + Determine the scheduling for the algorithm based on the provided parameters. + + Parameters + ---------- + info_pamc : dict + Dictionary containing the parameters for the PAMC algorithm. + + Returns + ------- + int + The number of temperature steps (numT) determined from the input parameters. + """ numsteps = info_pamc.get("numsteps", 0) numsteps_annealing = info_pamc.get("numsteps_annealing", 0) numT = info_pamc.get("Tnum", 0) @@ -317,15 +342,16 @@ def _run(self) -> None: def _gather_information(self, numT: int = None) -> Dict[str, np.ndarray]: """ - Arguments - --------- + Gather status information of each process - numT: int + Parameters + --------- + numT : int size of dataset Returns ------- - res: Dict[str, np.ndarray] + res : Dict[str, np.ndarray] key-value corresponding is the following - fxs @@ -373,6 +399,19 @@ def _gather_information(self, numT: int = None) -> Dict[str, np.ndarray]: return res def _save_stats(self, info: Dict[str, np.ndarray]) -> None: + """ + Save statistical information from the algorithm run. + + Parameters + ---------- + info : Dict[str, np.ndarray] + Dictionary containing the following keys: + - fxs: Objective function of each walker over all processes. + - logweights: Logarithm of weights. + - ns: Number of walkers in each process. + - ancestors: Ancestor (origin) of each walker. + - acceptance ratio: Acceptance ratio for each temperature. + """ fxs = info["fxs"] numT, nreplicas = fxs.shape endTindex = self.Tindex + 1 @@ -383,7 +422,7 @@ def _save_stats(self, info: Dict[str, np.ndarray]) -> None: logweights - logweights.max(axis=1).reshape(-1, 1) ) # to avoid overflow - # bias-corrected jackknife resampling method + # Bias-corrected jackknife resampling method fs = np.zeros((numT, nreplicas)) fw_sum = (fxs * weights).sum(axis=1) w_sum = weights.sum(axis=1) @@ -404,7 +443,7 @@ def _save_stats(self, info: Dict[str, np.ndarray]) -> None: logz = np.log(np.mean(weights, axis=1)) self.logZs[startTindex:endTindex] = self.logZ + logz if endTindex < len(self.betas): - # calculate the next weight before reset and evalute dF + # Calculate the next weight before reset and evaluate dF bdiff = self.betas[endTindex] - self.betas[endTindex - 1] w = np.exp(logweights[-1, :] - bdiff * fxs[-1, :]) self.logZ = self.logZs[startTindex] + np.log(w.mean()) @@ -421,6 +460,13 @@ def _save_stats(self, info: Dict[str, np.ndarray]) -> None: ]))) def _resample(self) -> None: + """ + Perform the resampling of walkers. + + This method gathers information, saves statistical data, and performs resampling + using either fixed or varied weights. The method ensures that the algorithm + maintains a balanced set of walkers across different temperature steps. + """ res = self._gather_information() self._save_stats(res) @@ -440,6 +486,19 @@ def _resample(self) -> None: self.logweights = np.zeros(self.nwalkers) def _resample_varied(self, weights: np.ndarray, offset: int) -> None: + """ + Perform resampling with varied weights. + + This method resamples the walkers based on the provided weights and updates + the state of the algorithm accordingly. + + Parameters + ---------- + weights : np.ndarray + Array of weights for resampling. + offset : int + Offset for the weights array. + """ weights_sum = np.sum(weights) expected_numbers = (self.nreplicas[0] / weights_sum) * weights[ offset : offset + self.nwalkers @@ -474,6 +533,17 @@ def _resample_varied(self, weights: np.ndarray, offset: int) -> None: self.nwalkers = np.sum(next_numbers) def _resample_fixed(self, weights: np.ndarray) -> None: + """ + Perform resampling with fixed weights. + + This method resamples the walkers based on the provided weights and updates + the state of the algorithm accordingly. + + Parameters + ---------- + weights : np.ndarray + Array of weights for resampling. + """ resampler = odatse.util.resampling.WalkerTable(weights) new_index = resampler.sample(self.rng, self.nwalkers) @@ -504,10 +574,22 @@ def _resample_fixed(self, weights: np.ndarray) -> None: self.x = self.node_coordinates[self.inode, :] def _prepare(self) -> None: + """ + Prepare the algorithm for execution. + + This method initializes the timers for the 'submit' and 'resampling' phases + of the algorithm run. + """ self.timer["run"]["submit"] = 0.0 self.timer["run"]["resampling"] = 0.0 - def _post(self) -> None: + """ + Post-processing after the algorithm execution. + + This method consolidates the results from different temperature steps + into single files for 'result' and 'trial'. It also gathers the best + results from all processes and writes them to 'best_result.txt'. + """ for name in ("result", "trial"): with open(self.proc_dir / f"{name}.txt", "w") as fout: self._write_result_header(fout, ["weight", "ancestor"]) @@ -573,6 +655,14 @@ def _post(self) -> None: } def _save_state(self, filename) -> None: + """ + Save the current state of the algorithm to a file. + + Parameters + ---------- + filename : str + The name of the file where the state will be saved. + """ data = { #-- _algorithm "mpisize": self.mpisize, @@ -614,6 +704,18 @@ def _save_state(self, filename) -> None: self._save_data(data, filename) def _load_state(self, filename, mode="resume", restore_rng=True): + """ + Load the saved state of the algorithm from a file. + + Parameters + ---------- + filename : str + The name of the file from which the state will be loaded. + mode : str, optional + The mode in which to load the state. Can be "resume" or "continue", by default "resume". + restore_rng : bool, optional + Whether to restore the random number generator state, by default True. + """ data = self._load_data(filename) if not data: print("ERROR: Load status file failed") diff --git a/src/odatse/domain/_domain.py b/src/odatse/domain/_domain.py index 78b8f81b..77813989 100644 --- a/src/odatse/domain/_domain.py +++ b/src/odatse/domain/_domain.py @@ -14,7 +14,29 @@ import odatse class DomainBase: + """ + Base class for domain management in the 2DMAT software. + + Attributes + ---------- + root_dir : Path + The root directory for the domain. + output_dir : Path + The output directory for the domain. + mpisize : int + The size of the MPI communicator. + mpirank : int + The rank of the MPI process. + """ def __init__(self, info: odatse.Info = None): + """ + Initializes the DomainBase instance. + + Parameters + ---------- + info : Info, optional + An instance of odatse.Info containing base directory information. + """ if info: self.root_dir = info.base["root_dir"] self.output_dir = info.base["output_dir"] @@ -24,4 +46,3 @@ def __init__(self, info: odatse.Info = None): self.mpisize = odatse.mpi.size() self.mpirank = odatse.mpi.rank() - diff --git a/src/odatse/domain/meshgrid.py b/src/odatse/domain/meshgrid.py index 259d5e54..fb681682 100644 --- a/src/odatse/domain/meshgrid.py +++ b/src/odatse/domain/meshgrid.py @@ -15,13 +15,25 @@ from ._domain import DomainBase class MeshGrid(DomainBase): + """ + MeshGrid class for handling grid data for quantum beam diffraction experiments. + """ + grid: List[Union[int, float]] = [] grid_local: List[Union[int, float]] = [] candicates: int - - def __init__(self, info: odatse.Info = None, - *, - param: Dict[str, Any] = None): + + def __init__(self, info: odatse.Info = None, *, param: Dict[str, Any] = None): + """ + Initialize the MeshGrid object. + + Parameters + ---------- + info : Info, optional + Information object containing algorithm parameters. + param : dict, optional + Dictionary containing parameters for setting up the grid. + """ super().__init__(info) if info: @@ -34,26 +46,42 @@ def __init__(self, info: odatse.Info = None, else: pass - def do_split(self): + """ + Split the grid data among MPI processes. + """ if self.mpisize > 1: index = [idx for idx, *v in self.grid] index_local = np.array_split(index, self.mpisize)[self.mpirank] self.grid_local = [[idx, *v] for idx, *v in self.grid if idx in index_local] else: self.grid_local = self.grid - def _setup(self, info_param): + """ + Setup the grid based on provided parameters. + + Parameters + ---------- + info_param + Dictionary containing parameters for setting up the grid. + """ if "mesh_path" in info_param: self._setup_from_file(info_param) else: self._setup_grid(info_param) self.ncandicates = len(self.grid) - def _setup_from_file(self, info_param): + """ + Setup the grid from a file. + + Parameters + ---------- + info_param + Dictionary containing parameters for setting up the grid. + """ if "mesh_path" not in info_param: raise ValueError("ERROR: mesh_path not defined") mesh_path = self.root_dir / Path(info_param["mesh_path"]).expanduser() @@ -71,7 +99,7 @@ def _setup_from_file(self, info_param): data = data.reshape(1, -1) # old format: index x1 x2 ... -> omit index - data = data[:,1:] + data = data[:, 1:] else: data = None @@ -80,8 +108,15 @@ def _setup_from_file(self, info_param): self.grid = [[idx, *v] for idx, v in enumerate(data)] - def _setup_grid(self, info_param): + """ + Setup the grid based on min, max, and num lists. + + Parameters + ---------- + info_param + Dictionary containing parameters for setting up the grid. + """ if "min_list" not in info_param: raise ValueError("ERROR: algorithm.param.min_list is not defined in the input") min_list = np.array(info_param["min_list"], dtype=float) @@ -96,7 +131,7 @@ def _setup_grid(self, info_param): if len(min_list) != len(max_list) or len(min_list) != len(num_list): raise ValueError("ERROR: lengths of min_list, max_list, num_list do not match") - + xs = [ np.linspace(mn, mx, num=nm) for mn, mx, nm in zip(min_list, max_list, num_list) @@ -110,23 +145,55 @@ def _setup_grid(self, info_param): ) ] - def store_file(self, store_path, *, header=""): + """ + Store the grid data to a file. + + Parameters + ---------- + store_path + Path to the file where the grid data will be stored. + header + Header to be included in the file. + """ if self.mpirank == 0: np.savetxt(store_path, [[*v] for idx, *v in self.grid], header=header) - @classmethod def from_file(cls, mesh_path): + """ + Create a MeshGrid object from a file. + + Parameters + ---------- + mesh_path + Path to the file containing the grid data. + + Returns + ------- + MeshGrid + a MeshGrid object. + """ return cls(param={"mesh_path": mesh_path}) - @classmethod def from_dict(cls, param): + """ + Create a MeshGrid object from a dictionary of parameters. + + Parameters + ---------- + param + Dictionary containing parameters for setting up the grid. + + Returns + ------- + MeshGrid + a MeshGrid object. + """ return cls(param=param) - if __name__ == "__main__": ms = MeshGrid.from_dict({ 'min_list': [0,0,0], diff --git a/src/odatse/domain/region.py b/src/odatse/domain/region.py index d1c53e77..b68f38c8 100644 --- a/src/odatse/domain/region.py +++ b/src/odatse/domain/region.py @@ -15,14 +15,37 @@ from ._domain import DomainBase class Region(DomainBase): + """ + A class to represent a region in the domain. + + Attributes + ---------- + min_list : np.array + Minimum values for each dimension. + max_list : np.array + Maximum values for each dimension. + unit_list : np.array + Unit values for each dimension. + initial_list : np.array + Initial values for each dimension. + """ + min_list: np.array max_list: np.array unit_list: np.array initial_list: np.array - def __init__(self, info: odatse.Info = None, - *, - param: Dict[str, Any] = None): + def __init__(self, info: odatse.Info = None, *, param: Dict[str, Any] = None): + """ + Initialize the Region object. + + Parameters + ---------- + info : odatse.Info, optional + Information object containing algorithm parameters. + param : dict, optional + Dictionary containing algorithm parameters. + """ super().__init__(info) if info: @@ -34,9 +57,16 @@ def __init__(self, info: odatse.Info = None, self._setup(param) else: pass - def _setup(self, info_param): + """ + Setup the region with the given parameters. + + Parameters + ---------- + info_param : dict + Dictionary containing the parameters for the region. + """ if "min_list" not in info_param: raise ValueError("ERROR: algorithm.param.min_list is not defined in the input") min_list = np.array(info_param["min_list"]) @@ -49,7 +79,7 @@ def _setup(self, info_param): raise ValueError("ERROR: lengths of min_list and max_list do not match") self.dimension = len(min_list) - + unit_list = np.array(info_param.get("unit_list", [1.0] * self.dimension)) self.min_list = min_list @@ -69,10 +99,19 @@ def _setup(self, info_param): self.initial_list = initial_list - def initialize(self, - rng=np.random, - limitation=odatse.util.limitation.Unlimited(), - num_walkers: int = 1): + def initialize(self, rng=np.random, limitation=odatse.util.limitation.Unlimited(), num_walkers: int = 1): + """ + Initialize the region with random values or predefined initial values. + + Parameters + ---------- + rng : numpy.random, optional + Random number generator. + limitation : odatse.util.limitation, optional + Limitation object to judge the validity of the values. + num_walkers : int, optional + Number of walkers to initialize. + """ if num_walkers > self.num_walkers: self.num_walkers = num_walkers @@ -81,10 +120,19 @@ def initialize(self, else: self._init_random(rng=rng, limitation=limitation) - def _init_random(self, - rng=np.random, - limitation=odatse.util.limitation.Unlimited(), - max_count=100): + def _init_random(self, rng=np.random, limitation=odatse.util.limitation.Unlimited(), max_count=100): + """ + Initialize the region with random values within the specified limits. + + Parameters + ---------- + rng : numpy.random, optional + Random number generator. + limitation : odatse.util.limitation, optional + Limitation object to judge the validity of the values. + max_count : int, optional + Maximum number of trials to generate valid values. + """ initial_list = np.zeros((self.num_walkers, self.dimension), dtype=float) is_ok = np.full(self.num_walkers, False) @@ -102,7 +150,6 @@ def _init_random(self, raise RuntimeError("ERROR: init_random: trial count exceeds {}".format(max_count)) self.initial_list = initial_list - if __name__ == "__main__": reg = Region(param={ "min_list": [0.0, 0.0, 0.0], diff --git a/src/odatse/exception.py b/src/odatse/exception.py index 51fdff50..35647826 100644 --- a/src/odatse/exception.py +++ b/src/odatse/exception.py @@ -13,11 +13,12 @@ class Error(Exception): class InputError(Error): - """Exception raised for errors in inputs + """ + Exception raised for errors in inputs - Attributes - ========== - message: str + Parameters + ---------- + message : str explanation """ diff --git a/src/odatse/solver/_solver.py b/src/odatse/solver/_solver.py index e3893731..a0d964ed 100644 --- a/src/odatse/solver/_solver.py +++ b/src/odatse/solver/_solver.py @@ -20,6 +20,10 @@ class SolverBase(object, metaclass=ABCMeta): + """ + Abstract base class for solvers in the 2DMAT software. + """ + root_dir: Path output_dir: Path proc_dir: Path @@ -30,6 +34,14 @@ class SolverBase(object, metaclass=ABCMeta): @abstractmethod def __init__(self, info: odatse.Info) -> None: + """ + Initialize the solver with the given information. + + Parameters + ---------- + info : Info + Information object containing configuration details. + """ self.root_dir = info.base["root_dir"] self.output_dir = info.base["output_dir"] self.proc_dir = self.output_dir / str(odatse.mpi.rank()) @@ -43,8 +55,35 @@ def __init__(self, info: odatse.Info) -> None: @property def name(self) -> str: + """ + Get the name of the solver. + + Returns + ------- + str + The name of the solver. + """ return self._name @abstractmethod def evaluate(self, x: np.ndarray, arg: Tuple = (), nprocs: int = 1, nthreads: int = 1) -> None: + """ + Evaluate the solver with the given parameters. + + Parameters + ---------- + x : np.ndarray + Input data array. + arg : Tuple, optional + Additional arguments for evaluation. Defaults to (). + nprocs : nt, optional + Number of processes to use. Defaults to 1. + nthreads : int, optional + Number of threads to use. Defaults to 1. + + Raises + ------ + NotImplementedError + This method should be implemented by subclasses. + """ raise NotImplementedError() diff --git a/src/odatse/solver/analytical.py b/src/odatse/solver/analytical.py index 93f73528..899fcdd9 100644 --- a/src/odatse/solver/analytical.py +++ b/src/odatse/solver/analytical.py @@ -11,28 +11,64 @@ import odatse import odatse.solver.function - def quadratics(xs: np.ndarray) -> float: - """quadratic (sphear) function - - It has one global miminum f(xs)=0 at xs = [0,0,...,0]. """ - return np.sum(xs * xs) + Quadratic (sphere) function. + Parameters + ---------- + xs : np.ndarray + Input array. -def quartics(xs: np.ndarray) -> float: - """quartic function with two minimum + Returns + ------- + float + The calculated value of the quadratic function. - It has two global minimum f(xs)=0 at xs = [1,1,...,1] and [0,0,...,0]. - It has one suddle point f(0,0,...,0) = 1.0. + Notes + ----- + It has one global minimum f(xs)=0 at xs = [0,0,...,0]. """ + return np.sum(xs * xs) +def quartics(xs: np.ndarray) -> float: + """ + Quartic function with two global minima. + + Parameters + ---------- + xs : np.ndarray + Input array. + + Returns + ------- + float + The calculated value of the quartic function. + + Notes + ----- + It has two global minima f(xs)=0 at xs = [1,1,...,1] and [0,0,...,0]. + It has one saddle point f(0,0,...,0) = 1.0. + """ return np.mean((xs - 1.0) ** 2) * np.mean((xs + 1.0) ** 2) def ackley(xs: np.ndarray) -> float: - """Ackley's function in arbitrary dimension + """ + Ackley's function in arbitrary dimension + Parameters + ---------- + xs : np.ndarray + Input array. + + Returns + ------- + float + The calculated value of Ackley's function. + + Notes + ----- It has one global minimum f(xs)=0 at xs=[0,0,...,0]. It has many local minima. """ @@ -42,18 +78,42 @@ def ackley(xs: np.ndarray) -> float: b = np.exp(0.5 * np.sum(b)) return 20.0 + np.exp(1.0) - a - b - def rosenbrock(xs: np.ndarray) -> float: - """Rosenbrock's function + """ + Rosenbrock's function. + + Parameters + ---------- + xs : np.ndarray + Input array. + + Returns + ------- + float + The calculated value of Rosenbrock's function. + Notes + ----- It has one global minimum f(xs) = 0 at xs=[1,1,...,1]. """ return np.sum(100.0 * (xs[1:] - xs[:-1] ** 2) ** 2 + (1.0 - xs[:-1]) ** 2) - def himmelblau(xs: np.ndarray) -> float: - """Himmelblau's function + """ + Himmelblau's function. + + Parameters + ---------- + xs : np.ndarray + Input array of shape (2,). + Returns + ------- + float + The calculated value of Himmelblau's function. + + Notes + ----- It has four global minima f(xs) = 0 at xs=[3,2], [-2.805118..., 3.131312...], [-3.779310..., -3.2831860], and [3.584428..., -1.848126...]. """ @@ -63,9 +123,9 @@ def himmelblau(xs: np.ndarray) -> float: ) return (xs[0] ** 2 + xs[1] - 11.0) ** 2 + (xs[0] + xs[1] ** 2 - 7.0) ** 2 - def linear_regression_test(xs: np.ndarray) -> float: - """ Negative log likelihood of linear regression with Gaussian noise N(0,sigma) + """ + Negative log likelihood of linear regression with Gaussian noise N(0,sigma) y = ax + b @@ -75,7 +135,17 @@ def linear_regression_test(xs: np.ndarray) -> float: a = xs[0], b = xs[1], log(sigma**2) = xs[2] It has a global minimum f(xs) = 1.005071.. at - xs = [0.628571..., 0.8, -0.664976...]. + xs = [0.628571..., 0.8, -0.664976...]. + + Parameters + ---------- + xs : np.ndarray + Input array of model parameters. + + Returns + ------- + float + The negative log likelihood of the linear regression model. """ if xs.shape[0] != 3: raise RuntimeError( @@ -90,7 +160,6 @@ def linear_regression_test(xs: np.ndarray) -> float: n * xs[2] + np.sum((xs[0] * xdata + xs[1] - ydata) ** 2) / np.exp(xs[2]) ) - class Solver(odatse.solver.function.Solver): """Function Solver with pre-defined benchmark functions""" @@ -104,6 +173,7 @@ def __init__(self, info: odatse.Info) -> None: Parameters ---------- info: Info + Information object containing solver configuration. """ super().__init__(info) self._name = "analytical" diff --git a/src/odatse/solver/function.py b/src/odatse/solver/function.py index b0efe54e..908a3523 100644 --- a/src/odatse/solver/function.py +++ b/src/odatse/solver/function.py @@ -17,6 +17,9 @@ class Solver(odatse.solver.SolverBase): + """ + Solver class for evaluating functions with given parameters. + """ x: np.ndarray fx: float _func: Optional[Callable[[np.ndarray], float]] @@ -28,6 +31,7 @@ def __init__(self, info: odatse.Info) -> None: Parameters ---------- info: Info + Information object containing solver configuration. """ super().__init__(info) self._name = "function" @@ -37,6 +41,25 @@ def __init__(self, info: odatse.Info) -> None: self.delay = info.solver.get("delay", 0.0) def evaluate(self, x: np.ndarray, args: Tuple = (), nprocs: int = 1, nthreads: int = 1) -> float: + """ + Evaluate the function with given parameters. + + Parameters + ---------- + x : np.ndarray + Input array for the function. + args : Tuple, optional + Additional arguments for the function. + nprocs : int, optional + Number of processes to use. + nthreads : int, optional + Number of threads to use. + + Returns + ------- + float + Result of the function evaluation. + """ self.prepare(x, args) cwd = os.getcwd() os.chdir(self.work_dir) @@ -46,9 +69,34 @@ def evaluate(self, x: np.ndarray, args: Tuple = (), nprocs: int = 1, nthreads: i return result def prepare(self, x: np.ndarray, args = ()) -> None: + """ + Prepare the solver with the given parameters. + + Parameters + ---------- + x : np.ndarray + Input array for the function. + args : tuple, optional + Additional arguments for the function. + """ self.x = x def run(self, nprocs: int = 1, nthreads: int = 1) -> None: + """ + Run the function evaluation. + + Parameters + ---------- + nprocs : int, optional + Number of processes to use. + nthreads : int, optional + Number of threads to use. + + Raises + ------ + RuntimeError + If the function is not set. + """ if self._func is None: raise RuntimeError( "ERROR: function is not set. Make sure that `set_function` is called." @@ -59,7 +107,23 @@ def run(self, nprocs: int = 1, nthreads: int = 1) -> None: time.sleep(self.delay) def get_results(self) -> float: + """ + Get the results of the function evaluation. + + Returns + ------- + float + Result of the function evaluation. + """ return self.fx def set_function(self, f: Callable[[np.ndarray], float]) -> None: + """ + Set the function to be evaluated. + + Parameters + ---------- + f : Callable[[np.ndarray], float] + Function to be evaluated. + """ self._func = f diff --git a/src/odatse/util/graph.py b/src/odatse/util/graph.py index 008b7b45..5c3cac73 100644 --- a/src/odatse/util/graph.py +++ b/src/odatse/util/graph.py @@ -14,6 +14,19 @@ def is_connected(nnlist: List[List[int]]) -> bool: + """ + Check if the graph represented by the neighbor list is connected. + + Parameters + ---------- + nnlist : List[List[int]] + A list of lists where each sublist represents the neighbors of a node. + + Returns + ------- + bool + True if the graph is connected, False otherwise. + """ nnodes = len(nnlist) visited = np.full(nnodes, False) nvisited = 1 @@ -30,13 +43,25 @@ def is_connected(nnlist: List[List[int]]) -> bool: def is_bidirectional(nnlist: List[List[int]]) -> bool: + """ + Check if the graph represented by the neighbor list is bidirectional. + + Parameters + ---------- + nnlist : List[List[int]] + A list of lists where each sublist represents the neighbors of a node. + + Returns + ------- + bool + True if the graph is bidirectional, False otherwise. + """ for i in range(len(nnlist)): for j in nnlist[i]: if i not in nnlist[j]: return False return True - if __name__ == "__main__": filename = "./neighborlist.txt" nnlist = [] diff --git a/src/odatse/util/limitation.py b/src/odatse/util/limitation.py index 355de4ba..46132e98 100644 --- a/src/odatse/util/limitation.py +++ b/src/odatse/util/limitation.py @@ -14,22 +14,84 @@ class LimitationBase(metaclass=ABCMeta): + """ + Abstract base class for limitations. + """ + @abstractmethod def __init__(self, is_limitary: bool): + """ + Initialize the limitation. + + Parameters + ---------- + is_limitary : bool + Boolean indicating if the limitation is active. + """ self.is_limitary = is_limitary @abstractmethod def judge(self, x: np.ndarray) -> bool: + """ + Abstract method to judge if the limitation is satisfied. + + Parameters + ---------- + x : np.ndarray + Input array to be judged. + + Returns + ------- + bool + Boolean indicating if the limitation is satisfied. + """ raise NotImplementedError class Unlimited(LimitationBase): + """ + Class representing an unlimited (no limitation) condition. + """ + def __init__(self): + """ + Initialize the unlimited condition. + """ super().__init__(False) + def judge(self, x: np.ndarray) -> bool: + """ + Always returns True as there is no limitation. + + Parameters + ---------- + x : np.ndarray + Input array to be judged. + + Returns + ------- + bool + Always True. + """ return True class Inequality(LimitationBase): + """ + Class representing an inequality limitation. + """ + def __init__(self, a: np.ndarray, b: np.ndarray, is_limitary: bool): + """ + Initialize the inequality limitation. + + Parameters + ---------- + a : np.ndarray + Coefficient matrix. + b : np.ndarray + Constant vector. + is_limitary : bool + Boolean indicating if the limitation is active. + """ super().__init__(is_limitary) if self.is_limitary: self.a = np.array(a) @@ -39,6 +101,19 @@ def __init__(self, a: np.ndarray, b: np.ndarray, is_limitary: bool): self.ndim = a.shape[1] def judge(self, x: np.ndarray) -> bool: + """ + Judge if the inequality limitation is satisfied. + + Parameters + ---------- + x : np.ndarray + Input array to be judged. + + Returns + ------- + bool + Boolean indicating if the limitation is satisfied. + """ if self.is_limitary: Ax_b = np.dot(self.a, x) + self.b judge_result = np.all(Ax_b > 0) @@ -48,12 +123,22 @@ def judge(self, x: np.ndarray) -> bool: @classmethod def from_dict(cls, d): + """ + Create an Inequality instance from a dictionary. + + Parameters + ---------- + d + Dictionary containing 'co_a' and 'co_b' keys. + + Returns + ------- + Inequality + an Inequality instance. + """ co_a: np.ndarray = read_matrix(d.get("co_a", [])) co_b: np.ndarray = read_matrix(d.get("co_b", [])) - # is_set_co_a = (co_a.size > 0 and co_a.ndim == 2 and co_a.shape[1] == dimension) - # is_set_co_b = (co_b.size > 0 and co_b.ndim == 2 and co_b.shape == (co_a.shape[0], 1)) - if co_a.size == 0: is_set_co_a = False else: diff --git a/src/odatse/util/logger.py b/src/odatse/util/logger.py index 5c5ceecf..a90f3f3b 100644 --- a/src/odatse/util/logger.py +++ b/src/odatse/util/logger.py @@ -23,6 +23,10 @@ # write_result class Logger: + """ + Logger class to handle logging of calls, elapsed time, and optionally input and result data. + """ + logfile: Path buffer_size: int buffer: List[str] @@ -40,7 +44,26 @@ def __init__(self, info: Optional[odatse.Info] = None, write_result: bool = False, params: Optional[Dict[str,Any]] = None, **rest) -> None: - + """ + Initialize the Logger. + + Parameters + ---------- + info : Info, optional + Information object containing logging parameters. + buffer_size : int + Size of the buffer before writing to the log file. + filename : str + Name of the log file. + write_input : bool + Flag to indicate if input should be logged. + write_result : bool + Flag to indicate if result should be logged. + params : Dict[str,Any]], optional + Additional parameters for logging. + **rest + Additional keyword arguments. + """ if info is not None: info_log = info.runner.get("log", {}) else: @@ -57,9 +80,25 @@ def __init__(self, info: Optional[odatse.Info] = None, self.buffer = [] def is_active(self) -> bool: + """ + Check if logging is active. + + Returns + ------- + bool + True if logging is active, False otherwise. + """ return self.buffer_size > 0 def prepare(self, proc_dir: Path) -> None: + """ + Prepare the log file for writing. + + Parameters + ---------- + proc_dir : Path + Directory where the log file will be created. + """ if not self.is_active(): return @@ -78,6 +117,18 @@ def prepare(self, proc_dir: Path) -> None: f.write("\n") def count(self, x: np.ndarray, args, result: float) -> None: + """ + Log a call with input and result data. + + Parameters + ---------- + x : np.ndarray + Input data. + args : + Additional arguments. + result : float + Result data. + """ if not self.is_active(): return @@ -102,6 +153,9 @@ def count(self, x: np.ndarray, args, result: float) -> None: self.write() def write(self) -> None: + """ + Write the buffered log entries to the log file. + """ if not self.is_active(): return with open(self.logfile, "a") as f: diff --git a/src/odatse/util/mapping.py b/src/odatse/util/mapping.py index 18ca7daf..a6d83e20 100644 --- a/src/odatse/util/mapping.py +++ b/src/odatse/util/mapping.py @@ -9,32 +9,80 @@ import copy import numpy as np -from .read_matrix import read_matrix, read_vector +from .read_matrix import read_matrix # type hints from typing import Optional class MappingBase: + """ + Base class for mapping operations. + """ + def __init__(self): pass def __call__(self, x: np.ndarray) -> np.ndarray: - raise NotImplemented + """ + Apply the mapping to the input array. + + Parameters + ---------- + x : np.ndarray + Input array. + + Returns + ------- + np.ndarray + Mapped array. + """ + raise NotImplementedError class TrivialMapping(MappingBase): + """ + A trivial mapping that returns the input array unchanged. + """ + def __init__(self): super().__init__() def __call__(self, x: np.ndarray) -> np.ndarray: + """ + Return the input array unchanged. + + Parameters + ---------- + x : np.ndarray + Input array. + + Returns + ------- + np.ndarray + The same input array. + """ return x class Affine(MappingBase): + """ + An affine mapping defined by a matrix A and a vector b. + """ + A: Optional[np.ndarray] b: Optional[np.ndarray] def __init__(self, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None): + """ + Initialize the affine mapping. + + Parameters + ---------- + A : np.ndarray, optional + Transformation matrix. + b : np.ndarray, optional + Translation vector. + """ # copy arguments self.A = np.array(A) if A is not None else None self.b = np.array(b) if b is not None else None @@ -50,8 +98,20 @@ def __init__(self, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = Non if not self.A.shape[0] == self.b.shape[0]: raise ValueError("shape of A and b mismatch") - def __call__(self, x: np.ndarray) -> np.ndarray: + """ + Apply the affine mapping to the input array. + + Parameters + ---------- + x : np.ndarray + Input array. + + Returns + ------- + np.ndarray + Mapped array. + """ if self.A is None: ret = copy.copy(x) else: @@ -63,6 +123,19 @@ def __call__(self, x: np.ndarray) -> np.ndarray: @classmethod def from_dict(cls, d): + """ + Create an Affine instance from a dictionary. + + Parameters + ---------- + d : dict + Dictionary containing 'A' and 'b' keys. + + Returns + ------- + Affine + An instance of the Affine class. + """ A: Optional[np.ndarray] = read_matrix(d.get("A", [])) b: Optional[np.ndarray] = read_matrix(d.get("b", [])) @@ -82,7 +155,7 @@ def from_dict(cls, d): if not (b.ndim == 2 and b.shape[1] == 1): raise ValueError("b should be a column vector") if not (A is not None and b.shape[0] == A.shape[0]): - raise ValueError("shape of A and b does not match") + raise ValueError("shape of A and b mismatch") b = b.reshape(-1) return cls(A, b) diff --git a/src/odatse/util/neighborlist.py b/src/odatse/util/neighborlist.py index 0ee05620..e312fea4 100644 --- a/src/odatse/util/neighborlist.py +++ b/src/odatse/util/neighborlist.py @@ -26,6 +26,10 @@ class Cells: + """ + A class to represent a grid of cells for spatial partitioning. + """ + cells: List[Set[int]] dimension: int mins: np.ndarray @@ -35,6 +39,18 @@ class Cells: cellsize: float def __init__(self, mins: np.ndarray, maxs: np.ndarray, cellsize: float): + """ + Initialize the Cells object. + + Parameters + ---------- + mins : np.ndarray + The minimum coordinates of the grid. + maxs : np.ndarray + The maximum coordinates of the grid. + cellsize : float + The size of each cell. + """ self.dimension = len(mins) self.mins = mins Ls = (maxs - mins) * 1.001 @@ -45,12 +61,51 @@ def __init__(self, mins: np.ndarray, maxs: np.ndarray, cellsize: float): self.cells = [set() for _ in range(self.ncell)] def coord2cellindex(self, x: np.ndarray) -> int: + """ + Convert coordinates to a cell index. + + Parameters + ---------- + x : np.ndarray + The coordinates to convert. + + Returns + ------- + int + The index of the cell. + """ return self.cellcoord2cellindex(self.coord2cellcoord(x)) def coord2cellcoord(self, x: np.ndarray) -> np.ndarray: + """ + Convert coordinates to cell coordinates. + + Parameters + ---------- + x : np.ndarray + The coordinates to convert. + + Returns + ------- + np.ndarray + The cell coordinates. + """ return np.floor((x - self.mins) / self.cellsize).astype(np.int64) def cellcoord2cellindex(self, ns: np.ndarray) -> int: + """ + Convert cell coordinates to a cell index. + + Parameters + ---------- + ns : np.ndarray + The cell coordinates to convert. + + Returns + ------- + int + The index of the cell. + """ index = 0 oldN = 1 for n, N in zip(ns, self.Ns): @@ -60,6 +115,19 @@ def cellcoord2cellindex(self, ns: np.ndarray) -> int: return index def cellindex2cellcoord(self, index: int) -> np.ndarray: + """ + Convert a cell index to cell coordinates. + + Parameters + ---------- + index : int + The index of the cell. + + Returns + ------- + np.ndarray + The cell coordinates. + """ ns = np.zeros(self.dimension, dtype=np.int64) for d in range(self.dimension): d = self.dimension - d - 1 @@ -69,6 +137,19 @@ def cellindex2cellcoord(self, index: int) -> np.ndarray: return ns def out_of_bound(self, ns: np.ndarray) -> bool: + """ + Check if cell coordinates are out of bounds. + + Parameters + ---------- + ns : np.ndarray + The cell coordinates to check. + + Returns + ------- + bool + True if out of bounds, False otherwise. + """ if np.any(ns < 0): return True if np.any(ns >= self.Ns): @@ -76,6 +157,19 @@ def out_of_bound(self, ns: np.ndarray) -> bool: return False def neighborcells(self, index: int) -> List[int]: + """ + Get the indices of neighboring cells. + + Parameters + ---------- + index : int + The index of the cell. + + Returns + ------- + List[int] + The indices of the neighboring cells. + """ neighbors: List[int] = [] center_coord = self.cellindex2cellcoord(index) for diff in itertools.product([-1, 0, 1], repeat=self.dimension): @@ -86,7 +180,6 @@ def neighborcells(self, index: int) -> List[int]: neighbors.append(other_coord_index) return neighbors - def make_neighbor_list_cell( X: np.ndarray, radius: float, @@ -94,6 +187,27 @@ def make_neighbor_list_cell( show_progress: bool, comm: mpi.Comm = None, ) -> List[List[int]]: + """ + Create a neighbor list using cell-based spatial partitioning. + + Parameters + ---------- + X : np.ndarray + The coordinates of the points. + radius : float + The radius within which neighbors are considered. + allow_selfloop : bool + Whether to allow self-loops in the neighbor list. + show_progress : bool + Whether to show a progress bar. + comm : mpi.Comm, optional + The MPI communicator. + + Returns + ------- + List[List[int]] + The neighbor list. + """ if comm is None: mpisize = 1 mpirank = 0 @@ -145,6 +259,27 @@ def make_neighbor_list_naive( show_progress: bool, comm: mpi.Comm = None, ) -> List[List[int]]: + """ + Create a neighbor list using a naive all-pairs approach. + + Parameters + ---------- + X : np.ndarray) + The coordinates of the points. + radius : float + The radius within which neighbors are considered. + allow_selfloop : bool + Whether to allow self-loops in the neighbor list. + show_progress : bool + Whether to show a progress bar. + comm : mpi.Comm, optional + The MPI communicator. + + Returns + ------- + List[List[int]] + The neighbor list. + """ if comm is None: mpisize = 1 mpirank = 0 @@ -187,6 +322,29 @@ def make_neighbor_list( show_progress: bool = False, comm: mpi.Comm = None, ) -> List[List[int]]: + """ + Create a neighbor list for given points. + + Parameters + ---------- + X : np.ndarray + The coordinates of the points. + radius : float + The radius within which neighbors are considered. + allow_selfloop : bool + Whether to allow self-loops in the neighbor list. + check_allpairs : bool + Whether to use the naive all-pairs approach. + show_progress : bool + Whether to show a progress bar. + comm : mpi.Comm, optional + The MPI communicator. + + Returns + ------- + List[List[int]] + The neighbor list. + """ if check_allpairs: return make_neighbor_list_naive( X, @@ -206,6 +364,21 @@ def make_neighbor_list( def load_neighbor_list(filename: PathLike, nnodes: int = None) -> List[List[int]]: + """ + Load a neighbor list from a file. + + Parameters + ---------- + filename : PathLike + The path to the file containing the neighbor list. + nnodes : int, optional + The number of nodes. If None, it will be determined from the file. + + Returns + ------- + List[List[int]] + The neighbor list. + """ if nnodes is None: nnodes = 0 with open(filename) as f: @@ -214,6 +387,7 @@ def load_neighbor_list(filename: PathLike, nnodes: int = None) -> List[List[int] if len(line) == 0: continue nnodes += 1 + neighbor_list: List[List[int]] = [[] for _ in range(nnodes)] with open(filename) as f: for line in f: @@ -233,6 +407,20 @@ def write_neighbor_list( radius: float = None, unit: np.ndarray = None, ): + """ + Write the neighbor list to a file. + + Parameters + ---------- + filename : str + The path to the output file. + nnlist : List[List[int]] + The neighbor list to write. + radius : float, optional + The neighborhood radius. Defaults to None. + unit : np.ndarray, optional + The unit for each coordinate. Defaults to None. + """ with open(filename, "w") as f: if radius is not None: f.write(f"# radius = {radius}\n") @@ -247,7 +435,6 @@ def write_neighbor_list( f.write(f" {o}") f.write("\n") - def main(): import argparse diff --git a/src/odatse/util/read_matrix.py b/src/odatse/util/read_matrix.py index 615898c7..734bae3d 100644 --- a/src/odatse/util/read_matrix.py +++ b/src/odatse/util/read_matrix.py @@ -12,6 +12,24 @@ def read_vector(inp: Union[str, List[float]]) -> np.ndarray: + """ + Converts an input string or list of floats into a numpy array vector. + + Parameters + ---------- + inp : Union[str, List[float]] + Input data, either as a space-separated string of numbers or a list of floats. + + Returns + ------- + np.ndarray + A numpy array representing the vector. + + Raises + ------ + RuntimeError + If the input is not a vector. + """ if isinstance(inp, str): vlist = [float(w) for w in inp.split()] else: @@ -22,8 +40,25 @@ def read_vector(inp: Union[str, List[float]]) -> np.ndarray: raise RuntimeError(msg) return v - def read_matrix(inp: Union[str, List[List[float]]]) -> np.ndarray: + """ + Converts an input string or list of lists of floats into a numpy array matrix. + + Parameters + ---------- + inp : Union[str, List[List[float]]] + Input data, either as a string with rows of space-separated numbers or a list of lists of floats. + + Returns + ------- + np.ndarray + A numpy array representing the matrix. + + Raises + ------ + RuntimeError + If the input is not a matrix. + """ if isinstance(inp, str): Alist: List[List[float]] = [] for line in inp.split("\n"): diff --git a/src/odatse/util/resampling.py b/src/odatse/util/resampling.py index 6070e95d..46beb7c1 100644 --- a/src/odatse/util/resampling.py +++ b/src/odatse/util/resampling.py @@ -27,43 +27,137 @@ def sample(self, rs: np.random.RandomState, size=None) -> Union[int, np.ndarray] class BinarySearch(Resampler): + """ + A resampler that uses binary search to sample based on given weights. + """ weights_accumulated: List[float] wmax: float def __init__(self, weights: Iterable): + """ + Initialize the BinarySearch resampler with the given weights. + + Parameters + ---------- + weights : Iterable + An iterable of weights. + """ self.reset(weights) def reset(self, weights: Iterable): + """ + Reset the resampler with new weights. + + Parameters + ---------- + weights : Iterable + An iterable of weights. + """ self.weights_accumulated = list(itertools.accumulate(weights)) self.wmax = self.weights_accumulated[-1] @typing.overload def sample(self, rs: np.random.RandomState) -> int: + """ + Sample a single index based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + + Returns + ------- + int + A single sampled index. + """ ... @typing.overload def sample(self, rs: np.random.RandomState, size) -> np.ndarray: + """ + Sample multiple indices based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + size : + The number of samples to generate. + + Returns + ------- + np.ndarray + An array of sampled indices. + """ ... def sample(self, rs: np.random.RandomState, size=None) -> Union[int, np.ndarray]: + """ + Sample indices based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + size : + The number of samples to generate. If None, a single sample is generated. + + Returns + ------- + int or np.ndarray + A single sampled index or an array of sampled indices. + """ if size is None: return self._sample(self.wmax * rs.rand()) else: return np.array([self._sample(r) for r in self.wmax * rs.rand(size)]) def _sample(self, r: float) -> int: + """ + Perform a binary search to find the index corresponding to the given random number. + + Parameters + ---------- + r : float + A random number scaled by the maximum weight. + + Returns + ------- + int + The index corresponding to the random number. + """ return typing.cast(int, np.searchsorted(self.weights_accumulated, r)) class WalkerTable(Resampler): + """ + A resampler that uses Walker's alias method to sample based on given weights. + """ N: int itable: np.ndarray ptable: np.ndarray def __init__(self, weights: Iterable): + """ + Initialize the WalkerTable resampler with the given weights. + + Parameters + ---------- + weights : Iterable + An iterable of weights. + """ self.reset(weights) def reset(self, weights: Iterable): + """ + Reset the resampler with new weights. + + Parameters + ---------- + weights : Iterable + An iterable of weights. + """ self.ptable = np.array(weights).astype(np.float64).flatten() self.N = len(self.ptable) self.itable = np.full(self.N, -1) @@ -85,13 +179,56 @@ def reset(self, weights: Iterable): @typing.overload def sample(self, rs: np.random.RandomState) -> int: + """ + Sample a single index based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + + Returns + ------- + int + A single sampled index. + """ ... @typing.overload def sample(self, rs: np.random.RandomState, size) -> np.ndarray: + """ + Sample multiple indices based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + size : + The number of samples to generate. + + Returns + ------- + np.ndarray + An array of sampled indices. + """ ... def sample(self, rs: np.random.RandomState, size=None) -> Union[int, np.ndarray]: + """ + Sample indices based on the weights. + + Parameters + ---------- + rs : np.random.RandomState + A random state for generating random numbers. + size : + The number of samples to generate. If None, a single sample is generated. + + Returns + ------- + int or np.ndarray + A single sampled index or an array of sampled indices. + """ if size is None: r = rs.rand() * self.N return self._sample(r) @@ -99,10 +236,23 @@ def sample(self, rs: np.random.RandomState, size=None) -> Union[int, np.ndarray] r = rs.rand(size) * self.N i = np.floor(r).astype(np.int64) p = r - i - ret = np.where(p < self.ptable[i], i, self.itable[i]) + ret = np.where(p < self.ptable[i], i, self.itable[i]) return ret def _sample(self, r: float) -> int: + """ + Perform a sampling operation based on the given random number. + + Parameters + ---------- + r : float + A random number scaled by the number of weights. + + Returns + ------- + int + The index corresponding to the random number. + """ i = int(np.floor(r)) p = r - i if p < self.ptable[i]: @@ -110,7 +260,6 @@ def _sample(self, r: float) -> int: else: return self.itable[i] - if __name__ == "__main__": import argparse diff --git a/src/odatse/util/separateT.py b/src/odatse/util/separateT.py index 6d5dc38b..0d04d8c9 100644 --- a/src/odatse/util/separateT.py +++ b/src/odatse/util/separateT.py @@ -26,6 +26,24 @@ def separateT( use_beta: bool, buffer_size: int = 10000, ) -> None: + """ + Separates and processes temperature data for quantum beam diffraction experiments. + + Parameters + ---------- + Ts : np.ndarray + Array of temperature values. + nwalkers : int + Number of walkers. + output_dir : PathLike + Directory to store the output files. + comm : mpi.Comm, optional + MPI communicator for parallel processing. + use_beta : bool + Flag to determine if beta values are used instead of temperature. + buffer_size : int, optional + Size of the buffer for reading input data. Default is 10000. + """ if comm is None: mpisize = 1 mpirank = 0 diff --git a/tests/bayes/do.sh b/tests/bayes/do.sh index d1244560..c25a0b92 100644 --- a/tests/bayes/do.sh +++ b/tests/bayes/do.sh @@ -1,14 +1,20 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Run the Python script with the input file and measure the time taken time python3 ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/BayesData.txt +# Compare the result file with the reference file echo diff $resfile ref.txt res=0 diff $resfile ref.txt || res=$? + +# Check the result of the diff command if [ $res -eq 0 ]; then echo TEST PASS true @@ -16,4 +22,3 @@ else echo TEST FAILED: $resfile and ref.txt differ false fi - diff --git a/tests/bayes_continue/do.sh b/tests/bayes_continue/do.sh index f488bc82..1685c3c3 100644 --- a/tests/bayes_continue/do.sh +++ b/tests/bayes_continue/do.sh @@ -1,23 +1,31 @@ #!/bin/sh +# Command to run the main Python script CMD="python3 -u ../../src/odatse_main.py" +# Remove the output1 directory if it exists rm -rf output1 +# Run the main script with input1a.toml and input1b.toml time $CMD input1a.toml time $CMD --cont input1b.toml +# Remove the output2 directory if it exists rm -rf output2 +# Run the main script with input2.toml time $CMD input2.toml - +# Define the result and reference files resfile=output1/BayesData.txt reffile=output2/BayesData.txt +# Compare the result and reference files echo diff $resfile $reffile res=0 diff $resfile $reffile || res=$? + +# Check the result of the comparison if [ $res -eq 0 ]; then echo TEST PASS true diff --git a/tests/exchange/do.sh b/tests/exchange/do.sh index 4f5e6447..0f5efac2 100644 --- a/tests/exchange/do.sh +++ b/tests/exchange/do.sh @@ -1,19 +1,26 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Run the Python script using mpiexec with 4 processes and measure the time taken time mpiexec --oversubscribe -np 4 python3 ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/best_result.txt +# Compare the result file with the reference file echo diff $resfile ref.txt res=0 diff $resfile ref.txt || res=$? + +# Check the result of the diff command if [ $res -eq 0 ]; then + # If the files are the same, print TEST PASS echo TEST PASS true else + # If the files differ, print TEST FAILED with the result file path echo TEST FAILED: $resfile and ref.txt differ false -fi - +fi \ No newline at end of file diff --git a/tests/exchange_continue/do.sh b/tests/exchange_continue/do.sh index 451e9fa0..d84dfc2b 100644 --- a/tests/exchange_continue/do.sh +++ b/tests/exchange_continue/do.sh @@ -1,30 +1,38 @@ #!/bin/sh +# Command to run the Python script with MPI CMD="mpiexec --oversubscribe -np 4 python3 -u ../../src/odatse_main.py" +# Remove the output1 directory if it exists rm -rf output1 +# Run the command with the first input file and measure the time taken time $CMD input1a.toml +# Run the command with the continuation input file and measure the time taken time $CMD --cont input1b.toml +# Remove the output2 directory if it exists rm -rf output2 +# Run the command with the second input file and measure the time taken time $CMD input2.toml - -#resfile=output1/best_result.txt -#reffile=output2/best_result.txt +# Define the result files to compare +# resfile=output1/best_result.txt +# reffile=output2/best_result.txt resfile=output1/result_T0.txt reffile=output2/result_T0.txt +# Compare the result files and store the result of the comparison echo diff $resfile $reffile res=0 diff $resfile $reffile || res=$? + +# Check the result of the comparison and print the appropriate message if [ $res -eq 0 ]; then echo TEST PASS true else echo TEST FAILED: $resfile and $reffile differ false -fi - +fi \ No newline at end of file diff --git a/tests/exchange_mesh/do.sh b/tests/exchange_mesh/do.sh index 09bf2322..40761310 100644 --- a/tests/exchange_mesh/do.sh +++ b/tests/exchange_mesh/do.sh @@ -1,28 +1,35 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Generate MeshData.txt using makemesh.py echo generate MeshData.txt time python3 ./makemesh.py > MeshData.txt echo +# Generate neighborlist.txt using odatse_neighborlist.py with a radius of 0.11 echo generate neighborlist.txt time python3 ../../src/odatse_neighborlist.py -r 0.11 MeshData.txt echo +# Perform exchange Monte Carlo simulation using odatse_main.py with input.toml echo perform exchange mc time python3 ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/best_result.txt +# Compare the result file with the reference file echo diff $resfile ref.txt res=0 diff $resfile ref.txt || res=$? if [ $res -eq 0 ]; then + # Output TEST PASS if files are identical echo TEST PASS true else + # Output TEST FAILED if files differ echo TEST FAILED: $resfile and ref.txt differ false -fi - +fi \ No newline at end of file diff --git a/tests/mapper/do.sh b/tests/mapper/do.sh index 2f192d47..14aa65a7 100644 --- a/tests/mapper/do.sh +++ b/tests/mapper/do.sh @@ -1,19 +1,24 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Run the Python script with MPI, using 2 processes time mpiexec --oversubscribe -np 2 python3 ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/ColorMap.txt +# Compare the result file with the reference file echo diff $resfile ref.txt res=0 diff $resfile ref.txt || res=$? + +# Check the result of the diff command if [ $res -eq 0 ]; then echo TEST PASS true else echo TEST FAILED: $resfile and ref.txt differ false -fi - +fi \ No newline at end of file diff --git a/tests/mapper_resume/do.sh b/tests/mapper_resume/do.sh index 92cf7727..6a59618a 100644 --- a/tests/mapper_resume/do.sh +++ b/tests/mapper_resume/do.sh @@ -1,30 +1,43 @@ #!/bin/sh +# Command to run the main Python script CMD="python3 -u ../../src/odatse_main.py" -#CMD="mpiexec -np 2 python3 -u ../../src/odatse_main.py" +# Uncomment the following line to run with MPI +# CMD="mpiexec -np 2 python3 -u ../../src/odatse_main.py" +# Remove the output1 directory if it exists rm -rf output1 +# Run the command with a timeout of 12 seconds using input1.toml time timeout 12s $CMD input1.toml +# Run the command with the --resume option using input1.toml time $CMD --resume input1.toml +# Remove the output2 directory if it exists rm -rf output2 +# Run the command using input2.toml time $CMD input2.toml - +# Define the result and reference files resfile=output1/ColorMap.txt reffile=output2/ColorMap.txt +# Print the diff command to be executed echo diff $resfile $reffile + +# Initialize the result variable res=0 + +# Compare the result and reference files, update res if they differ diff $resfile $reffile || res=$? + +# Check if the files are identical if [ $res -eq 0 ]; then echo TEST PASS true else echo TEST FAILED: $resfile and $reffile differ false -fi - +fi \ No newline at end of file diff --git a/tests/minsearch/do.sh b/tests/minsearch/do.sh index bd4bf163..40982da5 100644 --- a/tests/minsearch/do.sh +++ b/tests/minsearch/do.sh @@ -1,19 +1,30 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Run the Python script with the input file and measure the time taken time python3 ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/res.txt +# Display the diff command being executed echo diff $resfile ref.txt + +# Initialize the result variable res=0 + +# Compare the result file with the reference file, update the result variable if they differ diff $resfile ref.txt || res=$? + +# Check the result of the diff command if [ $res -eq 0 ]; then + # If the files are the same, print TEST PASS echo TEST PASS true else + # If the files differ, print TEST FAILED with the file names echo TEST FAILED: $resfile and ref.txt differ false -fi - +fi \ No newline at end of file diff --git a/tests/pamc/do.sh b/tests/pamc/do.sh index 01838abb..29336be1 100644 --- a/tests/pamc/do.sh +++ b/tests/pamc/do.sh @@ -1,19 +1,24 @@ #!/bin/sh +# Remove the output directory if it exists rm -rf output +# Run the Python script using MPI with 2 processes time mpiexec --oversubscribe -np 2 python3 -m mpi4py ../../src/odatse_main.py input.toml +# Define the result file path resfile=output/best_result.txt +# Compare the result file with the reference file echo diff $resfile ref.txt res=0 diff $resfile ref.txt || res=$? + +# Check the result of the diff command if [ $res -eq 0 ]; then echo TEST PASS true else echo TEST FAILED: $resfile and ref.txt differ false -fi - +fi \ No newline at end of file diff --git a/tests/pamc_continue/do.sh b/tests/pamc_continue/do.sh index afd70103..c9968b37 100644 --- a/tests/pamc_continue/do.sh +++ b/tests/pamc_continue/do.sh @@ -1,30 +1,43 @@ #!/bin/sh +# Command to run the Python script with MPI CMD="mpiexec --oversubscribe -np 2 python3 -u ../../src/odatse_main.py" +# Remove the output1 directory if it exists rm -rf output1 +# Run the command with input1a.toml and log the output time $CMD input1a.toml 2>&1 | tee run.log.1a + +# Run the command with input1b.toml in continuation mode and log the output time $CMD --cont input1b.toml 2>&1 | tee run.log.1b +# Remove the output2 directory if it exists rm -rf output2 +# Run the command with input2.toml and log the output time $CMD input2.toml 2>&1 | tee run.log.2 - -#resfile=output1/best_result.txt -#reffile=output2/best_result.txt +# Define the result and reference files for comparison +# resfile=output1/best_result.txt +# reffile=output2/best_result.txt resfile=output1/fx.txt reffile=output2/fx.txt +# Print the diff command to be executed echo diff $resfile $reffile + +# Initialize the result variable res=0 + +# Compare the result and reference files, update the result variable if they differ diff $resfile $reffile || res=$? + +# Check the result of the diff command and print the appropriate message if [ $res -eq 0 ]; then echo TEST PASS true else echo TEST FAILED: $resfile and $reffile differ false -fi - +fi \ No newline at end of file diff --git a/tests/transform/do.sh b/tests/transform/do.sh index db8cc6fb..a6ceae14 100644 --- a/tests/transform/do.sh +++ b/tests/transform/do.sh @@ -1,19 +1,28 @@ #!/bin/sh +# Remove the existing ColorMap.txt file from the output_transform directory rm -f output_transform/ColorMap.txt + +# Run the odatse_main.py script with the input_transform.toml configuration file python3 ../../src/odatse_main.py input_transform.toml +# Remove the existing ColorMap.txt file from the output_meshlist directory rm -f output_meshlist/ColorMap.txt + +# Run the odatse_main.py script with the input_meshlist.toml configuration file python3 ../../src/odatse_main.py input_meshlist.toml +# Calculate the difference between the ColorMap.txt files from both outputs res=$( paste output_transform/ColorMap.txt output_meshlist/ColorMap.txt \ | awk 'BEGIN {diff = 0.0} {diff += ($2 - $(NF))**2} END {print diff/NR}' ) + +# Check if the difference is zero if [ $res = 0 ]; then echo TEST PASS true else echo "TEST FAILED (diff = $res)" false -fi +fi \ No newline at end of file