Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple webapp to play with different parameters #715

Merged
merged 2 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/deploy-webapp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Simple workflow for deploying WebAssembly app to GitHub Pages
name: Deploy WebAssembly app to Pages

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
pull_request:
branches: ["main"]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Set Up Python
uses: actions/setup-python@v2
with:
python-version: 3.x

- name: Install Dependencies
run: |
pip install -r webapp/requirements.txt

- name: Build the WebAssembly app
run: |
shinylive export webapp site

- name: Setup Pages
uses: actions/configure-pages@v3

- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
# Upload the 'site' directory, where your app has been built
path: "site"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ ignore = [
[per-file-ignores]
"tests/*.py" = ["ALL"]
".github/*py" = ["INP001"]
"webapp/*py" = ["ALL"]

[flake8-pytest-style]
fixture-parentheses = false
Expand Down
268 changes: 268 additions & 0 deletions webapp/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
"""Simple web app to visualize brightness over time."""

import math

import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui


def lerp(x, x1, x2, y1, y2):
"""Linearly interpolate between two values."""
return y1 + (x - x1) * (y2 - y1) / (x2 - x1)


def clamp(value: float, minimum: float, maximum: float) -> float:
"""Clamp value between minimum and maximum."""
return max(minimum, min(value, maximum))


def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
b = x1 - (math.atanh(2 * y1 - 1) / a)
return a, b


def scaled_tanh(
x: float,
a: float,
b: float,
y_min: float = 0.0,
y_max: float = 1.0,
) -> float:
"""Apply a scaled and shifted tanh function to a given input."""
return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)


def is_closer_to_sunrise_than_sunset(time, sunrise_time, sunset_time):
"""Return True if the time is closer to sunrise than sunset."""
return abs(time - sunrise_time) < abs(time - sunset_time)


def brightness_linear(
time,
sunrise_time,
sunset_time,
time_light,
time_dark,
max_brightness,
min_brightness,
):
"""Calculate the brightness for the 'linear' mode."""
closer_to_sunrise = is_closer_to_sunrise_than_sunset(
time,
sunrise_time,
sunset_time,
)
if closer_to_sunrise:
brightness = lerp(
time,
x1=sunrise_time - time_dark,
x2=sunrise_time + time_light,
y1=min_brightness,
y2=max_brightness,
)
else:
brightness = lerp(
time,
x1=sunset_time - time_light,
x2=sunset_time + time_dark,
y1=max_brightness,
y2=min_brightness,
)
return clamp(brightness, min_brightness, max_brightness)


def brightness_tanh(
time,
sunrise_time,
sunset_time,
time_light,
time_dark,
max_brightness,
min_brightness,
):
"""Calculate the brightness for the 'tanh' mode."""
closer_to_sunrise = is_closer_to_sunrise_than_sunset(
time,
sunrise_time,
sunset_time,
)
if closer_to_sunrise:
a, b = find_a_b(
x1=-time_dark,
x2=time_light,
y1=0.05, # be at 5% of range at x1
y2=0.95, # be at 95% of range at x2
)
brightness = scaled_tanh(
time - sunrise_time,
a=a,
b=b,
y_min=min_brightness,
y_max=max_brightness,
)
else:
a, b = find_a_b(
x1=-time_light, # shifted timestamp for the start of sunset
x2=time_dark, # shifted timestamp for the end of sunset
y1=0.95, # be at 95% of range at the start of sunset
y2=0.05, # be at 5% of range at the end of sunset
)
brightness = scaled_tanh(
time - sunset_time,
a=a,
b=b,
y_min=min_brightness,
y_max=max_brightness,
)
return clamp(brightness, min_brightness, max_brightness)


SEC_PER_HR = 60 * 60

# Shiny UI
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"),
ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"),
ui.input_slider(
"dark_time",
"brightness_mode_time_dark",
0,
5 * SEC_PER_HR,
3 * SEC_PER_HR,
post=" sec",
),
ui.input_slider(
"light_time",
"brightness_mode_time_light",
0,
5 * SEC_PER_HR,
0.5 * SEC_PER_HR,
post=" sec",
),
ui.input_slider(
"sunrise_time",
"sunrise_time",
0,
24,
6,
step=0.5,
post=" hr",
),
ui.input_slider(
"sunset_time",
"sunset_time",
0,
24,
18,
step=0.5,
post=" hr",
),
),
ui.panel_main(ui.output_plot(id="brightness_plot")),
),
)


def server(input, output, session):
@output
@render.plot
def brightness_plot():
return plot_brightness(
min_brightness=input.min_brightness() / 100,
max_brightness=input.max_brightness() / 100,
brightness_mode_time_dark=input.dark_time() / SEC_PER_HR,
brightness_mode_time_light=input.light_time() / SEC_PER_HR,
sunrise_time=input.sunrise_time(),
sunset_time=input.sunset_time(),
)


def plot_brightness(
min_brightness,
max_brightness,
brightness_mode_time_dark,
brightness_mode_time_light,
sunrise_time=6, # 6 AM
sunset_time=18, # 6 PM
):
# Define the time range for our simulation
time_range = np.linspace(0, 24, 1000) # From 0 to 24 hours

# Calculate the brightness for each time in the time range for both modes
brightness_linear_values = [
brightness_linear(
time,
sunrise_time,
sunset_time,
brightness_mode_time_light,
brightness_mode_time_dark,
max_brightness,
min_brightness,
)
for time in time_range
]
brightness_tanh_values = [
brightness_tanh(
time,
sunrise_time,
sunset_time,
brightness_mode_time_light,
brightness_mode_time_dark,
max_brightness,
min_brightness,
)
for time in time_range
]

# Plot the brightness over time for both modes
plt.figure(figsize=(10, 6))
plt.plot(time_range, brightness_linear_values, label="Linear Mode")
plt.plot(time_range, brightness_tanh_values, label="Tanh Mode")
plt.vlines(sunrise_time, 0, 1, color="C2", label="Sunrise", linestyles="dashed")
plt.vlines(sunset_time, 0, 1, color="C3", label="Sunset", linestyles="dashed")
plt.xlim(0, 24)
plt.xticks(np.arange(0, 25, 1))
yticks = np.arange(0, 1.05, 0.05)
ytick_labels = [f"{100*label:.0f}%" for label in yticks]
plt.yticks(yticks, ytick_labels)
plt.xlabel("Time (hours)")
plt.ylabel("Brightness")
plt.title("Brightness over Time for Different Modes")

# Add text box
textstr = "\n".join(
(
f"Sunrise Time = {sunrise_time}:00:00",
f"Sunset Time = {sunset_time}:00:00",
f"Max Brightness = {max_brightness*100:.0f}%",
f"Min Brightness = {min_brightness*100:.0f}%",
f"Time Light = {brightness_mode_time_light:.1f} hours",
f"Time Dark = {brightness_mode_time_dark:.1f} hours",
),
)

# these are matplotlib.patch.Patch properties
props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}

plt.legend()
plt.grid(True)

# place a text box in upper left in axes coords
plt.gca().text(
0.4,
0.55,
textstr,
transform=plt.gca().transAxes,
fontsize=10,
verticalalignment="center",
bbox=props,
)

return plt.gcf()


app = App(app_ui, server)
1 change: 1 addition & 0 deletions webapp/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shinylive
Loading