wolfhece 2.0.13__py3-none-any.whl → 2.0.15__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,352 @@
1
+ from enum import Enum
2
+ import logging
3
+ from math import sqrt, ceil
4
+ import numpy as np
5
+
6
+ class TilePackingMode(Enum):
7
+ REGULAR = 1
8
+ #SHUFFLED = 2
9
+ TRANSPARENT = 3 # For debugging: set it to disable the tile packing.
10
+
11
+ class TilePacker:
12
+ """
13
+ A class that packs an array in tiles and provides an indirection map
14
+
15
+ After the initialization phase based on the "NAP" array, the class provides two methods:
16
+ - shuffle_and_pack_array
17
+ - unpack_and_deshuffle_array
18
+ """
19
+
20
+
21
+ def __init__(self, nap: np.ndarray, tile_size: int, mode: TilePackingMode = TilePackingMode.REGULAR):
22
+ """
23
+ Initialize the tile indirection system.
24
+
25
+ The motivation is that usually, due to the shape of our rivers, the computation
26
+ domain (a matrix) is mostly empty. Therefore we don't use the memory very
27
+ wisely if we keep a plain rectangular domain to store a riverbed.
28
+
29
+ What we do is:
30
+ - cut the computation domain in tiles. A tile is either active or inactive.
31
+ An active tile is one that contains meshes which are marke active in the NAP map.
32
+ - translate all the active tiles so that they are close together ("packing")
33
+
34
+ Of course, once the tiles are packed, the whole hydro computation must still continue
35
+ to work. So each time the hydro computation looks for a mesh at (i,j), we must
36
+ translate those coordinates to the "packed" domain. So in this function we also
37
+ compute an "indirection" map that tells us where a (t_i, t_j) tile falls in the
38
+ packed domain.
39
+
40
+ nap: the NAP array.
41
+ """
42
+ self._tile_size = tile_size
43
+ self._mode = mode
44
+ self._height, self._width = nap.shape
45
+
46
+ # Save the NAP for reuse later on (in our case, saving it to disk in the ResultStore)
47
+ # ATTENTION don't build upon it because it eats a lot of memory. It's just here
48
+ # as a cheap placeholder.
49
+ # FIXME Later on one should store it directly from the sim to the disk
50
+ self._original_nap = nap
51
+
52
+ nap = self._pad_array_to_tiles(nap)
53
+
54
+ nb_tiles_x, nb_tiles_y = self.size_in_tiles()
55
+
56
+ # Here we cut the original NAP array into tiles.
57
+ # (I(ve tried to do it with reshape, but it seems to not being able to do the
58
+ # job. E.g. cut a 4x4 array in 2x2 tiles: np.arange(16).reshape(4,4).T.reshape(2,2,2,2) fail...)
59
+ # In the end we get a (nb tiles x, nb tiles y) array of which
60
+ # each element is a (_tile_size, _tile_size) tile array.
61
+ # To understand the stride trick, do this:
62
+ # - read the shape parameter as the shape you *want*.
63
+ # - read the strides as strides over the flattened array. The first
64
+ # two strides are to choose the tile. The other two strides are
65
+ # to enumerate the elements inside a tile.
66
+
67
+ TS = self._tile_size
68
+
69
+ # # ATTENTION stride_tricks comes with tons of gotcha's !
70
+ # # array item size (in bytes)
71
+ # ais = nap.dtype.itemsize
72
+ # tiled = np.lib.stride_tricks.as_strided(
73
+ # np.ascontiguousarray(nap), # This one is really necessary (see warning in side_tricks documentation)
74
+ # shape=((nap.shape[0]//TS,nap.shape[1]//TS,TS,TS)),
75
+ # strides=(nap.shape[1]*TS*ais, TS*ais, # choose tile
76
+ # nap.shape[1]*ais, 1*ais), # enumerate meshes inside tile
77
+ # writeable=False)
78
+ # # Determine if a tile has some active meshes inside it
79
+ # # We do that by summing the NAP values inside each tile.
80
+ # tiles_sums = np.sum(tiled, axis=(2,3))
81
+
82
+ # Replacing the above lines by more numpy-ish code.
83
+ tiles_sums = np.sum(nap.reshape(nap.shape[0]//TS,TS,nap.shape[1]//TS,TS).swapaxes(1,2), axis=(2,3))
84
+
85
+ active_tiles = np.zeros_like(tiles_sums, np.uint32)
86
+
87
+ assert active_tiles.shape == (nb_tiles_y,nb_tiles_x), f"{active_tiles.shape} ?"
88
+
89
+ active_tiles[tiles_sums > 0] = 1
90
+
91
+ # Numbering active cells (0 == inactive, >= 1 == active)
92
+
93
+ # Note that we have an edge case.
94
+
95
+ # This case occurs when we compute meshes located on the border of a
96
+ # tile of which the neighbouring border tile (call it n_t) is not
97
+ # active. When we compute that mesh, we usually have a look at its
98
+ # neighbours. In this edge case, one of the neighbours in question (call
99
+ # it n_m) will be inside the neighbouring tile (n_t) which is inactive.
100
+ # Since this tile is inactive, it will not be part of the "packed tiles"
101
+ # and thus has no representation whatsoever. Therefore, the values
102
+ # h,qx,qy,bathy,... of n_m ill be unknown.
103
+
104
+ # On a regular (non packed) computation domain it's not a problem
105
+ # because we put default/neutral values (such as water height == 0) in
106
+ # inactive meshes. So when we look outside the domain, we get these safe
107
+ # values.
108
+
109
+ # Once tiles are packed, as we have seen, we may reach out to a not
110
+ # existing, or worse, random tile. To avoid that, we choose to have an
111
+ # "emtpy" tile and number it zero. Any lookup outside the regular active
112
+ # tiles will fall on the empty tile. Adding a tile imply that if we have
113
+ # N active tiles in the computation domain we'll store N+1 tiles. So we
114
+ # have to make enough room to allow that.
115
+
116
+ self._active_tiles_ndx = np.nonzero(active_tiles) # Only indices (don't multiply by tile_size)
117
+ self._nb_active_tiles = len(self._active_tiles_ndx[0])
118
+ active_tiles[self._active_tiles_ndx] = np.arange(1, self._nb_active_tiles+1) #+1 for the "empy" tile
119
+
120
+ # Now transforming the numbers of the numbered cells in coordinates
121
+
122
+ # Indir will map a tile index (ty,tx) into its indirected x (indir[ty,tx,0]) and y ([ty,tx,1])
123
+ # coordinates
124
+ # Note : dtype can not be changed because it's used in the OpenGL shader code or you have to modify the shader code too.
125
+ indir = np.zeros( (nb_tiles_y,nb_tiles_x,2), dtype=np.uint16)
126
+
127
+ used_tiles = self._nb_active_tiles + 1
128
+ # pack tiles in a square or almost in a square
129
+ self._packed_nb_tiles_x = int(sqrt(used_tiles))
130
+ self._packed_nb_tiles_y = int(ceil(used_tiles / self._packed_nb_tiles_x))
131
+ indir[:,:,0] = active_tiles % self._packed_nb_tiles_x
132
+ indir[:,:,1] = active_tiles // self._packed_nb_tiles_x
133
+
134
+ # Convert to mesh position (this spares a multiplication
135
+ # in the shader code)
136
+ indir *= self._tile_size
137
+
138
+ # When shuffling arrays, it should be faster to read directly
139
+ # the active cells.
140
+
141
+ if self._mode == TilePackingMode.TRANSPARENT:
142
+ # For debugging purpose: create some "harmless" indirection maps.
143
+ # This is basically a transparent map
144
+ nb_tiles_x, nb_tiles_y = self.size_in_tiles()
145
+ self._packed_nb_tiles_x, self._packed_nb_tiles_y = nb_tiles_x, nb_tiles_y
146
+
147
+ xs = np.repeat( np.atleast_2d( np.arange(nb_tiles_x)), nb_tiles_y, axis=0)
148
+ ys = np.repeat( np.atleast_2d( np.arange(nb_tiles_y)), nb_tiles_x, axis=0).T
149
+ indir = np.zeros( (nb_tiles_y,nb_tiles_x,2), dtype=np.uint16)
150
+ indir[:,:,0] = xs
151
+ indir[:,:,1] = ys
152
+ indir *= self._tile_size
153
+
154
+ self._active_tiles_ndx = None
155
+ self._nb_active_tiles = None
156
+
157
+ #indir = np.roll(indir, shift=nbx//4, axis = 1)
158
+ #indir = np.roll(indir, shift=nby//4, axis = 0)
159
+
160
+ #print(indir)
161
+ self._tile_indirection_map = indir
162
+
163
+ # FIXME : why not a property ?
164
+ def tile_indirection_map(self):
165
+ return self._tile_indirection_map
166
+
167
+ # FIXME : why not a property ?
168
+ def mode(self) -> TilePackingMode:
169
+ return self._mode
170
+
171
+ # FIXME : why not a property ?
172
+ def packed_size(self):
173
+ """ Size of the arrays after padding them and packing them in tiles,
174
+ expressed in meshes. Size is a (width, height) tuple.
175
+
176
+ Note that this size can be very different than the actual computation
177
+ domain size.
178
+ """
179
+ if self._mode != TilePackingMode.TRANSPARENT:
180
+ return (self._packed_nb_tiles_x * self._tile_size,
181
+ self._packed_nb_tiles_y * self._tile_size)
182
+ else:
183
+ return (self._width, self._height)
184
+
185
+ # FIXME : why not a property ?
186
+ def packed_size_in_tiles(self):
187
+ """ Size of the arrays after padding them and packing them in tiles,
188
+ expressed in tiles. Size is a (width, height) tuple.
189
+
190
+ Note that this size can be very different than the actual computation
191
+ domain size.
192
+ """
193
+ return (self._packed_nb_tiles_x,
194
+ self._packed_nb_tiles_y)
195
+
196
+ # FIXME : why not a property ?
197
+ def size_in_tiles(self):
198
+ """
199
+ Size of the (original, non packed, non tiled) computation domain, in
200
+ tiles. Not that we count full tiles. So if one dimension of the domain
201
+ is not a multiple of the tile size, then we round one tile up.
202
+
203
+ Size is a (width, height) tuple.
204
+ """
205
+ return ((self._width +self._tile_size-1) // self._tile_size,
206
+ (self._height+self._tile_size-1) // self._tile_size)
207
+
208
+ # FIXME : why not a property ?
209
+ def tile_size(self) -> int:
210
+ """ The tile size. Note that tiles are squared.
211
+ """
212
+ return self._tile_size
213
+
214
+ # FIXME : why not a property ?
215
+ def active_tiles_ndx(self):
216
+ return self._active_tiles_ndx
217
+
218
+ def unpack_and_deshuffle_array(self, a: np.ndarray) -> np.ndarray:
219
+ """ De-shuffle and un-pad an array that was shuffled and padded.
220
+ """
221
+ psw, psh = self.packed_size()
222
+ assert a.shape[0] == psh and a.shape[1] == psw, \
223
+ f"Seems like the array you gave is not shuffled/padded. Its shape is {a.shape}. " \
224
+ f"I was expecting something with a shape like ({psh},{psw},...)"
225
+
226
+ if self._mode == TilePackingMode.TRANSPARENT:
227
+ return a
228
+
229
+ s = list(a.shape)
230
+ s[0], s[1] = self._height, self._width
231
+ r = np.zeros( tuple(s), dtype=a.dtype )
232
+
233
+ for tile_pos in zip(self._active_tiles_ndx[0], self._active_tiles_ndx[1]):
234
+
235
+ j, i = tile_pos
236
+
237
+ if (i+1)*self._tile_size > self._width:
238
+ tw = self._tile_size - ((i+1)*self._tile_size - self._width)
239
+ else:
240
+ tw = self._tile_size
241
+
242
+ if (j+1)*self._tile_size > self._height:
243
+ th = self._tile_size - ((j+1)*self._tile_size - self._height)
244
+ else:
245
+ th = self._tile_size
246
+
247
+ dest_slice_i = slice(i*self._tile_size, i*self._tile_size + tw)
248
+ dest_slice_j = slice(j*self._tile_size, j*self._tile_size + th)
249
+
250
+ tc = self._tile_indirection_map[j,i,:]
251
+ source_slice_i = slice(tc[0], tc[0]+tw)
252
+ source_slice_j = slice(tc[1], tc[1]+th)
253
+
254
+ r[dest_slice_j, dest_slice_i, ...] = a[source_slice_j, source_slice_i, ...]
255
+
256
+ return r
257
+
258
+
259
+ def _unpad_array(self, a: np.ndarray) -> np.ndarray:
260
+ """ Undo `_pad_array_to_tiles`.
261
+ """
262
+ ntx, nty = self.size_in_tiles()
263
+ assert a.shape[0] == nty*self._tile_size, "Seems like the array you gave is not padded"
264
+ assert a.shape[1] == ntx*self._tile_size, "Seems like the array you gave is not padded"
265
+ return a[0:self._height, 0:self._width]
266
+
267
+
268
+ def _pad_array_to_tiles(self, a: np.ndarray) -> np.ndarray:
269
+ """ Make an array fit in a given number of tiles (on x and y axis).
270
+ After this, the array's dimensions are multiple of the tile_size.
271
+ """
272
+
273
+ assert a.shape[0] == self._height, "The array seems to have nothing to do with a computation domain"
274
+ assert a.shape[1] == self._width, "The array seems to have nothing to do with a computation domain"
275
+ ntx, nty = self.size_in_tiles()
276
+ mesh_to_add_on_y = nty*self._tile_size - a.shape[0]
277
+ mesh_to_add_on_x = ntx*self._tile_size - a.shape[1]
278
+ assert mesh_to_add_on_y >= 0, "Your array is too tall (or there's something wrong in the tiles)"
279
+ assert mesh_to_add_on_x >= 0, "Your array is too wide (or there's something wrong in the tiles)"
280
+
281
+ if len(a.shape) == 3:
282
+ return np.pad(a, [(0,mesh_to_add_on_y), (0,mesh_to_add_on_x), (0,0)])
283
+ elif len(a.shape) == 2:
284
+ return np.pad(a, [(0,mesh_to_add_on_y), (0,mesh_to_add_on_x)])
285
+ else:
286
+ raise Exception(f"Array shape {a.shape} is not not supported")
287
+
288
+
289
+ def shuffle_and_pack_array(self, a: np.ndarray, neutral_values = None) -> np.ndarray:
290
+ """ Reorganize an array by moving tiles around to
291
+ follow the ordering given by `self._tile_indirection_map`
292
+ The array is resized in order to be just as large as
293
+ needed to hold the active tiles plus the "empty" tile.
294
+
295
+ `neutral_values`: value to fill the empty tile with.
296
+ """
297
+
298
+ if self._mode == TilePackingMode.TRANSPARENT:
299
+ return a
300
+
301
+ logging.debug(f"Packing {a.shape}")
302
+
303
+ # Padding to avoid tricky computations over "incomplete" tiles.
304
+ a = self._pad_array_to_tiles(a)
305
+
306
+ packed_shape = list(a.shape) # Preserve the third dimension, if any.
307
+ packed_shape[0] = self._packed_nb_tiles_y * self._tile_size
308
+ packed_shape[1] = self._packed_nb_tiles_x * self._tile_size
309
+
310
+ # Clearing non used tiles because they're acutally use while computing
311
+ # max step size ('cos that computation doesn't use the indirection mechanism)
312
+
313
+ # FIXME We clear too much, only the last row of tiles and the "neutral" tile
314
+ # should be cleared.
315
+
316
+ # The array containing the active tiles, packed.
317
+ if neutral_values is None:
318
+ packed_tiles = np.zeros(tuple(packed_shape), dtype=a.dtype)
319
+ else:
320
+ packed_tiles = np.empty(tuple(packed_shape), dtype=a.dtype)
321
+ packed_tiles[:,:,...] = neutral_values
322
+
323
+ # There's the 0-th tile which is meant to be neutral. So we clear it
324
+ # because 0 is mostly neutral (it depends on what `a` (the input array)
325
+ # represents. If it's h,qx,qy then it's neutral, but for bathymetry it
326
+ # may be different.
327
+
328
+ # FIXME For the moment, I believe that if h,qx,qy then, the mesh is
329
+ # neutral, regardless of the other params such as bathymetry.
330
+
331
+ if neutral_values is None:
332
+ packed_tiles[0:self._tile_size, 0:self._tile_size, ...] = 0
333
+ else:
334
+ packed_tiles[0:self._tile_size, 0:self._tile_size, ...] = neutral_values
335
+
336
+ # Go over all NAP-active tiles and pack each of them.
337
+ for tile_coord in zip(self._active_tiles_ndx[0], self._active_tiles_ndx[1]):
338
+
339
+ j,i = tile_coord
340
+ source_i = slice(i*self._tile_size, (i+1)*self._tile_size)
341
+ source_j = slice(j*self._tile_size, (j+1)*self._tile_size)
342
+
343
+ # Remember that the active tiles are numbered 1-based. The 0-th tile
344
+ # is the "neutral value" tile (used to represent out of domain, neutral data)
345
+ tc = self._tile_indirection_map[j,i,:]
346
+ dest_i = slice(tc[0], tc[0]+self._tile_size)
347
+ dest_j = slice(tc[1], tc[1]+self._tile_size)
348
+
349
+ #logging.trace(f"{a.shape} -> {packed_shape}: {dest_i}, {dest_j}")
350
+ packed_tiles[dest_j, dest_i, ...] = a[source_j, source_i, ...]
351
+
352
+ return packed_tiles
@@ -5,13 +5,16 @@ from pathlib import Path
5
5
  import tempfile
6
6
  import logging
7
7
 
8
- from wolfhece.wolf_array import WolfArray
9
- from wolfhece.PyVertexvectors import Zones, zone, vector, wolfvertex
8
+ try:
9
+ from wolfhece.wolf_array import WolfArray
10
+ from wolfhece.PyVertexvectors import Zones, zone, vector, wolfvertex
11
+ except:
12
+ logging.error('WOLF not installed !')
10
13
 
11
14
  try:
12
15
  from wolfgpu.simple_simulation import SimpleSimulation, BoundaryConditionsTypes, Direction
13
16
  except:
14
- logging.error(_('WOLFGPU not installed !'))
17
+ logging.error('WOLFGPU not installed !')
15
18
 
16
19
  class Impose_Boundary_Conditions:
17
20
  """
@@ -3,10 +3,13 @@ import ast
3
3
  import importlib.util
4
4
  from pathlib import Path
5
5
  import tempfile
6
+ import logging
6
7
 
7
- from wolfhece.wolf_array import WolfArray
8
- from wolfhece.PyVertexvectors import Zones, zone, vector, wolfvertex
9
-
8
+ try:
9
+ from wolfhece.wolf_array import WolfArray
10
+ from wolfhece.PyVertexvectors import Zones, zone, vector, wolfvertex
11
+ except:
12
+ logging.error('WOLF not installed !')
10
13
 
11
14
  class Update_Sim:
12
15
  """
@@ -0,0 +1,12 @@
1
+ #version 460
2
+
3
+ in vec2 OutTexCoords;
4
+
5
+ out vec4 outColor;
6
+ uniform sampler2D samplerTex;
7
+
8
+ void main() {
9
+
10
+ outColor = texture(samplerTex, OutTexCoords);
11
+
12
+ }
@@ -0,0 +1,32 @@
1
+ #version 330 core
2
+
3
+ layout (lines) in;
4
+ layout (triangle_strip, max_vertices = 4) out;
5
+
6
+ uniform float gridSize;
7
+ uniform vec3 gridColor;
8
+
9
+ void main()
10
+ {
11
+ vec4 lineStart = gl_in[0].gl_Position;
12
+ vec4 lineEnd = gl_in[1].gl_Position;
13
+
14
+ vec3 lineDir = normalize(lineEnd.xyz - lineStart.xyz);
15
+ vec3 lineNormal = vec3(-lineDir.y, lineDir.x, 0.0);
16
+
17
+ vec3 offset = gridSize * lineNormal;
18
+
19
+ gl_Position = lineStart + vec4(offset, 0.0);
20
+ EmitVertex();
21
+
22
+ gl_Position = lineStart - vec4(offset, 0.0);
23
+ EmitVertex();
24
+
25
+ gl_Position = lineEnd + vec4(offset, 0.0);
26
+ EmitVertex();
27
+
28
+ gl_Position = lineEnd - vec4(offset, 0.0);
29
+ EmitVertex();
30
+
31
+ EndPrimitive();
32
+ }
@@ -0,0 +1,34 @@
1
+ #version 460 core
2
+
3
+ uniform vec3 sunPosition;
4
+ uniform float sunIntensity;
5
+
6
+ in vec4 gl_FragCoord ;
7
+ in vec3 Normal;
8
+ in vec3 ourColor;
9
+ in vec3 ourCoord;
10
+
11
+ layout(location=0) out vec4 FragColor;
12
+ layout(location=1) out vec4 CoordColor;
13
+
14
+ void main() {
15
+ // Calculate the direction from the fragment position to the sun position
16
+ vec3 sunDirection = normalize(vec4(sunPosition, 1.) - gl_FragCoord).xyz;
17
+
18
+ // Calculate the dot product between the surface normal and the sun direction
19
+ float lightIntensity = dot(Normal, sunDirection) * sunIntensity;
20
+
21
+ // Calculate the reflection direction for specular lighting
22
+ vec3 viewDirection = normalize(-gl_FragCoord.xyz);
23
+ vec3 reflectionDirection = reflect(-sunDirection, Normal);
24
+
25
+ // Calculate the specular intensity using the reflection direction
26
+ float specularIntensity = pow(max(dot(reflectionDirection, viewDirection), 0.0), 32.0);
27
+
28
+ // Apply the diffuse and specular intensities to the fragment color
29
+ vec3 diffuseColor = lightIntensity * ourColor.rgb;
30
+ vec3 specularColor = specularIntensity * vec3(1.0, 1.0, 1.0);
31
+ FragColor = vec4(diffuseColor + specularColor, 1.0);
32
+ CoordColor = vec4(ourCoord, 1.);
33
+
34
+ }