Source code for wolfhece.wolf_array._selection_data

"""
Author: HECE - University of Liege, Pierre Archambeau
Date: 2024

Copyright (c) 2024 University of Liege. All rights reserved.

This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""

from __future__ import annotations

import logging
import numpy as np
import numpy.ma as ma
from typing import Literal, TYPE_CHECKING
from enum import Enum

try:
    from OpenGL.GL import *
except ImportError:
    pass

try:
    import wx
except ImportError:
    pass

from ..PyTranslate import _
from ._header_wolf import header_wolf
from ..PyVertexvectors import vector, zone

if TYPE_CHECKING:
    from ._base import WolfArrayModel as WolfArray
    from ._mb import WolfArrayMB


[docs] ALL_SELECTED = 'all'
[docs] class StorageMode(Enum): """ Storage mode for selections in WolfArray """
[docs] LIST = 0
[docs] ARRAY = 1
[docs] class SelectionData(): """ User-selected data in a WolfArray Contains two storage elements : - myselection - selections( dict): Stored selection(s) to be used, for example, in a spatial interpolation operation. These selections are only lost in the event of a general reset. The selected nodes are stored using their "world" spatial coordinates so that they can be easily transferred to other objects. The "myselection" can be stored in two modes : - LIST: 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) for all nodes and excluded nodes - ARRAY: np.ndarray of selected nodes (boolean array) with shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively. The boolean array is True if the node is selected, False if not selected. The selection can be converted from one mode to another automatically based on the number of nodes in the array. If the number of nodes exceeds a threshold (default is 100,000), the selection is stored as an ARRAY. Otherwise, it is stored as a LIST. The selection can be forced to a specific storage mode using the `force_storage_mode` method. """ # 'all' or list of (x, y) coordinates or a tuple of ('all', np.ndarray) for all nodes and excluded nodes
[docs] _myselection:list[tuple[float, float]] | str | tuple[str, np.ndarray]
[docs] selections: dict[str:dict['select':list[tuple[float, float]], 'idgllist':int, 'color':list[float]]]
def __init__(self, parent:"WolfArray", threshold_array_mode:int = 100_000) -> None: """ Initialize the SelectionData object. :param parent: The parent WolfArray object to which this selection data belongs. :param threshold_array_mode: The threshold number of nodes to switch from LIST to ARRAY storage mode. """
[docs] self.parent: WolfArray
self.parent = parent
[docs] self.wx_exists = wx.GetApp() is not None
self._myselection = [] self.selections = {}
[docs] self._boolarray: np.ndarray | None = None # boolean array for selection - True if selected, False if not selected
[docs] self._storage_mode = StorageMode.LIST # 0: 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) for all nodes and excluded nodes, 1 for np.ndarray of selected nodes
[docs] self.update_plot_selection = False # force to update OpenGL list if True
[docs] self.hideselection = False
[docs] self.numlist_select = 0 # OpenGL list index
[docs] self.threshold_array_mode = threshold_array_mode # Default threshold for switching storage mode
[docs] def _auto_storage_mode(self): """ Choose the storage mode based on the number of nodes in the array """ if self.nb > self.threshold_array_mode: _storage_mode = StorageMode.ARRAY else: _storage_mode = StorageMode.LIST if self._storage_mode != _storage_mode: self._convert_to_storage_mode(_storage_mode)
[docs] def _convert_to_storage_mode(self, new_mode:StorageMode): """ Convert the selection to the new storage mode. :param new_mode: The new storage mode to convert to (StorageMode.LIST or StorageMode.ARRAY). """ logging.info('Switching storage mode to {}'.format(new_mode)) if self._storage_mode == StorageMode.LIST: # Convert from 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) to np.ndarray _bool_array = self._myselection_as_array if self._myselection == ALL_SELECTED: _bool_array[:,:] = True elif isinstance(self._myselection, list): if len(self._myselection) == 0: _bool_array[:,:] = False else: _bool_array[:,:] = False xy = np.asarray(self._myselection) ij = self.parent.xy2ij_np(xy) _bool_array[ij[:, 0], ij[:, 1]] = True elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: _bool_array[:,:] = True _excluded_nodes = self._myselection[1] if _excluded_nodes.size > 0: _excluded_nodes = self.parent.xy2ij_np(_excluded_nodes) _bool_array[_excluded_nodes[:, 0], _excluded_nodes[:, 1]] = False self._myselection = [] elif self._storage_mode == StorageMode.ARRAY: if self.is_all_selected(): self._myselection = ALL_SELECTED else: nb = self.nb if nb == 0: self._myselection = [] elif nb / max(self.parent.nbnotnull, 1) > 0.5: ij_not_selected = np.argwhere(~self._myselection_as_array) xy_not_selected = self.parent.ij2xy_np(ij_not_selected) self._myselection = (ALL_SELECTED, xy_not_selected) else: ij_selected = np.argwhere(self._myselection_as_array) xy_selected = self.parent.ij2xy_np(ij_selected) self._myselection = list(map(tuple, xy_selected)) self._storage_mode = new_mode
[docs] def force_storage_mode(self, new_mode:StorageMode): """ Force the storage mode to the new mode. :param new_mode: The new storage mode to force (StorageMode.LIST or StorageMode.ARRAY). """ if new_mode not in StorageMode: logging.error('Invalid storage mode - must be LIST or ARRAY') return if self._storage_mode != new_mode: self._convert_to_storage_mode(new_mode)
@property
[docs] def myselection(self) -> list[tuple[float, float]] | str: """ Current selection of nodes. Returns: - 'all' if all nodes are selected - list of (x, y) coordinates if specific nodes are selected """ if self._storage_mode == StorageMode.LIST: if self._myselection == ALL_SELECTED: return ALL_SELECTED elif isinstance(self._myselection, list): return self._myselection elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: excluded_nodes = self._myselection[1] if excluded_nodes.shape[0] == 0: return ALL_SELECTED else: if self.parent.usemask: # Return all nodes except the excluded ones loc_mask = ~ self.parent.array.mask.copy() else: loc_mask = np.ones((self.parent.nbx, self.parent.nby), dtype=bool) ij_excluded = self.parent.xy2ij_np(excluded_nodes) loc_mask[ij_excluded[:, 0], ij_excluded[:, 1]] = False ij = np.argwhere(loc_mask) xy = self.parent.ij2xy_np(ij) return list(map(tuple, xy)) # Convert to list of tuples elif self._storage_mode == StorageMode.ARRAY: if self.is_all_selected(): return ALL_SELECTED else: nbsel = np.count_nonzero(self._myselection_as_array) if nbsel == 0: return [] else: # Convert boolean array to list of (x, y) coordinates ij_selected = np.argwhere(self._myselection_as_array) xy_selected = self.parent.ij2xy_np(ij_selected) return list(map(tuple, xy_selected)) else: logging.error('Invalid storage mode - must be LIST or ARRAY') return []
@property
[docs] def _myselection_as_array(self) -> np.ndarray: """ Current selection of nodes as a numpy array. Returns: - np.ndarray of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively. - True if the node is selected, False if not selected. """ if self._storage_mode == StorageMode.LIST: if self._boolarray is not None: return self._boolarray self._boolarray = np.zeros((self.parent.nbx, self.parent.nby), dtype=bool) if self._myselection == ALL_SELECTED: if self.parent.usemask: self._boolarray[:,:] = ~self.parent.array.mask[:,:] else: self._boolarray[:,:] = True elif isinstance(self._myselection, list): # Convert list of (x, y) coordinates to boolean array if len(self._myselection) > 0: xy= np.asarray(self._myselection) ij = self.parent.xy2ij_np(xy) self._boolarray[ij[:, 0], ij[:, 1]] = True elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes if self.parent.usemask: self._boolarray[:,:] = ~self.parent.array.mask[:,:] else: self._boolarray[:,:] = True # Exclude nodes from the second element of the tuple excluded_nodes = self.myselection[1] if excluded_nodes.size > 0: excluded_ij = self.parent.xy2ij_np(excluded_nodes) self._boolarray[excluded_ij[:, 0], excluded_ij[:, 1]] = False elif self._storage_mode == StorageMode.ARRAY: if self._boolarray is None: self._boolarray = np.zeros((self.parent.nbx, self.parent.nby), dtype=bool) return self._boolarray
@_myselection_as_array.setter def _myselection_as_array(self, value: np.ndarray): """ Set the current selection of nodes as a numpy array. :param value: numpy array of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively. True if the node is selected, False if not selected. """ if not isinstance(value, np.ndarray): logging.error('Invalid value for myselection_as_array - must be a numpy array') if value.shape != (self.parent.nbx, self.parent.nby): logging.error('Invalid shape for myselection_as_array - must be ({}, {})'.format (self.parent.nbx, self.parent.nby)) if self._boolarray is None: self._boolarray = value.copy() else: self._boolarray[:,:] = value[:,:] @property
[docs] def myselection_npargwhere(self) -> np.ndarray: """ Current selection of nodes as a numpy array using np.argwhere """ return np.argwhere(self._myselection_as_array)
@myselection.setter def myselection(self, value: list[tuple[float, float]] | str | tuple[str, np.ndarray]): """ Set the current selection of nodes. :param value: 'all' to select all nodes, a list of (x, y) coordinates to select specific nodes, or a tuple of ('all', np.ndarray) for all nodes and excluded nodes. """ if self._storage_mode == StorageMode.ARRAY: # Convert to the new storage mode if necessary if isinstance(value, list): if len(value) == 0: self._myselection_as_array[:,:] = False else: ij = self.parent.xy2ij_np(np.array(value)) self._myselection_as_array[:,:] = False self._myselection_as_array[ij[:, 0], ij[:, 1]] = True elif isinstance(value, str): if value == ALL_SELECTED: if self.parent.usemask: self._myselection_as_array[:,:] = ~self.parent.array.mask[:,:] else: self._myselection_as_array[:,:] = True else: logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates') elif isinstance(value, tuple) and len(value) == 2 and value[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes if self.parent.usemask: self._myselection_as_array[:,:] = ~self.parent.array.mask[:,:] else: self._myselection_as_array[:,:] = True excluded_nodes = value[1] if excluded_nodes.size > 0: ij_excluded = self.parent.xy2ij_np(excluded_nodes) self._myselection_as_array[ij_excluded[:, 0], ij_excluded[:, 1]] = False else: logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates or a tuple of ("all", np.ndarray)') elif self._storage_mode == StorageMode.LIST: if isinstance(value, list): if len(value) == 0: self._myselection = [] else: self._myselection = value elif isinstance(value, str): if value == ALL_SELECTED: self._myselection = ALL_SELECTED else: logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates') elif isinstance(value, tuple) and len(value) == 2 and value[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes self._myselection = value else: logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates or a tuple of ("all", np.ndarray)') self.update_nb_nodes_selection()
[docs] def is_all_selected(self) -> bool: """ Check if all nodes are selected. :return: True if all nodes are selected, False otherwise """ if self._storage_mode == StorageMode.LIST: if self.myselection == ALL_SELECTED: return True elif isinstance(self.myselection, list): if self.parent.usemask: return len(self.myselection) == self.parent.nbnotnull else : return len(self.myselection) == self.parent.nbx * self.parent.nby elif isinstance(self.myselection, tuple) and len(self.myselection) == 2 and self.myselection[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes if self.myselection[1].size == 0: return True return False elif self._storage_mode == StorageMode.ARRAY: if self.nb > 0: # Check if all nodes are selected in the boolean array if self.parent.usemask: return np.all(self._myselection_as_array) else: return np.all(self._myselection_as_array == ~self.parent.array.mask) else: return False
[docs] def set_selection_from_list_xy(self, xylist: list[tuple[float, float]]): """ Set the current selection from a list of (x, y) coordinates. Alias for myselection setter to set a list of (x, y) coordinates. This will convert the list to the appropriate storage mode if necessary. """ self.myselection = xylist
@property
[docs] def dx(self) -> float: """ Resolution in x """ if self.parent is None: return 0. else: return self.parent.dx
@property
[docs] def dy(self) -> float: """ Resolution in y """ if self.parent is None: return 0. else: return self.parent.dy
@property
[docs] def nb(self) -> int: """ Number of selected nodes. """ if self._storage_mode == StorageMode.LIST: if self._myselection == ALL_SELECTED: if self.parent.usemask: return self.parent.nbnotnull else: return self.parent.nbx * self.parent.nby elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes if self.parent.usemask: return self.parent.nbnotnull - self._myselection[1].shape[0] else: return self.parent.nbx * self.parent.nby - self._myselection[1].shape[0] else: return len(self._myselection) elif self._storage_mode == StorageMode.ARRAY: return np.count_nonzero(self._myselection_as_array)
[docs] def unmask_selection(self, resetplot:bool=True): """ Unmask selection """ if self.nb == 0: return self.parent.array.mask[self.myselection_npargwhere] = False if resetplot: self.parent.reset_plot()
[docs] def reset(self): """ Reset the selection """ self.myselection = [] self._boolarray = None # Reset the boolean array self._storage_mode = StorageMode.LIST # Reset the storage mode to LIST
[docs] def reset_all(self): """ Reset the selection """ self.reset() self.selections = {}
[docs] def get_string(self, which:str = None, all_memories:bool= False) -> str: """ Get string of the current selection or of a stored one. :param which: id/key of the selection to get the string for. If None, get the current selection. :param all_memories: If True, include all stored selections in the output string. :return: String representation of the selection. """ if which is None: curlist = self.myselection txt = 'X\tY\n' else: if str(which) in self.selections: all_memories = False curlist = self.selections[str(which)]['select'] txt = 'Selection {}\n'.format(which) txt += 'Color : {}\n'.format(self.selections[str(which)]['color']) txt += 'X\tY\n' else: logging.error(_('Selection {} does not exist').format(which)) return '' if len(curlist) == 0: return '' if curlist == ALL_SELECTED: txt += 'all\n' return txt for cur in curlist: txt += str(cur[0]) + '\t' + str(cur[1]) + '\n' txt += 'i\tj\t1-based indices\n' for cur in curlist: i,j = self.parent.get_ij_from_xy(cur[0], cur[1], aswolf=True) txt += str(i) + '\t' + str(j) + '\n' if all_memories: for key, cur in self.selections.items(): txt += self.get_string(key) return txt
[docs] def get_script(self, which:int = None) -> str: """ Get script of the current selection or of a stored one. :param which: id/key of the selection to get the script for. If None, get the current selection. :return: Script representation of the selection. """ if self.myselection == ALL_SELECTED: logging.error(_('Cannot create script for "all" selection')) return '' txt = '# script adapted to a WolfGPU script\n' txt += '# - (i,j) are 1-based for add_boundary_condition -- Do not forget to adapt BC type, value and direction or use BC Manager\n' txt += '# - (i,j) are 0-based for infiltration zones\n\n' if which is None: curlist = self.myselection idx = 0 else: if str(which) in self.selections: txt += '# Selection {}\n'.format(which) curlist = self.selections[str(which)]['select'] idx = which else: logging.error(_('Selection {} does not exist').format(which)) return '' if len(curlist) == 0: return '' txt += '# For boundary conditions :\n' for cur in curlist: i,j = self.parent.get_ij_from_xy(cur[0], cur[1], aswolf=True) txt += "simul.add_boundary_condition(i={}, j={}, bc_type=BoundaryConditionsTypes.FROUDE_NORMAL, bc_value=.3, border=Direction.LEFT)".format(i, j) + '\n' txt += '\n\n# For infiltration zones :\n' for cur in curlist: i,j = self.parent.get_ij_from_xy(cur[0], cur[1], aswolf=True) txt += "infiltration_zones.array[{},{}]={}".format(i-1, j-1, idx) + '\n' txt += '\n\n"""If needed, selection as string :\n' txt += self.get_string(which) txt += '"""\n' return txt
[docs] def copy_to_clipboard(self, which:int = None, typestr:Literal['string', 'script'] = 'string'): """ Copy current selection to clipboard. -- ONLY WORKS WITH wxPython -- :param which: id/key of the selection to copy. If None, copy the current selection. :param typestr: Type of data to copy to clipboard - 'string' for string representation, 'script' for script representation. """ if self.wx_exists: if wx.TheClipboard.Open(): wx.TheClipboard.Clear() if typestr == 'string': wx.TheClipboard.SetData(wx.TextDataObject(self.get_string(which))) else: wx.TheClipboard.SetData(wx.TextDataObject(self.get_script(which))) wx.TheClipboard.Close() else: logging.warning(_('Cannot open the clipboard')) else: logging.warning(_('Clipboard is not available in this environment'))
[docs] def reselect_from_memory(self, idx:list[str] = None): """ Reselect a stored selection :param idx: id/key of the selection to reselect. If None, show a dialog to choose from available selections. """ if idx is None: if not self.wx_exists: logging.error(_('Cannot reselect from memory - no wxPython available')) logging.error(_('Please use the method reselect_from_memory with a list of keys')) return keys = list(self.selections.keys()) keys = [cur for cur in keys if len(self.selections[cur]['select']) > 0] with wx.MultiChoiceDialog(None, "Choose the memory to reselect", "Choices", keys+['All']) as dlg: ret = dlg.ShowModal() if ret == wx.ID_CANCEL: return idx = dlg.GetSelections() if len(idx) == 0: return if len(idx) == 1 and idx[0] == len(keys): idx = keys elif len(idx) in idx: idx = keys else: idx = [keys[i] for i in idx] for curidx in idx: if curidx in self.selections: self.add_nodes_to_selection(self.selections[curidx]['select']) # self.myselection += self.selections[curidx]['select'] else: logging.error(_('Selection {} does not exist').format(idx)) # self.update_nb_nodes_selection() self.parent.reset_plot()
[docs] def move_selectionto(self, idx:str, color:list[float], resetplot:bool=True): """ Transfer current selection to dictionary :param idx: id/key of the selection :param color: color of the selection - list of 4 integers between 0 and 255 :param resetplot: if True, reset the plot after moving the selection """ assert len(color) == 4, "color must be a list of 4 integers between 0 and 255" # force idx to be a string idtxt = str(idx) self.selections[idtxt] = {} curdict = self.selections[idtxt] curdict['select'] = self.myselection curdict['idgllist'] = 0 # will be created later - index of OpenGL list curdict['color'] = color self.myselection = [] # reset current selection # self.update_nb_nodes_selection() if resetplot: self.parent.reset_plot()
[docs] def plot_selection(self): """ Plot current selection and stored selections """ # Make a copy of the current value of the flag because it will be modified in the function _plot_selection # So, if we want to update the plot, we need to apply the flag on each selection (current ans stored) update_select = self.update_plot_selection if len(self.selections) > 0: # plot stored selections for cur in self.selections.values(): if cur['select'] != ALL_SELECTED: self.update_plot_selection = update_select col = cur['color'] cur['idgllist'] = self._plot_selection(cur['select'], (float(col[0]) / 255., float(col[1]) / 255., float(col[2]) / 255.), cur['idgllist']) if self._myselection != ALL_SELECTED and not isinstance(self._myselection, tuple): # plot current selection in RED if not 'all' if self.nb > 0: self.update_plot_selection = update_select self.numlist_select = self._plot_selection(self.myselection, (1., 0., 0.), self.numlist_select)
[docs] def _plot_selection(self, curlist:list[float], color:list[float], loclist:int=0): """ Plot a selection :param curlist: list of selected nodes -- list of tuples (x,y) :param color: color of the selection - list of 3 floats between 0 and 1 :param loclist: index of OpenGL list """ #FIXME : Is it a good idea to use SHADER rather than list ? if self.update_plot_selection: dx = self.dx dy = self.dy if loclist != 0: glDeleteLists(loclist, 1) loclist = glGenLists(1) glNewList(loclist, GL_COMPILE) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) glBegin(GL_QUADS) for cursel in curlist: x1 = cursel[0] - dx / 2. x2 = cursel[0] + dx / 2. y1 = cursel[1] - dy / 2. y2 = cursel[1] + dy / 2. glColor3f(color[0], color[1], color[2]) glVertex2f(x1, y1) glVertex2f(x2, y1) glVertex2f(x2, y2) glVertex2f(x1, y2) glEnd() glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) for cursel in curlist: glBegin(GL_LINE_STRIP) x1 = cursel[0] - dx / 2. x2 = cursel[0] + dx / 2. y1 = cursel[1] - dy / 2. y2 = cursel[1] + dy / 2. glColor3f(0., 1., 0.) glVertex2f(x1, y1) glVertex2f(x2, y1) glVertex2f(x2, y2) glVertex2f(x1, y2) glVertex2f(x1, y1) glEnd() glEndList() glCallList(loclist) self.update_plot_selection = False else: if loclist != 0: glCallList(loclist) return loclist
[docs] def add_node_to_selection(self, x:float, y:float, verif:bool=True): """ Add one coordinate to the selection :param x: x coordinate - float :param y: y coordinate - float :param verif: if True, the coordinates are checked to avoid duplicates """ # on repasse par les i,j car les coordonnées transférées peuvent venir d'un click souris # le but est de ne conserver que les coordonnées des CG de mailles i, j = self.parent.get_ij_from_xy(x, y) if self.parent.check_bounds_ij(i, j): # if i>=0 and j>=0 and i<self.parent.nbx and j<self.parent.nby: self._add_node_to_selectionij(i, j, verif) return 0 # useful for MB else: return -1 # useful for MB self.update_nb_nodes_selection()
[docs] def add_nodes_to_selection(self, xy:list[float] | np.ndarray, verif:bool=True): """ Add multiple coordinates to the selection. :param xy: list of coordinates or numpy array of shape (nb_nodes, 2) where nb_nodes is the number of nodes or a numpy array of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively. If a numpy array of shape (nbx, nby) is provided, it is assumed to be a boolean array where True indicates a selected node. :param verif: if True, the coordinates are checked to avoid duplicates """ # on repasse par les i,j car les coordonnées transférées peuvent venir d'un click souris # le but est de ne conserver que les coordonnées des CG de mailles if isinstance(xy, np.ndarray): self._add_nodes_to_selection_np(xy, verif) else: ij = [self.parent.get_ij_from_xy(x, y) for x, y in xy] self._add_nodes_to_selectionij(ij, verif)
[docs] def _add_node_to_selectionij(self, i:int, j:int, verif=True): """ Add one ij coordinate to the selection. :param i: i coordinate :param j: j coordinate :param verif: if True, the coordinates are checked to avoid duplicates """ if self._storage_mode == StorageMode.ARRAY: if verif: self._boolarray[i, j] = not self._boolarray[i, j] else: self._boolarray[i, j] = True elif self._storage_mode == StorageMode.LIST: x1, y1 = self.parent.get_xy_from_ij(i, j) if self._myselection == ALL_SELECTED: # If all nodes are selected, we need to exclude the current node self._myselection = (ALL_SELECTED, np.array([[x1, y1]])) # Exclude the current node elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: # check is the current node is in the excluded nodes excluded_nodes = self._myselection[1].tolist() if (x1, y1) in excluded_nodes: # If the current node is in the excluded nodes, we remove it from the excluded nodes ret = excluded_nodes.index((x1, y1)) excluded_nodes.pop(ret) if len(excluded_nodes) == 0: self._myselection = ALL_SELECTED # All nodes are selected again else: self._myselection = (ALL_SELECTED, np.array(excluded_nodes)) else: # The node is included in the selection, we add it to the excluded nodes excluded_nodes.append((x1, y1)) self._myselection = (ALL_SELECTED, np.array(excluded_nodes)) else: if verif: try: ret = self._myselection.index((x1, y1)) except: ret = -1 if ret >= 0: self._myselection.pop(ret) return 0 else: self._myselection.append((x1, y1)) return 0 else: self._myselection.append((x1, y1)) return 0
[docs] def _add_nodes_to_selectionij(self, ij:list[tuple[float, float]], verif:bool=True): """ Add multiple ij coordinates to the selection :param ij: list of ij coordinates :param verif: if True, the coordinates are checked to avoid duplicates """ if len(ij)==0: logging.info(_('Nothing to do in add_nodes_to_selectionij !')) return nbini = self.nb ij = np.asarray(ij) if self._storage_mode == StorageMode.ARRAY: if verif: self._boolarray[ij[:, 0], ij[:, 1]] = np.logical_xor(self._boolarray[ij[:, 0], ij[:, 1]], True) else: self._boolarray[ij[:, 0], ij[:, 1]] = True elif self._storage_mode == StorageMode.LIST: xy = self.parent.ij2xy_np(ij) if self._myselection == ALL_SELECTED: # If all nodes are selected, we need to exclude the current nodes self._myselection = (ALL_SELECTED, xy) # Exclude the current nodes elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: # check if the current nodes are in the excluded nodes excluded_nodes = self._myselection[1] # extend the excluded nodes with the new ones excluded_nodes = np.vstack((excluded_nodes, xy)) # Find the duplicates in the excluded nodes selunique, counts = np.unique(excluded_nodes, return_counts=True, axis=0) # les éléments énumérés plus d'une fois doivent être enlevés locsort = sorted(zip(counts.tolist(), selunique.tolist()), reverse=True) counts = [x[0] for x in locsort] sel = [tuple(x[1]) for x in locsort] # on recherche le premier 1 if 1 in counts: idx = counts.index(1) # on ne conserve que la portion de liste utile self._myselection = (ALL_SELECTED, np.array(sel[idx:])) else: self._myselection = ALL_SELECTED # All nodes are selected again else: # Add the new nodes to the selection if nbini != 0: self._myselection += xy.tolist() if verif: # trouve les éléments uniques dans la liste de tuples (--> axis=0) et retourne également le comptage selunique, counts = np.unique(self._myselection, return_counts=True, axis=0) # les éléments énumérés plus d'une fois doivent être enlevés # on trie par ordre décroissant locsort = sorted(zip(counts.tolist(), selunique.tolist()), reverse=True) counts = [x[0] for x in locsort] sel = [tuple(x[1]) for x in locsort] # on recherche le premier 1 if 1 in counts: idx = counts.index(1) # on ne conserve que la portion de liste utile self._myselection = sel[idx:] else: self._myselection = [] else: self._myselection = np.unique(self.myselection, axis=0).tolist() else: self._myselection = xy.tolist() self.update_nb_nodes_selection()
[docs] def _add_nodes_to_selection_np(self, ij:np.ndarray, verif:bool=True): """ Add multiple coordinates to the selection from a numpy array :param ij: numpy array of coordinates - same shape as the parent array (nbx, nby) or (nb_nodes, 2) :param verif: if True, the coordinates are checked to avoid duplicates """ assert ij.shape == (self.parent.nbx, self.parent.nby) or ij.shape[1]==2, _('Invalid shape for ij - must be ({}, {})'.format(self.parent.nbx, self.parent.nby)) dtype = ij.dtype if dtype not in [np.bool, np.int32, np.int64]: assert ij.shape[1] == 2, _('Invalid shape for ij - must be ({}, {}) or (nb_nodes, 2)'.format(self.parent.nbx, self.parent.nby)) ij = self.parent.xy2ij_np(ij) if self._storage_mode == StorageMode.ARRAY: if verif: if ij.shape[1] == 2: # using xy from np.where self._myselection_as_array[ij[:,0], ij[:,1]] = np.logical_xor(self._myselection_as_array[ij[:, 0], ij[:, 1]], True) else: self._myselection_as_array[:,:] = np.logical_xor(self._myselection_as_array, ij) else: if ij.shape[1] == 2: # using xy from np.where self._myselection_as_array[ij[:,0], ij[:,1]] = True else: self._myselection_as_array[ij] = True elif self._storage_mode == StorageMode.LIST: if ij.shape[1] == 2: # using xy from np.where self._add_nodes_to_selectionij(ij, verif) else: # Convert the numpy array to a list of tuples ij = np.argwhere(ij) self._add_nodes_to_selectionij(ij, verif)
[docs] def select_insidepoly(self, myvect: vector): """ Select nodes inside a polygon. :param myvect: vector defining the polygon """ nbini = self.nb self.hideselection = self.parent.usemask inside = self.parent.get_ij_inside_polygon(myvect, usemask=self.hideselection) if inside.shape[0] == 0: logging.info(_('No nodes inside the polygon')) return else: if inside.shape[0] > self.threshold_array_mode: self.force_storage_mode(StorageMode.ARRAY) self._add_nodes_to_selectionij(inside, verif= nbini != 0) self.hideselection=False
[docs] def select_underpoly(self, myvect: vector): """ Select nodes along a polyline :param myvect: vector defining the polyline """ nbini = self.nb myvect.find_minmax() mypoints = self.parent.get_ij_under_polyline(myvect) if self.parent.usemask: # If the parent uses a mask, we need to check if the points are not masked mypoints = mypoints[~self.parent.array.mask[mypoints[:, 0], mypoints[:, 1]]] if len(mypoints) == 0: logging.info(_('No nodes under the polyline')) return self.add_nodes_to_selection(mypoints, verif = nbini != 0)
[docs] def dilate_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None): """ Extend the selection. :param nb_iterations: Number of iterations to dilate the selection :param use_mask: If True, use the mask of the parent array to limit the dilation :param structure: Structuring element for dilation, default is a cross shape (nodes directly adjacent in the x or y direction) """ if self.is_all_selected(): logging.info(_('Cannot extend selection when all nodes are selected')) return if self.nb == 0: logging.info(_('No nodes selected')) return if nb_iterations < 1: logging.info(_('Number of iterations must be greater than 0')) return if self.parent.array is None: logging.info(_('No array to select from')) return from scipy import ndimage self._myselection_as_array[:,:] = ndimage.binary_dilation(self._myselection_as_array, iterations=nb_iterations, mask=~self.parent.array.mask if use_mask else None, structure=structure) self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode self.update_nb_nodes_selection()
[docs] def erode_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None): """ Reduce the selection. :param nb_iterations: Number of iterations to erode the selection :param use_mask: If True, use the mask of the parent array to limit the erosion :param structure: Structuring element for erosion, default is a cross shape (nodes directly adjacent in the x or y direction) """ if self.is_all_selected(): logging.info(_('Cannot reduce selection when all nodes are selected')) return if self.nb == 0: logging.info(_('No nodes selected')) return if nb_iterations < 1: logging.info(_('Number of iterations must be greater than 0')) return if self.parent.array is None: logging.info(_('No array to select from')) return from scipy import ndimage self._myselection_as_array[:,:] = ndimage.binary_erosion(self._myselection_as_array, iterations=nb_iterations, mask=~self.parent.array.mask if use_mask else None, structure=structure) self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode self.update_nb_nodes_selection()
[docs] def dilate_contour_selection(self, nbiter:int= 1, use_mask:bool = True, structure:np.ndarray = np.ones((3,3))): """ Dilate the contour of the selection. :param nbiter: Number of iterations to dilate the selection :param use_mask: If True, use the mask of the parent array to limit the dilation :param structure: Structuring element for dilation, default is a 3x3 square """ if self.nb > 0: oldsel = self._myselection_as_array.copy() self.dilate_selection(nbiter, use_mask, structure) # We keep only the new nodes that were not in the old selection self._myselection_as_array[:,:] = np.logical_and(self._myselection_as_array, ~oldsel) self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode self.update_nb_nodes_selection() else: logging.info('No selection to expand/dilate')
[docs] def erode_contour_selection(self): """ Erode the contour of the selection. :param nbiter: Number of iterations to erode the selection :param use_mask: If True, use the mask of the parent array to limit the erosion :param structure: Structuring element for erosion, default is a 3x3 square """ if self.nb > 0: oldselect = self._myselection_as_array.copy() self.erode_selection(1) self._myselection_as_array[:,:] = np.logical_and(self._myselection_as_array, oldselect) self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode self.update_nb_nodes_selection() else: logging.info('No selection to contract/erode')
[docs] def save_selection(self, filename:str=None): """ Save the selection to a file. :param filename: Name of the file to save the selection to. If None, a dialog will be shown to choose the file. """ if filename is None: with wx.FileDialog(None, 'Save selection', wildcard='Text files (*.txt)|*.txt', style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg: if dlg.ShowModal() == wx.ID_CANCEL: return filename = dlg.GetPath() with open(filename, 'w') as f: f.write(self.get_string(all_memories=True))
[docs] def load_selection(self, filename:str=None): """ Load a selection from a file. :param filename: Name of the file to load the selection from. If None, a dialog will be shown to choose the file. """ if filename is None: with wx.FileDialog(None, 'Load selection', wildcard='Text files (*.txt)|*.txt', style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as dlg: if dlg.ShowModal() == wx.ID_CANCEL: return filename = dlg.GetPath() with open(filename, 'r') as f: lines = f.readlines() xy = [] k=0 for line in lines: if line[0] == 'X': k+=1 continue if line[0] == 'i': k+=1 break x, y = line.split() xy.append((float(x), float(y))) k+=1 self.myselection = xy self.update_nb_nodes_selection() k += len(xy) while k < len(lines): lines = lines[k:] k=0 for line in lines: if 'Selection' in line: idx = line.split()[1] self.selections[idx] = {} xy = [] k+=1 elif 'Color' in line: color = [int(x.replace('[','').replace(']','').replace(',','')) for x in line.split()[2:]] self.selections[idx]['Color'] = color k+=1 elif line[0] == 'X': k+=1 continue elif line[0] == 'i': k+=len(xy)+1 self.selections[idx]['select']=xy break else: x, y = line.split() xy.append((float(x), float(y))) k+=1 self.parent.reset_plot()
[docs] def update_nb_nodes_selection(self, autostoragemode:bool=True) -> tuple[int, float, float, float, float]: """ Update the number of selected nodes. :param autostoragemode: If True, automatically switch to ARRAY mode if the number of selected nodes exceeds the threshold. :return: Tuple containing the number of selected nodes, and the bounds of the selection (xmin, xmax, ymin, ymax). """ if autostoragemode: self._auto_storage_mode() if self._storage_mode == StorageMode.ARRAY: nb = np.count_nonzero(self._myselection_as_array) elif self._storage_mode == StorageMode.LIST: if self.is_all_selected(): if self.parent.usemask: nb = self.parent.nbnotnull else: nb = self.parent.nbx * self.parent.nby elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED: # tuple of ('all', np.ndarray) for all nodes and excluded nodes if self.parent.usemask: nb = self.parent.nbnotnull else: nb = self.parent.nbx * self.parent.nby nb -= len(self._myselection[1]) else: nb = len(self._myselection) self.update_plot_selection = True if self.wx_exists: if nb > 10000: if not self.hideselection: self.update_plot_selection = False # on met par défaut à False car OpenGL va demander une MAJ de l'affichage le temps que l'utilisateur réponde dlg = wx.MessageDialog(None, 'Large selection !!' + str(nb) + '\n Do you want plot the selected cells?', style=wx.YES_NO) ret = dlg.ShowModal() if ret == wx.ID_YES: self.update_plot_selection = True else: self.update_plot_selection = False self.hideselection = True dlg.Destroy() else: self.update_plot_selection = True if nb>0: if self.is_all_selected(): [xmin, xmax], [ymin, ymax] = self.parent.get_bounds() else: if self._storage_mode == StorageMode.ARRAY: # Get the bounds of the selection from the boolean array sel = np.argwhere(self._myselection_as_array) xmin = np.min(sel[:, 0]) ymin = np.min(sel[:, 1]) xmax = np.max(sel[:, 0]) ymax = np.max(sel[:, 1]) xmin, ymin = self.parent.get_xy_from_ij(xmin, ymin) xmax, ymax = self.parent.get_xy_from_ij(xmax, ymax) elif self._storage_mode == StorageMode.LIST: xmin = np.min(np.asarray(self.myselection)[:, 0]) ymin = np.min(np.asarray(self.myselection)[:, 1]) xmax = np.max(np.asarray(self.myselection)[:, 0]) ymax = np.max(np.asarray(self.myselection)[:, 1]) else: xmin = -99999. ymin = -99999. xmax = -99999. ymax = -99999. if self.parent.myops is not None: self.parent.myops.nbselect.SetLabelText(str(nb)) self.parent.myops.nbselect2.SetLabelText(str(nb)) if nb>0: self.parent.myops.minx.SetLabelText('{:.3f}'.format(xmin)) self.parent.myops.miny.SetLabelText('{:.3f}'.format(ymin)) self.parent.myops.maxx.SetLabelText('{:.3f}'.format(xmax)) self.parent.myops.maxy.SetLabelText('{:.3f}'.format(ymax)) return nb, xmin, xmax, ymin, ymax
[docs] def condition_select(self, cond:str | int, condval, condval2 = 0, usemask:bool = False): """ Select nodes based on a condition. :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN', 6: '>=<=', 7: '><', 8: '<>') :param condval: value to compare with :param condval2: second value to compare with (for ranges) :param usemask: whether to use the mask or not """ array = self.parent.array nbini = self.nb if array.dtype == np.float32: condval = np.float32(condval) condval2 = np.float32(condval2) elif array.dtype == np.float64: condval = np.float64(condval) condval2 = np.float64(condval2) elif array.dtype == np.int32: condval = np.int32(condval) condval2 = np.int32(condval2) elif array.dtype == np.int64: condval = np.int64(condval) condval2 = np.int64(condval2) elif array.dtype == np.int16: condval = np.int16(condval) condval2 = np.int16(condval2) elif array.dtype == np.int8: condval = np.int8(condval) condval2 = np.int8(condval2) elif array.dtype == np.uint32: condval = np.uint32(condval) condval2 = np.uint32(condval2) elif array.dtype == np.uint64: condval = np.uint64(condval) condval2 = np.uint64(condval2) elif array.dtype == np.uint16: condval = np.uint16(condval) condval2 = np.uint16(condval2) elif array.dtype == np.uint8: condval = np.uint8(condval) condval2 = np.uint8(condval2) else: logging.error(_('Unknown dtype in treat_select !')) return if usemask : mask=np.logical_not(array.mask) if nbini == 0: try: if cond == 0 or cond=='<': # < ij = np.argwhere((array < condval) & mask) elif cond == 1 or cond=='<=': # <= ij = np.argwhere((array <= condval) & mask) elif cond == 2 or cond=='==': # == ij = np.argwhere((array == condval) & mask) elif cond == 3 or cond=='>=': # >= ij = np.argwhere((array >= condval) & mask) elif cond == 4 or cond=='>': # > ij = np.argwhere((array > condval) & mask) elif cond == 5 or cond=='NaN': # NaN ij = np.argwhere((np.isnan(array)) & mask) elif cond == 6 or cond=='>=<=': # interval with equality ij = np.argwhere(((array>=condval) & (array<=condval2)) & mask) elif cond == 7 or cond=='><': # interval without equality ij = np.argwhere(((array>condval) & (array<condval2)) & mask) elif cond == 8 or cond=='<>': # interval without equality ij = np.argwhere(((array<condval) | (array>condval2)) & mask) self._add_nodes_to_selectionij(ij, nbini != 0) except: logging.error(_('Error in condition_select -- nbini == 0 ! -- Please report this bug, specifying the context')) return else: try: if self.nb > self.threshold_array_mode: # If the selection is large, we use the boolean array ijall = np.argwhere(self._myselection_as_array) else: # Otherwise, we use the selection list sel = np.asarray(self.myselection) ijall = self.parent.xy2ij_np(sel) if cond == 0 or cond=='<': # < ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] < condval) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 1 or cond=='<=': # <= ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] <= condval) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 2 or cond=='==': # == ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] == condval) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 3 or cond=='>=': # >= ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] >= condval) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 4 or cond=='>': # > ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] > condval) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 5 or cond=='NaN': # NaN ij = np.argwhere((np.isnan(array[ijall[:, 0], ijall[:, 1]])) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 6 or cond=='>=<=': # interval with equality ij = np.argwhere(((array[ijall[:, 0], ijall[:, 1]]>=condval) & (array[ijall[:, 0], ijall[:, 1]]<=condval2)) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 7 or cond=='><': # interval without equality ij = np.argwhere(((array[ijall[:, 0], ijall[:, 1]]>condval) & (array[ijall[:, 0], ijall[:, 1]]<condval2)) & (mask[ijall[:, 0], ijall[:, 1]])) elif cond == 8 or cond=='<>': # interval without equality ij = np.argwhere(((array[ijall[:, 0], ijall[:, 1]]<condval) | (array[ijall[:, 0], ijall[:, 1]]>condval2)) & (mask[ijall[:, 0], ijall[:, 1]])) ij = ij.flatten() self._add_nodes_to_selectionij(ijall[ij], nbini != 0) except: logging.error(_('Error in condition_select ! -- Please report this bug, specifying the context')) return else: if nbini == 0: try: if cond == 0 or cond=='<': # < ij = np.argwhere(array < condval) elif cond == 1 or cond=='<=': # <= ij = np.argwhere(array <= condval) elif cond == 2 or cond=='==': # == ij = np.argwhere(array == condval) elif cond == 3 or cond=='>=': # >= ij = np.argwhere(array >= condval) elif cond == 4 or cond=='>': # > ij = np.argwhere(array > condval) elif cond == 5 or cond=='NaN': # NaN ij = np.argwhere(np.isnan(array)) elif cond == 6 or cond=='>=<=': # interval with equality ij = np.argwhere((array>=condval) & (array<=condval2)) elif cond == 7 or cond=='><': # interval without equality ij = np.argwhere((array>condval) & (array<condval2)) elif cond == 8 or cond=='<>': # interval without equality ij = np.argwhere((array<condval) | (array>condval2)) elif cond == -1 or cond=='Mask': # Mask ij = np.argwhere(array.mask) elif cond == -2 or cond=='NotMask': # Mask ij = np.argwhere(np.logical_not(array.mask)) self._add_nodes_to_selectionij(ij, nbini != 0) except: logging.error(_('Error in condition_select -- nbini == 0 ! -- Please report this bug, specifying the context')) return else: try: if self.nb > self.threshold_array_mode: ijall = np.argwhere(self._myselection_as_array) else: sel = np.asarray(self.myselection) ijall = self.parent.xy2ij_np(sel) if cond == 0 or cond=='<': # < ij = np.argwhere(array[ijall[:, 0], ijall[:, 1]] < condval) elif cond == 1 or cond=='<=': # <= ij = np.argwhere(array[ijall[:, 0], ijall[:, 1]] <= condval) elif cond == 2 or cond=='==': # == ij = np.argwhere(array[ijall[:, 0], ijall[:, 1]] == condval) elif cond == 3 or cond=='>=': # >= ij = np.argwhere(array[ijall[:, 0], ijall[:, 1]] >= condval) elif cond == 4 or cond=='>': # > ij = np.argwhere(array[ijall[:, 0], ijall[:, 1]] > condval) elif cond == 5 or cond=='NaN': # NaN ij = np.argwhere(np.isnan(array[ijall[:, 0], ijall[:, 1]])) elif cond == 6 or cond=='>=<=': # interval with equality ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]]>=condval) & (array[ijall[:, 0], ijall[:, 1]]<=condval2)) elif cond == 7 or cond=='><': # interval without equality ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]]>condval) & (array[ijall[:, 0], ijall[:, 1]]<condval2)) elif cond == 8 or cond=='<>': # interval without equality ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]]<condval) | (array[ijall[:, 0], ijall[:, 1]]>condval2) ) elif cond == -1 or cond=='Mask': # Mask ij = np.argwhere(array.mask[ijall[:, 0], ijall[:, 1]]) elif cond == -2 or cond=='NotMask': # Mask ij = np.argwhere(np.logical_not(array.mask[ijall[:, 0], ijall[:, 1]])) ij = ij.flatten() self._add_nodes_to_selectionij(ijall[ij], nbini != 0) except: logging.error(_('Error in condition_select ! -- Please report this bug, specifying the context')) return
# self.update_nb_nodes_selection()
[docs] def treat_select(self, op:int, cond:int, opval, condval): """ Apply an operation on the selected nodes based on a condition. :param op: operation to apply (0: '+', 1: '-', 2: '*', 3: '/', 4: 'replace') :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN') :param opval: value to apply the operation with :param condval: value to compare with """ # operationChoices = [ u"+", u"-", u"*", u"/", u"replace'" ] # conditionChoices = [ u"<", u"<=", u"=", u">=", u">",u"isNaN" ] def test(val, cond, condval): if cond == 0: return val < condval elif cond == 1: return val <= condval elif cond == 2: return val == condval elif cond == 3: return val >= condval elif cond == 4: return val > condval elif cond == 5: return np.isnan(val) array = self.parent.array if array.dtype == np.float32: opval = np.float32(opval) condval = np.float32(condval) elif array.dtype == np.float64: opval = np.float64(opval) condval = np.float64(condval) elif array.dtype == np.int32: opval = np.int32(opval) condval = np.int32(condval) elif array.dtype == np.int64: opval = np.int64(opval) condval = np.int64(condval) elif array.dtype == np.int16: opval = np.int16(opval) condval = np.int16(condval) elif array.dtype == np.int8: opval = np.int8(opval) condval = np.int8(condval) elif array.dtype == np.uint32: opval = np.uint32(opval) condval = np.uint32(condval) elif array.dtype == np.uint64: opval = np.uint64(opval) condval = np.uint64(condval) elif array.dtype == np.uint16: opval = np.uint16(opval) condval = np.uint16(condval) elif array.dtype == np.uint8: opval = np.uint8(opval) condval = np.uint8(condval) else: logging.error(_('Unknown dtype in treat_select !')) return if self.myselection == ALL_SELECTED: if op == 0: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] += opval elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] += opval elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] += opval elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] += opval elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] += opval elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif op == 1: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] -= opval elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] -= opval elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] -= opval elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] -= opval elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] -= opval elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif op == 2: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] *= opval elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] *= opval elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] *= opval elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] *= opval elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] *= opval elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif op == 3 and opval != 0.: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] /= opval elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] /= opval elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] /= opval elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] /= opval elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] /= opval elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = 0 elif op == 4: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array[ind[:, 0], ind[:, 1]] = opval else: if len(self.myselection) == 0: logging.info(_('Nothing to do in treat_select ! -- PLease select some nodes')) return ij = [self.parent.get_ij_from_xy(cur[0], cur[1]) for cur in self.myselection] if op == 0: for i, j in ij: if test(array.data[i, j], cond, condval): array.data[i, j] += opval elif op == 1: for i, j in ij: if test(array.data[i, j], cond, condval): array.data[i, j] -= opval elif op == 2: for i, j in ij: if test(array.data[i, j], cond, condval): array.data[i, j] *= opval elif op == 3 and opval != 0.: for i, j in ij: if test(array.data[i, j], cond, condval): array.data[i, j] /= opval elif op == 4: for i, j in ij: if test(array.data[i, j], cond, condval): array.data[i, j] = opval self.parent.mask_data(self.parent.nullvalue) self.refresh_parantarray()
[docs] def refresh_parantarray(self): """ Refresh the parent array after a selection """ self.parent.reset_plot()
[docs] def mask_condition(self, op:int, cond:int, opval, condval): """ Mask nodes based on a condition. :param op: operation to apply (0: '+', 1: '-', 2: '*', 3: '/', 4: 'replace') :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN') :param opval: value to apply the operation with :param condval: value to compare with """ # operationChoices = [ u"+", u"-", u"*", u"/", u"replace'" ] # conditionChoices = [ u"<", u"<=", u"=", u">=", u">",u"isNaN" ] def test(val, cond, condval): if cond == 0: return val < condval elif cond == 1: return val <= condval elif cond == 2: return val == condval elif cond == 3: return val >= condval elif cond == 4: return val > condval elif cond == 5: return np.isnan(val) array = self.parent.array if array.dtype == np.float32: opval = np.float32(opval) condval = np.float32(condval) elif array.dtype == np.float64: opval = np.float64(opval) condval = np.float64(condval) elif array.dtype == np.int32: opval = np.int32(opval) condval = np.int32(condval) elif array.dtype == np.int64: opval = np.int64(opval) condval = np.int64(condval) elif array.dtype == np.int16: opval = np.int16(opval) condval = np.int16(condval) elif array.dtype == np.int8: opval = np.int8(opval) condval = np.int8(condval) elif array.dtype == np.uint32: opval = np.uint32(opval) condval = np.uint32(condval) elif array.dtype == np.uint64: opval = np.uint64(opval) condval = np.uint64(condval) elif array.dtype == np.uint16: opval = np.uint16(opval) condval = np.uint16(condval) elif array.dtype == np.uint8: opval = np.uint8(opval) condval = np.uint8(condval) else: logging.error(_('Unknown dtype in treat_select !')) return if self.myselection == ALL_SELECTED: if cond == 0: # < ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask))) elif cond == 1: # <= ind = np.argwhere(np.logical_and(array <= condval, np.logical_not(array.mask))) elif cond == 2: # == ind = np.argwhere(np.logical_and(array == condval, np.logical_not(array.mask))) elif cond == 3: # >= ind = np.argwhere(np.logical_and(array >= condval, np.logical_not(array.mask))) elif cond == 4: # > ind = np.argwhere(np.logical_and(array > condval, np.logical_not(array.mask))) elif cond == 5: # NaN ind = np.argwhere(np.logical_and(np.isnan(array), np.logical_not(array.mask))) array.mask[ind[:, 0], ind[:, 1]] = True else: npself = np.asarray(self.myselection) ij = self.parent.xy2ij_np(npself) array.mask[ij[:, 0], ij[:, 1]] = test(array.data[ij[:, 0], ij[:, 1]], cond, condval) # ij = [self.parent.get_ij_from_xy(cur[0], cur[1]) for cur in self.myselection] # for i, j in ij: # if test(array.data[i, j], cond, condval): # array.mask[i, j] = True self.parent.nbnotnull = array.count() self.parent.updatepalette() self.parent.delete_lists()
[docs] def get_values_sel(self) -> np.ndarray: """ Get the values of the selected nodes. :return: numpy array of values of the selected nodes, or -99999 if all nodes are selected. """ if self.myselection == ALL_SELECTED: return -99999 else: sel = np.asarray(self.myselection) if len(sel) == 1: ijall = self.parent.xy2ij_np(sel) z = self.parent.array[ijall[0], ijall[1]] else: ijall = self.parent.xy2ij_np(sel) z = self.parent.array[ijall[:, 0], ijall[:, 1]].flatten() return z
[docs] def _get_header(self) -> header_wolf | None: """ Header corresponding to the selection """ sel = np.asarray(self.myselection) myhead = header_wolf() if self.dx == 0. or self.dy == 0.: logging.error(_('dx or dy is null in get_header - Abort !')) return None myhead.dx = self.dx myhead.dy = self.dy myhead.translx = 0. myhead.transly = 0. myhead.origx = np.amin(sel[:, 0]) - self.dx / 2. myhead.origy = np.amin(sel[:, 1]) - self.dy / 2. ex = np.amax(sel[:, 0]) + self.dx / 2. ey = np.amax(sel[:, 1]) + self.dy / 2. myhead.nbx = int((ex - myhead.origx) / self.dx) myhead.nby = int((ey - myhead.origy) / self.dy) return myhead
[docs] def get_newarray(self) -> "WolfArray": """ Create a new array from the selection. :return: a new WolfArray object containing the selected nodes, or None if the selection is empty. Null values are set to -99999. If all nodes are selected, returns a WolfArray molded from the parent. """ from ._base import WolfArrayModel # lazy import to avoid circular dependency if self.nb == 0: return None if self.myselection == ALL_SELECTED: return WolfArrayModel(mold=self.parent) newarray = WolfArrayModel() lochead = self._get_header() if lochead is None: logging.error(_('Error in get_newarray !')) return newarray.init_from_header(self._get_header()) sel = np.asarray(self.myselection) if len(sel) == 1: # ijall = np.asarray(self.parent.get_ij_from_xy(sel[0, 0], sel[0, 1])).transpose() ijall = self.parent.xy2ij_np(sel) z = self.parent.array[ijall[0], ijall[1]] else: ijall = np.asarray(self.parent.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose() ijall = self.parent.xy2ij_np(sel) z = self.parent.array[ijall[:, 0], ijall[:, 1]].flatten() newarray.array[:, :] = -99999. newarray.nullvalue = -99999. newarray.set_values_sel(sel, z) return newarray
[docs] def select_all(self): """ Select all nodes. It will set the selection to ALL_SELECTED, which is a special value indicating that all nodes are selected. """ self.myselection = ALL_SELECTED
[docs] def interp2Dpolygons(self, working_zone:zone, method:Literal["nearest", "linear", "cubic"] = None, resetplot:bool = True, keep:Literal['all', 'below', 'above'] = 'all'): """ Interpolation sous tous les polygones d'une zone cf parent.interp2Dpolygon Useful for WX GUI, where the method is chosen by the user. From script, you can call this method directly from the parent array with the desired method and keep parameters. """ if method is None: choices = ["nearest", "linear", "cubic"] dlg = wx.SingleChoiceDialog(None, "Pick an interpolate method", "Choices", choices) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return method = dlg.GetStringSelection() dlg.Destroy() self.parent.interpolate_on_polygons(working_zone, method, keep) if resetplot: self.parent.reset_plot()
[docs] def interp2Dpolygon(self, working_vector:vector, method:Literal["nearest", "linear", "cubic"] = None, resetplot:bool = True, keep:Literal['all', 'below', 'above'] = 'all'): """ Interpolation sous un polygone cf parent.interp2Dpolygon Useful for WX GUI, where the method is chosen by the user. From script, you can call this method directly from the parent array with the desired method and keep parameters. """ if method is None: choices = ["nearest", "linear", "cubic"] dlg = wx.SingleChoiceDialog(None, "Pick an interpolate method", "Choices", choices) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return method = dlg.GetStringSelection() dlg.Destroy() self.parent.interpolate_on_polygon(working_vector, method, keep) if resetplot: self.parent.reset_plot()
[docs] def interp2Dpolylines(self, working_zone:zone, resetplot:bool = True): """ Interpolation sous toutes les polylignes de la zone cf parent.interp2Dpolyline Useful for WX GUI, where the method is chosen by the user. From script, you can call this method directly from the parent array with the desired method and keep parameters. """ self.parent.interpolate_on_polylines(working_zone) if resetplot: self.parent.reset_plot()
[docs] def interp2Dpolyline(self, working_vector:vector, resetplot:bool = True): """ Interpolation sous la polyligne active cf parent.interp2Dpolyline Useful for WX GUI, where the method is chosen by the user. From script, you can call this method directly from the parent array with the desired method and keep parameters. """ self.parent.interpolate_on_polyline(working_vector) if resetplot: self.parent.reset_plot()
[docs] def copy(self, source:"SelectionData"): """ Copy the selection data from another SelectionData object. :param source: another SelectionData object to copy from. """ if source._storage_mode == StorageMode.ARRAY: self._storage_mode = StorageMode.ARRAY self._myselection_as_array = source._myselection_as_array.copy() elif source._storage_mode == StorageMode.LIST: self._storage_mode = StorageMode.LIST self._myselection = source._myselection.copy()
[docs] def volumesurface(self, show=True): """ Evaluation of stage-storage-surface relation from "volume_estimation" routine of the WolfArray. If the selection is empty, it will use the whole array. If the selection is ALL_SELECTED, it will use the whole array. If the selection is not empty, it will use the selected nodes only -- see "get_newarray" routine. If the parent array is linked to a MapViewer, it will apply the same operation to all linked active arrays. :param show: if True, the figure will be shown. """ if self.parent.get_mapviewer() is not None: mapviewer = self.parent.get_mapviewer() if mapviewer.linked: # If linked arrays, we copy the selection data to all linked arrays arrays = [cur.active_array for cur in mapviewer.linkedList] for cur in arrays: if cur is not self.parent: cur.SelectionData.copy(self.parent.SelectionData) if self.nb == 0 or self.myselection == ALL_SELECTED: for cur in arrays: fig, axs = cur.volume_estimation() if show: fig.show() else: for cur in arrays: myarray = cur.SelectionData.get_newarray() fig, axs = myarray.volume_estimation() if show: fig.show() else: if len(self.parent.SelectionData.myselection) == 0 or self.parent.SelectionData.myselection == ALL_SELECTED: myarray = self.parent else: myarray = self.parent.SelectionData.get_newarray() myarray.SelectionData.selections = self.parent.SelectionData.selections.copy() fig, axs = myarray.volume_estimation() if show: fig.show() else: if self.nb == 0 or self.myselection == ALL_SELECTED: myarray = self.parent else: myarray = self.get_newarray() myarray.SelectionData.selections = self.selections.copy() fig, axs = myarray.volume_estimation() if show: fig.show()
[docs] class SelectionDataMB(SelectionData): """ Extension of SelectionData to manage multiple blocks. All routines are not implemented yet, as they depend on the specificities of the blocks. For the rest, it is mainly a simple wrapper around the SelectionData of each block. """ def __init__(self, parent:"WolfArrayMB"): SelectionData.__init__(self, parent)
[docs] self.parent:"WolfArrayMB" = parent
@property
[docs] def nb(self): return np.sum([cur.SelectionData.nb for cur in self.parent.active_blocks])
[docs] def unmask_selection(self): for curblock in self.parent.active_blocks: curblock.SelectionData.unmask_selection(resetplot=False) self.parent.reset_plot()
[docs] def reset(self): for curblock in self.parent.active_blocks: curblock.SelectionData.reset()
[docs] def select_all(self): for curblock in self.parent.active_blocks: curblock.SelectionData.select_all()
[docs] def reset_all(self): """ Reset the selection """ for curblock in self.parent.active_blocks: curblock.SelectionData.reset_all()
[docs] def get_string(self, which:str = None) -> str: logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def save_selection(self, filename:str=None, which:str = None): logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def load_selection(self, filename:str=None, which:str = None): logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def get_script(self, which:int = None) -> str: logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def get_newarray(self): logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def add_node_to_selection(self, x:float, y:float, verif:bool=True): """ Add a node to the selection """ for curblock in self.parent.active_blocks: ret = curblock.SelectionData.add_node_to_selection(x, y, verif)
[docs] def add_nodes_to_selection(self, xy:list[float] | np.ndarray, verif:bool=True): """ Add nodes to the selection """ for curblock in self.parent.active_blocks: curblock.SelectionData.add_nodes_to_selection(xy, verif)
[docs] def select_insidepoly(self, myvect: vector): for curblock in self.parent.active_blocks: curblock.SelectionData.select_insidepoly(myvect)
[docs] def select_underpoly(self, myvect: vector): for curblock in self.parent.active_blocks: curblock.SelectionData.select_underpoly(myvect)
[docs] def dilate_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None): """ Extend the selection """ for curblock in self.parent.active_blocks: curblock.SelectionData.dilate_selection(nb_iterations, use_mask, structure)
[docs] def erode_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None): """ Reduce the selection """ for curblock in self.parent.active_blocks: curblock.SelectionData.erode_selection(nb_iterations, use_mask, structure)
[docs] def update_nb_nodes_selection(self): """ Update the number of nodes selected """ # Get infos from all blocks ret = [] for curblock in self.parent.active_blocks: ret.append(curblock.SelectionData.update_nb_nodes_selection()) # sum all the nodes nb = np.sum([cur[0] for cur in ret]) if nb > 0 : xmin = np.min([cur[1] for cur in ret if cur[1] != -99999.]) ymin = np.min([cur[3] for cur in ret if cur[3] != -99999.]) xmax = np.max([cur[2] for cur in ret if cur[2] != -99999.]) ymax = np.max([cur[4] for cur in ret if cur[4] != -99999.]) if self.parent.myops is not None: self.parent.myops.nbselect.SetLabelText(str(nb)) self.parent.myops.nbselect2.SetLabelText(str(nb)) if nb>0: self.parent.myops.minx.SetLabelText('{:.3f}'.format(xmin)) self.parent.myops.miny.SetLabelText('{:.3f}'.format(ymin)) self.parent.myops.maxx.SetLabelText('{:.3f}'.format(xmax)) self.parent.myops.maxy.SetLabelText('{:.3f}'.format(ymax)) else: self.parent.myops.minx.SetLabelText('') self.parent.myops.miny.SetLabelText('') self.parent.myops.maxx.SetLabelText('') self.parent.myops.maxy.SetLabelText('')
[docs] def condition_select(self, cond, condval, condval2=0, usemask=False): for curblock in self.parent.active_blocks: curblock.SelectionData.condition_select(cond, condval, condval2, usemask)
[docs] def treat_select(self, op, cond, opval, condval): for curblock in self.parent.active_blocks: curblock.SelectionData.treat_select(op, cond, opval, condval)
[docs] def mask_condition(self, op, cond, opval, condval): for curblock in self.parent.active_blocks: curblock.SelectionData.mask_condition(op, cond, opval, condval)
[docs] def plot_selection(self): for curblock in self.parent.active_blocks: curblock.SelectionData.plot_selection()
[docs] def move_selectionto(self, idx:str, color:list[float]): for curblock in self.parent.active_blocks: curblock.SelectionData.move_selectionto(idx, color, resetplot=False) self.parent.reset_plot()
[docs] def copy_to_clipboard(self, which:int = None, typestr:Literal['string', 'script'] = 'string'): logging.error(_('Not yet implemented for Multi-Blocks'))
[docs] def interp2Dpolygons(self, working_zone:zone, method:Literal["nearest", "linear", "cubic"] = None, resetplot:bool = True, keep:Literal['all', 'below', 'above'] = 'all'): """ Interpolation sous tous les polygones d'une zone cf parent.interp2Dpolygon """ if method is None: choices = ["nearest", "linear", "cubic"] dlg = wx.SingleChoiceDialog(None, "Pick an interpolate method", "Choices", choices) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return method = dlg.GetStringSelection() dlg.Destroy() self.parent.interpolate_on_polygons(working_zone, method, keep) if resetplot: self.parent.reset_plot()
[docs] def interp2Dpolygon(self, working_vector:vector, method:Literal["nearest", "linear", "cubic"] = None, resetplot:bool = True, keep:Literal['all', 'below', 'above'] = 'all'): """ Interpolation sous un polygone cf parent.interp2Dpolygon """ if method is None: choices = ["nearest", "linear", "cubic"] dlg = wx.SingleChoiceDialog(None, "Pick an interpolate method", "Choices", choices) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return method = dlg.GetStringSelection() dlg.Destroy() self.parent.interpolate_on_polygon(working_vector, method, keep) if resetplot: self.parent.reset_plot()
[docs] def interp2Dpolylines(self, working_zone:zone, resetplot:bool = True): """ Interpolation sous toutes les polylignes de la zone cf parent.interp2Dpolyline """ self.parent.interpolate_on_polylines(working_zone) self.parent.reset_plot()
[docs] def interp2Dpolyline(self, working_vector:vector, resetplot:bool = True): """ Interpolation sous la polyligne active cf parent.interp2Dpolyline """ self.parent.interpolate_on_polyline(working_vector) self.parent.reset_plot()
[docs] def volumesurface(self, show=True): """ Evaluation of stage-storage-surface relation """ logging.error(_('Not yet implemented for Multi-Blocks'))