From 69b0e4bd538855435c4a3975592a99a68744e6b9 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Mon, 7 Aug 2023 10:17:45 -0400 Subject: [PATCH 01/32] some integral symbols --- pyspeckit/spectrum/models/lte_molecule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyspeckit/spectrum/models/lte_molecule.py b/pyspeckit/spectrum/models/lte_molecule.py index e2f35e3f..a736e52a 100644 --- a/pyspeckit/spectrum/models/lte_molecule.py +++ b/pyspeckit/spectrum/models/lte_molecule.py @@ -500,7 +500,7 @@ def nupper_of_kkms(kkms, freq, Aul, replace_bad=None): .. math:: - N_u = (3 k) / (8 \pi^3 \nu S \mu^2 R_i) integ(T_R/f dv) + N_u = (3 k) / (8 \pi^3 \nu S \mu^2 R_i) \int(T_R/f dv) $\int(T_R/f dv)$ is the optically thin integrated intensity in K km/s dnu/nu = dv/c [doppler eqn], so to get $\int(T_R dnu)$, sub in $dv = c/\nu d\nu$ @@ -508,7 +508,7 @@ def nupper_of_kkms(kkms, freq, Aul, replace_bad=None): .. math:: - N_u = (3 k c) / (8 \pi^3 \nu^2 S \mu^2 R_i) integ(T_R/f d\nu) + N_u = (3 k c) / (8 \pi^3 \nu^2 S \mu^2 R_i) \int(T_R/f d\nu) We then need to deal with the S \mu^2 R_i term. We assume R_i = 1, since we From 4c848a8874d52593321ac97bfc4122ddf40b5238 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Mon, 7 Aug 2023 12:08:36 -0400 Subject: [PATCH 02/32] add nsf support --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 80957ba2..a0d784c9 100644 --- a/README.rst +++ b/README.rst @@ -67,3 +67,6 @@ The PySpecKit logo uses the Voyager 1 image of Earth known as the "Pale Blue Dot .. image:: https://zenodo.org/badge/6116896.svg :target: https://zenodo.org/badge/latestdoi/6116896 +pyspeckit development has been supported by the NSF under grants AST 2008101 and CAREER 2142300 + +.. image:: https://www.nsf.gov/policies/images/NSF_Official_logo_High_Res_1200ppi.png From 65c20540d9f45c2ac95016e24b798632e869b021 Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Mon, 7 Aug 2023 12:12:41 -0400 Subject: [PATCH 03/32] nsf logo --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a0d784c9..6399b093 100644 --- a/README.rst +++ b/README.rst @@ -69,4 +69,6 @@ The PySpecKit logo uses the Voyager 1 image of Earth known as the "Pale Blue Dot pyspeckit development has been supported by the NSF under grants AST 2008101 and CAREER 2142300 -.. image:: https://www.nsf.gov/policies/images/NSF_Official_logo_High_Res_1200ppi.png +.. image:: https://www.nsf.gov/news/mmg/media/images/nsf%20logo_ba604992-ed6d-46a7-8f5b-151b1c3e17e3.jpg + :target: https://www.nsf.gov/policies/images/NSF_Official_logo_High_Res_1200ppi.png + :width: 200 From 71246dc5a0d217be77c9df2535fb515f09af97fb Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Tue, 25 Jul 2023 11:16:32 -0400 Subject: [PATCH 04/32] attempt to fix issue 384 by using different attributes that _may_ be present in mpl 3.7 --- pyspeckit/spectrum/baseline.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pyspeckit/spectrum/baseline.py b/pyspeckit/spectrum/baseline.py index 52b373e5..6f51b742 100644 --- a/pyspeckit/spectrum/baseline.py +++ b/pyspeckit/spectrum/baseline.py @@ -385,16 +385,34 @@ def plot_baseline(self, annotate=True, baseline_fit_color='orange', for p in self.Spectrum.plotter.errorplot: if isinstance(p,matplotlib.collections.PolyCollection): if p in self.Spectrum.plotter.axis.collections: - self.Spectrum.plotter.axis.collections.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass if isinstance(p,matplotlib.lines.Line2D): if p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass # if we subtract the baseline, replot the now-subtracted data with rescaled Y axes if self.subtracted: if self.Spectrum.plotter.axis is not None: for p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass plotmask = self.OKmask*False # include nothing... plotmask[self.xmin:self.xmax] = self.OKmask[self.xmin:self.xmax] # then include everything OK in range self.Spectrum.plotter.ymin = abs(self.Spectrum.data[plotmask].min())*1.1*np.sign(self.Spectrum.data[plotmask].min()) @@ -407,7 +425,13 @@ def plot_baseline(self, annotate=True, baseline_fit_color='orange', for p in self._plots: # remove the old baseline plots if p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass self._plots += self.Spectrum.plotter.axis.plot( self.Spectrum.xarr, self.basespec, From 26cb233b74c60334a5c26f441fba419e42dfd5e5 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 11:05:08 -0400 Subject: [PATCH 05/32] numpy dtype issue385 fixed --- examples/synthetic_spectrum_example.py | 2 +- examples/synthetic_spectrum_example_witherrorestimates.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/synthetic_spectrum_example.py b/examples/synthetic_spectrum_example.py index 35b3429a..897f7b13 100644 --- a/examples/synthetic_spectrum_example.py +++ b/examples/synthetic_spectrum_example.py @@ -7,7 +7,7 @@ pl.close('all') pl.figure(1).clf() -xaxis = np.linspace(-50,150,100.) +xaxis = np.linspace(-50.,150.,100) sigma = 10. center = 50. synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) diff --git a/examples/synthetic_spectrum_example_witherrorestimates.py b/examples/synthetic_spectrum_example_witherrorestimates.py index 36eefac6..5137c8cf 100644 --- a/examples/synthetic_spectrum_example_witherrorestimates.py +++ b/examples/synthetic_spectrum_example_witherrorestimates.py @@ -10,7 +10,7 @@ pl.close('all') pl.figure(1).clf() -xaxis = np.linspace(-50,150,100.) +xaxis = np.linspace(-50.,150.,100) sigma = 10. center = 50. synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) From 9c660c1c0221d031496419e3e32ef373b18eb6ae Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Tue, 25 Jul 2023 11:16:32 -0400 Subject: [PATCH 06/32] attempt to fix issue 384 by using different attributes that _may_ be present in mpl 3.7 --- pyspeckit/spectrum/baseline.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pyspeckit/spectrum/baseline.py b/pyspeckit/spectrum/baseline.py index 52b373e5..6f51b742 100644 --- a/pyspeckit/spectrum/baseline.py +++ b/pyspeckit/spectrum/baseline.py @@ -385,16 +385,34 @@ def plot_baseline(self, annotate=True, baseline_fit_color='orange', for p in self.Spectrum.plotter.errorplot: if isinstance(p,matplotlib.collections.PolyCollection): if p in self.Spectrum.plotter.axis.collections: - self.Spectrum.plotter.axis.collections.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass if isinstance(p,matplotlib.lines.Line2D): if p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass # if we subtract the baseline, replot the now-subtracted data with rescaled Y axes if self.subtracted: if self.Spectrum.plotter.axis is not None: for p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass plotmask = self.OKmask*False # include nothing... plotmask[self.xmin:self.xmax] = self.OKmask[self.xmin:self.xmax] # then include everything OK in range self.Spectrum.plotter.ymin = abs(self.Spectrum.data[plotmask].min())*1.1*np.sign(self.Spectrum.data[plotmask].min()) @@ -407,7 +425,13 @@ def plot_baseline(self, annotate=True, baseline_fit_color='orange', for p in self._plots: # remove the old baseline plots if p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) + try: + self.Spectrum.plotter.axis.lines.remove(p) + except AttributeError: + try: + p.remove() + except Exception as ex: + pass self._plots += self.Spectrum.plotter.axis.plot( self.Spectrum.xarr, self.basespec, From 986e7c5a3b142db72c6728e40b8d93b2945e8547 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 11:15:41 -0400 Subject: [PATCH 07/32] handle xarrs with quantities in places they were raising issues --- pyspeckit/spectrum/fitters.py | 8 ++++++++ pyspeckit/spectrum/models/model.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyspeckit/spectrum/fitters.py b/pyspeckit/spectrum/fitters.py index a131613b..4e3b8607 100644 --- a/pyspeckit/spectrum/fitters.py +++ b/pyspeckit/spectrum/fitters.py @@ -754,6 +754,14 @@ def multifit(self, fittype=None, renormalize='auto', annotate=None, spectofit = self.spectofit[self.xmin:self.xmax][~self.mask_sliced].astype('float64') err = self.errspec[self.xmin:self.xmax][~self.mask_sliced].astype('float64') + # strip quantity properties before passing to fitter (fitters are not quantity-aware) + if hasattr(xtofit, 'value'): + xtofit = xtofit.value + if hasattr(spectofit, 'value'): + spectofit = spectofit.value + if hasattr(err, 'value'): + err = err.value + if np.all(err == 0): raise ValueError("Errors are all zero. This should not occur and " "is a bug. (if you set the errors to all zero, " diff --git a/pyspeckit/spectrum/models/model.py b/pyspeckit/spectrum/models/model.py index 32b0242a..08da7c1c 100644 --- a/pyspeckit/spectrum/models/model.py +++ b/pyspeckit/spectrum/models/model.py @@ -399,7 +399,16 @@ def L(x): for jj in range(int((len(parvals)-self.vheight)/self.npars)): lower_parind = jj*self.npars+self.vheight upper_parind = (jj+1)*self.npars+self.vheight - v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) + try: + v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) + except TypeError as ex: + # not all model functions are quantity-aware, so we strip quantities here + if hasattr(x, 'value'): + x = x.value + v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) + else: + raise ex + return v return L From ced214d758ef6873cff38e62008faf3f5a494a3d Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 11:42:02 -0400 Subject: [PATCH 08/32] add faq entry --- docs/faq.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 64b1e1db..d14dcfd8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -35,3 +35,31 @@ In particular, their specifies all valid spectral coordinate type codes. .. _FITS Paper III: http://adsabs.harvard.edu/abs/2006A%26A...446..747G + + +No plot appears when I run plot commands +---------------------------------------- + +Plots appearing and not appearing depends on your global matplotlib +configuration. + +If you are already in a python session, you can enable interactive plotting by +doing: + +.. code:: python + from matplotlib import pyplot + pyplot.ion() + pyplot.show() + +The ``.ion()`` call turns interactive mode on. If it wasn't on when you made +the plot, you need to call ``.show()`` next. If you call ``.ion()`` before +calling any pyspeckit plot command, though, the ``.show()`` is not required. + +If you want to make sure this is enabled automatically on startup, you can +start your python session with + +.. code:: bash + ipython --matplotlib + +(I tend to use ``ipython --pylab``, but it's `deprecated +`_) From b865001e35b7e5d5feef9afd68f4ad5ccf5c5f82 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 11:55:52 -0400 Subject: [PATCH 09/32] revert #389, which was the wrong fix to the problem. Also, start refactoring imports in tests --- pyspeckit/spectrum/fitters.py | 8 -------- pyspeckit/spectrum/models/model.py | 13 ++++--------- pyspeckit/spectrum/models/template.py | 4 ++-- pyspeckit/spectrum/models/tests/test_template.py | 5 ++--- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/pyspeckit/spectrum/fitters.py b/pyspeckit/spectrum/fitters.py index 4e3b8607..a131613b 100644 --- a/pyspeckit/spectrum/fitters.py +++ b/pyspeckit/spectrum/fitters.py @@ -754,14 +754,6 @@ def multifit(self, fittype=None, renormalize='auto', annotate=None, spectofit = self.spectofit[self.xmin:self.xmax][~self.mask_sliced].astype('float64') err = self.errspec[self.xmin:self.xmax][~self.mask_sliced].astype('float64') - # strip quantity properties before passing to fitter (fitters are not quantity-aware) - if hasattr(xtofit, 'value'): - xtofit = xtofit.value - if hasattr(spectofit, 'value'): - spectofit = spectofit.value - if hasattr(err, 'value'): - err = err.value - if np.all(err == 0): raise ValueError("Errors are all zero. This should not occur and " "is a bug. (if you set the errors to all zero, " diff --git a/pyspeckit/spectrum/models/model.py b/pyspeckit/spectrum/models/model.py index 08da7c1c..a8d78671 100644 --- a/pyspeckit/spectrum/models/model.py +++ b/pyspeckit/spectrum/models/model.py @@ -16,6 +16,7 @@ from . import fitter from . import mpfit_messages from pyspeckit.specwarnings import warn +from pyspeckit.spectrum.units import SpectroscopicAxis import itertools import operator import six @@ -391,6 +392,8 @@ def n_modelfunc(self, pars=None, debug=False, **kwargs): log.debug("pars to n_modelfunc: {0}, parvals:{1}".format(pars, parvals)) def L(x): + if hasattr(x, 'value') and not hasattr(x, 'x_to_coord'): + x = SpectroscopicAxis(x) v = np.zeros(len(x)) if self.vheight: v += parvals[0] @@ -399,15 +402,7 @@ def L(x): for jj in range(int((len(parvals)-self.vheight)/self.npars)): lower_parind = jj*self.npars+self.vheight upper_parind = (jj+1)*self.npars+self.vheight - try: - v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) - except TypeError as ex: - # not all model functions are quantity-aware, so we strip quantities here - if hasattr(x, 'value'): - x = x.value - v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) - else: - raise ex + v += self.modelfunc(x, *parvals[lower_parind:upper_parind], **kwargs) return v return L diff --git a/pyspeckit/spectrum/models/template.py b/pyspeckit/spectrum/models/template.py index 5b5a48e1..904a3e27 100644 --- a/pyspeckit/spectrum/models/template.py +++ b/pyspeckit/spectrum/models/template.py @@ -45,7 +45,7 @@ def spectral_template(xarr, scale, xshift, xshift_units=xshift_units): left=left, right=right, ) - + return model return spectral_template @@ -83,5 +83,5 @@ def template_fitter(template_spectrum, xshift_units='km/s'): shortvarnames=('A',r'\Delta x'), centroid_par='shift',) myclass.__name__ = "spectral_template" - + return myclass diff --git a/pyspeckit/spectrum/models/tests/test_template.py b/pyspeckit/spectrum/models/tests/test_template.py index 8469f56a..3cd1f37d 100644 --- a/pyspeckit/spectrum/models/tests/test_template.py +++ b/pyspeckit/spectrum/models/tests/test_template.py @@ -2,9 +2,8 @@ Tests for template fitter """ -from .... import spectrum -from ...classes import Spectrum -from ... import models +from pyspeckit import spectrum, models +from pyspeckit.spectrum.classes import Spectrum import numpy as np def test_template(): From 59d5a06cbd3e3415e5da72f22b8442b3737e3f6a Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 12:14:29 -0400 Subject: [PATCH 10/32] fix polyval incompatibility --- pyspeckit/spectrum/fitters.py | 5 +++++ pyspeckit/spectrum/models/polynomial_continuum.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyspeckit/spectrum/fitters.py b/pyspeckit/spectrum/fitters.py index a131613b..54604b38 100644 --- a/pyspeckit/spectrum/fitters.py +++ b/pyspeckit/spectrum/fitters.py @@ -19,6 +19,8 @@ from . import history from . import widgets +from pyspeckit.spectrum.units import SpectroscopicAxis + class Registry(object): """ This class is a simple wrapper to prevent fitter properties from being globals @@ -754,6 +756,9 @@ def multifit(self, fittype=None, renormalize='auto', annotate=None, spectofit = self.spectofit[self.xmin:self.xmax][~self.mask_sliced].astype('float64') err = self.errspec[self.xmin:self.xmax][~self.mask_sliced].astype('float64') + if hasattr(xtofit, 'value') and not hasattr(xtofit, 'x_to_coord'): + xtofit = SpectroscopicAxis(xtofit) + if np.all(err == 0): raise ValueError("Errors are all zero. This should not occur and " "is a bug. (if you set the errors to all zero, " diff --git a/pyspeckit/spectrum/models/polynomial_continuum.py b/pyspeckit/spectrum/models/polynomial_continuum.py index ff6e0e19..d4b33fc1 100644 --- a/pyspeckit/spectrum/models/polynomial_continuum.py +++ b/pyspeckit/spectrum/models/polynomial_continuum.py @@ -21,7 +21,11 @@ def polymodel(x, *pars, **kwargs): *args = polynomial parameters **kwargs = just to catch extra junk; not passed """ - return numpy.polyval(pars,x) + # polyval and astropy quantity are incompatible + if hasattr(x, 'value'): + x = x.value + + return numpy.polyval(pars, x) polymodel.__doc__ += numpy.polyval.__doc__ From a68e74312a80b02c08ba6673a7afbd21c53bd1e5 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 12:16:21 -0400 Subject: [PATCH 11/32] extend the fix in 386 --- pyspeckit/spectrum/fitters.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyspeckit/spectrum/fitters.py b/pyspeckit/spectrum/fitters.py index 54604b38..ca704d6d 100644 --- a/pyspeckit/spectrum/fitters.py +++ b/pyspeckit/spectrum/fitters.py @@ -1447,7 +1447,13 @@ def _clearlegend(self): # don't remove fitleg unless it's in the current axis # self.fitleg.set_visible(False) if self.fitleg in axis.artists: - axis.artists.remove(self.fitleg) + try: + axis.artists.remove(self.fitleg) + except AttributeError: + try: + self.fitleg.remove() + except Exception as ex: + pass if self.Spectrum.plotter.autorefresh: self.Spectrum.plotter.refresh() From 21d127a7dfd267df6deafbb8730288187fb8ff9c Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Wed, 9 Aug 2023 12:47:07 -0400 Subject: [PATCH 12/32] add a workaround for a broken test that doesn't act broken interactively --- pyspeckit/spectrum/plotters.py | 11 ++++++++--- pyspeckit/spectrum/tests/test_eqw.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pyspeckit/spectrum/plotters.py b/pyspeckit/spectrum/plotters.py index 5cc20593..468039ac 100644 --- a/pyspeckit/spectrum/plotters.py +++ b/pyspeckit/spectrum/plotters.py @@ -172,9 +172,14 @@ def _reconnect_matplotlib_keys(self): self.figure.canvas.callbacks.callbacks['key_press_event'].update(self._mpl_key_callbacks) elif self.figure is not None: mpl_keypress_handler = self.figure.canvas.manager.key_press_handler_id - bmp = BoundMethodProxy(self.figure.canvas.manager.key_press) - self.figure.canvas.callbacks.callbacks['key_press_event'].update({mpl_keypress_handler: - bmp}) + try: + bmp = BoundMethodProxy(self.figure.canvas.manager.key_press) + self.figure.canvas.callbacks.callbacks['key_press_event'].update({mpl_keypress_handler: + bmp}) + except AttributeError as ex: + print(f"Error {ex} was raised when trying to connect the key_press handler. " + "Please file an issue on github. You may try a different matplotlib backend " + "as a temporary workaround") def __call__(self, figure=None, axis=None, clear=True, autorefresh=None, plotscale=1.0, override_plotkwargs=False, **kwargs): diff --git a/pyspeckit/spectrum/tests/test_eqw.py b/pyspeckit/spectrum/tests/test_eqw.py index ea0fdbef..7e725779 100644 --- a/pyspeckit/spectrum/tests/test_eqw.py +++ b/pyspeckit/spectrum/tests/test_eqw.py @@ -1,7 +1,7 @@ import numpy as np import warnings -from .. import Spectrum +from pyspeckit.spectrum import Spectrum def test_eqw(): From 1aa2a4498d529dbd15d503f37ec58d0c76a00613 Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 10 Aug 2023 07:53:49 -0400 Subject: [PATCH 13/32] try to get modern CI running (#391) * try to get modern CI running * rearrange tests * change tests * try to make tests start * update tests * check for attributes before using in widgets * tox * tests * split code out of vega_echelle * what directory structure does tox see? * try to cd? * pwd & ls * sdfsadfasf * remove changedir * use2to3 not needed * install pytest * another req * another req * another req * another req * setup: drop unneeded thing? * fix parallel: only works with fork, not with spawn * refactor ammonia_hf to avoid tex = 0 * test update * drop coverage * add print statement, drop coverage more * more removes * add spectral-cube * doodoodude * more matplotlib refactor --- .github/workflows/main.yml | 61 +++++++++++++++++++ docs/example_vega_echelle.rst | 41 +------------ pyspeckit/parallel_map/parallel_map.py | 16 ++--- pyspeckit/spectrum/baseline.py | 5 +- pyspeckit/spectrum/fitters.py | 10 ++- pyspeckit/spectrum/interactive.py | 7 ++- pyspeckit/spectrum/models/ammonia.py | 2 +- .../spectrum/models/ammonia_constants.py | 2 + pyspeckit/spectrum/models/ammonia_hf.py | 7 ++- pyspeckit/spectrum/widgets.py | 40 +++++++----- pyspeckit/tests | 2 +- setup.py | 9 +-- tox.ini | 54 ++++++++++++++++ 13 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 tox.ini diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..5bb8e07e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,61 @@ +name: Run tests + +on: + push: + pull_request: + schedule: + # run every Tuesday at 5am UTC + - cron: '0 5 * * 2' + +jobs: + tests: + name: ${{ matrix.name}} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + python-version: 3.9 + name: Py3.9 mindeps + toxenv: py39-test + - os: ubuntu-latest + python-version: '3.10' + name: Py3.10 mindeps + toxenv: py310-test + - os: ubuntu-latest + python-version: 3.11 + name: Py3.11 mindeps + toxenv: py311-test + - os: ubuntu-latest + python-version: 3.8 + name: Py3.8 mindeps + toxenv: py38-test + - os: ubuntu-latest + python-version: 3.9 + name: Py3.9 dev + toxenv: py39-test-dev + - os: ubuntu-latest + python-version: 3.11 + name: Py3.11 dev + toxenv: py311-test-dev + - os: ubuntu-latest + python-version: 3.9 + name: Documentation + toxenv: build_docs + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install testing dependencies + run: python -m pip install tox codecov + - name: Run tests with ${{ matrix.name }} + run: tox -v -e ${{ matrix.toxenv }} + - name: Upload coverage to codecov + if: ${{ contains(matrix.toxenv,'-cov') }} + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml diff --git a/docs/example_vega_echelle.rst b/docs/example_vega_echelle.rst index 54f192db..50257e90 100644 --- a/docs/example_vega_echelle.rst +++ b/docs/example_vega_echelle.rst @@ -3,44 +3,8 @@ Optical Plotting - Echelle spectrum of Vega (in color!) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:: - - import pyspeckit - from pylab import * - import wav2rgb - - speclist = pyspeckit.wrappers.load_IRAF_multispec('evega.0039.rs.ec.dispcor.fits') - - for spec in speclist: - spec.units="Counts" - - SP = pyspeckit.Spectra(speclist) - SPa = pyspeckit.Spectra(speclist,xunits='angstroms',quiet=False) - - SP.plotter(figure=figure(1)) - SPa.plotter(figure=figure(2)) - - figure(3) - clf() - figure(4) - clf() - - #clr = [list(clr) for clr in matplotlib.cm.brg(linspace(0,1,len(speclist)))] - clr = [wav2rgb.wav2RGB(c) + [1.0] for c in linspace(380,780,len(speclist))][::-1] - for ii,(color,spec) in enumerate(zip(clr,speclist)): - spec.plotter(figure=figure(3), clear=False, reset=False, color=color, refresh=False) - - fig4=figure(4) - fig4.subplots_adjust(hspace=0.35,top=0.97,bottom=0.03) - spec.plotter(axis=subplot(10,1,ii%10+1), clear=False, reset=False, color=color, refresh=False) - spec.plotter.axis.yaxis.set_major_locator( matplotlib.ticker.MaxNLocator(4) ) - - if ii % 10 == 9: - spec.plotter.refresh() - spec.plotter.savefig('vega_subplots_%03i.png' % (ii/10+1)) - clf() - - spec.plotter.refresh() +.. import:: example_vega_echelle.py + :literal: .. figure:: images/vega_colorized.png @@ -77,4 +41,3 @@ Optical Plotting - Echelle spectrum of Vega (in color!) .. figure:: images/vega_subplots_010.png :height: 400 :align: center - diff --git a/pyspeckit/parallel_map/parallel_map.py b/pyspeckit/parallel_map/parallel_map.py index 8301f887..ef9803fd 100644 --- a/pyspeckit/parallel_map/parallel_map.py +++ b/pyspeckit/parallel_map/parallel_map.py @@ -14,7 +14,7 @@ # May raise ImportError import multiprocessing _multi=True - + # May raise NotImplementedError _ncpus = multiprocessing.cpu_count() except Exception as ex: @@ -24,6 +24,8 @@ __all__ = ('parallel_map',) +# https://stackoverflow.com/a/70876951/814354 +multiprocessing.set_start_method('fork') def worker(f, ii, chunk, out_q, err_q, lock): """ @@ -40,7 +42,7 @@ def worker(f, ii, chunk, out_q, err_q, lock): """ vals = [] - # iterate over slice + # iterate over slice for val in chunk: try: result = f(val) @@ -103,13 +105,13 @@ def run_tasks(procs, err_q, out_q, num): def parallel_map(function, sequence, numcores=None): """ A parallelized version of the native Python map function that - utilizes the Python multiprocessing module to divide and + utilizes the Python multiprocessing module to divide and conquer sequence. parallel_map does not yet support multiple argument sequences. :param function: callable function that accepts argument from iterable - :param sequence: iterable sequence + :param sequence: iterable sequence :param numcores: number of cores to use """ if not callable(function): @@ -131,7 +133,7 @@ def parallel_map(function, sequence, numcores=None): elif numcores is None: numcores = _ncpus - # Returns a started SyncManager object which can be used for sharing + # Returns a started SyncManager object which can be used for sharing # objects between processes. The returned manager object corresponds # to a spawned child process and has methods which will create shared # objects and return corresponding proxies. @@ -144,11 +146,11 @@ def parallel_map(function, sequence, numcores=None): err_q = manager.Queue() lock = manager.Lock() - # if sequence is less than numcores, only use len sequence number of + # if sequence is less than numcores, only use len sequence number of # processes if size < numcores: log.info("Reduced number of cores to {0}".format(size)) - numcores = size + numcores = size # group sequence into numcores-worth of chunks sequence = numpy.array_split(sequence, numcores) diff --git a/pyspeckit/spectrum/baseline.py b/pyspeckit/spectrum/baseline.py index 6f51b742..132f5123 100644 --- a/pyspeckit/spectrum/baseline.py +++ b/pyspeckit/spectrum/baseline.py @@ -505,7 +505,10 @@ def clearlegend(self): if self.blleg is not None: self.blleg.set_visible(False) if self.blleg in self.Spectrum.plotter.axis.artists: - self.Spectrum.plotter.axis.artists.remove(self.blleg) + if hasattr(self.Spectrum.plotter.axis.artists, 'remove'): + self.Spectrum.plotter.axis.artists.remove(self.blleg) + else: + self.blleg.remove() if self.Spectrum.plotter.autorefresh: self.Spectrum.plotter.refresh() def savefit(self): diff --git a/pyspeckit/spectrum/fitters.py b/pyspeckit/spectrum/fitters.py index ca704d6d..10e5c634 100644 --- a/pyspeckit/spectrum/fitters.py +++ b/pyspeckit/spectrum/fitters.py @@ -1423,13 +1423,19 @@ def clear(self, legend=True, components=True): if hasattr(self,'residualplot'): for L in self.residualplot: if L in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(L) + if hasattr(self.Spectrum.plotter.axis.lines, 'remove'): + self.Spectrum.plotter.axis.lines.remove(L) + else: + L.remove() def _clearcomponents(self): for pc in self._plotted_components: pc.set_visible(False) if pc in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(pc) + if hasattr(self.Spectrum.plotter.axis.lines, 'remove'): + self.Spectrum.plotter.axis.lines.remove(pc) + else: + pc.remove() if self.Spectrum.plotter.autorefresh: self.Spectrum.plotter.refresh() diff --git a/pyspeckit/spectrum/interactive.py b/pyspeckit/spectrum/interactive.py index 23554668..3e2dbf04 100644 --- a/pyspeckit/spectrum/interactive.py +++ b/pyspeckit/spectrum/interactive.py @@ -391,8 +391,11 @@ def clear_highlights(self): for p in self.button1plot: p.set_visible(False) if self.Spectrum.plotter.axis and p in self.Spectrum.plotter.axis.lines: - self.Spectrum.plotter.axis.lines.remove(p) - self.button1plot=[] # I should be able to just remove from the list... but it breaks the loop... + if hasattr(self.Spectrum.plotter.axis.lines, 'remove'): + self.Spectrum.plotter.axis.lines.remove(p) + else: + p.remove() + self.button1plot = [] # I should be able to just remove from the list... but it breaks the loop... self.Spectrum.plotter.refresh() def selectregion(self, xmin=None, xmax=None, xtype='wcs', highlight=False, diff --git a/pyspeckit/spectrum/models/ammonia.py b/pyspeckit/spectrum/models/ammonia.py index cf81c76e..8e5b24dd 100644 --- a/pyspeckit/spectrum/models/ammonia.py +++ b/pyspeckit/spectrum/models/ammonia.py @@ -29,9 +29,9 @@ import warnings from .ammonia_constants import (line_names, freq_dict, aval_dict, ortho_dict, + TCMB, voff_lines_dict, tau_wts_dict) -TCMB = 2.7315 # K def ammonia(xarr, trot=20, tex=None, ntot=14, width=1, xoff_v=0.0, fortho=0.0, tau=None, fillingfraction=None, return_tau=False, diff --git a/pyspeckit/spectrum/models/ammonia_constants.py b/pyspeckit/spectrum/models/ammonia_constants.py index ce6e258a..3c79cf08 100644 --- a/pyspeckit/spectrum/models/ammonia_constants.py +++ b/pyspeckit/spectrum/models/ammonia_constants.py @@ -2,6 +2,8 @@ from astropy import units as u import numpy as np +TCMB = 2.7326 # K + num_to_name = {0: 'zero', 1: 'one', 2: 'two', diff --git a/pyspeckit/spectrum/models/ammonia_hf.py b/pyspeckit/spectrum/models/ammonia_hf.py index d5c2c11e..91efeb21 100644 --- a/pyspeckit/spectrum/models/ammonia_hf.py +++ b/pyspeckit/spectrum/models/ammonia_hf.py @@ -20,6 +20,7 @@ from . import radex_modelgrid from . import model from .ammonia_constants import (line_names, freq_dict, aval_dict, ortho_dict, + TCMB, voff_lines_dict, tau_wts_dict, line_labels) from astropy import constants @@ -80,9 +81,9 @@ def nh3_vtau_multimodel(xarr, velocity, width, *args): for x in ('tex{0}'.format(ln), 'tau{0}'.format(ln)) ], - parlimited=[(False,False), (True,False),] + [(True, False),]*2*nlines, - parlimits=[(0,0), ]*(2+2*nlines), - shortvarnames=["v","\\sigma",] + [x + parlimited=[(False,False), (True,False),] + [(True, False),]*2*nlines, + parlimits=[(0,0), (0,0), ] + [(TCMB, 0), (0, 0)] * nlines, + shortvarnames=["v","\\sigma",] + [x for ln in linenames for x in ('T_{{ex}}({0})'.format(line_labels[ln]), diff --git a/pyspeckit/spectrum/widgets.py b/pyspeckit/spectrum/widgets.py index 8cbcb889..d7e4c9cc 100644 --- a/pyspeckit/spectrum/widgets.py +++ b/pyspeckit/spectrum/widgets.py @@ -7,7 +7,7 @@ class dictlist(list): def __init__(self, *args): list.__init__(self, *args) - + self._dict = {} self._dict_index = {} for ii,value in enumerate(self): @@ -61,7 +61,10 @@ def set_valmin(self, valmin): if self.valinit < self.valmin: self.valinit = (self.valmax-self.valmin)/2. + self.valmin if self.vline in self.ax.lines: - self.ax.lines.remove(self.vline) + if hasattr(self.ax.lines, 'remove'): + self.ax.lines.remove(self.vline) + else: + self.vline.remove() self.vline = self.ax.axvline(self.valinit,0,1, color='r', lw=1) def set_valmax(self, valmax): @@ -75,7 +78,10 @@ def set_valmax(self, valmax): if self.valinit > self.valmax: self.valinit = (self.valmax-self.valmin)/2. + self.valmin if self.vline in self.ax.lines: - self.ax.lines.remove(self.vline) + if hasattr(self.ax.lines, 'remove'): + self.ax.lines.remove(self.vline) + else: + self.vline.remove() self.vline = self.ax.axvline(self.valinit,0,1, color='r', lw=1) class FitterSliders(Widget): @@ -104,9 +110,9 @@ def __init__(self, specfit, targetfig, npars=1, toolfig=None, parlimitdict={}): matplotlib.rcParams['toolbar'] = 'None' self.toolfig = pyplot.figure(figsize=(6,3)) if hasattr(targetfig.canvas.manager,'window'): - if hasattr(targetfig.canvas.manager.window, 'title'): + if hasattr(targetfig.canvas.manager.window, 'title') and hasattr(targetfig.canvas, 'set_window_title'): self.toolfig.canvas.set_window_title("Fit Sliders for "+targetfig.canvas.manager.window.title()) - elif hasattr(targetfig.canvas.manager.window, 'windowTitle'): + elif hasattr(targetfig.canvas.manager.window, 'windowTitle') and hasattr(targetfig.canvas, 'set_window_title'): self.toolfig.canvas.set_window_title("Fit Sliders for "+targetfig.canvas.manager.window.windowTitle()) else: warnings.warn("Only Qt4 and TkAgg support window titles (apparently)") @@ -153,10 +159,11 @@ def reset(event): # during reset there can be a temporary invalid state # depending on the order of the reset so we turn off # validation for the resetting - validate = self.toolfig.subplotpars.validate - self.toolfig.subplotpars.validate = False - self.buttonreset.on_clicked(reset) - self.toolfig.subplotpars.validate = validate + if hasattr(self.toolfig.subplotpars, 'validate'): + validate = self.toolfig.subplotpars.validate + self.toolfig.subplotpars.validate = False + self.buttonreset.on_clicked(reset) + self.toolfig.subplotpars.validate = validate def clear_sliders(self): @@ -232,15 +239,15 @@ def update(value): else: vmax = 1 try: - self.sliders[name] = ModifiableSlider(ax, + self.sliders[name] = ModifiableSlider(ax, name, vmin, vmax, valinit=value) except ValueError: - self.sliders[name] = ModifiableSlider(ax, + self.sliders[name] = ModifiableSlider(ax, name, vmin.value, vmax.value, valinit=value) self.sliders[-1].on_changed(update) - + def get_values(self): return [s.val for s in self.sliders] @@ -287,7 +294,8 @@ def __init__(self, specfit, targetfig, toolfig=None, nsubplots=12): tbar = matplotlib.rcParams['toolbar'] # turn off the navigation toolbar for the toolfig matplotlib.rcParams['toolbar'] = 'None' self.toolfig = pyplot.figure(figsize=(6,3)) - self.toolfig.canvas.set_window_title("Fit Tools for "+targetfig.canvas.manager.window.title()) + if hasattr(self.toolfig.canvas, 'set_window_title'): + self.toolfig.canvas.set_window_title("Fit Tools for "+targetfig.canvas.manager.window.title()) self.toolfig.subplots_adjust(top=0.9,left=0.05,right=0.95) matplotlib.rcParams['toolbar'] = tbar else: @@ -301,7 +309,7 @@ def __init__(self, specfit, targetfig, toolfig=None, nsubplots=12): # buttons ruin everything. # fax = self.toolfig.add_axes([0.1, 0.05, 0.15, 0.075]) # self.buttonfit = Button(fax, 'Fit') - # + # # resetax = self.toolfig.add_axes([0.7, 0.05, 0.15, 0.075]) # self.buttonreset = Button(resetax, 'Reset') @@ -369,7 +377,7 @@ def reset(event): #menu = Menu(fig, menuitems) - self.axes = [self.toolfig.add_subplot(nsubplots,1,spnum, frame_on=False, navigate=False, xticks=[], yticks=[]) + self.axes = [self.toolfig.add_subplot(nsubplots,1,spnum, frame_on=False, navigate=False, xticks=[], yticks=[]) for spnum in xrange(1,nsubplots+1)] #self.axes = self.toolfig.add_axes([0,0,1,1]) @@ -447,4 +455,4 @@ def update_info_texts(self): # """ # A class to manipulate individual parameter values # """ -# def __init__(self, +# def __init__(self, diff --git a/pyspeckit/tests b/pyspeckit/tests index d3d30bed..c11ed9b3 160000 --- a/pyspeckit/tests +++ b/pyspeckit/tests @@ -1 +1 @@ -Subproject commit d3d30bed012e36411102111d70d5def42cb9d9d1 +Subproject commit c11ed9b3747a7b5bc18a48340ca3c2f207cd8c85 diff --git a/setup.py b/setup.py index 1a4b73c5..8ddbd1e8 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ import __builtin__ as builtins builtins._ASTROPY_SETUP_ = True -from astropy_helpers.setup_helpers import (register_commands, adjust_compiler, +from astropy_helpers.setup_helpers import (register_commands, get_debug_option, get_package_info) from astropy_helpers.git_helpers import get_git_devstr from astropy_helpers.version_helpers import generate_version_py @@ -59,10 +59,6 @@ # modify distutils' behavior. cmdclassd = register_commands(PACKAGENAME, VERSION, RELEASE) -# Adjust the compiler in case the default on this platform is to use a -# broken one. -adjust_compiler(PACKAGENAME) - # Freeze build information in version.py generate_version_py(PACKAGENAME, VERSION, RELEASE, get_debug_option(PACKAGENAME)) @@ -96,6 +92,8 @@ os.path.relpath(root, PACKAGENAME), filename)) package_info['package_data'][PACKAGENAME].extend(c_files) +print("Running setup") + setup(name=PACKAGENAME, version=VERSION, description=DESCRIPTION, @@ -110,7 +108,6 @@ long_description=LONG_DESCRIPTION, cmdclass=cmdclassd, zip_safe=False, - use_2to3=False, entry_points=entry_points, **package_info ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..142bf626 --- /dev/null +++ b/tox.ini @@ -0,0 +1,54 @@ +[tox] +envlist = + py{38,39,310,311}-test{,-dev} + build_docs + codestyle +requires = + setuptools >= 30.3.0 + pip >= 19.3.1 +set_env = + casa: PIP_EXTRA_INDEX_URL = {env:PIP_EXTRA_INDEX_URL:https://casa-pip.nrao.edu/repository/pypi-group/simple} + +[testenv] +passenv = HOME,DISPLAY,LC_ALL,LC_CTYPE,ON_TRAVIS +#changedir = .tmp/{envname} +description = run tests +deps = + numpy + astropy[all] + scipy + matplotlib + spectral-cube + radio-beam + pytest + pytest_remotedata + pytest_doctestplus + pytest_astropy_header +extras = + test + dev: dev + cov: cov + latest: latest +allowlist_externals = + pwd + ls +commands = + dev: pip install -U -i https://pypi.anaconda.org/astropy/simple astropy --pre + pip freeze + python setup.py test + +[testenv:build_docs] +changedir = + docs +description = + invoke sphinx-build to build the HTML docs +extras = + docs +commands = + sphinx-build -W -b html . _build/html {posargs} + +[testenv:codestyle] +deps = flake8 +skip_install = true +commands = + flake8 --max-line-length=100 pyspeckit From 69105273ceace4bb9743f2f51cb126a9f29d392e Mon Sep 17 00:00:00 2001 From: Tien-Hao Hsieh <79155188+tienhaohsieh@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:54:07 +0200 Subject: [PATCH 14/32] Update DCN model (#393) * Add dcn * update main frequency, and normalzing weights * update frequency --------- Co-authored-by: shawinchone@gmail.com --- pyspeckit/spectrum/models/dcn.py | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 pyspeckit/spectrum/models/dcn.py diff --git a/pyspeckit/spectrum/models/dcn.py b/pyspeckit/spectrum/models/dcn.py new file mode 100644 index 00000000..dc07bc98 --- /dev/null +++ b/pyspeckit/spectrum/models/dcn.py @@ -0,0 +1,106 @@ +""" +=========== +DCN fitter: +=========== +Reference for line params: + +Line strength and frequencies taken from CDMS in Aug. 17th 2023: +DCN v=0, tag: 28509 + +""" +from . import hyperfine +import astropy.units as u + +freq_dict_cen = { +''' +Frequency from CDMS without HFS +''' + 'J1-0': 72414.6936e6, + 'J2-1': 144828.0015e6, + 'J3-2': 217238.5378e6, + } + +freq_dict = { + ####### J 1-0 + 'J1-0_01': 72413.5040e6, + 'J1-0_02': 72414.9330e6, + 'J1-0_03': 72417.0280e6, + ####### J 2-1 + 'J2-1_01': 144826.5757e6, + 'J2-1_02': 144826.8216e6, + 'J2-1_03': 144828.0011e6, + 'J2-1_04': 144828.1106e6, + 'J2-1_05': 144828.9072e6, + 'J2-1_06': 144830.3326e6, + ####### J 3-2 + 'J3-2_01': 217236.9990e6, + 'J3-2_02': 217238.3000e6, + 'J3-2_03': 217238.5550e6, + 'J3-2_04': 217238.6120e6, + 'J3-2_05': 217239.0790e6, + 'J3-2_06': 217240.6220e6, + } + +line_strength_dict = { + ####### J 1-0 + 'J1-0_01': 0.33331025, + 'J1-0_02': 0.55558213, + 'J1-0_03': 0.11110761, + ####### J 2-1 + 'J2-1_01': 0.08333412, + 'J2-1_02': 0.11110224, + 'J2-1_03': 0.24999011, + 'J2-1_04': 0.46668391, + 'J2-1_05': 0.0055555, + 'J2-1_06': 0.08333412, + ####### J 3-2 + 'J3-2_01': 0.0370362, + 'J3-2_02': 0.19999833, + 'J3-2_03': 0.2962964, + 'J3-2_04': 0.42857454, + 'J3-2_05': 0.00105834, + 'J3-2_06': 0.0370362, + } + + +# Get offset velocity dictionary in km/s based on the lines frequencies and rest frequency +conv_J10 = u.doppler_radio(freq_dict_cen['J1-0']*u.Hz) +conv_J21 = u.doppler_radio(freq_dict_cen['J2-1']*u.Hz) +conv_J32 = u.doppler_radio(freq_dict_cen['J3-2']*u.Hz) + +voff_lines_dict = { + name: ((freq_dict[name]*u.Hz).to(u.km/u.s, equivalencies=conv_J10).value) for name in freq_dict.keys() if "J1-0" in name + } +voff_lines_dict.update({ + name: ((freq_dict[name]*u.Hz).to(u.km/u.s, equivalencies=conv_J21).value) for name in freq_dict.keys() if "J2-1" in name + }) +voff_lines_dict.update({ + name: ((freq_dict[name]*u.Hz).to(u.km/u.s, equivalencies=conv_J32).value) for name in freq_dict.keys() if "J3-2" in name + }) + +# relative_strength_total_degeneracy is not used in the CLASS implementation +# of the hfs fit. It is the sum of the degeneracy values for all hyperfines +# for a given line; it gives the relative weights between lines. +# Hyperfine weights are treated as normalized within one rotational transition. +w10 = sum(val for name,val in line_strength_dict.items() if 'J1-0' in name) +w21 = sum(val for name,val in line_strength_dict.items() if 'J2-1' in name) +w32 = sum(val for name,val in line_strength_dict.items() if 'J3-2' in name) +relative_strength_total_degeneracy = { + name : w10 for name in line_strength_dict.keys() if "J1-0" in name + } +relative_strength_total_degeneracy.update({ + name : w21 for name in line_strength_dict.keys() if "J2-1" in name + }) +relative_strength_total_degeneracy.update({ + name : w32 for name in line_strength_dict.keys() if "J3-2" in name + }) + +# Get the list of line names from the previous lists +line_names = [name for name in voff_lines_dict.keys()] + +dcn_vtau = hyperfine.hyperfinemodel(line_names, voff_lines_dict, freq_dict, + line_strength_dict, + relative_strength_total_degeneracy) +dcn_vtau_fitter = dcn_vtau.fitter +dcn_vtau_vheight_fitter = dcn_vtau.vheight_fitter +dcn_vtau_tbg_fitter = dcn_vtau.background_fitter From 262a94997bda663ed5b7501cb661fbf673c244b8 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Fri, 25 Aug 2023 14:56:10 -0400 Subject: [PATCH 15/32] bugfix: do not set multiprocessing context on import --- pyspeckit/parallel_map/parallel_map.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyspeckit/parallel_map/parallel_map.py b/pyspeckit/parallel_map/parallel_map.py index ef9803fd..e0e6f7e9 100644 --- a/pyspeckit/parallel_map/parallel_map.py +++ b/pyspeckit/parallel_map/parallel_map.py @@ -24,9 +24,6 @@ __all__ = ('parallel_map',) -# https://stackoverflow.com/a/70876951/814354 -multiprocessing.set_start_method('fork') - def worker(f, ii, chunk, out_q, err_q, lock): """ A worker function that maps an input function over a @@ -133,6 +130,10 @@ def parallel_map(function, sequence, numcores=None): elif numcores is None: numcores = _ncpus + # https://stackoverflow.com/a/70876951/814354 + # if this step fails, parallel_map won't work - it _must_ use forking, not spawning + multiprocessing.set_start_method('fork', force=True) + # Returns a started SyncManager object which can be used for sharing # objects between processes. The returned manager object corresponds # to a spawned child process and has methods which will create shared From 731451442bcc937e18eb44cd3e789265570437f6 Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Sat, 16 Sep 2023 10:07:50 -0400 Subject: [PATCH 16/32] refactor config.py for modern compatibiltyi (#399) --- pyspeckit/config.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyspeckit/config.py b/pyspeckit/config.py index df952444..cab8980f 100644 --- a/pyspeckit/config.py +++ b/pyspeckit/config.py @@ -117,13 +117,11 @@ def decorator(self, *args, **kwargs): have default values """ - with warnings.catch_warnings(): - # ignore deprecation warning: getargspec does what we want here and - # we have to jump through hoops to get "signature" to work. - # This is not python4-compatible, but I don't have internet access - # now and can't figure out how to make it so. - warnings.simplefilter('ignore') - all_args, all_vars, all_keys, all_defs = inspect.getargspec(f) + argspec = inspect.getfullargspec(f) + + all_args = argspec.args + all_defs = argspec.defaults + all_args.pop(0) # pop self # Construct dictionary containing all of f's keyword arguments From 28dcc34786d0cbfe48d21b70abcfd45ec2d2b14d Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Thu, 28 Sep 2023 17:52:43 -0400 Subject: [PATCH 17/32] fix type error --- docs/example_fromscratch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/example_fromscratch.rst b/docs/example_fromscratch.rst index 335daa28..bdaca2af 100644 --- a/docs/example_fromscratch.rst +++ b/docs/example_fromscratch.rst @@ -10,7 +10,7 @@ This example shows the initialization of a pyspeckit object from numpy arrays. import numpy as np import pyspeckit - xaxis = np.linspace(-50,150,100.) + xaxis = np.linspace(-50,150,100) sigma = 10. center = 50. synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) From 3f920d502c8559e17724271418c4831c37f350b4 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Thu, 28 Sep 2023 17:55:14 -0400 Subject: [PATCH 18/32] move example to py --- docs/example_fromscratch.py | 35 ++++++++++++++++++++++++++++++++ docs/example_fromscratch.rst | 39 ++---------------------------------- 2 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 docs/example_fromscratch.py diff --git a/docs/example_fromscratch.py b/docs/example_fromscratch.py new file mode 100644 index 00000000..0f5f07b3 --- /dev/null +++ b/docs/example_fromscratch.py @@ -0,0 +1,35 @@ +import numpy as np +import pyspeckit + +xaxis = np.linspace(-50, 150, 100) +sigma = 10. +center = 50. +synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) + +# Add noise +stddev = 0.1 +noise = np.random.randn(xaxis.size)*stddev +error = stddev*np.ones_like(synth_data) +data = noise+synth_data + +# this will give a "blank header" warning, which is fine +sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, + xarrkwargs={'unit':'km/s'}, + unit='erg/s/cm^2/AA') + +sp.plotter() + +# Fit with automatic guesses +sp.specfit(fittype='gaussian') + +# Fit with input guesses +# The guesses initialize the fitter +# This approach uses the 0th, 1st, and 2nd moments +amplitude_guess = data.max() +center_guess = (data*xaxis).sum()/data.sum() +width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 +guesses = [amplitude_guess, center_guess, width_guess] +sp.specfit(fittype='gaussian', guesses=guesses) + +sp.plotter(errstyle='fill') +sp.specfit.plot_fit() diff --git a/docs/example_fromscratch.rst b/docs/example_fromscratch.rst index bdaca2af..441335ef 100644 --- a/docs/example_fromscratch.rst +++ b/docs/example_fromscratch.rst @@ -5,40 +5,5 @@ Creating a Spectrum from scratch This example shows the initialization of a pyspeckit object from numpy arrays. -.. code-block:: python - - import numpy as np - import pyspeckit - - xaxis = np.linspace(-50,150,100) - sigma = 10. - center = 50. - synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) - - # Add noise - stddev = 0.1 - noise = np.random.randn(xaxis.size)*stddev - error = stddev*np.ones_like(synth_data) - data = noise+synth_data - - # this will give a "blank header" warning, which is fine - sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, - xarrkwargs={'unit':'km/s'}, - unit='erg/s/cm^2/AA') - - sp.plotter() - - # Fit with automatic guesses - sp.specfit(fittype='gaussian') - - # Fit with input guesses - # The guesses initialize the fitter - # This approach uses the 0th, 1st, and 2nd moments - amplitude_guess = data.max() - center_guess = (data*xaxis).sum()/data.sum() - width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 - guesses = [amplitude_guess, center_guess, width_guess] - sp.specfit(fittype='gaussian', guesses=guesses) - - sp.plotter(errstyle='fill') - sp.specfit.plot_fit() +.. include:: example_fromscratch.py + :literal: From f800462ceeaf2e70f20618fc05bc31cca2ef2cfe Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 28 Sep 2023 17:59:06 -0400 Subject: [PATCH 19/32] split code out into its own script (#392) --- docs/example_continuum_fromscratch.py | 50 ++++++++++++++++++++++++ docs/example_continuum_fromscratch.rst | 53 +------------------------- 2 files changed, 52 insertions(+), 51 deletions(-) create mode 100644 docs/example_continuum_fromscratch.py diff --git a/docs/example_continuum_fromscratch.py b/docs/example_continuum_fromscratch.py new file mode 100644 index 00000000..8bcec879 --- /dev/null +++ b/docs/example_continuum_fromscratch.py @@ -0,0 +1,50 @@ +import numpy as np +import pyspeckit + +xaxis = np.linspace(-50,150,100) +sigma = 10. +center = 50. + +baseline = np.poly1d([0.1, 0.25])(xaxis) + +synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) + baseline + +# Add noise +stddev = 0.1 +noise = np.random.randn(xaxis.size)*stddev +error = stddev*np.ones_like(synth_data) +data = noise+synth_data + +# this will give a "blank header" warning, which is fine +sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, + xarrkwargs={'unit':'km/s'}, + unit='erg/s/cm^2/AA') + +sp.plotter() + +sp.specfit.Registry.add_fitter('polycontinuum', + pyspeckit.models.polynomial_continuum.poly_fitter(), + 2) + +sp.specfit(fittype='polycontinuum', guesses=(0,0), exclude=[30, 70]) + +# subtract the model fit to create a new spectrum +sp_contsub = sp.copy() +sp_contsub.data -= sp.specfit.get_full_model() +sp_contsub.plotter() + +# Fit with automatic guesses +sp_contsub.specfit(fittype='gaussian') + +# Fit with input guesses +# The guesses initialize the fitter +# This approach uses the 0th, 1st, and 2nd moments +data = sp_contsub.data +amplitude_guess = data.max() +center_guess = (data*xaxis).sum()/data.sum() +width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 +guesses = [amplitude_guess, center_guess, width_guess] +sp_contsub.specfit(fittype='gaussian', guesses=guesses) + +sp_contsub.plotter(errstyle='fill') +sp_contsub.specfit.plot_fit() diff --git a/docs/example_continuum_fromscratch.rst b/docs/example_continuum_fromscratch.rst index 0d80a0dd..a51b9458 100644 --- a/docs/example_continuum_fromscratch.rst +++ b/docs/example_continuum_fromscratch.rst @@ -12,55 +12,6 @@ to the error on the polynomial fit parameters. No such parameters are accessible via the `pyspeckit.Spectrum.baseline` tools because they use `numpy.poly1d` to fit the data. -.. code-block:: python +.. include:: example_continuum_fromscratch.py + :literal: - import numpy as np - import pyspeckit - - xaxis = np.linspace(-50,150,100) - sigma = 10. - center = 50. - - baseline = np.poly1d([0.1, 0.25])(xaxis) - - synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) + baseline - - # Add noise - stddev = 0.1 - noise = np.random.randn(xaxis.size)*stddev - error = stddev*np.ones_like(synth_data) - data = noise+synth_data - - # this will give a "blank header" warning, which is fine - sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, - xarrkwargs={'unit':'km/s'}, - unit='erg/s/cm^2/AA') - - sp.plotter() - - sp.specfit.Registry.add_fitter('polycontinuum', - pyspeckit.models.polynomial_continuum.poly_fitter(), - 2) - - sp.specfit(fittype='polycontinuum', guesses=(0,0), exclude=[30, 70]) - - # subtract the model fit to create a new spectrum - sp_contsub = sp.copy() - sp_contsub.data -= sp.specfit.get_full_model() - sp_contsub.plotter() - - # Fit with automatic guesses - sp_contsub.specfit(fittype='gaussian') - - # Fit with input guesses - # The guesses initialize the fitter - # This approach uses the 0th, 1st, and 2nd moments - data = sp_contsub.data - amplitude_guess = data.max() - center_guess = (data*xaxis).sum()/data.sum() - width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 - guesses = [amplitude_guess, center_guess, width_guess] - sp_contsub.specfit(fittype='gaussian', guesses=guesses) - - sp_contsub.plotter(errstyle='fill') - sp_contsub.specfit.plot_fit() From 7fef274aa4f175dff37075742d6af43ebaab4add Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 28 Sep 2023 18:18:08 -0400 Subject: [PATCH 20/32] Push another document example into a script (#400) * fix type error * move example to py --- docs/example_fromscratch.py | 35 ++++++++++++++++++++++++++++++++ docs/example_fromscratch.rst | 39 ++---------------------------------- 2 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 docs/example_fromscratch.py diff --git a/docs/example_fromscratch.py b/docs/example_fromscratch.py new file mode 100644 index 00000000..0f5f07b3 --- /dev/null +++ b/docs/example_fromscratch.py @@ -0,0 +1,35 @@ +import numpy as np +import pyspeckit + +xaxis = np.linspace(-50, 150, 100) +sigma = 10. +center = 50. +synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) + +# Add noise +stddev = 0.1 +noise = np.random.randn(xaxis.size)*stddev +error = stddev*np.ones_like(synth_data) +data = noise+synth_data + +# this will give a "blank header" warning, which is fine +sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, + xarrkwargs={'unit':'km/s'}, + unit='erg/s/cm^2/AA') + +sp.plotter() + +# Fit with automatic guesses +sp.specfit(fittype='gaussian') + +# Fit with input guesses +# The guesses initialize the fitter +# This approach uses the 0th, 1st, and 2nd moments +amplitude_guess = data.max() +center_guess = (data*xaxis).sum()/data.sum() +width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 +guesses = [amplitude_guess, center_guess, width_guess] +sp.specfit(fittype='gaussian', guesses=guesses) + +sp.plotter(errstyle='fill') +sp.specfit.plot_fit() diff --git a/docs/example_fromscratch.rst b/docs/example_fromscratch.rst index 335daa28..441335ef 100644 --- a/docs/example_fromscratch.rst +++ b/docs/example_fromscratch.rst @@ -5,40 +5,5 @@ Creating a Spectrum from scratch This example shows the initialization of a pyspeckit object from numpy arrays. -.. code-block:: python - - import numpy as np - import pyspeckit - - xaxis = np.linspace(-50,150,100.) - sigma = 10. - center = 50. - synth_data = np.exp(-(xaxis-center)**2/(sigma**2 * 2.)) - - # Add noise - stddev = 0.1 - noise = np.random.randn(xaxis.size)*stddev - error = stddev*np.ones_like(synth_data) - data = noise+synth_data - - # this will give a "blank header" warning, which is fine - sp = pyspeckit.Spectrum(data=data, error=error, xarr=xaxis, - xarrkwargs={'unit':'km/s'}, - unit='erg/s/cm^2/AA') - - sp.plotter() - - # Fit with automatic guesses - sp.specfit(fittype='gaussian') - - # Fit with input guesses - # The guesses initialize the fitter - # This approach uses the 0th, 1st, and 2nd moments - amplitude_guess = data.max() - center_guess = (data*xaxis).sum()/data.sum() - width_guess = (data.sum() / amplitude_guess / (2*np.pi))**0.5 - guesses = [amplitude_guess, center_guess, width_guess] - sp.specfit(fittype='gaussian', guesses=guesses) - - sp.plotter(errstyle='fill') - sp.specfit.plot_fit() +.. include:: example_fromscratch.py + :literal: From 5dfb362a8fe142bd7ca53b777dd5b1635df535f1 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Thu, 28 Sep 2023 20:18:39 -0400 Subject: [PATCH 21/32] tag v1.0.2 release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8ddbd1e8..cc680ec6 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ builtins._ASTROPY_PACKAGE_NAME_ = PACKAGENAME # VERSION should be PEP386 compatible (http://www.python.org/dev/peps/pep-0386) -VERSION = '1.0.2.dev' +VERSION = '1.0.2' # Indicates if this version is a release version RELEASE = 'dev' not in VERSION From 8a9e800b496ab10af7f974a58ac903a7db952b31 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Thu, 28 Sep 2023 20:20:55 -0400 Subject: [PATCH 22/32] return to dev --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cc680ec6..234ca587 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ builtins._ASTROPY_PACKAGE_NAME_ = PACKAGENAME # VERSION should be PEP386 compatible (http://www.python.org/dev/peps/pep-0386) -VERSION = '1.0.2' +VERSION = '1.0.3.dev' # Indicates if this version is a release version RELEASE = 'dev' not in VERSION From df063f4828b862ba1ce5c6ee1435332ee3666448 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Mon, 13 Nov 2023 15:21:44 -0500 Subject: [PATCH 23/32] np.int -> int --- pyspeckit/spectrum/models/formaldehyde_mm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyspeckit/spectrum/models/formaldehyde_mm.py b/pyspeckit/spectrum/models/formaldehyde_mm.py index a1b97a5f..7cccf27c 100644 --- a/pyspeckit/spectrum/models/formaldehyde_mm.py +++ b/pyspeckit/spectrum/models/formaldehyde_mm.py @@ -208,10 +208,10 @@ def formaldehyde_mm_despotic_functions(gridtable): GridData_tau_321_220 = np.zeros((len(DensArr), len(ColArr), len(TempArr), len(DvArr))) + np.nan - ii = np.interp(gridtable['nH2'], DensArr, np.arange(len(DensArr))).astype(np.int) - jj = np.interp(gridtable['Column'], ColArr, np.arange(len(ColArr))).astype(np.int) - kk = np.interp(gridtable['Temperature'], TempArr, np.arange(len(TempArr))).astype(np.int) - ll = np.interp(gridtable['sigmaNT'], DvArr, np.arange(len(DvArr))).astype(np.int) + ii = np.interp(gridtable['nH2'], DensArr, np.arange(len(DensArr))).astype(int) + jj = np.interp(gridtable['Column'], ColArr, np.arange(len(ColArr))).astype(int) + kk = np.interp(gridtable['Temperature'], TempArr, np.arange(len(TempArr))).astype(int) + ll = np.interp(gridtable['sigmaNT'], DvArr, np.arange(len(DvArr))).astype(int) GridData_Tex_303_202[ii, jj, kk, ll] = gridtable['Tex_303_202'] GridData_Tex_322_221[ii, jj, kk, ll] = gridtable['Tex_322_221'] From a6059fb596debb3787f59af36e4d0b1b8aa4e43e Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Tue, 28 Nov 2023 19:41:06 -0500 Subject: [PATCH 24/32] Remove astropy-helpers (#402) * try upgrading astropy-helpers to 4.0.1 * try upgrading astropy-helpers to 4.0.1: part 2 * remove astropy helpers * drop astropy-helpers * remove _astropy_init stuff * add version to setup.cfg * fix name metadata * .dev version --- .gitmodules | 3 - ah_bootstrap.py | 986 ------------------------------------------ astropy_helpers | 1 - pyspeckit/__init__.py | 44 +- setup.cfg | 111 ++++- setup.py | 152 ++----- 6 files changed, 165 insertions(+), 1132 deletions(-) delete mode 100644 ah_bootstrap.py delete mode 160000 astropy_helpers diff --git a/.gitmodules b/.gitmodules index 7bf3cfa8..4bcb20e0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "astropy_helpers"] - path = astropy_helpers - url = https://github.com/astropy/astropy-helpers.git [submodule "tests"] path = pyspeckit/tests url = https://github.com/pyspeckit/pyspeckit-tests diff --git a/ah_bootstrap.py b/ah_bootstrap.py deleted file mode 100644 index 2cea5bd7..00000000 --- a/ah_bootstrap.py +++ /dev/null @@ -1,986 +0,0 @@ -""" -This bootstrap module contains code for ensuring that the astropy_helpers -package will be importable by the time the setup.py script runs. It also -includes some workarounds to ensure that a recent-enough version of setuptools -is being used for the installation. - -This module should be the first thing imported in the setup.py of distributions -that make use of the utilities in astropy_helpers. If the distribution ships -with its own copy of astropy_helpers, this module will first attempt to import -from the shipped copy. However, it will also check PyPI to see if there are -any bug-fix releases on top of the current version that may be useful to get -past platform-specific bugs that have been fixed. When running setup.py, use -the ``--offline`` command-line option to disable the auto-upgrade checks. - -When this module is imported or otherwise executed it automatically calls a -main function that attempts to read the project's setup.cfg file, which it -checks for a configuration section called ``[ah_bootstrap]`` the presences of -that section, and options therein, determine the next step taken: If it -contains an option called ``auto_use`` with a value of ``True``, it will -automatically call the main function of this module called -`use_astropy_helpers` (see that function's docstring for full details). -Otherwise no further action is taken and by default the system-installed version -of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers`` -may be called manually from within the setup.py script). - -This behavior can also be controlled using the ``--auto-use`` and -``--no-auto-use`` command-line flags. For clarity, an alias for -``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using -the latter if needed. - -Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same -names as the arguments to `use_astropy_helpers`, and can be used to configure -the bootstrap script when ``auto_use = True``. - -See https://github.com/astropy/astropy-helpers for more details, and for the -latest version of this module. -""" - -import contextlib -import errno -import io -import locale -import os -import re -import subprocess as sp -import sys - -__minimum_python_version__ = (2, 7) - -if sys.version_info < __minimum_python_version__: - print("ERROR: Python {} or later is required by astropy-helpers".format( - __minimum_python_version__)) - sys.exit(1) - -try: - from ConfigParser import ConfigParser, RawConfigParser -except ImportError: - from configparser import ConfigParser, RawConfigParser - - -if sys.version_info[0] < 3: - _str_types = (str, unicode) - _text_type = unicode - PY3 = False -else: - _str_types = (str, bytes) - _text_type = str - PY3 = True - - -# What follows are several import statements meant to deal with install-time -# issues with either missing or misbehaving pacakges (including making sure -# setuptools itself is installed): - -# Check that setuptools 1.0 or later is present -from distutils.version import LooseVersion - -try: - import setuptools - assert LooseVersion(setuptools.__version__) >= LooseVersion('1.0') -except (ImportError, AssertionError): - print("ERROR: setuptools 1.0 or later is required by astropy-helpers") - sys.exit(1) - -# typing as a dependency for 1.6.1+ Sphinx causes issues when imported after -# initializing submodule with ah_boostrap.py -# See discussion and references in -# https://github.com/astropy/astropy-helpers/issues/302 - -try: - import typing # noqa -except ImportError: - pass - - -# Note: The following import is required as a workaround to -# https://github.com/astropy/astropy-helpers/issues/89; if we don't import this -# module now, it will get cleaned up after `run_setup` is called, but that will -# later cause the TemporaryDirectory class defined in it to stop working when -# used later on by setuptools -try: - import setuptools.py31compat # noqa -except ImportError: - pass - - -# matplotlib can cause problems if it is imported from within a call of -# run_setup(), because in some circumstances it will try to write to the user's -# home directory, resulting in a SandboxViolation. See -# https://github.com/matplotlib/matplotlib/pull/4165 -# Making sure matplotlib, if it is available, is imported early in the setup -# process can mitigate this (note importing matplotlib.pyplot has the same -# issue) -try: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot -except: - # Ignore if this fails for *any* reason* - pass - - -# End compatibility imports... - - -# In case it didn't successfully import before the ez_setup checks -import pkg_resources - -from setuptools import Distribution -from setuptools.package_index import PackageIndex - -from distutils import log -from distutils.debug import DEBUG - - -# TODO: Maybe enable checking for a specific version of astropy_helpers? -DIST_NAME = 'astropy-helpers' -PACKAGE_NAME = 'astropy_helpers' - -if PY3: - UPPER_VERSION_EXCLUSIVE = None -else: - UPPER_VERSION_EXCLUSIVE = '3' - -# Defaults for other options -DOWNLOAD_IF_NEEDED = True -INDEX_URL = 'https://pypi.python.org/simple' -USE_GIT = True -OFFLINE = False -AUTO_UPGRADE = True - -# A list of all the configuration options and their required types -CFG_OPTIONS = [ - ('auto_use', bool), ('path', str), ('download_if_needed', bool), - ('index_url', str), ('use_git', bool), ('offline', bool), - ('auto_upgrade', bool) -] - - -class _Bootstrapper(object): - """ - Bootstrapper implementation. See ``use_astropy_helpers`` for parameter - documentation. - """ - - def __init__(self, path=None, index_url=None, use_git=None, offline=None, - download_if_needed=None, auto_upgrade=None): - - if path is None: - path = PACKAGE_NAME - - if not (isinstance(path, _str_types) or path is False): - raise TypeError('path must be a string or False') - - if PY3 and not isinstance(path, _text_type): - fs_encoding = sys.getfilesystemencoding() - path = path.decode(fs_encoding) # path to unicode - - self.path = path - - # Set other option attributes, using defaults where necessary - self.index_url = index_url if index_url is not None else INDEX_URL - self.offline = offline if offline is not None else OFFLINE - - # If offline=True, override download and auto-upgrade - if self.offline: - download_if_needed = False - auto_upgrade = False - - self.download = (download_if_needed - if download_if_needed is not None - else DOWNLOAD_IF_NEEDED) - self.auto_upgrade = (auto_upgrade - if auto_upgrade is not None else AUTO_UPGRADE) - - # If this is a release then the .git directory will not exist so we - # should not use git. - git_dir_exists = os.path.exists(os.path.join(os.path.dirname(__file__), '.git')) - if use_git is None and not git_dir_exists: - use_git = False - - self.use_git = use_git if use_git is not None else USE_GIT - # Declared as False by default--later we check if astropy-helpers can be - # upgraded from PyPI, but only if not using a source distribution (as in - # the case of import from a git submodule) - self.is_submodule = False - - @classmethod - def main(cls, argv=None): - if argv is None: - argv = sys.argv - - config = cls.parse_config() - config.update(cls.parse_command_line(argv)) - - auto_use = config.pop('auto_use', False) - bootstrapper = cls(**config) - - if auto_use: - # Run the bootstrapper, otherwise the setup.py is using the old - # use_astropy_helpers() interface, in which case it will run the - # bootstrapper manually after reconfiguring it. - bootstrapper.run() - - return bootstrapper - - @classmethod - def parse_config(cls): - if not os.path.exists('setup.cfg'): - return {} - - cfg = ConfigParser() - - try: - cfg.read('setup.cfg') - except Exception as e: - if DEBUG: - raise - - log.error( - "Error reading setup.cfg: {0!r}\n{1} will not be " - "automatically bootstrapped and package installation may fail." - "\n{2}".format(e, PACKAGE_NAME, _err_help_msg)) - return {} - - if not cfg.has_section('ah_bootstrap'): - return {} - - config = {} - - for option, type_ in CFG_OPTIONS: - if not cfg.has_option('ah_bootstrap', option): - continue - - if type_ is bool: - value = cfg.getboolean('ah_bootstrap', option) - else: - value = cfg.get('ah_bootstrap', option) - - config[option] = value - - return config - - @classmethod - def parse_command_line(cls, argv=None): - if argv is None: - argv = sys.argv - - config = {} - - # For now we just pop recognized ah_bootstrap options out of the - # arg list. This is imperfect; in the unlikely case that a setup.py - # custom command or even custom Distribution class defines an argument - # of the same name then we will break that. However there's a catch22 - # here that we can't just do full argument parsing right here, because - # we don't yet know *how* to parse all possible command-line arguments. - if '--no-git' in argv: - config['use_git'] = False - argv.remove('--no-git') - - if '--offline' in argv: - config['offline'] = True - argv.remove('--offline') - - if '--auto-use' in argv: - config['auto_use'] = True - argv.remove('--auto-use') - - if '--no-auto-use' in argv: - config['auto_use'] = False - argv.remove('--no-auto-use') - - if '--use-system-astropy-helpers' in argv: - config['auto_use'] = False - argv.remove('--use-system-astropy-helpers') - - return config - - def run(self): - strategies = ['local_directory', 'local_file', 'index'] - dist = None - - # First, remove any previously imported versions of astropy_helpers; - # this is necessary for nested installs where one package's installer - # is installing another package via setuptools.sandbox.run_setup, as in - # the case of setup_requires - for key in list(sys.modules): - try: - if key == PACKAGE_NAME or key.startswith(PACKAGE_NAME + '.'): - del sys.modules[key] - except AttributeError: - # Sometimes mysterious non-string things can turn up in - # sys.modules - continue - - # Check to see if the path is a submodule - self.is_submodule = self._check_submodule() - - for strategy in strategies: - method = getattr(self, 'get_{0}_dist'.format(strategy)) - dist = method() - if dist is not None: - break - else: - raise _AHBootstrapSystemExit( - "No source found for the {0!r} package; {0} must be " - "available and importable as a prerequisite to building " - "or installing this package.".format(PACKAGE_NAME)) - - # This is a bit hacky, but if astropy_helpers was loaded from a - # directory/submodule its Distribution object gets a "precedence" of - # "DEVELOP_DIST". However, in other cases it gets a precedence of - # "EGG_DIST". However, when activing the distribution it will only be - # placed early on sys.path if it is treated as an EGG_DIST, so always - # do that - dist = dist.clone(precedence=pkg_resources.EGG_DIST) - - # Otherwise we found a version of astropy-helpers, so we're done - # Just active the found distribution on sys.path--if we did a - # download this usually happens automatically but it doesn't hurt to - # do it again - # Note: Adding the dist to the global working set also activates it - # (makes it importable on sys.path) by default. - - try: - pkg_resources.working_set.add(dist, replace=True) - except TypeError: - # Some (much) older versions of setuptools do not have the - # replace=True option here. These versions are old enough that all - # bets may be off anyways, but it's easy enough to work around just - # in case... - if dist.key in pkg_resources.working_set.by_key: - del pkg_resources.working_set.by_key[dist.key] - pkg_resources.working_set.add(dist) - - @property - def config(self): - """ - A `dict` containing the options this `_Bootstrapper` was configured - with. - """ - - return dict((optname, getattr(self, optname)) - for optname, _ in CFG_OPTIONS if hasattr(self, optname)) - - def get_local_directory_dist(self): - """ - Handle importing a vendored package from a subdirectory of the source - distribution. - """ - - if not os.path.isdir(self.path): - return - - log.info('Attempting to import astropy_helpers from {0} {1!r}'.format( - 'submodule' if self.is_submodule else 'directory', - self.path)) - - dist = self._directory_import() - - if dist is None: - log.warn( - 'The requested path {0!r} for importing {1} does not ' - 'exist, or does not contain a copy of the {1} ' - 'package.'.format(self.path, PACKAGE_NAME)) - elif self.auto_upgrade and not self.is_submodule: - # A version of astropy-helpers was found on the available path, but - # check to see if a bugfix release is available on PyPI - upgrade = self._do_upgrade(dist) - if upgrade is not None: - dist = upgrade - - return dist - - def get_local_file_dist(self): - """ - Handle importing from a source archive; this also uses setup_requires - but points easy_install directly to the source archive. - """ - - if not os.path.isfile(self.path): - return - - log.info('Attempting to unpack and import astropy_helpers from ' - '{0!r}'.format(self.path)) - - try: - dist = self._do_download(find_links=[self.path]) - except Exception as e: - if DEBUG: - raise - - log.warn( - 'Failed to import {0} from the specified archive {1!r}: ' - '{2}'.format(PACKAGE_NAME, self.path, str(e))) - dist = None - - if dist is not None and self.auto_upgrade: - # A version of astropy-helpers was found on the available path, but - # check to see if a bugfix release is available on PyPI - upgrade = self._do_upgrade(dist) - if upgrade is not None: - dist = upgrade - - return dist - - def get_index_dist(self): - if not self.download: - log.warn('Downloading {0!r} disabled.'.format(DIST_NAME)) - return None - - log.warn( - "Downloading {0!r}; run setup.py with the --offline option to " - "force offline installation.".format(DIST_NAME)) - - try: - dist = self._do_download() - except Exception as e: - if DEBUG: - raise - log.warn( - 'Failed to download and/or install {0!r} from {1!r}:\n' - '{2}'.format(DIST_NAME, self.index_url, str(e))) - dist = None - - # No need to run auto-upgrade here since we've already presumably - # gotten the most up-to-date version from the package index - return dist - - def _directory_import(self): - """ - Import astropy_helpers from the given path, which will be added to - sys.path. - - Must return True if the import succeeded, and False otherwise. - """ - - # Return True on success, False on failure but download is allowed, and - # otherwise raise SystemExit - path = os.path.abspath(self.path) - - # Use an empty WorkingSet rather than the man - # pkg_resources.working_set, since on older versions of setuptools this - # will invoke a VersionConflict when trying to install an upgrade - ws = pkg_resources.WorkingSet([]) - ws.add_entry(path) - dist = ws.by_key.get(DIST_NAME) - - if dist is None: - # We didn't find an egg-info/dist-info in the given path, but if a - # setup.py exists we can generate it - setup_py = os.path.join(path, 'setup.py') - if os.path.isfile(setup_py): - # We use subprocess instead of run_setup from setuptools to - # avoid segmentation faults - see the following for more details: - # https://github.com/cython/cython/issues/2104 - sp.check_output([sys.executable, 'setup.py', 'egg_info'], cwd=path) - - for dist in pkg_resources.find_distributions(path, True): - # There should be only one... - return dist - - return dist - - def _do_download(self, version='', find_links=None): - if find_links: - allow_hosts = '' - index_url = None - else: - allow_hosts = None - index_url = self.index_url - - # Annoyingly, setuptools will not handle other arguments to - # Distribution (such as options) before handling setup_requires, so it - # is not straightforward to programmatically augment the arguments which - # are passed to easy_install - class _Distribution(Distribution): - def get_option_dict(self, command_name): - opts = Distribution.get_option_dict(self, command_name) - if command_name == 'easy_install': - if find_links is not None: - opts['find_links'] = ('setup script', find_links) - if index_url is not None: - opts['index_url'] = ('setup script', index_url) - if allow_hosts is not None: - opts['allow_hosts'] = ('setup script', allow_hosts) - return opts - - if version: - req = '{0}=={1}'.format(DIST_NAME, version) - else: - if UPPER_VERSION_EXCLUSIVE is None: - req = DIST_NAME - else: - req = '{0}<{1}'.format(DIST_NAME, UPPER_VERSION_EXCLUSIVE) - - attrs = {'setup_requires': [req]} - - # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure - # it honours the options set in the [easy_install] section, and we need - # to explicitly fetch the requirement eggs as setup_requires does not - # get honored in recent versions of setuptools: - # https://github.com/pypa/setuptools/issues/1273 - - try: - - context = _verbose if DEBUG else _silence - with context(): - dist = _Distribution(attrs=attrs) - try: - dist.parse_config_files(ignore_option_errors=True) - dist.fetch_build_eggs(req) - except TypeError: - # On older versions of setuptools, ignore_option_errors - # doesn't exist, and the above two lines are not needed - # so we can just continue - pass - - # If the setup_requires succeeded it will have added the new dist to - # the main working_set - return pkg_resources.working_set.by_key.get(DIST_NAME) - except Exception as e: - if DEBUG: - raise - - msg = 'Error retrieving {0} from {1}:\n{2}' - if find_links: - source = find_links[0] - elif index_url != INDEX_URL: - source = index_url - else: - source = 'PyPI' - - raise Exception(msg.format(DIST_NAME, source, repr(e))) - - def _do_upgrade(self, dist): - # Build up a requirement for a higher bugfix release but a lower minor - # release (so API compatibility is guaranteed) - next_version = _next_version(dist.parsed_version) - - req = pkg_resources.Requirement.parse( - '{0}>{1},<{2}'.format(DIST_NAME, dist.version, next_version)) - - package_index = PackageIndex(index_url=self.index_url) - - upgrade = package_index.obtain(req) - - if upgrade is not None: - return self._do_download(version=upgrade.version) - - def _check_submodule(self): - """ - Check if the given path is a git submodule. - - See the docstrings for ``_check_submodule_using_git`` and - ``_check_submodule_no_git`` for further details. - """ - - if (self.path is None or - (os.path.exists(self.path) and not os.path.isdir(self.path))): - return False - - if self.use_git: - return self._check_submodule_using_git() - else: - return self._check_submodule_no_git() - - def _check_submodule_using_git(self): - """ - Check if the given path is a git submodule. If so, attempt to initialize - and/or update the submodule if needed. - - This function makes calls to the ``git`` command in subprocesses. The - ``_check_submodule_no_git`` option uses pure Python to check if the given - path looks like a git submodule, but it cannot perform updates. - """ - - cmd = ['git', 'submodule', 'status', '--', self.path] - - try: - log.info('Running `{0}`; use the --no-git option to disable git ' - 'commands'.format(' '.join(cmd))) - returncode, stdout, stderr = run_cmd(cmd) - except _CommandNotFound: - # The git command simply wasn't found; this is most likely the - # case on user systems that don't have git and are simply - # trying to install the package from PyPI or a source - # distribution. Silently ignore this case and simply don't try - # to use submodules - return False - - stderr = stderr.strip() - - if returncode != 0 and stderr: - # Unfortunately the return code alone cannot be relied on, as - # earlier versions of git returned 0 even if the requested submodule - # does not exist - - # This is a warning that occurs in perl (from running git submodule) - # which only occurs with a malformatted locale setting which can - # happen sometimes on OSX. See again - # https://github.com/astropy/astropy/issues/2749 - perl_warning = ('perl: warning: Falling back to the standard locale ' - '("C").') - if not stderr.strip().endswith(perl_warning): - # Some other unknown error condition occurred - log.warn('git submodule command failed ' - 'unexpectedly:\n{0}'.format(stderr)) - return False - - # Output of `git submodule status` is as follows: - # - # 1: Status indicator: '-' for submodule is uninitialized, '+' if - # submodule is initialized but is not at the commit currently indicated - # in .gitmodules (and thus needs to be updated), or 'U' if the - # submodule is in an unstable state (i.e. has merge conflicts) - # - # 2. SHA-1 hash of the current commit of the submodule (we don't really - # need this information but it's useful for checking that the output is - # correct) - # - # 3. The output of `git describe` for the submodule's current commit - # hash (this includes for example what branches the commit is on) but - # only if the submodule is initialized. We ignore this information for - # now - _git_submodule_status_re = re.compile( - '^(?P[+-U ])(?P[0-9a-f]{40}) ' - '(?P\S+)( .*)?$') - - # The stdout should only contain one line--the status of the - # requested submodule - m = _git_submodule_status_re.match(stdout) - if m: - # Yes, the path *is* a git submodule - self._update_submodule(m.group('submodule'), m.group('status')) - return True - else: - log.warn( - 'Unexpected output from `git submodule status`:\n{0}\n' - 'Will attempt import from {1!r} regardless.'.format( - stdout, self.path)) - return False - - def _check_submodule_no_git(self): - """ - Like ``_check_submodule_using_git``, but simply parses the .gitmodules file - to determine if the supplied path is a git submodule, and does not exec any - subprocesses. - - This can only determine if a path is a submodule--it does not perform - updates, etc. This function may need to be updated if the format of the - .gitmodules file is changed between git versions. - """ - - gitmodules_path = os.path.abspath('.gitmodules') - - if not os.path.isfile(gitmodules_path): - return False - - # This is a minimal reader for gitconfig-style files. It handles a few of - # the quirks that make gitconfig files incompatible with ConfigParser-style - # files, but does not support the full gitconfig syntax (just enough - # needed to read a .gitmodules file). - gitmodules_fileobj = io.StringIO() - - # Must use io.open for cross-Python-compatible behavior wrt unicode - with io.open(gitmodules_path) as f: - for line in f: - # gitconfig files are more flexible with leading whitespace; just - # go ahead and remove it - line = line.lstrip() - - # comments can start with either # or ; - if line and line[0] in (':', ';'): - continue - - gitmodules_fileobj.write(line) - - gitmodules_fileobj.seek(0) - - cfg = RawConfigParser() - - try: - cfg.readfp(gitmodules_fileobj) - except Exception as exc: - log.warn('Malformatted .gitmodules file: {0}\n' - '{1} cannot be assumed to be a git submodule.'.format( - exc, self.path)) - return False - - for section in cfg.sections(): - if not cfg.has_option(section, 'path'): - continue - - submodule_path = cfg.get(section, 'path').rstrip(os.sep) - - if submodule_path == self.path.rstrip(os.sep): - return True - - return False - - def _update_submodule(self, submodule, status): - if status == ' ': - # The submodule is up to date; no action necessary - return - elif status == '-': - if self.offline: - raise _AHBootstrapSystemExit( - "Cannot initialize the {0} submodule in --offline mode; " - "this requires being able to clone the submodule from an " - "online repository.".format(submodule)) - cmd = ['update', '--init'] - action = 'Initializing' - elif status == '+': - cmd = ['update'] - action = 'Updating' - if self.offline: - cmd.append('--no-fetch') - elif status == 'U': - raise _AHBootstrapSystemExit( - 'Error: Submodule {0} contains unresolved merge conflicts. ' - 'Please complete or abandon any changes in the submodule so that ' - 'it is in a usable state, then try again.'.format(submodule)) - else: - log.warn('Unknown status {0!r} for git submodule {1!r}. Will ' - 'attempt to use the submodule as-is, but try to ensure ' - 'that the submodule is in a clean state and contains no ' - 'conflicts or errors.\n{2}'.format(status, submodule, - _err_help_msg)) - return - - err_msg = None - cmd = ['git', 'submodule'] + cmd + ['--', submodule] - log.warn('{0} {1} submodule with: `{2}`'.format( - action, submodule, ' '.join(cmd))) - - try: - log.info('Running `{0}`; use the --no-git option to disable git ' - 'commands'.format(' '.join(cmd))) - returncode, stdout, stderr = run_cmd(cmd) - except OSError as e: - err_msg = str(e) - else: - if returncode != 0: - err_msg = stderr - - if err_msg is not None: - log.warn('An unexpected error occurred updating the git submodule ' - '{0!r}:\n{1}\n{2}'.format(submodule, err_msg, - _err_help_msg)) - -class _CommandNotFound(OSError): - """ - An exception raised when a command run with run_cmd is not found on the - system. - """ - - -def run_cmd(cmd): - """ - Run a command in a subprocess, given as a list of command-line - arguments. - - Returns a ``(returncode, stdout, stderr)`` tuple. - """ - - try: - p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) - # XXX: May block if either stdout or stderr fill their buffers; - # however for the commands this is currently used for that is - # unlikely (they should have very brief output) - stdout, stderr = p.communicate() - except OSError as e: - if DEBUG: - raise - - if e.errno == errno.ENOENT: - msg = 'Command not found: `{0}`'.format(' '.join(cmd)) - raise _CommandNotFound(msg, cmd) - else: - raise _AHBootstrapSystemExit( - 'An unexpected error occurred when running the ' - '`{0}` command:\n{1}'.format(' '.join(cmd), str(e))) - - - # Can fail of the default locale is not configured properly. See - # https://github.com/astropy/astropy/issues/2749. For the purposes under - # consideration 'latin1' is an acceptable fallback. - try: - stdio_encoding = locale.getdefaultlocale()[1] or 'latin1' - except ValueError: - # Due to an OSX oddity locale.getdefaultlocale() can also crash - # depending on the user's locale/language settings. See: - # http://bugs.python.org/issue18378 - stdio_encoding = 'latin1' - - # Unlikely to fail at this point but even then let's be flexible - if not isinstance(stdout, _text_type): - stdout = stdout.decode(stdio_encoding, 'replace') - if not isinstance(stderr, _text_type): - stderr = stderr.decode(stdio_encoding, 'replace') - - return (p.returncode, stdout, stderr) - - -def _next_version(version): - """ - Given a parsed version from pkg_resources.parse_version, returns a new - version string with the next minor version. - - Examples - ======== - >>> _next_version(pkg_resources.parse_version('1.2.3')) - '1.3.0' - """ - - if hasattr(version, 'base_version'): - # New version parsing from setuptools >= 8.0 - if version.base_version: - parts = version.base_version.split('.') - else: - parts = [] - else: - parts = [] - for part in version: - if part.startswith('*'): - break - parts.append(part) - - parts = [int(p) for p in parts] - - if len(parts) < 3: - parts += [0] * (3 - len(parts)) - - major, minor, micro = parts[:3] - - return '{0}.{1}.{2}'.format(major, minor + 1, 0) - - -class _DummyFile(object): - """A noop writeable object.""" - - errors = '' # Required for Python 3.x - encoding = 'utf-8' - - def write(self, s): - pass - - def flush(self): - pass - - -@contextlib.contextmanager -def _verbose(): - yield - -@contextlib.contextmanager -def _silence(): - """A context manager that silences sys.stdout and sys.stderr.""" - - old_stdout = sys.stdout - old_stderr = sys.stderr - sys.stdout = _DummyFile() - sys.stderr = _DummyFile() - exception_occurred = False - try: - yield - except: - exception_occurred = True - # Go ahead and clean up so that exception handling can work normally - sys.stdout = old_stdout - sys.stderr = old_stderr - raise - - if not exception_occurred: - sys.stdout = old_stdout - sys.stderr = old_stderr - - -_err_help_msg = """ -If the problem persists consider installing astropy_helpers manually using pip -(`pip install astropy_helpers`) or by manually downloading the source archive, -extracting it, and installing by running `python setup.py install` from the -root of the extracted source code. -""" - - -class _AHBootstrapSystemExit(SystemExit): - def __init__(self, *args): - if not args: - msg = 'An unknown problem occurred bootstrapping astropy_helpers.' - else: - msg = args[0] - - msg += '\n' + _err_help_msg - - super(_AHBootstrapSystemExit, self).__init__(msg, *args[1:]) - - -BOOTSTRAPPER = _Bootstrapper.main() - - -def use_astropy_helpers(**kwargs): - """ - Ensure that the `astropy_helpers` module is available and is importable. - This supports automatic submodule initialization if astropy_helpers is - included in a project as a git submodule, or will download it from PyPI if - necessary. - - Parameters - ---------- - - path : str or None, optional - A filesystem path relative to the root of the project's source code - that should be added to `sys.path` so that `astropy_helpers` can be - imported from that path. - - If the path is a git submodule it will automatically be initialized - and/or updated. - - The path may also be to a ``.tar.gz`` archive of the astropy_helpers - source distribution. In this case the archive is automatically - unpacked and made temporarily available on `sys.path` as a ``.egg`` - archive. - - If `None` skip straight to downloading. - - download_if_needed : bool, optional - If the provided filesystem path is not found an attempt will be made to - download astropy_helpers from PyPI. It will then be made temporarily - available on `sys.path` as a ``.egg`` archive (using the - ``setup_requires`` feature of setuptools. If the ``--offline`` option - is given at the command line the value of this argument is overridden - to `False`. - - index_url : str, optional - If provided, use a different URL for the Python package index than the - main PyPI server. - - use_git : bool, optional - If `False` no git commands will be used--this effectively disables - support for git submodules. If the ``--no-git`` option is given at the - command line the value of this argument is overridden to `False`. - - auto_upgrade : bool, optional - By default, when installing a package from a non-development source - distribution ah_boostrap will try to automatically check for patch - releases to astropy-helpers on PyPI and use the patched version over - any bundled versions. Setting this to `False` will disable that - functionality. If the ``--offline`` option is given at the command line - the value of this argument is overridden to `False`. - - offline : bool, optional - If `False` disable all actions that require an internet connection, - including downloading packages from the package index and fetching - updates to any git submodule. Defaults to `True`. - """ - - global BOOTSTRAPPER - - config = BOOTSTRAPPER.config - config.update(**kwargs) - - # Create a new bootstrapper with the updated configuration and run it - BOOTSTRAPPER = _Bootstrapper(**config) - BOOTSTRAPPER.run() diff --git a/astropy_helpers b/astropy_helpers deleted file mode 160000 index 231c409a..00000000 --- a/astropy_helpers +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 231c409a632dcbf2beae1c2dea5b843d81ede511 diff --git a/pyspeckit/__init__.py b/pyspeckit/__init__.py index 54824d53..cc830fb1 100644 --- a/pyspeckit/__init__.py +++ b/pyspeckit/__init__.py @@ -3,31 +3,29 @@ For details, see https://pyspeckit.readthedocs.io/ """ -from ._astropy_init import * __all__ = ['spectrum','cubes','wrappers'] -if not _ASTROPY_SETUP_: - from . import spectrum - from . import specwarnings - from . import cubes - from . import wrappers - from .wrappers import * - from .cubes import * - from .spectrum import * +from . import spectrum +from . import specwarnings +from . import cubes +from . import wrappers +from .wrappers import * +from .cubes import * +from .spectrum import * - try: - from .tests import run_tests +try: + from .tests import run_tests - def test(*args, **kwargs): - #import os - #os.chdir(os.path.split(os.path.abspath(tests.__file__))[0]) - from .spectrum.tests import test_eqw - test_eqw.test_eqw() - from .spectrum.models.tests import test_template - test_template.test_template() - test_template.test_template_withcont() - run_tests.test_everything() - except ImportError: - # This makes no sense. - pass + def test(*args, **kwargs): + #import os + #os.chdir(os.path.split(os.path.abspath(tests.__file__))[0]) + from .spectrum.tests import test_eqw + test_eqw.test_eqw() + from .spectrum.models.tests import test_template + test_template.test_template() + test_template.test_template_withcont() + run_tests.test_everything() +except ImportError: + # This makes no sense. + pass diff --git a/setup.cfg b/setup.cfg index 57cbcac0..43f75d2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,19 +3,8 @@ source-dir = docs build-dir = docs/_build all_files = 1 -[upload_docs] -upload-dir = docs/_build/html -show-response = 1 - -[tool:pytest] -python_files = *_example.py -minversion = 2.2 -norecursedirs = build docs/_build - -[ah_bootstrap] -auto_use = True - [metadata] +name = pyspeckit package_name = pyspeckit description = Toolkit for fitting and manipulating spectroscopic data in python long_description = See README.rst and CHANGES.rst @@ -25,4 +14,102 @@ license = BSD url = http://pyspeckit.readthedocs.org edit_on_github = True github_project = pyspeckit/pyspeckit +version = 1.0.3.dev + +[options] +zip_safe = False +packages = find: +install_requires = + astropy + numpy>=1.8.0 + radio_beam>=0.3.3 + six + dask[array] + joblib + casa-formats-io + +[options.extras_require] +test = + pytest-astropy + pytest-cov + regions>=0.7 + numpy>=1.24.0 + astropy>=5.2.1 +docs = + sphinx-astropy + matplotlib +noviz = + zarr + fsspec + distributed + pvextractor + reproject>=0.9.1 + scipy +viz = + aplpy + matplotlib + reproject + pvextractor +viz_extra = + glue-core[qt] + yt ; python_version<'3.8' +dev = + pvextractor + radio-beam + reproject + regions + +[options.package_data] +pyspeckit.tests = + data/* + data/*/* + +[upload_docs] +upload-dir = docs/_build/html +show-response = 1 + +[tool:pytest] +minversion = 3.0 +norecursedirs = build docs/_build +doctest_plus = enabled +addopts = -p no:warnings +python_files = *_example.py + +[coverage:run] +omit = + pyspeckit/__init__* + pyspeckit/conftest.py + pyspeckit/*setup* + pyspeckit/*/tests/* + pyspeckit/tests/test_* + pyspeckit/extern/* + pyspeckit/utils/compat/* + pyspeckit/version* + pyspeckit/wcs/docstrings* + pyspeckit/_erfa/* + */pyspeckit/__init__* + */pyspeckit/conftest.py + */pyspeckit/*setup* + */pyspeckit/*/tests/* + */pyspeckit/tests/test_* + */pyspeckit/extern/* + */pyspeckit/utils/compat/* + */pyspeckit/version* + */pyspeckit/wcs/docstrings* + */pyspeckit/_erfa/* +[coverage:report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + # Don't complain about packages we have installed + except ImportError + # Don't complain if tests don't hit assertions + raise AssertionError + raise NotImplementedError + # Don't complain about script hooks + def main\(.*\): + # Ignore branches that don't pertain to this version of Python + pragma: py{ignore_python_version} + # Don't complain about IPython completion helper + def _ipython_key_completions_ diff --git a/setup.py b/setup.py index 234ca587..566fc325 100644 --- a/setup.py +++ b/setup.py @@ -1,113 +1,51 @@ #!/usr/bin/env python -# Licensed under a 3-clause BSD style license - see LICENSE.rst -import glob import os import sys - -import ah_bootstrap from setuptools import setup -#A dirty hack to get around some early import/configurations ambiguities -if sys.version_info[0] >= 3: - import builtins -else: - import __builtin__ as builtins -builtins._ASTROPY_SETUP_ = True - -from astropy_helpers.setup_helpers import (register_commands, - get_debug_option, get_package_info) -from astropy_helpers.git_helpers import get_git_devstr -from astropy_helpers.version_helpers import generate_version_py - -# Get some values from the setup.cfg -try: - from ConfigParser import ConfigParser -except ImportError: - from configparser import ConfigParser -conf = ConfigParser() -conf.read(['setup.cfg']) -metadata = dict(conf.items('metadata')) - -PACKAGENAME = metadata.get('package_name', 'packagename') -DESCRIPTION = metadata.get('description', 'Astropy affiliated package') -AUTHOR = metadata.get('author', '') -AUTHOR_EMAIL = metadata.get('author_email', '') -LICENSE = metadata.get('license', 'unknown') -URL = metadata.get('url', 'http://astropy.org') - -# Get the long description from the package's docstring -__import__(PACKAGENAME) -package = sys.modules[PACKAGENAME] -LONG_DESCRIPTION = package.__doc__ - -# Store the package name in a built-in variable so it's easy -# to get from other parts of the setup infrastructure -builtins._ASTROPY_PACKAGE_NAME_ = PACKAGENAME - -# VERSION should be PEP386 compatible (http://www.python.org/dev/peps/pep-0386) -VERSION = '1.0.3.dev' - -# Indicates if this version is a release version -RELEASE = 'dev' not in VERSION - -if not RELEASE: - VERSION += get_git_devstr(False) - -# Populate the dict of setup command overrides; this should be done before -# invoking any other functionality from distutils since it can potentially -# modify distutils' behavior. -cmdclassd = register_commands(PACKAGENAME, VERSION, RELEASE) - -# Freeze build information in version.py -generate_version_py(PACKAGENAME, VERSION, RELEASE, - get_debug_option(PACKAGENAME)) - -# Treat everything in scripts except README.rst as a script to be installed -scripts = [fname for fname in glob.glob(os.path.join('scripts', '*')) - if os.path.basename(fname) != 'README.rst'] - - -# Get configuration information from all of the various subpackages. -# See the docstring for setup_helpers.update_package_files for more -# details. -package_info = get_package_info() - -# Add the project-global data -package_info['package_data'].setdefault(PACKAGENAME, []) -package_info['package_data'][PACKAGENAME].append('data/*') - -# Define entry points for command-line scripts -entry_points = {} - -# Include all .c files, recursively, including those generated by -# Cython, since we can not do this in MANIFEST.in with a "dynamic" -# directory name. -c_files = [] -for root, dirs, files in os.walk(PACKAGENAME): - for filename in files: - if filename.endswith('.c'): - c_files.append( - os.path.join( - os.path.relpath(root, PACKAGENAME), filename)) -package_info['package_data'][PACKAGENAME].extend(c_files) - -print("Running setup") - -setup(name=PACKAGENAME, - version=VERSION, - description=DESCRIPTION, - scripts=scripts, - requires=['astropy', 'matplotlib', 'numpy'], - install_requires=['astropy', 'numpy', 'matplotlib>=1.4', 'wheel'], - provides=[PACKAGENAME], - author=AUTHOR, - author_email=AUTHOR_EMAIL, - license=LICENSE, - url=URL, - long_description=LONG_DESCRIPTION, - cmdclass=cmdclassd, - zip_safe=False, - entry_points=entry_points, - **package_info -) +TEST_HELP = """ +Note: running tests is no longer done using 'python setup.py test'. Instead +you will need to run: + + tox -e test + +If you don't already have tox installed, you can install it with: + + pip install tox + +If you only want to run part of the test suite, you can also use pytest +directly with:: + + pip install -e . + pytest + +For more information, see: + + http://docs.astropy.org/en/latest/development/testguide.html#running-tests +""" + +if 'test' in sys.argv: + print(TEST_HELP) + sys.exit(1) + +DOCS_HELP = """ +Note: building the documentation is no longer done using +'python setup.py build_docs'. Instead you will need to run: + + tox -e build_docs + +If you don't already have tox installed, you can install it with: + + pip install tox + +For more information, see: + + http://docs.astropy.org/en/latest/install.html#builddocs +""" + +if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: + print(DOCS_HELP) + sys.exit(1) + +setup(use_scm_version={'write_to': os.path.join('pyspeckit', 'version.py')}) From 1370fb1a1652064912ef531b9660d541d96d7a36 Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Tue, 28 Nov 2023 19:41:54 -0500 Subject: [PATCH 25/32] v1.0.3 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 43f75d2b..d8cfff98 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ license = BSD url = http://pyspeckit.readthedocs.org edit_on_github = True github_project = pyspeckit/pyspeckit -version = 1.0.3.dev +version = 1.0.3 [options] zip_safe = False From 48e0d156ae98db415c6d13f02140f428eb8073cf Mon Sep 17 00:00:00 2001 From: "Adam Ginsburg (keflavich)" Date: Tue, 28 Nov 2023 19:42:45 -0500 Subject: [PATCH 26/32] v1.0.4.dev --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d8cfff98..525a690b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ license = BSD url = http://pyspeckit.readthedocs.org edit_on_github = True github_project = pyspeckit/pyspeckit -version = 1.0.3 +version = 1.0.4.dev [options] zip_safe = False From 2432eb03f0686754052672f94431b97f64edf4f7 Mon Sep 17 00:00:00 2001 From: Mike Chen Date: Wed, 3 Jan 2024 12:46:57 -0500 Subject: [PATCH 27/32] Corrected the order of the x & y coordinates entered into get_spectrum for the creation of _temp_moment (#406) --- pyspeckit/cubes/SpectralCube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyspeckit/cubes/SpectralCube.py b/pyspeckit/cubes/SpectralCube.py index ad169fcf..47e96faf 100644 --- a/pyspeckit/cubes/SpectralCube.py +++ b/pyspeckit/cubes/SpectralCube.py @@ -1228,7 +1228,7 @@ def momenteach(self, verbose=True, verbose_level=1, multicore=1, **kwargs): valid_pixels = zip(xx[OK],yy[OK]) # run the moment process to find out how many elements are in a moment - _temp_moment = self.get_spectrum(yy[OK][0],xx[OK][0]).moments(**kwargs) + _temp_moment = self.get_spectrum(xx[OK][0], yy[OK][0]).moments(**kwargs) self.momentcube = np.zeros((len(_temp_moment),)+self.mapplot.plane.shape) From 9ef42bea0c0ec3242a1b7558aa9cd891e77b370f Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 29 Feb 2024 08:40:45 -0500 Subject: [PATCH 28/32] bugfix: only integers in slices (#409) --- pyspeckit/spectrum/smooth.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyspeckit/spectrum/smooth.py b/pyspeckit/spectrum/smooth.py index d99f5de0..03cfd1e6 100644 --- a/pyspeckit/spectrum/smooth.py +++ b/pyspeckit/spectrum/smooth.py @@ -15,19 +15,19 @@ def smooth(data, smooth, smoothtype='gaussian', downsample=True, Parameters ---------- - smooth : float + smooth : float Number of pixels to smooth by smoothtype : [ 'gaussian','hanning', or 'boxcar' ] type of smoothing kernel to use - downsample : bool + downsample : bool Downsample the data? - downsample_factor : int + downsample_factor : int Downsample by the smoothing factor, or something else? convmode : [ 'full','valid','same' ] see :mod:`numpy.convolve`. 'same' returns an array of the same length as 'data' (assuming data is larger than the kernel) """ - + roundsmooth = int(round(smooth)) # can only downsample by integers if downsample_factor is None and downsample: @@ -47,9 +47,9 @@ def smooth(data, smooth, smoothtype='gaussian', downsample=True, if len(kernel) > len(data): lengthdiff = len(kernel)-len(data) if lengthdiff % 2 == 0: # make kernel same size as data - kernel = kernel[lengthdiff/2:-lengthdiff/2] + kernel = kernel[int(lengthdiff/2):-int(lengthdiff/2)] else: # make kernel 1 pixel smaller than data but still symmetric - kernel = kernel[lengthdiff/2+1:-lengthdiff/2-1] + kernel = kernel[int(lengthdiff/2)+1:-int(lengthdiff/2)-1] elif smoothtype == 'boxcar': kernel = np.ones(roundsmooth)/float(roundsmooth) From 238cad8213d5b67be5634846004621a219f1c74d Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Sun, 3 Mar 2024 13:42:35 -0500 Subject: [PATCH 29/32] make the molecule search function more robust & flexible (#404) * make the molecule search function more robust & flexible * fix some name errors * more verbose, add docs, pass on some kwargs, and handle CDMS formatting quirks * typo ina comment * change print -> log * add an option to return the table from the query code --- pyspeckit/spectrum/models/lte_molecule.py | 60 +++++++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/pyspeckit/spectrum/models/lte_molecule.py b/pyspeckit/spectrum/models/lte_molecule.py index a736e52a..beaaeaf0 100644 --- a/pyspeckit/spectrum/models/lte_molecule.py +++ b/pyspeckit/spectrum/models/lte_molecule.py @@ -199,7 +199,11 @@ def line_brightness_cgs(tex, dnu, frequency, tbg=2.73, *args, **kwargs): return (Jnu(frequency, tex)-Jnu(frequency, tbg)) * (1 - np.exp(-tau)) def get_molecular_parameters(molecule_name, tex=50, fmin=1*u.GHz, fmax=1*u.THz, - catalog='JPL',**kwargs): + catalog='JPL', molecule_tag=None, + parse_name_locally=True, + flags=0, + return_table=False, + **kwargs): """ Get the molecular parameters for a molecule from the JPL or CDMS catalog @@ -209,7 +213,8 @@ def get_molecular_parameters(molecule_name, tex=50, fmin=1*u.GHz, fmax=1*u.THz, ---------- molecule_name : string The string name of the molecule (normal name, like CH3OH or CH3CH2OH, - but it has to match the JPL catalog spec) + but it has to match the JPL catalog spec). Will use partial string + matching if the full string name fails. tex : float Optional excitation temperature (basically checks if the partition function calculator works) @@ -218,6 +223,16 @@ def get_molecular_parameters(molecule_name, tex=50, fmin=1*u.GHz, fmax=1*u.THz, fmin : quantity with frequency units fmax : quantity with frequency units The minimum and maximum frequency to search over + molecule_tag : int, optional + If specified, this will override the molecule name. You can specify + molecules based on the 'TAG' column in the JPL table + parse_name_locally : bool + Option passed to the query tool to specify whether to use regex to + search for the molecule name + flags : int + Regular expression flags to pass to the regex search + return_table : bool + Also return the parameter table? Examples -------- @@ -239,16 +254,38 @@ def get_molecular_parameters(molecule_name, tex=50, fmin=1*u.GHz, fmax=1*u.THz, speciestab = QueryTool.get_species_table() if 'NAME' in speciestab.colnames: - jpltable = speciestab[speciestab['NAME'] == molecule_name] + molcol = 'NAME' elif 'molecule' in speciestab.colnames: - jpltable = speciestab[speciestab['molecule'] == molecule_name] + molcol = 'molecule' else: raise ValueError(f"Did not find NAME or molecule in table columns: {speciestab.colnames}") - if len(jpltable) != 1: - raise ValueError(f"Too many or too few matches to {molecule_name}") - jpltbl = QueryTool.query_lines(fmin, fmax, molecule=molecule_name, - parse_name_locally=True) + if molecule_tag is not None: + tagcol = 'tag' if 'tag' in speciestab.colnames else 'TAG' + match = speciestab[tagcol] == molecule_tag + molecule_name = speciestab[match][molcol][0] + if catalog == 'CDMS': + molsearchname = f'{molecule_tag:06d} {molecule_name}' + else: + molsearchname = f'{molecule_tag} {molecule_name}' + parse_names_locally = False + if molecule_name is not None: + log.warn(f"molecule_tag overrides molecule_name. New molecule_name={molecule_name}. Searchname = {molsearchname}") + else: + log.info(f"molecule_name={molecule_name} for tag molecule_tag={molecule_tag}. Searchname = {molsearchname}") + else: + molsearchname = molecule_name + match = speciestab[molcol] == molecule_name + if match.sum() == 0: + # retry using partial string matching + match = np.core.defchararray.find(speciestab[molcol], molecule_name) != -1 + + if match.sum() != 1: + raise ValueError(f"Too many or too few matches ({match.sum()}) to {molecule_name}") + jpltable = speciestab[match] + + jpltbl = QueryTool.query_lines(fmin, fmax, molecule=molsearchname, + parse_name_locally=parse_name_locally) def partfunc(tem): """ @@ -300,7 +337,10 @@ def partfunc(tem): ok = np.isfinite(aij) & np.isfinite(EU) & np.isfinite(deg) & np.isfinite(freqs) - return freqs[ok], aij[ok], deg[ok], EU[ok], partfunc + if return_table: + return freqs[ok], aij[ok], deg[ok], EU[ok], partfunc, jpltbl[ok] + else: + return freqs[ok], aij[ok], deg[ok], EU[ok], partfunc @@ -440,7 +480,7 @@ def modfunc(xarr, vcen, width, tex, column): jnu = (jnu_line-jnu_bg) # this is the same as below, but laid out more explicitly. This form of the - # equation implicity subtracts of a uniform-with-frequency background + # equation implicity subtracts off a uniform-with-frequency background # model = jnu_line*(1-np.exp(-model_tau)) + jnu_bg*(np.exp(-model_tau)) - jnu_bg model = jnu*(1-np.exp(-model_tau)) From 342713015af8cbe55c31494d6f2c446ed75521a2 Mon Sep 17 00:00:00 2001 From: Mike Chen Date: Wed, 21 Aug 2024 20:08:35 -0400 Subject: [PATCH 30/32] Remove extra dimension from paralel_map's output (#413) --- pyspeckit/parallel_map/parallel_map.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyspeckit/parallel_map/parallel_map.py b/pyspeckit/parallel_map/parallel_map.py index e0e6f7e9..d121383d 100644 --- a/pyspeckit/parallel_map/parallel_map.py +++ b/pyspeckit/parallel_map/parallel_map.py @@ -92,12 +92,11 @@ def run_tasks(procs, err_q, out_q, num): idx, result = out_q.get() results[idx] = result + # Remove extra dimension added by array_split try: - # Remove extra dimension added by array_split return list(numpy.concatenate(results)) except ValueError: - return list(results) - + return [r for row in results for r in row] def parallel_map(function, sequence, numcores=None): """ From d9c355d55be8ee6c2e50e15538cfc0683141dda8 Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 5 Sep 2024 16:08:10 -0400 Subject: [PATCH 31/32] bugfix: force version to exist (#416) * bugfix: only integers in slices * fix for missing data in Q tables * add back version --- pyspeckit/__init__.py | 3 +++ pyspeckit/spectrum/models/lte_molecule.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/pyspeckit/__init__.py b/pyspeckit/__init__.py index cc830fb1..f67cd642 100644 --- a/pyspeckit/__init__.py +++ b/pyspeckit/__init__.py @@ -14,6 +14,9 @@ from .cubes import * from .spectrum import * +# added 20240905 because version somehow got dropped +from .version import version as __version__ + try: from .tests import run_tests diff --git a/pyspeckit/spectrum/models/lte_molecule.py b/pyspeckit/spectrum/models/lte_molecule.py index beaaeaf0..3d2e1138 100644 --- a/pyspeckit/spectrum/models/lte_molecule.py +++ b/pyspeckit/spectrum/models/lte_molecule.py @@ -297,7 +297,13 @@ def partfunc(tem): keys = [k for k in jpltable.keys() if 'q' in k.lower()] logQs = jpltable[keys] logQs = np.array(list(logQs[0])) + + # filter out NaNs (which occur for molecules with limited Q calculations) + tems = tems[np.isfinite(logQs)] + logQs = logQs[np.isfinite(logQs)] + inds = np.argsort(tems) + #logQ = np.interp(tem, tems[inds], logQs[inds]) # linear interpolation is appropriate; Q is linear with T... for some cases... # it's a safer interpolation, anyway. From 4e1ed1c9c4759728cea04197d00d5c5f867b43f9 Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Thu, 5 Sep 2024 16:08:10 -0400 Subject: [PATCH 32/32] force version to a hardcoded number rather than relying on the "automatic" versioning system --- pyspeckit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyspeckit/__init__.py b/pyspeckit/__init__.py index f67cd642..89183b51 100644 --- a/pyspeckit/__init__.py +++ b/pyspeckit/__init__.py @@ -15,7 +15,7 @@ from .spectrum import * # added 20240905 because version somehow got dropped -from .version import version as __version__ +__version__ = '1.0.4.dev' try: from .tests import run_tests