-
Notifications
You must be signed in to change notification settings - Fork 3
add SofaStream.verify
#138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,14 +5,32 @@ | |
| from netCDF4 import Dataset | ||
| import numpy as np | ||
|
|
||
| from .io import _format_value_from_netcdf | ||
|
|
||
|
|
||
| class _LazyArray(np.ndarray): | ||
| """Represent a NetCDF numeric variable without reading its data.""" | ||
|
|
||
| def __new__(cls, shape, dtype): | ||
| data = np.array(0, dtype=dtype) | ||
| strides = (0, ) * len(shape) | ||
| return np.lib.stride_tricks.as_strided( | ||
| data, shape=shape, strides=strides).view(cls) | ||
|
|
||
| def copy(self, order='C'): | ||
| """Return a view instead of materializing the full array.""" | ||
| _ = order | ||
| return self | ||
|
|
||
|
|
||
| class SofaStream(): | ||
| """ | ||
| Read desired data from SOFA-file directly from disk without loading entire | ||
| file into memory. | ||
|
|
||
| :class:`SofaStream` opens a SOFA-file and retrieves only the requested | ||
| data. | ||
| data. It can also verify the file against the SOFA convention using the | ||
| same checks as :py:func:`~sofar.Sofa.verify`. | ||
|
|
||
| If you want to use all the data from a SOFA-file use :class:`Sofa` | ||
| class and :func:`read_sofa` function instead. | ||
|
|
@@ -83,6 +101,114 @@ def __exit__(self, *args): | |
| """ | ||
| self._file.close() | ||
|
|
||
| def verify(self, issue_handling="raise", mode="write"): | ||
| """ | ||
| Verify a SOFA file against the SOFA standard. | ||
|
|
||
| This method uses :py:func:`~sofar.Sofa.verify` internally with lazy | ||
| placeholders for numeric variables. Thus, numeric data are checked for | ||
| type and dimensions without loading the variable contents into memory. | ||
| String variables are loaded because their values and string lengths can | ||
| be relevant for convention verification. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| issue_handling : str, optional | ||
| Defines how detected issues are handled. See | ||
| :py:func:`~sofar.Sofa.verify` for details. | ||
| mode : str, optional | ||
| The SOFA standard is more strict for writing data than for reading | ||
| data. See :py:func:`~sofar.Sofa.verify` for details. | ||
|
|
||
|
Comment on lines
+116
to
+122
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to copy paste the parameter description from Sofa.verify to make this more self contained. |
||
| Returns | ||
| ------- | ||
| issues : str, None | ||
| Detected issues as a string. None if no issues were detected. Note | ||
| that this is only returned if ``issue_handling='return'``. | ||
| """ | ||
| if hasattr(self, "_file") and self._file.isopen(): | ||
| return self._verify_open_file(issue_handling, mode) | ||
|
|
||
| with Dataset(self._filename, mode="r") as file: | ||
| self._file = file | ||
| try: | ||
| return self._verify_open_file(issue_handling, mode) | ||
| finally: | ||
| del self._file | ||
|
|
||
| def _verify_open_file(self, issue_handling, mode): | ||
| """Verify currently opened NetCDF file without loading numeric data.""" | ||
| import sofar as sf | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imports should be at the top. Any reason that this is here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. slop :^) |
||
|
|
||
| # get SOFA object with default values and convention metadata | ||
| sofa = sf.Sofa( | ||
| self._file.SOFAConventions, | ||
| version=self._file.SOFAConventionsVersion, | ||
| verify=False) | ||
| sofa.protected = False | ||
|
|
||
| # NetCDF attribute _Encoding is skipped when reading SOFA files | ||
| skip = ["_Encoding"] | ||
| all_attr = [] | ||
|
|
||
| # load global attributes | ||
| for attr in self._file.ncattrs(): | ||
| value = getattr(self._file, attr) | ||
| key = f"GLOBAL_{attr}" | ||
| all_attr.append(key) | ||
|
|
||
| if not hasattr(sofa, key): | ||
| sofa._add_custom_api_entry(key, value, None, None, "attribute") | ||
| sofa.protected = False | ||
| else: | ||
| setattr(sofa, key, value) | ||
|
|
||
| # load variable metadata and string values | ||
| for var in self._file.variables.keys(): | ||
| netcdf_variable = self._file[var] | ||
| key = var.replace(".", "_") | ||
| all_attr.append(key) | ||
|
|
||
| if netcdf_variable.datatype == "S1": | ||
| value = _format_value_from_netcdf(netcdf_variable[:], var) | ||
| dtype = "string" | ||
| else: | ||
| value = _LazyArray( | ||
| netcdf_variable.shape, netcdf_variable.dtype) | ||
| dtype = "double" | ||
|
|
||
| if hasattr(sofa, key): | ||
| setattr(sofa, key, value) | ||
| else: | ||
| dimensions = "".join(list(netcdf_variable.dimensions)) | ||
| sofa._add_custom_api_entry( | ||
| key, value, None, dimensions, dtype) | ||
| sofa.protected = False | ||
|
|
||
| # load variable attributes | ||
| for attr in [a for a in netcdf_variable.ncattrs() | ||
| if a not in skip]: | ||
| value = getattr(netcdf_variable, attr) | ||
| attr_key = key + "_" + attr | ||
| all_attr.append(attr_key) | ||
|
|
||
| if not hasattr(sofa, attr_key): | ||
| sofa._add_custom_api_entry( | ||
| attr_key, value, None, None, "attribute") | ||
| sofa.protected = False | ||
| else: | ||
| setattr(sofa, attr_key, value) | ||
|
|
||
| # remove default entries that were not contained in the file | ||
| attrs = [attr for attr in sofa.__dict__.keys() | ||
| if not attr.startswith("_")] | ||
| for attr in attrs: | ||
| if attr not in all_attr: | ||
| delattr(sofa, attr) | ||
|
|
||
| sofa.protected = True | ||
| return sofa.verify(issue_handling=issue_handling, mode=mode) | ||
|
|
||
| def __getattr__(self, name): | ||
| """ | ||
| Executed when accessing data within a with statement | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import numpy as np | ||
| import os | ||
| import sofar as sf | ||
| import sofar.io as sfi | ||
|
|
||
|
|
||
| def test_sofastream_output(temp_sofa_file): | ||
|
|
@@ -39,6 +40,35 @@ def test_sofastream_attribute_error(temp_sofa_file): | |
| _ = file.Wrong_Attribute | ||
|
|
||
|
|
||
| def test_sofastream_verify(temp_sofa_file, tmp_path_factory, monkeypatch): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two suggestions because this test two things at once: How about having one test to only check that read_sofa is not called (and add a brief docstring to the test even though the existing tests do not follow this) Since you are calling
I think this could be done if the 'fake' sofa file that is created in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am also thinking that we could also add @f-brinkmann should we just concentrate on
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @artpelling I would definately focus on verify at the moment. I see how for example adding variables can be helpful for example to sequentially write data from measurements into SOFA files, but we should maybe check if its possible and worth the effort. |
||
|
|
||
| def fail_read_sofa(*_args, **_kwargs): | ||
| raise AssertionError("SofaStream.verify must not call read_sofa") | ||
|
|
||
| monkeypatch.setattr(sf, "read_sofa", fail_read_sofa) | ||
|
|
||
| with SofaStream(temp_sofa_file) as file: | ||
| assert file.verify(issue_handling="return") is None | ||
|
|
||
| monkeypatch.undo() | ||
|
|
||
| filename = tmp_path_factory.mktemp("data") / "test_sofastream_verify.sofa" | ||
| sofa = sf.Sofa("SimpleFreeFieldHRIR") | ||
| sofa.ListenerPosition_Units = "Meter" | ||
| sfi._write_sofa(filename, sofa, verify=False) | ||
|
|
||
| sofa_issues = sf.read_sofa(filename, verify=False).verify( | ||
| issue_handling="return", mode="write") | ||
|
|
||
| with SofaStream(filename) as file: | ||
| stream_issues = file.verify(issue_handling="return", mode="write") | ||
| with pytest.raises( | ||
| ValueError, match="lower case letters when writing"): | ||
| file.verify(mode="write") | ||
|
|
||
| assert stream_issues == sofa_issues | ||
|
|
||
|
|
||
| def test_sofastream_inspect(capfd, temp_sofa_file): | ||
|
|
||
| tempdir = TemporaryDirectory() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add something like 'see :py:func:
~sofar.Sofa.verifyfor more information on the verification process and rules.'?