Repository: NeurodataWithoutBorders/pynwb

Issue:
[Bug]: PatchClampSeries gain is optional in the schema but required in pynwb
### What happened?

I believe the schema indicates that `PatchClampSeries.gain` is an optional dataset with a quantity of 0 or 1:
https://github.com/NeurodataWithoutBorders/nwb-schema/blob/35889d36dfa088ec4a3614fa0350c02ced4a068c/core/nwb.icephys.yaml#L28-L31

while `pynwb` requires that gain is provided as an input to create a `PatchClampSeries` object:
https://github.com/NeurodataWithoutBorders/pynwb/blob/e938202b257e44c5536dc35476d55aa98e57d18d/src/pynwb/icephys.py#L109-L112

As a result, nwb files created without PatchClampSeries `gain` specified cannot be read with pynwb.


### Steps to Reproduce

Create a file in matnwb without specifying the gain for VoltageClampSeries, then try reading the file in with pynwb


### Traceback

```python
(root/acquisition/vcs GroupBuilder {'attributes': {'comments': 'no comments', 'description': 'no description', 'namespace': 'core', 'neurodata_type': 'VoltageClampSeries', 'object_id': '59cfdc19-9576-488a-a158-c7781f4e33f5', 'stimulus_description': 'N/A', 'sweep_number': 15}, 'groups': {}, 'datasets': {'capacitance_slow': root/acquisition/vcs/capacitance_slow DatasetBuilder {'attributes': {'unit': 'farads'}, 'data': 1e-10}, 'data': root/acquisition/vcs/data DatasetBuilder {'attributes': {'conversion': 1e-12, 'offset': 0.0, 'resolution': nan, 'unit': 'amperes'}, 'data': <Closed HDF5 dataset>}, 'resistance_comp_correction': root/acquisition/vcs/resistance_comp_correction DatasetBuilder {'attributes': {'unit': 'percent'}, 'data': 70.0}, 'starting_time': root/acquisition/vcs/starting_time DatasetBuilder {'attributes': {'rate': 20000.0, 'unit': 'seconds'}, 'data': 123.6}}, 'links': {'electrode': root/acquisition/vcs/electrode LinkBuilder {'builder': root/general/intracellular_ephys/elec0 GroupBuilder {'attributes': {'namespace': 'core', 'neurodata_type': 'IntracellularElectrode', 'object_id': '506001d9-e4bd-43a8-ba59-9c935b392932'}, 'groups': {}, 'datasets': {'description': root/general/intracellular_ephys/elec0/description DatasetBuilder {'attributes': {}, 'data': 'a mock intracellular electrode'}}, 'links': {'device': root/general/intracellular_ephys/elec0/device LinkBuilder {'builder': root/general/devices/Heka ITC-1600 GroupBuilder {'attributes': {'namespace': 'core', 'neurodata_type': 'Device', 'object_id': '1e0bc3f0-ece5-4db9-8830-19387b89d320'}, 'groups': {}, 'datasets': {}, 'links': {}}}}}}}}, "Could not construct VoltageClampSeries object due to: VoltageClampSeries.__init__: missing argument 'gain'")
  File "/Users/smprince/Documents/code/hdmf/src/hdmf/build/objectmapper.py", line 1357, in construct
    obj = self.__new_container__(cls, builder.source, parent, builder.attributes.get(self.__spec.id_key()),
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/smprince/Documents/code/hdmf/src/hdmf/build/objectmapper.py", line 1370, in __new_container__
    obj.__init__(**kwargs)
  File "/Users/smprince/Documents/code/hdmf/src/hdmf/utils.py", line 667, in func_call
    pargs = _check_args(args, kwargs)
            ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/smprince/Documents/code/hdmf/src/hdmf/utils.py", line 660, in _check_args
    raise ExceptionType(msg)
TypeError: VoltageClampSeries.__init__: missing argument 'gain'
```


### Operating System

macOS

### Python Executable

Conda

### Python Version

3.11

### Package Versions

_No response_

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/NeurodataWithoutBorders/pynwb/blob/dev/.github/CODE_OF_CONDUCT.rst)
- [X] Have you checked the [Contributing](https://github.com/NeurodataWithoutBorders/pynwb/blob/dev/docs/CONTRIBUTING.rst) document?
- [X] Have you ensured this bug was not already [reported](https://github.com/NeurodataWithoutBorders/pynwb/issues)?

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/1975?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.07%. Comparing base [(`e938202`)](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/commit/e938202b257e44c5536dc35476d55aa98e57d18d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders) to head [(`1c566db`)](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/commit/1c566db4c04263b1dc508517d6ee5ddf5fc083d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders).

> :exclamation:  There is a different number of reports uploaded between BASE (e938202) and HEAD (1c566db). Click for more details.
> 
> <details><summary>HEAD has 7 uploads less than BASE</summary>
>
>| Flag | BASE (e938202) | HEAD (1c566db) |
>|------|------|------|
>|unit|4|1|
>|integration|4|0|
></details>

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


```diff
@@            Coverage Diff             @@
##              dev    #1975      +/-   ##
==========================================
- Coverage   91.87%   83.07%   -8.80%     
==========================================
  Files          27       27              
  Lines        2695     2695              
  Branches      703      703              
==========================================
- Hits         2476     2239     -237     
- Misses        145      359     +214     
- Partials       74       97      +23     
```

| [Flag](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/1975/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders) | Coverage Δ | |
|---|---|---|
| [integration](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/1975/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders) | `?` | |
| [unit](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/1975/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders) | `83.07% <100.00%> (-0.26%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/NeurodataWithoutBorders/pynwb/pull/1975?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NeurodataWithoutBorders).   
: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=NeurodataWithoutBorders).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 971c9d79..d0989e10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,8 @@
 - Fixed bug in how `ElectrodeGroup.__init__` validates its `position` argument. @oruebel [#1770](https://github.com/NeurodataWithoutBorders/pynwb/pull/1770)
 - Changed `SpatialSeries.reference_frame` from required to optional as specified in the schema. @rly [#1986](https://github.com/NeurodataWithoutBorders/pynwb/pull/1986)
 
-### Enhancements and minor changes`
+### Enhancements and minor changes
+- Made gain an optional argument for PatchClampSeries to match the schema. @stephprince [#1975](https://github.com/NeurodataWithoutBorders/pynwb/pull/1975)
 - Added warning when writing files with `NWBHDF5IO` without the `.nwb` extension. @stephprince [#1978](https://github.com/NeurodataWithoutBorders/pynwb/pull/1978)
 
 ## PyNWB 2.8.2 (September 9, 2024)
diff --git a/src/pynwb/icephys.py b/src/pynwb/icephys.py
index 3101649f..ec44a2bd 100644
--- a/src/pynwb/icephys.py
+++ b/src/pynwb/icephys.py
@@ -109,7 +109,8 @@ class PatchClampSeries(TimeSeries):
             'name': 'gain',
             'type': float,
             'doc': 'Units: Volt/Amp (v-clamp) or Volt/Volt (c-clamp)',
-        },  # required
+            'default': None,
+        },
         {
             'name': 'stimulus_description',
             'type': str,
@@ -164,7 +165,7 @@ class CurrentClampSeries(PatchClampSeries):
                      'capacitance_compensation')
 
     @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'),  # required
-            {'name': 'gain', 'type': float, 'doc': 'Units - Volt/Volt'},
+            {'name': 'gain', 'type': float, 'doc': 'Units - Volt/Volt', 'default': None},
             *get_docval(PatchClampSeries.__init__, 'stimulus_description'),
             {'name': 'bias_current', 'type': float, 'doc': 'Unit - Amp', 'default': None},
             {'name': 'bridge_balance', 'type': float, 'doc': 'Unit - Ohm', 'default': None},
@@ -196,7 +197,7 @@ class IZeroClampSeries(CurrentClampSeries):
     __nwbfields__ = ()
 
     @docval(*get_docval(CurrentClampSeries.__init__, 'name', 'data', 'electrode'),  # required
-            {'name': 'gain', 'type': float, 'doc': 'Units: Volt/Volt'},  # required
+            {'name': 'gain', 'type': float, 'doc': 'Units: Volt/Volt', 'default': None},
             {'name': 'stimulus_description', 'type': str,
              'doc': ('The stimulus name/protocol. Setting this to a value other than "N/A" is deprecated as of '
                      'NWB 2.3.0.'),
@@ -238,16 +239,16 @@ class CurrentClampStimulusSeries(PatchClampSeries):
 
     __nwbfields__ = ()
 
-    @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode', 'gain'),  # required
-            *get_docval(PatchClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps',
-                        'starting_time', 'rate', 'comments', 'description', 'control', 'control_description',
-                        'sweep_number', 'offset'),
+    @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'),  # required
+            *get_docval(PatchClampSeries.__init__, 'gain', 'stimulus_description', 'resolution', 'conversion', 
+                        'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 
+                        'control_description', 'sweep_number', 'offset'),
             {'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'amperes')",
              'default': 'amperes'})
     def __init__(self, **kwargs):
-        name, data, unit, electrode, gain = popargs('name', 'data', 'unit', 'electrode', 'gain', kwargs)
+        name, data, unit, electrode = popargs('name', 'data', 'unit', 'electrode', kwargs)
         unit = ensure_unit(self, name, unit, 'amperes', '2.1.0')
-        super().__init__(name, data, unit, electrode, gain, **kwargs)
+        super().__init__(name, data, unit, electrode, **kwargs)
 
 
 @register_class('VoltageClampSeries', CORE_NAMESPACE)
@@ -267,7 +268,7 @@ class VoltageClampSeries(PatchClampSeries):
                      'whole_cell_series_resistance_comp')
 
     @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'),  # required
-            {'name': 'gain', 'type': float, 'doc': 'Units - Volt/Amp'},  # required
+            {'name': 'gain', 'type': float, 'doc': 'Units - Volt/Amp', 'default': None},
             *get_docval(PatchClampSeries.__init__, 'stimulus_description'),
             {'name': 'capacitance_fast', 'type': float, 'doc': 'Unit - Farad', 'default': None},
             {'name': 'capacitance_slow', 'type': float, 'doc': 'Unit - Farad', 'default': None},
@@ -307,16 +308,16 @@ class VoltageClampStimulusSeries(PatchClampSeries):
 
     __nwbfields__ = ()
 
-    @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode', 'gain'),  # required
-            *get_docval(PatchClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps',
-                        'starting_time', 'rate', 'comments', 'description', 'control', 'control_description',
-                        'sweep_number', 'offset'),
+    @docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'),  # required
+            *get_docval(PatchClampSeries.__init__, 'gain', 'stimulus_description', 'resolution', 'conversion', 
+                        'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control',
+                        'control_description', 'sweep_number', 'offset'),
             {'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'volts')",
              'default': 'volts'})
     def __init__(self, **kwargs):
-        name, data, unit, electrode, gain = popargs('name', 'data', 'unit', 'electrode', 'gain', kwargs)
+        name, data, unit, electrode = popargs('name', 'data', 'unit', 'electrode', kwargs)
         unit = ensure_unit(self, name, unit, 'volts', '2.1.0')
-        super().__init__(name, data, unit, electrode, gain, **kwargs)
+        super().__init__(name, data, unit, electrode, **kwargs)
 
 
 @register_class('SweepTable', CORE_NAMESPACE)


Test patch:
diff --git a/tests/integration/hdf5/test_icephys.py b/tests/integration/hdf5/test_icephys.py
index 4fba6d28..b50e5d9f 100644
--- a/tests/integration/hdf5/test_icephys.py
+++ b/tests/integration/hdf5/test_icephys.py
@@ -66,6 +66,27 @@ class TestPatchClampSeries(AcquisitionH5IOMixin, TestCase):
         nwbfile.add_device(self.device)
         super().addContainer(nwbfile)
 
+class TestPatchClampSeriesMin(AcquisitionH5IOMixin, TestCase):
+    """ Test a PatchClampSeries with minimum required args to read/write """
+
+    def setUpElectrode(self):
+        """ Set up the test IntracellularElectrode """
+        self.device = Device(name='device_name')
+        self.elec = IntracellularElectrode(name="elec0", description='a fake electrode object',
+                                           device=self.device)
+
+    def setUpContainer(self):
+        self.setUpElectrode()
+        return PatchClampSeries(name="pcs", data=[1, 2, 3, 4, 5], unit='A',
+                                starting_time=123.6, rate=10e3, electrode=self.elec)
+
+    def addContainer(self, nwbfile):
+        """
+        Add the test PatchClampSeries as an acquisition and IntracellularElectrode and Device to the given NWBFile
+        """
+        nwbfile.add_icephys_electrode(self.elec)
+        nwbfile.add_device(self.device)
+        super().addContainer(nwbfile)
 
 class TestCurrentClampStimulusSeries(TestPatchClampSeries):
 
diff --git a/tests/unit/test_icephys.py b/tests/unit/test_icephys.py
index e0e8332f..5ada7ec1 100644
--- a/tests/unit/test_icephys.py
+++ b/tests/unit/test_icephys.py
@@ -154,6 +154,13 @@ class PatchClampSeriesConstructor(TestCase):
         self.assertEqual(pCS.electrode, electrode_name)
         self.assertEqual(pCS.gain, 1.0)
 
+    def test_gain_optional(self):
+        electrode_name = GetElectrode()
+
+        pCS = PatchClampSeries('test_pCS', list(), 'unit',
+                               electrode_name, timestamps=list())
+        self.assertIsNone(pCS.gain)
+
     def test_sweepNumber_valid(self):
         electrode_name = GetElectrode()