diff --git a/.coveragerc b/.coveragerc index a67a87f0..a27c0920 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] plugins = Cython.Coverage source = cherab -omit = *tests* +omit = */tests/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1350bf7..b32bde9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - numpy-version: ["oldest-supported-numpy", "numpy"] + numpy-version: ["oldest-supported-numpy", "'numpy<2'"] python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - name: Checkout code @@ -23,9 +23,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies - run: python -m pip install --prefer-binary cython>=0.28 ${{ matrix.numpy-version }} scipy matplotlib "pyopencl[pocl]>=2022.2.4" + run: python -m pip install --prefer-binary cython~=3.0 ${{ matrix.numpy-version }} scipy matplotlib "pyopencl[pocl]>=2022.2.4" - name: Install Raysect from pypi - run: pip install raysect==0.7.1 + run: pip install raysect==0.8.1.* - name: Build cherab run: dev/build.sh - name: Run tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae0a7d3..5fe4cc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,41 @@ Project Changelog ================= +Release 1.5.0 (27 Aug 2024) +------------------- + +API changes: +* The line shape models are moved to a dedicated submodule. The user code should not be affected though. (#396) +* The line shape models now have AtomicData as a required parameter. +* The method show_supported_transitions() of StarkBroadenedLine and ParametrisedZeemanTriplet is removed. +* The argument stark_model_coefficients of StarkBroadenedLine is now a tuple instead of a dict. +* The argument line_parameters of ParametrisedZeemanTriplet is now a tuple instead of a dict. + +New: +* Support Raysect 0.8 +* Cython version 3 is now required to build the package. +* Add custom line shape support to BeamCXLine model. (#394) +* Add PeriodicTransformXD and VectorPeriodicTransformXD functions to support the data simulated with periodic boundary conditions. (#387) +* Add CylindricalTransform and VectorCylindricalTransform to transform functions from cylindrical to Cartesian coordinates. (#387) +* Add numerical integration of Bremsstrahlung spectrum over a spectral bin. (#395) +* Replace the coarse numerical constant in the Bremsstrahlung model with an exact expression. (#409) +* Add the kind attribute to RayTransferPipelineXD that determines whether the ray transfer matrix is multiplied by sensitivity ('power') or not ('radiance'). (#412) +* Improved parsing of metadata from the ADAS ADF15 'bnd' files for H-like ions. Raises a runtime error if the metadata cannot be parsed. (#424) +* **Beam dispersion calculation has changed from sigma(z) = sigma + z * tan(alpha) to sigma(z) = sqrt(sigma^2 + (z * tan(alpha))^2) for consistancy with the Gaussian beam model. Attention!!! The results of BES and CX spectroscopy are affected by this change. (#414)** +* Improved beam direction calculation to allow for natural broadening of the BES line shape due to beam divergence. (#414) +* Add kwargs to invert_regularised_nnls to pass them to scipy.optimize.nnls. (#438) +* StarkBroadenedLine now supports Doppler broadening and Zeeman splitting. (#393) +* Add the power radiated in spectral lines due to charge exchange with thermal neutral hydrogen to the TotalRadiatedPower model. (#370) +* Add thermal charge-exchange emission model. (#57) +* PECs for C VI spectral lines for n <= 5 are now included in populate(). Rerun populate() after upgrading to 1.5 to update the atomic data repository. +* All interpolated atomic rates now return 0 if plasma parameters <= 0, which matches the behaviour of emission models. (#450) + +Bug fixes: +* Fix deprecated transforms being cached in LaserMaterial after laser.transform update (#420) +* Fix IRVB calculate sensitivity method. +* Fix missing donor_metastable attribute in the core BeamCXPEC class (#411). +* **Fix the receiver ion density being passed to the BeamCXPEC instead of the total ion density in the BeamCXLine. Also fix incorrect BeamCXPEC dosctrings. Attention!!! The results of CX spectroscopy are affected by this change. (#441)** + Release 1.4.0 (3 Feb 2023) ------------------- @@ -9,7 +44,6 @@ API changes: * Support for Python 3.6 is dropped. It may still work, but is no longer actively tested. Bug fixes: -* Fixed generomak plasma edge data paths. * Fix and improve OpenCL utility functions. (#358) * Fixed Bremsstrahlung trapezium evaluation (#384). diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2f977070..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include README.md CHANGELOG.md LICENSE.txt CITE.md AUTHORS.md MANIFEST.in setup.py requirements.txt .gitignore -include cherab/core/VERSION -recursive-include cherab *.py *.pyx *.pxd *.json *.cl *.npy *.obj -prune demos* - - diff --git a/README.md b/README.md index 963b0834..177feb9b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ with a build-time dependency on Cherab need to use a Cython version newer than 3.0a5, due to a [bug](https://github.com/cython/cython/issues/2918) in how earlier versions of Cython handle namespaces. +By default, pip will install from wheel archives on PyPI. If a binary wheel is not +available for your version of Python, or if you are installing in editable mode +for development, the package will be compiled locally on your machine. Compilation +is done in parallel by default, using all available processors, but can be +overridden by setting the environment variable `CHERAB_NCPU` to the number of +processors to use. + Governance ---------- diff --git a/cherab/core/VERSION b/cherab/core/VERSION index 88c5fb89..bc80560f 100644 --- a/cherab/core/VERSION +++ b/cherab/core/VERSION @@ -1 +1 @@ -1.4.0 +1.5.0 diff --git a/cherab/core/atomic/data/lineshape/stark/d.json b/cherab/core/atomic/data/lineshape/stark/d.json new file mode 100644 index 00000000..c0092760 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/stark/d.json @@ -0,0 +1,70 @@ +{ + "0": { + "3 -> 2": [ + 3.71e-18, + 0.7665, + 0.064 + ], + "4 -> 2": [ + 8.425e-18, + 0.7803, + 0.050 + ], + "5 -> 2": [ + 1.31e-15, + 0.6796, + 0.030 + ], + "6 -> 2": [ + 3.954e-16, + 0.7149, + 0.028 + ], + "7 -> 2": [ + 6.258e-16, + 0.712, + 0.029 + ], + "8 -> 2": [ + 7.378e-16, + 0.7159, + 0.032 + ], + "9 -> 2": [ + 8.947e-16, + 0.7177, + 0.033 + ], + "4 -> 3": [ + 1.330e-16, + 0.7449, + 0.045 + ], + "5 -> 3": [ + 6.64e-16, + 0.7356, + 0.044 + ], + "6 -> 3": [ + 2.481e-15, + 0.7118, + 0.016 + ], + "7 -> 3": [ + 3.270e-15, + 0.7137, + 0.029 + ], + "8 -> 3": [ + 4.343e-15, + 0.7133, + 0.032 + ], + "9 -> 3": [ + 5.588e-15, + 0.7165, + 0.033 + ] + }, + "reference": "B. Lomanowski, et al. Inferring divertor plasma properties from hydrogen Balmer and Paschen series spectroscopy in JET-ILW. Nuclear Fusion 55.12 (2015) 123028" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/stark/h.json b/cherab/core/atomic/data/lineshape/stark/h.json new file mode 100644 index 00000000..c0092760 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/stark/h.json @@ -0,0 +1,70 @@ +{ + "0": { + "3 -> 2": [ + 3.71e-18, + 0.7665, + 0.064 + ], + "4 -> 2": [ + 8.425e-18, + 0.7803, + 0.050 + ], + "5 -> 2": [ + 1.31e-15, + 0.6796, + 0.030 + ], + "6 -> 2": [ + 3.954e-16, + 0.7149, + 0.028 + ], + "7 -> 2": [ + 6.258e-16, + 0.712, + 0.029 + ], + "8 -> 2": [ + 7.378e-16, + 0.7159, + 0.032 + ], + "9 -> 2": [ + 8.947e-16, + 0.7177, + 0.033 + ], + "4 -> 3": [ + 1.330e-16, + 0.7449, + 0.045 + ], + "5 -> 3": [ + 6.64e-16, + 0.7356, + 0.044 + ], + "6 -> 3": [ + 2.481e-15, + 0.7118, + 0.016 + ], + "7 -> 3": [ + 3.270e-15, + 0.7137, + 0.029 + ], + "8 -> 3": [ + 4.343e-15, + 0.7133, + 0.032 + ], + "9 -> 3": [ + 5.588e-15, + 0.7165, + 0.033 + ] + }, + "reference": "B. Lomanowski, et al. Inferring divertor plasma properties from hydrogen Balmer and Paschen series spectroscopy in JET-ILW. Nuclear Fusion 55.12 (2015) 123028" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/stark/t.json b/cherab/core/atomic/data/lineshape/stark/t.json new file mode 100644 index 00000000..c0092760 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/stark/t.json @@ -0,0 +1,70 @@ +{ + "0": { + "3 -> 2": [ + 3.71e-18, + 0.7665, + 0.064 + ], + "4 -> 2": [ + 8.425e-18, + 0.7803, + 0.050 + ], + "5 -> 2": [ + 1.31e-15, + 0.6796, + 0.030 + ], + "6 -> 2": [ + 3.954e-16, + 0.7149, + 0.028 + ], + "7 -> 2": [ + 6.258e-16, + 0.712, + 0.029 + ], + "8 -> 2": [ + 7.378e-16, + 0.7159, + 0.032 + ], + "9 -> 2": [ + 8.947e-16, + 0.7177, + 0.033 + ], + "4 -> 3": [ + 1.330e-16, + 0.7449, + 0.045 + ], + "5 -> 3": [ + 6.64e-16, + 0.7356, + 0.044 + ], + "6 -> 3": [ + 2.481e-15, + 0.7118, + 0.016 + ], + "7 -> 3": [ + 3.270e-15, + 0.7137, + 0.029 + ], + "8 -> 3": [ + 4.343e-15, + 0.7133, + 0.032 + ], + "9 -> 3": [ + 5.588e-15, + 0.7165, + 0.033 + ] + }, + "reference": "B. Lomanowski, et al. Inferring divertor plasma properties from hydrogen Balmer and Paschen series spectroscopy in JET-ILW. Nuclear Fusion 55.12 (2015) 123028" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/b.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/b.json new file mode 100644 index 00000000..b63c7c85 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/b.json @@ -0,0 +1,35 @@ +{ + "4": { + "6 -> 5": [ + 0.0083423, + 2.0519, + -0.2960 + ], + "7 -> 6": [ + 0.0228379, + 1.6546, + -0.2941 + ], + "8 -> 6": [ + 0.0084065, + 1.8041, + -0.3177 + ], + "8 -> 7": [ + 0.0541883, + 1.4128, + -0.2966 + ], + "9 -> 7": [ + 0.0190781, + 1.5440, + -0.3211 + ], + "10 -> 8": [ + 0.0391914, + 1.3569, + -0.3252 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/be.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/be.json new file mode 100644 index 00000000..a5e37caa --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/be.json @@ -0,0 +1,25 @@ +{ + "3": { + "5 -> 4": [ + 0.0060354, + 2.1245, + -0.3190 + ], + "6 -> 5": [ + 0.0202754, + 1.6538, + -0.3192 + ], + "7 -> 5": [ + 0.0078966, + 1.7017, + -0.3348 + ], + "8 -> 6": [ + 0.0205025, + 1.4581, + -0.3450 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/c.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/c.json new file mode 100644 index 00000000..8e6ad067 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/c.json @@ -0,0 +1,45 @@ +{ + "5": { + "6 -> 5": [ + 0.0040900, + 2.4271, + -0.2818 + ], + "7 -> 6": [ + 0.0110398, + 1.9785, + -0.2816 + ], + "8 -> 6": [ + 0.0040747, + 2.1776, + -0.3035 + ], + "8 -> 7": [ + 0.0261405, + 1.6689, + -0.2815 + ], + "9 -> 7": [ + 0.0092096, + 1.8495, + -0.3049 + ], + "10 -> 8": [ + 0.0189020, + 1.6191, + -0.3078 + ], + "11 -> 8": [ + 0.0110428, + 1.6600, + -0.3162 + ], + "10 -> 9": [ + 0.0359009, + 1.4464, + -0.3104 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/d.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/d.json new file mode 100644 index 00000000..3b7e8150 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/d.json @@ -0,0 +1,15 @@ +{ + "0": { + "3 -> 2": [ + 0.0402068, + 0.4384, + -0.5015 + ], + "4 -> 2": [ + 0.0220610, + 0.3702, + -0.5132 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/h.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/h.json new file mode 100644 index 00000000..13745495 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/h.json @@ -0,0 +1,15 @@ +{ + "0": { + "3 -> 2": [ + 0.0402267, + 0.3415, + -0.5247 + ], + "4 -> 2": [ + 0.0220724, + 0.2837, + -0.5346 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/he.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/he.json new file mode 100644 index 00000000..2ebd0625 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/he.json @@ -0,0 +1,25 @@ +{ + "1": { + "4 -> 3": [ + 0.0205206, + 1.6118, + -0.4838 + ], + "5 -> 3": [ + 0.0095879, + 1.4294, + -0.4975 + ], + "6 -> 4": [ + 0.0401955, + 1.0058, + -0.4918 + ], + "7 -> 4": [ + 0.0273521, + 0.9563, + -0.4981 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/he3.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/he3.json new file mode 100644 index 00000000..04729f74 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/he3.json @@ -0,0 +1,25 @@ +{ + "1": { + "4 -> 3": [ + 0.0205200, + 1.4418, + -0.4892 + ], + "5 -> 3": [ + 0.0095879, + 1.2576, + -0.5001 + ], + "6 -> 4": [ + 0.0401980, + 0.8976, + -0.4971 + ], + "7 -> 4": [ + 0.0273538, + 0.8529, + -0.5039 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/n.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/n.json new file mode 100644 index 00000000..d086f4a0 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/n.json @@ -0,0 +1,30 @@ +{ + "6": { + "7 -> 6": [ + 0.0060010, + 2.4789, + -0.2817 + ], + "8 -> 7": [ + 0.0141271, + 2.0249, + -0.2762 + ], + "9 -> 8": [ + 0.0300127, + 1.7415, + -0.2753 + ], + "10 -> 8": [ + 0.0102089, + 1.9464, + -0.2975 + ], + "11 -> 9": [ + 0.0193799, + 1.7133, + -0.2973 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/ne.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/ne.json new file mode 100644 index 00000000..07e2c41b --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/ne.json @@ -0,0 +1,25 @@ +{ + "9": { + "9 -> 8": [ + 0.0072488, + 2.8838, + -0.2758 + ], + "10 -> 9": [ + 0.0141002, + 2.4755, + -0.2718 + ], + "11 -> 9": [ + 0.0046673, + 2.8410, + -0.2917 + ], + "11 -> 10": [ + 0.0257292, + 2.1890, + -0.2715 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/data/lineshape/zeeman/parametrised/o.json b/cherab/core/atomic/data/lineshape/zeeman/parametrised/o.json new file mode 100644 index 00000000..3f3663d7 --- /dev/null +++ b/cherab/core/atomic/data/lineshape/zeeman/parametrised/o.json @@ -0,0 +1,30 @@ +{ + "7": { + "8 -> 7": [ + 0.0083081, + 2.4263, + -0.2747 + ], + "9 -> 8": [ + 0.0176049, + 2.0652, + -0.2721 + ], + "10 -> 8": [ + 0.0059933, + 2.3445, + -0.2944 + ], + "10 -> 9": [ + 0.0343805, + 1.8122, + -0.2718 + ], + "11 -> 9": [ + 0.0113640, + 2.0268, + -0.2911 + ] + }, + "reference": "A. Blom and C. Jupén. Parametrisation of the Zeeman effect for hydrogen-like spectra in high-temperature plasmas. Plasma Phys. Control. Fusion 44 (2002) 1229-1241" +} \ No newline at end of file diff --git a/cherab/core/atomic/interface.pxd b/cherab/core/atomic/interface.pxd index 66d8faf8..1dacfb57 100644 --- a/cherab/core/atomic/interface.pxd +++ b/cherab/core/atomic/interface.pxd @@ -45,6 +45,8 @@ cdef class AtomicData: cpdef RecombinationPEC recombination_pec(self, Element ion, int charge, tuple transition) + cpdef ThermalCXPEC thermal_cx_pec(self, Element donor_ion, int donor_charge, Element receiver_ion, int receiver_charge, tuple transition) + cpdef TotalRadiatedPower total_radiated_power(self, Element element) cpdef LineRadiationPower line_radiated_power_rate(self, Element element, int charge) @@ -57,5 +59,8 @@ cdef class AtomicData: cpdef ZeemanStructure zeeman_structure(self, Line line, object b_field=*) - cpdef FreeFreeGauntFactor free_free_gaunt_factor(self) + cpdef tuple zeeman_triplet_parameters(self, Line line) + cpdef tuple stark_model_coefficients(self, Line line) + + cpdef FreeFreeGauntFactor free_free_gaunt_factor(self) diff --git a/cherab/core/atomic/interface.pyx b/cherab/core/atomic/interface.pyx index 286ebfb4..52c396d6 100644 --- a/cherab/core/atomic/interface.pyx +++ b/cherab/core/atomic/interface.pyx @@ -1,6 +1,6 @@ -# Copyright 2016-2022 Euratom -# Copyright 2016-2022 United Kingdom Atomic Energy Authority -# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -17,6 +17,8 @@ # under the Licence. from .gaunt import MaxwellianFreeFreeGauntFactor +import json +from os import path cdef class AtomicData: @@ -29,72 +31,170 @@ cdef class AtomicData: cpdef double wavelength(self, Element ion, int charge, tuple transition): """ - Returns the natural wavelength of the specified transition in nm. + The natural wavelength of the specified transition in nm. """ raise NotImplementedError("The wavelength() virtual method is not implemented for this atomic data source.") cpdef IonisationRate ionisation_rate(self, Element ion, int charge): + """ + Electron impact ionisation rate for a given species in m^3/s. + """ + raise NotImplementedError("The ionisation_rate() virtual method is not implemented for this atomic data source.") cpdef RecombinationRate recombination_rate(self, Element ion, int charge): + """ + Recombination rate for a given species in m^3/s. + """ + raise NotImplementedError("The recombination_rate() virtual method is not implemented for this atomic data source.") cpdef ThermalCXRate thermal_cx_rate(self, Element donor_ion, int donor_charge, Element receiver_ion, int receiver_charge): + """ + Thermal charge exchange effective rate coefficient for a given donor and receiver species in m^3/s. + """ + raise NotImplementedError("The thermal_cx_rate() virtual method is not implemented for this atomic data source.") cpdef list beam_cx_pec(self, Element donor_ion, Element receiver_ion, int receiver_charge, tuple transition): """ - Returns a list of applicable charge exchange emission rates in W.m^3. + A list of Effective charge exchange photon emission coefficient for a given donor (beam) in W.m^3. """ raise NotImplementedError("The cxs_rates() virtual method is not implemented for this atomic data source.") cpdef BeamStoppingRate beam_stopping_rate(self, Element beam_ion, Element plasma_ion, int charge): """ - Returns a list of applicable beam stopping coefficients in m^3/s. + Beam stopping coefficient for a given beam and target species in m^3/s. """ raise NotImplementedError("The beam_stopping() virtual method is not implemented for this atomic data source.") cpdef BeamPopulationRate beam_population_rate(self, Element beam_ion, int metastable, Element plasma_ion, int charge): """ - Returns a list of applicable dimensionless beam population coefficients. + Dimensionless Beam population coefficient for a given beam and target species. """ raise NotImplementedError("The beam_population() virtual method is not implemented for this atomic data source.") cpdef BeamEmissionPEC beam_emission_pec(self, Element beam_ion, Element plasma_ion, int charge, tuple transition): """ - Returns a list of applicable beam emission coefficients in W.m^3. + The beam photon emission coefficient for a given beam and target species + and a given transition in W.m^3. """ raise NotImplementedError("The beam_emission() virtual method is not implemented for this atomic data source.") cpdef ImpactExcitationPEC impact_excitation_pec(self, Element ion, int charge, tuple transition): + """ + Electron impact excitation photon emission coefficient for a given species in W.m^3. + """ + raise NotImplementedError("The impact_excitation() virtual method is not implemented for this atomic data source.") cpdef RecombinationPEC recombination_pec(self, Element ion, int charge, tuple transition): + """ + Recombination photon emission coefficient for a given species in W.m^3. + """ + raise NotImplementedError("The recombination() virtual method is not implemented for this atomic data source.") + cpdef ThermalCXPEC thermal_cx_pec(self, Element donor_ion, int donor_charge, Element receiver_ion, int receiver_charge, tuple transition): + """ + Thermal charge exchange photon emission coefficient for given donor and receiver species in W.m^3. + """ + + raise NotImplementedError("The thermal_cx_pec() virtual method is not implemented for this atomic data source.") + cpdef TotalRadiatedPower total_radiated_power(self, Element element): + """ + The total (summed over all charge states) radiated power + in equilibrium conditions for a given species in W.m^3. + """ + raise NotImplementedError("The total_radiated_power() virtual method is not implemented for this atomic data source.") cpdef LineRadiationPower line_radiated_power_rate(self, Element element, int charge): + """ + Line radiated power coefficient for a given species in W.m^3. + """ + raise NotImplementedError("The line_radiated_power_rate() virtual method is not implemented for this atomic data source.") cpdef ContinuumPower continuum_radiated_power_rate(self, Element element, int charge): + """ + Continuum radiated power coefficient for a given species in W.m^3. + """ + raise NotImplementedError("The continuum_radiated_power_rate() virtual method is not implemented for this atomic data source.") cpdef CXRadiationPower cx_radiated_power_rate(self, Element element, int charge): + """ + Charge exchange radiated power coefficient for a given species in W.m^3. + """ + raise NotImplementedError("The cx_radiated_power_rate() virtual method is not implemented for this atomic data source.") cpdef FractionalAbundance fractional_abundance(self, Element ion, int charge): + """ + Fractional abundance of a given species in thermodynamic equilibrium. + """ + raise NotImplementedError("The fractional_abundance() virtual method is not implemented for this atomic data source.") cpdef ZeemanStructure zeeman_structure(self, Line line, object b_field=None): + r""" + Wavelengths and ratios of :math:`\pi`-/:math:`\sigma`-polarised Zeeman components + for any given value of magnetic field strength. + """ + raise NotImplementedError("The zeeman_structure() virtual method is not implemented for this atomic data source.") + cpdef tuple zeeman_triplet_parameters(self, Line line): + """ + Returns Zeeman truplet parameters. See Table 1 in A. Blom and C. Jupén. + "Parametrisation of the Zeeman effect for hydrogen-like spectra in + high-temperature plasmas", Plasma Phys. Control. Fusion 44 (2002) `1229-1241 + `_. + """ + + symbol = line.element.symbol.lower() + upper, lower = line.transition + encoded_transition = '{} -> {}'.format(str(upper).lower(), str(lower).lower()) + + try: + with open(path.join(path.dirname(__file__), "data/lineshape/zeeman/parametrised/{}.json".format(symbol))) as f: + data = json.load(f) + coefficients = data[str(line.charge)][encoded_transition] + except (FileNotFoundError, KeyError): + raise RuntimeError('Requested Zeeman triplet parameters (element={}, charge={}, transition={})' + ' are not available.'.format(line.element.symbol, line.charge, line.transition)) + + return tuple(coefficients) + + cpdef tuple stark_model_coefficients(self, Line line): + """ + Returns Stark model coefficients. See Table 1 in B. Lomanowski, et al. + "Inferring divertor plasma properties from hydrogen Balmer + and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) + `123028 `_. + """ + + symbol = line.element.symbol.lower() + upper, lower = line.transition + encoded_transition = '{} -> {}'.format(str(upper).lower(), str(lower).lower()) + + try: + with open(path.join(path.dirname(__file__), "data/lineshape/stark/{}.json".format(symbol))) as f: + data = json.load(f) + coefficients = data[str(line.charge)][encoded_transition] + except (FileNotFoundError, KeyError): + raise RuntimeError('Requested Stark model coefficients (element={}, charge={}, transition={})' + ' are not available.'.format(line.element.symbol, line.charge, line.transition)) + + return tuple(coefficients) + cpdef FreeFreeGauntFactor free_free_gaunt_factor(self): """ Returns the Maxwellian-averaged free-free Gaunt factor interpolated over the data diff --git a/cherab/core/atomic/rates.pxd b/cherab/core/atomic/rates.pxd index 1f844122..f47d0c28 100644 --- a/cherab/core/atomic/rates.pxd +++ b/cherab/core/atomic/rates.pxd @@ -46,11 +46,13 @@ cdef class RecombinationPEC(_PECRate): pass -cdef class ThermalCXPEC(_PECRate): - pass +cdef class ThermalCXPEC: + cpdef double evaluate(self, double electron_density, double electron_temperature, double donor_temperature) except? -1e999 cdef class BeamCXPEC: + cdef readonly int donor_metastable + cpdef double evaluate(self, double energy, double temperature, double density, double z_effective, double b_field) except? -1e999 @@ -84,7 +86,7 @@ cdef class _RadiatedPower: readonly Element element readonly int charge - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999 + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999 cdef class LineRadiationPower(_RadiatedPower): diff --git a/cherab/core/atomic/rates.pyx b/cherab/core/atomic/rates.pyx index 25b94e7e..913ddde3 100644 --- a/cherab/core/atomic/rates.pyx +++ b/cherab/core/atomic/rates.pyx @@ -35,9 +35,9 @@ cdef class IonisationRate: cpdef double evaluate(self, double density, double temperature) except? -1e999: """Returns an effective ionisation rate coefficient at the specified plasma conditions. + :param density: Electron density in m^-3. :param temperature: Electron temperature in eV. - :param density: Electron density in m^-3 - :return: The effective ionisation rate in m^-3. + :return: The effective ionisation rate in m^3.s^-1. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -57,9 +57,9 @@ cdef class RecombinationRate: cpdef double evaluate(self, double density, double temperature) except? -1e999: """Returns an effective recombination rate coefficient at the specified plasma conditions. + :param density: Electron density in m^-3. :param temperature: Electron temperature in eV. - :param density: Electron density in m^-3 - :return: The effective ionisation rate in m^-3. + :return: The effective ionisation rate in m^3.s^-1. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -79,9 +79,9 @@ cdef class ThermalCXRate: cpdef double evaluate(self, double density, double temperature) except? -1e999: """Returns an effective charge exchange rate coefficient at the specified plasma conditions. + :param density: Electron density in m^-3. :param temperature: Electron temperature in eV. - :param density: Electron density in m^-3 - :return: The effective charge exchange rate in m^-3. + :return: The effective charge exchange rate in m^3.s^-1. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -101,9 +101,9 @@ cdef class _PECRate: cpdef double evaluate(self, double density, double temperature) except? -1e999: """Returns a photon emissivity coefficient at given conditions. - :param temperature: Receiver ion temperature in eV. - :param density: Receiver ion density in m^-3 - :return: The effective PEC rate in W/m^3. + :param density: Electron density in m^-3. + :param temperature: Electron temperature in eV. + :return: The effective PEC rate in W.m^3. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -130,11 +130,27 @@ cdef class RecombinationPEC(_PECRate): pass -cdef class ThermalCXPEC(_PECRate): +cdef class ThermalCXPEC: """ Thermal charge exchange rate coefficient. """ - pass + + def __call__(self, double electron_density, double electron_temperature, donor_temperature): + """Returns a CX photon emissivity coefficient at the specified plasma conditions. + + This function just wraps the cython evaluate() method. + """ + return self.evaluate(electron_density, electron_temperature, donor_temperature) + + cpdef double evaluate(self, double electron_density, double electron_temperature, double donor_temperature) except? -1e999: + """Returns a CX photon emissivity coefficient at given conditions. + + :param electron_density: Electron density in m^-3. + :param electron_temperature: Electron temperature in eV. + :param donor_temperature: Donor temperature in eV. + :return: The effective CX PEC rate in W/m^3. + """ + raise NotImplementedError("The evaluate() virtual method must be implemented.") cdef class BeamCXPEC: @@ -144,8 +160,13 @@ cdef class BeamCXPEC: transition :math:`n\rightarrow n'` of ion :math:`Z^{(\alpha+1)+}` with electron donor :math:`H^0` in metastable state :math:`m_{i}`. Equivalent to :math:`q^{eff}_{n\rightarrow n'}` in `adf12 _`. + + :param donor_metastable: The metastable state of the donor species for which the rate data applies. """ + def __init__(self, int donor_metastable): + self.donor_metastable = donor_metastable + def __call__(self, double energy, double temperature, double density, double z_effective, double b_field): """Evaluates the Beam CX rate at the given plasma conditions. @@ -158,7 +179,7 @@ cdef class BeamCXPEC: :param float energy: Interaction energy in eV/amu. :param float temperature: Receiver ion temperature in eV. - :param float density: Receiver ion density in m^-3 + :param float density: Plasma total ion density in m^-3 :param float z_effective: Plasma Z-effective. :param float b_field: Magnetic field magnitude in Tesla. :return: The effective rate @@ -221,7 +242,7 @@ cdef class BeamEmissionPEC(_BeamRate): cdef class TotalRadiatedPower(): - """The total radiated power in equilibrium conditions.""" + """The total radiated power rate in equilibrium conditions.""" def __init__(self, Element element): @@ -229,21 +250,20 @@ cdef class TotalRadiatedPower(): def __call__(self, double electron_density, double electron_temperature): """ - Evaluate the radiated power rate at the given plasma conditions. - - Calls the cython evaluate() method under the hood. + Evaluate the total radiated power rate at the given plasma conditions. - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + This function just wraps the cython evaluate() method. """ return self.evaluate(electron_density, electron_temperature) cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: """ - Evaluate the radiated power at the given plasma conditions. + Evaluate the total radiated power rate at the given plasma conditions. + + :param float electron_density: Electron density in m^-3. + :param float electron_temperature: Electron temperature in eV. - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + :return: The total radiated power rate in W.m^3. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -260,19 +280,18 @@ cdef class _RadiatedPower: """ Evaluate the radiated power rate at the given plasma conditions. - Calls the cython evaluate() method under the hood. - - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + This function just wraps the cython evaluate() method. """ return self.evaluate(electron_density, electron_temperature) - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: """ Evaluate the radiated power at the given plasma conditions. - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + :param float density: Electron density in m^-3. + :param float temperature: Electron temperature in eV. + + :return: The radiated power rate in W.m^3. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -308,9 +327,9 @@ cdef class FractionalAbundance: """ Rate provider for fractional abundances in thermodynamic equilibrium. - :param Element element: the radiating element - :param int charge: the integer charge state for this ionisation stage - :param str name: optional label identifying this rate + :param Element element: the radiating element. + :param int charge: the integer charge state for this ionisation stage. + :param str name: optional label identifying this rate. """ def __init__(self, element, charge, name=''): @@ -326,8 +345,10 @@ cdef class FractionalAbundance: """ Evaluate the fractional abundance of this ionisation stage at the given plasma conditions. - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + :param float electron_density: Electron density in m^-3. + :param float electron_temperature: Electron temperature in eV. + + :return: Fractional abundance. """ raise NotImplementedError("The evaluate() virtual method must be implemented.") @@ -335,8 +356,7 @@ cdef class FractionalAbundance: """ Evaluate the fractional abundance of this ionisation stage at the given plasma conditions. - :param float electron_density: electron density in m^-3 - :param float electron_temperature: electron temperature in eV + This function just wraps the cython evaluate() method. """ return self.evaluate(electron_density, electron_temperature) diff --git a/cherab/core/beam/node.pxd b/cherab/core/beam/node.pxd index d8159bbb..8f58e0c0 100644 --- a/cherab/core/beam/node.pxd +++ b/cherab/core/beam/node.pxd @@ -47,7 +47,7 @@ cdef class Beam(Node): Vector3D BEAM_AXIS double _energy, _power, _temperature Element _element - double _divergence_x, _divergence_y + double _divergence_x, _divergence_y, _tanxdiv, _tanydiv double _length, _sigma Plasma _plasma AtomicData _atomic_data diff --git a/cherab/core/beam/node.pyx b/cherab/core/beam/node.pyx index 36877421..32d7291d 100644 --- a/cherab/core/beam/node.pyx +++ b/cherab/core/beam/node.pyx @@ -30,7 +30,7 @@ from cherab.core.beam.material cimport BeamMaterial from cherab.core.beam.model cimport BeamModel from cherab.core.atomic cimport AtomicData, Element from cherab.core.utility import Notifier -from libc.math cimport tan, M_PI +from libc.math cimport tan, M_PI, sqrt cdef double DEGREES_TO_RADIANS = M_PI / 180 @@ -144,6 +144,7 @@ cdef class Beam(Node): :ivar float sigma: The Gaussian beam width at the origin in m. :ivar float temperature: The broadening of the beam (eV). + .. code-block:: pycon >>> # This example shows how to initialise and populate a basic beam @@ -188,6 +189,8 @@ cdef class Beam(Node): self._element = element = None # beam species, an Element object self._divergence_x = 0.0 # beam divergence x (degrees) self._divergence_y = 0.0 # beam divergence y (degrees) + self._tanxdiv = 0.0 # tan(DEGREES_TO_RADIANS * divergence_x) + self._tanydiv = 0.0 # tan(DEGREES_TO_RADIANS * divergence_y) self._length = 1.0 # m self._sigma = 0.1 # m (gaussian beam width at origin) @@ -231,8 +234,25 @@ cdef class Beam(Node): return self._attenuator.density(x, y, z) cpdef Vector3D direction(self, double x, double y, double z): - """ - Calculates the beam direction vector at a point in space. + r""" + Calculates the beam direction vector at a point in beam coordinate space. + + The beam direction (non-normalised) is calculated as follows (z > 0): + + .. math:: + e_x = x\frac{(ztg(\alpha_x))^2}{\sigma^2 + (ztg(\alpha_x))^2}, + + e_y = y\frac{(ztg(\alpha_y))^2}{\sigma^2 + (ztg(\alpha_y))^2}, + + e_z = z, + + where :math:`\sigma` is the Gaussian beam deviation at origin, + :math:`\alpha_x` and :math:`\alpha_y` are the beam divergence angles + in the x and y dimensions respectively. + + For z <= 0 the beam direction is (0, 0, 1). + + The function returns normalised beam direction. Note the values of the beam outside of the beam envelope should be treated with caution. @@ -248,9 +268,15 @@ cdef class Beam(Node): return self.BEAM_AXIS # calculate direction from divergence - cdef double dx = tan(DEGREES_TO_RADIANS * self._divergence_x) - cdef double dy = tan(DEGREES_TO_RADIANS * self._divergence_y) - return new_vector3d(dx, dy, 1.0).normalise() + cdef double z_tanx_sqr = z * z * self._tanxdiv * self._tanxdiv + cdef double z_tany_sqr = z * z * self._tanydiv * self._tanydiv + cdef double sigma_sqr = self._sigma * self._sigma + cdef double sigma_x_sqr = sigma_sqr + z_tanx_sqr + cdef double sigma_y_sqr = sigma_sqr + z_tany_sqr + cdef double ex = x * z_tanx_sqr / sigma_x_sqr + cdef double ey = y * z_tany_sqr / sigma_y_sqr + + return new_vector3d(ex, ey, z).normalise() @property def energy(self): @@ -315,6 +341,7 @@ cdef class Beam(Node): if value < 0: raise ValueError('Beam x divergence cannot be less than zero.') self._divergence_x = value + self._tanxdiv = tan(DEGREES_TO_RADIANS * value) self.notifier.notify() cdef double get_divergence_x(self): @@ -329,6 +356,7 @@ cdef class Beam(Node): if value < 0: raise ValueError('Beam y divergence cannot be less than zero.') self._divergence_y = value + self._tanydiv = tan(DEGREES_TO_RADIANS * value) self.notifier.notify() cdef double get_divergence_y(self): @@ -501,10 +529,10 @@ cdef class Beam(Node): # radii of bounds at the beam origin (z=0) and the beam end (z=length) radius_start = num_sigma * self.sigma - radius_end = radius_start + self.length * num_sigma * drdz + radius_end = num_sigma * sqrt(self.sigma**2 + self.length**2 * drdz**2) # distance of the cone apex to the beam origin - distance_apex = radius_start / (num_sigma * drdz) + distance_apex = radius_start * self.length / (radius_end - radius_start) cone_height = self.length + distance_apex # calculate volumes diff --git a/cherab/core/laser/material.pxd b/cherab/core/laser/material.pxd index c91fa416..24695146 100644 --- a/cherab/core/laser/material.pxd +++ b/cherab/core/laser/material.pxd @@ -17,7 +17,7 @@ # under the Licence. -from raysect.core.scenegraph._nodebase cimport _NodeBase +from raysect.core cimport Primitive from raysect.core.math cimport AffineMatrix3D from raysect.optical.material.emitter cimport InhomogeneousVolumeEmitter @@ -28,4 +28,8 @@ cdef class LaserMaterial(InhomogeneousVolumeEmitter): cdef: AffineMatrix3D _laser_to_plasma, _laser_segment_to_laser_node + Primitive _primitive + Laser _laser list _models + + cdef void _cache_transforms(self) \ No newline at end of file diff --git a/cherab/core/laser/material.pyx b/cherab/core/laser/material.pyx index 4732e01a..75cc46a6 100644 --- a/cherab/core/laser/material.pyx +++ b/cherab/core/laser/material.pyx @@ -17,7 +17,7 @@ # under the Licence. -from raysect.core.scenegraph._nodebase cimport _NodeBase +from raysect.core cimport Primitive from raysect.optical cimport World, Primitive, Ray, Spectrum, Point3D, Vector3D, AffineMatrix3D from raysect.optical.material.emitter cimport InhomogeneousVolumeEmitter from raysect.optical.material.emitter.inhomogeneous cimport VolumeIntegrator @@ -28,12 +28,12 @@ from cherab.core.laser.model cimport LaserModel cdef class LaserMaterial(InhomogeneousVolumeEmitter): - def __init__(self, Laser laser not None, _NodeBase laser_segment not None, list models, VolumeIntegrator integrator not None): + def __init__(self, Laser laser not None, Primitive laser_segment not None, list models, VolumeIntegrator integrator not None): super().__init__(integrator) - self._laser_segment_to_laser_node = laser_segment.to(laser) - self._laser_to_plasma = laser_segment.to(laser.plasma) + self._laser = laser + self._primitive = laser_segment self.importance = laser.importance #validate and set models @@ -54,6 +54,10 @@ cdef class LaserMaterial(InhomogeneousVolumeEmitter): Point3D point_plasma, point_laser Vector3D direction_plasma, direction_laser LaserModel model + + # cache the important transforms + if self._laser_segment_to_laser_node is None or self._laser_to_plasma is None: + self._cache_transforms() point_laser = point.transform(self._laser_segment_to_laser_node) direction_laser = direction.transform(self._laser_segment_to_laser_node) # observation vector in the laser frame @@ -63,4 +67,16 @@ cdef class LaserMaterial(InhomogeneousVolumeEmitter): for model in self._models: spectrum = model.emission(point_plasma, direction_plasma, point_laser, direction_laser, spectrum) - return spectrum + return spectrum + + cdef void _cache_transforms(self): + """ + cache transforms from laser primitive to laser and plasma + """ + + # if transforms are cached, the material should be used only for one primitive for safety + if not len(self.primitives) == 1: + raise ValueError("LaserMaterial must be attached to exactly one primitive.") + + self._laser_segment_to_laser_node = self._primitive.to(self._laser) + self._laser_to_plasma = self._primitive.to(self._laser.get_plasma()) diff --git a/cherab/core/laser/node.pxd b/cherab/core/laser/node.pxd index 6045fb4a..8fec6821 100644 --- a/cherab/core/laser/node.pxd +++ b/cherab/core/laser/node.pxd @@ -52,4 +52,6 @@ cdef class Laser(Node): list _geometry VolumeIntegrator _integrator - cdef object __weakref__ \ No newline at end of file + cdef object __weakref__ + + cdef Plasma get_plasma(self) \ No newline at end of file diff --git a/cherab/core/laser/node.pyx b/cherab/core/laser/node.pyx index b0b821b5..9475fb69 100644 --- a/cherab/core/laser/node.pyx +++ b/cherab/core/laser/node.pyx @@ -153,6 +153,12 @@ cdef class Laser(Node): self._plasma.notifier.add(self._plasma_changed) self._configure_materials() + + cdef Plasma get_plasma(self): + """ + Fast method to obtain laser's plasma reference. + """ + return self._plasma @property def importance(self): diff --git a/cherab/core/laser/tests/test_laserspectrum.py b/cherab/core/laser/tests/test_laserspectrum.py index 9473e43d..6545f5c7 100644 --- a/cherab/core/laser/tests/test_laserspectrum.py +++ b/cherab/core/laser/tests/test_laserspectrum.py @@ -1,9 +1,11 @@ import unittest import numpy as np -from cherab.core.laser.laserspectrum import LaserSpectrum +# The evaluate() method is not implemented in the base LaserSpectrum, so importing the simplest subclass. +from cherab.core.model.laser.laserspectrum import ConstantSpectrum as LaserSpectrum from raysect.optical.spectrum import Spectrum + class TestLaserSpectrum(unittest.TestCase): def test_laserspectrum_init(self): @@ -24,12 +26,12 @@ def test_laserspectrum_init(self): msg="LaserSpectrum did not raise a ValueError with max_wavelength < min_wavelength."): LaserSpectrum(40, 30, 200) LaserSpectrum(30, 30, 200) - + # test bins > 0 with self.assertRaises(ValueError, - msg="LaserSpectrum did not raise a ValueError with max_wavelength < min_wavelength."): - LaserSpectrum(30, 30, 0) - LaserSpectrum(30, 30, -1) + msg="LaserSpectrum did not raise a ValueError with bins <= 0."): + LaserSpectrum(30, 40, 0) + LaserSpectrum(30, 40, -1) def test_laserspectrum_changes(self): laser_spectrum = LaserSpectrum(100, 200, 100) @@ -59,4 +61,4 @@ def test_laserspectrum_changes(self): # test caching of spectrum data, behaviour should be consistent with raysect.optical.spectrum.Spectrum self.assertTrue(np.array_equal(laser_spectrum.wavelengths, spectrum.wavelengths), "LaserSpectrum.wavelengths values are not equal to Spectrum.wavelengths " - "with same boundaries and number of bins") \ No newline at end of file + "with same boundaries and number of bins") diff --git a/cherab/core/math/__init__.pxd b/cherab/core/math/__init__.pxd index 99e92fbe..710ee381 100644 --- a/cherab/core/math/__init__.pxd +++ b/cherab/core/math/__init__.pxd @@ -25,3 +25,4 @@ from cherab.core.math.clamp cimport * from cherab.core.math.mappers cimport * from cherab.core.math.mask cimport * from cherab.core.math.slice cimport * +from cherab.core.math.transform cimport * diff --git a/cherab/core/math/__init__.py b/cherab/core/math/__init__.py index b3e9a031..85336afa 100644 --- a/cherab/core/math/__init__.py +++ b/cherab/core/math/__init__.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -31,6 +31,11 @@ from .caching import Caching1D, Caching2D, Caching3D from .clamp import ClampOutput1D, ClampOutput2D, ClampOutput3D from .clamp import ClampInput1D, ClampInput2D, ClampInput3D -from .mappers import IsoMapper2D, IsoMapper3D, Swizzle2D, Swizzle3D, AxisymmetricMapper, VectorAxisymmetricMapper +from .mappers import IsoMapper2D, IsoMapper3D +from .mappers import Swizzle2D, Swizzle3D +from .mappers import AxisymmetricMapper, VectorAxisymmetricMapper from .mask import PolygonMask2D from .slice import Slice2D, Slice3D +from .transform import CylindricalTransform, VectorCylindricalTransform +from .transform import PeriodicTransform1D, PeriodicTransform2D, PeriodicTransform3D +from .transform import VectorPeriodicTransform1D, VectorPeriodicTransform2D, VectorPeriodicTransform3D diff --git a/cherab/core/math/integrators/integrators1d.pyx b/cherab/core/math/integrators/integrators1d.pyx index 54ec4059..7ff9be74 100644 --- a/cherab/core/math/integrators/integrators1d.pyx +++ b/cherab/core/math/integrators/integrators1d.pyx @@ -201,6 +201,7 @@ cdef class GaussianQuadrature(Integrator1D): double newval, oldval, error, x, c, d oldval = INFINITY + newval = 0 ibegin = 0 c = 0.5 * (a + b) d = 0.5 * (b - a) diff --git a/cherab/core/math/mappers.pxd b/cherab/core/math/mappers.pxd index 5b455d9d..3835e949 100644 --- a/cherab/core/math/mappers.pxd +++ b/cherab/core/math/mappers.pxd @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -16,8 +16,9 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from cherab.core.math.function cimport Function1D, Function2D, Function3D, VectorFunction2D, VectorFunction3D -from raysect.core cimport Vector3D +from raysect.core.math.function.float cimport Function1D, Function2D, Function3D +from raysect.core.math.function.vector3d cimport Function2D as VectorFunction2D +from raysect.core.math.function.vector3d cimport Function3D as VectorFunction3D cdef class IsoMapper2D(Function2D): @@ -26,8 +27,6 @@ cdef class IsoMapper2D(Function2D): readonly Function1D function1d readonly Function2D function2d - cdef double evaluate(self, double x, double y) except? -1e999 - cdef class IsoMapper3D(Function3D): @@ -35,15 +34,11 @@ cdef class IsoMapper3D(Function3D): readonly Function3D function3d readonly Function1D function1d - cdef double evaluate(self, double x, double y, double z) except? -1e999 - cdef class Swizzle2D(Function2D): cdef readonly Function2D function2d - cdef double evaluate(self, double x, double y) except? -1e999 - cdef class Swizzle3D(Function3D): @@ -51,18 +46,12 @@ cdef class Swizzle3D(Function3D): readonly Function3D function3d int shape[3] - cdef double evaluate(self, double x, double y, double z) except? -1e999 - cdef class AxisymmetricMapper(Function3D): cdef readonly Function2D function2d - cdef double evaluate(self, double x, double y, double z) except? -1e999 - cdef class VectorAxisymmetricMapper(VectorFunction3D): cdef readonly VectorFunction2D function2d - - cdef Vector3D evaluate(self, double x, double y, double z) \ No newline at end of file diff --git a/cherab/core/math/mappers.pyx b/cherab/core/math/mappers.pyx index 5ff224a3..f1326f12 100644 --- a/cherab/core/math/mappers.pyx +++ b/cherab/core/math/mappers.pyx @@ -20,7 +20,9 @@ from libc.math cimport sqrt, atan2, M_PI -from cherab.core.math.function cimport autowrap_function1d, autowrap_function2d, autowrap_function3d, autowrap_vectorfunction2d +from raysect.core.math cimport Vector3D +from raysect.core.math.function.float cimport autowrap_function1d, autowrap_function2d, autowrap_function3d +from raysect.core.math.function.vector3d cimport autowrap_function2d as autowrap_vectorfunction2d from raysect.core cimport rotate_z cimport cython @@ -251,13 +253,10 @@ cdef class AxisymmetricMapper(Function3D): def __init__(self, object function2d): if not callable(function2d): - raise TypeError("Function3D is not callable.") + raise TypeError("Function2D is not callable.") self.function2d = autowrap_function2d(function2d) - def __call__(self, double x, double y, double z): - return self.evaluate(x, y, z) - cdef double evaluate(self, double x, double y, double z) except? -1e999: """Return the value of function2d when it is y-axis symmetrically extended to the 3D space.""" @@ -299,13 +298,11 @@ cdef class VectorAxisymmetricMapper(VectorFunction3D): self.function2d = autowrap_vectorfunction2d(vectorfunction2d) - def __call__(self, double x, double y, double z): - return self.evaluate(x, y, z) - @cython.cdivision(True) cdef Vector3D evaluate(self, double x, double y, double z): """Return the value of function2d when it is y-axis symmetrically extended to the 3D space.""" + cdef double r, phi # convert to cylindrical coordinates phi = atan2(y, x) / M_PI * 180 diff --git a/cherab/core/math/tests/test_mappers.py b/cherab/core/math/tests/test_mappers.py index 777a6a85..a1d0eb73 100644 --- a/cherab/core/math/tests/test_mappers.py +++ b/cherab/core/math/tests/test_mappers.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -16,6 +16,7 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. +from raysect.core.math import Vector3D from cherab.core.math import mappers import numpy as np import unittest @@ -35,6 +36,9 @@ def f2d(x, y): return x*np.sin(y) def f3d(x, y, z): return x*x*np.exp(y)-2*z*y self.function3d = f3d + def vecf2d(r, z): return Vector3D(0, r, z) + self.vectorfunction2d = vecf2d + def test_iso_mapper_2d(self): """Composition of a 1D and a 2D function.""" @@ -142,5 +146,17 @@ def test_axisymmetric_mapper_invalid_arg(self): """An error must be raised if the given argument is not callable.""" self.assertRaises(TypeError, mappers.AxisymmetricMapper, "blah") + def test_vector_axisymmetric_mapper(self): + """Vector axisymmetric mapper.""" + symm_func = mappers.VectorAxisymmetricMapper(self.vectorfunction2d) + vec1 = symm_func(1., 1., 1.) + vec2 = Vector3D(-1., 1., 1.) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + def test_vector_axisymmetric_mapper_invalid_arg(self): + """An error must be raised if the given argument is not callable.""" + self.assertRaises(TypeError, mappers.VectorAxisymmetricMapper, "blah") + + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/cherab/core/math/tests/test_transform.py b/cherab/core/math/tests/test_transform.py new file mode 100644 index 00000000..fc3a2445 --- /dev/null +++ b/cherab/core/math/tests/test_transform.py @@ -0,0 +1,260 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.core.math import Vector3D +from cherab.core.math import transform +import numpy as np +import unittest + + +class TestCylindricalTransform(unittest.TestCase): + """Cylindrical transform tests.""" + + def setUp(self): + """Initialisation with functions to map.""" + + def f3d(r, phi, z): + return r * np.cos(phi) + z + self.function3d = f3d + + def vecf3d(r, phi, z): + return Vector3D(np.sin(phi), r * z, np.cos(phi)) + self.vectorfunction3d = vecf3d + + def test_cylindrical_transform(self): + """Cylindrical transform.""" + cyl_func = transform.CylindricalTransform(self.function3d) + self.assertAlmostEqual(cyl_func(1., 1., 0.5), + self.function3d(np.sqrt(2.), 0.25 * np.pi, 0.5), + places=10) + + def test_cylindrical_transform_invalid_arg(self): + """An error must be raised if the given argument is not callable.""" + self.assertRaises(TypeError, transform.CylindricalTransform, "blah") + + def test_vector_cylindrical_transform(self): + """Cylindrical transform.""" + cyl_func = transform.VectorCylindricalTransform(self.vectorfunction3d) + vec1 = cyl_func(1., 1., 1.) + vec2 = Vector3D(-0.5, 1.5, 1 / np.sqrt(2)) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + def test_vector_cylindrical_transform_invalid_arg(self): + """An error must be raised if the given argument is not callable.""" + self.assertRaises(TypeError, transform.VectorCylindricalTransform, "blah") + + +class TestPeriodicTransform(unittest.TestCase): + """Periodic transform tests.""" + + def setUp(self): + """Initialisation with functions to map.""" + + def f1d(x): + return x * np.cos(x - 3) + self.function1d = f1d + + def f2d(x, y): + return x * np.sin(y) + self.function2d = f2d + + def f3d(x, y, z): + return x * x * np.exp(y) - 2 * z * y + self.function3d = f3d + + def vecf1d(x): + return Vector3D(x, x**2, x**3) + self.vectorfunction1d = vecf1d + + def vecf2d(x, y): + return Vector3D(x, y, x * y) + self.vectorfunction2d = vecf2d + + def vecf3d(x, y, z): + return Vector3D(x + y + z, (x + y) * z, x * y * z) + self.vectorfunction3d = vecf3d + + def test_periodic_transform_1d(self): + """1D periodic transform""" + period_func = transform.PeriodicTransform1D(self.function1d, np.pi) + self.assertAlmostEqual(period_func(1.4 * np.pi), + self.function1d(0.4 * np.pi), + places=10) + self.assertAlmostEqual(period_func(-0.4 * np.pi), + self.function1d(0.6 * np.pi), + places=10) + + def test_periodic_transform_1d_invalid_arg(self): + """1D periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.PeriodicTransform1D, "blah", np.pi) + # period is not a number + self.assertRaises(TypeError, transform.PeriodicTransform1D, self.function1d, "blah") + # period is negative + self.assertRaises(ValueError, transform.PeriodicTransform1D, self.function1d, -1) + + def test_periodic_transform_2d(self): + """2D periodic transform""" + period_func = transform.PeriodicTransform2D(self.function2d, 1, np.pi) + self.assertAlmostEqual(period_func(-0.4, 1.4 * np.pi), + self.function2d(0.6, 0.4 * np.pi), + places=10) + # Periodic only along x + period_func = transform.PeriodicTransform2D(self.function2d, 1., 0) + self.assertAlmostEqual(period_func(-0.4, 1.4 * np.pi), + self.function2d(0.6, 1.4 * np.pi), + places=10) + # Periodic only along y + period_func = transform.PeriodicTransform2D(self.function2d, 0, np.pi) + self.assertAlmostEqual(period_func(-0.4, 1.4 * np.pi), + self.function2d(-0.4, 0.4 * np.pi), + places=10) + + def test_periodic_transform_2d_invalid_arg(self): + """2D periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.PeriodicTransform2D, "blah", np.pi, np.pi) + # period is not a number + self.assertRaises(TypeError, transform.PeriodicTransform2D, self.function2d, "blah", np.pi) + self.assertRaises(TypeError, transform.PeriodicTransform2D, self.function2d, np.pi, "blah") + # period is negative + self.assertRaises(ValueError, transform.PeriodicTransform2D, self.function2d, -1, np.pi) + self.assertRaises(ValueError, transform.PeriodicTransform2D, self.function2d, np.pi, -1) + + def test_periodic_transform_3d(self): + """3D periodic transform""" + period_func = transform.PeriodicTransform3D(self.function3d, 1, 1, 1) + self.assertAlmostEqual(period_func(-0.4, 1.4, 2.1), + self.function3d(0.6, 0.4, 0.1), + places=10) + # Periodic only along y and z + period_func = transform.PeriodicTransform3D(self.function3d, 0, 1, 1) + self.assertAlmostEqual(period_func(-0.4, 1.4, 2.1), + self.function3d(-0.4, 0.4, 0.1), + places=10) + # Periodic only along x and z + period_func = transform.PeriodicTransform3D(self.function3d, 1, 0, 1) + self.assertAlmostEqual(period_func(-0.4, 1.4, 2.1), + self.function3d(0.6, 1.4, 0.1), + places=10) + # Periodic only along x and y + period_func = transform.PeriodicTransform3D(self.function3d, 1, 1, 0) + self.assertAlmostEqual(period_func(-0.4, 1.4, 2.1), + self.function3d(0.6, 0.4, 2.1), + places=10) + + def test_periodic_transform_3d_invalid_arg(self): + """3D periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.PeriodicTransform3D, "blah", np.pi, np.pi, np.pi) + # period is not a number + self.assertRaises(TypeError, transform.PeriodicTransform3D, self.function3d, "blah", np.pi, np.pi) + self.assertRaises(TypeError, transform.PeriodicTransform3D, self.function3d, np.pi, "blah", np.pi) + self.assertRaises(TypeError, transform.PeriodicTransform3D, self.function3d, np.pi, np.pi, "blah") + # period is negative + self.assertRaises(ValueError, transform.PeriodicTransform3D, self.function3d, -1, np.pi, np.pi) + self.assertRaises(ValueError, transform.PeriodicTransform3D, self.function3d, np.pi, -1, np.pi) + self.assertRaises(ValueError, transform.PeriodicTransform3D, self.function3d, np.pi, np.pi, -1) + + def test_vector_periodic_transform_1d(self): + """1D vector periodic transform""" + period_func = transform.VectorPeriodicTransform1D(self.vectorfunction1d, 1) + vec1 = period_func(1.4) + vec2 = Vector3D(0.4, 0.16, 0.064) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + def test_vector_periodic_transform_1d_invalid_arg(self): + """1D vector periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.VectorPeriodicTransform1D, "blah", 1.) + # period is not a number + self.assertRaises(TypeError, transform.VectorPeriodicTransform1D, self.vectorfunction1d, "blah") + # period is negative + self.assertRaises(ValueError, transform.VectorPeriodicTransform1D, self.vectorfunction1d, -1) + + def test_vector_periodic_transform_2d(self): + """2D vector periodic transform""" + period_func = transform.VectorPeriodicTransform2D(self.vectorfunction2d, 1, 1) + vec1 = period_func(-0.4, 1.6) + vec2 = Vector3D(0.6, 0.6, 0.36) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + # Periodic only along x + period_func = transform.VectorPeriodicTransform2D(self.vectorfunction2d, 1, 0) + vec1 = period_func(-0.4, 1.6) + vec2 = Vector3D(0.6, 1.6, 0.96) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + # Periodic only along y + period_func = transform.VectorPeriodicTransform2D(self.vectorfunction2d, 0, 1) + vec1 = period_func(-0.4, 1.6) + vec2 = Vector3D(-0.4, 0.6, -0.24) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + def test_vector_periodic_transform_2d_invalid_arg(self): + """2D vector periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.VectorPeriodicTransform2D, "blah", 1, 1) + # period is not a number + self.assertRaises(TypeError, transform.VectorPeriodicTransform2D, self.vectorfunction2d, "blah", 1) + self.assertRaises(TypeError, transform.VectorPeriodicTransform2D, self.vectorfunction2d, 1, "blah") + # period is negative + self.assertRaises(ValueError, transform.VectorPeriodicTransform2D, self.vectorfunction2d, -1, 1) + self.assertRaises(ValueError, transform.VectorPeriodicTransform2D, self.vectorfunction2d, 1, -1) + + def test_vector_periodic_transform_3d(self): + """3D vector periodic transform""" + period_func = transform.VectorPeriodicTransform3D(self.vectorfunction3d, 1, 1, 1) + vec1 = period_func(-0.4, 1.6, 1.2) + vec2 = Vector3D(1.4, 0.24, 0.072) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + # Periodic along y and z + period_func = transform.VectorPeriodicTransform3D(self.vectorfunction3d, 0, 1, 1) + vec1 = period_func(-0.4, 1.6, 1.2) + vec2 = Vector3D(0.4, 0.04, -0.048) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + # Periodic along x and z + period_func = transform.VectorPeriodicTransform3D(self.vectorfunction3d, 1, 0, 1) + vec1 = period_func(-0.4, 1.6, 1.2) + vec2 = Vector3D(2.4, 0.44, 0.192) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + # Periodic along x and y + period_func = transform.VectorPeriodicTransform3D(self.vectorfunction3d, 1, 1, 0) + vec1 = period_func(-0.4, 1.6, 1.2) + vec2 = Vector3D(2.4, 1.44, 0.432) + np.testing.assert_almost_equal([vec1.x, vec1.y, vec1.z], [vec2.x, vec2.y, vec2.z], decimal=10) + + def test_vector_periodic_transform_3d_invalid_arg(self): + """3D vector periodic transform. Invalid arguments.""" + # 1st argument is not callable + self.assertRaises(TypeError, transform.VectorPeriodicTransform3D, "blah", 1, 1, 1) + # period is not a number + self.assertRaises(TypeError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, "blah", 1, 1) + self.assertRaises(TypeError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, 1, "blah", 1) + self.assertRaises(TypeError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, 1, 1, "blah") + # period is negative + self.assertRaises(ValueError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, -1, 1, 1) + self.assertRaises(ValueError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, 1, -1, 1) + self.assertRaises(ValueError, transform.VectorPeriodicTransform3D, self.vectorfunction3d, 1, 1, -1) + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/math/transform/__init__.pxd b/cherab/core/math/transform/__init__.pxd new file mode 100644 index 00000000..3ce39b6b --- /dev/null +++ b/cherab/core/math/transform/__init__.pxd @@ -0,0 +1,20 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.math.transform.periodic cimport * +from cherab.core.math.transform.cylindrical cimport * \ No newline at end of file diff --git a/cherab/core/math/transform/__init__.py b/cherab/core/math/transform/__init__.py new file mode 100644 index 00000000..168bb305 --- /dev/null +++ b/cherab/core/math/transform/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from .cylindrical import CylindricalTransform, VectorCylindricalTransform +from .periodic import PeriodicTransform1D, PeriodicTransform2D, PeriodicTransform3D +from .periodic import VectorPeriodicTransform1D, VectorPeriodicTransform2D, VectorPeriodicTransform3D diff --git a/cherab/core/math/transform/cylindrical.pxd b/cherab/core/math/transform/cylindrical.pxd new file mode 100644 index 00000000..504b2fd5 --- /dev/null +++ b/cherab/core/math/transform/cylindrical.pxd @@ -0,0 +1,30 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.core.math.function.float cimport Function3D +from raysect.core.math.function.vector3d cimport Function3D as VectorFunction3D + + +cdef class CylindricalTransform(Function3D): + + cdef readonly Function3D function3d + + +cdef class VectorCylindricalTransform(VectorFunction3D): + + cdef readonly VectorFunction3D function3d diff --git a/cherab/core/math/transform/cylindrical.pyx b/cherab/core/math/transform/cylindrical.pyx new file mode 100644 index 00000000..cd716cad --- /dev/null +++ b/cherab/core/math/transform/cylindrical.pyx @@ -0,0 +1,128 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport sqrt, atan2, M_PI + +from raysect.core.math cimport Vector3D +from raysect.core.math.function.float cimport autowrap_function3d +from raysect.core.math.function.vector3d cimport autowrap_function3d as autowrap_vectorfunction3d +from raysect.core cimport rotate_z +cimport cython + +cdef class CylindricalTransform(Function3D): + """ + Converts Cartesian coordinates to cylindrical coordinates and calls a 3D function + defined in cylindrical coordinates, f(r, :math:`\\phi`, z). + + The angular coordinate is given in radians. + + Positive angular coordinate is measured counterclockwise from the xz plane. + + :param Function3D function3d: The function to be mapped. Must be defined + in the interval (:math:`-\\pi`, :math:`\\pi`] + on the angular axis. + + .. code-block:: pycon + + >>> from math import sqrt, cos + >>> from cherab.core.math import CylindricalTransform + >>> + >>> def my_func(r, phi, z): + >>> return r * cos(phi) + >>> + >>> f = CylindricalTransform(my_func) + >>> + >>> f(1, 0, 0) + 1.0 + >>> f(0.5 * sqrt(3), 0.5, 0) + 0.8660254037844385 + """ + + def __init__(self, object function3d): + + if not callable(function3d): + raise TypeError("Function3D is not callable.") + + self.function3d = autowrap_function3d(function3d) + + cdef double evaluate(self, double x, double y, double z) except? -1e999: + """ + Converts to cylindrical coordinates and evaluates the function + defined in cylindrical coordinates. + """ + cdef double r, phi + + r = sqrt(x * x + y * y) + phi = atan2(y, x) + + return self.function3d.evaluate(r, phi, z) + + +cdef class VectorCylindricalTransform(VectorFunction3D): + """ + Converts Cartesian coordinates to cylindrical coordinates, calls + a 3D vector function defined in cylindrical coordinates, f(r, :math:`\\phi`, z), + then converts the returned 3D vector to Cartesian coordinates. + + The angular coordinate is given in radians. + + Positive angular coordinate is measured counterclockwise from the xz plane. + + :param VectorFunction3D function3d: The function to be mapped. Must be defined + in the interval (:math:`-\\pi`, :math:`\\pi`] + on the angular axis. + + .. code-block:: pycon + + >>> from math import sqrt, cos + >>> from raysect.core.math import Vector3D + >>> from cherab.core.math import VectorCylindricalTransform + >>> + >>> def my_vec_func(r, phi, z): + >>> v = Vector3D(0, 1, 0) + >>> v.length = r * abs(cos(phi)) + >>> return v + >>> + >>> f = VectorCylindricalTransform(my_vec_func) + >>> + >>> f(1, 0, 0) + Vector3D(0.0, 1.0, 0.0) + >>> f(1/sqrt(2), 1/sqrt(2), 0) + Vector3D(-0.5, 0.5, 0.0) + """ + + def __init__(self, object function3d): + + if not callable(function3d): + raise TypeError("Function3D is not callable.") + + self.function3d = autowrap_vectorfunction3d(function3d) + + @cython.cdivision(True) + cdef Vector3D evaluate(self, double x, double y, double z): + """ + Converts to cylindrical coordinates, evaluates the vector function + defined in cylindrical coordinates and rotates the resulting vector + around z-axis. + """ + cdef double r, phi + + r = sqrt(x * x + y * y) + phi = atan2(y, x) + + return self.function3d.evaluate(r, phi, z).transform(rotate_z(phi / M_PI * 180)) diff --git a/cherab/core/math/transform/periodic.pxd b/cherab/core/math/transform/periodic.pxd new file mode 100644 index 00000000..e97e4602 --- /dev/null +++ b/cherab/core/math/transform/periodic.pxd @@ -0,0 +1,72 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport fmod +from raysect.core.math.function.float cimport Function1D, Function2D, Function3D +from raysect.core.math.function.vector3d cimport Function1D as VectorFunction1D +from raysect.core.math.function.vector3d cimport Function2D as VectorFunction2D +from raysect.core.math.function.vector3d cimport Function3D as VectorFunction3D + + +cdef inline double remainder(double x1, double x2) nogil: + if x2 == 0: + return x1 + x1 = fmod(x1, x2) + return x1 + x2 if (x1 < 0) else x1 + + +cdef class PeriodicTransform1D(Function1D): + + cdef: + readonly Function1D function1d + readonly double period + + +cdef class PeriodicTransform2D(Function2D): + + cdef: + readonly Function2D function2d + double period_x, period_y + + +cdef class PeriodicTransform3D(Function3D): + + cdef: + readonly Function3D function3d + readonly double period_x, period_y, period_z + + +cdef class VectorPeriodicTransform1D(VectorFunction1D): + + cdef: + readonly VectorFunction1D function1d + readonly double period + + +cdef class VectorPeriodicTransform2D(VectorFunction2D): + + cdef: + readonly VectorFunction2D function2d + readonly double period_x, period_y + + +cdef class VectorPeriodicTransform3D(VectorFunction3D): + + cdef: + readonly VectorFunction3D function3d + readonly double period_x, period_y, period_z diff --git a/cherab/core/math/transform/periodic.pyx b/cherab/core/math/transform/periodic.pyx new file mode 100644 index 00000000..13869458 --- /dev/null +++ b/cherab/core/math/transform/periodic.pyx @@ -0,0 +1,352 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.core.math cimport Vector3D +from raysect.core.math.function.float cimport autowrap_function1d, autowrap_function2d, autowrap_function3d +from raysect.core.math.function.vector3d cimport autowrap_function1d as autowrap_vectorfunction1d +from raysect.core.math.function.vector3d cimport autowrap_function2d as autowrap_vectorfunction2d +from raysect.core.math.function.vector3d cimport autowrap_function3d as autowrap_vectorfunction3d + + +cdef class PeriodicTransform1D(Function1D): + """ + Extends a periodic 1D function to an infinite 1D space. + + :param Function1D function1d: The periodic 1D function defined + in the [0, period) interval. + :param double period: The period of the function. + + .. code-block:: pycon + + >>> from cherab.core.math import PeriodicTransform1D + >>> + >>> def f1(x): + >>> return x + >>> + >>> f2 = PeriodicTransform1D(f1, 1.) + >>> + >>> f2(1.5) + 0.5 + >>> f2(-0.3) + 0.7 + """ + + def __init__(self, object function1d, double period): + + if not callable(function1d): + raise TypeError("function1d is not callable.") + + self.function1d = autowrap_function1d(function1d) + + if period <= 0: + raise ValueError("Argument period must be positive.") + + self.period = period + + cdef double evaluate(self, double x) except? -1e999: + """Return the value of periodic function.""" + + return self.function1d.evaluate(remainder(x, self.period)) + + +cdef class PeriodicTransform2D(Function2D): + """ + Extends a periodic 2D function to an infinite 2D space. + + Set period_x/period_y to 0 if the function is not periodic along x/y axis. + + :param Function2D function2d: The periodic 2D function defined + in the ([0, period_x), [0, period_y)) intervals. + :param double period_x: The period of the function along x-axis. + 0 if not periodic. + :param double period_y: The period of the function along y-axis. + 0 if not periodic. + + .. code-block:: pycon + + >>> from cherab.core.math import PeriodicTransform2D + >>> + >>> def f1(x, y): + >>> return x * y + >>> + >>> f2 = PeriodicTransform2D(f1, 1., 1.) + >>> + >>> f2(1.5, 1.5) + 0.25 + >>> f2(-0.3, -1.3) + 0.49 + >>> + >>> f3 = PeriodicTransform2D(f1, 1., 0) + >>> + >>> f3(1.5, 1.5) + 0.75 + >>> f3(-0.3, -1.3) + -0.91 + """ + + def __init__(self, object function2d, double period_x, double period_y): + + if not callable(function2d): + raise TypeError("function2d is not callable.") + + self.function2d = autowrap_function2d(function2d) + + if period_x < 0: + raise ValueError("Argument period_x must be >= 0.") + if period_y < 0: + raise ValueError("Argument period_y must be >= 0.") + + self.period_x = period_x + self.period_y = period_y + + cdef double evaluate(self, double x, double y) except? -1e999: + """Return the value of periodic function.""" + + x = remainder(x, self.period_x) + y = remainder(y, self.period_y) + + return self.function2d.evaluate(x, y) + + +cdef class PeriodicTransform3D(Function3D): + """ + Extends a periodic 3D function to an infinite 3D space. + + Set period_x/period_y/period_z to 0 if the function is not periodic along x/y/z axis. + + :param Function3D function3d: The periodic 3D function defined in the + ([0, period_x), [0, period_y), [0, period_z)) intervals. + :param double period_x: The period of the function along x-axis. + 0 if not periodic. + :param double period_y: The period of the function along y-axis. + 0 if not periodic. + :param double period_z: The period of the function along z-axis. + 0 if not periodic. + + .. code-block:: pycon + + >>> from cherab.core.math import PeriodicTransform3D + >>> + >>> def f1(x, y, z): + >>> return x * y * z + >>> + >>> f2 = PeriodicTransform3D(f1, 1., 1., 1.) + >>> + >>> f2(1.5, 1.5, 1.5) + 0.125 + >>> f2(-0.3, -1.3, -2.3) + 0.343 + >>> + >>> f3 = PeriodicTransform3D(f1, 0, 1., 0) + >>> + >>> f3(1.5, 1.5, 1.5) + 1.125 + >>> f3(-0.3, -1.3, -0.3) + 0.063 + """ + + def __init__(self, object function3d, double period_x, double period_y, double period_z): + + if not callable(function3d): + raise TypeError("function2d is not callable.") + + self.function3d = autowrap_function3d(function3d) + + if period_x < 0: + raise ValueError("Argument period_x must be >= 0.") + if period_y < 0: + raise ValueError("Argument period_y must be >= 0.") + if period_z < 0: + raise ValueError("Argument period_z must be >= 0.") + + self.period_x = period_x + self.period_y = period_y + self.period_z = period_z + + cdef double evaluate(self, double x, double y, double z) except? -1e999: + """Return the value of periodic function.""" + + x = remainder(x, self.period_x) + y = remainder(y, self.period_y) + z = remainder(z, self.period_z) + + return self.function3d.evaluate(x, y, z) + + +cdef class VectorPeriodicTransform1D(VectorFunction1D): + """ + Extends a periodic 1D vector function to an infinite 1D space. + + :param VectorFunction1D function1d: The periodic 1D vector function + defined in the [0, period) interval. + :param double period: The period of the function. + + .. code-block:: pycon + + >>> from raysect.core.math import Vector3D + >>> from cherab.core.math import VectorPeriodicTransform1D + >>> + >>> def f1(x): + >>> return Vector3D(x, 0, 0) + >>> + >>> f2 = VectorPeriodicTransform1D(f1, 1.) + >>> + >>> f2(1.5) + Vector3D(0.5, 0, 0) + >>> f2(-0.3) + Vector3D(0.7, 0, 0) + """ + + def __init__(self, object function1d, double period): + + if not callable(function1d): + raise TypeError("function1d is not callable.") + + self.function1d = autowrap_vectorfunction1d(function1d) + + if period <= 0: + raise ValueError("Argument period must be positive.") + + self.period = period + + cdef Vector3D evaluate(self, double x): + """Return the value of periodic function.""" + + return self.function1d.evaluate(remainder(x, self.period)) + + +cdef class VectorPeriodicTransform2D(VectorFunction2D): + """ + Extends a periodic 2D vector function to an infinite 2D space. + + Set period_x/period_y to 0 if the function is not periodic along x/y axis. + + :param VectorFunction2D function2d: The periodic 2D vector function defined in + the ([0, period_x), [0, period_y)) intervals. + :param double period_x: The period of the function along x-axis. + 0 if not periodic. + :param double period_y: The period of the function along y-axis. + 0 if not periodic. + + .. code-block:: pycon + + >>> from cherab.core.math import VectorPeriodicTransform2D + >>> + >>> def f1(x, y): + >>> return Vector3D(x, y, 0) + >>> + >>> f2 = VectorPeriodicTransform2D(f1, 1., 1.) + >>> + >>> f2(1.5, 1.5) + Vector3D(0.5, 0.5, 0) + >>> f2(-0.3, -1.3) + Vector3D(0.7, 0.7, 0) + >>> + >>> f3 = VectorPeriodicTransform2D(f1, 1., 0) + >>> + >>> f3(1.5, 1.5) + Vector3D(0.5, 1.5, 0) + >>> f3(-0.3, -1.3) + Vector3D(0.7, -1.3, 0) + """ + + def __init__(self, object function2d, double period_x, double period_y): + + if not callable(function2d): + raise TypeError("function2d is not callable.") + + self.function2d = autowrap_vectorfunction2d(function2d) + + if period_x < 0: + raise ValueError("Argument period_x must be >= 0.") + if period_y < 0: + raise ValueError("Argument period_y must be >= 0.") + + self.period_x = period_x + self.period_y = period_y + + cdef Vector3D evaluate(self, double x, double y): + """Return the value of periodic function.""" + + x = remainder(x, self.period_x) + y = remainder(y, self.period_y) + + return self.function2d.evaluate(x, y) + + +cdef class VectorPeriodicTransform3D(VectorFunction3D): + """ + Extends a periodic 3D vector function to an infinite 3D space. + + Set period_x/period_y/period_z to 0 if the function is not periodic along x/y/z axis. + + :param VectorFunction3D function3d: The periodic 3D vector function defined in the + ([0, period_x), [0, period_y), [0, period_z)) intervals. + :param double period_x: The period of the function along x-axis. + 0 if not periodic. + :param double period_y: The period of the function along y-axis. + 0 if not periodic. + :param double period_z: The period of the function along z-axis. + 0 if not periodic. + + .. code-block:: pycon + + >>> from cherab.core.math import PeriodicTransform3D + >>> + >>> def f1(x, y, z): + >>> return Vector3D(x, y, z) + >>> + >>> f2 = VectorPeriodicTransform3D(f1, 1., 1., 1.) + >>> + >>> f2(1.5, 1.5, 1.5) + Vector3D(0.5, 0.5, 0.5) + >>> f2(-0.3, -1.3, -2.3) + Vector3D(0.7, 0.7, 0.7) + >>> + >>> f3 = VectorPeriodicTransform3D(f1, 0, 1., 0) + >>> + >>> f3(1.5, 0.5, 1.5) + Vector3D(1.5, 0.5, 1.5) + """ + + def __init__(self, object function3d, double period_x, double period_y, double period_z): + + if not callable(function3d): + raise TypeError("function2d is not callable.") + + self.function3d = autowrap_vectorfunction3d(function3d) + + if period_x < 0: + raise ValueError("Argument period_x must be >= 0.") + if period_y < 0: + raise ValueError("Argument period_y must be >= 0.") + if period_z < 0: + raise ValueError("Argument period_z must be >= 0.") + + self.period_x = period_x + self.period_y = period_y + self.period_z = period_z + + cdef Vector3D evaluate(self, double x, double y, double z): + """Return the value of periodic function.""" + + x = remainder(x, self.period_x) + y = remainder(y, self.period_y) + z = remainder(z, self.period_z) + + return self.function3d.evaluate(x, y, z) diff --git a/cherab/core/model/attenuator/singleray.pyx b/cherab/core/model/attenuator/singleray.pyx index a2cfbde1..bfa7c701 100644 --- a/cherab/core/model/attenuator/singleray.pyx +++ b/cherab/core/model/attenuator/singleray.pyx @@ -18,7 +18,11 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from scipy.integrate import cumtrapz +try: + from scipy.integrate import cumulative_trapezoid +except ImportError: + from scipy.integrate import cumtrapz as cumulative_trapezoid + import numpy as np cimport numpy as np @@ -37,6 +41,21 @@ cimport cython # todo: attenuation calculation could be optimised further using memory views etc... cdef class SingleRayAttenuator(BeamAttenuator): + r""" + Calculates beam attenuation in the single-ray approximation. + Attenuation is calculated along the beam axis and extrapolated across the beam. + + :param double step: Distance between sample points along the beam axis in meters + for beam stopping calculation. Defaults to 0.01. + :param bint clamp_to_zero: Omptimises beam density calculation. + If True, the beam density outside the clamping range is zero. Defaults to False. + :param double clamp_sigma: The clamping range as a factor of beam :math:`\sigma(z)`. + Defaults to 5. + :param Beam beam: The beam instance to which this attenuator is attached. Defaults to None. + :param Plasma plasma: The plasma instance with which this beam interacts. Defaults to None. + :param AtomicData atomic_data: The atomic data provider class for this attenuator. + Defaults to None. + """ def __init__(self, double step=0.01, bint clamp_to_zero=False, double clamp_sigma=5.0, Beam beam=None, Plasma plasma=None, AtomicData atomic_data=None): @@ -86,10 +105,33 @@ cdef class SingleRayAttenuator(BeamAttenuator): @cython.cdivision(True) cpdef double density(self, double x, double y, double z) except? -1e999: - """ - Returns the beam density at the specified point. - - The point is specified in beam space. + r""" + Returns the beam density at the specified point in beam coordinate space. + The beam density is calculated as follows: + + .. math:: + n(x, y, z) = \frac{R}{2\pi v_0 \sigma_x\sigma_y} exp\left(-\frac{1}{2}\left(\frac{x^2}{\sigma_x^2}+\frac{y^2}{\sigma_y^2}\right)\right)exp\left(-\int_{0}^{z}\frac{S(z')}{v_0}dz'\right), + + \sigma_x = \sqrt{\sigma^2 + (ztg(\alpha_x))^2}\hspace{0.5cm}\sigma_y = \sqrt{\sigma^2 + (ztg(\alpha_y))^2}, + + where :math:`R=\frac{P}{E}` is the particle rate of the beam defined as the power + of the beam divided by the kinetic energy of the single particle, :math:`v_0=\sqrt{2E/m}` + is the particle speed, :math:`\sigma` is the Gaussian beam deviation at origin, + :math:`\alpha_x` and :math:`\alpha_y` are the beam divergence angles in the x and y + dimensions respectively, :math:`S(z)` is the composite beam attenuation coefficient due to + collisional-radiative interaction with the plasma species: + + .. math:: + S(z) = \sum_{i=1}^{N}Z_i n_i S_i(E_{int}, n_{i,e}^{(eq)}, T_i), + + n_{i,e}^{(eq)} = \frac{1}{Z_i}\sum_{j=1}^{N}Z_j^2 n_j. + + Here :math:`Z_i` is the charge of the i-th type of plasma ions, + :math:`n_i` is density of the i-th type of plasma ions, :math:`N` is the number of type of plasma + ions, :math:`E_{int}` is the kinetic energy of the beam atoms in the frame of reference where + ions of the i-th type are at rest, :math:`T_{i}` is the temperature of ions of the i-th type. + + The values of partial beam attenuation coefficients, :math:`S_i`, are provided by the atomic data source. :param x: x coordinate in meters. :param y: y coordinate in meters. @@ -97,7 +139,7 @@ cdef class SingleRayAttenuator(BeamAttenuator): :return: Density in m^-3. """ - cdef double sigma_x, sigma_y, norm_radius_sqr, gaussian_sample + cdef double sigma0_sqr, sigma_x, sigma_y, norm_radius_sqr, gaussian_sample # use cached data if available if self._stopping_data is None: @@ -107,8 +149,9 @@ cdef class SingleRayAttenuator(BeamAttenuator): self._calc_attenuation() # calculate beam width - sigma_x = self._beam.get_sigma() + z * self._tanxdiv - sigma_y = self._beam.get_sigma() + z * self._tanydiv + sigma0_sqr = self._beam.get_sigma()**2 + sigma_x = sqrt(sigma0_sqr + (z * self._tanxdiv)**2) + sigma_y = sqrt(sigma0_sqr + (z * self._tanydiv)**2) # normalised radius squared norm_radius_sqr = ((x / sigma_x)**2 + (y / sigma_y)**2) @@ -218,7 +261,7 @@ cdef class SingleRayAttenuator(BeamAttenuator): for i in range(naxis): stopping_coeff[i] = self._beam_stopping(x[i], y[i], z[i], beam_velocity) - return beam_density * np.exp(-cumtrapz(stopping_coeff, axis, initial=0.0) / speed) + return beam_density * np.exp(-cumulative_trapezoid(stopping_coeff, axis, initial=0) / speed) @cython.cdivision(True) cdef double _beam_stopping(self, double x, double y, double z, Vector3D beam_velocity): diff --git a/cherab/core/model/beam/__init__.pxd b/cherab/core/model/beam/__init__.pxd index 8a3c6e22..c5a0ebbe 100644 --- a/cherab/core/model/beam/__init__.pxd +++ b/cherab/core/model/beam/__init__.pxd @@ -1 +1,2 @@ from cherab.core.model.beam.charge_exchange cimport BeamCXLine +from cherab.core.model.beam.beam_emission cimport BeamEmissionLine diff --git a/cherab/core/model/beam/beam_emission.pyx b/cherab/core/model/beam/beam_emission.pyx index 8ca55331..e00aaf5d 100644 --- a/cherab/core/model/beam/beam_emission.pyx +++ b/cherab/core/model/beam/beam_emission.pyx @@ -1,8 +1,8 @@ # cython: language_level=3 -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -22,7 +22,8 @@ cimport cython from libc.math cimport sqrt from raysect.core cimport Point3D, Vector3D -from cherab.core cimport Species, Plasma, Beam, Element, BeamEmissionPEC, Spectrum, AtomicData +from raysect.optical cimport Spectrum +from cherab.core cimport Species, Plasma, Beam, Element, BeamEmissionPEC, AtomicData from cherab.core.math.function cimport autowrap_function1d, autowrap_function2d from cherab.core.atomic.elements import Isotope, hydrogen from cherab.core.model.lineshape cimport BeamEmissionMultiplet @@ -211,8 +212,8 @@ cdef class BeamEmissionLine(BeamModel): self._rates_list.append((species, rate)) # instance line shape renderer - self._lineshape = BeamEmissionMultiplet(self._line, self._wavelength, self._beam, self._sigma_to_pi, - self._sigma1_to_sigma0, self._pi2_to_pi3, self._pi4_to_pi3) + self._lineshape = BeamEmissionMultiplet(self._line, self._wavelength, self._beam, self._atomic_data, + self._sigma_to_pi, self._sigma1_to_sigma0, self._pi2_to_pi3, self._pi4_to_pi3) def _change(self): diff --git a/cherab/core/model/beam/charge_exchange.pxd b/cherab/core/model/beam/charge_exchange.pxd index 4d9220e5..46e22891 100644 --- a/cherab/core/model/beam/charge_exchange.pxd +++ b/cherab/core/model/beam/charge_exchange.pxd @@ -21,6 +21,7 @@ from raysect.optical cimport Node, World, Primitive, Ray, Spectrum, SpectralFunc from cherab.core cimport Species, Plasma, Beam, Line, AtomicData, BeamCXPEC from cherab.core.beam cimport BeamModel +from cherab.core.model.lineshape cimport LineShapeModel cdef class BeamCXLine(BeamModel): @@ -31,9 +32,11 @@ cdef class BeamCXLine(BeamModel): double _wavelength BeamCXPEC _ground_beam_rate list _excited_beam_data + LineShapeModel _lineshape + object _lineshape_class, _lineshape_args, _lineshape_kwargs cdef double _composite_cx_rate(self, double x, double y, double z, double interaction_energy, - Vector3D donor_velocity, double receiver_temperature, double receiver_density) except? -1e999 + Vector3D donor_velocity, double receiver_temperature) except? -1e999 cdef double _beam_population(self, double x, double y, double z, Vector3D beam_velocity, list population_data) except? -1e999 diff --git a/cherab/core/model/beam/charge_exchange.pyx b/cherab/core/model/beam/charge_exchange.pyx index 5cefa3d6..4b5c292e 100644 --- a/cherab/core/model/beam/charge_exchange.pyx +++ b/cherab/core/model/beam/charge_exchange.pyx @@ -1,8 +1,8 @@ # cython: language_level=3 -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -28,7 +28,7 @@ cimport cython from raysect.optical.material.emitter.inhomogeneous import NumericalIntegrator from cherab.core cimport Species, Plasma, Beam, Element, BeamPopulationRate -from cherab.core.model.lineshape cimport doppler_shift, thermal_broadening, add_gaussian_line +from cherab.core.model.lineshape cimport GaussianLine from cherab.core.utility.constants cimport RECIP_4_PI, ELEMENTARY_CHARGE, ATOMIC_MASS cdef double RECIP_ELEMENTARY_CHARGE = 1 / ELEMENTARY_CHARGE @@ -44,24 +44,59 @@ cdef double ms_to_evamu(double x): cdef class BeamCXLine(BeamModel): - """Calculates CX emission for a beam. - - :param line: - :param step: integration step in meters - :return: + """ + Calculates emission produced by charge-exchange of plasma ions + with beam species. + + :param Line line: The emission line object. + :param Beam beam: The beam object. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param object lineshape: The spectral line shape class. Must be a subclass of `LineShapeModel`. + Defaults to `GaussianLine`. + :param object lineshape_args: The arguments of spectral line shape class. Defaults is None. + :param object lineshape_kwargs: The keyword arguments of spectral line shape class. + Defaults is None. + + :ivar Line line: The emission line object. + + .. code-block:: pycon + + >>> from cherab.core.model import BeamCXLine + >>> from cherab.core.atomic import carbon + >>> from cherab.core.model import ParametrisedZeemanTriplet + >>> + >>> cVI_8_7 = Line(carbon, 5, (8, 7)) # emission line + >>> # define plasma, beam and atomic data, plasma mast contain C6+ ions. + >>> ... + >>> # here we override default line shape class, GaussianLine, + >>> # with ParametrisedZeemanTriplet to take into account Zeeman splitting. + >>> beam_cx_line = BeamCXLine(cVI_8_7, lineshape=ParametrisedZeemanTriplet) + >>> beam.models = [beam_cx_line] """ - def __init__(self, Line line not None, Beam beam=None, Plasma plasma=None, AtomicData atomic_data=None): + def __init__(self, Line line not None, Beam beam=None, Plasma plasma=None, AtomicData atomic_data=None, + object lineshape=None, object lineshape_args=None, object lineshape_kwargs=None): super().__init__(beam, plasma, atomic_data) self._line = line - # initialise cache to empty - self._target_species = None - self._wavelength = 0.0 - self._ground_beam_rate = None - self._excited_beam_data = None + self._lineshape_class = lineshape or GaussianLine + if not issubclass(self._lineshape_class, LineShapeModel): + raise TypeError("The attribute lineshape must be a subclass of LineShapeModel.") + + if lineshape_args: + self._lineshape_args = lineshape_args + else: + self._lineshape_args = [] + if lineshape_kwargs: + self._lineshape_kwargs = lineshape_kwargs + else: + self._lineshape_kwargs = {} + + # ensure that cache is initialised + self._change() @property def line(self): @@ -85,9 +120,9 @@ cdef class BeamCXLine(BeamModel): cdef: double x, y, z double donor_density - double receiver_temperature, receiver_density, receiver_ion_mass, interaction_speed, interaction_energy, emission_rate + double receiver_temperature, receiver_density, interaction_speed, interaction_energy, emission_rate Vector3D receiver_velocity, donor_velocity, interaction_velocity - double natural_wavelength, central_wavelength, radiance, sigma + double radiance # cache data on first run if self._target_species is None: @@ -116,7 +151,6 @@ cdef class BeamCXLine(BeamModel): return spectrum receiver_velocity = self._target_species.distribution.bulk_velocity(x, y, z) - receiver_ion_mass = self._target_species.element.atomic_weight donor_velocity = beam_direction.normalise().mul(evamu_to_ms(self._beam.get_energy())) @@ -125,22 +159,18 @@ cdef class BeamCXLine(BeamModel): interaction_energy = ms_to_evamu(interaction_speed) # calculate the composite charge-exchange emission coefficient - emission_rate = self._composite_cx_rate(x, y, z, interaction_energy, donor_velocity, receiver_temperature, receiver_density) - - # calculate emission line central wavelength, doppler shifted along observation direction - natural_wavelength = self._wavelength - central_wavelength = doppler_shift(natural_wavelength, observation_direction, receiver_velocity) + emission_rate = self._composite_cx_rate(x, y, z, interaction_energy, donor_velocity, receiver_temperature) # spectral line emission in W/m^3/str radiance = RECIP_4_PI * donor_density * receiver_density * emission_rate - sigma = thermal_broadening(natural_wavelength, receiver_temperature, receiver_ion_mass) - return add_gaussian_line(radiance, central_wavelength, sigma, spectrum) + + return self._lineshape.add_line(radiance, plasma_point, observation_direction, spectrum) @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) cdef double _composite_cx_rate(self, double x, double y, double z, double interaction_energy, - Vector3D donor_velocity, double receiver_temperature, double receiver_density) except? -1e999: + Vector3D donor_velocity, double receiver_temperature) except? -1e999: """ Performs a beam population weighted average of the effective cx rates. @@ -159,23 +189,26 @@ cdef class BeamCXLine(BeamModel): :param interaction_energy: The donor-receiver interaction energy in eV/amu. :param donor_velocity: A Vector defining the donor particle velocity in m/s. :param receiver_temperature: The receiver species temperature in eV. - :param receiver_density: The receiver species density in m^-3 :return: The composite charge exchange rate in W.m^3. """ cdef: - double z_effective, b_field, rate, total_population, population, effective_rate + double ion_density, z_effective + double b_field + double rate, effective_rate + double population, total_population BeamCXPEC cx_rate list population_data - # calculate z_effective and the B-field magnitude + # calculate ion density, z_effective and the B-field magnitude + ion_density = self._plasma.ion_density(x, y, z) z_effective = self._plasma.z_effective(x, y, z) b_field = self._plasma.get_b_field().evaluate(x, y, z).get_length() # rate for the ground state (metastable = 1) rate = self._ground_beam_rate.evaluate(interaction_energy, receiver_temperature, - receiver_density, + ion_density, z_effective, b_field) @@ -190,7 +223,7 @@ cdef class BeamCXLine(BeamModel): effective_rate = cx_rate.evaluate(interaction_energy, receiver_temperature, - receiver_density, + ion_density, z_effective, b_field) @@ -328,6 +361,10 @@ cdef class BeamCXLine(BeamModel): # link each rate with its population data self._excited_beam_data.append((rate, population_data)) + # instance line shape renderer + self._lineshape = self._lineshape_class(self._line, self._wavelength, self._target_species, self._plasma, self._atomic_data, + *self._lineshape_args, **self._lineshape_kwargs) + def _change(self): # clear cache to force regeneration on first use @@ -335,4 +372,3 @@ cdef class BeamCXLine(BeamModel): self._wavelength = 0.0 self._ground_beam_rate = None self._excited_beam_data = None - diff --git a/cherab/core/model/laser/model.pyx b/cherab/core/model/laser/model.pyx index dbab8803..5c78a055 100644 --- a/cherab/core/model/laser/model.pyx +++ b/cherab/core/model/laser/model.pyx @@ -30,7 +30,7 @@ from cherab.core.utility.constants cimport SPEED_OF_LIGHT, ELECTRON_CLASSICAL_RA cdef class SeldenMatobaThomsonSpectrum(LaserModel): - """ + r""" Thomson Scattering based on Selden-Matoba. The class calculates Thomson scattering of the laser to the spectrum. The model of the scattered spectrum used is based on diff --git a/cherab/core/model/lineshape.pxd b/cherab/core/model/lineshape.pxd deleted file mode 100644 index 2591efe3..00000000 --- a/cherab/core/model/lineshape.pxd +++ /dev/null @@ -1,112 +0,0 @@ -# cython: language_level=3 - -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas -# -# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the -# European Commission - subsequent versions of the EUPL (the "Licence"); -# You may not use this work except in compliance with the Licence. -# You may obtain a copy of the Licence at: -# -# https://joinup.ec.europa.eu/software/page/eupl5 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. -# -# See the Licence for the specific language governing permissions and limitations -# under the Licence. - -import numpy as np -cimport numpy as np - -from raysect.optical cimport Spectrum, Point3D, Vector3D -from cherab.core cimport Line, Species, Plasma, Beam -from cherab.core.math cimport Function1D, Function2D -from cherab.core.math.integrators cimport Integrator1D -from cherab.core.atomic.zeeman cimport ZeemanStructure - - -cpdef double doppler_shift(double wavelength, Vector3D observation_direction, Vector3D velocity) - -cpdef double thermal_broadening(double wavelength, double temperature, double atomic_weight) - -cpdef Spectrum add_gaussian_line(double radiance, double wavelength, double sigma, Spectrum spectrum) - - -cdef class LineShapeModel: - - cdef: - Line line - double wavelength - Species target_species - Plasma plasma - Integrator1D integrator - - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum) - - -cdef class GaussianLine(LineShapeModel): - pass - - -cdef class MultipletLineShape(LineShapeModel): - - cdef: - int _number_of_lines - np.ndarray _multiplet - double[:,::1] _multiplet_mv - - -cdef class StarkBroadenedLine(LineShapeModel): - - cdef double _aij, _bij, _cij - - pass - - -cdef class ZeemanLineShapeModel(LineShapeModel): - - cdef double _polarisation - - pass - - -cdef class ZeemanTriplet(ZeemanLineShapeModel): - - pass - - -cdef class ParametrisedZeemanTriplet(ZeemanLineShapeModel): - - cdef double _alpha, _beta, _gamma - - pass - - -cdef class ZeemanMultiplet(ZeemanLineShapeModel): - - cdef ZeemanStructure _zeeman_structure - - pass - - -cdef class BeamLineShapeModel: - - cdef: - - Line line - double wavelength - Beam beam - - cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, - Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum) - - -cdef class BeamEmissionMultiplet(BeamLineShapeModel): - - cdef: - - Function2D _sigma_to_pi - Function1D _sigma1_to_sigma0, _pi2_to_pi3, _pi4_to_pi3 diff --git a/cherab/core/model/lineshape.pyx b/cherab/core/model/lineshape.pyx deleted file mode 100644 index 69278560..00000000 --- a/cherab/core/model/lineshape.pyx +++ /dev/null @@ -1,968 +0,0 @@ -# cython: language_level=3 - -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas -# -# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the -# European Commission - subsequent versions of the EUPL (the "Licence"); -# You may not use this work except in compliance with the Licence. -# You may obtain a copy of the Licence at: -# -# https://joinup.ec.europa.eu/software/page/eupl5 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. -# -# See the Licence for the specific language governing permissions and limitations -# under the Licence. - -import numpy as np -from scipy.special import hyp2f1 - -cimport numpy as np -from libc.math cimport sqrt, erf, M_SQRT2, floor, ceil, fabs -from raysect.optical.spectrum cimport new_spectrum -from raysect.core.math.function.float cimport Function1D - -from cherab.core cimport Plasma -from cherab.core.atomic.elements import hydrogen, deuterium, tritium, helium, helium3, beryllium, boron, carbon, nitrogen, oxygen, neon -from cherab.core.math.function cimport autowrap_function1d, autowrap_function2d -from cherab.core.math.integrators cimport GaussianQuadrature -from cherab.core.utility.constants cimport ATOMIC_MASS, ELEMENTARY_CHARGE, SPEED_OF_LIGHT - -cimport cython - -# required by numpy c-api -np.import_array() - - -cdef double RECIP_ATOMIC_MASS = 1 / ATOMIC_MASS - - -cdef double evamu_to_ms(double x): - return sqrt(2 * x * ELEMENTARY_CHARGE * RECIP_ATOMIC_MASS) - - -@cython.cdivision(True) -cpdef double doppler_shift(double wavelength, Vector3D observation_direction, Vector3D velocity): - """ - Calculates the Doppler shifted wavelength for a given velocity and observation direction. - - :param wavelength: The wavelength to Doppler shift in nanometers. - :param observation_direction: A Vector defining the direction of observation. - :param velocity: A Vector defining the relative velocity of the emitting source in m/s. - :return: The Doppler shifted wavelength in nanometers. - """ - cdef double projected_velocity - - # flow velocity projected on the direction of observation - observation_direction = observation_direction.normalise() - projected_velocity = velocity.dot(observation_direction) - - return wavelength * (1 + projected_velocity / SPEED_OF_LIGHT) - - -@cython.cdivision(True) -cpdef double thermal_broadening(double wavelength, double temperature, double atomic_weight): - """ - Returns the line width for a gaussian line as a standard deviation. - - :param wavelength: Central wavelength. - :param temperature: Temperature in eV. - :param atomic_weight: Atomic weight in AMU. - :return: Standard deviation of gaussian line. - """ - - # todo: add input sanity checks - return sqrt(temperature * ELEMENTARY_CHARGE / (atomic_weight * ATOMIC_MASS)) * wavelength / SPEED_OF_LIGHT - - -# the number of standard deviations outside the rest wavelength the line is considered to add negligible value (including a margin for safety) -DEF GAUSSIAN_CUTOFF_SIGMA = 10.0 - - -@cython.cdivision(True) -@cython.initializedcheck(False) -@cython.boundscheck(False) -@cython.wraparound(False) -cpdef Spectrum add_gaussian_line(double radiance, double wavelength, double sigma, Spectrum spectrum): - r""" - Adds a Gaussian line to the given spectrum and returns the new spectrum. - - The formula used is based on the following definite integral: - :math:`\frac{1}{\sigma \sqrt{2 \pi}} \int_{\lambda_0}^{\lambda_1} \exp(-\frac{(x-\mu)^2}{2\sigma^2}) dx = \frac{1}{2} \left[ -Erf(\frac{a-\mu}{\sqrt{2}\sigma}) +Erf(\frac{b-\mu}{\sqrt{2}\sigma}) \right]` - - :param float radiance: Intensity of the line in radiance. - :param float wavelength: central wavelength of the line in nm. - :param float sigma: width of the line in nm. - :param Spectrum spectrum: the current spectrum to which the gaussian line is added. - :return: - """ - - cdef double temp - cdef double cutoff_lower_wavelength, cutoff_upper_wavelength - cdef double lower_wavelength, upper_wavelength - cdef double lower_integral, upper_integral - cdef int start, end, i - - if sigma <= 0: - return spectrum - - # calculate and check end of limits - cutoff_lower_wavelength = wavelength - GAUSSIAN_CUTOFF_SIGMA * sigma - if spectrum.max_wavelength < cutoff_lower_wavelength: - return spectrum - - cutoff_upper_wavelength = wavelength + GAUSSIAN_CUTOFF_SIGMA * sigma - if spectrum.min_wavelength > cutoff_upper_wavelength: - return spectrum - - # locate range of bins where there is significant contribution from the gaussian (plus a health margin) - start = max(0, floor((cutoff_lower_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) - end = min(spectrum.bins, ceil((cutoff_upper_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) - - # add line to spectrum - temp = 1 / (M_SQRT2 * sigma) - lower_wavelength = spectrum.min_wavelength + start * spectrum.delta_wavelength - lower_integral = erf((lower_wavelength - wavelength) * temp) - for i in range(start, end): - - upper_wavelength = spectrum.min_wavelength + spectrum.delta_wavelength * (i + 1) - upper_integral = erf((upper_wavelength - wavelength) * temp) - - spectrum.samples_mv[i] += radiance * 0.5 * (upper_integral - lower_integral) / spectrum.delta_wavelength - - lower_wavelength = upper_wavelength - lower_integral = upper_integral - - return spectrum - - -cdef class LineShapeModel: - """ - A base class for building line shapes. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param Integrator1D integrator: Integrator1D instance to integrate the line shape - over the spectral bin. Default is None. - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, Integrator1D integrator=None): - - self.line = line - self.wavelength = wavelength - self.target_species = target_species - self.plasma = plasma - self.integrator = integrator - - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - raise NotImplementedError('Child lineshape class must implement this method.') - - -cdef class GaussianLine(LineShapeModel): - """ - Produces Gaussian line shape. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - - .. code-block:: pycon - - >>> from cherab.core.atomic import Line, deuterium - >>> from cherab.core.model import ExcitationLine, GaussianLine - >>> - >>> # Adding Gaussian line to the plasma model. - >>> d_alpha = Line(deuterium, 0, (3, 2)) - >>> excit = ExcitationLine(d_alpha, lineshape=GaussianLine) - >>> plasma.models.add(excit) - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma): - - super().__init__(line, wavelength, target_species, plasma) - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef double ts, sigma, shifted_wavelength - cdef Vector3D ion_velocity - - ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) - if ts <= 0.0: - return spectrum - - ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) - - # calculate emission line central wavelength, doppler shifted along observation direction - shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) - - # calculate the line width - sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) - - return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) - - -DEF MULTIPLET_WAVELENGTH = 0 -DEF MULTIPLET_RATIO = 1 - - -cdef class MultipletLineShape(LineShapeModel): - """ - Produces Multiplet line shapes. - - The lineshape radiance is calculated from a base PEC rate that is unresolved. This - radiance is then divided over a number of components as specified in the multiplet - argument. The multiplet components are specified with an Nx2 array where N is the - number of components in the multiplet. The first axis of the array contains the - wavelengths of each component, the second contains the line ratio for each component. - The component line ratios must sum to one. For example: - - :param Line line: The emission line object for the base rate radiance calculation. - :param float wavelength: The rest wavelength of the base emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param multiplet: An Nx2 array that specifies the multiplet wavelengths and line ratios. - - .. code-block:: pycon - - >>> from cherab.core.atomic import Line, nitrogen - >>> from cherab.core.model import ExcitationLine, MultipletLineShape - >>> - >>> # multiplet specification in Nx2 array - >>> multiplet = [[403.509, 404.132, 404.354, 404.479, 405.692], [0.205, 0.562, 0.175, 0.029, 0.029]] - >>> - >>> # Adding the multiplet to the plasma model. - >>> nitrogen_II_404 = Line(nitrogen, 1, ("2s2 2p1 4f1 3G13.0", "2s2 2p1 3d1 3F10.0")) - >>> excit = ExcitationLine(nitrogen_II_404, lineshape=MultipletLineShape, lineshape_args=[multiplet]) - >>> plasma.models.add(excit) - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, - object multiplet): - - super().__init__(line, wavelength, target_species, plasma) - - multiplet = np.array(multiplet, dtype=np.float64) - - if not (len(multiplet.shape) == 2 and multiplet.shape[0] == 2): - raise ValueError("The multiplet specification must be an array of shape (Nx2).") - - if not multiplet[1,:].sum() == 1.0: - raise ValueError("The multiplet line ratios should sum to one.") - - self._number_of_lines = multiplet.shape[1] - self._multiplet = multiplet - self._multiplet_mv = self._multiplet - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef double ts, sigma, shifted_wavelength, component_wavelength, component_radiance - cdef Vector3D ion_velocity - - ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) - if ts <= 0.0: - return spectrum - - ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) - - # calculate the line width - sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) - - for i in range(self._number_of_lines): - - component_wavelength = self._multiplet_mv[MULTIPLET_WAVELENGTH, i] - component_radiance = radiance * self._multiplet_mv[MULTIPLET_RATIO, i] - - # calculate emission line central wavelength, doppler shifted along observation direction - shifted_wavelength = doppler_shift(component_wavelength, direction, ion_velocity) - - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - return spectrum - - -DEF LORENZIAN_CUTOFF_GAMMA = 50.0 - - -cdef class StarkFunction(Function1D): - """ - Normalised Stark function for the StarkBroadenedLine line shape. - """ - - cdef double _a, _x0, _norm - - STARK_NORM_COEFFICIENT = 4 * LORENZIAN_CUTOFF_GAMMA * hyp2f1(0.4, 1, 1.4, -(2 * LORENZIAN_CUTOFF_GAMMA)**2.5) - - def __init__(self, double wavelength, double lambda_1_2): - - if wavelength <= 0: - raise ValueError("Argument 'wavelength' must be positive.") - - if lambda_1_2 <= 0: - raise ValueError("Argument 'lambda_1_2' must be positive.") - - self._x0 = wavelength - self._a = (0.5 * lambda_1_2)**2.5 - # normalise, so the integral over x is equal to 1 in the limits - # (_x0 - LORENZIAN_CUTOFF_GAMMA * lambda_1_2, _x0 + LORENZIAN_CUTOFF_GAMMA * lambda_1_2) - self._norm = (0.5 * lambda_1_2)**1.5 / self.STARK_NORM_COEFFICIENT - - @cython.cdivision(True) - cdef double evaluate(self, double x) except? -1e999: - - return self._norm / ((fabs(x - self._x0))**2.5 + self._a) - - -cdef class StarkBroadenedLine(LineShapeModel): - """ - Parametrised Stark broadened line shape based on the Model Microfield Method (MMM). - Contains embedded atomic data in the form of fits to MMM. - Only Balmer and Paschen series are supported by default. - See B. Lomanowski, et al. "Inferring divertor plasma properties from hydrogen Balmer - and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) - `123028 `_. - - Call `show_supported_transitions()` to see the list of supported transitions and - default model coefficients. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param dict stark_model_coefficients: Alternative model coefficients in the form - {line_ij: (c_ij, a_ij, b_ij), ...}. - If None, the default model parameters will be used. - :param Integrator1D integrator: Integrator1D instance to integrate the line shape - over the spectral bin. Default is `GaussianQuadrature()`. - - """ - - STARK_MODEL_COEFFICIENTS_DEFAULT = { - Line(hydrogen, 0, (3, 2)): (3.71e-18, 0.7665, 0.064), - Line(hydrogen, 0, (4, 2)): (8.425e-18, 0.7803, 0.050), - Line(hydrogen, 0, (5, 2)): (1.31e-15, 0.6796, 0.030), - Line(hydrogen, 0, (6, 2)): (3.954e-16, 0.7149, 0.028), - Line(hydrogen, 0, (7, 2)): (6.258e-16, 0.712, 0.029), - Line(hydrogen, 0, (8, 2)): (7.378e-16, 0.7159, 0.032), - Line(hydrogen, 0, (9, 2)): (8.947e-16, 0.7177, 0.033), - Line(hydrogen, 0, (4, 3)): (1.330e-16, 0.7449, 0.045), - Line(hydrogen, 0, (5, 3)): (6.64e-16, 0.7356, 0.044), - Line(hydrogen, 0, (6, 3)): (2.481e-15, 0.7118, 0.016), - Line(hydrogen, 0, (7, 3)): (3.270e-15, 0.7137, 0.029), - Line(hydrogen, 0, (8, 3)): (4.343e-15, 0.7133, 0.032), - Line(hydrogen, 0, (9, 3)): (5.588e-15, 0.7165, 0.033), - Line(deuterium, 0, (3, 2)): (3.71e-18, 0.7665, 0.064), - Line(deuterium, 0, (4, 2)): (8.425e-18, 0.7803, 0.050), - Line(deuterium, 0, (5, 2)): (1.31e-15, 0.6796, 0.030), - Line(deuterium, 0, (6, 2)): (3.954e-16, 0.7149, 0.028), - Line(deuterium, 0, (7, 2)): (6.258e-16, 0.712, 0.029), - Line(deuterium, 0, (8, 2)): (7.378e-16, 0.7159, 0.032), - Line(deuterium, 0, (9, 2)): (8.947e-16, 0.7177, 0.033), - Line(deuterium, 0, (4, 3)): (1.330e-16, 0.7449, 0.045), - Line(deuterium, 0, (5, 3)): (6.64e-16, 0.7356, 0.044), - Line(deuterium, 0, (6, 3)): (2.481e-15, 0.7118, 0.016), - Line(deuterium, 0, (7, 3)): (3.270e-15, 0.7137, 0.029), - Line(deuterium, 0, (8, 3)): (4.343e-15, 0.7133, 0.032), - Line(deuterium, 0, (9, 3)): (5.588e-15, 0.7165, 0.033), - Line(tritium, 0, (3, 2)): (3.71e-18, 0.7665, 0.064), - Line(tritium, 0, (4, 2)): (8.425e-18, 0.7803, 0.050), - Line(tritium, 0, (5, 2)): (1.31e-15, 0.6796, 0.030), - Line(tritium, 0, (6, 2)): (3.954e-16, 0.7149, 0.028), - Line(tritium, 0, (7, 2)): (6.258e-16, 0.712, 0.029), - Line(tritium, 0, (8, 2)): (7.378e-16, 0.7159, 0.032), - Line(tritium, 0, (9, 2)): (8.947e-16, 0.7177, 0.033), - Line(tritium, 0, (4, 3)): (1.330e-16, 0.7449, 0.045), - Line(tritium, 0, (5, 3)): (6.64e-16, 0.7356, 0.044), - Line(tritium, 0, (6, 3)): (2.481e-15, 0.7118, 0.016), - Line(tritium, 0, (7, 3)): (3.270e-15, 0.7137, 0.029), - Line(tritium, 0, (8, 3)): (4.343e-15, 0.7133, 0.032), - Line(tritium, 0, (9, 3)): (5.588e-15, 0.7165, 0.033) - } - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, - dict stark_model_coefficients=None, integrator=GaussianQuadrature()): - - stark_model_coefficients = stark_model_coefficients or self.STARK_MODEL_COEFFICIENTS_DEFAULT - - try: - # Fitted Stark Constants - cij, aij, bij = stark_model_coefficients[line] - if cij <= 0: - raise ValueError('Coefficient c_ij must be positive.') - if aij <= 0: - raise ValueError('Coefficient a_ij must be positive.') - if bij <= 0: - raise ValueError('Coefficient b_ij must be positive.') - self._aij = aij - self._bij = bij - self._cij = cij - except IndexError: - raise ValueError('Stark broadening coefficients for {} is not currently available.'.format(line)) - - super().__init__(line, wavelength, target_species, plasma, integrator) - - def show_supported_transitions(self): - """ Prints all supported transitions.""" - for line, coeff in self.STARK_MODEL_COEFFICIENTS_DEFAULT.items(): - print('{}: c_ij={}, a_ij={}, b_ij={}'.format(line, coeff[0], coeff[1], coeff[2])) - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef: - double ne, te, lambda_1_2, lambda_5_2, wvl - double cutoff_lower_wavelength, cutoff_upper_wavelength - double lower_wavelength, upper_wavelength - double bin_integral - int start, end, i - Spectrum raw_lineshape - - ne = self.plasma.get_electron_distribution().density(point.x, point.y, point.z) - if ne <= 0.0: - return spectrum - - te = self.plasma.get_electron_distribution().effective_temperature(point.x, point.y, point.z) - if te <= 0.0: - return spectrum - - lambda_1_2 = self._cij * ne**self._aij / (te**self._bij) - - self.integrator.function = StarkFunction(self.wavelength, lambda_1_2) - - # calculate and check end of limits - cutoff_lower_wavelength = self.wavelength - LORENZIAN_CUTOFF_GAMMA * lambda_1_2 - if spectrum.max_wavelength < cutoff_lower_wavelength: - return spectrum - - cutoff_upper_wavelength = self.wavelength + LORENZIAN_CUTOFF_GAMMA * lambda_1_2 - if spectrum.min_wavelength > cutoff_upper_wavelength: - return spectrum - - # locate range of bins where there is significant contribution from the gaussian (plus a health margin) - start = max(0, floor((cutoff_lower_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) - end = min(spectrum.bins, ceil((cutoff_upper_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) - - # add line to spectrum - lower_wavelength = spectrum.min_wavelength + start * spectrum.delta_wavelength - - for i in range(start, end): - upper_wavelength = spectrum.min_wavelength + spectrum.delta_wavelength * (i + 1) - - bin_integral = self.integrator.evaluate(lower_wavelength, upper_wavelength) - spectrum.samples_mv[i] += radiance * bin_integral / spectrum.delta_wavelength - - lower_wavelength = upper_wavelength - - return spectrum - - -DEF BOHR_MAGNETON = 5.78838180123e-5 # in eV/T -DEF HC_EV_NM = 1239.8419738620933 # (Planck constant in eV s) x (speed of light in nm/s) - -DEF PI_POLARISATION = 0 -DEF SIGMA_POLARISATION = 1 -DEF SIGMA_PLUS_POLARISATION = 1 -DEF SIGMA_MINUS_POLARISATION = -1 -DEF NO_POLARISATION = 2 - - -cdef class ZeemanLineShapeModel(LineShapeModel): - r""" - A base class for building Zeeman line shapes. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: - "pi" - leave only :math:`\pi`-polarised components, - "sigma" - leave only :math:`\sigma`-polarised components, - "no" - leave all components (default). - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, polarisation): - super().__init__(line, wavelength, target_species, plasma) - - self.polarisation = polarisation - - @property - def polarisation(self): - if self._polarisation == PI_POLARISATION: - return 'pi' - if self._polarisation == SIGMA_POLARISATION: - return 'sigma' - if self._polarisation == NO_POLARISATION: - return 'no' - - @polarisation.setter - def polarisation(self, value): - if value.lower() == 'pi': - self._polarisation = PI_POLARISATION - elif value.lower() == 'sigma': - self._polarisation = SIGMA_POLARISATION - elif value.lower() == 'no': - self._polarisation = NO_POLARISATION - else: - raise ValueError('Select between "pi", "sigma" or "no", {} is unsupported.'.format(value)) - - -cdef class ZeemanTriplet(ZeemanLineShapeModel): - r""" - Simple Doppler-Zeeman triplet (Paschen-Back effect). - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: - "pi" - leave central component, - "sigma" - leave side components, - "no" - all components (default). - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, polarisation='no'): - - super().__init__(line, wavelength, target_species, plasma, polarisation) - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef double ts, sigma, shifted_wavelength, photon_energy, b_magn, component_radiance, cos_sqr, sin_sqr - cdef Vector3D ion_velocity, b_field - - ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) - if ts <= 0.0: - return spectrum - - ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) - - # calculate emission line central wavelength, doppler shifted along observation direction - shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) - - # calculate the line width - sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) - - # obtain magnetic field - b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) - b_magn = b_field.get_length() - - if b_magn == 0: - # no splitting if magnetic field strength is zero - if self._polarisation == NO_POLARISATION: - return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) - - return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) - - # coefficients for intensities parallel and perpendicular to magnetic field - cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 - sin_sqr = 1. - cos_sqr - - # adding pi component of the Zeeman triplet in case of NO_POLARISATION or PI_POLARISATION - if self._polarisation != SIGMA_POLARISATION: - component_radiance = 0.5 * sin_sqr * radiance - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - # adding sigma +/- components of the Zeeman triplet in case of NO_POLARISATION or SIGMA_POLARISATION - if self._polarisation != PI_POLARISATION: - component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance - - photon_energy = HC_EV_NM / self.wavelength - - shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy - BOHR_MAGNETON * b_magn), direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy + BOHR_MAGNETON * b_magn), direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - return spectrum - - -cdef class ParametrisedZeemanTriplet(ZeemanLineShapeModel): - r""" - Parametrised Doppler-Zeeman triplet. It takes into account additional broadening due to - the line's fine structure without resolving the individual components of the fine - structure. The model is described with three parameters: :math:`\alpha`, - :math:`\beta` and :math:`\gamma`. - - The distance between :math:`\sigma^+` and :math:`\sigma^-` peaks: - :math:`\Delta \lambda_{\sigma} = \alpha B`, - where `B` is the magnetic field strength. - The ratio between Zeeman and thermal broadening line widths: - :math:`\frac{W_{Zeeman}}{W_{Doppler}} = \beta T^{\gamma}`, - where `T` is the species temperature in eV. - - Call `show_supported_transitions()` to see the list of supported transitions and - default parameters of the model. - - For details see A. Blom and C. Jupén, Parametrisation of the Zeeman effect - for hydrogen-like spectra in high-temperature plasmas, - Plasma Phys. Control. Fusion 44 (2002) `1229-1241 - `_. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param dict line_parameters: Alternative parameters of the model in the form - {line_i: (alpha_i, beta_i, gamma_i), ...}. - If None, the default model parameters will be used. - :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: - "pi" - leave central component, - "sigma" - leave side components, - "no" - all components (default). - """ - - LINE_PARAMETERS_DEFAULT = { # alpha, beta, gamma parameters for selected lines - Line(hydrogen, 0, (3, 2)): (0.0402267, 0.3415, -0.5247), - Line(hydrogen, 0, (4, 2)): (0.0220724, 0.2837, -0.5346), - Line(deuterium, 0, (3, 2)): (0.0402068, 0.4384, -0.5015), - Line(deuterium, 0, (4, 2)): (0.0220610, 0.3702, -0.5132), - Line(helium3, 1, (4, 3)): (0.0205200, 1.4418, -0.4892), - Line(helium3, 1, (5, 3)): (0.0095879, 1.2576, -0.5001), - Line(helium3, 1, (6, 4)): (0.0401980, 0.8976, -0.4971), - Line(helium3, 1, (7, 4)): (0.0273538, 0.8529, -0.5039), - Line(helium, 1, (4, 3)): (0.0205206, 1.6118, -0.4838), - Line(helium, 1, (5, 3)): (0.0095879, 1.4294, -0.4975), - Line(helium, 1, (6, 4)): (0.0401955, 1.0058, -0.4918), - Line(helium, 1, (7, 4)): (0.0273521, 0.9563, -0.4981), - Line(beryllium, 3, (5, 4)): (0.0060354, 2.1245, -0.3190), - Line(beryllium, 3, (6, 5)): (0.0202754, 1.6538, -0.3192), - Line(beryllium, 3, (7, 5)): (0.0078966, 1.7017, -0.3348), - Line(beryllium, 3, (8, 6)): (0.0205025, 1.4581, -0.3450), - Line(boron, 4, (6, 5)): (0.0083423, 2.0519, -0.2960), - Line(boron, 4, (7, 6)): (0.0228379, 1.6546, -0.2941), - Line(boron, 4, (8, 6)): (0.0084065, 1.8041, -0.3177), - Line(boron, 4, (8, 7)): (0.0541883, 1.4128, -0.2966), - Line(boron, 4, (9, 7)): (0.0190781, 1.5440, -0.3211), - Line(boron, 4, (10, 8)): (0.0391914, 1.3569, -0.3252), - Line(carbon, 5, (6, 5)): (0.0040900, 2.4271, -0.2818), - Line(carbon, 5, (7, 6)): (0.0110398, 1.9785, -0.2816), - Line(carbon, 5, (8, 6)): (0.0040747, 2.1776, -0.3035), - Line(carbon, 5, (8, 7)): (0.0261405, 1.6689, -0.2815), - Line(carbon, 5, (9, 7)): (0.0092096, 1.8495, -0.3049), - Line(carbon, 5, (10, 8)): (0.0189020, 1.6191, -0.3078), - Line(carbon, 5, (11, 8)): (0.0110428, 1.6600, -0.3162), - Line(carbon, 5, (10, 9)): (0.0359009, 1.4464, -0.3104), - Line(nitrogen, 6, (7, 6)): (0.0060010, 2.4789, -0.2817), - Line(nitrogen, 6, (8, 7)): (0.0141271, 2.0249, -0.2762), - Line(nitrogen, 6, (9, 8)): (0.0300127, 1.7415, -0.2753), - Line(nitrogen, 6, (10, 8)): (0.0102089, 1.9464, -0.2975), - Line(nitrogen, 6, (11, 9)): (0.0193799, 1.7133, -0.2973), - Line(oxygen, 7, (8, 7)): (0.0083081, 2.4263, -0.2747), - Line(oxygen, 7, (9, 8)): (0.0176049, 2.0652, -0.2721), - Line(oxygen, 7, (10, 8)): (0.0059933, 2.3445, -0.2944), - Line(oxygen, 7, (10, 9)): (0.0343805, 1.8122, -0.2718), - Line(oxygen, 7, (11, 9)): (0.0113640, 2.0268, -0.2911), - Line(neon, 9, (9, 8)): (0.0072488, 2.8838, -0.2758), - Line(neon, 9, (10, 9)): (0.0141002, 2.4755, -0.2718), - Line(neon, 9, (11, 9)): (0.0046673, 2.8410, -0.2917), - Line(neon, 9, (11, 10)): (0.0257292, 2.1890, -0.2715) - } - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, dict line_parameters=None, polarisation='no'): - - super().__init__(line, wavelength, target_species, plasma, polarisation) - - line_parameters = line_parameters or self.LINE_PARAMETERS_DEFAULT - - try: - alpha, beta, gamma = line_parameters[self.line] - if alpha <= 0: - raise ValueError('Parameter alpha must be positive.') - if beta < 0: - raise ValueError('Parameter beta must be non-negative.') - self._alpha = alpha - self._beta = beta - self._gamma = gamma - - except KeyError: - raise ValueError('Data for {} is not available.'.format(self.line)) - - def show_supported_transitions(self): - """ Prints all supported transitions.""" - for line, param in self.LINE_PARAMETERS_DEFAULT.items(): - print('{}: alpha={}, beta={}, gamma={}'.format(line, param[0], param[1], param[2])) - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef double ts, sigma, shifted_wavelength, b_magn, component_radiance, cos_sqr, sin_sqr - cdef Vector3D ion_velocity, b_field - - ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) - if ts <= 0.0: - return spectrum - - ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) - - # calculate emission line central wavelength, doppler shifted along observation direction - shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) - - # calculate the line width - sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) - - # fine structure broadening correction - sigma *= sqrt(1. + self._beta * self._beta * ts**(2. * self._gamma)) - - # obtain magnetic field - b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) - b_magn = b_field.get_length() - - if b_magn == 0: - # no splitting if magnetic filed strength is zero - if self._polarisation == NO_POLARISATION: - return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) - - return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) - - # coefficients for intensities parallel and perpendicular to magnetic field - cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 - sin_sqr = 1. - cos_sqr - - # adding pi component of the Zeeman triplet in case of NO_POLARISATION or PI_POLARISATION - if self._polarisation != SIGMA_POLARISATION: - component_radiance = 0.5 * sin_sqr * radiance - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - # adding sigma +/- components of the Zeeman triplet in case of NO_POLARISATION or SIGMA_POLARISATION - if self._polarisation != PI_POLARISATION: - component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance - - shifted_wavelength = doppler_shift(self.wavelength + 0.5 * self._alpha * b_magn, direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - shifted_wavelength = doppler_shift(self.wavelength - 0.5 * self._alpha * b_magn, direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) - - return spectrum - - -cdef class ZeemanMultiplet(ZeemanLineShapeModel): - r""" - Doppler-Zeeman Multiplet. - - The lineshape radiance is calculated from a base PEC rate that is unresolved. This - radiance is then divided over a number of components as specified in the ``zeeman_structure`` - argument. The ``zeeman_structure`` specifies wavelengths and ratios of - :math:`\pi`-/:math:`\sigma`-polarised components as functions of the magnetic field strength. - These functions can be obtained using the output of the ADAS603 routines. - - :param Line line: The emission line object for the base rate radiance calculation. - :param float wavelength: The rest wavelength of the base emission line. - :param Species target_species: The target plasma species that is emitting. - :param Plasma plasma: The emitting plasma object. - :param zeeman_structure: A ``ZeemanStructure`` object that provides wavelengths and ratios - of :math:`\pi`-/:math:`\sigma^{+}`-/:math:`\sigma^{-}`-polarised - components for any given magnetic field strength. - :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: - "pi" - leave only :math:`\pi`-polarised components, - "sigma" - leave only :math:`\sigma`-polarised components, - "no" - leave all components (default). - - """ - - def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, - ZeemanStructure zeeman_structure, polarisation='no'): - - super().__init__(line, wavelength, target_species, plasma, polarisation) - - self._zeeman_structure = zeeman_structure - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): - - cdef int i - cdef double ts, sigma, shifted_wavelength, component_radiance - cdef Vector3D ion_velocity - cdef double[:, :] multiplet_pi_mv, multiplet_sigma_mv - - ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) - if ts <= 0.0: - return spectrum - - ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) - - # calculate the line width - sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) - - # obtain magnetic field - b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) - b_magn = b_field.get_length() - - if b_magn == 0: - # no splitting if magnetic filed strength is zero - shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) - if self._polarisation == NO_POLARISATION: - return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) - - return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) - - # coefficients for intensities parallel and perpendicular to magnetic field - cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 - sin_sqr = 1. - cos_sqr - - # adding pi components of the Zeeman multiplet in case of NO_POLARISATION or PI_POLARISATION - if self._polarisation != SIGMA_POLARISATION: - component_radiance = 0.5 * sin_sqr * radiance - multiplet_mv = self._zeeman_structure.evaluate(b_magn, PI_POLARISATION) - - for i in range(multiplet_mv.shape[1]): - shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) - - # adding sigma components of the Zeeman multiplet in case of NO_POLARISATION or SIGMA_POLARISATION - if self._polarisation != PI_POLARISATION: - component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance - - multiplet_mv = self._zeeman_structure.evaluate(b_magn, SIGMA_PLUS_POLARISATION) - - for i in range(multiplet_mv.shape[1]): - shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) - - multiplet_mv = self._zeeman_structure.evaluate(b_magn, SIGMA_MINUS_POLARISATION) - - for i in range(multiplet_mv.shape[1]): - shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) - spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) - - return spectrum - - -cdef class BeamLineShapeModel: - """ - A base class for building beam emission line shapes. - - :param Line line: The emission line object for this line shape. - :param float wavelength: The rest wavelength for this emission line. - :param Beam beam: The beam class that is emitting. - """ - - def __init__(self, Line line, double wavelength, Beam beam): - - self.line = line - self.wavelength = wavelength - self.beam = beam - - cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, - Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum): - raise NotImplementedError('Child lineshape class must implement this method.') - - -DEF STARK_SPLITTING_FACTOR = 2.77e-8 - - -cdef class BeamEmissionMultiplet(BeamLineShapeModel): - """ - Produces Beam Emission Multiplet line shape, also known as the Motional Stark Effect spectrum. - """ - - def __init__(self, Line line, double wavelength, Beam beam, object sigma_to_pi, - object sigma1_to_sigma0, object pi2_to_pi3, object pi4_to_pi3): - - super().__init__(line, wavelength, beam) - - self._sigma_to_pi = autowrap_function2d(sigma_to_pi) - self._sigma1_to_sigma0 = autowrap_function1d(sigma1_to_sigma0) - self._pi2_to_pi3 = autowrap_function1d(pi2_to_pi3) - self._pi4_to_pi3 = autowrap_function1d(pi4_to_pi3) - - @cython.cdivision(True) - cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, - Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum): - - cdef double x, y, z - cdef Plasma plasma - cdef double te, ne, beam_energy, sigma, stark_split, beam_ion_mass, beam_temperature - cdef double natural_wavelength, central_wavelength - cdef double sigma_to_pi, d, intensity_sig, intensity_pi, e_field - cdef double s1_to_s0, intensity_s0, intensity_s1 - cdef double pi2_to_pi3, pi4_to_pi3, intensity_pi2, intensity_pi3, intensity_pi4 - cdef Vector3D b_field, beam_velocity - - # extract for more compact code - x = plasma_point.x - y = plasma_point.y - z = plasma_point.z - - plasma = self.beam.get_plasma() - - te = plasma.get_electron_distribution().effective_temperature(x, y, z) - if te <= 0.0: - return spectrum - - ne = plasma.get_electron_distribution().density(x, y, z) - if ne <= 0.0: - return spectrum - - beam_energy = self.beam.get_energy() - - # calculate Stark splitting - b_field = plasma.get_b_field().evaluate(x, y, z) - beam_velocity = beam_direction.normalise().mul(evamu_to_ms(beam_energy)) - e_field = beam_velocity.cross(b_field).get_length() - stark_split = fabs(STARK_SPLITTING_FACTOR * e_field) # TODO - calculate splitting factor? Reject other lines? - - # calculate emission line central wavelength, doppler shifted along observation direction - natural_wavelength = self.wavelength - central_wavelength = doppler_shift(natural_wavelength, observation_direction, beam_velocity) - - # calculate doppler broadening - beam_ion_mass = self.beam.get_element().atomic_weight - beam_temperature = self.beam.get_temperature() - sigma = thermal_broadening(self.wavelength, beam_temperature, beam_ion_mass) - - # calculate relative intensities of sigma and pi lines - sigma_to_pi = self._sigma_to_pi.evaluate(ne, beam_energy) - d = 1 / (1 + sigma_to_pi) - intensity_sig = sigma_to_pi * d * radiance - intensity_pi = 0.5 * d * radiance - - # add Sigma lines to output - s1_to_s0 = self._sigma1_to_sigma0.evaluate(ne) - intensity_s0 = 1 / (s1_to_s0 + 1) - intensity_s1 = 0.5 * s1_to_s0 * intensity_s0 - - spectrum = add_gaussian_line(intensity_sig * intensity_s0, central_wavelength, sigma, spectrum) - spectrum = add_gaussian_line(intensity_sig * intensity_s1, central_wavelength + stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_sig * intensity_s1, central_wavelength - stark_split, sigma, spectrum) - - # add Pi lines to output - pi2_to_pi3 = self._pi2_to_pi3.evaluate(ne) - pi4_to_pi3 = self._pi4_to_pi3.evaluate(ne) - intensity_pi3 = 1 / (1 + pi2_to_pi3 + pi4_to_pi3) - intensity_pi2 = pi2_to_pi3 * intensity_pi3 - intensity_pi4 = pi4_to_pi3 * intensity_pi3 - - spectrum = add_gaussian_line(intensity_pi * intensity_pi2, central_wavelength + 2 * stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_pi * intensity_pi2, central_wavelength - 2 * stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_pi * intensity_pi3, central_wavelength + 3 * stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_pi * intensity_pi3, central_wavelength - 3 * stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_pi * intensity_pi4, central_wavelength + 4 * stark_split, sigma, spectrum) - spectrum = add_gaussian_line(intensity_pi * intensity_pi4, central_wavelength - 4 * stark_split, sigma, spectrum) - - return spectrum diff --git a/cherab/core/model/lineshape/__init__.pxd b/cherab/core/model/lineshape/__init__.pxd new file mode 100644 index 00000000..1eeb4f7c --- /dev/null +++ b/cherab/core/model/lineshape/__init__.pxd @@ -0,0 +1,25 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.model.lineshape.beam cimport BeamLineShapeModel, BeamEmissionMultiplet +from cherab.core.model.lineshape.base cimport LineShapeModel +from cherab.core.model.lineshape.doppler cimport doppler_shift, thermal_broadening +from cherab.core.model.lineshape.gaussian cimport add_gaussian_line, GaussianLine +from cherab.core.model.lineshape.multiplet cimport MultipletLineShape +from cherab.core.model.lineshape.stark cimport StarkBroadenedLine +from cherab.core.model.lineshape.zeeman cimport ZeemanLineShapeModel, ZeemanTriplet, ParametrisedZeemanTriplet, ZeemanMultiplet diff --git a/cherab/core/model/lineshape/__init__.py b/cherab/core/model/lineshape/__init__.py new file mode 100644 index 00000000..30100403 --- /dev/null +++ b/cherab/core/model/lineshape/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from .beam import BeamLineShapeModel, BeamEmissionMultiplet +from .base import LineShapeModel +from .doppler import doppler_shift, thermal_broadening +from .gaussian import add_gaussian_line, GaussianLine +from .multiplet import MultipletLineShape +from .stark import add_lorentzian_line, StarkBroadenedLine +from .zeeman import ZeemanLineShapeModel, ZeemanTriplet, ParametrisedZeemanTriplet, ZeemanMultiplet diff --git a/cherab/core/model/lineshape/base.pxd b/cherab/core/model/lineshape/base.pxd new file mode 100644 index 00000000..b6267d45 --- /dev/null +++ b/cherab/core/model/lineshape/base.pxd @@ -0,0 +1,39 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Spectrum, Point3D, Vector3D +from cherab.core.atomic cimport Line +from cherab.core.species cimport Species +from cherab.core.plasma cimport Plasma +from cherab.core.atomic cimport AtomicData +from cherab.core.math.integrators cimport Integrator1D + + +cdef class LineShapeModel: + + cdef: + Line line + double wavelength + Species target_species + Plasma plasma + AtomicData atomic_data + Integrator1D integrator + + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum) diff --git a/cherab/core/model/lineshape/base.pyx b/cherab/core/model/lineshape/base.pyx new file mode 100644 index 00000000..0b0cf7a2 --- /dev/null +++ b/cherab/core/model/lineshape/base.pyx @@ -0,0 +1,45 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + + +cdef class LineShapeModel: + """ + A base class for building line shapes. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param Integrator1D integrator: Integrator1D instance to integrate the line shape + over the spectral bin. Default is None. + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, Integrator1D integrator=None): + + self.line = line + self.wavelength = wavelength + self.target_species = target_species + self.plasma = plasma + self.atomic_data = atomic_data + self.integrator = integrator + + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + raise NotImplementedError('Child lineshape class must implement this method.') diff --git a/cherab/core/model/lineshape/beam/__init__.pxd b/cherab/core/model/lineshape/beam/__init__.pxd new file mode 100644 index 00000000..6172d6c3 --- /dev/null +++ b/cherab/core/model/lineshape/beam/__init__.pxd @@ -0,0 +1,20 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.model.lineshape.beam.base cimport * +from cherab.core.model.lineshape.beam.mse cimport * diff --git a/cherab/core/model/lineshape/beam/__init__.py b/cherab/core/model/lineshape/beam/__init__.py new file mode 100644 index 00000000..48d34582 --- /dev/null +++ b/cherab/core/model/lineshape/beam/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from .base import BeamLineShapeModel +from .mse import BeamEmissionMultiplet diff --git a/cherab/core/model/lineshape/beam/base.pxd b/cherab/core/model/lineshape/beam/base.pxd new file mode 100644 index 00000000..e5633446 --- /dev/null +++ b/cherab/core/model/lineshape/beam/base.pxd @@ -0,0 +1,37 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Spectrum, Point3D, Vector3D +from cherab.core.atomic cimport Line +from cherab.core.beam cimport Beam +from cherab.core.atomic cimport AtomicData + + +cdef class BeamLineShapeModel: + + cdef: + + Line line + double wavelength + Beam beam + AtomicData atomic_data + + cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, + Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum) diff --git a/cherab/core/model/lineshape/beam/base.pyx b/cherab/core/model/lineshape/beam/base.pyx new file mode 100644 index 00000000..d333dd6e --- /dev/null +++ b/cherab/core/model/lineshape/beam/base.pyx @@ -0,0 +1,41 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + + +cdef class BeamLineShapeModel: + """ + A base class for building beam emission line shapes. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Beam beam: The beam class that is emitting. + :param AtomicData atomic_data: The atomic data provider. + """ + + def __init__(self, Line line, double wavelength, Beam beam, AtomicData atomic_data): + + self.line = line + self.wavelength = wavelength + self.beam = beam + self.atomic_data = atomic_data + + cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, + Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum): + raise NotImplementedError('Child lineshape class must implement this method.') diff --git a/cherab/core/model/lineshape/beam/mse.pxd b/cherab/core/model/lineshape/beam/mse.pxd new file mode 100644 index 00000000..a7b6131a --- /dev/null +++ b/cherab/core/model/lineshape/beam/mse.pxd @@ -0,0 +1,30 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.math cimport Function1D, Function2D +from cherab.core.model.lineshape.beam.base cimport BeamLineShapeModel + + +cdef class BeamEmissionMultiplet(BeamLineShapeModel): + + cdef: + + Function2D _sigma_to_pi + Function1D _sigma1_to_sigma0, _pi2_to_pi3, _pi4_to_pi3 diff --git a/cherab/core/model/lineshape/beam/mse.pyx b/cherab/core/model/lineshape/beam/mse.pyx new file mode 100644 index 00000000..9c64d435 --- /dev/null +++ b/cherab/core/model/lineshape/beam/mse.pyx @@ -0,0 +1,135 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport fabs, sqrt +from raysect.optical cimport Spectrum, Point3D, Vector3D + +from cherab.core.plasma cimport Plasma +from cherab.core.beam cimport Beam +from cherab.core.atomic cimport AtomicData +from cherab.core.atomic cimport Line +from cherab.core.math.function cimport autowrap_function1d, autowrap_function2d +from cherab.core.utility.constants cimport ATOMIC_MASS, ELEMENTARY_CHARGE +from cherab.core.model.lineshape.gaussian cimport add_gaussian_line +from cherab.core.model.lineshape.doppler cimport thermal_broadening, doppler_shift + +cimport cython + + +cdef double RECIP_ATOMIC_MASS = 1 / ATOMIC_MASS + + +cdef double evamu_to_ms(double x): + return sqrt(2 * x * ELEMENTARY_CHARGE * RECIP_ATOMIC_MASS) + + +DEF STARK_SPLITTING_FACTOR = 2.77e-8 + + +cdef class BeamEmissionMultiplet(BeamLineShapeModel): + """ + Produces Beam Emission Multiplet line shape, also known as the Motional Stark Effect spectrum. + """ + + def __init__(self, Line line, double wavelength, Beam beam, AtomicData atomic_data, + object sigma_to_pi, object sigma1_to_sigma0, object pi2_to_pi3, object pi4_to_pi3): + + super().__init__(line, wavelength, beam, atomic_data) + + self._sigma_to_pi = autowrap_function2d(sigma_to_pi) + self._sigma1_to_sigma0 = autowrap_function1d(sigma1_to_sigma0) + self._pi2_to_pi3 = autowrap_function1d(pi2_to_pi3) + self._pi4_to_pi3 = autowrap_function1d(pi4_to_pi3) + + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D beam_point, Point3D plasma_point, + Vector3D beam_direction, Vector3D observation_direction, Spectrum spectrum): + + cdef double x, y, z + cdef Plasma plasma + cdef double te, ne, beam_energy, sigma, stark_split, beam_ion_mass, beam_temperature + cdef double natural_wavelength, central_wavelength + cdef double sigma_to_pi, d, intensity_sig, intensity_pi, e_field + cdef double s1_to_s0, intensity_s0, intensity_s1 + cdef double pi2_to_pi3, pi4_to_pi3, intensity_pi2, intensity_pi3, intensity_pi4 + cdef Vector3D b_field, beam_velocity + + # extract for more compact code + x = plasma_point.x + y = plasma_point.y + z = plasma_point.z + + plasma = self.beam.get_plasma() + + te = plasma.get_electron_distribution().effective_temperature(x, y, z) + if te <= 0.0: + return spectrum + + ne = plasma.get_electron_distribution().density(x, y, z) + if ne <= 0.0: + return spectrum + + beam_energy = self.beam.get_energy() + + # calculate Stark splitting + b_field = plasma.get_b_field().evaluate(x, y, z) + beam_velocity = beam_direction.normalise().mul(evamu_to_ms(beam_energy)) + e_field = beam_velocity.cross(b_field).get_length() + stark_split = fabs(STARK_SPLITTING_FACTOR * e_field) # TODO - calculate splitting factor? Reject other lines? + + # calculate emission line central wavelength, doppler shifted along observation direction + natural_wavelength = self.wavelength + central_wavelength = doppler_shift(natural_wavelength, observation_direction, beam_velocity) + + # calculate doppler broadening + beam_ion_mass = self.beam.get_element().atomic_weight + beam_temperature = self.beam.get_temperature() + sigma = thermal_broadening(self.wavelength, beam_temperature, beam_ion_mass) + + # calculate relative intensities of sigma and pi lines + sigma_to_pi = self._sigma_to_pi.evaluate(ne, beam_energy) + d = 1 / (1 + sigma_to_pi) + intensity_sig = sigma_to_pi * d * radiance + intensity_pi = 0.5 * d * radiance + + # add Sigma lines to output + s1_to_s0 = self._sigma1_to_sigma0.evaluate(ne) + intensity_s0 = 1 / (s1_to_s0 + 1) + intensity_s1 = 0.5 * s1_to_s0 * intensity_s0 + + spectrum = add_gaussian_line(intensity_sig * intensity_s0, central_wavelength, sigma, spectrum) + spectrum = add_gaussian_line(intensity_sig * intensity_s1, central_wavelength + stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_sig * intensity_s1, central_wavelength - stark_split, sigma, spectrum) + + # add Pi lines to output + pi2_to_pi3 = self._pi2_to_pi3.evaluate(ne) + pi4_to_pi3 = self._pi4_to_pi3.evaluate(ne) + intensity_pi3 = 1 / (1 + pi2_to_pi3 + pi4_to_pi3) + intensity_pi2 = pi2_to_pi3 * intensity_pi3 + intensity_pi4 = pi4_to_pi3 * intensity_pi3 + + spectrum = add_gaussian_line(intensity_pi * intensity_pi2, central_wavelength + 2 * stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_pi * intensity_pi2, central_wavelength - 2 * stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_pi * intensity_pi3, central_wavelength + 3 * stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_pi * intensity_pi3, central_wavelength - 3 * stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_pi * intensity_pi4, central_wavelength + 4 * stark_split, sigma, spectrum) + spectrum = add_gaussian_line(intensity_pi * intensity_pi4, central_wavelength - 4 * stark_split, sigma, spectrum) + + return spectrum diff --git a/cherab/core/model/lineshape/doppler.pxd b/cherab/core/model/lineshape/doppler.pxd new file mode 100644 index 00000000..3d4438fc --- /dev/null +++ b/cherab/core/model/lineshape/doppler.pxd @@ -0,0 +1,26 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Vector3D + + +cpdef double doppler_shift(double wavelength, Vector3D observation_direction, Vector3D velocity) + +cpdef double thermal_broadening(double wavelength, double temperature, double atomic_weight) diff --git a/cherab/core/model/lineshape/doppler.pyx b/cherab/core/model/lineshape/doppler.pyx new file mode 100644 index 00000000..93755733 --- /dev/null +++ b/cherab/core/model/lineshape/doppler.pyx @@ -0,0 +1,59 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport sqrt + +from cherab.core.utility.constants cimport ATOMIC_MASS, ELEMENTARY_CHARGE, SPEED_OF_LIGHT + +cimport cython + + +@cython.cdivision(True) +cpdef double doppler_shift(double wavelength, Vector3D observation_direction, Vector3D velocity): + """ + Calculates the Doppler shifted wavelength for a given velocity and observation direction. + + :param wavelength: The wavelength to Doppler shift in nanometers. + :param observation_direction: A Vector defining the direction of observation. + :param velocity: A Vector defining the relative velocity of the emitting source in m/s. + :return: The Doppler shifted wavelength in nanometers. + """ + cdef double projected_velocity + + # flow velocity projected on the direction of observation + observation_direction = observation_direction.normalise() + projected_velocity = velocity.dot(observation_direction) + + return wavelength * (1 + projected_velocity / SPEED_OF_LIGHT) + + +@cython.cdivision(True) +cpdef double thermal_broadening(double wavelength, double temperature, double atomic_weight): + """ + Returns the line width for a gaussian line as a standard deviation. + + :param wavelength: Central wavelength. + :param temperature: Temperature in eV. + :param atomic_weight: Atomic weight in AMU. + :return: Standard deviation of gaussian line. + """ + + # todo: add input sanity checks + return sqrt(temperature * ELEMENTARY_CHARGE / (atomic_weight * ATOMIC_MASS)) * wavelength / SPEED_OF_LIGHT diff --git a/cherab/core/model/lineshape/gaussian.pxd b/cherab/core/model/lineshape/gaussian.pxd new file mode 100644 index 00000000..cd8d0091 --- /dev/null +++ b/cherab/core/model/lineshape/gaussian.pxd @@ -0,0 +1,29 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Spectrum +from cherab.core.model.lineshape.base cimport LineShapeModel + + +cpdef Spectrum add_gaussian_line(double radiance, double wavelength, double sigma, Spectrum spectrum) + + +cdef class GaussianLine(LineShapeModel): + pass diff --git a/cherab/core/model/lineshape/gaussian.pyx b/cherab/core/model/lineshape/gaussian.pyx new file mode 100644 index 00000000..ef854ea9 --- /dev/null +++ b/cherab/core/model/lineshape/gaussian.pyx @@ -0,0 +1,139 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport erf, M_SQRT2, floor, ceil +from raysect.optical cimport Point3D, Vector3D + +from cherab.core.atomic cimport Line, AtomicData +from cherab.core.species cimport Species +from cherab.core.plasma cimport Plasma +from cherab.core.model.lineshape.doppler cimport doppler_shift, thermal_broadening + +cimport cython + + +# the number of standard deviations outside the rest wavelength the line is considered to add negligible value (including a margin for safety) +DEF GAUSSIAN_CUTOFF_SIGMA = 10.0 + + +@cython.cdivision(True) +@cython.initializedcheck(False) +@cython.boundscheck(False) +@cython.wraparound(False) +cpdef Spectrum add_gaussian_line(double radiance, double wavelength, double sigma, Spectrum spectrum): + r""" + Adds a Gaussian line to the given spectrum and returns the new spectrum. + + The formula used is based on the following definite integral: + :math:`\frac{1}{\sigma \sqrt{2 \pi}} \int_{\lambda_0}^{\lambda_1} \exp(-\frac{(x-\mu)^2}{2\sigma^2}) dx = \frac{1}{2} \left[ -Erf(\frac{a-\mu}{\sqrt{2}\sigma}) +Erf(\frac{b-\mu}{\sqrt{2}\sigma}) \right]` + + :param float radiance: Intensity of the line in radiance. + :param float wavelength: central wavelength of the line in nm. + :param float sigma: width of the line in nm. + :param Spectrum spectrum: the current spectrum to which the gaussian line is added. + :return: + """ + + cdef double temp + cdef double cutoff_lower_wavelength, cutoff_upper_wavelength + cdef double lower_wavelength, upper_wavelength + cdef double lower_integral, upper_integral + cdef int start, end, i + + if sigma <= 0: + return spectrum + + # calculate and check end of limits + cutoff_lower_wavelength = wavelength - GAUSSIAN_CUTOFF_SIGMA * sigma + if spectrum.max_wavelength < cutoff_lower_wavelength: + return spectrum + + cutoff_upper_wavelength = wavelength + GAUSSIAN_CUTOFF_SIGMA * sigma + if spectrum.min_wavelength > cutoff_upper_wavelength: + return spectrum + + # locate range of bins where there is significant contribution from the gaussian (plus a health margin) + start = max(0, floor((cutoff_lower_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) + end = min(spectrum.bins, ceil((cutoff_upper_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) + + # add line to spectrum + temp = 1 / (M_SQRT2 * sigma) + lower_wavelength = spectrum.min_wavelength + start * spectrum.delta_wavelength + lower_integral = erf((lower_wavelength - wavelength) * temp) + for i in range(start, end): + + upper_wavelength = spectrum.min_wavelength + spectrum.delta_wavelength * (i + 1) + upper_integral = erf((upper_wavelength - wavelength) * temp) + + spectrum.samples_mv[i] += radiance * 0.5 * (upper_integral - lower_integral) / spectrum.delta_wavelength + + lower_wavelength = upper_wavelength + lower_integral = upper_integral + + return spectrum + + +cdef class GaussianLine(LineShapeModel): + """ + Produces Gaussian line shape. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + + .. code-block:: pycon + + >>> from cherab.core.atomic import Line, deuterium + >>> from cherab.core.model import ExcitationLine, GaussianLine + >>> + >>> # Adding Gaussian line to the plasma model. + >>> d_alpha = Line(deuterium, 0, (3, 2)) + >>> excit = ExcitationLine(d_alpha, lineshape=GaussianLine) + >>> plasma.models.add(excit) + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data): + + super().__init__(line, wavelength, target_species, plasma, atomic_data) + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef double ts, sigma, shifted_wavelength + cdef Vector3D ion_velocity + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + if ts <= 0.0: + return spectrum + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate emission line central wavelength, doppler shifted along observation direction + shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) + + # calculate the line width + sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) + + return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) diff --git a/cherab/core/model/lineshape/multiplet.pxd b/cherab/core/model/lineshape/multiplet.pxd new file mode 100644 index 00000000..577afadc --- /dev/null +++ b/cherab/core/model/lineshape/multiplet.pxd @@ -0,0 +1,31 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +cimport numpy as np + +from cherab.core.model.lineshape.base cimport LineShapeModel + + +cdef class MultipletLineShape(LineShapeModel): + + cdef: + int _number_of_lines + np.ndarray _multiplet + double[:, ::1] _multiplet_mv diff --git a/cherab/core/model/lineshape/multiplet.pyx b/cherab/core/model/lineshape/multiplet.pyx new file mode 100644 index 00000000..6f9e39ab --- /dev/null +++ b/cherab/core/model/lineshape/multiplet.pyx @@ -0,0 +1,117 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import numpy as np + +from raysect.optical cimport Spectrum, Point3D, Vector3D + +from cherab.core.atomic cimport Line, AtomicData +from cherab.core.species cimport Species +from cherab.core.plasma cimport Plasma +from cherab.core.model.lineshape.doppler cimport doppler_shift, thermal_broadening +from cherab.core.model.lineshape.gaussian cimport add_gaussian_line + +cimport cython + +# required by numpy c-api +np.import_array() + + +DEF MULTIPLET_WAVELENGTH = 0 +DEF MULTIPLET_RATIO = 1 + + +cdef class MultipletLineShape(LineShapeModel): + """ + Produces Multiplet line shapes. + + The lineshape radiance is calculated from a base PEC rate that is unresolved. This + radiance is then divided over a number of components as specified in the multiplet + argument. The multiplet components are specified with an Nx2 array where N is the + number of components in the multiplet. The first axis of the array contains the + wavelengths of each component, the second contains the line ratio for each component. + The component line ratios must sum to one. For example: + + :param Line line: The emission line object for the base rate radiance calculation. + :param float wavelength: The rest wavelength of the base emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param multiplet: An Nx2 array that specifies the multiplet wavelengths and line ratios. + + .. code-block:: pycon + + >>> from cherab.core.atomic import Line, nitrogen + >>> from cherab.core.model import ExcitationLine, MultipletLineShape + >>> + >>> # multiplet specification in Nx2 array + >>> multiplet = [[403.509, 404.132, 404.354, 404.479, 405.692], [0.205, 0.562, 0.175, 0.029, 0.029]] + >>> + >>> # Adding the multiplet to the plasma model. + >>> nitrogen_II_404 = Line(nitrogen, 1, ("2s2 2p1 4f1 3G13.0", "2s2 2p1 3d1 3F10.0")) + >>> excit = ExcitationLine(nitrogen_II_404, lineshape=MultipletLineShape, lineshape_args=[multiplet]) + >>> plasma.models.add(excit) + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, + object multiplet): + + super().__init__(line, wavelength, target_species, plasma, atomic_data) + + multiplet = np.array(multiplet, dtype=np.float64) + + if not (len(multiplet.shape) == 2 and multiplet.shape[0] == 2): + raise ValueError("The multiplet specification must be an array of shape (Nx2).") + + if not multiplet[1, :].sum() == 1.0: + raise ValueError("The multiplet line ratios should sum to one.") + + self._number_of_lines = multiplet.shape[1] + self._multiplet = multiplet + self._multiplet_mv = self._multiplet + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef double ts, sigma, shifted_wavelength, component_wavelength, component_radiance + cdef Vector3D ion_velocity + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + if ts <= 0.0: + return spectrum + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate the line width + sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) + + for i in range(self._number_of_lines): + + component_wavelength = self._multiplet_mv[MULTIPLET_WAVELENGTH, i] + component_radiance = radiance * self._multiplet_mv[MULTIPLET_RATIO, i] + + # calculate emission line central wavelength, doppler shifted along observation direction + shifted_wavelength = doppler_shift(component_wavelength, direction, ion_velocity) + + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + return spectrum diff --git a/cherab/core/model/lineshape/stark.pxd b/cherab/core/model/lineshape/stark.pxd new file mode 100644 index 00000000..8b4ac4b0 --- /dev/null +++ b/cherab/core/model/lineshape/stark.pxd @@ -0,0 +1,36 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Spectrum +from cherab.core.math.integrators cimport Integrator1D +from cherab.core.model.lineshape.zeeman cimport ZeemanLineShapeModel + + +cpdef Spectrum add_lorentzian_line(double radiance, double wavelength, double lambda_1_2, Spectrum spectrum, + Integrator1D integrator) + + +cdef class StarkBroadenedLine(ZeemanLineShapeModel): + + cdef: + double _aij, _bij, _cij + double _fwhm_poly_coeff_gauss[7] + double _fwhm_poly_coeff_lorentz[7] + double _weight_poly_coeff[6] diff --git a/cherab/core/model/lineshape/stark.pyx b/cherab/core/model/lineshape/stark.pyx new file mode 100644 index 00000000..9e333a5f --- /dev/null +++ b/cherab/core/model/lineshape/stark.pyx @@ -0,0 +1,348 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from scipy.special import hyp2f1 + +from libc.math cimport sqrt, floor, ceil, fabs, log, exp +from raysect.core.math.function.float cimport Function1D +from raysect.optical cimport Point3D, Vector3D + +from cherab.core.atomic cimport Line, AtomicData +from cherab.core.species cimport Species +from cherab.core.plasma cimport Plasma +from cherab.core.atomic.elements import hydrogen, deuterium, tritium +from cherab.core.math.function cimport autowrap_function1d, autowrap_function2d +from cherab.core.math.integrators cimport GaussianQuadrature +from cherab.core.utility.constants cimport BOHR_MAGNETON, HC_EV_NM +from cherab.core.model.lineshape.doppler cimport doppler_shift, thermal_broadening +from cherab.core.model.lineshape.gaussian cimport add_gaussian_line + + +cimport cython + + +DEF PI_POLARISATION = 0 +DEF SIGMA_POLARISATION = 1 +DEF SIGMA_PLUS_POLARISATION = 1 +DEF SIGMA_MINUS_POLARISATION = -1 +DEF NO_POLARISATION = 2 + +DEF LORENTZIAN_CUTOFF_GAMMA = 50.0 + +cdef double _SIGMA2FWHM = 2 * sqrt(2 * log(2)) + + +cdef class StarkFunction(Function1D): + """ + Normalised Stark function for the StarkBroadenedLine line shape. + + :param float wavelength: central wavelength of the line in nm. + :param float lambda_1_2: FWHM of the function in nm. + """ + + cdef double _a, _x0, _norm + + STARK_NORM_COEFFICIENT = 4 * LORENTZIAN_CUTOFF_GAMMA * hyp2f1(0.4, 1, 1.4, -(2 * LORENTZIAN_CUTOFF_GAMMA)**2.5) + + def __init__(self, double wavelength, double lambda_1_2): + + if wavelength <= 0: + raise ValueError("Argument 'wavelength' must be positive.") + + if lambda_1_2 <= 0: + raise ValueError("Argument 'lambda_1_2' must be positive.") + + self._x0 = wavelength + self._a = (0.5 * lambda_1_2)**2.5 + # normalise, so the integral over x is equal to 1 in the limits + # (_x0 - LORENTZIAN_CUTOFF_GAMMA * lambda_1_2, _x0 + LORENTZIAN_CUTOFF_GAMMA * lambda_1_2) + self._norm = (0.5 * lambda_1_2)**1.5 / self.STARK_NORM_COEFFICIENT + + @cython.cdivision(True) + cdef double evaluate(self, double x) except? -1e999: + + return self._norm / ((fabs(x - self._x0))**2.5 + self._a) + + +@cython.cdivision(True) +@cython.initializedcheck(False) +@cython.boundscheck(False) +@cython.wraparound(False) +cpdef Spectrum add_lorentzian_line(double radiance, double wavelength, double lambda_1_2, Spectrum spectrum, Integrator1D integrator): + r""" + Adds a modified Lorentzian line to the given spectrum and returns the new spectrum. + + The modified Lorentzian: + :math:`L(\lambda-\lambda_0, \Delta\lambda_{1/2}^{(L)})=\frac{C_0(\Delta\lambda_{1/2}^{(L)})^{3/2}}{(\lambda-\lambda_0)^{5/2}+(\frac{\Delta\lambda_{1/2}^{(L)}}{2})^{5/2}},` + + :math:`C_0=\frac{(1/2)^{3/2}}{4R{_2}F_1(\frac{2}{5},1,\frac{7}{5},-R^{5/2})},` + + where :math:`\Delta\lambda_{1/2}^{(L)}` is the line FWHM, :math:`{_2}F_1` is the hypergeometric function and :math:`R=50`. + The line shape is truncated at :math:`\lambdaR\Delta\lambda_{1/2}^{(L)}`. + + See B. Lomanowski, et al. "Inferring divertor plasma properties from hydrogen Balmer + and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) + `123028 `_ for details. + + :param float radiance: Intensity of the line in radiance. + :param float wavelength: central wavelength of the line in nm. + :param float lambda_1_2: FWHM of the line shape in nm. + :param Spectrum spectrum: the current spectrum to which the Lorentzian line is added. + :param Integrator1D integrator: Integrator1D instance to integrate the line shape + over the spectral bin. + :return: + """ + + cdef double cutoff_lower_wavelength, cutoff_upper_wavelength + cdef double lower_wavelength, upper_wavelength + cdef double bin_integral + cdef int start, end, i + + if lambda_1_2 <= 0: + return spectrum + + integrator.function = StarkFunction(wavelength, lambda_1_2) + + # calculate and check end of limits + cutoff_lower_wavelength = wavelength - LORENTZIAN_CUTOFF_GAMMA * lambda_1_2 + if spectrum.max_wavelength < cutoff_lower_wavelength: + return spectrum + + cutoff_upper_wavelength = wavelength + LORENTZIAN_CUTOFF_GAMMA * lambda_1_2 + if spectrum.min_wavelength > cutoff_upper_wavelength: + return spectrum + + # locate range of bins where there is significant contribution from the gaussian (plus a health margin) + start = max(0, floor((cutoff_lower_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) + end = min(spectrum.bins, ceil((cutoff_upper_wavelength - spectrum.min_wavelength) / spectrum.delta_wavelength)) + + # add line to spectrum + lower_wavelength = spectrum.min_wavelength + start * spectrum.delta_wavelength + + for i in range(start, end): + upper_wavelength = spectrum.min_wavelength + spectrum.delta_wavelength * (i + 1) + + bin_integral = integrator.evaluate(lower_wavelength, upper_wavelength) + spectrum.samples_mv[i] += radiance * bin_integral / spectrum.delta_wavelength + + lower_wavelength = upper_wavelength + + return spectrum + + +cdef class StarkBroadenedLine(ZeemanLineShapeModel): + r""" + Parametrised Stark-Doppler-Zeeman line shape for Balmer and Paschen series based on + B. Lomanowski, et al. "Inferring divertor plasma properties from hydrogen Balmer + and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) + `123028 `_. + + The following approximations are used: + + * The Zeeman and Stark effects are considered independently. + * Zeeman splitting is taken in the form of a simple triplet with a :math:`\pi`-component + centred at :math:`\lambda`, :math:`\sigma^{+}`-component at :math:`\frac{hc}{hc/\lambda -\mu B}` + and :math:`\sigma^{-}`-component at :math:`\frac{hc}{hc/\lambda +\mu B}`. + * The model of Stark broadening is obtained by fitting the Model Microfield Method (MMM). + * The convolution of Stark-Zeeman and Doppler profiles is replaced with the weighted sum + to speed-up calculations (so-called pseudo-Voigt profile). + + The Stark-broadened line shape is modelled as modified Lorentzian: + :math:`L(\lambda-\lambda_0, \Delta\lambda_{1/2}^{(L)})=\frac{C_0(\Delta\lambda_{1/2}^{(L)})^{3/2}}{(\lambda-\lambda_0)^{5/2}+(\frac{\Delta\lambda_{1/2}^{(L)}}{2})^{5/2}},` + + :math:`C_0=\frac{(1/2)^{3/2}}{4R{_2}F_1(\frac{2}{5},1,\frac{7}{5},-R^{5/2})},` + + where :math:`\Delta\lambda_{1/2}^{(L)}=c_{ij}\frac{n_e^{a_{ij}}}{T_e^{b_{ij}}}` is the line FWHM, + :math:`{_2}F_1` is the hypergeometric function and :math:`R=50`. + The line shape is truncated at :math:`\lambdaR\Delta\lambda_{1/2}^{(L)}`. + The :math:`a_{ij}`, :math:`b_{ij}` and :math:`c_{ij}` are the fitting coefficients. + + Each Zeeman component is modelled as a weighted sum of Stark (Lorentzian), :math:`L(\lambda-\lambda_0, \Delta\lambda_{1/2}^{(L)})`, + and Doppler (Gauss), :math:`G(\lambda'-\lambda_0, \Delta\lambda_{1/2}^{(G)})` profiles: + + :math:`\eta L(\lambda-\lambda_0, \Delta\lambda_{1/2}^{(V)}) + (1-\eta)G(\lambda-\lambda_0, \Delta\lambda_{1/2}^{(V)})`, + + with :math:`\Delta\lambda_{1/2}^{(V)}\equiv \Delta\lambda_{1/2}^{(V)}(\Delta\lambda_{1/2}^{(G)}, \Delta\lambda_{1/2}^{(L)})` + and :math:`\eta\equiv \eta(\Delta\lambda_{1/2}^{(G)}, \Delta\lambda_{1/2}^{(L)})`. + + Both :math:`\Delta\lambda_{1/2}^{(V)}` and :math:`\eta` are obtained by fitting the convolution: + + :math:`\int_{-\infty}^{\infty}G(\lambda'-\lambda_0, \Delta\lambda_{1/2}^{(G)})L(\lambda-\lambda'-\lambda_0, \Delta\lambda_{1/2}^{(L)})d\lambda'`. + + The :math:`\Delta\lambda_{1/2}^{(V)}` function is fitted as: + :math:`\Delta\lambda_{1/2}^{(V)}=\sum_{n=0}^{6}a_n(\frac{\Delta\lambda_{1/2}^{(L)}}{\Delta\lambda_{1/2}^{(G)}})^n` + for :math:`\frac{\Delta\lambda_{1/2}^{(L)}}{\Delta\lambda_{1/2}^{(G)}} \le 1` and + :math:`\Delta\lambda_{1/2}^{(V)}=\sum_{n=0}^{6}b_n(\frac{\Delta\lambda_{1/2}^{(G)}}{\Delta\lambda_{1/2}^{(L)}})^n` + for :math:`\frac{\Delta\lambda_{1/2}^{(L)}}{\Delta\lambda_{1/2}^{(G)}} > 1` with + + `a` = [1., 0.15882, 1.04388, -1.38281, 0.46251, 0.82325, -0.58026] and + + `b` = [1., 0, 0.57575, 0.37902, -0.42519, -0.31525, 0.31718]. + + While the :math:`\eta` function is fitted as: + :math:`\eta=exp(\sum_{n=0}^{5}c_n(ln(\frac{\Delta\lambda_{1/2}^{(L)}}{\Delta\lambda_{1/2}^{(V)}}))^n` + for :math:`0.01<\frac{\Delta\lambda_{1/2}^{(L)}}{\Delta\lambda_{1/2}^{(V)}}<0.999` with + + `c` = [5.14820e-04, 1.38821e+00, -9.60424e-02, -3.83995e-02, -7.40042e-03, -5.47626e-04]. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param tuple stark_model_coefficients: Stark model coefficients in the form (c_ij, a_ij, b_ij). + Default is None (will use + `atomic_data.stark_model_coefficients`). + :param Integrator1D integrator: Integrator1D instance to integrate the line shape + over the spectral bin. Default is `GaussianQuadrature()`. + :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: + "pi" - leave only :math:`\pi`-polarised components, + "sigma" - leave only :math:`\sigma`-polarised components, + "no" - leave all components (default). + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, + tuple stark_model_coefficients=None, Integrator1D integrator=GaussianQuadrature(), polarisation='no'): + + super().__init__(line, wavelength, target_species, plasma, atomic_data, polarisation, integrator) + + try: + # Fitted Stark Constants + cij, aij, bij = stark_model_coefficients or self.atomic_data.stark_model_coefficients(line) + if cij <= 0: + raise ValueError('Coefficient c_ij must be positive.') + if aij <= 0: + raise ValueError('Coefficient a_ij must be positive.') + if bij <= 0: + raise ValueError('Coefficient b_ij must be positive.') + self._aij = aij + self._bij = bij + self._cij = cij + except IndexError: + raise ValueError('Stark broadening coefficients for {} is not currently available.'.format(line)) + + # polynomial coefficients in increasing powers + self._fwhm_poly_coeff_gauss = [1., 0, 0.57575, 0.37902, -0.42519, -0.31525, 0.31718] + self._fwhm_poly_coeff_lorentz = [1., 0.15882, 1.04388, -1.38281, 0.46251, 0.82325, -0.58026] + + self._weight_poly_coeff = [5.14820e-04, 1.38821e+00, -9.60424e-02, -3.83995e-02, -7.40042e-03, -5.47626e-04] + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef: + double ne, te, ts, shifted_wavelength, photon_energy, b_magn, comp_radiance, cos_sqr, sin_sqr + double sigma, fwhm_lorentz, fwhm_gauss, fwhm_full, fwhm_ratio, fwhm_lorentz_to_total, lorentz_weight, gauss_weight + cdef Vector3D ion_velocity, b_field + int i + + ne = self.plasma.get_electron_distribution().density(point.x, point.y, point.z) + + te = self.plasma.get_electron_distribution().effective_temperature(point.x, point.y, point.z) + + fwhm_lorentz = self._cij * ne**self._aij / (te**self._bij) if ne > 0 and te > 0 else 0 + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + + fwhm_gauss = _SIGMA2FWHM * thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) if ts > 0 else 0 + + if fwhm_lorentz == 0 and fwhm_gauss == 0: + return spectrum + + # calculating full FWHM + if fwhm_gauss <= fwhm_lorentz: + fwhm_ratio = fwhm_gauss / fwhm_lorentz + fwhm_full = self._fwhm_poly_coeff_gauss[0] + for i in range(1, 7): + fwhm_full += self._fwhm_poly_coeff_gauss[i] * fwhm_ratio**i + fwhm_full *= fwhm_lorentz + else: + fwhm_ratio = fwhm_lorentz / fwhm_gauss + fwhm_full = self._fwhm_poly_coeff_lorentz[0] + for i in range(1, 7): + fwhm_full += self._fwhm_poly_coeff_lorentz[i] * fwhm_ratio**i + fwhm_full *= fwhm_gauss + + sigma = fwhm_full / _SIGMA2FWHM + + fwhm_lorentz_to_total = fwhm_lorentz / fwhm_full + + # calculating Lorentzian weight + if fwhm_lorentz_to_total < 0.01: + lorentz_weight = 0 + fwhm_full = 0 # force add_lorentzian_line() to immediately return + elif fwhm_lorentz_to_total > 0.999: + lorentz_weight = 1 + sigma = 0 # force add_gaussian_line() to immediately return + else: + lorentz_weight = self._weight_poly_coeff[0] + for i in range(1, 6): + lorentz_weight += self._weight_poly_coeff[i] * log(fwhm_lorentz_to_total)**i + lorentz_weight = exp(lorentz_weight) + + gauss_weight = 1 - lorentz_weight + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate emission line central wavelength, doppler shifted along observation direction + shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) + + # obtain magnetic field + b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) + b_magn = b_field.get_length() + + if b_magn == 0: + # no splitting if magnetic field strength is zero + if self._polarisation != NO_POLARISATION: + radiance *= 0.5 # pi or sigma polarisation, collecting only half of intensity + + spectrum = add_gaussian_line(gauss_weight * radiance, shifted_wavelength, sigma, spectrum) + spectrum = add_lorentzian_line(lorentz_weight * radiance, shifted_wavelength, fwhm_full, spectrum, self.integrator) + + return spectrum + + # coefficients for intensities parallel and perpendicular to magnetic field + cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 + sin_sqr = 1. - cos_sqr + + # adding pi component of the Zeeman triplet in case of NO_POLARISATION or PI_POLARISATION + if self._polarisation != SIGMA_POLARISATION: + comp_radiance = 0.5 * sin_sqr * radiance + spectrum = add_gaussian_line(gauss_weight * comp_radiance, shifted_wavelength, sigma, spectrum) + spectrum = add_lorentzian_line(lorentz_weight * comp_radiance, shifted_wavelength, fwhm_full, spectrum, self.integrator) + + # adding sigma +/- components of the Zeeman triplet in case of NO_POLARISATION or SIGMA_POLARISATION + if self._polarisation != PI_POLARISATION: + comp_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance + + photon_energy = HC_EV_NM / self.wavelength + + shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy - BOHR_MAGNETON * b_magn), direction, ion_velocity) + spectrum = add_gaussian_line(gauss_weight * comp_radiance, shifted_wavelength, sigma, spectrum) + spectrum = add_lorentzian_line(lorentz_weight * comp_radiance, shifted_wavelength, fwhm_full, spectrum, self.integrator) + + shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy + BOHR_MAGNETON * b_magn), direction, ion_velocity) + spectrum = add_gaussian_line(gauss_weight * comp_radiance, shifted_wavelength, sigma, spectrum) + spectrum = add_lorentzian_line(lorentz_weight * comp_radiance, shifted_wavelength, fwhm_full, spectrum, self.integrator) + + return spectrum diff --git a/cherab/core/model/lineshape/zeeman.pxd b/cherab/core/model/lineshape/zeeman.pxd new file mode 100644 index 00000000..b67d2042 --- /dev/null +++ b/cherab/core/model/lineshape/zeeman.pxd @@ -0,0 +1,48 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.atomic.zeeman cimport ZeemanStructure +from cherab.core.model.lineshape.base cimport LineShapeModel + + +cdef class ZeemanLineShapeModel(LineShapeModel): + + cdef double _polarisation + + pass + + +cdef class ZeemanTriplet(ZeemanLineShapeModel): + + pass + + +cdef class ParametrisedZeemanTriplet(ZeemanLineShapeModel): + + cdef double _alpha, _beta, _gamma + + pass + + +cdef class ZeemanMultiplet(ZeemanLineShapeModel): + + cdef ZeemanStructure _zeeman_structure + + pass diff --git a/cherab/core/model/lineshape/zeeman.pyx b/cherab/core/model/lineshape/zeeman.pyx new file mode 100644 index 00000000..2be33020 --- /dev/null +++ b/cherab/core/model/lineshape/zeeman.pyx @@ -0,0 +1,365 @@ +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from libc.math cimport sqrt +from raysect.optical cimport Spectrum, Point3D, Vector3D + +from cherab.core.atomic cimport Line, AtomicData +from cherab.core.species cimport Species +from cherab.core.plasma cimport Plasma +from cherab.core.atomic.elements import hydrogen, deuterium, tritium, helium, helium3, beryllium, boron, carbon, nitrogen, oxygen, neon +from cherab.core.math.integrators cimport Integrator1D +from cherab.core.utility.constants cimport BOHR_MAGNETON, HC_EV_NM +from cherab.core.model.lineshape.doppler cimport doppler_shift, thermal_broadening +from cherab.core.model.lineshape.gaussian cimport add_gaussian_line + +cimport cython + + +DEF MULTIPLET_WAVELENGTH = 0 +DEF MULTIPLET_RATIO = 1 + +DEF PI_POLARISATION = 0 +DEF SIGMA_POLARISATION = 1 +DEF SIGMA_PLUS_POLARISATION = 1 +DEF SIGMA_MINUS_POLARISATION = -1 +DEF NO_POLARISATION = 2 + + +cdef class ZeemanLineShapeModel(LineShapeModel): + r""" + A base class for building Zeeman line shapes. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: + "pi" - leave only :math:`\pi`-polarised components, + "sigma" - leave only :math:`\sigma`-polarised components, + "no" - leave all components (default). + :param Integrator1D integrator: Integrator1D instance to integrate the line shape + over the spectral bin. Default is None. + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, + polarisation='no', Integrator1D integrator=None): + super().__init__(line, wavelength, target_species, plasma, atomic_data, integrator) + + self.polarisation = polarisation + + @property + def polarisation(self): + if self._polarisation == PI_POLARISATION: + return 'pi' + if self._polarisation == SIGMA_POLARISATION: + return 'sigma' + if self._polarisation == NO_POLARISATION: + return 'no' + + @polarisation.setter + def polarisation(self, value): + if value.lower() == 'pi': + self._polarisation = PI_POLARISATION + elif value.lower() == 'sigma': + self._polarisation = SIGMA_POLARISATION + elif value.lower() == 'no': + self._polarisation = NO_POLARISATION + else: + raise ValueError('Select between "pi", "sigma" or "no", {} is unsupported.'.format(value)) + + +cdef class ZeemanTriplet(ZeemanLineShapeModel): + r""" + Simple Doppler-Zeeman triplet (Paschen-Back effect). + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: + "pi" - leave central component, + "sigma" - leave side components, + "no" - all components (default). + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, polarisation='no'): + + super().__init__(line, wavelength, target_species, plasma, atomic_data, polarisation) + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef double ts, sigma, shifted_wavelength, photon_energy, b_magn, component_radiance, cos_sqr, sin_sqr + cdef Vector3D ion_velocity, b_field + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + if ts <= 0.0: + return spectrum + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate emission line central wavelength, doppler shifted along observation direction + shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) + + # calculate the line width + sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) + + # obtain magnetic field + b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) + b_magn = b_field.get_length() + + if b_magn == 0: + # no splitting if magnetic field strength is zero + if self._polarisation == NO_POLARISATION: + return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) + + return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) + + # coefficients for intensities parallel and perpendicular to magnetic field + cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 + sin_sqr = 1. - cos_sqr + + # adding pi component of the Zeeman triplet in case of NO_POLARISATION or PI_POLARISATION + if self._polarisation != SIGMA_POLARISATION: + component_radiance = 0.5 * sin_sqr * radiance + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + # adding sigma +/- components of the Zeeman triplet in case of NO_POLARISATION or SIGMA_POLARISATION + if self._polarisation != PI_POLARISATION: + component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance + + photon_energy = HC_EV_NM / self.wavelength + + shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy - BOHR_MAGNETON * b_magn), direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + shifted_wavelength = doppler_shift(HC_EV_NM / (photon_energy + BOHR_MAGNETON * b_magn), direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + return spectrum + + +cdef class ParametrisedZeemanTriplet(ZeemanLineShapeModel): + r""" + Parametrised Doppler-Zeeman triplet. It takes into account additional broadening due to + the line's fine structure without resolving the individual components of the fine + structure. The model is described with three parameters: :math:`\alpha`, + :math:`\beta` and :math:`\gamma`. + + The distance between :math:`\sigma^+` and :math:`\sigma^-` peaks: + :math:`\Delta \lambda_{\sigma} = \alpha B`, + where `B` is the magnetic field strength. + The ratio between Zeeman and thermal broadening line widths: + :math:`\frac{W_{Zeeman}}{W_{Doppler}} = \beta T^{\gamma}`, + where `T` is the species temperature in eV. + + For details see A. Blom and C. Jupén, Parametrisation of the Zeeman effect + for hydrogen-like spectra in high-temperature plasmas, + Plasma Phys. Control. Fusion 44 (2002) `1229-1241 + `_. + + :param Line line: The emission line object for this line shape. + :param float wavelength: The rest wavelength for this emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param tuple line_parameters: Parameters of the model in the form (alpha, beta, gamma). + Default is None (will use `atomic_data.zeeman_triplet_parameters`). + :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: + "pi" - leave central component, + "sigma" - leave side components, + "no" - all components (default). + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, + tuple line_parameters=None, polarisation='no'): + + super().__init__(line, wavelength, target_species, plasma, atomic_data, polarisation) + + try: + alpha, beta, gamma = line_parameters or self.atomic_data.zeeman_triplet_parameters(line) + if alpha <= 0: + raise ValueError('Parameter alpha must be positive.') + if beta < 0: + raise ValueError('Parameter beta must be non-negative.') + self._alpha = alpha + self._beta = beta + self._gamma = gamma + + except KeyError: + raise ValueError('Data for {} is not available.'.format(self.line)) + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef double ts, sigma, shifted_wavelength, b_magn, component_radiance, cos_sqr, sin_sqr + cdef Vector3D ion_velocity, b_field + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + if ts <= 0.0: + return spectrum + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate emission line central wavelength, doppler shifted along observation direction + shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) + + # calculate the line width + sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) + + # fine structure broadening correction + sigma *= sqrt(1. + self._beta * self._beta * ts**(2. * self._gamma)) + + # obtain magnetic field + b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) + b_magn = b_field.get_length() + + if b_magn == 0: + # no splitting if magnetic filed strength is zero + if self._polarisation == NO_POLARISATION: + return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) + + return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) + + # coefficients for intensities parallel and perpendicular to magnetic field + cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 + sin_sqr = 1. - cos_sqr + + # adding pi component of the Zeeman triplet in case of NO_POLARISATION or PI_POLARISATION + if self._polarisation != SIGMA_POLARISATION: + component_radiance = 0.5 * sin_sqr * radiance + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + # adding sigma +/- components of the Zeeman triplet in case of NO_POLARISATION or SIGMA_POLARISATION + if self._polarisation != PI_POLARISATION: + component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance + + shifted_wavelength = doppler_shift(self.wavelength + 0.5 * self._alpha * b_magn, direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + shifted_wavelength = doppler_shift(self.wavelength - 0.5 * self._alpha * b_magn, direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance, shifted_wavelength, sigma, spectrum) + + return spectrum + + +cdef class ZeemanMultiplet(ZeemanLineShapeModel): + r""" + Doppler-Zeeman Multiplet. + + The lineshape radiance is calculated from a base PEC rate that is unresolved. This + radiance is then divided over a number of components as specified in the ``zeeman_structure`` + argument. The ``zeeman_structure`` specifies wavelengths and ratios of + :math:`\pi`-/:math:`\sigma`-polarised components as functions of the magnetic field strength. + These functions can be obtained using the output of the ADAS603 routines. + + :param Line line: The emission line object for the base rate radiance calculation. + :param float wavelength: The rest wavelength of the base emission line. + :param Species target_species: The target plasma species that is emitting. + :param Plasma plasma: The emitting plasma object. + :param AtomicData atomic_data: The atomic data provider. + :param zeeman_structure: A ``ZeemanStructure`` object that provides wavelengths and ratios + of :math:`\pi`-/:math:`\sigma^{+}`-/:math:`\sigma^{-}`-polarised + components for any given magnetic field strength. + Default is None (will use atomic_data.zeeman_structure). + :param str polarisation: Leaves only :math:`\pi`-/:math:`\sigma`-polarised components: + "pi" - leave only :math:`\pi`-polarised components, + "sigma" - leave only :math:`\sigma`-polarised components, + "no" - leave all components (default). + + """ + + def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, AtomicData atomic_data, + ZeemanStructure zeeman_structure=None, polarisation='no'): + + super().__init__(line, wavelength, target_species, plasma, atomic_data, polarisation) + + self._zeeman_structure = zeeman_structure or self.atomic_data.zeeman_structure(line) + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + @cython.cdivision(True) + cpdef Spectrum add_line(self, double radiance, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef int i + cdef double ts, sigma, shifted_wavelength, component_radiance, b_magn, cos_sqr, sin_sqr + cdef Vector3D ion_velocity, b_field + cdef double[:, :] multiplet_mv + + ts = self.target_species.distribution.effective_temperature(point.x, point.y, point.z) + if ts <= 0.0: + return spectrum + + ion_velocity = self.target_species.distribution.bulk_velocity(point.x, point.y, point.z) + + # calculate the line width + sigma = thermal_broadening(self.wavelength, ts, self.line.element.atomic_weight) + + # obtain magnetic field + b_field = self.plasma.get_b_field().evaluate(point.x, point.y, point.z) + b_magn = b_field.get_length() + + if b_magn == 0: + # no splitting if magnetic filed strength is zero + shifted_wavelength = doppler_shift(self.wavelength, direction, ion_velocity) + if self._polarisation == NO_POLARISATION: + return add_gaussian_line(radiance, shifted_wavelength, sigma, spectrum) + + return add_gaussian_line(0.5 * radiance, shifted_wavelength, sigma, spectrum) + + # coefficients for intensities parallel and perpendicular to magnetic field + cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 + sin_sqr = 1. - cos_sqr + + # adding pi components of the Zeeman multiplet in case of NO_POLARISATION or PI_POLARISATION + if self._polarisation != SIGMA_POLARISATION: + component_radiance = 0.5 * sin_sqr * radiance + multiplet_mv = self._zeeman_structure.evaluate(b_magn, PI_POLARISATION) + + for i in range(multiplet_mv.shape[1]): + shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) + + # adding sigma components of the Zeeman multiplet in case of NO_POLARISATION or SIGMA_POLARISATION + if self._polarisation != PI_POLARISATION: + component_radiance = (0.25 * sin_sqr + 0.5 * cos_sqr) * radiance + + multiplet_mv = self._zeeman_structure.evaluate(b_magn, SIGMA_PLUS_POLARISATION) + + for i in range(multiplet_mv.shape[1]): + shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) + + multiplet_mv = self._zeeman_structure.evaluate(b_magn, SIGMA_MINUS_POLARISATION) + + for i in range(multiplet_mv.shape[1]): + shifted_wavelength = doppler_shift(multiplet_mv[MULTIPLET_WAVELENGTH, i], direction, ion_velocity) + spectrum = add_gaussian_line(component_radiance * multiplet_mv[MULTIPLET_RATIO, i], shifted_wavelength, sigma, spectrum) + + return spectrum diff --git a/cherab/core/model/plasma/__init__.pxd b/cherab/core/model/plasma/__init__.pxd index 6971f828..c398fd30 100644 --- a/cherab/core/model/plasma/__init__.pxd +++ b/cherab/core/model/plasma/__init__.pxd @@ -20,4 +20,5 @@ from cherab.core.model.plasma.bremsstrahlung cimport Bremsstrahlung from cherab.core.model.plasma.impact_excitation cimport ExcitationLine from cherab.core.model.plasma.recombination cimport RecombinationLine +from cherab.core.model.plasma.thermal_cx cimport ThermalCXLine from cherab.core.model.plasma.total_radiated_power cimport TotalRadiatedPower diff --git a/cherab/core/model/plasma/__init__.py b/cherab/core/model/plasma/__init__.py index 52766032..436060b5 100644 --- a/cherab/core/model/plasma/__init__.py +++ b/cherab/core/model/plasma/__init__.py @@ -20,4 +20,5 @@ from .bremsstrahlung import Bremsstrahlung from .impact_excitation import ExcitationLine from .recombination import RecombinationLine +from .thermal_cx import ThermalCXLine from .total_radiated_power import TotalRadiatedPower diff --git a/cherab/core/model/plasma/bremsstrahlung.pxd b/cherab/core/model/plasma/bremsstrahlung.pxd index f8251b28..44025c8c 100644 --- a/cherab/core/model/plasma/bremsstrahlung.pxd +++ b/cherab/core/model/plasma/bremsstrahlung.pxd @@ -19,20 +19,27 @@ # cython: language_level=3 from numpy cimport ndarray +from cherab.core.math cimport Function1D +from cherab.core.math.integrators cimport Integrator1D from cherab.core.atomic cimport FreeFreeGauntFactor from cherab.core.plasma cimport PlasmaModel -cdef class Bremsstrahlung(PlasmaModel): +cdef class BremsFunction(Function1D): cdef: - FreeFreeGauntFactor _gaunt_factor - bint _user_provided_gaunt_factor - ndarray _species_charge, _species_density - double[::1] _species_density_mv, _species_charge_mv + double ne, te + FreeFreeGauntFactor gaunt_factor + ndarray species_density, species_charge + double[::1] species_density_mv + double[::1] species_charge_mv - cdef double _bremsstrahlung(self, double wvl, double te, double ne) - cdef int _populate_cache(self) except -1 +cdef class Bremsstrahlung(PlasmaModel): + cdef: + BremsFunction _brems_func + bint _user_provided_gaunt_factor + Integrator1D _integrator + cdef int _populate_cache(self) except -1 diff --git a/cherab/core/model/plasma/bremsstrahlung.pyx b/cherab/core/model/plasma/bremsstrahlung.pyx index 2ba7245b..404750a2 100644 --- a/cherab/core/model/plasma/bremsstrahlung.pyx +++ b/cherab/core/model/plasma/bremsstrahlung.pyx @@ -21,16 +21,73 @@ import numpy as np from raysect.optical cimport Spectrum, Point3D, Vector3D from cherab.core cimport Plasma, AtomicData -from cherab.core.atomic cimport FreeFreeGauntFactor +from cherab.core.math.integrators cimport GaussianQuadrature from cherab.core.species cimport Species -from cherab.core.utility.constants cimport RECIP_4_PI, ELEMENTARY_CHARGE, SPEED_OF_LIGHT, PLANCK_CONSTANT -from libc.math cimport sqrt, log, exp +from cherab.core.utility.constants cimport RECIP_4_PI, ELEMENTARY_CHARGE, SPEED_OF_LIGHT, PLANCK_CONSTANT, ELECTRON_REST_MASS, VACUUM_PERMITTIVITY +from libc.math cimport sqrt, log, exp, M_PI cimport cython -cdef double PH_TO_J_FACTOR = PLANCK_CONSTANT * SPEED_OF_LIGHT * 1e9 +cdef double EXP_FACTOR = PLANCK_CONSTANT * SPEED_OF_LIGHT * 1e9 / ELEMENTARY_CHARGE -cdef double EXP_FACTOR = PH_TO_J_FACTOR / ELEMENTARY_CHARGE +cdef double BREMS_CONST = (ELEMENTARY_CHARGE**2 * RECIP_4_PI / VACUUM_PERMITTIVITY)**3 +BREMS_CONST *= 32 * M_PI**2 / (3 * sqrt(3) * ELECTRON_REST_MASS**2 * SPEED_OF_LIGHT**3) +BREMS_CONST *= sqrt(2 * ELECTRON_REST_MASS / (M_PI * ELEMENTARY_CHARGE)) +BREMS_CONST *= SPEED_OF_LIGHT * 1e9 * RECIP_4_PI + + +cdef class BremsFunction(Function1D): + """ + Calculates bremsstrahlung spectrum. + + :param FreeFreeGauntFactor gaunt_factor: Free-free Gaunt factor as a function of Z, Te and + wavelength. + :param object species_density: Array-like object wiyh ions' density in m-3. + :param object species_charge: Array-like object wiyh ions' charge. + :param double ne: Electron density in m-3. + :param double te: Electron temperature in eV. + """ + + def __init__(self, FreeFreeGauntFactor gaunt_factor, object species_density, object species_charge, double ne, double te): + + if ne <= 0: + raise ValueError("Argument ne must be positive.") + self.ne = ne + + if te <= 0: + raise ValueError("Argument te must be positive.") + self.te = te + + self.gaunt_factor = gaunt_factor + self.species_density = np.asarray(species_density, dtype=np.float64) # copied if type does not match + self.species_density_mv = self.species_density + self.species_charge = np.asarray(species_charge, dtype=np.float64) # copied if type does not match + self.species_charge_mv = self.species_charge + + @cython.cdivision(True) + @cython.boundscheck(False) + @cython.wraparound(False) + cdef double evaluate(self, double wvl) except? -1e999: + """ + :param double wvl: Wavelength in nm. + :return: + """ + + cdef double ni_gff_z2, radiance, pre_factor, ni, z + cdef int i + + ni_gff_z2 = 0 + for i in range(self.species_charge_mv.shape[0]): + z = self.species_charge_mv[i] + ni = self.species_density_mv[i] + if ni > 0: + ni_gff_z2 += ni * self.gaunt_factor.evaluate(z, self.te, wvl) * z * z + + # bremsstrahlung equation W/m^3/str/nm + pre_factor = BREMS_CONST / (sqrt(self.te) * wvl * wvl) * self.ne * ni_gff_z2 + radiance = pre_factor * exp(- EXP_FACTOR / (self.te * wvl)) + + return radiance # todo: doppler shift? @@ -38,26 +95,44 @@ cdef class Bremsstrahlung(PlasmaModel): """ Emitter that calculates bremsstrahlung emission from a plasma object. - The bremmstrahlung formula implemented is equation 2 from M. Beurskens, - et. al., 'ITER LIDAR performance analysis', Rev. Sci. Instrum. 79, 10E727 (2008), + The bremmstrahlung formula implemented is equation 5.3.40 + from I. H. Hutchinson, 'Principles of Plasma Diagnostics', second edition, + Cambridge University Press, 2002, ISBN: 9780511613630, + https://doi.org/10.1017/CBO9780511613630 + + Note that in eq. 5.3.40, the emissivity :math:`j(\\nu)` is given in (W/m^3/sr/Hz) with respect + to frequency, :math:`\\nu`. Here, the emissivity :math:`\\epsilon_{\\mathrm{ff}}(\\lambda)` + is given in (W/m^3/nm/sr) with respect to wavelength, :math:`\\lambda = \\frac{10^{9} c}{\\nu}`, + and taking into account that :math:`d\\nu=-\\frac{10^{9} c}{\\lambda^2}d\\lambda`. .. math:: - \\epsilon (\\lambda) = \\frac{0.95 \\times 10^{-19}}{\\lambda 4 \\pi} \\sum_{i} \\left(g_{ff}(Z_i, T_e, \\lambda) n_i Z_i^2\\right) n_e T_e^{-1/2} \\times \\exp{\\frac{-hc}{\\lambda T_e}}, + \\epsilon_{\\mathrm{ff}}(\\lambda) = \\left( \\frac{e^2}{4 \\pi \\varepsilon_0} \\right)^3 + \\frac{32 \\pi^2}{3 \\sqrt{3} m_\\mathrm{e}^2 c^3} + \\sqrt{\\frac{2 m_\\mathrm{e}^3}{\\pi e T_\\mathrm{e}}} + \\frac{10^{9} c}{4 \\pi \\lambda^2} + n_\\mathrm{e} \\sum_i \\left( n_\\mathrm{i} g_\\mathrm{ff} (Z_\\mathrm{i}, T_\\mathrm{e}, \\lambda) Z_\\mathrm{i}^2 \\right) + \\mathrm{e}^{-\\frac{10^9 hc}{e T_\\mathrm{e} \\lambda}}\\,, - where the emission :math:`\\epsilon (\\lambda)` is in units of radiance (ph/s/sr/m^3/nm). + where :math:`T_\\mathrm{e}` is in eV and :math:`\\lambda` is in nm. + + :math:`g_\\mathrm{ff} (Z_\\mathrm{i}, T_\\mathrm{e}, \\lambda)` is the free-free Gaunt factor. :ivar Plasma plasma: The plasma to which this emission model is attached. Default is None. :ivar AtomicData atomic_data: The atomic data provider for this model. Default is None. :ivar FreeFreeGauntFactor gaunt_factor: Free-free Gaunt factor as a function of Z, Te and wavelength. If not provided, the `atomic_data` is used. + :ivar Integrator1D integrator: Integrator1D instance to integrate Bremsstrahlung radiation + over the spectral bin. Default is `GaussianQuadrature`. """ - def __init__(self, Plasma plasma=None, AtomicData atomic_data=None, FreeFreeGauntFactor gaunt_factor=None): + def __init__(self, Plasma plasma=None, AtomicData atomic_data=None, FreeFreeGauntFactor gaunt_factor=None, Integrator1D integrator=None): super().__init__(plasma, atomic_data) + self._brems_func = BremsFunction.__new__(BremsFunction) self.gaunt_factor = gaunt_factor + self.integrator = integrator or GaussianQuadrature() # ensure that cache is initialised self._change() @@ -65,14 +140,25 @@ cdef class Bremsstrahlung(PlasmaModel): @property def gaunt_factor(self): - return self._gaunt_factor + return self._brems_func.gaunt_factor @gaunt_factor.setter def gaunt_factor(self, value): - self._gaunt_factor = value + self._brems_func.gaunt_factor = value self._user_provided_gaunt_factor = True if value else False + @property + def integrator(self): + + return self._integrator + + @integrator.setter + def integrator(self, Integrator1D value not None): + + self._integrator = value + self._integrator.function = self._brems_func + def __repr__(self): return '' @@ -84,13 +170,12 @@ cdef class Bremsstrahlung(PlasmaModel): cdef: double ne, te - double lower_wavelength, upper_wavelength - double lower_sample, upper_sample + double lower_wavelength, upper_wavelength, bin_integral Species species int i # cache data on first run - if self._species_charge is None: + if self._brems_func.species_charge is None: self._populate_cache() ne = self._plasma.get_electron_distribution().density(point.x, point.y, point.z) @@ -100,57 +185,28 @@ cdef class Bremsstrahlung(PlasmaModel): if te <= 0: return spectrum + self._brems_func.ne = ne + self._brems_func.te = te + # collect densities of charged species i = 0 for species in self._plasma.get_composition(): if species.charge > 0: - self._species_density_mv[i] = species.distribution.density(point.x, point.y, point.z) + self._brems_func.species_density_mv[i] = species.distribution.density(point.x, point.y, point.z) i += 1 - # numerically integrate using trapezium rule - # todo: add sub-sampling to increase numerical accuracy + # add bremsstrahlung to spectrum lower_wavelength = spectrum.min_wavelength - lower_sample = self._bremsstrahlung(lower_wavelength, te, ne) for i in range(spectrum.bins): - upper_wavelength = spectrum.min_wavelength + spectrum.delta_wavelength * (i + 1) - upper_sample = self._bremsstrahlung(upper_wavelength, te, ne) - spectrum.samples_mv[i] += 0.5 * (lower_sample + upper_sample) + bin_integral = self._integrator.evaluate(lower_wavelength, upper_wavelength) + spectrum.samples_mv[i] += bin_integral / spectrum.delta_wavelength lower_wavelength = upper_wavelength - lower_sample = upper_sample return spectrum - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cdef double _bremsstrahlung(self, double wvl, double te, double ne): - """ - :param double wvl: Wavelength in nm. - :param double te: Electron temperature in eV - :param double ne: Electron dencity in m^-3 - :return: - """ - - cdef double ni_gff_z2, radiance, pre_factor, ni, z - cdef int i - - ni_gff_z2 = 0 - for i in range(self._species_charge_mv.shape[0]): - z = self._species_charge_mv[i] - ni = self._species_density_mv[i] - if ni > 0: - ni_gff_z2 += ni * self._gaunt_factor.evaluate(z, te, wvl) * z * z - - # bremsstrahlung equation W/m^3/str/nm - pre_factor = 0.95e-19 * RECIP_4_PI * ni_gff_z2 * ne / (sqrt(te) * wvl) - radiance = pre_factor * exp(- EXP_FACTOR / (te * wvl)) * PH_TO_J_FACTOR - - # convert to W/m^3/str/nm - return radiance / wvl - cdef int _populate_cache(self) except -1: cdef list species_charge @@ -159,12 +215,12 @@ cdef class Bremsstrahlung(PlasmaModel): if self._plasma is None: raise RuntimeError("The emission model is not connected to a plasma object.") - if self._gaunt_factor is None: + if self._brems_func.gaunt_factor is None: if self._atomic_data is None: raise RuntimeError("The emission model is not connected to an atomic data source.") # initialise Gaunt factor on first run using the atomic data - self._gaunt_factor = self._atomic_data.free_free_gaunt_factor() + self._brems_func.gaunt_factor = self._atomic_data.free_free_gaunt_factor() species_charge = [] for species in self._plasma.get_composition(): @@ -172,18 +228,18 @@ cdef class Bremsstrahlung(PlasmaModel): species_charge.append(species.charge) # Gaunt factor takes Z as double to support Zeff, so caching Z as float64 - self._species_charge = np.array(species_charge, dtype=np.float64) - self._species_charge_mv = self._species_charge + self._brems_func.species_charge = np.array(species_charge, dtype=np.float64) + self._brems_func.species_charge_mv = self._brems_func.species_charge - self._species_density = np.zeros_like(self._species_charge) - self._species_density_mv = self._species_density + self._brems_func.species_density = np.zeros_like(self._brems_func.species_charge) + self._brems_func.species_density_mv = self._brems_func.species_density def _change(self): # clear cache to force regeneration on first use if not self._user_provided_gaunt_factor: - self._gaunt_factor = None - self._species_charge = None - self._species_charge_mv = None - self._species_density = None - self._species_density_mv = None + self._brems_func.gaunt_factor = None + self._brems_func.species_charge = None + self._brems_func.species_charge_mv = None + self._brems_func.species_density = None + self._brems_func.species_density_mv = None diff --git a/cherab/core/model/plasma/impact_excitation.pyx b/cherab/core/model/plasma/impact_excitation.pyx index 343059ad..b26336f1 100644 --- a/cherab/core/model/plasma/impact_excitation.pyx +++ b/cherab/core/model/plasma/impact_excitation.pyx @@ -1,6 +1,8 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -18,11 +20,34 @@ from raysect.optical cimport Spectrum, Point3D, Vector3D from cherab.core cimport Plasma, AtomicData -from cherab.core.model.lineshape cimport GaussianLine, LineShapeModel +from cherab.core.model.lineshape cimport GaussianLine from cherab.core.utility.constants cimport RECIP_4_PI cdef class ExcitationLine(PlasmaModel): + r""" + Emitter that calculates spectral line emission from a plasma object + as a result of excitation of the target species by electron impact. + + .. math:: + \epsilon_{\mathrm{excit}}(\lambda) = \frac{1}{4 \pi} n_{Z_\mathrm{i}} n_\mathrm{e} + \mathrm{PEC}_{\mathrm{excit}}(n_\mathrm{e}, T_\mathrm{e}) f(\lambda), + + where :math:`n_{Z_\mathrm{i}}` is the target species density, + :math:`\mathrm{PEC}_{\mathrm{excit}}` is the electron impact excitation photon emission coefficient + for the specified spectral line of the :math:`Z_\mathrm{i}` ion, + :math:`f(\lambda)` is the normalised spectral line shape, + + :param Line line: Spectroscopic emission line object. + :param Plasma plasma: The plasma to which this emission model is attached. Default is None. + :param AtomicData atomic_data: The atomic data provider for this model. Default is None. + :param object lineshape: Line shape model class. Default is None (GaussianLine). + :param object lineshape_args: A list of line shape model arguments. Default is None. + :param object lineshape_kwargs: A dictionary of line shape model keyword arguments. Default is None. + + :ivar Plasma plasma: The plasma to which this emission model is attached. + :ivar AtomicData atomic_data: The atomic data provider for this model. + """ def __init__(self, Line line, Plasma plasma=None, AtomicData atomic_data=None, object lineshape=None, object lineshape_args=None, object lineshape_kwargs=None): @@ -100,7 +125,7 @@ cdef class ExcitationLine(PlasmaModel): # instance line shape renderer self._lineshape = self._lineshape_class(self._line, self._wavelength, self._target_species, self._plasma, - *self._lineshape_args, **self._lineshape_kwargs) + self._atomic_data, *self._lineshape_args, **self._lineshape_kwargs) def _change(self): diff --git a/cherab/core/model/plasma/recombination.pyx b/cherab/core/model/plasma/recombination.pyx index 6edac138..a33009d0 100644 --- a/cherab/core/model/plasma/recombination.pyx +++ b/cherab/core/model/plasma/recombination.pyx @@ -1,6 +1,8 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# cython: language_level=3 + +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -23,6 +25,29 @@ from cherab.core.utility.constants cimport RECIP_4_PI cdef class RecombinationLine(PlasmaModel): + r""" + Emitter that calculates spectral line emission from a plasma object + as a result of dielectronic recombination of the target species. + + .. math:: + \epsilon_{\mathrm{recomb}}(\lambda) = \frac{1}{4 \pi} n_{Z_\mathrm{i} + 1} n_\mathrm{e} + \mathrm{PEC}_{\mathrm{recomb}}(n_\mathrm{e}, T_\mathrm{e}) f(\lambda), + + where :math:`n_{Z_\mathrm{i} + 1}` is the recombining species density, + :math:`\mathrm{PEC}_{\mathrm{recomb}}` is the dielectronic recombination photon emission coefficient + for the specified spectral line of the :math:`Z_\mathrm{i}` ion, + :math:`f(\lambda)` is the normalised spectral line shape, + + :param Line line: Spectroscopic emission line object. + :param Plasma plasma: The plasma to which this emission model is attached. Default is None. + :param AtomicData atomic_data: The atomic data provider for this model. Default is None. + :param object lineshape: Line shape model class. Default is None (GaussianLine). + :param object lineshape_args: A list of line shape model arguments. Default is None. + :param object lineshape_kwargs: A dictionary of line shape model keyword arguments. Default is None. + + :ivar Plasma plasma: The plasma to which this emission model is attached. + :ivar AtomicData atomic_data: The atomic data provider for this model. + """ def __init__(self, Line line, Plasma plasma=None, AtomicData atomic_data=None, object lineshape=None, object lineshape_args=None, object lineshape_kwargs=None): @@ -103,7 +128,7 @@ cdef class RecombinationLine(PlasmaModel): # instance line shape renderer self._lineshape = self._lineshape_class(self._line, self._wavelength, self._target_species, self._plasma, - *self._lineshape_args, **self._lineshape_kwargs) + self._atomic_data, *self._lineshape_args, **self._lineshape_kwargs) def _change(self): diff --git a/cherab/core/model/plasma/thermal_cx.pxd b/cherab/core/model/plasma/thermal_cx.pxd new file mode 100644 index 00000000..636fddaa --- /dev/null +++ b/cherab/core/model/plasma/thermal_cx.pxd @@ -0,0 +1,35 @@ +# Copyright 2016-2018 Euratom +# Copyright 2016-2018 United Kingdom Atomic Energy Authority +# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from cherab.core.atomic cimport Line +from cherab.core.plasma cimport PlasmaModel +from cherab.core.species cimport Species +from cherab.core.model.lineshape cimport LineShapeModel + + +cdef class ThermalCXLine(PlasmaModel): + + cdef: + Line _line + double _wavelength + Species _target_species + list _rates + LineShapeModel _lineshape + object _lineshape_class, _lineshape_args, _lineshape_kwargs + + cdef int _populate_cache(self) except -1 diff --git a/cherab/core/model/plasma/thermal_cx.pyx b/cherab/core/model/plasma/thermal_cx.pyx new file mode 100644 index 00000000..88af9ae8 --- /dev/null +++ b/cherab/core/model/plasma/thermal_cx.pyx @@ -0,0 +1,163 @@ +# Copyright 2016-2018 Euratom +# Copyright 2016-2018 United Kingdom Atomic Energy Authority +# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +from raysect.optical cimport Spectrum, Point3D, Vector3D +from cherab.core cimport Plasma, AtomicData +from cherab.core.atomic cimport ThermalCXPEC +from cherab.core.model.lineshape cimport GaussianLine, LineShapeModel +from cherab.core.utility.constants cimport RECIP_4_PI + + +cdef class ThermalCXLine(PlasmaModel): + r""" + Emitter that calculates spectral line emission from a plasma object + as a result of thermal charge exchange of the target species with the donor species. + + .. math:: + \epsilon_{\mathrm{CX}}(\lambda) = \frac{1}{4 \pi} n_{Z_\mathrm{i} + 1} + \sum_j{n_{Z_\mathrm{j}} \mathrm{PEC}_{\mathrm{cx}}(n_\mathrm{e}, T_\mathrm{e}, T_{Z_\mathrm{j}})} + f(\lambda), + + where :math:`n_{Z_\mathrm{i} + 1}` is the receiver species density, + :math:`n_{Z_\mathrm{j}}` is the donor species density, + :math:`\mathrm{PEC}_{\mathrm{cx}}` is the thermal CX photon emission coefficient + for the specified spectral line of the :math:`Z_\mathrm{i}` ion, + :math:`T_{Z_\mathrm{j}}` is the donor species temperature, + :math:`f(\lambda)` is the normalised spectral line shape, + + :param Line line: Spectroscopic emission line object. + :param Plasma plasma: The plasma to which this emission model is attached. Default is None. + :param AtomicData atomic_data: The atomic data provider for this model. Default is None. + :param object lineshape: Line shape model class. Default is None (GaussianLine). + :param object lineshape_args: A list of line shape model arguments. Default is None. + :param object lineshape_kwargs: A dictionary of line shape model keyword arguments. Default is None. + + :ivar Plasma plasma: The plasma to which this emission model is attached. + :ivar AtomicData atomic_data: The atomic data provider for this model. + """ + + def __init__(self, Line line, Plasma plasma=None, AtomicData atomic_data=None, object lineshape=None, + object lineshape_args=None, object lineshape_kwargs=None): + + super().__init__(plasma, atomic_data) + + self._line = line + + self._lineshape_class = lineshape or GaussianLine + if not issubclass(self._lineshape_class, LineShapeModel): + raise TypeError("The attribute lineshape must be a subclass of LineShapeModel.") + + if lineshape_args: + self._lineshape_args = lineshape_args + else: + self._lineshape_args = [] + if lineshape_kwargs: + self._lineshape_kwargs = lineshape_kwargs + else: + self._lineshape_kwargs = {} + + # ensure that cache is initialised + self._change() + + def __repr__(self): + return ''.format(self._line.element.name, self._line.charge, self._line.transition) + + cpdef Spectrum emission(self, Point3D point, Vector3D direction, Spectrum spectrum): + + cdef: + double ne, te, receiver_density, donor_density, donor_temperature, weighted_rate, radiance + Species species + ThermalCXPEC rate + + # cache data on first run + if self._target_species is None: + self._populate_cache() + + ne = self._plasma.get_electron_distribution().density(point.x, point.y, point.z) + if ne <= 0.0: + return spectrum + + te = self._plasma.get_electron_distribution().effective_temperature(point.x, point.y, point.z) + if te <= 0.0: + return spectrum + + receiver_density = self._target_species.distribution.density(point.x, point.y, point.z) + if receiver_density <= 0.0: + return spectrum + + # obtain composite CX PEC by iterating over all possible CX donors + weighted_rate = 0 + for species, rate in self._rates: + donor_density = species.distribution.density(point.x, point.y, point.z) + donor_temperature = species.distribution.effective_temperature(point.x, point.y, point.z) + weighted_rate += donor_density * rate.evaluate(ne, te, donor_temperature) + + # add emission line to spectrum + radiance = RECIP_4_PI * weighted_rate * receiver_density + return self._lineshape.add_line(radiance, point, direction, spectrum) + + cdef int _populate_cache(self) except -1: + + cdef: + int receiver_charge + Species species + ThermalCXPEC rate + + # sanity checks + if self._plasma is None: + raise RuntimeError("The emission model is not connected to a plasma object.") + if self._atomic_data is None: + raise RuntimeError("The emission model is not connected to an atomic data source.") + + if self._line is None: + raise RuntimeError("The emission line has not been set.") + + # locate target species + receiver_charge = self._line.charge + 1 + try: + self._target_species = self._plasma.composition.get(self._line.element, receiver_charge) + except ValueError: + raise RuntimeError("The plasma object does not contain the ion species for the specified CX line " + "(element={}, ionisation={}).".format(self._line.element.symbol, receiver_charge)) + + # obtain rate functions + self._rates = [] + # iterate over all posible electron donors in plasma composition + # and for each donor, cache the PEC rate function for the CX reaction with this receiver + for species in self._plasma.composition: + # exclude the receiver species from the list of donors and omit fully ionised species + if species != self._target_species and species.charge < species.element.atomic_number: + rate = self._atomic_data.thermal_cx_pec(species.element, species.charge, # donor + self._line.element, receiver_charge, # receiver + self._line.transition) + self._rates.append((species, rate)) + + # identify wavelength + self._wavelength = self._atomic_data.wavelength(self._line.element, self._line.charge, self._line.transition) + + # instance line shape renderer + self._lineshape = self._lineshape_class(self._line, self._wavelength, self._target_species, self._plasma, + self._atomic_data, *self._lineshape_args, **self._lineshape_kwargs) + + def _change(self): + + # clear cache to force regeneration on first use + self._target_species = None + self._wavelength = 0.0 + self._rates = None + self._lineshape = None diff --git a/cherab/core/model/plasma/total_radiated_power.pxd b/cherab/core/model/plasma/total_radiated_power.pxd index 1900d965..59499ba2 100644 --- a/cherab/core/model/plasma/total_radiated_power.pxd +++ b/cherab/core/model/plasma/total_radiated_power.pxd @@ -18,7 +18,7 @@ from cherab.core.atomic.elements cimport Element -from cherab.core.atomic.rates cimport LineRadiationPower, ContinuumPower +from cherab.core.atomic.rates cimport LineRadiationPower, ContinuumPower, CXRadiationPower from cherab.core.plasma cimport PlasmaModel from cherab.core.species cimport Species @@ -30,7 +30,9 @@ cdef class TotalRadiatedPower(PlasmaModel): Element _element int _charge Species _line_rad_species, _recom_species + list _hydrogen_species LineRadiationPower _plt_rate ContinuumPower _prb_rate + CXRadiationPower _prc_rate cdef int _populate_cache(self) except -1 diff --git a/cherab/core/model/plasma/total_radiated_power.pyx b/cherab/core/model/plasma/total_radiated_power.pyx index 28350d4e..84b42424 100644 --- a/cherab/core/model/plasma/total_radiated_power.pyx +++ b/cherab/core/model/plasma/total_radiated_power.pyx @@ -20,9 +20,40 @@ from raysect.optical cimport Spectrum, Point3D, Vector3D from cherab.core cimport Plasma, AtomicData from cherab.core.utility.constants cimport RECIP_4_PI +from cherab.core.atomic.elements import hydrogen, deuterium, tritium cdef class TotalRadiatedPower(PlasmaModel): + r""" + Emitter that calculates total power radiated by a given ion, which includes: + + - line power due to electron impact excitation, + - continuum and line power due to recombination and Bremsstrahlung, + - line power due to charge exchange with thermal neutral hydrogen and its isotopes. + + The emission calculated by this model is spectrally unresolved, + which means that the total radiated power will be spread of the entire + observable spectral range. + + .. math:: + \epsilon_{\mathrm{total}} = \frac{1}{4 \pi \Delta\lambda} \left( + n_{Z_\mathrm{i}} n_\mathrm{e} C_{\mathrm{excit}}(n_\mathrm{e}, T_\mathrm{e}) + + n_{Z_\mathrm{i} + 1} n_\mathrm{e} C_{\mathrm{recomb}}(n_\mathrm{e}, T_\mathrm{e}) + + n_{Z_\mathrm{i} + 1} n_\mathrm{hyd} C_{\mathrm{cx}}(n_\mathrm{e}, T_\mathrm{e}) \right) + + where :math:`n_{Z_\mathrm{i}}` is the target species density; + :math:`n_{Z_\mathrm{i} + 1}` is the recombining species density; + :math:`n_{\mathrm{hyd}}` is the total density of all hydrogen isotopes; + :math:`C_{\mathrm{excit}}, C_{\mathrm{recomb}}, C_{\mathrm{cx}}` are the radiated power + coefficients in :math:`W m^3` due to electron impact excitation, recombination + + Bremsstrahlung and charge exchange with thermal neutral hydrogen, respectively; + :math:`\Delta\lambda` is the observable spectral range. + + :param Element element: The atomic element/isotope. + :param int charge: The charge state of the element/isotope. + :param Plasma plasma: The plasma to which this emission model is attached. Default is None. + :param AtomicData atomic_data: The atomic data provider for this model. Default is None. + """ def __init__(self, Element element, int charge, Plasma plasma=None, AtomicData atomic_data=None): @@ -42,7 +73,9 @@ cdef class TotalRadiatedPower(PlasmaModel): cdef: int i - double ne, ni, ni_upper, te, plt_radiance, prb_radiance + double ne, ni, ni_upper, nhyd, te + double power_density, radiance + Species hyd_species # cache data on first run if not self._cache_loaded: @@ -60,22 +93,35 @@ cdef class TotalRadiatedPower(PlasmaModel): ni_upper = self._recom_species.distribution.density(point.x, point.y, point.z) + nhyd = 0 + for hyd_species in self._hydrogen_species: + nhyd += hyd_species.distribution.density(point.x, point.y, point.z) + # add emission to spectrum - if self._plt_rate and ni > 0: - plt_radiance = RECIP_4_PI * self._plt_rate.evaluate(ne, te) * ne * ni / (spectrum.max_wavelength - spectrum.min_wavelength) - else: - plt_radiance = 0 - if self._prb_rate and ni_upper > 0: - prb_radiance = RECIP_4_PI * self._prb_rate.evaluate(ne, te) * ne * ni_upper / (spectrum.max_wavelength - spectrum.min_wavelength) - else: - prb_radiance = 0 + power_density = 0 + + if self._plt_rate and ni > 0: # excitation + power_density += self._plt_rate.evaluate(ne, te) * ne * ni + + if self._prb_rate and ni_upper > 0: # recombination + bremsstrahlung + power_density += self._prb_rate.evaluate(ne, te) * ne * ni_upper + + if self._prc_rate and ni_upper > 0 and nhyd > 0: # charge exchange + power_density += self._prc_rate.evaluate(ne, te) * nhyd * ni_upper + + radiance = RECIP_4_PI * power_density / (spectrum.max_wavelength - spectrum.min_wavelength) + for i in range(spectrum.bins): - spectrum.samples_mv[i] += plt_radiance + prb_radiance + spectrum.samples_mv[i] += radiance return spectrum cdef int _populate_cache(self) except -1: + cdef: + Species hyd_species + Element hyd_isotope + # sanity checks if self._plasma is None: raise RuntimeError("The emission model is not connected to a plasma object.") @@ -85,7 +131,7 @@ cdef class TotalRadiatedPower(PlasmaModel): # cache line radiation species and rate self._plt_rate = self._atomic_data.line_radiated_power_rate(self._element, self._charge) try: - self._line_rad_species = self._plasma.composition.get(self._element, self._charge) + self._line_rad_species = self._plasma.get_composition().get(self._element, self._charge) except ValueError: raise RuntimeError("The plasma object does not contain the required ion species for calculating" "total line radiaton, (element={}, ionisation={})." @@ -94,12 +140,24 @@ cdef class TotalRadiatedPower(PlasmaModel): # cache recombination species and radiation rate self._prb_rate = self._atomic_data.continuum_radiated_power_rate(self._element, self._charge+1) try: - self._recom_species = self._plasma.composition.get(self._element, self._charge+1) + self._recom_species = self._plasma.get_composition().get(self._element, self._charge+1) except ValueError: raise RuntimeError("The plasma object does not contain the required ion species for calculating" "recombination/continuum emission, (element={}, ionisation={})." "".format(self._element.symbol, self._charge+1)) + # cache hydrogen species and CX radiation rate + self._prc_rate = self._atomic_data.cx_radiated_power_rate(self._element, self._charge+1) + + self._hydrogen_species = [] + for hyd_isotope in (hydrogen, deuterium, tritium): + try: + hyd_species = self._plasma.get_composition().get(hyd_isotope, 0) + except ValueError: + pass + else: + self._hydrogen_species.append(hyd_species) + self._cache_loaded = True def _change(self): @@ -108,5 +166,7 @@ cdef class TotalRadiatedPower(PlasmaModel): self._cache_loaded = False self._line_rad_species = None self._recom_species = None + self._hydrogen_species = None self._plt_rate = None self._prb_rate = None + self._prc_rate = None diff --git a/cherab/core/plasma/node.pyx b/cherab/core/plasma/node.pyx index 1122aa54..08d31069 100644 --- a/cherab/core/plasma/node.pyx +++ b/cherab/core/plasma/node.pyx @@ -394,7 +394,7 @@ cdef class Plasma(Node): @cython.cdivision(True) cpdef double z_effective(self, double x, double y, double z) except -1: - """ + r""" Calculates the effective Z of the plasma. .. math:: @@ -435,7 +435,7 @@ cdef class Plasma(Node): @cython.boundscheck(False) @cython.wraparound(False) cpdef double ion_density(self, double x, double y, double z): - """ + r""" Calculates the total ion density of the plasma. .. math:: diff --git a/cherab/core/tests/test_beam.py b/cherab/core/tests/test_beam.py new file mode 100644 index 00000000..7d2ec203 --- /dev/null +++ b/cherab/core/tests/test_beam.py @@ -0,0 +1,162 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import unittest + +import numpy as np + +from raysect.core import World, Vector3D, translate + +from cherab.core import Beam +from cherab.core.atomic import AtomicData, BeamStoppingRate +from cherab.core.atomic import deuterium +from cherab.tools.plasmas.slab import build_constant_slab_plasma +from cherab.core.model import SingleRayAttenuator + +from cherab.core.utility import EvAmuToMS, EvToJ + + +class ConstantBeamStoppingRate(BeamStoppingRate): + """ + Constant beam CX PEC for test purpose. + """ + + def __init__(self, donor_metastable, value): + self.donor_metastable = donor_metastable + self.value = value + + def evaluate(self, energy, density, temperature): + + return self.value + + +class MockAtomicData(AtomicData): + """Fake atomic data for test purpose.""" + + def beam_stopping_rate(self, beam_ion, plasma_ion, charge): + + return ConstantBeamStoppingRate(1, 1.e-13) + + +class TestBeam(unittest.TestCase): + + def setUp(self): + + self.atomic_data = MockAtomicData() + + self.world = World() + + self.plasma_density = 1.e19 + self.plasma_temperature = 1.e3 + plasma_species = [(deuterium, 1, self.plasma_density, self.plasma_temperature, Vector3D(0, 0, 0))] + plasma = build_constant_slab_plasma(length=1, width=1, height=1, + electron_density=self.plasma_density, + electron_temperature=self.plasma_temperature, + plasma_species=plasma_species) + plasma.atomic_data = self.atomic_data + plasma.parent = self.world + + beam = Beam(transform=translate(0.5, 0, 0)) + beam.atomic_data = self.atomic_data + beam.plasma = plasma + beam.attenuator = SingleRayAttenuator(clamp_to_zero=True) + beam.energy = 50000 + beam.power = 1e6 + beam.temperature = 10 + beam.element = deuterium + beam.parent = self.world + beam.sigma = 0.2 + beam.divergence_x = 1. + beam.divergence_y = 2. + beam.length = 10. + + self.plasma = plasma + self.beam = beam + + def test_beam_density(self): + + z0 = 0.8 + x0, y0 = 0.5, 0.5 + + density_on_axis = self.beam.density(0, 0, z0) + density_off_axis = self.beam.density(x0, y0, z0) + density_outside_beam = self.beam.density(0, 0, -1) + + # validating + + speed = EvAmuToMS.to(self.beam.energy) + # constant stopping rate + stopping_rate = self.atomic_data.beam_stopping_rate(deuterium, deuterium, 1)(0, 0, 0) + attenuation_factor = np.exp(-z0 * self.plasma_density * stopping_rate / speed) + + beam_particle_rate = self.beam.power / EvToJ.to(self.beam.energy * deuterium.atomic_weight) + + sigma0_sqr = self.beam.sigma**2 + tanxdiv = np.tan(np.deg2rad(self.beam.divergence_x)) + tanydiv = np.tan(np.deg2rad(self.beam.divergence_y)) + sigma_x = np.sqrt(sigma0_sqr + (z0 * tanxdiv)**2) + sigma_y = np.sqrt(sigma0_sqr + (z0 * tanydiv)**2) + + norm_radius_sqr = ((x0 / sigma_x)**2 + (y0 / sigma_y)**2) + + gaussian_sample_on_axis = 1. / (2 * np.pi * sigma_x * sigma_y) + gaussian_sample_off_axis = np.exp(-0.5 * norm_radius_sqr) / (2 * np.pi * sigma_x * sigma_y) + + test_density_on_axis = beam_particle_rate / speed * gaussian_sample_on_axis * attenuation_factor + test_density_off_axis = beam_particle_rate / speed * gaussian_sample_off_axis * attenuation_factor + + self.assertAlmostEqual(density_on_axis / test_density_on_axis, 1., delta=1.e-12, + msg='Beam.density() gives a wrong value on the beam axis.') + self.assertAlmostEqual(density_off_axis / test_density_off_axis, 1., delta=1.e-12, + msg='Beam.density() gives a wrong value off the beam axis.') + self.assertEqual(density_outside_beam, 0, + msg='Beam.density() gives a non-zero value outside beam.') + + def test_beam_direction(self): + # setting up the model + + z0 = 0.8 + x0, y0 = 0.5, 0.5 + + direction_on_axis = self.beam.direction(0, 0, z0) + direction_off_axis = self.beam.direction(x0, y0, z0) + direction_outside_beam = self.beam.direction(0, 0, -1) + + # validating + + sigma0_sqr = self.beam.sigma**2 + z_tanx_sqr = (z0 * np.tan(np.deg2rad(self.beam.divergence_x)))**2 + z_tany_sqr = (z0 * np.tan(np.deg2rad(self.beam.divergence_y)))**2 + + ex = x0 * z_tanx_sqr / (sigma0_sqr + z_tanx_sqr) + ey = y0 * z_tany_sqr / (sigma0_sqr + z_tany_sqr) + ez = z0 + + test_direction_off_axis = Vector3D(ex, ey, ez).normalise() + + self.assertEqual(direction_on_axis, Vector3D(0, 0, 1), + msg='Beam.density() gives a wrong value on the beam axis.') + for v, test_v in zip(direction_off_axis, test_direction_off_axis): + self.assertAlmostEqual(v, test_v, delta=1.e-12, + msg='Beam.direction() gives a wrong value off the beam axis.') + self.assertEqual(direction_outside_beam, Vector3D(0, 0, 1), + msg='Beam.density() gives a non-zero value outside beam.') + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/tests/test_beamcxline.py b/cherab/core/tests/test_beamcxline.py new file mode 100644 index 00000000..a93cf946 --- /dev/null +++ b/cherab/core/tests/test_beamcxline.py @@ -0,0 +1,173 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import unittest + +import numpy as np + +from raysect.core import Point3D, Vector3D, translate +from raysect.optical import World, Spectrum, Ray + +from cherab.core import Beam +from cherab.core.atomic import Line, AtomicData, BeamCXPEC, BeamStoppingRate +from cherab.core.atomic import deuterium +from cherab.tools.plasmas.slab import build_constant_slab_plasma +from cherab.core.model import SingleRayAttenuator, BeamCXLine, GaussianLine, ZeemanTriplet + + +class ConstantBeamCXPEC(BeamCXPEC): + """ + Constant beam CX PEC for test purpose. + """ + + def __init__(self, donor_metastable, value): + super().__init__(donor_metastable) + self.value = value + + def evaluate(self, energy, temperature, density, z_effective, b_field): + + return self.value + + +class ConstantBeamStoppingRate(BeamStoppingRate): + """ + Constant beam CX PEC for test purpose. + """ + + def __init__(self, donor_metastable, value): + self.donor_metastable = donor_metastable + self.value = value + + def evaluate(self, energy, density, temperature): + + return self.value + + +class MockAtomicData(AtomicData): + """Fake atomic data for test purpose.""" + + def beam_cx_pec(self, donor_ion, receiver_ion, receiver_charge, transition): + + return [ConstantBeamCXPEC(1, 3.4e-34)] + + def beam_stopping_rate(self, beam_ion, plasma_ion, charge): + + return ConstantBeamStoppingRate(1, 0) + + def wavelength(self, ion, charge, transition): + + return 656.104 + + +class TestBeamCXLine(unittest.TestCase): + + def setUp(self): + + self.world = World() + + self.atomic_data = MockAtomicData() + + plasma_species = [(deuterium, 1, 1.e19, 200., Vector3D(0, 0, 0))] + plasma = build_constant_slab_plasma(length=1, width=1, height=1, + electron_density=1e19, + electron_temperature=200., + plasma_species=plasma_species, + b_field=Vector3D(0, 10., 0)) + plasma.atomic_data = self.atomic_data + plasma.parent = self.world + + beam = Beam(transform=translate(0.5, 0, 0)) + beam.atomic_data = self.atomic_data + beam.plasma = plasma + beam.attenuator = SingleRayAttenuator(clamp_to_zero=True) + beam.energy = 50000 + beam.power = 1e6 + beam.temperature = 10 + beam.element = deuterium + beam.parent = self.world + + self.plasma = plasma + self.beam = beam + + def test_default_lineshape(self): + # setting up the model + line = Line(deuterium, 0, (3, 2)) # D-alpha line + self.beam.models = [BeamCXLine(line)] + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=655.1, max_wavelength=657.1, bins=512) + cx_spectrum = ray.trace(self.world) + + # validating + dx = self.beam.integrator.step + rate = self.atomic_data.beam_cx_pec(deuterium, deuterium, 1, (3, 2))[0].value + ni = self.plasma.ion_density(0.5, 0, 0) # constant slab + nd_beam = 0 # beam density + for i in range(-int(0.5 / dx), int(0.5 / dx)): + x = dx * (i + 0.5) + nd_beam += self.beam.density(x, 0, 0) # in internal beam coordinates + radiance = 0.25 * rate * ni * nd_beam * dx / np.pi + + target_species = self.plasma.composition.get(line.element, line.charge + 1) + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = gaussian_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(cx_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='BeamCXLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + def test_custom_lineshape(self): + # setting up the model + line = Line(deuterium, 0, (3, 2)) # D-alpha line + self.beam.models = [BeamCXLine(line, lineshape=ZeemanTriplet)] + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=655.1, max_wavelength=657.1, bins=512) + cx_spectrum = ray.trace(self.world) + + # validating + dx = self.beam.integrator.step + rate = self.atomic_data.beam_cx_pec(deuterium, deuterium, 1, (3, 2))[0].value + ni = self.plasma.ion_density(0.5, 0, 0) # constant slab + nd_beam = 0 # beam density + for i in range(-int(0.5 / dx), int(0.5 / dx)): + x = dx * (i + 0.5) + nd_beam += self.beam.density(x, 0, 0) # in internal beam coordinates + radiance = 0.25 * rate * ni * nd_beam * dx / np.pi + + target_species = self.plasma.composition.get(line.element, line.charge + 1) + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + zeeman_line = ZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = zeeman_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(cx_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='BeamCXLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/tests/test_bremsstrahlung.py b/cherab/core/tests/test_bremsstrahlung.py new file mode 100644 index 00000000..a77a1da5 --- /dev/null +++ b/cherab/core/tests/test_bremsstrahlung.py @@ -0,0 +1,99 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import unittest + +import numpy as np + +from raysect.core import Point3D, Vector3D +from raysect.optical import World, Ray + +from cherab.core.atomic import AtomicData, MaxwellianFreeFreeGauntFactor +from cherab.core.math.integrators import GaussianQuadrature +from cherab.core.atomic import deuterium, nitrogen +from cherab.tools.plasmas.slab import build_constant_slab_plasma +from cherab.core.model import Bremsstrahlung + +import scipy.constants as const + + +class TestBremsstrahlung(unittest.TestCase): + + def setUp(self): + + self.world = World() + + plasma_species = [(deuterium, 1, 1.e19, 2000., Vector3D(0, 0, 0)), + (nitrogen, 7, 1.e18, 2000., Vector3D(0, 0, 0))] + self.plasma = build_constant_slab_plasma(length=1, width=1, height=1, + electron_density=1e19, + electron_temperature=2000., + plasma_species=plasma_species) + self.plasma.parent = self.world + self.plasma.atomic_data = AtomicData() + + def test_bremsstrahlung_model(self): + # setting up the model + gaunt_factor = MaxwellianFreeFreeGauntFactor() + bremsstrahlung = Bremsstrahlung(gaunt_factor=gaunt_factor) + self.plasma.models = [bremsstrahlung] + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=400., max_wavelength=800., bins=128) + brems_spectrum = ray.trace(self.world) + + # validating + brems_const = (const.e**2 * 0.25 / np.pi / const.epsilon_0)**3 + brems_const *= 32 * np.pi**2 / (3 * np.sqrt(3) * const.m_e**2 * const.c**3) + brems_const *= np.sqrt(2 * const.m_e / (np.pi * const.e)) + brems_const *= const.c * 1e9 * 0.25 / np.pi + exp_factor = const.h * const.c * 1.e9 / const. e + + ne = self.plasma.electron_distribution.density(0.5, 0, 0) + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + + def brems_func(wvl): + ni_gff_z2 = 0 + for species in self.plasma.composition: + z = species.charge + ni = self.plasma.composition[(species.element, species.charge)].distribution.density(0.5, 0, 0) + ni_gff_z2 += ni * gaunt_factor(z, te, wvl) * z * z + + return brems_const * ni_gff_z2 * ne / (np.sqrt(te) * wvl * wvl) * np.exp(- exp_factor / (te * wvl)) + + integrator = GaussianQuadrature(brems_func) + + test_samples = np.zeros(brems_spectrum.bins) + delta_wavelength = (brems_spectrum.max_wavelength - brems_spectrum.min_wavelength) / brems_spectrum.bins + lower_wavelength = brems_spectrum.min_wavelength + for i in range(brems_spectrum.bins): + upper_wavelength = brems_spectrum.min_wavelength + delta_wavelength * (i + 1) + bin_integral = integrator(lower_wavelength, upper_wavelength) + test_samples[i] = bin_integral / delta_wavelength + lower_wavelength = upper_wavelength + + for i in range(brems_spectrum.bins): + self.assertAlmostEqual(brems_spectrum.samples[i], test_samples[i], delta=1e-10, + msg='BeamCXLine model gives a wrong value at {} nm.'.format(brems_spectrum.wavelengths[i])) + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/tests/test_line_emission.py b/cherab/core/tests/test_line_emission.py new file mode 100644 index 00000000..bf40aac0 --- /dev/null +++ b/cherab/core/tests/test_line_emission.py @@ -0,0 +1,324 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import unittest + +import numpy as np + +from raysect.core import Point3D, Vector3D, translate +from raysect.optical import World, Spectrum, Ray + +from cherab.core.atomic import Line, AtomicData, ImpactExcitationPEC, RecombinationPEC, ThermalCXPEC +from cherab.core.atomic import deuterium, carbon +from cherab.tools.plasmas.slab import build_constant_slab_plasma +from cherab.core.model import ExcitationLine, RecombinationLine, ThermalCXLine, GaussianLine, ZeemanTriplet + + +class ConstantImpactExcitationPEC(ImpactExcitationPEC): + """ + Constant electron impact excitation PEC for test purpose. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, density, temperature): + + return self.value + + +class ConstantRecombinationPEC(RecombinationPEC): + """ + Constant recombination PEC for test purpose. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, density, temperature): + + return self.value + + +class ConstantThermalCXPEC(ThermalCXPEC): + """ + Constant recombination PEC for test purpose. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, electron_density, electron_temperature, donor_temperature): + + return self.value + + +class MockAtomicData(AtomicData): + """Fake atomic data for test purpose.""" + + def impact_excitation_pec(self, ion, charge, transition): + + return ConstantImpactExcitationPEC(1.4e-39) + + def recombination_pec(self, ion, charge, transition): + + return ConstantRecombinationPEC(8.e-41) + + def thermal_cx_pec(self, donor_ion, donor_charge, receiver_ion, receiver_charge, transition): + + return ConstantThermalCXPEC(1.2e-46) + + def wavelength(self, ion, charge, transition): + + return 529.27 + + +class TestExcitationLine(unittest.TestCase): + + def setUp(self): + + self.world = World() + + self.atomic_data = MockAtomicData() + + plasma_species = [(carbon, 5, 2.e18, 800., Vector3D(0, 0, 0))] + self.slab_length = 1.2 + self.plasma = build_constant_slab_plasma(length=self.slab_length, width=1, height=1, + electron_density=1e19, electron_temperature=1000., + plasma_species=plasma_species, b_field=Vector3D(0, 10., 0)) + self.plasma.atomic_data = self.atomic_data + self.plasma.parent = self.world + + def test_default_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [ExcitationLine(line)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + excit_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.impact_excitation_pec(line.element, line.charge, line.transition)(ne, te) + target_species = self.plasma.composition.get(line.element, line.charge) + ni = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * ni * ne * self.slab_length + + gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = gaussian_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(excit_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='ExcitationLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + def test_custom_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [ExcitationLine(line, lineshape=ZeemanTriplet)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + excit_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.impact_excitation_pec(line.element, line.charge, line.transition)(ne, te) + target_species = self.plasma.composition.get(line.element, line.charge) + ni = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * ni * ne * self.slab_length + + zeeman_line = ZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = zeeman_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(excit_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='ExcitationLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + +class TestRecombinationLine(unittest.TestCase): + + def setUp(self): + + self.world = World() + + self.atomic_data = MockAtomicData() + + plasma_species = [(carbon, 6, 1.67e18, 800., Vector3D(0, 0, 0))] + self.slab_length = 1.2 + self.plasma = build_constant_slab_plasma(length=self.slab_length, width=1, height=1, + electron_density=1e19, electron_temperature=1000., + plasma_species=plasma_species, b_field=Vector3D(0, 10., 0)) + self.plasma.atomic_data = self.atomic_data + self.plasma.parent = self.world + + def test_default_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [RecombinationLine(line)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + recomb_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.recombination_pec(line.element, line.charge, line.transition)(ne, te) + target_species = self.plasma.composition.get(line.element, line.charge + 1) + ni = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * ni * ne * self.slab_length + + gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = gaussian_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(recomb_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='RecombinationLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + def test_custom_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [RecombinationLine(line, lineshape=ZeemanTriplet)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + recomb_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.recombination_pec(line.element, line.charge, line.transition)(ne, te) + target_species = self.plasma.composition.get(line.element, line.charge + 1) + ni = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * ni * ne * self.slab_length + + zeeman_line = ZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = zeeman_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(recomb_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='RecombinationLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + +class TestThermalCXLine(unittest.TestCase): + + def setUp(self): + + self.world = World() + + self.atomic_data = MockAtomicData() + + plasma_species = [(carbon, 6, 1.67e18, 800., Vector3D(0, 0, 0)), + (deuterium, 0, 1.e19, 100., Vector3D(0, 0, 0))] + self.slab_length = 1.2 + self.plasma = build_constant_slab_plasma(length=self.slab_length, width=1, height=1, + electron_density=1e19, electron_temperature=1000., + plasma_species=plasma_species, b_field=Vector3D(0, 10., 0)) + self.plasma.atomic_data = self.atomic_data + self.plasma.parent = self.world + + def test_default_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [ThermalCXLine(line)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + thermalcx_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + donor_species = self.plasma.composition.get(deuterium, 0) + donor_density = donor_species.distribution.density(0.5, 0, 0) + donor_temperature = donor_species.distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.thermal_cx_pec(deuterium, 0, line.element, line.charge, line.transition)(ne, te, donor_temperature) + target_species = self.plasma.composition.get(line.element, line.charge + 1) + receiver_density = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * receiver_density * donor_density * self.slab_length + + gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = gaussian_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(thermalcx_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='ThermalCXLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + def test_custom_lineshape(self): + # setting up the model + line = Line(carbon, 5, (8, 7)) + self.plasma.models = [ThermalCXLine(line, lineshape=ZeemanTriplet)] + wavelength = self.atomic_data.wavelength(line.element, line.charge, line.transition) + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=wavelength - 1.5, max_wavelength=wavelength + 1.5, bins=512) + thermalcx_spectrum = ray.trace(self.world) + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0, 0) # constant slab + te = self.plasma.electron_distribution.effective_temperature(0.5, 0, 0) + donor_species = self.plasma.composition.get(deuterium, 0) + donor_density = donor_species.distribution.density(0.5, 0, 0) + donor_temperature = donor_species.distribution.effective_temperature(0.5, 0, 0) + rate = self.atomic_data.thermal_cx_pec(deuterium, 0, line.element, line.charge, line.transition)(ne, te, donor_temperature) + target_species = self.plasma.composition.get(line.element, line.charge + 1) + receiver_density = target_species.distribution.density(0.5, 0, 0) + radiance = 0.25 / np.pi * rate * receiver_density * donor_density * self.slab_length + + zeeman_line = ZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) + spectrum = Spectrum(ray.min_wavelength, ray.max_wavelength, ray.bins) + spectrum = zeeman_line.add_line(radiance, Point3D(0.5, 0, 0), direction, spectrum) + + for i in range(ray.bins): + self.assertAlmostEqual(thermalcx_spectrum.samples[i], spectrum.samples[i], delta=1e-8, + msg='ThermalCXLine model gives a wrong value at {} nm.'.format(spectrum.wavelengths[i])) + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/tests/test_lineshapes.py b/cherab/core/tests/test_lineshapes.py index da4dc41d..e2122d41 100644 --- a/cherab/core/tests/test_lineshapes.py +++ b/cherab/core/tests/test_lineshapes.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -20,17 +20,18 @@ import numpy as np from scipy.special import erf, hyp2f1 -from scipy.integrate import quadrature +from scipy.integrate import quad from raysect.core import Point3D, Vector3D from raysect.core.math.function.float import Arg1D, Constant1D from raysect.optical import Spectrum -from cherab.core import Line +from cherab.core import Beam, Line, AtomicData from cherab.core.math.integrators import GaussianQuadrature from cherab.core.atomic import deuterium, nitrogen, ZeemanStructure from cherab.tools.plasmas.slab import build_constant_slab_plasma from cherab.core.model import GaussianLine, MultipletLineShape, StarkBroadenedLine, ZeemanTriplet, ParametrisedZeemanTriplet, ZeemanMultiplet +from cherab.core.model import BeamEmissionMultiplet ATOMIC_MASS = 1.66053906660e-27 @@ -42,17 +43,28 @@ class TestLineShapes(unittest.TestCase): - plasma_species = [(deuterium, 0, 1.e18, 5., Vector3D(2.e4, 0, 0)), - (nitrogen, 1, 1.e17, 10., Vector3D(1.e4, 5.e4, 0))] - plasma = build_constant_slab_plasma(length=1, width=1, height=1, electron_density=1e19, electron_temperature=20., - plasma_species=plasma_species, b_field=Vector3D(0, 5., 0)) + def setUp(self): + + plasma_species = [(deuterium, 0, 1.e18, 5., Vector3D(2.e4, 0, 0)), + (nitrogen, 1, 1.e17, 10., Vector3D(1.e4, 5.e4, 0))] + self.plasma = build_constant_slab_plasma(length=1, width=1, height=1, + electron_density=1e19, + electron_temperature=20., + plasma_species=plasma_species, + b_field=Vector3D(0, 5., 0)) + self.atomic_data = AtomicData() + self.beam = Beam() + self.beam.plasma = self.plasma + self.beam.energy = 60000 + self.beam.temperature = 10 + self.beam.element = deuterium def test_gaussian_line(self): # setting up a line shape model line = Line(deuterium, 0, (3, 2)) # D-alpha line target_species = self.plasma.composition.get(line.element, line.charge) wavelength = 656.104 - gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma) + gaussian_line = GaussianLine(line, wavelength, target_species, self.plasma, self.atomic_data) # spectrum parameters min_wavelength = wavelength - 0.5 @@ -87,7 +99,7 @@ def test_multiplet_line_shape(self): target_species = self.plasma.composition.get(line.element, line.charge) multiplet = [[403.509, 404.132, 404.354, 404.479, 405.692], [0.205, 0.562, 0.175, 0.029, 0.029]] wavelength = 404.21 - multiplet_line = MultipletLineShape(line, wavelength, target_species, self.plasma, multiplet) + multiplet_line = MultipletLineShape(line, wavelength, target_species, self.plasma, self.atomic_data, multiplet) # spectrum parameters min_wavelength = min(multiplet[0]) - 0.5 @@ -123,7 +135,7 @@ def test_zeeman_triplet(self): line = Line(deuterium, 0, (3, 2)) # D-alpha line target_species = self.plasma.composition.get(line.element, line.charge) wavelength = 656.104 - triplet = ZeemanTriplet(line, wavelength, target_species, self.plasma) + triplet = ZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) # spectrum parameters min_wavelength = wavelength - 0.5 @@ -174,7 +186,7 @@ def test_parametrised_zeeman_triplet(self): line = Line(deuterium, 0, (3, 2)) # D-alpha line target_species = self.plasma.composition.get(line.element, line.charge) wavelength = 656.104 - triplet = ParametrisedZeemanTriplet(line, wavelength, target_species, self.plasma) + triplet = ParametrisedZeemanTriplet(line, wavelength, target_species, self.plasma, self.atomic_data) # spectrum parameters min_wavelength = wavelength - 0.5 @@ -192,7 +204,7 @@ def test_parametrised_zeeman_triplet(self): spectrum[pol] = triplet.add_line(radiance, point, direction, spectrum[pol]) # validating - alpha, beta, gamma = triplet.LINE_PARAMETERS_DEFAULT[line] + alpha, beta, gamma = self.atomic_data.zeeman_triplet_parameters(line) temperature = target_species.distribution.effective_temperature(point.x, point.y, point.z) velocity = target_species.distribution.bulk_velocity(point.x, point.y, point.z) sigma = np.sqrt(temperature * ELEMENTARY_CHARGE / (line.element.atomic_weight * ATOMIC_MASS)) * wavelength / SPEED_OF_LIGHT @@ -233,7 +245,7 @@ def test_zeeman_multiplet(self): sigma_minus_components = [(HC_EV_NM / (photon_energy + BOHR_MAGNETON * Arg1D()), Constant1D(0.5))] zeeman_structure = ZeemanStructure(pi_components, sigma_plus_components, sigma_minus_components) - multiplet = ZeemanMultiplet(line, wavelength, target_species, self.plasma, zeeman_structure) + multiplet = ZeemanMultiplet(line, wavelength, target_species, self.plasma, self.atomic_data, zeeman_structure) # spectrum parameters min_wavelength = wavelength - 0.5 @@ -284,8 +296,9 @@ def test_stark_broadened_line(self): line = Line(deuterium, 0, (6, 2)) # D-delta line target_species = self.plasma.composition.get(line.element, line.charge) wavelength = 656.104 - integrator = GaussianQuadrature(relative_tolerance=1.e-5) - stark_line = StarkBroadenedLine(line, wavelength, target_species, self.plasma, integrator=integrator) + relative_tolerance = 1.e-8 + integrator = GaussianQuadrature(relative_tolerance=relative_tolerance) + stark_line = StarkBroadenedLine(line, wavelength, target_species, self.plasma, self.atomic_data, integrator=integrator) # spectrum parameters min_wavelength = wavelength - 0.2 @@ -300,24 +313,163 @@ def test_stark_broadened_line(self): spectrum = stark_line.add_line(radiance, point, direction, spectrum) # validating - cij, aij, bij = stark_line.STARK_MODEL_COEFFICIENTS_DEFAULT[line] + velocity = target_species.distribution.bulk_velocity(point.x, point.y, point.z) + doppler_factor = (1 + velocity.dot(direction.normalise()) / SPEED_OF_LIGHT) + + b_field = self.plasma.b_field(point.x, point.y, point.z) + b_magn = b_field.length + photon_energy = HC_EV_NM / wavelength + wl_sigma_plus = HC_EV_NM / (photon_energy - BOHR_MAGNETON * b_magn) + wl_sigma_minus = HC_EV_NM / (photon_energy + BOHR_MAGNETON * b_magn) + cos_sqr = (b_field.dot(direction.normalise()) / b_magn)**2 + sin_sqr = 1. - cos_sqr + + # Gaussian parameters + temperature = target_species.distribution.effective_temperature(point.x, point.y, point.z) + sigma = np.sqrt(temperature * ELEMENTARY_CHARGE / (line.element.atomic_weight * ATOMIC_MASS)) * wavelength / SPEED_OF_LIGHT + fwhm_gauss = 2 * np.sqrt(2 * np.log(2)) * sigma + + # Lorentzian parameters + cij, aij, bij = self.atomic_data.stark_model_coefficients(line) ne = self.plasma.electron_distribution.density(point.x, point.y, point.z) te = self.plasma.electron_distribution.effective_temperature(point.x, point.y, point.z) - lambda_1_2 = cij * ne**aij / (te**bij) + fwhm_lorentz = cij * ne**aij / (te**bij) + + # Total FWHM + if fwhm_gauss <= fwhm_lorentz: + fwhm_poly_coeff = [1., 0, 0.57575, 0.37902, -0.42519, -0.31525, 0.31718] + fwhm_ratio = fwhm_gauss / fwhm_lorentz + fwhm_full = fwhm_lorentz * np.poly1d(fwhm_poly_coeff[::-1])(fwhm_ratio) + else: + fwhm_poly_coeff = [1., 0.15882, 1.04388, -1.38281, 0.46251, 0.82325, -0.58026] + fwhm_ratio = fwhm_lorentz / fwhm_gauss + fwhm_full = fwhm_gauss * np.poly1d(fwhm_poly_coeff[::-1])(fwhm_ratio) + wavelengths, delta = np.linspace(min_wavelength, max_wavelength, bins + 1, retstep=True) + + # Gaussian part + temp = 2 * np.sqrt(np.log(2)) / fwhm_full + erfs = erf((wavelengths - wavelength * doppler_factor) * temp) + gaussian = 0.25 * sin_sqr * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - wl_sigma_plus * doppler_factor) * temp) + gaussian += 0.5 * (0.25 * sin_sqr + 0.5 * cos_sqr) * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - wl_sigma_minus * doppler_factor) * temp) + gaussian += 0.5 * (0.25 * sin_sqr + 0.5 * cos_sqr) * (erfs[1:] - erfs[:-1]) / delta + + # Lorentzian part lorenzian_cutoff_gamma = 50 stark_norm_coeff = 4 * lorenzian_cutoff_gamma * hyp2f1(0.4, 1, 1.4, -(2 * lorenzian_cutoff_gamma)**2.5) - norm = (0.5 * lambda_1_2)**1.5 / stark_norm_coeff + norm = (0.5 * fwhm_full)**1.5 / stark_norm_coeff + + def stark_lineshape_pi(x): + return norm / ((np.abs(x - wavelength * doppler_factor))**2.5 + (0.5 * fwhm_full)**2.5) + + def stark_lineshape_sigma_plus(x): + return norm / ((np.abs(x - wl_sigma_plus * doppler_factor))**2.5 + (0.5 * fwhm_full)**2.5) + + def stark_lineshape_sigma_minus(x): + return norm / ((np.abs(x - wl_sigma_minus * doppler_factor))**2.5 + (0.5 * fwhm_full)**2.5) + + weight_poly_coeff = [5.14820e-04, 1.38821e+00, -9.60424e-02, -3.83995e-02, -7.40042e-03, -5.47626e-04] + lorentz_weight = np.exp(np.poly1d(weight_poly_coeff[::-1])(np.log(fwhm_lorentz / fwhm_full))) + + for i in range(bins): + lorentz_bin = 0.5 * sin_sqr * quad(stark_lineshape_pi, wavelengths[i], wavelengths[i + 1], + epsrel=relative_tolerance)[0] + lorentz_bin += (0.25 * sin_sqr + 0.5 * cos_sqr) * quad(stark_lineshape_sigma_plus, wavelengths[i], wavelengths[i + 1], + epsrel=relative_tolerance)[0] + lorentz_bin += (0.25 * sin_sqr + 0.5 * cos_sqr) * quad(stark_lineshape_sigma_minus, wavelengths[i], wavelengths[i + 1], + epsrel=relative_tolerance)[0] + ref_value = lorentz_bin / delta * lorentz_weight + gaussian[i] * (1. - lorentz_weight) + if ref_value: + self.assertAlmostEqual(spectrum.samples[i] / ref_value, 1., delta=relative_tolerance, + msg='StarkBroadenedLine.add_line() method gives a wrong value at {} nm.'.format(wavelengths[i])) + else: + self.assertAlmostEqual(ref_value, spectrum.samples[i], delta=relative_tolerance, + msg='StarkBroadenedLine.add_line() method gives a wrong value at {} nm.'.format(wavelengths[i])) + + def test_beam_emission_multiplet(self): + # Test MSE line shape + # setting up a line shape model + line = Line(deuterium, 0, (3, 2)) # D-alpha line + wavelength = 656.104 + sigma_to_pi = 0.56 + sigma1_to_sigma0 = 0.7060001671878492 + pi2_to_pi3 = 0.3140003593919741 + pi4_to_pi3 = 0.7279994935840365 + mse_line = BeamEmissionMultiplet(line, wavelength, self.beam, self.atomic_data, + sigma_to_pi, sigma1_to_sigma0, pi2_to_pi3, pi4_to_pi3) + + # spectrum parameters + min_wavelength = wavelength - 3 + max_wavelength = wavelength + 3 + bins = 512 + point = Point3D(0.5, 0.5, 0.5) + direction = Vector3D(-1, 1, 0) / np.sqrt(2) + beam_direction = self.beam.direction(point.x, point.y, point.z) + + # obtaining spectrum + radiance = 1.0 + spectrum = Spectrum(min_wavelength, max_wavelength, bins) + spectrum = mse_line.add_line(radiance, point, point, beam_direction, direction, spectrum) + + # validating + + # calculate Stark splitting + b_field = self.plasma.b_field(point.x, point.y, point.z) + beam_velocity = beam_direction.normalise() * np.sqrt(2 * self.beam.energy * ELEMENTARY_CHARGE / ATOMIC_MASS) + e_field = beam_velocity.cross(b_field).length + STARK_SPLITTING_FACTOR = 2.77e-8 + stark_split = np.abs(STARK_SPLITTING_FACTOR * e_field) + + # calculate emission line central wavelength, doppler shifted along observation direction + central_wavelength = wavelength * (1 + beam_velocity.dot(direction.normalise()) / SPEED_OF_LIGHT) + + # calculate doppler broadening + beam_ion_mass = self.beam.element.atomic_weight + beam_temperature = self.beam.temperature + sigma = np.sqrt(beam_temperature * ELEMENTARY_CHARGE / (beam_ion_mass * ATOMIC_MASS)) * wavelength / SPEED_OF_LIGHT + temp = 1. / (np.sqrt(2.) * sigma) + + # calculate relative intensities of sigma and pi lines + d = 1 / (1 + sigma_to_pi) + intensity_sig = sigma_to_pi * d * radiance + intensity_pi = 0.5 * d * radiance wavelengths, delta = np.linspace(min_wavelength, max_wavelength, bins + 1, retstep=True) - def stark_lineshape(x): - return norm / ((np.abs(x - wavelength))**2.5 + (0.5 * lambda_1_2)**2.5) + # add Sigma lines to output + intensity_s0 = 1 / (sigma1_to_sigma0 + 1) + intensity_s1 = 0.5 * sigma1_to_sigma0 * intensity_s0 + + erfs = erf((wavelengths - central_wavelength) * temp) + test_spectrum = 0.5 * intensity_sig * intensity_s0 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength - stark_split) * temp) + test_spectrum += 0.5 * intensity_sig * intensity_s1 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength + stark_split) * temp) + test_spectrum += 0.5 * intensity_sig * intensity_s1 * (erfs[1:] - erfs[:-1]) / delta + + # add Pi lines to output + intensity_pi3 = 1 / (1 + pi2_to_pi3 + pi4_to_pi3) + intensity_pi2 = pi2_to_pi3 * intensity_pi3 + intensity_pi4 = pi4_to_pi3 * intensity_pi3 + + erfs = erf((wavelengths - central_wavelength - 2 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi2 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength + 2 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi2 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength - 3 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi3 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength + 3 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi3 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength - 4 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi4 * (erfs[1:] - erfs[:-1]) / delta + erfs = erf((wavelengths - central_wavelength + 4 * stark_split) * temp) + test_spectrum += 0.5 * intensity_pi * intensity_pi4 * (erfs[1:] - erfs[:-1]) / delta for i in range(bins): - stark_bin = quadrature(stark_lineshape, wavelengths[i], wavelengths[i + 1], rtol=integrator.relative_tolerance)[0] / delta - self.assertAlmostEqual(stark_bin, spectrum.samples[i], delta=1e-9, - msg='StarkBroadenedLine.add_line() method gives a wrong value at {} nm.'.format(wavelengths[i])) + self.assertAlmostEqual(test_spectrum[i], spectrum.samples[i], delta=1e-10, + msg='BeamEmissionMultiplet.add_line() method gives a wrong value at {} nm.'.format(wavelengths[i])) if __name__ == '__main__': diff --git a/cherab/core/tests/test_total_radiated_power.py b/cherab/core/tests/test_total_radiated_power.py new file mode 100644 index 00000000..aa6e4e68 --- /dev/null +++ b/cherab/core/tests/test_total_radiated_power.py @@ -0,0 +1,142 @@ +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import unittest + +import numpy as np + +from raysect.core import Point3D, Vector3D +from raysect.optical import Ray, World + +from cherab.core.atomic import AtomicData, LineRadiationPower, ContinuumPower, CXRadiationPower +from cherab.core.atomic import deuterium, hydrogen, nitrogen +from cherab.tools.plasmas.slab import build_constant_slab_plasma +from cherab.core.model import TotalRadiatedPower + +from cherab.core.utility import EvAmuToMS, EvToJ + + +class ConstantLineRadiationPower(LineRadiationPower): + """ + Constant line radiation power coefficient. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, density, temperature): + + return self.value + + +class ConstantContinuumPower(ContinuumPower): + """ + Constant continuum power coefficient. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, density, temperature): + + return self.value + + +class ConstantCXRadiationPower(CXRadiationPower): + """ + Constant charge exchange radiation power coefficient. + """ + + def __init__(self, value): + self.value = value + + def evaluate(self, density, temperature): + + return self.value + + +class MockAtomicData(AtomicData): + """Fake atomic data for test purpose.""" + + def line_radiated_power_rate(self, element, charge): + + return ConstantLineRadiationPower(1.e-32) + + def continuum_radiated_power_rate(self, element, charge): + + return ConstantContinuumPower(1.e-33) + + def cx_radiated_power_rate(self, element, charge): + + return ConstantCXRadiationPower(1.e-31) + + +class TestTotalRadiatedPower(unittest.TestCase): + + def setUp(self): + + self.world = World() + + self.atomic_data = MockAtomicData() + + plasma_species = [(deuterium, 0, 1.e18, 500., Vector3D(0, 0, 0)), + (hydrogen, 0, 1.e18, 500., Vector3D(0, 0, 0)), + (nitrogen, 6, 5.e18, 1100., Vector3D(0, 0, 0)), + (nitrogen, 7, 1.e19, 1100., Vector3D(0, 0, 0))] + self.plasma = build_constant_slab_plasma(length=1.2, width=1.2, height=1.2, + electron_density=1e19, electron_temperature=1000., + plasma_species=plasma_species) + self.plasma.parent = self.world + self.plasma.atomic_data = self.atomic_data + + def test_total_radiated_power(self): + + self.plasma.models = [TotalRadiatedPower(nitrogen, 6)] + + # observing + origin = Point3D(1.5, 0, 0) + direction = Vector3D(-1, 0, 0) + ray = Ray(origin=origin, direction=direction, + min_wavelength=500., max_wavelength=550., bins=2) + radiated_power = ray.trace(self.world).total() + + # validating + ne = self.plasma.electron_distribution.density(0.5, 0.5, 0.5) + n_n6 = self.plasma.composition[(nitrogen, 6)].distribution.density(0.5, 0.5, 0.5) + n_n7 = self.plasma.composition[(nitrogen, 7)].distribution.density(0.5, 0.5, 0.5) + n_h0 = self.plasma.composition[(hydrogen, 0)].distribution.density(0.5, 0.5, 0.5) + n_d0 = self.plasma.composition[(deuterium, 0)].distribution.density(0.5, 0.5, 0.5) + + integration_length = 1.2 + + plt_rate = self.atomic_data.line_radiated_power_rate(nitrogen, 6).value + plt_radiance = 0.25 / np.pi * plt_rate * ne * n_n6 * integration_length + + prb_rate = self.atomic_data.continuum_radiated_power_rate(nitrogen, 7).value + prb_radiance = 0.25 / np.pi * prb_rate * ne * n_n7 * integration_length + + prc_rate = self.atomic_data.cx_radiated_power_rate(nitrogen, 7).value + prc_radiance = 0.25 / np.pi * prc_rate * (n_h0 + n_d0) * n_n7 * integration_length + + test_radiated_power = plt_radiance + prb_radiance + prc_radiance + + self.assertAlmostEqual(radiated_power / test_radiated_power, 1., delta=1e-8) + + +if __name__ == '__main__': + unittest.main() diff --git a/cherab/core/utility/constants.pxd b/cherab/core/utility/constants.pxd index 9cce304d..5ec0ca4a 100644 --- a/cherab/core/utility/constants.pxd +++ b/cherab/core/utility/constants.pxd @@ -27,6 +27,9 @@ cdef: double ELEMENTARY_CHARGE double SPEED_OF_LIGHT double PLANCK_CONSTANT + double HC_EV_NM double ELECTRON_CLASSICAL_RADIUS double ELECTRON_REST_MASS double RYDBERG_CONSTANT_EV + double VACUUM_PERMITTIVITY + double BOHR_MAGNETON diff --git a/cherab/core/utility/constants.pyx b/cherab/core/utility/constants.pyx index 03e70617..423d15e8 100644 --- a/cherab/core/utility/constants.pyx +++ b/cherab/core/utility/constants.pyx @@ -29,6 +29,9 @@ cdef: double ELEMENTARY_CHARGE = 1.602176634e-19 double SPEED_OF_LIGHT = 299792458.0 double PLANCK_CONSTANT = 6.62607015e-34 + double HC_EV_NM = 1239.8419738620933 # (Planck constant in eV s) x (speed of light in nm/s) double ELECTRON_CLASSICAL_RADIUS = 2.8179403262e-15 double ELECTRON_REST_MASS = 9.1093837015e-31 double RYDBERG_CONSTANT_EV = 13.605693122994 + double VACUUM_PERMITTIVITY = 8.8541878128e-12 + double BOHR_MAGNETON = 5.78838180123e-5 # in eV/T diff --git a/cherab/generomak/plasma/plasma.py b/cherab/generomak/plasma/plasma.py index 45c5ed3e..e8799aca 100644 --- a/cherab/generomak/plasma/plasma.py +++ b/cherab/generomak/plasma/plasma.py @@ -305,7 +305,7 @@ def get_double_parabola(v_min, v_max, convexity, concavity, xmin=0, xmax=1): def get_exponential_growth(initial_value, growth_rate, initial_position=1): - """ + r""" returns exponentially growing Function1D The returned Function1D is of the form: diff --git a/cherab/openadas/install.py b/cherab/openadas/install.py index 27e0c5f4..53e68899 100644 --- a/cherab/openadas/install.py +++ b/cherab/openadas/install.py @@ -1,7 +1,7 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -21,9 +21,11 @@ import os import urllib.parse import urllib.request +import numpy as np from cherab.openadas import repository from cherab.openadas.parse import * +from cherab.core.atomic import hydrogen from cherab.core.utility import RecursiveDict, PerCm3ToPerM3, Cm3ToM3 @@ -264,19 +266,29 @@ def install_adf15(element, ionisation, file_path, download=False, repository_pat # decode file and write out rates rates, wavelengths = parse_adf15(element, ionisation, path, header_format=header_format) + + if 'thermalcx' in rates: + cx_rates = rates.pop('thermalcx') + # CX rates for Tdon = Trec (2D function of Ne, Te) + # converting to 3D function of Ne, Te, Tdon + repository.update_pec_thermal_cx_rates(_thermalcx_adf15_2dto3d_converter(cx_rates)) + repository.update_pec_rates(rates, repository_path) repository.update_wavelengths(wavelengths, repository_path) def install_adf21(beam_species, target_ion, target_charge, file_path, download=False, repository_path=None, adas_path=None): - # """ - # Adds the rate defined in an ADF21 file to the repository. - # - # :param file_path: Path relative to ADAS root. - # :param download: Attempt to download file if not present (Default=True). - # :param repository_path: Path to the repository in which to install the rates (optional). - # :param adas_path: Path to ADAS files repository (optional). - # """ + """ + Adds the beam stopping rate defined in an ADF21 file to the repository. + + :param beam_species: Beam neutral atom (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param file_path: Path relative to ADAS root. + :param download: Attempt to download file if not present (Default=True). + :param repository_path: Path to the repository in which to install the rates (optional). + :param adas_path: Path to ADAS files repository (optional). + """ print('Installing {}...'.format(file_path)) path = _locate_adas_file(file_path, download, adas_path, repository_path) @@ -289,15 +301,18 @@ def install_adf21(beam_species, target_ion, target_charge, file_path, download=F def install_adf22bmp(beam_species, beam_metastable, target_ion, target_charge, file_path, download=False, repository_path=None, adas_path=None): - pass - # """ - # Adds the rate defined in an ADF21 file to the repository. - # - # :param file_path: Path relative to ADAS root. - # :param download: Attempt to download file if not present (Default=True). - # :param repository_path: Path to the repository in which to install the rates (optional). - # :param adas_path: Path to ADAS files repository (optional). - # """ + """ + Adds the beam population rate defined in an ADF22 BMP file to the repository. + + :param beam_species: Beam neutral atom (Element/Isotope). + :param beam_metastable: Metastable/excitation level of beam neutral atom. + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param file_path: Path relative to ADAS root. + :param download: Attempt to download file if not present (Default=True). + :param repository_path: Path to the repository in which to install the rates (optional). + :param adas_path: Path to ADAS files repository (optional). + """ print('Installing {}...'.format(file_path)) path = _locate_adas_file(file_path, download, adas_path, repository_path) @@ -310,15 +325,18 @@ def install_adf22bmp(beam_species, beam_metastable, target_ion, target_charge, f def install_adf22bme(beam_species, target_ion, target_charge, transition, file_path, download=False, repository_path=None, adas_path=None): - pass - # """ - # Adds the rate defined in an ADF21 file to the repository. - # - # :param file_path: Path relative to ADAS root. - # :param download: Attempt to download file if not present (Default=True). - # :param repository_path: Path to the repository in which to install the rates (optional). - # :param adas_path: Path to ADAS files repository (optional). - # """ + """ + Adds the beam emission rate defined in an ADF22 BME file to the repository. + + :param beam_species: Beam neutral atom (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param transition: Tuple containing (initial level, final level). + :param file_path: Path relative to ADAS root. + :param download: Attempt to download file if not present (Default=True). + :param repository_path: Path to the repository in which to install the rates (optional). + :param adas_path: Path to ADAS files repository (optional). + """ print('Installing {}...'.format(file_path)) path = _locate_adas_file(file_path, download, adas_path, repository_path) @@ -393,3 +411,23 @@ def _notation_adf11_adas2cherab(rate_adas, filetype): return rate_cherab +def _thermalcx_adf15_2dto3d_converter(rates): + """ + Converts thermal CX PEC rates parsed from a standard ADF 15 file + to the format supported by the repository. + + In the standard ADF 15 file, it is assumed that the donor is H0 and Tdon = Trec. + """ + new_rates = RecursiveDict() + for element, charge_states in rates.items(): + for charge, transitions in charge_states.items(): + for transition, rate in transitions.items(): + data = np.empty((len(rate['ne']), len(rate['te']), 2)) + data[:, :, :] = rate['rate'][:, :, None] + new_rate = {'ne': rate['ne'], + 'te': rate['te'], + 'td': np.array([0.01, 10000]), + 'rate': data} + new_rates[hydrogen][0][element][charge + 1][transition] = new_rate + + return new_rates diff --git a/cherab/openadas/openadas.py b/cherab/openadas/openadas.py index 7e850bc4..5d42935a 100644 --- a/cherab/openadas/openadas.py +++ b/cherab/openadas/openadas.py @@ -386,6 +386,48 @@ def recombination_pec(self, ion, charge, transition): return RecombinationPEC(wavelength, data, extrapolate=self._permit_extrapolation) + def thermal_cx_pec(self, donor_element, donor_charge, receiver_element, receiver_charge, transition): + """ + Thermal CX photon emission coefficient for a given species. + + Open-ADAS data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used when permit_extrapolation is True. + + :param donor_element: Element object defining the donor ion type. + :param donor_charge: Charge state of the donor ion. + :param receiver_element: Element object defining the receiver ion type. + :param receiver_charge: Charge state of the receiver ion. + :param transition: Tuple containing (initial level, final level) of the receiver + in charge state receiver_charge - 1. + :return: Thermal charge exchange photon emission coefficient in W.m^3 + as a function of electron density, electron temperature and donor temperature. + """ + + # extract elements from isotopes because there are no isotope rates in ADAS + if isinstance(donor_element, Isotope): + donor_element = donor_element.element + + if isinstance(receiver_element, Isotope): + receiver_element = receiver_element.element + + try: + # read thermal CX rate from json file in the repository + data = repository.get_pec_thermal_cx_rate(donor_element, donor_charge, + receiver_element, receiver_charge, + transition, + repository_path=self._data_path) + + except RuntimeError: + if self._missing_rates_return_null: + return NullThermalCXPEC() + raise + + # obtain isotope's rest wavelength for a given transition + # the wavelength is used ot convert the PEC from photons/s/m3 to W/m3 + wavelength = self.wavelength(receiver_element, receiver_charge - 1, transition) + + return ThermalCXPEC(wavelength, data, extrapolate=self._permit_extrapolation) + def line_radiated_power_rate(self, ion, charge): """ Line radiated power coefficient for a given species. diff --git a/cherab/openadas/parse/adf11.py b/cherab/openadas/parse/adf11.py index f97ab132..a91a1a85 100644 --- a/cherab/openadas/parse/adf11.py +++ b/cherab/openadas/parse/adf11.py @@ -38,7 +38,7 @@ def parse_adf11(element, adf_file_path): with open(adf_file_path, "r") as source_file: lines = source_file.readlines() # read file contents by lines - tmp = re.split("\s{2,}", lines[0].strip()) # split into relevant variables + tmp = re.split(r"\s{2,}", lines[0].strip()) # split into relevant variables # exctract variables z_nuclear = int(tmp[0]) n_densities = int(tmp[1]) @@ -53,15 +53,15 @@ def parse_adf11(element, adf_file_path): "specified ADF11 file, '{}'.".format(element.name, element_name)) # check if it is a resolved file - if re.match("\s*[0-9]+", lines[3]): # is it unresolved? + if re.match(r"\s*[0-9]+", lines[3]): # is it unresolved? startsearch = 2 else: startsearch = 4 # skip vectors with info about resolved states # get temperature and density vectors for i in range(startsearch, len(lines)): - if re.match("^\s*C{0}-{2,}", lines[i]): - tmp = re.sub("\n*\s+", "\t", + if re.match(r"^\s*C{0}-{2,}", lines[i]): + tmp = re.sub(r"\n*\s+", "\t", "".join(lines[startsearch:i]).strip()) # replace unwanted chars tmp = np.fromstring(tmp, sep="\t", dtype=float) # put into nunpy array densities = tmp[:n_densities] # read density values @@ -77,13 +77,13 @@ def parse_adf11(element, adf_file_path): blockrates_stop = None for i in range(startsearch, len(lines)): - if re.match("^\s*C*-{2,}", lines[i]): # is it a rates block header? + if re.match(r"^\s*C*-{2,}", lines[i]): # is it a rates block header? # is it a first data block found? if not blockrates_start is None: blockrates_stop = i # end of the requested block - rates_table = re.sub("\n*\s+", "\t", + rates_table = re.sub(r"\n*\s+", "\t", "".join(lines[ blockrates_start:blockrates_stop]).strip()) # replace unwanted chars rates_table = np.fromstring(rates_table, sep="\t", @@ -95,18 +95,18 @@ def parse_adf11(element, adf_file_path): rates[element][ion_charge]['rates'] = np.swapaxes(rates_table, 0, 1) # if end of data block beak the loop or reassign start of data block for next stage - if re.match("^\s*C{1}-{2,}", lines[i]) or re.match("^\s*C{0,1}-{2,}", lines[i]) and \ - re.match("^\s*C\n", lines[i + 1]): + if re.match(r"^\s*C{1}-{2,}", lines[i]) or re.match(r"^\s*C{0,1}-{2,}", lines[i]) and \ + re.match(r"^\s*C\n", lines[i + 1]): break - z1_pos = re.search("Z1\s*=*\s*[0-9]+\s*", lines[i]).group() # get Z1 part - ion_charge = int(re.sub("Z1[\s*=]", "", z1_pos)) # remove Z1 to avoid getting 1 later - if not re.search("IGRD\s*=*\s*[0-9]+\s*", lines[i]) is None: # get the IGRD part - igrd_pos = re.search("IGRD\s*=*\s*[0-9]+\s*", lines[i]).group() # get the IGRD part + z1_pos = re.search(r"Z1\s*=*\s*[0-9]+\s*", lines[i]).group() # get Z1 part + ion_charge = int(re.sub(r"Z1[\s*=]", "", z1_pos)) # remove Z1 to avoid getting 1 later + if not re.search(r"IGRD\s*=*\s*[0-9]+\s*", lines[i]) is None: # get the IGRD part + igrd_pos = re.search(r"IGRD\s*=*\s*[0-9]+\s*", lines[i]).group() # get the IGRD part else: igrd_pos = "No spec" - if not re.search("IPRT\s*=*\s*[0-9]+\s*", lines[i]) is None: - iptr_pos = re.search("IPRT\s*=*\s*[0-9]+\s*", lines[i]).group() # get the IPRT part + if not re.search(r"IPRT\s*=*\s*[0-9]+\s*", lines[i]) is None: + iptr_pos = re.search(r"IPRT\s*=*\s*[0-9]+\s*", lines[i]).group() # get the IPRT part else: iptr_pos = "No spec" blockrates_start = i + 1 # if block start not known, check if we are at the right position diff --git a/cherab/openadas/parse/adf15.py b/cherab/openadas/parse/adf15.py index b9fe42be..12aa01a9 100644 --- a/cherab/openadas/parse/adf15.py +++ b/cherab/openadas/parse/adf15.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2023 Euratom +# Copyright 2016-2023 United Kingdom Atomic Energy Authority +# Copyright 2016-2023 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -60,18 +60,26 @@ def parse_adf15(element, charge, adf_file_path, header_format=None): # for check header line header = file.readline() - if not re.match('^\s*(\d*) {4}/(.*)/?\s*$', header): + if not re.match(r'^\s*(\d*) {4}/(.*)/?\s*$', header): raise ValueError('The specified path does not point to a valid ADF15 file.') # scrape transition information and wavelength # use simple electron configuration structure for hydrogen-like ions if header_format == 'hydrogen' or element == hydrogen: config = _scrape_metadata_hydrogen(file, element, charge) - elif header_format == 'hydrogen-like' or element.atomic_number - charge == 1: + elif header_format == 'hydrogen-like': config = _scrape_metadata_hydrogen_like(file, element, charge) + elif element.atomic_number - charge == 1: + config = _scrape_metadata_hydrogen_like(file, element, charge) + if not config and 'bnd#' in adf_file_path: + # ADF15 files with the "bnd" suffix may have metadata in the "hydrogen" format + config = _scrape_metadata_hydrogen(file, element, charge) else: config = _scrape_metadata_full(file, element, charge) + if not config: + raise RuntimeError("Unable to parse ADF15 metadata.") + # process rate data rates = RecursiveDict() for cls in ('excitation', 'recombination', 'thermalcx'): @@ -96,14 +104,14 @@ def _scrape_metadata_hydrogen(file, element, charge): file.seek(0) lines = file.readlines() - pec_index_header_match = '^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' + pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): lines.pop(0) index_lines = lines for i in range(len(index_lines)): - pec_hydrogen_transition_match = '^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)' + pec_hydrogen_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*N=\s*([0-9]*) - N=\s*([0-9]*)\s*([A-Z]*)' match = re.match(pec_hydrogen_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -118,7 +126,7 @@ def _scrape_metadata_hydrogen(file, element, charge): elif rate_type_adas == 'RECOM': rate_type = 'recombination' elif rate_type_adas == 'CHEXC': - rate_type = 'cx_thermal' + rate_type = 'thermalcx' else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -139,14 +147,14 @@ def _scrape_metadata_hydrogen_like(file, element, charge): file.seek(0) lines = file.readlines() - pec_index_header_match = '^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' + pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): lines.pop(0) index_lines = lines for i in range(len(index_lines)): - pec_full_transition_match = '^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' + pec_full_transition_match = r'^C\s*([0-9]*)\.\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -161,7 +169,7 @@ def _scrape_metadata_hydrogen_like(file, element, charge): elif rate_type_adas == 'RECOM': rate_type = 'recombination' elif rate_type_adas == 'CHEXC': - rate_type = 'cx_thermal' + rate_type = 'thermalcx' else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -185,10 +193,10 @@ def _scrape_metadata_full(file, element, charge): configuration_lines = [] configuration_dict = {} - configuration_header_match = '^C\s*Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy \(cm\*\*-1\)$' + configuration_header_match = r'^C\s*Configuration\s*\(2S\+1\)L\(w-1/2\)\s*Energy \(cm\*\*-1\)$' while not re.match(configuration_header_match, lines[0], re.IGNORECASE): lines.pop(0) - pec_index_header_match = '^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' + pec_index_header_match = r'^C\s*ISEL\s*WAVELENGTH\s*TRANSITION\s*TYPE' while not re.match(pec_index_header_match, lines[0], re.IGNORECASE): configuration_lines.append(lines[0]) lines.pop(0) @@ -196,7 +204,7 @@ def _scrape_metadata_full(file, element, charge): for i in range(len(configuration_lines)): - configuration_string_match = "^C\s*([0-9]*)\s*((?:[0-9][SPDFG][0-9]\s)*)\s*\(([0-9]*\.?[0-9]*)\)([0-9]*)\(\s*([0-9]*\.?[0-9]*)\)" + configuration_string_match = r"^C\s*([0-9]*)\s*((?:[0-9][SPDFG][0-9]\s)*)\s*\(([0-9]*\.?[0-9]*)\)([0-9]*)\(\s*([0-9]*\.?[0-9]*)\)" match = re.match(configuration_string_match, configuration_lines[i], re.IGNORECASE) if not match: continue @@ -212,7 +220,7 @@ def _scrape_metadata_full(file, element, charge): for i in range(len(index_lines)): - pec_full_transition_match = '^C\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' + pec_full_transition_match = r'^C\s*([0-9]*)\.?\s*([0-9]*\.[0-9]*)\s*([0-9]*)[\(\)\.0-9\s]*-\s*([0-9]*)[\(\)\.0-9\s]*([A-Z]*)' match = re.match(pec_full_transition_match, index_lines[i], re.IGNORECASE) if not match: continue @@ -229,7 +237,7 @@ def _scrape_metadata_full(file, element, charge): elif rate_type_adas == 'RECOM': rate_type = 'recombination' elif rate_type_adas == 'CHEXC': - rate_type = 'cx_thermal' + rate_type = 'thermalcx' else: raise ValueError("Unrecognised rate type - {}".format(rate_type_adas)) @@ -247,8 +255,8 @@ def _extract_rate(file, block_num): # search from start of file file.seek(0) - wavelength_match = "^\s*[0-9]*\.[0-9]* ?a? +.*$" - block_id_match = "^\s*[0-9]*\.[0-9]* ?a?\s*([0-9]*)\s*([0-9]*).*/type *= *([a-zA-Z]*).*/isel *= * ([0-9]*)$" + wavelength_match = r"^\s*[0-9]*\.[0-9]* ?a? +.*$" + block_id_match = r"^\s*[0-9]*\.[0-9]* ?a?\s*([0-9]*)\s*([0-9]*).*/type *= *([a-zA-Z]*).*/isel *= * ([0-9]*)$" for block in _group_by_block(file, wavelength_match): match = re.match(block_id_match, block[0], re.IGNORECASE) diff --git a/cherab/openadas/rates/atomic.pyx b/cherab/openadas/rates/atomic.pyx index 04500124..eecb9bf5 100644 --- a/cherab/openadas/rates/atomic.pyx +++ b/cherab/openadas/rates/atomic.pyx @@ -1,7 +1,7 @@ -# Copyright 2016-2021 Euratom -# Copyright 2016-2021 United Kingdom Atomic Energy Authority -# Copyright 2016-2021 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -24,12 +24,26 @@ from raysect.core.math.function.float cimport Interpolator2DArray cdef class IonisationRate(CoreIonisationRate): + """ + Ionisation rate. + + Data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param dict data: Ionisation rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with ionisation rate in m^3.s^-1. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, dict data, extrapolate=False): - """ - :param data: Dictionary containing rate data. - :param extrapolate: Enable extrapolation (default=False). - """ self.raw_data = data @@ -50,11 +64,8 @@ cdef class IonisationRate(CoreIonisationRate): cpdef double evaluate(self, double density, double temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(density), log10(temperature)) @@ -62,7 +73,7 @@ cdef class IonisationRate(CoreIonisationRate): cdef class NullIonisationRate(CoreIonisationRate): """ - A PEC rate that always returns zero. + An ionisation rate that always returns zero. Needed for use cases where the required atomic data is missing. """ @@ -71,12 +82,26 @@ cdef class NullIonisationRate(CoreIonisationRate): cdef class RecombinationRate(CoreRecombinationRate): + """ + Recombination rate. + + Data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param dict data: Recombination rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with recombination rate in m^3.s^-1. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, dict data, extrapolate=False): - """ - :param data: Dictionary containing rate data. - :param extrapolate: Enable extrapolation (default=False). - """ self.raw_data = data @@ -97,11 +122,8 @@ cdef class RecombinationRate(CoreRecombinationRate): cpdef double evaluate(self, double density, double temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(density), log10(temperature)) @@ -109,7 +131,7 @@ cdef class RecombinationRate(CoreRecombinationRate): cdef class NullRecombinationRate(CoreRecombinationRate): """ - A PEC rate that always returns zero. + A recombination rate that always returns zero. Needed for use cases where the required atomic data is missing. """ @@ -118,12 +140,26 @@ cdef class NullRecombinationRate(CoreRecombinationRate): cdef class ThermalCXRate(CoreThermalCXRate): + """ + Thermal charge exchange rate. + + Data is interpolated with cubic spline in log-log space. + Linear extrapolation is used if extrapolate is True. + + :param dict data: CX rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with thermal CX rate in m^3.s^-1. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, dict data, extrapolate=False): - """ - :param data: Dictionary containing rate data. - :param extrapolate: Enable extrapolation (default=False). - """ self.raw_data = data @@ -143,11 +179,8 @@ cdef class ThermalCXRate(CoreThermalCXRate): cpdef double evaluate(self, double density, double temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(density), log10(temperature)) @@ -155,7 +188,7 @@ cdef class ThermalCXRate(CoreThermalCXRate): cdef class NullThermalCXRate(CoreThermalCXRate): """ - A PEC rate that always returns zero. + A thermal CX rate that always returns zero. Needed for use cases where the required atomic data is missing. """ diff --git a/cherab/openadas/rates/beam.pyx b/cherab/openadas/rates/beam.pyx index f40af8dd..30d43664 100644 --- a/cherab/openadas/rates/beam.pyx +++ b/cherab/openadas/rates/beam.pyx @@ -1,6 +1,6 @@ -# Copyright 2016-2021 Euratom -# Copyright 2016-2021 United Kingdom Atomic Energy Authority -# Copyright 2016-2021 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -31,8 +31,26 @@ cdef class BeamStoppingRate(CoreBeamStoppingRate): """ The beam stopping coefficient interpolation class. - :param data: A dictionary holding the beam coefficient data. - :param extrapolate: Set to True to enable extrapolation, False to disable (default). + Data is interpolated with cubic spline in log-log space. + Linear and quadratic extrapolations are used for "sen" and "st" respectively + if extrapolate is True. + + :param dict data: A beam stopping rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n': 1D array of size (M) with target electron density in m^-3, + | 't': 1D array of size (K) with target electron temperature in eV, + | 'sen': 2D array of size (N, M) with beam stopping rate energy component in m^3.s^-1. + | 'st': 1D array of size (K) with beam stopping rate temperature component in m^3.s^-1. + | 'sref': reference beam stopping rate in m^3.s^-1. + | The total beam stopping rate: s = sen * st / sref. + + :param bint extrapolate: Set to True to enable extrapolation, False to disable (default). + + :ivar tuple beam_energy_range: Interaction energy interpolation range. + :ivar tuple density_range: Target electron density interpolation range. + :ivar tuple temperature_range: Target electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. """ @cython.cdivision(True) @@ -78,14 +96,8 @@ cdef class BeamStoppingRate(CoreBeamStoppingRate): """ # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if energy < 1.e-300: - energy = 1.e-300 - - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if energy <= 0 or density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** (self._npl_eb.evaluate(log10(energy), log10(density)) + self._tp.evaluate(log10(temperature))) @@ -93,7 +105,7 @@ cdef class BeamStoppingRate(CoreBeamStoppingRate): cdef class NullBeamStoppingRate(CoreBeamStoppingRate): """ - A beam rate that always returns zero. + A beam stopping rate that always returns zero. Needed for use cases where the required atomic data is missing. """ @@ -105,8 +117,26 @@ cdef class BeamPopulationRate(CoreBeamPopulationRate): """ The beam population coefficient interpolation class. - :param data: A dictionary holding the beam coefficient data. - :param extrapolate: Set to True to enable extrapolation, False to disable (default). + Data is interpolated with cubic spline in log-log space. + Linear and quadratic extrapolations are used for "sen" and "st" respectively + if extrapolate is True. + + :param dict data: Beam population rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n': 1D array of size (M) with target electron density in m^-3, + | 't': 1D array of size (K) with target electron temperature in eV, + | 'sen': 2D array of size (N, M) with dimensionless beam population rate energy component. + | 'st': 1D array of size (K) with dimensionless beam population rate temperature component. + | 'sref': reference dimensionless beam population rate. + | The total beam population rate: s = sen * st / sref. + + :param bint extrapolate: Set to True to enable extrapolation, False to disable (default). + + :ivar tuple beam_energy_range: Interaction energy interpolation range. + :ivar tuple density_range: Target electron density interpolation range. + :ivar tuple temperature_range: Target electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. """ @cython.cdivision(True) @@ -152,14 +182,8 @@ cdef class BeamPopulationRate(CoreBeamPopulationRate): """ # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if energy < 1.e-300: - energy = 1.e-300 - - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if energy <= 0 or density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** (self._npl_eb.evaluate(log10(energy), log10(density)) + self._tp.evaluate(log10(temperature))) @@ -167,7 +191,7 @@ cdef class BeamPopulationRate(CoreBeamPopulationRate): cdef class NullBeamPopulationRate(CoreBeamPopulationRate): """ - A beam rate that always returns zero. + A beam population rate that always returns zero. Needed for use cases where the required atomic data is missing. """ @@ -179,9 +203,26 @@ cdef class BeamEmissionPEC(CoreBeamEmissionPEC): """ The beam emission coefficient interpolation class. - :param data: A dictionary holding the beam coefficient data. - :param wavelength: The natural wavelength of the emission line associated with the rate data in nm. - :param extrapolate: Set to True to enable extrapolation, False to disable (default). + Data is interpolated with cubic spline in log-log space. + Linear and quadratic extrapolations are used for "sen" and "st" respectively + if extrapolate is True. + + :param dict data: Beam emission rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n' 1D array of size (M) with target electron density in m^-3, + | 't' 1D array of size (K) with target electron temperature in eV, + | 'sen' 2D array of size (N, M) with beam emission rate energy component in photon.m^3.s^-1. + | 'st' 1D array of size (K) with beam emission rate temperature component in photon.m^3.s^-1. + | 'sref': reference beam emission rate in photon.m^3.s^-1. + + :param double wavelength: The natural wavelength of the emission line associated with the rate data in nm. + :param bint extrapolate: Set to True to enable extrapolation, False to disable (default). + + :ivar tuple beam_energy_range: Interaction energy interpolation range. + :ivar tuple density_range: Target electron density interpolation range. + :ivar tuple temperature_range: Target electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. """ @cython.cdivision(True) @@ -194,7 +235,7 @@ cdef class BeamEmissionPEC(CoreBeamEmissionPEC): e = data["e"] # eV/amu n = data["n"] # m^-3 t = data["t"] # eV - sen = np.log10(PhotonToJ.to(data["sen"], wavelength)) # W.m^3/s + sen = np.log10(PhotonToJ.to(data["sen"], wavelength)) # W.m^3 st = np.log10(data["st"] / data["sref"]) # dimensionless # store limits of data @@ -228,14 +269,8 @@ cdef class BeamEmissionPEC(CoreBeamEmissionPEC): """ # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if energy < 1.e-300: - energy = 1.e-300 - - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if energy <= 0 or density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** (self._npl_eb.evaluate(log10(energy), log10(density)) + self._tp.evaluate(log10(temperature))) @@ -243,7 +278,7 @@ cdef class BeamEmissionPEC(CoreBeamEmissionPEC): cdef class NullBeamEmissionPEC(CoreBeamEmissionPEC): """ - A beam rate that always returns zero. + A beam emission PEC that always returns zero. Needed for use cases where the required atomic data is missing. """ diff --git a/cherab/openadas/rates/cx.pxd b/cherab/openadas/rates/cx.pxd index 393027d0..4afcadfc 100644 --- a/cherab/openadas/rates/cx.pxd +++ b/cherab/openadas/rates/cx.pxd @@ -25,7 +25,6 @@ cdef class BeamCXPEC(CoreBeamCXPEC): cdef readonly: dict raw_data double wavelength - int donor_metastable Function1D _eb, _ti, _ni, _zeff, _b readonly tuple beam_energy_range readonly tuple density_range diff --git a/cherab/openadas/rates/cx.pyx b/cherab/openadas/rates/cx.pyx index cb827a8b..3e0735d9 100644 --- a/cherab/openadas/rates/cx.pyx +++ b/cherab/openadas/rates/cx.pyx @@ -1,6 +1,6 @@ -# Copyright 2016-2021 Euratom -# Copyright 2016-2021 United Kingdom Atomic Energy Authority -# Copyright 2016-2021 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -26,18 +26,48 @@ from raysect.core.math.function.float cimport Interpolator1DArray, Constant1D cdef class BeamCXPEC(CoreBeamCXPEC): """ - The effective cx rate interpolation class. - - :param donor_metastable: The metastable state of the donor species for which the rate data applies. - :param wavelength: The natural wavelength of the emission line associated with the rate data in nm. - :param data: A dictionary holding the rate data. - :param extrapolate: Set to True to enable extrapolation, False to disable (default). + Effective charge exchange photon emission coefficient. + + The data for "qeb" is interpolated with a cubic spline in log-log space. + The data for "qti", "qni", "qz" and "qb" are interpolated with a cubic spline + in linear space. + + Quadratic extrapolation is used for "qeb" and nearest neighbour extrapolation is used for + "qti", "qni", "qz" and "qb" if extrapolate is True. + + :param int donor_metastable: The metastable state of the donor species for which the rate data applies. + :param double wavelength: The natural wavelength of the emission line associated with the rate data in nm. + :param data: Beam CX PEC dictionary containing the following entries: + + | 'eb': 1D array of size (N) with beam energy in eV/amu, + | 'ti': 1D array of size (M) with receiver ion temperature in eV, + | 'ni': 1D array of size (K) with receiver ion density in m^-3, + | 'z': 1D array of size (L) with receiver Z-effective, + | 'b': 1D array of size (J) with magnetic field strength in Tesla, + | 'qeb': 1D array of size (N) with CX PEC energy component in photon.m^3.s-1, + | 'qti': 1D array of size (M) with CX PEC temperature component in photon.m^3.s-1, + | 'qni': 1D array of size (K) with CX PEC density component in photon.m^3.s-1, + | 'qz': 1D array of size (L) with CX PEC Zeff component in photon.m^3.s-1, + | 'qb': 1D array of size (J) with CX PEC B-field component in photon.m^3.s-1, + | 'qref': reference CX PEC in photon.m^3.s-1. + | The total beam CX PEC: q = qeb * qti * qni * qz * qb / qref^4. + + :param bint extrapolate: Set to True to enable extrapolation, False to disable (default). + + :ivar tuple beam_energy_range: Interaction energy interpolation range. + :ivar tuple density_range: Receiver ion density interpolation range. + :ivar tuple temperature_range: Receiver ion temperature interpolation range. + :ivar tuple zeff_range: Z-effective interpolation range. + :ivar tuple b_field_range: Magnetic field strength interpolation range. + :ivar int donor_metastable: The metastable state of the donor species. + :ivar double wavelength: The natural wavelength of the emission line in nm. + :ivar dict raw_data: Dictionary containing the raw data. """ @cython.cdivision(True) def __init__(self, int donor_metastable, double wavelength, dict data, bint extrapolate=False): - self.donor_metastable = donor_metastable + super().__init__(donor_metastable) self.wavelength = wavelength self.raw_data = data @@ -79,7 +109,7 @@ cdef class BeamCXPEC(CoreBeamCXPEC): :param energy: Interaction energy in eV/amu. :param temperature: Receiver ion temperature in eV. - :param density: Receiver ion density in m^-3 + :param density: Plasma total ion density in m^-3 :param z_effective: Plasma Z-effective. :param b_field: Magnetic field magnitude in Tesla. :return: The effective cx rate in W.m^3 @@ -88,8 +118,8 @@ cdef class BeamCXPEC(CoreBeamCXPEC): cdef double rate # need to handle zeros for log-log interpolation - if energy < 1.e-300: - energy = 1.e-300 + if energy <= 0: + return 0 rate = 10 ** self._eb.evaluate(log10(energy)) diff --git a/cherab/openadas/rates/pec.pxd b/cherab/openadas/rates/pec.pxd index 46a54cbe..57bc7560 100644 --- a/cherab/openadas/rates/pec.pxd +++ b/cherab/openadas/rates/pec.pxd @@ -19,7 +19,7 @@ from cherab.core cimport ImpactExcitationPEC as CoreImpactExcitationPEC from cherab.core cimport RecombinationPEC as CoreRecombinationPEC from cherab.core cimport ThermalCXPEC as CoreThermalCXPEC -from cherab.core.math cimport Function2D +from cherab.core.math cimport Function2D, Function3D cdef class ImpactExcitationPEC(CoreImpactExcitationPEC): @@ -48,8 +48,14 @@ cdef class NullRecombinationPEC(CoreRecombinationPEC): pass -# cdef class CXThermalRate(CoreCXThermalRate): -# pass -# -# cdef class ThermalCXRate(CoreThermalCXRate): -# pass +cdef class ThermalCXPEC(CoreThermalCXPEC): + + cdef: + readonly dict raw_data + readonly double wavelength + readonly tuple density_range, temperature_range, donor_temperature_range + Function3D _rate + + +cdef class NullThermalCXPEC(CoreThermalCXPEC): + pass diff --git a/cherab/openadas/rates/pec.pyx b/cherab/openadas/rates/pec.pyx index eafc6c64..b10b0547 100644 --- a/cherab/openadas/rates/pec.pyx +++ b/cherab/openadas/rates/pec.pyx @@ -1,6 +1,6 @@ -# Copyright 2016-2021 Euratom -# Copyright 2016-2021 United Kingdom Atomic Energy Authority -# Copyright 2016-2021 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -20,18 +20,32 @@ import numpy as np from libc.math cimport INFINITY, log10 -from raysect.core.math.function.float cimport Interpolator2DArray +from raysect.core.math.function.float cimport Interpolator2DArray, Interpolator3DArray from cherab.core.utility.conversion import PhotonToJ cdef class ImpactExcitationPEC(CoreImpactExcitationPEC): + """ + Electron impact excitation photon emission coefficient. + + The data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param double wavelength: Resting wavelength of corresponding emission line in nm. + :param dict data: Excitation PEC dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with excitation PEC in photon.m^3.s^-1. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, double wavelength, dict data, extrapolate=False): - """ - :param wavelength: Resting wavelength of corresponding emission line in nm. - :param data: Dictionary containing rate data. - :param extrapolate: Enable extrapolation (default=False). - """ self.wavelength = wavelength self.raw_data = data @@ -56,11 +70,8 @@ cdef class ImpactExcitationPEC(CoreImpactExcitationPEC): cpdef double evaluate(self, double density, double temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(density), log10(temperature)) @@ -68,7 +79,7 @@ cdef class ImpactExcitationPEC(CoreImpactExcitationPEC): cdef class NullImpactExcitationPEC(CoreImpactExcitationPEC): """ - A PEC rate that always returns zero. + A electron impact excitation PEC rate that always returns zero. Needed for use cases where the required atomic data is missing. """ @@ -77,13 +88,27 @@ cdef class NullImpactExcitationPEC(CoreImpactExcitationPEC): cdef class RecombinationPEC(CoreRecombinationPEC): + """ + Recombination photon emission coefficient. + + The data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param double wavelength: Resting wavelength of corresponding emission line in nm. + :param dict data: Rcombination PEC dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with recombination PEC in photon.m^3.s^-1. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, double wavelength, dict data, extrapolate=False): - """ - :param wavelength: Resting wavelength of corresponding emission line in nm. - :param data: Dictionary containing rate data. - :param extrapolate: Enable extrapolation (default=False). - """ self.wavelength = wavelength self.raw_data = data @@ -108,11 +133,8 @@ cdef class RecombinationPEC(CoreRecombinationPEC): cpdef double evaluate(self, double density, double temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if density < 1.e-300: - density = 1.e-300 - - if temperature < 1.e-300: - temperature = 1.e-300 + if density <= 0 or temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(density), log10(temperature)) @@ -120,9 +142,60 @@ cdef class RecombinationPEC(CoreRecombinationPEC): cdef class NullRecombinationPEC(CoreRecombinationPEC): """ - A PEC rate that always returns zero. + A recombination PEC rate that always returns zero. Needed for use cases where the required atomic data is missing. """ cpdef double evaluate(self, double density, double temperature) except? -1e999: return 0.0 + + +cdef class ThermalCXPEC(CoreThermalCXPEC): + + def __init__(self, double wavelength, dict data, extrapolate=False): + """ + :param wavelength: Resting wavelength of corresponding emission line in nm. + :param data: Dictionary containing rate data. + :param extrapolate: Enable nearest-neighbour extrapolation (default=False). + """ + + self.wavelength = wavelength + self.raw_data = data + + # unpack + ne = data['ne'] + te = data['te'] + td = data['td'] + rate = data['rate'] + + # pre-convert data to W m^3 from Photons s^-1 cm^3 prior to interpolation + rate = np.log10(PhotonToJ.to(rate, wavelength)) + + # store limits of data + self.density_range = ne.min(), ne.max() + self.temperature_range = te.min(), te.max() + self.donor_temperature_range = td.min(), td.max() + + # interpolate rate + # using nearest extrapolation to avoid infinite values at 0 for some rates + extrapolation_type = 'nearest' if extrapolate else 'none' + self._rate = Interpolator3DArray(np.log10(ne), np.log10(te), np.log10(td), rate, 'cubic', extrapolation_type, INFINITY, INFINITY, INFINITY) + + cpdef double evaluate(self, double electron_density, double electron_temperature, double donor_temperature) except? -1e999: + + # need to handle zeros, also density and temperature can become negative due to cubic interpolation + if electron_density <= 0 or electron_temperature <= 0 or donor_temperature <= 0: + return 0 + + # calculate rate and convert from log10 space to linear space + return 10 ** self._rate.evaluate(log10(electron_density), log10(electron_temperature), log10(donor_temperature)) + + +cdef class NullThermalCXPEC(CoreThermalCXPEC): + """ + A PEC rate that always returns zero. + Needed for use cases where the required atomic data is missing. + """ + + cpdef double evaluate(self, double electron_density, double electron_temperature, double donor_temperature) except? -1e999: + return 0.0 diff --git a/cherab/openadas/rates/radiated_power.pyx b/cherab/openadas/rates/radiated_power.pyx index 570ced92..67a3dde0 100644 --- a/cherab/openadas/rates/radiated_power.pyx +++ b/cherab/openadas/rates/radiated_power.pyx @@ -1,7 +1,7 @@ -# Copyright 2016-2021 Euratom -# Copyright 2016-2021 United Kingdom Atomic Energy Authority -# Copyright 2016-2021 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -24,7 +24,26 @@ from raysect.core.math.function.float cimport Interpolator2DArray cdef class LineRadiationPower(CoreLineRadiationPower): - """Base class for radiated powers.""" + """ + Line radiated power coefficient. + + The data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param Element species: Element object defining the ion type. + :param int ionisation: Charge state of the ion. + :param dict data: Line radiated power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with radiated power rate in W.m^3. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, species, ionisation, dict data, extrapolate=False): @@ -46,14 +65,11 @@ cdef class LineRadiationPower(CoreLineRadiationPower): extrapolation_type = 'nearest' if extrapolate else 'none' self._rate = Interpolator2DArray(np.log10(ne), np.log10(te), rate, 'cubic', extrapolation_type, INFINITY, INFINITY) - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if electron_density < 1.e-300: - electron_density = 1.e-300 - - if electron_temperature < 1.e-300: - electron_temperature = 1.e-300 + if electron_density <= 0 or electron_temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(electron_density), log10(electron_temperature)) @@ -65,12 +81,31 @@ cdef class NullLineRadiationPower(CoreLineRadiationPower): Needed for use cases where the required atomic data is missing. """ - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: return 0.0 cdef class ContinuumPower(CoreContinuumPower): - """Base class for radiated powers.""" + """ + Recombination continuum radiated power coefficient. + + The data is interpolated with cubic spline in log-log space. + Nearest neighbour extrapolation is used if extrapolate is True. + + :param Element species: Element object defining the ion type. + :param int ionisation: Charge state of the ion. + :param dict data: Recombination continuum radiated power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with radiated power rate in W.m^3. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, species, ionisation, dict data, extrapolate=False): @@ -92,14 +127,11 @@ cdef class ContinuumPower(CoreContinuumPower): extrapolation_type = 'nearest' if extrapolate else 'none' self._rate = Interpolator2DArray(np.log10(ne), np.log10(te), rate, 'cubic', extrapolation_type, INFINITY, INFINITY) - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if electron_density < 1.e-300: - electron_density = 1.e-300 - - if electron_temperature < 1.e-300: - electron_temperature = 1.e-300 + if electron_density <= 0 or electron_temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(electron_density), log10(electron_temperature)) @@ -111,12 +143,31 @@ cdef class NullContinuumPower(CoreContinuumPower): Needed for use cases where the required atomic data is missing. """ - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: return 0.0 cdef class CXRadiationPower(CoreCXRadiationPower): - """Base class for radiated powers.""" + """ + Charge exchange radiated power coefficient. + + The data is interpolated with cubic spline in log-log space. + Linear extrapolation is used if extrapolate is True. + + :param Element species: Element object defining the ion type. + :param int ionisation: Charge state of the ion. + :param dict data: CX radiated power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with radiated power rate in W.m^3. + + :param bint extrapolate: Enable extrapolation (default=False). + + :ivar tuple density_range: Electron density interpolation range. + :ivar tuple temperature_range: Electron temperature interpolation range. + :ivar dict raw_data: Dictionary containing the raw data. + """ def __init__(self, species, ionisation, dict data, extrapolate=False): @@ -137,14 +188,11 @@ cdef class CXRadiationPower(CoreCXRadiationPower): extrapolation_type = 'linear' if extrapolate else 'none' self._rate = Interpolator2DArray(np.log10(ne), np.log10(te), rate, 'cubic', extrapolation_type, INFINITY, INFINITY) - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: # need to handle zeros, also density and temperature can become negative due to cubic interpolation - if electron_density < 1.e-300: - electron_density = 1.e-300 - - if electron_temperature < 1.e-300: - electron_temperature = 1.e-300 + if electron_density <= 0 or electron_temperature <= 0: + return 0 # calculate rate and convert from log10 space to linear space return 10 ** self._rate.evaluate(log10(electron_density), log10(electron_temperature)) @@ -156,5 +204,5 @@ cdef class NullCXRadiationPower(CoreCXRadiationPower): Needed for use cases where the required atomic data is missing. """ - cdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: + cpdef double evaluate(self, double electron_density, double electron_temperature) except? -1e999: return 0.0 diff --git a/cherab/openadas/repository/atomic.py b/cherab/openadas/repository/atomic.py index c24234f3..cb8add6c 100644 --- a/cherab/openadas/repository/atomic.py +++ b/cherab/openadas/repository/atomic.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -34,8 +34,15 @@ def add_ionisation_rate(species, charge, rate, repository_path=None): function instead. The update function avoids repeatedly opening and closing the rate files. - :param repository_path: - :return: + :param species: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param rate: Ionisation rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with ionisation rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ update_ionisation_rates({ @@ -47,11 +54,21 @@ def add_ionisation_rate(species, charge, rate, repository_path=None): def update_ionisation_rates(rates, repository_path=None): """ - Ionisation rate file structure - - /ionisation/.json + Updates the ionisation rate files `/ionisation/.json` + in atomic data repository. File contains multiple rates, indexed by the ion charge state. + + :param rates: Dictionary in the form {: {: }}, where + + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the ionisation rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with ionisation rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -75,8 +92,15 @@ def add_recombination_rate(species, charge, rate, repository_path=None): function instead. The update function avoids repeatedly opening and closing the rate files. - :param repository_path: - :return: + :param species: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param rate: Recombination rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with recombination rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ update_recombination_rates({ @@ -88,11 +112,21 @@ def add_recombination_rate(species, charge, rate, repository_path=None): def update_recombination_rates(rates, repository_path=None): """ - Ionisation rate file structure - - /recombination/.json + Updates the recombination rate files `/recombination/.json` + in the atomic data repository. File contains multiple rates, indexed by the ion charge state. + + :param rates: Dictionary in the form {: {: }}, where + + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the recombination rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with recombination rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -109,7 +143,6 @@ def update_recombination_rates(rates, repository_path=None): def add_thermal_cx_rate(donor_element, donor_charge, receiver_element, rate, repository_path=None): - """ Adds a single thermal charge exchange rate to the repository. @@ -118,11 +151,16 @@ def add_thermal_cx_rate(donor_element, donor_charge, receiver_element, rate, rep the rate files. :param donor_element: Element donating the electron. - :param donor_charge: Charge of the donating atom/ion - :param receiver_element: Element receiving the electron - :param rate: rates - :param repository_path: - :return: + :param donor_charge: Charge of the donating atom/ion. + :param receiver_element: Element receiving the electron. + :param receiver_charge: Charge of the receiving atom/ion. + :param rate: Thermal CX rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with thermal CX rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ rates2update = RecursiveDict() @@ -133,11 +171,25 @@ def add_thermal_cx_rate(donor_element, donor_charge, receiver_element, rate, rep def update_thermal_cx_rates(rates, repository_path=None): """ - Thermal charge exchange rate file structure - - /thermal_cx///.json + Updates the thermal charge exchange rate files + `/thermal_cx///.json` + in the atomic data repository. File contains multiple rates, indexed by the ion charge state. + + :param rates: Dictionary in the form: + + | { : { : { : { : } } } }, where + | is the element donating the electron. + | is the charge of the donating atom/ion. + | is the element receiving the electron. + | is the charge of the receiving atom/ion. + | is the thermal CX rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with thermal CX rate in m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -203,6 +255,21 @@ def _update_and_write_adf11(species, rate_data, path): def get_ionisation_rate(element, charge, repository_path=None): + """ + Reads the ionisation rate for the given species and charge + from the atomic data repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param repository_path: Path to the atomic data repository. + + :return rate: Ionisation rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with ionisation rate in m^3.s^-1. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -224,6 +291,21 @@ def get_ionisation_rate(element, charge, repository_path=None): def get_recombination_rate(element, charge, repository_path=None): + """ + Reads the recombination rate for the given species and charge + from the atomic data repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param repository_path: Path to the atomic data repository. + + :return rate: Recombination rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with recombination rate in m^3.s^-1. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -245,6 +327,23 @@ def get_recombination_rate(element, charge, repository_path=None): def get_thermal_cx_rate(donor_element, donor_charge, receiver_element, receiver_charge, repository_path=None): + """ + Reads the thermal charge exchange rate for the given species and charge + from the atomic data repository. + + :param donor_element: Element donating the electron. + :param donor_charge: Charge of the donating atom/ion. + :param receiver_element: Element receiving the electron. + :param receiver_charge: Charge of the receiving atom/ion. + :param repository_path: Path to the atomic data repository. + + :return rate: Thermal CX rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with thermal CX rate in m^3.s^-1. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH diff --git a/cherab/openadas/repository/beam/cx.py b/cherab/openadas/repository/beam/cx.py index 65bc3ceb..ef79c102 100644 --- a/cherab/openadas/repository/beam/cx.py +++ b/cherab/openadas/repository/beam/cx.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -29,19 +29,33 @@ def add_beam_cx_rate(donor_ion, donor_metastable, receiver_ion, receiver_charge, transition, rate, repository_path=None): """ - Adds a single beam CX rate to the repository. + Adds a single beam CX PEC to the repository. If adding multiple rate, consider using the update_beam_cx_rates() function instead. The update function avoid repeatedly opening and closing the rate files. - :param donor_ion: - :param donor_metastable: - :param receiver_ion: - :param receiver_charge: - :param rate: - :param repository_path: - :return: + :param donor_ion: Beam neutral atom (Element/Isotope) donating the electron. + :param donor_metastable: Metastable/excited level of beam neutral atom. + :param receiver_ion: Element/Isotope receiving the electron. + :param receiver_charge: Charge of the receiving atom/ion. + :param transition: Tuple containing (initial level, final level). + :param rate: Beam CX PEC dictionary containing the following entries: + + | 'eb': array-like of size (N) with beam energy in eV/amu, + | 'ti': array-like of size (M) with receiver ion temperature in eV, + | 'ni': array-like of size (K) with plasma ion density in m^-3, + | 'z': array-like of size (L) with plasma Z-effective, + | 'b': array-like of size (J) with magnetic field strength in Tesla, + | 'qeb': array-like of size (N) with CX PEC energy component in photon.m^3.s-1, + | 'qti': array-like of size (M) with CX PEC temperature component in photon.m^3.s-1, + | 'qni': array-like of size (K) with CX PEC density component in photon.m^3.s-1, + | 'qz': array-like of size (L) with CX PEC Zeff component in photon.m^3.s-1, + | 'qb': array-like of size (J) with CX PEC B-field component in photon.m^3.s-1, + | 'qref': reference CX PEC in photon.m^3.s-1. + | The total beam CX PEC: q = qeb * qti * qni * qz * qb / qref^4. + + :param repository_path: Path to the atomic data repository. """ update_beam_cx_rates({ @@ -58,10 +72,37 @@ def add_beam_cx_rate(donor_ion, donor_metastable, receiver_ion, receiver_charge, def update_beam_cx_rates(rates, repository_path=None): - # organisation in repository: - # beam/cx/donor_ion/receiver_ion/receiver_charge.json - # inside json file: - # transition: [list of donor_metastables with rates] + """ + Updates the beam CX PEC files + beam/cx///.json + in the atomic data repository. + + File contains multiple metastable-resolved rates, indexed by transition. + + :param rates: Dictionary in the form: + + | { : { : { : { : {: } } } } }, where + | is the beam neutral atom (Element/Isotope) donating the electron. + | is the metastable/excited level of beam neutral atom. + | is the Element/Isotope receiving the electron. + | is the charge of the receiving atom/ion. + | is the tuple containing (initial level, final level). + | is the beam CX PEC dictionary containing the following entries: + | 'eb': array-like of size (N) with beam energy in eV/amu, + | 'ti': array-like of size (M) with receiver ion temperature in eV, + | 'ni': array-like of size (K) with plasma ion density in m^-3, + | 'z': array-like of size (L) with plasma Z-effective, + | 'b': array-like of size (J) with magnetic field strength in Tesla, + | 'qeb': array-like of size (N) with CX PEC energy component in photon.m^3.s-1, + | 'qti': array-like of size (M) with CX PEC temperature component in photon.m^3.s-1, + | 'qni': array-like of size (K) with CX PEC density component in photon.m^3.s-1, + | 'qz': array-like of size (L) with CX PEC Zeff component in photon.m^3.s-1, + | 'qb': array-like of size (J) with CX PEC B-field component in photon.m^3.s-1, + | 'qref': reference CX PEC in photon.m^3.s-1. + | The total beam CX PEC: q = qeb * qti * qni * qz * qb / qref^4. + + :param repository_path: Path to the atomic data repository. + """ def sanitise_and_validate(data, x_key, x_name, y_key, y_name): """ @@ -167,6 +208,32 @@ def sanitise_and_validate(data, x_key, x_name, y_key, y_name): def get_beam_cx_rates(donor_ion, receiver_ion, receiver_charge, transition, repository_path=None): + """ + Reads a single beam CX PEC from the repository. + + :param donor_ion: Beam neutral atom (Element/Isotope) donating the electron. + :param donor_metastable: Metastable/excited level of beam neutral atom. + :param receiver_ion: Element/Isotope receiving the electron. + :param receiver_charge: Charge of the receiving atom/ion. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. + + :return rate: Beam CX PEC dictionary containing the following entries: + + | 'eb': 1D array of size (N) with beam energy in eV/amu, + | 'ti': 1D array of size (M) with receiver ion temperature in eV, + | 'ni': 1D array of size (K) with plasma ion density in m^-3, + | 'z': 1D array of size (L) with plasma Z-effective, + | 'b': 1D array of size (J) with magnetic field strength in Tesla, + | 'qeb': 1D array of size (N) with CX PEC energy component in photon.m^3.s-1, + | 'qti': 1D array of size (M) with CX PEC temperature component in photon.m^3.s-1, + | 'qni': 1D array of size (K) with CX PEC density component in photon.m^3.s-1, + | 'qz': 1D array of size (L) with CX PEC Zeff component in photon.m^3.s-1, + | 'qb': 1D array of size (J) with CX PEC B-field component in photon.m^3.s-1, + | 'qref': reference CX PEC in photon.m^3.s-1. + | The total beam CX PEC: q = qeb * qti * qni * qz * qb / qref^4. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH path = os.path.join(repository_path, 'beam/cx/{}/{}/{}.json'.format(donor_ion.symbol.lower(), receiver_ion.symbol.lower(), receiver_charge)) diff --git a/cherab/openadas/repository/beam/emission.py b/cherab/openadas/repository/beam/emission.py index b6cd14f2..c7c6e2e1 100644 --- a/cherab/openadas/repository/beam/emission.py +++ b/cherab/openadas/repository/beam/emission.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -36,8 +36,24 @@ def add_beam_emission_rate(beam_species, target_ion, target_charge, transition, function instead. The update function avoid repeatedly opening and closing the rate files. - :param repository_path: - :return: + :param beam_species: Beam neutral species (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param transition: Tuple containing (initial level, final level). + :param rate: Beam emission rate dictionary containing the following entries: + + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n' array-like of size (M) with target electron density in m^-3, + | 't' array-like of size (K) with target electron temperature in eV, + | 'sen' array-like of size (N, M) with beam emission rate energy component in photon.m^3.s^-1. + | 'st' array-like of size (K) with beam emission rate temperature component in photon.m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam emission rate in photon.m^3.s^-1. + | The total beam emission rate: s = sen * st / sref. + + :param repository_path: Path to the atomic data repository. """ update_beam_emission_rates({ @@ -53,11 +69,32 @@ def add_beam_emission_rate(beam_species, target_ion, target_charge, transition, def update_beam_emission_rates(rates, repository_path=None): """ - Beam emission rate file structure - + Updates the beam emission rate files: /beam/emission///.json + in the atomic repository. File contains multiple rates, indexed by transition. + + :param rates: Dictionary in the form: + + | { : { : { : {: } } } }, where + | is the beam neutral species (Element/Isotope) + | is the target species (Element/Isotope). + | is the charge of the target species. + | is the tuple containing (initial level, final level). + | Beam emission rate dictionary containing the following entries: + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n' array-like of size (M) with target electron density in m^-3, + | 't' array-like of size (K) with target electron temperature in eV, + | 'sen' array-like of size (N, M) with beam emission rate energy component in photon.m^3.s^-1. + | 'st' array-like of size (K) with beam emission rate temperature component in photon.m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam emission rate in photon.m^3.s^-1. + | The total beam emission rate: s = sen * st / sref. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -136,6 +173,29 @@ def update_beam_emission_rates(rates, repository_path=None): def get_beam_emission_rate(beam_species, target_ion, target_charge, transition, repository_path=None): + """ + Reads a single beam emission rate from the repository. + + :param beam_species: Beam neutral species (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. + + :return rate: Beam emission rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n' 1D array of size (M) with target electron density in m^-3, + | 't' 1D array of size (K) with target electron temperature in eV, + | 'sen' 2D array of size (N, M) with beam emission rate energy component in photon.m^3.s^-1. + | 'st' 1D array of size (K) with beam emission rate temperature component in photon.m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam emission rate in photon.m^3.s^-1. + | The total beam emission rate: s = sen * st / sref. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH path = os.path.join(repository_path, 'beam/emission/{}/{}/{}.json'.format(beam_species.symbol.lower(), target_ion.symbol.lower(), target_charge)) diff --git a/cherab/openadas/repository/beam/population.py b/cherab/openadas/repository/beam/population.py index 54c57df1..9ecfa6eb 100644 --- a/cherab/openadas/repository/beam/population.py +++ b/cherab/openadas/repository/beam/population.py @@ -31,12 +31,24 @@ def add_beam_population_rate(beam_species, beam_metastable, target_ion, target_c """ Adds a single beam population rate to the repository. - :param beam_species: - :param beam_metastable: - :param target_ion: - :param target_charge: - :param rate: - :return: + :param beam_species: Beam neutral species (Element/Isotope). + :param beam_metastable: Metastable level of beam neutral atom. + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param rate: Beam population rate dictionary containing the following entries: + + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n': array-like of size (M) with target electron density in m^-3, + | 't': array-like of size (K) with target electron temperature in eV, + | 'sen': array-like of size (N, M) with dimensionless beam population rate energy component. + | 'st': array-like of size (K) with dimensionless beam population rate temperature component. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference dimensionless beam population rate. + | The total beam population rate: s = sen * st / sref. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -102,11 +114,32 @@ def add_beam_population_rate(beam_species, beam_metastable, target_ion, target_c def update_beam_population_rates(rates, repository_path=None): """ - Beam population rate file structure - + Updates the beam population rate files /beam/population////.json + in the atomic data repository. Each json file contains a single rate, so it can simply be replaced. + + :param rates: Dictionary in the form: + + | { : { : { : {: } } } }, where + | is the beam neutral species (Element/Isotope) + | is the metastable level of beam neutral atom. + | is the target species (Element/Isotope). + | is the charge of the target species. + | is the beam population rate dictionary containing the following fields: + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n': array-like of size (M) with target electron density in m^-3, + | 't': array-like of size (K) with target electron temperature in eV, + | 'sen': array-like of size (N, M) with dimensionless beam population rate energy component. + | 'st': array-like of size (K) with dimensionless beam population rate temperature component. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference dimensionless beam population rate. + | The total beam population rate: s = sen * st / sref. + + :param repository_path: Path to the atomic data repository. """ for beam_species, beam_metastables in rates.items(): @@ -117,6 +150,29 @@ def update_beam_population_rates(rates, repository_path=None): def get_beam_population_rate(beam_species, beam_metastable, target_ion, target_charge, repository_path=None): + """ + Reads a single beam population rate from the repository. + + :param beam_species: Beam neutral species (Element/Isotope). + :param beam_metastable: Metastable level of beam neutral atom. + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param repository_path: Path to the atomic data repository. + + :return rate: Beam population rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n': 1D array of size (M) with target electron density in m^-3, + | 't': 1D array of size (K) with target electron temperature in eV, + | 'sen': 2D array of size (N, M) with dimensionless beam population rate energy component. + | 'st': 1D array of size (K) with dimensionless beam population rate temperature component. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference dimensionless beam population rate. + | The total beam population rate: s = sen * st / sref. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH path = os.path.join(repository_path, 'beam/population/{}/{}/{}/{}.json'.format(beam_species.symbol.lower(), beam_metastable, target_ion.symbol.lower(), target_charge)) diff --git a/cherab/openadas/repository/beam/stopping.py b/cherab/openadas/repository/beam/stopping.py index cf8d1492..46d8caae 100644 --- a/cherab/openadas/repository/beam/stopping.py +++ b/cherab/openadas/repository/beam/stopping.py @@ -31,11 +31,23 @@ def add_beam_stopping_rate(beam_species, target_ion, target_charge, rate, reposi """ Adds a single beam stopping/excitation rate to the repository. - :param beam_species: - :param target_ion: - :param target_charge: - :param rate: - :return: + :param beam_species: Beam neutral atom (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param rate: Beam stopping rate dictionary containing the following entries: + + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n': array-like of size (M) with target electron density in m^-3, + | 't': array-like of size (K) with target electron temperature in eV, + | 'sen': array-like of size (N, M) with beam stopping rate energy component in m^3.s^-1. + | 'st': array-like of size (K) with beam stopping rate temperature component in m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam stopping rate in m^3.s^-1. + | The total beam stopping rate: s = sen * st / sref. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -98,11 +110,30 @@ def add_beam_stopping_rate(beam_species, target_ion, target_charge, rate, reposi def update_beam_stopping_rates(rates, repository_path=None): """ - Beam stopping rate file structure - - /beam/stopping///.json + Updates the beam stopping rate files + /beam/stopping////.json + in the atomic data repository. Each json file contains a single rate, so it can simply be replaced. + + :param rates: Dictionary in the form: + + | { : { : { : {: } } } }, where + | is the beam neutral species (Element/Isotope). + | is the target species (Element/Isotope). + | is the charge of the target species. + | is the beam stopping rate dictionary containing the following entries: + | 'e': array-like of size (N) with interaction energy in eV/amu, + | 'n': array-like of size (M) with target electron density in m^-3, + | 't': array-like of size (K) with target electron temperature in eV, + | 'sen': array-like of size (N, M) with beam stopping rate energy component in m^3.s^-1. + | 'st': array-like of size (K) with beam stopping rate temperature component in m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam stopping rate in m^3.s^-1. + | The total beam stopping rate: s = sen * st / sref. + """ for beam_species, target_ions in rates.items(): @@ -112,6 +143,28 @@ def update_beam_stopping_rates(rates, repository_path=None): def get_beam_stopping_rate(beam_species, target_ion, target_charge, repository_path=None): + """ + Reads a single beam stopping/excitation rate from the repository. + + :param beam_species: Beam neutral atom (Element/Isotope). + :param target_ion: Target species (Element/Isotope). + :param target_charge: Charge of the target species. + :param repository_path: Path to the atomic data repository. + + :return rate: Beam stopping rate dictionary containing the following entries: + + | 'e': 1D array of size (N) with interaction energy in eV/amu, + | 'n': 1D array of size (M) with target electron density in m^-3, + | 't': 1D array of size (K) with target electron temperature in eV, + | 'sen': 2D array of size (N, M) with beam stopping rate energy component in m^3.s^-1. + | 'st': 1D array of size (K) with beam stopping rate temperature component in m^3.s^-1. + | 'eref': reference interaction energy in eV/amu, + | 'nref': reference target electron density in m^-3, + | 'tref': reference target electron temperature in eV, + | 'sref': reference beam stopping rate in m^3.s^-1. + | The total beam stopping rate: s = sen * st / sref. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH path = os.path.join(repository_path, 'beam/stopping/{}/{}/{}.json'.format(beam_species.symbol.lower(), target_ion.symbol.lower(), target_charge)) diff --git a/cherab/openadas/repository/create.py b/cherab/openadas/repository/create.py index 6b9a4f33..b42e5651 100644 --- a/cherab/openadas/repository/create.py +++ b/cherab/openadas/repository/create.py @@ -147,6 +147,7 @@ def populate(download=True, repository_path=None, adas_path=None): (carbon, 0, 'adf15/pec96#c/pec96#c_vsu#c0.dat'), (carbon, 1, 'adf15/pec96#c/pec96#c_vsu#c1.dat'), (carbon, 2, 'adf15/pec96#c/pec96#c_vsu#c2.dat'), + (carbon, 5, 'adf15/pec96#c/pec96#c_pju#c5.dat'), # (neon, 0, 'adf15/pec96#ne/pec96#ne_pju#ne0.dat'), #TODO: OPENADAS DATA CORRUPT # (neon, 1, 'adf15/pec96#ne/pec96#ne_pju#ne1.dat'), #TODO: OPENADAS DATA CORRUPT (nitrogen, 0, 'adf15/pec96#n/pec96#n_vsu#n0.dat'), diff --git a/cherab/openadas/repository/pec.py b/cherab/openadas/repository/pec.py index 8eb867fc..ef933b23 100644 --- a/cherab/openadas/repository/pec.py +++ b/cherab/openadas/repository/pec.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -36,12 +36,16 @@ def add_pec_excitation_rate(element, charge, transition, rate, repository_path=N instead. The update function avoid repeatedly opening and closing the rate files. - :param element: - :param charge: - :param transition: - :param rate: - :param repository_path: - :return: + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param rate: Excitation PEC dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with excitation PEC in photon.m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ update_pec_rates({ @@ -63,12 +67,16 @@ def add_pec_recombination_rate(element, charge, transition, rate, repository_pat instead. The update function avoid repeatedly opening and closing the rate files. - :param element: - :param charge: - :param transition: - :param rate: - :param repository_path: - :return: + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param rate: Recombination PEC dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with recombination PEC in photon.m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ update_pec_rates({ @@ -82,44 +90,59 @@ def add_pec_recombination_rate(element, charge, transition, rate, repository_pat }, repository_path) -def add_pec_thermalcx_rate(element, charge, transition, rate, repository_path=None): +def add_pec_thermal_cx_rate(donor_element, donor_charge, receiver_element, receiver_charge, transition, rate, repository_path=None): """ - Adds a single PEC thermalcx rate to the repository. + Adds a single PEC thermal charge exchange rate to the repository. - If adding multiple rate, consider using the update_pec_rates() function + If adding multiple rate, consider using the update_pec_thermal_rates() function instead. The update function avoid repeatedly opening and closing the rate files. - :param element: - :param charge: - :param transition: - :param rate: - :param repository_path: - :return: + :param donor_element: Electron donor plasma species (Element/Isotope). + :param donor_charge: Electron donor charge. + :param receiver_element: Electron receiver plasma species (Element/Isotope). + :param receiver_charge: Electron receiver charge. + :param transition: Tuple containing (initial level, final level). + :param rate: Thermal CX PEC dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'td': array-like of size (K) with donor temperature in eV, + | 'rate': array-like of size (N, M, K) with thermal CX PEC in photon.m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ + rates2update = RecursiveDict() + rates2update[donor_element][donor_charge][receiver_element][receiver_charge][transition] = rate - update_pec_rates({ - 'thermalcx': { - element: { - charge: { - transition: rate - } - } - } - }, repository_path) + update_pec_thermal_cx_rates(rates2update.freeze(), repository_path) def update_pec_rates(rates, repository_path=None): """ - PEC rate file structure + Updates excitation and recombination PEC files /pec///.json. + in the atomic data repository. - /pec///.json + File contains multiple PECs, indexed by the transition. + + :param rates: Dictionary in the form: + + | { : { : { : { : } } } }, where + | is the one of the following PEC types: 'excitation', 'recombination'. + | is the plasma species (Element/Isotope). + | is the charge of the plasma species. + | is the tuple containing (initial level, final level). + | is the PEC dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with PEC in photon.m^3.s^-1. + + :param repository_path: Path to the atomic data repository. """ valid_classes = [ 'excitation', - 'recombination', - 'thermalcx' + 'recombination' ] repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -183,16 +206,139 @@ def update_pec_rates(rates, repository_path=None): json.dump(content, f, indent=2, sort_keys=True) +def update_pec_thermal_cx_rates(rates, repository_path=None): + """ + Updates thermal CX PEC files /pec/thermal_cx////.json + in the atomic data repository. + + File contains multiple PECs, indexed by the transition. + + :param rates: Dictionary in the form: + + | { : { : { : { : { : } } } } }, where + | is the electron donor species (Element/Isotope). + | is the electron donor charge. + | is the electron receiver species (Element/Isotope). + | is the electron receiver charge. + | is the tuple containing (initial level, final level). + | is the PEC dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'td': array-like of size (K) with donor temperature in eV, + | 'rate': array-like of size (N, M, K) with PEC in photon.m^3.s^-1. + + :param repository_path: Path to the atomic data repository. + """ + repository_path = repository_path or DEFAULT_REPOSITORY_PATH + + for donor_element, donor_charge_states in rates.items(): + for donor_charge, receiver_elements in donor_charge_states.items(): + for receiver_element, receiver_charge_states in receiver_elements.items(): + for receiver_charge, transitions in receiver_charge_states.items(): + + # sanitise and validate + if not isinstance(donor_element, Element): + raise TypeError('The donor element must be an Element object.') + + if not valid_charge(donor_element, donor_charge + 1): + raise ValueError('Donor charge state is equal to or larger than the number of protons in the element.') + + if not isinstance(receiver_element, Element): + raise TypeError('The receiver element must be an Element object.') + + if not valid_charge(receiver_element, receiver_charge): + raise ValueError('Receiver charge state is larger than the number of protons in the element.') + + rate_path = 'pec/thermal_cx/{0}/{1}/{2}/{3}.json'.format(donor_element.symbol.lower(), donor_charge, + receiver_element.symbol.lower(), receiver_charge) + path = os.path.join(repository_path, rate_path) + + # read in any existing rates + try: + with open(path, 'r') as f: + content = RecursiveDict.from_dict(json.load(f)) + except FileNotFoundError: + content = RecursiveDict() + + # add/replace data for a transition + for transition, data in transitions.items(): + key = encode_transition(transition) + + # sanitise/validate data + ne = np.array(data['ne'], np.float64) + te = np.array(data['te'], np.float64) + td = np.array(data['td'], np.float64) + rate = np.array(data['rate'], np.float64) + + if ne.ndim != 1: + raise ValueError('Electron density array must be a 1D array.') + + if te.ndim != 1: + raise ValueError('Electron temperature array must be a 1D array.') + + if td.ndim != 1: + raise ValueError('Donor temperature array must be a 1D array.') + + if (ne.shape[0], te.shape[0], td.shape[0]) != rate.shape: + raise ValueError('Electron density, electron temperature, donor temperature' + ' and rate data arrays have inconsistent sizes.') + + content[key] = { + 'ne': ne.tolist(), + 'te': te.tolist(), + 'td': td.tolist(), + 'rate': rate.tolist() + } + + # create directory structure if missing + directory = os.path.dirname(path) + os.makedirs(directory, exist_ok=True) + + # write new data + with open(path, 'w') as f: + json.dump(content.freeze(), f, indent=2, sort_keys=True) + + def get_pec_excitation_rate(element, charge, transition, repository_path=None): + """ + Reads the excitation PEC from the repository for the given + element, charge and transition. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. + + :return rate: Excitation PEC dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with excitation PEC in photon.m^3.s^-1. + + """ + return _get_pec_rate('excitation', element, charge, transition, repository_path) def get_pec_recombination_rate(element, charge, transition, repository_path=None): - return _get_pec_rate('recombination', element, charge, transition, repository_path) + """ + Reads the recombination PEC from the repository for the given + element, charge and transition. + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. -def get_pec_thermalcx_rate(element, charge, transition, repository_path=None): - return _get_pec_rate('thermalcx', element, charge, transition, repository_path) + :return rate: Recombination PEC dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with recombination PEC in photon.m^3.s^-1. + + """ + + return _get_pec_rate('recombination', element, charge, transition, repository_path) def _get_pec_rate(cls, element, charge, transition, repository_path=None): @@ -214,3 +360,46 @@ def _get_pec_rate(cls, element, charge, transition, repository_path=None): return d + +def get_pec_thermal_cx_rate(donor_element, donor_charge, receiver_element, receiver_charge, transition, repository_path=None): + """ + Reads the thermal charge exchange PEC from the repository for the given + donor element, donor charge, receiver element, receiver charge and transition. + + :param donor_element: Electron donor plasma species (Element/Isotope). + :param donor_charge: Electron donor charge. + :param receiver_element: Electron receiver plasma species (Element/Isotope). + :param receiver_charge: Electron receiver charge. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. + + :return rate: Thermal CX PEC dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'td': 1D array of size (K) with donor temperature in eV, + | 'rate': 2D array of size (N, M, K) with thermal CX PEC in photon.m^3.s^-1. + + """ + + repository_path = repository_path or DEFAULT_REPOSITORY_PATH + + rate_path = 'pec/thermal_cx/{0}/{1}/{2}/{3}.json'.format(donor_element.symbol.lower(), donor_charge, + receiver_element.symbol.lower(), receiver_charge) + path = os.path.join(repository_path, rate_path) + try: + with open(path, 'r') as f: + content = json.load(f) + d = content[encode_transition(transition)] + except (FileNotFoundError, KeyError): + raise RuntimeError('Requested thermal charge-exchange PEC (donor={}, donor charge={}, receiver={}, receiver charge={})' + ' is not available.' + ''.format(donor_element.symbol, donor_charge, receiver_element.symbol, receiver_charge)) + + # convert to numpy arrays + d['ne'] = np.array(d['ne'], np.float64) + d['te'] = np.array(d['te'], np.float64) + d['td'] = np.array(d['td'], np.float64) + d['rate'] = np.array(d['rate'], np.float64) + + return d diff --git a/cherab/openadas/repository/radiated_power.py b/cherab/openadas/repository/radiated_power.py index a6a4b390..7b908cfc 100644 --- a/cherab/openadas/repository/radiated_power.py +++ b/cherab/openadas/repository/radiated_power.py @@ -1,7 +1,7 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -29,13 +29,21 @@ def add_line_power_rate(species, charge, rate, repository_path=None): """ - Adds a single LineRadiationPower rate to the repository. + Adds a single line radiated power rate to the repository. If adding multiple rates, consider using the update_line_power_rates() function instead. The update function avoids repeatedly opening and closing the rate files. - :param repository_path: + :param species: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param rate: Line radiated power rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with line radiated power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ update_line_power_rates({ @@ -47,13 +55,22 @@ def add_line_power_rate(species, charge, rate, repository_path=None): def update_line_power_rates(rates, repository_path=None): """ - Update the repository of LineRadiationPower rates. - - LineRadiationPower rate file structure - + Update the files for the line radiated power rates: /radiated_power/line/.json + in the atomic data repository. File contains multiple rates, indexed by the ion's charge state. + + :param rates: Dictionary in the form {: {: }}, where + + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the line radiated rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with line radiated power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -71,13 +88,21 @@ def update_line_power_rates(rates, repository_path=None): def add_continuum_power_rate(species, charge, rate, repository_path=None): """ - Adds a single ContinuumPower rate to the repository. + Adds a single continuum power rate to the repository. If adding multiple rates, consider using the update_continuum_power_rates() function instead. The update function avoids repeatedly opening and closing the rate files. - :param repository_path: + :param species: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param rate: Continuum power rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with continuum power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ update_line_power_rates({ @@ -89,13 +114,22 @@ def add_continuum_power_rate(species, charge, rate, repository_path=None): def update_continuum_power_rates(rates, repository_path=None): """ - Update the repository of ContinuumPower rates. - - ContinuumPower rate file structure - + Update the files for the continuum power rates: /radiated_power/continuum/.json + in the atomic data repository. File contains multiple rates, indexed by ion's charge state. + + :param rates: Dictionary in the form {: {: }}, where + + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the continuum power rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with continuum power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -113,13 +147,22 @@ def update_continuum_power_rates(rates, repository_path=None): def add_cx_power_rate(species, charge, rate, repository_path=None): """ - Adds a single CXRadiationPower rate to the repository. + Adds a single CX radiation power rate to the repository + (charge exchage with neutral hydrogen). If adding multiple rates, consider using the update_cx_power_rates() function instead. The update function avoids repeatedly opening and closing the rate files. - :param repository_path: + :param species: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param rate: CX power rate dictionary containing the following entries: + + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with CX power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ update_line_power_rates({ @@ -131,13 +174,23 @@ def add_cx_power_rate(species, charge, rate, repository_path=None): def update_cx_power_rates(rates, repository_path=None): """ - Update the repository of CXRadiationPower rates. - - CXRadiationPower rate file structure - + Update the files for the CX radiation power rates + (charge exchage with neutral hydrogen): /radiated_power/cx/.json + in the atomic data repository. File contains multiple rates, indexed by ion's charge state. + + :param rates: Dictionary in the form {: {: }}, where + + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the thermal CX power rate dictionary containing the following entries: + | 'ne': array-like of size (N) with electron density in m^-3, + | 'te': array-like of size (M) with electron temperature in eV, + | 'rate': array-like of size (N, M) with thermal CX power rate in W.m^3. + + :param repository_path: Path to the atomic data repository. """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -199,6 +252,21 @@ def _update_and_write_adf11(species, rate_data, path): def get_line_radiated_power_rate(element, charge, repository_path=None): + """ + Reads the line radiated power rate for the given species and charge + from the atomic data repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param repository_path: Path to the atomic data repository. + + :return rate: Line radiated power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with line radiated power rate in W.m^3. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -220,6 +288,21 @@ def get_line_radiated_power_rate(element, charge, repository_path=None): def get_continuum_radiated_power_rate(element, charge, repository_path=None): + """ + Reads the continuum power rate for the given species and charge + from the atomic data repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param repository_path: Path to the atomic data repository. + + :return rate: Continuum power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with continuum power rate in W.m^3. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -241,6 +324,21 @@ def get_continuum_radiated_power_rate(element, charge, repository_path=None): def get_cx_radiated_power_rate(element, charge, repository_path=None): + """ + Reads the CX radiation power rate for the given species and charge + from the atomic data repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param repository_path: Path to the atomic data repository. + + :return rate: CX radiation power rate dictionary containing the following entries: + + | 'ne': 1D array of size (N) with electron density in m^-3, + | 'te': 1D array of size (M) with electron temperature in eV, + | 'rate': 2D array of size (N, M) with CX radiation power rate in W.m^3. + + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH diff --git a/cherab/openadas/repository/wavelength.py b/cherab/openadas/repository/wavelength.py index 5981e9e6..83758f51 100644 --- a/cherab/openadas/repository/wavelength.py +++ b/cherab/openadas/repository/wavelength.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2024 Euratom +# Copyright 2016-2024 United Kingdom Atomic Energy Authority +# Copyright 2016-2024 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -35,11 +35,11 @@ def add_wavelength(element, charge, transition, wavelength, repository_path=None function instead. The update function avoid repeatedly opening and closing the rate files. - :param element: - :param charge: - :param transition: - :param wavelength: - :param repository_path: + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param wavelength: Transition's wavelength in nm. + :param repository_path: Path to the atomic data repository. """ update_wavelengths({ @@ -52,6 +52,22 @@ def add_wavelength(element, charge, transition, wavelength, repository_path=None def update_wavelengths(wavelengths, repository_path=None): + """ + Updates the wavelength files `/wavelength//.json` + in atomic data repository. + + File contains multiple rates, indexed by the transitions. + + :param wavelengths: Dictionary in the form: + + | { : { : { : } } }, where + | is the plasma species (Element/Isotope), + | is the charge of the plasma species, + | is the tuple containing (initial level, final level), + | is the transition's wavelength in nm. + + :param repository_path: Path to the atomic data repository. + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH @@ -90,6 +106,16 @@ def update_wavelengths(wavelengths, repository_path=None): def get_wavelength(element, charge, transition, repository_path=None): + """ + Reads the wavelength for the given species, charge and transition from the repository. + + :param element: Plasma species (Element/Isotope). + :param charge: Charge of the plasma species. + :param transition: Tuple containing (initial level, final level). + :param repository_path: Path to the atomic data repository. + + :return wavelength: Wavelength in nm. + """ repository_path = repository_path or DEFAULT_REPOSITORY_PATH path = os.path.join(repository_path, 'wavelength/{}/{}.json'.format(element.symbol.lower(), charge)) diff --git a/cherab/tools/emitters/radiation_function.pyx b/cherab/tools/emitters/radiation_function.pyx index a9b0800d..a4a89f63 100644 --- a/cherab/tools/emitters/radiation_function.pyx +++ b/cherab/tools/emitters/radiation_function.pyx @@ -24,7 +24,7 @@ cimport cython cdef class RadiationFunction(InhomogeneousVolumeEmitter): - """ + r""" A general purpose radiation material. Radiates power over 4 pi according to the supplied 3D radiation diff --git a/cherab/tools/equilibrium/efit.pyx b/cherab/tools/equilibrium/efit.pyx index cb23496a..5e4366a3 100644 --- a/cherab/tools/equilibrium/efit.pyx +++ b/cherab/tools/equilibrium/efit.pyx @@ -36,7 +36,7 @@ from cherab.core.math cimport IsoMapper2D, AxisymmetricMapper, VectorAxisymmetri from cherab.core.math cimport ClampOutput2D cdef class EFITEquilibrium: - """ + r""" An object representing an EFIT equilibrium time-slice. EFIT is a code commonly used throughout the Fusion research community @@ -278,7 +278,7 @@ cdef class EFITEquilibrium: return AxisymmetricMapper(self.map2d(profile, value_outside_lcfs)) def map_vector2d(self, object toroidal, object poloidal, object normal, Vector3D value_outside_lcfs=None): - """ + r""" Maps velocity components in flux coordinates onto flux surfaces in the r-z plane. It is often convenient to express the plasma velocity components in flux coordinates, @@ -344,7 +344,7 @@ cdef class EFITEquilibrium: return VectorBlend2D(value_outside_lcfs, v, self.inside_lcfs) def map_vector3d(self, object toroidal, object poloidal, object normal, Vector3D value_outside_lcfs=None): - """ + r""" Maps velocity components in flux coordinates onto flux surfaces in 3D space. It is often convenient to express the plasma velocity components in flux coordinates, diff --git a/cherab/tools/inversions/lstsq.py b/cherab/tools/inversions/lstsq.py index 204d1090..e352c68e 100644 --- a/cherab/tools/inversions/lstsq.py +++ b/cherab/tools/inversions/lstsq.py @@ -21,7 +21,7 @@ def invert_regularised_lstsq(w_matrix, b_vector, alpha=0.01, tikhonov_matrix=None): - """ + r""" Solves :math:`\mathbf{b} = \mathbf{W} \mathbf{x}` for the vector :math:`\mathbf{x}`, using Tikhonov regulariastion. diff --git a/cherab/tools/inversions/nnls.py b/cherab/tools/inversions/nnls.py index 76f8149a..34779f71 100644 --- a/cherab/tools/inversions/nnls.py +++ b/cherab/tools/inversions/nnls.py @@ -21,14 +21,17 @@ import scipy -def invert_regularised_nnls(w_matrix, b_vector, alpha=0.01, tikhonov_matrix=None): - """ +def invert_regularised_nnls(w_matrix, b_vector, alpha=0.01, tikhonov_matrix=None, **kwargs): + r""" Solves :math:`\mathbf{b} = \mathbf{W} \mathbf{x}` for the vector :math:`\mathbf{x}`, using Tikhonov regulariastion. This is a thin wrapper around scipy.optimize.nnls, which modifies the arguments to include the supplied Tikhonov regularisation matrix. + The values of w_matrix, b_vector and alpha * tikhonov_matrix are notmalised + by max(b_vector) before passing them to scipy.optimize.nnls(). + :param np.ndarray w_matrix: The sensitivity matrix describing the coupling between the detectors and the voxels. Must be an array with shape :math:`(N_d, N_s)`. :param np.ndarray b_vector: The measured power/radiance vector with shape :math:`(N_d)`. @@ -36,6 +39,7 @@ def invert_regularised_nnls(w_matrix, b_vector, alpha=0.01, tikhonov_matrix=None the regularisation strength of the tikhonov matrix. :param np.ndarray tikhonov_matrix: The tikhonov regularisation matrix operator, an array with shape :math:`(N_s, N_s)`. If None, the identity matrix is used. + :param \**kwargs: Keyword arguments passed to scipy.optimize.nnls. :return: (x, norm), the solution vector and the residual norm. .. code-block:: pycon @@ -60,6 +64,9 @@ def invert_regularised_nnls(w_matrix, b_vector, alpha=0.01, tikhonov_matrix=None d_vector = np.zeros(m+n) d_vector[0:m] = b_vector[:] - x_vector, rnorm = scipy.optimize.nnls(c_matrix, d_vector) + # Normalise c_matrix and d_vector to avoid possible issues with the nnls termination criteria. + vmax = d_vector.max() + + x_vector, rnorm = scipy.optimize.nnls(c_matrix / vmax, d_vector / vmax, **kwargs) - return x_vector, rnorm + return x_vector, rnorm * vmax diff --git a/cherab/tools/inversions/opencl/opencl_utils.py b/cherab/tools/inversions/opencl/opencl_utils.py index 51747a93..cefb78ba 100644 --- a/cherab/tools/inversions/opencl/opencl_utils.py +++ b/cherab/tools/inversions/opencl/opencl_utils.py @@ -75,7 +75,7 @@ def get_flops(device, verbose=False): elif "amd" in vendor or "advanced" in vendor: try: ww = device.get_info(cl.device_info.WAVEFRONT_WIDTH_AMD) - except: + except AttributeError: ww = 64 gflops = comp_units * ww * 2 * gpu_clock / 1000. @@ -85,6 +85,10 @@ def get_flops(device, verbose=False): elif "arm" in vendor: gflops = comp_units * 2 * 16 * gpu_clock / 1000. + elif "apple" in vendor: + alu_lanes = 128 + gflops = comp_units * 2 * alu_lanes * gpu_clock / 1000. + else: warnings.warn('Unsupported device vendor: {}. Unable to estimate theoretical peak performance.'.format(vendor)) return 0 diff --git a/cherab/tools/inversions/sart.pyx b/cherab/tools/inversions/sart.pyx index 40c19bfd..12d28769 100644 --- a/cherab/tools/inversions/sart.pyx +++ b/cherab/tools/inversions/sart.pyx @@ -25,7 +25,7 @@ cimport cython @cython.boundscheck(False) cpdef invert_sart(geometry_matrix, measurement_vector, object initial_guess=None, int max_iterations=250, double relaxation=1.0, double conv_tol=1.0E-4): - """ + r""" Performs a SART inversion on the specified measurement vector. This function implements the Simultaneous Algebraic Reconstruction Technique (SART), as published in @@ -161,7 +161,7 @@ cpdef invert_sart(geometry_matrix, measurement_vector, object initial_guess=None cpdef invert_constrained_sart(geometry_matrix, laplacian_matrix, measurement_vector, object initial_guess=None, int max_iterations=250, double relaxation=1.0, double beta_laplace=0.01, double conv_tol=1.0E-4): - """ + r""" Performs a constrained SART inversion on the specified measurement vector. diff --git a/cherab/tools/inversions/voxels.pyx b/cherab/tools/inversions/voxels.pyx index 6baaa821..c682e01a 100644 --- a/cherab/tools/inversions/voxels.pyx +++ b/cherab/tools/inversions/voxels.pyx @@ -682,7 +682,7 @@ class ToroidalVoxelGrid(VoxelCollection): patches = [] for voxel in self: - polygon = Polygon([(v.x, v.y) for v in voxel.vertices], True) + polygon = Polygon([(v.x, v.y) for v in voxel.vertices], closed=True) patches.append(polygon) p = PatchCollection(patches, cmap=cmap) diff --git a/cherab/tools/observers/bolometry.py b/cherab/tools/observers/bolometry.py index dc149b29..20179796 100644 --- a/cherab/tools/observers/bolometry.py +++ b/cherab/tools/observers/bolometry.py @@ -943,7 +943,7 @@ def calculate_sensitivity(self, voxel_collection, ray_count=None): raise TypeError("voxel_collection must be of type VoxelCollection") pipeline_class = self._SPECTRAL_PIPELINES[self._units] - pipeline = pipeline_class(display_progress=False) + pipeline = pipeline_class() voxel_collection.set_active("all") diff --git a/cherab/tools/raytransfer/emitters.pyx b/cherab/tools/raytransfer/emitters.pyx index 632c4a52..c0883baa 100644 --- a/cherab/tools/raytransfer/emitters.pyx +++ b/cherab/tools/raytransfer/emitters.pyx @@ -71,7 +71,7 @@ cdef class RayTransferIntegrator(VolumeIntegrator): cdef class CylindricalRayTransferIntegrator(RayTransferIntegrator): - """ + r""" Calculates the distances traveled by the ray through the voxels defined on a regular grid in cylindrical coordinate system: :math:`(R, \phi, Z)`. This integrator is used with the `CylindricalRayTransferEmitter` material class to calculate ray transfer matrices @@ -338,7 +338,7 @@ cdef class RayTransferEmitter(InhomogeneousVolumeEmitter): cdef class CylindricalRayTransferEmitter(RayTransferEmitter): - """ + r""" A unit emitter defined on a regular 3D :math:`(R, \phi, Z)` grid, which can be used to calculate ray transfer matrices (geometry matrices) for a single value of wavelength. diff --git a/cherab/tools/raytransfer/pipelines.py b/cherab/tools/raytransfer/pipelines.py index b94223ea..e051e3f7 100644 --- a/cherab/tools/raytransfer/pipelines.py +++ b/cherab/tools/raytransfer/pipelines.py @@ -34,24 +34,68 @@ from raysect.optical.observer.base import Pipeline0D, Pipeline1D, Pipeline2D, PixelProcessor -class RayTransferPipeline0D(Pipeline0D): +class RayTransferPipelineBase(): + + def __init__(self, name=None, kind='power'): + + self.name = name + self._matrix = None + self._samples = 0 + self._bins = 0 + self.kind = kind + + @property + def kind(self): + """ + The kind of the pipeline. Can be 'power' or 'radiance'. + In the case of 'power', the resulting matrix is multiplied by the sensitivity + of the detector, and the units of the matrix are [m^3 sr], which gives the units + of power [W] for the product of the ray transfer matrix and the emission profile. + In case of 'radiance', the sensitivity is not taken into account and + the matrix is calculated in [m], which gives the units of radiance [W m^-2 sr^-1] + for the product of the ray transfer matrix and the emission profile. + """ + return self._kind + + @kind.setter + def kind(self, value): + _kind = value.lower() + if _kind in ('power', 'radiance'): + self._kind = _kind + else: + raise ValueError("The kind property must be 'power' or 'radiance'.") + + @property + def matrix(self): + return self._matrix + + +class RayTransferPipeline0D(Pipeline0D, RayTransferPipelineBase): """ Simple 0D pipeline for ray transfer matrix (geometry matrix) calculation. + :param str name: The name of the pipeline. Default is 'RayTransferPipeline0D'. + :param str kind: The kind of the pipeline. Can be 'power' (default) or 'radiance'. + In the case of 'power', the resulting matrix is multiplied by the sensitivity + of the detector, and the units of the matrix are [m^3 sr], which gives the units + of power [W] for the product of the ray transfer matrix and the emission profile. + In case of 'radiance', the sensitivity is not taken into account and + the matrix is calculated in [m], which gives the units of radiance [W m^-2 sr^-1] + for the product of the ray transfer matrix and the emission profile. + Note that if the sensitivity of the detector is 1 (e.g. `PinholeCamera`, `VectorCamera`), + the 'power' and 'radiance' give the same results. + :ivar np.ndarray matrix: Ray transfer matrix, a 1D array of size :math:`N_{bin}`. .. code-block:: pycon >>> from cherab.tools.raytransfer import RayTransferPipeline0D - >>> pipeline = RayTransferPipeline0D() + >>> pipeline = RayTransferPipeline0D(kind='radiance') """ - def __init__(self, name=None): + def __init__(self, name='RayTransferPipeline0D', kind='power'): - self.name = name or 'RayTransferPipeline0D' - self._matrix = None - self._samples = 0 - self._bins = 0 + RayTransferPipelineBase.__init__(self, name, kind) def initialise(self, min_wavelength, max_wavelength, spectral_bins, spectral_slices, quiet): self._samples = 0 @@ -59,7 +103,10 @@ def initialise(self, min_wavelength, max_wavelength, spectral_bins, spectral_sli self._matrix = np.zeros(spectral_bins) def pixel_processor(self, slice_id): - return RayTransferPixelProcessor(self._bins) + if self._kind == 'power': + return PowerRayTransferPixelProcessor(self._bins) + else: + return RadianceRayTransferPixelProcessor(self._bins) def update(self, slice_id, packed_result, pixel_samples): self._samples += pixel_samples @@ -68,30 +115,34 @@ def update(self, slice_id, packed_result, pixel_samples): def finalise(self): self._matrix /= self._samples - @property - def matrix(self): - return self._matrix - -class RayTransferPipeline1D(Pipeline1D): +class RayTransferPipeline1D(Pipeline1D, RayTransferPipelineBase): """ Simple 1D pipeline for ray transfer matrix (geometry matrix) calculation. + :param str name: The name of the pipeline. Default is 'RayTransferPipeline0D'. + :param str kind: The kind of the pipeline. Can be 'power' (default) or 'radiance'. + In the case of 'power', the resulting matrix is multiplied by the sensitivity + of the detector, and the units of the matrix are [m^3 sr], which gives the units + of power [W] for the product of the ray transfer matrix and the emission profile. + In case of 'radiance', the sensitivity is not taken into account and + the matrix is calculated in [m], which gives the units of radiance [W m^-2 sr^-1] + for the product of the ray transfer matrix and the emission profile. + Note that if the sensitivity of the detector is 1 (e.g. `PinholeCamera`, `VectorCamera`), + the 'power' and 'radiance' give the same results. + :ivar np.ndarray matrix: Ray transfer matrix, a 2D array of shape :math:`(N_{pixel}, N_{bin})`. .. code-block:: pycon >>> from cherab.tools.raytransfer import RayTransferPipeline1D - >>> pipeline = RayTransferPipeline1D() + >>> pipeline = RayTransferPipeline1D(kind='radiance') """ - def __init__(self, name=None): + def __init__(self, name='RayTransferPipeline1D', kind='power'): - self.name = name or 'RayTransferPipeline1D' - self._matrix = None + RayTransferPipelineBase.__init__(self, name, kind) self._pixels = None - self._samples = 0 - self._bins = 0 def initialise(self, pixels, pixel_samples, min_wavelength, max_wavelength, spectral_bins, spectral_slices, quiet): self._pixels = pixels @@ -100,7 +151,10 @@ def initialise(self, pixels, pixel_samples, min_wavelength, max_wavelength, spec self._matrix = np.zeros((pixels, spectral_bins)) def pixel_processor(self, pixel, slice_id): - return RayTransferPixelProcessor(self._bins) + if self._kind == 'power': + return PowerRayTransferPixelProcessor(self._bins) + else: + return RadianceRayTransferPixelProcessor(self._bins) def update(self, pixel, slice_id, packed_result): self._matrix[pixel] = packed_result[0] / self._samples @@ -108,30 +162,34 @@ def update(self, pixel, slice_id, packed_result): def finalise(self): pass - @property - def matrix(self): - return self._matrix - -class RayTransferPipeline2D(Pipeline2D): +class RayTransferPipeline2D(Pipeline2D, RayTransferPipelineBase): """ Simple 2D pipeline for ray transfer matrix (geometry matrix) calculation. + :param str name: The name of the pipeline. Default is 'RayTransferPipeline0D'. + :param str kind: The kind of the pipeline. Can be 'power' (default) or 'radiance'. + In the case of 'power', the resulting matrix is multiplied by the sensitivity + of the detector, and the units of the matrix are [m^3 sr], which gives the units + of power [W] for the product of the ray transfer matrix and the emission profile. + In case of 'radiance', the sensitivity is not taken into account and + the matrix is calculated in [m], which gives the units of radiance [W m^-2 sr^-1] + for the product of the ray transfer matrix and the emission profile. + Note that if the sensitivity of the detector is 1 (e.g. `PinholeCamera`, `VectorCamera`), + the 'power' and 'radiance' give the same results. + :ivar np.ndarray matrix: Ray transfer matrix, a 3D array of shape :math:`(N_x, N_y, N_{bin})`. .. code-block:: pycon >>> from cherab.tools.raytransfer import RayTransferPipeline2D - >>> pipeline = RayTransferPipeline2D() + >>> pipeline = RayTransferPipeline2D(kind='radiance') """ - def __init__(self, name=None): + def __init__(self, name='RayTransferPipeline2D', kind='power'): - self.name = name or 'RayTransferPipeline2D' - self._matrix = None + RayTransferPipelineBase.__init__(self, name, kind) self._pixels = None - self._samples = 0 - self._bins = 0 def initialise(self, pixels, pixel_samples, min_wavelength, max_wavelength, spectral_bins, spectral_slices, quiet): self._pixels = pixels @@ -140,7 +198,10 @@ def initialise(self, pixels, pixel_samples, min_wavelength, max_wavelength, spec self._matrix = np.zeros((pixels[0], pixels[1], spectral_bins)) def pixel_processor(self, x, y, slice_id): - return RayTransferPixelProcessor(self._bins) + if self._kind == 'power': + return PowerRayTransferPixelProcessor(self._bins) + else: + return RadianceRayTransferPixelProcessor(self._bins) def update(self, x, y, slice_id, packed_result): self._matrix[x, y] = packed_result[0] / self._samples @@ -148,21 +209,32 @@ def update(self, x, y, slice_id, packed_result): def finalise(self): pass - @property - def matrix(self): - return self._matrix - -class RayTransferPixelProcessor(PixelProcessor): +class RayTransferPixelProcessorBase(PixelProcessor): """ - PixelProcessor that stores ray transfer matrix for each pixel. + Base class for PixelProcessor that stores ray transfer matrix for each pixel. """ def __init__(self, bins): self._matrix = np.zeros(bins) - def add_sample(self, spectrum, sensitivity): - self._matrix += spectrum.samples * sensitivity - def pack_results(self): return (self._matrix, 0) + + +class RadianceRayTransferPixelProcessor(RayTransferPixelProcessorBase): + """ + PixelProcessor that stores ray transfer matrix in the units of [m] for each pixel. + """ + + def add_sample(self, spectrum, sensitivity): + self._matrix += spectrum.samples + + +class PowerRayTransferPixelProcessor(RayTransferPixelProcessorBase): + """ + PixelProcessor that stores ray transfer matrix in the units of [m^3 sr] for each pixel. + """ + + def add_sample(self, spectrum, sensitivity): + self._matrix += spectrum.samples * sensitivity diff --git a/cherab/tools/raytransfer/raytransfer.py b/cherab/tools/raytransfer/raytransfer.py index edea5ebc..67ba1b00 100644 --- a/cherab/tools/raytransfer/raytransfer.py +++ b/cherab/tools/raytransfer/raytransfer.py @@ -127,7 +127,7 @@ def invert_voxel_map(self): class RayTransferCylinder(RayTransferObject): - """ + r""" Ray transfer object for cylindrical emitter defined on a regular 3D :math:`(R, \phi, Z)` grid. This emitter is periodic in :math:`\phi` direction. The base of the cylinder is located at `Z = 0` plane. Use `transform` diff --git a/cherab/tools/raytransfer/roughconductor.pyx b/cherab/tools/raytransfer/roughconductor.pyx index 04edf6f2..faef9cf1 100644 --- a/cherab/tools/raytransfer/roughconductor.pyx +++ b/cherab/tools/raytransfer/roughconductor.pyx @@ -18,7 +18,7 @@ # under the Licence. # -from raysect.optical cimport Point3D, Vector3D, AffineMatrix3D, World, Ray, Spectrum, new_vector3d +from raysect.optical cimport Point3D, Vector3D, AffineMatrix3D, World, Ray, Spectrum, new_vector3d, Intersection from libc.math cimport M_PI, sqrt from raysect.optical.material cimport RoughConductor cimport cython @@ -34,7 +34,8 @@ cdef class RToptimisedRoughConductor(RoughConductor): @cython.cdivision(True) cpdef Spectrum evaluate_shading(self, World world, Ray ray, Vector3D s_incoming, Vector3D s_outgoing, Point3D w_reflection_origin, Point3D w_transmission_origin, bint back_face, - AffineMatrix3D world_to_surface, AffineMatrix3D surface_to_world): + AffineMatrix3D world_to_surface, AffineMatrix3D surface_to_world, + Intersection intersection): cdef: double n, k diff --git a/cherab/tools/tests/test_raytransfer.py b/cherab/tools/tests/test_raytransfer.py index 38a5cdcf..76ecd4d4 100644 --- a/cherab/tools/tests/test_raytransfer.py +++ b/cherab/tools/tests/test_raytransfer.py @@ -18,9 +18,10 @@ import unittest import numpy as np -from raysect.optical import World, Ray, Point3D, Point2D, Vector3D, NumericalIntegrator +from raysect.optical import World, Ray, Point3D, Point2D, Vector3D, NumericalIntegrator, Spectrum from raysect.primitive import Box, Cylinder, Subtract from cherab.tools.raytransfer import RayTransferBox, RayTransferCylinder, CartesianRayTransferEmitter, CylindricalRayTransferEmitter +from cherab.tools.raytransfer import RayTransferPipeline0D, RayTransferPipeline1D, RayTransferPipeline2D from cherab.tools.inversions import ToroidalVoxelGrid @@ -239,3 +240,158 @@ def test_evaluate_function_3d(self): spectrum_test = np.zeros(12) spectrum_test[2] = spectrum_test[9] = np.sqrt(2.) self.assertTrue(np.allclose(spectrum_test, spectrum.samples, atol=0.001)) + + +class TestRayTransferPipeline0D(unittest.TestCase): + """ + Test cases for RayTransferPipeline0D class. + """ + + def test_initialise(self): + """ + Test initialise method. + """ + nbins = 10 + pipeline = RayTransferPipeline0D('test_pipeline_0D', kind='power') + pipeline.initialise(0, 0, nbins, 0, 0) + + self.assertTrue(pipeline.matrix.shape == (nbins,)) + self.assertTrue(pipeline.name == 'test_pipeline_0D') + self.assertTrue(pipeline.kind == 'power') + + self.assertRaises(ValueError, RayTransferPipeline0D, 'test_pipeline_0D', 'blah') + + def test_kind(self): + """ + Test if the 'kind' attribute works properly. + """ + nbins = 10 + sensitivity = 2. + spectral_value = 1. + spectrum = Spectrum(1., 2., nbins) + spectrum.samples[:] = spectral_value + + pipeline = RayTransferPipeline0D('test_pipeline_0D', kind='power') + pipeline.initialise(0, 0, nbins, 0, 0) + + pixel_processor = pipeline.pixel_processor(0) + + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # multiplied by sensitivity + self.assertTrue(np.all(matrix == sensitivity * spectral_value)) + + pipeline.kind = 'radiance' + pixel_processor = pipeline.pixel_processor(0) + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # not multiplied by sensitivity + self.assertTrue(np.all(matrix == spectral_value)) + + +class TestRayTransferPipeline1D(unittest.TestCase): + """ + Test cases for RayTransferPipeline1D class. + """ + + def test_initialise(self): + """ + Test initialise method. + """ + nbins = 10 + pixels = 20 + samples = 1 + pipeline = RayTransferPipeline1D('test_pipeline_1D', kind='radiance') + pipeline.initialise(pixels, samples, 0, 0, nbins, 1, 0) + + self.assertTrue(pipeline.matrix.shape == (pixels, nbins)) + self.assertTrue(pipeline.name == 'test_pipeline_1D') + self.assertTrue(pipeline.kind == 'radiance') + self.assertTrue(pipeline._samples == samples) + + self.assertRaises(ValueError, RayTransferPipeline1D, 'test_pipeline_1D', 'blah') + + def test_kind(self): + """ + Test if the 'kind' attribute works properly. + """ + nbins = 10 + pixels = 20 + samples = 1 + sensitivity = 2. + spectral_value = 1. + spectrum = Spectrum(1., 2., nbins) + spectrum.samples[:] = spectral_value + + pipeline = RayTransferPipeline1D('test_pipeline_1D', kind='power') + pipeline.initialise(pixels, samples, 0, 0, nbins, 1, 0) + + pixel_processor = pipeline.pixel_processor(0, 0) + + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # multiplied by sensitivity + self.assertTrue(np.all(matrix == sensitivity * spectral_value)) + + pipeline.kind = 'radiance' + pixel_processor = pipeline.pixel_processor(0, 0) + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # not multiplied by sensitivity + self.assertTrue(np.all(matrix == spectral_value)) + + +class TestRayTransferPipeline2D(unittest.TestCase): + """ + Test cases for RayTransferPipeline2D class. + """ + + def test_initialise(self): + """ + Test initialise method. + """ + nbins = 10 + pixels = (20, 5) + samples = 1 + pipeline = RayTransferPipeline2D('test_pipeline_2D', kind='radiance') + pipeline.initialise(pixels, samples, 0, 0, nbins, 1, 0) + + self.assertTrue(pipeline.matrix.shape == (pixels[0], pixels[1], nbins)) + self.assertTrue(pipeline.name == 'test_pipeline_2D') + self.assertTrue(pipeline.kind == 'radiance') + self.assertTrue(pipeline._samples == samples) + + self.assertRaises(ValueError, RayTransferPipeline2D, 'test_pipeline_2D', 'blah') + + def test_units(self): + """ + Test if the 'kind' attribute works properly. + """ + nbins = 10 + pixels = (20, 5) + samples = 1 + sensitivity = 2. + spectral_value = 1. + spectrum = Spectrum(1., 2., nbins) + spectrum.samples[:] = spectral_value + + pipeline = RayTransferPipeline2D('test_pipeline_2D', kind='power') + pipeline.initialise(pixels, samples, 0, 0, nbins, 1, 0) + + pixel_processor = pipeline.pixel_processor(0, 0, 0) + + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # multiplied by sensitivity + self.assertTrue(np.all(matrix == sensitivity * spectral_value)) + + pipeline.kind = 'radiance' + pixel_processor = pipeline.pixel_processor(0, 0, 0) + pixel_processor.add_sample(spectrum, sensitivity) + + matrix, _ = pixel_processor.pack_results() # not multiplied by sensitivity + self.assertTrue(np.all(matrix == spectral_value)) + + +if __name__ == '__main__': + unittest.main() diff --git a/demos/emission_models/beam_emission_spectrum.py b/demos/emission_models/beam_emission_spectrum.py index f7675449..cbf5505b 100644 --- a/demos/emission_models/beam_emission_spectrum.py +++ b/demos/emission_models/beam_emission_spectrum.py @@ -69,9 +69,9 @@ beam_energy = 110000 # keV beam_current = 10 # A beam_sigma = 0.05 -beam_divergence = 0.5 +beam_divergence = 1.3 # degree beam_length = 3.0 -beam_temperature = 20 +beam_temperature = 1.0 bes_full_model = BeamEmissionLine(Line(deuterium, 0, (3, 2)), sigma_to_pi=SIGMA_TO_PI, sigma1_to_sigma0=SIGMA1_TO_SIGMA0, diff --git a/demos/emission_models/charge_exchange.py b/demos/emission_models/charge_exchange.py index 11d95386..02d61235 100644 --- a/demos/emission_models/charge_exchange.py +++ b/demos/emission_models/charge_exchange.py @@ -122,7 +122,7 @@ integration_step = 0.0025 beam_transform = translate(-0.5, 0.0, 0) * rotate_basis(Vector3D(1, 0, 0), Vector3D(0, 0, 1)) -beam_energy = 50000 # keV +beam_energy = 50000 # eV beam_full = Beam(parent=world, transform=beam_transform) beam_full.plasma = plasma diff --git a/demos/emission_models/stark_broadening.py b/demos/emission_models/stark_broadening.py index 090e929a..b3f2cf95 100755 --- a/demos/emission_models/stark_broadening.py +++ b/demos/emission_models/stark_broadening.py @@ -1,6 +1,6 @@ -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas # # Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); @@ -34,7 +34,7 @@ # tunables -ion_density = 1e20 +ion_density = 2e20 sigma = 0.25 # setup scenegraph @@ -99,4 +99,5 @@ plt.xlabel('Wavelength (nm)') plt.ylabel('Radiance (W/m^2/str/nm)') plt.title('Observed Spectrum') +plt.yscale('log') plt.show() diff --git a/demos/emission_models/stark_zeeman.py b/demos/emission_models/stark_zeeman.py new file mode 100755 index 00000000..6dac025d --- /dev/null +++ b/demos/emission_models/stark_zeeman.py @@ -0,0 +1,95 @@ +# Copyright 2016-2022 Euratom +# Copyright 2016-2022 United Kingdom Atomic Energy Authority +# Copyright 2016-2022 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +""" +Calculates Balmer-alpha and Paschen-beta Stark-Zeeman spectral lines. + +Compare the figures with Figure 2 in B.A. Lomanowski at al, Nucl. Fusion 55 (2015) 123028, +https://iopscience.iop.org/article/10.1088/0029-5515/55/12/123028 +""" + + +# External imports +import matplotlib.pyplot as plt +from raysect.optical import World, Vector3D, Point3D, Ray + +# Cherab imports +from cherab.core import Line +from cherab.core.atomic.elements import deuterium +from cherab.core.model import ExcitationLine, StarkBroadenedLine +from cherab.openadas import OpenADAS +from cherab.tools.plasmas.slab import build_constant_slab_plasma + + +# tunables +ion_density = 2e20 +sigma = 0.25 + +# setup scenegraph +world = World() + +# create atomic data source +adas = OpenADAS(permit_extrapolation=True) + +# setup the Balmer and Paschen lines +balmer_alpha = Line(deuterium, 0, (3, 2)) +paschen_beta = Line(deuterium, 0, (5, 3)) + +# setup plasma for Balmer-alpha line +plasma_species = [(deuterium, 0, 1.e20, 0.3, Vector3D(0, 0, 0))] +plasma = build_constant_slab_plasma(length=1, width=1, height=1, electron_density=5e20, electron_temperature=1., + plasma_species=plasma_species, b_field=Vector3D(0, 3., 0), parent=world) +plasma.atomic_data = adas + +# add Balmer-alpha line to the plasma +plasma.models = [ExcitationLine(balmer_alpha, lineshape=StarkBroadenedLine)] + +# Ray-trace perpendicular to magnetic field and plot the results +r = Ray(origin=Point3D(-5, 0, 0), direction=Vector3D(1, 0, 0), + min_wavelength=655.9, max_wavelength=656.3, bins=200) +s = r.trace(world) + +plt.figure() +plt.plot(s.wavelengths, s.samples, ls='--', color='C3') +plt.xlabel('Wavelength (nm)') +plt.ylabel('Radiance (W/m^2/str/nm)') +plt.title('Balmer-alpha spectrum, Ne = 5e20 m-3, Te = 1 eV, B = 3 T') + +plasma.parent = None + +# setup plasma for Paschen-beta line +plasma_species = [(deuterium, 0, 1.e20, 0.3, Vector3D(0, 0, 0))] +plasma = build_constant_slab_plasma(length=1, width=1, height=1, electron_density=1e20, electron_temperature=1., + plasma_species=plasma_species, b_field=Vector3D(0, 3., 0), parent=world) +plasma.atomic_data = adas + +# add Paschen-beta line to the plasma +plasma.models = [ExcitationLine(paschen_beta, lineshape=StarkBroadenedLine)] + +# Ray-trace perpendicular to magnetic field and plot the results +r = Ray(origin=Point3D(-5, 0, 0), direction=Vector3D(1, 0, 0), + min_wavelength=1280.3, max_wavelength=1282.6, bins=200) +s = r.trace(world) + +plt.figure() +plt.plot(s.wavelengths, s.samples, ls='--', color='C3') +plt.xlabel('Wavelength (nm)') +plt.ylabel('Radiance (W/m^2/str/nm)') +plt.title('Paschen-beta spectrum, Ne = 1e20 m-3, Te = 1 eV, B = 3 T') + +plt.show() diff --git a/demos/emission_models/thermal_charge_exchange.py b/demos/emission_models/thermal_charge_exchange.py new file mode 100644 index 00000000..42c7b88a --- /dev/null +++ b/demos/emission_models/thermal_charge_exchange.py @@ -0,0 +1,126 @@ + +import matplotlib.pyplot as plt +from scipy.constants import electron_mass, atomic_mass + +from raysect.core import translate, rotate_basis, Point3D, Vector3D +from raysect.primitive import Box +from raysect.optical import World, Ray + +from cherab.core import Plasma, Beam, Maxwellian, Species +from cherab.core.math import ScalarToVectorFunction3D +from cherab.core.atomic import hydrogen, deuterium, carbon, Line +from cherab.core.model import SingleRayAttenuator, BeamCXLine, ThermalCXLine +from cherab.tools.plasmas.slab import NeutralFunction, IonFunction +from cherab.openadas import OpenADAS +from cherab.openadas.install import install_adf15 + + +############### +# Make Plasma # + +width = 1.0 +length = 1.0 +height = 3.0 +peak_density = 5e19 +edge_density = 1e18 +pedestal_top = 1 +neutral_temperature = 0.5 +peak_temperature = 2500 +edge_temperature = 25 +impurities = [(carbon, 6, 0.005)] + +world = World() +adas = OpenADAS(permit_extrapolation=True, missing_rates_return_null=True) + +plasma = Plasma(parent=world) +plasma.atomic_data = adas + +# plasma slab along x direction +plasma.geometry = Box(Point3D(0, -width / 2, -height / 2), Point3D(length, width / 2, height / 2)) + +species = [] + +# make a non-zero velocity profile for the plasma +vy_profile = IonFunction(1E5, 0, pedestal_top=pedestal_top) +velocity_profile = ScalarToVectorFunction3D(0, vy_profile, 0) + +# define neutral species distribution +h0_density = NeutralFunction(peak_density, 0.1, pedestal_top=pedestal_top) +h0_temperature = neutral_temperature +h0_distribution = Maxwellian(h0_density, h0_temperature, velocity_profile, + hydrogen.atomic_weight * atomic_mass) +species.append(Species(hydrogen, 0, h0_distribution)) + +# define hydrogen ion species distribution +h1_density = IonFunction(peak_density, edge_density, pedestal_top=pedestal_top) +h1_temperature = IonFunction(peak_temperature, edge_temperature, pedestal_top=pedestal_top) +h1_distribution = Maxwellian(h1_density, h1_temperature, velocity_profile, + hydrogen.atomic_weight * atomic_mass) +species.append(Species(hydrogen, 1, h1_distribution)) + +# add impurities +if impurities: + for impurity, ionisation, concentration in impurities: + imp_density = IonFunction(peak_density * concentration, edge_density * concentration, pedestal_top=pedestal_top) + imp_temperature = IonFunction(peak_temperature, edge_temperature, pedestal_top=pedestal_top) + imp_distribution = Maxwellian(imp_density, imp_temperature, velocity_profile, + impurity.atomic_weight * atomic_mass) + species.append(Species(impurity, ionisation, imp_distribution)) + +# define the electron distribution +e_density = IonFunction(peak_density, edge_density, pedestal_top=pedestal_top) +e_temperature = IonFunction(peak_temperature, edge_temperature, pedestal_top=pedestal_top) +e_distribution = Maxwellian(e_density, e_temperature, velocity_profile, electron_mass) + +# define species +plasma.electron_distribution = e_distribution +plasma.composition = species + +# add thermal CX emission model +cVI_5_4 = Line(carbon, 5, (5, 4)) +plasma.models = [ThermalCXLine(cVI_5_4)] + +# trace thermal CX spectrum along y direction +ray = Ray(origin=Point3D(0.4, -3.5, 0), direction=Vector3D(0, 1, 0), + min_wavelength=112.3, max_wavelength=112.7, bins=512) +thermal_cx_spectrum = ray.trace(world) + +########################### +# Inject beam into plasma # + +integration_step = 0.0025 +# injected along x direction +beam_transform = translate(-0.5, 0.0, 0) * rotate_basis(Vector3D(1, 0, 0), Vector3D(0, 0, 1)) +beam_energy = 50000 # eV + +beam = Beam(parent=world, transform=beam_transform) +beam.plasma = plasma +beam.atomic_data = adas +beam.energy = beam_energy +beam.power = 3e6 +beam.element = deuterium +beam.sigma = 0.05 +beam.divergence_x = 0.5 +beam.divergence_y = 0.5 +beam.length = 3.0 +beam.attenuator = SingleRayAttenuator(clamp_to_zero=True) +beam.integrator.step = integration_step +beam.integrator.min_samples = 10 + +# remove thermal CX model and add beam CX model +plasma.models = [] +beam.models = [BeamCXLine(cVI_5_4)] + +# trace the spectrum again +beam_cx_spectrum = ray.trace(world) + +# plot the spectra +plt.figure() +plt.plot(thermal_cx_spectrum.wavelengths, thermal_cx_spectrum.samples, label='thermal CX') +plt.plot(beam_cx_spectrum.wavelengths, beam_cx_spectrum.samples, label='beam CX') +plt.legend() +plt.xlabel('Wavelength (nm)') +plt.ylabel('Radiance (W/m^2/str/nm)') +plt.title('Sampled CX spectra') + +plt.show() diff --git a/demos/observers/bolometry/geometry_matrix_with_raytransfer.py b/demos/observers/bolometry/geometry_matrix_with_raytransfer.py index a9300636..35c525e1 100644 --- a/demos/observers/bolometry/geometry_matrix_with_raytransfer.py +++ b/demos/observers/bolometry/geometry_matrix_with_raytransfer.py @@ -240,7 +240,7 @@ def _point3d_to_rz(point): for camera in cameras: for foil in camera: print("Calculating sensitivity for {}...".format(foil.name)) - foil.pipelines = [RayTransferPipeline0D()] + foil.pipelines = [RayTransferPipeline0D(kind=foil.units)] # All objects in world have wavelength-independent material properties, # so it doesn't matter which wavelength range we use (as long as # max_wavelength - min_wavelength = 1) diff --git a/demos/plasmas/analytic_plasma_function_framework.py b/demos/plasmas/analytic_plasma_function_framework.py new file mode 100644 index 00000000..1459f28e --- /dev/null +++ b/demos/plasmas/analytic_plasma_function_framework.py @@ -0,0 +1,165 @@ + +# Copyright 2016-2018 Euratom +# Copyright 2016-2018 United Kingdom Atomic Energy Authority +# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + + +import numpy as np +import matplotlib.pyplot as plt +from scipy.constants import electron_mass, atomic_mass + +from raysect.core.math.function.float import Arg2D, Exp2D, Sqrt2D +from raysect.primitive import Cylinder +from raysect.optical import World, translate, Point3D, Vector3D, rotate_basis, Spectrum +from raysect.optical.observer import PinholeCamera, PowerPipeline2D + +from cherab.core import Species, Maxwellian, Plasma, Line +from cherab.core.math import sample3d, AxisymmetricMapper +from cherab.core.atomic import deuterium +from cherab.core.model import ExcitationLine +from cherab.openadas import OpenADAS + + +def NeutralFunction(peak_value, sigma, magnetic_axis, lcfs_radius=1): + """A neutral profile that is constant outside the plasma, + then exponentially decays inside the LCFS.""" + raxis = magnetic_axis[0] + zaxis = magnetic_axis[1] + radius_from_axis = Sqrt2D((Arg2D('x') - raxis)**2 + (Arg2D('y') - zaxis)**2) + scale = Exp2D(-((radius_from_axis - lcfs_radius)**2) / (2 * sigma**2)) + inside_lcfs = (radius_from_axis <= lcfs_radius) + # density = peak * scale * inside_lcfs + peak * (inside_lcfs - 1). + # Rearrange so inside_lcfs and scale are only called once each. + density = peak_value * (inside_lcfs * (scale - 1) + 1) + return AxisymmetricMapper(density) + + +def IonFunction(v_core, v_lcfs, magnetic_axis, p=4, q=3, lcfs_radius=1): + """An approximate toroidal plasma profile that follows a double + quadratic between the LCFS and magnetic axis.""" + r_axis = magnetic_axis[0] + z_axis = magnetic_axis[1] + radius_from_axis = Sqrt2D((Arg2D('x') - r_axis)**2 + (Arg2D('y') - z_axis)**2) + density = (v_core - v_lcfs) * (1 - (radius_from_axis / lcfs_radius)**p)**q + v_lcfs + inside_lcfs = (radius_from_axis <= lcfs_radius) + return AxisymmetricMapper(density * inside_lcfs) + + +# tunables +peak_density = 1e19 +peak_temperature = 2500 +magnetic_axis = (2.5, 0) + + +# setup scenegraph +world = World() + + +################### +# plasma creation # + +plasma = Plasma(parent=world) +plasma.atomic_data = OpenADAS(permit_extrapolation=True) +plasma.geometry = Cylinder(3.5, 2.2, transform=translate(0, 0, -1.1)) +plasma.geometry_transform = translate(0, 0, -1.1) + +# No net velocity for any species +zero_velocity = Vector3D(0, 0, 0) + +# define neutral species distribution +d0_density = NeutralFunction(peak_density, 0.1, magnetic_axis) +d0_temperature = 0.5 # constant 0.5eV temperature for all neutrals +d0_distribution = Maxwellian(d0_density, d0_temperature, zero_velocity, + deuterium.atomic_weight * atomic_mass) +d0_species = Species(deuterium, 0, d0_distribution) + +# define deuterium ion species distribution +d1_density = IonFunction(peak_density, 0, magnetic_axis) +d1_temperature = IonFunction(peak_temperature, 0, magnetic_axis) +d1_distribution = Maxwellian(d1_density, d1_temperature, zero_velocity, + deuterium.atomic_weight * atomic_mass) +d1_species = Species(deuterium, 1, d1_distribution) + +# define the electron distribution +e_density = IonFunction(peak_density, 0, magnetic_axis) +e_temperature = IonFunction(peak_temperature, 0, magnetic_axis) +e_distribution = Maxwellian(e_density, e_temperature, zero_velocity, electron_mass) + +# define species +plasma.b_field = Vector3D(1.0, 1.0, 1.0) +plasma.electron_distribution = e_distribution +plasma.composition = [d0_species, d1_species] + +# Add a balmer alpha line for visualisation purposes +d_alpha_excit = ExcitationLine(Line(deuterium, 0, (3, 2))) +plasma.models = [d_alpha_excit] + + +#################### +# Visualise Plasma # + +# Run some plots to check the distribution functions and emission profile are as expected +r, _, z, t_samples = sample3d(d1_temperature, (0, 4, 200), (0, 0, 1), (-2, 2, 200)) +plt.imshow(np.transpose(np.squeeze(t_samples)), extent=[0, 4, -2, 2]) +plt.colorbar() +plt.axis('equal') +plt.xlabel('r axis') +plt.ylabel('z axis') +plt.title("Ion temperature profile in r-z plane") + +plt.figure() +r, _, z, t_samples = sample3d(d1_temperature, (-4, 4, 400), (-4, 4, 400), (0, 0, 1)) +plt.imshow(np.transpose(np.squeeze(t_samples)), extent=[-4, 4, -4, 4]) +plt.colorbar() +plt.axis('equal') +plt.xlabel('x axis') +plt.ylabel('y axis') +plt.title("Ion temperature profile in x-y plane") + +plt.figure() +r, _, z, t_samples = sample3d(d0_density, (0, 4, 200), (0, 0, 1), (-2, 2, 200)) +plt.imshow(np.transpose(np.squeeze(t_samples)), extent=[0, 4, -2, 2]) +plt.colorbar() +plt.axis('equal') +plt.xlabel('r axis') +plt.ylabel('z axis') +plt.title("Neutral Density profile in r-z plane") + +plt.figure() +xrange = np.linspace(0, 4, 200) +yrange = np.linspace(-2, 2, 200) +d_alpha_rz_intensity = np.zeros((200, 200)) +direction = Vector3D(0, 1, 0) +for i, x in enumerate(xrange): + for j, y in enumerate(yrange): + emission = d_alpha_excit.emission(Point3D(x, 0.0, y), direction, Spectrum(650, 660, 1)) + d_alpha_rz_intensity[j, i] = emission.total() +plt.imshow(d_alpha_rz_intensity, extent=[0, 4, -2, 2], origin='lower') +plt.colorbar() +plt.xlabel('r axis') +plt.ylabel('z axis') +plt.title("D-alpha emission in R-Z") + + +camera = PinholeCamera((256, 256), pipelines=[PowerPipeline2D()], parent=world) +camera.transform = translate(2.5, -4.5, 0)*rotate_basis(Vector3D(0, 1, 0), Vector3D(0, 0, 1)) +camera.pixel_samples = 1 + +plt.ion() +camera.observe() +plt.ioff() +plt.show() diff --git a/demos/ray_transfer/1_ray_transfer_box.py b/demos/ray_transfer/1_ray_transfer_box.py index e4d5e8bb..43461709 100644 --- a/demos/ray_transfer/1_ray_transfer_box.py +++ b/demos/ray_transfer/1_ray_transfer_box.py @@ -36,8 +36,8 @@ from raysect.optical.observer import PinholeCamera, FullFrameSampler2D # RayTransferPipeline2D is optimised for calculation of ray transfer matrices. -# It's also possible to use SpectralRadiancePipeline2D but for the matrices with >1000 elements -# the performance will be lower. +# It's also possible to use SpectralRadiancePipeline2D or SpectralPowerPipeline2D but +# for the matrices with >1000 elements the performance will be lower. from cherab.tools.raytransfer import RayTransferPipeline2D, RayTransferBox # Here we use special materials optimised for calculation of ray transfer matrices. @@ -66,6 +66,9 @@ rtb.step = 0.2 # creating ray transfer pipeline +# Be careful when setting the 'kind' attribute of the pipeline to 'power' or 'radiance'. +# In the case of 'power', the matrix [m] is multiplied by the detector's sensitivity [m^2 sr]. +# For the PinholeCamera this does not matter, because its pixel sensitivity is 1. pipeline = RayTransferPipeline2D() # setting up the camera diff --git a/docs/source/atomic/atomic_data.rst b/docs/source/atomic/atomic_data.rst index 650d89b4..dcff3270 100644 --- a/docs/source/atomic/atomic_data.rst +++ b/docs/source/atomic/atomic_data.rst @@ -7,3 +7,7 @@ Atomic Data emission_lines rate_coefficients gaunt_factors + atomic_data_interface + data_interpolators + repository + openadas diff --git a/docs/source/atomic/atomic_data_interface.rst b/docs/source/atomic/atomic_data_interface.rst new file mode 100644 index 00000000..0d54ff67 --- /dev/null +++ b/docs/source/atomic/atomic_data_interface.rst @@ -0,0 +1,19 @@ + +Atomic Data Interface +===================== + +Abstract (interface) class +-------------------------- + +Abstract atomic data interface. + +.. autoclass:: cherab.core.atomic.interface.AtomicData + :members: + +OpenADAS atomic data source +--------------------------- + +Interface to local atomic data repository. + +.. autoclass:: cherab.openadas.openadas.OpenADAS + :members: diff --git a/docs/source/atomic/data_interpolators.rst b/docs/source/atomic/data_interpolators.rst new file mode 100644 index 00000000..56fa0327 --- /dev/null +++ b/docs/source/atomic/data_interpolators.rst @@ -0,0 +1,89 @@ +Atomic data interpolators +========================= + +The following classes interpolate atomic data defined on a numerical grid. + + +Atomic Processes +^^^^^^^^^^^^^^^^ + +.. autoclass:: cherab.openadas.rates.atomic.IonisationRate + :members: + +.. autoclass:: cherab.openadas.rates.atomic.NullIonisationRate + :members: + +.. autoclass:: cherab.openadas.rates.atomic.RecombinationRate + :members: + +.. autoclass:: cherab.openadas.rates.atomic.NullRecombinationRate + :members: + +.. autoclass:: cherab.openadas.rates.atomic.ThermalCXRate + :members: + +.. autoclass:: cherab.openadas.rates.atomic.NullThermalCXRate + :members: + +Photon Emissivity Coefficients +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: cherab.openadas.rates.pec.ImpactExcitationPEC + :members: + +.. autoclass:: cherab.openadas.rates.pec.NullImpactExcitationPEC + :members: + +.. autoclass:: cherab.openadas.rates.pec.RecombinationPEC + :members: + +.. autoclass:: cherab.openadas.rates.pec.NullRecombinationPEC + :members: + +Beam-Plasma Interaction Rates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: cherab.openadas.rates.cx.BeamCXPEC + :members: + +.. autoclass:: cherab.openadas.rates.cx.NullBeamCXPEC + :members: + +.. autoclass:: cherab.openadas.rates.beam.BeamStoppingRate + :members: + +.. autoclass:: cherab.openadas.rates.beam.NullBeamStoppingRate + :members: + +.. autoclass:: cherab.openadas.rates.beam.BeamPopulationRate + :members: + +.. autoclass:: cherab.openadas.rates.beam.NullBeamPopulationRate + :members: + +.. autoclass:: cherab.openadas.rates.beam.BeamEmissionPEC + :members: + +.. autoclass:: cherab.openadas.rates.beam.NullBeamEmissionPEC + :members: + +Radiated Power +^^^^^^^^^^^^^^ + +.. autoclass:: cherab.openadas.rates.radiated_power.LineRadiationPower + :members: + +.. autoclass:: cherab.openadas.rates.radiated_power.NullLineRadiationPower + :members: + +.. autoclass:: cherab.openadas.rates.radiated_power.ContinuumPower + :members: + +.. autoclass:: cherab.openadas.rates.radiated_power.NullContinuumPower + :members: + +.. autoclass:: cherab.openadas.rates.radiated_power.CXRadiationPower + :members: + +.. autoclass:: cherab.openadas.rates.radiated_power.NullCXRadiationPower + :members: diff --git a/docs/source/atomic/openadas.rst b/docs/source/atomic/openadas.rst new file mode 100644 index 00000000..3dde4242 --- /dev/null +++ b/docs/source/atomic/openadas.rst @@ -0,0 +1,29 @@ +Open-ADAS +--------- + +Although a typical Open-ADAS data set is installed to the local atomic data repository +using the `populate()` function, additional atomic data can be installed manually. + +The following functions allow to parse the Open-ADAS files and install the rates of the atomic processes +to the local atomic data repository. + +Parse +^^^^^ + +.. autofunction:: cherab.openadas.parse.adf11.parse_adf11 + +.. autofunction:: cherab.openadas.parse.adf12.parse_adf12 + +.. autofunction:: cherab.openadas.parse.adf15.parse_adf15 + +.. autofunction:: cherab.openadas.parse.adf21.parse_adf21 + +.. autofunction:: cherab.openadas.parse.adf22.parse_adf22bmp + +.. autofunction:: cherab.openadas.parse.adf22.parse_adf22bme + +Install +^^^^^^^ + +.. automodule:: cherab.openadas.install + :members: diff --git a/docs/source/atomic/rate_coefficients.rst b/docs/source/atomic/rate_coefficients.rst index b6547820..82116bf3 100644 --- a/docs/source/atomic/rate_coefficients.rst +++ b/docs/source/atomic/rate_coefficients.rst @@ -15,6 +15,35 @@ provide theoretical equations. Cherab emission models only need to know how to c them after they have been instantiated. +Atomic Processes +^^^^^^^^^^^^^^^^ + +.. autoclass:: cherab.core.atomic.rates.IonisationRate + +.. autoclass:: cherab.core.atomic.rates.RecombinationRate + +.. autoclass:: cherab.core.atomic.rates.ThermalCXRate + +The `IonisationRate`, `RecombinationRate` and `ThermalCXRate` classes all share +the same call signatures. + +.. function:: __call__(density, temperature) + + Returns an effective rate coefficient at the specified plasma conditions. + + This function just wraps the cython evaluate() method. + +.. function:: evaluate(density, temperature) + + an effective recombination rate coefficient at the specified plasma conditions. + + This function needs to be implemented by the atomic data provider. + + :param float density: Electron density in m^-3 + :param float temperature: Electron temperature in eV. + :return: The effective ionisation rate in [m^3.s^-1]. + + Photon Emissivity Coefficients ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -39,8 +68,8 @@ the same call signatures. This function needs to be implemented by the atomic data provider. - :param float temperature: Receiver ion temperature in eV. :param float density: Receiver ion density in m^-3 + :param float temperature: Receiver ion temperature in eV. :return: The effective PEC rate [Wm^3]. Some example code for requesting PEC objects and sampling them with the __call__() diff --git a/docs/source/atomic/repository.rst b/docs/source/atomic/repository.rst new file mode 100644 index 00000000..3a93085e --- /dev/null +++ b/docs/source/atomic/repository.rst @@ -0,0 +1,83 @@ + +Atomic data repository +---------------------- + +The following functions allow to manipulate the local atomic data repository: +add the rates of the atomic processes, update existing ones or get the data +already present in the repository. + +The default repository is created at `~/.cherab/openadas/repository`. +Cherab supports multiple atomic data repositories. The user can configure different +repositories by setting the `repository_path` parameter. +The data in these repositories can be accessed through the `OpenADAS` atomic data provider +by specifying the `data_path` parameter. + +To create the new atomic data repository at the default location and populate it with a typical +set of rates and wavelengths from Open-ADAS, do: + +.. code-block:: pycon + + >>> from cherab.openadas.repository import populate + >>> populate() + +.. autofunction:: cherab.openadas.repository.create.populate + +Wavelength +^^^^^^^^^^ + +.. automodule:: cherab.openadas.repository.wavelength + :members: + +Ionisation +^^^^^^^^^^ + +.. autofunction:: cherab.openadas.repository.atomic.add_ionisation_rate + +.. autofunction:: cherab.openadas.repository.atomic.get_ionisation_rate + +.. autofunction:: cherab.openadas.repository.atomic.update_ionisation_rates + +Recombination +^^^^^^^^^^^^^ + +.. autofunction:: cherab.openadas.repository.atomic.add_recombination_rate + +.. autofunction:: cherab.openadas.repository.atomic.get_recombination_rate + +.. autofunction:: cherab.openadas.repository.atomic.update_recombination_rates + +Thermal Charge Exchange +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: cherab.openadas.repository.atomic.add_thermal_cx_rate + +.. autofunction:: cherab.openadas.repository.atomic.get_thermal_cx_rate + +.. autofunction:: cherab.openadas.repository.atomic.update_thermal_cx_rates + +Photon Emissivity Coefficients +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: cherab.openadas.repository.pec + :members: + +Radiated Power +^^^^^^^^^^^^^^ + +.. automodule:: cherab.openadas.repository.radiated_power + :members: + +Beam +^^^^ + +.. automodule:: cherab.openadas.repository.beam.cx + :members: + +.. automodule:: cherab.openadas.repository.beam.emission + :members: + +.. automodule:: cherab.openadas.repository.beam.population + :members: + +.. automodule:: cherab.openadas.repository.beam.stopping + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 60589037..b2f22c70 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -39,6 +39,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.mathjax', + 'sphinx_tabs.tabs', ] # Add any paths that contain templates here, relative to this directory. @@ -55,16 +56,16 @@ # General information about the project. project = 'Cherab' -copyright = '2022, Cherab Team' +copyright = '2024, Cherab Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.4' +version = '1.5' # The full version, including alpha/beta/rc tags. -release = '1.4.0' +release = '1.5.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -142,6 +143,7 @@ html_context = {'css_files': [ '_static/theme_overrides.css', # override wide tables in RTD theme + '_static/tabs.css', ] } diff --git a/docs/source/demonstrations/active_spectroscopy/BES_camera.png b/docs/source/demonstrations/active_spectroscopy/BES_camera.png index 860e9c2d..7ad3b062 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/BES_camera.png and b/docs/source/demonstrations/active_spectroscopy/BES_camera.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/BES_sightline.png b/docs/source/demonstrations/active_spectroscopy/BES_sightline.png index c15a207f..c71d2f87 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/BES_sightline.png and b/docs/source/demonstrations/active_spectroscopy/BES_sightline.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/BES_spectrum_full.png b/docs/source/demonstrations/active_spectroscopy/BES_spectrum_full.png index 3fa35ddd..57eb5dff 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/BES_spectrum_full.png and b/docs/source/demonstrations/active_spectroscopy/BES_spectrum_full.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/BES_spectrum_zoomed.png b/docs/source/demonstrations/active_spectroscopy/BES_spectrum_zoomed.png index b3b461bb..3c288ee4 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/BES_spectrum_zoomed.png and b/docs/source/demonstrations/active_spectroscopy/BES_spectrum_zoomed.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/CXS_camera.png b/docs/source/demonstrations/active_spectroscopy/CXS_camera.png index c390e0cd..7f8fa5cf 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/CXS_camera.png and b/docs/source/demonstrations/active_spectroscopy/CXS_camera.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/CXS_multi_sightlines.png b/docs/source/demonstrations/active_spectroscopy/CXS_multi_sightlines.png index 4064de2a..f94e95ab 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/CXS_multi_sightlines.png and b/docs/source/demonstrations/active_spectroscopy/CXS_multi_sightlines.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/CXS_spectrum.png b/docs/source/demonstrations/active_spectroscopy/CXS_spectrum.png index db79a9c5..a258c6ed 100644 Binary files a/docs/source/demonstrations/active_spectroscopy/CXS_spectrum.png and b/docs/source/demonstrations/active_spectroscopy/CXS_spectrum.png differ diff --git a/docs/source/demonstrations/active_spectroscopy/beam_bes.rst b/docs/source/demonstrations/active_spectroscopy/beam_bes.rst index a37de688..c91957d9 100644 --- a/docs/source/demonstrations/active_spectroscopy/beam_bes.rst +++ b/docs/source/demonstrations/active_spectroscopy/beam_bes.rst @@ -1,3 +1,5 @@ +:orphan: + .. _beam_bes: diff --git a/docs/source/demonstrations/active_spectroscopy/beam_cxs.rst b/docs/source/demonstrations/active_spectroscopy/beam_cxs.rst index 5952ae3c..8859cad7 100644 --- a/docs/source/demonstrations/active_spectroscopy/beam_cxs.rst +++ b/docs/source/demonstrations/active_spectroscopy/beam_cxs.rst @@ -1,3 +1,5 @@ +:orphan: + .. _beam_cxs: diff --git a/docs/source/demonstrations/atomic_data/beam_plasma_interactions.rst b/docs/source/demonstrations/atomic_data/beam_plasma_interactions.rst index 58760f5d..eba6f3b1 100644 --- a/docs/source/demonstrations/atomic_data/beam_plasma_interactions.rst +++ b/docs/source/demonstrations/atomic_data/beam_plasma_interactions.rst @@ -1,3 +1,5 @@ +:orphan: + .. _beam_plasma_interaction_rates: diff --git a/docs/source/demonstrations/atomic_data/fractional_abundance.rst b/docs/source/demonstrations/atomic_data/fractional_abundance.rst index 00bc95e0..d1a80309 100644 --- a/docs/source/demonstrations/atomic_data/fractional_abundance.rst +++ b/docs/source/demonstrations/atomic_data/fractional_abundance.rst @@ -1,3 +1,5 @@ +:orphan: + .. _fractional_abundances: diff --git a/docs/source/demonstrations/atomic_data/photon_emissivity_coefficients.rst b/docs/source/demonstrations/atomic_data/photon_emissivity_coefficients.rst index 08948319..bfce073e 100644 --- a/docs/source/demonstrations/atomic_data/photon_emissivity_coefficients.rst +++ b/docs/source/demonstrations/atomic_data/photon_emissivity_coefficients.rst @@ -1,3 +1,5 @@ +:orphan: + .. _photon_emissivity_coefficients: diff --git a/docs/source/demonstrations/atomic_data/radiated_powers.rst b/docs/source/demonstrations/atomic_data/radiated_powers.rst index f0ba1249..a002aceb 100644 --- a/docs/source/demonstrations/atomic_data/radiated_powers.rst +++ b/docs/source/demonstrations/atomic_data/radiated_powers.rst @@ -1,3 +1,5 @@ +:orphan: + .. _radiated_powers: diff --git a/docs/source/demonstrations/bolometry/calculate_etendue.rst b/docs/source/demonstrations/bolometry/calculate_etendue.rst index 82f90379..4ab99b86 100644 --- a/docs/source/demonstrations/bolometry/calculate_etendue.rst +++ b/docs/source/demonstrations/bolometry/calculate_etendue.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_etendue: diff --git a/docs/source/demonstrations/bolometry/camera_from_mesh_and_coordinates.rst b/docs/source/demonstrations/bolometry/camera_from_mesh_and_coordinates.rst index 95d54b13..bec12605 100644 --- a/docs/source/demonstrations/bolometry/camera_from_mesh_and_coordinates.rst +++ b/docs/source/demonstrations/bolometry/camera_from_mesh_and_coordinates.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_from_mesh: diff --git a/docs/source/demonstrations/bolometry/camera_from_primitives.rst b/docs/source/demonstrations/bolometry/camera_from_primitives.rst index 8d08146b..1f51713a 100644 --- a/docs/source/demonstrations/bolometry/camera_from_primitives.rst +++ b/docs/source/demonstrations/bolometry/camera_from_primitives.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_from_primitives: diff --git a/docs/source/demonstrations/bolometry/geometry_matrix_from_voxels.rst b/docs/source/demonstrations/bolometry/geometry_matrix_from_voxels.rst index ff0a548d..08b50a94 100644 --- a/docs/source/demonstrations/bolometry/geometry_matrix_from_voxels.rst +++ b/docs/source/demonstrations/bolometry/geometry_matrix_from_voxels.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_geometry_voxels: diff --git a/docs/source/demonstrations/bolometry/geometry_matrix_with_raytransfer.rst b/docs/source/demonstrations/bolometry/geometry_matrix_with_raytransfer.rst index a450b8b8..d5fbd367 100644 --- a/docs/source/demonstrations/bolometry/geometry_matrix_with_raytransfer.rst +++ b/docs/source/demonstrations/bolometry/geometry_matrix_with_raytransfer.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_geometry_raytransfer: diff --git a/docs/source/demonstrations/bolometry/inversion_with_raytransfer.rst b/docs/source/demonstrations/bolometry/inversion_with_raytransfer.rst index 235fc029..c4e538d5 100644 --- a/docs/source/demonstrations/bolometry/inversion_with_raytransfer.rst +++ b/docs/source/demonstrations/bolometry/inversion_with_raytransfer.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_raytransfer_inversion: diff --git a/docs/source/demonstrations/bolometry/inversion_with_voxels.rst b/docs/source/demonstrations/bolometry/inversion_with_voxels.rst index 0aaab42c..68301d57 100644 --- a/docs/source/demonstrations/bolometry/inversion_with_voxels.rst +++ b/docs/source/demonstrations/bolometry/inversion_with_voxels.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_voxel_inversion: diff --git a/docs/source/demonstrations/bolometry/observing_radiation_function.rst b/docs/source/demonstrations/bolometry/observing_radiation_function.rst index aeedac6d..de52a7d3 100644 --- a/docs/source/demonstrations/bolometry/observing_radiation_function.rst +++ b/docs/source/demonstrations/bolometry/observing_radiation_function.rst @@ -1,3 +1,5 @@ +:orphan: + .. _bolometer_observing_radiation: diff --git a/docs/source/demonstrations/demonstrations.rst b/docs/source/demonstrations/demonstrations.rst index 6201aae1..0ee82ab3 100644 --- a/docs/source/demonstrations/demonstrations.rst +++ b/docs/source/demonstrations/demonstrations.rst @@ -1,3 +1,5 @@ +:orphan: + Atomic Data =========== @@ -151,16 +153,21 @@ Passive Spectroscopy - .. image:: ./passive_spectroscopy/multiplet_spectrum.png :height: 150px :width: 150px - * - :ref:`Stark Broadened Lines ` - - Specifying a Stark broadened lineshape instead of Doppler broadening. - - .. image:: ./passive_spectroscopy/stark_line_zoomed.png - :height: 150px - :width: 150px * - :ref:`Zeeman Spectroscopy ` - Specifying a Zeeman triplet or multiplet line shapes. - .. image:: ./passive_spectroscopy/zeeman_spectrum_45deg.png :height: 150px - :width: 150px + :width: 150px + * - :ref:`Stark Broadened Lines ` + - Specifying a Stark broadened lineshape. + - .. image:: ./passive_spectroscopy/stark_spectrum.png + :height: 150px + :width: 150px + * - :ref:`Stark-Zeeman Lines ` + - Modelling Stark-Zeeman lineshapes. + - .. image:: ./passive_spectroscopy/stark_zeeman_balmer_alpha.png + :height: 150px + :width: 150px Bolometry diff --git a/docs/source/demonstrations/jet_cxrs/jet_demo_76666.rst b/docs/source/demonstrations/jet_cxrs/jet_demo_76666.rst index f1f79f69..ccf89ed3 100644 --- a/docs/source/demonstrations/jet_cxrs/jet_demo_76666.rst +++ b/docs/source/demonstrations/jet_cxrs/jet_demo_76666.rst @@ -1,3 +1,5 @@ +:orphan: + .. _jet_cxrs_76666: diff --git a/docs/source/demonstrations/jet_cxrs/quickstart.rst b/docs/source/demonstrations/jet_cxrs/quickstart.rst index 28deddd0..6ed48f7e 100644 --- a/docs/source/demonstrations/jet_cxrs/quickstart.rst +++ b/docs/source/demonstrations/jet_cxrs/quickstart.rst @@ -1,3 +1,5 @@ +:orphan: + .. _jet_cxrs_quickstart: diff --git a/docs/source/demonstrations/line_emission/balmer_series_spectra.rst b/docs/source/demonstrations/line_emission/balmer_series_spectra.rst index 98ad540d..532df8f1 100644 --- a/docs/source/demonstrations/line_emission/balmer_series_spectra.rst +++ b/docs/source/demonstrations/line_emission/balmer_series_spectra.rst @@ -1,3 +1,5 @@ +:orphan: + .. _balmer_series_spectra: diff --git a/docs/source/demonstrations/line_emission/custom_emitter.rst b/docs/source/demonstrations/line_emission/custom_emitter.rst index 56f3c442..73ffde60 100644 --- a/docs/source/demonstrations/line_emission/custom_emitter.rst +++ b/docs/source/demonstrations/line_emission/custom_emitter.rst @@ -1,3 +1,5 @@ +:orphan: + .. _custom_emitter: diff --git a/docs/source/demonstrations/line_emission/mastu_forward_cameras.rst b/docs/source/demonstrations/line_emission/mastu_forward_cameras.rst index f3d54084..f2d77bac 100644 --- a/docs/source/demonstrations/line_emission/mastu_forward_cameras.rst +++ b/docs/source/demonstrations/line_emission/mastu_forward_cameras.rst @@ -1,3 +1,5 @@ +:orphan: + .. _mastu_forward_cameras: diff --git a/docs/source/demonstrations/passive_spectroscopy/balmer_series.rst b/docs/source/demonstrations/passive_spectroscopy/balmer_series.rst index 77fe64f3..53fbc9d1 100644 --- a/docs/source/demonstrations/passive_spectroscopy/balmer_series.rst +++ b/docs/source/demonstrations/passive_spectroscopy/balmer_series.rst @@ -1,3 +1,5 @@ +:orphan: + .. _impact_recom_lines: diff --git a/docs/source/demonstrations/passive_spectroscopy/multiplet.rst b/docs/source/demonstrations/passive_spectroscopy/multiplet.rst index 9caa3470..0aa1def1 100644 --- a/docs/source/demonstrations/passive_spectroscopy/multiplet.rst +++ b/docs/source/demonstrations/passive_spectroscopy/multiplet.rst @@ -1,3 +1,5 @@ +:orphan: + .. _multiplet_lines: diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_broadening.rst b/docs/source/demonstrations/passive_spectroscopy/stark_broadening.rst index bd670a49..0830b7e1 100644 --- a/docs/source/demonstrations/passive_spectroscopy/stark_broadening.rst +++ b/docs/source/demonstrations/passive_spectroscopy/stark_broadening.rst @@ -1,3 +1,5 @@ +:orphan: + .. _stark_broadening: @@ -12,9 +14,15 @@ electric field due to the presence of neighbouring ions. In normal tokamak operations this effect is negligible, except in the divertor region. It is possible to override the default doppler broadened line shape by -specifying the StarkBroadenedLine() lineshape class. In this example -we can see two stark broadened balmer series lines surrounding a -Nitrogen II multiplet feature. +specifying the StarkBroadenedLine() lineshape class. +This class suppors Balmer and Paschen series and is based on +the Stark-Doppler-Zeeman line shape model from B. Lomanowski, et al. +"Inferring divertor plasma properties from hydrogen Balmer +and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) +`123028 `_. +In this example we can see two stark broadened balmer series lines surrounding a +Nitrogen II multiplet feature. The logarithmic scale is chosen to illustrate +the power-law decay of the Stark-broadened line wings. .. literalinclude:: ../../../../demos/emission_models/stark_broadening.py @@ -23,11 +31,4 @@ Nitrogen II multiplet feature. :width: 450px **Caption:** The observed spectrum with two stark broadened balmer lines - (397nm and 410nm) surrounding a NII multiplet feature. - -.. figure:: stark_line_zoomed.png - :align: center - :width: 450px - - **Caption:** A zoomed in view of the 410nm Balmer line revealing the - characteristic stark broadened lineshape. + (397nm and 410nm) surrounding a NII multiplet feature in the logarithmic scale. diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_line_zoomed.png b/docs/source/demonstrations/passive_spectroscopy/stark_line_zoomed.png deleted file mode 100644 index 2a6ee04b..00000000 Binary files a/docs/source/demonstrations/passive_spectroscopy/stark_line_zoomed.png and /dev/null differ diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_spectrum.png b/docs/source/demonstrations/passive_spectroscopy/stark_spectrum.png index 53230968..04d90589 100644 Binary files a/docs/source/demonstrations/passive_spectroscopy/stark_spectrum.png and b/docs/source/demonstrations/passive_spectroscopy/stark_spectrum.png differ diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_zeeman.rst b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman.rst new file mode 100644 index 00000000..eb07750e --- /dev/null +++ b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman.rst @@ -0,0 +1,39 @@ + +.. _stark_zeeman: + +Stark-Zeeman line shape +======================= + +In B. Lomanowski, et al. "Inferring divertor plasma properties from hydrogen Balmer +and Paschen series spectroscopy in JET-ILW." Nuclear Fusion 55.12 (2015) +`123028 `_ Stark and Zeeman features +are treated independently. This is a simplification and for better accuracy these effects +must be taken into account jointly as in J. Rosato, Y. Marandet, R. Stamm, +Journal of Quantitative Spectroscopy & Radiative Transfer 187 (2017) +`333 `_. + +The StarkBroadenedLine() follows the Lomanowski's paper, but introduces a couple of additional +approximations: + +* Zeeman splitting is taken in the form of a simple triplet with a :math:`\pi`-component + centred at :math:`\lambda`, :math:`\sigma^{+}`-component at :math:`\frac{hc}{hc/\lambda -\mu B}` + and :math:`\sigma^{-}`-component at :math:`\frac{hc}{hc/\lambda +\mu B}`. +* The convolution of Stark-Zeeman and Doppler profiles is replaced with the weighted sum + to speed-up calculations (`pseudo-Voigt `_ approximation). + +This example calculates Balmer-alpha and Paschen-beta Stark-Zeeman spectral lines +for the same parameters of plasma as in Figure 2 in Lomanowski's paper. + +.. literalinclude:: ../../../../demos/emission_models/stark_zeeman.py + +.. figure:: stark_zeeman_balmer_alpha.png + :align: center + :width: 450px + + **Caption:** The Stark-Zeeman structure of the Balmer-alpha line. + +.. figure:: stark_zeeman_paschen_beta.png + :align: center + :width: 450px + + **Caption:** The Stark-Zeeman structure of the Paschen-beta line. diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_balmer_alpha.png b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_balmer_alpha.png new file mode 100644 index 00000000..532a8eab Binary files /dev/null and b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_balmer_alpha.png differ diff --git a/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_paschen_beta.png b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_paschen_beta.png new file mode 100644 index 00000000..afa5044a Binary files /dev/null and b/docs/source/demonstrations/passive_spectroscopy/stark_zeeman_paschen_beta.png differ diff --git a/docs/source/demonstrations/passive_spectroscopy/zeeman_spectroscopy.rst b/docs/source/demonstrations/passive_spectroscopy/zeeman_spectroscopy.rst index c5fc4466..2af89a7f 100644 --- a/docs/source/demonstrations/passive_spectroscopy/zeeman_spectroscopy.rst +++ b/docs/source/demonstrations/passive_spectroscopy/zeeman_spectroscopy.rst @@ -1,3 +1,5 @@ +:orphan: + .. _zeeman_spectroscopy: diff --git a/docs/source/demonstrations/plasmas/analytic_function_plasma.rst b/docs/source/demonstrations/plasmas/analytic_function_plasma.rst index e74ba874..6bbdfd8a 100644 --- a/docs/source/demonstrations/plasmas/analytic_function_plasma.rst +++ b/docs/source/demonstrations/plasmas/analytic_function_plasma.rst @@ -1,3 +1,5 @@ +:orphan: + .. _analytic_function_plasma: @@ -10,11 +12,25 @@ shows how to use these functions in a plasma and visualises the results. Note that while it is possible to use pure python functions for development, they are typically ~100 times slower than their cython counterparts. Therefore, for use cases where speed is important -we recommend moving these functions to cython classes. For an example of a cython function, -see the `Gaussian `_ -cython class in the demos folder. +we recommend moving these functions to cython classes. An alternative solution which may not require +writing and compiling any additional cython code is to use Raysect's +`function framework `_ to build up +expressions which will be evaluated like Python functions. These will typically run slightly slower +than a hand-coded cython implementation but still significantly faster than a pure python +implementation. + +Two examples are provided, one using a pure python implementation of analytic forms for neutral and +ion plasma species distributions, and one using objects from Raysect's function framework. + +.. tabs:: + + .. tab:: Pure python + + .. literalinclude:: ../../../../demos/plasmas/analytic_plasma.py + + .. tab:: Function framework -.. literalinclude:: ../../../../demos/plasmas/analytic_plasma.py + .. literalinclude:: ../../../../demos/plasmas/analytic_plasma_function_framework.py .. figure:: analytic_plasma_slices.png :align: center diff --git a/docs/source/demonstrations/plasmas/beam_attenuation.png b/docs/source/demonstrations/plasmas/beam_attenuation.png index 8f6c682d..e729570b 100644 Binary files a/docs/source/demonstrations/plasmas/beam_attenuation.png and b/docs/source/demonstrations/plasmas/beam_attenuation.png differ diff --git a/docs/source/demonstrations/plasmas/beam_density_xz.png b/docs/source/demonstrations/plasmas/beam_density_xz.png index 67c04fae..73aaf0c8 100644 Binary files a/docs/source/demonstrations/plasmas/beam_density_xz.png and b/docs/source/demonstrations/plasmas/beam_density_xz.png differ diff --git a/docs/source/demonstrations/plasmas/beam_into_plasma.png b/docs/source/demonstrations/plasmas/beam_into_plasma.png index 2a52ef75..d4ae9ce1 100644 Binary files a/docs/source/demonstrations/plasmas/beam_into_plasma.png and b/docs/source/demonstrations/plasmas/beam_into_plasma.png differ diff --git a/docs/source/demonstrations/plasmas/beams_into_plasmas.rst b/docs/source/demonstrations/plasmas/beams_into_plasmas.rst index 1e2d79aa..3479983c 100644 --- a/docs/source/demonstrations/plasmas/beams_into_plasmas.rst +++ b/docs/source/demonstrations/plasmas/beams_into_plasmas.rst @@ -1,3 +1,5 @@ +:orphan: + .. _beams_into_plasmas: diff --git a/docs/source/demonstrations/plasmas/equilibrium.rst b/docs/source/demonstrations/plasmas/equilibrium.rst index 09c35136..56e1b7a5 100644 --- a/docs/source/demonstrations/plasmas/equilibrium.rst +++ b/docs/source/demonstrations/plasmas/equilibrium.rst @@ -1,3 +1,5 @@ +:orphan: + .. _flux_function_plasmas: Flux Function Plasmas diff --git a/docs/source/demonstrations/plasmas/mesh2d_plasma.rst b/docs/source/demonstrations/plasmas/mesh2d_plasma.rst index 09fe8603..78c2e75d 100644 --- a/docs/source/demonstrations/plasmas/mesh2d_plasma.rst +++ b/docs/source/demonstrations/plasmas/mesh2d_plasma.rst @@ -1,3 +1,5 @@ +:orphan: + .. _mesh2d_plasma: diff --git a/docs/source/demonstrations/plasmas/slab_plasma.rst b/docs/source/demonstrations/plasmas/slab_plasma.rst index 2752bc2e..af20b701 100644 --- a/docs/source/demonstrations/plasmas/slab_plasma.rst +++ b/docs/source/demonstrations/plasmas/slab_plasma.rst @@ -1,3 +1,5 @@ +:orphan: + .. _slab_plasma: diff --git a/docs/source/demonstrations/radiation_loads/radiation_function.rst b/docs/source/demonstrations/radiation_loads/radiation_function.rst index 26d45d57..3ca6ccc1 100644 --- a/docs/source/demonstrations/radiation_loads/radiation_function.rst +++ b/docs/source/demonstrations/radiation_loads/radiation_function.rst @@ -1,3 +1,5 @@ +:orphan: + .. _radiation_function: diff --git a/docs/source/demonstrations/radiation_loads/surface_radiation_loads.rst b/docs/source/demonstrations/radiation_loads/surface_radiation_loads.rst index 63bb2ad9..eeb8c129 100644 --- a/docs/source/demonstrations/radiation_loads/surface_radiation_loads.rst +++ b/docs/source/demonstrations/radiation_loads/surface_radiation_loads.rst @@ -1,3 +1,5 @@ +:orphan: + .. _aug_solps_radiation_load: diff --git a/docs/source/demonstrations/radiation_loads/symmetric_power_load.rst b/docs/source/demonstrations/radiation_loads/symmetric_power_load.rst index a9d7be13..195a6028 100644 --- a/docs/source/demonstrations/radiation_loads/symmetric_power_load.rst +++ b/docs/source/demonstrations/radiation_loads/symmetric_power_load.rst @@ -1,3 +1,5 @@ +:orphan: + .. _symmetric_power_load: diff --git a/docs/source/demonstrations/radiation_loads/wall_from_polygon.rst b/docs/source/demonstrations/radiation_loads/wall_from_polygon.rst index 53b8a8e7..8de97008 100644 --- a/docs/source/demonstrations/radiation_loads/wall_from_polygon.rst +++ b/docs/source/demonstrations/radiation_loads/wall_from_polygon.rst @@ -1,3 +1,5 @@ +:orphan: + .. _wall_from_polygon: diff --git a/docs/source/demonstrations/ray_transfer/ray_transfer_box.rst b/docs/source/demonstrations/ray_transfer/ray_transfer_box.rst index 57ac572f..5188d532 100644 --- a/docs/source/demonstrations/ray_transfer/ray_transfer_box.rst +++ b/docs/source/demonstrations/ray_transfer/ray_transfer_box.rst @@ -1,3 +1,5 @@ +:orphan: + .. _ray_transfer_box: diff --git a/docs/source/demonstrations/ray_transfer/ray_transfer_cylinder.rst b/docs/source/demonstrations/ray_transfer/ray_transfer_cylinder.rst index 2eb55cbb..48e6011a 100644 --- a/docs/source/demonstrations/ray_transfer/ray_transfer_cylinder.rst +++ b/docs/source/demonstrations/ray_transfer/ray_transfer_cylinder.rst @@ -1,3 +1,5 @@ +:orphan: + .. _ray_transfer_cylinder: diff --git a/docs/source/demonstrations/ray_transfer/ray_transfer_map.rst b/docs/source/demonstrations/ray_transfer/ray_transfer_map.rst index e74f7962..04378b21 100644 --- a/docs/source/demonstrations/ray_transfer/ray_transfer_map.rst +++ b/docs/source/demonstrations/ray_transfer/ray_transfer_map.rst @@ -1,3 +1,5 @@ +:orphan: + .. _ray_transfer_map: diff --git a/docs/source/demonstrations/ray_transfer/ray_transfer_mask.rst b/docs/source/demonstrations/ray_transfer/ray_transfer_mask.rst index 6bd34451..e124e2d8 100644 --- a/docs/source/demonstrations/ray_transfer/ray_transfer_mask.rst +++ b/docs/source/demonstrations/ray_transfer/ray_transfer_mask.rst @@ -1,3 +1,5 @@ +:orphan: + .. _ray_transfer_mask: diff --git a/docs/source/demonstrations/solps/solps_plasma.rst b/docs/source/demonstrations/solps/solps_plasma.rst index b8bd788b..c924431e 100644 --- a/docs/source/demonstrations/solps/solps_plasma.rst +++ b/docs/source/demonstrations/solps/solps_plasma.rst @@ -1,3 +1,5 @@ +:orphan: + .. _mastu_solps_plasma: diff --git a/docs/source/installation_and_structure.rst b/docs/source/installation_and_structure.rst index 3af9d3ad..e9c051bf 100644 --- a/docs/source/installation_and_structure.rst +++ b/docs/source/installation_and_structure.rst @@ -82,6 +82,12 @@ line to install the packages under your own user account. Alternatively, conside `virtual environment `_ to avoid the risk of conflicting versions of packages in your Python environment. +By default, pip will install from wheel archives on PyPI. If a binary wheel is not available for +your version of Python, or if you are installing from source in editable mode for development (see +below), the package will be compiled locally on your machine. Compilation is done in parallel by +default, using all available processors, but can be overridden by setting the environment variable +``CHERAB_NCPU`` to the number of processors to use. + Installing from source ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/math/math.rst b/docs/source/math/math.rst index eab9a1fa..836cbe84 100644 --- a/docs/source/math/math.rst +++ b/docs/source/math/math.rst @@ -18,6 +18,7 @@ utilities that Cherab provides for slicing, dicing and projecting these function function interpolators mappers + transform mask samplers slice diff --git a/docs/source/math/transform.rst b/docs/source/math/transform.rst new file mode 100644 index 00000000..f3d6cfa8 --- /dev/null +++ b/docs/source/math/transform.rst @@ -0,0 +1,11 @@ + +Transformations +--------------- + +These functions perform coordinate transformations as wrappers for other functions. Unlike mappers, these wrappers do not change the dimensionality of the wrapped functions. + +.. automodule:: cherab.core.math.transform.cylindrical + :members: + +.. automodule:: cherab.core.math.transform.periodic + :members: diff --git a/docs/source/models/basic_line/basic_line_emission.rst b/docs/source/models/basic_line/basic_line_emission.rst index c07995db..b71fc03c 100644 --- a/docs/source/models/basic_line/basic_line_emission.rst +++ b/docs/source/models/basic_line/basic_line_emission.rst @@ -2,4 +2,8 @@ Basic Line Emission =================== -Documentation for this model will go here soon... +.. autoclass:: cherab.core.model.plasma.impact_excitation.ExcitationLine + +.. autoclass:: cherab.core.model.plasma.recombination.RecombinationLine + +.. autoclass:: cherab.core.model.plasma.thermal_cx.ThermalCXLine diff --git a/docs/source/models/beam/beam_attenuator.rst b/docs/source/models/beam/beam_attenuator.rst new file mode 100644 index 00000000..d0cfceb7 --- /dev/null +++ b/docs/source/models/beam/beam_attenuator.rst @@ -0,0 +1,10 @@ + +Beam Attenuation +---------------- + + +Single Ray Attenuator +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: cherab.core.model.attenuator.singleray.SingleRayAttenuator + :members: diff --git a/docs/source/models/cxs/cxs_model.rst b/docs/source/models/cxs/cxs_model.rst index a211801c..b069c522 100644 --- a/docs/source/models/cxs/cxs_model.rst +++ b/docs/source/models/cxs/cxs_model.rst @@ -2,15 +2,6 @@ CXS models ---------- -CXS models. - - -Single Ray Attenuator -^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: cherab.core.model.attenuator.singleray.SingleRayAttenuator - :members: - CXS Beam Plasma Intersection ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/models/emission_models.rst b/docs/source/models/emission_models.rst index 3a2c3b9c..52c76e19 100644 --- a/docs/source/models/emission_models.rst +++ b/docs/source/models/emission_models.rst @@ -6,6 +6,7 @@ Cherab contains a number of independent physics models for spectroscopic emissio .. toctree:: custom_models + beam/beam_attenuator cxs/cxs_model cxs/charge_exchange_calculation bes/bes_model diff --git a/docs/source/models/line_shapes/spectral_line_shapes.rst b/docs/source/models/line_shapes/spectral_line_shapes.rst index 5dc6325e..0d7fac55 100644 --- a/docs/source/models/line_shapes/spectral_line_shapes.rst +++ b/docs/source/models/line_shapes/spectral_line_shapes.rst @@ -2,36 +2,46 @@ Spectral Line Shapes ==================== -Cherab contains Doppler-broadened, Stark-broadened and Doppler-Zeeman line shapes of -atomic spectra. Stark-Doppler and Stark-Doppler-Zeeman line shapes of hydrogen spectra -will be added in the future. +Cherab contains Doppler-broadened, Doppler-Zeeman and Stark-Doppler-Zeeman line shapes of +atomic spectra. **Assumption: Maxwellian distribution of emitting species is assumed.** **A general model of Doppler broadening will be implemented in the future.** -.. autoclass:: cherab.core.model.lineshape.add_gaussian_line +.. autoclass:: cherab.core.model.lineshape.doppler.doppler_shift -.. autoclass:: cherab.core.model.lineshape.LineShapeModel +.. autoclass:: cherab.core.model.lineshape.doppler.thermal_broadening + +.. autoclass:: cherab.core.model.lineshape.gaussian.add_gaussian_line + +.. autoclass:: cherab.core.model.lineshape.stark.add_lorentzian_line + +.. autoclass:: cherab.core.model.lineshape.base.LineShapeModel :members: -.. autoclass:: cherab.core.model.lineshape.GaussianLine +.. autoclass:: cherab.core.model.lineshape.gaussian.GaussianLine :members: -.. autoclass:: cherab.core.model.lineshape.MultipletLineShape +.. autoclass:: cherab.core.model.lineshape.multiplet.MultipletLineShape :members: -.. autoclass:: cherab.core.model.lineshape.StarkBroadenedLine +.. autoclass:: cherab.core.model.lineshape.zeeman.ZeemanLineShapeModel :members: -.. autoclass:: cherab.core.model.lineshape.ZeemanLineShapeModel +.. autoclass:: cherab.core.model.lineshape.zeeman.ZeemanTriplet :members: -.. autoclass:: cherab.core.model.lineshape.ZeemanTriplet +.. autoclass:: cherab.core.model.lineshape.zeeman.ParametrisedZeemanTriplet :members: -.. autoclass:: cherab.core.model.lineshape.ParametrisedZeemanTriplet +.. autoclass:: cherab.core.model.lineshape.zeeman.ZeemanMultiplet :members: -.. autoclass:: cherab.core.model.lineshape.ZeemanMultiplet +.. autoclass:: cherab.core.model.lineshape.stark.StarkBroadenedLine :members: +.. autoclass:: cherab.core.model.lineshape.beam.base.BeamLineShapeModel + :members: + +.. autoclass:: cherab.core.model.lineshape.beam.mse.BeamEmissionMultiplet + :members: diff --git a/docs/source/models/radiated_power/radiated_power.rst b/docs/source/models/radiated_power/radiated_power.rst index 0b8594e1..d00fcec2 100644 --- a/docs/source/models/radiated_power/radiated_power.rst +++ b/docs/source/models/radiated_power/radiated_power.rst @@ -2,4 +2,4 @@ Total Radiated Power ==================== -Documentation for this model will go here soon... +.. autoclass:: cherab.core.model.plasma.total_radiated_power.TotalRadiatedPower diff --git a/docs/source/plasmas/beam_direction.png b/docs/source/plasmas/beam_direction.png new file mode 100644 index 00000000..782bfe46 Binary files /dev/null and b/docs/source/plasmas/beam_direction.png differ diff --git a/docs/source/plasmas/particle_beams.rst b/docs/source/plasmas/particle_beams.rst index 615358a3..e08d56d2 100644 --- a/docs/source/plasmas/particle_beams.rst +++ b/docs/source/plasmas/particle_beams.rst @@ -2,7 +2,11 @@ Mono-energetic Particle Beams ============================= -.. autoclass:: cherab.core.Beam +.. autoclass:: cherab.core.beam.node.Beam :members: +.. figure:: ./beam_direction.png + :align: center + The beam direction pattern in XZ-plane for the beam with ``sigma`` = 0.1 m + and ``divergence_x`` = 5 :math:`^{\circ}`. diff --git a/pyproject.toml b/pyproject.toml index f896f8f0..4849f0b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "oldest-supported-numpy", "cython>=0.28", "raysect==0.7.1"] +requires = ["setuptools>=62.3", "oldest-supported-numpy", "cython~=3.0", "raysect==0.8.1.*"] build-backend="setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 2b5e7b4e..9a13464d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -cython>=0.28 -numpy>=1.14 +cython~=3.0 +numpy>=1.14,<2.0 scipy matplotlib -raysect==0.7.1 \ No newline at end of file +raysect==0.8.1.* diff --git a/setup.py b/setup.py index d7810f52..f11dd08f 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,11 @@ -from setuptools import setup, find_packages, Extension +from collections import defaultdict import sys -import numpy import os import os.path as path +from pathlib import Path import multiprocessing +import numpy +from setuptools import setup, find_packages, Extension from Cython.Build import cythonize multiprocessing.set_start_method('fork') @@ -31,9 +33,13 @@ source_paths = ["cherab", "demos"] compilation_includes = [".", numpy.get_include()] -compilation_args = [] +compilation_args = ["-O3", "-Wno-unreachable-code-fallthrough"] cython_directives = {"language_level": 3} +macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] setup_path = path.dirname(path.abspath(__file__)) +num_processes = int(os.getenv("CHERAB_NCPU", "-1")) +if num_processes == -1: + num_processes = multiprocessing.cpu_count() if line_profile: compilation_args.append("-DCYTHON_TRACE=1") @@ -56,6 +62,7 @@ [pyx_file], include_dirs=compilation_includes, extra_compile_args=compilation_args, + define_macros=macros, ), ) @@ -68,6 +75,16 @@ compiler_directives=cython_directives, ) +# Include demos in a separate directory in the distribution as data_files. +demo_parent_path = Path("share/cherab/demos/core") +data_files = defaultdict(list) +demos_source = Path("demos") +for item in demos_source.rglob("*"): + if item.is_file(): + install_dir = demo_parent_path / item.parent.relative_to(demos_source) + data_files[str(install_dir)].append(str(item)) +data_files = list(data_files.items()) + # parse the package version number with open(path.join(path.dirname(__file__), "cherab/core/VERSION")) as version_file: version = version_file.read().strip() @@ -100,15 +117,28 @@ long_description=long_description, long_description_content_type="text/markdown", install_requires=[ - "numpy>=1.14", + "numpy>=1.14,<2.0", "scipy", "matplotlib", - "raysect==0.7.1", + "raysect==0.8.1.*", + ], + extras_require={ + # Running ./dev/build_docs.sh runs setup.py, which requires cython. + "docs": ["cython~=3.0", "sphinx", "sphinx-rtd-theme", "sphinx-tabs"], + }, + packages=find_packages(include=["cherab*"]), + package_data={"": [ + "**/*.pyx", "**/*.pxd", # Needed to build Cython extensions. + "**/*.json", "**/*.cl", "**/*.npy", "**/*.obj", # Supplementary data ], - packages=find_packages(), - include_package_data=True, + "cherab.core": ["VERSION"], # Used to determine version at run time + }, + data_files=data_files, zip_safe=False, ext_modules=extensions, + options=dict( + build_ext={"parallel": num_processes}, + ), ) # setup a rate repository with common rates