rfits 0.0.1

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,1328 @@
1
+ require 'ftools'
2
+ require 'ostruct'
3
+ require 'csv'
4
+
5
+ # RFits is a library for parsing the Flexible Image Transport System (FITS) files widely used in astronomy.
6
+ #
7
+ # ==== Installing
8
+ #
9
+ # RFits requires the C library CFITSIO (http://heasarc.gsfc.nasa.gov/docs/software/fitsio/fitsio.html)
10
+ # to be installed. In most cases this is as simple as:
11
+ #
12
+ # > tar zxvf cfitsioxxxx.tar.gz
13
+ # > cd cfitsio
14
+ # > ./configure
15
+ # > make shared
16
+ # > sudo make install
17
+ #
18
+ # You can then install via rubygems in the usual way:
19
+ #
20
+ # > gem install rfits
21
+ #
22
+ # ==== Using
23
+ #
24
+ # require 'rubygems'
25
+ # require 'rfits'
26
+ #
27
+ # RFits::File.open('m31.fits', 'rw') do |fits| # m31.fits exists
28
+ # # first extension is an image
29
+ # img = fits[0]
30
+ #
31
+ # # retrieve/write header values using hash syntax
32
+ # header = img.header
33
+ # puts header['TELESCOP'] #=> 'KPNO 4.0 meter telescope'
34
+ # puts header['TELEQUIN'] #=> 2000.0
35
+ # puts header['SIMPLE'] #=> true
36
+ # puts header['TELFOCUS'] #=> -9998
37
+ # header['MY_HDR1'] = 'A nice value'
38
+ # header['MY_HDR2'] = 10
39
+ # header['MY_HDR3'] = 9.1
40
+ # header['MY_HDR4'] = Complex.new(4, 1) # yes, you can do this
41
+ #
42
+ # # retrieve/write pixel values using array syntax
43
+ # pixels = img.data
44
+ # first_pixel = pixels[0] # the first pixel
45
+ # pixel_list = pixels[0..10] # pixels 0 through 10 as an array
46
+ # pixel_list = pixels[[0, 0], [10, 10]] # pixels between the points [0, 0] and [10, 10]
47
+ # pixels[10] = 5 # the ninth pixel value is set to 5
48
+ # pixels[3..7] = [1, 4, 2, 8] # pixels 3 through 7 are set
49
+ #
50
+ # # second extension is a binary table
51
+ # tbl = fits[1]
52
+ #
53
+ # # access a table using array syntax
54
+ # data = tbl.data
55
+ # row = data[0] # first row as a hetergeneous array
56
+ # col = data.column(2) # third column
57
+ # data[1] = [1, 1.2, 'blah', Complex.new(1, 1)] # second row is set
58
+ # data.set_column(1, [1.3, 4.5, 7.8, 2.2]) # the second column is set
59
+ # col << [1, 1.2, 'blah', Complex.new(1, 1)] # append a row
60
+ #
61
+ # # you can find out things about the table columns
62
+ # metadata = tbl.column_information
63
+ # puts metadata[0].data_type #=> :short
64
+ # puts metadata[0].name #=> 'catalog_id'
65
+ #
66
+ # # and add new ones
67
+ # metadata << {:name => 'new_column1', :format => 'A10'} # append a new string column
68
+ # metadata[3] = {:name => 'new_column2', :format => 'I'} # insert an integer column as the 4th column
69
+ # end
70
+ module RFits
71
+ require 'rfits/ext/rfitsio'
72
+
73
+ # A class representing a FITS file.
74
+ class File
75
+ include Enumerable
76
+
77
+ attr_reader :io
78
+
79
+ # Open an existing FITS file or create a new one. The mode parameter
80
+ # may be on of:
81
+ # * r opens a file for reading. If it doesn't exist, it creates it.
82
+ # * rw opens afile for reading and writing. If it doesn't exist, it creates it.
83
+ # * rw+ opens a file for reading and writing. If it exists, it truncates it.
84
+ #
85
+ # fits = RFits::File.new('m31.fits', 'rw') # this is probably what you want
86
+ def initialize(path, mode="rw")
87
+ iomode = mode.match('w') ? IO::Proxy::READWRITE : IO::Proxy::READONLY
88
+ truncate = mode.match('\+') ? true : false
89
+
90
+ if truncate and mode.match('w')
91
+ @io = IO::Proxy::fits_create_file("!#{path}")
92
+ else
93
+ @io = ::File.exists?(path) ? IO::Proxy.fits_open_file(path, iomode) : IO::Proxy::fits_create_file(path)
94
+ end
95
+ end
96
+
97
+ # Works as RFits::File#new but accepts a block. The FITS file is automatically
98
+ # closed after execution of the block.
99
+ #
100
+ # RFits::File.open('m31.fits', 'rw') do |fits|
101
+ # # do things to the FITS file, fits.close gets called automatically
102
+ # end
103
+ def self.open(path, mode="rw")
104
+ fits = File.new(path, mode)
105
+ if block_given?
106
+ yield fits
107
+ fits.close
108
+ else
109
+ fits
110
+ end
111
+ end
112
+
113
+ # Close the FITS file.
114
+ # file.close()
115
+ def close
116
+ IO::Proxy::fits_close_file(self.io)
117
+ end
118
+
119
+ # The filesystem path to the FITS file.
120
+ # puts file.path # m31.fits
121
+ def path
122
+ IO::Proxy::fits_file_name(self.io)
123
+ end
124
+
125
+ # The IO mode of the FITS file (i.e. IO::Proxy::READWRITE or IO::Proxy::READONLY)
126
+ # puts file.mode # 1 = IO::Proxy::READWRITE
127
+ def mode
128
+ IO::Proxy.fits_file_mode(self.io)
129
+ end
130
+
131
+ # The type of URL of the FITS file (i.e. file:// or ftp://).
132
+ # puts file.url_type # file://
133
+ def url_type
134
+ IO::Proxy.fits_url_type(self.io)
135
+ end
136
+
137
+ # Get the specified HDU associated with the FITS file. Contrary
138
+ # to FITS conventions this is *zero-based* access. So:
139
+ # primary_hdu = fits[0] # primary array
140
+ # hdu = fits[2] # third HDU
141
+ def [](extpos)
142
+ extpos = extpos < 0 ? (self.length + extpos) : extpos
143
+
144
+ set_position(extpos)
145
+ hdu = case IO::Proxy.fits_get_hdu_type(self.io)
146
+ when IO::Proxy::IMAGE_HDU then Image.new(self, extpos, nil, nil)
147
+ when IO::Proxy::ASCII_TBL then AsciiTable.new(self, extpos, nil, nil)
148
+ when IO::Proxy::BINARY_TBL then BinaryTable.new(self, extpos, nil, nil)
149
+ else HDU.new(self, extpos)
150
+ end
151
+
152
+ hdu
153
+ end
154
+
155
+ # Get the last HDU in the file.
156
+ def last
157
+ self[-1]
158
+ end
159
+
160
+ # Get the first HDU in the file.
161
+ def first
162
+ self[0]
163
+ end
164
+
165
+ # Iterates through each HDU in the FITS file.
166
+ # fits.each do |hdu|
167
+ # # do something...
168
+ # end
169
+ def each
170
+ (0...self.length).each do |i|
171
+ yield self[i]
172
+ end
173
+ end
174
+
175
+ # The number of HDUs in the FITS file.
176
+ def length
177
+ IO::Proxy.fits_get_num_hdus(self.io)
178
+ end
179
+
180
+ # Delete the HDU at the specified position. Again, this is
181
+ # zero-based access.
182
+ # fits.delete_at(2) # delete the third HDU
183
+ def delete_at(extpos)
184
+ set_position(extpos)
185
+ Proxy.fits_delete_hdu(self.io)
186
+ end
187
+
188
+ # Delete the specified HDU.
189
+ # fits.delete(hdu)
190
+ def delete(hdu)
191
+ delete_at(hdu.position)
192
+ end
193
+
194
+ # Append a new HDU to the file.
195
+ # fits << RFits::Image.new(@fits, nil, RFits::IO::Proxy::LONG_IMG, [4, 4]) # notice you can leave the position nil
196
+ def <<(hdu, *args)
197
+ hdu.create(self.length, *args)
198
+ end
199
+
200
+ # Insert a new HDU at the specified position.
201
+ # fits[1] = RFits::Image.new(@fits, nil, RFits::IO::Proxy::LONG_IMG, [2, 3]) # the old hdu at pos 1 gets pushed down
202
+ def []=(pos, *args)
203
+ hdu = args[0]
204
+ hdu.create(pos, *args[1..-1])
205
+ end
206
+
207
+ def set_position(extpos) #:nodoc:
208
+ IO::Proxy.fits_movabs_hdu(self.io, extpos + 1)
209
+ end
210
+ end
211
+
212
+ # Represents the header keyword information of an HDU. Behaves
213
+ # much like a hash.
214
+ class Header
215
+ include Enumerable
216
+
217
+ # The HDU associated with the header.
218
+ attr_reader :hdu
219
+
220
+ # Initialize the header. This doesn't actually write bytes to file--use Header#create for that.
221
+ # header = Header.new(hdu)
222
+ def initialize(hdu)
223
+ @hdu = hdu
224
+ end
225
+
226
+ # Retrieve the value of the specified keyword.
227
+ # If _include_comment_ is true, the returned value will be a two-element
228
+ # array with the value as the first element, and the comment as the second.
229
+ # The return type of the value will be determined automatically--this is
230
+ # almost always what you want. However, if necessary, you may specify the type using _as_,
231
+ # which may be one of: "C" (string), "L" (logical), "I" (integer), "F" (float) or "C" (complex)
232
+ # and the library will try to do the right thing.
233
+ #
234
+ # puts header['OBJNAME'] # M31
235
+ # puts header['OBJNAME', true] # ['M31', 'The name of the object']
236
+ # puts header['BITPIX'] # 8
237
+ # puts header['BITPIX', false, 'F'] # 8.0
238
+ def [](keyname, include_comment=false, as=nil)
239
+ self.hdu.reset_position()
240
+ IO::Proxy.fits_read_keyn(self.hdu.file.io, 0)
241
+
242
+ begin
243
+ keytype = as ? as : IO::Proxy.fits_get_keytype(IO::Proxy.fits_read_keyword(self.hdu.file.io, keyname).first) rescue IO::Exception
244
+ key = case keytype
245
+ when 'C' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TSTRING, keyname)
246
+ when 'L' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TLOGICAL, keyname)
247
+ when 'I' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TLONG, keyname)
248
+ when 'F' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TDOUBLE, keyname)
249
+ when 'X' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TDBLCOMPLEX, keyname)
250
+ else IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TSTRING, keyname)
251
+ end
252
+ include_comment ? key : key.first
253
+ rescue IO::Exception
254
+ nil
255
+ end
256
+ end
257
+
258
+ # Set the value of the keyword. If _keyvalue_ is an array, the first
259
+ # element of that array is assumed to be the value, and the second the
260
+ # comment. The value may be a: String, boolean, Fixnum, Float or Complex
261
+ # object. Anything else will be serialized using it's to_s method and
262
+ # saved as a string.
263
+ #
264
+ # header['TEST1'] = 123 # set keyword value to 123
265
+ # header['TEST1'] = [123, 'a test integer'] # set keyword value to 123 with "a test integer" as the comment
266
+ def []=(keyname, keyvalue)
267
+ self.hdu.reset_position()
268
+
269
+ value, comment = keyvalue.is_a?(Array) ? keyvalue : [keyvalue, nil]
270
+
271
+ dtype = case value
272
+ when String then IO::Proxy::TSTRING
273
+ when TrueClass then IO::Proxy::TLOGICAL
274
+ when FalseClass then IO::Proxy::TLOGICAL
275
+ when Fixnum then IO::Proxy::TLONG
276
+ when Float then IO::Proxy::TDOUBLE
277
+ when Complex then IO::Proxy::TDBLCOMPLEX
278
+ else
279
+ value = value.to_s
280
+ IO::Proxy::TSTRING
281
+ end
282
+
283
+ IO::Proxy.fits_update_key(self.hdu.file.io, dtype, keyname, value, comment)
284
+ end
285
+
286
+ # Iterate through each keyword pair in the header. By default,
287
+ # values will be converted to their appropriate types, but this can
288
+ # be overridden by setting _typecast_ to false. Additionally,
289
+ # COMMENT and HISTORY keywords are ignored unless _comment_keywords_
290
+ # is set to true.
291
+ #
292
+ # header.each do |name, value, comment|
293
+ # puts "#{name} = #{value}"
294
+ # end
295
+ def each(typecast=true, comment_keywords=false)
296
+ self.hdu.reset_position()
297
+
298
+ (1..self.length).each do |i|
299
+ keyname = value = comment = nil
300
+ if typecast
301
+ keyname = IO::Proxy.fits_read_keyn(self.hdu.file.io, i).first
302
+ value, comment = self[keyname, true]
303
+ else
304
+ keyname, value, comment = IO::Proxy.fits_read_keyn(self.hdu.file.io, i)
305
+ end
306
+
307
+ yield(keyname, value, comment) unless (keyname == "COMMENT" or keyname == "HISTORY") and !comment_keywords
308
+ end
309
+ end
310
+
311
+ # Delete the specified keyword.
312
+ # header.delete('OBJNAME')
313
+ def delete(keyname)
314
+ self.hdu.reset_position()
315
+ IO::Proxy.fits_delete_key(self.hdu.file.io, keyname)
316
+ end
317
+
318
+ # The number of keyword pairs in the header.
319
+ # puts header.length # 62
320
+ def length
321
+ self.hdu.reset_position()
322
+ IO::Proxy.fits_get_hdrspace(self.hdu.file.io).first
323
+ end
324
+
325
+ # Retrieve COMMENT keywords as a string.
326
+ # puts header.comments # This file is part of the EUVE Science Archive. It contains...
327
+ def comment
328
+ self.hdu.reset_position()
329
+
330
+ statements = []
331
+ self.each(false, true) do |keyname, keyvalue, keycomment|
332
+ statements << keycomment.strip.gsub(/^\'/, '').gsub(/\'$/, '') if keyname == 'COMMENT'
333
+ end
334
+
335
+ statements.join("\n")
336
+ end
337
+
338
+ # Append a COMMENT keyword.
339
+ # header.comment = "My comment"
340
+ def comment=(comment)
341
+ self.hdu.reset_position()
342
+ IO::Proxy.fits_write_comment(self.hdu.file.io, comment)
343
+ end
344
+
345
+ # Retrieve HISTORY keywords as a strong.
346
+ # puts header.history
347
+ def history
348
+ self.hdu.reset_position()
349
+
350
+ statements = []
351
+ self.each(false, true) do |keyname, keyvalue, keycomment|
352
+ statements << keycomment.strip.gsub(/^\'/, '').gsub(/\'$/, '') if keyname == 'HISTORY'
353
+ end
354
+
355
+ statements.join("\n")
356
+ end
357
+
358
+ # Append a HISTORY comment.
359
+ # header.history = "My history"
360
+ def history=(history)
361
+ self.hdu.reset_position()
362
+ IO::Proxy.fits_write_history(self.hdu.file.io, history)
363
+ end
364
+
365
+ # Convert the header to a string.
366
+ def to_s
367
+ self.hdu.reset_position()
368
+ IO::Proxy.fits_hdr2str(self.hdu.file.io, false, [], 0)
369
+ end
370
+ end
371
+
372
+ # Represents the actual pixels in a FITS file.
373
+ class ImageData
374
+ include Enumerable
375
+
376
+ BITPIX_IMG_MAP = {
377
+ IO::Proxy::BYTE_IMG => IO::Proxy::TBYTE,
378
+ IO::Proxy::SHORT_IMG => IO::Proxy::TSHORT,
379
+ IO::Proxy::LONG_IMG => IO::Proxy::TLONG,
380
+ IO::Proxy::LONGLONG_IMG => IO::Proxy::TLONGLONG,
381
+ IO::Proxy::FLOAT_IMG => IO::Proxy::TFLOAT,
382
+ IO::Proxy::DOUBLE_IMG => IO::Proxy::TDOUBLE,
383
+ IO::Proxy::SBYTE_IMG => IO::Proxy::TSBYTE,
384
+ IO::Proxy::USHORT_IMG => IO::Proxy::TUSHORT,
385
+ IO::Proxy::ULONG_IMG => IO::Proxy::TULONG
386
+ }
387
+
388
+ # The image the data belongs to.
389
+ attr_reader :image
390
+
391
+ # Create an ImageData object associated with the specified image.
392
+ def initialize(image)
393
+ @image = image
394
+ end
395
+
396
+ # An alias for ImageData#pixels, but automatically chooses the
397
+ # datatype for you based on the type of image (i.e. BITPIX).
398
+ def [](*args)
399
+ self.pixels(BITPIX_IMG_MAP[self.image.bitpix], *args)
400
+ end
401
+
402
+ # An alias for ImageData#set_pixels, but automatically chooses the
403
+ # datatype for you based on the type of image (i.e. BITPIX).
404
+ def []=(*args)
405
+ self.set_pixels(BITPIX_IMG_MAP[self.image.bitpix], *args)
406
+ end
407
+
408
+ # Retrieve the value of a pixel, or a range of pixels. The value
409
+ # is coerced into an appropriate class according to the datatype provided.
410
+ # Can be used in a number of ways. The simplest plucks the nth pixel from the image array:
411
+ #
412
+ # image.data.pixels(IO::Proxy::TSHORT, 0) # first pixel in the image (remember zero-based indexing?)
413
+ # image.data.pixels(IO::Proxy::TSHORT, image.data[image.data.size - 1]) # last pixel in the image
414
+ #
415
+ # You can also choose a range of contiguous pixels, in which case an array of values is returned:
416
+ #
417
+ # image.data.pixels(IO::Proxy::TSHORT, 0..10) # retrieve first 10 pixels
418
+ #
419
+ # or pick the single pixel at a particular coordinate:
420
+ #
421
+ # image.data.pixels(IO::Proxy::TSHORT, 4, 5)
422
+ #
423
+ # or equivalently:
424
+ #
425
+ # image.data.pixels(IO::Proxy::TSHORT, [4, 5])
426
+ #
427
+ # In addition, you can give an (x,y) coordinate and a number of pixels, assuming the coordinates are zero-based:
428
+ #
429
+ # image.data.pixels(IO::Proxy::TSHORT, [4, 5], 10) # 10 pixels starting at the point [4, 5]
430
+ #
431
+ # or get all the pixels between two points:
432
+ #
433
+ # image.data.pixels(IO::Proxy::TSHORT, [4, 5], [6, 6]) # all the pixels between the points [4, 5] and [6, 6]
434
+ def pixels(datatype, *args)
435
+ self.image.reset_position()
436
+
437
+ pixnum = nelem = nil
438
+ pixel_values = case args.size
439
+ when 1 # 1 argument
440
+ if args.first.is_a?(Range) # a range of pixels
441
+ pixnum = args.first.first.to_i < 0 ? self.length + args.first.first.to_i : args.first.first.to_i + 1
442
+ nelem = args.first.exclude_end? ?
443
+ args.first.last.to_i - args.first.first.to_i + 1:
444
+ args.first.last.to_i - args.first.first.to_i
445
+ elsif args.first.is_a?(Fixnum) # the nth pixel
446
+ pixnum = args.first.to_i < 0 ? self.length + args.first.to_i : args.first.to_i + 1
447
+ nelem = 1
448
+ elsif args.first.is_a?(Array) # array representing single pixel
449
+ return(IO::Proxy.fits_read_pix(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, 1, nil).first.first)
450
+ else
451
+ raise ArgumentError, "Argument should be an integer, an integer range, or an array of integers."
452
+ end
453
+
454
+ IO::Proxy.fits_read_img(self.image.file.io, datatype, pixnum, nelem, nil).first
455
+ when 2 # 2 arguments
456
+ if args.first.is_a?(Fixnum) and args.last.is_a?(Fixnum) # the pixel at the coordinate
457
+ IO::Proxy.fits_read_pix(self.image.file.io, datatype, [args.first.to_i+1, args.last.to_i+1], 1, nil).first
458
+ elsif args.first.is_a?(Array) and args.last.is_a?(Fixnum) # a starting coordinate + some number of pixels
459
+ fpixel = args.first.collect{ |p| p + 1 }
460
+ IO::Proxy.fits_read_pix(self.image.file.io, datatype, fpixel, args.last.to_i, nil).first
461
+ elsif args.first.is_a?(Array) and args.last.is_a?(Array) # a starting coordinate + ending coordinate
462
+ fpixel = args.first.collect{ |p| p + 1 }
463
+ lpixel = args.last.collect{ |p| p + 1 }
464
+ IO::Proxy.fits_read_subset(self.image.file.io, datatype, fpixel, lpixel, args.first.collect{ |dim| 1 }, nil).first
465
+ else
466
+ raise ArgumentError, "Arguments should be two fixnums, an array and a fixnum, or two arrays."
467
+ end
468
+ else
469
+ raise ArgumentError, "Arguments should be an integer, an integer range, an array of integers, two fixnums, an array and a fixnum, or two arrays."
470
+ end
471
+
472
+ pixel_values.size == 1 ? pixel_values.first : pixel_values
473
+ end
474
+
475
+ # Set the value of a pixel, or a range of pixels. The value
476
+ # is coerced into an appropriate class according to the datatype provided.
477
+ # Can be used in a number of ways. The simplest sets the nth pixel in the image array:
478
+ #
479
+ # image.data.set_pixels(IO::Proxy::TSHORT, 0, 5) # first pixel in the image is set to 5
480
+ #
481
+ # You can also set a range of contiguous pixels:
482
+ #
483
+ # image.data.set_pixels(IO::Proxy::TSHORT, 0..10, [5, 5, 3, 1, 0, 9, 5, 6, 7, 1]) # set first 10 pixels
484
+ #
485
+ # or set the single pixel at a particular coordinate:
486
+ #
487
+ # image.data.set_pixels(IO::Proxy::TSHORT, 4, 5, 10) # set pixel at [4, 5] to 10
488
+ #
489
+ # or equivalently:
490
+ #
491
+ # image.data.set_pixels(IO::Proxy::TSHORT, [4, 5], 10)
492
+ #
493
+ # In addition, you can give an (x,y) coordinate and a list of pixels, assuming the coordinates are zero-based:
494
+ #
495
+ # image.data.set_pixels(IO::Proxy::TSHORT, [4, 5], [1, 2, 3, 4]) # four pixels with values 1, 2, 3, 4 starting at [4, 5]
496
+ #
497
+ # or set the pixels between two points:
498
+ #
499
+ # image.data.set_pixels(IO::Proxy::TSHORT, [0, 0], [1, 1], [5, 6, 7, 8]) # set pixels to (5, 6, 7, 8) between [0, 0] and [1, 1]
500
+ def set_pixels(datatype, *args)
501
+ self.image.reset_position()
502
+
503
+ val = args.last.is_a?(Array) ? args.last : [args.last]
504
+ case args.size
505
+ when 2
506
+ if args.first.is_a?(Fixnum)
507
+ pixnum = args.first.to_i < 0 ? self.length + args.first.to_i : args.first.to_i + 1
508
+ IO::Proxy.fits_write_img(self.image.file.io, datatype, pixnum, val.size, val)
509
+ elsif args.first.is_a?(Range)
510
+ pixnum = args.first.first.to_i < 0 ? self.length + args.first.first.to_i : args.first.first.to_i + 1
511
+ nelem = args.first.exclude_end? ?
512
+ args.first.last.to_i - args.first.first.to_i + 1:
513
+ args.first.last.to_i - args.first.first.to_i
514
+ IO::Proxy.fits_write_img(self.image.file.io, datatype, pixnum, nelem, val)
515
+ elsif(args.first.is_a?(Array))
516
+ IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, val.size, val, nil)
517
+ else
518
+ raise ArgumentError, "Selector should be an integer, an integer range, or an array of integers."
519
+ end
520
+ when 3
521
+ if args.first.is_a?(Fixnum) and args[-2].is_a?(Fixnum)
522
+ IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args[0..-2].collect{ |c| c + 1 }, val.size, val, nil)
523
+ elsif args.first.is_a?(Array) and args[-2].is_a?(Fixnum)
524
+ IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, args[-2], val, nil)
525
+ elsif args.first.is_a?(Array) and args[-2].is_a?(Array)
526
+ IO::Proxy.fits_write_subset(self.image.file.io, datatype,
527
+ args.first.collect{ |c| c + 1 }, args[1].collect{ |c| c + 1 },
528
+ val)
529
+ else
530
+ raise ArgumentError, "Selectors should be two fixnums, an array and a fixnum, or two arrays."
531
+ end
532
+ else
533
+ raise ArgumentError, "Selectors should be an integer, an integer range, an array of integers, two fixnums, an array and a fixnum, or two arrays."
534
+ end
535
+ end
536
+
537
+ # The number of pixels in the image array.
538
+ def length
539
+ self.image.naxes.inject(1){ |product, n| product *= n }
540
+ end
541
+
542
+ # Iterate through each pixel in the array.
543
+ def each
544
+ (0...self.length).each do |i|
545
+ yield self[i]
546
+ end
547
+ end
548
+
549
+ # Retrieve the ith (where i is zero-based) row of the image array.
550
+ # Assumes a two-dimensional image.
551
+ def row(i)
552
+ self[[0, i], self.image.naxes.first]
553
+ end
554
+
555
+ # Set the ith row of a 2D image to the specified pixels.
556
+ def set_row(i, vals)
557
+ self[[0, i], self.image.naxes.first] = vals
558
+ end
559
+
560
+ # Iterate through each row of pixels in the array.
561
+ # Assumes a two-dimensional image.
562
+ def each_row
563
+ (0...self.image.naxes.last).each do |i|
564
+ yield(self.row(i), i)
565
+ end
566
+ end
567
+
568
+ # Retrieve the ith (where i is zero-based) column of the image array.
569
+ # Assumes a two-dimensional image.
570
+ def column(i)
571
+ pixel_list = []
572
+ (0...self.image.naxes.first).each do |row|
573
+ pixel_list << self[i, row]
574
+ end
575
+
576
+ pixel_list
577
+ end
578
+
579
+ # Set the ith column of a 2D image to the specified pixels.
580
+ def set_column(i, vals)
581
+ (0...self.image.naxes.first).each do |row|
582
+ self[i, row] = vals[row]
583
+ end
584
+ end
585
+
586
+ # Iterate through each column of pixels in the array.
587
+ # Assumes a two-dimensional image.
588
+ def each_column
589
+ (0...self.image.naxes.last).each do |i|
590
+ yield(self.column(i), i)
591
+ end
592
+ end
593
+
594
+ # The first pixel in the image array.
595
+ def first
596
+ self[0]
597
+ end
598
+
599
+ # The last pixel in the image array.
600
+ def last
601
+ self[self.length - 1]
602
+ end
603
+ end
604
+
605
+ # Represents tabular data in a FITS file.
606
+ class TableData
607
+ include Enumerable
608
+
609
+ # The table the data lives in.
610
+ attr_reader :table
611
+
612
+ # Create a TableData object that is associated with the specifed table.
613
+ # data = TableData.new(table)
614
+ def initialize(table)
615
+ @table = table
616
+ end
617
+
618
+ # Set the ith columns to the specified values.
619
+ # data.set_column(2, [1, 2, 3, 4, 5]) # set the 3rd column (which is an integer column)
620
+ def set_column(i, values)
621
+ self.table.reset_position
622
+
623
+ IO::Proxy.fits_write_colnull(self.table.file.io,
624
+ self.table.column_information[i].data_type(true), i+1, 1, 1, values.size,
625
+ values, nil)
626
+ end
627
+
628
+ # Retrieve the ith column.
629
+ # data.column(2) # [1, 2, 3, 4, 5]
630
+ def column(i)
631
+ self.table.reset_position
632
+
633
+ IO::Proxy.fits_read_col(self.table.file.io,
634
+ self.table.column_information[i].data_type(true), i+1, 1, 1, self.size, nil).first
635
+ end
636
+
637
+ # Retrieve the ith row of data.
638
+ # data[2] # ['a1', 1.1, 3, Complex.new(3, 4)]
639
+ # or the value of the cell at the specified row and column.
640
+ # data[2, 3] # Complex.new(3, 4) # the 4th cell in the 3rd row
641
+ def [](*args)
642
+ self.table.reset_position
643
+
644
+ result = case args.length
645
+ when 1 # retrieve the whole row
646
+ row = []
647
+ (0...self.table.column_information.size).each do |col|
648
+ row << IO::Proxy.fits_read_col(self.table.file.io,
649
+ self.table.column_information[col].data_type(true),
650
+ col+1, (args.first.to_i+1), 1, 1, nil).first.first
651
+ end
652
+ row
653
+ when 2 # retrieve the value of the cell
654
+ IO::Proxy.fits_read_col(self.table.file.io,
655
+ self.table.column_information[args.last].data_type(true),
656
+ (args.last.to_i)+1, (args.first.to_i)+1, 1, 1, nil).first.first
657
+ else
658
+ raise ArgumentError, "Selector should be an integer indicating the row, or two integers indicating the row and column."
659
+ end
660
+
661
+ result
662
+ end
663
+
664
+ # Alias for TableData#[]
665
+ def row(i)
666
+ self[i]
667
+ end
668
+
669
+ # Set the ith row to the specified values
670
+ # data[4] = ['new', 2.2, 7, Complex.new(5, 5)]
671
+ # or specify the value of a particular cell
672
+ # data[4, 1] = 2.2 # the second column of the 5th row
673
+ def []=(*args)
674
+ self.table.reset_position
675
+
676
+ i = args.first
677
+
678
+ case args.length
679
+ when 2
680
+ values = args[1..-1].first
681
+
682
+ (0...values.size).each do |col|
683
+ IO::Proxy.fits_write_colnull(self.table.file.io,
684
+ self.table.column_information[col].data_type(true), col+1, i+1, 1, 1,
685
+ [values[col]], nil)
686
+ end
687
+ when 3
688
+ col = args[1]
689
+ values = args[2..-1]
690
+
691
+ IO::Proxy.fits_write_colnull(self.table.file.io,
692
+ self.table.column_information[col].data_type(true), col+1, i+1, 1, 1,
693
+ values, nil)
694
+ else
695
+ raise ArgumentError, "Selector should be an integer indicating the row, or two integers indicating the row and column."
696
+ end
697
+ end
698
+
699
+ # Append a new row.
700
+ # data << ['new', 2.2, 7, Complex.new(5, 5)]
701
+ def <<(values)
702
+ self[self.size] = values
703
+ end
704
+
705
+ # Alias for TableData#[]=
706
+ def set_row(i, values)
707
+ self[i] = values
708
+ end
709
+
710
+ # The number of rows in the table.
711
+ # table.length # 5
712
+ def length
713
+ self.table.reset_position
714
+ IO::Proxy.fits_get_num_rows(self.table.file.io)
715
+ end
716
+
717
+ # Alias for TableData#length
718
+ def size
719
+ self.length
720
+ end
721
+
722
+ # Delete the ith row.
723
+ # table.delete(2) # delete the 3rd row
724
+ def delete(i)
725
+ self.table.reset_position
726
+ IO::Proxy.fits_delete_rows(self.table.file.io, i+1, 1)
727
+ end
728
+
729
+ # Alias for TableData#each.
730
+ def each_row
731
+ self.each do |row|
732
+ yield row
733
+ end
734
+ end
735
+
736
+ # Iterate through each row in the table.
737
+ def each
738
+ self.table.reset_position
739
+
740
+ (0...self.size).each do |i|
741
+ yield self[i]
742
+ end
743
+ end
744
+
745
+ # Iterate through each column in the table.
746
+ def each_column
747
+ (0...self.table.column_information.size).each do |i|
748
+ yield self.column(i)
749
+ end
750
+ end
751
+
752
+ # Retrieve the first row in the table.
753
+ # table.first # ['blah', 23.1, 9, Complex.new(4, 5)]
754
+ def first
755
+ self[0]
756
+ end
757
+
758
+ # Retrieve the last row in the table.
759
+ # table.last # ['new', 2.2, 7, Complex.new(5, 5)]
760
+ def last
761
+ self[self.size - 1]
762
+ end
763
+
764
+ # Convert the rows to a string. Currently an alias to TableData#to_csv
765
+ def to_s
766
+ self.to_csv
767
+ end
768
+
769
+ # Convert the rows in the table to CSV.
770
+ def to_csv
771
+ buffer = ''
772
+
773
+ self.each_row do |row|
774
+ CSV.generate_row(row.collect{ |cell| cell.to_s }, row.size, buffer)
775
+ end
776
+
777
+ buffer
778
+ end
779
+ end
780
+
781
+ # The base class for all HDUs. It's possible to use this on its
782
+ # own, but really it's intended to be abstract.
783
+ class HDU
784
+ HDU_TYPE_MAP = {
785
+ IO::Proxy::IMAGE_HDU => :image,
786
+ IO::Proxy::ASCII_TBL => :ascii_tbl,
787
+ IO::Proxy::BINARY_TBL => :binary_tbl,
788
+ IO::Proxy::ANY_HDU => :other
789
+ }
790
+
791
+ # The file object associated with the HDU.
792
+ attr_reader :file
793
+
794
+ # The header associated with the HDU.
795
+ attr_reader :header
796
+
797
+ # The data associated with the HDU.
798
+ attr_reader :data
799
+
800
+ # Initialize a new HDU in the specified file at the specified position (zero-based).
801
+ # This doesn't actually write bytes to file--use HDU#create for that.
802
+ # fits = File.open('m31.fits', 'rw') do |f|
803
+ # hdu = HDU.new(f, 2)
804
+ # end
805
+ def initialize(file, extpos)
806
+ @file = file
807
+ @extpos = extpos
808
+
809
+ @header = Header.new(self)
810
+ @data = ImageData.new(self)
811
+ end
812
+
813
+ # Get the type of HDU. Possibilities are: :image, :ascii_tbl, :binary_tbl, :other
814
+ def hdu_type
815
+ reset_position()
816
+ HDU_TYPE_MAP[IO::Proxy.fits_get_hdu_type(self.file.io)] || :unknown
817
+ end
818
+
819
+ # Get the position of the HDU.
820
+ # puts hdu.position # 2 -> this is the third HDU in the file
821
+ def position
822
+ reset_position()
823
+ IO::Proxy.fits_get_hdu_num(self.file.io) - 1
824
+ end
825
+
826
+ def reset_position
827
+ IO::Proxy.fits_movabs_hdu(self.file.io, @extpos + 1)
828
+ end
829
+ end
830
+
831
+ # A set of routines for setting and retrieving compression characterstics.
832
+ module Compressible
833
+ COMPRESSION_TYPE_MAP = {
834
+ IO::Proxy::RICE_1 => :rice,
835
+ IO::Proxy::GZIP_1 => :gzip,
836
+ IO::Proxy::PLIO_1 => :plio,
837
+ IO::Proxy::HCOMPRESS_1 => :hcompress
838
+ }
839
+
840
+ COMPRESSION_DIM_KEY = 'ZNAXIS'
841
+ COMPRESSION_TYPE_KEY = 'ZCOMPTYPE'
842
+ COMPRESSION_TILE_KEY_ROOT = 'ZTILE'
843
+ COMPRESSION_VAR_KEY_ROOT = 'ZNAME'
844
+ COMPRESSION_VAR_VAL_ROOT = 'ZVAL'
845
+ COMPRESSION_NOISE_BIT_NAME_VALUE = 'NOISEBIT'
846
+ COMPRESSION_SCALE_NAME_VALUE = 'SCALE'
847
+ COMPRESSION_SMOOTH_NAME_VALUE = 'SMOOTH'
848
+
849
+ # Activate compression on the image.
850
+ # "options" is a hash with the following keys:
851
+ # [:compression_type] the type of compression (:rice, :gzip, :plio, :hcompress). Required.
852
+ # [:tile_dim] the size of the tiles used in compression. Optional.
853
+ # [:noise_bits] applicable for floating point images only. Optional.
854
+ # [:hcomp_scale] scale factor for use in hcompress compression. Optional.
855
+ # [:hcomp_smooth] smoothing factor for use in hcompress compression. Optional.
856
+ def activate_compression
857
+ self.compression_type = self.compression_options[:compression_type] if self.compression_options[:compression_type]
858
+ self.tile_dim = self.compression_options[:tile_dim] if self.compression_options[:tile_dim]
859
+ self.noise_bits = self.compression_options[:noise_bits] if self.compression_options[:noise_bits]
860
+ self.hcomp_scale = self.compression_options[:hcomp_scale] if self.compression_options[:hcomp_scale]
861
+ self.hcomp_smooth = self.compression_options[:hcomp_smooth] if self.compression_options[:hcomp_smooth]
862
+ end
863
+
864
+ # Turn off compression for the image.
865
+ def deactivate_compression
866
+ self.compression_type = 0
867
+ end
868
+
869
+ # Retrieve the compression type of the image. Possible values: :rice, :gzip, :plio, :hcompress
870
+ def compression_type
871
+ reset_position()
872
+
873
+ ctype = case self.header['ZCMPTYPE']
874
+ when 'GZIP_1' then IO::Proxy::GZIP_1
875
+ when 'RICE_1' then IO::Proxy::RICE_1
876
+ when 'HCOMPRESS_1' then IO::Proxy::HCOMPRESS_1
877
+ when 'PLIO_1' then IO::Proxy::PLIO_1
878
+ else
879
+ raise "Unknown compression type #{self.header[COMPRESSION_TYPE_KEY]}."
880
+ end
881
+
882
+ COMPRESSION_TYPE_MAP[ctype]
883
+ end
884
+
885
+ # Set the compression type of the image. Possible values: :rice, :gzip, :plio, :hcompress
886
+ # image.compression_type = :gzip
887
+ def compression_type=(ctype)
888
+ ctype ? IO::Proxy.fits_set_compression_type(self.file.io, COMPRESSION_TYPE_MAP.invert[ctype.to_sym] || ctype) :
889
+ IO::Proxy.fits_set_compression_type(self.file.io, nil)
890
+ end
891
+
892
+ # Get the tile dimensions for the compressed image.
893
+ def tile_dim
894
+ reset_position()
895
+
896
+ tile_dims = []
897
+ (1..self.header[COMPRESSION_DIM_KEY]).each do |i|
898
+ tile_dims << self.header["#{COMPRESSION_TILE_KEY_ROOT}#{i}"]
899
+ end
900
+
901
+ tile_dims
902
+ end
903
+
904
+ # Set the tile dimensions for the compressed image.
905
+ # image.tile_dim = [5, 5]
906
+ def tile_dim=(tilesizes)
907
+ IO::Proxy.fits_set_tile_dim(self.file.io, tilesizes.size, tilesizes)
908
+ end
909
+
910
+ # Get the noise bits for the compressed image.
911
+ def noise_bits
912
+ reset_position()
913
+
914
+ zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
915
+ name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_NOISE_BIT_NAME_VALUE
916
+ }
917
+ zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
918
+ end
919
+
920
+ # Set the noise bits for the compressed image.
921
+ def noise_bits=(bits)
922
+ IO::Proxy.fits_set_noise_bits(self.file.io, bits.to_i)
923
+ end
924
+
925
+ # Get the hcompress scale if the image is compressed using hcompress.
926
+ def hcomp_scale
927
+ reset_position()
928
+
929
+ zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
930
+ name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_SCALE_NAME_VALUE
931
+ }
932
+ zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
933
+ end
934
+
935
+ # Set the hcompress scale if the image is compressed using hcompress.
936
+ def hcomp_scale=(scale)
937
+ IO::Proxy.fits_set_hcomp_scale(self.file.io, scale.to_i)
938
+ end
939
+
940
+ # Get the hcompress smoothing factor if the image is compressed using hcompress.
941
+ def hcomp_smooth
942
+ reset_position()
943
+
944
+ zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
945
+ name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_SMOOTH_NAME_VALUE
946
+ }
947
+ zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
948
+ end
949
+
950
+ # Set the hcompress smoothing factor if the image is compressed using hcompress.
951
+ def hcomp_smooth=(smooth)
952
+ IO::Proxy.fits_set_hcomp_smooth(self.file.io, smooth.to_i)
953
+ end
954
+ end
955
+
956
+ # An Image HDU.
957
+ class Image < HDU
958
+ include Compressible
959
+
960
+ attr_reader :compression_options
961
+
962
+ IMG_TYPE_MAP = {
963
+ IO::Proxy::BYTE_IMG => :byte,
964
+ IO::Proxy::SHORT_IMG => :short,
965
+ IO::Proxy::LONG_IMG => :long,
966
+ IO::Proxy::LONGLONG_IMG => :longlong,
967
+ IO::Proxy::FLOAT_IMG => :float,
968
+ IO::Proxy::DOUBLE_IMG => :double
969
+ }
970
+
971
+ # Instantiate a new image in the specified file at the specified position,
972
+ # of a certain data type and size.
973
+ # "coptions" is optional, but if present see Compressible#activate_compression for allowed format.
974
+ #
975
+ # If you do activate compression for an image there is one "gotcha" that might take you by surprise:
976
+ # if there are no existing HDUs in the file and you add a compressed image, an extra image HDU at the
977
+ # start of the file will be created. In other words, your new compressed image will be at fits[1]
978
+ # *not* fits[0]. This is because cfitsio compresses the image by wrapping it in a binary table,
979
+ # but the FITS specification mandates that the primary HDU must always be an image array.
980
+ def initialize(file, extpos, bitpix, naxes, coptions=nil)
981
+ super(file, extpos)
982
+
983
+ @bitpix = bitpix
984
+ @naxes = naxes
985
+
986
+ @compression_options = coptions
987
+ end
988
+
989
+ # Actually create a physical image (i.e. write bytes).
990
+ # "at" is the position at which to insert the image.
991
+ def create(at)
992
+ self.deactivate_compression
993
+
994
+ # A dummy image gets added if to the top of the file if
995
+ # the first image added is compressed.
996
+ @extpos = (self.file.length == 0 and self.compression_options) ? at + 1 : at
997
+
998
+ self.file.set_position(at - 1 < 0 ? 0 : at - 1)
999
+ self.activate_compression() if self.compression_options
1000
+ IO::Proxy.fits_insert_img(self.file.io, @bitpix, @naxes.size, @naxes)
1001
+
1002
+ # This just makes sure something is written out right away.
1003
+ self.header['RFITS1234'] = 'temp'
1004
+ self.header.delete('RFITS1234')
1005
+ end
1006
+
1007
+ # Alias for Image#dim
1008
+ def naxis
1009
+ self.dim
1010
+ end
1011
+
1012
+ # The number of dimensions of the image (i.e. 2).
1013
+ def dim
1014
+ reset_position()
1015
+ IO::Proxy.fits_get_img_dim(self.file.io)
1016
+ end
1017
+
1018
+ # The size of each dimension (i.e. [512, 512]).
1019
+ def size
1020
+ reset_position()
1021
+ IO::Proxy.fits_get_img_size(self.file.io, self.naxis)
1022
+ end
1023
+
1024
+ # Alias for Image#size
1025
+ def naxes
1026
+ self.size
1027
+ end
1028
+
1029
+ # Alter image dimensions to the specified size.
1030
+ # img.naxes = [1000, 200]
1031
+ def naxes=(naxes)
1032
+ reset_position()
1033
+ IO::Proxy.fits_resize_img(self.file.io, self.bitpix, naxes.size, naxes)
1034
+ end
1035
+
1036
+ # The type of image. Possibilities include: :byte, :short, :long, :longlong, :float, :double
1037
+ def image_type
1038
+ reset_position()
1039
+ etype = IO::Proxy.fits_get_img_equivtype(self.file.io)
1040
+ IMG_TYPE_MAP[etype] || etype
1041
+ end
1042
+
1043
+ # Number of bits per data pixel. By default returns the "equivalent type" unless _equiv_ is set to false.
1044
+ def bitpix(equiv=true)
1045
+ reset_position()
1046
+ equiv ? IO::Proxy.fits_get_img_equivtype(self.file.io) : IO::Proxy.fits_get_img_type(self.file.io)
1047
+ end
1048
+
1049
+ # Alter the datatype of the image.
1050
+ # img.bitpix = IO::Proxy::LONG_IMG
1051
+ def bitpix=(bitpix)
1052
+ reset_position()
1053
+ IO::Proxy.fits_resize_img(self.file.io, bitpix, self.naxis, self.naxes)
1054
+ end
1055
+
1056
+ # Resize the image to the specified dimensions and data type.
1057
+ # img.resize(IO::Proxy::LONG_IMG, [1000, 200])
1058
+ def resize(bitpix, naxes)
1059
+ reset_position()
1060
+ IO::Proxy.fits_resize_img(self.file.io, bitpix, naxes.size, naxes)
1061
+ end
1062
+
1063
+ # Returns true if the image is compressed, false otherwise.
1064
+ def compressed?
1065
+ reset_position()
1066
+ IO::Proxy.fits_is_compressed_image(self.file.io) == 1 ? true : false
1067
+ end
1068
+ end
1069
+
1070
+ # A class that encapsulates knowledge about the name and type of a column in a table.
1071
+ class ColumnInformation
1072
+ COLUMN_TYPE_MAP = {
1073
+ IO::Proxy::TBIT => :bit,
1074
+ IO::Proxy::TBYTE => :byte,
1075
+ IO::Proxy::TLOGICAL => :logical,
1076
+ IO::Proxy::TSTRING => :string,
1077
+ IO::Proxy::TSHORT => :short,
1078
+ IO::Proxy::TLONG => :long,
1079
+ IO::Proxy::TFLOAT => :float,
1080
+ IO::Proxy::TDOUBLE => :double,
1081
+ IO::Proxy::TCOMPLEX => :complex,
1082
+ IO::Proxy::TDBLCOMPLEX => :dblcomplex
1083
+ }
1084
+
1085
+ # The table the column belongs to.
1086
+ attr_reader :table
1087
+
1088
+ # The position of the column in the table.
1089
+ attr_reader :position
1090
+
1091
+ # Create a new ColumnInformation object on the specified table (of type AsciiTable or BinaryTable)
1092
+ # at the specified location (a zero-based index).
1093
+ # ci = ColumnInformation.new(tbl, 2) # the third column in the table
1094
+ def initialize(table, position)
1095
+ @table = table
1096
+ @position = position
1097
+ end
1098
+
1099
+ # The name of the column.
1100
+ # ci.name # 'column_3'
1101
+ def name
1102
+ self.table.reset_position
1103
+ IO::Proxy.fits_get_colname(self.table.file.io, IO::Proxy::CASEINSEN, "#{self.position + 1}").first
1104
+ end
1105
+
1106
+ # The data type of the column. May be one of:
1107
+ # :bit, :byte, :logical, :string, :short, :long, :float, :double, :complex, :dblcomplex
1108
+ # If the data type is unrecognized, an integer corresponding to the cfitsio data type
1109
+ # will be returned instead.
1110
+ # ci.data_type # :string
1111
+ def data_type(as_int=false)
1112
+ self.table.reset_position
1113
+ type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
1114
+
1115
+ as_int ? type_info[0] : COLUMN_TYPE_MAP[type_info[0]] || type_info[0]
1116
+ end
1117
+
1118
+ # The vector repeat count of the column. An AsciiTable will always have a repeat of 1.
1119
+ # ci.repeat # 10
1120
+ def repeat
1121
+ self.table.reset_position
1122
+ type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
1123
+
1124
+ type_info[1]
1125
+ end
1126
+
1127
+ # The width, in bytes, of the column.
1128
+ # ci.width # 10
1129
+ def width
1130
+ self.table.reset_position
1131
+ type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
1132
+
1133
+ type_info[2]
1134
+ end
1135
+
1136
+ # The display width (i.e. the width of the column if it were to be converted into it's string representation).
1137
+ # ci.display_width # 10
1138
+ def display_width
1139
+ self.table.reset_position
1140
+ IO::Proxy.fits_get_col_display_width(self.table.file.io, self.position+1)
1141
+ end
1142
+
1143
+ # The number of dimensions of the column. "maxdim" is the maximum number of dimensions
1144
+ # to return and defaults to 1.
1145
+ # ci.dim # 1
1146
+ def dim(maxdim=1)
1147
+ self.table.reset_position
1148
+ dim_info = IO::Proxy.fits_read_tdim(self.table.file.io, self.position+1, 1)
1149
+
1150
+ dim_info[0]
1151
+ end
1152
+
1153
+ # An alias for ColumnInformation#dim
1154
+ def naxis(*args)
1155
+ self.dim(*args)
1156
+ end
1157
+
1158
+ # The size of the dimensions of the column. "maxdim" is the maximum number of dimensions
1159
+ # to return and defaults to 1.
1160
+ # ci.size # [1]
1161
+ def size(maxdim=1)
1162
+ self.table.reset_position
1163
+ dim_info = IO::Proxy.fits_read_tdim(self.table.file.io, self.position+1, maxdim)
1164
+
1165
+ return dim_info[1]
1166
+ end
1167
+
1168
+ # An alias for ColumnInformation#size
1169
+ def naxes(*args)
1170
+ self.size(*args)
1171
+ end
1172
+ end
1173
+
1174
+ # A list of ColumnInformation objects representing the set
1175
+ # of column definitions. Behaves much like an array.
1176
+ class ColumnInformationList
1177
+ include Enumerable
1178
+
1179
+ # The table the columns belong to.
1180
+ attr_reader :table
1181
+
1182
+ # Create a list of ColumnInformation from the specified table.
1183
+ # cil = ColumnInformationList.new(table)
1184
+ def initialize(table)
1185
+ @table = table
1186
+ end
1187
+
1188
+ # Retrieve the ith column's information.
1189
+ # cil[1] # the second column's information
1190
+ def [](i)
1191
+ self.table.reset_position
1192
+ ColumnInformation.new(self.table, i)
1193
+ end
1194
+
1195
+ # Insert a column into the ith position.
1196
+ # "options" is a hash with two keys:
1197
+ #
1198
+ # [:name] the name of the column
1199
+ # [:format] the format of the column as explained in the cfitsio documentation
1200
+ #
1201
+ # You can also insert multiple columns at the same time by passing an array
1202
+ # of hashes of the format explained above.
1203
+ # cil[1] = {:name => 'new_column_1', :format => 'A10'}
1204
+ # cil[1] = [{:name => 'new_column_1', :format => 'A10'}, {:name => 'new_column_2', :format => 'I'}]
1205
+ def []=(i, options)
1206
+ self.table.reset_position
1207
+
1208
+ names = []
1209
+ formats = []
1210
+ if options.is_a?(Array)
1211
+ options.each do |spec|
1212
+ names << spec[:name]
1213
+ formats << spec[:format]
1214
+ end
1215
+ else
1216
+ names = [options[:name]]
1217
+ formats = [options[:format]]
1218
+ end
1219
+
1220
+ IO::Proxy.fits_insert_cols(self.table.file.io, i+1, names.size, names, formats)
1221
+ end
1222
+
1223
+ # Iterate through each ColumnInformation object.
1224
+ def each
1225
+ (0...self.size).each do |i|
1226
+ yield self[i]
1227
+ end
1228
+ end
1229
+
1230
+ # Append a new column to the table. See ColumnInformationList#[]= for details
1231
+ # on what "options" can look like.
1232
+ # cil << {:name => 'new_column_1', :format => 'A10'}
1233
+ # cil << [{:name => 'new_column_1', :format => 'A10'}, {:name => 'new_column_2', :format => 'I'}]
1234
+ def <<(options)
1235
+ self[self.size] = options
1236
+ end
1237
+
1238
+ # Delete the ith column.
1239
+ # cil.delete(1) # delete the second column
1240
+ def delete(i)
1241
+ self.table.reset_position
1242
+ IO::Proxy.fits_delete_col(self.table.file.io, i+1)
1243
+ end
1244
+
1245
+ # The number of columns in the table.
1246
+ # cil.size # 4
1247
+ def size
1248
+ self.table.reset_position
1249
+ IO::Proxy.fits_get_num_cols(self.table.file.io)
1250
+ end
1251
+ end
1252
+
1253
+ # A class representing a generic table extension. Is intended to be abstract.
1254
+ class Table < HDU
1255
+ # Metadata for each column.
1256
+ attr_reader :column_information
1257
+
1258
+ # Tabular data.
1259
+ attr_reader :data
1260
+
1261
+ def initialize(file, extpos, names, formats, extname=nil, units=nil)
1262
+ super(file, extpos)
1263
+
1264
+ @column_names = names
1265
+ @column_formats = formats
1266
+ @extname = extname
1267
+ @column_units = units
1268
+
1269
+ @column_information = ColumnInformationList.new(self)
1270
+ @data = TableData.new(self)
1271
+ end
1272
+
1273
+ # Convert a table to CSV.
1274
+ def to_csv
1275
+ col_names = self.column_information.collect{ |ci| ci.name }
1276
+
1277
+ buffer = ''
1278
+ CSV.generate_row(col_names, col_names.size, buffer)
1279
+ buffer << self.data.to_csv
1280
+
1281
+ buffer
1282
+ end
1283
+
1284
+ # Convert a table to a string. Currently an alias for Table#to_csv.
1285
+ def to_s
1286
+ self.to_csv
1287
+ end
1288
+ end
1289
+
1290
+ # A class representing an ASCII table.
1291
+ class AsciiTable < Table
1292
+ def create(at)
1293
+ self.file.set_position(at - 1 < 0 ? 0 : at - 1)
1294
+
1295
+ IO::Proxy.fits_insert_atbl(self.file.io, 0, 0, @column_names.size,
1296
+ @column_names,
1297
+ nil,
1298
+ @column_formats,
1299
+ @column_units,
1300
+ @extname, 0)
1301
+
1302
+ @extpos = at
1303
+
1304
+ # This just makes sure something is written out right away.
1305
+ self.header['RFITS1234'] = 'temp'
1306
+ self.header.delete('RFITS1234')
1307
+ end
1308
+ end
1309
+
1310
+ # A class representing a binary table.
1311
+ class BinaryTable < Table
1312
+ def create(at)
1313
+ self.file.set_position(at - 1 < 0 ? 0 : at - 1)
1314
+
1315
+ IO::Proxy.fits_insert_btbl(self.file.io, 0, @column_names.size,
1316
+ @column_names,
1317
+ @column_formats,
1318
+ @column_units,
1319
+ @extname, 0)
1320
+
1321
+ @extpos = at
1322
+
1323
+ # This just makes sure something is written out right away.
1324
+ self.header['RFITS1234'] = 'temp'
1325
+ self.header.delete('RFITS1234')
1326
+ end
1327
+ end
1328
+ end