From f2d4df249c1ea3b5bfa52aaaa87d36058af74688 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 6 Jan 2026 22:36:15 +0000 Subject: [PATCH 01/80] [#1166] Type hinting, docstr and better default for d3cross, d2cross --- .../electrons/emission/_xray_thin_target.py | 65 ++++++++++++------ .../emission/_xray_thin_target_integrated.py | 27 +++++--- .../_xray_thin_target_integrated_plot.py | 66 ++++++++++++++----- 3 files changed, 111 insertions(+), 47 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index 155497344..0b286a8a5 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -2,6 +2,7 @@ import os import warnings +from typing import Any, Optional # Dict import numpy as np @@ -12,6 +13,9 @@ import datastock as ds +TupleDict = tuple[dict] + + # #################################################### # #################################################### # DEFAULT @@ -28,8 +32,9 @@ def get_xray_thin_d3cross_ei( - # inputs - Z=None, + # target ion charge + Z: Optional[int] = None, + # Energy E_e0_eV=None, E_e1_eV=None, # directions @@ -37,19 +42,19 @@ def get_xray_thin_d3cross_ei( theta_e=None, dphi=None, # hypergeometric parameter - ninf=None, - source=None, + ninf: Optional[int] = None, + source: Optional[str] = None, # output customization - per_energy_unit=None, + per_energy_unit: Optional[str] = None, # version - version=None, + version: Optional[str] = None, # debug - debug=None, -): + debug: Optional[bool] = None, +) -> dict: """ Return a differential cross-section for thin-target bremsstrahlung - Allowws several formulas (version): - - 'BE': Elwert-Haug [1] + Allows several formulas (version): + - 'EH': Elwert-Haug [1] (default) . most general and accurate . Uses Sommerfield-Maue eigenfunctions . eq. (30) in [1] @@ -74,14 +79,15 @@ def get_xray_thin_d3cross_ei( Physics Reports, vol. 243, p. 317—353, 1994. - Inputs: + Inputs: (all angles in rad) E_e0_eV = kinetic energy of incident electron in eV E_e1_eV = kinetic energy of scattered electron in eV theta_e = (spherical) theta angle of scattered e vs incident e theta_ph = (spherical) theta angle of photon vs incident e phi_e = (spherical) phi angle of scattered e vs incident e phi_ph = (spherical) theta angle of photon vs incident e - (all angles in rad) + version = 'EH', 'BH' or 'BHE' + Limitations: - 'EH' implementation currently stalled because: @@ -1326,8 +1332,8 @@ def _hyp2F1( bb=None, cc=None, zz=None, - ninf=None, - source=None, + ninf: Optional[int] = None, + source: Optional[str] = None, ): """ Hypergeometric function 2F1 with complex arguments @@ -1389,8 +1395,12 @@ def _hyp2F1( # Number of terms # ---------- - if ninf is None: - ninf = 50 + ninf = ds._generic_check._check_var( + ninf, 'ninf', + types=int, + default=50, + sign='>0', + ) nn = np.arange(0, ninf)[None, :] @@ -1495,11 +1505,11 @@ def _hyp2F1( def plot_xray_thin_d3cross_ei_vs_Literature( - version=None, - ninf=None, - source=None, - dax=None, -): + version: Optional[str] = None, + ninf: Optional[int] = None, + source: Optional[str] = None, + dax: Optional[dict[str, Any]] = None, +) -> TupleDict: """ Compare computed cross-sections vs literature values from Elwert-Haug Triply differential cross-section @@ -1509,6 +1519,19 @@ def plot_xray_thin_d3cross_ei_vs_Literature( [2] W. Nakel, Physics Reports, 243, p. 317—353, 1994 + Return: + ------- + dax: dict + Dict of axes + ddata_iso: dict + Dict of + ddata_ph_dist: dict + Dict of + ddata_ph_dist_nakel: dict + Dict of + ddata_ph_spect_nakel: dict + Dict of + """ # -------------- diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 2cb60228c..0c9e68c5e 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -1,6 +1,7 @@ import os +from typing import Any, Optional # Dict import numpy as np @@ -14,6 +15,9 @@ from . import _xray_thin_target +TupleDict = tuple[dict] + + # #################################################### # #################################################### # DEFAULT @@ -40,25 +44,28 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( - # inputs - Z=None, + # target ion charge + Z: Optional[int] = None, + # energies E_e0_eV=None, E_ph_eV=None, theta_ph=None, # hypergeometric parameter - ninf=None, - source=None, + ninf: Optional[int] = None, + source: Optional[str] = None, # integration parameters nthetae=None, ndphi=None, # output customization - per_energy_unit=None, + per_energy_unit: Optional[str] = None, # version - version=None, + version: Optional[str] = None, # verb - verb=None, - verb_tab=None, -): + verb: Optional[bool] = None, + verb_tab: Optional[str] = None, +) -> dict: + """ Compute d2cross, which is d3cross integrated over dphi + """ # ------------ # inputs @@ -306,7 +313,7 @@ def _check( # #################################################### -def plot_xray_thin_d2cross_ei_vs_literature(): +def plot_xray_thin_d2cross_ei_vs_literature() -> TupleDict: """ Plot electron-angle-integrated cross section vs [1] G. Elwert and E. Haug, Phys. Rev., 183, pp. 90–105, 1969 diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index c967ae16b..605ef266d 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -1,6 +1,7 @@ import copy +from typing import Optional # Any, Dict import numpy as np @@ -15,12 +16,22 @@ from . import _xray_thin_target_integrated +TupleDict = tuple[dict] + + # #################################################### # #################################################### # DEFAULT # #################################################### +# Energy vectors +_E_E0_EV = np.logspace(3, 6, 51) +_E_PH_EV = np.linspace(1, 100, 25) * 1e3 +_THETA_PH = np.linspace(0, np.pi, 41) +_VERSION = 'BHE' + + # ANISOTROPY CASES _DCASES = { 0: { @@ -66,6 +77,8 @@ 'ms': 14, }, } + + # #################################################### # #################################################### # plot anisotropy @@ -73,26 +86,43 @@ def plot_xray_thin_d2cross_ei_anisotropy( - # compute - Z=None, + # target ion charge + Z: Optional[int] = None, + # Energy E_e0_eV=None, E_ph_eV=None, theta_ph=None, - per_energy_units=None, - version=None, - # hypergeometrc - ninf=None, - source=None, + # hypergeometric parameter + ninf: Optional[int] = None, + source: Optional[str] = None, + # output customization + per_energy_unit: Optional[str] = None, + # version + version: Optional[str] = None, # selected cases - dcases=None, + dcases: Optional[dict[int, dict]] = None, # plot - dax=None, - fontsize=None, + dax: Optional[dict] = None, + fontsize: Optional[int] = None, dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, dplot_integ=None, -): +) -> TupleDict: + """ Compute and plot a (E_e0, E_ph) countour map of the d2cross section + + Where d2cross is the fully differentiated cross-section (d3cross), + integrated over one of the two the emission angle (dphi) + + Actually 3 overlayed contour plots with: + - integral of of the cross-section (over photon emission angle) + - angle of max cross-section + - peaking of the cross-section (std vs angle) + + Can overlay a few selected cases and plot them vs angle of emission + In normalized-linear and log scales + + """ # --------------- # check inputs @@ -100,6 +130,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( ( E_e0_eV, E_ph_eV, theta_ph, + version, dcases, fontsize, dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_integ, @@ -130,7 +161,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( E_ph_eV=E_ph_eV[None, None, :], theta_ph=theta_ph[:, None, None], # output customization - per_energy_unit=per_energy_units, + per_energy_unit=per_energy_unit, # version version=version, # hypergeometric @@ -359,31 +390,33 @@ def _check_anisotropy( # E_e0_eV if E_e0_eV is None: - E_e0_eV = np.logspace(3, 6, 51) + E_e0_eV = _E_E0_EV E_e0_eV = ds._generic_check._check_flat1darray( E_e0_eV, 'E_e0_eV', dtype=float, sign='>0', + unique=True, ) # E_ph_eV if E_ph_eV is None: - E_ph_eV = np.linspace(1, 100, 25) * 1e3 + E_ph_eV = _E_PH_EV E_ph_eV = ds._generic_check._check_flat1darray( E_ph_eV, 'E_ph_eV', dtype=float, sign='>0', + unique=True, ) # theta_ph if theta_ph is None: - theta_ph = np.linspace(0, np.pi, 41) + theta_ph = _THETA_PH # version if version is None: - version = 'BHE' + version = _VERSION # ------------ # dcases @@ -462,6 +495,7 @@ def _check_anisotropy( return ( E_e0_eV, E_ph_eV, theta_ph, + version, dcases, fontsize, dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_integ, From a2bceb5c75da8811cedc9220f6d567bc57910fb2 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 7 Jan 2026 19:01:11 +0000 Subject: [PATCH 02/80] [#1166] Cleanup --- .../_xray_thin_target_integrated_plot.py | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 605ef266d..279bfa5a4 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -107,7 +107,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, - dplot_integ=None, + dplot_mean=None, ) -> TupleDict: """ Compute and plot a (E_e0, E_ph) countour map of the d2cross section @@ -133,7 +133,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( version, dcases, fontsize, - dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_integ, + dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_mean, ) = _check_anisotropy( E_e0_eV=E_e0_eV, E_ph_eV=E_ph_eV, @@ -146,7 +146,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( dplot_forbidden=dplot_forbidden, dplot_peaking=dplot_peaking, dplot_thetamax=dplot_thetamax, - dplot_integ=dplot_integ, + dplot_mean=dplot_mean, ) # --------------- @@ -193,20 +193,21 @@ def plot_xray_thin_d2cross_ei_anisotropy( for iv, (kk, vv) in enumerate(d2cross['cross'].items()): # compute integral and peaking - integ, peaking = _get_peaking( + mean, peaking = _get_peaking( vv['data'], theta_ph*180/np.pi, axis=0, ) + mean_units = vv['units'] # integral - if dplot_integ is not False: + if dplot_mean is not False: im0 = ax.contour( E_e0_eV * 1e-3, E_ph_eV * 1e-3, - np.log10(integ).T, - levels=dplot_integ['levels'], - colors=dplot_integ['colors'], + np.log10(mean).T, + levels=dplot_mean['levels'], + colors=dplot_mean['colors'], ) # clabels @@ -271,24 +272,27 @@ def plot_xray_thin_d2cross_ei_anisotropy( ax.add_patch(patch) # legend - lh = [ - mlines.Line2D( + lh = [] + if dplot_mean is not False: + lh.append(mlines.Line2D( [], [], - c=dplot_integ['colors'], - label='log10(integral)', - ), - mlines.Line2D( + c=dplot_mean['colors'], + label=f'log10() (log10({mean_units}))', + )) + if dplot_peaking is not False: + lh.append(mlines.Line2D( [], [], c=dplot_peaking['colors'], label='peaking (1/std)', - ), - mlines.Line2D( + )) + if dplot_thetamax is not False: + lh.append(mlines.Line2D( [], [], c=dplot_thetamax['colors'], label='theta_max (deg)', - ), - ] - ax.legend(handles=lh, loc='upper left') + )) + if len(lh) > 0: + ax.legend(handles=lh, loc='upper left') # add cases for ic, (kcase, vcase) in enumerate(dcases.items()): @@ -351,7 +355,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( ax.set_xlim(0, 180) # normalized - kax = 'abs' + kax = 'log' if dax.get(kax) is not None: ax = dax[kax]['handle'] ax.legend(prop={'size': 12}) @@ -385,7 +389,7 @@ def _check_anisotropy( dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, - dplot_integ=None, + dplot_mean=None, ): # E_e0_eV @@ -485,11 +489,11 @@ def _check_anisotropy( ddef, ) - # dplot_integ + # dplot_mean ddef = {'colors': 'g', 'levels': 20} - dplot_integ = _check_anisotropy_dplot( - dplot_integ, - 'dplot_integ', + dplot_mean = _check_anisotropy_dplot( + dplot_mean, + 'dplot_mean', ddef, ) @@ -498,7 +502,7 @@ def _check_anisotropy( version, dcases, fontsize, - dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_integ, + dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_mean, ) @@ -568,7 +572,7 @@ def _get_peaking(data, x, axis=None): x_avf = scpinteg.simpson(data_n * xf, x=x, axis=axis).reshape(shape_integ) std = np.sqrt(scpinteg.simpson(data_n * (xf - x_avf)**2, x=x, axis=axis)) - return integ, 1/std + return integ/180, 1/std # ############################################# @@ -584,13 +588,13 @@ def _get_axes_anisotropy( ): tit = ( - "Anisotropy" + "Thin-target Bremsstrahlung cross-section anisotropy" ) dmargin = { - 'left': 0.08, 'right': 0.95, - 'bottom': 0.06, 'top': 0.85, - 'wspace': 0.2, 'hspace': 0.40, + 'left': 0.06, 'right': 0.95, + 'bottom': 0.06, 'top': 0.90, + 'wspace': 0.20, 'hspace': 0.20, } fig = plt.figure(figsize=(15, 12)) @@ -659,11 +663,6 @@ def _get_axes_anisotropy( size=fontsize, fontweight='bold', ) - ax.set_ylabel( - r"$\frac{d^2\sigma_{ei}}{dkd\Omega_{ph}}$ ()", - size=fontsize, - fontweight='bold', - ) ax.set_title( "Absolute cross-section vs photon emission angle", size=fontsize, From 4d4c715dd7c07c8fdd4d3e5f4f221668012899ce Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 8 Jan 2026 21:20:13 +0000 Subject: [PATCH 03/80] [#1166] _xray_thin_target_integrated.py updated to load d2cross directly too --- .../electrons/emission/__init__.py | 1 + .../emission/_xray_thin_target_integrated.py | 537 +++++++++++++++--- .../_xray_thin_target_integrated_plot.py | 10 +- 3 files changed, 455 insertions(+), 93 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/__init__.py b/tofu/physics_tools/electrons/emission/__init__.py index 5deac83c0..324733945 100644 --- a/tofu/physics_tools/electrons/emission/__init__.py +++ b/tofu/physics_tools/electrons/emission/__init__.py @@ -13,4 +13,5 @@ from ._xray_thin_target_integrated_plot import plot_xray_thin_d2cross_ei_anisotropy from ._xray_thin_target_integrated_dist import get_xray_thin_integ_dist from ._xray_thin_target_integrated_d2crossphi import get_d2cross_phi +from ._xray_thin_target_integrated_dist_responsivity_plot import plot_xray_thin_integ_dist_filter_anisotropy from ._xray_thin_target_integrated_dist_plot import plot_xray_thin_integ_dist diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 0c9e68c5e..774564980 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -1,7 +1,9 @@ import os -from typing import Any, Optional # Dict +import copy +import warnings +from typing import Optional # Dict, Any import numpy as np @@ -37,6 +39,45 @@ _NDPHI = 51 +# Default naming +_DSCALE = { + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'k', + 0: '', + -3: 'm', + -6: 'u', + -9: 'n', +} + + +_DFORMAT = { + # energies + 'E_e0': { + 'data': None, + 'units': 'eV', + }, + 'E_ph': { + 'data': None, + 'units': 'eV', + }, + # angles + 'theta_ph': { + 'data': None, + 'units': 'rad', + }, + 'theta_e': { + 'data': None, + 'units': 'rad', + }, + 'dphi': { + 'data': None, + 'units': 'rad', + }, +} + + # #################################################### # #################################################### # main @@ -44,6 +85,8 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( + # optional input d2cross file + d2cross: Optional[str | dict] = None, # target ion charge Z: Optional[int] = None, # energies @@ -63,8 +106,17 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( # verb verb: Optional[bool] = None, verb_tab: Optional[str] = None, + # saving + save: Optional[bool] = None, + pfe_save: Optional[str] = None, + overwrite: Optional[bool] = None, ) -> dict: """ Compute d2cross, which is d3cross integrated over dphi + + Optionally loads / checks formatting of a pre-existing d2cross + - d2cross = str, should be a pfe to a local .npz + - d2cross = dict, will use as-is + """ # ------------ @@ -76,6 +128,7 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( nthetae, ndphi, shape, shape_theta_e, shape_dphi, verb, verb_tab, + save, pfe_save, overwrite, ) = _check( # inputs E_e0_eV=E_e0_eV, @@ -87,107 +140,36 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( # verb verb=verb, verb_tab=verb_tab, + # save + save=save, + pfe_save=pfe_save, + overwrite=overwrite, ) # ------------------ - # Derive angles + # compute # ------------------ - # E_e1_eV - E_e1_eV = E_e0_eV - E_ph_eV + if d2cross is None: - # angles - theta_e = np.pi * np.linspace(0, 1, nthetae) - dphi = np.pi * np.linspace(-1, 1, ndphi) - theta_ef = theta_e.reshape(shape_theta_e) - dphif = dphi.reshape(shape_dphi) + d2cross = _compute(**locals()) - # derived - sinte = np.sin(theta_ef) - - # ------------------ - # get d3cross - # ------------------ - - if verb >= 1: - msg = f"{verb_tab}Computing d3cross for shape {shape}... " - print(msg) - - d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( - # inputs - Z=Z, - E_e0_eV=E_e0_eV[..., None, None], - E_e1_eV=E_e1_eV[..., None, None], - # directions - theta_ph=theta_ph[..., None, None], - theta_e=theta_ef, - dphi=dphif, - # hypergeometric parameter - ninf=ninf, - source=source, - # output customization - per_energy_unit=per_energy_unit, - # version - version=version, - # debug - debug=False, - ) + # optional save + if save is True: + _save( + d2cross, + pfe_save=pfe_save, + overwrite=overwrite, + verb=verb, + ) # ------------------ - # prepare output + # load # ------------------ - d2cross = { - # energies - 'E_e0': { - 'data': E_e0_eV, - 'units': 'eV', - }, - 'E_ph': { - 'data': E_ph_eV, - 'units': 'eV', - }, - # angles - 'theta_ph': { - 'data': theta_ph, - 'units': 'rad', - }, - 'theta_e': { - 'data': theta_e, - 'units': 'rad', - }, - 'dphi': { - 'data': dphi, - 'units': 'rad', - }, - # cross-section - 'cross': { - vv: { - 'data': np.full(shape, 0.), - 'units': asunits.Unit(vcross['units']) * asunits.Unit('sr'), - } - for vv, vcross in d3cross['cross'].items() - }, - } - - # ------------------ - # integrate - # ------------------ + else: - if verb >= 1: - msg = f"{verb_tab}Integrating..." - print(msg) - - for vv, vcross in d3cross['cross'].items(): - d2cross['cross'][vv]['data'][...] = scpinteg.simpson( - scpinteg.simpson( - vcross['data'] * sinte, - x=theta_e, - axis=-1, - ), - x=dphi, - axis=-1, - ) + d2cross = _load(d2cross) return d2cross @@ -209,6 +191,10 @@ def _check( # verb verb=None, verb_tab=None, + # saving + save=None, + pfe_save=None, + overwrite=None, ): # ----------- @@ -299,14 +285,385 @@ def _check( ) verb_tab = '\t'*verb_tab + # ----------- + # save + # ----------- + + savedef = pfe_save not in [None, False] + save = ds._generic_check._check_var( + save, 'save', + types=bool, + default=savedef, + ) + + # ----------- + # pfe_save + # ----------- + + if save is True: + if pfe_save is not None: + pfe_save = int(ds._generic_check._check_var( + pfe_save, 'pfe_save', + types=str, + )) + + try: + pfe_save = os.path.abspath(pfe_save) + except Exception as err: + msg = ( + "Arg 'pfe_save' must point to an valid path/file.ext\n" + f"Provided: {pfe_save}\n" + ) + raise Exception(msg) from err + + if not pfe_save.endswith('.npz'): + msg = ( + "Arg 'pfe_save' must point to an valid path/file.npz\n" + f"Provided: {pfe_save}\n" + ) + raise Exception(msg) + + else: + pfe_save = None + + # ----------- + # overwrite + # ----------- + + overwrite = ds._generic_check._check_var( + overwrite, 'overwrite', + types=bool, + default=False, + ) + return ( E_e0_eV, E_ph_eV, theta_ph, nthetae, ndphi, shape, shape_theta_e, shape_dphi, verb, verb_tab, + save, pfe_save, overwrite, ) +# #################################################### +# #################################################### +# compute +# #################################################### + + +def _compute( + Z=None, + E_e0_eV=None, + E_e1_eV=None, + E_ph_eV=None, + theta_ph=None, + # shapes + nthetae=None, + shape_theta_e=None, + theta_ef=None, + ndphi=None, + shape_dphi=None, + # parameters + ninf=None, + source=None, + version=None, + per_energy_unit=None, + # misc + shape=None, + verb=None, + verb_tab=None, + # unused + **kwdargs, +): + + # ------------------ + # get angles and shape + # ------------------ + + # E_e1_eV + E_e1_eV = E_e0_eV - E_ph_eV + + # angles + theta_e = np.pi * np.linspace(0, 1, nthetae) + dphi = np.pi * np.linspace(-1, 1, ndphi) + theta_ef = theta_e.reshape(shape_theta_e) + dphif = dphi.reshape(shape_dphi) + + # derived + sinte = np.sin(theta_ef) + + # ------------------ + # get d3cross + # ------------------ + + if verb >= 1: + msg = f"{verb_tab}Computing d3cross for shape {shape}... " + print(msg) + + d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( + # inputs + Z=Z, + E_e0_eV=E_e0_eV[..., None, None], + E_e1_eV=E_e1_eV[..., None, None], + # directions + theta_ph=theta_ph[..., None, None], + theta_e=theta_ef, + dphi=dphif, + # hypergeometric parameter + ninf=ninf, + source=source, + # output customization + per_energy_unit=per_energy_unit, + # version + version=version, + # debug + debug=False, + ) + + # ------------------ + # prepare output + # ------------------ + + d2cross = copy.deepcopy(_DFORMAT) + for kk, vv in _DFORMAT.items(): + kv = f"{kk}_{vv['units']}" if vv['units'] == 'eV' else kk + d2cross[kk]['data'] = eval(kv) + + # cross-sections + d2cross['cross'] = { + vv: { + 'data': np.full(shape, 0.), + 'units': asunits.Unit(vcross['units']) * asunits.Unit('sr'), + } + for vv, vcross in d3cross['cross'].items() + } + + # ------------------ + # integrate + # ------------------ + + if verb >= 1: + msg = f"{verb_tab}Integrating..." + print(msg) + + for vv, vcross in d3cross['cross'].items(): + d2cross['cross'][vv]['data'][...] = scpinteg.trapezoid( + scpinteg.trapezoid( + vcross['data'] * sinte, + x=theta_e, + axis=-1, + ), + x=dphi, + axis=-1, + ) + + return d2cross + + +# #################################################### +# #################################################### +# save +# #################################################### + + +def _save( + d2cross=None, + pfe_save=None, + overwrite=None, + verb=None, +): + + # ---------- + # pfe_save + # ---------- + + if pfe_save is None: + + # extract sizes + ntheta = d2cross['theta_ph']['data'].size + Eph = _format_vect2str( + d2cross['E_ph']['data'], + base=d2cross['E_ph']['units'], + ) + Ee0 = _format_vect2str( + d2cross['E_e0']['data'], + base=d2cross['E_e0']['units'], + ) + + # extract boundaries + path = os.path.abspath(_PATH_HERE) + fname = f"d2cross_Ee0{Ee0}_Eph{Eph}_ntheta{ntheta}.npz" + pfe_save = os.path.join(path, f"{fname}.npz") + + # ---------- + # overwrite + # ---------- + + if os.path.isfile(pfe_save): + if overwrite is True: + if verb is True: + msg = f"Overwritting file {pfe_save}\n" + warnings.warn(msg) + else: + msg = ( + "File {pfe_save} already exists!\n" + "\t=> use overwrite=True to overwrite\n" + ) + raise Exception(msg) + + # ---------- + # save + # ---------- + + np.savez(pfe_save, **d2cross) + + # ---------- + # verb + # ---------- + + if verb is True: + msg = "Saved d2cross in:\n\t{pfe_save}\n" + print(msg) + + return + + +# #################################################### +# #################################################### +# Built str vector +# #################################################### + + +def _format_vect2str(vect, base='eV'): + + # ------------- + # extract + # ------------- + + v0 = vect.min() + v1 = vect.max() + nv = vect.size + + # ------------- + # test scale + # ------------- + + dlog = np.diff(np.log(vect)) + dlin = np.diff(vect) + islog = np.allclose(dlog, dlog[0]) + islin = np.allclose(dlin, dlin[0]) + + if islog is True: + scale = 'log' + elif islin is True: + scale = 'lin' + else: + scale = '' + + # ------------- + # format + # ------------- + + v0 = _format(v0, base=base) + v1 = _format(v1, base=base) + + return f"{v0}-{v1}-{nv}{scale}" + + +def _format(vv, base='eV'): + + ls = sorted(_DSCALE.keys()) + ind = np.searchsorted(ls, np.log10(vv), side='right') - 1 + key = ls[ind] + factor = 10**(-key) + + return f"{vv*factor:3.0f}{_DSCALE[key]}{base}".strip() + + +# #################################################### +# #################################################### +# load +# #################################################### + + +def _load( + d2cross=None, +): + + # --------- + # str + # --------- + + if isinstance(d2cross, str): + + if not (os.path.isfile(d2cross) and d2cross.endswith('.npz')): + msg = ( + "Arg 'd2cross', if a str, should be valid path/file.npz\n" + f"Provided: {d2cross}\n" + ) + raise Exception(msg) + + d2cross = dict(np.load(d2cross, allow_pickle=True)) + + # --------- + # dict + # --------- + + if not isinstance(d2cross, dict): + msg = "Arg 'd2cross' must be a dict!\nProvided: {type(d2cross)}\n" + raise Exception(msg) + + # ---------------- + # inner formatting + # ---------------- + + dfail = {} + for kk in _DFORMAT.keys(): + if not isinstance(d2cross.get(kk), dict): + dfail[kk] = 'not a key or value not a dict ()' + elif str(d2cross[kk].get('units')) != _DFORMAT[kk]['units']: + dfail[kk] = 'no or wrong units ()' + elif not isinstance(d2cross[kk]['data'], np.ndarray): + dfail[kk] = "data is not a np.ndarray (type(d2cross[kk]['data']))" + else: + dfail[kk] = 'ok' + + if any([vv != 'ok' for vv in dfail.values()]) > 0: + lstr = [f"\t- {kk}: {vv}" for kk, vv in dfail.items()] + msg = ( + "Arg 'd2cross' must be a dict of subdicts " + "{'data': np.ndarray, 'units': str}, with keys:\n" + + "\n".join(lstr) + ) + raise Exception(msg) + + # ---------------- + # cross formatting + # ---------------- + + lok = ['BHE', 'BH', 'EH'] + c0 = ( + isinstance(d2cross.get('cross'), dict) + and all([ + kk in lok + and isinstance(vv, dict) + and isinstance(vv.get('data'), np.ndarray) + and isinstance(vv.get('data'), str) + for kk, vv in d2cross['cross'].items() + ]) + ) + if not c0: + msg = ( + "Arg d2cross['cross'] must be a dict " + "with {'version': dict} subdict with:\n" + f"\t- 'version' in {lok}\n" + f"Provided:\n{d2cross.get('cross')}\n" + ) + raise Exception(msg) + + return d2cross + + # #################################################### # #################################################### # plot vs litterature diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 279bfa5a4..3705df786 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -13,7 +13,7 @@ import datastock as ds -from . import _xray_thin_target_integrated +from . import _xray_thin_target_integrated as _mod TupleDict = tuple[dict] @@ -86,6 +86,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( + # optional input d2cross file + d2cross: Optional[str | dict] = None, # target ion charge Z: Optional[int] = None, # Energy @@ -153,8 +155,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( # prepare data # --------------- - mod = _xray_thin_target_integrated - d2cross = mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( + d2cross = _mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( + d2cross=d2cross, # inputs Z=Z, E_e0_eV=E_e0_eV[None, :, None], @@ -182,6 +184,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( fontsize=fontsize, ) + dax = ds._generic_check._check_dax(dax) + # --------------- # plot - map # --------------- From 4153bd6039e118a39a936ed24dde1dedd339413f Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 8 Jan 2026 22:52:56 +0000 Subject: [PATCH 04/80] [#1166] operational for basics --- .../electrons/emission/_xray_thin_target_integrated_plot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 3705df786..f3d66b76a 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -455,6 +455,8 @@ def _check_anisotropy( + f"{ee0*1e-3:3.0f} / {eph*1e-3:3.0f} keV = " + f"{round(ee0 / eph, ndigits=1)}" ) + else: + dcases = {} # ------------ # plotting From c6c3eca744b343cd420e81a8437180789f1b3693 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 8 Jan 2026 22:53:12 +0000 Subject: [PATCH 05/80] [#1166] operational for basics --- ...arget_integrated_dist_responsivity_plot.py | 427 ++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py new file mode 100644 index 000000000..c6b93ac8f --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -0,0 +1,427 @@ + + +import copy +from typing import Optional # Any, Dict + + +import numpy as np +import astropy.units as asunits +import matplotlib.pyplot as plt +# import matplotlib.lines as mlines +import matplotlib.gridspec as gridspec +import datastock as ds + + +# from . import _xray_thin_target_integrated as _mod +from . import _xray_thin_target_integrated_plot as _mod_plot +# from ..distribution import get_distribution + + +TupleDict = tuple[dict] + + +# #################################################### +# #################################################### +# DEFAULT +# #################################################### + + +# Energy vectors +_E_E0_EV = np.logspace(3, 6, 51) +_E_PH_EV = np.linspace(1, 100, 25) * 1e3 +_THETA_PH = np.linspace(0, np.pi, 41) +_VERSION = 'BHE' + + +# DCASES +_DCASES = { + 'cvd no filter maxwell': { + 'E_ph': {}, + 'responsivity': {}, + 'dist': {}, + 'E_e0': {}, + }, +} + + +# #################################################### +# #################################################### +# plot integrated filtered anisotropy +# #################################################### + + +def plot_xray_thin_integ_dist_filter_anisotropy( + # optional input d2cross file + d2cross: Optional[str | dict] = None, + # target ion charge + Z: Optional[int] = None, + # Energy + E_e0_eV=None, + E_ph_eV=None, + theta_ph=None, + # hypergeometric parameter + ninf: Optional[int] = None, + source: Optional[str] = None, + # output customization + per_energy_unit: Optional[str] = None, + # version + version: Optional[str] = None, + # selected cases + dcases: Optional[dict[int, dict]] = None, + # plot + dax: Optional[dict] = None, + fs: Optional[tuple] = None, + fontsize: Optional[int] = None, + dplot_forbidden=None, + dplot_peaking=None, + dplot_thetamax=None, + dplot_mean=None, +) -> TupleDict: + """ Compute and plot a (E_e0, E_ph) countour map of the d2cross section + + Where d2cross is the fully differentiated cross-section (d3cross), + integrated over one of the two the emission angle (dphi) + + Actually 3 overlayed contour plots with: + - integral of of the cross-section (over photon emission angle) + - angle of max cross-section + - peaking of the cross-section (std vs angle) + + Can overlay a few selected cases and plot them vs angle of emission + In normalized-linear and log scales + + """ + + # --------------- + # check inputs + # --------------- + + dcases, fs, fontsize = _check( + dcases=dcases, + fs=fs, + fontsize=fontsize, + ) + + # --------------- + # prepare data + # --------------- + + # -------------- + # prepare axes + # -------------- + + if dax is None: + dax = _dax( + Z=Z, + version=version, + fs=fs, + fontsize=fontsize, + ) + + dax = ds._generic_check._check_dax(dax) + + # ------------------- + # plot cross-section + # ------------------- + + dax_cross, d2cross_cross = _mod_plot.plot_xray_thin_d2cross_ei_anisotropy( + # optional input d2cross file + d2cross=d2cross, + # target ion charge + Z=Z, + # Energy + E_e0_eV=E_e0_eV, + E_ph_eV=E_ph_eV, + theta_ph=theta_ph, + # hypergeometric parameter + ninf=ninf, + source=source, + # output customization + per_energy_unit=per_energy_unit, + # version + version=version, + # selected cases + dcases=False, + # plot + dax=dax, + dplot_forbidden=dplot_forbidden, + dplot_peaking=dplot_peaking, + dplot_thetamax=dplot_thetamax, + dplot_mean=dplot_mean, + ) + + # ------------------- + # plot responsivity + # ------------------- + + kax = 'responsivity' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for kk, vv in dcases.items(): + + l0, = ax.plot( + vv['responsivity']['data'], + vv['E_ph']['data']*1e-3, + c=vv.get('color'), + ls=vv.get('ls', '-'), + marker=vv.get('marker'), + lw=vv.get('lw', 1.), + label=kk, + ) + dcases[kk]['color'] = l0.get_color() + + ax.set_ylabel( + vv['responsivity']['units'], + fontsize=fontsize, + fontweight='bold', + ) + + # ------------------- + # plot distribution + # ------------------- + + kax = 'dist' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for kk, vv in dcases.items(): + + ax.semilogy( + vv['E_e0']['data']*1e-3, + vv['dist']['data'], + c=vv['color'], + ls=vv.get('ls', '-'), + marker=vv.get('marker'), + lw=vv.get('lw', 1.), + label=kk, + ) + + ax.set_ylabel( + vv['dist']['units'], + fontsize=fontsize, + fontweight='bold', + ) + + return dax + + +# ############################################# +# ############################################# +# Axes for anisotropy +# ############################################# + + +def _check( + dcases=None, + fs=None, + fontsize=None, +): + + # ------------ + # dcases + # ------------ + + ddef = copy.deepcopy(_DCASES) + if dcases in [None, False]: + dcases = {} + else: + for k0, v0 in dcases.items(): + dcases[k0] = _check_case( + v0, + f"dcases['{k0}']", + ddef[list(ddef.keys())[0]], + ) + + # ------------ + # fs + # ------------ + + if fs is None: + fs = (15, 12) + + # ------------ + # fontsize + # ------------ + + fontsize = ds._generic_check._check_var( + fontsize, 'fontsize', + types=int, + default=14, + sign='>0', + ) + + return dcases, fs, fontsize + + +def _check_case( + case=None, + key=None, + ddef=None, +): + + # -------------- + # general structure + # -------------- + + dfail = {} + lok = list(ddef.keys()) + ltunits = (str, asunits.Unit, asunits.CompositeUnit) + for kk in lok: + if not isinstance(case.get(kk), dict): + typ = type(case.get(kk)) + dfail[kk] = f'absent or not a dict ({typ})' + elif not isinstance(case[kk].get('data'), np.ndarray): + typ = type(case[kk].get('data')) + dfail[kk] = f'data key not a np.ndarray ({typ})' + elif not isinstance(case[kk].get('units'), ltunits): + typ = type(case[kk].get('units')) + dfail[kk] = f"units not a str ({typ})" + else: + dfail[kk] = 'ok' + + if any([vv != 'ok' for vv in dfail.values()]): + lstr = [f"\t- {kk}: {vv}" for kk, vv in dfail.items()] + msg = ( + f"Arg {key} must be a dict with keys {lok}, " + "where each is a {'data': np.ndarray, 'units': str} subdict!\n" + + "\n".join(lstr) + ) + raise Exception(msg) + + # -------------- + # shape consistency + # -------------- + + shape_Eph = case['E_ph']['data'].shape + shape_resp = case['responsivity']['data'].shape + if shape_Eph != shape_resp: + msg = ( + "The 2 fields below must have the same shape:\n" + f"{key}['E_ph']['data'].shape = {shape_Eph}\n" + f"{key}['responsivity']['data'].shape = {shape_resp}\n" + ) + raise Exception(msg) + + shape_Ee0 = case['E_e0']['data'].shape + shape_dist = case['dist']['data'].shape + if shape_Ee0 != shape_dist: + msg = ( + "The 2 fields below must have the same shape:\n" + f"{key}['E_e0']['data'].shape = {shape_Ee0}\n" + f"{key}['dist']['data'].shape = {shape_dist}\n" + ) + raise Exception(msg) + + return case + + +# ############################################# +# ############################################# +# Axes for anisotropy +# ############################################# + + +def _dax( + Z=None, + version=None, + fs=None, + fontsize=None, +): + + # -------------- + # prepare figure + # -------------- + + tit = ( + "Thin-target Bremsstrahlung emission anisotropy" + ) + + dmargin = { + 'left': 0.06, 'right': 0.95, + 'bottom': 0.06, 'top': 0.90, + 'wspace': 0.20, 'hspace': 0.20, + } + + fig = plt.figure(figsize=(15, 12)) + fig.suptitle(tit, size=fontsize+2, fontweight='bold') + + nh0, nh1, nh2 = 2, 5, 4 + nv0, nv1, nv2 = 3, 1, 2 + gs = gridspec.GridSpec( + ncols=nh0+nh1+nh2 + 1, + nrows=nv0 + nv1, + **dmargin, + ) + dax = {} + + # -------------- + # prepare axes + # -------------- + + # -------------- + # ax - isolines + + ax = fig.add_subplot(gs[:nv0, nh0:nh0+nh1], xscale='log') + ax.set_title( + r"$d^2\sigma(E_{e0}, E_{ph}, \theta_{ph}, Z)$" + + f"\n Z = {Z}, version = {version}", + size=fontsize, + fontweight='bold', + ) + + # store + dax['map'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - responsivity + + ax = fig.add_subplot(gs[:nv0, :nh0], sharey=dax['map']['handle']) + ax.set_title( + "responsivity", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + r"$E_{ph}$ (keV)", + size=fontsize, + fontweight='bold', + ) + ax.grid(True) + + # store + dax['responsivity'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - dist + + ax = fig.add_subplot(gs[nv0:, nh0:nh0 + nh1], sharex=dax['map']['handle']) + ax.set_xlabel( + r"$E_{e,0}$ (keV)", + size=fontsize, + fontweight='bold', + ) + ax.grid(True) + + # store + dax['dist'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - theta + + ax = fig.add_subplot(gs[:nv2, nh0 + nh1 + 1:]) + ax.set_xlabel( + r"$\theta_{ph}^B$ (deg)", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + "emiss (ph/sr/s/m3)", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta'] = {'handle': ax, 'type': 'isolines'} + + return dax From da008866be893e1b8d6b53b06fa4dfe7b652fa3f Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 17:21:17 +0000 Subject: [PATCH 06/80] [#1166] minor cleanup --- .../_xray_thin_target_integrated_dist_responsivity_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index c6b93ac8f..ef4862d0e 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -171,7 +171,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( ) dcases[kk]['color'] = l0.get_color() - ax.set_ylabel( + ax.set_xlabel( vv['responsivity']['units'], fontsize=fontsize, fontweight='bold', From 86e2d48f7d09eb03b3e6b29c29a5eb5fa2546323 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 17:22:13 +0000 Subject: [PATCH 07/80] [#1166] d2cross loop on E_e0, E_ph and theta_ph => too slow --- .../emission/_xray_thin_target_integrated.py | 128 +++++++++--------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 774564980..264b048e8 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -126,7 +126,7 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( ( E_e0_eV, E_ph_eV, theta_ph, nthetae, ndphi, - shape, shape_theta_e, shape_dphi, + shape, verb, verb_tab, save, pfe_save, overwrite, ) = _check( @@ -233,13 +233,9 @@ def _check( theta_ph=theta_ph, ) - # ----------- - # shapes - # ----------- - - shape = np.broadcast_shapes(E_e0_eV.shape, E_ph_eV.shape, theta_ph.shape) - shape_theta_e = (1,) * (len(shape)+1) + (-1,) - shape_dphi = (1,) * len(shape) + (-1, 1) + E_e0_eV, E_ph_eV, theta_ph = np.broadcast_arrays( + E_e0_eV, E_ph_eV, theta_ph, + ) # ----------- # integers @@ -339,7 +335,7 @@ def _check( return ( E_e0_eV, E_ph_eV, theta_ph, nthetae, ndphi, - shape, shape_theta_e, shape_dphi, + shape, verb, verb_tab, save, pfe_save, overwrite, ) @@ -359,10 +355,7 @@ def _compute( theta_ph=None, # shapes nthetae=None, - shape_theta_e=None, - theta_ef=None, ndphi=None, - shape_dphi=None, # parameters ninf=None, source=None, @@ -386,40 +379,12 @@ def _compute( # angles theta_e = np.pi * np.linspace(0, 1, nthetae) dphi = np.pi * np.linspace(-1, 1, ndphi) - theta_ef = theta_e.reshape(shape_theta_e) - dphif = dphi.reshape(shape_dphi) + theta_ef = np.broadcast_to(theta_e[None, :], (ndphi, nthetae)) + dphif = np.broadcast_to(dphi[:, None], (ndphi, nthetae)) # derived sinte = np.sin(theta_ef) - # ------------------ - # get d3cross - # ------------------ - - if verb >= 1: - msg = f"{verb_tab}Computing d3cross for shape {shape}... " - print(msg) - - d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( - # inputs - Z=Z, - E_e0_eV=E_e0_eV[..., None, None], - E_e1_eV=E_e1_eV[..., None, None], - # directions - theta_ph=theta_ph[..., None, None], - theta_e=theta_ef, - dphi=dphif, - # hypergeometric parameter - ninf=ninf, - source=source, - # output customization - per_energy_unit=per_energy_unit, - # version - version=version, - # debug - debug=False, - ) - # ------------------ # prepare output # ------------------ @@ -429,34 +394,73 @@ def _compute( kv = f"{kk}_{vv['units']}" if vv['units'] == 'eV' else kk d2cross[kk]['data'] = eval(kv) - # cross-sections - d2cross['cross'] = { - vv: { - 'data': np.full(shape, 0.), - 'units': asunits.Unit(vcross['units']) * asunits.Unit('sr'), - } - for vv, vcross in d3cross['cross'].items() - } - # ------------------ - # integrate + # get d3cross # ------------------ if verb >= 1: - msg = f"{verb_tab}Integrating..." + msg = f"{verb_tab}Computing d3cross for shape {shape}... " print(msg) - for vv, vcross in d3cross['cross'].items(): - d2cross['cross'][vv]['data'][...] = scpinteg.trapezoid( - scpinteg.trapezoid( - vcross['data'] * sinte, - x=theta_e, - axis=-1, - ), - x=dphi, - axis=-1, + # ------------------------------- + # loop on all but phi and theta_e + + size = np.prod(shape) + for ii, ind in enumerate(np.ndindex(shape)): + + # verb + if verb >= 2: + end = '\n' if ii == size - 1 else '\r' + msg = f"\t{ii+1} / {size}, index {ind} / {shape}" + print(msg, end=end) + + # sli + sli = ind + (None, None) + + # d3cross + d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( + # inputs + Z=Z, + E_e0_eV=E_e0_eV[sli], + E_e1_eV=E_e1_eV[sli], + # directions + theta_ph=theta_ph[sli], + theta_e=theta_ef, + dphi=dphif, + # hypergeometric parameter + ninf=ninf, + source=source, + # output customization + per_energy_unit=per_energy_unit, + # version + version=version, + # debug + debug=False, ) + if ii == 0: + srunits = asunits.Unit('sr') + # cross-sections + d2cross['cross'] = { + vv: { + 'data': np.full(shape, 0.), + 'units': asunits.Unit(vcross['units']) * srunits, + } + for vv, vcross in d3cross['cross'].items() + } + + # integrate of theta_e + for vv, vcross in d3cross['cross'].items(): + d2cross['cross'][vv]['data'][ind] = scpinteg.trapezoid( + scpinteg.trapezoid( + vcross['data'] * sinte, + x=theta_e, + axis=-1, + ), + x=dphi, + axis=-1, + ) + return d2cross From c1b3b1b8103b5f91cae14df52c1bb88618e8a932 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 19:33:02 +0000 Subject: [PATCH 08/80] [#1166] d2cross now looping over just the largest dimension (no angle) --- .../emission/_xray_thin_target_integrated.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 264b048e8..1023bf112 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -379,8 +379,19 @@ def _compute( # angles theta_e = np.pi * np.linspace(0, 1, nthetae) dphi = np.pi * np.linspace(-1, 1, ndphi) - theta_ef = np.broadcast_to(theta_e[None, :], (ndphi, nthetae)) - dphif = np.broadcast_to(dphi[:, None], (ndphi, nthetae)) + + # --------------------- + # determine how to loop + # --------------------- + + # loop on largest dimension + iloop = np.argmax(shape) + sli = np.array([slice(None)]*len(shape) + [None, None]) + slistr = [':'] * (len(shape) + 2) + + sli_ang = (None,) * (len(shape) - 1) + (slice(None),)*2 + theta_ef = theta_e[None, :][sli_ang] + dphif = dphi[:, None][sli_ang] # derived sinte = np.sin(theta_ef) @@ -405,26 +416,28 @@ def _compute( # ------------------------------- # loop on all but phi and theta_e - size = np.prod(shape) - for ii, ind in enumerate(np.ndindex(shape)): + size = shape[iloop] + for ii in range(size): + + # sli + sli[iloop] = ii + slit = tuple(sli) # verb if verb >= 2: + slistr[iloop] = str(ii) end = '\n' if ii == size - 1 else '\r' - msg = f"\t{ii+1} / {size}, index {ind} / {shape}" + msg = f"\t{ii+1} / {size}, index ({', '.join(slistr)}) / {shape}" print(msg, end=end) - # sli - sli = ind + (None, None) - # d3cross d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( # inputs Z=Z, - E_e0_eV=E_e0_eV[sli], - E_e1_eV=E_e1_eV[sli], + E_e0_eV=E_e0_eV[slit], + E_e1_eV=E_e1_eV[slit], # directions - theta_ph=theta_ph[sli], + theta_ph=theta_ph[slit], theta_e=theta_ef, dphi=dphif, # hypergeometric parameter @@ -451,7 +464,7 @@ def _compute( # integrate of theta_e for vv, vcross in d3cross['cross'].items(): - d2cross['cross'][vv]['data'][ind] = scpinteg.trapezoid( + d2cross['cross'][vv]['data'][slit[:-2]] = scpinteg.trapezoid( scpinteg.trapezoid( vcross['data'] * sinte, x=theta_e, From 75c8262394eefc3f21f6306f9b3f923b0bbe9bc9 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 19:33:26 +0000 Subject: [PATCH 09/80] [#1166] d3cross trying minor optimization --- .../electrons/emission/_xray_thin_target.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index 0b286a8a5..683868ac3 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -897,7 +897,13 @@ def _angle_dependent_internediates( sintp = np.sin(theta_ph) cosdphi = np.cos(dphi) - cossindphi = costp*coste + sintp*sinte*cosdphi + costpcoste = costp * coste + sintpsintecosdphi = sintp * sinte * cosdphi + + cossindphi = costpcoste + sintpsintecosdphi + + sintecostp = sinte * costp + costesintp = coste * sintp # ------------- # Vectors / scalar product @@ -905,15 +911,15 @@ def _angle_dependent_internediates( # scalar sca_kp0 = kk * p0 * costp - sca_p01 = p1 * p0 * coste + sca_p01 = p0 * p1 * coste sca_kp1 = kk * p1 * cossindphi # vect{q} = vect{p0 - p1 - k} q2 = ( p0**2 + p1**2 + k2 - - 2.*p0*p1*coste - - 2.*p0*kk*costp - + 2.*p1*kk*cossindphi + - 2. * sca_p01 + - 2. * sca_kp0 + + 2. * sca_kp1 ) # ------------- @@ -951,9 +957,9 @@ def _angle_dependent_internediates( # + (sinte*sintp)**2 * sin(phi_p - phi_e)**2 # ) eta12 = p12 * ( - (sinte*costp)**2 - + (coste*sintp)**2 - - 2*(coste*costp)*(sinte*sintp*cosdphi) + sintecostp**2 + + costesintp**2 + - 2 * costpcoste * sintpsintecosdphi + (sinte*sintp)**2 * (1 - cosdphi**2) ) @@ -972,7 +978,7 @@ def _angle_dependent_internediates( # coste*sintp*(sinpp**2 + cospp**2) # - sinte*costp*(sinpe*sinpp + cospe*cospp) # ) - sca_eta01 = p0 * p1 * sintp * (coste*sintp - sinte*costp*cosdphi) + sca_eta01 = p0 * p1 * sintp * (costesintp - sintecostp * cosdphi) # --------------- # Intermediates 1 From 9fa3adc695e585a946d118b6bd9fe75f5403dc94 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 22:24:03 +0000 Subject: [PATCH 10/80] [#1166] plot() of d2cross and d2cross filtered now take custom x/y scales and verb --- ...arget_integrated_dist_responsivity_plot.py | 108 ++++++++++++++++-- .../_xray_thin_target_integrated_plot.py | 93 +++++++++++---- 2 files changed, 169 insertions(+), 32 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index ef4862d0e..e50590e8d 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -33,6 +33,16 @@ _VERSION = 'BHE' +# DSCALES +_DSCALES = { + 'E_ph': 'log', + 'E_e0': 'log', + 'theta': 'linear', + 'dist': 'log', + 'resp': 'log', +} + + # DCASES _DCASES = { 'cvd no filter maxwell': { @@ -68,10 +78,17 @@ def plot_xray_thin_integ_dist_filter_anisotropy( version: Optional[str] = None, # selected cases dcases: Optional[dict[int, dict]] = None, + # verb + verb: Optional[bool] = None, # plot dax: Optional[dict] = None, fs: Optional[tuple] = None, fontsize: Optional[int] = None, + E_e0_scale: Optional[str] = None, + E_ph_scale: Optional[str] = None, + dist_scale: Optional[str] = None, + resp_scale: Optional[str] = None, + theta_scale: Optional[str] = None, dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, @@ -96,11 +113,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # check inputs # --------------- - dcases, fs, fontsize = _check( - dcases=dcases, - fs=fs, - fontsize=fontsize, - ) + dcases, dscales, fs, fontsize = _check(**locals()) # --------------- # prepare data @@ -116,6 +129,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( version=version, fs=fs, fontsize=fontsize, + dscales=dscales, ) dax = ds._generic_check._check_dax(dax) @@ -142,6 +156,8 @@ def plot_xray_thin_integ_dist_filter_anisotropy( version=version, # selected cases dcases=False, + # verb + verb=verb, # plot dax=dax, dplot_forbidden=dplot_forbidden, @@ -216,6 +232,13 @@ def _check( dcases=None, fs=None, fontsize=None, + E_e0_scale=None, + E_ph_scale=None, + dist_scale=None, + resp_scale=None, + theta_scale=None, + # unused + **kwdargs, ): # ------------ @@ -251,7 +274,29 @@ def _check( sign='>0', ) - return dcases, fs, fontsize + # ------------ + # scales + # ------------ + + dscales = { + 'E_ph': E_ph_scale, + 'E_e0': E_e0_scale, + 'theta': theta_scale, + 'dist': dist_scale, + 'resp': resp_scale, + } + + for kk, vv in dscales.items(): + dscales[kk] = ds._generic_check._check_var( + vv, f'{kk}_scale', + types=str, + allowed=['log', 'linear'], + default=_DSCALES[kk], + ) + + return ( + dcases, dscales, fs, fontsize, + ) def _check_case( @@ -327,6 +372,7 @@ def _dax( version=None, fs=None, fontsize=None, + dscales=None, ): # -------------- @@ -362,7 +408,11 @@ def _dax( # -------------- # ax - isolines - ax = fig.add_subplot(gs[:nv0, nh0:nh0+nh1], xscale='log') + ax = fig.add_subplot( + gs[:nv0, nh0:nh0+nh1], + xscale=dscales['E_e0'], + yscale=dscales['E_ph'], + ) ax.set_title( r"$d^2\sigma(E_{e0}, E_{ph}, \theta_{ph}, Z)$" + f"\n Z = {Z}, version = {version}", @@ -376,7 +426,11 @@ def _dax( # -------------- # ax - responsivity - ax = fig.add_subplot(gs[:nv0, :nh0], sharey=dax['map']['handle']) + ax = fig.add_subplot( + gs[:nv0, :nh0], + sharey=dax['map']['handle'], + xscale=dscales['resp'], + ) ax.set_title( "responsivity", size=fontsize, @@ -395,7 +449,11 @@ def _dax( # -------------- # ax - dist - ax = fig.add_subplot(gs[nv0:, nh0:nh0 + nh1], sharex=dax['map']['handle']) + ax = fig.add_subplot( + gs[nv0:, nh0:nh0 + nh1], + sharex=dax['map']['handle'], + yscale=dscales['dist'], + ) ax.set_xlabel( r"$E_{e,0}$ (keV)", size=fontsize, @@ -407,9 +465,35 @@ def _dax( dax['dist'] = {'handle': ax, 'type': 'isolines'} # -------------- - # ax - theta + # ax - theta - norm + + ax = fig.add_subplot( + gs[:nv2, nh0 + nh1 + 1:], + xscale=dscales['theta'], + yscale='linear', + ) + ax.set_xlabel( + r"$\theta_{ph}^B$ (deg)", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + "emiss (ph/sr/s/m3)", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta_norm'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - theta - abs - ax = fig.add_subplot(gs[:nv2, nh0 + nh1 + 1:]) + ax = fig.add_subplot( + gs[nv2:, nh0 + nh1 + 1:], + sharex=dax['theta_norm']['handle'], + yscale='log', + ) ax.set_xlabel( r"$\theta_{ph}^B$ (deg)", size=fontsize, @@ -422,6 +506,6 @@ def _dax( ) # store - dax['theta'] = {'handle': ax, 'type': 'isolines'} + dax['theta_abs'] = {'handle': ax, 'type': 'isolines'} return dax diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index f3d66b76a..289485b0a 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -32,6 +32,14 @@ _VERSION = 'BHE' +# DSCALES +_DSCALES = { + 'E_ph': 'linear', + 'E_e0': 'log', + 'theta': 'linear', +} + + # ANISOTROPY CASES _DCASES = { 0: { @@ -103,9 +111,14 @@ def plot_xray_thin_d2cross_ei_anisotropy( version: Optional[str] = None, # selected cases dcases: Optional[dict[int, dict]] = None, + # verb + verb: Optional[bool] = None, # plot dax: Optional[dict] = None, fontsize: Optional[int] = None, + E_e0_scale: Optional[str] = None, + E_ph_scale: Optional[str] = None, + theta_scale: Optional[str] = None, dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, @@ -134,22 +147,11 @@ def plot_xray_thin_d2cross_ei_anisotropy( E_e0_eV, E_ph_eV, theta_ph, version, dcases, + dscales, + verb, fontsize, dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_mean, - ) = _check_anisotropy( - E_e0_eV=E_e0_eV, - E_ph_eV=E_ph_eV, - theta_ph=theta_ph, - version=version, - # selected cases - dcases=dcases, - # plotting - fontsize=fontsize, - dplot_forbidden=dplot_forbidden, - dplot_peaking=dplot_peaking, - dplot_thetamax=dplot_thetamax, - dplot_mean=dplot_mean, - ) + ) = _check_anisotropy(**locals()) # --------------- # prepare data @@ -170,7 +172,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( ninf=ninf, source=source, # verb - verb=False, + verb=verb, ) # -------------- @@ -178,10 +180,11 @@ def plot_xray_thin_d2cross_ei_anisotropy( # -------------- if dax is None: - dax = _get_axes_anisotropy( + dax = _dax( Z=Z, version=version, fontsize=fontsize, + dscales=dscales, ) dax = ds._generic_check._check_dax(dax) @@ -388,12 +391,20 @@ def _check_anisotropy( version=None, # selected cases dcases=None, + # verb + verb=None, + # scales + E_e0_scale=None, + E_ph_scale=None, + theta_scale=None, # plotting fontsize=None, dplot_forbidden=None, dplot_peaking=None, dplot_thetamax=None, dplot_mean=None, + # unused + **kwdargs, ): # E_e0_eV @@ -458,6 +469,16 @@ def _check_anisotropy( else: dcases = {} + # ----------- + # verb + # ----------- + + verb = ds._generic_check._check_var( + verb, 'verb', + types=(bool, int), + default=False, + ) + # ------------ # plotting # ------------ @@ -503,10 +524,30 @@ def _check_anisotropy( ddef, ) + # ------------ + # scales + # ------------ + + dscales = { + 'E_ph': E_ph_scale, + 'E_e0': E_e0_scale, + 'theta': theta_scale, + } + + for kk, vv in dscales.items(): + dscales[kk] = ds._generic_check._check_var( + vv, f'{kk}_scale', + types=str, + allowed=['log', 'linear'], + default=_DSCALES[kk], + ) + return ( E_e0_eV, E_ph_eV, theta_ph, version, dcases, + dscales, + verb, fontsize, dplot_forbidden, dplot_peaking, dplot_thetamax, dplot_mean, ) @@ -587,10 +628,12 @@ def _get_peaking(data, x, axis=None): # ############################################# -def _get_axes_anisotropy( +def _dax( Z=None, version=None, fontsize=None, + dax=None, + dscales=None, ): tit = ( @@ -616,7 +659,11 @@ def _get_axes_anisotropy( # -------------- # ax - isolines - ax = fig.add_subplot(gs[:, 0], xscale='log') + ax = fig.add_subplot( + gs[:, 0], + xscale=dscales['E_e0'], + yscale=dscales['E_ph'], + ) ax.set_xlabel( r"$E_{e,0}$ (keV)", size=fontsize, @@ -640,7 +687,10 @@ def _get_axes_anisotropy( # -------------- # ax - norm - ax = fig.add_subplot(gs[0, 1]) + ax = fig.add_subplot( + gs[0, 1], + xscale=dscales['theta'], + ) ax.set_xlabel( r"$\theta_{ph}$ (deg)", size=fontsize, @@ -663,7 +713,10 @@ def _get_axes_anisotropy( # -------------- # ax - log - ax = fig.add_subplot(gs[1, 1], sharex=dax['norm']['handle']) + ax = fig.add_subplot( + gs[1, 1], + sharex=dax['norm']['handle'], + ) ax.set_xlabel( r"$\theta_{ph}$ (deg)", size=fontsize, From e854e797b0417119036687e04ebcd53eee7a34e6 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 22:24:33 +0000 Subject: [PATCH 11/80] [#1166] default version now at top of file --- tofu/physics_tools/electrons/emission/_xray_thin_target.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index 683868ac3..215cea37a 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -25,6 +25,10 @@ _PATH_HERE = os.path.dirname(__file__) +# version +_VERSION = 'EH' # most accurate, but slow + + # #################################################### # #################################################### # Differential cross-section @@ -360,7 +364,7 @@ def _check_cross( # ------------ if version is None: - version = 'EH' + version = _VERSION if isinstance(version, str): version = [version] From 8cbc7d9c9e2d0343b8456d8c82a158a868550cfe Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 22:25:06 +0000 Subject: [PATCH 12/80] [#1166] Better d2cross saving --- .../emission/_xray_thin_target_integrated.py | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 1023bf112..4e5a54f7c 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -38,6 +38,9 @@ _NTHETAE = 31 _NDPHI = 51 +# VERSION +_VERSION = 'BHE' # good compromise + # Default naming _DSCALE = { @@ -126,25 +129,11 @@ def get_xray_thin_d2cross_ei_integrated_thetae_dphi( ( E_e0_eV, E_ph_eV, theta_ph, nthetae, ndphi, + version, shape, verb, verb_tab, save, pfe_save, overwrite, - ) = _check( - # inputs - E_e0_eV=E_e0_eV, - E_ph_eV=E_ph_eV, - theta_ph=theta_ph, - # integration parameters - nthetae=nthetae, - ndphi=ndphi, - # verb - verb=verb, - verb_tab=verb_tab, - # save - save=save, - pfe_save=pfe_save, - overwrite=overwrite, - ) + ) = _check(**locals()) # ------------------ # compute @@ -188,6 +177,7 @@ def _check( # integration parameters nthetae=None, ndphi=None, + version=None, # verb verb=None, verb_tab=None, @@ -195,6 +185,8 @@ def _check( save=None, pfe_save=None, overwrite=None, + # unused + **kwdargs, ): # ----------- @@ -233,10 +225,6 @@ def _check( theta_ph=theta_ph, ) - E_e0_eV, E_ph_eV, theta_ph = np.broadcast_arrays( - E_e0_eV, E_ph_eV, theta_ph, - ) - # ----------- # integers # ----------- @@ -257,6 +245,22 @@ def _check( default=_NDPHI, ) + # ------------ + # version + # ------------ + + if version is None: + version = _VERSION + if isinstance(version, str): + version = [version] + + version = ds._generic_check._check_var_iter( + version, 'version', + types=(list, tuple), + types_iter=str, + allowed=['EH', 'BH', 'BHE'], + ) + # ----------- # verb # ----------- @@ -335,6 +339,7 @@ def _check( return ( E_e0_eV, E_ph_eV, theta_ph, nthetae, ndphi, + version, shape, verb, verb_tab, save, pfe_save, overwrite, @@ -363,6 +368,7 @@ def _compute( per_energy_unit=None, # misc shape=None, + daxis=None, verb=None, verb_tab=None, # unused @@ -387,6 +393,9 @@ def _compute( # loop on largest dimension iloop = np.argmax(shape) sli = np.array([slice(None)]*len(shape) + [None, None]) + sli_Ee0 = np.copy(sli) + sli_Ee1 = np.copy(sli) + sli_theta = np.copy(sli) slistr = [':'] * (len(shape) + 2) sli_ang = (None,) * (len(shape) - 1) + (slice(None),)*2 @@ -405,6 +414,10 @@ def _compute( kv = f"{kk}_{vv['units']}" if vv['units'] == 'eV' else kk d2cross[kk]['data'] = eval(kv) + # cross-sections + d2cross['cross'] = {vv: {'data': np.full(shape, 0.)} for vv in version} + srunits = asunits.Unit('sr') + # ------------------ # get d3cross # ------------------ @@ -421,7 +434,9 @@ def _compute( # sli sli[iloop] = ii - slit = tuple(sli) + sli_Ee0[iloop] = min(ii, E_e0_eV.shape[iloop]-1) + sli_Ee1[iloop] = min(ii, E_e1_eV.shape[iloop]-1) + sli_theta[iloop] = min(ii, theta_ph.shape[iloop]-1) # verb if verb >= 2: @@ -434,10 +449,10 @@ def _compute( d3cross = _xray_thin_target.get_xray_thin_d3cross_ei( # inputs Z=Z, - E_e0_eV=E_e0_eV[slit], - E_e1_eV=E_e1_eV[slit], + E_e0_eV=E_e0_eV[tuple(sli_Ee0)], + E_e1_eV=E_e1_eV[tuple(sli_Ee1)], # directions - theta_ph=theta_ph[slit], + theta_ph=theta_ph[tuple(sli_theta)], theta_e=theta_ef, dphi=dphif, # hypergeometric parameter @@ -451,20 +466,9 @@ def _compute( debug=False, ) - if ii == 0: - srunits = asunits.Unit('sr') - # cross-sections - d2cross['cross'] = { - vv: { - 'data': np.full(shape, 0.), - 'units': asunits.Unit(vcross['units']) * srunits, - } - for vv, vcross in d3cross['cross'].items() - } - # integrate of theta_e for vv, vcross in d3cross['cross'].items(): - d2cross['cross'][vv]['data'][slit[:-2]] = scpinteg.trapezoid( + d2cross['cross'][vv]['data'][tuple(sli[:-2])] = scpinteg.trapezoid( scpinteg.trapezoid( vcross['data'] * sinte, x=theta_e, @@ -473,6 +477,9 @@ def _compute( x=dphi, axis=-1, ) + d2cross['cross'][vv]['units'] = ( + srunits * asunits.Unit(vcross['units']) + ) return d2cross @@ -509,7 +516,7 @@ def _save( # extract boundaries path = os.path.abspath(_PATH_HERE) - fname = f"d2cross_Ee0{Ee0}_Eph{Eph}_ntheta{ntheta}.npz" + fname = f"d2cross_Ee0{Ee0}_Eph{Eph}_ntheta{ntheta}" pfe_save = os.path.join(path, f"{fname}.npz") # ---------- @@ -538,8 +545,8 @@ def _save( # verb # ---------- - if verb is True: - msg = "Saved d2cross in:\n\t{pfe_save}\n" + if verb >= 1: + msg = f"Saved d2cross in:\n\t{pfe_save}\n" print(msg) return @@ -565,17 +572,20 @@ def _format_vect2str(vect, base='eV'): # test scale # ------------- - dlog = np.diff(np.log(vect)) - dlin = np.diff(vect) - islog = np.allclose(dlog, dlog[0]) - islin = np.allclose(dlin, dlin[0]) + if np.max(vect.shape) == vect.size: + dlog = np.diff(np.log(vect)) + dlin = np.diff(vect) + islog = np.allclose(dlog, dlog[0]) + islin = np.allclose(dlin, dlin[0]) - if islog is True: - scale = 'log' - elif islin is True: - scale = 'lin' + if islog is True: + scale = 'log' + elif islin is True: + scale = 'lin' + else: + scale = 'pts' else: - scale = '' + scale = 'pts' # ------------- # format From da504ffbb0b3f1acf176dedc41f250fba4a1e643 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 9 Jan 2026 22:54:44 +0000 Subject: [PATCH 13/80] [#1166] Almost there loading d2cross --- .../emission/_xray_thin_target_integrated.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index 4e5a54f7c..ff9fcd339 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -441,8 +441,10 @@ def _compute( # verb if verb >= 2: slistr[iloop] = str(ii) + str0 = ', '.join(slistr) + str1 = str(shape + (None, None)) end = '\n' if ii == size - 1 else '\r' - msg = f"\t{ii+1} / {size}, index ({', '.join(slistr)}) / {shape}" + msg = f"\t{ii+1} / {size}, index ({str0}) / {str1}" print(msg, end=end) # d3cross @@ -630,7 +632,10 @@ def _load( ) raise Exception(msg) - d2cross = dict(np.load(d2cross, allow_pickle=True)) + d2cross = { + kk: vv.tolist() + for kk, vv in np.load(d2cross, allow_pickle=True).items() + } # --------- # dict @@ -669,13 +674,14 @@ def _load( # ---------------- lok = ['BHE', 'BH', 'EH'] + typunits = (str, asunits.Unit, asunits.CompositeUnit) c0 = ( isinstance(d2cross.get('cross'), dict) and all([ kk in lok and isinstance(vv, dict) and isinstance(vv.get('data'), np.ndarray) - and isinstance(vv.get('data'), str) + and isinstance(vv.get('units'), typunits) for kk, vv in d2cross['cross'].items() ]) ) From ea1c4ddad02ac96f82100a67310c9194c499c84b Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 12 Jan 2026 16:00:26 +0000 Subject: [PATCH 14/80] [#1166] plot_xray_thin_d2cross_ei_anisotropy(d2cross=pfe, dcases=str) operational --- .../_xray_thin_target_integrated_cases.py | 72 ++++++ .../_xray_thin_target_integrated_plot.py | 211 ++++++++++-------- 2 files changed, 193 insertions(+), 90 deletions(-) create mode 100644 tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py new file mode 100644 index 000000000..fbbb1f285 --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py @@ -0,0 +1,72 @@ + +# ANISOTROPY CASES +_DCASES_PRE = { + + # ------------- + # standard span + + 'standard': { + 0: { + 'E_e0_eV': 20e3, + 'E_ph_eV': 10e3, + 'color': 'r', + 'marker': '*', + 'ms': 14, + }, + 1: { + 'E_e0_eV': 100e3, + 'E_ph_eV': 50e3, + 'color': 'c', + 'marker': '*', + 'ms': 14, + }, + 2: { + 'E_e0_eV': 100e3, + 'E_ph_eV': 10e3, + 'color': 'm', + 'marker': '*', + 'ms': 14, + }, + 3: { + 'E_e0_eV': 1000e3, + 'E_ph_eV': 10e3, + 'color': (0.8, 0.8, 0), + 'marker': '*', + 'ms': 14, + }, + 4: { + 'E_e0_eV': 10000e3, + 'E_ph_eV': 10e3, + 'color': (0., 0.8, 0.8), + 'marker': '*', + 'ms': 14, + }, + 5: { + 'E_e0_eV': 1000e3, + 'E_ph_eV': 50e3, + 'color': (0.8, 0., 0.8), + 'marker': '*', + 'ms': 14, + }, + }, + + # ------------------ + # large span + + 'span_100eV_10MeV': { + 0: { + 'E_e0_eV': 20e3, + 'E_ph_eV': 10e3, + 'color': 'r', + 'marker': '*', + 'ms': 14, + }, + 1: { + 'E_e0_eV': 100e3, + 'E_ph_eV': 50e3, + 'color': 'c', + 'marker': '*', + 'ms': 14, + }, + }, +} diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 289485b0a..ed908aa22 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -14,6 +14,7 @@ from . import _xray_thin_target_integrated as _mod +from ._xray_thin_target_integrated_cases import _DCASES_PRE TupleDict = tuple[dict] @@ -34,59 +35,26 @@ # DSCALES _DSCALES = { - 'E_ph': 'linear', + 'E_ph': 'log', 'E_e0': 'log', 'theta': 'linear', } -# ANISOTROPY CASES -_DCASES = { - 0: { - 'E_e0_eV': 20e3, - 'E_ph_eV': 10e3, - 'color': 'r', - 'marker': '*', - 'ms': 14, - }, - 1: { - 'E_e0_eV': 100e3, - 'E_ph_eV': 50e3, - 'color': 'c', - 'marker': '*', - 'ms': 14, - }, - 2: { - 'E_e0_eV': 100e3, - 'E_ph_eV': 10e3, - 'color': 'm', - 'marker': '*', - 'ms': 14, - }, - 3: { - 'E_e0_eV': 1000e3, - 'E_ph_eV': 10e3, - 'color': (0.8, 0.8, 0), - 'marker': '*', - 'ms': 14, - }, - 4: { - 'E_e0_eV': 10000e3, - 'E_ph_eV': 10e3, - 'color': (0., 0.8, 0.8), - 'marker': '*', - 'ms': 14, - }, - 5: { - 'E_e0_eV': 1000e3, - 'E_ph_eV': 50e3, - 'color': (0.8, 0., 0.8), - 'marker': '*', - 'ms': 14, - }, +# ANISOTROPY CASES FORMATTING +_DCASES_FORMAT = { + 'E_e0_eV': (int, float), + 'E_ph_eV': (int, float), + 'color': (str, tuple), + 'marker': str, + 'ms': (int, float), } +# ANISOTROPY CASES DEFAULT +_DCASES_CASE = 'standard' + + # #################################################### # #################################################### # plot anisotropy @@ -146,7 +114,6 @@ def plot_xray_thin_d2cross_ei_anisotropy( ( E_e0_eV, E_ph_eV, theta_ph, version, - dcases, dscales, verb, fontsize, @@ -175,6 +142,22 @@ def plot_xray_thin_d2cross_ei_anisotropy( verb=verb, ) + # ------------------- + # update from d2cross + # ------------------- + + # if d2cross was provided + theta_ph = d2cross['theta_ph']['data'].ravel() + E_ph_eV = d2cross['E_ph']['data'].ravel() + E_e0_eV = d2cross['E_e0']['data'].ravel() + + # dcases + dcases = _check_dcases( + dcases=dcases, + E_e0_eV=E_e0_eV, + E_ph_eV=E_ph_eV, + ) + # -------------- # prepare axes # -------------- @@ -205,6 +188,10 @@ def plot_xray_thin_d2cross_ei_anisotropy( theta_ph*180/np.pi, axis=0, ) + mean_log10 = np.full(mean.shape, np.nan) + iok = np.isfinite(mean) + iok[iok] = mean[iok] > 0. + mean_log10[iok] = np.log10(mean[iok]) mean_units = vv['units'] # integral @@ -212,7 +199,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( im0 = ax.contour( E_e0_eV * 1e-3, E_ph_eV * 1e-3, - np.log10(mean).T, + mean_log10.T, levels=dplot_mean['levels'], colors=dplot_mean['colors'], ) @@ -315,7 +302,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( ) # limits - ax.set_ylim(0, ymax*1e-3) + if dscales['E_ph'] == 'linear': + ax.set_ylim(0, ymax*1e-3) # --------------- # plot - cases @@ -389,8 +377,6 @@ def _check_anisotropy( E_ph_eV=None, theta_ph=None, version=None, - # selected cases - dcases=None, # verb verb=None, # scales @@ -437,38 +423,6 @@ def _check_anisotropy( if version is None: version = _VERSION - # ------------ - # dcases - # ------------ - - ddef = copy.deepcopy(_DCASES) - if dcases in [None, True]: - dcases = ddef - - if dcases is not False: - for k0, v0 in dcases.items(): - dcases[k0] = _check_anisotropy_dplot( - v0, - f'dcases[{k0}]', - ddef[0], - ) - - # update with indices - ie = np.argmin(np.abs(E_e0_eV - dcases[k0]['E_e0_eV'])) - iph = np.argmin(np.abs(E_ph_eV - dcases[k0]['E_ph_eV'])) - dcases[k0].update({'ie': ie, 'iph': iph}) - - # update with label - ee0 = E_e0_eV[ie] - eph = E_ph_eV[iph] - dcases[k0]['lab'] = ( - r"$E_{e0} / E_{ph}$ = " - + f"{ee0*1e-3:3.0f} / {eph*1e-3:3.0f} keV = " - + f"{round(ee0 / eph, ndigits=1)}" - ) - else: - dcases = {} - # ----------- # verb # ----------- @@ -545,7 +499,6 @@ def _check_anisotropy( return ( E_e0_eV, E_ph_eV, theta_ph, version, - dcases, dscales, verb, fontsize, @@ -553,6 +506,69 @@ def _check_anisotropy( ) +def _check_dcases( + dcases=None, + E_e0_eV=None, + E_ph_eV=None, +): + + # -------------- + # dcases default + # -------------- + + ddef = copy.deepcopy(_DCASES_FORMAT) + if dcases in [None, True]: + dcases = _DCASES_CASE + + # -------------- + # dcases from predefined + # -------------- + + if isinstance(dcases, str): + lok = sorted(_DCASES_PRE.keys()) + if dcases not in lok: + lstr = [f"\t- {kk}" for kk in lok] + msg = ( + "Arg 'dcases' must be either:\n" + "\t- dict of cases\n" + "\t- a key to a predefined dict of cases\n" + "Available predefined dcases:\n" + + "\n".join(lstr) + ) + raise Exception(msg) + dcases = copy.deepcopy(_DCASES_PRE[dcases]) + + # -------------- + # generic check + # -------------- + + if dcases is not False: + for k0, v0 in dcases.items(): + dcases[k0] = _check_anisotropy_dplot( + v0, + f'dcases[{k0}]', + ddef, + ) + + # update with indices + ie = np.argmin(np.abs(E_e0_eV - dcases[k0]['E_e0_eV'])) + iph = np.argmin(np.abs(E_ph_eV - dcases[k0]['E_ph_eV'])) + dcases[k0].update({'ie': ie, 'iph': iph}) + + # update with label + ee0 = E_e0_eV[ie] + eph = E_ph_eV[iph] + dcases[k0]['lab'] = ( + r"$E_{e0} / E_{ph}$ = " + + f"{ee0*1e-3:3.0f} / {eph*1e-3:3.0f} keV = " + + f"{round(ee0 / eph, ndigits=1)}" + ) + else: + dcases = {} + + return dcases + + def _check_anisotropy_dplot(din, dname, ddef): # ------------- @@ -569,7 +585,11 @@ def _check_anisotropy_dplot(din, dname, ddef): if din is not False: c0 = ( isinstance(din, dict) - and all([kk in ddef.keys() for kk in din.keys()]) + and all([ + kk in ddef.keys() + and isinstance(din[kk], ddef[kk]) + for kk in din.keys() + ]) ) if not c0: lstr = [f"\t- ''{k0}': {v0}" for k0, v0 in ddef.items()] @@ -604,11 +624,18 @@ def _get_peaking(data, x, axis=None): # ---------- integ = scpinteg.trapezoid(data, x=x, axis=axis) - shape_integ = tuple([ - 1 if ii == axis else ss - for ii, ss in enumerate(data.shape) - ]) - data_n = data / integ.reshape(shape_integ) + shape_integ = list(data.shape) + shape_integ[axis] = 1 + + data_n = np.full(data.shape, np.nan) + iok = np.isfinite(integ) + iok[iok] = integ[iok] > 0 + iokn = iok.nonzero() + sli0 = list(iokn) + sli1 = list(iokn) + sli0.insert(axis, None) + sli1.insert(axis, slice(None)) + data_n[tuple(sli1)] = data[tuple(sli1)] / integ[tuple(sli0)] # ---------- # get average @@ -616,7 +643,11 @@ def _get_peaking(data, x, axis=None): shape_x = tuple([-1 if ii == axis else 1 for ii in range(data.ndim)]) xf = x.reshape(shape_x) - x_avf = scpinteg.simpson(data_n * xf, x=x, axis=axis).reshape(shape_integ) + x_avf = scpinteg.trapezoid( + data_n * xf, + x=x, + axis=axis, + ).reshape(shape_integ) std = np.sqrt(scpinteg.simpson(data_n * (xf - x_avf)**2, x=x, axis=axis)) return integ/180, 1/std From ae961214b0e5cefae1fa1f2f2b05bb09a68d1f31 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 12 Jan 2026 16:54:42 +0000 Subject: [PATCH 15/80] [#1166] plot_xray_thin_d2cross_ei_anisotropy() polished --- .../_xray_thin_target_integrated_cases.py | 58 +++++++++++++++---- .../_xray_thin_target_integrated_plot.py | 27 ++++++--- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py index fbbb1f285..59cb7c78b 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_cases.py @@ -53,20 +53,56 @@ # ------------------ # large span - 'span_100eV_10MeV': { + 'span_10eV_10MeV': { 0: { - 'E_e0_eV': 20e3, - 'E_ph_eV': 10e3, - 'color': 'r', - 'marker': '*', - 'ms': 14, + 'E_e0_eV': 100, + 'E_ph_eV': 50, + 'color': (1, 0, 0), }, 1: { - 'E_e0_eV': 100e3, - 'E_ph_eV': 50e3, - 'color': 'c', - 'marker': '*', - 'ms': 14, + 'E_e0_eV': 10e3, + 'E_ph_eV': 50, + 'color': (0.8, 0, 0), + }, + 2: { + 'E_e0_eV': 10e3, + 'E_ph_eV': 5e3, + 'color': (0, 1, 0), + }, + 3: { + 'E_e0_eV': 600e3, + 'E_ph_eV': 50, + 'color': (0.6, 0, 0), + }, + 4: { + 'E_e0_eV': 600e3, + 'E_ph_eV': 5e3, + 'color': (0, 0.75, 0), + }, + 5: { + 'E_e0_eV': 600e3, + 'E_ph_eV': 450e3, + 'color': (0, 0, 1), + }, + 6: { + 'E_e0_eV': 7e6, + 'E_ph_eV': 50, + 'color': (0.4, 0, 0), + }, + 7: { + 'E_e0_eV': 7e6, + 'E_ph_eV': 5e3, + 'color': (0, 0.5, 0), + }, + 8: { + 'E_e0_eV': 7e6, + 'E_ph_eV': 450e3, + 'color': (0, 0, 0.5), + }, + 9: { + 'E_e0_eV': 7e6, + 'E_ph_eV': 5.5e6, + 'color': 'y', }, }, } diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index ed908aa22..a36ff11d4 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -43,11 +43,12 @@ # ANISOTROPY CASES FORMATTING _DCASES_FORMAT = { - 'E_e0_eV': (int, float), - 'E_ph_eV': (int, float), - 'color': (str, tuple), - 'marker': str, - 'ms': (int, float), + 'E_e0_eV': {'type': (int, float), 'val': 1e3}, + 'E_ph_eV': {'type': (int, float), 'val': 10e3}, + 'color': {'type': (str, tuple), 'val': 'k'}, + 'marker': {'type': str, 'val': '*'}, + 'ms': {'type': (int, float), 'val': 18}, + 'ls': {'type': str, 'val': '-'}, } @@ -326,6 +327,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( theta_ph * 180/np.pi, yy / np.max(yy), c=vcase['color'], + ls=vcase['ls'], label=labi, ) @@ -338,6 +340,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( theta_ph * 180/np.pi, yy*1e28, c=vcase['color'], + ls=vcase['ls'], label=labi, ) @@ -587,12 +590,16 @@ def _check_anisotropy_dplot(din, dname, ddef): isinstance(din, dict) and all([ kk in ddef.keys() - and isinstance(din[kk], ddef[kk]) + and ( + isinstance(ddef[kk], dict) + and ddef[kk].get('type') is not None + and isinstance(din[kk], ddef[kk]['type']) + ) for kk in din.keys() ]) ) if not c0: - lstr = [f"\t- ''{k0}': {v0}" for k0, v0 in ddef.items()] + lstr = [f"\t- '{k0}': {v0['type']}" for k0, v0 in ddef.items()] msg = ( f"Arg '{dname}' must be either False or a dict with:\n" + "\n".join(lstr) @@ -606,7 +613,11 @@ def _check_anisotropy_dplot(din, dname, ddef): if din is not False: for k0, v0 in ddef.items(): - din[k0] = din.get(k0, v0) + if isinstance(v0, dict): + vv = v0['val'] + else: + vv = v0 + din[k0] = din.get(k0, vv) return din From c5ac714b5a20c95c7f04531ca3fef70fea810dc5 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 12 Jan 2026 21:06:43 +0000 Subject: [PATCH 16/80] [#1166] uniformized axes names + cascading dcases --- ...arget_integrated_dist_responsivity_plot.py | 85 ++++++++++++++----- .../_xray_thin_target_integrated_plot.py | 22 ++--- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index e50590e8d..01526215a 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -77,7 +77,8 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # version version: Optional[str] = None, # selected cases - dcases: Optional[dict[int, dict]] = None, + dcases_cross: Optional[dict[int, dict]] = None, + dcases_dist_resp: Optional[dict[int, dict]] = None, # verb verb: Optional[bool] = None, # plot @@ -113,7 +114,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # check inputs # --------------- - dcases, dscales, fs, fontsize = _check(**locals()) + dcases_dist_resp, dscales, fs, fontsize = _check(**locals()) # --------------- # prepare data @@ -155,7 +156,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # version version=version, # selected cases - dcases=False, + dcases=dcases_cross, # verb verb=verb, # plot @@ -174,7 +175,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if dax.get(kax) is not None: ax = dax[kax]['handle'] - for kk, vv in dcases.items(): + for kk, vv in dcases_dist_resp.items(): l0, = ax.plot( vv['responsivity']['data'], @@ -185,13 +186,14 @@ def plot_xray_thin_integ_dist_filter_anisotropy( lw=vv.get('lw', 1.), label=kk, ) - dcases[kk]['color'] = l0.get_color() + dcases_dist_resp[kk]['color'] = l0.get_color() ax.set_xlabel( vv['responsivity']['units'], fontsize=fontsize, fontweight='bold', ) + ax.invert_xaxis() # ------------------- # plot distribution @@ -201,7 +203,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if dax.get(kax) is not None: ax = dax[kax]['handle'] - for kk, vv in dcases.items(): + for kk, vv in dcases_dist_resp.items(): ax.semilogy( vv['E_e0']['data']*1e-3, @@ -229,7 +231,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( def _check( - dcases=None, + dcases_dist_resp=None, fs=None, fontsize=None, E_e0_scale=None, @@ -246,11 +248,11 @@ def _check( # ------------ ddef = copy.deepcopy(_DCASES) - if dcases in [None, False]: - dcases = {} + if dcases_dist_resp in [None, False]: + dcases_dist_resp = {} else: - for k0, v0 in dcases.items(): - dcases[k0] = _check_case( + for k0, v0 in dcases_dist_resp.items(): + dcases_dist_resp[k0] = _check_case( v0, f"dcases['{k0}']", ddef[list(ddef.keys())[0]], @@ -294,9 +296,7 @@ def _check( default=_DSCALES[kk], ) - return ( - dcases, dscales, fs, fontsize, - ) + return dcases_dist_resp, dscales, fs, fontsize def _check_case( @@ -392,10 +392,10 @@ def _dax( fig = plt.figure(figsize=(15, 12)) fig.suptitle(tit, size=fontsize+2, fontweight='bold') - nh0, nh1, nh2 = 2, 5, 4 + nh0, nh1, nh2 = 2, 6, 4 nv0, nv1, nv2 = 3, 1, 2 gs = gridspec.GridSpec( - ncols=nh0+nh1+nh2 + 1, + ncols=nh0+nh1+2*(nh2 + 1), nrows=nv0 + nv1, **dmargin, ) @@ -465,20 +465,20 @@ def _dax( dax['dist'] = {'handle': ax, 'type': 'isolines'} # -------------- - # ax - theta - norm + # ax - theta_cross - norm ax = fig.add_subplot( - gs[:nv2, nh0 + nh1 + 1:], + gs[:nv2, nh0 + nh1 + 1:nh0 + nh1 + 1 + nh2], xscale=dscales['theta'], yscale='linear', ) ax.set_xlabel( - r"$\theta_{ph}^B$ (deg)", + r"$\theta_{ph}^{e0}$ (deg)", size=fontsize, fontweight='bold', ) ax.set_ylabel( - "emiss (ph/sr/s/m3)", + "cross-section norm.", size=fontsize, fontweight='bold', ) @@ -487,10 +487,49 @@ def _dax( dax['theta_norm'] = {'handle': ax, 'type': 'isolines'} # -------------- - # ax - theta - abs + # ax - theta_cross - abs + + ax = fig.add_subplot( + gs[nv2:, nh0 + nh1 + 1:nh0 + nh1 + 1 + nh2], + sharex=dax['theta_norm']['handle'], + yscale='log', + ) + ax.set_xlabel( + r"$\theta_{ph}^{e0}$ (deg)", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta_abs'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - theta_emiss - norm + + ax = fig.add_subplot( + gs[:nv2, nh0 + nh1 + 2 + nh2:], + xscale=dscales['theta'], + yscale='linear', + ) + ax.set_xlabel( + r"$\theta_{ph}^B$ (deg)", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + "emiss norm.", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta_emiss_norm'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - theta_emiss - abs ax = fig.add_subplot( - gs[nv2:, nh0 + nh1 + 1:], + gs[nv2:, nh0 + nh1 + 2 + nh2:], sharex=dax['theta_norm']['handle'], yscale='log', ) @@ -506,6 +545,6 @@ def _dax( ) # store - dax['theta_abs'] = {'handle': ax, 'type': 'isolines'} + dax['theta_emiss_abs'] = {'handle': ax, 'type': 'isolines'} return dax diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index a36ff11d4..8688d968f 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -318,8 +318,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( yy = vv['data'][:, vcase['ie'], vcase['iph']] if np.any(yy > 0): - # normalized - kax = 'norm' + # theta_norm + kax = 'theta_norm' if dax.get(kax) is not None: ax = dax[kax]['handle'] @@ -331,8 +331,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( label=labi, ) - # abs - kax = 'log' + # theta_abs + kax = 'theta_abs' if dax.get(kax) is not None: ax = dax[kax]['handle'] @@ -345,7 +345,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( ) # normalized - kax = 'norm' + kax = 'theta_norm' if dax.get(kax) is not None: ax = dax[kax]['handle'] ax.legend(prop={'size': 12}) @@ -353,7 +353,7 @@ def plot_xray_thin_d2cross_ei_anisotropy( ax.set_xlim(0, 180) # normalized - kax = 'log' + kax = 'theta_abs' if dax.get(kax) is not None: ax = dax[kax]['handle'] ax.legend(prop={'size': 12}) @@ -727,7 +727,7 @@ def _dax( dax['map'] = {'handle': ax, 'type': 'isolines'} # -------------- - # ax - norm + # ax - theta_norm ax = fig.add_subplot( gs[0, 1], @@ -750,14 +750,14 @@ def _dax( ) # store - dax['norm'] = {'handle': ax, 'type': 'isolines'} + dax['theta_norm'] = {'handle': ax, 'type': 'isolines'} # -------------- - # ax - log + # ax - theta_abs ax = fig.add_subplot( gs[1, 1], - sharex=dax['norm']['handle'], + sharex=dax['theta_norm']['handle'], ) ax.set_xlabel( r"$\theta_{ph}$ (deg)", @@ -771,6 +771,6 @@ def _dax( ) # store - dax['log'] = {'handle': ax, 'type': 'isolines'} + dax['theta_abs'] = {'handle': ax, 'type': 'isolines'} return dax From fd6e41550f958ebe063aa1e6a3603002622b167a Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 12 Jan 2026 22:09:58 +0000 Subject: [PATCH 17/80] [#1166] Started integrand --- ...arget_integrated_dist_responsivity_plot.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 01526215a..63eb89679 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -167,6 +167,15 @@ def plot_xray_thin_integ_dist_filter_anisotropy( dplot_mean=dplot_mean, ) + # ------------------- + # Compute integrand + # ------------------- + + dinteg = _integrand( + d2cross=d2cross, + dcases=dcases, + ) + # ------------------- # plot responsivity # ------------------- @@ -224,6 +233,28 @@ def plot_xray_thin_integ_dist_filter_anisotropy( return dax +# ############################################# +# ############################################# +# Integrand +# ############################################# + + +def _integrand( + d2cross=None, + dcases=None, +): + + # -------------- + # loop on cases + # -------------- + + for kcase, vcase in dcases.items(): + + pass + + return + + # ############################################# # ############################################# # Axes for anisotropy @@ -384,9 +415,9 @@ def _dax( ) dmargin = { - 'left': 0.06, 'right': 0.95, - 'bottom': 0.06, 'top': 0.90, - 'wspace': 0.20, 'hspace': 0.20, + 'left': 0.05, 'right': 0.98, + 'bottom': 0.05, 'top': 0.92, + 'wspace': 0.30, 'hspace': 0.20, } fig = plt.figure(figsize=(15, 12)) From 761734d4247c4c33f641ed2a8e06e06b1c4a0c54 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 14 Jan 2026 23:22:52 +0000 Subject: [PATCH 18/80] [#1166] dranges and dtrans operational --- ...arget_integrated_dist_responsivity_plot.py | 297 ++++++++++++++++-- .../_xray_thin_target_integrated_plot.py | 2 +- 2 files changed, 264 insertions(+), 35 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 63eb89679..79882ba5c 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -6,6 +6,7 @@ import numpy as np import astropy.units as asunits +import scipy.constants as scpct import matplotlib.pyplot as plt # import matplotlib.lines as mlines import matplotlib.gridspec as gridspec @@ -14,6 +15,7 @@ # from . import _xray_thin_target_integrated as _mod from . import _xray_thin_target_integrated_plot as _mod_plot +from ... import transmission # from ..distribution import get_distribution @@ -53,6 +55,48 @@ }, } +# ----------- +# DTRANS + +_DTRANS = { + 'Al\n10 um': { + 'mat': 'Al', + 'thick': 10e-6, + }, + 'Steel\n1 cm': { + 'mat': 'StainlessSteel', + 'thick': 0.01, + }, +} + +_DTRANS_DEF = { + 'fontsize': 12, + 'fontweight': 'bold', + 'ls': '--', + 'lw': 1, + 'color': 'k', +} + +# ----------- +# DRANGES + +_DRANGES = { + 'visible': { + 'E': np.sort(scpct.h * scpct.c / (np.r_[380, 750]*1e-9) / scpct.e), + }, + 'UV': { + 'E': np.sort(np.r_[scpct.h * scpct.c / (350*1e-9) / scpct.e, 1e3]), + }, +} + +_DRANGES_DEF = { + 'fontsize': 12, + 'fontweight': 'bold', + 'color': 'k', + 'facecolor': (0.8, 0.8, 0.8), + 'alpha': 0.5, +} + # #################################################### # #################################################### @@ -79,6 +123,9 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # selected cases dcases_cross: Optional[dict[int, dict]] = None, dcases_dist_resp: Optional[dict[int, dict]] = None, + # decorative + dtrans: Optional[dict] = None, + dranges: Optional[dict] = None, # verb verb: Optional[bool] = None, # plot @@ -172,8 +219,24 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # ------------------- dinteg = _integrand( - d2cross=d2cross, - dcases=dcases, + d2cross=d2cross_cross, + dcases_dist_resp=dcases_dist_resp, + ) + + # ----------- + # decorative + # ----------- + + # transmissions + dtrans = _dtrans( + d2cross=d2cross_cross, + dtrans=dtrans, + ) + + # dranges + dranges = _dranges( + d2cross=d2cross_cross, + dranges=dranges, ) # ------------------- @@ -204,6 +267,60 @@ def plot_xray_thin_integ_dist_filter_anisotropy( ) ax.invert_xaxis() + # ------------------- + # plot comments + # ------------------- + + kax = 'comments' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # ------------ + # transmission + + for ktrans, vtrans in dtrans.items(): + + ax.axhline( + vtrans['E']*1e-3, + color=vtrans['color'], + ls=vtrans['ls'], + lw=vtrans['lw'], + ) + + ax.text( + 0.5, + vtrans['E']*1e-3, + ktrans, + fontweight=vtrans['fontweight'], + fontsize=vtrans['fontsize'], + verticalalignment='center', + horizontalalignment='center', + color=vtrans['color'], + ) + + # ------------ + # ranges + + for krang, vrang in dranges.items(): + + ax.axhspan( + vrang['E'][0]*1e-3, + vrang['E'][1]*1e-3, + facecolor=vrang['facecolor'], + alpha=vrang['alpha'] + ) + + ax.text( + 0.5, + np.sqrt(vrang['E'][0] * vrang['E'][1])*1e-3, + krang, + fontweight=vtrans['fontweight'], + fontsize=vtrans['fontsize'], + verticalalignment='center', + horizontalalignment='center', + color=vtrans['color'], + ) + # ------------------- # plot distribution # ------------------- @@ -233,28 +350,6 @@ def plot_xray_thin_integ_dist_filter_anisotropy( return dax -# ############################################# -# ############################################# -# Integrand -# ############################################# - - -def _integrand( - d2cross=None, - dcases=None, -): - - # -------------- - # loop on cases - # -------------- - - for kcase, vcase in dcases.items(): - - pass - - return - - # ############################################# # ############################################# # Axes for anisotropy @@ -392,6 +487,116 @@ def _check_case( return case +# ############################################# +# ############################################# +# Integrand +# ############################################# + + +def _integrand( + d2cross=None, + dcases_dist_resp=None, +): + + # -------------- + # loop on cases + # -------------- + + for kcase, vcase in dcases_dist_resp.items(): + + pass + + return + + +# ############################################# +# ############################################# +# Decorative +# ############################################# + + +def _dtrans( + dtrans=None, + d2cross=None, +): + + # ---------------- + # default + # ---------------- + + # default + if dtrans is None: + dtrans = copy.deepcopy(_DTRANS) + + # ---------------- + # get transmission + # ---------------- + + # False + if dtrans is False: + dtrans = {} + + else: + dout = transmission.get_xray_transmission( + dthick=dtrans, + E=d2cross['E_ph']['data'], + plot=False, + ) + + # extract info + E_inv = d2cross['E_ph']['data'].ravel()[::-1] + for kk, vv in dtrans.items(): + + # get last time > 0.5 + trans = dout['keys'][kk]['trans'].ravel()[::-1] + ii = np.nonzero(trans < 0.5)[0][0] + dtrans[kk]['E'] = E_inv[ii] + + # ---------------- + # check format + # ---------------- + + for ktrans, vtrans in dtrans.items(): + + for kk, vv in _DTRANS_DEF.items(): + dtrans[ktrans][kk] = vtrans.get(kk, vv) + + return dtrans + + +def _dranges( + dranges=None, + d2cross=None, +): + + # ---------------- + # default + # ---------------- + + # default + if dranges is None: + dranges = copy.deepcopy(_DRANGES) + + # ---------------- + # get transmission + # ---------------- + + # False + if dranges is False: + dranges = {} + + # ---------------- + # check format + # ---------------- + + for krang, vrang in dranges.items(): + + for kk, vv in _DRANGES_DEF.items(): + dranges[krang][kk] = vrang.get(kk, vv) + + return dranges + + # ############################################# # ############################################# # Axes for anisotropy @@ -417,16 +622,20 @@ def _dax( dmargin = { 'left': 0.05, 'right': 0.98, 'bottom': 0.05, 'top': 0.92, - 'wspace': 0.30, 'hspace': 0.20, + 'wspace': 0.70, 'hspace': 0.20, } fig = plt.figure(figsize=(15, 12)) fig.suptitle(tit, size=fontsize+2, fontweight='bold') - nh0, nh1, nh2 = 2, 6, 4 + nhvert = 3 + nhcom = 1 + nhlarge = 8 + nhint = 1 + nhtheta = 5 nv0, nv1, nv2 = 3, 1, 2 gs = gridspec.GridSpec( - ncols=nh0+nh1+2*(nh2 + 1), + ncols=nhvert + nhcom + nhlarge + nhint + nhtheta + nhtheta, nrows=nv0 + nv1, **dmargin, ) @@ -439,8 +648,9 @@ def _dax( # -------------- # ax - isolines + nh = nhvert + nhcom ax = fig.add_subplot( - gs[:nv0, nh0:nh0+nh1], + gs[:nv0, nh:nh + nhlarge], xscale=dscales['E_e0'], yscale=dscales['E_ph'], ) @@ -458,7 +668,7 @@ def _dax( # ax - responsivity ax = fig.add_subplot( - gs[:nv0, :nh0], + gs[:nv0, :nhvert], sharey=dax['map']['handle'], xscale=dscales['resp'], ) @@ -477,11 +687,26 @@ def _dax( # store dax['responsivity'] = {'handle': ax, 'type': 'isolines'} + # -------------- + # ax - comments + + ax = fig.add_subplot( + gs[:nv0, nhvert:nhvert+nhcom], + sharey=dax['map']['handle'], + yscale=dscales['resp'], + frameon=False, + ) + ax.axis('off') + + # store + dax['comments'] = {'handle': ax, 'type': 'isolines'} + # -------------- # ax - dist + nh = nhvert + nhcom ax = fig.add_subplot( - gs[nv0:, nh0:nh0 + nh1], + gs[nv0:, nh:nh + nhlarge], sharex=dax['map']['handle'], yscale=dscales['dist'], ) @@ -498,8 +723,9 @@ def _dax( # -------------- # ax - theta_cross - norm + nh = nhvert + nhcom + nhlarge + nhint ax = fig.add_subplot( - gs[:nv2, nh0 + nh1 + 1:nh0 + nh1 + 1 + nh2], + gs[:nv2, nh:nh + nhtheta], xscale=dscales['theta'], yscale='linear', ) @@ -520,8 +746,9 @@ def _dax( # -------------- # ax - theta_cross - abs + nh = nhvert + nhcom + nhlarge + nhint ax = fig.add_subplot( - gs[nv2:, nh0 + nh1 + 1:nh0 + nh1 + 1 + nh2], + gs[nv2:, nh:nh + nhtheta], sharex=dax['theta_norm']['handle'], yscale='log', ) @@ -537,8 +764,9 @@ def _dax( # -------------- # ax - theta_emiss - norm + nh = nhvert + nhcom + nhlarge + nhint + nhtheta ax = fig.add_subplot( - gs[:nv2, nh0 + nh1 + 2 + nh2:], + gs[:nv2, nh:], xscale=dscales['theta'], yscale='linear', ) @@ -559,8 +787,9 @@ def _dax( # -------------- # ax - theta_emiss - abs + nh = nhvert + nhcom + nhlarge + nhint + nhtheta ax = fig.add_subplot( - gs[nv2:, nh0 + nh1 + 2 + nh2:], + gs[nv2:, nh:], sharex=dax['theta_norm']['handle'], yscale='log', ) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 8688d968f..9c3de4aac 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -466,7 +466,7 @@ def _check_anisotropy( ) # dplot_thetamax - ddef = {'colors': 'b', 'levels': np.r_[0.1, 30, 50, 90]} + ddef = {'colors': 'b', 'levels': np.r_[0.01, 2, 30, 50, 90]} dplot_thetamax = _check_anisotropy_dplot( dplot_thetamax, 'dplot_thetamax', From ddf44e09ecdf8c8c09ec4db3e00516b5468c1907 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 16 Jan 2026 20:13:46 +0000 Subject: [PATCH 19/80] [#1166] Revized cases --- ...arget_integrated_dist_responsivity_plot.py | 322 ++++++++++++++---- 1 file changed, 251 insertions(+), 71 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 79882ba5c..71bfcb7a7 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -45,16 +45,39 @@ } +# DDIST +_UNITS = (str, asunits.Unit, asunits.CompositeUnit) +_DDIST = {} +_DDIST_FORMAT = { + 'E_e0': {'data': np.ndarray, 'units': _UNITS}, + 'dist': {'data': np.ndarray, 'units': _UNITS}, + 'marker': 'None', + 'lw': 1, + 'color': 'k', + 'ls': None, # cycle +} + + +# DRESP +_DRESP = {} +_DRESP_FORMAT = { + 'E_ph': {'data': np.ndarray, 'units': _UNITS}, + 'responsivity': {'data': np.ndarray, 'units': _UNITS}, + 'marker': 'None', + 'lw': 1, + 'ls': '-', + 'color': None, # cycle +} + + # DCASES -_DCASES = { - 'cvd no filter maxwell': { - 'E_ph': {}, - 'responsivity': {}, - 'dist': {}, - 'E_e0': {}, - }, +_DCASES = {} +_DCASE_FORMAT = { + 'dist': str, + 'resp': str, } + # ----------- # DTRANS @@ -122,6 +145,9 @@ def plot_xray_thin_integ_dist_filter_anisotropy( version: Optional[str] = None, # selected cases dcases_cross: Optional[dict[int, dict]] = None, + # distributions, responsivities and cases + ddist: Optional[dict] = None, + dresp: Optional[dict] = None, dcases_dist_resp: Optional[dict[int, dict]] = None, # decorative dtrans: Optional[dict] = None, @@ -161,7 +187,12 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # check inputs # --------------- - dcases_dist_resp, dscales, fs, fontsize = _check(**locals()) + ( + ddist, dresp, + dcases_dist_resp, + dscales, + fs, fontsize, + ) = _check(**locals()) # --------------- # prepare data @@ -247,7 +278,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if dax.get(kax) is not None: ax = dax[kax]['handle'] - for kk, vv in dcases_dist_resp.items(): + for kk, vv in dresp.items(): l0, = ax.plot( vv['responsivity']['data'], @@ -256,15 +287,10 @@ def plot_xray_thin_integ_dist_filter_anisotropy( ls=vv.get('ls', '-'), marker=vv.get('marker'), lw=vv.get('lw', 1.), - label=kk, + label=f"{kk}_{vv['responsivity']['units']}", ) - dcases_dist_resp[kk]['color'] = l0.get_color() + dresp[kk]['color'] = l0.get_color() - ax.set_xlabel( - vv['responsivity']['units'], - fontsize=fontsize, - fontweight='bold', - ) ax.invert_xaxis() # ------------------- @@ -329,9 +355,9 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if dax.get(kax) is not None: ax = dax[kax]['handle'] - for kk, vv in dcases_dist_resp.items(): + for kk, vv in ddist.items(): - ax.semilogy( + l0, = ax.semilogy( vv['E_e0']['data']*1e-3, vv['dist']['data'], c=vv['color'], @@ -340,6 +366,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( lw=vv.get('lw', 1.), label=kk, ) + ddist[kk]['color'] = l0.get_color() ax.set_ylabel( vv['dist']['units'], @@ -357,7 +384,11 @@ def plot_xray_thin_integ_dist_filter_anisotropy( def _check( + # distributions, responsivity, cases + ddist=None, + dresp=None, dcases_dist_resp=None, + # plotting fs=None, fontsize=None, E_e0_scale=None, @@ -369,20 +400,47 @@ def _check( **kwdargs, ): + # ------------ + # dresp + # ------------ + + if dresp is None: + dresp = _DRESP + + if dresp is False: + dresp = {} + + dresp = _check_dict( + din=dresp, + din_name='dresp', + ddef=_DRESP_FORMAT, + ) + + # ------------ + # ddist + # ------------ + + if ddist is None: + ddist = _DDIST + + if ddist is False: + ddist = {} + + ddist = _check_dict( + din=ddist, + din_name='ddist', + ddef=_DDIST_FORMAT, + ) + # ------------ # dcases # ------------ - ddef = copy.deepcopy(_DCASES) - if dcases_dist_resp in [None, False]: - dcases_dist_resp = {} - else: - for k0, v0 in dcases_dist_resp.items(): - dcases_dist_resp[k0] = _check_case( - v0, - f"dcases['{k0}']", - ddef[list(ddef.keys())[0]], - ) + dcases_dist_resp = _check_dcases( + dresp=dresp, + ddist=ddist, + dcases=dcases_dist_resp, + ) # ------------ # fs @@ -422,69 +480,186 @@ def _check( default=_DSCALES[kk], ) - return dcases_dist_resp, dscales, fs, fontsize + return ( + ddist, dresp, + dcases_dist_resp, + dscales, + fs, fontsize, + ) -def _check_case( - case=None, - key=None, +def _check_dict( + din=None, + din_name=None, ddef=None, ): # -------------- - # general structure + # overall structure # -------------- - dfail = {} - lok = list(ddef.keys()) - ltunits = (str, asunits.Unit, asunits.CompositeUnit) - for kk in lok: - if not isinstance(case.get(kk), dict): - typ = type(case.get(kk)) - dfail[kk] = f'absent or not a dict ({typ})' - elif not isinstance(case[kk].get('data'), np.ndarray): - typ = type(case[kk].get('data')) - dfail[kk] = f'data key not a np.ndarray ({typ})' - elif not isinstance(case[kk].get('units'), ltunits): - typ = type(case[kk].get('units')) - dfail[kk] = f"units not a str ({typ})" - else: - dfail[kk] = 'ok' - - if any([vv != 'ok' for vv in dfail.values()]): - lstr = [f"\t- {kk}: {vv}" for kk, vv in dfail.items()] + c0 = ( + isinstance(din, dict) + and all([ + isinstance(kk, str) + and isinstance(vv, dict) + for kk, vv in din.items() + ]) + ) + if not c0: msg = ( - f"Arg {key} must be a dict with keys {lok}, " - "where each is a {'data': np.ndarray, 'units': str} subdict!\n" - + "\n".join(lstr) + f"Arg '{din_name}' must be a dict of sub-dicts!\n" + f"Provided:\n{din}\n" ) raise Exception(msg) # -------------- - # shape consistency + # each key structure # -------------- - shape_Eph = case['E_ph']['data'].shape - shape_resp = case['responsivity']['data'].shape - if shape_Eph != shape_resp: - msg = ( - "The 2 fields below must have the same shape:\n" - f"{key}['E_ph']['data'].shape = {shape_Eph}\n" - f"{key}['responsivity']['data'].shape = {shape_resp}\n" - ) - raise Exception(msg) + dfail = {} + lok = [kk for kk, vv in ddef.items() if isinstance(vv, dict)] + for k0, v0 in din.items(): + for kk in lok: + if not isinstance(din[k0].get(kk), dict): + typ = type(din[k0].get(kk)) + dfail[kk] = f'absent or not a dict ({typ})' + elif not isinstance(din[k0][kk].get('data'), ddef[kk]['data']): + typ = type(din[k0][kk].get('data')) + dfail[kk] = f'data not a np.ndarray ({typ})' + elif not isinstance(din[k0][kk].get('units'), ddef[kk]['units']): + typ = type(din[k0][kk].get('units')) + dfail[kk] = f"units not a str ({typ})" + else: + dfail[kk] = 'ok' + + if any([vv != 'ok' for vv in dfail.values()]): + lstr = [f"\t- {kk}: {vv}" for kk, vv in dfail.items()] + msg = ( + f"Arg {din_name}['{k0}'] must be a dict with keys {lok}, " + "where each is {'data': np.ndarray, 'units': str} subdict!\n" + + "\n".join(lstr) + ) + raise Exception(msg) + + # -------------- + # shapes: all flat + # -------------- + + dfail = {} + for k0, v0 in din.items(): + + # squeeze + for kk in lok: + + shape = v0[kk]['data'].shape + if np.prod(shape) != np.max(shape): + dfail[kk] = ( + 'data must be squeeze-able to a flat 1d array!' + f' (shape = {shape})' + ) + continue + + din[k0][kk]['data'] = v0[kk]['data'].squeeze() + + # consistency + lsize = list(set([din[k0][kk]['data'].size for kk in lok])) + if len(lsize) != 1: + msg = ( + "All keys in {din_name}['{k0}'] must have the same data shape" + ) + raise Exception(msg) + + # ------------ + # units + # ------------ + + dfail = {} + for k0, v0 in din.items(): + for kk in lok: + + if not kk.startswith('E_'): + continue + + if str(v0[kk]['units']) != 'eV': + dfail[kk] = f"units should be eV ({v0[kk]['units']})" + + if len(dfail) > 0: + lstr = [ + f"\t- {din_name}['{k0}']['{kk}']: {vv}" + for kk, vv in dfail.items() + ] + msg = ( + "Units of the following keys is incorrect:\n" + + "\n".join(lstr) + ) + raise Exception(msg) + + # -------------- + # plotting + # -------------- + + lok = [kk for kk, vv in ddef.items() if not isinstance(vv, dict)] + for k0, v0 in din.items(): + for kk in lok: + din[k0][kk] = din[k0].get(kk, ddef[kk]) - shape_Ee0 = case['E_e0']['data'].shape - shape_dist = case['dist']['data'].shape - if shape_Ee0 != shape_dist: + return din + + +def _check_dcases( + dcases=None, + dresp=None, + ddist=None, +): + + # -------------- + # defaults + # -------------- + + if dcases is False: + dcases = {} + + if dcases is None: + dcases = {} + for kdist in ddist.keys(): + for kresp in dresp.keys(): + key = f"{kresp}_{kdist}" + dcases[key] = { + 'resp': kresp, + 'dist': kdist, + } + + # -------------- + # general structure + # -------------- + + c0 = ( + isinstance(dcases, dict) + and all([ + isinstance(kcase, str) + and ( + isinstance(vcase.get('resp'), str) + and vcase['resp'] in dresp.keys() + ) + and ( + isinstance(vcase.get('dist'), str) + and vcase['dist'] in ddist.keys() + ) + for kcase, vcase in dcases.items() + ]) + ) + if not c0: msg = ( - "The 2 fields below must have the same shape:\n" - f"{key}['E_e0']['data'].shape = {shape_Ee0}\n" - f"{key}['dist']['data'].shape = {shape_dist}\n" + "Arg dcase must be a dict of subdicts of the form " + "{'dist': key0, 'resp': key1}\n" + "Where key0 (resp. key1) refer to an existing key in " + "ddist (resp. dresp)\n" + f"Provided: {dcases}\n" ) raise Exception(msg) - return case + return dcases # ############################################# @@ -677,6 +852,11 @@ def _dax( size=fontsize, fontweight='bold', ) + ax.set_xlabel( + 'responsivity', + fontsize=fontsize, + fontweight='bold', + ) ax.set_ylabel( r"$E_{ph}$ (keV)", size=fontsize, From e8264480ddf7685b3f424c0b311a46387472c356 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 20 Jan 2026 14:03:45 +0000 Subject: [PATCH 20/80] [#1166] Better legend --- ..._thin_target_integrated_dist_responsivity_plot.py | 12 +++++++++++- .../emission/_xray_thin_target_integrated_plot.py | 8 ++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 71bfcb7a7..7743fb050 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -108,7 +108,10 @@ 'E': np.sort(scpct.h * scpct.c / (np.r_[380, 750]*1e-9) / scpct.e), }, 'UV': { - 'E': np.sort(np.r_[scpct.h * scpct.c / (350*1e-9) / scpct.e, 1e3]), + 'E': np.sort(np.r_[ + scpct.h * scpct.c / (350*1e-9) / scpct.e, + 30e15 * scpct.h / scpct.e, + ]), }, } @@ -293,6 +296,13 @@ def plot_xray_thin_integ_dist_filter_anisotropy( ax.invert_xaxis() + # legend + ax.legend( + bbox_to_anchor=(0, -0.1), + loc='upper left', + borderaxespad=0, + ) + # ------------------- # plot comments # ------------------- diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 9c3de4aac..6b91de514 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -344,19 +344,21 @@ def plot_xray_thin_d2cross_ei_anisotropy( label=labi, ) + leg = False + # normalized kax = 'theta_norm' if dax.get(kax) is not None: ax = dax[kax]['handle'] - ax.legend(prop={'size': 12}) ax.set_ylim(0, 1) ax.set_xlim(0, 180) + ax.legend(prop={'size': 12}) + leg = True # normalized kax = 'theta_abs' if dax.get(kax) is not None: ax = dax[kax]['handle'] - ax.legend(prop={'size': 12}) units = str(vv['units']) units.replace('m2', 'barn') ax.set_ylabel( @@ -365,6 +367,8 @@ def plot_xray_thin_d2cross_ei_anisotropy( fontweight='bold', ) ax.grid(True) + if leg is False: + ax.legend(prop={'size': 12}) return dax, d2cross From 5ced55fd26372c7f8b0e041ccffe353d8b50ca23 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 20 Jan 2026 22:19:47 +0000 Subject: [PATCH 21/80] [#1166] Better safeguards against wrong input for distributions --- .../electrons/distribution/_distribution.py | 22 ++++++++++-- .../distribution/_distribution_check.py | 35 +++++++++++++++++++ .../distribution/_distribution_maxwell.py | 1 + 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index c7aa37b9c..8750a59b3 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -161,13 +161,25 @@ def main( inan = np.isnan(ddist['dist'][kdist]['dist']['data']) ddist['dist'][kdist]['dist']['data'][inan] = 0. + # neg => error + ineg = ddist['dist'][kdist]['dist']['data'] < 0. + if np.any(ineg): + msg = ( + "Electron dist has negative values!\n" + f"\t- dist = '{kdist}'\n" + f"\t- version = '{dfunc[kdist]['version']}'\n" + f"\t- module = {dfunc[kdist]['func'].__module__}\n" + f"\t- func = {dfunc[kdist]['func'].__name__}\n" + ) + raise Exception(msg) + # scale ne_re = _scale( din=din, ddist=ddist, kdist=kdist, dcoords=dcoords, - version=version, + version=dfunc[kdist]['version'], ) # -------------- @@ -185,7 +197,7 @@ def main( ddist=ddist, kdist=kdist, dcoords=dcoords, - version=version, + version=dfunc[kdist]['version'], ) # store @@ -355,7 +367,10 @@ def _get_velocity_par(ddist, kdist): energy_kinetic_eV=ddist['coords']['x0']['data'], )['velocity_ms'] units = v_par_ms['units'] - v_par_ms = v_par_ms['data'] + # v_par_ms = v_par_ms['data'] + + # assume 0 drift velocity => average v_par = 0 + v_par_ms = np.zeros(v_par_ms['data'].shape) else: raise NotImplementedError(kcoords) @@ -401,6 +416,7 @@ def _integrate( ) ne = ddist['dist'][kdist]['dist']['data'] x0 = dcoords['x0']['data'] + else: current = scpinteg.trapezoid( scpct.e diff --git a/tofu/physics_tools/electrons/distribution/_distribution_check.py b/tofu/physics_tools/electrons/distribution/_distribution_check.py index 8fd02a1ba..dc9a925f2 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_check.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_check.py @@ -2,6 +2,7 @@ import numpy as np import astropy.units as asunits +import scipy.constants as scpct import datastock as ds import tofu as tf @@ -397,6 +398,40 @@ def _coords( ) raise Exception(msg) + # -------------- + # check values + # -------------- + + for k0, v0 in dcoords.items(): + + # check validity + if k0 == 'theta': + iout = (v0['data'] < 0.) | (v0['data'] > np.pi) + msg = "must be in [0; pi]" + elif k0 == 'pitch': + iout = (v0['data'] < -1) | (v0['data'] > 1) + msg = "must be in [-1; 1]" + elif k0.startswith('v_par'): + iout = (v0['data'] > scpct.c) + msg = "must be <= c" + elif k0.startswith('v_perp'): + iout = (v0['data'] > scpct.c) | (v0['data'] < 0.) + msg = "must be in [0; c]" + elif k0.startswith('p_perp') or k0 == 'E_eV': + iout = (v0['data'] < 0.) + msg = "must be >= 0" + else: + msg = '' + continue + + # Exception + if np.any(iout): + msg = ( + f"Some values in dcoords['{k0}'] are invalid:\n" + + msg + "\n" + ) + raise Exception(msg) + return dcoords diff --git a/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py b/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py index 65b4af443..591583bb8 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py @@ -209,6 +209,7 @@ def f3d_E_theta( v0_par_ms=v0_par_ms, ) + # caution: sin(theta) => negative values ! dist = np.sin(theta) * dist0 / (2.*np.pi) units = units0 * asunits.Unit('1/rad^2') From 7856f32f4dd782b7711dd0204f7c377769a61535 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 21 Jan 2026 19:05:59 +0000 Subject: [PATCH 22/80] [#1166] Better docstr --- ..._target_integrated_dist_responsivity_plot.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 7743fb050..76df3593d 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -171,18 +171,13 @@ def plot_xray_thin_integ_dist_filter_anisotropy( dplot_thetamax=None, dplot_mean=None, ) -> TupleDict: - """ Compute and plot a (E_e0, E_ph) countour map of the d2cross section + """ Plot a (E_e0, E_ph) countour map of the d2cross section and + interates it over an electron distribution and a responsivity - Where d2cross is the fully differentiated cross-section (d3cross), - integrated over one of the two the emission angle (dphi) - - Actually 3 overlayed contour plots with: - - integral of of the cross-section (over photon emission angle) - - angle of max cross-section - - peaking of the cross-section (std vs angle) - - Can overlay a few selected cases and plot them vs angle of emission - In normalized-linear and log scales + - with cases (E_e0, E_ph) vs theta_ph + - with electron distributions + - with sensor responsivities + - with distribution and responsivity-integrated emissivities vs theta_B """ From 68057e066180cea3a2245d7ec1b259c1a328d59d Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 21 Jan 2026 19:08:02 +0000 Subject: [PATCH 23/80] [#1166] get_d2cross_phi(d2cross=pfe) operational --- ..._xray_thin_target_integrated_d2crossphi.py | 302 ++++++++++++++++-- 1 file changed, 268 insertions(+), 34 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py index 86a8e9984..03cc5d667 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py @@ -6,6 +6,7 @@ import numpy as np import scipy.integrate as scpinteg +import scipy.interpolate as scpinterp import astropy.units as asunits import datastock as ds @@ -39,6 +40,8 @@ def get_d2cross_phi( # load from file pfe=None, + # tabulated d2cross + d2cross=None, # params Z=None, E_ph_eV=None, @@ -63,6 +66,7 @@ def get_d2cross_phi( d2cross_phi=None, save=None, pfe_save=None, + overwrite=None, # unused **kwdargs, ): @@ -72,10 +76,11 @@ def get_d2cross_phi( # ---------------- ( - save, pfe, verb, + save, overwrite, pfe, verb, ) = _check( pfe=pfe, save=save, + overwrite=overwrite, pfe_save=pfe_save, verb=verb, ) @@ -100,7 +105,12 @@ def get_d2cross_phi( # optional save if save is True: - _save(d2cross_phi, pfe_save) + _save( + d2cross_phi=d2cross_phi, + pfe_save=pfe_save, + overwrite=overwrite, + verb=verb, + ) # ---------------- # load @@ -121,6 +131,7 @@ def get_d2cross_phi( def _check( pfe=None, save=None, + overwrite=None, pfe_save=None, verb=None, ): @@ -136,6 +147,17 @@ def _check( default=pfe_save is not None, ) + # ------------- + # overwrite + # ------------- + + # overwrite + overwrite = ds._generic_check._check_var( + overwrite, 'overwrite', + types=bool, + default=False, + ) + # ------------- # pfe # ------------- @@ -167,7 +189,7 @@ def _check( )) return ( - save, pfe, verb, + save, overwrite, pfe, verb, ) @@ -178,6 +200,8 @@ def _check( def _check_compute( + # tabulated d2cross + d2cross=None, # params E_ph_eV=None, E_e0_eV=None, @@ -314,13 +338,7 @@ def _check_compute( # pfe # ---------- - if pfe_save is None: - nE = E_ph_eV.size - ntheta = theta_ph_vsB.size - fn = f"d2cross_phi_nEph{nE}_ntheta{ntheta}" - pfe_save = os.path.join(_PATH_HERE, f'{fn}.npz') - else: - + if pfe_save is not None: c0 = ( isinstance(pfe_save, str) and os.path.isdir(os.path.split(pfe_save)[0]) @@ -351,6 +369,9 @@ def _check_compute( def _compute( + # tabulated d2cross + d2cross=None, + # parameters E_ph_eV=None, E_e0_eV=None, theta_e0_vsB=None, @@ -372,6 +393,10 @@ def _compute( **kwdargs, ): + # ----------- + # prepare + # ----------- + # theta_ph_vs_e in (theta_ph_vsB, theta_e0_vsB, phi_e0_vsB) cos = ( np.cos(theta_e0_vsB[None, :, None]) @@ -390,6 +415,20 @@ def _compute( shape_integ = (E_e0_eV.size, theta_e0_vsB.size, phi_e0_vsB.size) d2cross_phi = np.zeros(shape_emiss + shape_integ[:-1], dtype=float) + + # ------------- + # load d2cross + # ------------- + + if d2cross is not None: + d2cross0, nthetae, ndphi = _get_interpolator(d2cross) + else: + d2cross0 = None + + # ------------- + # loop on index + # ------------- + for i0, ind in enumerate(np.ndindex(shape_emiss)): if verb >= 2: @@ -400,35 +439,60 @@ def _compute( msg = f"\tE_ph_eV {iEstr}, theta_ph_vsB {itstr} for shape {ish}" print(msg) + # ----------- # get integrated cross-section # theta_ph_vs_e = (theta_ph_vsB, theta_e0_vsB, phi_e0_vsB) # d2cross = (E_ph_eV, E_e0_eV, theta_ph_vsB) # = (E_ph_eV, E_e0_eV, theta_ph_vsB, theta_e0_vsB, phi_e0_vsB) - d2cross = _mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( - # inputs - Z=Z, - E_ph_eV=E_ph_eV[ind[0]], - E_e0_eV=E_e0_eV[iok, None, None], - theta_ph=theta_ph_vs_e[None, ind[1], :, :], - # hypergeometric parameter - ninf=ninf, - source=source, - # integration parameters - nthetae=nthetae, - ndphi=ndphi, - # output customization - per_energy_unit='eV', - # version - version=version_cross, - # verb - verb=verb > 2, - verb_tab=2, - ) + if d2cross0 is None: + d2cross = _mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( + # tabulated d2cross + d2cross=None, + # inputs + Z=Z, + E_ph_eV=E_ph_eV[ind[0]], + E_e0_eV=E_e0_eV[iok, None, None], + theta_ph=theta_ph_vs_e[None, ind[1], :, :], + # hypergeometric parameter + ninf=ninf, + source=source, + # integration parameters + nthetae=nthetae, + ndphi=ndphi, + # output customization + per_energy_unit='eV', + # version + version=version_cross, + # verb + verb=verb > 2, + verb_tab=2, + ) + + if i0 == 0: + units = d2cross['cross'][version_cross]['units'] + nthetae = d2cross['theta_e']['data'].size + ndphi = d2cross['dphi']['data'].size + + cross = d2cross['cross'][version_cross]['data'] + + # ----------- + # interpolate + else: + + xx = d2cross0[version_cross]['get_xx']( + E_ph_eV=E_ph_eV[ind[0]], + E_e0_eV=E_e0_eV[iok, None, None], + theta_ph=theta_ph_vs_e[None, ind[1], :, :], + ) + cross = d2cross0[version_cross]['interp'](xx) + units = d2cross0[version_cross]['units'] + + # ----------- # integrate over phi # MULTIPLY BY SIN PHI ????? d2cross_phi[ind[0], ind[1], iok, :] = scpinteg.trapezoid( - d2cross['cross'][version_cross]['data'], + cross, x=phi_e0_vsB, axis=-1, ) @@ -437,7 +501,6 @@ def _compute( # units # ---------- - units = d2cross['cross'][version_cross]['units'] units *= asunits.Unit('rad') # ------------- @@ -455,8 +518,8 @@ def _compute( 'theta_ph_vsB': theta_ph_vsB, 'phi_e0_vsB': phi_e0_vsB, 'Z': Z, - 'nthetae': d2cross['theta_e']['data'].size, - 'ndphi': d2cross['dphi']['data'].size, + 'nthetae': nthetae, + 'ndphi': ndphi, 'version_cross': version_cross, 'ninf': ninf, 'source': source, @@ -465,6 +528,133 @@ def _compute( return dout +# ########################################### +# ########################################### +# Interpolator +# ########################################### + + +def _get_interpolator( + d2cross=None, +): + + # --------------- + # load tabulated + # --------------- + + d2cross0 = _mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( + d2cross=d2cross, + ) + + # extract npts + nthetae = d2cross0['theta_e']['data'].size + ndphi = d2cross0['dphi']['data'].size + + # loop on versions + dinterp = {} + laxis = ['theta_ph', 'E_ph', 'E_e0'] + for version in d2cross0['cross'].keys(): + + # ---------------------- + # extract data and units + + dinterp[version] = { + 'axis': {}, + 'islog': {}, + 'interp': None, + 'units': d2cross0['cross'][version]['units'], + } + cross = d2cross0['cross'][version]['data'] + + # -------------- + # safety check + + if np.unique(cross.shape).size != cross.ndim: + msg = ( + "d2cross from file has ambiguous dimensions!\n" + f"\t- pfe: {d2cross}\n" + f"\t- cross shape: {cross.shape}\n" + ) + raise Exception(msg) + + # --------------- + # axes + + for kk in laxis: + data = d2cross0[kk]['data'] + assert np.prod(data.shape) == data.size + data = np.squeeze(data) + + dinterp[version]['axis'][kk] = cross.shape.index(data.size) + + diff = np.diff(data) + if np.allclose(diff, diff[0]): + dinterp[version]['islog'][kk] = False + else: + difflog = np.diff(np.log10(data)) + if np.allclose(difflog, difflog[0]): + dinterp[version]['islog'][kk] = True + else: + msg = ( + "Not linear nor log!\n" + f"\t- pfe = {d2cross}\n" + f"\t- kk = {kk}\n" + ) + raise Exception(msg) + + # --------------- + # grid point + # --------------- + + inds = np.argsort([dinterp[version]['axis'][kk] for kk in laxis]) + keys = [laxis[ii] for ii in inds] + xx = tuple([ + np.log10(d2cross0[kk]['data'].ravel()) + if dinterp[version]['islog'][kk] + else d2cross0[kk]['data'].ravel() + for kk in keys + ]) + + # --------------- + # interpolator + # --------------- + + dinterp[version]['interp'] = scpinterp.RegularGridInterpolator( + xx, + cross, + method='linear', + bounds_error=False, + fill_value=np.nan, + ) + + # --------------- + # get_xx + # --------------- + + islog = [dinterp[version]['islog'][kk] for kk in keys] + keys = [f"{kk}_eV" if kk[0] == 'E' else kk for kk in keys] + dinterp[version]['get_xx'] = _get_xx(keys, islog) + + return dinterp, nthetae, ndphi + + +def _get_xx(keys, islog): + + def func(**kwdargs): + args = [ + np.log10(kwdargs[kk]) if islog[ii] + else kwdargs[kk] + for ii, kk in enumerate(keys) + ] + return np.moveaxis( + np.array(np.broadcast_arrays(*args)), + 0, + -1, + ) + + return func + + # ########################################### # ########################################### # load @@ -553,8 +743,52 @@ def _load( def _save( d2cross_phi=None, pfe_save=None, + overwrite=None, + verb=None, ): + # ---------- + # pfe_save + # ---------- + + if pfe_save is None: + + # extract sizes + ntheta_ph = d2cross_phi['theta_ph_vsB'].size + ntheta_e0 = d2cross_phi['theta_e0_vsB'].size + Eph = _mod._format_vect2str( + d2cross_phi['E_ph_eV'], + base='eV', + ) + Ee0 = _mod._format_vect2str( + d2cross_phi['E_e0_eV'], + base='eV', + ) + + # extract boundaries + path = os.path.abspath(_PATH_HERE) + fname = ( + f"d2cross_phi_Ee0{Ee0}_Eph{Eph}" + f"_nthetaph{ntheta_ph}_nthetae0{ntheta_e0}" + ) + pfe_save = os.path.join(path, f"{fname}.npz") + + # ---------- + # overwrite + # ---------- + + if os.path.isfile(pfe_save): + if overwrite is True: + if verb is True: + msg = f"Overwritting file {pfe_save}\n" + warnings.warn(msg) + else: + msg = ( + "File {pfe_save} already exists!\n" + "\t=> use overwrite=True to overwrite\n" + ) + raise Exception(msg) + # ---------- # save # ---------- From 4529883a7774df9d997bb7c8fe37da3013232629 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 4 Feb 2026 20:44:11 +0000 Subject: [PATCH 24/80] [#1166] Debugged move_diagnostic_3d() --- tofu/data/_class08_move3d_check.py | 7 +++---- tofu/data/_class08_move_rotate3d_by.py | 23 ++++++++++++----------- tofu/data/_class08_move_translate3d_by.py | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tofu/data/_class08_move3d_check.py b/tofu/data/_class08_move3d_check.py index f8b241b76..909d7fceb 100644 --- a/tofu/data/_class08_move3d_check.py +++ b/tofu/data/_class08_move3d_check.py @@ -227,7 +227,7 @@ def _add_asis( if isinstance(kk, tuple): for ii, ki in enumerate(kk): k0 = ki.split('_')[0] - if dgeom0.get(k0) is not None: + if isinstance(dgeom0.get(k0), tuple): dgeom[ki] = coll.ddata[dgeom0[k0][ii]]['data'] elif dgeom0.get(kk) is not None: @@ -235,8 +235,7 @@ def _add_asis( if isinstance(dgeom0[kk], str): dgeom[kk] = coll.ddata[kk]['data'] - else: - if dgeom0.get(kk) is not None: - dgeom[kk] = dgeom0[kk] + elif not isinstance(dgeom0[kk], tuple): + dgeom[kk] = dgeom0[kk] return diff --git a/tofu/data/_class08_move_rotate3d_by.py b/tofu/data/_class08_move_rotate3d_by.py index dd765415f..1fb3112ee 100644 --- a/tofu/data/_class08_move_rotate3d_by.py +++ b/tofu/data/_class08_move_rotate3d_by.py @@ -268,26 +268,27 @@ def _rotate_camera( # unit vects lk_vect = ['nin', 'e0', 'e1'] for kk in lk_vect: - if dgeom0.get(kk) is not None: - dgeom[kk] = np.r_[_rotate_pts( + + if isinstance(dgeom0.get(kk), tuple): + kx, ky, kz = f"{kk}_x", f"{kk}_y", f"{kk}_z" + dgeom[kx], dgeom[ky], dgeom[kz] = _rotate_pts( axis_pt, axis_vect, angle, - *dgeom0[kk], + coll.ddata[dgeom0[kk][0]]['data'], + coll.ddata[dgeom0[kk][1]]['data'], + coll.ddata[dgeom0[kk][2]]['data'], isvect=True, - )] + ) - if dgeom0.get(f"{kk}_x") is not None: - kx, ky, kz = f"{kk}_x", f"{kk}_y", f"{kk}_z" - dgeom[kx], dgeom[ky], dgeom[kz] = _rotate_pts( + elif dgeom0.get(kk) is not None: + dgeom[kk] = np.r_[_rotate_pts( axis_pt, axis_vect, angle, - dgeom0[kx], - dgeom0[ky], - dgeom0[kz], + *dgeom0[kk], isvect=True, - ) + )] # ---------------- # add as-is diff --git a/tofu/data/_class08_move_translate3d_by.py b/tofu/data/_class08_move_translate3d_by.py index ef74a5767..a7d0bd594 100644 --- a/tofu/data/_class08_move_translate3d_by.py +++ b/tofu/data/_class08_move_translate3d_by.py @@ -212,9 +212,9 @@ def _translate_camera( # asis lk_asis = [ 'nin', 'e0', 'e1', - 'nin_x', 'nin_y', 'nin_z', - 'e0_x', 'e0_y', 'e0_z', - 'e1_x', 'e1_y', 'e1_z', + ('nin_x', 'nin_y', 'nin_z'), + ('e0_x', 'e0_y', 'e0_z'), + ('e1_x', 'e1_y', 'e1_z'), ('outline_x0', 'outline_x1'), ] From d72df4bbb1e9067d850108135fbbc8a82075d146 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 12 Feb 2026 19:52:24 +0000 Subject: [PATCH 25/80] [#1166] get_dist1d_E() implemented (from 2d) --- .../electrons/distribution/__init__.py | 1 + .../electrons/distribution/_distribution.py | 171 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/tofu/physics_tools/electrons/distribution/__init__.py b/tofu/physics_tools/electrons/distribution/__init__.py index 25e2d8bdd..d03fe90fd 100644 --- a/tofu/physics_tools/electrons/distribution/__init__.py +++ b/tofu/physics_tools/electrons/distribution/__init__.py @@ -1,5 +1,6 @@ from ._runaway_growth import get_RE_critical_dreicer_electric_fields from ._runaway_growth import get_RE_growth_source_terms from ._distribution import main as get_distribution +from ._distribution import get_dist1d_E from ._distribution_plot import main as plot_distribution from ._distribution_study import study_RE_vs_Maxwellian_distribution diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index 8750a59b3..987a84bc4 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -7,7 +7,9 @@ import numpy as np import scipy.integrate as scpinteg import scipy.constants as scpct +import scipy.stats as scpstats import astropy.units as asunits +import datastock as ds from . import _distribution_check as _check @@ -387,6 +389,175 @@ def _get_velocity_par(ddist, kdist): return velocity_par +# ##################################################### +# ##################################################### +# Get Energy +# ##################################################### + + +def get_dist1d_E(ddist, kdist, nbins=None): + + # ----------- + # check inputs + # ----------- + + nbins = int(ds._generic_check._check_var( + nbins, 'nbins', + types=(float, int), + default=ddist['coords']['x0']['data'].size, + sign='>3', + )) + + # ----------- + # prepare + # ----------- + + kcoords = tuple([ + ddist['coords'][kk]['key'] for kk in ['x0', 'x1'] + if ddist['coords'].get(kk) is not None + ]) + shape = ddist['dist'][kdist]['dist']['data'].shape + + # ----------- + # (E, theta) + # ----------- + + if kcoords == ('E_eV', 'theta'): + + assert ddist['coords']['x0']['units'] == 'eV' + E = ddist['coords']['x0']['data'] + + dist1d = scpinteg.trapezoid( + ddist['dist'][kdist]['dist']['data'], + x=ddist['coords']['x1']['data'], + axis=-1, + ) + units = ( + asunits.Unit(ddist['dist'][kdist]['dist']['units']) + * asunits.Unit(ddist['coords']['x1']['units']) + ) + + # ----------- + # (E, pitch) + # ----------- + + elif kcoords == ('E_eV', 'pitch'): + + assert ddist['coords']['x0']['units'] == 'eV' + E = ddist['coords']['x0']['data'] + + dist1d = scpinteg.trapezoid( + ddist['dist'][kdist]['dist']['data'], + x=ddist['coords']['x1']['data'], + axis=-1, + ) + units = ( + asunits.Unit(ddist['dist'][kdist]['dist']['units']) + * asunits.Unit(ddist['coords']['x1']['units']) + ) + + # ----------- + # (p_par, p_perp) + # ----------- + + elif kcoords == ('p_par_norm', 'p_perp_norm'): + + sli = (None,)*(len(shape)-2) + (slice(None),)*2 + pnorm = np.sqrt( + ddist['coords']['x0']['data'][:, None]**2 + + ddist['coords']['x1']['data'][None, :]**2 + )[sli] + pnorm = np.broadcast_to(pnorm, shape) + + # abs(velocity) + energy = _convert.convert_momentum_velocity_energy( + momentum_normalized=pnorm, + )['energy_kinetic_eV'] + + # nbins + E = np.linspace(np.min(energy)-1e-14, np.max(energy)+1e-14, nbins + 1) + dE = E[1] - E[0] + + # binning + dist1d = scpstats.binned_statictics( + energy, + ddist['dist'][kdist]['dist']['data'], + bins=E, + statistic='sum', + ).statistic / dE + E = 0.5*(E[1:] + E[:-1]) + + units = ( + asunits.Unit(ddist['dist'][kdist]['dist']['units']) + / asunits.unit('eV') + ) + + # ----------- + # (v_par, v_perp) + # ----------- + + elif kcoords == ('v_par_norm', 'v_perp_norm'): + + sli = (None,)*(len(shape)-2) + (slice(None),)*2 + vv = np.sqrt( + ddist['coords']['x0']['data'][:, None]**2 + + ddist['coords']['x1']['data'][None, :]**2 + )[sli] + vv = np.broadcast_to(vv, shape) + + # abs(velocity) + energy = _convert.convert_momentum_velocity_energy( + velocity_ms=vv, + )['energy_kinetic_eV'] + + # nbins + E = np.linspace(np.min(energy)-1e-14, np.max(energy)+1e-14, nbins + 1) + dE = E[1] - E[0] + + # binning + dist1d = scpstats.binned_statictics( + energy, + ddist['dist'][kdist]['dist']['data'], + bins=E, + statistic='sum', + ).statistic / dE + E = 0.5*(E[1:] + E[:-1]) + + units = ( + asunits.Unit(ddist['dist'][kdist]['dist']['units']) + / asunits.unit('eV') + ) + + # ----------- + # (E,) + # ----------- + + elif kcoords == ('E_eV',): + + E = ddist['coords']['x0']['data'] + dist1d = ddist['dist'][kdist]['dist']['data'] + units = ddist['dist'][kdist]['dist']['units'] + + else: + raise NotImplementedError(kcoords) + + # --------------- + # abs() => v_par + # --------------- + + ddist1d = { + 'E': { + 'data': E, + 'units': 'eV', + }, + 'dist': { + 'data': dist1d, + 'units': units, + }, + } + + return ddist1d + # ##################################################### # ##################################################### # Integrate numerically From a1beeeabcaebeb3b548b0d7ba47b912b5c42ba38 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 12 Feb 2026 19:53:47 +0000 Subject: [PATCH 26/80] [#1166] get_xray_thin_integ_dist(ddist=dict) implemented --- .../_xray_thin_target_integrated_dist.py | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py index b94c759db..b2fd2195f 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py @@ -52,6 +52,8 @@ def get_xray_thin_integ_dist( # ---------------- # electron distribution + ddist=None, + # compute Te_eV=None, ne_m3=None, nZ_m3=None, @@ -68,6 +70,9 @@ def get_xray_thin_integ_dist( dominant=None, # ---------------- # cross-section + # tabulated d2cross_phi + d2cross_phi=None, + # d2cross_phi computation E_ph_eV=None, E_e0_eV=None, E_e0_eV_npts=None, @@ -85,7 +90,6 @@ def get_xray_thin_integ_dist( # output customization version_cross=None, # save / load - pfe_d2cross_phi=None, save_d2cross_phi=None, # --------------------- # optional responsivity @@ -166,17 +170,18 @@ def get_xray_thin_integ_dist( msg = "Computing e distributions..." print(msg) - ddist = get_distribution( - # Energy, theta - E_eV=E_e0_eV, - theta=theta_e0_vsB, - # version - version='f3d_E_theta', - returnas=dict, - # plasma parameters - dominant=dominant, - **{kk: vv['data'] for kk, vv in dplasma.items()} - ) + if ddist is None: + ddist = get_distribution( + # Energy, theta + E_eV=E_e0_eV, + theta=theta_e0_vsB, + # version + version='f3d_E_theta', + returnas=dict, + # plasma parameters + dominant=dominant, + **{kk: vv['data'] for kk, vv in dplasma.items()} + ) # shape shape_plasma = ddist['plasma']['Te_eV']['data'].shape @@ -307,7 +312,10 @@ def get_xray_thin_integ_dist( iok = np.isfinite(demiss[kdist]['emiss']['data']) iok[iok] = demiss[kdist]['emiss']['data'][iok] >= 0. if np.any(~iok): - msg = f"\nSome non-finite or negative values in emiss {kdist} !\n" + msg = ( + f"\n({(~iok).sum()} / {iok.size}) non-finite or " + f"negative values in emiss '{kdist}' !\n" + ) warnings.warn(msg) # --------------------- @@ -652,10 +660,18 @@ def _responsivity( raise Exception(msg) # ph vs E + if 'ph' in str(dresponsivity['responsivity']['units']): + ph_vs_E_def = 'ph' + elif 'W' in str(dresponsivity['responsivity']['units']): + ph_vs_E_def = 'E' + else: + ph_vs_E_def = None + dresponsivity['ph_vs_E'] = ds._generic_check._check_var( dresponsivity['ph_vs_E'], 'ph_vs_E', types=str, allowed=['ph', 'E'], + default=ph_vs_E_def, extra_msg="dresponsivity['ph_vs_E'] integrated photons or energy", ) From 59b2ba7027388abdc9ab882d170800c77ec3d4c8 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 12 Feb 2026 19:55:11 +0000 Subject: [PATCH 27/80] [#1166] get_d2cross_phi(d2cross_phi=dict or str) implemented --- ..._xray_thin_target_integrated_d2crossphi.py | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py index 03cc5d667..f345594a9 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py @@ -39,7 +39,7 @@ def get_d2cross_phi( # load from file - pfe=None, + d2cross_phi=None, # tabulated d2cross d2cross=None, # params @@ -63,7 +63,6 @@ def get_d2cross_phi( # verb verb=None, # load / save - d2cross_phi=None, save=None, pfe_save=None, overwrite=None, @@ -76,9 +75,9 @@ def get_d2cross_phi( # ---------------- ( - save, overwrite, pfe, verb, + save, overwrite, d2cross_phi, verb, ) = _check( - pfe=pfe, + d2cross_phi=d2cross_phi, save=save, overwrite=overwrite, pfe_save=pfe_save, @@ -89,7 +88,7 @@ def get_d2cross_phi( # compute # ---------------- - if pfe is None: + if d2cross_phi is None: # check compute ( @@ -116,8 +115,8 @@ def get_d2cross_phi( # load # ---------------- - else: - d2cross_phi = _load(**locals()) + elif isinstance(d2cross_phi, str): + d2cross_phi = _load(pfe=d2cross_phi, **locals()) return d2cross_phi @@ -129,7 +128,7 @@ def get_d2cross_phi( def _check( - pfe=None, + d2cross_phi=None, save=None, overwrite=None, pfe_save=None, @@ -162,16 +161,25 @@ def _check( # pfe # ------------- - if pfe is not None: - - c0 = ( - isinstance(pfe, str) - and os.path.isfile(pfe) - and pfe.endswith('.npz') - ) - if not c0: + if d2cross_phi is not None and not isinstance(d2cross_phi, dict): + + dc = { + 'str': isinstance(d2cross_phi, str), + 'file': ( + isinstance(d2cross_phi, str) + and os.path.isfile(d2cross_phi) + ), + '.npz': ( + isinstance(d2cross_phi, str) + and d2cross_phi.endswith('.npz') + ), + } + if not all([vv for vv in dc.values()]): + lstr = [f"\t- {kk}: {vv}" for kk, vv in dc.items()] msg = ( - "Arg pfe must be a valid path to a .npz file!" + "Arg pfe must be a valid path to a .npz file!\n" + + "\n".join(lstr) + + f"\nProvided: {d2cross_phi}\n" ) raise Exception(msg) save = False @@ -189,7 +197,7 @@ def _check( )) return ( - save, overwrite, pfe, verb, + save, overwrite, d2cross_phi, verb, ) @@ -624,7 +632,7 @@ def _get_interpolator( cross, method='linear', bounds_error=False, - fill_value=np.nan, + fill_value=0., ) # --------------- From 3f9eb8b667441b3b6eb5a7ab4465b135b457f677 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 12 Feb 2026 19:56:50 +0000 Subject: [PATCH 28/80] [#1166] plot_xray_thin_integ_dist_filter_anisotropy() - advancing demiss + plotting dresp['theta_ph_vs_B'] --- ...arget_integrated_dist_responsivity_plot.py | 151 ++++++++++++++++-- 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 76df3593d..649a6d51f 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -15,6 +15,8 @@ # from . import _xray_thin_target_integrated as _mod from . import _xray_thin_target_integrated_plot as _mod_plot +from . import _xray_thin_target_integrated_dist as _mod_dist +from .. import distribution from ... import transmission # from ..distribution import get_distribution @@ -61,7 +63,7 @@ # DRESP _DRESP = {} _DRESP_FORMAT = { - 'E_ph': {'data': np.ndarray, 'units': _UNITS}, + 'E_eV': {'data': np.ndarray, 'units': _UNITS}, 'responsivity': {'data': np.ndarray, 'units': _UNITS}, 'marker': 'None', 'lw': 1, @@ -133,6 +135,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # optional input d2cross file d2cross: Optional[str | dict] = None, + d2cross_phi: Optional[str | dict] = None, # target ion charge Z: Optional[int] = None, # Energy @@ -156,7 +159,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( dtrans: Optional[dict] = None, dranges: Optional[dict] = None, # verb - verb: Optional[bool] = None, + verb: Optional[bool | int] = None, # plot dax: Optional[dict] = None, fs: Optional[tuple] = None, @@ -186,16 +189,12 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # --------------- ( - ddist, dresp, + ddist, ddist1d, dresp, dcases_dist_resp, dscales, fs, fontsize, ) = _check(**locals()) - # --------------- - # prepare data - # --------------- - # -------------- # prepare axes # -------------- @@ -243,6 +242,28 @@ def plot_xray_thin_integ_dist_filter_anisotropy( dplot_mean=dplot_mean, ) + # --------------- + # compute emiss + # --------------- + + demiss = {} + for kcase, vcase in dcases_dist_resp.items(): + + msg = f"\n\tComputing emiss for case '{kcase}'" + print(msg) + + demiss[kcase], ddist, d2cross_phi = _mod_dist.get_xray_thin_integ_dist( + ddist={ + 'plasma': ddist['plasma'], + 'dist': {vcase['dist']: ddist['dist'][vcase['dist']]}, + 'coords': ddist['coords'], + }, + d2cross_phi=d2cross_phi, + dresponsivity=dresp[vcase['resp']], + plot_responsivity_integration=False, + verb=verb, + ) + # ------------------- # Compute integrand # ------------------- @@ -280,7 +301,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( l0, = ax.plot( vv['responsivity']['data'], - vv['E_ph']['data']*1e-3, + vv['E_eV']['data']*1e-3, c=vv.get('color'), ls=vv.get('ls', '-'), marker=vv.get('marker'), @@ -360,7 +381,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if dax.get(kax) is not None: ax = dax[kax]['handle'] - for kk, vv in ddist.items(): + for kk, vv in ddist1d.items(): l0, = ax.semilogy( vv['E_e0']['data']*1e-3, @@ -379,6 +400,38 @@ def plot_xray_thin_integ_dist_filter_anisotropy( fontweight='bold', ) + # ------------------- + # plot emiss + # ------------------- + + kax = 'theta_emiss_norm' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # ----------- + # emissivity + + for kk, vv in ddist1d.items(): + pass + + # -------------------------- + # responsivity theta_ph_vs_B + + ktheta = 'theta_ph_vs_B' + for kk, vv in dresp.items(): + + if vv.get(ktheta) is None: + continue + + for (theta0, theta1) in vv[kk]: + ax.axvspan( + theta0, + theta1, + facecolr=vv['color'], + alpha=vv.get('alpha', 0.5), + label=kk, + ) + return dax @@ -421,6 +474,35 @@ def _check( ddef=_DRESP_FORMAT, ) + # ---------------------- + # optional theta_ph_vs_B + + kk = 'theta_ph_vs_B' + for k0, v0 in dresp.items(): + if v0.get(kk) is not None: + + vv = np.atleast_2d(v0[kk]).astype(float) + iok = np.isfinite(vv) + iok[iok] = (vv[iok] >= 0.) & (vv[iok] <= np.pi) + if not np.all(iok) or vv.shape[1] != 2: + msg = ( + f"Arg dresp['{k0}']['{kk}'] must be:\n" + f"\t- (2, N) array of floats\n" + f"\t- All finite values in [0, pi]\n" + f"Provided:\n{vv}\n" + ) + raise Exception(msg) + + if np.any(np.diff(vv, axis=0) <= 0): + msg = ( + f"Arg dresp['{k0}']['{kk}'] must be:\n" + f"\t- strictly increasing along axis=0\n" + f"Provided:\n{vv}\n" + ) + raise Exception(msg) + + dresp[k0][kk] = vv + # ------------ # ddist # ------------ @@ -431,11 +513,48 @@ def _check( if ddist is False: ddist = {} - ddist = _check_dict( - din=ddist, - din_name='ddist', - ddef=_DDIST_FORMAT, + c0 = ( + isinstance(ddist, dict) + and isinstance(ddist.get('dist'), dict) + and isinstance(ddist['coords'].get('x0'), dict) + and isinstance(ddist['coords'].get('x1'), dict) ) + if not c0: + msg = ( + "Arg ddist must be a dict of the form:\n" + "{" + "\t'dist': dict,\n" + "\t'coords': {'x0': dict, 'x1': dict}\n" + "}\n" + f"Provided: {ddist}\n" + ) + raise Exception(msg) + + for k0, v0 in ddist['dist'].items(): + + c0 = ( + isinstance(k0, str) + and isinstance(v0.get('dist'), dict) + and isinstance(v0['dist'].get('data'), np.ndarray) + ) + if not c0: + msg = ( + f"Arg ddist['dist']['{k0}']['dist'] must be a dict of the form:\n" + "{" + "\t'data': np.ndarray,\n" + "\t'units': str\n" + "}\n" + f"Provided: {v0}\n" + ) + raise Exception(msg) + + # ------------ + # dist1d + # ------------ + + ddist1d = {} + for k0, v0 in ddist['dist'].items(): + ddist1d[k0] = distribution.get_dist1d_E(ddist, k0, nbins=None) # ------------ # dcases @@ -486,7 +605,7 @@ def _check( ) return ( - ddist, dresp, + ddist, ddist1d, dresp, dcases_dist_resp, dscales, fs, fontsize, @@ -627,7 +746,7 @@ def _check_dcases( if dcases is None: dcases = {} - for kdist in ddist.keys(): + for kdist in ddist['dist'].keys(): for kresp in dresp.keys(): key = f"{kresp}_{kdist}" dcases[key] = { @@ -649,7 +768,7 @@ def _check_dcases( ) and ( isinstance(vcase.get('dist'), str) - and vcase['dist'] in ddist.keys() + and vcase['dist'] in ddist['dist'].keys() ) for kcase, vcase in dcases.items() ]) From 53d47084287c65a6b4b019ad6ee7cd6c3306385c Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 12 Feb 2026 23:49:46 +0000 Subject: [PATCH 29/80] [#1166] Debugging emiss and resp nan --- .../_xray_thin_target_integrated_dist.py | 11 +++ ...arget_integrated_dist_responsivity_plot.py | 78 ++++++++++++++++--- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py index b2fd2195f..b86fec363 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py @@ -283,6 +283,9 @@ def get_xray_thin_integ_dist( if debug is not False and debug(ind) is True: _plot_debug(**locals()) + if np.any(np.isnan(demiss['maxwell']['emiss']['data'])): + import pdb; pdb.set_trace() # DB + # ---------------- # prepare output # ---------------- @@ -696,6 +699,14 @@ def _responsivity( right=0, ) + # -------------- + # safety check + # -------------- + + # resp = nan => 0 + if np.any(np.isnan(resp_data)): + import pdb; pdb.set_trace() # DB + # -------------- # compute # -------------- diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 649a6d51f..8486503b8 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -384,15 +384,16 @@ def plot_xray_thin_integ_dist_filter_anisotropy( for kk, vv in ddist1d.items(): l0, = ax.semilogy( - vv['E_e0']['data']*1e-3, - vv['dist']['data'], - c=vv['color'], + vv['E']['data']*1e-3, + vv['dist']['data'].ravel(), + c=vv.get('color', 'k'), ls=vv.get('ls', '-'), marker=vv.get('marker'), lw=vv.get('lw', 1.), label=kk, ) - ddist[kk]['color'] = l0.get_color() + ddist['dist'][kk]['color'] = l0.get_color() + ddist1d[kk]['color'] = l0.get_color() ax.set_ylabel( vv['dist']['units'], @@ -401,7 +402,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( ) # ------------------- - # plot emiss + # plot emiss - norm # ------------------- kax = 'theta_emiss_norm' @@ -411,8 +412,21 @@ def plot_xray_thin_integ_dist_filter_anisotropy( # ----------- # emissivity - for kk, vv in ddist1d.items(): - pass + for k0, v0 in demiss.items(): + + kdist = dcases_dist_resp[k0]['dist'] + kresp = dcases_dist_resp[k0]['resp'] + yy = v0['emiss'][kdist]['emiss_integ']['data'][0, ...] + yy = yy / np.nanmax(yy) + + ax.plot( + v0['theta_ph_vsB']['data'], + yy, + color=dresp[kresp]['color'], + ls=ddist1d[kdist]['ls'], + lw=1, + label=k0, + ) # -------------------------- # responsivity theta_ph_vs_B @@ -423,16 +437,42 @@ def plot_xray_thin_integ_dist_filter_anisotropy( if vv.get(ktheta) is None: continue - for (theta0, theta1) in vv[kk]: + for (theta0, theta1) in vv[ktheta]: ax.axvspan( theta0, theta1, - facecolr=vv['color'], + facecolor=vv['color'], alpha=vv.get('alpha', 0.5), label=kk, ) - return dax + # ------------------- + # plot emiss - abs + # ------------------- + + kax = 'theta_emiss_abs' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # ----------- + # emissivity + + for k0, v0 in demiss.items(): + + kdist = dcases_dist_resp[k0]['dist'] + kresp = dcases_dist_resp[k0]['resp'] + yy = v0['emiss'][kdist]['emiss_integ']['data'][0, ...] + + ax.semilogy( + v0['theta_ph_vsB']['data'], + yy, + color=dresp[kresp]['color'], + ls=ddist1d[kdist]['ls'], + lw=1, + label=k0, + ) + + return dax, dresp, ddist, ddist1d, demiss # ############################################# @@ -548,13 +588,29 @@ def _check( ) raise Exception(msg) + # --------------------- + # make sure only one dist + + for k0, v0 in ddist['dist'].items(): + c0 = np.prod(v0['dist']['data'].shape) == v0['dist']['data'].size + if not c0: + msg = ( + f"Arg ddist['dist']['{k0}']['dist']['data'] must be:\n" + "\t- only vs E_e0: np.prod(shape) must be == size\n" + f"Provided shape = {v0['dist']['data'].shape}\n" + ) + raise Exception(msg) + # ------------ # dist1d # ------------ ddist1d = {} - for k0, v0 in ddist['dist'].items(): + lls = ['-', '--', ':', '-.'] + for ii, (k0, v0) in enumerate(ddist['dist'].items()): ddist1d[k0] = distribution.get_dist1d_E(ddist, k0, nbins=None) + ddist1d[k0]['ls'] = v0.get('ls', lls[ii % len(lls)]) + ddist1d[k0]['color'] = v0.get('color', 'k') # ------------ # dcases From f5ec3b7efa8d0f37cd3992e0bea79571d269e132 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 17:09:14 +0000 Subject: [PATCH 30/80] [#1166] d3cross now more robust vs nan --- .../electrons/emission/_xray_thin_target.py | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index 215cea37a..e28393912 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -484,25 +484,49 @@ def _get_cross( eps1=eps1, ) + # ---------------- + # valid points + # ---------------- + + ivalid = ( + (q2 > 0.) + & (kk < eps0) + & (D0D1 > 0.) + & (mu > 0.) + ) + + # adjust + lout = ['iok', 'ivalid'] + kwd = {k0: v0 for k0, v0 in locals().items() if k0 not in lout} + if np.any(~ivalid): + larr = [k0 for k0, v0 in kwd.items() if isinstance(v0, np.ndarray)] + for k0 in larr: + kwd[k0] = kwd[k0][ivalid] + # ---------------- # loop on versions # ---------------- for vv in version: + # ----------- + # initialize + + cross = np.zeros(ivalid.shape, dtype=float) + # ----------- # Elwert-Haug if vv == 'EH': - cross, dcrit = _cross_ElwertHaug(**locals()) + cross[ivalid], dcrit = _cross_ElwertHaug(**kwd) # ------------- # Bethe-Heitler else: - cross, dcrit = _cross_BetheHeitler(**locals()) + cross[ivalid], dcrit = _cross_BetheHeitler(**kwd) # optional Elwert factor if vv == 'BHE': @@ -555,7 +579,13 @@ def _cross_BetheHeitler( term0 = scpct.alpha * Z**2 * (r0/np.pi)**2 term1 = p1 / p0 - term2 = kk / q2**2 + + # very conservative handling of q2 == 0 + # should do better though, q2 = 0 => term2 = inf + # but d3cross vs q2 does not show inf + term2 = np.zeros(kk.shape, dtype=float) + iok = (q2 != 0.) + term2[iok] = kk[iok] / q2[iok]**2 # assembling in cross-section d3cross_ei = ( @@ -662,13 +692,6 @@ def _cross_ElwertHaug( # hypergeometric variable xx = 1. - mu*q2 / D0D1 - # safety check - assert np.all(kk < eps0) - assert np.all(D0D1 > 0.) - assert np.all(mu > 0.) - assert np.all(q2 > 0.) - assert np.all(xx < 1.) - # hypergeometric functions # V = scpsp.hyp2f1(1j*a0, 1j*a1, 1., x) # W = scpsp.hyp2f1(1. + 1j*a0, 1. + 1j*a1, 2., x) From 793859883425fc7089a37ff998dba4b00a114ac5 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 17:57:03 +0000 Subject: [PATCH 31/80] [#1166] Added pbs jobs for d2cross --- .../electrons/emission/_pbs_d2cross.pbs | 18 ++ .../electrons/emission/_pbs_d2cross.py | 155 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs create mode 100644 tofu/physics_tools/electrons/emission/_pbs_d2cross.py diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs new file mode 100644 index 000000000..76f54a016 --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -0,0 +1,18 @@ +#PBS -S /bin/bash +#PBS -q normal +#PBS -l nodes=1 +#PBS -l instance_type=c7i.8xlarge +#PBS -l walltime=24:00:00 +#PBS -P sparc-design +#PBS -N d2cross_3x400x401_BHE +#PBS -o d2cross_3x400x401_BHE.out +#PBS -e d2cross_3x400x401_BHE.err +#PBS -m abe +#PBS -l base_os=ubuntu2404 +#PBS -l instance_ami=ami-0d63a2e021bce724d +#PBS -l efa_support=false + +cd $PBS_O_WORKDIR/ +source ~/.bashrc + +python $PBS_O_WORKDIR/_pbs_d2cross_EH.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py new file mode 100644 index 000000000..44cc2b684 --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -0,0 +1,155 @@ + +import os +import sys +import argparse + + +import numpy as np + + +_PATH_TOFU = os.path.join( + os.path.expand_user('~'), + 'projects', + 'tofu', +) +sys.path.insert(0, _PATH_TOFU) +import tofu as tf + + +# ########################################### +# ########################################### +# Main +# ########################################### + + +def main( + nEph=None, + nEe0=None, + ntheta=None, + version=None, + ddef=None, +): + + # ------------------- + # msg + + lstr = [f"\t- {k0}: {v0}" for k0, v0 in locals().items()] + msg = ( + f"\ntofu file: {tf.__file__}\n\n" + f"Script: {__file__}\n" + "Input args:\n" + + "\n".join(lstr) + ) + print(msg) + + # ------------------- + # inputs + + if nEph is None: + nEph = ddef['nEph'] + if nEe0 is None: + nEe0 = ddef['nEe0'] + if ntheta is None: + ntheta = ddef['ntheta'] + if version is None: + version = ddef['version'] + + E_ph_eV = np.logspace(0, 8, nEph) + E_e0_eV = np.logspace(0, 8, nEe0) + theta_ph = np.linspace(0, np.pi, ntheta) + + # ------------------- + # call + + _mod = tf.physics_tools.electrons.emission + d2cross = _mod.get_xray_thin_d2cross_ei_integrated_thetae_dphi( + E_e0_eV=E_e0_eV[None, :, None], + E_ph_eV=E_ph_eV[None, None, :], + theta_ph=theta_ph[:, None, None], + save=True, + verb=2, + version='BHE', + ) + + return + + +# ########################################### +# ########################################### +# __main__ +# ########################################### + + +if __name__ == '__main__': + + # ----------- + # default + # ----------- + + msg = ( + "Tabulate d2cross over desired grid (E_ph, E_e0, theta_ph)" + ) + + ddef = { + 'nEph': 401, + 'nEe0': 400, + 'ntheta': 181, + 'version': 'EH', + } + + # ----------- + # parse args + # ----------- + + # Instanciate parser + parser = argparse.ArgumentParser(description=msg) + + # nEph + parser.add_argument( + '-nEph', + '--nEph_eV', + type=int, + help='Number of np.logspace(0, 8, nEph)', + required=False, + default=ddef['nEph'], + ) + + # nEe0 + parser.add_argument( + '-nEe0', + '--nEe0_eV', + type=int, + help='Number of np.logspace(0, 8, nEe0)', + required=False, + default=ddef['nEe0'], + ) + + # ntheta + parser.add_argument( + '-ntheta', + '--ntheta_ph', + type=int, + help='Number of np.linspace(0, np.pi, ntheta)', + required=False, + default=ddef['ntheta'], + ) + + # version + parser.add_argument( + '-v', + '--version', + type=str, + help="version of the cross-section in ['BHE', 'EH']", + required=False, + default=ddef['version'], + ) + + # ----------- + # call main + # ----------- + + # Parse arguments + args = parser.parse_args() + + # Call function + main(ddef=ddef, **dict(args._get_kwargs())) From f1d8d31df3d198fc75c4512bb1ee7fc31f7190f2 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:00:46 +0000 Subject: [PATCH 32/80] [#1166] Better d2cross file naming --- .../electrons/emission/_xray_thin_target_integrated.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index ff9fcd339..b2b6b05ea 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -516,9 +516,12 @@ def _save( base=d2cross['E_e0']['units'], ) + # extract versions + versions = '-'.join(sorted(d2cross['cross'].keys())) + # extract boundaries path = os.path.abspath(_PATH_HERE) - fname = f"d2cross_Ee0{Ee0}_Eph{Eph}_ntheta{ntheta}" + fname = f"d2cross_Ee0{Ee0}_Eph{Eph}_ntheta{ntheta}_{versions}" pfe_save = os.path.join(path, f"{fname}.npz") # ---------- From f5d6a77023c417cbec975486ff8bc685ceed2359 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:04:31 +0000 Subject: [PATCH 33/80] [#1166] Updated file name --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index 76f54a016..a484d0927 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -15,4 +15,4 @@ cd $PBS_O_WORKDIR/ source ~/.bashrc -python $PBS_O_WORKDIR/_pbs_d2cross_EH.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE +python $PBS_O_WORKDIR/_pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE From 89ac242393fe39825dba3ce4ae1c477644f8bf05 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:07:21 +0000 Subject: [PATCH 34/80] [#1166] Adjust ~/projects/tofu/tofu/physics_tools/electrons/emission in _pbs_d2cross.pbs --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index a484d0927..ea65dbbf7 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -12,7 +12,8 @@ #PBS -l instance_ami=ami-0d63a2e021bce724d #PBS -l efa_support=false -cd $PBS_O_WORKDIR/ source ~/.bashrc +export PBS_O_WORKDIR="~/projects/tofu/tofu/physics_tools/electrons/emission" +cd $PBS_O_WORKDIR/ python $PBS_O_WORKDIR/_pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE From be47ca1291a94f610a682e592ed5a00b3b59ec03 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:26:51 +0000 Subject: [PATCH 35/80] [#1166] Debug PBS --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index ea65dbbf7..adf2965a0 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -14,6 +14,7 @@ source ~/.bashrc -export PBS_O_WORKDIR="~/projects/tofu/tofu/physics_tools/electrons/emission" +# Beware: PBS does not expand the tilde ~/ => full explicit path needed +export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" cd $PBS_O_WORKDIR/ python $PBS_O_WORKDIR/_pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE From 6b193f4fde9d472a8ce9f37b852ee4b809e7d524 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:37:02 +0000 Subject: [PATCH 36/80] [#1166] cleanup --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index adf2965a0..cf4740a67 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -16,5 +16,5 @@ source ~/.bashrc # Beware: PBS does not expand the tilde ~/ => full explicit path needed export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" -cd $PBS_O_WORKDIR/ -python $PBS_O_WORKDIR/_pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE +cd ${PBS_O_WORKDIR}/ +python _pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE From 8d84ddd5205611cf0d87c28da9102c5d4f43fe63 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:38:06 +0000 Subject: [PATCH 37/80] [#1166] cleanup 2 --- tofu/physics_tools/electrons/emission/_pbs_d2cross.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py index 44cc2b684..3a0117a2d 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -8,7 +8,7 @@ _PATH_TOFU = os.path.join( - os.path.expand_user('~'), + os.path.expanduser('~'), 'projects', 'tofu', ) From f4176566da01020eac85aba677c8c0505b072397 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 18:53:06 +0000 Subject: [PATCH 38/80] [#1166] Debug PBS --- .../physics_tools/electrons/emission/_pbs_d2cross.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py index 3a0117a2d..169bc2514 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -107,9 +107,9 @@ def main( # nEph parser.add_argument( '-nEph', - '--nEph_eV', + '--nEph', type=int, - help='Number of np.logspace(0, 8, nEph)', + help='Number of np.logspace(0, 8, nEph) (eV)', required=False, default=ddef['nEph'], ) @@ -117,9 +117,9 @@ def main( # nEe0 parser.add_argument( '-nEe0', - '--nEe0_eV', + '--nEe0', type=int, - help='Number of np.logspace(0, 8, nEe0)', + help='Number of np.logspace(0, 8, nEe0) (eV)', required=False, default=ddef['nEe0'], ) @@ -127,9 +127,9 @@ def main( # ntheta parser.add_argument( '-ntheta', - '--ntheta_ph', + '--ntheta', type=int, - help='Number of np.linspace(0, np.pi, ntheta)', + help='Number of np.linspace(0, np.pi, ntheta) (rad)', required=False, default=ddef['ntheta'], ) From 5d64a515777f42ff961ccf97e13483d6f88d5b9a Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 19:04:18 +0000 Subject: [PATCH 39/80] [#1166] PBS operational --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index cf4740a67..54d1dc5f5 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -4,9 +4,9 @@ #PBS -l instance_type=c7i.8xlarge #PBS -l walltime=24:00:00 #PBS -P sparc-design -#PBS -N d2cross_3x400x401_BHE -#PBS -o d2cross_3x400x401_BHE.out -#PBS -e d2cross_3x400x401_BHE.err +#PBS -N d2cross_181x400x401_BHE +#PBS -o d2cross_181x400x401_BHE.out +#PBS -e d2cross_181x400x401_BHE.err #PBS -m abe #PBS -l base_os=ubuntu2404 #PBS -l instance_ami=ami-0d63a2e021bce724d @@ -17,4 +17,4 @@ source ~/.bashrc # Beware: PBS does not expand the tilde ~/ => full explicit path needed export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" cd ${PBS_O_WORKDIR}/ -python _pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 3 -v BHE +python _pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 181 -v BHE From a3a11cc03769df459c57766d26a1cd13a2987286 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 19:25:36 +0000 Subject: [PATCH 40/80] [#1166] Try d2cross 9x10x11 EH + add timedelta print to pbs job output --- .../electrons/emission/_pbs_d2cross.pbs | 8 ++++---- .../electrons/emission/_pbs_d2cross.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index 54d1dc5f5..feb57f190 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -4,9 +4,9 @@ #PBS -l instance_type=c7i.8xlarge #PBS -l walltime=24:00:00 #PBS -P sparc-design -#PBS -N d2cross_181x400x401_BHE -#PBS -o d2cross_181x400x401_BHE.out -#PBS -e d2cross_181x400x401_BHE.err +#PBS -N d2cross_9x10x11_EH +#PBS -o d2cross_9x10x11_EH.out +#PBS -e d2cross_9x10x11_EH.err #PBS -m abe #PBS -l base_os=ubuntu2404 #PBS -l instance_ami=ami-0d63a2e021bce724d @@ -17,4 +17,4 @@ source ~/.bashrc # Beware: PBS does not expand the tilde ~/ => full explicit path needed export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" cd ${PBS_O_WORKDIR}/ -python _pbs_d2cross.py -nEph 401 -nEe0 400 -ntheta 181 -v BHE +python _pbs_d2cross.py -nEph 11 -nEe0 10 -ntheta 9 -v EH diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py index 169bc2514..9882b1bef 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -2,6 +2,7 @@ import os import sys import argparse +import datetime as dtm import numpy as np @@ -30,6 +31,11 @@ def main( ddef=None, ): + # ------------------ + # timing + + t0 = dtm.datetime.now() + # ------------------- # msg @@ -71,6 +77,13 @@ def main( version='BHE', ) + # ------------------ + # timing + + dt = (dtm.datetime.now() - t0).total_seconds() + msg = f"\nCPU time = {dt/60} min" + print(msg) + return From f721da6b931d14596559d0056e21087d02c635b3 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 19:36:53 +0000 Subject: [PATCH 41/80] [#1166] Debug version in _pbs_d2cross.py --- tofu/physics_tools/electrons/emission/_pbs_d2cross.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py index 9882b1bef..aa7838af3 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -74,7 +74,7 @@ def main( theta_ph=theta_ph[:, None, None], save=True, verb=2, - version='BHE', + version=version, ) # ------------------ From 536e348c6c6147d11d3fcef358d109361b0880c1 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 21:27:46 +0000 Subject: [PATCH 42/80] [#1166] added version to save file name for d2cross_phi + added pbs for d2cross_phi --- .../electrons/emission/_pbs_d2cross_phi.pbs | 22 ++ .../electrons/emission/_pbs_d2cross_phi.py | 206 ++++++++++++++++++ ..._xray_thin_target_integrated_d2crossphi.py | 5 +- 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.pbs create mode 100644 tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.py diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.pbs new file mode 100644 index 000000000..ce197621c --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.pbs @@ -0,0 +1,22 @@ +#PBS -S /bin/bash +#PBS -q normal +#PBS -l nodes=1 +#PBS -l instance_type=c7i.8xlarge +#PBS -l walltime=24:00:00 +#PBS -P sparc-design +#PBS -N d2cross_phi_241x91x240x93_BHE +#PBS -o d2cross_phi_241x91x240x93_BHE.out +#PBS -e d2cross_phi_241x91x240x93_BHE.err +#PBS -m abe +#PBS -l base_os=ubuntu2404 +#PBS -l instance_ami=ami-0d63a2e021bce724d +#PBS -l efa_support=false + +source ~/.bashrc + +# Beware: PBS does not expand the tilde ~/ => full explicit path needed +export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" +export D2CROSS="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission/d2cross_Ee01eV-100MeV-400log_Eph1eV-100MeV-401log_ntheta181.npz" + +cd ${PBS_O_WORKDIR}/ +python _pbs_d2cross_phi.py -nEph 241 -nEe0 240 -ntheta_ph_vsB 91 -ntheta_e0_vsB 93 -nphi_e0_vsB 181 -d2cross ${D2CROSS} diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.py new file mode 100644 index 000000000..e74fe5b03 --- /dev/null +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross_phi.py @@ -0,0 +1,206 @@ + +import os +import sys +import argparse +import datetime as dtm + + +import numpy as np + + +_PATH_TOFU = os.path.join( + os.path.expanduser('~'), + 'projects', + 'tofu', +) +sys.path.insert(0, _PATH_TOFU) +import tofu as tf + + +# ########################################### +# ########################################### +# DEFAULTS +# ########################################### + + +_DDEF = { + 'd2cross': '', + 'nEph': 241, + 'nEe0': 240, + 'ntheta_ph_vsB': 91, + 'ntheta_e0_vsB': 93, + 'nphi_e0_vsB': 181, +} + + +# ########################################### +# ########################################### +# Main +# ########################################### + + +def main( + d2cross=None, + nEph=None, + nEe0=None, + ntheta_ph_vsB=None, + ntheta_e0_vsB=None, + nphi_e0_vsB=None, + ddef=None, +): + + # ------------------ + # timing + + t0 = dtm.datetime.now() + + # ------------------- + # msg + + lstr = [f"\t- {k0}: {v0}" for k0, v0 in locals().items()] + msg = ( + f"\ntofu file: {tf.__file__}\n\n" + f"Script: {__file__}\n" + "Input args:\n" + + "\n".join(lstr) + ) + print(msg) + + # ------------------- + # inputs + + if d2cross is None: + d2cross = ddef['d2cross'] + + if nEph is None: + nEph = ddef['nEph'] + if nEe0 is None: + nEe0 = ddef['nEe0'] + if ntheta_ph_vsB is None: + ntheta_ph_vsB = ddef['ntheta_ph_vsB'] + if ntheta_e0_vsB is None: + ntheta_e0_vsB = ddef['ntheta_e0_vsB'] + if nphi_e0_vsB is None: + nphi_e0_vsB = ddef['nphi_e0_vsB'] + + E_ph_eV = np.logspace(0, 8, nEph) + E_e0_eV = np.logspace(0, 8, nEe0) + theta_ph_vsB = np.linspace(0, np.pi, ntheta_ph_vsB) + + # ------------------- + # call + + _mod = tf.physics_tools.electrons.emission + d2cross_phi = _mod.get_d2cross_phi( + d2cross=d2cross, + E_ph_eV=E_ph_eV, + E_e0_eV=E_e0_eV, + theta_ph_vsB=theta_ph_vsB, + theta_e0_vsB_npts=ntheta_e0_vsB, + phi_e0_vsB_npts=nphi_e0_vsB, + save=True, + verb=2, + ) + + # ------------------ + # timing + + dt = (dtm.datetime.now() - t0).total_seconds() + msg = f"\nCPU time = {dt/60} min" + print(msg) + + return + + +# ########################################### +# ########################################### +# __main__ +# ########################################### + + +if __name__ == '__main__': + + # ----------- + # default + # ----------- + + msg = ( + "Tabulate d2cross over desired grid (E_ph, E_e0, theta_ph)" + ) + + # ----------- + # parse args + # ----------- + + # Instanciate parser + parser = argparse.ArgumentParser(description=msg) + + # d2cross + parser.add_argument( + '-d2cross', + '--d2cross', + type=str, + help=' to an existing d2cross_...npz tabulation', + required=False, + default=_DDEF['d2cross'], + ) + + # nEph + parser.add_argument( + '-nEph', + '--nEph', + type=int, + help='Number of np.logspace(0, 8, nEph) (eV)', + required=False, + default=_DDEF['nEph'], + ) + + # nEe0 + parser.add_argument( + '-nEe0', + '--nEe0', + type=int, + help='Number of np.logspace(0, 8, nEe0) (eV)', + required=False, + default=_DDEF['nEe0'], + ) + + # ntheta_ph_vsB + parser.add_argument( + '-ntheta_ph_vsB', + '--ntheta_ph_vsB', + type=int, + help='Number of np.linspace(0, np.pi, ntheta) (rad)', + required=False, + default=_DDEF['ntheta_ph_vsB'], + ) + + # ntheta_e0_vsB + parser.add_argument( + '-ntheta_e0_vsB', + '--ntheta_e0_vsB', + type=int, + help='Number of np.linspace(0, np.pi, ntheta) (rad)', + required=False, + default=_DDEF['ntheta_e0_vsB'], + ) + + # nphi_e0_vsB + parser.add_argument( + '-nphi_e0_vsB', + '--nphi_e0_vsB', + type=int, + help='Number of np.linspace(-np.pi, np.pi, ntheta) (rad)', + required=False, + default=_DDEF['nphi_e0_vsB'], + ) + + # ----------- + # call main + # ----------- + + # Parse arguments + args = parser.parse_args() + + # Call function + main(ddef=_DDEF, **dict(args._get_kwargs())) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py index f345594a9..9c493305d 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py @@ -773,11 +773,14 @@ def _save( base='eV', ) + # extract versions + versions = d2cross_phi['version_cross'] + # extract boundaries path = os.path.abspath(_PATH_HERE) fname = ( f"d2cross_phi_Ee0{Ee0}_Eph{Eph}" - f"_nthetaph{ntheta_ph}_nthetae0{ntheta_e0}" + f"_nthetaph{ntheta_ph}_nthetae0{ntheta_e0}_{versions}" ) pfe_save = os.path.join(path, f"{fname}.npz") From 129fcbb59cb3ec6e4e94298844e04ba247868c9b Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 21:28:20 +0000 Subject: [PATCH 43/80] [#1166] pbd d2cross_phi larger (91x200x201 EH) --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index feb57f190..28c2113ae 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -4,9 +4,9 @@ #PBS -l instance_type=c7i.8xlarge #PBS -l walltime=24:00:00 #PBS -P sparc-design -#PBS -N d2cross_9x10x11_EH -#PBS -o d2cross_9x10x11_EH.out -#PBS -e d2cross_9x10x11_EH.err +#PBS -N d2cross_91x200x201_EH +#PBS -o d2cross_91x200x201_EH.out +#PBS -e d2cross_91x200x201_EH.err #PBS -m abe #PBS -l base_os=ubuntu2404 #PBS -l instance_ami=ami-0d63a2e021bce724d @@ -17,4 +17,4 @@ source ~/.bashrc # Beware: PBS does not expand the tilde ~/ => full explicit path needed export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" cd ${PBS_O_WORKDIR}/ -python _pbs_d2cross.py -nEph 11 -nEe0 10 -ntheta 9 -v EH +python _pbs_d2cross.py -nEph 201 -nEe0 200 -ntheta 91 -v EH From d0c7b906240d8da056edf91b17d073de275ff43e Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 22:38:52 +0000 Subject: [PATCH 44/80] [#1166] Removed python 3.9 from tests --- .github/workflows/test-complete-matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-complete-matrix.yml b/.github/workflows/test-complete-matrix.yml index c8f0b1f6a..3d1d412cd 100644 --- a/.github/workflows/test-complete-matrix.yml +++ b/.github/workflows/test-complete-matrix.yml @@ -18,7 +18,7 @@ jobs: os: [ubuntu-latest] # macOS-latest: issue with latex + meson fails build # windows-latest: issue with TcL install error + meson fails to build - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11'] exclude: - python-version: ['3.11'] os: windows-latest From 6a601d9a7fbd55e49b1611efad508e8c9270b002 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 19 Feb 2026 22:51:04 +0000 Subject: [PATCH 45/80] [#1166] updated python and spectrally versions --- meson.build | 2 +- pyproject.toml | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/meson.build b/meson.build index 60fa8a631..5ef83b7f2 100644 --- a/meson.build +++ b/meson.build @@ -39,7 +39,7 @@ os = import('fs') # ------------------------ min_numpy_version = '1.26.4' # keep in sync with pyproject.toml -min_python_version = '3.9' # keep in sync with pyproject.toml +min_python_version = '3.10' # keep in sync with pyproject.toml python_version = py.language_version() if python_version.version_compare(f'<@min_python_version@') diff --git a/pyproject.toml b/pyproject.toml index 34ac44344..e86230b2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ maintainers = [ keywords = [ "tomography", "fusion", "synthetic diagnostic", "diagnostic design", ] -requires-python = ">=3.9" +requires-python = ">=3.10,<3.12" dependencies = [ "numpy<1.25", # for astropy compatibility vs deprecated np.product # "PySide2 ; platform_system != 'Windows'", @@ -58,10 +58,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Natural Language :: English", @@ -81,7 +77,7 @@ tofu = "scripts.main:main" [project.optional-dependencies] full = [ "Polygon3", - 'spectrally>=0.0.9', + 'spectrally>=0.0.10', 'pytest', # For testing 'pytest-xvfb', # For GUI testing 'pytest-cov', # For coverage From e63c40f9e400463bed9ca84a935da99261124d05 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Feb 2026 15:24:20 +0000 Subject: [PATCH 46/80] [#1166] nan-check on responsivity --- .../emission/_xray_thin_target_integrated_dist.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py index b86fec363..35d09b4aa 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py @@ -283,9 +283,6 @@ def get_xray_thin_integ_dist( if debug is not False and debug(ind) is True: _plot_debug(**locals()) - if np.any(np.isnan(demiss['maxwell']['emiss']['data'])): - import pdb; pdb.set_trace() # DB - # ---------------- # prepare output # ---------------- @@ -688,6 +685,10 @@ def _responsivity( dresponsivity['E_eV']['data'].size == E_ph_eV.size and np.allclose(dresponsivity['E_eV']['data'], E_ph_eV) ) + + iok = np.isfinite(dresponsivity['responsivity']['data']) + dresponsivity['E_eV']['data'] = dresponsivity['E_eV']['data'][iok] + dresponsivity['responsivity']['data'] = dresponsivity['responsivity']['data'][iok] if c0: resp_data = dresponsivity['responsivity']['data'] else: @@ -705,7 +706,8 @@ def _responsivity( # resp = nan => 0 if np.any(np.isnan(resp_data)): - import pdb; pdb.set_trace() # DB + msg = "Some NaN in responisivity!" + raise Exception(msg) # -------------- # compute From e6c2f62d8021c58349b393a2f53671a0e79edbaa Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Feb 2026 15:24:45 +0000 Subject: [PATCH 47/80] [#1166] nan-check on d3cross --- .../electrons/emission/_xray_thin_target.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index e28393912..171f46210 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -230,6 +230,20 @@ def get_xray_thin_d3cross_ei( for vv in version: ddata['cross'][vv]['data'] *= coef + # ----------------- + # Safety check + # ----------------- + + for vv in version: + nnan = np.sum(~np.isfinite(ddata['cross'][vv]['data'])) + if nnan > 0: + size = ddata['cross'][vv]['data'].size + msg = ( + "Some non-finite values found in d3cross!\n" + f"\t- ddata['cross']['{vv}']['data'] => {nnan} / {size}\n" + ) + raise Exception(msg) + return ddata From dad2b9e035058444a7ab05e98f2b27d08f5b52ea Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Feb 2026 15:53:26 +0000 Subject: [PATCH 48/80] [#1166] finalized emiss plotting for _xray_thin_target_integrated_dist_responsivity_plot.main() --- ...ay_thin_target_integrated_dist_responsivity_plot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 8486503b8..f3ad870b5 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -420,7 +420,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( yy = yy / np.nanmax(yy) ax.plot( - v0['theta_ph_vsB']['data'], + v0['theta_ph_vsB']['data'] * 180/np.pi, yy, color=dresp[kresp]['color'], ls=ddist1d[kdist]['ls'], @@ -428,6 +428,10 @@ def plot_xray_thin_integ_dist_filter_anisotropy( label=k0, ) + ax.set_xlim(0, 180) + ax.set_ylim(0, 1) + ax.legend(loc='lower left', fontsize=fontsize) + # -------------------------- # responsivity theta_ph_vs_B @@ -464,7 +468,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( yy = v0['emiss'][kdist]['emiss_integ']['data'][0, ...] ax.semilogy( - v0['theta_ph_vsB']['data'], + v0['theta_ph_vsB']['data'] * 180/np.pi, yy, color=dresp[kresp]['color'], ls=ddist1d[kdist]['ls'], @@ -1150,7 +1154,7 @@ def _dax( nh = nhvert + nhcom + nhlarge + nhint + nhtheta ax = fig.add_subplot( gs[nv2:, nh:], - sharex=dax['theta_norm']['handle'], + sharex=dax['theta_emiss_norm']['handle'], yscale='log', ) ax.set_xlabel( From 6d610a01deb560c81cbb6f5417ca1b6ac7192cfb Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Feb 2026 20:49:03 +0000 Subject: [PATCH 49/80] [#1166] Implemented ad hoc 'bump' RE distribution --- .../electrons/distribution/_distribution.py | 4 ++++ .../distribution/_distribution_check.py | 11 ++++++++- .../distribution/_distribution_plot.py | 24 +++++++++++++------ .../distribution/_distribution_re.py | 7 ++++++ ...arget_integrated_dist_responsivity_plot.py | 4 ++-- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index 987a84bc4..69db0b9ba 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -37,6 +37,10 @@ def main( Efield_par_Vm=None, lnG=None, sigmap=None, + # bump + step=None, + pnormW=None, + # dominant dominant=None, # ------------ # coordinates diff --git a/tofu/physics_tools/electrons/distribution/_distribution_check.py b/tofu/physics_tools/electrons/distribution/_distribution_check.py index dc9a925f2..b3d58e3f1 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_check.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_check.py @@ -63,6 +63,15 @@ 'def': 0.1, 'units': '', }, + # bump + 'step': { + 'def': 50, + 'units': '', + }, + 'pnormW': { + 'def': 0.4, + 'units': '', + }, } @@ -256,7 +265,7 @@ def _plasma( # initialize lk = list(_DPLASMA.keys()) - dinputs = {kk: kwdargs[kk] for kk in lk} + dinputs = {kk: kwdargs.get(kk) for kk in lk} # coll coll = kwdargs.get('coll') diff --git a/tofu/physics_tools/electrons/distribution/_distribution_plot.py b/tofu/physics_tools/electrons/distribution/_distribution_plot.py index e12845eee..29dd4f020 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_plot.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_plot.py @@ -71,9 +71,9 @@ # DCOORDS _EMAX_EV = 20e6 _DCOORDS = { - 'E_eV': np.logspace(1, np.log10(_EMAX_EV), 201), - 'ntheta': 41, - 'nperp': 201, + 'E_eV': np.logspace(1, np.log10(_EMAX_EV), 401), + 'ntheta': 61, + 'nperp': 401, } @@ -96,6 +96,14 @@ def main( Efield_par_Vm=None, lnG=None, sigmap=None, + # bump + step=None, + p_perp_norm0=None, + p_perp_normW=None, + p_par_norm0=None, + p_par_normW=None, + # dominant + dominant=None, # ----------- # coordinates E_eV=None, @@ -133,6 +141,8 @@ def main( # version dist=('maxwell', 'RE'), version='f2d_E_theta', + # dominant + dominant=dominant, # plasma **{kk: vv['data'] for kk, vv in dplasma.items()}, ) @@ -450,9 +460,9 @@ def _plot( # legend & lims ax.legend(handles=lh, loc='upper right', fontsize=12) ax.set_xlim(left=0.) - ax.set_ylim( + ax.set_ylabel( f"integral ({ddist_E_num['maxwell']['units']})", - fontisize=fontsize, + fontsize=fontsize, fontweight='bold', ) @@ -563,9 +573,9 @@ def _plot( # legend & lims ax.legend(handles=lh, loc='upper right', fontsize=12) ax.set_xlim(left=0.) - ax.set_ylim( + ax.set_ylabel( f"integral ({ddist_pnorm_num['maxwell']['units']})", - fontisize=fontsize, + fontsize=fontsize, fontweight='bold', ) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_re.py b/tofu/physics_tools/electrons/distribution/_distribution_re.py index 61be11026..2f1456925 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_re.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_re.py @@ -8,6 +8,7 @@ from . import _distribution_maxwell as _maxwell from . import _distribution_dreicer as _dreicer from . import _distribution_avalanche as _avalanche +from . import _distribution_bump as _bump # ######################################################## @@ -20,6 +21,7 @@ 'maxwell': _maxwell, 'dreicer': _dreicer, 'avalanche': _avalanche, + 'bump': _bump, } @@ -27,6 +29,7 @@ 'dreicer': 0, 'avalanche': 1, 'maxwell': 2, + 'bump': 3, } @@ -152,6 +155,10 @@ def main( 'Cs': Cs[sli0], 'lnG': dplasma['lnG']['data'][sli0], 'p_crit': p_crit[sli0], + # bump + 'step': dplasma['step']['data'][sli0], + 'pnorm0': pmax[sli0], + 'pnormW': dplasma['pnormW']['data'][sli0], } # update with coords diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index f3ad870b5..762e953d3 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -443,8 +443,8 @@ def plot_xray_thin_integ_dist_filter_anisotropy( for (theta0, theta1) in vv[ktheta]: ax.axvspan( - theta0, - theta1, + theta0*180/np.pi, + theta1*180/np.pi, facecolor=vv['color'], alpha=vv.get('alpha', 0.5), label=kk, From 75ce24ee7103f770d5e21e0ded3f689acff431a5 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 26 Feb 2026 19:59:53 +0000 Subject: [PATCH 50/80] [#1166] safety check on E_e0_eV from ddist vs E_e0_eV from d2cross_phi --- .../_xray_thin_target_integrated_dist.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py index 35d09b4aa..1c97c24b3 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py @@ -184,11 +184,28 @@ def get_xray_thin_integ_dist( ) # shape + kdist0 = list(ddist['dist'].keys())[0] shape_plasma = ddist['plasma']['Te_eV']['data'].shape - shape_dist = ddist['dist']['maxwell']['dist']['data'].shape + shape_dist = ddist['dist'][kdist0]['dist']['data'].shape shape_cross = d2cross_phi['d2cross_phi']['data'].shape shape_emiss = shape_plasma + (E_ph_eV.size, theta_ph_vsB.size) + # ----------------------- + # Safety check on E_e0_eV (should be identical) + c0 = ( + E_e0_eV.shape == ddist['coords']['x0']['data'].shape + and np.allclose(E_e0_eV, ddist['coords']['x0']['data']) + ) + if not c0: + dshape = ddist['coords']['x0']['data'].shape + msg = ( + "ddist and d2cross_phi must have the same E_e0_eV vector!\n" + f"\t- ddist['coords']['x0']['data'].shape = {dshape}\n" + f"\t- d2cross_phi['E_e0_eV'].shape = {E_e0_eV.shape}\n" + f"\t- or values are different!\n" + ) + raise Exception(msg) + # ------------ # add nZ_m3 @@ -713,7 +730,8 @@ def _responsivity( # compute # -------------- - sli = [None]*demiss['maxwell']['emiss']['data'].ndim + kdist0 = list(demiss.keys())[0] + sli = [None]*demiss[kdist0]['emiss']['data'].ndim sli[-2] = slice(None) sli = tuple(sli) dintegrand = {} From c76ce928669d5bfd2a95496a3e2a45a8049223f6 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 26 Feb 2026 20:01:04 +0000 Subject: [PATCH 51/80] [#1166] minor debug + better legend --- ...in_target_integrated_dist_responsivity_plot.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py index 762e953d3..92db723c6 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_responsivity_plot.py @@ -8,7 +8,7 @@ import astropy.units as asunits import scipy.constants as scpct import matplotlib.pyplot as plt -# import matplotlib.lines as mlines +import matplotlib.lines as mlines import matplotlib.gridspec as gridspec import datastock as ds @@ -252,7 +252,7 @@ def plot_xray_thin_integ_dist_filter_anisotropy( msg = f"\n\tComputing emiss for case '{kcase}'" print(msg) - demiss[kcase], ddist, d2cross_phi = _mod_dist.get_xray_thin_integ_dist( + demiss[kcase], ddisti, d2cross_phi = _mod_dist.get_xray_thin_integ_dist( ddist={ 'plasma': ddist['plasma'], 'dist': {vcase['dist']: ddist['dist'][vcase['dist']]}, @@ -425,12 +425,19 @@ def plot_xray_thin_integ_dist_filter_anisotropy( color=dresp[kresp]['color'], ls=ddist1d[kdist]['ls'], lw=1, - label=k0, + label=k0 if ddist1d[kdist]['ls'] == '-' else None, ) ax.set_xlim(0, 180) ax.set_ylim(0, 1) - ax.legend(loc='lower left', fontsize=fontsize) + leg = ax.legend(loc='lower left', fontsize=fontsize) + ax.add_artist(leg) + + lh = [ + mlines.Line2D([], [], c='k', ls=ddist1d[kdist]['ls'], label=kdist) + for kdist in ddist['dist'].keys() + ] + ax.legend(handles=lh, loc='lower center') # -------------------------- # responsivity theta_ph_vs_B From 5c82b60271f733152a99601527ca85d44e79e2d2 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 26 Feb 2026 20:01:21 +0000 Subject: [PATCH 52/80] [#1166] PBS update --- tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs index 28c2113ae..24983f96f 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.pbs @@ -2,11 +2,11 @@ #PBS -q normal #PBS -l nodes=1 #PBS -l instance_type=c7i.8xlarge -#PBS -l walltime=24:00:00 +#PBS -l walltime=1500:00:00 #PBS -P sparc-design -#PBS -N d2cross_91x200x201_EH -#PBS -o d2cross_91x200x201_EH.out -#PBS -e d2cross_91x200x201_EH.err +#PBS -N d2cross_61x80x81_EH +#PBS -o d2cross_61x80x81_EH.out +#PBS -e d2cross_61x80x81_EH.err #PBS -m abe #PBS -l base_os=ubuntu2404 #PBS -l instance_ami=ami-0d63a2e021bce724d @@ -17,4 +17,4 @@ source ~/.bashrc # Beware: PBS does not expand the tilde ~/ => full explicit path needed export PBS_O_WORKDIR="/data/home/dvezinet/projects/tofu/tofu/physics_tools/electrons/emission" cd ${PBS_O_WORKDIR}/ -python _pbs_d2cross.py -nEph 201 -nEe0 200 -ntheta 91 -v EH +python _pbs_d2cross.py -nEph 81 -nEe0 80 -ntheta 61 -v EH From 36a95532ef1b81f72c8d5009c87b0b35a8f27d1a Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 26 Feb 2026 20:02:06 +0000 Subject: [PATCH 53/80] [#1166] electrons/distribution/_distribution_bump.py --- .../distribution/_distribution_bump.py | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 tofu/physics_tools/electrons/distribution/_distribution_bump.py diff --git a/tofu/physics_tools/electrons/distribution/_distribution_bump.py b/tofu/physics_tools/electrons/distribution/_distribution_bump.py new file mode 100644 index 000000000..97790f37e --- /dev/null +++ b/tofu/physics_tools/electrons/distribution/_distribution_bump.py @@ -0,0 +1,195 @@ + + +import numpy as np +import scipy.constants as scpct +import astropy.units as asunits + + +from .. import _convert + + +# ##################################################### +# ##################################################### +# Dict of functions +# ##################################################### + + +def f2d_momentum_pitch( + pnorm=None, + pitch=None, + # params + step=None, + pnorm0=None, + pnormW=None, + E_hat=None, + Zeff=None, + # unused + **kwdargs, +): + """ Ad-hoc bump on tail + + """ + # B = (E_hat + 1) / (Zeff + 1) + B = 1. + + shape = np.broadcast_shapes( + pnorm.shape, + pitch.shape, + E_hat.shape, + ) + pnorm = np.broadcast_to(pnorm, shape) + iok = np.broadcast_to((pitch > 0.) & (pnorm > 0.), shape) + + pitch_term = (1 - pitch**2) / np.abs(pitch) + + dist = np.zeros(shape, dtype=float) + dreicer_like = np.exp(-0.5*B * pitch_term * pnorm)[iok] / pnorm[iok] + drop = np.exp(-(pnorm - (pnorm0 + 3*pnormW))**2/pnormW**2)[iok] + ione = (pnorm <= (pnorm0 + 3*pnormW))[iok] + drop[ione] = 1. + + dist[iok] = ( + dreicer_like + + (step * np.exp(-pitch_term * (pnorm - pnorm0)**4/pnormW**4))[iok] + ) * drop + + units = asunits.Unit('') + + return dist, units + + +def f2d_momentum_theta( + pnorm=None, + theta=None, + # params + step=None, + pnorm0=None, + pnormW=None, + E_hat=None, + Zeff=None, + # unused + **kwdargs, +): + dist0, units0 = f2d_momentum_pitch( + pnorm=pnorm, + pitch=np.cos(theta), + # params + step=step, + pnorm0=pnorm0, + pnormW=pnormW, + E_hat=E_hat, + Zeff=Zeff, + ) + + dist = np.sin(theta) * dist0 + units = units0 * asunits.Unit('1/rad') + + return dist, units + + +def f2d_E_theta( + E_eV=None, + theta=None, + # params + step=None, + pnorm0=None, + pnormW=None, + E_hat=None, + Zeff=None, + # unused + **kwdargs, +): + + # ----------------------- + # get momentum normalized + + pnorm = _convert.convert_momentum_velocity_energy( + energy_kinetic_eV=E_eV, + )['momentum_normalized']['data'] + + # --------- + # get dist0 + + dist0, units0 = f2d_momentum_theta( + pnorm=pnorm, + theta=theta, + # params + step=step, + pnorm0=pnorm0, + pnormW=pnormW, + E_hat=E_hat, + Zeff=Zeff, + ) + + # ------------- + # jacobian + # dp = gam / sqrt(gam^2 - 1) dgam + # dgam = dE / mc2 + + gamma = _convert.convert_momentum_velocity_energy( + energy_kinetic_eV=E_eV, + )['gamma']['data'] + mc2_eV = scpct.m_e * scpct.c**2 / scpct.e + + jac = gamma / np.sqrt(gamma**2 - 1) / mc2_eV + + dist = dist0 * jac + units = units0 * asunits.Unit('1/eV') + + return dist, units + + +def f3d_E_theta( + E_eV=None, + theta=None, + # params + step=None, + pnorm0=None, + pnormW=None, + E_hat=None, + Zeff=None, + # unused + **kwdargs, +): + + # --------- + # get dist0 + + dist0, units0 = f2d_E_theta( + E_eV=E_eV, + theta=theta, + # params + step=step, + pnorm0=pnorm0, + pnormW=pnormW, + E_hat=E_hat, + Zeff=Zeff, + ) + + # --------- + # adjust + + dist = dist0 / (2.*np.pi) + units = units0 * asunits.Unit('1/rad') + + return dist, units + + +# ##################################################### +# ##################################################### +# Dict of functions +# ##################################################### + + +_DFUNC = { + 'f2d_E_theta_bump': { + 'func': f2d_E_theta, + 'latex': ( + r"$dn_e = \int_{E_{min}}^{E_{max}} \int_0^{\pi}$" + r"$f^{2D}_{E, \theta}(E, \theta) dEd\theta$" + + "\n" + + r"\begin{eqnarray*}" + r"\end{eqnarray*}" + ), + }, +} From fc2ad3efe65b4e4feee8bbd42d1554cd26d8c5c0 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 9 Mar 2026 21:52:02 +0000 Subject: [PATCH 54/80] [#1166] Setting up Ekin_min_eV, TBF --- .../electrons/distribution/_distribution.py | 1 + .../electrons/distribution/_distribution_check.py | 4 ++++ .../electrons/distribution/_distribution_re.py | 11 ++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index 69db0b9ba..c9cf0b60b 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -34,6 +34,7 @@ def main( ne_m3_re=None, Zeff=None, Ekin_max_eV=None, + Ekin_min_eV=None, Efield_par_Vm=None, lnG=None, sigmap=None, diff --git a/tofu/physics_tools/electrons/distribution/_distribution_check.py b/tofu/physics_tools/electrons/distribution/_distribution_check.py index b3d58e3f1..28ccc8c0b 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_check.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_check.py @@ -51,6 +51,10 @@ 'def': 10e6, 'units': 'eV', }, + 'Ekin_min_eV': { + 'def': 1e3, + 'units': 'eV', + }, 'Efield_par_Vm': { 'def': 0.1, 'units': 'V/m', diff --git a/tofu/physics_tools/electrons/distribution/_distribution_re.py b/tofu/physics_tools/electrons/distribution/_distribution_re.py index 2f1456925..aff214fc5 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_re.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_re.py @@ -75,6 +75,11 @@ def main( energy_kinetic_eV=dplasma['Ekin_max_eV']['data'], )['momentum_normalized']['data'] + # get momentum min from total energy eV.s/m - shape + pmin = _convert.convert_momentum_velocity_energy( + energy_kinetic_eV=dplasma['Ekin_min_eV']['data'], + )['momentum_normalized']['data'] + # Critical electric field - shape Ec_Vm = _runaway_growth.get_RE_critical_dreicer_electric_fields( ne_m3=dplasma['ne_m3']['data'], @@ -176,7 +181,11 @@ def main( if dominant['meaning'][vv] != 'maxwell': pnorm = np.broadcast_to(_get_pnorm(dcoords), shape) iokp = np.copy(np.broadcast_to(iok, shape)) - iokp[iokp] = pnorm[iokp] < np.broadcast_to(p_crit, shape)[iokp] + + # pc = min(p_crit, pmin) + pc = np.broadcast_to(p_crit, shape)[iokp] + + iokp[iokp] = pnorm[iokp] < pc re_dist[iokp] = 0. # ---------------------- From be3ab0dc34f530a3360277f846a824ee59efdc0f Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 17 Mar 2026 15:14:54 +0000 Subject: [PATCH 55/80] [#1166] Created publications/ --- .../_figures.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 publications/2026_runawayBremsstrahlungDetection/_figures.py diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py new file mode 100644 index 000000000..a87eb78b9 --- /dev/null +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -0,0 +1,130 @@ + + +import os +import sys + + +import numpy as np +import matplotlib.pyplot as plt +import tofu as tf + + +# ##################################################### +# ##################################################### +# DEFAULTS +# ##################################################### + + +_PATH_HERE = os.path.dirname(__file__) + + +_PFE_DCROSS = os.path.join( + _PATH_HERE, + 'd2cross_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_ntheta61_EH.npz', +) + + +# ##################################################### +# ##################################################### +# Fig 1 - cross-section +# ##################################################### + + +def fig01_cross_section( + pfe=None, + Eph_eV=[5e3, 50e3, 500e3], + Ee0_eV=[1e5, 1e6], +): + + # -------------- + # load + # -------------- + + dout = { + kk: vv.tolist() + for kk, vv in dict(np.load(pfe, allow_pickle=True)).items() + } + + # -------------- + # extract + # -------------- + + indEph = [ + np.argmin(np.abs(dout['E_ph']['data'] - eph)) + for eph in Eph_eV + ] + + indEe0 = [ + np.argmin(np.abs(dout['E_e0']['data'] - e0)) + for e0 in Ee0_eV + ][0] + + Ee0 = dout['E_e0']['data'][0, indEe0, 0] + Eph = dout['E_ph']['data'][0, 0, indEph] + theta_ph = dout['theta_ph']['data'].squeeze()*180/np.pi + + cross = dout['cross']['EH']['data'][:, indEe0, indEph] + + units = dout['cross']['EH']['units'] + + # -------------- + # prepare plot + # -------------- + + fig = plt.figure(figsize=(13, 7)) + gs = None + dax = {} + + ax0 = fig.add_subplot(121) + ax0.set_xlabel( + "Angle of photon emission" + r"$\theta_{ph}$" + " (deg)", + fontweight='bold', + fontsize=14, + ) + ax0.set_ylabel( + f'Cross-section {units}', + fontweight='bold', + fontsize=14, + ) + + ax1 = fig.add_subplot(122, sharex=ax0) + ax1.set_xlabel( + "Angle of photon emission" + r"$\theta_{ph}$" + " (deg)", + fontweight='bold', + fontsize=14, + ) + ax1.set_ylabel( + f'Cross-section {units}', + fontweight='bold', + fontsize=14, + ) + + # -------------- + # plot + # -------------- + + for ii, eph in enumerate(Eph): + + l0, = ax0.plot( + theta_ph, + cross[:, ii], + label=f"Eph = {eph*1e-3:3.0f} keV", + ) + + ax1.plot( + theta_ph, + cross[:, ii] / np.max(cross[:, ii]), + color=l0.get_color(), + label=f"Eph = {eph*1e-3:03.0f} keV", + ) + + # -------------- + # adjust + # -------------- + + ax0.set_xlim(0, 180) + ax0.set_ylim(bottom=0) + ax1.set_ylim(0, 1) + ax0.legend() + + return dax From 582c39701c13f837f86deccad930c5c4f4d41b52 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Mar 2026 19:52:53 +0000 Subject: [PATCH 56/80] [#1166] feedback Z --- tofu/physics_tools/electrons/emission/_xray_thin_target.py | 4 ++++ .../electrons/emission/_xray_thin_target_integrated.py | 3 +++ .../electrons/emission/_xray_thin_target_integrated_plot.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target.py b/tofu/physics_tools/electrons/emission/_xray_thin_target.py index 171f46210..7127186a5 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target.py @@ -170,6 +170,10 @@ def get_xray_thin_d3cross_ei( } for vv in version }, + 'Z': { + 'data': Z, + 'units': None, + }, } # ------------- diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py index b2b6b05ea..047ed221e 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated.py @@ -483,6 +483,9 @@ def _compute( srunits * asunits.Unit(vcross['units']) ) + # Add Z + d2cross['Z'] = d3cross['Z'] + return d2cross diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 6b91de514..41eb383f1 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -143,6 +143,9 @@ def plot_xray_thin_d2cross_ei_anisotropy( verb=verb, ) + version = list(d2cross['cross'].keys())[0] + Z = d2cross.get('Z', {'data': 1})['data'] + # ------------------- # update from d2cross # ------------------- From e8439faa605e6d04b2b1db2918984f1587659200 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Mar 2026 21:46:08 +0000 Subject: [PATCH 57/80] [#1166] better labelling --- .../_xray_thin_target_integrated_plot.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py index 41eb383f1..9669ce59a 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_plot.py @@ -49,6 +49,7 @@ 'marker': {'type': str, 'val': '*'}, 'ms': {'type': (int, float), 'val': 18}, 'ls': {'type': str, 'val': '-'}, + 'label': {'type': (str, type(None)), 'val': None}, } @@ -315,9 +316,12 @@ def plot_xray_thin_d2cross_ei_anisotropy( for ic, (kcase, vcase) in enumerate(dcases.items()): - lab = vcase['lab'] + lab = vcase['label'] for kv, vv in d2cross['cross'].items(): - labi = lab + f" - {kv}" + if len(d2cross['cross']) > 1: + labi = lab + f" - {kv}" + else: + labi = lab yy = vv['data'][:, vcase['ie'], vcase['iph']] if np.any(yy > 0): @@ -568,11 +572,13 @@ def _check_dcases( # update with label ee0 = E_e0_eV[ie] eph = E_ph_eV[iph] - dcases[k0]['lab'] = ( - r"$E_{e0} / E_{ph}$ = " - + f"{ee0*1e-3:3.0f} / {eph*1e-3:3.0f} keV = " - + f"{round(ee0 / eph, ndigits=1)}" - ) + + if dcases[k0].get('label') is None: + dcases[k0]['label'] = ( + r"$E_{e0} / E_{ph}$ = " + + f"{ee0*1e-3:3.0f} / {eph*1e-3:3.0f} keV = " + + f"{round(ee0 / eph, ndigits=1)}" + ) else: dcases = {} From 69078cd65989954faab1e1b316ca16c972ba1085 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 20 Mar 2026 21:46:21 +0000 Subject: [PATCH 58/80] [#1166] figure 1 almost finished --- .../_figures.py | 262 ++++++++++++++---- 1 file changed, 203 insertions(+), 59 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index a87eb78b9..a3865ad9b 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -1,12 +1,15 @@ import os -import sys import numpy as np import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + + import tofu as tf +tfphysemis = tf.physics_tools.electrons.emission # ##################################################### @@ -18,10 +21,16 @@ _PATH_HERE = os.path.dirname(__file__) -_PFE_DCROSS = os.path.join( - _PATH_HERE, - 'd2cross_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_ntheta61_EH.npz', -) +_DPFE_DCROSS = { + 'EH': os.path.join( + _PATH_HERE, + 'd2cross_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_ntheta61_EH.npz', + ), + 'BHE': os.path.join( + _PATH_HERE, + 'd2cross_Ee01eV-100MeV-400log_Eph1eV-100MeV-401log_ntheta181_BHE.npz', + ), +} # ##################################################### @@ -31,100 +40,235 @@ def fig01_cross_section( - pfe=None, - Eph_eV=[5e3, 50e3, 500e3], - Ee0_eV=[1e5, 1e6], + figsize=(15, 9), + version='EH', + Eph_eV=np.r_[1e3, 10e3, 500e3], + Ee0_eV=np.r_[20e3, 1e6], + fontsize=14, + pfe_save=None, ): # -------------- # load # -------------- + pfe = _DPFE_DCROSS[version] + dout = { kk: vv.tolist() for kk, vv in dict(np.load(pfe, allow_pickle=True)).items() } + units = dout['cross'][version]['units'] + Z = dout.get('Z', {'data': 1})['data'] # -------------- - # extract + # prepare axes # -------------- - indEph = [ - np.argmin(np.abs(dout['E_ph']['data'] - eph)) - for eph in Eph_eV - ] - - indEe0 = [ - np.argmin(np.abs(dout['E_e0']['data'] - e0)) - for e0 in Ee0_eV - ][0] - - Ee0 = dout['E_e0']['data'][0, indEe0, 0] - Eph = dout['E_ph']['data'][0, 0, indEph] - theta_ph = dout['theta_ph']['data'].squeeze()*180/np.pi + dmargin = { + 'left': 0.06, 'right': 0.99, + 'bottom': 0.06, 'top': 0.95, + 'wspace': 0.25, 'hspace': 0.10, + } - cross = dout['cross']['EH']['data'][:, indEe0, indEph] + fig = plt.figure(figsize=figsize) - units = dout['cross']['EH']['units'] + gs = gridspec.GridSpec(ncols=4, nrows=2, **dmargin) + dax = {} # -------------- - # prepare plot + # prepare axes # -------------- - fig = plt.figure(figsize=(13, 7)) - gs = None - dax = {} + # -------------- + # ax - isolines - ax0 = fig.add_subplot(121) - ax0.set_xlabel( - "Angle of photon emission" + r"$\theta_{ph}$" + " (deg)", + ax = fig.add_subplot( + gs[:, -2:], + xscale='log', + yscale='log', + aspect='equal', + ) + ax.set_xlabel( + r"$E_{e,0}$ (keV)", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + r"$E_{ph}$ (keV)", + size=fontsize, fontweight='bold', - fontsize=14, ) - ax0.set_ylabel( - f'Cross-section {units}', + ax.set_title( + r"$d^2\sigma(E_{e0}, E_{ph}, \theta_{ph}, Z)$" + + f"\n Z = {Z} - version = {version}", + size=fontsize, fontweight='bold', - fontsize=14, ) - ax1 = fig.add_subplot(122, sharex=ax0) - ax1.set_xlabel( - "Angle of photon emission" + r"$\theta_{ph}$" + " (deg)", + # store + dax['map'] = {'handle': ax, 'type': 'isolines'} + + # -------------- + # ax - theta_norm + + # theta_norm0 + ax = fig.add_subplot( + gs[0, 0], + xscale='linear', + ) + ax.set_ylabel( + "normalized cross-section (adim.)", + size=fontsize, fontweight='bold', - fontsize=14, ) - ax1.set_ylabel( - f'Cross-section {units}', + ax.set_title( + r"$E_{e0}$" + f" = {Ee0_eV[0]*1e-3:2.0f} keV", + size=fontsize, fontweight='bold', - fontsize=14, ) + # store + dax['theta_norm0'] = {'handle': ax, 'type': 'isolines'} + + # theta_norm1 + ax = fig.add_subplot( + gs[0, 1], + sharex=dax['theta_norm0']['handle'], + sharey=dax['theta_norm0']['handle'], + ) + ax.set_title( + r"$E_{e0}$" + f" = {Ee0_eV[1]*1e-6:2.0f} MeV", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta_norm1'] = {'handle': ax, 'type': 'isolines'} + # -------------- - # plot - # -------------- + # ax - theta_abs - for ii, eph in enumerate(Eph): + # theta_abs0 + ax = fig.add_subplot( + gs[1, 0], + sharex=dax['theta_norm0']['handle'], + ) + ax.set_xlabel( + r"$\theta_{ph}$ (deg)", + size=fontsize, + fontweight='bold', + ) + ax.set_ylabel( + r"$d\sigma$" + f"({units})", + size=fontsize, + fontweight='bold', + ) + + # store + dax['theta_abs0'] = {'handle': ax, 'type': 'isolines'} + + # theta_abs1 + ax = fig.add_subplot( + gs[1, 1], + sharex=dax['theta_norm0']['handle'], + sharey=dax['theta_abs0']['handle'], + ) + ax.set_xlabel( + r"$\theta_{ph}$ (deg)", + size=fontsize, + fontweight='bold', + ) - l0, = ax0.plot( - theta_ph, - cross[:, ii], - label=f"Eph = {eph*1e-3:3.0f} keV", + # store + dax['theta_abs1'] = {'handle': ax, 'type': 'isolines'} + + # ------------------ + # call built-in + # ------------------ + + # cases 100 keV + lc = ['r', 'g', 'b'] + for i0, e0 in enumerate(Ee0_eV): + iphok = Eph_eV < e0 + dcases = { + i1: { + 'E_e0_eV': e0, + 'E_ph_eV': eph, + 'color': lc[i1], + 'label': f"Eph = {eph*1e-3:3.0f} keV", + } + for i1, eph in enumerate(Eph_eV[iphok]) + } + _dax, _d2cross = tfphysemis.plot_xray_thin_d2cross_ei_anisotropy( + d2cross=pfe, + dcases=dcases, + dax={ + 'map': dax['map']['handle'], + 'theta_norm': dax[f'theta_norm{i0}']['handle'], + 'theta_abs': dax[f'theta_abs{i0}']['handle'], + }, ) - ax1.plot( - theta_ph, - cross[:, ii] / np.max(cross[:, ii]), - color=l0.get_color(), - label=f"Eph = {eph*1e-3:03.0f} keV", + # remove contour plots + if i0 == 0: + for cc in dax['map']['handle'].get_children(): + if cc.__class__.__name__ == 'QuadContourSet': + cc.remove() + + # remove legend + dax['theta_norm0']['handle'].get_legend().remove() + + # --------------------- + # Adjust map x/y scales + # --------------------- + + dax['map']['handle'].set_xlim(0.1, 10e3) + dax['map']['handle'].set_ylim(0.1, 10e3) + dax['theta_abs1']['handle'].set_ylabel('') + dax['theta_norm1']['handle'].legend(loc='lower right') + + # -------------- + # add a, b, c, d, e + # -------------- + + dabc = { + 'theta_norm0': '(a)', + 'theta_norm1': '(c)', + 'theta_abs0': '(b)', + 'theta_abs1': '(d)', + } + for kax, abc in dabc.items(): + dax[kax]['handle'].text( + 0.95, 0.95, + abc, + fontsize=fontsize, + fontweight='bold', + horizontalalignment='right', + verticalalignment='top', + transform=ax.transAxes, ) + dax['map']['handle'].text( + 0., 1.05, + "(e)", + fontsize=fontsize, + fontweight='bold', + horizontalalignment='left', + verticalalignment='bottom', + transform=ax.transAxes, + ) + # -------------- - # adjust + # save # -------------- - ax0.set_xlim(0, 180) - ax0.set_ylim(bottom=0) - ax1.set_ylim(0, 1) - ax0.legend() + if pfe_save is not False: + if pfe_save is None: + name = 'fig01_crosssection.png' + pfe_save = os.path.join(_PATH_HERE, name) + fig.savefig(pfe_save, format='png', dpi=300) + msg = f"Saved figure in:\n\t{pfe_save}\n" + print(msg) return dax From 0e603ff46564e6321acb79f2fbc252860f6acfdf Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 23 Mar 2026 17:58:43 +0000 Subject: [PATCH 59/80] [#1166] figure 01 ok --- .../2026_runawayBremsstrahlungDetection/_figures.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index a3865ad9b..d06b66c29 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -40,7 +40,7 @@ def fig01_cross_section( - figsize=(15, 9), + figsize=(15, 7), version='EH', Eph_eV=np.r_[1e3, 10e3, 500e3], Ee0_eV=np.r_[20e3, 1e6], @@ -67,7 +67,7 @@ def fig01_cross_section( dmargin = { 'left': 0.06, 'right': 0.99, - 'bottom': 0.06, 'top': 0.95, + 'bottom': 0.08, 'top': 0.93, 'wspace': 0.25, 'hspace': 0.10, } @@ -239,6 +239,7 @@ def fig01_cross_section( 'theta_abs1': '(d)', } for kax, abc in dabc.items(): + dax[kax]['handle'].grid(visible=True, which='major', axis='both') dax[kax]['handle'].text( 0.95, 0.95, abc, @@ -246,17 +247,17 @@ def fig01_cross_section( fontweight='bold', horizontalalignment='right', verticalalignment='top', - transform=ax.transAxes, + transform=dax[kax]['handle'].transAxes, ) dax['map']['handle'].text( - 0., 1.05, + 0., 1.02, "(e)", fontsize=fontsize, fontweight='bold', horizontalalignment='left', verticalalignment='bottom', - transform=ax.transAxes, + transform=dax['map']['handle'].transAxes, ) # -------------- From 3477259b6c56b2916f8b7943593f1c561184df4a Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 24 Mar 2026 13:30:46 +0000 Subject: [PATCH 60/80] [#1166] pbs job d2cross from 100 eV to 10 MeV --- tofu/physics_tools/electrons/emission/_pbs_d2cross.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py index aa7838af3..116c9ca3c 100644 --- a/tofu/physics_tools/electrons/emission/_pbs_d2cross.py +++ b/tofu/physics_tools/electrons/emission/_pbs_d2cross.py @@ -60,8 +60,8 @@ def main( if version is None: version = ddef['version'] - E_ph_eV = np.logspace(0, 8, nEph) - E_e0_eV = np.logspace(0, 8, nEe0) + E_ph_eV = np.logspace(2, 7, nEph) + E_e0_eV = np.logspace(2, 7, nEe0) theta_ph = np.linspace(0, np.pi, ntheta) # ------------------- From e049ec2a1378102ebb40c7e54344a0d95c2623df Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 3 Apr 2026 13:21:17 +0000 Subject: [PATCH 61/80] [#1166] Added safety check to v0_par_ms and vt_ms in Maxwellian distribution --- .../distribution/_distribution_maxwell.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py b/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py index 591583bb8..34ca4efba 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_maxwell.py @@ -38,6 +38,20 @@ def main( ) vt_ms = np.sqrt(2. * kbT_J / me) + # check v0_par_ms + if np.any(v0_par_ms > scpct.c): + msg = ( + "Shifted Maxwellian has invalid v0_par_ms > c!\n" + ) + raise Exception(msg) + + # check vt_ms + if np.any(vt_ms > scpct.c): + msg = ( + "Shifted Maxwellian has invalid vt_ms > c!\n" + ) + raise Exception(msg) + # -------------- # format output # -------------- From 997f9eb5553f4a873ceb5950ca7956dc0f2e9672 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 8 Apr 2026 13:17:14 +0000 Subject: [PATCH 62/80] [1166] Updated _distribution_plot.py --- .../distribution/_distribution_plot.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_plot.py b/tofu/physics_tools/electrons/distribution/_distribution_plot.py index 29dd4f020..b0acf5408 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_plot.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_plot.py @@ -53,6 +53,10 @@ 'def': 10e6, 'units': 'eV', }, + 'Ekin_min_eV': { + 'def': 1e3, + 'units': 'eV', + }, 'Efield_par_Vm': { 'def': 1., 'units': 'V/m', @@ -65,6 +69,15 @@ 'def': 1., 'units': None, }, + # bump + 'step': { + 'def': 50, + 'units': '', + }, + 'pnormW': { + 'def': 0.4, + 'units': '', + }, } @@ -93,15 +106,13 @@ def main( # RE-specific Zeff=None, Ekin_max_eV=None, + Ekin_min_eV=None, Efield_par_Vm=None, lnG=None, sigmap=None, # bump step=None, - p_perp_norm0=None, - p_perp_normW=None, - p_par_norm0=None, - p_par_normW=None, + pnormW=None, # dominant dominant=None, # ----------- From 258a77016c69751b9e908973a30ae70915ffb97c Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 8 Apr 2026 13:17:48 +0000 Subject: [PATCH 63/80] [#1166] updated pfe_cross in figure --- .../2026_runawayBremsstrahlungDetection/_figures.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index d06b66c29..48a4115f9 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -22,10 +22,14 @@ _DPFE_DCROSS = { - 'EH': os.path.join( + 'EH0': os.path.join( _PATH_HERE, 'd2cross_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_ntheta61_EH.npz', ), + 'EH1': os.path.join( + _PATH_HERE, + 'd2cross_Ee0100eV-10MeV-80log_Eph100eV-10MeV-81log_ntheta61_EH.npz', + ), 'BHE': os.path.join( _PATH_HERE, 'd2cross_Ee01eV-100MeV-400log_Eph1eV-100MeV-401log_ntheta181_BHE.npz', @@ -41,6 +45,7 @@ def fig01_cross_section( figsize=(15, 7), + pfe_cross='EH0', version='EH', Eph_eV=np.r_[1e3, 10e3, 500e3], Ee0_eV=np.r_[20e3, 1e6], @@ -52,7 +57,7 @@ def fig01_cross_section( # load # -------------- - pfe = _DPFE_DCROSS[version] + pfe = _DPFE_DCROSS[pfe_cross] dout = { kk: vv.tolist() From 204a9ed17cf34a1e3a40f2cc3fd61bb88fe502ae Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 8 Apr 2026 13:18:09 +0000 Subject: [PATCH 64/80] [#1166] Testing RE bump distributions --- .../electrons/distribution/_distribution_bump.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_bump.py b/tofu/physics_tools/electrons/distribution/_distribution_bump.py index 97790f37e..b91bc92df 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_bump.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_bump.py @@ -30,7 +30,7 @@ def f2d_momentum_pitch( """ # B = (E_hat + 1) / (Zeff + 1) - B = 1. + # B = 1. shape = np.broadcast_shapes( pnorm.shape, @@ -43,7 +43,7 @@ def f2d_momentum_pitch( pitch_term = (1 - pitch**2) / np.abs(pitch) dist = np.zeros(shape, dtype=float) - dreicer_like = np.exp(-0.5*B * pitch_term * pnorm)[iok] / pnorm[iok] + dreicer_like = np.exp(- pitch_term * pnorm)[iok] / pnorm[iok] drop = np.exp(-(pnorm - (pnorm0 + 3*pnormW))**2/pnormW**2)[iok] ione = (pnorm <= (pnorm0 + 3*pnormW))[iok] drop[ione] = 1. @@ -53,6 +53,11 @@ def f2d_momentum_pitch( + (step * np.exp(-pitch_term * (pnorm - pnorm0)**4/pnormW**4))[iok] ) * drop + dist[iok] = ( + dreicer_like + * (step * np.exp(-pitch_term * (pnorm - pnorm0)**4/pnormW**4))[iok] + ) + units = asunits.Unit('') return dist, units From 98655682a254169a49291e5dd9e6199b45fd6fc1 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 8 May 2026 20:14:34 +0000 Subject: [PATCH 65/80] [#1166] Started fig02, tbf --- .../_figures.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 48a4115f9..4e9bf9454 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -278,3 +278,95 @@ def fig01_cross_section( print(msg) return dax + + +# ##################################################### +# ##################################################### +# Fig 1 - cross-section +# ##################################################### + + +_DR = { + 'R0': 1.8, + 'rplasma': 0.6, + 'RFW': [1.2, 2.45], + 'Rcryo': 4.6, + 'PP_length': 2.2, + 'PP_width': None, + 'PP_phi': np.r_[-30, 30] * np.pi/180, +} + + +def fig02_tokamak( + # tokamak + R0=None, + rplasma=None, + RFW=None, + Rcryo=None, + PP_length=None, + PP_width=None, + PP_phi=None, + # plot + figsize=(15, 7), + fontsize=14, + pfe_save=None, +): + + # -------------- + # Load SPARC + # -------------- + + config = tf.load_config('SPARC') + + # -------------- + # prepare axes + # -------------- + + dmargin = { + 'left': 0.06, 'right': 0.99, + 'bottom': 0.08, 'top': 0.93, + 'wspace': 0.25, 'hspace': 0.10, + } + + fig = plt.figure(figsize=figsize) + + gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) + dax = {} + + # -------------- + # prepare axes + # -------------- + + ax = fig.add_subplot(gs[0, 0], aspect='equal') + ax.set_xlabel('X (m)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel('Y (m)', fontsize=fontsize, fontweight='bold') + ax.set_title('SPARC-like tokamak', fontsize=fontsize, fontweight='bold') + + dax['hor'] = ax + + dax = ds._generic_check._check_dax(dax) + + # -------------- + # plot tokamak + # -------------- + + config.plot(lax=dax['hor']['handle'], proj='hor') + + # -------------- + # add port plug + # -------------- + + # -------------- + # save + # -------------- + + if pfe_save is not False: + if pfe_save is None: + name = 'fig02_tokamak.png' + pfe_save = os.path.join(_PATH_HERE, name) + fig.savefig(pfe_save, format='png', dpi=300) + msg = f"Saved figure in:\n\t{pfe_save}\n" + print(msg) + + return dax + From cb6f7e5bd23232bc5eafec5b55c1746a75747913 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 11 May 2026 09:51:30 +0000 Subject: [PATCH 66/80] [#1166] Added fig02 with R --- .../_figures.py | 137 +++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 4e9bf9454..bbc020ae9 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -6,6 +6,7 @@ import numpy as np import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec +import datastock as ds import tofu as tf @@ -316,7 +317,11 @@ def fig02_tokamak( # Load SPARC # -------------- - config = tf.load_config('SPARC') + config, dinput = _fig02_check(**locals()) + + phi = np.pi * np.linspace(-1, 1, 181) + cos = np.cos(phi) + sin = np.sin(phi) # -------------- # prepare axes @@ -347,10 +352,22 @@ def fig02_tokamak( dax = ds._generic_check._check_dax(dax) # -------------- - # plot tokamak + # plot R # -------------- - config.plot(lax=dax['hor']['handle'], proj='hor') + lk = [kk for kk in dinput.keys() if kk[0] == 'R'] + for k0 in lk: + v0 = np.r_[dinput[k0]['data']] + for v1 in v0: + ax.plot( + v0*cos, + v0*sin, + **dinput[k0]['prop'], + ) + + # -------------- + # plot port plug + # -------------- # -------------- # add port plug @@ -370,3 +387,117 @@ def fig02_tokamak( return dax + +def _fig02_check( + config=None, + # tokamak + R0=None, + rplasma=None, + RFW=None, + Rcryo=None, + PP_length=None, + PP_width=None, + PP_phi=None, + # unused + **kwdargs, +): + + # ------------------ + # config + # ------------------ + + if config is None: + config = 'SPARC' + + if isinstance(config, str): + config = tf.load_config(config) + + # ------------------ + # Geometry - R + # ------------------ + + lk = ['R0', 'rplasma', 'RFW', 'Rcryo', 'PP_length', 'PP_width', 'PP_phi'] + dinput = { + kk: {'data': None, 'color': 'k', 'ls': '-', 'lw': 1, 'label': None} + for kk in lk + } + + # R0 + dinput['R0']['data'] = float(ds._generic_check._check_var( + R0, 'R0', + types=(float, int), + sign='>0', + default=_DR['R0'], + )) + + # rplasma + dinput['rplasma']['data'] = float(ds._generic_check._check_var( + rplasma, 'rplasma', + types=(float, int), + sign='>0', + default=_DR['rplasma'], + )) + assert dinput['rplasma']['data'] < dinput['R0']['data'] + + dinput['Rplasma'] = { + 'data': dinput['R0']['data'] + dinput['rplasma']['data']*np.r_[-1, 1], + 'color': 'k', + 'lw': 1, + 'ls': '-', + } + + # RFW + dinput['RFW']['data'] = ds._generic_check._check_1darray( + RFW, 'RFW', + dtypes=float, + sign='>0', + unique=True, + default=_DR['RFW'], + size=2, + ) + Rlim = dinput['R0']['data'] - dinput['rplasma']['data'] + assert dinput['RFW'][0]['data'] < Rlim + Rlim = dinput['R0']['data'] + dinput['rplasma']['data'] + assert dinput['RFW'][1]['data'] > Rlim + + # rplasma + dinput['Rcryo']['data'] = float(ds._generic_check._check_var( + Rcryo, 'Rcryo', + types=(float, int), + sign='>0', + default=_DR['Rcryo'], + )) + assert dinput['Rcryo']['data'] > dinput['RFW']['data'][1] + + # ------------------ + # Geometry - Port plug + # ------------------ + + # PP_length + dinput['PP_length']['data'] = float(ds._generic_check._check_var( + PP_length, 'PP_length', + types=(float, int), + sign='>0', + default=_DR['PP_length'], + )) + Rin = dinput['R0']['data'] + dinput['rplasma']['data'] + Rlim = dinput['Rcryo']['data'] - Rin + assert dinput['PP_length']['data']/2 < Rlim + + # PP_width + dinput['PP_width']['data'] = float(ds._generic_check._check_var( + PP_width, 'PP_width', + types=(float, int), + sign='>0', + default=_DR['PP_width'], + )) + + # PP_phi + PP_phi = float(ds._generic_check._check_var( + PP_phi, 'PP_phi', + types=(float, int), + default=_DR['PP_phi'], + )) + dinput['PP_phi']['data'] = np.arctan2(np.sin(PP_phi), np.cos(PP_phi)) + + return config, dinput From 7641b2be553fe9da64d1ab5243a607e0433595e6 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 13 May 2026 09:39:14 +0000 Subject: [PATCH 67/80] [#1166] fig03_tokamak - first version ok --- .../_figures.py | 704 ++++++++++++++++-- 1 file changed, 639 insertions(+), 65 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index bbc020ae9..4368e85ad 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -6,6 +6,8 @@ import numpy as np import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec +import matplotlib.patches as mpatches +import matplotlib.path as mpath import datastock as ds @@ -289,27 +291,73 @@ def fig01_cross_section( _DR = { 'R0': 1.8, - 'rplasma': 0.6, - 'RFW': [1.2, 2.45], + 'rplasma': 0.60, + 'RVes': [1.2, 2.66], 'Rcryo': 4.6, - 'PP_length': 2.2, - 'PP_width': None, - 'PP_phi': np.r_[-30, 30] * np.pi/180, + 'PP_R': np.r_[2.50, 4.7], # 4.2 + 'PP_width': 0.47, + 'PP_phi': np.r_[-180, 0] * np.pi/180, } -def fig02_tokamak( +_DSENSORS = { + 'in': { + 'pp': 0, + 'R': 2.55, + 'cw': False, + 'rplasma_ratio': 0.7, + 'color': 'b', + 'marker': '.', + 'ms': 2, + 'alpha': 0.6, + }, + 'ex': { + 'pp': 1, + 'R': 6, + 'cw': False, + 'color': 'g', + 'width': 0.10, + 'dist': 4, + 'marker': '.', + 'ms': 2, + 'alpha': 0.6, + 'wall': True, + 'beamdump': True, + }, +} + + +def fig03_tokamak( # tokamak R0=None, rplasma=None, - RFW=None, + RVes=None, Rcryo=None, - PP_length=None, + # port plug + PP_R=None, PP_width=None, PP_phi=None, + # sensors + in_pp=None, + in_R=None, + in_cw=None, + in_rplasma_ratio=None, + in_color=None, + in_marker=None, + in_ms=None, + # ex + res=None, + ex_pp=None, + ex_R=None, + ex_cw=None, + ex_width=None, + ex_dist=None, + ex_color=None, + ex_marker=None, + ex_ms=None, # plot - figsize=(15, 7), - fontsize=14, + figsize=(5, 6), + fontsize=12, pfe_save=None, ): @@ -328,50 +376,292 @@ def fig02_tokamak( # -------------- dmargin = { - 'left': 0.06, 'right': 0.99, - 'bottom': 0.08, 'top': 0.93, - 'wspace': 0.25, 'hspace': 0.10, + 'left': 0.11, 'right': 0.97, + 'bottom': 0.08, 'top': 0.95, + 'wspace': 0.25, 'hspace': 0.35, } fig = plt.figure(figsize=figsize) - gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) + gs = gridspec.GridSpec(ncols=1, nrows=2, **dmargin) dax = {} # -------------- - # prepare axes + # axes - hor # -------------- ax = fig.add_subplot(gs[0, 0], aspect='equal') ax.set_xlabel('X (m)', fontsize=fontsize, fontweight='bold') ax.set_ylabel('Y (m)', fontsize=fontsize, fontweight='bold') ax.set_title('SPARC-like tokamak', fontsize=fontsize, fontweight='bold') + ax.text( + 0, + 1, + '(a)', + horizontalalignment='left', + verticalalignment='bottom', + fontsize=fontsize, + fontweight='bold', + transform=ax.transAxes, + ) dax['hor'] = ax + # -------------- + # axes - ang vs rplasma + # -------------- + + ax = fig.add_subplot(gs[1, 0], aspect='auto') + ax.set_xlabel('r / a', fontsize=fontsize, fontweight='bold') + ax.set_ylabel( + r'$\theta_{ph,B}$ (deg)', + fontsize=fontsize, + fontweight='bold', + ) + ax.set_title('FOV sampling', fontsize=fontsize, fontweight='bold') + ax.text( + 0, + 1, + '(b)', + horizontalalignment='left', + verticalalignment='bottom', + fontsize=fontsize, + fontweight='bold', + transform=ax.transAxes, + ) + + dax['theta_vs_B'] = ax + dax = ds._generic_check._check_dax(dax) # -------------- - # plot R + # plot hor # -------------- - lk = [kk for kk in dinput.keys() if kk[0] == 'R'] - for k0 in lk: - v0 = np.r_[dinput[k0]['data']] - for v1 in v0: + kax = 'hor' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # -------------- + # plot R + + lk = [kk for kk in dinput.keys() if kk[0] == 'R'] + for k0 in lk: + + if 'plasma' in k0: + inner = dinput[k0]['data'][0] * np.array([cos, sin]).T + outer = dinput[k0]['data'][1] * np.array([cos, sin]).T + vertices = np.concatenate((inner, outer[::-1]), axis=0) + + codes = np.ones( + len(inner), + dtype=mpath.Path.code_type, + ) * mpath.Path.LINETO + codes[0] = mpath.Path.MOVETO + all_codes = np.concatenate((codes, codes)) + + path = mpath.Path(vertices, all_codes) + patch = mpatches.PathPatch( + path, + facecolor='r', + alpha=0.1, + edgecolor='r', + ) + ax.add_patch(patch) + + else: + for v1 in dinput[k0]['data']: + ax.plot( + v1*cos, + v1*sin, + **dinput[k0]['prop'], + ) + + # -------------- + # add arrows + + R = dinput['R0']['data'][0] + 0.5 * dinput['rplasma']['data'][0] + phi = np.r_[100, 160] * np.pi / 180 + # dist = R * np.hypot( + # np.cos(phi[0]) - np.cos(phi[1]), + # np.sin(phi[0]) - np.sin(phi[1]), + # ) + # rad = (R * (1 - np.cos(np.abs(np.diff(phi)/2))) / dist)[0] + rad = -0.3 + ax.annotate( + "RE", + xy=(R*np.cos(phi[0]), R*np.sin(phi[0])), + xycoords='data', + xytext=(R*np.cos(phi[1]), R*np.sin(phi[1])), + textcoords='data', + color='r', + fontweight='bold', + fontsize=fontsize, + arrowprops=dict( + arrowstyle="->", + lw=1.5, + color='r', + shrinkA=5, shrinkB=5, + patchA=None, patchB=None, + connectionstyle=f'arc3,rad={rad}', + ), + ) + + # -------------- + # plot port plug + + for ii, phi in enumerate(dinput['PP_phi']['data']): + + # edges + width = dinput['PP_width']['data'][0] + ppR0 = dinput['PP_R']['data'][0] + ppR1 = dinput['PP_R']['data'][1] + length = ppR1 - ppR0 + cent = 0.5 * (ppR0+ppR1) * np.r_[np.cos(phi), np.sin(phi)] + xy = ( + cent[0] - 0.5 * length, + cent[1] - 0.5 * width, + ) + + # patch + patch = mpatches.Rectangle( + xy, + length, + width, + angle=phi*180/np.pi, + rotation_point='center', + facecolor='w', + alpha=1., + edgecolor='None', + zorder=10, + ) + ax.add_patch(patch) + + # central line + ppR0 = dinput['PP_R']['data'][0] + ppR1 = dinput['PP_R']['data'][1] + + centx = np.r_[ppR0, ppR1] * np.cos(phi) + centy = np.r_[ppR0, ppR1] * np.sin(phi) + ax.plot( - v0*cos, - v0*sin, - **dinput[k0]['prop'], + centx, + centy, + c='k', + lw=1, + ls='--', + alpha=0.3, + zorder=15, ) - # -------------- - # plot port plug - # -------------- + # edges + ephi = np.r_[-np.sin(phi), np.cos(phi)] + edgex = ( + centx[None, :] + + 0.5 * width * ephi[0] * np.r_[1, np.nan, -1][:, None] + ).ravel() + edgey = ( + centy[None, :] + + 0.5 * width * ephi[1] * np.r_[1, np.nan, -1][:, None] + ).ravel() - # -------------- - # add port plug - # -------------- + ax.plot( + edgex, + edgey, + c='k', + lw=1, + ls='-', + zorder=20, + ) + + # -------------- + # add sensors + + dsensors = _sensors(**locals()) + + for k0, v0 in dsensors.items(): + + # FOV + patch = mpatches.PathPatch( + v0['path'], + facecolor=v0['color'], + alpha=v0['alpha'], + zorder=25, + edgecolor=v0['color'], + ) + ax.add_patch(patch) + + # sensor + ax.plot( + [v0['cent'][0]], + [v0['cent'][1]], + c=v0['color'], + ls='None', + lw=2, + marker=v0['marker'], + zorder=30, + label=v0.get('label', k0), + ) + + # FOV sampling + ax.plot( + v0['ptsx'], + v0['ptsy'], + c=v0['color'], + marker=v0.get('marker', '.'), + ls='None', + zorder=30, + ms=v0.get('ms', 4), + ) + + # ---------------- + # plot theta_vs_B + # ---------------- + + kax = 'theta_vs_B' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for k0, v0 in dsensors.items(): + + ax.plot( + v0['rplasma_norm'], + v0['theta_vs_B'] * 180 / np.pi, + marker=v0.get('marker', '.'), + ms=v0.get('ms', 6), + color=v0['color'], + ls=v0.get('ls', 'None'), + label=v0.get('label', k0), + ) + + ax.axhline(90, c='k', ls='--', lw=1) + ax.set_xlim(-1, 1) + ax.set_ylim(0, 180) + ax.grid(True) + + # comments + ax.text( + -0.97, + 55, + 'forward', + horizontalalignment='left', + verticalalignment='top', + rotation=90, + fontsize=fontsize, + fontweight='bold', + transform=ax.transData, + ) + ax.text( + -0.97, + 110, + 'backward', + horizontalalignment='left', + verticalalignment='bottom', + rotation=90, + fontsize=fontsize, + fontweight='bold', + transform=ax.transData, + ) # -------------- # save @@ -379,7 +669,7 @@ def fig02_tokamak( if pfe_save is not False: if pfe_save is None: - name = 'fig02_tokamak.png' + name = 'fig03_tokamak.png' pfe_save = os.path.join(_PATH_HERE, name) fig.savefig(pfe_save, format='png', dpi=300) msg = f"Saved figure in:\n\t{pfe_save}\n" @@ -393,9 +683,9 @@ def _fig02_check( # tokamak R0=None, rplasma=None, - RFW=None, + RVes=None, Rcryo=None, - PP_length=None, + PP_R=None, PP_width=None, PP_phi=None, # unused @@ -416,88 +706,372 @@ def _fig02_check( # Geometry - R # ------------------ - lk = ['R0', 'rplasma', 'RFW', 'Rcryo', 'PP_length', 'PP_width', 'PP_phi'] + lk = ['R0', 'rplasma', 'RVes', 'Rcryo', 'PP_R', 'PP_width', 'PP_phi'] dinput = { - kk: {'data': None, 'color': 'k', 'ls': '-', 'lw': 1, 'label': None} + kk: { + 'data': None, + 'prop': {'color': 'k', 'ls': '-', 'lw': 1, 'label': None} + } for kk in lk } # R0 - dinput['R0']['data'] = float(ds._generic_check._check_var( + dinput['R0']['data'] = np.r_[float(ds._generic_check._check_var( R0, 'R0', types=(float, int), sign='>0', default=_DR['R0'], - )) + ))] + dinput['R0']['prop']['ls'] = '--' # rplasma - dinput['rplasma']['data'] = float(ds._generic_check._check_var( + dinput['rplasma']['data'] = np.r_[float(ds._generic_check._check_var( rplasma, 'rplasma', types=(float, int), sign='>0', default=_DR['rplasma'], - )) + ))] assert dinput['rplasma']['data'] < dinput['R0']['data'] dinput['Rplasma'] = { 'data': dinput['R0']['data'] + dinput['rplasma']['data']*np.r_[-1, 1], - 'color': 'k', - 'lw': 1, - 'ls': '-', + 'prop': { + 'color': 'r', + 'lw': 1, + 'ls': '-', + 'label': 'plasma', + } } - # RFW - dinput['RFW']['data'] = ds._generic_check._check_1darray( - RFW, 'RFW', - dtypes=float, + # RVes + if RVes is None: + RVes = _DR['RVes'] + dinput['RVes']['data'] = ds._generic_check._check_flat1darray( + RVes, 'RVes', + dtype=float, sign='>0', unique=True, - default=_DR['RFW'], size=2, ) Rlim = dinput['R0']['data'] - dinput['rplasma']['data'] - assert dinput['RFW'][0]['data'] < Rlim + assert dinput['RVes']['data'][0] < Rlim Rlim = dinput['R0']['data'] + dinput['rplasma']['data'] - assert dinput['RFW'][1]['data'] > Rlim + assert dinput['RVes']['data'][1] > Rlim + dinput['RVes']['prop']['lw'] = 2 # rplasma - dinput['Rcryo']['data'] = float(ds._generic_check._check_var( + dinput['Rcryo']['data'] = np.r_[float(ds._generic_check._check_var( Rcryo, 'Rcryo', types=(float, int), sign='>0', default=_DR['Rcryo'], - )) - assert dinput['Rcryo']['data'] > dinput['RFW']['data'][1] + ))] + assert dinput['Rcryo']['data'] > dinput['RVes']['data'][1] + dinput['Rcryo']['prop']['lw'] = 2 # ------------------ # Geometry - Port plug # ------------------ - # PP_length - dinput['PP_length']['data'] = float(ds._generic_check._check_var( - PP_length, 'PP_length', - types=(float, int), + # PP_R + if PP_R is None: + PP_R = _DR['PP_R'] + dinput['PP_R']['data'] = ds._generic_check._check_flat1darray( + PP_R, 'PP_R', + dtype=float, sign='>0', - default=_DR['PP_length'], - )) + unique=True, + size=2, + ) Rin = dinput['R0']['data'] + dinput['rplasma']['data'] - Rlim = dinput['Rcryo']['data'] - Rin - assert dinput['PP_length']['data']/2 < Rlim + assert dinput['PP_R']['data'][0] > Rin + assert dinput['PP_R']['data'][1] > dinput['Rcryo']['data'] # PP_width - dinput['PP_width']['data'] = float(ds._generic_check._check_var( + dinput['PP_width']['data'] = np.r_[float(ds._generic_check._check_var( PP_width, 'PP_width', types=(float, int), sign='>0', default=_DR['PP_width'], - )) + ))] # PP_phi - PP_phi = float(ds._generic_check._check_var( + if PP_phi is None: + PP_phi = _DR['PP_phi'] + PP_phi = ds._generic_check._check_flat1darray( PP_phi, 'PP_phi', - types=(float, int), - default=_DR['PP_phi'], - )) + dtype=float, + unique=True, + size=2, + ) dinput['PP_phi']['data'] = np.arctan2(np.sin(PP_phi), np.cos(PP_phi)) return config, dinput + + +def _sensors( + res=None, + # sensors - in + in_pp=None, + in_R=None, + in_cw=None, + in_rplasma_ratio=None, + in_color=None, + in_marker=None, + in_ms=None, + # sensors - ex + ex_pp=None, + ex_R=None, + ex_cw=None, + ex_width=None, + ex_dist=None, + ex_color=None, + ex_marker=None, + ex_ms=None, + # dinput + dinput=None, + # unused + **kwdargs, +): + + # -------------- + # inputs + # -------------- + + res = ds._generic_check._check_var( + res, 'res', + types=float, + sign='>0', + default=0.01, + ) + + # -------------- + # initialize + # -------------- + + dsensors = { + 'in': { + kk.replace('in_', ''): vv for kk, vv in locals().items() + if kk.startswith('in_') + }, + 'ex': { + kk.replace('ex_', ''): vv for kk, vv in locals().items() + if kk.startswith('ex_') + }, + } + + # -------------- + # check + # -------------- + + for k0, v0 in dsensors.items(): + for k1, v1 in v0.items(): + if v1 is None: + dsensors[k0][k1] = _DSENSORS[k0][k1] + for k1, v1 in _DSENSORS[k0].items(): + if dsensors[k0].get(k1) is None: + dsensors[k0][k1] = v1 + + # -------------- + # Derive + # -------------- + + for k0, v0 in dsensors.items(): + + phi = dinput['PP_phi']['data'][v0['pp']] + eR = np.r_[np.cos(phi), np.sin(phi)] + ephi = np.r_[-np.sin(phi), np.cos(phi)] + sign = v0["cw"] * 2 - 1 + width = dinput['PP_width']['data'] + + # ---------- + # cent + + if k0 == 'in': + cent = v0['R'] * eR + sign * 0.5 * width * ephi + else: + length = dinput['PP_R']['data'][1] - dinput['PP_R']['data'][0] + dphi = np.arctan2(width - v0['width'], length) + eRs = eR * np.cos(dphi) + sign * ephi * np.sin(dphi) + ppc = np.mean(dinput['PP_R']['data']) * eR + cent = ppc + v0["dist"] * eRs + + # store + dsensors[k0]["dphi"] = dphi + dsensors[k0]["ppc"] = ppc + dsensors[k0]["eRs"] = eRs + + dsensors[k0]['cent'] = cent + + # ---------- + # FOV + + if k0 == 'in': + + R0 = dinput['R0']['data'][0] + rplasma = dinput['rplasma']['data'][0] + + # out + R = R0 + rplasma * v0["rplasma_ratio"] + vect_out = _tangent(cent, R, sign) + + # in + R = R0 - rplasma * v0["rplasma_ratio"] + vect_in = _tangent(cent, R, sign) + + else: + ephis = np.r_[-eRs[1], eRs[0]] + vect_out = ( + (length + v0["dist"]) * (-eRs) + 0.5 * v0['width'] * ephis + ) + vect_in = ( + (length + v0["dist"]) * (-eRs) - 0.5 * v0['width'] * ephis + ) + vect_out = vect_out / np.linalg.norm(vect_out) + vect_in = vect_in / np.linalg.norm(vect_in) + + # Get FOV from cent + 2 vect + xx, yy = _FOV(cent, vect_out, vect_in, R0, rplasma) + path = mpath.Path(np.array([xx, yy]).T) + + # Sample FOV + DX = np.max(xx) - np.min(xx) + DY = np.max(yy) - np.min(yy) + nptsx = int(DX / res) + nptsy = int(DY / res) + ptsx = np.linspace(np.min(xx), np.max(xx), nptsx) + ptsy = np.linspace(np.min(yy), np.max(yy), nptsy) + ptsx = np.repeat(ptsx[:, None], nptsy, axis=1).ravel() + ptsy = np.repeat(ptsy[None, :], nptsx, axis=0).ravel() + iok = ( + path.contains_points(np.array([ptsx, ptsy]).T) + & (np.hypot(ptsx, ptsy) >= R0 - rplasma) + & (np.hypot(ptsx, ptsy) <= R0 + rplasma) + ) + ptsx = ptsx[iok] + ptsy = ptsy[iok] + + # Angle + pts_phi = np.arctan2(ptsy, ptsx) + pts_ephi0 = -np.sin(pts_phi) + pts_ephi1 = np.cos(pts_phi) + vect0 = ptsx - cent[0] + vect1 = ptsy - cent[1] + vectn = np.sqrt(vect0**2 + vect1**2) + vect0 = vect0 / vectn + vect1 = vect1 / vectn + theta_vs_B = np.arccos(vect0 * pts_ephi0 + vect1 * pts_ephi1) + + # store + dsensors[k0]["ptsx"] = ptsx + dsensors[k0]["ptsy"] = ptsy + dsensors[k0]["rplasma_norm"] = ( + (np.hypot(ptsx, ptsy) - R0) / dinput['rplasma']['data'] + ) + dsensors[k0]["theta_vs_B"] = theta_vs_B + dsensors[k0]["path"] = path + + return dsensors + + +def _tangent( + cent=None, + R=None, + sign=None, +): + + # --------- + # + + phi = np.arctan2(cent[1], cent[0]) + eR = np.r_[np.cos(phi), np.sin(phi)] + ephi = np.r_[-np.sin(phi), np.cos(phi)] + + ang = np.arcsin(R / np.hypot(*cent)) + vect = (-eR) * np.cos(ang) - sign * ephi * np.sin(ang) + vect = vect / np.linalg.norm(vect) + + return vect + + +def _FOV(cent, vect_out, vect_in, R0, rplasma): + + # ------------------ + # intersect vect_out + # ------------------ + + kk_out_out, isout_out_out = _intersect(cent, vect_out, R0 + rplasma) + kk_out_in, isout_out_in = _intersect(cent, vect_out, R0 - rplasma) + + kk_out = np.r_[kk_out_out, kk_out_in] + iok_out = np.r_[isout_out_out, ~isout_out_in] + iok = np.isfinite(kk_out) & iok_out + assert iok.sum() >= 1 + kout = np.min(kk_out[iok]) + pt_out = cent + kout * vect_out + + # ------------------ + # intersect vect_in + # ------------------ + + kk_in_out, isout_in_out = _intersect(cent, vect_in, R0 + rplasma) + kk_in_in, isout_in_in = _intersect(cent, vect_in, R0 - rplasma) + + kk_in = np.r_[kk_in_out, kk_in_in] + iok_in = np.r_[isout_in_out, ~isout_in_in] + iok = np.isfinite(kk_in) & iok_in + assert iok.sum() >= 1 + kin = np.min(kk_in[iok]) + pt_in = cent + kin * vect_in + + assert np.allclose(np.linalg.norm(pt_out), np.linalg.norm(pt_in)) + Rpts = np.linalg.norm(pt_out) + + # ------------------ + # polyx, polyy + # ------------------ + + # ang + ang_out = np.arctan2(pt_out[1], pt_out[0]) + ang_in = np.arctan2(pt_in[1], pt_in[0]) + ang_min = min(ang_out, ang_in) + ang_max = max(ang_out, ang_in) + if np.abs(ang_min - ang_max) > np.pi: + ang_min, ang_max = ang_max, ang_min + 2*np.pi + ang = np.linspace(ang_min, ang_max, 31) + + polyx = np.r_[cent[0], Rpts * np.cos(ang), cent[0]] + polyy = np.r_[cent[1], Rpts * np.sin(ang), cent[1]] + + return polyx, polyy + + +def _intersect(cent, vect, R): + + # ---------- + # kk + + # AM = ku + # R^2 = (OA + AM)^2 = RA^2 + k^2 + 2 ku OA + a = 1 + b = 2 * np.sum(vect * cent) + c = np.sum(cent**2) - R**2 + delta = b**2 - 4 * a * c + + kk = np.full((2,), np.nan) + if delta == 0: + kk[0] = -b / (2*a) + elif delta > 0: + kk = (-b + np.r_[1, -1] * np.sqrt(delta)) / (2 * a) + + # ---------- + # isout + + xx = cent[0] + kk * vect[0] + yy = cent[1] + kk * vect[1] + phi = np.arctan2(yy, xx) + isout = (vect[0] * np.cos(phi) + vect[1] * np.sin(phi)) > 0. + + assert isout.sum() <= 1 + + return kk, isout From 960b20ea39f978dd90b0392612112778ccae69ab Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 13 May 2026 09:51:57 +0000 Subject: [PATCH 68/80] [#1166] Better fig03_tokamak --- .../_figures.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 4368e85ad..80aad4987 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -356,7 +356,7 @@ def fig03_tokamak( ex_marker=None, ex_ms=None, # plot - figsize=(5, 6), + figsize=(5, 7), fontsize=12, pfe_save=None, ): @@ -377,8 +377,8 @@ def fig03_tokamak( dmargin = { 'left': 0.11, 'right': 0.97, - 'bottom': 0.08, 'top': 0.95, - 'wspace': 0.25, 'hspace': 0.35, + 'bottom': 0.06, 'top': 0.99, + 'wspace': 0.25, 'hspace': 0.20, } fig = plt.figure(figsize=figsize) @@ -393,13 +393,12 @@ def fig03_tokamak( ax = fig.add_subplot(gs[0, 0], aspect='equal') ax.set_xlabel('X (m)', fontsize=fontsize, fontweight='bold') ax.set_ylabel('Y (m)', fontsize=fontsize, fontweight='bold') - ax.set_title('SPARC-like tokamak', fontsize=fontsize, fontweight='bold') ax.text( - 0, - 1, + 0.01, + 0.99, '(a)', horizontalalignment='left', - verticalalignment='bottom', + verticalalignment='top', fontsize=fontsize, fontweight='bold', transform=ax.transAxes, @@ -418,13 +417,12 @@ def fig03_tokamak( fontsize=fontsize, fontweight='bold', ) - ax.set_title('FOV sampling', fontsize=fontsize, fontweight='bold') ax.text( - 0, - 1, + 0.01, + 0.99, '(b)', horizontalalignment='left', - verticalalignment='bottom', + verticalalignment='top', fontsize=fontsize, fontweight='bold', transform=ax.transAxes, From 389bf5e60b379497bc8070d4bdc1ccac4c710f23 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Wed, 13 May 2026 14:40:11 +0000 Subject: [PATCH 69/80] [#1166] Started fig02_distributions --- .../_figures.py | 216 +++++++++++++++++- 1 file changed, 215 insertions(+), 1 deletion(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 80aad4987..8a86c4c28 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -4,6 +4,7 @@ import numpy as np +import scipy.integrate as scpinteg import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import matplotlib.patches as mpatches @@ -285,7 +286,220 @@ def fig01_cross_section( # ##################################################### # ##################################################### -# Fig 1 - cross-section +# Fig 02 - RE dist +# ##################################################### + + +_DDIST = { + # maxwell + 'Te_eV': np.r_[1e3, 1e3, 3e3, 3e3], + 'ne_m3': 1e20, + 'jp_Am2': 1e6, + # RE + 'jp_fraction_re': np.r_[0.2, 0.8, 0.2, 0.8], + 'dominant': 'bump', + 'Ekin_max_eV': 20e6, + 'E_eV': np.logspace(0, 8, 80), + 'theta': np.linspace(0, 180, 181) * np.pi / 180, +} + + +def fig02_distributions( + E_eV=None, + theta=None, + ne_m3=None, + jp_Am2=None, + jp_fraction_re=None, + Ekin_max_eV=None, + # plot + figsize=(5, 7), + fontsize=12, + pfe_save=None, +): + + # ------------ + # inputs + # ------------ + + din = locals() + din = { + kk: vv if din.get(kk) is None else din[kk] + for kk, vv in _DDIST.items() + } + + # ------------ + # compute + # ------------ + + # dout = {'dist': dict, 'plasma': dist, 'coords': dist} + dout = tf.physics.electrons.distribution.get_distribution(**din) + + # -------------- + # prepare axes + # -------------- + + dmargin = { + 'left': 0.11, 'right': 0.97, + 'bottom': 0.06, 'top': 0.99, + 'wspace': 0.25, 'hspace': 0.20, + } + + fig = plt.figure(figsize=figsize) + + gs = gridspec.GridSpec(ncols=1, nrows=2, **dmargin) + dax = {} + + # -------------- + # axes - 2d + # -------------- + + ax = fig.add_subplot(gs[0, 0], aspect='auto') + ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel( + r'$\theta_{e0}$ (deg)', + fontsize=fontsize, + fontweight='bold', + ) + ax.text( + 0.01, + 0.99, + '(a)', + horizontalalignment='left', + verticalalignment='top', + fontsize=fontsize, + fontweight='bold', + transform=ax.transAxes, + ) + + dax['2d'] = ax + + # -------------- + # axes - 1d + # -------------- + + ax = fig.add_subplot(gs[1, 0], aspect='auto', sharex=ax) + ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel( + '', + fontsize=fontsize, + fontweight='bold', + ) + ax.text( + 0.01, + 0.99, + '(b)', + horizontalalignment='left', + verticalalignment='top', + fontsize=fontsize, + fontweight='bold', + transform=ax.transAxes, + ) + + dax['1d'] = ax + + # ------------ + # plot 1d + # ------------ + + kax = '1d' + dcolor = {} + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # prepare + dataRE = scpinteg.trapezoid( + dout['dist']['RE']['dist']['data'], + x=dout['coords']['x1']['data'], + axis=-1, + ) + dataMax = scpinteg.trapezoid( + dout['dist']['maxwell']['dist']['data'], + x=dout['coords']['x1']['data'], + axis=-1, + ) + + # loop plot + for ind in np.ndproduct(dataRE.shape[:-2]): + sli = ind + (slice(None),) + + # Max + l0, = ax.plot( + dout['coords']['x0']['data']*1e-3, + dataMax[sli], + ls='-', + lw=1, + color=dcolor[ind], + ) + dcolor[ind] = l0.get_color() + + # RE + ax.plot( + dout['coords']['x0']['data']*1e-3, + dataRE[sli], + ls='--', + lw=1, + color=dcolor[ind], + ) + + # Total + ax.plot( + dout['coords']['x0']['data']*1e-3, + dataMax[sli] + dataRE[sli], + ls='-', + lw=2, + color=dcolor[ind], + label=str(ind), + ) + + ax.legend() + + # ------------ + # plot 2d + # ------------ + + kax = '2d' + dcolor = {} + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # prepare + dataRE = dout['dist']['RE']['dist']['data'] + dataMax = dout['dist']['maxwell']['dist']['data'] + + # loop plot + for ind in np.ndproduct(dataRE.shape[:-2]): + sli = ind + (slice(None), slice(None)) + + cc = ax.contour( + dout['coords']['x0']['data'], + dout['coords']['x1']['data'], + dataRE[sli] + dataMax[sli], + 20, + ls='-', + vmin=0, + vmax=None, + label=str(ind), + color=dcolor[ind], + ) + + # -------------- + # save + # -------------- + + if pfe_save is not False: + if pfe_save is None: + name = 'fig02_distributions.png' + pfe_save = os.path.join(_PATH_HERE, name) + fig.savefig(pfe_save, format='png', dpi=300) + msg = f"Saved figure in:\n\t{pfe_save}\n" + print(msg) + + return dax + + +# ##################################################### +# ##################################################### +# Fig 03 - cross-section # ##################################################### From fd50b8089e94bd7d6732430cfd84b34a5b4a1df9 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 4 Jun 2026 15:12:49 +0000 Subject: [PATCH 70/80] [#1166] RE bump operational --- .../electrons/distribution/_distribution.py | 1 + .../distribution/_distribution_bump.py | 40 ++++++++++++------- .../distribution/_distribution_check.py | 4 ++ .../distribution/_distribution_re.py | 5 +++ 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index c9cf0b60b..b72ccd136 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -41,6 +41,7 @@ def main( # bump step=None, pnormW=None, + theta_width=None, # dominant dominant=None, # ------------ diff --git a/tofu/physics_tools/electrons/distribution/_distribution_bump.py b/tofu/physics_tools/electrons/distribution/_distribution_bump.py index b91bc92df..eb7c617f8 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_bump.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_bump.py @@ -20,7 +20,9 @@ def f2d_momentum_pitch( # params step=None, pnorm0=None, + pmin=None, pnormW=None, + pitch_width=None, E_hat=None, Zeff=None, # unused @@ -40,24 +42,22 @@ def f2d_momentum_pitch( pnorm = np.broadcast_to(pnorm, shape) iok = np.broadcast_to((pitch > 0.) & (pnorm > 0.), shape) - pitch_term = (1 - pitch**2) / np.abs(pitch) - dist = np.zeros(shape, dtype=float) - dreicer_like = np.exp(- pitch_term * pnorm)[iok] / pnorm[iok] - drop = np.exp(-(pnorm - (pnorm0 + 3*pnormW))**2/pnormW**2)[iok] - ione = (pnorm <= (pnorm0 + 3*pnormW))[iok] - drop[ione] = 1. - dist[iok] = ( - dreicer_like - + (step * np.exp(-pitch_term * (pnorm - pnorm0)**4/pnormW**4))[iok] - ) * drop + dreicer_like = pnorm[iok] - dist[iok] = ( - dreicer_like - * (step * np.exp(-pitch_term * (pnorm - pnorm0)**4/pnormW**4))[iok] - ) + drop = np.exp(-(pnorm - pnorm0)**2/(2*pnormW)**2)[iok] + ione = (pnorm <= pnorm0)[iok] + drop[ione] = 1. + ilow = (pnorm <= pmin)[iok] + drop[ilow] = 0. + + pitch_term = np.broadcast_to( + np.exp(-(1 - pitch**2)**2 / pitch_width**2), + shape, + )[iok] + dist[iok] = drop * (dreicer_like + 0) * pitch_term units = asunits.Unit('') return dist, units @@ -69,7 +69,9 @@ def f2d_momentum_theta( # params step=None, pnorm0=None, + pmin=None, pnormW=None, + pitch_width=None, E_hat=None, Zeff=None, # unused @@ -81,7 +83,9 @@ def f2d_momentum_theta( # params step=step, pnorm0=pnorm0, + pmin=pmin, pnormW=pnormW, + pitch_width=pitch_width, E_hat=E_hat, Zeff=Zeff, ) @@ -98,7 +102,9 @@ def f2d_E_theta( # params step=None, pnorm0=None, + pmin=None, pnormW=None, + pitch_width=None, E_hat=None, Zeff=None, # unused @@ -121,7 +127,9 @@ def f2d_E_theta( # params step=step, pnorm0=pnorm0, + pmin=pmin, pnormW=pnormW, + pitch_width=pitch_width, E_hat=E_hat, Zeff=Zeff, ) @@ -150,7 +158,9 @@ def f3d_E_theta( # params step=None, pnorm0=None, + pmin=None, pnormW=None, + pitch_width=None, E_hat=None, Zeff=None, # unused @@ -166,7 +176,9 @@ def f3d_E_theta( # params step=step, pnorm0=pnorm0, + pmin=pmin, pnormW=pnormW, + pitch_width=pitch_width, E_hat=E_hat, Zeff=Zeff, ) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_check.py b/tofu/physics_tools/electrons/distribution/_distribution_check.py index 28ccc8c0b..2c479aee2 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_check.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_check.py @@ -76,6 +76,10 @@ 'def': 0.4, 'units': '', }, + 'theta_width': { + 'def': 20 * np.pi/180, + 'units': 'rad', + }, } diff --git a/tofu/physics_tools/electrons/distribution/_distribution_re.py b/tofu/physics_tools/electrons/distribution/_distribution_re.py index aff214fc5..0de35d142 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_re.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_re.py @@ -87,6 +87,9 @@ def main( lnG=dplasma['lnG']['data'], )['E_C']['data'] + # pitch_width + pitch_width = 1 - np.cos(dplasma['theta_width']['data']) + # ------------- # Intermediates # ------------- @@ -164,6 +167,8 @@ def main( 'step': dplasma['step']['data'][sli0], 'pnorm0': pmax[sli0], 'pnormW': dplasma['pnormW']['data'][sli0], + 'pitch_width': pitch_width, + 'pmin': pmin[sli0], } # update with coords From d2d8d42cfbec3493d20760eea7c7c57acd16afad Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 4 Jun 2026 15:13:32 +0000 Subject: [PATCH 71/80] [#1166] fig02_distributions() operational --- .../_figures.py | 237 ++++++++++++++---- 1 file changed, 193 insertions(+), 44 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 8a86c4c28..ff8883cf9 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -1,6 +1,7 @@ import os +import sys import numpy as np @@ -9,10 +10,17 @@ import matplotlib.gridspec as gridspec import matplotlib.patches as mpatches import matplotlib.path as mpath +import astropy.units as asunits import datastock as ds +_PATH_HERE = os.path.dirname(__file__) +_PATH_TF = os.path.dirname(os.path.dirname(_PATH_HERE)) +sys.path.insert(0, _PATH_TF) import tofu as tf +sys.path.pop(0) + +tfphysdist = tf.physics_tools.electrons.distribution tfphysemis = tf.physics_tools.electrons.emission @@ -292,25 +300,37 @@ def fig01_cross_section( _DDIST = { # maxwell - 'Te_eV': np.r_[1e3, 1e3, 3e3, 3e3], - 'ne_m3': 1e20, + 'Te_eV': np.r_[0.1e3, 0.1e3, 1e3, 1e3], + 'ne_m3': 1e19, 'jp_Am2': 1e6, # RE - 'jp_fraction_re': np.r_[0.2, 0.8, 0.2, 0.8], + 'jp_fraction_re': np.r_[0.1, 0.9, 0.1, 0.9], 'dominant': 'bump', - 'Ekin_max_eV': 20e6, + 'Ekin_max_eV': 1e6, + 'Ekin_min_eV': 100, + 'step': 1, + 'pnormW': 5, + 'theta_width': 20*np.pi/180, + # coords 'E_eV': np.logspace(0, 8, 80), 'theta': np.linspace(0, 180, 181) * np.pi / 180, } def fig02_distributions( + # coords E_eV=None, theta=None, + # Maxwell ne_m3=None, jp_Am2=None, + # RE + dominant=None, jp_fraction_re=None, Ekin_max_eV=None, + Ekin_min_eV=None, + step=None, + pnormW=None, # plot figsize=(5, 7), fontsize=12, @@ -332,16 +352,94 @@ def fig02_distributions( # ------------ # dout = {'dist': dict, 'plasma': dist, 'coords': dist} - dout = tf.physics.electrons.distribution.get_distribution(**din) + dout = tfphysdist.get_distribution(**din) + + # units + units2d = asunits.Unit(dout['dist']['RE']['dist']['units']) + units1d = units2d * asunits.Unit(dout['coords']['x1']['units']) + + # ------------ + # Derive 1d data + # ------------ + + dataRE = scpinteg.trapezoid( + dout['dist']['RE']['dist']['data'], + x=dout['coords']['x1']['data'], + axis=-1, + ) + dataMax = scpinteg.trapezoid( + dout['dist']['maxwell']['dist']['data'], + x=dout['coords']['x1']['data'], + axis=-1, + ) + + # ------------ + # Derive levels, vmin, vmax + # ------------ + + Ekin_max = dout['plasma']['Ekin_max_eV']['data'] + vminRE_2d = np.inf + vminRE_1d = np.inf + for ind in np.ndindex(dataRE.shape[:-1]): + indE = np.argmin(np.abs(dout['coords']['x0']['data'] - Ekin_max[ind])) + sli = ind + (indE, slice(None)) + vmaxRE_2d = np.nanmax(dout['dist']['RE']['dist']['data'][sli]) + vminRE_2d = min(vminRE_2d, vmaxRE_2d) + sli = ind + (indE,) + vmaxRE_1d = dataRE[sli] + vminRE_1d = min(vminRE_1d, vmaxRE_1d) + vmaxRE_2d = np.nanmax(dout['dist']['RE']['dist']['data']) + vmaxRE_1d = np.nanmax(dataRE) + vmaxMax_2d = np.nanmax(dout['dist']['maxwell']['dist']['data']) + vmaxMax_1d = np.nanmax(dataMax) + + # 1d + vmaxlog10_1d = np.log10(max(vmaxRE_1d, vmaxMax_1d)) + dlog10_1d = vmaxlog10_1d - np.log10(vminRE_1d) + vmaxlog10_1d = np.ceil(vmaxlog10_1d) + vminlog10_1d = np.floor(vmaxlog10_1d - 1 - 1.2*dlog10_1d) + vmax_1d = 10**vmaxlog10_1d + vmin_1d = 10**vminlog10_1d + + # 2d + vmaxlog10_2d = np.log10(max(vmaxRE_2d, vmaxMax_2d)) + dlog10_2d = vmaxlog10_2d - np.log10(vminRE_2d) + vmaxlog10_2d = np.ceil(vmaxlog10_2d) + vminlog10_2d = np.floor(vmaxlog10_2d - 1 - 1.2*dlog10_2d) + levels_2d = np.logspace(vminlog10_2d, vmaxlog10_2d - 1, 6) + + # -------------- + # labels + # -------------- + + dlabel = {} + for ind in np.ndindex(dout['dist']['RE']['dist']['data'].shape[:-2]): + Te = dout['plasma']['Te_eV']['data'][ind] * 1e-3 + jpf = dout['plasma']['jp_fraction_re']['data'][ind] + + dlabel[ind] = f"{jpf:2.1f} , {Te:2.1f} keV" + + # title + ne = np.unique(dout['plasma']['ne_m3']['data']) + assert ne.size == 1 + jp = np.unique(dout['plasma']['jp_Am2']['data']) + assert jp.size == 1 + tit = f"ne = {ne[0]:2.1e}, jp_tot = {jp[0]*1e-6:2.1f} MA/m2" + + # -------------- + # print + # -------------- + + _print(dout) # -------------- # prepare axes # -------------- dmargin = { - 'left': 0.11, 'right': 0.97, - 'bottom': 0.06, 'top': 0.99, - 'wspace': 0.25, 'hspace': 0.20, + 'left': 0.12, 'right': 0.98, + 'bottom': 0.06, 'top': 0.97, + 'wspace': 0.25, 'hspace': 0.10, } fig = plt.figure(figsize=figsize) @@ -353,13 +451,13 @@ def fig02_distributions( # axes - 2d # -------------- - ax = fig.add_subplot(gs[0, 0], aspect='auto') - ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') + ax = fig.add_subplot(gs[0, 0], aspect='auto', xscale='log') ax.set_ylabel( - r'$\theta_{e0}$ (deg)', + r'$\theta_{e_0,B}$ (deg)', fontsize=fontsize, fontweight='bold', ) + ax.set_title(tit, fontsize=fontsize, fontweight='bold') ax.text( 0.01, 0.99, @@ -377,10 +475,10 @@ def fig02_distributions( # axes - 1d # -------------- - ax = fig.add_subplot(gs[1, 0], aspect='auto', sharex=ax) + ax = fig.add_subplot(gs[1, 0], aspect='auto', sharex=ax, yscale='log') ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') ax.set_ylabel( - '', + f" ({units1d})", fontsize=fontsize, fontweight='bold', ) @@ -397,6 +495,8 @@ def fig02_distributions( dax['1d'] = ax + dax = ds._generic_check._check_dax(dax) + # ------------ # plot 1d # ------------ @@ -406,20 +506,11 @@ def fig02_distributions( if dax.get(kax) is not None: ax = dax[kax]['handle'] - # prepare - dataRE = scpinteg.trapezoid( - dout['dist']['RE']['dist']['data'], - x=dout['coords']['x1']['data'], - axis=-1, - ) - dataMax = scpinteg.trapezoid( - dout['dist']['maxwell']['dist']['data'], - x=dout['coords']['x1']['data'], - axis=-1, - ) + # for legend + ax.plot([], [], c='w', ls='-', lw=1, label='j_frac, Te') # loop plot - for ind in np.ndproduct(dataRE.shape[:-2]): + for ind in np.ndindex(dataRE.shape[:-1]): sli = ind + (slice(None),) # Max @@ -428,7 +519,6 @@ def fig02_distributions( dataMax[sli], ls='-', lw=1, - color=dcolor[ind], ) dcolor[ind] = l0.get_color() @@ -448,38 +538,46 @@ def fig02_distributions( ls='-', lw=2, color=dcolor[ind], - label=str(ind), + label=dlabel[ind], ) + # Add critical energy + Ec = tf.physics_tools.electrons.convert_momentum_velocity_energy( + momentum_normalized=dout['dist']['RE']['p_crit']['data'], + )['energy_kinetic_eV']['data'] + for ec in np.unique(Ec): + ax.axvline(ec*1e-3, c='k', lw=1, ls='--') + + # decorate ax.legend() + ax.grid(True) + ax.set_ylim(vmin_1d, vmax_1d) # ------------ # plot 2d # ------------ kax = '2d' - dcolor = {} if dax.get(kax) is not None: ax = dax[kax]['handle'] - # prepare - dataRE = dout['dist']['RE']['dist']['data'] - dataMax = dout['dist']['maxwell']['dist']['data'] - # loop plot - for ind in np.ndproduct(dataRE.shape[:-2]): + for ind in np.ndindex(dataRE.shape[:-1]): sli = ind + (slice(None), slice(None)) - cc = ax.contour( - dout['coords']['x0']['data'], - dout['coords']['x1']['data'], - dataRE[sli] + dataMax[sli], - 20, - ls='-', - vmin=0, - vmax=None, - label=str(ind), - color=dcolor[ind], + # data + data = ( + dout['dist']['maxwell']['dist']['data'][sli] + + dout['dist']['RE']['dist']['data'][sli] + ) + + # contour + ax.contour( + dout['coords']['x0']['data']*1e-3, + dout['coords']['x1']['data']*180/np.pi, + data.T, + levels_2d, + colors=dcolor[ind], ) # -------------- @@ -494,7 +592,58 @@ def fig02_distributions( msg = f"Saved figure in:\n\t{pfe_save}\n" print(msg) - return dax + return dax, dout + + +def _print(dout, sep=' '): + + # ----------- + # header + + head = [ + 'ind', + 'Te (keV)', + 'ne (1e20/m3)', 'Max / RE', + 'jp (MA/m2)', 'Max / RE', + ] + lmax = np.max([len(ss) for ss in head]) + + # ----------- + # header + + lc = [] + for ind in np.ndindex(dout['dist']['RE']['dist']['data'].shape[:-2]): + Te = dout['plasma']['Te_eV']['data'][ind]*1e-3 + ne = dout['plasma']['ne_m3']['data'][ind]*1e-20 + jp = dout['plasma']['jp_Am2']['data'][ind]*1e-6 + ne_max = dout['dist']['maxwell']['integ_ne']['data'][ind]*1e-20 + ne_RE = dout['dist']['RE']['integ_ne']['data'][ind]*1e-20 + jp_max = dout['dist']['maxwell']['integ_jp']['data'][ind]*1e-6 + jp_RE = dout['dist']['RE']['integ_jp']['data'][ind]*1e-6 + + cc = [ + str(ind), + f'{Te:2.1f}', + f'{ne:2.2f}', f"{ne_max:2.2f} / {ne_RE:2.2f}", + f'{jp:2.2f}', f"{jp_max:2.2f} / {jp_RE:2.2f}", + ] + lc.append(cc) + lmax = max(lmax, np.max([len(ss) for ss in cc])) + + # ---------------- + # concatenate + + line = sep.join(['-'*lmax for ss in head]) + head = sep.join([ss.ljust(lmax) for ss in head]) + lc = [ + sep.join([ss.ljust(lmax) for ss in cc]) + for cc in lc + ] + + msg = '\n'.join([head, line] + lc) + print(msg) + + return # ##################################################### From 59b6f7c719e306b542426076f00077d7a2d14e0f Mon Sep 17 00:00:00 2001 From: dvezinet Date: Thu, 4 Jun 2026 16:45:24 +0000 Subject: [PATCH 72/80] [#1166] Minor quality of life improvement --- tofu/physics_tools/electrons/distribution/_distribution.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tofu/physics_tools/electrons/distribution/_distribution.py b/tofu/physics_tools/electrons/distribution/_distribution.py index b72ccd136..5c376895a 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution.py +++ b/tofu/physics_tools/electrons/distribution/_distribution.py @@ -63,6 +63,8 @@ def main( verb=None, # return returnas=None, + # unused + **kwdargs, ): # -------------- From fabeb2bc5531e229ce3de2e07474489dd4d3ce32 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 19:23:38 +0000 Subject: [PATCH 73/80] [#1166] get_2drcoss_phi() uses vectors from d2cross if provided as a dict --- ..._xray_thin_target_integrated_d2crossphi.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py index 9c493305d..a8983cf87 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_d2crossphi.py @@ -69,6 +69,11 @@ def get_d2cross_phi( # unused **kwdargs, ): + """ Integrates d2cross over phi_e0_vsB + + Intermediate between d2cross and integration over an electron distribution + + """ # ---------------- # inputs @@ -230,7 +235,10 @@ def _check_compute( # ---------- if E_ph_eV is None: - E_ph_eV = _E_PH_EV + if isinstance(d2cross, dict): + E_ph_eV = d2cross['E_ph']['data'] + else: + E_ph_eV = _E_PH_EV E_ph_eV = ds._generic_check._check_flat1darray( E_ph_eV, 'E_ph_eV', @@ -250,11 +258,14 @@ def _check_compute( )) if E_e0_eV is None: - E_e0_eV = np.logspace( - np.log10(E_ph_eV.min()), - np.ceil(np.log10(E_ph_eV.max())) + 2, - E_e0_eV_npts, - ) + if isinstance(d2cross, dict): + E_e0_eV = d2cross['E_e0']['data'] + else: + E_e0_eV = np.logspace( + np.log10(E_ph_eV.min()), + np.ceil(np.log10(E_ph_eV.max())) + 2, + E_e0_eV_npts, + ) E_e0_eV = np.unique(ds._generic_check._check_flat1darray( E_e0_eV, 'E_e0_eV', @@ -293,7 +304,10 @@ def _check_compute( # ------------ if theta_ph_vsB is None: - theta_ph_vsB = _THETA_PH_VSB + if isinstance(d2cross, dict): + theta_ph_vsB = d2cross['theta_ph']['data'] + else: + theta_ph_vsB = _THETA_PH_VSB theta_ph_vsB = ds._generic_check._check_flat1darray( theta_ph_vsB, 'theta_ph_vsB', @@ -312,11 +326,16 @@ def _check_compute( # theta_e0_vsB # ------------ + if isinstance(d2cross, dict): + npdef = d2cross['theta_e']['data'].size + else: + npdef = _THETA_E0_VSB_NPTS + theta_e0_vsB_npts = int(ds._generic_check._check_var( theta_e0_vsB_npts, 'theta_e0_vsB_npts', types=(int, float), sign='>=3', - default=_THETA_E0_VSB_NPTS, + default=npdef, )) theta_e0_vsB = np.linspace(0, np.pi, theta_e0_vsB_npts) From 411249f0088c77e448a2fdb8f29021b398de7ffb Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 19:25:01 +0000 Subject: [PATCH 74/80] [#1166] get_xray_thin_integ_dist() updated for using bump RE distribution --- .../emission/_xray_thin_target_integrated_dist.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py index 1c97c24b3..659019bfc 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist.py @@ -60,13 +60,19 @@ def get_xray_thin_integ_dist( jp_Am2=None, jp_fraction_re=None, # RE-specific + Te_eV_re=None, + ne_m3_re=None, Zeff=None, Ekin_max_eV=None, + Ekin_min_eV=None, Efield_par_Vm=None, lnG=None, sigmap=None, - Te_eV_re=None, - ne_m3_re=None, + # bump + step=None, + pnormW=None, + theta_width=None, + # dominant dominant=None, # ---------------- # cross-section From 4859b8921599cefa2835c1df2d1d1b7726f47638 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 19:27:24 +0000 Subject: [PATCH 75/80] [#1166] plot_xray_thin_integ_dist() updated for bump --- .../_xray_thin_target_integrated_dist_plot.py | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_plot.py b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_plot.py index 1074ce778..9bcf53026 100644 --- a/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_plot.py +++ b/tofu/physics_tools/electrons/emission/_xray_thin_target_integrated_dist_plot.py @@ -54,13 +54,19 @@ def plot_xray_thin_integ_dist( jp_Am2=None, jp_fraction_re=None, # RE-specific + Te_eV_re=None, + ne_m3_re=None, Zeff=None, Ekin_max_eV=None, + Ekin_min_eV=None, Efield_par_Vm=None, lnG=None, sigmap=None, - Te_eV_re=None, - ne_m3_re=None, + # bump + step=None, + pnormW=None, + theta_width=None, + # dominant dominant=None, # ---------------- # cross-section @@ -114,23 +120,7 @@ def plot_xray_thin_integ_dist( plot_angular_spectra, plot_anisotropy_map, verb, - ) = _check( - # electron distribution - Te_eV=Te_eV, - ne_m3=ne_m3, - jp_Am2=jp_Am2, - jp_fraction_re=jp_fraction_re, - # cross-section - E_ph_eV=E_ph_eV, - E_e0_eV=E_e0_eV, - theta_e0_vsB_npts=theta_e0_vsB_npts, - theta_ph_vsB=theta_ph_vsB, - version_cross=version_cross, - # plots - plot_angular_spectra=plot_angular_spectra, - plot_anisotropy_map=plot_anisotropy_map, - verb=verb, - ) + ) = _check(**locals()) # ------------- # compute @@ -205,18 +195,52 @@ def _check( ne_m3=None, jp_Am2=None, jp_fraction_re=None, + # RE-specific + Te_eV_re=None, + ne_m3_re=None, + Zeff=None, + Ekin_max_eV=None, + Ekin_min_eV=None, + Efield_par_Vm=None, + lnG=None, + sigmap=None, + # bump + step=None, + pnormW=None, + theta_width=None, + # dominant + dominant=None, # ---------------- # cross-section E_ph_eV=None, E_e0_eV=None, + E_e0_eV_npts=None, theta_e0_vsB_npts=None, + phi_e0_vsB_npts=None, theta_ph_vsB=None, - # version + # inputs + Z=None, + # hypergeometric parameter + ninf=None, + source=None, + # integration parameters + nthetae=None, + ndphi=None, version_cross=None, + # save / load + pfe_d2cross_phi=None, + # ----------------- + # optional responsivity + dresponsivity=None, + # ----------------- # plots plot_angular_spectra=None, plot_anisotropy_map=None, + plot_E_ph=None, + # verb verb=None, + # unused + **kwdargs, ): # -------------------- From aecb034aeef490654816e9b8e2bfbce86f0caaf9 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 19:27:45 +0000 Subject: [PATCH 76/80] [#1166] _figure updated for fig04_Bremsstrahlung --- .../_figures.py | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index ff8883cf9..54803e2b1 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -1436,3 +1436,291 @@ def _intersect(cent, vect, R): assert isout.sum() <= 1 return kk, isout + + +# ##################################################### +# ##################################################### +# Fig 04 - Bremsstrahlung +# ##################################################### + + +_PFE_D2CROSS_PHI = os.path.join( + _PATH_HERE, + 'd2cross_phi_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_nthetaph61_nthetae060_EH.npz', +) + + +_CASES = { + 'case': { + '0': {'Te': 0.1e3, 'jp_frac': 0.9, 'color': 'r'}, + '1': {'Te': 1e3, 'jp_frac': 0.1, 'color': 'b'}, + }, + 'theta_ph_vsB': { + 'val': np.r_[0, 0.5, 1]*np.pi, + 'ls': ['-', '--', ':'], + }, + 'E_ph_eV': np.r_[0.1, 1, 10]*1e3, +} + + +def fig04_Bremsstrahlung( + d2cross_phi=None, + # cases + cases=None, + # plot + figsize=(7, 4), + fontsize=14, + # save + pfe_save=None, +): + + # ------------ + # d2cross_phi + # ------------ + + if d2cross_phi is None: + d2cross_phi = _PFE_D2CROSS_PHI + + # ------------ + # ddist + # ------------ + + ddist = locals() + ddist = { + kk: vv if ddist.get(kk) is None else ddist[kk] + for kk, vv in _DDIST.items() + if kk not in ['E_eV', 'theta'] + } + + # -------------- + # integrated cross-section + # -------------- + + demiss, ddist, d2cross_phi = tfphysemis.get_xray_thin_integ_dist( + # ---------------- + # cross-section + # tabulated d2cross_phi + d2cross_phi=d2cross_phi, + # d2cross_phi computation + E_ph_eV=None, + E_e0_eV=None, + E_e0_eV_npts=None, + theta_e0_vsB_npts=None, + phi_e0_vsB_npts=None, + theta_ph_vsB=None, + # inputs + Z=None, + # hypergeometric parameter + ninf=None, + source=None, + # integration parameters + nthetae=None, + ndphi=None, + # output customization + version_cross=None, + # save / load + save_d2cross_phi=False, + # --------------------- + # optional responsivity + dresponsivity=None, + plot_responsivity_integration=None, + # ----------- + # verb + debug=False, + verb=True, + # ---------------- + # electron distribution + **ddist, + ) + + units = demiss['emiss']['RE']['emiss']['units'] + + # -------------- + # cases + # -------------- + + if cases is None: + cases = _CASES + + # -------------- + # prepare axes + # -------------- + + dmargin = { + 'left': 0.11, 'right': 0.97, + 'bottom': 0.12, 'top': 0.98, + 'wspace': 0.25, 'hspace': 0.20, + } + + fig = plt.figure(figsize=figsize) + + gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) + dax = {} + + # ---------------- + # ax - spectra + # ---------------- + + ax = fig.add_subplot(gs[0, 0], aspect='auto') + ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel(f'emiss ({units})', fontsize=fontsize, fontweight='bold') + + dax['spectra'] = ax + + dax = ds._generic_check._check_dax(dax) + + # -------------- + # plot - resp + # -------------- + + kax = 'spectra' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for kdist in demiss['emiss'].keys(): + + # loop on spectra + for i0, (k0, v0) in enumerate(cases['case'].items()): + for i1, cc in enumerate(cases['theta_ph_vsB']['val']): + + # slice + ic = ( + (ddist['plasma']['jp_fraction_re']['data'] == v0['jp_frac']) + & (ddist['plasma']['Te_eV']['data'] == v0['Te']) + ) + assert ic.sum() == 1 + it = np.argmin(np.abs(demiss['theta_ph_vsB']['data'] - cc)) + sli = (ic.nonzero()[0][0], slice(None), it) + + # plot + import pdb; pdb.set_trace() # DB + ax.loglog( + demiss['E_ph_eV']['data']*1e-3, + demiss['emiss'][kdist]['emiss']['data'][sli], + ls=cases['theta_ph_vsB']['ls'][i1], + lw=1 if kdist == 'RE' else 2, + marker='None', + color=v0['color'], + ) + + return dax, demiss, ddist, d2cross_phi + + +# ##################################################### +# ##################################################### +# Fig 05 - responsivities +# ##################################################### + + +_PFE_RESPONSIVITIES = os.path.join(_PATH_HERE, 'responsivities.npz') + + +def fig05_responsivities( + pfe=None, + lw=2, + figsize=(7, 4), + fontsize=14, + pfe_save=None, +): + + # -------------- + # load + # -------------- + + if pfe is None: + pfe = _PFE_RESPONSIVITIES + + dresp = { + k0: v0.tolist() + for k0, v0 in np.load(pfe, allow_pickle=True).items() + } + + # -------------- + # prepare axes + # -------------- + + dmargin = { + 'left': 0.11, 'right': 0.97, + 'bottom': 0.12, 'top': 0.98, + 'wspace': 0.25, 'hspace': 0.20, + } + + fig = plt.figure(figsize=figsize) + + gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) + dax = {} + + # -------------- + # axes - resp + # -------------- + + ax = fig.add_subplot(gs[0, 0], aspect='auto') + ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel('responsivity', fontsize=fontsize, fontweight='bold') + + dax['resp'] = ax + + dax = ds._generic_check._check_dax(dax) + + # -------------- + # plot - resp + # -------------- + + kax = 'resp' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + # --------------- + # loop on sensors + + dme = {'mesxr': False, 'mehxr': False, 'cvd': False} + for k0, v0 in dresp.items(): + + # resp, color, lab + lk = [kk for kk in dme.keys() if kk in k0] + if len(lk) == 1: + kk = lk[0] + if dme[kk] is False: + dme[kk] = v0.get('color', 'k') + lab = f"{kk} - {v0['responsivity']['units']}" + else: + v0['color'] = dme[kk] + v0['ls'] = '--' + lab = None + else: + lab = f"{k0} - {v0['responsivity']['units']}" + + # lw + if lw is None: + lwi = v0.get('lw', 1) + else: + lwi = lw + + # plot + ax.loglog( + v0['E_eV']['data']*1e-3, + v0['responsivity']['data'], + ls=v0.get('ls', '-'), + lw=lwi, + c=v0.get('color', 'k'), + marker=v0.get('marker', 'None'), + label=lab, + ) + + ax.set_ylim(1e-4, 2) + ax.grid(True) + ax.legend() + + # -------------- + # save + # -------------- + + if pfe_save is not False: + if pfe_save is None: + name = 'fig05_responsivities.png' + pfe_save = os.path.join(_PATH_HERE, name) + fig.savefig(pfe_save, format='png', dpi=300) + msg = f"Saved figure in:\n\t{pfe_save}\n" + print(msg) + + return dax, dresp From 95ec92ed8cd2709ce0bc1da5c291aae66ff81f1e Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 21:56:49 +0000 Subject: [PATCH 77/80] [#1166] Fixed RE distribution shape / slices --- .../electrons/distribution/_distribution_re.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tofu/physics_tools/electrons/distribution/_distribution_re.py b/tofu/physics_tools/electrons/distribution/_distribution_re.py index 0de35d142..ffaae195d 100644 --- a/tofu/physics_tools/electrons/distribution/_distribution_re.py +++ b/tofu/physics_tools/electrons/distribution/_distribution_re.py @@ -167,12 +167,16 @@ def main( 'step': dplasma['step']['data'][sli0], 'pnorm0': pmax[sli0], 'pnormW': dplasma['pnormW']['data'][sli0], - 'pitch_width': pitch_width, + 'pitch_width': pitch_width[sli0], 'pmin': pmin[sli0], } # update with coords - kwdargsi.update(**dcoords) + nc0 = kwdargsi['Cs'].ndim-2 + sli_coord = (0,) * nc0 + (slice(None), slice(None)) + kwdargsi.update(**{ + kk: vv[sli_coord] for kk, vv in dcoords.items() + }) # compute re_dist[sli1], dunits[dominant['meaning'][vv]] = getattr( From 5794297987c94f96c8be2ee0820aaf7c08d4d855 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Mon, 8 Jun 2026 21:57:02 +0000 Subject: [PATCH 78/80] [#1166] Alsmot done fig04_Bremsstrahlung --- .../_figures.py | 138 +++++++++++++++--- 1 file changed, 119 insertions(+), 19 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 54803e2b1..432d671ca 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -1459,7 +1459,10 @@ def _intersect(cent, vect, R): 'val': np.r_[0, 0.5, 1]*np.pi, 'ls': ['-', '--', ':'], }, - 'E_ph_eV': np.r_[0.1, 1, 10]*1e3, + 'E_ph_eV': { + 'val': np.r_[0.1, 1, 10]*1e3, + 'ls': ['-', '--', ':'], + }, } @@ -1491,6 +1494,8 @@ def fig04_Bremsstrahlung( for kk, vv in _DDIST.items() if kk not in ['E_eV', 'theta'] } + ddist['Te_eV'] = 1e3 * np.linspace(0.1, 2.5, 25)[:, None] + ddist['jp_fraction_re'] = np.linspace(0., 1., 11)[None, :] # -------------- # integrated cross-section @@ -1542,6 +1547,23 @@ def fig04_Bremsstrahlung( if cases is None: cases = _CASES + # -------------- + # Elim + # -------------- + + shape = ddist['plasma']['Te_eV']['data'].shape + Elim = np.full(shape, np.nan) + theta_Elim = 0 + for ii, ind in enumerate(np.ndindex(shape)): + + sli_emiss = ind + (slice(None), 0) + emiss_RE = demiss['emiss']['RE']['emiss']['data'][sli_emiss] + emiss_max = demiss['emiss']['maxwell']['emiss']['data'][sli_emiss] + + ilim = (emiss_RE > emiss_max) + if np.any(ilim): + Elim[ind] = np.min(demiss['E_ph_eV']['data'][ilim]) + # -------------- # prepare axes # -------------- @@ -1554,7 +1576,7 @@ def fig04_Bremsstrahlung( fig = plt.figure(figsize=figsize) - gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) + gs = gridspec.GridSpec(ncols=2, nrows=2, **dmargin) dax = {} # ---------------- @@ -1567,42 +1589,120 @@ def fig04_Bremsstrahlung( dax['spectra'] = ax + # ---------------- + # ax - theta + # ---------------- + + ax = fig.add_subplot(gs[1, 0], aspect='auto') + ax.set_xlabel( + r'$\theta_{ph,B}$' + ' (deg)', + fontsize=fontsize, + fontweight='bold', + ) + ax.set_ylabel(f'emiss ({units})', fontsize=fontsize, fontweight='bold') + ax.set_xlim(0, 180) + + dax['theta'] = ax + + # ---------------- + # ax - Elim + # ---------------- + + ax = fig.add_subplot(gs[:, 1], aspect='auto') + ax.set_xlabel('Te (keV)', fontsize=fontsize, fontweight='bold') + ax.set_ylabel('jp_frac', fontsize=fontsize, fontweight='bold') + + dax['Elim'] = ax + dax = ds._generic_check._check_dax(dax) # -------------- - # plot - resp + # plot - cases # -------------- - kax = 'spectra' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] + Teu = np.unique(ddist['plasma']['Te_eV']['data']) + jp_fracu = np.unique(ddist['plasma']['jp_fraction_re']['data']) + for kdist in demiss['emiss'].keys(): - for kdist in demiss['emiss'].keys(): + # loop on spectra + for i0, (k0, v0) in enumerate(cases['case'].items()): - # loop on spectra - for i0, (k0, v0) in enumerate(cases['case'].items()): - for i1, cc in enumerate(cases['theta_ph_vsB']['val']): + # slice + Te = Teu[np.argmin(np.abs(Teu - v0['jp_frac']))] + jpf = jp_fracu[np.argmin(np.abs(jp_fracu - v0['jp_frac']))] + ic = ( + (ddist['plasma']['jp_fraction_re']['data'] == jpf) + & (ddist['plasma']['Te_eV']['data'] == Te) + ) + assert ic.sum() == 1 - # slice - ic = ( - (ddist['plasma']['jp_fraction_re']['data'] == v0['jp_frac']) - & (ddist['plasma']['Te_eV']['data'] == v0['Te']) - ) - assert ic.sum() == 1 + # -------- + # spectra + + kax = 'spectra' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for i1, cc in enumerate(cases['theta_ph_vsB']['val']): it = np.argmin(np.abs(demiss['theta_ph_vsB']['data'] - cc)) - sli = (ic.nonzero()[0][0], slice(None), it) + sli = tuple([cc[0] for cc in ic.nonzero()]) + (slice(None), it) + emiss_E = demiss['emiss'][kdist]['emiss']['data'][sli] # plot - import pdb; pdb.set_trace() # DB ax.loglog( demiss['E_ph_eV']['data']*1e-3, - demiss['emiss'][kdist]['emiss']['data'][sli], + emiss_E, ls=cases['theta_ph_vsB']['ls'][i1], lw=1 if kdist == 'RE' else 2, marker='None', color=v0['color'], ) + # -------- + # theta + + kax = 'theta' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for i1, cc in enumerate(cases['E_ph_eV']['val']): + iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - cc)) + sli = sli[:-2] + (iE, slice(None)) + emiss_theta = demiss['emiss'][kdist]['emiss']['data'][sli] + + # plot + ax.plot( + demiss['theta_ph_vsB']['data']*180/np.pi, + emiss_theta / emiss_theta.max(), + ls=cases['theta_ph_vsB']['ls'][i1], + lw=1 if kdist == 'RE' else 2, + marker='None', + color=v0['color'], + ) + + # -------------- + # plot - Elim + # -------------- + + kax = 'Elim' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + cs = ax.contour( + ddist['plasma']['Te_eV']['data'] * 1e-3, + ddist['plasma']['jp_fraction_re']['data'], + Elim * 1e-3, + cmap=plt.cm.viridis, + levels=np.r_[0.1, 0.2, 0.5, 1, 2, 5, 10, 20]*1e3, + vmin=0.1, + vmax=20, + ) + + ax.clabel(cs, cs.levels, fontsize=12) + + ax.set_xlim(0, ddist['plasma']['Te_eV']['data'].max()*1e-3) + ax.set_ylim(0, 1) + return dax, demiss, ddist, d2cross_phi From ee25d8848270e807485928b275b2b0a30308112e Mon Sep 17 00:00:00 2001 From: dvezinet Date: Tue, 9 Jun 2026 21:51:16 +0000 Subject: [PATCH 79/80] [#1166] Finalizing fig04 --- .../_figures.py | 171 +++++++++++++----- 1 file changed, 123 insertions(+), 48 deletions(-) diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py index 432d671ca..53a723423 100644 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ b/publications/2026_runawayBremsstrahlungDetection/_figures.py @@ -1453,15 +1453,15 @@ def _intersect(cent, vect, R): _CASES = { 'case': { '0': {'Te': 0.1e3, 'jp_frac': 0.9, 'color': 'r'}, - '1': {'Te': 1e3, 'jp_frac': 0.1, 'color': 'b'}, + '1': {'Te': 2.5e3, 'jp_frac': 0.1, 'color': 'b'}, }, 'theta_ph_vsB': { 'val': np.r_[0, 0.5, 1]*np.pi, 'ls': ['-', '--', ':'], }, 'E_ph_eV': { - 'val': np.r_[0.1, 1, 10]*1e3, - 'ls': ['-', '--', ':'], + 'val': np.r_[0.1, 5, 20]*1e3, + 'ls': ['-', '-', '-'], }, } @@ -1471,7 +1471,7 @@ def fig04_Bremsstrahlung( # cases cases=None, # plot - figsize=(7, 4), + figsize=(15, 7), fontsize=14, # save pfe_save=None, @@ -1495,7 +1495,7 @@ def fig04_Bremsstrahlung( if kk not in ['E_eV', 'theta'] } ddist['Te_eV'] = 1e3 * np.linspace(0.1, 2.5, 25)[:, None] - ddist['jp_fraction_re'] = np.linspace(0., 1., 11)[None, :] + ddist['jp_fraction_re'] = np.linspace(0., 1., 21)[None, :] # -------------- # integrated cross-section @@ -1539,6 +1539,7 @@ def fig04_Bremsstrahlung( ) units = demiss['emiss']['RE']['emiss']['units'] + ne = np.unique(ddist['plasma']['ne_m3']['data'])[0] # -------------- # cases @@ -1569,23 +1570,33 @@ def fig04_Bremsstrahlung( # -------------- dmargin = { - 'left': 0.11, 'right': 0.97, - 'bottom': 0.12, 'top': 0.98, - 'wspace': 0.25, 'hspace': 0.20, + 'left': 0.06, 'right': 0.98, + 'bottom': 0.06, 'top': 0.95, + 'wspace': 0.25, 'hspace': 0.30, } fig = plt.figure(figsize=figsize) - gs = gridspec.GridSpec(ncols=2, nrows=2, **dmargin) + nE = len(cases['E_ph_eV']['val']) + gs = gridspec.GridSpec(ncols=nE + 2, nrows=2, **dmargin) dax = {} # ---------------- # ax - spectra # ---------------- - ax = fig.add_subplot(gs[0, 0], aspect='auto') + ax = fig.add_subplot(gs[0, :nE], aspect='auto') ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel(f'emiss ({units})', fontsize=fontsize, fontweight='bold') + ax.set_ylabel( + r"$\epsilon$" + f' ({units})', + fontsize=fontsize, + fontweight='bold', + ) + ax.set_title( + r"$n_e$" + f" = {ne:1.0e} /m3", + fontsize=fontsize, + fontweight='bold', + ) dax['spectra'] = ax @@ -1593,22 +1604,32 @@ def fig04_Bremsstrahlung( # ax - theta # ---------------- - ax = fig.add_subplot(gs[1, 0], aspect='auto') - ax.set_xlabel( - r'$\theta_{ph,B}$' + ' (deg)', - fontsize=fontsize, - fontweight='bold', - ) - ax.set_ylabel(f'emiss ({units})', fontsize=fontsize, fontweight='bold') - ax.set_xlim(0, 180) + ax0 = None + for ii in range(nE): + ax = fig.add_subplot( + gs[1, ii], + aspect='auto', + sharex=ax0, + sharey=ax0, + ) + ax.set_xlabel( + r'$\theta_{ph,B}$' + ' (deg)', + fontsize=fontsize, + fontweight='bold', + ) + if ii == 0: + ax.set_ylabel('emiss (norm.)', fontsize=fontsize, fontweight='bold') + ax.set_xlim(0, 180) + ax.set_ylim(0, 1) + ax0 = ax - dax['theta'] = ax + dax[f'theta_{ii}'] = ax # ---------------- # ax - Elim # ---------------- - ax = fig.add_subplot(gs[:, 1], aspect='auto') + ax = fig.add_subplot(gs[:, nE:], aspect='auto') ax.set_xlabel('Te (keV)', fontsize=fontsize, fontweight='bold') ax.set_ylabel('jp_frac', fontsize=fontsize, fontweight='bold') @@ -1622,30 +1643,32 @@ def fig04_Bremsstrahlung( Teu = np.unique(ddist['plasma']['Te_eV']['data']) jp_fracu = np.unique(ddist['plasma']['jp_fraction_re']['data']) - for kdist in demiss['emiss'].keys(): - # loop on spectra - for i0, (k0, v0) in enumerate(cases['case'].items()): + # loop on cases + for i0, (k0, v0) in enumerate(cases['case'].items()): - # slice - Te = Teu[np.argmin(np.abs(Teu - v0['jp_frac']))] - jpf = jp_fracu[np.argmin(np.abs(jp_fracu - v0['jp_frac']))] - ic = ( - (ddist['plasma']['jp_fraction_re']['data'] == jpf) - & (ddist['plasma']['Te_eV']['data'] == Te) - ) - assert ic.sum() == 1 + # slice + Te = Teu[np.argmin(np.abs(Teu - v0['Te']))] + jpf = jp_fracu[np.argmin(np.abs(jp_fracu - v0['jp_frac']))] + ic = ( + (ddist['plasma']['jp_fraction_re']['data'] == jpf) + & (ddist['plasma']['Te_eV']['data'] == Te) + ) + assert ic.sum() == 1 + ic = tuple([cc[0] for cc in ic.nonzero()]) - # -------- - # spectra + # -------- + # spectra - kax = 'spectra' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] + kax = 'spectra' + if dax.get(kax) is not None: + ax = dax[kax]['handle'] + + for i1, cc in enumerate(cases['theta_ph_vsB']['val']): + it = np.argmin(np.abs(demiss['theta_ph_vsB']['data'] - cc)) + sli = ic + (slice(None), it) - for i1, cc in enumerate(cases['theta_ph_vsB']['val']): - it = np.argmin(np.abs(demiss['theta_ph_vsB']['data'] - cc)) - sli = tuple([cc[0] for cc in ic.nonzero()]) + (slice(None), it) + for kdist in demiss['emiss'].keys(): emiss_E = demiss['emiss'][kdist]['emiss']['data'][sli] # plot @@ -1656,30 +1679,59 @@ def fig04_Bremsstrahlung( lw=1 if kdist == 'RE' else 2, marker='None', color=v0['color'], + label=f'{kdist}_{ic}_{cc*180/np.pi:3.0f}deg', ) - # -------- - # theta + # vlines + for i1, cc in enumerate(cases['E_ph_eV']['val']): + ax.axvline(cc*1e-3, c='k', ls='--', lw=1) - kax = 'theta' + # Elim + iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - Elim[ic])) + ax.plot( + np.r_[Elim[ic], Elim[ic]] * 1e-3, + [emiss_E[iE], 1e16], + color=v0['color'], + ls='--', + lw=1, + ) + + ax.set_ylim(1e0, 1e16) + ax.set_xlim(1e-2, 2e4) + + # -------- + # theta + + for i1, cc in enumerate(cases['E_ph_eV']['val']): + + kax = f'theta_{i1}' if dax.get(kax) is not None: ax = dax[kax]['handle'] - for i1, cc in enumerate(cases['E_ph_eV']['val']): - iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - cc)) - sli = sli[:-2] + (iE, slice(None)) + iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - cc)) + sli = ic + (iE, slice(None)) + + for kdist in demiss['emiss'].keys(): emiss_theta = demiss['emiss'][kdist]['emiss']['data'][sli] # plot ax.plot( demiss['theta_ph_vsB']['data']*180/np.pi, emiss_theta / emiss_theta.max(), - ls=cases['theta_ph_vsB']['ls'][i1], + ls=cases['E_ph_eV']['ls'][i1], lw=1 if kdist == 'RE' else 2, marker='None', color=v0['color'], + label=f'{kdist}_{ic}_{cc*1e-3:3.1f}keV', ) + # deco + ax.set_title( + r"$E_{ph,B}$" + f" = {cc*1e-3:3.1f} keV", + fontsize=fontsize, + fontweight='bold', + ) + # -------------- # plot - Elim # -------------- @@ -1693,16 +1745,39 @@ def fig04_Bremsstrahlung( ddist['plasma']['jp_fraction_re']['data'], Elim * 1e-3, cmap=plt.cm.viridis, - levels=np.r_[0.1, 0.2, 0.5, 1, 2, 5, 10, 20]*1e3, + levels=np.r_[1, 2, 5, 7.5, 10, 15], vmin=0.1, vmax=20, ) + # cases + for i0, (k0, v0) in enumerate(cases['case'].items()): + ax.plot( + v0['Te']*1e-3, + v0['jp_frac'], + marker='*', + markersize=8, + markerfacecolor=v0['color'], + color=v0['color'], + ) + ax.clabel(cs, cs.levels, fontsize=12) ax.set_xlim(0, ddist['plasma']['Te_eV']['data'].max()*1e-3) ax.set_ylim(0, 1) + # -------------- + # save + # -------------- + + if pfe_save is not False: + if pfe_save is None: + name = 'fig04_bemsstrahlung.png' + pfe_save = os.path.join(_PATH_HERE, name) + fig.savefig(pfe_save, format='png', dpi=300) + msg = f"Saved figure in:\n\t{pfe_save}\n" + print(msg) + return dax, demiss, ddist, d2cross_phi From 8236928d6a3018f3b45c335c38d6c03845e0f9b4 Mon Sep 17 00:00:00 2001 From: dvezinet Date: Fri, 12 Jun 2026 19:30:20 +0000 Subject: [PATCH 80/80] [#1166] Moved publications to dedicated git project --- .../_figures.py | 1901 ----------------- 1 file changed, 1901 deletions(-) delete mode 100644 publications/2026_runawayBremsstrahlungDetection/_figures.py diff --git a/publications/2026_runawayBremsstrahlungDetection/_figures.py b/publications/2026_runawayBremsstrahlungDetection/_figures.py deleted file mode 100644 index 53a723423..000000000 --- a/publications/2026_runawayBremsstrahlungDetection/_figures.py +++ /dev/null @@ -1,1901 +0,0 @@ - - -import os -import sys - - -import numpy as np -import scipy.integrate as scpinteg -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec -import matplotlib.patches as mpatches -import matplotlib.path as mpath -import astropy.units as asunits -import datastock as ds - - -_PATH_HERE = os.path.dirname(__file__) -_PATH_TF = os.path.dirname(os.path.dirname(_PATH_HERE)) -sys.path.insert(0, _PATH_TF) -import tofu as tf -sys.path.pop(0) - -tfphysdist = tf.physics_tools.electrons.distribution -tfphysemis = tf.physics_tools.electrons.emission - - -# ##################################################### -# ##################################################### -# DEFAULTS -# ##################################################### - - -_PATH_HERE = os.path.dirname(__file__) - - -_DPFE_DCROSS = { - 'EH0': os.path.join( - _PATH_HERE, - 'd2cross_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_ntheta61_EH.npz', - ), - 'EH1': os.path.join( - _PATH_HERE, - 'd2cross_Ee0100eV-10MeV-80log_Eph100eV-10MeV-81log_ntheta61_EH.npz', - ), - 'BHE': os.path.join( - _PATH_HERE, - 'd2cross_Ee01eV-100MeV-400log_Eph1eV-100MeV-401log_ntheta181_BHE.npz', - ), -} - - -# ##################################################### -# ##################################################### -# Fig 1 - cross-section -# ##################################################### - - -def fig01_cross_section( - figsize=(15, 7), - pfe_cross='EH0', - version='EH', - Eph_eV=np.r_[1e3, 10e3, 500e3], - Ee0_eV=np.r_[20e3, 1e6], - fontsize=14, - pfe_save=None, -): - - # -------------- - # load - # -------------- - - pfe = _DPFE_DCROSS[pfe_cross] - - dout = { - kk: vv.tolist() - for kk, vv in dict(np.load(pfe, allow_pickle=True)).items() - } - units = dout['cross'][version]['units'] - Z = dout.get('Z', {'data': 1})['data'] - - # -------------- - # prepare axes - # -------------- - - dmargin = { - 'left': 0.06, 'right': 0.99, - 'bottom': 0.08, 'top': 0.93, - 'wspace': 0.25, 'hspace': 0.10, - } - - fig = plt.figure(figsize=figsize) - - gs = gridspec.GridSpec(ncols=4, nrows=2, **dmargin) - dax = {} - - # -------------- - # prepare axes - # -------------- - - # -------------- - # ax - isolines - - ax = fig.add_subplot( - gs[:, -2:], - xscale='log', - yscale='log', - aspect='equal', - ) - ax.set_xlabel( - r"$E_{e,0}$ (keV)", - size=fontsize, - fontweight='bold', - ) - ax.set_ylabel( - r"$E_{ph}$ (keV)", - size=fontsize, - fontweight='bold', - ) - ax.set_title( - r"$d^2\sigma(E_{e0}, E_{ph}, \theta_{ph}, Z)$" - + f"\n Z = {Z} - version = {version}", - size=fontsize, - fontweight='bold', - ) - - # store - dax['map'] = {'handle': ax, 'type': 'isolines'} - - # -------------- - # ax - theta_norm - - # theta_norm0 - ax = fig.add_subplot( - gs[0, 0], - xscale='linear', - ) - ax.set_ylabel( - "normalized cross-section (adim.)", - size=fontsize, - fontweight='bold', - ) - ax.set_title( - r"$E_{e0}$" + f" = {Ee0_eV[0]*1e-3:2.0f} keV", - size=fontsize, - fontweight='bold', - ) - - # store - dax['theta_norm0'] = {'handle': ax, 'type': 'isolines'} - - # theta_norm1 - ax = fig.add_subplot( - gs[0, 1], - sharex=dax['theta_norm0']['handle'], - sharey=dax['theta_norm0']['handle'], - ) - ax.set_title( - r"$E_{e0}$" + f" = {Ee0_eV[1]*1e-6:2.0f} MeV", - size=fontsize, - fontweight='bold', - ) - - # store - dax['theta_norm1'] = {'handle': ax, 'type': 'isolines'} - - # -------------- - # ax - theta_abs - - # theta_abs0 - ax = fig.add_subplot( - gs[1, 0], - sharex=dax['theta_norm0']['handle'], - ) - ax.set_xlabel( - r"$\theta_{ph}$ (deg)", - size=fontsize, - fontweight='bold', - ) - ax.set_ylabel( - r"$d\sigma$" + f"({units})", - size=fontsize, - fontweight='bold', - ) - - # store - dax['theta_abs0'] = {'handle': ax, 'type': 'isolines'} - - # theta_abs1 - ax = fig.add_subplot( - gs[1, 1], - sharex=dax['theta_norm0']['handle'], - sharey=dax['theta_abs0']['handle'], - ) - ax.set_xlabel( - r"$\theta_{ph}$ (deg)", - size=fontsize, - fontweight='bold', - ) - - # store - dax['theta_abs1'] = {'handle': ax, 'type': 'isolines'} - - # ------------------ - # call built-in - # ------------------ - - # cases 100 keV - lc = ['r', 'g', 'b'] - for i0, e0 in enumerate(Ee0_eV): - iphok = Eph_eV < e0 - dcases = { - i1: { - 'E_e0_eV': e0, - 'E_ph_eV': eph, - 'color': lc[i1], - 'label': f"Eph = {eph*1e-3:3.0f} keV", - } - for i1, eph in enumerate(Eph_eV[iphok]) - } - _dax, _d2cross = tfphysemis.plot_xray_thin_d2cross_ei_anisotropy( - d2cross=pfe, - dcases=dcases, - dax={ - 'map': dax['map']['handle'], - 'theta_norm': dax[f'theta_norm{i0}']['handle'], - 'theta_abs': dax[f'theta_abs{i0}']['handle'], - }, - ) - - # remove contour plots - if i0 == 0: - for cc in dax['map']['handle'].get_children(): - if cc.__class__.__name__ == 'QuadContourSet': - cc.remove() - - # remove legend - dax['theta_norm0']['handle'].get_legend().remove() - - # --------------------- - # Adjust map x/y scales - # --------------------- - - dax['map']['handle'].set_xlim(0.1, 10e3) - dax['map']['handle'].set_ylim(0.1, 10e3) - dax['theta_abs1']['handle'].set_ylabel('') - dax['theta_norm1']['handle'].legend(loc='lower right') - - # -------------- - # add a, b, c, d, e - # -------------- - - dabc = { - 'theta_norm0': '(a)', - 'theta_norm1': '(c)', - 'theta_abs0': '(b)', - 'theta_abs1': '(d)', - } - for kax, abc in dabc.items(): - dax[kax]['handle'].grid(visible=True, which='major', axis='both') - dax[kax]['handle'].text( - 0.95, 0.95, - abc, - fontsize=fontsize, - fontweight='bold', - horizontalalignment='right', - verticalalignment='top', - transform=dax[kax]['handle'].transAxes, - ) - - dax['map']['handle'].text( - 0., 1.02, - "(e)", - fontsize=fontsize, - fontweight='bold', - horizontalalignment='left', - verticalalignment='bottom', - transform=dax['map']['handle'].transAxes, - ) - - # -------------- - # save - # -------------- - - if pfe_save is not False: - if pfe_save is None: - name = 'fig01_crosssection.png' - pfe_save = os.path.join(_PATH_HERE, name) - fig.savefig(pfe_save, format='png', dpi=300) - msg = f"Saved figure in:\n\t{pfe_save}\n" - print(msg) - - return dax - - -# ##################################################### -# ##################################################### -# Fig 02 - RE dist -# ##################################################### - - -_DDIST = { - # maxwell - 'Te_eV': np.r_[0.1e3, 0.1e3, 1e3, 1e3], - 'ne_m3': 1e19, - 'jp_Am2': 1e6, - # RE - 'jp_fraction_re': np.r_[0.1, 0.9, 0.1, 0.9], - 'dominant': 'bump', - 'Ekin_max_eV': 1e6, - 'Ekin_min_eV': 100, - 'step': 1, - 'pnormW': 5, - 'theta_width': 20*np.pi/180, - # coords - 'E_eV': np.logspace(0, 8, 80), - 'theta': np.linspace(0, 180, 181) * np.pi / 180, -} - - -def fig02_distributions( - # coords - E_eV=None, - theta=None, - # Maxwell - ne_m3=None, - jp_Am2=None, - # RE - dominant=None, - jp_fraction_re=None, - Ekin_max_eV=None, - Ekin_min_eV=None, - step=None, - pnormW=None, - # plot - figsize=(5, 7), - fontsize=12, - pfe_save=None, -): - - # ------------ - # inputs - # ------------ - - din = locals() - din = { - kk: vv if din.get(kk) is None else din[kk] - for kk, vv in _DDIST.items() - } - - # ------------ - # compute - # ------------ - - # dout = {'dist': dict, 'plasma': dist, 'coords': dist} - dout = tfphysdist.get_distribution(**din) - - # units - units2d = asunits.Unit(dout['dist']['RE']['dist']['units']) - units1d = units2d * asunits.Unit(dout['coords']['x1']['units']) - - # ------------ - # Derive 1d data - # ------------ - - dataRE = scpinteg.trapezoid( - dout['dist']['RE']['dist']['data'], - x=dout['coords']['x1']['data'], - axis=-1, - ) - dataMax = scpinteg.trapezoid( - dout['dist']['maxwell']['dist']['data'], - x=dout['coords']['x1']['data'], - axis=-1, - ) - - # ------------ - # Derive levels, vmin, vmax - # ------------ - - Ekin_max = dout['plasma']['Ekin_max_eV']['data'] - vminRE_2d = np.inf - vminRE_1d = np.inf - for ind in np.ndindex(dataRE.shape[:-1]): - indE = np.argmin(np.abs(dout['coords']['x0']['data'] - Ekin_max[ind])) - sli = ind + (indE, slice(None)) - vmaxRE_2d = np.nanmax(dout['dist']['RE']['dist']['data'][sli]) - vminRE_2d = min(vminRE_2d, vmaxRE_2d) - sli = ind + (indE,) - vmaxRE_1d = dataRE[sli] - vminRE_1d = min(vminRE_1d, vmaxRE_1d) - vmaxRE_2d = np.nanmax(dout['dist']['RE']['dist']['data']) - vmaxRE_1d = np.nanmax(dataRE) - vmaxMax_2d = np.nanmax(dout['dist']['maxwell']['dist']['data']) - vmaxMax_1d = np.nanmax(dataMax) - - # 1d - vmaxlog10_1d = np.log10(max(vmaxRE_1d, vmaxMax_1d)) - dlog10_1d = vmaxlog10_1d - np.log10(vminRE_1d) - vmaxlog10_1d = np.ceil(vmaxlog10_1d) - vminlog10_1d = np.floor(vmaxlog10_1d - 1 - 1.2*dlog10_1d) - vmax_1d = 10**vmaxlog10_1d - vmin_1d = 10**vminlog10_1d - - # 2d - vmaxlog10_2d = np.log10(max(vmaxRE_2d, vmaxMax_2d)) - dlog10_2d = vmaxlog10_2d - np.log10(vminRE_2d) - vmaxlog10_2d = np.ceil(vmaxlog10_2d) - vminlog10_2d = np.floor(vmaxlog10_2d - 1 - 1.2*dlog10_2d) - levels_2d = np.logspace(vminlog10_2d, vmaxlog10_2d - 1, 6) - - # -------------- - # labels - # -------------- - - dlabel = {} - for ind in np.ndindex(dout['dist']['RE']['dist']['data'].shape[:-2]): - Te = dout['plasma']['Te_eV']['data'][ind] * 1e-3 - jpf = dout['plasma']['jp_fraction_re']['data'][ind] - - dlabel[ind] = f"{jpf:2.1f} , {Te:2.1f} keV" - - # title - ne = np.unique(dout['plasma']['ne_m3']['data']) - assert ne.size == 1 - jp = np.unique(dout['plasma']['jp_Am2']['data']) - assert jp.size == 1 - tit = f"ne = {ne[0]:2.1e}, jp_tot = {jp[0]*1e-6:2.1f} MA/m2" - - # -------------- - # print - # -------------- - - _print(dout) - - # -------------- - # prepare axes - # -------------- - - dmargin = { - 'left': 0.12, 'right': 0.98, - 'bottom': 0.06, 'top': 0.97, - 'wspace': 0.25, 'hspace': 0.10, - } - - fig = plt.figure(figsize=figsize) - - gs = gridspec.GridSpec(ncols=1, nrows=2, **dmargin) - dax = {} - - # -------------- - # axes - 2d - # -------------- - - ax = fig.add_subplot(gs[0, 0], aspect='auto', xscale='log') - ax.set_ylabel( - r'$\theta_{e_0,B}$ (deg)', - fontsize=fontsize, - fontweight='bold', - ) - ax.set_title(tit, fontsize=fontsize, fontweight='bold') - ax.text( - 0.01, - 0.99, - '(a)', - horizontalalignment='left', - verticalalignment='top', - fontsize=fontsize, - fontweight='bold', - transform=ax.transAxes, - ) - - dax['2d'] = ax - - # -------------- - # axes - 1d - # -------------- - - ax = fig.add_subplot(gs[1, 0], aspect='auto', sharex=ax, yscale='log') - ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel( - f" ({units1d})", - fontsize=fontsize, - fontweight='bold', - ) - ax.text( - 0.01, - 0.99, - '(b)', - horizontalalignment='left', - verticalalignment='top', - fontsize=fontsize, - fontweight='bold', - transform=ax.transAxes, - ) - - dax['1d'] = ax - - dax = ds._generic_check._check_dax(dax) - - # ------------ - # plot 1d - # ------------ - - kax = '1d' - dcolor = {} - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - # for legend - ax.plot([], [], c='w', ls='-', lw=1, label='j_frac, Te') - - # loop plot - for ind in np.ndindex(dataRE.shape[:-1]): - sli = ind + (slice(None),) - - # Max - l0, = ax.plot( - dout['coords']['x0']['data']*1e-3, - dataMax[sli], - ls='-', - lw=1, - ) - dcolor[ind] = l0.get_color() - - # RE - ax.plot( - dout['coords']['x0']['data']*1e-3, - dataRE[sli], - ls='--', - lw=1, - color=dcolor[ind], - ) - - # Total - ax.plot( - dout['coords']['x0']['data']*1e-3, - dataMax[sli] + dataRE[sli], - ls='-', - lw=2, - color=dcolor[ind], - label=dlabel[ind], - ) - - # Add critical energy - Ec = tf.physics_tools.electrons.convert_momentum_velocity_energy( - momentum_normalized=dout['dist']['RE']['p_crit']['data'], - )['energy_kinetic_eV']['data'] - for ec in np.unique(Ec): - ax.axvline(ec*1e-3, c='k', lw=1, ls='--') - - # decorate - ax.legend() - ax.grid(True) - ax.set_ylim(vmin_1d, vmax_1d) - - # ------------ - # plot 2d - # ------------ - - kax = '2d' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - # loop plot - for ind in np.ndindex(dataRE.shape[:-1]): - sli = ind + (slice(None), slice(None)) - - # data - data = ( - dout['dist']['maxwell']['dist']['data'][sli] - + dout['dist']['RE']['dist']['data'][sli] - ) - - # contour - ax.contour( - dout['coords']['x0']['data']*1e-3, - dout['coords']['x1']['data']*180/np.pi, - data.T, - levels_2d, - colors=dcolor[ind], - ) - - # -------------- - # save - # -------------- - - if pfe_save is not False: - if pfe_save is None: - name = 'fig02_distributions.png' - pfe_save = os.path.join(_PATH_HERE, name) - fig.savefig(pfe_save, format='png', dpi=300) - msg = f"Saved figure in:\n\t{pfe_save}\n" - print(msg) - - return dax, dout - - -def _print(dout, sep=' '): - - # ----------- - # header - - head = [ - 'ind', - 'Te (keV)', - 'ne (1e20/m3)', 'Max / RE', - 'jp (MA/m2)', 'Max / RE', - ] - lmax = np.max([len(ss) for ss in head]) - - # ----------- - # header - - lc = [] - for ind in np.ndindex(dout['dist']['RE']['dist']['data'].shape[:-2]): - Te = dout['plasma']['Te_eV']['data'][ind]*1e-3 - ne = dout['plasma']['ne_m3']['data'][ind]*1e-20 - jp = dout['plasma']['jp_Am2']['data'][ind]*1e-6 - ne_max = dout['dist']['maxwell']['integ_ne']['data'][ind]*1e-20 - ne_RE = dout['dist']['RE']['integ_ne']['data'][ind]*1e-20 - jp_max = dout['dist']['maxwell']['integ_jp']['data'][ind]*1e-6 - jp_RE = dout['dist']['RE']['integ_jp']['data'][ind]*1e-6 - - cc = [ - str(ind), - f'{Te:2.1f}', - f'{ne:2.2f}', f"{ne_max:2.2f} / {ne_RE:2.2f}", - f'{jp:2.2f}', f"{jp_max:2.2f} / {jp_RE:2.2f}", - ] - lc.append(cc) - lmax = max(lmax, np.max([len(ss) for ss in cc])) - - # ---------------- - # concatenate - - line = sep.join(['-'*lmax for ss in head]) - head = sep.join([ss.ljust(lmax) for ss in head]) - lc = [ - sep.join([ss.ljust(lmax) for ss in cc]) - for cc in lc - ] - - msg = '\n'.join([head, line] + lc) - print(msg) - - return - - -# ##################################################### -# ##################################################### -# Fig 03 - cross-section -# ##################################################### - - -_DR = { - 'R0': 1.8, - 'rplasma': 0.60, - 'RVes': [1.2, 2.66], - 'Rcryo': 4.6, - 'PP_R': np.r_[2.50, 4.7], # 4.2 - 'PP_width': 0.47, - 'PP_phi': np.r_[-180, 0] * np.pi/180, -} - - -_DSENSORS = { - 'in': { - 'pp': 0, - 'R': 2.55, - 'cw': False, - 'rplasma_ratio': 0.7, - 'color': 'b', - 'marker': '.', - 'ms': 2, - 'alpha': 0.6, - }, - 'ex': { - 'pp': 1, - 'R': 6, - 'cw': False, - 'color': 'g', - 'width': 0.10, - 'dist': 4, - 'marker': '.', - 'ms': 2, - 'alpha': 0.6, - 'wall': True, - 'beamdump': True, - }, -} - - -def fig03_tokamak( - # tokamak - R0=None, - rplasma=None, - RVes=None, - Rcryo=None, - # port plug - PP_R=None, - PP_width=None, - PP_phi=None, - # sensors - in_pp=None, - in_R=None, - in_cw=None, - in_rplasma_ratio=None, - in_color=None, - in_marker=None, - in_ms=None, - # ex - res=None, - ex_pp=None, - ex_R=None, - ex_cw=None, - ex_width=None, - ex_dist=None, - ex_color=None, - ex_marker=None, - ex_ms=None, - # plot - figsize=(5, 7), - fontsize=12, - pfe_save=None, -): - - # -------------- - # Load SPARC - # -------------- - - config, dinput = _fig02_check(**locals()) - - phi = np.pi * np.linspace(-1, 1, 181) - cos = np.cos(phi) - sin = np.sin(phi) - - # -------------- - # prepare axes - # -------------- - - dmargin = { - 'left': 0.11, 'right': 0.97, - 'bottom': 0.06, 'top': 0.99, - 'wspace': 0.25, 'hspace': 0.20, - } - - fig = plt.figure(figsize=figsize) - - gs = gridspec.GridSpec(ncols=1, nrows=2, **dmargin) - dax = {} - - # -------------- - # axes - hor - # -------------- - - ax = fig.add_subplot(gs[0, 0], aspect='equal') - ax.set_xlabel('X (m)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel('Y (m)', fontsize=fontsize, fontweight='bold') - ax.text( - 0.01, - 0.99, - '(a)', - horizontalalignment='left', - verticalalignment='top', - fontsize=fontsize, - fontweight='bold', - transform=ax.transAxes, - ) - - dax['hor'] = ax - - # -------------- - # axes - ang vs rplasma - # -------------- - - ax = fig.add_subplot(gs[1, 0], aspect='auto') - ax.set_xlabel('r / a', fontsize=fontsize, fontweight='bold') - ax.set_ylabel( - r'$\theta_{ph,B}$ (deg)', - fontsize=fontsize, - fontweight='bold', - ) - ax.text( - 0.01, - 0.99, - '(b)', - horizontalalignment='left', - verticalalignment='top', - fontsize=fontsize, - fontweight='bold', - transform=ax.transAxes, - ) - - dax['theta_vs_B'] = ax - - dax = ds._generic_check._check_dax(dax) - - # -------------- - # plot hor - # -------------- - - kax = 'hor' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - # -------------- - # plot R - - lk = [kk for kk in dinput.keys() if kk[0] == 'R'] - for k0 in lk: - - if 'plasma' in k0: - inner = dinput[k0]['data'][0] * np.array([cos, sin]).T - outer = dinput[k0]['data'][1] * np.array([cos, sin]).T - vertices = np.concatenate((inner, outer[::-1]), axis=0) - - codes = np.ones( - len(inner), - dtype=mpath.Path.code_type, - ) * mpath.Path.LINETO - codes[0] = mpath.Path.MOVETO - all_codes = np.concatenate((codes, codes)) - - path = mpath.Path(vertices, all_codes) - patch = mpatches.PathPatch( - path, - facecolor='r', - alpha=0.1, - edgecolor='r', - ) - ax.add_patch(patch) - - else: - for v1 in dinput[k0]['data']: - ax.plot( - v1*cos, - v1*sin, - **dinput[k0]['prop'], - ) - - # -------------- - # add arrows - - R = dinput['R0']['data'][0] + 0.5 * dinput['rplasma']['data'][0] - phi = np.r_[100, 160] * np.pi / 180 - # dist = R * np.hypot( - # np.cos(phi[0]) - np.cos(phi[1]), - # np.sin(phi[0]) - np.sin(phi[1]), - # ) - # rad = (R * (1 - np.cos(np.abs(np.diff(phi)/2))) / dist)[0] - rad = -0.3 - ax.annotate( - "RE", - xy=(R*np.cos(phi[0]), R*np.sin(phi[0])), - xycoords='data', - xytext=(R*np.cos(phi[1]), R*np.sin(phi[1])), - textcoords='data', - color='r', - fontweight='bold', - fontsize=fontsize, - arrowprops=dict( - arrowstyle="->", - lw=1.5, - color='r', - shrinkA=5, shrinkB=5, - patchA=None, patchB=None, - connectionstyle=f'arc3,rad={rad}', - ), - ) - - # -------------- - # plot port plug - - for ii, phi in enumerate(dinput['PP_phi']['data']): - - # edges - width = dinput['PP_width']['data'][0] - ppR0 = dinput['PP_R']['data'][0] - ppR1 = dinput['PP_R']['data'][1] - length = ppR1 - ppR0 - cent = 0.5 * (ppR0+ppR1) * np.r_[np.cos(phi), np.sin(phi)] - xy = ( - cent[0] - 0.5 * length, - cent[1] - 0.5 * width, - ) - - # patch - patch = mpatches.Rectangle( - xy, - length, - width, - angle=phi*180/np.pi, - rotation_point='center', - facecolor='w', - alpha=1., - edgecolor='None', - zorder=10, - ) - ax.add_patch(patch) - - # central line - ppR0 = dinput['PP_R']['data'][0] - ppR1 = dinput['PP_R']['data'][1] - - centx = np.r_[ppR0, ppR1] * np.cos(phi) - centy = np.r_[ppR0, ppR1] * np.sin(phi) - - ax.plot( - centx, - centy, - c='k', - lw=1, - ls='--', - alpha=0.3, - zorder=15, - ) - - # edges - ephi = np.r_[-np.sin(phi), np.cos(phi)] - edgex = ( - centx[None, :] - + 0.5 * width * ephi[0] * np.r_[1, np.nan, -1][:, None] - ).ravel() - edgey = ( - centy[None, :] - + 0.5 * width * ephi[1] * np.r_[1, np.nan, -1][:, None] - ).ravel() - - ax.plot( - edgex, - edgey, - c='k', - lw=1, - ls='-', - zorder=20, - ) - - # -------------- - # add sensors - - dsensors = _sensors(**locals()) - - for k0, v0 in dsensors.items(): - - # FOV - patch = mpatches.PathPatch( - v0['path'], - facecolor=v0['color'], - alpha=v0['alpha'], - zorder=25, - edgecolor=v0['color'], - ) - ax.add_patch(patch) - - # sensor - ax.plot( - [v0['cent'][0]], - [v0['cent'][1]], - c=v0['color'], - ls='None', - lw=2, - marker=v0['marker'], - zorder=30, - label=v0.get('label', k0), - ) - - # FOV sampling - ax.plot( - v0['ptsx'], - v0['ptsy'], - c=v0['color'], - marker=v0.get('marker', '.'), - ls='None', - zorder=30, - ms=v0.get('ms', 4), - ) - - # ---------------- - # plot theta_vs_B - # ---------------- - - kax = 'theta_vs_B' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - for k0, v0 in dsensors.items(): - - ax.plot( - v0['rplasma_norm'], - v0['theta_vs_B'] * 180 / np.pi, - marker=v0.get('marker', '.'), - ms=v0.get('ms', 6), - color=v0['color'], - ls=v0.get('ls', 'None'), - label=v0.get('label', k0), - ) - - ax.axhline(90, c='k', ls='--', lw=1) - ax.set_xlim(-1, 1) - ax.set_ylim(0, 180) - ax.grid(True) - - # comments - ax.text( - -0.97, - 55, - 'forward', - horizontalalignment='left', - verticalalignment='top', - rotation=90, - fontsize=fontsize, - fontweight='bold', - transform=ax.transData, - ) - ax.text( - -0.97, - 110, - 'backward', - horizontalalignment='left', - verticalalignment='bottom', - rotation=90, - fontsize=fontsize, - fontweight='bold', - transform=ax.transData, - ) - - # -------------- - # save - # -------------- - - if pfe_save is not False: - if pfe_save is None: - name = 'fig03_tokamak.png' - pfe_save = os.path.join(_PATH_HERE, name) - fig.savefig(pfe_save, format='png', dpi=300) - msg = f"Saved figure in:\n\t{pfe_save}\n" - print(msg) - - return dax - - -def _fig02_check( - config=None, - # tokamak - R0=None, - rplasma=None, - RVes=None, - Rcryo=None, - PP_R=None, - PP_width=None, - PP_phi=None, - # unused - **kwdargs, -): - - # ------------------ - # config - # ------------------ - - if config is None: - config = 'SPARC' - - if isinstance(config, str): - config = tf.load_config(config) - - # ------------------ - # Geometry - R - # ------------------ - - lk = ['R0', 'rplasma', 'RVes', 'Rcryo', 'PP_R', 'PP_width', 'PP_phi'] - dinput = { - kk: { - 'data': None, - 'prop': {'color': 'k', 'ls': '-', 'lw': 1, 'label': None} - } - for kk in lk - } - - # R0 - dinput['R0']['data'] = np.r_[float(ds._generic_check._check_var( - R0, 'R0', - types=(float, int), - sign='>0', - default=_DR['R0'], - ))] - dinput['R0']['prop']['ls'] = '--' - - # rplasma - dinput['rplasma']['data'] = np.r_[float(ds._generic_check._check_var( - rplasma, 'rplasma', - types=(float, int), - sign='>0', - default=_DR['rplasma'], - ))] - assert dinput['rplasma']['data'] < dinput['R0']['data'] - - dinput['Rplasma'] = { - 'data': dinput['R0']['data'] + dinput['rplasma']['data']*np.r_[-1, 1], - 'prop': { - 'color': 'r', - 'lw': 1, - 'ls': '-', - 'label': 'plasma', - } - } - - # RVes - if RVes is None: - RVes = _DR['RVes'] - dinput['RVes']['data'] = ds._generic_check._check_flat1darray( - RVes, 'RVes', - dtype=float, - sign='>0', - unique=True, - size=2, - ) - Rlim = dinput['R0']['data'] - dinput['rplasma']['data'] - assert dinput['RVes']['data'][0] < Rlim - Rlim = dinput['R0']['data'] + dinput['rplasma']['data'] - assert dinput['RVes']['data'][1] > Rlim - dinput['RVes']['prop']['lw'] = 2 - - # rplasma - dinput['Rcryo']['data'] = np.r_[float(ds._generic_check._check_var( - Rcryo, 'Rcryo', - types=(float, int), - sign='>0', - default=_DR['Rcryo'], - ))] - assert dinput['Rcryo']['data'] > dinput['RVes']['data'][1] - dinput['Rcryo']['prop']['lw'] = 2 - - # ------------------ - # Geometry - Port plug - # ------------------ - - # PP_R - if PP_R is None: - PP_R = _DR['PP_R'] - dinput['PP_R']['data'] = ds._generic_check._check_flat1darray( - PP_R, 'PP_R', - dtype=float, - sign='>0', - unique=True, - size=2, - ) - Rin = dinput['R0']['data'] + dinput['rplasma']['data'] - assert dinput['PP_R']['data'][0] > Rin - assert dinput['PP_R']['data'][1] > dinput['Rcryo']['data'] - - # PP_width - dinput['PP_width']['data'] = np.r_[float(ds._generic_check._check_var( - PP_width, 'PP_width', - types=(float, int), - sign='>0', - default=_DR['PP_width'], - ))] - - # PP_phi - if PP_phi is None: - PP_phi = _DR['PP_phi'] - PP_phi = ds._generic_check._check_flat1darray( - PP_phi, 'PP_phi', - dtype=float, - unique=True, - size=2, - ) - dinput['PP_phi']['data'] = np.arctan2(np.sin(PP_phi), np.cos(PP_phi)) - - return config, dinput - - -def _sensors( - res=None, - # sensors - in - in_pp=None, - in_R=None, - in_cw=None, - in_rplasma_ratio=None, - in_color=None, - in_marker=None, - in_ms=None, - # sensors - ex - ex_pp=None, - ex_R=None, - ex_cw=None, - ex_width=None, - ex_dist=None, - ex_color=None, - ex_marker=None, - ex_ms=None, - # dinput - dinput=None, - # unused - **kwdargs, -): - - # -------------- - # inputs - # -------------- - - res = ds._generic_check._check_var( - res, 'res', - types=float, - sign='>0', - default=0.01, - ) - - # -------------- - # initialize - # -------------- - - dsensors = { - 'in': { - kk.replace('in_', ''): vv for kk, vv in locals().items() - if kk.startswith('in_') - }, - 'ex': { - kk.replace('ex_', ''): vv for kk, vv in locals().items() - if kk.startswith('ex_') - }, - } - - # -------------- - # check - # -------------- - - for k0, v0 in dsensors.items(): - for k1, v1 in v0.items(): - if v1 is None: - dsensors[k0][k1] = _DSENSORS[k0][k1] - for k1, v1 in _DSENSORS[k0].items(): - if dsensors[k0].get(k1) is None: - dsensors[k0][k1] = v1 - - # -------------- - # Derive - # -------------- - - for k0, v0 in dsensors.items(): - - phi = dinput['PP_phi']['data'][v0['pp']] - eR = np.r_[np.cos(phi), np.sin(phi)] - ephi = np.r_[-np.sin(phi), np.cos(phi)] - sign = v0["cw"] * 2 - 1 - width = dinput['PP_width']['data'] - - # ---------- - # cent - - if k0 == 'in': - cent = v0['R'] * eR + sign * 0.5 * width * ephi - else: - length = dinput['PP_R']['data'][1] - dinput['PP_R']['data'][0] - dphi = np.arctan2(width - v0['width'], length) - eRs = eR * np.cos(dphi) + sign * ephi * np.sin(dphi) - ppc = np.mean(dinput['PP_R']['data']) * eR - cent = ppc + v0["dist"] * eRs - - # store - dsensors[k0]["dphi"] = dphi - dsensors[k0]["ppc"] = ppc - dsensors[k0]["eRs"] = eRs - - dsensors[k0]['cent'] = cent - - # ---------- - # FOV - - if k0 == 'in': - - R0 = dinput['R0']['data'][0] - rplasma = dinput['rplasma']['data'][0] - - # out - R = R0 + rplasma * v0["rplasma_ratio"] - vect_out = _tangent(cent, R, sign) - - # in - R = R0 - rplasma * v0["rplasma_ratio"] - vect_in = _tangent(cent, R, sign) - - else: - ephis = np.r_[-eRs[1], eRs[0]] - vect_out = ( - (length + v0["dist"]) * (-eRs) + 0.5 * v0['width'] * ephis - ) - vect_in = ( - (length + v0["dist"]) * (-eRs) - 0.5 * v0['width'] * ephis - ) - vect_out = vect_out / np.linalg.norm(vect_out) - vect_in = vect_in / np.linalg.norm(vect_in) - - # Get FOV from cent + 2 vect - xx, yy = _FOV(cent, vect_out, vect_in, R0, rplasma) - path = mpath.Path(np.array([xx, yy]).T) - - # Sample FOV - DX = np.max(xx) - np.min(xx) - DY = np.max(yy) - np.min(yy) - nptsx = int(DX / res) - nptsy = int(DY / res) - ptsx = np.linspace(np.min(xx), np.max(xx), nptsx) - ptsy = np.linspace(np.min(yy), np.max(yy), nptsy) - ptsx = np.repeat(ptsx[:, None], nptsy, axis=1).ravel() - ptsy = np.repeat(ptsy[None, :], nptsx, axis=0).ravel() - iok = ( - path.contains_points(np.array([ptsx, ptsy]).T) - & (np.hypot(ptsx, ptsy) >= R0 - rplasma) - & (np.hypot(ptsx, ptsy) <= R0 + rplasma) - ) - ptsx = ptsx[iok] - ptsy = ptsy[iok] - - # Angle - pts_phi = np.arctan2(ptsy, ptsx) - pts_ephi0 = -np.sin(pts_phi) - pts_ephi1 = np.cos(pts_phi) - vect0 = ptsx - cent[0] - vect1 = ptsy - cent[1] - vectn = np.sqrt(vect0**2 + vect1**2) - vect0 = vect0 / vectn - vect1 = vect1 / vectn - theta_vs_B = np.arccos(vect0 * pts_ephi0 + vect1 * pts_ephi1) - - # store - dsensors[k0]["ptsx"] = ptsx - dsensors[k0]["ptsy"] = ptsy - dsensors[k0]["rplasma_norm"] = ( - (np.hypot(ptsx, ptsy) - R0) / dinput['rplasma']['data'] - ) - dsensors[k0]["theta_vs_B"] = theta_vs_B - dsensors[k0]["path"] = path - - return dsensors - - -def _tangent( - cent=None, - R=None, - sign=None, -): - - # --------- - # - - phi = np.arctan2(cent[1], cent[0]) - eR = np.r_[np.cos(phi), np.sin(phi)] - ephi = np.r_[-np.sin(phi), np.cos(phi)] - - ang = np.arcsin(R / np.hypot(*cent)) - vect = (-eR) * np.cos(ang) - sign * ephi * np.sin(ang) - vect = vect / np.linalg.norm(vect) - - return vect - - -def _FOV(cent, vect_out, vect_in, R0, rplasma): - - # ------------------ - # intersect vect_out - # ------------------ - - kk_out_out, isout_out_out = _intersect(cent, vect_out, R0 + rplasma) - kk_out_in, isout_out_in = _intersect(cent, vect_out, R0 - rplasma) - - kk_out = np.r_[kk_out_out, kk_out_in] - iok_out = np.r_[isout_out_out, ~isout_out_in] - iok = np.isfinite(kk_out) & iok_out - assert iok.sum() >= 1 - kout = np.min(kk_out[iok]) - pt_out = cent + kout * vect_out - - # ------------------ - # intersect vect_in - # ------------------ - - kk_in_out, isout_in_out = _intersect(cent, vect_in, R0 + rplasma) - kk_in_in, isout_in_in = _intersect(cent, vect_in, R0 - rplasma) - - kk_in = np.r_[kk_in_out, kk_in_in] - iok_in = np.r_[isout_in_out, ~isout_in_in] - iok = np.isfinite(kk_in) & iok_in - assert iok.sum() >= 1 - kin = np.min(kk_in[iok]) - pt_in = cent + kin * vect_in - - assert np.allclose(np.linalg.norm(pt_out), np.linalg.norm(pt_in)) - Rpts = np.linalg.norm(pt_out) - - # ------------------ - # polyx, polyy - # ------------------ - - # ang - ang_out = np.arctan2(pt_out[1], pt_out[0]) - ang_in = np.arctan2(pt_in[1], pt_in[0]) - ang_min = min(ang_out, ang_in) - ang_max = max(ang_out, ang_in) - if np.abs(ang_min - ang_max) > np.pi: - ang_min, ang_max = ang_max, ang_min + 2*np.pi - ang = np.linspace(ang_min, ang_max, 31) - - polyx = np.r_[cent[0], Rpts * np.cos(ang), cent[0]] - polyy = np.r_[cent[1], Rpts * np.sin(ang), cent[1]] - - return polyx, polyy - - -def _intersect(cent, vect, R): - - # ---------- - # kk - - # AM = ku - # R^2 = (OA + AM)^2 = RA^2 + k^2 + 2 ku OA - a = 1 - b = 2 * np.sum(vect * cent) - c = np.sum(cent**2) - R**2 - delta = b**2 - 4 * a * c - - kk = np.full((2,), np.nan) - if delta == 0: - kk[0] = -b / (2*a) - elif delta > 0: - kk = (-b + np.r_[1, -1] * np.sqrt(delta)) / (2 * a) - - # ---------- - # isout - - xx = cent[0] + kk * vect[0] - yy = cent[1] + kk * vect[1] - phi = np.arctan2(yy, xx) - isout = (vect[0] * np.cos(phi) + vect[1] * np.sin(phi)) > 0. - - assert isout.sum() <= 1 - - return kk, isout - - -# ##################################################### -# ##################################################### -# Fig 04 - Bremsstrahlung -# ##################################################### - - -_PFE_D2CROSS_PHI = os.path.join( - _PATH_HERE, - 'd2cross_phi_Ee01eV-100MeV-80log_Eph1eV-100MeV-81log_nthetaph61_nthetae060_EH.npz', -) - - -_CASES = { - 'case': { - '0': {'Te': 0.1e3, 'jp_frac': 0.9, 'color': 'r'}, - '1': {'Te': 2.5e3, 'jp_frac': 0.1, 'color': 'b'}, - }, - 'theta_ph_vsB': { - 'val': np.r_[0, 0.5, 1]*np.pi, - 'ls': ['-', '--', ':'], - }, - 'E_ph_eV': { - 'val': np.r_[0.1, 5, 20]*1e3, - 'ls': ['-', '-', '-'], - }, -} - - -def fig04_Bremsstrahlung( - d2cross_phi=None, - # cases - cases=None, - # plot - figsize=(15, 7), - fontsize=14, - # save - pfe_save=None, -): - - # ------------ - # d2cross_phi - # ------------ - - if d2cross_phi is None: - d2cross_phi = _PFE_D2CROSS_PHI - - # ------------ - # ddist - # ------------ - - ddist = locals() - ddist = { - kk: vv if ddist.get(kk) is None else ddist[kk] - for kk, vv in _DDIST.items() - if kk not in ['E_eV', 'theta'] - } - ddist['Te_eV'] = 1e3 * np.linspace(0.1, 2.5, 25)[:, None] - ddist['jp_fraction_re'] = np.linspace(0., 1., 21)[None, :] - - # -------------- - # integrated cross-section - # -------------- - - demiss, ddist, d2cross_phi = tfphysemis.get_xray_thin_integ_dist( - # ---------------- - # cross-section - # tabulated d2cross_phi - d2cross_phi=d2cross_phi, - # d2cross_phi computation - E_ph_eV=None, - E_e0_eV=None, - E_e0_eV_npts=None, - theta_e0_vsB_npts=None, - phi_e0_vsB_npts=None, - theta_ph_vsB=None, - # inputs - Z=None, - # hypergeometric parameter - ninf=None, - source=None, - # integration parameters - nthetae=None, - ndphi=None, - # output customization - version_cross=None, - # save / load - save_d2cross_phi=False, - # --------------------- - # optional responsivity - dresponsivity=None, - plot_responsivity_integration=None, - # ----------- - # verb - debug=False, - verb=True, - # ---------------- - # electron distribution - **ddist, - ) - - units = demiss['emiss']['RE']['emiss']['units'] - ne = np.unique(ddist['plasma']['ne_m3']['data'])[0] - - # -------------- - # cases - # -------------- - - if cases is None: - cases = _CASES - - # -------------- - # Elim - # -------------- - - shape = ddist['plasma']['Te_eV']['data'].shape - Elim = np.full(shape, np.nan) - theta_Elim = 0 - for ii, ind in enumerate(np.ndindex(shape)): - - sli_emiss = ind + (slice(None), 0) - emiss_RE = demiss['emiss']['RE']['emiss']['data'][sli_emiss] - emiss_max = demiss['emiss']['maxwell']['emiss']['data'][sli_emiss] - - ilim = (emiss_RE > emiss_max) - if np.any(ilim): - Elim[ind] = np.min(demiss['E_ph_eV']['data'][ilim]) - - # -------------- - # prepare axes - # -------------- - - dmargin = { - 'left': 0.06, 'right': 0.98, - 'bottom': 0.06, 'top': 0.95, - 'wspace': 0.25, 'hspace': 0.30, - } - - fig = plt.figure(figsize=figsize) - - nE = len(cases['E_ph_eV']['val']) - gs = gridspec.GridSpec(ncols=nE + 2, nrows=2, **dmargin) - dax = {} - - # ---------------- - # ax - spectra - # ---------------- - - ax = fig.add_subplot(gs[0, :nE], aspect='auto') - ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel( - r"$\epsilon$" + f' ({units})', - fontsize=fontsize, - fontweight='bold', - ) - ax.set_title( - r"$n_e$" + f" = {ne:1.0e} /m3", - fontsize=fontsize, - fontweight='bold', - ) - - dax['spectra'] = ax - - # ---------------- - # ax - theta - # ---------------- - - ax0 = None - for ii in range(nE): - ax = fig.add_subplot( - gs[1, ii], - aspect='auto', - sharex=ax0, - sharey=ax0, - ) - ax.set_xlabel( - r'$\theta_{ph,B}$' + ' (deg)', - fontsize=fontsize, - fontweight='bold', - ) - if ii == 0: - ax.set_ylabel('emiss (norm.)', fontsize=fontsize, fontweight='bold') - ax.set_xlim(0, 180) - ax.set_ylim(0, 1) - ax0 = ax - - dax[f'theta_{ii}'] = ax - - # ---------------- - # ax - Elim - # ---------------- - - ax = fig.add_subplot(gs[:, nE:], aspect='auto') - ax.set_xlabel('Te (keV)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel('jp_frac', fontsize=fontsize, fontweight='bold') - - dax['Elim'] = ax - - dax = ds._generic_check._check_dax(dax) - - # -------------- - # plot - cases - # -------------- - - Teu = np.unique(ddist['plasma']['Te_eV']['data']) - jp_fracu = np.unique(ddist['plasma']['jp_fraction_re']['data']) - - # loop on cases - for i0, (k0, v0) in enumerate(cases['case'].items()): - - # slice - Te = Teu[np.argmin(np.abs(Teu - v0['Te']))] - jpf = jp_fracu[np.argmin(np.abs(jp_fracu - v0['jp_frac']))] - ic = ( - (ddist['plasma']['jp_fraction_re']['data'] == jpf) - & (ddist['plasma']['Te_eV']['data'] == Te) - ) - assert ic.sum() == 1 - ic = tuple([cc[0] for cc in ic.nonzero()]) - - # -------- - # spectra - - kax = 'spectra' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - for i1, cc in enumerate(cases['theta_ph_vsB']['val']): - it = np.argmin(np.abs(demiss['theta_ph_vsB']['data'] - cc)) - sli = ic + (slice(None), it) - - for kdist in demiss['emiss'].keys(): - emiss_E = demiss['emiss'][kdist]['emiss']['data'][sli] - - # plot - ax.loglog( - demiss['E_ph_eV']['data']*1e-3, - emiss_E, - ls=cases['theta_ph_vsB']['ls'][i1], - lw=1 if kdist == 'RE' else 2, - marker='None', - color=v0['color'], - label=f'{kdist}_{ic}_{cc*180/np.pi:3.0f}deg', - ) - - # vlines - for i1, cc in enumerate(cases['E_ph_eV']['val']): - ax.axvline(cc*1e-3, c='k', ls='--', lw=1) - - # Elim - iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - Elim[ic])) - ax.plot( - np.r_[Elim[ic], Elim[ic]] * 1e-3, - [emiss_E[iE], 1e16], - color=v0['color'], - ls='--', - lw=1, - ) - - ax.set_ylim(1e0, 1e16) - ax.set_xlim(1e-2, 2e4) - - # -------- - # theta - - for i1, cc in enumerate(cases['E_ph_eV']['val']): - - kax = f'theta_{i1}' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - iE = np.argmin(np.abs(demiss['E_ph_eV']['data'] - cc)) - sli = ic + (iE, slice(None)) - - for kdist in demiss['emiss'].keys(): - emiss_theta = demiss['emiss'][kdist]['emiss']['data'][sli] - - # plot - ax.plot( - demiss['theta_ph_vsB']['data']*180/np.pi, - emiss_theta / emiss_theta.max(), - ls=cases['E_ph_eV']['ls'][i1], - lw=1 if kdist == 'RE' else 2, - marker='None', - color=v0['color'], - label=f'{kdist}_{ic}_{cc*1e-3:3.1f}keV', - ) - - # deco - ax.set_title( - r"$E_{ph,B}$" + f" = {cc*1e-3:3.1f} keV", - fontsize=fontsize, - fontweight='bold', - ) - - # -------------- - # plot - Elim - # -------------- - - kax = 'Elim' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - cs = ax.contour( - ddist['plasma']['Te_eV']['data'] * 1e-3, - ddist['plasma']['jp_fraction_re']['data'], - Elim * 1e-3, - cmap=plt.cm.viridis, - levels=np.r_[1, 2, 5, 7.5, 10, 15], - vmin=0.1, - vmax=20, - ) - - # cases - for i0, (k0, v0) in enumerate(cases['case'].items()): - ax.plot( - v0['Te']*1e-3, - v0['jp_frac'], - marker='*', - markersize=8, - markerfacecolor=v0['color'], - color=v0['color'], - ) - - ax.clabel(cs, cs.levels, fontsize=12) - - ax.set_xlim(0, ddist['plasma']['Te_eV']['data'].max()*1e-3) - ax.set_ylim(0, 1) - - # -------------- - # save - # -------------- - - if pfe_save is not False: - if pfe_save is None: - name = 'fig04_bemsstrahlung.png' - pfe_save = os.path.join(_PATH_HERE, name) - fig.savefig(pfe_save, format='png', dpi=300) - msg = f"Saved figure in:\n\t{pfe_save}\n" - print(msg) - - return dax, demiss, ddist, d2cross_phi - - -# ##################################################### -# ##################################################### -# Fig 05 - responsivities -# ##################################################### - - -_PFE_RESPONSIVITIES = os.path.join(_PATH_HERE, 'responsivities.npz') - - -def fig05_responsivities( - pfe=None, - lw=2, - figsize=(7, 4), - fontsize=14, - pfe_save=None, -): - - # -------------- - # load - # -------------- - - if pfe is None: - pfe = _PFE_RESPONSIVITIES - - dresp = { - k0: v0.tolist() - for k0, v0 in np.load(pfe, allow_pickle=True).items() - } - - # -------------- - # prepare axes - # -------------- - - dmargin = { - 'left': 0.11, 'right': 0.97, - 'bottom': 0.12, 'top': 0.98, - 'wspace': 0.25, 'hspace': 0.20, - } - - fig = plt.figure(figsize=figsize) - - gs = gridspec.GridSpec(ncols=1, nrows=1, **dmargin) - dax = {} - - # -------------- - # axes - resp - # -------------- - - ax = fig.add_subplot(gs[0, 0], aspect='auto') - ax.set_xlabel('E (keV)', fontsize=fontsize, fontweight='bold') - ax.set_ylabel('responsivity', fontsize=fontsize, fontweight='bold') - - dax['resp'] = ax - - dax = ds._generic_check._check_dax(dax) - - # -------------- - # plot - resp - # -------------- - - kax = 'resp' - if dax.get(kax) is not None: - ax = dax[kax]['handle'] - - # --------------- - # loop on sensors - - dme = {'mesxr': False, 'mehxr': False, 'cvd': False} - for k0, v0 in dresp.items(): - - # resp, color, lab - lk = [kk for kk in dme.keys() if kk in k0] - if len(lk) == 1: - kk = lk[0] - if dme[kk] is False: - dme[kk] = v0.get('color', 'k') - lab = f"{kk} - {v0['responsivity']['units']}" - else: - v0['color'] = dme[kk] - v0['ls'] = '--' - lab = None - else: - lab = f"{k0} - {v0['responsivity']['units']}" - - # lw - if lw is None: - lwi = v0.get('lw', 1) - else: - lwi = lw - - # plot - ax.loglog( - v0['E_eV']['data']*1e-3, - v0['responsivity']['data'], - ls=v0.get('ls', '-'), - lw=lwi, - c=v0.get('color', 'k'), - marker=v0.get('marker', 'None'), - label=lab, - ) - - ax.set_ylim(1e-4, 2) - ax.grid(True) - ax.legend() - - # -------------- - # save - # -------------- - - if pfe_save is not False: - if pfe_save is None: - name = 'fig05_responsivities.png' - pfe_save = os.path.join(_PATH_HERE, name) - fig.savefig(pfe_save, format='png', dpi=300) - msg = f"Saved figure in:\n\t{pfe_save}\n" - print(msg) - - return dax, dresp