From 1dc7ad054c2ee891738674db661b876133a195f8 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 23 Jul 2024 13:37:40 -0400 Subject: [PATCH 1/7] Draft surface-volume combination parsing. --- niworkflows/utils/spaces.py | 78 +++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/niworkflows/utils/spaces.py b/niworkflows/utils/spaces.py index 5fddda253a4..3897ffc1353 100644 --- a/niworkflows/utils/spaces.py +++ b/niworkflows/utils/spaces.py @@ -168,9 +168,25 @@ def __attrs_post_init__(self): if self.space.startswith("fs"): object.__setattr__(self, "dim", 2) + if "volspace" in self.spec: + volspace = self.spec["volspace"] + if (self.space in self._standard_spaces) and (volspace not in self._standard_spaces): + raise ValueError( + f"Surface space ({self.space}) is a standard space, " + f"but volume space ({volspace}) is not. " + "Mixing standard and non-standard spaces is not currently allowed." + ) + elif (self.space not in self._standard_spaces) and (volspace in self._standard_spaces): + raise ValueError( + f"Surface space ({self.space}) is a non-standard space, " + f"but volume space ({volspace}) is a standard space. " + "Mixing standard and non-standard spaces is not currently allowed." + ) + if self.space in self._standard_spaces: object.__setattr__(self, "standard", True) + # Check that cohort is handled appropriately _cohorts = ["%s" % t for t in _tfapi.TF_LAYOUT.get_cohorts(template=self.space)] if "cohort" in self.spec: if not _cohorts: @@ -191,6 +207,30 @@ def __attrs_post_init__(self): "Set a valid cohort selector from: %s." % (self.space, _cohorts) ) + # Check that cohort is handled appropriately for the volume template if necessary + if "volspace" in self.spec: + _cohorts = [ + "%s" % t for t in _tfapi.TF_LAYOUT.get_cohorts(template=self.spec["volspace"]) + ] + if "volcohort" in self.spec: + if not _cohorts: + raise ValueError( + 'standard space "%s" does not accept a cohort ' + "specification." % self.spec["volspace"] + ) + + if str(self.spec["volcohort"]) not in _cohorts: + raise ValueError( + 'standard space "%s" does not contain any cohort ' + 'named "%s".' % (self.spec["volspace"], self.spec["volcohort"]) + ) + elif _cohorts: + _cohorts = ", ".join(['"cohort-%s"' % c for c in _cohorts]) + raise ValueError( + 'standard space "%s" is not fully defined.\n' + "Set a valid cohort selector from: %s." % (self.spec["volspace"], _cohorts) + ) + @property def fullname(self): """ @@ -205,9 +245,17 @@ def fullname(self): 'MNIPediatricAsym:cohort-1' """ - if "cohort" not in self.spec: - return self.space - return "%s:cohort-%s" % (self.space, self.spec["cohort"]) + name = self.space + + if "cohort" in self.spec: + name += f":cohort-{self.spec['cohort']}" + + if "volspace" in self.spec: + name += f"::{self.spec['volspace']}" + if "volcohort" in self.spec: + name += f":cohort-{self.spec['volcohort']}" + + return name @property def legacyname(self): @@ -330,13 +378,37 @@ def from_string(cls, value): Reference(space='MNIPediatricAsym', spec={'cohort': '6', 'res': '2'}), Reference(space='MNIPediatricAsym', spec={'cohort': '6', 'res': 'iso1.6mm'})] + >>> Reference.from_string( + ... "dhcpAsym:cohort-42:den-32k::dhcpVol:cohort-44:res-2" + ... ) # doctest: +NORMALIZE_WHITESPACE + [Reference(space='dhcpAsym', spec={'cohort': '42', 'den': '32k', 'volspace': 'dhcpVol', + 'volcohort': '44', 'res': '2'})] + """ + volume_value = None + if "::" in value: + # CIFTI definition with both surface and volume spaces defined + value, volume_value = value.split("::") + # We treat the surface space definition as the "primary" space + _args = value.split(":") + _args = value.split(":") spec = defaultdict(list, {}) for modifier in _args[1:]: mitems = modifier.split("-", 1) spec[mitems[0]].append(len(mitems) == 1 or mitems[1]) + if volume_value: + # Tack on the volume space definition to the surface space definition + volume_args = volume_value.split(":") + # There are two special entities to prevent overloading: volspace and volcohort + spec["volspace"] = [volume_args[0]] + for modifier in volume_args[1:]: + mitems = modifier.split("-", 1) + if mitems[0] == "cohort": + mitems[0] = "volcohort" + spec[mitems[0]].append(len(mitems) == 1 or mitems[1]) + allspecs = _expand_entities(spec) return [cls(_args[0], s) for s in allspecs] From 98a753c407b2c686bf80ca2146f86f28a1686378 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 23 Jul 2024 14:04:11 -0400 Subject: [PATCH 2/7] Add cifti attribute. --- niworkflows/utils/spaces.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/niworkflows/utils/spaces.py b/niworkflows/utils/spaces.py index 3897ffc1353..627b23696a2 100644 --- a/niworkflows/utils/spaces.py +++ b/niworkflows/utils/spaces.py @@ -148,6 +148,8 @@ class Reference: """The dictionary of specs.""" standard = attr.ib(default=False, repr=False, type=bool) """Whether this space is standard or not.""" + cifti = attr.ib(default=False, repr=False, type=bool) + """Whether this space is a CIFTI space or not.""" dim = attr.ib(default=3, repr=False, type=int) """Dimensionality of the sampling manifold.""" @@ -165,6 +167,7 @@ def __attrs_post_init__(self): spec["den"] = FSAVERAGE_DENSITY[space] object.__setattr__(self, "spec", spec) + # XXX: This won't cover dhcpAsym, dhcpSym, or onavg if self.space.startswith("fs"): object.__setattr__(self, "dim", 2) @@ -186,6 +189,9 @@ def __attrs_post_init__(self): if self.space in self._standard_spaces: object.__setattr__(self, "standard", True) + if "volspace" in self.spec: + object.__setattr__(self, "cifti", True) + # Check that cohort is handled appropriately _cohorts = ["%s" % t for t in _tfapi.TF_LAYOUT.get_cohorts(template=self.space)] if "cohort" in self.spec: From 76027abfcd638e948b265751006dcdd4c562fcad Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 23 Jul 2024 14:44:41 -0400 Subject: [PATCH 3/7] Update test_spaces.py --- niworkflows/utils/tests/test_spaces.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/niworkflows/utils/tests/test_spaces.py b/niworkflows/utils/tests/test_spaces.py index 2c72378f987..5180a2a8f35 100644 --- a/niworkflows/utils/tests/test_spaces.py +++ b/niworkflows/utils/tests/test_spaces.py @@ -82,6 +82,10 @@ def parser(): ), ), (("MNI152NLin6Asym", "func"), ("MNI152NLin6Asym:res-native", "func")), + ( + ("dhcpAsym:cohort-42:den-32k::dhcpVol:cohort-44:res-2",), + ("dhcpAsym:cohort-42:den-32k:volspace-dhcpVol:volcohort-44:res-2",), + ), ], ) def test_space_action(parser, spaces, expected): From 15569a9d6e9f99fafdec0d7ee1d6bc2c02abf813 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 23 Jul 2024 15:09:58 -0400 Subject: [PATCH 4/7] Fix test. --- niworkflows/utils/tests/test_spaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/utils/tests/test_spaces.py b/niworkflows/utils/tests/test_spaces.py index 5180a2a8f35..4fe3565adbd 100644 --- a/niworkflows/utils/tests/test_spaces.py +++ b/niworkflows/utils/tests/test_spaces.py @@ -84,7 +84,7 @@ def parser(): (("MNI152NLin6Asym", "func"), ("MNI152NLin6Asym:res-native", "func")), ( ("dhcpAsym:cohort-42:den-32k::dhcpVol:cohort-44:res-2",), - ("dhcpAsym:cohort-42:den-32k:volspace-dhcpVol:volcohort-44:res-2",), + ("dhcpAsym:cohort-42:den-32k:res-2:volcohort-44:volspace-dhcpVol",), ), ], ) From f3523eb35d7684bd217ee360d486ea3668e18fa3 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 5 Aug 2024 16:17:23 -0400 Subject: [PATCH 5/7] Update spaces.py --- niworkflows/utils/spaces.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/niworkflows/utils/spaces.py b/niworkflows/utils/spaces.py index 627b23696a2..6d8ccb8d896 100644 --- a/niworkflows/utils/spaces.py +++ b/niworkflows/utils/spaces.py @@ -171,6 +171,12 @@ def __attrs_post_init__(self): if self.space.startswith("fs"): object.__setattr__(self, "dim", 2) + if self.space in self._standard_spaces: + object.__setattr__(self, "standard", True) + + if "volspace" in self.spec: + object.__setattr__(self, "cifti", True) + if "volspace" in self.spec: volspace = self.spec["volspace"] if (self.space in self._standard_spaces) and (volspace not in self._standard_spaces): @@ -186,12 +192,6 @@ def __attrs_post_init__(self): "Mixing standard and non-standard spaces is not currently allowed." ) - if self.space in self._standard_spaces: - object.__setattr__(self, "standard", True) - - if "volspace" in self.spec: - object.__setattr__(self, "cifti", True) - # Check that cohort is handled appropriately _cohorts = ["%s" % t for t in _tfapi.TF_LAYOUT.get_cohorts(template=self.space)] if "cohort" in self.spec: From e0f427853b57e4fba0884f85e2cc41f31d554f59 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 6 Aug 2024 09:20:33 -0400 Subject: [PATCH 6/7] Add cifti argument to get_*. --- niworkflows/utils/spaces.py | 66 +++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/niworkflows/utils/spaces.py b/niworkflows/utils/spaces.py index 89b59c9c7f8..b38f701f3fc 100644 --- a/niworkflows/utils/spaces.py +++ b/niworkflows/utils/spaces.py @@ -400,7 +400,7 @@ def from_string(cls, value): ... "dhcpAsym:cohort-42:den-32k::dhcpVol:cohort-44:res-2" ... ) # doctest: +NORMALIZE_WHITESPACE [Reference(space='dhcpAsym', spec={'cohort': '42', 'den': '32k', 'volspace': 'dhcpVol', - 'volcohort': '44', 'res': '2'})] + 'volcohort': '44', 'volres': '2'})] """ volume_value = None @@ -423,9 +423,7 @@ def from_string(cls, value): spec["volspace"] = [volume_args[0]] for modifier in volume_args[1:]: mitems = modifier.split("-", 1) - if mitems[0] == "cohort": - mitems[0] = "volcohort" - spec[mitems[0]].append(len(mitems) == 1 or mitems[1]) + spec[f"vol{mitems[0]}"].append(len(mitems) == 1 or mitems[1]) allspecs = _expand_entities(spec) @@ -662,9 +660,8 @@ def insert(self, index, value, error=True): elif error is True: raise ValueError('space "%s" already in spaces.' % str(value)) - def get_spaces(self, standard=True, nonstandard=True, dim=(2, 3)): - """ - Return space names. + def get_spaces(self, standard=True, nonstandard=True, dim=(2, 3), cifti=(True, False)): + """Return space names. Parameters ---------- @@ -674,6 +671,8 @@ def get_spaces(self, standard=True, nonstandard=True, dim=(2, 3)): Return nonstandard spaces. dim : :obj:`tuple`, optional Desired dimensions of the standard spaces (default is ``(2, 3)``) + cifti : :obj:`tuple`, optional + Desired CIFTI status of the standard spaces (default is ``(True, False)``). Examples -------- @@ -708,13 +707,13 @@ def get_spaces(self, standard=True, nonstandard=True, dim=(2, 3)): s.fullname not in out and (s.standard is standard or s.standard is not nonstandard) and s.dim in dim + and s.cifti in cifti ): out.append(s.fullname) return out - def get_standard(self, full_spec=False, dim=(2, 3)): - """ - Return output spaces. + def get_standard(self, full_spec=False, dim=(2, 3), cifti=(True, False)): + """Return standard output spaces. Parameters ---------- @@ -723,30 +722,39 @@ def get_standard(self, full_spec=False, dim=(2, 3)): have density or resolution set). dim : :obj:`tuple`, optional Desired dimensions of the standard spaces (default is ``(2, 3)``) - + cifti : :obj:`tuple`, optional + Desired CIFTI status of the standard spaces (default is ``(True, False)``). """ + out = [s for s in self.references if s.standard] + out = [s for s in out if s.dim in dim] + out = [s for s in out if s.cifti in cifti] if not full_spec: - return [s for s in self.references if s.standard and s.dim in dim] + return out - return [ - s - for s in self.references - if s.standard - and s.dim in dim - and (hasspec("res", s.spec) or hasspec("den", s.spec)) - ] + out = [s for s in out if hasspec("res", s.spec) or hasspec("den", s.spec)] + return out + + def get_nonstandard(self, full_spec=False, dim=(2, 3), cifti=(True, False)): + """Return nonstandard output spaces. - def get_nonstandard(self, full_spec=False, dim=(2, 3)): - """Return nonstandard spaces.""" + Parameters + ---------- + full_spec : :obj:`bool` + Return only fully-specified standard references (i.e., they must either + have density or resolution set). + dim : :obj:`tuple`, optional + Desired dimensions of the standard spaces (default is ``(2, 3)``) + cifti : :obj:`tuple`, optional + Desired CIFTI status of the standard spaces (default is ``(True, False)``). + """ + out = [s for s in self.references if not s.standard] + out = [s for s in out if s.dim in dim] + out = [s for s in out if s.cifti in cifti] if not full_spec: - return [s.space for s in self.references if not s.standard and s.dim in dim] - return [ - s.space - for s in self.references - if not s.standard - and s.dim in dim - and (hasspec("res", s.spec) or hasspec("den", s.spec)) - ] + return out + + out = [s for s in out if hasspec("res", s.spec) or hasspec("den", s.spec)] + return out def get_fs_spaces(self): """ From b7faf1901c6c281b25de5aae993ff31265290d53 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 6 Aug 2024 09:36:08 -0400 Subject: [PATCH 7/7] Fix test. --- niworkflows/utils/tests/test_spaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/utils/tests/test_spaces.py b/niworkflows/utils/tests/test_spaces.py index 4fe3565adbd..7a10651f221 100644 --- a/niworkflows/utils/tests/test_spaces.py +++ b/niworkflows/utils/tests/test_spaces.py @@ -84,7 +84,7 @@ def parser(): (("MNI152NLin6Asym", "func"), ("MNI152NLin6Asym:res-native", "func")), ( ("dhcpAsym:cohort-42:den-32k::dhcpVol:cohort-44:res-2",), - ("dhcpAsym:cohort-42:den-32k:res-2:volcohort-44:volspace-dhcpVol",), + ("dhcpAsym:cohort-42:den-32k:volcohort-44:volres-2:volspace-dhcpVol",), ), ], )