-
Notifications
You must be signed in to change notification settings - Fork 16
[Experimental] Debugger #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b8c44cb
0a379d8
95ab6aa
530208c
05668c7
ac000d8
f8dfbd9
2c43481
c5d02dc
454a898
1de97cf
440b5ca
c2684e2
fa81043
f1afbb1
2e8680e
92d0557
a16ade6
ac9e105
d5f77af
ea518dd
f8fe74b
ee4775e
a23ce03
a7351c7
368803c
379bdfd
ceb6d6c
dda1013
8ca571d
5a2f376
45ace08
ca9e105
d28198e
2595438
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from .config import ndsl_debugger | ||
|
|
||
|
|
||
| __all__ = ["ndsl_debugger"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| """ | ||
| This module provides configuration for the global debugger `ndsl_debugger` | ||
|
|
||
| When loading, the configuration will be searched in the global environment variable | ||
| `NDSL_DEBUG_CONFIG` | ||
|
|
||
| Configuration is a yaml file of the shape | ||
| ```yaml | ||
| stencils_or_class: | ||
| - copy_corners_x_nord | ||
| - copy_corners_y_nord | ||
| - DGridShallowWaterLagrangianDynamics.__call__ | ||
| track_parameter_by_name: | ||
| - fy | ||
| ``` | ||
|
|
||
| Global variable: | ||
| ndsl_debugger: Debugger accessible throughout the middleware, default to `None` | ||
| if there is no configuration | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| import yaml | ||
|
|
||
| from ndsl.comm.mpi import MPIComm | ||
| from ndsl.debug.debugger import Debugger | ||
| from ndsl.logging import ndsl_log | ||
|
|
||
|
|
||
| ndsl_debugger = None | ||
|
|
||
|
|
||
| def _set_debugger(): | ||
| config = os.getenv("NDSL_DEBUG_CONFIG", "") | ||
| if not os.path.exists(config): | ||
| if config != "": | ||
| ndsl_log.warning( | ||
| f"NDSL_DEBUG_CONFIG set but path {config} does not exists." | ||
| ) | ||
| else: | ||
| return | ||
| with open(config) as file: | ||
| config_dict = yaml.load(file.read(), Loader=yaml.SafeLoader) | ||
| global ndsl_debugger | ||
| ndsl_debugger = Debugger(rank=MPIComm().Get_rank(), **config_dict) | ||
| ndsl_log.info("[NDSL Debugger] On") | ||
| ndsl_log.debug(f"[NDSL Debugger] Config:\n{config_dict}") | ||
|
|
||
|
|
||
| _set_debugger() | ||
|
oelbert marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import dataclasses | ||
| import numbers | ||
| import os | ||
| import pathlib | ||
|
|
||
| import pandas as pd | ||
| import xarray as xr | ||
|
|
||
| from ndsl.logging import ndsl_log | ||
| from ndsl.quantity import Quantity | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class Debugger: | ||
| """Debugger relying on `ndsl.debug.config` for setup capable | ||
| of doing automatic data save on external configuration.""" | ||
|
|
||
| # Configuration | ||
| stencils_or_class: list[str] = dataclasses.field(default_factory=list) | ||
| track_parameter_by_name: list[str] = dataclasses.field(default_factory=list) | ||
| save_compute_domain_only: bool = False | ||
| dir_name: str = "./" | ||
|
|
||
| # Runtime data | ||
| rank: int = -1 | ||
| calls_count: dict[str, int] = dataclasses.field(default_factory=dict) | ||
| track_parameter_count: dict[str, int] = dataclasses.field(default_factory=dict) | ||
|
|
||
| def _to_xarray(self, data, name) -> xr.DataArray: | ||
| if isinstance(data, Quantity): | ||
| if self.save_compute_domain_only: | ||
| mem = data.field | ||
| shp = data.field.shape | ||
| else: | ||
| mem = data.data | ||
| shp = data.shape | ||
| elif hasattr(data, "shape"): | ||
| mem = data | ||
| shp = data.shape | ||
| elif ( | ||
| pd.api.types.is_numeric_dtype(data) | ||
| or pd.api.types.is_string_dtype(data) | ||
| or isinstance(data, numbers.Number) | ||
| ): | ||
| return xr.DataArray(data) | ||
| else: | ||
| ndsl_log.error(f"[Debugger] Cannot save data of type {type(data)}") | ||
| return xr.DataArray([0]) | ||
| return xr.DataArray(mem, dims=[f"dim_{i}_{s}" for i, s in enumerate(shp)]) | ||
|
|
||
| def track_data(self, data_as_dict, source_as_name, is_in) -> None: | ||
| for name, data in data_as_dict.items(): | ||
| if name not in self.track_parameter_by_name: | ||
| continue | ||
|
|
||
| if name not in self.track_parameter_count: | ||
| self.track_parameter_count[name] = 0 | ||
| count = self.track_parameter_count[name] | ||
|
|
||
| path = pathlib.Path(f"{self.dir_name}/debug/tracks/{name}/R{self.rank}/") | ||
| os.makedirs(path, exist_ok=True) | ||
| path = pathlib.Path( | ||
| f"{path}/{count}_{name}_{source_as_name}-{'In' if is_in else 'Out'}.nc4" | ||
| ) | ||
| try: | ||
| self._to_xarray(data, name).to_netcdf(path) | ||
| except ValueError as e: | ||
| from ndsl import ndsl_log | ||
|
|
||
| ndsl_log.error(f"[Debugger] Failure to save {data}: {e}") | ||
|
|
||
| self.track_parameter_count[name] += 1 | ||
|
|
||
| def save_as_dataset(self, data_as_dict, savename, is_in) -> None: | ||
| """Save dictionnary of data to NetCDF | ||
|
|
||
| Note: Unknown types in the dictionnary won't be saved. | ||
| """ | ||
| if savename not in self.stencils_or_class: | ||
| return | ||
|
|
||
| data_arrays = {} | ||
| for name, data in data_as_dict.items(): | ||
| if dataclasses.is_dataclass(data): | ||
| for field in dataclasses.fields(data): | ||
| data_arrays[f"{name}.{field.name}"] = self._to_xarray( | ||
| getattr(data, field.name), field.name | ||
| ) | ||
| else: | ||
| data_arrays[name] = self._to_xarray(data, name) | ||
|
|
||
| call_count = ( | ||
| self.calls_count[savename] if savename in self.calls_count.keys() else 0 | ||
| ) | ||
| path = pathlib.Path(f"{self.dir_name}/debug/savepoints/R{self.rank}/") | ||
| os.makedirs(path, exist_ok=True) | ||
| path = pathlib.Path( | ||
| f"{path}/{savename}-Call{call_count}-{'In' if is_in else 'Out'}.nc4" | ||
| ) | ||
| try: | ||
| xr.Dataset(data_arrays).to_netcdf(path) | ||
| except ValueError as e: | ||
| ndsl_log.error(f"[DebugInfo] Failure to save {savename}: {e}") | ||
|
|
||
| def increment_call_count(self, savename: str): | ||
| """Increment the call count for this savename""" | ||
| if savename not in self.calls_count.keys(): | ||
| self.calls_count[savename] = 0 | ||
| self.calls_count[savename] += 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import inspect | ||
| from functools import wraps | ||
| from typing import Any, Callable | ||
|
|
||
| from ndsl.debug.config import ndsl_debugger | ||
|
|
||
|
|
||
| def instrument(func) -> Callable: | ||
| @wraps(func) | ||
| def wrapper(self, *args: Any, **kwargs: Any): | ||
| if ndsl_debugger is None: | ||
| return func(self, *args, **kwargs) | ||
| savename = func.__qualname__ | ||
| params = inspect.signature(func).parameters | ||
| data_as_dict = {} | ||
|
|
||
| # Positional | ||
| positional_count = 0 | ||
| for name, param in params.items(): | ||
| if param.kind in ( | ||
| inspect.Parameter.POSITIONAL_ONLY, | ||
| inspect.Parameter.POSITIONAL_OR_KEYWORD, | ||
| ): | ||
| if positional_count == 0: # self | ||
| positional_count += 1 | ||
| continue | ||
| if positional_count < len(args) + 1: | ||
| data_as_dict[name] = args[positional_count - 1] | ||
| positional_count += 1 | ||
| # Keyword arguments | ||
| for name, value in kwargs.items(): | ||
| if name in params: | ||
| data_as_dict[name] = value | ||
| if ndsl_debugger is not None: | ||
| ndsl_debugger.save_as_dataset(data_as_dict, func.__qualname__, is_in=True) | ||
| ndsl_debugger.track_data(data_as_dict, func.__qualname__, is_in=True) | ||
| r = func(self, *args, **kwargs) | ||
| if ndsl_debugger is not None: | ||
| ndsl_debugger.save_as_dataset(data_as_dict, func.__qualname__, is_in=False) | ||
| ndsl_debugger.track_data(data_as_dict, func.__qualname__, is_in=False) | ||
| ndsl_debugger.increment_call_count(savename) | ||
| return r | ||
|
|
||
| return wrapper |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from .cube_sphere import plot_cube_sphere | ||
|
|
||
|
|
||
| __all__ = ["plot_cube_sphere"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import numpy as np | ||
| from cartopy import crs as ccrs | ||
| from matplotlib import pyplot as plt | ||
|
|
||
| from ndsl import Quantity, ndsl_log | ||
| from ndsl.comm.communicator import Communicator | ||
| from ndsl.grid import GridData | ||
| from ndsl.viz.fv3 import pcolormesh_cube | ||
|
|
||
|
|
||
| def plot_cube_sphere( | ||
| quantity: Quantity, | ||
| k_level: int, | ||
| comm: Communicator, | ||
| grid_data: GridData, | ||
| save_to_path: str, | ||
| ): | ||
| if len(quantity.shape) < 2 or len(quantity.shape) > 3: | ||
| ndsl_log.error( | ||
| f"[Plot Cube] Can't plot quantity with shape == {quantity.shape}" | ||
| ) | ||
| return | ||
|
|
||
| data = comm.gather(quantity) | ||
| lat = comm.gather(grid_data.lat) | ||
| lon = comm.gather(grid_data.lon) | ||
|
|
||
| if comm.rank == 0: | ||
| fig, ax = plt.subplots(1, 1, subplot_kw={"projection": ccrs.Robinson()}) | ||
| pcolormesh_cube( | ||
| lat.view[:] * 180.0 / np.pi, | ||
| lon.view[:] * 180.0 / np.pi, | ||
| data.view[:] if len(data.shape) == 3 else data.view[:, :, :, k_level], | ||
| ax=ax, | ||
| ) | ||
| fig.savefig(save_to_path) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Acknowledgment | ||
|
|
||
| This code was lifted from <https://github.com/ai2cm/fv3net> and developped by AI2 under the MIT license (see below). | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're sure there's not a license violation or conflict between this and our top-level license?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also assuming you talked to the AI2 folks about this? Actually, why not directly import fv3net? I have it as a Pace dependency (at least in the dockerfile...)?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because I'll check the license, I think it's ok.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a layer here, but the MIT license (as stated below) is pretty loose. In particular it allows to modify and re-distribute the code provided that the license header is preserved.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I checked and this is correct. If we were to make "substantial" changes we could rope the code in the Apache 2.0. MIT is basically covering up AI2 for any side effect and by knock-on free us to use or reuse as is. If we modify we can argue that the license applying to the code is the one under we operate
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also checked lol, but yeah it seems like this is fine. Probably still good to drop Oli WM a line to let him know if you haven't already |
||
|
|
||
| ## MIT License | ||
|
|
||
| The MIT License (MIT) | ||
| Copyright (c) 2019, The Allen Institute for Artificial Intelligence | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
Uh oh!
There was an error while loading. Please reload this page.