├── .github └── workflows │ └── betel120d_flux.yml ├── .gitignore ├── LICENSE ├── README.md ├── aavso_vis.csv ├── banner.jpg ├── betel120d.py ├── betel120d_flux.py ├── betel125y.py ├── betel20d.py ├── betel5y.py ├── betel_ani.py ├── betel_video.gif ├── betellib.py ├── plot120d_flux.png └── plot20d.png /.github/workflows/betel120d_flux.yml: -------------------------------------------------------------------------------- 1 | name: betel120dflux 2 | 3 | #on: [push] 4 | 5 | #on: 6 | # schedule: 7 | # - cron: '00 19 * * *' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up Python 3.7 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.7 19 | - name: Install dependencies 20 | run: | 21 | pip install twython numpy bs4 wotan matplotlib requests astropy 22 | - name: betel 23 | env: 24 | consumer_key: ${{ secrets.consumer_key }} 25 | consumer_secret: ${{ secrets.consumer_secret }} 26 | access_token: ${{ secrets.access_token }} 27 | access_token_secret: ${{ secrets.access_token_secret }} 28 | run: | 29 | python betel120d_flux.py 30 | - name: Commit files 31 | run: | 32 | git config --local user.email ${{ secrets.SECRET_MAIL_FROM }} 33 | git config --local user.name ${{ secrets.SECRET_GITHUB_USERNAME }} 34 | git add . 35 | git add --all 36 | # git commit -m "Add changes" -a 37 | git diff --quiet && git diff --staged --quiet || git commit -am 'Add changes' 38 | - name: Push changes 39 | uses: ad-m/github-push-action@master 40 | with: 41 | github_token: ${{ secrets.SECRET_GITHUB_TOKEN }} 42 | branch: master 43 | -------------------------------------------------------------------------------- /.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 | MIT License 2 | 3 | Copyright (c) 2020 Michael Hippke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://raw.githubusercontent.com/hippke/betelbot/master/banner.jpg) 2 | 3 | # Betelbot: Betelgeuse Supernova Twitter Bot 4 | 5 | Tracks [AAVSO observations](https://www.aavso.org/lcg/plot?auid=000-BBK-383&starname=BETELGEUSE&lastdays=200&start=&stop=2458869.83791&obscode=&obscode_symbol=2&obstotals=yes&calendar=calendar&forcetics=&pointsize=1&width=800&height=450&mag1=&mag2=&mean=&vmean=&grid=on&visual=on&uband=on&bband=on&v=on), calculates average magnitudes, and makes a plot. Daily [Twitter tweets](https://twitter.com/betelbot). 6 | 7 | > My visual mag from last night was X.XX (avg of XX observations). That is X.XX mag dimmer than the avg of the X previous nights (n=XXX, 0.Xσ). 8 | 9 | 10 | 11 | This bot runs as a [Github Action](https://github.com/hippke/betelbot/actions). You are free to adapt the idea and code provided here to make your own projects. Feedback is welcome. 12 | -------------------------------------------------------------------------------- /banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hippke/betelbot/1e08a8e2ba17b8e7f0f13701b1a6d7023a670546/banner.jpg -------------------------------------------------------------------------------- /betel120d.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime 3 | from matplotlib import pyplot as plt 4 | from wotan import flatten 5 | from betellib import tweet, build_string, get_mags_from_AAVSO 6 | 7 | 8 | def make_plot(days_ago, dates, mag): 9 | print('Making plot...') 10 | time_span = np.max(dates) - np.min(dates) 11 | min_plot = 0.0 12 | max_plot = 1.75 13 | x_days = 120 14 | 15 | # Make daily bins 16 | nights = np.arange(0, max(days_ago), 1) 17 | daily_mags = [] 18 | errors = [] 19 | for night in nights: 20 | selector = np.where((days_agonight)) 21 | n_obs = np.size(mag[selector]) 22 | flux = np.mean(mag[selector]) 23 | error = np.std(mag[selector]) / np.sqrt(n_obs) 24 | if error > 0.75: 25 | error = 0 26 | daily_mags.append(flux) 27 | errors.append(error) 28 | print(night, flux, error, n_obs, np.std(mag[selector])) 29 | plt.errorbar(nights+0.5, daily_mags, yerr=errors, fmt='.k') 30 | plt.xlabel('Days before today') 31 | plt.ylabel('Visual magnitude') 32 | mid = np.median(mag) 33 | plt.ylim(min_plot, max_plot) 34 | plt.xlim(0, x_days) 35 | plt.gca().invert_yaxis() 36 | plt.gca().invert_xaxis() 37 | date_text = datetime.datetime.now().strftime("%d %b %Y") 38 | plt.text(x_days-2, max_plot-0.05, 'AAVSO visual (by-eye) daily bins. Update: '+date_text) 39 | plt.savefig(plot_file, bbox_inches='tight', dpi=300) 40 | print('Plot made, test120') 41 | 42 | 43 | # Pull the last 10 pages from AAVSO and collate the dates and mags 44 | plot_file = 'plot120d.png' 45 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=vis&page=' 46 | pages = np.arange(1, 20, 1) 47 | all_dates = np.array([]) 48 | all_mags = np.array([]) 49 | for page in pages: 50 | url = url_base + str(page) 51 | print(url) 52 | dates, mags = get_mags_from_AAVSO(url) 53 | all_dates = np.concatenate((all_dates, dates)) 54 | all_mags = np.concatenate((all_mags, mags)) 55 | dates = all_dates 56 | mags = all_mags 57 | days_ago = np.max(dates) - dates 58 | text = build_string(days_ago, mags) 59 | if text is not None: 60 | make_plot(days_ago, dates, mags) 61 | #tweet(text, plot_file) 62 | -------------------------------------------------------------------------------- /betel120d_flux.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime 3 | from matplotlib import pyplot as plt 4 | from betellib import tweet, build_string, get_mags_from_AAVSO 5 | from astropy.stats import biweight_location 6 | 7 | 8 | def make_plot(days_ago, dates, mag): 9 | print('Making plot...') 10 | time_span = np.max(dates) - np.min(dates) 11 | min_plot = 0 12 | max_plot = +1.7 13 | x_days = 300 14 | 15 | # Make bins 16 | bin_width = 1 17 | nights = np.arange(0, max(days_ago), bin_width) 18 | bin_mags = [] 19 | errors = [] 20 | for night in nights: 21 | selector = np.where((days_agonight)) 22 | n_obs = np.size(mag[selector]) 23 | flux = biweight_location(mag[selector]) 24 | error = np.std(mag[selector]) / np.sqrt(n_obs) 25 | if error > 0.2: 26 | error = 0 27 | if error == 0:# and flux < 0.2: 28 | flux = np.nan 29 | bin_mags.append(flux) 30 | errors.append(error) 31 | print(night, flux, error, n_obs, np.std(mag[selector])) 32 | 33 | # Convert magnitudes to fluxes 34 | bin_mags = np.array(bin_mags) 35 | flux = 1 / (10**(0.4 * (bin_mags - baseline_mag))) 36 | latest_flux = flux[0] 37 | if np.isnan(latest_flux): 38 | latest_flux = flux[1] 39 | 40 | plt.errorbar(nights+0.5, flux, yerr=errors, fmt='.k') 41 | plt.xlabel('Days before today') 42 | plt.ylabel('Normalized flux (0.5 mag baseline)') 43 | plt.ylim(min_plot, max_plot) 44 | plt.xlim(x_days, 0) 45 | date_text = datetime.datetime.now().strftime("%d %b %Y") 46 | try: 47 | lumi = str(int((round(latest_flux*100, 0)))) 48 | text = "#Betelgeuse at " + lumi + r"% of its usual brightness @betelbot " 49 | except: 50 | text = "No new #Betelgeuse brightness tonight @betelbot" 51 | lumi = 0 52 | plt.text(x_days-2, 0.19, "Update: " + date_text) 53 | plt.text(x_days-2, 0.12, text) 54 | plt.text(x_days-2, 0.05, "AAVSO visual (by-eye) daily bins") 55 | plt.savefig(plot_file, bbox_inches='tight', dpi=300) 56 | print('Plot made') 57 | return lumi 58 | 59 | 60 | # Pull the last 10 pages from AAVSO and collate the dates and mags 61 | plot_file = 'plot120d_flux.png' 62 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=vis&page=' 63 | baseline_mag = 0.5 64 | pages = np.arange(1, 20, 1) 65 | all_dates = np.array([]) 66 | all_mags = np.array([]) 67 | for page in pages: 68 | url = url_base + str(page) 69 | print(url) 70 | dates, mags = get_mags_from_AAVSO(url) 71 | all_dates = np.concatenate((all_dates, dates)) 72 | all_mags = np.concatenate((all_mags, mags)) 73 | dates = all_dates 74 | mags = all_mags 75 | days_ago = np.max(dates) - dates 76 | 77 | lumi = make_plot(days_ago, dates, mags) 78 | if lumi == 0: 79 | text = "No new #Betelgeuse brightness tonight" 80 | else: 81 | text = "Now at " + lumi + r"% of my usual brightness! #Betelgeuse" 82 | tweet(text, plot_file) 83 | print(text) 84 | 85 | -------------------------------------------------------------------------------- /betel125y.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import pyplot as plt 3 | import datetime 4 | from betellib import tweet 5 | 6 | 7 | def make_plot(days_ago, dates, mag): 8 | print('Making plot...') 9 | time_span = np.max(dates) - np.min(dates) 10 | min_plot = 1.75 11 | max_plot = 0 12 | 13 | # 30-day bins for the century scale 14 | bin_width = 30 # days 15 | nights = np.arange(0, max(days_ago), bin_width) 16 | bin_mags = [] 17 | errors = [] 18 | for night in nights: 19 | selector = np.where((days_agonight)) 20 | n_obs = np.size(mag[selector]) 21 | flux = np.mean(mag[selector]) 22 | error = np.std(mag[selector]) / np.sqrt(n_obs) 23 | if error > 0.2: 24 | error = 0 25 | bin_mags.append(flux) 26 | errors.append(error) 27 | # Convert days to digital years 28 | date = datetime.datetime.now() 29 | digi_year = (float(date.strftime("%j"))-1) / 366 + float(date.strftime("%Y")) 30 | days = nights+bin_width/2 31 | years_before = digi_year - (days / 365.2524) 32 | 33 | fig, ax = plt.subplots() 34 | plt.errorbar(years_before, bin_mags, yerr=errors, fmt='.k', alpha=0.5) 35 | plt.scatter(years_before[0], bin_mags[0], s=50, marker="o", color='red', alpha=0.5) 36 | 37 | # 3-day bins for the last 90 days 38 | bin_width = 3 # days 39 | nights = np.arange(0, 90, bin_width) 40 | bin_mags = [] 41 | errors = [] 42 | for night in nights: 43 | selector = np.where((days_agonight)) 44 | n_obs = np.size(mag[selector]) 45 | flux = np.mean(mag[selector]) 46 | error = np.std(mag[selector]) / np.sqrt(n_obs) 47 | if error > 0.2: 48 | error = 0 49 | bin_mags.append(flux) 50 | errors.append(error) 51 | # Convert days to digital years 52 | date = datetime.datetime.now() 53 | digi_year = (float(date.strftime("%j"))-1) / 366 + float(date.strftime("%Y")) 54 | days = nights+bin_width/2 55 | years_before = digi_year - (days / 365.2524) 56 | plt.plot(years_before, bin_mags, color='blue', alpha=0.5) 57 | plt.scatter(years_before[0], bin_mags[0], marker="x", color='blue', s=50) 58 | print(bin_mags[0]) 59 | 60 | plt.xlabel('Year') 61 | plt.ylabel('Visual magnitude') 62 | mid = np.median(mag) 63 | plt.ylim(min_plot, max_plot) 64 | plt.xlim(1890, digi_year+5) 65 | date_text = datetime.datetime.now().strftime("%d %b %Y") 66 | plt.text(1893, 1.7, 'AAVSO visual (by-eye) 30-day bins. Update: '+date_text) 67 | plt.savefig(plot_file, bbox_inches='tight', dpi=300) 68 | print('Plot made') 69 | return str(round(bin_mags[0], 2)) # e.g., "1.61" mags 70 | 71 | 72 | baseline_mag = 0.5 73 | plot_file = "longest.png" 74 | filename = 'aavso_vis.csv' 75 | dates, mags = np.loadtxt(filename, unpack=True) 76 | days_ago = np.max(dates) - dates 77 | lumi = make_plot(days_ago, dates, mags) 78 | text = "#Betelgeuse today at its dimmest in 125 years at " + lumi + " mag" 79 | print(text) 80 | tweet(text, plot_file) 81 | -------------------------------------------------------------------------------- /betel20d.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime 3 | from matplotlib import pyplot as plt 4 | from wotan import flatten 5 | from betellib import tweet, build_string, get_mags_from_AAVSO 6 | import requests 7 | from bs4 import BeautifulSoup 8 | from astropy.stats import biweight_location 9 | 10 | 11 | def make_plot(days_ago, dates, mag): 12 | print('Making plot...') 13 | time_span = np.max(dates) - np.min(dates) 14 | flatten_lc, trend_lc = flatten( 15 | days_ago, 16 | mag, 17 | method='lowess', 18 | window_length=time_span/5, 19 | return_trend=True, 20 | ) 21 | plt.scatter(days_ago, mag, s=5, color='blue', alpha=0.5) 22 | plt.plot(days_ago, trend_lc, color='red', linewidth=1) 23 | 24 | flatten_lc1, trend_lc1 = flatten( 25 | days_ago1, 26 | all_mags1, 27 | method='lowess', 28 | window_length=time_span/5, 29 | return_trend=True, 30 | ) 31 | plt.scatter(days_ago1, all_mags1, s=10, color='black', alpha=0.8, marker="x") 32 | plt.plot(days_ago1, trend_lc1, color='red', linewidth=1) 33 | plt.xlabel('Days before today') 34 | plt.ylabel('Visual magnitude') 35 | #mid = biweight_location(mag) 36 | mid = 0.25 37 | plt.ylim(mid-1, mid+1) 38 | plt.xlim(-1, 20) 39 | 40 | plt.gca().invert_yaxis() 41 | plt.gca().invert_xaxis() 42 | date_text = datetime.datetime.now().strftime("%d %b %Y") 43 | data_last24hrs = np.where(days_ago<1) 44 | mean_last24hrs = biweight_location(mag[data_last24hrs]) 45 | lumi = str(format(mean_last24hrs, '.2f')) 46 | plt.text(19.5, mid+1-0.25, "AAVSO observations visual (by-eye) in blue", color='blue') 47 | plt.text(19.5, mid+1-0.15, "AAVSO observations from CCDs in black", color='black') 48 | plt.text(19.5, mid+1-0.05, "LOESS trend in red", color='red') 49 | plt.text(19.5, mid-1+0.1, '#Betelgeuse brightness ' + lumi + " mag on " + date_text + " by @betelbot") 50 | plt.savefig(plot_file, bbox_inches='tight', dpi=300) 51 | print('Done.') 52 | 53 | 54 | def get_mags_from_AAVSO_V(url): 55 | r = requests.get(url) 56 | soup = BeautifulSoup(r.content, 'html.parser') 57 | rows = soup.select('tbody tr') 58 | dates = [] 59 | mags = [] 60 | for row in rows: 61 | string = '' + row.text 62 | string = string.split('\n') 63 | try: 64 | date = float(string[3]) 65 | mag = float(string[5]) 66 | band = string[7] 67 | #print(date, mag, band) 68 | if band == "V": 69 | dates.append(date) 70 | mags.append(mag) 71 | #print(date, mag) 72 | #print(mag) 73 | except: 74 | pass 75 | return np.array(dates), np.array(mags) 76 | 77 | 78 | # CCDs 79 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=dslr+ptg+pep+ccd+visdig&page=' 80 | baseline_mag = 0.5 81 | pages = np.arange(1, 2, 1) 82 | all_dates1 = np.array([]) 83 | all_mags1 = np.array([]) 84 | for page in pages: 85 | url = url_base + str(page) 86 | #print(url) 87 | dates, mags = get_mags_from_AAVSO_V(url) 88 | print(dates, mags) 89 | all_dates1 = np.concatenate((all_dates1, dates)) 90 | all_mags1 = np.concatenate((all_mags1, mags)) 91 | 92 | days_ago1 = np.max(all_dates1) - all_dates1 93 | print(all_dates1, all_mags1) 94 | 95 | 96 | 97 | # Pull the last 10 pages from AAVSO and collate the dates and mags 98 | plot_file = 'plot20d.png' 99 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=vis&page=' 100 | pages = np.arange(1, 10, 1) 101 | all_dates = np.array([]) 102 | all_mags = np.array([]) 103 | for page in pages: 104 | url = url_base + str(page) 105 | print(url) 106 | dates, mags = get_mags_from_AAVSO(url) 107 | all_dates = np.concatenate((all_dates, dates)) 108 | all_mags = np.concatenate((all_mags, mags)) 109 | dates = all_dates 110 | mags = all_mags 111 | days_ago = np.max(dates) - dates 112 | text = build_string(days_ago, mags) 113 | if text is not None: 114 | make_plot(days_ago, dates, mags) 115 | tweet(text, plot_file) 116 | -------------------------------------------------------------------------------- /betel5y.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime 3 | from matplotlib import pyplot as plt 4 | from betellib import tweet, build_string, get_mags_from_AAVSO 5 | 6 | 7 | def make_plot(days_ago, dates, mag): 8 | print('Making plot...') 9 | time_span = np.max(dates) - np.min(dates) 10 | min_plot = 0 11 | max_plot = 1.4 12 | x_days = 2000 13 | 14 | # Make bins 15 | bin_width = 10 16 | nights = np.arange(0, max(days_ago), bin_width) 17 | bin_mags = [] 18 | errors = [] 19 | for night in nights: 20 | selector = np.where((days_agonight)) 21 | n_obs = np.size(mag[selector]) 22 | flux = np.mean(mag[selector]) 23 | error = np.std(mag[selector]) / np.sqrt(n_obs) 24 | if error > 0.2: 25 | error = 0 26 | bin_mags.append(flux) 27 | errors.append(error) 28 | print(night, flux, error, n_obs, np.std(mag[selector])) 29 | 30 | # Convert magnitudes to fluxes 31 | bin_mags = np.array(bin_mags) 32 | flux = 1 / (10**(0.4 * (bin_mags - baseline_mag))) 33 | print(flux) 34 | 35 | # Convert days to digital years 36 | date = datetime.datetime.now() 37 | digi_year = (float(date.strftime("%j"))-1) / 366 + float(date.strftime("%Y")) 38 | days = nights+bin_width/2 39 | years_before = digi_year - (days / 365.2524) 40 | 41 | fig, ax = plt.subplots() 42 | plt.errorbar(years_before, flux, yerr=errors, fmt='.k') 43 | plt.xlabel('Year') 44 | plt.ylabel('Normalized flux') 45 | mid = np.median(mag) 46 | plt.ylim(min_plot, max_plot) 47 | plt.xlim(2015, digi_year+0.25) 48 | date_text = datetime.datetime.now().strftime("%d %b %Y") 49 | plt.text(2015.1, 0.03, 'AAVSO visual (by-eye) 10-day bins. Update: '+date_text) 50 | plt.savefig(plot_file, bbox_inches='tight', dpi=300) 51 | print('Plot made') 52 | 53 | 54 | # Pull the last 10 pages from AAVSO and collate the dates and mags 55 | plot_file = 'plot5y.png' 56 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=vis&page=' 57 | baseline_mag = 0.5 58 | pages = np.arange(1, 25, 1) 59 | all_dates = np.array([]) 60 | all_mags = np.array([]) 61 | for page in pages: 62 | url = url_base + str(page) 63 | print(url) 64 | dates, mags = get_mags_from_AAVSO(url) 65 | all_dates = np.concatenate((all_dates, dates)) 66 | all_mags = np.concatenate((all_mags, mags)) 67 | dates = all_dates 68 | mags = all_mags 69 | days_ago = np.max(dates) - dates 70 | 71 | data_last24hrs = np.where(days_ago<1) 72 | mean_last24hrs = np.median(mags[data_last24hrs]) 73 | flux = 1 / (10**(0.4 * (mean_last24hrs - baseline_mag))) 74 | percentage = str(int(round(flux * 100, 0))) 75 | text = "Now at " + percentage + r"% of my usual brightness! #Betelgeuse" 76 | print(text) 77 | 78 | if text is not None: 79 | make_plot(days_ago, dates, mags) 80 | tweet(text, plot_file) 81 | -------------------------------------------------------------------------------- /betel_ani.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import glob 4 | import PIL 5 | import numpy as np 6 | from PIL import Image, ImageDraw 7 | from matplotlib import pyplot as plt 8 | from astropy.stats import biweight_location 9 | 10 | from transitleastsquares import cleaned_array 11 | from betellib import build_string, get_mags_from_AAVSO, tweet 12 | 13 | from sklearn import gaussian_process 14 | from sklearn.gaussian_process import GaussianProcessRegressor 15 | from sklearn.gaussian_process.kernels import Matern, WhiteKernel, ConstantKernel 16 | 17 | 18 | def make_plot(days_ago, dates, mag): 19 | print('Making plot...') 20 | time_span = np.max(dates) - np.min(dates) 21 | min_plot = -0.5 22 | max_plot = 1.5 23 | x_days = -120 24 | 25 | # Make daily bins 26 | nights = np.arange(0, 120, 1) 27 | daily_mags = [] 28 | errors = [] 29 | for night in nights: 30 | selector = np.where((days_agonight)) 31 | n_obs = np.size(mag[selector]) 32 | flux = biweight_location(mag[selector]) 33 | error = np.std(mag[selector]) / np.sqrt(n_obs) 34 | if error > 0.75: 35 | error = 0 36 | daily_mags.append(flux) 37 | errors.append(error) 38 | print(night, flux, error, n_obs, np.std(mag[selector])) 39 | nights_all = nights.copy() 40 | daily_mags_all = daily_mags.copy() 41 | errors_all = errors.copy() 42 | 43 | lookback = np.arange(1, 20, 1) 44 | 45 | for missing_days in lookback: 46 | nights = nights_all.copy()[missing_days:] 47 | daily_mags = daily_mags_all.copy()[missing_days:] 48 | errors = errors_all.copy()[missing_days:] 49 | plt.errorbar(-(nights+0.5), daily_mags, yerr=errors, fmt='.k', alpha=0.5) 50 | plt.xlabel('Days from today') 51 | plt.ylabel('Visual magnitude') 52 | mid = biweight_location(mag) 53 | plt.ylim(min_plot, max_plot) 54 | plt.xlim(-100, 100) 55 | plt.gca().invert_yaxis() 56 | date_text = datetime.datetime.now().strftime("%d %b %Y") 57 | plt.text(95, min_plot+0.1, 'AAVSO visual (by-eye) daily bins', ha='right') 58 | plt.text(95, min_plot+0.2, 'Gaussian process regression, Matern 3/2 kernel', ha='right') 59 | plt.text(95, min_plot+0.3, '@betelbot update ' + date_text, ha='right') 60 | use_days = 100-missing_days 61 | X = np.array(nights+0.5) 62 | X = X[:use_days] 63 | y = np.array(daily_mags) 64 | y = y[:use_days] 65 | X, y = cleaned_array(X, y) 66 | length_scale = 2 67 | kernel = ConstantKernel() + Matern(length_scale=length_scale, nu=3/2) + WhiteKernel(noise_level=1) 68 | X = X.reshape(-1, 1) 69 | gp = gaussian_process.GaussianProcessRegressor(kernel=kernel) 70 | gp.fit(X, y) 71 | GaussianProcessRegressor(alpha=1e-10, copy_X_train=True, 72 | kernel=1**2 + Matern(length_scale=length_scale, nu=1.5) + WhiteKernel(noise_level=1), 73 | n_restarts_optimizer=0, normalize_y=False, 74 | optimizer='fmin_l_bfgs_b', random_state=None) 75 | x_pred = np.linspace(60, -120, 250).reshape(-1,1) 76 | y_pred, sigma = gp.predict(x_pred, return_std=True) 77 | plt.plot(-x_pred, y_pred, linestyle='dashed', color='blue') 78 | plt.fill_between(-x_pred.ravel(), y_pred+sigma, y_pred-sigma, alpha=0.5) 79 | idx = 20 - missing_days 80 | if idx < 10: 81 | filename = "0" + str(idx) +'.png' 82 | else: 83 | filename = str(idx) +'.png' 84 | 85 | plt.savefig(filename, bbox_inches='tight', dpi=100) 86 | print('Plot made', filename) 87 | plt.clf() 88 | 89 | 90 | # Clear old crap 91 | files = glob.glob('*.png') 92 | for file in files: 93 | os.remove(file) 94 | 95 | # Pull the last 10 pages from AAVSO and collate the dates and mags 96 | plot_file = 'plot120d.png' 97 | url_base = 'https://www.aavso.org/apps/webobs/results/?star=betelgeuse&num_results=200&obs_types=vis&page=' 98 | pages = np.arange(1, 25, 1) 99 | all_dates = np.array([]) 100 | all_mags = np.array([]) 101 | for page in pages: 102 | url = url_base + str(page) 103 | print(url) 104 | dates, mags = get_mags_from_AAVSO(url) 105 | all_dates = np.concatenate((all_dates, dates)) 106 | all_mags = np.concatenate((all_mags, mags)) 107 | dates = all_dates 108 | mags = all_mags 109 | days_ago = np.max(dates) - dates 110 | make_plot(days_ago, dates, mags) 111 | 112 | # Make animation 113 | frames = [] 114 | files = glob.glob('*.png') 115 | files.sort() 116 | for file in files: 117 | print('Appending file', file) 118 | new_frame = PIL.Image.open(file, mode='r') 119 | frames.append(new_frame) 120 | 121 | # Make last frame last longer 122 | for i in range(10): 123 | print(file) 124 | frames.append(new_frame) 125 | 126 | # Save gif 127 | frames[0].save( 128 | 'betel_video.gif', 129 | format='GIF', 130 | append_images=frames, 131 | save_all=True, 132 | duration=500, 133 | optimize=True, 134 | loop=0) # forever 135 | 136 | for file in files: 137 | os.remove(file) 138 | 139 | tweet("Updated #Betelgeuse forecast", 'betel_video.gif') 140 | -------------------------------------------------------------------------------- /betel_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hippke/betelbot/1e08a8e2ba17b8e7f0f13701b1a6d7023a670546/betel_video.gif -------------------------------------------------------------------------------- /betellib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import numpy as np 4 | from twython import Twython 5 | from bs4 import BeautifulSoup 6 | from astropy.stats import biweight_location 7 | 8 | 9 | consumer_key = os.environ.get('consumer_key') 10 | consumer_secret = os.environ.get('consumer_secret') 11 | access_token = os.environ.get('access_token') 12 | access_token_secret = os.environ.get('access_token_secret') 13 | 14 | 15 | def tweet(text, image): 16 | print('Tweeting...') 17 | twitter = Twython(consumer_key, consumer_secret, access_token, access_token_secret) 18 | response = twitter.upload_media(media=open(image, 'rb')) 19 | twitter.update_status(status=text, media_ids=[response['media_id']]) 20 | print("Done.") 21 | 22 | 23 | def build_string(days_ago, mag): 24 | print('Building string...') 25 | data_last24hrs = np.where(days_ago<1) 26 | data_last1_6_days = np.where((days_ago<6) & (days_ago>1)) 27 | n_obs_last24hrs = np.size(mag[data_last24hrs]) 28 | n_obs_last1_6_days = np.size(mag[data_last1_6_days]) 29 | mean_last24hrs = biweight_location(mag[data_last24hrs]) 30 | mean_last1_6_days = biweight_location(mag[data_last1_6_days]) 31 | stdev = np.std(mag[data_last24hrs]) / np.sqrt(n_obs_last24hrs) \ 32 | + np.std(mag[data_last1_6_days]) / np.sqrt(n_obs_last1_6_days) 33 | diff = mean_last24hrs - mean_last1_6_days 34 | sigma = diff / stdev 35 | 36 | if n_obs_last24hrs < 1 or n_obs_last1_6_days < 1: 37 | return "No new observations last night" 38 | else: 39 | 40 | if diff > 0: 41 | changeword = 'dimmer' 42 | else: 43 | changeword = 'brighter' 44 | 45 | mag_text = "My visual mag from last night was " + \ 46 | str(format(mean_last24hrs, '.2f')) + \ 47 | ' (robust mean of ' + \ 48 | str(n_obs_last24hrs) + \ 49 | ' observations). ' 50 | 51 | change_text = 'That is ' + \ 52 | format(abs(diff), '.2f') + \ 53 | ' mag ' + \ 54 | changeword + \ 55 | ' than the robust mean of the 5 previous nights (n=' + \ 56 | str(n_obs_last1_6_days) + \ 57 | ', ' + \ 58 | format(abs(sigma), '.1f') + \ 59 | 'σ). #Betelgeuse' 60 | 61 | text = mag_text + change_text 62 | print(text) 63 | return text 64 | 65 | 66 | 67 | def get_mags_from_AAVSO(url): 68 | r = requests.get(url) 69 | soup = BeautifulSoup(r.content, 'html.parser') 70 | rows = soup.select('tbody tr') 71 | dates = [] 72 | mags = [] 73 | for row in rows: 74 | string = '' + row.text 75 | string = string.split('\n') 76 | try: 77 | date = float(string[3]) 78 | mag = float(string[5]) 79 | print(date, mag) 80 | # Remove crap 81 | if mag < 3 and date > 1000000: 82 | dates.append(date) 83 | mags.append(mag) 84 | except: 85 | pass 86 | return np.array(dates), np.array(mags) 87 | -------------------------------------------------------------------------------- /plot120d_flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hippke/betelbot/1e08a8e2ba17b8e7f0f13701b1a6d7023a670546/plot120d_flux.png -------------------------------------------------------------------------------- /plot20d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hippke/betelbot/1e08a8e2ba17b8e7f0f13701b1a6d7023a670546/plot20d.png --------------------------------------------------------------------------------