diff --git a/chandra_aca/drift.py b/chandra_aca/drift.py index 0f6b3bac..3940503c 100644 --- a/chandra_aca/drift.py +++ b/chandra_aca/drift.py @@ -17,12 +17,16 @@ from astropy.table import Table from astropy.utils.data import download_file from Chandra.Time import DateTime -from ska_helpers.paths import aca_drift_model_path +from ska_helpers import chandra_models from ska_helpers.utils import LazyDict def load_drift_pars(): - pars = json.loads(aca_drift_model_path().read_text()) + pars_txt, info = chandra_models.get_data( + Path("chandra_models") / "aca_drift" / "aca_drift_model.json" + ) + pars = json.loads(pars_txt) + pars["info"] = info return pars diff --git a/chandra_aca/star_probs.py b/chandra_aca/star_probs.py index 2509946e..6e33c2cc 100644 --- a/chandra_aca/star_probs.py +++ b/chandra_aca/star_probs.py @@ -2,23 +2,39 @@ """ Functions related to probabilities for star acquisition and guide tracking. -Current default acquisition probability model: grid-floor-2020-02 +Current default acquisition probability model: ``grid-*`` (latest grid model) This can +be changed by setting the module configuration value ``conf.default_model`` to an +available model or model glob in the ``chandra_models`` repository. + +The grid-local-quadratic-2023-05 model definition and fit values based on: + +- https://github.com/sot/aca_stats/blob/master/fit_acq_model-2023-05-local-quadratic.ipynb +- https://github.com/sot/aca_stats/blob/master/validate-2023-05-local-quadratic.ipynb +- SS&AWG review 2023-02-01 The grid-floor-2020-02 model definition and fit values based on: - https://github.com/sot/aca_stats/blob/master/fit_acq_model-2020-02-binned-poly-binom-floor.ipynb -SSAWG review: 2020-01-29 +- https://github.com/sot/aca_stats/blob/master/fit_acq_model-2020-02-binned-poly-binom-floor.ipynb +- SSAWG review: 2020-01-29 """ import functools +import os +import re import warnings +from pathlib import Path +from typing import Optional import numpy as np import scipy.stats +from astropy import config +from astropy.io import fits from Chandra.Time import DateTime +from cxotime import CxoTimeLike from numba import jit +from scipy.interpolate import RegularGridInterpolator from scipy.optimize import bisect, brentq -from ska_helpers.paths import aca_acq_prob_models_path +from ska_helpers import chandra_models from chandra_aca.transform import ( broadcast_arrays, @@ -26,16 +42,12 @@ snr_mag_for_t_ccd, ) -# Default acquisition probability model -DEFAULT_MODEL = "grid-floor-2020-02" - # Cache of cubic spline functions. Eval'd only on the first time. SPLINE_FUNCS = {} -# Value (N100) used for fitting +# Value (N100) used for fitting the `sota` model WARM_THRESHOLD = 100 - # Min and max star acquisition probabilities, regardless of model predictions MIN_ACQ_PROB = 1e-6 MAX_ACQ_PROB = 0.985 @@ -43,6 +55,24 @@ MULT_STARS_ENABLED = False +class ConfigItem(config.ConfigItem): + rootname = "chandra_aca.star_probs" + + +class Conf(config.ConfigNamespace): + default_model = ConfigItem( + defaultvalue="grid-*", + description=( + "Default acquisition probability model. This can be a specific model name " + "or a glob pattern (e.g. ``'grid-*'`` for latest grid model)." + ), + ) + + +# Create a configuration instance for the user +conf = Conf() + + def get_box_delta(halfwidth): """ Transform from halfwidth (arcsec) to the box_delta value which gets added @@ -220,37 +250,37 @@ def prob_n_acq(star_probs): def acq_success_prob( - date=None, - t_ccd=-10.0, - mag=10.0, - color=0.6, - spoiler=False, - halfwidth=120, - model=None, -): - """ + date: CxoTimeLike = None, + t_ccd: float | np.ndarray[float] = -10.0, + mag: float | np.ndarray[float] = 10.0, + color: float | np.ndarray[float] = 0.6, + spoiler: bool | np.ndarray[bool] = False, + halfwidth: int | np.ndarray[int] = 120, + model: Optional[str] = None, +) -> float | np.ndarray[float]: + r""" Return probability of acquisition success for given date, temperature, star properties and search box size. Any of the inputs can be scalars or arrays, with the output being the result of the broadcasted dimension of the inputs. - The default probability model is defined by ``DEFAULT_MODEL`` in this module. + The default probability model is defined by ``conf.default_model`` in this module. Allowed values for the ``model`` name are 'sota', 'spline', or a grid model specified by 'grid--' (e.g. 'grid-floor-2018-11'). :param date: Date(s) (scalar or np.ndarray, default=NOW) - :param t_ccd: CD temperature(s) (degC, scalar or np.ndarray, default=-10C) + :param t_ccd: CCD temperature(s) (degC, scalar or np.ndarray, default=-10C) :param mag: Star magnitude(s) (scalar or np.ndarray, default=10.0) :param color: Star color(s) (scalar or np.ndarray, default=0.6) :param spoiler: Star spoiled (boolean or np.ndarray, default=False) :param halfwidth: Search box halfwidth (arcsec, default=120) - :param model: probability model name: 'sota' | 'spline' | 'grid-*' + :param model: probability model name: 'sota' | 'spline' | 'grid-\*' :returns: Acquisition success probability(s) """ if model is None: - model = DEFAULT_MODEL + model = conf.default_model date = DateTime(date).secs is_scalar, dates, t_ccds, mags, colors, spoilers, halfwidths = broadcast_arrays( @@ -331,7 +361,8 @@ def get_grid_axis_values(hdr, axis): """Get grid model axis values from FITS header. This is an irregularly-spaced grid if ``hdr`` has ``{axis}_0`` .. ``{axis}_``. - Otherwise it is a regularly-spaced grid: + Otherwise it is a regularly-spaced grid:: + linspace(hdr[f"{axis}_lo"], hdr[f"{axis}_hi"], n_vals) :param hdr: FITS header (dict-like) @@ -349,19 +380,42 @@ def get_grid_axis_values(hdr, axis): @functools.lru_cache -def get_grid_func_model(model): - from astropy.io import fits - from scipy.interpolate import RegularGridInterpolator - - # Read the model file and put into local vars - filepath = aca_acq_prob_models_path() / (model + ".fits.gz") - if not filepath.exists(): - raise IOError(f"model file {filepath} does not exist") - - hdus = fits.open(filepath) - hdu0 = hdus[0] - probit_p_fail_no_1p5 = hdus[1].data - probit_p_fail_1p5 = hdus[2].data +def get_grid_func_model( + model: Optional[str] = None, + version: Optional[str] = None, + repo_path: Optional[str | Path] = None, +): + """Get grid model from the ``model`` name. + + This reads the model data from the FITS file in the ``chandra_models`` repository. + The ``model`` name can be a glob pattern like ``grid-*``, which will match the + grid model with the most recent date. If not provided the ``DEFAULT_MODEL`` is used. + + The return value is a dict with necessary data to use the model:: + + "filename": filepath (Path), + "func_no_1p5": RegularGridInterpolator for stars with color != 1.5 (common) + "func_1p5": RegularGridInterpolator for stars with color = 1.5 (less common) + "mag_lo": lower bound of mag axis + "mag_hi": upper bound of mag axis + "t_ccd_lo": lower bound of t_ccd axis + "t_ccd_hi": upper bound of t_ccd axis + "halfw_lo": lower bound of halfw axis + "halfw_hi": upper bound of halfw axis + + :param model: Model name (optional) + :param version: Version / tag / branch of ``chandra_models`` repository (optional) + :param repo_path: Path to ``chandra_models`` repository (optional) + :returns: dict of model data + """ + data, info = chandra_models.get_data( + file_path="chandra_models/aca_acq_prob", + read_func=_read_grid_func_model, + read_func_kwargs={"model_name": model}, + version=version, + repo_path=repo_path, + ) + hdu0, probit_p_fail_no_1p5, probit_p_fail_1p5 = data hdr = hdu0.header grid_mags = get_grid_axis_values(hdr, "mag") @@ -391,7 +445,7 @@ def get_grid_func_model(model): halfw_hi = hdr["halfw_hi"] out = { - "filename": filepath, + "filename": info["data_file_path"], "func_no_1p5": func_no_1p5, "func_1p5": func_1p5, "mag_lo": mag_lo, @@ -400,10 +454,43 @@ def get_grid_func_model(model): "t_ccd_hi": t_ccd_hi, "halfw_lo": halfw_lo, "halfw_hi": halfw_hi, + "info": info, } return out +def _read_grid_func_model(models_dir: Path, model_name: Optional[str] = None): + """Read the model file and put into local vars""" + if model_name is None: + model_name = conf.default_model + filepaths = models_dir.glob(model_name + ".fits.gz") + filepaths = list(filepaths) + filepaths = sorted(filepaths, key=_get_date_from_model_filename) + if len(filepaths) == 0: + raise IOError(f"no model files found for {model_name}") + + filepath = filepaths[-1] + if not filepath.exists(): + raise IOError(f"model file {filepath} does not exist") + + with fits.open(filepath) as hdus: + hdu0 = hdus[0] + probit_p_fail_no_1p5 = hdus[1].data + probit_p_fail_1p5 = hdus[2].data + + # Pack the output data as a tuple + data = (hdu0, probit_p_fail_no_1p5, probit_p_fail_1p5) + return data, filepath + + +def _get_date_from_model_filename(filepath: Path): + match = re.search(r"(\d{4}-\d{2})\.fits\.gz$", filepath.name) + if match: + return match.group(1) + else: + raise ValueError(f"could not parse date from {filepath}") + + def grid_model_acq_prob( mag=10.0, t_ccd=-12.0, color=0.6, halfwidth=120, probit=False, model=None ): @@ -425,7 +512,11 @@ def grid_model_acq_prob( """ # Get the grid model function and model parameters from a FITS file. This function # call is cached. - gfm = get_grid_func_model(model) + gfm = get_grid_func_model( + model, + version=os.environ.get("CHANDRA_MODELS_DEFAULT_VERSION"), + repo_path=os.environ.get("CHANDRA_MODELS_REPO_PATH"), + ) func_no_1p5 = gfm["func_no_1p5"] func_1p5 = gfm["func_1p5"] @@ -481,13 +572,15 @@ def spline_model_acq_prob( success for a star with specified mag, t_ccd, color, and search box halfwidth. The model definition and fit values based on: + - https://github.com/sot/aca_stats/blob/master/fit_acq_prob_model-2018-04-poly-spline-tccd.ipynb See also: + - Description of the motivation and initial model development. - https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2018x04x11 + https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2018x04x11 - Final review and approval. - https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2018x04x18 + https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2018x04x18 :param mag: ACA magnitude (float or np.ndarray) :param t_ccd: CCD temperature (degC, float or ndarray) @@ -787,7 +880,7 @@ def guide_count(mags, t_ccd, count_9th=False): One feature is the slight incline in the guide_count curve from 1.0005 at mag=6.0 to 1.0 at mag=10.0. This does not show up in standard outputs - of guide_counts to two decimal places (8 * 0.0005 = 0.004), but helps with + of guide_counts to two decimal places (``8 * 0.0005 = 0.004``), but helps with minimization. :param mags: float, array diff --git a/chandra_aca/tests/data/chandra_models/aca_drift/aca_drift_model.json b/chandra_aca/tests/data/chandra_models/aca_drift/aca_drift_model.json deleted file mode 100644 index 76e23c7e..00000000 --- a/chandra_aca/tests/data/chandra_models/aca_drift/aca_drift_model.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "dy": { - "year0": 2016.0, - "scale": 2.1467, - "offset": -6.012, - "trend": -1.108, - "jumps": [ - [ - "2015:006", - -4.6 - ], - [ - "2015:265", - -4.669 - ], - [ - "2016:064", - -1.793 - ], - [ - "2017:066", - -1.725 - ], - [ - "2018:285", - -12.505 - ], - [ - "2022:294", - -7.966 - ] - ] - }, - "dz": { - "year0": 2016.0, - "scale": 1.004, - "offset": -15.963, - "trend": -0.159, - "jumps": [ - [ - "2015:006", - -2.109 - ], - [ - "2015:265", - -0.368 - ], - [ - "2016:064", - -0.902 - ], - [ - "2017:066", - -0.856 - ], - [ - "2018:285", - -6.056 - ], - [ - "2022:294", - -1.5133 - ] - ] - } -} \ No newline at end of file diff --git a/chandra_aca/tests/test_drift.py b/chandra_aca/tests/test_drift.py index 3c0b551e..1f865b36 100644 --- a/chandra_aca/tests/test_drift.py +++ b/chandra_aca/tests/test_drift.py @@ -4,7 +4,6 @@ import astropy.table as tbl import numpy as np import pytest -import ska_helpers.paths from chandra_aca import drift @@ -54,12 +53,10 @@ def test_get_aca_offsets_legacy(): @pytest.mark.parametrize("kwargs", kwargs_list) -@pytest.mark.parametrize("env_override", [None, str(Path(__file__).parent / "data")]) -def test_get_aca_offsets(kwargs, env_override, monkeypatch): - """Regression test that ACA offsets match the original flight values to expected - precision.""" - if env_override: - monkeypatch.setenv(ska_helpers.paths.CHANDRA_MODELS_ROOT_ENV_VAR, env_override) +def test_get_aca_offsets(kwargs, monkeypatch): + """Regression test that ACA offsets match the original flight values from 2022-11 + analysis to expected precision.""" + monkeypatch.setenv("CHANDRA_MODELS_DEFAULT_VERSION", "3.48") kwargs = kwargs.copy() aca_offset_y = kwargs.pop("aca_offset_y") aca_offset_z = kwargs.pop("aca_offset_z") diff --git a/chandra_aca/tests/test_star_probs.py b/chandra_aca/tests/test_star_probs.py index b1891897..950ef81d 100644 --- a/chandra_aca/tests/test_star_probs.py +++ b/chandra_aca/tests/test_star_probs.py @@ -9,9 +9,9 @@ from ska_helpers.paths import aca_acq_prob_models_path from chandra_aca.star_probs import ( - DEFAULT_MODEL, acq_success_prob, binom_ppf, + conf, grid_model_acq_prob, guide_count, mag_for_p_acq, @@ -564,12 +564,7 @@ def test_grid_floor_2020_02(): def test_default_acq_prob_model(): - """Ensure that the default model matches expectation. This test needs to be - updated whenever a new (default) flight model is released. - """ - # UPDATE with the expectation for the current flight default model - assert DEFAULT_MODEL == "grid-floor-2020-02" - + """Ensure that acq_success_prob correctly selects the default model.""" mags = [9, 9.5, 10.5] t_ccds = [-10, -5] halfws = [60, 120, 160] @@ -577,7 +572,7 @@ def test_default_acq_prob_model(): # Specifically call out the default model name probs1 = grid_model_acq_prob( - mag, t_ccd, halfwidth=halfw, color=1.0, model=DEFAULT_MODEL + mag, t_ccd, halfwidth=halfw, color=1.0, model=conf.default_model ) # Use the high-level (model-independent) API with defaults probs2 = acq_success_prob(t_ccd=t_ccd, mag=mag, halfwidth=halfw) diff --git a/chandra_aca/transform.py b/chandra_aca/transform.py index 24a121e0..ec72a346 100644 --- a/chandra_aca/transform.py +++ b/chandra_aca/transform.py @@ -93,14 +93,14 @@ def broadcast_arrays(*args): - """ - Broadcast *args inputs to same shape and return an ``is_scalar`` flag and + r""" + Broadcast ``*args`` inputs to same shape and return an ``is_scalar`` flag and the broadcasted version of inputs. This lets intermediate code work on arrays that are guaranteed to be the same shape and at least a 1-d array, but reshape the output at the end. :param args: tuple of scalar / array inputs - :returns: [is_scalar, *flat_args] + :returns: [is_scalar, \*flat_args] """ is_scalar = all(np.array(arg).ndim == 0 for arg in args) @@ -110,13 +110,13 @@ def broadcast_arrays(*args): def broadcast_arrays_flatten(*args): - """Broadcast *args inputs to same shape and then return that shape and the + r"""Broadcast ``*args`` inputs to same shape and then return that shape and the flattened view of all the inputs. This lets intermediate code work on all scalars or all arrays that are the same-length 1-d array and then reshape the output at the end (if necessary). :param args: tuple of scalar / array inputs - :returns: [shape, *flat_args] + :returns: [shape, \*flat_args] """ is_scalar, *outs = broadcast_arrays(*args) diff --git a/docs/aca_image.rst b/docs/aca_image.rst new file mode 100644 index 00000000..0d352685 --- /dev/null +++ b/docs/aca_image.rst @@ -0,0 +1,246 @@ +chandra_aca.aca_image +====================== + +The `aca_image` module contains classes and utilities related to ACA readout +images. This includes the `ACAImage class`_ for manipulating images in +ACA coordinates, a first moment centroiding routine, and a library for +generating synthetic ACA images that have a high-fidelity point spread function +(PSF). + +.. automodule:: chandra_aca.aca_image + :members: + +ACAImage class +^^^^^^^^^^^^^^ + +ACAImage is an ndarray subclass that supports functionality for the Chandra +ACA. Most importantly it allows image indexing and slicing in absolute +"aca" coordinates, where the image lower left coordinate is specified +by object ``row0`` and ``col0`` attributes. + +It also provides a ``meta`` dict that can be used to store additional useful +information. Any keys which are all upper-case will be exposed as object +attributes, e.g. ``img.BGDAVG`` <=> ``img.meta['BGDAVG']``. The ``row0`` +attribute is a proxy for ``img.meta['IMGROW0']``, and likewise for ``col0``. + +Creation +"""""""" + +When initializing an ``ACAImage``, additional ``*args`` and ``**kwargs`` are +used to try initializing via ``np.array(*args, **kwargs)``. If this fails +then ``np.zeros(*args, **kwargs)`` is tried. In this way one can either +initialize from array data or create a new array of zeros. + +One can easily create a new ``ACAImage`` as shown below. Note that it must +always be 2-dimensional. The initial ``row0`` and ``col0`` values default +to zero. +:: + + >>> from chandra_aca.aca_image import ACAImage + >>> im4 = np.arange(16).reshape(4, 4) + >>> a = ACAImage(im4, row0=10, col0=20) + >>> a + + +One could also initialize by providing a ``meta`` dict:: + + >>> a = ACAImage(im4, meta={'IMGROW0': 10, 'IMGCOL0': 20, 'BGDAVG': 5.2}) + >>> a + + +.. Note:: + + The printed representation of an ``ACAImage`` is always shown as the + rounded integer version of the values, but the full float value + is stored internally. If you ``print()`` the image then the floating + point values are shown. + +Image access and setting +"""""""""""""""""""""""" + +You can access array elements as usual, and in fact do any normal numpy array operations:: + + >>> a[3, 3] + 15 + +The special nature of ``ACAImage`` comes by doing array access via the ``aca`` attribute. +In this case all index values are in absolute coordinates, which might be negative. In +this case we can access the pixel at ACA coordinates row=13, col=23, which is equal to +``a[3, 3]`` for the given ``row0`` and ``col0`` offset:: + + >>> a.aca[13, 23] + 15 + +Creating a new array by slicing adjusts the ``row0`` and ``col0`` values +like you would expect:: + + >>> a2 = a.aca[12:, 22:] + >>> a2 + + +You can set values in absolute coordinates:: + + >>> a.aca[11:13, 21:23] = 500 + >>> a + + +Now let's make an image that represents the full ACA CCD and set a +sub-image from our 4x4 image ``a``. This uses the absolute location +of ``a`` to define a slice into ``b``:: + + >>> b = ACAImage(shape=(1024,1024), row0=-512, col0=-512) + >>> b[a] = a + >>> b.aca[8:16, 18:26] + + +You can also do things like adding 100 to every pixel in ``b`` +within the area of ``a``:: + + >>> b[a] += 100 + >>> b.aca[8:16, 18:26] + + +Image arithmetic operations +""""""""""""""""""""""""""" + +In addition to doing image arithmetic operations using explicit slices +as shown previously, one can also use normal arithmetic operators like +``+`` (for addition) or ``+=`` for in-place addition. + +**When the right-side operand is included via its ``.aca`` attribute, then the operation is +done in ACA coordinates.** + +This means that the operation is only done on overlapping pixels. This is +shown in the examples below. The supported operations for this are: + +- Addition (``+`` and ``+=``) +- Subtraction (``-`` and ``-=``) +- Multiplication (``*`` and ``*=``) +- Division (``/`` and ``/=``) +- True division (``/`` and ``/=`` in Py3+ and with __future__ division) +- Floor division (``//`` and ``//=``) +- Modulus (``%`` and ``%=``) +- Power (``**`` and ``**=``) + +**Initialize images (different shape and offset)** + + >>> a = ACAImage(shape=(6, 6), row0=10, col0=20) + 1 + >>> a + + >>> b = ACAImage(np.arange(1, 17).reshape(4, 4), row0=8, col0=18) * 10 + >>> b + + +**Add images (output has shape and row0/col0 of left side input)** + + >>> a + b.aca + + >>> b + a.aca + + + >>> b += a.aca + >>> b + + +**Make ``b`` image be fully contained in ``a``** + + >>> b.row0 = 11 + >>> b.col0 = 21 + >>> a += b.aca + >>> a + + +**Normal image addition fails if shape is mismatched** + + >>> a + b + Traceback (most recent call last): + File "", line 1, in + a + b + File "chandra_aca/aca_image.py", line 68, in _operator + out = op(self, other) # returns self for inplace ops + ValueError: operands could not be broadcast together with shapes (6,6) (4,4) + + +Meta-data +""""""""" + +Finally, the ``ACAImage`` object can store arbitrary metadata in the +``meta`` dict attribute. However, in order to make this convenient and +distinct from native numpy attributes, the ``meta`` attributes should +have UPPER CASE names. In this case they can be directly accessed +as object attributes instead of going through the ``meta`` dict:: + + >>> a.IMGROW0 + 10 + >>> a.meta + {'IMGCOL0': 20, 'IMGROW0': 10} + >>> a.NEWATTR = 'hello' + >>> a.meta + {'IMGCOL0': 20, 'NEWATTR': 'hello', 'IMGROW0': 10} + >>> a.NEWATTR + 'hello' + >>> a.meta['fail'] = 1 + >>> a.fail + Traceback (most recent call last): + AttributeError: 'ACAImage' object has no attribute 'fail' + diff --git a/docs/centroid_resid.rst b/docs/centroid_resid.rst new file mode 100644 index 00000000..ce48489c --- /dev/null +++ b/docs/centroid_resid.rst @@ -0,0 +1,5 @@ +chandra_aca.centroid_resid +========================== + +.. automodule:: chandra_aca.centroid_resid + :members: diff --git a/docs/conf.py b/docs/conf.py index 2392977b..f6a84dfe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,14 +14,13 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import os -import shlex import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # noqa -from chandra_aca import __version__ +from chandra_aca import __version__ # noqa: E402 # -- General configuration ------------------------------------------------ @@ -70,7 +69,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/drift.rst b/docs/drift.rst new file mode 100644 index 00000000..d7259526 --- /dev/null +++ b/docs/drift.rst @@ -0,0 +1,5 @@ +chandra_aca.drift +================= + +.. automodule:: chandra_aca.drift + :members: diff --git a/docs/index.rst b/docs/index.rst index 8dd626bc..f5cf2ba7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,293 +12,67 @@ Camera Assembly and associated aspect functionality. .. toctree:: :maxdepth: 2 -Plotting --------- - -.. automodule:: chandra_aca.plot - :members: - -Star probabilities ------------------- - -.. automodule:: chandra_aca.star_probs - :members: - -Transformations ---------------- - -.. automodule:: chandra_aca.transform - :members: - -Centroid residuals ------------------- - -.. automodule:: chandra_aca.centroid_resid - :members: ACA alignment drift ------------------- -.. automodule:: chandra_aca.drift - :members: +.. toctree:: + :maxdepth: 2 + + drift.rst ACA images ---------- -The `aca_image` module contains classes and utilities related to ACA readout -images. This includes the `ACAImage class`_ for manipulating images in -ACA coordinates, a first moment centroiding routine, and a library for -generating synthetic ACA images that have a high-fidelity point spread function -(PSF). - -.. automodule:: chandra_aca.aca_image - :members: - -ACAImage class -^^^^^^^^^^^^^^ - -ACAImage is an ndarray subclass that supports functionality for the Chandra -ACA. Most importantly it allows image indexing and slicing in absolute -"aca" coordinates, where the image lower left coordinate is specified -by object ``row0`` and ``col0`` attributes. - -It also provides a ``meta`` dict that can be used to store additional useful -information. Any keys which are all upper-case will be exposed as object -attributes, e.g. ``img.BGDAVG`` <=> ``img.meta['BGDAVG']``. The ``row0`` -attribute is a proxy for ``img.meta['IMGROW0']``, and likewise for ``col0``. - -Creation -"""""""" - -When initializing an ``ACAImage``, additional ``*args`` and ``**kwargs`` are -used to try initializing via ``np.array(*args, **kwargs)``. If this fails -then ``np.zeros(*args, **kwargs)`` is tried. In this way one can either -initialize from array data or create a new array of zeros. - -One can easily create a new ``ACAImage`` as shown below. Note that it must -always be 2-dimensional. The initial ``row0`` and ``col0`` values default -to zero. -:: - - >>> from chandra_aca.aca_image import ACAImage - >>> im4 = np.arange(16).reshape(4, 4) - >>> a = ACAImage(im4, row0=10, col0=20) - >>> a - - -One could also initialize by providing a ``meta`` dict:: - - >>> a = ACAImage(im4, meta={'IMGROW0': 10, 'IMGCOL0': 20, 'BGDAVG': 5.2}) - >>> a - - -.. Note:: - - The printed representation of an ``ACAImage`` is always shown as the - rounded integer version of the values, but the full float value - is stored internally. If you ``print()`` the image then the floating - point values are shown. - -Image access and setting -"""""""""""""""""""""""" - -You can access array elements as usual, and in fact do any normal numpy array operations:: - - >>> a[3, 3] - 15 - -The special nature of ``ACAImage`` comes by doing array access via the ``aca`` attribute. -In this case all index values are in absolute coordinates, which might be negative. In -this case we can access the pixel at ACA coordinates row=13, col=23, which is equal to -``a[3, 3]`` for the given ``row0`` and ``col0`` offset:: - - >>> a.aca[13, 23] - 15 - -Creating a new array by slicing adjusts the ``row0`` and ``col0`` values -like you would expect:: - - >>> a2 = a.aca[12:, 22:] - >>> a2 - - -You can set values in absolute coordinates:: - - >>> a.aca[11:13, 21:23] = 500 - >>> a - - -Now let's make an image that represents the full ACA CCD and set a -sub-image from our 4x4 image ``a``. This uses the absolute location -of ``a`` to define a slice into ``b``:: - - >>> b = ACAImage(shape=(1024,1024), row0=-512, col0=-512) - >>> b[a] = a - >>> b.aca[8:16, 18:26] - - -You can also do things like adding 100 to every pixel in ``b`` -within the area of ``a``:: - - >>> b[a] += 100 - >>> b.aca[8:16, 18:26] - - -Image arithmetic operations -""""""""""""""""""""""""""" - -In addition to doing image arithmetic operations using explicit slices -as shown previously, one can also use normal arithmetic operators like -``+`` (for addition) or ``+=`` for in-place addition. - -**When the right-side operand is included via its ``.aca`` attribute, then the operation is -done in ACA coordinates.** - -This means that the operation is only done on overlapping pixels. This is -shown in the examples below. The supported operations for this are: - -- Addition (``+`` and ``+=``) -- Subtraction (``-`` and ``-=``) -- Multiplication (``*`` and ``*=``) -- Division (``/`` and ``/=``) -- True division (``/`` and ``/=`` in Py3+ and with __future__ division) -- Floor division (``//`` and ``//=``) -- Modulus (``%`` and ``%=``) -- Power (``**`` and ``**=``) - -**Initialize images (different shape and offset)** +.. toctree:: + :maxdepth: 3 - >>> a = ACAImage(shape=(6, 6), row0=10, col0=20) + 1 - >>> a - - >>> b = ACAImage(np.arange(1, 17).reshape(4, 4), row0=8, col0=18) * 10 - >>> b - + aca_image.rst -**Add images (output has shape and row0/col0 of left side input)** +Centroid residuals +------------------ - >>> a + b.aca - - >>> b + a.aca - +.. toctree:: + :maxdepth: 2 - >>> b += a.aca - >>> b - + centroid_resid.rst -**Make ``b`` image be fully contained in ``a``** +MAUDE Decom +----------- - >>> b.row0 = 11 - >>> b.col0 = 21 - >>> a += b.aca - >>> a - +.. toctree:: + :maxdepth: 2 -**Normal image addition fails if shape is mismatched** + maude_decom.rst - >>> a + b - Traceback (most recent call last): - File "", line 1, in - a + b - File "chandra_aca/aca_image.py", line 68, in _operator - out = op(self, other) # returns self for inplace ops - ValueError: operands could not be broadcast together with shapes (6,6) (4,4) +Planet Positions +---------------- +.. toctree:: + :maxdepth: 2 + planets.rst -Meta-data -""""""""" +Plotting +-------- -Finally, the ``ACAImage`` object can store arbitrary metadata in the -``meta`` dict attribute. However, in order to make this convenient and -distinct from native numpy attributes, the ``meta`` attributes should -have UPPER CASE names. In this case they can be directly accessed -as object attributes instead of going through the ``meta`` dict:: +.. toctree:: + :maxdepth: 2 - >>> a.IMGROW0 - 10 - >>> a.meta - {'IMGCOL0': 20, 'IMGROW0': 10} - >>> a.NEWATTR = 'hello' - >>> a.meta - {'IMGCOL0': 20, 'NEWATTR': 'hello', 'IMGROW0': 10} - >>> a.NEWATTR - 'hello' - >>> a.meta['fail'] = 1 - >>> a.fail - Traceback (most recent call last): - AttributeError: 'ACAImage' object has no attribute 'fail' + plot.rst +Star probabilities +------------------ +.. toctree:: + :maxdepth: 2 -MAUDE Decom ------------ + star_probs.rst -.. automodule:: chandra_aca.maude_decom - :members: +Transformations +--------------- +.. toctree:: + :maxdepth: 2 -Planet Positions ----------------- + transform.rst -.. automodule:: chandra_aca.planets - :members: diff --git a/docs/maude_decom.rst b/docs/maude_decom.rst new file mode 100644 index 00000000..bdfdfc5d --- /dev/null +++ b/docs/maude_decom.rst @@ -0,0 +1,5 @@ +chandra_aca.maude_decom +======================= + +.. automodule:: chandra_aca.maude_decom + :members: diff --git a/docs/planets.rst b/docs/planets.rst new file mode 100644 index 00000000..cdee4af4 --- /dev/null +++ b/docs/planets.rst @@ -0,0 +1,5 @@ +chandra_aca.planets +=================== + +.. automodule:: chandra_aca.planets + :members: diff --git a/docs/plot.rst b/docs/plot.rst new file mode 100644 index 00000000..cf5c0b59 --- /dev/null +++ b/docs/plot.rst @@ -0,0 +1,5 @@ +chandra_aca.plot +================ + +.. automodule:: chandra_aca.plot + :members: diff --git a/docs/star_probs.rst b/docs/star_probs.rst new file mode 100644 index 00000000..e2cc46ad --- /dev/null +++ b/docs/star_probs.rst @@ -0,0 +1,5 @@ +chandra_aca.star_probs +======================= + +.. automodule:: chandra_aca.star_probs + :members: diff --git a/docs/transform.rst b/docs/transform.rst new file mode 100644 index 00000000..a2d19c59 --- /dev/null +++ b/docs/transform.rst @@ -0,0 +1,5 @@ +chandra_aca.transform +===================== + +.. automodule:: chandra_aca.transform + :members: