File size: 7,113 Bytes
09ae054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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).")