-
Notifications
You must be signed in to change notification settings - Fork 25
Impurity radiation corrections #4536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ab9c152
89a325f
cbecb1d
8f8926b
8282bff
a9b3913
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,13 +49,15 @@ class PROCESSRunMode(IntEnum): | |
| """In this mode, the code will not perform any optimisation, and will instead | ||
| simply evaluate the constraints for the given input parameters, which is useful | ||
| for testing and for evaluating the performance of a given design point without | ||
| trying to optimise it. Internally, PROCESS uses `fsolve` (a Newton-Krylov/hybrd | ||
| trying to optimise it. | ||
| """ | ||
| SOLUTION = (-1, "Solution mode (no optimisation)") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we accidentaly remove solution mode when setting up the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solution mode was removed a long time ago, it was replaced by evaluation mode. This is because solution mode did not produce consistent solutions and so was essentially useless (except for testing, see #4044)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On my fork I have optimisation, solution (solve equalities) and evaluation (just run models until idempotent). I think it's worth adding them back in, but this is for another PR. |
||
| """Internally, PROCESS uses `fsolve` (a Newton-Krylov/hybrd | ||
| root-finding method from `scipy.optimize`) to seek a *consistent* solution by | ||
| varying a subset of the iteration variables until the consistency constraints | ||
| (equality constraints whose residuals must be driven to zero) are simultaneously | ||
| satisfied; no figure-of-merit is optimised, and the solver simply tries to find | ||
| a root of the constraint-residual vector. | ||
| """ | ||
| a root of the constraint-residual vector.""" | ||
| OPTIMISATION = (1, "Optimisation mode (e.g. via VMCON)") | ||
| """In this mode, the code will perform optimisation using the VMCON solver | ||
| (or a custom solver if specified) to try to find a design point that optimises | ||
|
|
@@ -182,6 +184,14 @@ class NumericsData: | |
| nvar: int = 0 | ||
| """number of iteration variables to use""" | ||
|
|
||
| # Constraint residuals, updated on every iteration | ||
| constraint_residuals_normalised: list[float] = field( | ||
| default_factory=lambda: np.array([0] * IPEQNS) | ||
| ) | ||
| constraint_residuals: list[float] = field( | ||
| default_factory=lambda: np.array([0] * IPEQNS) | ||
| ) | ||
|
|
||
| nviter: int = 0 | ||
| """number of optimisation iterations performed""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1078,6 +1078,9 @@ class PhysicsData: | |||||
| pden_plasma_core_rad_mw: float = 0.0 | ||||||
| """total core radiation power per volume (MW/m3)""" | ||||||
|
|
||||||
| pden_plasma_core_rad_tauE_mw: float = 0.0 | ||||||
| """reduced total core radiation power for tauE calculation (MW/m3)""" | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to do in the actual PR for this. |
||||||
|
|
||||||
| p_dd_total_mw: float = 0.0 | ||||||
| """deuterium-deuterium fusion power (MW)""" | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,6 +119,8 @@ | |
| ) | ||
| from process.models.vacuum import Vacuum, VacuumVessel | ||
| from process.models.water_use import WaterUse | ||
| from process.core.caller import write_output_files | ||
| from process.core.solver.iteration_variables import load_iteration_variables | ||
|
|
||
| PACKAGE_LOGGING = True | ||
| """Can be set False to disable package-level logging, e.g. in the test suite""" | ||
|
|
@@ -449,10 +451,19 @@ def run_scan(self): | |
| # ioptimz == 1: optimisation | ||
| if self.data.numerics.ioptimz == PROCESSRunMode.OPTIMISATION: | ||
| pass | ||
| elif self.data.numerics.ioptimz == PROCESSRunMode.EVALUATION: | ||
| # No optimisation: | ||
| # solve equality (consistency) constraints only using fsolve (HYBRD) | ||
| # ioptimz == -1: solution | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again will leave this one for @timothy-nunn
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My mistake: irrelevant for this PR. |
||
| elif self.data.numerics.ioptimz == PROCESSRunMode.SOLUTION: | ||
| # Solve equality (consistency) constraints only using fsolve (HYBRD) | ||
| self.solver = "fsolve" | ||
| # ioptimz == -2: evaluation | ||
| elif self.data.numerics.ioptimz == PROCESSRunMode.EVALUATION: | ||
| # Evalutation only: compute the output variables now | ||
| # Get optimisation parameters x, evaluate models | ||
| load_iteration_variables(self.data) | ||
| self.ifail = 6 | ||
| write_output_files(data=self.data, models=self.models, ifail=self.ifail) | ||
| self.show_errors() | ||
| return | ||
| else: | ||
| raise ValueError( | ||
| f"Invalid ioptimz value: {self.data.numerics.ioptimz}. Please " | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2854,7 +2854,8 @@ def coelc(self): | |
|
|
||
| # Capital recovery factor | ||
|
|
||
| crfcdr = (fefcdr * self.data.costs.discount_rate) / (fefcdr - 1.0e0) | ||
| # crfcdr = (fefcdr * self.data.costs.discount_rate) / (fefcdr - 1.0e0) | ||
| crfcdr = 1.0 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why this is now just set to 1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This calculation would often cause div by 0 errors. |
||
|
|
||
| # Annual cost of replacements | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| PlasmaIgnitionModel, | ||
| ) | ||
| from process.models.physics.plasma_geometry import PlasmaGeom | ||
| from process.models.physics import impurity_radiation | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -166,8 +167,16 @@ def calculate_confinement_time( | |
| try: | ||
| model = ConfinementRadiationLossModel(int(self.data.physics.i_rad_loss)) | ||
|
|
||
| # pden_plasma_core_rad_mw was reduced here, but if full radiation, not used! | ||
| # rad_reduction_for_tauE_only option allows full radiation model in PPB, | ||
| # but reduced radiation here in confinement time calculation | ||
| if model == ConfinementRadiationLossModel.FULL_RADIATION: | ||
| p_plasma_loss_mw -= self.data.physics.pden_plasma_rad_mw * vol_plasma | ||
| if impurity_radiation.rad_reduction_for_tauE_only: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this not be in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My argument here is that we should always use the full radiation in the plasma power balance, and the optional reduction only in the tau E calculation. You are right to point this out, but this is only done here to show the cumulative effect of these various changes when plotting. |
||
| # Reduced rad | ||
| p_plasma_loss_mw -= pden_plasma_core_rad_mw * vol_plasma | ||
| else: | ||
| # Not reduced rad | ||
| p_plasma_loss_mw -= self.data.physics.pden_plasma_rad_mw * vol_plasma | ||
| elif model == ConfinementRadiationLossModel.CORE_ONLY: | ||
| p_plasma_loss_mw -= pden_plasma_core_rad_mw * vol_plasma | ||
| # NO_RADIATION: do not adjust p_plasma_loss_mw for radiation | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,10 @@ | |
| from process.models.physics.plasma_profiles import PlasmaProfile | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
| include_edge_radiation = False | ||
| int_edge_rad = False | ||
| rho_fix = False | ||
| rad_reduction_for_tauE_only = False | ||
|
|
||
|
|
||
| def initialise_imprad(data: DataStructure): | ||
|
|
@@ -629,6 +633,12 @@ def element2index(element: str, data: DataStructure): | |
| ) from e | ||
|
|
||
|
|
||
| # Globals for ease of extraction for investigation plotting only | ||
| global pden_impurity_rad_profile | ||
| global pden_impurity_core_rad_profile | ||
| global pden_impurity_rad_edge_profile | ||
|
|
||
|
|
||
| class ImpurityRadiation: | ||
| """Calculates the impurity radiation losses for given temperature and | ||
| density profiles. The considers the total impurity radiation from the core | ||
|
|
@@ -660,13 +670,17 @@ def __init__(self, plasma_profile: PlasmaProfile, data_structure: DataStructure) | |
| self.pden_impurity_core_rad_profile = np.zeros( | ||
| self.data.physics.n_plasma_profile_elements | ||
| ) | ||
| self.pden_impurity_core_rad_profile_tauE = np.zeros( | ||
| self.data.physics.n_plasma_profile_elements | ||
| ) | ||
| self.pden_impurity_rad_edge_profile = np.zeros( | ||
| self.data.physics.n_plasma_profile_elements | ||
| ) | ||
|
|
||
| self.pden_impurity_rad_total_mw = 0.0 | ||
| self.pden_impurity_core_rad_total_mw = 0.0 | ||
| self.pden_impurity_rad_edge_total_mw = 0.0 | ||
| self.pden_impurity_core_rad_total_tauE_mw = 0.0 | ||
|
|
||
| def run(self): | ||
| """ImpurityRadiation model isn't run""" | ||
|
|
@@ -705,25 +719,94 @@ def calculate_radiation_loss_profiles(self): | |
| radiation (pden_impurity_rad_total_mw). Update the stored arrays with the | ||
| values. | ||
| """ | ||
| pden_impurity_rad_total = ( | ||
| self.pden_impurity_radiation_profile | ||
| * self.plasma_profile.neprofile.profile_x | ||
| # Core region radiation profile | ||
| # Multiplication by "profile_x" (formerly rho, normalised minor radius) | ||
| # wrong here: causes 0 power density at rho = 0. Should be performed in | ||
| # power integral instead | ||
| if rho_fix: | ||
| rho = np.ones_like(self.plasma_profile.neprofile.profile_x) | ||
| else: | ||
| rho = self.plasma_profile.neprofile.profile_x | ||
|
|
||
| # Optionally treat core radiation reduction separately for tauE calculation and | ||
| # plasma power balance | ||
| f_p_plasma_core_rad_reduction_tauE = ( | ||
| self.data.impurity_radiation.f_p_plasma_core_rad_reduction | ||
| ) | ||
| if rad_reduction_for_tauE_only: | ||
| # Only reduce radiation for tauE calculation | ||
| f_p_plasma_core_rad_reduction = 1.0 | ||
| else: | ||
| # Reduce radiation in PPB as well | ||
| f_p_plasma_core_rad_reduction = f_p_plasma_core_rad_reduction_tauE | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume this will be removed once we put the PR up for the PPB fix?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. |
||
|
|
||
| pden_impurity_core_rad_total = self.pden_impurity_radiation_profile * ( | ||
| self.plasma_profile.neprofile.profile_x | ||
| rho | ||
| * create_f_rad_core_profile( | ||
| rho=self.plasma_profile.neprofile.profile_x, | ||
| radius_plasma_core_norm=self.data.impurity_radiation.radius_plasma_core_norm, | ||
| f_p_plasma_core_rad_reduction=self.data.impurity_radiation.f_p_plasma_core_rad_reduction, | ||
| f_p_plasma_core_rad_reduction=f_p_plasma_core_rad_reduction, | ||
| ) | ||
| ) | ||
| pden_impurity_core_rad_total_tauE = self.pden_impurity_radiation_profile * ( | ||
| rho | ||
| * create_f_rad_core_profile( | ||
| rho=self.plasma_profile.neprofile.profile_x, | ||
| radius_plasma_core_norm=self.data.impurity_radiation.radius_plasma_core_norm, | ||
| f_p_plasma_core_rad_reduction=f_p_plasma_core_rad_reduction_tauE, | ||
| ) | ||
| ) | ||
|
|
||
| if include_edge_radiation: | ||
| # Explicitly include edge radiation | ||
| # Edge region radiation profile | ||
| fradedge_profile = np.zeros_like(self.plasma_profile.neprofile.profile_x) | ||
| edge_mask = ( | ||
| self.plasma_profile.neprofile.profile_x | ||
| >= self.data.impurity_radiation.radius_plasma_core_norm | ||
| ) | ||
| fradedge_profile[edge_mask] = 1.0 # Edge region gets full value | ||
| pden_impurity_rad_edge_total = ( | ||
| self.pden_impurity_radiation_profile * fradedge_profile | ||
| ) | ||
|
|
||
| # Total radiation profile (core + edge) | ||
| pden_impurity_rad_total = ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you multiply the core and edge
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is shown in the first plot. |
||
| pden_impurity_core_rad_total + pden_impurity_rad_edge_total | ||
| ) | ||
|
|
||
| self.pden_impurity_rad_edge_profile = np.add( | ||
| self.pden_impurity_rad_edge_profile, pden_impurity_rad_edge_total | ||
| ) | ||
| else: | ||
| # Old case: don't explicitly calculate edge radiation density | ||
| if rho_fix: | ||
| pden_impurity_rad_total = self.pden_impurity_radiation_profile | ||
|
|
||
| else: | ||
| pden_impurity_rad_total = ( | ||
| self.pden_impurity_radiation_profile | ||
| * self.plasma_profile.neprofile.profile_x | ||
| ) | ||
|
|
||
| self.pden_impurity_rad_profile = np.add( | ||
| self.pden_impurity_rad_profile, pden_impurity_rad_total | ||
| ) | ||
| self.pden_impurity_core_rad_profile = np.add( | ||
| self.pden_impurity_core_rad_profile, pden_impurity_core_rad_total | ||
| ) | ||
| self.pden_impurity_core_rad_profile_tauE = np.add( | ||
| self.pden_impurity_core_rad_profile_tauE, pden_impurity_core_rad_total_tauE | ||
| ) | ||
| global pden_impurity_rad_profile | ||
| global pden_impurity_core_rad_profile | ||
| global pden_impurity_rad_edge_profile | ||
| global pden_impurity_core_rad_profile_tauE | ||
|
|
||
| pden_impurity_rad_profile = self.pden_impurity_rad_profile | ||
| pden_impurity_core_rad_profile = self.pden_impurity_core_rad_profile | ||
| pden_impurity_rad_edge_profile = self.pden_impurity_rad_edge_profile | ||
| pden_impurity_core_rad_profile_tauE = self.pden_impurity_core_rad_profile_tauE | ||
|
|
||
| def integrate_radiation_loss_profiles(self): | ||
| """Integrate the radiation loss profiles using the Simpson rule. | ||
|
|
@@ -734,16 +817,37 @@ def integrate_radiation_loss_profiles(self): | |
| # but are correct: | ||
| # see github.com/ukaea/PROCESS/issues/3968#issuecomment-3491154712 | ||
| # and github.com/ukaea/PROCESS/issues/3968#issuecomment-4935567006 | ||
|
|
||
| # Old case: rho multiplication already performed incorrectly in power | ||
| # density calculation: don't multiply again here | ||
| # New case (rho_fix): multiply correct power density here by rho | ||
| # for integration | ||
| rho = 1.0 | ||
| if rho_fix: | ||
| rho = self.plasma_profile.neprofile.profile_x | ||
| self.pden_impurity_rad_total_mw = 2.0e-6 * integrate.simpson( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it is better here to rename these variables to have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. |
||
| self.pden_impurity_rad_profile, | ||
| self.pden_impurity_rad_profile * rho, | ||
| x=self.plasma_profile.neprofile.profile_x, | ||
| dx=self.plasma_profile.neprofile.profile_dx, | ||
| ) | ||
| self.pden_impurity_core_rad_total_mw = 2.0e-6 * integrate.simpson( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the core value I think we need to be explicit in stating that this is till done as a full plasma volume integral with the mask of 0's where the core would be. This has always caused confusion as to if the core or edge value is only done as a function of its own volume or that of the total plasma
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, good point. |
||
| self.pden_impurity_core_rad_profile, | ||
| self.pden_impurity_core_rad_profile * rho, | ||
| x=self.plasma_profile.neprofile.profile_x, | ||
| dx=self.plasma_profile.neprofile.profile_dx, | ||
| ) | ||
| self.pden_impurity_core_rad_total_tauE_mw = 2.0e-6 * integrate.simpson( | ||
| self.pden_impurity_core_rad_profile_tauE * rho, | ||
| x=self.plasma_profile.neprofile.profile_x, | ||
| dx=self.plasma_profile.neprofile.profile_dx, | ||
| ) | ||
|
|
||
| if include_edge_radiation: | ||
| # Integrate edge explicitly | ||
| self.pden_impurity_rad_edge_total_mw = 2.0e-6 * integrate.simpson( | ||
| self.pden_impurity_rad_edge_profile * rho, | ||
| x=self.plasma_profile.neprofile.profile_x, | ||
| dx=self.plasma_profile.neprofile.profile_dx, | ||
| ) | ||
|
|
||
| def calculate_imprad(self): | ||
| """Call the map function to calculate impurity radiation parameters for each | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about this change, probs best if someone like @timothy-nunn checks it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR does not include objective function 20 so these changes are unnecessary at this point.
Also,
call_models_and_write_outputreturns the objective value and normalised residuals, but I cannot see them being used anywhere in this PR.I suggest all of these changes in caller can be left for a later PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is an artefact of other changes on my fork and shouldn't have been in this PR. They can be ignored here.