diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index 8bbcb7957..95bcc6795 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -94,7 +94,7 @@ class Orientation(Rotation,Crystal): """ - @util.extend_docstring(_parameter_doc) + @util.extend_docstring(extra_parameters=_parameter_doc) def __init__(self, rotation: Union[FloatSequence, Rotation] = np.array([1.,0.,0.,0.]), *, @@ -300,84 +300,95 @@ class Orientation(Rotation,Crystal): @classmethod - @util.extended_docstring(Rotation.from_random, _parameter_doc) + @util.extend_docstring(Rotation.from_random, + extra_parameters=_parameter_doc) def from_random(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_random) return cls(rotation=Rotation.from_random(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_quaternion,_parameter_doc) + @util.extend_docstring(Rotation.from_quaternion, + extra_parameters=_parameter_doc) def from_quaternion(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_quaternion) return cls(rotation=Rotation.from_quaternion(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_Euler_angles,_parameter_doc) + @util.extend_docstring(Rotation.from_Euler_angles, + extra_parameters=_parameter_doc) def from_Euler_angles(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_Euler_angles) return cls(rotation=Rotation.from_Euler_angles(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_axis_angle,_parameter_doc) + @util.extend_docstring(Rotation.from_axis_angle, + extra_parameters=_parameter_doc) def from_axis_angle(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_axis_angle) return cls(rotation=Rotation.from_axis_angle(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_basis,_parameter_doc) + @util.extend_docstring(Rotation.from_basis, + extra_parameters=_parameter_doc) def from_basis(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_basis) return cls(rotation=Rotation.from_basis(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_matrix,_parameter_doc) + @util.extend_docstring(Rotation.from_matrix, + extra_parameters=_parameter_doc) def from_matrix(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_matrix) return cls(rotation=Rotation.from_matrix(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_Rodrigues_vector,_parameter_doc) + @util.extend_docstring(Rotation.from_Rodrigues_vector, + extra_parameters=_parameter_doc) def from_Rodrigues_vector(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_Rodrigues_vector) return cls(rotation=Rotation.from_Rodrigues_vector(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_homochoric,_parameter_doc) + @util.extend_docstring(Rotation.from_homochoric, + extra_parameters=_parameter_doc) def from_homochoric(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_homochoric) return cls(rotation=Rotation.from_homochoric(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_cubochoric,_parameter_doc) + @util.extend_docstring(Rotation.from_cubochoric, + extra_parameters=_parameter_doc) def from_cubochoric(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_cubochoric) return cls(rotation=Rotation.from_cubochoric(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_spherical_component,_parameter_doc) + @util.extend_docstring(Rotation.from_spherical_component, + extra_parameters=_parameter_doc) def from_spherical_component(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_spherical_component) return cls(rotation=Rotation.from_spherical_component(**kwargs_rot),**kwargs_ori) @classmethod - @util.extended_docstring(Rotation.from_fiber_component,_parameter_doc) + @util.extend_docstring(Rotation.from_fiber_component, + extra_parameters=_parameter_doc) def from_fiber_component(cls, **kwargs) -> 'Orientation': kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_fiber_component) return cls(rotation=Rotation.from_fiber_component(**kwargs_rot),**kwargs_ori) @classmethod - @util.extend_docstring(_parameter_doc) + @util.extend_docstring(extra_parameters=_parameter_doc) def from_directions(cls, uvw: FloatSequence, hkl: FloatSequence, @@ -392,6 +403,10 @@ class Orientation(Rotation,Crystal): hkl : numpy.ndarray, shape (...,3) Lattice plane normal aligned with lab frame z-direction. + Returns + ------- + new : damask.Orientation + """ o = cls(**kwargs) x = o.to_frame(uvw=uvw) @@ -538,8 +553,7 @@ class Orientation(Rotation,Crystal): Notes ----- - Currently requires same crystal family for both orientations. - For extension to cases with differing symmetry see A. Heinz and P. Neumann 1991 and 10.1107/S0021889808016373. + Requires same crystal family for both orientations. Examples -------- @@ -569,6 +583,8 @@ class Orientation(Rotation,Crystal): >>> plt.show() """ + # For extension to cases with differing symmetry see + # https://doi.org/10.1107/S0021889808016373 and https://doi.org/10.1107/S0108767391006864 if self.family != other.family: raise NotImplementedError('disorientation between different crystal families') diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 204a735ad..c5c344765 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -732,7 +732,7 @@ class Rotation: Returns ------- x : numpy.ndarray, shape (...,3) - Cubochoric vector (x_1, x_2, x_3) with max(x_i) < 1/2*π^(2/3). + Cubochoric vector (x_1, x_2, x_3) with max(x_i) < 1/2*π^(2/3). Examples -------- @@ -768,6 +768,10 @@ class Rotation: P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. + Returns + ------- + new : damask.Rotation + """ qu = np.array(q,dtype=float) if qu.shape[:-2:-1] != (4,): raise ValueError('invalid shape') @@ -800,6 +804,10 @@ class Rotation: degrees : bool, optional Euler angles are given in degrees. Defaults to False. + Returns + ------- + new : damask.Rotation + Notes ----- Bunge Euler angles correspond to a rotation axis sequence of z–x'–z''. @@ -834,6 +842,10 @@ class Rotation: P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. + Returns + ------- + new : damask.Rotation + """ ax = np.array(axis_angle,dtype=float) if ax.shape[:-2:-1] != (4,): raise ValueError('invalid shape') @@ -898,6 +910,10 @@ class Rotation: R : numpy.ndarray, shape (...,3,3) Rotation matrix with det(R) = 1 and R.T ∙ R = I. + Returns + ------- + new : damask.Rotation + """ return Rotation.from_basis(R) @@ -914,6 +930,10 @@ class Rotation: b : numpy.ndarray, shape (...,2,3) Corresponding three-dimensional vectors of second basis. + Returns + ------- + new : damask.Rotation + """ a_ = np.array(a,dtype=float) b_ = np.array(b,dtype=float) @@ -946,6 +966,10 @@ class Rotation: P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. + Returns + ------- + new : damask.Rotation + """ ro = np.array(rho,dtype=float) if ro.shape[:-2:-1] != (4,): raise ValueError('invalid shape') @@ -974,6 +998,10 @@ class Rotation: P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. + Returns + ------- + new : damask.Rotation + """ ho = np.array(h,dtype=float) if ho.shape[:-2:-1] != (3,): raise ValueError('invalid shape') @@ -999,6 +1027,10 @@ class Rotation: P : int ∈ {-1,1}, optional Sign convention. Defaults to -1. + Returns + ------- + new : damask.Rotation + """ cu = np.array(x,dtype=float) if cu.shape[:-2:-1] != (3,): raise ValueError('invalid shape') @@ -1025,6 +1057,10 @@ class Rotation: A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. + Returns + ------- + new : damask.Rotation + """ rng = np.random.default_rng(rng_seed) r = rng.random(3 if shape is None else tuple(shape)+(3,) if hasattr(shape, '__iter__') else (shape,3)) # type: ignore @@ -1066,6 +1102,10 @@ class Rotation: A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. + Returns + ------- + new : damask.Rotation + Notes ----- Due to the distortion of Euler space in the vicinity of ϕ = 0, probability densities, p, defined on @@ -1150,7 +1190,7 @@ class Rotation: sigma: float = 0., shape: Union[int, IntSequence] = None, degrees: bool = False, - rng_seed: NumpyRngSeed = None): + rng_seed: NumpyRngSeed = None) -> 'Rotation': """ Initialize with samples from a Gaussian distribution around a given direction. @@ -1173,6 +1213,10 @@ class Rotation: A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. + Returns + ------- + new : damask.Rotation + Notes ----- The crystal direction for (θ=0,φ=0) is [0 0 1], diff --git a/python/damask/seeds.py b/python/damask/seeds.py index 89d9bf935..7cea9d51c 100644 --- a/python/damask/seeds.py +++ b/python/damask/seeds.py @@ -1,4 +1,3 @@ - """Functionality for generation of seed points for Voronoi or Laguerre tessellation.""" from typing import Tuple as _Tuple diff --git a/python/damask/util.py b/python/damask/util.py index 04e448422..d8ed340f8 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -210,6 +210,14 @@ def open_text(fname: _FileHandle, open(_Path(fname).expanduser(),mode,newline=('\n' if mode == 'w' else None)) +def execution_stamp(class_name: str, + function_name: str = None) -> str: + """Timestamp the execution of a (function within a) class.""" + now = _datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z') + _function_name = '' if function_name is None else f'.{function_name}' + return f'damask.{class_name}{_function_name} v{_version} ({now})' + + def natural_sort(key: str) -> _List[_Union[int, str]]: """ Natural sort. @@ -403,13 +411,6 @@ def project_equal_area(vector: _np.ndarray, return _np.roll(_np.block([v[...,:2]/_np.sqrt(1.0+_np.abs(v[...,2:3])),_np.zeros_like(v[...,2:3])]), -shift if keepdims else 0,axis=-1)[...,:3 if keepdims else 2] -def execution_stamp(class_name: str, - function_name: str = None) -> str: - """Timestamp the execution of a (function within a) class.""" - now = _datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z') - _function_name = '' if function_name is None else f'.{function_name}' - return f'damask.{class_name}{_function_name} v{_version} ({now})' - def hybrid_IA(dist: _np.ndarray, N: int, @@ -486,18 +487,18 @@ def shapeshifter(fro: _Tuple[int, ...], final_shape: _List[int] = [] index = 0 for i,item in enumerate(_to): - if item==_fro[index]: + if item == _fro[index]: final_shape.append(item) index+=1 else: final_shape.append(1) - if _fro[index]==1 and not keep_ones: + if _fro[index] == 1 and not keep_ones: index+=1 - if index==len(_fro): + if index == len(_fro): final_shape = final_shape+[1]*(len(_to)-i-1) break - if index!=len(_fro): raise ValueError(f'shapes cannot be shifted {fro} --> {to}') - return tuple(final_shape[::-1] if mode=='left' else final_shape) + if index != len(_fro): raise ValueError(f'shapes cannot be shifted {fro} --> {to}') + return tuple(final_shape[::-1] if mode == 'left' else final_shape) def shapeblender(a: _Tuple[int, ...], b: _Tuple[int, ...]) -> _Tuple[int, ...]: @@ -528,37 +529,82 @@ def shapeblender(a: _Tuple[int, ...], return a + b[i:] -def extend_docstring(extra_docstring: str) -> _Callable: +def _docstringer(docstring: _Union[str, _Callable], + extra_parameters: str = None, + # extra_examples: str = None, + # extra_notes: str = None, + return_type: _Union[str, _Callable] = None) -> str: """ - Decorator: Append to function's docstring. + Extend a docstring. Parameters ---------- - extra_docstring : str - Docstring to append. + docstring : str or callable, optional + Docstring (of callable) to extend. + extra_parameters : str, optional + Additional information to append to Parameters section. + return_type : str or callable, optional + Type of return variable. """ - def _decorator(func): - func.__doc__ += extra_docstring - return func - return _decorator + docstring_ = str( docstring if isinstance(docstring,str) + else docstring.__doc__ if hasattr(docstring,'__doc__') + else '') + d = dict(Parameters=extra_parameters, + # Examples=extra_examples, + # Notes=extra_notes, + ) + for key,extra in [(k,v) for (k,v) in d.items() if v is not None]: + if not (heading := _re.search(fr'^([ ]*){key}\s*\n\1{"-"*len(key)}', + docstring_,flags=_re.MULTILINE)): + raise RuntimeError(f"Docstring {docstring_} lacks a correctly formatted {key} section to insert values into") + content = [line for line in extra.split('\n') if line.strip()] + indent = len(heading.group(1)) + shift = min([len(line)-len(line.lstrip(' '))-indent for line in content]) + extra = '\n'.join([(line[shift:] if shift > 0 else + f'{" "*-shift}{line}') for line in content]) + docstring_ = _re.sub(fr'(^([ ]*){key}\s*\n\2{"-"*len(key)}[\n ]*[A-Za-z0-9 ]*: ([^\n]+\n)*)', + fr'\1{extra}\n', + docstring_,flags=_re.MULTILINE) + if return_type is None: + return docstring_ + else: + if isinstance(return_type,str): + return_type_ = return_type + else: + return_class = return_type.__annotations__.get('return','') + return_type_ = (_sys.modules[return_type.__module__].__name__.split('.')[0] + +'.' + +(return_class.__name__ if not isinstance(return_class,str) else return_class) + ) -def extended_docstring(f: _Callable, - extra_docstring: str) -> _Callable: + return _re.sub(r'(^([ ]*)Returns\s*\n\2-------\s*\n[ ]*[A-Za-z0-9 ]*: )(.*)\n', + fr'\1{return_type_}\n', + docstring_,flags=_re.MULTILINE) + +def extend_docstring(docstring: _Union[str, _Callable] = None, + extra_parameters: str = None) -> _Callable: """ - Decorator: Combine another function's docstring with a given docstring. + Decorator: Extend the function's docstring. Parameters ---------- - f : function - Function of which the docstring is taken. - extra_docstring : str - Docstring to append. + docstring : str or callable, optional + Docstring to extend. Defaults to that of decorated function. + extra_parameters : str, optional + Additional information to append to Parameters section. + + Notes + ----- + Return type will become own type if docstring is callable. """ def _decorator(func): - func.__doc__ = f.__doc__ + extra_docstring + func.__doc__ = _docstringer(func.__doc__ if docstring is None else docstring, + extra_parameters, + func if isinstance(docstring,_Callable) else None, + ) return func return _decorator @@ -649,7 +695,6 @@ def Bravais_to_Miller(*, [0,0,0,1]])) return _np.einsum('il,...l',basis,axis) - def Miller_to_Bravais(*, uvw: _np.ndarray = None, hkl: _np.ndarray = None) -> _np.ndarray: @@ -706,7 +751,6 @@ def dict_prune(d: _Dict) -> _Dict: return new - def dict_flatten(d: _Dict) -> _Dict: """ Recursively remove keys of single-entry dictionaries. diff --git a/python/tests/test_util.py b/python/tests/test_util.py index 80786249a..b2e689471 100644 --- a/python/tests/test_util.py +++ b/python/tests/test_util.py @@ -8,7 +8,6 @@ import h5py from damask import util - class TestUtil: @pytest.mark.xfail(sys.platform == 'win32', reason='echo is not a Windows command') @@ -208,3 +207,128 @@ class TestUtil: @pytest.mark.parametrize('kw_Miller,kw_Bravais',[('uvw','uvtw'),('hkl','hkil')]) def test_Bravais_Miller_Bravais(self,vector,kw_Miller,kw_Bravais): assert np.all(vector == util.Miller_to_Bravais(**{kw_Miller:util.Bravais_to_Miller(**{kw_Bravais:vector})})) + + + @pytest.mark.parametrize('extra_parameters',[""" + p2 : str, optional + p2 description 1 + p2 description 2 + """, + """ + + p2 : str, optional + p2 description 1 + p2 description 2 + + """, + """ +p2 : str, optional + p2 description 1 + p2 description 2 + """]) + @pytest.mark.parametrize('invalid_docstring',[""" + Function description + + Parameters ---------- + p0 : numpy.ndarray, shape (...,4) + p0 description 1 + p0 description 2 + p1 : int, optional + p1 description + + Remaining description + """, + """ + Function description + + Parameters + ---------- + p0 : numpy.ndarray, shape (...,4) + p0 description 1 + p0 description 2 + p1 : int, optional + p1 description + + Remaining description + """,]) + def test_extend_docstring_parameters(self,extra_parameters,invalid_docstring): + test_docstring = """ + Function description + + Parameters + ---------- + p0 : numpy.ndarray, shape (...,4) + p0 description 1 + p0 description 2 + p1 : int, optional + p1 description + + Remaining description + """ + invalid_docstring = """ + Function description + + Parameters ---------- + p0 : numpy.ndarray, shape (...,4) + p0 description 1 + p0 description 2 + p1 : int, optional + p1 description + + Remaining description + """ + expected = """ + Function description + + Parameters + ---------- + p0 : numpy.ndarray, shape (...,4) + p0 description 1 + p0 description 2 + p1 : int, optional + p1 description + p2 : str, optional + p2 description 1 + p2 description 2 + + Remaining description + """.split("\n") + assert expected == util._docstringer(test_docstring,extra_parameters).split('\n') + with pytest.raises(RuntimeError): + util._docstringer(invalid_docstring,extra_parameters) + + def test_replace_docstring_return_type(self): + class TestClassOriginal: + pass + + def original_func() -> TestClassOriginal: + pass + + class TestClassDecorated: + def decorated_func_bound(self) -> 'TestClassDecorated': + pass + + def decorated_func() -> TestClassDecorated: + pass + + original_func.__doc__ = """ + Function description/Parameters + + Returns + ------- + Return value : test_util.TestClassOriginal + + Remaining description + """ + + expected = """ + Function description/Parameters + + Returns + ------- + Return value : test_util.TestClassDecorated + + Remaining description + """ + assert expected == util._docstringer(original_func,return_type=decorated_func) + assert expected == util._docstringer(original_func,return_type=TestClassDecorated.decorated_func_bound)