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).")