[docs]classDims(EventedModel):"""Dimensions object modeling slicing and displaying. Parameters ---------- ndim : int Number of dimensions. ndisplay : int Number of displayed dimensions. range : tuple of 3-tuple of float List of tuples (min, max, step), one for each dimension in world coordinates space. Lower and upper bounds are inclusive. point : tuple of floats Dims position in world coordinates for each dimension. margin_left : tuple of floats Left margin in world pixels of the slice for each dimension. margin_right : tuple of floats Right margin in world pixels of the slice for each dimension. order : tuple of int Tuple of ordering the dimensions, where the last dimensions are rendered. axis_labels : tuple of str Tuple of labels for each dimension. last_used : int Dimension which was last interacted with. Attributes ---------- ndim : int Number of dimensions. ndisplay : int Number of displayed dimensions. range : tuple of 3-tuple of float List of tuples (min, max, step), one for each dimension in world coordinates space. Lower and upper bounds are inclusive. point : tuple of floats Dims position in world coordinates for each dimension. margin_left : tuple of floats Left margin (=thickness) in world pixels of the slice for each dimension. margin_right : tuple of floats Right margin (=thickness) in world pixels of the slice for each dimension. order : tuple of int Tuple of ordering the dimensions, where the last dimensions are rendered. axis_labels : tuple of str Tuple of labels for each dimension. last_used : int Dimension which was last used. Tuple the slider position for each dims slider, in world coordinates. current_step : tuple of int Current step for each dimension (same as point, but in slider coordinates). nsteps : tuple of int Number of steps available to each slider. These are calculated from the ``range``. thickness : tuple of floats Thickness of the slice (sum of both margins) for each dimension in world coordinates. displayed : tuple of int List of dimensions that are displayed. These are calculated from the ``order`` and ``ndisplay``. not_displayed : tuple of int List of dimensions that are not displayed. These are calculated from the ``order`` and ``ndisplay``. displayed_order : tuple of int Order of only displayed dimensions. These are calculated from the ``displayed`` dimensions. rollable : tuple of bool Tuple of axis roll state. If True the axis is rollable. """# fieldsndim:int=2ndisplay:Literal[2,3]=2order:tuple[int,...]=()axis_labels:tuple[str,...]=()rollable:tuple[bool,...]=()range:tuple[RangeTuple,...]=()margin_left:tuple[float,...]=()margin_right:tuple[float,...]=()point:tuple[float,...]=()last_used:int=0# private vars_play_ready:bool=True# False if currently awaiting a draw event_scroll_progress:int=0# validators# check fields is false to allow private fields to work@validator('order','axis_labels','rollable','point','margin_left','margin_right',pre=True,allow_reuse=True,)def_as_tuple(v):returntuple(v)@validator('range',pre=True)def_check_ranges(ranges):""" Ensure the range values are sane. - start < stop - step > 0 """foraxis,(start,stop,step)inenumerate(ranges):ifstart>stop:raiseValueError(trans._('start and stop must be strictly increasing, but got ({start}, {stop}) for axis {axis}',deferred=True,start=start,stop=stop,axis=axis,))ifstep<=0:raiseValueError(trans._('step must be strictly positive, but got {step} for axis {axis}.',deferred=True,step=step,axis=axis,))returnranges@root_validator(skip_on_failure=True,allow_reuse=True)def_check_dims(cls,values):"""Check the consistency of dimensionality for all attributes. Parameters ---------- values : dict Values dictionary to update dims model with. """updated={}ndim=values['ndim']range_=ensure_len(values['range'],ndim,pad_width=(0.0,2.0,1.0))updated['range']=tuple(RangeTuple(*rng)forrnginrange_)point=ensure_len(values['point'],ndim,pad_width=0.0)# ensure point is limited to rangeupdated['point']=tuple(np.clip(pt,rng.start,rng.stop)forpt,rnginzip(point,updated['range']))updated['margin_left']=ensure_len(values['margin_left'],ndim,pad_width=0.0)updated['margin_right']=ensure_len(values['margin_right'],ndim,pad_width=0.0)# order and label default computation is too different to include in ensure_len()# Check the order tuple has same number of elements as ndimorder=values['order']iflen(order)<ndim:order_ndim=len(order)# new dims are always prependedprepended_dims=tuple(range(ndim-order_ndim))# maintain existing order, but shift accordinglyexisting_order=tuple(o+ndim-order_ndimforoinorder)order=prepended_dims+existing_ordereliflen(order)>ndim:order=reorder_after_dim_reduction(order[-ndim:])updated['order']=order# Check the order is a permutation of 0, ..., ndim - 1ifset(updated['order'])!=set(range(ndim)):raiseValueError(trans._('Invalid ordering {order} for {ndim} dimensions',deferred=True,order=updated['order'],ndim=ndim,))# Check the axis labels tuple has same number of elements as ndimaxis_labels=values['axis_labels']labels_ndim=len(axis_labels)iflabels_ndim<ndim:# Append new "default" labels to existing onesifaxis_labels==tuple(map(str,range(labels_ndim))):updated['axis_labels']=tuple(map(str,range(ndim)))else:updated['axis_labels']=(tuple(map(str,range(ndim-labels_ndim)))+axis_labels)eliflabels_ndim>ndim:updated['axis_labels']=axis_labels[-ndim:]# Check the rollable axes tuple has same number of elements as ndimupdated['rollable']=ensure_len(values['rollable'],ndim,True)# If the last used slider is no longer visible, use the first.last_used=values['last_used']ndisplay=values['ndisplay']dims_range=updated['range']nsteps=cls._nsteps_from_range(dims_range)not_displayed=[dfordinorder[:-ndisplay]iflen(nsteps)>dandnsteps[d]>1]iflen(not_displayed)>0andlast_usednotinnot_displayed:updated['last_used']=not_displayed[0]return{**values,**updated}@staticmethoddef_nsteps_from_range(dims_range)->tuple[float,...]:returntuple(# "or 1" ensures degenerate dimension worksint((rng.stop-rng.start)/(rng.stepor1))+1forrngindims_range)@propertydefnsteps(self)->tuple[float,...]:returnself._nsteps_from_range(self.range)@nsteps.setterdefnsteps(self,value):self.range=tuple(RangeTuple(rng.start,rng.stop,(rng.stop-rng.start)/(nsteps-1))forrng,nstepsinzip(self.range,value))@propertydefcurrent_step(self):returntuple(int(round((point-rng.start)/(rng.stepor1)))forpoint,rnginzip(self.point,self.range))@current_step.setterdefcurrent_step(self,value):self.point=tuple(rng.start+point*rng.stepforpoint,rnginzip(value,self.range))@propertydefthickness(self)->tuple[float,...]:returntuple(left+rightforleft,rightinzip(self.margin_left,self.margin_right))@thickness.setterdefthickness(self,value):self.margin_left=self.margin_right=tuple(val/2forvalinvalue)@propertydefdisplayed(self)->tuple[int,...]:"""Tuple: Dimensions that are displayed."""returnself.order[-self.ndisplay:]@propertydefnot_displayed(self)->tuple[int,...]:"""Tuple: Dimensions that are not displayed."""returnself.order[:-self.ndisplay]@propertydefdisplayed_order(self)->tuple[int,...]:returntuple(argsort(self.displayed))
[docs]defset_range(self,axis:Union[int,Sequence[int]],_range:Union[Sequence[Union[int,float]],Sequence[Sequence[Union[int,float]]]],):"""Sets ranges (min, max, step) for the given dimensions. Parameters ---------- axis : int or sequence of int Dimension index or a sequence of axes whos range will be set. _range : tuple or sequence of tuple Range specified as (min, max, step) or a sequence of these range tuples. """axis,value=self._sanitize_input(axis,_range,value_is_sequence=True)full_range=list(self.range)forax,valinzip(axis,value):full_range[ax]=valself.range=tuple(full_range)
[docs]defset_point(self,axis:Union[int,Sequence[int]],value:Union[float,Sequence[float]],):"""Sets point to slice dimension in world coordinates. Parameters ---------- axis : int or sequence of int Dimension index or a sequence of axes whos point will be set. value : scalar or sequence of scalars Value of the point for each axis. """axis,value=self._sanitize_input(axis,value,value_is_sequence=False)full_point=list(self.point)forax,valinzip(axis,value):full_point[ax]=valself.point=tuple(full_point)
[docs]defset_axis_label(self,axis:Union[int,Sequence[int]],label:Union[str,Sequence[str]],):"""Sets new axis labels for the given axes. Parameters ---------- axis : int or sequence of int Dimension index or a sequence of axes whos labels will be set. label : str or sequence of str Given labels for the specified axes. """axis,label=self._sanitize_input(axis,label,value_is_sequence=False)full_axis_labels=list(self.axis_labels)forax,valinzip(axis,label):full_axis_labels[ax]=valself.axis_labels=tuple(full_axis_labels)
[docs]defreset(self):"""Reset dims values to initial states."""# Don't reset axis labels# TODO: could be optimized with self.update, but need to fix# event firing in EventedModel firstself.range=((0,2,1),)*self.ndimself.point=(0,)*self.ndimself.order=tuple(range(self.ndim))self.margin_left=(0,)*self.ndimself.margin_right=(0,)*self.ndimself.rollable=(True,)*self.ndim
[docs]deftranspose(self):"""Transpose displayed dimensions. This swaps the order of the last two displayed dimensions. The order of the displayed is taken from Dims.order. """order=list(self.order)order[-2],order[-1]=order[-1],order[-2]self.order=order
def_increment_dims_right(self,axis:Optional[int]=None):"""Increment dimensions to the right along given axis, or last used axis if None Parameters ---------- axis : int, optional Axis along which to increment dims, by default None """ifaxisisNone:axis=self.last_usedself.set_current_step(axis,self.current_step[axis]+1)def_increment_dims_left(self,axis:Optional[int]=None):"""Increment dimensions to the left along given axis, or last used axis if None Parameters ---------- axis : int, optional Axis along which to increment dims, by default None """ifaxisisNone:axis=self.last_usedself.set_current_step(axis,self.current_step[axis]-1)def_focus_up(self):"""Shift focused dimension slider to be the next slider above."""sliders=[dfordinself.not_displayedifself.nsteps[d]>1]iflen(sliders)==0:returnindex=(sliders.index(self.last_used)+1)%len(sliders)self.last_used=sliders[index]def_focus_down(self):"""Shift focused dimension slider to be the next slider bellow."""sliders=[dfordinself.not_displayedifself.nsteps[d]>1]iflen(sliders)==0:returnindex=(sliders.index(self.last_used)-1)%len(sliders)self.last_used=sliders[index]
[docs]defroll(self):"""Roll order of dimensions for display."""order=np.array(self.order)# we combine "rollable" and "nsteps" into a mask for rolling# this mask has to be aligned to "order" as "rollable" and# "nsteps" are static but order is dynamic, meaning "rollable"# and "nsteps" encode the axes by position, whereas "order"# encodes axis by numbervalid=np.logical_and(self.rollable,np.array(self.nsteps)>1)[order]order[valid]=np.roll(order[valid],shift=1)self.order=order
def_go_to_center_step(self):self.current_step=[int((ns-1)/2)fornsinself.nsteps]def_sanitize_input(self,axis,value,value_is_sequence=False)->tuple[list[int],list]:""" Ensure that axis and value are the same length, that axes are not out of bounds, and coerces to lists for easier processing. """ifisinstance(axis,Integral):if(isinstance(value,Sequence)andnotisinstance(value,str)andnotvalue_is_sequence):raiseValueError(trans._('cannot set multiple values to a single axis'))axis=[axis]value=[value]else:axis=list(axis)value=list(value)iflen(axis)!=len(value):raiseValueError(trans._('axis and value sequences must have equal length'))foraxinaxis:ensure_axis_in_bounds(ax,self.ndim)returnaxis,value
defensure_len(value:tuple,length:int,pad_width:Any):""" Ensure that the value has the required number of elements. Right-crop if value is too long; left-pad with default if too short. Parameters ---------- value : Tuple A tuple of values to be resized. ndim : int Number of desired values. default : Tuple Default element for left-padding. """iflen(value)<length:# left padvalue=(pad_width,)*(length-len(value))+valueeliflen(value)>length:# right-cropvalue=value[-length:]returnvaluedefensure_axis_in_bounds(axis:int,ndim:int)->int:"""Ensure a given value is inside the existing axes of the image. Returns ------- axis : int The axis which was checked for validity. ndim : int The dimensionality of the layer. Raises ------ ValueError The given axis index is out of bounds. """ifaxisnotinrange(-ndim,ndim):msg=trans._('Axis {axis} not defined for dimensionality {ndim}. Must be in [{ndim_lower}, {ndim}).',deferred=True,axis=axis,ndim=ndim,ndim_lower=-ndim,)raiseValueError(msg)returnaxis%ndim