geometallurgy 0.4.13__py3-none-any.whl → 0.4.15__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.
- elphick/geomet/__init__.py +11 -11
- elphick/geomet/base.py +1133 -1133
- elphick/geomet/block_model.py +319 -319
- elphick/geomet/config/__init__.py +1 -1
- elphick/geomet/config/config_read.py +39 -39
- elphick/geomet/config/flowsheet_example_partition.yaml +31 -31
- elphick/geomet/config/flowsheet_example_simple.yaml +25 -25
- elphick/geomet/config/mc_config.yml +35 -35
- elphick/geomet/data/downloader.py +39 -39
- elphick/geomet/data/register.csv +12 -12
- elphick/geomet/datasets/__init__.py +2 -2
- elphick/geomet/datasets/datasets.py +47 -47
- elphick/geomet/datasets/downloader.py +40 -40
- elphick/geomet/datasets/register.csv +12 -12
- elphick/geomet/datasets/sample_data.py +196 -196
- elphick/geomet/extras.py +35 -35
- elphick/geomet/flowsheet/__init__.py +1 -1
- elphick/geomet/flowsheet/flowsheet.py +1216 -1216
- elphick/geomet/flowsheet/loader.py +99 -99
- elphick/geomet/flowsheet/operation.py +256 -256
- elphick/geomet/flowsheet/stream.py +39 -39
- elphick/geomet/interval_sample.py +641 -641
- elphick/geomet/io.py +379 -379
- elphick/geomet/plot.py +147 -147
- elphick/geomet/sample.py +28 -28
- elphick/geomet/utils/amenability.py +49 -49
- elphick/geomet/utils/block_model_converter.py +93 -93
- elphick/geomet/utils/components.py +136 -136
- elphick/geomet/utils/data.py +49 -49
- elphick/geomet/utils/estimates.py +108 -108
- elphick/geomet/utils/interp.py +193 -193
- elphick/geomet/utils/interp2.py +134 -134
- elphick/geomet/utils/layout.py +72 -72
- elphick/geomet/utils/moisture.py +61 -61
- elphick/geomet/utils/pandas.py +378 -378
- elphick/geomet/utils/parallel.py +29 -29
- elphick/geomet/utils/partition.py +63 -63
- elphick/geomet/utils/size.py +51 -51
- elphick/geomet/utils/timer.py +80 -80
- elphick/geomet/utils/viz.py +56 -56
- elphick/geomet/validate.py.hide +176 -176
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/LICENSE +21 -21
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/METADATA +2 -3
- geometallurgy-0.4.15.dist-info/RECORD +48 -0
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/WHEEL +1 -1
- elphick/geomet/utils/output.html +0 -617
- geometallurgy-0.4.13.dist-info/RECORD +0 -49
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/entry_points.txt +0 -0
elphick/geomet/utils/interp.py
CHANGED
|
@@ -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
|