-
Notifications
You must be signed in to change notification settings - Fork 32
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
Add the "state of ground" to bring in the extra snow depth reports #1368
base: develop
Are you sure you want to change the base?
Changes from all commits
1c8096f
365675d
aee4dc2
0c02522
e57ac67
4376049
244693c
76a5e94
2a72e4d
a2d87fd
fbd1bb9
db46314
91f160e
dc8d9b8
d7bf5d7
3c1dbf7
55b54cd
5cd21ab
47bfb19
bb85eb6
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 |
---|---|---|
|
@@ -17,20 +17,17 @@ bufr: | |
query: "[*/CLON, */CLONH]" | ||
stationIdentification: | ||
query: "*/RPID" | ||
|
||
stationElevation: | ||
query: "[*/SELV, */HSMSL]" | ||
type: float | ||
|
||
# ObsValue | ||
totalSnowDepth: | ||
query: "[*/SNWSQ1/TOSD, */MTRMSC/TOSD, */STGDSNDM/TOSD]" | ||
transforms: | ||
- scale: 1000.0 | ||
filters: | ||
- bounding: | ||
variable: totalSnowDepth | ||
lowerBound: 0 | ||
upperBound: 10000000 | ||
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. For my understanding:
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 previously put this large number (10000m) here for removing the missing values. Here, we don't need this any more because we need to set the missing snod values to 0 when sogr satisfies the defined conditions. 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 might still have missing values though wouldn't we? 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. The missing values will be removed after applying sogr conditions. |
||
groundState: | ||
query: "[*/GRDSQ1/SOGR, */STGDSNDM/SOGR]" | ||
|
||
encoder: | ||
variables: | ||
|
@@ -65,11 +62,18 @@ encoder: | |
coordinates: "longitude latitude" | ||
source: variables/stationIdentification | ||
longName: "Identification of Observing Location" | ||
units: "m" | ||
units: "index" | ||
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. @jiaruidong2017 The unit of stationIdentification is not defined (empty) in the IODA convention table, and it is left for users to decide. Have you discussed the unit for |
||
|
||
# ObsValue | ||
- name: "ObsValue/totalSnowDepth" | ||
coordinates: "longitude latitude" | ||
source: variables/totalSnowDepth | ||
longName: "Total Snow Depth" | ||
units: "mm" | ||
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. @jiaruidong2017 The unit of the 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 have to keep using the 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. @jiaruidong2017 Is it possible to read in the totalSnowDepth in 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. It is possible, but will require additional efforts. We can discuss this issue with the physics team. 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. @jiaruidong2017 I guess only a one-line change is necessary for UFS. You just need to convert 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. This issue (mismatch between units proscribed by JEDI and those used in the UFS) is bigger than this PR - as all other snow depth IODA files are in mm too. If we change these observations to mm, we'll need to change the others. I suggest that we leave this in mm for now, and create a separate issue to address the unit mismatch (by introducing a unit transform somewhere). 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. Agreed. Let's keep it as "mm" for now. |
||
|
||
- name: "ObsValue/groundState" | ||
coordinates: "longitude latitude" | ||
source: variables/groundState | ||
longName: "STATE OF THE GROUND" | ||
units: "index" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
#!/usr/bin/env python3 | ||
import sys | ||
import os | ||
import argparse | ||
import time | ||
import numpy as np | ||
import bufr | ||
from pyioda.ioda.Engines.Bufr import Encoder as iodaEncoder | ||
from bufr.encoders.netcdf import Encoder as netcdfEncoder | ||
from wxflow import Logger | ||
|
||
# Initialize Logger | ||
# Get log level from the environment variable, default to 'INFO it not set | ||
log_level = os.getenv('LOG_LEVEL', 'INFO') | ||
logger = Logger('BUFR2IODA_sfcsno.py', level=log_level, colored_log=False) | ||
|
||
|
||
def logging(comm, level, message): | ||
""" | ||
Logs a message to the console or log file, based on the specified logging level. | ||
|
||
This function ensures that logging is only performed by the root process (`rank 0`) | ||
in a distributed computing environment. The function maps the logging level to | ||
appropriate logger methods and defaults to the 'INFO' level if an invalid level is provided. | ||
|
||
Parameters: | ||
comm: object | ||
The communicator object, typically from a distributed computing framework | ||
(e.g., MPI). It must have a `rank()` method to determine the process rank. | ||
level: str | ||
The logging level as a string. Supported levels are: | ||
- 'DEBUG' | ||
- 'INFO' | ||
- 'WARNING' | ||
- 'ERROR' | ||
- 'CRITICAL' | ||
If an invalid level is provided, a warning will be logged, and the level | ||
will default to 'INFO'. | ||
message: str | ||
The message to be logged. | ||
|
||
Behavior: | ||
- Logs messages only on the root process (`comm.rank() == 0`). | ||
- Maps the provided logging level to a method of the logger object. | ||
- Defaults to 'INFO' and logs a warning if an invalid logging level is given. | ||
- Supports standard logging levels for granular control over log verbosity. | ||
|
||
Example: | ||
>>> logging(comm, 'DEBUG', 'This is a debug message.') | ||
>>> logging(comm, 'ERROR', 'An error occurred!') | ||
|
||
Notes: | ||
- Ensure that a global `logger` object is configured before using this function. | ||
- The `comm` object should conform to MPI-like conventions (e.g., `rank()` method). | ||
""" | ||
|
||
if comm.rank() == 0: | ||
# Define a dictionary to map levels to logger methods | ||
log_methods = { | ||
'DEBUG': logger.debug, | ||
'INFO': logger.info, | ||
'WARNING': logger.warning, | ||
'ERROR': logger.error, | ||
'CRITICAL': logger.critical, | ||
} | ||
|
||
# Get the appropriate logging method, default to 'INFO' | ||
log_method = log_methods.get(level.upper(), logger.info) | ||
|
||
if log_method == logger.info and level.upper() not in log_methods: | ||
# Log a warning if the level is invalid | ||
logger.warning(f'log level = {level}: not a valid level --> set to INFO') | ||
|
||
# Call the logging method | ||
log_method(message) | ||
|
||
|
||
def _mask_container(container, mask): | ||
|
||
new_container = bufr.DataContainer() | ||
for var_name in container.list(): | ||
var = container.get(var_name) | ||
paths = container.get_paths(var_name) | ||
new_container.add(var_name, var[mask], paths) | ||
|
||
return new_container | ||
|
||
|
||
def _make_description(mapping_path, update=False): | ||
|
||
description = bufr.encoders.Description(mapping_path) | ||
|
||
return description | ||
|
||
|
||
def _make_obs(comm, input_path, mapping_path): | ||
""" | ||
Create the ioda snow depth observations: | ||
- reads state of ground (sogr) and snow depth (snod) | ||
- applys sogr conditions to the missing snod values | ||
- removes the filled/missing snow values and creates the masked container | ||
|
||
Parameters | ||
---------- | ||
comm: object | ||
The communicator object (e.g., MPI) | ||
input_path: str | ||
The input bufr file | ||
mapping_path: str | ||
The input bufr2ioda mapping file | ||
""" | ||
|
||
# Get container from mapping file first | ||
logging(comm, 'INFO', 'Get container from bufr') | ||
container = bufr.Parser(input_path, mapping_path).parse(comm) | ||
|
||
logging(comm, 'DEBUG', f'container list (original): {container.list()}') | ||
|
||
# Add new/derived data into container | ||
sogr = container.get('variables/groundState') | ||
snod = container.get('variables/totalSnowDepth') | ||
snod[(sogr <= 11.0) & snod.mask] = 0.0 | ||
snod[(sogr == 15.0) & snod.mask] = 0.0 | ||
snod.mask = (snod < 0.0) | snod.mask | ||
container.replace('variables/totalSnowDepth', snod) | ||
snod_upd = container.get('variables/totalSnowDepth') | ||
|
||
masked_container = _mask_container(container, (~snod.mask)) | ||
|
||
return masked_container | ||
|
||
|
||
def create_obs_group(input_path, mapping_path, env): | ||
|
||
comm = bufr.mpi.Comm(env["comm_name"]) | ||
|
||
description = _make_description(mapping_path, update=False) | ||
container = _make_obs(comm, input_path, mapping_path) | ||
|
||
# Gather data from all tasks into all tasks. Each task will have the complete record | ||
logging(comm, 'INFO', f'Gather data from all tasks into all tasks') | ||
container.all_gather(comm) | ||
|
||
# Encode the data | ||
logging(comm, 'INFO', f'Encode data') | ||
data = next(iter(iodaEncoder(mapping_path).encode(container).values())) | ||
|
||
logging(comm, 'INFO', f'Return the encoded data') | ||
|
||
return data | ||
|
||
|
||
def create_obs_file(input_path, mapping_path, output_path): | ||
|
||
comm = bufr.mpi.Comm("world") | ||
container = _make_obs(comm, input_path, mapping_path) | ||
container.gather(comm) | ||
|
||
description = _make_description(mapping_path, update=False) | ||
|
||
# Encode the data | ||
if comm.rank() == 0: | ||
netcdfEncoder(description).encode(container, output_path) | ||
|
||
logging(comm, 'INFO', f'Return the encoded data') | ||
|
||
|
||
if __name__ == '__main__': | ||
|
||
start_time = time.time() | ||
|
||
bufr.mpi.App(sys.argv) | ||
comm = bufr.mpi.Comm("world") | ||
|
||
# Required input arguments as positional arguments | ||
parser = argparse.ArgumentParser(description="Convert BUFR to NetCDF using a mapping file.") | ||
parser.add_argument('input', type=str, help='Input BUFR file') | ||
parser.add_argument('mapping', type=str, help='BUFR2IODA Mapping File') | ||
parser.add_argument('output', type=str, help='Output NetCDF file') | ||
|
||
args = parser.parse_args() | ||
mapping = args.mapping | ||
infile = args.input | ||
output = args.output | ||
|
||
create_obs_file(infile, mapping, output) | ||
|
||
end_time = time.time() | ||
running_time = end_time - start_time | ||
logging(comm, 'INFO', f'Total running time: {running_time}') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on IODA data convention, the unit of
totalSnowDepth
ism
. We should remove the conversion of it from meter to millimeter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above to keep use the conversion.