emccd-detect 2.2.5__py3-none-any.whl

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.
@@ -0,0 +1,187 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Generate random numbers according to EM gain pdfs."""
3
+
4
+ import numpy as np
5
+ from scipy import special
6
+
7
+
8
+ class RandEMGainException(Exception):
9
+ """Exception class for rand_em_gain module."""
10
+
11
+
12
+ def rand_em_gain(n_in_array, em_gain):
13
+ """Generate random numbers according to EM gain pdfs.
14
+
15
+ Parameters
16
+ ----------
17
+ n_in_array : array_like
18
+ Array of electron values (e-).
19
+ em_gain : float
20
+ CCD em_gain (e-/photon).
21
+
22
+ Returns
23
+ -------
24
+ array_like
25
+ Electron values multiplied by random EM gain distribution (e-).
26
+
27
+ Notes
28
+ -----
29
+ This function returns an array of the same size as n_in_array. Every element
30
+ in n_in_array is multiplied by em_gain*rand_val, where rand_val is a random
31
+ number drawn from a specific pdf selected based on the value of the
32
+ n_in_array element.
33
+
34
+ References
35
+ ----------
36
+ [1] http://matlabtricks.com/post-44/generate-random-numbers-with-a-given-distribution
37
+ [2] https://arxiv.org/pdf/astro-ph/0307305.pdf
38
+
39
+ """
40
+ if em_gain < 1:
41
+ raise RandEMGainException('EM gain cannot be set to less than 1')
42
+ elif em_gain == 1:
43
+ return n_in_array
44
+ else:
45
+ # Apply gain to regular counts
46
+ n_out_array = np.random.gamma(n_in_array, em_gain)
47
+ n_out_array = np.round(n_out_array)
48
+ return n_out_array
49
+
50
+
51
+
52
+
53
+ if __name__ == '__main__':
54
+ import time
55
+ import matplotlib.pyplot as plt
56
+
57
+ # Generally, the agreement b/w the old and new methods is good. The new
58
+ # method just speeds up the code a lot, especially when cosmics are present.
59
+ # Old method functions below:
60
+
61
+ def _apply_gain(n_in_array, em_gain, max_out):
62
+ """Apply a specific em_gain to all nonzero n_in values."""
63
+ # Initialize output count array
64
+ n_out_array = np.zeros_like(n_in_array)
65
+
66
+ # Get unique nonzero n_in values
67
+ n_in_unique = np.unique(n_in_array)
68
+ n_in_unique = n_in_unique[n_in_unique > 0]
69
+
70
+ # Generate random numbers according to the gain distribution for each n_in
71
+ for n_in in n_in_unique:
72
+ inds = np.where(n_in_array == n_in)[0]
73
+ n_out_array[inds] = _rand_pdf(n_in, em_gain, max_out, len(inds))
74
+
75
+ # n_out_array = np.random.gamma(n_in_array, em_gain)
76
+ # n_out_array = np.round(n_out_array)
77
+
78
+ return n_out_array
79
+
80
+
81
+ def _rand_pdf(n_in, em_gain, x_max, size):
82
+ """Draw samples from the EM gain distribution."""
83
+ x = np.random.random(size)
84
+
85
+ # Use exact solutions for n_in == 1 and 2
86
+ if n_in == 1:
87
+ n_out = -em_gain * np.log(1 - x)
88
+ elif n_in == 2:
89
+ n_out = -em_gain * special.lambertw((x-1)/np.exp(1), -1).real - em_gain
90
+ else:
91
+ # For n > 2 use CDF approximation
92
+ # Use x values ranging from 0 to maximum allowable x output
93
+ x_axis = np.arange(0, x_max).astype(float)
94
+ x_axis[0] = np.finfo(float).eps # Use epsilon to avoid divide by 0
95
+ cdf = _get_cdf(n_in, em_gain, x_axis)
96
+
97
+ if cdf is None:
98
+ # If cdf maxes out, return maximum value
99
+ n_out = np.ones_like(x) * x_max
100
+ else:
101
+ # Draw random samples from the CDF
102
+ cdf_lookups = (cdf.max() - cdf.min()) * x + cdf.min()
103
+ n_out = x_axis[np.searchsorted(cdf, cdf_lookups)] # XXX This could be made more accurate
104
+
105
+ return np.round(n_out)
106
+
107
+
108
+ def _get_cdf(n_in, em_gain, x):
109
+ """Return an approximate CDF for the EM gain distribution.
110
+
111
+ Basden 2003 probability distribution function is as follows:
112
+
113
+ pdf = x^(n_in-1) * exp(-x/g) / (g^n_in * factorial(n_in-1))
114
+
115
+ """
116
+ # Because of the cancellation of very large numbers, first work in log space
117
+ logpdf = (n_in-1)*np.log(x) - x/em_gain - n_in*np.log(em_gain) - special.gammaln(n_in)
118
+ pdf = np.exp(logpdf)
119
+
120
+ # XXX This is a rough but safe solution
121
+ sum_pdf = np.sum(pdf)
122
+ if sum_pdf == 0:
123
+ cdf = None
124
+ else:
125
+ cdf = np.cumsum(pdf / sum_pdf)
126
+
127
+ return cdf
128
+
129
+ def compare_stats(g, n, n_samples, max_val, num_bins, plot=False):
130
+
131
+ n_in_array = np.array([n]*n_samples)
132
+ old_method = _apply_gain(n_in_array, g, max_val)
133
+
134
+ # gamma distribution:
135
+ x = rand_em_gain(n_in_array, g)
136
+
137
+ print("For n={}, g={}:".format(n,g))
138
+ print('Mean for old method: ', np.mean(old_method))
139
+ print('Std dev for old method: ', np.std(old_method))
140
+ print('Mean of gamma distribution: ', np.mean(x))
141
+ print('Std dev for gamma distribution: ', np.std(x))
142
+ print('theoretical mean: ', g*n)
143
+ print('theortical std dev: ', g*np.sqrt(n))
144
+ print()
145
+
146
+ if plot==True:
147
+ fig, ax = plt.subplots()
148
+ H = ax.hist(old_method, bins = num_bins)
149
+ ax.set_ylabel('number of occurrences')
150
+ ax.set_xlabel('gained counts (e-)')
151
+ ax.set_title('Histogram of Gained Counts (Old Method, n={})'.format(n))
152
+
153
+ fig, ax = plt.subplots()
154
+ H = ax.hist(x, bins = num_bins)
155
+ ax.set_ylabel('number of occurrences')
156
+ ax.set_xlabel('gained counts (e-)')
157
+ ax.set_title('Histogram of Gained Counts (Gamma Distribution, n={})'.format(n))
158
+
159
+ plt.show()
160
+
161
+ g = 2 #20 #200
162
+ n_samples = 10000
163
+ #max_val = 200000
164
+ num_bins = 40
165
+ # let max_out be the mean + 4*std dev from gamma dist for the max value
166
+ # from serial_counts (using ENF ~ sqrt(2), which is fine even for low
167
+ # gain since we just want an upper limit)
168
+ def max_val(g, n):
169
+ return g*n + 4*g*np.sqrt(2*n)
170
+
171
+ # in original code, max_val used max(n_in_array) where that array was for
172
+ # all serial cells; so artifically inflate by multiplying by 100
173
+ n = 1
174
+ compare_stats(g, n, n_samples, 100*max_val(g,n), num_bins)
175
+
176
+ n2 = 2
177
+ compare_stats(g, n2, n_samples, 100*max_val(g,n), num_bins)
178
+
179
+ # now a value of n for which these methods differed
180
+ n3 = 3
181
+ compare_stats(g, n3, n_samples, 100*max_val(g,n), num_bins)
182
+
183
+ n4 = 40
184
+ compare_stats(g, n4, n_samples, 100*max_val(g,n), num_bins)
185
+
186
+ n5 = 100
187
+ compare_stats(g, n5, n_samples, 100*max_val(g,n), num_bins)
@@ -0,0 +1 @@
1
+ # -*- coding: utf-8 -*-
@@ -0,0 +1,60 @@
1
+ # Any regions not specified are assumed to be shielded or unused pixels.
2
+ #
3
+ # Geometry regions are defined as:
4
+ # rows: number of region rows
5
+ # cols: number of region columns
6
+ # r0c0: coordinates (row, col) of the corner closest to (0, 0) of the array
7
+ #
8
+ # For example, in a 5x5 array, the following geometry would look as follows:
9
+ # rows: 2
10
+ # cols: 3
11
+ # r0c0:
12
+ # - 2
13
+ # - 1
14
+ #
15
+ # [
16
+ # [0, 0, 0, 0, 0],
17
+ # [0, 0, 0, 0, 0],
18
+ # [0, 1, 1, 1, 0],
19
+ # [0, 1, 1, 1, 0],
20
+ # [0, 0, 0, 0, 0]
21
+ # ]
22
+ #
23
+ # where (0, 0) is defined as the top left of this array.
24
+ frame_rows: 1200
25
+ frame_cols: 2200
26
+ geom:
27
+ image:
28
+ rows: 1024
29
+ cols: 1024
30
+ r0c0:
31
+ - 13
32
+ - 1088
33
+ prescan:
34
+ rows: 1200
35
+ cols: 1088
36
+ # good, reliable cols used for getting row-by-row bias, relative to r0c0
37
+ col_start: 800
38
+ col_end: 1000
39
+ r0c0:
40
+ - 0
41
+ - 0
42
+ parallel_overscan:
43
+ rows: 163
44
+ cols: 1056
45
+ r0c0:
46
+ - 1037
47
+ - 1088
48
+ serial_overscan:
49
+ rows: 1200
50
+ cols: 56
51
+ r0c0:
52
+ - 0
53
+ - 2144
54
+ eperdn: 8.2
55
+ fwc: 105000 #serial
56
+ sat_thresh: 0.99
57
+ plat_thresh: 0.85
58
+ cosm_filter: 2
59
+ tail_filter: #5
60
+ cic_thresh: #100
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Detector geometry metadata."""
3
+
4
+ import yaml
5
+
6
+
7
+ class ReadMetadataException(Exception):
8
+ """Exception class for read_metadata module."""
9
+
10
+
11
+ class Metadata(object):
12
+ """Create masks for different regions of the WFIRST CGI EMCCD detector.
13
+
14
+ Parameters
15
+ ----------
16
+ meta_path : str
17
+ Full path of metadta yaml.
18
+
19
+ Attributes
20
+ ----------
21
+ data : dict
22
+ Data from metadata file.
23
+ geom : SimpleNamespace
24
+ Geometry specific data.
25
+ eperdn : float
26
+ Electrons per dn conversion factor (detector k gain).
27
+ fwc : float
28
+ Full well capacity of detector.
29
+ sat_thresh : float
30
+ Multiplication factor for fwc that determines saturated cosmic pixels.
31
+ plat_thresh : float
32
+ Multiplication factor for fwc that determines edges of cosmic plateu.
33
+ cosm_filter : int
34
+ Minimum length in pixels of cosmic plateus to be identified.
35
+ tail_filter : int
36
+ Moving median filter window size for cosmic tail subtraction.
37
+ cic_thresh : float
38
+ Multiplication factor for readnoise that determines beginning of cic.
39
+
40
+ B Nemati and S Miller - UAH - 03-Aug-2018
41
+
42
+ """
43
+
44
+ def __init__(self, meta_path):
45
+ self.meta_path = meta_path
46
+
47
+ self.data = self.get_data()
48
+ self.frame_rows = self.data['frame_rows']
49
+ self.frame_cols = self.data['frame_cols']
50
+ self.geom = self.data['geom']
51
+ self.eperdn = self.data['eperdn']
52
+ self.fwc = self.data['fwc']
53
+ self.sat_thresh = self.data['sat_thresh']
54
+ self.plat_thresh = self.data['plat_thresh']
55
+ self.cosm_filter = self.data['cosm_filter']
56
+ self.tail_filter = self.data['tail_filter']
57
+ self.cic_thresh = self.data['cic_thresh']
58
+
59
+ def get_data(self):
60
+ """Read yaml data into dictionary."""
61
+ with open(self.meta_path, 'r') as stream:
62
+ data = yaml.safe_load(stream)
63
+ return data
64
+
65
+ def slice_section(self, frame, key):
66
+ """Slice 2d section out of frame.
67
+
68
+ Parameters
69
+ ----------
70
+ frame : array_like
71
+ Full frame consistent with size given in frame_rows, frame_cols.
72
+ key : str
73
+ Keyword referencing section to be sliced; must exist in geom.
74
+
75
+ """
76
+ rows, cols, r0c0 = self._unpack_geom(key)
77
+
78
+ section = frame[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols]
79
+ if section.size == 0:
80
+ raise ReadMetadataException('Corners invalid')
81
+ return section
82
+
83
+ def _unpack_geom(self, key):
84
+ """Safely check format of geom sub-dictionary and return values."""
85
+ coords = self.geom[key]
86
+ rows = coords['rows']
87
+ cols = coords['cols']
88
+ r0c0 = coords['r0c0']
89
+
90
+ return rows, cols, r0c0
@@ -0,0 +1,157 @@
1
+ """Wrapper for read_metadata to allow use in emccd_detect simulator."""
2
+
3
+ import numpy as np
4
+
5
+ from emccd_detect.util.read_metadata import Metadata
6
+
7
+
8
+ class ReadMetadataWrapperException(Exception):
9
+ """Exception class for read_metadata_wrapper module."""
10
+
11
+
12
+ class MetadataWrapper(Metadata):
13
+ """Wrapper for Metadata class to add functionality for emccd_detect.
14
+
15
+ Parameters
16
+ ----------
17
+ meta_path : str
18
+ Full path of metadta yaml.
19
+
20
+ Attributes
21
+ ----------
22
+ data : dict
23
+ Data from metadata file.
24
+ geom : SimpleNamespace
25
+ Geometry specific data.
26
+ eperdn : float
27
+ Electrons per dn conversion factor (detector k gain).
28
+ fwc : float
29
+ Full well capacity of detector.
30
+ sat_thresh : float
31
+ Multiplication factor for fwc that determines saturated cosmic pixels.
32
+ plat_thresh : float
33
+ Multiplication factor for fwc that determines edges of cosmic plateu.
34
+ cosm_filter : int
35
+ Minimum length in pixels of cosmic plateus to be identified.
36
+ tail_filter : int
37
+ Moving median filter window size for cosmic tail subtraction.
38
+ cic_thresh : float
39
+ Multiplication factor for readnoise that determines beginning of cic.
40
+
41
+ """
42
+
43
+ def __init__(self, *args, **kwargs):
44
+ super().__init__(*args, **kwargs)
45
+
46
+ # Get imaging area geometry
47
+ self.rows_im, self.cols_im, self.r0c0_im = self._imaging_area_geom()
48
+
49
+ # Make some zeros frames for initial creation of arrays
50
+ self.imaging_area_zeros = np.zeros((self.rows_im, self.cols_im))
51
+ self.full_frame_zeros = np.zeros((self.frame_rows, self.frame_cols))
52
+
53
+ def mask(self, key):
54
+ full_frame_m = self.full_frame_zeros.copy()
55
+
56
+ rows, cols, r0c0 = self._unpack_geom(key)
57
+ full_frame_m[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols] = 1
58
+ return full_frame_m.astype(bool)
59
+
60
+ def embed(self, frame, key, data):
61
+ rows, cols, r0c0 = self._unpack_geom(key)
62
+ try:
63
+ frame[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols] = data
64
+ except Exception:
65
+ raise ReadMetadataWrapperException('Data does not fit in selected '
66
+ 'section')
67
+ return frame
68
+
69
+ def embed_im(self, im_area, key, data):
70
+ rows, cols, r0c0 = self._unpack_geom_im(key)
71
+ try:
72
+ im_area[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols] = data
73
+ except Exception:
74
+ raise ReadMetadataWrapperException('Data does not fit in selected '
75
+ 'section')
76
+ return im_area
77
+
78
+ def imaging_slice(self, frame):
79
+ """Select only the real counts from full frame and exclude virtual.
80
+
81
+ Use this to transform mask and embed from acting on the full frame to
82
+ acting on only the image frame.
83
+
84
+ """
85
+ rows, cols, r0c0 = self._imaging_area_geom()
86
+
87
+ return frame[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols]
88
+
89
+ def imaging_embed(self, frame, im_area):
90
+ """Add the imaging area back to the full frame."""
91
+ rows, cols, r0c0 = self._imaging_area_geom()
92
+
93
+ frame[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols] = im_area
94
+ return frame
95
+
96
+ def _unpack_geom_corners(self, key):
97
+ """Returns corners corresponding to geometry."""
98
+ rows, cols, r0c0 = self._unpack_geom(key)
99
+ r1c1 = (r0c0[0]+rows-1, r0c0[1]+cols-1)
100
+
101
+ return r0c0, r1c1
102
+
103
+ def _unpack_geom_im(self, key):
104
+ """Wrapper for _unpack_geom, transforms r0c0 locations from full frame
105
+ coords to imaging area coords.
106
+
107
+ """
108
+ # Unpack geomotry of requested section
109
+ rows, cols, r0c0_original = self._unpack_geom(key)
110
+ # Unpack geometry of imaging area
111
+ _, _, r0c0_im = self._imaging_area_geom()
112
+
113
+ # Shift r0c0 locations by the r0c0 of the imaging area
114
+ r0c0 = r0c0_original.copy()
115
+ r0c0[0] -= r0c0_im[0]
116
+ r0c0[1] -= r0c0_im[1]
117
+
118
+ # Make sure new geom is valid
119
+ pass
120
+
121
+ return rows, cols, r0c0
122
+
123
+ def _imaging_area_geom(self):
124
+ """Return geometry of imaging area in reference to full frame."""
125
+ _, cols_pre, _ = self._unpack_geom('prescan')
126
+ _, cols_serial_ovr, _ = self._unpack_geom('serial_overscan')
127
+ rows_parallel_ovr, _, _ = self._unpack_geom('parallel_overscan')
128
+ #_, _, r0c0_image = self._unpack_geom('image')
129
+ fluxmap_rows, _, r0c0_image = self._unpack_geom('image')
130
+
131
+ rows_im = self.frame_rows - rows_parallel_ovr
132
+ cols_im = self.frame_cols - cols_pre - cols_serial_ovr
133
+ r0c0_im = r0c0_image.copy()
134
+ r0c0_im[0] = r0c0_im[0] - (rows_im - fluxmap_rows)
135
+
136
+ return rows_im, cols_im, r0c0_im
137
+
138
+ def slice_section_im(self, im_area, key):
139
+ """Slice 2d section out of imaging area of frame.
140
+
141
+ Parameters
142
+ ----------
143
+ im_area : array_like
144
+ Imaging area of frame, i.e. the full frame with the prescan and
145
+ overscan removed:
146
+ full_frame[:overscan[r0c0[0]], overscan[r0c0[1]]:]
147
+ key : str
148
+ Keyword referencing section to be sliced; must exist in geom and
149
+ must not be 'prescan' or 'overscan'.
150
+
151
+ """
152
+ rows, cols, r0c0 = self._unpack_geom_im(key)
153
+
154
+ section = im_area[r0c0[0]:r0c0[0]+rows, r0c0[1]:r0c0[1]+cols]
155
+ if section.size == 0:
156
+ raise ReadMetadataWrapperException('Corners invalid')
157
+ return section
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: emccd_detect
3
+ Version: 2.2.5
4
+ Summary: EMCCD detector image simulation
5
+ Home-page: https://github.jpl.nasa.gov/WFIRST-CGI/emccd_detect
6
+ Author: Bijan Nemati, Sam Miller, Kevin Ludwick
7
+ Author-email: bijan.nemati@tellus1.com, sam.miller@uah.edu, kevin.ludwick@uah.edu
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.6
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Requires-Python: >=3.6
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: astropy
17
+ Requires-Dist: matplotlib
18
+ Requires-Dist: numpy
19
+ Requires-Dist: scipy
20
+ Requires-Dist: pynufft ==2020.0.0
21
+ Requires-Dist: pyyaml
22
+
23
+ # EMCCD Detect
24
+
25
+ Given an input fluxmap, emccd_detect will return a simulated EMCCD detector image.
26
+
27
+
28
+ # Version
29
+
30
+ The latest version of emccd\_detect is 2.2.5.
31
+
32
+
33
+ ## Getting Started
34
+ ### Installing
35
+
36
+ This package requires Python version 3.6 or higher. If the user wants the ability to apply charge transfer inefficiency (CTI) to detector frames using the optional tool provided in emccd\_detect, then the Python version should be >=3.6 and <=3.9.
37
+
38
+ To install emccd\_detect, navigate to the emccd\_detect directory where setup.py is located and use
39
+
40
+ pip install .
41
+
42
+ This will install emccd\_detect and its dependencies, which are as follows:
43
+
44
+ * astropy
45
+ * matplotlib
46
+ * numpy
47
+ * scipy
48
+ * pynufft==2020.0.0
49
+ * pyyaml
50
+
51
+ To optionally implement CTI capabilities, navigate to the arcticpy directory (<https://github.com/roman-corgi/emccd_detect/tree/master/arcticpy_folder>), and there will be a file called setup.py in that directory. Use
52
+
53
+ pip install .
54
+
55
+ This will install arcticpy version 1.0, which is an older version of arcticpy which runs purely on Python (<https://github.com/jkeger/arcticpy/tree/row_wise/arcticpy>). If
56
+ you have Python>=3.10, the CTI functionality will not work if you are using the arcticpy installation that was included with this emccd_detect package, but everything else will work fine.
57
+
58
+
59
+ ### Usage
60
+
61
+ For an example of how to use emccd\_detect, see example_script.py.
62
+
63
+
64
+ ## Authors
65
+
66
+ * Bijan Nemati (<bijan.nemati@tellus1.com>)
67
+ * Sam Miller (<sam.miller@uah.edu>)
68
+ * Kevin Ludwick (<kevin.ludwick@uah.edu>)
69
+
@@ -0,0 +1,12 @@
1
+ emccd_detect/__init__.py,sha256=rQ0MYUU5kTHWjOTy7kyoV4spMKntp-VbXP69qDLTRwI,45
2
+ emccd_detect/cosmics.py,sha256=wRE47QOB3fwxkH5PHTeoyLjJ0fgJki6Z8hLDPx2h3SQ,3991
3
+ emccd_detect/emccd_detect.py,sha256=x8At7uER_ccsdjO6DHq9mLdrKdO71CcdcBdnl1YdBgE,22363
4
+ emccd_detect/rand_em_gain.py,sha256=3eF9T7PNYFZT0coGZwBmNTYz6B9pj2lXxTR8ROG6_7Q,6297
5
+ emccd_detect/util/__init__.py,sha256=iwhKnzeBJLKxpRVjvzwiRE63_zNpIBfaKLITauVph-0,24
6
+ emccd_detect/util/metadata.yaml,sha256=hcKTg4kMy12dfbCmmvZeKEqjeUA3BYiAcZc5vZpc3bE,1149
7
+ emccd_detect/util/read_metadata.py,sha256=r511ENJ6zbIvLDWiVic7KsVXYrBphSdTKUQbuwocjLs,2764
8
+ emccd_detect/util/read_metadata_wrapper.py,sha256=oBh2KpJ-uLTSIY_hPzw8sNIcJ6MENBSf38ks8OaFOSQ,5473
9
+ emccd_detect-2.2.5.dist-info/METADATA,sha256=AtPSg_JzaaRAvEuvGgOSubLj21JNEF0NdReNez9V4kA,2307
10
+ emccd_detect-2.2.5.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
11
+ emccd_detect-2.2.5.dist-info/top_level.txt,sha256=V0qxOcGf8TowSJXnTxWEMuK9BBsySwhtKNitfdokD0A,13
12
+ emccd_detect-2.2.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (74.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ emccd_detect