Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions process/core/caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,28 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int):
OutputFileManager.close_idempotence_files(
self.data.globals.output_prefix
)

# Now idempotent, return
# Pass model caller and opt params for stability constraint evaluation

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Collaborator

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_output returns 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.

Copy link
Copy Markdown
Contributor Author

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.

con_residuals_normalised, _, con_residuals, _, _ = (
constraints.constraint_eqns(
self.data.numerics.neqns + self.data.numerics.nineqns,
-1,
self.data,
)
)
# Evaluate constraints and store in numerics during solver iterations:
# can be used in objective function 20. Hence evaluate before objective
# calculated
self.data.numerics.constraint_residuals_normalised = (
con_residuals_normalised
)
self.data.numerics.constraint_residuals = con_residuals
# Evaluate objective function and constraints
objf = objective_function(self.data.numerics.minmax, self.data)
# Write final output file and mfile
finalise(self.models, self.data, ifail)
return
return objf, con_residuals_normalised

# Mfiles not yet idempotent: need to re-evaluate models
logger.debug("Mfiles not idempotent, evaluating models again")
Expand Down Expand Up @@ -419,7 +438,7 @@ def finalise(models, data, ifail: int, non_idempotent_msg: str | None = None):
po.oheadr(constants.NOUT, "Final UNFEASIBLE Point")

# Output relevant to no optimisation
if data.numerics.ioptimz == PROCESSRunMode.EVALUATION:
if data.numerics.ioptimz in {PROCESSRunMode.EVALUATION, PROCESSRunMode.SOLUTION}:
output_evaluation(data)

# Print non-idempotence warning to OUT.DAT only
Expand Down
16 changes: 13 additions & 3 deletions process/data_structure/numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we accidentaly remove solution mode when setting up the enum @jonmaddock @timothy-nunn ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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"""

Expand Down
3 changes: 3 additions & 0 deletions process/data_structure/physics_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""reduced total core radiation power for tauE calculation (MW/m3)"""
"""reduced total core radiation power for τₑ calculation [MW/m³]"""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)"""

Expand Down
17 changes: 14 additions & 3 deletions process/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again will leave this one for @timothy-nunn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 "
Expand Down
3 changes: 2 additions & 1 deletion process/models/costs/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is now just set to 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Expand Down
11 changes: 10 additions & 1 deletion process/models/physics/confinement_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
PlasmaIgnitionModel,
)
from process.models.physics.plasma_geometry import PlasmaGeom
from process.models.physics import impurity_radiation

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be in the ConfinementRadiationLossModel.CORE_ONLY condition below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
118 changes: 111 additions & 7 deletions process/models/physics/impurity_radiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you multiply the core and edge pden by the total plasma volume and then add them together does it equal pden_impurity_rad_total * vol_plasma?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand All @@ -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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is better here to rename these variables to have _vol_avg in them as thats the returned value from the integration when multiplying by rho at each point. These values are then multiplied by vol_plasma to get the total radiation power. This should make it more clear as to what is happening

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
9 changes: 8 additions & 1 deletion process/models/physics/physics.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,9 @@ def run(self):
self.data.physics.pden_plasma_core_rad_mw = radpwrdata.pden_plasma_core_rad_mw
self.data.physics.pden_plasma_outer_rad_mw = radpwrdata.pden_plasma_outer_rad_mw
self.data.physics.pden_plasma_rad_mw = radpwrdata.pden_plasma_rad_mw
self.data.physics.pden_plasma_core_rad_tauE_mw = (
radpwrdata.pden_plasma_core_rad_tauE_mw
)

self.data.physics.p_plasma_sync_mw = (
self.data.physics.pden_plasma_sync_mw * self.data.physics.vol_plasma
Expand Down Expand Up @@ -871,6 +874,10 @@ def run(self):

# Calculate transport losses and energy confinement time using the
# chosen scaling law
# Reduce pden to effectively modify the highly radiative regime confinement
# time scaling
reduced_pden = self.data.physics.pden_plasma_core_rad_tauE_mw

confinement_time_data = self.confinement.calculate_confinement_time(
m_fuel_amu=self.data.physics.m_fuel_amu,
p_alpha_total_mw=self.data.physics.p_alpha_total_mw,
Expand All @@ -887,7 +894,7 @@ def run(self):
p_non_alpha_charged_mw=self.data.physics.p_non_alpha_charged_mw,
p_hcd_injected_total_mw=self.data.current_drive.p_hcd_injected_total_mw,
plasma_current=self.data.physics.plasma_current,
pden_plasma_core_rad_mw=self.data.physics.pden_plasma_core_rad_mw,
pden_plasma_core_rad_mw=reduced_pden,
rmajor=self.data.physics.rmajor,
rminor=self.data.physics.rminor,
temp_plasma_electron_density_weighted_kev=self.data.physics.temp_plasma_electron_density_weighted_kev,
Expand Down
Loading