agwx_biophys 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ #include <stdio.h>
2
+ #include <stdlib.h>
3
+ #include <string.h>
4
+ #include <math.h>
5
+ #include <unistd.h>
6
+
7
+ #include <awsdir.h>
8
+ #include <awstime.h>
9
+ #include <awserror.h>
10
+ #include <grid/grid.h>
11
+
12
+ #ifndef O_RDWR
13
+ # define O_RDWR 0000002 /* Open for reading or writing */
14
+ # define O_CREAT 0000400 /* Open with file create (uses third open arg) */
15
+ #endif
16
+
17
+ Grid * CalcCumDD(char * prefix, int StartDoy,
18
+ int EndDoy, int Year, int Method, float BaseTemp,
19
+ int Subset, float xLowSub, float xHighSub, float yLowSub,
20
+ float yHighSub);
21
+
22
+ /*========================================================================
23
+ * MAIN
24
+ */
25
+ main( int argc, char ** argv)
26
+ {
27
+
28
+ char Sub[40];
29
+
30
+ char buf[255];
31
+
32
+ char TMinFileName[255];
33
+ char TMaxFileName[255];
34
+ char GradCtlFile[100];
35
+ char GradDatFile[100];
36
+ char Date[100];
37
+ char prefix[10];
38
+ int fd;
39
+ FILE * fhctl;
40
+
41
+ Grid * pTMinG =NULL;
42
+ Grid * pTMaxG =NULL;
43
+ Grid * pDDG =NULL;
44
+ GridCursor * pTMinGC =NULL;
45
+ GridCursor * pTMaxGC =NULL;
46
+ GLayer * pDDL =NULL;
47
+ GLView * pGLV =NULL;
48
+ GLayer * pGL =NULL;
49
+ float ** CumDD=NULL;
50
+
51
+ int Year;
52
+ int StartDoy;
53
+ int EndDoy;
54
+ int Method;
55
+ float BaseTemp;
56
+
57
+ float TMin;
58
+ float TMax;
59
+ int TMinErr;
60
+ int TMaxErr;
61
+ float DD;
62
+
63
+ int doy;
64
+ int earlydoy;
65
+
66
+ int srcdoy;
67
+ int TMinDoy,TMaxDoy;
68
+ float xPos,yPos;
69
+ int xIdx,yIdx;
70
+ int FoundMin;
71
+ int FoundMax;
72
+
73
+ int xNo,yNo;
74
+ float xLow, xHigh, yLow, yHigh;
75
+ float lon,lat;
76
+
77
+ float xLowSub, xHighSub, yLowSub, yHighSub;
78
+ float xIncr,yIncr;
79
+ int xStrt,xEnd,yStrt,yEnd;
80
+ int lStrt,lEnd;
81
+ float BadValue;
82
+ int yr, mnth ,day;
83
+
84
+ int Subset = FALSE;
85
+
86
+ int x,y,l;
87
+
88
+
89
+
90
+ strcpy(Sub,"GetStnDD:Main");
91
+
92
+ StartConPrint();
93
+
94
+ /*-------------------------------
95
+ * Check Parameters
96
+ */
97
+ if ( !(argc==9) ) {
98
+ goto USAGE;
99
+ }
100
+ Year = atoi(argv[1]);
101
+ StartDoy = atoi(argv[2]);
102
+ EndDoy = atoi(argv[3]);
103
+ if ( !(strlen(argv[4])==4 &&
104
+ ( argv[4][0]=='R' || argv[4][0]=='S' || argv[4][0]=='M'
105
+ || argv[4][0]=='P' ) ) ) {
106
+ printf("Method Must be 'Rect' or 'Sine' or 'ModB' or 'PDay' \n");
107
+ goto USAGE;
108
+ }
109
+ switch (argv[4][0]) {
110
+ case 'R' :
111
+ Method = GRD_DD_RECTANGULAR;
112
+ break;
113
+ case 'S' :
114
+ Method = GRD_DD_SINE_WAVE;
115
+ break;
116
+ case 'M' :
117
+ Method = GRD_DD_MODIFIED_BASE;
118
+ break;
119
+ case 'P' :
120
+ Method = GRD_DD_PDAY;
121
+ break;
122
+ }
123
+ BaseTemp = atof(argv[5]);
124
+ sprintf(prefix,argv[6]);
125
+ lat = atof(argv[7]);
126
+ lon = atof(argv[8]);
127
+
128
+ Subset=TRUE;
129
+ xLowSub = lon-0.5;
130
+ xHighSub = lon+0.5;
131
+ yLowSub = lat-0.5;
132
+ yHighSub = lat+0.5;
133
+
134
+ if (StartDoy <1 || StartDoy > 366) {
135
+ RegErr(Sub,"","StartDoy Should be 1-366");
136
+ goto USAGE;
137
+ }
138
+ if (EndDoy <1 || EndDoy > 366) {
139
+ RegErr(Sub,"","EndDoy Should be 1-366");
140
+ goto USAGE;
141
+ }
142
+ if (BaseTemp < 30 || BaseTemp > 90 ) {
143
+ RegErr(Sub,"",
144
+ "Base Temp Should be in Deg F, <30 or >90 seems unreasonable");
145
+ goto USAGE;
146
+ }
147
+
148
+ /*----------------------------------
149
+ * Compute the Cumulative DD
150
+ */
151
+ if ( (pDDG=CalcCumDD(prefix,StartDoy,EndDoy,Year,Method,
152
+ BaseTemp,Subset,xLowSub,xHighSub,yLowSub,
153
+ yHighSub)) == NULL ) {
154
+ RegErr(Sub,"","Error computing cumulative DD");
155
+ goto ERROR;
156
+ }
157
+ if ( (pDDL=GetGLayer(pDDG,1)) == NULL ) {
158
+ RegErr(Sub,"","Error retrieving layer from cumulative DD grid");
159
+ goto ERROR;
160
+ }
161
+
162
+
163
+ /*---------------------------------------
164
+ * Get the DD at lat,lon and print
165
+ */
166
+ GetGLVal(pDDG,pDDL,lon,lat,&DD);
167
+ printf("%.0f",DD);
168
+
169
+ EndConPrint();
170
+ return AWS_OK;
171
+
172
+ ERROR:
173
+
174
+ EndConPrint();
175
+ return AWS_FAIL;
176
+
177
+ USAGE:
178
+ printf(
179
+ "Usage: %s Year StartDoy EndDoy {Rect|ModB|Sine|Pday} BaseTemp TempPrefix Lat Long\n"
180
+ , argv[0]);
181
+ printf(
182
+ "Example: %s 96 66 73 ModB 50 Wi 43 -90\n"
183
+ ,argv[0]);
184
+ EndConPrint();
185
+ return AWS_FAIL;
186
+ }
@@ -1,6 +1,6 @@
1
1
  require "agwx_biophys/version"
2
- require "agwx_biophys/et"
3
-
2
+ require 'agwx_biophys/et'
3
+ require 'agwx_biophys/degree_days'
4
4
 
5
5
  module AgwxBiophys
6
6
  end
@@ -0,0 +1,347 @@
1
+ module AgwxBiophys
2
+ module DegreeDays
3
+
4
+ # return an array so that we can access arr[0..yDim][0..xDim]
5
+ def cumulate_array(value=0.0)
6
+ arr = Array.new(Y_DIM)
7
+ for y in 0..MAX_Y_INDEX
8
+ arr[y] = Array.new(X_DIM,value)
9
+ end
10
+ arr
11
+ end
12
+
13
+ # Degree-day calcs taken from prg/src/tisdat/calccumdd.c
14
+
15
+ # Calculate rect DD from min and max for this day. Everything is in Fahrenheit.
16
+ def rect_DD(min,max,base=50)
17
+ rect = ((max + min) / 2.0) - base
18
+ # Never return a negative number
19
+ # puts "rect_dd for #{min},#{max},#{base},#{upper} returning #{rect}"
20
+ rect >= 0.0 ? rect : 0.0
21
+ end
22
+
23
+ def rect_DD_from_avg(avg,base=50)
24
+ rect = avg - base
25
+ rect >= 0.0 ? rect : 0.0
26
+ end
27
+
28
+ def modB_DD(min,max,base=50,upper=86)
29
+ min = [base,min].max
30
+ max = [base,max].max
31
+
32
+ min = [min,upper].min
33
+ max = [max,upper].min
34
+ rect_DD(min,max,base)
35
+ end
36
+
37
+ DD_MAX_TEMP = 86
38
+ M_1_PI = 0.31830988618379067154
39
+
40
+ def sine_DD(min,max,base=50,upper=DD_MAX_TEMP)
41
+ alpha = nil
42
+ o1 = o2 = nil
43
+ dd = nil
44
+ avg = (min + max) / 2.0
45
+ #
46
+ # Source Degree-Days: The Calculation and Use
47
+ # of Heat Units in Pest management
48
+ # Original returns an error if min > max
49
+ # if (TMin > TMax) {
50
+ # RegErr("CalcDayDD:SineDD:","","Tmin > Tmax");
51
+ # return -9999999;
52
+ return upper - base if (min >= upper)
53
+ return 0 if (max <= base)
54
+ return avg - base if (max <= upper && min >= base)
55
+
56
+ alpha = (max-min)/2;
57
+ if (max <= upper && min < base)
58
+ o1 = Math.asin( (base-avg)/alpha)
59
+ return M_1_PI*( (avg-base) * (Math::PI / 2-o1) + alpha*Math.cos(o1) )
60
+ end
61
+
62
+ if (max > upper && min >= base)
63
+ o2 = Math.asin( (upper-avg)/alpha);
64
+ return M_1_PI*( (avg-base) * (o2+M_1_PI) +
65
+ (upper-base) * (Math::PI / 2-o2) - alpha*Math.cos(o2) )
66
+ end
67
+
68
+ if (max > upper && min < base)
69
+ o1 = Math.asin( (base-avg)/alpha);
70
+ o2 = Math.asin( (upper-avg)/alpha);
71
+ return M_1_PI*( (avg-base)*(o2-o1) + alpha*(Math.cos(o1)-Math.cos(o2))
72
+ + (upper-base)*(Math::PI / 2-o2) )
73
+ end
74
+ end
75
+
76
+
77
+ def cumulate(min_temp_hash,max_temp_hash,start_doy,end_doy,base,upper=86)
78
+ prev = 0
79
+ days_found = 0
80
+ cumulation = (start_doy..end_doy).inject(0) do |sum,doy|
81
+ # Need a better interpolation algorithm here!
82
+ if min_temp_hash[doy] && max_temp_hash[doy]
83
+ days_found += 1
84
+ prev = yield(min_temp_hash[doy],max_temp_hash[doy],base,upper)
85
+ else
86
+ raise "doy #{doy} not found!"
87
+ end
88
+ sum + prev
89
+ end
90
+ [cumulation,days_found]
91
+ end
92
+
93
+
94
+ def to_fahrenheit(celsius)
95
+ (celsius * (9.0 / 5.0)) + 32.0
96
+ end
97
+
98
+ def frost?(grid,longi_index,lati_index,doy,frost_value)
99
+ val = grid.get_by_index(longi_index,lati_index,doy)
100
+ return false unless val
101
+ return false unless val != grid.mD.badVal
102
+ return (val <= frost_value)
103
+ end
104
+
105
+ # Find the date of first frost -- defined as the first day after July 1 with a min temp below 0 C --
106
+ # for each grid point for each year. Return a hash keyed by year, of X by Y arrays of DOYs.
107
+ def frost_doys(min_grids,start_year=START_YEAR,end_year=END_YEAR,frost_value=0.0)
108
+ frost_doys = {}
109
+ for year in (start_year..end_year)
110
+ frost_doys[year] = cumulate_array(NO_DOY)
111
+ # We start with 0 grid cells at frost (it's July, after all). When the number reaches
112
+ # NUM_GRID_CELLS, we know we've seen frost everywhere on the grid and can safely
113
+ # terminate the loop.
114
+ num_frosty_grid_cells = 0
115
+ # Can't use START_DOY or END_DOY here since frost might extend outside the growing season
116
+ (START_OF_JULY..366).each do |doy|
117
+ if min_grids[year].get_by_index(0,0,doy)
118
+ for lati_index in (0..MAX_Y_INDEX)
119
+ for longi_index in (0..MAX_X_INDEX)
120
+ if frost_doys[year][lati_index][longi_index] == NO_DOY && frost?(min_grids[year],longi_index,lati_index,doy,frost_value)
121
+ frost_doys[year][lati_index][longi_index] = doy
122
+ num_frosty_grid_cells += 1
123
+ break if num_frosty_grid_cells >= NUM_GRID_CELLS
124
+ end
125
+ break if num_frosty_grid_cells >= NUM_GRID_CELLS
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ frost_doys
132
+ end
133
+
134
+ def cell_series(grid_hash,lati_index,longi_index)
135
+ series = []
136
+ grid_hash.keys.sort.each do |key|
137
+ series << grid_hash[key][lati_index][longi_index]
138
+ end
139
+ series
140
+ end
141
+
142
+ def median(array)
143
+ return nil unless array && array.size > 0 && (array = array.compact).size > 0
144
+ sorted = array.sort
145
+ len = sorted.length
146
+ return (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
147
+ end
148
+
149
+ def build_averages(min_grids,max_grids,start_year=START_YEAR,end_year=END_YEAR)
150
+ averages = {}
151
+ layers_for_doy = {}
152
+ # print "building averages. "
153
+ # check how many years have data for each doy
154
+ (START_DOY..END_DOY).each do |doy|
155
+ for year in (start_year..end_year)
156
+ # Does this year have this DOY?
157
+ if min_grids[year].get_by_index(0,0,doy)
158
+ if layers_for_doy[doy]
159
+ layers_for_doy[doy] += 1
160
+ else
161
+ layers_for_doy[doy] = 1
162
+ end
163
+ end
164
+ end
165
+ end
166
+ (START_DOY..END_DOY).each do |doy|
167
+ # print "doy #{doy}.."; $stdout.flush
168
+ averages[doy] = cumulate_array
169
+
170
+ for lati_index in (0..MAX_Y_INDEX)
171
+ for longi_index in (0..MAX_X_INDEX)
172
+ for year in (start_year..end_year)
173
+ # Does this year have this DOY?
174
+ if min = min_grids[year].get_by_index(longi_index,lati_index,doy)
175
+ max = max_grids[year].get_by_index(longi_index,lati_index,doy)
176
+ begin
177
+ averages[doy][lati_index][longi_index] += (max + min) / 2.0
178
+ if lati_index == 2 && longi_index == 22
179
+ # puts "year #{year} doy #{doy}, min #{min} max #{max} avg #{averages[doy][lati_index][longi_index]}"
180
+ end
181
+ rescue Exception => e
182
+ puts "BORKED!\nlati #{lati_index}, longi #{longi_index}, avgs[doy] #{averages[doy].inspect}"
183
+ raise e
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ # print "\ndoing the averaging. "
191
+ # Now we have the sum of all the existing daily average temps. Divide by the number
192
+ # of layers that had that DOY.
193
+ (START_DOY..END_DOY).each do |doy|
194
+ "doy #{doy}.."; $stdout.flush
195
+ for lati_index in (0..MAX_Y_INDEX)
196
+ for longi_index in (0..MAX_X_INDEX)
197
+ begin
198
+ raise 'zero in lfd' if layers_for_doy[doy] == 0
199
+ averages[doy][lati_index][longi_index] /= layers_for_doy[doy]
200
+ rescue Exception => e
201
+ puts "BORKED on division! doy #{doy}, lati_index #{lati_index}, longi_index #{longi_index}, lfd[doy] #{layers_for_doy[doy]}"
202
+ puts averages[doy][lati_index].inspect
203
+ raise e
204
+ end
205
+ end
206
+ end
207
+ end
208
+ # puts ""
209
+ averages
210
+ end
211
+
212
+ # def cumulate(min_grids,max_grids,averages,start_year=START_YEAR,end_year=END_YEAR,base=10.0,upper=30.0,fahrenheit=false)
213
+ # # construct a set of DD arrays, one per year
214
+ # yearly_DD_accums = {}
215
+ # # print "cumulating. "
216
+ # for year in start_year..end_year
217
+ # yearly_DD_accums[year] = cumulate_array
218
+ # # print "#{year}.."; $stdout.flush
219
+ # for doy in (START_DOY..END_DOY)
220
+ # for lati_index in (0..MAX_Y_INDEX)
221
+ # for longi_index in (0..MAX_X_INDEX)
222
+ # begin
223
+ # min = min_grids[year].get_by_index(longi_index,lati_index,doy) || averages[doy][lati_index][longi_index]
224
+ # max = max_grids[year].get_by_index(longi_index,lati_index,doy) || averages[doy][lati_index][longi_index]
225
+ # if fahrenheit
226
+ # min = to_fahrenheit(min)
227
+ # max = to_fahrenheit(max)
228
+ # base = to_fahrenheit(base) if base == 10.0
229
+ # upper = to_fahrenheit(upper) if upper == 30.0
230
+ # end
231
+ # if block_given?
232
+ # dd = yield(min,max,base,upper)
233
+ # else
234
+ # dd = rect_DD(min,max,base,upper)
235
+ # end
236
+ # yearly_DD_accums[year][lati_index][longi_index] += dd
237
+ # rescue Exception => e
238
+ # puts "cumulate: problem at #{year}, #{doy}, #{lati_index}, #{longi_index}"
239
+ # raise e
240
+ # end
241
+ # end
242
+ # end
243
+ # end
244
+ # end
245
+ # # puts ""
246
+ # yearly_DD_accums
247
+ # end
248
+
249
+ def year_dd_grid(max_grids,min_grids,year)
250
+ return {1 => 4, 6 => 5}
251
+ end
252
+
253
+ # For a given year, return a hash of DOYs at a grid point with the daily DDs for each, using the passed-in block to calculate a day's DD
254
+ def dds_for_year_at_point(p)
255
+ calculate_fahrenheit = p[:calculate_fahrenheit] || true
256
+ base = p[:base] || 50.0
257
+ upper = p[:upper] || 86.0
258
+ start_doy = p[:start_doy] || START_DOY # May 1
259
+ [:max_grids,:min_grids,:year,:lati_index,:longi_index].each { |sym| raise "Required param #{sym.to_s} missing" unless p[sym] }
260
+ doy_dds = {} # Will be a Hash keyed by DOY
261
+
262
+ (start_doy..366).each do |doy|
263
+ min = p[:min_grids][p[:year]].get_by_index(p[:longi_index],p[:lati_index],doy)
264
+ max = p[:max_grids][p[:year]].get_by_index(p[:longi_index],p[:lati_index],doy)
265
+ next unless min && max
266
+ if block_given?
267
+ doy_dds[doy] = yield(to_fahrenheit(min),to_fahrenheit(max),base,upper)
268
+ else
269
+ doy_dds[doy] = modB_DD(to_fahrenheit(min),to_fahrenheit(max),base,upper)
270
+ end
271
+ end
272
+ doy_dds
273
+ end
274
+
275
+ def frost_doy(grid,longi_index,lati_index,frost_value=0.0)
276
+ (START_OF_JULY..366).each do |doy|
277
+ min = grid.get_by_index(longi_index,lati_index,doy)
278
+ if min && min <= frost_value
279
+ return doy
280
+ end
281
+ end
282
+ nil
283
+ end
284
+
285
+ def remaining_dds_for_year_at_point(dds_at_point,frost_doy,start_doy=120)
286
+ remaining_dds = {}
287
+ sum = 0
288
+ frost_doy.to_i.downto(start_doy) do |doy|
289
+ next unless dds_at_point[doy]
290
+ sum += dds_at_point[doy]
291
+ remaining_dds[doy] = sum
292
+ end
293
+ remaining_dds
294
+ end
295
+
296
+ # Return a hash of remaining-DD values for a grid point up to the first frost for that year.
297
+ # Most of the params get passed unchanged to dds_for_year_at_point, which handles defaults and such.
298
+ # FIXME: Extend interface to include a passed-in block for DD calcs. For now,
299
+ # just uses dds_for_year_at_point's, which is ModB / 50.0 / 86.0
300
+ def pre_frost_remaining_dds_at_point(p,proc=nil)
301
+
302
+ frost_value = p[:frost_value] || 0.0
303
+ [:max_grids,:min_grids,:year,:lati_index,:longi_index].each { |sym| raise "Required param #{sym.to_s} missing" unless p[sym] }
304
+ fd = frost_doy(p[:min_grids][p[:year]],p[:longi_index],p[:lati_index],frost_value)
305
+ return nil unless fd
306
+ if block_given?
307
+ dds = dds_for_year_at_point(p,&proc)
308
+ else
309
+ dds = dds_for_year_at_point(p)
310
+ end
311
+ [remaining_dds_for_year_at_point(dds,fd),fd]
312
+ end
313
+
314
+ # Return a hash, by doy, of the median remaining-DD value for that doy over all years for a point
315
+ def median_pre_frost_remaining_dds(p)
316
+ # hash by year of per-day remaining DDs
317
+ remaining = {}
318
+ [:min_grids,:max_grids,:years,:longi_index,:lati_index].each { |sym| raise "Required param #{sym.to_s} missing" unless p[sym] }
319
+ # Assemble a dataset of remaining DDs for this point; it'll be a hash (by year) of hashes (by doy) of remaining DD
320
+ p[:years].each do |year|
321
+ p[:year] = year # Hack the param hash so it's good to go for pre_frost_remaining_dds_at_point
322
+ remaining[year],frost_doy = pre_frost_remaining_dds_at_point(p)
323
+ end
324
+ # Iterate over each possible DOY, find the median for that DOY. Will pointlessly access many nonexistent DOYs after
325
+ # first frost, but that means we don't terminate prematurely if a DOY happens to be missing
326
+ medians = {}
327
+ (START_DOY..366).each do |doy|
328
+ dds_for_doy = (p[:years].collect {|year| remaining[year][doy]}).compact
329
+ next unless dds_for_doy.size > 0
330
+ medians[doy] = median(dds_for_doy)
331
+ end
332
+ medians
333
+ end
334
+
335
+ def print_samples(yearly_DD_accums,start_year=START_YEAR,end_year=END_YEAR)
336
+ for year in start_year..end_year
337
+ print "\n#{year}:"
338
+ (0..30).step(5) do |longi_index|
339
+ (0..20).step(5) do |lati_index|
340
+ print "#{yearly_DD_accums[year][longi_index][lati_index]}, "
341
+ end
342
+ end
343
+ puts ""
344
+ end
345
+ end
346
+ end
347
+ end