walopy 0.2.7__tar.gz → 0.3.0__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.
Files changed (69) hide show
  1. {walopy-0.2.7 → walopy-0.3.0}/PKG-INFO +637 -463
  2. walopy-0.2.7/src/walopy.egg-info/PKG-INFO → walopy-0.3.0/README.md +625 -495
  3. walopy-0.3.0/pyproject.toml +79 -0
  4. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/__init__.py +132 -97
  5. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/__main__.py +17 -17
  6. walopy-0.3.0/src/walopy/_utils.py +97 -0
  7. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/advanced.py +387 -212
  8. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/bottleneck.py +50 -32
  9. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/fitting.py +54 -41
  10. walopy-0.3.0/src/walopy/inventory.py +2825 -0
  11. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/kpi.py +111 -62
  12. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/network.py +46 -43
  13. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/operations.py +111 -68
  14. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/plotting.py +124 -119
  15. walopy-0.3.0/src/walopy/project.py +393 -0
  16. walopy-0.3.0/src/walopy/py.typed +0 -0
  17. walopy-0.3.0/src/walopy/queuing.py +628 -0
  18. walopy-0.3.0/src/walopy/reliability.py +593 -0
  19. walopy-0.3.0/src/walopy/scheduling.py +558 -0
  20. {walopy-0.2.7 → walopy-0.3.0}/src/walopy/solver.py +193 -96
  21. walopy-0.2.7/README.md → walopy-0.3.0/src/walopy.egg-info/PKG-INFO +669 -458
  22. {walopy-0.2.7 → walopy-0.3.0}/src/walopy.egg-info/SOURCES.txt +23 -1
  23. {walopy-0.2.7 → walopy-0.3.0}/src/walopy.egg-info/requires.txt +8 -2
  24. walopy-0.3.0/tests/test_abc_xyz.py +180 -0
  25. {walopy-0.2.7 → walopy-0.3.0}/tests/test_advanced.py +10 -13
  26. walopy-0.3.0/tests/test_cli.py +67 -0
  27. walopy-0.3.0/tests/test_complejidad.py +107 -0
  28. walopy-0.3.0/tests/test_contrato_entradas.py +222 -0
  29. walopy-0.3.0/tests/test_cpm_pert.py +185 -0
  30. walopy-0.3.0/tests/test_documentacion.py +31 -0
  31. walopy-0.3.0/tests/test_ejemplos.py +26 -0
  32. {walopy-0.2.7 → walopy-0.3.0}/tests/test_exchange_curve.py +5 -4
  33. {walopy-0.2.7 → walopy-0.3.0}/tests/test_fitting.py +2 -2
  34. walopy-0.3.0/tests/test_idioma.py +111 -0
  35. {walopy-0.2.7 → walopy-0.3.0}/tests/test_inventory.py +2 -4
  36. {walopy-0.2.7 → walopy-0.3.0}/tests/test_inventory_new.py +9 -12
  37. {walopy-0.2.7 → walopy-0.3.0}/tests/test_kpi.py +9 -11
  38. {walopy-0.2.7 → walopy-0.3.0}/tests/test_mg1.py +2 -4
  39. walopy-0.3.0/tests/test_mmc_estable.py +58 -0
  40. walopy-0.3.0/tests/test_mrp.py +95 -0
  41. walopy-0.3.0/tests/test_neh_flowshop.py +113 -0
  42. {walopy-0.2.7 → walopy-0.3.0}/tests/test_network.py +2 -2
  43. {walopy-0.2.7 → walopy-0.3.0}/tests/test_new_models.py +9 -9
  44. {walopy-0.2.7 → walopy-0.3.0}/tests/test_operations.py +1 -1
  45. walopy-0.3.0/tests/test_plotting.py +84 -0
  46. walopy-0.3.0/tests/test_politicas_inventario.py +104 -0
  47. {walopy-0.2.7 → walopy-0.3.0}/tests/test_queuing.py +2 -4
  48. walopy-0.3.0/tests/test_readme.py +42 -0
  49. walopy-0.3.0/tests/test_referencias_externas.py +57 -0
  50. walopy-0.3.0/tests/test_regresion_validacion.py +305 -0
  51. walopy-0.3.0/tests/test_reliability.py +91 -0
  52. {walopy-0.2.7 → walopy-0.3.0}/tests/test_safety_stock_curve.py +7 -5
  53. walopy-0.3.0/tests/test_scheduling.py +91 -0
  54. {walopy-0.2.7 → walopy-0.3.0}/tests/test_solver.py +28 -4
  55. walopy-0.3.0/tests/test_trazabilidad.py +72 -0
  56. walopy-0.3.0/tests/test_version.py +33 -0
  57. walopy-0.3.0/tests/test_weibull.py +111 -0
  58. walopy-0.2.7/pyproject.toml +0 -52
  59. walopy-0.2.7/src/walopy/_utils.py +0 -67
  60. walopy-0.2.7/src/walopy/inventory.py +0 -2004
  61. walopy-0.2.7/src/walopy/queuing.py +0 -448
  62. walopy-0.2.7/src/walopy/reliability.py +0 -298
  63. walopy-0.2.7/src/walopy/scheduling.py +0 -315
  64. walopy-0.2.7/tests/test_v026.py +0 -276
  65. {walopy-0.2.7 → walopy-0.3.0}/LICENSE +0 -0
  66. {walopy-0.2.7 → walopy-0.3.0}/setup.cfg +0 -0
  67. {walopy-0.2.7 → walopy-0.3.0}/src/walopy.egg-info/dependency_links.txt +0 -0
  68. {walopy-0.2.7 → walopy-0.3.0}/src/walopy.egg-info/top_level.txt +0 -0
  69. {walopy-0.2.7 → walopy-0.3.0}/tests/test_bottleneck.py +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: walopy
3
- Version: 0.2.7
4
- Summary: Queuing theory, operations analysis, OEE, bottleneck analysis and KPI trees for Python
3
+ Version: 0.3.0
4
+ Summary: Herramientas de investigación de operaciones: colas, inventarios, scheduling, CPM/PERT, confiabilidad, OEE, cuellos de botella y árboles de KPI
5
5
  Author-email: LeoSanta15 <angelsanta1@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/LeoSanta15/walopy
8
8
  Project-URL: Issues, https://github.com/LeoSanta15/walopy/issues
9
9
  Project-URL: Changelog, https://github.com/LeoSanta15/walopy/blob/main/CHANGELOG.md
10
- Keywords: queuing theory,operations research,little's law,kingman,bottleneck,OEE,efficiency,utilization,unit cost,KPI
10
+ Keywords: operations research,queuing theory,little's law,kingman,inventory,EOQ,MRP,ABC analysis,scheduling,flow-shop,CPM,PERT,reliability,weibull,bottleneck,OEE,KPI
11
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
12
13
  Classifier: Programming Language :: Python :: 3.9
13
14
  Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
@@ -17,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
17
18
  Classifier: Operating System :: OS Independent
18
19
  Classifier: Intended Audience :: Science/Research
19
20
  Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Typing :: Typed
20
22
  Requires-Python: >=3.9
21
23
  Description-Content-Type: text/markdown
22
24
  License-File: LICENSE
@@ -27,8 +29,13 @@ Requires-Dist: plotly>=5.0
27
29
  Provides-Extra: dev
28
30
  Requires-Dist: pytest>=7; extra == "dev"
29
31
  Requires-Dist: pytest-cov>=4; extra == "dev"
30
- Requires-Dist: ruff>=0.6; extra == "dev"
31
- Requires-Dist: mypy>=1.10; extra == "dev"
32
+ Requires-Dist: pytest-timeout>=2; extra == "dev"
33
+ Requires-Dist: ruff~=0.16.10; extra == "dev"
34
+ Requires-Dist: mypy<3,>=1.10; extra == "dev"
35
+ Provides-Extra: release
36
+ Requires-Dist: build; extra == "release"
37
+ Requires-Dist: twine; extra == "release"
38
+ Requires-Dist: pip-audit; extra == "release"
32
39
  Provides-Extra: docs
33
40
  Requires-Dist: sphinx>=7; extra == "docs"
34
41
  Requires-Dist: sphinx-rtd-theme>=2; extra == "docs"
@@ -66,15 +73,16 @@ pip install walopy
66
73
  12. [Curvas de intercambio](#12-curvas-de-intercambio)
67
74
  13. [Programación de producción (scheduling)](#13-programación-de-producción-scheduling)
68
75
  14. [Confiabilidad](#14-confiabilidad--reliability)
69
- 15. [Árboles de KPI](#15-árboles-de-kpi)
70
- 16. [Solvers y optimización](#16-solvers-y-optimización)
71
- 17. [Análisis de sensibilidad](#17-análisis-de-sensibilidad)
72
- 18. [Escenarios en lote (batch_model)](#18-escenarios-en-lote-batch_model)
73
- 19. [Comparar múltiples resultados](#19-comparar-múltiples-resultados)
74
- 20. [Gráficas](#20-gráficas)
75
- 21. [CLI](#21-cli)
76
- 22. [Referencia de módulos](#22-referencia-de-módulos)
77
- 23. [Requisitos](#23-requisitos)
76
+ 15. [Proyectos: CPM y PERT](#15-proyectos-cpm-y-pert--project)
77
+ 16. [Árboles de KPI](#16-árboles-de-kpi)
78
+ 17. [Solvers y optimización](#17-solvers-y-optimización)
79
+ 18. [Análisis de sensibilidad](#18-análisis-de-sensibilidad)
80
+ 19. [Escenarios en lote (batch_model)](#19-escenarios-en-lote-batch_model)
81
+ 20. [Comparar múltiples resultados](#20-comparar-múltiples-resultados)
82
+ 21. [Gráficas](#21-gráficas)
83
+ 22. [CLI](#22-cli)
84
+ 23. [Referencia de módulos](#23-referencia-de-módulos)
85
+ 24. [Requisitos](#24-requisitos)
78
86
 
79
87
  ---
80
88
 
@@ -119,14 +127,14 @@ import walopy as wl
119
127
 
120
128
  r = wl.mm1(lam=3.0, mu=5.0)
121
129
  print(r)
122
- # Model : M/M/1
123
- # λ : 3.0 (arrival rate)
124
- # μ : 5.0 (service rate per server)
125
- # ρ : 0.6 (utilization)
126
- # L : 1.5 (avg units in system)
127
- # Lq : 0.9 (avg units in queue)
128
- # W : 0.5 (avg time in system)
129
- # Wq : 0.3 (avg wait time in queue)
130
+ # Modelo : M/M/1
131
+ # λ : 3.0 (tasa de llegada)
132
+ # μ : 5.0 (tasa de servicio por servidor)
133
+ # ρ : 0.6 (utilización por servidor)
134
+ # L : 1.5 (unidades promedio en el sistema)
135
+ # Lq : 0.9 (unidades promedio en cola)
136
+ # W : 0.5 (tiempo promedio en el sistema)
137
+ # Wq : 0.3 (tiempo promedio de espera en cola)
130
138
 
131
139
  df = r.to_frame() # exportar a DataFrame
132
140
  r.plot() # gráfica Wq y Lq vs ρ
@@ -193,7 +201,9 @@ r = wl.kingman(lam=3.0, mu=5.0, ca2=0.5, cs2=1.0)
193
201
  r = wl.kingman(lam=3.0, mu=5.0, ca2=1.0, cs2=3.0)
194
202
 
195
203
  # Recuperar parámetros ajustados desde datos reales y usarlos directamente
196
- fit = wl.fit_from_data(inter_arrivals=..., service_times=...)
204
+ import numpy as np
205
+ rng = np.random.default_rng(42)
206
+ fit = wl.fit_from_data(inter_arrivals=rng.exponential(0.2, 500), service_times=rng.exponential(0.1, 500))
197
207
  r = wl.kingman(**fit.to_model_kwargs())
198
208
  ```
199
209
 
@@ -266,12 +276,12 @@ Cola con **capacidad finita** K (servidor + sala de espera). Los clientes que ll
266
276
 
267
277
  **Parámetros adicionales en `result.params`:**
268
278
  - `PK (blocking prob)` — probabilidad de rechazo (sistema lleno)
269
- - `lam_eff` — tasa de llegada efectiva = λ · (1 − P_K)
279
+ - `λ_eff (effective rate)` — tasa de llegada efectiva = λ · (1 − P_K)
270
280
 
271
281
  ```python
272
282
  r = wl.mm1k(lam=5.0, mu=3.0, K=10)
273
- print(r.params["PK (blocking prob)"]) # fracción de clientes rechazados
274
- print(r.params["lam_eff"]) # tasa real de entrada al sistema
283
+ print(r.params["PK (prob. de bloqueo)"]) # fracción de clientes rechazados
284
+ print(r.params["λ_eff (tasa efectiva)"]) # tasa real de entrada al sistema
275
285
  ```
276
286
 
277
287
  ---
@@ -282,7 +292,7 @@ Multi-servidor con capacidad finita. Generaliza tanto M/M/c (K→∞) como M/M/1
282
292
 
283
293
  ```python
284
294
  r = wl.mmck(lam=8.0, mu=3.0, c=2, K=20)
285
- print(r.params["PK (blocking prob)"])
295
+ print(r.params["PK (prob. de bloqueo)"])
286
296
 
287
297
  # Verificación: con K muy grande se acerca a M/M/c
288
298
  r_inf = wl.mmc(lam=3.0, mu=5.0, c=2)
@@ -455,8 +465,9 @@ print(fit_ia.mu) # None
455
465
  ts = np.cumsum(rng.exponential(0.2, 1000)) # timestamps acumulados
456
466
  fit = wl.fit_from_data(arrival_timestamps=ts)
457
467
 
458
- # 4. Conectar directamente con un modelo
459
- r = wl.kingman(**fit.to_model_kwargs()) # usa lam, mu, ca2, cs2
468
+ # 4. Conectar directamente con un modelo (necesita λ, μ, ca² y cs²: ajuste con ambas muestras)
469
+ fit_completo = wl.fit_from_data(ia, svc)
470
+ r = wl.kingman(**fit_completo.to_model_kwargs()) # usa lam, mu, ca2, cs2
460
471
  ```
461
472
 
462
473
  **`FitResult` — atributos:**
@@ -570,12 +581,12 @@ r = wl.eoq(
570
581
  holding_cost=2, # h: costo por unidad por período
571
582
  )
572
583
  print(r)
573
- # EOQ (order quantity): 223.6 units
574
- # Order frequency : 4.472 orders/period
575
- # Cycle time : 0.2236 periods
576
- # Total cost : 447.2
577
- # Holding cost : 223.6
578
- # Ordering cost : 223.6 ← iguales en el óptimo
584
+ # EOQ (cantidad de pedido): 223.6 unidades
585
+ # Frecuencia de pedidos : 4.472 pedidos/periodo
586
+ # Tiempo de ciclo : 0.2236 periodos
587
+ # Costo total : 447.2
588
+ # Costo de mantener : 223.6
589
+ # Costo de ordenar : 223.6 ← iguales en el óptimo
579
590
 
580
591
  r.plot() # gráfica de curvas de costo
581
592
  df = r.to_frame()
@@ -613,11 +624,11 @@ r = wl.reorder_point(
613
624
  service_level=0.95, # nivel de servicio deseado
614
625
  )
615
626
  print(r)
616
- # Reorder point : 132.4 units
617
- # Safety stock : 32.4 units
618
- # Service level : 95.00%
619
- # z-score : 1.645
620
- # Mean demand LT : 100.0
627
+ # Punto de reorden : 132.4 unidades
628
+ # Stock de seguridad : 32.4 unidades
629
+ # Nivel de servicio : 95.00%
630
+ # z : 1.645
631
+ # Demanda media en LT : 100.0
621
632
  # Std demand LT : 19.72
622
633
 
623
634
  # Sin variabilidad → solo demanda esperada durante LT
@@ -665,14 +676,14 @@ r = wl.newsvendor(
665
676
  salvage=2, # valor residual de unidades no vendidas
666
677
  )
667
678
  print(r)
668
- # Optimal quantity : 100.0 units
669
- # Critical ratio : 0.5
670
- # Expected profit : 320.0
671
- # Expected sales : 100.0
672
- # Expected leftover : 0.0
673
- # Expected stockout : 0.0
674
- # Underage cost Cu : 4.0
675
- # Overage cost Co : 4.0
679
+ # Cantidad óptima : 100.0 unidades
680
+ # Razón crítica : 0.5
681
+ # Utilidad esperada : 320.0
682
+ # Ventas esperadas : 100.0
683
+ # Sobrante esperado : 0.0
684
+ # Faltante esperado : 0.0
685
+ # Costo de subestimar Cu: 4.0
686
+ # Costo de sobrestimar Co: 4.0
676
687
 
677
688
  # Producto con alto margen → pedir por encima de la media
678
689
  r_alto = wl.newsvendor(demand_mean=100, demand_std=20,
@@ -719,15 +730,15 @@ r = wl.ebq(
719
730
  production_rate=4000, # P: tasa de producción (debe ser > D)
720
731
  )
721
732
  print(r)
722
- # EBQ (batch size) : 258.2 units
723
- # Max inventory : 193.6
724
- # Avg inventory : 96.82
725
- # Order frequency : 3.873 runs/period
726
- # Cycle time : 0.2582 periods
727
- # Production time/cyc : 0.06455 periods
728
- # Total cost : 193.6
729
- # Holding cost : 96.82
730
- # Setup cost : 96.82
733
+ # EBQ (tamaño de lote): 258.2 unidades
734
+ # Inventario máximo : 193.6
735
+ # Inventario promedio : 96.82
736
+ # Frecuencia de lotes : 3.873 lotes/periodo
737
+ # Tiempo de ciclo : 0.2582 periodos
738
+ # Tiempo de producción: 0.06455 periodos
739
+ # Costo total : 193.6
740
+ # Costo de mantener : 96.82
741
+ # Costo de preparación: 96.82
731
742
  ```
732
743
 
733
744
  **`EBQResult` — atributos:**
@@ -833,9 +844,9 @@ r = wl.lot_for_lot(demands, setup_cost=200, holding_cost=1)
833
844
  print(r.total_holding_cost) # 0.0 — sin inventario residual
834
845
  print(r.n_orders) # 4 — un pedido por período con demanda > 0
835
846
  print(r)
836
- # Method : Lot-for-Lot
837
- # Number of orders : 4
838
- # Total cost : 800.0
847
+ # Método : Lote por lote
848
+ # Número de pedidos : 4
849
+ # Costo total : 800.0
839
850
  ```
840
851
 
841
852
  ---
@@ -898,11 +909,83 @@ df = r.to_frame() # todos los candidatos evaluados
898
909
 
899
910
  ---
900
911
 
912
+ ---
913
+
914
+ ### ABC, XYZ y ABC-XYZ — clasificación de artículos
915
+
916
+ `abc_analysis` clasifica por **valor anual** (Pareto): un artículo es A mientras el acumulado *previo* a él sea
917
+ menor que `a_threshold` (80 %), B mientras sea menor que `b_threshold` (95 %) y C en el resto. Los artículos
918
+ sin demanda quedan en C. `xyz_analysis` clasifica por **variabilidad** (coeficiente de variación): X si
919
+ CV ≤ `x_threshold` (0.5), Y hasta `y_threshold` (1.0) y Z por encima. `abc_xyz` combina ambas y devuelve la
920
+ matriz 3×3.
921
+
922
+ ```python
923
+ items = [
924
+ {"name": "A", "demand": 500, "unit_value": 10.0, "demand_std": 120},
925
+ {"name": "B", "demand": 300, "unit_value": 10.0, "demand_std": 60},
926
+ {"name": "C", "demand": 100, "unit_value": 10.0, "demand_std": 150},
927
+ {"name": "D", "demand": 700, "unit_value": 1.0, "demand_std": 300},
928
+ {"name": "E", "demand": 300, "unit_value": 1.0, "demand_std": 10},
929
+ ]
930
+
931
+ abc = wl.abc_analysis(items)
932
+ print(abc) # resumen por clase: A = 2 artículos (80 % del valor)
933
+ print(abc.to_frame()[["artículo", "valor_anual", "pct_acumulado", "clase"]])
934
+
935
+ # XYZ: cada artículo aporta 'cv' directamente o 'demand_std' + 'demand_mean' (o 'demand_rate')
936
+ xyz = wl.xyz_analysis([
937
+ {"name": i["name"], "demand_std": i["demand_std"], "demand_mean": i["demand"]} for i in items
938
+ ])
939
+ print(xyz.to_frame()) # columnas: artículo, cv, clase
940
+
941
+ # ABC-XYZ: matriz de conteos (filas ABC, columnas XYZ)
942
+ combo = wl.abc_xyz([{**i, "cv": i["demand_std"] / i["demand"]} for i in items])
943
+ print(combo.matrix_frame())
944
+ print(combo.items[0]["combined_class"]) # 'AX'
945
+ ```
946
+
947
+ | Parámetro | Descripción |
948
+ |---|---|
949
+ | `items` | Lista de diccionarios. ABC: `demand` (≥ 0) y `unit_value` (> 0). XYZ: `cv` o `demand_std` + `demand_mean`/`demand_rate` |
950
+ | `a_threshold`, `b_threshold` | Límites acumulados de las clases A y B (0 < a < b < 1) |
951
+ | `x_threshold`, `y_threshold` | Límites de CV de las clases X e Y |
952
+
953
+ Errores: lista vacía, elementos que no son diccionarios, claves faltantes y valores no finitos lanzan `ValueError`/`TypeError`
954
+ con el elemento y la clave afectados; si todas las demandas son 0 no hay nada que clasificar y se lanza `ValueError`.
955
+
956
+ ---
957
+
958
+ ### MRP de un nivel — `mrp`
959
+
960
+ Plan de requerimientos de materiales para un artículo: necesidades netas, órdenes planificadas (recepción y
961
+ liberación) y existencias proyectadas. Admite lote por lote (`"LFL"`) o lote fijo, tiempo de entrega,
962
+ stock de seguridad y recepciones programadas.
963
+
964
+ ```python
965
+ r = wl.mrp(
966
+ gross_requirements=[0, 50, 0, 30, 60, 20],
967
+ initial_on_hand=40,
968
+ scheduled_receipts=[0, 0, 25, 0, 0, 0],
969
+ lead_time=1,
970
+ lot_size=40, # "LFL" para lote por lote
971
+ safety_stock=10,
972
+ item_name="Eje",
973
+ )
974
+ print(r) # tabla por periodo: GR, SR, OH, NR, PR, PO
975
+ print(r.to_frame()) # el mismo plan como DataFrame
976
+ print(r.past_due_releases) # 0.0
977
+ ```
978
+
979
+ Si el `lead_time` es mayor que el periodo en que se necesita la orden, la liberación cae antes del periodo 1:
980
+ `mrp` emite un `UserWarning` y acumula esa cantidad en `past_due_releases` (no aparece en `planned_releases`).
981
+
982
+ ---
983
+
901
984
  ## 8. Análisis de operaciones
902
985
 
903
986
  ### `oee(availability, performance, quality)` — OEE
904
987
 
905
- Calcula la **Eficiencia Global de los Equipos** (OEE = Availability × Performance × Quality).
988
+ Calcula la **Eficiencia Global de los Equipos** (OEE = Disponibilidad × Rendimiento × Calidad).
906
989
 
907
990
  ```python
908
991
  r = wl.oee(
@@ -917,21 +1000,21 @@ print(r)
917
1000
  r.plot() # gráfica horizontal con referencia world-class (85%)
918
1001
  ```
919
1002
 
920
- ### `utilization_efficiency(actual_output, max_output)` — Utilización
1003
+ ### `utilization_efficiency(actual_output, capacity)` — Utilización
921
1004
 
922
1005
  ```python
923
- r = wl.utilization_efficiency(actual_output=750, max_output=1000)
1006
+ r = wl.utilization_efficiency(actual_output=750, capacity=1000)
924
1007
  print(r.utilization) # 0.75 — 75% de utilización
925
- print(r.idle_fraction) # 0.25 — 25% capacidad ociosa
1008
+ print(1 - r.utilization) # 0.25 — 25% de capacidad ociosa
926
1009
  ```
927
1010
 
928
- ### `unit_cost(fixed_cost, variable_cost, units)` — Costo unitario
1011
+ ### `unit_cost(fixed_cost, variable_cost_per_unit, units_produced)` — Costo unitario
929
1012
 
930
1013
  ```python
931
- r = wl.unit_cost(fixed_cost=10_000, variable_cost=5_000, units=500)
932
- print(r.unit_cost) # 30.0 $/unidad
933
- print(r.fixed_cost_per_unit) # 20.0
934
- print(r.var_cost_per_unit) # 10.0
1014
+ r = wl.unit_cost(fixed_cost=10_000, variable_cost_per_unit=10, units_produced=500)
1015
+ print(r.unit_cost) # 30.0 $/unidad
1016
+ print(r.fixed_cost_per_unit) # 20.0
1017
+ print(r.variable_cost_per_unit) # 10.0
935
1018
  ```
936
1019
 
937
1020
  ---
@@ -949,7 +1032,7 @@ r = wl.bottleneck_analysis(
949
1032
  demand_rate=70.0, # demanda requerida
950
1033
  )
951
1034
  print(r)
952
- # Bottleneck: Soldadura (capacidad 80 < demanda 70 con menor margen)
1035
+ # Cuello de botella: Soldadura (capacidad 80 < demanda 70 con menor margen)
953
1036
  # Utilization: Corte=58.3%, Soldadura=87.5%, Pintura=70.0%
954
1037
 
955
1038
  r.plot() # barras de utilización con cuello de botella en rojo
@@ -968,19 +1051,19 @@ takt = wl.takt_time(available_time=480, demand=60)
968
1051
  print(takt) # 8.0 min/unidad
969
1052
  ```
970
1053
 
971
- ### `line_balance(names, cycle_times, takt)` — Balance de línea
1054
+ ### `line_balance(station_names, cycle_times, takt)` — Balance de línea
972
1055
 
973
1056
  Analiza si cada estación puede cumplir con el tiempo takt y calcula la eficiencia de balance.
974
1057
 
975
1058
  ```python
976
1059
  lb = wl.line_balance(
977
- names=["A", "B", "C"],
1060
+ station_names=["A", "B", "C"],
978
1061
  cycle_times=[5.0, 9.0, 4.0],
979
1062
  takt=10.0,
980
1063
  )
981
1064
  print(lb.bottleneck) # "B" — estación más lenta
982
- print(lb.balance_efficiency) # 0.60 — 60% de eficiencia
983
- print(lb.total_idle_time) # tiempo ocioso total
1065
+ print(lb.balance_efficiency) # eficiencia de balance de la línea
1066
+ print(lb.theoretical_min_stations) # número mínimo teórico de estaciones
984
1067
 
985
1068
  lb.plot() # barras de cycle time vs takt
986
1069
  ```
@@ -1000,8 +1083,8 @@ be = wl.break_even(
1000
1083
  )
1001
1084
  print(be.bep_units) # 1000.0 — punto de equilibrio en unidades
1002
1085
  print(be.bep_revenue) # 25000.0 — ingresos en el punto de equilibrio
1003
- print(be.margin_of_safety_pct) # 33.3% — margen de seguridad sobre ventas actuales
1004
- print(be.profit) # 5000.0 — beneficio con actual_units
1086
+ print(be.margin_of_safety_pct) # 0.333 — margen de seguridad (fracción) sobre ventas actuales
1087
+ print(be.margin_of_safety_units) # 500.0 — unidades por encima del punto de equilibrio
1005
1088
 
1006
1089
  be.plot() # líneas de ingresos/costos con BEP y zona de beneficio
1007
1090
  ```
@@ -1024,7 +1107,7 @@ print(r.bep_units_total) # unidades totales en el punto de equilibrio
1024
1107
  print(r.bep_revenue_total) # ingresos totales en el punto de equilibrio
1025
1108
 
1026
1109
  for item in r.items:
1027
- print(item["Name"], item["BEP units"])
1110
+ print(item["Producto"], item["PE unidades"])
1028
1111
 
1029
1112
  r.to_frame() # DataFrame con una fila por producto
1030
1113
  ```
@@ -1058,577 +1141,668 @@ print(r.margin_of_safety_pct) # 0.375 — porcentaje del margen de seguridad
1058
1141
 
1059
1142
  ---
1060
1143
 
1061
- ## 15. Árboles de KPI
1144
+ ## 12. Curvas de intercambio
1062
1145
 
1063
- Los árboles de KPI crean jerarquías interactivas visualizadas como **treemap** o **sunburst** con Plotly.
1146
+ Herramientas para tomar decisiones de política agregada en familias de artículos.
1064
1147
 
1065
- ### `oee_kpi_tree(availability, performance, quality)`
1148
+ ### Tipo 1 — ciclo: `exchange_curve`
1149
+
1150
+ Traza la hipérbola entre número de pedidos por año (N) e inversión promedio en inventario de ciclo (I):
1066
1151
 
1067
- ```python
1068
- tree = wl.oee_kpi_tree(0.90, 0.80, 0.95)
1069
- tree.plot() # treemap (por defecto)
1070
- tree.plot(kind="sunburst") # diagrama radial
1152
+ ```
1153
+ N(k) = N* / k I(k) = k · I* → N · I = N* · I* (constante)
1071
1154
  ```
1072
1155
 
1073
- ### `throughput_kpi_tree(actual_throughput, capacity, defect_rate)`
1156
+ Dado un target de N o de I, resuelve el multiplicador k óptimo y recalcula Q para cada artículo.
1074
1157
 
1075
1158
  ```python
1076
- tree = wl.throughput_kpi_tree(
1077
- actual_throughput=750,
1078
- capacity=1000,
1079
- defect_rate=0.05,
1080
- )
1081
- tree.plot()
1082
- ```
1159
+ import walopy as wl
1083
1160
 
1084
- ### `roi_kpi_tree(revenue, fixed_cost, variable_cost_per_unit, units_sold, investment)`
1161
+ items = [
1162
+ {"name": "A", "demand": 1000, "ordering_cost": 50, "holding_cost": 2, "unit_value": 10},
1163
+ {"name": "B", "demand": 500, "ordering_cost": 30, "holding_cost": 1, "unit_value": 5},
1164
+ {"name": "C", "demand": 2000, "ordering_cost": 100, "holding_cost": 4, "unit_value": 8},
1165
+ ]
1085
1166
 
1086
- ```python
1087
- tree = wl.roi_kpi_tree(
1088
- revenue=50_000,
1089
- fixed_cost=10_000,
1090
- variable_cost_per_unit=8,
1091
- units_sold=2_000,
1092
- investment=20_000,
1093
- )
1094
- print(tree.value) # ROI = 0.2 (20%)
1095
- tree.plot()
1167
+ # Sin target → punto EOQ (k = 1)
1168
+ r = wl.exchange_curve(items)
1169
+ print(r)
1096
1170
  ```
1097
1171
 
1098
- ### `KPINode` — árbol personalizado
1172
+ ```
1173
+ Objetivo : eoq
1174
+ Multiplicador k : 1.0000
1175
+ N pedidos/año (EOQ) : 87.27
1176
+ N pedidos/año (óptimo) : 87.27
1177
+ Inversión (EOQ) : 5430
1178
+ Inversión (óptimo) : 5430
1179
+ ```
1099
1180
 
1100
1181
  ```python
1101
- root = wl.KPINode("EBITDA", value=100_000, unit="€")
1102
- rev = root.add_child("Revenue", value=200_000, unit="€")
1103
- cost = root.add_child("Operating costs", value=100_000, unit="€")
1104
- rev.add_child("Product A", value=120_000, unit="€")
1105
- rev.add_child("Product B", value=80_000, unit="€")
1182
+ # Reducir a 20 pedidos/año (k > 1 → Q más grandes → más inversión)
1183
+ r = wl.exchange_curve(items, target_orders=20)
1106
1184
 
1107
- from walopy.plotting import plot_kpi_tree
1108
- fig = plot_kpi_tree(root, kind="treemap")
1109
- fig.show()
1185
+ # Limitar inversión a 3 000 (k < 1 → Q más pequeños → más pedidos)
1186
+ r = wl.exchange_curve(items, target_investment=3000)
1187
+
1188
+ # Tabla por artículo
1189
+ df = r.to_frame()
1190
+ # columnas: artículo | Q_eoq | Q_óptima | n_pedidos | inversión
1191
+
1192
+ # Hipérbola para graficar
1193
+ curve = r.curve_to_frame()
1194
+ # columnas: N | I
1110
1195
  ```
1111
1196
 
1197
+ **Parámetros de cada artículo**
1198
+
1199
+ | Clave | Requerido | Descripción |
1200
+ |-------|-----------|-------------|
1201
+ | `demand` | ✓ | Demanda anual Di |
1202
+ | `ordering_cost` | ✓ | Costo de pedido Ki |
1203
+ | `holding_cost` | ✓ | Costo de mantenimiento hi |
1204
+ | `unit_value` | — | Valor unitario vi (default 1) |
1205
+ | `name` | — | Etiqueta (default I1, I2, …) |
1206
+
1112
1207
  ---
1113
1208
 
1114
- ## 16. Solvers y optimización
1209
+ ### Tipo 2 — seguridad: `safety_stock_curve`
1115
1210
 
1116
- ### `solve_lam(metric, target, *, mu, model)` — Máxima tasa de llegada
1211
+ Traza la curva entre inversión en stock de seguridad e nivel de servicio por ciclo usando una **política z común** para toda la familia:
1117
1212
 
1118
- Encuentra la mayor λ tal que una métrica no supere el objetivo.
1213
+ ```
1214
+ SS_i(z) = z · σᵢ_DLT · vᵢ σᵢ_DLT = √(Lᵢ·σ²_Di + D²ᵢ·σ²_Li)
1215
+ SS_total(z) = z · Σ σᵢ_DLT · vᵢ SL(z) = Φ(z)
1216
+ ```
1119
1217
 
1120
1218
  ```python
1121
- r = wl.solve_lam("Wq", target=0.5, mu=5.0, model="mm1")
1122
- print(r.value) # λ máxima ≈ 3.33
1123
- print(r.achieved_value) # Wq ≈ 0.5
1124
- print(r.model_result) # QueueResult completo en la solución
1219
+ items = [
1220
+ {"name": "A", "demand_rate": 100, "demand_std": 10, "lead_time": 2, "unit_value": 10},
1221
+ {"name": "B", "demand_rate": 50, "demand_std": 5, "lead_time": 1, "unit_value": 5},
1222
+ {"name": "C", "demand_rate": 200, "demand_std": 20, "lead_time": 3, "unit_value": 8},
1223
+ ]
1224
+
1225
+ # Target: nivel de servicio 95%
1226
+ r = wl.safety_stock_curve(items, target_service_level=0.95)
1227
+ print(r)
1125
1228
  ```
1126
1229
 
1127
- ### `solve_mu(metric, target, *, lam, model)` — Mínima tasa de servicio
1230
+ ```
1231
+ Objetivo : nivel_de_servicio=95.0000%
1232
+ z : 1.6449
1233
+ Nivel de servicio : 95.0000%
1234
+ Inversión en SS : 1 847
1235
+ ```
1128
1236
 
1129
1237
  ```python
1130
- r = wl.solve_mu("Wq", target=0.3, lam=3.0, model="mm1")
1131
- print(r.value) # μ mínima necesaria ≈ 5.0
1132
- ```
1238
+ # Target: presupuesto máximo de SS
1239
+ r = wl.safety_stock_curve(items, target_ss_investment=1000)
1133
1240
 
1134
- ### `solve_servers(metric, target, *, lam, mu)` — Mínimo número de servidores
1241
+ # Tabla por artículo (incluye punto de reorden cuando se da demand_rate)
1242
+ df = r.to_frame()
1243
+ # columnas: artículo | sigma_dlt | stock_de_seguridad | inversión | punto_de_reorden
1135
1244
 
1136
- ```python
1137
- r = wl.solve_servers("Wq", target=0.1, lam=8.0, mu=5.0)
1138
- print(r.value) # c mínimo = 3
1139
- print(r.model_result) # QueueResult para M/M/3
1245
+ # Curva completa (z de −2 a 4)
1246
+ curve = r.curve_to_frame()
1247
+ # columnas: z | service_level | ss_investment
1140
1248
  ```
1141
1249
 
1142
- ### `optimize_servers(lam, mu, cost_per_server, cost_per_wait)` — Optimización de costo total
1250
+ **Parámetros de cada artículo**
1143
1251
 
1144
- Minimiza cost_per_server × c + cost_per_wait × λ × Wq evaluando todos los c viables.
1252
+ | Clave | Requerido | Descripción |
1253
+ |-------|-----------|-------------|
1254
+ | `demand_std` | ✓ | Desviación estándar de la demanda |
1255
+ | `lead_time` | ✓ | Lead time medio |
1256
+ | `demand_rate` | — | Demanda media; activa columna `reorder_point` |
1257
+ | `lead_time_std` | — | Desviación estándar del lead time (default 0) |
1258
+ | `unit_value` | — | Valor unitario (default 1) |
1259
+ | `name` | — | Etiqueta (default I1, I2, …) |
1145
1260
 
1146
- ```python
1147
- r = wl.optimize_servers(
1148
- lam=6.0,
1149
- mu=5.0,
1150
- cost_per_server=10.0, # costo fijo por servidor por unidad de tiempo
1151
- cost_per_wait=5.0, # costo por unidad de tiempo de espera de cada cliente
1152
- )
1153
- print(r.optimal_servers) # c óptimo
1154
- print(r.optimal_cost) # costo mínimo
1261
+ ---
1155
1262
 
1156
- r.plot() # gráfica de costo vs número de servidores
1157
- ```
1263
+ ### Relación entre ambas curvas
1264
+
1265
+ | | Tipo 1 (ciclo) | Tipo 2 (seguridad) |
1266
+ |---|---|---|
1267
+ | **Función** | `exchange_curve` | `safety_stock_curve` |
1268
+ | **Decide** | Tamaño de lote Q | Punto de reorden r |
1269
+ | **Tradeoff** | N pedidos/año ↔ Inversión ciclo | Nivel de servicio ↔ SS |
1270
+ | **Resultado** | `ExchangeCurveResult` | `SafetyStockCurveResult` |
1271
+ | **Parámetro** | Multiplicador k | Factor z |
1272
+
1273
+ Usadas juntas cubren la política completa `(Q, r)` para toda la familia.
1158
1274
 
1159
1275
  ---
1160
1276
 
1161
- ## 17. Análisis de sensibilidad
1277
+ ## 13. Programación de producción (scheduling)
1162
1278
 
1163
- ### `sensitivity(model_fn, param, values, **fixed_kwargs)` — Barrido paramétrico
1279
+ ### Máquina única — `schedule_single`
1164
1280
 
1165
- Evalúa un modelo sobre un rango de valores de un parámetro.
1281
+ Asigna *n* trabajos a una sola máquina según una regla de prioridad.
1166
1282
 
1167
1283
  ```python
1168
- import numpy as np
1169
-
1170
- df = wl.sensitivity(
1171
- wl.mm1,
1172
- param="lam",
1173
- values=np.linspace(0.5, 4.5, 20),
1174
- mu=5.0, # parámetros fijos
1175
- )
1176
- print(df.columns.tolist()) # ['lam', 'rho', 'L', 'Lq', 'W', 'Wq', ...]
1284
+ import walopy as wl
1177
1285
 
1178
- # Barrido sobre μ
1179
- df2 = wl.sensitivity(wl.mm1, "mu", np.linspace(4.0, 10.0, 15), lam=3.0)
1286
+ p = [3, 1, 4, 1, 5] # tiempos de proceso
1287
+ d = [8, 3, 10, 4, 12] # fechas de entrega
1288
+ w = [2, 5, 1, 4, 1] # pesos de importancia
1180
1289
 
1181
- # Barrido sobre número de servidores en M/M/c
1182
- df3 = wl.sensitivity(wl.mmc, "c", range(1, 6), lam=8.0, mu=5.0)
1290
+ r = wl.schedule_single(p, rule="SPT", due_dates=d, weights=w,
1291
+ names=["A","B","C","D","E"])
1292
+ print(r)
1293
+ ```
1183
1294
 
1184
- # Visualizar
1185
- from walopy.plotting import plot_sensitivity
1186
- fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq"])
1187
- fig.show()
1295
+ ```
1296
+ Regla : SPT
1297
+ Secuencia : B → D → A → C → E
1298
+ Makespan (Cmax) : 14
1299
+ Finalización total ΣCj : 37
1300
+ Finalización pond. ΣwCj : 89
1301
+ Retraso máximo : 2
1302
+ Tardanza total ΣTj : 2
1303
+ Trabajos con retraso : 1
1188
1304
  ```
1189
1305
 
1190
- ---
1306
+ **Reglas disponibles**
1191
1307
 
1192
- ## 18. Escenarios en lote (batch_model)
1308
+ | `rule` | Criterio minimizado | Datos requeridos |
1309
+ |--------|---------------------|-----------------|
1310
+ | `'SPT'` | ΣCj (tiempo total de completación) | — |
1311
+ | `'EDD'` | Lmax (máxima tardanza) | `due_dates` |
1312
+ | `'WSPT'` | ΣwjCj (completación ponderada) | `weights` |
1313
+ | `'CR'` | Ratio crítico dj/pj | `due_dates` |
1314
+ | `'FIFO'` | Orden de llegada (línea base) | — |
1193
1315
 
1194
- ### `batch_model(model_fn, df, **fixed_kwargs)` — Aplicar modelo a un DataFrame
1316
+ **Atributos de `ScheduleResult`**
1195
1317
 
1196
- Aplica cualquier función de walopy a cada fila de un DataFrame de escenarios.
1318
+ | Atributo | Descripción |
1319
+ |----------|-------------|
1320
+ | `rule` | Regla utilizada |
1321
+ | `sequence` | Lista de nombres en orden de procesamiento |
1322
+ | `jobs` | Lista de `JobSchedule` (por trabajo) |
1323
+ | `makespan` | Cmax = suma de tiempos de proceso |
1324
+ | `total_completion_time` | ΣCj |
1325
+ | `total_weighted_completion_time` | ΣwjCj |
1326
+ | `max_lateness` | max(Lj) |
1327
+ | `total_tardiness` | ΣTj |
1328
+ | `n_tardy` | Número de trabajos tarde |
1197
1329
 
1198
1330
  ```python
1199
- import pandas as pd
1331
+ df = r.to_frame() # columnas: Trabajo, p, d, w, Inicio, C, L, T, Con retraso
1332
+ ```
1200
1333
 
1201
- # Varios escenarios con distintas tasas de llegada
1202
- escenarios = pd.DataFrame({
1203
- "lam": [1.0, 2.0, 3.0, 4.0]
1204
- })
1205
- resultado = wl.batch_model(wl.mm1, escenarios, mu=5.0)
1206
- print(resultado[["lam", "rho", "Wq", "L"]])
1334
+ ---
1207
1335
 
1208
- # Escenarios mixtos (lambda y mu variables)
1209
- escenarios2 = pd.DataFrame({
1210
- "lam": [2.0, 3.0, 4.0],
1211
- "mu": [5.0, 6.0, 7.0],
1212
- })
1213
- resultado2 = wl.batch_model(wl.mm1, escenarios2)
1336
+ ### Flow-shop de 2 máquinas — `johnson_flowshop`
1214
1337
 
1215
- # Con número de servidores variable (columna int)
1216
- escenarios3 = pd.DataFrame({
1217
- "c": [1, 2, 3, 4],
1218
- })
1219
- resultado3 = wl.batch_model(wl.mmc, escenarios3, lam=8.0, mu=5.0)
1338
+ El algoritmo de Johnson encuentra la secuencia óptima que minimiza el makespan cuando todos los trabajos pasan primero por M1 y luego por M2.
1339
+
1340
+ ```python
1341
+ m1 = [3, 8, 5, 7, 2] # tiempos en máquina 1
1342
+ m2 = [5, 2, 8, 4, 6] # tiempos en máquina 2
1343
+
1344
+ r = wl.johnson_flowshop(m1, m2)
1345
+ print(r)
1346
+ ```
1347
+
1348
+ ```
1349
+ Secuencia : J1 → J5 → J4 → J3 → J2
1350
+ Makespan : 34
1351
+ ```
1220
1352
 
1221
- # Las filas con error quedan marcadas en "_error"
1222
- print(resultado[resultado["_error"].notna()]) # filas fallidas
1353
+ ```python
1354
+ df = r.to_frame()
1355
+ # columnas: Job | M1 start | M1 end | M2 start | M2 end
1223
1356
  ```
1224
1357
 
1225
1358
  ---
1226
1359
 
1227
- ## 19. Comparar múltiples resultados
1360
+ ### Flow-shop de m máquinas — `neh_flowshop`
1228
1361
 
1229
- ### `compare(*results, labels)` — DataFrame comparativo
1230
-
1231
- Construye un DataFrame con una fila por resultado para facilitar la comparación.
1362
+ Heurística de **Nawaz-Enscore-Ham** para el flow-shop de permutación con cualquier número de máquinas:
1363
+ ordena los trabajos por tiempo total decreciente e inserta cada uno en la mejor posición (aceleración de
1364
+ Taillard: O(n²·m)). Es una heurística: en casos pequeños el makespan queda típicamente a pocos puntos
1365
+ porcentuales del óptimo.
1232
1366
 
1233
1367
  ```python
1234
- df = wl.compare(
1235
- wl.mm1(3.0, 5.0),
1236
- wl.mmc(3.0, 5.0, 2),
1237
- wl.md1(3.0, 5.0),
1238
- labels=["M/M/1", "M/M/2", "M/D/1"],
1239
- )
1240
- print(df[["label", "ρ (utilization)", "Wq (wait time)", "L (system)"]])
1241
- # label ρ (utilization) Wq (wait time) L (system)
1242
- # 0 M/M/1 0.6 0.3000 1.50
1243
- # 1 M/M/2 0.3 0.0302 1.02
1244
- # 2 M/D/1 0.6 0.1500 1.05
1245
-
1246
- # Sin etiquetas → scenario_1, scenario_2, ...
1247
- df2 = wl.compare(wl.mm1(1, 5), wl.mm1(2, 5), wl.mm1(3, 5))
1368
+ # filas = trabajos, columnas = máquinas
1369
+ tiempos = [[5, 9, 8], [9, 3, 10], [9, 4, 5], [4, 8, 8]]
1370
+ r = wl.neh_flowshop(tiempos, names=["P1", "P2", "P3", "P4"])
1371
+ print(r) # secuencia y makespan
1372
+ print(r.to_frame()) # Gantt: columnas M1_inicio, M1_fin, M2_inicio, ...
1373
+ print(r.sequence, r.makespan)
1248
1374
  ```
1249
1375
 
1250
1376
  ---
1251
1377
 
1252
- ## 20. Gráficas
1378
+ ## 14. Confiabilidad — `reliability`
1253
1379
 
1254
- Todos los resultados exponen `.plot()` que devuelve una figura de Matplotlib o Plotly. También se pueden llamar directamente desde `walopy.plotting`.
1380
+ Modelos de confiabilidad con distribución exponencial (tasa de falla constante).
1255
1381
 
1256
- | Función `.plot()` | Tipo | Descripción |
1257
- |---|---|---|
1258
- | `QueueResult.plot()` | Matplotlib | Curvas Wq y Lq vs ρ con punto operativo |
1259
- | `OEEResult.plot()` | Matplotlib | Barras horizontales OEE con referencia 85% |
1260
- | `BottleneckResult.plot()` | Matplotlib | Utilización por estación, cuello de botella en rojo |
1261
- | `EOQResult.plot()` | Matplotlib | Curvas de costo de mantener, ordenar y total |
1262
- | `LineBalanceResult.plot()` | Plotly | Cycle time vs takt por estación |
1263
- | `BreakEvenResult.plot()` | Plotly | Líneas ingreso/costo con BEP y zona de beneficio |
1264
- | `SimulationResult.plot()` | Plotly | Histograma de Wq + CDF empírica con percentiles |
1265
- | `OptimizeResult.plot()` | Plotly | Costo por servidor, costo de espera y total vs c |
1266
- | `KPINode.plot(kind=)` | Plotly | Treemap o sunburst interactivo |
1382
+ ### Componente individual — `mtbf_analysis`
1267
1383
 
1268
1384
  ```python
1269
- # Guardar como HTML interactivo (Plotly)
1270
- fig = r.plot()
1271
- fig.write_html("resultado.html")
1385
+ r = wl.mtbf_analysis(failure_rate=0.01, mttr=5.0, t=100)
1386
+ print(r)
1387
+ ```
1272
1388
 
1273
- # Guardar como imagen (Matplotlib)
1274
- fig = wl.oee(0.9, 0.8, 0.95).plot()
1275
- fig.savefig("oee.png", dpi=150)
1389
+ ```
1390
+ Topología : component
1391
+ Componentes : 1
1392
+ Tasas de falla λ: ['0.01']
1393
+ MTBF : 100
1394
+ R(t=100) : 0.367879
1395
+ Disponibilidad : 95.2381%
1276
1396
  ```
1277
1397
 
1278
- Para la curva de sensibilidad:
1398
+ ### Sistema en serie — `series_system`
1399
+
1400
+ El sistema falla si **cualquier** componente falla. λ_sys = Σλi.
1401
+
1279
1402
  ```python
1280
- from walopy.plotting import plot_sensitivity
1281
- df = wl.sensitivity(wl.mm1, "lam", np.linspace(0.5, 4.5, 20), mu=5.0)
1282
- fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq", "L"])
1283
- fig.show()
1403
+ r = wl.series_system([0.01, 0.02, 0.03], t=10)
1404
+ print(r.mtbf) # 1/0.06 ≈ 16.67
1405
+ print(r.R_t) # e^{-0.06·10} ≈ 0.549
1284
1406
  ```
1285
1407
 
1286
- ---
1287
-
1288
- ## 21. CLI
1408
+ ### Sistema en paralelo — `parallel_system`
1289
1409
 
1290
- walopy incluye una interfaz de línea de comandos para uso rápido sin escribir código.
1410
+ El sistema opera si **al menos uno** de los componentes opera.
1411
+ R_sys(t) = 1 − ∏(1 − e^{−λi·t}). MTBF calculado numéricamente.
1291
1412
 
1292
- ```bash
1293
- # M/M/1
1294
- python -m walopy mm1 --lam 3 --mu 5
1413
+ ```python
1414
+ r = wl.parallel_system([0.01, 0.02], t=50)
1415
+ print(r.R_t) # mayor que el sistema en serie
1416
+ print(r.mtbf) # mayor que cualquier componente individual
1417
+ ```
1295
1418
 
1296
- # M/M/c
1297
- python -m walopy mmc --lam 8 --mu 5 --c 2
1419
+ ### Sistema k-de-n — `koon_system`
1298
1420
 
1299
- # M/D/1
1300
- python -m walopy md1 --lam 3 --mu 5
1421
+ Opera si al menos **k** de **n** componentes idénticos están en servicio.
1422
+ MTBF exacto = (1/λ) · Σ_{j=k}^{n} (1/j).
1301
1423
 
1302
- # G/G/1 (Kingman)
1303
- python -m walopy gg1 --lam 3 --mu 5 --ca2 1.2 --cs2 0.8
1424
+ ```python
1425
+ # 2-de-3: requiere al menos 2 de 3 componentes idénticos
1426
+ r = wl.koon_system(n=3, k=2, failure_rate=0.01, t=50)
1427
+ print(r.mtbf) # (1/0.01) * (1/2 + 1/3) ≈ 83.33
1428
+ print(r.R_t)
1429
+ ```
1304
1430
 
1305
- # Ley de Little (resolver W)
1306
- python -m walopy littles --L 2.5 --lam 5.0
1431
+ **Casos especiales**
1432
+ - `k=1` → equivale a `parallel_system` con n componentes idénticos.
1433
+ - `k=n` → equivale a `series_system` con n componentes idénticos.
1307
1434
 
1308
- # EOQ
1309
- python -m walopy eoq --demand 1000 --ordering 50 --holding 2
1435
+ **Atributos de `ReliabilityResult`**
1310
1436
 
1311
- # Versión
1312
- python -m walopy --version
1313
- ```
1437
+ | Atributo | Descripción |
1438
+ |----------|-------------|
1439
+ | `topology` | `'component'`, `'series'`, `'parallel'`, `'k-of-n'` |
1440
+ | `n_components` | Número de componentes |
1441
+ | `failure_rates` | Lista de λi |
1442
+ | `mtbf` | MTBF del sistema |
1443
+ | `R_t` | Confiabilidad R(t) si se proporcionó `t` |
1444
+ | `availability` | Disponibilidad estacionaria A si se proporcionó `mttr` |
1445
+ | `mttr` | Tiempo medio de reparación (si se proporcionó) |
1314
1446
 
1315
- Ejemplo de salida:
1316
- ```
1317
- Model : M/M/1
1318
- λ : 3.0 (arrival rate)
1319
- μ : 5.0 (service rate per server)
1320
- ρ : 0.6 (utilization)
1321
- L : 1.5 (avg units in system)
1322
- Lq : 0.9 (avg units in queue)
1323
- W : 0.5 (avg time in system)
1324
- Wq : 0.3 (avg wait time in queue)
1325
- P0 : 0.4
1447
+ ```python
1448
+ df = r.to_frame() # columnas: Topología, Componentes, MTBF[, R(t), Disponibilidad]
1449
+ r.R(t=200) # evalúa R(t) en cualquier tiempo posterior
1326
1450
  ```
1327
1451
 
1328
1452
  ---
1329
1453
 
1330
- ## 12. Curvas de intercambio
1331
-
1332
- Herramientas para tomar decisiones de política agregada en familias de artículos.
1454
+ ### Distribución de Weibull — `weibull_analysis`
1333
1455
 
1334
- ### Tipo 1 — ciclo: `exchange_curve`
1456
+ Ajusta una Weibull de dos parámetros (forma β, escala η) a tiempos de falla completos por **máxima
1457
+ verosimilitud** (`method="MLE"`, bisección sobre la ecuación de verosimilitud) o por **regresión de rangos**
1458
+ (`method="RRY"`, rangos medianos de Benard). β < 1 indica mortalidad infantil, β ≈ 1 fallas aleatorias y
1459
+ β > 1 desgaste.
1335
1460
 
1336
- Traza la hipérbola entre número de pedidos por año (N) e inversión promedio en inventario de ciclo (I):
1461
+ ```python
1462
+ tiempos = [12.5, 18.3, 24.1, 31.7, 38.2, 45.9, 52.4, 61.0, 70.8, 85.3]
1463
+ w = wl.weibull_analysis(tiempos)
1464
+ print(w) # β ≈ 2.10, η ≈ 49.86, MTTF ≈ 44.16
1465
+ print(w.R(30), w.F(30)) # confiabilidad y probabilidad de falla a t = 30
1466
+ print(w.h(30)) # tasa de falla instantánea
1467
+ print(w.b10, w.b50, w.b_life(5)) # vidas B10, B50 y B5
1468
+ print(w.to_frame())
1337
1469
 
1338
- ```
1339
- N(k) = N* / k I(k) = k · I* → N · I = N* · I* (constante)
1470
+ w_rry = wl.weibull_analysis(tiempos, method="RRY")
1340
1471
  ```
1341
1472
 
1342
- Dado un target de N o de I, resuelve el multiplicador k óptimo y recalcula Q para cada artículo.
1473
+ Se necesitan al menos 2 tiempos finitos y positivos; si todos son iguales la forma no es estimable
1474
+ (`ValueError`) y, si casi no hay dispersión, se avisa (`UserWarning`) de que β alcanzó la cota superior de 100.
1343
1475
 
1344
- ```python
1345
- import walopy as wl
1476
+ ---
1346
1477
 
1347
- items = [
1348
- {"name": "A", "demand": 1000, "ordering_cost": 50, "holding_cost": 2, "unit_value": 10},
1349
- {"name": "B", "demand": 500, "ordering_cost": 30, "holding_cost": 1, "unit_value": 5},
1350
- {"name": "C", "demand": 2000, "ordering_cost": 100, "holding_cost": 4, "unit_value": 8},
1351
- ]
1478
+ ## 15. Proyectos: CPM y PERT — `project`
1352
1479
 
1353
- # Sin target → punto EOQ (k = 1)
1354
- r = wl.exchange_curve(items)
1355
- print(r)
1356
- ```
1480
+ Programación de proyectos sobre una red de actividades. Cada actividad es un diccionario con `name`,
1481
+ `predecessors` (lista de nombres, puede omitirse) y su duración. Los nombres deben ser únicos; los
1482
+ ciclos y los predecesores desconocidos lanzan `ValueError`.
1357
1483
 
1358
- ```
1359
- Target : eoq
1360
- Multiplier k : 1.0000
1361
- N orders/yr (EOQ) : 87.27
1362
- N orders/yr (optimal) : 87.27
1363
- Investment (EOQ) : 5430
1364
- Investment (optimal) : 5430
1365
- ```
1484
+ ### CPM — duraciones determinísticas
1485
+
1486
+ Paso hacia adelante (ES, EF) y hacia atrás (LS, LF); holgura total HT = LS − ES, holgura libre HL y ruta crítica (HT = 0).
1366
1487
 
1367
1488
  ```python
1368
- # Reducir a 20 pedidos/año (k > 1 → Q más grandes → más inversión)
1369
- r = wl.exchange_curve(items, target_orders=20)
1489
+ acts = [
1490
+ {"name": "A", "duration": 3, "predecessors": []},
1491
+ {"name": "B", "duration": 4, "predecessors": ["A"]},
1492
+ {"name": "C", "duration": 2, "predecessors": ["A"]},
1493
+ {"name": "D", "duration": 1, "predecessors": ["B", "C"]},
1494
+ ]
1495
+ r = wl.cpm(acts)
1496
+ print(r) # duración 8, ruta crítica A → B → D
1497
+ print(r.to_frame()) # ES, EF, LS, LF, HT, HL y si es crítica
1498
+ print(r.critical_path) # ['A', 'B', 'D']
1499
+ ```
1370
1500
 
1371
- # Limitar inversión a 3 000 (k < 1 → Q más pequeños → más pedidos)
1372
- r = wl.exchange_curve(items, target_investment=3000)
1501
+ ### PERT — tres estimaciones por actividad
1373
1502
 
1374
- # Tabla por artículo
1375
- df = r.to_frame()
1376
- # columnas: name | Q_eoq | Q_optimal | n_orders | investment
1503
+ Duración esperada tₑ = (a + 4m + b)/6 y varianza ((b − a)/6)²; la varianza del proyecto es la suma de las
1504
+ de la ruta crítica. `probability(T)` da P(duración ≤ T) con aproximación normal.
1377
1505
 
1378
- # Hipérbola para graficar
1379
- curve = r.curve_to_frame()
1380
- # columnas: N | I
1506
+ ```python
1507
+ acts = [
1508
+ {"name": "A", "optimistic": 1, "most_likely": 3, "pessimistic": 5, "predecessors": []},
1509
+ {"name": "B", "optimistic": 2, "most_likely": 4, "pessimistic": 9, "predecessors": ["A"]},
1510
+ ]
1511
+ p = wl.pert(acts)
1512
+ print(p) # duración esperada 7.5, σ ≈ 1.34
1513
+ print(p.probability(9.0)) # ≈ 0.868: probabilidad de terminar en 9 unidades o menos
1381
1514
  ```
1382
1515
 
1383
- **Parámetros de cada artículo**
1384
-
1385
- | Clave | Requerido | Descripción |
1386
- |-------|-----------|-------------|
1387
- | `demand` | ✓ | Demanda anual Di |
1388
- | `ordering_cost` | ✓ | Costo de pedido Ki |
1389
- | `holding_cost` | ✓ | Costo de mantenimiento hi |
1390
- | `unit_value` | — | Valor unitario vi (default 1) |
1391
- | `name` | — | Etiqueta (default I1, I2, …) |
1516
+ `probability` solo está disponible en resultados PERT con varianza distinta de cero.
1392
1517
 
1393
1518
  ---
1394
1519
 
1395
- ### Tipo 2 — seguridad: `safety_stock_curve`
1520
+ ## 16. Árboles de KPI
1396
1521
 
1397
- Traza la curva entre inversión en stock de seguridad e nivel de servicio por ciclo usando una **política z común** para toda la familia:
1522
+ Los árboles de KPI crean jerarquías interactivas visualizadas como **treemap** o **sunburst** con Plotly.
1398
1523
 
1399
- ```
1400
- SS_i(z) = z · σᵢ_DLT · vᵢ σᵢ_DLT = √(Lᵢ·σ²_Di + D²ᵢ·σ²_Li)
1401
- SS_total(z) = z · Σ σᵢ_DLT · vᵢ SL(z) = Φ(z)
1402
- ```
1524
+ ### `oee_kpi_tree(availability, performance, quality)`
1403
1525
 
1404
1526
  ```python
1405
- items = [
1406
- {"name": "A", "demand_rate": 100, "demand_std": 10, "lead_time": 2, "unit_value": 10},
1407
- {"name": "B", "demand_rate": 50, "demand_std": 5, "lead_time": 1, "unit_value": 5},
1408
- {"name": "C", "demand_rate": 200, "demand_std": 20, "lead_time": 3, "unit_value": 8},
1409
- ]
1410
-
1411
- # Target: nivel de servicio 95%
1412
- r = wl.safety_stock_curve(items, target_service_level=0.95)
1413
- print(r)
1527
+ tree = wl.oee_kpi_tree(0.90, 0.80, 0.95)
1528
+ tree.plot() # treemap (por defecto)
1529
+ tree.plot(kind="sunburst") # diagrama radial
1414
1530
  ```
1415
1531
 
1416
- ```
1417
- Target : service_level=95.0000%
1418
- z : 1.6449
1419
- Service level : 95.0000%
1420
- SS investment : 1 847
1421
- ```
1532
+ ### `throughput_kpi_tree(actual_throughput, capacity, defect_rate)`
1422
1533
 
1423
1534
  ```python
1424
- # Target: presupuesto máximo de SS
1425
- r = wl.safety_stock_curve(items, target_ss_investment=1000)
1535
+ tree = wl.throughput_kpi_tree(
1536
+ actual_throughput=750,
1537
+ capacity=1000,
1538
+ defect_rate=0.05,
1539
+ )
1540
+ tree.plot()
1541
+ ```
1426
1542
 
1427
- # Tabla por artículo (incluye punto de reorden cuando se da demand_rate)
1428
- df = r.to_frame()
1429
- # columnas: name | sigma_dlt | safety_stock | investment | reorder_point
1543
+ ### `roi_kpi_tree(revenue, fixed_cost, variable_cost_per_unit, units_sold, investment)`
1430
1544
 
1431
- # Curva completa (z de −2 a 4)
1432
- curve = r.curve_to_frame()
1433
- # columnas: z | service_level | ss_investment
1545
+ ```python
1546
+ tree = wl.roi_kpi_tree(
1547
+ revenue=50_000,
1548
+ fixed_cost=10_000,
1549
+ variable_cost_per_unit=8,
1550
+ units_sold=2_000,
1551
+ investment=20_000,
1552
+ )
1553
+ print(tree.value) # ROI = 0.2 (20%)
1554
+ tree.plot()
1434
1555
  ```
1435
1556
 
1436
- **Parámetros de cada artículo**
1557
+ ### `KPINode` — árbol personalizado
1437
1558
 
1438
- | Clave | Requerido | Descripción |
1439
- |-------|-----------|-------------|
1440
- | `demand_std` | ✓ | Desviación estándar de la demanda |
1441
- | `lead_time` | ✓ | Lead time medio |
1442
- | `demand_rate` | — | Demanda media; activa columna `reorder_point` |
1443
- | `lead_time_std` | — | Desviación estándar del lead time (default 0) |
1444
- | `unit_value` | — | Valor unitario (default 1) |
1445
- | `name` | — | Etiqueta (default I1, I2, …) |
1559
+ ```python
1560
+ rev = wl.KPINode("Revenue", value=200_000, unit="€", children=[
1561
+ wl.KPINode("Product A", value=120_000, unit="€"),
1562
+ wl.KPINode("Product B", value=80_000, unit="€"),
1563
+ ])
1564
+ cost = wl.KPINode("Operating costs", value=100_000, unit="€")
1565
+ root = wl.KPINode("EBITDA", value=100_000, unit="€", formula="Revenue − Operating costs",
1566
+ children=[rev, cost])
1567
+
1568
+ from walopy.plotting import plot_kpi_tree
1569
+ fig = plot_kpi_tree(root, kind="treemap")
1570
+ fig.show()
1571
+ ```
1446
1572
 
1447
1573
  ---
1448
1574
 
1449
- ### Relación entre ambas curvas
1575
+ ## 17. Solvers y optimización
1450
1576
 
1451
- | | Tipo 1 (ciclo) | Tipo 2 (seguridad) |
1452
- |---|---|---|
1453
- | **Función** | `exchange_curve` | `safety_stock_curve` |
1454
- | **Decide** | Tamaño de lote Q | Punto de reorden r |
1455
- | **Tradeoff** | N pedidos/año ↔ Inversión ciclo | Nivel de servicio ↔ SS |
1456
- | **Resultado** | `ExchangeCurveResult` | `SafetyStockCurveResult` |
1457
- | **Parámetro** | Multiplicador k | Factor z |
1577
+ ### `solve_lam(target_metric, target_value, *, mu, model)` — Máxima tasa de llegada
1458
1578
 
1459
- Usadas juntas cubren la política completa `(Q, r)` para toda la familia.
1579
+ Encuentra la mayor λ tal que una métrica no supere el objetivo.
1460
1580
 
1461
- ---
1581
+ ```python
1582
+ r = wl.solve_lam("Wq", 0.5, mu=5.0, model="mm1")
1583
+ print(r.value) # λ máxima ≈ 3.33
1584
+ print(r.achieved_value) # Wq ≈ 0.5
1585
+ print(r.model_result) # QueueResult completo en la solución
1586
+ ```
1462
1587
 
1463
- ## 13. Programación de producción (scheduling)
1588
+ ### `solve_mu(target_metric, target_value, *, lam, model)` — Mínima tasa de servicio
1464
1589
 
1465
- ### Máquina única — `schedule_single`
1590
+ ```python
1591
+ r = wl.solve_mu("Wq", 0.3, lam=3.0, model="mm1")
1592
+ print(r.value) # μ mínima necesaria ≈ 5.0
1593
+ ```
1466
1594
 
1467
- Asigna *n* trabajos a una sola máquina según una regla de prioridad.
1595
+ ### `solve_servers(target_metric, target_value, *, lam, mu)` — Mínimo número de servidores
1468
1596
 
1469
1597
  ```python
1470
- import walopy as wl
1598
+ r = wl.solve_servers("Wq", 0.1, lam=8.0, mu=5.0)
1599
+ print(r.value) # c mínimo = 3
1600
+ print(r.model_result) # QueueResult para M/M/3
1601
+ ```
1471
1602
 
1472
- p = [3, 1, 4, 1, 5] # tiempos de proceso
1473
- d = [8, 3, 10, 4, 12] # fechas de entrega
1474
- w = [2, 5, 1, 4, 1] # pesos de importancia
1603
+ ### `optimize_servers(lam, mu, cost_per_server, cost_per_wait)` — Optimización de costo total
1475
1604
 
1476
- r = wl.schedule_single(p, rule="SPT", due_dates=d, weights=w,
1477
- names=["A","B","C","D","E"])
1478
- print(r)
1479
- ```
1605
+ Minimiza cost_per_server × c + cost_per_wait × λ × Wq evaluando todos los c viables.
1480
1606
 
1481
- ```
1482
- Rule : SPT
1483
- Sequence : B → D → A → C → E
1484
- Makespan (Cmax) : 14
1485
- Total completion ΣCj : 37
1486
- Weighted completion ΣwCj: 89
1487
- Max lateness : 2
1488
- Total tardiness ΣTj : 2
1489
- Tardy jobs : 1
1607
+ ```python
1608
+ r = wl.optimize_servers(
1609
+ lam=6.0,
1610
+ mu=5.0,
1611
+ cost_per_server=10.0, # costo fijo por servidor por unidad de tiempo
1612
+ cost_per_wait=5.0, # costo por unidad de tiempo de espera de cada cliente
1613
+ )
1614
+ print(r.optimal_servers) # c óptimo
1615
+ print(r.min_cost) # costo mínimo
1616
+
1617
+ r.plot() # gráfica de costo vs número de servidores
1490
1618
  ```
1491
1619
 
1492
- **Reglas disponibles**
1620
+ ---
1493
1621
 
1494
- | `rule` | Criterio minimizado | Datos requeridos |
1495
- |--------|---------------------|-----------------|
1496
- | `'SPT'` | ΣCj (tiempo total de completación) | — |
1497
- | `'EDD'` | Lmax (máxima tardanza) | `due_dates` |
1498
- | `'WSPT'` | ΣwjCj (completación ponderada) | `weights` |
1499
- | `'CR'` | Ratio crítico dj/pj | `due_dates` |
1500
- | `'FIFO'` | Orden de llegada (línea base) | — |
1622
+ ## 18. Análisis de sensibilidad
1501
1623
 
1502
- **Atributos de `ScheduleResult`**
1624
+ ### `sensitivity(model_fn, param, values, **fixed_kwargs)` — Barrido paramétrico
1503
1625
 
1504
- | Atributo | Descripción |
1505
- |----------|-------------|
1506
- | `rule` | Regla utilizada |
1507
- | `sequence` | Lista de nombres en orden de procesamiento |
1508
- | `jobs` | Lista de `JobSchedule` (por trabajo) |
1509
- | `makespan` | Cmax = suma de tiempos de proceso |
1510
- | `total_completion_time` | ΣCj |
1511
- | `total_weighted_completion_time` | ΣwjCj |
1512
- | `max_lateness` | max(Lj) |
1513
- | `total_tardiness` | ΣTj |
1514
- | `n_tardy` | Número de trabajos tarde |
1626
+ Evalúa un modelo sobre un rango de valores de un parámetro.
1515
1627
 
1516
1628
  ```python
1517
- df = r.to_frame() # columnas: Name, p, d, w, Start, C, L, T, Tardy
1629
+ import numpy as np
1630
+
1631
+ df = wl.sensitivity(
1632
+ wl.mm1,
1633
+ param="lam",
1634
+ values=np.linspace(0.5, 4.5, 20),
1635
+ mu=5.0, # parámetros fijos
1636
+ )
1637
+ print(df.columns.tolist()) # ['lam', 'rho', 'L', 'Lq', 'W', 'Wq', ...]
1638
+
1639
+ # Barrido sobre μ
1640
+ df2 = wl.sensitivity(wl.mm1, "mu", np.linspace(4.0, 10.0, 15), lam=3.0)
1641
+
1642
+ # Barrido sobre número de servidores en M/M/c
1643
+ df3 = wl.sensitivity(wl.mmc, "c", range(1, 6), lam=8.0, mu=5.0)
1644
+
1645
+ # Visualizar
1646
+ from walopy.plotting import plot_sensitivity
1647
+ fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq"])
1648
+ fig.show()
1518
1649
  ```
1519
1650
 
1520
1651
  ---
1521
1652
 
1522
- ### Flow-shop de 2 máquinas — `johnson_flowshop`
1653
+ ## 19. Escenarios en lote (batch_model)
1523
1654
 
1524
- El algoritmo de Johnson encuentra la secuencia óptima que minimiza el makespan cuando todos los trabajos pasan primero por M1 y luego por M2.
1655
+ ### `batch_model(model_fn, df, **fixed_kwargs)` — Aplicar modelo a un DataFrame
1656
+
1657
+ Aplica cualquier función de walopy a cada fila de un DataFrame de escenarios.
1525
1658
 
1526
1659
  ```python
1527
- m1 = [3, 8, 5, 7, 2] # tiempos en máquina 1
1528
- m2 = [5, 2, 8, 4, 6] # tiempos en máquina 2
1660
+ import pandas as pd
1529
1661
 
1530
- r = wl.johnson_flowshop(m1, m2)
1531
- print(r)
1532
- ```
1662
+ # Varios escenarios con distintas tasas de llegada
1663
+ escenarios = pd.DataFrame({
1664
+ "lam": [1.0, 2.0, 3.0, 4.0]
1665
+ })
1666
+ resultado = wl.batch_model(wl.mm1, escenarios, mu=5.0)
1667
+ print(resultado[["lam", "rho", "Wq", "L"]])
1533
1668
 
1534
- ```
1535
- Sequence : J1 → J5 → J4 → J3 → J2
1536
- Makespan : 34
1537
- ```
1669
+ # Escenarios mixtos (lambda y mu variables)
1670
+ escenarios2 = pd.DataFrame({
1671
+ "lam": [2.0, 3.0, 4.0],
1672
+ "mu": [5.0, 6.0, 7.0],
1673
+ })
1674
+ resultado2 = wl.batch_model(wl.mm1, escenarios2)
1538
1675
 
1539
- ```python
1540
- df = r.to_frame()
1541
- # columnas: Job | M1 start | M1 end | M2 start | M2 end
1676
+ # Con número de servidores variable (columna int)
1677
+ escenarios3 = pd.DataFrame({
1678
+ "c": [1, 2, 3, 4],
1679
+ })
1680
+ resultado3 = wl.batch_model(wl.mmc, escenarios3, lam=8.0, mu=5.0)
1681
+
1682
+ # Las filas con error quedan marcadas en "_error" (la columna solo existe si alguna fila falla;
1683
+ # aquí c=1 es inestable porque λ=8 > μ=5)
1684
+ print(resultado3[resultado3["_error"].notna()]) # filas fallidas
1685
+
1686
+ # Para que la primera excepción se propague en lugar de recogerse:
1687
+ # wl.batch_model(wl.mmc, escenarios3, errors="raise", lam=8.0, mu=5.0)
1542
1688
  ```
1543
1689
 
1544
1690
  ---
1545
1691
 
1546
- ## 14. Confiabilidad — `reliability`
1692
+ ## 20. Comparar múltiples resultados
1547
1693
 
1548
- Modelos de confiabilidad con distribución exponencial (tasa de falla constante).
1694
+ ### `compare(*results, labels)` — DataFrame comparativo
1549
1695
 
1550
- ### Componente individual — `mtbf_analysis`
1696
+ Construye un DataFrame con una fila por resultado para facilitar la comparación.
1551
1697
 
1552
1698
  ```python
1553
- r = wl.mtbf_analysis(failure_rate=0.01, mttr=5.0, t=100)
1554
- print(r)
1555
- ```
1699
+ df = wl.compare(
1700
+ wl.mm1(3.0, 5.0),
1701
+ wl.mmc(3.0, 5.0, 2),
1702
+ wl.md1(3.0, 5.0),
1703
+ labels=["M/M/1", "M/M/2", "M/D/1"],
1704
+ )
1705
+ print(df[["label", "ρ (utilización)", "Wq (tiempo de espera)", "L (en el sistema)"]])
1706
+ # label ρ (utilización) Wq (tiempo de espera) L (en el sistema)
1707
+ # 0 M/M/1 0.6 0.3000 1.50
1708
+ # 1 M/M/2 0.3 0.0302 1.02
1709
+ # 2 M/D/1 0.6 0.1500 1.05
1556
1710
 
1557
- ```
1558
- Topology : component
1559
- Components : 1
1560
- Failure rates λ : ['0.01']
1561
- MTBF : 100
1562
- R(t=100) : 0.367879
1563
- Availability : 95.2381%
1711
+ # Sin etiquetas → scenario_1, scenario_2, ...
1712
+ df2 = wl.compare(wl.mm1(1, 5), wl.mm1(2, 5), wl.mm1(3, 5))
1564
1713
  ```
1565
1714
 
1566
- ### Sistema en serie — `series_system`
1715
+ ---
1567
1716
 
1568
- El sistema falla si **cualquier** componente falla. λ_sys = Σλi.
1717
+ ## 21. Gráficas
1569
1718
 
1570
- ```python
1571
- r = wl.series_system([0.01, 0.02, 0.03], t=10)
1572
- print(r.mtbf) # 1/0.06 ≈ 16.67
1573
- print(r.R_t) # e^{-0.06·10} ≈ 0.549
1574
- ```
1719
+ Todos los resultados exponen `.plot()` que devuelve una figura de Matplotlib o Plotly. También se pueden llamar directamente desde `walopy.plotting`.
1575
1720
 
1576
- ### Sistema en paralelo — `parallel_system`
1721
+ | Función `.plot()` | Tipo | Descripción |
1722
+ |---|---|---|
1723
+ | `QueueResult.plot()` | Matplotlib | Curvas Wq y Lq vs ρ con punto operativo |
1724
+ | `OEEResult.plot()` | Matplotlib | Barras horizontales OEE con referencia 85% |
1725
+ | `BottleneckResult.plot()` | Matplotlib | Utilización por estación, cuello de botella en rojo |
1726
+ | `EOQResult.plot()` | Matplotlib | Curvas de costo de mantener, ordenar y total |
1727
+ | `LineBalanceResult.plot()` | Plotly | Tiempo de ciclo vs takt por estación |
1728
+ | `BreakEvenResult.plot()` | Plotly | Líneas ingreso/costo con BEP y zona de beneficio |
1729
+ | `SimulationResult.plot()` | Plotly | Histograma de Wq + CDF empírica con percentiles |
1730
+ | `OptimizeResult.plot()` | Plotly | Costo por servidor, costo de espera y total vs c |
1731
+ | `KPINode.plot(kind=)` | Plotly | Treemap o sunburst interactivo |
1577
1732
 
1578
- El sistema opera si **al menos uno** de los componentes opera.
1579
- R_sys(t) = 1 − ∏(1 − e^{−λi·t}). MTBF calculado numéricamente.
1733
+ ```python
1734
+ # Guardar como HTML interactivo (Plotly)
1735
+ fig = r.plot()
1736
+ fig.write_html("resultado.html")
1580
1737
 
1738
+ # Guardar como imagen (Matplotlib)
1739
+ fig = wl.oee(0.9, 0.8, 0.95).plot()
1740
+ fig.savefig("oee.png", dpi=150)
1741
+ ```
1742
+
1743
+ Para la curva de sensibilidad:
1581
1744
  ```python
1582
- r = wl.parallel_system([0.01, 0.02], t=50)
1583
- print(r.R_t) # mayor que el sistema en serie
1584
- print(r.mtbf) # mayor que cualquier componente individual
1745
+ from walopy.plotting import plot_sensitivity
1746
+ df = wl.sensitivity(wl.mm1, "lam", np.linspace(0.5, 4.5, 20), mu=5.0)
1747
+ fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq", "L"])
1748
+ fig.show()
1585
1749
  ```
1586
1750
 
1587
- ### Sistema k-de-n — `koon_system`
1751
+ ---
1588
1752
 
1589
- Opera si al menos **k** de **n** componentes idénticos están en servicio.
1590
- MTBF exacto = (1/λ) · Σ_{j=k}^{n} (1/j).
1753
+ ## 22. CLI
1591
1754
 
1592
- ```python
1593
- # 2-de-3: requiere al menos 2 de 3 componentes idénticos
1594
- r = wl.koon_system(n=3, k=2, failure_rate=0.01, t=50)
1595
- print(r.mtbf) # (1/0.01) * (1/2 + 1/3) ≈ 83.33
1596
- print(r.R_t)
1597
- ```
1755
+ walopy incluye una interfaz de línea de comandos para uso rápido sin escribir código.
1598
1756
 
1599
- **Casos especiales**
1600
- - `k=1` → equivale a `parallel_system` con n componentes idénticos.
1601
- - `k=n` → equivale a `series_system` con n componentes idénticos.
1757
+ ```bash
1758
+ # M/M/1
1759
+ python -m walopy mm1 --lam 3 --mu 5
1602
1760
 
1603
- **Atributos de `ReliabilityResult`**
1761
+ # M/M/c
1762
+ python -m walopy mmc --lam 8 --mu 5 --c 2
1604
1763
 
1605
- | Atributo | Descripción |
1606
- |----------|-------------|
1607
- | `topology` | `'component'`, `'series'`, `'parallel'`, `'k-of-n'` |
1608
- | `n_components` | Número de componentes |
1609
- | `failure_rates` | Lista de λi |
1610
- | `mtbf` | MTBF del sistema |
1611
- | `R_t` | Confiabilidad R(t) si se proporcionó `t` |
1612
- | `availability` | Disponibilidad estacionaria A si se proporcionó `mttr` |
1613
- | `mttr` | Tiempo medio de reparación (si se proporcionó) |
1764
+ # M/D/1
1765
+ python -m walopy md1 --lam 3 --mu 5
1614
1766
 
1615
- ```python
1616
- df = r.to_frame() # columnas: Topology, Components, MTBF[, R(t), Availability]
1617
- r.R(t=200) # evalúa R(t) en cualquier tiempo posterior
1767
+ # G/G/1 (Kingman)
1768
+ python -m walopy gg1 --lam 3 --mu 5 --ca2 1.2 --cs2 0.8
1769
+
1770
+ # Ley de Little (resolver W)
1771
+ python -m walopy littles --L 2.5 --lam 5.0
1772
+
1773
+ # EOQ
1774
+ python -m walopy eoq --demand 1000 --ordering 50 --holding 2
1775
+
1776
+ # Versión
1777
+ python -m walopy --version
1778
+ ```
1779
+
1780
+ Ejemplo de salida:
1781
+ ```
1782
+ Modelo : M/M/1
1783
+ λ : 3.0 (tasa de llegada)
1784
+ μ : 5.0 (tasa de servicio por servidor)
1785
+ ρ : 0.6 (utilización por servidor)
1786
+ L : 1.5 (unidades promedio en el sistema)
1787
+ Lq : 0.9 (unidades promedio en cola)
1788
+ W : 0.5 (tiempo promedio en el sistema)
1789
+ Wq : 0.3 (tiempo promedio de espera en cola)
1790
+ P0 : 0.4
1618
1791
  ```
1619
1792
 
1620
1793
  ---
1621
1794
 
1622
- ## 22. Referencia de módulos
1795
+ ## 23. Referencia de módulos
1623
1796
 
1624
1797
  | Módulo | Funciones y clases principales |
1625
1798
  |---|---|
1626
1799
  | `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `mg1`, `littles_law`, `QueueResult`, `cv2_normal`, `cv2_triangular`, `cv2_uniform`, `cv2_erlang`, `cv2_gamma`, `cv2_lognormal`, `cv2_weibull` |
1627
1800
  | `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `break_even_multi`, `break_even_sales`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult`, `BreakEvenMultiResult` |
1628
1801
  | `fitting` | `fit_from_data`, `FitResult` |
1629
- | `inventory` | `eoq`, `ebq`, `eoq_multi`, `ebq_multi`, `eoq_multi_constrained`, `lot_for_lot`, `silver_meal`, `eoq_quantity_discount`, `wagner_whitin`, `rq_policy`, `rs_policy`, `exchange_curve`, `safety_stock_curve`, `reorder_point`, `newsvendor`, `EOQResult`, `EBQResult`, `MultiItemResult`, `ConstrainedMultiEOQResult`, `LotSizingResult`, `QuantityDiscountResult`, `ReorderResult`, `NewsvendorResult`, `RQPolicyResult`, `RSPolicyResult`, `ExchangeCurveResult`, `SafetyStockCurveResult` |
1630
- | `scheduling` | `schedule_single`, `johnson_flowshop`, `ScheduleResult`, `JobSchedule`, `FlowShopResult` |
1631
- | `reliability` | `mtbf_analysis`, `series_system`, `parallel_system`, `koon_system`, `ReliabilityResult` |
1802
+ | `inventory` | `eoq`, `ebq`, `eoq_multi`, `ebq_multi`, `eoq_multi_constrained`, `lot_for_lot`, `silver_meal`, `eoq_quantity_discount`, `wagner_whitin`, `rq_policy`, `rs_policy`, `exchange_curve`, `safety_stock_curve`, `abc_analysis`, `xyz_analysis`, `abc_xyz`, `mrp`, `reorder_point`, `newsvendor`, `EOQResult`, `EBQResult`, `MultiItemResult`, `ConstrainedMultiEOQResult`, `LotSizingResult`, `QuantityDiscountResult`, `ReorderResult`, `NewsvendorResult`, `RQPolicyResult`, `RSPolicyResult`, `ExchangeCurveResult`, `SafetyStockCurveResult`, `ABCResult`, `XYZResult`, `ABCXYZResult`, `MRPResult` |
1803
+ | `scheduling` | `schedule_single`, `johnson_flowshop`, `neh_flowshop`, `ScheduleResult`, `JobSchedule`, `FlowShopResult`, `NEHResult` |
1804
+ | `project` | `cpm`, `pert`, `ProjectResult`, `ActivityResult` |
1805
+ | `reliability` | `mtbf_analysis`, `series_system`, `parallel_system`, `koon_system`, `weibull_analysis`, `ReliabilityResult`, `WeibullResult` |
1632
1806
  | `network` | `jackson_network`, `JacksonResult`, `StationMetrics` |
1633
1807
  | `operations` | `oee`, `utilization_efficiency`, `unit_cost`, `OEEResult`, `UtilizationResult`, `UnitCostResult` |
1634
1808
  | `bottleneck` | `bottleneck_analysis`, `BottleneckResult`, `StationResult` |
@@ -1638,7 +1812,7 @@ r.R(t=200) # evalúa R(t) en cualquier tiempo posterior
1638
1812
 
1639
1813
  ---
1640
1814
 
1641
- ## 23. Requisitos
1815
+ ## 24. Requisitos
1642
1816
 
1643
1817
  ```
1644
1818
  Python ≥ 3.9