Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ICD_test_stages/file_browser.tests
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ src/test/GET_FILELIST_ROOTPATH_CONCURRENT.test.ts
src/test/FILEINFO_FITS_MULTIHDU.test.ts
src/test/FILEINFO_EXCEPTIONS.test.ts
src/test/OPEN_SWAPPED_IMAGES.test.ts
src/test/OPENFILE_AIPS_BEAM.test.ts
77 changes: 77 additions & 0 deletions docs/source/open_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,80 @@ This test verifies that beam table information is correctly extracted from CASA
- fileInfo.HDUList = [""]

:red-text:`Check 2:` the beam table entries should match identical values as the FITS beam table test at channels 0, 10, 200, 1000, 2000, and 3839

OPENFILE_AIPS_BEAM
~~~~~~~~~~~~~~~~~~

See the `source code <https://github.com/CARTAvis/ICD-RxJS/blob/dev/src/test/OPENFILE_AIPS_BEAM.test.ts>`__.

This test verifies the ``support_aips_beam`` flag of OPEN_FILE and FILE_INFO_REQUEST. Some FITS
images written by AIPS carry no ``BMAJ``/``BMIN``/``BPA`` keywords and no beam table, and record the
restoring beam only in a ``HISTORY`` card. When the flag is set, the backend recovers that beam and
marks it as derived from the history headers; when it is not set, no restoring beam is reported at
all. In the frontend the flag is driven by the *AIPS beam support* compatibility preference.

.. note::

The fixture ``aips_history_beam.fits`` carries **two** history beams, an earlier
4" x 3" at 10 deg and a later 2" x 1.5" at 30 deg. casacore picks up the *first* one; the backend
must strip it and report the *last* one instead. The two axes differ so that a major/minor
mix-up is detectable.

1. Frontend sends: **OPEN_FILE** (``OpenFile``) with the flag off

.. code-block:: protobuf

directory = "set_QA"
file = "aips_history_beam.fits"
hdu = "0"
file_id = 0
render_mode = RASTER
support_aips_beam = false

2. Backend returns: **OPEN_FILE_ACK** (``OpenFileAck``) and **REGION_HISTOGRAM_DATA**

:red-text:`Check 1:` the OPEN_FILE_ACK should satisfy:

- OPEN_FILE_ACK.success = True
- file_info_extended.computed_entries contains no "Restoring beam" entry
- file_info_extended.header_entries contains no BMAJ, BMIN or BPA entry
- beam_table is empty

3. Frontend sends: **OPEN_FILE** (``OpenFile``) for the same image with ``support_aips_beam = true``

4. Backend returns: **OPEN_FILE_ACK** (``OpenFileAck``) and **REGION_HISTOGRAM_DATA**

:red-text:`Check 2:` the computed entry should satisfy:

- "Restoring beam" = ``2" X 1.5", 30 deg (extracted from HISTORY)``

That is the last history beam, not the first one casacore parsed.

:red-text:`Check 3:` the header entries should satisfy:

- BMAJ, BMIN and BPA are all present
- each has entry_type = FLOAT and comment = "extracted from HISTORY"
- BPA = 30 deg
- the two axis values, compared as an unordered pair, are 2" and 1.5" expressed in degrees

5. Frontend sends: **FILE_INFO_REQUEST** (``FileInfoRequest``) for the same image with
``support_aips_beam = true``

6. Backend returns: **FILE_INFO_RESPONSE** (``FileInfoResponse``)

:red-text:`Check 4:` the FILE_INFO_RESPONSE should satisfy:

- success = True
- file_info_extended["0"].computed_entries "Restoring beam" = ``2" X 1.5", 30 deg (extracted from HISTORY)``

.. note::

Check 3 compares the two axis values as an unordered pair on purpose. The backend currently
assigns the major axis to ``BMIN`` and the minor axis to ``BMAJ`` in the header entries, although
the computed "Restoring beam" string above them is correct. A stricter check which pins each name
to its own axis is present in the test but skipped until that is fixed.

Two further cases are skipped for the same reason. The gzipped copy of the fixture reports no
history beam at all through the compressed FITS path, and a FILE_INFO_REQUEST with the flag off
followed by one with the flag on returns the stale, beam-less result for the same file within a
session, so only the flag-on case is exercised here.
100 changes: 100 additions & 0 deletions scripts/make_aips_history_beam_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Generate the AIPS-history-beam FITS fixture for ICD issue #33.

The image carries no BMAJ/BMIN/BPA keywords and no BEAMS table; the beam exists
only in HISTORY cards. Two cards are written: an earlier decoy and the real one,
so the test also pins down that the backend keeps the *last* history beam.

The card format matters. casacore recovers a history beam in
FITSImage::crackHeader -> ImageInfo::getRestoringBeam(LoggerHolder&), which
splits the line on whitespace and requires BMAJ, BMIN and BPA tokens each
followed by a value token in degrees. Without that, ImageInfo::hasBeam() stays
false and carta-backend's FitsLoader::ResetImageBeam never fires. The
`RESTOR Beam = 2.0 x 1.5 arcsec, pa = 30 degrees` form that
ParseHistoryBeamHeader also accepts is NOT recognised by casacore, so it cannot
be used for an uncompressed FITS fixture.
"""

import gzip
import os
import shutil
import sys

import numpy as np
from astropy.io import fits

USAGE = 'usage: {} <set_QA directory> <set_compressed_fits directory>'.format(os.path.basename(sys.argv[0]))

if len(sys.argv) != 3:
sys.exit(USAGE)

QA_DIR = sys.argv[1]
COMPRESSED_DIR = sys.argv[2]

for directory in (QA_DIR, COMPRESSED_DIR):
if not os.path.isdir(directory):
sys.exit('{}\nnot a directory: {}'.format(USAGE, directory))

BASENAME = 'aips_history_beam.fits'

# Beam the backend must report: 2.0" x 1.5", pa 30 deg. Deliberately
# non-circular so that a BMAJ/BMIN mix-up is detectable.
BMAJ_DEG = 2.0 / 3600.0
BMIN_DEG = 1.5 / 3600.0
BPA_DEG = 30.0

# An earlier, different beam. casacore picks up the *first* history beam; the
# backend must strip it and use the *last* one instead.
DECOY_BMAJ_DEG = 4.0 / 3600.0
DECOY_BMIN_DEG = 3.0 / 3600.0
DECOY_BPA_DEG = 10.0

CARD = 'AIPS CLEAN BMAJ= {:.6E} BMIN= {:.6E} BPA= {:.2f}'
HISTORY_CARDS = [
CARD.format(DECOY_BMAJ_DEG, DECOY_BMIN_DEG, DECOY_BPA_DEG),
CARD.format(BMAJ_DEG, BMIN_DEG, BPA_DEG),
]


def make_data(nx=128, ny=128):
y, x = np.mgrid[0:ny, 0:nx]
g = 5.0 * np.exp(-(((x - 64.0) / 12.0) ** 2 + ((y - 64.0) / 8.0) ** 2) / 2.0)
rng = np.random.default_rng(20230803)
return (g + rng.normal(0.0, 0.1, (ny, nx))).astype(np.float32)


hdu = fits.PrimaryHDU(make_data())
h = hdu.header
h['BUNIT'] = ('Jy/beam', 'Brightness unit')
h['CTYPE1'] = 'RA---SIN'
h['CRVAL1'] = 275.0
h['CDELT1'] = -0.0002777777777778
h['CRPIX1'] = 64.0
h['CUNIT1'] = 'deg'
h['CTYPE2'] = 'DEC--SIN'
h['CRVAL2'] = -16.0
h['CDELT2'] = 0.0002777777777778
h['CRPIX2'] = 64.0
h['CUNIT2'] = 'deg'
h['RADESYS'] = 'FK5'
h['EQUINOX'] = 2000.0
h['TELESCOP'] = 'ALMA'
h['OBJECT'] = 'AIPS_HISTORY_BEAM_TEST'
for card in HISTORY_CARDS:
h.add_history(card)

fits_path = os.path.join(QA_DIR, BASENAME)
fits.HDUList([hdu]).writeto(fits_path, overwrite=True)

gz_path = os.path.join(COMPRESSED_DIR, BASENAME + '.gz')
with open(fits_path, 'rb') as src, gzip.open(gz_path, 'wb') as dst:
shutil.copyfileobj(src, dst)

for path in (fits_path, gz_path):
print('{} ({} bytes)'.format(path, os.path.getsize(path)))
header = fits.getheader(fits_path)
for card in header['HISTORY']:
print(' HISTORY', card)
for key in ('BMAJ', 'BMIN', 'BPA'):
assert key not in header, 'fixture must not carry a %s keyword' % key
assert len(fits.open(fits_path)) == 1, 'fixture must have no extensions'
print(' no BMAJ/BMIN/BPA keywords, no extensions: OK')
7 changes: 4 additions & 3 deletions src/test/MessageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,12 @@ export class MessageController {
async getFileInfo(
directory: string | null | undefined,
file: string | null | undefined,
hdu: string | null | undefined
hdu: string | null | undefined,
supportAipsBeam: boolean = false
): Promise<CARTA.IFileInfoResponse> {
if (this.connectionStatus !== ConnectionStatus.ACTIVE) {
throw new Error('Not connected');
} else {
const supportAipsBeam = false;
const message = CARTA.FileInfoRequest.create({
directory,
file,
Expand Down Expand Up @@ -702,6 +702,7 @@ export class MessageController {
let hdu = input.hdu;
let fileId = input.fileId;
let imageArithmetic = input.lelExpr;
let supportAipsBeam = input.supportAipsBeam ?? false;
if (this.connectionStatus !== ConnectionStatus.ACTIVE) {
throw new Error('Not connected');
} else {
Expand All @@ -712,7 +713,7 @@ export class MessageController {
fileId,
lelExpr: imageArithmetic,
renderMode: CARTA.RenderMode.RASTER,
supportAipsBeam: false,
supportAipsBeam,
});
const requestId = this.eventCounter;
this.logEvent(CARTA.EventType.OPEN_FILE, requestId, message, false);
Expand Down
Loading