climate-ref 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- climate_ref/__init__.py +30 -0
- climate_ref/_config_helpers.py +214 -0
- climate_ref/alembic.ini +114 -0
- climate_ref/cli/__init__.py +138 -0
- climate_ref/cli/_utils.py +68 -0
- climate_ref/cli/config.py +28 -0
- climate_ref/cli/datasets.py +205 -0
- climate_ref/cli/executions.py +201 -0
- climate_ref/cli/providers.py +84 -0
- climate_ref/cli/solve.py +23 -0
- climate_ref/config.py +475 -0
- climate_ref/constants.py +8 -0
- climate_ref/database.py +223 -0
- climate_ref/dataset_registry/obs4ref_reference.txt +2 -0
- climate_ref/dataset_registry/sample_data.txt +60 -0
- climate_ref/datasets/__init__.py +40 -0
- climate_ref/datasets/base.py +214 -0
- climate_ref/datasets/cmip6.py +202 -0
- climate_ref/datasets/obs4mips.py +224 -0
- climate_ref/datasets/pmp_climatology.py +15 -0
- climate_ref/datasets/utils.py +16 -0
- climate_ref/executor/__init__.py +274 -0
- climate_ref/executor/local.py +89 -0
- climate_ref/migrations/README +22 -0
- climate_ref/migrations/env.py +139 -0
- climate_ref/migrations/script.py.mako +26 -0
- climate_ref/migrations/versions/2025-05-02T1418_341a4aa2551e_regenerate.py +292 -0
- climate_ref/models/__init__.py +33 -0
- climate_ref/models/base.py +42 -0
- climate_ref/models/dataset.py +206 -0
- climate_ref/models/diagnostic.py +61 -0
- climate_ref/models/execution.py +306 -0
- climate_ref/models/metric_value.py +195 -0
- climate_ref/models/provider.py +39 -0
- climate_ref/provider_registry.py +146 -0
- climate_ref/py.typed +0 -0
- climate_ref/solver.py +395 -0
- climate_ref/testing.py +109 -0
- climate_ref-0.5.0.dist-info/METADATA +97 -0
- climate_ref-0.5.0.dist-info/RECORD +44 -0
- climate_ref-0.5.0.dist-info/WHEEL +4 -0
- climate_ref-0.5.0.dist-info/entry_points.txt +2 -0
- climate_ref-0.5.0.dist-info/licenses/LICENCE +201 -0
- climate_ref-0.5.0.dist-info/licenses/NOTICE +3 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import ForeignKey, func
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
|
+
|
|
7
|
+
from climate_ref.models.base import Base
|
|
8
|
+
from climate_ref_core.datasets import SourceDatasetType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Dataset(Base):
|
|
12
|
+
"""
|
|
13
|
+
Represents a dataset
|
|
14
|
+
|
|
15
|
+
A dataset is a collection of data files, that is used as an input to the benchmarking process.
|
|
16
|
+
Adding/removing or updating a dataset will trigger a new diagnostic calculation.
|
|
17
|
+
|
|
18
|
+
A polymorphic association is used to capture the different types of datasets as each
|
|
19
|
+
dataset type may have different metadata fields.
|
|
20
|
+
This enables the use of a single table to store all datasets,
|
|
21
|
+
but still allows for querying specific metadata fields for each dataset type.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__tablename__ = "dataset"
|
|
25
|
+
|
|
26
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
27
|
+
slug: Mapped[str] = mapped_column(unique=True)
|
|
28
|
+
"""
|
|
29
|
+
Globally unique identifier for the dataset.
|
|
30
|
+
|
|
31
|
+
In the case of CMIP6 datasets, this is the instance_id.
|
|
32
|
+
"""
|
|
33
|
+
dataset_type: Mapped[SourceDatasetType] = mapped_column(nullable=False)
|
|
34
|
+
"""
|
|
35
|
+
Type of dataset
|
|
36
|
+
"""
|
|
37
|
+
created_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now())
|
|
38
|
+
"""
|
|
39
|
+
When the dataset was added to the database
|
|
40
|
+
"""
|
|
41
|
+
updated_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
|
42
|
+
"""
|
|
43
|
+
When the dataset was updated.
|
|
44
|
+
|
|
45
|
+
Updating a dataset will trigger a new diagnostic calculation.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"<Dataset slug={self.slug} dataset_type={self.dataset_type} >"
|
|
50
|
+
|
|
51
|
+
__mapper_args__: ClassVar[Any] = {"polymorphic_on": dataset_type} # type: ignore
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DatasetFile(Base):
|
|
55
|
+
"""
|
|
56
|
+
Capture the metadata for a file in a dataset
|
|
57
|
+
|
|
58
|
+
A dataset may have multiple files, but is represented as a single dataset in the database.
|
|
59
|
+
A lot of the metadata will be duplicated for each file in the dataset,
|
|
60
|
+
but this will be more efficient for querying, filtering and building a data catalog.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
__tablename__ = "dataset_file"
|
|
64
|
+
|
|
65
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
66
|
+
dataset_id: Mapped[int] = mapped_column(ForeignKey("dataset.id", ondelete="CASCADE"), nullable=False)
|
|
67
|
+
"""
|
|
68
|
+
Foreign key to the dataset table
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
start_time: Mapped[datetime.datetime] = mapped_column(nullable=True)
|
|
72
|
+
"""
|
|
73
|
+
Start time of a given file
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
end_time: Mapped[datetime.datetime] = mapped_column(nullable=True)
|
|
77
|
+
"""
|
|
78
|
+
Start time of a given file
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
path: Mapped[str] = mapped_column()
|
|
82
|
+
"""
|
|
83
|
+
Prefix that describes where the dataset is stored relative to the data directory
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
dataset = relationship("Dataset", backref="files")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CMIP6Dataset(Dataset):
|
|
90
|
+
"""
|
|
91
|
+
Represents a CMIP6 dataset
|
|
92
|
+
|
|
93
|
+
Fields that are not marked as required in
|
|
94
|
+
https://wcrp-cmip.github.io/WGCM_Infrastructure_Panel/Papers/CMIP6_global_attributes_filenames_CVs_v6.2.7.pdf
|
|
95
|
+
are optional.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
__tablename__ = "cmip6_dataset"
|
|
99
|
+
id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
|
|
100
|
+
|
|
101
|
+
activity_id: Mapped[str] = mapped_column()
|
|
102
|
+
branch_method: Mapped[str] = mapped_column(nullable=True)
|
|
103
|
+
branch_time_in_child: Mapped[float] = mapped_column(nullable=True)
|
|
104
|
+
branch_time_in_parent: Mapped[float] = mapped_column(nullable=True)
|
|
105
|
+
experiment: Mapped[str] = mapped_column()
|
|
106
|
+
experiment_id: Mapped[str] = mapped_column()
|
|
107
|
+
frequency: Mapped[str] = mapped_column()
|
|
108
|
+
grid: Mapped[str] = mapped_column()
|
|
109
|
+
grid_label: Mapped[str] = mapped_column()
|
|
110
|
+
institution_id: Mapped[str] = mapped_column()
|
|
111
|
+
long_name: Mapped[str] = mapped_column(nullable=True)
|
|
112
|
+
member_id: Mapped[str] = mapped_column()
|
|
113
|
+
nominal_resolution: Mapped[str] = mapped_column()
|
|
114
|
+
parent_activity_id: Mapped[str] = mapped_column(nullable=True)
|
|
115
|
+
parent_experiment_id: Mapped[str] = mapped_column(nullable=True)
|
|
116
|
+
parent_source_id: Mapped[str] = mapped_column(nullable=True)
|
|
117
|
+
parent_time_units: Mapped[str] = mapped_column(nullable=True)
|
|
118
|
+
parent_variant_label: Mapped[str] = mapped_column(nullable=True)
|
|
119
|
+
realm: Mapped[str] = mapped_column()
|
|
120
|
+
product: Mapped[str] = mapped_column()
|
|
121
|
+
source_id: Mapped[str] = mapped_column()
|
|
122
|
+
standard_name: Mapped[str] = mapped_column()
|
|
123
|
+
source_type: Mapped[str] = mapped_column()
|
|
124
|
+
sub_experiment: Mapped[str] = mapped_column()
|
|
125
|
+
sub_experiment_id: Mapped[str] = mapped_column()
|
|
126
|
+
table_id: Mapped[str] = mapped_column()
|
|
127
|
+
units: Mapped[str] = mapped_column()
|
|
128
|
+
variable_id: Mapped[str] = mapped_column()
|
|
129
|
+
variant_label: Mapped[str] = mapped_column()
|
|
130
|
+
vertical_levels: Mapped[int] = mapped_column(nullable=True)
|
|
131
|
+
version: Mapped[str] = mapped_column()
|
|
132
|
+
|
|
133
|
+
instance_id: Mapped[str] = mapped_column()
|
|
134
|
+
"""
|
|
135
|
+
Unique identifier for the dataset.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
__mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.CMIP6} # type: ignore
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class Obs4MIPsDataset(Dataset):
|
|
142
|
+
"""
|
|
143
|
+
Represents a obs4mips dataset
|
|
144
|
+
|
|
145
|
+
TODO: Should the metadata fields be part of the file or dataset?
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
__tablename__ = "obs4mips_dataset"
|
|
149
|
+
id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
|
|
150
|
+
|
|
151
|
+
activity_id: Mapped[str] = mapped_column()
|
|
152
|
+
frequency: Mapped[str] = mapped_column()
|
|
153
|
+
grid: Mapped[str] = mapped_column()
|
|
154
|
+
grid_label: Mapped[str] = mapped_column()
|
|
155
|
+
institution_id: Mapped[str] = mapped_column()
|
|
156
|
+
long_name: Mapped[str] = mapped_column()
|
|
157
|
+
nominal_resolution: Mapped[str] = mapped_column()
|
|
158
|
+
realm: Mapped[str] = mapped_column()
|
|
159
|
+
product: Mapped[str] = mapped_column()
|
|
160
|
+
source_id: Mapped[str] = mapped_column()
|
|
161
|
+
source_type: Mapped[str] = mapped_column()
|
|
162
|
+
units: Mapped[str] = mapped_column()
|
|
163
|
+
variable_id: Mapped[str] = mapped_column()
|
|
164
|
+
variant_label: Mapped[str] = mapped_column()
|
|
165
|
+
vertical_levels: Mapped[int] = mapped_column()
|
|
166
|
+
source_version_number: Mapped[str] = mapped_column()
|
|
167
|
+
|
|
168
|
+
instance_id: Mapped[str] = mapped_column()
|
|
169
|
+
"""
|
|
170
|
+
Unique identifier for the dataset.
|
|
171
|
+
"""
|
|
172
|
+
__mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.obs4MIPs} # type: ignore
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PMPClimatologyDataset(Dataset):
|
|
176
|
+
"""
|
|
177
|
+
Represents a climatology dataset from PMP
|
|
178
|
+
|
|
179
|
+
These data are similar to obs4MIPs datasets, but are post-processed
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
__tablename__ = "pmp_climatology_dataset"
|
|
183
|
+
id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
|
|
184
|
+
|
|
185
|
+
activity_id: Mapped[str] = mapped_column()
|
|
186
|
+
frequency: Mapped[str] = mapped_column()
|
|
187
|
+
grid: Mapped[str] = mapped_column()
|
|
188
|
+
grid_label: Mapped[str] = mapped_column()
|
|
189
|
+
institution_id: Mapped[str] = mapped_column()
|
|
190
|
+
long_name: Mapped[str] = mapped_column()
|
|
191
|
+
nominal_resolution: Mapped[str] = mapped_column()
|
|
192
|
+
realm: Mapped[str] = mapped_column()
|
|
193
|
+
product: Mapped[str] = mapped_column()
|
|
194
|
+
source_id: Mapped[str] = mapped_column()
|
|
195
|
+
source_type: Mapped[str] = mapped_column()
|
|
196
|
+
units: Mapped[str] = mapped_column()
|
|
197
|
+
variable_id: Mapped[str] = mapped_column()
|
|
198
|
+
variant_label: Mapped[str] = mapped_column()
|
|
199
|
+
vertical_levels: Mapped[int] = mapped_column()
|
|
200
|
+
source_version_number: Mapped[str] = mapped_column()
|
|
201
|
+
|
|
202
|
+
instance_id: Mapped[str] = mapped_column()
|
|
203
|
+
"""
|
|
204
|
+
Unique identifier for the dataset.
|
|
205
|
+
"""
|
|
206
|
+
__mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.PMPClimatology} # type: ignore
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import ForeignKey, UniqueConstraint
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
|
+
|
|
6
|
+
from climate_ref.models.base import Base, CreatedUpdatedMixin
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from climate_ref.models.execution import ExecutionGroup
|
|
10
|
+
from climate_ref.models.provider import Provider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Diagnostic(CreatedUpdatedMixin, Base):
|
|
14
|
+
"""
|
|
15
|
+
Represents a diagnostic that can be calculated
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "diagnostic"
|
|
19
|
+
__table_args__ = (UniqueConstraint("provider_id", "slug", name="diagnostic_ident"),)
|
|
20
|
+
|
|
21
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
22
|
+
slug: Mapped[str] = mapped_column(unique=True)
|
|
23
|
+
"""
|
|
24
|
+
Unique identifier for the diagnostic
|
|
25
|
+
|
|
26
|
+
This will be used to reference the diagnostic in the benchmarking process
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: Mapped[str] = mapped_column()
|
|
30
|
+
"""
|
|
31
|
+
Long name of the diagnostic
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
provider_id: Mapped[int] = mapped_column(ForeignKey("provider.id"))
|
|
35
|
+
"""
|
|
36
|
+
The provider that provides the diagnostic
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
enabled: Mapped[bool] = mapped_column(default=True)
|
|
40
|
+
"""
|
|
41
|
+
Whether the diagnostic is enabled or not
|
|
42
|
+
|
|
43
|
+
If a diagnostic is not enabled, it will not be used for any calculations.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
provider: Mapped["Provider"] = relationship(back_populates="diagnostics")
|
|
47
|
+
execution_groups: Mapped[list["ExecutionGroup"]] = relationship(back_populates="diagnostic")
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"<Metric slug={self.slug}>"
|
|
51
|
+
|
|
52
|
+
def full_slug(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Get the full slug of the diagnostic, including the provider slug
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
str
|
|
59
|
+
Full slug of the diagnostic
|
|
60
|
+
"""
|
|
61
|
+
return f"{self.provider.slug}/{self.slug}"
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, func
|
|
7
|
+
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
|
8
|
+
from sqlalchemy.orm.query import RowReturningQuery
|
|
9
|
+
|
|
10
|
+
from climate_ref.models import Dataset
|
|
11
|
+
from climate_ref.models.base import Base, CreatedUpdatedMixin
|
|
12
|
+
from climate_ref_core.datasets import ExecutionDatasetCollection
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from climate_ref.database import Database
|
|
16
|
+
from climate_ref.models.diagnostic import Diagnostic
|
|
17
|
+
from climate_ref.models.metric_value import MetricValue
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExecutionGroup(CreatedUpdatedMixin, Base):
|
|
21
|
+
"""
|
|
22
|
+
Represents a group of executions with a shared set of input datasets.
|
|
23
|
+
|
|
24
|
+
When solving, the `ExecutionGroup`s are derived from the available datasets,
|
|
25
|
+
the defined diagnostics and their data requirements. From the information in the
|
|
26
|
+
group an execution can be triggered, which is an actual run of a diagnostic calculation
|
|
27
|
+
with a specific set of input datasets.
|
|
28
|
+
|
|
29
|
+
When the `ExecutionGroup` is created, it is marked dirty, meaning there are no
|
|
30
|
+
current executions available. When an Execution was run successfully for a
|
|
31
|
+
ExecutionGroup, the dirty mark is removed. After ingesting new data and
|
|
32
|
+
solving again and if new versions of the input datasets are available, the
|
|
33
|
+
ExecutionGroup will be marked dirty again.
|
|
34
|
+
|
|
35
|
+
The diagnostic_id and key form a unique identifier for `ExecutionGroup`s.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__tablename__ = "execution_group"
|
|
39
|
+
__table_args__ = (UniqueConstraint("diagnostic_id", "key", name="execution_ident"),)
|
|
40
|
+
|
|
41
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
42
|
+
|
|
43
|
+
diagnostic_id: Mapped[int] = mapped_column(ForeignKey("diagnostic.id"))
|
|
44
|
+
"""
|
|
45
|
+
The diagnostic that this execution group belongs to
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
key: Mapped[str] = mapped_column(index=True)
|
|
49
|
+
"""
|
|
50
|
+
Key for the datasets in this Execution group.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
dirty: Mapped[bool] = mapped_column(default=False)
|
|
54
|
+
"""
|
|
55
|
+
Whether the execution group should be rerun
|
|
56
|
+
|
|
57
|
+
An execution group is dirty if the diagnostic or any of the input datasets has been
|
|
58
|
+
updated since the last execution.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
selectors: Mapped[dict[str, Any]] = mapped_column(default=dict)
|
|
62
|
+
"""
|
|
63
|
+
Collection of selectors that define the group
|
|
64
|
+
|
|
65
|
+
These selectors are the unique key, value pairs that were selected during the initial groupby
|
|
66
|
+
operation.
|
|
67
|
+
These are also used to define the dataset key.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
diagnostic: Mapped["Diagnostic"] = relationship(back_populates="execution_groups")
|
|
71
|
+
executions: Mapped[list["Execution"]] = relationship(
|
|
72
|
+
back_populates="execution_group", order_by="Execution.created_at"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def should_run(self, dataset_hash: str) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Check if the diagnostic execution group needs to be executed.
|
|
78
|
+
|
|
79
|
+
The diagnostic execution group should be run if:
|
|
80
|
+
|
|
81
|
+
* the execution group is marked as dirty
|
|
82
|
+
* no executions have been performed ever
|
|
83
|
+
* the dataset hash is different from the last run
|
|
84
|
+
"""
|
|
85
|
+
if not self.executions:
|
|
86
|
+
logger.debug(f"Execution group {self.diagnostic.slug}/{self.key} was never executed")
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
if self.executions[-1].dataset_hash != dataset_hash:
|
|
90
|
+
logger.debug(
|
|
91
|
+
f"Execution group {self.diagnostic.slug}/{self.key} hash mismatch:"
|
|
92
|
+
f" {self.executions[-1].dataset_hash} != {dataset_hash}"
|
|
93
|
+
)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
if self.dirty:
|
|
97
|
+
logger.debug(f"Execution group {self.diagnostic.slug}/{self.key} is dirty")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
execution_datasets = Table(
|
|
104
|
+
"execution_dataset",
|
|
105
|
+
Base.metadata,
|
|
106
|
+
Column("execution_id", ForeignKey("execution.id")),
|
|
107
|
+
Column("dataset_id", ForeignKey("dataset.id")),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Execution(CreatedUpdatedMixin, Base):
|
|
112
|
+
"""
|
|
113
|
+
Represents a single execution of a diagnostic
|
|
114
|
+
|
|
115
|
+
Each result is part of a group of executions that share similar input datasets.
|
|
116
|
+
|
|
117
|
+
An execution group might be run multiple times as new data becomes available,
|
|
118
|
+
each run will create a `Execution`.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
__tablename__ = "execution"
|
|
122
|
+
|
|
123
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
124
|
+
|
|
125
|
+
output_fragment: Mapped[str] = mapped_column()
|
|
126
|
+
"""
|
|
127
|
+
Relative directory to store the output of the execution.
|
|
128
|
+
|
|
129
|
+
During execution this directory is relative to the temporary directory.
|
|
130
|
+
If the diagnostic execution is successful, the executions will be moved to the final output directory
|
|
131
|
+
and the temporary directory will be cleaned up.
|
|
132
|
+
This directory may contain multiple input and output files.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
execution_group_id: Mapped[int] = mapped_column(
|
|
136
|
+
ForeignKey(
|
|
137
|
+
"execution_group.id",
|
|
138
|
+
name="fk_execution_id",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
"""
|
|
142
|
+
The execution group that this execution belongs to
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
dataset_hash: Mapped[str] = mapped_column(index=True)
|
|
146
|
+
"""
|
|
147
|
+
Hash of the datasets used to calculate the diagnostic
|
|
148
|
+
|
|
149
|
+
This is used to verify if an existing diagnostic execution has been run with the same datasets.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
successful: Mapped[bool] = mapped_column(nullable=True)
|
|
153
|
+
"""
|
|
154
|
+
Was the run successful
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
path: Mapped[str] = mapped_column(nullable=True)
|
|
158
|
+
"""
|
|
159
|
+
Path to the output bundle
|
|
160
|
+
|
|
161
|
+
Relative to the diagnostic execution result output directory
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
retracted: Mapped[bool] = mapped_column(default=False)
|
|
165
|
+
"""
|
|
166
|
+
Whether the diagnostic execution result has been retracted or not
|
|
167
|
+
|
|
168
|
+
This may happen if a dataset has been retracted, or if the diagnostic execution was incorrect.
|
|
169
|
+
Rather than delete the values, they are marked as retracted.
|
|
170
|
+
These data may still be visible in the UI, but should be marked as retracted.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
execution_group: Mapped["ExecutionGroup"] = relationship(back_populates="executions")
|
|
174
|
+
outputs: Mapped[list["ExecutionOutput"]] = relationship(back_populates="execution")
|
|
175
|
+
values: Mapped[list["MetricValue"]] = relationship(back_populates="execution")
|
|
176
|
+
|
|
177
|
+
datasets: Mapped[list[Dataset]] = relationship(secondary=execution_datasets)
|
|
178
|
+
"""
|
|
179
|
+
The datasets used in this execution
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def register_datasets(self, db: "Database", execution_dataset: ExecutionDatasetCollection) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Register the datasets used in the diagnostic calculation with the execution
|
|
185
|
+
"""
|
|
186
|
+
for _, dataset in execution_dataset.items():
|
|
187
|
+
db.session.execute(
|
|
188
|
+
execution_datasets.insert(),
|
|
189
|
+
[{"execution_id": self.id, "dataset_id": idx} for idx in dataset.index],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def mark_successful(self, path: pathlib.Path | str) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Mark the diagnostic execution as successful
|
|
195
|
+
"""
|
|
196
|
+
# TODO: this needs to accept both a diagnostic and output bundle
|
|
197
|
+
self.successful = True
|
|
198
|
+
self.path = str(path)
|
|
199
|
+
|
|
200
|
+
def mark_failed(self) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Mark the diagnostic execution as unsuccessful
|
|
203
|
+
"""
|
|
204
|
+
self.successful = False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ResultOutputType(enum.Enum):
|
|
208
|
+
"""
|
|
209
|
+
Types of supported outputs
|
|
210
|
+
|
|
211
|
+
These map to the categories of output in the CMEC output bundle
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
Plot = "plot"
|
|
215
|
+
Data = "data"
|
|
216
|
+
HTML = "html"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ExecutionOutput(CreatedUpdatedMixin, Base):
|
|
220
|
+
"""
|
|
221
|
+
An output generated as part of an execution.
|
|
222
|
+
|
|
223
|
+
This output may be a plot, data file or HTML file.
|
|
224
|
+
These outputs are defined in the CMEC output bundle
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
__tablename__ = "execution_output"
|
|
228
|
+
|
|
229
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
230
|
+
|
|
231
|
+
execution_id: Mapped[int] = mapped_column(ForeignKey("execution.id"), index=True)
|
|
232
|
+
|
|
233
|
+
output_type: Mapped[ResultOutputType] = mapped_column(index=True)
|
|
234
|
+
"""
|
|
235
|
+
Type of the output
|
|
236
|
+
|
|
237
|
+
This will determine how the output is displayed
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
filename: Mapped[str] = mapped_column(nullable=True)
|
|
241
|
+
"""
|
|
242
|
+
Path to the output
|
|
243
|
+
|
|
244
|
+
Relative to the diagnostic execution result output directory
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
short_name: Mapped[str] = mapped_column(nullable=True)
|
|
248
|
+
"""
|
|
249
|
+
Short key of the output
|
|
250
|
+
|
|
251
|
+
This is unique for a given result and output type
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
long_name: Mapped[str] = mapped_column(nullable=True)
|
|
255
|
+
"""
|
|
256
|
+
Human readable name describing the plot
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
description: Mapped[str] = mapped_column(nullable=True)
|
|
260
|
+
"""
|
|
261
|
+
Long description describing the plot
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
execution: Mapped["Execution"] = relationship(back_populates="outputs")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_execution_group_and_latest(
|
|
268
|
+
session: Session,
|
|
269
|
+
) -> RowReturningQuery[tuple[ExecutionGroup, Execution | None]]:
|
|
270
|
+
"""
|
|
271
|
+
Query to get the most recent result for each execution group
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
session
|
|
276
|
+
The database session to use for the query.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
Query to get the most recent result for each execution group.
|
|
281
|
+
The result is a tuple of the execution group and the most recent result,
|
|
282
|
+
which can be None.
|
|
283
|
+
"""
|
|
284
|
+
# Find the most recent result for each execution group
|
|
285
|
+
# This uses an aggregate function because it is more efficient than order by
|
|
286
|
+
subquery = (
|
|
287
|
+
session.query(
|
|
288
|
+
Execution.execution_group_id,
|
|
289
|
+
func.max(Execution.created_at).label("latest_created_at"),
|
|
290
|
+
)
|
|
291
|
+
.group_by(Execution.execution_group_id)
|
|
292
|
+
.subquery()
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Join the diagnostic execution with the latest result
|
|
296
|
+
query = (
|
|
297
|
+
session.query(ExecutionGroup, Execution)
|
|
298
|
+
.outerjoin(subquery, ExecutionGroup.id == subquery.c.execution_group_id)
|
|
299
|
+
.outerjoin(
|
|
300
|
+
Execution,
|
|
301
|
+
(Execution.execution_group_id == ExecutionGroup.id)
|
|
302
|
+
& (Execution.created_at == subquery.c.latest_created_at),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return query # type: ignore
|