cejsp 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cejsp/__init__.py +19 -0
- cejsp/__main__.py +3 -0
- cejsp/batch.py +3 -0
- cejsp/classifier.py +4 -0
- cejsp/cli.py +3 -0
- cejsp/estimator.py +3 -0
- cejsp/graphs.py +3 -0
- cejsp/instances.py +3 -0
- cejsp/io.py +3 -0
- cejsp/labels.py +3 -0
- cejsp-0.4.1.dist-info/METADATA +134 -0
- cejsp-0.4.1.dist-info/RECORD +40 -0
- cejsp-0.4.1.dist-info/WHEEL +5 -0
- cejsp-0.4.1.dist-info/entry_points.txt +3 -0
- cejsp-0.4.1.dist-info/licenses/LICENSE +21 -0
- cejsp-0.4.1.dist-info/top_level.txt +2 -0
- xai_complex/__init__.py +16 -0
- xai_complex/_graph_builder.py +293 -0
- xai_complex/_graph_features.py +573 -0
- xai_complex/batch.py +37 -0
- xai_complex/bottleneck.py +61 -0
- xai_complex/calibration.py +69 -0
- xai_complex/categories.py +11 -0
- xai_complex/classifier.py +244 -0
- xai_complex/cli.py +60 -0
- xai_complex/cumulative.py +70 -0
- xai_complex/estimator.py +199 -0
- xai_complex/feature_selector.py +27 -0
- xai_complex/graphs.py +49 -0
- xai_complex/instances.py +170 -0
- xai_complex/io.py +158 -0
- xai_complex/labels.py +102 -0
- xai_complex/landscape.py +77 -0
- xai_complex/proof_features.py +98 -0
- xai_complex/research.py +507 -0
- xai_complex/search_features.py +118 -0
- xai_complex/specialists.py +66 -0
- xai_complex/structural.py +64 -0
- xai_complex/subset_voting.py +60 -0
- xai_complex/supervised.py +39 -0
cejsp/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""CEJSP: JSP Complexity Estimator, with compatible model persistence."""
|
|
2
|
+
from xai_complex import normalize_instance, summarize_instance, estimate_features
|
|
3
|
+
from xai_complex import SolverTargetCalibrator, status_to_complexity, __version__
|
|
4
|
+
from .io import load_instance
|
|
5
|
+
from xai_complex.categories import CATEGORIES
|
|
6
|
+
|
|
7
|
+
__all__ = ["normalize_instance", "summarize_instance", "estimate_features",
|
|
8
|
+
"SolverTargetCalibrator", "status_to_complexity", "ComplexityEstimator",
|
|
9
|
+
"JobShopComplexityClassifier", "load_instance", "CATEGORIES"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
if name == "JobShopComplexityClassifier":
|
|
14
|
+
from .classifier import JobShopComplexityClassifier
|
|
15
|
+
return JobShopComplexityClassifier
|
|
16
|
+
if name == "ComplexityEstimator":
|
|
17
|
+
from xai_complex import ComplexityEstimator
|
|
18
|
+
return ComplexityEstimator
|
|
19
|
+
raise AttributeError(name)
|
cejsp/__main__.py
ADDED
cejsp/batch.py
ADDED
cejsp/classifier.py
ADDED
cejsp/cli.py
ADDED
cejsp/estimator.py
ADDED
cejsp/graphs.py
ADDED
cejsp/instances.py
ADDED
cejsp/io.py
ADDED
cejsp/labels.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cejsp
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: CEJSP: JSP Complexity Estimator for job shop scheduling instances
|
|
5
|
+
Author: Planning and Scheduling Research Group
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy<3,>=1.26
|
|
14
|
+
Requires-Dist: pandas<3,>=2.1
|
|
15
|
+
Requires-Dist: scikit-learn<2,>=1.4
|
|
16
|
+
Requires-Dist: scipy<2,>=1.11
|
|
17
|
+
Requires-Dist: joblib<2,>=1.3
|
|
18
|
+
Provides-Extra: integration
|
|
19
|
+
Requires-Dist: IGJSP==2.0.1; extra == "integration"
|
|
20
|
+
Requires-Dist: ortools<10,>=9.15; extra == "integration"
|
|
21
|
+
Provides-Extra: graphs
|
|
22
|
+
Requires-Dist: torch>=2.2; extra == "graphs"
|
|
23
|
+
Requires-Dist: torch-geometric>=2.5; extra == "graphs"
|
|
24
|
+
Requires-Dist: networkx>=3; extra == "graphs"
|
|
25
|
+
Requires-Dist: tqdm>=4; extra == "graphs"
|
|
26
|
+
Provides-Extra: research
|
|
27
|
+
Requires-Dist: ortools<10,>=9.15; extra == "research"
|
|
28
|
+
Requires-Dist: catboost<2,>=1.2.10; extra == "research"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# CEJSP: JSP Complexity Estimator
|
|
36
|
+
|
|
37
|
+
La API 0.2 añade entrada directa JSON/DZN/Taillard y un clasificador supervisado reentrenable de cinco clases: **toy, easy, medium, hard, open**. Consultar [el flujo de cinco clases](docs/FIVE_CLASSES.md) para entrenar, clasificar y versionar referencias. Requiere un protocolo explícito y ejemplos etiquetados; no se incluye todavía una referencia real de cinco clases validada.
|
|
38
|
+
|
|
39
|
+
Estimación empírica de dificultad de instancias de scheduling ya generadas. Este paquete no genera instancias ni modifica IGJSP.
|
|
40
|
+
|
|
41
|
+
El código procede del proyecto XAI-COMPLEX. Conserva el extractor de características de grafos y ofrece la metodología por lotes del estimador ponderado, además de una API explícita para ajustar una referencia y predecir nuevas instancias sin recalibrar cada lote.
|
|
42
|
+
|
|
43
|
+
## Instalación
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
python -m pip install .
|
|
47
|
+
python -m pip install ".[graphs,dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
El extra `graphs` instala las dependencias del extractor original. No se necesita IGJSP para instalar ni utilizar XAI-COMPLEX con instancias guardadas o características extraídas.
|
|
51
|
+
|
|
52
|
+
## Usar instancias que ya tienes
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import json
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from cejsp.graphs import extract_instance_features
|
|
58
|
+
from cejsp import ComplexityEstimator
|
|
59
|
+
|
|
60
|
+
instances = [json.loads(p.read_text(encoding="utf-8"))
|
|
61
|
+
for p in Path("instancias_existentes").glob("*.json")]
|
|
62
|
+
features = [extract_instance_features(inst) for inst in instances]
|
|
63
|
+
model = ComplexityEstimator().fit(features)
|
|
64
|
+
resultados = model.predict(features[:1])
|
|
65
|
+
model.save("referencia.joblib")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Se necesitan al menos tres instancias de referencia con variación; para una calibración útil se requiere un conjunto representativo y una evaluación independiente. No se distribuye un modelo preentrenado ni se afirma precisión predictiva sin evaluación.
|
|
69
|
+
|
|
70
|
+
Para evaluar una instancia nueva se carga la referencia ya ajustada:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
model = ComplexityEstimator.load("referencia.joblib", trusted=True)
|
|
74
|
+
resultado = model.predict([extract_instance_features(instancia_nueva)])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Solo se deben cargar modelos de confianza. Se verifican esquema y versiones de dependencias del artefacto.
|
|
78
|
+
|
|
79
|
+
## Mantener el análisis por lotes del proyecto
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import pandas as pd
|
|
83
|
+
from cejsp import estimate_features
|
|
84
|
+
|
|
85
|
+
features = pd.read_csv("graphs/features.csv").set_index("file")
|
|
86
|
+
scores = estimate_features(features)
|
|
87
|
+
scores.to_csv("complexity_scores.csv")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Esta ruta conserva Isolation Forest, LOF, MinCovDet, vecinos y prior del estimador ponderado. Sus normalizaciones y categorías son relativas al lote. La API `ComplexityEstimator` congela la referencia; usa los mismos cuatro tipos de señal sin prior y, opcionalmente, mezcla regresión supervisada. Es una variante inductiva explícita, no una reproducción numérica idéntica del análisis por lotes.
|
|
91
|
+
|
|
92
|
+
La extracción usa el grafo JSP original. Las instancias con varias velocidades requieren `speed_index` explícito. El adaptador de entrada conserva todas las velocidades, fechas y disponibilidad; el extractor original rechaza FJSP porque su grafo de rutas fijas no modela la elección de máquina. No se declara compatibilidad científica con FJSP sin validar esa representación.
|
|
93
|
+
|
|
94
|
+
`summarize_instance` ofrece estadísticas descriptivas; no sustituye las características de grafos del estimador original.
|
|
95
|
+
|
|
96
|
+
## Etiquetas y evaluación
|
|
97
|
+
|
|
98
|
+
`SolverTargetCalibrator` calibra señales de solver en el entrenamiento y mantiene fijos esos parámetros. `evaluate_supervised` ajusta objetivo y preprocesamiento dentro de cada pliegue. Usa `base_instance_id` o grupos equivalentes para que variantes de una misma instancia no aparezcan en entrenamiento y validación. Las etiquetas miden dificultad empírica respecto al solver, hardware y límites de tiempo; no una clase de complejidad computacional.
|
|
99
|
+
|
|
100
|
+
Las correcciones de etiquetas cambian los resultados anteriores: se rechazan objetivos sin señales, se validan pesos, se renormalizan componentes disponibles y se conserva el significado absoluto del estado del solver.
|
|
101
|
+
|
|
102
|
+
## Comandos
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
cejsp fit reference_features.json referencia.joblib
|
|
106
|
+
cejsp predict query_features.json referencia.joblib --trust-model
|
|
107
|
+
python -m unittest discover -s tests -v
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Los JSON de la CLI contienen listas de diccionarios de características numéricas, sin identificadores de texto. Los identificadores se mantienen por separado o en el índice del DataFrame para la API por lotes.
|
|
111
|
+
|
|
112
|
+
Consulta `docs/PUBLICAR_PYPI.md` para construir, verificar y publicar.
|
|
113
|
+
|
|
114
|
+
## Estado de esta candidata
|
|
115
|
+
|
|
116
|
+
La ejecución `validacion-20261022-175907` del 22/10/2026 completó la validación: 32 pruebas sin omisiones en el entorno inicial y en instalaciones separadas de wheel y sdist, `pip check` correcto, 162 JSON válidos y flujo completo con 20 benchmarks en los tres entornos. Ambos artefactos superan `twine check --strict`. La invariancia individual/lotes se comprueba con tolerancia absoluta `1e-9` e igualdad de categorías y señales; la persistencia no cambia las puntuaciones. Esto valida funcionamiento y distribución, no precisión científica ni mejoras de rendimiento. No se ha publicado. Consultar `RESULTADOS.md` en la raíz del repositorio para el historial.
|
|
117
|
+
|
|
118
|
+
Las etiquetas necesitan el mismo identificador completo que las características. No se infieren correspondencias a partir del tamaño, configuración o semilla, ni se promedian registros repetidos automáticamente. Por ejemplo, `10_10-0-1-cp-sat` no se convierte silenciosamente en `10-10-0.pt`: la configuración y el protocolo del solver deben resolverse antes mediante una correspondencia explícita y verificada. Para variantes, incluir `base_instance_id` en todas las etiquetas o pasar `groups` a `evaluate_supervised`; el resultado incluye los índices de cada pliegue para auditar su separación.
|
|
119
|
+
|
|
120
|
+
`fit_instances(..., speed_index=...)` y `predict_instances(..., speed_index=...)` permiten la proyección explícita de velocidad. La invariancia por lote pertenece a la API inductiva; no se exige al análisis relativo por lotes.
|
|
121
|
+
|
|
122
|
+
## Nombre e interoperabilidad
|
|
123
|
+
|
|
124
|
+
La distribución se llama cejsp y el comando principal es cejsp. Los imports xai_complex y el comando anterior se conservan por compatibilidad. Las clases mantienen su identidad y los esquemas de persistencia anteriores para permitir cargar referencias guardadas; no se duplica el estimador. La disponibilidad del nombre en PyPI no está verificada. La validación completa anterior corresponde a la distribución xai-complex; CEJSP requiere repetir la validación tras el cambio de nombre.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Mejora supervisada CEJSP 0.3.0
|
|
128
|
+
|
|
129
|
+
Referencia recomendada: `models/jsplib-8a6e36ef3126-v2/reference.joblib`, características estructurales y Extra Trees. Se conservan las referencias anteriores. Resultados, limitaciones y experimentos IGJSP: `evaluation/MEJORA_CLASIFICADOR.md`. El aumento con pseudoetiquetas no se incorpora por empeorar el resultado controlado. No hay validación externa de dificultad IGJSP.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Estudio easy y referencias 0.4.0
|
|
133
|
+
|
|
134
|
+
Se completaron cinco rondas de nuevas variables, parámetros y selección dentro de los pliegues. No se alcanzó el 95% fuera del entrenamiento. El perfil de acierto global obtiene 84,19% exploratorio; el orientado a easy reconoce 7/15 casos con 79,41% global y precisión easy de 21,21%. Son perfiles experimentales con distintos compromisos; los modelos anteriores se conservan. Informe y comandos: `evaluation/RESULTADOS_EASY.md`; perfiles: `models/PROFILES.json`. Las sondas añaden resolución limitada durante inferencia.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
cejsp/__init__.py,sha256=mtLu33dzYndlt4JwoXtGCWi4N6tISjxl38g51KoSU6o,878
|
|
2
|
+
cejsp/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
|
3
|
+
cejsp/batch.py,sha256=KlzWt9bn4wGT41zc5nWwyrONwGL2lO8jpnNjaprtiQE,81
|
|
4
|
+
cejsp/classifier.py,sha256=wBEGU0pbNneWi8R7WtzsbrejzcAsn-1J8YU_JWT43lM,200
|
|
5
|
+
cejsp/cli.py,sha256=30XX4YNBxtogAb09VTbhfd8PIZ91eKwJm5xqILGY8_M,53
|
|
6
|
+
cejsp/estimator.py,sha256=jVGQ01oUkZb8_sjoHzfZt_vk2izhP5Vt-7pm-oup5yg,133
|
|
7
|
+
cejsp/graphs.py,sha256=Evm6aMsee8cnooTTBZwGJUtFzsgQHEfbcD-bw-oMgcM,98
|
|
8
|
+
cejsp/instances.py,sha256=3ta5nvxkh1t3-f1HTknZY-MgYdPnkDVbl7s0dAW925E,129
|
|
9
|
+
cejsp/io.py,sha256=jlT4Ve5jni_xitx579RCQpdTf71UTphnBYGtDEXQC1A,128
|
|
10
|
+
cejsp/labels.py,sha256=ME5UzlcORaV91PM5M046A5uyp48CKvmJz1xbOjyiM84,138
|
|
11
|
+
cejsp-0.4.1.dist-info/licenses/LICENSE,sha256=f7RDRO-z_nMoooAya7NAb8sXtrHR6WnttYtyUc9fB-c,1116
|
|
12
|
+
xai_complex/__init__.py,sha256=jLg7QSlW1-g79q5x9-4LWjFqFPxbGpHrla-bQun4qsg,625
|
|
13
|
+
xai_complex/_graph_builder.py,sha256=GfivfJ51T5l0l_Ieu1cpR8R-O_5QLqmnWML1ETl1ewE,12393
|
|
14
|
+
xai_complex/_graph_features.py,sha256=JSxemLlPwDLDkY0ML-Dv-9p-Z81zmh4qzRfpOiRmMYo,19901
|
|
15
|
+
xai_complex/batch.py,sha256=VSv7zhPbtIzLQ8_8WlFf_GYyq6P1ceZJ-8tMCy1uEHs,2085
|
|
16
|
+
xai_complex/bottleneck.py,sha256=F5cU4lxFaGCmbbKOSEABKU6IoEJftX-NhSzHngj3bT8,3677
|
|
17
|
+
xai_complex/calibration.py,sha256=wGFTA1fyIeRjDGrOAVGRk9xNocnyyJgU9slxOBfo5Ug,3792
|
|
18
|
+
xai_complex/categories.py,sha256=l4G0N7_Rs-p-u9H8OYTbFQ7Q92gwscs3tI9FO_fP6z0,618
|
|
19
|
+
xai_complex/classifier.py,sha256=BzHg5Hvlz7cL5tBkni2Q3l6yAoMnp2vsQZbpStXHVCs,16363
|
|
20
|
+
xai_complex/cli.py,sha256=vzjMQiuHPuE-9jLKD07yfj46kNeomNy460ivwwIczYY,3134
|
|
21
|
+
xai_complex/cumulative.py,sha256=ZmHGa9YCrs4dF1pyGqmkE7cKCB8x3-CJ3DEZbfKIi8M,3443
|
|
22
|
+
xai_complex/estimator.py,sha256=1QfFW1dZ0-wNJ37PbtkK4T_d4uNUSRA4qM3VG9f3YDc,10638
|
|
23
|
+
xai_complex/feature_selector.py,sha256=SjGNLJgRdXW5BsHFNFfXF6yuDQfFbL8cKyCxo7UMzEc,1712
|
|
24
|
+
xai_complex/graphs.py,sha256=b4LJYuzxb-wBMgVHUvELXU8TNxqxjjtkzwTtGOfNEG8,3183
|
|
25
|
+
xai_complex/instances.py,sha256=qVSF7rxV0HTXpjaSILEEWM6ZmyDENAWSRD7ukr7W9q8,8894
|
|
26
|
+
xai_complex/io.py,sha256=EhZ3Y2qM0xbo3YflTJNfpXQYkw0-kUiR5NUOvHKcgQg,8540
|
|
27
|
+
xai_complex/labels.py,sha256=f-Qo9dnuxipkRHkJVJP1RT5w8dnJc2NlpaXb4SxT7t4,4353
|
|
28
|
+
xai_complex/landscape.py,sha256=PAlldRvIPM4A9IhzxeZRejK7b_dl7ZySE_q7ouCwwTI,5232
|
|
29
|
+
xai_complex/proof_features.py,sha256=ZXOl44kZ7eE--3gzYXwuvRYDqpG5Y8BfyYwue9OrT5g,5366
|
|
30
|
+
xai_complex/research.py,sha256=LC_j0EJpfFVtQ1-tUjlVcBbFd8vw4JKtYxo96Ubc7FU,20509
|
|
31
|
+
xai_complex/search_features.py,sha256=UV845tlVt78ZVhuYhiCBZTEexGLxSjjsOMKkQ6lbFRI,6370
|
|
32
|
+
xai_complex/specialists.py,sha256=A7ekFzZrQ73telwmudwz70-Wa5Q8qeG0Fl9n5VyRHvQ,3960
|
|
33
|
+
xai_complex/structural.py,sha256=FQyRtIbY3VnHVJaVgDOxlKQn3TonfnZUKDUWjlQIDYA,3801
|
|
34
|
+
xai_complex/subset_voting.py,sha256=RO65oBb4fWnJaUBFheJBNNhise79i0SGihhI-HiXRQ4,2848
|
|
35
|
+
xai_complex/supervised.py,sha256=uOOq83d_-xYxbUwkEtNjPbwnkSWC84fd1cfJ_xk_w9I,2529
|
|
36
|
+
cejsp-0.4.1.dist-info/METADATA,sha256=mCgdIzjMFq8FgsAGM8N53R6sC11h09BWctKHJZVdQBs,8964
|
|
37
|
+
cejsp-0.4.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
38
|
+
cejsp-0.4.1.dist-info/entry_points.txt,sha256=s9YGng9OkNOf_uhQJ9HIifrkbYUr06RFcBSxnS9m1Cs,76
|
|
39
|
+
cejsp-0.4.1.dist-info/top_level.txt,sha256=CcmLFtEFMuF-elLUsLlsuDDrcCeiB-wQUSh6ZMQqlHM,18
|
|
40
|
+
cejsp-0.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Planning and Scheduling Research Group
|
|
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.
|
xai_complex/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Empirical scheduling difficulty; no training or filesystem work on import."""
|
|
2
|
+
|
|
3
|
+
from .instances import summarize_instance, normalize_instance
|
|
4
|
+
from .labels import SolverTargetCalibrator, status_to_complexity
|
|
5
|
+
from .batch import estimate_features
|
|
6
|
+
|
|
7
|
+
__version__ = "0.4.1"
|
|
8
|
+
__all__ = ["normalize_instance", "summarize_instance",
|
|
9
|
+
"SolverTargetCalibrator", "status_to_complexity", "ComplexityEstimator", "estimate_features"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
if name == "ComplexityEstimator":
|
|
14
|
+
from .estimator import ComplexityEstimator
|
|
15
|
+
return ComplexityEstimator
|
|
16
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from itertools import permutations
|
|
4
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
5
|
+
|
|
6
|
+
import itertools
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from torch_geometric.data import HeteroData
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_operations_strict(data: Dict[str, Any]) -> List[Tuple[int, int, int, float, float]]:
|
|
16
|
+
"""
|
|
17
|
+
Extrae operaciones asumiendo estructura estricta:
|
|
18
|
+
- ProcessingTime: (J, O) o (J, O, 1)
|
|
19
|
+
- Orden: (J, O) con id de máquina por operación
|
|
20
|
+
- EnergyConsumption: (J, O) o (J, O, 1)
|
|
21
|
+
"""
|
|
22
|
+
PT = np.asarray(data["ProcessingTime"])
|
|
23
|
+
OR = np.asarray(data["Orden"])
|
|
24
|
+
EC = np.asarray(data["EnergyConsumption"])
|
|
25
|
+
|
|
26
|
+
num_jobs = int(data["numJobs"])
|
|
27
|
+
if (PT.ndim == 3 and PT.shape[2] != 1) or (EC.ndim == 3 and EC.shape[2] != 1):
|
|
28
|
+
raise ValueError("Seleccione explícitamente una velocidad antes de construir el grafo JSP")
|
|
29
|
+
if any(k in data for k in ("AllowedMachines", "allowed_machines", "available_machines", "AVJM", "avail_machines", "machine_mask")):
|
|
30
|
+
raise ValueError("El grafo original de rutas fijas no representa FJSP")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if PT.ndim == 3:
|
|
34
|
+
num_ops = PT.shape[1]
|
|
35
|
+
get_pt = lambda j, o: float(PT[j, o, 0])
|
|
36
|
+
elif PT.ndim == 2:
|
|
37
|
+
num_ops = PT.shape[1]
|
|
38
|
+
get_pt = lambda j, o: float(PT[j, o])
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("ProcessingTime tiene una forma inesperada. Esperado (J,O[,1]).")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if EC.ndim == 3:
|
|
44
|
+
get_ec = lambda j, o: float(EC[j, o, 0])
|
|
45
|
+
elif EC.ndim == 2:
|
|
46
|
+
get_ec = lambda j, o: float(EC[j, o])
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError("EnergyConsumption tiene una forma inesperada. Esperado (J,O[,1]).")
|
|
49
|
+
|
|
50
|
+
ops = []
|
|
51
|
+
for j in range(num_jobs):
|
|
52
|
+
for o in range(num_ops):
|
|
53
|
+
proc_time = get_pt(j, o)
|
|
54
|
+
energy = get_ec(j, o)
|
|
55
|
+
mach = int(OR[j, o])
|
|
56
|
+
ops.append((j, o, mach, proc_time, energy))
|
|
57
|
+
return ops
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _maybe_get(mat: Dict[str, Any], keys: List[str]) -> Optional[np.ndarray]:
|
|
61
|
+
"""Devuelve np.array si existe alguna de las keys y no está vacía."""
|
|
62
|
+
for k in keys:
|
|
63
|
+
if k in mat and mat[k] is not None:
|
|
64
|
+
arr = np.asarray(mat[k])
|
|
65
|
+
if arr.size > 0:
|
|
66
|
+
return arr
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _flatten_generator_features(inst: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Normaliza los nombres de métricas precomputadas del generador y
|
|
73
|
+
las devuelve como escalares Python (o np.* convertidos).
|
|
74
|
+
Mantiene también una copia del dict original 'features' para referencia.
|
|
75
|
+
"""
|
|
76
|
+
out: Dict[str, Any] = {}
|
|
77
|
+
|
|
78
|
+
if "numJobs" in inst: out["n_jobs"] = int(inst["numJobs"])
|
|
79
|
+
if "numMchs" in inst: out["n_machs"] = int(inst["numMchs"])
|
|
80
|
+
if "speed" in inst: out["speed"] = int(inst["speed"])
|
|
81
|
+
if "rddd" in inst: out["rddd"] = int(inst["rddd"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if "max_makespan" in inst: out["makespan_max"] = int(inst["max_makespan"])
|
|
85
|
+
if "min_makespan" in inst: out["makespan_min"] = int(inst["min_makespan"])
|
|
86
|
+
|
|
87
|
+
if "max_min_makespan" in inst:
|
|
88
|
+
out["makespan_range"] = int(inst["max_min_makespan"])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# el generador usa dos variantes de nombre: max_energy/min_energy y max_sum_energy/min_sum_energy
|
|
92
|
+
if "max_energy" in inst: out["energy_sum_max"] = int(inst["max_energy"])
|
|
93
|
+
if "min_energy" in inst: out["energy_sum_min"] = int(inst["min_energy"])
|
|
94
|
+
if "max_min_energy" in inst: out["energy_sum_range"] = int(inst["max_min_energy"])
|
|
95
|
+
|
|
96
|
+
feats = inst.get("features", {})
|
|
97
|
+
if isinstance(feats, dict):
|
|
98
|
+
# sobreescribimos si están también en 'features' con otros nombres
|
|
99
|
+
if "max_makespan" in feats: out["makespan_max"] = int(feats["max_makespan"])
|
|
100
|
+
if "min_makespan" in feats: out["makespan_min"] = int(feats["min_makespan"])
|
|
101
|
+
|
|
102
|
+
if "max_sum_energy" in feats: out["energy_sum_max"] = int(feats["max_sum_energy"])
|
|
103
|
+
if "min_sum_energy" in feats: out["energy_sum_min"] = int(feats["min_sum_energy"])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if "max_processing_time_value" in feats: out["p_value_max"] = int(feats["max_processing_time_value"])
|
|
107
|
+
if "min_processing_time_value" in feats: out["p_value_min"] = int(feats["min_processing_time_value"])
|
|
108
|
+
if "mean_processing_time_value" in feats: out["p_value_mean"] = float(feats["mean_processing_time_value"])
|
|
109
|
+
|
|
110
|
+
if "max_energy_value" in feats: out["e_value_max"] = int(feats["max_energy_value"])
|
|
111
|
+
if "min_energy_value" in feats: out["e_value_min"] = int(feats["min_energy_value"])
|
|
112
|
+
if "mean_energy_value" in feats: out["e_value_mean"] = float(feats["mean_energy_value"])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if "min_window" in feats: out["window_min"] = float(feats["min_window"])
|
|
116
|
+
if "mean_window" in feats: out["window_mean"] = float(feats["mean_window"])
|
|
117
|
+
if "max_window" in feats: out["window_max"] = float(feats["max_window"]) if feats.get("max_window") is not None else None
|
|
118
|
+
if "overlap" in feats: out["overlap"] = float(feats["overlap"])
|
|
119
|
+
if "max_tardiness" in feats: out["max_tardiness"] = float(feats["max_tardiness"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
out["gen_features"] = feats
|
|
123
|
+
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GraphBuilderStrict:
|
|
128
|
+
def __init__(self, instance_dict: Dict[str, Any]):
|
|
129
|
+
self.data_dict = instance_dict
|
|
130
|
+
self.num_jobs = int(instance_dict["numJobs"])
|
|
131
|
+
self.num_machs = int(instance_dict["numMchs"])
|
|
132
|
+
self.operations = extract_operations_strict(instance_dict)
|
|
133
|
+
|
|
134
|
+
self.node_features: List[List[float]] = []
|
|
135
|
+
self.node_mapping: Dict[Tuple[int, int], int] = {}
|
|
136
|
+
self.darcs: List[List[int]] = []
|
|
137
|
+
self.carcs: List[List[int]] = []
|
|
138
|
+
self.data = None
|
|
139
|
+
|
|
140
|
+
self.build_graph()
|
|
141
|
+
|
|
142
|
+
def _ops_per_job(self) -> int:
|
|
143
|
+
PT = np.asarray(self.data_dict["ProcessingTime"])
|
|
144
|
+
return PT.shape[1] if PT.ndim in (2, 3) else int(round(len(self.operations) / self.num_jobs))
|
|
145
|
+
|
|
146
|
+
def build_graph(self):
|
|
147
|
+
|
|
148
|
+
node_id = 0
|
|
149
|
+
include_virtual = True
|
|
150
|
+
|
|
151
|
+
if include_virtual:
|
|
152
|
+
self.node_features.append([0.0, -1.0])
|
|
153
|
+
self.node_mapping[("source", 0)] = node_id
|
|
154
|
+
|
|
155
|
+
op_node_index: List[int] = []
|
|
156
|
+
for (job_id, op_id, mac_id, proc_time, energy) in self.operations:
|
|
157
|
+
node_id += 1
|
|
158
|
+
self.node_features.append([float(proc_time), float(mac_id)])
|
|
159
|
+
self.node_mapping[(job_id, op_id)] = node_id
|
|
160
|
+
op_node_index.append(node_id)
|
|
161
|
+
|
|
162
|
+
if include_virtual:
|
|
163
|
+
node_id += 1
|
|
164
|
+
self.node_features.append([0.0, -1.0])
|
|
165
|
+
self.node_mapping[("sink", 0)] = node_id
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
origen, destino = [], []
|
|
169
|
+
for j in range(self.num_jobs):
|
|
170
|
+
nodes = [self.node_mapping[(j, o)] for o in range(self._ops_per_job())]
|
|
171
|
+
if include_virtual:
|
|
172
|
+
origen.append(self.node_mapping[("source", 0)]); destino.append(nodes[0])
|
|
173
|
+
for i in range(len(nodes) - 1):
|
|
174
|
+
origen.append(nodes[i]); destino.append(nodes[i + 1])
|
|
175
|
+
if include_virtual:
|
|
176
|
+
origen.append(nodes[-1]); destino.append(self.node_mapping[("sink", 0)])
|
|
177
|
+
self.carcs = [origen, destino]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
mach_to_nodes = defaultdict(list)
|
|
181
|
+
for (job_id, op_id, mac_id, proc_time, energy) in self.operations:
|
|
182
|
+
nid = self.node_mapping[(job_id, op_id)]
|
|
183
|
+
mach_to_nodes[mac_id].append(nid)
|
|
184
|
+
|
|
185
|
+
origen_d, destino_d = [], []
|
|
186
|
+
for nodes in mach_to_nodes.values():
|
|
187
|
+
if len(nodes) < 2:
|
|
188
|
+
continue
|
|
189
|
+
for a, b in permutations(nodes, 2):
|
|
190
|
+
origen_d.append(a); destino_d.append(b)
|
|
191
|
+
self.darcs = [origen_d, destino_d]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
data = HeteroData()
|
|
195
|
+
data["node"].x = torch.tensor(self.node_features, dtype=torch.float)
|
|
196
|
+
|
|
197
|
+
data[("node", "conjunctive", "node")].edge_index = (
|
|
198
|
+
torch.tensor(self.carcs, dtype=torch.long) if len(self.carcs[0]) > 0
|
|
199
|
+
else torch.empty((2, 0), dtype=torch.long)
|
|
200
|
+
)
|
|
201
|
+
data[("node", "disjunctive", "node")].edge_index = (
|
|
202
|
+
torch.tensor(self.darcs, dtype=torch.long) if len(self.darcs[0]) > 0
|
|
203
|
+
else torch.empty((2, 0), dtype=torch.long)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
N_ops = len(self.operations)
|
|
208
|
+
job_ids = np.fromiter((j for (j, o, m, p, e) in self.operations), dtype=np.int64, count=N_ops)
|
|
209
|
+
op_ids = np.fromiter((o for (j, o, m, p, e) in self.operations), dtype=np.int64, count=N_ops)
|
|
210
|
+
mach_ids = np.fromiter((m for (j, o, m, p, e) in self.operations), dtype=np.int64, count=N_ops)
|
|
211
|
+
P = np.fromiter((p for (j, o, m, p, e) in self.operations), dtype=np.float64, count=N_ops).reshape(N_ops, 1)
|
|
212
|
+
E = np.fromiter((e for (j, o, m, p, e) in self.operations), dtype=np.float64, count=N_ops).reshape(N_ops, 1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
op_cost = _maybe_get(self.data_dict, ["operationCost", "opCost", "operation_cost"])
|
|
216
|
+
if op_cost is not None:
|
|
217
|
+
op_cost = np.asarray(op_cost, dtype=np.float64).reshape(self.num_jobs, self._ops_per_job())
|
|
218
|
+
op_cost = op_cost.reshape(-1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
R = _maybe_get(self.data_dict, ["ReleaseDate", "Release", "R"])
|
|
222
|
+
D = _maybe_get(self.data_dict, ["DueDate", "Due", "D"])
|
|
223
|
+
if R is not None:
|
|
224
|
+
R = R.reshape(self.num_jobs, -1).astype(np.float64)
|
|
225
|
+
if R.shape[1] == 1:
|
|
226
|
+
R = np.repeat(R, self._ops_per_job(), axis=1)
|
|
227
|
+
R = R[:, :self._ops_per_job()].reshape(-1)
|
|
228
|
+
if D is not None:
|
|
229
|
+
D = D.reshape(self.num_jobs, -1).astype(np.float64)
|
|
230
|
+
if D.shape[1] == 1:
|
|
231
|
+
D = np.repeat(D, self._ops_per_job(), axis=1)
|
|
232
|
+
D = D[:, :self._ops_per_job()].reshape(-1)
|
|
233
|
+
|
|
234
|
+
# AVJM (si no existe, derivamos del uso de máquinas en cada job)
|
|
235
|
+
avjm = _maybe_get(self.data_dict, ["AllowedMachines", "allowed_machines", "AVJM", "avail_machines", "machine_mask"])
|
|
236
|
+
if avjm is None:
|
|
237
|
+
avjm = np.zeros((self.num_jobs, self.num_machs), dtype=np.int64)
|
|
238
|
+
for j in range(self.num_jobs):
|
|
239
|
+
used = np.unique(mach_ids[j * self._ops_per_job() : (j + 1) * self._ops_per_job()])
|
|
240
|
+
avjm[j, used] = 1
|
|
241
|
+
else:
|
|
242
|
+
avjm = (np.asarray(avjm) > 0).astype(np.int64)
|
|
243
|
+
if avjm.shape == (N_ops, self.num_machs):
|
|
244
|
+
tmp = np.zeros((self.num_jobs, self.num_machs), dtype=np.int64)
|
|
245
|
+
for j in range(self.num_jobs):
|
|
246
|
+
mask = (job_ids == j)
|
|
247
|
+
if np.any(mask):
|
|
248
|
+
tmp[j] = (avjm[mask].max(axis=0) > 0).astype(np.int64)
|
|
249
|
+
avjm = tmp
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
setup_mat = _maybe_get(self.data_dict, ["Setup", "SetupTime", "setup", "mtt", "Changeover", "setup_matrix"])
|
|
253
|
+
if setup_mat is not None:
|
|
254
|
+
setup_mat = setup_mat.astype(np.float64)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
data.n_jobs = int(self.num_jobs)
|
|
258
|
+
data.n_machs = int(self.num_machs)
|
|
259
|
+
data.P = torch.tensor(P, dtype=torch.float)
|
|
260
|
+
data.E = torch.tensor(E, dtype=torch.float)
|
|
261
|
+
data.job = torch.tensor(job_ids, dtype=torch.long)
|
|
262
|
+
data.machine = torch.tensor(mach_ids, dtype=torch.long)
|
|
263
|
+
data.op = torch.tensor(op_ids, dtype=torch.long)
|
|
264
|
+
data.op_node_index = torch.tensor(op_node_index, dtype=torch.long)
|
|
265
|
+
|
|
266
|
+
if op_cost is not None:
|
|
267
|
+
data.operation_cost = torch.tensor(op_cost, dtype=torch.float)
|
|
268
|
+
|
|
269
|
+
if R is not None: data.R = torch.tensor(R, dtype=torch.float)
|
|
270
|
+
if D is not None: data.D = torch.tensor(D, dtype=torch.float)
|
|
271
|
+
if setup_mat is not None: data.setup = torch.tensor(setup_mat, dtype=torch.float)
|
|
272
|
+
if avjm is not None: data.allowed_machines = torch.tensor(avjm, dtype=torch.long)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
N_nodes = len(self.node_features)
|
|
276
|
+
op_mask = torch.zeros(N_nodes, dtype=torch.bool)
|
|
277
|
+
op_mask[data.op_node_index] = True
|
|
278
|
+
data["node"].op_mask = op_mask
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if include_virtual:
|
|
282
|
+
data.source_idx = int(self.node_mapping[("source", 0)])
|
|
283
|
+
data.sink_idx = int(self.node_mapping[("sink", 0)])
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
gf = _flatten_generator_features(self.data_dict)
|
|
287
|
+
|
|
288
|
+
for k, v in gf.items():
|
|
289
|
+
setattr(data, k, v if not isinstance(v, np.generic) else v.item())
|
|
290
|
+
|
|
291
|
+
self.data = data
|
|
292
|
+
|
|
293
|
+
|