From 638b488dab029e90a1b9c8f59c7bc68c7ce6eb68 Mon Sep 17 00:00:00 2001 From: OttaviaTr Date: Thu, 21 May 2026 15:57:12 +0100 Subject: [PATCH 01/10] added food scaling from land function --- agrifoodpy/food/model.py | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 79387a0..5f4f0b9 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -515,3 +515,111 @@ def scale_above_threshold( out = out/conversion_arr return out + + + +@pipeline_node(["fbs", "land_current", "land_reference"]) +def food_scaling_from_land( + fbs, + land_current, + land_reference, + categories, + element, + items=None, + land_dimension='percentage_target', # might need to modify default + category_dim=None, + keep_elements_constant=False, + target_items=None, + origin=None, + add_to_origin=True, + elasticity=None, + fallback=None, + add_to_fallback=True, + conversion_arr=None, + out_key=None, + data_block=None + ): + ''' + This function changes food quantities in a Food Balance Sheet array element + by scaling them relative to the change in a Land Data Array category quantities. + + Parameters + ---------- + fbs : xarray.DataSet + Food balance sheet dataset containing the current food quantities. + land_current : xarray.DataArray + Land array containing the current land quantities. + land_reference : xarray.DataArray + Land array containing the reference land quantities. + categories : string, list, tuple + Item or list of items to be used for scaling. If not provided, all items are used. + element : string + Name of the DataArray to scale. + items : list, optional + List of items to scaled in the food balance sheet. If None, all items are scaled and 'constant' is ignored. + land_dimension : string, optional + Name of the land category dimension in the land datasets. Default is 'percentage_target'. + category_dim : string, optional + Name of the category dimension in the land datasets. If None, the first non-spatial dimension is used. + keep_elements_constant : bool, optional + If set to True, the sum of element remains constant by scaling the non selected items accordingly + categories : string, list, tuple, optional + Item or list of items to be used for scaling. If not provided, all items are used. + target_items : list, optional + List of items to use for scaling when 'constant' is True. If None, all non-selected items are used for scaling. + origin : string, list, optional + Names of the DataArrays which will be used as source for the quantity changes. Any change to the "element" DataArray will be reflected in this DataArray + add_to_origin : bool, array, optional + Whether to add or subtract the difference from the respective origins + elasticity : float, array, optional + Relative fraction of the total difference to be assigned to each origin element. Values are not normalized. + fallback : string + Name of the DataArray to use as fallback in case the origin quantities fall below zero + add_to_fallback : bool, optional + Whether to add or subtract the difference below zero in the origin DataArray to the fallback array. + conversion_arr : string, xarray.DataArray, tuple or float + Conversion array to pre-scale quantities. If provided, the input food balance sheet is first converted using the conversion array, then the scaling is applied, + and finally the results are converted back to the original units using the inverse of the conversion array. + out_key : string, tuple + Output datablock path to write results to. If not given, input path is overwritten + data_block : dict, optional + Dictionary containing data + + Returns + ------- + scales_fbs : + ''' + # Use the first non spatial dimension if category dimension not provided + if category_dim is None: + category_dim = [d for d in land_current.dims if d not in ["Year", "x", "y"]][0] + + # Make categories into a list if not already + if np.isscalar(categories): + categories = [categories] + + # Obtain reference and current land quantities + ref_quantities = land_reference.sel({category_dim: categories}).sum(dim=category_dim).sum('x').sum('y') + obs_quantities = land_current.sel({category_dim: categories}).sum(dim=category_dim).sum('x').sum('y') + + # Compute scaling factor + scaling_factor = obs_quantities / ref_quantities + scaling_factor = scaling_factor[land_dimension].values + + scaled_fbs = balanced_scaling( + fbs=fbs, + scale=scaling_factor, + element=element, + items=items, + constant=keep_elements_constant, + padding_items=target_items, + origin=origin, + add_to_origin=add_to_origin, + elasticity=elasticity, + fallback=fallback, + add_to_fallback=add_to_fallback, + conversion_arr=conversion_arr, + out_key=out_key, + datablock=data_block, + ) + + return scaled_fbs \ No newline at end of file From 7807e7e1c6518b5f86f364191d75088d69685307 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Fri, 22 May 2026 10:25:09 +0100 Subject: [PATCH 02/10] Update agrifoodpy/food/model.py multiple sum Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 5f4f0b9..58bb503 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -598,8 +598,8 @@ def food_scaling_from_land( categories = [categories] # Obtain reference and current land quantities - ref_quantities = land_reference.sel({category_dim: categories}).sum(dim=category_dim).sum('x').sum('y') - obs_quantities = land_current.sel({category_dim: categories}).sum(dim=category_dim).sum('x').sum('y') + ref_quantities = land_reference.sel({category_dim: categories}).sum(dim=[category_dim, 'x', 'y']) + obs_quantities = land_current.sel({category_dim: categories}).sum(dim=[category_dim, 'x', 'y']) # Compute scaling factor scaling_factor = obs_quantities / ref_quantities From 75d95b8538d831bf9d92f3615cd3bb4266182904 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Fri, 22 May 2026 10:25:39 +0100 Subject: [PATCH 03/10] Update agrifoodpy/food/model.py Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 58bb503..514eaf0 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -603,7 +603,6 @@ def food_scaling_from_land( # Compute scaling factor scaling_factor = obs_quantities / ref_quantities - scaling_factor = scaling_factor[land_dimension].values scaled_fbs = balanced_scaling( fbs=fbs, From bca98cc94628b548906f18a43fd94aa416770578 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Fri, 22 May 2026 10:26:13 +0100 Subject: [PATCH 04/10] Update agrifoodpy/food/model.py removed land_dimension option Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 514eaf0..6e67ac8 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -526,7 +526,6 @@ def food_scaling_from_land( categories, element, items=None, - land_dimension='percentage_target', # might need to modify default category_dim=None, keep_elements_constant=False, target_items=None, From 6220edc09464992f5b69390ee84e94dfb7968236 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Fri, 22 May 2026 10:26:21 +0100 Subject: [PATCH 05/10] Update agrifoodpy/food/model.py Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 6e67ac8..026b28e 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -556,8 +556,6 @@ def food_scaling_from_land( Name of the DataArray to scale. items : list, optional List of items to scaled in the food balance sheet. If None, all items are scaled and 'constant' is ignored. - land_dimension : string, optional - Name of the land category dimension in the land datasets. Default is 'percentage_target'. category_dim : string, optional Name of the category dimension in the land datasets. If None, the first non-spatial dimension is used. keep_elements_constant : bool, optional From a43ef74a75c325b70f14636a9fb0f78d8ef7fa19 Mon Sep 17 00:00:00 2001 From: OttaviaTr Date: Mon, 25 May 2026 13:53:00 +0100 Subject: [PATCH 06/10] added unittest and characters limit --- agrifoodpy/food/model.py | 48 ++++-- agrifoodpy/food/tests/test_model.py | 234 ++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 16 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 026b28e..a0ad47a 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -551,34 +551,48 @@ def food_scaling_from_land( land_reference : xarray.DataArray Land array containing the reference land quantities. categories : string, list, tuple - Item or list of items to be used for scaling. If not provided, all items are used. + Item or list of items to be used for scaling. If not provided, all + items are used. element : string Name of the DataArray to scale. items : list, optional - List of items to scaled in the food balance sheet. If None, all items are scaled and 'constant' is ignored. + List of items to scaled in the food balance sheet. If None, all items + are scaled and 'constant' is ignored. category_dim : string, optional - Name of the category dimension in the land datasets. If None, the first non-spatial dimension is used. + Name of the category dimension in the land datasets. If None, the first + non-spatial dimension is used. keep_elements_constant : bool, optional - If set to True, the sum of element remains constant by scaling the non selected items accordingly + If set to True, the sum of element remains constant by scaling the + non selected items accordingly categories : string, list, tuple, optional - Item or list of items to be used for scaling. If not provided, all items are used. + Item or list of items to be used for scaling. If not provided, all + items are used. target_items : list, optional - List of items to use for scaling when 'constant' is True. If None, all non-selected items are used for scaling. + List of items to use for scaling when 'constant' is True. If None, all + non-selected items are used for scaling. origin : string, list, optional - Names of the DataArrays which will be used as source for the quantity changes. Any change to the "element" DataArray will be reflected in this DataArray + Names of the DataArrays which will be used as source for the quantity + changes. Any change to the "element" DataArray will be reflected in + this DataArray add_to_origin : bool, array, optional Whether to add or subtract the difference from the respective origins elasticity : float, array, optional - Relative fraction of the total difference to be assigned to each origin element. Values are not normalized. + Relative fraction of the total difference to be assigned to each origin + element. Values are not normalized. fallback : string - Name of the DataArray to use as fallback in case the origin quantities fall below zero + Name of the DataArray to use as fallback in case the origin quantities + fall below zero add_to_fallback : bool, optional - Whether to add or subtract the difference below zero in the origin DataArray to the fallback array. + Whether to add or subtract the difference below zero in the origin + DataArray to the fallback array. conversion_arr : string, xarray.DataArray, tuple or float - Conversion array to pre-scale quantities. If provided, the input food balance sheet is first converted using the conversion array, then the scaling is applied, - and finally the results are converted back to the original units using the inverse of the conversion array. + Conversion array to pre-scale quantities. If provided, the input food + balance sheet is first converted using the conversion array, then the + scaling is applied, and finally the results are converted back to the + original units using the inverse of the conversion array. out_key : string, tuple - Output datablock path to write results to. If not given, input path is overwritten + Output datablock path to write results to. If not given, input path is + overwritten data_block : dict, optional Dictionary containing data @@ -595,12 +609,14 @@ def food_scaling_from_land( categories = [categories] # Obtain reference and current land quantities - ref_quantities = land_reference.sel({category_dim: categories}).sum(dim=[category_dim, 'x', 'y']) - obs_quantities = land_current.sel({category_dim: categories}).sum(dim=[category_dim, 'x', 'y']) + ref_quantities = land_reference.sel({category_dim: categories})\ + .sum(dim=[category_dim, 'x', 'y']) + obs_quantities = land_current.sel({category_dim: categories})\ + .sum(dim=[category_dim, 'x', 'y']) # Compute scaling factor scaling_factor = obs_quantities / ref_quantities - + scaled_fbs = balanced_scaling( fbs=fbs, scale=scaling_factor, diff --git a/agrifoodpy/food/tests/test_model.py b/agrifoodpy/food/tests/test_model.py index 1d89444..49a6525 100644 --- a/agrifoodpy/food/tests/test_model.py +++ b/agrifoodpy/food/tests/test_model.py @@ -583,3 +583,237 @@ def test_scale_above_threshold(): + +def test_food_scaling_from_land_basic(): + from agrifoodpy.food.model import food_scaling_from_land + + # Food data + items = ["Beef", "Apples"] + food_coords = {"Item": items} + food_data = np.random.rand(len(food_coords["Item"])) + + fbs = xr.Dataset( + data_vars={"production": (["Item"], food_data)}, + coords=food_coords + ) + + # Land data + categories = ["Pasture", "Arable", "Forest"] + land_coords = { + "x": [0, 1, 2], + "y": [0, 1], + "category": categories, + } + + land_data = np.random.rand( + len(land_coords["x"]), + len(land_coords["y"]), + len(land_coords["category"])) + land_ref = xr.DataArray(land_data, coords=land_coords) + + land_obs = land_ref.copy(deep=True) + print(land_obs) + print() + print(fbs) + land_obs *= 2 + + result = food_scaling_from_land( + fbs=fbs, + land_current=land_obs, + land_reference=land_ref, + categories='Pasture', + element='production' + ) + + truth = fbs.copy(deep=True) + truth["production"] *= 2.0 + xr.testing.assert_allclose(result, truth) + + +def test_food_scaling_from_land_basic_change(): + from agrifoodpy.food.model import food_scaling_from_land + + # Food data + items = ["Beef", "Apples"] + food_coords = {"Item": items} + food_data = np.random.rand(len(food_coords["Item"])) + + fbs = xr.Dataset( + data_vars={"production": (["Item"], food_data)}, + coords=food_coords + ) + + # Land data + categories = ["Pasture", "Arable", "Forest"] + land_coords = { + "x": [0, 1, 2], + "y": [0, 1], + "category": categories, + } + + land_data = np.random.rand( + len(land_coords["x"]), + len(land_coords["y"]), + len(land_coords["category"])) + land_ref = xr.DataArray(land_data, coords=land_coords) + + land_obs = land_ref.copy(deep=True) + land_obs *= 2 + + result = food_scaling_from_land( + fbs=fbs, + land_current=land_obs, + land_reference=land_ref, + categories='Pasture', + element='production' + ) + + truth = fbs.copy(deep=True) + truth["production"] *= 2.0 + xr.testing.assert_allclose(result, truth) + + +def test_food_scaling_from_land_with_item(): + from agrifoodpy.food.model import food_scaling_from_land + + # Food data + items = ["Beef", "Apples"] + food_coords = {"Item": items} + food_data = np.random.rand(len(food_coords["Item"])) + + fbs = xr.Dataset( + data_vars={"production": (["Item"], food_data)}, + coords=food_coords + ) + + # Land data + categories = ["Pasture", "Arable", "Forest"] + land_coords = { + "x": [0, 1, 2], + "y": [0, 1], + "category": categories, + } + + land_data = np.random.rand( + len(land_coords["x"]), + len(land_coords["y"]), + len(land_coords["category"])) + land_ref = xr.DataArray(land_data, coords=land_coords) + + land_obs = land_ref.copy(deep=True) + print(land_obs) + land_obs *= 2 + + result = food_scaling_from_land( + fbs=fbs, + land_current=land_obs, + land_reference=land_ref, + categories='Pasture', + element='production', + items='Beef' + ) + + truth = fbs.copy(deep=True) + truth['production'] = truth['production'].where(truth['Item'] != 'Beef', + other=truth['production'] * 2) + xr.testing.assert_allclose(result, truth) + + +def test_food_scaling_from_land_target_items(): + from agrifoodpy.food.model import food_scaling_from_land + + # Food data + items = ["Beef", "Apples"] + food_coords = {"Item": items} + food_data = np.random.rand(len(food_coords["Item"])) + + fbs = xr.Dataset( + data_vars={"production": (["Item"], food_data)}, + coords=food_coords + ) + + # Land data + categories = ["Pasture", "Arable", "Forest"] + land_coords = { + "x": [0, 1, 2], + "y": [0, 1], + "category": categories, + } + + land_data = np.random.rand( + len(land_coords["x"]), + len(land_coords["y"]), + len(land_coords["category"])) + land_ref = xr.DataArray(land_data, coords=land_coords) + + land_scale = np.random.rand() * 0.5 + 1 + land_obs = land_ref.copy(deep=True) + land_obs *= land_scale + + result = food_scaling_from_land( + fbs=fbs, + land_current=land_obs, + land_reference=land_ref, + categories='Pasture', + element='production', + items = ['Beef'], + keep_elements_constant=True, + target_items=['Apples'] + ) + + truth = fbs.copy(deep=True) + truth["production"].loc[{'Item':'Beef'}] *= land_scale + truth["production"].loc[{"Item": "Apples"}] -= fbs["production"].loc[{"Item": "Beef"}] \ + * (land_scale - 1) + xr.testing.assert_allclose(result, truth) + + +def test_food_scaling_from_land_with_time_dependent_fbs(): + from agrifoodpy.food.model import food_scaling_from_land + + # Food data + items = ["Beef", "Apples"] + years = [2000, 2010] + food_coords = {"Item": items, "Year": years} + food_data = np.random.rand( + len(food_coords["Item"]), + len(food_coords["Year"]) + ) + fbs = xr.Dataset( + data_vars={"production": (["Item", "Year"], food_data)}, + coords=food_coords + ) + + # Land data + categories = ["Pasture", "Arable", "Forest"] + land_coords = { + "x": [0, 1, 2], + "y": [0, 1], + "category": categories, + } + + land_data = np.random.rand( + len(land_coords["x"]), + len(land_coords["y"]), + len(land_coords["category"])) + land_ref = xr.DataArray(land_data, coords=land_coords) + + land_scale = np.random.rand() * 0.5 + 1 + land_obs = land_ref.copy(deep=True) + land_obs *= land_scale + + result = food_scaling_from_land( + fbs=fbs, + land_current=land_obs, + land_reference=land_ref, + categories='Pasture', + element='production', + items='Beef' + ) + + truth = fbs.copy(deep=True) + truth["production"].loc[{'Item':'Beef'}] *= land_scale + xr.testing.assert_allclose(result, truth) + + + From 47e6fbf9b067d2e594353923af3541c0c96d9a40 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:25:56 +0100 Subject: [PATCH 07/10] Update agrifoodpy/food/model.py Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index a0ad47a..2897b26 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -535,8 +535,6 @@ def food_scaling_from_land( fallback=None, add_to_fallback=True, conversion_arr=None, - out_key=None, - data_block=None ): ''' This function changes food quantities in a Food Balance Sheet array element From 26d1c5fdd469542e557f9deeb7139374651fdc09 Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:26:11 +0100 Subject: [PATCH 08/10] Update agrifoodpy/food/model.py Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 2897b26..bb6ab6d 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -548,9 +548,9 @@ def food_scaling_from_land( Land array containing the current land quantities. land_reference : xarray.DataArray Land array containing the reference land quantities. - categories : string, list, tuple - Item or list of items to be used for scaling. If not provided, all - items are used. + categories : string, list + Name or list of land use category names to be monitored for relative + food scaling. element : string Name of the DataArray to scale. items : list, optional From 514da0c4bef197687524d6da8292e1dd70e6273c Mon Sep 17 00:00:00 2001 From: OttaviaTr <74863355+OttaviaTr@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:26:21 +0100 Subject: [PATCH 09/10] Update agrifoodpy/food/model.py Co-authored-by: Juan Pablo Cordero <34517350+jucordero@users.noreply.github.com> --- agrifoodpy/food/model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index bb6ab6d..31b95c7 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -628,8 +628,6 @@ def food_scaling_from_land( fallback=fallback, add_to_fallback=add_to_fallback, conversion_arr=conversion_arr, - out_key=out_key, - datablock=data_block, ) return scaled_fbs \ No newline at end of file From 14607cd75eb91ac520d38d9159c976b6bd559e6b Mon Sep 17 00:00:00 2001 From: OttaviaTr Date: Wed, 3 Jun 2026 09:33:31 +0100 Subject: [PATCH 10/10] removed datablock and out_key from food_scaling_from_land --- agrifoodpy/food/model.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/agrifoodpy/food/model.py b/agrifoodpy/food/model.py index 31b95c7..1fc5eca 100644 --- a/agrifoodpy/food/model.py +++ b/agrifoodpy/food/model.py @@ -588,11 +588,6 @@ def food_scaling_from_land( balance sheet is first converted using the conversion array, then the scaling is applied, and finally the results are converted back to the original units using the inverse of the conversion array. - out_key : string, tuple - Output datablock path to write results to. If not given, input path is - overwritten - data_block : dict, optional - Dictionary containing data Returns -------