xhermes 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xhermes-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: xhermes
3
+ Version: 0.1.0
4
+ Summary: Analyse data from Hermes-3 simulations using xarray
5
+ License: Apache
6
+ Classifier: Development Status :: 2 - Pre-Alpha
7
+ Classifier: Intended Audience :: Science/Research
8
+ Classifier: Intended Audience :: Education
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: xarray
17
+ Requires-Dist: xbout
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: license
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
24
+
25
+ # xHermes
26
+
27
+ xHermes is a post-processing package for Hermes-3 in 1D, 2D and 3D which provides automatic conversion to SI units and many useful plotting routines.
28
+
29
+ xHermes a wrapper around [xBOUT](https://github.com/boutproject/xBOUT) to provide [Hermes-3](https://github.com/bendudson/hermes-3) specific
30
+ features. At the moment, xHermes is used for loading in the Hermes-3 results, performing SI unit conversion and calculating some geometry terms.
31
+ You may still want to use xBOUT, in particular if you are running 2D and 3D cases and want plotting routines for those.
32
+
33
+ There is also a collection of Mike Kryjak's personal post-processing scripts [sdtools](https://github.com/mikekryjak/sdtools/tree/main/hermes3) which have extensive additional functionality for 1D and 2D postprocessing.
34
+ These scripts are not officially supported, but feel free to take a look.
35
+ It is intended for all those additional features to be merged into xHermes over time.
36
+
37
+ Both xBOUT and xHermes use [Xarray](https://docs.xarray.dev/en/stable/) which provides a scalable and powerful framework
38
+ for dealing with large amounts of data while preserving dimensional
39
+ consistency. If you have ever used [Pandas](https://pandas.pydata.org/), you will find Xarray very familiar, as it is effectively Pandas in more dimensions.
40
+
41
+ While Xarray is based on Numpy arrays under the hood and the data
42
+ can be accessed in this way, Xarray's main benefit comes in its
43
+ powerful abilities to select, filter and operate on data. The
44
+ syntax is similar to Pandas and the basics are explained in
45
+ Xarray's excellent documentation.
46
+ If you are new to Xarray, it may be useful to start with reviewing the [selection syntax](https://docs.xarray.dev/en/stable/user-guide/indexing.html).
47
+
48
+ ## Installation
49
+
50
+ Hermes-3 is pip installable. In the terminal with your Python environment enabled, run:
51
+
52
+ pip install xhermes
53
+
54
+ However, you may not be able to get the latest version this way. For now, it is highly recommended you do an editable install instead:
55
+
56
+ git clone https://github.com/boutproject/xhermes
57
+ cd xhermes
58
+ pip install -e .
59
+
60
+ In this way, pip will install the Python package from the xHermes folder you cloned. If you use git to checkout branches or modify the contents in any way, it will be automatically work.
61
+
62
+ ## Loading simulations
63
+
64
+ The data from a Hermes-3 run can be loaded with just
65
+
66
+ import xhermes
67
+ ds = xhermes.open(hermes_simulation_path)
68
+
69
+ where the `hermes_simulation_path` directory is assumed to contain the `BOUT.dmp.*.nc`
70
+ files and a `BOUT.settings` file. `open` returns an instance of an
71
+ `xarray.Dataset` which contains BOUT- and Hermes-specific information
72
+ in the `attrs`, so represents a general structure for storing all of
73
+ the output of a simulation, including data, input options and (soon)
74
+ grid data.
75
+
76
+ When working with 2D or 3D simulations of tokamaks, it can be useful to
77
+ load the grid along with the simulation file and pass the "toroidal" flag to xBOUT:
78
+
79
+ import xhermes
80
+ ds = xhermes.open(hermes_simulation_path, gridfilepath = grid_file_path, geometry = "toroidal")
81
+
82
+ This loads the metric tensor and grid corner coordinates from the grid which
83
+ enables geometrical operations, polygonal plotting routines and additional metadata on topology.
84
+
85
+ ## Working with the data
86
+
87
+ ### Normalisation and geometry extraction
88
+
89
+ All variables within Hermes-3 are normalised. The normalisation factors, the units and variable names
90
+ are provided in the Hermes-3 output for most variables.
91
+ This information can be accessed for each variable DataArray:
92
+
93
+ ds["Pe"].attrs
94
+
95
+ xHermes automatically applies the unnormalisation when the simulation is loaded in.
96
+
97
+ When working with 1D and 2D tokamak simulations, it can be useful to extract
98
+ cell lengths, volumes and cross-sectional areas as well as some additional metadata from the model:
99
+
100
+ ds = ds.hermes.extract_1D_tokamak_geometry()
101
+
102
+ or
103
+
104
+ ds = ds.hermes.extract_2D_tokamak_geometry()
105
+
106
+ ### The Dataset
107
+
108
+ The Dataset `ds` contains useful objects:
109
+ - A DataArray for each variable: `ds.data_vars` shows a list. `ds["Pe"]` returns the Pe DataArray.
110
+ - A DataArray for each coordinate: `ds.coords` shows a list. `ds["t"]` or `ds.coords["t"]` returns the t DataArray.
111
+ - A summary of dimensions in the model: `ds.dims`
112
+ - Model metadata: `ds.metadata`
113
+ - Model input settings: `ds.options`
114
+
115
+ ### The DataArray
116
+
117
+ The DataArray (e.g. `ds["Pe"]`) contains the array with the variable data, its dimensionality and
118
+ attributes as well as metadata and options inherited from the parent dataset.
119
+
120
+ A visual summary of the DataArray contents and size can be obtained by simply:
121
+
122
+ ds["Pe"]
123
+
124
+ Note that this may be slow due to the Xarray overhead. To access the underlying data as a Numpy array, one can use:
125
+
126
+ ds["Pe"].values
127
+
128
+ or
129
+
130
+ ds["Pe"].data
131
+
132
+ ### Plotting
133
+ Xarray contains a lot of built-in plotting routines which can be used to prepare [excellent 1D and 2D plots](https://docs.xarray.dev/en/stable/user-guide/plotting.html).
134
+ Unfortunately, it is unable to deal with 2D plots in tokamak geometry.
135
+ Routines to plot those are available in xBOUT which has some excellent [example notebooks](https://github.com/boutproject/xBOUT-examples).
136
+
137
+
138
+
@@ -0,0 +1,114 @@
1
+ # xHermes
2
+
3
+ xHermes is a post-processing package for Hermes-3 in 1D, 2D and 3D which provides automatic conversion to SI units and many useful plotting routines.
4
+
5
+ xHermes a wrapper around [xBOUT](https://github.com/boutproject/xBOUT) to provide [Hermes-3](https://github.com/bendudson/hermes-3) specific
6
+ features. At the moment, xHermes is used for loading in the Hermes-3 results, performing SI unit conversion and calculating some geometry terms.
7
+ You may still want to use xBOUT, in particular if you are running 2D and 3D cases and want plotting routines for those.
8
+
9
+ There is also a collection of Mike Kryjak's personal post-processing scripts [sdtools](https://github.com/mikekryjak/sdtools/tree/main/hermes3) which have extensive additional functionality for 1D and 2D postprocessing.
10
+ These scripts are not officially supported, but feel free to take a look.
11
+ It is intended for all those additional features to be merged into xHermes over time.
12
+
13
+ Both xBOUT and xHermes use [Xarray](https://docs.xarray.dev/en/stable/) which provides a scalable and powerful framework
14
+ for dealing with large amounts of data while preserving dimensional
15
+ consistency. If you have ever used [Pandas](https://pandas.pydata.org/), you will find Xarray very familiar, as it is effectively Pandas in more dimensions.
16
+
17
+ While Xarray is based on Numpy arrays under the hood and the data
18
+ can be accessed in this way, Xarray's main benefit comes in its
19
+ powerful abilities to select, filter and operate on data. The
20
+ syntax is similar to Pandas and the basics are explained in
21
+ Xarray's excellent documentation.
22
+ If you are new to Xarray, it may be useful to start with reviewing the [selection syntax](https://docs.xarray.dev/en/stable/user-guide/indexing.html).
23
+
24
+ ## Installation
25
+
26
+ Hermes-3 is pip installable. In the terminal with your Python environment enabled, run:
27
+
28
+ pip install xhermes
29
+
30
+ However, you may not be able to get the latest version this way. For now, it is highly recommended you do an editable install instead:
31
+
32
+ git clone https://github.com/boutproject/xhermes
33
+ cd xhermes
34
+ pip install -e .
35
+
36
+ In this way, pip will install the Python package from the xHermes folder you cloned. If you use git to checkout branches or modify the contents in any way, it will be automatically work.
37
+
38
+ ## Loading simulations
39
+
40
+ The data from a Hermes-3 run can be loaded with just
41
+
42
+ import xhermes
43
+ ds = xhermes.open(hermes_simulation_path)
44
+
45
+ where the `hermes_simulation_path` directory is assumed to contain the `BOUT.dmp.*.nc`
46
+ files and a `BOUT.settings` file. `open` returns an instance of an
47
+ `xarray.Dataset` which contains BOUT- and Hermes-specific information
48
+ in the `attrs`, so represents a general structure for storing all of
49
+ the output of a simulation, including data, input options and (soon)
50
+ grid data.
51
+
52
+ When working with 2D or 3D simulations of tokamaks, it can be useful to
53
+ load the grid along with the simulation file and pass the "toroidal" flag to xBOUT:
54
+
55
+ import xhermes
56
+ ds = xhermes.open(hermes_simulation_path, gridfilepath = grid_file_path, geometry = "toroidal")
57
+
58
+ This loads the metric tensor and grid corner coordinates from the grid which
59
+ enables geometrical operations, polygonal plotting routines and additional metadata on topology.
60
+
61
+ ## Working with the data
62
+
63
+ ### Normalisation and geometry extraction
64
+
65
+ All variables within Hermes-3 are normalised. The normalisation factors, the units and variable names
66
+ are provided in the Hermes-3 output for most variables.
67
+ This information can be accessed for each variable DataArray:
68
+
69
+ ds["Pe"].attrs
70
+
71
+ xHermes automatically applies the unnormalisation when the simulation is loaded in.
72
+
73
+ When working with 1D and 2D tokamak simulations, it can be useful to extract
74
+ cell lengths, volumes and cross-sectional areas as well as some additional metadata from the model:
75
+
76
+ ds = ds.hermes.extract_1D_tokamak_geometry()
77
+
78
+ or
79
+
80
+ ds = ds.hermes.extract_2D_tokamak_geometry()
81
+
82
+ ### The Dataset
83
+
84
+ The Dataset `ds` contains useful objects:
85
+ - A DataArray for each variable: `ds.data_vars` shows a list. `ds["Pe"]` returns the Pe DataArray.
86
+ - A DataArray for each coordinate: `ds.coords` shows a list. `ds["t"]` or `ds.coords["t"]` returns the t DataArray.
87
+ - A summary of dimensions in the model: `ds.dims`
88
+ - Model metadata: `ds.metadata`
89
+ - Model input settings: `ds.options`
90
+
91
+ ### The DataArray
92
+
93
+ The DataArray (e.g. `ds["Pe"]`) contains the array with the variable data, its dimensionality and
94
+ attributes as well as metadata and options inherited from the parent dataset.
95
+
96
+ A visual summary of the DataArray contents and size can be obtained by simply:
97
+
98
+ ds["Pe"]
99
+
100
+ Note that this may be slow due to the Xarray overhead. To access the underlying data as a Numpy array, one can use:
101
+
102
+ ds["Pe"].values
103
+
104
+ or
105
+
106
+ ds["Pe"].data
107
+
108
+ ### Plotting
109
+ Xarray contains a lot of built-in plotting routines which can be used to prepare [excellent 1D and 2D plots](https://docs.xarray.dev/en/stable/user-guide/plotting.html).
110
+ Unfortunately, it is unable to deal with 2D plots in tokamak geometry.
111
+ Routines to plot those are available in xBOUT which has some excellent [example notebooks](https://github.com/boutproject/xBOUT-examples).
112
+
113
+
114
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
xhermes-0.1.0/setup.py ADDED
@@ -0,0 +1,32 @@
1
+ import os
2
+ from setuptools import setup, find_packages
3
+
4
+
5
+ # Utility function to read the README file.
6
+ # Used for the long_description.
7
+ def read(fname):
8
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
9
+
10
+
11
+ setup(
12
+ name="xhermes",
13
+ version="0.1.0",
14
+ description="Analyse data from Hermes-3 simulations using xarray",
15
+ license="Apache",
16
+ long_description=read("README.md"),
17
+ long_description_content_type="text/markdown",
18
+ classifiers=[
19
+ "Development Status :: 2 - Pre-Alpha",
20
+ "Intended Audience :: Science/Research",
21
+ "Intended Audience :: Education",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: Apache Software License",
24
+ "Natural Language :: English",
25
+ "Operating System :: POSIX :: Linux",
26
+ "Programming Language :: Python :: 3.6",
27
+ "Topic :: Scientific/Engineering :: Physics",
28
+ ],
29
+ install_requires=["xarray", "xbout"],
30
+ packages=find_packages(),
31
+ include_package_data=True,
32
+ )
@@ -0,0 +1,2 @@
1
+ from .load import open_hermesdataset, open
2
+ from .accessors import HermesDatasetAccessor, HermesDataArrayAccessor
@@ -0,0 +1,249 @@
1
+ from xarray import register_dataset_accessor, register_dataarray_accessor
2
+ from xbout import BoutDatasetAccessor, BoutDataArrayAccessor
3
+ import numpy as np
4
+
5
+
6
+ @register_dataset_accessor("hermes")
7
+ class HermesDatasetAccessor(BoutDatasetAccessor):
8
+ """
9
+ Methods on Hermes-3 datasets
10
+ """
11
+
12
+ def __init__(self, ds):
13
+ super().__init__(ds)
14
+
15
+ def unnormalise(self):
16
+ """
17
+ In-place modify data, converting to SI units
18
+
19
+ Normalisation values set in load.open_hermesdataset
20
+ in attrs['conversion']
21
+ """
22
+
23
+ # Un-normalise data arrays
24
+ for key, da in self.data.items():
25
+ da.hermes.unnormalise()
26
+
27
+ # Un-normalise coordinates
28
+ for coord in ["dx", "dy", "dz", "t"]:
29
+ units_type = self.data[coord].attrs.get("units_type", "unknown")
30
+ if (units_type == "unknown") or (units_type == "SI"):
31
+ continue
32
+ elif units_type == "hermes":
33
+ # Un-normalise coordinate, modifying in place
34
+ self.data[coord] = (
35
+ self.data[coord] * self.data[coord].attrs["conversion"]
36
+ )
37
+ self.data[coord].attrs["units_type"] = "SI"
38
+ else:
39
+ raise ValueError("Unrecognised units_type: " + units_type)
40
+
41
+ def extract_1d_tokamak_geometry(self, remove_outer = False):
42
+ """
43
+ Process the results to generate 1D relevant geometry data:
44
+ - Reconstruct pos, the cell position in [m] from upstream from dy
45
+ - Calculate cross-sectional area da and volume dv
46
+
47
+ Notes
48
+ ----------
49
+ - dy is technically in flux space, but in 1D
50
+ this is the same as in regular space and in units of [m]
51
+
52
+ Returns
53
+ ----------
54
+ - Dataset with the new geometry
55
+ """
56
+ ds = self.data.squeeze() # Get rid of 1-length dimensions
57
+
58
+ # Reconstruct grid position (pos, as in position) from dy
59
+ dy = ds.coords["dy"].values
60
+ n = len(dy)
61
+ pos = np.zeros(n)
62
+ pos[0] = 0.5*dy[0]
63
+
64
+ for i in range(1,n):
65
+ pos[i] = pos[i-1] + 0.5*dy[i-1] + 0.5*dy[i]
66
+
67
+ # Set 0 to be at first cell boundary in domain
68
+ if remove_outer is True:
69
+ pos -= (pos[2] + pos[3]) / 2
70
+ else:
71
+ pos -= (pos[1] + pos[2]) / 2
72
+
73
+
74
+ ds["pos"] = (["y"], pos)
75
+
76
+ # Make pos the main coordinate instead of y
77
+ ds = ds.swap_dims({"y":"pos"})
78
+ ds.coords["pos"].attrs = ds.coords["y"].attrs
79
+
80
+ ds["pos"].attrs.update({
81
+ "conversion" : 1,
82
+ "units" : "m",
83
+ "standard_name" : "parallel position",
84
+ "long_name" : "Parallel connection length"})
85
+
86
+
87
+ # Derive and append metadata for the cross-sectional area
88
+ # and volume. The conversions are 1 because the derivation
89
+ # is from already-normalised parameters
90
+ ds["da"] = ds.dx * ds.dz * ds.J / np.sqrt(ds.g_22)
91
+ ds["da"].attrs.update({
92
+ "conversion" : 1,
93
+ "units" : "m2",
94
+ "standard_name" : "cross-sectional area",
95
+ "long_name" : "Cell parallel cross-sectional area"})
96
+
97
+ ds["dv"] = ds.J * ds.dx * ds.dy * ds.dz
98
+ ds["dv"].attrs.update({
99
+ "conversion" : 1,
100
+ "units" : "m3",
101
+ "standard_name" : "cell volume",
102
+ "long_name" : "Cell Volume"})
103
+
104
+ return ds
105
+
106
+ def extract_2d_tokamak_geometry(self):
107
+ """
108
+ Process the results to generate 2D relevant geometry data:
109
+
110
+ Calculates
111
+ ----------
112
+ - nxg and nyg, versions of ny and nx which always include guard cells
113
+ if they exist in the dataset, and do not include them if they don't
114
+ - j1_1g, j1_2g, ..., versions of jyseps which account for guards in the
115
+ same way as nxg and nyg
116
+ - x_idx, y_idx: arrays of radial and poloidal indices of all cells
117
+ - dv, dr, dthe, dl: cell volume and real space cell dimensions
118
+ - Adds target names to the metadata accounting for single or double null
119
+
120
+ Notes
121
+ ----------
122
+ - Adds copies of jyseps1_1 that are shortened to j1_1 etc.
123
+ This is potentially annoying.
124
+ - The reason it's useful to have arrays of coordinate indices
125
+ is because Xarray is surprisingly awkward when it comes to
126
+ obtaining this from the coordinates.
127
+ - This method has a hardcoded requirement for providing the grid file.
128
+
129
+ Returns
130
+ ----------
131
+ - Dataset with the new geometry
132
+ """
133
+
134
+ ds = self.data.squeeze()
135
+ m = ds.metadata
136
+
137
+ if "topology" not in m:
138
+ raise Exception(
139
+ "2D Tokamak topology missing from metadata. Please load model with the flag geometry = 'toroidal' and provide grid")
140
+
141
+ # Add theta index to coords so that both X and theta can be accessed index-wise
142
+ # It is surprisingly hard to extract the index of coordinates in Xarray...
143
+ ds.coords["theta_idx"] = (["theta"], range(len(ds.coords["theta"])))
144
+
145
+ # Extract target names. This is done here and not in load because load is not
146
+ # tokamak specific.
147
+ if "single-null" in m["topology"]:
148
+ m["targets"] = ["inner_lower", "outer_lower"]
149
+ elif "double-null" in m["topology"]:
150
+ m["targets"] = ["inner_lower", "outer_lower", "inner_upper", "outer_upper"]
151
+
152
+ num_targets = len(m["targets"])
153
+
154
+ # nyg, nxg: cell counts which are always with guard cells
155
+ # if they exist, or not if they don't
156
+ if m["keep_xboundaries"] == 0:
157
+ m["nxg"] = m["nx"] - m["MXG"] * 2
158
+ m["MXG"] = 0
159
+ else:
160
+ m["nxg"] = m["nx"]
161
+
162
+ if m["keep_yboundaries"] == 0:
163
+ m["nyg"] = m["ny"]
164
+ m["MYG"] = 0
165
+ else:
166
+ m["nyg"] = m["ny"] + m["MYG"] * num_targets
167
+
168
+ # Simplified names of the separatrix indices
169
+ m["j1_1"] = m["jyseps1_1"]
170
+ m["j1_2"] = m["jyseps1_2"]
171
+ m["j2_1"] = m["jyseps2_1"]
172
+ m["j2_2"] = m["jyseps2_2"]
173
+
174
+ # Separatrix indices which account for guard cells in the same way as nxg, nyg
175
+ m["j1_1g"] = m["j1_1"] + m["MYG"]
176
+ m["j1_2g"] = m["j1_2"] + m["MYG"] * (num_targets - 1)
177
+ m["j2_1g"] = m["j2_1"] + m["MYG"]
178
+ m["j2_2g"] = m["j2_2"] + m["MYG"] * (num_targets - 1)
179
+
180
+ # Array of radial (x) indices and of poloidal (y) indices for each cell
181
+ # This is useful because Xarray makes it awkward to extract indices in certain cases
182
+ ds["x_idx"] = (["x", "theta"], np.array([np.array(range(m["nxg"]))] * int(m["nyg"])).transpose())
183
+ ds["y_idx"] = (["x", "theta"], np.array([np.array(range(m["nyg"]))] * int(m["nxg"])))
184
+
185
+ # Cell volume calculation
186
+ ds["dv"] = (["x", "theta"], ds["dx"].data * ds["dy"].data * ds["dz"].data * ds["J"].data)
187
+ ds["dv"].attrs.update({
188
+ "conversion" : 1,
189
+ "units" : "m3",
190
+ "standard_name" : "cell volume",
191
+ "long_name" : "Cell volume",
192
+ "source" : "xHermes"})
193
+
194
+ # Cell areas in real space - comes from Jacobian
195
+ # Note: can be calculated from flux space or real space coordinates:
196
+ # dV = (hthe/Bpol) * (R*Bpol*dr) * dy*2pi = hthe * dy * dr * 2pi * R
197
+ ds["dr"] = (["x", "theta"], ds.dx.data / (ds.R.data * ds.Bpxy.data))
198
+ ds["dr"].attrs.update({
199
+ "conversion" : 1,
200
+ "units" : "m",
201
+ "standard_name" : "radial length",
202
+ "long_name" : "Length of cell in the radial direction",
203
+ "source" : "xHermes"})
204
+
205
+ ds["hthe"] = (["x", "theta"], ds["J"].data * ds["Bpxy"].data) # h_theta
206
+ ds["hthe"].attrs.update({
207
+ "conversion" : 1,
208
+ "units" : "m/radian",
209
+ "standard_name" : "h_theta: poloidal arc length per radian",
210
+ "long_name" : "h_theta: poloidal arc length per radian",
211
+ "source" : "xHermes"})
212
+
213
+ ds["dl"] = (["x", "theta"], ds["dy"].data * ds["hthe"].data) # poloidal arc length
214
+ ds["dl"].attrs.update({
215
+ "conversion" : 1,
216
+ "units" : "m",
217
+ "standard_name" : "poloidal arc length",
218
+ "long_name" : "Poloidal arc length",
219
+ "source" : "xHermes"})
220
+
221
+ return ds
222
+
223
+
224
+ @register_dataarray_accessor("hermes")
225
+ class HermesDataArrayAccessor(BoutDataArrayAccessor):
226
+ """
227
+ Methods on Hermes-3 arrays
228
+ """
229
+
230
+ def __init__(self, da):
231
+ super().__init__(da)
232
+
233
+ def unnormalise(self):
234
+ """
235
+ In-place modify data, converting to SI units
236
+
237
+ Normalisation values set in load.open_hermesdataset
238
+ in attrs['conversion']
239
+ """
240
+ units_type = self.data.attrs.get("units_type", "unknown")
241
+
242
+ if units_type == "SI":
243
+ # already un-normalised
244
+ return
245
+ elif ("units" in self.data.attrs) and ("conversion" in self.data.attrs):
246
+ # Normalise using values
247
+ self.data *= self.data.attrs["conversion"]
248
+ self.data.attrs["units_type"] = "SI"
249
+ return self
@@ -0,0 +1,425 @@
1
+ from xbout import open_boutdataset
2
+
3
+ import os
4
+
5
+
6
+ def open_hermesdataset(
7
+ datapath="./BOUT.dmp.*.nc",
8
+ inputfilepath=None,
9
+ chunks={},
10
+ run_name=None,
11
+ info=False,
12
+ unnormalise=True,
13
+ debug_variable_names = False,
14
+ **kwargs,
15
+ ):
16
+ """
17
+ Load a dataset from a set of Hermes output files
18
+
19
+ Example:
20
+ ```
21
+ import xhermes
22
+ ds = xhermes.open_hermesdataset('data/BOUT.dmp.*.nc')
23
+ ```
24
+
25
+ Returns
26
+ -------
27
+
28
+ ds : xarray.Dataset
29
+ """
30
+
31
+ ds = open_boutdataset(
32
+ datapath=datapath,
33
+ inputfilepath=inputfilepath,
34
+ chunks=chunks,
35
+ run_name=run_name,
36
+ info=info,
37
+ **kwargs,
38
+ )
39
+
40
+ # Record if guard cells read in or not
41
+ if "keep_xboundaries" in kwargs:
42
+ ds.metadata["keep_xboundaries"] = kwargs["keep_xboundaries"]
43
+ else:
44
+ ds.metadata["keep_xboundaries"] = True # Default
45
+
46
+ if "keep_yboundaries" in kwargs:
47
+ ds.metadata["keep_yboundaries"] = kwargs["keep_yboundaries"]
48
+ else:
49
+ ds.metadata["keep_yboundaries"] = False # Default
50
+
51
+ # Normalisation
52
+ meta = ds.attrs["metadata"]
53
+ Cs0 = meta["Cs0"]
54
+ Omega_ci = meta["Omega_ci"]
55
+ rho_s0 = meta["rho_s0"]
56
+ Nnorm = meta["Nnorm"]
57
+ Tnorm = meta["Tnorm"]
58
+ Bnorm = meta["Bnorm"]
59
+
60
+ # SI values
61
+ Mp = 1.67e-27 # Proton mass
62
+ e = 1.602e-19 # Coulombs
63
+
64
+
65
+
66
+ for varname in list(ds.data_vars) + list(ds.coords):
67
+ da = ds[varname]
68
+ if len(da.dims) == 4: # Time-evolving field
69
+ da.attrs["units_type"] = "hermes"
70
+
71
+ # Check if data already has units and conversion attributes
72
+ if ("units" in da.attrs) and ("conversion" in da.attrs):
73
+ if debug_variable_names is True:
74
+ print(varname + " already annotated")
75
+ continue # No need to add attributes
76
+
77
+ # Mark as Hermes-normalised data
78
+ da.attrs["units_type"] = "hermes"
79
+
80
+ if varname[:2] == "NV":
81
+ # Momentum
82
+ da.attrs.update(
83
+ {
84
+ "units": "kg m / s",
85
+ "conversion": Mp * Nnorm * Cs0,
86
+ "standard_name": "momentum",
87
+ "long_name": varname[2:] + " parallel momentum",
88
+ }
89
+ )
90
+ elif varname[0] == "N":
91
+ # Density
92
+ da.attrs.update(
93
+ {
94
+ "units": "m^-3",
95
+ "conversion": Nnorm,
96
+ "standard_name": "density",
97
+ "long_name": varname[1:] + " number density",
98
+ }
99
+ )
100
+
101
+ elif varname[0] == "T":
102
+ # Temperature
103
+ da.attrs.update(
104
+ {
105
+ "units": "eV",
106
+ "conversion": Tnorm,
107
+ "standard_name": "temperature",
108
+ "long_name": varname[1:] + " temperature",
109
+ }
110
+ )
111
+
112
+ elif varname[0] == "V":
113
+ # Velocity
114
+ da.attrs.update(
115
+ {
116
+ "units": "m / s",
117
+ "conversion": Cs0,
118
+ "standard_name": "velocity",
119
+ "long_name": varname[1:] + " parallel velocity",
120
+ }
121
+ )
122
+
123
+ elif varname[0] == "P":
124
+ # Pressure
125
+ da.attrs.update(
126
+ {
127
+ "units": "Pa",
128
+ "conversion": e * Tnorm * Nnorm,
129
+ "standard_name": "pressure",
130
+ "long_name": varname[1:] + " pressure",
131
+ }
132
+ )
133
+ elif varname == "phi":
134
+ # Potential
135
+ da.attrs.update(
136
+ {
137
+ "units": "V",
138
+ "conversion": Tnorm,
139
+ "standard_name": "potential",
140
+ "long_name": "Plasma potential",
141
+ }
142
+ )
143
+ else:
144
+ # Don't know what this is
145
+ da.attrs["units_type"] = "unknown"
146
+
147
+
148
+
149
+ # Coordinates
150
+ if varname == "dy":
151
+ # Poloidal cell angular width
152
+ da.attrs.update(
153
+ {
154
+ "units": "radian",
155
+ "conversion": 1,
156
+ "standard_name": "poloidal cell angular width",
157
+ "long_name": "Poloidal cell angular width",
158
+ }
159
+ )
160
+ elif varname == "dx":
161
+ # Radial cell width
162
+ da.attrs.update(
163
+ {
164
+ "units_type": "hermes",
165
+ "units": "Wb",
166
+ "conversion": rho_s0**2 * Bnorm,
167
+ "standard_name": "radial cell width",
168
+ "long_name": "Radial cell width in flux space",
169
+ }
170
+ )
171
+ elif varname == "dz":
172
+ # Radial cell width
173
+ da.attrs.update(
174
+ {
175
+ "units_type": "SI",
176
+ "units": "radian",
177
+ "conversion": 1,
178
+ "standard_name": "toroidal cell angular width",
179
+ "long_name": "Poloidal cell angular width",
180
+ }
181
+ )
182
+ elif varname == "J":
183
+ # Jacobian
184
+ da.attrs.update(
185
+ {
186
+ "units_type": "hermes",
187
+ "units": "m/radian T",
188
+ "conversion": rho_s0 / Bnorm,
189
+ "standard_name": "Jacobian",
190
+ "long_name": "Jacobian to translate from flux to cylindrical coordinates in real space",
191
+ }
192
+ )
193
+ elif varname == "g11":
194
+ # Metric tensor term
195
+ da.attrs.update(
196
+ {
197
+ "units_type": "hermes",
198
+ "units": "T2 m2",
199
+ "conversion": (Bnorm * rho_s0)**2,
200
+ "standard_name": "g11",
201
+ "long_name": "g11 term in the metric tensor",
202
+ }
203
+ )
204
+ elif varname == "g22":
205
+ # Metric tensor term
206
+ da.attrs.update(
207
+ {
208
+ "units_type": "hermes",
209
+ "units": "m-2",
210
+ "conversion": 1/(rho_s0)**2,
211
+ "standard_name": "g22",
212
+ "long_name": "g22 term in the metric tensor",
213
+ }
214
+ )
215
+ elif varname == "g33":
216
+ # Metric tensor term
217
+ da.attrs.update(
218
+ {
219
+ "units_type": "hermes",
220
+ "units": "m-2",
221
+ "conversion": 1/(rho_s0)**2,
222
+ "standard_name": "g33",
223
+ "long_name": "g33 term in the metric tensor",
224
+ }
225
+ )
226
+ elif varname == "g12":
227
+ # Metric tensor term
228
+ da.attrs.update(
229
+ {
230
+ "units_type": "hermes",
231
+ "units": "T",
232
+ "conversion": Bnorm,
233
+ "standard_name": "g12",
234
+ "long_name": "g12 term in the metric tensor",
235
+ }
236
+ )
237
+ elif varname == "g13":
238
+ # Metric tensor term
239
+ da.attrs.update(
240
+ {
241
+ "units_type": "hermes",
242
+ "units": "T",
243
+ "conversion": Bnorm,
244
+ "standard_name": "g13",
245
+ "long_name": "g13 term in the metric tensor",
246
+ }
247
+ )
248
+ elif varname == "g23":
249
+ # Metric tensor term
250
+ da.attrs.update(
251
+ {
252
+ "units_type": "hermes",
253
+ "units": "m-2",
254
+ "conversion": 1/(rho_s0)**2,
255
+ "standard_name": "g23",
256
+ "long_name": "g23 term in the metric tensor",
257
+ }
258
+ )
259
+ elif varname == "g_11":
260
+ # Metric tensor term
261
+ da.attrs.update(
262
+ {
263
+ "units_type": "hermes",
264
+ "units": "T-2m-2",
265
+ "conversion": 1/(Bnorm * rho_s0)**2,
266
+ "standard_name": "g_11",
267
+ "long_name": "g_11 term in the metric tensor",
268
+ }
269
+ )
270
+ elif varname == "g_22":
271
+ # Metric tensor term
272
+ da.attrs.update(
273
+ {
274
+ "units_type": "hermes",
275
+ "units": "m2",
276
+ "conversion": (rho_s0)**2,
277
+ "standard_name": "g_22",
278
+ "long_name": "g_22 term in the metric tensor",
279
+ }
280
+ )
281
+ elif varname == "g_33":
282
+ # Metric tensor term
283
+ da.attrs.update(
284
+ {
285
+ "units_type": "hermes",
286
+ "units": "m2",
287
+ "conversion": (rho_s0)**2,
288
+ "standard_name": "g_33",
289
+ "long_name": "g_33 term in the metric tensor",
290
+ }
291
+ )
292
+ elif varname == "g_12":
293
+ # Metric tensor term
294
+ da.attrs.update(
295
+ {
296
+ "units_type": "hermes",
297
+ "units": "T-1",
298
+ "conversion": 1/Bnorm,
299
+ "standard_name": "g_12",
300
+ "long_name": "g_12 term in the metric tensor",
301
+ }
302
+ )
303
+ elif varname == "g_13":
304
+ # Metric tensor term
305
+ da.attrs.update(
306
+ {
307
+ "units_type": "hermes",
308
+ "units": "T-1",
309
+ "conversion": 1/Bnorm,
310
+ "standard_name": "g_13",
311
+ "long_name": "g_13 term in the metric tensor",
312
+ }
313
+ )
314
+ elif varname == "g_23":
315
+ # Metric tensor term
316
+ da.attrs.update(
317
+ {
318
+ "units_type": "hermes",
319
+ "units": "m2",
320
+ "conversion": (rho_s0)**2,
321
+ "standard_name": "g_23",
322
+ "long_name": "g_23 term in the metric tensor",
323
+ }
324
+ )
325
+ elif varname == "t" or varname == "t_array":
326
+ # Time
327
+ da.attrs.update(
328
+ {
329
+ "units_type": "hermes",
330
+ "units": "s",
331
+ "conversion": 1/Omega_ci,
332
+ "standard_name": "time",
333
+ "long_name": "Time",
334
+ }
335
+ )
336
+
337
+ if unnormalise:
338
+ ds.hermes.unnormalise()
339
+
340
+ if ds.attrs["options"] is not None:
341
+ # Process options
342
+ options = ds.attrs["options"]
343
+
344
+ # List of components
345
+ component_list = [
346
+ c.strip() for c in options["hermes"]["components"].strip(" ()\t").split(",")
347
+ ]
348
+
349
+ # Turn into a dictionary
350
+ components = {}
351
+ for component in component_list:
352
+ if component in options:
353
+ c_opts = options[component]
354
+ if "type" in c_opts:
355
+ c_type = c_opts["type"]
356
+ else:
357
+ c_type = component # Type is the name of the component
358
+ else:
359
+ c_opts = None
360
+ c_type = component
361
+ components[component] = {"type": c_type, "options": c_opts}
362
+ ds.attrs["components"] = components
363
+
364
+ # Identify dimensions
365
+ dims = list(ds.squeeze().dims)
366
+ if "t" in dims: dims.remove("t")
367
+ meta["dimensions"] = len(dims)
368
+
369
+ # Identify species
370
+ meta["species"] = [x.split("P")[1] for x in ds.data_vars if x.startswith("P") and len(x) < 4]
371
+ meta["charged_species"] = [x for x in meta["species"] if "e" in x or "+" in x]
372
+ meta["ion_species"] = [x for x in meta["species"] if "+" in x]
373
+ meta["neutral_species"] = list(set(meta["species"]).difference(set(meta["charged_species"])))
374
+
375
+ # Prepare dictionary mapping recycling species pairs
376
+ if "recycling" in ds.attrs["components"]:
377
+ meta["recycling_pairs"] = dict()
378
+ for ion in meta["ion_species"]:
379
+ if "recycle_as" in ds.options[ion].keys():
380
+ meta["recycling_pairs"][ion] = ds.options[ion]["recycle_as"]
381
+ else:
382
+ print(f"No recycling partner found for {ion}")
383
+
384
+ return ds
385
+
386
+
387
+ def open(
388
+ path,
389
+ **kwargs,
390
+ ):
391
+ """
392
+ Open a data directory containing Hermes output files.
393
+
394
+ Example:
395
+ ```
396
+ import xhermes
397
+ ds = xhermes.open('data')
398
+ ```
399
+ where 'data' is a directory.
400
+
401
+ Can also load a grid file containing geometry information:
402
+
403
+ Example:
404
+ ```
405
+ bd = xhermes.open(".", geometry="toroidal", gridfilepath="../tokamak.nc")
406
+ ```
407
+
408
+ Parameters
409
+ ----------
410
+
411
+ path : string
412
+ A directory containing BOUT.dmp.* files and a BOUT.settings file
413
+
414
+ All other parameters are passed through to `open_hermesdataset`
415
+
416
+ Returns
417
+ -------
418
+
419
+ ds : xarray.Dataset
420
+ """
421
+ return open_hermesdataset(
422
+ datapath=os.path.join(path, "BOUT.dmp.*.nc"),
423
+ inputfilepath=os.path.join(path, "BOUT.settings"),
424
+ **kwargs,
425
+ )
@@ -0,0 +1,44 @@
1
+ import xarray
2
+
3
+ def guard_replace_1d(da):
4
+ """
5
+ Replace the inner guard cells with the values of their respective
6
+ cell edges, i.e. the values at the model inlet and at the target.
7
+ This is done by interpolating the value between the two neighbouring
8
+ cell centres. Checks for presence of guard cells if passed a DataArray.
9
+
10
+ Cell order at target:
11
+ ... | last | guard | second guard (unused)
12
+ ^target
13
+ | -3 | -2 | -1
14
+
15
+ Parameters
16
+ ----------
17
+ - da: Numpy array or Xarray DataArray with guard cells
18
+
19
+ Returns
20
+ ----------
21
+ - Numpy array with guard replacement
22
+
23
+ """
24
+
25
+ da = da.copy()
26
+
27
+ if type(da) == xarray.core.dataarray.DataArray:
28
+ if da.metadata["keep_yboundaries"] is False:
29
+ raise ValueError("Cannot guard replace if y-boundaries are not kept")
30
+
31
+ if da.name in da.coords:
32
+ raise ValueError("Cannot guard replace DataArray if it is a coordinate, try passing da.values() instead")
33
+
34
+ da[{"pos" : -2}] = (da[{"pos" : -2}] + da[{"pos" : -3}])/2
35
+ da[{"pos" : 1}] = (da[{"pos" : 1}] + da[{"pos" : 2}])/2
36
+
37
+ da = da.isel(pos = slice(1, -1))
38
+
39
+ else:
40
+ da[-2] = (da[-2] + da[-3])/2
41
+ da[1] = (da[1] + da[2])/2
42
+ da = da[1:-1]
43
+
44
+ return da
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: xhermes
3
+ Version: 0.1.0
4
+ Summary: Analyse data from Hermes-3 simulations using xarray
5
+ License: Apache
6
+ Classifier: Development Status :: 2 - Pre-Alpha
7
+ Classifier: Intended Audience :: Science/Research
8
+ Classifier: Intended Audience :: Education
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: xarray
17
+ Requires-Dist: xbout
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: license
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
24
+
25
+ # xHermes
26
+
27
+ xHermes is a post-processing package for Hermes-3 in 1D, 2D and 3D which provides automatic conversion to SI units and many useful plotting routines.
28
+
29
+ xHermes a wrapper around [xBOUT](https://github.com/boutproject/xBOUT) to provide [Hermes-3](https://github.com/bendudson/hermes-3) specific
30
+ features. At the moment, xHermes is used for loading in the Hermes-3 results, performing SI unit conversion and calculating some geometry terms.
31
+ You may still want to use xBOUT, in particular if you are running 2D and 3D cases and want plotting routines for those.
32
+
33
+ There is also a collection of Mike Kryjak's personal post-processing scripts [sdtools](https://github.com/mikekryjak/sdtools/tree/main/hermes3) which have extensive additional functionality for 1D and 2D postprocessing.
34
+ These scripts are not officially supported, but feel free to take a look.
35
+ It is intended for all those additional features to be merged into xHermes over time.
36
+
37
+ Both xBOUT and xHermes use [Xarray](https://docs.xarray.dev/en/stable/) which provides a scalable and powerful framework
38
+ for dealing with large amounts of data while preserving dimensional
39
+ consistency. If you have ever used [Pandas](https://pandas.pydata.org/), you will find Xarray very familiar, as it is effectively Pandas in more dimensions.
40
+
41
+ While Xarray is based on Numpy arrays under the hood and the data
42
+ can be accessed in this way, Xarray's main benefit comes in its
43
+ powerful abilities to select, filter and operate on data. The
44
+ syntax is similar to Pandas and the basics are explained in
45
+ Xarray's excellent documentation.
46
+ If you are new to Xarray, it may be useful to start with reviewing the [selection syntax](https://docs.xarray.dev/en/stable/user-guide/indexing.html).
47
+
48
+ ## Installation
49
+
50
+ Hermes-3 is pip installable. In the terminal with your Python environment enabled, run:
51
+
52
+ pip install xhermes
53
+
54
+ However, you may not be able to get the latest version this way. For now, it is highly recommended you do an editable install instead:
55
+
56
+ git clone https://github.com/boutproject/xhermes
57
+ cd xhermes
58
+ pip install -e .
59
+
60
+ In this way, pip will install the Python package from the xHermes folder you cloned. If you use git to checkout branches or modify the contents in any way, it will be automatically work.
61
+
62
+ ## Loading simulations
63
+
64
+ The data from a Hermes-3 run can be loaded with just
65
+
66
+ import xhermes
67
+ ds = xhermes.open(hermes_simulation_path)
68
+
69
+ where the `hermes_simulation_path` directory is assumed to contain the `BOUT.dmp.*.nc`
70
+ files and a `BOUT.settings` file. `open` returns an instance of an
71
+ `xarray.Dataset` which contains BOUT- and Hermes-specific information
72
+ in the `attrs`, so represents a general structure for storing all of
73
+ the output of a simulation, including data, input options and (soon)
74
+ grid data.
75
+
76
+ When working with 2D or 3D simulations of tokamaks, it can be useful to
77
+ load the grid along with the simulation file and pass the "toroidal" flag to xBOUT:
78
+
79
+ import xhermes
80
+ ds = xhermes.open(hermes_simulation_path, gridfilepath = grid_file_path, geometry = "toroidal")
81
+
82
+ This loads the metric tensor and grid corner coordinates from the grid which
83
+ enables geometrical operations, polygonal plotting routines and additional metadata on topology.
84
+
85
+ ## Working with the data
86
+
87
+ ### Normalisation and geometry extraction
88
+
89
+ All variables within Hermes-3 are normalised. The normalisation factors, the units and variable names
90
+ are provided in the Hermes-3 output for most variables.
91
+ This information can be accessed for each variable DataArray:
92
+
93
+ ds["Pe"].attrs
94
+
95
+ xHermes automatically applies the unnormalisation when the simulation is loaded in.
96
+
97
+ When working with 1D and 2D tokamak simulations, it can be useful to extract
98
+ cell lengths, volumes and cross-sectional areas as well as some additional metadata from the model:
99
+
100
+ ds = ds.hermes.extract_1D_tokamak_geometry()
101
+
102
+ or
103
+
104
+ ds = ds.hermes.extract_2D_tokamak_geometry()
105
+
106
+ ### The Dataset
107
+
108
+ The Dataset `ds` contains useful objects:
109
+ - A DataArray for each variable: `ds.data_vars` shows a list. `ds["Pe"]` returns the Pe DataArray.
110
+ - A DataArray for each coordinate: `ds.coords` shows a list. `ds["t"]` or `ds.coords["t"]` returns the t DataArray.
111
+ - A summary of dimensions in the model: `ds.dims`
112
+ - Model metadata: `ds.metadata`
113
+ - Model input settings: `ds.options`
114
+
115
+ ### The DataArray
116
+
117
+ The DataArray (e.g. `ds["Pe"]`) contains the array with the variable data, its dimensionality and
118
+ attributes as well as metadata and options inherited from the parent dataset.
119
+
120
+ A visual summary of the DataArray contents and size can be obtained by simply:
121
+
122
+ ds["Pe"]
123
+
124
+ Note that this may be slow due to the Xarray overhead. To access the underlying data as a Numpy array, one can use:
125
+
126
+ ds["Pe"].values
127
+
128
+ or
129
+
130
+ ds["Pe"].data
131
+
132
+ ### Plotting
133
+ Xarray contains a lot of built-in plotting routines which can be used to prepare [excellent 1D and 2D plots](https://docs.xarray.dev/en/stable/user-guide/plotting.html).
134
+ Unfortunately, it is unable to deal with 2D plots in tokamak geometry.
135
+ Routines to plot those are available in xBOUT which has some excellent [example notebooks](https://github.com/boutproject/xBOUT-examples).
136
+
137
+
138
+
@@ -0,0 +1,11 @@
1
+ README.md
2
+ setup.py
3
+ xhermes/__init__.py
4
+ xhermes/accessors.py
5
+ xhermes/load.py
6
+ xhermes/utils.py
7
+ xhermes.egg-info/PKG-INFO
8
+ xhermes.egg-info/SOURCES.txt
9
+ xhermes.egg-info/dependency_links.txt
10
+ xhermes.egg-info/requires.txt
11
+ xhermes.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ xarray
2
+ xbout
@@ -0,0 +1 @@
1
+ xhermes