Skip to content

Commit 7b457ee

Browse files
authored
Merge pull request #39 from MeteoSwiss-APN/dev-Peiderk
Fix issue 38
2 parents bdecc36 + 9d7815e commit 7b457ee

6 files changed

Lines changed: 107 additions & 39 deletions

File tree

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ instructions here below to set up a conda environment and test multiple use-case
3232

3333
### Create environment
3434

35-
Create an environment and install the package dependencies with the script `setup_env.sh` provided in the `tools` directory.
36-
Check available options with
35+
Create an environment and install the package dependencies with conda
3736

38-
tools/setup_env.sh -h
37+
conda env create -n pytrajplot -f requirements/environment.yml
3938

4039
We distinguish pinned installations based on exported (reproducible) environments,
4140
saved in `requirements/environment.yml`,
@@ -72,7 +71,7 @@ installed by conda and should not be modified by pip, use the `--no-deps` flag.
7271

7372
For development, install the package in editable mode:
7473

75-
pip install --editable --no-deps .
74+
pip install --no-deps --editable .
7675

7776
*Warning:* Make sure you use the right pip, i.e. the one from the installed conda environment (`which pip` should point to something like `path/to/miniconda/envs/<package_env_name>/bin/pip`).
7877

src/pytrajplot/parse_data.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -360,34 +360,34 @@ def read_trajectory(trajectory_file_path, start_df, plot_info_dict):
360360
# clean up the df in case its generated from a COSMO trajectory file
361361
if case == "COSMO":
362362
# at missing data points (i.e. if trajectory leaves computational domain), the z & hsurf values default to -999
363-
traj_df.loc[(traj_df["z"] < 0), "z"] = np.NaN
364-
traj_df.loc[(traj_df["hsurf"] < 0), "hsurf"] = np.NaN
363+
traj_df.loc[(traj_df["z"] < 0), "z"] = np.nan
364+
traj_df.loc[(traj_df["hsurf"] < 0), "hsurf"] = np.nan
365365
traj_df.dropna(
366366
subset=["lon"], inplace=True
367367
) # remove rows containing only the origin/z_type
368368
traj_df.reset_index(inplace=True)
369369

370370
# clean up the df in case its generated from a HRES trajectory file
371371
if case == "HRES":
372-
traj_df.loc[(traj_df["lon"] == -999.00), "lon"] = np.NaN
373-
traj_df.loc[(traj_df["lat"] == -999.00), "lat"] = np.NaN
374-
traj_df.loc[(traj_df["z"] == -999), "z"] = np.NaN
372+
traj_df.loc[(traj_df["lon"] == -999.00), "lon"] = np.nan
373+
traj_df.loc[(traj_df["lat"] == -999.00), "lat"] = np.nan
374+
traj_df.loc[(traj_df["z"] == -999), "z"] = np.nan
375375

376376
# add various (empty) keys to the trajectory dataframe
377377
traj_df["z_type"] = None
378378
traj_df["origin"] = None
379379
traj_df["side_traj"] = None
380-
traj_df["start_altitude"] = np.NaN
381-
traj_df["lon_precise"] = np.NaN
382-
traj_df["lat_precise"] = np.NaN
380+
traj_df["start_altitude"] = np.nan
381+
traj_df["lon_precise"] = np.nan
382+
traj_df["lat_precise"] = np.nan
383383
traj_df["altitude_levels"] = None
384384
traj_df["#trajectories"] = number_of_trajectories
385385
traj_df["block_length"] = number_of_times
386386
traj_df["trajectory_direction"] = trajectory_file_path[
387387
-1:
388388
] # last letter of key = F/B --> direction of trajectory
389-
traj_df["subplot_index"] = np.NaN
390-
traj_df["max_start_altitude"] = np.NaN
389+
traj_df["subplot_index"] = np.nan
390+
traj_df["max_start_altitude"] = np.nan
391391

392392
# add information to newly created (empty) keys in trajectory dataframe
393393
# basically, at this point the information from the start_df, gets merged into the trajectory dataframe
@@ -443,8 +443,8 @@ def read_trajectory(trajectory_file_path, start_df, plot_info_dict):
443443
reference_time=reference_time,
444444
)
445445

446-
# change the lon/lat values where the trajectory leaves the domain from their computational domain-boundary values to np.NaN.
447-
traj_df.loc[np.isnan(traj_df["z"]), ["lon", "lat"]] = np.NaN
446+
# change the lon/lat values where the trajectory leaves the domain from their computational domain-boundary values to np.nan.
447+
traj_df.loc[np.isnan(traj_df["z"]), ["lon", "lat"]] = np.nan
448448

449449
return traj_df
450450

src/pytrajplot/plotting/analyse_trajectories.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import matplotlib.pyplot as plt
1010
import numpy as np
1111
import pandas as pd
12-
from numpy.lib.function_base import mean
12+
from numpy import mean
1313

1414

1515
# FUNCTIONS
@@ -306,7 +306,8 @@ def _create_plot(traj_dict, central_longitude, dynamic_domain):
306306
dynamic_domain[2] - offset,
307307
dynamic_domain[3] + offset,
308308
],
309-
ccrs.PlateCarree(central_longitude=0),
309+
#in the following line i substituted 0 with the second central_longitude
310+
ccrs.PlateCarree(central_longitude=central_longitude),
310311
)
311312

312313
# https://stackoverflow.com/questions/65086715/longitude-bounds-of-cartopy-crs-geodetic

src/pytrajplot/plotting/plot_map.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import pandas as pd
1616

1717
# Local
18-
from ..__init__ import cities_data_path
18+
from .. import cities_data_path
1919
from .plot_utils import alps_cities_list
2020
from .plot_utils import centraleurope_cities_list
2121
from .plot_utils import ch_cities_list
@@ -216,10 +216,6 @@ def is_visible(lat, lon, domain_boundaries, cross_dateline) -> bool:
216216
bool True if city is within domain boundaries, else false.
217217
218218
"""
219-
if cross_dateline:
220-
if lon < 0:
221-
lon = 360 - abs(lon)
222-
223219
lon = float(lon.iloc[0]) if isinstance(lon, pd.Series) else float(lon)
224220
lat = float(lat.iloc[0]) if isinstance(lat, pd.Series) else float(lat)
225221
is_in_domain = (
@@ -721,6 +717,64 @@ def get_intersection_point_on_domain_boundaries(
721717
intersection_point = intersection_point + 0.001 * (point_in - intersection_point)
722718
return intersection_point
723719

720+
#the next two functions are used later to plot the slice of traj beyond the dateline
721+
def _unwrap_dateline_series(lon: pd.Series | np.ndarray) -> pd.Series:
722+
"""Makes the trajectory continuous avoiding the jump -180↔+180 (e.g. -179 → -181)."""
723+
arr = np.asarray(lon, dtype=float)
724+
if arr.size == 0:
725+
return pd.Series(arr, index=getattr(lon, "index", None))
726+
diffs = np.diff(arr)
727+
offset = 0.0
728+
out = [arr[0]]
729+
for d, x in zip(diffs, arr[1:]):
730+
if d > 180: # e.g. 179 → -179 (big backwards jump)
731+
offset -= 360
732+
elif d < -180: # e.g. -179 → 179 (big forwards jump)
733+
offset += 360
734+
out.append(x + offset)
735+
return pd.Series(out, index=getattr(lon, "index", None))
736+
737+
def create_trajectory_slice_over_dateline(
738+
lon: pd.Series | np.ndarray,
739+
lat: pd.Series | np.ndarray,
740+
threshold: float = 180.0
741+
) -> tuple[pd.Series, pd.Series]:
742+
"""
743+
gives back the subset (lon_slice, lat_slice) of the points that, after the unwrapping, have |lon| >= threshold (default 180).
744+
745+
Parameters
746+
---------
747+
lon : pd.Series | np.ndarray
748+
Longitudes of the trajectory.
749+
lat : pd.Series | np.ndarray
750+
Corresponding latitudes (same lenght).
751+
threshold : float, default 180.0
752+
threshold for the unwrapped longitudes |lon_unwrapped|.
753+
754+
Returns
755+
-------
756+
(lon_slice, lat_slice) : tuple[pd.Series, pd.Series]
757+
filtered series (maintain the original indexes if lon/lat were Series).
758+
"""
759+
# Convert to Series
760+
lon_s = lon if isinstance(lon, pd.Series) else pd.Series(np.asarray(lon, dtype=float))
761+
lat_s = lat if isinstance(lat, pd.Series) else pd.Series(np.asarray(lat, dtype=float))
762+
763+
if len(lon_s) != len(lat_s):
764+
raise ValueError("lon e lat must have the same length.")
765+
766+
# Unwrap longitudes
767+
lon_unwrapped = _unwrap_dateline_series(lon_s)
768+
769+
# identify points over the dateline
770+
mask = np.abs(lon_unwrapped.values) >= float(threshold)
771+
772+
# apply the mask mantaining the index
773+
lon_slice = lon_unwrapped[mask]
774+
lat_slice = lat_s[mask]
775+
776+
return lon_slice, lat_slice
777+
724778

725779
def add_trajectories_within_domain(
726780
plot_dict: dict,
@@ -753,10 +807,8 @@ def add_trajectories_within_domain(
753807
for traj in range(5):
754808
latitude = plot_dict["altitude_" + str(i)]["traj_" + str(traj)]["lat"]
755809
longitude = plot_dict["altitude_" + str(i)]["traj_" + str(traj)]["lon"]
756-
if cross_dateline:
757-
longitude = longitude.apply(
758-
lambda lon: 360 - np.abs(lon) if lon < 0 else lon
759-
)
810+
# Unwrap the longitude over the dateline
811+
longitude_unwrapped = _unwrap_dateline_series(longitude)
760812
ystart = latitude.iloc[0]
761813
xstart = longitude.iloc[0]
762814
linestyle = subplot_properties_dict[sub_index]
@@ -809,6 +861,21 @@ def add_trajectories_within_domain(
809861
transform=ccrs.Geodetic(),
810862
rasterized=True,
811863
)
864+
# The following plots the slice of the trajectory beyond the dateline that gets clipped
865+
if cross_dateline:
866+
lon_slice, lat_slice= create_trajectory_slice_over_dateline(longitude_unwrapped, latitude, 180.0)
867+
ax.plot(
868+
lon_slice, # define x-axis
869+
lat_slice, # define y-axis
870+
linestyle, # define linestyle
871+
alpha=alpha, # define line opacity
872+
label=(
873+
None
874+
), # We don't want this slice to have its own label
875+
transform=ccrs.Geodetic(),
876+
rasterized=True,
877+
)
878+
812879
if is_main_trajectory:
813880
# add time interval points to main trajectory
814881
add_time_interval_points_within_domain(
@@ -894,7 +961,7 @@ def generate_map_plot(
894961
domain=domain,
895962
custom_domain_boundaries=trajectory_expansion,
896963
origin_coordinates=origin_coordinates,
897-
) # sets extent of map
964+
)
898965
if not domain_boundaries:
899966
return ax.text(
900967
0.5,

tests/test_pytrajplot.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pytrajplot $input_dir_tests/test_hres/2_altitudes $output_dir_tests/test_hres/2_
2828
pytrajplot $input_dir_tests/test_hres/1_altitudes $output_dir_tests/test_hres/1_altitudes --datatype png --domain europe
2929

3030
echo "Test HRES backward trajectory"
31-
pytrajplot $input_dir_tests/test_hres/backward $output_dir_tests/test_hres/backward --datatype png --domain europe
31+
pytrajplot $input_dir_tests/test_hres/backward $output_dir_tests/test_hres/backward --datatype png --domain europe --domain dynamic
3232

3333
echo "Test all domains combined w/ HRES model"
3434
pytrajplot $input_dir_tests/test_hres/4_altitudes $output_dir_tests/test_hres/4_altitudes --datatype png --domain ch --domain alps --domain centraleurope --domain europe --domain dynamic
@@ -37,7 +37,7 @@ echo "Test HRES dateline crossing"
3737
pytrajplot $input_dir_tests/test_hres/dateline $output_dir_tests/test_hres/dateline --datatype png --domain dynamic
3838

3939
echo "Test HRES w/o side trajectories and the german case"
40-
pytrajplot $input_dir_tests/test_hres/dateline $output_dir_tests/test_hres/dateline/german --datatype png --domain europe --language de
40+
pytrajplot $input_dir_tests/test_hres/dateline $output_dir_tests/test_hres/dateline/german --datatype png --domain dynamic --language de
4141

4242
echo "Test HRES Europe with trajectory crossing of zero longitude from east and then leaving domain"
4343
# (failed in v1.0.0 due to NaNs in the argument of the numpy max function - v1.0.1 uses nanmax instead)

tests/test_recent_opr.sh

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
# Conda default environment
77
conda_env=pytrajplot
88
# Lagranto-output storage location
9-
store_osm=/store/mch/msopr/osm
9+
store_osm=/store_new/mch/msopr/osm
1010
# pytrajplot output location
11-
pytrajplot_out=.
11+
pytrajplot_out=local
1212
# Graphics format
13-
datatype_opt="--datatype png --datatype pdf"
13+
datatype_opt="--datatype png" # --datatype pdf"
1414
# Domain options for
15-
# COSMO-1E-CTR (c1), IFS-HRES-Europe (ie), IFS-HRES global (ig)
16-
domain_opt_c1="--domain ch --domain alps"
15+
# COSMO-1E-CTR (i1), IFS-HRES-Europe (ie), IFS-HRES global (ig)
16+
domain_opt_i1="--domain ch --domain alps"
1717
domain_opt_ie="--domain alps --domain centraleurope --domain europe"
1818
domain_opt_ig="--domain dynamic --domain dynamic_zoom"
1919
# --------
@@ -32,7 +32,8 @@ bt_00=$(date --utc --date="today 00" +%Y%m%d%H)
3232
bt_03=$(date --utc --date="today 03" +%Y%m%d%H)
3333

3434
# Load conda env for pytrajplot if CONDA_PREFIX not defined
35-
[[ -z $CONDA_PREFIX ]] && conda activate $conda_env
35+
#[[ -z $CONDA_PREFIX ]] &&
36+
conda activate $conda_env
3637
echo CONDA_PREFIX=$CONDA_PREFIX
3738
# Report version
3839
$CONDA_PREFIX/bin/pytrajplot --version
@@ -46,17 +47,17 @@ for basetime in $bt_06 $bt_12 $bt_18 $bt_21 $bt_00 $bt_03 ; do
4647
echo Basetime: $(date --utc --date="${basetime:0:8} $hh" "+%F %H UTC")
4748

4849
# Operational INPUT_DIRs:
49-
input_dir_c1=$(echo $store_osm/COSMO-1E/FCST${yy}/${yymmddhh}_4??/lagranto_c/000)
50+
input_dir_i1=$(echo $store_osm/ICON-CH1-EPS/FCST${yy}/${yymmddhh}_???/lagranto_c/000)
5051
input_dir_ie=$store_osm/IFS-HRES/IFS-HRES-LAGRANTO${yy}/${yymmddhh}_LIH/lagranto_f
5152
input_dir_ig=$store_osm/IFS-HRES/IFS-HRES-LAGRANTO${yy}/${yymmddhh}_LIH/lagranto_c
5253

5354
# Output directories
54-
output_dir_c1=$pytrajplot_out/plot_c1_${basetime}
55+
output_dir_i1=$pytrajplot_out/plot_i1_${basetime}
5556
output_dir_ie=$pytrajplot_out/plot_ie_${basetime}
5657
output_dir_ig=$pytrajplot_out/plot_ig_${basetime}
5758

5859
# Submit jobs
59-
for model in c1 ie ig ; do
60+
for model in i1 ie ig ; do
6061
eval input_dir=\$input_dir_$model
6162
eval output_dir=\$output_dir_$model
6263
eval domain_opt=\$domain_opt_$model

0 commit comments

Comments
 (0)