diff --git a/.flake8 b/.flake8
index dd695b6..1ba190c 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,4 @@
[flake8]
# See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length
max-line-length = 88
-extend-ignore = "E203"
+extend-ignore = E203
diff --git a/run/play.py b/run/play.py
new file mode 100644
index 0000000..8d96667
--- /dev/null
+++ b/run/play.py
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: BSD-3-Clause
+import vendeeglobe as vg
+from template_bot import Bot2
+
+nbots = 200
+
+# ais = np.random.choice([Bot, Bot2], nbots)
+ais = [Bot2] * nbots
+
+# players = {
+# "Alice": Bot,
+# "Bob": Bot2,
+# "Charlie": Bot,
+# "David": Bot2,
+# "Eve": Bot,
+# "Frank": Bot2,
+# }
+
+
+start = None
+# start = vg.Location(longitude=-68.004373, latitude=18.180470)
+
+bots = []
+for i, ai in enumerate(ais):
+ bots.append(ai())
+ bots[-1].team = f"Team-{i}"
+
+# start = bots[-1].course[-3]
+
+vg.play(bots=bots, start=start, seed=None, ncores=10, high_contrast=False)
diff --git a/run/template_bot.py b/run/template_bot.py
index e6e16ee..ff68298 100644
--- a/run/template_bot.py
+++ b/run/template_bot.py
@@ -1,23 +1,24 @@
# SPDX-License-Identifier: BSD-3-Clause
# flake8: noqa F401
-
+from collections.abc import Callable
import numpy as np
from vendeeglobe import (
Checkpoint,
- Heading,
Instructions,
Location,
- MapProxy,
- Vector,
- WeatherForecast,
+ # MapProxy,
+ # WeatherForecast,
config,
)
from vendeeglobe.utils import distance_on_surface
-# This is your team name
-CREATOR = "TeamName"
+
+def add_spread(course, spread=0.2):
+ for ch in course[:-1]:
+ ch.latitude += np.random.uniform(-spread, spread)
+ ch.longitude += np.random.uniform(-spread, spread)
class Bot:
@@ -26,7 +27,7 @@ class Bot:
"""
def __init__(self):
- self.team = CREATOR # Mandatory attribute
+ self.team = "TeamName" # Mandatory attribute
self.avatar = 1 # Optional attribute
self.course = [
Checkpoint(longitude=-45.5481686, latitude=39.0722068, radius=200),
@@ -56,8 +57,10 @@ def __init__(self):
radius=5,
),
]
- # for ch in self.course[:9]:
- # ch.reached = True
+ for ch in self.course[:-3]:
+ ch.reached = True
+
+ # add_spread(self.course)
def run(
self,
@@ -68,8 +71,8 @@ def run(
heading: float,
speed: float,
vector: np.ndarray,
- forecast: WeatherForecast,
- map: MapProxy,
+ forecast: Callable,
+ world_map: Callable,
):
"""
This is the method that will be called at every time step to get the
@@ -96,6 +99,10 @@ def run(
map:
The map of the world: 1 for sea, 0 for land.
"""
+ current_position_forecast = forecast(
+ latitudes=latitude, longitudes=longitude, times=0
+ )
+ current_position_terrain = world_map(latitudes=latitude, longitudes=longitude)
instructions = Instructions()
for ch in self.course:
dist = distance_on_surface(
@@ -126,7 +133,7 @@ class Bot2:
"""
def __init__(self):
- self.team = CREATOR # Mandatory attribute
+ self.team = "TeamName" # Mandatory attribute
self.avatar = 2 # Optional attribute
self.course = [
Checkpoint(latitude=43.797109, longitude=-11.264905, radius=50),
@@ -139,6 +146,7 @@ def __init__(self):
Checkpoint(latitude=-15.668984, longitude=77.674694, radius=1190.0),
Checkpoint(latitude=-39.438937, longitude=19.836265, radius=50.0),
Checkpoint(latitude=14.881699, longitude=-21.024326, radius=50.0),
+ Checkpoint(latitude=43, longitude=-19, radius=5),
Checkpoint(latitude=44.076538, longitude=-18.292936, radius=50.0),
Checkpoint(
latitude=config.start.latitude,
@@ -147,9 +155,11 @@ def __init__(self):
),
]
- # for ch in self.course[:6]:
+ # for ch in self.course[:-3]:
# ch.reached = True
+ add_spread(self.course, 1.0)
+
def run(
self,
t: float,
@@ -159,9 +169,13 @@ def run(
heading: float,
speed: float,
vector: np.ndarray,
- forecast: WeatherForecast,
- map: MapProxy,
+ forecast: Callable,
+ world_map: Callable,
):
+ current_position_forecast = forecast(
+ latitudes=latitude, longitudes=longitude, times=0
+ )
+ current_position_terrain = world_map(latitudes=latitude, longitudes=longitude)
loc = None
for ch in self.course:
dist = distance_on_surface(
diff --git a/run/test.py b/run/test.py
deleted file mode 100644
index b5e199b..0000000
--- a/run/test.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# SPDX-License-Identifier: BSD-3-Clause
-
-import vendeeglobe as vg
-from template_bot import Bot, Bot2
-
-
-players = {"Alice": Bot, "Bob": Bot2}
-
-
-start = None
-# start = vg.Location(longitude=-68.004373, latitude=18.180470)
-
-bots = []
-for team, bot in players.items():
- bots.append(bot())
- bots[-1].team = team
-
-vg.play(bots=bots, start=start, seed=None, time_limit=60 * 8)
diff --git a/run/tournament.py b/run/tournament.py
index 6849b95..c4e5def 100644
--- a/run/tournament.py
+++ b/run/tournament.py
@@ -10,4 +10,4 @@
module = importlib.import_module(f"{repo}")
bots.append(module.Bot())
-vg.play(bots=bots, test=False)
+vg.play(bots=bots, safe=True, ncores=8)
diff --git a/src/vendeeglobe/__init__.py b/src/vendeeglobe/__init__.py
index 3b93a75..a86f38b 100644
--- a/src/vendeeglobe/__init__.py
+++ b/src/vendeeglobe/__init__.py
@@ -5,10 +5,9 @@
from .config import config
from .core import Checkpoint, Heading, Instructions, Location, Vector
from .engine import Engine
-from .map import MapProxy
-from .weather import WeatherForecast
+from .main import play
-def play(*args, **kwargs):
- eng = Engine(*args, **kwargs)
- eng.run()
+# def play(*args, **kwargs):
+# eng = Engine(*args, **kwargs)
+# eng.run()
diff --git a/src/vendeeglobe/config.py b/src/vendeeglobe/config.py
index 7be459c..766af5f 100644
--- a/src/vendeeglobe/config.py
+++ b/src/vendeeglobe/config.py
@@ -13,6 +13,7 @@ class Config:
map_radius: float = 6371.0
resourcedir: Path = Path(__file__).parent / "resources"
ntracers: int = 5000
+ number_of_new_tracers = 2
tracer_lifetime: int = 50
start: Checkpoint = Checkpoint(longitude=-1.81, latitude=46.494275, radius=5.0)
checkpoints: Tuple[Checkpoint, Checkpoint] = (
@@ -27,6 +28,9 @@ class Config:
avatar_size = [64, 64]
score_step = 100_000
max_name_length = 15
+ max_track_length = 1000
+ fps = 30
+ time_limit: float = 8 * 60 # in seconds
config = Config()
diff --git a/src/vendeeglobe/core.py b/src/vendeeglobe/core.py
index 0c3dbe5..1a38f33 100644
--- a/src/vendeeglobe/core.py
+++ b/src/vendeeglobe/core.py
@@ -1,9 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
from dataclasses import dataclass
-from typing import Optional, Tuple
-
-import numpy as np
+from typing import Optional
@dataclass
diff --git a/src/vendeeglobe/engine.py b/src/vendeeglobe/engine.py
index 62c0388..b1e662e 100644
--- a/src/vendeeglobe/engine.py
+++ b/src/vendeeglobe/engine.py
@@ -2,118 +2,68 @@
import datetime
import time
-from typing import List, Optional
+from typing import Any, Dict
import numpy as np
-import pyqtgraph as pg
-from pyqtgraph.Qt import QtCore
-try:
- from PyQt5.QtWidgets import (
- QCheckBox,
- QFrame,
- QHBoxLayout,
- QLabel,
- QMainWindow,
- QSizePolicy,
- QSlider,
- QVBoxLayout,
- QWidget,
- )
- from PyQt5.QtCore import Qt
-except ImportError:
- from PySide2.QtWidgets import (
- QMainWindow,
- QWidget,
- QLabel,
- QHBoxLayout,
- QVBoxLayout,
- QCheckBox,
- QSizePolicy,
- QSlider,
- QFrame,
- )
- from PySide2.QtCore import Qt
from . import config
-from .core import Location
-from .graphics import Graphics
-from .map import Map, MapProxy
+from .map import MapData
from .player import Player
-from .scores import (
- finalize_scores,
- get_player_points,
- read_fastest_times,
- read_scores,
- write_fastest_times,
-)
-from .utils import distance_on_surface, longitude_difference, pre_compile
+from .scores import get_player_points, write_times
+from . import utils as ut
from .weather import Weather
class Engine:
def __init__(
self,
- bots: dict,
- test: bool = True,
- time_limit: float = 8 * 60,
- seed: int = None,
- start: Optional[Location] = None,
+ pid: int,
+ seed: int,
+ bots: Dict[str, Any],
+ players: Dict[str, Player],
+ bot_index_begin: int,
+ buffers: dict,
+ safe: bool,
+ world_map: MapData,
):
- pre_compile()
-
- self.time_limit = time_limit
- self.start_time = None
- self.safe = not test
- self.test = test
-
- t0 = time.time()
- print("Generating players...", end=" ", flush=True)
- self.bots = {bot.team: bot for bot in bots}
- self.players = {}
- for name, bot in self.bots.items():
- self.players[name] = Player(
- team=name, avatar=getattr(bot, 'avatar', 1), start=start
- )
- print(f"done [{time.time() - t0:.2f} s]")
-
- self.map = Map()
- self.map_proxy = MapProxy(self.map.array, self.map.dlat, self.map.dlon)
- self.weather = Weather(seed=seed, time_limit=self.time_limit)
- self.graphics = Graphics(
- game_map=self.map, weather=self.weather, players=self.players
+ self.bot_index_begin = bot_index_begin
+ self.bot_index_end = bot_index_begin + len(bots)
+ self.buffers = {
+ key: ut.array_from_shared_mem(*value) for key, value in buffers.items()
+ }
+ self.pid = pid
+ self.safe = safe
+ self.bots = bots
+ self.players = players
+ self.player_tracks = np.zeros(
+ [len(self.players), 2 * config.time_limit * config.fps, 3]
+ )
+ self.position_counter = 0
+
+ self.map = world_map
+ self.weather = Weather(
+ self.pid,
+ seed,
+ weather_u=self.buffers["weather_u"],
+ weather_v=self.buffers["weather_v"],
+ forecast_u=self.buffers["forecast_u"],
+ forecast_v=self.buffers["forecast_v"],
+ forecast_t=self.buffers["forecast_t"],
+ tracer_positions=self.buffers["tracer_positions"],
)
+ self.buffers["all_shutdown"][self.pid] = False
self.players_not_arrived = list(self.players.keys())
self.forecast = self.weather.get_forecast(0)
- self.set_schedule()
- self.group_counter = 0
- self.fastest_times = read_fastest_times(self.players)
-
- def initialize_time(self):
- self.start_time = time.time()
+ def initialize_time(self, start_time: float):
+ self.start_time = start_time
self.last_player_update = self.start_time
self.last_graphics_update = self.start_time
self.last_time_update = self.start_time
self.last_forecast_update = self.start_time
self.previous_clock_time = self.start_time
-
- def set_schedule(self):
- times = []
- for player in self.players.values():
- t0 = time.time()
- self.execute_player_bot(player=player, t=0, dt=0)
- times.append(((time.time() - t0), player))
- ng = 3
- time_groups = {i: [] for i in range(ng)}
- self.player_groups = {i: [] for i in range(ng)}
- for t in sorted(times, key=lambda tup: tup[0], reverse=True):
- ind = np.argmin([sum(g) for g in time_groups.values()])
- time_groups[ind].append(t[0])
- self.player_groups[ind].append(t[1])
- empty_groups = [i for i, g in time_groups.items() if len(g) == 0]
- for i in empty_groups:
- del self.player_groups[i]
+ self.update_interval = 1 / config.fps
def execute_player_bot(self, player, t: float, dt: float):
instructions = None
@@ -125,8 +75,8 @@ def execute_player_bot(self, player, t: float, dt: float):
"heading": player.heading,
"speed": player.speed,
"vector": player.get_vector(),
- "forecast": self.forecast,
- "map": self.map_proxy,
+ "forecast": self.forecast.get_uv,
+ "world_map": self.map.get_terrain,
}
if self.safe:
try:
@@ -137,8 +87,8 @@ def execute_player_bot(self, player, t: float, dt: float):
instructions = self.bots[player.team].run(**args)
return instructions
- def call_player_bots(self, t: float, dt: float, players: List[Player]):
- for player in players:
+ def call_player_bots(self, t: float, dt: float):
+ for player in [p for p in self.players.values() if not p.arrived]:
if self.safe:
try:
player.execute_bot_instructions(
@@ -155,8 +105,8 @@ def move_players(self, weather: Weather, t: float, dt: float):
latitudes = np.array([player.latitude for player in self.players.values()])
longitudes = np.array([player.longitude for player in self.players.values()])
u, v = weather.get_uv(latitudes, longitudes, np.array([t]))
- for i, player in enumerate([p for p in self.players.values() if not p.arrived]):
- lat, lon = player.get_path(dt, u[i], v[i])
+ for i, player in enumerate(self.players.values()):
+ lat, lon = player.get_path(0 if player.arrived else dt, u[i], v[i])
terrain = self.map.get_terrain(longitudes=lon, latitudes=lat)
w = np.where(terrain == 0)[0]
if len(w) > 0:
@@ -166,235 +116,113 @@ def move_players(self, weather: Weather, t: float, dt: float):
if ind > 0:
next_lat = lat[ind]
next_lon = lon[ind]
- player.distance_travelled += distance_on_surface(
+ player.distance_travelled += ut.distance_on_surface(
longitude1=player.longitude,
latitude1=player.latitude,
longitude2=next_lon,
latitude2=next_lat,
)
- player.dlat = next_lat - player.latitude
- player.dlon = longitude_difference(next_lon, player.longitude)
player.latitude = next_lat
player.longitude = next_lon
- else:
- player.dlat = 0
- player.dlon = 0
- for checkpoint in player.checkpoints:
- if not checkpoint.reached:
- d = distance_on_surface(
- longitude1=player.longitude,
- latitude1=player.latitude,
- longitude2=checkpoint.longitude,
- latitude2=checkpoint.latitude,
- )
- if d < checkpoint.radius:
- checkpoint.reached = True
- print(f"{player.team} reached {checkpoint}")
- dist_to_finish = distance_on_surface(
- longitude1=player.longitude,
- latitude1=player.latitude,
- longitude2=config.start.longitude,
- latitude2=config.start.latitude,
- )
- if dist_to_finish < config.start.radius and all(
- ch.reached for ch in player.checkpoints
- ):
- player.arrived = True
- player.bonus = config.score_step * len(self.players_not_arrived)
- n_not_arrived = len(self.players_not_arrived)
- n_players = len(self.players)
- if n_not_arrived == n_players:
- pos_str = "st"
- elif n_not_arrived == n_players - 1:
- pos_str = "nd"
- elif n_not_arrived == n_players - 2:
- pos_str = "rd"
- else:
- pos_str = "th"
- print(
- f"{player.team} finished in {n_players - n_not_arrived + 1}"
- f"{pos_str} position!"
- )
- self.players_not_arrived.remove(player.team)
- self.fastest_times[player.team] = min(
- t, self.fastest_times[player.team]
+ if not player.arrived:
+ for checkpoint in player.checkpoints:
+ if not checkpoint.reached:
+ d = ut.distance_on_surface(
+ longitude1=player.longitude,
+ latitude1=player.latitude,
+ longitude2=checkpoint.longitude,
+ latitude2=checkpoint.latitude,
+ )
+ if d < checkpoint.radius:
+ checkpoint.reached = True
+ print(f"{player.team} reached {checkpoint}")
+ dist_to_finish = ut.distance_on_surface(
+ longitude1=player.longitude,
+ latitude1=player.latitude,
+ longitude2=config.start.longitude,
+ latitude2=config.start.latitude,
)
+ if dist_to_finish < config.start.radius and all(
+ ch.reached for ch in player.checkpoints
+ ):
+ player.arrived = True
+ player.bonus = config.score_step - t
+ time_str = str(datetime.timedelta(seconds=int(t)))[2:]
+ print(f"{player.team} finished in {time_str}")
+ self.players_not_arrived.remove(player.team)
+ player.trip_time = t
+
+ x, y, z = ut.to_xyz(
+ ut.lon_to_phi(player.longitude), ut.lat_to_theta(player.latitude)
+ )
+ self.player_tracks[i, self.position_counter, ...] = [x, y, z]
+
+ self.position_counter += 1
+
+ inds = np.round(
+ np.linspace(0, self.position_counter - 1, config.max_track_length)
+ ).astype(int)
+ self.buffers["player_positions"][
+ self.bot_index_begin : self.bot_index_end, ...
+ ] = self.player_tracks[:, inds, :][:, ::-1, :]
def shutdown(self):
- final_scores = finalize_scores(players=self.players, test=self.test)
- write_fastest_times(self.fastest_times)
- self.update_leaderboard(final_scores, self.fastest_times)
- self.timer.stop()
+ print("inside engine shutdown")
+ self.update_scoreboard()
+ write_times({team: p.trip_time for team, p in self.players.items()})
+ self.buffers["all_shutdown"][self.pid] = True
def update(self):
- clock_time = time.time()
- t = clock_time - self.start_time
- dt = (clock_time - self.previous_clock_time) * config.seconds_to_hours
- if t > self.time_limit:
- self.shutdown()
+ if self.buffers["game_flow"][0]:
+ return
- if (clock_time - self.last_time_update) > config.time_update_interval:
- self.update_scoreboard(self.time_limit - t)
- self.last_time_update = clock_time
+ # if len(self.players_not_arrived) == 0:
+ # return
- if (clock_time - self.last_forecast_update) > config.weather_update_interval:
- self.forecast = self.weather.get_forecast(t)
- self.last_forecast_update = clock_time
+ clock_time = time.time()
+ t = clock_time - self.start_time
+ dt = clock_time - self.previous_clock_time
- self.call_player_bots(
- t=t * config.seconds_to_hours,
- dt=dt,
- players=self.player_groups[self.group_counter % len(self.player_groups)],
- )
- self.move_players(self.weather, t=t, dt=dt)
- if self.tracer_checkbox.isChecked():
+ if dt > self.update_interval:
+ dt = dt * config.seconds_to_hours
self.weather.update_wind_tracers(t=np.array([t]), dt=dt)
- self.graphics.update_wind_tracers(
- self.weather.tracer_lat, self.weather.tracer_lon
- )
- self.graphics.update_player_positions(self.players)
- self.group_counter += 1
- if len(self.players_not_arrived) == 0:
- self.shutdown()
-
- self.previous_clock_time = clock_time
-
- def update_scoreboard(self, t: float):
- time = str(datetime.timedelta(seconds=int(t)))[2:]
- self.time_label.setText(f"Time left: {time} s")
- status = [
- (
- get_player_points(player),
- player.distance_travelled,
- player.team,
- player.speed,
- player.color,
- len([ch for ch in player.checkpoints if ch.reached]),
- )
- for player in self.players.values()
- ]
- for i, (_, dist, team, speed, col, nch) in enumerate(
- sorted(status, reverse=True)
- ):
- self.player_boxes[i].setText(
- f'
■
{i+1}. '
- f'{team[:config.max_name_length]}: {int(dist)} km, '
- f'{int(speed)} km/h [{nch}]'
- )
-
- def update_leaderboard(self, scores, fastest_times):
- sorted_scores = dict(
- sorted(scores.items(), key=lambda item: item[1], reverse=True)
- )
- for i, (name, score) in enumerate(sorted_scores.items()):
- self.score_boxes[i].setText(
- f'■
'
- f'{i+1}. {name[:config.max_name_length]}: {score}'
+ if len(self.players_not_arrived) > 0:
+ if (clock_time - self.last_time_update) > config.time_update_interval:
+ self.update_scoreboard()
+ self.last_time_update = clock_time
+
+ if (
+ clock_time - self.last_forecast_update
+ ) > config.weather_update_interval:
+ self.forecast = self.weather.get_forecast(t)
+ self.last_forecast_update = clock_time
+
+ self.call_player_bots(t=t * config.seconds_to_hours, dt=dt)
+ self.move_players(self.weather, t=t, dt=dt)
+ # self.weather.update_wind_tracers(t=np.array([t]), dt=dt)
+
+ if len(self.players_not_arrived) == 0:
+ self.buffers["all_arrived"][self.pid] = True
+ self.update_scoreboard()
+
+ self.previous_clock_time = clock_time
+
+ def update_scoreboard(self):
+ for i, player in enumerate(self.players.values()):
+ self.buffers["player_status"][self.bot_index_begin + i, ...] = np.array(
+ [
+ get_player_points(player),
+ player.distance_travelled,
+ player.speed,
+ len([ch for ch in player.checkpoints if ch.reached]),
+ ],
+ dtype=float,
)
- sorted_times = dict(sorted(fastest_times.items(), key=lambda item: item[1]))
- time_list = list(enumerate(sorted_times.items()))
- for i, (name, t) in time_list[:3]:
- try:
- time = str(datetime.timedelta(seconds=int(t)))[2:]
- except OverflowError:
- time = "None"
- self.fastest_boxes[i].setText(
- f'■
'
- f'{i+1}. {name[:config.max_name_length]}: {time}'
- )
-
- def run(self):
- window = QMainWindow()
- window.setWindowTitle("Vendée Globe")
- window.setGeometry(100, 100, 1280, 720)
-
- # Create a central widget to hold the two widgets
- central_widget = QWidget()
- window.setCentralWidget(central_widget)
-
- # Create a layout for the central widget
- layout = QHBoxLayout(central_widget)
-
- # Create the first widget with vertical checkboxes
- widget1 = QWidget()
- layout.addWidget(widget1)
- widget1_layout = QVBoxLayout(widget1)
- widget1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
- widget1.setMinimumWidth(int(window.width() * 0.2))
-
- self.time_label = QLabel("Time left:")
- widget1_layout.addWidget(self.time_label)
- self.tracer_checkbox = QCheckBox("Wind tracers", checked=True)
- self.tracer_checkbox.stateChanged.connect(self.graphics.toggle_wind_tracers)
- widget1_layout.addWidget(self.tracer_checkbox)
-
- thickness_slider = QSlider(Qt.Horizontal)
- thickness_slider.setMinimum(1)
- thickness_slider.setMaximum(10)
- thickness_slider.setSingleStep(1)
- thickness_slider.setTickInterval(1)
- thickness_slider.setTickPosition(QSlider.TicksBelow)
- thickness_slider.setValue(int(self.graphics.tracers.size))
- thickness_slider.valueChanged.connect(self.graphics.set_tracer_thickness)
- widget1_layout.addWidget(thickness_slider)
-
- texture_checkbox = QCheckBox("High contrast", checked=False)
- widget1_layout.addWidget(texture_checkbox)
- texture_checkbox.stateChanged.connect(self.graphics.toggle_texture)
-
- stars_checkbox = QCheckBox("Background stars", checked=True)
- widget1_layout.addWidget(stars_checkbox)
- stars_checkbox.stateChanged.connect(self.graphics.toggle_stars)
-
- separator = QFrame()
- separator.setFrameShape(QFrame.HLine)
- separator.setLineWidth(1)
- widget1_layout.addWidget(separator)
-
- self.player_boxes = {}
- for i, p in enumerate(self.players.values()):
- self.player_boxes[i] = QLabel("")
- widget1_layout.addWidget(self.player_boxes[i])
- widget1_layout.addStretch()
-
- layout.addWidget(self.graphics.window)
- self.graphics.window.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
-
- widget2 = QWidget()
- layout.addWidget(widget2)
- widget2_layout = QVBoxLayout(widget2)
- widget2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
- widget2.setMinimumWidth(int(window.width() * 0.08))
- widget2_layout.addWidget(QLabel("Leader board"))
- separator = QFrame()
- separator.setFrameShape(QFrame.HLine)
- separator.setLineWidth(1)
- widget2_layout.addWidget(separator)
- widget2_layout.addWidget(QLabel("Scores:"))
- self.score_boxes = {}
- for i, p in enumerate(self.players.values()):
- self.score_boxes[i] = QLabel(p.team)
- widget2_layout.addWidget(self.score_boxes[i])
- separator = QFrame()
- separator.setFrameShape(QFrame.HLine)
- separator.setLineWidth(1)
- widget2_layout.addWidget(separator)
- widget2_layout.addWidget(QLabel("Fastest finish:"))
- self.fastest_boxes = {}
- for i in range(3):
- self.fastest_boxes[i] = QLabel(str(i + 1))
- widget2_layout.addWidget(self.fastest_boxes[i])
- widget2_layout.addStretch()
- self.update_leaderboard(
- read_scores(self.players.keys(), test=self.test), self.fastest_times
- )
-
- window.show()
- self.timer = QtCore.QTimer()
- self.timer.timeout.connect(self.update)
- self.initialize_time()
- self.timer.start(0)
- pg.exec()
+ def run(self, start_time: float):
+ self.initialize_time(start_time)
+ while not self.buffers["game_flow"][1]:
+ self.update()
+ self.shutdown()
diff --git a/src/vendeeglobe/graphics.py b/src/vendeeglobe/graphics.py
index 5cd2fa2..1bfcd6d 100644
--- a/src/vendeeglobe/graphics.py
+++ b/src/vendeeglobe/graphics.py
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: BSD-3-Clause
-
-# flake8: noqa F405
+import datetime
import time
from typing import Any, Dict
@@ -8,152 +7,58 @@
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from matplotlib.colors import to_rgba
-from OpenGL.GL import * # noqa
-from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem
+from pyqtgraph.Qt import QtCore
+
+
+try:
+ from PyQt5.QtWidgets import (
+ QCheckBox,
+ QFrame,
+ QHBoxLayout,
+ QLabel,
+ QMainWindow,
+ QSizePolicy,
+ QSlider,
+ QVBoxLayout,
+ QWidget,
+ QPushButton,
+ )
+ from PyQt5.QtCore import Qt
+except ImportError:
+ from PySide2.QtWidgets import (
+ QMainWindow,
+ QWidget,
+ QLabel,
+ QHBoxLayout,
+ QVBoxLayout,
+ QCheckBox,
+ QSizePolicy,
+ QSlider,
+ QFrame,
+ QPushButton,
+ )
+ from PySide2.QtCore import Qt
from . import config
from . import utils as ut
-from .map import Map
+from .map import MapTextures
from .player import Player
-from .weather import Weather
-
+from .scores import finalize_scores, read_fastest_times, read_scores
+from .sphere import GLTexturedSphereItem
+from .utils import array_from_shared_mem, string_to_color
-class GLTexturedSphereItem(GLGraphicsItem):
- """
- **Bases:** :class:`GLGraphicsItem `
-
- Displays image data as a textured quad.
- """
+class Graphics:
def __init__(
- self,
- data: np.ndarray,
- smooth: bool = False,
- glOptions: str = "translucent",
- parentItem: Any = None,
+ self, players: Dict[str, Player], high_contrast: bool, buffers: Dict[str, Any]
):
- """
- **Arguments:**
- data:
- Volume data to be rendered. *Must* be 3D numpy array (x, y, RGBA) with
- dtype=ubyte. (See functions.makeRGBA)
- smooth:
- If True, the volume slices are rendered with linear interpolation
- """
-
- self.smooth = smooth
- self._needUpdate = False
- super().__init__(parentItem=parentItem)
- self.setData(data)
- self.setGLOptions(glOptions)
- self.texture = None
-
- def initializeGL(self):
- if self.texture is not None:
- return
- glEnable(GL_TEXTURE_2D)
- self.texture = glGenTextures(1)
-
- def setData(self, data: np.ndarray):
- self.data = data
- self._needUpdate = True
- self.update()
-
- def _updateTexture(self):
- glBindTexture(GL_TEXTURE_2D, self.texture)
- if self.smooth:
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
- else:
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
- # glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
- shape = self.data.shape
-
- ## Test texture dimensions first
- glTexImage2D(
- GL_PROXY_TEXTURE_2D,
- 0,
- GL_RGBA,
- shape[0],
- shape[1],
- 0,
- GL_RGBA,
- GL_UNSIGNED_BYTE,
- None,
- )
- if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0:
- raise Exception(
- "OpenGL failed to create 2D texture (%dx%d); too large for this hardware."
- % shape[:2]
- )
-
- data = np.ascontiguousarray(self.data.transpose((1, 0, 2)))
- glTexImage2D(
- GL_TEXTURE_2D,
- 0,
- GL_RGBA,
- shape[0],
- shape[1],
- 0,
- GL_RGBA,
- GL_UNSIGNED_BYTE,
- data,
- )
- glDisable(GL_TEXTURE_2D)
-
- def paint(self):
- if self._needUpdate:
- self._updateTexture()
- self._needUpdate = False
- glEnable(GL_TEXTURE_2D)
- glBindTexture(GL_TEXTURE_2D, self.texture)
-
- self.setupGLState()
-
- glColor4f(1, 1, 1, 1)
-
- theta = np.linspace(0, np.pi, 25, dtype="float32")
- phi = np.linspace(0, 2 * np.pi, 49, dtype="float32")
- t_n = theta / np.pi
- p_n = phi / (2 * np.pi)
-
- phi_grid, theta_grid = np.meshgrid(phi, theta, indexing="ij")
- x, y, z = ut.to_xyz(phi_grid, theta_grid, gl=True)
-
- glBegin(GL_QUADS)
- for j in range(len(theta) - 1):
- for i in range(len(phi) - 1):
- glTexCoord2f(p_n[i], t_n[j])
- glVertex3f(x[i, j], y[i, j], z[i, j])
- glTexCoord2f(p_n[i], t_n[j + 1])
- glVertex3f(x[i, j + 1], y[i, j + 1], z[i, j + 1])
- glTexCoord2f(p_n[i + 1], t_n[j + 1])
- glVertex3f(x[i + 1, j + 1], y[i + 1, j + 1], z[i + 1, j + 1])
- glTexCoord2f(p_n[i + 1], t_n[j])
- glVertex3f(x[i + 1, j], y[i + 1, j], z[i + 1, j])
-
- glEnd()
- glDisable(GL_TEXTURE_2D)
-
-
-"""
-Use GLImageItem to display image data on rectangular planes.
-
-In this example, the image data is sampled from a volume and the image planes
-placed as if they slice through the volume.
-"""
-
-from pyqtgraph.widgets.RemoteGraphicsView import RemoteGraphicsView
-
-
-class Graphics:
- def __init__(self, game_map: Map, weather: Weather, players: Dict[str, Player]):
t0 = time.time()
print("Composing graphics...", end=" ", flush=True)
+
+ self.scoreboard_max_players = 20
+ self.high_contrast = high_contrast
+
self.app = pg.mkQApp("Vendee Globe")
self.window = gl.GLViewWidget()
self.window.setWindowTitle("Vendee Globe")
@@ -163,10 +68,21 @@ def __init__(self, game_map: Map, weather: Weather, players: Dict[str, Player]):
azimuth=180 + config.start.longitude,
)
- self.default_texture = np.fliplr(np.transpose(game_map.array, axes=[1, 0, 2]))
+ self.map_textures = MapTextures()
+
+ self.default_texture = np.fliplr(
+ np.transpose(self.map_textures.default_texture, axes=[1, 0, 2])
+ )
self.high_contrast_texture = np.transpose(
- game_map.high_contrast_texture, axes=[1, 0, 2]
+ self.map_textures.contrast_texture, axes=[1, 0, 2]
)
+
+ self.players = players
+
+ self.buffers = {
+ key: array_from_shared_mem(*value) for key, value in buffers.items()
+ }
+
self.sphere = GLTexturedSphereItem(self.default_texture)
self.sphere.setGLOptions("opaque")
self.window.addItem(self.sphere)
@@ -208,102 +124,76 @@ def __init__(self, game_map: Map, weather: Weather, players: Dict[str, Player]):
mesh.rotate(np.degrees(ut.lon_to_phi(ch.longitude)), 0, 0, 1)
self.window.addItem(mesh)
- # Add tracers
- x, y, z = ut.to_xyz(
- ut.lon_to_phi(weather.tracer_lon.ravel()),
- ut.lat_to_theta(weather.tracer_lat.ravel()),
- )
- self.default_tracer_colors = weather.tracer_colors
- self.high_contrast_tracer_colors = weather.tracer_colors.copy()
- self.high_contrast_tracer_colors[..., :3] *= 0.8
+ self.tracer_positions = self.buffers["tracer_positions"]
+
+ self.default_tracer_colors = np.ones(self.tracer_positions.shape[:-1] + (4,))
+ self.default_tracer_colors[..., 3] = np.linspace(
+ 1, 0, config.tracer_lifetime
+ ).reshape((-1, 1))
+
+ self.high_contrast_tracer_colors = self.default_tracer_colors.copy()
+ self.high_contrast_tracer_colors[..., :3] *= 0.5
self.tracers = gl.GLScatterPlotItem(
- pos=np.array([x, y, z]).T,
- color=self.default_tracer_colors,
+ pos=self.tracer_positions.reshape((-1, 3)),
+ color=self.default_tracer_colors.reshape((-1, 4)),
size=2,
pxMode=True,
)
# self.tracers.setGLOptions("opaque")
- self.tracers.setGLOptions('translucent')
+ self.tracers.setGLOptions("translucent")
self.window.addItem(self.tracers)
# Add players
- latitudes = np.array([player.latitude for player in players.values()])
- longitudes = np.array([player.longitude for player in players.values()])
- colors = np.array([to_rgba(player.color) for player in players.values()])
- x, y, z = ut.to_xyz(ut.lon_to_phi(longitudes), ut.lat_to_theta(latitudes))
+ self.player_positions = self.buffers["player_positions"]
+ player_colors = [string_to_color(p.team) for p in self.players.values()]
+ colors = np.array([to_rgba(color) for color in player_colors])
- self.players = gl.GLScatterPlotItem(
- pos=np.array([x, y, z]).T,
+ self.player_markers = gl.GLScatterPlotItem(
+ pos=self.player_positions,
color=colors,
size=10,
pxMode=True,
)
- self.players.setGLOptions("opaque")
- # self.tracers.setGLOptions('translucent')
- self.window.addItem(self.players)
-
- self.tracks = {}
- self.avatars = {}
- self.labels = {}
- for i, (name, player) in enumerate(players.items()):
+ self.player_markers.setGLOptions("opaque")
+ # # self.tracers.setGLOptions('translucent')
+ self.window.addItem(self.player_markers)
+
+ self.tracks = []
+ self.avatars = []
+ for i, player in enumerate(self.players.values()):
x, y, z = ut.to_xyz(
ut.lon_to_phi(player.longitude),
ut.lat_to_theta(player.latitude),
)
pos = np.array([[x], [y], [z]]).T
- self.tracks[name] = {
- 'pos': pos,
- 'artist': gl.GLLinePlotItem(
- pos=pos, color=tuple(colors[i]), width=4, antialias=True
- ),
- }
- self.tracks[name]['artist'].setGLOptions("opaque")
- self.window.addItem(self.tracks[name]['artist'])
-
- self.avatars[name] = gl.GLImageItem(
- np.fliplr(np.transpose(np.array(player.avatar), axes=[1, 0, 2]))
+ track = gl.GLLinePlotItem(
+ pos=pos,
+ color=tuple(colors[i]),
+ width=4,
+ antialias=True,
)
- offset = config.avatar_size[0] / 2
- self.avatars[name].translate(-offset, -offset, 0)
- self.avatars[name].rotate(90, 1, 0, 0)
- self.avatars[name].rotate(180, 0, 0, 1)
- self.avatars[name].translate(0, config.map_radius, 0)
- self.avatars[name].rotate(90, 0, 0, 1)
- self.avatars[name].rotate(player.longitude, 0, 0, 1)
- perp_vec = np.cross([x, y, 0], [0, 0, 1])
- perp_vec /= np.linalg.norm(perp_vec)
- self.avatars[name].rotate(player.latitude, *perp_vec)
- self.window.addItem(self.avatars[name])
-
- print(f'done [{time.time() - t0:.2f} s]')
-
- def update_wind_tracers(self, tracer_lat: np.ndarray, tracer_lon: np.ndarray):
- x, y, z = ut.to_xyz(
- ut.lon_to_phi(tracer_lon.ravel()),
- ut.lat_to_theta(tracer_lat.ravel()),
- )
- self.tracers.setData(pos=np.array([x, y, z]).T)
-
- def update_player_positions(self, players: Dict[str, Player]):
- latitudes = np.array([player.latitude for player in players.values()])
- longitudes = np.array([player.longitude for player in players.values()])
- x, y, z = ut.to_xyz(ut.lon_to_phi(longitudes), ut.lat_to_theta(latitudes))
- self.players.setData(pos=np.array([x, y, z]).T)
-
- for i, (name, player) in enumerate(players.items()):
- if not player.arrived:
- arr = np.array([x[i], y[i], z[i]])
- pos = np.vstack(
- [self.tracks[name]['pos'], arr],
- )
- npos = len(pos)
- step = (npos // 1000) if npos > 1000 else 1
- self.tracks[name]['artist'].setData(pos=pos[::step])
- self.tracks[name]['pos'] = pos
- self.avatars[name].rotate(player.dlon, 0, 0, 1)
- perp_vec = np.cross([x[i], y[i], 0], [0, 0, 1])
- perp_vec /= np.linalg.norm(perp_vec)
- self.avatars[name].rotate(player.dlat, *perp_vec)
+ track.setGLOptions("opaque")
+ self.window.addItem(track)
+ self.tracks.append(track)
+
+ print(f"done [{time.time() - t0:.2f} s]")
+
+ def initialize_time(self, start_time: float):
+ self.start_time = start_time
+ self.last_player_update = self.start_time
+ self.last_graphics_update = self.start_time
+ self.last_time_update = self.start_time
+ self.last_forecast_update = self.start_time
+ self.previous_clock_time = self.start_time
+ self.update_interval = 1 / config.fps
+
+ def update_wind_tracers(self):
+ self.tracers.setData(pos=self.tracer_positions.reshape((-1, 3)))
+
+ def update_player_positions(self):
+ self.player_markers.setData(pos=self.player_positions[:, 0, :])
+ for i in range(len(self.player_positions)):
+ self.tracks[i].setData(pos=self.player_positions[i, ...])
def toggle_wind_tracers(self, val):
self.tracers.setVisible(val)
@@ -321,3 +211,188 @@ def set_tracer_thickness(self, val):
def toggle_stars(self, val):
self.background_stars.setVisible(val)
+
+ def update_scoreboard(self, t: float):
+ time_str = str(datetime.timedelta(seconds=int(t)))[2:]
+ self.time_label.setText(f"Time left: {time_str} s")
+ status = [
+ (
+ self.buffers["player_status"][i, 0], # points
+ self.buffers["player_status"][i, 1], # distance travelled
+ player.team,
+ self.buffers["player_status"][i, 2], # speed
+ player.color,
+ int(self.buffers["player_status"][i, 3]), # checkpoints reached
+ )
+ for i, player in enumerate(self.players.values())
+ ]
+
+ for i, (_, dist, team, speed, col, nch) in enumerate(
+ sorted(status, reverse=True)[: self.scoreboard_max_players]
+ ):
+ self.player_boxes[i].setText(
+ f'■
{i+1}. '
+ f"{team[:config.max_name_length]}: {int(dist)} km, "
+ f"{int(speed)} km/h [{nch}]"
+ )
+
+ def shutdown(self):
+ for name, points in zip(
+ self.players.keys(), self.buffers["player_status"][:, 0]
+ ):
+ print(f"{name}: {points}")
+ self.update_leaderboard(
+ scores=finalize_scores(
+ self.players, player_points=self.buffers["player_status"][:, 0]
+ )
+ )
+ self.timer.stop()
+
+ def end_round(self):
+ self.buffers["game_flow"][1] = True
+
+ def update(self):
+ if self.buffers["game_flow"][0]:
+ return
+
+ if all(self.buffers["all_arrived"]):
+ self.buffers["game_flow"][1] = True
+
+ if all(self.buffers["all_shutdown"]):
+ self.shutdown()
+
+ clock_time = time.time()
+ t = clock_time - self.start_time
+ self.update_wind_tracers()
+ self.update_player_positions()
+ if (clock_time - self.last_time_update) > config.time_update_interval:
+ self.update_scoreboard(config.time_limit - t)
+ self.last_time_update = clock_time
+ if t > config.time_limit:
+ self.buffers["game_flow"][1] = True
+
+ def update_leaderboard(self, scores: Dict[str, int]):
+ fastest_times = read_fastest_times(self.players)
+ sorted_scores = dict(
+ sorted(scores.items(), key=lambda item: item[1], reverse=True)
+ )
+ for i, (name, score) in enumerate(
+ list(sorted_scores.items())[: self.scoreboard_max_players]
+ ):
+ self.score_boxes[i].setText(
+ f'■
'
+ f"{i+1}. {name[:config.max_name_length]}: {score:.2f}"
+ )
+
+ sorted_times = dict(sorted(fastest_times.items(), key=lambda item: item[1]))
+ time_list = list(enumerate(sorted_times.items()))
+ for i, (name, t) in time_list[:3]:
+ try:
+ time = str(datetime.timedelta(seconds=int(t)))[2:]
+ except OverflowError:
+ time = "None"
+ self.fastest_boxes[i].setText(
+ f'■
'
+ f"{i+1}. {name[:config.max_name_length]}: {time}"
+ )
+
+ def run(self, start_time: float):
+ main_window = QMainWindow()
+ main_window.setWindowTitle("Vendée Globe")
+ main_window.setGeometry(100, 100, 1280, 720)
+
+ # Create a central widget to hold the two widgets
+ central_widget = QWidget()
+ main_window.setCentralWidget(central_widget)
+
+ # Create a layout for the central widget
+ layout = QHBoxLayout(central_widget)
+
+ # Create the first widget with vertical checkboxes
+ widget1 = QWidget()
+ layout.addWidget(widget1)
+ widget1_layout = QVBoxLayout(widget1)
+ widget1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
+ widget1.setMinimumWidth(int(main_window.width() * 0.2))
+
+ self.time_label = QLabel("Time left:")
+ widget1_layout.addWidget(self.time_label)
+ self.tracer_checkbox = QCheckBox("Wind tracers", checked=True)
+ self.tracer_checkbox.stateChanged.connect(self.toggle_wind_tracers)
+ widget1_layout.addWidget(self.tracer_checkbox)
+
+ thickness_slider = QSlider(Qt.Horizontal)
+ thickness_slider.setMinimum(1)
+ thickness_slider.setMaximum(10)
+ thickness_slider.setSingleStep(1)
+ thickness_slider.setTickInterval(1)
+ thickness_slider.setTickPosition(QSlider.TicksBelow)
+ thickness_slider.setValue(int(self.tracers.size))
+ thickness_slider.valueChanged.connect(self.set_tracer_thickness)
+ widget1_layout.addWidget(thickness_slider)
+
+ texture_checkbox = QCheckBox("High contrast", checked=False)
+ widget1_layout.addWidget(texture_checkbox)
+ texture_checkbox.stateChanged.connect(self.toggle_texture)
+ texture_checkbox.setChecked(self.high_contrast)
+
+ stars_checkbox = QCheckBox("Background stars", checked=True)
+ widget1_layout.addWidget(stars_checkbox)
+ stars_checkbox.stateChanged.connect(self.toggle_stars)
+
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setLineWidth(1)
+ widget1_layout.addWidget(separator)
+
+ self.player_boxes = {}
+ for i in range(min(len(self.players), self.scoreboard_max_players)):
+ self.player_boxes[i] = QLabel("")
+ widget1_layout.addWidget(self.player_boxes[i])
+ widget1_layout.addStretch()
+
+ layout.addWidget(self.window)
+ self.window.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+
+ widget2 = QWidget()
+ layout.addWidget(widget2)
+ widget2_layout = QVBoxLayout(widget2)
+ widget2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
+ widget2.setMinimumWidth(int(main_window.width() * 0.08))
+ widget2_layout.addWidget(QLabel("Leader board"))
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setLineWidth(1)
+ widget2_layout.addWidget(separator)
+ widget2_layout.addWidget(QLabel("Scores:"))
+ self.score_boxes = {}
+ for i in range(min(len(self.players), self.scoreboard_max_players)):
+ self.score_boxes[i] = QLabel("")
+ widget2_layout.addWidget(self.score_boxes[i])
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setLineWidth(1)
+ widget2_layout.addWidget(separator)
+ widget2_layout.addWidget(QLabel("Fastest finish:"))
+ self.fastest_boxes = {}
+ for i in range(3):
+ self.fastest_boxes[i] = QLabel("")
+ widget2_layout.addWidget(self.fastest_boxes[i])
+ widget2_layout.addStretch()
+ self.cancel_button = QPushButton("End round")
+ self.cancel_button.clicked.connect(self.end_round)
+ widget2_layout.addWidget(self.cancel_button)
+
+ self.initialize_time(start_time=start_time)
+
+ self.update_leaderboard(scores=read_scores(self.players))
+
+ main_window.show()
+ self.timer = QtCore.QTimer()
+ self.timer.timeout.connect(self.update)
+ # self.initialize_time()
+ self.timer.setInterval(1000 // config.fps)
+ self.timer.start()
+ pg.exec()
+
+ self.buffers["game_flow"][1] = True
diff --git a/src/vendeeglobe/main.py b/src/vendeeglobe/main.py
new file mode 100644
index 0000000..83cf3bd
--- /dev/null
+++ b/src/vendeeglobe/main.py
@@ -0,0 +1,138 @@
+# SPDX-License-Identifier: BSD-3-Clause
+
+import time
+from multiprocessing import Process
+from multiprocessing.managers import SharedMemoryManager
+
+
+import numpy as np
+
+
+from . import config
+from .engine import Engine
+from .graphics import Graphics
+from .map import MapData
+from .player import Player
+from .utils import array_from_shared_mem, pre_compile
+from .weather import WeatherData
+
+
+class Clock:
+ def __init__(self):
+ self._start_time = None
+
+ @property
+ def start_time(self):
+ if self._start_time is None:
+ self._start_time = time.time()
+ return self._start_time
+
+
+clock = Clock()
+
+
+def spawn_graphics(*args):
+ graphics = Graphics(*args)
+ graphics.run(start_time=clock.start_time)
+
+
+def spawn_engine(*args):
+ engine = Engine(*args)
+ engine.run(start_time=clock.start_time)
+
+
+def play(bots, seed=None, start=None, safe=False, ncores=8, high_contrast=False):
+ pre_compile()
+
+ n_sub_processes = ncores
+ bots = {bot.team: bot for bot in bots}
+ players = {name: Player(team=name, start=start) for name in bots}
+
+ # # TODO: Cheat!
+ # for player in players.values():
+ # for ch in player.checkpoints:
+ # ch.reached = True
+
+ groups = np.array_split(list(bots.keys()), n_sub_processes)
+ ntracers = (
+ config.ntracers
+ // (n_sub_processes * config.number_of_new_tracers)
+ * config.number_of_new_tracers
+ )
+ tracer_positions = np.empty((n_sub_processes, config.tracer_lifetime, ntracers, 3))
+ player_positions = np.empty((len(bots), config.max_track_length, 3))
+ player_status = np.zeros((len(bots), 4)) # points, dist travelled, speed, checks
+ game_flow = np.zeros(2, dtype=bool) # pause, exit_from_graphics
+ arrived = np.zeros(n_sub_processes, dtype=bool)
+ shutdown = np.zeros(n_sub_processes, dtype=bool)
+
+ weather = WeatherData(seed=seed)
+ world_map = MapData()
+
+ buffer_mapping = {
+ "tracer_positions": tracer_positions,
+ "player_positions": player_positions,
+ "weather_u": weather.u,
+ "weather_v": weather.v,
+ "forecast_u": weather.forecast_u,
+ "forecast_v": weather.forecast_v,
+ "forecast_t": weather.forecast_times,
+ "game_flow": game_flow,
+ "player_status": player_status,
+ "all_arrived": arrived,
+ "all_shutdown": shutdown,
+ }
+
+ with SharedMemoryManager() as smm:
+ buffers = {}
+ for key, arr in buffer_mapping.items():
+ mem = smm.SharedMemory(size=arr.nbytes)
+ arr_shared = array_from_shared_mem(mem, arr.dtype, arr.shape)
+ arr_shared[...] = arr
+ buffers[key] = (mem, arr.dtype, arr.shape)
+
+ graphics = Process(
+ target=spawn_graphics,
+ args=(
+ players,
+ high_contrast,
+ {
+ key: buffers[key]
+ for key in (
+ "tracer_positions",
+ "player_positions",
+ "game_flow",
+ "player_status",
+ "all_arrived",
+ "all_shutdown",
+ )
+ },
+ ),
+ )
+
+ engines = []
+ bot_index_begin = 0
+ for i, group in enumerate(groups):
+ engines.append(
+ Process(
+ target=spawn_engine,
+ args=(
+ i,
+ seed,
+ {name: bots[name] for name in group},
+ {name: players[name] for name in group},
+ bot_index_begin,
+ buffers,
+ safe,
+ world_map,
+ ),
+ )
+ )
+ bot_index_begin += len(group)
+
+ graphics.start()
+ for engine in engines:
+ engine.start()
+ graphics.join()
+ for engine in engines:
+ engine.join()
diff --git a/src/vendeeglobe/map.py b/src/vendeeglobe/map.py
index 8365464..7a4d820 100644
--- a/src/vendeeglobe/map.py
+++ b/src/vendeeglobe/map.py
@@ -1,8 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
import os
-from dataclasses import dataclass
-from typing import Tuple, Union
+from typing import Union
import numpy as np
from PIL import Image
@@ -13,14 +12,21 @@
def create_map_data(fname):
im = Image.open(os.path.join(config.resourcedir, config.map_file))
- array = np.array(im.convert('RGBA'))
- img16 = array.astype('int16')
+ array = np.array(im.convert("RGBA"))
+ img16 = array.astype("int16")
sea_array = np.flipud(
np.where(img16[:, :, 2] > (img16[:, :, 0] + img16[:, :, 1]), 1, 0)
)
- high_contrast_texture = np.array(
- Image.fromarray(np.uint8(sea_array * 255)).convert('RGBA')
- )
+ # high_contrast_texture = np.array(
+ # Image.fromarray(np.uint8(sea_array * 255)).convert('RGBA')
+ # )
+ high_contrast_texture = np.zeros((*sea_array.shape, 4), dtype="uint8")
+ high_contrast_texture[..., 3] = 255
+ inds = sea_array == 1
+ high_contrast_texture[inds, 0] = 200
+ high_contrast_texture[inds, 1] = 255
+ high_contrast_texture[inds, 2] = 255
+ high_contrast_texture[~inds, 1] = 50
np.savez(
fname,
array=array,
@@ -29,17 +35,20 @@ def create_map_data(fname):
)
-class Map:
+class MapTextures:
def __init__(self):
- t0 = time.time()
- print('Creating world map...', end=' ', flush=True)
+ mapdata = np.load(os.path.join(config.resourcedir, "mapdata.npz"))
+ self.default_texture = mapdata["array"]
+ self.contrast_texture = mapdata["high_contrast_texture"]
- mapdata = np.load(os.path.join(config.resourcedir, 'mapdata.npz'))
- self.array = mapdata['array']
- self.sea_array = mapdata['sea_array']
- self.high_contrast_texture = mapdata['high_contrast_texture']
- self.nlat, self.nlon, _ = self.array.shape
+class MapData:
+ def __init__(self):
+ t0 = time.time()
+ print("Creating world map...", end=" ", flush=True)
+ mapdata = np.load(os.path.join(config.resourcedir, "mapdata.npz"))
+ self.sea_array = mapdata["sea_array"]
+ self.nlat, self.nlon, _ = mapdata["array"].shape
lat_min = -90
lat_max = 90
self.dlat = (lat_max - lat_min) / self.nlat
@@ -54,29 +63,24 @@ def __init__(self):
)
self.lon_grid, self.lat_grid = np.meshgrid(self.lon, self.lat)
self.sea_array.setflags(write=False)
- print(f'done [{time.time() - t0:.2f} s]')
-
- def get_terrain(self, longitudes, latitudes):
- ilon = ((longitudes + 180.0) / self.dlon).astype(int)
- ilat = ((latitudes + 90.0) / self.dlat).astype(int)
- return self.sea_array[ilat, ilon]
+ print(f"done [{time.time() - t0:.2f} s]")
-
-@dataclass(frozen=True)
-class MapProxy:
- array: np.ndarray
- dlat: float
- dlon: float
-
- def get_inds(
- self, latitude: Union[float, np.ndarray], longitude: Union[float, np.ndarray]
- ) -> Tuple[np.ndarray, np.ndarray]:
- ilat = ((np.asarray(latitude) + 90.0) / self.dlat).astype(int)
- ilon = ((np.asarray(longitude) + 180.0) / self.dlon).astype(int)
- return ilat, ilon
-
- def get_data(
- self, latitude: Union[float, np.ndarray], longitude: Union[float, np.ndarray]
+ def get_terrain(
+ self,
+ *,
+ latitudes: Union[float, np.ndarray],
+ longitudes: Union[float, np.ndarray],
) -> Union[float, np.ndarray]:
- ilat, ilon = self.get_inds(latitude, longitude)
- return self.array[ilat, ilon]
+ """
+ Get the terrain type (sea or land) at the supplied latitude(s) and longitude(s).
+
+ Parameters
+ ----------
+ latitudes:
+ The latitude(s) in degrees.
+ longitudes:
+ The longitude(s) in degrees.
+ """
+ ilon = ((np.asarray(longitudes) + 180.0) / self.dlon).astype(int)
+ ilat = ((np.asarray(latitudes) + 90.0) / self.dlat).astype(int)
+ return self.sea_array[ilat, ilon]
diff --git a/src/vendeeglobe/player.py b/src/vendeeglobe/player.py
index 215b284..fe5f7b1 100644
--- a/src/vendeeglobe/player.py
+++ b/src/vendeeglobe/player.py
@@ -4,8 +4,6 @@
from typing import Optional, Sequence, Tuple, Union
import numpy as np
-from matplotlib.colors import hex2color
-from PIL import Image
from . import config
from . import utils as utl
@@ -13,12 +11,7 @@
class Player:
- def __init__(
- self,
- team: str,
- avatar: Union[str, int],
- start: Optional[Location] = None,
- ):
+ def __init__(self, team: str, start: Optional[Location] = None):
self.team = team
self.bonus = 0
self.heading = 180.0
@@ -34,24 +27,12 @@ def __init__(
Checkpoint(**asdict(checkpoint)) for checkpoint in config.checkpoints
]
self.arrived = False
+ self.trip_time = np.inf
self.distance_travelled = 0.0
self.dlat = 0.0
self.dlon = 0.0
- self.make_avatar(avatar)
self.sail = 1.0
- def make_avatar(self, avatar):
- if isinstance(avatar, str):
- self.avatar = Image.open(avatar).resize(config.avatar_size).convert('RGBA')
- else:
- img = Image.open(config.resourcedir / f'ship{avatar}.png')
- img = img.resize(config.avatar_size).convert("RGBA")
- data = img.getdata()
- self.avatar = np.array(data).reshape(img.height, img.width, 4)
- rgb = hex2color(self.color)
- for i in range(3):
- self.avatar[..., i] = int(round(rgb[i] * 255))
-
def execute_bot_instructions(self, instructions: Union[Location, Heading, Vector]):
if [instructions.location, instructions.heading, instructions.vector].count(
None
@@ -102,16 +83,12 @@ def goto(self, longitude: float, latitude: float):
"""
Point the vehicle towards the given longitude and latitude.
"""
- lon1 = np.radians(self.longitude)
- lat1 = np.radians(self.latitude)
- lon2 = np.radians(longitude)
- lat2 = np.radians(latitude)
-
- dlon = lon2 - lon1
- y = np.sin(dlon) * np.cos(lat2)
- x = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(dlon)
- initial_bearing = -np.arctan2(y, x) + (np.pi * 0.5)
- self.set_heading((np.degrees(initial_bearing) + 360) % 360)
+ self.set_heading(
+ utl.goto(
+ origin=Location(longitude=self.longitude, latitude=self.latitude),
+ to=Location(longitude=longitude, latitude=latitude),
+ )
+ )
def get_distance(self, longitude: float, latitude: float) -> float:
"""
diff --git a/src/vendeeglobe/resources/mapdata.npz b/src/vendeeglobe/resources/mapdata.npz
index eba61d5..846df8f 100644
Binary files a/src/vendeeglobe/resources/mapdata.npz and b/src/vendeeglobe/resources/mapdata.npz differ
diff --git a/src/vendeeglobe/scores.py b/src/vendeeglobe/scores.py
index a605cf9..cb3cdaa 100644
--- a/src/vendeeglobe/scores.py
+++ b/src/vendeeglobe/scores.py
@@ -10,23 +10,35 @@
from .utils import distance_on_surface
-def read_scores(players: Dict[str, Player], test: bool) -> Dict[str, int]:
+def _make_folder(folder: str):
+ try:
+ os.makedirs(folder)
+ except FileExistsError:
+ pass
+
+
+def read_scores(players: Dict[str, Player]) -> Dict[str, int]:
scores = {p: 0 for p in players}
- fname = "scores.txt"
- if os.path.exists(fname) and (not test):
- with open(fname, "r") as f:
- contents = f.readlines()
- for line in contents:
- name, score = line.split(":")
- scores[name] = int(score.strip())
+ folder = ".scores"
+ if not os.path.exists(folder):
+ return scores
+ for player in players:
+ fname = os.path.join(folder, f"{player}_scores.txt")
+ if os.path.exists(fname):
+ with open(fname, "r") as f:
+ contents = f.readlines()
+ scores[player] = sum(float(line.strip()) for line in contents)
return scores
def _write_scores(scores: Dict[str, int]):
- fname = "scores.txt"
- with open(fname, "w") as f:
- for name, score in scores.items():
- f.write(f"{name}: {score}\n")
+ folder = ".scores"
+ _make_folder(folder)
+ for name, score in scores.items():
+ fname = os.path.join(folder, f"{name}_scores.txt")
+ mode = "a" if os.path.exists(fname) else "w"
+ with open(fname, mode) as f:
+ f.write(f"{score}\n")
def get_player_points(player: Player) -> int:
@@ -66,18 +78,28 @@ def get_player_points(player: Player) -> int:
return points
-def get_rankings(players: Dict[str, Player]) -> Dict[str, int]:
- status = [(get_player_points(player), player.team) for player in players.values()]
+def get_rankings(
+ players: Dict[str, Player], player_points: np.ndarray
+) -> Dict[str, int]:
+ status = [
+ (player_points[i], player.team) for i, player in enumerate(players.values())
+ ]
return [team for _, team in sorted(status, reverse=True)]
-def _get_final_scores(players: Dict[str, Player], scores: Dict[str, int]):
- rankings = get_rankings(players)
- for_grabs = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1]
+def _get_final_scores(
+ players: Dict[str, Player], scores: Dict[str, int], player_points: np.ndarray
+):
+ rankings = get_rankings(players, player_points)
+ # exponential points distribution
+ n = len(players) # Points for the 1st position
+ k = np.log(n) / (n - 1)
+ points = n * np.exp(-k * (np.arange(1, n + 1) - 1))
+ # for_grabs = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1]
final_scores = {}
round_scores = {}
- for team in rankings:
- round_scores[team] = for_grabs.pop(0) if for_grabs else 0
+ for team, p in zip(rankings, points):
+ round_scores[team] = p # for_grabs.pop(0) if for_grabs else 0
final_scores[team] = scores[team] + round_scores[team]
return round_scores, final_scores
@@ -92,31 +114,36 @@ def _print_scores(
sorted_scores = sorted(all_scores, key=lambda tup: tup[2], reverse=True)
print("Scores:")
for i, (name, score, total) in enumerate(sorted_scores):
- print(f"{i + 1}. {name}: {total} ({score})")
+ print(f"{i + 1}. {name}: {total:.2f} ({score:.2f})")
-def finalize_scores(players: Dict[str, Player], test: bool = False):
- scores = read_scores(players, test=test)
- round_scores, final_scores = _get_final_scores(players, scores)
+def finalize_scores(players: Dict[str, Player], player_points: np.ndarray):
+ scores = read_scores(players)
+ round_scores, final_scores = _get_final_scores(players, scores, player_points)
_print_scores(round_scores=round_scores, final_scores=final_scores)
- _write_scores(final_scores)
+ _write_scores(round_scores)
return final_scores
def read_fastest_times(players: Dict[str, Player]) -> Dict[str, int]:
times = {p: np.inf for p in players}
- fname = "fastest_times.txt"
- if os.path.exists(fname):
- with open(fname, "r") as f:
- contents = f.readlines()
- for line in contents:
- name, t = line.split(":")
- times[name] = float(t.strip())
+ folder = ".scores"
+ if not os.path.exists(folder):
+ return times
+ for name in players:
+ fname = os.path.join(folder, f"{name}_times.txt")
+ if os.path.exists(fname):
+ with open(fname, "r") as f:
+ contents = f.readlines()
+ times[name] = min(float(t.strip()) for t in contents)
return times
-def write_fastest_times(times: Dict[str, Player]):
- fname = "fastest_times.txt"
- with open(fname, "w") as f:
- for name, t in times.items():
- f.write(f"{name}: {t}\n")
+def write_times(times: Dict[str, Player]):
+ folder = ".scores"
+ _make_folder(folder)
+ for name, t in times.items():
+ fname = os.path.join(folder, f"{name}_times.txt")
+ mode = "a" if os.path.exists(fname) else "w"
+ with open(fname, mode) as f:
+ f.write(f"{t}\n")
diff --git a/src/vendeeglobe/sphere.py b/src/vendeeglobe/sphere.py
new file mode 100644
index 0000000..7b7b317
--- /dev/null
+++ b/src/vendeeglobe/sphere.py
@@ -0,0 +1,147 @@
+# flake8: noqa F405
+import time
+from typing import Any, Dict
+
+import numpy as np
+import pyqtgraph as pg
+import pyqtgraph.opengl as gl
+from matplotlib.colors import to_rgba
+from OpenGL.GL import * # noqa
+from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem
+
+
+from . import config
+from . import utils as ut
+from .map import MapTextures
+from .player import Player
+from .utils import array_from_shared_mem
+from .weather import Weather
+
+
+class GLTexturedSphereItem(GLGraphicsItem):
+ """
+ **Bases:** :class:`GLGraphicsItem `
+
+ Displays image data as a textured quad.
+ """
+
+ def __init__(
+ self,
+ data: np.ndarray,
+ smooth: bool = False,
+ glOptions: str = "translucent",
+ parentItem: Any = None,
+ ):
+ """
+ **Arguments:**
+ data:
+ Volume data to be rendered. *Must* be 3D numpy array (x, y, RGBA) with
+ dtype=ubyte. (See functions.makeRGBA)
+ smooth:
+ If True, the volume slices are rendered with linear interpolation
+ """
+
+ self.smooth = smooth
+ self._needUpdate = False
+ super().__init__(parentItem=parentItem)
+ self.setData(data)
+ self.setGLOptions(glOptions)
+ self.texture = None
+
+ def initializeGL(self):
+ if self.texture is not None:
+ return
+ glEnable(GL_TEXTURE_2D)
+ self.texture = glGenTextures(1)
+
+ def setData(self, data: np.ndarray):
+ self.data = data
+ self._needUpdate = True
+ self.update()
+
+ def _updateTexture(self):
+ glBindTexture(GL_TEXTURE_2D, self.texture)
+ if self.smooth:
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ else:
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
+ # glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
+ shape = self.data.shape
+
+ ## Test texture dimensions first
+ glTexImage2D(
+ GL_PROXY_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ shape[0],
+ shape[1],
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ None,
+ )
+ if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0:
+ raise Exception(
+ "OpenGL failed to create 2D texture (%dx%d); too large for this hardware."
+ % shape[:2]
+ )
+
+ data = np.ascontiguousarray(self.data.transpose((1, 0, 2)))
+ glTexImage2D(
+ GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ shape[0],
+ shape[1],
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ data,
+ )
+ glDisable(GL_TEXTURE_2D)
+
+ def paint(self):
+ if self._needUpdate:
+ self._updateTexture()
+ self._needUpdate = False
+ glEnable(GL_TEXTURE_2D)
+ glBindTexture(GL_TEXTURE_2D, self.texture)
+
+ self.setupGLState()
+
+ glColor4f(1, 1, 1, 1)
+
+ theta = np.linspace(0, np.pi, 25, dtype="float32")
+ phi = np.linspace(0, 2 * np.pi, 49, dtype="float32")
+ t_n = theta / np.pi
+ p_n = phi / (2 * np.pi)
+
+ phi_grid, theta_grid = np.meshgrid(phi, theta, indexing="ij")
+ x, y, z = ut.to_xyz(phi_grid, theta_grid, gl=True)
+
+ glBegin(GL_QUADS)
+ for j in range(len(theta) - 1):
+ for i in range(len(phi) - 1):
+ glTexCoord2f(p_n[i], t_n[j])
+ glVertex3f(x[i, j], y[i, j], z[i, j])
+ glTexCoord2f(p_n[i], t_n[j + 1])
+ glVertex3f(x[i, j + 1], y[i, j + 1], z[i, j + 1])
+ glTexCoord2f(p_n[i + 1], t_n[j + 1])
+ glVertex3f(x[i + 1, j + 1], y[i + 1, j + 1], z[i + 1, j + 1])
+ glTexCoord2f(p_n[i + 1], t_n[j])
+ glVertex3f(x[i + 1, j], y[i + 1, j], z[i + 1, j])
+
+ glEnd()
+ glDisable(GL_TEXTURE_2D)
+
+
+"""
+Use GLImageItem to display image data on rectangular planes.
+
+In this example, the image data is sampled from a volume and the image planes
+placed as if they slice through the volume.
+"""
diff --git a/src/vendeeglobe/utils.py b/src/vendeeglobe/utils.py
index 040c141..20462a5 100644
--- a/src/vendeeglobe/utils.py
+++ b/src/vendeeglobe/utils.py
@@ -2,16 +2,28 @@
import hashlib
import time
+from multiprocessing.shared_memory import SharedMemory
from typing import Tuple, Union
import numba
import numpy as np
from . import config
+from .core import Location
RADIUS = float(config.map_radius)
+def array_from_shared_mem(
+ shared_mem: SharedMemory,
+ shared_data_dtype: np.dtype,
+ shared_data_shape: Tuple[int, ...],
+) -> np.ndarray:
+ arr = np.frombuffer(shared_mem.buf, dtype=shared_data_dtype)
+ arr = arr.reshape(shared_data_shape)
+ return arr
+
+
def string_to_color(input_string: str) -> str:
hash_object = hashlib.md5(input_string.encode())
hex_hash = hash_object.hexdigest()
@@ -119,45 +131,20 @@ def longitude_difference(lon1: float, lon2: float) -> float:
return -min(-lon_diff, crossing_diff)
-# def gkern(sigma=1):
-# """
-# Creates gaussian kernel
-# """
-# ax = np.linspace(-(sigma - 1) / 2.0, (sigma - 1) / 2.0, sigma)
-# gauss = np.exp(-0.5 * np.square(ax) / np.square(sigma))
-# kernel = np.outer(gauss, gauss)
-# return kernel / np.sum(kernel)
-
-
-# @numba.njit(cache=True)
-# def blur_weather(u: np.ndarray, v: np.ndarray, n: int) -> Tuple[np.ndarray, np.ndarray]:
-# u_out = np.zeros((n,) + u.shape)
-# v_out = np.zeros_like(u_out)
-# nt, ny, nx = u.shape
-# for k in range(n):
-# sigma = 2 * k + 1
-# ax = np.linspace(-(sigma - 1) / 2.0, (sigma - 1) / 2.0, sigma)
-# gauss = np.exp(-0.5 * np.square(ax) / np.square(sigma))
-# kernel = np.outer(gauss, gauss)
-# kernel /= np.sum(kernel)
-
-# for t in range(nt):
-# for j in range(ny):
-# for i in range(nx):
-# for jj in range(-k, k + 1):
-# for ii in range(-k, k + 1):
-# u_out[k, t, j, i] += (
-# kernel[jj, ii] * u[t, (j + jj) % ny, (i + ii) % nx]
-# )
-# v_out[k, t, j, i] += (
-# kernel[jj, ii] * v[t, (j + jj) % ny, (i + ii) % nx]
-# )
-# # u_out[0] = u
-# # v_out[0] = v
-# # for i in range(1, n):
-# # u_out[i] = uniform_filter(u, size=i * 2, mode="wrap")
-# # v_out[i] = uniform_filter(v, size=i * 2, mode="wrap")
-# return u_out, v_out
+def goto(origin: Location, to: Location):
+ """
+ Find the heading angle (in degrees) for the shortest distance from `origin` to `to`.
+ """
+ lon1 = np.radians(origin.longitude)
+ lat1 = np.radians(origin.latitude)
+ lon2 = np.radians(to.longitude)
+ lat2 = np.radians(to.latitude)
+
+ dlon = lon2 - lon1
+ y = np.sin(dlon) * np.cos(lat2)
+ x = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(dlon)
+ initial_bearing = -np.arctan2(y, x) + (np.pi * 0.5)
+ return (np.degrees(initial_bearing) + 360) % 360
def pre_compile():
diff --git a/src/vendeeglobe/weather.py b/src/vendeeglobe/weather.py
index 664db12..28dfdba 100644
--- a/src/vendeeglobe/weather.py
+++ b/src/vendeeglobe/weather.py
@@ -8,7 +8,7 @@
from scipy.ndimage import gaussian_filter, uniform_filter
from . import config
-from .utils import lat_degs_from_length, lon_degs_from_length, wrap
+from . import utils as ut
@dataclass(frozen=True)
@@ -20,25 +20,37 @@ class WeatherForecast:
dt: float
def get_uv(
- self, lat: np.ndarray, lon: np.ndarray, t: np.ndarray
+ self, *, latitudes: np.ndarray, longitudes: np.ndarray, times: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
- iv = ((lat + 90.0) / self.dv).astype(int)
- iu = ((lon + 180.0) / self.du).astype(int)
- it = ((t / config.seconds_to_hours) / self.dt).astype(int) # % self.nt
+ """
+ Get the wind speed at the given latitude, longitude and time.
+
+ Parameters
+ ----------
+ latitudes: np.ndarray
+ Latitude(s) in degrees.
+ longitudes: np.ndarray
+ Longitude(s) in degrees.
+ times: np.ndarray
+ Time in hours.
+ """
+ iv = ((np.asarray(latitudes) + 90.0) / self.dv).astype(int)
+ iu = ((np.asarray(longitudes) + 180.0) / self.du).astype(int)
+ it = ((np.asarray(times) / config.seconds_to_hours) / self.dt).astype(int)
u = self.u[it, iv, iu]
v = self.v[it, iv, iu]
return u, v
-class Weather:
- def __init__(self, time_limit: int, seed: Optional[int] = None):
+class WeatherData:
+ def __init__(self, seed: Optional[int] = None):
t0 = time.time()
print("Generating weather...", end=" ", flush=True)
rng = np.random.default_rng(seed)
self.ny = 128
self.nx = self.ny * 2
- self.nt = int(time_limit / config.weather_update_interval)
+ self.nt = int(config.time_limit / config.weather_update_interval)
self.dt = config.weather_update_interval # weather changes every 12 hours
@@ -68,22 +80,6 @@ def __init__(self, time_limit: int, seed: Optional[int] = None):
self.u *= speed
self.v *= speed
- lat_min = -90
- lat_max = 90
- self.dv = (lat_max - lat_min) / self.ny
- lon_min = -180
- lon_max = 180
- self.du = (lon_max - lon_min) / self.nx
-
- size = (config.tracer_lifetime, config.ntracers)
- self.tracer_lat = np.random.uniform(-89.9, 89.9, size=size)
- self.tracer_lon = np.random.uniform(-180, 180, size=size)
- self.tracer_colors = np.ones(self.tracer_lat.shape + (4,))
- self.tracer_colors[..., 3] = np.linspace(1, 0, 50).reshape((-1, 1))
-
- self.number_of_new_tracers = 5
- self.new_tracer_counter = 0
-
# Make forecast data
self.forecast_times = np.arange(
0, config.forecast_length * 6, config.weather_update_interval
@@ -111,10 +107,49 @@ def __init__(self, time_limit: int, seed: Optional[int] = None):
self.v.setflags(write=False)
self.forecast_u.setflags(write=False)
self.forecast_v.setflags(write=False)
+
print(f"done [{time.time() - t0:.2f} s]")
+
+class Weather:
+ def __init__(
+ self,
+ pid: int,
+ seed: int,
+ weather_u: np.ndarray,
+ weather_v: np.ndarray,
+ forecast_u: np.ndarray,
+ forecast_v: np.ndarray,
+ forecast_t: np.ndarray,
+ tracer_positions: np.ndarray,
+ ):
+ self.pid = pid
+ self.u = weather_u
+ self.v = weather_v
+ self.forecast_u = forecast_u
+ self.forecast_v = forecast_v
+ self.forecast_t = forecast_t
+ self.tracer_positions = tracer_positions
+
+ self.nt, self.ny, self.nx = self.u.shape
+ self.dt = config.weather_update_interval # weather changes every 12 hours
+
+ lat_min = -90
+ lat_max = 90
+ self.dv = (lat_max - lat_min) / self.ny
+ lon_min = -180
+ lon_max = 180
+ self.du = (lon_max - lon_min) / self.nx
+
+ size = self.tracer_positions.shape[1:-1]
+ self.rng = np.random.default_rng(pid + (seed if seed is not None else 0))
+
+ self.tracer_lat = self.rng.uniform(-89.9, 89.9, size=size)
+ self.tracer_lon = self.rng.uniform(-180, 180, size=size)
+ self.new_tracer_counter = 0
+
def get_forecast(self, t: float) -> WeatherForecast:
- t = t + self.forecast_times
+ t = t + self.forecast_t
it = (t / self.dt).astype(int) % self.nt
ik = np.arange(len(t))
return WeatherForecast(
@@ -136,6 +171,7 @@ def get_uv(
return u, v
def update_wind_tracers(self, t: float, dt: float):
+ # print('update_wind_tracers', t)
self.tracer_lat = np.roll(self.tracer_lat, 1, axis=0)
self.tracer_lon = np.roll(self.tracer_lon, 1, axis=0)
@@ -144,20 +180,26 @@ def update_wind_tracers(self, t: float, dt: float):
scaling = 1.0
incr_x = u * dt * scaling
incr_y = v * dt * scaling
- incr_lon = lon_degs_from_length(incr_x, self.tracer_lat[1, :])
- incr_lat = lat_degs_from_length(incr_y)
+ incr_lon = ut.lon_degs_from_length(incr_x, self.tracer_lat[1, :])
+ incr_lat = ut.lat_degs_from_length(incr_y)
- self.tracer_lat[0, :], self.tracer_lon[0, :] = wrap(
+ self.tracer_lat[0, :], self.tracer_lon[0, :] = ut.wrap(
lat=self.tracer_lat[1, :] + incr_lat, lon=self.tracer_lon[1, :] + incr_lon
)
# Randomly replace tracers
- new_lat = np.random.uniform(-89.9, 89.9, size=(self.number_of_new_tracers,))
- new_lon = np.random.uniform(-180, 180, size=(self.number_of_new_tracers,))
+ new_lat = self.rng.uniform(-89.9, 89.9, size=(config.number_of_new_tracers,))
+ new_lon = self.rng.uniform(-180, 180, size=(config.number_of_new_tracers,))
istart = self.new_tracer_counter
- iend = self.new_tracer_counter + self.number_of_new_tracers
+ iend = self.new_tracer_counter + config.number_of_new_tracers
self.tracer_lat[0, istart:iend] = new_lat
self.tracer_lon[0, istart:iend] = new_lon
self.new_tracer_counter = (
- self.new_tracer_counter + self.number_of_new_tracers
- ) % config.ntracers
+ self.new_tracer_counter + config.number_of_new_tracers
+ ) % self.tracer_lat.shape[1]
+
+ x, y, z = ut.to_xyz(
+ ut.lon_to_phi(self.tracer_lon), ut.lat_to_theta(self.tracer_lat)
+ )
+ # Using transpose is apparently faster than np.stack
+ self.tracer_positions[self.pid, ...] = np.array([x, y, z]).transpose(1, 2, 0)