Skip to content

Commit 40d9a6c

Browse files
Merge pull request #25 from gregory-halverson-jpl/main
- removing hard-coded version - removing numpy version limit - organizing notebooks - zero cloud optical thickness correction
2 parents f253b7a + 09c563f commit 40d9a6c

13 files changed

+785
-86
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,5 @@ cython_debug/
160160
# and can be added to the global gitignore or merged into this file. For a more nuclear
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162162
.idea/
163+
164+
.vscode

FLiESANN/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
Artificial Neural Network Implementation
44
for the Breathing Earth Systems Simulator (BESS)
55
"""
6+
import warnings
67

78
from .FLiESANN import *
9+
from .version import __version__
810

9-
from os.path import join, abspath, dirname
10-
11-
with open(join(abspath(dirname(__file__)), "version.txt")) as f:
12-
version = f.read()
13-
14-
__version__ = version
1511
__author__ = "Gregory H. Halverson, Robert Freepartner, Hideki Kobayashi, Youngryel Ryu"
12+
13+
warnings.filterwarnings(
14+
"ignore",
15+
message="__array_wrap__ must accept context and return_scalar arguments*",
16+
category=DeprecationWarning
17+
)

FLiESANN/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
GEOS5FP_DIRECTORY = "~/data/GEOS5FP_download"
77

8-
DEFAULT_MODEL_FILENAME = join(abspath(dirname(__file__)), "FLiESANN.h5")
8+
MODEL_FILENAME = join(abspath(dirname(__file__)), "FLiESANN.h5")
9+
ZERO_COT_CORRECTION = True
910
SPLIT_ATYPES_CTYPES = True
1011

1112
DEFAULT_PREVIEW_QUALITY = 20

FLiESANN/load_FLiES_model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ def mae(y_true, y_pred):
1919
return tf.reduce_mean(tf.abs(y_true - y_pred))
2020

2121
def load_FLiES_model(model_filename: str = DEFAULT_MODEL_FILENAME):
22-
return load_model(model_filename, custom_objects={'mae': mae})
22+
with warnings.catch_warnings():
23+
warnings.simplefilter("ignore")
24+
return load_model(model_filename, custom_objects={'mae': mae}, compile=False)

FLiESANN/process_FLiES_ANN.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ def FLiESANN(
3232
NASADEM_connection: NASADEMConnection = NASADEM,
3333
resampling: str = "cubic",
3434
ANN_model=None,
35-
model_filename=DEFAULT_MODEL_FILENAME,
36-
split_atypes_ctypes=SPLIT_ATYPES_CTYPES) -> dict:
35+
model_filename: str = MODEL_FILENAME,
36+
split_atypes_ctypes: bool = SPLIT_ATYPES_CTYPES,
37+
zero_COT_correction: bool = ZERO_COT_CORRECTION) -> dict:
3738
"""
3839
Processes Forest Light Environmental Simulator (FLiES) calculations using an
3940
artificial neural network (ANN) emulator.
@@ -115,7 +116,9 @@ def FLiESANN(
115116
if KG_climate is None:
116117
raise ValueError("Koppen Geieger climate classification or geometry must be given")
117118

118-
if COT is None and geometry is not None and time_UTC is not None:
119+
if zero_COT_correction:
120+
COT = np.zeros(albedo.shape, dtype=np.float32)
121+
elif COT is None and geometry is not None and time_UTC is not None:
119122
COT = GEOS5FP_connection.COT(
120123
time_UTC=time_UTC,
121124
geometry=geometry,

FLiESANN/run_FLiES_ANN_inference.py

Lines changed: 100 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def run_FLiES_ANN_inference(
1515
elevation_km: np.ndarray,
1616
SZA: np.ndarray,
1717
ANN_model=None,
18-
model_filename=DEFAULT_MODEL_FILENAME,
18+
model_filename=MODEL_FILENAME,
1919
split_atypes_ctypes=SPLIT_ATYPES_CTYPES) -> dict:
2020
"""
2121
Runs inference for an artificial neural network (ANN) emulator of the Forest Light
@@ -52,76 +52,106 @@ def run_FLiES_ANN_inference(
5252
- 'fdnir': Diffuse fraction of radiation in the near-infrared band (np.ndarray).
5353
"""
5454

55-
if ANN_model is None:
56-
# Load the ANN model if not provided
57-
ANN_model = load_FLiES_model(model_filename)
58-
59-
# Prepare inputs for the ANN model
60-
inputs = prepare_FLiES_ANN_inputs(
61-
atype=atype,
62-
ctype=ctype,
63-
COT=COT,
64-
AOT=AOT,
65-
vapor_gccm=vapor_gccm,
66-
ozone_cm=ozone_cm,
67-
albedo=albedo,
68-
elevation_km=elevation_km,
69-
SZA=SZA,
70-
split_atypes_ctypes=split_atypes_ctypes
71-
)
55+
import os
56+
import warnings
57+
# Save current TF_CPP_MIN_LOG_LEVEL and TF logger level
58+
old_tf_log_level = os.environ.get('TF_CPP_MIN_LOG_LEVEL', None)
59+
try:
60+
import tensorflow as tf
61+
old_logger_level = tf.get_logger().level
62+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
63+
tf.get_logger().setLevel('ERROR')
64+
except Exception:
65+
old_logger_level = None
7266

73-
# Convert DataFrame to numpy array and reshape for the model
74-
inputs_array = inputs.values
75-
76-
# Check what input shape the model expects and adapt accordingly
77-
# Different TensorFlow/Keras versions may have different input requirements
7867
try:
79-
model_input_shape = ANN_model.input_shape
80-
if len(model_input_shape) == 3:
81-
# Model expects 3D input: (batch_size, sequence_length, features)
82-
# Reshape from (batch_size, features) to (batch_size, 1, features)
83-
inputs_array = inputs_array.reshape(inputs_array.shape[0], 1, inputs_array.shape[1])
84-
expects_3d = True
85-
elif len(model_input_shape) == 2:
86-
# Model expects 2D input: (batch_size, features)
87-
# Keep the original 2D shape
88-
expects_3d = False
89-
else:
90-
# Fallback: try 2D first
68+
if ANN_model is None:
69+
# Load the ANN model if not provided
70+
ANN_model = load_FLiES_model(model_filename)
71+
72+
# Prepare inputs for the ANN model
73+
inputs = prepare_FLiES_ANN_inputs(
74+
atype=atype,
75+
ctype=ctype,
76+
COT=COT,
77+
AOT=AOT,
78+
vapor_gccm=vapor_gccm,
79+
ozone_cm=ozone_cm,
80+
albedo=albedo,
81+
elevation_km=elevation_km,
82+
SZA=SZA,
83+
split_atypes_ctypes=split_atypes_ctypes
84+
)
85+
86+
# Convert DataFrame to numpy array and reshape for the model
87+
inputs_array = inputs.values
88+
89+
# Check what input shape the model expects and adapt accordingly
90+
# Different TensorFlow/Keras versions may have different input requirements
91+
try:
92+
model_input_shape = ANN_model.input_shape
93+
if len(model_input_shape) == 3:
94+
# Model expects 3D input: (batch_size, sequence_length, features)
95+
# Reshape from (batch_size, features) to (batch_size, 1, features)
96+
inputs_array = inputs_array.reshape(inputs_array.shape[0], 1, inputs_array.shape[1])
97+
expects_3d = True
98+
elif len(model_input_shape) == 2:
99+
# Model expects 2D input: (batch_size, features)
100+
# Keep the original 2D shape
101+
expects_3d = False
102+
else:
103+
# Fallback: try 2D first
104+
expects_3d = False
105+
except (AttributeError, TypeError):
106+
# If input_shape is not available, try 2D first
91107
expects_3d = False
92-
except (AttributeError, TypeError):
93-
# If input_shape is not available, try 2D first
94-
expects_3d = False
95-
96-
# Run inference using the ANN model
97-
try:
98-
outputs = ANN_model.predict(inputs_array)
99-
except ValueError as e:
100-
error_msg = str(e)
101-
if not expects_3d and ("expected shape" in error_msg or "incompatible" in error_msg):
102-
# Try reshaping to 3D if 2D failed
103-
inputs_array = inputs.values # Reset to original 2D shape
104-
inputs_array = inputs_array.reshape(inputs_array.shape[0], 1, inputs_array.shape[1])
105-
outputs = ANN_model.predict(inputs_array)
106-
expects_3d = True
107-
else:
108-
raise e
109-
110-
# Handle output dimensions based on input dimensions used
111-
if expects_3d and len(outputs.shape) == 3:
112-
outputs = outputs.squeeze(axis=1)
113-
114-
shape = COT.shape
115-
116-
# Prepare the results dictionary
117-
results = {
118-
'tm': np.clip(outputs[:, 0].reshape(shape), 0, 1).astype(np.float32), # Total transmittance
119-
'puv': np.clip(outputs[:, 1].reshape(shape), 0, 1).astype(np.float32), # Proportion of UV radiation
120-
'pvis': np.clip(outputs[:, 2].reshape(shape), 0, 1).astype(np.float32), # Proportion of visible radiation
121-
'pnir': np.clip(outputs[:, 3].reshape(shape), 0, 1).astype(np.float32), # Proportion of NIR radiation
122-
'fduv': np.clip(outputs[:, 4].reshape(shape), 0, 1).astype(np.float32), # Diffuse fraction of UV radiation
123-
'fdvis': np.clip(outputs[:, 5].reshape(shape), 0, 1).astype(np.float32), # Diffuse fraction of visible radiation
124-
'fdnir': np.clip(outputs[:, 6].reshape(shape), 0, 1).astype(np.float32) # Diffuse fraction of NIR radiation
125-
}
126108

127-
return results
109+
# Run inference using the ANN model with warnings suppressed
110+
try:
111+
with warnings.catch_warnings():
112+
warnings.simplefilter("ignore")
113+
outputs = ANN_model.predict(inputs_array)
114+
except ValueError as e:
115+
error_msg = str(e)
116+
if not expects_3d and ("expected shape" in error_msg or "incompatible" in error_msg):
117+
# Try reshaping to 3D if 2D failed
118+
inputs_array = inputs.values # Reset to original 2D shape
119+
inputs_array = inputs_array.reshape(inputs_array.shape[0], 1, inputs_array.shape[1])
120+
with warnings.catch_warnings():
121+
warnings.simplefilter("ignore")
122+
outputs = ANN_model.predict(inputs_array)
123+
expects_3d = True
124+
else:
125+
raise e
126+
127+
# Handle output dimensions based on input dimensions used
128+
if expects_3d and len(outputs.shape) == 3:
129+
outputs = outputs.squeeze(axis=1)
130+
131+
shape = COT.shape
132+
133+
# Prepare the results dictionary
134+
results = {
135+
'tm': np.clip(outputs[:, 0].reshape(shape), 0, 1).astype(np.float32), # Total transmittance
136+
'puv': np.clip(outputs[:, 1].reshape(shape), 0, 1).astype(np.float32), # Proportion of UV radiation
137+
'pvis': np.clip(outputs[:, 2].reshape(shape), 0, 1).astype(np.float32), # Proportion of visible radiation
138+
'pnir': np.clip(outputs[:, 3].reshape(shape), 0, 1).astype(np.float32), # Proportion of NIR radiation
139+
'fduv': np.clip(outputs[:, 4].reshape(shape), 0, 1).astype(np.float32), # Diffuse fraction of UV radiation
140+
'fdvis': np.clip(outputs[:, 5].reshape(shape), 0, 1).astype(np.float32), # Diffuse fraction of visible radiation
141+
'fdnir': np.clip(outputs[:, 6].reshape(shape), 0, 1).astype(np.float32) # Diffuse fraction of NIR radiation
142+
}
143+
144+
return results
145+
finally:
146+
# Restore previous TF_CPP_MIN_LOG_LEVEL and logger level
147+
if old_tf_log_level is not None:
148+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = old_tf_log_level
149+
else:
150+
if 'TF_CPP_MIN_LOG_LEVEL' in os.environ:
151+
del os.environ['TF_CPP_MIN_LOG_LEVEL']
152+
try:
153+
import tensorflow as tf
154+
if old_logger_level is not None:
155+
tf.get_logger().setLevel(old_logger_level)
156+
except Exception:
157+
pass

FLiESANN/version.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from importlib.metadata import version
2+
3+
__version__ = version("FLiESANN")

FLiESANN/version.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

notebooks/ECOSTRESS example with automated inputs.ipynb

Lines changed: 266 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)