Merge branch '250-configmaterial-material_add-simplifications' into 'development'
several improvements to ConfigMaterial and Config Closes #250 See merge request damask/DAMASK!714
This commit is contained in:
commit
4ae3274ac4
|
@ -2,6 +2,7 @@ import copy
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
import abc
|
import abc
|
||||||
|
import platform
|
||||||
from typing import Optional, Union, Dict, Any, Type, TypeVar
|
from typing import Optional, Union, Dict, Any, Type, TypeVar
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -69,13 +70,30 @@ class Config(dict):
|
||||||
**kwargs: arbitray keyword-value pairs, optional
|
**kwargs: arbitray keyword-value pairs, optional
|
||||||
Top level entries of the configuration.
|
Top level entries of the configuration.
|
||||||
|
|
||||||
"""
|
Notes
|
||||||
if isinstance(config,str):
|
-----
|
||||||
kwargs.update(yaml.load(config, Loader=SafeLoader))
|
Values given as keyword-value pairs take precedence
|
||||||
elif isinstance(config,dict):
|
over entries with the same keyword in 'config'.
|
||||||
kwargs.update(config)
|
|
||||||
|
"""
|
||||||
|
if int(platform.python_version_tuple()[1]) >= 9:
|
||||||
|
if isinstance(config,str):
|
||||||
|
kwargs = yaml.load(config, Loader=SafeLoader) | kwargs
|
||||||
|
elif isinstance(config,dict):
|
||||||
|
kwargs = config | kwargs # type: ignore
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
else:
|
||||||
|
if isinstance(config,str):
|
||||||
|
c = yaml.load(config, Loader=SafeLoader)
|
||||||
|
elif isinstance(config,dict):
|
||||||
|
c = config.copy()
|
||||||
|
else:
|
||||||
|
c = {}
|
||||||
|
c.update(kwargs)
|
||||||
|
|
||||||
|
super().__init__(**c)
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, Union, Sequence, Dict, Any, Collection
|
from typing import Optional, Union, Sequence, Dict, Any, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import h5py
|
import h5py
|
||||||
|
@ -8,6 +8,7 @@ from . import Config
|
||||||
from . import Rotation
|
from . import Rotation
|
||||||
from . import Orientation
|
from . import Orientation
|
||||||
from . import util
|
from . import util
|
||||||
|
from . import tensor
|
||||||
from . import Table
|
from . import Table
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,8 +24,10 @@ class ConfigMaterial(Config):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[Union[str,Dict[str,Any]]] = None,*,
|
||||||
**kwargs):
|
homogenization: Optional[Dict[str,Dict]] = None,
|
||||||
|
phase: Optional[Dict[str,Dict]] = None,
|
||||||
|
material: Optional[List[Dict[str,Any]]] = None):
|
||||||
"""
|
"""
|
||||||
New material configuration.
|
New material configuration.
|
||||||
|
|
||||||
|
@ -32,16 +35,23 @@ class ConfigMaterial(Config):
|
||||||
----------
|
----------
|
||||||
config : dict or str, optional
|
config : dict or str, optional
|
||||||
Material configuration. String needs to be valid YAML.
|
Material configuration. String needs to be valid YAML.
|
||||||
Defaults to None, in which case empty entries for
|
homogenization : dict, optional
|
||||||
any missing material, homogenization, and phase entry are created.
|
Homogenization configuration.
|
||||||
kwargs : key=value pairs, optional
|
Defaults to an empty dict if 'config' is not given.
|
||||||
Initial content specified as pairs of key=value.
|
phase : dict, optional
|
||||||
|
Phase configuration.
|
||||||
|
Defaults to an empty dict if 'config' is not given.
|
||||||
|
material : dict, optional
|
||||||
|
Materialpoint configuration.
|
||||||
|
Defaults to an empty list if 'config' is not given.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
default: Collection
|
kwargs: Dict[str,Union[Dict[str,Dict],List[Dict[str,Any]]]] = {}
|
||||||
if config is None:
|
for arg,value in zip(['homogenization','phase','material'],[homogenization,phase,material]):
|
||||||
for section, default in {'material':[],'homogenization':{},'phase':{}}.items():
|
if value is None and config is None:
|
||||||
if section not in kwargs: kwargs.update({section:default})
|
kwargs[arg] = [] if arg == 'material' else {}
|
||||||
|
elif value is not None:
|
||||||
|
kwargs[arg] = value
|
||||||
|
|
||||||
super().__init__(config,**kwargs)
|
super().__init__(config,**kwargs)
|
||||||
|
|
||||||
|
@ -170,8 +180,12 @@ class ConfigMaterial(Config):
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_table(table: Table,
|
def from_table(table: Table,*,
|
||||||
**kwargs) -> 'ConfigMaterial':
|
homogenization: Optional[Union[str,StrSequence]] = None,
|
||||||
|
phase: Optional[Union[str,StrSequence]] = None,
|
||||||
|
v: Optional[Union[str,FloatSequence]] = None,
|
||||||
|
O: Optional[Union[str,FloatSequence]] = None,
|
||||||
|
V_e: Optional[Union[str,FloatSequence]] = None) -> 'ConfigMaterial':
|
||||||
"""
|
"""
|
||||||
Generate from an ASCII table.
|
Generate from an ASCII table.
|
||||||
|
|
||||||
|
@ -179,16 +193,33 @@ class ConfigMaterial(Config):
|
||||||
----------
|
----------
|
||||||
table : damask.Table
|
table : damask.Table
|
||||||
Table that contains material information.
|
Table that contains material information.
|
||||||
**kwargs
|
homogenization: (array-like) of str, optional
|
||||||
Keyword arguments where the key is the property name and
|
Homogenization label.
|
||||||
the value specifies either the label of the data column in the table
|
phase: (array-like) of str, optional
|
||||||
or a constant value.
|
Phase label (per constituent).
|
||||||
|
v: (array-like) of float or str, optional
|
||||||
|
Constituent volume fraction (per constituent).
|
||||||
|
Defaults to 1/N_constituent.
|
||||||
|
O: (array-like) of damask.Rotation or np.array/list of shape(4) or str, optional
|
||||||
|
Orientation as unit quaternion (per constituent).
|
||||||
|
V_e: (array-like) of np.array/list of shape(3,3) or str, optional
|
||||||
|
Left elastic stretch (per constituent).
|
||||||
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
new : damask.ConfigMaterial
|
new : damask.ConfigMaterial
|
||||||
Material configuration from values in table.
|
Material configuration from values in table.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
If the value of an argument is a string that is a column label,
|
||||||
|
data from the table is used to fill the corresponding entry in
|
||||||
|
the material configuration. Otherwise, the value is used directly.
|
||||||
|
|
||||||
|
First index of array-like values that are defined per constituent
|
||||||
|
runs over materials, whereas second index runs over constituents.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import damask
|
>>> import damask
|
||||||
|
@ -230,15 +261,16 @@ class ConfigMaterial(Config):
|
||||||
phase: {Aluminum: null, Steel: null}
|
phase: {Aluminum: null, Steel: null}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
kwargs_ = {k:table.get(v) if v in table.labels else np.atleast_2d([v]*len(table)).T for k,v in kwargs.items()}
|
kwargs = {}
|
||||||
|
for arg,val in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
|
||||||
|
if val is not None:
|
||||||
|
kwargs[arg] = table.get(val) if val in table.labels else np.atleast_2d([val]*len(table)).T # type: ignore
|
||||||
|
|
||||||
_,idx = np.unique(np.hstack(list(kwargs_.values())),return_index=True,axis=0)
|
_,idx = np.unique(np.hstack(list(kwargs.values())),return_index=True,axis=0)
|
||||||
idx = np.sort(idx)
|
idx = np.sort(idx)
|
||||||
kwargs_ = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs_.items()}
|
kwargs = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs.items()}
|
||||||
for what in ['phase','homogenization']:
|
|
||||||
if what not in kwargs_: kwargs_[what] = what+'_label'
|
|
||||||
|
|
||||||
return ConfigMaterial().material_add(**kwargs_)
|
return ConfigMaterial().material_add(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -434,7 +466,7 @@ class ConfigMaterial(Config):
|
||||||
Phase label (per constituent).
|
Phase label (per constituent).
|
||||||
v: (array-like) of float, optional
|
v: (array-like) of float, optional
|
||||||
Constituent volume fraction (per constituent).
|
Constituent volume fraction (per constituent).
|
||||||
Defaults to 1/N_constituents
|
Defaults to 1/N_constituent.
|
||||||
O: (array-like) of damask.Rotation or np.array/list of shape(4), optional
|
O: (array-like) of damask.Rotation or np.array/list of shape(4), optional
|
||||||
Orientation as unit quaternion (per constituent).
|
Orientation as unit quaternion (per constituent).
|
||||||
V_e: (array-like) of np.array/list of shape(3,3), optional
|
V_e: (array-like) of np.array/list of shape(3,3), optional
|
||||||
|
@ -527,49 +559,48 @@ class ConfigMaterial(Config):
|
||||||
phase: {Austenite: null, Ferrite: null}
|
phase: {Austenite: null, Ferrite: null}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
kwargs = {}
|
dim = {'O':(4,),'V_e':(3,3,)}
|
||||||
for keyword,value in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
|
ex = dict((keyword, -len(val)) for keyword,val in dim.items())
|
||||||
if value is not None: kwargs[keyword] = value
|
|
||||||
|
|
||||||
_constituent_properties = ['phase','O','v','V_e']
|
|
||||||
_dim = {'O':(4,),'V_e':(3,3,)}
|
|
||||||
_ex = dict((k, -len(v)) for k, v in _dim.items())
|
|
||||||
|
|
||||||
N_materials,N_constituents = 1,1
|
N_materials,N_constituents = 1,1
|
||||||
shaped : Dict[str, Union[None,np.ndarray]] = \
|
shape = {}
|
||||||
{'v': None,
|
for arg,val in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
|
||||||
'phase': None,
|
if val is None: continue
|
||||||
'homogenization': None,
|
shape[arg] = np.array(val)
|
||||||
}
|
s = shape[arg].shape[:ex.get(arg,None)] # type: ignore
|
||||||
|
|
||||||
for arg,value in kwargs.items():
|
|
||||||
shaped[arg] = np.array(value)
|
|
||||||
s = shaped[arg].shape[:_ex.get(arg,None)] # type: ignore
|
|
||||||
N_materials = max(N_materials,s[0]) if len(s)>0 else N_materials
|
N_materials = max(N_materials,s[0]) if len(s)>0 else N_materials
|
||||||
N_constituents = max(N_constituents,s[1]) if len(s)>1 else N_constituents
|
N_constituents = max(N_constituents,s[1]) if len(s)>1 else N_constituents
|
||||||
|
|
||||||
shaped['v'] = np.array(1./N_constituents) if shaped['v'] is None else shaped['v']
|
shape['v'] = np.array(shape.get('v',1./N_constituents),float)
|
||||||
|
|
||||||
mat: Sequence[dict] = [{'constituents':[{} for _ in range(N_constituents)]} for _ in range(N_materials)]
|
mat: Sequence[dict] = [{'constituents':[{} for _ in range(N_constituents)]} for _ in range(N_materials)]
|
||||||
|
|
||||||
for k,v in shaped.items():
|
for k,v in shape.items():
|
||||||
target = (N_materials,N_constituents) + _dim.get(k,())
|
target = (N_materials,N_constituents) + dim.get(k,())
|
||||||
obj = np.broadcast_to(np.array(v).reshape(util.shapeshifter(() if v is None else v.shape,
|
broadcasted = np.broadcast_to(np.array(v).reshape(util.shapeshifter(np.array(v).shape,target,'right')),target)
|
||||||
target,
|
if k == 'v':
|
||||||
mode = 'right')),
|
if np.min(broadcasted) < 0 or np.max(broadcasted) > 1:
|
||||||
target)
|
raise ValueError('volume fraction "v" out of range')
|
||||||
|
if len(np.atleast_1d(broadcasted)) > 1:
|
||||||
|
total = np.sum(broadcasted,axis=-1)
|
||||||
|
if np.min(total) < 0 or np.max(total) > 1:
|
||||||
|
raise ValueError('volume fraction "v" out of range')
|
||||||
|
if k == 'O' and not np.allclose(1.0,np.linalg.norm(broadcasted,axis=-1)):
|
||||||
|
raise ValueError('orientation "O" is not a unit quaterion')
|
||||||
|
elif k == 'V_e' and not np.allclose(broadcasted,tensor.symmetric(broadcasted)):
|
||||||
|
raise ValueError('elastic stretch "V_e" is not symmetric')
|
||||||
for i in range(N_materials):
|
for i in range(N_materials):
|
||||||
if k in _constituent_properties:
|
if k == 'homogenization':
|
||||||
for j in range(N_constituents):
|
mat[i][k] = broadcasted[i,0]
|
||||||
mat[i]['constituents'][j][k] = obj[i,j].item() if isinstance(obj[i,j],np.generic) else obj[i,j]
|
|
||||||
else:
|
else:
|
||||||
mat[i][k] = obj[i,0].item() if isinstance(obj[i,0],np.generic) else obj[i,0]
|
for j in range(N_constituents):
|
||||||
|
mat[i]['constituents'][j][k] = broadcasted[i,j]
|
||||||
|
|
||||||
dup = self.copy()
|
dup = self.copy()
|
||||||
dup['material'] = dup['material'] + mat if 'material' in dup else mat
|
dup['material'] = dup['material'] + mat if 'material' in dup else mat
|
||||||
|
|
||||||
for what in [item for item in ['phase','homogenization'] if shaped[item] is not None]:
|
for what in [item for item in ['phase','homogenization'] if item in shape]:
|
||||||
for k in np.unique(shaped[what]): # type: ignore
|
for k in np.unique(shape[what]): # type: ignore
|
||||||
if k not in dup[what]: dup[what][str(k)] = None
|
if k not in dup[what]: dup[what][str(k)] = None
|
||||||
|
|
||||||
return dup
|
return dup
|
||||||
|
|
|
@ -7,6 +7,17 @@ from damask import Orientation
|
||||||
|
|
||||||
class TestConfig:
|
class TestConfig:
|
||||||
|
|
||||||
|
def test_init_keyword(self):
|
||||||
|
assert Config(p=4)['p'] == 4
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('config',[{'p':1},'{p: 1}'])
|
||||||
|
def test_init_config(self,config):
|
||||||
|
assert Config(config)['p'] == 1
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('config',[{'p':1},'{p: 1}'])
|
||||||
|
def test_init_both(self,config):
|
||||||
|
assert Config(config,p=2)['p'] == 2
|
||||||
|
|
||||||
@pytest.mark.parametrize('flow_style',[None,True,False])
|
@pytest.mark.parametrize('flow_style',[None,True,False])
|
||||||
def test_load_save_str(self,tmp_path,flow_style):
|
def test_load_save_str(self,tmp_path,flow_style):
|
||||||
config = Config()
|
config = Config()
|
||||||
|
@ -36,7 +47,6 @@ class TestConfig:
|
||||||
assert (config | Config(dummy)).delete({ 'hello':1,'foo':2 }) == config
|
assert (config | Config(dummy)).delete({ 'hello':1,'foo':2 }) == config
|
||||||
assert (config | Config(dummy)).delete(Config({'hello':1 })) == config | {'foo':'bar'}
|
assert (config | Config(dummy)).delete(Config({'hello':1 })) == config | {'foo':'bar'}
|
||||||
|
|
||||||
|
|
||||||
def test_repr(self,tmp_path):
|
def test_repr(self,tmp_path):
|
||||||
config = Config()
|
config = Config()
|
||||||
config['A'] = 1
|
config['A'] = 1
|
||||||
|
|
|
@ -16,6 +16,27 @@ def ref_path(ref_path_base):
|
||||||
|
|
||||||
class TestConfigMaterial:
|
class TestConfigMaterial:
|
||||||
|
|
||||||
|
def test_init_empty(self):
|
||||||
|
c = ConfigMaterial()
|
||||||
|
assert len(c) == 3
|
||||||
|
assert c['homogenization'] == {}
|
||||||
|
assert c['phase'] == {}
|
||||||
|
assert c['material'] == []
|
||||||
|
|
||||||
|
def test_init_d(self):
|
||||||
|
c = ConfigMaterial(config={'phase':4})
|
||||||
|
assert len(c) == 1
|
||||||
|
assert c['phase'] == 4
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('kwargs',[{'homogenization':{'SX':{}}},
|
||||||
|
{'phase':{'Aluminum':{}}},
|
||||||
|
{'material':[{'A':1},{'B':2}]}])
|
||||||
|
def test_init_some(self,kwargs):
|
||||||
|
c = ConfigMaterial(**kwargs)
|
||||||
|
assert len(c) == 3
|
||||||
|
for k,v in kwargs.items():
|
||||||
|
if k in kwargs: assert v == kwargs[k]
|
||||||
|
|
||||||
@pytest.mark.parametrize('fname',[None,'test.yaml'])
|
@pytest.mark.parametrize('fname',[None,'test.yaml'])
|
||||||
def test_load_save(self,ref_path,tmp_path,fname):
|
def test_load_save(self,ref_path,tmp_path,fname):
|
||||||
reference = ConfigMaterial.load(ref_path/'material.yaml')
|
reference = ConfigMaterial.load(ref_path/'material.yaml')
|
||||||
|
@ -88,14 +109,14 @@ class TestConfigMaterial:
|
||||||
def test_from_table(self):
|
def test_from_table(self):
|
||||||
N = np.random.randint(3,10)
|
N = np.random.randint(3,10)
|
||||||
a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])),
|
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.zeros(N*2),np.ones(N*2),np.zeros(N*2),np.zeros(N*2),
|
||||||
np.ones(N*2),
|
np.ones(N*2),
|
||||||
)).T
|
)).T
|
||||||
t = Table({'varying':1,'constant':4,'ones':1},a)
|
t = Table({'varying':1,'constant':4,'ones':1},a)
|
||||||
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'})
|
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'})
|
||||||
assert len(c['material']) == N
|
assert len(c['material']) == N
|
||||||
for i,m in enumerate(c['material']):
|
for i,m in enumerate(c['material']):
|
||||||
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [1,0,1,1]).all()
|
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [0,1,0,0]).all()
|
||||||
|
|
||||||
def test_updated_dicts(self,ref_path):
|
def test_updated_dicts(self,ref_path):
|
||||||
m1 = ConfigMaterial().material_add(phase=['Aluminum'],O=[1.0,0.0,0.0,0.0],homogenization='SX')
|
m1 = ConfigMaterial().material_add(phase=['Aluminum'],O=[1.0,0.0,0.0,0.0],homogenization='SX')
|
||||||
|
@ -109,14 +130,14 @@ class TestConfigMaterial:
|
||||||
def test_from_table_with_constant(self):
|
def test_from_table_with_constant(self):
|
||||||
N = np.random.randint(3,10)
|
N = np.random.randint(3,10)
|
||||||
a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])),
|
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.zeros(N*2),np.ones(N*2),np.zeros(N*2),np.zeros(N*2),
|
||||||
np.ones(N*2),
|
np.ones(N*2),
|
||||||
)).T
|
)).T
|
||||||
t = Table({'varying':1,'constant':4,'ones':1},a)
|
t = Table({'varying':1,'constant':4,'ones':1},a)
|
||||||
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':1})
|
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':1})
|
||||||
assert len(c['material']) == N
|
assert len(c['material']) == N
|
||||||
for i,m in enumerate(c['material']):
|
for i,m in enumerate(c['material']):
|
||||||
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [1,0,1,1]).all()
|
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [0,1,0,0]).all()
|
||||||
|
|
||||||
@pytest.mark.parametrize('N,n,kw',[
|
@pytest.mark.parametrize('N,n,kw',[
|
||||||
(1,1,{'phase':'Gold',
|
(1,1,{'phase':'Gold',
|
||||||
|
@ -137,6 +158,19 @@ class TestConfigMaterial:
|
||||||
assert len(m['material']) == N
|
assert len(m['material']) == N
|
||||||
assert len(m['material'][0]['constituents']) == n
|
assert len(m['material'][0]['constituents']) == n
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('shape',[(),(4,),(5,2)])
|
||||||
|
@pytest.mark.parametrize('kw',[{'V_e':np.random.rand(3,3)},
|
||||||
|
{'O':np.random.rand(4)},
|
||||||
|
{'v':np.array(2)}])
|
||||||
|
def test_material_add_invalid(self,kw,shape):
|
||||||
|
kw = {arg:np.broadcast_to(val,shape+val.shape) for arg,val in kw.items()}
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ConfigMaterial().material_add(**kw)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('v',[2,np.ones(3)*2,np.ones((2,2))])
|
||||||
|
def test_material_add_invalid_v(self,v):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ConfigMaterial().material_add(v=v)
|
||||||
|
|
||||||
@pytest.mark.parametrize('cell_ensemble_data',[None,'CellEnsembleData'])
|
@pytest.mark.parametrize('cell_ensemble_data',[None,'CellEnsembleData'])
|
||||||
def test_load_DREAM3D(self,ref_path,cell_ensemble_data):
|
def test_load_DREAM3D(self,ref_path,cell_ensemble_data):
|
||||||
|
|
Loading…
Reference in New Issue