praca_magisterska/pytania/generate_q18_diagrams.py

147 lines
5.0 KiB
Python
Raw Normal View History

2026-03-04 21:45:02 +01:00
#!/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.')