walopy 0.2.2__tar.gz → 0.2.5__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.2.2/src/walopy.egg-info → walopy-0.2.5}/PKG-INFO +295 -4
- {walopy-0.2.2 → walopy-0.2.5}/README.md +294 -3
- {walopy-0.2.2 → walopy-0.2.5}/pyproject.toml +1 -1
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/__init__.py +46 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/advanced.py +207 -0
- walopy-0.2.5/src/walopy/inventory.py +1282 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/queuing.py +146 -0
- {walopy-0.2.2 → walopy-0.2.5/src/walopy.egg-info}/PKG-INFO +295 -4
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy.egg-info/SOURCES.txt +2 -0
- walopy-0.2.5/tests/test_inventory_new.py +304 -0
- walopy-0.2.5/tests/test_mg1.py +141 -0
- walopy-0.2.2/src/walopy/inventory.py +0 -407
- {walopy-0.2.2 → walopy-0.2.5}/LICENSE +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/setup.cfg +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/__main__.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/_utils.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/bottleneck.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/fitting.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/kpi.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/network.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/operations.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/plotting.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy/solver.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy.egg-info/dependency_links.txt +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy.egg-info/requires.txt +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/src/walopy.egg-info/top_level.txt +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_advanced.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_bottleneck.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_fitting.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_inventory.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_kpi.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_network.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_new_models.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_operations.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_queuing.py +0 -0
- {walopy-0.2.2 → walopy-0.2.5}/tests/test_solver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: walopy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Queuing theory, operations analysis, OEE, bottleneck analysis and KPI trees for Python
|
|
5
5
|
Author-email: LeoSanta15 <angelsanta1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -200,6 +200,49 @@ r = wl.kingman(**fit.to_model_kwargs())
|
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
203
|
+
### `mg1(lam, mu, cs2)` — M/G/1 (Pollaczek-Khinchine exacto)
|
|
204
|
+
|
|
205
|
+
Llegadas Poisson, servicio de **distribución arbitraria** parametrizado por su media (1/μ) y su variabilidad (cs²). La fórmula P-K es **exacta**, no una aproximación.
|
|
206
|
+
|
|
207
|
+
**Fórmula P-K:**
|
|
208
|
+
Wq = λ · E[S²] / (2 · (1 − ρ)), donde E[S²] = (1 + cs²) / μ²
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# Servicio con distribución triangular: calcular cs² primero
|
|
212
|
+
cs2 = wl.cv2_triangular(a=2, m=5, b=10) # a=mín, m=moda, b=máx
|
|
213
|
+
r = wl.mg1(lam=0.15, mu=1/5, cs2=cs2)
|
|
214
|
+
|
|
215
|
+
# Equivalencia con M/M/1 (cs²=1) y M/D/1 (cs²=0)
|
|
216
|
+
r_mm1 = wl.mg1(lam=3.0, mu=5.0, cs2=1.0) # idéntico a mm1(3, 5)
|
|
217
|
+
r_md1 = wl.mg1(lam=3.0, mu=5.0, cs2=0.0) # idéntico a md1(3, 5)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Helpers `cv2_*` — CV² por distribución
|
|
221
|
+
|
|
222
|
+
El CV² (varianza / media²) es el único parámetro de distribución que M/G/1 necesita:
|
|
223
|
+
|
|
224
|
+
| Función | Distribución | CV² |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `cv2_normal(mean, std)` | Normal | (σ/μ)² |
|
|
227
|
+
| `cv2_triangular(a, m, b)` | Triangular | fórmula exacta |
|
|
228
|
+
| `cv2_uniform(a, b)` | Uniforme | (b−a)² / (12·μ²) |
|
|
229
|
+
| `cv2_erlang(k)` | Erlang-k | 1/k |
|
|
230
|
+
| `cv2_gamma(shape)` | Gamma | 1/shape |
|
|
231
|
+
| `cv2_lognormal(mean, std)` | Lognormal | (σ/μ)² |
|
|
232
|
+
| `cv2_weibull(shape)` | Weibull | Γ(1+2/k)/Γ(1+1/k)²−1 |
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
# Servicio con distribución Weibull de forma 2 (rayleigh)
|
|
236
|
+
cs2 = wl.cv2_weibull(shape=2.0) # ≈ 0.273
|
|
237
|
+
r = wl.mg1(lam=1.0, mu=2.0, cs2=cs2)
|
|
238
|
+
print(r.Wq) # tiempo medio de espera en cola
|
|
239
|
+
|
|
240
|
+
# Servicio Erlang-3 (menos variable que exponencial)
|
|
241
|
+
r = wl.mg1(lam=3.0, mu=5.0, cs2=wl.cv2_erlang(3))
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
203
246
|
### `littles_law(*, L, lam, W)` — Ley de Little
|
|
204
247
|
|
|
205
248
|
Resuelve L = λ · W para la variable que falte. Exactamente uno de los tres parámetros debe ser `None`.
|
|
@@ -654,6 +697,204 @@ print(r_bajo.optimal_qty < 100) # True
|
|
|
654
697
|
|
|
655
698
|
---
|
|
656
699
|
|
|
700
|
+
### `ebq(demand_rate, setup_cost, holding_cost, production_rate)` — Lote Económico de Producción (EBQ/EPQ)
|
|
701
|
+
|
|
702
|
+
Extiende el EOQ al caso en que la producción y el consumo ocurren **simultáneamente** (tasa de producción finita P > D).
|
|
703
|
+
|
|
704
|
+
**Fórmula:**
|
|
705
|
+
```
|
|
706
|
+
Q* = √(2 · D · S / (h · (1 − D/P)))
|
|
707
|
+
Inventario máximo = Q* · (1 − D/P)
|
|
708
|
+
Inventario medio = Inventario máximo / 2
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
r = wl.ebq(
|
|
713
|
+
demand_rate=1000, # D: unidades/período
|
|
714
|
+
setup_cost=50, # S: costo fijo de setup por lote
|
|
715
|
+
holding_cost=2, # h: costo/unidad/período
|
|
716
|
+
production_rate=4000, # P: tasa de producción (debe ser > D)
|
|
717
|
+
)
|
|
718
|
+
print(r)
|
|
719
|
+
# EBQ (batch size) : 258.2 units
|
|
720
|
+
# Max inventory : 193.6
|
|
721
|
+
# Avg inventory : 96.82
|
|
722
|
+
# Order frequency : 3.873 runs/period
|
|
723
|
+
# Cycle time : 0.2582 periods
|
|
724
|
+
# Production time/cyc : 0.06455 periods
|
|
725
|
+
# Total cost : 193.6
|
|
726
|
+
# Holding cost : 96.82
|
|
727
|
+
# Setup cost : 96.82
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**`EBQResult` — atributos:**
|
|
731
|
+
|
|
732
|
+
| Atributo | Descripción |
|
|
733
|
+
|---|---|
|
|
734
|
+
| `ebq` | Lote óptimo Q* |
|
|
735
|
+
| `max_inventory` | Nivel máximo de inventario Q*(1−D/P) |
|
|
736
|
+
| `avg_inventory` | Inventario promedio = max/2 |
|
|
737
|
+
| `production_time` | Tiempo de producción por ciclo = Q*/P |
|
|
738
|
+
| `cycle_time` | Duración del ciclo = Q*/D |
|
|
739
|
+
| `total_cost` | Costo total mínimo |
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
### `eoq_multi(demand_rates, ordering_costs, holding_costs, *, names)` — EOQ multi-ítem independiente
|
|
744
|
+
|
|
745
|
+
Resuelve el EOQ para cada ítem por separado sin restricciones compartidas.
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
r = wl.eoq_multi(
|
|
749
|
+
demand_rates = [1000, 500, 800],
|
|
750
|
+
ordering_costs = [50, 30, 40 ],
|
|
751
|
+
holding_costs = [2, 1, 1.5],
|
|
752
|
+
names = ["A", "B", "C"],
|
|
753
|
+
)
|
|
754
|
+
print(r.total_cost) # suma de costos óptimos individuales
|
|
755
|
+
df = r.to_frame() # DataFrame con EOQ, costos y frecuencias por ítem
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
### `ebq_multi(demand_rates, setup_costs, holding_costs, production_rates, *, names)` — EBQ multi-ítem independiente
|
|
761
|
+
|
|
762
|
+
```python
|
|
763
|
+
r = wl.ebq_multi(
|
|
764
|
+
demand_rates = [1000, 500],
|
|
765
|
+
setup_costs = [50, 30 ],
|
|
766
|
+
holding_costs = [2, 1 ],
|
|
767
|
+
production_rates= [4000, 2000],
|
|
768
|
+
)
|
|
769
|
+
df = r.to_frame() # EBQ, inventario máximo/medio, costo por ítem
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
### `eoq_multi_constrained(demand_rates, ordering_costs, holding_costs, ...)` — EOQ multi-ítem con restricciones
|
|
775
|
+
|
|
776
|
+
Minimiza el costo total de inventario sujeto a **restricciones lineales** sobre las cantidades de pedido mediante **relajación Lagrangiana** (búsqueda binaria sobre el multiplicador).
|
|
777
|
+
|
|
778
|
+
**Restricciones soportadas:**
|
|
779
|
+
|
|
780
|
+
| Parámetro | Restricción |
|
|
781
|
+
|---|---|
|
|
782
|
+
| `budget`, `budget_unit_costs` | Σ(c_i · Q_i / 2) ≤ budget — valor promedio en inventario |
|
|
783
|
+
| `space`, `space_per_unit` | Σ(s_i · Q_i / 2) ≤ space — espacio promedio ocupado |
|
|
784
|
+
| `constraints` | Lista de `{"name", "weights", "bound"}` — Σ(a_i · Q_i) ≤ B |
|
|
785
|
+
|
|
786
|
+
```python
|
|
787
|
+
# Restricción de presupuesto
|
|
788
|
+
r = wl.eoq_multi_constrained(
|
|
789
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
790
|
+
budget=5000,
|
|
791
|
+
budget_unit_costs=[10, 8, 12], # costo unitario de compra
|
|
792
|
+
)
|
|
793
|
+
print(r.binding_constraints) # ["budget"] si la restricción está activa
|
|
794
|
+
print(r.lagrange_multipliers) # {"budget": λ}
|
|
795
|
+
df = r.to_frame() # Q*, holding cost, ordering cost por ítem
|
|
796
|
+
|
|
797
|
+
# Restricción de espacio
|
|
798
|
+
r2 = wl.eoq_multi_constrained(
|
|
799
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
800
|
+
space=200,
|
|
801
|
+
space_per_unit=[0.5, 0.3, 0.4],
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# Restricción genérica: suma total de unidades pedidas ≤ 400
|
|
805
|
+
r3 = wl.eoq_multi_constrained(
|
|
806
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
807
|
+
constraints=[{"name": "total_qty", "weights": [1, 1, 1], "bound": 400}],
|
|
808
|
+
)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**`ConstrainedMultiEOQResult` — atributos:**
|
|
812
|
+
|
|
813
|
+
| Atributo | Descripción |
|
|
814
|
+
|---|---|
|
|
815
|
+
| `items` | Lista de dicts por ítem: nombre, Q*, costos |
|
|
816
|
+
| `total_cost` | Costo total bajo las restricciones |
|
|
817
|
+
| `unconstrained_total_cost` | Costo sin restricciones (cota inferior) |
|
|
818
|
+
| `lagrange_multipliers` | Diccionario {nombre_restricción: λ} |
|
|
819
|
+
| `binding_constraints` | Nombres de restricciones activas (λ > 0) |
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
### `lot_for_lot(demands, setup_cost, holding_cost)` — Lote por Lote
|
|
824
|
+
|
|
825
|
+
Heurística de dimensionado dinámico: ordena **exactamente la demanda de cada período**. Minimiza el costo de almacenamiento (cero inventario en tránsito) a cambio de un setup por período.
|
|
826
|
+
|
|
827
|
+
```python
|
|
828
|
+
demands = [100, 80, 0, 120, 60] # demandas por período
|
|
829
|
+
r = wl.lot_for_lot(demands, setup_cost=200, holding_cost=1)
|
|
830
|
+
print(r.total_holding_cost) # 0.0 — sin inventario residual
|
|
831
|
+
print(r.n_orders) # 4 — un pedido por período con demanda > 0
|
|
832
|
+
print(r)
|
|
833
|
+
# Method : Lot-for-Lot
|
|
834
|
+
# Number of orders : 4
|
|
835
|
+
# Total cost : 800.0
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
### `silver_meal(demands, setup_cost, holding_cost)` — Silver-Meal
|
|
841
|
+
|
|
842
|
+
Heurística de dimensionado dinámico que **agrupa períodos** mientras el costo promedio por período decrece, reduciendo el número de setups.
|
|
843
|
+
|
|
844
|
+
```python
|
|
845
|
+
r = wl.silver_meal(demands, setup_cost=200, holding_cost=1)
|
|
846
|
+
print(r.total_cost <= wl.lot_for_lot(demands, 200, 1).total_cost) # True
|
|
847
|
+
df = r.to_frame() # pedidos: período, cantidad, períodos cubiertos
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**`LotSizingResult` — atributos (Lot-for-Lot y Silver-Meal):**
|
|
851
|
+
|
|
852
|
+
| Atributo | Descripción |
|
|
853
|
+
|---|---|
|
|
854
|
+
| `orders` | Lista de pedidos: período, cantidad, períodos cubiertos |
|
|
855
|
+
| `total_cost` | Costo total (setup + almacenamiento) |
|
|
856
|
+
| `total_setup_cost` | Costo total de setups |
|
|
857
|
+
| `total_holding_cost` | Costo total de almacenamiento |
|
|
858
|
+
| `n_orders` | Número de pedidos emitidos |
|
|
859
|
+
| `method` | Nombre de la heurística usada |
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
### `eoq_quantity_discount(demand_rate, ordering_cost, holding_cost_rate, price_breaks)` — EOQ con descuentos por cantidad
|
|
864
|
+
|
|
865
|
+
Evalúa todos los tramos de precio (all-units) y selecciona la cantidad que **minimiza el costo total anual** (compra + pedido + almacenamiento), ajustando el EOQ al tramo válido cuando cae fuera de rango.
|
|
866
|
+
|
|
867
|
+
```python
|
|
868
|
+
r = wl.eoq_quantity_discount(
|
|
869
|
+
demand_rate=1000,
|
|
870
|
+
ordering_cost=50,
|
|
871
|
+
holding_cost_rate=0.20, # 20% del precio unitario por año
|
|
872
|
+
price_breaks=[
|
|
873
|
+
(0, 10.00), # precio base
|
|
874
|
+
(500, 9.50), # ≥ 500 unidades → $9.50
|
|
875
|
+
(1000, 9.00), # ≥ 1000 unidades → $9.00
|
|
876
|
+
],
|
|
877
|
+
)
|
|
878
|
+
print(r.optimal_qty) # cantidad óptima seleccionada
|
|
879
|
+
print(r.unit_price) # precio en ese tramo
|
|
880
|
+
print(r.total_cost) # costo anual mínimo
|
|
881
|
+
df = r.to_frame() # todos los candidatos evaluados
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**`QuantityDiscountResult` — atributos:**
|
|
885
|
+
|
|
886
|
+
| Atributo | Descripción |
|
|
887
|
+
|---|---|
|
|
888
|
+
| `optimal_qty` | Cantidad óptima ajustada al tramo |
|
|
889
|
+
| `unit_price` | Precio unitario en el tramo seleccionado |
|
|
890
|
+
| `total_cost` | Costo total anual (compra + pedido + almacenamiento) |
|
|
891
|
+
| `purchase_cost` | Costo de compra anual D · precio |
|
|
892
|
+
| `ordering_cost_total` | Costo de pedido anual (D/Q) · K |
|
|
893
|
+
| `holding_cost_total` | Costo de almacenamiento anual (Q/2) · h · precio |
|
|
894
|
+
| `candidates` | Lista de todos los tramos evaluados |
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
657
898
|
## 8. Análisis de operaciones
|
|
658
899
|
|
|
659
900
|
### `oee(availability, performance, quality)` — OEE
|
|
@@ -762,6 +1003,56 @@ print(be.profit) # 5000.0 — beneficio con actual_units
|
|
|
762
1003
|
be.plot() # líneas de ingresos/costos con BEP y zona de beneficio
|
|
763
1004
|
```
|
|
764
1005
|
|
|
1006
|
+
### `break_even_multi(fixed_cost, prices, variable_costs, sales_mix)` — Multi-producto
|
|
1007
|
+
|
|
1008
|
+
Calcula el punto de equilibrio para una mezcla de varios productos usando la **contribución marginal ponderada (WACM)**.
|
|
1009
|
+
|
|
1010
|
+
**Fórmula:** WACM = Σ(CMᵢ × mixᵢ) / Σmixᵢ, luego BEP_total = FC / WACM
|
|
1011
|
+
|
|
1012
|
+
```python
|
|
1013
|
+
r = wl.break_even_multi(
|
|
1014
|
+
fixed_cost=120_000,
|
|
1015
|
+
prices=[50, 80, 120],
|
|
1016
|
+
variable_costs=[30, 50, 70],
|
|
1017
|
+
sales_mix=[3, 2, 1], # proporción de ventas
|
|
1018
|
+
)
|
|
1019
|
+
print(r.weighted_avg_cm) # WACM ≈ 36.67
|
|
1020
|
+
print(r.bep_units_total) # unidades totales en el punto de equilibrio
|
|
1021
|
+
print(r.bep_revenue_total) # ingresos totales en el punto de equilibrio
|
|
1022
|
+
|
|
1023
|
+
for item in r.items:
|
|
1024
|
+
print(item["Name"], item["BEP units"])
|
|
1025
|
+
|
|
1026
|
+
r.to_frame() # DataFrame con una fila por producto
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
| Atributo | Descripción |
|
|
1030
|
+
|---|---|
|
|
1031
|
+
| `weighted_avg_cm` | Contribución marginal ponderada |
|
|
1032
|
+
| `bep_units_total` | Unidades totales en el BEP |
|
|
1033
|
+
| `bep_revenue_total` | Ingresos totales en el BEP |
|
|
1034
|
+
| `items` | Lista de dicts por producto con `BEP units`, `BEP revenue` |
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
### `break_even_sales(fixed_cost, variable_cost_ratio, *, actual_revenue)` — Punto de equilibrio en ingresos
|
|
1039
|
+
|
|
1040
|
+
Calcula el BEP en términos de ingresos cuando los costos variables se expresan como porcentaje de las ventas.
|
|
1041
|
+
|
|
1042
|
+
**Fórmula:** BEP = FC / (1 − RCV)
|
|
1043
|
+
|
|
1044
|
+
```python
|
|
1045
|
+
r = wl.break_even_sales(
|
|
1046
|
+
fixed_cost=50_000,
|
|
1047
|
+
variable_cost_ratio=0.60, # 60% de los ingresos son costos variables
|
|
1048
|
+
actual_revenue=200_000,
|
|
1049
|
+
)
|
|
1050
|
+
print(r.bep_revenue) # 125 000 — ingresos en el BEP
|
|
1051
|
+
print(r.contribution_margin_ratio) # 0.40 — margen de contribución
|
|
1052
|
+
print(r.margin_of_safety_units) # 75 000 — margen de seguridad en ingresos
|
|
1053
|
+
print(r.margin_of_safety_pct) # 0.375 — porcentaje del margen de seguridad
|
|
1054
|
+
```
|
|
1055
|
+
|
|
765
1056
|
---
|
|
766
1057
|
|
|
767
1058
|
## 12. Árboles de KPI
|
|
@@ -1037,10 +1328,10 @@ Wq : 0.3 (avg wait time in queue)
|
|
|
1037
1328
|
|
|
1038
1329
|
| Módulo | Funciones y clases principales |
|
|
1039
1330
|
|---|---|
|
|
1040
|
-
| `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `littles_law`, `QueueResult` |
|
|
1041
|
-
| `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult` |
|
|
1331
|
+
| `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `mg1`, `littles_law`, `QueueResult`, `cv2_normal`, `cv2_triangular`, `cv2_uniform`, `cv2_erlang`, `cv2_gamma`, `cv2_lognormal`, `cv2_weibull` |
|
|
1332
|
+
| `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `break_even_multi`, `break_even_sales`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult`, `BreakEvenMultiResult` |
|
|
1042
1333
|
| `fitting` | `fit_from_data`, `FitResult` |
|
|
1043
|
-
| `inventory` | `eoq`, `reorder_point`, `newsvendor`, `EOQResult`, `ReorderResult`, `NewsvendorResult` |
|
|
1334
|
+
| `inventory` | `eoq`, `ebq`, `eoq_multi`, `ebq_multi`, `eoq_multi_constrained`, `lot_for_lot`, `silver_meal`, `eoq_quantity_discount`, `reorder_point`, `newsvendor`, `EOQResult`, `EBQResult`, `MultiItemResult`, `ConstrainedMultiEOQResult`, `LotSizingResult`, `QuantityDiscountResult`, `ReorderResult`, `NewsvendorResult` |
|
|
1044
1335
|
| `network` | `jackson_network`, `JacksonResult`, `StationMetrics` |
|
|
1045
1336
|
| `operations` | `oee`, `utilization_efficiency`, `unit_cost`, `OEEResult`, `UtilizationResult`, `UnitCostResult` |
|
|
1046
1337
|
| `bottleneck` | `bottleneck_analysis`, `BottleneckResult`, `StationResult` |
|
|
@@ -163,6 +163,49 @@ r = wl.kingman(**fit.to_model_kwargs())
|
|
|
163
163
|
|
|
164
164
|
---
|
|
165
165
|
|
|
166
|
+
### `mg1(lam, mu, cs2)` — M/G/1 (Pollaczek-Khinchine exacto)
|
|
167
|
+
|
|
168
|
+
Llegadas Poisson, servicio de **distribución arbitraria** parametrizado por su media (1/μ) y su variabilidad (cs²). La fórmula P-K es **exacta**, no una aproximación.
|
|
169
|
+
|
|
170
|
+
**Fórmula P-K:**
|
|
171
|
+
Wq = λ · E[S²] / (2 · (1 − ρ)), donde E[S²] = (1 + cs²) / μ²
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# Servicio con distribución triangular: calcular cs² primero
|
|
175
|
+
cs2 = wl.cv2_triangular(a=2, m=5, b=10) # a=mín, m=moda, b=máx
|
|
176
|
+
r = wl.mg1(lam=0.15, mu=1/5, cs2=cs2)
|
|
177
|
+
|
|
178
|
+
# Equivalencia con M/M/1 (cs²=1) y M/D/1 (cs²=0)
|
|
179
|
+
r_mm1 = wl.mg1(lam=3.0, mu=5.0, cs2=1.0) # idéntico a mm1(3, 5)
|
|
180
|
+
r_md1 = wl.mg1(lam=3.0, mu=5.0, cs2=0.0) # idéntico a md1(3, 5)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Helpers `cv2_*` — CV² por distribución
|
|
184
|
+
|
|
185
|
+
El CV² (varianza / media²) es el único parámetro de distribución que M/G/1 necesita:
|
|
186
|
+
|
|
187
|
+
| Función | Distribución | CV² |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `cv2_normal(mean, std)` | Normal | (σ/μ)² |
|
|
190
|
+
| `cv2_triangular(a, m, b)` | Triangular | fórmula exacta |
|
|
191
|
+
| `cv2_uniform(a, b)` | Uniforme | (b−a)² / (12·μ²) |
|
|
192
|
+
| `cv2_erlang(k)` | Erlang-k | 1/k |
|
|
193
|
+
| `cv2_gamma(shape)` | Gamma | 1/shape |
|
|
194
|
+
| `cv2_lognormal(mean, std)` | Lognormal | (σ/μ)² |
|
|
195
|
+
| `cv2_weibull(shape)` | Weibull | Γ(1+2/k)/Γ(1+1/k)²−1 |
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Servicio con distribución Weibull de forma 2 (rayleigh)
|
|
199
|
+
cs2 = wl.cv2_weibull(shape=2.0) # ≈ 0.273
|
|
200
|
+
r = wl.mg1(lam=1.0, mu=2.0, cs2=cs2)
|
|
201
|
+
print(r.Wq) # tiempo medio de espera en cola
|
|
202
|
+
|
|
203
|
+
# Servicio Erlang-3 (menos variable que exponencial)
|
|
204
|
+
r = wl.mg1(lam=3.0, mu=5.0, cs2=wl.cv2_erlang(3))
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
166
209
|
### `littles_law(*, L, lam, W)` — Ley de Little
|
|
167
210
|
|
|
168
211
|
Resuelve L = λ · W para la variable que falte. Exactamente uno de los tres parámetros debe ser `None`.
|
|
@@ -617,6 +660,204 @@ print(r_bajo.optimal_qty < 100) # True
|
|
|
617
660
|
|
|
618
661
|
---
|
|
619
662
|
|
|
663
|
+
### `ebq(demand_rate, setup_cost, holding_cost, production_rate)` — Lote Económico de Producción (EBQ/EPQ)
|
|
664
|
+
|
|
665
|
+
Extiende el EOQ al caso en que la producción y el consumo ocurren **simultáneamente** (tasa de producción finita P > D).
|
|
666
|
+
|
|
667
|
+
**Fórmula:**
|
|
668
|
+
```
|
|
669
|
+
Q* = √(2 · D · S / (h · (1 − D/P)))
|
|
670
|
+
Inventario máximo = Q* · (1 − D/P)
|
|
671
|
+
Inventario medio = Inventario máximo / 2
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
```python
|
|
675
|
+
r = wl.ebq(
|
|
676
|
+
demand_rate=1000, # D: unidades/período
|
|
677
|
+
setup_cost=50, # S: costo fijo de setup por lote
|
|
678
|
+
holding_cost=2, # h: costo/unidad/período
|
|
679
|
+
production_rate=4000, # P: tasa de producción (debe ser > D)
|
|
680
|
+
)
|
|
681
|
+
print(r)
|
|
682
|
+
# EBQ (batch size) : 258.2 units
|
|
683
|
+
# Max inventory : 193.6
|
|
684
|
+
# Avg inventory : 96.82
|
|
685
|
+
# Order frequency : 3.873 runs/period
|
|
686
|
+
# Cycle time : 0.2582 periods
|
|
687
|
+
# Production time/cyc : 0.06455 periods
|
|
688
|
+
# Total cost : 193.6
|
|
689
|
+
# Holding cost : 96.82
|
|
690
|
+
# Setup cost : 96.82
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**`EBQResult` — atributos:**
|
|
694
|
+
|
|
695
|
+
| Atributo | Descripción |
|
|
696
|
+
|---|---|
|
|
697
|
+
| `ebq` | Lote óptimo Q* |
|
|
698
|
+
| `max_inventory` | Nivel máximo de inventario Q*(1−D/P) |
|
|
699
|
+
| `avg_inventory` | Inventario promedio = max/2 |
|
|
700
|
+
| `production_time` | Tiempo de producción por ciclo = Q*/P |
|
|
701
|
+
| `cycle_time` | Duración del ciclo = Q*/D |
|
|
702
|
+
| `total_cost` | Costo total mínimo |
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
### `eoq_multi(demand_rates, ordering_costs, holding_costs, *, names)` — EOQ multi-ítem independiente
|
|
707
|
+
|
|
708
|
+
Resuelve el EOQ para cada ítem por separado sin restricciones compartidas.
|
|
709
|
+
|
|
710
|
+
```python
|
|
711
|
+
r = wl.eoq_multi(
|
|
712
|
+
demand_rates = [1000, 500, 800],
|
|
713
|
+
ordering_costs = [50, 30, 40 ],
|
|
714
|
+
holding_costs = [2, 1, 1.5],
|
|
715
|
+
names = ["A", "B", "C"],
|
|
716
|
+
)
|
|
717
|
+
print(r.total_cost) # suma de costos óptimos individuales
|
|
718
|
+
df = r.to_frame() # DataFrame con EOQ, costos y frecuencias por ítem
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
### `ebq_multi(demand_rates, setup_costs, holding_costs, production_rates, *, names)` — EBQ multi-ítem independiente
|
|
724
|
+
|
|
725
|
+
```python
|
|
726
|
+
r = wl.ebq_multi(
|
|
727
|
+
demand_rates = [1000, 500],
|
|
728
|
+
setup_costs = [50, 30 ],
|
|
729
|
+
holding_costs = [2, 1 ],
|
|
730
|
+
production_rates= [4000, 2000],
|
|
731
|
+
)
|
|
732
|
+
df = r.to_frame() # EBQ, inventario máximo/medio, costo por ítem
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
### `eoq_multi_constrained(demand_rates, ordering_costs, holding_costs, ...)` — EOQ multi-ítem con restricciones
|
|
738
|
+
|
|
739
|
+
Minimiza el costo total de inventario sujeto a **restricciones lineales** sobre las cantidades de pedido mediante **relajación Lagrangiana** (búsqueda binaria sobre el multiplicador).
|
|
740
|
+
|
|
741
|
+
**Restricciones soportadas:**
|
|
742
|
+
|
|
743
|
+
| Parámetro | Restricción |
|
|
744
|
+
|---|---|
|
|
745
|
+
| `budget`, `budget_unit_costs` | Σ(c_i · Q_i / 2) ≤ budget — valor promedio en inventario |
|
|
746
|
+
| `space`, `space_per_unit` | Σ(s_i · Q_i / 2) ≤ space — espacio promedio ocupado |
|
|
747
|
+
| `constraints` | Lista de `{"name", "weights", "bound"}` — Σ(a_i · Q_i) ≤ B |
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
# Restricción de presupuesto
|
|
751
|
+
r = wl.eoq_multi_constrained(
|
|
752
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
753
|
+
budget=5000,
|
|
754
|
+
budget_unit_costs=[10, 8, 12], # costo unitario de compra
|
|
755
|
+
)
|
|
756
|
+
print(r.binding_constraints) # ["budget"] si la restricción está activa
|
|
757
|
+
print(r.lagrange_multipliers) # {"budget": λ}
|
|
758
|
+
df = r.to_frame() # Q*, holding cost, ordering cost por ítem
|
|
759
|
+
|
|
760
|
+
# Restricción de espacio
|
|
761
|
+
r2 = wl.eoq_multi_constrained(
|
|
762
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
763
|
+
space=200,
|
|
764
|
+
space_per_unit=[0.5, 0.3, 0.4],
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Restricción genérica: suma total de unidades pedidas ≤ 400
|
|
768
|
+
r3 = wl.eoq_multi_constrained(
|
|
769
|
+
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
|
|
770
|
+
constraints=[{"name": "total_qty", "weights": [1, 1, 1], "bound": 400}],
|
|
771
|
+
)
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**`ConstrainedMultiEOQResult` — atributos:**
|
|
775
|
+
|
|
776
|
+
| Atributo | Descripción |
|
|
777
|
+
|---|---|
|
|
778
|
+
| `items` | Lista de dicts por ítem: nombre, Q*, costos |
|
|
779
|
+
| `total_cost` | Costo total bajo las restricciones |
|
|
780
|
+
| `unconstrained_total_cost` | Costo sin restricciones (cota inferior) |
|
|
781
|
+
| `lagrange_multipliers` | Diccionario {nombre_restricción: λ} |
|
|
782
|
+
| `binding_constraints` | Nombres de restricciones activas (λ > 0) |
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
### `lot_for_lot(demands, setup_cost, holding_cost)` — Lote por Lote
|
|
787
|
+
|
|
788
|
+
Heurística de dimensionado dinámico: ordena **exactamente la demanda de cada período**. Minimiza el costo de almacenamiento (cero inventario en tránsito) a cambio de un setup por período.
|
|
789
|
+
|
|
790
|
+
```python
|
|
791
|
+
demands = [100, 80, 0, 120, 60] # demandas por período
|
|
792
|
+
r = wl.lot_for_lot(demands, setup_cost=200, holding_cost=1)
|
|
793
|
+
print(r.total_holding_cost) # 0.0 — sin inventario residual
|
|
794
|
+
print(r.n_orders) # 4 — un pedido por período con demanda > 0
|
|
795
|
+
print(r)
|
|
796
|
+
# Method : Lot-for-Lot
|
|
797
|
+
# Number of orders : 4
|
|
798
|
+
# Total cost : 800.0
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
### `silver_meal(demands, setup_cost, holding_cost)` — Silver-Meal
|
|
804
|
+
|
|
805
|
+
Heurística de dimensionado dinámico que **agrupa períodos** mientras el costo promedio por período decrece, reduciendo el número de setups.
|
|
806
|
+
|
|
807
|
+
```python
|
|
808
|
+
r = wl.silver_meal(demands, setup_cost=200, holding_cost=1)
|
|
809
|
+
print(r.total_cost <= wl.lot_for_lot(demands, 200, 1).total_cost) # True
|
|
810
|
+
df = r.to_frame() # pedidos: período, cantidad, períodos cubiertos
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**`LotSizingResult` — atributos (Lot-for-Lot y Silver-Meal):**
|
|
814
|
+
|
|
815
|
+
| Atributo | Descripción |
|
|
816
|
+
|---|---|
|
|
817
|
+
| `orders` | Lista de pedidos: período, cantidad, períodos cubiertos |
|
|
818
|
+
| `total_cost` | Costo total (setup + almacenamiento) |
|
|
819
|
+
| `total_setup_cost` | Costo total de setups |
|
|
820
|
+
| `total_holding_cost` | Costo total de almacenamiento |
|
|
821
|
+
| `n_orders` | Número de pedidos emitidos |
|
|
822
|
+
| `method` | Nombre de la heurística usada |
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
### `eoq_quantity_discount(demand_rate, ordering_cost, holding_cost_rate, price_breaks)` — EOQ con descuentos por cantidad
|
|
827
|
+
|
|
828
|
+
Evalúa todos los tramos de precio (all-units) y selecciona la cantidad que **minimiza el costo total anual** (compra + pedido + almacenamiento), ajustando el EOQ al tramo válido cuando cae fuera de rango.
|
|
829
|
+
|
|
830
|
+
```python
|
|
831
|
+
r = wl.eoq_quantity_discount(
|
|
832
|
+
demand_rate=1000,
|
|
833
|
+
ordering_cost=50,
|
|
834
|
+
holding_cost_rate=0.20, # 20% del precio unitario por año
|
|
835
|
+
price_breaks=[
|
|
836
|
+
(0, 10.00), # precio base
|
|
837
|
+
(500, 9.50), # ≥ 500 unidades → $9.50
|
|
838
|
+
(1000, 9.00), # ≥ 1000 unidades → $9.00
|
|
839
|
+
],
|
|
840
|
+
)
|
|
841
|
+
print(r.optimal_qty) # cantidad óptima seleccionada
|
|
842
|
+
print(r.unit_price) # precio en ese tramo
|
|
843
|
+
print(r.total_cost) # costo anual mínimo
|
|
844
|
+
df = r.to_frame() # todos los candidatos evaluados
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**`QuantityDiscountResult` — atributos:**
|
|
848
|
+
|
|
849
|
+
| Atributo | Descripción |
|
|
850
|
+
|---|---|
|
|
851
|
+
| `optimal_qty` | Cantidad óptima ajustada al tramo |
|
|
852
|
+
| `unit_price` | Precio unitario en el tramo seleccionado |
|
|
853
|
+
| `total_cost` | Costo total anual (compra + pedido + almacenamiento) |
|
|
854
|
+
| `purchase_cost` | Costo de compra anual D · precio |
|
|
855
|
+
| `ordering_cost_total` | Costo de pedido anual (D/Q) · K |
|
|
856
|
+
| `holding_cost_total` | Costo de almacenamiento anual (Q/2) · h · precio |
|
|
857
|
+
| `candidates` | Lista de todos los tramos evaluados |
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
620
861
|
## 8. Análisis de operaciones
|
|
621
862
|
|
|
622
863
|
### `oee(availability, performance, quality)` — OEE
|
|
@@ -725,6 +966,56 @@ print(be.profit) # 5000.0 — beneficio con actual_units
|
|
|
725
966
|
be.plot() # líneas de ingresos/costos con BEP y zona de beneficio
|
|
726
967
|
```
|
|
727
968
|
|
|
969
|
+
### `break_even_multi(fixed_cost, prices, variable_costs, sales_mix)` — Multi-producto
|
|
970
|
+
|
|
971
|
+
Calcula el punto de equilibrio para una mezcla de varios productos usando la **contribución marginal ponderada (WACM)**.
|
|
972
|
+
|
|
973
|
+
**Fórmula:** WACM = Σ(CMᵢ × mixᵢ) / Σmixᵢ, luego BEP_total = FC / WACM
|
|
974
|
+
|
|
975
|
+
```python
|
|
976
|
+
r = wl.break_even_multi(
|
|
977
|
+
fixed_cost=120_000,
|
|
978
|
+
prices=[50, 80, 120],
|
|
979
|
+
variable_costs=[30, 50, 70],
|
|
980
|
+
sales_mix=[3, 2, 1], # proporción de ventas
|
|
981
|
+
)
|
|
982
|
+
print(r.weighted_avg_cm) # WACM ≈ 36.67
|
|
983
|
+
print(r.bep_units_total) # unidades totales en el punto de equilibrio
|
|
984
|
+
print(r.bep_revenue_total) # ingresos totales en el punto de equilibrio
|
|
985
|
+
|
|
986
|
+
for item in r.items:
|
|
987
|
+
print(item["Name"], item["BEP units"])
|
|
988
|
+
|
|
989
|
+
r.to_frame() # DataFrame con una fila por producto
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
| Atributo | Descripción |
|
|
993
|
+
|---|---|
|
|
994
|
+
| `weighted_avg_cm` | Contribución marginal ponderada |
|
|
995
|
+
| `bep_units_total` | Unidades totales en el BEP |
|
|
996
|
+
| `bep_revenue_total` | Ingresos totales en el BEP |
|
|
997
|
+
| `items` | Lista de dicts por producto con `BEP units`, `BEP revenue` |
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
### `break_even_sales(fixed_cost, variable_cost_ratio, *, actual_revenue)` — Punto de equilibrio en ingresos
|
|
1002
|
+
|
|
1003
|
+
Calcula el BEP en términos de ingresos cuando los costos variables se expresan como porcentaje de las ventas.
|
|
1004
|
+
|
|
1005
|
+
**Fórmula:** BEP = FC / (1 − RCV)
|
|
1006
|
+
|
|
1007
|
+
```python
|
|
1008
|
+
r = wl.break_even_sales(
|
|
1009
|
+
fixed_cost=50_000,
|
|
1010
|
+
variable_cost_ratio=0.60, # 60% de los ingresos son costos variables
|
|
1011
|
+
actual_revenue=200_000,
|
|
1012
|
+
)
|
|
1013
|
+
print(r.bep_revenue) # 125 000 — ingresos en el BEP
|
|
1014
|
+
print(r.contribution_margin_ratio) # 0.40 — margen de contribución
|
|
1015
|
+
print(r.margin_of_safety_units) # 75 000 — margen de seguridad en ingresos
|
|
1016
|
+
print(r.margin_of_safety_pct) # 0.375 — porcentaje del margen de seguridad
|
|
1017
|
+
```
|
|
1018
|
+
|
|
728
1019
|
---
|
|
729
1020
|
|
|
730
1021
|
## 12. Árboles de KPI
|
|
@@ -1000,10 +1291,10 @@ Wq : 0.3 (avg wait time in queue)
|
|
|
1000
1291
|
|
|
1001
1292
|
| Módulo | Funciones y clases principales |
|
|
1002
1293
|
|---|---|
|
|
1003
|
-
| `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `littles_law`, `QueueResult` |
|
|
1004
|
-
| `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult` |
|
|
1294
|
+
| `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `mg1`, `littles_law`, `QueueResult`, `cv2_normal`, `cv2_triangular`, `cv2_uniform`, `cv2_erlang`, `cv2_gamma`, `cv2_lognormal`, `cv2_weibull` |
|
|
1295
|
+
| `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `break_even_multi`, `break_even_sales`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult`, `BreakEvenMultiResult` |
|
|
1005
1296
|
| `fitting` | `fit_from_data`, `FitResult` |
|
|
1006
|
-
| `inventory` | `eoq`, `reorder_point`, `newsvendor`, `EOQResult`, `ReorderResult`, `NewsvendorResult` |
|
|
1297
|
+
| `inventory` | `eoq`, `ebq`, `eoq_multi`, `ebq_multi`, `eoq_multi_constrained`, `lot_for_lot`, `silver_meal`, `eoq_quantity_discount`, `reorder_point`, `newsvendor`, `EOQResult`, `EBQResult`, `MultiItemResult`, `ConstrainedMultiEOQResult`, `LotSizingResult`, `QuantityDiscountResult`, `ReorderResult`, `NewsvendorResult` |
|
|
1007
1298
|
| `network` | `jackson_network`, `JacksonResult`, `StationMetrics` |
|
|
1008
1299
|
| `operations` | `oee`, `utilization_efficiency`, `unit_cost`, `OEEResult`, `UtilizationResult`, `UnitCostResult` |
|
|
1009
1300
|
| `bottleneck` | `bottleneck_analysis`, `BottleneckResult`, `StationResult` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "walopy"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.5"
|
|
8
8
|
description = "Queuing theory, operations analysis, OEE, bottleneck analysis and KPI trees for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|