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.
Files changed (85) hide show
  1. {walopy-0.4.2/src/walopy.egg-info → walopy-0.4.4}/PKG-INFO +1 -1
  2. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/__init__.py +1 -1
  3. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_catalogo_en.py +7 -0
  4. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_catalogo_es.py +7 -0
  5. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/advanced.py +19 -2
  6. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/inventory.py +27 -8
  7. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/project.py +36 -1
  8. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/reliability.py +22 -5
  9. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/solver.py +40 -9
  10. {walopy-0.4.2 → walopy-0.4.4/src/walopy.egg-info}/PKG-INFO +1 -1
  11. {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/SOURCES.txt +9 -0
  12. {walopy-0.4.2 → walopy-0.4.4}/tests/test_advanced.py +1 -0
  13. walopy-0.4.4/tests/test_avisos_validez.py +46 -0
  14. {walopy-0.4.2 → walopy-0.4.4}/tests/test_cpm_pert.py +14 -8
  15. walopy-0.4.4/tests/test_dibujo_plotly.py +153 -0
  16. walopy-0.4.4/tests/test_disponibilidad_redundante.py +63 -0
  17. {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventario_i18n.py +7 -0
  18. walopy-0.4.4/tests/test_pert_varianza.py +74 -0
  19. {walopy-0.4.2 → walopy-0.4.4}/tests/test_plotting.py +1 -1
  20. walopy-0.4.4/tests/test_propiedades_algoritmos.py +194 -0
  21. walopy-0.4.4/tests/test_propiedades_inventario_fiabilidad.py +221 -0
  22. walopy-0.4.4/tests/test_servidores_cmax.py +59 -0
  23. walopy-0.4.4/tests/test_servidores_minimos.py +79 -0
  24. {walopy-0.4.2 → walopy-0.4.4}/tests/test_solver.py +1 -0
  25. walopy-0.4.4/tests/test_solver_contratos.py +142 -0
  26. {walopy-0.4.2 → walopy-0.4.4}/LICENSE +0 -0
  27. {walopy-0.4.2 → walopy-0.4.4}/README.md +0 -0
  28. {walopy-0.4.2 → walopy-0.4.4}/pyproject.toml +0 -0
  29. {walopy-0.4.2 → walopy-0.4.4}/setup.cfg +0 -0
  30. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/__main__.py +0 -0
  31. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_i18n.py +0 -0
  32. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/_utils.py +0 -0
  33. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/bottleneck.py +0 -0
  34. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/fitting.py +0 -0
  35. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/kpi.py +0 -0
  36. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/network.py +0 -0
  37. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/operations.py +0 -0
  38. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/plotting.py +0 -0
  39. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/py.typed +0 -0
  40. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/queuing.py +0 -0
  41. {walopy-0.4.2 → walopy-0.4.4}/src/walopy/scheduling.py +0 -0
  42. {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/dependency_links.txt +0 -0
  43. {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/requires.txt +0 -0
  44. {walopy-0.4.2 → walopy-0.4.4}/src/walopy.egg-info/top_level.txt +0 -0
  45. {walopy-0.4.2 → walopy-0.4.4}/tests/test_abc_xyz.py +0 -0
  46. {walopy-0.4.2 → walopy-0.4.4}/tests/test_bottleneck.py +0 -0
  47. {walopy-0.4.2 → walopy-0.4.4}/tests/test_cli.py +0 -0
  48. {walopy-0.4.2 → walopy-0.4.4}/tests/test_complejidad.py +0 -0
  49. {walopy-0.4.2 → walopy-0.4.4}/tests/test_contrato_entradas.py +0 -0
  50. {walopy-0.4.2 → walopy-0.4.4}/tests/test_documentacion.py +0 -0
  51. {walopy-0.4.2 → walopy-0.4.4}/tests/test_documentacion_ingles.py +0 -0
  52. {walopy-0.4.2 → walopy-0.4.4}/tests/test_ejemplos.py +0 -0
  53. {walopy-0.4.2 → walopy-0.4.4}/tests/test_exchange_curve.py +0 -0
  54. {walopy-0.4.2 → walopy-0.4.4}/tests/test_fitting.py +0 -0
  55. {walopy-0.4.2 → walopy-0.4.4}/tests/test_i18n.py +0 -0
  56. {walopy-0.4.2 → walopy-0.4.4}/tests/test_i18n_paridad.py +0 -0
  57. {walopy-0.4.2 → walopy-0.4.4}/tests/test_idioma.py +0 -0
  58. {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventory.py +0 -0
  59. {walopy-0.4.2 → walopy-0.4.4}/tests/test_inventory_new.py +0 -0
  60. {walopy-0.4.2 → walopy-0.4.4}/tests/test_kpi.py +0 -0
  61. {walopy-0.4.2 → walopy-0.4.4}/tests/test_kpi_grafica.py +0 -0
  62. {walopy-0.4.2 → walopy-0.4.4}/tests/test_mg1.py +0 -0
  63. {walopy-0.4.2 → walopy-0.4.4}/tests/test_mmc_estable.py +0 -0
  64. {walopy-0.4.2 → walopy-0.4.4}/tests/test_mrp.py +0 -0
  65. {walopy-0.4.2 → walopy-0.4.4}/tests/test_neh_flowshop.py +0 -0
  66. {walopy-0.4.2 → walopy-0.4.4}/tests/test_network.py +0 -0
  67. {walopy-0.4.2 → walopy-0.4.4}/tests/test_new_models.py +0 -0
  68. {walopy-0.4.2 → walopy-0.4.4}/tests/test_notas_release.py +0 -0
  69. {walopy-0.4.2 → walopy-0.4.4}/tests/test_operations.py +0 -0
  70. {walopy-0.4.2 → walopy-0.4.4}/tests/test_politicas_inventario.py +0 -0
  71. {walopy-0.4.2 → walopy-0.4.4}/tests/test_propiedades_diferenciales.py +0 -0
  72. {walopy-0.4.2 → walopy-0.4.4}/tests/test_queuing.py +0 -0
  73. {walopy-0.4.2 → walopy-0.4.4}/tests/test_readme.py +0 -0
  74. {walopy-0.4.2 → walopy-0.4.4}/tests/test_referencias_externas.py +0 -0
  75. {walopy-0.4.2 → walopy-0.4.4}/tests/test_regresion_validacion.py +0 -0
  76. {walopy-0.4.2 → walopy-0.4.4}/tests/test_reliability.py +0 -0
  77. {walopy-0.4.2 → walopy-0.4.4}/tests/test_safety_stock_curve.py +0 -0
  78. {walopy-0.4.2 → walopy-0.4.4}/tests/test_salida_identica.py +0 -0
  79. {walopy-0.4.2 → walopy-0.4.4}/tests/test_salida_por_idioma.py +0 -0
  80. {walopy-0.4.2 → walopy-0.4.4}/tests/test_scheduling.py +0 -0
  81. {walopy-0.4.2 → walopy-0.4.4}/tests/test_scorecard.py +0 -0
  82. {walopy-0.4.2 → walopy-0.4.4}/tests/test_trazabilidad.py +0 -0
  83. {walopy-0.4.2 → walopy-0.4.4}/tests/test_version.py +0 -0
  84. {walopy-0.4.2 → walopy-0.4.4}/tests/test_wagner_whitin_ceros.py +0 -0
  85. {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.2
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.2" # fuente única de la versión; pyproject.toml la lee con [tool.setuptools.dynamic]
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=500, seed=1)
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.0427
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
- Probabilidad de no tener faltante por ciclo de reposición ∈ (0, 1).
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). Por defecto 0.95.
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). Por defecto 0.95.
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 = sum(variances[nm] for nm in crit)
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 para calcular la disponibilidad.
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 para calcular la disponibilidad.
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
- avail = mtbf_val / (mtbf_val + mttr) if mttr is not None else None
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 para calcular la disponibilidad.
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
- avail = mtbf_val / (mtbf_val + mttr) if mttr is not None else None
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
- 3.0
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 = int(np.ceil(lam / mu)) + 1 # min c for stability
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
- 30.7232
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 = int(np.ceil(lam / mu)) + 1
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
- kwargs[k] = int(v) if k in int_cols else v
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
- row = r.to_frame().iloc[0].to_dict()
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.2
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
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 = wl.pert(PERT_ACTS)
168
+ r = _pert_red_de_prueba()
163
169
  assert r.probability(r.project_duration + 1) > r.probability(r.project_duration)
164
170
 
165
171