ruby-grads 1.0.0

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,709 @@
1
+ #
2
+ #
3
+ #
4
+ # A library to read GrADS gridded data into CArray.
5
+ #
6
+ # TODO
7
+ # * options
8
+ #
9
+ # NOTE
10
+ # * 'dset' should be a file
11
+ #
12
+ # --- supported -------
13
+ # dset
14
+ # dtype
15
+ # title
16
+ # undef
17
+ # options
18
+ # pdef
19
+ # xdef
20
+ # ydef
21
+ # zdef
22
+ # tdef
23
+ # vectorpairs
24
+ # * comment
25
+ #
26
+ # --- not supported ---
27
+ # chsub
28
+ # fileheader
29
+ # theader
30
+ # tailerbytes
31
+ # xyheader
32
+ #
33
+
34
+ require "carray"
35
+ require "strscan"
36
+
37
+ module GrADS
38
+ end
39
+
40
+ class GrADS::Gridded
41
+ end
42
+
43
+ class GrADS::Gridded::Variable
44
+
45
+ def initialize (name, ctl)
46
+ @name = name
47
+ @dim = ctl.vardims[name]
48
+ @tsize = ctl.tsize
49
+ @dset = ctl.dset
50
+ @chunksize = ctl.chunksize
51
+ @offset = ctl.varoffsets[name]
52
+ @undef = ctl.undef
53
+ end
54
+
55
+ attr_reader :name, :dim, :size, :dset, :offset
56
+
57
+ def each_trange (trng=nil)
58
+ case trng
59
+ when NilClass, FalseClass
60
+ (0...@tsize).each do |t|
61
+ yield(t)
62
+ end
63
+ when Range
64
+ trng.each do |t|
65
+ yield(t)
66
+ end
67
+ else
68
+ t = trng
69
+ yield(t)
70
+ end
71
+ end
72
+
73
+ def tsize (trng=nil)
74
+ case trng
75
+ when NilClass, FalseClass
76
+ return @tsize
77
+ when Range
78
+ return trng.to_a.size
79
+ else
80
+ return 1
81
+ end
82
+ end
83
+
84
+ def [] (*argv)
85
+ trng = argv.shift
86
+ buf = CArray.float(*@dim)
87
+ out = nil
88
+ open(@dset) { |io|
89
+ if tsize(trng) == 1
90
+ each_trange(trng) { |t|
91
+ io.seek(t * @chunksize + offset)
92
+ buf.load_binary(io)
93
+ out = buf[*argv]
94
+ }
95
+ else
96
+ outlist = []
97
+ i = 0
98
+ bind_ok = false
99
+ each_trange(trng) { |t|
100
+ t * @chunksize + offset
101
+ io.seek(t * @chunksize + offset)
102
+ buf.load_binary(io)
103
+ obj = buf[*argv].to_ca
104
+ if obj.is_a?(CArray)
105
+ outlist << obj
106
+ else
107
+ outlist << CA_FLOAT(obj)
108
+ bind_ok = true
109
+ end
110
+ i += 1
111
+ }
112
+ if bind_ok
113
+ out = CArray.bind(:float, outlist, 0)
114
+ else
115
+ out = CArray.merge(:float, outlist, 0)
116
+ end
117
+ end
118
+ }
119
+ out[:eq, @undef] = UNDEF
120
+ return out
121
+ end
122
+
123
+ def to_ca
124
+ return self[]
125
+ end
126
+
127
+ end
128
+
129
+ class GrADS::Gridded
130
+
131
+ def initialize (ctl_file)
132
+ @ctl_file = ctl_file
133
+ @entries = scan_ctl_file(ctl_file)
134
+ parse_entries(@entries)
135
+ end
136
+
137
+ attr_reader :ctl_file, :entries
138
+ attr_reader :dset, :title, :undef
139
+ attr_reader :pdef, :proj
140
+ attr_reader :isize, :jsize
141
+ attr_reader :xsize, :ysize, :zsize, :tsize
142
+ attr_reader :xaxis, :yaxis, :zaxis, :taxis
143
+ attr_reader :vectorpair
144
+ attr_reader :varnames, :vars, :vardims, :varoffsets
145
+ attr_reader :chunksize
146
+
147
+ private
148
+
149
+ def scan_ctl_file (ctl_file)
150
+ io = open(ctl_file, "r:ASCII-8BIT")
151
+ edefs = ""
152
+ vdefs = ""
153
+ headers = ""
154
+ while line = io.gets
155
+ case line
156
+ when /\A\s*vars/i
157
+ vdefs << line
158
+ while line = io.gets
159
+ vdefs << line
160
+ if line =~ /\A\s*endvars/i
161
+ break
162
+ end
163
+ end
164
+ when /\A\s*edef/i
165
+ raise "edef entry is not supported, sorry!"
166
+ edefs << line
167
+ while line = io.gets
168
+ ddefs << line
169
+ if line =~ /\A\s*endedef/i
170
+ break
171
+ end
172
+ end
173
+ else
174
+ headers << line
175
+ end
176
+ end
177
+ entries = scan_headers(headers)
178
+ entries["vars"] = scan_vars(vdefs)
179
+ return entries
180
+ end
181
+
182
+ def scan_headers (headers)
183
+ items = {}
184
+ buffer = ""
185
+ headers.each_line do |line|
186
+ case line
187
+ when /\A\s*\*/, /\A\s*\Z/
188
+ next
189
+ when /A\s*(dtype)\s*(.*)\Z/i
190
+ raise "data file is not GrADS gridded binary"
191
+ when /\A\s*(dset|title|undef|options|pdef|xdef|ydef|zdef|tdef|vectorpairs)\s*(.*)\Z/i
192
+ name = $1.downcase
193
+ if items[name]
194
+ items[name] = [items[name]]
195
+ items[name] << $2.rstrip
196
+ buffer = items[name].last
197
+ else
198
+ items[name] = $2.rstrip
199
+ buffer = items[name]
200
+ end
201
+ when /\A\s*(chsub|fileheader|theader|tailerbyts|xyheader)\s*(.*)\Z/i
202
+ raise "#{$1} entry is not supported, sorry"
203
+ else
204
+ line = line.chomp.sub(/\\\z/,'').rstrip
205
+ buffer.replace(buffer + " " + line)
206
+ end
207
+ end
208
+ return items
209
+ end
210
+
211
+ def scan_vars (vars)
212
+ items = {:namelist=>[]}
213
+ nvars = nil
214
+ vars.each_line do |line|
215
+ case line
216
+ when /\A\s*\*/, /\A\s*\Z/
217
+ next
218
+ when /\A\s*vars\s+(\d+)/i
219
+ nvars = $1.to_i
220
+ when /\A\s*endvars/i
221
+ break
222
+ when /\A\s*(\w+)\s*(.*)\Z/
223
+ name = $1
224
+ list = $2
225
+ items[:namelist].push(name)
226
+ items[name] = list
227
+ end
228
+ end
229
+ if items[:namelist].size != nvars
230
+ raise "invalid vars number"
231
+ end
232
+ return items
233
+ end
234
+
235
+ def parse_entries (entries)
236
+ parse_dset(entries["dset"])
237
+ if entries.has_key?("title")
238
+ parse_title(entries["title"])
239
+ end
240
+ parse_undef(entries["undef"])
241
+ parse_options(*entries["options"])
242
+ if entries.has_key?("pdef")
243
+ parse_pdef(entries["pdef"])
244
+ parse_xdef(entries["xdef"], with_pdef = true)
245
+ parse_ydef(entries["ydef"], with_pdef = true)
246
+ else
247
+ parse_xdef(entries["xdef"], with_pdef = false)
248
+ parse_ydef(entries["ydef"], with_pdef = false)
249
+ end
250
+ parse_zdef(entries["zdef"])
251
+ parse_tdef(entries["tdef"])
252
+ if entries.has_key?("vectorpairs")
253
+ parse_vectorpairs(entries["vectorpairs"])
254
+ end
255
+ parse_vars(entries["vars"][:namelist], entries["vars"])
256
+ end
257
+
258
+ def parse_dset (line)
259
+ line.strip!
260
+ if line =~ /\A\^/
261
+ cwd = File.dirname(File.expand_path(@ctl_file))
262
+ @dset = File.join(cwd, line[1..-1])
263
+ else
264
+ @dset = line
265
+ end
266
+ end
267
+
268
+ def parse_title (line)
269
+ @title = line
270
+ end
271
+
272
+ def parse_undef (line)
273
+ @undef = Float(line)
274
+ end
275
+
276
+ def parse_options (*lines)
277
+ # yrev o
278
+ # zrev o
279
+ # template x
280
+ # squential o
281
+ # 365_day_calendar ?
282
+ # byteswapped o
283
+ # big_endian o
284
+ # little_endian o
285
+ # cray_32bit_ieee ?
286
+ end
287
+
288
+ def parse_pdef (line)
289
+ list = line.split(/\s+/)
290
+ @pdef = list
291
+ @isize = list[0].to_i
292
+ @jsize = list[1].to_i
293
+ case list[2].downcase
294
+ when "nps", "sps"
295
+ parse_pdef_xps(list[2..-1])
296
+ when "lcc", "lccr"
297
+ parse_pdef_lcc(list[2..-1])
298
+ when "eta.u"
299
+ parse_pdef_eta_u(list[2..-1])
300
+ when "pse"
301
+ parse_pdef_pse(list[2..-1])
302
+ when "ops"
303
+ parse_pdef_ops(list[2..-1])
304
+ when "rotll", "rotllr"
305
+ parse_pdef_rotll(list[2..-1])
306
+ when "bilin"
307
+ parse_pdef_bilin(list[2..-1])
308
+ when "general"
309
+ parse_pdef_general(list[2..-1])
310
+ when "file"
311
+ parse_pdef_file(list[2..-1])
312
+ else
313
+ raise "pdef #{list[2].downcase} is not supported"
314
+ end
315
+ end
316
+
317
+ def parse_pdef_xps (args)
318
+ raise NotImplementedError, "not implemented"
319
+ end
320
+
321
+ def parse_pdef_lcc (args)
322
+ require "proj4r"
323
+ rlat = args[1].to_f
324
+ rlon = args[2].to_f
325
+ xi = args[3].to_f
326
+ yi = args[4].to_f
327
+ slat1 = args[5].to_f
328
+ slat2 = args[6].to_f
329
+ slon = args[7].to_f
330
+ dx = args[8].to_f
331
+ dy = args[9].to_f
332
+ slat0 = slat1 > 0 ? 90 : -90
333
+ proj = PROJ4::Proj.new %{
334
+ +ellps=sphere +proj=lcc \
335
+ +lat_1=#{slat1} +lat_2=#{slat2} +lat_0=90 +lon_0=#{slon}
336
+ }
337
+ rx, ry = proj.forward(rlon, rlat)
338
+ rx0 = rx - (xi-1)*dx
339
+ ry0 = ry - (yi-1)*dy
340
+ @forward = lambda { |lon, lat|
341
+ mx, my = proj.forward(lon, lat)
342
+ [(mx - rx0)/dx, (my - ry0)/dy]
343
+ }
344
+ @inverse = lambda { |fi, fj|
345
+ mx = fi*dx + rx0
346
+ my = fj*dy + ry0
347
+ proj.inverse(mx, my)
348
+ }
349
+ end
350
+
351
+ def parse_pdef_eta_u (args)
352
+ raise NotImplementedError, "not implemented"
353
+ end
354
+
355
+ def parse_pdef_pse (args)
356
+ raise NotImplementedError, "not implemented"
357
+ end
358
+
359
+ def parse_pdef_ops (args)
360
+ raise NotImplementedError, "not implemented"
361
+ end
362
+
363
+ def parse_pdef_rotll (args)
364
+ raise NotImplementedError, "not implemented"
365
+ end
366
+
367
+ def parse_pdef_general (args)
368
+ raise NotImplementedError, "not implemented"
369
+ end
370
+
371
+ def parse_pdef_file (args)
372
+ raise NotImplementedError, "not implemented"
373
+ end
374
+
375
+ def parse_xdef (line, with_pdef=false)
376
+ list = line.split(/\s+/)
377
+ if with_pdef
378
+ @xsize = list[0].to_i
379
+ else
380
+ @isize = list[0].to_i
381
+ @xsize = @isize
382
+ end
383
+ case list[1]
384
+ when /linear/i
385
+ @xaxis = []
386
+ @xsize.times do |i|
387
+ @xaxis << list[2].to_f + i*list[3].to_f
388
+ end
389
+ when /levels/i
390
+ @xaxis = list[2..-1].map{|x| x.to_f}
391
+ end
392
+ end
393
+
394
+ def parse_ydef (line, with_pdef=false)
395
+ list = line.split(/\s+/)
396
+ if with_pdef
397
+ @ysize = list[0].to_i
398
+ else
399
+ @jsize = list[0].to_i
400
+ @ysize = @jsize
401
+ end
402
+ case list[1]
403
+ when /linear/i
404
+ @yaxis = []
405
+ @ysize.times do |i|
406
+ @yaxis << list[2].to_f + i*list[3].to_f
407
+ end
408
+ when /levels/i
409
+ @yaxis = list[2..-1].map{|y| y.to_f}
410
+ end
411
+ end
412
+
413
+ def parse_zdef (line)
414
+ list = line.split(/\s+/)
415
+ @zsize = list[0].to_i
416
+ case list[1]
417
+ when /linear/i
418
+ @zaxis = []
419
+ @zsize.times do |i|
420
+ @zaxis << list[2].to_f + i*list[3].to_f
421
+ end
422
+ when /levels/i
423
+ @zaxis = list[2..-1].map{|z| z.to_f}
424
+ end
425
+ end
426
+
427
+ def parse_tdef (line)
428
+ list = line.split(/\s+/)
429
+ @tsize = list[0].to_i
430
+ @inittime = DateTime.parse(list[2].sub(/z/i,' '))
431
+ @taxis = [@inittime]
432
+ case list[3]
433
+ when /\A(\d+)(mn|hr|dy|mo|yr)/
434
+ case $2
435
+ when "mn"
436
+ (1..@tsize-1).each do |i|
437
+ @taxis[i] = @taxis[i-1] + ($1.to_i).quo(1440)
438
+ end
439
+ when "hr"
440
+ (1..@tsize-1).each do |i|
441
+ @taxis[i] = @taxis[i-1] + ($1.to_i).quo(24)
442
+ end
443
+ when "dy"
444
+ (1..@tsize-1).each do |i|
445
+ @taxis[i] = @taxis[i-1] + $1.to_i
446
+ end
447
+ when "mn"
448
+ (1..@tsize-1).each do |i|
449
+ @taxis[i] = @taxis[i-1] >> $1.to_i
450
+ end
451
+ when "yr"
452
+ (1..@tsize-1).each do |i|
453
+ @taxis[i] = @taxis[i-1] >> (12*$1.to_i)
454
+ end
455
+ end
456
+ else
457
+ raise "invalid time increment"
458
+ end
459
+ end
460
+
461
+ def parse_vectorpairs (line)
462
+ list = line.split(/\s+/)
463
+ @vectorpair = list.map{|pair| pair.downcase.split(/\s*,\s*/)}
464
+ end
465
+
466
+ def parse_vars (namelist, hash)
467
+ @varnames = namelist
468
+ @vars = {}
469
+ @vardims = {}
470
+ @varoffsets = {}
471
+ @chunksize = 0
472
+ namelist.each do |name|
473
+ line = hash[name]
474
+ ss = StringScanner.new(line)
475
+ znum = ss.scan(/\A.*?\s+/).rstrip.to_i
476
+ units = ss.scan(/\A.*?\s+/).rstrip
477
+ desc = ss.rest
478
+ @vars[name] = [znum, units, desc]
479
+ if znum == 0
480
+ @vardims[name] = [@jsize,@isize]
481
+ else
482
+ @vardims[name] = [znum, @jsize,@isize]
483
+ end
484
+ @varoffsets[name] = @chunksize
485
+ @chunksize += 4 * @vardims[name].inject(1) {|s,d| s * d}
486
+ end
487
+ end
488
+
489
+ public
490
+
491
+ def inverse (fi, fj)
492
+ return @inverse[fi,fj]
493
+ end
494
+
495
+ def forward (rlon, rlat)
496
+ return @forward[rlon, rlat]
497
+ end
498
+
499
+ def var (name)
500
+ unless @varnames.include?(name)
501
+ raise "invalid variable name '#{name}'"
502
+ end
503
+ return Variable.new(name, self)
504
+ end
505
+
506
+ def template (ctl_file, &block)
507
+ asm = Writer.new(self)
508
+ asm.define(&block)
509
+ asm.write(ctl_file)
510
+ end
511
+
512
+ end
513
+
514
+ class GrADS::Gridded::Writer
515
+
516
+ def initialize (ctl = nil)
517
+ @dset = nil
518
+ @title = nil
519
+ @options = []
520
+ @vectorpairs = nil
521
+ @vars = []
522
+ @nt = 1
523
+ if ctl
524
+ e = ctl.entries
525
+ @undef = e["undef"]
526
+ @pdef = e["pdef"]
527
+ @xdef = e["xdef"]
528
+ @ydef = e["ydef"]
529
+ @zdef = e["zdef"]
530
+ @tdef = e["tdef"]
531
+ else
532
+ @undef = nil
533
+ @pdef = nil
534
+ @xdef = nil
535
+ @ydef = nil
536
+ @zdef = nil
537
+ @tdef = "1 linear 0z1jan2000"
538
+ end
539
+ end
540
+
541
+ private
542
+
543
+ def dset (arg)
544
+ @dset = arg
545
+ end
546
+
547
+ def title (arg)
548
+ @title = arg
549
+ end
550
+
551
+ def undef! (arg)
552
+ @undef = arg
553
+ end
554
+
555
+ def options (*args)
556
+ @options.push(*args)
557
+ end
558
+
559
+ def pdef (*args)
560
+ @pdef = args.join(" ")
561
+ end
562
+
563
+ def xdef (*args)
564
+ if args.size == 1 and
565
+ ( args.first.is_a?(CArray) or args.first.is_a?(Array) )
566
+ ary = args.first
567
+ @xdef = [ary.length, "levels", ary.to_a.join("\n")].join(" ")
568
+ else
569
+ @xdef = args.join(" ")
570
+ end
571
+ end
572
+
573
+ def ydef (*args)
574
+ if args.size == 1 and
575
+ ( args.first.is_a?(CArray) or args.first.is_a?(Array) )
576
+ ary = args.first
577
+ @ydef = [ary.length, "levels", ary.to_a.join("\n")].join(" ")
578
+ else
579
+ @ydef = args.join(" ")
580
+ end
581
+ end
582
+
583
+ def zdef (*args)
584
+ if args.size == 1 and
585
+ ( args.first.is_a?(CArray) or args.first.is_a?(Array) )
586
+ ary = args.first
587
+ @zdef = [ary.length, "levels", ary.to_a.join("\n")].join(" ")
588
+ else
589
+ @zdef = args.join(" ")
590
+ end
591
+ end
592
+
593
+ def tdef (*args)
594
+ if args.size == 1 and
595
+ ( args.first.is_a?(CArray) or args.first.is_a?(Array) )
596
+ ary = args.first
597
+ @tdef = [ary.length, "levels", ary.to_a.join("\n")].join(" ")
598
+ else
599
+ @tdef = args.join(" ")
600
+ end
601
+ end
602
+
603
+ def vectorpairs (args)
604
+ @vectorpairs = args.join(" ")
605
+ end
606
+
607
+ def var2d (name, obj, desc = "x")
608
+ @vars.push([name, 2, obj, desc])
609
+ if obj.rank == 3 and obj.dim0 > @nt
610
+ @nt = obj.dim0
611
+ end
612
+ end
613
+
614
+ def var3d (name, obj, desc = "x")
615
+ @vars.push([name, 3, obj, desc])
616
+ if obj.rank == 4 and obj.dim0 > @nt
617
+ @nt = obj.dim0
618
+ end
619
+ end
620
+
621
+ public
622
+
623
+ def define (&block)
624
+ instance_eval(&block)
625
+ end
626
+
627
+ def write (ctl_file)
628
+ open(ctl_file, "w") { |io|
629
+ if @dset
630
+ io.puts "dset #{@dset}"
631
+ else
632
+ raise "dset is required"
633
+ end
634
+ if @title
635
+ io.puts "title #{@title}"
636
+ end
637
+ io.puts "undef #{@undef}"
638
+ @options.each do |opt|
639
+ io.puts %{options #{opt}}
640
+ end
641
+ if @pdef
642
+ io.puts %{pdef #{@pdef}}
643
+ end
644
+ io.puts %{xdef #{@xdef}}
645
+ io.puts %{ydef #{@ydef}}
646
+ io.puts %{zdef #{@zdef}}
647
+ io.puts %{tdef #{@tdef}}
648
+ if @vectorpairs
649
+ io.puts %{vectorpairs #{@vectorpairs}}
650
+ end
651
+
652
+ if @dset =~ /\A\^/
653
+ cwd = File.dirname(File.expand_path(ctl_file))
654
+ dset = File.join(cwd, @dset[1..-1])
655
+ else
656
+ dset = @dset
657
+ end
658
+
659
+ io.puts %{vars #{@vars.size}}
660
+ @vars.each do |name, d, obj, desc|
661
+ case d
662
+ when 2
663
+ io.puts %{#{name} 1 99 #{desc}}
664
+ when 3
665
+ case obj.rank
666
+ when 3
667
+ io.puts %{#{name} #{obj.dim0} 99 #{desc} }
668
+ when 4
669
+ io.puts %{#{name} #{obj.dim1} 99 #{desc} }
670
+ else
671
+ raise "invalid size"
672
+ end
673
+ end
674
+ end
675
+ io.puts "endvars"
676
+
677
+ open(dset, "w") { |dat|
678
+ @nt.times do |i|
679
+ @vars.each do |name, d, obj, desc|
680
+ case d
681
+ when 2
682
+ case obj.rank
683
+ when 2
684
+ obj.float.dump_binary(dat)
685
+ when 3
686
+ obj[i,false].float.dump_binary(dat)
687
+ end
688
+ when 3
689
+ case obj.rank
690
+ when 3
691
+ obj.float.dump_binary(dat)
692
+ when 4
693
+ obj[i,false].float.dump_binary(dat)
694
+ end
695
+ end
696
+ end
697
+ end
698
+ }
699
+ }
700
+
701
+
702
+ end
703
+
704
+ def template (ctlfile, &block)
705
+ define(&block)
706
+ write(ctlfile)
707
+ end
708
+
709
+ end