Source code for tracmass.config

"""Configuration management for the seaview processor.

This module handles loading and managing configuration settings using
Dynaconf. Settings are loaded from multiple locations in order of
increasing priority:

1. Global settings (/etc/seaview/)
2. User settings (~/.config/seaview/)
3. Current directory settings (./)
4. Any ``trm_*.toml`` file in the current directory (e.g. ``trm_omi.toml``),
   sorted by filename
5. Environment variable specified file (SEAVIEW_SETTINGS_FILE_FOR_DYNACONF)

Attributes
----------
ENV : str
    Default environment name.
USER_DIR : pathlib.Path
    Path to user configuration directory.
GLOB_DIR : pathlib.Path
    Path to global configuration directory.
CURR_DIR : pathlib.Path
    Path to current working directory.
settings : Dynaconf
    The Dynaconf settings object with loaded configuration.
"""
import os
import pathlib
import logging

from dynaconf import Dynaconf

USER_DIR = pathlib.Path("~/.config/tracmass").expanduser()
USER_DIR.mkdir(parents=True, exist_ok=True)
GLOB_DIR = pathlib.Path("/etc/tracmass/")
CURR_DIR = pathlib.Path("./").absolute()
# Any ``trm_*.toml`` file sitting in the current directory is picked up
# automatically (e.g. a per-run ``trm_omi.toml`` dropped next to a script),
# sorted by filename so the merge order is deterministic; later files in
# settings_files win on conflicting keys.
TRM_FILES = sorted(CURR_DIR.glob("trm_*.toml"))
settings_files = [
    GLOB_DIR / "tracmass.toml",
    GLOB_DIR / ".secrets.toml",
    USER_DIR / "tracmass.toml",
    USER_DIR / ".secrets.toml",
    CURR_DIR / "tracmass.toml",
    CURR_DIR / ".secrets.toml",
    *TRM_FILES,
    ]
extra_file = os.getenv("SEAVIEW_SETTINGS_FILE_FOR_DYNACONF")
if extra_file:
    settings_files.append(pathlib.Path(extra_file).absolute())

settings = Dynaconf(
    merge_enabled = False,
    envvar_prefix="SEAVIEW",
    DEBUG_LEVEL_FOR_DYNACONF='DEBUG',
    settings_files=settings_files,
    #secrets=[
    #    "/etc/seaview/.secrets.toml",
    #    "~/.config/seaview/.secrets.toml",
    #    "./.seaview.toml",
    #],
    environments=True,
    load_dotenv=True,
)

[docs] def change_env(new_env): """Change the active Dynaconf environment. Parameters ---------- new_env : str The environment name to switch to (e.g., 'development', 'production'). """ settings.setenv(new_env) settings.reload()
[docs] def move_vertically() -> bool: """Return the ``move_vertically`` flag from settings. Controls whether particles are advected by the vertical velocity ``w``. Read from the active ``tracmass.toml`` environment; defaults to ``True`` (particles do move vertically) when the key is absent, matching the historical ``no_wflux=False`` / ``include_wflux=True`` defaults. Returns ------- bool ``True`` if vertical advection is enabled. The integrators map this to ``no_wflux = not move_vertically()`` and the ECCO2 adapter maps it to ``include_wflux = move_vertically()`` whenever those arguments are not passed explicitly. """ value = settings.get("move_vertically", True) return True if value is None else bool(value)
[docs] def n_particle_batches() -> int: """Return the ``n_particle_batches`` factor from settings. Number of equal-sized particle groups each ``lax.while_loop`` call is split into by the full-grid integrators (:func:`tracmass.steady_state`, :func:`tracmass.time_dependent`), to cap peak per-call GPU scratch memory. Read from the active ``tracmass.toml`` environment; defaults to ``1`` (no batching) when the key is absent or unparseable. ``time_dependent_patched`` / ``time_dependent_streaming`` accept the argument for API symmetry but do not batch. Returns ------- int Batching factor, clamped to ``>= 1``. """ value = settings.get("n_particle_batches", 1) try: return max(1, int(value)) except (TypeError, ValueError): return 1
[docs] def gcm_name(default: str | None = None) -> str | None: """Return the ``gcm.name`` adapter name from the active settings environment. Reads the ``name`` key of the ``[env.gcm]`` table in ``tracmass.toml`` (e.g. ``"glorys"``, ``"ecco2"``, ``"theoretical"``, ``"fabboni"``). A bare ``gcm = "<name>"`` string (the pre-``[gcm]``-table form) is still accepted. Parameters ---------- default : str or None, optional Value to return when neither form is set. Default ``None``. Returns ------- str or None The adapter name, or *default* if unset. """ gcm = settings.get("gcm", None) if hasattr(gcm, "get"): return gcm.get("name", default) if isinstance(gcm, str): return gcm return default
# Canonical ordering of the GCM spatial-subset keys. ``i``/``j`` index the # source grid's x/y axes as 0-based Python slice endpoints; ``k1``/``k2`` are # 1-based TRACMASS layer numbers; ``*step`` are optional slice strides. GCM_SUBSET_KEYS: tuple = ( "i1", "i2", "istep", "j1", "j2", "jstep", "k1", "k2", "kstep", )
[docs] def gcm_subset() -> dict: """Return the GCM spatial subset from the active settings environment. Reads the ``subset`` key of the ``[env.gcm]`` table in ``tracmass.toml`` and returns it as a plain ``dict`` with every key in :data:`GCM_SUBSET_KEYS` present. Missing keys — and keys written as ``"@none"`` or ``false`` — come back as ``None`` ("no limit on that edge"). Returns ------- dict ``{"i1": .., "i2": .., "istep": .., "j1": .., "j2": .., "jstep": .., "k1": .., "k2": .., "kstep": ..}`` — each value an ``int`` or ``None``. See Also -------- apply_gcm_subset : push this dict into a GCM adapter's ``settings``. Examples -------- >>> settings.setenv("omi") # [omi.gcm] subset = { i1 = 2600, ... } >>> gcm_subset()["i1"] 2600 """ gcm = settings.get("gcm", None) raw = gcm.get("subset", None) if hasattr(gcm, "get") else None try: raw = dict(raw) if raw else {} except (TypeError, ValueError): raw = {} out: dict = {} for key in GCM_SUBSET_KEYS: val = raw.get(key, None) if val is None or val is False or val == "" or val == "@none": out[key] = None else: out[key] = int(val) return out
[docs] def apply_gcm_subset(module, subset: dict | None = None) -> dict: """Copy the settings-driven spatial subset into a GCM adapter. Updates ``module.settings["subset"]`` in place so that a subsequent ``module.fluxes(...)`` call slices the source grid to the configured region. Only keys with a non-``None`` value are written, so a partially-specified ``[env.gcm]`` ``subset`` table in ``tracmass.toml`` composes with any limits set directly on ``module.settings["subset"]``. Adapters that generate their grid and expose no ``settings["subset"]`` (:mod:`tracmass.gcm.theoretical`, :mod:`tracmass.gcm.fabboni`) are left untouched. Parameters ---------- module : module A ``tracmass.gcm`` adapter (e.g. ``tracmass.gcm.ecco2``). subset : dict, optional Subset mapping to apply. Defaults to :func:`gcm_subset` (the active settings environment). Returns ------- dict The fully-resolved subset (all :data:`GCM_SUBSET_KEYS`, ``int`` or ``None``), whether or not the adapter had a slot to receive it. Examples -------- >>> from tracmass import gcm >>> apply_gcm_subset(gcm.glorys) # honour [env.gcm] subset >>> gcm.glorys.settings["subset"]["k1"] 1 """ subset = gcm_subset() if subset is None else subset target = getattr(module, "settings", None) if isinstance(target, dict) and isinstance(target.get("subset"), dict): target["subset"].update( {key: val for key, val in subset.items() if val is not None} ) return subset
#print("Loaded files:", settings.loaded_files) #print("\nSettings:", dict(settings)) def config_log(): logpath = pathlib.Path.home() / ".local/state/seaview/" logpath.mkdir(parents=True, exist_ok=True) logger = logging.getLogger("seaview") logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) logging.getLogger('copernicusmarine').setLevel("WARNING") logging.getLogger('copernicusmarine').handlers.clear() logging.getLogger('copernicusmarine_blank_logger').handlers.clear() logging.getLogger('copernicusmarine').addHandler(logging.NullHandler())