axsdb 0.0.2__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.
- axsdb/__init__.py +27 -0
- axsdb/_version.py +8 -0
- axsdb/cli.py +75 -0
- axsdb/core.py +893 -0
- axsdb/error.py +200 -0
- axsdb/factory.py +135 -0
- axsdb/py.typed +0 -0
- axsdb/typing.py +4 -0
- axsdb/units.py +72 -0
- axsdb-0.0.2.dist-info/METADATA +30 -0
- axsdb-0.0.2.dist-info/RECORD +13 -0
- axsdb-0.0.2.dist-info/WHEEL +4 -0
- axsdb-0.0.2.dist-info/entry_points.txt +2 -0
axsdb/error.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
import attrs
|
|
8
|
+
|
|
9
|
+
# ------------------------------------------------------------------------------
|
|
10
|
+
# Exceptions
|
|
11
|
+
# ------------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataError(Exception):
|
|
15
|
+
"""Raised when encountering issues with data."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InterpolationError(Exception):
|
|
21
|
+
"""Raised when encountering errors during interpolation."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------------------
|
|
27
|
+
# Error handling components
|
|
28
|
+
# ------------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ErrorHandlingAction(enum.Enum):
|
|
32
|
+
"""
|
|
33
|
+
Error handling action descriptors.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
IGNORE = "ignore" #: Ignore the error.
|
|
37
|
+
RAISE = "raise" #: Raise the error.
|
|
38
|
+
WARN = "warn" #: Emit a warning.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@attrs.define
|
|
42
|
+
class ErrorHandlingPolicy:
|
|
43
|
+
"""
|
|
44
|
+
Error handling policy.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
missing : ErrorHandlingAction
|
|
49
|
+
Action to perform when a variable is missing.
|
|
50
|
+
|
|
51
|
+
scalar : ErrorHandlingAction
|
|
52
|
+
Action to perform when a dimension is scalar.
|
|
53
|
+
|
|
54
|
+
bounds : ErrorHandlingAction
|
|
55
|
+
Action to perform when an off-bounds query is made.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
missing: ErrorHandlingAction
|
|
59
|
+
scalar: ErrorHandlingAction
|
|
60
|
+
bounds: ErrorHandlingAction
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def convert(cls, value):
|
|
64
|
+
"""
|
|
65
|
+
Convert a value to an :class:`.ErrorHandlingPolicy`.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
value
|
|
70
|
+
Value to convert. Dictionaries values are tentatively converted to
|
|
71
|
+
:class:`.ErrorHandlingAction`, then passed as keyword arguments to
|
|
72
|
+
the constructor.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
ErrorHandlingPolicy
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(value, Mapping):
|
|
79
|
+
kwargs = {k: ErrorHandlingAction(v) for k, v in value.items()}
|
|
80
|
+
return cls(**kwargs)
|
|
81
|
+
else:
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@attrs.define
|
|
86
|
+
class ErrorHandlingConfiguration:
|
|
87
|
+
"""
|
|
88
|
+
Error handling configuration.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
x : ErrorHandlingPolicy
|
|
93
|
+
Error handling policy for species concentrations.
|
|
94
|
+
|
|
95
|
+
p : ErrorHandlingPolicy
|
|
96
|
+
Error handling policy for pressure.
|
|
97
|
+
|
|
98
|
+
t : ErrorHandlingPolicy
|
|
99
|
+
Error handling policy for temperature.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
x: ErrorHandlingPolicy = attrs.field(converter=ErrorHandlingPolicy.convert)
|
|
103
|
+
p: ErrorHandlingPolicy = attrs.field(converter=ErrorHandlingPolicy.convert)
|
|
104
|
+
t: ErrorHandlingPolicy = attrs.field(converter=ErrorHandlingPolicy.convert)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def convert(cls, value):
|
|
108
|
+
"""
|
|
109
|
+
Convert a value to an :class:`.ErrorHandlingConfiguration`.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
value
|
|
114
|
+
Value to convert. Dictionaries values are passed as keyword arguments
|
|
115
|
+
to the constructor.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
ErrorHandlingConfiguration
|
|
120
|
+
"""
|
|
121
|
+
if isinstance(value, Mapping):
|
|
122
|
+
return cls(**value)
|
|
123
|
+
else:
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_error(error: InterpolationError, action: ErrorHandlingAction):
|
|
128
|
+
"""
|
|
129
|
+
Apply an error handling policy.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
error : .InterpolationError
|
|
134
|
+
The error that is handled.
|
|
135
|
+
|
|
136
|
+
action : ErrorHandlingAction
|
|
137
|
+
If ``IGNORE``, do nothing; if ``WARN``, emit a warning; if ``RAISE``,
|
|
138
|
+
raise the error.
|
|
139
|
+
"""
|
|
140
|
+
if action is ErrorHandlingAction.IGNORE:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if action is ErrorHandlingAction.WARN:
|
|
144
|
+
warnings.warn(str(error), UserWarning)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if action is ErrorHandlingAction.RAISE:
|
|
148
|
+
raise error
|
|
149
|
+
|
|
150
|
+
raise NotImplementedError
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#: Global default error handling configuration
|
|
154
|
+
_DEFAULT_ERROR_HANDLING_CONFIG: ErrorHandlingConfiguration | None = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def set_error_handling_config(value: Mapping | ErrorHandlingConfiguration) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Set the global default error handling configuration.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
value : Mapping | ErrorHandlingConfiguration
|
|
164
|
+
Error handling configuration.
|
|
165
|
+
|
|
166
|
+
Raises
|
|
167
|
+
------
|
|
168
|
+
ValueError
|
|
169
|
+
If ``value`` cannot be converted to an :class:`.ErrorHandlingConfiguration`.
|
|
170
|
+
"""
|
|
171
|
+
global _DEFAULT_ERROR_HANDLING_CONFIG
|
|
172
|
+
value = ErrorHandlingConfiguration.convert(value)
|
|
173
|
+
if not isinstance(value, ErrorHandlingConfiguration):
|
|
174
|
+
raise ValueError("could not convert value to ErrorHandlingConfiguration")
|
|
175
|
+
_DEFAULT_ERROR_HANDLING_CONFIG = value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_error_handling_config() -> ErrorHandlingConfiguration:
|
|
179
|
+
"""
|
|
180
|
+
Retrieve the current global default error handling configuration.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
ErrorHandlingConfiguration
|
|
185
|
+
"""
|
|
186
|
+
global _DEFAULT_ERROR_HANDLING_CONFIG
|
|
187
|
+
if _DEFAULT_ERROR_HANDLING_CONFIG is None: # No config yet: assign a default
|
|
188
|
+
set_error_handling_config(
|
|
189
|
+
{
|
|
190
|
+
# This default configuration ignores bound errors on pressure and temperature
|
|
191
|
+
# variables because this usually occurs at high altitude, where the absorption
|
|
192
|
+
# coefficient is very low and can be safely forced to 0.
|
|
193
|
+
"p": {"missing": "raise", "scalar": "raise", "bounds": "ignore"},
|
|
194
|
+
"t": {"missing": "raise", "scalar": "raise", "bounds": "ignore"},
|
|
195
|
+
# Ignore missing molecule coordinates, raise on bound error.
|
|
196
|
+
"x": {"missing": "ignore", "scalar": "ignore", "bounds": "raise"},
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return _DEFAULT_ERROR_HANDLING_CONFIG
|
axsdb/factory.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import attrs
|
|
5
|
+
from typing import Callable, Type, TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from axsdb import AbsorptionDatabase
|
|
9
|
+
|
|
10
|
+
AbsorptionDatabaseT = Type[AbsorptionDatabase]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@attrs.define
|
|
14
|
+
class RegistryEntry:
|
|
15
|
+
name: str = attrs.field()
|
|
16
|
+
cls: AbsorptionDatabaseT = attrs.field(repr=False)
|
|
17
|
+
_path: Path | Callable = attrs.field(repr=False)
|
|
18
|
+
kwargs: dict[str, Any] = attrs.field(repr=False, factory=dict)
|
|
19
|
+
|
|
20
|
+
def path(self):
|
|
21
|
+
return self._path() if callable(self._path) else self._path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@attrs.define
|
|
25
|
+
class AbsorptionDatabaseFactory:
|
|
26
|
+
"""
|
|
27
|
+
This factory instantiates :class:`.AbsorptionDatabase` subclasses given a
|
|
28
|
+
name. Internally, a registry maps registered names to a matching database
|
|
29
|
+
type, a path where its data is located, and default loading options.
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
Initialize a factory instance:
|
|
34
|
+
|
|
35
|
+
>>> factory = AbsorptionDatabaseFactory()
|
|
36
|
+
|
|
37
|
+
Register a new database:
|
|
38
|
+
|
|
39
|
+
>>> factory.register(
|
|
40
|
+
... name="nanockd", cls=CKDAbsorptionDatabase, path="~/Downloads/nanockd"
|
|
41
|
+
... )
|
|
42
|
+
|
|
43
|
+
Instantiate a database:
|
|
44
|
+
|
|
45
|
+
>>> factory.create("nanockd")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_registry: dict[str, RegistryEntry] = attrs.field(factory=dict, init=False)
|
|
49
|
+
|
|
50
|
+
def register(
|
|
51
|
+
self,
|
|
52
|
+
name: str,
|
|
53
|
+
cls: AbsorptionDatabaseT,
|
|
54
|
+
path: Path | Callable,
|
|
55
|
+
kwargs: dict[str, Any] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Register a new database to the factory.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
name : str
|
|
63
|
+
A unique identifier for this entry.
|
|
64
|
+
|
|
65
|
+
cls : type
|
|
66
|
+
The matching database type.
|
|
67
|
+
|
|
68
|
+
path : path-like or callable
|
|
69
|
+
The path to the directory where database data are located. If the
|
|
70
|
+
name is dynamic (*e.g.* resolved by Eradiate's file resolver), a
|
|
71
|
+
callable with signature ``f() -> Path | str`` can be passed.
|
|
72
|
+
|
|
73
|
+
kwargs : dict, optional
|
|
74
|
+
Default loading options passed to the constructor when instantiating
|
|
75
|
+
this database.
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
Simplest registration pattern:
|
|
80
|
+
|
|
81
|
+
>>> factory.register(
|
|
82
|
+
... name="nanomono", cls=MonoAbsorptionDatabase, path="~/Data/nanomono"
|
|
83
|
+
... )
|
|
84
|
+
|
|
85
|
+
Get path dynamically from a callable:
|
|
86
|
+
|
|
87
|
+
>>> def path():
|
|
88
|
+
... return "~/Data/nanomono"
|
|
89
|
+
>>> factory.register(
|
|
90
|
+
... name="nanomono",
|
|
91
|
+
... cls=MonoAbsorptionDatabase,
|
|
92
|
+
... path=path,
|
|
93
|
+
... )
|
|
94
|
+
|
|
95
|
+
Add default keyword arguments:
|
|
96
|
+
|
|
97
|
+
>>> factory.register(
|
|
98
|
+
... name="nanomono",
|
|
99
|
+
... cls=MonoAbsorptionDatabase,
|
|
100
|
+
... path="~/Data/nanomono",
|
|
101
|
+
... kwargs={"lazy": True},
|
|
102
|
+
... )
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
if kwargs is None:
|
|
106
|
+
kwargs = {}
|
|
107
|
+
self._registry[name] = RegistryEntry(
|
|
108
|
+
name=name, cls=cls, path=path, kwargs=kwargs
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def create(self, name: str, **kwargs) -> AbsorptionDatabase:
|
|
112
|
+
"""
|
|
113
|
+
Instantiate a database given its name using the
|
|
114
|
+
:meth:`~.AbsorptionDatabase.from_directory` constructor.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
name : str
|
|
119
|
+
Name of a registered databased.
|
|
120
|
+
|
|
121
|
+
kwargs
|
|
122
|
+
Optional keyword arguments passed to the database constructor. These
|
|
123
|
+
settings will override registered defaults.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
AbsorptionDatabase
|
|
128
|
+
Created database instance.
|
|
129
|
+
"""
|
|
130
|
+
entry = self._registry[name]
|
|
131
|
+
cls = entry.cls
|
|
132
|
+
path = entry.path()
|
|
133
|
+
kwargs = {**entry.kwargs, **kwargs}
|
|
134
|
+
|
|
135
|
+
return cls.from_directory(path, **kwargs)
|
axsdb/py.typed
ADDED
|
File without changes
|
axsdb/typing.py
ADDED
axsdb/units.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
import xarray as xr
|
|
5
|
+
import pint
|
|
6
|
+
|
|
7
|
+
#: An alias to Pint's application registry.
|
|
8
|
+
ureg = pint.get_application_registry()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ensure_units(
|
|
12
|
+
value: Any, default_units: pint.Unit, convert: bool = False
|
|
13
|
+
) -> pint.Quantity:
|
|
14
|
+
"""
|
|
15
|
+
Ensure that a value is wrapped in a Pint quantity container.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
value
|
|
20
|
+
Checked value.
|
|
21
|
+
|
|
22
|
+
default_units : pint.Unit
|
|
23
|
+
Units to use to initialize the :class:`pint.Quantity` if ``value`` is
|
|
24
|
+
not a :class:`pint.Quantity`.
|
|
25
|
+
|
|
26
|
+
convert : bool, default: False
|
|
27
|
+
If ``True``, ``value`` will also be converted to ``default_units`` if it is a
|
|
28
|
+
:class:`pint.Quantity`.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
Converted ``value``.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(value, pint.Quantity):
|
|
35
|
+
if convert:
|
|
36
|
+
return value.to(default_units)
|
|
37
|
+
else:
|
|
38
|
+
return value
|
|
39
|
+
else:
|
|
40
|
+
return value * default_units
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def xarray_to_quantity(da: xr.DataArray) -> pint.Quantity:
|
|
44
|
+
"""
|
|
45
|
+
Converts a :class:`~xarray.DataArray` to a :class:`~pint.Quantity`.
|
|
46
|
+
The array's ``attrs`` metadata mapping must contain a ``units`` field.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
da : DataArray
|
|
51
|
+
:class:`~xarray.DataArray` instance which will be converted.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
quantity
|
|
56
|
+
The corresponding Pint quantity.
|
|
57
|
+
|
|
58
|
+
Raises
|
|
59
|
+
------
|
|
60
|
+
ValueError
|
|
61
|
+
If array attributes do not contain a ``units`` field.
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
This function can also be used on coordinate variables.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
units = da.attrs["units"]
|
|
69
|
+
except KeyError as e:
|
|
70
|
+
raise ValueError("this DataArray has no 'units' attribute field") from e
|
|
71
|
+
|
|
72
|
+
return ureg.Quantity(da.values, units)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axsdb
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: An absorption database reader for the Eradiate radiative transfer model.
|
|
5
|
+
Author-email: Vincent Leroy <vincent.leroy@rayference.eu>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: attrs
|
|
8
|
+
Requires-Dist: cachetools
|
|
9
|
+
Requires-Dist: netcdf4
|
|
10
|
+
Requires-Dist: pint
|
|
11
|
+
Requires-Dist: scipy
|
|
12
|
+
Requires-Dist: typer
|
|
13
|
+
Requires-Dist: xarray
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# AxsDB — The Eradiate Absorption Cross-section Database Interface
|
|
17
|
+
|
|
18
|
+
[](https://pypi.org/project/axsdb)
|
|
19
|
+
[](https://github.com/eradiate/axsdb/actions/workflows/ci.yml)
|
|
20
|
+
[](https://axsdb.readthedocs.io)
|
|
21
|
+
[](https://github.com/astral-sh/uv)
|
|
22
|
+
[](https://github.com/astral-sh/ruff)
|
|
23
|
+
|
|
24
|
+
This library provides an interface to read and
|
|
25
|
+
query [Eradiate](https://eradiate.eu)'s absorption databases.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
The Eradiate Absorption Database Reader is distributed under the terms of the
|
|
30
|
+
[GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
axsdb/__init__.py,sha256=9-EMYK0zJ6dTDZxRvqxpoAwZYER6xHjhNRROhCDPqEs,666
|
|
2
|
+
axsdb/_version.py,sha256=zpfzURzIlh9ajjpvYv4uvkfd404RwJGzy_drTalmMm0,163
|
|
3
|
+
axsdb/cli.py,sha256=UUtmlrS1Y0y6TrXDZtjDY2xBeqPlH_RRSDdRQjv9_b4,1689
|
|
4
|
+
axsdb/core.py,sha256=_FXP-gio8yTGvYEuiOjWCizi6OV8r9WdI0UNo9Kqk3U,31343
|
|
5
|
+
axsdb/error.py,sha256=dmdurgFPkZxyutyWnYb85ddt7TV3jFw1etLln14boPQ,5621
|
|
6
|
+
axsdb/factory.py,sha256=Pr8kxQ51hgQY8wBCuMy7sarHT5ORmaRYhFWiu5styhk,3710
|
|
7
|
+
axsdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
axsdb/typing.py,sha256=Aol4ouIrfrUs-HBBgblW6Iv-xJDSpFe1RxWCQj8QDFs,94
|
|
9
|
+
axsdb/units.py,sha256=XxbcC-dXiXd8ZkB3cBtd3NTT8zSEd1g8FGQUr5-nrhQ,1762
|
|
10
|
+
axsdb-0.0.2.dist-info/METADATA,sha256=CN4Ek0lbULDF88DFdZnXDni-oquWTsGdAkU6L_TQ-JQ,1457
|
|
11
|
+
axsdb-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
axsdb-0.0.2.dist-info/entry_points.txt,sha256=4OnXeexRQs2Bl3T4F_jwijW6hPb6vOsIcQAECTsnHUE,41
|
|
13
|
+
axsdb-0.0.2.dist-info/RECORD,,
|