├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py ├── examples.rst ├── figs │ └── map.png ├── index.rst ├── installation.rst ├── license.rst ├── modules.rst ├── requirements.txt └── selectionfunctions.rst ├── selectionfunctions ├── __init__.py ├── cog_ii.py ├── cog_v.py ├── config.py ├── equirectangular_map.py ├── examples │ ├── FIRE │ │ ├── example_fire.ipynb │ │ ├── expected_gobs_efficiency_edr3.h │ │ ├── gamp_query.py │ │ ├── gobs_eff_query.py │ │ └── median_gamp_edr3.h │ ├── __init__.py │ ├── example_EDR3.ipynb │ ├── example_cog_ii.ipynb │ └── example_cog_v.ipynb ├── fetch_utils.py ├── healpix_map.py ├── json_serializers.py ├── map.py ├── output │ └── .gitignore ├── rrl_mateu_2020.py ├── sets.py ├── sfexceptions.py ├── source.py ├── std_paths.py ├── tests │ ├── __init__.py │ └── test_cog_ii.py └── unstructured_map.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info* 3 | *.png 4 | testing*.ipynb 5 | *.dat 6 | *.bak 7 | *.ipynb_checkpoints 8 | .DS_Store 9 | dist/ 10 | venv/ 11 | docs/_build/ 12 | build/ 13 | selectionfunctions/data/ 14 | frames/ 15 | *.h 16 | *.csv 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dustmaps/tests/ned_output.json 2 | include dustmaps/data/bh/bh.h5 3 | include LICENSE.txt 4 | include README.md 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![DOI](http://joss.theoj.org/papers/10.21105/joss.00695/status.svg)](https://doi.org/10.21105/joss.00695) 2 | 3 | selectionfunctions 4 | ================== 5 | 6 | The ``selectionfunctions`` package aspires to provide a uniform interface to the selection functions of the major astronomical surveys. 7 | This package is entirely derivative of the truly excellent ``dustmaps`` package created by Gregory M. Green. 8 | The ``selectionfunctions`` package is a product of the [Completeness of the *Gaia*-verse (CoG)](https://www.gaiaverse.space/) collaboration. 9 | 10 | Supported Selection Functions 11 | ----------------------------- 12 | 13 | The currently supported selection functions are: 14 | 15 | 1. Gaia DR2 source catalogue (cog_ii.dr2_sf, Boubert & Everall 2020, submitted) 16 | 17 | To request addition of another selection function in this package, [file an issue on 18 | GitHub](https://github.com/gaiaverse/selectionfunctions/issues), or submit a pull request. 19 | 20 | 21 | Installation 22 | ------------ 23 | 24 | Download the repository from [GitHub](https://github.com/gaiaverse/selectionfunctions) and 25 | then run: 26 | 27 | python setup.py install --large-data-dir=/path/where/you/want/large/data/files/stored 28 | 29 | Alternatively, you can use the Python package manager `pip`: 30 | 31 | pip install selectionfunctions 32 | 33 | 34 | Getting the Data 35 | ---------------- 36 | 37 | To fetch the data for the GaiaDR2 selectionfunction, run: 38 | 39 | python setup.py fetch --map-name=cog_ii 40 | 41 | You can download the other selection functions by changing "cog_ii" to (other selection functions will be added in future). 42 | 43 | Alternatively, if you have used `pip` to install `selectionfunctions`, then you can 44 | configure the data directory and download the data by opening up a python 45 | interpreter and running: 46 | 47 | >>> from selectionfunctions.config import config 48 | >>> config['data_dir'] = '/path/where/you/want/large/data/files/stored' 49 | >>> 50 | >>> import selectionfunctions.cog_ii 51 | >>> selectionfunctions.cog_ii.fetch() 52 | 53 | 54 | Querying the selection functions 55 | ----------------- 56 | 57 | Selection functions are queried using Source objects, which are a variant on the 58 | [`astropy.coordinates.SkyCoord`](http://docs.astropy.org/en/stable/api/astropy.coordinates.SkyCoord.html#astropy.coordinates.SkyCoord) 59 | object. This means that any coordinate system supported by `astropy` can be 60 | used as input. For example, we can query the Gaia DR2 selection function as follows: 61 | 62 | >>> import selectionfunctions.cog_ii as CoGII 63 | >>> from selectionfunctions.source import Source 64 | >>> 65 | >>> dr2_sf = CoGII.dr2_sf() 66 | >>> 67 | >>> c = Source( 68 | '22h54m51.68s', 69 | '-51d11m44.19s', 70 | photometry={'gaia_g':16.02}, 71 | frame='icrs') 72 | >>> print(dr2_sf(c)) 73 | 74 | 75 | Above, we have used the ICRS coordinate system (the inputs are RA and Dec). We 76 | can use other coordinate systems, such as Galactic coordinates, and we can 77 | provide coordinate arrays. The following example uses both features: 78 | 79 | >>> c = Source( 80 | [75.00000000, 130.00000000], 81 | [-89.00000000, 10.00000000], 82 | photometry={'gaia_g':[2.3,17.8]}, 83 | frame='galactic', 84 | unit='deg') 85 | >>> print(dr2_sf(c)) 86 | 87 | 88 | 89 | Documentation 90 | ------------- 91 | 92 | Read the full documentation at http://selectionfunctions.readthedocs.io/en/latest/. 93 | 94 | 95 | Citation 96 | -------- 97 | 98 | If you make use of this software in a publication, please always cite 99 | [Green (2018) in The Journal of Open Source Software](https://doi.org/10.21105/joss.00695). 100 | 101 | You should also cite the papers behind the selection functions you use. 102 | 103 | 1. cog_ii.dr2_sf - Please cite Completeness of the Gaia-verse [Paper I](https://ui.adsabs.harvard.edu/abs/2020arXiv200414433B/abstract) and [Paper II](https://ui.adsabs.harvard.edu/abs/2020arXiv200508983B/abstract). 104 | 105 | Development 106 | ----------- 107 | 108 | Development of `selectionfunctions` takes place on GitHub, at 109 | https://github.com/gaiaverse/selectionfunctions. Any bugs, feature requests, pull requests, 110 | or other issues can be filed there. Contributions to the software are welcome. 111 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dustmaps.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dustmaps.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/dustmaps" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dustmaps" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # dustmaps documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Oct 14 17:20:58 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | # sys.path.insert(0, os.path.join(os.path.dirname(__name__), '..')) 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.doctest', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.mathjax', 39 | 'sphinx.ext.viewcode', 40 | 'sphinxcontrib.napoleon', 41 | # 'sphinxcontrib.googleanalytics' 42 | # 'sphinxcontrib.programoutput' 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The encoding of source files. 55 | # 56 | # source_encoding = 'utf-8-sig' 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # General information about the project. 62 | project = u'selectionfunctions' 63 | copyright = u'2019, Douglas Boubert & Andrew Everall' 64 | author = u'Douglas Boubert' 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | version = u'v0.1' 72 | # The full version, including alpha/beta/rc tags. 73 | release = u'v0.1.0' 74 | 75 | # The language for content autogenerated by Sphinx. Refer to documentation 76 | # for a list of supported languages. 77 | # 78 | # This is also used if you do content translation via gettext catalogs. 79 | # Usually you set "language" from the command line for these cases. 80 | language = None 81 | 82 | # There are two options for replacing |today|: either, you set today to some 83 | # non-false value, then it is used: 84 | # 85 | # today = '' 86 | # 87 | # Else, today_fmt is used as the format for a strftime call. 88 | # 89 | # today_fmt = '%B %d, %Y' 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This patterns also effect to html_static_path and html_extra_path 94 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 95 | 96 | # The reST default role (used for this markup: `text`) to use for all 97 | # documents. 98 | # 99 | # default_role = None 100 | 101 | # If true, '()' will be appended to :func: etc. cross-reference text. 102 | # 103 | # add_function_parentheses = True 104 | 105 | # If true, the current module name will be prepended to all description 106 | # unit titles (such as .. function::). 107 | # 108 | # add_module_names = True 109 | 110 | # If true, sectionauthor and moduleauthor directives will be shown in the 111 | # output. They are ignored by default. 112 | # 113 | # show_authors = False 114 | 115 | # The name of the Pygments (syntax highlighting) style to use. 116 | pygments_style = 'sphinx' 117 | 118 | # A list of ignored prefixes for module index sorting. 119 | # modindex_common_prefix = [] 120 | 121 | # If true, keep warnings as "system message" paragraphs in the built documents. 122 | # keep_warnings = False 123 | 124 | # If true, `todo` and `todoList` produce output, else they produce nothing. 125 | todo_include_todos = False 126 | 127 | 128 | # -- Options for HTML output ---------------------------------------------- 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | # 133 | html_theme = 'default' 134 | # html_theme = 'alabaster' 135 | 136 | # Theme options are theme-specific and customize the look and feel of a theme 137 | # further. For a list of options available for each theme, see the 138 | # documentation. 139 | # 140 | # html_theme_options = {} 141 | 142 | # Add any paths that contain custom themes here, relative to this directory. 143 | # html_theme_path = [] 144 | 145 | # The name for this set of Sphinx documents. 146 | # " v documentation" by default. 147 | # 148 | # html_title = u'dustmaps vv0.1a3' 149 | 150 | # A shorter title for the navigation bar. Default is the same as html_title. 151 | # 152 | # html_short_title = None 153 | 154 | # The name of an image file (relative to this directory) to place at the top 155 | # of the sidebar. 156 | # 157 | # html_logo = None 158 | 159 | # The name of an image file (relative to this directory) to use as a favicon of 160 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 161 | # pixels large. 162 | # 163 | # html_favicon = None 164 | 165 | # Add any paths that contain custom static files (such as style sheets) here, 166 | # relative to this directory. They are copied after the builtin static files, 167 | # so a file named "default.css" will overwrite the builtin "default.css". 168 | html_static_path = ['_static'] 169 | 170 | # Add any extra paths that contain custom files (such as robots.txt or 171 | # .htaccess) here, relative to this directory. These files are copied 172 | # directly to the root of the documentation. 173 | # 174 | # html_extra_path = [] 175 | 176 | # If not None, a 'Last updated on:' timestamp is inserted at every page 177 | # bottom, using the given strftime format. 178 | # The empty string is equivalent to '%b %d, %Y'. 179 | # 180 | # html_last_updated_fmt = None 181 | 182 | # If true, SmartyPants will be used to convert quotes and dashes to 183 | # typographically correct entities. 184 | # 185 | # html_use_smartypants = True 186 | 187 | # Custom sidebar templates, maps document names to template names. 188 | # 189 | # html_sidebars = {} 190 | 191 | # Additional templates that should be rendered to pages, maps page names to 192 | # template names. 193 | # 194 | # html_additional_pages = {} 195 | 196 | # If false, no module index is generated. 197 | # 198 | # html_domain_indices = True 199 | 200 | # If false, no index is generated. 201 | # 202 | # html_use_index = True 203 | 204 | # If true, the index is split into individual pages for each letter. 205 | # 206 | # html_split_index = False 207 | 208 | # If true, links to the reST sources are added to the pages. 209 | # 210 | # html_show_sourcelink = True 211 | 212 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 213 | # 214 | # html_show_sphinx = True 215 | 216 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 217 | # 218 | # html_show_copyright = True 219 | 220 | # If true, an OpenSearch description file will be output, and all pages will 221 | # contain a tag referring to it. The value of this option must be the 222 | # base URL from which the finished HTML is served. 223 | # 224 | # html_use_opensearch = '' 225 | 226 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 227 | # html_file_suffix = None 228 | 229 | # Language to be used for generating the HTML full-text search index. 230 | # Sphinx supports the following languages: 231 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 232 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 233 | # 234 | # html_search_language = 'en' 235 | 236 | # A dictionary with options for the search language support, empty by default. 237 | # 'ja' uses this config value. 238 | # 'zh' user can custom change `jieba` dictionary path. 239 | # 240 | # html_search_options = {'type': 'default'} 241 | 242 | # The name of a javascript file (relative to the configuration directory) that 243 | # implements a search results scorer. If empty, the default will be used. 244 | # 245 | # html_search_scorer = 'scorer.js' 246 | 247 | # Output file base name for HTML help builder. 248 | htmlhelp_basename = 'selectionfunctionsdoc' 249 | 250 | # -- Options for LaTeX output --------------------------------------------- 251 | 252 | latex_elements = { 253 | # The paper size ('letterpaper' or 'a4paper'). 254 | # 255 | # 'papersize': 'letterpaper', 256 | 257 | # The font size ('10pt', '11pt' or '12pt'). 258 | # 259 | # 'pointsize': '10pt', 260 | 261 | # Additional stuff for the LaTeX preamble. 262 | # 263 | # 'preamble': '', 264 | 265 | # Latex figure (float) alignment 266 | # 267 | # 'figure_align': 'htbp', 268 | } 269 | 270 | # Grouping the document tree into LaTeX files. List of tuples 271 | # (source start file, target name, title, 272 | # author, documentclass [howto, manual, or own class]). 273 | latex_documents = [ 274 | (master_doc, 'selectionfunctions.tex', u'selectionfunctions Documentation', 275 | u'Douglas Boubert', 'manual'), 276 | ] 277 | 278 | # The name of an image file (relative to this directory) to place at the top of 279 | # the title page. 280 | # 281 | # latex_logo = None 282 | 283 | # For "manual" documents, if this is true, then toplevel headings are parts, 284 | # not chapters. 285 | # 286 | # latex_use_parts = False 287 | 288 | # If true, show page references after internal links. 289 | # 290 | # latex_show_pagerefs = False 291 | 292 | # If true, show URL addresses after external links. 293 | # 294 | # latex_show_urls = False 295 | 296 | # Documents to append as an appendix to all manuals. 297 | # 298 | # latex_appendices = [] 299 | 300 | # It false, will not define \strong, \code, itleref, \crossref ... but only 301 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 302 | # packages. 303 | # 304 | # latex_keep_old_macro_names = True 305 | 306 | # If false, no module index is generated. 307 | # 308 | # latex_domain_indices = True 309 | 310 | 311 | # -- Options for manual page output --------------------------------------- 312 | 313 | # One entry per manual page. List of tuples 314 | # (source start file, name, description, authors, manual section). 315 | man_pages = [ 316 | (master_doc, 'selectionfunctions', u'selectionfunctions Documentation', 317 | [author], 1) 318 | ] 319 | 320 | # If true, show URL addresses after external links. 321 | # 322 | # man_show_urls = False 323 | 324 | 325 | # -- Options for Texinfo output ------------------------------------------- 326 | 327 | # Grouping the document tree into Texinfo files. List of tuples 328 | # (source start file, target name, title, author, 329 | # dir menu entry, description, category) 330 | texinfo_documents = [ 331 | (master_doc, 'selectionfunctions', u'selectionfunctions Documentation', 332 | author, 'selectionfunctions', 'One line description of project.', 333 | 'Miscellaneous'), 334 | ] 335 | 336 | # Documents to append as an appendix to all manuals. 337 | # 338 | # texinfo_appendices = [] 339 | 340 | # If false, no module index is generated. 341 | # 342 | # texinfo_domain_indices = True 343 | 344 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 345 | # 346 | # texinfo_show_urls = 'footnote' 347 | 348 | # If true, do not generate a @detailmenu in the "Top" node's menu. 349 | # 350 | # texinfo_no_detailmenu = False 351 | 352 | # Google Analytics 353 | # googleanalytics_id = 'UA-57454625-3' 354 | 355 | # Mock modules, rather than importing them. 356 | 357 | # import sys 358 | # from mock import Mock as MagicMock 359 | # 360 | # class Mock(MagicMock): 361 | # @classmethod 362 | # def __getattr__(cls, name): 363 | # return Mock() 364 | 365 | autodoc_mock_imports = [ 366 | 'astropy', 367 | 'astropy.coordinates', 368 | 'astropy.coordinates.SkyCoord', 369 | 'astropy.io', 370 | 'astropy.io.fits', 371 | 'astropy.units', 372 | 'astropy.wcs', 373 | 'contextlib', 374 | 'contextlib.closing', 375 | 'h5py', 376 | 'hashlib', 377 | 'healpy', 378 | 'numpy', 379 | 'PIL', 380 | 'PIL.Image', 381 | 'scipy', 382 | 'scipy.ndimage', 383 | 'scipy.ndimage.map_coordinates', 384 | 'scipy.spatial', 385 | 'scipy.spatial.cKDTree', 386 | 'shutil'] 387 | # 'progressbar', 388 | # 'progressbar.ProgressBar', 389 | # 'progressbar.widgets', 390 | # 'progressbar.widgets.DataSize', 391 | # 'progressbar.widgets.AdaptiveTransferSpeed', 392 | # 'progressbar.widgets.Bar', 393 | # 'progressbar.widgets.AdaptiveETA', 394 | # 'progressbar.widgets.Percentage', 395 | # 'progressbar.widgets.FormatCustomText', 396 | # 'progressbar.utils', 397 | # 'progressbar.utils.scale_1024'] 398 | 399 | # sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) 400 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. role:: python(code) 2 | :language: python 3 | 4 | Examples 5 | ======== 6 | 7 | Getting Started 8 | --------------- 9 | 10 | Here, we'll look up a selection function at a number of different locations on the sky and a number of different magnitudes. 11 | The principal object in `selectionfunctions` is the `Source` object, which has both a `SkyCoord` attribute giving the position and a `Photometry` attribute giving the photometric measurements. 12 | We specify coordinates on the sky using 13 | `astropy.coordinates.SkyCoord `_ 14 | objects. This allows us a great deal of flexibility in how we specify sky 15 | coordinates. We can use different coordinate frames (e.g., 16 | `Galactic `_, 17 | `equatorial `_, 18 | `ecliptic `_), 19 | different units (e.g., degrees, radians, 20 | `hour angles `_), and either 21 | scalar or vector input. 22 | 23 | For our first example, let's load the 24 | `Boubert & Everall (2020, submitted)` 25 | -- or "cog_ii" -- selection function for Gaia DR2, and then query the selection function of a G=21 source at one location 26 | on the sky: 27 | 28 | .. code-block :: python 29 | 30 | from selectionfunctions.source import Source 31 | from selectionfunctions import cog_ii 32 | 33 | coords = Source('12h30m25.3s', '15d15m58.1s', frame='icrs', photometry={'gaia_g':21.0}) 34 | dr2_sf = cog_ii.dr2_sf(version='modelAB',crowding=True) 35 | prob_selection = dr2_sf(coords) 36 | 37 | print('Probability of selection = {:.3f}%'.format(prob_selection*100.0)) 38 | 39 | >>> Probability of selection = 69.877% 40 | 41 | A couple of things to note here: 42 | 43 | 1. Above, we used the 44 | `ICRS coordinate system `_, 45 | by specifying :python:`frame='icrs'`. 46 | 2. We specified the apparent Gaia G magnitude of the star through a Python dictionary. Photometric transformations have not yet been implemented. 47 | 3. We used the keywords :python:`version='modelAB'` and :python:`crowding=True` when constructing the selection function. By default, :python:`crowding=False`. 48 | 49 | In future, you will be able to query other selection funtions from the :code:`selectionfunctions` package with only minor 50 | modification to the above code. 51 | 52 | 53 | Querying Selection Function at an Array of Coordinates 54 | --------------------------------------------- 55 | 56 | We can also query an array of coordinates, as follows: 57 | 58 | 59 | .. code-block :: python 60 | 61 | import numpy as np 62 | from selectionfunctions.source import Source 63 | from selectionfunctions import cog_ii 64 | 65 | l = np.array([0., 90., 180.]) 66 | b = np.array([15., 0., -15.]) 67 | g = np.array([20.8,21.0,21.2]) 68 | 69 | coords = Source(l, b, unit='deg', frame='galactic', photometry={'gaia_g':g}) 70 | 71 | dr2_sf = cog_ii.dr2_sf() 72 | dr2_sf(coords) 73 | >>> array([0.99997069, 0.96233884, 0.58957493]) 74 | 75 | The input need not be a flat array. It can have any shape -- the shape of the 76 | output will match the shape of the input: 77 | 78 | .. code-block :: python 79 | 80 | import numpy as np 81 | from selectionfunctions.source import Source 82 | from selectionfunctions import cog_ii 83 | 84 | l = np.linspace(0., 180., 12) 85 | b = np.zeros(12) 86 | g = 21.0*np.ones(12) 87 | l.shape = (3, 4) 88 | b.shape = (3, 4) 89 | g.shape = (3, 4) 90 | 91 | coords = Source(l, b, unit='deg', frame='galactic', photometry={'gaia_g':g}) 92 | 93 | dr2_sf = cog_ii.dr2_sf() 94 | 95 | prob_selection = dr2_sf(coords) 96 | 97 | print(prob_selection) 98 | >>> [[0.74045863 0.69877491 0.74045863 0.94768624] 99 | [0.98794938 0.93834743 0.95561436 0.96803869] 100 | [0.99962099 0.97286789 0.91445208 0.59940653]] 101 | 102 | print(prob_selection.shape) 103 | >>> (3, 4) 104 | 105 | 106 | Plotting a Selection Function 107 | ---------------------- 108 | 109 | We'll finish by plotting the Gaia DR2 selection function. First, we'll import the necessary modules: 110 | 111 | .. code-block :: python 112 | 113 | import matplotlib 114 | import matplotlib.pyplot as plt 115 | import numpy as np 116 | 117 | import astropy.units as units 118 | 119 | from selectionfunctions.source import Source 120 | from selectionfunctions import cog_ii 121 | 122 | Next, we'll set up a grid of coordinates to plot: 123 | 124 | .. code-block :: python 125 | 126 | l = np.linspace(-180.0, 180.0, 1000) 127 | b = np.linspace(-90.0,90.0, 500) 128 | l, b = np.meshgrid(l, b) 129 | g = 21.0*np.ones(l.shape) 130 | coords = Source(l*units.deg, b*units.deg, frame='galactic', photometry={'gaia_g':g}) 131 | 132 | Then, we'll load up and query the Gaia DR2 selection function: 133 | 134 | .. code-block :: python 135 | 136 | dr2_sf = cog_ii.dr2_sf(version='modelAB',crowding=True) 137 | prob_selection = dr2_sf(coords) 138 | 139 | Finally, we create the figure using :code:`matplotlib`: 140 | 141 | .. code-block :: python 142 | 143 | fig = plt.figure(figsize=(12,4), dpi=150) 144 | 145 | plt.imshow( 146 | prob_selection[::,::-1], 147 | vmin=0., 148 | vmax=1., 149 | origin='lower', 150 | interpolation='nearest', 151 | cmap='viridis', 152 | aspect='equal', 153 | extent=[-180,180,-90,90] 154 | ) 155 | 156 | plt.axis('off') 157 | plt.savefig('map.png', bbox_inches='tight', dpi=150) 158 | 159 | Here's the result: 160 | 161 | .. image :: figs/map.png 162 | -------------------------------------------------------------------------------- /docs/figs/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaiaverse/selectionfunctions/ad30503dd72a8d1710156fdba58d1d90577194c3/docs/figs/map.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. dustmaps documentation master file, created by 2 | sphinx-quickstart on Fri Oct 14 17:20:58 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | selectionfunctions documentation 7 | ==================================== 8 | 9 | :code:`selectionfunctions` provides a unified interface for the selection functions of several major 10 | astronomical surveys. This module is entirely derivative of the excellent :code:`dustmaps` package by Gregory M. Green. 11 | The :code:`selectionfunctions` package is a product of the `Completeness of the Gaia-verse (CoG) `_ collaboration. 12 | 13 | To get started, take a look at :doc:`installation` and 14 | :doc:`examples`. To see a list of all available selection functions, take a look 15 | at :doc:`selectionfunctions`. For a complete reference to the API, see 16 | :doc:`modules`. 17 | 18 | If you make use of :code:`selectionfunctions` in your research, please cite 19 | `Green (2018) `_:: 20 | 21 | @ARTICLE{2018JOSS....3..695M, 22 | author = {{Green}, {Gregory M.}}, 23 | title = "{dustmaps: A Python interface for maps of interstellar dust}", 24 | journal = {The Journal of Open Source Software}, 25 | year = "2018", 26 | month = "Jun", 27 | volume = {3}, 28 | number = {26}, 29 | pages = {695}, 30 | doi = {10.21105/joss.00695}, 31 | adsurl = {https://ui.adsabs.harvard.edu/abs/2018JOSS....3..695M}, 32 | adsnote = {Provided by the SAO/NASA Astrophysics Data System} 33 | } 34 | 35 | 36 | Contents 37 | ======== 38 | 39 | .. toctree:: 40 | :maxdepth: 2 41 | 42 | installation 43 | examples 44 | selectionfunctions 45 | modules 46 | license 47 | 48 | 49 | Indices and tables 50 | ================== 51 | 52 | * :ref:`genindex` 53 | * :ref:`modindex` 54 | * :ref:`search` 55 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | There are two ways to install :code:`selectionfunctions`. If you are familiar with the installation of the :code:`dustmaps` module, 5 | then this will be familiar. 6 | 7 | 8 | 1. Using :code:`pip` 9 | -------------------- 10 | 11 | From the commandline, run 12 | 13 | .. code-block :: bash 14 | 15 | pip install selectionfunctions 16 | 17 | You may have to use :code:`sudo`. 18 | 19 | Next, we'll configure the package and download the selection functions we'll want to use. 20 | Start up a python interpreter and type: 21 | 22 | .. code-block :: python 23 | 24 | from selectionfunctions.config import config 25 | config['data_dir'] = '/path/to/store/maps/in' 26 | 27 | import selectionfunctions.cog_ii 28 | selectionfunctions.cog_ii.fetch() 29 | 30 | All the selection functions should now be in the path you gave to 31 | :code:`config['data_dir']`. Note that these selection functions can be very large - some 32 | are several Gigabytes! Only download those you think you'll need. 33 | 34 | 35 | 2. Using :code:`setup.py` 36 | ------------------------- 37 | 38 | An alternative way to download :code:`selectionfunctions`, if you don't want to use 39 | :code:`pip`, is to download or clone the respository from 40 | https://https://github.com/gaiaverse/selectionfunctions. 41 | 42 | 43 | In this case, you will have to manually make sure that the dependencies are 44 | satisfied: 45 | 46 | * :code:`numpy` 47 | * :code:`scipy` 48 | * :code:`astropy` 49 | * :code:`h5py` 50 | * :code:`healpy` 51 | * :code:`requests` 52 | * :code:`six` 53 | * :code:`progressbar2` 54 | 55 | These packages can typically be installed using the Python package manager, 56 | :code:`pip`. 57 | 58 | Once these dependencies are installed, run the following command from the root 59 | directory of the :code:`selectionfunctions` package: 60 | 61 | .. code-block :: bash 62 | 63 | python setup.py install --large-data-dir=/path/to/store/maps/in 64 | 65 | Then, fetch the selection functions you'd like to use. Depending on which selection functions you choose 66 | to download, this step can take up several Gigabytes of disk space. Be careful 67 | to only download those you think you'll need: 68 | 69 | .. code-block :: bash 70 | 71 | python setup.py fetch --map-name=cog_ii 72 | 73 | That's it! 74 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | The :code:`selectionfunctions` documentation is covered by the MIT License, as given below. 5 | 6 | 7 | The MIT License (MIT) 8 | --------------------- 9 | 10 | Copyright (c) 2020 `Douglas Boubert and Andrew Everall `_ 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | selectionfunctions modules 2 | ========================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 8 | cog_ii (Boubert & Everall, 2020, submitted) 9 | -------------------------------------------------------- 10 | .. automodule:: selectionfunctions.cog_ii 11 | :members: 12 | :special-members: 13 | :show-inheritance: 14 | 15 | fetch_utils 16 | ----------- 17 | .. automodule:: selectionfunctions.fetch_utils 18 | :members: 19 | :special-members: 20 | :show-inheritance: 21 | 22 | 23 | map 24 | -------- 25 | .. automodule:: selectionfunctions.map 26 | :members: 27 | :special-members: __call__ 28 | :show-inheritance: 29 | 30 | source 31 | ----------- 32 | .. automodule:: selectionfunctions.source 33 | :members: 34 | :special-members: __call__ 35 | :show-inheritance: 36 | 37 | 38 | healpix_map 39 | ----------- 40 | .. automodule:: selectionfunctions.healpix_map 41 | :members: 42 | :special-members: 43 | :show-inheritance: 44 | 45 | 46 | unstructured_map 47 | ---------------- 48 | .. automodule:: selectionfunctions.unstructured_map 49 | :members: 50 | :special-members: 51 | :show-inheritance: 52 | 53 | 54 | config 55 | ------ 56 | .. automodule:: selectionfunctions.config 57 | :members: 58 | :show-inheritance: 59 | 60 | 61 | std_paths 62 | --------- 63 | .. automodule:: selectionfunctions.std_paths 64 | :members: 65 | :special-members: 66 | :show-inheritance: 67 | 68 | 69 | json_serializers 70 | ---------------- 71 | .. automodule:: selectionfunctions.json_serializers 72 | :members: 73 | :special-members: 74 | :show-inheritance: 75 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | argparse>=1.2.1 2 | nose>=1.3.7 3 | Sphinx>=1.4.6 4 | sphinx-autobuild>=0.6.0 5 | sphinxcontrib-napoleon>=0.5.3 6 | sphinxcontrib-programoutput>=0.8 7 | progressbar2>=3.30.2 8 | six>=1.10.0 9 | -------------------------------------------------------------------------------- /docs/selectionfunctions.rst: -------------------------------------------------------------------------------- 1 | Available Selection Functions 2 | ============================= 3 | 4 | 5 | Gaia selection functions 6 | ---------------------------- 7 | 8 | 9 | Gaia DR2 (cog_ii.dr2_sf) 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | The selection function of the Gaia source catalogue is principally determined by the scanning law, which gives the number of times that Gaia observes every location on the sky. 13 | Sources must be detected at least five times to have made it into Gaia DR2, but sources are not detected every time Gaia observes them. 14 | This selection function models the probability that a source is detected as a function of G magnitude and, optionally, the local source density. 15 | 16 | There are four variants of this selection function implemented, which can be used by changing the `version` and `crowding` parameters. We recommend setting :python:`version='modelAB'` and :python:`crowding=True` for most applications, but these are not set by default. 17 | 18 | * **Reference**: `Boubert & Everall (2020, submitted)` 19 | -------------------------------------------------------------------------------- /selectionfunctions/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # __init__.py 4 | # Makes the contents of the package "dustmaps" discoverable. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | -------------------------------------------------------------------------------- /selectionfunctions/cog_ii.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # cog_ii.py 4 | # Reads the Gaia DR2 selection function from Completeness 5 | # of the Gaia-verse Paper II, Boubert & Everall (2020). 6 | # 7 | # Copyright (C) 2020 Douglas Boubert & Andrew Everall 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | # 23 | 24 | from __future__ import print_function, division 25 | 26 | import os 27 | import h5py 28 | import numpy as np 29 | 30 | import astropy.coordinates as coordinates 31 | import astropy.units as units 32 | import h5py 33 | import healpy as hp 34 | from scipy import interpolate, special 35 | 36 | from .std_paths import * 37 | from .map import SelectionFunction, ensure_flat_icrs, coord2healpix 38 | from .source import ensure_gaia_g 39 | from . import fetch_utils 40 | 41 | from time import time 42 | 43 | 44 | class dr2_sf(SelectionFunction): 45 | """ 46 | Queries the Gaia DR2 selection function (Boubert & Everall, 2019). 47 | """ 48 | 49 | def __init__(self, map_fname=None, version='modelAB', crowding=False, bounds=True): 50 | """ 51 | Args: 52 | map_fname (Optional[:obj:`str`]): Filename of the BoubertEverall2019 selection function. Defaults to 53 | :obj:`None`, meaning that the default location is used. 54 | version (Optional[:obj:`str`]): The selection function version to download. Valid versions 55 | are :obj:`'modelT'` and :obj:`'modelAB'` 56 | Defaults to :obj:`'modelT'`. 57 | crowding (Optional[:obj:`bool`]): Whether or not the selection function includes crowding. 58 | Defaults to :obj:`'False'`. 59 | bounds (Optional[:obj:`bool`]): Whether or not the selection function is bounded to 0.0 < G < 25.0. 60 | Defaults to :obj:`'True'`. 61 | """ 62 | 63 | if map_fname is None: 64 | map_fname = os.path.join(data_dir(), 'cog_ii', 'cog_ii_dr2.h5') 65 | 66 | t_start = time() 67 | 68 | with h5py.File(map_fname, 'r') as f: 69 | # Load auxilliary data 70 | print('Loading auxilliary data ...') 71 | self._g_grid = f['g_grid'][...] 72 | self._n_field = f['n_field'][...] 73 | self._nside = hp.npix2nside(self._n_field.shape[0]) 74 | self._crowding = crowding 75 | self._bounds = bounds 76 | if crowding == True: 77 | self._nside_crowding = 1024 78 | self._log10_rho_grid = f['log10_rho_grid'][...] 79 | self._log10_rho_field = np.log10(np.maximum(1.0,f['neighbour_field'][...])/hp.nside2pixarea(self._nside_crowding,degrees=True)) 80 | 81 | t_auxilliary = time() 82 | 83 | # Load selection function 84 | print('Loading selection function ...') 85 | if version == 'modelT': 86 | if crowding == True: 87 | self._theta = f['t_theta_percentiles'][:,:,2] 88 | else: 89 | self._theta = f['t_theta_percentiles'][0,:,2] 90 | elif version == 'modelAB': 91 | if crowding == True: 92 | self._alpha = f['ab_alpha_percentiles'][:,:,2] 93 | self._beta = f['ab_beta_percentiles'][:,:,2] 94 | else: 95 | self._alpha = f['ab_alpha_percentiles'][0,:,2] 96 | self._beta = f['ab_beta_percentiles'][0,:,2] 97 | 98 | if bounds == True: 99 | self._g_min = 0.0 100 | self._g_max = 25.0 101 | else: 102 | self._g_min = -np.inf 103 | self._g_max = np.inf 104 | 105 | t_sf = time() 106 | 107 | # Create interpolator 108 | print('Creating selection function interpolator...') 109 | if version == 'modelT': 110 | if crowding == True: 111 | self._theta_interpolator = interpolate.RectBivariateSpline(self._log10_rho_grid,self._g_grid,self._theta) 112 | self._interpolator = lambda _log10_rho, _g : (self._theta_interpolator(_log10_rho, _g, grid = False),) 113 | else: 114 | self._theta_interpolator = interpolate.interp1d(self._g_grid,self._theta,kind='linear',fill_value='extrapolate') 115 | self._interpolator = lambda _g : (self._theta_interpolator(_g),) 116 | elif version == 'modelAB': 117 | if crowding == True: 118 | self._alpha_interpolator = interpolate.RectBivariateSpline(self._log10_rho_grid,self._g_grid,self._alpha) 119 | self._beta_interpolator = interpolate.RectBivariateSpline(self._log10_rho_grid,self._g_grid,self._beta) 120 | self._interpolator = lambda _log10_rho, _g : (self._alpha_interpolator(_log10_rho, _g, grid = False),self._beta_interpolator(_log10_rho, _g, grid = False)) 121 | else: 122 | self._alpha_interpolator = interpolate.interp1d(self._g_grid,self._alpha,kind='linear',fill_value='extrapolate') 123 | self._beta_interpolator = interpolate.interp1d(self._g_grid,self._beta,kind='linear',fill_value='extrapolate') 124 | self._interpolator = lambda _g : (self._alpha_interpolator(_g),self._beta_interpolator(_g)) 125 | 126 | t_interpolator = time() 127 | 128 | t_finish = time() 129 | 130 | print('t = {:.3f} s'.format(t_finish - t_start)) 131 | print(' auxilliary: {: >7.3f} s'.format(t_auxilliary-t_start)) 132 | print(' sf: {: >7.3f} s'.format(t_sf-t_auxilliary)) 133 | print('interpolator: {: >7.3f} s'.format(t_interpolator-t_sf)) 134 | 135 | def _selection_function(self,_n,_parameters): 136 | if len(_parameters) == 1: 137 | 138 | # This must be Model T, _parameters = (theta) 139 | _t = _parameters[0] 140 | 141 | # 0 < theta < 1, make it so! 142 | _t[_t<0.0] = 1e-6 143 | _t[_t>1.0] = 1-1e-6 144 | 145 | _result = special.betainc(5,_n-4,_t) 146 | 147 | elif len(_parameters) == 2: 148 | 149 | # This must be Model AB, _parameters = (alpha,beta) 150 | _a, _b = _parameters 151 | 152 | # 0.1 < alpha,beta < 10000, make it so! 153 | _a[_a<1e-1] = 1e-1 154 | _a[_a>1e+4] = 1e+4 155 | _b[_b<1e-1] = 1e-1 156 | _b[_b>1e+4] = 1e+4 157 | 158 | _result = np.ones(_a.shape) 159 | for _m in range(1,5)[::-1]: 160 | _result = 1.0 + _result*((_n-_m+1)/_m)*(_a+_m-1)/(_b+_n-_m) 161 | _result = 1.0 - _result*special.beta(_a,_b+_n)/special.beta(_a,_b) 162 | 163 | return _result 164 | 165 | 166 | @ensure_flat_icrs 167 | @ensure_gaia_g 168 | def query(self, sources): 169 | """ 170 | Returns the selection function at the requested coordinates. 171 | 172 | Args: 173 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates to query. 174 | 175 | Returns: 176 | Selection function at the specified coordinates, as a fraction. 177 | 178 | """ 179 | 180 | # Convert coordinates to healpix indices 181 | hpxidx = coord2healpix(sources.coord, 'icrs', self._nside, nest=True) 182 | 183 | # Calculate the number of observations of each source 184 | n = self._n_field[hpxidx] 185 | 186 | # Extract Gaia G magnitude 187 | G = sources.photometry.measurement['gaia_g'] 188 | 189 | if self._crowding == True: 190 | 191 | # Work out HEALPix index in crowding nside 192 | hpxidx_crowding = np.floor(hpxidx * hp.nside2npix(self._nside_crowding) / hp.nside2npix(self._nside)).astype(np.int) 193 | 194 | # Calculate the local density field at each source 195 | log10_rho = self._log10_rho_field[hpxidx_crowding] 196 | 197 | # Calculate parameters 198 | sf_parameters = self._interpolator(log10_rho,G) 199 | 200 | else: 201 | 202 | # Calculate parameters 203 | sf_parameters = self._interpolator(G) 204 | 205 | # Evaluate selection function 206 | selection_function = self._selection_function(n,sf_parameters) 207 | 208 | if self._bounds == True: 209 | _outside_bounds = np.where( (Gself._g_max) ) 210 | selection_function[_outside_bounds] = 0.0 211 | 212 | return selection_function 213 | 214 | 215 | class dr3_sf(dr2_sf): 216 | def __init__(self, map_fname_dr3=None,map_fname_dr2=None, version='modelAB', crowding=False, bounds=True): 217 | 218 | if map_fname_dr3 is None: 219 | map_fname_dr3 = os.path.join(data_dir(), 'cog_ii', 'n_field_dr3.h5') 220 | 221 | dr2_sf.__init__(self, map_fname_dr2, version, crowding, bounds) 222 | 223 | with h5py.File(map_fname_dr3, 'r') as f: 224 | # Load auxilliary data 225 | print('Loading auxilliary data ...') 226 | self._n_field = f['n_field'][...] 227 | self._nside = hp.npix2nside(self._n_field.shape[0]) 228 | 229 | 230 | def fetch(): 231 | """ 232 | Downloads the specified version of the Bayestar dust map. 233 | 234 | Args: 235 | version (Optional[:obj:`str`]): The map version to download. Valid versions are 236 | :obj:`'bayestar2019'` (Green, Schlafly, Finkbeiner et al. 2019), 237 | :obj:`'bayestar2017'` (Green, Schlafly, Finkbeiner et al. 2018) and 238 | :obj:`'bayestar2015'` (Green, Schlafly, Finkbeiner et al. 2015). Defaults 239 | to :obj:`'bayestar2019'`. 240 | 241 | Raises: 242 | :obj:`ValueError`: The requested version of the map does not exist. 243 | 244 | :obj:`DownloadError`: Either no matching file was found under the given DOI, or 245 | the MD5 sum of the file was not as expected. 246 | 247 | :obj:`requests.exceptions.HTTPError`: The given DOI does not exist, or there 248 | was a problem connecting to the Dataverse. 249 | """ 250 | 251 | doi = {'dr2': '10.7910/DVN/PDFOVC', 252 | 'edr3_nfield': '10.7910/DVN/I3TGTS'} 253 | 254 | requirements = {'dr2': {'filename': 'cog_ii_dr2.h5'}, 255 | 'edr3_nfield':{'filename': 'n_field_dr3.h5'}} 256 | 257 | local_fname = os.path.join(data_dir(), 'cog_ii', requirements['dr2']['filename']) 258 | # Download the data 259 | fetch_utils.dataverse_download_doi( 260 | doi['dr2'], 261 | local_fname, 262 | file_requirements=requirements['dr2']) 263 | 264 | local_fname = os.path.join(data_dir(), 'cog_ii', requirements['edr3_nfield']['filename']) 265 | # Download the data 266 | fetch_utils.dataverse_download_doi( 267 | doi['edr3_nfield'], 268 | local_fname, 269 | file_requirements=requirements['edr3_nfield']) 270 | -------------------------------------------------------------------------------- /selectionfunctions/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # config.py 4 | # Allow configuration options to be set. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import json 26 | import os 27 | 28 | 29 | class ConfigError(Exception): 30 | pass 31 | 32 | 33 | class Configuration(object): 34 | """ 35 | A class that stores the package configuration. 36 | """ 37 | 38 | def __init__(self, fname): 39 | self._success = False 40 | self.fname = fname 41 | self.load() 42 | 43 | def load(self): 44 | if os.path.isfile(self.fname): 45 | with open(self.fname, 'r') as f: 46 | try: 47 | self._options = json.load(f) 48 | self._success = True 49 | except ValueError as error: 50 | print(('The config file appears to be corrupted:\n\n' 51 | ' {fname}\n\n' 52 | 'Either fix the config file manually, or overwrite ' 53 | 'it with a blank configuration as follows:\n\n' 54 | ' from selectionfunctions.config import config\n' 55 | ' config.reset()\n\n' 56 | 'Note that this will delete your configuration! For ' 57 | 'example, if you have specified a data directory, ' 58 | 'then selectionfunctions will forget about its location.' 59 | ).format(fname=self.fname)) 60 | self._options = {} 61 | else: 62 | self._options = {} 63 | self._success = True 64 | 65 | def save(self, force=False): 66 | """ 67 | Saves the configuration to a JSON, in the standard config location. 68 | 69 | Args: 70 | force (Optional[:obj:`bool`]): Continue writing, even if the original 71 | config file was not loaded properly. This is dangerous, because 72 | it could cause the previous configuration options to be lost. 73 | Defaults to :obj:`False`. 74 | 75 | Raises: 76 | :obj:`ConfigError`: if the configuration file was not successfully 77 | loaded on initialization of the class, and 78 | :obj:`force` is :obj:`False`. 79 | """ 80 | if (not self._success) and (not force): 81 | raise ConfigError(( 82 | 'The config file appears to be corrupted:\n\n' 83 | ' {fname}\n\n' 84 | 'Before attempting to save the configuration, please either ' 85 | 'fix the config file manually, or overwrite it with a blank ' 86 | 'configuration as follows:\n\n' 87 | ' from selectionfunctions.config import config\n' 88 | ' config.reset()\n\n' 89 | ).format(fname=self.fname)) 90 | 91 | with open(self.fname, 'w') as f: 92 | json.dump(self._options, f, indent=2) 93 | 94 | def __setitem__(self, key, value): 95 | self._options[key] = value 96 | self.save() 97 | 98 | def __getitem__(self, key): 99 | return self._options.get(key, None) 100 | 101 | def get(self, key, default=None): 102 | """ 103 | Gets a configuration option, returning a default value if the specified 104 | key isn't set. 105 | """ 106 | return self._options.get(key, default) 107 | 108 | def remove(self, key): 109 | """ 110 | Deletes a key from the configuration. 111 | """ 112 | self._options.pop(key, None) 113 | self.save() 114 | 115 | def reset(self): 116 | """ 117 | Resets the configuration, and overwrites the existing configuration 118 | file. 119 | """ 120 | self._options = {} 121 | self.save(force=True) 122 | self._success = True 123 | 124 | 125 | # The package configuration filename 126 | config_fname = os.path.expanduser('~/.selectionfunctionsrc') 127 | 128 | #: The package configuration. This is the object that the user should interact 129 | #: with in order to change settings. For example, to set the directory where 130 | #: large files (e.g., selections functions) will be stored: 131 | #: 132 | #: .. code-block:: python 133 | #: 134 | #: from selectionfunctions.config import config 135 | #: config['data_dir'] = '/path/to/data/directory' 136 | config = Configuration(config_fname) 137 | -------------------------------------------------------------------------------- /selectionfunctions/equirectangular_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # equirectangular_map.py 4 | # Implements a class for querying selection functions that are stored in an 5 | # Equirectangular projection. 6 | # 7 | # Copyright (C) 2020 Douglas Boubert 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | # 23 | 24 | from __future__ import print_function, division 25 | 26 | import numpy as np 27 | import astropy.coordinates as coordinates 28 | import astropy.units as units 29 | from scipy.spatial import cKDTree as KDTree 30 | from astropy.coordinates import Longitude 31 | 32 | from .map_base import SelectionFunction, ensure_flat_coords 33 | 34 | 35 | class EquirectangularSelectionFunction(SelectionFunction): 36 | """ 37 | A class for querying selection functions stored in an Equirectangular 38 | projection. The maps may optionally include distances as well. 39 | """ 40 | 41 | def __init__(self, pix_values, lon0, lon1, lat0, lat1, 42 | dist0=None, dist1=None, 43 | axis_order=('lon','lat','dist'), 44 | frame='galactic', 45 | dist_interp='linear'): 46 | """ 47 | Args: 48 | pix_values (:obj:`np.ndarray`): An array containing the pixel 49 | values. For a 3D selection function (with distance), the array should be 50 | 3D, with the order of the axes corresponding to 51 | :obj:`axis_order`. For a 2D selection function, the array should be 2D. 52 | lon0 (float): The lower limiting longitude of the map, in deg. 53 | lon1 (float): The upper limiting longitude of the map, in deg. 54 | lat0 (float): The lower limiting latitude of the map, in deg. 55 | lat1 (float): The upper limiting latitude of the map, in deg. 56 | dist0 (Optional[:obj:`astropy.units.Quantity`]): The lower 57 | limiting distance of the map. If :obj:`dist0` has units of 58 | distance of the map. If :obj:`dist0` has units of 59 | :obj:`'mag'`, then it is assumed to be a distance modulus, 60 | and the distance bins are assumed to be spaced linearly 61 | in distance modulus, instead of distance. 62 | dist1 (Optional[:obj:`astropy.units.Quantity`]): The upper 63 | limiting distance of the map. If :obj:`dist1` has units of 64 | :obj:`'mag'`, then it is assumed to be a distance modulus, 65 | and the distance bins are assumed to be spaced linearly 66 | in distance modulus, instead of distance. 67 | axis_order (tuple of str): The order of the axes in 68 | :obj:`pix_values`. Defaults to :obj:`('lon','lat','dist')`. 69 | For 2D maps, do not include :obj:`'dist'`. 70 | frame (Optional[:obj:`str`]): The coordinate frame to which the 71 | longitudes and latitudes correspond. Must be a frame 72 | understood by :obj:`astropy.coordinates.SkyCoord`. 73 | Defaults to :obj:`'galactic'`. 74 | dist_interp (:obj:`str`): How to interpolate between distance 75 | slices in the map. Valid choices are :obj:`'step'` and 76 | :obj:`'linear'`. Defaults to :obj:`'linear'`. 77 | """ 78 | self._frame = frame 79 | 80 | # Read (lon, lat, dist) bounds 81 | self._wrap_angle = lon0 * units.deg 82 | self._lon_lim = ( 83 | Longitude(lon0, unit='deg', wrap_angle=self._wrap_angle), 84 | Longitude(lon1, unit='deg', wrap_angle=self._wrap_angle), 85 | ) 86 | 87 | self._lat_lim = (lat0, lat1) 88 | 89 | if dist0 is not None: 90 | if dist0.unit == 'mag': 91 | self._dm = True 92 | self._dist_lim = (dist0.value, dist1.value) 93 | else: 94 | self._dm = False 95 | self._dist_lim = ( 96 | dist0.to('kpc').value, 97 | dist1.to('kpc').value 98 | ) 99 | 100 | # Read mapping from axis -> (lon, lat, dist) 101 | self._axis_dist = None 102 | 103 | if len(axis_order) < 2: 104 | raise ValueError("axis_order must have at least " + 105 | "two entries ('lon' and 'lat').") 106 | 107 | for a in ('lon', 'lat'): 108 | if a not in axis_order: 109 | raise ValueError("'{}' must be in axis_order.".format(a)) 110 | 111 | for i,a in enumerate(axis_order): 112 | if a == 'lon': 113 | self._axis_lon = i 114 | elif a == 'lat': 115 | self._axis_lat = i 116 | elif a == 'dist': 117 | self._axis_dist = i 118 | else: 119 | raise ValueError("Unknown entry in axis_order: " + 120 | "'{}'".format(a)) 121 | 122 | # Get (lon,lat,dist) grid properties 123 | self._n_lon = pix_values.shape[self._axis_lon] 124 | self._n_lat = pix_values.shape[self._axis_lat] 125 | 126 | if self._lon_lim[1] == self._lon_lim[0]: 127 | self._dlon = 360. / self._n_lon 128 | else: 129 | self._dlon = (self._lon_lim[1] - self._lon_lim[0]) / self._n_lon 130 | self._dlon = self._dlon.to('deg').value 131 | self._dlat = (self._lat_lim[1] - self._lat_lim[0]) / self._n_lat 132 | 133 | if self._axis_dist is not None: 134 | self._n_dist = pix_values.shape[self._axis_dist] 135 | self._ddist = (self._dist_lim[1] - self._dist_lim[0]) / self._n_dist 136 | 137 | if self._dm: 138 | # Physical distance of each slice (in kpc) 139 | self._ddist_phys = 10.**( 140 | 0.2 * np.linspace( 141 | self._dist_lim[0], 142 | self._dist_lim[1], 143 | self._n_dist 144 | ) - 2. 145 | ) 146 | 147 | # Get pixel values 148 | self._pix_values = pix_values 149 | self._n_axes = len(pix_values.shape) 150 | 151 | self._dist_interp = dist_interp 152 | 153 | def _coords2idx(self, coords, diff=False): 154 | c = coords.transform_to(self._frame).represent_as('spherical') 155 | 156 | idx = np.empty(coords.shape + (self._n_axes,), dtype='i4') 157 | 158 | lon = Longitude(c.lon, wrap_angle=self._wrap_angle) 159 | lon_idx = (lon - self._lon_lim[0]).to('deg').value / self._dlon 160 | lat_idx = (c.lat.deg - self._lat_lim[0]) / self._dlat 161 | 162 | lon_idx = np.floor(lon_idx).astype('i4') 163 | lat_idx = np.floor(lat_idx).astype('i4') 164 | 165 | mask = ( 166 | (lon_idx < 0) | (lon_idx >= self._n_lon) | 167 | (lat_idx < 0) | (lat_idx >= self._n_lat) 168 | ) 169 | 170 | if self._axis_dist is not None: 171 | dist = c.distance.to('kpc').value 172 | 173 | if self._dm: 174 | dist = 5. * (np.log10(dist) + 2.) 175 | 176 | dist_idx = (dist - self._dist_lim[0]) / self._ddist 177 | dist_idx_close = np.floor(dist_idx).astype('i4') 178 | 179 | # Differential extinction 180 | if diff: 181 | dist_idx_far = (dist_idx_close + 1).clip(0, self._n_dist-1) 182 | close_mask = (dist_idx_close < 0) 183 | far_mask = (dist_idx_close >= self._n_dist) 184 | elif self._dist_interp == 'step': 185 | dist_idx_close = dist_idx_close.clip(-1, self._n_dist-1) 186 | close_mask = (dist_idx_close == -1) 187 | elif self._dist_interp == 'linear': 188 | far_weight = dist_idx - dist_idx_close 189 | 190 | # Beyond farthest distance slice 191 | far_mask = (dist_idx_close >= self._n_dist-1) 192 | if np.any(far_mask): 193 | dist_idx_close[far_mask] = self._n_dist - 2 194 | far_weight[far_mask] = 1.0 195 | 196 | # Closer than closest distance slice 197 | close_mask = (dist_idx_close < 0) 198 | dist_idx_close[close_mask] = -1 199 | if np.any(close_mask): 200 | if self._dm: 201 | far_weight[close_mask] = ( 202 | 10.**(0.2 * (dist[close_mask]-self._dist_lim[0])) 203 | ) 204 | else: 205 | far_weight[close_mask] = ( 206 | dist[close_mask] / self._dist_lim[0] 207 | ) 208 | 209 | #mask |= (dist_idx < 0) | (dist_idx >= self._n_dist) 210 | 211 | 212 | if np.any(mask): 213 | lon_idx[mask] = -1 214 | lat_idx[mask] = -1 215 | 216 | if self._axis_dist is not None: 217 | dist_idx_close[mask] = -1 218 | 219 | idx[:,self._axis_lon] = lon_idx 220 | idx[:,self._axis_lat] = lat_idx 221 | 222 | # Include distance indices 223 | if self._axis_dist is not None: 224 | if diff: 225 | # For differential selection function, need: 226 | # 1. Index of distance bin just beyond d 227 | # 2. Mask of out-of-bounds (lon, lat) 228 | # 3. Mask of which pixels are closer than closest slice 229 | # 3. Mask of which pixels are farther than farthest slice 230 | idx[:,self._axis_dist] = dist_idx_far 231 | return idx, mask, (close_mask, far_mask) 232 | else: 233 | idx[:,self._axis_dist] = dist_idx_close 234 | 235 | if self._dist_interp == 'step': 236 | # For step-function interpolation, need: 237 | # 1. Index of distance bin just inside d 238 | # 2. Mask of out-of-bounds (lon, lat) 239 | # 3. Mask of which pixels are closer than closest slice 240 | return idx, mask, close_mask 241 | elif self._dist_interp == 'linear': 242 | # For piecewise-linear interpolation, need: 243 | # 1. Index of distance bin just inside d 244 | # 2. Mask of out-of-bounds (lon, lat) 245 | # 3. For each pixel, weight to apply to next distance bin 246 | return idx, mask, far_weight 247 | 248 | return idx, mask, None 249 | 250 | @ensure_flat_coords 251 | def query(self, coords, diff=False): 252 | """ 253 | Returns the selection function or reddening density (in mag/kpc) 254 | at the given coordinates. 255 | 256 | Args: 257 | coords (:obj:`astropy.coordinates.SkyCoord`): Coordinates at which 258 | to query the reddening. If the map is 3D, these coordinates 259 | must include distance. If the map is 2D, then the distance 260 | of the coordinates will be ignored. 261 | diff (bool): If :obj:`False` (the default), then the cumulative 262 | reddening is returned. If :obj:`True`, then the reddening 263 | density (in mag/kpc) is returned. This parameter is ignored 264 | for 2D maps. 265 | 266 | Returns: 267 | Either the cumulative reddening or reddening density, as a 268 | numpy array or float, with the same shape as the input 269 | :obj:`coords`. 270 | """ 271 | idx,mask,dist_info = self._coords2idx(coords, diff=diff) 272 | idx_tuple = tuple([idx[...,i] for i in range(idx.shape[-1])]) 273 | 274 | v = self._pix_values[idx_tuple] 275 | 276 | if self._axis_dist is not None: 277 | if diff: 278 | if self._dm: 279 | # Convert distance modulus of closest slice to 280 | # physical distance 281 | d0 = 10.**(0.2 * self._dist_lim[0] - 2.) 282 | else: 283 | d0 = self._dist_lim[0] 284 | 285 | close_mask, far_mask = dist_info 286 | 287 | if np.any(close_mask): 288 | # Inside closest distance slice, interpolate linearly 289 | # between reddening = 0 at distance = 0 and the reddening 290 | # of the first distance slice. 291 | v[close_mask] /= d0 292 | 293 | if np.any(far_mask): 294 | # Beyond farthest distance bin, no differential reddening. 295 | v[far_mask] = 0. 296 | 297 | middle_mask = ~close_mask & ~far_mask 298 | 299 | if np.any(middle_mask): 300 | # Subtract reddening in previous bin, and divide 301 | # by width of distance bins. 302 | if self._dm: 303 | ddist = ( 304 | self._ddist_phys[idx_tuple[self._axis_dist]] - 305 | self._ddist_phys[idx_tuple[self._axis_dist]-1] 306 | )[middle_mask] 307 | else: 308 | ddist = self._ddist 309 | 310 | idx_tuple[self._axis_dist][:] -= 1 311 | v[middle_mask] -= self._pix_values[idx_tuple][middle_mask] 312 | v[middle_mask] /= ddist 313 | 314 | v *= units.mag / units.kpc 315 | elif self._dist_interp == 'step': 316 | close_mask = dist_info 317 | if np.any(close_mask): 318 | # Zero reddening nearer than closest distance slice. 319 | v[close_mask] = 0. 320 | elif self._dist_interp == 'linear': 321 | close_mask = (idx_tuple[self._axis_dist] == -1) 322 | if np.any(close_mask): 323 | # Insert reddening = 0 slice at distance = 0 324 | v[close_mask] = 0. 325 | 326 | # Weight near slice and add in weighted far slice 327 | v *= (1. - dist_info) 328 | idx_tuple[self._axis_dist][:] += 1 329 | v += dist_info * self._pix_values[idx_tuple] 330 | 331 | if np.any(mask): 332 | # Set reddening to NaN for out-of-bounds (lon, lat) 333 | v[mask] = np.nan 334 | 335 | return v 336 | -------------------------------------------------------------------------------- /selectionfunctions/examples/FIRE/example_fire.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Imports for plotting" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%matplotlib inline\n", 17 | "import matplotlib as mpl\n", 18 | "mpl.rcParams['figure.dpi']= 300\n", 19 | "%config InlineBackend.figure_format = 'retina'\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import numpy as np, healpy as hp, h5py, scipy" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 2, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "%config Completer.use_jedi = False" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "# Load data" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "with h5py.File('/data/asfe2/Projects/sims/fire/lsr-1-rslice-5.m12f-res7100-md-sliced-gcat-dr2.hdf5', 'r') as hf:\n", 55 | " print(hf.keys())" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 4, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "data = {}\n", 65 | "keys = ['ra_true', 'dec_true', 'parallax_true', 'pmra_true', 'pmdec_true', 'phot_g_mean_mag_true']\n", 66 | "with h5py.File('/data/asfe2/Projects/sims/fire/lsr-1-rslice-5.m12f-res7100-md-sliced-gcat-dr2.hdf5', 'r') as hf:\n", 67 | " for key in keys:\n", 68 | " data[key]=hf[key][...]" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "# Load Selection Function" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 5, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "from selectionfunctions.config import config\n", 85 | "config['data_dir'] = '/data/asfe2/Projects/testselectionfunctions/'" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 6, 91 | "metadata": {}, 92 | "outputs": [ 93 | { 94 | "name": "stdout", 95 | "output_type": "stream", 96 | "text": [ 97 | "Checking existing file to see if MD5 sum matches ...\n", 98 | "File exists. Not overwriting.\n", 99 | "Checking existing file to see if MD5 sum matches ...\n", 100 | "File exists. Not overwriting.\n" 101 | ] 102 | } 103 | ], 104 | "source": [ 105 | "import selectionfunctions.cog_ii\n", 106 | "selectionfunctions.cog_ii.fetch()" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "import selectionfunctions.cog_ii as CoGII\n", 116 | "from selectionfunctions.source import Source\n", 117 | "from selectionfunctions.map import coord2healpix" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "The DR3 selection function is very approximate. It uses the magnitude relation for the DR2 selection function with the DR3 scanning law. We're working on a new DR3 selection function but we won't have it for a while." 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 8, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "name": "stdout", 134 | "output_type": "stream", 135 | "text": [ 136 | "Loading auxilliary data ...\n", 137 | "Loading selection function ...\n", 138 | "Creating selection function interpolator...\n", 139 | "t = 5.335 s\n", 140 | " auxilliary: 5.332 s\n", 141 | " sf: 0.002 s\n", 142 | "interpolator: 0.001 s\n", 143 | "Loading auxilliary data ...\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "dr3_sf = CoGII.dr3_sf(version='modelAB',crowding=True)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "# Apparent magnitude uncertainty" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "### G-band uncertainty per observation\n", 163 | "$G_\\mathrm{amp} = \\frac{\\sqrt{N_G}\\sigma_{F_G}}{F_G}$\n", 164 | "\n", 165 | "This is the expected G-band uncertainty per observation. I've taken the median in magnitude bins for all Gaia EDR3 data." 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 9, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "med_gamp = {}\n", 175 | "with h5py.File('median_gamp_edr3.h', 'r') as hf:\n", 176 | " for key in ['magbin','med_gamp']: med_gamp[key]=hf[key][...]" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 10, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "gamp_interp = scipy.interpolate.interp1d(med_gamp['magbin']+0.05, med_gamp['med_gamp'], bounds_error=False)" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 11, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "phot_g_error_amp = gamp_interp(data['phot_g_mean_mag_true'])" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "### Expected number of observations\n", 202 | "\n", 203 | "$N_\\mathrm{transit}$ is the expected number of scans across the sky from the scanning law. This is saved inside the dr3 selection function as \"_n_field\".\n", 204 | "\n", 205 | "I've used the Beta-Binomial distribution to get the expected number of G-band observations." 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 12, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "coords = Source(data['ra_true'], data['dec_true'], unit='deg', frame='icrs', \n", 215 | " photometry={'gaia_g':data['phot_g_mean_mag_true']})" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 13, 221 | "metadata": {}, 222 | "outputs": [], 223 | "source": [ 224 | "# Get healpix pixels\n", 225 | "hpxidx = coord2healpix(coords.coord, 'icrs', dr3_sf._nside, nest=True)\n", 226 | "# Get number of transits from scanning law.\n", 227 | "n_transit = dr3_sf._n_field[hpxidx]" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 14, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "# G-observations efficiency\n", 237 | "exp_eff = {}\n", 238 | "with h5py.File('expected_gobs_efficiency_edr3.h', 'r') as hf:\n", 239 | " for key in ['magbin','mean_eff']: exp_eff[key]=hf[key][...]" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 15, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "# Expected value of Beta-Binomial distribution\n", 249 | "eff_interp = scipy.interpolate.interp1d(exp_eff['magbin']+0.05, exp_eff['mean_eff'], bounds_error=False)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": 16, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "# There are 9 CCD observations per transit in Gaia. \n", 259 | "# The efficiency is the expected number of CCD observations which results in a G-band measurement.\n", 260 | "phot_g_n_obs = n_transit * 9 * eff_interp(data['phot_g_mean_mag_true'])" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": {}, 266 | "source": [ 267 | "### Expected G uncertainty\n", 268 | "\n", 269 | "Invert the G flux amplitude to get back the flux error then use this to resample an observed apparent magnitude.\n", 270 | "\n", 271 | "$\\sigma_{F_G} = \\frac{G_\\mathrm{amp} F_G}{\\sqrt{N_G}}$" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": 17, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "data['phot_g_mean_flux_true'] = 10**(-data['phot_g_mean_mag_true']/2.5)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 18, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "data['phot_g_mean_flux_error'] = phot_g_error_amp * data['phot_g_mean_flux_true'] / np.sqrt(phot_g_n_obs)\n", 290 | "data['phot_g_mean_flux'] = np.random.normal(data['phot_g_mean_flux_true'], data['phot_g_mean_flux_error'])" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 19, 296 | "metadata": {}, 297 | "outputs": [], 298 | "source": [ 299 | "data['phot_g_mean_mag'] = -2.5*np.log10(data['phot_g_mean_flux'])" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "metadata": {}, 305 | "source": [ 306 | "## Apply Selection Function to survey data" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 20, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "coords = Source(data['ra_true'], data['dec_true'], unit='deg', frame='icrs', \n", 316 | " photometry={'gaia_g':data['phot_g_mean_mag']})" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 21, 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "# Evaluate selection probability\n", 326 | "data['prob_selection'] = dr3_sf(coords)" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 22, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "# Draw selected sample from Bernoulli distribution\n", 336 | "data['selected'] = np.random.rand(len(data['prob_selection'])) tol: 124 | # print('File size is wrong:') 125 | # print(' expected: {: >16d}'.format(size_guess)) 126 | # print(' found: {: >16d}'.format(size)) 127 | return False 128 | 129 | # Check the datasets in the file 130 | if len(dsets): 131 | import h5py 132 | try: 133 | with h5py.File(fname, 'r') as f: 134 | for key in dsets: 135 | # Check that dataset is in file 136 | if key not in f: 137 | # print('Dataset "{}" not in file.'.format(key)) 138 | return False 139 | # Check that the shape of the dataset is correct 140 | if dsets[key] is not None: 141 | if f[key].shape != dsets[key]: 142 | # print('Dataset "{}" has wrong shape:'.format(key)) 143 | # print(' expected: {}'.format(dsets[key])) 144 | # print(' found: {}'.format(f[key].shape)) 145 | return False 146 | except IOError: 147 | # print('Problem reading file.') 148 | return False 149 | 150 | return True 151 | 152 | 153 | class FileTransferProgressBar(ProgressBar): 154 | def __init__(self, content_length): 155 | prefixes = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi') 156 | if content_length is None: 157 | size_txt = '?' 158 | content_length = UnknownLength 159 | else: 160 | scaled, power = scale_1024(content_length, len(prefixes)) 161 | size_txt = '{:.1f} {:s}B'.format(scaled, prefixes[power]) 162 | widgets = [ 163 | DataSize(), 164 | FormatCustomText(' of {:s} | '.format(size_txt)), 165 | AdaptiveTransferSpeed(samples=100), 166 | FormatCustomText(' '), 167 | Bar(), 168 | FormatCustomText(' '), 169 | Percentage(), 170 | FormatCustomText(' | '), 171 | AdaptiveETA(samples=100)] 172 | super(FileTransferProgressBar, self).__init__( 173 | max_value=content_length, 174 | widgets=widgets) 175 | 176 | 177 | def check_md5sum(fname, md5sum, chunk_size=1024): 178 | """ 179 | Checks that a file exists, and has the correct MD5 checksum. 180 | 181 | Args: 182 | fname (str): The filename of the file. 183 | md5sum (str): The expected MD5 sum. 184 | chunk_size (Optional[int]): Process in chunks of this size (in Bytes). 185 | Defaults to 1024. 186 | """ 187 | if os.path.isfile(fname): 188 | md5_existing = get_md5sum(fname, chunk_size=chunk_size) 189 | return (md5_existing == md5sum) 190 | return False 191 | 192 | 193 | def download_and_verify(url, md5sum, fname=None, 194 | chunk_size=1024, clobber=False, 195 | verbose=True): 196 | """ 197 | Downloads a file and verifies the MD5 sum. 198 | 199 | Args: 200 | url (str): The URL to download. 201 | md5sum (str): The expected MD5 sum. 202 | fname (Optional[str]): The filename to store the downloaded file in. 203 | If `None`, infer the filename from the URL. Defaults to `None`. 204 | chunk_size (Optional[int]): Process in chunks of this size (in Bytes). 205 | Defaults to 1024. 206 | clobber (Optional[bool]): If `True`, any existing, identical file will 207 | be overwritten. If `False`, the MD5 sum of any existing file with 208 | the destination filename will be checked. If the MD5 sum does not 209 | match, the existing file will be overwritten. Defaults to `False`. 210 | verbose (Optional[bool]): If `True` (the default), then a progress bar 211 | will be shownd during downloads. 212 | 213 | Returns: 214 | The filename the URL was downloaded to. 215 | 216 | Raises: 217 | DownloadError: The MD5 sum of the downloaded file does not match 218 | `md5sum`. 219 | requests.exceptions.HTTPError: There was a problem connecting to the 220 | URL. 221 | """ 222 | 223 | # Determine the filename 224 | if fname is None: 225 | fname = url.split('/')[-1] 226 | 227 | # Check if the file already exists on disk 228 | if (not clobber) and os.path.isfile(fname): 229 | print('Checking existing file to see if MD5 sum matches ...') 230 | md5_existing = get_md5sum(fname, chunk_size=chunk_size) 231 | if md5_existing == md5sum: 232 | print('File exists. Not overwriting.') 233 | return fname 234 | 235 | # Make sure the directory it's going into exists 236 | dir_name = os.path.dirname(fname) 237 | if not os.path.exists(dir_name): 238 | os.makedirs(dir_name) 239 | 240 | sig = hashlib.md5() 241 | 242 | if verbose: 243 | print('Downloading {} ...'.format(url)) 244 | 245 | if url.startswith('http://') or url.startswith('https://'): 246 | # Stream the URL as a file, copying to local disk 247 | with contextlib.closing(requests.get(url, stream=True)) as r: 248 | try: 249 | r.raise_for_status() 250 | except requests.exceptions.HTTPError as error: 251 | print('Error connecting to URL: "{}"'.format(url)) 252 | print(r.text) 253 | raise error 254 | 255 | with open(fname, 'wb') as f: 256 | content_length = r.headers.get('content-length') 257 | if content_length is not None: 258 | content_length = int(content_length) 259 | bar = FileTransferProgressBar(content_length) 260 | 261 | for k,chunk in enumerate(r.iter_content(chunk_size=chunk_size)): 262 | f.write(chunk) 263 | sig.update(chunk) 264 | 265 | if verbose: 266 | bar_val = chunk_size*(k+1) 267 | if content_length is not None: 268 | bar_val = min(bar_val, content_length) 269 | bar.update(bar_val) 270 | else: # e.g., ftp:// 271 | with contextlib.closing(urlopen(url)) as r: 272 | content_length = r.headers.get('content-length') 273 | if content_length is not None: 274 | content_length = int(content_length) 275 | bar = FileTransferProgressBar(content_length) 276 | 277 | with open(fname, 'wb') as f: 278 | k = 0 279 | while True: 280 | chunk = r.read(chunk_size) 281 | 282 | if not chunk: 283 | break 284 | 285 | f.write(chunk) 286 | sig.update(chunk) 287 | 288 | if verbose: 289 | k += 1 290 | bar_val = chunk_size*k 291 | if content_length is not None: 292 | bar_val = min(bar_val, content_length) 293 | bar.update(bar_val) 294 | 295 | 296 | if sig.hexdigest() != md5sum: 297 | raise DownloadError('The MD5 sum of the downloaded file is incorrect.\n' 298 | + ' download: {}\n'.format(sig.hexdigest()) 299 | + ' expected: {}\n'.format(md5sum)) 300 | 301 | return fname 302 | 303 | 304 | def download(url, fname=None): 305 | """ 306 | Downloads a file. 307 | 308 | Args: 309 | url (str): The URL to download. 310 | fname (Optional[str]): The filename to store the downloaded file in. If 311 | `None`, take the filename from the URL. Defaults to `None`. 312 | 313 | Returns: 314 | The filename the URL was downloaded to. 315 | 316 | Raises: 317 | requests.exceptions.HTTPError: There was a problem connecting to the 318 | URL. 319 | """ 320 | # Determine the filename 321 | if fname is None: 322 | fname = url.split('/')[-1] 323 | 324 | # Stream the URL as a file, copying to local disk 325 | with contextlib.closing(requests.get(url, stream=True)) as r: 326 | try: 327 | r.raise_for_status() 328 | except requests.exceptions.HTTPError as error: 329 | print('Error connecting to URL: "{}"'.format(url)) 330 | print(r.text) 331 | raise error 332 | 333 | with open(fname, 'wb') as f: 334 | shutil.copyfileobj(r.raw, f) 335 | 336 | return fname 337 | 338 | 339 | def dataverse_search_doi(doi): 340 | """ 341 | Fetches metadata pertaining to a Digital Object Identifier (DOI) in the 342 | Harvard Dataverse. 343 | 344 | Args: 345 | doi (str): The Digital Object Identifier (DOI) of the entry in the 346 | Dataverse. 347 | 348 | Raises: 349 | requests.exceptions.HTTPError: The given DOI does not exist, or there 350 | was a problem connecting to the Dataverse. 351 | """ 352 | 353 | url = '{}/api/datasets/:persistentId?persistentId=doi:{}'.format(dataverse, doi) 354 | r = requests.get(url) 355 | 356 | try: 357 | r.raise_for_status() 358 | except requests.exceptions.HTTPError as error: 359 | print('Error looking up DOI "{}" in the Harvard Dataverse.'.format(doi)) 360 | print(r.text) 361 | raise error 362 | 363 | return json.loads(r.text) 364 | 365 | 366 | def dataverse_download_id(file_id, md5sum, original, **kwargs): 367 | if original == True: 368 | url = '{}/api/access/datafile/{}?format=original'.format(dataverse, file_id) 369 | download_and_verify(url, md5sum, **kwargs) 370 | else: 371 | url = '{}/api/access/datafile/{}'.format(dataverse, file_id) 372 | download_and_verify(url, md5sum, **kwargs) 373 | 374 | 375 | 376 | def dataverse_download_doi(doi, 377 | local_fname=None, 378 | file_requirements={}, 379 | original=False, 380 | clobber=False): 381 | """ 382 | Downloads a file from the Dataverse, using a DOI and set of metadata 383 | parameters to locate the file. 384 | 385 | Args: 386 | doi (str): Digital Object Identifier (DOI) containing the file. 387 | local_fname (Optional[str]): Local filename to download the file to. If 388 | `None`, then use the filename provided by the Dataverse. Defaults to 389 | `None`. 390 | file_requirements (Optional[dict]): Select the file containing the 391 | given metadata entries. If multiple files meet these requirements, 392 | only the first in downloaded. Defaults to `{}`, corresponding to no 393 | requirements. 394 | original (Optional[bool]): Should the original version of the file be downloaded? 395 | Only applicable for tabular data. Defaults to `False`. 396 | 397 | Raises: 398 | DownloadError: Either no matching file was found under the given DOI, or 399 | the MD5 sum of the file was not as expected. 400 | requests.exceptions.HTTPError: The given DOI does not exist, or there 401 | was a problem connecting to the Dataverse. 402 | 403 | """ 404 | metadata = dataverse_search_doi(doi) 405 | 406 | def requirements_match(metadata): 407 | for key in file_requirements.keys(): 408 | if metadata['dataFile'].get(key, None) != file_requirements[key]: 409 | return False 410 | return True 411 | 412 | for file_metadata in metadata['data']['latestVersion']['files']: 413 | if requirements_match(file_metadata): 414 | file_id = file_metadata['dataFile']['id'] 415 | md5sum = file_metadata['dataFile']['md5'] 416 | 417 | if local_fname is None: 418 | local_fname = file_metadata['dataFile']['filename'] 419 | 420 | # Check if the file already exists on disk 421 | if (not clobber) and os.path.isfile(local_fname): 422 | print('Checking existing file to see if MD5 sum matches ...') 423 | md5_existing = get_md5sum(local_fname) 424 | if md5_existing == md5sum: 425 | print('File exists. Not overwriting.') 426 | return 427 | 428 | print("Downloading data to '{}' ...".format(local_fname)) 429 | 430 | dataverse_download_id(file_id, md5sum, original=original, 431 | fname=local_fname, clobber=False) 432 | 433 | return 434 | 435 | raise DownloadError( 436 | 'No file found under the given DOI matches the requirements.\n' 437 | 'The metadata found for this DOI was:\n' 438 | + json.dumps(file_metadata, indent=2, sort_keys=True)) 439 | 440 | 441 | def download_demo(): 442 | doi = '10.5072/FK2/ZSEMG9' 443 | requirements = {'filename': 'ResizablePng.png'} 444 | dataverse_download_doi(doi, file_requirements=requirements) 445 | -------------------------------------------------------------------------------- /selectionfunctions/healpix_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # healpix_map.py 4 | # A set of HEALPix map classes. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | import six 25 | 26 | import healpy as hp 27 | import astropy.io.fits as fits 28 | 29 | from .map import SelectionFunction, coord2healpix 30 | 31 | 32 | class HEALPixQuery(SelectionFunction): 33 | """ 34 | A class for querying HEALPix maps. 35 | """ 36 | 37 | def __init__(self, pix_val, nest, coord_frame): 38 | """ 39 | Args: 40 | pix_val (array): Value of the map in every pixel. The length of the 41 | array must be of the form `12 * nside**2`, where `nside` is a 42 | power of two. 43 | nest (bool): `True` if the map uses nested ordering. `False` if 44 | ring ordering is used. 45 | coord_frame (str): The coordinate system that the HEALPix map is in. 46 | Should be one of the frames supported by `astropy.coordinates`. 47 | """ 48 | self._nside = hp.pixelfunc.npix2nside(len(pix_val)) 49 | self._pix_val = pix_val 50 | self._nest = nest 51 | self._frame = coord_frame 52 | super(HEALPixQuery, self).__init__() 53 | 54 | def query(self, coords): 55 | """ 56 | Args: 57 | coords (`astropy.coordinates.SkyCoord`): The coordinates to query. 58 | 59 | Returns: 60 | A float array of the value of the map at the given coordinates. The 61 | shape of the output is the same as the shape of the coordinates 62 | stored by `coords`. 63 | """ 64 | pix_idx = coord2healpix(coords, self._frame, 65 | self._nside, nest=self._nest) 66 | return self._pix_val[pix_idx] 67 | 68 | 69 | class HEALPixFITSQuery(HEALPixQuery): 70 | """ 71 | A HEALPix map class that is initialized from a FITS file. 72 | """ 73 | 74 | def __init__(self, fname, coord_frame, hdu=0, field=None, dtype='f8'): 75 | """ 76 | Args: 77 | fname (str, HDUList, TableHDU or BinTableHDU): The filename, HDUList 78 | or HDU from which the map should be loaded. 79 | coord_frame (str): The coordinate system in which the HEALPix map is 80 | defined. Must be a coordinate frame which ``astropy`` 81 | understands. 82 | hdu (Optional[int or str]): Specifies which HDU to load the map 83 | from. Defaults to ``0``. 84 | field (Optional[int or str]): Specifies which field (column) to load 85 | the map from. Defaults to ``None``, meaning that ``hdu.data[:]`` 86 | is used. 87 | dtype (Optional[str or type]): The data will be coerced to this 88 | datatype. Can be any type specification that numpy understands. 89 | Defaults to ``'f8'``, for IEEE754 double precision. 90 | """ 91 | close_file = False 92 | 93 | if isinstance(fname, six.string_types): 94 | close_file = True 95 | hdulist = fits.open(fname) 96 | print(hdulist.info()) 97 | hdu = hdulist[hdu] 98 | elif isinstance(fname, fits.HDUList): 99 | hdu = fname[hdu] 100 | elif (isinstance(fname, fits.TableHDU) 101 | or isinstance(fname, fits.BinTableHDU)): 102 | hdu = fname 103 | else: 104 | raise TypeError('`fname` must be a `str`, `HDUList`, `TableHDU` or ' 105 | '`BinTableHDU`.') 106 | 107 | if field is None: 108 | pix_val = hdu.data[:].ravel().astype(dtype) 109 | else: 110 | pix_val = hdu.data[field][:].ravel().astype(dtype) 111 | nest = hdu.header.get('ORDERING', 'NESTED').strip() == 'NESTED' 112 | 113 | if close_file: 114 | hdulist.close() 115 | 116 | super(HEALPixFITSQuery, self).__init__(pix_val, nest, coord_frame) 117 | -------------------------------------------------------------------------------- /selectionfunctions/json_serializers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # json_serializers.py 4 | # Contains JSON (de)serializers for: 5 | # astropy.units.Quantity 6 | # astropy.coordinates.SkyCoord 7 | # numpy.ndarray 8 | # numpy.dtype 9 | # numpy.floating 10 | # numpy.integer 11 | # 12 | # Copyright (C) 2020 Douglas Boubert 13 | # 14 | # This program is free software; you can redistribute it and/or modify 15 | # it under the terms of the GNU General Public License as published by 16 | # the Free Software Foundation; either version 2 of the License, or 17 | # (at your option) any later version. 18 | # 19 | # This program is distributed in the hope that it will be useful, 20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | # GNU General Public License for more details. 23 | # 24 | # You should have received a copy of the GNU General Public License along 25 | # with this program; if not, write to the Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 27 | # 28 | 29 | 30 | from __future__ import print_function 31 | import six 32 | 33 | import json 34 | import base64 35 | import io 36 | import numpy as np 37 | import astropy.units as units 38 | import astropy.coordinates as coords 39 | 40 | 41 | def deserialize_tuple(d): 42 | """ 43 | Deserializes a JSONified tuple. 44 | 45 | Args: 46 | d (:obj:`dict`): A dictionary representation of the tuple. 47 | 48 | Returns: 49 | A tuple. 50 | """ 51 | return tuple(d['items']) 52 | 53 | 54 | def serialize_dtype(o): 55 | """ 56 | Serializes a :obj:`numpy.dtype`. 57 | 58 | Args: 59 | o (:obj:`numpy.dtype`): :obj:`dtype` to be serialized. 60 | 61 | Returns: 62 | A dictionary that can be passed to :obj:`json.dumps`. 63 | """ 64 | if len(o) == 0: 65 | return dict( 66 | _type='np.dtype', 67 | descr=str(o)) 68 | return dict( 69 | _type='np.dtype', 70 | descr=o.descr) 71 | # res = [] 72 | # for k in range(len(o)): 73 | # res.append((o.names[k], str(o[k]))) 74 | # return dict( 75 | # _type='np.dtype', 76 | # desc=res) 77 | 78 | 79 | def deserialize_dtype(d): 80 | """ 81 | Deserializes a JSONified :obj:`numpy.dtype`. 82 | 83 | Args: 84 | d (:obj:`dict`): A dictionary representation of a :obj:`dtype` object. 85 | 86 | Returns: 87 | A :obj:`dtype` object. 88 | """ 89 | if isinstance(d['descr'], six.string_types): 90 | return np.dtype(d['descr']) 91 | descr = [] 92 | for col in d['descr']: 93 | col_descr = [] 94 | for c in col: 95 | if isinstance(c, six.string_types): 96 | col_descr.append(str(c)) 97 | elif type(c) is list: 98 | col_descr.append(tuple(c)) 99 | else: 100 | col_descr.append(c) 101 | descr.append(tuple(col_descr)) 102 | return np.dtype(descr) 103 | 104 | 105 | def serialize_ndarray_b64(o): 106 | """ 107 | Serializes a :obj:`numpy.ndarray` in a format where the datatype and shape are 108 | human-readable, but the array data itself is binary64 encoded. 109 | 110 | Args: 111 | o (:obj:`numpy.ndarray`): :obj:`ndarray` to be serialized. 112 | 113 | Returns: 114 | A dictionary that can be passed to :obj:`json.dumps`. 115 | """ 116 | if o.flags['C_CONTIGUOUS']: 117 | o_data = o.data 118 | else: 119 | o_data = np.ascontiguousarray(o).data 120 | data_b64 = base64.b64encode(o_data) 121 | return dict( 122 | _type='np.ndarray', 123 | data=data_b64.decode('utf-8'), 124 | dtype=o.dtype, 125 | shape=o.shape) 126 | 127 | 128 | def hint_tuples(o): 129 | """ 130 | Annotates tuples before JSON serialization, so that they can be 131 | reconstructed during deserialization. Each tuple is converted into a 132 | dictionary of the form: 133 | 134 | {'_type': 'tuple', 'items': (...)} 135 | 136 | This function acts recursively on lists, so that tuples nested inside a list 137 | (or doubly nested, triply nested, etc.) will also be annotated. 138 | """ 139 | if isinstance(o, tuple): 140 | return dict(_type='tuple', items=o) 141 | elif isinstance(o, list): 142 | return [hint_tuples(el) for el in o] 143 | else: 144 | return o 145 | 146 | 147 | def serialize_ndarray_readable(o): 148 | """ 149 | Serializes a :obj:`numpy.ndarray` in a human-readable format. 150 | 151 | Args: 152 | o (:obj:`numpy.ndarray`): :obj:`ndarray` to be serialized. 153 | 154 | Returns: 155 | A dictionary that can be passed to :obj:`json.dumps`. 156 | """ 157 | return dict( 158 | _type='np.ndarray', 159 | dtype=o.dtype, 160 | value=hint_tuples(o.tolist())) 161 | 162 | 163 | def serialize_ndarray_npy(o): 164 | """ 165 | Serializes a :obj:`numpy.ndarray` using numpy's built-in :obj:`save` function. 166 | This produces totally unreadable (and very un-JSON-like) results (in "npy" 167 | format), but it's basically guaranteed to work in 100% of cases. 168 | 169 | Args: 170 | o (:obj:`numpy.ndarray`): :obj:`ndarray` to be serialized. 171 | 172 | Returns: 173 | A dictionary that can be passed to :obj:`json.dumps`. 174 | """ 175 | with io.BytesIO() as f: 176 | np.save(f, o) 177 | f.seek(0) 178 | serialized = json.dumps(f.read().decode('latin-1')) 179 | return dict( 180 | _type='np.ndarray', 181 | npy=serialized) 182 | 183 | 184 | def deserialize_ndarray_npy(d): 185 | """ 186 | Deserializes a JSONified :obj:`numpy.ndarray` that was created using numpy's 187 | :obj:`save` function. 188 | 189 | Args: 190 | d (:obj:`dict`): A dictionary representation of an :obj:`ndarray` object, created 191 | using :obj:`numpy.save`. 192 | 193 | Returns: 194 | An :obj:`ndarray` object. 195 | """ 196 | with io.BytesIO() as f: 197 | f.write(json.loads(d['npy']).encode('latin-1')) 198 | f.seek(0) 199 | return np.load(f) 200 | 201 | 202 | def deserialize_ndarray(d): 203 | """ 204 | Deserializes a JSONified :obj:`numpy.ndarray`. Can handle arrays serialized 205 | using any of the methods in this module: :obj:`"npy"`, :obj:`"b64"`, 206 | :obj:`"readable"`. 207 | 208 | Args: 209 | d (`dict`): A dictionary representation of an :obj:`ndarray` object. 210 | 211 | Returns: 212 | An :obj:`ndarray` object. 213 | """ 214 | if 'data' in d: 215 | x = np.fromstring( 216 | base64.b64decode(d['data']), 217 | dtype=d['dtype']) 218 | x.shape = d['shape'] 219 | return x 220 | elif 'value' in d: 221 | return np.array(d['value'], dtype=d['dtype']) 222 | elif 'npy' in d: 223 | return deserialize_ndarray_npy(d) 224 | else: 225 | raise ValueError('Malformed np.ndarray encoding.') 226 | 227 | 228 | def serialize_quantity(o): 229 | """ 230 | Serializes an :obj:`astropy.units.Quantity`, for JSONification. 231 | 232 | Args: 233 | o (:obj:`astropy.units.Quantity`): :obj:`Quantity` to be serialized. 234 | 235 | Returns: 236 | A dictionary that can be passed to :obj:`json.dumps`. 237 | """ 238 | return dict( 239 | _type='astropy.units.Quantity', 240 | value=o.value, 241 | unit=o.unit.to_string()) 242 | 243 | 244 | def deserialize_quantity(d): 245 | """ 246 | Deserializes a JSONified :obj:`astropy.units.Quantity`. 247 | 248 | Args: 249 | d (:obj:`dict`): A dictionary representation of a :obj:`Quantity` object. 250 | 251 | Returns: 252 | A :obj:`Quantity` object. 253 | """ 254 | return units.Quantity( 255 | d['value'], 256 | unit=d['unit']) 257 | 258 | 259 | def serialize_skycoord(o): 260 | """ 261 | Serializes an :obj:`astropy.coordinates.SkyCoord`, for JSONification. 262 | 263 | Args: 264 | o (:obj:`astropy.coordinates.SkyCoord`): :obj:`SkyCoord` to be serialized. 265 | 266 | Returns: 267 | A dictionary that can be passed to :obj:`json.dumps`. 268 | """ 269 | representation = o.representation.get_name() 270 | frame = o.frame.name 271 | 272 | r = o.represent_as('spherical') 273 | 274 | d = dict( 275 | _type='astropy.coordinates.SkyCoord', 276 | frame=frame, 277 | representation=representation, 278 | lon=r.lon, 279 | lat=r.lat) 280 | 281 | if len(o.distance.unit.to_string()): 282 | d['distance'] = r.distance 283 | 284 | return d 285 | 286 | 287 | def deserialize_skycoord(d): 288 | """ 289 | Deserializes a JSONified :obj:`astropy.coordinates.SkyCoord`. 290 | 291 | Args: 292 | d (:obj:`dict`): A dictionary representation of a :obj:`SkyCoord` object. 293 | 294 | Returns: 295 | A :obj:`SkyCoord` object. 296 | """ 297 | if 'distance' in d: 298 | args = (d['lon'], d['lat'], d['distance']) 299 | else: 300 | args = (d['lon'], d['lat']) 301 | 302 | return coords.SkyCoord( 303 | *args, 304 | frame=d['frame'], 305 | representation='spherical') 306 | 307 | 308 | def get_encoder(ndarray_mode='b64'): 309 | """ 310 | Returns a JSON encoder that can handle: 311 | * :obj:`numpy.ndarray` 312 | * :obj:`numpy.floating` (converted to :obj:`float`) 313 | * :obj:`numpy.integer` (converted to :obj:`int`) 314 | * :obj:`numpy.dtype` 315 | * :obj:`astropy.units.Quantity` 316 | * :obj:`astropy.coordinates.SkyCoord` 317 | 318 | Args: 319 | ndarray_mode (Optional[:obj:`str`]): Which method to use to serialize 320 | :obj:`numpy.ndarray` objects. Defaults to :obj:`'b64'`, which converts the 321 | array data to binary64 encoding (non-human-readable), and stores the 322 | datatype/shape in human-readable formats. Other options are 323 | :obj:`'readable'`, which produces fully human-readable output, and 324 | :obj:`'npy'`, which uses numpy's built-in :obj:`save` function and 325 | produces completely unreadable output. Of all the methods :obj:`'npy'` 326 | is the most reliable, but also least human-readable. :obj:`'readable'` 327 | produces the most human-readable output, but is the least reliable 328 | and loses precision. 329 | 330 | Returns: 331 | A subclass of :obj:`json.JSONEncoder`. 332 | """ 333 | 334 | # Use specified numpy.ndarray serialization mode 335 | serialize_fns = { 336 | 'b64': serialize_ndarray_b64, 337 | 'readable': serialize_ndarray_readable, 338 | 'npy': serialize_ndarray_npy} 339 | 340 | if ndarray_mode not in serialize_fns: 341 | raise ValueError('"ndarray_mode" must be one of {}'.format( 342 | serialize_fns.keys)) 343 | 344 | serialize_ndarray = serialize_fns[ndarray_mode] 345 | 346 | class MultiJSONEncoder(json.JSONEncoder): 347 | """ 348 | A JSON encoder that can handle: 349 | * :obj:`numpy.ndarray` 350 | * :obj:`numpy.floating` (converted to :obj:`float`) 351 | * :obj:`numpy.integer` (converted to :obj:`int`) 352 | * :obj:`numpy.dtype` 353 | * :obj:`astropy.units.Quantity` 354 | * :obj:`astropy.coordinates.SkyCoord` 355 | """ 356 | def default(self, o): 357 | if isinstance(o, coords.SkyCoord): 358 | return serialize_skycoord(o) 359 | if isinstance(o, units.Quantity): 360 | return serialize_quantity(o) 361 | elif isinstance(o, np.ndarray): 362 | return serialize_ndarray(o) 363 | elif isinstance(o, np.dtype): 364 | return serialize_dtype(o) 365 | elif isinstance(o, np.floating): 366 | return float(o) 367 | elif isinstance(o, np.integer): 368 | return int(o) 369 | elif isinstance(o, np.bool_): 370 | return bool(o) 371 | elif isinstance(o, np.void): 372 | try: 373 | o = np.array(o) 374 | except: 375 | pass 376 | else: 377 | return o 378 | return json.JSONEncoder.default(self, o) 379 | 380 | return MultiJSONEncoder 381 | 382 | 383 | class MultiJSONDecoder(json.JSONDecoder): 384 | """ 385 | A JSON decoder that can handle: 386 | * :obj:`numpy.ndarray` 387 | * :obj:`numpy.dtype` 388 | * :obj:`astropy.units.Quantity` 389 | * :obj:`astropy.coordinates.SkyCoord` 390 | """ 391 | def __init__(self, *args, **kwargs): 392 | json.JSONDecoder.__init__( 393 | self, 394 | object_hook=self.object_hook, 395 | *args, 396 | **kwargs) 397 | 398 | def object_hook(self, d): 399 | if isinstance(d, dict): 400 | if ('_type' in d): 401 | if d['_type'] == 'astropy.coordinates.SkyCoord': 402 | return deserialize_skycoord(d) 403 | elif d['_type'] == 'astropy.units.Quantity': 404 | return deserialize_quantity(d) 405 | elif d['_type'] == 'np.ndarray': 406 | return deserialize_ndarray(d) 407 | elif d['_type'] == 'np.dtype': 408 | return deserialize_dtype(d) 409 | elif d['_type'] == 'tuple': 410 | return deserialize_tuple(d) 411 | return d 412 | -------------------------------------------------------------------------------- /selectionfunctions/map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # map.py 4 | # A generic interface to a 3D selection function. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import numpy as np 26 | import healpy as hp 27 | import astropy.coordinates as coordinates 28 | import astropy.units as units 29 | 30 | from functools import wraps 31 | import inspect 32 | import requests 33 | import json 34 | import copy 35 | 36 | from . import json_serializers 37 | from . import sfexceptions 38 | from .source import Source 39 | 40 | # import time 41 | 42 | 43 | def coord2healpix(coords, frame, nside, nest=True): 44 | """ 45 | Calculate HEALPix indices from an astropy SkyCoord. Assume the HEALPix 46 | system is defined on the coordinate frame ``frame``. 47 | 48 | Args: 49 | coords (:obj:`astropy.coordinates.SkyCoord`): The input coordinates. 50 | frame (:obj:`str`): The frame in which the HEALPix system is defined. 51 | nside (:obj:`int`): The HEALPix nside parameter to use. Must be a power of 2. 52 | nest (Optional[:obj:`bool`]): ``True`` (the default) if nested HEALPix ordering 53 | is desired. ``False`` for ring ordering. 54 | 55 | Returns: 56 | An array of pixel indices (integers), with the same shape as the input 57 | SkyCoord coordinates (:obj:`coords.shape`). 58 | 59 | Raises: 60 | :obj:`sfexceptions.CoordFrameError`: If the specified frame is not supported. 61 | """ 62 | if coords.frame.name != frame: 63 | c = coords.transform_to(frame) 64 | else: 65 | c = coords 66 | 67 | if hasattr(c, 'ra'): 68 | phi = c.ra.rad 69 | theta = 0.5*np.pi - c.dec.rad 70 | return hp.pixelfunc.ang2pix(nside, theta, phi, nest=nest) 71 | elif hasattr(c, 'l'): 72 | phi = c.l.rad 73 | theta = 0.5*np.pi - c.b.rad 74 | return hp.pixelfunc.ang2pix(nside, theta, phi, nest=nest) 75 | elif hasattr(c, 'x'): 76 | return hp.pixelfunc.vec2pix(nside, c.x.kpc, c.y.kpc, c.z.kpc, nest=nest) 77 | elif hasattr(c, 'w'): 78 | return hp.pixelfunc.vec2pix(nside, c.w.kpc, c.u.kpc, c.v.kpc, nest=nest) 79 | else: 80 | raise sfexceptions.CoordFrameError( 81 | 'No method to transform from coordinate frame "{}" to HEALPix.'.format( 82 | frame)) 83 | 84 | 85 | def ensure_coord_type(f): 86 | """ 87 | A decorator for class methods of the form 88 | 89 | .. code-block:: python 90 | 91 | Class.method(self, coords, **kwargs) 92 | 93 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 94 | 95 | The decorator raises a :obj:`TypeError` if the ``coords`` that gets passed to 96 | ``Class.method`` is not an :obj:`astropy.coordinates.SkyCoord` instance. 97 | 98 | Args: 99 | f (class method): A function with the signature 100 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 101 | object containing an array. 102 | 103 | Returns: 104 | A function that raises a :obj:`TypeError` if ``coords`` is not an 105 | :obj:`astropy.coordinates.SkyCoord` object, but which otherwise behaves 106 | the same as the decorated function. 107 | """ 108 | @wraps(f) 109 | def _wrapper_func(self, sources, **kwargs): 110 | if not isinstance(sources, Source): 111 | raise TypeError('`sources` must be a selectionfunctions.Source object.') 112 | return f(self, sources, **kwargs) 113 | return _wrapper_func 114 | 115 | 116 | def reshape_coords(coords, shape): 117 | pos_attr = ['l', 'b', 'ra', 'dec', 'x', 'y', 'z', 'w', 'u', 'v', 'distance'] 118 | pos_kwargs = {} 119 | 120 | for attr in pos_attr: 121 | if hasattr(coords, pos_attr): 122 | pos_kwargs[attr] = np.reshape() 123 | # TODO: finish reshape 124 | 125 | 126 | def coords_to_shape(gal, shape): 127 | l = np.reshape(gal.l.deg, shape) * units.deg 128 | b = np.reshape(gal.b.deg, shape) * units.deg 129 | 130 | has_dist = hasattr(gal.distance, 'kpc') 131 | d = np.reshape(gal.distance.kpc, shape) * units.kpc if has_dist else None 132 | 133 | return coordinates.SkyCoord(l, b, distance=d, frame='galactic') 134 | 135 | 136 | def ensure_flat_frame(f, frame=None): 137 | def _wrapper_func(self, coords, **kwargs): 138 | if (frame is not None) and (coords.frame.name != frame): 139 | coords_transf = coords.transform_to(frame) 140 | else: 141 | coords_transf = coords 142 | 143 | is_array = not coords.isscalar 144 | if is_array: 145 | orig_shape = coords.shape 146 | shape_flat = (np.prod(orig_shape),) 147 | coords_transf = coords_to_shape(coords_transf, shape_flat) 148 | else: 149 | coords_transf = coords_to_shape(coords_transf, (1,)) 150 | 151 | out = f(self, coords_transf, **kwargs) 152 | 153 | if is_array: 154 | out.shape = orig_shape + out.shape[1:] 155 | else: 156 | out = out[0] 157 | 158 | return out 159 | 160 | return _wrapper_func 161 | 162 | 163 | def equ_to_shape(equ, shape): 164 | ra = np.reshape(equ.coord.ra.deg, shape)*units.deg 165 | dec = np.reshape(equ.coord.dec.deg, shape)*units.deg 166 | 167 | has_dist = hasattr(equ.coord.distance, 'kpc') 168 | d = np.reshape(equ.coord.distance.kpc, shape)*units.kpc if has_dist else None 169 | 170 | has_photometry = equ.photometry is not None 171 | if has_photometry: 172 | photometry = {k:np.reshape(v, shape) for k,v in equ.photometry.measurement.items()} 173 | 174 | has_photometry_error = equ.photometry.error is not None 175 | if has_photometry_error: 176 | photometry_error = {k:np.reshape(v, shape) for k,v in equ.photometry.error.items()} 177 | else: 178 | photometry_error = None 179 | else: 180 | photometry = None 181 | photometry_error = None 182 | 183 | return Source(ra, dec, distance=d, photometry=photometry, photometry_error=photometry_error, frame='icrs') 184 | 185 | def ensure_flat_icrs(f): 186 | """ 187 | A decorator for class methods of the form 188 | 189 | .. code-block:: python 190 | 191 | Class.method(self, coords, **kwargs) 192 | 193 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 194 | 195 | The decorator ensures that the ``coords`` that gets passed to 196 | ``Class.method`` is a flat array of Equatorial coordinates. It also reshapes 197 | the output of ``Class.method`` to have the same shape (possibly scalar) as 198 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 199 | (instead of an array), each element in the output is reshaped instead. 200 | 201 | Args: 202 | f (class method): A function with the signature 203 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 204 | object containing an array. 205 | 206 | Returns: 207 | A function that takes :obj:`SkyCoord` input with any shape (including 208 | scalar). 209 | """ 210 | 211 | @wraps(f) 212 | def _wrapper_func(self, sources, **kwargs): 213 | # t0 = time.time() 214 | 215 | equ = copy.copy(sources) 216 | if equ.coord.frame.name != 'icrs': 217 | equ.coord = equ.coord.transform_to('icrs') 218 | 219 | # t1 = time.time() 220 | 221 | is_array = not equ.coord.isscalar 222 | if is_array: 223 | orig_shape = sources.coord.shape 224 | shape_flat = (np.prod(orig_shape),) 225 | # print 'Original shape: {}'.format(orig_shape) 226 | # print 'Flattened shape: {}'.format(shape_flat) 227 | equ = equ_to_shape(equ, shape_flat) 228 | else: 229 | equ = equ_to_shape(equ, (1,)) 230 | 231 | # t2 = time.time() 232 | 233 | out = f(self, equ, **kwargs) 234 | 235 | # t3 = time.time() 236 | 237 | if is_array: 238 | if isinstance(out, list) or isinstance(out, tuple): 239 | # Apply to each array in output list 240 | for o in out: 241 | o.shape = orig_shape + o.shape[1:] 242 | else: # Only one array in output 243 | out.shape = orig_shape + out.shape[1:] 244 | else: 245 | if isinstance(out, list) or isinstance(out, tuple): 246 | out = list(out) 247 | 248 | # Apply to each array in output list 249 | for k,o in enumerate(out): 250 | out[k] = o[0] 251 | else: # Only one array in output 252 | out = out[0] 253 | 254 | # t4 = time.time() 255 | 256 | # print('') 257 | # print('time inside ensure_flat_galactic: {:.4f} s'.format(t4-t0)) 258 | # print('{: >7.4f} s : {: >6.4f} s : transform_to("galactic")'.format(t1-t0, t1-t0)) 259 | # print('{: >7.4f} s : {: >6.4f} s : reshape coordinates'.format(t2-t0, t2-t1)) 260 | # print('{: >7.4f} s : {: >6.4f} s : execute query'.format(t3-t0, t3-t2)) 261 | # print('{: >7.4f} s : {: >6.4f} s : reshape output'.format(t4-t0, t4-t3)) 262 | # print('') 263 | 264 | return out 265 | 266 | return _wrapper_func 267 | 268 | def gal_to_shape(gal, shape): 269 | l = np.reshape(gal.coord.l.deg, shape)*units.deg 270 | b = np.reshape(gal.coord.b.deg, shape)*units.deg 271 | 272 | has_dist = hasattr(gal.coord.distance, 'kpc') 273 | d = np.reshape(gal.coord.distance.kpc, shape)*units.kpc if has_dist else None 274 | 275 | has_photometry = gal.photometry is not None 276 | if has_photometry: 277 | photometry = {k:np.reshape(v, shape) for k,v in gal.photometry.measurement.items()} 278 | 279 | has_photometry_error = gal.photometry.error is not None 280 | if has_photometry_error: 281 | photometry_error = {k:np.reshape(v, shape) for k,v in gal.photometry.error.items()} 282 | else: 283 | photometry_error = None 284 | else: 285 | photometry = None 286 | photometry_error = None 287 | 288 | return Source(l, b, distance=d, photometry=photometry, photometry_error=photometry_error, frame='galactic') 289 | 290 | def ensure_flat_galactic(f): 291 | """ 292 | A decorator for class methods of the form 293 | 294 | .. code-block:: python 295 | 296 | Class.method(self, coords, **kwargs) 297 | 298 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 299 | 300 | The decorator ensures that the ``coords`` that gets passed to 301 | ``Class.method`` is a flat array of Galactic coordinates. It also reshapes 302 | the output of ``Class.method`` to have the same shape (possibly scalar) as 303 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 304 | (instead of an array), each element in the output is reshaped instead. 305 | 306 | Args: 307 | f (class method): A function with the signature 308 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 309 | object containing an array. 310 | 311 | Returns: 312 | A function that takes :obj:`SkyCoord` input with any shape (including 313 | scalar). 314 | """ 315 | 316 | @wraps(f) 317 | def _wrapper_func(self, sources, **kwargs): 318 | # t0 = time.time() 319 | 320 | gal = copy.copy(sources) 321 | if gal.coord.frame.name != 'galactic': 322 | gal.coord = gal.coord.transform_to('galactic') 323 | 324 | # t1 = time.time() 325 | 326 | is_array = not gal.coord.isscalar 327 | if is_array: 328 | orig_shape = sources.coord.shape 329 | shape_flat = (np.prod(orig_shape),) 330 | # print 'Original shape: {}'.format(orig_shape) 331 | # print 'Flattened shape: {}'.format(shape_flat) 332 | gal = gal_to_shape(gal, shape_flat) 333 | else: 334 | gal = gal_to_shape(gal, (1,)) 335 | 336 | # t2 = time.time() 337 | 338 | out = f(self, gal, **kwargs) 339 | 340 | # t3 = time.time() 341 | 342 | if is_array: 343 | if isinstance(out, list) or isinstance(out, tuple): 344 | # Apply to each array in output list 345 | for o in out: 346 | o.shape = orig_shape + o.shape[1:] 347 | else: # Only one array in output 348 | out.shape = orig_shape + out.shape[1:] 349 | else: 350 | if isinstance(out, list) or isinstance(out, tuple): 351 | out = list(out) 352 | 353 | # Apply to each array in output list 354 | for k,o in enumerate(out): 355 | out[k] = o[0] 356 | else: # Only one array in output 357 | out = out[0] 358 | 359 | # t4 = time.time() 360 | 361 | # print('') 362 | # print('time inside ensure_flat_galactic: {:.4f} s'.format(t4-t0)) 363 | # print('{: >7.4f} s : {: >6.4f} s : transform_to("galactic")'.format(t1-t0, t1-t0)) 364 | # print('{: >7.4f} s : {: >6.4f} s : reshape coordinates'.format(t2-t0, t2-t1)) 365 | # print('{: >7.4f} s : {: >6.4f} s : execute query'.format(t3-t0, t3-t2)) 366 | # print('{: >7.4f} s : {: >6.4f} s : reshape output'.format(t4-t0, t4-t3)) 367 | # print('') 368 | 369 | return out 370 | 371 | return _wrapper_func 372 | 373 | 374 | def ensure_flat_coords(f): 375 | """ 376 | A decorator for class methods of the form 377 | 378 | .. code-block:: python 379 | 380 | Class.method(self, coords, **kwargs) 381 | 382 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 383 | 384 | The decorator ensures that the ``coords`` that gets passed to 385 | ``Class.method`` is a flat array. It also reshapes 386 | the output of ``Class.method`` to have the same shape (possibly scalar) as 387 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 388 | (instead of an array), each element in the output is reshaped instead. 389 | 390 | Args: 391 | f (class method): A function with the signature 392 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 393 | object containing an array. 394 | 395 | Returns: 396 | A function that takes :obj:`SkyCoord` input with any shape (including 397 | scalar). 398 | """ 399 | 400 | @wraps(f) 401 | def _wrapper_func(self, coords, **kwargs): 402 | is_array = not coords.isscalar 403 | 404 | if is_array: 405 | orig_shape = coords.shape 406 | shape_flat = (np.prod(orig_shape),) 407 | coords = coords.reshape(shape_flat) 408 | else: 409 | coords = coords.reshape((1,)) 410 | 411 | # t2 = time.time() 412 | 413 | out = f(self, coords, **kwargs) 414 | 415 | # t3 = time.time() 416 | 417 | if is_array: 418 | if isinstance(out, list) or isinstance(out, tuple): 419 | # Apply to each array in output list 420 | for o in out: 421 | o.shape = orig_shape + o.shape[1:] 422 | else: # Only one array in output 423 | out.shape = orig_shape + out.shape[1:] 424 | else: 425 | if isinstance(out, list) or isinstance(out, tuple): 426 | out = list(out) 427 | 428 | # Apply to each array in output list 429 | for k,o in enumerate(out): 430 | out[k] = o[0] 431 | else: # Only one array in output 432 | out = out[0] 433 | 434 | # t4 = time.time() 435 | 436 | # print('') 437 | # print('time inside ensure_flat_galactic: {:.4f} s'.format(t4-t0)) 438 | # print('{: >7.4f} s : {: >6.4f} s : transform_to("galactic")'.format(t1-t0, t1-t0)) 439 | # print('{: >7.4f} s : {: >6.4f} s : reshape coordinates'.format(t2-t0, t2-t1)) 440 | # print('{: >7.4f} s : {: >6.4f} s : execute query'.format(t3-t0, t3-t2)) 441 | # print('{: >7.4f} s : {: >6.4f} s : reshape output'.format(t4-t0, t4-t3)) 442 | # print('') 443 | 444 | return out 445 | 446 | return _wrapper_func 447 | 448 | 449 | def web_api_method(url, 450 | encoder=json_serializers.get_encoder(), 451 | decoder=json_serializers.MultiJSONDecoder): 452 | def decorator(f): 453 | @wraps(f) 454 | def api_wrapper(self, *args, **kwargs): 455 | # Collect the arguments 456 | data = inspect.getcallargs(f, self, *args, **kwargs) 457 | data.pop('self') 458 | kw = data.pop('kwargs', {}) 459 | data.update(**kw) 460 | 461 | # Serialize the arguments 462 | data = json.dumps(data, cls=encoder) 463 | 464 | # POST request to server 465 | headers = {'content-type': 'application/json'} 466 | r = requests.post( 467 | self.base_url.rstrip('/') + '/' + url.lstrip('/'), 468 | data=data, 469 | headers=headers) 470 | try: 471 | r.raise_for_status() 472 | except requests.exceptions.HTTPError as err: 473 | print('Response received from server:') 474 | print(r.text) 475 | raise err 476 | 477 | # Deserialize the response 478 | return json.loads(r.text, cls=decoder) 479 | return api_wrapper 480 | return decorator 481 | 482 | 483 | 484 | class SelectionFunction(object): 485 | """ 486 | Base class for querying selectionfunctions. For each individual selection function, a different 487 | subclass should be written, implementing the :obj:`query()` function. 488 | """ 489 | 490 | def __init__(self): 491 | pass 492 | 493 | @ensure_coord_type 494 | def __call__(self, coords, **kwargs): 495 | """ 496 | An alias for :obj:`SelectionFunction.query`. 497 | """ 498 | return self.query(coords, **kwargs) 499 | 500 | def query(self, coords, **kwargs): 501 | """ 502 | Query the selection function at a set of coordinates. 503 | 504 | Args: 505 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates at which to 506 | query the selection function. 507 | 508 | Raises: 509 | :obj:`NotImplementedError`: This function must be defined by derived 510 | classes. 511 | """ 512 | raise NotImplementedError( 513 | '`SelectionFunction.query` must be implemented by subclasses.\n' 514 | 'The `SelectionFunction` base class should not itself be used.') 515 | 516 | def query_gal(self, l, b, d=None, **kwargs): 517 | """ 518 | Query using Galactic coordinates. 519 | 520 | Args: 521 | l (:obj:`float`, scalar or array-like): Galactic longitude, in degrees, 522 | or as an :obj:`astropy.unit.Quantity`. 523 | b (:obj:`float`, scalar or array-like): Galactic latitude, in degrees, 524 | or as an :obj:`astropy.unit.Quantity`. 525 | d (Optional[:obj:`float`, scalar or array-like]): Distance from the Solar 526 | System, in kpc, or as an :obj:`astropy.unit.Quantity`. Defaults to 527 | ``None``, meaning no distance is specified. 528 | **kwargs: Any additional keyword arguments accepted by derived 529 | classes. 530 | 531 | Returns: 532 | The results of the query, which must be implemented by derived 533 | classes. 534 | """ 535 | 536 | if not isinstance(l, units.Quantity): 537 | l = l * units.deg 538 | if not isinstance(b, units.Quantity): 539 | b = b * units.deg 540 | 541 | if d is None: 542 | coords = coordinates.SkyCoord(l, b, frame='galactic') 543 | else: 544 | if not isinstance(d, units.Quantity): 545 | d = d * units.kpc 546 | coords = coordinates.SkyCoord( 547 | l, b, 548 | distance=d, 549 | frame='galactic') 550 | 551 | return self.query(coords, **kwargs) 552 | 553 | def query_equ(self, ra, dec, d=None, frame='icrs', **kwargs): 554 | """ 555 | Query using Equatorial coordinates. By default, the ICRS frame is used, 556 | although other frames implemented by :obj:`astropy.coordinates` may also be 557 | specified. 558 | 559 | Args: 560 | ra (:obj:`float`, scalar or array-like): Galactic longitude, in degrees, 561 | or as an :obj:`astropy.unit.Quantity`. 562 | dec (`float`, scalar or array-like): Galactic latitude, in degrees, 563 | or as an :obj:`astropy.unit.Quantity`. 564 | d (Optional[:obj:`float`, scalar or array-like]): Distance from the Solar 565 | System, in kpc, or as an :obj:`astropy.unit.Quantity`. Defaults to 566 | ``None``, meaning no distance is specified. 567 | frame (Optional[:obj:`icrs`]): The coordinate system. Can be ``'icrs'`` (the 568 | default), ``'fk5'``, ``'fk4'`` or ``'fk4noeterms'``. 569 | **kwargs: Any additional keyword arguments accepted by derived 570 | classes. 571 | 572 | Returns: 573 | The results of the query, which must be implemented by derived 574 | classes. 575 | """ 576 | 577 | valid_frames = ['icrs', 'fk4', 'fk5', 'fk4noeterms'] 578 | 579 | if frame not in valid_frames: 580 | raise ValueError( 581 | '`frame` not understood. Must be one of {}.'.format(valid_frames)) 582 | 583 | if not isinstance(ra, units.Quantity): 584 | ra = ra * units.deg 585 | if not isinstance(dec, units.Quantity): 586 | dec = dec * units.deg 587 | 588 | if d is None: 589 | coords = coordinates.SkyCoord(ra, dec, frame='icrs') 590 | else: 591 | if not isinstance(d, units.Quantity): 592 | d = d * units.kpc 593 | coords = coordinates.SkyCoord( 594 | ra, dec, 595 | distance=d, 596 | frame='icrs') 597 | 598 | return self.query(coords, **kwargs) 599 | 600 | 601 | class WebSelectionFunction(object): 602 | """ 603 | Base class for querying selection functions through a web API. For each individual 604 | selection functions, a different subclass should be written, specifying the base URL. 605 | """ 606 | 607 | def __init__(self, api_url=None, map_name=''): 608 | """ 609 | Initialize the :obj:`WebSelectionFunctions` object. 610 | 611 | Args: 612 | api_url (Optional[:obj:`str`]): The base URL for the API. Defaults to 613 | ``'http://argonaut.skymaps.info/api/v2/'``. 614 | map_name (Optional[:obj:`str`]): The name of the selection function to query. For 615 | example, the Green et al. (2015) dust map is hosted at 616 | ``http://argonaut.skymaps.info/api/v2/bayestar2015``, so the 617 | correct specifier for that map is ``map_name='bayestar2015'``. 618 | """ 619 | if api_url is None: 620 | api_url = 'http://argonaut.skymaps.info/api/v2/' 621 | self.base_url = api_url.rstrip('/') + '/' + map_name.lstrip('/') 622 | 623 | @ensure_coord_type 624 | def __call__(self, coords, **kwargs): 625 | """ 626 | An alias for :obj:`WebDustMap.query()`. 627 | """ 628 | return self.query(coords, **kwargs) 629 | 630 | @ensure_coord_type 631 | @web_api_method('/query') 632 | def query(self, coords, **kwargs): 633 | """ 634 | A web API version of :obj:`SelectionFunction.query`. See the documentation for the 635 | corresponding local query object. 636 | 637 | Args: 638 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates at which to 639 | query the selection function. 640 | """ 641 | pass 642 | 643 | @web_api_method('/query') 644 | def query_gal(self, l, b, d=None, **kwargs): 645 | """ 646 | A web API version of :obj:`SelectionFunction.query_gal()`. See the documentation for 647 | the corresponding local query object. Queries using Galactic 648 | coordinates. 649 | 650 | Args: 651 | l (:obj:`float`, scalar or array-like): Galactic longitude, in degrees, 652 | or as an :obj:`astropy.unit.Quantity`. 653 | b (:obj:`float`, scalar or array-like): Galactic latitude, in degrees, 654 | or as an :obj:`astropy.unit.Quantity`. 655 | d (Optional[:obj:`float`, scalar or array-like]): Distance from the Solar 656 | System, in kpc, or as an :obj:`astropy.unit.Quantity`. Defaults to 657 | ``None``, meaning no distance is specified. 658 | **kwargs: Any additional keyword arguments accepted by derived 659 | classes. 660 | 661 | Returns: 662 | The results of the query. 663 | """ 664 | pass 665 | 666 | @web_api_method('/query') 667 | def query_equ(self, ra, dec, d=None, frame='icrs', **kwargs): 668 | """ 669 | A web API version of :obj:`SelectionFunction.query_equ()`. See the documentation for 670 | the corresponding local query object. Queries using Equatorial 671 | coordinates. By default, the ICRS frame is used, although other frames 672 | implemented by :obj:`astropy.coordinates` may also be specified. 673 | 674 | Args: 675 | ra (:obj:`float`, scalar or array-like): Galactic longitude, in degrees, 676 | or as an :obj:`astropy.unit.Quantity`. 677 | dec (:obj:`float`, scalar or array-like): Galactic latitude, in degrees, 678 | or as an :obj:`astropy.unit.Quantity`. 679 | d (Optional[:obj:`float`, scalar or array-like]): Distance from the Solar 680 | System, in kpc, or as an :obj:`astropy.unit.Quantity`. Defaults to 681 | ``None``, meaning no distance is specified. 682 | frame (Optional[icrs]): The coordinate system. Can be 'icrs' (the 683 | default), 'fk5', 'fk4' or 'fk4noeterms'. 684 | **kwargs: Any additional keyword arguments accepted by derived 685 | classes. 686 | 687 | Returns: 688 | The results of the query. 689 | """ 690 | pass 691 | -------------------------------------------------------------------------------- /selectionfunctions/output/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory, except .gitignore 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /selectionfunctions/rrl_mateu_2020.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # rrl_mateu_2020.py 4 | # Implements querying of the completeness maps from 5 | # Mateu, Holl, De Ridder & Rimoldini (2020). 6 | # 7 | # Copyright (C) 2020 Douglas Boubert & Andrew Everall 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | # 23 | 24 | from __future__ import print_function, division 25 | 26 | import os 27 | import h5py 28 | import numpy as np 29 | 30 | import astropy.coordinates as coordinates 31 | import astropy.units as units 32 | import h5py 33 | import healpy as hp 34 | import pandas as pd 35 | 36 | from .std_paths import * 37 | from .map import SelectionFunction, ensure_flat_galactic, coord2healpix 38 | from .source import ensure_gaia_g, ensure_distance 39 | from . import fetch_utils 40 | 41 | from time import time 42 | 43 | 44 | class rrl_sf(SelectionFunction): 45 | """ 46 | Queries the Gaia DR2 selection function (Boubert & Everall, 2019). 47 | """ 48 | 49 | def __init__(self, survey='gaiadr2', map_dimension='2d_bright', rrl_type='rrab', bounds_value=0.0, null_value=np.nan, map_fname=None): 50 | """ 51 | Args: 52 | survey (Optional[:obj:`str`]): Which survey to return the completeness of. Valid surveys 53 | are :obj:`'gaiadr2'`, :obj:`'asas'` and :obj:`'ps1'`. 54 | Defaults to :obj:`'gaiadr2'`. 55 | map_dimension (Optional[:obj:`str`]): Whether to use the :obj:`'2d_bright'`, :obj:`'2d_faint'` or :obj:`'3d'` map. 56 | Defaults to :obj:`'2d_bright'`. 57 | rrl_type (Optional[:obj:`str`]): Whether to return the completeness of :obj:`'rrab'` or :obj:`'rrc'` RRL stars. 58 | Defaults to :obj:`'ab'`. 59 | bounds_value (Optional[:obj:`float`]): What value to use for the completeness of RRL that fall outside the map grid. 60 | Defaults to :obj:`'0.0'`. 61 | null_value (Optional[:obj:`float`]): What value to use for the completeness of RRL that fall outside the map grid. 62 | Defaults to :obj:`'0.0'`. 63 | map_fname (Optional[:obj:`str`]): Filename of the completeness map to use overriding the other keywords. Defaults to 64 | :obj:`None`. 65 | """ 66 | 67 | # Identify which map we are loading 68 | if map_fname is None: 69 | 70 | # Validate input 71 | if survey == 'gaiadr2' or survey == 'asas' or survey == 'ps1': 72 | self._survey = survey 73 | else: 74 | raise ValueError('survey must be either "gaiadr2", "asas" or "ps1".') 75 | 76 | if map_dimension[:2] == '2d' and (map_dimension[3:] == 'bright' or map_dimension[3:] == 'faint'): 77 | self._dimension = '2d' 78 | self._bright_or_faint = map_dimension[3:] 79 | self._fname = 'completeness2d'+'.'+self._bright_or_faint 80 | elif map_dimension == '3d': 81 | self._dimension = '3d' 82 | self._fname = 'completeness3d'+'.'+self._survey 83 | if self._survey == 'gaiadr2': 84 | self._fname = self._fname + '.vcsos' 85 | 86 | else: 87 | raise ValueError('map_dimension must be either "2d_bright", "2d_faint", or "3d".') 88 | 89 | 90 | if rrl_type == 'rrab' or rrl_type == 'rrc': 91 | self._rrl_type = rrl_type 92 | self._fname = self._fname + '.' + self._rrl_type + '.tab' 93 | else: 94 | raise ValueError('rrl_type must be either "rrab" or "rrc".') 95 | 96 | # Verify that our combination is valid 97 | if self._dimension == '2d' and self._bright_or_faint == 'bright' and self._survey == 'ps1': 98 | raise ValueError('Only "gaia" and "asas" have a "2d_bright" completeness map.') 99 | 100 | self._null_value = null_value 101 | map_fname = os.path.join(data_dir(), 'rrl_mateu_2020', self._fname) 102 | 103 | 104 | t_start = time() 105 | 106 | print('Loading data ...') 107 | map_data = pd.read_csv(map_fname) 108 | 109 | if self._dimension == '2d': 110 | 111 | if self._bright_or_faint == 'bright': 112 | self._nside = 4 113 | self._g_bins = np.array([11.0,13.0,15.0,17.0]) 114 | 115 | if self._survey == 'gaiadr2': 116 | self._completeness = np.stack([map_data['Gaia[11,13]'],map_data['Gaia[13,15]'],map_data['Gaia[15,17]']]) 117 | elif self._survey == 'asas': 118 | self._completeness = np.stack([map_data['ASAS[11,13]'],map_data['ASAS[13,15]'],map_data['ASAS[15,17]']]) 119 | 120 | else: 121 | self._nside = 16 122 | self._g_bins = np.array([13.0,16.0,18.0,22.0]) 123 | if self._survey == 'gaiadr2': 124 | self._completeness = np.stack([map_data['Gaia[13,16]'],map_data['Gaia[16,18]'],map_data['Gaia[18,22]']]) 125 | elif self._survey == 'asas': 126 | self._completeness = np.stack([map_data['ASAS[13,16]'],map_data['ASAS[16,18]'],map_data['ASAS[18,22]']]) 127 | elif self._survey == 'ps1': 128 | self._completeness = np.stack([map_data['PS1[13,16]'],map_data['PS1[16,18]'],map_data['PS1[18,22]']]) 129 | 130 | self._completeness_dict = {_n:{'bins':self._g_bins,'completeness':self._completeness[:,_n]} for _n in range(hp.nside2npix(self._nside))} 131 | 132 | elif self._dimension == '3d': 133 | self._nside = 4 134 | self._hpx = map_data['hpix'].values 135 | self._distance_lower = map_data['D_o'].values 136 | self._distance_upper = map_data['D_f'].values 137 | self._completeness = map_data['C'].values 138 | 139 | # Create the dictionary 140 | self._completeness_dict = {} 141 | counts = np.unique(self._hpx,return_counts=True)[1] 142 | _idx = 0 143 | for _n in range(hp.nside2npix(self._nside)): 144 | d_bins = np.concatenate([self._distance_lower[_idx:_idx+counts[_n]],[self._distance_upper[_idx+counts[_n]-1]]]) 145 | self._completeness_dict[_n] = {'bins':d_bins,'completeness':self._completeness[_idx:_idx+counts[_n]]} 146 | _idx += counts[_n] 147 | 148 | 149 | 150 | t_data = time() 151 | 152 | t_finish = time() 153 | 154 | print('t = {:.3f} s'.format(t_finish - t_start)) 155 | print(' data: {: >7.3f} s'.format(t_data-t_start)) 156 | 157 | 158 | @ensure_flat_galactic 159 | def query(self, sources): 160 | """ 161 | Returns the selection function at the requested coordinates. 162 | 163 | Args: 164 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates to query. 165 | 166 | Returns: 167 | Selection function at the specified coordinates, as a fraction. 168 | 169 | """ 170 | if self._dimension == '2d': 171 | @ensure_gaia_g 172 | def _query(self,sources): 173 | 174 | # Convert coordinates to healpix indices 175 | hpxidx = coord2healpix(sources.coord, 'galactic', self._nside, nest=False) 176 | 177 | # Extract Gaia G magnitude 178 | x = sources.photometry.measurement['gaia_g'] 179 | 180 | n_coords = hpxidx.size 181 | selection_function = np.zeros(n_coords) 182 | for _idx in range(n_coords): 183 | box = self._completeness_dict[hpxidx[_idx]] 184 | _cidx = np.searchsorted(box['bins'],x[_idx]) 185 | if _cidx == 0 or _cidx == box['bins'].size: 186 | selection_function[_idx] = self._null_value 187 | else: 188 | selection_function[_idx] = box['completeness'][_cidx-1] 189 | return selection_function 190 | 191 | elif self._dimension == '3d': 192 | @ensure_distance 193 | def _query(self,sources): 194 | 195 | # Convert coordinates to healpix indices 196 | hpxidx = coord2healpix(sources.coord, 'galactic', self._nside, nest=False) 197 | 198 | # Extract distance 199 | x = sources.coord.distance.kpc 200 | 201 | n_coords = hpxidx.size 202 | selection_function = np.zeros(n_coords) 203 | for _idx in range(n_coords): 204 | box = self._completeness_dict[hpxidx[_idx]] 205 | _cidx = np.searchsorted(box['bins'],x[_idx]) 206 | if _cidx == 0 or _cidx == box['bins'].size: 207 | selection_function[_idx] = self._null_value 208 | else: 209 | selection_function[_idx] = box['completeness'][_cidx-1] 210 | return selection_function 211 | 212 | return _query(self,sources) 213 | 214 | 215 | def fetch(): 216 | """ 217 | Downloads the specified version of the Bayestar dust map. 218 | 219 | Args: 220 | version (Optional[:obj:`str`]): The map version to download. Valid versions are 221 | :obj:`'bayestar2019'` (Green, Schlafly, Finkbeiner et al. 2019), 222 | :obj:`'bayestar2017'` (Green, Schlafly, Finkbeiner et al. 2018) and 223 | :obj:`'bayestar2015'` (Green, Schlafly, Finkbeiner et al. 2015). Defaults 224 | to :obj:`'bayestar2019'`. 225 | 226 | Raises: 227 | :obj:`ValueError`: The requested version of the map does not exist. 228 | 229 | :obj:`DownloadError`: Either no matching file was found under the given DOI, or 230 | the MD5 sum of the file was not as expected. 231 | 232 | :obj:`requests.exceptions.HTTPError`: The given DOI does not exist, or there 233 | was a problem connecting to the Dataverse. 234 | """ 235 | 236 | doi = '10.7910/DVN/HX0RNB' 237 | for _fname in ['completeness2d.bright.rrab.tab', 238 | 'completeness2d.bright.rrc.tab', 239 | 'completeness2d.faint.rrab.tab', 240 | 'completeness2d.faint.rrc.tab', 241 | 'completeness3d.asas.rrab.tab', 242 | 'completeness3d.asas.rrc.tab', 243 | 'completeness3d.gaiadr2.vcsos.rrab.tab', 244 | 'completeness3d.gaiadr2.vcsos.rrc.tab', 245 | 'completeness3d.ps1.rrab.tab', 246 | 'completeness3d.ps1.rrc.tab']: 247 | 248 | requirements = {'filename': _fname} 249 | 250 | local_fname = os.path.join(data_dir(), 'rrl_mateu_2020', _fname) 251 | 252 | # Download the data 253 | fetch_utils.dataverse_download_doi( 254 | doi, 255 | local_fname, 256 | original = True, 257 | file_requirements=requirements) 258 | -------------------------------------------------------------------------------- /selectionfunctions/sets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # sets.py 4 | # Generates selection functions for combined samples (e.g. APOGEE + GaiaDR2) 5 | # 6 | # Copyright (C) 2020 Douglas Boubert & Andrew Everall 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import os 26 | import h5py 27 | import numpy as np 28 | 29 | import astropy.coordinates as coordinates 30 | import astropy.units as units 31 | import h5py 32 | import healpy as hp 33 | from scipy import interpolate, special 34 | 35 | from .std_paths import * 36 | from .map import SelectionFunction, ensure_flat_icrs, coord2healpix 37 | from . import fetch_utils 38 | 39 | from time import time 40 | 41 | 42 | class intersect_sf(SelectionFunction): 43 | """ 44 | Queries the Gaia DR2 selection function (Boubert & Everall, 2019). 45 | """ 46 | 47 | def __init__(self, instances): 48 | """ 49 | Args: 50 | map_fname (Optional[:obj:`str`]): Filename of the BoubertEverall2019 selection function. Defaults to 51 | :obj:`None`, meaning that the default location is used. 52 | version (Optional[:obj:`str`]): The selection function version to download. Valid versions 53 | are :obj:`'modelT'` and :obj:`'modelAB'` 54 | Defaults to :obj:`'modelT'`. 55 | crowding (Optional[:obj:`bool`]): Whether or not the selection function includes crowding. 56 | Defaults to :obj:`'False'`. 57 | bounds (Optional[:obj:`bool`]): Whether or not the selection function is bounded to 0.0 < G < 25.0. 58 | Defaults to :obj:`'True'`. 59 | """ 60 | 61 | t_start = time() 62 | 63 | self.sf_inst = instances 64 | 65 | t_sf = time() 66 | t_finish = time() 67 | 68 | print('t = {:.3f} s'.format(t_finish - t_start)) 69 | print(' sf: {: >7.3f} s'.format(t_sf-t_start)) 70 | 71 | def _selection_function(self,sources, sources_shape): 72 | 73 | 74 | _result=np.ones(sources_shape) 75 | 76 | for ii in range(len(self.sf_inst)): 77 | _result *= self.sf_inst[ii](sources) 78 | 79 | return _result 80 | 81 | 82 | @ensure_flat_icrs 83 | def query(self, sources): 84 | """ 85 | Returns the selection function at the requested coordinates. 86 | 87 | Args: 88 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates to query. 89 | 90 | Returns: 91 | Selection function at the specified coordinates, as a fraction. 92 | 93 | """ 94 | 95 | # Extract sources array shape (if float, get's converted to 1D array) 96 | sources_shape = np.array(next(iter(sources.photometry.measurement.values()))).shape 97 | 98 | # Evaluate selection function 99 | selection_function = self._selection_function(sources, sources_shape) 100 | 101 | return selection_function 102 | 103 | 104 | class union_sf(SelectionFunction): 105 | """ 106 | Queries the Gaia DR2 selection function (Boubert & Everall, 2019). 107 | """ 108 | 109 | def __init__(self, instances): 110 | """ 111 | Args: 112 | map_fname (Optional[:obj:`str`]): Filename of the BoubertEverall2019 selection function. Defaults to 113 | :obj:`None`, meaning that the default location is used. 114 | version (Optional[:obj:`str`]): The selection function version to download. Valid versions 115 | are :obj:`'modelT'` and :obj:`'modelAB'` 116 | Defaults to :obj:`'modelT'`. 117 | crowding (Optional[:obj:`bool`]): Whether or not the selection function includes crowding. 118 | Defaults to :obj:`'False'`. 119 | bounds (Optional[:obj:`bool`]): Whether or not the selection function is bounded to 0.0 < G < 25.0. 120 | Defaults to :obj:`'True'`. 121 | """ 122 | 123 | t_start = time() 124 | 125 | self.sf_inst = instances 126 | 127 | t_sf = time() 128 | t_finish = time() 129 | 130 | print('t = {:.3f} s'.format(t_finish - t_start)) 131 | print(' sf: {: >7.3f} s'.format(t_sf-t_start)) 132 | 133 | def _selection_function(self,sources, sources_shape): 134 | 135 | 136 | _not_result=np.ones(sources_shape) 137 | 138 | for ii in range(len(self.sf_inst)): 139 | _not_result *= (1-self.sf_inst[ii](sources)) 140 | 141 | _result = 1-_not_result 142 | 143 | return _result 144 | 145 | 146 | @ensure_flat_icrs 147 | def query(self, sources): 148 | """ 149 | Returns the selection function at the requested coordinates. 150 | 151 | Args: 152 | coords (:obj:`astropy.coordinates.SkyCoord`): The coordinates to query. 153 | 154 | Returns: 155 | Selection function at the specified coordinates, as a fraction. 156 | 157 | """ 158 | 159 | # Extract sources array shape (if float, get's converted to 1D array) 160 | sources_shape = np.array(next(iter(sources.photometry.measurement.values()))).shape 161 | 162 | # Evaluate selection function 163 | selection_function = self._selection_function(sources, sources_shape) 164 | 165 | return selection_function 166 | -------------------------------------------------------------------------------- /selectionfunctions/sfexceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # sfexceptions.py 4 | # Defines exceptions for the selectionfunctions package. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | from . import std_paths 26 | 27 | class Error(Exception): 28 | pass 29 | 30 | class CoordFrameError(Error): 31 | pass 32 | 33 | 34 | def data_missing_message(package, name): 35 | return ("The {name} selection function is not in the data directory:\n\n" 36 | " {data_dir}\n\n" 37 | "To change the data directory, call:\n\n" 38 | " from selectionfunctions.config import config\n" 39 | " config['data_dir'] = '/path/to/data/directory'\n\n" 40 | "To download the {name} selection function to the data directory, call:\n\n" 41 | " import selectionfunctions.{package}\n" 42 | " selectionfunctions.{package}.fetch()\n").format( 43 | data_dir=std_paths.data_dir(), 44 | package=package, 45 | name=name) 46 | -------------------------------------------------------------------------------- /selectionfunctions/source.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # source.py 4 | # Provides a new class that extends astropy SkyCoord. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import astropy.coordinates as coordinates 26 | import astropy.units as units 27 | 28 | from functools import wraps 29 | 30 | class Photometry(): 31 | def __init__(self,photometry,photometry_error): 32 | self.measurement = {k:v for k,v in photometry.items()} 33 | self.error = {k:v for k,v in photometry_error.items()} if photometry_error is not None else None 34 | 35 | class Source(): 36 | def __init__(self,*args,photometry={},photometry_error={},**kwargs): 37 | self.coord = coordinates.SkyCoord(*args,**kwargs) 38 | self.photometry = Photometry(photometry,photometry_error) if photometry is not None else None 39 | 40 | def ensure_distance(f): 41 | """ 42 | A decorator for class methods of the form 43 | 44 | .. code-block:: python 45 | 46 | Class.method(self, coords, **kwargs) 47 | 48 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 49 | 50 | The decorator ensures that the ``coords`` that gets passed to 51 | ``Class.method`` is a flat array of Equatorial coordinates. It also reshapes 52 | the output of ``Class.method`` to have the same shape (possibly scalar) as 53 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 54 | (instead of an array), each element in the output is reshaped instead. 55 | 56 | Args: 57 | f (class method): A function with the signature 58 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 59 | object containing an array. 60 | 61 | Returns: 62 | A function that takes :obj:`SkyCoord` input with any shape (including 63 | scalar). 64 | """ 65 | 66 | @wraps(f) 67 | def _wrapper_func(self, sources, **kwargs): 68 | # t0 = time.time() 69 | 70 | has_distance = hasattr(sources.coord, 'distance') 71 | if has_distance: 72 | pass 73 | else: 74 | raise ValueError('You need to pass in a distance to use this selection function.') 75 | 76 | out = f(self, sources, **kwargs) 77 | 78 | return out 79 | 80 | return _wrapper_func 81 | 82 | def ensure_gaia_g(f): 83 | """ 84 | A decorator for class methods of the form 85 | 86 | .. code-block:: python 87 | 88 | Class.method(self, coords, **kwargs) 89 | 90 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 91 | 92 | The decorator ensures that the ``coords`` that gets passed to 93 | ``Class.method`` is a flat array of Equatorial coordinates. It also reshapes 94 | the output of ``Class.method`` to have the same shape (possibly scalar) as 95 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 96 | (instead of an array), each element in the output is reshaped instead. 97 | 98 | Args: 99 | f (class method): A function with the signature 100 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 101 | object containing an array. 102 | 103 | Returns: 104 | A function that takes :obj:`SkyCoord` input with any shape (including 105 | scalar). 106 | """ 107 | 108 | @wraps(f) 109 | def _wrapper_func(self, sources, **kwargs): 110 | # t0 = time.time() 111 | 112 | has_photometry = hasattr(sources, 'photometry') 113 | if has_photometry: 114 | has_gaia_g = 'gaia_g' in sources.photometry.measurement.keys() 115 | if has_gaia_g: 116 | pass 117 | else: 118 | print('No Gaia G passed, but transformation is not yet implemented.') 119 | raise ValueError('You need to pass in Gaia G-band photometric magnitudes to use this selection function.') 120 | else: 121 | raise ValueError('You need to pass in Gaia G-band photometric magnitudes to use this selection function.') 122 | 123 | out = f(self, sources, **kwargs) 124 | 125 | return out 126 | 127 | return _wrapper_func 128 | 129 | def ensure_gaia_g_gaia_rp(f): 130 | """ 131 | A decorator for class methods of the form 132 | 133 | .. code-block:: python 134 | 135 | Class.method(self, coords, **kwargs) 136 | 137 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 138 | 139 | The decorator ensures that the ``coords`` that gets passed to 140 | ``Class.method`` is a flat array of Equatorial coordinates. It also reshapes 141 | the output of ``Class.method`` to have the same shape (possibly scalar) as 142 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 143 | (instead of an array), each element in the output is reshaped instead. 144 | 145 | Args: 146 | f (class method): A function with the signature 147 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 148 | object containing an array. 149 | 150 | Returns: 151 | A function that takes :obj:`SkyCoord` input with any shape (including 152 | scalar). 153 | """ 154 | 155 | @wraps(f) 156 | def _wrapper_func(self, sources, **kwargs): 157 | # t0 = time.time() 158 | 159 | has_photometry = hasattr(sources, 'photometry') 160 | if has_photometry: 161 | has_gaia_g_gaia_rp = ('gaia_g_gaia_rp' in sources.photometry.measurement.keys()) | \ 162 | (('gaia_g' in sources.photometry.measurement.keys())&\ 163 | ('gaia_rp'in sources.photometry.measurement.keys())) 164 | if has_gaia_g_gaia_rp | ( not self.require_colour ): 165 | pass 166 | else: 167 | print('No Gaia G-Grp passed.') 168 | raise ValueError('You need to pass in Gaia G-Grp photometric colour to use this selection function.') 169 | else: 170 | raise ValueError('You need to pass in Gaia G-band photometric magnitudes and G-Grp colour to use this selection function.') 171 | 172 | out = f(self, sources, **kwargs) 173 | 174 | return out 175 | 176 | return _wrapper_func 177 | 178 | def ensure_tmass_hjk(f): 179 | """ 180 | A decorator for class methods of the form 181 | 182 | .. code-block:: python 183 | 184 | Class.method(self, coords, **kwargs) 185 | 186 | where ``coords`` is an :obj:`astropy.coordinates.SkyCoord` object. 187 | 188 | The decorator ensures that the ``coords`` that gets passed to 189 | ``Class.method`` is a flat array of Equatorial coordinates. It also reshapes 190 | the output of ``Class.method`` to have the same shape (possibly scalar) as 191 | the input ``coords``. If the output of ``Class.method`` is a tuple or list 192 | (instead of an array), each element in the output is reshaped instead. 193 | 194 | Args: 195 | f (class method): A function with the signature 196 | ``(self, coords, **kwargs)``, where ``coords`` is a :obj:`SkyCoord` 197 | object containing an array. 198 | 199 | Returns: 200 | A function that takes :obj:`SkyCoord` input with any shape (including 201 | scalar). 202 | """ 203 | 204 | @wraps(f) 205 | def _wrapper_func(self, sources, **kwargs): 206 | # t0 = time.time() 207 | 208 | has_photometry = hasattr(sources, 'photometry') 209 | if has_photometry: 210 | has_tmass_h = 'tmass_h' in sources.photometry.measurement.keys() 211 | has_tmass_jk = 'tmass_jk' in sources.photometry.measurement.keys() 212 | if has_tmass_h: pass 213 | else: 214 | print('No 2MASS H magnitude passed (tmass_h), but transformation is not yet implemented.') 215 | raise ValueError('You need to pass in 2MASS H photometric magnitudes to use this selection function.') 216 | if has_tmass_jk: pass 217 | else: 218 | print('No 2MASS J-K colour passed (tmass_jk), but transformation is not yet implemented.') 219 | raise ValueError('You need to pass in 2MASS J-K colour to use this selection function.') 220 | else: 221 | raise ValueError('You need to pass in 2MASS H photometric magnitudes and J-K colours to use this selection function.') 222 | 223 | out = f(self, sources, **kwargs) 224 | 225 | return out 226 | 227 | return _wrapper_func 228 | -------------------------------------------------------------------------------- /selectionfunctions/std_paths.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # std_paths.py 4 | # Defines a set of paths used by scripts in the selectionfunctions module. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import os 26 | from .config import config 27 | 28 | 29 | script_dir = os.path.dirname(os.path.realpath(__file__)) 30 | data_dir_default = os.path.abspath(os.path.join(script_dir, 'data')) 31 | test_dir = os.path.abspath(os.path.join(script_dir, 'tests')) 32 | output_dir_default = os.path.abspath(os.path.join(script_dir, 'output')) 33 | 34 | 35 | def fix_path(path): 36 | """ 37 | Returns an absolute path, with '~' expanded to the user's home directory. 38 | """ 39 | return os.path.abspath(os.path.expanduser(path)) 40 | 41 | 42 | def data_dir(): 43 | """ 44 | Returns the directory used to store large data files (e.g., dust maps). 45 | """ 46 | dirname = config.get('data_dir', data_dir_default) 47 | return fix_path(dirname) 48 | 49 | 50 | def output_dir(): 51 | """ 52 | Returns a directory that can be used to store temporary output. 53 | """ 54 | dirname = config.get('output_dir', output_dir_default) 55 | return fix_path(dirname) 56 | -------------------------------------------------------------------------------- /selectionfunctions/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # __init__.py 4 | # Makes the tests in the package "selectionfunctions" discoverable. 5 | # 6 | # Copyright (C) 2019 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | -------------------------------------------------------------------------------- /selectionfunctions/tests/test_cog_ii.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # test_cog_ii.py 4 | # Test query code for the Boubert & Everall (2020) selection function. 5 | # 6 | # Copyright (C) 2020 Douglas Boubert & Andrew Everall. 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | import unittest 26 | 27 | import numpy as np 28 | import astropy.coordinates as coords 29 | import astropy.units as units 30 | import os 31 | import re 32 | import time 33 | 34 | from .. import cog_ii 35 | from ..std_paths import * 36 | from ..source import Source 37 | 38 | class TestCoGII(unittest.TestCase): 39 | @classmethod 40 | def setUpClass(self): 41 | print('Loading cog_ii.dr2_sf query object ...') 42 | t0 = time.time() 43 | 44 | # Set up query object 45 | self._cog_ii_dr2_sf = cog_ii.dr2_sf(crowding=True) 46 | 47 | t1 = time.time() 48 | print('Loaded cog_ii.dr2_sf test function in {:.5f} s.'.format(t1-t0)) 49 | 50 | def test_plot_selection_function(self): 51 | # Draw random coordinates, both above and below dec = -30 degree line 52 | n_pix = 100000 53 | ra = -180. + 360.*np.random.random(n_pix) 54 | dec = -45. + 90.*np.random.random(n_pix) # 45 degrees above/below 55 | G = 23.5*np.random.random(n_pix) 56 | c = Source(ra, dec, photometry={'gaia_g':G}, frame='icrs', unit='deg') 57 | 58 | sf_calc = self._cog_ii_dr2_sf(c) 59 | 60 | import matplotlib.pyplot as plt 61 | plt.hexbin(G,sf_calc,mincnt=1) 62 | #plt.savefig('./tests/test.png',dpi=500) 63 | plt.show() 64 | 65 | def test_bounds(self): 66 | """ 67 | Test that out-of-bounds magnitudes return 0.0 selection. 68 | """ 69 | 70 | # Draw random coordinates, both above and below dec = -30 degree line 71 | n_pix = 1000 72 | ra = -180. + 360.*np.random.random(n_pix) 73 | dec = -75. + 90.*np.random.random(n_pix) # 45 degrees above/below 74 | G = -25+100*np.random.random(n_pix) 75 | c = Source(ra, dec, photometry={'gaia_g':G}, frame='icrs', unit='deg') 76 | 77 | sf_calc = self._cog_ii_dr2_sf(c) 78 | 79 | zero_below = sf_calc[G < 0.0]<1e-8 80 | zero_above = sf_calc[G > 25.0]<1e-8 81 | 82 | # print r'{:s}: {:.5f}% nan above dec=-25 deg.'.format(mode, 100.*pct_nan_above) 83 | 84 | self.assertTrue(np.all(zero_below)) 85 | self.assertTrue(np.all(zero_above)) 86 | 87 | def test_shape(self): 88 | """ 89 | Test that the output shapes are as expected with input coordinate arrays 90 | of different shapes. 91 | """ 92 | 93 | for reps in range(5): 94 | # Draw random coordinates, with different shapes 95 | n_dim = np.random.randint(1,4) 96 | shape = np.random.randint(1,7, size=(n_dim,)) 97 | 98 | ra = -180. + 360.*np.random.random(shape) 99 | dec = -90. + 180. * np.random.random(shape) 100 | G = 1.7+19.8*np.random.random(shape) 101 | c = Source(ra, dec, photometry={'gaia_g':G}, frame='icrs', unit='deg') 102 | 103 | sf_calc = self._cog_ii_dr2_sf(c) 104 | 105 | np.testing.assert_equal(sf_calc.shape[:n_dim], shape) 106 | 107 | self.assertEqual(len(sf_calc.shape), n_dim) 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /selectionfunctions/unstructured_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # unstructured_map.py 4 | # Implements a class for querying selection functions with unstructured pixels. Sky 5 | # coordinates are assigned to the nearest pixel. 6 | # 7 | # Copyright (C) 2020 Douglas Boubert 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | # 23 | 24 | from __future__ import print_function, division 25 | 26 | import numpy as np 27 | import astropy.coordinates as coordinates 28 | import astropy.units as units 29 | from scipy.spatial import cKDTree as KDTree 30 | 31 | from .map_base import SelectionFunction 32 | 33 | 34 | class UnstructuredSelectionFunction(SelectionFunction): 35 | """ 36 | A class for querying selection functions with unstructured pixels. Sky coordinates are 37 | assigned to the nearest pixel. 38 | """ 39 | 40 | def __init__(self, pix_coords, max_pix_scale, metric_p=2, frame=None): 41 | """ 42 | Args: 43 | pix_coords (array-like :obj:`astropy.coordinates.SkyCoord`): The sky 44 | coordinates of the pixels. 45 | max_pix_scale (scalar :obj:`astropy.units.Quantity`): Maximum angular 46 | extent of a pixel. If no pixel is within this distance of a 47 | query point, NaN will be returned for that query point. 48 | metric_p (Optional[:obj:`float`]): The metric to use. Defaults to 2, which 49 | is the Euclidean metric. A value of 1 corresponds to the 50 | Manhattan metric, while a value approaching infinity yields the 51 | maximum component metric. 52 | frame (Optional[:obj:`str`]): The coordinate frame to use internally. Must 53 | be a frame understood by :obj:`astropy.coordinates.SkyCoord`. 54 | Defaults to :obj:`None`, meaning that the frame will be inferred 55 | from :obj:`pix_coords`. 56 | """ 57 | self._n_pix = pix_coords.shape[0] 58 | self._metric_p = metric_p 59 | 60 | if frame is None: 61 | self._frame = pix_coords.frame 62 | else: 63 | self._frame = frame 64 | 65 | # Tesselate the space 66 | self._pix_vec = self._coords2vec(pix_coords) 67 | self._kd = KDTree(self._pix_vec) 68 | 69 | # Don't query more than this distance from any point 70 | self._max_pix_scale = max_pix_scale.to('rad').value 71 | 72 | def _coords2vec(self, coords): 73 | """ 74 | Converts from sky coordinates to unit vectors. Before conversion to unit 75 | vectors, the coordiantes are transformed to the coordinate system used 76 | internally by the :obj:`UnstructuredDustMap`, which can be set during 77 | initialization of the class. 78 | 79 | Args: 80 | coords (:obj:`astropy.coordinates.SkyCoord`): Input coordinates to 81 | convert to unit vectors. 82 | 83 | Returns: 84 | Cartesian unit vectors corresponding to the input coordinates, after 85 | transforming to the coordinate system used internally by the 86 | :obj:`UnstructuredDustMap`. 87 | """ 88 | 89 | # c = coords.transform_to(self._frame) 90 | # vec = np.empty((c.shape[0], 2), dtype='f8') 91 | # vec[:,0] = coordinates.Longitude(coords.l, wrap_angle=360.*units.deg).deg[:] 92 | # vec[:,1] = coords.b.deg[:] 93 | # return np.radians(vec) 94 | 95 | c = coords.transform_to(self._frame).represent_as('cartesian') 96 | vec_norm = np.sqrt(c.x**2 + c.y**2 + c.z**2) 97 | 98 | vec = np.empty((c.shape[0], 3), dtype=c.x.dtype) 99 | vec[:,0] = (c.x / vec_norm).value[:] 100 | vec[:,1] = (c.y / vec_norm).value[:] 101 | vec[:,2] = (c.z / vec_norm).value[:] 102 | 103 | return vec 104 | 105 | def _coords2idx(self, coords): 106 | """ 107 | Converts from sky coordinates to pixel indices. 108 | 109 | Args: 110 | coords (:obj:`astropy.coordinates.SkyCoord`): Sky coordinates. 111 | 112 | Returns: 113 | Pixel indices of the coordinates, with the same shape as the input 114 | coordinates. Pixels which are outside the map are given an index 115 | equal to the number of pixels in the map. 116 | """ 117 | 118 | x = self._coords2vec(coords) 119 | idx = self._kd.query(x, p=self._metric_p, 120 | distance_upper_bound=self._max_pix_scale) 121 | return idx[1] 122 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # setup.py 4 | # Package "selectionfunctions" for pip. 5 | # 6 | # Copyright (C) 2019 Douglas Boubert 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, write to the Free Software Foundation, Inc., 20 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21 | # 22 | 23 | from __future__ import print_function, division 24 | 25 | from setuptools import setup, Extension 26 | from setuptools.command.install import install 27 | import distutils.cmd 28 | 29 | import os 30 | import json 31 | import io 32 | 33 | 34 | class InstallCommand(install): 35 | description = install.description 36 | user_options = install.user_options + [ 37 | ('large-data-dir=', None, 'Directory to store large data files in.') 38 | ] 39 | 40 | def initialize_options(self): 41 | install.initialize_options(self) 42 | self.large_data_dir = None 43 | 44 | def finalize_options(self): 45 | if not self.large_data_dir is None: 46 | self.large_data_dir = os.path.abspath(os.path.expanduser(self.large_data_dir)) 47 | 48 | install.finalize_options(self) 49 | 50 | def run(self): 51 | if not self.large_data_dir is None: 52 | print('Large data directory is set to: {}'.format(self.large_data_dir)) 53 | with open(os.path.expanduser('~/.selectionfunctionsrc'), 'w') as f: 54 | json.dump({'data_dir': self.large_data_dir}, f, indent=2) 55 | 56 | # install.do_egg_install(self) # Due to bug in setuptools that causes old-style install 57 | install.run(self) 58 | 59 | 60 | def fetch_cog_ii(): 61 | import selectionfunctions.cog_ii 62 | selectionfunctions.cog_ii.fetch() 63 | 64 | 65 | class FetchCommand(distutils.cmd.Command): 66 | description = ('Fetch selection functions from the web, and store them in the data ' 67 | 'directory.') 68 | user_options = [ 69 | ('map-name=', None, 'Which selection functions to load.')] 70 | 71 | map_funcs = { 72 | 'cog_ii': fetch_cog_ii, 73 | } 74 | 75 | def initialize_options(self): 76 | self.map_name = None 77 | 78 | def finalize_options(self): 79 | try: 80 | import selectionfunctions 81 | except ImportError: 82 | print('You must install the package selectionfunctions before running the ' 83 | 'fetch command.') 84 | if not self.map_name in self.map_funcs: 85 | print('Valid map names are: {}'.format(self.map_funcs.keys())) 86 | 87 | def run(self): 88 | print('Fetching map: {}'.format(self.map_name)) 89 | self.map_funcs[self.map_name]() 90 | 91 | 92 | def readme(): 93 | with io.open('README.md', mode='r', encoding='utf-8') as f: 94 | return f.read() 95 | 96 | 97 | setup( 98 | name='selectionfunctions', 99 | version='1.0.0', 100 | description='Uniform interface for the selection functions of astronomical surveys.', 101 | long_description=readme(), 102 | long_description_content_type='text/markdown', 103 | url='https://github.com/gaiaverse/selectionfunctions', 104 | download_url='https://github.com/gaiaverse/selectionfunctions/archive/v1.0.0.tar.gz', 105 | author='Douglas Boubert', 106 | author_email='douglasboubert@gmail.com', 107 | license='GPLv2', 108 | packages=['selectionfunctions'], 109 | install_requires=[ 110 | 'numpy', 111 | 'scipy', 112 | 'astropy', 113 | 'h5py', 114 | 'healpy', 115 | 'requests', 116 | 'progressbar2', 117 | 'six' 118 | ], 119 | include_package_data=True, 120 | test_suite='nose.collector', 121 | tests_require=['nose'], 122 | zip_safe=False, 123 | cmdclass = { 124 | 'install': InstallCommand, 125 | 'fetch': FetchCommand, 126 | }, 127 | ) 128 | --------------------------------------------------------------------------------