Source code for pelagos_py.steps.input_output.gen_data

# This file is part of pelagos_py.
#
# Copyright 2025-2026 National Oceanography Centre and The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Step for generating synthetic data for testing pipelines"""

import polars as pl
import xarray as xr
import numpy as np
from pelagos_py.steps.base_step import BaseStep, register_step
from datetime import date, timedelta


@register_step
[docs] class GenerateData(BaseStep): """ Generates random synthetic data (Useful for devs/testing). Parameters ---------- sampling_info: list [start_date, end_date, sample_rate] Start and end dates should be in str("yyyy-mm-dd") format and sample rate is in seconds. additional_variables: list Adds additional variables to be generated. "LATITUDE", "LONGITUDE", "PRES", "TEMP" and "CNDC" are generated by default. value_limits: dict Specifies the [min, max] range to generate random values over. Variables without a specified range will default to [0, 1] Examples -------- Example usage in a pipeline configuration: .. code-block:: yaml steps: - name: Generate Data parameters: sampling_info: ["2025-01-01", "2025-01-02", 20] additional_variables: ["DENSITY", "ABS_SALINITY"] value_limits: "DENSITY": [1028, 1032] """ step_name = "Generate Data" required_variables = [] provided_variables = ["TIME", "LATITUDE", "LONGITUDE", "PRES", "TEMP", "CNDC"] parameter_schema = { "sampling_info": { "type": list, "default": ["2025-01-01", "2025-01-02", 20], "description": "[start_date (y-m-d), end_date (y-m-d), sample_period (s)].", }, "additional_variables": { "type": list, "default": [], "description": "Extra variables to generate on top of TIME, GPS and raw CTD.", }, "value_limits": { "type": dict, "default": {}, "description": "Per-variable random sampling range, e.g. {'DENSITY': [1028, 1032]}.", }, "gen_fixed_data": { "type": bool, "default": False, "description": "Generate deterministic fixed data instead of random data (testing).", }, } def run(self): # Check if the data is already in the context if "data" in self.context: raise ValueError( "[Generate Data] WARNING: Data found in context. This will be replaced by generated data." ) if self.gen_fixed_data: self.log("Generating fixed data") import itertools ncols = 2 column_names = ["A", "B", "C"][:ncols] qc_values = np.array(list(itertools.product(range(10), repeat=ncols))) values = [[i] * int(10**ncols) for i in range(1, ncols + 1)] df = pl.DataFrame( { **{col: values[i] for i, col in enumerate(column_names)}, **{ f"{col}_QC": qc_values[:, i] for i, col in enumerate(column_names) }, } ) else: self.log("Generating random data") # Load config parameters start_date, end_date, sample_period = self.sampling_info additional_variables = self.additional_variables user_value_limits = self.value_limits diagnostics = self.diagnostics # Add aditional variables variable_names = {"LATITUDE", "LONGITUDE", "PRES", "TEMP", "CNDC"} variable_names.update(additional_variables) # Define variable limits and update with user values variable_limits = { "LATITUDE": [-90, 90], # Degrees "LONGITUDE": [-180, 180], # Degrees "PRES": [0, 100], # Bar "TEMP": [0, 20], # Celcius "CNDC": [34, 35], # S/m } variable_limits.update(user_value_limits) if diagnostics: self.log(f"[Generate Data] Variables: {variable_limits}") # Make time index for dataframe (df) df = pl.select( pl.datetime_range( date(*map(int, start_date.split("-"))), date(*map(int, end_date.split("-"))), timedelta(seconds=sample_period), time_unit="ns", ).alias("TIME") ) data_length = len(df) # Generate random data for the remaining variables for variable_name in variable_names: # Check the limits if variable_name in variable_limits.keys(): lower, upper = variable_limits[variable_name] if upper <= lower: raise ValueError( f"Upper limit must be greater than lower limit for {variable_name}" ) else: self.log( f"The additional variable {variable_name} has not been set limits. Defaulting to [0, 1]." ) lower, upper = [0, 1] # Add the new column df = df.with_columns( pl.lit(np.random.uniform(lower, upper, data_length)).alias( variable_name ) ) # Make the xarray data from the polars dataframe and ship it # TODO: Add metadata flexibility data_vars = {col: ("N_MEASUREMENTS", df[col].to_numpy()) for col in df.columns} data = xr.Dataset( data_vars, coords={"N_MEASUREMENTS": np.arange(len(df))}, ) data["N_PARAM"] = list(df.columns) self.context["data"] = data return self.context