├── .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 | 
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
--------------------------------------------------------------------------------