geometallurgy 0.4.12__py3-none-any.whl → 0.4.13__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.
Files changed (48) hide show
  1. elphick/geomet/__init__.py +11 -11
  2. elphick/geomet/base.py +1133 -1133
  3. elphick/geomet/block_model.py +319 -358
  4. elphick/geomet/config/__init__.py +1 -1
  5. elphick/geomet/config/config_read.py +39 -39
  6. elphick/geomet/config/flowsheet_example_partition.yaml +31 -31
  7. elphick/geomet/config/flowsheet_example_simple.yaml +25 -25
  8. elphick/geomet/config/mc_config.yml +35 -35
  9. elphick/geomet/data/downloader.py +39 -39
  10. elphick/geomet/data/register.csv +12 -12
  11. elphick/geomet/datasets/__init__.py +2 -2
  12. elphick/geomet/datasets/datasets.py +47 -47
  13. elphick/geomet/datasets/downloader.py +40 -40
  14. elphick/geomet/datasets/register.csv +12 -12
  15. elphick/geomet/datasets/sample_data.py +196 -196
  16. elphick/geomet/extras.py +35 -35
  17. elphick/geomet/flowsheet/__init__.py +1 -1
  18. elphick/geomet/flowsheet/flowsheet.py +1216 -1216
  19. elphick/geomet/flowsheet/loader.py +99 -99
  20. elphick/geomet/flowsheet/operation.py +256 -256
  21. elphick/geomet/flowsheet/stream.py +39 -39
  22. elphick/geomet/interval_sample.py +641 -641
  23. elphick/geomet/io.py +379 -379
  24. elphick/geomet/plot.py +147 -147
  25. elphick/geomet/sample.py +28 -28
  26. elphick/geomet/utils/amenability.py +49 -49
  27. elphick/geomet/utils/block_model_converter.py +93 -93
  28. elphick/geomet/utils/components.py +136 -136
  29. elphick/geomet/utils/data.py +49 -49
  30. elphick/geomet/utils/estimates.py +108 -108
  31. elphick/geomet/utils/interp.py +193 -193
  32. elphick/geomet/utils/interp2.py +134 -134
  33. elphick/geomet/utils/layout.py +72 -72
  34. elphick/geomet/utils/moisture.py +61 -61
  35. elphick/geomet/utils/output.html +617 -0
  36. elphick/geomet/utils/pandas.py +378 -378
  37. elphick/geomet/utils/parallel.py +29 -29
  38. elphick/geomet/utils/partition.py +63 -63
  39. elphick/geomet/utils/size.py +51 -51
  40. elphick/geomet/utils/timer.py +80 -80
  41. elphick/geomet/utils/viz.py +56 -56
  42. elphick/geomet/validate.py.hide +176 -176
  43. {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/LICENSE +21 -21
  44. {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/METADATA +7 -5
  45. geometallurgy-0.4.13.dist-info/RECORD +49 -0
  46. {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/WHEEL +1 -1
  47. geometallurgy-0.4.12.dist-info/RECORD +0 -48
  48. {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/entry_points.txt +0 -0
@@ -1,193 +1,193 @@
1
- from typing import Optional, Iterable, Union
2
-
3
- import numpy as np
4
- import pandas as pd
5
- from scipy.interpolate import pchip_interpolate
6
-
7
- from elphick.geomet.utils.pandas import composition_to_mass, mass_to_composition, weight_average
8
-
9
-
10
- def mass_preserving_interp(df_intervals: pd.DataFrame, interval_edges: Union[Iterable, int],
11
- include_original_edges: bool = True, precision: Optional[int] = None,
12
- mass_wet: Optional[str] = 'mass_wet', mass_dry: str = 'mass_dry',
13
- interval_data_as_mass: bool = False) -> pd.DataFrame:
14
- """Interpolate with zero mass loss using pchip
15
-
16
- This interpolates data_vars independently for a single dimension (coord) at a time.
17
-
18
- The function will:
19
- - convert from relative composition (%) to absolute (mass) (subject to interval_data_as_mass argument)
20
- - convert the index from interval to a float representing the right edge of the interval
21
- - cumsum to provide monotonic increasing data
22
- - interpolate with a pchip spline to preserve mass
23
- - diff to recover the original fractional data
24
- - reconstruct the interval index from the right edges
25
- - convert from absolute to relative composition
26
-
27
- Args:
28
- df_intervals: A pd.DataFrame with a single interval index, with mass, composition context.
29
- interval_edges: The values of the new grid (interval edges). If an int, will up-sample by that factor, for
30
- example the value of 10 will automatically define edges that create 10 x the resolution (up-sampled).
31
- include_original_edges: If True include the original index edges in the result
32
- precision: Number of decimal places to round the index (edge) values.
33
- mass_wet: The wet mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
34
- mass_dry: The dry mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
35
- interval_data_as_mass: If True, the data is assumed to be mass data, not composition data, negating the need
36
- to convert to mass.
37
-
38
- Returns:
39
-
40
- """
41
-
42
- if not isinstance(df_intervals.index, pd.IntervalIndex):
43
- raise NotImplementedError(f"The index `{df_intervals.index}` of the dataframe is not a pd.Interval. "
44
- f"Only 1D interval indexes are valid")
45
-
46
- composition_in: pd.DataFrame = df_intervals.copy()
47
-
48
- if isinstance(interval_edges, int):
49
- grid_vals = _upsample_grid_by_factor(indx=composition_in.sort_index().index, factor=interval_edges)
50
- else:
51
- grid_vals = np.sort(np.array(interval_edges))
52
-
53
- if precision is not None:
54
- composition_in.index = pd.IntervalIndex.from_arrays(np.round(df_intervals.index.left, precision),
55
- np.round(df_intervals.index.right, precision),
56
- closed=df_intervals.index.closed,
57
- name=df_intervals.index.name)
58
-
59
- grid_vals = np.round(grid_vals, precision)
60
-
61
- if include_original_edges:
62
- original_edges = np.hstack([df_intervals.index.left, df_intervals.index.right])
63
- grid_vals = np.sort(np.unique(np.hstack([grid_vals, original_edges])))
64
-
65
- if not isinstance(grid_vals, np.ndarray):
66
- grid_vals = np.array(grid_vals)
67
-
68
- if not interval_data_as_mass:
69
- # convert from relative composition (%) to absolute (mass)
70
- mass_in: pd.DataFrame = composition_to_mass(composition_in, mass_wet=mass_wet, mass_dry=mass_dry)
71
- else:
72
- mass_in: pd.DataFrame = composition_in.copy()
73
- # convert the index from interval to a float representing the right edge of the interval
74
- mass_in.index = mass_in.index.right
75
- # add a row of zeros
76
- mass_in = pd.concat(
77
- [mass_in, pd.Series(0, index=mass_in.columns, name=composition_in.index.left.min()).to_frame().T],
78
- axis=0).sort_index(ascending=True)
79
- # cumsum to provide monotonic increasing data
80
- mass_cum: pd.DataFrame = mass_in.cumsum()
81
- # if the new grid extrapolates (on the coarse side), mass will be lost, so we assume that when extrapolating.
82
- # the mass in the extrapolated fractions is zero. By inserting these records the spline will conform.
83
- x_extra = grid_vals[grid_vals > mass_cum.index.max()]
84
- if len(x_extra) > 0:
85
- cum_max: pd.Series = mass_cum.iloc[-1, :]
86
- mass_cum = mass_cum.reindex(index=mass_cum.index.append(pd.Index(x_extra))) # reindex to enable insert
87
- mass_cum.loc[x_extra, :] = cum_max.values
88
- # interpolate with a pchip spline to preserve mass
89
- chunks = []
90
- for col in mass_cum:
91
- tmp = mass_cum[col].dropna() # drop any missing values
92
- new_vals = pchip_interpolate(tmp.index.values, tmp.values, grid_vals)
93
- chunks.append(new_vals)
94
- mass_cum_upsampled: pd.DataFrame = pd.DataFrame(chunks, index=mass_in.columns, columns=grid_vals).T
95
- # diff to recover the original fractional data
96
- mass_fractions_upsampled: pd.DataFrame = mass_cum_upsampled.diff().dropna(axis=0)
97
- # reconstruct the interval index from the grid
98
- mass_fractions_upsampled.index = pd.IntervalIndex.from_arrays(left=grid_vals[:-1],
99
- right=grid_vals[1:],
100
- closed=df_intervals.index.closed,
101
- name=df_intervals.index.name)
102
- interval_spans: dict[pd.Interval, float] = {interval: interval.right - interval.left for interval in
103
- mass_fractions_upsampled.index}
104
- zero_spans = [k for k, v in interval_spans.items() if v == 0]
105
- if len(zero_spans) > 1:
106
- raise ValueError(f"The interpolated index contains zero width intervals on left edges: {zero_spans}")
107
-
108
- # convert from absolute to relative composition
109
- res = mass_to_composition(mass_fractions_upsampled, mass_wet=mass_wet, mass_dry=mass_dry).sort_index(
110
- ascending=False)
111
-
112
- # confirm the weight average is preserved
113
- if not np.isclose(mass_in.sum().sum(), mass_fractions_upsampled.sum().sum(), rtol=1e-6):
114
- raise ValueError("The mass is not preserved in the interpolation")
115
- return res
116
-
117
-
118
- def _upsample_grid_by_factor(indx: pd.IntervalIndex, factor):
119
- # TODO: must be a better way than this - vectorised?
120
- grid_vals: list = [indx.left.min()]
121
- for interval in indx:
122
- increment = (interval.right - interval.left) / factor
123
- for i in range(0, factor):
124
- grid_vals.append(interval.left + (i + 1) * increment)
125
- grid_vals.sort()
126
- return grid_vals
127
-
128
-
129
- def mass_preserving_interp_2d(intervals: pd.DataFrame, interval_edges: dict[str, Iterable],
130
- include_original_edges: bool = True, precision: Optional[int] = None,
131
- mass_dry: str = 'mass_dry') -> pd.DataFrame:
132
- """Interpolate 2D interval data with zero mass loss using pchip
133
-
134
- This function applies mass-preserving up-sampling to 2D interval data. The function will:
135
- - resample the first dimension using the mass_preserving_interp function
136
- - resample the second dimension using the mass_preserving_interp function at each of the new first
137
- dimension intervals
138
- - apply the upsampled mass proportions of the second dimension to the first dimension to create the final result
139
-
140
- Args:
141
- intervals: Dataframe with two pd.IntervalIndexes, in mass-composition space.
142
- interval_edges: Dict of the values of the new grid (interval edges) for each dimension, keyed by
143
- index name (dimension).
144
- include_original_edges: If True include the original index edges in the result
145
- precision: Number of decimal places to round the index (edge) values.
146
- mass_dry: The dry mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
147
-
148
- Returns:
149
-
150
- """
151
-
152
- # reduce the dataframe to the first dimension
153
- dim_1_name, dim_2_name = intervals.index.names[0], intervals.index.names[1]
154
- dim_1: pd.DataFrame = intervals.groupby(dim_1_name).apply(weight_average, **{'mass_dry': mass_dry})
155
-
156
- # interpolate the first dimension
157
- dim_1_interp: pd.DataFrame = mass_preserving_interp(dim_1, interval_edges=interval_edges[dim_1_name],
158
- include_original_edges=include_original_edges,
159
- precision=precision, mass_dry=mass_dry)
160
-
161
- chunks: list = []
162
- # iterate the original dim_1 fractions
163
- for dim_1_interval in dim_1.index:
164
- # reduce the dataframe to the second dimension for the current dim1 interval
165
- dim_2: pd.DataFrame = intervals.loc[dim_1_interval].copy()
166
- # interpolate the second dimension
167
- dim_2_interp: pd.DataFrame = mass_preserving_interp(dim_2, interval_edges=interval_edges[dim_2_name],
168
- include_original_edges=include_original_edges,
169
- precision=precision, mass_dry=mass_dry)
170
- # convert to recovery to enable proportioning of the interpolated dim_1 values
171
- dim_2_mass: pd.DataFrame = composition_to_mass(dim_2_interp, mass_dry=mass_dry)
172
- dim_2_deportment: pd.DataFrame = dim_2_mass.div(dim_2_mass.sum(axis=0), axis=1)
173
-
174
- # Filter the intervals from dim_1_interp that fall within the provided dim_1_interval, convert to mass
175
- filtered_intervals_mass = composition_to_mass(
176
- dim_1_interp.loc[dim_1_interp.index.map(lambda x: x.overlaps(dim_1_interval))].copy(), mass_dry=mass_dry)
177
-
178
- # expand the dimensions of the interpolated dim_1 data to include the second upsampled dimension
179
- for dim_1_interp_interval in filtered_intervals_mass.index:
180
- # proportion the dim_1 interpolated values by the dim_2 recovery
181
- new_vals: pd.DataFrame = dim_2_deportment.mul(filtered_intervals_mass.loc[dim_1_interp_interval].values)
182
- # create a multiindex by combining the dim_1 and dim_2 intervals
183
- new_index = pd.MultiIndex.from_arrays(
184
- [pd.IntervalIndex([dim_1_interp_interval] * len(new_vals)), new_vals.index])
185
- new_vals.index = new_index
186
- chunks.append(new_vals)
187
-
188
- # concatenate the results
189
- res: pd.DataFrame = pd.concat(chunks, axis=0)
190
- res.index.names = [dim_1_name, dim_2_name]
191
- # convert to composition
192
- res = mass_to_composition(res, mass_dry=mass_dry)
193
- return res
1
+ from typing import Optional, Iterable, Union
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.interpolate import pchip_interpolate
6
+
7
+ from elphick.geomet.utils.pandas import composition_to_mass, mass_to_composition, weight_average
8
+
9
+
10
+ def mass_preserving_interp(df_intervals: pd.DataFrame, interval_edges: Union[Iterable, int],
11
+ include_original_edges: bool = True, precision: Optional[int] = None,
12
+ mass_wet: Optional[str] = 'mass_wet', mass_dry: str = 'mass_dry',
13
+ interval_data_as_mass: bool = False) -> pd.DataFrame:
14
+ """Interpolate with zero mass loss using pchip
15
+
16
+ This interpolates data_vars independently for a single dimension (coord) at a time.
17
+
18
+ The function will:
19
+ - convert from relative composition (%) to absolute (mass) (subject to interval_data_as_mass argument)
20
+ - convert the index from interval to a float representing the right edge of the interval
21
+ - cumsum to provide monotonic increasing data
22
+ - interpolate with a pchip spline to preserve mass
23
+ - diff to recover the original fractional data
24
+ - reconstruct the interval index from the right edges
25
+ - convert from absolute to relative composition
26
+
27
+ Args:
28
+ df_intervals: A pd.DataFrame with a single interval index, with mass, composition context.
29
+ interval_edges: The values of the new grid (interval edges). If an int, will up-sample by that factor, for
30
+ example the value of 10 will automatically define edges that create 10 x the resolution (up-sampled).
31
+ include_original_edges: If True include the original index edges in the result
32
+ precision: Number of decimal places to round the index (edge) values.
33
+ mass_wet: The wet mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
34
+ mass_dry: The dry mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
35
+ interval_data_as_mass: If True, the data is assumed to be mass data, not composition data, negating the need
36
+ to convert to mass.
37
+
38
+ Returns:
39
+
40
+ """
41
+
42
+ if not isinstance(df_intervals.index, pd.IntervalIndex):
43
+ raise NotImplementedError(f"The index `{df_intervals.index}` of the dataframe is not a pd.Interval. "
44
+ f"Only 1D interval indexes are valid")
45
+
46
+ composition_in: pd.DataFrame = df_intervals.copy()
47
+
48
+ if isinstance(interval_edges, int):
49
+ grid_vals = _upsample_grid_by_factor(indx=composition_in.sort_index().index, factor=interval_edges)
50
+ else:
51
+ grid_vals = np.sort(np.array(interval_edges))
52
+
53
+ if precision is not None:
54
+ composition_in.index = pd.IntervalIndex.from_arrays(np.round(df_intervals.index.left, precision),
55
+ np.round(df_intervals.index.right, precision),
56
+ closed=df_intervals.index.closed,
57
+ name=df_intervals.index.name)
58
+
59
+ grid_vals = np.round(grid_vals, precision)
60
+
61
+ if include_original_edges:
62
+ original_edges = np.hstack([df_intervals.index.left, df_intervals.index.right])
63
+ grid_vals = np.sort(np.unique(np.hstack([grid_vals, original_edges])))
64
+
65
+ if not isinstance(grid_vals, np.ndarray):
66
+ grid_vals = np.array(grid_vals)
67
+
68
+ if not interval_data_as_mass:
69
+ # convert from relative composition (%) to absolute (mass)
70
+ mass_in: pd.DataFrame = composition_to_mass(composition_in, mass_wet=mass_wet, mass_dry=mass_dry)
71
+ else:
72
+ mass_in: pd.DataFrame = composition_in.copy()
73
+ # convert the index from interval to a float representing the right edge of the interval
74
+ mass_in.index = mass_in.index.right
75
+ # add a row of zeros
76
+ mass_in = pd.concat(
77
+ [mass_in, pd.Series(0, index=mass_in.columns, name=composition_in.index.left.min()).to_frame().T],
78
+ axis=0).sort_index(ascending=True)
79
+ # cumsum to provide monotonic increasing data
80
+ mass_cum: pd.DataFrame = mass_in.cumsum()
81
+ # if the new grid extrapolates (on the coarse side), mass will be lost, so we assume that when extrapolating.
82
+ # the mass in the extrapolated fractions is zero. By inserting these records the spline will conform.
83
+ x_extra = grid_vals[grid_vals > mass_cum.index.max()]
84
+ if len(x_extra) > 0:
85
+ cum_max: pd.Series = mass_cum.iloc[-1, :]
86
+ mass_cum = mass_cum.reindex(index=mass_cum.index.append(pd.Index(x_extra))) # reindex to enable insert
87
+ mass_cum.loc[x_extra, :] = cum_max.values
88
+ # interpolate with a pchip spline to preserve mass
89
+ chunks = []
90
+ for col in mass_cum:
91
+ tmp = mass_cum[col].dropna() # drop any missing values
92
+ new_vals = pchip_interpolate(tmp.index.values, tmp.values, grid_vals)
93
+ chunks.append(new_vals)
94
+ mass_cum_upsampled: pd.DataFrame = pd.DataFrame(chunks, index=mass_in.columns, columns=grid_vals).T
95
+ # diff to recover the original fractional data
96
+ mass_fractions_upsampled: pd.DataFrame = mass_cum_upsampled.diff().dropna(axis=0)
97
+ # reconstruct the interval index from the grid
98
+ mass_fractions_upsampled.index = pd.IntervalIndex.from_arrays(left=grid_vals[:-1],
99
+ right=grid_vals[1:],
100
+ closed=df_intervals.index.closed,
101
+ name=df_intervals.index.name)
102
+ interval_spans: dict[pd.Interval, float] = {interval: interval.right - interval.left for interval in
103
+ mass_fractions_upsampled.index}
104
+ zero_spans = [k for k, v in interval_spans.items() if v == 0]
105
+ if len(zero_spans) > 1:
106
+ raise ValueError(f"The interpolated index contains zero width intervals on left edges: {zero_spans}")
107
+
108
+ # convert from absolute to relative composition
109
+ res = mass_to_composition(mass_fractions_upsampled, mass_wet=mass_wet, mass_dry=mass_dry).sort_index(
110
+ ascending=False)
111
+
112
+ # confirm the weight average is preserved
113
+ if not np.isclose(mass_in.sum().sum(), mass_fractions_upsampled.sum().sum(), rtol=1e-6):
114
+ raise ValueError("The mass is not preserved in the interpolation")
115
+ return res
116
+
117
+
118
+ def _upsample_grid_by_factor(indx: pd.IntervalIndex, factor):
119
+ # TODO: must be a better way than this - vectorised?
120
+ grid_vals: list = [indx.left.min()]
121
+ for interval in indx:
122
+ increment = (interval.right - interval.left) / factor
123
+ for i in range(0, factor):
124
+ grid_vals.append(interval.left + (i + 1) * increment)
125
+ grid_vals.sort()
126
+ return grid_vals
127
+
128
+
129
+ def mass_preserving_interp_2d(intervals: pd.DataFrame, interval_edges: dict[str, Iterable],
130
+ include_original_edges: bool = True, precision: Optional[int] = None,
131
+ mass_dry: str = 'mass_dry') -> pd.DataFrame:
132
+ """Interpolate 2D interval data with zero mass loss using pchip
133
+
134
+ This function applies mass-preserving up-sampling to 2D interval data. The function will:
135
+ - resample the first dimension using the mass_preserving_interp function
136
+ - resample the second dimension using the mass_preserving_interp function at each of the new first
137
+ dimension intervals
138
+ - apply the upsampled mass proportions of the second dimension to the first dimension to create the final result
139
+
140
+ Args:
141
+ intervals: Dataframe with two pd.IntervalIndexes, in mass-composition space.
142
+ interval_edges: Dict of the values of the new grid (interval edges) for each dimension, keyed by
143
+ index name (dimension).
144
+ include_original_edges: If True include the original index edges in the result
145
+ precision: Number of decimal places to round the index (edge) values.
146
+ mass_dry: The dry mass column, not optional. Consider solve_mass_moisture prior to this call if needed.
147
+
148
+ Returns:
149
+
150
+ """
151
+
152
+ # reduce the dataframe to the first dimension
153
+ dim_1_name, dim_2_name = intervals.index.names[0], intervals.index.names[1]
154
+ dim_1: pd.DataFrame = intervals.groupby(dim_1_name).apply(weight_average, **{'mass_dry': mass_dry})
155
+
156
+ # interpolate the first dimension
157
+ dim_1_interp: pd.DataFrame = mass_preserving_interp(dim_1, interval_edges=interval_edges[dim_1_name],
158
+ include_original_edges=include_original_edges,
159
+ precision=precision, mass_dry=mass_dry)
160
+
161
+ chunks: list = []
162
+ # iterate the original dim_1 fractions
163
+ for dim_1_interval in dim_1.index:
164
+ # reduce the dataframe to the second dimension for the current dim1 interval
165
+ dim_2: pd.DataFrame = intervals.loc[dim_1_interval].copy()
166
+ # interpolate the second dimension
167
+ dim_2_interp: pd.DataFrame = mass_preserving_interp(dim_2, interval_edges=interval_edges[dim_2_name],
168
+ include_original_edges=include_original_edges,
169
+ precision=precision, mass_dry=mass_dry)
170
+ # convert to recovery to enable proportioning of the interpolated dim_1 values
171
+ dim_2_mass: pd.DataFrame = composition_to_mass(dim_2_interp, mass_dry=mass_dry)
172
+ dim_2_deportment: pd.DataFrame = dim_2_mass.div(dim_2_mass.sum(axis=0), axis=1)
173
+
174
+ # Filter the intervals from dim_1_interp that fall within the provided dim_1_interval, convert to mass
175
+ filtered_intervals_mass = composition_to_mass(
176
+ dim_1_interp.loc[dim_1_interp.index.map(lambda x: x.overlaps(dim_1_interval))].copy(), mass_dry=mass_dry)
177
+
178
+ # expand the dimensions of the interpolated dim_1 data to include the second upsampled dimension
179
+ for dim_1_interp_interval in filtered_intervals_mass.index:
180
+ # proportion the dim_1 interpolated values by the dim_2 recovery
181
+ new_vals: pd.DataFrame = dim_2_deportment.mul(filtered_intervals_mass.loc[dim_1_interp_interval].values)
182
+ # create a multiindex by combining the dim_1 and dim_2 intervals
183
+ new_index = pd.MultiIndex.from_arrays(
184
+ [pd.IntervalIndex([dim_1_interp_interval] * len(new_vals)), new_vals.index])
185
+ new_vals.index = new_index
186
+ chunks.append(new_vals)
187
+
188
+ # concatenate the results
189
+ res: pd.DataFrame = pd.concat(chunks, axis=0)
190
+ res.index.names = [dim_1_name, dim_2_name]
191
+ # convert to composition
192
+ res = mass_to_composition(res, mass_dry=mass_dry)
193
+ return res