walopy 0.4.0__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.
- {walopy-0.4.0/src/walopy.egg-info → walopy-0.4.2}/PKG-INFO +4 -1
- {walopy-0.4.0 → walopy-0.4.2}/README.md +3 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/__init__.py +1 -1
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/inventory.py +13 -2
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/plotting.py +18 -7
- {walopy-0.4.0 → walopy-0.4.2/src/walopy.egg-info}/PKG-INFO +4 -1
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy.egg-info/SOURCES.txt +3 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_complejidad.py +11 -4
- walopy-0.4.2/tests/test_kpi_grafica.py +82 -0
- walopy-0.4.2/tests/test_propiedades_diferenciales.py +118 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_salida_identica.py +3 -0
- walopy-0.4.2/tests/test_wagner_whitin_ceros.py +61 -0
- {walopy-0.4.0 → walopy-0.4.2}/LICENSE +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/pyproject.toml +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/setup.cfg +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/__main__.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/_catalogo_en.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/_catalogo_es.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/_i18n.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/_utils.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/advanced.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/bottleneck.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/fitting.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/kpi.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/network.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/operations.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/project.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/py.typed +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/queuing.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/reliability.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/scheduling.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy/solver.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy.egg-info/dependency_links.txt +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy.egg-info/requires.txt +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/src/walopy.egg-info/top_level.txt +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_abc_xyz.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_advanced.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_bottleneck.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_cli.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_contrato_entradas.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_cpm_pert.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_documentacion.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_documentacion_ingles.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_ejemplos.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_exchange_curve.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_fitting.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_i18n.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_i18n_paridad.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_idioma.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_inventario_i18n.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_inventory.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_inventory_new.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_kpi.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_mg1.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_mmc_estable.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_mrp.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_neh_flowshop.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_network.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_new_models.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_notas_release.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_operations.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_plotting.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_politicas_inventario.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_queuing.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_readme.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_referencias_externas.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_regresion_validacion.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_reliability.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_safety_stock_curve.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_salida_por_idioma.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_scheduling.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_scorecard.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_solver.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_trazabilidad.py +0 -0
- {walopy-0.4.0 → walopy-0.4.2}/tests/test_version.py +0 -0
- {walopy-0.4.0 → 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.
|
|
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
|
|
@@ -1563,6 +1563,9 @@ tree.plot()
|
|
|
1563
1563
|
|
|
1564
1564
|
### `KPINode` — árbol personalizado
|
|
1565
1565
|
|
|
1566
|
+
El rótulo de cada rectángulo es el valor real del nodo; su **tamaño** es el mayor entre ese valor y la suma de sus hijos (un KPI como EBITDA = Ingresos − Costos no
|
|
1567
|
+
es la suma de sus hijos, y Plotly dibuja en blanco un padre menor que ellos).
|
|
1568
|
+
|
|
1566
1569
|
```python
|
|
1567
1570
|
rev = wl.KPINode("Revenue", value=200_000, unit="€", children=[
|
|
1568
1571
|
wl.KPINode("Product A", value=120_000, unit="€"),
|
|
@@ -1515,6 +1515,9 @@ tree.plot()
|
|
|
1515
1515
|
|
|
1516
1516
|
### `KPINode` — árbol personalizado
|
|
1517
1517
|
|
|
1518
|
+
El rótulo de cada rectángulo es el valor real del nodo; su **tamaño** es el mayor entre ese valor y la suma de sus hijos (un KPI como EBITDA = Ingresos − Costos no
|
|
1519
|
+
es la suma de sus hijos, y Plotly dibuja en blanco un padre menor que ellos).
|
|
1520
|
+
|
|
1518
1521
|
```python
|
|
1519
1522
|
rev = wl.KPINode("Revenue", value=200_000, unit="€", children=[
|
|
1520
1523
|
wl.KPINode("Product A", value=120_000, unit="€"),
|
|
@@ -164,7 +164,7 @@ from .solver import (
|
|
|
164
164
|
solve_servers,
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
__version__ = "0.4.
|
|
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] =
|
|
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] =
|
|
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,
|
|
@@ -193,6 +193,9 @@ def plot_kpi_tree(
|
|
|
193
193
|
) -> go.Figure:
|
|
194
194
|
"""Árbol de KPI interactivo con Plotly.
|
|
195
195
|
|
|
196
|
+
El rectángulo de cada nodo rotula su valor real; su tamaño es el mayor entre ese valor y la suma de los tamaños de sus hijos (un KPI como
|
|
197
|
+
EBITDA = Ingresos − Costos no es la suma de sus hijos, y Plotly no dibuja un padre menor que ellos).
|
|
198
|
+
|
|
196
199
|
Parameters
|
|
197
200
|
----------
|
|
198
201
|
root : KPINode
|
|
@@ -214,20 +217,26 @@ def plot_kpi_tree(
|
|
|
214
217
|
labels: list[str] = []
|
|
215
218
|
parents: list[str] = []
|
|
216
219
|
values: list[float] = []
|
|
220
|
+
shown: list[str] = [] # valor real de cada nodo (el que se rotula)
|
|
217
221
|
hover: list[str] = []
|
|
218
222
|
|
|
219
|
-
def _collect(node: KPINode, parent_id: str = "") ->
|
|
223
|
+
def _collect(node: KPINode, parent_id: str = "") -> float:
|
|
220
224
|
node_id = f"{parent_id}/{node.name}" if parent_id else node.name
|
|
225
|
+
pos = len(ids)
|
|
221
226
|
ids.append(node_id)
|
|
222
227
|
labels.append(node.name)
|
|
223
228
|
parents.append(parent_id)
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
values.append(0.0)
|
|
230
|
+
shown.append(f"{node.value:,.8g}")
|
|
226
231
|
unit_str = f" {node.unit}" if node.unit else ""
|
|
227
232
|
formula_str = _t("plotting.grafica.collect.br_2", formula=node.formula) if node.formula else ""
|
|
228
233
|
hover.append(_t("plotting.grafica.collect.br", name=node.name, value=node.value, unit_str=unit_str, formula_str=formula_str))
|
|
229
|
-
for child in node.children
|
|
230
|
-
|
|
234
|
+
children_size = sum(_collect(child, node_id) for child in node.children)
|
|
235
|
+
# Tamaño del rectángulo: con branchvalues="total" Plotly exige que el padre valga al menos la suma de sus hijos, y si no lo
|
|
236
|
+
# cumple dibuja la figura en blanco. Un KPI no es la suma de sus hijos (EBITDA = Ingresos − Costos; OEE = A × P × Q), así que
|
|
237
|
+
# el tamaño del padre es max(su valor, la suma de los tamaños de sus hijos); el valor rotulado sigue siendo el del nodo.
|
|
238
|
+
values[pos] = max(abs(node.value), children_size, 1e-9)
|
|
239
|
+
return values[pos]
|
|
231
240
|
|
|
232
241
|
_collect(root)
|
|
233
242
|
|
|
@@ -239,10 +248,11 @@ def plot_kpi_tree(
|
|
|
239
248
|
labels=labels,
|
|
240
249
|
parents=parents,
|
|
241
250
|
values=values,
|
|
251
|
+
text=shown,
|
|
242
252
|
customdata=hover,
|
|
243
253
|
hovertemplate=_t("plotting.grafica.plot_kpi_tree.customdata_extra_extra"),
|
|
244
254
|
branchvalues="total",
|
|
245
|
-
|
|
255
|
+
texttemplate="%{label}<br>%{text}",
|
|
246
256
|
insidetextorientation="radial",
|
|
247
257
|
marker=dict(colorscale="Blues"),
|
|
248
258
|
)
|
|
@@ -256,10 +266,11 @@ def plot_kpi_tree(
|
|
|
256
266
|
labels=labels,
|
|
257
267
|
parents=parents,
|
|
258
268
|
values=values,
|
|
269
|
+
text=shown,
|
|
259
270
|
customdata=hover,
|
|
260
271
|
hovertemplate=_t("plotting.grafica.plot_kpi_tree.customdata_extra_extra"),
|
|
261
272
|
branchvalues="total",
|
|
262
|
-
texttemplate="<b>%{label}</b><br>%{
|
|
273
|
+
texttemplate="<b>%{label}</b><br>%{text}",
|
|
263
274
|
textfont=dict(size=13),
|
|
264
275
|
marker=dict(
|
|
265
276
|
colorscale="Blues",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: walopy
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -1563,6 +1563,9 @@ tree.plot()
|
|
|
1563
1563
|
|
|
1564
1564
|
### `KPINode` — árbol personalizado
|
|
1565
1565
|
|
|
1566
|
+
El rótulo de cada rectángulo es el valor real del nodo; su **tamaño** es el mayor entre ese valor y la suma de sus hijos (un KPI como EBITDA = Ingresos − Costos no
|
|
1567
|
+
es la suma de sus hijos, y Plotly dibuja en blanco un padre menor que ellos).
|
|
1568
|
+
|
|
1566
1569
|
```python
|
|
1567
1570
|
rev = wl.KPINode("Revenue", value=200_000, unit="€", children=[
|
|
1568
1571
|
wl.KPINode("Product A", value=120_000, unit="€"),
|
|
@@ -45,6 +45,7 @@ tests/test_inventario_i18n.py
|
|
|
45
45
|
tests/test_inventory.py
|
|
46
46
|
tests/test_inventory_new.py
|
|
47
47
|
tests/test_kpi.py
|
|
48
|
+
tests/test_kpi_grafica.py
|
|
48
49
|
tests/test_mg1.py
|
|
49
50
|
tests/test_mmc_estable.py
|
|
50
51
|
tests/test_mrp.py
|
|
@@ -55,6 +56,7 @@ tests/test_notas_release.py
|
|
|
55
56
|
tests/test_operations.py
|
|
56
57
|
tests/test_plotting.py
|
|
57
58
|
tests/test_politicas_inventario.py
|
|
59
|
+
tests/test_propiedades_diferenciales.py
|
|
58
60
|
tests/test_queuing.py
|
|
59
61
|
tests/test_readme.py
|
|
60
62
|
tests/test_referencias_externas.py
|
|
@@ -68,4 +70,5 @@ tests/test_scorecard.py
|
|
|
68
70
|
tests/test_solver.py
|
|
69
71
|
tests/test_trazabilidad.py
|
|
70
72
|
tests/test_version.py
|
|
73
|
+
tests/test_wagner_whitin_ceros.py
|
|
71
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 << (
|
|
83
|
-
inicios = [0] + [i + 1 for i in range(
|
|
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.
|
|
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,82 @@
|
|
|
1
|
+
"""Los árboles de KPI se dibujan (W-26).
|
|
2
|
+
|
|
3
|
+
Con ``branchvalues="total"`` Plotly exige que el valor de cada padre sea **al menos** la suma de los de sus hijos; si no, la figura sale en blanco en el
|
|
4
|
+
navegador. Un KPI casi nunca es la suma de sus hijos (EBITDA = Ingresos − Costos; OEE = A × P × Q), así que ``plot_kpi_tree`` usa como tamaño del
|
|
5
|
+
rectángulo ``max(valor, suma de los tamaños de los hijos)`` y rotula el valor real del nodo. Estos tests comprueban esa condición sobre la figura
|
|
6
|
+
(el objeto ``Figure`` no se renderiza aquí).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import walopy as wl
|
|
13
|
+
|
|
14
|
+
pytest.importorskip("plotly")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ebitda() -> wl.KPINode:
|
|
18
|
+
a = wl.KPINode("Producto A", value=120_000, unit="€")
|
|
19
|
+
b = wl.KPINode("Producto B", value=80_000, unit="€")
|
|
20
|
+
ingresos = wl.KPINode("Ingresos", value=200_000, unit="€", children=[a, b])
|
|
21
|
+
costos = wl.KPINode("Costos de operación", value=100_000, unit="€")
|
|
22
|
+
return wl.KPINode("EBITDA", value=100_000, unit="€", children=[ingresos, costos])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _suma_exacta() -> wl.KPINode:
|
|
26
|
+
return wl.KPINode("Total", value=300.0, children=[wl.KPINode("A", value=100.0), wl.KPINode("B", value=200.0)])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ARBOLES = {
|
|
30
|
+
"ebitda": _ebitda,
|
|
31
|
+
"oee": lambda: wl.oee_kpi_tree(0.9, 0.8, 0.95),
|
|
32
|
+
"throughput": lambda: wl.throughput_kpi_tree(750.0, 1000.0, 0.05),
|
|
33
|
+
"roi": lambda: wl.roi_kpi_tree(50_000.0, 10_000.0, 8.0, 2_000.0, 20_000.0),
|
|
34
|
+
"suma_exacta": _suma_exacta,
|
|
35
|
+
"una_hoja": lambda: wl.KPINode("Solo", value=5.0),
|
|
36
|
+
"con_cero": lambda: wl.KPINode("R", value=0.0, children=[wl.KPINode("H", value=0.0)]),
|
|
37
|
+
"negativo": lambda: wl.KPINode("R", value=-10.0, children=[wl.KPINode("H1", value=-30.0), wl.KPINode("H2", value=5.0)]),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _violaciones(traza) -> list[str]:
|
|
42
|
+
"""Padres cuyo valor es menor que la suma de sus hijos (lo que deja a Plotly con la figura en blanco)."""
|
|
43
|
+
valor = dict(zip(traza.ids, traza.values))
|
|
44
|
+
suma: dict[str, float] = {}
|
|
45
|
+
for id_, padre in zip(traza.ids, traza.parents):
|
|
46
|
+
if padre:
|
|
47
|
+
suma[padre] = suma.get(padre, 0.0) + valor[id_]
|
|
48
|
+
return [f"{p}: {valor[p]:g} < {s:g}" for p, s in suma.items() if valor[p] < s * (1 - 1e-12)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("kind", ["treemap", "sunburst"])
|
|
52
|
+
@pytest.mark.parametrize("nombre", sorted(ARBOLES))
|
|
53
|
+
def test_ningun_padre_vale_menos_que_la_suma_de_sus_hijos(nombre, kind):
|
|
54
|
+
fig = ARBOLES[nombre]().plot(kind=kind)
|
|
55
|
+
traza = fig.data[0]
|
|
56
|
+
assert traza.branchvalues == "total"
|
|
57
|
+
assert not _violaciones(traza), f"{nombre} ({kind}) se dibujaría en blanco: {_violaciones(traza)}"
|
|
58
|
+
assert all(v > 0 for v in traza.values)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("kind", ["treemap", "sunburst"])
|
|
62
|
+
def test_el_rotulo_es_el_valor_real_del_nodo_no_el_tamano(kind):
|
|
63
|
+
fig = _ebitda().plot(kind=kind)
|
|
64
|
+
traza = fig.data[0]
|
|
65
|
+
rotulos = dict(zip(traza.labels, traza.text))
|
|
66
|
+
assert rotulos["EBITDA"] == "100,000" # no 300 000, que es el tamaño del rectángulo
|
|
67
|
+
assert rotulos["Ingresos"] == "200,000" and rotulos["Producto A"] == "120,000"
|
|
68
|
+
assert "%{text}" in traza.texttemplate
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_un_arbol_valido_conserva_los_tamanos_de_siempre():
|
|
72
|
+
"""Si el padre ya es la suma (o mayor), nada cambia: tamaño = valor del nodo."""
|
|
73
|
+
traza = _suma_exacta().plot().data[0]
|
|
74
|
+
assert dict(zip(traza.labels, traza.values)) == {"Total": 300.0, "A": 100.0, "B": 200.0}
|
|
75
|
+
mayor = wl.KPINode("P", value=500.0, children=[wl.KPINode("H", value=100.0)])
|
|
76
|
+
assert dict(zip(mayor.plot().data[0].labels, mayor.plot().data[0].values)) == {"P": 500.0, "H": 100.0}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_el_texto_emergente_sigue_mostrando_el_valor_y_la_formula():
|
|
80
|
+
raiz = wl.KPINode("EBITDA", value=100_000, unit="€", formula="Ingresos − Costos", children=[wl.KPINode("X", value=1.0)])
|
|
81
|
+
hover = raiz.plot().data[0].customdata[0]
|
|
82
|
+
assert "100000" in hover and "Ingresos − Costos" in hover and "€" in hover
|
|
@@ -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)
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
``tests/golden/salida_es.json`` se generó con ``python scripts/instantanea_salida.py --escribir`` sobre el código de v0.3.0, ANTES de
|
|
4
4
|
mover los textos a un catálogo. Cubre resultados, tablas, mensajes de error, avisos, gráficas, README y CLI, con el idioma por defecto.
|
|
5
5
|
Si cambia a propósito un texto en español, regenera la instantánea y revisa el diff como parte del PR.
|
|
6
|
+
|
|
7
|
+
Excepción deliberada (W-26): la ``grafica`` de ``oee_kpi_tree``, ``throughput_kpi_tree`` y ``roi_kpi_tree`` se actualizó a mano en la instantánea: ahora
|
|
8
|
+
rotula el valor real de cada nodo en ``text`` (los árboles salían en blanco en el navegador, ver ``tests/test_kpi_grafica.py``).
|
|
6
9
|
"""
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
@@ -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
|
|
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
|