Spaces:
Paused
Paused
| 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).") |