osiris-utils 1.1.2__py3-none-any.whl → 1.1.4__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,203 @@
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
+ Example
35
+ -------
36
+ >>> sim = Simulation('electrons', 'path/to/simulation')
37
+ >>> diag = sim['e1']
38
+ >>> diag.load_all()
39
+
40
+ >>> sim = Simulation('electrons', 'path/to/simulation')
41
+ >>> diag = sim['e1']
42
+ >>> diag[<index>]
43
+ '''
44
+ def __init__(self, input_deck_path):
45
+ folder_path = os.path.dirname(input_deck_path)
46
+ self._input_deck_path = input_deck_path
47
+ self._input_deck = InputDeckIO(self._input_deck_path, verbose=False)
48
+
49
+ self._species = list(self._input_deck.species.keys())
50
+
51
+ self._simulation_folder = folder_path
52
+ self._diagnostics = {} # Dictionary to store diagnostics for each quantity
53
+ self._species_handler = {}
54
+
55
+ def delete_all_diagnostics(self):
56
+ """
57
+ Delete all diagnostics.
58
+ """
59
+ self._diagnostics = {}
60
+
61
+ def delete_diagnostic(self, key):
62
+ """
63
+ Delete a diagnostic."
64
+ """
65
+ if key in self._diagnostics:
66
+ del self._diagnostics[key]
67
+ else:
68
+ print(f"Diagnostic {key} not found in simulation")
69
+
70
+ def __getitem__(self, key):
71
+ # check if key is a species
72
+ if key in self._species:
73
+ # check if species handler already exists
74
+ if key not in self._species_handler:
75
+ self._species_handler[key] = Species_Handler(self._simulation_folder, self._input_deck.species[key], self._input_deck)
76
+ return self._species_handler[key]
77
+
78
+ if key in self._diagnostics:
79
+ return self._diagnostics[key]
80
+
81
+ # Create a temporary diagnostic for this quantity - this is for quantities that are not species related
82
+ diag = Diagnostic(simulation_folder=self._simulation_folder, species=None, input_deck=self._input_deck)
83
+ diag.get_quantity(key)
84
+
85
+ original_load_all = diag.load_all
86
+
87
+ def patched_load_all(*args, **kwargs):
88
+ result = original_load_all(*args, **kwargs)
89
+ self._diagnostics[key] = diag
90
+ return diag
91
+
92
+ diag.load_all = patched_load_all
93
+
94
+ return diag
95
+
96
+ def add_diagnostic(self, diagnostic, name=None):
97
+ """
98
+ Add a custom diagnostic to the simulation.
99
+
100
+ Parameters
101
+ ----------
102
+ diagnostic : Diagnostic or array-like
103
+ The diagnostic to add. If not a Diagnostic object, it will be wrapped
104
+ in a Diagnostic object.
105
+ name : str, optional
106
+ The name to use as the key for accessing the diagnostic.
107
+ If None, an auto-generated name will be used.
108
+
109
+ Returns
110
+ -------
111
+ str
112
+ The name (key) used to store the diagnostic
113
+
114
+ Example
115
+ -------
116
+ >>> sim = Simulation('path/to/simulation', 'input_deck.txt')
117
+ >>> nT = sim['electrons']['n'] * sim['electrons']['T11']
118
+ >>> sim.add_diagnostic(nT, 'nT')
119
+ >>> sim['nT'] # Access the custom diagnostic
120
+ """
121
+ # Generate a name if none provided
122
+ if name is None:
123
+ # Find an unused name
124
+ i = 1
125
+ while f"custom_diag_{i}" in self._diagnostics:
126
+ i += 1
127
+ name = f"custom_diag_{i}"
128
+
129
+ # If already a Diagnostic, store directly
130
+ if isinstance(diagnostic, Diagnostic):
131
+ self._diagnostics[name] = diagnostic
132
+ else:
133
+ raise ValueError("Only Diagnostic objects are supported for now")
134
+
135
+ @property
136
+ def species(self):
137
+ return self._species
138
+
139
+ # This is to handle species related diagnostics
140
+ class Species_Handler:
141
+ def __init__(self, simulation_folder, species_name, input_deck):
142
+ self._simulation_folder = simulation_folder
143
+ self._species_name = species_name
144
+ self._input_deck = input_deck
145
+ self._diagnostics = {}
146
+
147
+ def __getitem__(self, key):
148
+ if key in self._diagnostics:
149
+ return self._diagnostics[key]
150
+
151
+ # Create a temporary diagnostic for this quantity
152
+ diag = Diagnostic(simulation_folder=self._simulation_folder, species=self._species_name, input_deck=self._input_deck)
153
+ diag.get_quantity(key)
154
+
155
+ original_load_all = diag.load_all
156
+
157
+ def patched_load_all(*args, **kwargs):
158
+ result = original_load_all(*args, **kwargs)
159
+ self._diagnostics[key] = diag
160
+ return diag
161
+
162
+ diag.load_all = patched_load_all
163
+
164
+ return diag
165
+
166
+ def add_diagnostic(self, diagnostic, name=None):
167
+ """
168
+ Add a custom diagnostic to the simulation.
169
+
170
+ Parameters
171
+ ----------
172
+ diagnostic : Diagnostic or array-like
173
+ The diagnostic to add. If not a Diagnostic object, it will be wrapped
174
+ in a Diagnostic object.
175
+ name : str, optional
176
+ The name to use as the key for accessing the diagnostic.
177
+ If None, an auto-generated name will be used.
178
+
179
+ Returns
180
+ -------
181
+ str
182
+ The name (key) used to store the diagnostic
183
+
184
+ Example
185
+ -------
186
+ >>> sim = Simulation('path/to/simulation', 'input_deck.txt')
187
+ >>> nT = sim['electrons']['n'] * sim['electrons']['T11']
188
+ >>> sim.add_diagnostic(nT, 'nT')
189
+ >>> sim['nT'] # Access the custom diagnostic
190
+ """
191
+ # Generate a name if none provided
192
+ if name is None:
193
+ # Find an unused name
194
+ i = 1
195
+ while f"custom_diag_{i}" in self._diagnostics:
196
+ i += 1
197
+ name = f"custom_diag_{i}"
198
+
199
+ # If already a Diagnostic, store directly
200
+ if isinstance(diagnostic, Diagnostic):
201
+ self._diagnostics[name] = diagnostic
202
+ else:
203
+ raise ValueError("Only Diagnostic objects are supported for now")
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