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)