osiris-utils 1.1.3__py3-none-any.whl → 1.1.6__py3-none-any.whl

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.
@@ -0,0 +1,216 @@
1
+ from matplotlib.style import available
2
+ from ..data.diagnostic import *
3
+ from ..utils import *
4
+ from ..decks.decks import InputDeckIO
5
+
6
+
7
+ class Simulation:
8
+ '''
9
+ Class to handle the simulation data. It is a wrapper for the Diagnostic class.'
10
+
11
+ Parameters
12
+ ----------
13
+ input_deck_path : str
14
+ Path to the input deck (It must be in the folder where the simulation was run)
15
+
16
+ Attributes
17
+ ----------
18
+ simulation_folder : str
19
+ The simulation folder.
20
+ species : Specie object
21
+ The species to analyze.
22
+ diagnostics : dict
23
+ Dictionary to store diagnostics for each quantity when `load_all` method is used.
24
+
25
+ Methods
26
+ -------
27
+ delete_all_diagnostics()
28
+ Delete all diagnostics.
29
+ delete_diagnostic(key)
30
+ Delete a diagnostic.
31
+ __getitem__(key)
32
+ Get a diagnostic.
33
+
34
+
35
+ '''
36
+ def __init__(self, input_deck_path):
37
+ folder_path = os.path.dirname(input_deck_path)
38
+ self._input_deck_path = input_deck_path
39
+ self._input_deck = InputDeckIO(self._input_deck_path, verbose=False)
40
+
41
+ self._species = list(self._input_deck.species.keys())
42
+
43
+ self._simulation_folder = folder_path
44
+ self._diagnostics = {} # Dictionary to store diagnostics for each quantity
45
+ self._species_handler = {}
46
+
47
+ def delete_all_diagnostics(self):
48
+ """
49
+ Delete all diagnostics.
50
+ """
51
+ self._diagnostics = {}
52
+
53
+ def delete_diagnostic(self, key):
54
+ """
55
+ Delete a diagnostic."
56
+ """
57
+ if key in self._diagnostics:
58
+ del self._diagnostics[key]
59
+ else:
60
+ print(f"Diagnostic {key} not found in simulation")
61
+
62
+ def __getitem__(self, key):
63
+ # check if key is a species
64
+ if key in self._species:
65
+ # check if species handler already exists
66
+ if key not in self._species_handler:
67
+ self._species_handler[key] = Species_Handler(self._simulation_folder, self._input_deck.species[key], self._input_deck)
68
+ return self._species_handler[key]
69
+
70
+ if key in self._diagnostics:
71
+ return self._diagnostics[key]
72
+
73
+ # Create a temporary diagnostic for this quantity - this is for quantities that are not species related
74
+ diag = Diagnostic(simulation_folder=self._simulation_folder, species=None, input_deck=self._input_deck)
75
+ diag.get_quantity(key)
76
+
77
+ original_load_all = diag.load_all
78
+
79
+ def patched_load_all(*args, **kwargs):
80
+ result = original_load_all(*args, **kwargs)
81
+ self._diagnostics[key] = diag
82
+ return diag
83
+
84
+ diag.load_all = patched_load_all
85
+
86
+ return diag
87
+
88
+ def add_diagnostic(self, diagnostic, name=None):
89
+ """
90
+ Add a custom diagnostic to the simulation.
91
+
92
+ Parameters
93
+ ----------
94
+ diagnostic : Diagnostic or array-like
95
+ The diagnostic to add. If not a Diagnostic object, it will be wrapped
96
+ in a Diagnostic object.
97
+ name : str, optional
98
+ The name to use as the key for accessing the diagnostic.
99
+ If None, an auto-generated name will be used.
100
+
101
+ Returns
102
+ -------
103
+ str
104
+ The name (key) used to store the diagnostic
105
+
106
+ Example
107
+ -------
108
+ >>> sim = Simulation('path/to/simulation', 'input_deck.txt')
109
+ >>> nT = sim['electrons']['n'] * sim['electrons']['T11']
110
+ >>> sim.add_diagnostic(nT, 'nT')
111
+ >>> sim['nT'] # Access the custom diagnostic
112
+ """
113
+ # Generate a name if none provided
114
+ if name is None:
115
+ # Find an unused name
116
+ i = 1
117
+ while f"custom_diag_{i}" in self._diagnostics:
118
+ i += 1
119
+ name = f"custom_diag_{i}"
120
+
121
+ # If already a Diagnostic, store directly
122
+ if isinstance(diagnostic, Diagnostic):
123
+ self._diagnostics[name] = diagnostic
124
+ else:
125
+ raise ValueError("Only Diagnostic objects are supported for now")
126
+
127
+ @property
128
+ def species(self):
129
+ return self._species
130
+
131
+ @property
132
+ def loaded_diagnostics(self):
133
+ return self._diagnostics
134
+
135
+ # This is to handle species related diagnostics
136
+ class Species_Handler:
137
+ def __init__(self, simulation_folder, species_name, input_deck):
138
+ self._simulation_folder = simulation_folder
139
+ self._species_name = species_name
140
+ self._input_deck = input_deck
141
+ self._diagnostics = {}
142
+
143
+ def __getitem__(self, key):
144
+ if key in self._diagnostics:
145
+ return self._diagnostics[key]
146
+
147
+ # Create a temporary diagnostic for this quantity
148
+ diag = Diagnostic(simulation_folder=self._simulation_folder, species=self._species_name, input_deck=self._input_deck)
149
+ diag.get_quantity(key)
150
+
151
+ original_load_all = diag.load_all
152
+
153
+ def patched_load_all(*args, **kwargs):
154
+ result = original_load_all(*args, **kwargs)
155
+ self._diagnostics[key] = diag
156
+ return diag
157
+
158
+ diag.load_all = patched_load_all
159
+
160
+ return diag
161
+
162
+ def add_diagnostic(self, diagnostic, name=None):
163
+ """
164
+ Add a custom diagnostic to the simulation.
165
+
166
+ Parameters
167
+ ----------
168
+ diagnostic : Diagnostic or array-like
169
+ The diagnostic to add. If not a Diagnostic object, it will be wrapped
170
+ in a Diagnostic object.
171
+ name : str, optional
172
+ The name to use as the key for accessing the diagnostic.
173
+ If None, an auto-generated name will be used.
174
+
175
+ Returns
176
+ -------
177
+ str
178
+ The name (key) used to store the diagnostic
179
+
180
+ """
181
+ # Generate a name if none provided
182
+ if name is None:
183
+ # Find an unused name
184
+ i = 1
185
+ while f"custom_diag_{i}" in self._diagnostics:
186
+ i += 1
187
+ name = f"custom_diag_{i}"
188
+
189
+ # If already a Diagnostic, store directly
190
+ if isinstance(diagnostic, Diagnostic):
191
+ self._diagnostics[name] = diagnostic
192
+ else:
193
+ raise ValueError("Only Diagnostic objects are supported for now")
194
+
195
+ def delete_diagnostic(self, key):
196
+ """
197
+ Delete a diagnostic.
198
+ """
199
+ if key in self._diagnostics:
200
+ del self._diagnostics[key]
201
+ else:
202
+ print(f"Diagnostic {key} not found in species {self._species_name}")
203
+ return None
204
+
205
+ def delete_all_diagnostics(self):
206
+ """
207
+ Delete all diagnostics.
208
+ """
209
+ self._diagnostics = {}
210
+
211
+ @property
212
+ def species(self):
213
+ return self._species_name
214
+ @property
215
+ def loaded_diagnostics(self):
216
+ return self._diagnostics
File without changes
@@ -0,0 +1,288 @@
1
+ import re
2
+ import ast
3
+ import copy
4
+ import numpy as np
5
+
6
+ from .species import Specie
7
+
8
+
9
+ def deval(x):
10
+ """
11
+ Auxiliar to handle eval of Fortran formatted numbers (e.g. 1.4d-5)
12
+ """
13
+ if "d" in x:
14
+ x = x.replace("d", "e")
15
+ return float(x)
16
+
17
+
18
+ class InputDeckIO:
19
+ """
20
+ Class to handle parsing/re-writing of OSIRIS input decks
21
+
22
+ Parameters
23
+ ----------
24
+ filename : str
25
+ Path to OSIRIS input deck file.
26
+
27
+ verbose : bool
28
+ If True, prints additional information when parsing file.
29
+ Helpful for debugging issues if input deck parsing fails.
30
+
31
+ Attributes
32
+ ----------
33
+ filename : str
34
+ Path to original input file used to create the InputDeckIO object.
35
+
36
+ sections : list[dict]
37
+ List of pairs (section_name: str, section_dict: dict) which contain
38
+ current state of InputDeckIO object.
39
+
40
+ dim : int
41
+ Number of dimensions in the simulation (1, 2, or 3).
42
+ """
43
+
44
+ def __init__(self, filename: str, verbose: bool = True):
45
+ self._filename = str(filename)
46
+ self._sections = self._parse_input_deck(verbose)
47
+ self._dim = self._get_dim()
48
+ self._species = self._get_species()
49
+
50
+ def _parse_input_deck(self, verbose):
51
+
52
+ section_list = []
53
+
54
+ if verbose:
55
+ print(f"\nParsing input deck : {self._filename}")
56
+
57
+ with open(self._filename, "r", encoding="utf-8") as f:
58
+ lines = f.readlines()
59
+
60
+ # remove comments
61
+ lines = [l[: l.find("!")] if "!" in l else l for l in lines]
62
+
63
+ # join into single string (makes it easier to parse using regex)
64
+ lines = "".join(lines)
65
+
66
+ # remove tabs/spaces/paragraphs (except spaces inside "")
67
+ lines = re.sub(
68
+ r'"[^"]*"|(\s+)', lambda x: "" if x.group(1) else x.group(0), lines
69
+ )
70
+
71
+ # split sections
72
+ # get name before brackets
73
+ section_names = re.findall(r"(?:^|\})(.*?)(?:\{)", lines)
74
+ # get content inside brackets
75
+ section_infos = re.findall(r"(?:\{)(.*?)(?:\})", lines)
76
+
77
+ if len(section_names) != len(section_infos):
78
+ raise RuntimeError(
79
+ "Unexpected problems parsing the document!\n"
80
+ f"Number of section names detected ({len(section_names)}) "
81
+ f"is different from number of sections ({len(section_infos)}).\n"
82
+ "Might be a bug in the code, or problem with input deck format!"
83
+ "Check if you could run the deck with OSIRIS."
84
+ )
85
+
86
+ # parse section information
87
+ for section, info in zip(section_names, section_infos):
88
+ if verbose:
89
+ print(f"Reading {section}")
90
+
91
+ # split section contents at commas (unless comma inside brackets e.g. pmax(1:2, 1))
92
+ info = re.split(r",(?![^()]*\))\s*", info)
93
+ info = list(filter(None, info))
94
+
95
+ # save pairs of (param, values) to dict
96
+ section_dict = {}
97
+ param = ""
98
+ value = ""
99
+
100
+ for i in info:
101
+ aux = i.split("=")
102
+ # solution to deal with parameters given by multiple values
103
+ # (e.g. ps_np(1:3) = 512,128,128 which was split into:
104
+ # ['ps_np(1:3)=512', '128', '128'])
105
+ # need to be able to regroup '128's with the previous value
106
+ if len(aux) == 1 and param != "":
107
+ value = ",".join([value, aux[0]])
108
+ section_dict[param] = value
109
+ # simplest case where we simply have "param=value"
110
+ elif len(aux) == 2:
111
+ param, value = aux
112
+ section_dict[param] = value
113
+ # case where we have multipel '=' inside strings
114
+ # happens for e.g. with mathfuncs in density profiles
115
+ else:
116
+ param = aux[0]
117
+ value = "".join(aux[1:])
118
+ # check that value is actually wrapped inside a string
119
+ if value[0] in ['"', "'"] and value[-1] in ['"', "'"]:
120
+ section_dict[param] = value
121
+ # because if not, then there is an error in the parser
122
+ # or a problem with the input deck
123
+ else:
124
+ raise RuntimeError(
125
+ f'Error parsing section: "{section}".\n'
126
+ "Might be a bug in the code, or problem with input deck format!\n"
127
+ "Check if you could run the deck with OSIRIS."
128
+ )
129
+
130
+ section_list.append([section, section_dict])
131
+
132
+ if verbose:
133
+ for k, v in section_dict.items():
134
+ print(f" {k} = {v}")
135
+
136
+ if verbose:
137
+ print("Input deck successfully parsed\n")
138
+
139
+ return section_list
140
+
141
+ def _get_dim(self):
142
+ dim = None
143
+ for i in range(1, 4):
144
+ try:
145
+ self.get_param(section="grid", param=f"nx_p(1:{i})")
146
+ except KeyError:
147
+ pass
148
+ else:
149
+ dim = i
150
+ break
151
+ if dim is None:
152
+ raise RuntimeError(
153
+ "Error parsing grid dimension. Grid dimension could not be estabilished."
154
+ )
155
+ return dim
156
+
157
+ def _get_species(self):
158
+ s_names = self.get_param("species", "name")
159
+ s_rqm = self.get_param("species", "rqm")
160
+ # real charge is optional in OSIRIS
161
+ # if real charge not provided assume electron charge
162
+ try:
163
+ s_qreal = self.get_param("species", "q_real")
164
+ s_qreal = np.array([float(q) for q in s_qreal])
165
+ except KeyError:
166
+ s_qreal = np.ones(len(s_names))
167
+ # check if we have information for all species
168
+ if len(s_names) != self.n_species:
169
+ raise RuntimeError(
170
+ "Number of specie names does not match number of species: "
171
+ f"{len(s_names)} != {len(self.n_species)}."
172
+ )
173
+ if len(s_rqm) != self.n_species:
174
+ raise RuntimeError(
175
+ "Number of specie rqm does not match number of species: "
176
+ f"{len(s_rqm)} != {len(self.n_species)}."
177
+ )
178
+ if len(s_qreal) != self.n_species:
179
+ raise RuntimeError(
180
+ "Number of specie rqm does not match number of species: "
181
+ f"{len(s_qreal)} != {len(self.n_species)}."
182
+ )
183
+
184
+ return {
185
+ ast.literal_eval(s_names[i]): Specie(
186
+ name=ast.literal_eval(s_names[i]),
187
+ rqm=float(s_rqm[i]),
188
+ q=int(s_qreal[0]) * np.sign(float(s_rqm[i])),
189
+ )
190
+ for i in range(self.n_species)
191
+ }
192
+
193
+ def set_param(self, section, param, value, i_use=None, unexistent_ok=False):
194
+ # get all sections with the same name
195
+ # (e.g. there might be multiple 'species')
196
+ i_sections = [i for i, m in enumerate(self._sections) if m[0] == section]
197
+
198
+ if len(i_sections) == 0:
199
+ raise KeyError(f'section "{section}" not found')
200
+
201
+ if i_use is not None:
202
+ try:
203
+ i_sections = [im for i, im in enumerate(i_sections) if i in i_use]
204
+ except TypeError:
205
+ i_sections = [i_sections[i_use]]
206
+
207
+ for i in i_sections:
208
+ if not unexistent_ok and param not in self._sections[i][1]:
209
+ raise KeyError(
210
+ f'"{param}" not yet inside section "{section}" '
211
+ "(set unexistent_ok=True to ignore)."
212
+ )
213
+ if isinstance(value, str):
214
+ self._sections[i][1][param] = str(f'"{value}"')
215
+ elif isinstance(value, list):
216
+ self._sections[i][1][param] = ",".join(map(str, value))
217
+ else:
218
+ self._sections[i][1][param] = str(value)
219
+
220
+ def set_tag(self, tag, value):
221
+ for im, (_, params) in enumerate(self._sections):
222
+ for p, v in params.items():
223
+ self._sections[im][1][p] = v.replace(tag, str(value))
224
+
225
+ def get_param(self, section, param):
226
+ i_sections = [i for i, m in enumerate(self._sections) if m[0] == section]
227
+
228
+ if len(i_sections) == 0:
229
+ print("section not found")
230
+ return []
231
+
232
+ values = []
233
+ for i in i_sections:
234
+ if param not in self._sections[i][1]:
235
+ raise KeyError(f'"{param}" not found inside section "{section}"')
236
+ values.append(copy.deepcopy(self._sections[i][1][param]))
237
+
238
+ return values
239
+
240
+ def delete_param(self, section, param):
241
+ sections_new = []
242
+ for m_name, m_dict in self._sections:
243
+ if m_name == section and param in m_dict:
244
+ m_dict.pop(param)
245
+ sections_new.append([m_name, m_dict])
246
+ self._sections = sections_new
247
+
248
+ def print_to_file(self, filename):
249
+ with open(filename, "w", encoding="utf-8") as f:
250
+ for section, section_dict in self._sections:
251
+ f.write(f"{section}\n{{\n")
252
+ for k, v in section_dict.items():
253
+ f.write(f'\t{k} = {v.replace(",", ", ")},\n')
254
+ f.write("}\n\n")
255
+
256
+ def __getitem__(self, section):
257
+ return copy.deepcopy([m[1] for m in self._sections if m[0] == section])
258
+
259
+ # Getters
260
+ @property
261
+ def filename(self):
262
+ return self._filename
263
+
264
+ @property
265
+ def sections(self):
266
+ return self._sections
267
+
268
+ @property
269
+ def dim(self):
270
+ return self._dim
271
+
272
+ @property
273
+ def n_species(self):
274
+ try:
275
+ return int(self["particles"][0]["num_species"])
276
+ except (KeyError, IndexError):
277
+ # If num_species doesn't exist, try num_cathode
278
+ try:
279
+ return int(self["particles"][0]["num_cathode"])
280
+ except (KeyError, IndexError):
281
+ # If neither exists, raise an informative error
282
+ raise KeyError(
283
+ "Could not find 'num_species' or 'num_cathode' in the particles section"
284
+ )
285
+
286
+ @property
287
+ def species(self):
288
+ return self._species
@@ -0,0 +1,55 @@
1
+ class Specie:
2
+ """
3
+ Class to store OSIRIS species object.
4
+
5
+ Parameters
6
+ ----------
7
+ name : str
8
+ Specie name.
9
+
10
+ rqm : float
11
+ Specie charge to mass ratio.
12
+
13
+ q : int
14
+ Specie charge in units of the electron charge.
15
+ Electrons would be represented by q=-1 and protons q=1.
16
+
17
+ Attributes
18
+ ----------
19
+ name : str
20
+ Specie name.
21
+
22
+ rqm : float
23
+ Specie charge to mass ratio.
24
+
25
+ q : int
26
+ Specie charge in units of the electron charge.
27
+
28
+ m : float
29
+ Specie mass in units of the electron mass.
30
+ """
31
+
32
+ def __init__(self, name, rqm, q: int = 1):
33
+ self._name = name
34
+ self._rqm = rqm
35
+ self._q = q
36
+ self._m = rqm * q
37
+
38
+ def __repr__(self):
39
+ return f"Specie(name={self._name}, rqm={self._rqm}, q={self._q}, m={self._m})"
40
+
41
+ @property
42
+ def name(self):
43
+ return self._name
44
+
45
+ @property
46
+ def rqm(self):
47
+ return self._rqm
48
+
49
+ @property
50
+ def q(self):
51
+ return self._q
52
+
53
+ @property
54
+ def m(self):
55
+ return self._m
File without changes