diff --git a/documentation/source/usage/running-process.md b/documentation/source/usage/running-process.md index 86844903ad..c709679158 100644 --- a/documentation/source/usage/running-process.md +++ b/documentation/source/usage/running-process.md @@ -52,6 +52,15 @@ will produce the following output files in the same directory as the input file: ### VaryRun +VaryRun is a tool which takes an input file that does not converge and varies the initial values of the iteration variables, within a tolerance, to find an initial point that converges, and creates an input file using these variables. + +VaryRun requires a `.conf` file, which specifies certain parameters needed for VaryRun. These +include a path to the original input file, the maximum number of iterations to perform, and a +factor within which the iteration variables are changed. + +For a given iteration, `X`, of VaryRun, the values of the iteration variables will be +changed and a new input file, `X_IN.DAT` will be created in the same directory as the `.conf` file. `PROCESS` will be run on this input file to produce the associated output files, `X_MFILE.DAT`, `X_OUT.DAT` and `X_process.log`. If `VaryRun` is not able to find a converging input file within the maximum number of iterations, some more information on error status can be found in the created README.txt file. If no converging solution is found, you could try increasing the maximum number of iterations, changing the factor within which the iteration variables are changed, or by changing the initial values of the iteration variables. + The default VaryRun configuration filename is `run_process.conf`. If no configuration filename is given as an argument in the command line, `run_process.conf` is assumed to be present in the current directory: ```bash # Use a configuration file called run_process.conf in the current directory @@ -87,10 +96,10 @@ The configuration file has the following format: * Path to working directory in which PROCESS is run. WDIR = . -* original IN.DAT name (should not be called IN.DAT!) -ORIGINAL_IN_DAT = large_tokamak_IN.DAT +* original IN.DAT name +ORIGINAL_IN_DAT = path/to/original_IN.DAT -* ONE line comment to be put into README.txt +* optional ONE line comment to be put into README.txt COMMENT = * Maximum number of runs diff --git a/examples/data/run_process.conf b/examples/data/run_process.conf index 4c8cd345bb..7b5df8f93c 100644 --- a/examples/data/run_process.conf +++ b/examples/data/run_process.conf @@ -5,8 +5,8 @@ * Path to working directory in which PROCESS is run. WDIR = . -* original IN.DAT name (should not be called IN.DAT!) -ORIGINAL_IN_DAT = large_tokamak_IN.DAT +* original IN.DAT name +ORIGINAL_IN_DAT = large_tokamak_varyrun_IN.DAT * Max no. iterations NITER = 30 diff --git a/examples/vary_run_example.ex.py b/examples/vary_run_example.ex.py index 7183ae1e4a..bee2addeae 100644 --- a/examples/vary_run_example.ex.py +++ b/examples/vary_run_example.ex.py @@ -34,8 +34,8 @@ # `VaryRun` requires a `.conf` file which specifies certain parameters needed for `VaryRun`. # In this file, you specify the original input file, the maximum number of iterations to be performed, and a factor within which the iteration variables are changed. # -# If `VaryRun` is able to find a new initial point within the maximum number of iterations, it produces a new input file, called `IN.DAT`, in the same directory as your initial input file. This new file will now converge when you run `PROCESS`. -# +# If `VaryRun` is able to find a new initial point within the maximum number of iterations, you can find this file in the same directory as your initial input file. This new file will now converge when you run `PROCESS`. +# `VaryRun` will produce new, numbered sets of files for each iteration it performs. # %% [markdown] # # VaryRun setup # @@ -55,7 +55,7 @@ # * Path to working directory in which PROCESS is run. # WDIR = . # -# * original IN.DAT name (should not be called IN.DAT!) +# * original IN.DAT name # ORIGINAL_IN_DAT = ORIGINAL_IN.DAT # # * Max no. iterations @@ -71,7 +71,7 @@ # %% [markdown] # ## Run `VaryRun` # -# Run `PROCESS` on an input file using the `VaryRun` class. The initial input file, `large_tokamak_varyrun_IN.DAT` does not converge. `VaryRun` will vary the initial values of the iteration variables to find an initial point that converges, and will create a new input file, `IN.DAT`, with these new values. +# Run `PROCESS` on an input file using the `VaryRun` class. The initial input file, `large_tokamak_varyrun_IN.DAT` does not converge. `VaryRun` will vary the initial values of the iteration variables to find an initial point that converges, and will create a new input file with these new values. # %% # %load_ext autoreload @@ -92,7 +92,7 @@ input_file = data_dir / "large_tokamak_varyrun_IN.DAT" temp_dir = tempfile.TemporaryDirectory() -input_path = Path(temp_dir.name) / "large_tokamak_IN.DAT" +input_path = Path(temp_dir.name) / "large_tokamak_varyrun_IN.DAT" conf_path = Path(temp_dir.name) / "run_process.conf" shutil.copy(input_file, input_path) shutil.copy(conf_file, conf_path) @@ -110,7 +110,7 @@ vary_run.run() os.chdir(cwd) - +# %% # Get the initial values from the original input file iteration_variable_names, original_iteration_variable_values = ( get_mfile_initial_ixc_values(input_file) @@ -120,7 +120,7 @@ # VaryRun always produces a file called IN.DAT in the same directory # as the conf file _, updated_iteration_variable_values = get_mfile_initial_ixc_values( - Path(temp_dir.name) / "IN.DAT" + Path(temp_dir.name) / "2_IN.DAT" ) # %% [markdown] diff --git a/process/core/input.py b/process/core/input.py index 825efef1df..1d917ca628 100644 --- a/process/core/input.py +++ b/process/core/input.py @@ -11,7 +11,10 @@ import process from process import data_structure -from process.core.exceptions import ProcessValidationError, ProcessValueError +from process.core.exceptions import ( + ProcessValidationError, + ProcessValueError, +) from process.core.solver.constraints import ConstraintManager if TYPE_CHECKING: diff --git a/process/core/io/mfile/base.py b/process/core/io/mfile/base.py index d8c36a578c..8ed8943aba 100644 --- a/process/core/io/mfile/base.py +++ b/process/core/io/mfile/base.py @@ -587,7 +587,7 @@ def get_mfile_initial_ixc_values(file_path: Path): Parameters ---------- file_path : - The path to the MFile to get the initial iteration variable values from. + The path to the input file to get the initial iteration variable values from. Notes ----- diff --git a/process/core/io/run_process.conf b/process/core/io/run_process.conf deleted file mode 100644 index 4c2eb6cbcd..0000000000 --- a/process/core/io/run_process.conf +++ /dev/null @@ -1,59 +0,0 @@ -* TEMPLATE CONFIG FILE FOR RUNNING PROCESS WITH MODIFIED IN.DAT -* This is used by VaryRun in Python Process -* This file is an example: e.g. PROCESS_DIR/RUN_DIR is not substituted as a -* variable: it must be defined explcitly by the user -* (Does not allow for inline comments!) - -* Path to working directory in which PROCESS is run. -WDIR = PROCESS_DIR/RUN_DIR - -* original IN.DAT name. Should not be called IN.DAT, as the intermediate input -* files created by VaryRun are called IN.DAT -ORIGINAL_IN_DAT = PROCESS_DIR/RUN_DIR/STEP_IN.DAT - -* PATH to PROCESS binary -* This is no longer used in Python Process, and will be removed -* PROCESS= PROCESS_DIR/bin/process.exe - -* ONE line comment to be put into README.txt -COMMENT = - -* Max no. iterations -NITER = 50 - -* integer seed for random number generator; use None for random seed -SEED = 2 - -* factor within which the iteration variables are changed -FACTOR = 1.5 - -* Number of allowed unfeasible points that do not trigger rerunning. -NO_ALLOWED_UNFEASIBLE = 0 - -* include a summary file with the iteration variables at each stage. -INCLUDE_ITERVAR_DIFF = True - -* add iteration variables - comma separated -ADD_IXC = - -* remove iteration variables - comma separated -DEL_IXC = - -* add constraint equations - comma separated -ADD_ICC = - -* remove constraint equations - comma separated -DEL_ICC = - - -* set any variable to a new value, the variable does not have to exist in IN.DAT -* VAR_example = 10 sets example = 10 - -*VAR_IOPTIMZ = - -*VAR_EPSVMC = - -*VAR_TFTORT = - -* remove variables -* DEL_VAR_IITER diff --git a/process/core/io/vary_run/config.py b/process/core/io/vary_run/config.py index 4aa948fb3e..28724696b5 100644 --- a/process/core/io/vary_run/config.py +++ b/process/core/io/vary_run/config.py @@ -1,7 +1,5 @@ """ -Interfaces for Configuration values for programs -- run_process.py -- test_process.py +Configuration for VaryRun. """ import logging @@ -15,6 +13,7 @@ import click from numpy.random import default_rng +from process import data_structure from process.core.io.in_dat import InDat from process.core.io.mfile import MFile from process.core.io.vary_run.tools import ( @@ -35,10 +34,10 @@ class ProcessConfig: """Configuration parameters for PROCESS runs""" + or_in_dat: Path + """Original IN.DAT file""" wdir: Path = field(default_factory=Path.cwd) """Working directory""" - or_in_dat: Path = Path("IN.DAT") - """Original IN.DAT file""" niter: int = 10 """(Maximum) number of iterations""" u_seed: int | None = None @@ -51,7 +50,7 @@ class ProcessConfig: comment: str = " " """additional comment to be written into README.txt""" _filename: Path | None = None - """filename if supplied""" + """filename of .conf file if supplied""" @classmethod def from_file(cls, filename: str | Path, solver: str = "vmcon"): @@ -97,7 +96,7 @@ def from_file(cls, filename: str | Path, solver: str = "vmcon"): print(f"No comment in config file {filename}") return cls( - wdir, or_in_dat, niter, u_seed, factor, solver, comment or " ", filename + or_in_dat, wdir, niter, u_seed, factor, solver, comment or " ", filename ) @staticmethod @@ -146,15 +145,29 @@ def __iter__(self): return self def __next__(self): - _neqns, itervars = get_neqns_itervars(wdir=self.wdir) - lbs, ubs = get_variable_range(itervars, self.factor, self.wdir) + _neqns, itervars = get_neqns_itervars(in_dat=self.infile, wdir=self.wdir) + lbs, ubs = get_variable_range(itervars, self.factor, self.infile, self.wdir) + self.run_process(self.wdir / self.infile, self.solver) + check_input_error(mfile=self.outfile, wdir=self.wdir) + indat, mfile = self.infile, self.outfile + + # See if converged + m_file = MFile(filename=self.wdir / mfile) + ifail = m_file.data["ifail"].get_scan(-1) + + if ifail != 1: + print(f"VaryRun iteration {self._current_iteration} did not converge.\n") + else: + print( + f"PROCESS found a converged solution using VaryRun. The converging input file is {self._current_iteration}_IN.DAT\n" + ) + self._current_iteration += 1 if self._current_iteration >= self.niter: - self.error_status2readme(mfile=mfile) + # Reached maximum number of iterations + self.error_status2readme(mfile=self.prev_outfile) raise StopIteration - self.run_process(self.wdir / indat, self.solver) - check_input_error(wdir=self.wdir, mfile=mfile) return indat, mfile, itervars, lbs, ubs @@ -166,6 +179,18 @@ def infile(self): def outfile(self): return self._base_output.format(self._current_iteration) + @property + def prev_outfile(self): + return self._base_output.format(self._current_iteration - 1) + + @property + def prev_infile(self): + return + + @property + def initial_infile(self): + return self._base_input.format(0) + def echo(self): """Echos the attributes of the class""" if self.wdir != Path.cwd(): @@ -186,7 +211,7 @@ def prepare_wdir(self): if not self.wdir.is_dir(): self.wdir.mkdir(exist_ok=True, parents=True) - copy(self.or_in_dat, self.wdir / "0_IN.DAT") + copy(self.or_in_dat, self.wdir / self.initial_infile) if self._filename is not None: with suppress(SameFileError): @@ -196,10 +221,7 @@ def prepare_wdir(self): "OUT.DAT", "MFILE.DAT", "README.txt", - "SolverTest.out", "process.log", - "uncertainties.nc", - "time.info", ): Path(self.wdir, file).unlink(missing_ok=True) for f in self.wdir.glob("*.pdf"): @@ -212,20 +234,29 @@ def create_readme(self): if self.comment != "": Path(self.wdir, "README.txt").write_text(self.comment) - def error_status2readme(self, mfile="MFILE.DAT"): - """Appends PROCESS outcome to README.txt""" + def error_status2readme(self, mfile): + """Appends PROCESS outcome to README.txt if reached maximum iterations""" m_file = MFile(filename=self.wdir / mfile) error_status = ( f"Error status: {m_file.data['error_status'].get_scan(-1)} " f"Error ID: {m_file.data['error_id'].get_scan(-1)}\n" ) + ifail = m_file.data["ifail"].get_scan(-1) + if ifail != 1: + ifail_msg = f"PROCESS has been unable to find a converging input file within the chosen maximum number of iterations.\nYou could try increasing the maximum number of iterations (which is currently set to {self.niter}),\nchanging the factor within which the iteration variables are changed,\nor by changing the initial values of the iteration variables." + else: + ifail_msg = f"PROCESS found a converged solution using VaryRun. The converging input file is {self._current_iteration - 1}_IN.DAT" if self.comment != "": with open(Path(self.wdir, "README.txt"), "a") as readme: readme.write(error_status) + readme.write(ifail_msg) + print(ifail_msg) else: - Path(self.wdir, "README.txt").write_text(error_status) + with open(Path(self.wdir, "README.txt"), "a") as readme: + readme.write(error_status) + print(ifail_msg) def modify_in_dat(self): """Modifies the original IN.DAT file""" @@ -240,10 +271,15 @@ def setup(self): self.modify_in_dat() - check_in_dat("0_IN.DAT") + check_in_dat(self.initial_infile) self.generator = default_rng(seed=self.u_seed) + data_structure.global_variables.output_prefix = str( + self.wdir / self.outfile.strip("MFILE.DAT") + ) + data_structure.global_variables.fileprefix = str(self.wdir / self.infile) + def run_process(self, input_path: Path, solver: str = "vmcon"): """Perform a single run of PROCESS, catching any errors. @@ -330,8 +366,8 @@ def from_file(cls, filename: str | Path = "run_process.conf", solver: str = "vmc del_icc = cls.get_attribute_csv_list(self._filename, "del_icc") return cls( - self.wdir, self.or_in_dat, + self.wdir, self.niter, self.u_seed, self.factor, @@ -464,7 +500,7 @@ def modify_in_dat(self): def modify_vars(self): """Modifies IN.DAT by adding, deleting and modifiying variables""" - in_dat = InDat(filename="0_IN.DAT") + in_dat = InDat(filename=self.initial_infile) # add and modify variables for key in self.dictvar: @@ -482,10 +518,10 @@ def modify_vars(self): else: in_dat.remove_parameter(key) - in_dat.write_in_dat(output_filename=self.wdir / "0_IN.DAT") + in_dat.write_in_dat(output_filename=self.wdir / self.initial_infile) def modify_ixc(self): - """Modifies the array of iteration variables in IN.DAT""" + """Modifies the array of iteration variables in the IN.DAT""" # check that there is no variable in both lists if set(self.add_ixc).intersection(self.del_ixc) != set(): logger.error( @@ -494,7 +530,8 @@ def modify_ixc(self): ) sys.exit() - in_dat = InDat(filename="0_IN.DAT") + # Fine here + in_dat = InDat(filename=self.wdir / self.initial_infile) for iter_var in self.add_ixc: in_dat.add_iteration_variable(int(iter_var)) @@ -502,7 +539,7 @@ def modify_ixc(self): for iter_var in self.del_ixc: in_dat.remove_iteration_variable(int(iter_var)) - in_dat.write_in_dat(output_filename=self.wdir / "0_IN.DAT") + in_dat.write_in_dat(output_filename=self.wdir / self.initial_infile) def modify_icc(self): """Modifies the array of constraint equations in IN.DAT""" @@ -514,7 +551,7 @@ def modify_icc(self): ) sys.exit() - in_dat = InDat(filename="0_IN.DAT") + in_dat = InDat(filename=self.wdir / self.initial_infile) for constr in self.add_icc: in_dat.add_constraint_equation(int(constr)) @@ -522,4 +559,4 @@ def modify_icc(self): for constr in self.del_icc: in_dat.remove_constraint_equation(int(constr)) - in_dat.write_in_dat(output_filename=self.wdir / "0_IN.DAT") + in_dat.write_in_dat(output_filename=self.wdir / self.initial_infile) diff --git a/process/core/io/vary_run/tools.py b/process/core/io/vary_run/tools.py index f7bff01d36..641367f6c3 100644 --- a/process/core/io/vary_run/tools.py +++ b/process/core/io/vary_run/tools.py @@ -15,13 +15,13 @@ logger = logging.getLogger(__name__) -def get_neqns_itervars(wdir="."): +def get_neqns_itervars(in_dat, wdir="."): """Returns the number of equations and a list of variable names of all iteration variables """ # Load dicts from dicts JSON file dicts = get_dicts() - in_dat = InDat(Path(wdir, "IN.DAT")) + in_dat = InDat(Path(wdir, in_dat)) ixc_list = in_dat.data["ixc"].get_value @@ -36,7 +36,7 @@ def get_neqns_itervars(wdir="."): return in_dat.number_of_constraints, itervars -def update_ixc_bounds(wdir=".", indat="IN.DAT"): +def update_ixc_bounds(indat, wdir="."): """Updates the lower and upper bounds in DICT_IXC_BOUNDS from IN.DAT """ @@ -55,7 +55,7 @@ def update_ixc_bounds(wdir=".", indat="IN.DAT"): dicts["DICT_IXC_BOUNDS"][name]["ub"] = float(value["u"]) -def get_variable_range(itervars, factor, wdir=".", indat="IN.DAT"): +def get_variable_range(itervars, factor, indat, wdir="."): """Returns the lower and upper bounds of the variable range for each iteration variable. @@ -129,7 +129,7 @@ def get_variable_range(itervars, factor, wdir=".", indat="IN.DAT"): return lbs, ubs -def check_in_dat(filename="IN.DAT"): +def check_in_dat(filename): """Tests IN.DAT during setup: 1)Are ixc bounds outside of allowed input ranges? """ @@ -194,10 +194,10 @@ def check_in_dat(filename="IN.DAT"): ) sleep(1) - in_dat.write_in_dat(output_filename="IN.DAT") + in_dat.write_in_dat(output_filename=filename) -def check_input_error(wdir=".", mfile="MFILE.DAT"): +def check_input_error(mfile, wdir="."): """Checks, if an input error has occurred. Stops as a consequence. Will also fail if the MFILE.DAT isn't found. @@ -265,7 +265,7 @@ def no_unfeasible_mfile(wdir=".", mfile="MFILE.DAT"): def vary_iteration_variables(itervars, lbs, ubs, config): - """Routine to change the iteration variables in IN.DAT + """Routine to change the iteration variables in the initial IN.DAT within given bounds. Parameters @@ -279,7 +279,7 @@ def vary_iteration_variables(itervars, lbs, ubs, config): generator : Generator numpy generator to create random numbers """ - in_dat = InDat() + in_dat = InDat(config.initial_infile) new_values = [] diff --git a/process/data_structure/global_variables.py b/process/data_structure/global_variables.py index 57e36bdf7e..b599b85961 100644 --- a/process/data_structure/global_variables.py +++ b/process/data_structure/global_variables.py @@ -12,7 +12,7 @@ """Maximum number of solver iterations""" fileprefix: str = None -"""Input file path prefix""" +"""Path to input file""" output_prefix: str = None """Output file path prefix""" diff --git a/process/main.py b/process/main.py index 7d405fbccb..81b8ce3c85 100644 --- a/process/main.py +++ b/process/main.py @@ -250,9 +250,10 @@ class VaryRun: An IN.DAT file as specified in the config file Output files: - All of them in the work directory specified in the config file - OUT.DAT - PROCESS output - MFILE.DAT - PROCESS output + All of them in the working directory specified in the config file + X_IN.DAT - PROCESS input + X_OUT.DAT - PROCESS output + X_MFILE.DAT - PROCESS output process.log - logfile of PROCESS output to stdout README.txt - contains comments from config file """ @@ -284,11 +285,11 @@ def run(self): FileNotFoundError if input file doesn't exist """ + init.init_all_module_vars() self.config.setup() setup_loggers(Path(self.config.wdir) / "process.log") - init.init_all_module_vars() init.init_process(self.data) # TODO add diff ixc summary part diff --git a/tests/integration/data/run_process.conf b/tests/integration/data/run_process.conf index 7685c96531..6abe2f7034 100644 --- a/tests/integration/data/run_process.conf +++ b/tests/integration/data/run_process.conf @@ -5,7 +5,7 @@ * Path to working directory in which PROCESS is run. WDIR = . -* original IN.DAT name (should not be called IN.DAT!) +* original IN.DAT name ORIGINAL_IN_DAT = large_tokamak_IN.DAT * ONE line comment to be put into README.txt