File size: 7,871 Bytes
80f8293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""Dataset generation functions for testing BackpropNEAT."""

import numpy as np
import jax.numpy as jnp

def generate_xor_data(n_samples: int = 200, complexity: float = 1.0) -> tuple:
    """Generate complex XOR dataset with multiple clusters and rotations.
    
    Args:
        n_samples: Number of samples per quadrant
        complexity: Controls the complexity of the pattern (rotation and noise)
        
    Returns:
        Tuple of (features, labels)
    """
    points = []
    labels = []
    
    # Generate multiple clusters per quadrant
    n_clusters = 3
    samples_per_cluster = n_samples // n_clusters
    
    for cluster in range(n_clusters):
        # Add rotation to each subsequent cluster
        rotation = complexity * cluster * np.pi / 6  # 30 degree rotation per cluster
        
        # Define cluster centers with gaps
        centers = [
            # (x, y, radius, label)
            (-0.7 - 0.3*cluster, -0.7 - 0.3*cluster, 0.2, -1),  # Bottom-left
            (0.7 + 0.3*cluster, 0.7 + 0.3*cluster, 0.2, -1),   # Top-right
            (-0.7 - 0.3*cluster, 0.7 + 0.3*cluster, 0.2, 1),   # Top-left
            (0.7 + 0.3*cluster, -0.7 - 0.3*cluster, 0.2, 1),   # Bottom-right
        ]
        
        for cx, cy, radius, label in centers:
            # Generate points in a circle around center
            theta = np.random.uniform(0, 2*np.pi, samples_per_cluster)
            r = np.random.uniform(0, radius, samples_per_cluster)
            
            # Convert to cartesian coordinates
            x = r * np.cos(theta)
            y = r * np.sin(theta)
            
            # Apply rotation
            x_rot = x * np.cos(rotation) - y * np.sin(rotation)
            y_rot = x * np.sin(rotation) + y * np.cos(rotation)
            
            # Add cluster center and noise
            x = cx + x_rot + np.random.normal(0, 0.05, samples_per_cluster)
            y = cy + y_rot + np.random.normal(0, 0.05, samples_per_cluster)
            
            # Add points
            cluster_points = np.column_stack([x, y])
            points.append(cluster_points)
            labels.extend([label] * samples_per_cluster)
    
    # Convert to arrays
    X = np.vstack(points)
    y = np.array(labels, dtype=np.float32)
    
    # Add global rotation
    theta = complexity * np.pi / 4  # 45 degree global rotation
    rotation_matrix = np.array([
        [np.cos(theta), -np.sin(theta)],
        [np.sin(theta), np.cos(theta)]
    ])
    X = X @ rotation_matrix
    
    # Shuffle data
    perm = np.random.permutation(len(X))
    X = X[perm]
    y = y[perm]
    
    return jnp.array(X), jnp.array(y)

def generate_circle_data(n_samples: int = 1000, noise: float = 0.1) -> tuple:
    """Generate circle classification dataset.
    
    Args:
        n_samples: Number of samples per class
        noise: Standard deviation of Gaussian noise
        
    Returns:
        Tuple of (features, labels)
    """
    # Generate random angles
    theta = np.random.uniform(0, 2*np.pi, n_samples)
    
    # Inner circle (class -1)
    r_inner = 0.5 + np.random.normal(0, noise, n_samples)
    X_inner = np.column_stack([
        r_inner * np.cos(theta),
        r_inner * np.sin(theta)
    ])
    y_inner = np.full(n_samples, -1.0)
    
    # Outer circle (class 1)
    r_outer = 1.5 + np.random.normal(0, noise, n_samples)
    X_outer = np.column_stack([
        r_outer * np.cos(theta),
        r_outer * np.sin(theta)
    ])
    y_outer = np.full(n_samples, 1.0)
    
    # Combine and shuffle
    X = np.vstack([X_inner, X_outer])
    y = np.hstack([y_inner, y_outer])
    
    # Shuffle
    perm = np.random.permutation(len(X))
    return X[perm], y[perm]

def generate_spiral_dataset(n_points=1000, noise=0.1):
    """Generate a spiral dataset with rotation-invariant features."""
    # Generate theta values with more points near the center
    theta = np.sqrt(np.random.uniform(0, 1, n_points)) * 4 * np.pi
    
    # Generate two spirals
    data = []
    labels = []
    eps = 1e-8
    
    for i in range(n_points):
        # Base radius increases with theta
        r_base = theta[i] / (4 * np.pi)
        
        # Add noise that scales with radius
        noise_scale = noise * (1 - np.exp(-2 * r_base))
        
        for spiral_idx in range(2):
            # Rotate second spiral by pi
            angle = theta[i] + np.pi * spiral_idx
            
            # Add controlled noise to radius and angle
            r = r_base + np.random.normal(0, noise_scale)
            angle_noise = np.random.normal(0, noise_scale * 0.1)  # Less noise in angle
            angle += angle_noise
            
            # Calculate cartesian coordinates
            x = r * np.cos(angle)
            y = r * np.sin(angle)
            
            # Calculate polar coordinates
            r_point = np.sqrt(x*x + y*y)
            theta_point = np.arctan2(y, x)
            
            # Unwrap theta to handle multiple revolutions
            theta_unwrapped = theta_point + 2 * np.pi * (angle // (2 * np.pi))
            
            # Calculate spiral-specific features
            
            # 1. Local curvature (how much the spiral curves at this point)
            curvature = 1 / (r_point + eps)
            
            # 2. Spiral phase (position along spiral revolution)
            phase = theta_unwrapped % (2 * np.pi) / (2 * np.pi)
            
            # 3. Radial velocity (how fast radius changes with angle)
            dr_dtheta = 1 / (4 * np.pi)
            
            # 4. Normalized angular position (accounts for multiple revolutions)
            angular_pos = theta_unwrapped / (4 * np.pi)
            
            # 5. Spiral tightness (local measure of how tight the spiral is)
            tightness = r_point / (theta_unwrapped + eps)
            
            # 6. Relative position features (help distinguish between spirals)
            # Distance to other spiral
            other_angle = angle + np.pi
            other_x = r * np.cos(other_angle)
            other_y = r * np.sin(other_angle)
            dist_to_other = np.sqrt((x - other_x)**2 + (y - other_y)**2)
            
            # 7. Rotation-invariant features
            sin_phase = np.sin(phase * 2 * np.pi)
            cos_phase = np.cos(phase * 2 * np.pi)
            
            # Combine features with careful normalization
            features = np.array([
                x / 2.0,  # Normalize coordinates
                y / 2.0,
                r_point / 2.0,  # Normalize radius
                sin_phase,  # Already normalized
                cos_phase,  # Already normalized
                np.tanh(curvature * 2),  # Normalize curvature
                angular_pos / 2.0,  # Normalize angular position
                np.tanh(tightness),  # Normalize tightness
                np.tanh(dr_dtheta * 10),  # Normalize radial velocity
                dist_to_other / 4.0  # Normalize distance to other spiral
            ])
            
            data.append(features)
            labels.append(spiral_idx * 2 - 1)  # Convert to [-1, 1]
    
    return np.array(data), np.array(labels)

def generate_checkerboard_data(n_samples: int = 200) -> tuple:
    """Generate checkerboard dataset.
    
    Args:
        n_samples: Number of samples per class
        
    Returns:
        Tuple of (features, labels)
    """
    # Generate random points
    X = np.random.uniform(-2, 2, (n_samples * 2, 2))
    
    # Assign labels based on checkerboard pattern
    y = np.zeros(n_samples * 2)
    for i in range(len(X)):
        x1, x2 = X[i]
        y[i] = 1 if (int(np.floor(x1)) + int(np.floor(x2))) % 2 == 0 else 0
    
    return jnp.array(X), jnp.array(y)

# Export dataset functions
__all__ = ['generate_xor_data', 'generate_circle_data', 'generate_spiral_dataset', 
           'generate_checkerboard_data']