├── binarystarsolve ├── __init__.py ├── .DS_Store └── binarystarsolve.py ├── requirements.txt ├── .DS_Store ├── setup.py ├── .gitignore ├── LICENSE └── README.md /binarystarsolve/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | numpy 3 | scipy 4 | twine==1.13.0 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMilsonPhysics/BinaryStarSolver/HEAD/.DS_Store -------------------------------------------------------------------------------- /binarystarsolve/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMilsonPhysics/BinaryStarSolver/HEAD/binarystarsolve/.DS_Store -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as readme_file: 4 | readme = readme_file.read() 5 | 6 | 7 | setup( 8 | name="BinaryStarSolver", 9 | version="1.2.0", 10 | author="Nicholas Milson", 11 | author_email="nick.milson@dal.ca", 12 | description="Solves for the orbital elements of binary stars, given radial velocity time series", 13 | long_description=readme, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/NickMilsonPhysics/BinaryStarSolver", 16 | packages=find_packages(), 17 | classifiers=[ 18 | "Programming Language :: Python :: 3.7", 19 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 20 | ], 21 | python_requires='>=3.6' 22 | ) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AUTHORS 2 | 3 | Caroline Barton, Dalhousie University and Nicholas Milson, Dalhousie University 4 | 5 | ## SUPERVISOR 6 | 7 | Dr. Philip Bennett, Dalhousie University 8 | 9 | ## INSTALLATION 10 | 11 | In your command line, call _pip install BinaryStarSolver_. 12 | Then, in your python code, write: _from binarystarsolve.binarystarsolve import StarSolve_ 13 | 14 | ## DESCRIPTION 15 | 16 | Given a series of radial velocities as a function of time for a star in a binary system, this program solves for various orbital parameters. Namely, it solves for eccentricity (e), argument of periastron (ω), velocity amplitude (K), long term average radial velocity (γ), and orbital period (P). 17 | 18 | If the orbital parameters of a primary star are already known, the program can also find the orbital parameters of a companion star, with only a few radial velocity data points. 19 | 20 | In the case of double-lined binary where both stars have equally good orbital coverage, then the program can solve for the orbital parameters of both stars at once. 21 | 22 | Note that K = n * a1 * sin(i) / ((1-e)^0.5), where a1 is the primary star's semi-major axis, n is the mean motion (n = 2π / P), and i is the inclination angle of the orbit. 23 | 24 | The equation the data is being fitted to is V(t) = γ + K*(cos(v(t) + ω) + e * cos(ω)), where v(t) is the true anomaly (angle from periastron). 25 | 26 | For more information on the implementation of this code, refer to the paper "A Python Code to Determine Orbital Parameters of Spectroscopic Binaries": https://arxiv.org/abs/2011.13914 (But also note the solving for both stars at once case may not be on the paper right away, but it should be there soon!) 27 | 28 | ## USER INSTRUCTIONS 29 | 30 | All the functionality of this program is accessed via the function StarSolve(). Whether the parameters being solved for are for a primary star or a companion star (or both at once), StarSolve() is to be used. 31 | 32 | The first two arguments of the function StarSolve() are data_file and star. data_file is a string, which is either the name of the txt file (if the file is in your working directory), or the file path (if the file is not in the working directory). star is also a string, where the three options are "primary", "secondary", or "both". Note that star is not case sensitive. 33 | 34 | ### PRIMARY STAR APPLICATION INSTRUCTIONS 35 | 36 | For solving the parameters of the primary star, this program works for two general types of radial velocity data sets. It works for data sets where all the data is from a singular deduction of the radial velocities. It also works for data sets which are composed of sub data sets, where each sub data set's radial velocities were deduced separately from the other subsets. Having a data set composed of subsets with radial velocities deduced separately may result in discrepancies of the long term average radial velocity (γ) between the subsets. This program can deal with this discrepancy by choosing one of the sub data sets' γ as the "true" γ, and shifting the other sub data set's radial velocities to match up with the chosen subset. 37 | 38 | For the case where all the data is from a singular deduction of the radial velocities, the user must supply their radial velocity data they wish to fit as a tab separated txt file, with the first value being the time in Reduced Julian Date (RJD), the second being the radial velocity in km/s, and the third (optional) value being the weights. 39 | 40 | For the case with the RV data set composed of multiple sub data sets, the user must supply their radial velocity data they wish to fit as a tab separated txt file, with the first value being the time in Reduced Julian Date (RJD), the second being the radial velocity in km/s, the third value being the weights, and the fourth value being an integer signifying which sub data set the data comes from. Note, if the user does not wish to assign weights to the data, the third column still must be filled and thus should be a column of ones. 41 | 42 | If the period of the orbit is already known, the user may pass in a float with the keyword argument Period (with the period in days). If the period of the orbit is already known, but the user would still like the period to be solved for, the user may pass in a float with the keyword argument Pguess (in days). 43 | 44 | If the user would like the estimated covariance matrix returned, the boolean keyword argument covariance may be passed in as True. If the period is previously known (and passed in using Period), the covariance matrix returned will be a 5 by 5 matrix. Otherwise, it will be a 6 by 6. By default, when star = "primary", StarSolve() creates two plots of the fit (one for RV vs RJD, one for RV vs phase). If the user would not like these graphs shown, they may use the optional boolean keyword graphs, and set it as False. 45 | 46 | For circular orbits (e = 0), the argument of periastron (ω) becomes undefined. If the user has reason to believe that the eccentricity is 0, they may pass in the optional boolean keyword parameter zeroEcc as True. This will set e and ω to 0 for the minimization, making the minimization run faster. It is also acceptable to have zeroEcc = False for circular orbits, but just note that the ω returned will not mean anything physcally. 47 | 48 | StarSolve will return a list of the solved parameters (along with asini and f(M)), in the order [γ, K, ω, e, T0, P, asin(i), f(M)], followed by a list of uncertainties associated with each parameter (and asin(i) and f(M)). As previously stated, the argument keyword covariance may be passed as True, so that the covariance matrix is returned too 49 | 50 | Warning: If no Period or Pguess is provided, StarSolve() cannot find reliable results if the RV data supplied does not span at least 1.5 periods and will fail if the data spans less than one full period. Furthermore, if the data set is composed of multiple sub data sets with different γ velocities, the program may fail to correct the offset in γ velocities if none of the sub data sets span at least 1.5 periods. Using a period determined by other methods (e.g. photometrically) is often preferable to allowing the program to solve for the period. The convergence of the minimization is fairly sensitive to initial period estimates. 51 | 52 | **Examples**: 53 | ``` 54 | params, err = StarSolve(data_file = "myRVdata.txt", star = "primary") 55 | ``` 56 | 57 | ``` 58 | params, err, cov = StarSolve(data_file = "myRVdata.txt", star = "primary", Period = 3784.3, covariance = True) 59 | ``` 60 | 61 | ``` 62 | params, err = StarSolve(data_file = "myRVdata.txt", star = "primary", Pguess = 7430, graphs = False) 63 | ``` 64 | 65 | ### COMPANION STAR APPLICATION INSTRUCTIONS 66 | 67 | The RV data must be formatted the same way for star = "secondary" as for star = "primary", i.e. as a tab separated txt file, with the first value being the time in Reduced Julian Date (RJD), the second being the radial velocity in km/s, and the third (optional) value being the weights. 68 | 69 | To find the parameters of a companion star, a list of the known parameters of the primary star, in the order [γ, K, ω, e, T0, P] (with ω in degrees), must be passed in using the keyword argument X. 70 | 71 | If the user wishes to return the error on the parameters of the companion star, they must also input a list of errors of the known parameters of the primary star (in the order [γ, K, ω, e, T0, P]), using the keyword err. 72 | 73 | Due to various observational reasons, there is often a discrepancy between the average radial velocity of the primary's γ1, and the companion star's γ2, though both should be equal. γ1 is assumed to be the "correct" γ, and is the γ returned; but for purposes of fitting the curve to the data, a γ2 is solved for. Using the boolean keyword shift = True, the discrepancy between average velocities (γ2 - γ1) is returned. 74 | 75 | By default, when star = "secondary", StarSolve() creates two plots of the fit (one for RV vs RJD, one for RV vs phase). If the user would not like these graphs shown, they may use the optional boolean keyword graphs, and set it as False. 76 | 77 | **Examples**: 78 | 79 | ``` 80 | params = StarSolve(data_file = "companion.txt", star = "secondary", X = [-6.4354, 13.956, 203.991, 0.20544, 56108.8, 3770.68]) 81 | ``` 82 | 83 | ``` 84 | params,err = StarSolve(data_file = "companion.txt", star = "secondary", X = [-6.4354, 13.956, 203.991, 0.20544, 56108.8, 3770.68], err = [0.025298, 0.034264, 0.613298, 0.0020712, 5.80324, 0]) 85 | ``` 86 | 87 | ``` 88 | params,shift = StarSolve(data_file = "companion.txt", star = "secondary", X = [-6.4354, 13.956, 203.991, 0.20544, 56108.8, 3770.68], shift = True, graphs = False) 89 | ``` 90 | 91 | ### PRIMARY AND SECONDARY STARS TOGETHER APPLICATION INSTRUCTIONS 92 | 93 | Solving the parameters of both stars at once using the keyword star = "both" only works for data sets that for every time t, there is a V_1(t) point and a V_2(t) point. 94 | 95 | Like with the star = "primary" case, the program works for two general types of radial velocity data sets. It works for data sets where all the data is from a singular deduction of the radial velocities. It also works for data sets which are composed of sub data sets, where each sub data set's radial velocities were deduced separately from the other subsets (so the γ's may be different). For the both stars case, this program deals with this discrepancy by finding γ for each sub data set, performing a weighted average to get a value of γ, and then shifting all the data sets to said weighted average value of γ. 96 | 97 | For the case where all the data is from a singular deduction of the radial velocities, the user must supply their radial velocity data they wish to fit as a tab separated txt file, with the first value being the time in Reduced Julian Date (RJD), the second being the radial velocity of the primary star in km/s, the third being the radial velocity of the secondary star in km/s, and the fourth and fifth (optional) values being the respective weights of the primary and second star's radial velocities. 98 | 99 | For the case with the RV data set composed of multiple sub data sets, the user must supply their radial velocity data they wish to fit as a tab separated txt file, with the first value being the time in Reduced Julian Date (RJD), the second being the primary star's radial velocity in km/s, the third value being the companion star's radial velocity in km/s, the fourth and fifth values being the respective weights of the primary and second star's radial velocities, and the sixth value being an integer signifying which sub data set the data comes from. Note, if the user does not wish to assign weights to the data, the fourth and fifth columns still must be filled and thus should be a column of ones. 100 | 101 | If the period of the orbit is already known, the user may pass in a float with the keyword argument Period (with the period in days). If the period of the orbit is already known, but the user would still like the period to be solved for, the user may pass in a float with the keyword argument Pguess (in days). 102 | 103 | If the user would like the estimated covariance matrices returned, the boolean keyword argument covariance may be passed in as True. This returns two covariance matrices (one for each star - primary star first). If the period is previously known (and passed in using Period), the covariance matrices returned will be 5 by 5 matrix. Otherwise, they will be 6 by 6. 104 | 105 | By default, when star = "both", StarSolve() creates four plots of the fits (one for RV vs RJD of the primary star, one for RV vs phase of the primary, one for RV vs RJD of the companion star, and one for RV vs phase of the companion). If the user would not like these graphs shown, they may use the optional boolean keyword graphs, and set it as False. 106 | 107 | For circular orbits (e = 0), the argument of periastron (ω) becomes undefined. If the user has reason to believe that the eccentricity is 0, they may pass in the optional boolean keyword parameter zeroEcc as True. This will set e and ω to 0 for the minimization, making the minimization run faster. It is also acceptable to have zeroEcc = False for circular orbits, but just note that the ω returned will not mean anything physically. 108 | 109 | When star = "both", StarSolve will return a list of the solved parameters for each star (along with asini and f(M)), in the order [γ, K, ω, e, T0, P, asin(i), f(M)], followed by a list for each star of uncertainties associated with each parameter (and asin(i) and f(M)). As previously stated, the argument keyword covariance may be passed as True, so that the covariance matrices are returned too. Note that for all these lists, the primary star's are returned first. 110 | 111 | **Examples**: 112 | 113 | ``` 114 | params, err, cov = StarSolve(data_file = "rvData.txt", star = "both",covariance = True, zeroEcc = True) 115 | ``` 116 | 117 | ``` 118 | params, err = StarSolve(data_file = "rvData.txt", star = "both", Pguess = 33.8, graphs = False) 119 | ``` 120 | 121 | 122 | If there are any questions, please contact me at nmilson@ualberta.ca. 123 | -------------------------------------------------------------------------------- /binarystarsolve/binarystarsolve.py: -------------------------------------------------------------------------------- 1 | """ 2 | All of the functionality of this package is through the function StarSolve(). 3 | Call help(binarystarsolve.binarystarsolve.StarSolve) for the associated Docstring. 4 | For a more detailed description of how to use this package, please read the ReadMe. 5 | https://github.com/NickMilsonPhysics/BinaryStarSolver/blob/master/README.md 6 | """ 7 | 8 | # Import statements 9 | import math 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | import sys 13 | 14 | 15 | 16 | def StarSolve(data_file,star = "primary",Period=None,Pguess=None, covariance = False,graphs = True, zeroEcc = 0, X = None, err = None,shift = False): 17 | """ 18 | 19 | Please first read the read me at: 20 | https://github.com/NickMilsonPhysics/BinaryStarSolver/blob/master/README.md 21 | 22 | Keyword Arguments: 23 | 24 | ** Note some arguments pertain to when parameters of the primary are 25 | being solved for, some pertain to when parameters of the secondary 26 | are being solved for, and some pertain to the case when both stars are 27 | being solved for at once. If an argument doesn't pertain to the situation 28 | this function is being used for, it may be ignored, as all the case 29 | specific arguments are None by default. ** 30 | 31 | Keyword Arguments mandatory regardless of situation: 32 | data_file - String holding the name of the tab separated txt file 33 | star - Can be "primary", "secondary", or "both". Is "primary" by default. 34 | star is a string for if the parameters of the primary or secondary 35 | are being solved for (or both) 36 | 37 | 38 | Keyword Arguments pertaining to primary star application: 39 | Period (optional) - Known value for period (if none provided, by default 40 | PrimarySolve makes its own guess). 41 | Pguess (optional) - If period is not known precisely, but still an 42 | estimate is available. 43 | covariance (optional) - If covariance = True, PrimarySolve will return the 44 | entire covariance matrix. 45 | graphs (optional) - If graphs = true, PrimarySolve creates 2 plots. First, a 46 | plot of the radial velocity data as a function of time 47 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 48 | where [γ, K, ω, e, T0] are the orbital parameters 49 | solved for by PrimarySolve(). Second, a plot of the 50 | radial velocity data as a function of phase (between 0 51 | and 1) of the star's orbit. Again, fit with 52 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 53 | function of phase. On both plots, the long-term average 54 | velocity, γ, is also shown. 55 | zeroEcc (optional) - If zeroEcc = True, eccentricity and arguement of 56 | periastron are kept at 0 57 | 58 | Keyword Arguments pertaining to both stars at once application: 59 | Period (optional) - Known value for period (if none provided, by default 60 | PrimarySolve makes its own guess). 61 | Pguess (optional) - If period is not known precisely, but still an 62 | estimate is available. 63 | covariance (optional) - If covariance = True, PrimarySolve will return the 64 | entire covariance matrix. 65 | graphs (optional) - If graphs = true, BothStars creates 4 plots. First, a 66 | plot of the radial velocity data as a function of time 67 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 68 | where [γ, K, ω, e, T0] are the orbital parameters 69 | solved for by BothSTars(). Second, a plot of the 70 | radial velocity data as a function of phase (between 0 71 | and 1) of the star's orbit. Again, fit with 72 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 73 | function of phase. On both plots, the long-term average 74 | velocity, γ, is also shown. Then these two plots are 75 | made but for the second star. (so K and omega will 76 | be different). 77 | zeroEcc (optional) - If zeroEcc = True, eccentricity and arguement of 78 | periastron are kept at 0 79 | 80 | Keyword Arguments pertaining to secondary star application: 81 | X - List of elements of the primary star, in the order [γ, K, w, e, T0, P]. 82 | (with w in degrees). K does not necessarily need to be known but a 83 | value must be input. 84 | err (optional) - List of errors on the elements in X (in the same order) 85 | shift (optional) - If shift = True, returns discrepancy between γ of primary 86 | graphs (optional) - If graphs = True, CompanionSolve creates 2 plots. First, a 87 | plot of the radial velocity data as a function of time 88 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 89 | where [γ, K, ω, e, T0] are the orbital parameters 90 | solved for by CompanionSolve(). Second, a plot of the 91 | radial velocity data as a function of phase (between 0 92 | and 1) of the star's orbit. Again, fit with 93 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 94 | function of phase. On both plots, the long-term average 95 | velocity, γ, is also shown. 96 | 97 | 98 | Returns: 99 | 100 | ** Note which variables are returned depends on the application of this 101 | function. ** 102 | 103 | Returns from the primary star (and both stars) application: 104 | x - List(s) of solved orbital parameters, the semi major axis and the 105 | mass function [γ, K, ω, e, T0, P, a, f(M)] 106 | err - List(s) of standard errors on the determination of the orbital elements, 107 | in the order [γ, K, ω, e, T0, P, a, f(M)] 108 | C - Estimated covariance matrix (or matrices) (if argument covariance = True) 109 | 110 | Returns from the secondary star application: 111 | x - List of solved elements of the companion star, in the order 112 | [γ, K, w, e, T0, P, asin(i), f(m)]. 113 | err (optional) - If errors associated with primary star are supplied, list 114 | of errors on the elements in x 115 | shift (optional) - Difference in average radial velocity of primary star, γ1, 116 | and of secondary star, γ2 (if argument shift = True). The 117 | radial velocity of the primary star is taken to be the 118 | 'correct' γ in x. Thus shift returned = γ2 - γ1. 119 | """ 120 | 121 | #%% SET UP 122 | 123 | star = star.lower() # making star not case sensitive 124 | 125 | # sorting data chronologically 126 | data = np.loadtxt(data_file) 127 | data_sorted = np.array(sorted(data,key=lambda l:l[0])) 128 | time = data_sorted[:,0] # RJD times 129 | V = data_sorted[:,1] # radial velocities 130 | 131 | N = len(time) # number of data points 132 | 133 | if star != "both": 134 | if X != None: 135 | Period = X[5] 136 | # weights 137 | if data.shape[1] >= 3: 138 | weight = data_sorted[:,2] 139 | else: 140 | weight = np.ones(N) 141 | if data.shape[1] >= 4: 142 | setNum = data_sorted[:,3] 143 | 144 | 145 | #%% Initial guess 146 | 147 | # estimates of period 148 | def PGuess(test = False): 149 | """ 150 | 151 | Keyword Arguments: 152 | test (false by default) - If true, PGuess is used as a check to see if 153 | finding a period is possible. If false, PGuess 154 | estimates a period or exits the program. 155 | 156 | When test = False, PGuess returns two estimates of the period, found 157 | using different methods. 158 | 159 | If less than one period is observed, will call sys.exit signaling that not 160 | enough data is provided to make an initial estimate of the period. In this 161 | case the user must provide a known period to StarSolve(). 162 | 163 | For both methods, begins by finding all pairs of points where data crosses 164 | the average radial velocity axis. 165 | 166 | The first method provides an estimate by finding the two points where the 167 | data crosses the average radial velocity axis, seperated by an integer 168 | number of periods, that have the smallest difference in radial velocity. 169 | 170 | The period is found from the times (RJD) that these points occur, divided 171 | by the number of periods apart they are. 172 | 173 | 174 | The second method provides an estimate by averaging the separation of 175 | points crossing the average radial velocity axis by one period, and the 176 | maxima and minima in between these points. 177 | """ 178 | 179 | # find all cross points 180 | Vavg = sum(V)/N 181 | if star == "both": 182 | Vavg /= 2 183 | low = None 184 | crossid = [] 185 | for i in range(1, N-1): 186 | if V[i+1] > Vavg and V[i-1] < Vavg or V[i+1] < Vavg and V[i-1] > Vavg: 187 | crossid.append(i) 188 | 189 | if N <=2: 190 | if test: 191 | return np.nan 192 | print("Not enough data to estimate period.") 193 | sys.exit() 194 | 195 | # corrects for double crossings 196 | l = len(crossid) 197 | for i in range(l-1): 198 | a = crossid[i] 199 | b = crossid[i+1] 200 | if a+1 != b: 201 | good = False 202 | for j in range(a, b): 203 | if abs(V[j] - Vavg) > (max(V) - Vavg) / 4: 204 | good = True 205 | break 206 | if not good: 207 | if abs(V[a] - Vavg) < abs(V[b] - Vavg): 208 | crossid[i+1] = a + 1 209 | else: 210 | crossid[i] = b - 1 211 | 212 | # if data covers less than one period 213 | if len(crossid) == 0 and test == False: 214 | print("Not enough data to estimate period. Please use Pguess or Period keywords") 215 | sys.exit() 216 | if crossid[0] + 1 != crossid[1]: 217 | if l < 4: 218 | if test: 219 | return np.nan 220 | print("Not enough data to estimate period. Please use Pguess or Period keywords") 221 | sys.exit() 222 | else: 223 | if l < 5: 224 | if test: 225 | return np.nan 226 | print("Not enough data to estimate period. Please use Pguess or Period keywords") 227 | sys.exit() 228 | 229 | # excludes last two sets of points from finding period 230 | if crossid[l-1] - 1 == crossid[l-2]: 231 | r = l - 4 232 | check = False 233 | else: 234 | r = l - 5 235 | check = True 236 | 237 | Psum = 0 # sum of periods for method 2 238 | n = 0 # number of periods measured for method 2 239 | 240 | # for method 1, finds difference in radial velocity using every pair of 241 | # points seperated by an integer number of periods along Vavg 242 | # simoultaneously adds time between all pairs of cross points seperated by 243 | # one period to Tsum for method 2 244 | first = True 245 | for i in range(r): 246 | # method 1: checks difference using every point 4n away 247 | # method 2: adds time to 4th point ahead to Psum 248 | Psum += time[crossid[i+4]] - time[crossid[i]] 249 | for j in range(4, l-i, 4): 250 | if first: 251 | low = abs(V[crossid[4]] - V[crossid[0]]) 252 | besta = time[crossid[0]] 253 | bestb = time[crossid[4]] 254 | p = 1 # number of periods between points 255 | first = False 256 | if abs(V[crossid[i]] - V[crossid[i+j]]) < low: 257 | low = abs(V[crossid[i]] - V[crossid[i+j]]) 258 | besta = time[crossid[i]] 259 | bestb = time[crossid[i+j]] 260 | p = j/4 261 | # if cross point is first in pair: 262 | # method 1: checks every point 5 + 4n away 263 | # method 2: adds time to 5th point ahead to Psum 264 | if crossid[i] + 1 == crossid[i+1]: 265 | for j in range(5, l-i, 4): 266 | if abs(V[crossid[i]] - V[crossid[i+j]]) < low: 267 | low = abs(V[crossid[i]] - V[crossid[i+j]]) 268 | besta = time[crossid[i]] 269 | bestb = time[crossid[i+j]] 270 | p = (j-1)/2 271 | Psum += time[crossid[i+5]] - time[crossid[i]] 272 | # if cross point is second in pair: 273 | # method 1: checks every point 3 + 4n away 274 | # method 2: adds time to 3rd point ahead to Psum 275 | else: 276 | for j in range(3, l-i, 4): 277 | if abs(V[crossid[i]] - V[crossid[i+j]]) < low: 278 | low = abs(V[crossid[i]] - V[crossid[i+j]]) 279 | besta = time[crossid[i]] 280 | bestb = time[crossid[i+j]] 281 | p = (j+1)/2 282 | Psum += time[crossid[i+3]] - time[crossid[i]] 283 | n += 2 284 | 285 | # if last point does not belong to pair (will not have been checked with 286 | # pair of points one period earlier): 287 | # method 1: checks radial velocity diffence between last point and pair 288 | # one period before 289 | # method 2: measures period using last point and both points from one 290 | # period before, adding to Psum 291 | if check: 292 | n += 2 293 | Psum += time[crossid[r+4]] - time[crossid[r]] 294 | Psum += time[crossid[r+4]] - time[crossid[r+1]] 295 | if low != None: 296 | if abs(V[crossid[r]] - V[crossid[r+4]]) < low: 297 | low = abs(V[crossid[r]] - V[crossid[r+4]]) 298 | besta = time[crossid[r]] 299 | bestb = time[crossid[r+4]] 300 | p = 1 301 | if abs(V[crossid[r+1]] - V[crossid[r+4]]) < low: 302 | besta = time[crossid[r+1]] 303 | bestb = time[crossid[r+4]] 304 | p = 1 305 | 306 | if low == None: 307 | P1 = 1000 308 | else: 309 | P1 = (bestb - besta)/p # period from first method 310 | 311 | # find maxima and minima for second method 312 | maxima = [] 313 | minima = [] 314 | # if less than four crossings, neither two maxima nor two minima exist, 315 | # so no information about period can be gathered, skip finding 316 | if check and l < 6: 317 | P2 = Psum/n 318 | elif not check and l < 7: 319 | P2 = Psum/n 320 | else: 321 | for i in range(l-1): 322 | if crossid[i] + 1 != crossid[i+1]: 323 | if V[crossid[i]] > Vavg: 324 | maximum = V[crossid[i]] 325 | for j in range(crossid[i], crossid[i+1]): 326 | if V[j] > maximum: 327 | maximum = V[j] 328 | maxima.append(time[j]) 329 | else: 330 | minimum = V[crossid[i]] 331 | for j in range(crossid[i], crossid[i+1]): 332 | if V[j] < minimum: 333 | minimum = V[j] 334 | minima.append(time[j]) 335 | else: 336 | pass 337 | # add time between maxima/minima to Psum 338 | for i in range(len(maxima)-1): 339 | Psum += maxima[i+1] - maxima[i] 340 | n += 1 341 | for i in range(len(minima)-1): 342 | Psum += minima[i+1] - minima[i] 343 | n += 1 344 | 345 | P2 = Psum/n # period from second method 346 | 347 | return [P1, P2] 348 | 349 | 350 | # Mean anomaly at a given time 351 | def mean(t, T0, P): 352 | M = 2*np.pi*(t-T0) / P 353 | while M < 0: 354 | M = M + 2*np.pi 355 | while M > 2*np.pi: 356 | M = M - 2*np.pi 357 | return M 358 | 359 | # Kepler's Equation rearranged so RHS = 0 360 | def Kep(t, T0, E, e, P): 361 | return mean(t, T0, P) - E + e*np.sin(E) 362 | 363 | # Derivative of kep wrt E 364 | def dKep(E, e): 365 | return -1 + e*np.cos(E) 366 | 367 | def ecc(t, T0, e, P): 368 | """ 369 | Uses Newton's Method to find the eccentric anomaly from Kepler equation 370 | If t is a list, will return array of corresponding E's 371 | If t is a single value, will return E at time t 372 | """ 373 | # if time is a scalar: 374 | if np.isscalar(t): 375 | E = mean(t, T0, P) + e*np.sin(mean(t, T0, P)) # starting guess 376 | while abs(Kep(t, T0, E, e, P)) > 0.000005: 377 | E = E - Kep(t, T0, E, e, P)/dKep(E, e) 378 | while E > 2*np.pi: 379 | E = E - 2*np.pi 380 | while E < 0: 381 | E = E + 2*np.pi 382 | 383 | return E 384 | # if time is a list: 385 | else: 386 | EList = [] 387 | for i in range(len(t)): 388 | # starting guess (Green p. 145); 389 | E = mean(t[i], T0, P) + e*np.sin(mean(t[i], T0, P)) 390 | # if not within error, improves guess until within error: 391 | while abs(Kep(t[i], T0, E, e, P)) > 0.000005: 392 | E = E - Kep(t[i], T0, E, e, P)/dKep(E, e) 393 | # simplifies angle to be between 0 and 2pi 394 | while E > 2*np.pi or E <= 0: 395 | if E > 2*np.pi: 396 | E = E - 2*np.pi 397 | elif E <= 0: 398 | E = E + 2 * np.pi 399 | EList.append(E) 400 | 401 | return np.array(EList) 402 | 403 | 404 | def true(t, T0, e, P, ec = False): 405 | """ 406 | From E, finds true anomaly using tan(v/2) = sqrt((1+e)/(1-e)) * tan(E/2) 407 | If t is a list, will return array of corresponding v's 408 | If t is a single value, will return v at time t 409 | """ 410 | # if t is a scalar: 411 | if np.isscalar(t): 412 | E = ecc(t, T0, e, P) 413 | v = 2*np.arctan(np.sqrt((1+e)/(1-e))*np.tan(E/2)) 414 | while v > 2*np.pi: 415 | v = v - 2*np.pi 416 | while v < 0: 417 | v = v + 2*np.pi 418 | if not ec: 419 | return v 420 | else: 421 | return v, E 422 | # if t is a list: 423 | else: 424 | EList = ecc(t, T0, e, P) 425 | vList = [] 426 | for i in range(len(EList)): 427 | v = 2*np.arctan(np.sqrt((1+e)/(1-e)) * np.tan(EList[i]/2)) 428 | vList.append(v) 429 | if not ec: 430 | return np.array(vList) 431 | else: 432 | return np.array(vList), np.array(EList) 433 | 434 | def epoch(w, e, P): 435 | """ 436 | given w, e, P can return an estimate for T0 437 | """ 438 | vmax = -w 439 | Emax = 2 * np.arctan((((1+e)/(1-e))**(-0.5)) * np.tan(vmax/2)) 440 | T0 = time[V.argmax()] - (P/(2*np.pi)) * (Emax - e * np.sin(Emax)) 441 | return T0 442 | 443 | def x0(period = None, star = "primary", gam = 0, zeroEcc = False): 444 | """ 445 | Makes initial approximation for the orbital parameters γ, K, ω, e, T0, & P. 446 | 447 | X0 can find an initial estimate for period, but a user supplied estimate 448 | can be inputted using the keyword argument 'period' 449 | 450 | Note that, in the following two scenerios, x0() may fail to make a good 451 | guess: 452 | i) If the RV data does not cover a full period 453 | ii) If the RV data is very non-uniformly distributed. E.g. there are 454 | significantly more points at the peak than at the trough. 455 | """ 456 | if period == None: 457 | P1, P2 = PGuess() 458 | 459 | 460 | for j in range(2): # peforms twice as there are two estimates of period 461 | if period == None: 462 | if j == 0: 463 | P = P1 464 | elif j == 1: 465 | P = P2 466 | else: 467 | P = period 468 | j = 2 469 | 470 | K = (max(V) - min(V))/2 471 | 472 | if star == "both": 473 | V0 = gam 474 | else: 475 | V0 = sum(V) / len(V) 476 | 477 | lowSS = None # holds lowest sum of squared deviations 478 | bestX = [None, None, None, None, None, None] # holds best estimate 479 | 480 | if zeroEcc: 481 | 482 | e,w = 0,0 483 | T0 = epoch(w, e, P) 484 | bestX = [V0, K, w, e, T0, P] 485 | 486 | 487 | else: 488 | for i in np.arange(0.01, 0.95, 0.01): # iterates through eccentricities 489 | e = i 490 | temp = (K*e)**-1 * ((max(V) + min(V))* 0.5 - V0) 491 | if abs(temp) <= 1 : 492 | w1 = np.arccos(temp) 493 | x1 = [V0, K, w1, e, epoch(w1,e,P), P] 494 | w2 = -w1 % (2 * np.pi) # Non principle value 495 | x2 = [V0, K, w2, e, epoch(w2,e,P), P] 496 | 497 | if SumSquared(x1, None) < SumSquared(x2, None): 498 | w = w1 499 | else: 500 | w = w2 501 | 502 | T0 = epoch(w, e, P) 503 | x = [V0, K, w, e, T0, P] 504 | if lowSS == None: 505 | lowSS = SumSquared(x, None) 506 | bestX = x.copy() 507 | 508 | elif SumSquared(x, None) < lowSS: 509 | lowSS = SumSquared(x, None) 510 | bestX = x.copy() 511 | if j == 0: 512 | xa = bestX.copy() 513 | elif j == 1: 514 | xb = bestX.copy() 515 | else: 516 | return bestX 517 | 518 | if SumSquared(xa, None) < SumSquared(xb, None): 519 | bestX = xa 520 | elif SumSquared(xb, None) < SumSquared(xa, None): 521 | bestX = xb 522 | 523 | return bestX 524 | 525 | #%% FIT OF DATA 526 | 527 | # rounding function. Makes final output neater. 528 | def sigFig(x, figs): 529 | return round(x, -int(math.floor(math.log10(abs(x))) - (figs - 1))) 530 | 531 | def lsq_bisect(x,y,wx,wy): 532 | """ 533 | Parameters 534 | ---------- 535 | x : x points 536 | y : y points 537 | wx : x weights 538 | wy : y weights 539 | 540 | Returns 541 | ------- 542 | cov: coefficient vector of the bisector fit line 543 | """ 544 | 545 | 546 | 547 | cof1= np.polyfit( x, y, 1, w = wy) 548 | cof2= np.polyfit( y, x, 1, w = wx) 549 | 550 | b1= cof1[0] #slope of normal fit: y= a1 + b1*x 551 | b2= 1.0/cof2[0] #slope of inverted fit: x= a2 + b2*y --> y= -a2/b2 + x/b2 552 | 553 | # now compute the slope of the bisector of these two OLS lines 554 | 555 | t1= np.sqrt( 1.0 + b1**2 ) 556 | t2= np.sqrt( 1.0 + b2**2 ) 557 | 558 | b = ( b2*t1 + b1*t2 )/( t1 + t2 ) #slope of bisector line 559 | 560 | # the regression line always passes through the centroid (mean(x),mean(y)) and 561 | # so we use this to find the constant term: a = mean(y) - bs*mean(x) 562 | 563 | xm= np.mean(x) 564 | ym= np.mean(y) 565 | a = ym - b*xm 566 | cof= np.array([b, a]) #define the coefficient vector of the bisector fit line 567 | 568 | return cof 569 | 570 | def derivative(f, params, P, ang, der, h=0.000001, zeroEcc = False, starNum = None): 571 | """ 572 | Numerical derivative using central differences 573 | 574 | Keyword Arguments: 575 | f - The function to differentiate. 576 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 577 | derivative is to be calculated. 578 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 579 | are passed in as parameters. 580 | ang - list of true anomalies given parameters [γ, K, w, e, T0, P] 581 | der - which variable to take derivative w.r.t. The options are "V0", "K", 582 | "w", "e", and "T0"/ 583 | h - small change variable for taking difference 584 | starNum - If its for a single star during the star="both" case 585 | """ 586 | 587 | 588 | if len(params) == 6: 589 | V0, K, w, e, T0, P = params 590 | elif len(params) == 5: 591 | V0, K, w, e, T0 = params 592 | elif len(params) == 4: 593 | V0, K, T0,P = params 594 | e,w = 0,0 595 | else: 596 | V0, K, T0 = params 597 | e,w = 0,0 598 | 599 | if der == "V0": 600 | paramsAb = [V0 + h, K, w, e, T0] 601 | paramsBe = [V0 - h, K, w, e, T0] 602 | angAb = ang 603 | angBe = ang 604 | elif der == "K": 605 | paramsAb = [V0, K + h, w, e, T0] 606 | paramsBe = [V0, K - h, w, e, T0] 607 | angAb = ang 608 | angBe = ang 609 | elif der == "w": 610 | paramsAb = [V0, K, w + h, e, T0] 611 | paramsBe = [V0, K, w - h, e, T0] 612 | angAb = ang 613 | angBe = ang 614 | elif der == "e": 615 | paramsAb = [V0, K, w, e + h, T0] 616 | paramsBe = [V0, K, w, e - h, T0] 617 | angAb = true(time, T0, e + h, P, ec = True) 618 | angBe = true(time, T0, e - h, P, ec = True) 619 | elif der == "T0": 620 | paramsAb = [V0, K, w, e, T0 + h] 621 | paramsBe = [V0, K, w, e, T0 - h] 622 | angAb = true(time, T0 + h, e, P, ec = True) 623 | angBe = true(time, T0 - h, e, P, ec = True) 624 | else: 625 | params = [V0, K, w, e, T0] 626 | angAb = true(time, T0, e, P + h, ec = True) 627 | angBe = true(time, T0, e, P - h, ec = True) 628 | return (f(params, angAb, P+h,starNum) - f(params,angBe, P+h,starNum))/(2*h) 629 | 630 | return (f(paramsAb, angAb, P,starNum) - f(paramsBe, angBe, P, starNum))/(2*h) 631 | 632 | # Sum of squared deviations between the fit and the actual RV data 633 | def SumSquared(params, P, starNum = None): 634 | if len(params) == 6: 635 | V0, K, w, e, T0, P = params 636 | elif len(params) == 5: 637 | V0, K, w, e, T0 = params 638 | elif len(params) == 4: 639 | V0, K, T0,P = params 640 | e,w = 0,0 641 | else: 642 | V0, K, T0 = params 643 | e,w = 0,0 644 | 645 | if star == "both": 646 | if starNum == 1: 647 | t = time1 648 | epsilon = ((V0 + ((np.cos(true(t, T0, e, P) + w) + e*np.cos(w))*K)) - V1)**2 649 | SS = sum(epsilon*weight1) 650 | 651 | elif starNum == 2: 652 | t = time2 653 | epsilon = ((V0 + ((np.cos(true(t, T0, e, P) + w) + e*np.cos(w))*K)) - V2)**2 654 | SS = sum(epsilon*weight2) 655 | else: 656 | t = timecomb 657 | epsilon = ((V0 + ((np.cos(true(t, T0, e, P) + w) + e*np.cos(w))*K)) - Vcomb)**2 658 | SS = sum(epsilon*weightcomb) 659 | else: 660 | 661 | epsilon = ((V0 + ((np.cos(true(time, T0, e, P) + w) + e*np.cos(w))*K)) - V)**2 662 | SS = sum(epsilon* weight) 663 | 664 | return SS 665 | 666 | def partV0(params, ang, P = None, starNum = None): 667 | """ 668 | Derivative of residual wrt γ. 669 | 670 | Keyword Arguments: 671 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 672 | derivative is to be calculated. 673 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 674 | are passed in as parameters. 675 | ang - list of true anomalies given parameters [γ, K, w, e, T0, P] 676 | """ 677 | 678 | if len(params) == 6: 679 | V0, K, w, e, T0, P = params 680 | elif len(params) == 5: 681 | V0, K, w, e, T0 = params 682 | if P == None: 683 | print('P must be supplied') 684 | sys.exit() 685 | elif len(params) == 4: 686 | V0, K, T0,P = params 687 | e,w = 0,0 688 | else: 689 | V0, K, T0 = params 690 | e,w = 0,0 691 | v = ang[0] 692 | 693 | if starNum == 1: 694 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V1) *weight1 695 | 696 | elif starNum == 2: 697 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V2) *weight2 698 | 699 | elif star == "both": 700 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - Vcomb) *weightcomb 701 | 702 | else: 703 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V) *weight 704 | 705 | 706 | return sum(2*temp) 707 | 708 | def partK(params, ang, P = None, starNum = None): 709 | """ 710 | Derivative of residual wrt K. 711 | 712 | Keyword Arguments: 713 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 714 | derivative is to be calculated. 715 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 716 | are passed in as parameters. 717 | ang - list of true anomalies given parameters [γ, K, w, e, T0, P] 718 | """ 719 | 720 | if len(params) == 6: 721 | V0, K, w, e, T0, P = params 722 | elif len(params) == 5: 723 | V0, K, w, e, T0 = params 724 | if P == None: 725 | print('P must be supplied') 726 | sys.exit() 727 | elif len(params) == 4: 728 | V0, K, T0,P = params 729 | e,w = 0,0 730 | else: 731 | V0, K, T0 = params 732 | e,w = 0,0 733 | v = ang[0] 734 | 735 | if starNum == 1: 736 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V1) *weight1 737 | 738 | elif starNum == 2: 739 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V2) *weight2 740 | 741 | elif star == "both": 742 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - Vcomb) *weightcomb 743 | 744 | else: 745 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w)))*K) - V) *weight 746 | 747 | temp *= (np.cos(v+w) + e*np.cos(w)) 748 | return sum(2*temp) 749 | 750 | def partw(params, ang, P = None, starNum = None): 751 | """ 752 | Derivative of residual wrt w. 753 | 754 | Keyword Arguments: 755 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 756 | derivative is to be calculated. 757 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 758 | are passed in as parameters. 759 | ang - list of true anomalies given parameters [γ, K, w, e, T0, P] 760 | """ 761 | 762 | if len(params) == 6: 763 | V0, K, w, e, T0, P = params 764 | elif len(params) == 5: 765 | V0, K, w, e, T0 = params 766 | if P == None: 767 | print('P must be supplied') 768 | sys.exit() 769 | elif len(params) == 4: 770 | V0, K, T0,P = params 771 | e,w = 0,0 772 | else: 773 | V0, K, T0 = params 774 | e,w = 0,0 775 | v = ang[0] 776 | 777 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w))*K)) - V) *weight 778 | temp *= (np.sin(v+w)+e*np.sin(w)) 779 | return sum((-2*K*temp)) 780 | 781 | def parte(params, ang, P = None, starNum = None): 782 | """ 783 | Derivative of residual wrt e. 784 | 785 | Keyword Arguments: 786 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 787 | derivative is to be calculated. 788 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 789 | are passed in as parameters. 790 | ang - list of true anomalies and eccentric anomalies given parameters 791 | [γ, K, w, e, T0, P] 792 | """ 793 | 794 | if len(params) == 6: 795 | V0, K, w, e, T0, P = params 796 | elif len(params) == 5: 797 | V0, K, w, e, T0 = params 798 | if P == None: 799 | print('P must be supplied') 800 | sys.exit() 801 | elif len(params) == 4: 802 | V0, K, T0,P = params 803 | e,w = 0,0 804 | else: 805 | V0, K, T0 = params 806 | e,w = 0,0 807 | v, E = ang 808 | 809 | 810 | dEde = np.sin(E)/(1-e*np.cos(E)) 811 | dvde = np.sqrt((e+1)/(1-e)) * (0.5) * (np.cos(E/2)**-2) 812 | dvde *= dEde 813 | dvde += (1-e)**-2 * np.sqrt((1-e)/(1+e)) * np.tan(E/2) 814 | dvde *= 2 * np.cos(v/2)**2 815 | temp = K*(-np.sin(v + w)*dvde + np.cos(w)) 816 | temp *= (V0 + (np.cos(v + w) + e*np.cos(w))*K - V) *weight 817 | return sum(2*temp) 818 | 819 | def partT0(params, ang, P = None, starNum = None): 820 | """ 821 | Derivative of residual wrt T0. 822 | 823 | Keyword Arguments: 824 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 825 | derivative is to be calculated. 826 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 827 | are passed in as parameters. 828 | ang - list of true anomalies and eccentric anomalies given parameters 829 | [γ, K, w, e, T0, P] 830 | """ 831 | 832 | if len(params) == 6: 833 | V0, K, w, e, T0, P = params 834 | elif len(params) == 5: 835 | V0, K, w, e, T0 = params 836 | if P == None: 837 | print('P must be supplied') 838 | sys.exit() 839 | elif len(params) == 4: 840 | V0, K, T0,P = params 841 | e,w = 0,0 842 | else: 843 | V0, K, T0 = params 844 | e,w = 0,0 845 | v, E = ang 846 | 847 | temp = ((V0 + ((np.cos(v + w) + e*np.cos(w))*K)) - V)*weight 848 | temp *= K * np.sqrt((1+e)/(1-e)) * np.sin(v + w) 849 | temp *= (1+ np.cos(v)) / (1 + np.cos(E)) 850 | temp *= 2*np.pi / (P*(1-e*np.cos(E))) 851 | return sum(2*temp) 852 | 853 | def partP(params, ang, P = None, starNum = None): 854 | """ 855 | Derivative of residual wrt P. 856 | 857 | Keyword Arguments: 858 | params - value of parameters [γ, K, w, e, T0] or [γ, K, w, e, T0, P] where 859 | derivative is to be calculated. 860 | P - period of orbit (if known). Must be supplied if only [γ, K, w, e, T0] 861 | are passed in as parameters. 862 | ang - list of true anomalies and eccentric anomalies given parameters 863 | [γ, K, w, e, T0, P] 864 | """ 865 | 866 | if len(params) == 6: 867 | V0, K, w, e, T0, P = params 868 | elif len(params) == 5: 869 | V0, K, w, e, T0 = params 870 | if P == None: 871 | print('P must be supplied') 872 | sys.exit() 873 | elif len(params) == 4: 874 | V0, K, T0,P = params 875 | e,w = 0,0 876 | else: 877 | V0, K, T0 = params 878 | e,w = 0,0 879 | v, E = ang 880 | 881 | 882 | dEdP = ((-2*np.pi)/(P**2)) * (time - T0) * (1-e*np.cos(E))**(-1) 883 | dvdP = np.sqrt((1+e)/(1-e)) * ((1+ np.cos(v)) / (1 + np.cos(E))) * dEdP 884 | temp = -K * np.sin(v + w) * dvdP 885 | temp *= ((V0 + ((np.cos(v + w) + e*np.cos(w))*K)) - V)*weight 886 | return sum(2*temp) 887 | 888 | def PrimarySolve(period = None, Pguess = None,covariance = False, graphs = True,correction = False): 889 | """ 890 | Solves for orbital parameters from radial velocity data 891 | 892 | Keyword Arguments: 893 | period (optional) - Known value for period (if none provided, by default 894 | PrimarySolve makes its own guess) 895 | covariance (optional) - If covariance = True, PrimarySolve will return the 896 | entire covariance matrix. 897 | graphs (optional) - If graphs = true, PrimarySolve creates 2 plots. First, a 898 | plot of the radial velocity data as a function of time 899 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 900 | where [γ, K, ω, e, T0] are the orbital parameters 901 | solved for by PrimarySolve(). Second, a plot of the 902 | radial velocity data as a function of phase (between 0 903 | and 1) of the star's orbit. Again, fit with 904 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 905 | function of phase. On both plots, the long-term average 906 | velocity, γ, is also shown. 907 | 908 | Returns: 909 | x - List of solved orbital parameters, the semi major axis and the 910 | mass function [γ, K, ω, e, T0, P, a, f(M)] 911 | err - List of standard errors on the determination of the orbital elements, 912 | in the order [γ, K, ω, e, T0, P, a, f(M)] 913 | C - Estimated covariance matrix (if argument covariance = True) 914 | 915 | """ 916 | if Pguess != None: 917 | period = Pguess 918 | if not correction: 919 | print("Finding initial guesses...") 920 | x = x0(period, zeroEcc = zeroEcc) # initial guess 921 | if x == None: 922 | print('User must provide an estimate for P.') 923 | sys.exit() 924 | 925 | if Pguess != None: 926 | period = None 927 | 928 | if period != None: # if period known and provided by user 929 | P = period 930 | x.pop() 931 | V0, K, w, e, T0 = x 932 | var = 5 933 | else: 934 | V0, K, w, e, T0, P = x 935 | var = 6 936 | 937 | if zeroEcc: 938 | if period != None: 939 | x = [V0, K, T0] 940 | var = 3 941 | else: 942 | x = [V0, K,T0,P] 943 | var = 4 944 | l = 3 945 | if not correction: 946 | print("Minimizing...") 947 | 948 | while True: # perfoming minimization 949 | 950 | if e < 0: 951 | e = 0 # so some square roots dont cause us trouble 952 | x[3] = e 953 | if e >=1: 954 | e = 0.99 955 | x[3]= e 956 | 957 | xLast = x 958 | ang = true(time, T0, e, P, ec = True) 959 | 960 | pVK = derivative(partK, x, P, ang, 'V0') 961 | pVT0 = derivative(partT0, x, P, ang, 'V0') 962 | pKT0 = derivative(partT0, x, P, ang, 'K') 963 | 964 | 965 | 966 | if period == None: 967 | 968 | # Building Hessian matrix 969 | 970 | 971 | pVP = derivative(partV0, x, P, ang, 'P') 972 | pKP = derivative(partK, x, P, ang, 'P') 973 | pT0P = derivative(partT0, x, P, ang, 'P') 974 | 975 | if not zeroEcc: 976 | peP = derivative(parte, x, P, ang, 'P') 977 | pVe = derivative(parte, x, P, ang, 'V0') 978 | pKe = derivative(parte, x, P, ang, 'K') 979 | pwe = derivative(parte, x, P, ang, 'w') 980 | peT0 = derivative(partT0, x, P, ang, 'e') 981 | pVw = derivative(partw, x, P, ang, 'V0') 982 | pKw = derivative(partw, x, P, ang, 'K') 983 | pwT0 = derivative(partT0, x, P, ang, 'w') 984 | pwP = derivative(partw, x, P, ang, 'P') 985 | 986 | H1 = [derivative(partV0, x, P, ang, 'V0') * (1+l), pVK, pVw, pVe, pVT0, pVP] 987 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKw, pKe, pKT0, pKP] 988 | H3 = [pVw, pKw, derivative(partw, x, P, ang, 'w') * (1+l), pwe, pwT0, pwP] 989 | H4 = [pVe, pKe, pwe, derivative(parte, x, P, ang, 'e') * (l+1), peT0, peP] 990 | H5 = [pVT0, pKT0, pwT0, peT0, derivative(partT0, x, P, ang, 'T0') * (1+l), pT0P] 991 | H6 = [pVP, pKP, pwP, peP, pT0P, derivative(partP, x, P, ang, 'P') * (1+l)] 992 | a = 0.5 * np.array([H1, H2, H3, H4, H5, H6]) 993 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), partw(x, ang, P), 994 | parte(x, ang, P), partT0(x, ang, P), partP(x, ang, P)]) 995 | 996 | else: 997 | H1 = [derivative(partV0, x, P, ang, 'V0') * (1+l), pVK, pVT0, pVP] 998 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKT0, pKP] 999 | H3 = [pVT0, pKT0, derivative(partT0, x, P, ang, 'T0') * (1+l), pT0P] 1000 | H4 = [pVP, pKP, pT0P, derivative(partP, x, P, ang, 'P') * (1+l)] 1001 | a = 0.5 * np.array([H1, H2, H3, H4]) 1002 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), 1003 | partT0(x, ang, P), partP(x, ang, P)]) 1004 | 1005 | else: 1006 | 1007 | if not zeroEcc: 1008 | 1009 | pVe = derivative(parte, x, P, ang, 'V0') 1010 | pKe = derivative(parte, x, P, ang, 'K') 1011 | pwe = derivative(parte, x, P, ang, 'w') 1012 | peT0 = derivative(partT0, x, P, ang, 'e') 1013 | pVw = derivative(partw, x, P, ang, 'V0') 1014 | pKw = derivative(partw, x, P, ang, 'K') 1015 | pwT0 = derivative(partT0, x, P, ang, 'w') 1016 | 1017 | H1 = [derivative(partV0, x, P, ang, "V0") * (1+l), pVK, pVw, pVe, pVT0] 1018 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKw, pKe, pKT0] 1019 | H3 = [pVw, pKw, derivative(partw, x, P, ang, 'w') * (1+l), pwe, pwT0] 1020 | H4 = [pVe, pKe, pwe, derivative(parte, x, P, ang, 'e') * (l+1), peT0] 1021 | H5 = [pVT0, pKT0, pwT0, peT0, derivative(partT0, x, P, ang, 'T0') * (1+l)] 1022 | a = 0.5 * np.array([H1,H2,H3,H4,H5]) 1023 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), partw(x, ang, P), 1024 | parte(x, ang, P), partT0(x, ang, P)]) 1025 | 1026 | else: 1027 | H1 = [derivative(partV0, x, P, ang, "V0") * (1+l), pVK, pVT0] 1028 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKT0] 1029 | H3 = [pVT0, pKT0, derivative(partT0, x, P, ang, 'T0') * (1+l)] 1030 | a = 0.5 * np.array([H1,H2,H3]) 1031 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), 1032 | partT0(x, ang, P)]) 1033 | x = xLast + np.linalg.solve(a, b) # Next iterations parameter values 1034 | 1035 | if period == None: 1036 | if zeroEcc: 1037 | V0, K, T0, P = x 1038 | else: 1039 | V0, K, w, e, T0, P = x 1040 | else: 1041 | if zeroEcc: 1042 | V0, K, T0 = x 1043 | else: 1044 | V0, K, w, e, T0 = x 1045 | 1046 | if SumSquared(x, P) < SumSquared(xLast, P): # if params got better 1047 | if abs(SumSquared(xLast, P) - SumSquared(x, P)) < 0.01 or abs(1-(SumSquared(x, P)/SumSquared(xLast, P))) < 1e-3: 1048 | break # convergence acheived! (probably) 1049 | else: 1050 | l /= 9 # lowering damping parameter 1051 | l = max(l, 10e-7) 1052 | else: # if params got worse 1053 | l *= 11 # raising damping parameter 1054 | l = min(l, 10e7) 1055 | 1056 | if not zeroEcc: 1057 | if x[2] < -np.pi or x[2] >= np.pi: 1058 | while x[2] < -np.pi: 1059 | x[2] = x[2] + 2*np.pi 1060 | while x[2] >= np.pi: 1061 | x[2] = x[2] - 2 * np.pi 1062 | w = x[2] 1063 | 1064 | if period == None: 1065 | if zeroEcc: 1066 | P = x[3] 1067 | else: 1068 | P = x[5] 1069 | x = [V0,K,w,e,T0,P] 1070 | 1071 | # other values to be returned 1072 | nsec = 2*np.pi / (24*3600*P) 1073 | a1 = (x[1]*np.sqrt(1-x[3]**2) / nsec) * 10e3 / 10e9 1074 | fm = 3.985e-20 * (a1 * 10e9 / 10e3)**3 / P**2 1075 | x.append(a1) 1076 | x.append(fm) 1077 | 1078 | 1079 | # if period == None: 1080 | # x = [x[0], x[1], x[2], x[3], x[4], x[5], a1, fm] 1081 | # else: 1082 | # x = [x[0], x[1], x[2], x[3], x[4], P, a1, fm] 1083 | 1084 | C = np.linalg.inv(a) # covariance matrix 1085 | err = [] 1086 | 1087 | for q in range(var): 1088 | err.append(np.sqrt(C[q][q])) 1089 | if period != None: 1090 | err.append(0) 1091 | if zeroEcc: 1092 | err.insert(2,0) 1093 | err.insert(3,0) 1094 | 1095 | # Finding uncertainty in a1 1096 | dade = (-x[1] * x[3] * x[5] * (3600*24)) / (2 * np.pi * np.sqrt(1-x[3]**2)) 1097 | dadT = (x[1] * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1098 | dadK = (x[5] * (3600*24) * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1099 | aErr = np.sqrt((dadT * err[5] * (3600*24))**2 + (dadK * err[1])**2 + (dade * err[3])**2) 1100 | aErr *= (10e3 / 10e9) 1101 | 1102 | # Finding uncertainty in fm 1103 | dfda = 3 * (x[6] * (10e9 / 10e3) )**2 * (3.985e-20) * (x[5])**-2 1104 | dfdT = -2 * (x[6] * (10e9 / 10e3) )**3 * (3.985e-20) * (x[5])**-3 1105 | fErr = np.sqrt((dfda * aErr * (10e9 / 10e3) )**2 + (dfdT * err[5])**2) 1106 | 1107 | err.append(aErr) 1108 | err.append(fErr) 1109 | 1110 | # create plots if expected 1111 | if graphs: 1112 | 1113 | # RV vs time data 1114 | if N < 500: 1115 | M = 500 1116 | else: 1117 | M = N 1118 | times = np.linspace(min(time), max(time),M) # times for fit 1119 | v = true(times, x[4], x[3], P) # array of true anomalies at every time for fit 1120 | fit = x[0] + x[1]*(np.cos(v + x[2])+ x[3]*np.cos(x[2])) 1121 | 1122 | # RV vs phase data 1123 | phase = ((time - x[4]) / P) % 1 # phase for every RV data point 1124 | # phases = np.linspace(min(phase), max(phase), N) 1125 | phases = ((times - x[4]) / P) % 1 # phase for every point on fit 1126 | # rearranging the data to fit as a function of phase 1127 | phaseV = [] 1128 | phasesfit = [] 1129 | for i in range(len(phases)): 1130 | if i < len(V): 1131 | phaseV.append([phase[i], V[i]]) 1132 | phasesfit.append([phases[i], fit[i]]) 1133 | phaseV = np.array(sorted(phaseV,key=lambda l:l[0])) 1134 | phasesfit = np.array(sorted(phasesfit,key=lambda l:l[0])) 1135 | # print(min(phases), min(phase)) 1136 | 1137 | # average velocity, γ 1138 | gam = np.zeros(100) + x[0] # average velocity 1139 | timeLin = np.linspace(min(times), max(times),100) 1140 | phaseLin = np.linspace(0,1,100) 1141 | 1142 | # time plot 1143 | fig, ax = plt.subplots() 1144 | ax.scatter(time, V, s = 18) 1145 | ax.plot(times, fit, color = "orange") 1146 | ax.set_xlabel("RJD") 1147 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1148 | ax.plot(timeLin, gam, ls = "--", color = "black") 1149 | ax.minorticks_on() 1150 | ax.tick_params(right=True, top=True) 1151 | ax.tick_params(right=True, top=True, which="minor") 1152 | 1153 | # phase plot 1154 | fig, ax = plt.subplots() 1155 | ax.scatter(phaseV[:,0], phaseV[:,1],s = 18) 1156 | ax.plot(phasesfit[:,0], phasesfit[:,1], color = "orange") 1157 | ax.set_xlabel("Phase") 1158 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1159 | ax.plot(phaseLin, gam, ls = "--", color = "black") 1160 | ax.minorticks_on() 1161 | ax.tick_params(right=True, top=True) 1162 | ax.tick_params(right=True, top=True, which="minor") 1163 | 1164 | # rounding 1165 | V0 = sigFig(x[0], 5) 1166 | K = sigFig(x[1], 5) 1167 | if not zeroEcc: 1168 | w = sigFig((x[2] * 180/np.pi)%360, 6) 1169 | e = sigFig(x[3], 5) 1170 | T0 = sigFig(x[4], 6) 1171 | a1 = sigFig(x[6], 5) 1172 | fm = sigFig(x[7], 5) 1173 | 1174 | V0err = sigFig(err[0], 5) 1175 | Kerr = sigFig(err[1], 5) 1176 | werr = err[2] 1177 | eerr = err[3] 1178 | if not zeroEcc: 1179 | werr = sigFig((err[2] * 180/np.pi)%360, 6) 1180 | eerr = sigFig(err[3], 5) 1181 | T0err = sigFig(err[4], 6) 1182 | a1err = sigFig(err[6], 5) 1183 | fmerr = sigFig(err[7], 5) 1184 | 1185 | if period == None: 1186 | P = sigFig(x[5], 6) 1187 | Perr = sigFig(err[5], 6) 1188 | else: 1189 | P = x[5] 1190 | Perr = err[5] 1191 | 1192 | x = [V0, K, w, e, T0, P, a1, fm] 1193 | err = [V0err, Kerr, werr, eerr, T0err, Perr, a1err, fmerr] 1194 | 1195 | # return expected values 1196 | if covariance: 1197 | for i in range(len(C)): 1198 | for j in range(len(C)): 1199 | C[i][j] = sigFig(C[i][j], 5) 1200 | return x, err, C 1201 | else: 1202 | return x, err 1203 | 1204 | 1205 | def CompanionSolve(X, err = [None, None, None, None, None, None], shift = False, graphs = True): 1206 | """ 1207 | Solves orbital parameters for a companion star 1208 | 1209 | Keyword Arguments: 1210 | X - List of elements of the primary star, in the order [γ, K, w, e, T0, P]. 1211 | (with w in degrees). K does not necessarily need to be known but a 1212 | value must be input. 1213 | err (optional) - List of errors on the elements in X (in the same order) 1214 | shift (optional) - If shift = True, returns discrepancy between γ of primary 1215 | star and of companion star 1216 | graphs (optional) - If graphs = True, companion creates 2 plots. First, a 1217 | plot of the radial velocity data as a function of time 1218 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 1219 | where [γ, K, ω, e, T0] are the orbital parameters 1220 | solved for by companion(). Second, a plot of the 1221 | radial velocity data as a function of phase (between 0 1222 | and 1) of the star's orbit. Again, fit with 1223 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 1224 | function of phase. On both plots, the long-term average 1225 | velocity, γ, is also shown. 1226 | 1227 | Returns: 1228 | x - List of solved elements of the companion star, in the order 1229 | [γ, K, w, e, T0, P, asin(i), f(m)]. 1230 | err (optional) - If errors associated with primary star are supplied, list 1231 | of errors on the elements in x 1232 | shift (optional) - Difference in average radial velocity of primary star, γ1, 1233 | and of secondary star, γ2 (if argument shift = True). The 1234 | radial velocity of the primary star is taken to be the 1235 | 'correct' γ in x. Thus shift returned = γ2 - γ1. 1236 | """ 1237 | 1238 | V0, K, wdeg, e, T0, P = X 1239 | wdeg = (wdeg + 180)%360 1240 | w = wdeg*np.pi/180 1241 | if not isinstance(err,list): 1242 | print(err) 1243 | err = [None] 1244 | 1245 | K = (max(V) - min(V))/2 1246 | 1247 | params = V0, K, w, e, T0, P 1248 | ang = true(time, T0, e, P, ec = True) 1249 | 1250 | print("Minimizing...") 1251 | while True: 1252 | paramsLast = params 1253 | varLast = params[0], params[1] 1254 | 1255 | pVK = derivative(partK, params, P, ang, 'V0') 1256 | 1257 | # Building Hessian matrix 1258 | H1 = [derivative(partV0, params, P, ang, 'V0'), pVK] 1259 | H2 = [pVK, derivative(partK, params, P, ang, 'K') ] 1260 | a = 0.5 * np.array([H1, H2]) 1261 | b = -0.5 * np.array([partV0(params, ang, P), partK(params, ang, P)]) 1262 | 1263 | var = varLast + np.linalg.solve(a, b) 1264 | 1265 | params = var[0], var[1], w, e, T0, P 1266 | 1267 | if abs(SumSquared(paramsLast, None) - SumSquared(params, None)) < 0.01: 1268 | break 1269 | 1270 | K = params[1] 1271 | 1272 | if shift: 1273 | dV0 = sigFig((params[0] - V0),5) 1274 | 1275 | nsec = 2*np.pi / (P*24*3600) 1276 | a1 = (K*np.sqrt(1-e**2) / nsec) * 10e3 / 10e9 1277 | fm = 3.985e-20 * (a1 * 10e9 / 10e3)**3 / P**2 1278 | 1279 | K = sigFig(K, 5) 1280 | a1 = sigFig(a1, 5) 1281 | fm = sigFig(fm, 5) 1282 | 1283 | x = [V0, K, w, e, T0, P, a1, fm] 1284 | 1285 | if graphs: 1286 | 1287 | # RV vs time data 1288 | times = np.arange(min(time), max(time)+1, 10) 1289 | v = true(times, x[4], x[3], P) # array of true anomalies at every time for fit 1290 | fit = x[0] + x[1]*(np.cos(v + x[2])+ x[3]*np.cos(x[2])) 1291 | 1292 | # RV vs phase data 1293 | phase = ((time - x[4]) / P) % 1 # phase for every RV data point 1294 | phases = ((times - x[4]) / P) % 1 # phase for every point on fit 1295 | 1296 | 1297 | # rearranging the data to fit as a function of phase 1298 | phaseV = [] 1299 | phasesfit = [] 1300 | for i in range(N): 1301 | phaseV.append([phase[i], V[i]]) 1302 | for i in range(len(phases)): 1303 | phasesfit.append([phases[i], fit[i]]) 1304 | phaseV = np.array(sorted(phaseV,key=lambda l:l[0])) 1305 | phasesfit = np.array(sorted(phasesfit,key=lambda l:l[0])) 1306 | 1307 | # average velocity, γ 1308 | gam = np.zeros(100) + x[0] # average velocity 1309 | timeLin = np.linspace(min(times), max(times),100) 1310 | phaseLin = np.linspace(0,1,100) 1311 | 1312 | # time plot 1313 | plt.figure() 1314 | plt.scatter(time,V, s = 18) 1315 | plt.plot(times,fit, color = "orange") 1316 | plt.xlabel("RJD") 1317 | plt.ylabel("Velocity (km s$^{-1}$)") 1318 | plt.plot(timeLin,gam, ls = "--", color = "black") 1319 | 1320 | # phase plot 1321 | plt.figure() 1322 | plt.scatter(phaseV[:,0],phaseV[:,1], s = 18) 1323 | plt.plot(phasesfit[:,0] ,phasesfit[:,1], color = "orange") 1324 | plt.xlabel("Phase") 1325 | plt.ylabel("Velocity (km s$^{-1}$)") 1326 | plt.plot(phaseLin,gam,ls = "--", color = "black") 1327 | 1328 | if err[0] != None: # if user inputted errors 1329 | err[1] += 0.01 # adding error from newtons method onto K error 1330 | 1331 | dade = (-x[1] * x[3] * x[5] * (3600*24)) / (2 * np.pi * np.sqrt(1-x[3]**2)) 1332 | dadT = (x[1] * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1333 | dadK = (x[5] * (3600*24) * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1334 | aErr = np.sqrt((dadT * err[5] * (3600*24))**2 + (dadK * err[1])**2 + (dade * err[3])**2) 1335 | aErr *= (10e3 / 10e9) 1336 | 1337 | # Finding uncertainty in fm 1338 | dfda = 3 * (x[6] * (10e9 / 10e3) )**2 * (3.985e-20) * (x[5])**-2 1339 | dfdT = -2 * (x[6] * (10e9 / 10e3) )**3 * (3.985e-20) * (x[5])**-3 1340 | fErr = np.sqrt((dfda * aErr * (10e9 / 10e3) )**2 + (dfdT * err[5])**2) 1341 | 1342 | err.append(aErr) 1343 | err.append(fErr) 1344 | 1345 | x[2] = sigFig(wdeg, 6) 1346 | 1347 | V0err = sigFig(err[0], 5) 1348 | Kerr = sigFig(err[1], 5) 1349 | werr = sigFig(err[2],6) 1350 | eerr = sigFig(err[3], 5) 1351 | T0err = sigFig(err[4], 6) 1352 | a1err = sigFig(err[6], 5) 1353 | fmerr = sigFig(err[7], 5) 1354 | 1355 | if shift: 1356 | return x, [V0err,Kerr,werr,eerr,T0err,a1err,fmerr], dV0 1357 | else: 1358 | return x, [V0err,Kerr,werr,eerr,T0err,a1err,fmerr] 1359 | 1360 | else: 1361 | x[2] = sigFig(wdeg, 6) 1362 | if shift: 1363 | return x, dV0 1364 | else: 1365 | return x 1366 | 1367 | # setting up data for special case of stars = "both" 1368 | if star == "both": 1369 | V1 = np.copy(V) # primary radial velocities 1370 | time1 = np.copy(time) 1371 | V2 = data_sorted[:,2] # companion radial velocities 1372 | time2 = np.copy(time) 1373 | # weights for both stars 1374 | if data.shape[1] >= 4: 1375 | weight1 = data_sorted[:,3] 1376 | else: 1377 | weight1 = np.ones(N) 1378 | if data.shape[1] >= 5: 1379 | weight2 = data_sorted[:,4] 1380 | else: 1381 | weight2 = np.ones(N) 1382 | weight = np.concatenate((weight1,weight2)) 1383 | 1384 | if data.shape[1] >= 6: 1385 | setNum = data_sorted[:,5] 1386 | else: 1387 | linFit = lsq_bisect(V1,V2,weight1,weight2) 1388 | beta, alpha = linFit[0], linFit[1] 1389 | gamma = alpha / (1 - beta) 1390 | time = np.concatenate((time,time)) 1391 | V = np.concatenate((V,(V2-gamma)/beta + gamma)) 1392 | timecomb = np.copy(time) # holds both stars' times 1393 | Vcomb = np.copy(V) # holds both stars' RV 1394 | weightcomb = np.copy(weight) # holds both stars' weights 1395 | 1396 | 1397 | def BothStars(period = None, Pguess = None,covariance = False, graphs = True, correction = False): 1398 | """ 1399 | Solves for orbital parameters from radial velocity data, for both stars at once 1400 | 1401 | Keyword Arguments: 1402 | period (optional) - Known value for period (if none provided, by default 1403 | PrimarySolve makes its own guess) 1404 | covariance (optional) - If covariance = True, PrimarySolve will return the 1405 | entire covariance matrix. 1406 | graphs (optional) - If graphs = true, PrimarySolve creates 2 plots. First, a 1407 | plot of the radial velocity data as a function of time 1408 | (RJD), fitted with V(t) = γ + K(cos(v(t) + ω) + e*cos(ω) 1409 | where [γ, K, ω, e, T0] are the orbital parameters 1410 | solved for by PrimarySolve(). Second, a plot of the 1411 | radial velocity data as a function of phase (between 0 1412 | and 1) of the star's orbit. Again, fit with 1413 | V = γ + K(cos(v + ω) + e*cos(ω) where now V and v are a 1414 | function of phase. On both plots, the long-term average 1415 | velocity, γ, is also shown. 1416 | 1417 | Returns: 1418 | x - List of solved orbital parameters, the semi major axis and the 1419 | mass function (for both stars), in the form: 1420 | [[γ, K1, ω1, e, T0, P, a1, f1(M)], [γ, K2, ω2, e, T0, P, a1, f2(M)]] 1421 | err - List of standard errors on the determination of the orbital elements, 1422 | in the order [γ, K, ω, e, T0, P, a, f(M)] 1423 | C - Estimated covariance matrix (if argument covariance = True) 1424 | 1425 | # """ 1426 | 1427 | 1428 | # V = np.copy(Vcomb) 1429 | # weight = np.copy(weightcomb) 1430 | # time = np.copy(timecomb) 1431 | # print(len(V), len(weight)) 1432 | 1433 | if Pguess != None: 1434 | period = Pguess 1435 | if not correction: 1436 | print("Finding initial guesses...") 1437 | x = x0(period, star = "both",gam = gamma, zeroEcc = zeroEcc) # initial guess 1438 | if x == None: 1439 | print('User must provide an estimate for P.') 1440 | sys.exit() 1441 | 1442 | if Pguess != None: 1443 | period = None 1444 | 1445 | if period != None: # if period known and provided by user 1446 | P = period 1447 | x.pop() 1448 | V0, K, w, e, T0 = x 1449 | var = 5 1450 | else: 1451 | V0, K, w, e, T0, P = x 1452 | var = 6 1453 | 1454 | if zeroEcc: 1455 | if period != None: 1456 | x = V0, K, T0 1457 | var = 3 1458 | else: 1459 | x = V0, K,T0,P 1460 | var = 4 1461 | l = 3 1462 | if not correction: 1463 | print("Minimizing...") 1464 | 1465 | 1466 | 1467 | while True: # perfoming minimization 1468 | 1469 | if e < 0 and zeroEcc == False: 1470 | e = 0 # so some sqrts dont cause us troubles 1471 | x[3] = e 1472 | xLast = x 1473 | ang = true(timecomb, T0, e, P, ec = True) 1474 | 1475 | pVK = derivative(partK, x, P, ang, 'V0') 1476 | pVT0 = derivative(partT0, x, P, ang, 'V0') 1477 | pKT0 = derivative(partT0, x, P, ang, 'K') 1478 | 1479 | 1480 | 1481 | if period == None: 1482 | 1483 | # Building Hessian matrix 1484 | 1485 | 1486 | pVP = derivative(partV0, x, P, ang, 'P') 1487 | pKP = derivative(partK, x, P, ang, 'P') 1488 | pT0P = derivative(partT0, x, P, ang, 'P') 1489 | 1490 | if not zeroEcc: 1491 | peP = derivative(parte, x, P, ang, 'P') 1492 | pVe = derivative(parte, x, P, ang, 'V0') 1493 | pKe = derivative(parte, x, P, ang, 'K') 1494 | pwe = derivative(parte, x, P, ang, 'w') 1495 | peT0 = derivative(partT0, x, P, ang, 'e') 1496 | pVw = derivative(partw, x, P, ang, 'V0') 1497 | pKw = derivative(partw, x, P, ang, 'K') 1498 | pwT0 = derivative(partT0, x, P, ang, 'w') 1499 | pwP = derivative(partw, x, P, ang, 'P') 1500 | 1501 | H1 = [derivative(partV0, x, P, ang, 'V0') * (1+l), pVK, pVw, pVe, pVT0, pVP] 1502 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKw, pKe, pKT0, pKP] 1503 | H3 = [pVw, pKw, derivative(partw, x, P, ang, 'w') * (1+l), pwe, pwT0, pwP] 1504 | H4 = [pVe, pKe, pwe, derivative(parte, x, P, ang, 'e') * (l+1), peT0, peP] 1505 | H5 = [pVT0, pKT0, pwT0, peT0, derivative(partT0, x, P, ang, 'T0') * (1+l), pT0P] 1506 | H6 = [pVP, pKP, pwP, peP, pT0P, derivative(partP, x, P, ang, 'P') * (1+l)] 1507 | a = 0.5 * np.array([H1, H2, H3, H4, H5, H6]) 1508 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), partw(x, ang, P), 1509 | parte(x, ang, P), partT0(x, ang, P), partP(x, ang, P)]) 1510 | 1511 | else: 1512 | H1 = [derivative(partV0, x, P, ang, 'V0') * (1+l), pVK, pVT0, pVP] 1513 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKT0, pKP] 1514 | H3 = [pVT0, pKT0, derivative(partT0, x, P, ang, 'T0') * (1+l), pT0P] 1515 | H4 = [pVP, pKP, pT0P, derivative(partP, x, P, ang, 'P') * (1+l)] 1516 | a = 0.5 * np.array([H1, H2, H3, H4]) 1517 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), 1518 | partT0(x, ang, P), partP(x, ang, P)]) 1519 | 1520 | else: 1521 | 1522 | if not zeroEcc: 1523 | 1524 | pVe = derivative(parte, x, P, ang, 'V0') 1525 | pKe = derivative(parte, x, P, ang, 'K') 1526 | pwe = derivative(parte, x, P, ang, 'w') 1527 | peT0 = derivative(partT0, x, P, ang, 'e') 1528 | pVw = derivative(partw, x, P, ang, 'V0') 1529 | pKw = derivative(partw, x, P, ang, 'K') 1530 | pwT0 = derivative(partT0, x, P, ang, 'w') 1531 | 1532 | H1 = [derivative(partV0, x, P, ang, "V0") * (1+l), pVK, pVw, pVe, pVT0] 1533 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKw, pKe, pKT0] 1534 | H3 = [pVw, pKw, derivative(partw, x, P, ang, 'w') * (1+l), pwe, pwT0] 1535 | H4 = [pVe, pKe, pwe, derivative(parte, x, P, ang, 'e') * (l+1), peT0] 1536 | H5 = [pVT0, pKT0, pwT0, peT0, derivative(partT0, x, P, ang, 'T0') * (1+l)] 1537 | a = 0.5 * np.array([H1,H2,H3,H4,H5]) 1538 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), partw(x, ang, P), 1539 | parte(x, ang, P), partT0(x, ang, P)]) 1540 | 1541 | else: 1542 | H1 = [derivative(partV0, x, P, ang, "V0") * (1+l), pVK, pVT0] 1543 | H2 = [pVK, derivative(partK, x, P, ang, 'K') * (1+l), pKT0] 1544 | H3 = [pVT0, pKT0, derivative(partT0, x, P, ang, 'T0') * (1+l)] 1545 | a = 0.5 * np.array([H1,H2,H3]) 1546 | b = -0.5 * np.array([partV0(x, ang, P), partK(x, ang, P), 1547 | partT0(x, ang, P)]) 1548 | 1549 | x = xLast + np.linalg.solve(a, b) # Next iterations parameter values 1550 | 1551 | if period == None: 1552 | if zeroEcc: 1553 | V0, K, T0, P = x 1554 | else: 1555 | V0, K, w, e, T0, P = x 1556 | else: 1557 | if zeroEcc: 1558 | V0, K, T0 = x 1559 | else: 1560 | V0, K, w, e, T0 = x 1561 | 1562 | 1563 | if SumSquared(x, P) < SumSquared(xLast, P): # if params got better 1564 | if abs(SumSquared(xLast, P) - SumSquared(x, P)) < 0.01 or abs(1-(SumSquared(x, P)/SumSquared(xLast, P))) < 1e-3: 1565 | break # convergence acheived! (probably) 1566 | else: 1567 | l /= 9 # lowering damping parameter 1568 | l = max(l, 10e-7) 1569 | else: # if params got worse 1570 | l *= 11 # raising damping parameter 1571 | l = min(l, 10e7) 1572 | 1573 | if not zeroEcc: 1574 | if x[2] < -np.pi or x[2] >= np.pi: 1575 | while x[2] < -np.pi: 1576 | x[2] = x[2] + 2*np.pi 1577 | while x[2] >= np.pi: 1578 | x[2] = x[2] - 2 * np.pi 1579 | w = x[2] 1580 | 1581 | if period == None: 1582 | if zeroEcc: 1583 | P = x[3] 1584 | else: 1585 | P = x[5] 1586 | x = [V0,K,w,e,T0,P] 1587 | 1588 | # other values to be returned 1589 | nsec = 2*np.pi / (24*3600*P) 1590 | a1 = (x[1]*np.sqrt(1-x[3]**2) / nsec) * 10e3 / 10e9 1591 | fm = 3.985e-20 * (a1 * 10e9 / 10e3)**3 / P**2 1592 | x.append(a1) 1593 | x.append(fm) 1594 | K2 = -1*beta * K 1595 | 1596 | 1597 | # if period == None: 1598 | # x = [x[0], x[1], x[2], x[3], x[4], x[5], a1, fm] 1599 | # else: 1600 | # x = [x[0], x[1], x[2], x[3], x[4], P, a1, fm] 1601 | 1602 | C = np.linalg.inv(a) # covariance matrix 1603 | err = [] 1604 | 1605 | for q in range(var): 1606 | err.append(np.sqrt(C[q][q])) 1607 | if period != None: 1608 | err.append(0) 1609 | if zeroEcc: 1610 | err.insert(2,0) 1611 | err.insert(3,0) 1612 | 1613 | # Finding uncertainty in a1 1614 | dade = (-x[1] * x[3] * x[5] * (3600*24)) / (2 * np.pi * np.sqrt(1-x[3]**2)) 1615 | dadT = (x[1] * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1616 | dadK = (x[5] * (3600*24) * np.sqrt(1-x[3]**2)) / (2 * np.pi) 1617 | aErr = np.sqrt((dadT * err[5] * (3600*24))**2 + (dadK * err[1])**2 + (dade * err[3])**2) 1618 | aErr *= (10e3 / 10e9) 1619 | 1620 | # Finding uncertainty in fm 1621 | dfda = 3 * (x[6] * (10e9 / 10e3) )**2 * (3.985e-20) * (x[5])**-2 1622 | dfdT = -2 * (x[6] * (10e9 / 10e3) )**3 * (3.985e-20) * (x[5])**-3 1623 | fErr = np.sqrt((dfda * aErr * (10e9 / 10e3) )**2 + (dfdT * err[5])**2) 1624 | 1625 | err.append(aErr) 1626 | err.append(fErr) 1627 | 1628 | 1629 | # rounding 1630 | 1631 | V0 = sigFig(x[0], 5) 1632 | K = sigFig(x[1], 5) 1633 | 1634 | if not zeroEcc: 1635 | w = sigFig((x[2] * 180/np.pi)%360, 6) 1636 | e = sigFig(x[3], 5) 1637 | T0 = sigFig(x[4], 6) 1638 | a1 = sigFig(x[6], 5) 1639 | fm = sigFig(x[7], 5) 1640 | 1641 | V0err = sigFig(err[0], 5) 1642 | Kerr = sigFig(err[1], 5) 1643 | werr = err[2] 1644 | eerr = err[3] 1645 | if not zeroEcc: 1646 | werr = sigFig((err[2] * 180/np.pi)%360, 6) 1647 | eerr = sigFig(err[3], 5) 1648 | T0err = sigFig(err[4], 6) 1649 | a1err = sigFig(err[6], 5) 1650 | fmerr = sigFig(err[7], 5) 1651 | 1652 | if period == None: 1653 | P = sigFig(x[5], 6) 1654 | Perr = sigFig(err[5], 6) 1655 | else: 1656 | P = x[5] 1657 | Perr = err[5] 1658 | 1659 | # K2 = -1*beta * K 1660 | a2 = (K2*np.sqrt(1-e**2) / nsec) * 10e3 / 10e9 1661 | fm2 = 3.985e-20 * (a2 * 10e9 / 10e3)**3 / P**2 1662 | 1663 | w2 = (w + 180)%360 1664 | 1665 | # there are twice as any params to return because stars = "both"! 1666 | x = np.array([[V0, K, w, e, T0, P, a1, fm], [V0, K2, w2, e, T0, P, a2, fm2]]) 1667 | err = [V0err, Kerr, werr, eerr, T0err, Perr, a1err, fmerr] 1668 | err = np.array([err,err]) 1669 | C = np.array([C,C]) 1670 | 1671 | #now going through one more time to solve Ks and gammas better 1672 | #first star 1 1673 | X = x[0][0:6] 1674 | X[2] = np.radians(X[2])%(2*np.pi) 1675 | 1676 | # V = V1 1677 | # weight = weight1 1678 | # time = np.array(list(set(timecomb))) 1679 | ang = true(time1, T0, e, P, ec = True) 1680 | while True: 1681 | 1682 | Xlast = X 1683 | varLast = np.array([X[0], X[1]]) 1684 | 1685 | pVK = derivative(partK, X, P, ang, 'V0', starNum = 1) 1686 | 1687 | # Building Hessian matrix 1688 | H1 = [derivative(partV0, X, P, ang, 'V0',starNum = 1), pVK] 1689 | H2 = [pVK, derivative(partK, X, P, ang, 'K',starNum = 1) ] 1690 | a = 0.5 * np.array([H1, H2]) 1691 | b = -0.5 * np.array([partV0(X, ang, P,starNum = 1), partK(X, ang, P, starNum = 1)]) 1692 | 1693 | var = varLast + np.linalg.solve(a, b) 1694 | 1695 | X = [var[0], var[1], X[2], e, T0, P] 1696 | 1697 | if abs(SumSquared(Xlast, None, starNum = 1) - SumSquared(X, None, starNum = 1)) < 0.01: 1698 | break 1699 | 1700 | K = X[1] 1701 | V0 = X[0] 1702 | 1703 | nsec = 2*np.pi / (P*24*3600) 1704 | a1 = (K*np.sqrt(1-e**2) / nsec) * 10e3 / 10e9 1705 | fm = 3.985e-20 * (a1 * 10e9 / 10e3)**3 / P**2 1706 | 1707 | K = sigFig(K, 5) 1708 | a1 = sigFig(a1, 5) 1709 | fm = sigFig(fm, 5) 1710 | 1711 | x[0] = [V0, K,w, e, T0, P, a1, fm] 1712 | 1713 | C1 = np.linalg.inv(a) # covariance matrix for only star 1 (V0 and K1) 1714 | err[0][0] = sigFig(np.sqrt(C1[0][0]), 5) 1715 | err[0][1] = sigFig(np.sqrt(C1[1][1]), 5) 1716 | C[0][0][0] = (C1[0][0]) 1717 | C[0][0][1] = (C1[0][1]) 1718 | C[0][1][0] = (C1[1][0]) 1719 | C[0][1][1] = (C1[1][1]) 1720 | 1721 | # now doing the other star 1722 | X = x[1][0:6] 1723 | X[2] = np.radians(X[2])%(2*np.pi) 1724 | 1725 | # V = V2 1726 | # weight = weight2 1727 | # time = np.array(list(set(timecomb))) 1728 | # print(K2,X[1],X[2]) 1729 | ang = true(time2, T0, e, P, ec = True) 1730 | while True: 1731 | 1732 | Xlast = X 1733 | varLast = np.array([X[0], X[1]]) 1734 | pVK = derivative(partK, X, P, ang, 'V0', starNum = 2) 1735 | 1736 | # Building Hessian matrix 1737 | H1 = [derivative(partV0, X, P, ang, 'V0',starNum = 2), pVK] 1738 | H2 = [pVK, derivative(partK, X, P, ang, 'K',starNum = 2) ] 1739 | a = 0.5 * np.array([H1, H2]) 1740 | b = -0.5 * np.array([partV0(X, ang, P,starNum = 2), partK(X, ang, P,starNum = 2)]) 1741 | 1742 | var = varLast + np.linalg.solve(a, b) 1743 | 1744 | X = [var[0], var[1], X[2], e, T0, P] 1745 | 1746 | if abs(SumSquared(Xlast, None,starNum = 2) - SumSquared(X, None,starNum = 2)) < 0.01: 1747 | break 1748 | 1749 | K2 = X[1] 1750 | V0 = X[0] 1751 | 1752 | a2 = (K2*np.sqrt(1-e**2) / nsec) * 10e3 / 10e9 1753 | fm2 = 3.985e-20 * (a1 * 10e9 / 10e3)**3 / P**2 1754 | 1755 | K2 = sigFig(K2, 5) 1756 | a2 = sigFig(a2, 5) 1757 | fm2 = sigFig(fm2, 5) 1758 | 1759 | x[1] = [V0, K2, w2, e, T0, P, a2, fm2] 1760 | 1761 | C2 = np.linalg.inv(a) # covariance matrix for only star 1 (V0 and K1) 1762 | err[1][0] = sigFig(np.sqrt(C2[0][0]), 5) 1763 | err[1][1] = sigFig(np.sqrt(C2[1][1]), 5) 1764 | C[1][0][0] = (C2[0][0]) 1765 | C[1][0][1] = (C2[0][1]) 1766 | C[1][1][0] = (C2[1][0]) 1767 | C[1][1][1] = (C2[1][1]) 1768 | 1769 | # create plots if expected 1770 | if graphs: 1771 | # x[0][2] = 141.8 1772 | # x[0][0:6] = [-14.24,17.7,(141.8), 0.92, 52956.3,6550] 1773 | 1774 | # RV vs time data 1775 | if N < 500: 1776 | M = 500 1777 | else: 1778 | M = N 1779 | times = np.linspace(min(time1), max(time1),M) # times for fit 1780 | v = true(times, x[0][4], x[0][3], P) # array of true anomalies at every time for fit 1781 | fit = x[0][0] + x[0][1]*(np.cos(v + np.radians(x[0][2]))+ x[0][3]*np.cos(x[0][2])) 1782 | fit2 = x[1][0] + x[1][1] * (np.cos(v + np.radians(x[1][2]))+ x[1][3]*np.cos(x[1][2])) 1783 | 1784 | # RV vs phase data 1785 | phase = ((time - T0) / P) % 1 # phase for every RV data point 1786 | phase = phase[int(len(phase)/2):len(phase)] 1787 | # phases = np.linspace(min(phase), max(phase), N) 1788 | phases = ((times - T0) / P) % 1 # phase for every point on fit 1789 | # rearranging the data to fit as a function of phase 1790 | phaseV = [] 1791 | phasesfit = [] 1792 | for i in range(len(phases)): 1793 | if i < len(V2): 1794 | phaseV.append([phase[i], V[i]]) 1795 | phasesfit.append([phases[i], fit[i]]) 1796 | phaseV = np.array(sorted(phaseV,key=lambda l:l[0])) 1797 | phasesfit = np.array(sorted(phasesfit,key=lambda l:l[0])) 1798 | 1799 | 1800 | 1801 | # average velocity, γ 1802 | gam1 = np.zeros(100) + x[0][0] # first star 1803 | gam2 = np.zeros(100) + x[1][0] # other star 1804 | timeLin = np.linspace(min(times), max(times),100) 1805 | phaseLin = np.linspace(0,1,100) 1806 | 1807 | # time plot 1808 | fig, ax = plt.subplots() 1809 | ax.scatter(time[int(len(time)/2):len(time)], V1, s = 18, label = "Primary") 1810 | ax.plot(times, fit, color = "orange") 1811 | ax.set_xlabel("RJD") 1812 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1813 | ax.plot(timeLin, gam1, ls = "--", color = "black") 1814 | ax.minorticks_on() 1815 | ax.tick_params(right=True, top=True) 1816 | ax.tick_params(right=True, top=True, which="minor") 1817 | ax.legend() 1818 | 1819 | fig, ax = plt.subplots() 1820 | ax.scatter(time[int(len(time)/2):len(time)], V2, s = 18, label = "Companion", color = "C3") 1821 | ax.plot(times, fit2, color = "orange") 1822 | ax.set_xlabel("RJD") 1823 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1824 | ax.plot(timeLin, gam2, ls = "--", color = "black") 1825 | ax.minorticks_on() 1826 | ax.tick_params(right=True, top=True) 1827 | ax.tick_params(right=True, top=True, which="minor") 1828 | ax.legend() 1829 | 1830 | # phase plot 1831 | fig, ax = plt.subplots() 1832 | ax.scatter(phaseV[:,0], phaseV[:,1],s = 18, label = "primary") 1833 | ax.plot(phasesfit[:,0], phasesfit[:,1], color = "orange") 1834 | ax.set_xlabel("Phase") 1835 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1836 | ax.plot(phaseLin, gam1, ls = "--", color = "black") 1837 | ax.minorticks_on() 1838 | ax.tick_params(right=True, top=True) 1839 | ax.tick_params(right=True, top=True, which="minor") 1840 | ax.legend() 1841 | 1842 | phaseV = [] 1843 | phasesfit = [] 1844 | for i in range(len(phases)): 1845 | if i < len(V2): 1846 | phaseV.append([phase[i], V2[i]]) 1847 | phasesfit.append([phases[i], fit2[i]]) 1848 | phaseV = np.array(sorted(phaseV,key=lambda l:l[0])) 1849 | phasesfit = np.array(sorted(phasesfit,key=lambda l:l[0])) 1850 | 1851 | fig, ax = plt.subplots() 1852 | ax.scatter(phaseV[:,0], phaseV[:,1],s = 18, label = "companion", color = "C3") 1853 | ax.plot(phasesfit[:,0], phasesfit[:,1], color = "orange") 1854 | ax.set_xlabel("Phase") 1855 | ax.set_ylabel("Velocity (km s$^{-1}$)") 1856 | ax.plot(phaseLin, gam2, ls = "--", color = "black") 1857 | ax.minorticks_on() 1858 | ax.tick_params(right=True, top=True) 1859 | ax.tick_params(right=True, top=True, which="minor") 1860 | ax.legend() 1861 | 1862 | 1863 | # return requested values 1864 | if covariance: # FIX THIS MILSON YOU LAZY DEGENERATE! 1865 | for k in range(2): 1866 | for i in range(np.size(C,1)): 1867 | for j in range(np.size(C,2)): 1868 | C[k][i][j] = sigFig(C[k][i][j], 5) 1869 | return x, err, C 1870 | else: 1871 | return x, err 1872 | 1873 | 1874 | def meanMatch(X): 1875 | """ 1876 | Finds the γ of an offset dataset 1877 | 1878 | Keyword Arguments: 1879 | X - List of elements of the primary star, in the order [γ, K, w, e, T0, P]. 1880 | (with w in degrees). 1881 | 1882 | Returns: 1883 | V0 - The corrected value of γ 1884 | """ 1885 | V0, K, wdeg, e, T0, P = X 1886 | w = wdeg*np.pi/180 1887 | params = V0, K, w, e, T0, P 1888 | 1889 | ang = true(time, T0, e, P, ec = True) 1890 | 1891 | while True: # single variable Newton's method 1892 | paramsLast = params 1893 | V0 = params[0] 1894 | V0 = V0 - partV0(params, ang, P)/(derivative(partV0, params, P, ang, 'V0')) 1895 | params = V0, K, w, e, T0, P 1896 | if abs(SumSquared(paramsLast, None) - SumSquared(params, None)) < 0.01 : 1897 | break 1898 | 1899 | V0 = params[0] 1900 | 1901 | return V0 1902 | 1903 | 1904 | #%% Correcting γ offset bewteen sub data sets 1905 | 1906 | def split(): 1907 | """ 1908 | Splits a data set into its constituent data sets. 1909 | """ 1910 | sets = np.array(list(set(setNum))) # indices of the different data sets 1911 | origSets = np.empty([len(setNum),data.shape[1],len(sets)],float) # 3D array 1912 | origSets[:,:,:] = np.nan 1913 | # seperating the sets 1914 | for i in range(len(setNum)): 1915 | for j in range(len(sets)): 1916 | if setNum[i] == sets[j]: 1917 | origSets[i,:,j] = data_sorted[i,:] 1918 | mask = np.isfinite(origSets) # mask for which elements have a value 1919 | return (origSets,mask) 1920 | 1921 | if data.shape[1] >= 4: # if γ correcting is required 1922 | if star != "both" or data.shape[1] >= 6: 1923 | print("Correcting sub-dataset offsets...") 1924 | splitSets, splitMask = split() # splitting sets 1925 | contenders = [] # holds if a data set could be the best 1926 | 1927 | 1928 | if star == "primary": 1929 | 1930 | for j in range(splitSets.shape[2]): 1931 | V = splitSets[:,1,j][splitMask[:,1,j]] 1932 | time = splitSets[:,0,j][splitMask[:,0,j]] 1933 | N = len(time) 1934 | if np.isfinite(PGuess(test = True)).any(): # if a period can be found 1935 | contenders.append(j) 1936 | if len(contenders) == 0: # if no sub dataset can estimate period 1937 | big = np.array([0,None]) # simply choose the largest set 1938 | for j in range(splitSets.shape[2]): 1939 | time = splitSets[:,0,j][splitMask[:,0,j]] 1940 | N = len(time) 1941 | if N >= big[0]: 1942 | big[0], big[1] = N,j 1943 | contenders.append(big[1]) 1944 | 1945 | SS = np.inf # will hold sum of squared deviations 1946 | tempContenders = np.copy(contenders) 1947 | for j in tempContenders: # figuring out which contender is best 1948 | time = splitSets[:,0,int(j)][splitMask[:,0,int(j)]] 1949 | N = len(time) 1950 | V = splitSets[:,1,int(j)][splitMask[:,1,int(j)]] 1951 | weight = np.ones(N) 1952 | if Pguess != None: 1953 | Period = Pguess 1954 | X = x0(Period) 1955 | if (SumSquared(X,None))/N <= SS: 1956 | SS = (SumSquared(X,None))/N 1957 | del contenders[:contenders.index(j)] 1958 | else: 1959 | contenders.remove(j) 1960 | 1961 | time = splitSets[:,0,contenders[0]][splitMask[:,0,contenders[0]]] 1962 | V = splitSets[:,1,contenders[0]][splitMask[:,1,contenders[0]]] 1963 | weight = splitSets[:,2,contenders[0]][splitMask[:,2,contenders[0]]] 1964 | N = len(time) 1965 | # finding parameters for "best" data set 1966 | X,err = PrimarySolve(period = Period, covariance = False, graphs = False,correction = True) 1967 | sets = list(set(setNum)) 1968 | sets.remove(setNum[contenders[0]]) # removing the "best" set from sets 1969 | for i in sets: # shifting other data sets 1970 | # V = splitSets[:,1,int(i-1)][splitMask[:,1,int(i-1)]] 1971 | # time = splitSets[:,0,int(i-1)][splitMask[:,0,int(i-1)]] 1972 | # weight = splitSets[:,2,int(i-1)][splitMask[:,2,int(i-1)]] 1973 | N = len(time) 1974 | V0 = meanMatch(X[:6]) # finding γ for set i 1975 | shift = V0 - X[0] # the difference in mean RV 1976 | # putting shifted data back together 1977 | for j in range(data_sorted.shape[0]): 1978 | if data_sorted[j,3] == i: 1979 | data_sorted[j,1] -= shift # shifting set i 1980 | 1981 | 1982 | elif star == "both": 1983 | # will hold all the different gammas, and their weights 1984 | gammaList = np.empty((splitSets.shape[2],2)) 1985 | for j in range(splitSets.shape[2]): 1986 | V = splitSets[:,1,j][splitMask[:,1,j]] 1987 | V2 = splitSets[:,2,j][splitMask[:,2,j]] 1988 | time = splitSets[:,0,j][splitMask[:,0,j]] 1989 | N = len(time) 1990 | linFit = np.polyfit(V,V2,1) 1991 | beta, alpha = linFit[0], linFit[1] 1992 | gamma = alpha / (1 - beta) # this set's gamma 1993 | gammaWeight = np.concatenate((splitSets[:,3,j][splitMask[:,3,j]],splitSets[:,4,j][splitMask[:,4,j]])) 1994 | gammaWeight = np.mean(gammaWeight) 1995 | gammaList[j] = [gamma,gammaWeight] 1996 | 1997 | #finding weighted mean 1998 | gamma = np.sum(gammaList[:,0] * gammaList[:,1]) / np.sum(gammaList[:,1]) 1999 | #shifting data sets to be at weighted mean gamma 2000 | sets = list(set(setNum)) 2001 | for i in sets: 2002 | shift = gamma - gammaList[sets.index(i)][0] 2003 | # putting shifted data back together 2004 | for j in range(data_sorted.shape[0]): 2005 | if data_sorted[j,5] == i: 2006 | # shifting set i 2007 | # print(shift) 2008 | data_sorted[j,1] += shift 2009 | data_sorted[j,2] += shift 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | if Pguess != None: 2016 | Period = None 2017 | time = data_sorted[:,0] 2018 | V = data_sorted[:,1] 2019 | weight = data_sorted[:,2] 2020 | N = len(time) 2021 | 2022 | 2023 | if star == "both": 2024 | V1 = np.copy(V) 2025 | V2 = data_sorted[:,2] 2026 | linFit = np.polyfit(V,V2,1) 2027 | beta, alpha = linFit[0], linFit[1] 2028 | gamma = alpha / (1 - beta) 2029 | time = np.concatenate((time,time)) 2030 | timecomb = np.copy(time) 2031 | V = np.concatenate((V,(V2-gamma)/beta + gamma)) 2032 | Vcomb = np.copy(V) 2033 | weight = np.concatenate((data_sorted[:,3],data_sorted[:,4])) 2034 | weightcomb = np.copy(weight) 2035 | 2036 | if star == "primary": 2037 | if covariance: 2038 | x,err,C = PrimarySolve(Period,Pguess,covariance,graphs) 2039 | return x,err,C 2040 | else: 2041 | x,err = PrimarySolve(Period,Pguess,covariance,graphs) 2042 | return x,err 2043 | 2044 | elif star == "secondary": 2045 | if shift: 2046 | if err != None: 2047 | x,err,dV0 =CompanionSolve(X,err,shift,graphs) 2048 | return x,err,dV0 2049 | else: 2050 | x,dV0 =CompanionSolve(X,err,shift,graphs) 2051 | return x,dV0 2052 | else: 2053 | if err != None: 2054 | x,err =CompanionSolve(X,err,shift,graphs) 2055 | return x,err 2056 | else: 2057 | x =CompanionSolve(X,err,shift,graphs) 2058 | return x 2059 | elif star == "both": 2060 | if covariance: 2061 | x,err,C = BothStars(Period,Pguess,covariance,graphs) 2062 | return x,err,C 2063 | else: 2064 | x,err = BothStars(Period,Pguess,covariance,graphs) 2065 | return x,err 2066 | else: 2067 | print("Please let the parameter, star, be the string \"primary\" or \"secondary\" or \"both\" ") 2068 | sys.exit() 2069 | 2070 | 2071 | --------------------------------------------------------------------------------