Repository: mne-tools/mne-bids

Issue:
write_raw_bids() shouldn't require info['line_freq']
According to the [BIDS specs](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/03-electroencephalography.html#sidecar-json-_eegjson), `PowerLineFrequency` may also be `'n/a'`. This should be our default if `raw.info['line_freq']` has not been set. Currently, we'd raise an exception:

```python
ValueError: PowerLineFrequency parameter is required in the sidecar files. 
Please specify it in info['line_freq'] before saving to BIDS (e.g.
raw.info['line_freq'] = 60 or with --line_freq option in the command line.).
```

We should only raise a warning in this case.

Thoughts?
write_raw_bids() shouldn't require info['line_freq']
According to the [BIDS specs](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/03-electroencephalography.html#sidecar-json-_eegjson), `PowerLineFrequency` may also be `'n/a'`. This should be our default if `raw.info['line_freq']` has not been set. Currently, we'd raise an exception:

```python
ValueError: PowerLineFrequency parameter is required in the sidecar files. 
Please specify it in info['line_freq'] before saving to BIDS (e.g.
raw.info['line_freq'] = 60 or with --line_freq option in the command line.).
```

We should only raise a warning in this case.

Thoughts?


Discussion:
Can users do `raw.info['line_freq'] = "n/a"`? If yes, we could add that to the warning string.

I think prodding the user to set some values in their raw files is good, because it results in better BIDS datasets.
raw.info['line_freq'] = None

maybe as it should be a float (it cannot be a string for sure)

>

> I think prodding the user to set some values in their raw files is good, because it results in better BIDS datasets.

Yeah but … then we should also enforce `EEGReference` and a couple others we currently simply set to `'n/a'` by default…
If we **could** do something like `raw.info["EEGReference"] = "electrode placed 1cm above FCz location foo bar"`, then I would vote to use that feature and force users to set it prior to passing `raw` to mne-bids.

But we don't have that for EEGReference.

We **do** have it for LineFrequency --> So I suggest to use it. Ask users to add one line to their loop in their script:

`raw.info['line_freq'] = None`

that's not too much to ask IMHO. And if it's `None`, or `np.nan` (whatever is possible), then we set it to `n/a` in BIDS


EDIT: going back to my original suggestion --> we just have to make a better error msg, so that users know they can set it to None in case they don't know it.
Can users do `raw.info['line_freq'] = "n/a"`? If yes, we could add that to the warning string.

I think prodding the user to set some values in their raw files is good, because it results in better BIDS datasets.
raw.info['line_freq'] = None

maybe as it should be a float (it cannot be a string for sure)

>

> I think prodding the user to set some values in their raw files is good, because it results in better BIDS datasets.

Yeah but … then we should also enforce `EEGReference` and a couple others we currently simply set to `'n/a'` by default…
If we **could** do something like `raw.info["EEGReference"] = "electrode placed 1cm above FCz location foo bar"`, then I would vote to use that feature and force users to set it prior to passing `raw` to mne-bids.

But we don't have that for EEGReference.

We **do** have it for LineFrequency --> So I suggest to use it. Ask users to add one line to their loop in their script:

`raw.info['line_freq'] = None`

that's not too much to ask IMHO. And if it's `None`, or `np.nan` (whatever is possible), then we set it to `n/a` in BIDS


EDIT: going back to my original suggestion --> we just have to make a better error msg, so that users know they can set it to None in case they don't know it.

Resolving patch:
diff --git a/mne_bids/commands/mne_bids_raw_to_bids.py b/mne_bids/commands/mne_bids_raw_to_bids.py
--- a/mne_bids/commands/mne_bids_raw_to_bids.py
+++ b/mne_bids/commands/mne_bids_raw_to_bids.py
@@ -54,7 +54,8 @@ def run():
                       help="whether to allow non maxfiltered data (BOOLEAN)",
                       action='store_true')
     parser.add_option('--line_freq', dest='line_freq',
-                      help="The frequency of the line noise (e.g. 50 or 60).")
+                      help="The frequency of the line noise in Hz "
+                           "(e.g. 50 or 60). If unknown, pass None")
 
     opt, args = parser.parse_args()
 
@@ -75,7 +76,8 @@ def run():
                     hsp=opt.hsp, config=opt.config,
                     allow_maxshield=opt.allow_maxshield)
     if opt.line_freq is not None:
-        raw.info['line_freq'] = opt.line_freq
+        line_freq = None if opt.line_freq == "None" else opt.line_freq
+        raw.info['line_freq'] = line_freq
     write_raw_bids(raw, bids_path, event_id=opt.event_id,
                    events_data=opt.events_data, overwrite=opt.overwrite,
                    verbose=True)
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -569,13 +569,20 @@ def _sidecar_json(raw, task, manufacturer, fname, datatype, overwrite=False,
 
     """
     sfreq = raw.info['sfreq']
-    powerlinefrequency = raw.info.get('line_freq', None)
-    if powerlinefrequency is None:
-        raise ValueError("PowerLineFrequency parameter is required "
-                         "in the sidecar files. Please specify it "
-                         "in info['line_freq'] before saving to BIDS "
-                         "(e.g. raw.info['line_freq'] = 60 or with "
-                         "--line_freq option in the command line.).")
+    try:
+        powerlinefrequency = raw.info['line_freq']
+        powerlinefrequency = ('n/a' if powerlinefrequency is None else
+                              powerlinefrequency)
+    except KeyError:
+        raise ValueError(
+            "PowerLineFrequency parameter is required in the sidecar files. "
+            "Please specify it in info['line_freq'] before saving to BIDS, "
+            "e.g. by running: "
+            "    raw.info['line_freq'] = 60"
+            "in your script, or by passing: "
+            "    --line_freq 60 "
+            "in the command line for a 60 Hz line frequency. If the frequency "
+            "is unknown, set it to None")
 
     if isinstance(raw, BaseRaw):
         rec_type = 'continuous'


Test patch:
diff --git a/mne_bids/commands/tests/test_cli.py b/mne_bids/commands/tests/test_cli.py
--- a/mne_bids/commands/tests/test_cli.py
+++ b/mne_bids/commands/tests/test_cli.py
@@ -68,6 +68,12 @@ def test_raw_to_bids(tmpdir):
                      '--line_freq', 60)):
         mne_bids_raw_to_bids.run()
 
+    # Test line_freq == 'None'
+    with ArgvSetter(('--subject_id', subject_id, '--task', task, '--raw',
+                     raw_fname, '--bids_root', output_path,
+                     '--line_freq', 'None', '--overwrite', 1)):
+        mne_bids_raw_to_bids.run()
+
     # Test EDF files as well
     edf_data_path = op.join(base_path, 'edf', 'tests', 'data')
     edf_fname = op.join(edf_data_path, 'test.edf')
diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py
--- a/mne_bids/tests/test_read.py
+++ b/mne_bids/tests/test_read.py
@@ -381,8 +381,18 @@ def test_handle_info_reading():
     raw = read_raw_bids(bids_path=bids_path)
     assert raw.info['line_freq'] == 60
 
-    # 2. if line frequency is not set in raw file, then ValueError
+    # setting line_freq to None should produce 'n/a' in the JSON sidecar
     raw.info['line_freq'] = None
+    write_raw_bids(raw, bids_path, overwrite=True)
+    raw = read_raw_bids(bids_path=bids_path)
+    assert raw.info['line_freq'] is None
+
+    with open(sidecar_fname, 'r', encoding='utf-8') as fin:
+        sidecar_json = json.load(fin)
+    assert sidecar_json["PowerLineFrequency"] == 'n/a'
+
+    # 2. if line frequency is not set in raw file, then ValueError
+    del raw.info['line_freq']
     with pytest.raises(ValueError, match="PowerLineFrequency .* required"):
         write_raw_bids(raw, bids_path, overwrite=True)
 
@@ -390,6 +400,8 @@ def test_handle_info_reading():
     # to check that we make sure we always get the right sidecar
     # in addition, it should not break the sidecar reading
     # in `read_raw_bids`
+    raw.info['line_freq'] = 60
+    write_raw_bids(raw, bids_path, overwrite=True)
     deriv_dir = op.join(bids_root, "derivatives")
     sidecar_copy = op.join(deriv_dir, op.basename(sidecar_fname))
     os.mkdir(deriv_dir)
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -347,6 +347,36 @@ def test_create_fif(_bids_validate):
     _bids_validate(bids_root)
 
 
+@pytest.mark.parametrize('line_freq', [60, None])
+def test_line_freq(line_freq, _bids_validate):
+    """Test the power line frequency is written correctly."""
+    out_dir = _TempDir()
+    bids_root = _TempDir()
+    bids_path = _bids_path.copy().update(root=bids_root)
+    sfreq, n_points = 1024., int(1e6)
+    info = mne.create_info(['ch1', 'ch2', 'ch3', 'ch4', 'ch5'], sfreq,
+                           ['eeg'] * 5)
+    rng = np.random.RandomState(99)
+    raw = mne.io.RawArray(rng.random((5, n_points)) * 1e-6, info)
+
+    raw.save(op.join(out_dir, 'test-raw.fif'))
+    raw = _read_raw_fif(op.join(out_dir, 'test-raw.fif'))
+    raw.info['line_freq'] = line_freq
+    write_raw_bids(raw, bids_path, verbose=False, overwrite=True)
+    _bids_validate(bids_root)
+
+    eeg_json_fpath = (bids_path.copy()
+                      .update(suffix='eeg', extension='.json')
+                      .fpath)
+    with open(eeg_json_fpath, 'r', encoding='utf-8') as fin:
+        eeg_json = json.load(fin)
+
+    if line_freq == 60:
+        assert eeg_json['PowerLineFrequency'] == line_freq
+    elif line_freq is None:
+        assert eeg_json['PowerLineFrequency'] == 'n/a'
+
+
 @requires_version('pybv', '0.4')
 @pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
 def test_fif(_bids_validate):