Skip to content

brain_image_library

Raised when the Brain Image Library API returns an error.

Source code in navis/interfaces/brain_image_library.py
85
86
class BILError(Exception):
    """Raised when the Brain Image Library API returns an error."""

Clear the in-process metadata cache.

Source code in navis/interfaces/brain_image_library.py
351
352
353
def clear_metadata_cache() -> None:
    """Clear the in-process metadata cache."""
    _META_CACHE.clear()

Download files from a BIL dataset.

PARAMETER DESCRIPTION
x
        Dataset ID ("bildid"), or the output of
        [`navis.interfaces.brain_image_library.list_files`][] - in
        which case exactly those files are downloaded.

TYPE: str | pandas.DataFrame

filepath
        Directory to download to. The dataset's directory structure
        is preserved underneath it.

TYPE: str | pathlib.Path

pattern
        Only download files matching this glob, e.g. `"*.swc"`.

TYPE: str DEFAULT: None

max_size
        Refuse to download more than this in total (e.g. `"10G"`).
        BIL hosts datasets of hundreds of terabytes - this is here
        to stop you pulling one by accident. Set to `None` to
        disable the check.

TYPE: str DEFAULT: DEFAULT_MAX_DOWNLOAD

skip_existing
        Skip files that already exist locally.

TYPE: bool DEFAULT: True

parallel
        Whether to download in parallel.

TYPE: bool DEFAULT: True

max_threads
        Max number of parallel threads to use.

TYPE: int DEFAULT: 4

**kwargs
        Passed to [`navis.interfaces.brain_image_library.list_files`][].

DEFAULT: {}

RETURNS DESCRIPTION
pandas.DataFrame

The file table with an added filepath column.

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> files = bil.download_files('ace-boo-van', '~/bil', pattern='*.swc')
Source code in navis/interfaces/brain_image_library.py
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
def download_files(
    x,
    filepath: Union[str, Path],
    pattern: Optional[str] = None,
    *,
    max_size: Optional[str] = DEFAULT_MAX_DOWNLOAD,
    skip_existing: bool = True,
    parallel: bool = True,
    max_threads: int = 4,
    **kwargs,
) -> pd.DataFrame:
    """Download files from a BIL dataset.

    Parameters
    ----------
    x :             str | pandas.DataFrame
                    Dataset ID ("bildid"), or the output of
                    [`navis.interfaces.brain_image_library.list_files`][] - in
                    which case exactly those files are downloaded.
    filepath :      str | pathlib.Path
                    Directory to download to. The dataset's directory structure
                    is preserved underneath it.
    pattern :       str, optional
                    Only download files matching this glob, e.g. `"*.swc"`.
    max_size :      str, optional
                    Refuse to download more than this in total (e.g. `"10G"`).
                    BIL hosts datasets of hundreds of terabytes - this is here
                    to stop you pulling one by accident. Set to `None` to
                    disable the check.
    skip_existing : bool
                    Skip files that already exist locally.
    parallel :      bool
                    Whether to download in parallel.
    max_threads :   int
                    Max number of parallel threads to use.
    **kwargs
                    Passed to [`navis.interfaces.brain_image_library.list_files`][].

    Returns
    -------
    pandas.DataFrame
                    The file table with an added `filepath` column.

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> files = bil.download_files('ace-boo-van', '~/bil', pattern='*.swc')   # doctest: +SKIP

    """
    files = x if _is_file_table(x) else list_files(x, pattern=pattern, **kwargs)

    if _is_file_table(x) and pattern:
        files = files[[fnmatch(n, pattern) for n in files.name]]

    if not len(files):
        raise ValueError("No files to download.")

    # Guardrail. Note `list_files` may legitimately have been called with
    # `force=True`, so this is an independent check.
    total = files["size"].dropna().sum()
    if max_size is not None:
        limit = _parse_size(max_size)
        if limit is None:
            raise ValueError(f"Could not parse `max_size='{max_size}'`.")
        if total > limit:
            raise ValueError(
                f"Downloading these {len(files):,} files would fetch "
                f"~{utils.sizeof_fmt(total)}, which exceeds `max_size={max_size}`.\n"
                "Your options:\n"
                "  - narrow the selection with `pattern=...`\n"
                "  - raise `max_size` or set it to `None` to disable this check\n"
                f"  - {_HELP_GLOBUS}"
            )

    filepath = Path(filepath).expanduser()

    targets = []
    for row in files.itertuples():
        target = filepath / str(row.directory or "") / row.name
        targets.append(target)
    files = files.assign(filepath=targets)

    todo = [
        (row.url, Path(row.filepath), row.size)
        for row in files.itertuples()
        if not (skip_existing and Path(row.filepath).exists())
    ]

    n_skipped = len(files) - len(todo)
    if n_skipped:
        logger.info(f"Skipping {n_skipped} file(s) that already exist.")

    if todo:
        todo_bytes = sum(s for _, _, s in todo if not pd.isnull(s))
        with config.tqdm(
            desc="Downloading",
            total=todo_bytes if todo_bytes else None,
            unit="B",
            unit_scale=True,
            leave=config.pbar_leave,
            disable=config.pbar_hide,
        ) as pbar:
            with ThreadPoolExecutor(
                max_workers=1 if not parallel else max_threads
            ) as executor:
                futures = {
                    executor.submit(_download_file, url, target, pbar): url
                    for url, target, _ in todo
                }
                for f in as_completed(futures):
                    f.result()  # raise any exception

    return files

Return the download URL(s) for the given dataset(s).

PARAMETER DESCRIPTION
x
    Dataset ID(s) ("bildid").

TYPE: str | list of str | pandas.DataFrame

RETURNS DESCRIPTION
str

If a single dataset was requested.

list of str

If multiple datasets were requested.

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> bil.get_dataset_url('ace-boo-van')
'https://download.brainimagelibrary.org/22/5c/225c37cacfbd897c/'
Source code in navis/interfaces/brain_image_library.py
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
def get_dataset_url(x) -> Union[str, List[str]]:
    """Return the download URL(s) for the given dataset(s).

    Parameters
    ----------
    x :         str | list of str | pandas.DataFrame
                Dataset ID(s) ("bildid").

    Returns
    -------
    str
                If a single dataset was requested.
    list of str
                If multiple datasets were requested.

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> bil.get_dataset_url('ace-boo-van')
    'https://download.brainimagelibrary.org/22/5c/225c37cacfbd897c/'

    """
    ids = _extract_ids(x)
    meta = get_metadata(ids)

    urls = []
    for bildid, url in zip(meta.bildid, meta.url):
        if not url:
            raise BILError(
                f"Dataset '{bildid}' has no public download directory "
                "(it may be embargoed or not yet released)."
            )
        urls.append(url)

    return urls[0] if len(ids) == 1 else urls

Fetch metadata for one or more BIL datasets.

PARAMETER DESCRIPTION
x
        The dataset ID(s) ("bildid"). Can also be a DataFrame with
        a `bildid` column, e.g. the output of
        [`navis.interfaces.brain_image_library.search`][].

TYPE: str | list of str | pandas.DataFrame

raw
        If True, return the raw (nested) JSON records instead of a
        DataFrame. Use this to get at the `Assets`, `Contributors`
        and `Publication` divisions which do not survive
        flattening intact.

TYPE: bool DEFAULT: False

parallel
        Whether to fetch in parallel.

TYPE: bool DEFAULT: True

max_threads
        Max number of parallel threads to use.

TYPE: int DEFAULT: 4

chunk_size
        Number of datasets to request per call.

TYPE: int DEFAULT: 100

RETURNS DESCRIPTION
pandas.DataFrame

One row per dataset. Specimen entries are collapsed: a field is a scalar if all specimens agree and a tuple otherwise. n_specimens always tells you how many there actually were.

list of dict

If raw=True.

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> meta = bil.get_metadata('ace-boo-van')
>>> meta.title.values[0]
'Single neuron reconstruction from fMOST images'
Source code in navis/interfaces/brain_image_library.py
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def get_metadata(
    x,
    *,
    raw: bool = False,
    parallel: bool = True,
    max_threads: int = 4,
    chunk_size: int = 100,
) -> Union[pd.DataFrame, List[dict]]:
    """Fetch metadata for one or more BIL datasets.

    Parameters
    ----------
    x :             str | list of str | pandas.DataFrame
                    The dataset ID(s) ("bildid"). Can also be a DataFrame with
                    a `bildid` column, e.g. the output of
                    [`navis.interfaces.brain_image_library.search`][].
    raw :           bool
                    If True, return the raw (nested) JSON records instead of a
                    DataFrame. Use this to get at the `Assets`, `Contributors`
                    and `Publication` divisions which do not survive
                    flattening intact.
    parallel :      bool
                    Whether to fetch in parallel.
    max_threads :   int
                    Max number of parallel threads to use.
    chunk_size :    int
                    Number of datasets to request per call.

    Returns
    -------
    pandas.DataFrame
                    One row per dataset. `Specimen` entries are collapsed: a
                    field is a scalar if all specimens agree and a tuple
                    otherwise. `n_specimens` always tells you how many there
                    actually were.
    list of dict
                    If `raw=True`.

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> meta = bil.get_metadata('ace-boo-van')
    >>> meta.title.values[0]
    'Single neuron reconstruction from fMOST images'

    """
    ids = _extract_ids(x)
    if not ids:
        raise ValueError("Got no dataset IDs to fetch metadata for.")

    _retrieve(ids, parallel=parallel, max_threads=max_threads, chunk_size=chunk_size)

    records = [_META_CACHE[i] for i in ids if i in _META_CACHE]

    if not records:
        raise BILError(f"No metadata found for: {', '.join(ids)}")

    if raw:
        return records

    return pd.DataFrame.from_records([_flatten_record(r) for r in records])

Fetch neuron reconstructions from a BIL dataset.

Skeletons are streamed straight into memory - nothing is written to disk. Use navis.interfaces.brain_image_library.download_files if you want the files themselves.

PARAMETER DESCRIPTION
x
        Dataset ID ("bildid"), or the output of
        [`navis.interfaces.brain_image_library.list_files`][] - in
        which case exactly those files are loaded. The latter lets
        you inspect what you are about to fetch first.

TYPE: str | pandas.DataFrame

pattern
        Which files to read. Defaults to `"*.swc"`.

TYPE: str DEFAULT: '*.swc'

max_neurons
        Cap the number of neurons fetched.

TYPE: int DEFAULT: None

parallel
        Whether to fetch in parallel.

TYPE: bool DEFAULT: True

max_threads
        Max number of parallel threads to use.

TYPE: int DEFAULT: 4

**kwargs
        Passed to [`navis.read_swc`][]. Note that BIL does not
        reliably record the units of its reconstructions - pass
        e.g. `units='um'` if you know them. The `image.stepsizex`
        field in the metadata (e.g. "0.35 micron/pixel") tells you
        the voxel size if the coordinates are in voxels.

DEFAULT: {}

RETURNS DESCRIPTION
navis.NeuronList

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> # Look before you leap
>>> files = bil.list_files('ace-boo-van', pattern='*.swc')
>>> nl = bil.get_neurons(files, max_neurons=5)
>>> len(nl)
5
Source code in navis/interfaces/brain_image_library.py
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
def get_neurons(
    x,
    pattern: str = "*.swc",
    *,
    max_neurons: Optional[int] = None,
    parallel: bool = True,
    max_threads: int = 4,
    **kwargs,
) -> NeuronList:
    """Fetch neuron reconstructions from a BIL dataset.

    Skeletons are streamed straight into memory - nothing is written to disk.
    Use [`navis.interfaces.brain_image_library.download_files`][] if you want
    the files themselves.

    Parameters
    ----------
    x :             str | pandas.DataFrame
                    Dataset ID ("bildid"), or the output of
                    [`navis.interfaces.brain_image_library.list_files`][] - in
                    which case exactly those files are loaded. The latter lets
                    you inspect what you are about to fetch first.
    pattern :       str
                    Which files to read. Defaults to `"*.swc"`.
    max_neurons :   int, optional
                    Cap the number of neurons fetched.
    parallel :      bool
                    Whether to fetch in parallel.
    max_threads :   int
                    Max number of parallel threads to use.
    **kwargs
                    Passed to [`navis.read_swc`][]. Note that BIL does not
                    reliably record the units of its reconstructions - pass
                    e.g. `units='um'` if you know them. The `image.stepsizex`
                    field in the metadata (e.g. "0.35 micron/pixel") tells you
                    the voxel size if the coordinates are in voxels.

    Returns
    -------
    navis.NeuronList

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> # Look before you leap
    >>> files = bil.list_files('ace-boo-van', pattern='*.swc')
    >>> nl = bil.get_neurons(files, max_neurons=5)
    >>> len(nl)
    5

    """
    # Split off the kwargs meant for `list_files` - the rest go to `read_swc`
    list_kwargs = {k: kwargs.pop(k) for k in list(kwargs) if k in _LIST_FILES_KWARGS}

    # Note we deliberately crawl *without* the pattern and filter afterwards:
    # it costs no extra requests and means we can tell the user which file types
    # the dataset actually contains if nothing matches.
    available = x if _is_file_table(x) else list_files(x, **list_kwargs)
    files = available[[fnmatch(n, pattern) for n in available.name]]

    if not len(files):
        found = sorted({Path(n).suffix for n in available.name if Path(n).suffix})
        raise ValueError(
            f'No files matching "{pattern}" in this dataset.'
            + (f" File types present: {', '.join(found)}." if found else "")
            + " Note that BIL cell morphology datasets often ship Vaa3D "
            "reconstructions (.eswc, .ano, .apo) instead of plain SWC - navis "
            "cannot read those directly."
        )

    if max_neurons and len(files) > max_neurons:
        logger.warning(
            f"Restricting to the first {max_neurons} of {len(files)} files."
        )
        files = files.iloc[:max_neurons]

    urls = list(files.url)

    # Pass `max_threads` explicitly so that the caller controls the number of
    # threads we hit the BIL servers with. `read_swc` takes care of `name` and
    # `origin` (it parses them from the URL).
    nl = read_swc(urls, parallel=max_threads if parallel else False, **kwargs)

    # A dataset is one cell more often than not, so a bare file name is rarely
    # unique across datasets. Give each neuron a composite id and keep the
    # dataset it came from - neither is something navis can derive itself.
    # Order is preserved, but check to be safe.
    if len(nl) == len(files):
        for neuron, row in zip(nl, files.itertuples()):
            neuron.id = f"{row.bildid}/{Path(row.name).stem}"
            neuron.bildid = row.bildid
    else:
        logger.warning(
            "Could not match neurons back to their source files - `id` and "
            "`bildid` may be missing."
        )

    return nl

List the files in a BIL dataset.

This works by crawling the dataset's directory listing. Note that BIL hosts datasets with over a million files - see the guardrails below.

PARAMETER DESCRIPTION
x
        Dataset ID(s) ("bildid"), or a DataFrame with a `bildid`
        column. Note that BIL often splits a collection into one
        small dataset per cell, so listing several at once is a
        perfectly normal thing to do.

TYPE: str | list of str | pandas.DataFrame

pattern
        Only return files matching this glob, e.g. `"*.swc"`.
        Note this filters the *results*: we still have to visit
        every directory to see what is in it, so a pattern does
        not make crawling a huge dataset any cheaper.

TYPE: str DEFAULT: None

recursive
        Whether to descend into subdirectories.

TYPE: bool DEFAULT: True

max_depth
        How deep to descend. BIL layouts are shallow.

TYPE: int DEFAULT: 3

max_files
        Stop after this many files (per dataset).

TYPE: int DEFAULT: 10000

max_requests
        Stop after this many directory listings (per dataset).

TYPE: int DEFAULT: 500

force
        BIL hosts datasets of hundreds of terabytes and millions of
        files. By default we refuse to crawl anything larger than
        `SIZE_WARN_GB`/`FILE_WARN_COUNT`. Set True to override.

TYPE: bool DEFAULT: False

parallel
        Whether to crawl in parallel.

TYPE: bool DEFAULT: True

max_threads
        Max number of parallel threads to use.

TYPE: int DEFAULT: 4

RETURNS DESCRIPTION
pandas.DataFrame

Columns: name, url, size (bytes), last_modified, directory, depth and bildid. Datasets are returned in the order they were requested. Feed this straight into navis.interfaces.brain_image_library.get_neurons or navis.interfaces.brain_image_library.download_files.

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> files = bil.list_files('ace-boo-van', pattern='*.swc')
Source code in navis/interfaces/brain_image_library.py
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
def list_files(
    x,
    pattern: Optional[str] = None,
    *,
    recursive: bool = True,
    max_depth: int = 3,
    max_files: int = 10_000,
    max_requests: int = 500,
    force: bool = False,
    parallel: bool = True,
    max_threads: int = 4,
) -> pd.DataFrame:
    """List the files in a BIL dataset.

    This works by crawling the dataset's directory listing. Note that BIL
    hosts datasets with over a million files - see the guardrails below.

    Parameters
    ----------
    x :             str | list of str | pandas.DataFrame
                    Dataset ID(s) ("bildid"), or a DataFrame with a `bildid`
                    column. Note that BIL often splits a collection into one
                    small dataset per cell, so listing several at once is a
                    perfectly normal thing to do.
    pattern :       str, optional
                    Only return files matching this glob, e.g. `"*.swc"`.
                    Note this filters the *results*: we still have to visit
                    every directory to see what is in it, so a pattern does
                    not make crawling a huge dataset any cheaper.
    recursive :     bool
                    Whether to descend into subdirectories.
    max_depth :     int
                    How deep to descend. BIL layouts are shallow.
    max_files :     int
                    Stop after this many files (per dataset).
    max_requests :  int
                    Stop after this many directory listings (per dataset).
    force :         bool
                    BIL hosts datasets of hundreds of terabytes and millions of
                    files. By default we refuse to crawl anything larger than
                    `SIZE_WARN_GB`/`FILE_WARN_COUNT`. Set True to override.
    parallel :      bool
                    Whether to crawl in parallel.
    max_threads :   int
                    Max number of parallel threads to use.

    Returns
    -------
    pandas.DataFrame
                    Columns: `name`, `url`, `size` (bytes), `last_modified`,
                    `directory`, `depth` and `bildid`. Datasets are returned in
                    the order they were requested. Feed this straight into
                    [`navis.interfaces.brain_image_library.get_neurons`][] or
                    [`navis.interfaces.brain_image_library.download_files`][].

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> files = bil.list_files('ace-boo-van', pattern='*.swc')

    """
    ids = _extract_ids(x)
    if not ids:
        raise ValueError("Got no dataset IDs to list files for.")

    # Fetch metadata for all datasets up front (one round-trip) so that the
    # per-dataset crawls below hit the cache.
    get_metadata(ids, parallel=parallel, max_threads=max_threads)

    frames = []
    with config.tqdm(
        desc="Listing datasets",
        total=len(ids),
        leave=config.pbar_leave,
        disable=len(ids) == 1 or config.pbar_hide,
    ) as pbar:
        for bildid in ids:
            frames.append(
                _list_files_single(
                    bildid,
                    pattern=pattern,
                    recursive=recursive,
                    max_depth=max_depth,
                    max_files=max_files,
                    max_requests=max_requests,
                    force=force,
                    parallel=parallel,
                    max_threads=max_threads,
                )
            )
            pbar.update(1)

    return pd.concat(frames, ignore_index=True)

Run a single raw query against the BIL metadata API.

This is a low-level escape hatch for division/element combinations not covered by navis.interfaces.brain_image_library.search. No validation is performed on division/element.

PARAMETER DESCRIPTION
division
    The metadata division, e.g. "dataset" or "specimen".

TYPE: str

element
    The metadata element to search, e.g. "generalmodality".

TYPE: str

value
    The value to search for. Note that BIL matches values
    *exactly* - there is no substring or fuzzy matching.

TYPE: str

RETURNS DESCRIPTION
list of str

The IDs ("bildids") of matching datasets. Empty if no match.

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> ids = bil.query('dataset', 'generalmodality', 'cell morphology')
Source code in navis/interfaces/brain_image_library.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def query(division: str, element: str, value: str) -> List[str]:
    """Run a single raw query against the BIL metadata API.

    This is a low-level escape hatch for division/element combinations not
    covered by [`navis.interfaces.brain_image_library.search`][]. No validation
    is performed on `division`/`element`.

    Parameters
    ----------
    division :  str
                The metadata division, e.g. "dataset" or "specimen".
    element :   str
                The metadata element to search, e.g. "generalmodality".
    value :     str
                The value to search for. Note that BIL matches values
                *exactly* - there is no substring or fuzzy matching.

    Returns
    -------
    list of str
                The IDs ("bildids") of matching datasets. Empty if no match.

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> ids = bil.query('dataset', 'generalmodality', 'cell morphology')

    """
    url = utils.make_url(BASEURL, "query", *str(division).split("/"), **{element: value})
    return list(_get_json(url).get("bildids", []))

Search BIL for datasets matching the given criteria.

PARAMETER DESCRIPTION
limit
        Cap the number of datasets returned.

TYPE: int DEFAULT: None

metadata
        If True (default), fetch full metadata for the hits and
        return it as a DataFrame. If False, return just the IDs -
        much faster if you only need the IDs.

TYPE: bool DEFAULT: True

parallel
        Whether to run the individual queries in parallel.

TYPE: bool DEFAULT: True

max_threads
        Max number of parallel threads to use.

TYPE: int DEFAULT: 4

**filters
        Search criteria as `field=value`. See
        [`navis.interfaces.brain_image_library.FIELDS`][] for the
        available fields.

        Note the semantics:

        - across fields the filters are combined with **AND**
        - within a field, multiple values are combined with **OR**
          (e.g. `species=['mouse', 'rat']`)

        Values must match **exactly** - BIL does no substring or
        fuzzy matching. Use `text=...` for a full-text search.

TYPE: Union[str, Iterable[str]] DEFAULT: {}

RETURNS DESCRIPTION
pandas.DataFrame

Examples:

>>> import navis.interfaces.brain_image_library as bil
>>> # Find mouse single-neuron reconstructions
>>> ds = bil.search(species='mouse', generalmodality='cell morphology')
>>> # Full-text search
>>> ds = bil.search(text='barrel cortex')

Note that class is a Python keyword and hence can't be used as a keyword argument. Pass it as a dict instead:

>>> ds = bil.search(**{'class': 'somevalue'})
Source code in navis/interfaces/brain_image_library.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def search(
    *,
    limit: Optional[int] = None,
    metadata: bool = True,
    parallel: bool = True,
    max_threads: int = 4,
    **filters: Union[str, Iterable[str]],
) -> pd.DataFrame:
    """Search BIL for datasets matching the given criteria.

    Parameters
    ----------
    limit :         int, optional
                    Cap the number of datasets returned.
    metadata :      bool
                    If True (default), fetch full metadata for the hits and
                    return it as a DataFrame. If False, return just the IDs -
                    much faster if you only need the IDs.
    parallel :      bool
                    Whether to run the individual queries in parallel.
    max_threads :   int
                    Max number of parallel threads to use.
    **filters
                    Search criteria as `field=value`. See
                    [`navis.interfaces.brain_image_library.FIELDS`][] for the
                    available fields.

                    Note the semantics:

                    - across fields the filters are combined with **AND**
                    - within a field, multiple values are combined with **OR**
                      (e.g. `species=['mouse', 'rat']`)

                    Values must match **exactly** - BIL does no substring or
                    fuzzy matching. Use `text=...` for a full-text search.

    Returns
    -------
    pandas.DataFrame
                    One row per dataset. Feed this straight into
                    [`navis.interfaces.brain_image_library.get_neurons`][] or
                    [`navis.interfaces.brain_image_library.list_files`][].

    Examples
    --------
    >>> import navis.interfaces.brain_image_library as bil
    >>> # Find mouse single-neuron reconstructions
    >>> ds = bil.search(species='mouse', generalmodality='cell morphology')
    >>> # Full-text search
    >>> ds = bil.search(text='barrel cortex')

    Note that `class` is a Python keyword and hence can't be used as a keyword
    argument. Pass it as a dict instead:

    >>> ds = bil.search(**{'class': 'somevalue'})                # doctest: +SKIP

    """
    if not filters:
        raise ValueError(
            "`search` requires at least one filter, e.g. `search(species='mouse')`. "
            "See `brain_image_library.FIELDS` for available fields."
        )

    _check_fields(filters)

    # Flatten to a list of (division, element, value) jobs
    jobs = [
        (FIELDS[field], field, value)
        for field, values in filters.items()
        for value in utils.make_iterable(values)
    ]

    # OR within a field, AND across fields
    per_field: Dict[str, set] = {f: set() for f in filters}
    with ThreadPoolExecutor(max_workers=1 if not parallel else max_threads) as executor:
        futures = {executor.submit(query, div, el, val): el for div, el, val in jobs}
        with config.tqdm(
            desc="Querying",
            total=len(futures),
            leave=config.pbar_leave,
            disable=len(futures) == 1 or config.pbar_hide,
        ) as pbar:
            for f in as_completed(futures):
                pbar.update(1)
                per_field[futures[f]] |= set(f.result())

    ids = sorted(set.intersection(*per_field.values()))

    if not ids:
        logger.warning(
            "No datasets matched. Note that BIL matches values *exactly* - "
            "check spelling and capitalisation, or try a full-text search "
            "via `search(text=...)`."
        )
        return pd.DataFrame(columns=["bildid"])

    if limit:
        ids = ids[:limit]

    if not metadata:
        return pd.DataFrame({"bildid": ids})

    return get_metadata(ids, parallel=parallel, max_threads=max_threads)