We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pandas method FutureWarning: frame.append
With:
- Python 3.9.7
- pybids 0.15.0
- pandas 0.14.0

I'm getting the following `FutureWarning` when running `bids = BIDSLayout(self.dataset.path, derivatives=True)`:

```
...bids/variables/entities.py:245: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.
```

pointing to [this line](https://github.com/bids-standard/pybids/blob/e92d4e8d058fc0e1797c8882dd98024068a303bf/bids/variables/entities.py#L245).

Should I submit a PR to replace that, or are there reasons why this should not be done?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bids/variables/entities.py`
Content:
```
1 """ Data classes for internal BIDS data hierarchy. """
2 
3 from itertools import chain
4 from collections import namedtuple
5 
6 import pandas as pd
7 
8 from . import collections as clc
9 from bids.utils import matches_entities
10 
11 
12 class Node(object):
13     """Base class for objects that represent a single object in the BIDS
14     hierarchy.
15 
16     Parameters
17     ----------
18     id : int or str
19         A value uniquely identifying this node. Typically the
20         entity value extracted from the filename via layout.
21     """
22 
23     def __init__(self, level, entities):
24         self.level = level.lower()
25         self.entities = entities
26         self.variables = {}
27 
28     def add_variable(self, variable):
29         """Adds a BIDSVariable to the current Node's list.
30 
31         Parameters
32         ----------
33         variable : BIDSVariable
34             The Variable to add to the list.
35         """
36         self.variables[variable.name] = variable
37 
38 
39 class RunNode(Node):
40     """Represents a single Run in a BIDS project.
41 
42     Parameters
43     ----------
44     id : int
45         The index of the run.
46     entities : dict
47         Dictionary of entities for this Node.
48     image_file : str
49         The full path to the corresponding nifti image.
50     duration : float
51         Duration of the run, in seconds.
52     repetition_time : float
53         TR for the run.
54     task : str
55         The task name for this run.
56     """
57 
58     def __init__(self, entities, image_file, duration, repetition_time, n_vols):
59         self.image_file = image_file
60         self.duration = duration
61         self.repetition_time = repetition_time
62         self.n_vols = n_vols
63         super(RunNode, self).__init__('run', entities)
64 
65     def get_info(self):
66         # Note: do not remove the dict() call! self.entities is a SQLAlchemy
67         # association_proxy mapping, and without the conversion, the connection
68         # to the DB persists, causing problems on Python 3.5 if we try to clone
69         # a RunInfo or any containing object.
70         entities = dict(self.entities)
71         return RunInfo(entities, self.duration,
72                        self.repetition_time, self.image_file, self.n_vols)
73 
74 
75 # Stores key information for each Run.
76 RunInfo_ = namedtuple('RunInfo', ['entities', 'duration', 'tr', 'image', 'n_vols'])
77 
78 
79 # Wrap with class to provide docstring
80 class RunInfo(RunInfo_):
81     """ A namedtuple storing run-related information.
82 
83     Properties include 'entities', 'duration', 'tr', and 'image', 'n_vols'.
84     """
85     pass
86 
87 
88 class NodeIndex(object):
89     """Represents the top level in a BIDS hierarchy. """
90 
91     def __init__(self):
92         super(NodeIndex, self).__init__()
93         self.index = pd.DataFrame()
94         self.nodes = []
95 
96     def get_collections(self, unit, names=None, merge=False,
97                         sampling_rate=None, **entities):
98         """Retrieve variable data for a specified level in the Dataset.
99 
100         Parameters
101         ----------
102         unit : str
103             The unit of analysis to return variables for. Must be
104             one of 'run', 'session', 'subject', or 'dataset'.
105         names : list
106             Optional list of variables names to return. If
107             None, all available variables are returned.
108         merge : bool
109             If True, variables are merged across all observations
110             of the current unit. E.g., if unit='subject' and return_type=
111             'collection', variables from all subjects will be merged into a
112             single collection. If False, each observation is handled
113             separately, and the result is returned as a list.
114         sampling_rate : int or str
115             If unit='run', the sampling rate to
116             pass onto the returned BIDSRunVariableCollection.
117         entities : dict
118             Optional constraints used to limit what gets returned.
119 
120         Returns
121         -------
122         A list of BIDSVariableCollections if merge=False; a single
123         BIDSVariableCollection if merge=True.
124         """
125 
126         nodes = self.get_nodes(unit, entities)
127         var_sets = []
128 
129         for n in nodes:
130             var_set = list(n.variables.values())
131             var_set = [v for v in var_set if matches_entities(v, entities)]
132             if names is not None:
133                 var_set = [v for v in var_set if v.name in names]
134             # Additional filtering on Variables past run level, because their
135             # contents are extracted from TSV files containing rows from
136             # multiple observations
137             if unit != 'run':
138                 var_set = [v.filter(entities) for v in var_set]
139             var_sets.append(var_set)
140 
141         if merge:
142             var_sets = [list(chain(*var_sets))]
143 
144         results = []
145         for vs in var_sets:
146             if not vs:
147                 continue
148             if unit == 'run':
149                 vs = clc.BIDSRunVariableCollection(vs, sampling_rate)
150             else:
151                 vs = clc.BIDSVariableCollection(vs)
152             results.append(vs)
153 
154         if merge:
155             return results[0] if results else None
156 
157         return results
158 
159     def get_nodes(self, level=None, entities=None, strict=False):
160         """Retrieves all nodes that match the specified criteria.
161 
162         Parameters
163         ----------
164         level : str
165             The level of analysis of nodes to return.
166         entities : dict
167             Entities to filter on. All nodes must have
168             matching values on all defined keys to be included.
169         strict : bool
170             If True, an exception will be raised if the entities
171             dict contains any keys that aren't contained in the current
172             index.
173 
174         Returns
175         -------
176         A list of Node instances.
177         """
178 
179         entities = {} if entities is None else entities.copy()
180 
181         if level is not None:
182             entities['level'] = level
183 
184         if entities is None:
185             return self.nodes
186 
187         match_ents = set(entities.keys())
188         common_cols = list(match_ents & set(self.index.columns))
189 
190         if strict and match_ents - common_cols:
191             raise ValueError("Invalid entities: ", match_ents - common_cols)
192 
193         if not common_cols:
194             return self.nodes
195 
196         # Construct query string that handles both single values and iterables
197         query = []
198         for col in common_cols:
199             oper = 'in' if isinstance(entities[col], (list, tuple)) else '=='
200             q = '{name} {oper} {val}'.format(name=col, oper=oper,
201                                              val=repr(entities[col]))
202             query.append(q)
203         query = ' and '.join(query)
204 
205         rows = self.index.query(query)
206         if rows.empty:
207             return []
208 
209         # Sort and return
210         sort_cols = ['subject', 'session', 'task', 'run', 'node_index',
211                      'suffix', 'level', 'datatype']
212         sort_cols = [sc for sc in sort_cols if sc in set(rows.columns)]
213         rows = rows.sort_values(sort_cols)
214         inds = rows['node_index'].astype(int)
215         return [self.nodes[i] for i in inds]
216 
217     def create_node(self, level, entities, *args, **kwargs):
218         """Creates a new child Node.
219 
220         Parameters
221         ----------
222         level : str
223             The level of analysis of the new Node.
224         entities : dict
225             Dictionary of entities belonging to Node.
226         args, kwargs : dict
227             Optional positional or named arguments to pass on to
228             class-specific initializers. These arguments are only used if
229             a Node that matches the passed entities doesn't already exist,
230             and a new one must be created.
231 
232         Returns
233         -------
234         A Node instance.
235         """
236 
237         if level == 'run':
238             node = RunNode(entities, *args, **kwargs)
239         else:
240             node = Node(level, entities)
241 
242         entities = dict(entities, node_index=len(self.nodes), level=level)
243         self.nodes.append(node)
244         node_row = pd.Series(entities)
245         self.index = self.index.append(node_row, ignore_index=True)
246         return node
247 
248     def get_or_create_node(self, level, entities, *args, **kwargs):
249         """Retrieves a child Node based on the specified criteria, creating a
250         new Node if necessary.
251 
252         Parameters
253         ----------
254         level : str
255             The level of analysis of the Node.
256         entities : dict
257             Dictionary of entities to include in newly-created
258             Nodes or filter existing ones.
259         args, kwargs : dict
260             Optional positional or named arguments to pass on to
261             class-specific initializers. These arguments are only used if
262             a Node that matches the passed entities doesn't already exist,
263             and a new one must be created.
264 
265         Returns
266         -------
267         A Node instance.
268         """
269 
270         result = self.get_nodes(level, entities)
271 
272         if result:
273             if len(result) > 1:
274                 raise ValueError("More than one matching Node found! If you're"
275                                  " expecting more than one Node, use "
276                                  "get_nodes() instead of get_or_create_node()."
277                                  )
278             return result[0]
279 
280         return self.create_node(level, entities, *args, **kwargs)
281
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bids/variables/entities.py b/bids/variables/entities.py
--- a/bids/variables/entities.py
+++ b/bids/variables/entities.py
@@ -241,8 +241,11 @@
 
         entities = dict(entities, node_index=len(self.nodes), level=level)
         self.nodes.append(node)
-        node_row = pd.Series(entities)
-        self.index = self.index.append(node_row, ignore_index=True)
+        # Because "entities" may have non-scalar values (such as `SliceTiming`)
+        # we need to first create a Series to avoid expansion
+        # From here we can concatenate
+        node_row = pd.DataFrame(pd.Series(entities)).T
+        self.index = pd.concat([self.index, node_row], ignore_index=True)
         return node
 
     def get_or_create_node(self, level, entities, *args, **kwargs):

{"golden_diff": "diff --git a/bids/variables/entities.py b/bids/variables/entities.py\n--- a/bids/variables/entities.py\n+++ b/bids/variables/entities.py\n@@ -241,8 +241,11 @@\n \n         entities = dict(entities, node_index=len(self.nodes), level=level)\n         self.nodes.append(node)\n-        node_row = pd.Series(entities)\n-        self.index = self.index.append(node_row, ignore_index=True)\n+        # Because \"entities\" may have non-scalar values (such as `SliceTiming`)\n+        # we need to first create a Series to avoid expansion\n+        # From here we can concatenate\n+        node_row = pd.DataFrame(pd.Series(entities)).T\n+        self.index = pd.concat([self.index, node_row], ignore_index=True)\n         return node\n \n     def get_or_create_node(self, level, entities, *args, **kwargs):\n", "issue": "Pandas method FutureWarning: frame.append\nWith:\r\n- Python 3.9.7\r\n- pybids 0.15.0\r\n- pandas 0.14.0\r\n\r\nI'm getting the following `FutureWarning` when running `bids = BIDSLayout(self.dataset.path, derivatives=True)`:\r\n\r\n```\r\n...bids/variables/entities.py:245: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.\r\n```\r\n\r\npointing to [this line](https://github.com/bids-standard/pybids/blob/e92d4e8d058fc0e1797c8882dd98024068a303bf/bids/variables/entities.py#L245).\r\n\r\nShould I submit a PR to replace that, or are there reasons why this should not be done?\n", "before_files": [{"content": "\"\"\" Data classes for internal BIDS data hierarchy. \"\"\"\n\nfrom itertools import chain\nfrom collections import namedtuple\n\nimport pandas as pd\n\nfrom . import collections as clc\nfrom bids.utils import matches_entities\n\n\nclass Node(object):\n    \"\"\"Base class for objects that represent a single object in the BIDS\n    hierarchy.\n\n    Parameters\n    ----------\n    id : int or str\n        A value uniquely identifying this node. Typically the\n        entity value extracted from the filename via layout.\n    \"\"\"\n\n    def __init__(self, level, entities):\n        self.level = level.lower()\n        self.entities = entities\n        self.variables = {}\n\n    def add_variable(self, variable):\n        \"\"\"Adds a BIDSVariable to the current Node's list.\n\n        Parameters\n        ----------\n        variable : BIDSVariable\n            The Variable to add to the list.\n        \"\"\"\n        self.variables[variable.name] = variable\n\n\nclass RunNode(Node):\n    \"\"\"Represents a single Run in a BIDS project.\n\n    Parameters\n    ----------\n    id : int\n        The index of the run.\n    entities : dict\n        Dictionary of entities for this Node.\n    image_file : str\n        The full path to the corresponding nifti image.\n    duration : float\n        Duration of the run, in seconds.\n    repetition_time : float\n        TR for the run.\n    task : str\n        The task name for this run.\n    \"\"\"\n\n    def __init__(self, entities, image_file, duration, repetition_time, n_vols):\n        self.image_file = image_file\n        self.duration = duration\n        self.repetition_time = repetition_time\n        self.n_vols = n_vols\n        super(RunNode, self).__init__('run', entities)\n\n    def get_info(self):\n        # Note: do not remove the dict() call! self.entities is a SQLAlchemy\n        # association_proxy mapping, and without the conversion, the connection\n        # to the DB persists, causing problems on Python 3.5 if we try to clone\n        # a RunInfo or any containing object.\n        entities = dict(self.entities)\n        return RunInfo(entities, self.duration,\n                       self.repetition_time, self.image_file, self.n_vols)\n\n\n# Stores key information for each Run.\nRunInfo_ = namedtuple('RunInfo', ['entities', 'duration', 'tr', 'image', 'n_vols'])\n\n\n# Wrap with class to provide docstring\nclass RunInfo(RunInfo_):\n    \"\"\" A namedtuple storing run-related information.\n\n    Properties include 'entities', 'duration', 'tr', and 'image', 'n_vols'.\n    \"\"\"\n    pass\n\n\nclass NodeIndex(object):\n    \"\"\"Represents the top level in a BIDS hierarchy. \"\"\"\n\n    def __init__(self):\n        super(NodeIndex, self).__init__()\n        self.index = pd.DataFrame()\n        self.nodes = []\n\n    def get_collections(self, unit, names=None, merge=False,\n                        sampling_rate=None, **entities):\n        \"\"\"Retrieve variable data for a specified level in the Dataset.\n\n        Parameters\n        ----------\n        unit : str\n            The unit of analysis to return variables for. Must be\n            one of 'run', 'session', 'subject', or 'dataset'.\n        names : list\n            Optional list of variables names to return. If\n            None, all available variables are returned.\n        merge : bool\n            If True, variables are merged across all observations\n            of the current unit. E.g., if unit='subject' and return_type=\n            'collection', variables from all subjects will be merged into a\n            single collection. If False, each observation is handled\n            separately, and the result is returned as a list.\n        sampling_rate : int or str\n            If unit='run', the sampling rate to\n            pass onto the returned BIDSRunVariableCollection.\n        entities : dict\n            Optional constraints used to limit what gets returned.\n\n        Returns\n        -------\n        A list of BIDSVariableCollections if merge=False; a single\n        BIDSVariableCollection if merge=True.\n        \"\"\"\n\n        nodes = self.get_nodes(unit, entities)\n        var_sets = []\n\n        for n in nodes:\n            var_set = list(n.variables.values())\n            var_set = [v for v in var_set if matches_entities(v, entities)]\n            if names is not None:\n                var_set = [v for v in var_set if v.name in names]\n            # Additional filtering on Variables past run level, because their\n            # contents are extracted from TSV files containing rows from\n            # multiple observations\n            if unit != 'run':\n                var_set = [v.filter(entities) for v in var_set]\n            var_sets.append(var_set)\n\n        if merge:\n            var_sets = [list(chain(*var_sets))]\n\n        results = []\n        for vs in var_sets:\n            if not vs:\n                continue\n            if unit == 'run':\n                vs = clc.BIDSRunVariableCollection(vs, sampling_rate)\n            else:\n                vs = clc.BIDSVariableCollection(vs)\n            results.append(vs)\n\n        if merge:\n            return results[0] if results else None\n\n        return results\n\n    def get_nodes(self, level=None, entities=None, strict=False):\n        \"\"\"Retrieves all nodes that match the specified criteria.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of nodes to return.\n        entities : dict\n            Entities to filter on. All nodes must have\n            matching values on all defined keys to be included.\n        strict : bool\n            If True, an exception will be raised if the entities\n            dict contains any keys that aren't contained in the current\n            index.\n\n        Returns\n        -------\n        A list of Node instances.\n        \"\"\"\n\n        entities = {} if entities is None else entities.copy()\n\n        if level is not None:\n            entities['level'] = level\n\n        if entities is None:\n            return self.nodes\n\n        match_ents = set(entities.keys())\n        common_cols = list(match_ents & set(self.index.columns))\n\n        if strict and match_ents - common_cols:\n            raise ValueError(\"Invalid entities: \", match_ents - common_cols)\n\n        if not common_cols:\n            return self.nodes\n\n        # Construct query string that handles both single values and iterables\n        query = []\n        for col in common_cols:\n            oper = 'in' if isinstance(entities[col], (list, tuple)) else '=='\n            q = '{name} {oper} {val}'.format(name=col, oper=oper,\n                                             val=repr(entities[col]))\n            query.append(q)\n        query = ' and '.join(query)\n\n        rows = self.index.query(query)\n        if rows.empty:\n            return []\n\n        # Sort and return\n        sort_cols = ['subject', 'session', 'task', 'run', 'node_index',\n                     'suffix', 'level', 'datatype']\n        sort_cols = [sc for sc in sort_cols if sc in set(rows.columns)]\n        rows = rows.sort_values(sort_cols)\n        inds = rows['node_index'].astype(int)\n        return [self.nodes[i] for i in inds]\n\n    def create_node(self, level, entities, *args, **kwargs):\n        \"\"\"Creates a new child Node.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of the new Node.\n        entities : dict\n            Dictionary of entities belonging to Node.\n        args, kwargs : dict\n            Optional positional or named arguments to pass on to\n            class-specific initializers. These arguments are only used if\n            a Node that matches the passed entities doesn't already exist,\n            and a new one must be created.\n\n        Returns\n        -------\n        A Node instance.\n        \"\"\"\n\n        if level == 'run':\n            node = RunNode(entities, *args, **kwargs)\n        else:\n            node = Node(level, entities)\n\n        entities = dict(entities, node_index=len(self.nodes), level=level)\n        self.nodes.append(node)\n        node_row = pd.Series(entities)\n        self.index = self.index.append(node_row, ignore_index=True)\n        return node\n\n    def get_or_create_node(self, level, entities, *args, **kwargs):\n        \"\"\"Retrieves a child Node based on the specified criteria, creating a\n        new Node if necessary.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of the Node.\n        entities : dict\n            Dictionary of entities to include in newly-created\n            Nodes or filter existing ones.\n        args, kwargs : dict\n            Optional positional or named arguments to pass on to\n            class-specific initializers. These arguments are only used if\n            a Node that matches the passed entities doesn't already exist,\n            and a new one must be created.\n\n        Returns\n        -------\n        A Node instance.\n        \"\"\"\n\n        result = self.get_nodes(level, entities)\n\n        if result:\n            if len(result) > 1:\n                raise ValueError(\"More than one matching Node found! If you're\"\n                                 \" expecting more than one Node, use \"\n                                 \"get_nodes() instead of get_or_create_node().\"\n                                 )\n            return result[0]\n\n        return self.create_node(level, entities, *args, **kwargs)\n", "path": "bids/variables/entities.py"}], "after_files": [{"content": "\"\"\" Data classes for internal BIDS data hierarchy. \"\"\"\n\nfrom itertools import chain\nfrom collections import namedtuple\n\nimport pandas as pd\n\nfrom . import collections as clc\nfrom bids.utils import matches_entities\n\n\nclass Node(object):\n    \"\"\"Base class for objects that represent a single object in the BIDS\n    hierarchy.\n\n    Parameters\n    ----------\n    id : int or str\n        A value uniquely identifying this node. Typically the\n        entity value extracted from the filename via layout.\n    \"\"\"\n\n    def __init__(self, level, entities):\n        self.level = level.lower()\n        self.entities = entities\n        self.variables = {}\n\n    def add_variable(self, variable):\n        \"\"\"Adds a BIDSVariable to the current Node's list.\n\n        Parameters\n        ----------\n        variable : BIDSVariable\n            The Variable to add to the list.\n        \"\"\"\n        self.variables[variable.name] = variable\n\n\nclass RunNode(Node):\n    \"\"\"Represents a single Run in a BIDS project.\n\n    Parameters\n    ----------\n    id : int\n        The index of the run.\n    entities : dict\n        Dictionary of entities for this Node.\n    image_file : str\n        The full path to the corresponding nifti image.\n    duration : float\n        Duration of the run, in seconds.\n    repetition_time : float\n        TR for the run.\n    task : str\n        The task name for this run.\n    \"\"\"\n\n    def __init__(self, entities, image_file, duration, repetition_time, n_vols):\n        self.image_file = image_file\n        self.duration = duration\n        self.repetition_time = repetition_time\n        self.n_vols = n_vols\n        super(RunNode, self).__init__('run', entities)\n\n    def get_info(self):\n        # Note: do not remove the dict() call! self.entities is a SQLAlchemy\n        # association_proxy mapping, and without the conversion, the connection\n        # to the DB persists, causing problems on Python 3.5 if we try to clone\n        # a RunInfo or any containing object.\n        entities = dict(self.entities)\n        return RunInfo(entities, self.duration,\n                       self.repetition_time, self.image_file, self.n_vols)\n\n\n# Stores key information for each Run.\nRunInfo_ = namedtuple('RunInfo', ['entities', 'duration', 'tr', 'image', 'n_vols'])\n\n\n# Wrap with class to provide docstring\nclass RunInfo(RunInfo_):\n    \"\"\" A namedtuple storing run-related information.\n\n    Properties include 'entities', 'duration', 'tr', and 'image', 'n_vols'.\n    \"\"\"\n    pass\n\n\nclass NodeIndex(object):\n    \"\"\"Represents the top level in a BIDS hierarchy. \"\"\"\n\n    def __init__(self):\n        super(NodeIndex, self).__init__()\n        self.index = pd.DataFrame()\n        self.nodes = []\n\n    def get_collections(self, unit, names=None, merge=False,\n                        sampling_rate=None, **entities):\n        \"\"\"Retrieve variable data for a specified level in the Dataset.\n\n        Parameters\n        ----------\n        unit : str\n            The unit of analysis to return variables for. Must be\n            one of 'run', 'session', 'subject', or 'dataset'.\n        names : list\n            Optional list of variables names to return. If\n            None, all available variables are returned.\n        merge : bool\n            If True, variables are merged across all observations\n            of the current unit. E.g., if unit='subject' and return_type=\n            'collection', variables from all subjects will be merged into a\n            single collection. If False, each observation is handled\n            separately, and the result is returned as a list.\n        sampling_rate : int or str\n            If unit='run', the sampling rate to\n            pass onto the returned BIDSRunVariableCollection.\n        entities : dict\n            Optional constraints used to limit what gets returned.\n\n        Returns\n        -------\n        A list of BIDSVariableCollections if merge=False; a single\n        BIDSVariableCollection if merge=True.\n        \"\"\"\n\n        nodes = self.get_nodes(unit, entities)\n        var_sets = []\n\n        for n in nodes:\n            var_set = list(n.variables.values())\n            var_set = [v for v in var_set if matches_entities(v, entities)]\n            if names is not None:\n                var_set = [v for v in var_set if v.name in names]\n            # Additional filtering on Variables past run level, because their\n            # contents are extracted from TSV files containing rows from\n            # multiple observations\n            if unit != 'run':\n                var_set = [v.filter(entities) for v in var_set]\n            var_sets.append(var_set)\n\n        if merge:\n            var_sets = [list(chain(*var_sets))]\n\n        results = []\n        for vs in var_sets:\n            if not vs:\n                continue\n            if unit == 'run':\n                vs = clc.BIDSRunVariableCollection(vs, sampling_rate)\n            else:\n                vs = clc.BIDSVariableCollection(vs)\n            results.append(vs)\n\n        if merge:\n            return results[0] if results else None\n\n        return results\n\n    def get_nodes(self, level=None, entities=None, strict=False):\n        \"\"\"Retrieves all nodes that match the specified criteria.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of nodes to return.\n        entities : dict\n            Entities to filter on. All nodes must have\n            matching values on all defined keys to be included.\n        strict : bool\n            If True, an exception will be raised if the entities\n            dict contains any keys that aren't contained in the current\n            index.\n\n        Returns\n        -------\n        A list of Node instances.\n        \"\"\"\n\n        entities = {} if entities is None else entities.copy()\n\n        if level is not None:\n            entities['level'] = level\n\n        if entities is None:\n            return self.nodes\n\n        match_ents = set(entities.keys())\n        common_cols = list(match_ents & set(self.index.columns))\n\n        if strict and match_ents - common_cols:\n            raise ValueError(\"Invalid entities: \", match_ents - common_cols)\n\n        if not common_cols:\n            return self.nodes\n\n        # Construct query string that handles both single values and iterables\n        query = []\n        for col in common_cols:\n            oper = 'in' if isinstance(entities[col], (list, tuple)) else '=='\n            q = '{name} {oper} {val}'.format(name=col, oper=oper,\n                                             val=repr(entities[col]))\n            query.append(q)\n        query = ' and '.join(query)\n\n        rows = self.index.query(query)\n        if rows.empty:\n            return []\n\n        # Sort and return\n        sort_cols = ['subject', 'session', 'task', 'run', 'node_index',\n                     'suffix', 'level', 'datatype']\n        sort_cols = [sc for sc in sort_cols if sc in set(rows.columns)]\n        rows = rows.sort_values(sort_cols)\n        inds = rows['node_index'].astype(int)\n        return [self.nodes[i] for i in inds]\n\n    def create_node(self, level, entities, *args, **kwargs):\n        \"\"\"Creates a new child Node.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of the new Node.\n        entities : dict\n            Dictionary of entities belonging to Node.\n        args, kwargs : dict\n            Optional positional or named arguments to pass on to\n            class-specific initializers. These arguments are only used if\n            a Node that matches the passed entities doesn't already exist,\n            and a new one must be created.\n\n        Returns\n        -------\n        A Node instance.\n        \"\"\"\n\n        if level == 'run':\n            node = RunNode(entities, *args, **kwargs)\n        else:\n            node = Node(level, entities)\n\n        entities = dict(entities, node_index=len(self.nodes), level=level)\n        self.nodes.append(node)\n        # Because \"entities\" may have non-scalar values (such as `SliceTiming`)\n        # we need to first create a Series to avoid expansion\n        # From here we can concatenate\n        node_row = pd.DataFrame(pd.Series(entities)).T\n        self.index = pd.concat([self.index, node_row], ignore_index=True)\n        return node\n\n    def get_or_create_node(self, level, entities, *args, **kwargs):\n        \"\"\"Retrieves a child Node based on the specified criteria, creating a\n        new Node if necessary.\n\n        Parameters\n        ----------\n        level : str\n            The level of analysis of the Node.\n        entities : dict\n            Dictionary of entities to include in newly-created\n            Nodes or filter existing ones.\n        args, kwargs : dict\n            Optional positional or named arguments to pass on to\n            class-specific initializers. These arguments are only used if\n            a Node that matches the passed entities doesn't already exist,\n            and a new one must be created.\n\n        Returns\n        -------\n        A Node instance.\n        \"\"\"\n\n        result = self.get_nodes(level, entities)\n\n        if result:\n            if len(result) > 1:\n                raise ValueError(\"More than one matching Node found! If you're\"\n                                 \" expecting more than one Node, use \"\n                                 \"get_nodes() instead of get_or_create_node().\"\n                                 )\n            return result[0]\n\n        return self.create_node(level, entities, *args, **kwargs)\n", "path": "bids/variables/entities.py"}]}