Skip to content

Commit

Permalink
Merge pull request #93 from oloapinivad/investigate-xesmf
Browse files Browse the repository at this point in the history
Allow `xesmf>0.8` and refactor the tests
  • Loading branch information
oloapinivad authored Oct 2, 2023
2 parents 0868e29 + 4d705b4 commit 578bd1f
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 69 deletions.
2 changes: 0 additions & 2 deletions .coveragerc

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/mambatest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
#python-version: ["3.11"]
defaults:
run:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pypitest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
#python-version: ["3.10"]
defaults:
run:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

Unreleased is the current development version, which is currently lying in `main` branch.

## [v0.1.6]

- Remove support for python 3.8
- Refactor tests so that comparison is element-wise and not file-wise (Allow xesmf>0.8 and refactor the tests #93)
- Simple support for allow running the code on MacOS and pinning of xesmf<0.8 to avoid code slowdown (support for macos #86)
- Matplotlib pinning for solve temporary seaborn bug (Various updates and fixing the issue of matplotlib #89)

Expand Down
2 changes: 1 addition & 1 deletion dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name : dev-ecmean
channels:
- conda-forge
dependencies:
- python=>3.8
- python=>3.9
- libnetcdf
- xesmf
- eccodes
Expand Down
14 changes: 6 additions & 8 deletions docs/sphinx/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ Using PyPi

.. warning::

Please note that although ECmean4 is distributed via PyPi, it depends on packages that currently are available only on conda-forge and
on configuration files available from the GitHub repository. Therefore, the installation via pip requires the creation of a conda environment as well as the clone from the repository.
Please note that although ECmean4 is distributed via PyPi, it depends on packages that currently are available only on conda-forge and on configuration files available from the GitHub repository. Therefore, the installation via pip requires the creation of a conda environment as well as the clone from the repository.


It will bring you the last version available on PyPi.
You can create a conda/mamba environment which incudes the python, `eccodes <https://github.com/ecmwf/eccodes-python>`_ and `xESMF <https://xesmf.readthedocs.io/en/latest/>`_ dependencies, and then install ECmean4.
However, you should start by cloning the repository from GitHub ::
However, you should start by cloning the repository from GitHub, since the configuration files used for running ECmean4 are placed there ::

> git clone https://github.com/oloapinivad/ECmean4.git
> mamba create -n ecmean "python>=3.8" xesmf eccodes
> mamba create -n ecmean "python>=3.9" xesmf eccodes
> mamba activate ecmean
> pip install ECmean4



Using GitHub
------------

Expand Down Expand Up @@ -65,16 +63,16 @@ You can test by running in shell command line and you should and output as::

You can also run tests by simply calling ``pytest`` - as long as you have the corresponding Python package installed - from the ECmean4 folder ::

> pytest
> python -m pytest

Requirements
------------

The required packages are listed in ``environment.yml`` and in ``pyproject.toml``.
A secondary environment available in ``dev-environment.yml`` can be used for development, including testing capabilities and jupyter notebooks.

.. warning::
Python >=3.8 is requested
.. note::
Both Unix and MacOS are supported. Python >=3.9 is requested.



Expand Down
2 changes: 1 addition & 1 deletion ecmean/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""ECmean4 module"""

__version__ = '0.1.5'
__version__ = '0.1.6'

# functions to be accessible everywhere
from ecmean.libs.diagnostic import Diagnostic
Expand Down
28 changes: 28 additions & 0 deletions ecmean/libs/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import logging
import platform
import math
import multiprocessing
import pandas as pd
import numpy as np
Expand Down Expand Up @@ -228,3 +229,30 @@ def init_mydict(one, two):
dd[o][t] = float('NaN')

return dd


def are_dicts_equal(dict1, dict2, tolerance=1e-6):
"""
Recursively compare two dictionaries with a given tolerance for float comparisons.
To be used for testing purposes
"""
if isinstance(dict1, dict) and isinstance(dict2, dict):
keys1 = set(dict1.keys())
keys2 = set(dict2.keys())

if keys1 != keys2:
return False

for key in keys1:
if not are_dicts_equal(dict1[key], dict2[key], tolerance):
return False
return True
else:
try:
dict1 = float(dict1)
dict2 = float(dict2)
if math.isnan(dict1) and math.isnan(dict2):
return True
return math.isclose(dict1, dict2, rel_tol=tolerance)
except ValueError:
return dict1 == dict2
16 changes: 13 additions & 3 deletions ecmean/libs/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import logging
import xarray as xr
import xesmf as xe
import sys
from time import time
import numpy as np
from ecmean.libs.ncfixers import xr_preproc
from ecmean.libs.files import inifiles_priority
Expand All @@ -29,6 +31,8 @@ def __init__(self, component, atmdict, ocedict, areas=True, remap=False, targetg
"""Class for masks, areas and interpolation (xESMF-based)
for both atmospheric and oceanic component"""

loggy.info('Running with xesmf version %s', xe.__version__)

# define the basics
self.atmareafile = inifiles_priority(atmdict)
self.oceareafile = inifiles_priority(ocedict)
Expand All @@ -49,7 +53,7 @@ def __init__(self, component, atmdict, ocedict, areas=True, remap=False, targetg
# loading and examining atmospheric file
self.atmfield = self.load_area_field(self.atmareafile, comp='atm')
self.atmgridtype = identify_grid(self.atmfield)
loggy.warning('Atmosphere grid is is a %s grid!', self.atmgridtype)
loggy.info('Atmosphere grid is is a %s grid!', self.atmgridtype)

# compute atmopheric area
if areas:
Expand All @@ -66,7 +70,7 @@ def __init__(self, component, atmdict, ocedict, areas=True, remap=False, targetg
if self.oceareafile:
self.ocefield = self.load_area_field(self.oceareafile, comp='oce')
self.ocegridtype = identify_grid(self.ocefield)
loggy.warning('Oceanic grid is is a %s grid!', self.ocegridtype)
loggy.info('Oceanic grid is is a %s grid!', self.ocegridtype)

# compute oceanic area
if areas:
Expand Down Expand Up @@ -121,7 +125,10 @@ def make_atm_masks(self):

else:
raise KeyError("ERROR: make_atm_masks -> Atmospheric component not supported!")


# loading the mask to avoid issues with xesmf
mask = mask.load()

# interp the mask if required
if self.atmremap is not None:
if self.atmfix:
Expand Down Expand Up @@ -150,6 +157,9 @@ def make_oce_masks(self):

else:
raise KeyError("ERROR: make_oce_masks -> Oceanic component not supported!")

# load mask to avoid issue with xesmf
mask = mask.load()

# interp the mask if required
if self.oceremap is not None:
Expand Down
9 changes: 4 additions & 5 deletions ecmean/performance_indices.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def pi_worker(util, piclim, face, diag, field_3d, varstat, varlist):

# horizontal averaging with land-sea mask
else:

complete = (final - cfield)**2 / vfield
outarray = mask_field(xfield=complete,
mask_type=piclim[var]['mask'],
Expand All @@ -193,12 +193,11 @@ def pi_worker(util, piclim, face, diag, field_3d, varstat, varlist):
for region in diag.regions:

slicearray = select_region(outarray, region)

# latitude-based averaging
weights = np.cos(np.deg2rad(slicearray.lat))
out = slicearray.weighted(weights).mean().data

# store the PI
# store the PI
result[season][region] = round(float(out), 3)

# diagnostic
Expand Down Expand Up @@ -298,7 +297,7 @@ def performance_indices(exp, year1, year2,

# loop on the variables, create the parallel process
for varlist in weight_split(diag.field_all, diag.numproc):
print(varlist)
#print(varlist)


core = Process(
Expand Down
4 changes: 2 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ name : ecmean
channels:
- conda-forge
dependencies:
- python>=3.8
- python>=3.9
- libnetcdf
- xesmf<0.8
- xesmf
- eccodes
- esmpy
- pip
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors = [
]
description = "ECmean4 Global Climate Model lightweight evaluation tool"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"Intended Audience :: Science/Research",
Expand Down Expand Up @@ -60,3 +60,7 @@ performance_indices = "ecmean.performance_indices:pi_entry_point"

[tool.setuptools.dynamic]
version = {attr = "ecmean.__version__"}

[tool.coverage.run]
concurrency = ["multiprocessing"]

90 changes: 67 additions & 23 deletions tests/test_global_mean.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,97 @@

import os
import subprocess
#import pytest
from filecmp import cmp
import xarray as xr
from ecmean.global_mean import global_mean
from ecmean.libs.ncfixers import xr_preproc
#from ecmean.libs.general import set_multiprocessing_start_method
from ecmean.libs.general import are_dicts_equal

# set TOLERANCE
TOLERANCE = 1e-3

# set up coverage env var
env = {**os.environ, "COVERAGE_PROCESS_START": ".coveragerc"}

#@pytest.fixture(scope="session", autouse=True)
#def always_spawn():
# set_multiprocessing_start_method()
# Open the text file for reading
def load_gm_txt_files(textfile):
"""
Function to the read the global mean text files and extract the model values
in order to create a dictionary that can be used for comparison
"""

data_dict = {}
with open(textfile, 'r', encoding='utf8') as file:
# Read the file line by line
for line in file:
# Remove leading and trailing whitespace and split the line by '|'
columns = line.strip().split('|')

# Check if there are at least 5 columns (including the header row)
if len(columns) >= 5:
# Extract the first and fourth columns and remove leading/trailing whitespace
variable = columns[1].strip()
value = columns[4].strip()

# Add the data to the dictionary if it's not empty
if variable and value:
data_dict[variable] = value
return data_dict

# call on coupled ECE using parser and debug mode
def test_cmd_global_mean_coupled():
thefile = 'tests/table/global_mean_cpld_EC-Earth4_r1i1p1f1_1990_1990.txt'
if os.path.isfile(thefile):
os.remove(thefile)
file1 = 'tests/table/global_mean_cpld_EC-Earth4_r1i1p1f1_1990_1990.txt'
file2 = 'tests/table/global_mean_cpld_1990_1990.ref'
if os.path.isfile(file1):
os.remove(file1)
subprocess.run(['global_mean', 'cpld', '1990', '1990', '-j', '2',
'-c', 'tests/config.yml', '-t', '-v', 'debug'],
env=env, check=True)
assert cmp(thefile, 'tests/table/global_mean_cpld_1990_1990.ref')

data1 = load_gm_txt_files(file1)
data2 = load_gm_txt_files(file2)

assert are_dicts_equal(data1, data2, TOLERANCE), "TXT files are not identical."


# call on amip ECE
def test_global_mean_amip():
thefile = 'tests/table/global_mean_amip_EC-Earth4_r1i1p1f1_1990_1990.txt'
if os.path.isfile(thefile):
os.remove(thefile)
file1 = 'tests/table/global_mean_amip_EC-Earth4_r1i1p1f1_1990_1990.txt'
file2 = 'tests/table/global_mean_amip_1990_1990.ref'
if os.path.isfile(file1):
os.remove(file1)
global_mean(exp='amip', year1=1990, year2=1990, numproc=1, config='tests/config.yml',
line=True)
assert cmp(thefile, 'tests/table/global_mean_amip_1990_1990.ref')

data1 = load_gm_txt_files(file1)
data2 = load_gm_txt_files(file2)

assert are_dicts_equal(data1, data2, TOLERANCE), "TXT files are not identical."

# call on amip ECE using the xdataset option
def test_global_mean_amip_xdataset():
thefile = 'tests/table/global_mean_amip_EC-Earth4_r1i1p1f1_1990_1990.txt'
if os.path.isfile(thefile):
os.remove(thefile)
file1 = 'tests/table/global_mean_amip_EC-Earth4_r1i1p1f1_1990_1990.txt'
file2 = 'tests/table/global_mean_amip_1990_1990.ref'
if os.path.isfile(file1):
os.remove(file1)
xfield = xr.open_mfdataset('tests/data/amip/output/oifs/*.nc', preprocess=xr_preproc)
global_mean(exp='amip', year1=1990, year2=1990, numproc=4, config='tests/config.yml',
xdataset=xfield)
assert cmp(thefile, 'tests/table/global_mean_amip_1990_1990.ref')
xdataset=xfield)
data1 = load_gm_txt_files(file1)
data2 = load_gm_txt_files(file2)

assert are_dicts_equal(data1, data2, TOLERANCE), "TXT files are not identical."


# call on historical CMIP6
def test_global_mean_CMIP6():
thefile = 'tests/table/global_mean_historical_EC-Earth3_r1i1p1f1_1990_1991.txt'
if os.path.isfile(thefile):
os.remove(thefile)
file1 = 'tests/table/global_mean_historical_EC-Earth3_r1i1p1f1_1990_1991.txt'
file2 = 'tests/table/global_mean_CMIP6_1990_1991.ref'
if os.path.isfile(file1):
os.remove(file1)
global_mean(exp='historical', year1=1990, year2=1991, numproc=2, config='tests/config_CMIP6.yml', trend=True)
assert cmp(thefile, 'tests/table/global_mean_CMIP6_1990_1991.ref')

data1 = load_gm_txt_files(file1)
data2 = load_gm_txt_files(file2)

assert are_dicts_equal(data1, data2, TOLERANCE), "TXT files are not identical."

Loading

0 comments on commit 578bd1f

Please sign in to comment.