Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
58573cc
Created basic project class.
joernweissenborn Oct 17, 2021
f0d97d9
Added `Project.markdown`
joernweissenborn Dec 3, 2021
c265ceb
🩹 Fixed issue with circular imports
s-weigand Jan 26, 2022
4f8c291
🩹 Fixed changed generator API
s-weigand Jan 26, 2022
14f3e39
🩹 Fixed changed import paths of simulated test data
s-weigand Jan 26, 2022
29e8e70
🩹 Fixed broken tests
s-weigand Jan 26, 2022
6340e6f
🩹 Fixed wrong stacklevel of folder and legacy plugin warnings
s-weigand Jan 26, 2022
cc2d101
♻️ Refactored by Sourcery
Jan 26, 2022
6db5ab3
🩹 Use save result "yml" instead of "folder"
s-weigand Jan 26, 2022
7ea5462
🧹 Removed unused variables
s-weigand Jan 26, 2022
d64e834
Switched to builtin yaml in project
joernweissenborn Jan 27, 2022
3648358
Refactored parameter generation
joernweissenborn Jan 27, 2022
497c063
Changed default saving of result to yml
joernweissenborn Jan 27, 2022
3d1c38b
Changed project open and create logic.
joernweissenborn Jan 28, 2022
d5edfc1
Added all known formats to project data.
joernweissenborn Jan 28, 2022
0a8bcad
Various tweaks.
joernweissenborn Jan 28, 2022
c65b2d3
Moved get_script_folder to utils.io.
joernweissenborn Jan 28, 2022
e24df5a
Tweaks.
joernweissenborn Jan 28, 2022
048f5e7
♻️ Refactored by Sourcery
Jan 28, 2022
47adfeb
🧪🩹 Changed expected yml string due to bug in ruamel yaml
s-weigand Jan 30, 2022
babe083
Added tests for load result.
joernweissenborn Feb 6, 2022
a90bd58
Added unit test for Model.get_parameter_labels.
joernweissenborn Mar 17, 2022
4bc226e
Removed unescessray folder.exist checks.
joernweissenborn Mar 17, 2022
09bf751
Guard Project.create from accidential overwrite.
joernweissenborn Mar 17, 2022
f95bef1
Renamed argument 'name' of Project.gernerate_model to 'generator_name'.
joernweissenborn Mar 17, 2022
a5ee140
Added methods to get a projects model and parameter directory.
joernweissenborn Mar 17, 2022
9cfd874
Use dedent in project markdown.
joernweissenborn Mar 17, 2022
ec963c4
Removed unnessecary Path instance checks.
joernweissenborn Mar 17, 2022
173f3b1
Use save dataset in project import dataset.
joernweissenborn Mar 17, 2022
34e176f
Make project_folder field in Project class optionally if instanciated…
joernweissenborn Mar 17, 2022
7a43f83
Removed unnessecary Path instance checks.
joernweissenborn Mar 17, 2022
b5634f0
Renamed argument 'generate' of ProjectModelRegistry.generate_model to…
joernweissenborn Mar 17, 2022
0e3a889
Improved result name creation.
joernweissenborn Mar 17, 2022
5a040c4
Fix Project.open.
joernweissenborn Mar 17, 2022
11c97da
Tweaked project test.
joernweissenborn Mar 17, 2022
43e3a10
Added guard to project model and parameter generation.
joernweissenborn Mar 17, 2022
1e4c2c5
Return the project instance in Project.create.
joernweissenborn Mar 17, 2022
7f994aa
Included sourcery and sonarcloud suggestions.
joernweissenborn Mar 17, 2022
4df7ff5
Renamed overwrite to allow overwrite in project.
joernweissenborn Mar 26, 2022
45e97c1
Use numbered results also when name is specified.
joernweissenborn Mar 26, 2022
4ab3d44
Replaced 'with_suffix().name' with 'stem'
joernweissenborn Mar 26, 2022
c2c4e78
Fixed project template string quotation
joernweissenborn Mar 26, 2022
bfe71bb
Added 'ignore_existing' to project data import.
joernweissenborn Mar 26, 2022
995fa42
👌 Changed implicit reraise to from reraise
s-weigand Mar 30, 2022
d821ddd
👌 Added allow_overwrite and ignore_existing options to project genera…
s-weigand Mar 30, 2022
2b23d57
👌 Added zero padding to result run number to improve string sorting
s-weigand Mar 31, 2022
6f5be80
👌 Added latest result fallback if run specifier is missing in result …
s-weigand Mar 31, 2022
c5375ff
🧪 Raised test coverage
s-weigand Mar 31, 2022
9eed892
👌 Added tests for markdown and magic jupyter method
s-weigand Mar 31, 2022
fa29e79
🩹 Fixed wrong renaming
s-weigand Mar 31, 2022
2eef676
🩹 Fixed naming inconsistency
s-weigand Mar 31, 2022
c6b91c1
🩹 Fix wrong item order on linux
s-weigand Mar 31, 2022
ea9a5d5
👌 Changed result run zero padding to 04
s-weigand Mar 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion glotaran/builtin/io/folder/folder_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def save_result(
"glotaran.io.save_result(result, Path(result_path) / 'result.yml')"
),
to_be_removed_in_version="0.8.0",
stacklevel=5,
)

return save_result(
Expand Down Expand Up @@ -143,7 +144,8 @@ def save_result(
"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."
)
),
stacklevel=4,
)

result_folder = Path(result_path)
Expand Down
1 change: 1 addition & 0 deletions glotaran/builtin/io/folder/test/test_folder_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def test_save_result_folder(
)

assert len(record) == 1
assert Path(record[0].filename) == Path(__file__)
if format_name == "legacy":
assert record[0].category == GlotaranApiDeprecationWarning
else:
Expand Down
14 changes: 7 additions & 7 deletions glotaran/builtin/io/yml/test/test_save_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@
megacomplex_sequential_decay:
type: decay-sequential
compartments:
- species_1
- species_2
- species_3
- species_1
- species_2
- species_3
rates:
- rates.species_1
- rates.species_2
- rates.species_3
- rates.species_1
- rates.species_2
- rates.species_3
dimension: time
dataset:
dataset_1:
group: default
megacomplex:
- megacomplex_sequential_decay
- megacomplex_sequential_decay
irf: gaussian_irf
"""

Expand Down
10 changes: 5 additions & 5 deletions glotaran/builtin/io/yml/test/test_save_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def test_save_result_yml(
termination_reason: The maximum number of function evaluations is exceeded.
glotaran_version: {__version__}
free_parameter_labels:
- rates.species_1
- rates.species_2
- rates.species_3
- irf.center
- irf.width
- rates.species_1
- rates.species_2
- rates.species_3
- irf.center
- irf.width
scheme: scheme.yml
initial_parameters: initial_parameters.csv
optimized_parameters: optimized_parameters.csv
Expand Down
65 changes: 65 additions & 0 deletions glotaran/builtin/io/yml/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Utility functionality module for ``glotaran.builtin.io.yml.yml``"""
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from ruamel.yaml import YAML
from ruamel.yaml.compat import StringIO

if TYPE_CHECKING:
from typing import Any
from typing import Mapping
from typing import Sequence

from ruamel.yaml.nodes import ScalarNode
from ruamel.yaml.representer import BaseRepresenter


def write_dict(
data: Mapping[str, Any] | Sequence[Any], file_name: str | Path | None = None, offset: int = 0
) -> str | None:
yaml = YAML()
yaml.representer.add_representer(type(None), _yaml_none_representer)
yaml.indent(mapping=2, sequence=2, offset=offset)

if file_name is not None:
with open(file_name, "w") as f:
yaml.dump(data, f)
else:
stream = StringIO()
yaml.dump(data, stream)
return stream.getvalue()


def load_dict(source: str | Path, is_file: bool) -> dict[str, Any]:
yaml = YAML()
yaml.representer.add_representer(type(None), _yaml_none_representer)
if is_file:
with open(source) as f:
spec = yaml.load(f)
else:
spec = yaml.load(source)
return spec


def _yaml_none_representer(representer: BaseRepresenter, data: Mapping[str, Any]) -> ScalarNode:
"""Yaml repr for ``None`` python values.

Parameters
----------
representer : BaseRepresenter
Representer of the :class:`YAML` instance.
data : Mapping[str, Any]
Data to write to yaml.

Returns
-------
ScalarNode
Node representing the value.

References
----------
https://stackoverflow.com/a/44314840
"""
return representer.represent_scalar("tag:yaml.org,2002:null", "null")
56 changes: 5 additions & 51 deletions glotaran/builtin/io/yml/yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from pathlib import Path
from typing import TYPE_CHECKING

from ruamel.yaml import YAML
from ruamel.yaml.compat import StringIO

from glotaran.builtin.io.yml.utils import load_dict
from glotaran.builtin.io.yml.utils import write_dict
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
Expand All @@ -17,18 +16,14 @@
from glotaran.io import save_scheme
from glotaran.model import Model
from glotaran.parameter import ParameterGroup
from glotaran.project import Result
from glotaran.project import Scheme
from glotaran.project.dataclass_helpers import asdict
from glotaran.project.dataclass_helpers import fromdict
from glotaran.project.project import Result
from glotaran.project.scheme import Scheme
from glotaran.utils.sanitize import sanitize_yaml

if TYPE_CHECKING:
from typing import Any
from typing import Mapping

from ruamel.yaml.nodes import ScalarNode
from ruamel.yaml.representer import BaseRepresenter


@register_project_io(["yml", "yaml", "yml_str"])
Expand Down Expand Up @@ -192,45 +187,4 @@ def save_result(
return paths

def _load_yml(self, file_name: str) -> dict[str, Any]:
yaml = YAML()
if self.format == "yml_str":
spec = yaml.load(file_name)
else:
with open(file_name) as f:
spec = yaml.load(f)
return spec


def write_dict(data: Mapping[str, Any], file_name: str | None = None) -> str | None:
yaml = YAML()
yaml.representer.add_representer(type(None), _yaml_none_representer)
yaml.indent(mapping=2, sequence=2, offset=2)
if file_name is not None:
with open(file_name, "w") as f:
yaml.dump(data, f)
else:
stream = StringIO()
yaml.dump(data, stream)
return stream.getvalue()


def _yaml_none_representer(representer: BaseRepresenter, data: Mapping[str, Any]) -> ScalarNode:
"""Yaml repr for ``None`` python values.

Parameters
----------
representer : BaseRepresenter
Representer of the :class:`YAML` instance.
data : Mapping[str, Any]
Data to write to yaml.

Returns
-------
ScalarNode
Node representing the value.

References
----------
https://stackoverflow.com/a/44314840
"""
return representer.represent_scalar("tag:yaml.org,2002:null", "null")
return load_dict(file_name, self.format != "yml_str")
19 changes: 19 additions & 0 deletions glotaran/model/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ def decorator(cls):
markdown = _markdown_factory(cls)
setattr(cls, "markdown", markdown)

get_parameter_labels = _get_parameter_labels_factory(cls)
setattr(cls, "get_parameter_labels", get_parameter_labels)

return cls

return decorator
Expand Down Expand Up @@ -372,3 +375,19 @@ def mprint_item(
return MarkdownStr(md)

return mprint_item


def _get_parameter_labels_factory(cls):
@wrap_func_as_method(cls, name="get_parameter_labels")
def get_parameter_labels(self) -> list[str]:

parameter_labels = []

for name in self._glotaran_properties:
prop = getattr(self.__class__, name)
value = getattr(self, name)
parameter_labels += prop.glotaran_get_parameter_labels(value)

return parameter_labels

return get_parameter_labels
61 changes: 43 additions & 18 deletions glotaran/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"scale": {"type": Parameter, "default": None, "allow_none": True},
}

root_parameter_error = ModelError(
"The root parameter group cannot contain both groups and parameters."
)


class Model:
"""A base class for global analysis models."""
Expand Down Expand Up @@ -125,9 +129,7 @@ def from_dict(
warn(f"Unknown model item type '{item_name}'.")
continue

is_list = isinstance(getattr(model, item_name), list)

if is_list:
if isinstance(getattr(model, item_name), list):
model._add_list_items(item_name, items)
else:
model._add_dict_items(item_name, items)
Expand All @@ -138,8 +140,7 @@ def _add_dict_items(self, item_name: str, items: dict):

for label, item in items.items():
item_cls = self.model_items[item_name]
is_typed = hasattr(item_cls, "_glotaran_model_item_typed")
if is_typed:
if hasattr(item_cls, "_glotaran_model_item_typed"):
if "type" not in item and item_cls.get_default_type() is None:
raise ValueError(f"Missing type for attribute '{item_name}'")
item_type = item.get("type", item_cls.get_default_type())
Expand All @@ -156,8 +157,7 @@ def _add_list_items(self, item_name: str, items: list):

for item in items:
item_cls = self.model_items[item_name]
is_typed = hasattr(item_cls, "_glotaran_model_item_typed")
if is_typed:
if hasattr(item_cls, "_glotaran_model_item_typed"):
if "type" not in item:
raise ValueError(f"Missing type for attribute '{item_name}'")
item_type = item["type"]
Expand Down Expand Up @@ -212,14 +212,14 @@ def _add_model_item(self, item_name: str, item: type):
def _add_dataset_property(self, property_name: str, dataset_property: dict[str, any]):
if property_name in self._dataset_properties:
known_type = (
self._dataset_properties[property_name]
if not isinstance(self._dataset_properties, dict)
else self._dataset_properties[property_name]["type"]
self._dataset_properties[property_name]["type"]
if isinstance(self._dataset_properties, dict)
else self._dataset_properties[property_name]
)
new_type = (
dataset_property
if not isinstance(dataset_property, dict)
else dataset_property["type"]
dataset_property["type"]
if isinstance(dataset_property, dict)
else dataset_property
)
if known_type != new_type:
raise ModelError(
Expand Down Expand Up @@ -312,13 +312,39 @@ def as_dict(self) -> dict:

return model_dict

def get_parameters(self) -> list[str]:
parameters = []
def get_parameter_labels(self) -> list[str]:
parameter_labels = []
for item_name in self.model_items:
items = getattr(self, item_name)
item_iterator = items if isinstance(items, list) else items.values()
for item in item_iterator:
parameters += item.get_parameters()
parameter_labels += item.get_parameter_labels()
return parameter_labels

def generate_parameters(self) -> dict | list:
parameters: dict | list = {}
for parameter in self.get_parameter_labels():
groups = parameter.split(".")
label = groups.pop()
if len(groups) == 0:
if isinstance(parameters, dict):
if len(parameters) != 0:
raise root_parameter_error
else:
parameters = []
parameters.append(Parameter.create_default_list(label))
else:
if isinstance(parameters, list):
raise root_parameter_error
this_group = groups.pop()
group = parameters
for name in groups:
if name not in group:
group[name] = {}
group = group[name]
if this_group not in group:
group[this_group] = []
group[this_group].append(Parameter.create_default_list(label))
return parameters

def need_index_dependent(self) -> bool:
Expand Down Expand Up @@ -374,8 +400,7 @@ def validate(self, parameters: ParameterGroup = None, raise_exception: bool = Fa
"""
result = ""

problems = self.problem_list(parameters)
if problems:
if problems := self.problem_list(parameters):
result = f"Your model has {len(problems)} problems:\n"
for p in problems:
result += f"\n * {p}"
Expand Down
Loading