2727import org .hortonmachine .hmachine .geoframe .io .database .tables .implementation .BasinPolygonSchema ;
2828import org .hortonmachine .hmachine .geoframe .io .database .tables .implementation .BasinPolygonSchema .BasinMultiPolygonField ;
2929import org .hortonmachine .hmachine .geoframe .io .database .tables .implementation .VarSchema .EnvironmentalVariableType ;
30+ import org .hortonmachine .hmachine .geoframe .io .database .tables .implementation .VarSchema .TimeResolution ;
3031import org .hortonmachine .hmachine .geoframe .utils .radiation .NetRadiationPointCase ;
3132import org .hortonmachine .hmachine .geoframe .utils .radiation .LwrbPointCase .Lwrb ;
3233import 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 /**
0 commit comments