walopy 0.4.2__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.2/src/walopy.egg-info → walopy-0.4.4}/PKG-INFO +1 -1
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/__init__.py +1 -1
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_catalogo_en.py +7 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_catalogo_es.py +7 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/advanced.py +19 -2
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/inventory.py +27 -8
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/project.py +36 -1
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/reliability.py +22 -5
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/solver.py +40 -9
- {walopy-0.4.2 → walopy-0.4.4/src/walopy.egg-info}/PKG-INFO +1 -1
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/SOURCES.txt +9 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_advanced.py +1 -0
- walopy-0.4.4/tests/test_avisos_validez.py +46 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_cpm_pert.py +14 -8
- walopy-0.4.4/tests/test_dibujo_plotly.py +153 -0
- walopy-0.4.4/tests/test_disponibilidad_redundante.py +63 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventario_i18n.py +7 -0
- walopy-0.4.4/tests/test_pert_varianza.py +74 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_plotting.py +1 -1
- walopy-0.4.4/tests/test_propiedades_algoritmos.py +194 -0
- walopy-0.4.4/tests/test_propiedades_inventario_fiabilidad.py +221 -0
- walopy-0.4.4/tests/test_servidores_cmax.py +59 -0
- walopy-0.4.4/tests/test_servidores_minimos.py +79 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_solver.py +1 -0
- walopy-0.4.4/tests/test_solver_contratos.py +142 -0
- {walopy-0.4.2 → walopy-0.4.4}/LICENSE +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/README.md +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/pyproject.toml +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/setup.cfg +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/__main__.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_i18n.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_utils.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/bottleneck.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/fitting.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/kpi.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/network.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/operations.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/plotting.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/py.typed +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/queuing.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy/scheduling.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/dependency_links.txt +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/requires.txt +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/top_level.txt +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_abc_xyz.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_bottleneck.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_cli.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_complejidad.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_contrato_entradas.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_documentacion.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_documentacion_ingles.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_ejemplos.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_exchange_curve.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_fitting.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_i18n.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_i18n_paridad.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_idioma.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventory.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventory_new.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_kpi.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_kpi_grafica.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_mg1.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_mmc_estable.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_mrp.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_neh_flowshop.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_network.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_new_models.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_notas_release.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_operations.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_politicas_inventario.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_propiedades_diferenciales.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_queuing.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_readme.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_referencias_externas.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_regresion_validacion.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_reliability.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_safety_stock_curve.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_salida_identica.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_salida_por_idioma.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_scheduling.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_scorecard.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_trazabilidad.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_version.py +0 -0
- {walopy-0.4.2 → walopy-0.4.4}/tests/test_wagner_whitin_ceros.py +0 -0
- {walopy-0.4.2 → 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}'.",
|
|
@@ -584,6 +587,10 @@ EN: dict[str, str] = {
|
|
|
584
587
|
"solver.error.solve_lam.supera_incluso_muy_baja_objetivo": "{target_metric} exceeds {target_value} even with a very low λ. The target may be infeasible for this model / μ.",
|
|
585
588
|
"solver.error.solve_mu.sigue_superando_objetivo_puede_ser": "{target_metric} still exceeds {target_value} with μ = {mu_max:.4g}. The target may be numerically infeasible.",
|
|
586
589
|
"solver.error.solve_servers.pudo_cumplir_hasta_servidores_considere": "Could not meet {target_metric} ≤ {target_value} with up to {c_max} servers. Consider increasing μ or relaxing the target.",
|
|
590
|
+
"solver.error.optimize_servers.sin_servidores_estables_hasta_c_max": "With λ={lam} and μ={mu} at least {c_min} servers are needed for the system to be stable, but c_max={c_max}. Increase c_max.",
|
|
591
|
+
"solver.error.sensitivity.values_no_puede_estar_vacio": "values cannot be empty.",
|
|
592
|
+
"solver.error.sensitivity.param_no_es_parametro_modelo": "{param!r} is not a parameter of {modelo}.",
|
|
593
|
+
"solver.error.compare.sin_resultados": "compare needs at least one result.",
|
|
587
594
|
"solver.etiqueta.compare.escenario": "scenario_{expr}",
|
|
588
595
|
"solver.etiqueta.summary.convergio": "did NOT converge",
|
|
589
596
|
"solver.etiqueta.summary.convergio_2": "converged",
|
|
@@ -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}'.",
|
|
@@ -584,6 +587,10 @@ ES: dict[str, str] = {
|
|
|
584
587
|
"solver.error.solve_lam.supera_incluso_muy_baja_objetivo": "{target_metric} supera {target_value} incluso con λ muy baja. El objetivo puede ser infactible para este modelo / μ.",
|
|
585
588
|
"solver.error.solve_mu.sigue_superando_objetivo_puede_ser": "{target_metric} sigue superando {target_value} con μ = {mu_max:.4g}. El objetivo puede ser numéricamente infactible.",
|
|
586
589
|
"solver.error.solve_servers.pudo_cumplir_hasta_servidores_considere": "No se pudo cumplir {target_metric} ≤ {target_value} con hasta {c_max} servidores. Considere aumentar μ o relajar el objetivo.",
|
|
590
|
+
"solver.error.optimize_servers.sin_servidores_estables_hasta_c_max": "Con λ={lam} y μ={mu} hacen falta al menos {c_min} servidores para que el sistema sea estable, pero c_max={c_max}. Aumente c_max.",
|
|
591
|
+
"solver.error.sensitivity.values_no_puede_estar_vacio": "values no puede estar vacío.",
|
|
592
|
+
"solver.error.sensitivity.param_no_es_parametro_modelo": "{param!r} no es un parámetro de {modelo}.",
|
|
593
|
+
"solver.error.compare.sin_resultados": "compare necesita al menos un resultado.",
|
|
587
594
|
"solver.etiqueta.compare.escenario": "escenario_{expr}",
|
|
588
595
|
"solver.etiqueta.summary.convergio": "NO convergió",
|
|
589
596
|
"solver.etiqueta.summary.convergio_2": "convergió",
|
|
@@ -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,7 @@
|
|
|
1
1
|
"""Solvers: encuentran parámetros óptimos o metas definidas por la persona usuaria en modelos de colas y operaciones."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import inspect
|
|
4
5
|
from collections.abc import Sequence
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Callable
|
|
@@ -9,7 +10,7 @@ import numpy as np
|
|
|
9
10
|
import pandas as pd
|
|
10
11
|
|
|
11
12
|
from ._i18n import t as _t
|
|
12
|
-
from ._utils import as_positive
|
|
13
|
+
from ._utils import MAX_SERVIDORES, as_int_positive, as_positive
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
import plotly.graph_objects as go
|
|
@@ -316,6 +317,14 @@ def solve_mu(
|
|
|
316
317
|
# solve_servers — minimum c to meet a target
|
|
317
318
|
# ---------------------------------------------------------------------------
|
|
318
319
|
|
|
320
|
+
def _min_servidores_estables(lam: float, mu: float) -> int:
|
|
321
|
+
"""Menor número de servidores *c* con ρ = λ/(c·μ) < 1 (la misma condición que exige ``mmc``)."""
|
|
322
|
+
c = max(1, int(lam / mu))
|
|
323
|
+
while lam / (c * mu) >= 1.0:
|
|
324
|
+
c += 1
|
|
325
|
+
return c
|
|
326
|
+
|
|
327
|
+
|
|
319
328
|
def solve_servers(
|
|
320
329
|
target_metric: str,
|
|
321
330
|
target_value: float,
|
|
@@ -355,15 +364,16 @@ def solve_servers(
|
|
|
355
364
|
--------
|
|
356
365
|
>>> r = solve_servers(target_metric='Wq', target_value=0.5, lam=4.0, mu=3.0)
|
|
357
366
|
>>> round(r.value, 4)
|
|
358
|
-
|
|
367
|
+
2.0
|
|
359
368
|
"""
|
|
360
369
|
from .queuing import mmc
|
|
361
370
|
|
|
362
371
|
lam = as_positive(lam, "lam")
|
|
363
372
|
mu = as_positive(mu, "mu")
|
|
364
373
|
target_value = as_positive(target_value, "target_value")
|
|
374
|
+
c_max = as_int_positive(c_max, "c_max", max=MAX_SERVIDORES)
|
|
365
375
|
|
|
366
|
-
c_min_stable =
|
|
376
|
+
c_min_stable = _min_servidores_estables(lam, mu)
|
|
367
377
|
|
|
368
378
|
for c in range(c_min_stable, c_max + 1):
|
|
369
379
|
result = mmc(lam, mu, c)
|
|
@@ -428,7 +438,7 @@ def optimize_servers(
|
|
|
428
438
|
--------
|
|
429
439
|
>>> r = optimize_servers(lam=4.0, mu=3.0, cost_per_server=10.0, cost_per_wait=5.0)
|
|
430
440
|
>>> round(r.min_cost, 4)
|
|
431
|
-
|
|
441
|
+
25.3333
|
|
432
442
|
"""
|
|
433
443
|
from .queuing import mmc
|
|
434
444
|
|
|
@@ -436,8 +446,11 @@ def optimize_servers(
|
|
|
436
446
|
mu = as_positive(mu, "mu")
|
|
437
447
|
cost_per_server = as_positive(cost_per_server, "cost_per_server")
|
|
438
448
|
cost_per_wait = as_positive(cost_per_wait, "cost_per_wait")
|
|
449
|
+
c_max = as_int_positive(c_max, "c_max", max=MAX_SERVIDORES)
|
|
439
450
|
|
|
440
|
-
c_min =
|
|
451
|
+
c_min = _min_servidores_estables(lam, mu)
|
|
452
|
+
if c_min > c_max:
|
|
453
|
+
raise ValueError(_t("solver.error.optimize_servers.sin_servidores_estables_hasta_c_max", lam=lam, mu=mu, c_min=c_min, c_max=c_max))
|
|
441
454
|
rows: list[dict[str, Any]] = []
|
|
442
455
|
|
|
443
456
|
for c in range(c_min, c_max + 1):
|
|
@@ -518,6 +531,15 @@ def sensitivity(
|
|
|
518
531
|
>>> import numpy as np
|
|
519
532
|
>>> df = sensitivity(mm1, "lam", np.linspace(0.5, 4.5, 20), mu=5.0)
|
|
520
533
|
"""
|
|
534
|
+
if len(values) == 0:
|
|
535
|
+
raise ValueError(_t("solver.error.sensitivity.values_no_puede_estar_vacio"))
|
|
536
|
+
try:
|
|
537
|
+
parametros = inspect.signature(model_fn).parameters
|
|
538
|
+
except (TypeError, ValueError): # callables sin firma inspeccionable: se intenta igualmente
|
|
539
|
+
parametros = None
|
|
540
|
+
if parametros is not None and param not in parametros and not any(p.kind is p.VAR_KEYWORD for p in parametros.values()):
|
|
541
|
+
nombre_modelo = repr(model_fn) if not hasattr(model_fn, "__name__") else model_fn.__name__
|
|
542
|
+
raise ValueError(_t("solver.error.sensitivity.param_no_es_parametro_modelo", param=param, modelo=nombre_modelo))
|
|
521
543
|
rows: list[dict[str, Any]] = []
|
|
522
544
|
for v in values:
|
|
523
545
|
try:
|
|
@@ -601,6 +623,8 @@ def batch_model(
|
|
|
601
623
|
|
|
602
624
|
# Pre-compute integer columns so iterrows() float-upcast can be reversed
|
|
603
625
|
int_cols = {col for col in df.columns if pd.api.types.is_integer_dtype(df[col].dtype)}
|
|
626
|
+
# Una columna entera con NaN (DataFrame disperso) pasa a float64: sus valores enteros vuelven a ser int al llamar al modelo
|
|
627
|
+
sparse_int_cols = {col for col in df.columns if pd.api.types.is_float_dtype(df[col].dtype) and df[col].isna().any()}
|
|
604
628
|
|
|
605
629
|
for _, row_series in df.iterrows():
|
|
606
630
|
# Merge: fixed_kwargs as base, row values (non-NaN) take precedence
|
|
@@ -609,7 +633,10 @@ def batch_model(
|
|
|
609
633
|
if isinstance(v, float) and np.isnan(v):
|
|
610
634
|
continue
|
|
611
635
|
# iterrows() upcasts int64 columns to float64; reverse that cast
|
|
612
|
-
|
|
636
|
+
if k in int_cols or (k in sparse_int_cols and isinstance(v, (float, np.floating)) and float(v).is_integer()):
|
|
637
|
+
kwargs[k] = int(v)
|
|
638
|
+
else:
|
|
639
|
+
kwargs[k] = v
|
|
613
640
|
|
|
614
641
|
try:
|
|
615
642
|
result = model_fn(**kwargs)
|
|
@@ -655,7 +682,8 @@ def compare(
|
|
|
655
682
|
----------
|
|
656
683
|
*results
|
|
657
684
|
Cualquier objeto de resultado de walopy (``QueueResult``, ``SimulationResult``,
|
|
658
|
-
``EOQResult``, etc.) o diccionarios simples.
|
|
685
|
+
``EOQResult``, etc.) o diccionarios simples. Si la tabla ``to_frame()`` de un resultado tiene varias filas
|
|
686
|
+
(pedidos, actividades…), se comparan sus campos escalares (costo total, duración del proyecto…).
|
|
659
687
|
labels : sequence of str, optional
|
|
660
688
|
Etiquetas de las filas. Por defecto ``'escenario_1'``, ``'escenario_2'``, …
|
|
661
689
|
|
|
@@ -680,11 +708,14 @@ def compare(
|
|
|
680
708
|
>>> tabla["c (servidores)"].tolist()
|
|
681
709
|
[1, 2]
|
|
682
710
|
"""
|
|
711
|
+
if len(results) == 0:
|
|
712
|
+
raise ValueError(_t("solver.error.compare.sin_resultados"))
|
|
683
713
|
rows: list[dict[str, Any]] = []
|
|
684
714
|
for i, r in enumerate(results):
|
|
685
715
|
label = labels[i] if (labels and i < len(labels)) else _t("solver.etiqueta.compare.escenario", expr=i + 1)
|
|
686
|
-
if hasattr(r, "to_frame")
|
|
687
|
-
|
|
716
|
+
tabla = r.to_frame() if hasattr(r, "to_frame") else None
|
|
717
|
+
if tabla is not None and len(tabla) == 1:
|
|
718
|
+
row = tabla.iloc[0].to_dict()
|
|
688
719
|
elif isinstance(r, dict):
|
|
689
720
|
row = dict(r)
|
|
690
721
|
elif hasattr(r, "__dict__"):
|
|
@@ -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,11 +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
|
|
37
|
+
tests/test_dibujo_plotly.py
|
|
38
|
+
tests/test_disponibilidad_redundante.py
|
|
36
39
|
tests/test_documentacion.py
|
|
37
40
|
tests/test_documentacion_ingles.py
|
|
38
41
|
tests/test_ejemplos.py
|
|
@@ -54,9 +57,12 @@ tests/test_network.py
|
|
|
54
57
|
tests/test_new_models.py
|
|
55
58
|
tests/test_notas_release.py
|
|
56
59
|
tests/test_operations.py
|
|
60
|
+
tests/test_pert_varianza.py
|
|
57
61
|
tests/test_plotting.py
|
|
58
62
|
tests/test_politicas_inventario.py
|
|
63
|
+
tests/test_propiedades_algoritmos.py
|
|
59
64
|
tests/test_propiedades_diferenciales.py
|
|
65
|
+
tests/test_propiedades_inventario_fiabilidad.py
|
|
60
66
|
tests/test_queuing.py
|
|
61
67
|
tests/test_readme.py
|
|
62
68
|
tests/test_referencias_externas.py
|
|
@@ -67,7 +73,10 @@ tests/test_salida_identica.py
|
|
|
67
73
|
tests/test_salida_por_idioma.py
|
|
68
74
|
tests/test_scheduling.py
|
|
69
75
|
tests/test_scorecard.py
|
|
76
|
+
tests/test_servidores_cmax.py
|
|
77
|
+
tests/test_servidores_minimos.py
|
|
70
78
|
tests/test_solver.py
|
|
79
|
+
tests/test_solver_contratos.py
|
|
71
80
|
tests/test_trazabilidad.py
|
|
72
81
|
tests/test_version.py
|
|
73
82
|
tests/test_wagner_whitin_ceros.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
|
|