mirror of
https://github.com/kuhyx/praca_magisterska.git
synced 2026-07-04 13:43:05 +02:00
147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate EOQ diagrams for PYTANIE 18: Zarządzanie zapasami w łańcuchu dostaw.
|
|
Monochrome, A4-printable PNGs (300 DPI).
|
|
"""
|
|
|
|
import matplotlib
|
|
matplotlib.use('Agg')
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import os
|
|
|
|
DPI = 300
|
|
BG = 'white'
|
|
LN = 'black'
|
|
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img')
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
|
|
def generate_sawtooth_inventory():
|
|
"""Sawtooth inventory diagram showing Q dropping to 0 across cycles,
|
|
with Q/2 average line and delivery markers."""
|
|
|
|
fig, ax = plt.subplots(figsize=(7, 3.5), dpi=DPI, facecolor=BG)
|
|
|
|
Q = 1000
|
|
n_cycles = 4
|
|
cycle_len = 1.0 # arbitrary time unit per cycle
|
|
|
|
for i in range(n_cycles):
|
|
t_start = i * cycle_len
|
|
t_end = (i + 1) * cycle_len
|
|
ax.plot([t_start, t_end], [Q, 0], color=LN, lw=2, solid_capstyle='round')
|
|
# Vertical jump at delivery (except the very first which starts at t=0)
|
|
if i > 0:
|
|
ax.plot([t_start, t_start], [0, Q], color=LN, lw=2,
|
|
linestyle='--', alpha=0.5)
|
|
|
|
# Average inventory line
|
|
ax.axhline(y=Q / 2, color='gray', linestyle='-.', lw=1.2, zorder=0)
|
|
ax.text(n_cycles * cycle_len * 1.02, Q / 2,
|
|
f'średni zapas = Q/2 = {Q // 2}',
|
|
va='center', ha='left', fontsize=8, color='gray')
|
|
|
|
# Delivery markers
|
|
for i in range(n_cycles):
|
|
ax.plot(i * cycle_len, Q, 'ko', markersize=5, zorder=5)
|
|
ax.annotate('dostawa', xy=(i * cycle_len, Q),
|
|
xytext=(i * cycle_len + 0.05, Q + 60),
|
|
fontsize=7, ha='left', color='black',
|
|
arrowprops=dict(arrowstyle='->', color='black', lw=0.8))
|
|
|
|
# Labels
|
|
ax.set_xlabel('Czas', fontsize=10)
|
|
ax.set_ylabel('Poziom zapasu', fontsize=10)
|
|
ax.set_title('Model EOQ — piłokształtny przebieg zapasu', fontsize=11,
|
|
fontweight='bold', pad=10)
|
|
|
|
ax.set_xlim(-0.1, n_cycles * cycle_len + 0.6)
|
|
ax.set_ylim(-50, Q + 150)
|
|
ax.set_yticks([0, Q // 2, Q])
|
|
ax.set_yticklabels(['0', 'Q/2', 'Q'], fontsize=9)
|
|
ax.set_xticks([i * cycle_len for i in range(n_cycles + 1)])
|
|
ax.set_xticklabels([f'T{i}' for i in range(n_cycles + 1)], fontsize=9)
|
|
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
|
|
fig.tight_layout()
|
|
path = os.path.join(OUTPUT_DIR, 'q18_eoq_sawtooth.png')
|
|
fig.savefig(path, dpi=DPI, bbox_inches='tight', facecolor=BG)
|
|
plt.close(fig)
|
|
print(f'Saved: {path}')
|
|
|
|
|
|
def generate_cost_curves():
|
|
"""EOQ cost curve: ordering cost, holding cost, and total cost
|
|
with optimum Q* marked."""
|
|
|
|
fig, ax = plt.subplots(figsize=(7, 4.5), dpi=DPI, facecolor=BG)
|
|
|
|
D = 10000
|
|
K = 100
|
|
h = 2
|
|
Q_star = np.sqrt(2 * K * D / h) # 1000
|
|
|
|
Q = np.linspace(200, 2500, 500)
|
|
ordering = K * D / Q
|
|
holding = h * Q / 2
|
|
total = ordering + holding
|
|
|
|
ax.plot(Q, ordering, color='black', lw=1.8, linestyle='--',
|
|
label='Koszt zamawiania K·D/Q')
|
|
ax.plot(Q, holding, color='black', lw=1.8, linestyle=':',
|
|
label='Koszt utrzymania h·Q/2')
|
|
ax.plot(Q, total, color='black', lw=2.5, linestyle='-',
|
|
label='Koszt całkowity TC(Q)')
|
|
|
|
# Mark optimum
|
|
TC_star = np.sqrt(2 * K * D * h)
|
|
ax.plot(Q_star, TC_star, 'ko', markersize=8, zorder=5)
|
|
ax.annotate(f'Q* = {int(Q_star)}\nTC* = {int(TC_star)} PLN',
|
|
xy=(Q_star, TC_star),
|
|
xytext=(Q_star + 300, TC_star + 600),
|
|
fontsize=9, fontweight='bold', ha='left',
|
|
arrowprops=dict(arrowstyle='->', color='black', lw=1.2),
|
|
bbox=dict(boxstyle='round,pad=0.3', facecolor='#F0F0F0',
|
|
edgecolor='black', lw=0.8))
|
|
|
|
# Vertical dashed line from Q* to x-axis
|
|
ax.plot([Q_star, Q_star], [0, TC_star], color='gray', linestyle='-.',
|
|
lw=0.8, zorder=0)
|
|
|
|
# Show that ordering = holding at Q*
|
|
cross_y = K * D / Q_star # = h * Q_star / 2 = 1000
|
|
ax.annotate('koszt zamawiania\n= koszt utrzymania',
|
|
xy=(Q_star, cross_y),
|
|
xytext=(Q_star - 500, cross_y + 800),
|
|
fontsize=7.5, ha='center', style='italic',
|
|
arrowprops=dict(arrowstyle='->', color='gray', lw=0.8),
|
|
color='gray')
|
|
|
|
ax.set_xlabel('Wielkość zamówienia Q [szt]', fontsize=10)
|
|
ax.set_ylabel('Koszt roczny [PLN]', fontsize=10)
|
|
ax.set_title('Model EOQ — krzywe kosztów', fontsize=11,
|
|
fontweight='bold', pad=10)
|
|
|
|
ax.legend(loc='upper right', fontsize=8.5, framealpha=0.9,
|
|
edgecolor='black')
|
|
ax.set_xlim(100, 2600)
|
|
ax.set_ylim(0, 5500)
|
|
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
|
|
fig.tight_layout()
|
|
path = os.path.join(OUTPUT_DIR, 'q18_eoq_cost_curves.png')
|
|
fig.savefig(path, dpi=DPI, bbox_inches='tight', facecolor=BG)
|
|
plt.close(fig)
|
|
print(f'Saved: {path}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
generate_sawtooth_inventory()
|
|
generate_cost_curves()
|
|
print('Done — all Q18 diagrams generated.')
|