cornflow-client 1.3.4rc2__tar.gz → 1.3.5__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.
- {cornflow_client-1.3.4rc2/cornflow_client.egg-info → cornflow_client-1.3.5}/PKG-INFO +2 -1
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/airflow/dag_utilities.py +55 -5
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/constants.py +8 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/application.py +87 -16
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/experiment.py +155 -11
- cornflow_client-1.3.5/cornflow_client/core/tools.py +243 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/cornflow_client.py +3 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/raw_cornflow_client.py +22 -3
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/integration/test_cornflow_integration.py +1 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/integration/test_raw_cornflow_integration.py +1 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_abc.py +6 -4
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_core.py +129 -0
- cornflow_client-1.3.5/cornflow_client/tests/unit/test_dag_utilities.py +94 -0
- cornflow_client-1.3.5/cornflow_client/tests/unit/test_execution_files.py +535 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5/cornflow_client.egg-info}/PKG-INFO +2 -1
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client.egg-info/SOURCES.txt +1 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client.egg-info/requires.txt +1 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/requirements.txt +1 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/setup.py +1 -1
- cornflow_client-1.3.4rc2/cornflow_client/core/tools.py +0 -31
- cornflow_client-1.3.4rc2/cornflow_client/tests/unit/test_dag_utilities.py +0 -40
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/LICENSE +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/MANIFEST.in +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/README.rst +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/airflow/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/airflow/api.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/instance.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/instance_solution.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/read_tools.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/core/solution.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/empty_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/pulp_json_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/schema_validator.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/solver_config.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/data/vrp_solution_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/databricks/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/databricks/api.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/schema/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/schema/dictSchema.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/schema/dict_functions.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/schema/manager.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/schema/tools.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/const.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/data_input_bad.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/data_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/endpoints_access.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/endpoints_methods.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/gc_input.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/gc_output.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/graph_coloring_input.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/graph_coloring_output.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/hk_data_input.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/hk_data_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/hk_solution_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/instance-hackathon2.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/name_problem_schema.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/pulp_example_data.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/schema_with_fk.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/schema_without_fk.json +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/test_mps.mps +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/xl_with_access.xlsx +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/xl_with_fk.xlsx +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/xl_with_methods.xlsx +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/data/xl_without_fk.xlsx +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/integration/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/integration/test_airflow_integration.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/__init__.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_instance_solution_methods.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_schema_from_excel.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_schema_manager.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client/tests/unit/test_varnames.py +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client.egg-info/dependency_links.txt +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/cornflow_client.egg-info/top_level.txt +0 -0
- {cornflow_client-1.3.4rc2 → cornflow_client-1.3.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cornflow-client
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Client to connect to a cornflow server
|
|
5
5
|
Home-page: https://github.com/baobabsoluciones/cornflow
|
|
6
6
|
Author: baobab soluciones
|
|
@@ -16,6 +16,7 @@ Requires-Dist: requests==2.32.4
|
|
|
16
16
|
Requires-Dist: genson==1.2.2
|
|
17
17
|
Requires-Dist: jsonschema==4.21.1
|
|
18
18
|
Requires-Dist: marshmallow==3.26.2
|
|
19
|
+
Requires-Dist: openpyxl==3.1.5
|
|
19
20
|
Requires-Dist: pytups==0.86.2
|
|
20
21
|
Requires-Dist: ortools==9.8.3296
|
|
21
22
|
Requires-Dist: PuLP==2.9.0
|
|
@@ -8,6 +8,10 @@ from datetime import datetime, timedelta
|
|
|
8
8
|
from urllib.parse import urlparse, urljoin
|
|
9
9
|
|
|
10
10
|
from cornflow_client import CornFlow, CornFlowApiError
|
|
11
|
+
from cornflow_client.constants import (
|
|
12
|
+
EXECUTION_FILES_STATUS_ERROR,
|
|
13
|
+
EXECUTION_FILES_STATUS_NOT_GENERATED,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
# TODO: convert everything to an object that encapsulates everything
|
|
13
17
|
# to make it clear and avoid all the arguments.
|
|
@@ -147,6 +151,22 @@ def try_to_write_solution(client, exec_id, payload):
|
|
|
147
151
|
raise AirflowDagException("The writing of the instance checks failed")
|
|
148
152
|
|
|
149
153
|
|
|
154
|
+
def try_to_write_file(client, exec_id, payload):
|
|
155
|
+
"""
|
|
156
|
+
Tries to write the generated execution files into cornflow.
|
|
157
|
+
If it fails, tries to write again that it failed.
|
|
158
|
+
If it fails at least once: raises an exception.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
client.write_execution_files(execution_id=exec_id, **payload)
|
|
162
|
+
except CornFlowApiError:
|
|
163
|
+
client.write_execution_files(
|
|
164
|
+
execution_id=exec_id, execution_files_status=EXECUTION_FILES_STATUS_ERROR
|
|
165
|
+
)
|
|
166
|
+
# attempt to update the execution with a failed status.
|
|
167
|
+
raise AirflowDagException("The writing of the files failed")
|
|
168
|
+
|
|
169
|
+
|
|
150
170
|
def get_schema(dag_name):
|
|
151
171
|
# TODO: check if in use
|
|
152
172
|
_file = os.path.join(os.path.dirname(__file__), f"{dag_name}_output.json")
|
|
@@ -185,9 +205,16 @@ def cf_solve(fun, dag_name, secrets, **kwargs):
|
|
|
185
205
|
config = execution_data["config"]
|
|
186
206
|
inst_id = execution_data["id"]
|
|
187
207
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
(
|
|
209
|
+
solution,
|
|
210
|
+
sol_checks,
|
|
211
|
+
inst_checks,
|
|
212
|
+
kpis,
|
|
213
|
+
execution_zip_file,
|
|
214
|
+
zip_file_status,
|
|
215
|
+
log,
|
|
216
|
+
log_json,
|
|
217
|
+
) = fun(data, config, solution_data)
|
|
191
218
|
|
|
192
219
|
# We connect again to cornflow in case that more than 24 hours
|
|
193
220
|
# have passed from the first time we connect
|
|
@@ -201,11 +228,20 @@ def cf_solve(fun, dag_name, secrets, **kwargs):
|
|
|
201
228
|
inst_checks=inst_checks,
|
|
202
229
|
inst_id=inst_id,
|
|
203
230
|
)
|
|
231
|
+
|
|
204
232
|
if not solution:
|
|
205
233
|
# No solution found: we just send everything to cornflow.
|
|
206
234
|
if config.get("msg", True):
|
|
207
235
|
print("No solution found: we save what we have.")
|
|
208
236
|
try_to_write_solution(client, exec_id, payload)
|
|
237
|
+
|
|
238
|
+
if zip_file_status != EXECUTION_FILES_STATUS_NOT_GENERATED:
|
|
239
|
+
file_payload = {
|
|
240
|
+
"execution_files_status": zip_file_status,
|
|
241
|
+
"execution_file": execution_zip_file,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try_to_write_file(client, exec_id, file_payload)
|
|
209
245
|
return "Solution was not saved"
|
|
210
246
|
# There is a solution:
|
|
211
247
|
# we first need to validate the schema.
|
|
@@ -223,6 +259,14 @@ def cf_solve(fun, dag_name, secrets, **kwargs):
|
|
|
223
259
|
|
|
224
260
|
try_to_write_solution(client, exec_id, payload)
|
|
225
261
|
|
|
262
|
+
if zip_file_status != EXECUTION_FILES_STATUS_NOT_GENERATED:
|
|
263
|
+
file_payload = {
|
|
264
|
+
"execution_files_status": zip_file_status,
|
|
265
|
+
"execution_file": execution_zip_file,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try_to_write_file(client, exec_id, file_payload)
|
|
269
|
+
|
|
226
270
|
# The validation went correctly: can save the solution without problem
|
|
227
271
|
return "Solution saved"
|
|
228
272
|
|
|
@@ -268,8 +312,8 @@ def cf_check_generate_kpis(fun_check_generate_kpis, dag_name, secrets, **kwargs)
|
|
|
268
312
|
inst_id = execution_data["id"]
|
|
269
313
|
solution_data = execution_data["solution_data"]
|
|
270
314
|
|
|
271
|
-
inst_checks, sol_checks, kpis, log_json =
|
|
272
|
-
instance_data, solution_data
|
|
315
|
+
inst_checks, sol_checks, kpis, execution_zip_file, zip_file_status, log_json = (
|
|
316
|
+
fun_check_generate_kpis(instance_data, solution_data)
|
|
273
317
|
)
|
|
274
318
|
|
|
275
319
|
if config.get("checks_and_kpis_only"):
|
|
@@ -299,6 +343,12 @@ def cf_check_generate_kpis(fun_check_generate_kpis, dag_name, secrets, **kwargs)
|
|
|
299
343
|
payload["kpis"] = kpis
|
|
300
344
|
|
|
301
345
|
try_to_write_solution(client, exec_id, payload)
|
|
346
|
+
if zip_file_status != EXECUTION_FILES_STATUS_NOT_GENERATED:
|
|
347
|
+
file_payload = {
|
|
348
|
+
"execution_files_status": zip_file_status,
|
|
349
|
+
"execution_file": execution_zip_file,
|
|
350
|
+
}
|
|
351
|
+
try_to_write_file(client, exec_id, file_payload)
|
|
302
352
|
|
|
303
353
|
case_id = kwargs["dag_run"].conf.get("case_id")
|
|
304
354
|
if case_id is not None:
|
|
@@ -27,6 +27,7 @@ SOLUTION_SCHEMA = "solution"
|
|
|
27
27
|
CONFIG_SCHEMA = "config"
|
|
28
28
|
INSTANCE_CHECKS_SCHEMA = "instance_checks"
|
|
29
29
|
SOLUTION_CHECKS_SCHEMA = "solution_checks"
|
|
30
|
+
KPIS_SCHEMA = "kpis"
|
|
30
31
|
|
|
31
32
|
# why it stopped
|
|
32
33
|
STATUS_NOT_SOLVED = 0
|
|
@@ -60,6 +61,13 @@ STATUS_CONV = {
|
|
|
60
61
|
SOLUTION_STATUS_INFEASIBLE = 0
|
|
61
62
|
SOLUTION_STATUS_FEASIBLE = 2
|
|
62
63
|
|
|
64
|
+
# are there execution files?
|
|
65
|
+
EXECUTION_FILES_STATUS_NOT_GENERATED = 0
|
|
66
|
+
EXECUTION_FILES_STATUS_ERROR = -1
|
|
67
|
+
EXECUTION_FILES_STATUS_DELETED = -2
|
|
68
|
+
EXECUTION_FILES_STATUS_NOT_UP_TO_DATE = -3
|
|
69
|
+
EXECUTION_FILES_STATUS_OK = 1
|
|
70
|
+
|
|
63
71
|
PYOMO_STOP_MAPPING = {
|
|
64
72
|
"unbounded": STATUS_UNBOUNDED,
|
|
65
73
|
"infeasible": STATUS_INFEASIBLE,
|
|
@@ -21,6 +21,7 @@ from cornflow_client.constants import (
|
|
|
21
21
|
BadConfiguration,
|
|
22
22
|
BadSolution,
|
|
23
23
|
BadInstance,
|
|
24
|
+
EXECUTION_FILES_STATUS_NOT_GENERATED,
|
|
24
25
|
)
|
|
25
26
|
from .experiment import ExperimentCore
|
|
26
27
|
|
|
@@ -186,7 +187,8 @@ class ApplicationCore(ABC):
|
|
|
186
187
|
)
|
|
187
188
|
return inst, sol
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _check_instance_errors(inst):
|
|
190
192
|
"""Performs instance data checks and identifies critical errors."""
|
|
191
193
|
instance_checks = SuperDict(inst.data_checks())
|
|
192
194
|
warnings_tables = (
|
|
@@ -209,6 +211,31 @@ class ApplicationCore(ABC):
|
|
|
209
211
|
|
|
210
212
|
return instance_checks, has_errors
|
|
211
213
|
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _check_solution_errors(exp):
|
|
216
|
+
"""Performs solution checks and identifies critical errors."""
|
|
217
|
+
solution_checks = SuperDict(exp.data_checks())
|
|
218
|
+
warnings_tables = (
|
|
219
|
+
SuperDict.from_dict(exp.schema_checks)
|
|
220
|
+
.get("properties", SuperDict())
|
|
221
|
+
.vfilter(lambda v: v.get("is_warning", False))
|
|
222
|
+
.keys()
|
|
223
|
+
)
|
|
224
|
+
solution_errors = solution_checks.kfilter(lambda k: k not in warnings_tables)
|
|
225
|
+
|
|
226
|
+
has_errors = False
|
|
227
|
+
for error_table in solution_errors.values():
|
|
228
|
+
# Check if the error table/value indicates an error
|
|
229
|
+
if isinstance(error_table, (list, dict)) and len(error_table) > 0:
|
|
230
|
+
has_errors = True
|
|
231
|
+
break
|
|
232
|
+
# Check for non-empty scalar values considered errors
|
|
233
|
+
elif error_table and not isinstance(error_table, (list, dict)):
|
|
234
|
+
has_errors = True
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
return solution_checks, has_errors
|
|
238
|
+
|
|
212
239
|
def _execute_solver(self, inst, sol, config):
|
|
213
240
|
"""Instantiates and runs the solver, returning output and timing."""
|
|
214
241
|
solver_name = config.get("solver", self.get_default_solver_name())
|
|
@@ -285,16 +312,16 @@ class ApplicationCore(ABC):
|
|
|
285
312
|
# and the solver implements data_checks
|
|
286
313
|
# Checks for non-None and non-empty dict
|
|
287
314
|
if final_sol_dict:
|
|
288
|
-
solution_checks =
|
|
315
|
+
solution_checks, solution_has_errors = self._check_solution_errors(algo)
|
|
289
316
|
|
|
290
317
|
# Generate KPIs only if there are no solution checks
|
|
291
318
|
# (i.e., no issues found in the solution)
|
|
292
|
-
if not
|
|
319
|
+
if not solution_has_errors:
|
|
293
320
|
kpis = algo.get_kpis()
|
|
294
321
|
kpis_checks = algo.kpis_checks()
|
|
295
322
|
solution_checks.update(kpis_checks)
|
|
296
323
|
|
|
297
|
-
return final_sol_dict, solution_checks, kpis
|
|
324
|
+
return final_sol_dict, solution_checks, solution_has_errors, kpis
|
|
298
325
|
|
|
299
326
|
def _format_log_and_solution(self, algo, output, solver_name, elapsed_time):
|
|
300
327
|
"""Formats logs, validates and checks the solution, generates the kpis"""
|
|
@@ -313,14 +340,22 @@ class ApplicationCore(ABC):
|
|
|
313
340
|
# 4. Validate and check solution if feasible/optimal
|
|
314
341
|
final_sol_dict = None
|
|
315
342
|
solution_checks = None
|
|
343
|
+
solution_has_errors = None
|
|
316
344
|
kpis = None
|
|
317
345
|
if final_sol_code > 0:
|
|
318
346
|
# Raises BadSolution if schema validation fails
|
|
319
|
-
final_sol_dict, solution_checks, kpis =
|
|
320
|
-
algo
|
|
347
|
+
final_sol_dict, solution_checks, solution_has_errors, kpis = (
|
|
348
|
+
self._validate_and_check_solution(algo)
|
|
321
349
|
)
|
|
322
350
|
|
|
323
|
-
return
|
|
351
|
+
return (
|
|
352
|
+
final_sol_dict,
|
|
353
|
+
solution_checks,
|
|
354
|
+
solution_has_errors,
|
|
355
|
+
kpis,
|
|
356
|
+
log_txt,
|
|
357
|
+
log_json,
|
|
358
|
+
)
|
|
324
359
|
|
|
325
360
|
def solve(
|
|
326
361
|
self, data: dict, config: dict, solution_data: dict = None
|
|
@@ -345,9 +380,9 @@ class ApplicationCore(ABC):
|
|
|
345
380
|
inst, sol_initial = self._prepare_instance_and_solution(data, solution_data)
|
|
346
381
|
|
|
347
382
|
# 3. Check instance for critical errors
|
|
348
|
-
instance_checks,
|
|
383
|
+
instance_checks, instance_has_errors = self._check_instance_errors(inst)
|
|
349
384
|
|
|
350
|
-
if
|
|
385
|
+
if instance_has_errors:
|
|
351
386
|
# Return early if instance has critical errors
|
|
352
387
|
solver_name = config.get("solver", self.get_default_solver_name())
|
|
353
388
|
log_json = dict(
|
|
@@ -358,7 +393,16 @@ class ApplicationCore(ABC):
|
|
|
358
393
|
sol_code=SOLUTION_STATUS_INFEASIBLE,
|
|
359
394
|
)
|
|
360
395
|
# Ensure solution dict is empty, not None, consistent with successful returns
|
|
361
|
-
return
|
|
396
|
+
return (
|
|
397
|
+
dict(),
|
|
398
|
+
None,
|
|
399
|
+
instance_checks,
|
|
400
|
+
None,
|
|
401
|
+
None,
|
|
402
|
+
EXECUTION_FILES_STATUS_NOT_GENERATED,
|
|
403
|
+
"",
|
|
404
|
+
log_json,
|
|
405
|
+
)
|
|
362
406
|
|
|
363
407
|
# 4. Execute solver
|
|
364
408
|
algo, output, solver_name, elapsed_time = self._execute_solver(
|
|
@@ -366,15 +410,28 @@ class ApplicationCore(ABC):
|
|
|
366
410
|
)
|
|
367
411
|
|
|
368
412
|
# 5. Format log and process solution
|
|
369
|
-
sol_final, solution_checks, kpis, log_txt, log_json = (
|
|
413
|
+
sol_final, solution_checks, solution_has_errors, kpis, log_txt, log_json = (
|
|
370
414
|
self._format_log_and_solution(algo, output, solver_name, elapsed_time)
|
|
371
415
|
)
|
|
372
416
|
|
|
417
|
+
execution_zip_file, zip_file_status = algo._get_zip_file(
|
|
418
|
+
instance_checks, instance_has_errors, solution_checks, solution_has_errors
|
|
419
|
+
)
|
|
420
|
+
|
|
373
421
|
# Ensure solution is an empty dict if None for type consistency
|
|
374
422
|
if sol_final is None:
|
|
375
423
|
sol_final = dict()
|
|
376
424
|
|
|
377
|
-
return
|
|
425
|
+
return (
|
|
426
|
+
sol_final,
|
|
427
|
+
solution_checks,
|
|
428
|
+
instance_checks,
|
|
429
|
+
kpis,
|
|
430
|
+
execution_zip_file,
|
|
431
|
+
zip_file_status,
|
|
432
|
+
log_txt,
|
|
433
|
+
log_json,
|
|
434
|
+
)
|
|
378
435
|
|
|
379
436
|
def check_generate_kpis(
|
|
380
437
|
self, instance_data: dict, solution_data: dict = None
|
|
@@ -391,27 +448,33 @@ class ApplicationCore(ABC):
|
|
|
391
448
|
raise NoSolverException("No solver is available")
|
|
392
449
|
inst = self.instance.from_dict(instance_data)
|
|
393
450
|
|
|
394
|
-
instance_checks =
|
|
451
|
+
instance_checks, instance_has_errors = self._check_instance_errors(inst)
|
|
395
452
|
|
|
453
|
+
solution_has_errors = None
|
|
396
454
|
if solution_data is not None:
|
|
397
455
|
sol = self.solution.from_dict(solution_data)
|
|
398
456
|
algo = solver_class(inst, sol)
|
|
399
457
|
start = timer()
|
|
400
|
-
solution_checks =
|
|
458
|
+
solution_checks, solution_has_errors = self._check_solution_errors(algo)
|
|
401
459
|
kpis = None
|
|
402
460
|
|
|
403
461
|
# Generate KPIs only if there are no solution checks
|
|
404
462
|
# (i.e., no issues found in the solution)
|
|
405
463
|
# Then, run the checks on the KPIs.
|
|
406
|
-
if not
|
|
464
|
+
if not instance_has_errors and not solution_has_errors:
|
|
407
465
|
kpis = algo.get_kpis()
|
|
408
466
|
kpis_checks = algo.kpis_checks()
|
|
409
467
|
solution_checks.update(kpis_checks)
|
|
410
468
|
else:
|
|
469
|
+
algo = solver_class(inst, None)
|
|
411
470
|
start = timer()
|
|
412
471
|
solution_checks = None
|
|
413
472
|
kpis = None
|
|
414
473
|
|
|
474
|
+
execution_zip_file, zip_file_status = algo._get_zip_file(
|
|
475
|
+
instance_checks, instance_has_errors, solution_checks, solution_has_errors
|
|
476
|
+
)
|
|
477
|
+
|
|
415
478
|
log = dict(
|
|
416
479
|
time=timer() - start,
|
|
417
480
|
solver=solver,
|
|
@@ -420,7 +483,14 @@ class ApplicationCore(ABC):
|
|
|
420
483
|
sol_code=SOLUTION_STATUS_FEASIBLE,
|
|
421
484
|
)
|
|
422
485
|
|
|
423
|
-
return
|
|
486
|
+
return (
|
|
487
|
+
instance_checks,
|
|
488
|
+
solution_checks,
|
|
489
|
+
kpis,
|
|
490
|
+
execution_zip_file,
|
|
491
|
+
zip_file_status,
|
|
492
|
+
log,
|
|
493
|
+
)
|
|
424
494
|
|
|
425
495
|
def get_solver(self, name: str = "default") -> Union[Type[ExperimentCore], None]:
|
|
426
496
|
"""
|
|
@@ -450,4 +520,5 @@ class ApplicationCore(ABC):
|
|
|
450
520
|
config=self.schema,
|
|
451
521
|
instance_checks=self.instance.schema_checks,
|
|
452
522
|
solution_checks=list(self.solvers.values())[0].schema_checks,
|
|
523
|
+
kpis=list(self.solvers.values())[0].schema_kpis,
|
|
453
524
|
)
|
|
@@ -3,20 +3,28 @@ Base code for the experiment template.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import List, Dict, Union, Tuple
|
|
7
|
-
import logging as log
|
|
8
|
-
|
|
9
6
|
from jsonschema import Draft7Validator
|
|
7
|
+
from typing import List, Dict, Union, Tuple, Optional
|
|
8
|
+
import io
|
|
9
|
+
import logging as log
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import traceback
|
|
13
|
+
import zipfile
|
|
10
14
|
|
|
11
15
|
from cornflow_client.constants import (
|
|
12
16
|
PARAMETER_SOLVER_TRANSLATING_MAPPING,
|
|
13
17
|
SOLVER_CONVERTER,
|
|
14
18
|
BadSolutionChecks,
|
|
15
19
|
BadKPIs,
|
|
20
|
+
EXECUTION_FILES_STATUS_NOT_GENERATED,
|
|
21
|
+
EXECUTION_FILES_STATUS_ERROR,
|
|
22
|
+
EXECUTION_FILES_STATUS_OK,
|
|
16
23
|
)
|
|
17
24
|
from .instance import InstanceCore
|
|
18
25
|
from .instance_solution import CheckCore
|
|
19
26
|
from .solution import SolutionCore
|
|
27
|
+
from .tools import to_excel_memory_file
|
|
20
28
|
|
|
21
29
|
|
|
22
30
|
class ExperimentCore(CheckCore, ABC):
|
|
@@ -24,6 +32,8 @@ class ExperimentCore(CheckCore, ABC):
|
|
|
24
32
|
The solver template.
|
|
25
33
|
"""
|
|
26
34
|
|
|
35
|
+
schema_kpis = {}
|
|
36
|
+
|
|
27
37
|
def __init__(
|
|
28
38
|
self,
|
|
29
39
|
instance: InstanceCore,
|
|
@@ -191,14 +201,6 @@ class ExperimentCore(CheckCore, ABC):
|
|
|
191
201
|
kpis = {k: v for k, v in kpis.items() if v is not None and len(v)}
|
|
192
202
|
return kpis
|
|
193
203
|
|
|
194
|
-
@property
|
|
195
|
-
def schema_kpis(self) -> dict:
|
|
196
|
-
"""
|
|
197
|
-
A dictionary representation of the json-schema for the dictionary returned by
|
|
198
|
-
the method ExperimentCore.generate_kpis()
|
|
199
|
-
"""
|
|
200
|
-
return {}
|
|
201
|
-
|
|
202
204
|
def check_kpis(self) -> dict:
|
|
203
205
|
"""
|
|
204
206
|
Method that checks the solution with the KPIs. By default, it does not perform any check.
|
|
@@ -218,6 +220,148 @@ class ExperimentCore(CheckCore, ABC):
|
|
|
218
220
|
"""
|
|
219
221
|
return self.kpis
|
|
220
222
|
|
|
223
|
+
def generate_output_files(
|
|
224
|
+
self, instance_checks, instance_has_errors, solution_checks, solution_has_errors
|
|
225
|
+
) -> Optional[Dict[str, Union[str, io.BytesIO]]]:
|
|
226
|
+
"""
|
|
227
|
+
Method that generates the output files.
|
|
228
|
+
This method can be overriden by the user.
|
|
229
|
+
:return: a dictionary, where:
|
|
230
|
+
- the keys are the names of the files or directories
|
|
231
|
+
- the values are either:
|
|
232
|
+
* an io.BytesIO instance
|
|
233
|
+
* an io.StringIO instance
|
|
234
|
+
* a string representing an absolute path to a file or directory.
|
|
235
|
+
If the values are paths, the files or directories will be deleted after generating the zip file.
|
|
236
|
+
"""
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _clean_output_files(output_files):
|
|
241
|
+
"""
|
|
242
|
+
Removes output files after generating the zip file.
|
|
243
|
+
"""
|
|
244
|
+
for _, item in output_files.items():
|
|
245
|
+
try:
|
|
246
|
+
if not isinstance(item, str):
|
|
247
|
+
# Item is a bytesIO or stringIO object
|
|
248
|
+
continue
|
|
249
|
+
if not os.path.exists(item):
|
|
250
|
+
# Path was not found
|
|
251
|
+
continue
|
|
252
|
+
if os.path.isdir(item):
|
|
253
|
+
# Path is a directory
|
|
254
|
+
shutil.rmtree(item)
|
|
255
|
+
else:
|
|
256
|
+
# Path is a file
|
|
257
|
+
os.remove(item)
|
|
258
|
+
except:
|
|
259
|
+
print(f"File/Directory {item} could not be deleted")
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
def _get_default_output_files(self, instance_checks, solution_checks):
|
|
263
|
+
"""
|
|
264
|
+
Method that generates the default output files:
|
|
265
|
+
- Instance data Excel
|
|
266
|
+
- Solution data Excel
|
|
267
|
+
- Checks Excel
|
|
268
|
+
- Solution checks Excel
|
|
269
|
+
- KPIs Excel
|
|
270
|
+
"""
|
|
271
|
+
default_files = {}
|
|
272
|
+
for excel_name, data in [
|
|
273
|
+
("instance", self.instance.to_dict()),
|
|
274
|
+
("solution", self.solution.to_dict()),
|
|
275
|
+
("checks", instance_checks),
|
|
276
|
+
("solution_checks", solution_checks),
|
|
277
|
+
("kpis", self.kpis),
|
|
278
|
+
]:
|
|
279
|
+
if data is None:
|
|
280
|
+
continue
|
|
281
|
+
data = {k: v for k, v in data.items() if len(v)}
|
|
282
|
+
if not len(data):
|
|
283
|
+
continue
|
|
284
|
+
default_files[f"{excel_name}.xlsx"] = to_excel_memory_file(data)
|
|
285
|
+
return default_files
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def _add_folder_to_zip(cls, zip_file, dir_path_in_zip, dir_path):
|
|
289
|
+
"""
|
|
290
|
+
Recursively add the folder contents to the zip file.
|
|
291
|
+
"""
|
|
292
|
+
for item in os.listdir(dir_path):
|
|
293
|
+
full_path = os.path.join(dir_path, item)
|
|
294
|
+
if os.path.isfile(full_path):
|
|
295
|
+
# File
|
|
296
|
+
file_path_in_zip = os.path.join(dir_path_in_zip, item)
|
|
297
|
+
zip_file.write(full_path, file_path_in_zip)
|
|
298
|
+
else:
|
|
299
|
+
# Subdirectory
|
|
300
|
+
subdirectory_path = os.path.join(dir_path, item)
|
|
301
|
+
subdir_path_in_zip = os.path.join(dir_path_in_zip, item)
|
|
302
|
+
cls._add_folder_to_zip(zip_file, subdir_path_in_zip, subdirectory_path)
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def _generate_zip_file(cls, output_items):
|
|
306
|
+
"""
|
|
307
|
+
Method that generates a zip file for the execution files.
|
|
308
|
+
:return: a zip file as a BytesIO object.
|
|
309
|
+
"""
|
|
310
|
+
memory_file = io.BytesIO()
|
|
311
|
+
|
|
312
|
+
with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
313
|
+
for item_path_in_zip, item in output_items.items():
|
|
314
|
+
if item_path_in_zip.startswith("/") or item_path_in_zip.startswith(
|
|
315
|
+
"\\"
|
|
316
|
+
):
|
|
317
|
+
item_path_in_zip = item_path_in_zip[1:]
|
|
318
|
+
if isinstance(item, str) and os.path.isdir(item):
|
|
319
|
+
# Directory path
|
|
320
|
+
cls._add_folder_to_zip(zf, item_path_in_zip, item)
|
|
321
|
+
elif isinstance(item, str):
|
|
322
|
+
# File path
|
|
323
|
+
zf.write(item, item_path_in_zip)
|
|
324
|
+
elif isinstance(item, io.BytesIO) or isinstance(item, io.StringIO):
|
|
325
|
+
# File stream
|
|
326
|
+
zf.writestr(item_path_in_zip, item.getvalue())
|
|
327
|
+
else:
|
|
328
|
+
print(item_path_in_zip, item)
|
|
329
|
+
raise Exception(
|
|
330
|
+
"Only paths, BytesIO and StringIO objects are accepted for output files"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
memory_file.seek(0)
|
|
334
|
+
return memory_file
|
|
335
|
+
|
|
336
|
+
def _get_zip_file(
|
|
337
|
+
self, instance_checks, instance_has_errors, solution_checks, solution_has_errors
|
|
338
|
+
):
|
|
339
|
+
"""
|
|
340
|
+
Method that generates a zip file for the execution files.
|
|
341
|
+
:return: a zip file, and a status indicating whether the generation went correctly.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
output_files = self.generate_output_files(
|
|
345
|
+
instance_checks,
|
|
346
|
+
instance_has_errors,
|
|
347
|
+
solution_checks,
|
|
348
|
+
solution_has_errors,
|
|
349
|
+
)
|
|
350
|
+
if output_files is None:
|
|
351
|
+
return None, EXECUTION_FILES_STATUS_NOT_GENERATED
|
|
352
|
+
output_files = {
|
|
353
|
+
**output_files,
|
|
354
|
+
**self._get_default_output_files(instance_checks, solution_checks),
|
|
355
|
+
}
|
|
356
|
+
zip_file = self._generate_zip_file(output_files)
|
|
357
|
+
self._clean_output_files(output_files)
|
|
358
|
+
zip_file_status = EXECUTION_FILES_STATUS_OK
|
|
359
|
+
except:
|
|
360
|
+
print(f"Error generating zipfile: {traceback.format_exc()}")
|
|
361
|
+
zip_file = None
|
|
362
|
+
zip_file_status = EXECUTION_FILES_STATUS_ERROR
|
|
363
|
+
return zip_file, zip_file_status
|
|
364
|
+
|
|
221
365
|
@staticmethod
|
|
222
366
|
def get_solver_config(
|
|
223
367
|
config, lib="pyomo", default_solver="cbc", remove_unknown=False
|