Skip to content
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

πŸ‘Œ Add SavingOptions to save_result API #966

Merged
merged 11 commits into from
Jan 24, 2022
Merged
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### πŸ‘Œ Minor Improvements:

- πŸ‘ŒπŸŽ¨ Add proper repr for DatasetMapping (#957)
- πŸ‘Œ Add SavingOptions to save_result API (#966)

### 🩹 Bug fixes

Expand All @@ -19,6 +20,8 @@

### πŸ—‘οΈ Deprecations (due in 0.8.0)

- `glotaran.io.save_result(result, result_path, format_name='legacy')` -> `glotaran.io.save_result(result, Path(result_path) / 'result.yml')`

### 🚧 Maintenance

- πŸ”§ Improve packaging tooling (#923)
Expand Down
104 changes: 85 additions & 19 deletions glotaran/builtin/io/folder/folder_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

from pathlib import Path
from typing import TYPE_CHECKING
from warnings import warn

from glotaran.deprecation import warn_deprecated
from glotaran.io import save_dataset
from glotaran.io import save_model
from glotaran.io import save_parameters
from glotaran.io import save_scheme
from glotaran.io import save_result
from glotaran.io.interface import ProjectIoInterface
from glotaran.plugin_system.project_io_registration import SAVING_OPTIONS_DEFAULT
from glotaran.plugin_system.project_io_registration import register_project_io
Expand All @@ -22,7 +23,68 @@
from glotaran.project import Result


@register_project_io(["folder", "legacy"])
@register_project_io("legacy")
class LegacyProjectIo(ProjectIoInterface):
"""Project Io plugin to save result data in a backward compatible manner."""

def save_result(
self,
result: Result,
result_path: str,
*,
saving_options: SavingOptions = SAVING_OPTIONS_DEFAULT,
) -> list[str]:
"""Save the result to a given folder.

Warning
-------
Deprecated use ``glotaran.io.save_result(result, Path(result_path) / 'result.yml')``
instead.

Returns a list with paths of all saved items.
The following files are saved if not configured otherwise:
* ``result.md``: The result with the model formatted as markdown text.
* ``result.yml``: Yaml spec file of the result
* ``model.yml``: Model spec file.
* ``scheme.yml``: Scheme spec file.
* ``initial_parameters.csv``: Initially used parameters.
* ``optimized_parameters.csv``: The optimized parameter as csv file.
* ``parameter_history.csv``: Parameter changes over the optimization
* ``{dataset_label}.nc``: The result data for each dataset as NetCDF file.

Parameters
----------
result : Result
Result instance to be saved.
result_path : str
The path to the folder in which to save the result.
saving_options : SavingOptions
Options for saving the the result.

Returns
-------
list[str]
List of file paths which were created.
"""
warn_deprecated(
deprecated_qual_name_usage=(
"glotaran.io.save_result(result, result_path, format_name='legacy')"
),
new_qual_name_usage=(
"glotaran.io.save_result(result, Path(result_path) / 'result.yml')"
),
to_be_removed_in_version="0.8.0",
)

return save_result(
result=result,
result_path=Path(result_path) / "result.yml",
saving_options=saving_options,
allow_overwrite=True,
)


@register_project_io("folder")
class FolderProjectIo(ProjectIoInterface):
"""Project Io plugin to save result data to a folder.

Expand All @@ -36,18 +98,17 @@ def save_result(
result_path: str,
*,
saving_options: SavingOptions = SAVING_OPTIONS_DEFAULT,
used_inside_of_plugin: bool = False,
) -> list[str]:
"""Save the result to a given folder.

Returns a list with paths of all saved items.
The following files are saved if not configured otherwise:
* `result.md`: The result with the model formatted as markdown text.
* `model.yml`: Model spec file.
* `scheme.yml`: Scheme spec file.
* `initial_parameters.csv`: Initially used parameters.
* `optimized_parameters.csv`: The optimized parameter as csv file.
* `parameter_history.csv`: Parameter changes over the optimization
* `{dataset_label}.nc`: The result data for each dataset as NetCDF file.
* ``result.md``: The result with the model formatted as markdown text.
* ``initial_parameters.csv``: Initially used parameters.
* ``optimized_parameters.csv``: The optimized parameter as csv file.
* ``parameter_history.csv``: Parameter changes over the optimization
* ``{dataset_label}.nc``: The result data for each dataset as NetCDF file.

Note
----
Expand All @@ -62,7 +123,9 @@ def save_result(
The path to the folder in which to save the result.
saving_options : SavingOptions
Options for saving the the result.

used_inside_of_plugin: bool
Denote that this plugin is used from inside another plugin,
if false a user warning will be thrown. , by default False

Returns
-------
Expand All @@ -74,6 +137,15 @@ def save_result(
ValueError
If ``result_path`` is a file.
"""
if used_inside_of_plugin is not True:
warn(
UserWarning(
"The folder plugin is only intended for internal use by other plugins "
"as quick way to save most of the files. The saved result will be incomplete, "
"thus it is not recommended to be used directly."
)
)

result_folder = Path(result_path)
if result_folder.is_file():
raise ValueError(f"The path '{result_folder}' is not a directory.")
Expand All @@ -85,10 +157,6 @@ def save_result(
report_path.write_text(str(result.markdown()))
paths.append(report_path.as_posix())

model_path = result_folder / "model.yml"
save_model(result.scheme.model, model_path, allow_overwrite=True)
paths.append(model_path.as_posix())

initial_parameters_path = f"initial_parameters.{saving_options.parameter_format}"
save_parameters(
result.scheme.parameters,
Expand All @@ -107,16 +175,14 @@ def save_result(
)
paths.append((result_folder / optimized_parameters_path).as_posix())

scheme_path = result_folder / "scheme.yml"
save_scheme(result.scheme, scheme_path, allow_overwrite=True)
paths.append(scheme_path.as_posix())

parameter_history_path = result_folder / "parameter_history.csv"
result.parameter_history.to_csv(parameter_history_path)
paths.append(parameter_history_path.as_posix())

for label, dataset in result.data.items():
data_path = result_folder / f"{label}.{saving_options.data_format}"
if saving_options.data_filter is not None:
dataset = dataset[saving_options.data_filter]
save_dataset(
dataset,
data_path,
Expand Down
36 changes: 23 additions & 13 deletions glotaran/builtin/io/folder/test/test_folder_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from glotaran.analysis.optimize import optimize
from glotaran.deprecation import GlotaranApiDeprecationWarning
from glotaran.io import save_result
from glotaran.project.result import Result
from glotaran.testing.simulated_data.sequential_spectral_decay import SCHEME
Expand All @@ -27,19 +28,27 @@ def test_save_result_folder(
"""Check all files exist."""

result_dir = tmp_path / "testresult"
save_paths = save_result(
result_path=str(result_dir), format_name=format_name, result=dummy_result
)
assert not result_dir.exists()
with pytest.warns(UserWarning) as record:
save_paths = save_result(
result_path=str(result_dir), format_name=format_name, result=dummy_result
)

assert len(record) == 1
if format_name == "legacy":
assert record[0].category == GlotaranApiDeprecationWarning
else:
assert record[0].category == UserWarning

wanted_files = [
"result.md",
"scheme.yml",
"model.yml",
"initial_parameters.csv",
"optimized_parameters.csv",
"parameter_history.csv",
"dataset_1.nc",
]
if format_name == "legacy":
wanted_files += ["scheme.yml", "model.yml", "result.yml"]
for wanted in wanted_files:
assert (result_dir / wanted).exists()
assert (result_dir / wanted).as_posix() in save_paths
Expand All @@ -53,13 +62,14 @@ def test_save_result_folder_error_path_is_file(
):
"""Raise error if result_path is a file without extension and overwrite is true."""

result_dir = tmp_path / "testresult"
result_dir = tmp_path / "testresulterror"
result_dir.touch()

with pytest.raises(ValueError, match="The path '.+?' is not a directory."):
save_result(
result_path=str(result_dir),
format_name=format_name,
result=dummy_result,
allow_overwrite=True,
)
with pytest.warns(UserWarning):
with pytest.raises(ValueError, match="The path '.+?' is not a directory."):
save_result(
result_path=str(result_dir),
format_name=format_name,
result=dummy_result,
allow_overwrite=True,
)
6 changes: 4 additions & 2 deletions glotaran/builtin/io/yml/test/test_save_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
@pytest.fixture(scope="session")
def dummy_result():
"""Dummy result for testing."""
print(SCHEME.data["dataset_1"])
scheme = replace(SCHEME, maximum_number_function_evaluations=1)
print(scheme.data["dataset_1"])
yield optimize(scheme, raise_exception=True)


Expand Down Expand Up @@ -58,4 +58,6 @@ def test_save_result_yml(
assert (result_dir / "dataset_1.nc").exists()

# We can't check equality due to numerical fluctuations
assert expected in (result_dir / "result.yml").read_text()
got = (result_dir / "result.yml").read_text()
print(got)
assert expected in got
55 changes: 51 additions & 4 deletions glotaran/builtin/io/yml/yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@

from glotaran.deprecation.modules.builtin_io_yml import model_spec_deprecations
from glotaran.deprecation.modules.builtin_io_yml import scheme_spec_deprecations
from glotaran.io import SAVING_OPTIONS_DEFAULT
from glotaran.io import ProjectIoInterface
from glotaran.io import SavingOptions
from glotaran.io import register_project_io
from glotaran.io import save_model
from glotaran.io import save_result
from glotaran.io import save_scheme
from glotaran.model import Model
from glotaran.parameter import ParameterGroup
from glotaran.project import Result
Expand Down Expand Up @@ -130,19 +134,62 @@ def load_result(self, result_path: str) -> Result:
spec = self._load_yml(result_path)
return fromdict(Result, spec, folder=Path(result_path).parent)

def save_result(self, result: Result, result_path: str):
"""Write a :class:`Result` instance to a spec file.
def save_result(
self,
result: Result,
result_path: str,
saving_options: SavingOptions = SAVING_OPTIONS_DEFAULT,
) -> list[str]:
"""Write a :class:`Result` instance to a spec file and data files.

Returns a list with paths of all saved items.
The following files are saved if not configured otherwise:
* ``result.md``: The result with the model formatted as markdown text.
* ``result.yml``: Yaml spec file of the result
* ``model.yml``: Model spec file.
* ``scheme.yml``: Scheme spec file.
* ``initial_parameters.csv``: Initially used parameters.
* ``optimized_parameters.csv``: The optimized parameter as csv file.
* ``parameter_history.csv``: Parameter changes over the optimization
* ``{dataset_label}.nc``: The result data for each dataset as NetCDF file.

Parameters
----------
result : Result
:class:`Result` instance to write.
result_path : str | PathLike[str]
Path to write the result data to.
saving_options : SavingOptions
Options for saving the the result.

Returns
-------
list[str]
List of file paths which were created.
"""
save_result(result, Path(result_path).parent.as_posix(), format_name="folder")
result_dict = asdict(result, folder=Path(result_path).parent)
result_folder = Path(result_path).parent
paths = save_result(
result,
result_folder,
format_name="folder",
saving_options=saving_options,
allow_overwrite=True,
used_inside_of_plugin=True,
)

model_path = result_folder / "model.yml"
save_model(result.scheme.model, model_path, allow_overwrite=True)
paths.append(model_path.as_posix())

scheme_path = result_folder / "scheme.yml"
save_scheme(result.scheme, scheme_path, allow_overwrite=True)
paths.append(scheme_path.as_posix())

result_dict = asdict(result, folder=result_folder)
write_dict(result_dict, file_name=result_path)
paths.append(result_path)

return paths

def _load_yml(self, file_name: str) -> dict[str, Any]:
yaml = YAML()
Expand Down
13 changes: 13 additions & 0 deletions glotaran/deprecation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
"""Deprecation helpers and place to put deprecated implementations till removing."""
from glotaran.deprecation.deprecation_utils import GlotaranApiDeprecationWarning
from glotaran.deprecation.deprecation_utils import GlotaranDeprectedApiError
from glotaran.deprecation.deprecation_utils import deprecate
from glotaran.deprecation.deprecation_utils import deprecate_dict_entry
from glotaran.deprecation.deprecation_utils import deprecate_module_attribute
from glotaran.deprecation.deprecation_utils import deprecate_submodule
from glotaran.deprecation.deprecation_utils import raise_deprecation_error
from glotaran.deprecation.deprecation_utils import warn_deprecated

__all__ = [
"deprecate",
"deprecate_dict_entry",
"deprecate_module_attribute",
"deprecate_submodule",
"raise_deprecation_error",
"warn_deprecated",
"GlotaranApiDeprecationWarning",
"GlotaranDeprectedApiError",
]
6 changes: 3 additions & 3 deletions glotaran/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
reexports functions from the pluginsystem from a common place.
"""

from glotaran.io.interface import SAVING_OPTIONS_DEFAULT
from glotaran.io.interface import SAVING_OPTIONS_MINIMAL
from glotaran.io.interface import DataIoInterface
from glotaran.io.interface import ProjectIoInterface
from glotaran.io.interface import SavingOptions
from glotaran.io.prepare_dataset import prepare_time_trace_dataset
from glotaran.plugin_system.data_io_registration import data_io_plugin_table
from glotaran.plugin_system.data_io_registration import get_dataloader
Expand All @@ -17,9 +20,6 @@
from glotaran.plugin_system.data_io_registration import save_dataset
from glotaran.plugin_system.data_io_registration import set_data_plugin
from glotaran.plugin_system.data_io_registration import show_data_io_method_help
from glotaran.plugin_system.project_io_registration import SAVING_OPTIONS_DEFAULT
from glotaran.plugin_system.project_io_registration import SAVING_OPTIONS_MINIMAL
from glotaran.plugin_system.project_io_registration import SavingOptions
from glotaran.plugin_system.project_io_registration import get_project_io_method
from glotaran.plugin_system.project_io_registration import load_model
from glotaran.plugin_system.project_io_registration import load_parameters
Expand Down
Loading