kgsim 0.3.2__tar.gz → 0.3.4__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.
Files changed (32) hide show
  1. {kgsim-0.3.2 → kgsim-0.3.4}/PKG-INFO +1 -1
  2. {kgsim-0.3.2 → kgsim-0.3.4}/pyproject.toml +1 -1
  3. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/dhybridr.py +66 -4
  4. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/turbulence.py +22 -22
  5. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/fields/__init__.py +1 -0
  6. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/fields/scalar.py +22 -12
  7. kgsim-0.3.4/src/kgsim/fields/structure.py +161 -0
  8. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/fields/vector.py +15 -5
  9. {kgsim-0.3.2 → kgsim-0.3.4}/README.md +0 -0
  10. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/__init__.py +0 -0
  11. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/athena/__init__.py +0 -0
  12. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/athena/athena.py +0 -0
  13. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/athena/athena_read.py +0 -0
  14. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/__init__.py +0 -0
  15. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/anvil_submit.py +0 -0
  16. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/initializer.py +0 -0
  17. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/dhybridr/io.py +0 -0
  18. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/exceptions.py +0 -0
  19. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/fields/calc.py +0 -0
  20. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/particles.py +0 -0
  21. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/simulation.py +0 -0
  22. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/3dHybridR/config +0 -0
  23. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/3dHybridR/input/input +0 -0
  24. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/3dHybridR/sim_init_3D.py +0 -0
  25. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/3dHybridR/submit_anvil.sh +0 -0
  26. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/__init__.py +0 -0
  27. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/dHybridR/input/input +0 -0
  28. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/dHybridR/sim_init.py +0 -0
  29. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/templates/dHybridR/submit_anvil.sh +0 -0
  30. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/tristan/__init__.py +0 -0
  31. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/tristan/kappa.py +0 -0
  32. {kgsim-0.3.2 → kgsim-0.3.4}/src/kgsim/tristan/tristan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kgsim
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: A library to interact with simulations with.
5
5
  Author: Keyan Gootkin
6
6
  Author-email: Keyan Gootkin <keyangootkin@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kgsim"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "A library to interact with simulations with."
5
5
  authors = [
6
6
  { name="Keyan Gootkin", email="keyangootkin@gmail.com" },
@@ -7,11 +7,11 @@ from glob import glob
7
7
  from os import system
8
8
  from os.path import exists
9
9
  from h5py import File as h5File
10
- from numpy import mean, linspace, diff, exp, array, prod, inf, vstack, nanmin, \
11
- nanmax, append
10
+ from numpy import mean, linspace, diff, exp, array, prod, inf, vstack, nanmin, nanmax, append, \
11
+ ndarray, arange
12
12
  from numpy.random import choice
13
13
 
14
- from kgsim.fields import ScalarField, VectorField, Az
14
+ from kgsim.fields import ScalarField, VectorField, Az, single_point_single_lag_structure_function
15
15
  from kgsim.simulation import GenericSimulation, SimulationGroup
16
16
  from kgsim.particles import Species, Particle
17
17
  from kgsim.dhybridr.io import dHybridRinput, dHybridRout
@@ -22,7 +22,7 @@ from kgsim.templates import dHybridRtemplate
22
22
  from kplot import show, func_video
23
23
  from kbasic.bar import verbose_bar
24
24
  from kbasic.strings import purple
25
- from kbasic.parsing import Folder
25
+ from kbasic.parsing import Folder, Path, ensure_path
26
26
  from kbasic.vectors import Vector
27
27
 
28
28
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
@@ -35,6 +35,28 @@ track_keys = [
35
35
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
36
36
  # >-|===|> Functions <|===|-<
37
37
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
38
+ def create_dhybridr_data_file(data: ndarray, path: Path) -> h5File:
39
+ """docstring"""
40
+ with h5File(path, 'a') as file:
41
+ file['DATA'] = data
42
+ for i,si in enumerate(data.shape):
43
+ file['AXIS'][f'X{i+1} AXIS'] = [0., si]
44
+ return file
45
+
46
+ def save_dhybridr_scalar_field(field: ScalarField, destination: Path):
47
+ """docstring"""
48
+ assert exists(destination)
49
+ assert hasattr(field.parent, 'iters')
50
+ for f,it in zip(field, field.parent.iters):
51
+ fn = destination / f'{str(it).zfill(8)}_{field.name}.h5'
52
+ create_dhybridr_data_file(f, fn)
53
+
54
+ def save_dhybridr_vector_field(field: VectorField, destination: Path):
55
+ """docstring"""
56
+ save_dhybridr_scalar_field(field.x, destination/'x')
57
+ save_dhybridr_scalar_field(field.y, destination/'y')
58
+ save_dhybridr_scalar_field(field.z, destination/'z')
59
+
38
60
  # simulation parsing
39
61
  def extract_energy(file_name: str) -> tuple:
40
62
  """docstring"""
@@ -45,9 +67,13 @@ def extract_energy(file_name: str) -> tuple:
45
67
  dlne = diff(lne)[0]
46
68
  E = exp(lne)
47
69
  return E, fE, dlne
70
+
71
+
48
72
  def iters(simulation) -> list[int]:
49
73
  """Grab the iterations of each snapshot for a simulation"""
50
74
  return [int(fn[-11:-3]) for fn in simulation.density.file_names]
75
+
76
+
51
77
  def times(simulation: GenericSimulation, ndigits: int = 7) -> list[float]:
52
78
  """return the time (in simulation units) of each measurement
53
79
 
@@ -61,6 +87,8 @@ def times(simulation: GenericSimulation, ndigits: int = 7) -> list[float]:
61
87
  """
62
88
  x = list(array(iters(simulation)).astype(float) * simulation.dt)
63
89
  return [round(xi, ndigits) for xi in x]
90
+
91
+
64
92
  def particle_video(
65
93
  sim,
66
94
  particles: list[str] | int,
@@ -139,9 +167,11 @@ class dHybridRparticle(Particle):
139
167
  Particle.__init__(self, species=species, tag=tag)
140
168
  self.loaded: bool = False
141
169
  if load: self.load()
170
+
142
171
  def __len__(self) -> int:
143
172
  assert self.loaded, f"Tried to take the length of {self} without loading first"
144
173
  return len(self.x)
174
+
145
175
  def load(self):
146
176
  """docstring"""
147
177
  with h5File(self.species.path) as file:
@@ -158,6 +188,20 @@ class dHybridRparticle(Particle):
158
188
  self.B = Vector(self.B1, self.B2, self.B3)
159
189
  self.E = Vector(self.E1, self.E2, self.E3)
160
190
  self.loaded: bool = True
191
+
192
+ def structure_function(self, l):
193
+ """docstring"""
194
+ u = abs(self.parent.u)
195
+ xcells = (self.x // self.parent.dx).astype(int)
196
+ ycells = (self.y // self.parent.dy).astype(int)
197
+ pis = arange(len(self.parent)-1) * self.species.pipsi
198
+ return array([
199
+ single_point_single_lag_structure_function(
200
+ xcells[i], ycells[i], 1, u[bi], l
201
+ ) for i,bi in verbose_bar(zip(pis, range(len(self.parent))), self.parent.verbose)
202
+ ])
203
+
204
+
161
205
  class dHybridRspecies(Species):
162
206
  def __init__(
163
207
  self,
@@ -176,11 +220,15 @@ class dHybridRspecies(Species):
176
220
  if self.input.track_dump:
177
221
  self.path = parent.path+f"/Output/Tracks/Sp{self.nstr}/track_Sp{self.nstr}.h5"
178
222
  self.load()
223
+
179
224
  def __repr__(self) -> str: return self.name
225
+
180
226
  def __str__(self) -> str: return self.name
227
+
181
228
  def __len__(self) -> int:
182
229
  assert self.loaded, f"Tried to take the length of {self} without loading first"
183
230
  return len(self.tags)
231
+
184
232
  def __getitem__(self, key):
185
233
  match key:
186
234
  case str(tag): return dHybridRparticle(self, tag)
@@ -193,6 +241,7 @@ class dHybridRspecies(Species):
193
241
  1 if di is None else di
194
242
  )
195
243
  ]
244
+
196
245
  def load(self):
197
246
  """docstring"""
198
247
  with h5File(self.path) as file:
@@ -209,6 +258,8 @@ class dHybridRspecies(Species):
209
258
  ])
210
259
  if len(particle_list)==1: return particle_list[0]
211
260
  else: return particle_list
261
+
262
+
212
263
  class dHybridR(GenericSimulation):
213
264
  """
214
265
  A simulation class to interact with dHybridR simulations in python
@@ -304,11 +355,13 @@ class dHybridR(GenericSimulation):
304
355
  self.dy: float = self.input.boxsize[1]/self.input.ncells[1]
305
356
  self.dz: float = self.input.boxsize[2]/self.input.ncells[2] \
306
357
  if len(self.input.boxsize)==3 else inf
358
+
307
359
  def run(self, initializer: dHybridRinitializer, submit_script: AnvilSubmitScript) -> None:
308
360
  """docstring"""
309
361
  initializer.prepare_simulation()
310
362
  submit_script.write()
311
363
  system(f"sh {submit_script.path}")
364
+
312
365
  def parse_output(self) -> None:
313
366
  """docstring"""
314
367
  self.out = dHybridRout(self.path+"/out")
@@ -413,6 +466,15 @@ class dHybridR(GenericSimulation):
413
466
  self.start = time()
414
467
  self.iter = array(iters(self))
415
468
  self.time = array(times(self))
469
+
470
+ def save_field(self, field: ScalarField | VectorField) -> None:
471
+ """docstring"""
472
+ destination = self.output / f"Postfacto/{field.name}"
473
+ system(f"mkdir -p {destination}")
474
+ match field:
475
+ case ScalarField(): save_dhybridr_scalar_field(field, destination)
476
+ case VectorField(): save_dhybridr_vector_field(field, destination)
477
+
416
478
  def magnetic_potential_extrema(self):
417
479
  """docstring"""
418
480
  n, x = inf, -inf
@@ -45,28 +45,28 @@ def non_thermal_slope(E, f, mach):
45
45
  [slope, b], _ = curve_fit(line, log10(E), log10(f/E))
46
46
  return slope, b
47
47
 
48
- def S(p: int, u: ndarray, l: int):
49
- """
50
- Compute structure function of power p, of field u, at lag l
51
- S_p(u, l) = <(u(x) - u(x + l))^p>
52
- p: int - order of structure function
53
- u: Array - field to take structure function of
54
- l: int - lag, i.e. how many pixels away should the function look
55
- """
56
- (Nx, Ny) = u.shape
57
- assert l < min(u.shape), f"l ({l}) is larger than smallest dimension ({min(u.shape)})"
58
- grid = mgrid[-Nx//2:Nx//2, -Ny//2:Ny//2]
59
- grid = hypot(grid[0], grid[1])
60
- grid = where(grid//1 == l, True, False)
61
- in_annulus = lambda i, j: roll(grid, (i - Nx//2, j - Ny//2), axis=(0,1))
62
- # for some reason this needs to be transposed
63
- return array([
64
- [
65
- mean(
66
- (u[i, j] - u[in_annulus(i, j)]) ** p
67
- ) for i in arange(Nx)
68
- ] for j in progress_bar(arange(Ny))
69
- ]).T
48
+ # def S(p: int, u: ndarray, l: int):
49
+ # """
50
+ # Compute structure function of power p, of field u, at lag l
51
+ # S_p(u, l) = <(u(x) - u(x + l))^p>
52
+ # p: int - order of structure function
53
+ # u: Array - field to take structure function of
54
+ # l: int - lag, i.e. how many pixels away should the function look
55
+ # """
56
+ # (Nx, Ny) = u.shape
57
+ # assert l < min(u.shape), f"l ({l}) is larger than smallest dimension ({min(u.shape)})"
58
+ # grid = mgrid[-Nx//2:Nx//2, -Ny//2:Ny//2]
59
+ # grid = hypot(grid[0], grid[1])
60
+ # grid = where(grid//1 == l, True, False)
61
+ # in_annulus = lambda i, j: roll(grid, (i - Nx//2, j - Ny//2), axis=(0,1))
62
+ # # for some reason this needs to be transposed
63
+ # return array([
64
+ # [
65
+ # mean(
66
+ # (u[i, j] - u[in_annulus(i, j)]) ** p
67
+ # ) for i in arange(Nx)
68
+ # ] for j in progress_bar(arange(Ny))
69
+ # ]).T
70
70
 
71
71
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
72
72
  # >-|===|> Classes <|===|-<
@@ -1,3 +1,4 @@
1
+ from kgsim.fields.structure import *
1
2
  from kgsim.fields.scalar import *
2
3
  from kgsim.fields.vector import *
3
4
  from kgsim.fields.calc import *
@@ -3,7 +3,6 @@
3
3
  # >-|===|> Imports <|===|-<
4
4
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
5
5
  from typing import Optional, Self, Callable
6
- from glob import glob
7
6
  from functools import cached_property
8
7
  from os.path import isdir, isfile
9
8
  import logging
@@ -63,7 +62,7 @@ class ScalarField:
63
62
  caching: bool = False,
64
63
  verbose: bool = False,
65
64
  debug: bool = False,
66
- ) -> None:
65
+ ) -> None:
67
66
  self.name: str = name
68
67
  self.latex: str = latex
69
68
  self.parent = parent
@@ -82,36 +81,39 @@ class ScalarField:
82
81
  #if its a folder
83
82
  case Folder() | str() if isdir(source):
84
83
  self.path = Folder(source)
85
- self.single = False
86
84
  example_file: File = self.path.children[0]
87
85
  if example_file.extension==".h5": self._from_folder_of_h5(self.path)
88
86
  #otherwise its a file
89
87
  case File() | str() if isfile(source):
90
88
  self.path = File(source)
91
- self.single = True
92
89
  if self.path.extension=="h5": self._from_h5(self.path)
93
90
  else: self._from_csv(self.path)
94
91
  #or if its already been read
95
92
  case Array():
96
- self.single = True
97
93
  self._from_numpy(array(source))
98
94
  case _: raise TypeError(f"{source} of wrong type: {type(source)}")
99
95
  # read the stats file
100
96
  if parent: self.stats = Folder(f"{self.parent.path}/stats")
101
97
  if self.stats: self._read_stats()
102
- def __len__(self) -> int: return 1 if self.single else len(self.file_names)
98
+
99
+ def __len__(self) -> int:
100
+ if hasattr(self, 'array'):
101
+ return 1 if len(self.array.shape)==2 else len(self.array)
102
+ return len(self.file_names)
103
+
103
104
  def __iter__(self) -> Self:
104
- assert not self.single, "Cannot iterate through single scalar field"
105
105
  self.index = 0
106
106
  return self
107
+
107
108
  def __next__(self) -> NDArray:
108
109
  if self.index < len(self):
109
110
  i = self.index
110
111
  self.index += 1
111
112
  return self[i]
112
113
  else: raise StopIteration
114
+
113
115
  def __getitem__(self, item: int|slice|ArrayLike) -> NDArray:
114
- if self.single: return self.array[item]
116
+ if hasattr(self, 'array'): return self.array[item]
115
117
  match item:
116
118
  case int():
117
119
  return self.cache[item] if self.caching and item in self.cache \
@@ -132,32 +134,35 @@ class ScalarField:
132
134
  self.cache[i] if self.caching and i in self.cache \
133
135
  else self.reader(self.file_names[i], i) for i in item
134
136
  ])
137
+
135
138
  def _from_folder_of_h5(self, path: Folder) -> None:
136
139
  self.file_names: list = sorted(self.path.glob("*.h5"))
137
140
  self.reader: Callable = self._read_h5_file
138
141
  self.shape = self[0].shape
139
142
  self.ndims = len(self.shape)
140
143
  self.size = prod(self.shape) * len(self)
144
+
141
145
  def _from_h5(self, file:str) -> None:
142
146
  self.file = file.path if isinstance(file, File) else file
143
147
  self.array = self._read_h5_file(file, 0)
144
148
  self.shape = self.array.shape
145
149
  self.ndims = len(self.shape)
146
150
  self.size = prod(self.shape) * len(self)
151
+
147
152
  def _read_h5_file(self, file:str, item) -> NDArray:
148
153
  with h5File(file, 'r') as f:
149
154
  output = array(f["DATA"][:])
150
155
  #GODDMANIT I HATE THAT IT DOES Y,X and not X,Y
151
156
  if self.caching: self.cache[item] = output
152
157
  return output
153
- def _from_csv(self, source) -> None:
154
- ...
158
+
155
159
  def _read_csv_file(self, file:str) -> None: pass
160
+
156
161
  def _from_numpy(self, arr:ndarray) -> None:
157
- self.single = True
158
162
  self.array = arr
159
163
  self.shape = arr.shape
160
164
  self.ndims = len(self.shape)
165
+
161
166
  def _read_stats(self) -> None:
162
167
  ensure_path(self.stats.path)
163
168
  self.statsFile = File(f"{self.stats.path}/{self.name}.csv")
@@ -187,6 +192,7 @@ class ScalarField:
187
192
  self.min = nanmin(data[0])
188
193
  self.max = nanmax(data[1])
189
194
  savetxt(self.statsFile.path, data, delimiter=',', header="min,max,median,mean,std")
195
+
190
196
  @cached_property #cached property only is used if self.min not set by stats file
191
197
  def min(self) -> Number:
192
198
  """docstring"""
@@ -195,6 +201,7 @@ class ScalarField:
195
201
  y = append(frame, x)
196
202
  x = nanmin(y)
197
203
  return x
204
+
198
205
  @cached_property #cached property only is used if self.max not set by stats file
199
206
  def max(self) -> Number:
200
207
  """docstring"""
@@ -203,17 +210,20 @@ class ScalarField:
203
210
  y = append(frame, x)
204
211
  x = nanmax(y)
205
212
  return x
213
+
206
214
  @cached_property
207
215
  def extrema(self) -> tuple[Number]:
208
216
  """docstring"""
209
217
  return self.min, self.max
218
+
210
219
  def show(self, item:int, **kwargs) -> None:
211
220
  """docstring"""
212
221
  if hasattr(self.parent, 'dx'):
213
222
  x_ticks = arange(0, self.parent.input.boxsize[0], self.parent.dx)
214
223
  y_ticks = arange(0, self.parent.input.boxsize[1], self.parent.dy)
215
- show(self[item], x=x_ticks, y=y_ticks, **kwargs)
224
+ return show(self[item], x=x_ticks, y=y_ticks, **kwargs)
216
225
  assert False
226
+
217
227
  def movie(self, file_name=None, norm='none', cmap=default_cmap, func=None,**kwds) -> None:
218
228
  """docstring"""
219
229
  show_video(
@@ -0,0 +1,161 @@
1
+ """calculate the structure function of arrays and fields"""
2
+ # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
3
+ # >-|===|> Imports <|===|-<
4
+ # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
5
+ from math import sqrt
6
+ from collections.abc import Iterable
7
+ from kbasic import verbose_bar, Tex
8
+ from numpy import ndarray, array, mgrid, random, floor_divide, hypot, where, roll, interp, log10, \
9
+ argsort
10
+ from numpy import abs as npabs
11
+ from numpy import mean as npmean
12
+
13
+ # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
14
+ # >-|===|> Functions <|===|-<
15
+ # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
16
+ def select_random_pixels(image: ndarray, fill_fraction):
17
+ """select fill_fraction of pixels of image randomly"""
18
+ (Nx, Ny) = image.shape
19
+ if fill_fraction==1:
20
+ X2D, Y2D = mgrid[:Nx, :Ny]
21
+ x, y = X2D.flatten(), Y2D.flatten()
22
+ return x, y
23
+ pixel_numbers = random.choice(
24
+ image.size,
25
+ size=int((image.size*fill_fraction)//1),
26
+ replace=False
27
+ )
28
+ x = floor_divide(pixel_numbers, Nx)
29
+ y = pixel_numbers % Nx
30
+ return x, y
31
+
32
+ def single_point_single_lag_structure_function(i, j, p, u, l):
33
+ """docstring"""
34
+ (Nx, Ny) = u.shape
35
+ # prepare annulus selection function
36
+ grid = mgrid[-Nx//2:Nx//2, -Ny//2:Ny//2]
37
+ grid = hypot(grid[0], grid[1])
38
+ grid = where(grid//1 == l, True, False)
39
+ in_annulus = lambda i, j: roll(grid, (i - Nx//2, j - Ny//2), axis=(0,1))
40
+ return npmean(npabs(u[i,j] - u[in_annulus(i,j)])**p)
41
+
42
+ def single_structure_function(p, u, l, sample, verbose, _is_multi: bool = False):
43
+ """
44
+ for a single value of l, return the structure function
45
+
46
+ Args:
47
+ p (int): structure function order
48
+ u (ndarray): the field to compute the structure function for
49
+ l (int): lag
50
+ sample (str|float): 'all' if you want to sample every point else the fraction
51
+ of possible points to select
52
+ verbose (bool): whether to provide verbose output
53
+
54
+ Returns:
55
+ ndarray: The p^th order structure function of u with lag l
56
+ """
57
+ (Nx, Ny) = u.shape
58
+ # select the pixels to calculate the structure function for
59
+ x_selected, y_selected = select_random_pixels(u, 1 if sample=='all' else sample)
60
+ # prepare annulus selection function
61
+ grid = mgrid[-Nx//2:Nx//2, -Ny//2:Ny//2]
62
+ grid = hypot(grid[0], grid[1])
63
+ grid = where(grid//1 == l, True, False)
64
+ in_annulus = lambda i, j: roll(grid, (i - Nx//2, j - Ny//2), axis=(0,1))
65
+ if sample=='all':
66
+ return array([
67
+ # for pixel i,j
68
+ npmean(
69
+ # take the mean of |u_ij - u_annulus|^p for all values of u in the annulus
70
+ npabs(u[i, j] - u[in_annulus(i, j)]) ** p
71
+ ) for i,j in verbose_bar(zip(x_selected, y_selected), verbose, total=len(x_selected))
72
+ ]).reshape(u.shape)
73
+ br = verbose_bar(
74
+ zip(x_selected, y_selected),
75
+ verbose,
76
+ desc=Tex(rf"calculate \mathcal[S]^{p}(\ell={l})").string,
77
+ total=len(x_selected),
78
+ position=1,
79
+ leave=False
80
+ )
81
+ # take the mean of |u_ij - u_annulus|^p for all selected pixels
82
+ return npmean(array([
83
+ npmean(
84
+ npabs(u[i, j] - u[in_annulus(i, j)]) ** p
85
+ ) for i,j in br
86
+ ]))
87
+
88
+ def S(
89
+ p: int,
90
+ u: ndarray,
91
+ l: int|ndarray|list,
92
+ sample: str|float = "all",
93
+ verbose: bool = True
94
+ ):
95
+ """structure function factory
96
+
97
+ Args:
98
+ p (int): _description_
99
+ u (ndarray): _description_
100
+ l (int | ndarray | list): _description_
101
+ sample (str | float, optional): _description_. Defaults to "all".
102
+ verbose (bool, optional): _description_. Defaults to True.
103
+
104
+ Returns:
105
+ _type_: _description_
106
+ """
107
+ match l:
108
+ # if l is a single number
109
+ case int(): return single_structure_function(p,u,l,sample,verbose)
110
+ # if we want to calculate a series of l's
111
+ case Iterable():
112
+ lbar = verbose_bar(l, verbose, total=len(l), position=0)
113
+ return array([
114
+ single_structure_function(p,u,li,sample,False,_is_multi=True) for li in lbar
115
+ ])
116
+
117
+ def log_interp(x_interp, x, y):
118
+ """perform interpolation in logspace"""
119
+ return 10**interp(log10(x_interp), log10(x), log10(y))
120
+
121
+ def sonic_scale(u, sample=0.01, tolerance=0.05, Ntries=10, return_spectrum=False, verbose=True):
122
+ """compute the lag at which S(1, u, l)=1"""
123
+ du = lambda li: S(1, u, li, verbose=False, sample=sample)
124
+
125
+ left = 1
126
+ Sl = du(left)
127
+ right = int(max(u.shape)//sqrt(2))
128
+ Sr = du(right)
129
+ assert Sl < 1 and Sr > 1, f"Sonic point {'above' if Sr < 1 else 'below'} inertial range of \
130
+ {left}-{right} and {Sl}-{Sr}."
131
+
132
+ guess = int(log_interp(1, [Sl, Sr], [left, right])//1)
133
+ Sg = du(guess)
134
+ Ng = 0
135
+
136
+ all_l, all_s = [left, guess, right], [Sl, Sg, Sr]
137
+
138
+ while npabs(Sg-1) > tolerance:
139
+ if Sg>1:
140
+ Sr=Sg
141
+ right=guess
142
+ else:
143
+ Sl=Sg
144
+ left=guess
145
+
146
+ if left-right==1:
147
+ if verbose: print("didn't hit tolerance, but guess range is 1")
148
+ break
149
+ guess = int(log_interp(1, [Sl, Sr], [left, right])//1)
150
+ all_l.append(guess)
151
+ Sg = du(guess)
152
+ all_s.append(Sg)
153
+ Ng+=1
154
+ if Ng>=Ntries:
155
+ if verbose: print(f"didn't hit tolerance of {tolerance} in {Ntries} tries")
156
+ if verbose: print(f"S({guess})={Sg}")
157
+ break
158
+ if return_spectrum:
159
+ sort = argsort(all_l)
160
+ return guess, array(all_l)[sort], array(all_s)[sort]
161
+ return guess
@@ -64,13 +64,15 @@ class VectorField:
64
64
  self.caching = caching
65
65
  # set self.components, self.ndims, self.size
66
66
  self._parse_components(components)
67
+
67
68
  def __len__(self) -> int: return min([len(c) for c in self.components])
69
+
68
70
  def __abs__(self) -> NDArray | ScalarField:
69
71
  if hasattr(self, 'magnitude'):
70
- return self.magnitude
71
- if (p:=self.path / "Intensity").exists:
72
+ return getattr(self, 'magnitude')
73
+ if type(p:=self.path / "Intensity")==Folder:
72
74
  self.magnitude = ScalarField(
73
- p,
75
+ p,
74
76
  name=self.name+'_magnitude',
75
77
  latex=f"$|{self.latex}|$",
76
78
  parent=self.parent,
@@ -79,8 +81,9 @@ class VectorField:
79
81
  debug=self.debug
80
82
  )
81
83
  return self.magnitude
84
+ self.magnitude = _vector_mag(self)
85
+ return self.magnitude
82
86
 
83
- return _vector_mag(self)
84
87
  def __getitem__(self, item: int|slice) -> NDArray:
85
88
  match type(item):
86
89
  case int(): return array([c[item] for c in self.components])
@@ -97,6 +100,7 @@ class VectorField:
97
100
  c[i] for c in self.components
98
101
  ] for i in item_iters
99
102
  ])
103
+
100
104
  def _parse_components(self, source: tuple) -> None:
101
105
  """
102
106
  Parse user input components into class attributes
@@ -153,6 +157,7 @@ class VectorField:
153
157
  for j in range(other.shape[1])]
154
158
  for k in verbose_bar(range(len(other)), self.verbose, desc="constructing B...")])
155
159
  return sum(A * B, axis=2)
160
+
156
161
  def cross(self, other: Self, k:int) -> NDArray:
157
162
  if type(other)==VectorField:
158
163
  assert self.ndims==3, "only 3D vector fields can be crossed at this time"
@@ -162,11 +167,13 @@ class VectorField:
162
167
  self.z[k]*other.x[k] - self.x[k]*other.z[k],
163
168
  self.x[k]*other.y[k] - self.y[k]*other.x[k]
164
169
  ])
170
+
165
171
  @cached_property
166
172
  def potential(self) -> NDArray:
167
173
  match len(self.x.shape):
168
174
  case 2:
169
175
  return array([Az(self.x[i], self.y[i], dx=self.dx, dy=self.dy) for i in range(len(self))])
176
+
170
177
  def calc_perp(self, item=None) -> NDArray:
171
178
  if not item:
172
179
  self.perp = array([
@@ -185,6 +192,7 @@ class VectorField:
185
192
  ]
186
193
  self.perp = array([hypot(self.perpendicular[0][j], self.perpendicular[1][j]) for j in item_iters])
187
194
  else: raise TypeError(f"calc_perp only takes ints, slices, or None for item, not {type(item)}-type objects")
195
+
188
196
  def set_parallel(self, component:str) -> None:
189
197
  match component.lower():
190
198
  case 'x':
@@ -196,6 +204,7 @@ class VectorField:
196
204
  case 'z':
197
205
  self.parallel = None if not hasattr(self, 'z') else self.z
198
206
  self.perpendicular = self.x, self.y
207
+
199
208
  def movie(self, mode='mag', norm='none', cmap=default_cmap, **kwrg) -> None:
200
209
  match mode.lower():
201
210
  case 'mag'|'magnitude'|'abs':
@@ -214,6 +223,7 @@ class VectorField:
214
223
  )
215
224
  def reveal_thyself(s, **kwargs): return array([self.parallel[i] for i in range(len(self))])
216
225
  reveal_thyself(self if self.parent is None else self.parent, **kwrg)
226
+
217
227
  def quiver(
218
228
  self,
219
229
  ind: int,
@@ -225,7 +235,7 @@ class VectorField:
225
235
  transpose: bool = False,
226
236
  #everything else goes into matplotlib command
227
237
  **kwargs
228
- ) -> None:
238
+ ) -> None:
229
239
  if not ax: ax = gca()
230
240
  match x, y:
231
241
  case None, None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes