diff --git a/CMakeLists.txt b/CMakeLists.txt index 46979e38c..a2be73910 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,6 @@ LIST(APPEND PROCESS_SRCS buildings_variables.f90 maths_library.f90 iteration_variables.f90 - evaluators.f90 water_usage_variables.f90 constraint_equations.f90 constants.f90 diff --git a/README.md b/README.md index 2e6bb2efd..925a12452 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ PROCESS is the reactor systems code at the [UK Atomic Energy Authority](https://ccfe.ukaea.uk/). More information on PROCESS can be found on the PROCESS [webpage](https://ccfe.ukaea.uk/resources/process/). -PROCESS was originally a Fortran code, but is currently a mixture of Python and Python-wrapped Fortran; the eventual aim is to have an entirely Python code base. In order to use PROCESS, the Fortran must be compiled and a Python-Fortran interface generated for the Python to import. Once built, it can be installed and run as a Python package. +PROCESS was originally a Fortran code, but is currently a mixture of Python and Python-wrapped Fortran; the eventual aim is to have an entirely Python code base. In order to use PROCESS, the Fortran must be compiled and a Python-Fortran interface generated for the Python to import. Once built, it can be installed and run as a Python package. + +Due to this ongoing conversion work, **PROCESS version 3 is unstable and does not guarantee backward compatibility**. PROCESS version 4 will be the first major version to enforce backward-compatible API changes and will be released following confirmation of variable names and the Python data structure. diff --git a/process/caller.py b/process/caller.py index 8bbe91018..b80d03a9b 100644 --- a/process/caller.py +++ b/process/caller.py @@ -3,6 +3,7 @@ import numpy as np import logging from process.final import finalise +from process.objectives import objective_function from process.io.mfile import MFile from process.utilities.f2py_string_patch import f2py_compatible_to_string from typing import Union, Tuple, TYPE_CHECKING @@ -70,7 +71,7 @@ def call_models(self, xc: np.ndarray, m: int) -> Tuple[float, np.ndarray]: for _ in range(10): self._call_models_once(xc) # Evaluate objective function and constraints - objf = ft.function_evaluator.funfom() + objf = objective_function(ft.numerics.minmax) conf, _, _, _, _ = ft.constraints.constraint_eqns(m, -1) if objf_prev is None and conf_prev is None: diff --git a/process/final.py b/process/final.py index df764b977..bd0775d01 100644 --- a/process/final.py +++ b/process/final.py @@ -5,11 +5,11 @@ from process.fortran import ( process_output as po, constants, - function_evaluator, numerics, constraints, ) from process import output as op +from process.objectives import objective_function from process.utilities.f2py_string_patch import f2py_compatible_to_string @@ -50,7 +50,7 @@ def output_once_through(): po.oblnkl(constants.nout) # Evaluate objective function - norm_objf = function_evaluator.funfom() + norm_objf = objective_function(numerics.minmax) po.ovarre( constants.mfile, "Normalised objective function", "(norm_objf)", norm_objf ) diff --git a/process/io/plot_proc.py b/process/io/plot_proc.py index 7f9420ebb..ee33c92f9 100755 --- a/process/io/plot_proc.py +++ b/process/io/plot_proc.py @@ -462,13 +462,13 @@ def color_key(axis, mfile_data, scan, colour_scheme): [0.7, -0.3], 1, 0.4, lw=0, facecolor=CRYOSTAT_COLOUR[colour_scheme - 1] ) ) - - axis.text(-5, 1, "Cryostat", ha="left", va="top", size="medium") - axis.add_patch( - patches.Rectangle( - [0.7, 0.7], 1, 0.1, lw=0, facecolor=CRYOSTAT_COLOUR[colour_scheme - 1] + else: + axis.text(-5, 1, "Cryostat", ha="left", va="top", size="medium") + axis.add_patch( + patches.Rectangle( + [0.7, 0.7], 1, 0.1, lw=0, facecolor=CRYOSTAT_COLOUR[colour_scheme - 1] + ) ) - ) def toroidal_cross_section(axis, mfile_data, scan, demo_ranges, colour_scheme): @@ -883,44 +883,6 @@ def read_imprad_data(skiprows, data_path): return impdata -def synchrotron_rad(): - """Function for Synchrotron radiation power calculation from Albajar, Nuclear Fusion 41 (2001) 665 - Fidone, Giruzzi, Granata, Nuclear Fusion 41 (2001) 1755 - - Arguments: - """ - # tbet is betaT in Albajar, not to be confused with plasma beta - - tbet = 2.0 - # rpow is the(1-Rsyn) power dependence based on plasma shape - # (see Fidone) - rpow = 0.62 - kap = plasma_volume / (2.0 * 3.1415**2 * rmajor * rminor**2) - - # No account is taken of pedestal profiles here, other than use of - # the correct ne0 and te0... - de2o = 1.0e-20 * ne0 - pao = 6.04e3 * (rminor * de2o) / bt - gfun = 0.93 * (1.0 + 0.85 * np.exp(-0.82 * rmajor / rminor)) - kfun = (alphan + 3.87e0 * alphat + 1.46) ** (-0.79) - kfun = kfun * (1.98 + alphat) ** 1.36 * tbet**2.14 - kfun = kfun * (tbet**1.53 + 1.87 * alphat - 0.16) ** (-1.33) - dum = 1.0 + 0.12 * (te0 / (pao**0.41)) * (1.0 - ssync) ** 0.41 - # Very high T modification, from Fidone - dum = dum ** (-1.51) - - psync = 3.84e-8 * (1.0e0 - ssync) ** rpow * rmajor * rminor**1.38 - psync = psync * kap**0.79 * bt**2.62 * de2o**0.38 - psync = psync * te0 * (16.0 + te0) ** 2.61 * dum * gfun * kfun - - # psyncpv should be per unit volume - # Albajar gives it as total - psyncpv = psync / plasma_volume - print("psyncpv = ", psyncpv * plasma_volume) # matches the out.dat file - - return psyncpv - - def plot_radprofile(prof, mfile_data, scan, impp, demo_ranges) -> float: """Function to plot radiation profile, formula taken from ???. @@ -1001,22 +963,11 @@ def plot_radprofile(prof, mfile_data, scan, impp, demo_ranges) -> float: 1 - min(0.9999, rhopedt) ) - # ncore = neped + (ne0-neped) * (1-rhocore**2/rhopedn**2)**alphan - # nsep = nesep + (neped-nesep) * (1-rhosep)/(1-min(0.9999, rhopedn)) - # ne = np.append(ncore, nsep) - - # The temperatue profile - # tcore = teped + (te0-teped) * (1-(rhocore/rhopedt)**tbeta)**alphat - # tsep = tesep + (teped-tesep)* (1-rhosep)/(1-min(0.9999,rhopedt)) - # te = np.append(tcore,tsep) - # Intailise the radiation profile arrays pimpden = np.zeros([imp_data.shape[0], te.shape[0]]) lz = np.zeros([imp_data.shape[0], te.shape[0]]) prad = np.zeros(te.shape[0]) - # psyncpv = synchrotron_rad() - # Intailise the impurity radiation profile for k in range(te.shape[0]): for i in range(imp_data.shape[0]): @@ -1041,18 +992,6 @@ def plot_radprofile(prof, mfile_data, scan, impp, demo_ranges) -> float: for l in range(imp_data.shape[0]): # noqa: E741 prad[k] = prad[k] + pimpden[l][k] * 2.0e-6 - # benchmark prad again outfile so mod prad - # pbremint = (rho[1:] * pbrem[1:]) @ drho - # pradint = prad[1:] @ drho * 2.0e-5 - # pbremint = pbrem[1:] @ drho * 2.0e-5 - - # print('prad = ',prad) - # print('pbrem = ',pbrem) - # print(1.0e32*lz[12]) - # print('pradpv = ',pradint) - # print('pbremmw = ',pbremint*plasma_volume) - # print('pradmw = ', pradint*plasma_volume, 'MW') # pimp = pline + pbrem - prof.plot(rho, prad, label="Total") prof.plot(rho, pimpden[0] * 2.0e-6, label="H") prof.plot(rho, pimpden[1] * 2.0e-6, label="He") @@ -1438,19 +1377,6 @@ def plot_firstwall(axis, mfile_data, scan, colour_scheme): ) -def angle_check(angle1, angle2): - """Function to perform TF coil angle check""" - if angle1 > 1: - angle1 = 1 - if angle1 < -1: - angle1 = -1 - if angle2 > 1: - angle2 = 1 - if angle2 < -1: - angle2 = -1 - return angle1, angle2 - - def plot_tf_coils(axis, mfile_data, scan, colour_scheme): """Function to plot TF coils @@ -2791,7 +2717,7 @@ def plot_current_drive_info(axis, mfile_data, scan): (flh, r"$\frac{P_{\mathrm{div}}}{P_{\mathrm{LH}}}$", ""), (hstar, "H* (non-rad. corr.)", ""), ] - if "iefrffix" in mfile_data.data.keys(): + if mfile_data.data["iefrffix"].get_scan(scan) != 0: data.insert( 1, ("pinjmwfix", f"{secondary_heating} secondary auxiliary power", "MW") ) diff --git a/process/objectives.py b/process/objectives.py new file mode 100644 index 000000000..e27039cd5 --- /dev/null +++ b/process/objectives.py @@ -0,0 +1,95 @@ +import numpy as np + +from process.fortran import ( + physics_variables, + tfcoil_variables, + pf_power_variables, + current_drive_variables, + cost_variables, + divertor_variables, + times_variables, + heat_transport_variables, +) + + +def objective_function(minmax: int) -> float: + """Calculate the specified objective function + + :param minimax: the ID and sign of the figure of merit to evaluate. + A negative value indicates maximisation. + A positive value indicates minimisation. + * 1: Major radius + * 3: Neutron wall load + * 4: TF coil + PF coil power + * 5: Fusion gain + * 6: Cost of electricity + * 7: Direct/constructed/capital cost + * 8: Aspect ratio + * 9: Divertor heat load + * 10: Toroidal field on axis + * 11: Injected power + * 14: Pulse length + * 15: Plant availability + * 16: Major radius/burn time + * 17: Net electrical output + * 18: NULL, f(x) = 1 + * 19: Major radius/burn time + :type minimax: int + """ + figure_of_merit = abs(minmax) + + # -1 = maximise + # +1 = minimise + objective_sign = np.sign(minmax) + + match figure_of_merit: + case 1: + objective_metric = 0.2 * physics_variables.rmajor + case 3: + objective_metric = physics_variables.wallmw + case 4: + objective_metric = ( + tfcoil_variables.tfcmw + 1e-3 * pf_power_variables.srcktpm + ) / 10.0 + case 5: + objective_metric = physics_variables.fusion_power / ( + current_drive_variables.pinjmw + + current_drive_variables.porbitlossmw + + physics_variables.pohmmw + ) + case 6: + objective_metric = cost_variables.coe / 100.0 + case 7: + objective_metric = ( + cost_variables.cdirt / 1.0e3 + if cost_variables.ireactor == 0 + else cost_variables.concost / 1.0e4 + ) + case 8: + objective_metric = physics_variables.aspect + case 9: + objective_metric = divertor_variables.hldiv + case 10: + objective_metric = physics_variables.bt + case 11: + objective_metric = current_drive_variables.pinjmw + case 14: + objective_metric = times_variables.t_burn / 2.0e4 + case 15: + if cost_variables.iavail != 1: + raise ValueError("minmax=15 requires iavail=1") + objective_metric = cost_variables.cfactr + case 16: + objective_metric = 0.95 * (physics_variables.rmajor / 9.0) - 0.05 * ( + times_variables.t_burn / 7200.0 + ) + case 17: + objective_metric = heat_transport_variables.pnetelmw / 500.0 + case 18: + objective_metric = 1.0 + case 19: + objective_metric = -0.5 * (current_drive_variables.bigq / 20.0) - 0.5 * ( + times_variables.t_burn / 7200.0 + ) + + return objective_sign * objective_metric diff --git a/process/process_script_advanced.py b/process/process_script_advanced.py deleted file mode 100644 index 3512ec856..000000000 --- a/process/process_script_advanced.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python -"""Script to build and run PROCESS simply. - -This compiles, runs and displays output from PROCESS. The aim is to provide a -common entry point to the code in Python, and to automate a standard workflow. -""" -# TODO This requires converting to run the Python-wrapped version of Process -import subprocess -import argparse -from shutil import copy -from pathlib import Path -from difflib import unified_diff -from utilities.process_io_lib import input_validator - -# Set required paths outside the Python package directory -ROOT_DIR = Path(__file__).parent.parent -# The root project directory (for running cmake) -PROCESS_EXE_PATH = ROOT_DIR.joinpath("bin/process.exe") -# The Process executable (for running Process) - - -class Process(object): - """A Process workflow based on command line arguments.""" - - def __init__(self): - """Parse command line arguments and run accordingly.""" - # File paths - self.run_dir = None - self.input = None - self.ref_input = None - - # Run actions - self.parse_args() - if self.args.build: - self.build_process() - self.create_dicts() - - # Find the input file and look for changes to it - self.set_input() - self.set_ref_input() - self.input_file_diff() - - self.validate_input() - self.run_process() - if self.args.util: - # Only create dicts if necessary for utilities - self.create_dicts() - self.run_utils() - - def parse_args(self): - """Parse the command line arguments.""" - parser = argparse.ArgumentParser( - description="Script to automate " "running PROCESS" - ) - - # Optional arguments - parser.add_argument( - "--input", - "-i", - metavar="input_file_path", - help="The path to the input file that Process runs on", - ) - parser.add_argument( - "--ref_input", - "-r", - metavar="reference_input", - help="Reference input file to record changes against", - ) - parser.add_argument( - "--build", "-b", action="store_true", help="Rebuilds Process if present" - ) - parser.add_argument( - "--util", - "-u", - metavar="util_name", - help="Utility to run after Process runs", - ) - - self.args = parser.parse_args() - # Store namespace object of the args - - def build_process(self): - """Build Process""" - # cmake must be run from the project root dir - subprocess.run(["cmake", "-H.", "-Bbuild"], cwd=ROOT_DIR) - subprocess.run(["cmake", "--build", "build"], cwd=ROOT_DIR) - - def set_input(self): - """Try to find or validate the input file path, then store it. - - Also set the run directory according to the input file path. - """ - if self.args.input: - # Input file path specified as CLI argument - input_path = Path(self.args.input) - if input_path.exists(): - self.input = input_path - # Set input as Path object - else: - raise FileNotFoundError("Input file not found on this path.") - else: - # No input file specified; try to find one in the current dir - input_files = [ - path - for path in Path.cwd().iterdir() - if "IN.DAT" in path.name and "REF_IN.DAT" not in path.name - ] - - if len(input_files) == 0: - # No input file found - raise FileNotFoundError("Input file not found in this dir.") - elif len(input_files) == 1: - # Only one input file in this dir; use it - self.input = input_files[0] - else: - # More than one input file; ambiguous which one to use - raise Exception( - "More than one IN.DAT found in this dir. " - 'Specifiy which one to use with the "-i" option, or ' - "remove the other ones." - ) - - # self.input is now defined - self.run_dir = self.input.parent - # Set run directory as dir that contains the input file - - def set_ref_input(self): - """Find an input file to use as a reference for changes. - - Find or create a reference input file to compare with the current - input file, to allow the changes to be tracked. - """ - # self.args.ref_input: reference input file path; can already be set as - # command line arg - if self.args.ref_input: - # Ref input specified as command line arg. Check it exists - path = Path(self.args.ref_input) - if path.exists(): - # Store path object - self.ref_input = path - else: - raise FileNotFoundError( - "Reference input file not found; " "check the path." - ) - - # Input file exists - if "REF_IN.DAT" in self.ref_input.name: - # Ref file specified; got a ref file - pass - elif "IN.DAT" in self.ref_input.name: - # Regular input file specified. Make a reference copy as - # REF_IN.DAT and update self.ref_input - self.create_ref_input() - else: - # File isn't an IN.DAT or a REF_IN.DAT - raise ValueError( - "Reference input file should end in IN.DAT or " "REF_IN.DAT" - ) - else: - # Ref input not specified: try to find one in the input file's dir - ref_inputs = list(self.run_dir.glob("*REF_IN.DAT")) - if len(ref_inputs) == 0: - # No ref input files: create one from input file - self.create_ref_input() - elif len(ref_inputs) == 1: - # One pre-existing ref input file: set as ref input - self.ref_input = ref_inputs[0] - else: - # More than one ref file: error - raise Exception( - "More than one REF_IN.DAT found in this dir. " - 'Specifiy which one to use with the "-r" option, or ' - "remove the other ones." - ) - - # Now either have raised an exception or have a ref input file set, - # with suffix "REF_IN.DAT" - - def create_ref_input(self): - """Create a reference input file from a pre-existing input file.""" - # First, ensure that self.ref_input is set as an IN.DAT to copy - if not self.ref_input: - # No IN.DAT specified to use as ref: use input file instead - self.ref_input = self.input - - # Copy the ref input and save it with the REF_IN.DAT extension - # Then set this as the new ref input - new_ref_input_name = self.ref_input.name.replace("IN.DAT", "REF_IN.DAT") - new_ref_input = Path(self.run_dir).joinpath(new_ref_input_name) - copy(self.ref_input, new_ref_input) - self.ref_input = new_ref_input - - def input_file_diff(self): - """Perform a diff between the reference input and input files. - - This compares the REF_IN.DAT and IN.DAT files to highlight the - modifications. - """ - # Run a diff on the files - before_file = open(self.ref_input) - after_file = open(self.input) - before = before_file.readlines() - after = after_file.readlines() - before_file.close() - after_file.close() - diff = unified_diff( - before, after, fromfile=self.ref_input.name, tofile=self.input.name - ) - - # Write diff to output file - diff_path = self.run_dir.joinpath("input.diff") - with open(diff_path, "w") as diff_file: - diff_file.writelines(diff) - - def create_dicts(self): - """Create Python dictionaries""" - # cmake must be run from the project root directory - subprocess.run(["cmake", "--build", "build", "--target", "dicts"], cwd=ROOT_DIR) - - def validate_input(self): - """Validate the input file using the input_validator module.""" - input_validator.validate(self.input) - - def run_process(self): - """Run Process using the input file path.""" - subprocess.run([PROCESS_EXE_PATH, self.input]) - - def run_utils(self): - """Run a utility if specified on the command line.""" - # TODO allow multiple utils to run - # TODO allow options to be passed to utils - subprocess.run(["python", "./utilities/" + self.args.util + ".py"]) - - -def main(): - """Run Process.""" - print("Running process.py") - Process() - - -if __name__ == "__main__": - main() diff --git a/source/fortran/evaluators.f90 b/source/fortran/evaluators.f90 deleted file mode 100644 index 7a79c03de..000000000 --- a/source/fortran/evaluators.f90 +++ /dev/null @@ -1,214 +0,0 @@ -! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -module function_evaluator - - !! Module containing function evaluators for HYBRD and VMCON - !! solvers - !! author: P J Knight, CCFE, Culham Science Centre - !! N/A - !! This module contains the function evaluators required - !! by the two equation solvers in the code. - - !! None - ! - ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -#ifndef dp - use, intrinsic :: iso_fortran_env, only: dp=>real64 -#endif - implicit none - - public - -contains - - ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -! fcnhyb() is commented out temporarily. It calls the caller() subroutine, which -! is being moved to Python. fcnhyb() is passed into maths_library.hybrd() as an -! external subroutine argument, which calls fcnhyb() within a goto block. This -! is difficult to unravel when converting to Python (so that caller() is called -! from Python), so it has been decided to comment out hybrd() and fcnhyb() -! temporarily, disabling the non-optimising solver, to allow Python conversion -! work using the optimising solver (vmcon()) to continue. - -! subroutine fcnhyb(n,xc,rc,iflag) - -! !! Function evaluator for EQSOLV -! !! author: P J Knight, CCFE, Culham Science Centre -! !! n : input integer : Number of equations and unknowns -! !! xc(n) : input/output real array : On input XC must contain -! !! an initial estimate of the solution vector. On output XC -! !! contains the final estimate of the solution vector. -! !! rc(n) : output real array : Functions evaluated at the output XC -! !! iflag : input/output integer : Terminate execution of EQSOLV -! !! by setting IFLAG to a negative integer. -! !! This subroutine is the function evaluator for -! !! EQSOLV (q.v.). -! !! None -! ! -! ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -! use constraints, only: constraint_eqns -! use numerics, only: neqns -! use caller_module, only: caller -! implicit none - -! ! Arguments - -! integer, intent(in) :: n -! real(dp), dimension(n), intent(inout) :: xc -! real(dp), dimension(n), intent(out) :: rc -! integer, intent(inout) :: iflag - -! ! Local variables - -! integer :: ncon, nvars - -! ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -! nvars = neqns -! ncon = neqns - -! call caller(xc,nvars) -! call constraint_eqns(ncon,rc,-1) - -! ! Set iflag < 0 if program is to be terminated here. - -! iflag = 1 * iflag - -! end subroutine fcnhyb - - subroutine funfom(fc) - - !! Objective function evaluator for VMCON - !! author: P J Knight, CCFE, Culham Science Centre - !! fc : output real : value of objective function at the output point - !! This routine evaluates the value of the objective function - !! i.e. the (normalised) figure-of-merit, at the nvar-dimensional - !! point of interest. - !!

Each equation for fc gives a value of the - !! order of unity for the sake of the numerics. - !! - ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - use global_variables, only: xlabel, iscan_global - use constants, only: nout, iotty, mfile - use constraints, only: constraint_eqns - use cost_variables, only: concost, cfactr, cdirt, ireactor, iavail, coe - use current_drive_variables, only: bigq, porbitlossmw, pinjmw - use divertor_variables, only: hldiv - use error_handling, only: idiags, fdiags, errors_on, report_error - use heat_transport_variables, only: pnetelmw - use numerics, only: minmax - use physics_variables, only: fusion_power, bt, rmajor, wallmw, aspect, pohmmw - use pf_power_variables, only: srcktpm - use process_output, only: int_to_string3 - use tfcoil_variables, only: tfcmw - use times_variables, only: t_burn - implicit none - - ! Arguments - - real(dp), intent(out) :: fc -! real(c_double), intent(out) :: fc - ! Local variables - - integer :: iab - real(dp) :: sgn - -! write(*,*) 'Figure of merit 2 (fusion power / input power) is not used.' - ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - iab = abs(minmax) - sgn = sign(1.0D0, real(minmax, kind(1.0D0))) - - ! If sgn is -1 the value of fc will be maximised - ! If sgn is +1 the value of fc will be minimised - - select case (iab) - - case (1) ! major radius - fc = sgn * 0.2D0 * rmajor - - case (2) ! fusion power / input power - write(*,*) 'Figure of merit 2 (fusion power / input power) is not used.' - write(*,*) 'Figure of merit 5 (fusion gain Q) is available.' - stop 1 - ! fc = sgn * fusion_power / (pinjmw + porbitlossmw + tfcpmw + ppump/1.0D6) - - case (3) ! neutron wall load - fc = sgn * wallmw - - case (4) ! TF coil + PF coil power - fc = sgn * (tfcmw + 1.0D-3*srcktpm)/10.0D0 - - case (5) ! Q = fusion gain Issue #540 - fc = sgn * fusion_power / (pinjmw + porbitlossmw + pohmmw) - !fc = sgn * fusion_power / pinjmw - - case (6) ! cost of electricity - fc = sgn * coe/100.0D0 - - case (7) ! direct/constructed/capital cost - if (ireactor == 0) then - fc = sgn * cdirt/1.0D3 - else - fc = sgn * concost/1.0D4 - end if - - case (8) ! aspect ratio - fc = sgn * aspect - - case (9) ! divertor heat load - fc = sgn * hldiv - - case (10) ! toroidal field on axis - fc = sgn * bt - - case (11) ! injection power - fc = sgn * pinjmw - - case (12) ! hydrogen production capital cost - ! #506 OBSOLETE - write(*,*) 'Figure of Merit 13 (Hydrogen production) is no longer supported.' - stop 1 - case (13) ! hydrogen production rate - ! #506 OBSOLETE - write(*,*) 'Figure of Merit 13 (Hydrogen production) is no longer supported.' - stop 1 - - case (14) ! pulse length - fc = sgn * t_burn / 2.0D4 - - case (15) ! plant availability factor (N.B. requires iavail = 1) - - if (iavail /= 1) call report_error(23) - - fc = sgn * cfactr - - case (16) ! major radius/burn time - fc = sgn * ( 0.95d0 * (rmajor/9.0d0) - 0.05d0 * (t_burn/7200.d0) ) - - case (17) ! net electrical output - fc = sgn * pnetelmw / 500.0d0 - - case (18) ! Null figure of merit - fc = 1d0 - - case (19) ! major radius/burn time - fc = sgn * ( -0.5d0 * (bigq/20.0D0) - 0.5d0 * (t_burn/7200.d0) ) - - case default - idiags(1) = iab ; call report_error(24) - - end select - - ! Crude method of catching NaN errors - - if ((abs(fc) > 9.99D99).or.(fc /= fc)) then - idiags(1) = iab ; call report_error(25) - end if - - end subroutine funfom - -end module function_evaluator diff --git a/source/fortran/scan.f90 b/source/fortran/scan.f90 index ced511e14..397b168e1 100644 --- a/source/fortran/scan.f90 +++ b/source/fortran/scan.f90 @@ -869,7 +869,6 @@ subroutine post_optimise(ifail) !! use constraints use error_handling - use function_evaluator use numerics use process_output use utilities, only:upper_case diff --git a/tests/integration/data/test_solver.conf b/tests/integration/data/test_solver.conf deleted file mode 100644 index a3acd009c..000000000 --- a/tests/integration/data/test_solver.conf +++ /dev/null @@ -1,42 +0,0 @@ -* CONFIG FILE FOR SOLVER TEST -* - -* No iterations -NITER = 1 - -* sets flag of same name in IN.DAT: -* None - keeps original value -* -1 - Non-optimisation only -* 0 - one non-optimisation step followed by optimised iteration -* 1 - optimisation only -* 2 - NAG solver -IOPTIMZ = 1 - -* sets the error tolerance for VMCON (default = 1.0D-3) -* None - keeps the original value (IN.DAT or default) -EPSVMC = None - -* sets the step length for HYBRD (default = 1.0D-3) -* None - keeps the original value (IN.DAT or default) -EPSFCN = None - -* sets the feasibility tolerance of the NAG solver -* None - keeps the original value (IN.DAT or default) -FEASTOL = None - -*sets the function precision of the NAG solver -* None - keeps the original value (IN.DAT or default) -FUNCPREC = None - -*sets the figure of merit -*positive value - minimise, negative value - maximise -* None - keeps the original value -* 1 - plasma major radius -* 6 - cost of electricity -MINMAX = None - -* integer seed for random number generator; use None for random seed -SEED = 2 - -* factor within which the iteration variables are changed -FACTOR = 1.1 diff --git a/tests/integration/test_solver.py b/tests/integration/test_solver.py deleted file mode 100755 index a2ccf0de4..000000000 --- a/tests/integration/test_solver.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Tests for the Process solver.""" - -import time -from numpy import histogram -from process.io.mfile import MFile -from process.io.python_fortran_dicts import get_dicts -from process.io.process_config import TestProcessConfig -from process.io.process_funcs import ( - get_neqns_itervars, - update_ixc_bounds, - get_variable_range, - vary_iteration_variables, - process_stopped, - get_solution_from_mfile, - process_warnings, -) -from process import fortran - -# Required to prevent pytest collecting this as a test -TestProcessConfig.__test__ = False - -# Load dicts from dicts JSON file -process_dicts = get_dicts() -IFAIL_SUCCESS = process_dicts["IFAIL_SUCCESS"] - - -# TODO: I'm not sure how useful this test is anymore? TN -def test_solver(temp_data): - """Code to test PROCESS Solver by choosing different starting values - for the iteration parameters around the initial values in the INPUT file - Code modified by Sarah Medley in April 2015 to also calculate Q (fusion power/ - injected power) and output this to SolverTest.out - - Author: H. Lux (Hanni.Lux@ccfe.ac.uk) - - Date: November 2013 (Initial version) - March 2014 (Initial release version) - April 2015 - Code modified by Sarah Medley to also calculate Q - see above - - - Input files - - test_solver.conf: in integration/data dir - - - Output files - - Saved to temporary test dir - OUT.DAT - PROCESS output of last run - MFILE.DAT - PROCESS output of last run - process.log - logfile of PROCESS output to stdout - time.info - stores the run time of the inner loop - SolverTest.out - - - Ifail values: - -1: Error in logfile, no solution vector in OUT.DAT - 0: Error in VMCON: improper input parameters - 1: normal run - 2: too many function calls in VMCON - 3/4: either constraint/objective function + gradients - are not calculated well or data too noisy - 5: either no feasible solution to the problem, - or identity matrix is a bad approximation ot the Hessian - 6: restriction by an artificial bound or singular matrix - in quadratic problem. - - :param temp_data: temp test dir containing integration test data - :type temp_data: Path - """ - # Path to config file and varied input file - conf_path = temp_data / "test_solver.conf" - input_path = temp_data / "large_tokamak_IN.DAT" - - CONFIG = TestProcessConfig(conf_path) - CONFIG.setup() - - NEQNS, ITERVARS = get_neqns_itervars() - - update_ixc_bounds() - - fortran.init_module.init_all_module_vars() - fortran.init_module.init() - - LBS, UBS = get_variable_range(ITERVARS, CONFIG.factor) - - TABLE_ERR = [] - TABLE_OBJ = [] - TABLE_CON = [] - TABLE_IN = [] - TABLE_OUT = [] - TABLE_POWF = [] - TABLE_PINJ = [] - TABLE_RATIO_POWF_PINJ = [] - - WARNING_CNT = 0 - - START_TIME = time.time() - - for i in range(CONFIG.niter): - # modify IN.DAT each time - input_values = vary_iteration_variables(ITERVARS, LBS, UBS) - - TABLE_IN += [input_values] - - print(i, end=" ") - - # Actually perform the Process run for this varied input file - CONFIG.run_process(input_path) - - if process_stopped(): - ifail, objective_function, constraints, table_sol, table_res = ( - -1, - "0", - "0", - ["0"] * len(ITERVARS), - ["0"] * NEQNS, - ) - fusion_power = "0" - injected_power = "0" - ratio_fus_inj_power = 0 - else: - ( - ifail, - objective_function, - constraints, - table_sol, - table_res, - ) = get_solution_from_mfile(NEQNS, len(ITERVARS)) - - # Access MFILE.DAT - m_file = MFile(filename=str(temp_data / "large_tokamak_MFILE.DAT")) - ind = -1 # last scan point - - # Obtain fusion power and injected power from MFILE.DAT - fusion_power = m_file.data["fusion_power"].get_scan(ind) - injected_power = m_file.data["pinjmw"].get_scan(ind) - ratio_fus_inj_power = fusion_power / injected_power - - if ifail == IFAIL_SUCCESS and process_warnings(): - WARNING_CNT += 1 - - TABLE_OUT += [table_sol + table_res] - TABLE_ERR += [ifail] - TABLE_OBJ += [objective_function] - TABLE_CON += [constraints] - TABLE_POWF += [fusion_power] - TABLE_PINJ += [injected_power] - TABLE_RATIO_POWF_PINJ += [ratio_fus_inj_power] - - i += 1 - - END_TIME = time.time() - - ########## - TIMEFILE = open("time.info", "w") - TIMEFILE.write("wall clock time of run %f s" % (END_TIME - START_TIME)) - TIMEFILE.close() - - ######### - print("\n Error codes:") - ERRHIST, ERRBINS = histogram( - TABLE_ERR, bins=[-1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5] - ) - - for i in range(len(ERRHIST)): - print( - "code %2i: %4i out of %4i = %3.1f " - % ( - int((ERRBINS[i + 1] + ERRBINS[i]) / 2.0), - ERRHIST[i], - CONFIG.niter, - ERRHIST[i] * 100.0 / CONFIG.niter, - ) - + "%" - ) - - if WARNING_CNT > 0: - # TODO: calculate fraction of warnings in sucessful runs. - print("\nIn %f%% of the successful runs warnings occurred." % WARNING_CNT) - - ########## - OUTFILE = open("SolverTest.out", "w") - - OUTHEADER = "#Err\t ObjectiveF\t Constraints\t" - for inp_str in ITERVARS: # input variables - OUTHEADER += "%s_in\t" % inp_str - for var_no in range(len(ITERVARS)): # output variables - OUTHEADER += "itvar%03i\t" % (var_no + 1) - for con_no in range(NEQNS): # constraints - OUTHEADER += "constr%03i\t" % (con_no + 1) - # OUTHEADER += 'Fusion_Power_(MW)\t' - # OUTHEADER += 'Injected_Power_(MW)\t' - OUTHEADER += "Ratio_fus_inj_power_(MW)" - OUTHEADER += "\n" - OUTFILE.write(OUTHEADER) - - for i in range(CONFIG.niter): - outstr = "%d\t %s\t %s\t" % (TABLE_ERR[i], TABLE_OBJ[i], TABLE_CON[i]) - for valuein in TABLE_IN[i]: - outstr += "%s\t" % valuein - for valueout in TABLE_OUT[i]: - outstr += "%s\t" % valueout - # outstr += '%d\t' % TABLE_POWF[i] - # outstr += '%d\t' % TABLE_PINJ[i] - outstr += "%.1f\t" % TABLE_RATIO_POWF_PINJ[i] - outstr += "\n" - OUTFILE.write(outstr) - - OUTFILE.close() - - # TODO This might need to assert something, like ifail == 1?