31
31
ds9.set("zscale")
32
32
ds9.set("cmap viridis")
33
33
34
- The get method will return a value (as a string or None if there is
35
- no response).
34
+ The get method will return a value (as a string, FITS HDUList, NumPy
35
+ array, or None if there is no response).
36
36
37
37
Syntax errors are displayed as a screen message (to stdout) but they
38
38
do not stop the connection. Lower-level errors - such as the DS9
155
155
"""
156
156
157
157
from contextlib import contextmanager
158
+ from dataclasses import dataclass
158
159
from enum import Enum
159
160
import importlib .metadata
160
161
import os
162
+ from pathlib import Path
161
163
import sys
162
164
import tempfile
163
165
from typing import Protocol
@@ -202,6 +204,16 @@ class Cube(Enum):
202
204
"Data is stored in Hue, Saturation, Value order."
203
205
204
206
207
+ @dataclass (frozen = True )
208
+ class ImgInfo :
209
+ """Metadata about the image."""
210
+
211
+ dtype : np .dtype
212
+ """The datatype."""
213
+ shape : tuple [int , ...]
214
+ """The size of the image. It is expected to be 2D or 3D."""
215
+
216
+
205
217
def add_color (txt ):
206
218
"""Allow ANSI color escape codes unless NO_COLOR env var is set
207
219
or sys.stderr is not a TTY.
@@ -289,7 +301,16 @@ def warning(msg: str) -> None:
289
301
print (f"{ lhs } { msg } " )
290
302
291
303
292
- def extract_url (url : str ) -> str | None :
304
+ def read_array (path : str | Path , img : ImgInfo ) -> np .ndarray :
305
+ """Read in an array from a file."""
306
+
307
+ fp = np .memmap (path , mode = "r" , dtype = img .dtype , shape = img .shape )
308
+ return fp [:]
309
+
310
+
311
+ def extract_url (
312
+ url : str , img : ImgInfo | None
313
+ ) -> str | np .ndarray | fits .HDUList | None :
293
314
"""Read in the URL and return the values.
294
315
295
316
It relies on heuristics to determine the type of the data pointed
@@ -307,18 +328,38 @@ def extract_url(url: str) -> str | None:
307
328
error (f"expected file url, not { url } " )
308
329
return None
309
330
310
- if res .path .endswith (".dat" ):
331
+ # Look at all the DS9 samp commands at
332
+ # https://ds9.si.edu/doc/ref/samp.html
333
+ # that have an example of "string url = ds9.get(string cmd):
334
+ #
335
+ # command suffix encoding
336
+ # ------- ------ --------
337
+ # array .arr binary
338
+ # data .dat.dat ascii
339
+ # pixeltable .pix.txt ascii
340
+ # region .reg.rgn ascii
341
+ #
342
+ # Not included on this page are
343
+ #
344
+ # command suffix encoding
345
+ # ------- ------ --------
346
+ # fits .fits FITS
347
+ #
348
+ #
349
+ if any (res .path .endswith (f".{ end } " ) for end in ["dat" , "rgn" , "txt" ]):
311
350
# What's the best encoding?
312
351
with open (res .path , mode = "rt" , encoding = "ascii" ) as fh :
313
352
return fh .read ()
314
353
315
354
if res .path .endswith (".fits" ):
316
- # Should this extract the data (assuming the input is an
317
- # image)? Also, how do we convert to a string?
318
- #
319
- # return fits.open(res.path)
320
- error ("Unable to convert FITS file to a string" )
321
- return None
355
+ return fits .open (res .path )
356
+
357
+ if res .path .endswith (".arr" ):
358
+ if img is None :
359
+ error ("Sent array but no image data" )
360
+ return None
361
+
362
+ return read_array (res .path , img )
322
363
323
364
error (f"Unable to determine contents of { url } " )
324
365
return None
@@ -348,15 +389,17 @@ def __str__(self) -> str:
348
389
349
390
return f"Connection to DS9 { version } (client { self .client } )"
350
391
351
- def _get (
392
+ def get_raw (
352
393
self , command : str , timeout : int | None = None
353
- ) -> str | dict [str , str ]:
394
+ ) -> dict [str , str ] | None :
354
395
"""Call ds9.get for the given command and arguments.
355
396
356
397
If the call fails then an error message is displayed (to
357
398
stdout) and None is returned. This call will raise an error if
358
399
there is a SAMP commmunication problem.
359
400
401
+ .. versionadded:: 0.1.0
402
+
360
403
Parameters
361
404
----------
362
405
command
@@ -369,7 +412,12 @@ def _get(
369
412
-------
370
413
retval
371
414
The dictionary represents the 'samp.result' field of the
372
- query, and may be empty.
415
+ query, and may be empty. It will be None if there was an
416
+ error with the call.
417
+
418
+ See Also
419
+ --------
420
+ get
373
421
374
422
"""
375
423
@@ -400,7 +448,9 @@ def _get(
400
448
401
449
return out ["samp.result" ]
402
450
403
- def get (self , command : str , timeout : int | None = None ) -> str | None :
451
+ def get (
452
+ self , command : str , timeout : int | None = None
453
+ ) -> str | np .ndarray | fits .HDUList | None :
404
454
"""Call ds9.get for the given command and arguments.
405
455
406
456
If the call fails then an error message is displayed (to
@@ -426,14 +476,21 @@ def get(self, command: str, timeout: int | None = None) -> str | None:
426
476
The return value, as a string, or None if there was no
427
477
return value.
428
478
479
+ See Also
480
+ --------
481
+ get_raw, set
482
+
429
483
"""
430
484
431
485
# The result is assumed to be one of:
432
486
# - the value field
433
487
# - the url field
434
488
# - otherwise we just return None
435
489
#
436
- result = self ._get (command = command , timeout = timeout )
490
+ result = self .get_raw (command = command , timeout = timeout )
491
+ if result is None :
492
+ return None
493
+
437
494
value = result .get ("value" )
438
495
if value is not None :
439
496
return value
@@ -446,10 +503,78 @@ def get(self, command: str, timeout: int | None = None) -> str | None:
446
503
if self .debug :
447
504
debug (f"DS9 returned data in URL={ url } " )
448
505
449
- return extract_url (url )
506
+ # In order to interpret the data from an array call
507
+ # we need extra information. This would be easier if
508
+ # it was sent in-band (i.e. as part of the response).
509
+ # Are there other cases it is needed?
510
+ #
511
+ if command .startswith ("array" ):
512
+ img = self .get_image_info (timeout = timeout )
513
+ if img is None :
514
+ # Return an empty array
515
+ return np .zeros (0 )
516
+
517
+ else :
518
+ img = None
519
+
520
+ return extract_url (url , img = img )
450
521
451
522
return None
452
523
524
+ def get_image_info (self , timeout : int | None = None ) -> ImgInfo | None :
525
+ """Return information on the current image.
526
+
527
+ Parameters
528
+ ----------
529
+ timeout: optional
530
+ Over-ride the default timeout setting. Use 0 to remove
531
+ any timeout.
532
+
533
+ Returns
534
+ -------
535
+ img
536
+ A structure describing the current image, or None if
537
+ there is none.
538
+
539
+ """
540
+
541
+ # These values should convert, so do not try to improve the
542
+ # error handling.
543
+ #
544
+ def convert (arg : str ) -> int :
545
+ result = self .get_raw (f"fits { arg } " , timeout = timeout )
546
+ if result is None :
547
+ return 0
548
+
549
+ value = result .get ("value" )
550
+ if value is None :
551
+ return 0
552
+
553
+ return int (value )
554
+
555
+ bitpix = convert ("bitpix" )
556
+ nx = convert ("width" )
557
+ ny = convert ("height" )
558
+ nz = convert ("depth" )
559
+
560
+ if nx == 0 or ny == 0 :
561
+ return None
562
+
563
+ dtype = bitpix_to_dtype (bitpix )
564
+ if dtype is None :
565
+ if self .debug :
566
+ debug (f"Unsupported BITPIX: { bitpix } " )
567
+
568
+ return None
569
+
570
+ shape : tuple [int , ...]
571
+ if nz > 1 :
572
+ shape = (nz , ny , nx )
573
+ else :
574
+ shape = (ny , nx )
575
+
576
+ return ImgInfo (dtype , shape )
577
+
453
578
def set (self , command : str , timeout : int | None = None ) -> None :
454
579
"""Call ds9.set for the given command and arguments.
455
580
@@ -466,6 +591,10 @@ def set(self, command: str, timeout: int | None = None) -> None:
466
591
Over-ride the default timeout setting. Use 0 to remove
467
592
any timeout.
468
593
594
+ See Also
595
+ --------
596
+ set
597
+
469
598
"""
470
599
471
600
# Use ecall_and_wait to
@@ -702,41 +831,17 @@ def retrieve_array(
702
831
# Get the data information before creating the temporary file.
703
832
# Do we have to worry about WCS messing around with the units?
704
833
#
705
- # These values should convert, so do not try to improve the
706
- # error handling.
707
- #
708
- def convert (arg ):
709
- val = self .get (f"fits { arg } " )
710
- if val is None :
711
- return 0
712
- return int (val )
713
-
714
- bitpix = convert ("bitpix" )
715
- nx = convert ("width" )
716
- ny = convert ("height" )
717
- nz = convert ("depth" )
718
-
719
- if nx == 0 or ny == 0 :
834
+ img = self .get_image_info (timeout = timeout )
835
+ if img is None :
836
+ # We could return an empty array, but is it worth it?
720
837
error ("DS9 appears to contain no data" )
721
838
return None
722
839
723
- dtype = bitpix_to_dtype (bitpix )
724
- if dtype is None :
725
- error (f"Unsupported BITPIX: { bitpix } " )
726
- return None
727
-
728
- shape : tuple [int , ...]
729
- if nz > 1 :
730
- shape = (nz , ny , nx )
731
- else :
732
- shape = (ny , nx )
733
-
734
840
with tempfile .NamedTemporaryFile (prefix = "ds9samp" , suffix = ".arr" ) as fh :
735
841
cmd = f"export array { fh .name } native"
736
842
self .set (cmd , timeout = timeout )
737
843
738
- fp = np .memmap (fh .name , dtype = dtype , mode = "r" , shape = shape )
739
- out = fp [:]
844
+ out = read_array (fh .name , img )
740
845
741
846
return out
742
847
@@ -837,7 +942,10 @@ def retrieve_fits(
837
942
# The result is assumed to be given by the url field.
838
943
# Any other response is an error.
839
944
#
840
- result = self ._get (command = "fits" , timeout = timeout )
945
+ result = self .get_raw (command = "fits" , timeout = timeout )
946
+ if result is None :
947
+ return None
948
+
841
949
url = result .get ("url" )
842
950
if url is None :
843
951
error ("SAMP call returned unexpected data" )
@@ -916,7 +1024,9 @@ def send_cat(
916
1024
"data mut be a table, not image(s)"
917
1025
) from None
918
1026
919
- data .writeto (fh , overwrite = True , output_verify = "fix+warn" )
1027
+ data .writeto (
1028
+ fh , overwrite = True , output_verify = "fix+warn" , checksum = True
1029
+ )
920
1030
921
1031
cmd = f"catalog import fits { fh .name } "
922
1032
self .set (cmd , timeout = timeout )
0 commit comments