diff --git a/python/damask/_table.py b/python/damask/_table.py index ee64ba017..fb7e17ef8 100644 --- a/python/damask/_table.py +++ b/python/damask/_table.py @@ -27,20 +27,69 @@ class Table: self.comments = [] if comments_ is None else [c for c in comments_] self.data = pd.DataFrame(data=data) self.shapes = { k:(v,) if isinstance(v,(np.int64,np.int32,int)) else v for k,v in shapes.items() } - self._label_uniform() + self._relabel('uniform') + def __repr__(self): """Brief overview.""" - return '\n'.join(['# '+c for c in self.comments])+'\n'+self.data.__repr__() + self._relabel('shapes') + data_repr = self.data.__repr__() + self._relabel('uniform') + return '\n'.join(['# '+c for c in self.comments])+'\n'+data_repr + def __getitem__(self,item): - """Return slice according to item.""" - return self.__class__(data=self.data[item],shapes=self.shapes,comments=self.comments) + """ + Slice the Table according to item. + + Parameters + ---------- + item : row and/or column indexer + Slice to select from Table. + + Returns + ------- + slice : Table + Sliced part of the Table. + + Examples + -------- + >>> import damask + >>> import numpy as np + >>> tbl = damask.Table(data=np.arange(12).reshape((4,3)), + ... shapes=dict(colA=(1,),colB=(1,),colC=(1,))) + >>> tbl['colA','colB'] + colA colB + 0 0 1 + 1 3 4 + 2 6 7 + 3 9 10 + >>> tbl[::2,['colB','colA']] + colB colA + 0 1 0 + 2 7 6 + >>> tbl[1:2,'colB'] + colB + 1 4 + 2 7 + + """ + item = (item,slice(None,None,None)) if isinstance(item,slice) else \ + item if isinstance(item[0],slice) else \ + (slice(None,None,None),item) + sliced = self.data.loc[item] + cols = np.array(sliced.columns if isinstance(sliced,pd.core.frame.DataFrame) else [item[1]]) + _,idx = np.unique(cols,return_index=True) + return self.__class__(data=sliced, + shapes = {k:self.shapes[k] for k in cols[np.sort(idx)]}, + comments=self.comments) + def __len__(self): """Number of rows.""" return len(self.data) + def __copy__(self): """Create deep copy.""" return copy.deepcopy(self) @@ -48,21 +97,51 @@ class Table: copy = __copy__ - def _label_discrete(self): - """Label data individually, e.g. v v v ==> 1_v 2_v 3_v.""" + def _label(self,what,how): + """ + Expand labels according to data shape. + + Parameters + ---------- + what : str or list + Labels to expand. + how : str + Mode of labeling. + 'uniform' ==> v v v + 'shapes' ==> 3:v v v + 'linear' ==> 1_v 2_v 3_v + + """ + what = [what] if isinstance(what,str) else what labels = [] - for label,shape in self.shapes.items(): - size = int(np.prod(shape)) - labels += [('' if size == 1 else f'{i+1}_')+label for i in range(size)] - self.data.columns = labels + for label in what: + shape = self.shapes[label] + size = np.prod(shape,dtype=int) + if how == 'uniform': + labels += [label] * size + elif how == 'shapes': + labels += [('' if size == 1 or i>0 else f'{util.srepr(shape,"x")}:')+label for i in range(size)] + elif how == 'linear': + labels += [('' if size == 1 else f'{i+1}_')+label for i in range(size)] + else: + raise KeyError + return labels - def _label_uniform(self): - """Label data uniformly, e.g. 1_v 2_v 3_v ==> v v v.""" - labels = [] - for label,shape in self.shapes.items(): - labels += [label] * int(np.prod(shape)) - self.data.columns = labels + def _relabel(self,how): + """ + Modify labeling of data in-place. + + Parameters + ---------- + how : str + Mode of labeling. + 'uniform' ==> v v v + 'shapes' ==> 3:v v v + 'linear' ==> 1_v 2_v 3_v + + """ + self.data.columns = self._label(self.shapes,how) def _add_comment(self,label,shape,info): @@ -72,6 +151,62 @@ class Table: self.comments.append(f'{specific} / {general}') + def isclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Report where values are approximately equal to corresponding ones of other Table. + + Parameters + ---------- + other : Table + Table to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + mask : numpy.ndarray bool + Mask indicating where corresponding table values are close. + + """ + return np.isclose( self.data.to_numpy(), + other.data.to_numpy(), + rtol=rtol, + atol=atol, + equal_nan=equal_nan) + + + def allclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Test whether all values are approximately equal to corresponding ones of other Table. + + Parameters + ---------- + other : Table + Table to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + answer : bool + Whether corresponding values are close between both tables. + + """ + return np.allclose( self.data.to_numpy(), + other.data.to_numpy(), + rtol=rtol, + atol=atol, + equal_nan=equal_nan) + + @staticmethod def load(fname): """ @@ -130,12 +265,13 @@ class Table: return Table(data,shapes,comments) + @staticmethod def load_ang(fname): """ Load from ang file. - A valid TSL ang file needs to contains the following columns: + A valid TSL ang file has to have the following columns: * Euler angles (Bunge notation) in radians, 3 floats, label 'eu'. * Spatial position in meters, 2 floats, label 'pos'. * Image quality, 1 float, label 'IQ'. @@ -225,10 +361,12 @@ class Table: """ dup = self.copy() dup._add_comment(label,data.shape[1:],info) - - if re.match(r'[0-9]*?_',label): - idx,key = label.split('_',1) - iloc = dup.data.columns.get_loc(key).tolist().index(True) + int(idx) -1 + m = re.match(r'(.*)\[((\d+,)*(\d+))\]',label) + if m: + key = m.group(1) + idx = np.ravel_multi_index(tuple(map(int,m.group(2).split(","))), + self.shapes[key]) + iloc = dup.data.columns.get_loc(key).tolist().index(True) + idx dup.data.iloc[:,iloc] = data else: dup.data[label] = data.reshape(dup.data[label].shape) @@ -331,10 +469,18 @@ class Table: Updated table. """ + labels_ = [labels] if isinstance(labels,str) else labels.copy() + for i,l in enumerate(labels_): + m = re.match(r'(.*)\[((\d+,)*(\d+))\]',l) + if m: + idx = np.ravel_multi_index(tuple(map(int,m.group(2).split(','))), + self.shapes[m.group(1)]) + labels_[i] = f'{1+idx}_{m.group(1)}' + dup = self.copy() - dup._label_discrete() - dup.data.sort_values(labels,axis=0,inplace=True,ascending=ascending) - dup._label_uniform() + dup._relabel('linear') + dup.data.sort_values(labels_,axis=0,inplace=True,ascending=ascending) + dup._relabel('uniform') dup.comments.append(f'sorted {"ascending" if ascending else "descending"} by {labels}') return dup @@ -399,7 +545,7 @@ class Table: ---------- fname : file, str, or pathlib.Path Filename or file for writing. - legacy : Boolean, optional + legacy : bool, optional Write table in legacy style, indicating header lines by "N header" in contrast to using comment sign ('#') at beginning of lines. diff --git a/python/damask/_test.py b/python/damask/_test.py index f8fb24cca..455a92520 100644 --- a/python/damask/_test.py +++ b/python/damask/_test.py @@ -399,7 +399,7 @@ class Test: tables = [damask.Table.load(filename) for filename in files] for table in tables: - table._label_discrete() + table._relabel('linear') columns += [columns[0]]*(len(files)-len(columns)) # extend to same length as files columns = columns[:len(files)] # truncate to same length as files @@ -419,7 +419,7 @@ class Test: data = [] for table,labels in zip(tables,columns): - table._label_uniform() + table._relabel('uniform') data.append(np.hstack(list(table.get(label) for label in labels))) diff --git a/python/tests/test_ConfigMaterial.py b/python/tests/test_ConfigMaterial.py index 5ac0e546a..cf4a8ab9d 100644 --- a/python/tests/test_ConfigMaterial.py +++ b/python/tests/test_ConfigMaterial.py @@ -86,9 +86,12 @@ class TestConfigMaterial: def test_from_table(self): N = np.random.randint(3,10) - a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])),np.ones(N*2),np.zeros(N*2),np.ones(N*2),np.ones(N*2))).T - t = Table(a,{'varying':1,'constant':4}) - c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'4_constant'}) + a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])), + np.ones(N*2),np.zeros(N*2),np.ones(N*2),np.ones(N*2), + np.ones(N*2), + )).T + t = Table(a,{'varying':1,'constant':4,'ones':1}) + c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'}) assert len(c['material']) == N for i,m in enumerate(c['material']): assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [1,0,1,1]).all() diff --git a/python/tests/test_Grid.py b/python/tests/test_Grid.py index 7e94686ee..018ff8a30 100644 --- a/python/tests/test_Grid.py +++ b/python/tests/test_Grid.py @@ -407,7 +407,8 @@ class TestGrid: z=np.ones(cells.prod()) z[cells[:2].prod()*int(cells[2]/2):]=0 t = Table(np.column_stack((coords,z)),{'coords':3,'z':1}) - g = Grid.from_table(t,'coords',['1_coords','z']) + t = t.add('indicator',t.get('coords')[:,0]) + g = Grid.from_table(t,'coords',['indicator','z']) assert g.N_materials == g.cells[0]*2 and (g.material[:,:,-1]-g.material[:,:,0] == cells[0]).all() diff --git a/python/tests/test_Table.py b/python/tests/test_Table.py index 8f617aff5..286c176e6 100644 --- a/python/tests/test_Table.py +++ b/python/tests/test_Table.py @@ -36,13 +36,33 @@ class TestTable: d = default.get('F') assert np.allclose(d,1.0) and d.shape[1:] == (3,3) - def test_get_component(self,default): - d = default.get('5_F') - assert np.allclose(d,1.0) and d.shape[1:] == (1,) + def test_set(self,default): + d = default.set('F',np.zeros((5,3,3)),'set to zero').get('F') + assert np.allclose(d,0.0) and d.shape[1:] == (3,3) - @pytest.mark.parametrize('N',[10,40]) - def test_getitem(self,N): - assert len(Table(np.random.rand(N,1),{'X':1})[:N//2]) == N//2 + def test_set_component(self,default): + d = default.set('F[0,0]',np.zeros((5)),'set to zero').get('F') + assert np.allclose(d[...,0,0],0.0) and d.shape[1:] == (3,3) + + def test_labels(self,default): + assert default.labels == ['F','v','s'] + + def test_add(self,default): + d = np.random.random((5,9)) + assert np.allclose(d,default.add('nine',d,'random data').get('nine')) + + def test_isclose(self,default): + assert default.isclose(default).all() + + def test_allclose(self,default): + assert default.allclose(default) + + @pytest.mark.parametrize('N',[1,3,4]) + def test_slice(self,default,N): + assert len(default[:N]) == 1+N + assert len(default[:N,['F','s']]) == 1+N + assert default[N:].get('F').shape == (len(default)-N,3,3) + assert (default[:N,['v','s']].data == default['v','s'][:N].data).all().all() @pytest.mark.parametrize('mode',['str','path']) def test_write_read(self,default,tmp_path,mode): @@ -91,21 +111,6 @@ class TestTable: with open(ref_path/fname) as f: Table.load(f) - def test_set(self,default): - d = default.set('F',np.zeros((5,3,3)),'set to zero').get('F') - assert np.allclose(d,0.0) and d.shape[1:] == (3,3) - - def test_set_component(self,default): - d = default.set('1_F',np.zeros((5)),'set to zero').get('F') - assert np.allclose(d[...,0,0],0.0) and d.shape[1:] == (3,3) - - def test_labels(self,default): - assert default.labels == ['F','v','s'] - - def test_add(self,default): - d = np.random.random((5,9)) - assert np.allclose(d,default.add('nine',d,'random data').get('nine')) - def test_rename_equivalent(self): x = np.random.random((5,13)) t = Table(x,{'F':(3,3),'v':(3,),'s':(1,)},['random test data']) @@ -176,15 +181,15 @@ class TestTable: def test_sort_component(self): x = np.random.random((5,12)) t = Table(x,{'F':(3,3),'v':(3,)},['random test data']) - unsort = t.get('4_F') - sort = t.sort_by('4_F').get('4_F') + unsort = t.get('F')[:,1,0] + sort = t.sort_by('F[1,0]').get('F')[:,1,0] assert np.all(np.sort(unsort,0)==sort) def test_sort_revert(self): x = np.random.random((5,12)) t = Table(x,{'F':(3,3),'v':(3,)},['random test data']) - sort = t.sort_by('4_F',ascending=False).get('4_F') - assert np.all(np.sort(sort,0)==sort[::-1,:]) + sort = t.sort_by('F[1,0]',ascending=False).get('F')[:,1,0] + assert np.all(np.sort(sort,0)==sort[::-1]) def test_sort(self): t = Table(np.array([[0,1,],[2,1,]]), @@ -192,4 +197,4 @@ class TestTable: ['test data'])\ .add('s',np.array(['b','a']))\ .sort_by('s') - assert np.all(t.get('1_v') == np.array([2,0]).reshape(2,1)) + assert np.all(t.get('v')[:,0] == np.array([2,0]))