From 9eb4e2d0de764825c33b55640550a0a8db3c684b Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 14 Nov 2020 17:13:38 +0100 Subject: [PATCH 01/15] corrected handling of scalar/array arguments + more tests + corrected handling of numpy types --- python/damask/_configmaterial.py | 27 +++++++++++++++------ python/tests/test_ConfigMaterial.py | 37 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/python/damask/_configmaterial.py b/python/damask/_configmaterial.py index 4f3eba816..6de2283f4 100644 --- a/python/damask/_configmaterial.py +++ b/python/damask/_configmaterial.py @@ -226,13 +226,13 @@ class ConfigMaterial(Config): return dup - def material_add(self,constituents,**kwargs): + def material_add(self,constituents=None,**kwargs): """ Add material entries. Parameters ---------- - constituents : dict + constituents : dict, optional Entries for 'constituents' as key-value pair. **kwargs Key-value pairs. @@ -263,13 +263,26 @@ class ConfigMaterial(Config): homogenization: SX """ - c = [{'constituents':u} for u in ConfigMaterial._constituents(**constituents)] + length = -1 + for v in kwargs.values(): + if hasattr(v,'__len__') and not isinstance(v,str): + if length != -1 and len(v) != length: + raise ValueError('Cannot add entries of different length') + else: + length = len(v) + length = max(1,length) + + c = [{} for _ in range(length)] if constituents is None else \ + [{'constituents':u} for u in ConfigMaterial._constituents(**constituents)] + if len(c) == 1: c = [copy.deepcopy(c[0]) for _ in range(length)] + + if length != 1 and length != len(c): + raise ValueError('Cannot add entries of different length') + for k,v in kwargs.items(): if hasattr(v,'__len__') and not isinstance(v,str): - if len(v) != len(c): - raise ValueError('Cannot add entries of different length') for i,vv in enumerate(v): - c[i][k] = [w.item() for w in vv] if isinstance(vv,np.ndarray) else vv.item() + c[i][k] = vv.item() if isinstance(vv,np.generic) else vv else: for i in range(len(c)): c[i][k] = v @@ -293,7 +306,7 @@ class ConfigMaterial(Config): if len(v) != N_material: raise ValueError('Cannot add entries of different length') for i,vv in enumerate(np.array(v)): - m[i][0][k] = [w.item() for w in vv] if isinstance(vv,np.ndarray) else vv.item() + m[i][0][k] = vv.item() if isinstance(vv,np.generic) else vv else: for i in range(N_material): m[i][0][k] = v diff --git a/python/tests/test_ConfigMaterial.py b/python/tests/test_ConfigMaterial.py index 0f2b6a90c..25c05d711 100644 --- a/python/tests/test_ConfigMaterial.py +++ b/python/tests/test_ConfigMaterial.py @@ -62,6 +62,12 @@ class TestConfigMaterial: del material_config['material'][0]['homogenization'] assert not material_config.is_complete + def test_incomplete_homogenization_N_constituents(self,reference_dir): + material_config = ConfigMaterial.load(reference_dir/'material.yaml') + for h in material_config['homogenization'].keys(): + del material_config['homogenization'][h]['N_constituents'] + assert not material_config.is_complete + def test_incomplete_phase_lattice(self,reference_dir): material_config = ConfigMaterial.load(reference_dir/'material.yaml') del material_config['phase']['Aluminum']['lattice'] @@ -85,9 +91,36 @@ class TestConfigMaterial: assert len(c['material']) == N for i,m in enumerate(c['material']): c = m['constituents'][0] - assert m['c'] == 1 and c['b'] == 0 and c['a'] == [i,1] + assert m['c'] == 1 and c['b'] == 0 and (c['a'] == [i,1]).all() - def test__constituents(self): + def test_constituents(self): c = ConfigMaterial._constituents(c=1,v=[2,3]) assert c[0][0]['c'] == c[1][0]['c'] == 1 assert c[0][0]['v'] == c[1][0]['v'] -1 ==2 + + @pytest.mark.parametrize('constituents',[{'W':1,'X':[2,3]},{'Y':4},{'Z':[5,6]}]) + @pytest.mark.parametrize('a',[[7.,8.],9.]) + @pytest.mark.parametrize('b',['bd',['efg','hi']]) + def test_material_add(self,tmp_path,constituents,a,b): + len_c = len(ConfigMaterial()._constituents(1,**constituents)) + len_a = len(a) if isinstance(a,list) else 1 + len_b = len(b) if isinstance(b,list) else 1 + m = ConfigMaterial().material_add(constituents,a=a,b=b) + m.save() + assert len(m['material']) == np.max([len_a,len_b,len_c]) + + @pytest.mark.parametrize('constituents',[{'W':1,'X':np.array([2,3])},{'Y':4},{'Z':np.array([5,6])}]) + @pytest.mark.parametrize('a',[np.array([7,8]),9]) + def test_material_add_np(self,tmp_path,constituents,a): + len_c = len(ConfigMaterial()._constituents(1,**constituents)) + len_a = len(a) if isinstance(a,np.ndarray) else 1 + m = ConfigMaterial().material_add(constituents,ld=a) + m.save() + assert len(m['material']) == np.max([len_a,len_c]) + + @pytest.mark.parametrize('constituents',[{'X':np.array([2,3,4,5])},{'Y':4}]) + @pytest.mark.parametrize('a',[np.array([1,2,3]),[4,5,6]]) + @pytest.mark.parametrize('b',[np.array([6.,7.]),[8.,9.]]) + def test_material_add_invalid(self,constituents,a,b): + with pytest.raises(ValueError): + ConfigMaterial().material_add(constituents,a=a,u=b) From 764aacf2a10c686a031bc4bfa5e0c047d8981a99 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 14 Nov 2020 17:54:47 +0100 Subject: [PATCH 02/15] increasing test coverage --- python/damask/_environment.py | 6 +++--- python/damask/_geom.py | 10 +++++++++- python/tests/test_Geom.py | 5 +++++ python/tests/test_VTK.py | 9 +++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/python/damask/_environment.py b/python/damask/_environment.py index 9983a91aa..b040920c2 100644 --- a/python/damask/_environment.py +++ b/python/damask/_environment.py @@ -9,8 +9,6 @@ class Environment: @property def screen_size(self): - width = 1024 - height = 768 try: import wx _ = wx.App(False) # noqa @@ -23,7 +21,9 @@ class Environment: height = tk.winfo_screenheight() tk.destroy() except Exception as e: - pass + width = 1024 + height = 768 + return (width,height) diff --git a/python/damask/_geom.py b/python/damask/_geom.py index 3d73861ad..798bad0f8 100644 --- a/python/damask/_geom.py +++ b/python/damask/_geom.py @@ -2,6 +2,7 @@ import copy import multiprocessing as mp from functools import partial from os import path +import warnings import numpy as np import pandas as pd @@ -188,12 +189,16 @@ class Geom: """ Read a geom file. + Storing geometry files in ASCII format is deprecated. + This function will be removed in a future version of DAMASK. + Parameters ---------- fname : str, pathlib.Path, or file handle Geometry file to read. """ + warnings.warn('Support for ASCII-based geom format will be removed in DAMASK 3.1.0', DeprecationWarning) try: f = open(fname) except TypeError: @@ -247,7 +252,6 @@ class Geom: return Geom(material.reshape(grid,order='F'),size,origin,comments) - @staticmethod def load_DREAM3D(fname,base_group,point_data=None,material='FeatureIds'): """ @@ -523,6 +527,9 @@ class Geom: """ Write a geom file. + Storing geometry files in ASCII format is deprecated. + This function will be removed in a future version of DAMASK. + Parameters ---------- fname : str or file handle @@ -531,6 +538,7 @@ class Geom: Compress geometry with 'x of y' and 'a to b'. """ + warnings.warn('Support for ASCII-based geom format will be removed in DAMASK 3.1.0', DeprecationWarning) header = [f'{len(self.comments)+4} header'] + self.comments \ + ['grid a {} b {} c {}'.format(*self.grid), 'size x {} y {} z {}'.format(*self.size), diff --git a/python/tests/test_Geom.py b/python/tests/test_Geom.py index a9b9c91b5..09c43fe7e 100644 --- a/python/tests/test_Geom.py +++ b/python/tests/test_Geom.py @@ -45,6 +45,8 @@ class TestGeom: new = Geom(default.material[1:,1:,1:]+1,default.size*.9,np.ones(3)-default.origin,comments=['modified']) assert str(default.diff(new)) != '' + def test_repr(self,default): + print(default) def test_read_write_vtr(self,default,tmp_path): default.save(tmp_path/'default') @@ -70,6 +72,9 @@ class TestGeom: Geom(default.material[1:,1:,1:], size=np.ones(2)) + def test_save_load_ASCII(self,default,tmp_path): + default.save_ASCII(tmp_path/'ASCII') + assert geom_equal(Geom.load_ASCII(tmp_path/'ASCII'),default) def test_invalid_origin(self,default): with pytest.raises(ValueError): diff --git a/python/tests/test_VTK.py b/python/tests/test_VTK.py index bab6051b0..3b14981ae 100644 --- a/python/tests/test_VTK.py +++ b/python/tests/test_VTK.py @@ -84,6 +84,15 @@ class TestVTK: time.sleep(.5) assert(False) + def test_compress(self,tmp_path): + points = np.random.rand(102,3) + v = VTK.from_poly_data(points) + fname_c = tmp_path/'compreesed.vtp' + fname_p = tmp_path/'plain.vtp' + v.save(fname_c,parallel=False,compress=False) + v.save(fname_p,parallel=False,compress=True) + assert(VTK.load(fname_c).__repr__() == VTK.load(fname_p).__repr__()) + @pytest.mark.parametrize('fname',['a','a.vtp','a.b','a.b.vtp']) def test_filename_variations(self,tmp_path,fname): From 9160677ff330541e6c96fd0b2d6d4ca9933a07e3 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 14 Nov 2020 19:00:51 +0100 Subject: [PATCH 03/15] no need for special damask class (no action=extend) --- processing/post/addCompatibilityMismatch.py | 2 +- processing/post/addDisplacement.py | 2 +- processing/post/addSchmidfactors.py | 2 +- processing/pre/geom_fromDREAM3D.py | 2 +- processing/pre/geom_fromMinimalSurface.py | 2 +- processing/pre/geom_fromOsteonGeometry.py | 2 +- processing/pre/mentat_pbcOnBoxMesh.py | 38 ++++++++++----------- processing/pre/mentat_spectralBox.py | 2 +- processing/pre/seeds_fromDistribution.py | 2 +- processing/pre/seeds_fromPokes.py | 2 +- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/processing/post/addCompatibilityMismatch.py b/processing/post/addCompatibilityMismatch.py index 0e7d3ea42..9a2bd1a5b 100755 --- a/processing/post/addCompatibilityMismatch.py +++ b/processing/post/addCompatibilityMismatch.py @@ -136,7 +136,7 @@ def shapeMismatch(size,F,nodes,centres): # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [ASCIItable(s)]', description = """ +parser = OptionParser(usage='%prog options [ASCIItable(s)]', description = """ Add column(s) containing the shape and volume mismatch resulting from given deformation gradient. Operates on periodic three-dimensional x,y,z-ordered data sets. diff --git a/processing/post/addDisplacement.py b/processing/post/addDisplacement.py index a6cff86ab..069d923e8 100755 --- a/processing/post/addDisplacement.py +++ b/processing/post/addDisplacement.py @@ -16,7 +16,7 @@ scriptID = ' '.join([scriptName,damask.version]) # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [ASCIItable(s)]', description = """ +parser = OptionParser(usage='%prog options [ASCIItable(s)]', description = """ Add displacments resulting from deformation gradient field. Operates on periodic three-dimensional x,y,z-ordered data sets. Outputs at cell centers or cell nodes (into separate file). diff --git a/processing/post/addSchmidfactors.py b/processing/post/addSchmidfactors.py index 8f43308cb..10f2443be 100755 --- a/processing/post/addSchmidfactors.py +++ b/processing/post/addSchmidfactors.py @@ -98,7 +98,7 @@ slipSystems = { # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [ASCIItable(s)]', description = """ +parser = OptionParser(usage='%prog options [ASCIItable(s)]', description = """ Add columns listing Schmid factors (and optional trace vector of selected system) for given Euler angles. """, version = scriptID) diff --git a/processing/pre/geom_fromDREAM3D.py b/processing/pre/geom_fromDREAM3D.py index eca837455..eb4fe443e 100755 --- a/processing/pre/geom_fromDREAM3D.py +++ b/processing/pre/geom_fromDREAM3D.py @@ -14,7 +14,7 @@ scriptID = ' '.join([scriptName,damask.version]) # MAIN #-------------------------------------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [DREAM.3Dfile(s)]', description = """ +parser = OptionParser(usage='%prog options [DREAM.3Dfile(s)]', description = """ Converts DREAM.3D file. Input can be cell data (direct pointwise takeover) or grain data (individual grains are segmented). Requires orientation data as quaternion. diff --git a/processing/pre/geom_fromMinimalSurface.py b/processing/pre/geom_fromMinimalSurface.py index eb0cdcc3b..5f58d960c 100755 --- a/processing/pre/geom_fromMinimalSurface.py +++ b/processing/pre/geom_fromMinimalSurface.py @@ -17,7 +17,7 @@ minimal_surfaces = list(damask.Geom._minimal_surface.keys()) # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [geomfile]', description = """ +parser = OptionParser(usage='%prog options [geomfile]', description = """ Generate a bicontinuous structure of given type. """, version = scriptID) diff --git a/processing/pre/geom_fromOsteonGeometry.py b/processing/pre/geom_fromOsteonGeometry.py index 0b6d48001..51aca3056 100755 --- a/processing/pre/geom_fromOsteonGeometry.py +++ b/processing/pre/geom_fromOsteonGeometry.py @@ -17,7 +17,7 @@ scriptID = ' '.join([scriptName,damask.version]) # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [geomfile]', description = """ +parser = OptionParser(usage='%prog options [geomfile]', description = """ Generate description of an osteon enclosing the Harvesian canal and separated by interstitial tissue. The osteon phase is lamellar with a twisted plywood structure. Its fiber orientation is oscillating by +/- amplitude within one period. diff --git a/processing/pre/mentat_pbcOnBoxMesh.py b/processing/pre/mentat_pbcOnBoxMesh.py index 3677a5efa..e03a4e5c5 100755 --- a/processing/pre/mentat_pbcOnBoxMesh.py +++ b/processing/pre/mentat_pbcOnBoxMesh.py @@ -30,11 +30,11 @@ def parseMFD(dat): # lines that start with a space are numerical data if line[0] == ' ': formatted[section]['els'].append([]) - + # grab numbers nums = re.split(r'\s+', line.strip()) - - for num in nums: + + for num in nums: # floating point has format ' -x.xxxxxxxxxxxxe+yy' # scientific notation is used for float if (len(num) >= 4) and (num[-4] == 'e'): @@ -47,7 +47,7 @@ def parseMFD(dat): else: formatted[section]['els'].append([]) formatted[section]['els'][-1] = line - + else: # Not in a section, we are looking for a =beg= now search = re.search(r'=beg=\s+(\d+)\s\((.*?)\)', line) if search is not None: # found start of a new section @@ -60,7 +60,7 @@ def parseMFD(dat): section += 1 formatted.append({'label': '', 'uid': -2, 'els': []}) # make dummy section to store unrecognized data formatted[section]['els'].append(line) - + return formatted def asMFD(mfd_data): @@ -93,14 +93,14 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to 'max': np.zeros(3,dtype='d'), 'delta': np.zeros(3,dtype='d'), } - + mfd_dict = {} for i in range(len(mfd_data)): mfd_dict[mfd_data[i]['label']] = i NodeCoords = np.array(mfd_data[mfd_dict['nodes']]['els'][1::4])[:,1:4] - Nnodes = NodeCoords.shape[0] - + Nnodes = NodeCoords.shape[0] + box['min'] = NodeCoords.min(axis=0) # find the bounding box box['max'] = NodeCoords.max(axis=0) box['delta'] = box['max']-box['min'] @@ -131,7 +131,7 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to elif (key[base[coord]] == "%.8e"%box['max'][coord]): # compare to max of bounding box (i.e. is on outer face?) Nmax += 1 # count outer (front) face membership maxFlag[coord] = True # remember face membership (for linked nodes) - + if Nmin > 0: # node is on a back face # prepare for any non-existing entries in the data structure if key['x'] not in baseNode.keys(): @@ -140,17 +140,17 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to baseNode[key['x']][key['y']] = {} if key['z'] not in baseNode[key['x']][key['y']].keys(): baseNode[key['x']][key['y']][key['z']] = 0 - + baseNode[key['x']][key['y']][key['z']] = node+1 # remember the base node id if Nmax > 0 and Nmax >= Nmin: # node is on at least as many front than back faces if any([maxFlag[i] and active[i] for i in range(3)]): linkNodes.append({'id': node+1,'coord': NodeCoords[node], 'faceMember': [maxFlag[i] and active[i] for i in range(3)]}) - + mfd_data[mfd_dict['entities']]['els'][0][0] += len(linkNodes) * 3 - + baseCorner = baseNode["%.8e"%box['min'][0]]["%.8e"%box['min'][1]]["%.8e"%box['min'][2]] # detect ultimate base node - + links = {'uid': 1705, 'label': 'links', 'els': [[7,0],[9,0]]} linkID = 0 for node in linkNodes: # loop over all linked nodes @@ -165,7 +165,7 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to for dof in [1,2,3]: tied_node = node['id'] nterms = 1 + nLinks - + linkID += 1 # Link header links['els'].append('link{0}\n'.format(linkID)) @@ -173,10 +173,10 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to links['els'].append([0]) links['els'].append([0]) links['els'].append([0, 0, 0, tied_node]) - + # these need to be put in groups of four link_payload = [dof, 0, nterms] - + # Individual node contributions (node, dof, coef.) for i in range(nterms): if i == nLinks: @@ -190,7 +190,7 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to link_payload.append(1.0 - nLinks) else: link_payload.append(1.0) - + # Needs to be formatted 4 data points per row, character width of 20, so 80 total for j in range(0, len(link_payload), 4): links['els'].append(link_payload[j:j+4]) @@ -206,9 +206,9 @@ def add_servoLinks(mfd_data,active=[True,True,True]): # directions on which to #-------------------------------------------------------------------------------------------------- # MAIN -#-------------------------------------------------------------------------------------------------- +#-------------------------------------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [file[s]]', description = """ +parser = OptionParser(usage='%prog options [file[s]]', description = """ Set up servo linking to achieve periodic boundary conditions for a regular hexahedral mesh. Use *py_connection to operate on model presently opened in MSC.Mentat. """, version = scriptID) diff --git a/processing/pre/mentat_spectralBox.py b/processing/pre/mentat_spectralBox.py index 7d78cb973..57c2644c2 100755 --- a/processing/pre/mentat_spectralBox.py +++ b/processing/pre/mentat_spectralBox.py @@ -168,7 +168,7 @@ def initial_conditions(material): # MAIN #-------------------------------------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [file[s]]', description = """ +parser = OptionParser(usage='%prog options [file[s]]', description = """ Generate MSC.Marc FE hexahedral mesh from geom file. """, version = scriptID) diff --git a/processing/pre/seeds_fromDistribution.py b/processing/pre/seeds_fromDistribution.py index 375f33ad3..1f20db995 100755 --- a/processing/pre/seeds_fromDistribution.py +++ b/processing/pre/seeds_fromDistribution.py @@ -164,7 +164,7 @@ class myThread (threading.Thread): # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [file[s]]', description = """ +parser = OptionParser(usage='%prog options [file[s]]', description = """ Monte Carlo simulation to produce seed file that gives same size distribution like given geometry file. """, version = scriptID) diff --git a/processing/pre/seeds_fromPokes.py b/processing/pre/seeds_fromPokes.py index 887d76392..67acf0f6b 100755 --- a/processing/pre/seeds_fromPokes.py +++ b/processing/pre/seeds_fromPokes.py @@ -16,7 +16,7 @@ scriptID = ' '.join([scriptName,damask.version]) # MAIN # -------------------------------------------------------------------- -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [file[s]]', description = """ +parser = OptionParser(usage='%prog options [file[s]]', description = """ Create seeds file by poking at 45 degree through given geom file. Mimics APS Beamline 34-ID-E DAXM poking. From 149fce0a7eb8c5149ab1e5302bea2816034b0cf5 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 14 Nov 2020 19:24:31 +0100 Subject: [PATCH 04/15] increase test coverage --- python/damask/util.py | 13 ++----------- python/tests/test_Table.py | 6 ++++++ python/tests/test_util.py | 8 ++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/python/damask/util.py b/python/damask/util.py index d02877f8f..fa983d64e 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -56,6 +56,8 @@ def croak(what, newline = True): """ Write formated to stderr. + DEPRECATED + Parameters ---------- what : str or iterable @@ -393,17 +395,6 @@ class bcolors: UNDERLINE = '\033[4m' CROSSOUT = '\033[9m' - def disable(self): - self.HEADER = '' - self.OKBLUE = '' - self.OKGREEN = '' - self.WARNING = '' - self.FAIL = '' - self.ENDC = '' - self.BOLD = '' - self.UNDERLINE = '' - self.CROSSOUT = '' - class return_message: """Object with formatted return message.""" diff --git a/python/tests/test_Table.py b/python/tests/test_Table.py index 9dad681bd..8c4465dc1 100644 --- a/python/tests/test_Table.py +++ b/python/tests/test_Table.py @@ -17,6 +17,12 @@ def reference_dir(reference_dir_base): class TestTable: + def test_repr(self,default): + print(default) + + def test_len(self): + len(Table(np.random.rand(7,3),{'X':3})) == 7 + def test_get_scalar(self,default): d = default.get('s') assert np.allclose(d,1.0) and d.shape[1:] == (1,) diff --git a/python/tests/test_util.py b/python/tests/test_util.py index 19b6377f6..9906689e9 100644 --- a/python/tests/test_util.py +++ b/python/tests/test_util.py @@ -15,6 +15,10 @@ class TestUtil: out,err = util.execute('sh -c "echo $test_for_execute"',env={'test_for_execute':'test'}) assert out=='test\n' and err=='' + def test_execute_invalid(self): + with pytest.raises(RuntimeError): + util.execute('/bin/false') + def test_croak(self): util.croak('Burp!') @@ -93,3 +97,7 @@ class TestUtil: ]) def test_shapeblender(self,a,b,answer): assert util.shapeblender(a,b) == answer + + @pytest.mark.parametrize('style',[util.emph,util.deemph,util.delete,util.strikeout]) + def test_decorate(self,style): + assert 'DAMASK' in style('DAMASK') From 1eb9d494c79bc560fbd126cbe9e52f8f6f7772c5 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sat, 14 Nov 2020 19:51:15 +0100 Subject: [PATCH 05/15] not specific to Orientation class --- python/damask/_orientation.py | 44 +++++++++++------------------------ python/damask/util.py | 20 ++++++++++++++-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index e0a00e0a0..c849b0d89 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -4,7 +4,7 @@ from . import Rotation from . import util from . import mechanics -__parameter_doc__ = \ +_parameter_doc = \ """lattice : str Either a crystal family out of [triclinic, monoclinic, orthorhombic, tetragonal, hexagonal, cubic] or a Bravais lattice out of [aP, mP, mS, oP, oS, oI, oF, tP, tI, hP, cP, cI, cF]. @@ -27,22 +27,6 @@ __parameter_doc__ = \ """ -def extend_docstring(): - """Decorator: Append Orientation parameter documentation to function's docstring.""" - def _decorator(func): - func.__doc__ += __parameter_doc__ - return func - return _decorator - - -def extended_docstring(f): - """Decorator: Combine Orientation parameter documentation with another function's docstring.""" - def _decorator(func): - func.__doc__ = f.__doc__ + __parameter_doc__ - return func - return _decorator - - class Orientation(Rotation): """ Representation of crystallographic orientation as combination of rotation and either crystal family or Bravais lattice. @@ -128,7 +112,7 @@ class Orientation(Rotation): } - @extend_docstring() + @util.extend_docstring(_parameter_doc) def __init__(self, rotation = None, lattice = None, @@ -279,73 +263,73 @@ class Orientation(Rotation): @classmethod - @extended_docstring(Rotation.from_random) + @util.extended_docstring(Rotation.from_random,_parameter_doc) def from_random(cls,**kwargs): return cls(rotation=Rotation.from_random(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_quaternion) + @util.extended_docstring(Rotation.from_quaternion,_parameter_doc) def from_quaternion(cls,**kwargs): return cls(rotation=Rotation.from_quaternion(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_Eulers) + @util.extended_docstring(Rotation.from_Eulers,_parameter_doc) def from_Eulers(cls,**kwargs): return cls(rotation=Rotation.from_Eulers(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_axis_angle) + @util.extended_docstring(Rotation.from_axis_angle,_parameter_doc) def from_axis_angle(cls,**kwargs): return cls(rotation=Rotation.from_axis_angle(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_basis) + @util.extended_docstring(Rotation.from_basis,_parameter_doc) def from_basis(cls,**kwargs): return cls(rotation=Rotation.from_basis(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_matrix) + @util.extended_docstring(Rotation.from_matrix,_parameter_doc) def from_matrix(cls,**kwargs): return cls(rotation=Rotation.from_matrix(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_Rodrigues) + @util.extended_docstring(Rotation.from_Rodrigues,_parameter_doc) def from_Rodrigues(cls,**kwargs): return cls(rotation=Rotation.from_Rodrigues(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_homochoric) + @util.extended_docstring(Rotation.from_homochoric,_parameter_doc) def from_homochoric(cls,**kwargs): return cls(rotation=Rotation.from_homochoric(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_cubochoric) + @util.extended_docstring(Rotation.from_cubochoric,_parameter_doc) def from_cubochoric(cls,**kwargs): return cls(rotation=Rotation.from_cubochoric(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_spherical_component) + @util.extended_docstring(Rotation.from_spherical_component,_parameter_doc) def from_spherical_component(cls,**kwargs): return cls(rotation=Rotation.from_spherical_component(**kwargs),**kwargs) @classmethod - @extended_docstring(Rotation.from_fiber_component) + @util.extended_docstring(Rotation.from_fiber_component,_parameter_doc) def from_fiber_component(cls,**kwargs): return cls(rotation=Rotation.from_fiber_component(**kwargs),**kwargs) @classmethod - @extend_docstring() + @util.extend_docstring(_parameter_doc) def from_directions(cls,uvw,hkl,**kwargs): """ Initialize orientation object from two crystallographic directions. diff --git a/python/damask/util.py b/python/damask/util.py index fa983d64e..d1b3ed842 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -26,8 +26,8 @@ __all__=[ 'return_message', 'extendableOption', 'execution_stamp', - 'shapeshifter', - 'shapeblender', + 'shapeshifter', 'shapeblender', + 'extend_docstring', 'extended_docstring' ] #################################################################################################### @@ -302,6 +302,22 @@ def shapeblender(a,b): return a + b[i:] +def extend_docstring(general): + """Decorator: Append Orientation parameter documentation to function's docstring.""" + def _decorator(func): + func.__doc__ += general + return func + return _decorator + + +def extended_docstring(f,general): + """Decorator: Combine Orientation parameter documentation with another function's docstring.""" + def _decorator(func): + func.__doc__ = f.__doc__ + general + return func + return _decorator + + #################################################################################################### # Classes #################################################################################################### From 05c1007addfae8ade0ba6462e0c123db978ccc28 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 10:22:01 +0100 Subject: [PATCH 06/15] documentation improvments + acceptance of lists example code at respective function, no space in 'or' variable names (sphinx cannot handle this) --- python/damask/_orientation.py | 85 ++++++++++++++++++++++------------- python/damask/_rotation.py | 2 +- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index c849b0d89..aa15879bf 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -69,18 +69,6 @@ class Orientation(Rotation): An array of 3 x 5 random orientations reduced to the fundamental zone of tetragonal symmetry: >>> damask.Orientation.from_random(shape=(3,5),lattice='tetragonal').reduced - Disorientation between two specific orientations of hexagonal symmetry: - >>> a = damask.Orientation.from_Eulers(phi=[123,32,21],degrees=True,lattice='hexagonal') - >>> b = damask.Orientation.from_Eulers(phi=[104,11,87],degrees=True,lattice='hexagonal') - >>> a.disorientation(b) - - Inverse pole figure color of the e_3 direction for a crystal in "Cube" orientation with cubic symmetry: - >>> o = damask.Orientation(lattice='cubic') - >>> o.IPF_color(o.to_SST(np.array([0,0,1]))) - - Schmid matrix (in lab frame) of slip systems of a face-centered cubic crystal in "Goss" orientation: - >>> damask.Orientation.from_Eulers(phi=[0,45,0],degrees=True,lattice='cF').Schmid('slip') - """ crystal_families = ['triclinic', @@ -831,6 +819,14 @@ class Orientation(Rotation): rgb : numpy.ndarray of shape (...,3) RGB array of IPF colors. + Examples + -------- + Inverse pole figure color of the e_3 direction for a crystal in "Cube" orientation with cubic symmetry: + + >>> o = damask.Orientation(lattice='cubic') + >>> o.IPF_color(o.to_SST([0,0,1])) + array([1., 0., 0.]) + References ---------- Bases are computed from @@ -851,7 +847,8 @@ class Orientation(Rotation): ... } """ - if vector.shape[-1] != 3: + vector_ = np.array(vector) + if vector_.shape[-1] != 3: raise ValueError('Input is not a field of three-dimensional vectors.') if self.family == 'cubic': @@ -887,23 +884,23 @@ class Orientation(Rotation): [ 0., 1., 0.] ]), } else: # direct exit for unspecified symmetry - return np.zeros_like(vector) + return np.zeros_like(vector_) if proper: components_proper = np.around(np.einsum('...ji,...i', - np.broadcast_to(basis['proper'], vector.shape+(3,)), - vector), 12) + np.broadcast_to(basis['proper'], vector_.shape+(3,)), + vector_), 12) components_improper = np.around(np.einsum('...ji,...i', - np.broadcast_to(basis['improper'], vector.shape+(3,)), - vector), 12) + np.broadcast_to(basis['improper'], vector_.shape+(3,)), + vector_), 12) in_SST = np.all(components_proper >= 0.0,axis=-1) \ | np.all(components_improper >= 0.0,axis=-1) components = np.where((in_SST & np.all(components_proper >= 0.0,axis=-1))[...,np.newaxis], components_proper,components_improper) else: components = np.around(np.einsum('...ji,...i', - np.broadcast_to(basis['improper'], vector.shape+(3,)), - np.block([vector[...,:2],np.abs(vector[...,2:3])])), 12) + np.broadcast_to(basis['improper'], vector_.shape+(3,)), + np.block([vector_[...,:2],np.abs(vector_[...,2:3])])), 12) in_SST = np.all(components >= 0.0,axis=-1) @@ -941,6 +938,22 @@ class Orientation(Rotation): 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. + Examples + -------- + Disorientation between two specific orientations of hexagonal symmetry: + + >>> import damask + >>> a = damask.Orientation.from_Eulers(phi=[123,32,21],degrees=True,lattice='hexagonal') + >>> b = damask.Orientation.from_Eulers(phi=[104,11,87],degrees=True,lattice='hexagonal') + >>> a.disorientation(b) + Crystal family hexagonal + Quaternion: (real=0.976, imag=<+0.189, +0.018, +0.103>) + Matrix: + [[ 0.97831006 0.20710935 0.00389135] + [-0.19363288 0.90765544 0.37238141] + [ 0.07359167 -0.36505797 0.92807163]] + Bunge Eulers / deg: (11.40, 21.86, 0.60) + """ if self.family is None or other.family is None: raise ValueError('Missing crystal symmetry') @@ -1049,8 +1062,8 @@ class Orientation(Rotation): raise ValueError('Missing crystal symmetry') eq = self.equivalent - blend = util.shapeblender(eq.shape,vector.shape[:-1]) - poles = eq.broadcast_to(blend,mode='right') @ np.broadcast_to(vector,blend+(3,)) + blend = util.shapeblender(eq.shape,np.array(vector).shape[:-1]) + poles = eq.broadcast_to(blend,mode='right') @ np.broadcast_to(np.array(vector),blend+(3,)) ok = self.in_SST(poles,proper=proper) ok &= np.cumsum(ok,axis=0) == 1 loc = np.where(ok) @@ -1069,12 +1082,12 @@ class Orientation(Rotation): Parameters ---------- - uvtw | hkil : numpy.ndarray of shape (...,4) + uvtw|hkil : numpy.ndarray of shape (...,4) Miller–Bravais indices of crystallographic direction [uvtw] or plane normal (hkil). Returns ------- - uvw | hkl : numpy.ndarray of shape (...,3) + uvw|hkl : numpy.ndarray of shape (...,3) Miller indices of [uvw] direction or (hkl) plane normal. """ @@ -1097,12 +1110,12 @@ class Orientation(Rotation): Parameters ---------- - uvw | hkl : numpy.ndarray of shape (...,3) + uvw|hkl : numpy.ndarray of shape (...,3) Miller indices of crystallographic direction [uvw] or plane normal (hkl). Returns ------- - uvtw | hkil : numpy.ndarray of shape (...,4) + uvtw|hkil : numpy.ndarray of shape (...,4) Miller–Bravais indices of [uvtw] direction or (hkil) plane normal. """ @@ -1126,7 +1139,7 @@ class Orientation(Rotation): Parameters ---------- - direction | normal : numpy.ndarray of shape (...,3) + direction|normal : numpy.ndarray of shape (...,3) Vector along direction or plane normal. Returns @@ -1150,7 +1163,7 @@ class Orientation(Rotation): Parameters ---------- - uvw | hkl : numpy.ndarray of shape (...,3) + uvw|hkl : numpy.ndarray of shape (...,3) Miller indices of crystallographic direction or plane normal. with_symmetry : bool, optional Calculate all N symmetrically equivalent vectors. @@ -1178,7 +1191,7 @@ class Orientation(Rotation): Parameters ---------- - uvw | hkl : numpy.ndarray of shape (...,3) + uvw|hkl : numpy.ndarray of shape (...,3) Miller indices of crystallographic direction or plane normal. with_symmetry : bool, optional Calculate all N symmetrically equivalent vectors. @@ -1201,13 +1214,25 @@ class Orientation(Rotation): Parameters ---------- mode : str - Type of kinematics, e.g. 'slip' or 'twin'. + Type of kinematics, i.e. 'slip' or 'twin'. Returns ------- P : numpy.ndarray of shape (...,N,3,3) Schmid matrix for each of the N deformation systems. + Examples + -------- + Schmid matrix (in lab frame) of slip systems of a face-centered + cubic crystal in "Goss" orientation. + + >>> import damask + >>> import numpy as np + >>> np.set_printoptions(3,suppress=True,floatmode='fixed') + >>> damask.Orientation.from_Eulers(phi=[0,45,0],degrees=True,lattice='cF').Schmid('slip')[0] + array([[ 0.000, 0.000, 0.000], + [ 0.577, -0.000, 0.816], + [ 0.000, 0.000, 0.000]]) """ d = self.to_frame(uvw=self.kinematics[mode]['direction'],with_symmetry=False) p = self.to_frame(hkl=self.kinematics[mode]['plane'] ,with_symmetry=False) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index deae8b6ac..145b27884 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -404,7 +404,7 @@ class Rotation: Returns ------- h : numpy.ndarray of shape (...,3) - Homochoric vector: (h_1, h_2, h_3), ǀhǀ < 1/2*π^(2/3). + Homochoric vector: (h_1, h_2, h_3), ǀhǀ < (3/4*π)^(1/3). """ return Rotation._qu2ho(self.quaternion) From 51e5dda702fda11c157fd53dc8f3a0b8d8bd6927 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 10:53:23 +0100 Subject: [PATCH 07/15] documentation + consistent string formatting --- python/damask/_geom.py | 10 +++---- python/damask/_result.py | 4 +-- python/damask/util.py | 55 ++++++++++++++++++++++------------- python/tests/test_Rotation.py | 6 ++-- python/tests/test_util.py | 2 +- 5 files changed, 45 insertions(+), 32 deletions(-) diff --git a/python/damask/_geom.py b/python/damask/_geom.py index 798bad0f8..d364ad6e8 100644 --- a/python/damask/_geom.py +++ b/python/damask/_geom.py @@ -74,23 +74,23 @@ class Geom: """ message = [] if np.any(other.grid != self.grid): - message.append(util.delete(f'grid a b c: {util.srepr(other.grid," x ")}')) + message.append(util.deemph(f'grid a b c: {util.srepr(other.grid," x ")}')) message.append(util.emph( f'grid a b c: {util.srepr( self.grid," x ")}')) if not np.allclose(other.size,self.size): - message.append(util.delete(f'size x y z: {util.srepr(other.size," x ")}')) + message.append(util.deemph(f'size x y z: {util.srepr(other.size," x ")}')) message.append(util.emph( f'size x y z: {util.srepr( self.size," x ")}')) if not np.allclose(other.origin,self.origin): - message.append(util.delete(f'origin x y z: {util.srepr(other.origin," ")}')) + message.append(util.deemph(f'origin x y z: {util.srepr(other.origin," ")}')) message.append(util.emph( f'origin x y z: {util.srepr( self.origin," ")}')) if other.N_materials != self.N_materials: - message.append(util.delete(f'# materials: {other.N_materials}')) + message.append(util.deemph(f'# materials: {other.N_materials}')) message.append(util.emph( f'# materials: { self.N_materials}')) if np.nanmax(other.material) != np.nanmax(self.material): - message.append(util.delete(f'max material: {np.nanmax(other.material)}')) + message.append(util.deemph(f'max material: {np.nanmax(other.material)}')) message.append(util.emph( f'max material: {np.nanmax( self.material)}')) return util.return_message(message) diff --git a/python/damask/_result.py b/python/damask/_result.py index 5bed4347f..45a7fafc6 100644 --- a/python/damask/_result.py +++ b/python/damask/_result.py @@ -167,9 +167,7 @@ class Result: def allow_modification(self): - print(util.bcolors().WARNING+util.bcolors().BOLD+ - 'Warning: Modification of existing datasets allowed!'+ - util.bcolors().ENDC) + print(util.warn('Warning: Modification of existing datasets allowed!')) self._allow_modification = True def disallow_modification(self): diff --git a/python/damask/util.py b/python/damask/util.py index d1b3ed842..49e85c9f5 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -17,7 +17,7 @@ __all__=[ 'srepr', 'croak', 'report', - 'emph','deemph','delete','strikeout', + 'emph','deemph','warn','strikeout', 'execute', 'show_progress', 'scale_to_coprime', @@ -74,7 +74,7 @@ def croak(what, newline = True): def report(who = None, what = None): """ - Reports script and file name. + Report script and file name. DEPRECATED @@ -86,16 +86,13 @@ def emph(what): """Formats string with emphasis.""" return bcolors.BOLD+srepr(what)+bcolors.ENDC - def deemph(what): """Formats string with deemphasis.""" return bcolors.DIM+srepr(what)+bcolors.ENDC - -def delete(what): - """Formats string as deleted.""" - return bcolors.DIM+srepr(what)+bcolors.ENDC - +def warn(what): + """Formats string for warning.""" + return bcolors.WARNING+emph(what)+bcolors.ENDC def strikeout(what): """Formats string as strikeout.""" @@ -302,20 +299,38 @@ def shapeblender(a,b): return a + b[i:] -def extend_docstring(general): - """Decorator: Append Orientation parameter documentation to function's docstring.""" - def _decorator(func): - func.__doc__ += general - return func - return _decorator +def extend_docstring(extra_docstring): + """ + Decorator: Append to function's docstring. + + Parameters + ---------- + extra_docstring : str + Docstring to append. + + """ + def _decorator(func): + func.__doc__ += extra_docstring + return func + return _decorator -def extended_docstring(f,general): - """Decorator: Combine Orientation parameter documentation with another function's docstring.""" - def _decorator(func): - func.__doc__ = f.__doc__ + general - return func - return _decorator +def extended_docstring(f,extra_docstring): + """ + Decorator: Combine another function's docstring with a given docstring. + + Parameters + ---------- + f : function + Function of which the docstring is taken. + extra_docstring : str + Docstring to append. + + """ + def _decorator(func): + func.__doc__ = f.__doc__ + extra_docstring + return func + return _decorator #################################################################################################### diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 9361008e5..20eb49b7e 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -772,15 +772,15 @@ class TestRotation: Rotation.from_random(shape) def test_equal(self): - r = Rotation.from_random(seed=0) + r = Rotation.from_random() assert r == r def test_unequal(self): - r = Rotation.from_random(seed=0) + r = Rotation.from_random() assert not (r != r) def test_inversion(self): - r = Rotation.from_random(seed=0) + r = Rotation.from_random() assert r == ~~r @pytest.mark.parametrize('shape',[None,1,(1,),(4,2),(1,1,1)]) diff --git a/python/tests/test_util.py b/python/tests/test_util.py index 9906689e9..eb1084b09 100644 --- a/python/tests/test_util.py +++ b/python/tests/test_util.py @@ -98,6 +98,6 @@ class TestUtil: def test_shapeblender(self,a,b,answer): assert util.shapeblender(a,b) == answer - @pytest.mark.parametrize('style',[util.emph,util.deemph,util.delete,util.strikeout]) + @pytest.mark.parametrize('style',[util.emph,util.deemph,util.warn,util.strikeout]) def test_decorate(self,style): assert 'DAMASK' in style('DAMASK') From 15af12bbb4c3696b344415b6d75aca2cbf51b3b6 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 11:38:26 +0100 Subject: [PATCH 08/15] __ne__ is automatically set to !__eq__, __neq__ has no special meaning --- python/damask/_rotation.py | 16 ---------------- python/tests/test_Rotation.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 145b27884..a77c40ca5 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -107,22 +107,6 @@ class Rotation: and np.allclose(self.quaternion,other.quaternion) - def __neq__(self,other): - """ - Not Equal to other. - - Equality is determined taking limited floating point precision into - account. See numpy.allclose for details. - - Parameters - ---------- - other : Rotation - Rotation to check for inequality. - - """ - return not self.__eq__(other) - - @property def shape(self): return self.quaternion.shape[:-1] diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 20eb49b7e..fe30726e3 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -769,15 +769,16 @@ class TestRotation: @pytest.mark.parametrize('shape',[None,1,(4,4)]) def test_random(self,shape): - Rotation.from_random(shape) + r = Rotation.from_random(shape) + if shape is None: + assert r.shape == () + elif shape == 1: + assert r.shape == (1,) + else: + assert r.shape == shape def test_equal(self): - r = Rotation.from_random() - assert r == r - - def test_unequal(self): - r = Rotation.from_random() - assert not (r != r) + assert Rotation.from_random(seed=1) == Rotation.from_random(seed=1) def test_inversion(self): r = Rotation.from_random() From 01d84c1477b93e3de7a52c674b67027d4a027e9d Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 11:49:52 +0100 Subject: [PATCH 09/15] testing also functions with graphical output --- python/tests/conftest.py | 2 +- python/tests/test_Colormap.py | 2 +- python/tests/test_Geom.py | 2 +- python/tests/test_VTK.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 9c58eedd9..5f5de52ba 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -26,7 +26,7 @@ def patch_datetime_now(monkeypatch): monkeypatch.setattr(datetime, 'datetime', mydatetime) @pytest.fixture -def execution_stamp(monkeypatch): +def patch_execution_stamp(monkeypatch): """Set damask.util.execution_stamp for reproducible tests results.""" def execution_stamp(class_name,function_name=None): _function_name = '' if function_name is None else f'.{function_name}' diff --git a/python/tests/test_Colormap.py b/python/tests/test_Colormap.py index 71d896016..9b317b451 100644 --- a/python/tests/test_Colormap.py +++ b/python/tests/test_Colormap.py @@ -17,7 +17,7 @@ def reference_dir(reference_dir_base): class TestColormap: @pytest.fixture(autouse=True) - def _execution_stamp(self, execution_stamp): + def _patch_execution_stamp(self, patch_execution_stamp): print('patched damask.util.execution_stamp') def test_conversion(self): diff --git a/python/tests/test_Geom.py b/python/tests/test_Geom.py index 09c43fe7e..5fd4d3b1c 100644 --- a/python/tests/test_Geom.py +++ b/python/tests/test_Geom.py @@ -34,7 +34,7 @@ def reference_dir(reference_dir_base): class TestGeom: @pytest.fixture(autouse=True) - def _execution_stamp(self, execution_stamp): + def _patch_execution_stamp(self, patch_execution_stamp): print('patched damask.util.execution_stamp') def test_diff_equal(self,default): diff --git a/python/tests/test_VTK.py b/python/tests/test_VTK.py index 3b14981ae..53fbaa2f4 100644 --- a/python/tests/test_VTK.py +++ b/python/tests/test_VTK.py @@ -23,7 +23,7 @@ def default(): class TestVTK: @pytest.fixture(autouse=True) - def _execution_stamp(self, execution_stamp): + def _patch_execution_stamp(self, patch_execution_stamp): print('patched damask.util.execution_stamp') def test_rectilinearGrid(self,tmp_path): From 1541ac0add6f58cb229ad8804d2c437c38778a6a Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 12:00:26 +0100 Subject: [PATCH 10/15] more intuitive use --- python/damask/_colormap.py | 23 +++++++++++------------ python/damask/_geom.py | 3 +-- python/tests/conftest.py | 12 +++++++++++- python/tests/test_Colormap.py | 3 +++ 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/python/damask/_colormap.py b/python/damask/_colormap.py index 6803f4c60..87135caf9 100644 --- a/python/damask/_colormap.py +++ b/python/damask/_colormap.py @@ -36,6 +36,17 @@ class Colormap(mpl.colors.ListedColormap): """Return inverted colormap.""" return self.reversed() + def __repr__(self): + """Show colormap as matplotlib figure.""" + fig = plt.figure(figsize=(5,.5)) + ax1 = fig.add_axes([0, 0, 1, 1]) + ax1.set_axis_off() + ax1.imshow(np.linspace(0,1,self.N).reshape(1,-1), + aspect='auto', cmap=self, interpolation='nearest') + plt.show(block = False) + return self.name + + @staticmethod def from_range(low,high,name='DAMASK colormap',N=256,model='rgb'): """ @@ -203,18 +214,6 @@ class Colormap(mpl.colors.ListedColormap): mode='RGBA') - def show(self,aspect=10,vertical=False): - """Show colormap as matplotlib figure.""" - fig = plt.figure(figsize=(5/aspect,5) if vertical else (5,5/aspect)) - ax1 = fig.add_axes([0, 0, 1, 1]) - ax1.set_axis_off() - ax1.imshow(np.linspace(1 if vertical else 0, - 0 if vertical else 1, - self.N).reshape((-1,1) if vertical else (1,-1)), - aspect='auto', cmap=self, interpolation='nearest') - plt.show() - - def reversed(self,name=None): """ Make a reversed instance of the colormap. diff --git a/python/damask/_geom.py b/python/damask/_geom.py index d364ad6e8..f75a48fff 100644 --- a/python/damask/_geom.py +++ b/python/damask/_geom.py @@ -555,8 +555,7 @@ class Geom: def show(self): """Show on screen.""" - v = VTK.from_rectilinear_grid(self.grid,self.size,self.origin) - v.show() + VTK.from_rectilinear_grid(self.grid,self.size,self.origin).show() def add_primitive(self,dimension,center,exponent, diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 5f5de52ba..f402c4614 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,8 +1,13 @@ from pathlib import Path import datetime +import os import numpy as np import pytest +import matplotlib as mpl +if os.name == 'posix' and 'DISPLAY' not in os.environ: + mpl.use('Agg') +import matplotlib.pyplot as plt import damask @@ -13,7 +18,6 @@ def patch_damask_version(monkeypatch): """Set damask.version for reproducible tests results.""" monkeypatch.setattr(damask, 'version', patched_version) - patched_date = datetime.datetime(2019, 11, 2, 11, 58, 0) @pytest.fixture def patch_datetime_now(monkeypatch): @@ -34,6 +38,12 @@ def patch_execution_stamp(monkeypatch): monkeypatch.setattr(damask.util, 'execution_stamp', execution_stamp) +@pytest.fixture +def patch_plt_show(monkeypatch): + def _None(block=None): + pass + monkeypatch.setattr(plt, 'show', _None, raising=True) + def pytest_addoption(parser): parser.addoption("--update", diff --git a/python/tests/test_Colormap.py b/python/tests/test_Colormap.py index 9b317b451..279f7eea9 100644 --- a/python/tests/test_Colormap.py +++ b/python/tests/test_Colormap.py @@ -20,6 +20,9 @@ class TestColormap: def _patch_execution_stamp(self, patch_execution_stamp): print('patched damask.util.execution_stamp') + def test_repr(self,patch_plt_show): + print(Colormap.from_predefined('stress')) + def test_conversion(self): specials = np.array([[0.,0.,0.], [1.,0.,0.], From fd8743af5e2d9d457f68b037b32c3df0a4b3a51a Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 15 Nov 2020 13:06:26 +0100 Subject: [PATCH 11/15] clearly distinguish seeds for tessellation and seeds for RNG --- python/damask/_orientation.py | 1 + python/damask/_rotation.py | 24 ++++++++++++------------ python/damask/seeds.py | 12 ++++++------ python/damask/util.py | 30 ++++++++++++++++++++++++++---- python/tests/conftest.py | 6 ++++++ python/tests/test_Orientation.py | 20 ++++++++++---------- python/tests/test_Rotation.py | 2 +- 7 files changed, 62 insertions(+), 33 deletions(-) diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index aa15879bf..e60459489 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -1233,6 +1233,7 @@ class Orientation(Rotation): array([[ 0.000, 0.000, 0.000], [ 0.577, -0.000, 0.816], [ 0.000, 0.000, 0.000]]) + """ d = self.to_frame(uvw=self.kinematics[mode]['direction'],with_symmetry=False) p = self.to_frame(hkl=self.kinematics[mode]['plane'] ,with_symmetry=False) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index a77c40ca5..60dd055c6 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -682,7 +682,7 @@ class Rotation: @staticmethod def from_random(shape = None, - seed = None, + rng_seed = None, **kwargs): """ Draw random rotation. @@ -694,12 +694,12 @@ class Rotation: shape : tuple of ints, optional Shape of the sample. Defaults to None which gives a single rotation - seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None. If None, then fresh, unpredictable entropy will be pulled from the OS. """ - rng = np.random.default_rng(seed) + 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)) A = np.sqrt(r[...,2]) @@ -718,7 +718,7 @@ class Rotation: N = 500, degrees = True, fractions = True, - seed = None, + rng_seed = None, **kwargs): """ Sample discrete values from a binned ODF. @@ -737,7 +737,7 @@ class Rotation: fractions : boolean, optional ODF values correspond to volume fractions, not probability density. Defaults to True. - seed: {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed: {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. @@ -768,7 +768,7 @@ class Rotation: dg = 1.0 if fractions else _dg(Eulers,degrees) dV_V = dg * np.maximum(0.0,weights.squeeze()) - return Rotation.from_Eulers(Eulers[util.hybrid_IA(dV_V,N,seed)],degrees) + return Rotation.from_Eulers(Eulers[util.hybrid_IA(dV_V,N,rng_seed)],degrees) @staticmethod @@ -776,7 +776,7 @@ class Rotation: sigma, N = 500, degrees = True, - seed = None, + rng_seed = None, **kwargs): """ Calculate set of rotations with Gaussian distribution around center. @@ -791,12 +791,12 @@ class Rotation: Number of samples, defaults to 500. degrees : boolean, optional sigma is given in degrees. - seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. """ - rng = np.random.default_rng(seed) + rng = np.random.default_rng(rng_seed) sigma = np.radians(sigma) if degrees else sigma u,Theta = (rng.random((N,2)) * 2.0 * np.array([1,np.pi]) - np.array([1.0, 0])).T omega = abs(rng.normal(scale=sigma,size=N)) @@ -813,7 +813,7 @@ class Rotation: sigma = 0.0, N = 500, degrees = True, - seed = None, + rng_seed = None, **kwargs): """ Calculate set of rotations with Gaussian distribution around direction. @@ -831,12 +831,12 @@ class Rotation: Number of samples, defaults to 500. degrees : boolean, optional sigma, alpha, and beta are given in degrees. - seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None, i.e. unpredictable entropy will be pulled from the OS. """ - rng = np.random.default_rng(seed) + rng = np.random.default_rng(rng_seed) sigma_,alpha_,beta_ = map(np.radians,(sigma,alpha,beta)) if degrees else (sigma,alpha,beta) d_cr = np.array([np.sin(alpha_[0])*np.cos(alpha_[1]), np.sin(alpha_[0])*np.sin(alpha_[1]), np.cos(alpha_[0])]) diff --git a/python/damask/seeds.py b/python/damask/seeds.py index 9aab953d0..717fc620b 100644 --- a/python/damask/seeds.py +++ b/python/damask/seeds.py @@ -5,7 +5,7 @@ from . import util from . import grid_filters -def from_random(size,N_seeds,grid=None,seed=None): +def from_random(size,N_seeds,grid=None,rng_seed=None): """ Random seeding in space. @@ -18,12 +18,12 @@ def from_random(size,N_seeds,grid=None,seed=None): grid : numpy.ndarray of shape (3), optional. If given, ensures that all seeds initiate one grain if using a standard Voronoi tessellation. - seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None. If None, then fresh, unpredictable entropy will be pulled from the OS. """ - rng = _np.random.default_rng(seed) + rng = _np.random.default_rng(rng_seed) if grid is None: coords = rng.random((N_seeds,3)) * size else: @@ -34,7 +34,7 @@ def from_random(size,N_seeds,grid=None,seed=None): return coords -def from_Poisson_disc(size,N_seeds,N_candidates,distance,periodic=True,seed=None): +def from_Poisson_disc(size,N_seeds,N_candidates,distance,periodic=True,rng_seed=None): """ Seeding in space according to a Poisson disc distribution. @@ -50,12 +50,12 @@ def from_Poisson_disc(size,N_seeds,N_candidates,distance,periodic=True,seed=None Minimum acceptable distance to other seeds. periodic : boolean, optional Calculate minimum distance for periodically repeated grid. - seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional A seed to initialize the BitGenerator. Defaults to None. If None, then fresh, unpredictable entropy will be pulled from the OS. """ - rng = _np.random.default_rng(seed) + rng = _np.random.default_rng(rng_seed) coords = _np.empty((N_seeds,3)) coords[0] = rng.random(3) * size diff --git a/python/damask/util.py b/python/damask/util.py index 49e85c9f5..fb122bd11 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -42,7 +42,7 @@ def srepr(arg,glue = '\n'): arg : iterable Items to join. glue : str, optional - Defaults to \n. + Glue used for joining operation. Defaults to \n. """ if (not hasattr(arg, "strip") and @@ -163,7 +163,15 @@ def show_progress(iterable,N_iter=None,prefix='',bar_length=50): def scale_to_coprime(v): - """Scale vector to co-prime (relatively prime) integers.""" + """ + Scale vector to co-prime (relatively prime) integers. + + Parameters + ---------- + v : numpy.ndarray of shape (:) + Vector to scale. + + """ MAX_DENOMINATOR = 1000000 def get_square_denominator(x): @@ -214,7 +222,21 @@ def execution_stamp(class_name,function_name=None): return f'damask.{class_name}{_function_name} v{version} ({now})' -def hybrid_IA(dist,N,seed=None): +def hybrid_IA(dist,N,rng_seed=None): + """ + Hybrid integer approximation. + + Parameters + ---------- + dist : numpy.ndarray + Distribution to be approximated + N : int + Number of samples to draw. + rng_seed : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator}, optional + A seed to initialize the BitGenerator. Defaults to None. + If None, then fresh, unpredictable entropy will be pulled from the OS. + + """ N_opt_samples,N_inv_samples = (max(np.count_nonzero(dist),N),0) # random subsampling if too little samples requested scale_,scale,inc_factor = (0.0,float(N_opt_samples),1.0) @@ -225,7 +247,7 @@ def hybrid_IA(dist,N,seed=None): if N_inv_samples < N_opt_samples else \ (scale_,0.5*(scale_ + scale), 1.0) - return np.repeat(np.arange(len(dist)),repeats)[np.random.default_rng(seed).permutation(N_inv_samples)[:N]] + return np.repeat(np.arange(len(dist)),repeats)[np.random.default_rng(rng_seed).permutation(N_inv_samples)[:N]] def shapeshifter(fro,to,mode='left',keep_ones=False): diff --git a/python/tests/conftest.py b/python/tests/conftest.py index f402c4614..9410ba15a 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -18,6 +18,7 @@ def patch_damask_version(monkeypatch): """Set damask.version for reproducible tests results.""" monkeypatch.setattr(damask, 'version', patched_version) + patched_date = datetime.datetime(2019, 11, 2, 11, 58, 0) @pytest.fixture def patch_datetime_now(monkeypatch): @@ -29,6 +30,7 @@ def patch_datetime_now(monkeypatch): monkeypatch.setattr(datetime, 'datetime', mydatetime) + @pytest.fixture def patch_execution_stamp(monkeypatch): """Set damask.util.execution_stamp for reproducible tests results.""" @@ -38,6 +40,7 @@ def patch_execution_stamp(monkeypatch): monkeypatch.setattr(damask.util, 'execution_stamp', execution_stamp) + @pytest.fixture def patch_plt_show(monkeypatch): def _None(block=None): @@ -50,16 +53,19 @@ def pytest_addoption(parser): action="store_true", default=False) + @pytest.fixture def update(request): """Store current results as new reference results.""" return request.config.getoption("--update") + @pytest.fixture def reference_dir_base(): """Directory containing reference results.""" return Path(__file__).parent/'reference' + @pytest.fixture def set_of_quaternions(): """A set of n random rotations.""" diff --git a/python/tests/test_Orientation.py b/python/tests/test_Orientation.py index fe35bda14..3e6aad134 100644 --- a/python/tests/test_Orientation.py +++ b/python/tests/test_Orientation.py @@ -125,9 +125,9 @@ class TestOrientation: def test_from_fiber_component(self): r = Rotation.from_fiber_component(alpha=np.zeros(2),beta=np.zeros(2), - sigma=0.0,N=1,seed=0) + sigma=0.0,N=1,rng_seed=0) assert np.all(Orientation.from_fiber_component(alpha=np.zeros(2),beta=np.zeros(2), - sigma=0.0,N=1,seed=0,lattice='triclinic').quaternion + sigma=0.0,N=1,rng_seed=0,lattice='triclinic').quaternion == r.quaternion) @pytest.mark.parametrize('kwargs',[ @@ -175,8 +175,8 @@ class TestOrientation: @pytest.mark.parametrize('lattice',Orientation.crystal_families) @pytest.mark.parametrize('N',[1,8,32]) def test_disorientation(self,lattice,N): - o = Orientation.from_random(lattice=lattice,shape=N,seed=0) - p = Orientation.from_random(lattice=lattice,shape=N,seed=1) + o = Orientation.from_random(lattice=lattice,shape=N) + p = Orientation.from_random(lattice=lattice,shape=N) d,ops = o.disorientation(p,return_operators=True) @@ -198,8 +198,8 @@ class TestOrientation: (None,None), ]) def test_disorientation_blending(self,lattice,a,b): - o = Orientation.from_random(lattice=lattice,shape=a,seed=0) - p = Orientation.from_random(lattice=lattice,shape=b,seed=1) + o = Orientation.from_random(lattice=lattice,shape=a) + p = Orientation.from_random(lattice=lattice,shape=b) blend = util.shapeblender(o.shape,p.shape) for loc in np.random.randint(0,blend,(10,len(blend))): assert o[tuple(loc[:len(o.shape)])].disorientation(p[tuple(loc[-len(p.shape):])]) \ @@ -214,7 +214,7 @@ class TestOrientation: @pytest.mark.parametrize('lattice',Orientation.crystal_families) @pytest.mark.parametrize('shape',[(1),(2,3),(4,3,2)]) def test_reduced_vectorization(self,lattice,shape): - o = Orientation.from_random(lattice=lattice,shape=shape,seed=0) + o = Orientation.from_random(lattice=lattice,shape=shape) for r, theO in zip(o.reduced.flatten(),o.flatten()): assert r == theO.reduced @@ -223,7 +223,7 @@ class TestOrientation: @pytest.mark.parametrize('vector',np.array([[1,0,0],[1,2,3],[-1,1,-1]])) @pytest.mark.parametrize('proper',[True,False]) def test_to_SST_vectorization(self,lattice,shape,vector,proper): - o = Orientation.from_random(lattice=lattice,shape=shape,seed=0) + o = Orientation.from_random(lattice=lattice,shape=shape) for r, theO in zip(o.to_SST(vector=vector,proper=proper).reshape((-1,3)),o.flatten()): assert np.allclose(r,theO.to_SST(vector=vector,proper=proper)) @@ -232,7 +232,7 @@ class TestOrientation: @pytest.mark.parametrize('vector',np.array([[1,0,0],[1,2,3],[-1,1,-1]])) @pytest.mark.parametrize('proper',[True,False]) def test_IPF_color_vectorization(self,lattice,shape,vector,proper): - o = Orientation.from_random(lattice=lattice,shape=shape,seed=0) + o = Orientation.from_random(lattice=lattice,shape=shape) poles = o.to_SST(vector=vector,proper=proper) for r, theO in zip(o.IPF_color(poles,proper=proper).reshape((-1,3)),o.flatten()): assert np.allclose(r,theO.IPF_color(theO.to_SST(vector=vector,proper=proper),proper=proper)) @@ -245,7 +245,7 @@ class TestOrientation: (None,(3,)), ]) def test_to_SST_blending(self,lattice,a,b): - o = Orientation.from_random(lattice=lattice,shape=a,seed=0) + o = Orientation.from_random(lattice=lattice,shape=a) v = np.random.random(b+(3,)) blend = util.shapeblender(o.shape,b) for loc in np.random.randint(0,blend,(10,len(blend))): diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index fe30726e3..fe90e2aea 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -778,7 +778,7 @@ class TestRotation: assert r.shape == shape def test_equal(self): - assert Rotation.from_random(seed=1) == Rotation.from_random(seed=1) + assert Rotation.from_random(rng_seed=1) == Rotation.from_random(rng_seed=1) def test_inversion(self): r = Rotation.from_random() From de20e6b35dbdbc6e2d9fc4bb9696ddb795f58559 Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Mon, 16 Nov 2020 10:23:01 -0500 Subject: [PATCH 12/15] code shortening; list_predefined() --> property "predefined" --- python/damask/_colormap.py | 142 ++++++++++++++-------------------- python/tests/test_Colormap.py | 4 +- 2 files changed, 61 insertions(+), 85 deletions(-) diff --git a/python/damask/_colormap.py b/python/damask/_colormap.py index 87135caf9..b148b8b2e 100644 --- a/python/damask/_colormap.py +++ b/python/damask/_colormap.py @@ -22,6 +22,16 @@ _ref_white = np.array([.95047, 1.00000, 1.08883]) # - support NaN color (paraview) class Colormap(mpl.colors.ListedColormap): + """ + References + ---------- + [1] DAMASK colormap theory + https://www.kennethmoreland.com/color-maps/ColorMapsExpanded.pdf + [2] DAMASK colormaps first use + https://doi.org/10.1016/j.ijplas.2012.09.012 + [3] Matplotlib colormaps overview + https://matplotlib.org/tutorials/colors/colormaps.html + """ def __add__(self,other): """Concatenate colormaps.""" @@ -137,40 +147,16 @@ class Colormap(mpl.colors.ListedColormap): """ # matplotlib presets - for cat in Colormap._predefined_mpl: - for n in cat[1]: - if n == name: - colormap = cm.__dict__[name] - if isinstance(colormap,mpl.colors.LinearSegmentedColormap): - return Colormap(np.array(list(map(colormap,np.linspace(0,1,N)))),name=name) - else: - return Colormap(np.array(colormap.colors),name=name) - - # DAMASK presets - definition = Colormap._predefined_DAMASK[name] - return Colormap.from_range(definition['low'],definition['high'],name,N) - - - @staticmethod - def list_predefined(): - """ - List predefined colormaps by category. - - References - ---------- - [1] DAMASK colormap theory - https://www.kennethmoreland.com/color-maps/ColorMapsExpanded.pdf - [2] DAMASK colormaps first use - https://doi.org/10.1016/j.ijplas.2012.09.012 - [3] Matplotlib colormaps overview - https://matplotlib.org/tutorials/colors/colormaps.html - - """ - print('DAMASK colormaps') - print(' '+', '.join(Colormap._predefined_DAMASK.keys())) - for cat in Colormap._predefined_mpl: - print(f'{cat[0]}') - print(' '+', '.join(cat[1])) + try: + colormap = cm.__dict__[name] + return Colormap(np.array(list(map(colormap,np.linspace(0,1,N))) + if isinstance(colormap,mpl.colors.LinearSegmentedColormap) else + colormap.colors), + name=name) + except: + # DAMASK presets + definition = Colormap._predefined_DAMASK[name] + return Colormap.from_range(definition['low'],definition['high'],name,N) def shade(self,field,bounds=None,gap=None): @@ -179,9 +165,9 @@ class Colormap(mpl.colors.ListedColormap): Parameters ---------- - field : numpy.array of shape(:,:) + field : numpy.array of shape (:,:) Data to be shaded. - bounds : iterable of len(2), optional + bounds : iterable of len (2), optional Colormap value range (low,high). gap : field.dtype, optional Transparent value. NaN will always be rendered transparent. @@ -234,7 +220,6 @@ class Colormap(mpl.colors.ListedColormap): return Colormap(np.array(rev.colors),rev.name[:-4] if rev.name.endswith('_r_r') else rev.name) - def save_paraview(self,fname=None): """ Write colormap to JSON file for Paraview. @@ -246,13 +231,13 @@ class Colormap(mpl.colors.ListedColormap): consist of the name of the colormap and extension '.json'. """ - if fname is not None: + if fname is None: + fhandle = None + else: try: fhandle = open(fname,'w') except TypeError: fhandle = fname - else: - fhandle = None colors = [] for i,c in enumerate(np.round(self.colors,6).tolist()): @@ -265,11 +250,9 @@ class Colormap(mpl.colors.ListedColormap): 'DefaultMap':True, 'RGBPoints':colors }] - if fhandle is None: - with open(self.name.replace(' ','_')+'.json', 'w') as f: - json.dump(out, f,indent=4) - else: - json.dump(out,fhandle,indent=4) + + with open(self.name.replace(' ','_')+'.json', 'w') if fhandle is None else fhandle as f: + json.dump(out, f,indent=4) def save_ASCII(self,fname=None): @@ -283,22 +266,19 @@ class Colormap(mpl.colors.ListedColormap): consist of the name of the colormap and extension '.txt'. """ - if fname is not None: + if fname is None: + fhandle = None + else: try: fhandle = open(fname,'w') except TypeError: fhandle = fname - else: - fhandle = None labels = {'RGBA':4} if self.colors.shape[1] == 4 else {'RGB': 3} t = Table(self.colors,labels,f'Creator: {util.execution_stamp("Colormap")}') - if fhandle is None: - with open(self.name.replace(' ','_')+'.txt', 'w') as f: - t.save(f) - else: - t.save(fhandle) + with open(self.name.replace(' ','_')+'.txt', 'w') if fhandle is None else fhandle as f: + t.save(f) def save_GOM(self,fname=None): @@ -312,24 +292,21 @@ class Colormap(mpl.colors.ListedColormap): consist of the name of the colormap and extension '.legend'. """ - if fname is not None: + if fname is None: + fhandle = None + else: try: fhandle = open(fname,'w') except TypeError: fhandle = fname - else: - fhandle = None # ToDo: test in GOM GOM_str = '1 1 {name} 9 {name} '.format(name=self.name.replace(" ","_")) \ + '0 1 0 3 0 0 -1 9 \\ 0 0 0 255 255 255 0 0 255 ' \ + f'30 NO_UNIT 1 1 64 64 64 255 1 0 0 0 0 0 0 3 0 {len(self.colors)}' \ + ' '.join([f' 0 {c[0]} {c[1]} {c[2]} 255 1' for c in reversed((self.colors*255).astype(int))]) \ + '\n' - if fhandle is None: - with open(self.name.replace(' ','_')+'.legend', 'w') as f: - f.write(GOM_str) - else: - fhandle.write(GOM_str) + with open(self.name.replace(' ','_')+'.legend', 'w') if fhandle is None else fhandle as f: + f.write(GOM_str) def save_gmsh(self,fname=None): @@ -343,22 +320,19 @@ class Colormap(mpl.colors.ListedColormap): consist of the name of the colormap and extension '.msh'. """ - if fname is not None: + if fname is None: + fhandle = None + else: try: fhandle = open(fname,'w') except TypeError: fhandle = fname - else: - fhandle = None # ToDo: test in gmsh gmsh_str = 'View.ColorTable = {\n' \ +'\n'.join([f'{c[0]},{c[1]},{c[2]},' for c in self.colors[:,:3]*255]) \ +'\n}\n' - if fhandle is None: - with open(self.name.replace(' ','_')+'.msh', 'w') as f: - f.write(gmsh_str) - else: - fhandle.write(gmsh_str) + with open(self.name.replace(' ','_')+'.msh', 'w') if fhandle is None else fhandle as f: + f.write(gmsh_str) @staticmethod @@ -386,7 +360,6 @@ class Colormap(mpl.colors.ListedColormap): if msh_sat[2] < - np.pi/3.0: hSpin *= -1.0 return msh_sat[2] + hSpin - lo = np.array(low) hi = np.array(high) @@ -406,28 +379,28 @@ class Colormap(mpl.colors.ListedColormap): return (1.0 - frac) * lo + frac * hi - _predefined_mpl= [('Perceptually Uniform Sequential', [ - 'viridis', 'plasma', 'inferno', 'magma', 'cividis']), - ('Sequential', [ + _predefined_mpl= {'Perceptually Uniform Sequential': [ + 'viridis', 'plasma', 'inferno', 'magma', 'cividis'], + 'Sequential': [ 'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']), - ('Sequential (2)', [ + 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn'], + 'Sequential (2)': [ 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', - 'hot', 'afmhot', 'gist_heat', 'copper']), - ('Diverging', [ + 'hot', 'afmhot', 'gist_heat', 'copper'], + 'Diverging': [ 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', - 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']), - ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), - ('Qualitative', [ + 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic'], + 'Cyclic': ['twilight', 'twilight_shifted', 'hsv'], + 'Qualitative': [ 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', - 'tab10', 'tab20', 'tab20b', 'tab20c']), - ('Miscellaneous', [ + 'tab10', 'tab20', 'tab20b', 'tab20c'], + 'Miscellaneous': [ 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', - 'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'])] + 'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar']} _predefined_DAMASK = {'orientation': {'low': [0.933334,0.878432,0.878431], 'high': [0.250980,0.007843,0.000000]}, @@ -436,6 +409,9 @@ class Colormap(mpl.colors.ListedColormap): 'stress': {'low': [0.878432,0.874511,0.949019], 'high': [0.000002,0.000000,0.286275]}} + predefined = dict(**{'DAMASK':list(_predefined_DAMASK)},**_predefined_mpl) + + @staticmethod def _hsv2rgb(hsv): """ diff --git a/python/tests/test_Colormap.py b/python/tests/test_Colormap.py index 279f7eea9..054158c22 100644 --- a/python/tests/test_Colormap.py +++ b/python/tests/test_Colormap.py @@ -141,8 +141,8 @@ class TestColormap: diff = ImageChops.difference(img_reference.convert('RGB'),img_current.convert('RGB')) assert not diff.getbbox() - def test_list(self): - Colormap.list_predefined() + def test_predefined(self): + assert (isinstance(Colormap.predefined,dict)) @pytest.mark.parametrize('format,ext',[('ASCII','.txt'), ('paraview','.json'), From fa25a1326488eac57664fb27c6b6ca19a8f2dc9f Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Mon, 16 Nov 2020 10:34:49 -0500 Subject: [PATCH 13/15] fixed typo --- python/tests/test_VTK.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tests/test_VTK.py b/python/tests/test_VTK.py index 53fbaa2f4..48dce4707 100644 --- a/python/tests/test_VTK.py +++ b/python/tests/test_VTK.py @@ -87,7 +87,7 @@ class TestVTK: def test_compress(self,tmp_path): points = np.random.rand(102,3) v = VTK.from_poly_data(points) - fname_c = tmp_path/'compreesed.vtp' + fname_c = tmp_path/'compressed.vtp' fname_p = tmp_path/'plain.vtp' v.save(fname_c,parallel=False,compress=False) v.save(fname_p,parallel=False,compress=True) From 5a53f8743b64d01b658d6f67fa6827f0335c4ab2 Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Mon, 16 Nov 2020 10:39:43 -0500 Subject: [PATCH 14/15] added to docstring --- python/damask/_colormap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/damask/_colormap.py b/python/damask/_colormap.py index b148b8b2e..8cd8a6aa9 100644 --- a/python/damask/_colormap.py +++ b/python/damask/_colormap.py @@ -23,6 +23,8 @@ _ref_white = np.array([.95047, 1.00000, 1.08883]) class Colormap(mpl.colors.ListedColormap): """ + Enhance matplotlib colormap functionality to be used within DAMASK. + References ---------- [1] DAMASK colormap theory @@ -31,6 +33,7 @@ class Colormap(mpl.colors.ListedColormap): https://doi.org/10.1016/j.ijplas.2012.09.012 [3] Matplotlib colormaps overview https://matplotlib.org/tutorials/colors/colormaps.html + """ def __add__(self,other): @@ -153,7 +156,7 @@ class Colormap(mpl.colors.ListedColormap): if isinstance(colormap,mpl.colors.LinearSegmentedColormap) else colormap.colors), name=name) - except: + except KeyError: # DAMASK presets definition = Colormap._predefined_DAMASK[name] return Colormap.from_range(definition['low'],definition['high'],name,N) From a8fec922d5a643d1ea205479434cd8368f300b56 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Mon, 16 Nov 2020 17:25:13 +0100 Subject: [PATCH 15/15] show name in title --- python/damask/_colormap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/damask/_colormap.py b/python/damask/_colormap.py index 8cd8a6aa9..aaa3fdc4a 100644 --- a/python/damask/_colormap.py +++ b/python/damask/_colormap.py @@ -51,7 +51,7 @@ class Colormap(mpl.colors.ListedColormap): def __repr__(self): """Show colormap as matplotlib figure.""" - fig = plt.figure(figsize=(5,.5)) + fig = plt.figure(self.name,figsize=(5,.5)) ax1 = fig.add_axes([0, 0, 1, 1]) ax1.set_axis_off() ax1.imshow(np.linspace(0,1,self.N).reshape(1,-1),