Skip to content

Commit ba43619

Browse files
committed
Merge remote-tracking branch 'upstream/master' into fix/matching-paths
2 parents c602bde + 9f01802 commit ba43619

File tree

210 files changed

+199332
-210
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

210 files changed

+199332
-210
lines changed

.github/workflows/codespell.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ jobs:
1313
runs-on: ubuntu-latest
1414

1515
steps:
16-
- uses: actions/checkout@v2
16+
- uses: actions/checkout@v3
1717
- uses: codespell-project/actions-codespell@master

.github/workflows/docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ jobs:
2222
include:
2323
- python-version: 3.9
2424
steps:
25-
- uses: actions/checkout@v2
25+
- uses: actions/checkout@v3
2626
with:
2727
submodules: recursive
2828
fetch-depth: 0
2929
- name: Set up Python ${{ matrix.python-version }}
30-
uses: actions/setup-python@v2
30+
uses: actions/setup-python@v3
3131
with:
3232
python-version: ${{ matrix.python-version }}
3333
- name: Display Python version
@@ -44,7 +44,7 @@ jobs:
4444
source tools/ci/activate.sh
4545
make -C doc html
4646
- name: Upload docs as artifacts
47-
uses: actions/upload-artifact@v2
47+
uses: actions/upload-artifact@v3
4848
with:
4949
path: doc/_build/html
5050
- name: Deploy (on tags)

.github/workflows/package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ jobs:
2323
- os: ubuntu-latest
2424
python-version: "3.10"
2525
steps:
26-
- uses: actions/checkout@v2
26+
- uses: actions/checkout@v3
2727
with:
2828
submodules: recursive
2929
fetch-depth: 0
3030
- name: Set up Python ${{ matrix.python-version }}
31-
uses: actions/setup-python@v2
31+
uses: actions/setup-python@v3
3232
with:
3333
python-version: ${{ matrix.python-version }}
3434
- name: Display Python version

.github/workflows/pre-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ jobs:
3838
OS_TYPE: ${{ matrix.os }}
3939

4040
steps:
41-
- uses: actions/checkout@v2
41+
- uses: actions/checkout@v3
4242
with:
4343
submodules: recursive
4444
fetch-depth: 0
4545
- name: Install dependencies
4646
run: tools/ci/install_dependencies.sh
4747
- name: Set up Python ${{ matrix.python-version }}
48-
uses: actions/setup-python@v2
48+
uses: actions/setup-python@v3
4949
with:
5050
python-version: ${{ matrix.python-version }}
5151
- name: Display Python version

.github/workflows/stable.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,14 @@ jobs:
7070
OS_TYPE: ${{ matrix.os }}
7171

7272
steps:
73-
- uses: actions/checkout@v2
73+
- uses: actions/checkout@v3
7474
with:
7575
submodules: recursive
7676
fetch-depth: 0
7777
- name: Install dependencies
7878
run: tools/ci/install_dependencies.sh
7979
- name: Set up Python ${{ matrix.python-version }}
80-
uses: actions/setup-python@v2
80+
uses: actions/setup-python@v3
8181
with:
8282
python-version: ${{ matrix.python-version }}
8383
- name: Display Python version

bids/layout/config/bids.json

Lines changed: 45 additions & 32 deletions
Large diffs are not rendered by default.

bids/layout/tests/test_path_building.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,92 @@ def test_insufficient_entities(layout, strict, validate):
4646
"""Check https://github.com/bids-standard/pybids/pull/574#discussion_r366447600."""
4747
with pytest.raises(ValueError):
4848
layout.build_path({'subject': '01'}, strict=strict, validate=validate)
49+
50+
51+
"""
52+
The following tests that indexes some datasets from bids examples,
53+
parses the files for entities and
54+
reconstructs the fullpath of each file by relying on pybids config
55+
and compares it what's actually in the dataset
56+
"""
57+
58+
@pytest.mark.parametrize("scope", ["raw"])
59+
@pytest.mark.parametrize(
60+
"dataset",
61+
[
62+
("qmri_irt1"),
63+
("qmri_mese"),
64+
("qmri_mp2rage"),
65+
("qmri_mp2rageme"),
66+
("qmri_mtsat"),
67+
("qmri_sa2rage"),
68+
("qmri_vfa"),
69+
("ds000117"),
70+
],
71+
)
72+
def test_path_building_on_examples_with_derivatives(dataset, scope, bids_examples):
73+
layout = BIDSLayout(bids_examples / dataset, derivatives=True)
74+
files = layout.get(subject=".*", datatype=".*", regex_search = True, scope=scope)
75+
for bf in files:
76+
entities = bf.get_entities()
77+
path = layout.build_path(entities)
78+
assert(path==bf.path)
79+
80+
@pytest.mark.parametrize(
81+
"dataset",
82+
[
83+
("micr_SEM"),
84+
("micr_SPIM"),
85+
("asl001"),
86+
("asl002"),
87+
("asl003"),
88+
("asl004"),
89+
("asl005"),
90+
("pet001"),
91+
("pet002"),
92+
("pet003"),
93+
("pet004"),
94+
("pet005"),
95+
("qmri_megre"),
96+
("qmri_tb1tfl"),
97+
("eeg_cbm"),
98+
("ieeg_filtered_speech"),
99+
("ieeg_visual_multimodal"),
100+
("ds000248"),
101+
("ds001"),
102+
("ds114"),
103+
],
104+
)
105+
def test_path_building_on_examples_with_no_derivatives(dataset, bids_examples):
106+
layout = BIDSLayout(bids_examples / dataset, derivatives=False)
107+
files = layout.get(subject=".*", datatype=".*", regex_search = True)
108+
for bf in files:
109+
entities = bf.get_entities()
110+
path = layout.build_path(entities)
111+
assert(path==bf.path)
112+
113+
@pytest.mark.parametrize("scope", ["raw"])
114+
@pytest.mark.parametrize(
115+
"dataset",
116+
[
117+
pytest.param(
118+
"ds000247",
119+
marks=pytest.mark.xfail(strict=True,
120+
reason="meg ds folder"
121+
),
122+
),
123+
pytest.param(
124+
"ds000246",
125+
marks=pytest.mark.xfail(strict=True,
126+
reason="meg ds folder"
127+
),
128+
),
129+
],
130+
)
131+
def test_path_building_on_examples_with_derivatives_meg_ds_folder(dataset, scope, bids_examples):
132+
layout = BIDSLayout(bids_examples / dataset, derivatives=True)
133+
files = layout.get(subject=".*", datatype=".*", regex_search = True, scope=scope)
134+
for bf in files:
135+
entities = bf.get_entities()
136+
path = layout.build_path(entities)
137+
assert(path==bf.path)

bids/modeling/statsmodels.py

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections import namedtuple, OrderedDict, Counter, defaultdict
55
import itertools
66
from functools import reduce
7+
from multiprocessing.sharedctypes import Value
78
import re
89
import fnmatch
910

@@ -263,11 +264,11 @@ class BIDSStatsModelsNode:
263264
overridden if one is passed when run() is called on a node.
264265
"""
265266

266-
def __init__(self, level, name, transformations=None, model=None,
267-
contrasts=None, dummy_contrasts=False, group_by=None):
267+
def __init__(self, level, name, model, group_by, transformations=None,
268+
contrasts=None, dummy_contrasts=False):
268269
self.level = level.lower()
269270
self.name = name
270-
self.model = model or {}
271+
self.model = model
271272
if transformations is None:
272273
transformations = {"transformer": "pybids-transforms-v1",
273274
"instructions": []}
@@ -279,13 +280,7 @@ def __init__(self, level, name, transformations=None, model=None,
279280
self.children = []
280281
self.parents = []
281282
if group_by is None:
282-
group_by = []
283-
# Loop over contrasts after first level
284-
if self.level != "run":
285-
group_by.append("contrast")
286-
# Loop over node level of this node
287-
if self.level != "dataset":
288-
group_by.append(self.level)
283+
raise ValueError(f"group_by is not defined for Node: {name}")
289284
self.group_by = group_by
290285

291286
# Check for intercept only run level model and throw an error
@@ -592,25 +587,21 @@ def __init__(self, node, entities={}, collections=None, inputs=None,
592587

593588
var_names = list(self.node.model['x'])
594589

595-
# Handle the special 1 construct. If it's present, we add a
596-
# column of 1's to the design matrix. But behavior varies:
597-
# * If there's only a single contrast across all of the inputs,
598-
# the intercept column is given the same name as the input contrast.
599-
# It may already exist, in which case we do nothing.
600-
# * Otherwise, we name the column 'intercept'.
601-
int_name = None
590+
# Handle the special 1 construct.
591+
# Add column of 1's to the design matrix called "intercept"
602592
if 1 in var_names:
603-
if ('contrast' not in df.columns or df['contrast'].nunique() > 1):
604-
int_name = 'intercept'
605-
else:
606-
int_name = df['contrast'].unique()[0]
607-
608-
var_names.remove(1)
609-
610-
if int_name not in df.columns:
611-
df.insert(0, int_name, 1)
612-
else:
613-
var_names.append(int_name)
593+
if "intercept" in var_names:
594+
raise ValueError("Cannot define both '1' and 'intercept' in 'X'")
595+
596+
var_names = ['intercept' if i == 1 else i for i in var_names]
597+
if 'intercept' not in df.columns:
598+
df.insert(0, 'intercept', 1)
599+
600+
# If a single incoming contrast
601+
if ('contrast' in df.columns and df['contrast'].nunique() == 1):
602+
unique_in_contrast = df['contrast'].unique()[0]
603+
else:
604+
unique_in_contrast = None
614605

615606
var_names = expand_wildcards(var_names, df.columns)
616607

@@ -626,7 +617,7 @@ def __init__(self, node, entities={}, collections=None, inputs=None,
626617

627618
# Create ModelSpec and build contrasts
628619
self.model_spec = create_model_spec(self.data, node.model, self.metadata)
629-
self.contrasts = self._build_contrasts(int_name)
620+
self.contrasts = self._build_contrasts(unique_in_contrast)
630621

631622
def _collections_to_dfs(self, collections):
632623
"""Merges collections and converts them to a pandas DataFrame."""
@@ -690,17 +681,58 @@ def _inputs_to_df(self, inputs):
690681
input_df.loc[input_df.index[i], con.name] = 1
691682
return input_df
692683

693-
def _build_contrasts(self, int_name):
694-
"""Contrast list of ContrastInfo objects based on current state."""
695-
contrasts = {}
684+
def _build_contrasts(self, unique_in_contrast=None):
685+
"""Contrast list of ContrastInfo objects based on current state.
686+
687+
Parameters
688+
----------
689+
unique_in_contrast : string
690+
Name of unique incoming contrast inputs (i.e. if there is only 1)
691+
"""
692+
in_contrasts = self.node.contrasts.copy()
696693
col_names = set(self.X.columns)
697-
for con in self.node.contrasts:
698-
name = con["name"]
694+
695+
# Create dummy contrasts as regular contrasts
696+
dummies = self.node.dummy_contrasts
697+
if dummies:
698+
if 'conditionlist' in dummies:
699+
conditions = set(dummies['condition_list'])
700+
else:
701+
conditions = col_names
702+
703+
for col_name in conditions:
704+
if col_name == "intercept":
705+
col_name = 1
706+
707+
in_contrasts.insert(0,
708+
{
709+
'name': col_name,
710+
'condition_list': [col_name],
711+
'weights': [1],
712+
'test': dummies.get('test')
713+
}
714+
)
715+
716+
# Process all contrasts, starting with dummy contrasts
717+
# Dummy contrasts are replaced if a contrast is defined with same name
718+
contrasts = {}
719+
for con in in_contrasts:
699720
condition_list = list(con["condition_list"])
700-
if 1 in condition_list and int_name is not None:
701-
condition_list[condition_list.index(1)] = int_name
702-
if name == 1 and int_name is not None:
703-
name = int_name
721+
722+
# Rename special 1 construct
723+
condition_list = ['intercept' if i == 1 else i for i in condition_list]
724+
725+
name = con["name"]
726+
727+
# Rename contrast name
728+
if name == 1:
729+
name = unique_in_contrast or 'intercept'
730+
else:
731+
# If Node has single contrast input, as is grouped by contrast
732+
# Rename contrast to append incoming contrast name
733+
if unique_in_contrast:
734+
name = f"{unique_in_contrast}_{name}"
735+
704736
missing_vars = set(condition_list) - col_names
705737
if missing_vars:
706738
if self.invalid_contrasts == 'error':
@@ -711,31 +743,13 @@ def _build_contrasts(self, int_name):
711743
elif self.invalid_contrasts == 'drop':
712744
continue
713745
weights = np.atleast_2d(con['weights'])
746+
714747
# Add contrast name to entities; can be used in grouping downstream
715748
entities = {**self.entities, 'contrast': name}
716749
ci = ContrastInfo(name, condition_list,
717750
con['weights'], con.get("test"), entities)
718751
contrasts[name] = ci
719752

720-
dummies = self.node.dummy_contrasts
721-
if dummies:
722-
conditions = col_names
723-
if 'conditions' in dummies:
724-
conds = set(dummies['conditions'])
725-
if 1 in conds and int_name is not None:
726-
conds.discard(1)
727-
conds.add(int_name)
728-
conditions &= conds
729-
conditions -= set(c.name for c in contrasts.values())
730-
731-
for col_name in conditions:
732-
if col_name in contrasts:
733-
continue
734-
entities = {**self.entities, 'contrast': col_name}
735-
ci = ContrastInfo(col_name, [col_name], [1], dummies.get("test"),
736-
entities)
737-
contrasts[col_name] = ci
738-
739753
return list(contrasts.values())
740754

741755
@property

0 commit comments

Comments
 (0)