walopy 0.4.3__tar.gz → 0.4.4__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.3/src/walopy.egg-info → walopy-0.4.4}/PKG-INFO +1 -1
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/__init__.py +1 -1
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/_catalogo_en.py +3 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/_catalogo_es.py +3 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/advanced.py +19 -2
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/inventory.py +27 -8
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/project.py +36 -1
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/reliability.py +22 -5
- {walopy-0.4.3 → walopy-0.4.4/src/walopy.egg-info}/PKG-INFO +1 -1
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy.egg-info/SOURCES.txt +3 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_advanced.py +1 -0
- walopy-0.4.4/tests/test_avisos_validez.py +46 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_cpm_pert.py +14 -8
- walopy-0.4.4/tests/test_disponibilidad_redundante.py +63 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_inventario_i18n.py +3 -0
- walopy-0.4.4/tests/test_pert_varianza.py +74 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_plotting.py +1 -1
- {walopy-0.4.3 → walopy-0.4.4}/LICENSE +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/README.md +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/pyproject.toml +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/setup.cfg +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/__main__.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/_i18n.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/_utils.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/bottleneck.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/fitting.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/kpi.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/network.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/operations.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/plotting.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/py.typed +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/queuing.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/scheduling.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy/solver.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy.egg-info/dependency_links.txt +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy.egg-info/requires.txt +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/src/walopy.egg-info/top_level.txt +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_abc_xyz.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_bottleneck.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_cli.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_complejidad.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_contrato_entradas.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_dibujo_plotly.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_documentacion.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_documentacion_ingles.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_ejemplos.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_exchange_curve.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_fitting.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_i18n.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_i18n_paridad.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_idioma.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_inventory.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_inventory_new.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_kpi.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_kpi_grafica.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_mg1.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_mmc_estable.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_mrp.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_neh_flowshop.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_network.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_new_models.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_notas_release.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_operations.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_politicas_inventario.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_propiedades_algoritmos.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_propiedades_diferenciales.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_propiedades_inventario_fiabilidad.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_queuing.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_readme.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_referencias_externas.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_regresion_validacion.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_reliability.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_safety_stock_curve.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_salida_identica.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_salida_por_idioma.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_scheduling.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_scorecard.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_servidores_cmax.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_servidores_minimos.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_solver.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_solver_contratos.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_trazabilidad.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_version.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/tests/test_wagner_whitin_ceros.py +0 -0
- {walopy-0.4.3 → walopy-0.4.4}/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.4
|
|
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.
|
|
167
|
+
__version__ = "0.4.4" # 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
|
|
@@ -39,6 +39,7 @@ EN: dict[str, str] = {
|
|
|
39
39
|
"advanced.error.mm1_priority.sistema_inestable_total": "Unstable system: ρ_total = {rho_total:.4g} ≥ 1.",
|
|
40
40
|
"advanced.error.mmck.capacidad_sistema_debe_ser_servidores": "'K' (system capacity) must be ≥ c (servers); got K={K}, c={c}.",
|
|
41
41
|
"advanced.error.monte_carlo_gg1.sistema_inestable": "Unstable system: ρ = {rho:.4g} ≥ 1.",
|
|
42
|
+
"advanced.aviso.monte_carlo_gg1.estimacion_imprecisa": "The estimate Wq = {wq:.4g} has a 95 % half-width of ±{semiancho:.3g} ({relativo:.0%}) (means of 20 batches) with {n} customers: increase n_customers or average several replications (different seeds).",
|
|
42
43
|
"advanced.error.queue_length_pmf.max_debe_estar_entre_recibio": "'n_max' must be between 0 and {MAX_ESTADOS}, got {n_max!r}.",
|
|
43
44
|
"advanced.error.queue_length_pmf.max_debe_ser_entero_recibio": "'n_max' must be an integer >= 0, got {__name__!r}.",
|
|
44
45
|
"advanced.etiqueta.summary.margen_contribucion": "Contribution margin : {contribution_margin:.4g}",
|
|
@@ -231,6 +232,7 @@ EN: dict[str, str] = {
|
|
|
231
232
|
"inventory.error.mrp.periods_debe_tener_misma_longitud": "'periods' must have the same length as 'gross_requirements'.",
|
|
232
233
|
"inventory.error.mrp.scheduled_receipts_debe_tener_misma": "'scheduled_receipts' must have the same length as 'gross_requirements'.",
|
|
233
234
|
"inventory.error.newsvendor.cost_debe_ser_menor_price": "'cost' must be less than 'price' (otherwise Cu ≤ 0).",
|
|
235
|
+
"inventory.aviso.newsvendor.demanda_normal_cv_alto": "The normal demand has CV = {cv:.2f} > 0.5: the model assigns P(D < 0) = {p_negativa:.1%} and Q* may be biased. With such variable demand a non-negative distribution (lognormal, gamma or empirical) is preferable.",
|
|
234
236
|
"inventory.error.newsvendor.salvage_debe_ser_menor_cost": "'salvage' must be less than 'cost' (otherwise Co ≤ 0).",
|
|
235
237
|
"inventory.error.reorder_point.service_level_debe_estar_estrictamente": "'service_level' must be strictly between 0 and 1.",
|
|
236
238
|
"inventory.error.safety_stock_curve.indique_como_maximo_uno_target": "Provide at most one of 'target_service_level' or 'target_ss_investment'.",
|
|
@@ -483,6 +485,7 @@ EN: dict[str, str] = {
|
|
|
483
485
|
"project.error.leer_actividades.activities_predecessors_debe_ser_lista": "activities[{i}]['predecessors'] must be a list of names.",
|
|
484
486
|
"project.error.leer_actividades.nombre_actividad_duplicado_cada_actividad": "Duplicate activity name: '{nm}'. Each activity must have a unique name.",
|
|
485
487
|
"project.error.pert.actividad_falta_clave": "Activity '{nm}': missing key '{clave}'.",
|
|
488
|
+
"project.aviso.pert.rutas_casi_criticas": "{n} activities outside the critical path used have total float ≤ the project σ ({sigma:.3g}): {actividades}. The PERT probability considers a single path and is optimistic when other near-critical paths merge; consider a Monte Carlo simulation.",
|
|
486
489
|
"project.error.pert.actividad_requiere_optimistic_most_likely": "Activity '{nm}': optimistic ≤ most_likely ≤ pessimistic is required.",
|
|
487
490
|
"project.error.probability.probability_requiere_resultado_pert_varianza": "probability() requires a PERT result with nonzero variance.",
|
|
488
491
|
"project.error.toposort_and_succ.actividad_referencia_predecesor_desconocido": "Activity '{name}' references an unknown predecessor '{pred}'.",
|
|
@@ -39,6 +39,7 @@ ES: dict[str, str] = {
|
|
|
39
39
|
"advanced.error.mm1_priority.sistema_inestable_total": "Sistema inestable: ρ_total = {rho_total:.4g} ≥ 1.",
|
|
40
40
|
"advanced.error.mmck.capacidad_sistema_debe_ser_servidores": "'K' (capacidad del sistema) debe ser ≥ c (servidores); se recibió K={K}, c={c}.",
|
|
41
41
|
"advanced.error.monte_carlo_gg1.sistema_inestable": "Sistema inestable: ρ = {rho:.4g} ≥ 1.",
|
|
42
|
+
"advanced.aviso.monte_carlo_gg1.estimacion_imprecisa": "La estimación Wq = {wq:.4g} tiene un semiancho de ±{semiancho:.3g} ({relativo:.0%}) al 95 % (medias de 20 lotes) con {n} clientes: aumente n_customers o promedie varias réplicas (seed distintas).",
|
|
42
43
|
"advanced.error.queue_length_pmf.max_debe_estar_entre_recibio": "'n_max' debe estar entre 0 y {MAX_ESTADOS}, se recibió {n_max!r}.",
|
|
43
44
|
"advanced.error.queue_length_pmf.max_debe_ser_entero_recibio": "'n_max' debe ser un entero >= 0, se recibió {__name__!r}.",
|
|
44
45
|
"advanced.etiqueta.summary.margen_contribucion": "Margen de contribución : {contribution_margin:.4g}",
|
|
@@ -231,6 +232,7 @@ ES: dict[str, str] = {
|
|
|
231
232
|
"inventory.error.mrp.periods_debe_tener_misma_longitud": "'periods' debe tener la misma longitud que 'gross_requirements'.",
|
|
232
233
|
"inventory.error.mrp.scheduled_receipts_debe_tener_misma": "'scheduled_receipts' debe tener la misma longitud que 'gross_requirements'.",
|
|
233
234
|
"inventory.error.newsvendor.cost_debe_ser_menor_price": "'cost' debe ser menor que 'price' (de lo contrario Cu ≤ 0).",
|
|
235
|
+
"inventory.aviso.newsvendor.demanda_normal_cv_alto": "La demanda normal tiene CV = {cv:.2f} > 0,5: el modelo asigna P(D < 0) = {p_negativa:.1%} y Q* puede estar sesgada. Con demanda tan variable conviene una distribución no negativa (lognormal, gamma o empírica).",
|
|
234
236
|
"inventory.error.newsvendor.salvage_debe_ser_menor_cost": "'salvage' debe ser menor que 'cost' (de lo contrario Co ≤ 0).",
|
|
235
237
|
"inventory.error.reorder_point.service_level_debe_estar_estrictamente": "'service_level' debe estar estrictamente entre 0 y 1.",
|
|
236
238
|
"inventory.error.safety_stock_curve.indique_como_maximo_uno_target": "Indique como máximo uno de 'target_service_level' o 'target_ss_investment'.",
|
|
@@ -483,6 +485,7 @@ ES: dict[str, str] = {
|
|
|
483
485
|
"project.error.leer_actividades.activities_predecessors_debe_ser_lista": "activities[{i}]['predecessors'] debe ser una lista de nombres.",
|
|
484
486
|
"project.error.leer_actividades.nombre_actividad_duplicado_cada_actividad": "Nombre de actividad duplicado: '{nm}'. Cada actividad debe tener un nombre único.",
|
|
485
487
|
"project.error.pert.actividad_falta_clave": "Actividad '{nm}': falta la clave '{clave}'.",
|
|
488
|
+
"project.aviso.pert.rutas_casi_criticas": "Hay {n} actividades fuera de la ruta crítica usada con holgura ≤ σ del proyecto ({sigma:.3g}): {actividades}. La probabilidad PERT solo considera una ruta y es optimista cuando otras rutas casi críticas convergen; considere una simulación Monte Carlo.",
|
|
486
489
|
"project.error.pert.actividad_requiere_optimistic_most_likely": "Actividad '{nm}': se requiere optimistic ≤ most_likely ≤ pessimistic.",
|
|
487
490
|
"project.error.probability.probability_requiere_resultado_pert_varianza": "probability() requiere un resultado PERT con varianza distinta de cero.",
|
|
488
491
|
"project.error.toposort_and_succ.actividad_referencia_predecesor_desconocido": "La actividad '{name}' referencia un predecesor desconocido '{pred}'.",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import math
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
@@ -226,6 +227,11 @@ class SimulationResult:
|
|
|
226
227
|
return plot_simulation(self, **kwargs)
|
|
227
228
|
|
|
228
229
|
|
|
230
|
+
LOTES_MC = 20 # lotes para el intervalo de confianza de Wq por medias de lotes
|
|
231
|
+
T_975_19 = 2.093 # cuantil 0,975 de la t de Student con 19 grados de libertad
|
|
232
|
+
PRECISION_RELATIVA_MC = 0.10 # avisar si el semiancho al 95 % supera el 10 % de Wq
|
|
233
|
+
|
|
234
|
+
|
|
229
235
|
def monte_carlo_gg1(
|
|
230
236
|
lam: float,
|
|
231
237
|
mu: float,
|
|
@@ -271,9 +277,9 @@ def monte_carlo_gg1(
|
|
|
271
277
|
|
|
272
278
|
Examples
|
|
273
279
|
--------
|
|
274
|
-
>>> r = monte_carlo_gg1(lam=2.0, mu=3.0, ca2=1.0, cs2=1.0, n_customers=
|
|
280
|
+
>>> r = monte_carlo_gg1(lam=2.0, mu=3.0, ca2=1.0, cs2=1.0, n_customers=50_000, seed=1)
|
|
275
281
|
>>> round(r.L, 4)
|
|
276
|
-
2.
|
|
282
|
+
2.0169
|
|
277
283
|
"""
|
|
278
284
|
lam = as_positive(lam, "lam")
|
|
279
285
|
mu = as_positive(mu, "mu")
|
|
@@ -311,6 +317,17 @@ def monte_carlo_gg1(
|
|
|
311
317
|
sojourn = depart - arrival
|
|
312
318
|
pct = np.percentile(wait, [50, 90, 95, 99])
|
|
313
319
|
|
|
320
|
+
# Precisión de Wq por medias de lotes (20 lotes, t de Student con 19 g.l. al 95 %); una sola réplica puede engañar
|
|
321
|
+
lotes = wait[: n_customers - n_customers % LOTES_MC].reshape(LOTES_MC, -1).mean(axis=1) if n_customers >= LOTES_MC else None
|
|
322
|
+
if lotes is not None and wait.mean() > 0:
|
|
323
|
+
semiancho = T_975_19 * float(lotes.std(ddof=1)) / math.sqrt(LOTES_MC)
|
|
324
|
+
if semiancho > PRECISION_RELATIVA_MC * wait.mean():
|
|
325
|
+
warnings.warn(
|
|
326
|
+
_t("advanced.aviso.monte_carlo_gg1.estimacion_imprecisa", wq=float(wait.mean()), semiancho=semiancho,
|
|
327
|
+
relativo=semiancho / float(wait.mean()), n=n_customers),
|
|
328
|
+
UserWarning, stacklevel=2,
|
|
329
|
+
)
|
|
330
|
+
|
|
314
331
|
return SimulationResult(
|
|
315
332
|
model=_t("advanced.modelo.monte_carlo_gg1.simulacion_ca2_cs2", ca2=ca2, cs2=cs2),
|
|
316
333
|
lam=lam,
|
|
@@ -253,12 +253,14 @@ def reorder_point(
|
|
|
253
253
|
lead_time : float
|
|
254
254
|
Tiempo de entrega medio L̄ (en las mismas unidades de tiempo que demand_rate).
|
|
255
255
|
demand_std : float
|
|
256
|
-
Desviación estándar de la demanda por periodo σ_D (por defecto 0).
|
|
256
|
+
Desviación estándar de la demanda por periodo σ_D (por defecto 0). Si la demanda se pronostica, use la
|
|
257
|
+
desviación del error de pronóstico por periodo (p. ej. el RMSE), que es la incertidumbre que cubre el stock
|
|
258
|
+
de seguridad.
|
|
257
259
|
lead_time_std : float
|
|
258
260
|
Desviación estándar del tiempo de entrega σ_L (por defecto 0).
|
|
259
261
|
service_level : float
|
|
260
|
-
|
|
261
|
-
Por defecto 0.95.
|
|
262
|
+
Nivel de servicio por ciclo (tipo 1): probabilidad de no tener faltante en un ciclo de reposición ∈ (0, 1).
|
|
263
|
+
No es la tasa de llenado (fracción de la demanda servida desde inventario), que suele ser mayor. Por defecto 0.95.
|
|
262
264
|
|
|
263
265
|
Returns
|
|
264
266
|
-------
|
|
@@ -368,6 +370,9 @@ class NewsvendorResult:
|
|
|
368
370
|
}])
|
|
369
371
|
|
|
370
372
|
|
|
373
|
+
CV_MAX_NEWSVENDOR_NORMAL = 0.5 # con CV = σ/μ > 0,5 la normal da P(D < 0) > 2,3 %
|
|
374
|
+
|
|
375
|
+
|
|
371
376
|
def newsvendor(
|
|
372
377
|
demand_mean: float,
|
|
373
378
|
demand_std: float,
|
|
@@ -425,6 +430,13 @@ def newsvendor(
|
|
|
425
430
|
if s >= c:
|
|
426
431
|
raise ValueError(_t("inventory.error.newsvendor.salvage_debe_ser_menor_cost"))
|
|
427
432
|
|
|
433
|
+
if sig > CV_MAX_NEWSVENDOR_NORMAL * mu:
|
|
434
|
+
# La normal asigna probabilidad apreciable a demanda negativa: Q* y las ventas esperadas pierden sentido
|
|
435
|
+
warnings.warn(
|
|
436
|
+
_t("inventory.aviso.newsvendor.demanda_normal_cv_alto", cv=sig / mu, p_negativa=NormalDist().cdf(-mu / sig)),
|
|
437
|
+
UserWarning, stacklevel=2,
|
|
438
|
+
)
|
|
439
|
+
|
|
428
440
|
Cu = p - c # underage cost (opportunity loss)
|
|
429
441
|
Co = c - s # overage cost (holding/disposal loss)
|
|
430
442
|
CR = Cu / (Cu + Co) # critical ratio
|
|
@@ -1587,11 +1599,14 @@ def rq_policy(
|
|
|
1587
1599
|
lead_time : float
|
|
1588
1600
|
Tiempo de entrega medio de reposición L̄.
|
|
1589
1601
|
demand_std : float
|
|
1590
|
-
Desviación estándar de la demanda por periodo σ_D (por defecto 0).
|
|
1602
|
+
Desviación estándar de la demanda por periodo σ_D (por defecto 0). Si la demanda se pronostica, use la
|
|
1603
|
+
desviación del error de pronóstico por periodo (p. ej. el RMSE), que es la incertidumbre que cubre el stock
|
|
1604
|
+
de seguridad.
|
|
1591
1605
|
lead_time_std : float
|
|
1592
1606
|
Desviación estándar del tiempo de entrega σ_L (por defecto 0).
|
|
1593
1607
|
service_level : float
|
|
1594
|
-
Nivel de servicio por ciclo ∈ (0, 1).
|
|
1608
|
+
Nivel de servicio por ciclo (tipo 1): probabilidad de no tener faltante en un ciclo de reposición ∈ (0, 1).
|
|
1609
|
+
No es la tasa de llenado (fracción de la demanda servida desde inventario), que suele ser mayor. Por defecto 0.95.
|
|
1595
1610
|
|
|
1596
1611
|
Returns
|
|
1597
1612
|
-------
|
|
@@ -1730,11 +1745,14 @@ def rs_policy(
|
|
|
1730
1745
|
review_period : float
|
|
1731
1746
|
Intervalo de revisión R (periodos entre revisiones de inventario).
|
|
1732
1747
|
demand_std : float
|
|
1733
|
-
Desviación estándar de la demanda por periodo σ_D (por defecto 0).
|
|
1748
|
+
Desviación estándar de la demanda por periodo σ_D (por defecto 0). Si la demanda se pronostica, use la
|
|
1749
|
+
desviación del error de pronóstico por periodo (p. ej. el RMSE), que es la incertidumbre que cubre el stock
|
|
1750
|
+
de seguridad.
|
|
1734
1751
|
lead_time_std : float
|
|
1735
1752
|
Desviación estándar del tiempo de entrega σ_L (por defecto 0).
|
|
1736
1753
|
service_level : float
|
|
1737
|
-
Nivel de servicio por ciclo ∈ (0, 1).
|
|
1754
|
+
Nivel de servicio por ciclo (tipo 1): probabilidad de no tener faltante en un ciclo de reposición ∈ (0, 1).
|
|
1755
|
+
No es la tasa de llenado (fracción de la demanda servida desde inventario), que suele ser mayor. Por defecto 0.95.
|
|
1738
1756
|
|
|
1739
1757
|
Returns
|
|
1740
1758
|
-------
|
|
@@ -2078,7 +2096,8 @@ def safety_stock_curve(
|
|
|
2078
2096
|
items : list of dict
|
|
2079
2097
|
Cada diccionario debe contener:
|
|
2080
2098
|
|
|
2081
|
-
- ``demand_std`` (float) — desviación estándar de la demanda por unidad de tiempo
|
|
2099
|
+
- ``demand_std`` (float) — desviación estándar de la demanda por unidad de tiempo (o del error de
|
|
2100
|
+
pronóstico, si la demanda se pronostica).
|
|
2082
2101
|
- ``lead_time`` (float) — tiempo de entrega de reposición (misma unidad de tiempo).
|
|
2083
2102
|
|
|
2084
2103
|
Claves opcionales:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import math
|
|
5
|
+
import warnings
|
|
5
6
|
from collections import deque
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from statistics import NormalDist
|
|
@@ -211,6 +212,33 @@ def _forward_backward(acts: dict, durations: dict) -> tuple:
|
|
|
211
212
|
return ES, EF, LS, LF, T, succ
|
|
212
213
|
|
|
213
214
|
|
|
215
|
+
def _ruta_critica_de_mayor_varianza(acts: dict, crit: list, variances: dict, ES: dict, EF: dict, T: float) -> tuple:
|
|
216
|
+
"""Ruta crítica (de inicio a fin) con la mayor varianza y esa varianza.
|
|
217
|
+
|
|
218
|
+
Con varias rutas críticas empatadas, la varianza del proyecto es la de **una** ruta (la convención es la de mayor
|
|
219
|
+
varianza), no la suma de todas las actividades críticas.
|
|
220
|
+
"""
|
|
221
|
+
criticas = set(crit)
|
|
222
|
+
mejor: dict[str, float] = {}
|
|
223
|
+
previa: dict[str, str | None] = {}
|
|
224
|
+
for nm in [n for n in _toposort_and_succ(acts)[0] if n in criticas]:
|
|
225
|
+
candidatas = [p for p in acts[nm]["predecessors"] if p in criticas and abs(EF[p] - ES[nm]) < 1e-9]
|
|
226
|
+
p_max = max(candidatas, key=lambda p: mejor[p], default=None)
|
|
227
|
+
mejor[nm] = variances[nm] + (mejor[p_max] if p_max is not None else 0.0)
|
|
228
|
+
previa[nm] = p_max
|
|
229
|
+
finales = [nm for nm in mejor if abs(EF[nm] - T) < 1e-9]
|
|
230
|
+
if not finales:
|
|
231
|
+
return [], 0.0
|
|
232
|
+
final = max(finales, key=lambda n: mejor[n])
|
|
233
|
+
varianza = mejor[final]
|
|
234
|
+
nodo: str | None = final
|
|
235
|
+
ruta: list[str] = []
|
|
236
|
+
while nodo is not None:
|
|
237
|
+
ruta.append(nodo)
|
|
238
|
+
nodo = previa[nodo]
|
|
239
|
+
return ruta[::-1], varianza
|
|
240
|
+
|
|
241
|
+
|
|
214
242
|
def _build_results(acts: dict, durations: dict, ES, EF, LS, LF, T, succ,
|
|
215
243
|
opt=None, ml=None, pess=None, variances=None) -> list:
|
|
216
244
|
results = []
|
|
@@ -382,7 +410,14 @@ def pert(activities: list) -> ProjectResult:
|
|
|
382
410
|
results = _build_results(acts, durations, ES, EF, LS, LF, T, succ,
|
|
383
411
|
opt=opt_d, ml=ml_d, pess=pess_d, variances=variances)
|
|
384
412
|
crit = [a.name for a in results if a.is_critical]
|
|
385
|
-
proj_var =
|
|
413
|
+
ruta, proj_var = _ruta_critica_de_mayor_varianza(acts, crit, variances, ES, EF, T)
|
|
414
|
+
sigma = math.sqrt(proj_var)
|
|
415
|
+
casi_criticas = [a.name for a in results if a.name not in ruta and a.total_float <= sigma + 1e-9]
|
|
416
|
+
if sigma > 0 and casi_criticas:
|
|
417
|
+
warnings.warn(
|
|
418
|
+
_t("project.aviso.pert.rutas_casi_criticas", n=len(casi_criticas), actividades=", ".join(casi_criticas[:5]), sigma=sigma),
|
|
419
|
+
UserWarning, stacklevel=2,
|
|
420
|
+
)
|
|
386
421
|
|
|
387
422
|
return ProjectResult(
|
|
388
423
|
activities=results,
|
|
@@ -199,7 +199,8 @@ def series_system(
|
|
|
199
199
|
t : float, optional
|
|
200
200
|
Tiempo para evaluar R(t).
|
|
201
201
|
mttr : float, optional
|
|
202
|
-
Tiempo medio de reparación
|
|
202
|
+
Tiempo medio de reparación. Disponibilidad A = MTBF / (MTBF + MTTR) del sistema: mientras se repara el
|
|
203
|
+
componente que falló el sistema está detenido y los demás no fallan.
|
|
203
204
|
|
|
204
205
|
Returns
|
|
205
206
|
-------
|
|
@@ -237,6 +238,11 @@ def series_system(
|
|
|
237
238
|
)
|
|
238
239
|
|
|
239
240
|
|
|
241
|
+
def _disponibilidad_componente(lam: float, mttr: float) -> float:
|
|
242
|
+
"""Disponibilidad en régimen de un componente reparable: MTBF / (MTBF + MTTR) = 1 / (1 + λ·MTTR)."""
|
|
243
|
+
return 1.0 / (1.0 + lam * mttr)
|
|
244
|
+
|
|
245
|
+
|
|
240
246
|
def parallel_system(
|
|
241
247
|
failure_rates: Sequence[float],
|
|
242
248
|
*,
|
|
@@ -256,7 +262,9 @@ def parallel_system(
|
|
|
256
262
|
t : float, optional
|
|
257
263
|
Tiempo para evaluar R(t).
|
|
258
264
|
mttr : float, optional
|
|
259
|
-
Tiempo medio de reparación
|
|
265
|
+
Tiempo medio de reparación de cada componente. La disponibilidad supone que cada componente se repara
|
|
266
|
+
en cuanto falla, de forma independiente: A = 1 − ∏(1 − Aᵢ), con Aᵢ = MTBFᵢ / (MTBFᵢ + MTTR). El MTBF
|
|
267
|
+
del resultado es el tiempo medio hasta la falla del sistema sin reparación.
|
|
260
268
|
|
|
261
269
|
Returns
|
|
262
270
|
-------
|
|
@@ -283,7 +291,8 @@ def parallel_system(
|
|
|
283
291
|
return _r_system("parallel", lams, None, tt)
|
|
284
292
|
|
|
285
293
|
mtbf_val = _mtbf_numeric(R_func, min(lams))
|
|
286
|
-
|
|
294
|
+
# Disponibilidad en régimen con reparación independiente de cada componente: A = 1 − ∏(1 − Aᵢ)
|
|
295
|
+
avail = 1.0 - math.prod(1.0 - _disponibilidad_componente(lam, mttr) for lam in lams) if mttr is not None else None
|
|
287
296
|
R_t = R_func(t) if t is not None else None
|
|
288
297
|
return ReliabilityResult(
|
|
289
298
|
topology="parallel",
|
|
@@ -326,7 +335,10 @@ def koon_system(
|
|
|
326
335
|
t : float, optional
|
|
327
336
|
Tiempo para evaluar R(t).
|
|
328
337
|
mttr : float, optional
|
|
329
|
-
Tiempo medio de reparación
|
|
338
|
+
Tiempo medio de reparación de cada componente. La disponibilidad supone reparación independiente de cada
|
|
339
|
+
componente: A = Σ_{j=k}^{n} C(n,j)·aʲ·(1 − a)ⁿ⁻ʲ, con a = MTBF / (MTBF + MTTR) de un componente. El MTBF
|
|
340
|
+
del resultado es el tiempo medio hasta la falla del sistema sin reparación. Con k = n, ``series_system``
|
|
341
|
+
supone en cambio que los demás componentes no fallan mientras el sistema está detenido.
|
|
330
342
|
|
|
331
343
|
Returns
|
|
332
344
|
-------
|
|
@@ -357,7 +369,12 @@ def koon_system(
|
|
|
357
369
|
# Exact MTBF for k-of-n identical exponential: (1/λ) * Σ_{j=k}^{n} 1/j
|
|
358
370
|
mtbf_val = (1.0 / lam) * sum(1.0 / j for j in range(k, n + 1))
|
|
359
371
|
|
|
360
|
-
|
|
372
|
+
if mttr is not None:
|
|
373
|
+
# Reparación independiente: cada componente está disponible con probabilidad a, y el sistema si funcionan ≥ k
|
|
374
|
+
a = _disponibilidad_componente(lam, mttr)
|
|
375
|
+
avail: float | None = sum(math.comb(n, j) * a**j * (1.0 - a) ** (n - j) for j in range(k, n + 1))
|
|
376
|
+
else:
|
|
377
|
+
avail = None
|
|
361
378
|
R_t = _r_system("k-of-n", lams, k, t) if t is not None else None
|
|
362
379
|
return ReliabilityResult(
|
|
363
380
|
topology="k-of-n",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: walopy
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -28,12 +28,14 @@ src/walopy.egg-info/requires.txt
|
|
|
28
28
|
src/walopy.egg-info/top_level.txt
|
|
29
29
|
tests/test_abc_xyz.py
|
|
30
30
|
tests/test_advanced.py
|
|
31
|
+
tests/test_avisos_validez.py
|
|
31
32
|
tests/test_bottleneck.py
|
|
32
33
|
tests/test_cli.py
|
|
33
34
|
tests/test_complejidad.py
|
|
34
35
|
tests/test_contrato_entradas.py
|
|
35
36
|
tests/test_cpm_pert.py
|
|
36
37
|
tests/test_dibujo_plotly.py
|
|
38
|
+
tests/test_disponibilidad_redundante.py
|
|
37
39
|
tests/test_documentacion.py
|
|
38
40
|
tests/test_documentacion_ingles.py
|
|
39
41
|
tests/test_ejemplos.py
|
|
@@ -55,6 +57,7 @@ tests/test_network.py
|
|
|
55
57
|
tests/test_new_models.py
|
|
56
58
|
tests/test_notas_release.py
|
|
57
59
|
tests/test_operations.py
|
|
60
|
+
tests/test_pert_varianza.py
|
|
58
61
|
tests/test_plotting.py
|
|
59
62
|
tests/test_politicas_inventario.py
|
|
60
63
|
tests/test_propiedades_algoritmos.py
|
|
@@ -76,6 +76,7 @@ def test_monte_carlo_gg1_deterministic_service():
|
|
|
76
76
|
assert r_sim.Wq_mean == pytest.approx(r_analytic.Wq, rel=0.05)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
@pytest.mark.filterwarnings("ignore::UserWarning") # solo el orden de los percentiles; la precisión avisa aparte
|
|
79
80
|
def test_monte_carlo_percentiles_ordered():
|
|
80
81
|
r = monte_carlo_gg1(lam=3.0, mu=5.0, ca2=1.0, cs2=1.0,
|
|
81
82
|
n_customers=10_000, seed=7)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Avisos de validez (0.4.4): el modelo se usa fuera del rango en que su resultado es fiable.
|
|
2
|
+
|
|
3
|
+
* ``newsvendor`` con demanda normal y CV > 0,5: la normal asigna probabilidad apreciable a demanda negativa.
|
|
4
|
+
* ``monte_carlo_gg1``: el semiancho al 95 % de Wq (medias de 20 lotes) supera el 10 %; una réplica puede engañar.
|
|
5
|
+
* ``pert``: ver ``test_pert_varianza.py``.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
import walopy as wl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_newsvendor_avisa_con_cv_alto():
|
|
17
|
+
with pytest.warns(UserWarning) as avisos:
|
|
18
|
+
wl.newsvendor(20.0, 20.0, 10.0, 4.0)
|
|
19
|
+
assert "15.9%" in str(avisos[0].message) or "15,9" in str(avisos[0].message) # P(D < 0) del modelo
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_newsvendor_no_avisa_con_cv_moderado():
|
|
23
|
+
with warnings.catch_warnings():
|
|
24
|
+
warnings.simplefilter("error")
|
|
25
|
+
wl.newsvendor(100.0, 30.0, 10.0, 4.0)
|
|
26
|
+
wl.newsvendor(100.0, 50.0, 10.0, 4.0) # CV = 0,5 justo en el límite
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_simulacion_avisa_si_la_estimacion_es_imprecisa():
|
|
30
|
+
with pytest.warns(UserWarning) as avisos:
|
|
31
|
+
wl.monte_carlo_gg1(0.95, 1.0, 1.0, 1.0, n_customers=20_000, seed=1)
|
|
32
|
+
assert "n_customers" in str(avisos[0].message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_simulacion_no_avisa_si_la_estimacion_es_precisa():
|
|
36
|
+
with warnings.catch_warnings():
|
|
37
|
+
warnings.simplefilter("error")
|
|
38
|
+
wl.monte_carlo_gg1(2.0, 3.0, 1.0, 1.0, n_customers=50_000, seed=1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_el_aviso_no_cambia_el_resultado():
|
|
42
|
+
with warnings.catch_warnings():
|
|
43
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
44
|
+
a = wl.monte_carlo_gg1(0.95, 1.0, 1.0, 1.0, n_customers=5_000, seed=3)
|
|
45
|
+
b = wl.newsvendor(20.0, 20.0, 10.0, 4.0)
|
|
46
|
+
assert a.Wq_mean > 0 and b.optimal_qty > 0
|
|
@@ -119,34 +119,40 @@ def test_cpm_empty_raises():
|
|
|
119
119
|
|
|
120
120
|
# ─── PERT ─────────────────────────────────────────────────────────────────────
|
|
121
121
|
|
|
122
|
+
def _pert_red_de_prueba():
|
|
123
|
+
# A→C y B→D duran lo mismo (5): dos rutas críticas empatadas, así que pert avisa de rutas casi críticas
|
|
124
|
+
with pytest.warns(UserWarning):
|
|
125
|
+
return wl.pert(PERT_ACTS)
|
|
126
|
+
|
|
127
|
+
|
|
122
128
|
def test_pert_returns_result():
|
|
123
|
-
r =
|
|
129
|
+
r = _pert_red_de_prueba()
|
|
124
130
|
assert isinstance(r, wl.ProjectResult)
|
|
125
131
|
|
|
126
132
|
|
|
127
133
|
def test_pert_method():
|
|
128
|
-
r =
|
|
134
|
+
r = _pert_red_de_prueba()
|
|
129
135
|
assert r.method == "PERT"
|
|
130
136
|
|
|
131
137
|
|
|
132
138
|
def test_pert_expected_duration():
|
|
133
|
-
r =
|
|
139
|
+
r = _pert_red_de_prueba()
|
|
134
140
|
assert r.project_duration == pytest.approx(5.0, abs=1e-9)
|
|
135
141
|
|
|
136
142
|
|
|
137
143
|
def test_pert_has_variance():
|
|
138
|
-
r =
|
|
144
|
+
r = _pert_red_de_prueba()
|
|
139
145
|
assert r.project_variance is not None
|
|
140
146
|
assert r.project_variance > 0
|
|
141
147
|
|
|
142
148
|
|
|
143
149
|
def test_pert_std_is_sqrt_variance():
|
|
144
|
-
r =
|
|
150
|
+
r = _pert_red_de_prueba()
|
|
145
151
|
assert r.project_std == pytest.approx(math.sqrt(r.project_variance), rel=1e-9)
|
|
146
152
|
|
|
147
153
|
|
|
148
154
|
def test_pert_activity_variance_formula():
|
|
149
|
-
r =
|
|
155
|
+
r = _pert_red_de_prueba()
|
|
150
156
|
for a in r.activities:
|
|
151
157
|
if a.variance is not None:
|
|
152
158
|
expected = ((a.pessimistic - a.optimistic) / 6.0) ** 2
|
|
@@ -154,12 +160,12 @@ def test_pert_activity_variance_formula():
|
|
|
154
160
|
|
|
155
161
|
|
|
156
162
|
def test_pert_probability_below_mean_is_50pct():
|
|
157
|
-
r =
|
|
163
|
+
r = _pert_red_de_prueba()
|
|
158
164
|
assert r.probability(r.project_duration) == pytest.approx(0.5, abs=1e-6)
|
|
159
165
|
|
|
160
166
|
|
|
161
167
|
def test_pert_probability_increases_with_target():
|
|
162
|
-
r =
|
|
168
|
+
r = _pert_red_de_prueba()
|
|
163
169
|
assert r.probability(r.project_duration + 1) > r.probability(r.project_duration)
|
|
164
170
|
|
|
165
171
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""W-33: la disponibilidad de los sistemas redundantes suponía que nada se repara hasta que cae todo el sistema.
|
|
2
|
+
|
|
3
|
+
``parallel_system`` y ``koon_system`` calculaban ``A = MTTF_sistema / (MTTF_sistema + MTTR)``: con dos componentes en paralelo
|
|
4
|
+
(λ = 0,01, MTTR = 10) daban 0,9375, cuando con reparación independiente de cada componente es 1 − (1 − Aᵢ)² = 0,9917
|
|
5
|
+
(8 veces menos indisponibilidad). Ahora se usa la disponibilidad en régimen con reparación independiente.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import walopy as wl
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _a(lam: float, mttr: float) -> float:
|
|
18
|
+
return (1 / lam) / (1 / lam + mttr)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_paralelo_de_dos_con_reparacion_independiente():
|
|
22
|
+
r = wl.parallel_system([0.01, 0.01], mttr=10.0)
|
|
23
|
+
assert r.availability == pytest.approx(1 - (1 - 100 / 110) ** 2) # 0,9917; antes 0,9375
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_k_de_n_es_la_binomial_de_la_disponibilidad_de_cada_componente():
|
|
27
|
+
a = _a(0.01, 10.0)
|
|
28
|
+
r = wl.koon_system(n=3, k=2, failure_rate=0.01, mttr=10.0)
|
|
29
|
+
assert r.availability == pytest.approx(3 * a**2 * (1 - a) + a**3)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("semilla", range(40))
|
|
33
|
+
def test_paralelo_y_k_de_n_contra_formulas_independientes(semilla):
|
|
34
|
+
r = random.Random(3300 + semilla)
|
|
35
|
+
lams = [r.uniform(1e-4, 0.1) for _ in range(r.randint(2, 5))]
|
|
36
|
+
mttr = r.uniform(0.1, 50)
|
|
37
|
+
esperado = 1 - math.prod(1 - _a(x, mttr) for x in lams)
|
|
38
|
+
assert wl.parallel_system(lams, mttr=mttr).availability == pytest.approx(esperado, rel=1e-12)
|
|
39
|
+
n = r.randint(2, 6)
|
|
40
|
+
k = r.randint(1, n - 1)
|
|
41
|
+
a = _a(lams[0], mttr)
|
|
42
|
+
binomial = sum(math.comb(n, j) * a**j * (1 - a) ** (n - j) for j in range(k, n + 1))
|
|
43
|
+
assert wl.koon_system(n=n, k=k, failure_rate=lams[0], mttr=mttr).availability == pytest.approx(binomial, rel=1e-12)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_redundancia_nunca_es_menos_disponible_que_un_componente():
|
|
47
|
+
for lam, mttr in ((0.01, 10.0), (0.2, 3.0), (1e-3, 100.0)):
|
|
48
|
+
a = _a(lam, mttr)
|
|
49
|
+
assert wl.parallel_system([lam, lam], mttr=mttr).availability > a
|
|
50
|
+
assert wl.koon_system(n=3, k=1, failure_rate=lam, mttr=mttr).availability > a
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_un_solo_componente_y_la_serie_no_cambian():
|
|
54
|
+
# Guarda: un componente (paralelo de 1 o 1-de-1) y la serie conservan A = MTBF / (MTBF + MTTR)
|
|
55
|
+
assert wl.parallel_system([0.01], mttr=10.0).availability == pytest.approx(_a(0.01, 10.0))
|
|
56
|
+
assert wl.koon_system(n=1, k=1, failure_rate=0.01, mttr=10.0).availability == pytest.approx(_a(0.01, 10.0))
|
|
57
|
+
assert wl.series_system([0.01, 0.02], mttr=10.0).availability == pytest.approx(1 / (1 + 0.03 * 10.0))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_sin_mttr_no_hay_disponibilidad():
|
|
61
|
+
# Guarda
|
|
62
|
+
assert wl.parallel_system([0.01, 0.02]).availability is None
|
|
63
|
+
assert wl.koon_system(n=3, k=2, failure_rate=0.01).availability is None
|
|
@@ -135,6 +135,9 @@ AÑADIDAS_TRAS_V030 = {
|
|
|
135
135
|
"solver.error.sensitivity.values_no_puede_estar_vacio", # W-32
|
|
136
136
|
"solver.error.sensitivity.param_no_es_parametro_modelo",
|
|
137
137
|
"solver.error.compare.sin_resultados", # W-31
|
|
138
|
+
"project.aviso.pert.rutas_casi_criticas", # 0.4.4: avisos de validez de los modelos
|
|
139
|
+
"inventory.aviso.newsvendor.demanda_normal_cv_alto",
|
|
140
|
+
"advanced.aviso.monte_carlo_gg1.estimacion_imprecisa",
|
|
138
141
|
"project.cabecera.to_frame.ht", # «HT»/«HL» (holgura total/libre): acrónimos españoles que el extractor no veía; ya existían en v0.3.0
|
|
139
142
|
"project.cabecera.to_frame.hl",
|
|
140
143
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""W-34: con varias rutas críticas empatadas, ``pert`` sumaba las varianzas de **todas** las actividades críticas.
|
|
2
|
+
|
|
3
|
+
La varianza del proyecto en PERT es la de una ruta (la convención, con empates, es la de mayor varianza). Con 5 rutas
|
|
4
|
+
idénticas de 3 actividades, σ salía 3,23 en lugar de 1,44 (√5 veces mayor). Además ``pert`` avisa cuando hay rutas casi
|
|
5
|
+
críticas, porque entonces la probabilidad PERT (una sola ruta) es optimista.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
import walopy as wl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _rutas_paralelas(n_rutas: int = 5) -> list[dict]:
|
|
17
|
+
acts = []
|
|
18
|
+
for p in range(n_rutas):
|
|
19
|
+
previa: list[str] = []
|
|
20
|
+
for j in range(3):
|
|
21
|
+
nombre = f"P{p}A{j}"
|
|
22
|
+
acts.append({"name": nombre, "optimistic": 4, "most_likely": 5, "pessimistic": 9, "predecessors": previa})
|
|
23
|
+
previa = [nombre]
|
|
24
|
+
acts.append({"name": "FIN", "optimistic": 1, "most_likely": 1, "pessimistic": 1, "predecessors": [f"P{p}A2" for p in range(n_rutas)]})
|
|
25
|
+
return acts
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _pert(acts):
|
|
29
|
+
with warnings.catch_warnings():
|
|
30
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
31
|
+
return wl.pert(acts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_rutas_empatadas_usan_la_varianza_de_una_ruta():
|
|
35
|
+
r = _pert(_rutas_paralelas())
|
|
36
|
+
assert r.project_variance == pytest.approx(3 * (5 / 6) ** 2) # antes 15 · (5/6)² = 10,42
|
|
37
|
+
assert len(r.critical_path) == 16 # el conjunto de actividades críticas no cambia
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_con_empate_se_elige_la_ruta_de_mayor_varianza():
|
|
41
|
+
acts = [
|
|
42
|
+
{"name": "A", "optimistic": 2, "most_likely": 5, "pessimistic": 8, "predecessors": []}, # var 1
|
|
43
|
+
{"name": "B", "optimistic": 4, "most_likely": 5, "pessimistic": 6, "predecessors": []}, # var 1/9
|
|
44
|
+
{"name": "C", "optimistic": 1, "most_likely": 1, "pessimistic": 1, "predecessors": ["A", "B"]},
|
|
45
|
+
]
|
|
46
|
+
assert _pert(acts).project_variance == pytest.approx(1.0) # antes 1 + 1/9
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_una_sola_ruta_critica_no_cambia():
|
|
50
|
+
# Guarda: sin empates la varianza es la de la ruta crítica, como siempre
|
|
51
|
+
acts = [
|
|
52
|
+
{"name": "A", "optimistic": 2, "most_likely": 4, "pessimistic": 6, "predecessors": []},
|
|
53
|
+
{"name": "B", "optimistic": 1, "most_likely": 1, "pessimistic": 1, "predecessors": []},
|
|
54
|
+
{"name": "C", "optimistic": 1, "most_likely": 2, "pessimistic": 3, "predecessors": ["A", "B"]},
|
|
55
|
+
]
|
|
56
|
+
r = wl.pert(acts)
|
|
57
|
+
assert r.project_variance == pytest.approx((4 / 6) ** 2 + (2 / 6) ** 2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_avisa_si_hay_rutas_casi_criticas():
|
|
61
|
+
with pytest.warns(UserWarning) as avisos:
|
|
62
|
+
wl.pert(_rutas_paralelas())
|
|
63
|
+
assert "P1A0" in str(avisos[0].message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_no_avisa_si_las_demas_rutas_tienen_holgura_amplia():
|
|
67
|
+
acts = [
|
|
68
|
+
{"name": "Larga", "optimistic": 20, "most_likely": 22, "pessimistic": 24, "predecessors": []},
|
|
69
|
+
{"name": "Corta", "optimistic": 1, "most_likely": 2, "pessimistic": 3, "predecessors": []},
|
|
70
|
+
{"name": "Fin", "optimistic": 1, "most_likely": 1, "pessimistic": 1, "predecessors": ["Larga", "Corta"]},
|
|
71
|
+
]
|
|
72
|
+
with warnings.catch_warnings():
|
|
73
|
+
warnings.simplefilter("error")
|
|
74
|
+
wl.pert(acts)
|
|
@@ -37,7 +37,7 @@ RESULTADOS = {
|
|
|
37
37
|
"break_even_sales_real": lambda: wl.break_even_sales(1000.0, 0.4, actual_revenue=3000.0),
|
|
38
38
|
"eoq": lambda: wl.eoq(1000.0, 50.0, 2.0),
|
|
39
39
|
"optimize_servers": lambda: wl.optimize_servers(4.0, 3.0, cost_per_server=10.0, cost_per_wait=5.0),
|
|
40
|
-
"monte_carlo": lambda: wl.monte_carlo_gg1(2.0, 3.0, 1.0, 1.0, n_customers=
|
|
40
|
+
"monte_carlo": lambda: wl.monte_carlo_gg1(2.0, 3.0, 1.0, 1.0, n_customers=50_000, seed=1), # sin aviso de precisión
|
|
41
41
|
"oee_arbol": lambda: wl.oee_kpi_tree(0.9, 0.9, 0.9),
|
|
42
42
|
"throughput_arbol": lambda: wl.throughput_kpi_tree(80.0, 100.0, 0.05),
|
|
43
43
|
"roi_arbol": lambda: wl.roi_kpi_tree(1000.0, 200.0, 3.0, 100.0, 500.0),
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|