From 5c5bf9a52f24f9c3b85bfb849ecd54ede17dfa98 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Tue, 28 Jan 2025 13:40:20 -0500 Subject: [PATCH 01/23] draft script for converting MFP CSV to YAML schedule --- scripts/CoordinatesExport-Filled.csv | 31 ++++++++++++ scripts/coordinates_to_yaml_output.yaml | 52 ++++++++++++++++++++ scripts/mfp_2_yaml.py | 64 +++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 scripts/CoordinatesExport-Filled.csv create mode 100644 scripts/coordinates_to_yaml_output.yaml create mode 100644 scripts/mfp_2_yaml.py diff --git a/scripts/CoordinatesExport-Filled.csv b/scripts/CoordinatesExport-Filled.csv new file mode 100644 index 00000000..3bfaba2f --- /dev/null +++ b/scripts/CoordinatesExport-Filled.csv @@ -0,0 +1,31 @@ +Station Type,Name,Latitude,Longitude,Instrument,,,,,,,,, +Sampling Station,Sampling Station - 7,54.31186,5.215944,CTD,,,,,,,,, +Sampling Station,Sampling Station - 6,55.124089,5.156524,"CTD, DRIFTER",,,,,,,,, +Sampling Station,Sampling Station - 5,55.909027,4.601939,DRIFTER,,,,,,,,, +Sampling Station,Sampling Station - 4,55.886817,2.522248,ARGO_FLOAT,,,,,,,,, +Sampling Station,Sampling Station - 3,55.808982,1.036754,CTD,,,,,,,,, +Sampling Station,Sampling Station - 2,55.02203,2.403408,XBT,,,,,,,,, +Sampling Station,Sampling Station,54.091736,3.948322,CTD,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, +,,,,,,,,,,,,, diff --git a/scripts/coordinates_to_yaml_output.yaml b/scripts/coordinates_to_yaml_output.yaml new file mode 100644 index 00000000..f396cc45 --- /dev/null +++ b/scripts/coordinates_to_yaml_output.yaml @@ -0,0 +1,52 @@ +space_time_region: + spatial_range: + maximum_depth: 2000 + maximum_latitude: 57.909027 + maximum_longitude: 7.215944 + minimum_depth: 0 + minimum_latitude: 52.091736 + minimum_longitude: -0.963246 + time_range: + end_time: '2023-02-01 00:00:00' + start_time: '2023-01-01 00:00:00' +waypoints: +- instrument: CTD + location: + latitude: 54.31186 + longitude: 5.215944 + time: '2023-01-01 00:00:00' +- instrument: CTD + location: + latitude: 55.124089 + longitude: 5.156524 + time: '2023-01-01 01:00:00' +- instrument: DRIFTER + location: + latitude: 55.124089 + longitude: 5.156524 + time: '2023-01-01 01:00:00' +- instrument: DRIFTER + location: + latitude: 55.909027 + longitude: 4.601939 + time: '2023-01-01 02:00:00' +- instrument: ARGO_FLOAT + location: + latitude: 55.886817 + longitude: 2.522248 + time: '2023-01-01 03:00:00' +- instrument: CTD + location: + latitude: 55.808982 + longitude: 1.036754 + time: '2023-01-01 04:00:00' +- instrument: XBT + location: + latitude: 55.02203 + longitude: 2.403408 + time: '2023-01-01 05:00:00' +- instrument: CTD + location: + latitude: 54.091736 + longitude: 3.948322 + time: '2023-01-01 06:00:00' diff --git a/scripts/mfp_2_yaml.py b/scripts/mfp_2_yaml.py new file mode 100644 index 00000000..73f94c16 --- /dev/null +++ b/scripts/mfp_2_yaml.py @@ -0,0 +1,64 @@ +import yaml +import pandas as pd +import numpy as np + +start_time = "2023-01-01 00:00:00" +end_time = "2023-02-01 00:00:00" + + +coordinates_data = pd.read_csv("CoordinatesExport-Filled.csv", usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"]) +coordinates_data = coordinates_data.dropna() + +# Define maximum depth for each instrument +instrument_depth_map = { + "CTD": 2000, + "DRIFTER": 1, + "ARGO_FLOAT": 1500, +} + +unique_instruments = np.unique(np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values)) + +# Determine the maximum depth based on the unique instruments +maximum_depth = max(instrument_depth_map.get(inst, 0) for inst in unique_instruments) + +minimum_depth = 0 + +buffer = 2 + +# template for the yaml output +yaml_output = { + "space_time_region": { + "spatial_range": { + "minimum_longitude": coordinates_data["Longitude"].min()-buffer, + "maximum_longitude": coordinates_data["Longitude"].max()+buffer, + "minimum_latitude": coordinates_data["Latitude"].min()-buffer, + "maximum_latitude": coordinates_data["Latitude"].max()+buffer, + "minimum_depth": minimum_depth, + "maximum_depth": maximum_depth, + }, + "time_range": { + "start_time": start_time, + "end_time": end_time, + } + }, + "waypoints": [] +} + +for index, row in coordinates_data.iterrows(): + instruments = row["Instrument"].split(", ") + for instrument in instruments: + waypoint = { + "instrument": instrument, + "location": { + "latitude": row["Latitude"], + "longitude": row["Longitude"] + }, + "time": f"2023-01-01 {index:02d}:00:00" # Placeholder time TODO + } + yaml_output["waypoints"].append(waypoint) + +# Save the YAML content to a file +yaml_file_path = './coordinates_to_yaml_output.yaml' +with open(yaml_file_path, 'w') as file: + yaml.dump(yaml_output, file, default_flow_style=False) + From 40c7ff9dd0a0c01c18b1ac948dd2b8233635f820 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:43:19 +0000 Subject: [PATCH 02/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/coordinates_to_yaml_output.yaml | 84 ++++++++++++------------- scripts/mfp_2_yaml.py | 37 +++++------ 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/scripts/coordinates_to_yaml_output.yaml b/scripts/coordinates_to_yaml_output.yaml index f396cc45..e492e6f9 100644 --- a/scripts/coordinates_to_yaml_output.yaml +++ b/scripts/coordinates_to_yaml_output.yaml @@ -7,46 +7,46 @@ space_time_region: minimum_latitude: 52.091736 minimum_longitude: -0.963246 time_range: - end_time: '2023-02-01 00:00:00' - start_time: '2023-01-01 00:00:00' + end_time: "2023-02-01 00:00:00" + start_time: "2023-01-01 00:00:00" waypoints: -- instrument: CTD - location: - latitude: 54.31186 - longitude: 5.215944 - time: '2023-01-01 00:00:00' -- instrument: CTD - location: - latitude: 55.124089 - longitude: 5.156524 - time: '2023-01-01 01:00:00' -- instrument: DRIFTER - location: - latitude: 55.124089 - longitude: 5.156524 - time: '2023-01-01 01:00:00' -- instrument: DRIFTER - location: - latitude: 55.909027 - longitude: 4.601939 - time: '2023-01-01 02:00:00' -- instrument: ARGO_FLOAT - location: - latitude: 55.886817 - longitude: 2.522248 - time: '2023-01-01 03:00:00' -- instrument: CTD - location: - latitude: 55.808982 - longitude: 1.036754 - time: '2023-01-01 04:00:00' -- instrument: XBT - location: - latitude: 55.02203 - longitude: 2.403408 - time: '2023-01-01 05:00:00' -- instrument: CTD - location: - latitude: 54.091736 - longitude: 3.948322 - time: '2023-01-01 06:00:00' + - instrument: CTD + location: + latitude: 54.31186 + longitude: 5.215944 + time: "2023-01-01 00:00:00" + - instrument: CTD + location: + latitude: 55.124089 + longitude: 5.156524 + time: "2023-01-01 01:00:00" + - instrument: DRIFTER + location: + latitude: 55.124089 + longitude: 5.156524 + time: "2023-01-01 01:00:00" + - instrument: DRIFTER + location: + latitude: 55.909027 + longitude: 4.601939 + time: "2023-01-01 02:00:00" + - instrument: ARGO_FLOAT + location: + latitude: 55.886817 + longitude: 2.522248 + time: "2023-01-01 03:00:00" + - instrument: CTD + location: + latitude: 55.808982 + longitude: 1.036754 + time: "2023-01-01 04:00:00" + - instrument: XBT + location: + latitude: 55.02203 + longitude: 2.403408 + time: "2023-01-01 05:00:00" + - instrument: CTD + location: + latitude: 54.091736 + longitude: 3.948322 + time: "2023-01-01 06:00:00" diff --git a/scripts/mfp_2_yaml.py b/scripts/mfp_2_yaml.py index 73f94c16..801f4580 100644 --- a/scripts/mfp_2_yaml.py +++ b/scripts/mfp_2_yaml.py @@ -1,12 +1,15 @@ -import yaml -import pandas as pd import numpy as np +import pandas as pd +import yaml start_time = "2023-01-01 00:00:00" end_time = "2023-02-01 00:00:00" -coordinates_data = pd.read_csv("CoordinatesExport-Filled.csv", usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"]) +coordinates_data = pd.read_csv( + "CoordinatesExport-Filled.csv", + usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"], +) coordinates_data = coordinates_data.dropna() # Define maximum depth for each instrument @@ -16,7 +19,9 @@ "ARGO_FLOAT": 1500, } -unique_instruments = np.unique(np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values)) +unique_instruments = np.unique( + np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values) +) # Determine the maximum depth based on the unique instruments maximum_depth = max(instrument_depth_map.get(inst, 0) for inst in unique_instruments) @@ -29,19 +34,19 @@ yaml_output = { "space_time_region": { "spatial_range": { - "minimum_longitude": coordinates_data["Longitude"].min()-buffer, - "maximum_longitude": coordinates_data["Longitude"].max()+buffer, - "minimum_latitude": coordinates_data["Latitude"].min()-buffer, - "maximum_latitude": coordinates_data["Latitude"].max()+buffer, + "minimum_longitude": coordinates_data["Longitude"].min() - buffer, + "maximum_longitude": coordinates_data["Longitude"].max() + buffer, + "minimum_latitude": coordinates_data["Latitude"].min() - buffer, + "maximum_latitude": coordinates_data["Latitude"].max() + buffer, "minimum_depth": minimum_depth, "maximum_depth": maximum_depth, }, "time_range": { "start_time": start_time, "end_time": end_time, - } + }, }, - "waypoints": [] + "waypoints": [], } for index, row in coordinates_data.iterrows(): @@ -49,16 +54,12 @@ for instrument in instruments: waypoint = { "instrument": instrument, - "location": { - "latitude": row["Latitude"], - "longitude": row["Longitude"] - }, - "time": f"2023-01-01 {index:02d}:00:00" # Placeholder time TODO + "location": {"latitude": row["Latitude"], "longitude": row["Longitude"]}, + "time": f"2023-01-01 {index:02d}:00:00", # Placeholder time TODO } yaml_output["waypoints"].append(waypoint) # Save the YAML content to a file -yaml_file_path = './coordinates_to_yaml_output.yaml' -with open(yaml_file_path, 'w') as file: +yaml_file_path = "./coordinates_to_yaml_output.yaml" +with open(yaml_file_path, "w") as file: yaml.dump(yaml_output, file, default_flow_style=False) - From 366dede91ae118cea232102c743f3341bce56723 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Mon, 3 Feb 2025 12:31:45 -0500 Subject: [PATCH 03/23] add openpyxl --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index e5f6ea5b..b83dace7 100644 --- a/environment.yml +++ b/environment.yml @@ -12,6 +12,7 @@ dependencies: - pip - pyyaml - copernicusmarine >= 2 + - openpyxl >= 3.1.5 # linting - pre-commit From d0464444fbb6a0b9dd896291aa361f1dfcdfb40e Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Mon, 3 Feb 2025 12:34:22 -0500 Subject: [PATCH 04/23] add mfp_to_yaml function --- src/virtualship/utils.py | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 95d47d31..089297b2 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -1,6 +1,9 @@ from functools import lru_cache from importlib.resources import files from typing import TextIO +import pandas as pd +import numpy as np +import os import yaml from pydantic import BaseModel @@ -37,3 +40,88 @@ def _dump_yaml(model: BaseModel, stream: TextIO) -> str | None: def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel: """Load a yaml string into a pydantic model.""" return model.model_validate(yaml.safe_load(data)) + + + +def mfp_to_yaml(excel_file_path: str, save_directory: str): + """ + Generates a YAML file (`schedule.yaml`) with spatial and temporal information based on instrument data from MFP excel file. + + Parameters: + - excel_file_path (str): Path to the Excel file containing coordinate and instrument data. + - save_directory (str): Directory where `schedule.yaml` will be saved. + + The function: + 1. Reads instrument and location data from the Excel file. + 2. Determines the maximum depth and buffer based on the instruments present. + 3. Ensures longitude and latitude values remain valid after applying buffer adjustments. + 4. Saves `schedule.yaml` in the specified directory. + """ + + # Read data from Excel + coordinates_data = pd.read_excel(excel_file_path, usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"]) + coordinates_data = coordinates_data.dropna() + + # Define maximum depth and buffer for each instrument + instrument_properties = { + "CTD": {"depth": 5000, "buffer": 1}, + "DRIFTER": {"depth": 1, "buffer": 5}, + "ARGO_FLOAT": {"depth": 2000, "buffer": 5}, + } + + # Extract unique instruments from dataset + unique_instruments = np.unique(np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values)) + + # Determine the maximum depth based on the unique instruments + maximum_depth = max(instrument_properties.get(inst, {"depth": 0})["depth"] for inst in unique_instruments) + minimum_depth = 0 + + # Determine the buffer based on the maximum buffer of the instruments present + buffer = max(instrument_properties.get(inst, {"buffer": 0})["buffer"] for inst in unique_instruments) + + # Adjusted spatial range + min_longitude = coordinates_data["Longitude"].min() - buffer + max_longitude = coordinates_data["Longitude"].max() + buffer + min_latitude = coordinates_data["Latitude"].min() - buffer + max_latitude = coordinates_data["Latitude"].max() + buffer + + # Template for the YAML output + yaml_output = { + "space_time_region": { + "spatial_range": { + "minimum_longitude": min_longitude, + "maximum_longitude": max_longitude, + "minimum_latitude": min_latitude, + "maximum_latitude": max_latitude, + "minimum_depth": minimum_depth, + "maximum_depth": maximum_depth, + }, + "time_range": { + "start_time": "", # Blank start time + "end_time": "", # Blank end time + } + }, + "waypoints": [] + } + + # Populate waypoints + for _, row in coordinates_data.iterrows(): + instruments = row["Instrument"].split(", ") + for instrument in instruments: + waypoint = { + "instrument": instrument, + "location": { + "latitude": row["Latitude"], + "longitude": row["Longitude"] + }, + "time": "" # Blank time + } + yaml_output["waypoints"].append(waypoint) + + # Ensure save directory exists + os.makedirs(save_directory, exist_ok=True) + + # Save the YAML file + yaml_file_path = os.path.join(save_directory, SCHEDULE) + with open(yaml_file_path, 'w') as file: + yaml.dump(yaml_output, file, default_flow_style=False) From e3199fa0b31ddc0801c047321142f230f285bbad Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Mon, 3 Feb 2025 12:35:25 -0500 Subject: [PATCH 05/23] add new command to init to accept mfp file as input --- src/virtualship/cli/commands.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/virtualship/cli/commands.py b/src/virtualship/cli/commands.py index db4309b0..1def8a25 100644 --- a/src/virtualship/cli/commands.py +++ b/src/virtualship/cli/commands.py @@ -16,7 +16,7 @@ hash_to_filename, ) from virtualship.expedition.do_expedition import _get_schedule, do_expedition -from virtualship.utils import SCHEDULE, SHIP_CONFIG +from virtualship.utils import SCHEDULE, SHIP_CONFIG, mfp_to_yaml @click.command() @@ -24,8 +24,18 @@ "path", type=click.Path(exists=False, file_okay=False, dir_okay=True), ) -def init(path): - """Initialize a directory for a new expedition, with an example schedule and ship config files.""" +@click.option( + "--mfp_file", + type=str, + default=None, + help="Partially initialise a project from an exported xlsx or csv file from NIOZ' Marine Facilities Planning tool (specifically the \"Export Coordinates > DD\" option). User edits are required after initialisation.", +) +def init(path, mfp_file): + """ + Initialize a directory for a new expedition, with an example schedule and ship config files. + + If --mfp_file is provided, it will generate the schedule from the MPF file instead. + """ path = Path(path) path.mkdir(exist_ok=True) @@ -43,7 +53,14 @@ def init(path): ) config.write_text(utils.get_example_config()) - schedule.write_text(utils.get_example_schedule()) + + if mfp_file: + # Generate schedule.yaml from the MPF file + click.echo(f"Generating schedule from {mfp_file}...") + mfp_to_yaml(mfp_file, str(path)) # Pass the path to save in the correct directory + else: + # Create a default example schedule + schedule.write_text(utils.get_example_schedule()) click.echo(f"Created '{config.name}' and '{schedule.name}' at {path}.") From d9fe46a7699481bee010e75ee40592a058da596b Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Mon, 3 Feb 2025 12:36:34 -0500 Subject: [PATCH 06/23] delete files from scripts/ --- scripts/CoordinatesExport-Filled.csv | 31 ------------ scripts/coordinates_to_yaml_output.yaml | 52 -------------------- scripts/mfp_2_yaml.py | 64 ------------------------- 3 files changed, 147 deletions(-) delete mode 100644 scripts/CoordinatesExport-Filled.csv delete mode 100644 scripts/coordinates_to_yaml_output.yaml delete mode 100644 scripts/mfp_2_yaml.py diff --git a/scripts/CoordinatesExport-Filled.csv b/scripts/CoordinatesExport-Filled.csv deleted file mode 100644 index 3bfaba2f..00000000 --- a/scripts/CoordinatesExport-Filled.csv +++ /dev/null @@ -1,31 +0,0 @@ -Station Type,Name,Latitude,Longitude,Instrument,,,,,,,,, -Sampling Station,Sampling Station - 7,54.31186,5.215944,CTD,,,,,,,,, -Sampling Station,Sampling Station - 6,55.124089,5.156524,"CTD, DRIFTER",,,,,,,,, -Sampling Station,Sampling Station - 5,55.909027,4.601939,DRIFTER,,,,,,,,, -Sampling Station,Sampling Station - 4,55.886817,2.522248,ARGO_FLOAT,,,,,,,,, -Sampling Station,Sampling Station - 3,55.808982,1.036754,CTD,,,,,,,,, -Sampling Station,Sampling Station - 2,55.02203,2.403408,XBT,,,,,,,,, -Sampling Station,Sampling Station,54.091736,3.948322,CTD,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, -,,,,,,,,,,,,, diff --git a/scripts/coordinates_to_yaml_output.yaml b/scripts/coordinates_to_yaml_output.yaml deleted file mode 100644 index f396cc45..00000000 --- a/scripts/coordinates_to_yaml_output.yaml +++ /dev/null @@ -1,52 +0,0 @@ -space_time_region: - spatial_range: - maximum_depth: 2000 - maximum_latitude: 57.909027 - maximum_longitude: 7.215944 - minimum_depth: 0 - minimum_latitude: 52.091736 - minimum_longitude: -0.963246 - time_range: - end_time: '2023-02-01 00:00:00' - start_time: '2023-01-01 00:00:00' -waypoints: -- instrument: CTD - location: - latitude: 54.31186 - longitude: 5.215944 - time: '2023-01-01 00:00:00' -- instrument: CTD - location: - latitude: 55.124089 - longitude: 5.156524 - time: '2023-01-01 01:00:00' -- instrument: DRIFTER - location: - latitude: 55.124089 - longitude: 5.156524 - time: '2023-01-01 01:00:00' -- instrument: DRIFTER - location: - latitude: 55.909027 - longitude: 4.601939 - time: '2023-01-01 02:00:00' -- instrument: ARGO_FLOAT - location: - latitude: 55.886817 - longitude: 2.522248 - time: '2023-01-01 03:00:00' -- instrument: CTD - location: - latitude: 55.808982 - longitude: 1.036754 - time: '2023-01-01 04:00:00' -- instrument: XBT - location: - latitude: 55.02203 - longitude: 2.403408 - time: '2023-01-01 05:00:00' -- instrument: CTD - location: - latitude: 54.091736 - longitude: 3.948322 - time: '2023-01-01 06:00:00' diff --git a/scripts/mfp_2_yaml.py b/scripts/mfp_2_yaml.py deleted file mode 100644 index 73f94c16..00000000 --- a/scripts/mfp_2_yaml.py +++ /dev/null @@ -1,64 +0,0 @@ -import yaml -import pandas as pd -import numpy as np - -start_time = "2023-01-01 00:00:00" -end_time = "2023-02-01 00:00:00" - - -coordinates_data = pd.read_csv("CoordinatesExport-Filled.csv", usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"]) -coordinates_data = coordinates_data.dropna() - -# Define maximum depth for each instrument -instrument_depth_map = { - "CTD": 2000, - "DRIFTER": 1, - "ARGO_FLOAT": 1500, -} - -unique_instruments = np.unique(np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values)) - -# Determine the maximum depth based on the unique instruments -maximum_depth = max(instrument_depth_map.get(inst, 0) for inst in unique_instruments) - -minimum_depth = 0 - -buffer = 2 - -# template for the yaml output -yaml_output = { - "space_time_region": { - "spatial_range": { - "minimum_longitude": coordinates_data["Longitude"].min()-buffer, - "maximum_longitude": coordinates_data["Longitude"].max()+buffer, - "minimum_latitude": coordinates_data["Latitude"].min()-buffer, - "maximum_latitude": coordinates_data["Latitude"].max()+buffer, - "minimum_depth": minimum_depth, - "maximum_depth": maximum_depth, - }, - "time_range": { - "start_time": start_time, - "end_time": end_time, - } - }, - "waypoints": [] -} - -for index, row in coordinates_data.iterrows(): - instruments = row["Instrument"].split(", ") - for instrument in instruments: - waypoint = { - "instrument": instrument, - "location": { - "latitude": row["Latitude"], - "longitude": row["Longitude"] - }, - "time": f"2023-01-01 {index:02d}:00:00" # Placeholder time TODO - } - yaml_output["waypoints"].append(waypoint) - -# Save the YAML content to a file -yaml_file_path = './coordinates_to_yaml_output.yaml' -with open(yaml_file_path, 'w') as file: - yaml.dump(yaml_output, file, default_flow_style=False) - From a79433cabb06af007fbdfaa8a804f80b03f7b884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:37:44 +0000 Subject: [PATCH 07/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/virtualship/cli/commands.py | 8 +++--- src/virtualship/utils.py | 45 ++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/virtualship/cli/commands.py b/src/virtualship/cli/commands.py index 1def8a25..5bc72ffe 100644 --- a/src/virtualship/cli/commands.py +++ b/src/virtualship/cli/commands.py @@ -28,7 +28,7 @@ "--mfp_file", type=str, default=None, - help="Partially initialise a project from an exported xlsx or csv file from NIOZ' Marine Facilities Planning tool (specifically the \"Export Coordinates > DD\" option). User edits are required after initialisation.", + help='Partially initialise a project from an exported xlsx or csv file from NIOZ\' Marine Facilities Planning tool (specifically the "Export Coordinates > DD" option). User edits are required after initialisation.', ) def init(path, mfp_file): """ @@ -53,11 +53,13 @@ def init(path, mfp_file): ) config.write_text(utils.get_example_config()) - + if mfp_file: # Generate schedule.yaml from the MPF file click.echo(f"Generating schedule from {mfp_file}...") - mfp_to_yaml(mfp_file, str(path)) # Pass the path to save in the correct directory + mfp_to_yaml( + mfp_file, str(path) + ) # Pass the path to save in the correct directory else: # Create a default example schedule schedule.write_text(utils.get_example_schedule()) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 089297b2..69486fa4 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -1,10 +1,10 @@ +import os from functools import lru_cache from importlib.resources import files from typing import TextIO -import pandas as pd -import numpy as np -import os +import numpy as np +import pandas as pd import yaml from pydantic import BaseModel @@ -42,12 +42,12 @@ def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel: return model.model_validate(yaml.safe_load(data)) - def mfp_to_yaml(excel_file_path: str, save_directory: str): """ Generates a YAML file (`schedule.yaml`) with spatial and temporal information based on instrument data from MFP excel file. - Parameters: + Parameters + ---------- - excel_file_path (str): Path to the Excel file containing coordinate and instrument data. - save_directory (str): Directory where `schedule.yaml` will be saved. @@ -56,10 +56,13 @@ def mfp_to_yaml(excel_file_path: str, save_directory: str): 2. Determines the maximum depth and buffer based on the instruments present. 3. Ensures longitude and latitude values remain valid after applying buffer adjustments. 4. Saves `schedule.yaml` in the specified directory. - """ + """ # Read data from Excel - coordinates_data = pd.read_excel(excel_file_path, usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"]) + coordinates_data = pd.read_excel( + excel_file_path, + usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"], + ) coordinates_data = coordinates_data.dropna() # Define maximum depth and buffer for each instrument @@ -70,14 +73,22 @@ def mfp_to_yaml(excel_file_path: str, save_directory: str): } # Extract unique instruments from dataset - unique_instruments = np.unique(np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values)) + unique_instruments = np.unique( + np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values) + ) # Determine the maximum depth based on the unique instruments - maximum_depth = max(instrument_properties.get(inst, {"depth": 0})["depth"] for inst in unique_instruments) + maximum_depth = max( + instrument_properties.get(inst, {"depth": 0})["depth"] + for inst in unique_instruments + ) minimum_depth = 0 # Determine the buffer based on the maximum buffer of the instruments present - buffer = max(instrument_properties.get(inst, {"buffer": 0})["buffer"] for inst in unique_instruments) + buffer = max( + instrument_properties.get(inst, {"buffer": 0})["buffer"] + for inst in unique_instruments + ) # Adjusted spatial range min_longitude = coordinates_data["Longitude"].min() - buffer @@ -98,10 +109,10 @@ def mfp_to_yaml(excel_file_path: str, save_directory: str): }, "time_range": { "start_time": "", # Blank start time - "end_time": "", # Blank end time - } + "end_time": "", # Blank end time + }, }, - "waypoints": [] + "waypoints": [], } # Populate waypoints @@ -112,16 +123,16 @@ def mfp_to_yaml(excel_file_path: str, save_directory: str): "instrument": instrument, "location": { "latitude": row["Latitude"], - "longitude": row["Longitude"] + "longitude": row["Longitude"], }, - "time": "" # Blank time + "time": "", # Blank time } yaml_output["waypoints"].append(waypoint) # Ensure save directory exists os.makedirs(save_directory, exist_ok=True) - + # Save the YAML file yaml_file_path = os.path.join(save_directory, SCHEDULE) - with open(yaml_file_path, 'w') as file: + with open(yaml_file_path, "w") as file: yaml.dump(yaml_output, file, default_flow_style=False) From 11332f892ff05a4b5a1116c2a08db5055fdfc2fd Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 14:28:53 -0500 Subject: [PATCH 08/23] export the schedule body instead of saving file --- src/virtualship/utils.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 69486fa4..9c22ec89 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -42,20 +42,19 @@ def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel: return model.model_validate(yaml.safe_load(data)) -def mfp_to_yaml(excel_file_path: str, save_directory: str): +def mfp_to_yaml(excel_file_path: str): """ - Generates a YAML file (`schedule.yaml`) with spatial and temporal information based on instrument data from MFP excel file. + Generates a YAML file with spatial and temporal information based on instrument data from MFP excel file. Parameters ---------- - excel_file_path (str): Path to the Excel file containing coordinate and instrument data. - - save_directory (str): Directory where `schedule.yaml` will be saved. The function: 1. Reads instrument and location data from the Excel file. 2. Determines the maximum depth and buffer based on the instruments present. 3. Ensures longitude and latitude values remain valid after applying buffer adjustments. - 4. Saves `schedule.yaml` in the specified directory. + 4. returns the yaml information. """ # Read data from Excel @@ -129,10 +128,6 @@ def mfp_to_yaml(excel_file_path: str, save_directory: str): } yaml_output["waypoints"].append(waypoint) - # Ensure save directory exists - os.makedirs(save_directory, exist_ok=True) + return yaml.dump(yaml_output, default_flow_style=False) + - # Save the YAML file - yaml_file_path = os.path.join(save_directory, SCHEDULE) - with open(yaml_file_path, "w") as file: - yaml.dump(yaml_output, file, default_flow_style=False) From ad54992d12d8fc941e47e432c8dbfce480ef37cb Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 14:29:47 -0500 Subject: [PATCH 09/23] change name of cli param and adapt for new mfp_to_yaml function --- src/virtualship/cli/commands.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/virtualship/cli/commands.py b/src/virtualship/cli/commands.py index 5bc72ffe..84365735 100644 --- a/src/virtualship/cli/commands.py +++ b/src/virtualship/cli/commands.py @@ -25,7 +25,7 @@ type=click.Path(exists=False, file_okay=False, dir_okay=True), ) @click.option( - "--mfp_file", + "--mfp-file", type=str, default=None, help='Partially initialise a project from an exported xlsx or csv file from NIOZ\' Marine Facilities Planning tool (specifically the "Export Coordinates > DD" option). User edits are required after initialisation.', @@ -34,7 +34,7 @@ def init(path, mfp_file): """ Initialize a directory for a new expedition, with an example schedule and ship config files. - If --mfp_file is provided, it will generate the schedule from the MPF file instead. + If --mfp-file is provided, it will generate the schedule from the MPF file instead. """ path = Path(path) path.mkdir(exist_ok=True) @@ -57,12 +57,11 @@ def init(path, mfp_file): if mfp_file: # Generate schedule.yaml from the MPF file click.echo(f"Generating schedule from {mfp_file}...") - mfp_to_yaml( - mfp_file, str(path) - ) # Pass the path to save in the correct directory + schedule_body = mfp_to_yaml(mfp_file) else: # Create a default example schedule - schedule.write_text(utils.get_example_schedule()) + schedule_body = utils.get_example_schedule() + schedule.write_text(schedule_body) click.echo(f"Created '{config.name}' and '{schedule.name}' at {path}.") From 66adb1859720f7b81c036dc8291db821f21386cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:31:00 +0000 Subject: [PATCH 10/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/virtualship/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 9c22ec89..65ed9931 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -1,4 +1,3 @@ -import os from functools import lru_cache from importlib.resources import files from typing import TextIO @@ -129,5 +128,3 @@ def mfp_to_yaml(excel_file_path: str): yaml_output["waypoints"].append(waypoint) return yaml.dump(yaml_output, default_flow_style=False) - - From 2672afa57e798a1daf98f06d7303e22491dea0e0 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 17:18:28 -0500 Subject: [PATCH 11/23] add warning message for time entry on yaml --- src/virtualship/cli/commands.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/virtualship/cli/commands.py b/src/virtualship/cli/commands.py index 84365735..fb49e5c5 100644 --- a/src/virtualship/cli/commands.py +++ b/src/virtualship/cli/commands.py @@ -57,11 +57,16 @@ def init(path, mfp_file): if mfp_file: # Generate schedule.yaml from the MPF file click.echo(f"Generating schedule from {mfp_file}...") - schedule_body = mfp_to_yaml(mfp_file) + schedule_body = mfp_to_yaml(mfp_file, schedule) + click.echo( + "\n⚠️ The generated schedule does not contain time values. " + "\nPlease edit 'schedule.yaml' and manually add the necessary time values." + "\n🕒 Expected time format: 'YYYY-MM-DD HH:MM:SS' (e.g., '2023-10-20 01:00:00').\n" + ) else: # Create a default example schedule - schedule_body = utils.get_example_schedule() - schedule.write_text(schedule_body) + # schedule_body = utils.get_example_schedule() + schedule.write_text(utils.get_example_schedule()) click.echo(f"Created '{config.name}' and '{schedule.name}' at {path}.") From a37064185a014a737e917d98339ebe63362fa454 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 17:20:00 -0500 Subject: [PATCH 12/23] change to pydantic and change name of variables --- src/virtualship/utils.py | 99 +++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 65ed9931..f3f2b279 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -41,7 +41,7 @@ def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel: return model.model_validate(yaml.safe_load(data)) -def mfp_to_yaml(excel_file_path: str): +def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): """ Generates a YAML file with spatial and temporal information based on instrument data from MFP excel file. @@ -56,6 +56,13 @@ def mfp_to_yaml(excel_file_path: str): 4. returns the yaml information. """ + + # Importing Schedule and related models from expedition module + from virtualship.expedition.schedule import Schedule + from virtualship.expedition.space_time_region import SpaceTimeRegion, SpatialRange, TimeRange + from virtualship.expedition.waypoint import Waypoint, Location + from virtualship.expedition.instrument_type import InstrumentType + # Read data from Excel coordinates_data = pd.read_excel( excel_file_path, @@ -63,21 +70,24 @@ def mfp_to_yaml(excel_file_path: str): ) coordinates_data = coordinates_data.dropna() - # Define maximum depth and buffer for each instrument + # Define maximum depth (in meters) and buffer (in degrees) for each instrument instrument_properties = { - "CTD": {"depth": 5000, "buffer": 1}, - "DRIFTER": {"depth": 1, "buffer": 5}, - "ARGO_FLOAT": {"depth": 2000, "buffer": 5}, + "XBT": {"maximum_depth": 2000, "buffer": 1}, + "CTD": {"maximum_depth": 5000, "buffer": 1}, + "DRIFTER": {"maximum_depth": 1, "buffer": 5}, + "ARGO_FLOAT": {"maximum_depth": 2000, "buffer": 5}, } - # Extract unique instruments from dataset - unique_instruments = np.unique( - np.hstack(coordinates_data["Instrument"].apply(lambda a: a.split(", ")).values) - ) + # Extract unique instruments from dataset using a set + unique_instruments = set() + + for instrument_list in coordinates_data["Instrument"]: + instruments = instrument_list.split(", ") # Split by ", " to get individual instruments + unique_instruments |= set(instruments) # Union update with set of instruments # Determine the maximum depth based on the unique instruments maximum_depth = max( - instrument_properties.get(inst, {"depth": 0})["depth"] + instrument_properties.get(inst, {"maximum_depth": 0})["maximum_depth"] for inst in unique_instruments ) minimum_depth = 0 @@ -94,37 +104,42 @@ def mfp_to_yaml(excel_file_path: str): min_latitude = coordinates_data["Latitude"].min() - buffer max_latitude = coordinates_data["Latitude"].max() + buffer - # Template for the YAML output - yaml_output = { - "space_time_region": { - "spatial_range": { - "minimum_longitude": min_longitude, - "maximum_longitude": max_longitude, - "minimum_latitude": min_latitude, - "maximum_latitude": max_latitude, - "minimum_depth": minimum_depth, - "maximum_depth": maximum_depth, - }, - "time_range": { - "start_time": "", # Blank start time - "end_time": "", # Blank end time - }, - }, - "waypoints": [], - } - # Populate waypoints + spatial_range = SpatialRange( + minimum_longitude=coordinates_data["Longitude"].min() - buffer, + maximum_longitude=coordinates_data["Longitude"].max() + buffer, + minimum_latitude=coordinates_data["Latitude"].min() - buffer, + maximum_latitude=coordinates_data["Latitude"].max() + buffer, + minimum_depth=0, + maximum_depth=maximum_depth, + ) + + + # Create space-time region object + space_time_region = SpaceTimeRegion( + spatial_range=spatial_range, + time_range=TimeRange(), + ) + + # Generate waypoints + waypoints = [] for _, row in coordinates_data.iterrows(): - instruments = row["Instrument"].split(", ") - for instrument in instruments: - waypoint = { - "instrument": instrument, - "location": { - "latitude": row["Latitude"], - "longitude": row["Longitude"], - }, - "time": "", # Blank time - } - yaml_output["waypoints"].append(waypoint) - - return yaml.dump(yaml_output, default_flow_style=False) + instruments = [InstrumentType(instrument) for instrument in row["Instrument"].split(", ")] + waypoints.append( + Waypoint( + instrument=instruments, + location=Location(latitude=row["Latitude"], longitude=row["Longitude"]), + ) + ) + + # Create Schedule object + schedule = Schedule( + waypoints=waypoints, + space_time_region=space_time_region, + ) + + # Save to YAML file + schedule.to_yaml(yaml_output_path) + + + From b87d944c0392be9ef52c62b5eab518493318feb3 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 17:20:22 -0500 Subject: [PATCH 13/23] add XBT --- src/virtualship/expedition/instrument_type.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/virtualship/expedition/instrument_type.py b/src/virtualship/expedition/instrument_type.py index 556d8464..c8c93713 100644 --- a/src/virtualship/expedition/instrument_type.py +++ b/src/virtualship/expedition/instrument_type.py @@ -9,3 +9,5 @@ class InstrumentType(Enum): CTD = "CTD" DRIFTER = "DRIFTER" ARGO_FLOAT = "ARGO_FLOAT" + XBT = "XBT" + From eba08b8abc4032c3a8207d49b86c7d6953a70ea0 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 17:21:10 -0500 Subject: [PATCH 14/23] accept nonetype time --- src/virtualship/expedition/space_time_region.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/virtualship/expedition/space_time_region.py b/src/virtualship/expedition/space_time_region.py index 37aaee08..ee4608cd 100644 --- a/src/virtualship/expedition/space_time_region.py +++ b/src/virtualship/expedition/space_time_region.py @@ -38,20 +38,19 @@ def _check_lon_lat_domain(self) -> Self: raise ValueError("minimum_depth must be less than maximum_depth") return self - class TimeRange(BaseModel): """Defines the temporal boundaries for a space-time region.""" - start_time: datetime - end_time: datetime + start_time: datetime | None = None + end_time: datetime | None = None @model_validator(mode="after") def _check_time_range(self) -> Self: - if not self.start_time < self.end_time: - raise ValueError("start_time must be before end_time") + if self.start_time and self.end_time: + if not self.start_time < self.end_time: + raise ValueError("start_time must be before end_time") return self - class SpaceTimeRegion(BaseModel): """An space-time region with spatial and temporal boundaries.""" From c0a52acd69d8f4d98038d821a578cc6b1fefa23b Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 17:22:01 -0500 Subject: [PATCH 15/23] change to Waypoint to BaseModel and add field_serializer for instrument and time --- src/virtualship/expedition/waypoint.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/virtualship/expedition/waypoint.py b/src/virtualship/expedition/waypoint.py index 85e99181..258cb5fc 100644 --- a/src/virtualship/expedition/waypoint.py +++ b/src/virtualship/expedition/waypoint.py @@ -1,5 +1,6 @@ """Waypoint class.""" +from pydantic import BaseModel, field_serializer from dataclasses import dataclass from datetime import datetime @@ -7,10 +8,21 @@ from .instrument_type import InstrumentType -@dataclass -class Waypoint: +class Waypoint(BaseModel): """A Waypoint to sail to with an optional time and an optional instrument.""" location: Location time: datetime | None = None instrument: InstrumentType | list[InstrumentType] | None = None + + @field_serializer("instrument") + def serialize_instrument(self, instrument): + """Ensure InstrumentType is serialized as a string (or list of strings).""" + if isinstance(instrument, list): + return [inst.value for inst in instrument] + return instrument.value if instrument else None + + @field_serializer("time") + def serialize_time(self, time): + """Ensure datetime is formatted properly in YAML.""" + return time.strftime("%Y-%m-%d %H:%M:%S") if time else None \ No newline at end of file From 526d2af6da3c3ae0bda2464380ac6b0f0eefc18d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:22:23 +0000 Subject: [PATCH 16/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/virtualship/expedition/instrument_type.py | 1 - .../expedition/space_time_region.py | 2 ++ src/virtualship/expedition/waypoint.py | 6 ++-- src/virtualship/utils.py | 29 ++++++++++--------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/virtualship/expedition/instrument_type.py b/src/virtualship/expedition/instrument_type.py index c8c93713..82360c7b 100644 --- a/src/virtualship/expedition/instrument_type.py +++ b/src/virtualship/expedition/instrument_type.py @@ -10,4 +10,3 @@ class InstrumentType(Enum): DRIFTER = "DRIFTER" ARGO_FLOAT = "ARGO_FLOAT" XBT = "XBT" - diff --git a/src/virtualship/expedition/space_time_region.py b/src/virtualship/expedition/space_time_region.py index ee4608cd..a63734c7 100644 --- a/src/virtualship/expedition/space_time_region.py +++ b/src/virtualship/expedition/space_time_region.py @@ -38,6 +38,7 @@ def _check_lon_lat_domain(self) -> Self: raise ValueError("minimum_depth must be less than maximum_depth") return self + class TimeRange(BaseModel): """Defines the temporal boundaries for a space-time region.""" @@ -51,6 +52,7 @@ def _check_time_range(self) -> Self: raise ValueError("start_time must be before end_time") return self + class SpaceTimeRegion(BaseModel): """An space-time region with spatial and temporal boundaries.""" diff --git a/src/virtualship/expedition/waypoint.py b/src/virtualship/expedition/waypoint.py index 258cb5fc..307f5f63 100644 --- a/src/virtualship/expedition/waypoint.py +++ b/src/virtualship/expedition/waypoint.py @@ -1,9 +1,9 @@ """Waypoint class.""" -from pydantic import BaseModel, field_serializer -from dataclasses import dataclass from datetime import datetime +from pydantic import BaseModel, field_serializer + from ..location import Location from .instrument_type import InstrumentType @@ -25,4 +25,4 @@ def serialize_instrument(self, instrument): @field_serializer("time") def serialize_time(self, time): """Ensure datetime is formatted properly in YAML.""" - return time.strftime("%Y-%m-%d %H:%M:%S") if time else None \ No newline at end of file + return time.strftime("%Y-%m-%d %H:%M:%S") if time else None diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index f3f2b279..530c7690 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -2,7 +2,6 @@ from importlib.resources import files from typing import TextIO -import numpy as np import pandas as pd import yaml from pydantic import BaseModel @@ -56,13 +55,16 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): 4. returns the yaml information. """ - # Importing Schedule and related models from expedition module - from virtualship.expedition.schedule import Schedule - from virtualship.expedition.space_time_region import SpaceTimeRegion, SpatialRange, TimeRange - from virtualship.expedition.waypoint import Waypoint, Location from virtualship.expedition.instrument_type import InstrumentType - + from virtualship.expedition.schedule import Schedule + from virtualship.expedition.space_time_region import ( + SpaceTimeRegion, + SpatialRange, + TimeRange, + ) + from virtualship.expedition.waypoint import Location, Waypoint + # Read data from Excel coordinates_data = pd.read_excel( excel_file_path, @@ -80,9 +82,11 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # Extract unique instruments from dataset using a set unique_instruments = set() - + for instrument_list in coordinates_data["Instrument"]: - instruments = instrument_list.split(", ") # Split by ", " to get individual instruments + instruments = instrument_list.split( + ", " + ) # Split by ", " to get individual instruments unique_instruments |= set(instruments) # Union update with set of instruments # Determine the maximum depth based on the unique instruments @@ -104,7 +108,6 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): min_latitude = coordinates_data["Latitude"].min() - buffer max_latitude = coordinates_data["Latitude"].max() + buffer - spatial_range = SpatialRange( minimum_longitude=coordinates_data["Longitude"].min() - buffer, maximum_longitude=coordinates_data["Longitude"].max() + buffer, @@ -114,7 +117,6 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): maximum_depth=maximum_depth, ) - # Create space-time region object space_time_region = SpaceTimeRegion( spatial_range=spatial_range, @@ -124,7 +126,9 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # Generate waypoints waypoints = [] for _, row in coordinates_data.iterrows(): - instruments = [InstrumentType(instrument) for instrument in row["Instrument"].split(", ")] + instruments = [ + InstrumentType(instrument) for instrument in row["Instrument"].split(", ") + ] waypoints.append( Waypoint( instrument=instruments, @@ -140,6 +144,3 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # Save to YAML file schedule.to_yaml(yaml_output_path) - - - From c51043dd8838943331fc6c5ceb73846671bdabce Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 21:16:39 -0500 Subject: [PATCH 17/23] remove restriction for version --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index b83dace7..42319ad6 100644 --- a/environment.yml +++ b/environment.yml @@ -12,7 +12,7 @@ dependencies: - pip - pyyaml - copernicusmarine >= 2 - - openpyxl >= 3.1.5 + - openpyxl # linting - pre-commit From f3daaa7b63515bb3e580edea538560b7442d6215 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 21:17:19 -0500 Subject: [PATCH 18/23] add checking for columns from excel file --- src/virtualship/utils.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 530c7690..08dc9666 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -65,11 +65,35 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): ) from virtualship.expedition.waypoint import Location, Waypoint + # Expected column headers + expected_columns = {"Station Type", "Name", "Latitude", "Longitude", "Instrument"} + # Read data from Excel - coordinates_data = pd.read_excel( - excel_file_path, - usecols=["Station Type", "Name", "Latitude", "Longitude", "Instrument"], - ) + coordinates_data = pd.read_excel(excel_file_path) + + # Check if the headers match the expected ones + actual_columns = set(coordinates_data.columns) + + missing_columns = expected_columns - actual_columns + if missing_columns: + raise ValueError( + f"Error: Found columns {list(actual_columns)}, but expected columns {list(expected_columns)}. " + "Are you sure that you're using the correct export from MFP?" + ) + + extra_columns = actual_columns - expected_columns + if extra_columns: + print( + f"Warning: Found additional unexpected columns {list(extra_columns)}. " + "Manually added columns have no effect. " + "If the MFP export format changed, please submit an issue: " + "https://github.com/OceanParcels/virtualship/issues." + ) + + # Drop unexpected columns (optional, only if you want to ensure strict conformity) + coordinates_data = coordinates_data[list(expected_columns)] + + # Continue with the rest of the function after validation... coordinates_data = coordinates_data.dropna() # Define maximum depth (in meters) and buffer (in degrees) for each instrument From 4c59420186dc2a991a78ad42f6840a56cab7c1f7 Mon Sep 17 00:00:00 2001 From: iury simoes-sousa Date: Thu, 13 Feb 2025 21:17:40 -0500 Subject: [PATCH 19/23] add unit tests --- tests/test_mfp_to_yaml.py | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/test_mfp_to_yaml.py diff --git a/tests/test_mfp_to_yaml.py b/tests/test_mfp_to_yaml.py new file mode 100644 index 00000000..3c5027f4 --- /dev/null +++ b/tests/test_mfp_to_yaml.py @@ -0,0 +1,97 @@ +import pytest +import pandas as pd +import yaml +from unittest.mock import patch +from virtualship.utils import mfp_to_yaml +from virtualship.expedition.instrument_type import InstrumentType +from virtualship.expedition.schedule import Schedule +from pathlib import Path + + +# Sample correct MFP data +VALID_MFP_DATA = pd.DataFrame({ + "Station Type": ["A", "B", "C"], + "Name": ["Station1", "Station2", "Station3"], + "Latitude": [30, 31, 32], + "Longitude": [-44, -45, -46], + "Instrument": ["CTD, DRIFTER", "ARGO_FLOAT", "XBT, CTD, DRIFTER"] +}) + +# Missing required columns +MISSING_HEADERS_DATA = pd.DataFrame({ + "Station Type": ["A"], + "Name": ["Station1"], + "Latitude": [10.5] +}) + +# Extra unexpected columns +EXTRA_HEADERS_DATA = VALID_MFP_DATA.copy() +EXTRA_HEADERS_DATA["Unexpected Column"] = ["Extra1", "Extra2", "Extra3"] + + +@patch("pandas.read_excel", return_value=VALID_MFP_DATA) +def test_mfp_to_yaml_success(mock_read_excel, tmp_path): + """Test that mfp_to_yaml correctly processes a valid MFP Excel file.""" + + yaml_output_path = tmp_path / "schedule.yaml" + + # Run function (No need to mock open() for YAML, real file is created) + mfp_to_yaml("mock_file.xlsx", yaml_output_path) + + # Ensure the YAML file was written + assert yaml_output_path.exists() + + # Load YAML and validate contents + data = Schedule.from_yaml(yaml_output_path) + + assert len(data.waypoints) == 3 + assert data.waypoints[0].instrument == [InstrumentType.CTD, InstrumentType.DRIFTER] + assert data.waypoints[1].instrument == [InstrumentType.ARGO_FLOAT] + assert data.waypoints[2].instrument == [InstrumentType.XBT, InstrumentType.CTD, InstrumentType.DRIFTER] + + +@patch("pandas.read_excel", return_value=MISSING_HEADERS_DATA) +def test_mfp_to_yaml_missing_headers(mock_read_excel, tmp_path): + """Test that mfp_to_yaml raises an error when required columns are missing.""" + + yaml_output_path = tmp_path / "schedule.yaml" + + with pytest.raises(ValueError, match="Error: Found columns .* but expected columns .*"): + mfp_to_yaml("mock_file.xlsx", yaml_output_path) + + +@patch("pandas.read_excel", return_value=EXTRA_HEADERS_DATA) +@patch("builtins.print") # Capture printed warnings +def test_mfp_to_yaml_extra_headers(mock_print, mock_read_excel, tmp_path): + """Test that mfp_to_yaml prints a warning when extra columns are found.""" + + yaml_output_path = tmp_path / "schedule.yaml" + + # Run function + mfp_to_yaml("mock_file.xlsx", yaml_output_path) + + # Ensure a warning message was printed + mock_print.assert_any_call( + "Warning: Found additional unexpected columns ['Unexpected Column']. " + "Manually added columns have no effect. " + "If the MFP export format changed, please submit an issue: " + "https://github.com/OceanParcels/virtualship/issues." + ) + + +@patch("pandas.read_excel", return_value=VALID_MFP_DATA) +def test_mfp_to_yaml_instrument_conversion(mock_read_excel, tmp_path): + """Test that instruments are correctly converted into InstrumentType enums.""" + + yaml_output_path = tmp_path / "schedule.yaml" + + # Run function + mfp_to_yaml("mock_file.xlsx", yaml_output_path) + + # Load the generated YAML + data = Schedule.from_yaml(yaml_output_path) + + assert isinstance(data.waypoints[0].instrument, list) + assert data.waypoints[0].instrument == [InstrumentType.CTD, InstrumentType.DRIFTER] + assert data.waypoints[1].instrument == [InstrumentType.ARGO_FLOAT] + assert data.waypoints[2].instrument == [InstrumentType.XBT, InstrumentType.CTD, InstrumentType.DRIFTER] From b67b15d97383073b2824fb2e9320afeb1cde9f72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 02:17:54 +0000 Subject: [PATCH 20/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/virtualship/utils.py | 2 +- tests/test_mfp_to_yaml.py | 65 +++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 08dc9666..558a30ff 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -80,7 +80,7 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): f"Error: Found columns {list(actual_columns)}, but expected columns {list(expected_columns)}. " "Are you sure that you're using the correct export from MFP?" ) - + extra_columns = actual_columns - expected_columns if extra_columns: print( diff --git a/tests/test_mfp_to_yaml.py b/tests/test_mfp_to_yaml.py index 3c5027f4..e6446947 100644 --- a/tests/test_mfp_to_yaml.py +++ b/tests/test_mfp_to_yaml.py @@ -1,28 +1,27 @@ -import pytest -import pandas as pd -import yaml from unittest.mock import patch -from virtualship.utils import mfp_to_yaml + +import pandas as pd +import pytest + from virtualship.expedition.instrument_type import InstrumentType from virtualship.expedition.schedule import Schedule -from pathlib import Path - +from virtualship.utils import mfp_to_yaml # Sample correct MFP data -VALID_MFP_DATA = pd.DataFrame({ - "Station Type": ["A", "B", "C"], - "Name": ["Station1", "Station2", "Station3"], - "Latitude": [30, 31, 32], - "Longitude": [-44, -45, -46], - "Instrument": ["CTD, DRIFTER", "ARGO_FLOAT", "XBT, CTD, DRIFTER"] -}) +VALID_MFP_DATA = pd.DataFrame( + { + "Station Type": ["A", "B", "C"], + "Name": ["Station1", "Station2", "Station3"], + "Latitude": [30, 31, 32], + "Longitude": [-44, -45, -46], + "Instrument": ["CTD, DRIFTER", "ARGO_FLOAT", "XBT, CTD, DRIFTER"], + } +) # Missing required columns -MISSING_HEADERS_DATA = pd.DataFrame({ - "Station Type": ["A"], - "Name": ["Station1"], - "Latitude": [10.5] -}) +MISSING_HEADERS_DATA = pd.DataFrame( + {"Station Type": ["A"], "Name": ["Station1"], "Latitude": [10.5]} +) # Extra unexpected columns EXTRA_HEADERS_DATA = VALID_MFP_DATA.copy() @@ -32,12 +31,11 @@ @patch("pandas.read_excel", return_value=VALID_MFP_DATA) def test_mfp_to_yaml_success(mock_read_excel, tmp_path): """Test that mfp_to_yaml correctly processes a valid MFP Excel file.""" - yaml_output_path = tmp_path / "schedule.yaml" - + # Run function (No need to mock open() for YAML, real file is created) mfp_to_yaml("mock_file.xlsx", yaml_output_path) - + # Ensure the YAML file was written assert yaml_output_path.exists() @@ -47,16 +45,21 @@ def test_mfp_to_yaml_success(mock_read_excel, tmp_path): assert len(data.waypoints) == 3 assert data.waypoints[0].instrument == [InstrumentType.CTD, InstrumentType.DRIFTER] assert data.waypoints[1].instrument == [InstrumentType.ARGO_FLOAT] - assert data.waypoints[2].instrument == [InstrumentType.XBT, InstrumentType.CTD, InstrumentType.DRIFTER] + assert data.waypoints[2].instrument == [ + InstrumentType.XBT, + InstrumentType.CTD, + InstrumentType.DRIFTER, + ] @patch("pandas.read_excel", return_value=MISSING_HEADERS_DATA) def test_mfp_to_yaml_missing_headers(mock_read_excel, tmp_path): """Test that mfp_to_yaml raises an error when required columns are missing.""" - yaml_output_path = tmp_path / "schedule.yaml" - - with pytest.raises(ValueError, match="Error: Found columns .* but expected columns .*"): + + with pytest.raises( + ValueError, match="Error: Found columns .* but expected columns .*" + ): mfp_to_yaml("mock_file.xlsx", yaml_output_path) @@ -64,9 +67,8 @@ def test_mfp_to_yaml_missing_headers(mock_read_excel, tmp_path): @patch("builtins.print") # Capture printed warnings def test_mfp_to_yaml_extra_headers(mock_print, mock_read_excel, tmp_path): """Test that mfp_to_yaml prints a warning when extra columns are found.""" - yaml_output_path = tmp_path / "schedule.yaml" - + # Run function mfp_to_yaml("mock_file.xlsx", yaml_output_path) @@ -82,9 +84,8 @@ def test_mfp_to_yaml_extra_headers(mock_print, mock_read_excel, tmp_path): @patch("pandas.read_excel", return_value=VALID_MFP_DATA) def test_mfp_to_yaml_instrument_conversion(mock_read_excel, tmp_path): """Test that instruments are correctly converted into InstrumentType enums.""" - yaml_output_path = tmp_path / "schedule.yaml" - + # Run function mfp_to_yaml("mock_file.xlsx", yaml_output_path) @@ -94,4 +95,8 @@ def test_mfp_to_yaml_instrument_conversion(mock_read_excel, tmp_path): assert isinstance(data.waypoints[0].instrument, list) assert data.waypoints[0].instrument == [InstrumentType.CTD, InstrumentType.DRIFTER] assert data.waypoints[1].instrument == [InstrumentType.ARGO_FLOAT] - assert data.waypoints[2].instrument == [InstrumentType.XBT, InstrumentType.CTD, InstrumentType.DRIFTER] + assert data.waypoints[2].instrument == [ + InstrumentType.XBT, + InstrumentType.CTD, + InstrumentType.DRIFTER, + ] From 6f63cd45a9af108bfc0f9a5fbd861ed63993533a Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:32:08 +0100 Subject: [PATCH 21/23] Add update comments and var naming --- src/virtualship/cli/commands.py | 10 +++++----- .../expedition/space_time_region.py | 5 ++++- src/virtualship/utils.py | 18 ++++-------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/virtualship/cli/commands.py b/src/virtualship/cli/commands.py index fb49e5c5..34ac1c21 100644 --- a/src/virtualship/cli/commands.py +++ b/src/virtualship/cli/commands.py @@ -25,12 +25,12 @@ type=click.Path(exists=False, file_okay=False, dir_okay=True), ) @click.option( - "--mfp-file", + "--from-mfp", type=str, default=None, help='Partially initialise a project from an exported xlsx or csv file from NIOZ\' Marine Facilities Planning tool (specifically the "Export Coordinates > DD" option). User edits are required after initialisation.', ) -def init(path, mfp_file): +def init(path, from_mfp): """ Initialize a directory for a new expedition, with an example schedule and ship config files. @@ -53,11 +53,11 @@ def init(path, mfp_file): ) config.write_text(utils.get_example_config()) - - if mfp_file: + if from_mfp: + mfp_file = Path(from_mfp) # Generate schedule.yaml from the MPF file click.echo(f"Generating schedule from {mfp_file}...") - schedule_body = mfp_to_yaml(mfp_file, schedule) + mfp_to_yaml(mfp_file, schedule) click.echo( "\n⚠️ The generated schedule does not contain time values. " "\nPlease edit 'schedule.yaml' and manually add the necessary time values." diff --git a/src/virtualship/expedition/space_time_region.py b/src/virtualship/expedition/space_time_region.py index a63734c7..22008805 100644 --- a/src/virtualship/expedition/space_time_region.py +++ b/src/virtualship/expedition/space_time_region.py @@ -42,12 +42,15 @@ def _check_lon_lat_domain(self) -> Self: class TimeRange(BaseModel): """Defines the temporal boundaries for a space-time region.""" + #! TODO: Remove the `| None` for `start_time` and `end_time`, and have the MFP functionality not use pydantic (with testing to avoid codebase drift) start_time: datetime | None = None end_time: datetime | None = None @model_validator(mode="after") def _check_time_range(self) -> Self: - if self.start_time and self.end_time: + if ( + self.start_time and self.end_time + ): #! TODO: remove this check once `start_time` and `end_time` are required if not self.start_time < self.end_time: raise ValueError("start_time must be before end_time") return self diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index 558a30ff..c76980ea 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -40,7 +40,7 @@ def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel: return model.model_validate(yaml.safe_load(data)) -def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): +def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # noqa: D417 """ Generates a YAML file with spatial and temporal information based on instrument data from MFP excel file. @@ -96,7 +96,7 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # Continue with the rest of the function after validation... coordinates_data = coordinates_data.dropna() - # Define maximum depth (in meters) and buffer (in degrees) for each instrument + # maximum depth (in meters), buffer (in degrees) for each instrument instrument_properties = { "XBT": {"maximum_depth": 2000, "buffer": 1}, "CTD": {"maximum_depth": 5000, "buffer": 1}, @@ -104,21 +104,17 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): "ARGO_FLOAT": {"maximum_depth": 2000, "buffer": 5}, } - # Extract unique instruments from dataset using a set unique_instruments = set() for instrument_list in coordinates_data["Instrument"]: - instruments = instrument_list.split( - ", " - ) # Split by ", " to get individual instruments - unique_instruments |= set(instruments) # Union update with set of instruments + instruments = instrument_list.split(", ") + unique_instruments |= set(instruments) # Determine the maximum depth based on the unique instruments maximum_depth = max( instrument_properties.get(inst, {"maximum_depth": 0})["maximum_depth"] for inst in unique_instruments ) - minimum_depth = 0 # Determine the buffer based on the maximum buffer of the instruments present buffer = max( @@ -126,12 +122,6 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): for inst in unique_instruments ) - # Adjusted spatial range - min_longitude = coordinates_data["Longitude"].min() - buffer - max_longitude = coordinates_data["Longitude"].max() + buffer - min_latitude = coordinates_data["Latitude"].min() - buffer - max_latitude = coordinates_data["Latitude"].max() + buffer - spatial_range = SpatialRange( minimum_longitude=coordinates_data["Longitude"].min() - buffer, maximum_longitude=coordinates_data["Longitude"].max() + buffer, From 222df85e3c4d3d17fda71b18a93812c4e9842b0c Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:08:36 +0100 Subject: [PATCH 22/23] Remove buffering from mfp conversion --- src/virtualship/utils.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/virtualship/utils.py b/src/virtualship/utils.py index c76980ea..43608b60 100644 --- a/src/virtualship/utils.py +++ b/src/virtualship/utils.py @@ -97,11 +97,11 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # noqa: D417 coordinates_data = coordinates_data.dropna() # maximum depth (in meters), buffer (in degrees) for each instrument - instrument_properties = { - "XBT": {"maximum_depth": 2000, "buffer": 1}, - "CTD": {"maximum_depth": 5000, "buffer": 1}, - "DRIFTER": {"maximum_depth": 1, "buffer": 5}, - "ARGO_FLOAT": {"maximum_depth": 2000, "buffer": 5}, + instrument_max_depths = { + "XBT": 2000, + "CTD": 5000, + "DRIFTER": 1, + "ARGO_FLOAT": 2000, } unique_instruments = set() @@ -112,21 +112,14 @@ def mfp_to_yaml(excel_file_path: str, yaml_output_path: str): # noqa: D417 # Determine the maximum depth based on the unique instruments maximum_depth = max( - instrument_properties.get(inst, {"maximum_depth": 0})["maximum_depth"] - for inst in unique_instruments - ) - - # Determine the buffer based on the maximum buffer of the instruments present - buffer = max( - instrument_properties.get(inst, {"buffer": 0})["buffer"] - for inst in unique_instruments + instrument_max_depths.get(instrument, 0) for instrument in unique_instruments ) spatial_range = SpatialRange( - minimum_longitude=coordinates_data["Longitude"].min() - buffer, - maximum_longitude=coordinates_data["Longitude"].max() + buffer, - minimum_latitude=coordinates_data["Latitude"].min() - buffer, - maximum_latitude=coordinates_data["Latitude"].max() + buffer, + minimum_longitude=coordinates_data["Longitude"].min(), + maximum_longitude=coordinates_data["Longitude"].max(), + minimum_latitude=coordinates_data["Latitude"].min(), + maximum_latitude=coordinates_data["Latitude"].max(), minimum_depth=0, maximum_depth=maximum_depth, ) From c94567bb3c41ab20b665760cd1c51a23f5fff21e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:36:42 +0100 Subject: [PATCH 23/23] update references to Waypoint --- src/virtualship/expedition/waypoint.py | 5 ----- tests/expedition/test_schedule.py | 6 ++++-- tests/expedition/test_simulate_schedule.py | 8 ++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/virtualship/expedition/waypoint.py b/src/virtualship/expedition/waypoint.py index 307f5f63..018ccecb 100644 --- a/src/virtualship/expedition/waypoint.py +++ b/src/virtualship/expedition/waypoint.py @@ -21,8 +21,3 @@ def serialize_instrument(self, instrument): if isinstance(instrument, list): return [inst.value for inst in instrument] return instrument.value if instrument else None - - @field_serializer("time") - def serialize_time(self, time): - """Ensure datetime is formatted properly in YAML.""" - return time.strftime("%Y-%m-%d %H:%M:%S") if time else None diff --git a/tests/expedition/test_schedule.py b/tests/expedition/test_schedule.py index fd1ed959..33ffc74d 100644 --- a/tests/expedition/test_schedule.py +++ b/tests/expedition/test_schedule.py @@ -12,9 +12,11 @@ def test_schedule(tmpdir) -> None: schedule = Schedule( waypoints=[ - Waypoint(Location(0, 0), time=base_time, instrument=None), + Waypoint(location=Location(0, 0), time=base_time, instrument=None), Waypoint( - Location(1, 1), time=base_time + timedelta(hours=1), instrument=None + location=Location(1, 1), + time=base_time + timedelta(hours=1), + instrument=None, ), ] ) diff --git a/tests/expedition/test_simulate_schedule.py b/tests/expedition/test_simulate_schedule.py index 8f92c678..01544c42 100644 --- a/tests/expedition/test_simulate_schedule.py +++ b/tests/expedition/test_simulate_schedule.py @@ -20,8 +20,8 @@ def test_simulate_schedule_feasible() -> None: ship_config.ship_speed_meter_per_second = 5.14 schedule = Schedule( waypoints=[ - Waypoint(Location(0, 0), base_time), - Waypoint(Location(0.01, 0), base_time + timedelta(days=1)), + Waypoint(location=Location(0, 0), time=base_time), + Waypoint(location=Location(0.01, 0), time=base_time + timedelta(days=1)), ] ) @@ -38,8 +38,8 @@ def test_simulate_schedule_too_far() -> None: ship_config = ShipConfig.from_yaml("expedition_dir/ship_config.yaml") schedule = Schedule( waypoints=[ - Waypoint(Location(0, 0), base_time), - Waypoint(Location(1.0, 0), base_time + timedelta(minutes=1)), + Waypoint(location=Location(0, 0), time=base_time), + Waypoint(location=Location(1.0, 0), time=base_time + timedelta(minutes=1)), ] )