Skip to content
Open
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
15 changes: 15 additions & 0 deletions disruption_py/machine/cmod/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ imas = "/summary/global_quantities/greenwald_fraction/value"
units = "dimensionless"
validity = [0, 1.5]

[cmod.physics.attributes.h98]
description = "H98 confinement enhancement factor."
units = "dimensionless"
validity = [0.05, 1.75]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

imas?


[cmod.physics.attributes.h_alpha]
description = "H-alpha line emission intensity."
units = "W/(m2*sr)"
validity = [0.0, 161.920898]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

imas?


[cmod.physics.attributes.i_efc]
description = "Error field correction (EFC) coil current."
units = "A"
Expand Down Expand Up @@ -180,6 +190,11 @@ imas = "/bolometer/power_radiated_total"
units = "W"
validity = [0, inf]

[cmod.physics.attributes.pow_thr_LH_Martin]
description = "Martin 2008 L-H transition power threshold scaling."
units = "W"
validity = [0, 0.1e8]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

imas?


[cmod.physics.attributes.prad_peaking]
description = "Peaking factor of the radiated power."
units = "dimensionless"
Expand Down
165 changes: 165 additions & 0 deletions disruption_py/machine/cmod/physics.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
interp1,
)
from disruption_py.inout.mds import mdsExceptions
from disruption_py.machine.cmod.efit import CmodEfitMethods
from disruption_py.machine.cmod.thomson import CmodThomsonDensityMeasure
from disruption_py.machine.tokamak import Tokamak

Expand Down Expand Up @@ -2106,3 +2107,167 @@ def _is_on_blacklist(shot_id: int) -> bool:
or 1150000000 < shot_id < 1150610000
or 1160000000 < shot_id < 1160303000
)

@staticmethod
@physics_method(columns=["h_alpha"], tokamak=Tokamak.CMOD)
def get_h_alpha(params: PhysicsMethodParams):
"""
Get the H_alpha line emission intensity.

The intensity of H-alpha radiance indicates presence of ELMs, and/or
radiative events, and changes of confinement regimes.

In case of using this signal for ELM detection, it is recommended to
use the native time base of the signal to avoid loosing ELMs.

Parameters
----------
params : PhysicsMethodParams
The parameters containing the MDSplus connection, shot id and more.

Returns
-------
dict
A dictionary with the H-alpha signal (`h_alpha`). In IS brightness units [W/(m2*sr)].

Comment on lines +2120 to +2132
Last major update by Enrique Zapata zapata_e@mit.edu

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you might want to reference this very PR -- take a look at the other docstrings for inspiration/formatting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would remove the "Last major update..." line and add the "References" block like other physics methods.

"""
output = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think you need this: any unhandled exception will result in returning NaNs.

"h_alpha": [np.nan],
}
# Get signals from SPECTROSCOPY tree
try:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you do not need the try block, the framework will catch and dump any exception as needed.

h_alpha, time_halpha = params.data_conn.get_data_with_dims(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you should be able to invoke get_data* directly from params.
make sure you are based off the latest dev and remove data_conn from this call.

r"\SPECTROSCOPY::HA_2_BRIGHT", group="SPECTROSCOPY"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

although group is there and works, you should still use tree as usual.
also, I'm pretty sure we made all MDSplus paths lower case for consistency, LMK otherwise.

) # [mW/(cm2*sr)], [s]
# Interpolate Halpha to params.times
h_alpha_interp = interp1(time_halpha, h_alpha, params.times)
output["h_alpha"] = 10 * h_alpha_interp # [W/(m2*sr)]
except ValueError as e:
params.logger.warning("Failed to get H_alpha signal. Returning NaNs.")
params.logger.opt(exception=True).debug(e)
Comment on lines +2146 to +2148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this try-except block would catch any ValueError. The only thing that could fail is R2140-2142 params.data_conn.get_data_with_dims (which can now be shortened to params.get_data_with_dims) which could be automatically caught by the framework.

Since the method will just return output=[nan] if R2140-2142 fails, we can safely move R2140-2145 outside the try-except block, then remove R2135-2137 & R2146-2148.

return output
Comment on lines +2138 to +2149

@staticmethod
@physics_method(columns=["h98"], tokamak=Tokamak.CMOD)
def get_h98(params: PhysicsMethodParams):
"""
Compute H98 by getting tau_E

Parameters
----------
params : PhysicsMethodParams
The parameters containing the MDSplus connection, shot id and more.

Returns
-------
dict
A dictionary with the H98 confinement enhancement factor (`h98`), which is dimensionless

References
----------
Scaling from eq. 20, ITER Physics Basis Chapter 2
https://iopscience.iop.org/article/10.1088/0029-5515/39/12/302/pdf
Comment on lines +2167 to +2170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would make sure the format here is consistent with the References section in other physics methods' docstring. We've never referenced materials outside the disruption-py repo before; I think we can put this as a standalone bullet point and put the URL to a hyperlink, so something like this:

References
--------
- Scaling from eq. 20, [ITER Physics Basis Chapter 2](https://iopscience.iop.org/article/10.1088/0029-5515/39/12/302/pdf)
- pull requests: #[562](<link_to_this_pr>)


Original Author: Andrew Maris (maris@mit.edu)
Last major update by Enrique Zapata (zapata_e@mit.edu)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you might want to reference this very PR -- take a look at the other docstrings for inspiration/formatting.

"""

# Get parameters for calculating confinement time
powers_df = CmodPhysicsMethods.get_power(params=params)
efit_df = CmodEfitMethods.get_efit_parameters(params=params)
density_df = CmodPhysicsMethods.get_densities(params=params)
ip_df = CmodPhysicsMethods.get_ip_parameters(params=params)
Comment on lines +2177 to +2180

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. All these methods return a dictionary, not a dataframe, so it'd be better to change the variable names to reflect their data type.
  2. I'm always a little wary of calling other physics methods instead of fetching the raw signals directly from MDSplus, and then do some additional computation. The physics methods will return the signals already interpolated onto the requested timebase, so there could be a chance that this would introduce some errors when the original signals are sampled at a much lower frequency, or the timebase of the original signal is severely misaligned with the requested timebase. That being said I've done similar things before, and I don't think this is going to be a major problem here; it's just something to consider for future physics methods that involve using data from e.g. the Thomson system.


# Get btor
btor, t_mag = params.data_conn.get_data_with_dims(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

drop data_conn, use tree rather than group.

r"\btor", group="magnetics"
) # tmag: [s]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

at this point mention units for both quantities, here, I guess T and s.

# Toroidal power supply takes time to turn on, from ~ -1.8 and should be
# on by t=-1. So pick the time before that to calculate baseline
baseline_indices = np.where(t_mag <= -1.8)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

don't you need a comma, here? please double check dimensions.
your slicing might work in the following line, but this might not be what you want -- a list of indices.

btor = btor - np.mean(btor[baseline_indices])
btor = np.abs(interp1(t_mag, btor, params.times))

# Get signals
ip = np.abs(ip_df.get("ip")) / 1.0e6 # [A] -> [MA]
n_e = density_df.get("n_e") / 1.0e19 # [m^-3] -> [10^19 m^-3]
p_input = powers_df.get("p_input") / 1.0e6 # [W] -> [MW]
dwmhd_dt = efit_df.get("dwmhd_dt") / 1.0e6 # [W] -> [MW]
Comment on lines +2193 to +2196
wmhd = efit_df.get("wmhd") / 1.0e6 # [J] -> [MJ]
r0 = efit_df.get("rmagx") # [m]
a_minor = efit_df.get("a_minor") # [m]
epsilon = a_minor / r0
kappa = efit_df.get("kappa")
tau = wmhd / (p_input - dwmhd_dt)

# Compute 1998 tau_E scaling, taking A (atomic mass) = 2.
tau_98 = (
0.0562
* (np.sign(n_e) * np.abs(n_e) ** 0.41)
* (2**0.19)
* (np.sign(ip) * np.abs(ip) ** 0.93)
* (np.sign(r0) * np.abs(r0) ** 1.97)
* (np.sign(epsilon) * np.abs(epsilon) ** 0.58)
* (np.sign(kappa) * np.abs(kappa) ** 0.78)
* (np.sign(btor) * np.abs(btor) ** 0.15)
* (np.sign((p_input - dwmhd_dt)) * np.abs((p_input - dwmhd_dt)) ** -0.69)
)
h98 = tau / tau_98
h98[h98 <= 0] = 0
output = {
"h98": h98,
}
return output

@staticmethod
@physics_method(columns=["pow_thr_LH_Martin"], tokamak=Tokamak.CMOD)

@yumouwei yumouwei Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd use all lowercase for the signal name to align with the convention.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I also just found out that RetrievalSettings(run_columns=['pow_thr_LH_Martin'] doesn't call the method correctly unless I switch it to all lowercase

def get_itpa_pow_thr(params: PhysicsMethodParams):
"""
Power threshold for L-H transition.

Parameters
----------
params : PhysicsMethodParams
The parameters containing the MDSplus connection, shot id and more.

Returns
-------
dict
A dictionary with the power threshold for L-H transition
(`pow_thr_LH_Martin`), in Watts [W]

References
----------
Scaling from Y R Martin et al 2008 J. Phys.: Conf. Ser. 123 012033
DOI 10.1088/1742-6596/123/1/012033

Last major update by Enrique Zapata

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above.

"""

density_df = CmodPhysicsMethods.get_densities(params=params)
areao_df = CmodPhysicsMethods.get_kappa_area(params=params)

# Get BT
btor, t_mag = params.data_conn.get_data_with_dims(
r"\btor", group="magnetics"
) # tmag: [s]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above.

# Toroidal power supply takes time to turn on, from ~ -1.8 and should be
# on by t=-1. So pick the time before that to calculate baseline
baseline_indices = np.where(t_mag <= -1.8)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above.

btor = btor - np.mean(btor[baseline_indices])
btor = np.abs(interp1(t_mag, btor, params.times)) # [T]
Comment on lines +2251 to +2259

@yumouwei yumouwei Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We might want to distill the btor computation in get_n_equal_1_amplitude, get_h98 and get_itpa_pow_thr into a single physics method similar to D3DPhysicsMethods.get_btor, or just call get_n_equal_1_amplitude to get the btor signal.

I do realize this suggestion is contradictory with my suggestion in R2180 so take this with a grain of salt.

n_e = density_df.get("n_e") / 1.0e20 # [m^-3] -> [10^20 m^-3]
areao = areao_df.get("kappa_area") # [m^2]

Comment on lines +2248 to +2262
# Estimate power threshold.
itpa_power_thr = (
0.0488
* (np.sign(n_e) * np.abs(n_e) ** 0.717)
* (np.sign(btor) * np.abs(btor) ** 0.803)
* (np.sign(areao) * np.abs(areao) ** 0.941)
)
output = {
"pow_thr_LH_Martin": 1e6 * itpa_power_thr,
}
return output
Comment on lines +2270 to +2273

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can just do return {"pow_thr_lh_martin": 1e6 * itpa_power_thr}