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