mod-30-lab / app.py
dan9423's picture
Create app.py
09ae054 verified
Raw
History Blame Contribute Delete
7.11 kB
import streamlit as st
import math
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# --- Core Mathematical Functions (The "Engineer" part) ---
# These are the exact functions we will later move to a separate toolkit.
# They ensure our Space, dataset, and reports all agree.
def mod_30(n: int) -> int:
"""Return n modulo 30."""
return n % 30
def coprime_to_30(n: int) -> bool:
"""Return True if n is coprime to 30 (i.e., gcd(n, 30) == 1)."""
return math.gcd(n, 30) == 1
def residue_lane(n: int) -> str:
"""Classify n based on its residue modulo 30 into one of the four 'lanes'."""
r = n % 30
if r in {1, 7, 11, 13, 17, 19, 23, 29}:
return "πŸ”΅ Coprime Lane (the eight residue classes)"
elif r % 2 == 0:
return "πŸ”΄ Divisible by 2"
elif r % 3 == 0:
return "🟠 Divisible by 3"
elif r % 5 == 0:
return "🟒 Divisible by 5"
else:
# This case should theoretically not occur (r can only be 0-29)
return "βšͺ Other"
def prime_status(n: int) -> str:
"""A simple (but inefficient) primality test for small numbers."""
if n < 2:
return "Neither prime nor composite"
# Trial division up to sqrt(n)
for i in range(2, int(math.isqrt(n)) + 1):
if n % i == 0:
return "Composite"
return "Prime"
# --- Streamlit App User Interface ---
st.set_page_config(page_title="Mod-30 Laboratory", page_icon="πŸ§ͺ")
st.title("πŸ§ͺ Mod-30 Laboratory")
st.markdown("Explore the mathematical structure of integers through the lens of modulo 30 and the eight coprime residue classes.")
st.divider()
# --- Sidebar: Context ---
with st.sidebar:
st.header("About the Eight Residues")
st.write(
"""
The set **{1, 7, 11, 13, 17, 19, 23, 29}** are the integers less than 30
that are coprime to 30 (they share no common factors with 2, 3, or 5).
Any integer's residue modulo 30 tells us immediately:
- If it's in this set, it is *not* divisible by 2, 3, or 5.
- Otherwise, it falls into a 'divisible by' lane.
*Remember: Being in the coprime lane is necessary but not sufficient for a number to be prime (e.g., 49 is composite but coprime to 30).*
"""
)
st.divider()
st.caption("Data and code are reproducible. See our dataset at readingpoint/mod-30-observations.")
# --- Main App Tabs: Single Query & Range Visualizer ---
tab1, tab2 = st.tabs(["πŸ”Ž Single Number Query", "πŸ“Š Range Visualizer"])
# --- TAB 1: Single Query ---
with tab1:
st.header("Analyze a Single Integer")
# Input
user_input = st.number_input(
"Enter an integer:",
value=137,
step=1,
format="%d"
)
if st.button("Analyze", type="primary"):
n = int(user_input)
r = mod_30(n)
lane = residue_lane(n)
prime = prime_status(n)
# Display Results
col1, col2, col3 = st.columns(3)
with col1:
st.metric(label="Modulo 30", value=r)
with col2:
st.metric(label="Classification", value=lane, help="The 'lane' the number falls into.")
with col3:
st.metric(label="Primality", value=prime)
# Visual Wheel
st.subheader("Residue Wheel")
fig, ax = plt.subplots(figsize=(6, 6))
# Create a circle of residues 0-29
angles = np.linspace(0, 2 * np.pi, 30, endpoint=False)
# Color mapping
colors = []
for i in range(30):
if i in {1, 7, 11, 13, 17, 19, 23, 29}:
colors.append('#1f77b4') # Blue for coprime
elif i % 2 == 0:
colors.append('#d62728') # Red for even
elif i % 3 == 0:
colors.append('#ff7f0e') # Orange for divisible by 3
elif i % 5 == 0:
colors.append('#2ca02c') # Green for divisible by 5
else:
colors.append('#7f7f7f') # Grey
# Highlight the selected residue
highlight = ['gold' if i == r else colors[i] for i in range(30)]
# Plot as a bar chart wrapped around a circle (polar plot)
ax = plt.subplot(111, projection='polar')
bars = ax.bar(angles, [1]*30, width=2*np.pi/30, color=highlight, alpha=0.7, edgecolor='black', linewidth=0.5)
ax.set_xticks(angles)
ax.set_xticklabels([str(i) for i in range(30)], fontsize=8)
ax.set_yticklabels([])
ax.set_title(f"Residue {r} Highlighted in Gold", va='bottom')
st.pyplot(fig)
st.info(f"**{n}** is in the '{lane}' and is **{prime}**.", icon="πŸ’‘")
# --- TAB 2: Range Visualizer ---
with tab2:
st.header("Visualize a Range of Integers")
col1, col2 = st.columns(2)
with col1:
start_val = st.number_input("Start of range:", value=1, step=1)
with col2:
end_val = st.number_input("End of range:", value=100, step=1, min_value=start_val+1)
if st.button("Generate Visualization", type="primary"):
n_range = list(range(int(start_val), int(end_val)+1))
residues = [mod_30(n) for n in n_range]
lanes = [residue_lane(n) for n in n_range]
primes = [prime_status(n) for n in n_range]
df = pd.DataFrame({
'Integer': n_range,
'mod_30': residues,
'Lane': lanes,
'Primality': primes
})
st.subheader("Data Preview")
st.dataframe(df.head(50), use_container_width=True)
# Scatter Plot: Integer vs Residue, colored by Lane
st.subheader("Residue Scatter Plot")
fig2, ax2 = plt.subplots(figsize=(10, 6))
# Create a color map for lanes
lane_colors = {
'πŸ”΅ Coprime Lane (the eight residue classes)': 'blue',
'πŸ”΄ Divisible by 2': 'red',
'🟠 Divisible by 3': 'orange',
'🟒 Divisible by 5': 'green',
'βšͺ Other': 'gray'
}
color_list = [lane_colors.get(lane, 'black') for lane in lanes]
scatter = ax2.scatter(n_range, residues, c=color_list, alpha=0.7)
ax2.set_xlabel('Integer (n)')
ax2.set_ylabel('n mod 30')
ax2.set_title('Residue Distribution Across the Range')
ax2.grid(True, linestyle='--', alpha=0.5)
ax2.set_yticks(range(0, 30, 5))
# Create a custom legend
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='blue', label='πŸ”΅ Coprime Lane'),
Patch(facecolor='red', label='πŸ”΄ Divisible by 2'),
Patch(facecolor='orange', label='🟠 Divisible by 3'),
Patch(facecolor='green', label='🟒 Divisible by 5'),
]
ax2.legend(handles=legend_elements, title='Lane')
st.pyplot(fig2)
st.divider()
st.caption("Built with Streamlit. Explore the full dataset at [readingpoint/mod-30-observations](https://huggingface.co/datasets/readingpoint/mod-30-observations).")