Skip to content

Commit

Permalink
Prepare imas2xarray submodule (#680)
Browse files Browse the repository at this point in the history
* Rename `IDSMapping.sync` to `ImasHandle.update_from`
* Move IDSMapping to imas2xarray
* Move rebase functions
* Move IDSVariableModel
* Add H5Handle for loading data
* Refactor variable lookup
* Fix docs and read hdf5 data
  • Loading branch information
stefsmeets committed Nov 28, 2023
1 parent 021fc97 commit 95ebfbd
Show file tree
Hide file tree
Showing 36 changed files with 740 additions and 276 deletions.
2 changes: 1 addition & 1 deletion docs/gendocs.py
Expand Up @@ -15,10 +15,10 @@
from duqtools.config._schema_create import CreateConfigModel
from duqtools.config._schema_root import ConfigModel
from duqtools.ids._schema import ImasBaseModel
from duqtools.imas2xarray import IDSVariableModel
from duqtools.schema import (
ARange,
IDSOperationDim,
IDSVariableModel,
LinSpace,
OperationDim,
)
Expand Down
2 changes: 1 addition & 1 deletion scripts/modify_imas_data.py
Expand Up @@ -10,4 +10,4 @@

core_profiles['profiles_1d/0/t_i_average'] *= 1.1

core_profiles.sync(target)
target.update_from(core_profiles)
3 changes: 2 additions & 1 deletion scripts/plot_with_matplotlib_multi.py
Expand Up @@ -2,7 +2,8 @@
import pandas as pd
import xarray as xr

from duqtools.api import ImasHandle, standardize_grid_and_time
from duqtools.api import ImasHandle
from duqtools.imas2xarray import standardize_grid_and_time

runs = 8000, 8001, 8002

Expand Down
21 changes: 1 addition & 20 deletions src/duqtools/api.py
Expand Up @@ -7,15 +7,10 @@
- [recreate][duqtools.api.recreate]
- [submit][duqtools.api.submit]
- [duqmap][duqtools.api.duqmap]
- [rebase_on_grid][duqtools.api.rebase_on_grid]
- [rebase_on_time][duqtools.api.rebase_on_time]
- [standardize_grid_and_time][duqtools.api.standardize_grid_and_time]
Data classes:
- [ImasHandle][duqtools.api.ImasHandle]
- [IDSMapping][duqtools.api.IDSMapping]
- [Variable][duqtools.api.Variable]
- [Job][duqtools.api.Job]
- [Run][duqtools.api.Run]
- [Runs][duqtools.api.Runs]
Expand All @@ -31,16 +26,8 @@
from .create import create_api as create
from .create import recreate_api as recreate
from .duqmap import duqmap
from .ids import (
IDSMapping,
ImasHandle,
rebase_all_coords,
rebase_on_grid,
rebase_on_time,
standardize_grid_and_time,
)
from .ids import ImasHandle
from .models import Job, Run, Runs
from .schema import IDSVariableModel as Variable
from .status import status_api as get_status
from .submit import submit_api as submit

Expand All @@ -50,16 +37,10 @@
'create',
'duqmap',
'get_status',
'IDSMapping',
'ImasHandle',
'Job',
'rebase_all_coords',
'rebase_on_grid',
'rebase_on_time',
'recreate',
'Run',
'Runs',
'standardize_grid_and_time',
'submit',
'Variable',
]
3 changes: 2 additions & 1 deletion src/duqtools/apply_model.py
Expand Up @@ -6,9 +6,10 @@
from types import SimpleNamespace
from typing import Optional

from duqtools.imas2xarray import IDSVariableModel

from .ids._apply_model import _apply_ids
from .schema import IDSOperation
from .schema.variables import IDSVariableModel
from .systems.base_system import AbstractSystem
from .systems.jetto import BaseJettoSystem
from .systems.jetto._dimensions import JettoOperation
Expand Down
3 changes: 1 addition & 2 deletions src/duqtools/config/__init__.py
@@ -1,12 +1,11 @@
from __future__ import annotations

from ._config import CFG, Config, load_config
from ._variables import lookup_vars, var_lookup
from ._variables import var_lookup

__all__ = [
'CFG',
'Config',
'load_config',
'lookup_vars',
'var_lookup',
]
18 changes: 4 additions & 14 deletions src/duqtools/config/_models.py
Expand Up @@ -2,20 +2,10 @@

from typing import Union

from duqtools.schema import IDSVariableModel, RootModel
from duqtools.imas2xarray import IDSVariableModel, VariableConfigModel
from duqtools.systems.jetto import IDS2JettoVariableModel, JettoVariableModel


class VariableConfigModel(RootModel):
root: list[Union[JettoVariableModel, IDSVariableModel,
IDS2JettoVariableModel]]

def __iter__(self):
yield from self.root

def __getitem__(self, index: int):
return self.root[index]

def to_variable_dict(self) -> dict:
"""Return dict of variables."""
return {variable.name: variable for variable in self}
class DuqtoolsVariableConfigModel(VariableConfigModel):
root: list[Union[ # type: ignore
JettoVariableModel, IDSVariableModel, IDS2JettoVariableModel]]
4 changes: 2 additions & 2 deletions src/duqtools/config/_schema_root.py
Expand Up @@ -10,8 +10,8 @@
from duqtools.systems.jetto import JettoSystemModel
from duqtools.systems.no_system import NoSystemModel

from ._models import DuqtoolsVariableConfigModel
from ._schema_create import CreateConfigModel
from ._variables import VariableConfigModel


class ConfigModel(BaseModel):
Expand All @@ -26,7 +26,7 @@ class ConfigModel(BaseModel):
description=
'Configuration for the create subcommand. See model for more info.')

extra_variables: Optional[VariableConfigModel] = Field(
extra_variables: Optional[DuqtoolsVariableConfigModel] = Field(
None, description='Specify extra variables for this run.')

system: Union[NoSystemModel, Ets6SystemModel, JettoSystemModel] = Field(
Expand Down
169 changes: 8 additions & 161 deletions src/duqtools/config/_variables.py
@@ -1,175 +1,22 @@
from __future__ import annotations

import logging
import operator
import os
import sys
from collections import UserDict
from pathlib import Path, PosixPath
from typing import Hashable, Sequence

from pydantic_yaml import parse_yaml_raw_as
from duqtools.imas2xarray import VariableConfigLoader

from ..schema import IDSVariableModel
from ..utils import groupby
from ._models import VariableConfigModel
from ._models import DuqtoolsVariableConfigModel

if sys.version_info < (3, 10):
from importlib_resources import files
else:
from importlib.resources import files

logger = logging.getLogger(__name__)

VAR_ENV = 'DUQTOOLS_VARDEF'
USER_CONFIG_HOME = Path.home() / '.config'
LOCAL_DIR = Path('.').absolute()
DUQTOOLS_DIR = 'duqtools'
VAR_FILENAME = 'variables.yaml'
VAR_FILENAME_GLOB = 'variables*.yaml'
ERROR_SUFFIX = '_error_upper'
class DuqtoolsVariableConfigLoader(VariableConfigLoader):
MODEL = DuqtoolsVariableConfigModel
VAR_DIR = 'duqtools'
VAR_ENV = 'DUQTOOLS_VARDEF'
MODULE = files('duqtools.data')


class VarLookup(UserDict):
_prefix = '$'
"""Variable lookup table.
Subclasses `UserDict` to embed some commonly used operations, like
grouping and filtering.
"""
_ids_variable_key = 'IDS-variable'

def __getitem__(self, key: str) -> IDSVariableModel:
return self.data[self.normalize(key)]

def error_upper(self, key: str) -> IDSVariableModel:
"""Return error variable for given key.
i.e. `t_i_ave` -> `t_i_ave_error_upper`
"""
var = self[key.removesuffix(ERROR_SUFFIX)].copy()
var.name += ERROR_SUFFIX
var.path += ERROR_SUFFIX
return var

def normalize(self, *keys: str) -> str | tuple[str, ...]:
"""Normalize variable names (remove `$`)."""
keys = tuple(key.lstrip(self._prefix) for key in keys)
if len(keys) == 1:
return keys[0]
return keys

def filter_type(self, type: str, *, invert: bool = False) -> VarLookup:
"""Filter all entries of given type."""
cmp = operator.ne if invert else operator.eq
return VarLookup({k: v for k, v in self.items() if cmp(v.type, type)})

def groupby_type(self) -> dict[Hashable, list[IDSVariableModel]]:
"""Group entries by type."""
grouped_ids_vars = groupby(self.values(), keyfunc=lambda var: var.type)
return grouped_ids_vars

def filter_ids(self, ids: str) -> VarLookup:
"""Filter all entries of given IDS."""
ids_vars = self.filter_type(self._ids_variable_key)

return VarLookup({k: v for k, v in ids_vars.items() if v.ids == ids})

def groupby_ids(self) -> dict[Hashable, list[IDSVariableModel]]:
"""Group entries by IDS."""
ids_vars = self.filter_type(self._ids_variable_key).values()

grouped_ids_vars = groupby(ids_vars, keyfunc=lambda var: var.ids)
return grouped_ids_vars


class VariableConfigLoader:

def __init__(self):
self.paths = self.get_config_path()

def load(self) -> VarLookup:
"""Load the variables config."""
var_lookup = VarLookup()

for path in self.paths:
logger.debug(f'Loading variables from: {path}')
with open(path) as f:
var_config = parse_yaml_raw_as(VariableConfigModel, f)
var_lookup.update(var_config.to_variable_dict())

return var_lookup

def get_config_path(self) -> tuple[Path, ...]:
"""Try to get the config file with variable definitions.
Search order:
1. environment variable
(2. local directory, not sure if this should be implemented)
3. config home (first $XDG_CONFIG_HOME/duqtools then `$HOME/.config/duqtools`)
4. fall back to variable definitions in package
"""
for paths in (
self._get_paths_from_environment_variable(),
self._get_paths_from_config_home(),
self._get_paths_local_directory(),
):
if paths:
return paths

return self._get_paths_fallback()

def _get_paths_from_environment_variable(self) -> tuple[Path, ...] | None:
env = os.environ.get(VAR_ENV)
if env:
path = Path(env)
drc = path.parent

if not drc.exists():
raise OSError(f'{path} defined by ${VAR_ENV} does not exist!')

return tuple(drc.glob(path.name))

return None

def _get_paths_local_directory(self) -> tuple[Path, ...] | None:
return None # Not implemented

def _get_paths_from_config_home(self) -> tuple[Path, ...] | None:
config_home = os.environ.get('XDG_CONFIG_HOME', USER_CONFIG_HOME)

drc = Path(config_home) / DUQTOOLS_DIR
if drc.exists():
return tuple(drc.glob(VAR_FILENAME_GLOB))

return None

def _get_paths_fallback(self) -> tuple[Path, ...]:
module = files('duqtools.data')
assert module.is_dir()
drc: PosixPath = module._paths[0] # type: ignore
return tuple(drc.glob(VAR_FILENAME_GLOB))


def lookup_vars(
variables: Sequence[(str | IDSVariableModel)]
) -> list[IDSVariableModel]:
"""Helper function to look up a bunch of variables.
If str, look up the variable from the `var_lookup`. Else, check if
the variable is an `IDSVariableModel`.
"""
var_models = []
for var in variables:
if isinstance(var, str):
if var.endswith(ERROR_SUFFIX):
var = var_lookup.error_upper(var)
else:
var = var_lookup[var]
if not isinstance(var, IDSVariableModel):
raise ValueError(f'Cannot lookup variable with type {type(var)}')
var_models.append(var)
return var_models


var_lookup = VariableConfigLoader().load()
var_lookup = DuqtoolsVariableConfigLoader().load()
3 changes: 2 additions & 1 deletion src/duqtools/dashboard/_shared.py
Expand Up @@ -8,9 +8,10 @@
import streamlit as st
import xarray as xr

from duqtools.api import ImasHandle, standardize_grid_and_time
from duqtools.api import ImasHandle
from duqtools.config import var_lookup
from duqtools.ids._mapping import EmptyVarError
from duqtools.imas2xarray import standardize_grid_and_time

if sys.version_info < (3, 10):
from importlib_resources import files
Expand Down
18 changes: 0 additions & 18 deletions src/duqtools/ids/__init__.py
Expand Up @@ -4,30 +4,12 @@

from ._handle import ImasHandle
from ._imas import imas_mocked
from ._mapping import IDSMapping
from ._merge import merge_data
from ._rebase import (
rebase_all_coords,
rebase_on_grid,
rebase_on_time,
rezero_time,
squash_placeholders,
standardize_grid,
standardize_grid_and_time,
)

logger = logging.getLogger(__name__)

__all__ = [
'IDSMapping',
'ImasHandle',
'merge_data',
'rebase_on_grid',
'rebase_on_time',
'rebase_all_coords',
'standardize_grid_and_time',
'standardize_grid',
'rezero_time',
'squash_placeholders',
'imas_mocked',
]
5 changes: 3 additions & 2 deletions src/duqtools/ids/_apply_model.py
Expand Up @@ -11,8 +11,9 @@
if TYPE_CHECKING:
from types import SimpleNamespace

from duqtools.imas2xarray import IDSMapping

from ..schema import IDSOperation
from ._mapping import IDSMapping

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,4 +90,4 @@ def _apply_ids(model: IDSOperation,

if target_in:
logger.info('Writing data entry: %s', target_in)
ids_mapping.sync(target_in)
target_in.update_from(ids_mapping)

0 comments on commit 95ebfbd

Please sign in to comment.