Skip to content

Commit

Permalink
Save emulation training data using call_py_fort (#212)
Browse files Browse the repository at this point in the history
This PR adds the beginning infrastructure to this repository for using call_py_fort to save out training data and run prognostic emulation experiments.

Currently it adds hooks into the emulation python code from the GFS_physics_driver.f90 module which are conditionally enabled during compilation (configure.fv3.gnu_docker). The call_py_fort environment is part of the prepared build dependencies, and the functional compiled image can be generated using make build_emulation from this repo.

This version will microphysics parameterization data to the working directory in a zarr file (state_output.zarr) and individual netcdf files by tile and time (under netcdf_output).

* Add call-py-fort

* Add initial training stubs

* Switch 'lev' to 'z' in dimension names

* Add parameter metadata for microphysics

* Add initial microphysics state saving

* Add temporary variables for state setting

* remove extra level addition

* Add requirements to setup.py

* Add callpy compiler flags for fv3

* Fix state reference to Stateout for intermediate vars

* Successful compilation with training saving

* Correct module call for store

* Remove problematic function call

* Working ZC scheme run

* Fix rain output name

* Fix data output shape and parameter standard names

* Update names to human readable

* Update names to human readable

* Fix key -> attr mapping function

* Add direct to netcdf saving from the monitor

* Add time filtering and direct netcdf saving

* Switch to cwd usage for some monitor options

* Add conditional call-py-fort statements

* Make specific call_py_fort configuration

* Start call py for dockerfile install

* Add callpyfort build targets

* Add make target for emulation

* Fix numpy/tflow version conflicts

* Fix ifdef statements to start of line

* Fix callpyfort lib paths for compilation

* Use call_py_fort environment as fv3gfs-environment for emulation target

* Adjust makefile targets for emulation build

* Remove whitespace changes

* Remove unused image alias

* Fix serialbox image typo

* Update the emulation package readme

* Move compile configuration into emulation environment

* Remove whitespace

* Add back in the correct configuration file handling

* Revert to original lib/include paths

* Fix emulation function names in GFS_physics_driver.f90

* Fix requirements and python installation for callpyfort

* Undo duplicatedimage name for callpyfort dep image

* Update docker/Dockerfile

Co-authored-by: Noah D. Brenowitz <[email protected]>

* Remove pfunit from callpyfort install

* Remove call_py_fort submodule and use clone of v0.2.0 instead

* Add emulation build to ci

* Remove unsaved conflict leftovers

* Remove callpyfort specific configuration in place of conditional flags

* Adjust attribute name away from special attr 'dims'

* Fix cmake conditional setting in makefile

* Requirements pinned in setup file

* Fix build step linking to callpyfort and python install

* Change dataset attributes to serialized data

* Add emulation build test

* Remove code root

* Remove unused package, glob

Co-authored-by: Noah D. Brenowitz <[email protected]>
  • Loading branch information
frodre and Noah D. Brenowitz authored Sep 7, 2021
1 parent cc3fad6 commit 201f4cc
Show file tree
Hide file tree
Showing 14 changed files with 1,169 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
command: |
git submodule init
git submodule update
- run: DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain BUILD_FROM_INTERMEDIATE=y make build build_debug build_serialize
- run: DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain BUILD_FROM_INTERMEDIATE=y make build build_debug build_serialize build_emulation
- restore_cache:
keys:
- v1.4-{{ checksum "requirements.txt"}}
Expand Down
5 changes: 5 additions & 0 deletions FV3/conf/configure.fv3.gnu_docker
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ FFLAGS += $(FFLAGS_VERBOSE)
LDFLAGS += $(LDFLAGS_VERBOSE)
endif

ifneq ($(CALLPYFORT),)
FFLAGS += -I/usr/local/include -DENABLE_CALLPYFORT
LDFLAGS += -L/usr/local/lib -lcallpy
endif

LIBS += -lFMS -L/usr/local/esmf/lib -lesmf -lnetcdff -lnetcdf -llapack -lblas -lc -lrt

LDFLAGS += $(LIBS)
74 changes: 74 additions & 0 deletions FV3/gfsphysics/GFS_layer/GFS_physics_driver.F90
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ module module_physics_driver
use cires_ugwp_module, only: cires_ugwp_driver, knob_ugwp_version
!

#ifdef ENABLE_CALLPYFORT
use callpy_mod
#endif

implicit none


Expand Down Expand Up @@ -590,6 +594,18 @@ subroutine GFS_physics_driver &
real (kind=kind_phys), dimension(size(Grid%xlon,1)) :: &
z01d, zt1d, bexp1d, xlai1d, alb1d, vegf1d
real(kind=kind_phys) :: cdfz


#ifdef ENABLE_CALLPYFORT
!--- intermediate for callpyfort set_state
real(kind=kind_phys), dimension(size(Grid%xlon,1),Model%levs) :: &
qv_cpf, qc_cpf, qvp_cpf, tp_cpf, qvp1_cpf, tp1_cpf, &
qv_post_gscond, qc_post_gscond, qv_post_precpd, qc_post_precpd

real(kind=kind_phys), dimension(size(Grid%xlon,1)) :: &
psp_cpf, psp1_cpf
#endif

!--- ALLOCATABLE ELEMENTS
!--- in clw, the first two varaibles are cloud water and ice.
!--- from third to ntrac are convective transportable tracers,
Expand Down Expand Up @@ -4486,6 +4502,44 @@ subroutine GFS_physics_driver &
psautco_l, prautco_l, Model%evpco, Model%wminco, &
Tbd%phy_f3d(1,1,ntot3d-2), lprnt, ipr)
else
#ifdef ENABLE_CALLPYFORT
! copy tracer fields for state saving
do k=1,levs
do i=1,im
qv_cpf(i,k) = Stateout%gq0(i,k,1)
qc_cpf(i,k) = Stateout%gq0(i,k,ntcw)
tp_cpf(i,k) = Tbd%phy_f3d(i,k,1)
qvp_cpf(i,k) = Tbd%phy_f3d(i,k,2)
tp1_cpf(i,k) = Tbd%phy_f3d(i,k,3)
qvp1_cpf(i,k) = Tbd%phy_f3d(i,k,4)
enddo
enddo

do i=1,im
psp_cpf(i) = Tbd%phy_f2d(i,1)
psp1_cpf(i) = Tbd%phy_f2d(i,2)
enddo

! For creating training data & emulation
call set_state("model_time", Model%jdat)
call set_state("latitude", Grid%xlat)
call set_state("longitude", Grid%xlon)
call set_state("pressure_thickness_of_atmospheric_layer", del)
call set_state("air_pressure", Statein%prsl)
call set_state("surface_air_pressure", Statein%pgr)
call set_state("air_temperature_input", Stateout%gt0)
call set_state("specific_humidity_input", qv_cpf)
call set_state("cloud_water_mixing_ratio_input", qc_cpf)
! previous timestep
call set_state("air_temperature_two_time_steps_back", tp_cpf)
call set_state("specific_humidity_two_time_steps_back", qvp_cpf)
call set_state("surface_air_pressure_two_time_steps_back", psp_cpf)
! tp1,qp1,psp1 only used if physics dt > dynamics dt + 1e-3
call set_state("air_temperature_at_previous_time_step", tp1_cpf)
call set_state("specific_humidity_at_previous_time_step", qvp1_cpf)
call set_state("surface_air_pressure_at_previous_time_step", psp1_cpf)
#endif

call gscond (im, ix, levs, dtp, dtf, Statein%prsl, Statein%pgr, &
Stateout%gq0(1,1,1), Stateout%gq0(1,1,ntcw), &
Stateout%gt0, Tbd%phy_f3d(1,1,1), Tbd%phy_f3d(1,1,2), &
Expand All @@ -4496,6 +4550,26 @@ subroutine GFS_physics_driver &
Stateout%gq0(1,1,1), Stateout%gq0(1,1,ntcw), &
Stateout%gt0, rain1, Diag%sr, rainp, rhc, psautco_l, &
prautco_l, Model%evpco, Model%wminco, lprnt, ipr)

#ifdef ENABLE_CALLPYFORT
do k=1,levs
do i=1,im
qv_post_precpd(i,k) = Stateout%gq0(i,k,1)
qc_post_precpd(i,k) = Stateout%gq0(i,k,ntcw)
enddo
enddo

call set_state("air_temperature_output", Stateout%gt0)
call set_state("specific_humidity_output", qv_post_precpd)
call set_state("cloud_water_mixing_ratio_output", qc_post_precpd)

call set_state("total_precipitation", rain1)
call set_state("ratio_of_snowfall_to_rainfall", Diag%sr)
call set_state("tendency_of_rain_water_mixing_ratio_due_to_microphysics", rainp)
call call_function("emulation.monitor", "store_zarr")
call call_function("emulation.monitor", "store_netcdf")
#endif

endif
! if (lprnt) then
! write(0,*)' prsl=',prsl(ipr,:)
Expand Down
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ BUILD_ARGS += --build-arg BASE_IMAGE=$(BASE_IMAGE)
COMPILED_TAG_NAME ?=$(DEP_TAG_NAME)
COMPILED_IMAGE ?= $(GCR_URL)/$(COMPILE_TARGET):$(COMPILED_TAG_NAME)
SERIALIZE_IMAGE ?= $(GCR_URL)/$(COMPILE_TARGET):$(COMPILED_TAG_NAME)-serialize
EMULATION_IMAGE ?= $(GCR_URL)/$(COMPILE_TARGET):$(COMPILED_TAG_NAME)-emulation
ENVIRONMENT_IMAGE ?= $(GCR_URL)/$(ENVIRONMENT_TARGET):$(ENVIRONMENT_TAG_NAME)
MPI_IMAGE ?= $(GCR_URL)/mpi-build:$(DEP_TAG_NAME)
FMS_IMAGE ?= $(GCR_URL)/fms-build:$(DEP_TAG_NAME)
ESMF_IMAGE ?= $(GCR_URL)/esmf-build:$(DEP_TAG_NAME)
SERIALBOX_IMAGE ?= $(GCR_URL)/serialbox-build:$(DEP_TAG_NAME)
CALLPYFORT_IMAGE ?= $(GCR_URL)/emulation-build:$(DEP_TAG_NAME)

# used to shorten build times in CircleCI
ifeq ($(BUILD_FROM_INTERMEDIATE),y)
Expand Down Expand Up @@ -64,25 +66,33 @@ build_serialize: ## build container image for serialization
COMPILED_IMAGE=$(SERIALIZE_IMAGE) \
$(MAKE) build_compiled

build_emulation: ## build container image for emulation
BUILD_ARGS="$(BUILD_ARGS) --build-arg fv3_build_env_tag=emulation" \
COMPILE_OPTION="CALLPYFORT=Y" \
COMPILED_IMAGE=$(EMULATION_IMAGE) \
$(MAKE) build_compiled

build_deps: ## build container images of dependnecies (FMS, ESMF, SerialBox)
docker build -f $(DOCKERFILE) -t $(MPI_IMAGE) $(BUILD_ARGS) --target fv3gfs-mpi .
docker build -f $(DOCKERFILE) -t $(FMS_IMAGE) $(BUILD_ARGS) --target fv3gfs-fms .
docker build -f $(DOCKERFILE) -t $(ESMF_IMAGE) $(BUILD_ARGS) --target fv3gfs-esmf .
docker build -f $(DOCKERFILE) -t $(SERIALBOX_IMAGE) $(BUILD_ARGS) --target fv3gfs-environment-serialbox .
docker build -f $(DOCKERFILE) -t $(CALLPYFORT_IMAGE) $(BUILD_ARGS) --target fv3gfs-environment-emulation .

push_image_%:
docker tag $(GCR_URL)/$*:$(DEP_TAG_NAME) $(GCR_URL)/$*:$(DEP_TAG_NAME)-$(COMMIT_SHA)
docker push $(GCR_URL)/$*:$(DEP_TAG_NAME)
docker push $(GCR_URL)/$*:$(DEP_TAG_NAME)-$(COMMIT_SHA)

## push container images of dependencies to GCP
push_deps: push_image_mpi-build push_image_fms-build push_image_esmf-build push_image_serialbox-build
push_deps: push_image_mpi-build push_image_fms-build push_image_esmf-build push_image_serialbox-build push_image_emulation-build

pull_deps: ## pull container images of dependencies from GCP (for faster builds)
docker pull $(MPI_IMAGE)
docker pull $(FMS_IMAGE)
docker pull $(ESMF_IMAGE)
docker pull $(SERIALBOX_IMAGE)
docker pull $(CALLPYFORT_IMAGE)

enter: ## run and enter production container for development
docker run --rm \
Expand Down
48 changes: 47 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ARG serialize=false
ARG fv3_build_env_tag=standard
ARG BASE_IMAGE
ARG MPI_IMAGE=fv3gfs-mpi
ARG FMS_IMAGE=fv3gfs-fms
Expand Down Expand Up @@ -102,6 +103,45 @@ RUN cd serialbox && \
# setup link for backwards comatibility
RUN ln -s /serialbox /serialbox2

##
## Call_py_fort environment
##-----------------------------------------------------

FROM fv3gfs-environment as fv3gfs-environment-emulation

# Python stuff for callpyfort

RUN apt-get update && \
DEBIAN_FRONTEND="noninteractive" apt-get -y install tzdata && \
apt-get install -y --no-install-recommends \
cmake \
python3 \
libpython3-dev \
python3-dev \
python3-pip

RUN ln -s /usr/bin/python3 /usr/bin/python
RUN python3 -m pip install -U pip setuptools && \
python3 -m pip install cffi cython


# install call-py-fort

RUN cd /opt && git clone https://github.com/nbren12/call_py_fort.git --branch=v0.2.0
ENV CALLPY=/opt/call_py_fort \
PYTHONPATH=${CALLPY}/src/:${CALLPY}/test:$PYTHONPATH
RUN cd ${CALLPY} && make && make install && ldconfig

# install fv3gfs-util for monitoring-related code
RUN python3 -m pip install git+https://github.com/VulcanClimateModeling/fv3gfs-util.git@1d7c302b836befe905d776b0a972f464bfd3a255

# copy emulation target
COPY emulation/ /emulation
RUN cd /emulation && python3 -m pip install .

ENV VAR_META_PATH=/emulation/microphysics_parameter_metadata.yaml \
OUTPUT_FREQ_SEC=18000

## Build FMS
##---------------------------------------------------------------------------------
FROM fv3gfs-environment AS fv3gfs-fms
Expand Down Expand Up @@ -189,15 +229,21 @@ RUN cp $FV3_BASE/conf/$configure_file \
cd $FV3_BASE && \
make serialize_preprocess


## Convert FV3GFS sources for emulation image
##---------------------------------------------------------------------------------

FROM fv3gfs-src-serialize-$serialize AS fv3gfs-sources

FROM $FMS_IMAGE AS fms_image
FROM $ESMF_IMAGE AS esmf_image
FROM $SERIALBOX_IMAGE AS serialbox_image

FROM fv3gfs-environment AS fv3gfs-environment-standard

## Build FV3 executable in its own image
##---------------------------------------------------------------------------------
FROM fv3gfs-environment AS fv3gfs-build
FROM fv3gfs-environment-${fv3_build_env_tag} AS fv3gfs-build

ENV SERIALBOX_DIR=/serialbox \
SERIALBOX_OUTDIR=/FV3 \
Expand Down
27 changes: 27 additions & 0 deletions emulation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
emulation
=========

This is a stripped down set of modules for adding into a prognostic run using `call_py_fort`. It's currently used to create training datasets directly from data from the microphysics parameterization.

The interaction points with python are located in `GFS_physics_driver.f90` as the `set_state` or `get_state` commands. These are enabled for the emulation image (compiled using `make build_emulation`) when `-DENABLE_CALLPYFORT` is passed to the compiler.

### Example snippet

```
#ifdef ENABLE_CALLPYFORT
do k=1,levs
do i=1,im
qv_post_precpd(i,k) = Stateout%gq0(i,k,1)
qc_post_precpd(i,k) = Stateout%gq0(i,k,ntcw)
enddo
enddo
call set_state("air_temperature_output", Stateout%gt0)
call set_state("specific_humidity_output", qv_post_precpd)
call set_state("cloud_water_mixing_ratio_output", qc_post_precpd)
```

## Training Data
By default, the training data is saved out to the current working directory with a zarr monitor to state_output.zarr (time, tile, x, y), or individual netCDF files for each time and tile under $(cwd)/netcdf_output.

To change the frequency for which data are saved (defaults to 5 hours [18,000 s]), prescribe the `OUTPUT_FREQ_SEC` environment variable in the runtime image.
Empty file added emulation/emulation/__init__.py
Empty file.
43 changes: 43 additions & 0 deletions emulation/emulation/_filesystem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# keras has several routines which interact with file paths directly as opposed to
# filesystem objects, which means we need these wrappers so we can allow remote paths

import contextlib
import tempfile
import fsspec
import os


@contextlib.contextmanager
def put_dir(path: str):
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
fs, _, _ = fsspec.get_fs_token_paths(path)
fs.makedirs(os.path.dirname(path), exist_ok=True)
# cannot use fs.put as it cannot merge directories
_put_directory(tmpdir, path)


@contextlib.contextmanager
def get_dir(path: str):
with tempfile.TemporaryDirectory() as tmpdir:
fs, _, _ = fsspec.get_fs_token_paths(path)
# fsspec places the directory inside the tmpdir, as a subdirectory
fs.get(path, tmpdir, recursive=True)
yield tmpdir


def _put_directory(
local_source_dir: str, dest_dir: str, fs: fsspec.AbstractFileSystem = None,
):
"""Copy the contents of a local directory to a local or remote directory.
"""
if fs is None:
fs, _, _ = fsspec.get_fs_token_paths(dest_dir)
fs.makedirs(dest_dir, exist_ok=True)
for token in os.listdir(local_source_dir):
source = os.path.join(os.path.abspath(local_source_dir), token)
dest = os.path.join(dest_dir, token)
if os.path.isdir(source):
_put_directory(source, dest, fs=fs)
else:
fs.put(source, dest)
53 changes: 53 additions & 0 deletions emulation/emulation/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
A module for testing/debugging call routines
"""
import logging
import os
import traceback
import numpy as np
from datetime import datetime

logger = logging.getLogger(__name__)


def print_errors(func):
def new_func(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(traceback.print_exc())
raise e

return new_func


def print_arr_info(state):

logger = logging.getLogger(__name__)
for varname, arr in state.items():
logger.info(f"{varname}: shape[{arr.shape}] isfortran[{np.isfortran(arr)}]")


def print_location_ping(state):

logger = logging.getLogger(__name__)
logger.info("Ping reached!")


def dump_state(state):

DUMP_PATH = str(os.environ.get("STATE_DUMP_PATH"))

logger = logging.getLogger(__name__)

try:
rank = state.get("rank")
except KeyError:
logger.info("Could not save state. No rank included in state.")
return

time_str = datetime.now().strftime("%Y%m%d.%H%M%S")
filename = f"state_dump.{time_str}.tile{int(rank.squeeze()[0])}.npz"
outfile = os.path.join(DUMP_PATH, filename)
logger.info(f"Dumping state to {outfile}")
np.savez(outfile, **state)
Loading

0 comments on commit 201f4cc

Please sign in to comment.