parasolpy 0.0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Kasprzyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: parasolpy
3
+ Version: 0.0.1
4
+ Summary: Python tools for multi-objective decision analysis and visualization
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Author: Joseph Kasprzyk
8
+ Author-email: joseph.kasprzyk@colorado.edu
9
+ Requires-Python: >=3.12
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
15
+ Requires-Dist: dash (>=4.1.0,<5.0.0)
16
+ Requires-Dist: hiplot (>=0.1.33,<0.2.0)
17
+ Requires-Dist: matplotlib (>=3.10.8,<4.0.0)
18
+ Requires-Dist: numpy (>=2.4.4,<3.0.0)
19
+ Requires-Dist: pandas (>=3.0.2,<4.0.0)
20
+ Requires-Dist: platypus-opt (>=1.4.1,<2.0.0)
21
+ Requires-Dist: plotly (>=6.7.0,<7.0.0)
22
+ Requires-Dist: scikit-learn (>=1.8.0,<2.0.0)
23
+ Requires-Dist: scipy (>=1.17.1,<2.0.0)
24
+ Requires-Dist: seaborn (>=0.13.2,<0.14.0)
25
+ Description-Content-Type: text/markdown
26
+
27
+ # parasolpy
28
+
29
+ Python tools for multi-objective decision analysis and visualization, continuing the Parasol project originally published as:
30
+
31
+ Raseman, W. J., Jacobson, J., & Kasprzyk, J. R. (2019). Parasol: An open source, interactive parallel coordinates library for multi-objective decision making. *Environmental Modelling & Software*, 116, 153–163. https://doi.org/10.1016/j.envsoft.2019.03.005
32
+
33
+ ## Status
34
+
35
+ 🚧 Early development. More documentation and functionality coming.
36
+
37
+ ## Lineage
38
+
39
+ This package is developed by the Kasprzyk Research Group at CU Boulder. It is not affiliated with the unrelated `parasol` or `pyparasol` packages on PyPI, which are independent projects that share the name.
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,17 @@
1
+ # parasolpy
2
+
3
+ Python tools for multi-objective decision analysis and visualization, continuing the Parasol project originally published as:
4
+
5
+ Raseman, W. J., Jacobson, J., & Kasprzyk, J. R. (2019). Parasol: An open source, interactive parallel coordinates library for multi-objective decision making. *Environmental Modelling & Software*, 116, 153–163. https://doi.org/10.1016/j.envsoft.2019.03.005
6
+
7
+ ## Status
8
+
9
+ 🚧 Early development. More documentation and functionality coming.
10
+
11
+ ## Lineage
12
+
13
+ This package is developed by the Kasprzyk Research Group at CU Boulder. It is not affiliated with the unrelated `parasol` or `pyparasol` packages on PyPI, which are independent projects that share the name.
14
+
15
+ ## License
16
+
17
+ MIT
@@ -0,0 +1,127 @@
1
+ """Top-level package exports for parasolpy.
2
+
3
+ Importing names here lets callers write:
4
+ from parasolpy import has_superheader
5
+ instead of the longer:
6
+ from parasolpy.file_processing import has_superheader
7
+
8
+ Both styles always work — the short form is just a convenience. The long
9
+ form (importing directly from the submodule) is never broken by anything here:
10
+ from parasolpy.tradeoff import append_Kmeans # always valid
11
+ from parasolpy import append_Kmeans # also valid
12
+ """
13
+
14
+ __version__ = "0.0.1"
15
+
16
+ from parasolpy.dash_tools import (
17
+ build_tradeoff_dash_app,
18
+ load_dash_inputs,
19
+ )
20
+ from parasolpy.file_processing import (
21
+ convert_solutions_csv_to_single_header,
22
+ has_superheader,
23
+ load_solutions_dataframe,
24
+ split_solutions_csv,
25
+ split_solutions_dataframe,
26
+ )
27
+ from parasolpy.interactive import (
28
+ ExitInteractiveSession,
29
+ load_experiment_epsilons,
30
+ prompt_epsilons,
31
+ prompt_experiment_name,
32
+ prompt_starting_epsilons,
33
+ )
34
+ from parasolpy.ism import create_ism_traces
35
+ from parasolpy.nowak import (
36
+ choose_analog_index,
37
+ sim_multi_trace,
38
+ sim_single_year,
39
+ )
40
+ from parasolpy.plotting import (
41
+ plot_trace_exceedance,
42
+ plot_trace_fan_chart,
43
+ plot_trace_heatmap,
44
+ plot_trace_monthly_seasonality,
45
+ plot_trace_spaghetti,
46
+ )
47
+ from parasolpy.reservoir import sequent_peak
48
+ from parasolpy.tradeoff import (
49
+ append_Kmeans,
50
+ build_radar_figure,
51
+ df_to_pt,
52
+ label_eps_nd,
53
+ load_objective_names,
54
+ load_objectives_and_solutions,
55
+ log_eps_experiment,
56
+ normalize_for_radar,
57
+ parallel_plot_hp,
58
+ resolve_solutions_csv,
59
+ run_eps_experiment,
60
+ )
61
+ from parasolpy.util import (
62
+ convert_cfs_to_af,
63
+ convert_cfs_to_cms,
64
+ convert_cms_to_mcm,
65
+ ensure_dir,
66
+ pivot_timeseries_by_year,
67
+ process_xml,
68
+ read_xml,
69
+ script_local_path,
70
+ )
71
+
72
+ # __all__ controls what is exported when someone does `from parasolpy import *`.
73
+ # It also signals to IDEs and documentation tools which names are part of the
74
+ # intentional public API (as opposed to internal helpers imported for convenience).
75
+ # Names starting with _ are intentionally excluded — they are private implementation details.
76
+ __all__ = [
77
+ # dash_tools
78
+ "build_tradeoff_dash_app",
79
+ "load_dash_inputs",
80
+ # file_processing
81
+ "has_superheader",
82
+ "load_solutions_dataframe",
83
+ "convert_solutions_csv_to_single_header",
84
+ "split_solutions_dataframe",
85
+ "split_solutions_csv",
86
+ # interactive
87
+ "ExitInteractiveSession",
88
+ "load_experiment_epsilons",
89
+ "prompt_epsilons",
90
+ "prompt_experiment_name",
91
+ "prompt_starting_epsilons",
92
+ # ism
93
+ "create_ism_traces",
94
+ # nowak
95
+ "choose_analog_index",
96
+ "sim_multi_trace",
97
+ "sim_single_year",
98
+ # plotting
99
+ "plot_trace_exceedance",
100
+ "plot_trace_fan_chart",
101
+ "plot_trace_heatmap",
102
+ "plot_trace_monthly_seasonality",
103
+ "plot_trace_spaghetti",
104
+ # reservoir
105
+ "sequent_peak",
106
+ # tradeoff
107
+ "append_Kmeans",
108
+ "build_radar_figure",
109
+ "df_to_pt",
110
+ "label_eps_nd",
111
+ "load_objective_names",
112
+ "load_objectives_and_solutions",
113
+ "log_eps_experiment",
114
+ "normalize_for_radar",
115
+ "parallel_plot_hp",
116
+ "resolve_solutions_csv",
117
+ "run_eps_experiment",
118
+ # util
119
+ "convert_cfs_to_af",
120
+ "convert_cfs_to_cms",
121
+ "convert_cms_to_mcm",
122
+ "ensure_dir",
123
+ "pivot_timeseries_by_year",
124
+ "process_xml",
125
+ "read_xml",
126
+ "script_local_path",
127
+ ]
@@ -0,0 +1,57 @@
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ from dash import Dash, Input, Output, dcc, html
4
+
5
+ from parasolpy.tradeoff import load_objectives_and_solutions
6
+
7
+
8
+ def load_dash_inputs(output_folder, config_filename=None, solutions_filename=None):
9
+ """Load objective names and solutions DataFrame for a tradeoff Dash app."""
10
+ objective_names, solutions = load_objectives_and_solutions(
11
+ output_folder,
12
+ config_filename=config_filename,
13
+ solutions_filename=solutions_filename,
14
+ return_paths=False,
15
+ )
16
+
17
+ default_color_column = next((name for name in objective_names if name in solutions.columns), solutions.columns[0])
18
+ return objective_names, solutions, default_color_column
19
+
20
+
21
+ def build_tradeoff_dash_app(solutions,
22
+ default_color_column,
23
+ title="Tradeoff Explorer",
24
+ default_colorscale="viridis"):
25
+ """Create a Dash app for exploring solution tradeoffs via parallel coordinates."""
26
+ if not isinstance(solutions, pd.DataFrame):
27
+ raise TypeError("Input 'solutions' must be a pandas DataFrame.")
28
+ if solutions.empty:
29
+ raise ValueError("Input 'solutions' cannot be empty.")
30
+ if not isinstance(default_color_column, str) or default_color_column not in solutions.columns:
31
+ raise ValueError("Input 'default_color_column' must be a column name in 'solutions'.")
32
+
33
+ colorscales = px.colors.named_colorscales()
34
+ if default_colorscale not in colorscales:
35
+ default_colorscale = "viridis"
36
+
37
+ app = Dash()
38
+ app.layout = [
39
+ html.H1(children=title, style={"textAlign": "center"}),
40
+ dcc.Dropdown(options=list(solutions.columns), value=default_color_column, id="colorcolumn"),
41
+ dcc.Dropdown(options=colorscales, value=default_colorscale, id="colorscale"),
42
+ dcc.Graph(figure={}, id="tradeoff-plot"),
43
+ ]
44
+
45
+ @app.callback(
46
+ Output("tradeoff-plot", "figure"),
47
+ Input("colorcolumn", "value"),
48
+ Input("colorscale", "value"),
49
+ )
50
+ def update_graph(colorcolumn, colorscale):
51
+ return px.parallel_coordinates(
52
+ solutions,
53
+ color=colorcolumn,
54
+ color_continuous_scale=colorscale,
55
+ )
56
+
57
+ return app
@@ -0,0 +1,355 @@
1
+ """File-processing helpers for Borg/RiverWare solution CSVs.
2
+
3
+ This module focuses on common post-processing tasks for solution files,
4
+ including:
5
+ 1) Normalizing two-line "superheader" CSVs to single-header CSVs.
6
+ 2) Splitting decision-variable and objective columns for downstream analysis.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ from pathlib import Path
13
+
14
+ import pandas as pd
15
+
16
+
17
+ _DECISION_GROUP_NAMES = {
18
+ "decision variables",
19
+ "decision variable",
20
+ "decisions",
21
+ }
22
+
23
+ _OBJECTIVE_GROUP_NAMES = {
24
+ "objectives",
25
+ "objective",
26
+ "objective functions",
27
+ }
28
+
29
+
30
+ def _normalize_text(value):
31
+ """Normalize header/group labels for case-insensitive matching."""
32
+ if value is None:
33
+ return ""
34
+ return str(value).strip()
35
+
36
+
37
+ def _is_number(value):
38
+ """Return True when a string can be parsed as a finite float."""
39
+ text = _normalize_text(value)
40
+ if not text:
41
+ return False
42
+ try:
43
+ float(text)
44
+ return True
45
+ except ValueError:
46
+ return False
47
+
48
+
49
+ def _row_is_mostly_numeric(row):
50
+ """Heuristic check used to distinguish header rows from data rows."""
51
+ cells = [_normalize_text(cell) for cell in row if _normalize_text(cell) != ""]
52
+ if not cells:
53
+ return False
54
+ numeric_count = sum(_is_number(cell) for cell in cells)
55
+ return numeric_count / len(cells) >= 0.8
56
+
57
+
58
+ def _read_first_rows(csv_path, n_rows=2):
59
+ """Read the first N rows from a CSV with UTF-8 BOM compatibility."""
60
+ with open(csv_path, "r", encoding="utf-8-sig", newline="") as handle:
61
+ reader = csv.reader(handle)
62
+ rows = []
63
+ for _ in range(n_rows):
64
+ try:
65
+ rows.append(next(reader))
66
+ except StopIteration:
67
+ break
68
+ return rows
69
+
70
+
71
+ def has_superheader(csv_path):
72
+ """Detect whether a solutions CSV likely uses a two-line superheader.
73
+
74
+ A superheader file usually has grouped labels on row 1 (for example,
75
+ "Decision Variables", "Objectives") and the actual column names on row 2.
76
+ """
77
+ path = Path(csv_path)
78
+ if not path.exists() or not path.is_file():
79
+ raise FileNotFoundError(f"CSV file not found: {path}")
80
+
81
+ rows = _read_first_rows(path, n_rows=2)
82
+ if len(rows) < 2:
83
+ return False
84
+
85
+ row1 = [_normalize_text(cell) for cell in rows[0]]
86
+ row2 = [_normalize_text(cell) for cell in rows[1]]
87
+
88
+ if _row_is_mostly_numeric(row2):
89
+ return False
90
+
91
+ group_label_set = {
92
+ "decision variables",
93
+ "objectives",
94
+ "metrics",
95
+ }
96
+ has_known_group_label = any(cell.lower() in group_label_set for cell in row1)
97
+
98
+ first_cell_blank = bool(row1) and row1[0] == ""
99
+ has_any_group_text = any(cell != "" for cell in row1[1:])
100
+
101
+ return has_known_group_label or (first_cell_blank and has_any_group_text)
102
+
103
+
104
+ def _flatten_columns_and_groups(columns):
105
+ """Flatten MultiIndex columns and capture per-column group labels."""
106
+ flattened = []
107
+ column_to_group = {}
108
+ used = {}
109
+ current_group = None
110
+
111
+ for idx, column in enumerate(columns):
112
+ if isinstance(column, tuple):
113
+ top = _normalize_text(column[0])
114
+ bottom = _normalize_text(column[1])
115
+ if top.lower().startswith("unnamed"):
116
+ top = ""
117
+ if bottom.lower().startswith("unnamed"):
118
+ bottom = ""
119
+
120
+ if top:
121
+ current_group = top
122
+ group = current_group
123
+
124
+ name = bottom or top or f"column_{idx + 1}"
125
+ else:
126
+ name = _normalize_text(column) or f"column_{idx + 1}"
127
+ group = None
128
+
129
+ count = used.get(name, 0) + 1
130
+ used[name] = count
131
+ unique_name = name if count == 1 else f"{name}__{count}"
132
+
133
+ flattened.append(unique_name)
134
+ column_to_group[unique_name] = group
135
+
136
+ return flattened, column_to_group
137
+
138
+
139
+ def load_solutions_dataframe(csv_path, superheader="auto", return_metadata=False):
140
+ """Load a solution CSV and normalize headers for downstream processing.
141
+
142
+ Args:
143
+ csv_path: Path to the input CSV.
144
+ superheader: One of "auto", "yes", "no".
145
+ return_metadata: If True, also returns a metadata dict with grouped column info.
146
+
147
+ Returns:
148
+ DataFrame, or (DataFrame, metadata) when return_metadata is True.
149
+ """
150
+ path = Path(csv_path)
151
+ if not path.exists() or not path.is_file():
152
+ raise FileNotFoundError(f"CSV file not found: {path}")
153
+
154
+ if superheader not in {"auto", "yes", "no"}:
155
+ raise ValueError("Input 'superheader' must be one of: 'auto', 'yes', 'no'.")
156
+
157
+ if superheader == "auto":
158
+ has_multi_header = has_superheader(path)
159
+ else:
160
+ has_multi_header = superheader == "yes"
161
+
162
+ if has_multi_header:
163
+ df = pd.read_csv(path, header=[0, 1])
164
+ else:
165
+ df = pd.read_csv(path)
166
+
167
+ if df.empty:
168
+ raise ValueError(f"Solutions file is empty: {path}")
169
+
170
+ flat_columns, column_to_group = _flatten_columns_and_groups(df.columns)
171
+ df.columns = flat_columns
172
+
173
+ groups_to_columns = {}
174
+ for column in df.columns:
175
+ group = column_to_group[column]
176
+ if group is None:
177
+ continue
178
+ groups_to_columns.setdefault(group, []).append(column)
179
+
180
+ metadata = {
181
+ "has_superheader": has_multi_header,
182
+ "column_to_group": column_to_group,
183
+ "groups_to_columns": groups_to_columns,
184
+ }
185
+
186
+ if return_metadata:
187
+ return df, metadata
188
+ return df
189
+
190
+
191
+ def convert_solutions_csv_to_single_header(input_csv, output_csv=None, superheader="auto", index=False):
192
+ """Convert a solutions CSV to a normalized single-header CSV file.
193
+
194
+ Args:
195
+ input_csv: Source CSV path.
196
+ output_csv: Destination path. If None, writes next to input using
197
+ '<stem>.single_header.csv'.
198
+ superheader: One of "auto", "yes", "no".
199
+ index: Whether to write DataFrame index to output CSV.
200
+
201
+ Returns:
202
+ pathlib.Path to the written CSV.
203
+ """
204
+ input_path = Path(input_csv)
205
+ if output_csv is None:
206
+ output_path = input_path.with_name(f"{input_path.stem}.single_header.csv")
207
+ else:
208
+ output_path = Path(output_csv)
209
+
210
+ solutions = load_solutions_dataframe(input_path, superheader=superheader, return_metadata=False)
211
+ solutions.to_csv(output_path, index=index)
212
+ return output_path
213
+
214
+
215
+ def split_solutions_dataframe(
216
+ solutions,
217
+ metadata=None,
218
+ decision_columns=None,
219
+ objective_columns=None,
220
+ id_columns=("Solution", "Solution ID"),
221
+ include_id_columns=True,
222
+ ):
223
+ """Split a solutions DataFrame into decision and objective DataFrames.
224
+
225
+ Args:
226
+ solutions: Input DataFrame.
227
+ metadata: Metadata returned from load_solutions_dataframe(..., return_metadata=True).
228
+ Used to auto-detect grouped columns from a superheader.
229
+ decision_columns: Explicit decision-variable columns. If None, inferred from metadata.
230
+ objective_columns: Explicit objective columns. If None, inferred from metadata.
231
+ id_columns: Candidate ID columns to preserve in each output DataFrame.
232
+ include_id_columns: Whether to keep detected ID columns in outputs.
233
+
234
+ Returns:
235
+ dict with keys 'decisions', 'objectives', 'decision_columns', 'objective_columns'.
236
+ """
237
+ if not isinstance(solutions, pd.DataFrame):
238
+ raise TypeError("Input 'solutions' must be a pandas DataFrame.")
239
+
240
+ if decision_columns is not None:
241
+ if not isinstance(decision_columns, list) or not all(isinstance(c, str) for c in decision_columns):
242
+ raise TypeError("Input 'decision_columns' must be a list of strings or None.")
243
+ if objective_columns is not None:
244
+ if not isinstance(objective_columns, list) or not all(isinstance(c, str) for c in objective_columns):
245
+ raise TypeError("Input 'objective_columns' must be a list of strings or None.")
246
+
247
+ if decision_columns is None or objective_columns is None:
248
+ groups_to_columns = {}
249
+ if metadata is not None:
250
+ groups_to_columns = metadata.get("groups_to_columns", {})
251
+
252
+ if decision_columns is None:
253
+ decision_columns = []
254
+ for group, cols in groups_to_columns.items():
255
+ if _normalize_text(group).lower() in _DECISION_GROUP_NAMES:
256
+ decision_columns.extend(cols)
257
+
258
+ if objective_columns is None:
259
+ objective_columns = []
260
+ for group, cols in groups_to_columns.items():
261
+ if _normalize_text(group).lower() in _OBJECTIVE_GROUP_NAMES:
262
+ objective_columns.extend(cols)
263
+
264
+ missing_decision = [c for c in decision_columns if c not in solutions.columns]
265
+ if missing_decision:
266
+ raise ValueError(f"Decision columns missing from DataFrame: {missing_decision}")
267
+
268
+ missing_objective = [c for c in objective_columns if c not in solutions.columns]
269
+ if missing_objective:
270
+ raise ValueError(f"Objective columns missing from DataFrame: {missing_objective}")
271
+
272
+ if not decision_columns and not objective_columns:
273
+ raise ValueError(
274
+ "Could not infer decision/objective columns. Provide explicit column lists "
275
+ "or load a CSV with a superheader for automatic grouping."
276
+ )
277
+
278
+ id_cols_present = [c for c in id_columns if c in solutions.columns]
279
+ prefix = id_cols_present if include_id_columns else []
280
+
281
+ decisions_df = solutions[prefix + decision_columns].copy()
282
+ objectives_df = solutions[prefix + objective_columns].copy()
283
+
284
+ return {
285
+ "decisions": decisions_df,
286
+ "objectives": objectives_df,
287
+ "decision_columns": decision_columns,
288
+ "objective_columns": objective_columns,
289
+ }
290
+
291
+
292
+ def split_solutions_csv(
293
+ input_csv,
294
+ decisions_csv=None,
295
+ objectives_csv=None,
296
+ superheader="auto",
297
+ decision_columns=None,
298
+ objective_columns=None,
299
+ id_columns=("Solution", "Solution ID"),
300
+ include_id_columns=True,
301
+ index=False,
302
+ ):
303
+ """Split a solutions CSV and write decisions-only and objectives-only CSVs.
304
+
305
+ Args:
306
+ input_csv: Source solutions CSV path.
307
+ decisions_csv: Destination for decision-variable subset.
308
+ Defaults to '<stem>.decisions.csv'.
309
+ objectives_csv: Destination for objective subset.
310
+ Defaults to '<stem>.objectives.csv'.
311
+ superheader: One of "auto", "yes", "no".
312
+ decision_columns: Explicit decision columns, optional.
313
+ objective_columns: Explicit objective columns, optional.
314
+ id_columns: Candidate ID columns to keep.
315
+ include_id_columns: Include ID columns in both outputs.
316
+ index: Whether to write DataFrame index to output CSVs.
317
+
318
+ Returns:
319
+ dict with output paths and detected column groupings.
320
+ """
321
+ input_path = Path(input_csv)
322
+ if decisions_csv is None:
323
+ decisions_path = input_path.with_name(f"{input_path.stem}.decisions.csv")
324
+ else:
325
+ decisions_path = Path(decisions_csv)
326
+
327
+ if objectives_csv is None:
328
+ objectives_path = input_path.with_name(f"{input_path.stem}.objectives.csv")
329
+ else:
330
+ objectives_path = Path(objectives_csv)
331
+
332
+ solutions, metadata = load_solutions_dataframe(
333
+ input_path,
334
+ superheader=superheader,
335
+ return_metadata=True,
336
+ )
337
+
338
+ split = split_solutions_dataframe(
339
+ solutions,
340
+ metadata=metadata,
341
+ decision_columns=decision_columns,
342
+ objective_columns=objective_columns,
343
+ id_columns=id_columns,
344
+ include_id_columns=include_id_columns,
345
+ )
346
+
347
+ split["decisions"].to_csv(decisions_path, index=index)
348
+ split["objectives"].to_csv(objectives_path, index=index)
349
+
350
+ return {
351
+ "decisions_path": decisions_path,
352
+ "objectives_path": objectives_path,
353
+ "decision_columns": split["decision_columns"],
354
+ "objective_columns": split["objective_columns"],
355
+ }