Skip to content

Commit 0bd0bfe

Browse files
Merge pull request #1 from gregory-halverson-jpl/main
initial release
2 parents b0b4461 + bc54bcf commit 0bd0bfe

10 files changed

+377
-2
lines changed

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
python-version: ["3.10"]
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v3 # This is already the latest version
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v4 # This is also the latest version
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
pip install .[dev] # Use dev to install pytest
31+
32+
- name: Run tests
33+
run: |
34+
pytest

.github/workflows/python-publish.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# This workflow will upload a Python Package using Twine when a release is created
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3+
4+
# This workflow uses actions that are not certified by GitHub.
5+
# They are provided by a third-party and are governed by
6+
# separate terms of service, privacy policy, and support
7+
# documentation.
8+
9+
name: Upload Python Package
10+
11+
on:
12+
release:
13+
types: [published]
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
deploy:
20+
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
- name: Set up Python
26+
uses: actions/setup-python@v3
27+
with:
28+
python-version: '3.x'
29+
- name: Install dependencies
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install build
33+
- name: Build package
34+
run: python -m build
35+
- name: Publish package
36+
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37+
with:
38+
user: __token__
39+
password: ${{ secrets.PYPI_API_TOKEN }}

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1-
# verma-net-radiation
2-
Net Radiation and Daily Upscaling Remote Sensing in Python
1+
# Net Radiation and Daily Upscaling Remote Sensing in Python
2+
3+
This Python package implements the net radiation and daily upscaling methods described in Verma et al 2016.
4+
5+
[Gregory H. Halverson](https://github.com/gregory-halverson-jpl) (they/them)<br>
6+
[gregory.h.halverson@jpl.nasa.gov](mailto:gregory.h.halverson@jpl.nasa.gov)<br>
7+
Lead developer<br>
8+
NASA Jet Propulsion Laboratory 329G
9+
10+
## References
11+
12+
Verma, M., Fisher, J. B., Mallick, K., Ryu, Y., Kobayashi, H., Guillaume, A., Moore, G., Ramakrishnan, L., Hendrix, V. C., Wolf, S., Sikka, M., Kiely, G., Wohlfahrt, G., Gielen, B., Roupsard, O., Toscano, P., Arain, A., & Cescatti, A. (2016). Global surface net-radiation at 5 km from MODIS Terra. *Remote Sensing, 8*, 739. [Link](https://api.semanticscholar.org/CorpusID:1517647)

makefile

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.PHONY: dist
2+
3+
PACKAGE_NAME = verma-net-radiation
4+
ENVIRONMENT_NAME = $(PACKAGE_NAME)
5+
DOCKER_IMAGE_NAME = $(PACKAGE_NAME)
6+
7+
clean:
8+
rm -rf *.o *.out *.log
9+
rm -rf build/
10+
rm -rf dist/
11+
rm -rf *.egg-info
12+
rm -rf .pytest_cache
13+
find . -type d -name "__pycache__" -exec rm -rf {} +
14+
15+
test:
16+
pytest
17+
18+
build:
19+
python -m build
20+
21+
twine-upload:
22+
twine upload dist/*
23+
24+
dist:
25+
make clean
26+
make build
27+
make twine-upload
28+
29+
remove-environment:
30+
mamba env remove -y -n $(ENVIRONMENT_NAME)
31+
32+
install:
33+
pip install -e .[dev]
34+
35+
uninstall:
36+
pip uninstall $(PACKAGE_NAME)
37+
38+
reinstall:
39+
make uninstall
40+
make install
41+
42+
environment:
43+
mamba create -y -n $(ENVIRONMENT_NAME) -c conda-forge python=3.10
44+
45+
colima-start:
46+
colima start -m 16 -a x86_64 -d 100
47+
48+
docker-build:
49+
docker build -t $(DOCKER_IMAGE_NAME):latest .
50+
51+
docker-build-environment:
52+
docker build --target environment -t $(DOCKER_IMAGE_NAME):latest .
53+
54+
docker-build-installation:
55+
docker build --target installation -t $(DOCKER_IMAGE_NAME):latest .
56+
57+
docker-interactive:
58+
docker run -it $(DOCKER_IMAGE_NAME) fish
59+
60+
docker-remove:
61+
docker rmi -f $(DOCKER_IMAGE_NAME)

pyproject.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
4+
[project]
5+
name = "verma-net-radiation"
6+
version = "1.0.1"
7+
description = "Net Radiation and Daily Upscaling Remote Sensing in Python"
8+
readme = "README.md"
9+
authors = [
10+
{ name = "Gregory Halverson", email = "gregory.h.halverson@jpl.nasa.gov" }
11+
]
12+
classifiers = [
13+
"Programming Language :: Python :: 3",
14+
"Operating System :: OS Independent",
15+
]
16+
dependencies = [
17+
"numpy",
18+
"rasters"
19+
]
20+
requires-python = ">=3.10"
21+
22+
[project.optional-dependencies]
23+
dev = [
24+
"build",
25+
"pytest>=6.0",
26+
"pytest-cov",
27+
"jupyter",
28+
"pytest",
29+
"twine"
30+
]
31+
32+
[tool.setuptools.package-data]
33+
verma_net_radiation = ["*.txt", "*.tif"]
34+
35+
[project.urls]
36+
"Homepage" = "https://github.com/JPL-Evapotranspiration-Algorithms/verma-net-radiation"

tests/test_import_dependencies.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pytest
2+
3+
# List of dependencies
4+
dependencies = [
5+
"numpy",
6+
"rasters"
7+
]
8+
9+
# Generate individual test functions for each dependency
10+
@pytest.mark.parametrize("dependency", dependencies)
11+
def test_dependency_import(dependency):
12+
__import__(dependency)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test_import_verma_net_radiation():
2+
import verma_net_radiation

verma_net_radiation/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .verma_net_radiation import *
2+
3+
from os.path import join, abspath, dirname
4+
5+
with open(join(abspath(dirname(__file__)), "version.txt")) as f:
6+
version = f.read()
7+
8+
__version__ = version
9+
__author__ = "Gregory H. Halverson"
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from typing import Union, Dict
2+
import warnings
3+
import numpy as np
4+
import rasters as rt
5+
from rasters import Raster
6+
7+
STEFAN_BOLTZMAN_CONSTANT = 5.67036713e-8 # SI units watts per square meter per kelvin to the fourth
8+
9+
10+
def daylight_from_SHA(SHA_deg: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
11+
"""
12+
This function calculates daylight hours from sunrise hour angle in degrees.
13+
"""
14+
return (2.0 / 15.0) * SHA_deg
15+
16+
17+
def sunrise_from_SHA(SHA_deg: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
18+
"""
19+
This function calculates sunrise hour from sunrise hour angle in degrees.
20+
"""
21+
return 12.0 - (SHA_deg / 15.0)
22+
23+
24+
def solar_dec_deg_from_day_angle_rad(day_angle_rad: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
25+
"""
26+
This function calculates solar declination in degrees from day angle in radians.
27+
"""
28+
return (0.006918 - 0.399912 * np.cos(day_angle_rad) + 0.070257 * np.sin(day_angle_rad) - 0.006758 * np.cos(
29+
2 * day_angle_rad) + 0.000907 * np.sin(2 * day_angle_rad) - 0.002697 * np.cos(
30+
3 * day_angle_rad) + 0.00148 * np.sin(
31+
3 * day_angle_rad)) * (180 / np.pi)
32+
33+
34+
def day_angle_rad_from_doy(doy: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
35+
"""
36+
This function calculates day angle in radians from day of year between 1 and 365.
37+
"""
38+
return (2 * np.pi * (doy - 1)) / 365
39+
40+
41+
def SHA_deg_from_doy_lat(doy: Union[Raster, np.ndarray, int], latitude: np.ndarray) -> Raster:
42+
"""
43+
This function calculates sunrise hour angle in degrees from latitude in degrees and day of year between 1 and 365.
44+
"""
45+
# calculate day angle in radians
46+
day_angle_rad = day_angle_rad_from_doy(doy)
47+
48+
# calculate solar declination in degrees
49+
solar_dec_deg = solar_dec_deg_from_day_angle_rad(day_angle_rad)
50+
51+
# convert latitude to radians
52+
latitude_rad = np.radians(latitude)
53+
54+
# convert solar declination to radians
55+
solar_dec_rad = np.radians(solar_dec_deg)
56+
57+
# calculate cosine of sunrise angle at latitude and solar declination
58+
# need to keep the cosine for polar correction
59+
sunrise_cos = -np.tan(latitude_rad) * np.tan(solar_dec_rad)
60+
61+
# calculate sunrise angle in radians from cosine
62+
with warnings.catch_warnings():
63+
warnings.filterwarnings("ignore")
64+
sunrise_rad = np.arccos(sunrise_cos)
65+
66+
# convert to degrees
67+
sunrise_deg = np.degrees(sunrise_rad)
68+
69+
# apply polar correction
70+
sunrise_deg = rt.where(sunrise_cos >= 1, 0, sunrise_deg)
71+
sunrise_deg = rt.where(sunrise_cos <= -1, 180, sunrise_deg)
72+
73+
return sunrise_deg
74+
75+
def process_verma_net_radiation(
76+
SWin: np.ndarray,
77+
albedo: np.ndarray,
78+
ST_C: np.ndarray,
79+
emissivity: np.ndarray,
80+
Ta_C: np.ndarray,
81+
RH: np.ndarray,
82+
cloud_mask: np.ndarray = None) -> Dict:
83+
results = {}
84+
85+
# Convert surface temperature from Celsius to Kelvin
86+
ST_K = ST_C + 273.15
87+
88+
# Convert air temperature from Celsius to Kelvin
89+
Ta_K = Ta_C + 273.15
90+
91+
# Calculate water vapor pressure in Pascals using air temperature and relative humidity
92+
Ea_Pa = (RH * 0.6113 * (10 ** (7.5 * (Ta_K - 273.15) / (Ta_K - 35.85)))) * 1000
93+
94+
# constrain albedo between 0 and 1
95+
albedo = np.clip(albedo, 0, 1)
96+
97+
# calculate outgoing shortwave from incoming shortwave and albedo
98+
SWout = np.clip(SWin * albedo, 0, None)
99+
results["SWout"] = SWout
100+
101+
# calculate instantaneous net radiation from components
102+
SWnet = np.clip(SWin - SWout, 0, None)
103+
104+
# calculate atmospheric emissivity
105+
eta1 = 0.465 * Ea_Pa / Ta_K
106+
# atmospheric_emissivity = (1 - (1 + eta1) * np.exp(-(1.2 + 3 * eta1) ** 0.5))
107+
eta2 = -(1.2 + 3 * eta1) ** 0.5
108+
eta2 = eta2.astype(float)
109+
eta3 = np.exp(eta2)
110+
atmospheric_emissivity = np.where(eta2 != 0, (1 - (1 + eta1) * eta3), np.nan)
111+
112+
if cloud_mask is None:
113+
# calculate incoming longwave for clear sky
114+
LWin = atmospheric_emissivity * STEFAN_BOLTZMAN_CONSTANT * Ta_K ** 4
115+
else:
116+
# calculate incoming longwave for clear sky and cloudy
117+
LWin = np.where(
118+
~cloud_mask,
119+
atmospheric_emissivity * STEFAN_BOLTZMAN_CONSTANT * Ta_K ** 4,
120+
STEFAN_BOLTZMAN_CONSTANT * Ta_K ** 4
121+
)
122+
123+
results["LWin"] = LWin
124+
125+
# constrain emissivity between 0 and 1
126+
emissivity = np.clip(emissivity, 0, 1)
127+
128+
# calculate outgoing longwave from land surface temperature and emissivity
129+
LWout = emissivity * STEFAN_BOLTZMAN_CONSTANT * ST_K ** 4
130+
results["LWout"] = LWout
131+
132+
# LWnet = np.clip(LWin - LWout, 0, None)
133+
LWnet = np.clip(LWin - LWout, 0, None)
134+
135+
# constrain negative values of instantaneous net radiation
136+
Rn = np.clip(SWnet + LWnet, 0, None)
137+
results["Rn"] = Rn
138+
139+
return results
140+
141+
def daily_Rn_integration_verma(
142+
Rn: Union[Raster, np.ndarray],
143+
hour_of_day: Union[Raster, np.ndarray],
144+
doy: Union[Raster, np.ndarray] = None,
145+
lat: Union[Raster, np.ndarray] = None,
146+
sunrise_hour: Union[Raster, np.ndarray] = None,
147+
daylight_hours: Union[Raster, np.ndarray] = None) -> Raster:
148+
"""
149+
calculate daily net radiation using solar parameters
150+
this is the average rate of energy transfer from sunrise to sunset
151+
in watts per square meter
152+
watts are joules per second
153+
to get the total amount of energy transferred, factor seconds out of joules
154+
the number of seconds for which this average is representative is (daylight_hours * 3600)
155+
documented in verma et al, bisht et al, and lagouARDe et al
156+
:param Rn:
157+
:param hour_of_day:
158+
:param sunrise_hour:
159+
:param daylight_hours:
160+
:return:
161+
"""
162+
if daylight_hours is None or sunrise_hour is None and doy is not None and lat is not None:
163+
sha_deg = SHA_deg_from_doy_lat(doy, lat)
164+
daylight_hours = daylight_from_SHA(sha_deg)
165+
sunrise_hour = sunrise_from_SHA(sha_deg)
166+
167+
with warnings.catch_warnings():
168+
warnings.filterwarnings("ignore")
169+
Rn_daily = 1.6 * Rn / (np.pi * np.sin(np.pi * (hour_of_day - sunrise_hour) / (daylight_hours)))
170+
171+
return Rn_daily

verma_net_radiation/version.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.1

0 commit comments

Comments
 (0)