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/imgplot.py ADDED
@@ -0,0 +1,1464 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # -------------------------------------------------------------------------
5
+ # Python module: 'imgplot.py'
6
+ # author: Julien Straubhaar
7
+ # date: dec-2017
8
+ # -------------------------------------------------------------------------
9
+
10
+ """
11
+ Module for custom plots of images (class :class:`geone.img.Img`) in 2D.
12
+ """
13
+
14
+ from geone import img
15
+ from geone.img import Img
16
+ from geone import customcolors as ccol
17
+ from geone.customcolors import add_colorbar
18
+
19
+ import numpy as np
20
+ import matplotlib.pyplot as plt
21
+
22
+ from matplotlib import rcParams as mpl_rcParams
23
+ import matplotlib.colors as mcolors
24
+
25
+ # ============================================================================
26
+ class ImgplotError(Exception):
27
+ """
28
+ Custom exception related to `imgplot` module.
29
+ """
30
+ pass
31
+ # ============================================================================
32
+
33
+ # ----------------------------------------------------------------------------
34
+ def drawImage2D(
35
+ im, ix=None, iy=None, iz=None, iv=None,
36
+ plot_empty_grid=False,
37
+ cmap=ccol.cmap_def,
38
+ alpha=None,
39
+ excludedVal=None,
40
+ categ=False, categVal=None,
41
+ categCol=None, categColCycle=False, categColbad=ccol.cbad_def,
42
+ nCategMax=100,
43
+ vmin=None, vmax=None,
44
+ contourf=False,
45
+ contour=False,
46
+ contour_clabel=False,
47
+ levels=None,
48
+ contourf_kwargs=None,
49
+ contour_kwargs={'linestyles':'solid', 'colors':'gray'},
50
+ contour_clabel_kwargs={'inline':1},
51
+ interpolation='none',
52
+ aspect='equal',
53
+ frame=True, xaxis=True, yaxis=True,
54
+ title=None,
55
+ xlabel=None, xticks=None, xticklabels=None, xticklabels_max_decimal=None,
56
+ ylabel=None, yticks=None, yticklabels=None, yticklabels_max_decimal=None,
57
+ clabel=None, cticks=None, cticklabels=None, cticklabels_max_decimal=None,
58
+ colorbar_extend='neither',
59
+ colorbar_aspect=20, colorbar_pad_fraction=1.0,
60
+ showColorbar=True, removeColorbar=False, showColorbarOnly=0,
61
+ verbose=1,
62
+ logger=None,
63
+ **kwargs):
64
+ # animated : bool, default: False
65
+ # keyword argument passed to `matplotlib.pyplot.imshow` for animation...
66
+ """
67
+ Displays a 2D image (or a slice of a 3D image).
68
+
69
+ Parameters
70
+ ----------
71
+ im : :class:`geone.img.Img`
72
+ image
73
+
74
+ ix : int, optional
75
+ grid index along x axis of the yz-slice to be displayed;
76
+ by default (`None`): no slice along x axis
77
+
78
+ iy : int, optional
79
+ grid index along y axis of the xz-slice to be displayed;
80
+ by default (`None`): no slice along y axis
81
+
82
+ iz : int, optional
83
+ grid index along z axis of the xy-slice to be displayed;
84
+ by default (`None`): no slice along z axis, but
85
+ if `ix=None`, `iy=None`, `iz=None`, then `iz=0` is used
86
+
87
+ iv : int, optional
88
+ index of the variable to be displayed;
89
+ by default (`None`): the variable of index 0 (`iv=0` is used) if
90
+ the image has at leas one variable (`im.nv > 0`), otherwise, an "empty"
91
+ grid is displayed (a "fake" variable with `numpy.nan` (missing value)
92
+ over the entire grid is considered)
93
+
94
+ plot_empty_grid : bool, default: False
95
+ if `True`: an "empty" grid is displayed (a "fake" variable with `numpy.nan`
96
+ (missing value) over the entire grid is considered), and `iv` is ignored
97
+
98
+ cmap : colormap
99
+ color map (can be a string, in this case the color map is obtained by
100
+ `matplotlib.pyplot.get_cmap(cmap)`)
101
+
102
+ alpha : float, optional
103
+ value of the "alpha" channel (for transparency);
104
+ by default (`None`): `alpha=1.0` is used (no transparency)
105
+
106
+ excludedVal : sequence of values, or single value, optional
107
+ values to be excluded from the plot;
108
+ note not used if `categ=True` and `categVal` is not `None`
109
+
110
+ categ : bool, default: False
111
+ indicates if the variable of the image to diplay has to be treated as a
112
+ categorical (discrete) variable (`True`), or continuous variable (`False`)
113
+
114
+ categVal : sequence of values, or single value, optional
115
+ used only if `categ=True`:
116
+ explicit list of the category values to be displayed;
117
+ by default (`None`): the list of all unique values are automatically
118
+ retrieved
119
+
120
+ categCol: sequence of colors, optional
121
+ used only if `categ=True`:
122
+ sequence of colors, (given as 3-tuple (RGB code), 4-tuple (RGBA code) or
123
+ str), used for the category values that will be displayed:
124
+
125
+ - if `categVal` is not `None`: `categCol` must have the same length as \
126
+ `categVal`
127
+ - if `categVal=None`:
128
+ - first colors of `categCol` are used if its length is greater than \
129
+ or equal to the number of displayed category values,
130
+ - otherwise: the colors of `categCol` are used cyclically if \
131
+ `categColCycle=True`, and the colors taken from the color map `cmap` \
132
+ are used if `categColCycle=False`
133
+
134
+ categColCycle : bool, default: False
135
+ used only if `categ=True`:
136
+ indicates if the colors of `categCol` can be used cyclically or not
137
+ (when the number of displayed category values exceeds the length of
138
+ `categCol`)
139
+
140
+ categColbad : color
141
+ used only if `categ=True`:
142
+ color (3-tuple (RGB code), 4-tuple (RGBA code) or str) used for bad
143
+ categorical value
144
+
145
+ nCategMax : int, default: 100
146
+ used only if `categ=True`: maximal number of categories, if there is
147
+ more distinct values to be plotted, an error is raised
148
+
149
+ vmin : float, optional
150
+ used only if `categ=False`:
151
+ minimal value to be displayed; by default: minimal value of the displayed
152
+ variable is used for `vmin`
153
+
154
+ vmax : float, optional
155
+ used only if `categ=False`:
156
+ maximal value to be displayed; by default: maximal value of the displayed
157
+ variable is used for `vmax`
158
+
159
+ contourf : bool, default: False
160
+ indicates if `matplotlib.pyplot.contourf` is used, i.e. contour map with
161
+ filled area between levels, instead of standard plot
162
+ (`matplotlib.pyplot.imshow`)
163
+
164
+ contour : bool, default: False
165
+ indicates if contour levels are added to the plot (using
166
+ `matplotlib.pyplot.contour`)
167
+
168
+ contour_clabel : bool, default: False
169
+ indicates if labels are added to contour (if `contour=True`)
170
+
171
+ levels : array-like, or int, optional
172
+ keyword argument 'levels' passed to `matplotlib.pyplot.contourf` (if
173
+ `contourf=True`) and/or `matplotlib.pyplot.contour` (if `contour=True`)
174
+
175
+ contourf_kwargs : dict, optional
176
+ keyword arguments passed to `matplotlib.pyplot.contourf` (if
177
+ `contourf=True`); note: the parameters `levels` (see above) is used as
178
+ keyword argument, i.e. it prevails over the key 'levels' in
179
+ `contourf_kwargs` (if given)
180
+
181
+ contour_kwargs : dict
182
+ keyword arguments passed to `matplotlib.pyplot.contour` (if
183
+ `contour=True`); note: the parameters `levels` (see above) is used as
184
+ keyword argument, i.e. it prevails over the key 'levels' in
185
+ `contour_kwargs` (if given)
186
+
187
+ contour_clabel_kwargs : dict
188
+ keyword arguments passed to `matplotlib.pyplot.clabel` (if
189
+ `contour_clabel=True`)
190
+
191
+ interpolation : str, default: 'none'
192
+ keyword argument 'interpolation' to be passed `matplotlib.pyplot.imshow`
193
+
194
+ aspect : str, or scalar, default: 'equal'
195
+ keyword argument 'aspect' to be passed `matplotlib.pyplot.imshow`
196
+
197
+ frame : bool, default: True
198
+ indicates if a frame is drawn around the image
199
+
200
+ xaxis : bool, default: True
201
+ indicates if x axis is visible
202
+
203
+ yaxis : bool, default: True
204
+ indicates if y axis is visible
205
+
206
+ title : str, optional
207
+ title of the figure
208
+
209
+ xlabel : str, optional
210
+ label for x axis
211
+
212
+ ylabel : str, optional
213
+ label for y axis
214
+
215
+ clabel : str, optional
216
+ label for color bar
217
+
218
+ xticks : sequence of values, optional
219
+ values where to place ticks along x axis
220
+
221
+ yticks : sequence of values, optional
222
+ values where to place ticks along y axis
223
+
224
+ cticks : sequence of values, optional
225
+ values where to place ticks along the color bar
226
+
227
+ xticklabels : sequence of strs, optional,
228
+ sequence of strings for ticks along x axis
229
+
230
+ yticklabels : sequence of strs, optional,
231
+ sequence of strings for ticks along y axis
232
+
233
+ cticklabels : sequence of strs, optional,
234
+ sequence of strings for ticks along the color bar
235
+
236
+ xticklabels_max_decimal : int, optional
237
+ maximal number of decimals (fractional part) for tick labels along x axis
238
+
239
+ yticklabels_max_decimal : int, optional
240
+ maximal number of decimals (fractional part) for tick labels along y axis
241
+
242
+ cticklabels_max_decimal : int, optional
243
+ maximal number of decimals (fractional part) for tick labels along the
244
+ color bar
245
+
246
+ colorbar_extend : str {'neither', 'both', 'min', 'max'}, default: 'neither'
247
+ used only if `categ=False`:
248
+ keyword argument 'extend' to be passed to `matplotlib.pyplot.colorbar`
249
+ (or `geone.customcolors.add_colorbar`)
250
+
251
+ colorbar_aspect : float or int, default: 20
252
+ keyword argument 'aspect' to be passed to
253
+ `geone.customcolors.add_colorbar`
254
+
255
+ colorbar_pad_fraction : float or int, default: 1.0
256
+ keyword argument 'pad_fraction' to be passed to
257
+ `geone.customcolors.add_colorbar`
258
+
259
+ showColorbar : bool, default: True
260
+ indicates if the color bar (vertical) is shown
261
+
262
+ removeColorbar : bool, default: False
263
+ if True (and if `showColorbar=True`), then the colorbar is removed;
264
+ note: it can be useful to draw and then remove the color bar for size of
265
+ the plotted image...)
266
+
267
+ showColorbarOnly : int, default: 0
268
+ mode defining how the color bar (vertical) is shown:
269
+
270
+ - `showColorbarOnly=0`: not used / not applied
271
+ - `showColorbarOnly>0`: only the color bar is shown (even if \
272
+ `showColorbar=False` or if `removeColorbar=True`):
273
+ - `showColorbarOnly=1`: the plotted image is "cleared"
274
+ - `showColorbarOnly=2`: an image of same color as the background is \
275
+ drawn onto the plotted image
276
+
277
+ verbose : int, default: 1
278
+ verbose mode, higher implies more printing (info)
279
+
280
+ logger : :class:`logging.Logger`, optional
281
+ logger (see package `logging`)
282
+ if specified, messages are written via `logger` (no print)
283
+
284
+ kwargs : dict
285
+ additional keyword arguments : each keyword argument with the key
286
+ 'xxx_<name>' will be passed as keyword argument with the key '<name>' to
287
+ a function related to 'xxx';
288
+ possibilities for 'xxx\\_' and related function:
289
+
290
+ .. list-table::
291
+ :widths: 25 45
292
+ :header-rows: 1
293
+
294
+ * - string (prefix)
295
+ - method from `Axes` from `matplotlib`
296
+ * - 'title\\_'
297
+ - `ax.set_title()`
298
+ * - 'xlabel\\_'
299
+ - `ax.set_xlabel()`
300
+ * - 'xticks\\_'
301
+ - `ax.set_xticks()`
302
+ * - 'xticklabels\\_'
303
+ - `ax.set_xticklabels()`
304
+ * - 'ylabel\\_'
305
+ - `ax.set_ylabel()`
306
+ * - 'yticks\\_'
307
+ - `ax.set_yticks()`
308
+ * - 'yticklabels\\_'
309
+ - `ax.set_yticklabels()`
310
+ * - 'clabel\\_'
311
+ - `cbar.set_label()`
312
+ * - 'cticks\\_'
313
+ - `cbar.set_ticks()`
314
+ * - 'cticklabels\\_'
315
+ - `cbar.ax.set_yticklabels()`
316
+
317
+ for examples:
318
+
319
+ - 'title_fontsize', '<x|y|c>label_fontsize' (keys)
320
+ - 'title_fontweight', '<x|y|c>label_fontweight' (keys), with \
321
+ possible values: numeric value in 0-1000 or \
322
+ 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', \
323
+ 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \
324
+ 'extra bold', 'black'
325
+
326
+ Note that
327
+
328
+ - default value for font size is `matplotlib.rcParams['font.size']`
329
+ - default value for font weight is `matplotlib.rcParams['font.weight']`
330
+
331
+ Returns
332
+ -------
333
+ imout : list of `matplotlib` object
334
+ list of plotted objects
335
+ """
336
+ # - 'title\_' fun: ax.set_title()
337
+ # - 'xlabel\_' fun: ax.set_xlabel()
338
+ # - 'xticks\_' fun: ax.set_xticks()
339
+ # - 'xticklabels\_' fun: ax.set_xticklabels()
340
+ # - 'ylabel\_' fun: ax.set_ylabel()
341
+ # - 'yticks\_' fun: ax.set_yticks()
342
+ # - 'yticklabels\_' fun: ax.set_yticklabels()
343
+ # - 'clabel\_' fun: cbar.set_label()
344
+ # - 'cticks\_' fun: cbar.set_ticks()
345
+ # - 'cticklabels\_' fun: cbar.ax.set_yticklabels()
346
+ fname = 'drawImage2D'
347
+
348
+ # Initialization for output
349
+ imout = []
350
+ #ax, cbar = None, None
351
+
352
+ if plot_empty_grid:
353
+ iv = None
354
+ else:
355
+ # Check / set iv
356
+ if iv is None:
357
+ if im.nv > 0:
358
+ iv = 0
359
+ else:
360
+ if iv < 0:
361
+ iv = im.nv + iv
362
+
363
+ if iv < 0 or iv >= im.nv:
364
+ err_msg = f'{fname}: invalid `iv` index'
365
+ if logger: logger.error(err_msg)
366
+ raise ImgplotError(err_msg)
367
+
368
+ # Check slice direction and indices
369
+ n = int(ix is not None) + int(iy is not None) + int(iz is not None)
370
+
371
+ if n == 0:
372
+ sliceDir = 'z'
373
+ iz = 0
374
+
375
+ elif n==1:
376
+ if ix is not None:
377
+ if ix < 0:
378
+ ix = im.nx + ix
379
+
380
+ if ix < 0 or ix >= im.nx:
381
+ err_msg = f'{fname}: invalid `ix` index'
382
+ if logger: logger.error(err_msg)
383
+ raise ImgplotError(err_msg)
384
+
385
+ sliceDir = 'x'
386
+
387
+ elif iy is not None:
388
+ if iy < 0:
389
+ iy = im.ny + iy
390
+
391
+ if iy < 0 or iy >= im.ny:
392
+ err_msg = f'{fname}: invalid `iy` index'
393
+ if logger: logger.error(err_msg)
394
+ raise ImgplotError(err_msg)
395
+
396
+ sliceDir = 'y'
397
+
398
+ else: # iz is not None
399
+ if iz < 0:
400
+ iz = im.nz + iz
401
+
402
+ if iz < 0 or iz >= im.nz:
403
+ err_msg = f'{fname}: invalid `iz` index'
404
+ if logger: logger.error(err_msg)
405
+ raise ImgplotError(err_msg)
406
+
407
+ sliceDir = 'z'
408
+
409
+ else: # n > 1
410
+ err_msg = f'{fname}: slice specified in more than one direction'
411
+ if logger: logger.error(err_msg)
412
+ raise ImgplotError(err_msg)
413
+
414
+ # Extract what to be plotted
415
+ if sliceDir == 'x':
416
+ dim0 = im.ny
417
+ min0 = im.oy
418
+ max0 = im.ymax()
419
+
420
+ dim1 = im.nz
421
+ min1 = im.oz
422
+ max1 = im.zmax()
423
+ if iv is None: # empty image
424
+ zz = np.nan * np.ones((im.nz, im.ny))
425
+ else:
426
+ zz = np.array(im.val[iv, :, :, ix].reshape(dim1, dim0)) # np.array() to get a copy
427
+ # reshape to force 2-dimensional array
428
+
429
+ elif sliceDir == 'y':
430
+ dim0 = im.nx
431
+ min0 = im.ox
432
+ max0 = im.xmax()
433
+
434
+ dim1 = im.nz
435
+ min1 = im.oz
436
+ max1 = im.zmax()
437
+ if iv is None: # empty image
438
+ zz = np.nan * np.ones((im.nz, im.nx))
439
+ else:
440
+ zz = np.array(im.val[iv, :, iy, :].reshape(dim1, dim0)) # np.array() to get a copy
441
+
442
+ else: # sliceDir == 'z'
443
+ dim0 = im.nx
444
+ min0 = im.ox
445
+ max0 = im.xmax()
446
+
447
+ dim1 = im.ny
448
+ min1 = im.oy
449
+ max1 = im.ymax()
450
+ if iv is None: # empty image
451
+ zz = np.nan * np.ones((im.ny, im.nx))
452
+ else:
453
+ zz = np.array(im.val[iv, iz, :, :].reshape(dim1, dim0)) # np.array() to get a copy
454
+
455
+ # Get the color map
456
+ if isinstance(cmap, str):
457
+ try:
458
+ cmap = plt.get_cmap(cmap)
459
+ except:
460
+ err_msg = f'{fname}: invalid `cmap` string'
461
+ if logger: logger.error(err_msg)
462
+ raise ImgplotError(err_msg)
463
+
464
+ if categ:
465
+ # --- Treat categorical variable ---
466
+ if categCol is not None \
467
+ and type(categCol) is not list \
468
+ and type(categCol) is not tuple:
469
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
470
+ if logger: logger.error(err_msg)
471
+ raise ImgplotError(err_msg)
472
+
473
+ # Get array 'dval' of displayed values
474
+ if categVal is not None:
475
+ dval = np.array(categVal).reshape(-1) # force to be a 1d array
476
+
477
+ if len(np.unique(dval)) != len(dval):
478
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
479
+ if logger: logger.error(err_msg)
480
+ raise ImgplotError(err_msg)
481
+
482
+ # Check 'categCol' (if not None)
483
+ if categCol is not None and len(categCol) != len(dval):
484
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
485
+ if logger: logger.error(err_msg)
486
+ raise ImgplotError(err_msg)
487
+
488
+ else:
489
+ # Possibly exclude values from zz
490
+ if excludedVal is not None:
491
+ for val in np.array(excludedVal).reshape(-1):
492
+ np.putmask(zz, zz == val, np.nan)
493
+
494
+ # Get the unique value in zz
495
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
496
+
497
+ if not len(dval) and verbose > 0: # len(dval) == 0
498
+ if logger:
499
+ logger.warning(f'{fname}: no value to be drawn!')
500
+ else:
501
+ print(f'{fname}: WARNING: no value to be drawn!')
502
+
503
+ if len(dval) > nCategMax:
504
+ # Prevent from plotting in category mode if too many categories...
505
+ err_msg = f'{fname}: too many categories to be plotted (> `nCategMax` = {nCategMax})'
506
+ if logger: logger.error(err_msg)
507
+ raise ImgplotError(err_msg)
508
+
509
+ # Replace dval[i] by i in zz and other values by np.nan
510
+ zz2 = np.array(zz) # copy array
511
+ zz[...] = np.nan # initialize
512
+ for i, v in enumerate(dval):
513
+ zz[zz2 == v] = i
514
+
515
+ del zz2
516
+
517
+ # Set 'colorList': the list of colors to use
518
+ colorList = None
519
+ if categCol is not None:
520
+ if len(categCol) >= len(dval):
521
+ colorList = [categCol[i] for i in range(len(dval))]
522
+ # colorList = [mcolors.ColorConverter().to_rgba(categCol[i]) for i in range(len(dval))]
523
+
524
+ elif categColCycle:
525
+ if verbose > 0:
526
+ if logger:
527
+ logger.warning(f'{fname}: `categCol` is used cyclically (too few entries)')
528
+ else:
529
+ print(f'{fname}: WARNING: `categCol` is used cyclically (too few entries)')
530
+ colorList = [categCol[i%len(categCol)] for i in range(len(dval))]
531
+
532
+ else:
533
+ if verbose > 0:
534
+ if logger:
535
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
536
+ else:
537
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
538
+
539
+ if colorList is None:
540
+ # Use colors from cmap
541
+ colorList = [cmap(x) for x in np.arange(len(dval)) * 1.0/(len(dval)-1)]
542
+
543
+ # Set the colormap: 'cmap'
544
+ if len(dval) == 1:
545
+ cmap = ccol.custom_cmap([colorList[0], colorList[0]], ncol=2,
546
+ cbad=categColbad, alpha=alpha)
547
+
548
+ else: # len(dval) == len(colorList) > 1
549
+ # cmap = mcolors.ListedColormap(colorList)
550
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList),
551
+ cbad=categColbad, alpha=alpha)
552
+
553
+ # Set the min and max of the colorbar
554
+ vmin, vmax = -0.5, len(dval) - 0.5
555
+
556
+ # Set colorbar ticks and ticklabels if not given
557
+ if cticks is None:
558
+ cticks = range(len(dval))
559
+
560
+ if cticklabels is None:
561
+ #cticklabels = [f'{v:.3g}' for v in cticks]
562
+ cticklabels = [f'{v:.3g}' for v in dval]
563
+
564
+ # Reset cextend if needed
565
+ colorbar_extend = 'neither'
566
+
567
+ else: # categ == False
568
+ # --- Treat continuous variable ---
569
+ # Possibly exclude values from zz
570
+ if excludedVal is not None:
571
+ for val in np.array(excludedVal).reshape(-1): # force to be a 1d array
572
+ np.putmask(zz, zz == val, np.nan)
573
+
574
+ # Generate "sub-dictionaries" from kwargs
575
+ # For each item 'xxx_<name>':<value> from kwargs (whose the key begins
576
+ # with "xxx_"): the item '<name>':<value> is added to the dictionary
577
+ # xxx_kwargs
578
+ # "xxx_" is replaced by the strings below (in sub_prefix list)
579
+ # These sub-dictionaries are passed (unpacked) as keyword arguments to
580
+ # functions that draw labels, ticklabels, ...
581
+ title_kwargs = {}
582
+
583
+ xlabel_kwargs = {}
584
+ xticks_kwargs = {}
585
+ xticklabels_kwargs = {}
586
+
587
+ ylabel_kwargs = {}
588
+ yticks_kwargs = {}
589
+ yticklabels_kwargs = {}
590
+
591
+ clabel_kwargs = {}
592
+ cticks_kwargs = {}
593
+ cticklabels_kwargs = {}
594
+
595
+ colorbar_kwargs = {'aspect':colorbar_aspect, 'pad_fraction':colorbar_pad_fraction}
596
+
597
+ sub_prefix = ['title_',
598
+ 'xlabel_', 'xticks_', 'xticklabels_',
599
+ 'ylabel_', 'yticks_', 'yticklabels_',
600
+ 'clabel_', 'cticks_', 'cticklabels_',
601
+ 'colorbar_']
602
+ sub_kwargs = [title_kwargs,
603
+ xlabel_kwargs, xticks_kwargs, xticklabels_kwargs,
604
+ ylabel_kwargs, yticks_kwargs, yticklabels_kwargs,
605
+ clabel_kwargs, cticks_kwargs, cticklabels_kwargs,
606
+ colorbar_kwargs] # list of dictionaries
607
+
608
+ for k, v in kwargs.items():
609
+ for i in range(len(sub_kwargs)):
610
+ n = len(sub_prefix[i])
611
+ if k[0:n] == sub_prefix[i]:
612
+ sub_kwargs[i][k[n:]] = v # add item k[n:]:v to dictionary sub_kwargs[i]
613
+
614
+ if showColorbarOnly:
615
+ # Overwrite some parameters if needed
616
+ frame = False
617
+ xaxis = False
618
+ yaxis = False
619
+ title = None
620
+ showColorbar = True
621
+ removeColorbar = False
622
+
623
+ # Get current axis (for plotting)
624
+ ax = plt.gca()
625
+
626
+ # image plot
627
+ im_plot = ax.imshow(zz, cmap=cmap, alpha=alpha, vmin=vmin, vmax=vmax,
628
+ origin='lower', extent=[min0, max0, min1, max1],
629
+ interpolation=interpolation, aspect=aspect) #, animated=animated)
630
+ imout.append(im_plot)
631
+
632
+ if contourf:
633
+ # imshow is still used above to account for 'aspect'
634
+ # Set key word argument 'levels' from the argument 'levels'
635
+ if contourf_kwargs is None:
636
+ contourf_kwargs = {}
637
+ contourf_kwargs['levels'] = levels
638
+ im_contf = ax.contourf(zz, cmap=cmap, alpha=alpha, vmin=vmin, vmax=vmax,
639
+ origin='lower', extent=[min0, max0, min1, max1],
640
+ **contourf_kwargs)
641
+ imout.append(im_contf)
642
+
643
+ if contour:
644
+ # Set key word argument 'levels' from the argument 'levels'
645
+ contour_kwargs['levels']=levels
646
+ im_cont = ax.contour(zz,
647
+ origin='lower', extent=[min0, max0, min1, max1],
648
+ **contour_kwargs)
649
+ imout.append(im_cont)
650
+ if contour_clabel:
651
+ ax.clabel(im_cont, **contour_clabel_kwargs)
652
+
653
+ # title
654
+ if title is not None:
655
+ ax.set_title(title, **title_kwargs)
656
+
657
+ # xlabel, xticks and xticklabels
658
+ if xlabel is not None:
659
+ ax.set_xlabel(xlabel, **xlabel_kwargs)
660
+
661
+ if xticks is not None:
662
+ ax.set_xticks(xticks, **xticks_kwargs)
663
+
664
+ if xticklabels is not None:
665
+ ax.set_xticklabels(xticklabels, **xticklabels_kwargs)
666
+ elif xticklabels_max_decimal is not None:
667
+ s = 10**xticklabels_max_decimal
668
+ labels = [np.round(t*s)/s for t in ax.get_xticks()]
669
+ ax.set_xticklabels(labels, **xticklabels_kwargs)
670
+ elif len(xticklabels_kwargs):
671
+ ax.set_xticklabels(ax.get_xticks(), **xticklabels_kwargs)
672
+ # else... default xticklabels....
673
+
674
+ # ylabel, yticks and yticklabels
675
+ if ylabel is not None:
676
+ ax.set_ylabel(ylabel, **ylabel_kwargs)
677
+
678
+ if yticks is not None:
679
+ ax.set_yticks(yticks, **yticks_kwargs)
680
+
681
+ if yticklabels is not None:
682
+ ax.set_yticklabels(yticklabels, **yticklabels_kwargs)
683
+ elif yticklabels_max_decimal is not None:
684
+ s = 10**yticklabels_max_decimal
685
+ labels = [np.round(t*s)/s for t in ax.get_yticks()]
686
+ ax.set_yticklabels(labels, **yticklabels_kwargs)
687
+ elif len(yticklabels_kwargs):
688
+ ax.set_yticklabels(ax.get_yticks(), **yticklabels_kwargs)
689
+ # else... default yticklabels....
690
+
691
+ # Display or hide: frame, xaxis, yaxis
692
+ if not frame:
693
+ ax.set_frame_on(False)
694
+
695
+ if not xaxis:
696
+ ax.get_xaxis().set_visible(False)
697
+
698
+ if not yaxis:
699
+ ax.get_yaxis().set_visible(False)
700
+
701
+ # Colorbar
702
+ if showColorbar:
703
+ #cbar_kwargs = {'aspect':colorbar_aspect, 'pad_fraction':colorbar_pad_fraction}
704
+ if not contourf:
705
+ colorbar_kwargs['extend']=colorbar_extend
706
+ cbar = add_colorbar(im_plot, **colorbar_kwargs)
707
+
708
+ if clabel is not None:
709
+ cbar.set_label(clabel, **clabel_kwargs)
710
+
711
+ if cticks is not None:
712
+ cbar.set_ticks(cticks, **cticks_kwargs)
713
+
714
+ if cticklabels is not None:
715
+ cbar.ax.set_yticklabels(cticklabels, **cticklabels_kwargs)
716
+ elif cticklabels_max_decimal is not None:
717
+ s = 10**cticklabels_max_decimal
718
+ labels = [np.round(t*s)/s for t in cbar.get_ticks()]
719
+ cbar.ax.set_yticklabels(labels, **cticklabels_kwargs)
720
+ elif len(cticklabels_kwargs):
721
+ cbar.ax.set_yticklabels(cbar.get_ticks(), **cticklabels_kwargs)
722
+ # else... default cticklabels....
723
+
724
+ if removeColorbar:
725
+ # cbar.ax.get_xaxis().set_visible(False)
726
+ # cbar.ax.get_yaxis().set_visible(False)
727
+ # cbar.ax.clear()
728
+ cbar.ax.set_visible(False)
729
+
730
+ if showColorbarOnly:
731
+ if showColorbarOnly == 1:
732
+ ax.clear() # change the size of the colorbar...
733
+ else:
734
+ # Trick: redraw the image in background color...
735
+ zz[...] = 0
736
+ # bg_color = mpl_rcParams['figure.facecolor'] # background color
737
+ bg_color = ax.get_facecolor() # background color
738
+ # bg_color = plt.gcf().get_facecolor() # background color
739
+ ncmap = ccol.custom_cmap([bg_color,bg_color], ncol=2)
740
+ ax.imshow(zz, cmap=ncmap)
741
+ ax.set_frame_on(True)
742
+ for pos in ('bottom', 'top', 'right', 'left'):
743
+ ax.spines[pos].set_color(bg_color)
744
+ ax.spines[pos].set_linewidth(10)
745
+
746
+ return imout
747
+ #return (ax, cbar)
748
+ # ----------------------------------------------------------------------------
749
+
750
+ # ----------------------------------------------------------------------------
751
+ def get_colors_from_values(
752
+ val,
753
+ cmap=ccol.cmap_def,
754
+ alpha=None,
755
+ excludedVal=None,
756
+ categ=False, categVal=None,
757
+ categCol=None, categColCycle=False, categColbad=ccol.cbad_def,
758
+ vmin=None, vmax=None,
759
+ cmin=None, cmax=None,
760
+ verbose=1,
761
+ logger=None):
762
+ """
763
+ Gets the colors for given values, according to color settings as used in function :func:`drawImage2D`.
764
+
765
+ Parameters
766
+ ----------
767
+ val : array-like of floats, or float
768
+ values for which the colors have to be retrieved
769
+
770
+ cmap : see function :func:`drawImage2D`
771
+
772
+ alpha : see function :func:`drawImage2D`
773
+
774
+ excludedVal : see function :func:`drawImage2D`
775
+
776
+ categ : see function :func:`drawImage2D`
777
+
778
+ categVal : see function :func:`drawImage2D`
779
+
780
+ categCol : see function :func:`drawImage2D`
781
+
782
+ categColCycle : see function :func:`drawImage2D`
783
+
784
+ categColbad : see function :func:`drawImage2D`
785
+
786
+ vmin : see function :func:`drawImage2D`
787
+
788
+ vmax : see function :func:`drawImage2D`
789
+
790
+ cmin : float, optional
791
+ alternative keyword for `vmin` (for compatibility with color settings
792
+ in the functions of the module `geone.imgplot3d`)
793
+
794
+ cmax : float, optional
795
+ alternative keyword for `vmax` (for compatibility with color settings
796
+ in the functions of the module `geone.imgplot3d`)
797
+
798
+ verbose : int, default: 1
799
+ verbose mode, higher implies more printing (info)
800
+
801
+ logger : :class:`logging.Logger`, optional
802
+ logger (see package `logging`)
803
+ if specified, messages are written via `logger` (no print)
804
+
805
+ Returns
806
+ -------
807
+ col : 1D array of colors
808
+ colors used for values in `val` according to the given settings
809
+ """
810
+ fname = 'get_colors_from_values'
811
+
812
+ # Check vmin, cmin and vmax, cmax
813
+ if vmin is not None and cmin is not None:
814
+ err_msg = f'{fname}: use `vmin` or `cmin` (not both)'
815
+ if logger: logger.error(err_msg)
816
+ raise ImgplotError(err_msg)
817
+
818
+ if vmax is not None and cmax is not None:
819
+ err_msg = f'{fname}: use `vmax` or `cmax` (not both)'
820
+ if logger: logger.error(err_msg)
821
+ raise ImgplotError(err_msg)
822
+
823
+ if vmin is None:
824
+ vmin = cmin
825
+
826
+ if vmax is None:
827
+ vmax = cmax
828
+
829
+ # Copy val in a 1d array
830
+ zz = np.copy(np.atleast_1d(val)).reshape(-1)
831
+
832
+ # --- Code adapted from function drawImage2D - start ----
833
+ # Get the color map
834
+ if isinstance(cmap, str):
835
+ try:
836
+ cmap = plt.get_cmap(cmap)
837
+ except:
838
+ err_msg = f'{fname}: invalid `cmap` string'
839
+ if logger: logger.error(err_msg)
840
+ raise ImgplotError(err_msg)
841
+
842
+ if categ:
843
+ # --- Treat categorical variable ---
844
+ if categCol is not None \
845
+ and type(categCol) is not list \
846
+ and type(categCol) is not tuple:
847
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
848
+ if logger: logger.error(err_msg)
849
+ raise ImgplotError(err_msg)
850
+
851
+ # Get array 'dval' of displayed values
852
+ if categVal is not None:
853
+ dval = np.array(categVal).reshape(-1) # force to be a 1d array
854
+
855
+ if len(np.unique(dval)) != len(dval):
856
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
857
+ if logger: logger.error(err_msg)
858
+ raise ImgplotError(err_msg)
859
+
860
+ # Check 'categCol' (if not None)
861
+ if categCol is not None and len(categCol) != len(dval):
862
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
863
+ if logger: logger.error(err_msg)
864
+ raise ImgplotError(err_msg)
865
+
866
+ else:
867
+ # Possibly exclude values from zz
868
+ if excludedVal is not None:
869
+ for val in np.array(excludedVal).reshape(-1):
870
+ np.putmask(zz, zz == val, np.nan)
871
+
872
+ # Get the unique value in zz
873
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
874
+
875
+ if not len(dval) and verbose > 0: # len(dval) == 0
876
+ if logger:
877
+ logger.warning(f'{fname}: no value to be drawn!')
878
+ else:
879
+ print(f'{fname}: WARNING: no value to be drawn!')
880
+
881
+ # Replace dval[i] by i in zz and other values by np.nan
882
+ zz2 = np.array(zz) # copy array
883
+ zz[...] = np.nan # initialize
884
+ for i, v in enumerate(dval):
885
+ zz[zz2 == v] = i
886
+
887
+ del zz2
888
+
889
+ # Set 'colorList': the list of colors to use
890
+ colorList = None
891
+ if categCol is not None:
892
+ if len(categCol) >= len(dval):
893
+ colorList = [categCol[i] for i in range(len(dval))]
894
+ # colorList = [mcolors.ColorConverter().to_rgba(categCol[i]) for i in range(len(dval))]
895
+
896
+ elif categColCycle:
897
+ if verbose > 0:
898
+ if logger:
899
+ logger.warning(f'{fname}: `categCol` is used cyclically (too few entries)')
900
+ else:
901
+ print(f'{fname}: WARNING: `categCol` is used cyclically (too few entries)')
902
+ colorList = [categCol[i%len(categCol)] for i in range(len(dval))]
903
+
904
+ else:
905
+ if verbose > 0:
906
+ if logger:
907
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
908
+ else:
909
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
910
+
911
+ if colorList is None:
912
+ # Use colors from cmap
913
+ colorList = [cmap(x) for x in np.arange(len(dval)) * 1.0/(len(dval)-1)]
914
+
915
+ # Set the colormap: 'cmap'
916
+ if len(dval) == 1:
917
+ cmap = ccol.custom_cmap([colorList[0], colorList[0]], ncol=2,
918
+ cbad=categColbad, alpha=alpha)
919
+
920
+ else: # len(dval) == len(colorList) > 1
921
+ # cmap = mcolors.ListedColormap(colorList)
922
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList),
923
+ cbad=categColbad, alpha=alpha)
924
+
925
+ # Set the min and max of the colorbar
926
+ vmin, vmax = -0.5, len(dval) - 0.5
927
+
928
+ else: # categ == False
929
+ # --- Treat continuous variable ---
930
+ # Possibly exclude values from zz
931
+ if excludedVal is not None:
932
+ for v in np.array(excludedVal).reshape(-1): # force to be a 1d array
933
+ np.putmask(zz, zz == v, np.nan)
934
+
935
+ if np.all(np.isnan(zz)):
936
+ vmin, vmax= 0.0, 1.0 # any values
937
+ else:
938
+ if vmin is None:
939
+ vmin = np.nanmin(zz)
940
+
941
+ if vmax is None:
942
+ vmax = np.nanmax(zz)
943
+
944
+ col = cmap((zz-vmin)/(vmax-vmin))
945
+
946
+ return col
947
+ # ----------------------------------------------------------------------------
948
+
949
+ # ----------------------------------------------------------------------------
950
+ def drawImage2Drgb(im, nancol=(1.0, 0.0, 0.0), logger=None):
951
+ """
952
+ Displays a 2D image with 3 or 4 variables interpreted as RGB or RGBA code.
953
+
954
+ Parameters
955
+ ----------
956
+ im : :class:`geone.img.Img`
957
+ input image, with `im.nv=3` or `im.nv=4` variables interpreted as RGB or
958
+ RBGA code (normalized in [0, 1])
959
+
960
+ nancol : color, default: (1.0, 0.0, 0.0)
961
+ color (3-tuple for RGB code, 4-tuple for RGBA code, str) used for missing
962
+ value (`numpy.nan`) in the input image
963
+
964
+ logger : :class:`logging.Logger`, optional
965
+ logger (see package `logging`)
966
+ if specified, messages are written via `logger` (no print)
967
+ """
968
+ fname = 'drawImage2Drgb'
969
+
970
+ # Check image parameters
971
+ if im.nz != 1:
972
+ err_msg = f'{fname}: `im.nz` must be 1'
973
+ if logger: logger.error(err_msg)
974
+ raise ImgplotError(err_msg)
975
+
976
+ if im.nv != 3 and im.nv != 4:
977
+ err_msg = f'{fname}: `im.nv` must be 3 or 4'
978
+ if logger: logger.error(err_msg)
979
+ raise ImgplotError(err_msg)
980
+
981
+ vv = im.val.reshape(im.nv, -1).T
982
+
983
+ if vv.shape[1] == 3:
984
+ nancolf = mcolors.to_rgb(nancol)
985
+ else: # vv.shape[1] == 4
986
+ nancolf = mcolors.to_rgba(nancol)
987
+
988
+ ind_isnan = np.any(np.isnan(vv), axis=1)
989
+ vv[ind_isnan, :] = nancolf
990
+
991
+ min0, max0 = im.ox, im.xmax()
992
+ min1, max1 = im.oy, im.ymax()
993
+
994
+ plt.imshow(vv.reshape(im.ny, im.nx, -1), origin='lower', extent=[min0, max0, min1, max1])
995
+ # ----------------------------------------------------------------------------
996
+
997
+ # ----------------------------------------------------------------------------
998
+ def drawGeobodyMap2D(im, iv=0, logger=None):
999
+ """
1000
+ Displays a geobody 2D map, with adapted color bar.
1001
+
1002
+ Parameters
1003
+ ----------
1004
+ im : :class:`geone.img.Img`
1005
+ input image, with variable of index `iv` interpreted as geobody labels,
1006
+ i.e.:
1007
+
1008
+ - value 0: cell not in the considered medium,
1009
+ - value n > 0: cell in the n-th geobody (connected component)
1010
+
1011
+ iv : int, default: 0
1012
+ index of the variable to be displayed
1013
+
1014
+ logger : :class:`logging.Logger`, optional
1015
+ logger (see package `logging`)
1016
+ if specified, messages are written via `logger` (no print)
1017
+ """
1018
+ # fname = 'drawGeobodyMap2D'
1019
+
1020
+ categ = True
1021
+ ngeo = int(im.val[iv].max())
1022
+ categVal = [i for i in range(1, ngeo+1)]
1023
+ categCol = None
1024
+ if ngeo <= 10:
1025
+ categCol = plt.get_cmap('tab10').colors[:ngeo]
1026
+ cticks = np.arange(ngeo)
1027
+ cticklabels = 1 + cticks
1028
+ elif ngeo <= 20:
1029
+ categCol = plt.get_cmap('tab20').colors[:ngeo]
1030
+ cticks = np.arange(ngeo)
1031
+ cticklabels = 1 + cticks
1032
+ elif ngeo <= 40:
1033
+ categCol = plt.get_cmap('tab20b').colors + plt.get_cmap('tab20c').colors[:ngeo-20]
1034
+ cticks = np.arange(0,ngeo,5)
1035
+ cticklabels = 1 + cticks
1036
+ else:
1037
+ categ = False
1038
+ cticks = None
1039
+ cticklabels = None
1040
+ drawImage2D(im, iv=iv, excludedVal=0, categ=categ, categVal=categVal, categCol=categCol,
1041
+ cticks=cticks, cticklabels=cticklabels, logger=logger)
1042
+ # ----------------------------------------------------------------------------
1043
+
1044
+ # ----------------------------------------------------------------------------
1045
+ def writeImage2Dppm(
1046
+ im,
1047
+ filename,
1048
+ ix=None, iy=None, iz=None, iv=0,
1049
+ cmap=ccol.cmap_def,
1050
+ excludedVal=None,
1051
+ categ=False, categVal=None, categCol=None,
1052
+ vmin=None, vmax=None,
1053
+ verbose=1,
1054
+ logger=None):
1055
+ """
1056
+ Writes an image in a file in ppm format.
1057
+
1058
+ The colors according the to given settings, as defined in the function
1059
+ `drawImage2D` are used.
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ im : :class:`geone.img.Img`
1064
+ input image
1065
+
1066
+ filename : str
1067
+ name of the file
1068
+
1069
+ ix : see function :func:`drawImage2D`
1070
+
1071
+ iy : see function :func:`drawImage2D`
1072
+
1073
+ iz : see function :func:`drawImage2D`
1074
+
1075
+ iv : see function :func:`drawImage2D`
1076
+
1077
+ cmap : see function :func:`drawImage2D`
1078
+
1079
+ excludedVal : see function :func:`drawImage2D`
1080
+
1081
+ categ : see function :func:`drawImage2D`
1082
+
1083
+ categVal : see function :func:`drawImage2D`
1084
+
1085
+ categCol : see function :func:`drawImage2D`
1086
+
1087
+ vmin : see function :func:`drawImage2D`
1088
+
1089
+ vmax : see function :func:`drawImage2D`
1090
+
1091
+ verbose : int, default: 1
1092
+ verbose mode, higher implies more printing (info)
1093
+
1094
+ logger : :class:`logging.Logger`, optional
1095
+ logger (see package `logging`)
1096
+ if specified, messages are written via `logger` (no print)
1097
+ """
1098
+ fname = 'writeImage2Dppm'
1099
+
1100
+ # Check iv
1101
+ if iv < 0:
1102
+ iv = im.nv + iv
1103
+
1104
+ if iv < 0 or iv >= im.nv:
1105
+ err_msg = f'{fname}: invalid `iv` index'
1106
+ if logger: logger.error(err_msg)
1107
+ raise ImgplotError(err_msg)
1108
+
1109
+ # Check slice direction and indices
1110
+ n = int(ix is not None) + int(iy is not None) + int(iz is not None)
1111
+
1112
+ if n == 0:
1113
+ sliceDir = 'z'
1114
+ iz = 0
1115
+
1116
+ elif n==1:
1117
+ if ix is not None:
1118
+ if ix < 0:
1119
+ ix = im.nx + ix
1120
+
1121
+ if ix < 0 or ix >= im.nx:
1122
+ err_msg = f'{fname}: invalid `ix` index'
1123
+ if logger: logger.error(err_msg)
1124
+ raise ImgplotError(err_msg)
1125
+
1126
+ sliceDir = 'x'
1127
+
1128
+ elif iy is not None:
1129
+ if iy < 0:
1130
+ iy = im.ny + iy
1131
+
1132
+ if iy < 0 or iy >= im.ny:
1133
+ err_msg = f'{fname}: invalid `iy` index'
1134
+ if logger: logger.error(err_msg)
1135
+ raise ImgplotError(err_msg)
1136
+
1137
+ sliceDir = 'y'
1138
+
1139
+ else: # iz is not None
1140
+ if iz < 0:
1141
+ iz = im.nz + iz
1142
+
1143
+ if iz < 0 or iz >= im.nz:
1144
+ err_msg = f'{fname}: invalid `iz` index'
1145
+ if logger: logger.error(err_msg)
1146
+ raise ImgplotError(err_msg)
1147
+
1148
+ sliceDir = 'z'
1149
+
1150
+ else: # n > 1
1151
+ err_msg = f'{fname}: slice specified in more than one direction'
1152
+ if logger: logger.error(err_msg)
1153
+ raise ImgplotError(err_msg)
1154
+
1155
+ # Extract what to be plotted
1156
+ if sliceDir == 'x':
1157
+ dim0 = im.ny
1158
+ min0 = im.oy
1159
+ max0 = im.ymax()
1160
+
1161
+ dim1 = im.nz
1162
+ min1 = im.oz
1163
+ max1 = im.zmax()
1164
+ zz = np.array(im.val[iv, :, :, ix].reshape(dim1, dim0)) # np.array() to get a copy
1165
+ # reshape to force 2-dimensional array
1166
+
1167
+ elif sliceDir == 'y':
1168
+ dim0 = im.nx
1169
+ min0 = im.ox
1170
+ max0 = im.xmax()
1171
+
1172
+ dim1 = im.nz
1173
+ min1 = im.oz
1174
+ max1 = im.zmax()
1175
+ zz = np.array(im.val[iv, :, iy, :].reshape(dim1, dim0)) # np.array() to get a copy
1176
+
1177
+ else: # sliceDir == 'z'
1178
+ dim0 = im.nx
1179
+ min0 = im.ox
1180
+ max0 = im.xmax()
1181
+
1182
+ dim1 = im.ny
1183
+ min1 = im.oy
1184
+ max1 = im.ymax()
1185
+ zz = np.array(im.val[iv, iz, :, :].reshape(dim1, dim0)) # np.array() to get a copy
1186
+
1187
+ if categ:
1188
+ # --- Treat categorical variable ---
1189
+ if categCol is not None \
1190
+ and type(categCol) is not list \
1191
+ and type(categCol) is not tuple:
1192
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
1193
+ if logger: logger.error(err_msg)
1194
+ raise ImgplotError(err_msg)
1195
+
1196
+ # Get array 'dval' of displayed values
1197
+ if categVal is not None:
1198
+ dval = np.array(categVal).reshape(-1) # force to be a 1d array
1199
+
1200
+ if len(np.unique(dval)) != len(dval):
1201
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
1202
+ if logger: logger.error(err_msg)
1203
+ raise ImgplotError(err_msg)
1204
+
1205
+ # Check 'categCol' (if not None)
1206
+ if categCol is not None and len(categCol) != len(dval):
1207
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
1208
+ if logger: logger.error(err_msg)
1209
+ raise ImgplotError(err_msg)
1210
+
1211
+ else:
1212
+ # Possibly exclude values from zz
1213
+ if excludedVal is not None:
1214
+ for val in np.array(excludedVal).reshape(-1):
1215
+ np.putmask(zz, zz == val, np.nan)
1216
+
1217
+ # Get the unique value in zz
1218
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
1219
+
1220
+ if not len(dval): # len(dval) == 0
1221
+ err_msg = f'{fname}: no value to be drawn' # Warning instead and not raise error...
1222
+ if logger: logger.error(err_msg)
1223
+ raise ImgplotError(err_msg)
1224
+
1225
+ # Replace dval[i] by i in zz and other values by np.nan
1226
+ zz2 = np.array(zz) # copy array
1227
+ zz[...] = np.nan # initialize
1228
+ for i, v in enumerate(dval):
1229
+ zz[zz2 == v] = i
1230
+
1231
+ del zz2
1232
+
1233
+ # Set 'colorList': the list of colors to use
1234
+ colorList = None
1235
+ if categCol is not None:
1236
+ if len(categCol) >= len(dval):
1237
+ colorList = [categCol[i] for i in range(len(dval))]
1238
+
1239
+ else:
1240
+ if verbose > 0:
1241
+ if logger:
1242
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
1243
+ else:
1244
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
1245
+
1246
+ if colorList is None:
1247
+ # Use colors from cmap
1248
+ colorList = [cmap(0)]
1249
+ if len(dval) > 1:
1250
+ t = 1./(len(dval)-1)
1251
+ for i in range(1,len(dval)):
1252
+ colorList.append(cmap(i*t))
1253
+
1254
+ # Set the colormap: 'cmap'
1255
+ if len(dval) == 1:
1256
+ cmap = ccol.custom_cmap([colorList[0], colorList[0]], ncol=2,
1257
+ cbad=ccol.cbad_def)
1258
+
1259
+ else: # len(dval) == len(colorList) > 1
1260
+ # cmap = mcolors.ListedColormap(colorList)
1261
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList),
1262
+ cbad=ccol.cbad_def)
1263
+
1264
+ # Set the min and max of the colorbar
1265
+ vmin, vmax = -0.5, len(dval) - 0.5
1266
+
1267
+ # Set colorbar ticks and ticklabels if not given
1268
+ if cticks is None:
1269
+ cticks = range(len(dval))
1270
+
1271
+ if cticklabels is None:
1272
+ cticklabels = [f'{v:.3g}' for v in dval]
1273
+
1274
+ # Reset cextend if needed
1275
+ colorbar_extend = 'neither'
1276
+
1277
+ else: # categ == False
1278
+ # --- Treat continuous variable ---
1279
+ # Possibly exclude values from zz
1280
+ if excludedVal is not None:
1281
+ for val in np.array(excludedVal).reshape(-1): # force to be a 1d array
1282
+ np.putmask(zz, zz == val, np.nan)
1283
+
1284
+ # Get dimension of zz, flip zz vertically, then reshape as a list of value
1285
+ ny = zz.shape[0]
1286
+ nx = zz.shape[1]
1287
+ zz = zz[list(reversed(range(zz.shape[0]))),:].reshape(-1)
1288
+
1289
+ # Set vmin and vmax (if needed)
1290
+ if vmin is None:
1291
+ vmin = np.nanmin(zz)
1292
+
1293
+ if vmax is None:
1294
+ vmax = np.nanmax(zz)
1295
+
1296
+ # Get indices of bad, under, over value
1297
+ ind_bad = np.isnan(zz)
1298
+ ind_under = np.all((~ind_bad, zz < vmin),0)
1299
+ ind_over = np.all((~ind_bad, zz > vmax),0)
1300
+
1301
+ # Normalize value according to colorbar
1302
+ zz = np.maximum(np.minimum((zz-vmin)/(vmax-vmin), 1.0), 0.0)
1303
+
1304
+ # Get rgba code at each pixel
1305
+ rgba_arr = np.asarray([cmap(v) for v in zz])
1306
+
1307
+ if cmap._rgba_bad is not None:
1308
+ rgba_arr[ind_bad] = cmap._rgba_bad
1309
+
1310
+ if cmap._rgba_under is not None:
1311
+ rgba_arr[ind_under] = cmap._rgba_under
1312
+
1313
+ if cmap._rgba_over is not None:
1314
+ rgba_arr[ind_over] = cmap._rgba_over
1315
+
1316
+ # Convert rgb from 0-1 to integer in 0-255 (ignored a chanel)
1317
+ zz = np.round(rgba_arr[:,0:3]*255)
1318
+
1319
+ # Write file (ppm)
1320
+ shead = ("P3\n"
1321
+ "# CREATED BY PYTHON3-CODE\n"
1322
+ "{0} {1}\n"
1323
+ "255\n").format(nx, ny) # header of ppm file
1324
+ # Open the file in write binary mode
1325
+ with open(filename,'wb') as ff:
1326
+ ff.write(shead.encode()) # write header
1327
+ # Write rgb values
1328
+ np.savetxt(ff, zz.reshape(-1), fmt='%d')
1329
+ # ----------------------------------------------------------------------------
1330
+
1331
+ if __name__ == "__main__":
1332
+ print("Module 'geone.imgplot' example:")
1333
+ import matplotlib.pyplot as plt
1334
+
1335
+ # Set image with 50 variables
1336
+ # ---------------------------
1337
+ # Set domain and number of cell in each direction
1338
+ xmin, xmax = -2, 2
1339
+ ymin, ymax = -1, 2
1340
+ nx, ny = 200, 150
1341
+
1342
+ # Set the cell size
1343
+ sx, sy = (xmax-xmin)/nx, (ymax-ymin)/ny
1344
+
1345
+ # Set the meshgrid
1346
+ x, y = xmin + 0.5 * sx + sx * np.arange(nx), ymin + 0.5 * sy + sy * np.arange(ny)
1347
+ # # equivalently:
1348
+ # x, y = np.arange(xmin+sx/2, xmax, sx), np.arange(ymin+sy/2, ymax ,sy)
1349
+ # x, y = np.linspace(xmin+sx/2, xmax-sx/2, nx), np.linspace(ymin+sy/2, ymax-sy/2, ny)
1350
+ xx,yy = np.meshgrid(x, y)
1351
+
1352
+ # function values
1353
+ zz = xx**2 + yy**2 - 2
1354
+
1355
+ # Set some values to nan
1356
+ zz[np.where(zz < -1.7)] = np.nan
1357
+
1358
+ # set image, where each variable consists in
1359
+ # the function values 'zz' + a gaussian noise
1360
+ nv = 50
1361
+ im = Img(nx=nx, ny=ny, nz=1, nv=nv,
1362
+ sx=sx, sy=sy, sz=1.0,
1363
+ ox=xmin, oy=ymin, oz=0.0)
1364
+
1365
+ for i in range(nv):
1366
+ im.set_var(ind=i, val=zz.reshape(-1)+np.random.normal(size=im.nxy()))
1367
+
1368
+ # Compute the mean and standard deviation
1369
+ # ---------------------------------------
1370
+ imMean = img.imageContStat(im,op='mean')
1371
+ imStd = img.imageContStat(im,op='std',ddof=1)
1372
+
1373
+ # Draw images
1374
+ # -----------
1375
+ # Set min and max value to be displayed
1376
+ vmin, vmax = -1.0, 3.0
1377
+
1378
+ fig, ax = plt.subplots(2,2,figsize=(12,10))
1379
+ plt.subplot(2,2,1)
1380
+ drawImage2D(im, iv=0, vmin=vmin, vmax=vmax, title='1-st real',
1381
+ colorbar_extend='both')
1382
+
1383
+ plt.subplot(2,2,2)
1384
+ drawImage2D(im, iv=1, vmin=vmin, vmax=vmax, title='2-nd real',
1385
+ colorbar_extend='both')
1386
+
1387
+ plt.subplot(2,2,3)
1388
+ drawImage2D(imMean, vmin=vmin, vmax=vmax,
1389
+ title=f'Mean over {nv} real',
1390
+ colorbar_extend='both')
1391
+
1392
+ plt.subplot(2,2,4)
1393
+ drawImage2D(imStd, title=f'Std over {nv} real')
1394
+
1395
+ # plt.tight_layout()
1396
+
1397
+ # fig.show()
1398
+ plt.show()
1399
+
1400
+ # Copy im and categorize
1401
+ # ----------------------
1402
+ imCat = img.copyImg(im)
1403
+ bins = np.array([-np.inf, -1., 0., 1., np.inf])
1404
+ # set category j to a value v of image im as follows:
1405
+ # bins[j-1] <= v < bins[j], for j=1,...,bins.size-1
1406
+ # and j=np.nan if v is np.nan
1407
+
1408
+ defInd = np.array(~np.isnan(im.val)).reshape(-1) # defined indices
1409
+
1410
+ imCat.val[...] = np.nan # initialization
1411
+
1412
+ for j in range(1, bins.size):
1413
+ # Set category j
1414
+ imCat.val[np.all((defInd,
1415
+ np.asarray(im.val >= bins[j-1]).reshape(-1),
1416
+ np.asarray(im.val < bins[j]).reshape(-1)),
1417
+ 0).reshape(imCat.val.shape)] = j
1418
+
1419
+ categ = list(range(1, bins.size))
1420
+ imCatProp = img.imageCategProp(imCat,categ)
1421
+
1422
+ # Draw images
1423
+ # -----------
1424
+ # Set background color to "white"
1425
+ mpl_rcParams['figure.facecolor'] = 'white'
1426
+
1427
+ fig, ax = plt.subplots(2,3,figsize=(18,10))
1428
+ plt.subplot(2,3,1)
1429
+ drawImage2D(imCat, iv=0, categ=True, title='1-st real')
1430
+
1431
+ plt.subplot(2,3,2)
1432
+ # drawImage2D(imCat, iv=1, categ=True, title='2-nd real')
1433
+ drawImage2D(imCat, iv=1, categ=True, title='2-nd real',
1434
+ title_fontsize=18, title_fontweight='bold',
1435
+ xlabel="x-axis", xlabel_fontsize=8,
1436
+ xticks=[-2,0,2], xticklabels_fontsize=8,
1437
+ clabel="facies",clabel_fontsize=16,clabel_rotation=90,
1438
+ cticklabels=['A','B','C','D'],cticklabels_fontsize=8)
1439
+
1440
+ plt.subplot(2,3,3)
1441
+ drawImage2D(imCatProp, iv=0, vmin=0, vmax=0.7, colorbar_extend='max',
1442
+ title=f'Prop. of "{categ[0]}" over {nv} real')
1443
+
1444
+ plt.subplot(2,3,4)
1445
+ drawImage2D(imCatProp, iv=1, vmin=0, vmax=0.7, colorbar_extend='max',
1446
+ title=f'Prop. of "{categ[1]}" over {nv} real')
1447
+
1448
+ plt.subplot(2,3,5)
1449
+ drawImage2D(imCatProp, iv=2, vmin=0, vmax=0.7, colorbar_extend='max',
1450
+ title=f'Prop. of "{categ[2]}" over {nv} real')
1451
+
1452
+ plt.subplot(2,3,6)
1453
+ drawImage2D(imCatProp, iv=3, vmin=0, vmax=0.7, colorbar_extend='max',
1454
+ title=f'Prop. of "{categ[3]}" over {nv} real',
1455
+ cticks=np.arange(0,.8,.1), cticklabels=[f'{i:4.2f}' for i in np.arange(0,.8,.1)],
1456
+ cticklabels_fontweight='bold')
1457
+
1458
+ plt.suptitle('Categorized images...')
1459
+ # plt.tight_layout()
1460
+
1461
+ # fig.show()
1462
+ plt.show()
1463
+
1464
+ a = input("Press enter to continue...")