{
    "process_graph": {
        "vectorbuffer1": {
            "process_id": "vector_buffer",
            "arguments": {
                "distance": -10,
                "geometry": {
                    "from_parameter": "geometry"
                },
                "unit": "meter"
            }
        },
        "dateshift1": {
            "process_id": "date_shift",
            "arguments": {
                "date": {
                    "from_parameter": "startdate"
                },
                "unit": "day",
                "value": -80
            }
        },
        "dateshift2": {
            "process_id": "date_shift",
            "arguments": {
                "date": {
                    "from_parameter": "enddate"
                },
                "unit": "day",
                "value": 80
            }
        },
        "loadcollection1": {
            "process_id": "load_collection",
            "arguments": {
                "bands": [
                    "VH",
                    "VV"
                ],
                "id": "SENTINEL1_GRD",
                "properties": {
                    "polarization": {
                        "process_graph": {
                            "eq1": {
                                "process_id": "eq",
                                "arguments": {
                                    "x": {
                                        "from_parameter": "value"
                                    },
                                    "y": "VV&VH"
                                },
                                "result": true
                            }
                        }
                    }
                },
                "spatial_extent": {
                    "from_node": "vectorbuffer1"
                },
                "temporal_extent": [
                    {
                        "from_node": "dateshift1"
                    },
                    {
                        "from_node": "dateshift2"
                    }
                ]
            }
        },
        "sarbackscatter1": {
            "process_id": "sar_backscatter",
            "arguments": {
                "coefficient": "sigma0-ellipsoid",
                "contributing_area": false,
                "data": {
                    "from_node": "loadcollection1"
                },
                "elevation_model": "COPERNICUS_30",
                "ellipsoid_incidence_angle": false,
                "local_incidence_angle": false,
                "mask": false,
                "noise_removal": true
            }
        },
        "loadcollection2": {
            "process_id": "load_collection",
            "arguments": {
                "bands": [
                    "B04",
                    "B08"
                ],
                "id": "SENTINEL2_L2A",
                "properties": {
                    "eo:cloud_cover": {
                        "process_graph": {
                            "lte1": {
                                "process_id": "lte",
                                "arguments": {
                                    "x": {
                                        "from_parameter": "value"
                                    },
                                    "y": 95
                                },
                                "result": true
                            }
                        }
                    }
                },
                "spatial_extent": {
                    "from_node": "vectorbuffer1"
                },
                "temporal_extent": [
                    {
                        "from_node": "dateshift1"
                    },
                    {
                        "from_node": "dateshift2"
                    }
                ]
            }
        },
        "loadcollection3": {
            "process_id": "load_collection",
            "arguments": {
                "bands": [
                    "SCL"
                ],
                "id": "SENTINEL2_L2A",
                "properties": {
                    "eo:cloud_cover": {
                        "process_graph": {
                            "lte2": {
                                "process_id": "lte",
                                "arguments": {
                                    "x": {
                                        "from_parameter": "value"
                                    },
                                    "y": 95
                                },
                                "result": true
                            }
                        }
                    }
                },
                "spatial_extent": {
                    "from_node": "vectorbuffer1"
                },
                "temporal_extent": [
                    {
                        "from_node": "dateshift1"
                    },
                    {
                        "from_node": "dateshift2"
                    }
                ]
            }
        },
        "toscldilationmask1": {
            "process_id": "to_scl_dilation_mask",
            "arguments": {
                "data": {
                    "from_node": "loadcollection3"
                },
                "erosion_kernel_size": 3,
                "kernel1_size": 17,
                "kernel2_size": 77,
                "mask1_values": [
                    2,
                    4,
                    5,
                    6,
                    7
                ],
                "mask2_values": [
                    3,
                    8,
                    9,
                    10,
                    11
                ]
            }
        },
        "mask1": {
            "process_id": "mask",
            "arguments": {
                "data": {
                    "from_node": "loadcollection2"
                },
                "mask": {
                    "from_node": "toscldilationmask1"
                }
            }
        },
        "ndvi1": {
            "process_id": "ndvi",
            "arguments": {
                "data": {
                    "from_node": "mask1"
                },
                "nir": "B08",
                "red": "B04",
                "target_band": "NDVI"
            }
        },
        "filterbands1": {
            "process_id": "filter_bands",
            "arguments": {
                "bands": [
                    "NDVI"
                ],
                "data": {
                    "from_node": "ndvi1"
                }
            }
        },
        "biopar1": {
            "process_id": "biopar",
            "arguments": {
                "biopar_type": "FCOVER",
                "spatial_extent": {
                    "from_node": "vectorbuffer1"
                },
                "temporal_extent": [
                    {
                        "from_node": "dateshift1"
                    },
                    {
                        "from_node": "dateshift2"
                    }
                ]
            },
            "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/vito/biopar/openeo_udp/biopar.json"
        },
        "filterbands2": {
            "process_id": "filter_bands",
            "arguments": {
                "bands": [
                    "FCOVER"
                ],
                "data": {
                    "from_node": "biopar1"
                }
            }
        },
        "biopar2": {
            "process_id": "biopar",
            "arguments": {
                "biopar_type": "FAPAR",
                "spatial_extent": {
                    "from_node": "vectorbuffer1"
                },
                "temporal_extent": [
                    {
                        "from_node": "dateshift1"
                    },
                    {
                        "from_node": "dateshift2"
                    }
                ]
            },
            "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/vito/biopar/openeo_udp/biopar.json"
        },
        "filterbands3": {
            "process_id": "filter_bands",
            "arguments": {
                "bands": [
                    "FAPAR"
                ],
                "data": {
                    "from_node": "biopar2"
                }
            }
        },
        "eq2": {
            "process_id": "eq",
            "arguments": {
                "case_sensitive": false,
                "x": {
                    "from_parameter": "output"
                },
                "y": "fapar"
            }
        },
        "if1": {
            "process_id": "if",
            "arguments": {
                "accept": {
                    "from_node": "filterbands3"
                },
                "reject": null,
                "value": {
                    "from_node": "eq2"
                }
            }
        },
        "eq3": {
            "process_id": "eq",
            "arguments": {
                "case_sensitive": false,
                "x": {
                    "from_parameter": "output"
                },
                "y": "fcover"
            }
        },
        "if2": {
            "process_id": "if",
            "arguments": {
                "accept": {
                    "from_node": "filterbands2"
                },
                "reject": {
                    "from_node": "if1"
                },
                "value": {
                    "from_node": "eq3"
                }
            }
        },
        "mergecubes1": {
            "process_id": "merge_cubes",
            "arguments": {
                "cube1": {
                    "from_node": "if2"
                },
                "cube2": {
                    "from_node": "filterbands1"
                }
            }
        },
        "eq4": {
            "process_id": "eq",
            "arguments": {
                "case_sensitive": false,
                "x": {
                    "from_parameter": "output"
                },
                "y": "ndvi"
            }
        },
        "if3": {
            "process_id": "if",
            "arguments": {
                "accept": {
                    "from_node": "filterbands1"
                },
                "reject": {
                    "from_node": "mergecubes1"
                },
                "value": {
                    "from_node": "eq4"
                }
            }
        },
        "resamplecubespatial1": {
            "process_id": "resample_cube_spatial",
            "arguments": {
                "data": {
                    "from_node": "sarbackscatter1"
                },
                "target": {
                    "from_node": "if3"
                }
            }
        },
        "mergecubes2": {
            "process_id": "merge_cubes",
            "arguments": {
                "cube1": {
                    "from_node": "resamplecubespatial1"
                },
                "cube2": {
                    "from_node": "if3"
                }
            }
        },
        "aggregatespatial1": {
            "process_id": "aggregate_spatial",
            "arguments": {
                "data": {
                    "from_node": "mergecubes2"
                },
                "geometries": {
                    "from_node": "vectorbuffer1"
                },
                "reducer": {
                    "process_graph": {
                        "mean1": {
                            "process_id": "mean",
                            "arguments": {
                                "data": {
                                    "from_parameter": "data"
                                }
                            },
                            "result": true
                        }
                    }
                }
            }
        },
        "runudf1": {
            "process_id": "run_udf",
            "arguments": {
                "context": {
                    "start": {
                        "from_parameter": "startdate"
                    },
                    "end": {
                        "from_parameter": "enddate"
                    },
                    "minimal_input_checks": {
                        "from_parameter": "minimal_input_checks"
                    },
                    "output": {
                        "from_parameter": "output"
                    }
                },
                "data": {
                    "from_node": "aggregatespatial1"
                },
                "runtime": "Python",
                "udf": "import logging\nfrom openeo.udf.udf_data import UdfData\nfrom openeo.udf.structured_data import StructuredData\nfrom openeo.rest.conversions import timeseries_json_to_pandas\nimport pandas as pd\nimport sys\nfrom datetime import datetime, timedelta\n\nsys.path.append(\"cropsar_onnx\")\nlogger = logging.getLogger(__name__)\nfrom cropsar.preprocessing.retrieve_timeseries_openeo import run_cropsar_dataframes\n\n# calculate the cropsar curve for each field and the regional average of all the input fields\n\ndef get_cropar_timeseries(\n    timeseries_dataframe: pd.DataFrame, \n    unique_ids_fields: set, \n    metrics_order: list, \n    time_range: list, \n    minimal_input_checks=False,\n    s2_data_layer: str = 'S2_FAPAR') -> pd.DataFrame:\n    \"\"\"\"\n    Calculate the CropSAR timeseries for the given input timeseries dataframe.\n    :param timeseries_dataframe: Input timeseries dataframe\n    :param unique_ids_fields: Unique identifiers for the fields in the dataframe\n    :param metrics_order: Order of the metrics in the dataframe columns\n    :param time_range: Time range for the output timeseries\n    :param minimal_input_checks: Whether to perform minimal input checks\n    :param s2_data_layer: Sentinel-2 feature layer name used by CropSAR preprocessing\n    \"\"\"\n    # extract the index of the BIOPAR metric and the corresponding column\n    metrics_index_map = {m: i for i, m in enumerate(metrics_order)}\n    column_indices = timeseries_dataframe.columns.get_level_values(1)\n    indices = column_indices.isin([metrics_index_map['BIOPAR']])\n\n\n    df_S2 = timeseries_dataframe.loc[:, indices].sort_index().T\n    df_S2 = df_S2.rename(index=dict(enumerate(unique_ids_fields)))\n    if(df_S2.empty):\n        raise ValueError(\"Received an empty Sentinel-2 input dataframe while trying to compute cropSAR!\")\n\n    df_VHVV = timeseries_dataframe.loc[:, column_indices.isin([metrics_index_map['VH'], metrics_index_map['VV']])].sort_index().T\n    df_VHVV = df_VHVV.rename(index={0: 'vh', 1: 'vv'}, level=1)\n    df_VHVV = df_VHVV.rename(index=dict(enumerate(unique_ids_fields)))\n\n    cropsar_df, cropsar_df_q10, cropsar_df_q90 = run_cropsar_dataframes(\n        df_S2, df_VHVV, date_range=time_range, s2_data_layer=s2_data_layer, minimal_input_checks=minimal_input_checks\n    )\n\n    if (len(cropsar_df.index) == 0):\n        logger.warning(\"CropSAR returned an empty dataframe. For input, Sentinel-2 input: \")\n        logger.warning(str(df_S2.to_json(indent=2)))\n        logger.warning(\"Sentinel-1 VH-VV: \")\n        logger.warning(str(df_VHVV.to_json(indent=2)))\n\n    cropsar_df = cropsar_df.rename(\n        columns=dict(zip(list(cropsar_df.columns.values), [str(item) + '_cropSAR' for item in unique_ids_fields])))\n    cropsar_df_q10 = cropsar_df_q10.rename(\n        columns=dict(zip(list(cropsar_df_q10.columns.values), [str(item) + '_cropSAR_q10' for item in unique_ids_fields])))\n    cropsar_df_q90 = cropsar_df_q90.rename(\n        columns=dict(zip(list(cropsar_df_q90.columns.values), [str(item) + '_cropSAR_q90' for item in unique_ids_fields])))\n\n    cropsar_df = cropsar_df.round(decimals=3)\n    cropsar_df_q10 = cropsar_df_q10.round(decimals=3)\n    cropsar_df_q90 = cropsar_df_q90.round(decimals=3)\n    # FIX: Keep pandas Timestamp index (not datetime.date) for downstream pandas compatibility.\n    # FIX: Strip timezone info to avoid tz-aware/tz-naive comparison errors.\n    cropsar_df.index = pd.to_datetime(cropsar_df.index).tz_localize(None)\n    cropsar_df_q10.index = pd.to_datetime(cropsar_df_q10.index).tz_localize(None)\n    cropsar_df_q90.index = pd.to_datetime(cropsar_df_q90.index).tz_localize(None)\n    # FIX: Slice with Timestamp bounds to avoid Timestamp-vs-date comparison errors.\n    cropsar_df = cropsar_df.loc[pd.to_datetime(time_range[0], format = '%Y-%m-%d'):pd.to_datetime(time_range[1], format = '%Y-%m-%d')]\n    cropsar_df_q10 = cropsar_df_q10.loc[pd.to_datetime(time_range[0], format = '%Y-%m-%d'):pd.to_datetime(time_range[1], format = '%Y-%m-%d')]\n    cropsar_df_q90 = cropsar_df_q90.loc[pd.to_datetime(time_range[0], format = '%Y-%m-%d'):pd.to_datetime(time_range[1], format = '%Y-%m-%d')]\n    # merge into a single dataframe with three columns for the cropsar, cropsar_q10 and cropsar_q90\n    cropsar_df = pd.concat([cropsar_df, cropsar_df_q10, cropsar_df_q90], axis=1)\n    return cropsar_df\n\n\ndef udf_cropsar(udf_data: UdfData):\n    # extract the context values\n    user_context = udf_data.user_context\n    time_range = [user_context.get('start'), user_context.get('end')]\n    columns_order = ['VH', 'VV', 'BIOPAR']\n    minimal_input_checks = user_context.get('minimal_input_checks')\n    output = str(user_context.get('output', 'FAPAR')).upper()\n    output = output[3:] if output.startswith('S2_') else output\n    s2_data_layer = f\"S2_{output}\" if output in ['FAPAR', 'FCOVER', 'NDVI'] else 'S2_FAPAR'\n\n    # load the timeseries data calculated from BioPAR\n    timeseries_dict = udf_data.get_structured_data_list()[0].data\n    if not timeseries_dict:  # workaround of timeseries_dict is empty\n        logger.error(\"Received an empty timeseries_dict in the CropSAR UDF! Recheck input datacube\")\n        return\n    # json to pandas dataframe\n    TimeSeries_df = timeseries_json_to_pandas(timeseries_dict)\n    # FIX: Keep pandas Timestamp index (not datetime.date) for downstream pandas compatibility.\n    # FIX: Strip timezone info to avoid tz-aware/tz-naive comparison errors.\n    TimeSeries_df.index = pd.to_datetime(TimeSeries_df.index).tz_localize(None)\n\n    logger.info(f\"Input timeseries dataframe shape: {TimeSeries_df.shape} and columns: {TimeSeries_df.columns}\")\n\n    # change to multiindex if not already. This is needed when there is only one polygon in the input\n    if not isinstance(TimeSeries_df.columns, pd.MultiIndex):\n        TimeSeries_df.columns = pd.MultiIndex.from_product([[0], TimeSeries_df.columns])\n\n    # get the unique ids of the polygons\n    number_of_polygons = next(iter(timeseries_dict.values()))\n    unique_ids_polygons = ['Field_{}'.format(str(p)) for p in range(len(number_of_polygons))]\n\n    logger.info(\"CropSAR input dataframe:\\n\" + str(TimeSeries_df.describe()))\n    logger.info(\"Input time range: \" + str(time_range))\n\n    # calculate the cropsar timeseries\n    cropsar_timeseries = get_cropar_timeseries(\n        TimeSeries_df,\n        unique_ids_polygons,\n        columns_order,\n        time_range,\n        minimal_input_checks=minimal_input_checks,\n        s2_data_layer=s2_data_layer,\n    )\n    cropsar_timeseries = cropsar_timeseries.round(decimals=3)\n    cropsar_timeseries.index = cropsar_timeseries.index.astype(str)\n\n\n    udf_data.set_structured_data_list(\n        [StructuredData(description='CropSAR_BioPAR', data=cropsar_timeseries.to_dict(), type=\"dict\")])\n\n    return udf_data"
            },
            "result": true
        }
    },
    "id": "cropsar1d",
    "summary": "CropSAR 1D - Crop type mapping from Sentinel-1 time series",
    "description": "CropSAR is an innovative technique that uses Sentinel-1 radar observations to augment those of Sentinel-2. By doing so, more continuous vegetation properties monitoring can be performed, including vegetation information for situations where these properties cannot be retrieved from Sentinel-2 observations due to cloud occurrence. This technique is especially beneficial to operational agricultural monitoring, as the entire crop growth cycle can be observed, particularly the drastic changes during its development. The Sentinel-1 \u03c30 backscatter signal contains information on vegetation's structure and moisture content and the underlying soil. This correlation is demonstrated in Figure 1. The CropSAR temporal resolution is defined by the combined revisit rate of Sentinel-1 and cloud-free Sentinel-2 observations. For Belgium and its surroundings (~50\u00b0N), this is almost every day (because both the ascending and descending tracks for Sentinel-1 are acquired).\n\n\nA more detailed introduction to CropSAR, it's working principles, and added value is given in this [blog](https://blog.vito.be/remotesensing/cropsar2019). The various CropSAR web service elements are explained below.\n\n\n>**Note:** CropSAR is intended to provide the augmented Sentinel-1 and -2 vegetation indices for individual agricultural fields. Although it can be requested over larger areas (e.g. at the community level), the added value for such areas will be limited, and it will take a considerable time for the algorithm to provide you with the results.\n\n\n>**Disclaimer:** Requesting CropSAR in NRT (near real-time) will result in a lower performance of the fAPAR estimates. This lower performance only occurs for the fAPAR estimates in 3 months before the real-time.\n\n\n### **Methodology**\nCropSAR takes as input the cloud-interrupted time series of Sentinel-2 fAPAR and adds the uninterrupted time series of Sentinel-1 backscatter. The algorithm then returns a completely cloud-free time series of fAPAR together with its confidence level.",
    "parameters": [
        {
            "name": "geometry",
            "description": "Area of interest in GeoJSON format",
            "schema": {
                "type": "object",
                "subtype": "geojson"
            }
        },
        {
            "name": "startdate",
            "description": "Start of the temporal interval. The service will generate an image every 5 days starting from this date.",
            "schema": {
                "type": "string",
                "subtype": "date"
            }
        },
        {
            "name": "enddate",
            "description": "End of the temporal interval.",
            "schema": {
                "type": "string",
                "subtype": "date"
            }
        },
        {
            "name": "minimal_input_checks",
            "description": "Optional: by enabling this property, the service will be more forgiving with respect to input data, but may generate unreliable results.",
            "schema": {
                "type": "boolean"
            },
            "default": false,
            "optional": true
        },
        {
            "name": "output",
            "description": "Output type of the result: NDVI, FAPAR, FCOVER",
            "schema": {
                "type": "string",
                "enum": [
                    "NDVI",
                    "FAPAR",
                    "FCOVER"
                ]
            },
            "default": "NDVI",
            "optional": true
        }
    ]
}