Repository: mne-tools/mne-bids
Pull request #1215 (2024-01-10 20:02:18)

Do not zero-pad indices in BIDSPath entities anymore; do not warn about prepending missing dots for extensions
PR Description
--------------

I often have subject ids like `1` and `2` and I don't always store them as strings. I think it would be nice if `mne_bids` just handled this to allow for integer subject ids, runs, sessions, acquisitions etc.

Merge checklist
---------------

Maintainer, please confirm the following before merging.
If applicable:

- [x] All comments are resolved
- [x] This is not your own PR
- [x] All CIs are happy
- [ ] PR title starts with [MRG]
- [ ] [whats_new.rst](https://github.com/mne-tools/mne-bids/blob/main/doc/whats_new.rst) is updated
- [x] New contributors have been added to [CITATION.cff](https://github.com/mne-tools/mne-bids/blob/main/CITATION.cff)
- [ ] PR description includes phrase "closes <#issue-number>"


alexrockhill: Hmmm it looks like this was by design that integers were excluded, there's even a test (failing in this case) that `subject=1` fails. I find this a bit annoying because I don't understand the need for IDs like `"01"` on modern computing systems, it seems like a design choice that some but not all people make that can lead to Y2K phenomena if for instance you end up collecting more than 100 or 1000 subjects when you started with a "00" or "000" system. Is there a rationale for blocking users from doing this? To me it seems overly pedantic.
alexrockhill: ![image](https://github.com/mne-tools/mne-bids/assets/13473576/1bf69a53-323b-41fd-b8be-98c8aa5f9fa2)

Given that it's the `"01"` way on the homepage of BIDS, I guess this is fairly used but I don't recall seeing an explanation of why you need to format them this way. With the key-value tags in BIDS, this seems superfluous and from an era when strings were hardcoded to a certain length which is a brittle design choice for long identifiers..
alexrockhill: Also if you pass `run=1` it automatically gets mapped to `"01"` which seems very odd and unexpected.
hoechenberger: I agree:

- we should accept integers and simply convert them to strings
- we should not prepend zeros (zfill)
- if a user wants to zfill, they can do this manually and pass the resulting string
alexrockhill: Nice, okay, I thought I was alone in thinking this was a bit silly and that everyone else labeled their subjects this way for a minute. If the consensus is that this is that the labels should be less constrained, I'm happy to add that to the PR.
hoechenberger: @alexrockhill We better double-check check the BIDS specs before we proceed here
Otherwise I'm +1
alexrockhill: https://github.com/bids-standard/bids-specification/discussions/1679
Remi-Gau: Note that in BIDS runs are indices (i.e must be a digit) and subjects, sessions (and most other entities) are labels and can be alphanumeric.

https://bids-specification.readthedocs.io/en/latest/appendices/entities.html#sub

https://bids-specification.readthedocs.io/en/latest/appendices/entities.html#run

This partly explains why you can assume:

run-01 --> 1

but not

sub-01 --> 1 because you could also have sub-1

Hope this helps a bit.
jasmainak: I think @Remi-Gau is on the right track. It's why we disabled integers so that users are explicit and don't mix `sub-01` and `sub-1`
hoechenberger: @alexrockhill I think if you pass strings, no alterations will take place. So this would be the easiest workaround I suppose?
alexrockhill: > I think @Remi-Gau is on the right track. It's why we disabled integers so that users are explicit and don't mix `sub-01` and `sub-1`

I understand the principle but I don't understand the use case. Who labels their subjects `1` and `01`?

My preference would be to allow integers and cast them to strings since `subject=1` and `subject="1"` seem like reasonable subject identifiers to me and would mean the same subject. Since it's just annoying in code to cast everything e.g. `subject=str(subject)` and so it seems convenient to do this within `mne_bids` to make it as usable as possible. In general, I will say the annoying thing about BIDS is that it can sometimes feel like death by a million paper cuts where when you try and use it you get caught up on a bunch of little things like this and each one alone is rather silly to complain about but the end result is that people think it's more of a pain than it is. For example, in this context, figuring out if each tag is an index or a label (i.e. accepts int or string), having to remember if `extension=".edf"` is right or  `extension="edf"` is right or having to use `extension` and `suffix` even though if you pass `datatype` to `read_raw_bids` so the file type is able to be inferred (I know internally it's this way because `BIDSPath` isn't just used for `read_raw_bids` but also modified to find sidecars but that doesn't really matter from an end-user perspective). Anyway, the point being, this is one of several pain points that I think, especially for `write_raw_bids` and `read_raw_bids` is pretty mission-critical for adoption so that people don't run into an error that isn't one that is essential to ensuring that the dataset contains all the necessary information.

The `run` one is just a bug, that seems pretty simple. You should either not be allowed to pass integers or `run=1` should be cast to `run="1"` but definitely not `run="01"`.
alexrockhill: I'd actually go further if it was entirely my design choice and I would match `subject=1` to `subject="01"` and only throw an error at run time if there are both `subject="01"` and `subject="1"`. To me, it seems way more practical and common of a use case to have incrementing subject indices and I think you'd actually want an error when you try and make `subject="1"` and `subject="01"` that seems like a situation that should not be allowed, it's too confusing.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mne-tools/mne-bids/pull/1215?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mne-tools) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`cd2354a`)](https://app.codecov.io/gh/mne-tools/mne-bids/commit/cd2354a1b418331b658cdea88636b79ce552e72c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mne-tools) 97.61% compared to head [(`fbd83e4`)](https://app.codecov.io/gh/mne-tools/mne-bids/pull/1215?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mne-tools) 97.52%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1215      +/-   ##
==========================================
- Coverage   97.61%   97.52%   -0.10%     
==========================================
  Files          40       40              
  Lines        8685     8675      -10     
==========================================
- Hits         8478     8460      -18     
- Misses        207      215       +8     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mne-tools/mne-bids/pull/1215?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mne-tools).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mne-tools).

alexrockhill: Just wanted to add, I'm not trying to force this PR through, this is just my vision and addresses what I perceive to be pain points in the use case. Happy to discuss or close if people don't want to discuss and to just keep things how they are.
hoechenberger: I agree our API is hard to use. 
jasmainak: I was just trying to explain what might have been a possible reason. It's important not to cause a regression bug. I'll let you figure out the best path forward with @hoechenberger !
agramfort: Allowing different ways to be valid means adding more magic to the code. You need to multiply test to support 1 or str 01 and also tests of if both are subjects etc. You create more corner cases in the code and complexity is exponential with the number of options. Any bad default you point out was maybe a natural default for someone with a different prior and this person did not complain here. I am not trying to force the status quo here but be mindful of these arguments. 

alexrockhill: Totally, I asked why it was that way so the explanation is very helpful!

I think this would be backward compatible except if someone upgrades in the middle of converting a dataset which they aren't supposed to do.

It will silently return `subject="01"`  if you have a dataset with `subject="01"` and `subject="1"` which I think is reasonable, again I hope this isn't a use case. But otherwise, this PR just takes integers and casts them to the BIDS recommendation which is the best of both worlds, I think (i.e. I just say `subject=1` but that gets translated to `subject="01"` per BIDS recommendation). This obviates the need for the user to even think about zero padding which I would argue they probably don't want to and just want things to work and give readable results.
alexrockhill: @agramfort agreed but there are only two cases so it seems manageable.
hoechenberger: I'm not decided whether I want to change anything :) I just wanted to say that I think our API is not very easy to use :)
sappelhoff: I agree that it's important to keep in mind what @agramfort said here: https://github.com/mne-tools/mne-bids/pull/1215#issuecomment-1887950194

If it were just me, I'd maybe improve the documentation about this, but I wouldn't start adding more "magic conversion" behind the scenes. The present behavior has been in place for a very long time and we rarely get issues about it (we did however, admittedly, get one about it not too long though: https://mne.discourse.group/t/remove-automatic-zero-padding-when-specifing-run-in-bidspath/7865/4?u=sappelhoff
hoechenberger: Also we kept on **removing** "magic" over the past years, because it would cause all sorts of problems
alexrockhill: My point was just that it makes sense to be consistent and have this magic everywhere (`subject` as well as `run`) if we want to enforce the recommendation or to have it nowhere.

Again, per the discourse use case comment/issue, people try and pass `int`s and by not handling it consistently, we're making them dive into these kinds of discussions zero padding etc. that are going to make people think BIDS is too much of a work-in-progress for _them_ to use currently.

So, if it's about removing magic, then maybe just a straight cast to string for integers with no modification.

I think people are going to pass integers though and throwing them an error doesn't seem helpful since the information is fundamentally there.
alexrockhill: So now all the magic is removed from `run` and `split` as well which solves the discourse issue but will be slightly backward incompatible because if someone has `run=1` in their code, they'll have to change it to `run="01"` to access an already-converted dataset.

To do (in follow-up PRs):
- [ ] Remove matches to sidecars in `read_raw_bids` so that it doesn't have to be over-specified
alexrockhill: I'm getting a bunch of `pydantic` import errors when I went to test `mne_bids_pipeline`...
alexrockhill: Could someone with a working version of `mne-bids-pipeline` test this and confirm that it doesn't break anything please?
larsoner: An easy way if you don't want to set it up yourself would be to open a PR to mne-bids-pipeline where you change `pyproject.toml` to tell it to install your branch of mne-bids:

https://github.com/mne-tools/mne-bids-pipeline/blob/main/pyproject.toml#L49

Something like `mne-bids[full] @ git+https://github.com/alexrockhill/mne-bids@int` rather than just `mne-bids[full]` should do it.
alexrockhill: Looks like it doesn't break anything in `mne-bids-pipeline` so looks good to go possibly
alexrockhill: LGTM?
hoechenberger: Unfortunately I currently don't have the tine to really think through the implications of this change, so I'd like to withhold my vote for now
alexrockhill: Oh I was under the impression we talked it through and came to an intermediate compromise everyone was happy with. I.e. remove magic zero padding for all fields (it was on run before) and allow int for all fields but just cast it to string. That includes no magic and changes current behavior in a small way: i.e. run=1 just goes to run='1' not run='01'.
alexrockhill: Also worth noting, I'm really not opinionated as to whether `subject/run=1` goes to `subject/run="01"` or `subject/run='1'`, I think both are fine, I just made this PR because I find `mne_bids` quite annoying that it doesn't accept `subject=1` (and `extension='edf'` not being allowed is also very annoying).
sappelhoff: I'll take some time and think through this within the next days, sorry for causing some frustration here.
alexrockhill: No worries, take your time, not frustrated by you, just a bit annoyed by how `mne_bids` works but that's what PRs are for!

Patch:
diff --git a/doc/whats_new.rst b/doc/whats_new.rst
index d87fb95c..07d32b09 100644
--- a/doc/whats_new.rst
+++ b/doc/whats_new.rst
@@ -21,6 +21,8 @@ The following authors contributed for the first time. Thank you so much! 🤩
 
 The following authors had contributed before. Thank you for sticking around! 🤘
 
+* `Alex Rockhill`_
+* `Eric Larson`_
 * `Laetitia Fesselier`_
 * `Richard Höchenberger`_
 * `Stefan Appelhoff`_
@@ -38,6 +40,10 @@ Detailed list of changes
 
 - The experimental support for running MNE-BIDS examples from your browser using Binder has
   been removed, by `Stefan Appelhoff`_ (:gh:`1202`)
+- MNE-BIDS will no longer zero-pad ("zfill") entity indices passed to :class:`~mne_bids.BIDSPath`.
+  For example, If ``run=1`` is passed to MNE-BIDS, it will no longer be silently auto-converted to ``run-01``, by `Alex Rockhill`_ (:gh:`1215`)
+- MNE-BIDS will no longer warn about missing leading punctuation marks for extensions passed :class:`~mne_bids.BIDSPath`.
+  For example, MNE-BIDS will now silently auto-convert ``edf`` to ```.edf``, by `Alex Rockhill`_ (:gh:`1215`)
 
 🛠 Requirements
 ^^^^^^^^^^^^^^^
@@ -52,7 +58,7 @@ Detailed list of changes
 ^^^^^^^^^^^^
 
 - The datatype in the dataframe returned by :func:`mne_bids.stats.count_events` is now
-  ``pandas.Int64Dtype`` instead of ``float64``.
+  ``pandas.Int64Dtype`` instead of ``float64``, by `Eric Larson`_ (:gh:`1227`)
 
 ⚕️ Code health
 ^^^^^^^^^^^^^^
@@ -67,6 +73,8 @@ Detailed list of changes
   changed runtime behavior, by `Richard Höchenberger`_ (:gh:`1204`)
 - Display of the version number on the website is now truncated for over-long version strings,
   by `Daniel McCloy`_ (:gh:`1206`)
+- The long deprecated ``events_data`` parameter has been fully removed from
+  :func:`~mne_bids.write_raw_bids` in favor of ``events``, by `Stefan Appelhoff`_ (:gh:`1229`)
 
 :doc:`Find out what was new in previous releases <whats_new_previous_releases>`
 
diff --git a/mne_bids/commands/mne_bids_raw_to_bids.py b/mne_bids/commands/mne_bids_raw_to_bids.py
index 955cb8a4..b2fefe22 100644
--- a/mne_bids/commands/mne_bids_raw_to_bids.py
+++ b/mne_bids/commands/mne_bids_raw_to_bids.py
@@ -45,9 +45,6 @@ def run():
     parser.add_option(
         "--acq", dest="acq", help="acquisition parameter for this dataset"
     )
-    parser.add_option(
-        "--events_data", dest="events_data", help="Deprecated. Pass --events instead."
-    )
     parser.add_option("--events", dest="events", help="events file (events.tsv)")
     parser.add_option(
         "--event_id", dest="event_id", help="event id dict", metavar="eid"
@@ -120,7 +117,6 @@ def run():
         event_id=opt.event_id,
         events=opt.events,
         overwrite=opt.overwrite,
-        events_data=opt.events_data,
         verbose=True,
     )
 
diff --git a/mne_bids/path.py b/mne_bids/path.py
index d05f01b4..39e6d5bb 100644
--- a/mne_bids/path.py
+++ b/mne_bids/path.py
@@ -966,24 +966,12 @@ class BIDSPath:
                 if isinstance(val, str) and not val.isdigit():
                     raise ValueError(f"{key} is not an index (Got {val})")
                 elif isinstance(val, int):
-                    kwargs[key] = f"{val:02}"
+                    kwargs[key] = f"{val}"
 
         # ensure extension starts with a '.'
         extension = kwargs.get("extension")
         if extension is not None and not extension.startswith("."):
-            warn(
-                f'extension should start with a period ".", but got: '
-                f'"{extension}". Prepending "." to form: ".{extension}". '
-                f"This will raise an exception starting with MNE-BIDS 0.12.",
-                category=FutureWarning,
-            )
             kwargs["extension"] = f".{extension}"
-            # Uncomment in 0.12, and remove above code:
-            #
-            # raise ValueError(
-            #     f'Extension must start wie a period ".", but got: '
-            #     f'{extension}'
-            # )
         del extension
 
         # error check entities
diff --git a/mne_bids/write.py b/mne_bids/write.py
index 9799a075..c42d0179 100644
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -1368,7 +1368,6 @@ def write_raw_bids(
     montage=None,
     acpc_aligned=False,
     overwrite=False,
-    events_data=None,
     verbose=None,
 ):
     """Save raw data to a BIDS-compliant folder structure.
@@ -1551,9 +1550,6 @@ def write_raw_bids(
         and ``participants.tsv`` by a user will be retained.
         If ``False``, no existing data will be overwritten or
         replaced.
-    events_data
-        .. deprecated:: 0.11
-           Use ``events`` instead.
 
     %(verbose)s
 
@@ -1614,21 +1610,6 @@ def write_raw_bids(
     mne.events_from_annotations
 
     """
-    if events_data is not None and events is not None:
-        raise ValueError("Only one of events and events_data can be passed.")
-
-    if events_data is not None:
-        warn(
-            message="The events_data parameter has been deprecated in favor "
-            "the new events parameter, to ensure better consistency  "
-            "with MNE-Python. The events_data parameter will be "
-            "removed in MNE-BIDS 0.14. Please use the events "
-            "parameter instead.",
-            category=FutureWarning,
-        )
-        events = events_data
-        del events_data
-
     if not isinstance(raw, BaseRaw):
         raise ValueError(
             "raw_file must be an instance of BaseRaw, " "got %s" % type(raw)


Test patch:
diff --git a/mne_bids/tests/test_path.py b/mne_bids/tests/test_path.py
index dedf69d6..efe449c7 100644
--- a/mne_bids/tests/test_path.py
+++ b/mne_bids/tests/test_path.py
@@ -352,10 +352,10 @@ def test_parse_ext():
 @pytest.mark.parametrize(
     "fname",
     [
-        "sub-01_ses-02_task-test_run-3_split-01_meg.fif",
-        "sub-01_ses-02_task-test_run-3_split-01",
-        "/bids_root/sub-01/ses-02/meg/sub-01_ses-02_task-test_run-3_split-01_meg.fif",  # noqa: E501
-        "sub-01/ses-02/meg/sub-01_ses-02_task-test_run-3_split-01_meg.fif",
+        "sub-01_ses-02_task-test_run-3_split-1_meg.fif",
+        "sub-01_ses-02_task-test_run-3_split-1",
+        "/bids_root/sub-01/ses-02/meg/sub-01_ses-02_task-test_run-3_split-1_meg.fif",  # noqa: E501
+        "sub-01/ses-02/meg/sub-01_ses-02_task-test_run-3_split-1_meg.fif",
     ],
 )
 def test_get_bids_path_from_fname(fname):
@@ -377,12 +377,12 @@ def test_get_bids_path_from_fname(fname):
 @pytest.mark.parametrize(
     "fname",
     [
-        "sub-01_ses-02_task-test_run-3_split-01_desc-filtered_meg.fif",
-        "sub-01_ses-02_task-test_run-3_split-01_desc-filtered.fif",
-        "sub-01_ses-02_task-test_run-3_split-01_desc-filtered",
+        "sub-01_ses-02_task-test_run-3_split-1_desc-filtered_meg.fif",
+        "sub-01_ses-02_task-test_run-3_split-1_desc-filtered.fif",
+        "sub-01_ses-02_task-test_run-3_split-1_desc-filtered",
         (
             "/bids_root/sub-01/ses-02/meg/"
-            + "sub-01_ses-02_task-test_run-3_split-01_desc-filtered_meg.fif"
+            + "sub-01_ses-02_task-test_run-3_split-1_desc-filtered_meg.fif"
         ),
     ],
 )
@@ -394,7 +394,7 @@ def test_get_entities_from_fname(fname):
     assert params["run"] == "3"
     assert params["task"] == "test"
     assert params["description"] == "filtered"
-    assert params["split"] == "01"
+    assert params["split"] == "1"
     assert list(params.keys()) == [
         "subject",
         "session",
@@ -471,7 +471,7 @@ def test_get_entities_from_fname_errors(fname):
         # First candidate is disqualified (session doesn't match)
         (["sub-01_ses-01", "sub-01_ses-02"], ["sub-01_ses-02"]),
         # Multiple equally good candidates
-        (["sub-01_run-01", "sub-01_run-02"], ["sub-01_run-01", "sub-01_run-02"]),
+        (["sub-01_run-1", "sub-01_run-2"], ["sub-01_run-1", "sub-01_run-2"]),
     ],
 )
 def test_find_best_candidates(candidate_list, best_candidates):
@@ -796,7 +796,7 @@ def test_bids_path(return_bids_test_dir):
 
     # test that split gets properly set
     bids_path.update(split=1)
-    assert bids_path.basename == "sub-01_ses-02_task-03_split-01_ieeg.mat"
+    assert bids_path.basename == "sub-01_ses-02_task-03_split-1_ieeg.mat"
 
     # test home dir expansion
     bids_path = BIDSPath(root="~/foo")
@@ -858,7 +858,7 @@ def test_make_filenames():
         datatype="ieeg",
     )
     expected_str = (
-        "sub-one_ses-two_task-three_acq-four_run-01_proc-six_" "rec-seven_ieeg.json"
+        "sub-one_ses-two_task-three_acq-four_run-1_proc-six_" "rec-seven_ieeg.json"
     )
     assert BIDSPath(**prefix_data).basename == expected_str
     assert (
@@ -869,7 +869,7 @@ def test_make_filenames():
     # subsets of keys works
     assert (
         BIDSPath(subject="one", task="three", run=4).basename
-        == "sub-one_task-three_run-04"
+        == "sub-one_task-three_run-4"
     )
     assert (
         BIDSPath(subject="one", task="three", suffix="meg", extension=".json").basename
@@ -897,7 +897,7 @@ def test_make_filenames():
     basename = BIDSPath(**prefix_data, check=False)
     assert (
         basename.basename
-        == "sub-one_ses-two_task-three_acq-four_run-01_proc-six_rec-seven_ieeg.h5"
+        == "sub-one_ses-two_task-three_acq-four_run-1_proc-six_rec-seven_ieeg.h5"
     )  # noqa
 
     # what happens with scans.tsv file
@@ -960,7 +960,7 @@ def test_match(return_bids_test_dir):
     bids_path_01 = BIDSPath(root=bids_root, run="01")
     paths = bids_path_01.match()
     assert len(paths) == 3
-    assert paths[0].basename == ("sub-01_ses-01_task-testing_run-01_" "channels.tsv")
+    assert paths[0].basename == ("sub-01_ses-01_task-testing_run-01_channels.tsv")
 
     bids_path_01 = BIDSPath(root=bids_root, subject="unknown")
     paths = bids_path_01.match()
@@ -1474,12 +1474,6 @@ def test_setting_entities():
         assert getattr(bids_path, entity_name) is None
 
 
-def test_deprecation():
-    """Test deprecated behavior."""
-    with pytest.warns(FutureWarning, match="This will raise an exception"):
-        BIDSPath(extension="vhdr")  # no leading period
-
-
 def test_dont_create_dirs_on_fpath_access(tmp_path):
     """Regression test: don't create directories when accessing .fpath."""
     bp = BIDSPath(subject="01", datatype="eeg", root=tmp_path)
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
index fc7830bb..92c601fc 100644
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -4051,44 +4051,6 @@ def test_repeat_write_location(tmpdir):
         write_raw_bids(raw, bids_path, overwrite=True, verbose=False)
 
 
-@testing.requires_testing_data
-def test_events_data_deprecation(tmp_path):
-    """Test that passing events_data raises a FutureWarning."""
-    bids_root = tmp_path / "bids"
-    bids_path = _bids_path.copy().update(root=bids_root)
-    raw_path = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif"
-    events_path = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw-eve.fif"
-    event_id = {
-        "Auditory/Left": 1,
-        "Auditory/Right": 2,
-        "Visual/Left": 3,
-        "Visual/Right": 4,
-        "Smiley": 5,
-        "Button": 32,
-    }
-
-    # Drop unknown events.
-    events = mne.read_events(events_path)
-    events = events[events[:, 2] != 0]
-
-    raw = _read_raw_fif(raw_path)
-    with pytest.warns(FutureWarning, match="will be removed"):
-        write_raw_bids(
-            raw=raw, bids_path=bids_path, events_data=events, event_id=event_id
-        )
-
-    with pytest.raises(
-        ValueError, match="Only one of events and events_data can be passed"
-    ):
-        write_raw_bids(
-            raw=raw,
-            bids_path=bids_path,
-            events=events,
-            events_data=events,
-            event_id=event_id,
-        )
-
-
 @testing.requires_testing_data
 def test_unknown_extension(_bids_validate, tmp_path):
     """Write data with unknown extension to BIDS."""