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 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
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
cejsp/batch.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.batch import estimate_features
2
+
3
+ __all__ = ["estimate_features"]
cejsp/classifier.py ADDED
@@ -0,0 +1,4 @@
1
+ from xai_complex.classifier import JobShopComplexityClassifier, train_manifest
2
+ from xai_complex.categories import CATEGORIES
3
+
4
+ __all__ = ["JobShopComplexityClassifier", "train_manifest", "CATEGORIES"]
cejsp/cli.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.cli import main
2
+
3
+ __all__ = ["main"]
cejsp/estimator.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.estimator import ComplexityEstimator, evaluate_supervised
2
+
3
+ __all__ = ["ComplexityEstimator", "evaluate_supervised"]
cejsp/graphs.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.graphs import extract_instance_features
2
+
3
+ __all__ = ["extract_instance_features"]
cejsp/instances.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.instances import normalize_instance, summarize_instance
2
+
3
+ __all__ = ["normalize_instance", "summarize_instance"]
cejsp/io.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.io import load_instance, parse_dzn, parse_taillard
2
+
3
+ __all__ = ["load_instance", "parse_dzn", "parse_taillard"]
cejsp/labels.py ADDED
@@ -0,0 +1,3 @@
1
+ from xai_complex.labels import SolverTargetCalibrator, status_to_complexity
2
+
3
+ __all__ = ["SolverTargetCalibrator", "status_to_complexity"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cejsp = cejsp.cli:main
3
+ xai-complex = xai_complex.cli:main
@@ -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.
@@ -0,0 +1,2 @@
1
+ cejsp
2
+ xai_complex
@@ -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
+