From 01d8f5303ff740943879a06663447670df978159 Mon Sep 17 00:00:00 2001 From: Julio Campagnolo Date: Sun, 12 May 2024 10:39:05 -0300 Subject: [PATCH] astrometry: added new solve-field command class --- astropop/astrometry/astrometrynet.py | 66 +++++----- astropop/py_utils.py | 3 +- tests/test_astrometry.py | 181 +++++---------------------- 3 files changed, 70 insertions(+), 180 deletions(-) diff --git a/astropop/astrometry/astrometrynet.py b/astropop/astrometry/astrometrynet.py index 7d832ced..926c24f4 100644 --- a/astropop/astrometry/astrometrynet.py +++ b/astropop/astrometry/astrometrynet.py @@ -5,10 +5,12 @@ improvements. """ +from multiprocessing import Value import os import shutil from subprocess import CalledProcessError import copy +from packaging import version from tempfile import NamedTemporaryFile, mkdtemp import warnings @@ -27,11 +29,37 @@ __all__ = ['AstrometrySolver', 'solve_astrometry_xy', 'solve_astrometry_image', 'solve_astrometry_framedata', 'create_xyls', - 'AstrometryNetUnsolvedField'] + 'AstrometryNetUnsolvedField', 'SolveFieldCommand'] _solve_field = shutil.which('solve-field') + +class SolveFieldCommand: + """Astrometry.net solve field check and run command.""" + _version = None + + def __init__(self, command=_solve_field): + if isinstance(command, SolveFieldCommand): + self._version = command._version + command = command._command + elif command is None or not os.path.exists(command): + raise FileNotFoundError('solve-field command not found.') + self._command = command + + @property + def version(self): + """Return the version of the solve-field command.""" + if self._version is None: + _, sout, _ = run_command([self._command, '--version']) + self._version = version.parse(sout[0]) + return self._version + + def run(self, *args, **kwargs): + """Run the solve-field command.""" + return run_command([self._command, *args], **kwargs) + + _center_help = 'only search in indexes within `radius` of the field center ' \ 'given by `ra` and `dec`' solve_field_params = { @@ -222,35 +250,13 @@ 'To astrometry.cfg file. If no scale estimate is given, use these ' 'limits on field width in deg.' ), - 'inparallel': ( - '', - 'To astrometry.cfg file. Check indexes in parallel. Only enable it if ' - 'you have memory to store all indexes.' - ), - 'index': ( - '', - 'To astrometry.cfg file. Explicitly list the indices to load.' - ' Disables ``autoindex``' - ), - 'autoindex': ( - '', - 'To astrometry.cfg file. Load any indices found in the directories ' - 'listed.' - ), - 'add_path': ( + 'index-dir': ( '', - 'To astrometry.cfg file. Add a location of astrometry.net index files.' + 'Add a location of astrometry.net index files.' ' ``astrometry-engine`` will search index files in all listed folders.' ' Additive, do not override defaults.' - ), - 'depths': ( - '', - 'To astrometry.cfg file. If no depths are given, use these.' ) } -# These are the parameters that can be set in the astrometry.cfg file -_conf_file = ['inparallel', 'minwidth', 'maxwidth', 'depths', - 'add_path', 'autoindex', 'index'] def get_options_help(): @@ -473,17 +479,17 @@ class AstrometrySolver(): Explicitly list the indices to load. """ - def __init__(self, solve_field=None, config=None, config_file=None, - defaults=None, keep_files=False): + def __init__(self, solve_field=_solve_field, defaults=None, + keep_files=False): # declare the defaults here to be safer self._defaults = {'no-plots': None, 'overwrite': None} if defaults is None: defaults = {} self._defaults.update(defaults) - self.config = self._read_config(config_file, config) - - self._command = solve_field or _solve_field + self._command = SolveFieldCommand(solve_field) + if self._command.version < version.parse('0.95'): + raise ValueError('Astrometry.net version must be at least 0.95.') self._keep = keep_files def solve_field(self, filename, options=None, output_dir=None, **kwargs): diff --git a/astropop/py_utils.py b/astropop/py_utils.py index 5bbd6e15..ff6a65c4 100644 --- a/astropop/py_utils.py +++ b/astropop/py_utils.py @@ -184,9 +184,8 @@ def proccess_out(line, std_l, loglevel): std_l.append(line) logger.log(loglevel, line) - # TODO: when deprecate py37, use shlex.join(args) proc = await asyncio.create_subprocess_shell( - ' '.join(shlex.quote(arg) for arg in args), + shlex.join(args), limit=2**23, # 8 MB stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, diff --git a/tests/test_astrometry.py b/tests/test_astrometry.py index 2e8fcc91..cee13cca 100644 --- a/tests/test_astrometry.py +++ b/tests/test_astrometry.py @@ -5,6 +5,7 @@ import numpy as np import os +from packaging.version import Version from astroquery.skyview import SkyView from astropy.coordinates import Angle, SkyCoord from astropy.config import get_cache_dir @@ -15,12 +16,13 @@ from astropy import units from astropy.utils.data import download_file -from astropop.astrometry.astrometrynet import _solve_field, \ - solve_astrometry_image, \ +from astropop.astrometry.astrometrynet import solve_astrometry_image, \ solve_astrometry_xy, \ solve_astrometry_hdu, \ solve_astrometry_framedata, \ - AstrometrySolver + AstrometrySolver, \ + SolveFieldCommand, \ + _solve_field from astropop.astrometry.astrometrynet import _parse_angle, \ _parse_coordinates, \ _parse_crpix, \ @@ -45,7 +47,24 @@ def compare_wcs(wcs, nwcs): "False)") -@pytest.mark.remote_data +class Test_SolveFieldCommand: + def test_empty_error(self): + with pytest.raises(FileNotFoundError, + match='solve-field command not found.'): + SolveFieldCommand(command=None) + + def test_not_exists_error(self): + with pytest.raises(FileNotFoundError, + match='solve-field command not found.'): + SolveFieldCommand(command='not_exists') + + def test_version(self): + s = SolveFieldCommand() + v = s.version + assert_is_instance(v, Version) + + +#@pytest.mark.remote_data class Test_AstrometrySolver: def get_image(self): # return image name and index name @@ -186,150 +205,16 @@ def test_parse_crpix_fails(self): assert_equal(_parse_crpix({}), []) @skip_astrometry - def test_read_cfg(self): - a = AstrometrySolver() # read the default configuration - cfg = a._read_config() - assert_not_in('index', cfg) - assert_false(cfg['inparallel']) - assert_not_in('', cfg) - assert_not_in('minwidth', cfg) - assert_not_in('maxwidth', cfg) - assert_equal(cfg['cpulimit'], '300') - assert_true(cfg['autoindex']) - assert_equal(len(cfg['add_path']), 1) - - @skip_astrometry - def test_read_cfg_fname(self, tmpdir): - fname = tmpdir / 'test.cfg' - f = open(fname, 'w') - f.write("inparallel\n") - f.write(" minwidth 0.1 \n") - f.write(" maxwidth 180\n") - f.write("depths 10 20 30 40 50 60\n") - f.write("cpulimit 300\n") - f.write("\n\n") - f.write("# comment\n") - f.write("add_path /data # commented path\n") - f.write("add_path /data1\n") - f.write("#add_path /data2\n") # data2 will not be present - f.write("autoindex\n") - f.write("index index-219\n") - f.write("index index-220\n") - f.write("index index-221\n") - f.write("# index index-222\n") # 222 will not be present - f.close() - - a = AstrometrySolver() - cfg = a._read_config(fname) - assert_not_equal("", cfg) - assert_true(cfg['inparallel']) - assert_equal(cfg['minwidth'], '0.1') - assert_equal(cfg['maxwidth'], '180') - assert_equal(cfg['depths'], [10, 20, 30, 40, 50, 60]) - assert_equal(cfg['cpulimit'], '300') - assert_equal(cfg['add_path'], ['/data', '/data1']) - assert_equal(cfg['index'], ['index-219', 'index-220', 'index-221']) - assert_true(cfg['autoindex']) - - @skip_astrometry - def test_read_cfg_with_options(self, tmpdir): - fname = tmpdir / 'test.cfg' - f = open(fname, 'w') - f.write("inparallel\n") - f.write(" minwidth 0.1 \n") - f.write(" maxwidth 180\n") - f.write("depths 10 20 30 40 50 60\n") - f.write("cpulimit 300\n") - f.write("\n\n") - f.write("# comment\n") - f.write("add_path /data # commented path\n") - f.write("add_path /data1\n") - f.write("#add_path /data2\n") # data2 will not be present - f.write("autoindex\n") - f.write("index index-219\n") - f.write("index index-220\n") - f.write("index index-221\n") - f.write("# index index-222\n") # 222 will not be present - f.close() - - a = AstrometrySolver() - cfg = a._read_config(fname, {'depths': [10, 30, 50], - 'add_path': '/data3', - 'index': ['indx4', 'indx5']}) - assert_not_equal("", cfg) - assert_true(cfg['inparallel']) - assert_equal(cfg['minwidth'], '0.1') - assert_equal(cfg['maxwidth'], '180') - assert_equal(cfg['depths'], [10, 30, 50]) - assert_equal(cfg['cpulimit'], '300') - assert_equal(cfg['add_path'], ['/data', '/data1', '/data3']) - assert_equal(cfg['index'], ['index-219', 'index-220', 'index-221', - 'indx4', 'indx5']) - assert_true(cfg['autoindex']) - - @skip_astrometry - def test_write_config(self, tmpdir): - fname = tmpdir / 'test.cfg' - - a = AstrometrySolver() - a.config = {'inparallel': False, - 'autoindex': True, - 'cpulimit': 300, - 'minwidth': 0.1, - 'maxwidth': 180, - 'depths': [20, 40, 60], - 'index': ['011', '012'], - 'add_path': ['/path1', '/path2']} - a._write_config(fname) - - with open(fname, 'r') as f: - for line in f.readlines(): - assert_in(line.strip('\n'), ['autoindex', 'inparallel', - 'cpulimit 300', 'minwidth 0.1', - 'maxwidth 180', 'depths 20 40 60', - 'index 011', 'index 012', - 'add_path /path1', - 'add_path /path2']) - - @skip_astrometry - def test_pop_config(self): - a = AstrometrySolver() - options1 = {'inparallel': False, - 'autoindex': True, - 'cpulimit': 300, - 'minwidth': 0.1, - 'maxwidth': 180, - 'depths': [20, 40, 60], - 'index': ['011', '012'], - 'add_path': ['/path1', '/path2'], - 'ra': 0.0, 'dec': 0.0, 'radius': 1.0} - options, cfg = a._pop_config(options1) - assert_is_not(options1, options) - assert_equal(options['ra'], 0.0) - assert_equal(options['dec'], 0.0) - assert_equal(options['radius'], 1.0) - assert_equal(options['cpulimit'], 300) - assert_equal(cfg['inparallel'], False) - assert_equal(cfg['autoindex'], True) - assert_equal(cfg['minwidth'], 0.1) - assert_equal(cfg['maxwidth'], 180) - assert_equal(cfg['depths'], [20, 40, 60]) - assert_equal(cfg['index'], ['011', '012']) - assert_equal(cfg['add_path'], ['/path1', '/path2']) - - @skip_astrometry - def test_only_write_config_when_needed(self, tmpdir): - a = AstrometrySolver() - args = a._get_args(tmpdir/'1/', tmpdir/'1/fitsfile.fits', - {'ra': 0.0, 'dec': 0.0, 'radius': 1.0}, - output_dir=tmpdir, correspond='test.correspond') - assert_not_in('--config', args) - - args = a._get_args(tmpdir/'2/', tmpdir/'2/fitsfile.fits', - {'ra': 0.0, 'dec': 0.0, 'radius': 1.0, - 'inparallel': False}, - output_dir=tmpdir, correspond='test.correspond') - assert_in('--config', args) + def test_solve_field_version(self): + com = SolveFieldCommand() + com._version = Version('0.95') + # 0.95, no error + AstrometrySolver(solve_field=com) + com._version = Version('0.70') + # 0.70, error + with pytest.raises(ValueError, + match='Astrometry.net version must be at least 0.95.'): + AstrometrySolver(solve_field=com) @skip_astrometry def test_solve_astrometry_hdu(self, tmpdir):