From e06ed5e72c0ad39c21351795d58318c6adeb5f2c Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 2 Aug 2017 00:52:45 -0400 Subject: [PATCH 1/9] first implementation of aseg stats reader --- nibabel/freesurfer/io.py | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index 970d7adc6e..7f8691e74f 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -47,6 +47,72 @@ def _fread3_many(fobj, n): return (b1 << 16) + (b2 << 8) + b3 +def read_aseg_stats(seg_stats_file, set ='subcortical', volumes_only = False): + """ + Returns the subcortical stats found in Freesurfer output: subid/stats/aseg.stats + + Tries to match the outputs returned by Freesurfer's Matlab counter part: load_segstats.m + + Parameters + ---------- + filepath : str + Abs path to aseg.stats file. + + set : str + Which set of volumes to return, among ['subcortical', 'wholebrain']. Default: 'subcortical'. + The choice 'subcortical' returns the usual subortical segmentations. + The choice 'wholebrain' returns only volumes (only stat available), + whose segmentations include [ 'BrainSegVol', 'BrainSegVolNotVent', + 'lhCortexVol', 'rhCortexVol', 'lhCorticalWhiteMatterVol', 'rhCorticalWhiteMatterVol', + 'SubCortGrayVol', 'TotalGrayVol', 'SupraTentorialVol', 'SupraTentorialVolNotVent', + 'MaskVol', 'BrainSegVol-to-eTIV', 'MaskVol-to-eTIV', 'lhSurfaceHoles', 'rhSurfaceHoles', + 'eTIV' ] + + Returns + ------- + seg_name : numpy array of strings + Array of segmentation names + seg_index : numpy array + Array of indices of segmentations into the Freesurfer color lookup table. + seg_stats : numpy array + Matrix of subcortical statistics, with the following 5 columns by default. + If volumes_only = True, only the volumes in mm^3 are returned. + Columns in the full output are: + 1. number of voxels + 2. volume of voxels (mm^3) -- same as number but scaled by voxvol + 3. mean intensity over space + 4. std intensity over space + 5. min intensity over space + 6. max intensity over space + 7. range intensity over space + + """ + + acceptable_choices = ['subcortical', 'wholebrain', 'eTIV'] + set = set.lower() + if set not in acceptable_choices: + raise ValueError('invalid choice. Choose one among: {}'.format(acceptable_choices)) + + if set in 'subcortical': + stats = np.loadtxt(seg_stats_file, dtype="i1,i1,i4,f4,S50,f4,f4,f4,f4,f4") + if volumes_only: + out_data = np.array([seg[3] for seg in stats]) + else: + # need to ensure both two types return data correspond in seg order + out_data = stats + + elif set in [ 'wholebrain', 'eTIV']: + wb_regex_pattern = r'# Measure ([\w/+_\- ]+), ([\w/+_\- ]+), ([\w/+_\- ]+), ([\d\.]+), ([\w/+_\-^]+)' + datatypes = np.dtype('U100,U100,U100,f8,U10') + stats = np.fromregex(seg_stats_file, wb_regex_pattern, dtype=datatypes) + if set in ['eTIV']: + out_data = np.array([seg[3] for seg in stats if seg[1] == 'eTIV']) + else: + out_data = np.array([seg[3] for seg in stats]) + + return out_data + + def _read_volume_info(fobj): """Helper for reading the footer from a surface file.""" volume_info = OrderedDict() From 870bb1d9f1f21dcc8c6807d035ef923f12a81d60 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Tue, 28 Nov 2017 00:42:40 -0500 Subject: [PATCH 2/9] updating docs and improving implementation --- nibabel/freesurfer/io.py | 55 +++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index 7f8691e74f..cd6802460a 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -6,6 +6,7 @@ import numpy as np import getpass import time +from os.path import exists as pexists, realpath from collections import OrderedDict from six.moves import xrange @@ -47,7 +48,9 @@ def _fread3_many(fobj, n): return (b1 << 16) + (b2 << 8) + b3 -def read_aseg_stats(seg_stats_file, set ='subcortical', volumes_only = False): +def read_aseg_stats(seg_stats_file, + set_wanted = 'subcortical', + volumes_only = False): """ Returns the subcortical stats found in Freesurfer output: subid/stats/aseg.stats @@ -55,21 +58,31 @@ def read_aseg_stats(seg_stats_file, set ='subcortical', volumes_only = False): Parameters ---------- - filepath : str + seg_stats_file : str Abs path to aseg.stats file. - set : str - Which set of volumes to return, among ['subcortical', 'wholebrain']. Default: 'subcortical'. - The choice 'subcortical' returns the usual subortical segmentations. - The choice 'wholebrain' returns only volumes (only stat available), - whose segmentations include [ 'BrainSegVol', 'BrainSegVolNotVent', - 'lhCortexVol', 'rhCortexVol', 'lhCorticalWhiteMatterVol', 'rhCorticalWhiteMatterVol', - 'SubCortGrayVol', 'TotalGrayVol', 'SupraTentorialVol', 'SupraTentorialVolNotVent', - 'MaskVol', 'BrainSegVol-to-eTIV', 'MaskVol-to-eTIV', 'lhSurfaceHoles', 'rhSurfaceHoles', - 'eTIV' ] + set_wanted : str + Which set of volumes to return, among ['subcortical', 'wholebrain', 'etiv_only' ]. + Default: 'subcortical'. + The choice 'subcortical' returns the usual subortical segmentations. + The choice 'wholebrain' returns the volumes in aseg.stats coded as : + [ 'BrainSegVol', 'BrainSegVolNotVent', 'lhCortexVol', 'rhCortexVol', 'lhCorticalWhiteMatterVol', + 'rhCorticalWhiteMatterVol', 'SubCortGrayVol', 'TotalGrayVol', 'SupraTentorialVol', + 'SupraTentorialVolNotVent', 'MaskVol', 'BrainSegVol-to-eTIV', 'MaskVol-to-eTIV', + 'lhSurfaceHoles', 'rhSurfaceHoles', 'eTIV' ] + These are noted as 'Measure' in the commented section of stats/aseg.stats file. + The choice 'etiv_only' returns the value for eTIV (estimated total intra-cranial volume) only. + + volumes_only : bool + Flag to indicate only the volumes are wanted. + + Default: False, returning all info available, to closely match the outputs returned by Freesurfer's Matlab counter part: + https://github.com/freesurfer/freesurfer/blob/dev/matlab/load_segstats.m Returns ------- + By default (volumes_only=False), three arrays are returned: + seg_name : numpy array of strings Array of segmentation names seg_index : numpy array @@ -86,14 +99,20 @@ def read_aseg_stats(seg_stats_file, set ='subcortical', volumes_only = False): 6. max intensity over space 7. range intensity over space + When volumes_only=True, only one array is returned containing only volumes. + """ - acceptable_choices = ['subcortical', 'wholebrain', 'eTIV'] - set = set.lower() - if set not in acceptable_choices: - raise ValueError('invalid choice. Choose one among: {}'.format(acceptable_choices)) + seg_stats_file = realpath(seg_stats_file) + if not pexists(seg_stats_file): + raise IOError('given path does not exist : {}'.format(seg_stats_file)) + + acceptable_choices = ['subcortical', 'wholebrain', 'etiv_only'] + set_wanted = set_wanted.lower() + if set_wanted not in acceptable_choices: + raise ValueError('Invalid choice. Choose one among: {}'.format(acceptable_choices)) - if set in 'subcortical': + if set_wanted in 'subcortical': stats = np.loadtxt(seg_stats_file, dtype="i1,i1,i4,f4,S50,f4,f4,f4,f4,f4") if volumes_only: out_data = np.array([seg[3] for seg in stats]) @@ -101,11 +120,11 @@ def read_aseg_stats(seg_stats_file, set ='subcortical', volumes_only = False): # need to ensure both two types return data correspond in seg order out_data = stats - elif set in [ 'wholebrain', 'eTIV']: + elif set_wanted in ['wholebrain', 'etiv_only']: wb_regex_pattern = r'# Measure ([\w/+_\- ]+), ([\w/+_\- ]+), ([\w/+_\- ]+), ([\d\.]+), ([\w/+_\-^]+)' datatypes = np.dtype('U100,U100,U100,f8,U10') stats = np.fromregex(seg_stats_file, wb_regex_pattern, dtype=datatypes) - if set in ['eTIV']: + if set_wanted in ['etiv_only']: out_data = np.array([seg[3] for seg in stats if seg[1] == 'eTIV']) else: out_data = np.array([seg[3] for seg in stats]) From e4825f4bf4e4a78ba9fc8349e69658812979cacb Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Tue, 28 Nov 2017 00:42:58 -0500 Subject: [PATCH 3/9] first stab at a simple test --- nibabel/freesurfer/__init__.py | 2 +- nibabel/freesurfer/tests/test_io.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/nibabel/freesurfer/__init__.py b/nibabel/freesurfer/__init__.py index a588fb06e5..34a7b1c406 100644 --- a/nibabel/freesurfer/__init__.py +++ b/nibabel/freesurfer/__init__.py @@ -2,5 +2,5 @@ """ from .io import read_geometry, read_morph_data, write_morph_data, \ - read_annot, read_label, write_geometry, write_annot + read_annot, read_label, write_geometry, write_annot, read_aseg_stats from .mghformat import load, save, MGHImage diff --git a/nibabel/freesurfer/tests/test_io.py b/nibabel/freesurfer/tests/test_io.py index 66673382ac..a1225fd686 100644 --- a/nibabel/freesurfer/tests/test_io.py +++ b/nibabel/freesurfer/tests/test_io.py @@ -14,7 +14,8 @@ from numpy.testing import assert_equal, assert_raises, dec, assert_allclose from .. import (read_geometry, read_morph_data, read_annot, read_label, - write_geometry, write_morph_data, write_annot) + write_geometry, write_morph_data, write_annot, + read_aseg_stats) from ...tests.nibabel_data import get_nibabel_data, needs_nibabel_data from ...fileslice import strided_scalar @@ -145,6 +146,18 @@ def test_morph_data(): assert_equal(curv2, curv) +@freesurfer_test +def test_aseg_stats(): + """Test reader for stats/aseg.stats """ + stats_path = pjoin(data_path, "stats", "aseg.stats") + + stats = read_aseg_stats(stats_path, volumes_only=True) + assert_true(np.alltrue(np.isfinite(stats))) + + seg_name, seg_index, seg_stats = read_aseg_stats(stats_path, volumes_only=False) + + + def test_write_morph_data(): """Test write_morph_data edge cases""" values = np.arange(20, dtype='>f4') @@ -222,3 +235,5 @@ def test_label(): labels, scalars = read_label(label_path, True) assert_true(np.all(labels == label)) assert_true(len(labels) == len(scalars)) + +test_aseg_stats() \ No newline at end of file From af4e04616a6b76c961290ac27ab60313a53421f8 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Sun, 11 Feb 2018 15:44:40 -0500 Subject: [PATCH 4/9] basic implementation of aparc.stats reader (returning everything) --- nibabel/freesurfer/__init__.py | 2 +- nibabel/freesurfer/io.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/__init__.py b/nibabel/freesurfer/__init__.py index 34a7b1c406..b73c72639e 100644 --- a/nibabel/freesurfer/__init__.py +++ b/nibabel/freesurfer/__init__.py @@ -2,5 +2,5 @@ """ from .io import read_geometry, read_morph_data, write_morph_data, \ - read_annot, read_label, write_geometry, write_annot, read_aseg_stats + read_annot, read_label, write_geometry, write_annot, read_aseg_stats, read_aparc_stats from .mghformat import load, save, MGHImage diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index cd6802460a..3db2cb7b18 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -132,6 +132,38 @@ def read_aseg_stats(seg_stats_file, return out_data +def read_aparc_stats(file_path): + """Read statistics on cortical features (such as thickness, curvature etc) produced by Freesurfer. + + file_path would contain whether it is from the right or left hemisphere. + + """ + + # ColHeaders StructName NumVert SurfArea GrayVol ThickAvg ThickStd MeanCurv GausCurv FoldInd CurvInd + aparc_roi_dtype = [('StructName', 'S50'), ('NumVert', ' Date: Mon, 19 Mar 2018 00:13:11 -0400 Subject: [PATCH 5/9] detecting if the input is empty early on! Apparently no one needed it ;) --- nibabel/loadsave.py | 6 +++++- nibabel/py3k.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index 1dc576498b..96319e338a 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -17,7 +17,7 @@ from .filebasedimages import ImageFileError from .imageclasses import all_image_classes from .arrayproxy import is_proxy -from .py3k import FileNotFoundError +from .py3k import FileNotFoundError, FileEmptyError from .deprecated import deprecate_with_version @@ -38,6 +38,10 @@ def load(filename, **kwargs): ''' if not op.exists(filename): raise FileNotFoundError("No such file: '%s'" % filename) + + if op.getsize(filename) <= 0: + raise FileEmptyError("Given file is empty: '%s'" % filename) + sniff = None for image_klass in all_image_classes: is_valid, sniff = image_klass.path_maybe_image(filename, sniff) diff --git a/nibabel/py3k.py b/nibabel/py3k.py index 1c3300ac91..aa09859f3b 100644 --- a/nibabel/py3k.py +++ b/nibabel/py3k.py @@ -67,6 +67,10 @@ class FileNotFoundError(IOError): pass +class FileEmptyError(IOError): + pass + + def getexception(): return sys.exc_info()[1] From a01855bef195a9eb8e0c1adb7f77453ee4c0b2ad Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Mon, 19 Mar 2018 00:20:00 -0400 Subject: [PATCH 6/9] merging with upstream master to undo changes not ready to PR/merge --- nibabel/freesurfer/__init__.py | 2 +- nibabel/freesurfer/io.py | 117 ---------------------------- nibabel/freesurfer/tests/test_io.py | 17 +--- 3 files changed, 2 insertions(+), 134 deletions(-) diff --git a/nibabel/freesurfer/__init__.py b/nibabel/freesurfer/__init__.py index b73c72639e..a588fb06e5 100644 --- a/nibabel/freesurfer/__init__.py +++ b/nibabel/freesurfer/__init__.py @@ -2,5 +2,5 @@ """ from .io import read_geometry, read_morph_data, write_morph_data, \ - read_annot, read_label, write_geometry, write_annot, read_aseg_stats, read_aparc_stats + read_annot, read_label, write_geometry, write_annot from .mghformat import load, save, MGHImage diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index 3db2cb7b18..970d7adc6e 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -6,7 +6,6 @@ import numpy as np import getpass import time -from os.path import exists as pexists, realpath from collections import OrderedDict from six.moves import xrange @@ -48,122 +47,6 @@ def _fread3_many(fobj, n): return (b1 << 16) + (b2 << 8) + b3 -def read_aseg_stats(seg_stats_file, - set_wanted = 'subcortical', - volumes_only = False): - """ - Returns the subcortical stats found in Freesurfer output: subid/stats/aseg.stats - - Tries to match the outputs returned by Freesurfer's Matlab counter part: load_segstats.m - - Parameters - ---------- - seg_stats_file : str - Abs path to aseg.stats file. - - set_wanted : str - Which set of volumes to return, among ['subcortical', 'wholebrain', 'etiv_only' ]. - Default: 'subcortical'. - The choice 'subcortical' returns the usual subortical segmentations. - The choice 'wholebrain' returns the volumes in aseg.stats coded as : - [ 'BrainSegVol', 'BrainSegVolNotVent', 'lhCortexVol', 'rhCortexVol', 'lhCorticalWhiteMatterVol', - 'rhCorticalWhiteMatterVol', 'SubCortGrayVol', 'TotalGrayVol', 'SupraTentorialVol', - 'SupraTentorialVolNotVent', 'MaskVol', 'BrainSegVol-to-eTIV', 'MaskVol-to-eTIV', - 'lhSurfaceHoles', 'rhSurfaceHoles', 'eTIV' ] - These are noted as 'Measure' in the commented section of stats/aseg.stats file. - The choice 'etiv_only' returns the value for eTIV (estimated total intra-cranial volume) only. - - volumes_only : bool - Flag to indicate only the volumes are wanted. - - Default: False, returning all info available, to closely match the outputs returned by Freesurfer's Matlab counter part: - https://github.com/freesurfer/freesurfer/blob/dev/matlab/load_segstats.m - - Returns - ------- - By default (volumes_only=False), three arrays are returned: - - seg_name : numpy array of strings - Array of segmentation names - seg_index : numpy array - Array of indices of segmentations into the Freesurfer color lookup table. - seg_stats : numpy array - Matrix of subcortical statistics, with the following 5 columns by default. - If volumes_only = True, only the volumes in mm^3 are returned. - Columns in the full output are: - 1. number of voxels - 2. volume of voxels (mm^3) -- same as number but scaled by voxvol - 3. mean intensity over space - 4. std intensity over space - 5. min intensity over space - 6. max intensity over space - 7. range intensity over space - - When volumes_only=True, only one array is returned containing only volumes. - - """ - - seg_stats_file = realpath(seg_stats_file) - if not pexists(seg_stats_file): - raise IOError('given path does not exist : {}'.format(seg_stats_file)) - - acceptable_choices = ['subcortical', 'wholebrain', 'etiv_only'] - set_wanted = set_wanted.lower() - if set_wanted not in acceptable_choices: - raise ValueError('Invalid choice. Choose one among: {}'.format(acceptable_choices)) - - if set_wanted in 'subcortical': - stats = np.loadtxt(seg_stats_file, dtype="i1,i1,i4,f4,S50,f4,f4,f4,f4,f4") - if volumes_only: - out_data = np.array([seg[3] for seg in stats]) - else: - # need to ensure both two types return data correspond in seg order - out_data = stats - - elif set_wanted in ['wholebrain', 'etiv_only']: - wb_regex_pattern = r'# Measure ([\w/+_\- ]+), ([\w/+_\- ]+), ([\w/+_\- ]+), ([\d\.]+), ([\w/+_\-^]+)' - datatypes = np.dtype('U100,U100,U100,f8,U10') - stats = np.fromregex(seg_stats_file, wb_regex_pattern, dtype=datatypes) - if set_wanted in ['etiv_only']: - out_data = np.array([seg[3] for seg in stats if seg[1] == 'eTIV']) - else: - out_data = np.array([seg[3] for seg in stats]) - - return out_data - - -def read_aparc_stats(file_path): - """Read statistics on cortical features (such as thickness, curvature etc) produced by Freesurfer. - - file_path would contain whether it is from the right or left hemisphere. - - """ - - # ColHeaders StructName NumVert SurfArea GrayVol ThickAvg ThickStd MeanCurv GausCurv FoldInd CurvInd - aparc_roi_dtype = [('StructName', 'S50'), ('NumVert', ' Date: Mon, 19 Mar 2018 01:23:47 -0400 Subject: [PATCH 7/9] removing unnecessary space that nibabel checks dont like --- nibabel/loadsave.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index 96319e338a..1b9bf611fd 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -41,7 +41,6 @@ def load(filename, **kwargs): if op.getsize(filename) <= 0: raise FileEmptyError("Given file is empty: '%s'" % filename) - sniff = None for image_klass in all_image_classes: is_valid, sniff = image_klass.path_maybe_image(filename, sniff) From d2073606e181e4fa4594ccbf9a53af4cfaba2df7 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Mon, 19 Mar 2018 16:38:05 -0400 Subject: [PATCH 8/9] Using another existing exception, and shorter message --- nibabel/loadsave.py | 5 ++--- nibabel/py3k.py | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index 1b9bf611fd..1beba335c0 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -17,7 +17,7 @@ from .filebasedimages import ImageFileError from .imageclasses import all_image_classes from .arrayproxy import is_proxy -from .py3k import FileNotFoundError, FileEmptyError +from .py3k import FileNotFoundError from .deprecated import deprecate_with_version @@ -38,9 +38,8 @@ def load(filename, **kwargs): ''' if not op.exists(filename): raise FileNotFoundError("No such file: '%s'" % filename) - if op.getsize(filename) <= 0: - raise FileEmptyError("Given file is empty: '%s'" % filename) + raise ImageFileError("Empty file: '%s'" % filename) sniff = None for image_klass in all_image_classes: is_valid, sniff = image_klass.path_maybe_image(filename, sniff) diff --git a/nibabel/py3k.py b/nibabel/py3k.py index aa09859f3b..1c3300ac91 100644 --- a/nibabel/py3k.py +++ b/nibabel/py3k.py @@ -67,10 +67,6 @@ class FileNotFoundError(IOError): pass -class FileEmptyError(IOError): - pass - - def getexception(): return sys.exc_info()[1] From a974b70ce399879248e6c0e57687d085c9f40a0a Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Mon, 19 Mar 2018 23:04:56 -0400 Subject: [PATCH 9/9] better file checks - path.exists is try/except for os.stat anyways! --- nibabel/loadsave.py | 10 ++++++---- nibabel/tests/test_loadsave.py | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index 1beba335c0..c239c1a568 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -9,7 +9,7 @@ # module imports """ Utilities to load and save image objects """ -import os.path as op +import os import numpy as np from .filename_parser import splitext_addext @@ -36,9 +36,11 @@ def load(filename, **kwargs): img : ``SpatialImage`` Image of guessed type ''' - if not op.exists(filename): - raise FileNotFoundError("No such file: '%s'" % filename) - if op.getsize(filename) <= 0: + try: + stat_result = os.stat(filename) + except OSError: + raise FileNotFoundError("No such file or no access: '%s'" % filename) + if stat_result.st_size <= 0: raise ImageFileError("Empty file: '%s'" % filename) sniff = None for image_klass in all_image_classes: diff --git a/nibabel/tests/test_loadsave.py b/nibabel/tests/test_loadsave.py index a5f36100d9..676c09c121 100644 --- a/nibabel/tests/test_loadsave.py +++ b/nibabel/tests/test_loadsave.py @@ -58,6 +58,14 @@ def test_file_not_found(): assert_raises(FileNotFoundError, load, 'does_not_exist.nii.gz') +def test_load_empty_image(): + with InTemporaryDirectory(): + open('empty.nii', 'w').close() + with assert_raises(ImageFileError) as err: + load('empty.nii') + assert_true(err.exception.args[0].startswith('Empty file: ')) + + def test_read_img_data_nifti(): shape = (2, 3, 4) data = np.random.normal(size=shape)