Skip to content

Commit d7cdc43

Browse files
authored
Add files via upload
1 parent bdeb765 commit d7cdc43

File tree

11 files changed

+645
-0
lines changed

11 files changed

+645
-0
lines changed

README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# PDRL: Position-Dependent Richardson-Lucy deconvolution
2+
Code for this paper
3+
4+
**Please note that the provided code is not a finished product, and we do not guarantee its functionality or suitability for any specific purpose. Use it at your own risk.**
5+
6+
## Overview
7+
Position-Dependent Richardson-Lucy (PDRL) deconvolution is an extension of the Richardson-Lucy deconvolution method, specifically designed for images with multiple positional point spread functions (PSFs).
8+
9+
![001](figures/casA_PDRL.jpg)<br>
10+
Figure 2. (a) X-ray image in the 0.5\UTF{2013}7.0 keV band of Cas A obtained with Chandra. (a-1, -2, -3) Enlarged images specified by the colored frames in (a). (b) Same as (a), but for the RLsv-deconvolved results. The unit of flux in the images is $\rm{photons~cm^{-2}~s^{-1}}$.
11+
12+
The 35 x 35 pixel spacing of PSFs used in the above PDRL image.
13+
![002](figures/PSFs_35bin.jpg)
14+
Cas A image (Obs. ID = 4636) and the two-dimensional probabil- ities of the PSFs. The integral of each PSF is normalized to be 1. The PSF color scale is a fixed range. The location of the optical axis is indicated with a green cross.
15+
16+
## Our Environments
17+
* DISTRIB_ID=Ubuntu
18+
* DISTRIB_RELEASE=20.04
19+
* DISTRIB_CODENAME=focal
20+
* DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
21+
* Chandra CIAO-4.13
22+
* marx-5.5.1
23+
* python=3.8.2
24+
* astropy==5.1
25+
* numpy==1.22.4
26+
* tqdm==4.64.0
27+
28+
## Description of Each File
29+
Our code is optimized for Chandra X-ray observed images and utilizes [CIAO](https://cxc.cfa.harvard.edu/ciao/ahelp/merge_obs.html) and [MARX](https://space.mit.edu/ASC/MARX/) for PSF simulation.
30+
31+
### 0. Install MARX and build environment
32+
Please refer to the CIAO [simulate_psf](https://cxc.cfa.harvard.edu/ciao/ahelp/simulate_psf.html) documentation for instructions on installing MARX and setting up the environment. (If you have not installed CIAO, please refer to the official CIAO documentation.)
33+
34+
### 1. Prepare count map and exposure map
35+
Prepare the count map and exposure map files required for the PDRL method.
36+
37+
### 2. Obtain ra, dec coordinates for PSF simulation
38+
Use the "pixel2skycoord.py" script to obtain the sky coordinates for each position and save them in a .txt file for use with CIAO simulate_psf.
39+
```
40+
python pixel2skycoord.py [-h] infile outfile psf_bins
41+
```
42+
infile: Input counts map file<br>
43+
outfile: Output .txt file containing image_y, image_x, ra, dec<br>
44+
psf_bins: Pixel spacing for creating PSF (Smaller values result in better accuracy but require more computation time)
45+
46+
### 3. Generate PSF from coordinates file created in Step 2
47+
Use CIAO simulate_psf to generate the PSF for each position using the ra, dec .txt file.
48+
49+
**It is recommended to run the code in the background (e.g., using the "screen" command) as simulating PSFs can take a significant amount of time.**
50+
```
51+
bash simulate_psf.sh [-h] infile_txt infile_fits monoenergy outdir
52+
```
53+
infile_txt: Input .txt file containing image_y, image_x, ra, dec<br>
54+
infile_fits: Input event FITS file (Note: not a counts map file)<br>
55+
monoenergy: Set the PSF monoenergy<br>
56+
outdir: Output directory for each position's PSF
57+
58+
### 4. PSF adjustment
59+
Move the center of the PSFs to the image center and adjust all PSFs to the same size.
60+
```
61+
repro_psfs.py [-h] infile psfs_dir outfile psfs_bins
62+
```
63+
infile: input counts map file for made psfs (Note: not evt file)<br>
64+
psfs_dir: input raw psf dir for each location<br>
65+
outfile: output all psf .npz file<br>
66+
psfs_bins: spacing of pixels you made each position psf<br>
67+
68+
### 5. Run PDRL
69+
This is PDRL method. Results for all iterations are saved.
70+
```
71+
python LDRL.py [-h] thresh_img expmap psfs outdir [--num_iter NUM_ITER] [--lambda_tv LAMBDA_TV]
72+
[--data_type DATA_TYPE] [--im_deconv_0_flat] [--poisson_err]
73+
[--boundary_px BOUNDARY_PX]
74+
```
75+
76+
thresh_img: Input counts map file for PDRL<br>
77+
expmap: Input exposure map file for PDRL<br>
78+
psfs: Input PSF file for each infile position (.npz)<br>
79+
outdir: Output directory for each iteration of PDRL results<br>
80+
--num_iter NUM_ITER: Number of iterations for PDRL (default: 200)<br>
81+
--lambda_tv LAMBDA_TV: TV (Total Variation) regularization lambda for PDRL (default: 0.002)<br>
82+
--data_type DATA_TYPE: Numpy data type (default: float64). If "killed" is returned, using float32 can help reduce memory usage.<br>
83+
--im_deconv_0_flat: Initial value for the 0th iteration of the LDRL method (default: False, meaning the input file)<br>
84+
--poisson_err: Add a Poisson distribution random number according to the input count file for each iteration (default: False)<br>
85+
--boundary_px BOUNDARY_PX: Randomly select a PSF from boundary_px pixels near the boundary of the PSFs (default: 1)
86+
87+
### 6. Run PDRL error analysis
88+
Calculate the uncertainty of the PDRL method using error propagation.
89+
```
90+
python PDRL_err.py [-h] thresh_img expmap psfs im_deconv outfile [--data_type DATA_TYPE]
91+
```
92+
93+
thresh_img: Input counts map file for PDRL
94+
expmap: Input exposure map file for PDRL
95+
psfs: Input PSF file for each infile position (.npz)
96+
im_deconv: Input PDRL result to calculate the next iteration's PDRL uncertainty
97+
outfile: Output file for PDRL error result
98+
--data_type DATA_TYPE: Default: float64. Numpy data type. (Note: If `killed` is returned, using float32 can be useful to reduce memory usage)
99+
100+
## Demonstration
101+
The demonstration code provides the results mentioned above. After completing step 0, run the following code:
102+
```
103+
bash demo.sh
104+
```
105+
106+
## Quick Demonstration
107+
Place the following files directly in the demo directory:
108+
* [merged_4636_4637_4639_5319](https://drive.google.com/file/d/1jNSmAmFOXCZtuikxnf2gb5QXl2mmt0Gg/view?usp=share_link)(merged Obs. ID=4636, 4637, 4639, and 5319 counts map and exposure map files)
109+
* [repro_psfs_35bin.npz](https://drive.google.com/file/d/1qa6gsLghqQbbsElNKirOgW-UdVKlTzIv/view?usp=share_link)(35 × 35 pixels Obs. ID 4636 PSFs)
110+
111+
Then, in the demo directory, run the PDRL method with the following command:
112+
```
113+
python ../pyscript/PDRL.py merged_4636_4637_4639_5319/broad_thresh.img merged_4636_4637_4639_5319/broad_thresh.expmap \
114+
repro_psfs_35bin.npz PDRL_results --lambda_tv 0 --boundary_px 0
115+
```
116+
117+
After completion, to obtain the uncertainty of the 200th iteration based on the 199th iteration of the PDRL method, use the following command:
118+
119+
```
120+
python ../pyscript/PDRL_err.py merged_4636_4637_4639_5319/broad_thresh.img merged_4636_4637_4639_5319/broad_thresh.expmap \
121+
repro_psfs_35bin.npz PDRL_results/iter_0199.fits iter_0200_err.fits
122+
```
123+
124+
## Citing
125+
If you find our code helpful, we would appreciate it if you could cite the following reference as a token of acknowledgment:

demo/demo.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
# Merge the data.
3+
merge_obs 4636/repro/acisf04636_repro_evt2.fits,\
4+
4637/repro/acisf04637_repro_evt2.fits,\
5+
4639/repro/acisf04639_repro_evt2.fits,\
6+
5319/repro/acisf05319_repro_evt2.fits \
7+
merged_4636_4637_4639_5319/ bands=broad binsize=1
8+
9+
# Create a txt file converted from pixel coordinates to radec coordinates.
10+
python ../pyscript/pixel2skycoord.py merged_4636_4637_4639_5319/broad_thresh.img pixel2sky_35bin.txt 35
11+
12+
# Simulation of psf by marx using Obs. ID 4636 as a representative.
13+
bash ../shscript/simulate_psfs.sh pixel2sky_35bin.txt 4636/repro/acisf04636_repro_evt2.fits 2.3 psfs_35bin
14+
15+
# Adjust the psf so that the center of the psf is at the center of the image.
16+
# Save an array with psf for each location.
17+
python ../pyscript/repro_psfs.py merged_4636_4637_4639_5319/broad_thresh.img psfs_35bin repro_psfs_35bin.npz 35
18+
19+
# Run PDRL method.
20+
python ../pyscript/PDRL.py merged_4636_4637_4639_5319/broad_thresh.img merged_4636_4637_4639_5319/broad_thresh.expmap \
21+
repro_psfs_35bin.npz PDRL_results --lambda_tv 0 --boundary_px 0
22+
23+
# Run the error of PDRL by the law of error propagation.
24+
python ../pyscript/PDRL_err.py merged_4636_4637_4639_5319/broad_thresh.img merged_4636_4637_4639_5319/broad_thresh.expmap \
25+
repro_psfs_35bin.npz PDRL_results/iter_0199.fits iter_0200_err.fits

figures/PSFs_35bin.jpg

544 KB
Loading

figures/casA_PDRL.jpg

1.28 MB
Loading

pyscript/PDRL.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import os
2+
import random
3+
from tqdm import tqdm
4+
import numpy as np
5+
from scipy.signal import convolve
6+
import astropy.io.fits as fits
7+
import argparse
8+
import lib_fits
9+
10+
11+
def main():
12+
# Parameters
13+
parser = argparse.ArgumentParser(description='This is PDRL method. Results for all iterations are saved.')
14+
parser.add_argument('thresh_img', type=str, help='input counts map file for PDRL')
15+
parser.add_argument('expmap', type=str, help='input exposure map file for PDRL')
16+
parser.add_argument('psfs', type=str, help='input psf file for each infile position (.npz)')
17+
parser.add_argument('outdir', type=str, help='output dir for each iteration of PDRL results')
18+
parser.add_argument('--num_iter', type=int, default=200, help='default:200. the number of iterations for PDRL')
19+
parser.add_argument('--lambda_tv', type=float, default=0.002, help='default:0.002. the TV regularization lambda')
20+
parser.add_argument('--data_type', type=str, default='float64', help='default:float64. numpy data type (Note:If `killed` is returned, `float32` is useful to lower the memory.)')
21+
parser.add_argument("--im_deconv_0_flat", action='store_true', help='default:False. initial value for the 0th iteration of the ldrl method (`False` mean the input file)')
22+
parser.add_argument("--poisson_err", action='store_true', help='default:False. add a Poisson distribution random number according to the input count file for each iteration')
23+
parser.add_argument("--boundary_px", type=int, default=1, help='default:1. Randomly select a PSF from `boundary_px` pixels near the boundary of the PSFs')
24+
args = parser.parse_args()
25+
26+
# Loads the thresh.img and expmap.
27+
thresh_img, wcs = lib_fits.load_fits(infile=args.thresh_img, data_type=args.data_type)
28+
expmap, _ = lib_fits.load_fits(infile=args.expmap, data_type=args.data_type)
29+
30+
# Loads all PSFs as numpy array.
31+
print('PSF loading in progress.')
32+
psfs, psfs_bins = load_all_psf(args.psfs, args.data_type)
33+
34+
# PDRL method.
35+
np.random.seed(seed=2023)
36+
print('PDRL method is start.')
37+
os.makedirs(args.outdir, exist_ok=True)
38+
pdrl = Position_Dependent_Richardson_Lucy(args.outdir, thresh_img, expmap, psfs, psfs_bins, args.num_iter, args.lambda_tv, wcs, args.data_type, args.im_deconv_0_flat, args.poisson_err, args.boundary_px)
39+
deconvs = pdrl.position_dependent_richardson_lucy()
40+
41+
42+
def load_all_psf(psfs_infile, data_type):
43+
repro_psfs = np.load(psfs_infile)
44+
psfs = np.array(repro_psfs['repro_psfs'], dtype=data_type)
45+
psfs_bins = repro_psfs['psfs_bins'].tolist()
46+
return psfs, psfs_bins
47+
48+
49+
class Position_Dependent_Richardson_Lucy:
50+
def __init__(self, outdir, thresh_img, expmap, psfs, psfs_bins, num_iter=200, lambda_tv=0.002, wcs=None, data_type='float64', im_deconv_0_flat=False, poisson_err=False, boundary_px=1):
51+
self.outdir = outdir
52+
self.thresh_img = thresh_img
53+
self.expmap = expmap + 1e-15 # Used to avoid 0 divisions
54+
self.psfs = psfs
55+
self.psfs_bins = psfs_bins
56+
self.num_iter = num_iter
57+
self.lambda_tv = lambda_tv
58+
self.wcs = wcs
59+
self.data_type = data_type
60+
self.im_deconv_0_flat = im_deconv_0_flat
61+
self.poisson_err = poisson_err
62+
self.boundary_px = boundary_px
63+
64+
65+
def _convolve_each_kernel(self, image, kernels, kernels_xgrid, kernels_ygrid):
66+
y_shape, x_shape = image.shape
67+
*_, k_y_shape, k_x_shape = kernels.shape
68+
pad_y, pad_x = (k_y_shape // 2, k_x_shape // 2)
69+
pad_conv = np.zeros([y_shape + pad_y*2, x_shape + pad_x*2], dtype=self.data_type) # For convolution edge works
70+
y_range = range(self.boundary_px, y_shape-self.boundary_px)
71+
x_range = range(self.boundary_px, x_shape-self.boundary_px)
72+
for y in tqdm(y_range):
73+
for x in x_range:
74+
pad_conv[y:y+k_y_shape, x:x+k_x_shape] += image[y, x] * kernels[(y+kernels_ygrid[y, x])//self.psfs_bins, (x+kernels_xgrid[y, x])//self.psfs_bins]
75+
conv = pad_conv[pad_y:-pad_y, pad_x:-pad_x] # Make array size equal to input size
76+
return conv
77+
78+
79+
def _convolve_each_kernel_dependent(self, image, kernels, kernels_xgrid, kernels_ygrid):
80+
y_shape, x_shape = image.shape
81+
*_, k_y_shape, k_x_shape = kernels.shape
82+
k_y_half, k_x_half = (k_y_shape // 2, k_x_shape // 2)
83+
conv = np.zeros_like(image, dtype=self.data_type)
84+
y_range = range(k_y_half+1+self.boundary_px, y_shape-k_y_half-1-self.boundary_px)
85+
x_range = range(k_x_half+1+self.boundary_px, x_shape-k_x_half-1-self.boundary_px)
86+
for y in tqdm(y_range):
87+
for x in x_range:
88+
conv[y, x] = np.sum(image[y-k_y_half:y+k_y_half+1, x-k_x_half:x+k_x_half+1] * kernels[(y+kernels_ygrid[y, x])//self.psfs_bins, (x+kernels_xgrid[y, x])//self.psfs_bins])
89+
return conv
90+
91+
def _generate_poisson_img(self, thresh_img):
92+
poisson_func = lambda x: np.random.poisson(x)
93+
poisson_thresh_img = poisson_func(thresh_img)
94+
return poisson_thresh_img
95+
96+
def position_dependent_richardson_lucy(self):
97+
if self.im_deconv_0_flat:
98+
im_deconv = np.full(self.thresh_img.shape, 0.5, dtype=self.data_type)
99+
else:
100+
im_deconv = np.copy(self.thresh_img) / self.expmap
101+
102+
eps = 1e-15 # Small regularization parameter used to avoid 0 divisions
103+
104+
for _iter in range(1, self.num_iter+1):
105+
print(f'iter {_iter} start.')
106+
print('Bottom calculation in progress.')
107+
psfs_xgrid = np.random.randint(-self.boundary_px, self.boundary_px+1, size=im_deconv.shape)
108+
psfs_ygrid = np.random.randint(-self.boundary_px, self.boundary_px+1, size=im_deconv.shape)
109+
conv = self._convolve_each_kernel(im_deconv, self.psfs, psfs_xgrid, psfs_ygrid) + eps
110+
111+
if self.poisson_err:
112+
relative_blur = self._generate_poisson_img(self.thresh_img) / self.expmap / conv
113+
else:
114+
relative_blur = self.thresh_img / self.expmap / conv
115+
116+
# TVregularization
117+
grad_x = np.gradient(im_deconv, axis=1)
118+
grad_y = np.gradient(im_deconv, axis=0)
119+
grad_norm = np.sqrt(grad_x**2+grad_y**2) + eps
120+
tv_reg = np.gradient(grad_x / grad_norm, axis=1) + np.gradient(grad_y / grad_norm, axis=0)
121+
122+
print('Top calculation in progress.')
123+
im_deconv = im_deconv/(1-self.lambda_tv*tv_reg)*self._convolve_each_kernel_dependent(relative_blur, self.psfs, psfs_xgrid, psfs_ygrid)
124+
125+
# Save the results of deconvolution for each iteration
126+
outfile = os.path.join(self.outdir, f'iter_{_iter:0>4}.fits')
127+
lib_fits.np2fits(np_array=im_deconv, outfile=outfile, wcs=self.wcs)
128+
129+
return
130+
131+
132+
if __name__ == '__main__':
133+
main()

pyscript/PDRL_err.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import os
2+
from tqdm import tqdm
3+
import numpy as np
4+
import astropy.io.fits as fits
5+
import argparse
6+
import lib_fits
7+
8+
9+
def main():
10+
# Parameters
11+
parser = argparse.ArgumentParser(description='Calculate the uncertainty of PDRL method by the law of error propagation.')
12+
parser.add_argument('thresh_img', type=str, help='input counts map file for PDRL')
13+
parser.add_argument('expmap', type=str, help='input exposure map file for PDRL')
14+
parser.add_argument('psfs', type=str, help='input psf file for each infile position (.npz)')
15+
parser.add_argument('im_deconv', type=str, help='input a PDRL result to get the next iteration PDRL uncertainty')
16+
parser.add_argument('outfile', type=str, help='output file for PDRL error result')
17+
parser.add_argument('--data_type', type=str, default='float64', help='default:float64. numpy data type (Note:If `killed` is returned, `float32` is useful to lower the memory.)')
18+
args = parser.parse_args()
19+
20+
# Loads the thresh.img, expmap and im_deconv.
21+
thresh_img, wcs = lib_fits.load_fits(infile=args.thresh_img, data_type=args.data_type)
22+
expmap, _ = lib_fits.load_fits(infile=args.expmap, data_type=args.data_type)
23+
im_deconv, _ = lib_fits.load_fits(infile=args.im_deconv, data_type=args.data_type)
24+
25+
# Loads all PSFs as numpy array.
26+
print('PSF loading in progress.')
27+
psfs, psfs_bins = load_all_psf(args.psfs, args.data_type)
28+
29+
# PDRL error method.
30+
print('Estimate the PDRL error method is start.')
31+
outdir = os.path.dirname(args.outfile)
32+
os.makedirs(outdir, exist_ok=True)
33+
pdrl_err = Position_Dependent_Richardson_Lucy_Err(args.outfile, thresh_img, expmap, psfs, psfs_bins, im_deconv, wcs, args.data_type)
34+
deconvs_err = pdrl_err.position_dependent_richardson_lucy_err()
35+
print('Done.')
36+
37+
38+
def load_all_psf(psfs_infile, data_type):
39+
repro_psfs = np.load(psfs_infile)
40+
psfs = np.array(repro_psfs['repro_psfs'], dtype=data_type)
41+
psfs_bins = repro_psfs['psfs_bins'].tolist()
42+
return psfs, psfs_bins
43+
44+
45+
class Position_Dependent_Richardson_Lucy_Err:
46+
def __init__(self, outfile, thresh_img, expmap, psfs, psfs_bins, im_deconv, wcs=None, data_type='float64'):
47+
self.outfile = outfile
48+
self.thresh_img = thresh_img
49+
self.expmap = expmap + 1e-15 # Used to avoid 0 divisions
50+
self.psfs = psfs
51+
self.psfs_bins = psfs_bins
52+
self.im_deconv = im_deconv
53+
self.wcs = wcs
54+
self.data_type = data_type
55+
56+
57+
def _convolve_each_kernel(self, image, kernels):
58+
y_shape, x_shape = image.shape
59+
*_, k_y_shape, k_x_shape = kernels.shape
60+
pad_y, pad_x = (k_y_shape // 2, k_x_shape // 2)
61+
pad_conv = np.zeros([y_shape + pad_y*2, x_shape + pad_x*2], dtype=self.data_type) # For convolution edge works
62+
y_range = range(y_shape)
63+
x_range = range(x_shape)
64+
for y in tqdm(y_range):
65+
for x in x_range:
66+
pad_conv[y:y+k_y_shape, x:x+k_x_shape] += image[y, x] * kernels[y//self.psfs_bins, x//self.psfs_bins]
67+
conv = pad_conv[pad_y:-pad_y, pad_x:-pad_x] # Make array size equal to input size
68+
return conv
69+
70+
71+
def _convolve_each_kernel_dependent(self, image, kernels):
72+
y_shape, x_shape = image.shape
73+
*_, k_y_shape, k_x_shape = kernels.shape
74+
k_y_half, k_x_half = (k_y_shape // 2, k_x_shape // 2)
75+
conv = np.zeros_like(image, dtype=self.data_type)
76+
y_range = range(k_y_half+1, y_shape-k_y_half-1, 1)
77+
x_range = range(k_x_half+1, x_shape-k_x_half-1, 1)
78+
for y in tqdm(y_range):
79+
for x in x_range:
80+
conv[y, x] = np.sum(image[y-k_y_half:y+k_y_half+1, x-k_x_half:x+k_x_half+1] * kernels[y//self.psfs_bins, x//self.psfs_bins])
81+
return conv
82+
83+
84+
def position_dependent_richardson_lucy_err(self):
85+
psfs_sq = self.psfs ** 2
86+
expmap_sq = self.expmap ** 2
87+
88+
eps = 1e-15 # Small regularization parameter used to avoid 0 divisions
89+
90+
print('Bottom calculation in progress.')
91+
conv = self._convolve_each_kernel(self.im_deconv, self.psfs) + eps
92+
relative_blur = self.thresh_img / expmap_sq / conv**2
93+
94+
print('Top calculation in progress.')
95+
im_deconv_err = self.im_deconv*np.sqrt(self._convolve_each_kernel_dependent(relative_blur, psfs_sq))
96+
97+
# Save the results of deconvolution err
98+
lib_fits.np2fits(np_array=im_deconv_err, outfile=self.outfile, wcs=self.wcs)
99+
100+
return
101+
102+
103+
if __name__ == '__main__':
104+
main()
1.27 KB
Binary file not shown.

0 commit comments

Comments
 (0)