geone 1.2.10__py311-none-manylinux_2_35_x86_64.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.
geone/tools.py ADDED
@@ -0,0 +1,762 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # -------------------------------------------------------------------------
5
+ # Python module: 'tools.py'
6
+ # author: Julien Straubhaar
7
+ # date: nov-2023
8
+ # -------------------------------------------------------------------------
9
+
10
+ """
11
+ Module for miscellaneous tools.
12
+ """
13
+
14
+ import sys
15
+ import multiprocessing
16
+
17
+ import numpy as np
18
+ import matplotlib.pyplot as plt
19
+ from matplotlib.backend_bases import MouseButton
20
+
21
+ from geone import img
22
+
23
+ # -----------------------------------------------------------------------------
24
+ def add_path_by_drawing(path_list, close=False, show_instructions=True, last_point_marker='o', last_point_color='red', **kwargs):
25
+ """
26
+ Add paths in a list, by interatively drawing on a plot.
27
+
28
+ The first argument of the function is a list that is updated when the
29
+ function is called by appending one (or more) path(s) (see notes below).
30
+ A path is a 2D array of floats with two columns, each row is (the x and y
31
+ coordinates of) a point. A path is interactively determined on the plot on
32
+ the current axis (get with `matplotlib.pyplot.gca()`), with the following
33
+ rules:
34
+
35
+ - left click: add the nex point (or first one)
36
+ - right click: remove the last point
37
+
38
+ When pressing a key:
39
+
40
+ - key n/N: terminate the current path, and start a new path
41
+ - key ENTER (or other): terminate the current path and exits
42
+
43
+ Parameters
44
+ ----------
45
+ path_list : list
46
+ list of paths, that will be updated by appending the path interactively
47
+ drawn in the current axis (one can start with `path_list = []`)
48
+
49
+ close : bool, default: False
50
+ if `True`: when a path is terminated, the first points of the path is
51
+ replicated at the end of the path to form a close line / path
52
+
53
+ show_instructions : bool, default: True
54
+ if `True`: instructions are printed in the standard output
55
+
56
+ last_point_marker : "matplotlib marker", default: 'o'
57
+ marker used for highlighting the last clicked point
58
+
59
+ last_point_color : "matplotlib color", default: 'red'
60
+ color used for highlighting the last clicked point
61
+
62
+ kwargs : dict
63
+ keyword arguments passed to `matplotlib.pyplot.plot` to plot the path(s)
64
+
65
+ Notes
66
+ -----
67
+ * The function does not return anything. The first argument, `path_list` is \
68
+ updated, with the path(s) drawn interactively, for example `path_list[-1]` \
69
+ is the last path, a 2D array, where `path_list[-1][i]` is the i-th point \
70
+ (1D array of two floats) of that path
71
+ * An interactive maplotlib backend must be used, so that this function works \
72
+ properly
73
+ """
74
+ ax = plt.gca()
75
+ obj_drawn = []
76
+ x, y = [], []
77
+
78
+ set_default_color = False
79
+ if 'color' not in kwargs.keys() and 'c' not in kwargs.keys():
80
+ set_default_color = True
81
+ col = plt.rcParams['axes.prop_cycle'].by_key()['color']
82
+ ncol = len(col)
83
+ col_ind = [0]
84
+ kwargs['color'] = col[col_ind[0]] # default color for lines (segments)
85
+ # if 'color' not in kwargs.keys() and 'c' not in kwargs.keys():
86
+ # kwargs['color'] = 'tab:blue' # default color for lines (segments)
87
+
88
+ if show_instructions:
89
+ instruct0 = '\n'.join([' Left click: add next point', ' Right click: remove last point', ' Key n/N: select a new line', ' key ENTER (or other): finish (quit)'])
90
+ print('\n'.join(['Draw path', instruct0]))
91
+ sys.stdout.flush()
92
+
93
+ def on_click(event):
94
+ if not event.inaxes:
95
+ return
96
+ if event.button is MouseButton.LEFT:
97
+ if len(x):
98
+ # remove last point from obj_drawn
99
+ ax.lines[-1].remove()
100
+ # add clicked point
101
+ x.append(event.xdata)
102
+ y.append(event.ydata)
103
+ if len(x) > 1:
104
+ # add line (segment) to obj_drawn
105
+ obj_drawn.append(plt.plot(x[-2:], y[-2:], **kwargs))
106
+ # add point to obj_drawn
107
+ obj_drawn.append(plt.plot(x[-1], y[-1], marker=last_point_marker, color=last_point_color))
108
+ if event.button is MouseButton.RIGHT:
109
+ if len(x):
110
+ # remove last clicked point
111
+ del x[-1], y[-1]
112
+ # remove last point from obj_drawn
113
+ ax.lines[-1].remove()
114
+ if len(x):
115
+ # point(s) are still in the line
116
+ # remove last line (segment) from obj_drawn
117
+ ax.lines[-1].remove()
118
+ # add last point to obj_drawn
119
+ obj_drawn.append(plt.plot(x[-1], y[-1], marker=last_point_marker, color=last_point_color))
120
+
121
+ def on_key(event):
122
+ if len(x):
123
+ # remove last point from obj_drawn
124
+ ax.lines[-1].remove()
125
+ if close:
126
+ # close the line
127
+ if len(x):
128
+ x.append(x[0])
129
+ y.append(y[0])
130
+ # add line (closing segment) to obj_drawn
131
+ obj_drawn.append(plt.plot(x[-2:], y[-2:], **kwargs))
132
+ # add path to path_list
133
+ if len(x):
134
+ path_list.append(np.array((x, y)).T)
135
+ if event.key.lower() == 'n':
136
+ if show_instructions:
137
+ print('\n'.join(['Draw next path', instruct0]))
138
+ sys.stdout.flush()
139
+ x.clear() # Do not use: x = [], because x is global for this function!
140
+ y.clear()
141
+ if set_default_color:
142
+ # Set color for next line
143
+ col_ind[0] = (col_ind[0]+1)%ncol
144
+ kwargs['color'] = col[col_ind[0]] # default color for lines (segments)
145
+ return
146
+
147
+ plt.disconnect(cid_click)
148
+ plt.disconnect(cid_key)
149
+ return
150
+ # if
151
+ # path_list.append(np.array((x, y)).T)
152
+ # cid_click = plt.connect('button_press_event', on_click)
153
+
154
+
155
+ cid_click = plt.connect('button_press_event', on_click)
156
+ cid_key = plt.connect('key_press_event', on_key)
157
+ # -----------------------------------------------------------------------------
158
+
159
+ # -----------------------------------------------------------------------------
160
+ def is_in_polygon(x, vertices, wrap=None, **kwargs):
161
+ """
162
+ Checks if point(s) is (are) in a polygon given by its vertices forming a close line.
163
+
164
+ To check if a point is in the polygon, the method consists in computing
165
+ the vectors from the given point to the vertices of the polygon and
166
+ the sum of signed angles between two successives vectors (and the last
167
+ and first one). Then, the point is in the polygon if and only if this sum
168
+ is equal to +/- 2 pi.
169
+
170
+ Parameters
171
+ ----------
172
+ x : 2D array-like or 1D array-like
173
+ point(s) coordinates, each row `x[i]` (if 2D array-like) (or `x` if
174
+ 1D array-like) contains the two coordinates of one point
175
+
176
+ vertices : 2D array
177
+ vertices of a polygon in 2D, each row of `vertices` contains the two
178
+ coordinates of one vertex; the segments of the polygon are obtained by
179
+ linking two successive vertices (as well as the last one with the first
180
+ one, if `wrap=True` (see below)), so that the vertices form a close line
181
+ (clockwise or counterclockwise)
182
+
183
+ wrap : bool, optional
184
+ - if `True`: last and first vertices has to be linked to form a close line
185
+ - if `False`: last and first vertices should be the same ones (i.e. the \
186
+ vertices form a close line);
187
+
188
+ by default (`None`): `wrap` is automatically computed
189
+
190
+ kwargs :
191
+ keyword arguments passed to function `numpy.isclose`
192
+
193
+ Returns
194
+ -------
195
+ out : 1D array of bools, or bool
196
+ indicates for each point in `x` if it is inside (True) or outside (False)
197
+ the polygon;
198
+ note: if `x` is of shape (m, 2), then `out` is a 1D array of shape (m, ),
199
+ and if `x` is of shape (2, ) (one point), `out` is bool
200
+ """
201
+ # Set wrap (and adjust vertices) if needed
202
+ if wrap is None:
203
+ wrap = ~np.isclose(np.sqrt(((vertices[-1] - vertices[0])**2).sum()), 0.0)
204
+ if not wrap:
205
+ # remove last vertice (should be equal to the first one)
206
+ vertices = np.delete(vertices, -1, axis=0)
207
+
208
+ # Array of shifted indices (on vertices)
209
+ ind = np.hstack((np.arange(1, vertices.shape[0]), [0]))
210
+
211
+ # Initialization
212
+ xx = np.atleast_2d(x)
213
+ res = np.zeros(xx.shape[0], dtype='bool')
214
+
215
+ xmin, ymin = vertices.min(axis=0)
216
+ xmax, ymax = vertices.max(axis=0)
217
+
218
+ for j, xj in enumerate(xx):
219
+ if xj[0] < xmin or xj[0] > xmax or xj[1] < ymin or xj[1] > ymax:
220
+ continue
221
+
222
+ # Set a, b, c: edge of the triangles to compute angle between a and b
223
+ a = xj - vertices
224
+
225
+ # a_norm2: square norm of a
226
+ a_norm2 = (a**2).sum(axis=1)
227
+
228
+ b = a[ind]
229
+ b_norm2 = a_norm2[ind]
230
+ ab_norm = np.sqrt(a_norm2*b_norm2)
231
+
232
+ if np.any(np.isclose(ab_norm, 0, **kwargs)):
233
+ # xj on the border of the polygon
234
+ continue
235
+
236
+ # Compute the sum of angles using the theorem of cosine
237
+ c = b - a
238
+ c_norm2 = (c**2).sum(axis=1)
239
+ sign = 2*(a[:,0]*b[:,1] - a[:,1]*b[:,0] > 0) - 1
240
+ sum_angles = np.sum(sign*np.arccos(np.minimum(1.0, np.maximum(-1.0, (a_norm2 + b_norm2- c_norm2)/(2.0*ab_norm)))))
241
+
242
+ res[j] = np.isclose(np.abs(sum_angles), 2*np.pi)
243
+
244
+ if np.asarray(x).ndim == 1:
245
+ res = res[0]
246
+
247
+ return res
248
+ # -----------------------------------------------------------------------------
249
+
250
+ # -----------------------------------------------------------------------------
251
+ def is_in_polygon_mp(x, vertices, wrap=None, nproc=-1, **kwargs):
252
+ """
253
+ Computes the same as the function :func:`tools.is_in_polygon`, using multiprocessing.
254
+
255
+ All the parameters except `nproc` are the same as those of the function
256
+ :func:`tools.is_in_polygon`.
257
+
258
+ The number of processes used (in parallel) is n, and determined by the
259
+ parameter `nproc` (int, optional) as follows:
260
+
261
+ - if `nproc > 0`: n = `nproc`,
262
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
263
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
264
+ i.e. all cpus except `-nproc` is used (but at least one)
265
+
266
+ See function :func:`tools.is_in_polygon`.
267
+ """
268
+ # Set wrap (and adjust vertices) if needed
269
+ if wrap is None:
270
+ wrap = ~np.isclose(np.sqrt(((vertices[-1] - vertices[0])**2).sum()), 0.0)
271
+ if not wrap:
272
+ # remove last vertice (should be equal to the first one)
273
+ vertices = np.delete(vertices, -1, axis=0)
274
+
275
+ # Set wrap key in keywords arguments
276
+ kwargs['wrap'] = True
277
+
278
+ # Initialization
279
+ xx = np.atleast_2d(x)
280
+
281
+ # Set number of processes (n)
282
+ if nproc > 0:
283
+ n = nproc
284
+ else:
285
+ n = min(multiprocessing.cpu_count()+nproc, 1)
286
+
287
+ # Set index for distributing tasks
288
+ q, r = np.divmod(xx.shape[0], n)
289
+ ids_proc = [i*q + min(i, r) for i in range(n+1)]
290
+
291
+ # Set pool of n workers
292
+ pool = multiprocessing.Pool(n)
293
+ out_pool = []
294
+ for i in range(n):
295
+ # Set i-th process
296
+ out_pool.append(pool.apply_async(is_in_polygon, args=(xx[ids_proc[i]:ids_proc[i+1]], vertices), kwds=kwargs))
297
+
298
+ # Properly end working process
299
+ pool.close() # Prevents any more tasks from being submitted to the pool,
300
+ pool.join() # then, wait for the worker processes to exit.
301
+
302
+ # Get result from each process
303
+ res = np.hstack([w.get() for w in out_pool])
304
+
305
+ if np.asarray(x).ndim == 1:
306
+ res = res[0]
307
+
308
+ return res
309
+ # -----------------------------------------------------------------------------
310
+
311
+ # -----------------------------------------------------------------------------
312
+ def rasterize_polygon_2d(vertices,
313
+ nx=None, ny=None,
314
+ sx=None, sy=None,
315
+ ox=None, oy=None,
316
+ xmin_ext=0.0, xmax_ext=0.0,
317
+ ymin_ext=0.0, ymax_ext=0.0,
318
+ wrap=None, **kwargs):
319
+ """
320
+ Rasterizes a polygon (close line) in a 2D grid.
321
+
322
+ This function returns an image with one variable indicating for each cell
323
+ if it is inside (1) or outside (0) the polygon defined by the given
324
+ vertices.
325
+
326
+ The grid geometry of the output image is set by the given parameters or
327
+ computed from the vertices, as in function :func:`img.imageFromPoints`,
328
+ i.e. for the x axis (similar for y):
329
+
330
+ - `ox` (origin), `nx` (number of cells) and `sx` (resolution, cell size)
331
+ - or only `nx`: `ox` and `sx` automatically computed
332
+ - or only `sx`: `ox` and `nx` automatically computed
333
+
334
+ In the two last cases, the parameters `xmin_ext`, `xmax_ext`, are used and
335
+ the approximate limit of the grid along x axis is set to x0, x1, where
336
+
337
+ - x0: min x coordinate of the vertices minus `xmin_ext`
338
+ - x1: max x coordinate of the vertices plus `xmax_ext`
339
+
340
+ Parameters
341
+ ----------
342
+ vertices : 2D array
343
+ vertices of a polygon in 2D, each row of `vertices` contains the two
344
+ coordinates of one vertex; the segments of the polygon are obtained by
345
+ linking two successive vertices (as well as the last one with the first
346
+ one, if `wrap=True` (see below)), so that the vertices form a close line
347
+ (clockwise or counterclockwise)
348
+
349
+ nx : int, optional
350
+ number of grid cells along x axis; see above for possible inputs
351
+
352
+ ny : int
353
+ number of grid cells along y axis; see above for possible inputs
354
+
355
+ sx : float
356
+ cell size along x axis; see above for possible inputs
357
+
358
+ sy : float
359
+ cell size along y axis; see above for possible inputs
360
+
361
+ ox : float
362
+ origin of the grid along x axis (x coordinate of cell border);
363
+ see above for possible
364
+
365
+ oy : float
366
+ origin of the grid along y axis (y coordinate of cell border);
367
+ see above for possible
368
+
369
+ Note: `(ox, oy)` is the "lower-left" corner of the grid
370
+
371
+ xmin_ext : float, default: 0.0
372
+ extension beyond the min x coordinate of the vertices (see above)
373
+
374
+ xmax_ext : float, default: 0.0
375
+ extension beyond the max x coordinate of the vertices (see above)
376
+
377
+ ymin_ext : float, default: 0.0
378
+ extension beyond the min y coordinate of the vertices (see above)
379
+
380
+ ymax_ext : float, default: 0.0
381
+ extension beyond the max y coordinate of the vertices (see above)
382
+
383
+ wrap : bool, optional
384
+ - if `True`: last and first vertices has to be linked to form a close line
385
+ - if `False`: last and first vertices should be the same ones (i.e. the \
386
+ vertices form a close line);
387
+
388
+ by default (`None`): `wrap` is automatically computed
389
+
390
+ kwargs:
391
+ keyword arguments passed to function :func:`tools.is_in_polygon`
392
+
393
+ Returns
394
+ -------
395
+ im : :class:`geone.img.Img`
396
+ output image (see above);
397
+ note: the image grid is defined in 3D with `nz=1`, `sz=1.0`, `oz=-0.5`
398
+ """
399
+ # Define grid geometry (image with no variable)
400
+ im = img.imageFromPoints(vertices,
401
+ nx=nx, ny=ny, sx=sx, sy=sy, ox=ox, oy=oy,
402
+ xmin_ext=xmin_ext, xmax_ext=xmax_ext,
403
+ ymin_ext=ymin_ext, ymax_ext=ymax_ext)
404
+
405
+ # Rasterize: for each cell, check if its center is within the grid
406
+ v = np.asarray(is_in_polygon(np.array((im.xx().reshape(-1), im.yy().reshape(-1))).T, vertices, wrap=wrap, **kwargs)).astype('float')
407
+ im.append_var(v, varname='in')
408
+
409
+ return im
410
+ # -----------------------------------------------------------------------------
411
+
412
+ # -----------------------------------------------------------------------------
413
+ def rasterize_polygon_2d_mp(vertices,
414
+ nx=None, ny=None,
415
+ sx=None, sy=None,
416
+ ox=None, oy=None,
417
+ xmin_ext=0.0, xmax_ext=0.0,
418
+ ymin_ext=0.0, ymax_ext=0.0,
419
+ wrap=None, nproc=-1, **kwargs):
420
+ """
421
+ Computes the same as the function :func:`tools.rasterize_polygon_2d`, using multiprocessing.
422
+
423
+ All the parameters except `nproc` are the same as those of the function
424
+ :func:`tools.rasterize_polygon_2d`.
425
+
426
+ The number of processes used (in parallel) is n, and determined by the
427
+ parameter `nproc` (int, optional) as follows:
428
+
429
+ - if `nproc > 0`: n = `nproc`,
430
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
431
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
432
+ i.e. all cpus except `-nproc` is used (but at least one).
433
+
434
+ See function :func:`tools.rasterize_polygon_2d`.
435
+ """
436
+ # Define grid geometry (image with no variable)
437
+ im = img.imageFromPoints(vertices,
438
+ nx=nx, ny=ny, sx=sx, sy=sy, ox=ox, oy=oy,
439
+ xmin_ext=xmin_ext, xmax_ext=xmax_ext,
440
+ ymin_ext=ymin_ext, ymax_ext=ymax_ext)
441
+
442
+ # Rasterize: for each cell, check if its center is within the grid
443
+ v = np.asarray(is_in_polygon_mp(np.array((im.xx().reshape(-1), im.yy().reshape(-1))).T, vertices, wrap=wrap, nproc=nproc, **kwargs)).astype('float')
444
+ im.append_var(v, varname='in')
445
+
446
+ return im
447
+ # -----------------------------------------------------------------------------
448
+
449
+ # -----------------------------------------------------------------------------
450
+ def curv_coord_2d_from_center_line(
451
+ x, cl_position, im_cl_dist,
452
+ cl_u=None, gradx=None, grady=None,
453
+ dg=None, gradtol=1.e-5, path_len_max=10000,
454
+ return_path=False):
455
+ """
456
+ Computes curvilinear coordinates in 2D from a center line, for points given in standard coordinates.
457
+
458
+ This functions allows to change coordinates system in 2D. For a point in 2D,
459
+ let the coordinates
460
+
461
+ - u = (u1, u2) (in 2D) in curvilinear system
462
+ - x = (x1, x2) (in 2D) in standard system
463
+
464
+ The curvilinear coordinates system (u) is defined according to a center line
465
+ in a 2D grid as follows:
466
+
467
+ - considering the distance map (geone image `im_cl_dist`) of L2 distance \
468
+ to the center line (`cl_position`)
469
+ - the path from x to the point I on the center line is computed, descending \
470
+ the gradient (`gradx`, `grady`) of the distance map
471
+ - u = (u1, u2) is defined as:
472
+ - u1: the distance along the center line to the point I,
473
+ - u2: +/-the value of the distance map at x, with
474
+ * sign + for point "at left" of the center line and,
475
+ * sign - for point "at right" of the center line.
476
+
477
+ Parameters
478
+ ----------
479
+ x : 2D array-like or 1D array-like
480
+ points coordinates in standard system (should be in the grid of the image
481
+ `im_cl_dist`, see below), each row `x[i]` (if 2D array-like) (or `x` if
482
+ 1D array-like) contains the two coordinates of one point
483
+
484
+ cl_position : 2D array of shape (n, 2)
485
+ position of the points of the center line (in standard system);
486
+ note: the distance between two successive points of the center line give
487
+ the resolution of the u1 coordinate
488
+
489
+ im_cl_dist : :class:`geone.img.Img`
490
+ image of the distance to the center line:
491
+
492
+ - its grid is the "support" of standard coordinate system and it \
493
+ should contain all the points `x`
494
+ - the center line (`cl_position`) should "separate" the image grid \
495
+ in two (disconnected) regions
496
+ - this image can be computed using the function \
497
+ `geone.geosclassicinterface.imgDistanceImage`
498
+
499
+ cl_u : 1D array-like of length n, optional
500
+ distance along the center line (automatically computed if not given
501
+ (`None`)), used for determining u1 coordinate
502
+
503
+ gradx, grady : 2D array-like, optional
504
+ gradient of the distance to the centerline, array of shape
505
+ `(im_cl_dist.ny, im_cl_dist.nx)` (automatically computed if not given
506
+ (`None`));
507
+ `gradx[iy, ix], grady[iy, ix]`: gradient in grid cell of index `iy`, `ix`
508
+ along x and y axes respectively
509
+
510
+ dg : float, optional
511
+ step (length) used for descending the gradient map; by default (`None`):
512
+ `dg` is set to the minimal distance between two successive points in
513
+ `cl_position`, (i.e. minimal difference between two succesive values in
514
+ `cl_u`)
515
+
516
+ gradtol : float, default: 1.e-5
517
+ tolerance for the gradient magnitude (if the magintude of the gradient
518
+ is below `gradtol`, it is considered as zero vector)
519
+
520
+ path_len_max : int, default: 10000
521
+ maximal length of the path from the initial point(s) (`x`) to the center
522
+ line
523
+
524
+ return_path : bool, default: False
525
+ indicates if the path(s) from the initial point(s) (`x`) to the point(s)
526
+ I of the centerline (descending the gradient of the distance map) is
527
+ (are) retrieved
528
+
529
+ Returns
530
+ -------
531
+ u : 2D array or 1D array (same shape as `x`)
532
+ coordinates in curvilinear system of the input point(s) `x` (see above)
533
+ x_path : list of 2D arrays (or 2D array), optional
534
+ path(s) from the initial point(s) to the point(s) I of the centerline
535
+ (descending the gradient of the distance map):
536
+
537
+ - `x_path[i]` : 2D array of floats with 2 columns
538
+ * `xpath[i][j]` is the coordinates (in standard system) of the \
539
+ j-th point of the path from `x[i]` to the center line
540
+
541
+ note: if `x` is reduced to one point and given as 1D array-like, then
542
+ `x_path` is a 2D array of floats with 2 columns containing the path from
543
+ `x` to the center line;
544
+ returned if `returned_path=True`
545
+ """
546
+ if cl_u is None:
547
+ cl_u = np.insert(np.cumsum(np.sqrt(((cl_position[1:,:] - cl_position[:-1,:])**2).sum(axis=1))), 0, 0.0)
548
+
549
+ if gradx is None or grady is None:
550
+ # Gradient of distance map
551
+ grady, gradx = np.gradient(im_cl_dist.val[0,0])
552
+ gradx = gradx / im_cl_dist.sx
553
+ grady = grady / im_cl_dist.sy
554
+
555
+ if dg is None:
556
+ dg = np.diff(cl_u).min()
557
+
558
+ x = np.asarray(x)
559
+ x_ndim = x.ndim
560
+ x = np.atleast_2d(x)
561
+ u = np.zeros_like(x)
562
+
563
+ if return_path:
564
+ x_path = []
565
+
566
+ for i in range(x.shape[0]):
567
+ # Treat the i-th point
568
+ x_cur = x[i]
569
+ if return_path:
570
+ xi_path = [x_cur]
571
+
572
+ u2 = 0.0
573
+ d_prev = np.inf
574
+
575
+ for j in range(path_len_max):
576
+ # index in the grid (and interpolation factor) of the "current" point
577
+ tx = max(0, min((x_cur[0] - im_cl_dist.ox)/im_cl_dist.sx - 0.5, im_cl_dist.nx - 1.00001))
578
+ ix = int(tx)
579
+ tx = tx - int(tx)
580
+
581
+ ty = max(0, min((x_cur[1] - im_cl_dist.oy)/im_cl_dist.sy - 0.5, im_cl_dist.ny - 1.00001))
582
+ iy = int(ty)
583
+ ty = ty - int(ty)
584
+
585
+ # "current" distance to the center line
586
+ d_cur = (1.-ty)*((1.-tx)*im_cl_dist.val[0, 0, iy, ix] + tx*im_cl_dist.val[0, 0, iy, ix+1]) + ty*((1.-tx)*im_cl_dist.val[0, 0, iy+1, ix] + tx*im_cl_dist.val[0, 0, iy+1, ix+1])
587
+
588
+ # if np.abs(d_cur) < dg or d_cur*d_prev < 0:
589
+ if d_cur < dg or d_cur > d_prev:
590
+ break
591
+
592
+ # "current" gradient
593
+ gradx_cur = (1.-ty)*((1.-tx)*gradx[iy, ix] + tx*gradx[iy, ix+1]) + ty*((1.-tx)*gradx[iy+1, ix] + tx*gradx[iy+1, ix+1])
594
+ grady_cur = (1.-ty)*((1.-tx)*grady[iy, ix] + tx*grady[iy, ix+1]) + ty*((1.-tx)*grady[iy+1, ix] + tx*grady[iy+1, ix+1])
595
+
596
+ gradl = np.sqrt(gradx_cur**2 + grady_cur**2)
597
+ if gradl < gradtol:
598
+ break
599
+
600
+ # compute next point descending the gradient
601
+ # x_cur = x_cur - np.sign(d_cur) * dg*np.array([gradx_cur, grady_cur])/gradl
602
+ x_cur = x_cur - dg*np.array([gradx_cur, grady_cur])/gradl
603
+ if return_path:
604
+ xi_path.append(x_cur)
605
+
606
+ u2 = u2 + dg
607
+ d_prev = d_cur
608
+
609
+ # Finalize the computation of coordinate (u1, u2)
610
+ d_cur = ((cl_position[:-1,:] - x_cur)**2).sum(axis=1)
611
+ try:
612
+ k = np.where(d_cur == d_cur.min())[0][0]
613
+ except:
614
+ k = len(cl_position[-2])
615
+ print('WARNING: closest point on center line not found...')
616
+ u1 = cl_u[k]
617
+
618
+ s = np.sign(np.linalg.det(np.vstack((cl_position[k+1] - cl_position[k], x[i] - x_cur))))
619
+ u2 = s*u2
620
+
621
+ u[i] = np.array([u1, u2])
622
+
623
+ if return_path:
624
+ x_path.append(np.asarray(xi_path))
625
+
626
+ if return_path:
627
+ if x_ndim == 1:
628
+ u = u[0]
629
+ x_path = x_path[0]
630
+ return u, x_path
631
+ else:
632
+ if x_ndim == 1:
633
+ u = u[0]
634
+ return u
635
+ # -----------------------------------------------------------------------------
636
+
637
+ # -----------------------------------------------------------------------------
638
+ def curv_coord_2d_from_center_line_mp(
639
+ x, cl_position, im_cl_dist,
640
+ cl_u=None, gradx=None, grady=None,
641
+ dg=None, gradtol=1.e-5, path_len_max=10000,
642
+ return_path=False, nproc=-1):
643
+ """
644
+ Computes the same as the function :func:`tools.curv_coord_2d_from_center_line`, using multiprocessing.
645
+
646
+ All the parameters except `nproc` are the same as those of the function
647
+ :func:`tools.curv_coord_2d_from_center_line`.
648
+
649
+ The number of processes used (in parallel) is n, and determined by the
650
+ parameter `nproc` (int, optional) as follows:
651
+
652
+ - if `nproc > 0`: n = `nproc`,
653
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
654
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
655
+ i.e. all cpus except `-nproc` is used (but at least one).
656
+
657
+ See function :func:`tools.curv_coord_2d_from_center_line`.
658
+ """
659
+ # Initialization
660
+ xx = np.atleast_2d(x)
661
+
662
+ # Set number of processes (n)
663
+ if nproc > 0:
664
+ n = nproc
665
+ else:
666
+ n = min(multiprocessing.cpu_count()+nproc, 1)
667
+
668
+ # Set index for distributing tasks
669
+ q, r = np.divmod(xx.shape[0], n)
670
+ ids_proc = [i*q + min(i, r) for i in range(n+1)]
671
+
672
+ kwargs = dict(cl_u=cl_u, gradx=gradx, grady=grady, dg=dg, gradtol=gradtol, path_len_max=path_len_max, return_path=return_path)
673
+ # Set pool of n workers
674
+ pool = multiprocessing.Pool(n)
675
+ out_pool = []
676
+ for i in range(n):
677
+ # Set i-th process
678
+ out_pool.append(pool.apply_async(curv_coord_2d_from_center_line, args=(xx[ids_proc[i]:ids_proc[i+1]], cl_position, im_cl_dist), kwds=kwargs))
679
+
680
+ # Properly end working process
681
+ pool.close() # Prevents any more tasks from being submitted to the pool,
682
+ pool.join() # then, wait for the worker processes to exit.
683
+
684
+ # Get result from each process
685
+ if return_path:
686
+ u = []
687
+ x_path = []
688
+ for p in out_pool:
689
+ u_p, x_path_p = p.get()
690
+ u.extend(u_p)
691
+ x_path.extend(x_path_p)
692
+ if np.asarray(x).ndim == 1:
693
+ u = u[0]
694
+ x_path = x_path[0]
695
+ return u, x_path
696
+ else:
697
+ u = np.vstack([p.get() for p in out_pool])
698
+ if np.asarray(x).ndim == 1:
699
+ u = u[0]
700
+ return u
701
+ # -----------------------------------------------------------------------------
702
+
703
+ ##### OLD BELOW #####
704
+ # # -----------------------------------------------------------------------------
705
+ # def sector_angle(x, line, **kwargs):
706
+ # """
707
+ # Checks if point(s) (`x`) is (are) in a polygon given by its vertices
708
+ # `vertices` forming a close line.
709
+ #
710
+ # To check if a point is in the polygon, the method consists in computing
711
+ # the vectors from the given point to the vertices of the polygon and
712
+ # the sum of signed angles between two successives vectors (and the last
713
+ # and first one). The point is in the polygon if and only if this sum is
714
+ # equal to +/- 2 pi.
715
+ #
716
+ # :param x: (1d-array of 2 floats, or 2d-array of shape (m, 2))
717
+ # one or several points in 2D
718
+ # :param vertices:(2d-array of shape (n,2)) vertices of a polygon in 2D,
719
+ # the segments of the polygon are obtained by linking
720
+ # two successive vertices (and the last one with the
721
+ # first one, if `wrap` is True (see below) or automatically
722
+ # checked if it is needed), so that the vertices form a
723
+ # close line (clockwise or counterclockwise)
724
+ # :param wrap: (bool)
725
+ # - if True, last and first vertices has to be linked
726
+ # to form a close line,
727
+ # - if False, last and first vertices should be the same
728
+ # ones (i.e. the vertices form a close line)
729
+ # - if None, automatically computed
730
+ # :param kwargs: keyword arguments passed to `numpy.isclose` (see below)
731
+ #
732
+ # :return: (bool or 1d-array of bool of shape (m,)) True / False
733
+ # value(s) indicating if the point(s) `x` is (are)
734
+ # inside / outside the polygon
735
+ # """
736
+ #
737
+ # # Initialization
738
+ # xx = np.atleast_2d(x)
739
+ # res = np.zeros(xx.shape[0], dtype='bool')
740
+ #
741
+ # for j, xj in enumerate(xx):
742
+ # # Set a, b, c: edge of the triangles to compute angle between a and b
743
+ # v = xj - vertices
744
+ #
745
+ # # v_norm2: square norm of v
746
+ # v_norm2 = (a**2).sum(axis=1)
747
+ #
748
+ # a = v[:-1]
749
+ # b = v[1:]
750
+ # # b_norm2 = v_norm2[1:]
751
+ # ab_norm = np.sqrt(v_norm2[:-1]*v_norm2[1:])
752
+ #
753
+ # ind = np.isclose(ab_norm, 0, **kwargs)
754
+ # # Compute the sum of angles using the theorem of cosine
755
+ # c = b - a
756
+ # c_norm2 = (c**2).sum(axis=1)
757
+ # sign = 2*(a[:,0]*b[:,1] - a[:,1]*b[:,0] > 0) - 1
758
+ # sum_angles = np.sum(sign[ind]*np.arccos(np.minimum(1.0, np.maximum(-1.0, (a_norm2[ind] + b_norm2[ind] - c_norm2[ind])/(2.0*ab_norm[ind])))))
759
+ # res[i] = sum_angles > 0
760
+ #
761
+ # return res
762
+ # # -----------------------------------------------------------------------------