Skip to content

Commit 14d0c03

Browse files
committed
Tentative solution to handle daily radiation properly - also fixes for same matter in ET
1 parent a51cb96 commit 14d0c03

5 files changed

Lines changed: 145 additions & 56 deletions

File tree

hmachine/src/main/java/org/hortonmachine/hmachine/geoframe/ermworkflow/ErmPrestleyEt.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema;
1010
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.BasinDataSchema.BasinDataField;
1111
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.EnvironmentalVariableType;
12+
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.TimeResolution;
1213
import org.hortonmachine.hmachine.geoframe.utils.IWaterBudgetSimulationRunner;
1314
import org.hortonmachine.hmachine.geoframe.utils.PrestleyETAtCentroid;
1415

@@ -64,12 +65,21 @@ public class ErmPrestleyEt extends HMModel {
6465
@In
6566
public String pEndTimestamp;
6667

68+
@Description("The expected time resolution of the data. Daily and hourly (default) is supported.")
69+
@In
70+
public TimeResolution pTimeResolution = TimeResolution.HOURLY;
71+
6772
@Description("If true, existing output files are overwritten.")
6873
@In
6974
public boolean doOverwrite = false;
7075

7176
@Execute
7277
public void process() throws Exception {
78+
if (pTimeResolution == TimeResolution.MONTHLY || pTimeResolution == TimeResolution.YEARLY) {
79+
throw new UnsupportedOperationException(
80+
"ErmPrestleyEt only supports HOURLY and DAILY resolutions, got " + pTimeResolution);
81+
}
82+
7383
try (ASpatialDb db = EDb.GEOPACKAGE.getSpatialDb()) {
7484
db.open(inGpkg);
7585

@@ -99,7 +109,7 @@ public void process() throws Exception {
99109

100110
var ptEt = new PrestleyETAtCentroid();
101111
ptEt.inGeoframeDB = db;
102-
ptEt.isHourly = true;
112+
ptEt.isHourly = pTimeResolution == TimeResolution.HOURLY;
103113
ptEt.pAlpha = 1.26;
104114
ptEt.inTempReader = temperatureReader;
105115
ptEt.inNetReader = netReader;

hmachine/src/main/java/org/hortonmachine/hmachine/geoframe/ermworkflow/ErmRadiation.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@
4242
* database; humidity and the atmospheric clearness index are left at their
4343
* clear-sky defaults, since this launcher assumes clear-sky conditions with
4444
* only temperature known.
45+
*
46+
* <p>
47+
* Note that the output is always a flux, in W/m2:
48+
* <ul>
49+
* <li>for {@link TimeResolution#HOURLY} it is the instantaneous net
50+
* radiation for that hour</li>
51+
* <li>for {@link TimeResolution#DAILY} - the
52+
* avg of 24 hourly samples taken at that day's actual sun positions
53+
* </li>
54+
* </ul>
4555
*/
4656
@Description("Radiation calculator.")
4757
@Author(name = "Daniele Andreis", contact = "")
@@ -69,10 +79,15 @@ public class ErmRadiation extends HMModel {
6979
@In
7080
public String pEndTimestamp;
7181

72-
@Description("Time resolution. Defaults to hourly")
82+
@Description("The expected time resolution of the data. Daily and hourly (default) is supported.")
7383
@In
7484
public TimeResolution pTimeResolution = TimeResolution.HOURLY;
7585

86+
@Description("Number of sun-position samples used to average net radiation over a day if "
87+
+ "pTimeResolution is DAILY. 24 (one per hour) is the most accurate but also slowest.")
88+
@In
89+
public int pDailySubSamples = 24;
90+
7691
@Description("If true, existing output files are overwritten.")
7792
@In
7893
public boolean doOverwrite = false;
@@ -83,6 +98,11 @@ public class ErmRadiation extends HMModel {
8398

8499
@Execute
85100
public void process() throws Exception {
101+
if (pTimeResolution == TimeResolution.MONTHLY || pTimeResolution == TimeResolution.YEARLY) {
102+
throw new UnsupportedOperationException(
103+
"ErmRadiation only supports HOURLY and DAILY resolutions, got " + pTimeResolution);
104+
}
105+
86106
Paths p = new Paths(inDtm, doOverwrite);
87107

88108
try (ASpatialDb db = EDb.GEOPACKAGE.getSpatialDb()) {
@@ -116,7 +136,8 @@ public void process() throws Exception {
116136
radiation.dem = dtm; // TODO Daniele, why where you using the pit here?
117137
radiation.inSkyview = skyview;
118138
radiation.lwrvModeel = "6";
119-
radiation.doHourly = pTimeResolution == TimeResolution.HOURLY;
139+
radiation.pTimeResolution = pTimeResolution;
140+
radiation.pDailySubSamples = pDailySubSamples;
120141
radiation.init();
121142
radiation.process();
122143

hmachine/src/main/java/org/hortonmachine/hmachine/geoframe/io/database/TableUtils.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
import org.hortonmachine.dbs.compat.ASpatialDb;
77
import org.hortonmachine.dbs.compat.objects.QueryResult;
88
import org.hortonmachine.gears.libs.modules.HMConstants;
9-
import org.hortonmachine.hmachine.geoframe.io.database.tables.GeoFrameGeoTable;
10-
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.StationSchema.Station;
11-
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.StationSchema.StationType;
129
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.EnvironmentalVariable;
1310
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.EnvironmentalVariableType;
1411
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.TimeResolution;
@@ -26,27 +23,17 @@ public final static List<EnvironmentalVariable> getFixedEnviramentalVariable(Tim
2623

2724
String mmFlux = "mm/";
2825
String temperatureUnit = "°C";
29-
String radiationUnit = null;
26+
27+
// Radiation is always a flux and in W/m² and is always related to the size of the timestep.
28+
String radiationUnit = "W/m²";
3029
String dischargeUnit = "m³/s";
3130

3231
if (resolution != null) {
3332
switch (resolution) {
34-
case HOURLY -> {
35-
mmFlux = mmFlux + "h";
36-
radiationUnit = "W/m²";
37-
}
38-
case DAILY -> {
39-
mmFlux = mmFlux + "day";
40-
radiationUnit = "MJ/m²/day";
41-
}
42-
case MONTHLY -> {
43-
mmFlux = mmFlux + "month";
44-
radiationUnit = "MJ/m²/month";
45-
}
46-
case YEARLY -> {
47-
mmFlux = mmFlux + "year";
48-
radiationUnit = "MJ/m²/year";
49-
}
33+
case HOURLY -> mmFlux = mmFlux + "h";
34+
case DAILY -> mmFlux = mmFlux + "day";
35+
case MONTHLY -> mmFlux = mmFlux + "month";
36+
case YEARLY -> mmFlux = mmFlux + "year";
5037
}
5138
} else {
5239
mmFlux = null;

hmachine/src/main/java/org/hortonmachine/hmachine/geoframe/utils/RadiationAtCentroid.java

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.BasinPolygonSchema;
2828
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.BasinPolygonSchema.BasinMultiPolygonField;
2929
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.EnvironmentalVariableType;
30+
import org.hortonmachine.hmachine.geoframe.io.database.tables.implementation.VarSchema.TimeResolution;
3031
import org.hortonmachine.hmachine.geoframe.utils.radiation.NetRadiationPointCase;
3132
import org.hortonmachine.hmachine.geoframe.utils.radiation.LwrbPointCase.Lwrb;
3233
import org.hortonmachine.hmachine.geoframe.utils.radiation.swrbPointCase.ShortwaveRadiationBalancePointCase;
@@ -80,10 +81,15 @@ public class RadiationAtCentroid extends HMModel {
8081
@In
8182
public double bCloud = 1;
8283

83-
@Description("Toggle to compute hourly radiation values, if false daily values are computed")
84+
@Description("The expected time resolution of the data. Daily and hourly (default) is supported.")
8485
@In
85-
public boolean doHourly = true;
86-
86+
public TimeResolution pTimeResolution = TimeResolution.HOURLY;
87+
88+
@Description("Number of sun-position samples used to average net radiation over a day if "
89+
+ "pTimeResolution is DAILY. 24 (one per hour) is the most accurate but also slowest.")
90+
@In
91+
public int pDailySubSamples = 24;
92+
8793
public double alpha = 0.26;
8894

8995
@Description("Ozone layer thickness in cm")
@@ -129,11 +135,21 @@ public class RadiationAtCentroid extends HMModel {
129135
private HashMap<Integer, double[]> inNan;
130136
private SimpleFeatureCollection inBasinsFC;
131137

138+
private static final long MILLIS_PER_HOUR = 3_600_000L;
139+
private static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR;
140+
132141
@Initialize
133142
public void init() throws Exception {
134143
if (lwrvModeel == null || lwrvModeel.isEmpty()) {
135144
throw new IllegalArgumentException();
136145
}
146+
if (pTimeResolution == TimeResolution.MONTHLY || pTimeResolution == TimeResolution.YEARLY) {
147+
throw new UnsupportedOperationException(
148+
"ErmRadiation only supports HOURLY and DAILY resolutions, got " + pTimeResolution);
149+
}
150+
if (pDailySubSamples < 1) {
151+
throw new IllegalArgumentException("pDailySubSamples must be >= 1, got " + pDailySubSamples);
152+
}
137153
checkNull(inGeoframeDb);
138154

139155
if (!(inGeoframeDb.hasTable(GeoFrameGeoTable.BASIN.tableName())
@@ -201,7 +217,7 @@ private Lwrb createLwrb() {
201217
*/
202218
private ShortwaveRadiationBalancePointCase createSwrb() {
203219
ShortwaveRadiationBalancePointCase s = new ShortwaveRadiationBalancePointCase();
204-
s.doHourly = doHourly;
220+
s.doHourly = pTimeResolution == TimeResolution.HOURLY;
205221
s.fStationsid = BasinMultiPolygonField.ID.columnName();
206222
s.inStationsFC = inBasinsFC;
207223
s.inDem = dem;
@@ -289,23 +305,8 @@ public void process() throws Exception {
289305

290306
private void processTimestep(HashMap<Integer, double[]> temperature, HashMap<Integer, double[]> humidity,
291307
HashMap<Integer, double[]> clearSky, long t) throws Exception {
292-
lwrb.inAirTemperatureValuesHM = temperature;
293-
lwrb.inSoilTempratureValuesHM = temperature;
294-
lwrb.inHumidityValuesHM = humidity;
295-
lwrb.inClearnessIndexValuesHM = clearSky;
296-
swrb.inHumidityValues = humidity;
297-
swrb.inTemperatureValues = temperature;
298-
swrb.tCurrentDateString = GeoframeEnvDatabaseIterator.ts2str(t);
299-
300-
lwrb.process();
301-
swrb.process();
302-
303-
nrpc.inShortwaveValues = swrb.outHMtotal;
304-
nrpc.inDownwellingValues = lwrb.outHMlongwaveDownwellingHM;
305-
nrpc.inUpwellingValues = lwrb.outHMlongwaveUpwellingHM;
306-
nrpc.process();
307-
308-
HashMap<Integer, double[]> out = nrpc.outHMnetRad;
308+
HashMap<Integer, double[]> out = computeNetRadiation(lwrb, swrb, nrpc, temperature, humidity, clearSky, t);
309+
309310
String insertSql = GeoFrameSimpleTable.BASINDATA.getSchema().buildInsertAll();
310311
inGeoframeDb.execOnConnection(conn -> {
311312
boolean autoCommit = conn.getAutoCommit();
@@ -350,9 +351,7 @@ private static class TimestepResult {
350351
* <p>
351352
* The result map is copied out of {@code nrpc.outHMnetRad} because that
352353
* field is reused and overwritten in place on every {@code process()}
353-
* call on the same instance - if a thread goes on to compute another
354-
* timestep before this result is consumed, an uncopied reference would
355-
* end up pointing at that later timestep's data instead.
354+
* call on the same instance.
356355
*/
357356
private TimestepResult computeTimestepParallel(int idx, int[] ids, ThreadLocal<Lwrb> tlLwrb,
358357
ThreadLocal<ShortwaveRadiationBalancePointCase> tlSwrb, ThreadLocal<NetRadiationPointCase> tlNrpc)
@@ -376,23 +375,95 @@ private TimestepResult computeTimestepParallel(int idx, int[] ids, ThreadLocal<L
376375
ShortwaveRadiationBalancePointCase swrbLocal = tlSwrb.get();
377376
NetRadiationPointCase nrpcLocal = tlNrpc.get();
378377

378+
HashMap<Integer, double[]> out = computeNetRadiation(lwrbLocal, swrbLocal, nrpcLocal, temperature, humidity,
379+
clearSky, timestamp);
380+
return new TimestepResult(timestamp, out);
381+
}
382+
383+
/**
384+
* Computes net radiation (W/m2) for one timestep, dispatching to
385+
* either a single hourly sample or a daily average of 24 hourly samples
386+
* depending on the time resolution.
387+
*
388+
* <p>
389+
* The returned map is always a fresh copy, independent of
390+
* {@code nrpc.outHMnetRad}, which is reused and overwritten in place on
391+
* every {@code process()} call - without a copy, a thread going on to
392+
* compute another timestep before this result is consumed would leave the
393+
* caller holding a reference to that later timestep's data instead.
394+
*/
395+
private HashMap<Integer, double[]> computeNetRadiation(Lwrb lwrbLocal, ShortwaveRadiationBalancePointCase swrbLocal,
396+
NetRadiationPointCase nrpcLocal, HashMap<Integer, double[]> temperature, HashMap<Integer, double[]> humidity,
397+
HashMap<Integer, double[]> clearSky, long t) throws Exception {
379398
lwrbLocal.inAirTemperatureValuesHM = temperature;
380399
lwrbLocal.inSoilTempratureValuesHM = temperature;
381400
lwrbLocal.inHumidityValuesHM = humidity;
382401
lwrbLocal.inClearnessIndexValuesHM = clearSky;
402+
// Longwave depends only on temperature/humidity/clearness - not on time of day -
403+
// so one process() call covers the whole timestep, hourly or daily alike.
404+
lwrbLocal.process();
405+
383406
swrbLocal.inHumidityValues = humidity;
384407
swrbLocal.inTemperatureValues = temperature;
385-
swrbLocal.tCurrentDateString = GeoframeEnvDatabaseIterator.ts2str(timestamp);
386408

387-
lwrbLocal.process();
388-
swrbLocal.process();
409+
if (pTimeResolution == TimeResolution.HOURLY) {
410+
swrbLocal.doHourly = true;
411+
swrbLocal.tCurrentDateString = GeoframeEnvDatabaseIterator.ts2str(t);
412+
swrbLocal.process();
389413

390-
nrpcLocal.inShortwaveValues = swrbLocal.outHMtotal;
391-
nrpcLocal.inDownwellingValues = lwrbLocal.outHMlongwaveDownwellingHM;
392-
nrpcLocal.inUpwellingValues = lwrbLocal.outHMlongwaveUpwellingHM;
393-
nrpcLocal.process();
414+
nrpcLocal.inShortwaveValues = swrbLocal.outHMtotal;
415+
nrpcLocal.inDownwellingValues = lwrbLocal.outHMlongwaveDownwellingHM;
416+
nrpcLocal.inUpwellingValues = lwrbLocal.outHMlongwaveUpwellingHM;
417+
nrpcLocal.process();
418+
return new HashMap<>(nrpcLocal.outHMnetRad);
419+
} else {
420+
return averageDailyNetRadiation(lwrbLocal, swrbLocal, nrpcLocal, t);
421+
}
422+
}
394423

395-
return new TimestepResult(timestamp, new HashMap<>(nrpcLocal.outHMnetRad));
424+
/**
425+
* Averages net radiation over a full day into a daily-mean flux.
426+
*
427+
* <p>
428+
* {@link ShortwaveRadiationBalancePointCase} only ever evaluates the sun's
429+
* position (and the DEM-derived shading/skyview weighting that goes with
430+
* it) at one instant, and that DEM-wide shadow recomputation is what
431+
* dominates runtime. With daily-resolution input there is a single
432+
* temperature/humidity/clearness reading for the whole day, so this
433+
* processes that same daily reading at {@link #pDailySubSamples} sun
434+
* positions evenly spaced across the day and averages the results. At the
435+
* default of 24 this means running hourly resolution for the day and
436+
* averaging the 24 outputs; lower values are cheaper but coarser
437+
* approximations, useful for quick test runs.
438+
*/
439+
private HashMap<Integer, double[]> averageDailyNetRadiation(Lwrb lwrbLocal,
440+
ShortwaveRadiationBalancePointCase swrbLocal, NetRadiationPointCase nrpcLocal, long dayTimestamp)
441+
throws Exception {
442+
long dayStartMillis = Math.floorDiv(dayTimestamp, MILLIS_PER_DAY) * MILLIS_PER_DAY;
443+
long stepMillis = MILLIS_PER_DAY / pDailySubSamples;
444+
445+
HashMap<Integer, Double> sumWattsPerM2 = new HashMap<>();
446+
swrbLocal.doHourly = true; // sample the actual sun position each time, rather than one fixed instant
447+
for (int sample = 0; sample < pDailySubSamples; sample++) {
448+
long sampleMillis = dayStartMillis + sample * stepMillis;
449+
swrbLocal.tCurrentDateString = GeoframeEnvDatabaseIterator.ts2str(sampleMillis);
450+
swrbLocal.process();
451+
452+
nrpcLocal.inShortwaveValues = swrbLocal.outHMtotal;
453+
nrpcLocal.inDownwellingValues = lwrbLocal.outHMlongwaveDownwellingHM;
454+
nrpcLocal.inUpwellingValues = lwrbLocal.outHMlongwaveUpwellingHM;
455+
nrpcLocal.process();
456+
457+
for (Map.Entry<Integer, double[]> entry : nrpcLocal.outHMnetRad.entrySet()) {
458+
sumWattsPerM2.merge(entry.getKey(), entry.getValue()[0], Double::sum);
459+
}
460+
}
461+
462+
HashMap<Integer, double[]> dailyMean = new HashMap<>();
463+
for (Map.Entry<Integer, Double> entry : sumWattsPerM2.entrySet()) {
464+
dailyMean.put(entry.getKey(), new double[] { entry.getValue() / pDailySubSamples });
465+
}
466+
return dailyMean;
396467
}
397468

398469
/**

hmachine/src/main/java/org/hortonmachine/hmachine/modules/hydrogeomorphology/etp/OmsPresteyTaylorEtpModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public void process() throws Exception {
167167
}
168168

169169
double etp = getET(pGmorn, pGnight, pAlpha, inputNetRadiation,
170-
doHourly ? defaultHourlyNetradiation : defaultHourlyNetradiation, t, defaultTemp, pressure,
170+
doHourly ? defaultHourlyNetradiation : defaultDailyNetradiation, t, defaultTemp, pressure,
171171
defaultPressure, doHourly, tCurrent);
172172
outPTEtp.put(basinId, new double[] { etp });
173173
}

0 commit comments

Comments
 (0)