walopy 0.4.1__tar.gz → 0.4.2__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 (76) hide show
  1. {walopy-0.4.1/src/walopy.egg-info → walopy-0.4.2}/PKG-INFO +1 -1
  2. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/__init__.py +1 -1
  3. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/inventory.py +13 -2
  4. {walopy-0.4.1 → walopy-0.4.2/src/walopy.egg-info}/PKG-INFO +1 -1
  5. {walopy-0.4.1 → walopy-0.4.2}/src/walopy.egg-info/SOURCES.txt +2 -0
  6. {walopy-0.4.1 → walopy-0.4.2}/tests/test_complejidad.py +11 -4
  7. walopy-0.4.2/tests/test_propiedades_diferenciales.py +118 -0
  8. walopy-0.4.2/tests/test_wagner_whitin_ceros.py +61 -0
  9. {walopy-0.4.1 → walopy-0.4.2}/LICENSE +0 -0
  10. {walopy-0.4.1 → walopy-0.4.2}/README.md +0 -0
  11. {walopy-0.4.1 → walopy-0.4.2}/pyproject.toml +0 -0
  12. {walopy-0.4.1 → walopy-0.4.2}/setup.cfg +0 -0
  13. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/__main__.py +0 -0
  14. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/_catalogo_en.py +0 -0
  15. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/_catalogo_es.py +0 -0
  16. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/_i18n.py +0 -0
  17. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/_utils.py +0 -0
  18. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/advanced.py +0 -0
  19. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/bottleneck.py +0 -0
  20. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/fitting.py +0 -0
  21. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/kpi.py +0 -0
  22. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/network.py +0 -0
  23. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/operations.py +0 -0
  24. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/plotting.py +0 -0
  25. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/project.py +0 -0
  26. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/py.typed +0 -0
  27. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/queuing.py +0 -0
  28. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/reliability.py +0 -0
  29. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/scheduling.py +0 -0
  30. {walopy-0.4.1 → walopy-0.4.2}/src/walopy/solver.py +0 -0
  31. {walopy-0.4.1 → walopy-0.4.2}/src/walopy.egg-info/dependency_links.txt +0 -0
  32. {walopy-0.4.1 → walopy-0.4.2}/src/walopy.egg-info/requires.txt +0 -0
  33. {walopy-0.4.1 → walopy-0.4.2}/src/walopy.egg-info/top_level.txt +0 -0
  34. {walopy-0.4.1 → walopy-0.4.2}/tests/test_abc_xyz.py +0 -0
  35. {walopy-0.4.1 → walopy-0.4.2}/tests/test_advanced.py +0 -0
  36. {walopy-0.4.1 → walopy-0.4.2}/tests/test_bottleneck.py +0 -0
  37. {walopy-0.4.1 → walopy-0.4.2}/tests/test_cli.py +0 -0
  38. {walopy-0.4.1 → walopy-0.4.2}/tests/test_contrato_entradas.py +0 -0
  39. {walopy-0.4.1 → walopy-0.4.2}/tests/test_cpm_pert.py +0 -0
  40. {walopy-0.4.1 → walopy-0.4.2}/tests/test_documentacion.py +0 -0
  41. {walopy-0.4.1 → walopy-0.4.2}/tests/test_documentacion_ingles.py +0 -0
  42. {walopy-0.4.1 → walopy-0.4.2}/tests/test_ejemplos.py +0 -0
  43. {walopy-0.4.1 → walopy-0.4.2}/tests/test_exchange_curve.py +0 -0
  44. {walopy-0.4.1 → walopy-0.4.2}/tests/test_fitting.py +0 -0
  45. {walopy-0.4.1 → walopy-0.4.2}/tests/test_i18n.py +0 -0
  46. {walopy-0.4.1 → walopy-0.4.2}/tests/test_i18n_paridad.py +0 -0
  47. {walopy-0.4.1 → walopy-0.4.2}/tests/test_idioma.py +0 -0
  48. {walopy-0.4.1 → walopy-0.4.2}/tests/test_inventario_i18n.py +0 -0
  49. {walopy-0.4.1 → walopy-0.4.2}/tests/test_inventory.py +0 -0
  50. {walopy-0.4.1 → walopy-0.4.2}/tests/test_inventory_new.py +0 -0
  51. {walopy-0.4.1 → walopy-0.4.2}/tests/test_kpi.py +0 -0
  52. {walopy-0.4.1 → walopy-0.4.2}/tests/test_kpi_grafica.py +0 -0
  53. {walopy-0.4.1 → walopy-0.4.2}/tests/test_mg1.py +0 -0
  54. {walopy-0.4.1 → walopy-0.4.2}/tests/test_mmc_estable.py +0 -0
  55. {walopy-0.4.1 → walopy-0.4.2}/tests/test_mrp.py +0 -0
  56. {walopy-0.4.1 → walopy-0.4.2}/tests/test_neh_flowshop.py +0 -0
  57. {walopy-0.4.1 → walopy-0.4.2}/tests/test_network.py +0 -0
  58. {walopy-0.4.1 → walopy-0.4.2}/tests/test_new_models.py +0 -0
  59. {walopy-0.4.1 → walopy-0.4.2}/tests/test_notas_release.py +0 -0
  60. {walopy-0.4.1 → walopy-0.4.2}/tests/test_operations.py +0 -0
  61. {walopy-0.4.1 → walopy-0.4.2}/tests/test_plotting.py +0 -0
  62. {walopy-0.4.1 → walopy-0.4.2}/tests/test_politicas_inventario.py +0 -0
  63. {walopy-0.4.1 → walopy-0.4.2}/tests/test_queuing.py +0 -0
  64. {walopy-0.4.1 → walopy-0.4.2}/tests/test_readme.py +0 -0
  65. {walopy-0.4.1 → walopy-0.4.2}/tests/test_referencias_externas.py +0 -0
  66. {walopy-0.4.1 → walopy-0.4.2}/tests/test_regresion_validacion.py +0 -0
  67. {walopy-0.4.1 → walopy-0.4.2}/tests/test_reliability.py +0 -0
  68. {walopy-0.4.1 → walopy-0.4.2}/tests/test_safety_stock_curve.py +0 -0
  69. {walopy-0.4.1 → walopy-0.4.2}/tests/test_salida_identica.py +0 -0
  70. {walopy-0.4.1 → walopy-0.4.2}/tests/test_salida_por_idioma.py +0 -0
  71. {walopy-0.4.1 → walopy-0.4.2}/tests/test_scheduling.py +0 -0
  72. {walopy-0.4.1 → walopy-0.4.2}/tests/test_scorecard.py +0 -0
  73. {walopy-0.4.1 → walopy-0.4.2}/tests/test_solver.py +0 -0
  74. {walopy-0.4.1 → walopy-0.4.2}/tests/test_trazabilidad.py +0 -0
  75. {walopy-0.4.1 → walopy-0.4.2}/tests/test_version.py +0 -0
  76. {walopy-0.4.1 → walopy-0.4.2}/tests/test_weibull.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: walopy
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
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
@@ -164,7 +164,7 @@ from .solver import (
164
164
  solve_servers,
165
165
  )
166
166
 
167
- __version__ = "0.4.1" # fuente única de la versión; pyproject.toml la lee con [tool.setuptools.dynamic]
167
+ __version__ = "0.4.2" # fuente única de la versión; pyproject.toml la lee con [tool.setuptools.dynamic]
168
168
 
169
169
  __all__ = [
170
170
  # idioma de los textos
@@ -1455,13 +1455,21 @@ def wagner_whitin(
1455
1455
  n = len(demands_v)
1456
1456
 
1457
1457
  INF = float("inf")
1458
- dp = [INF] * (n + 1) # dp[j] = min cost to satisfy demands 0..j-1
1458
+ dp = [INF] * (n + 1) # dp[j] = costo mínimo de satisfacer las demandas 1..j
1459
1459
  dp[0] = 0.0
1460
- last = [-1] * (n + 1) # last[j] = period i where order was placed
1460
+ last = [-1] * (n + 1) # last[j] = periodo del último pedido (0 = ninguno: demanda nula)
1461
1461
 
1462
1462
  # Recursión hacia adelante: el costo de mantener se acumula de forma incremental
1463
1463
  # (O(n²) en total; antes se recalculaba la suma completa para cada par (i, j): O(n³)).
1464
+ # Solo se pide en periodos con demanda positiva: pedir antes de que haga falta nunca
1465
+ # abarata el plan, y un periodo de demanda nula no justifica pagar la preparación.
1464
1466
  for i in range(1, n + 1):
1467
+ if demands_v[i - 1] == 0:
1468
+ # Sin demanda no hay pedido: se arrastra el costo del periodo anterior.
1469
+ if dp[i - 1] < dp[i]:
1470
+ dp[i] = dp[i - 1]
1471
+ last[i] = 0
1472
+ continue
1465
1473
  acum = 0.0 # Σ_{k=i..j} (k - i) · d_k
1466
1474
  for j in range(i, n + 1):
1467
1475
  acum += (j - i) * demands_v[j - 1]
@@ -1476,6 +1484,9 @@ def wagner_whitin(
1476
1484
  j = n
1477
1485
  while j > 0:
1478
1486
  i = last[j]
1487
+ if i == 0: # periodo sin demanda ni pedido
1488
+ j -= 1
1489
+ continue
1479
1490
  qty = sum(demands_v[k - 1] for k in range(i, j + 1))
1480
1491
  orders.append({
1481
1492
  "period": i,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: walopy
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
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
@@ -56,6 +56,7 @@ tests/test_notas_release.py
56
56
  tests/test_operations.py
57
57
  tests/test_plotting.py
58
58
  tests/test_politicas_inventario.py
59
+ tests/test_propiedades_diferenciales.py
59
60
  tests/test_queuing.py
60
61
  tests/test_readme.py
61
62
  tests/test_referencias_externas.py
@@ -69,4 +70,5 @@ tests/test_scorecard.py
69
70
  tests/test_solver.py
70
71
  tests/test_trazabilidad.py
71
72
  tests/test_version.py
73
+ tests/test_wagner_whitin_ceros.py
72
74
  tests/test_weibull.py
@@ -77,10 +77,18 @@ def test_neh_n200_m10_es_rapido():
77
77
 
78
78
 
79
79
  def _ww_fuerza_bruta(d, S, h):
80
+ """Óptimo por enumeración, independiente de la programación dinámica.
81
+
82
+ Un pedido solo se coloca en un periodo con demanda positiva (no hay nada que cubrir en uno nulo) y el primero
83
+ de ellos debe tener pedido. Un horizonte sin demanda cuesta 0.
84
+ """
85
+ positivos = [t for t, x in enumerate(d) if x > 0]
86
+ if not positivos:
87
+ return 0.0
80
88
  n = len(d)
81
89
  mejor = math.inf
82
- for mascara in range(1 << (n - 1)):
83
- inicios = [0] + [i + 1 for i in range(n - 1) if mascara >> i & 1]
90
+ for mascara in range(1 << (len(positivos) - 1)):
91
+ inicios = [positivos[0]] + [positivos[i + 1] for i in range(len(positivos) - 1) if mascara >> i & 1]
84
92
  costo = 0.0
85
93
  for si, a in enumerate(inicios):
86
94
  fin = inicios[si + 1] if si + 1 < len(inicios) else n
@@ -93,8 +101,7 @@ def _ww_fuerza_bruta(d, S, h):
93
101
  def test_wagner_whitin_coincide_con_fuerza_bruta(semilla):
94
102
  r = random.Random(semilla)
95
103
  n = r.randint(2, 9)
96
- d = [r.randint(0, 40) for _ in range(n)]
97
- d[0] = d[0] or 1
104
+ d = [r.choice([0, 0, r.randint(1, 40)]) for _ in range(n)] # ceros en cualquier posición (W-27)
98
105
  S, h = r.randint(10, 100), r.choice([0.5, 1, 2])
99
106
  assert wl.wagner_whitin(d, S, h).total_cost == pytest.approx(_ww_fuerza_bruta(d, S, h), abs=1e-9)
100
107
 
@@ -0,0 +1,118 @@
1
+ """Relaciones que deben cumplir funciones distintas del mismo modelo, sobre escenarios aleatorios con semilla fija.
2
+
3
+ Complementan a los tests de valores concretos: detectan errores que un caso a mano no cubre (como W-27). Carga moderada
4
+ (``N`` escenarios por familia); la semilla fija hace que un fallo sea reproducible.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import random
10
+
11
+ import walopy as wl
12
+
13
+ N = 300
14
+ SEMILLA = 20261007
15
+
16
+
17
+ def _cerca(a: float, b: float, rel: float = 1e-7) -> bool:
18
+ return math.isclose(a, b, rel_tol=rel, abs_tol=1e-12)
19
+
20
+
21
+ def _costo_eoq(D: float, K: float, h: float, q: float) -> float:
22
+ return D / q * K + h * q / 2
23
+
24
+
25
+ def _escenarios(familia: str):
26
+ r = random.Random(f"{SEMILLA}-{familia}")
27
+ return [random.Random(r.random()) for _ in range(N)]
28
+
29
+
30
+ def _cola(r):
31
+ mu = r.uniform(0.2, 50)
32
+ lam = r.uniform(0.01, 0.97) * mu
33
+ return lam, mu, wl.mm1(lam, mu)
34
+
35
+
36
+ def test_mg1_con_cs2_igual_a_1_es_mm1():
37
+ for r in _escenarios("mg1"):
38
+ lam, mu, m = _cola(r)
39
+ g = wl.mg1(lam, mu, 1.0)
40
+ assert _cerca(g.L, m.L) and _cerca(g.Wq, m.Wq), (lam, mu)
41
+
42
+
43
+ def test_kingman_con_cv_1_coincide_con_mm1():
44
+ for r in _escenarios("kingman"):
45
+ lam, mu, m = _cola(r)
46
+ assert _cerca(wl.kingman(lam, mu, 1.0, 1.0).Wq, m.Wq), (lam, mu)
47
+
48
+
49
+ def test_md1_tiene_la_mitad_de_cola_que_mm1():
50
+ for r in _escenarios("md1"):
51
+ lam, mu, m = _cola(r)
52
+ assert _cerca(wl.md1(lam, mu).Lq, m.Lq / 2), (lam, mu)
53
+
54
+
55
+ def test_mmc_con_un_servidor_es_mm1():
56
+ for r in _escenarios("mmc1"):
57
+ lam, mu, m = _cola(r)
58
+ c1 = wl.mmc(lam, mu, 1)
59
+ assert _cerca(c1.Wq, m.Wq) and _cerca(c1.L, m.L), (lam, mu)
60
+
61
+
62
+ def test_erlang_b_decrece_al_añadir_servidores():
63
+ for r in _escenarios("erlang"):
64
+ c, a = r.randint(1, 40), r.uniform(0.1, 60)
65
+ b0, b1 = wl.erlang_b(a, 1.0, c), wl.erlang_b(a, 1.0, c + 1)
66
+ assert 0 <= b1 <= b0 + 1e-15 and b0 <= 1, (a, c, b0, b1)
67
+
68
+
69
+ def test_mmc_la_espera_decrece_al_añadir_un_servidor():
70
+ for r in _escenarios("mmc_c"):
71
+ c, mu = r.randint(1, 15), r.uniform(1, 20)
72
+ lam = r.uniform(0.05, 0.95) * c * mu
73
+ w1, w2 = wl.mmc(lam, mu, c).Wq, wl.mmc(lam, mu, c + 1).Wq
74
+ assert w2 <= w1 + 1e-12, (lam, mu, c, w1, w2)
75
+
76
+
77
+ def test_lotes_wagner_whitin_no_supera_a_las_heuristicas():
78
+ for r in _escenarios("lotes"):
79
+ d = [r.choice([0, 0, r.uniform(1, 200)]) for _ in range(r.randint(2, 12))]
80
+ if sum(d) == 0:
81
+ continue
82
+ S, h = r.uniform(1, 500), r.uniform(0.05, 5)
83
+ ww = wl.wagner_whitin(d, S, h).total_cost
84
+ assert ww <= wl.silver_meal(d, S, h).total_cost + 1e-6, (d, S, h)
85
+ assert ww <= wl.lot_for_lot(d, S, h).total_cost + 1e-6, (d, S, h)
86
+
87
+
88
+ def test_fiabilidad_serie_y_paralelo_acotan_a_los_componentes():
89
+ for r in _escenarios("fiabilidad"):
90
+ n = r.randint(2, 6)
91
+ lams = [r.uniform(1e-3, 0.2) for _ in range(n)]
92
+ t = r.uniform(0.5, 100)
93
+ Ri = [math.exp(-x * t) for x in lams]
94
+ assert wl.series_system(lams, t=t).R_t <= min(Ri) + 1e-12, (lams, t)
95
+ assert wl.parallel_system(lams, t=t).R_t >= max(Ri) - 1e-12, (lams, t)
96
+
97
+
98
+ def test_k_de_n_esta_entre_serie_y_paralelo():
99
+ for r in _escenarios("koon"):
100
+ n = r.randint(2, 6)
101
+ k, lam, t = r.randint(1, n), r.uniform(1e-3, 0.2), r.uniform(0.5, 100)
102
+ rk = wl.koon_system(n=n, k=k, failure_rate=lam, t=t).R_t
103
+ assert math.exp(-lam * t) ** n - 1e-12 <= rk <= 1 - (1 - math.exp(-lam * t)) ** n + 1e-12, (n, k, lam, t)
104
+
105
+
106
+ def test_eoq_es_el_minimo_del_costo_y_coincide_con_la_formula():
107
+ for r in _escenarios("eoq"):
108
+ D, K, h = r.uniform(1, 5000), r.uniform(1, 500), r.uniform(0.01, 10)
109
+ e = wl.eoq(D, K, h)
110
+
111
+ assert _cerca(e.total_cost, _costo_eoq(D, K, h, e.eoq), 1e-9), (D, K, h)
112
+ assert all(e.total_cost <= _costo_eoq(D, K, h, e.eoq * f) + 1e-9 for f in (0.5, 0.9, 1.1, 2)), (D, K, h)
113
+
114
+
115
+ def test_eoq_decrece_con_el_costo_de_mantener():
116
+ for r in _escenarios("eoq_h"):
117
+ D, K, h = r.uniform(1, 5000), r.uniform(1, 500), r.uniform(0.01, 10)
118
+ assert wl.eoq(D, K, h * 1.5).eoq < wl.eoq(D, K, h).eoq, (D, K, h)
@@ -0,0 +1,61 @@
1
+ """W-27: ``wagner_whitin`` cobraba preparación en periodos sin demanda y obligaba a pedir en el periodo 1."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import random
6
+
7
+ import pytest
8
+
9
+ import walopy as wl
10
+ from tests.test_complejidad import _ww_fuerza_bruta
11
+
12
+
13
+ def _pedidos(r):
14
+ return [(o["period"], o["order_qty"]) for o in r.orders]
15
+
16
+
17
+ def test_ceros_iniciales_no_obligan_a_pedir_en_el_periodo_1():
18
+ r = wl.wagner_whitin([0, 0, 100, 0, 0], setup_cost=100, holding_cost=1)
19
+ assert r.total_cost == pytest.approx(100.0) # antes 200: pedido vacío en el periodo 1 + el real
20
+ assert _pedidos(r) == [(3, 100.0)]
21
+
22
+
23
+ def test_ceros_intermedios_no_pagan_preparacion():
24
+ r = wl.wagner_whitin([0, 50, 0, 100], setup_cost=100, holding_cost=1)
25
+ assert r.total_cost == pytest.approx(200.0) # antes 250
26
+ assert _pedidos(r) == [(2, 50.0), (4, 100.0)]
27
+
28
+
29
+ def test_horizonte_sin_demanda_cuesta_cero_y_no_pide():
30
+ r = wl.wagner_whitin([0, 0, 0], setup_cost=100, holding_cost=1)
31
+ assert r.total_cost == 0.0
32
+ assert r.orders == [] and r.n_orders == 0
33
+ assert r.total_setup_cost == 0.0 and r.total_holding_cost == 0.0
34
+
35
+
36
+ def test_ningun_pedido_es_de_cantidad_cero():
37
+ r = random.Random(27)
38
+ for _ in range(200):
39
+ d = [r.choice([0, 0, r.randint(1, 40)]) for _ in range(r.randint(1, 12))]
40
+ res = wl.wagner_whitin(d, r.randint(5, 80), r.choice([0.5, 1, 2]))
41
+ assert all(o["order_qty"] > 0 for o in res.orders)
42
+ assert sum(o["order_qty"] for o in res.orders) == pytest.approx(sum(d))
43
+
44
+
45
+ @pytest.mark.parametrize("semilla", range(150))
46
+ def test_costo_optimo_exacto_con_ceros_en_cualquier_posicion(semilla):
47
+ r = random.Random(7000 + semilla)
48
+ d = [r.choice([0, 0, r.randint(1, 60)]) for _ in range(r.randint(1, 10))]
49
+ S, h = r.uniform(1, 200), r.uniform(0.05, 3)
50
+ res = wl.wagner_whitin(d, S, h)
51
+ assert res.total_cost == pytest.approx(_ww_fuerza_bruta(d, S, h), rel=1e-9, abs=1e-9)
52
+ assert res.total_cost == pytest.approx(res.total_setup_cost + res.total_holding_cost)
53
+ assert res.total_setup_cost == pytest.approx(res.n_orders * S)
54
+
55
+
56
+ def test_con_demanda_positiva_el_resultado_no_cambia():
57
+ # Caso de referencia del docstring: sin ceros el plan es el de siempre.
58
+ r = wl.wagner_whitin([100, 80, 120, 60], setup_cost=200, holding_cost=1)
59
+ assert r.total_cost == pytest.approx(_ww_fuerza_bruta([100, 80, 120, 60], 200, 1))
60
+ assert r.orders[0]["period"] == 1
61
+ assert math.isfinite(r.total_cost)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes