Files
f1-circuits/circuits/rotation_calculation.py
T

969 lines
29 KiB
Python

import numpy as np
import matplotlib.pyplot as plt
data = {
"type": "FeatureCollection",
"name": "hu-1986",
"bbox": [
19.242326,
47.577571,
19.256609,
47.588474
],
"features": [
{
"type": "Feature",
"properties": {
"id": "hu-1986",
"Location": "Budapest",
"Name": "Hungaroring",
"opened": 1986,
"firstgp": 1986,
"length": 4381,
"altitude": 239
},
"bbox": [
19.242326,
47.577571,
19.256609,
47.588474
],
"geometry": {
"type": "LineString",
"coordinates": [
[
19.245888,
47.58026
],
[
19.243226,
47.581696
],
[
19.242439,
47.582111
],
[
19.242368,
47.582191
],
[
19.242326,
47.582271
],
[
19.242338,
47.582361
],
[
19.242391,
47.582431
],
[
19.242468,
47.582478
],
[
19.242569,
47.582511
],
[
19.242717,
47.582526
],
[
19.242936,
47.582526
],
[
19.243546,
47.582516
],
[
19.243954,
47.582469
],
[
19.244327,
47.582394
],
[
19.244718,
47.58229
],
[
19.245014,
47.582177
],
[
19.245238,
47.582073
],
[
19.247411,
47.580913
],
[
19.24757,
47.580857
],
[
19.247724,
47.580833
],
[
19.247878,
47.580833
],
[
19.248003,
47.580862
],
[
19.248139,
47.580895
],
[
19.248245,
47.580951
],
[
19.248334,
47.581017
],
[
19.248393,
47.581078
],
[
19.248446,
47.581163
],
[
19.248464,
47.581281
],
[
19.248458,
47.581347
],
[
19.248411,
47.581437
],
[
19.24837,
47.581526
],
[
19.247748,
47.582238
],
[
19.247683,
47.58237
],
[
19.247671,
47.582474
],
[
19.247707,
47.582563
],
[
19.247754,
47.582676
],
[
19.247813,
47.58278
],
[
19.2485,
47.583704
],
[
19.249719,
47.585368
],
[
19.249938,
47.585632
],
[
19.250193,
47.585863
],
[
19.250512,
47.586113
],
[
19.250577,
47.586183
],
[
19.250625,
47.586254
],
[
19.250636,
47.586334
],
[
19.250601,
47.586424
],
[
19.250045,
47.587838
],
[
19.250033,
47.587946
],
[
19.25005,
47.58805
],
[
19.250092,
47.588154
],
[
19.250157,
47.588253
],
[
19.250275,
47.588333
],
[
19.250417,
47.588404
],
[
19.250565,
47.588455
],
[
19.250713,
47.588474
],
[
19.250897,
47.58847
],
[
19.251074,
47.588441
],
[
19.251288,
47.58838
],
[
19.25153,
47.588276
],
[
19.251702,
47.588196
],
[
19.251903,
47.588097
],
[
19.252116,
47.587984
],
[
19.252383,
47.587796
],
[
19.253519,
47.586966
],
[
19.253572,
47.586886
],
[
19.25359,
47.586824
],
[
19.253584,
47.586758
],
[
19.253525,
47.586702
],
[
19.25343,
47.586641
],
[
19.253353,
47.586589
],
[
19.253324,
47.586542
],
[
19.25333,
47.586457
],
[
19.253359,
47.586363
],
[
19.253773,
47.585311
],
[
19.253862,
47.58517
],
[
19.253927,
47.585113
],
[
19.253992,
47.585062
],
[
19.254081,
47.585014
],
[
19.254188,
47.584991
],
[
19.254389,
47.584963
],
[
19.255247,
47.584878
],
[
19.255425,
47.584831
],
[
19.255537,
47.584779
],
[
19.255632,
47.584727
],
[
19.255715,
47.584637
],
[
19.255744,
47.584557
],
[
19.25578,
47.584463
],
[
19.25578,
47.584359
],
[
19.25575,
47.584246
],
[
19.255537,
47.583233
],
[
19.255531,
47.583053
],
[
19.255549,
47.58294
],
[
19.255596,
47.582823
],
[
19.255662,
47.582709
],
[
19.256502,
47.581804
],
[
19.256585,
47.581668
],
[
19.256603,
47.581573
],
[
19.256609,
47.581484
],
[
19.256591,
47.58139
],
[
19.256561,
47.581295
],
[
19.256502,
47.58121
],
[
19.256407,
47.581126
],
[
19.254963,
47.580032
],
[
19.253679,
47.579028
],
[
19.253241,
47.578698
],
[
19.253134,
47.578656
],
[
19.253034,
47.578646
],
[
19.252939,
47.578656
],
[
19.252856,
47.578689
],
[
19.252773,
47.57874
],
[
19.251897,
47.57941
],
[
19.251755,
47.579504
],
[
19.251086,
47.579872
],
[
19.250968,
47.579914
],
[
19.25085,
47.579947
],
[
19.250707,
47.579961
],
[
19.250583,
47.579952
],
[
19.250417,
47.5799
],
[
19.250317,
47.579825
],
[
19.250246,
47.579744
],
[
19.250204,
47.579674
],
[
19.250193,
47.579579
],
[
19.25021,
47.579485
],
[
19.250269,
47.579396
],
[
19.250346,
47.579325
],
[
19.251666,
47.578608
],
[
19.251909,
47.578458
],
[
19.25198,
47.578377
],
[
19.252033,
47.578264
],
[
19.252057,
47.578132
],
[
19.252039,
47.57801
],
[
19.25198,
47.577906
],
[
19.251909,
47.577807
],
[
19.251797,
47.577722
],
[
19.251613,
47.577642
],
[
19.251424,
47.57759
],
[
19.251217,
47.577571
],
[
19.250998,
47.57759
],
[
19.250802,
47.577642
],
[
19.250619,
47.577727
],
[
19.245888,
47.58026
]
]
}
}
]
}
def rotate(xy, *, angle):
"""Rotate coordinates by the given angle."""
rot_mat = np.array([[np.cos(angle), np.sin(angle)],
[-np.sin(angle), np.cos(angle)]])
return np.matmul(xy, rot_mat)
def calculate_aspect_ratio_rotation(coordinates, preferred_ratio=1.618): # Golden ratio by default
"""Find rotation that gives closest match to desired aspect ratio."""
best_rotation = 0
best_ratio_diff = float('inf')
for angle in range(0, 180, 5): # Check every 5 degrees
rad_angle = np.radians(angle)
rotated = rotate(coordinates, angle=rad_angle)
# Calculate bounding box
x_coords = [p[0] for p in rotated]
y_coords = [p[1] for p in rotated]
width = max(x_coords) - min(x_coords)
height = max(y_coords) - min(y_coords)
current_ratio = width / height
ratio_diff = abs(current_ratio - preferred_ratio)
if ratio_diff < best_ratio_diff:
best_ratio_diff = ratio_diff
best_rotation = angle
return best_rotation
def calculate_minimal_area_rotation(coordinates):
"""Find rotation that minimizes the bounding box area."""
best_rotation = 0
min_area = float('inf')
for angle in range(0, 180, 5):
rad_angle = np.radians(angle)
rotated = rotate(coordinates, angle=rad_angle)
x_coords = [p[0] for p in rotated]
y_coords = [p[1] for p in rotated]
width = max(x_coords) - min(x_coords)
height = max(y_coords) - min(y_coords)
area = width * height
if area < min_area:
min_area = area
best_rotation = angle
return best_rotation
def calculate_pca_rotation(coordinates):
"""Use PCA to align the track with its principal axes."""
from sklearn.decomposition import PCA
# Convert coordinates to numpy array if not already
coords_array = np.array(coordinates)
# Fit PCA
pca = PCA(n_components=2)
pca.fit(coords_array)
# Calculate rotation angle from first principal component
first_component = pca.components_[0]
angle = np.arctan2(first_component[1], first_component[0])
return np.degrees(angle)
def calculate_start_straight_rotation(coordinates, straight_length=10):
"""Align the track so the start/finish straight is vertical/horizontal."""
# Assuming first points are from start/finish straight
start_points = coordinates[:straight_length]
# Calculate direction vector of the straight
dx = start_points[-1][0] - start_points[0][0]
dy = start_points[-1][1] - start_points[0][1]
# Calculate angle to horizontal
angle = np.degrees(np.arctan2(dy, dx))
# Return rotation needed to align with horizontal (0°) or vertical (90°)
horizontal_rotation = -angle
vertical_rotation = 90 - angle
# Return whichever requires less rotation
return horizontal_rotation if abs(horizontal_rotation) < abs(vertical_rotation) else vertical_rotation
def find_longest_straight(coordinates, window_size=5):
"""Find the longest approximately straight section of the track, including wrap-around."""
max_distance = 0
best_start_idx = 0
n = len(coordinates)
# Helper function to check straightness
def is_straight(points, start, end, length):
direction = (end - start) / length
distances = []
for point in points:
projection = start + np.dot(point - start, direction) * direction
distance = np.linalg.norm(point - projection)
distances.append(distance)
return max(distances) < length * 0.05 # 5% tolerance
# Check all possible segments, including wrap-around
for i in range(n):
# Get window_size points, handling wrap-around
segment = []
for j in range(window_size):
idx = (i + j) % n
segment.append(np.array(coordinates[idx]))
start = np.array(segment[0])
end = np.array(segment[-1])
length = np.linalg.norm(end - start)
if length > max_distance:
# Check if all points are roughly on the line
if is_straight(segment, start, end, length):
max_distance = length
best_start_idx = i
# Return indices that might wrap around
end_idx = (best_start_idx + window_size) % len(coordinates)
return best_start_idx, end_idx
def calculate_longest_straight_rotation(coordinates):
"""Find the rotation that places the longest straight section horizontally at the bottom."""
# First find the longest straight section
start_idx, end_idx = find_longest_straight(coordinates)
best_angle = 0
min_y_diff = float('inf')
coordinates_array = np.array(coordinates)
# Create a figure for visualization
plt.figure(figsize=(15, 5))
# Try angles in smaller increments for more precision
for angle in np.linspace(0, 2*np.pi, 72): # 5-degree increments
# Rotate the entire track
rotated_track = rotate(coordinates_array, angle=angle)
# Get y-coordinates of the straight section
straight_y1 = rotated_track[start_idx][1]
straight_y2 = rotated_track[end_idx][1]
y_diff = abs(straight_y1 - straight_y2)
# If this is the best rotation so far, show it
if y_diff < min_y_diff:
min_y_diff = y_diff
best_angle = angle
# Clear previous plots
plt.clf()
# Create three subplots
plt.subplot(131)
plt.title('Original Track')
plt.plot(coordinates_array[:, 0], coordinates_array[:, 1], 'k-')
plt.plot([coordinates_array[start_idx][0], coordinates_array[end_idx][0]],
[coordinates_array[start_idx][1], coordinates_array[end_idx][1]], 'r-', linewidth=2)
plt.axis('equal')
plt.subplot(132)
plt.title(f'Current Rotation ({angle:.1f} rad)')
plt.plot(rotated_track[:, 0], rotated_track[:, 1], 'k-')
plt.plot([rotated_track[start_idx][0], rotated_track[end_idx][0]],
[rotated_track[start_idx][1], rotated_track[end_idx][1]], 'r-', linewidth=2)
plt.axis('equal')
# Show y-difference
plt.text(0.5, -0.1, f'Y-diff: {y_diff:.2f}',
horizontalalignment='center', transform=plt.gca().transAxes)
plt.subplot(133)
plt.title('Y-coordinates of Straight')
plt.plot([0, 1], [straight_y1, straight_y2], 'b-')
plt.axhline(y=0, color='k', linestyle='--')
plt.ylim(min(straight_y1, straight_y2) - 1, max(straight_y1, straight_y2) + 1)
plt.tight_layout()
plt.pause(0.1) # Show the plot for a moment
# Now rotate all coordinates with best angle
rotated_coords = rotate(coordinates_array, angle=best_angle)
# Check if we need to flip 180 degrees
straight_y = np.mean([rotated_coords[start_idx][1], rotated_coords[end_idx][1]])
track_center_y = np.mean(rotated_coords[:, 1])
if straight_y > track_center_y:
best_angle += np.pi
rotated_coords = rotate(coordinates_array, angle=best_angle)
# Show final result
plt.clf()
plt.title('Final Result')
plt.plot(rotated_coords[:, 0], rotated_coords[:, 1], 'k-')
plt.plot([rotated_coords[start_idx][0], rotated_coords[end_idx][0]],
[rotated_coords[start_idx][1], rotated_coords[end_idx][1]], 'r-', linewidth=2)
plt.axis('equal')
plt.show()
return best_angle
def evaluate_rotation_strategies(coordinates):
"""Compare different rotation strategies and score them."""
strategies = {
'aspect_ratio': calculate_aspect_ratio_rotation,
'minimal_area': calculate_minimal_area_rotation,
'pca': calculate_pca_rotation,
'start_straight': calculate_start_straight_rotation,
'longest_straight': calculate_longest_straight_rotation,
}
results = {}
for name, strategy in strategies.items():
angle = strategy(coordinates)
rotated = rotate(coordinates, angle=np.radians(angle))
# Calculate metrics
x_coords = [p[0] for p in rotated]
y_coords = [p[1] for p in rotated]
width = max(x_coords) - min(x_coords)
height = max(y_coords) - min(y_coords)
results[name] = {
'rotation_angle': angle,
'aspect_ratio': width / height,
'area': width * height,
'width': width,
'height': height
}
return results
def plot_rotation_outcomes(coordinates):
# Set up the figure
fig = plt.figure(figsize=(15, 12))
grid = plt.GridSpec(3, 2, figure=fig)
axes = [
fig.add_subplot(grid[0, 0]),
fig.add_subplot(grid[0, 1]),
fig.add_subplot(grid[1, 0]),
fig.add_subplot(grid[1, 1]),
fig.add_subplot(grid[2, :])
]
# Get results from all strategies
results = evaluate_rotation_strategies(coordinates)
longest_straight_angle = calculate_longest_straight_rotation(coordinates)
rotated = rotate(coordinates, angle=np.radians(longest_straight_angle))
x_coords = [p[0] for p in rotated]
y_coords = [p[1] for p in rotated]
width = max(x_coords) - min(x_coords)
height = max(y_coords) - min(y_coords)
results['longest_straight'] = {
'rotation_angle': longest_straight_angle,
'aspect_ratio': width / height,
'area': width * height,
'width': width,
'height': height
}
# Find the longest straight section once
start_idx, end_idx = find_longest_straight(coordinates)
# Create the straight section handling wrap-around
straight_section = []
i = start_idx
while i != end_idx:
straight_section.append(coordinates[i])
i = (i + 1) % len(coordinates)
straight_section.append(coordinates[end_idx])
for (name, metrics), ax in zip(results.items(), axes):
# Rotate coordinates using the calculated angle
rotated = rotate(coordinates, angle=np.radians(metrics['rotation_angle']))
rotated_straight = rotate(straight_section, angle=np.radians(metrics['rotation_angle']))
# Plot the main track in blue
x_coords = [p[0] for p in rotated]
y_coords = [p[1] for p in rotated]
ax.plot(x_coords, y_coords, 'b-', linewidth=2, label='Track')
# Plot the longest straight section in red
straight_x = [p[0] for p in rotated_straight]
straight_y = [p[1] for p in rotated_straight]
ax.plot(straight_x, straight_y, 'r-', linewidth=3, alpha=0.8, label='Longest straight')
ax.set_aspect('equal')
# Add title with metrics
title = f"{name}\nRotation: {metrics['rotation_angle']:.1f}°\n"
title += f"Aspect Ratio: {metrics['aspect_ratio']:.2f}\n"
title += f"Area: {metrics['area']:.0f}"
ax.set_title(title)
# Add bounding box
min_x, max_x = min(x_coords), max(x_coords)
min_y, max_y = min(y_coords), max(y_coords)
bbox = plt.Rectangle((min_x, min_y),
max_x - min_x,
max_y - min_y,
fill=False,
color='red',
linestyle='--')
ax.add_patch(bbox)
# Add legend
ax.legend(loc='upper right')
plt.tight_layout()
plt.show()
def get_best_rotation(coordinates, preferences):
"""
Get best rotation based on user preferences.
preferences: dict with weights for different factors:
{
'aspect_ratio_weight': 0.3,
'area_weight': 0.2,
'start_straight_weight': 0.3,
'preferred_orientation': 'landscape', # or 'portrait'
'preferred_ratio': 1.618 # desired aspect ratio
}
"""
results = evaluate_rotation_strategies(coordinates)
scores = {}
for strategy, metrics in results.items():
score = 0
# Aspect ratio scoring
if preferences['preferred_orientation'] == 'landscape':
score += (metrics['aspect_ratio'] > 1) * preferences['aspect_ratio_weight']
else:
score += (metrics['aspect_ratio'] < 1) * preferences['aspect_ratio_weight']
# Area efficiency scoring
min_area = min(r['area'] for r in results.values())
score += (min_area / metrics['area']) * preferences['area_weight']
# Start straight alignment scoring
if strategy == 'start_straight':
score += preferences['start_straight_weight']
scores[strategy] = score
best_strategy = max(scores.items(), key=lambda x: x[1])[0]
return results[best_strategy]['rotation_angle']
coordinates = data.get("features", [])[0].get("geometry", {}).get("coordinates", [])
plot_rotation_outcomes(coordinates)
# preferences = {
# 'aspect_ratio_weight': 0.3,
# 'area_weight': 0.2,
# 'start_straight_weight': 0.3,
# 'preferred_orientation': 'landscape',
# 'preferred_ratio': 1.618
# }
# optimal_rotation = get_best_rotation(coordinates, preferences)