Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4cabe3e
Notes
confluence Apr 8, 2023
5199c1d
Merge remote-tracking branch 'origin/dev' into confluence/zoom_centre…
confluence May 13, 2023
c534dad
First pass of implementation. Needs testing.
confluence May 16, 2023
6bd7dca
Some fixes and revisions; added functions for setting and clearing cu…
confluence May 17, 2023
ffe2837
improved property mocking
confluence May 17, 2023
f7d4a3b
some basic set_center tests
confluence May 17, 2023
a3dddfe
more consistent type coercion
confluence May 17, 2023
562869d
fixed bug; completed set_center tests
confluence May 17, 2023
0b47f84
More tests; fixed some bugs
confluence May 17, 2023
e1f5a39
Added and refined tests
confluence May 17, 2023
9d0fd14
Added some session tests
confluence May 17, 2023
4e970ff
The actual correct CoordinateSystem values
confluence May 17, 2023
3bf31f0
Number format function; more tests
confluence May 17, 2023
d1ca655
Added validation tests and corrected logic for hms/dms with letter se…
confluence May 18, 2023
9f55cca
Fixed some documentation issues
confluence May 18, 2023
3ce9b20
bumped version
confluence May 18, 2023
c2ffbe9
Arguments to non-WCS zoom and center actions must be numbers
confluence May 18, 2023
a66d241
Fixed function names
confluence May 18, 2023
a795416
number_format should wrap return values in NumberFormat enums
confluence May 19, 2023
2433ae2
Refactored internal implementation; fixed tests
confluence May 27, 2023
74a1e86
add extra angular size unit aliases
confluence May 31, 2023
d03077f
Added support for sub-arcsecond units.
confluence May 31, 2023
6c3a6e6
partial refactoring in preparation for range checks
confluence Jun 1, 2023
cc3d67f
Rename object tree
confluence Jun 1, 2023
cd337f4
rename angular size class
confluence Jun 1, 2023
ecb80ab
Renamed and refactored pixel value class; fixed tests
confluence Jun 1, 2023
094df0d
Simplified AngularSize and WorldCoordinate usage
confluence Jun 1, 2023
c9d5da6
Refactored angular size class
confluence Jun 1, 2023
195a6b4
Added support for unicode arcmin and arcsec symbols.
confluence Jun 1, 2023
fd7924d
Refactoring and test rewrites
confluence Jun 5, 2023
d7b56c2
Finish refactoring tests
confluence Jun 5, 2023
0cb42a0
Restored previously removed support for empty H, M and/or S in sexage…
confluence Jun 5, 2023
db25e2e
Refactored angular size classes so that child classes work correctly …
confluence Jun 5, 2023
8f240bf
Finished refactoring angular size tests
confluence Jun 5, 2023
09b9ef9
Shift permitted range for set_center pixel values to allow for offset…
confluence Jun 8, 2023
6bf54f8
Added hint to error message about missing WCS.
confluence Jun 8, 2023
57988b2
Combined zoom_to_size_* functions into one function with axis parameter
confluence Jun 8, 2023
7666c36
Remove range validation entirely for set_center with image coordinate…
confluence Jun 8, 2023
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__
build
carta.egg-info
.DS_Store
.coverage
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,26 @@ To create a new frontend session which is controlled by the wrapper instead of c
Some example usage of the client as a module is shown in the [documentation](https://carta-python.readthedocs.io).

The client is under rapid development and this API should be considered experimental and subject to change depending on feedback. The current overall design principle considers session and image objects to be lightweight conduits to the frontend. They store as little state as possible and are not guaranteed to be unique or valid connections -- it is the caller's responsibility to manage the objects and store retrieved data as required.

Unit tests
----------

Running the unit tests requires the installation of additional dependencies:
```
pip install pytest
pip install pytest-mock
pip install pytest-cov
```

To run all the unit tests (from the root directory of the repository):
```
pytest tests # concise
pytest -v tests # more verbose
```

To view the code coverage:
```
pytest --cov=carta tests/
```

See the [`pytest` documentation](https://docs.pytest.org/) for more usage options.
17 changes: 15 additions & 2 deletions carta/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""This module provides a collection of enums corresponding to various enumerated types and other literal lists of options defined in the frontend. The members of these enums should be used in place of literal strings and numbers to represent these values; for example: ``Colormap.VIRIDIS`` rather than ``"viridis"``. """

from enum import Enum, IntEnum
from enum import Enum, IntEnum, auto


# TODO make sure the __str__ is right for all the string values
Expand All @@ -21,10 +21,23 @@ class ArithmeticExpression(str, Enum):
Scaling.__doc__ = """Colormap scaling types."""


CoordinateSystem = Enum('CoordinateSystem', {c.upper(): c for c in ("Auto", "Ecliptic", "FK4", "FK5", "Galactic", "ICRS")}, type=str)
CoordinateSystem = Enum('CoordinateSystem', {c: c for c in ("AUTO", "ECLIPTIC", "FK4", "FK5", "GALACTIC", "ICRS")}, type=str)
CoordinateSystem.__doc__ = """Coordinate systems."""


class NumberFormat(str, Enum):
"""Number formats."""
DEGREES = "d"
HMS = "hms"
DMS = "dms"


class SpatialAxis(str, Enum):
"""Spatial axes."""
X = "x"
Y = "y"


class LabelType(str, Enum):
"""Label types."""
INTERIOR = "Interior"
Expand Down
90 changes: 79 additions & 11 deletions carta/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"""
import posixpath

from .constants import Colormap, Scaling, SmoothingMode, ContourDashMode, Polarization
from .util import Macro, cached
from .validation import validate, Number, Color, Constant, Boolean, NoneOr, IterableOf, Evaluate, Attr, Attrs, OneOf
from .constants import Colormap, Scaling, SmoothingMode, ContourDashMode, Polarization, CoordinateSystem, SpatialAxis
from .util import Macro, cached, PixelValue, AngularSize, WorldCoordinate
from .validation import validate, Number, Color, Constant, Boolean, NoneOr, IterableOf, Evaluate, Attr, Attrs, OneOf, Size, Coordinate


class Image:
Expand Down Expand Up @@ -261,7 +261,7 @@ def shape(self):
@property
@cached
def width(self):
"""The width of the image.
"""The width of the image in pixels.

Returns
-------
Expand All @@ -273,7 +273,7 @@ def width(self):
@property
@cached
def height(self):
"""The height of the image.
"""The height of the image in pixels.

Returns
-------
Expand Down Expand Up @@ -426,23 +426,91 @@ def set_polarization(self, polarization, recursive=True):

self.call_action("setChannels", self.macro("", "requiredChannel"), polarization, recursive)

@validate(Number(), Number())
def set_center(self, x, y):
"""Set the center position.
@property
@cached
def valid_wcs(self):
"""Whether the image contains valid WCS information.

Returns
-------
boolean
Whether the image has WCS information.
"""
return self.get_value("validWcs")

TODO: what are the units?
@validate(Coordinate(), Coordinate(), NoneOr(Constant(CoordinateSystem)))
def set_center(self, x, y, system=None):
"""Set the center position, in image or world coordinates. Optionally change the session-wide coordinate system.

Coordinates must either both be image coordinates or match the current number formats. Numbers and numeric strings with no units are interpreted as degrees.

Parameters
----------
x : {0}
The X position.
y : {1}
The Y position.
system : {2}
The coordinate system. If this parameter is provided, the coordinate system will be changed session-wide before the X and Y coordinates are parsed.

Raises
------
ValueError
If a mix of image and world coordinates is provided, if world coordinates are provided and the image has no valid WCS information, or if world coordinates do not match the session-wide number format.
"""
self.call_action("setCenter", x, y)
if system is not None:
self.session.set_coordinate_system(system)

x_is_pixel = PixelValue.valid(str(x))
y_is_pixel = PixelValue.valid(str(y))

if x_is_pixel and y_is_pixel:
# Image coordinates
x_value = PixelValue.as_float(str(x))
y_value = PixelValue.as_float(str(y))
self.call_action("setCenter", x_value, y_value)

elif x_is_pixel or y_is_pixel:
raise ValueError("Cannot mix image and world coordinates.")

else:
if not self.valid_wcs:
raise ValueError("Cannot parse world coordinates. This image does not contain valid WCS information. Please use image coordinates (in pixels) instead.")

number_format_x, number_format_y, _ = self.session.number_format()
x_value = WorldCoordinate.with_format(number_format_x).from_string(str(x), SpatialAxis.X)
y_value = WorldCoordinate.with_format(number_format_y).from_string(str(y), SpatialAxis.Y)
self.call_action("setCenterWcs", str(x_value), str(y_value))

@validate(Size(), Constant(SpatialAxis))
def zoom_to_size(self, size, axis):
"""Zoom to the given size along the specified axis.

Numbers and numeric strings with no units are interpreted as arcseconds.

Parameters
----------
size : {0}
The size to zoom to.
axis : {1}
The spatial axis to use.

Raises
------
ValueError
If world coordinates are provided and the image has no valid WCS information.
"""
size = str(size)

if PixelValue.valid(size):
self.call_action(f"zoomToSize{axis.upper()}", PixelValue.as_float(size))
else:
if not self.valid_wcs:
raise ValueError("Cannot parse angular size. This image does not contain valid WCS information. Please use a pixel size instead.")
self.call_action(f"zoomToSize{axis.upper()}Wcs", str(AngularSize.from_string(size)))

@validate(Number(), Boolean())
def set_zoom(self, zoom, absolute=True):
def set_zoom_level(self, zoom, absolute=True):
"""Set the zoom level.

TODO: explain this more rigorously.
Expand Down
50 changes: 49 additions & 1 deletion carta/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import base64

from .image import Image
from .constants import CoordinateSystem, LabelType, BeamType, PaletteColor, Overlay, PanelMode, GridMode, ArithmeticExpression
from .constants import CoordinateSystem, LabelType, BeamType, PaletteColor, Overlay, PanelMode, GridMode, ArithmeticExpression, NumberFormat
from .backend import Backend
from .protocol import Protocol
from .util import logger, Macro, split_action_path, CartaBadID, CartaBadSession, CartaBadUrl
Expand Down Expand Up @@ -495,6 +495,16 @@ def set_coordinate_system(self, system=CoordinateSystem.AUTO):
"""
self.call_action("overlayStore.global.setSystem", system)

def coordinate_system(self):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we make it clear between native coordinate system (as defined in header) and "rendered" coordinate system?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly unambiguous because it's a function on the session, not the image (and will probably soon move to a separate image viewer class). But we can explain this in more detail in the docstring.

"""Get the coordinate system.

Returns
----------
:obj:`carta.constants.CoordinateSystem`
The coordinate system.
"""
return CoordinateSystem(self.get_value("overlayStore.global.system"))

@validate(Constant(LabelType))
def set_label_type(self, label_type):
"""Set the label type.
Expand Down Expand Up @@ -554,6 +564,44 @@ def set_font(self, component, font=None, font_size=None):
if font_size is not None:
self.call_action(f"overlayStore.{component}.setFontSize", font_size)

@validate(NoneOr(Constant(NumberFormat)), NoneOr(Constant(NumberFormat)))
def set_custom_number_format(self, x_format=None, y_format=None):
"""Set a custom X and Y number format.

Parameters
----------
x_format : {0}
The X format. If this is unset, the last custom X format to be set will be restored.
x_format : {1}
The Y format. If this is unset, the last custom Y format to be set will be restored.
"""
if x_format is not None:
self.call_overlay_action(Overlay.NUMBERS, "setFormatX", x_format)
if y_format is not None:
self.call_overlay_action(Overlay.NUMBERS, "setFormatY", y_format)
self.call_overlay_action(Overlay.NUMBERS, "setCustomFormat", True)

def clear_custom_number_format(self):
"""Disable the custom X and Y number format."""
self.call_overlay_action(Overlay.NUMBERS, "setCustomFormat", False)

def number_format(self):
"""Return the current X and Y number formats, and whether they are a custom setting.

If the image has no WCS information, both the X and Y formats will be ``None``.

If a custom number format is not set, the format is derived from the coordinate system.

Returns
-------
tuple (a member of :obj:`carta.constants.NumberFormat` or ``None``, a member of :obj:`carta.constants.NumberFormat` or ``None``, boolean)
A tuple containing the X format, the Y format, and whether a custom format is set.
"""
number_format_x = self.get_overlay_value(Overlay.NUMBERS, "formatTypeX")
number_format_y = self.get_overlay_value(Overlay.NUMBERS, "formatTypeY")
custom_format = self.get_overlay_value(Overlay.NUMBERS, "customFormat")
return NumberFormat(number_format_x), NumberFormat(number_format_y), custom_format

@validate(NoneOr(Constant(BeamType)), NoneOr(Number()), NoneOr(Number()), NoneOr(Number()))
def set_beam(self, beam_type=None, width=None, shift_x=None, shift_y=None):
"""Set the beam properties.
Expand Down
Loading