nh3pred 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nh3pred/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from .api import predict
2
+
3
+ __all__ = ['predict']
4
+
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("nh3pred")
8
+
nh3pred/api.py ADDED
@@ -0,0 +1,172 @@
1
+ import torch
2
+ from importlib.resources import files, as_file
3
+
4
+ from .model_def import AmmoniaRNN
5
+
6
+ from .utils import generate_tensors_predictors
7
+
8
+ from .utils import check_allowed_cols
9
+ from .utils import check_needed_cols
10
+ from .utils import check_nan
11
+ from .utils import check_unique_values_for_plot_level_variables
12
+ from .utils import check_correct_values_for_categorical_variables
13
+ from .utils import check_t_incorp_col
14
+ from .utils import check_t_incorp_values
15
+
16
+ from .utils import complete_incorp
17
+ from .utils import complete_other
18
+ from .utils import add_dt
19
+ from .utils import convert_cat_variables
20
+ from .utils import reorder_variables
21
+
22
+ DEVICE = "cpu"
23
+ num_layers = 1
24
+ nonlinearity = "relu"
25
+ bidirectional = True
26
+ hidden_size = 512
27
+
28
+ cat_dims = [5, 3, 2]
29
+ embedding_dims = [10, 9, 8]
30
+ input_size = 13
31
+ output_size = 1
32
+
33
+ model = AmmoniaRNN(input_size = input_size,
34
+ output_size = output_size,
35
+ hidden_size = hidden_size,
36
+ nonlinearity = nonlinearity,
37
+ num_layers = num_layers,
38
+ bidirectional = bidirectional,
39
+ cat_dims = cat_dims,
40
+ embedding_dims = embedding_dims).to(DEVICE)
41
+
42
+
43
+ resource = files(__package__).joinpath("data").joinpath("final_model.pth")
44
+ with as_file(resource) as path:
45
+ model.load_state_dict(torch.load(path, weights_only = True, map_location=torch.device('cpu')))
46
+
47
+
48
+
49
+ def predict (df):
50
+
51
+ """
52
+ Prediction of ammonia emissions following field fertilization.
53
+
54
+ Parameters
55
+ ----------
56
+ df: pandas.DataFrame
57
+ Input DataFrame containing the environmental conditions for which predictions are made.
58
+ This DataFrame must include some mandatory columns and may include optional columns.
59
+
60
+ Mandatory columns:
61
+ - pmid: identifier for plots
62
+ - ct: time since fertilizer application (h)
63
+ - tan_app: total ammoniacal nitrogen applied (kgN/ha)
64
+
65
+ Optional columns:
66
+ Dynamic variables:
67
+ - air_temp: air temperature (°C)
68
+ - wind_2m: wind speed (m/s)
69
+ - rain_rate: rainfall rate (mm/h)
70
+ Plot-level variables:
71
+ - app_rate: application rate (t/ha)
72
+ - man_dm: manure dry matter content (%)
73
+ - man_ph: manure pH
74
+ - app_mthd: application method (must belong to {bc, bsth, ts, os, cs} (1))
75
+ - man_source: manure source (must belong to {pig, cat})
76
+ - incorp: incorporation (must belong to {none, shallow, deep})
77
+ - t_incorp: time of incorporation (h) (2)
78
+
79
+ (1) bc = broadacst, bsth = band spreading trailing hose, ts = trailing shoe, os = open slot, cs = closed slot.
80
+
81
+ (2) When incorp = none, t_incorp must be set to NaN.
82
+
83
+ Default values for optional columns: air_temp = 13.89, wind_2m = 3.11, rain_rate = 0, app_rate = 29.38, man_dm = 6.25, man_ph = 7.38, app_mthd = bsth, man_source = cat, incorp = none.
84
+
85
+ Returns
86
+ -------
87
+ pandas.DataFrame
88
+ The DataFrame completed with the column 'prediction_ecum' (cumulative prediction, kgN/ha) as well as the optional columns not initially provided, filled with their default values.
89
+
90
+ Example
91
+ --------
92
+ >>> # Prediction for two plots, identified with pmid = 1 and pmid = 2
93
+ >>> import pandas as pd
94
+ >>> from nh3pred import predict
95
+
96
+ >>> df = pd.DataFrame ({
97
+ ... "pmid": [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2],
98
+ ... "ct": [3, 6, 10, 24, 48, 72, 1, 3, 6, 10, 21, 46],
99
+ ... "tan_app": [42, 42, 42, 42, 42, 42, 120, 120, 120, 120, 120, 120],
100
+ ... "air_temp": [18, 23, 24, 15, 21, 20, 9, 10, 11, 6, 7, 10],
101
+ ... "wind_2m": [2, 2, 1, 1, 2, 2, 4, 5, 5, 6, 3, 4],
102
+ ... "rain_rate": 0,
103
+ ... "app_rate": [20, 20, 20, 20, 20, 20, 28, 28, 28, 28, 28, 28],
104
+ ... "man_dm": [8.3, 8.3, 8.3, 8.3, 8.3, 8.3, 13, 13, 13, 13, 13, 13],
105
+ ... "man_ph": [7.1, 7.1, 7.1, 7.1, 7.1, 7.1, 7.7, 7.7, 7.7, 7.7, 7.7, 7.7],
106
+ ... "app_mthd": ["ts", "ts", "ts", "ts", "ts", "ts", "bc", "bc", "bc", "bc", "bc", "bc"],
107
+ ... "man_source": ["cat", "cat", "cat", "cat", "cat", "cat", "pig", "pig", "pig", "pig", "pig", "pig"]
108
+ ... })
109
+
110
+ >>> predict (df)
111
+ pmid ct air_temp wind_2m rain_rate app_mthd incorp man_source tan_app app_rate man_dm man_ph t_incorp prediction_ecum
112
+ 0 1 3 18 2 0 ts none cat 42 20 8.3 7.1 NaN 3.090000
113
+ 1 1 6 23 2 0 ts none cat 42 20 8.3 7.1 NaN 5.620000
114
+ 2 1 10 24 1 0 ts none cat 42 20 8.3 7.1 NaN 7.460000
115
+ 3 1 24 15 1 0 ts none cat 42 20 8.3 7.1 NaN 8.030000
116
+ 4 1 48 21 2 0 ts none cat 42 20 8.3 7.1 NaN 9.760000
117
+ 5 1 72 20 2 0 ts none cat 42 20 8.3 7.1 NaN 10.480000
118
+ 6 2 1 9 4 0 bc none pig 120 28 13.0 7.7 NaN 8.550000
119
+ 7 2 3 10 5 0 bc none pig 120 28 13.0 7.7 NaN 27.150000
120
+ 8 2 6 11 5 0 bc none pig 120 28 13.0 7.7 NaN 41.639999
121
+ 9 2 10 6 6 0 bc none pig 120 28 13.0 7.7 NaN 48.549999
122
+ 10 2 21 7 3 0 bc none pig 120 28 13.0 7.7 NaN 52.610001
123
+ 11 2 46 10 4 0 bc none pig 120 28 13.0 7.7 NaN 61.220001
124
+ """
125
+
126
+ check_allowed_cols (df)
127
+ check_needed_cols (df)
128
+ check_nan (df)
129
+ check_unique_values_for_plot_level_variables (df)
130
+ check_correct_values_for_categorical_variables (df)
131
+ check_t_incorp_col (df)
132
+ check_t_incorp_values (df)
133
+
134
+ data_predictions = df.copy()
135
+
136
+ data_predictions = complete_incorp (data_predictions)
137
+ data_predictions = complete_other (data_predictions)
138
+ data_predictions = add_dt (data_predictions)
139
+ data_predictions = convert_cat_variables (data_predictions)
140
+ data_predictions = reorder_variables (data_predictions)
141
+
142
+
143
+ pmids = data_predictions['pmid'].unique()
144
+
145
+ data_predictions['prediction_ecum'] = None
146
+ data_predictions['prediction_delta_ecum'] = None
147
+
148
+ with torch.no_grad():
149
+
150
+ all_predictions = torch.empty(0).to(DEVICE)
151
+
152
+
153
+ for i in pmids:
154
+
155
+ x = generate_tensors_predictors (data_predictions, i, device = DEVICE)
156
+ y = model(x)
157
+ all_predictions = torch.cat ((all_predictions, y.squeeze()), 0)
158
+
159
+ data_predictions['prediction_delta_ecum'] = all_predictions.to("cpu").detach()
160
+
161
+ data_predictions['prediction_ecum'] = data_predictions.groupby('pmid')['prediction_delta_ecum'].cumsum()
162
+ data_predictions["prediction_ecum"] = data_predictions["prediction_ecum"].round(2)
163
+
164
+ data_predictions = data_predictions.drop (['dt', 'prediction_delta_ecum'], axis = 1)
165
+
166
+
167
+ data_predictions["app_mthd"] = data_predictions["app_mthd"].map({0: "bc", 1: "bsth", 2: "os", 3: "ts", 4:"cs"})
168
+ data_predictions["man_source"] = data_predictions["man_source"].map({0: "pig", 1: "cat"})
169
+ data_predictions["incorp"] = data_predictions["incorp"].map({0: "none", 1: "shallow", 2: "deep"})
170
+ data_predictions["t_incorp"] = data_predictions["t_incorp"].replace(1000, float("nan"))
171
+
172
+ return data_predictions
Binary file
nh3pred/model_def.py ADDED
@@ -0,0 +1,52 @@
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ class AmmoniaRNN(nn.Module):
5
+
6
+ def __init__(self,
7
+ input_size,
8
+ output_size,
9
+ hidden_size,
10
+ num_layers,
11
+ nonlinearity,
12
+ bidirectional,
13
+ cat_dims = None, embedding_dims = None):
14
+
15
+ super(AmmoniaRNN, self).__init__()
16
+
17
+ D = 1 + 1 * bidirectional
18
+
19
+ self.embeddings = nn.ModuleList([
20
+ nn.Embedding(num_embeddings = cat_dim, embedding_dim = embed_dim)
21
+ for cat_dim, embed_dim in zip(cat_dims, embedding_dims)
22
+ ])
23
+
24
+ input_size = input_size - len(cat_dims) + sum(embedding_dims)
25
+
26
+ self.rnn = nn.RNN(input_size,
27
+ hidden_size,
28
+ num_layers = num_layers,
29
+ nonlinearity = nonlinearity,
30
+ bidirectional = bidirectional)
31
+
32
+ self.fc1 = nn.Linear(hidden_size * D, 6)
33
+ self.relu = nn.ReLU()
34
+ self.fc2 = nn.Linear(6, output_size)
35
+
36
+ def forward(self, x):
37
+
38
+ x_continuous = x[0]
39
+ x_categoricals = x[1]
40
+
41
+ x_embeds = [embed(x_cat) for embed, x_cat in zip(self.embeddings, x_categoricals)]
42
+
43
+ x = torch.cat([x_continuous] + x_embeds, dim = -1)
44
+
45
+ h, _ = self.rnn(x)
46
+
47
+ out = self.fc1(h)
48
+ out = self.relu(out)
49
+ out = self.fc2(out)
50
+
51
+ return out
52
+
nh3pred/utils.py ADDED
@@ -0,0 +1,154 @@
1
+ import torch
2
+ import pandas as pd
3
+
4
+ def generate_tensors_predictors(df, pmid, device):
5
+
6
+ data_filtered = df[df['pmid'] == pmid]
7
+
8
+ x_cont = data_filtered[['ct', 'dt', 'air_temp', 'wind_2m', 'rain_rate', 'tan_app', 'app_rate', 'man_dm', 'man_ph', 't_incorp']]
9
+
10
+ x_cont_tensor = torch.tensor(x_cont.values, dtype=torch.float32).view(len(x_cont), len(x_cont.columns))
11
+ x_cont_tensor = x_cont_tensor.to(device)
12
+
13
+ x_cat = data_filtered[['app_mthd', 'incorp', 'man_source']]
14
+
15
+ x_cat_tensor = torch.tensor(x_cat.values, dtype=torch.long).view(len(x_cat), len(x_cat.columns))
16
+ x_cat_tensor = x_cat_tensor.to(device)
17
+ x_cat_tensor = torch.unbind (x_cat_tensor, dim = 1)
18
+
19
+ output = [x_cont_tensor, x_cat_tensor]
20
+
21
+ return output
22
+
23
+ def check_allowed_cols (_df):
24
+
25
+ allowed_cols = ['pmid', 'ct', 'air_temp', 'wind_2m', 'rain_rate', 'tan_app', 'app_rate', 'man_dm', 'man_ph', 'app_mthd', 'man_source', 'incorp', 't_incorp']
26
+
27
+ invalid_cols = list(set(_df.columns) - set(allowed_cols))
28
+
29
+ if invalid_cols:
30
+ raise ValueError(f"The following columns are not allowed: {invalid_cols}")
31
+
32
+ def check_needed_cols (_df):
33
+
34
+ needed_cols = ['pmid', 'ct', 'tan_app']
35
+
36
+ missing_cols = list (set(needed_cols) - set(_df.columns))
37
+
38
+ if missing_cols:
39
+ raise ValueError (f"The following column(s) is(are) missing: {missing_cols}")
40
+
41
+ def check_nan (_df):
42
+
43
+ for col in list (set (_df.columns) - set (['t_incorp'])):
44
+
45
+ if _df[col].isna().any():
46
+ raise ValueError(f"NaN values in {col}.")
47
+
48
+ def check_unique_values_for_plot_level_variables (_df):
49
+
50
+ plot_level_variables = ["app_mthd", "man_source", "incorp", "man_ph", "man_dm"]
51
+
52
+ pmids = _df['pmid'].unique()
53
+ variables_to_check = list (set (plot_level_variables) & set (_df.columns))
54
+
55
+ for var in variables_to_check:
56
+ df_tmp = _df.groupby('pmid')[var].unique()
57
+ for i in pmids:
58
+ if len (df_tmp[i]) != 1:
59
+ raise ValueError (f"Not unique value for {var} in pmid {i}")
60
+
61
+ def check_correct_values_for_categorical_variables (_df):
62
+
63
+ categorical_variables = ['app_mthd', 'incorp', 'man_source']
64
+
65
+ expected_values = {'app_mthd': ['bc', 'bsth', 'ts', 'os', 'cs'],
66
+ 'incorp': ['none', 'deep', 'shallow'],
67
+ 'man_source': ['pig', 'cat']}
68
+
69
+ variables_to_check = list (set (categorical_variables) & set (_df.columns))
70
+
71
+ for var in variables_to_check:
72
+ unvalid_values = list (set (_df[var].unique()) - set (expected_values[var]))
73
+ if unvalid_values:
74
+ raise ValueError(f"The following values are not allowed in {var}: {unvalid_values}")
75
+
76
+ def check_t_incorp_col (_df):
77
+ if 'incorp' in _df.columns:
78
+ if list (set (_df['incorp'].unique()) - set (["none"])):
79
+ if 't_incorp' not in _df.columns:
80
+ raise ValueError ("Column t_incorp is missing.")
81
+ else:
82
+ if 't_incorp' in _df.columns:
83
+ raise ValueError ("Column t_incorp is not allowed since there is no incorp column.")
84
+
85
+ def check_t_incorp_values (_df):
86
+
87
+ if 't_incorp' in _df.columns:
88
+
89
+ df_tmp = _df [(_df['incorp'] == "none") & (_df ['t_incorp'].notna())]
90
+ if df_tmp.shape[0] > 0:
91
+ raise ValueError ("t_incorp should be set to NaN when incorp = none.")
92
+
93
+ df_tmp = _df [(_df['incorp'] != "none") & (_df ['t_incorp'].isna())]
94
+ if df_tmp.shape[0] > 0:
95
+ raise ValueError ("NaN values in t_incorp for incorp != none.")
96
+
97
+ def complete_incorp (_df):
98
+
99
+ if 'incorp' in _df.columns:
100
+ if 't_incorp' in _df.columns:
101
+ _df['t_incorp'] = _df['t_incorp'].fillna (1000)
102
+
103
+ elif 't_incorp' not in _df.columns:
104
+ _df["t_incorp"] = [1000 if val == "none" else 0 for val in _df["incorp"]]
105
+
106
+
107
+ elif 'incorp' not in _df.columns:
108
+ _df.insert (len (_df.columns), 'incorp', "none")
109
+ _df.insert (len (_df.columns), 't_incorp', 1000)
110
+
111
+ return _df
112
+
113
+ def complete_other (_df):
114
+ all_columns = ['pmid', 'ct', 'air_temp', 'wind_2m', 'rain_rate', 'app_mthd', 'man_source', 'tan_app',
115
+ 'app_rate', 'man_dm', 'man_ph']
116
+
117
+ default_values = pd.Series({
118
+ 'air_temp': 13.89,
119
+ 'wind_2m': 3.11,
120
+ 'rain_rate': 0,
121
+ 'app_mthd': "bsth",
122
+ 'man_source' : 'cat',
123
+ 'tan_app' : 62.07,
124
+ 'app_rate': 29.38,
125
+ 'man_dm': 6.25,
126
+ 'man_ph': 7.38,
127
+
128
+ })
129
+
130
+ missing_col = list (set (all_columns) - set (_df.columns))
131
+
132
+ for col in missing_col:
133
+ _df.insert (len (_df.columns), col, default_values[col])
134
+
135
+ return _df
136
+
137
+ def add_dt (_df):
138
+ _df.insert (1, 'ct_shift', _df.groupby("pmid")["ct"].shift(1, fill_value=0))
139
+ _df['dt'] = _df['ct'] - _df['ct_shift']
140
+ _df = _df.drop (['ct_shift'], axis = 1)
141
+ return _df
142
+
143
+ def convert_cat_variables (_df):
144
+ _df["app_mthd"] = _df["app_mthd"].map({"bc": 0, "bsth": 1, "os": 2, "ts": 3, "cs": 4})
145
+ _df["man_source"] = _df["man_source"].map({"pig": 0, "cat": 1})
146
+ _df["incorp"] = _df["incorp"].map({"none": 0, "shallow": 1, "deep": 2})
147
+
148
+ return _df
149
+
150
+ def reorder_variables (_df):
151
+ return _df[['pmid', 'ct', 'dt', 'air_temp', 'wind_2m', 'rain_rate', 'app_mthd', 'incorp', 'man_source', 'tan_app',
152
+ 'app_rate', 'man_dm', 'man_ph', 't_incorp']]
153
+
154
+
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: nh3pred
3
+ Version: 0.1.1
4
+ Summary: Predict ammonia emissions with a recurrent neural network model
5
+ Author-email: Armand Favrot <armand.favrot@inrae.fr>
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python
9
+ Requires-Python: >=3.12.3
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pandas>=2.2.3
12
+ Requires-Dist: torch>=2.5.0
13
+ Provides-Extra: build
14
+ Requires-Dist: build; extra == "build"
15
+ Requires-Dist: twine; extra == "build"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+
19
+ # nh3pred
20
+
21
+ This package provides a single function, `predict`, which estimates ammonia emissions following field fertilization under given environmental conditions.
22
+ The underlying model is a recurrent neural network described in [ref] under the name *"rnn 9 – data a."*.
23
+ The `predict` function works similarly to the `alfam2` function from the R package [ALFAM2](https://cran.r-project.org/web/packages/ALFAM2/index.html).
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install nh3pred
29
+ ```
30
+
31
+ ## Documentation
32
+
33
+ A complete documentation for the `predict` function is available [here](https://nh3pred.readthedocs.io/en/latest/api.html#nh3pred.api.predict).
34
+
35
+ ## Usage
36
+
37
+ You can use the package in Python as follows:
38
+
39
+ ```python
40
+ import pandas as pd
41
+ from ammonia_predict_3 import predict
42
+
43
+ df = pd.DataFrame ({
44
+ "pmid": [1, 1, 1, 1, 1, 1],
45
+ "ct": [3, 6, 10, 24, 48, 72],
46
+ "tan_app": [42, 42, 42, 42, 42, 42],
47
+ "air_temp": [18, 23, 24, 15, 21, 20],
48
+ "wind_2m": [2, 2, 1, 1, 2, 2],
49
+ "rain_rate": 0,
50
+ "app_rate": [20, 20, 20, 20, 20, 20],
51
+ "man_dm": [8.3, 8.3, 8.3, 8.3, 8.3, 8.3],
52
+ "man_ph": [7.1, 7.1, 7.1, 7.1, 7.1, 7.1],
53
+ "app_mthd": ["ts", "ts", "ts", "ts", "ts", "ts"],
54
+ "man_source": ["cat", "cat", "cat", "cat", "cat", "cat"]
55
+ })
56
+
57
+ pred = predict(df)
58
+ print(pred)
59
+ ```
60
+
61
+
62
+ ## Notes
63
+
64
+ - The trained weights are included in the package under `ammonia_predict/data/final_model.pth`.
65
+ - The package requires **Python ≥3.12**, **PyTorch ≥2.5.0**, and **pandas ≥2.2.3**.
66
+
67
+
68
+
@@ -0,0 +1,9 @@
1
+ nh3pred/__init__.py,sha256=gR8afgOvwZBjb2w-AONXJZCHqINKQydV5wYNObFcXtM,125
2
+ nh3pred/api.py,sha256=IqT9YZQNdz9Llz3HBw5TVQHn6UPrKOGCGBM5pTxd1w4,7902
3
+ nh3pred/model_def.py,sha256=aue_GP7nnJGFwp5645ScluyDHdt0kBi9g8WxcFs17mI,1528
4
+ nh3pred/utils.py,sha256=z2H6dxebdvVJrN96_Qx4XWYVt8czMj7fFo7xzQocxgE,5352
5
+ nh3pred/data/final_model.pth,sha256=RSAaLGytjB9uQPlFW_EAKyrwGjaDmlQKhLgDozJfB18,2285808
6
+ nh3pred-0.1.1.dist-info/METADATA,sha256=sgEKMbzf5Y1VGVrzYYN1zL328I2-dpJO6nXi4uU-6LY,2077
7
+ nh3pred-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ nh3pred-0.1.1.dist-info/top_level.txt,sha256=pCAF7nBmtSkoPGy2HebVx00kF1r6gascwX4NOmfLFVw,8
9
+ nh3pred-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ nh3pred