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