plotastrodata 1.9.20__tar.gz → 1.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {plotastrodata-1.9.20/plotastrodata.egg-info → plotastrodata-1.10.0}/PKG-INFO +5 -3
  2. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/README.md +2 -1
  3. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/__init__.py +1 -1
  4. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/analysis_utils.py +4 -3
  5. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/fitting_utils.py +45 -18
  6. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/other_utils.py +2 -2
  7. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/plot_utils.py +129 -57
  8. {plotastrodata-1.9.20 → plotastrodata-1.10.0/plotastrodata.egg-info}/PKG-INFO +5 -3
  9. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata.egg-info/requires.txt +2 -1
  10. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/setup.cfg +2 -1
  11. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/LICENSE +0 -0
  12. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/MANIFEST.in +0 -0
  13. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/const_utils.py +0 -0
  14. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/coord_utils.py +0 -0
  15. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/ext_utils.py +0 -0
  16. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/fft_utils.py +0 -0
  17. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/fits_utils.py +0 -0
  18. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/los_utils.py +0 -0
  19. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/matrix_utils.py +0 -0
  20. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata/noise_utils.py +0 -0
  21. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata.egg-info/SOURCES.txt +0 -0
  22. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata.egg-info/dependency_links.txt +0 -0
  23. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata.egg-info/not-zip-safe +0 -0
  24. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/plotastrodata.egg-info/top_level.txt +0 -0
  25. {plotastrodata-1.9.20 → plotastrodata-1.10.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.20
3
+ Version: 1.10.0
4
4
  Summary: plotastrodata is a tool for astronomers to create figures from FITS files and perform fundamental data analyses with ease.
5
5
  Home-page: https://github.com/yusukeaso-astron/plotastrodata
6
6
  Download-URL: https://github.com/yusukeaso-astron/plotastrodata
@@ -14,12 +14,13 @@ License-File: LICENSE
14
14
  Requires-Dist: astropy>=7.2
15
15
  Requires-Dist: corner
16
16
  Requires-Dist: dynesty
17
- Requires-Dist: emcee
17
+ Requires-Dist: emcee>=3.1.5
18
18
  Requires-Dist: matplotlib
19
19
  Requires-Dist: numpy>=2.0
20
20
  Requires-Dist: pillow
21
21
  Requires-Dist: plotly
22
22
  Requires-Dist: ptemcee
23
+ Requires-Dist: pydantic>=2
23
24
  Requires-Dist: scikit-image
24
25
  Requires-Dist: scipy
25
26
  Requires-Dist: tqdm
@@ -84,13 +85,14 @@ plotastrodata can do the following things.
84
85
  * astropy >= 7.2
85
86
  * corner (only for fitting)
86
87
  * dynesty (only for fitting)
87
- * emcee >= 3.0 (only for fitting)
88
+ * emcee >= 3.1.5 (only for fitting)
88
89
  * ffmpeg (only for movie)
89
90
  * matplotlib
90
91
  * numpy >= 2.0
91
92
  * pillow (only for RGB figures)
92
93
  * plotly (only for html cube)
93
94
  * ptemcee (only for fitting)
95
+ * pydantic >= 2
94
96
  * scikit-image (only for html cube)
95
97
  * scipy
96
98
  * tqdm (only for fitting)
@@ -56,13 +56,14 @@ plotastrodata can do the following things.
56
56
  * astropy >= 7.2
57
57
  * corner (only for fitting)
58
58
  * dynesty (only for fitting)
59
- * emcee >= 3.0 (only for fitting)
59
+ * emcee >= 3.1.5 (only for fitting)
60
60
  * ffmpeg (only for movie)
61
61
  * matplotlib
62
62
  * numpy >= 2.0
63
63
  * pillow (only for RGB figures)
64
64
  * plotly (only for html cube)
65
65
  * ptemcee (only for fitting)
66
+ * pydantic >= 2
66
67
  * scikit-image (only for html cube)
67
68
  * scipy
68
69
  * tqdm (only for fitting)
@@ -1,4 +1,4 @@
1
1
  import warnings
2
2
 
3
3
  warnings.simplefilter('ignore', FutureWarning)
4
- __version__ = '1.9.20'
4
+ __version__ = '1.10.0'
@@ -3,7 +3,8 @@ import warnings
3
3
  from dataclasses import dataclass
4
4
  from scipy.interpolate import RegularGridInterpolator as RGI
5
5
  from scipy.signal import convolve
6
- from typing import Any, Callable
6
+ from pydantic.dataclasses import dataclass as pydantic_dataclass
7
+ from typing import Any, Callable, Literal
7
8
 
8
9
  from plotastrodata import const_utils as cu
9
10
  from plotastrodata.coord_utils import coord2xy, rel2abs, xy2coord
@@ -621,7 +622,7 @@ ASTRODATA_ARGS = ['fitsimage', 'data', 'Tb', 'sigma', 'center', 'restfreq',
621
622
  'beam_org', 'fitsheader', 'pv', 'pvpa']
622
623
 
623
624
 
624
- @dataclass
625
+ @pydantic_dataclass
625
626
  class AstroFrame():
626
627
  """Parameter set to limit and reshape the data in the AstroData format.
627
628
 
@@ -662,7 +663,7 @@ class AstroFrame():
662
663
  yflip: bool = False
663
664
  swapxy: bool = False
664
665
  pv: bool = False
665
- quadrants: str | None = None
666
+ quadrants: Literal['13', '24'] | None = None
666
667
 
667
668
  def __post_init__(self) -> None:
668
669
  self.xdir = -1 if self.xflip else 1
@@ -2,9 +2,11 @@ import corner
2
2
  import emcee
3
3
  import matplotlib.pyplot as plt
4
4
  import numpy as np
5
+ import pickle
5
6
  import ptemcee
6
7
  import warnings
7
8
  from dynesty import DynamicNestedSampler as DNS
9
+ from functools import partial
8
10
  from multiprocessing import Pool
9
11
  from tqdm import tqdm
10
12
  from typing import Any, Callable
@@ -18,6 +20,25 @@ bar = None
18
20
  global_progressbar = True
19
21
 
20
22
 
23
+ def _gaussian_log_likelihood(x: np.ndarray, model: Callable,
24
+ xdata: np.ndarray, ydata: np.ndarray,
25
+ sigma: float | np.ndarray) -> float:
26
+ """Return a Gaussian log likelihood using pickleable arguments."""
27
+ chi2 = np.sum((ydata - model(xdata, *x))**2 / sigma**2)
28
+ return chi2 / (-2)
29
+
30
+
31
+ def _bounded_log_probability(x: np.ndarray, log_likelihood: Callable,
32
+ bounds: np.ndarray,
33
+ update_progress: bool = False) -> float:
34
+ """Combine a bounded uniform prior with a log likelihood."""
35
+ if update_progress:
36
+ bar.update(1)
37
+ if np.all((bounds[:, 0] < x) & (x < bounds[:, 1])):
38
+ return log_likelihood(x)
39
+ return -np.inf
40
+
41
+
21
42
  def logp(x: np.ndarray) -> float:
22
43
  """Log prior function made from the boundary (global_bounds) of fitting parameters.
23
44
 
@@ -95,9 +116,8 @@ class EmceeCorner():
95
116
  if logl is None and (model is not None
96
117
  and xdata is not None
97
118
  and ydata is not None):
98
- def logl(x: np.ndarray) -> float:
99
- chi2 = np.sum((ydata - model(xdata, *x))**2 / sigma**2)
100
- return chi2 / (-2)
119
+ logl = partial(_gaussian_log_likelihood, model=model,
120
+ xdata=xdata, ydata=ydata, sigma=sigma)
101
121
  self.bounds = global_bounds
102
122
  self.dim = len(self.bounds)
103
123
  self.logl = logl
@@ -125,21 +145,27 @@ class EmceeCorner():
125
145
  'logl': self.logl, 'logp': self.logp}
126
146
  else:
127
147
  if ncores > 1:
128
- print('Use logl as log_prob_fn to avoid function-in-function.')
129
- log_prob_fn = self.logl
130
- else:
131
- def log_prob_fn(x: np.ndarray) -> float:
132
- return self.logp(x) + self.logl(x)
148
+ try:
149
+ pickle.dumps(self.logl)
150
+ except (pickle.PicklingError, AttributeError, TypeError) as exc:
151
+ raise TypeError(
152
+ 'logl and model must be pickleable when ncores > 1. '
153
+ 'Define them at module scope instead of inside another '
154
+ 'function.') from exc
133
155
 
134
156
  sampler_cls = emcee.EnsembleSampler
135
157
  sampler_kwargs = {'nwalkers': nwalkers, 'ndim': self.dim,
136
- 'log_prob_fn': log_prob_fn}
158
+ 'log_prob_fn': _bounded_log_probability,
159
+ 'args': (self.logl, self.bounds,
160
+ global_progressbar and ncores == 1)}
137
161
  if ncores > 1:
138
162
  with Pool(ncores) as pool:
139
163
  sampler = sampler_cls(**sampler_kwargs, pool=pool)
164
+ # This run_mcmc is duplicated so sampling finishes before the pool closes.
165
+ sampler.run_mcmc(pos0, nsteps)
140
166
  else:
141
167
  sampler = sampler_cls(**sampler_kwargs, pool=None)
142
- sampler.run_mcmc(pos0, nsteps)
168
+ sampler.run_mcmc(pos0, nsteps)
143
169
  return sampler
144
170
 
145
171
  def _get_samples(self, sampler: Any, nburnin: int,
@@ -190,7 +216,7 @@ class EmceeCorner():
190
216
  ntry (int, optional): Number of trials for the Gelman-Rubin check. Defaults to 1.
191
217
  pos0 (np.nparray, optional): Initial parameter set in the shape of (ntemps, nwalkers, dim). Defaults to None.
192
218
  savechain (str, optional): File name of the chain in format of .npy. Existing files with the same name are overwritten by ``numpy.save``. Defaults to None.
193
- ncores (int, optional): Number of cores for multiprocessing.Pool. ncores=1 does not use multiprocessing. Defaults to 1.
219
+ ncores (int, optional): Number of cores for multiprocessing.Pool. ncores=1 does not use multiprocessing. For ncores > 1, user-supplied logl and model functions must be pickleable, such as functions defined at module scope. Defaults to 1.
194
220
  grcheck (bool, optional): Whether to check Gelman-Rubin statistics. Defaults to False.
195
221
  pt (bool, optional): Whether to use ptemcee; otherwise, emcee is used. Defaults to False.
196
222
  """
@@ -243,13 +269,14 @@ class EmceeCorner():
243
269
  labels = [f'Par {i:d}' for i in range(self.dim)]
244
270
  if cornerrange is None:
245
271
  cornerrange = self.bounds
246
- corner.corner(np.reshape(self.samples, (-1, self.dim)),
247
- truths=self.popt,
248
- quantiles=[self.percent[0] / 100,
249
- 0.5,
250
- self.percent[1] / 100],
251
- show_titles=True, labels=labels, range=cornerrange)
252
- close_figure(plt, savefig, show, tight=False)
272
+ fig = corner.corner(np.reshape(self.samples, (-1, self.dim)),
273
+ truths=self.popt,
274
+ quantiles=[self.percent[0] / 100,
275
+ 0.5,
276
+ self.percent[1] / 100],
277
+ show_titles=True, labels=labels,
278
+ range=cornerrange)
279
+ close_figure(fig, savefig, show, tight=False)
253
280
 
254
281
  def plotchain(self, labels: list | None = None, ylim: list | None = None,
255
282
  savefig: dict | str | None = None,
@@ -296,7 +296,7 @@ def close_figure(fig: object, savefig: dict | str | None = None,
296
296
  show: bool = False, tight: bool = True) -> None:
297
297
  """Save, show, and close the figure.
298
298
 
299
- If ``savefig`` is provided, the figure is saved with Matplotlib ``Figure.savefig``. Existing files with the same name are overwritten by Matplotlib. After optional saving/showing, the figure is closed with ``plt.close()``.
299
+ If ``savefig`` is provided, the figure is saved with Matplotlib ``Figure.savefig``. Existing files with the same name are overwritten by Matplotlib. After optional saving/showing, the figure is closed with ``plt.close(fig)``.
300
300
 
301
301
  Default keyword values:
302
302
  Figure.savefig: ``bbox_inches='tight'`` and ``transparent=True``. Values in ``savefig`` override these defaults.
@@ -316,4 +316,4 @@ def close_figure(fig: object, savefig: dict | str | None = None,
316
316
  fig.savefig(**savefig0)
317
317
  if show:
318
318
  plt.show()
319
- plt.close()
319
+ plt.close(fig)
@@ -1,9 +1,10 @@
1
1
  import matplotlib as mpl
2
2
  import matplotlib.pyplot as plt
3
3
  import numpy as np
4
- from dataclasses import dataclass
5
4
  from matplotlib.patches import Ellipse, Rectangle
6
- from typing import Any, TypeVar, Callable
5
+ from pydantic import Field, field_validator
6
+ from pydantic.dataclasses import dataclass as pydantic_dataclass
7
+ from typing import Any, Callable, Literal, TypeVar
7
8
 
8
9
  from plotastrodata.analysis_utils import AstroData, AstroFrame
9
10
  from plotastrodata.coord_utils import (coord2xy, xy2coord,
@@ -17,6 +18,13 @@ from plotastrodata.other_utils import (close_figure, listing,
17
18
  plt.ioff() # force to turn off interactive mode
18
19
 
19
20
  T = TypeVar('T')
21
+ Stretch = Literal['linear', 'log', 'asinh', 'power']
22
+ AxisScale = Literal['linear', 'log', 'symlog', 'asinh', 'logit']
23
+ FloatOrList = float | list[float]
24
+ OptionalFloatOrList = float | list[float | None] | None
25
+ BeamTriple = list[float | None]
26
+ BeamValue = BeamTriple | list[BeamTriple]
27
+ BeamPosition = list[float] | list[list[float] | None] | None
20
28
 
21
29
 
22
30
  def set_rcparams(fontsize: int = 18, nancolor: str = 'w',
@@ -187,24 +195,24 @@ def _get_vskipfill(nv: int, v_org: np.ndarray, vskip: int,
187
195
  return vskipfill
188
196
 
189
197
 
190
- @dataclass
198
+ @pydantic_dataclass
191
199
  class Stretcher():
192
200
  """Arguments and methods related to the stretch in PlotAstroData.add_color() and add_rgb().
193
201
 
194
202
  Args:
195
203
  stretch (str, optional): 'log', 'asinh', 'power', or 'linear'. Any other means 'linear'. 'log' means the mapped data are logarithmic. 'asinh' means the mapped data are arc sin hyperbolic. 'power' means the mapped data are power-law (see also stretchpower). Defaults to 'linear'.
196
- stretchscale (float, optional): The output is asinh(data / stretchscale). Defaults to None.
197
- stretchpower (float, optional): The output is data**stretchpower / stretchpower. 1 means the linear scale, while 0 means the logarithmic scale. Defaults to 0.5.
198
- vmin (float, optional): The minimum value for Axes.pcolormesh() of matplotlib. Defaults to None.
199
- vmax (float, optional): The maximum value for Axes.pcolormesh() of matplotlib. Defaults to None.
200
- sigma (float, optional): Noise level. Defaults to 0.
204
+ stretchscale (float or list, optional): The output is asinh(data / stretchscale). Defaults to None.
205
+ stretchpower (float or list, optional): The output is data**stretchpower / stretchpower. 1 means the linear scale, while 0 means the logarithmic scale. Defaults to 0.5.
206
+ vmin (float or list, optional): The minimum value for Axes.pcolormesh() of matplotlib. Defaults to None.
207
+ vmax (float or list, optional): The maximum value for Axes.pcolormesh() of matplotlib. Defaults to None.
208
+ sigma (float, list, or None, optional): Noise level. None means no noise level is used. Defaults to 0.
201
209
  """
202
- stretch: str = 'linear'
203
- stretchscale: float | None = None
204
- stretchpower: float = 0.5
205
- vmin: float | None = None
206
- vmax: float | None = None
207
- sigma: float = 0
210
+ stretch: Stretch | list[Stretch] = 'linear'
211
+ stretchscale: OptionalFloatOrList = None
212
+ stretchpower: FloatOrList = 0.5
213
+ vmin: OptionalFloatOrList = None
214
+ vmax: OptionalFloatOrList = None
215
+ sigma: OptionalFloatOrList = 0
208
216
 
209
217
  def __post_init__(self) -> None:
210
218
  self.n = 1 if isinstance(self.stretch, str) else len(self.stretch)
@@ -301,27 +309,88 @@ class Stretcher():
301
309
  return dataout, vminout, vmaxout
302
310
 
303
311
 
312
+ @pydantic_dataclass
304
313
  class Beam():
305
314
  """Arguments for PlotAstroData.add_beam().
306
315
 
307
316
  Args:
308
317
  show_beam (bool, optional): Defaults to True.
309
- beam (list, optional): [bmaj, bmin, bpa]. This may be a list of list. Defaults to [None, None, None].
310
- beamcolor (str, optional): matplotlib color. This may be a list of str. Defaults to 'gray'.
311
- beampos (list, optional): Relative position. This may be a list of list or a list of None. Defaults to None.
312
- beam_kwargs (dict, optional): Additional arguments for matplotlib.patches. Defaults to {}.
318
+ beam (list or np.ndarray, optional): One ``[bmaj, bmin, bpa]`` triple or a list of triples. ``bmaj`` and ``bmin`` must be positive; ``bpa`` is in degrees. ``[None, None, None]`` means no beam is available. Partial triples are not supported. Defaults to ``[None, None, None]``.
319
+ beamcolor (str or list of str, optional): Matplotlib color. One color is used for every beam; when a list is given, provide one color per beam. Defaults to ``'gray'``.
320
+ beampos (list or list of list, optional): One relative ``[x, y]`` position is used for every beam; when a list of positions is given, provide one position per beam. Each coordinate must be from 0 (left or bottom) to 1 (right or top). None selects the automatic position. Defaults to None.
321
+ beam_kwargs (dict, optional): Additional Matplotlib patch arguments. Defaults to {}.
313
322
  """
314
- def __init__(self,
315
- show_beam: bool = True,
316
- beam: list[float | None] = [None] * 3,
317
- beamcolor: str = 'gray',
318
- beampos: list[float] | None = None,
319
- beam_kwargs: dict = {}) -> None:
320
- self.show_beam = show_beam
321
- self.beam = beam
322
- self.beamcolor = beamcolor
323
- self.beampos = beampos
324
- self.beam_kwargs = beam_kwargs
323
+ show_beam: bool = True
324
+ beam: BeamValue = Field(default_factory=lambda: [None] * 3)
325
+ beamcolor: str | list[str] = 'gray'
326
+ beampos: BeamPosition = None
327
+ beam_kwargs: dict[str, Any] = Field(default_factory=dict)
328
+
329
+ @field_validator('beam', mode='before')
330
+ @classmethod
331
+ def _convert_beam_arrays(cls, value: Any) -> Any:
332
+ """Convert documented NumPy beam inputs to the canonical list form.
333
+
334
+ A single NumPy triple becomes a list, and NumPy triples inside a
335
+ list of beams are converted individually. This keeps subsequent
336
+ validation and plotting code independent of the input container.
337
+ """
338
+ if isinstance(value, np.ndarray):
339
+ value = value.tolist()
340
+ if isinstance(value, list):
341
+ return [item.tolist() if isinstance(item, np.ndarray) else item
342
+ for item in value]
343
+ return value
344
+
345
+ @field_validator('beam')
346
+ @classmethod
347
+ def _validate_beam(cls, value: BeamValue) -> BeamValue:
348
+ beams = value if value and isinstance(value[0], list) else [value]
349
+ for beam in beams:
350
+ if len(beam) != 3:
351
+ raise ValueError('Each beam must contain [bmaj, bmin, bpa].')
352
+ if all(component is None for component in beam):
353
+ continue
354
+ if any(component is None for component in beam):
355
+ raise ValueError('A beam must be complete or all None.')
356
+ if not all(np.isfinite(component) for component in beam):
357
+ raise ValueError('Beam values must be finite.')
358
+ if beam[0] <= 0 or beam[1] <= 0:
359
+ raise ValueError('bmaj and bmin must be positive.')
360
+ return value
361
+
362
+ @field_validator('beampos')
363
+ @classmethod
364
+ def _validate_beampos(cls, value: BeamPosition) -> BeamPosition:
365
+ if value is None:
366
+ return value
367
+ positions = [value] if cls._is_position(value) else value
368
+ for position in positions:
369
+ if position is None:
370
+ continue
371
+ if len(position) != 2:
372
+ raise ValueError('Each beam position must contain [x, y].')
373
+ if not all(0 <= coordinate <= 1 for coordinate in position):
374
+ raise ValueError('Beam positions must be between 0 and 1.')
375
+ return value
376
+
377
+ @staticmethod
378
+ def _is_position(value: Any) -> bool:
379
+ return (isinstance(value, list) and len(value) == 2
380
+ and all(isinstance(coordinate, float)
381
+ for coordinate in value))
382
+
383
+ def validate_display(self) -> None:
384
+ """Validate color and position lists against the number of beams."""
385
+ multi = self.beam and isinstance(self.beam[0], list)
386
+ beams = self.beam if multi else [self.beam]
387
+ nbeams = len(beams)
388
+ if isinstance(self.beamcolor, list) and len(self.beamcolor) != nbeams:
389
+ raise ValueError('Provide one beamcolor for each beam.')
390
+ if (self.beampos is not None
391
+ and not self._is_position(self.beampos)
392
+ and len(self.beampos) != nbeams):
393
+ raise ValueError('Provide one beampos for each beam.')
325
394
 
326
395
  def todict(self) -> dict[str, Any]:
327
396
  """Return beam display settings as a dictionary.
@@ -337,13 +406,13 @@ class Beam():
337
406
  return tmp
338
407
 
339
408
 
340
- @dataclass
409
+ @pydantic_dataclass
341
410
  class PlotAxes2D():
342
411
  """Use Axes.set_* to adjust x and y axes.
343
412
 
344
413
  Args:
345
414
  samexy (bool, optional): True supports same ticks between x and y. Defaults to True.
346
- loglog (float, optional): If a float is given, plot on a log-log plane, and xim=(xmax / loglog, xmax) and so does ylim. Defaults to None.
415
+ loglog (float, optional): A positive value plots on a log-log plane, with xim=(xmax / loglog, xmax) and similarly for ylim. Defaults to None.
347
416
  xscale (str, optional): ``'log'`` labels decade ticks and ticks near the limits; other intermediate ticks are minor and unlabeled. Defaults to ``'linear'``.
348
417
  yscale (str, optional): ``'log'`` labels decade ticks and ticks near the limits; other intermediate ticks are minor and unlabeled. Defaults to ``'linear'``.
349
418
  xlim (list, optional): Defaults to None.
@@ -360,9 +429,9 @@ class PlotAxes2D():
360
429
  aspect (dict or float, optional): Defaults to None.
361
430
  """
362
431
  samexy: bool = True
363
- loglog: float | None = None
364
- xscale: str = 'linear'
365
- yscale: str = 'linear'
432
+ loglog: float | None = Field(default=None, gt=0)
433
+ xscale: AxisScale = 'linear'
434
+ yscale: AxisScale = 'linear'
366
435
  xlim: list | None = None
367
436
  ylim: list | None = None
368
437
  xlabel: str | None = None
@@ -564,13 +633,18 @@ class PlotAstroData(AstroFrame):
564
633
  figsize=figsize,
565
634
  ncols=ncols, nrows=nrows, nchan=nchan)
566
635
  need_vlabel = nchan > 1 or animation
636
+ figs = []
567
637
  for ch in range(nchan):
568
638
  n, i, j = ch2nij(ch)
569
- if internalfig and n not in plt.get_fignums():
570
- fig = plt.figure(n, figsize=figsize)
639
+ if n == len(figs):
640
+ if internalfig:
641
+ fig = plt.figure(figsize=figsize)
642
+ figs.append(fig)
571
643
  if need_vlabel:
572
644
  fig.subplots_adjust(hspace=0, wspace=0,
573
645
  right=0.87, top=0.87)
646
+ else:
647
+ fig = figs[n]
574
648
  if internalax:
575
649
  sharex = ax[nij2ch(n, i - 1, j)] if i > 0 else None
576
650
  sharey = ax[nij2ch(n, i, j - 1)] if j > 0 else None
@@ -582,6 +656,7 @@ class PlotAstroData(AstroFrame):
582
656
  rf'${vlabel:.{veldigit}f}$', color='black',
583
657
  backgroundcolor='white', zorder=20)
584
658
  self.fig = None if internalfig else fig
659
+ self.figs = figs
585
660
  self.ax = ax
586
661
  self.rowcol = nrows * ncols
587
662
  self.npages = npages
@@ -612,9 +687,12 @@ class PlotAstroData(AstroFrame):
612
687
  self.sigma = d.sigma
613
688
  singlepix = d.dx is None or d.dy is None
614
689
  if len(d.beam) == 4:
615
- b.beam = self.beam = next(b for b in d.beam if None not in b)
690
+ beam = next((onebeam for onebeam in d.beam
691
+ if all(a is not None for a in onebeam)),
692
+ [None, None, None],)
616
693
  else:
617
- b.beam = self.beam = d.beam
694
+ beam = d.beam
695
+ b.beam = self.beam = Beam(beam=beam).beam
618
696
  self.add_beam(**b.todict())
619
697
  return (d.data, d.x, d.y, d.v, d.sigma, d.bunit,
620
698
  self._kw, singlepix)
@@ -657,8 +735,6 @@ class PlotAstroData(AstroFrame):
657
735
  for ch, axnow in enumerate(self.ax):
658
736
  if ch not in self._validchan(include_chan):
659
737
  continue
660
- if self.fig is None:
661
- plt.figure(ch // self.rowcol)
662
738
  if patch == 'rectangle':
663
739
  a = np.radians(angle)
664
740
  xp = x - (width*np.cos(a) + height*np.sin(a)) / 2.
@@ -674,7 +750,7 @@ class PlotAstroData(AstroFrame):
674
750
  def add_beam(self, **kwargs: Any) -> None:
675
751
  """Use add_region() to plot the beam.
676
752
 
677
- kwargs may include the arguments of Beam, except for beam_kwargs, to specify the beam appearance. Those arguments may be a list of each format.
753
+ kwargs may include the arguments of Beam, except for beam_kwargs, to specify the beam appearance. A single ``beamcolor`` or ``beampos`` value applies to every beam; per-beam lists must provide one value for each beam. A ``[None, None, None]`` beam is skipped.
678
754
 
679
755
  Default keyword values:
680
756
  Beam patch: ``facecolor=beamcolor`` and ``edgecolor=None``. Other keyword arguments override these values and are passed to ``add_region``.
@@ -683,6 +759,7 @@ class PlotAstroData(AstroFrame):
683
759
  show_beam, beamcolor, beampos = b.show_beam, b.beamcolor, b.beampos
684
760
  beam = b.beam
685
761
  del kwargs['beam']
762
+ b.validate_display()
686
763
  if not show_beam:
687
764
  return
688
765
 
@@ -691,7 +768,7 @@ class PlotAstroData(AstroFrame):
691
768
  blist = [beam] if np.ndim(beam) == 1 else beam
692
769
  n = len(blist)
693
770
  bclist = beamcolor if isinstance(beamcolor, list) else [beamcolor] * n
694
- islist = beampos == [None] * 3 or np.ndim(beampos) == 2
771
+ islist = beampos is not None and not b._is_position(beampos)
695
772
  bplist = beampos if islist else [beampos] * n
696
773
  for (bmaj, bmin, bpa), bc, bp in zip(blist, bclist, bplist):
697
774
  if None in [bmaj, bmin, bpa]:
@@ -857,16 +934,13 @@ class PlotAstroData(AstroFrame):
857
934
  if not show_cbar:
858
935
  return
859
936
 
860
- if self.fig is None:
861
- fig = plt.figure(ch // self.rowcol)
862
- else:
863
- fig = self.fig
937
+ fig = self.figs[ch // self.rowcol]
864
938
  if len(self.ax) == 1:
865
939
  ax = self.ax[ch]
866
940
  cb = fig.colorbar(mappable[ch], ax=ax, label=cblabel,
867
941
  format=cbformat, location=cblocation)
868
942
  else:
869
- cax = plt.axes([0.88, 0.105, 0.015, 0.77])
943
+ cax = fig.add_axes([0.88, 0.105, 0.015, 0.77])
870
944
  cb = fig.colorbar(mappable[ch], cax=cax, label=cblabel,
871
945
  format=cbformat)
872
946
  cb.ax.tick_params(labelsize=cbtickfontsize)
@@ -1104,14 +1178,13 @@ class PlotAstroData(AstroFrame):
1104
1178
  axnow.set_ylabel('')
1105
1179
  if len(self.ax) == 1:
1106
1180
  if self.fig is None:
1107
- plt.figure(0).tight_layout()
1181
+ self.figs[0].tight_layout()
1108
1182
  if title is not None:
1109
1183
  if len(self.ax) > 1:
1110
1184
  t = {'y': 0.9}
1111
1185
  t_in = {'t': title} if isinstance(title, str) else title
1112
1186
  t.update(t_in)
1113
- for i in range(self.npages):
1114
- fig = plt.figure(i)
1187
+ for fig in self.figs:
1115
1188
  fig.suptitle(**t)
1116
1189
  else:
1117
1190
  t = {'label': title} if isinstance(title, str) else title
@@ -1249,13 +1322,13 @@ class PlotAstroData(AstroFrame):
1249
1322
  show: bool = False, **kwargs: Any) -> None:
1250
1323
  """Use savefig of matplotlib.
1251
1324
 
1252
- If ``filename`` is provided, existing files with the same name are overwritten by Matplotlib. This method closes all Matplotlib figures with ``plt.close('all')`` after optional saving/showing.
1325
+ If ``filename`` is provided, existing files with the same name are overwritten by Matplotlib. After optional saving/showing, figures managed by this instance are closed.
1253
1326
 
1254
1327
  Default keyword values:
1255
1328
  Figure.savefig: ``transparent=True`` and ``bbox_inches='tight'``. User-supplied keyword arguments override these values.
1256
1329
 
1257
1330
  Args:
1258
- filename (str, optional): Output image file name. Existing files may be overwritten, and all Matplotlib figures are closed after saving/showing. Defaults to None.
1331
+ filename (str, optional): Output image file name. Existing files may be overwritten, and figures managed by this instance are closed after saving/showing. Defaults to None.
1259
1332
  show (bool, optional): True means doing plt.show(). Defaults to False.
1260
1333
  """
1261
1334
  _kw = {'transparent': True, 'bbox_inches': 'tight'}
@@ -1265,15 +1338,15 @@ class PlotAstroData(AstroFrame):
1265
1338
  axnow.set_ylim(*self.Ylim)
1266
1339
  if isinstance(filename, str):
1267
1340
  ext = filename.split('.')[-1]
1268
- for i in range(self.npages):
1341
+ for i, fig in enumerate(self.figs):
1269
1342
  ver = '' if self.npages == 1 else f'_{i:d}'
1270
- fig = plt.figure(i)
1271
1343
  fig.patch.set_alpha(0)
1272
1344
  fname = filename.replace(f'.{ext}', f'{ver}.{ext}')
1273
1345
  fig.savefig(fname, **_kw)
1274
1346
  if show:
1275
1347
  plt.show()
1276
- plt.close('all')
1348
+ for fig in self.figs:
1349
+ plt.close(fig)
1277
1350
 
1278
1351
  def get_figax(self) -> tuple[object, object] | None:
1279
1352
  """Output the external fig and ax after plotting.
@@ -1286,8 +1359,7 @@ class PlotAstroData(AstroFrame):
1286
1359
  + ' with channel maps')
1287
1360
  return
1288
1361
 
1289
- fig = plt.figure(0) if self.fig is None else self.fig
1290
- return fig, self.ax[0]
1362
+ return self.figs[0], self.ax[0]
1291
1363
 
1292
1364
 
1293
1365
  def _get_ylabel_profile(_kw: dict, Tb: bool, flux: bool, bunit: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.20
3
+ Version: 1.10.0
4
4
  Summary: plotastrodata is a tool for astronomers to create figures from FITS files and perform fundamental data analyses with ease.
5
5
  Home-page: https://github.com/yusukeaso-astron/plotastrodata
6
6
  Download-URL: https://github.com/yusukeaso-astron/plotastrodata
@@ -14,12 +14,13 @@ License-File: LICENSE
14
14
  Requires-Dist: astropy>=7.2
15
15
  Requires-Dist: corner
16
16
  Requires-Dist: dynesty
17
- Requires-Dist: emcee
17
+ Requires-Dist: emcee>=3.1.5
18
18
  Requires-Dist: matplotlib
19
19
  Requires-Dist: numpy>=2.0
20
20
  Requires-Dist: pillow
21
21
  Requires-Dist: plotly
22
22
  Requires-Dist: ptemcee
23
+ Requires-Dist: pydantic>=2
23
24
  Requires-Dist: scikit-image
24
25
  Requires-Dist: scipy
25
26
  Requires-Dist: tqdm
@@ -84,13 +85,14 @@ plotastrodata can do the following things.
84
85
  * astropy >= 7.2
85
86
  * corner (only for fitting)
86
87
  * dynesty (only for fitting)
87
- * emcee >= 3.0 (only for fitting)
88
+ * emcee >= 3.1.5 (only for fitting)
88
89
  * ffmpeg (only for movie)
89
90
  * matplotlib
90
91
  * numpy >= 2.0
91
92
  * pillow (only for RGB figures)
92
93
  * plotly (only for html cube)
93
94
  * ptemcee (only for fitting)
95
+ * pydantic >= 2
94
96
  * scikit-image (only for html cube)
95
97
  * scipy
96
98
  * tqdm (only for fitting)
@@ -1,12 +1,13 @@
1
1
  astropy>=7.2
2
2
  corner
3
3
  dynesty
4
- emcee
4
+ emcee>=3.1.5
5
5
  matplotlib
6
6
  numpy>=2.0
7
7
  pillow
8
8
  plotly
9
9
  ptemcee
10
+ pydantic>=2
10
11
  scikit-image
11
12
  scipy
12
13
  tqdm
@@ -19,12 +19,13 @@ install_requires =
19
19
  astropy >= 7.2
20
20
  corner
21
21
  dynesty
22
- emcee
22
+ emcee >= 3.1.5
23
23
  matplotlib
24
24
  numpy >= 2.0
25
25
  pillow
26
26
  plotly
27
27
  ptemcee
28
+ pydantic >= 2
28
29
  scikit-image
29
30
  scipy
30
31
  tqdm
File without changes
File without changes